diff --git a/packages/app/src/context/global-sync/utils.test.ts b/packages/app/src/context/global-sync/utils.test.ts index 69ca494992..5c62a8f6a0 100644 --- a/packages/app/src/context/global-sync/utils.test.ts +++ b/packages/app/src/context/global-sync/utils.test.ts @@ -103,6 +103,7 @@ describe("normalizeProviderList", () => { ) expect(result.connected).toEqual(["openai"]) + expect(result.defaultModel).toEqual({ providerID: "openai", modelID: "gpt-5" }) expect(result.default).toEqual({ openai: "gpt-5" }) expect(result.all.get("openai")?.models["gpt-old"]).toBeUndefined() expect(result.all.get("openai")?.models["gpt-5"]).toMatchObject({ @@ -113,6 +114,10 @@ describe("normalizeProviderList", () => { variants: { high: {} }, }) }) + + test("preserves an empty current default", () => { + expect(normalizeProviderList([] as ProviderListOutput["data"], [], null).defaultModel).toBeNull() + }) }) describe("directoryKey", () => { diff --git a/packages/app/src/context/global-sync/utils.ts b/packages/app/src/context/global-sync/utils.ts index 59632e53c9..1b2c2f4d2d 100644 --- a/packages/app/src/context/global-sync/utils.ts +++ b/packages/app/src/context/global-sync/utils.ts @@ -139,6 +139,7 @@ export function normalizeProviderList( return { all, connected: providers.map((provider) => provider.id), + defaultModel: defaultModel ? { providerID: defaultModel.providerID, modelID: defaultModel.id } : null, default: Object.fromEntries( providers.flatMap((provider) => { const model = diff --git a/packages/app/src/context/local.tsx b/packages/app/src/context/local.tsx index 83db571385..b99ce2db90 100644 --- a/packages/app/src/context/local.tsx +++ b/packages/app/src/context/local.tsx @@ -6,6 +6,7 @@ import { createStore } from "solid-js/store" import { useModels } from "@/context/models" import { useSettings } from "@/context/settings" import { useProviders } from "@/hooks/use-providers" +import { resolveDefaultModel } from "@/hooks/provider-catalog" import { Persist, persisted } from "@/utils/persist" import { hasCustomAgent, resolveAgent } from "./local-agent" import { cycleModelVariant, getConfiguredAgentVariant, resolveModelVariant } from "./model-variant" @@ -149,10 +150,8 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ }) const configuredModel = () => { - const configured = sync().data.config.model - if (!configured) return - const [providerID, modelID] = configured.split("/") - const model = { providerID, modelID } + const model = resolveDefaultModel(providers.defaultModel(), sync().data.config.model) + if (!model) return if (validModel(model)) return model } diff --git a/packages/app/src/hooks/provider-catalog.test.ts b/packages/app/src/hooks/provider-catalog.test.ts index 04fbe5ab66..99e3884ab8 100644 --- a/packages/app/src/hooks/provider-catalog.test.ts +++ b/packages/app/src/hooks/provider-catalog.test.ts @@ -1,6 +1,6 @@ import { expect, test } from "bun:test" import type { NormalizedProviderListResponse } from "@opencode-ai/session-ui/context" -import { selectProviderCatalog } from "./provider-catalog" +import { resolveDefaultModel, selectProviderCatalog } from "./provider-catalog" const catalog = (id: string): NormalizedProviderListResponse => ({ all: new Map([[id, { id, name: id, source: "api", env: [], options: {}, models: {} }]]), @@ -57,3 +57,21 @@ test("falls back to the global catalog for route consumers", () => { }), ).toBe(global) }) + +test("uses the current server default model", () => { + expect(resolveDefaultModel({ providerID: "openai", modelID: "gpt-5" }, "anthropic/claude")).toEqual({ + providerID: "openai", + modelID: "gpt-5", + }) +}) + +test("does not use legacy config when the current server has no default", () => { + expect(resolveDefaultModel(null, "anthropic/claude")).toBeUndefined() +}) + +test("uses config for legacy servers", () => { + expect(resolveDefaultModel(undefined, "anthropic/claude")).toEqual({ + providerID: "anthropic", + modelID: "claude", + }) +}) diff --git a/packages/app/src/hooks/provider-catalog.ts b/packages/app/src/hooks/provider-catalog.ts index 01b966eda6..d02b84c633 100644 --- a/packages/app/src/hooks/provider-catalog.ts +++ b/packages/app/src/hooks/provider-catalog.ts @@ -25,3 +25,13 @@ export function selectProviderCatalog(input: ProviderCatalogInput) { if (input.explicit) return emptyProviderCatalog return input.global } + +export function resolveDefaultModel( + current: NormalizedProviderListResponse["defaultModel"], + legacy: string | undefined, +) { + if (current !== undefined) return current ?? undefined + if (!legacy) return undefined + const [providerID, modelID] = legacy.split("/") + return { providerID, modelID } +} diff --git a/packages/app/src/hooks/use-providers.ts b/packages/app/src/hooks/use-providers.ts index 60ad01c867..7a7774b62e 100644 --- a/packages/app/src/hooks/use-providers.ts +++ b/packages/app/src/hooks/use-providers.ts @@ -41,6 +41,7 @@ export function useProviders(directory: Accessor) { return { all: () => providers().all, default: () => providers().default, + defaultModel: () => providers().defaultModel, popular: () => pipe( providers().all, diff --git a/packages/app/src/pages/session/composer/prompt-model-selection.ts b/packages/app/src/pages/session/composer/prompt-model-selection.ts index 3f5bb0f477..fe890c7f4a 100644 --- a/packages/app/src/pages/session/composer/prompt-model-selection.ts +++ b/packages/app/src/pages/session/composer/prompt-model-selection.ts @@ -6,6 +6,7 @@ import { usePrompt } from "@/context/prompt" import { useSDK } from "@/context/sdk" import { useSync } from "@/context/sync" import { useProviders } from "@/hooks/use-providers" +import { resolveDefaultModel } from "@/hooks/provider-catalog" export function createPromptModelSelection(input: { agent: () => { model?: ModelKey; variant?: string } | undefined }) { const sdk = useSDK() @@ -21,10 +22,8 @@ export function createPromptModelSelection(input: { agent: () => { model?: Model } const configured = () => { - const value = sync().data.config.model - if (!value) return - const [providerID, modelID] = value.split("/") - const model = { providerID, modelID } + const model = resolveDefaultModel(providers.defaultModel(), sync().data.config.model) + if (!model) return if (valid(model)) return model } diff --git a/packages/session-ui/src/context/data.tsx b/packages/session-ui/src/context/data.tsx index 056fc9c0fd..dd82896a26 100644 --- a/packages/session-ui/src/context/data.tsx +++ b/packages/session-ui/src/context/data.tsx @@ -5,6 +5,10 @@ import { PreloadMultiFileDiffResult } from "@pierre/diffs/ssr" export type NormalizedProviderListResponse = { all: Map + defaultModel?: { + providerID: string + modelID: string + } | null default: { [key: string]: string }