diff --git a/docs/architecture/baselines/renderer-application-boundaries-baseline.json b/docs/architecture/baselines/renderer-application-boundaries-baseline.json index 17006db5e4..2abd4374ca 100644 --- a/docs/architecture/baselines/renderer-application-boundaries-baseline.json +++ b/docs/architecture/baselines/renderer-application-boundaries-baseline.json @@ -152,7 +152,11 @@ "specifier": "@/components/settings/ModelConfigDialog.vue" }, { - "file": "src/renderer/settings/components/AddCustomProviderDialog.vue", + "file": "src/renderer/settings/components/AddProviderFlow.vue", + "specifier": "@/stores/modelStore" + }, + { + "file": "src/renderer/settings/components/AddProviderFlow.vue", "specifier": "@/stores/providerStore" }, { @@ -459,6 +463,18 @@ "file": "src/renderer/settings/components/ProviderApiConfig.vue", "specifier": "@/stores/modelCheck" }, + { + "file": "src/renderer/settings/components/ProviderCatalog.vue", + "specifier": "@/components/icons/ModelIcon.vue" + }, + { + "file": "src/renderer/settings/components/ProviderCatalog.vue", + "specifier": "@/stores/providerStore" + }, + { + "file": "src/renderer/settings/components/ProviderCatalog.vue", + "specifier": "@/stores/theme" + }, { "file": "src/renderer/settings/components/ProviderConfigImportDialog.vue", "specifier": "@/lib/utils" @@ -483,6 +499,10 @@ "file": "src/renderer/settings/components/ProviderModelList.vue", "specifier": "@/stores/uiSettingsStore" }, + { + "file": "src/renderer/settings/components/ProviderSettingsShell.vue", + "specifier": "@/stores/providerStore" + }, { "file": "src/renderer/settings/components/SettingsOverview.vue", "specifier": "@/stores/modelStore" @@ -532,5 +552,5 @@ "specifier": "@/i18n/bootstrap" } ], - "settingsToChatAppImportCount": 120 + "settingsToChatAppImportCount": 125 } diff --git a/src/main/app/settingsRoutes.ts b/src/main/app/settingsRoutes.ts index 148152eb9b..e763e3d2c5 100644 --- a/src/main/app/settingsRoutes.ts +++ b/src/main/app/settingsRoutes.ts @@ -65,6 +65,8 @@ export function createAppSettingsRoutes(deps: { read('artifact_think_collapse') read('providerOrder') read('providerTimestamps') + read('configuredProviders') + read('providerHealth') read('sidebar_group_mode') read('input_enabledMcpTools') return values diff --git a/src/main/provider/index.ts b/src/main/provider/index.ts index a2455655a5..773f2f9c03 100644 --- a/src/main/provider/index.ts +++ b/src/main/provider/index.ts @@ -922,6 +922,57 @@ export class ProviderRuntime return provider.getKeyStatus() } + /** + * Validates a draft provider configuration and loads its model catalog in one + * operation, without persisting the provider or toggling any enable flag. + * Used by the add-provider "Connect and load models" flow. + */ + async validateDraft(draft: LLM_PROVIDER): Promise<{ + isOk: boolean + errorMsg: string | null + models: MODEL_META[] + }> { + let instance: BaseLLMProvider | undefined + try { + // enable:false keeps the base-provider constructor from kicking off its + // background init fetch; check() and fetchModels() below are explicit. + instance = this.providerInstanceManager.createDraftInstance({ ...draft, enable: false }) + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error) + return { isOk: false, errorMsg: errorMessage, models: [] } + } + if (!instance) { + return { isOk: false, errorMsg: `Unsupported provider type: ${draft.apiType}`, models: [] } + } + + try { + const checkResult = await instance.check() + if (!checkResult.isOk) { + return { ...checkResult, models: [] } + } + + const models = await instance.fetchModels({ suppressErrors: false }) + return { isOk: true, errorMsg: null, models } + } catch (error) { + // fetchModels with suppressErrors:false rethrows before any persisted + // write, so there is no partial catalog to clean up. Clearing here would + // erase an existing provider's saved models if draft.id matches it. + const errorMessage = error instanceof Error ? error.message : String(error) + return { isOk: false, errorMsg: errorMessage, models: [] } + } finally { + if ( + 'cleanup' in instance && + typeof (instance as { cleanup?: unknown }).cleanup === 'function' + ) { + try { + ;(instance as unknown as { cleanup: () => void }).cleanup() + } catch (error) { + console.error(`Failed to clean up draft provider instance ${draft.id}:`, error) + } + } + } + } + private getEnabledProviderIdsUsingProviderDb(): string[] { return this.providerInstanceManager .getProviders() diff --git a/src/main/provider/managers/providerInstanceManager.ts b/src/main/provider/managers/providerInstanceManager.ts index 006c657356..28a97fc4e7 100644 --- a/src/main/provider/managers/providerInstanceManager.ts +++ b/src/main/provider/managers/providerInstanceManager.ts @@ -315,6 +315,16 @@ export class ProviderInstanceManager { } } + /** + * Creates a transient instance for draft validation. The instance is not + * registered in the runtime maps, so validating a draft never touches the + * persisted provider list or an existing provider's live instance. + */ + createDraftInstance(draft: LLM_PROVIDER): BaseLLMProvider | undefined { + if (this.closed) throw new Error('[Provider] Runtime is closed') + return this.createProviderInstance(draft) + } + /** * Creates a provider instance while preserving backward compatibility. * Lookup order MUST remain id -> apiType so that legacy configs lacking ids continue to work. diff --git a/src/main/provider/routes.ts b/src/main/provider/routes.ts index 1f49cba051..1667c2597d 100644 --- a/src/main/provider/routes.ts +++ b/src/main/provider/routes.ts @@ -39,6 +39,7 @@ import { oauthXaiGrokLogoutRoute, oauthXaiGrokStartDeviceLoginRoute, providersAddRoute, + providersValidateDraftRoute, providersGetAcpProcessConfigOptionsRoute, providersGetEmbeddingDimensionsRoute, providersGetKeyStatusRoute, @@ -295,6 +296,22 @@ export function createProviderRoutes(deps: { return result } ], + [ + providersValidateDraftRoute.name, + async (rawInput) => { + const input = providersValidateDraftRoute.input.parse(rawInput) + const result = await providerRuntime.validateDraft(input.provider) + return providersValidateDraftRoute.output.parse({ + isOk: result.isOk, + errorMsg: result.errorMsg, + models: result.models.map((model) => ({ + id: model.id, + name: model.name, + ...(model.type ? { type: model.type } : {}) + })) + }) + } + ], [ providersRemoveRoute.name, async (rawInput, context) => { diff --git a/src/renderer/api/ProviderClient.ts b/src/renderer/api/ProviderClient.ts index 2388cf7cda..139a8a1ed0 100644 --- a/src/renderer/api/ProviderClient.ts +++ b/src/renderer/api/ProviderClient.ts @@ -30,6 +30,7 @@ import { providersSyncModelScopeMcpServersRoute, providersTestConnectionRoute, providersUpdateRoute, + providersValidateDraftRoute, providersUpdateRateLimitRoute, providersWarmupAcpProcessRoute } from '@shared/contracts/routes' @@ -85,6 +86,10 @@ export function createProviderClient(bridge: DeepchatBridge = getDeepchatBridge( return result.provider } + async function validateDraftProvider(provider: LLM_PROVIDER) { + return await bridge.invoke(providersValidateDraftRoute.name, { provider }) + } + async function removeProviderAtomic(providerId: string) { const result = await bridge.invoke(providersRemoveRoute.name, { providerId }) return result.removed @@ -293,6 +298,7 @@ export function createProviderClient(bridge: DeepchatBridge = getDeepchatBridge( setProviderById, updateProviderAtomic, addProviderAtomic, + validateDraftProvider, removeProviderAtomic, reorderProvidersAtomic, listModels, diff --git a/src/renderer/settings/components/AddCustomProviderDialog.vue b/src/renderer/settings/components/AddCustomProviderDialog.vue deleted file mode 100644 index 426259227c..0000000000 --- a/src/renderer/settings/components/AddCustomProviderDialog.vue +++ /dev/null @@ -1,281 +0,0 @@ - - - diff --git a/src/renderer/settings/components/AddProviderFlow.vue b/src/renderer/settings/components/AddProviderFlow.vue new file mode 100644 index 0000000000..0b13b84b87 --- /dev/null +++ b/src/renderer/settings/components/AddProviderFlow.vue @@ -0,0 +1,273 @@ + + + diff --git a/src/renderer/settings/components/BedrockProviderSettingsDetail.vue b/src/renderer/settings/components/BedrockProviderSettingsDetail.vue index a4f95c5eb6..9dbc10111f 100644 --- a/src/renderer/settings/components/BedrockProviderSettingsDetail.vue +++ b/src/renderer/settings/components/BedrockProviderSettingsDetail.vue @@ -1,9 +1,9 @@