From 669618b7b7fb7ff933746e5b3a7ac24518fb8622 Mon Sep 17 00:00:00 2001 From: Den <2119348+dzianisv@users.noreply.github.com> Date: Mon, 22 Jun 2026 00:38:18 -0700 Subject: [PATCH] fix: model selection precedence to prefer provider default over agent model (#36) Fixes #35. The app was selecting claude-sonnet-4-6 (agent default) instead of gpt-5.4 (provider default), causing send_message failures in CI where only Azure provider is available. --- src/lib/model-selection.test.ts | 50 +++++++++++++++++++++++++++++++++ src/lib/model-selection.ts | 50 +++++++++++++++++++++++++++++++++ src/stores/catalog.ts | 22 +++++++-------- 3 files changed, 110 insertions(+), 12 deletions(-) create mode 100644 src/lib/model-selection.test.ts create mode 100644 src/lib/model-selection.ts diff --git a/src/lib/model-selection.test.ts b/src/lib/model-selection.test.ts new file mode 100644 index 0000000..1afde1b --- /dev/null +++ b/src/lib/model-selection.test.ts @@ -0,0 +1,50 @@ +import { test } from "node:test" +import assert from "node:assert/strict" +import { chooseModelSelection } from "./model-selection.ts" + +const providers = [ + { + id: "azure", + models: [{ id: "gpt-5.4" }, { id: "gpt-5.2" }], + }, +] + +test("keeps existing selection when still available", () => { + const selected = chooseModelSelection({ + providers, + defaults: { azure: "gpt-5.4" }, + existing: { providerID: "azure", modelID: "gpt-5.2" }, + agentModel: { providerID: "azure", modelID: "claude-sonnet-4-6" }, + }) + assert.deepEqual(selected, { providerID: "azure", modelID: "gpt-5.2" }) +}) + +test("prefers provider default over unavailable agent model", () => { + const selected = chooseModelSelection({ + providers, + defaults: { azure: "gpt-5.4" }, + existing: null, + agentModel: { providerID: "azure", modelID: "claude-sonnet-4-6" }, + }) + assert.deepEqual(selected, { providerID: "azure", modelID: "gpt-5.4" }) +}) + +test("falls back to first connected provider model when default missing", () => { + const selected = chooseModelSelection({ + providers, + defaults: {}, + existing: null, + agentModel: null, + }) + assert.deepEqual(selected, { providerID: "azure", modelID: "gpt-5.4" }) +}) + +test("returns null when no connected providers", () => { + const selected = chooseModelSelection({ + providers: [], + defaults: {}, + existing: null, + agentModel: null, + }) + assert.equal(selected, null) +}) diff --git a/src/lib/model-selection.ts b/src/lib/model-selection.ts new file mode 100644 index 0000000..a0729ed --- /dev/null +++ b/src/lib/model-selection.ts @@ -0,0 +1,50 @@ +export interface ProviderModelRef { + id: string +} + +export interface ProviderRef { + id: string + models: ProviderModelRef[] +} + +export interface ModelSelection { + providerID: string + modelID: string +} + +export function isModelAvailable( + providers: ProviderRef[], + selection: ModelSelection | null | undefined, +): selection is ModelSelection { + if (!selection) return false + const provider = providers.find((p) => p.id === selection.providerID) + if (!provider) return false + return provider.models.some((m) => m.id === selection.modelID) +} + +export function chooseModelSelection(params: { + providers: ProviderRef[] + defaults: Record + existing: ModelSelection | null + agentModel: ModelSelection | null +}): ModelSelection | null { + const { providers, defaults, existing, agentModel } = params + + if (isModelAvailable(providers, existing)) return existing + + for (const provider of providers) { + const defaultModelID = defaults[provider.id] + if (!defaultModelID) continue + if (provider.models.some((m) => m.id === defaultModelID)) { + return { providerID: provider.id, modelID: defaultModelID } + } + } + + if (providers.length > 0 && providers[0].models.length > 0) { + return { providerID: providers[0].id, modelID: providers[0].models[0].id } + } + + if (isModelAvailable(providers, agentModel)) return agentModel + + return null +} diff --git a/src/stores/catalog.ts b/src/stores/catalog.ts index aaf5594..59bcf35 100644 --- a/src/stores/catalog.ts +++ b/src/stores/catalog.ts @@ -1,6 +1,7 @@ import { create } from "zustand" import { useConnections } from "./connections" import type { Agent, Command } from "../lib/sdk" +import { chooseModelSelection } from "../lib/model-selection" export interface ProviderModel { id: string @@ -92,19 +93,16 @@ export const useCatalog = create((set, get) => ({ const current = get().agent const agent = current && visible.some((a) => a.name === current) ? current : visible[0]?.name || "build" - // Default model: use default agent's model, or first connected provider's default model + // Default model: keep valid existing selection; otherwise prefer connected + // provider defaults, then first connected model; agent model is last fallback. const existing = get().model - const fallback = (() => { - const defaultAgent = visible[0] - if (defaultAgent?.model) return defaultAgent.model - for (const p of providers) { - const defaultModelID = defaults[p.id] - const match = defaultModelID ? p.models.find((m) => m.id === defaultModelID) : p.models[0] - if (match) return { providerID: p.id, modelID: match.id } - } - return null - })() - const model = existing || fallback + const defaultAgent = visible[0] + const model = chooseModelSelection({ + providers, + defaults, + existing, + agentModel: defaultAgent?.model || null, + }) set({ agents: visible, commands, providers, defaults, agent, model, loaded: true }) },