From 53173b67ad764ae3a2c2d5e9577bd6b54aad9974 Mon Sep 17 00:00:00 2001 From: Gagan Date: Wed, 24 Jun 2026 15:54:24 +0530 Subject: [PATCH 1/2] fix(onboarding): BYOK saved entire model catalog as assistant models testAndSaveActiveProvider persisted the provider's full fetched catalog into config.models. That field is the user's curated assistant-model list (rendered as one row per entry in Settings > Models), so every available model showed up as a separate assistant-model dropdown. Seed it with just the selected model; users add more from Settings. --- .../components/onboarding/use-onboarding-state.ts | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/apps/x/apps/renderer/src/components/onboarding/use-onboarding-state.ts b/apps/x/apps/renderer/src/components/onboarding/use-onboarding-state.ts index 12b880e43..17832f4b1 100644 --- a/apps/x/apps/renderer/src/components/onboarding/use-onboarding-state.ts +++ b/apps/x/apps/renderer/src/components/onboarding/use-onboarding-state.ts @@ -429,13 +429,17 @@ export function useOnboardingState(open: boolean, onComplete: () => void) { return false } - const models: string[] = result.models ?? [] + const catalog: string[] = result.models ?? [] const preferred = preferredDefaults[llmProvider] const model = - (preferred && models.includes(preferred) && preferred) || - models[0] || activeConfig.model.trim() || "" - - await window.ipc.invoke("models:saveConfig", { provider, model, models }) + (preferred && catalog.includes(preferred) && preferred) || + catalog[0] || activeConfig.model.trim() || "" + + // `models` is the user's curated assistant-model list (shown in Settings), + // NOT the full provider catalog. Onboarding seeds it with just the selected + // model; users add more from Settings. Persisting the whole catalog here + // rendered every model as a separate assistant-model row. + await window.ipc.invoke("models:saveConfig", { provider, model, models: model ? [model] : [] }) window.dispatchEvent(new Event('models-config-changed')) setTestState({ status: "success" }) setConnectedFlavors(prev => new Set(prev).add(llmProvider)) From 77dbb24df30114e7006155f7e9512d691963356d Mon Sep 17 00:00:00 2001 From: Gagan Date: Wed, 24 Jun 2026 16:28:53 +0530 Subject: [PATCH 2/2] feat(byok): decouple chat model picker from config.models BYOK config.models was doing double duty: it both seeded the in-chat model picker AND was rendered as a per-entry dropdown stack in Settings. That coupling forced a tradeoff between a clean Settings UI and full model choice in chat, and baked a stale catalog snapshot into config. Decouple them so BYOK matches the signed-in experience: - Chat picker (signed-out) now lists the full live provider catalog from models:list, filtered to providers the user has a key/baseURL for, with the saved default model leading. No longer limited to config.models, so newly released models appear without re-saving. - Settings assistant model collapses to a single default-model dropdown (removed the .map() stack + add/remove). config.models is now just the one default; save/load/delete logic is unchanged. --- .../components/chat-input-with-mentions.tsx | 54 ++++++++--- .../src/components/settings-dialog.tsx | 97 +++++-------------- 2 files changed, 66 insertions(+), 85 deletions(-) diff --git a/apps/x/apps/renderer/src/components/chat-input-with-mentions.tsx b/apps/x/apps/renderer/src/components/chat-input-with-mentions.tsx index 412b4bbf6..f36092125 100644 --- a/apps/x/apps/renderer/src/components/chat-input-with-mentions.tsx +++ b/apps/x/apps/renderer/src/components/chat-input-with-mentions.tsx @@ -401,18 +401,50 @@ function ChatInputInner({ } else { const result = await window.ipc.invoke('workspace:readFile', { path: 'config/models.json' }) const parsed = JSON.parse(result.data) + + // Offer every model the configured key supports — the same full catalog + // Settings uses for its dropdowns — so BYOK chat matches the signed-in + // gateway picker. The picker is no longer limited to a hand-curated + // config.models list. Providers with no catalog (Ollama, OpenAI-compatible) + // fall back to the model saved in config. + const catalog: Record = {} + try { + const listResult = await window.ipc.invoke('models:list', null) + for (const p of listResult.providers || []) { + catalog[p.id] = (p.models || []).map((m: { id: string }) => m.id) + } + } catch { /* offline / no catalog — fall back to saved config below */ } + const models: ConfiguredModel[] = [] - if (parsed?.providers) { - for (const [flavor, entry] of Object.entries(parsed.providers)) { - const e = entry as Record - const modelList: string[] = Array.isArray(e.models) ? e.models as string[] : [] - const singleModel = typeof e.model === 'string' ? e.model : '' - const allModels = modelList.length > 0 ? modelList : singleModel ? [singleModel] : [] - for (const model of allModels) { - if (model) { - models.push({ provider: flavor as ProviderName, model }) - } - } + const seen = new Set() + const push = (provider: string, model: string) => { + if (!model) return + const key = `${provider}/${model}` + if (seen.has(key)) return + seen.add(key) + models.push({ provider: provider as ProviderName, model }) + } + + // List the default provider first so its default model leads the picker. + const defaultFlavor = typeof parsed?.provider?.flavor === 'string' ? parsed.provider.flavor : '' + const flavors = Object.keys(parsed?.providers || {}) + .sort((a, b) => (a === defaultFlavor ? -1 : b === defaultFlavor ? 1 : 0)) + + for (const flavor of flavors) { + const e = (parsed.providers[flavor] || {}) as Record + const hasKey = typeof e.apiKey === 'string' && (e.apiKey as string).trim().length > 0 + const hasBaseURL = typeof e.baseURL === 'string' && (e.baseURL as string).trim().length > 0 + if (!hasKey && !hasBaseURL) continue // provider not configured + + // The provider's saved default model leads, then the rest of its catalog. + push(flavor, typeof e.model === 'string' ? e.model : '') + const catalogModels = catalog[flavor] || [] + if (catalogModels.length > 0) { + for (const m of catalogModels) push(flavor, m) + } else { + // No catalog (local provider) — fall back to whatever is saved. + const saved = Array.isArray(e.models) ? e.models as string[] : [] + for (const m of saved) push(flavor, m) } } setConfiguredModels(models) diff --git a/apps/x/apps/renderer/src/components/settings-dialog.tsx b/apps/x/apps/renderer/src/components/settings-dialog.tsx index 52873761b..ef249836a 100644 --- a/apps/x/apps/renderer/src/components/settings-dialog.tsx +++ b/apps/x/apps/renderer/src/components/settings-dialog.tsx @@ -384,38 +384,6 @@ function ModelSettings({ dialogOpen }: { dialogOpen: boolean }) { [] ) - const updateModelAt = useCallback( - (prov: LlmProviderFlavor, index: number, value: string) => { - setProviderConfigs(prev => { - const models = [...prev[prov].models] - models[index] = value - return { ...prev, [prov]: { ...prev[prov], models } } - }) - setTestState({ status: "idle" }) - }, - [] - ) - - const addModel = useCallback( - (prov: LlmProviderFlavor) => { - setProviderConfigs(prev => ({ - ...prev, - [prov]: { ...prev[prov], models: [...prev[prov].models, ""] }, - })) - }, - [] - ) - - const removeModel = useCallback( - (prov: LlmProviderFlavor, index: number) => { - setProviderConfigs(prev => { - const models = prev[prov].models.filter((_, i) => i !== index) - return { ...prev, [prov]: { ...prev[prov], models: models.length > 0 ? models : [""] } } - }) - setTestState({ status: "idle" }) - }, - [] - ) // Load current config from file useEffect(() => { @@ -727,48 +695,29 @@ function ModelSettings({ dialogOpen }: { dialogOpen: boolean }) { ) : (
- {activeConfig.models.map((model, index) => ( -
- {showModelInput ? ( - updateModelAt(provider, index, e.target.value)} - placeholder="Enter model" - /> - ) : ( - - )} - {activeConfig.models.length > 1 && ( - - )} -
- ))} - + {showModelInput ? ( + updateConfig(provider, { models: [e.target.value] })} + placeholder="Enter model" + /> + ) : ( + + )}
)} {modelsError && (