fix(models): restore disabled custom providers

This commit is contained in:
matevip 2026-08-19 23:16:28 -04:00
parent aecc619bef
commit 281ea53551
4 changed files with 68 additions and 7 deletions

View File

@ -175,7 +175,11 @@ public class ModelProviderService {
throw new MateClawException("err.llm.provider_id_invalid",
"Provider id 仅允许字母/数字及 . _ -(不允许斜杠或空格),首字符必须是字母或数字,长度 1-64: " + request.getId());
}
if (modelProviderMapper.selectById(request.getId()) != null) {
ModelProviderEntity existing = modelProviderMapper.selectById(request.getId());
if (existing != null && canRestoreCustomProvider(existing)) {
return restoreCustomProvider(existing, request);
}
if (existing != null) {
throw new MateClawException("err.llm.provider_exists", "Provider 已存在: " + request.getId());
}
ModelProtocol protocol = ModelProtocol.resolve(request.getProtocol(), request.getChatModel());
@ -211,6 +215,32 @@ public class ModelProviderService {
return toProviderInfo(provider, modelConfigService.listModelsByProvider(request.getId()));
}
private boolean canRestoreCustomProvider(ModelProviderEntity provider) {
return Boolean.TRUE.equals(provider.getIsCustom()) && !Boolean.TRUE.equals(provider.getEnabled());
}
private ProviderInfoDTO restoreCustomProvider(ModelProviderEntity provider, CreateCustomProviderRequest request) {
ModelProtocol protocol = ModelProtocol.resolve(request.getProtocol(), request.getChatModel());
provider.setName(request.getName());
provider.setApiKeyPrefix(request.getApiKeyPrefix());
provider.setChatModel(protocol.getChatModelClass());
provider.setBaseUrl(request.getDefaultBaseUrl());
provider.setIsCustom(true);
provider.setIsLocal(false);
provider.setEnabled(true);
provider.setSupportModelDiscovery(protocol.supportsSelfConfiguredDiscovery());
provider.setRequireApiKey(request.getRequireApiKey() == null || Boolean.TRUE.equals(request.getRequireApiKey()));
modelProviderMapper.updateById(provider);
if (request.getModels() != null) {
for (ModelInfoDTO model : request.getModels()) {
modelConfigService.addModelToProvider(request.getId(), model.getId(), model.getName(), false);
}
}
tryAutoActivateModel(request.getId(), provider);
eventPublisher.publishEvent(new ModelConfigChangedEvent("provider-enabled"));
return toProviderInfo(provider, modelConfigService.listModelsByProvider(request.getId()));
}
public void deleteCustomProvider(String providerId) {
ModelProviderEntity provider = getProvider(providerId);
if (!Boolean.TRUE.equals(provider.getIsCustom())) {

View File

@ -14,6 +14,7 @@ import vip.mate.llm.failover.AvailableProviderPool;
import vip.mate.llm.failover.ProviderHealthProperties;
import vip.mate.llm.failover.ProviderHealthTracker;
import vip.mate.llm.failover.ProviderInitProbe;
import vip.mate.llm.model.CreateCustomProviderRequest;
import vip.mate.llm.model.EnableResult;
import vip.mate.llm.model.ModelConfigEntity;
import vip.mate.llm.model.ModelProviderEntity;
@ -112,6 +113,36 @@ class ModelProviderServiceEnableTest {
verify(eventPublisher, never()).publishEvent(any());
}
@Test
@DisplayName("createCustomProvider with an existing disabled custom row re-enables and updates it")
void createCustomProviderReEnablesDisabledCustomProvider() {
ModelProviderEntity existing = providerEntity("custom-openai", false);
existing.setIsCustom(true);
existing.setName("Old name");
when(providerMapper.selectById("custom-openai")).thenReturn(existing);
when(modelConfigService.listModelsByProvider("custom-openai")).thenReturn(new ArrayList<>());
CreateCustomProviderRequest request = new CreateCustomProviderRequest();
request.setId("custom-openai");
request.setName("New name");
request.setDefaultBaseUrl("https://new.example.com/v1");
request.setApiKeyPrefix("mk-");
request.setProtocol("openai-compatible");
request.setRequireApiKey(true);
service.createCustomProvider(request);
assertTrue(existing.getEnabled(), "disabled custom provider should be visible again");
assertEquals("New name", existing.getName());
assertEquals("https://new.example.com/v1", existing.getBaseUrl());
assertEquals("mk-", existing.getApiKeyPrefix());
verify(providerMapper).updateById(existing);
verify(providerMapper, never()).insert(any(ModelProviderEntity.class));
ArgumentCaptor<ModelConfigChangedEvent> evtCap = ArgumentCaptor.forClass(ModelConfigChangedEvent.class);
verify(eventPublisher).publishEvent(evtCap.capture());
assertEquals("provider-enabled", evtCap.getValue().reason());
}
@Test
@DisplayName("setEnabled(false) when provider's model is current default: auto-switches and reports new")
void disableSwitchesDefault() {

View File

@ -1137,15 +1137,15 @@ export default {
livenessUnprobedTooltip: 'Checking availability after startup; will refresh shortly',
livenessUnconfigured: 'Needs setup',
// RFC-074 PR-2: enable / disable / drawer
enableProviderCta: '+ Enable Provider',
enableProviderCta: '+ Enable / Restore Provider',
addCustomProvider: '+ Custom',
enable: 'Enable',
disable: 'Disable',
alreadyEnabled: 'Enabled',
disableConfirm: 'Disable provider "{name}"? It will disappear from the dropdown but its config is kept — you can re-enable any time.',
defaultSwitchedToast: 'Default model switched to {provider} / {model}',
addProviderDrawerTitle: 'Add Provider',
addProviderDrawerSubtitle: 'Once enabled the provider appears in the model dropdown. Cloud providers still need an API key after enabling.',
addProviderDrawerTitle: 'Enable / Restore Provider',
addProviderDrawerSubtitle: 'Disabled providers can be restored here. Once enabled, the provider appears in the model dropdown. Cloud providers may still need an API key.',
catalogEmpty: 'Catalog is empty',
catalogSearchPlaceholder: 'Search providers…',
catalogSearchEmpty: 'No providers match “{query}”',

View File

@ -982,15 +982,15 @@ export default {
livenessUnprobedTooltip: '启动后正在检测可用性,稍候自动更新',
livenessUnconfigured: '需要配置',
// RFC-074 PR-2: enable / disable / drawer
enableProviderCta: '+ 启用提供商',
enableProviderCta: '+ 启用/恢复提供商',
addCustomProvider: '+ 自定义提供商',
enable: '启用',
disable: '禁用',
alreadyEnabled: '已启用',
disableConfirm: '确定禁用提供商"{name}"?它将从下拉中消失,但配置保留,可随时再启用。',
defaultSwitchedToast: '已将默认模型切换到 {provider} / {model}',
addProviderDrawerTitle: '添加提供商',
addProviderDrawerSubtitle: '启用后会出现在模型下拉中。如果是云端提供商,启用后还需要填 API Key。',
addProviderDrawerTitle: '启用/恢复提供商',
addProviderDrawerSubtitle: '禁用后的提供商可在这里恢复;启用后会出现在模型下拉中。云端提供商可能还需要填 API Key。',
catalogEmpty: '目录为空',
catalogSearchPlaceholder: '搜索提供商…',
catalogSearchEmpty: '没有匹配“{query}”的提供商',