From c96d01acb732c5dbb72394969b776f1d004d0900 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Sun, 12 Apr 2026 12:24:49 -0500 Subject: [PATCH] fix(model): require provider readiness for saved default selection --- .../model-resolver-initial-model-auth.test.ts | 78 +++++++++++++++++++ .../src/core/model-resolver.ts | 40 +++++----- 2 files changed, 100 insertions(+), 18 deletions(-) create mode 100644 packages/pi-coding-agent/src/core/model-resolver-initial-model-auth.test.ts diff --git a/packages/pi-coding-agent/src/core/model-resolver-initial-model-auth.test.ts b/packages/pi-coding-agent/src/core/model-resolver-initial-model-auth.test.ts new file mode 100644 index 000000000..e7d5fb46a --- /dev/null +++ b/packages/pi-coding-agent/src/core/model-resolver-initial-model-auth.test.ts @@ -0,0 +1,78 @@ +import assert from "node:assert/strict"; +import { describe, it } from "node:test"; +import type { Api, Model } from "@gsd/pi-ai"; +import type { ModelRegistry } from "./model-registry.js"; +import { findInitialModel } from "./model-resolver.js"; + +function makeModel(provider: string, id: string): Model { + return { + id, + name: id, + provider, + api: "openai-responses", + reasoning: false, + input: ["text"], + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: 128000, + maxTokens: 8192, + } as Model; +} + +function makeRegistry(opts: { + readyProviders?: Set; + byProviderAndId?: Map>; + available?: Model[]; +}): ModelRegistry { + const readyProviders = opts.readyProviders ?? new Set(); + const byProviderAndId = opts.byProviderAndId ?? new Map>(); + const available = opts.available ?? []; + + return { + find: (provider: string, modelId: string) => byProviderAndId.get(`${provider}/${modelId}`), + getAvailable: async () => available, + isProviderRequestReady: (provider: string) => readyProviders.has(provider), + } as unknown as ModelRegistry; +} + +describe("findInitialModel auth gating for saved defaults", () => { + it("uses saved default when provider is request-ready", async () => { + const saved = makeModel("anthropic", "claude-opus-4-6"); + const registry = makeRegistry({ + readyProviders: new Set(["anthropic"]), + byProviderAndId: new Map([[`anthropic/claude-opus-4-6`, saved]]), + available: [saved], + }); + + const result = await findInitialModel({ + scopedModels: [], + isContinuing: false, + defaultProvider: "anthropic", + defaultModelId: "claude-opus-4-6", + modelRegistry: registry, + }); + + assert.equal(result.model?.provider, "anthropic"); + assert.equal(result.model?.id, "claude-opus-4-6"); + }); + + it("skips saved default when provider is not request-ready and falls back to available", async () => { + const staleDefault = makeModel("anthropic", "claude-opus-4-6"); + const fallback = makeModel("openai", "gpt-5.4"); + const registry = makeRegistry({ + readyProviders: new Set(["openai"]), + byProviderAndId: new Map([[`anthropic/claude-opus-4-6`, staleDefault]]), + available: [fallback], + }); + + const result = await findInitialModel({ + scopedModels: [], + isContinuing: false, + defaultProvider: "anthropic", + defaultModelId: "claude-opus-4-6", + modelRegistry: registry, + }); + + assert.equal(result.model?.provider, "openai"); + assert.equal(result.model?.id, "gpt-5.4"); + }); +}); diff --git a/packages/pi-coding-agent/src/core/model-resolver.ts b/packages/pi-coding-agent/src/core/model-resolver.ts index 3e3b266f7..1a85c2bfa 100644 --- a/packages/pi-coding-agent/src/core/model-resolver.ts +++ b/packages/pi-coding-agent/src/core/model-resolver.ts @@ -504,27 +504,31 @@ export async function findInitialModel(options: { // 3. Try saved default from settings if (defaultProvider && defaultModelId) { - const found = modelRegistry.find(defaultProvider, defaultModelId); - if (found) { - // Check if the provider's recommended default is a higher-capability variant - // of the saved model (e.g. saved "claude-opus-4-6" vs recommended "claude-opus-4-6-extended"). - // If so, prefer the recommended variant to avoid using a smaller context window (#1125). - const recommendedId = defaultModelPerProvider[defaultProvider as KnownProvider]; - if (recommendedId && recommendedId !== defaultModelId && recommendedId.startsWith(defaultModelId)) { - const recommended = modelRegistry.find(defaultProvider, recommendedId); - if (recommended) { - model = recommended; - if (defaultThinkingLevel) { - thinkingLevel = defaultThinkingLevel; + // Guard against stale settings defaults: only use the saved provider/model + // if the provider is actually request-ready (auth/OAuth/CLI ready). + if (modelRegistry.isProviderRequestReady(defaultProvider)) { + const found = modelRegistry.find(defaultProvider, defaultModelId); + if (found) { + // Check if the provider's recommended default is a higher-capability variant + // of the saved model (e.g. saved "claude-opus-4-6" vs recommended "claude-opus-4-6-extended"). + // If so, prefer the recommended variant to avoid using a smaller context window (#1125). + const recommendedId = defaultModelPerProvider[defaultProvider as KnownProvider]; + if (recommendedId && recommendedId !== defaultModelId && recommendedId.startsWith(defaultModelId)) { + const recommended = modelRegistry.find(defaultProvider, recommendedId); + if (recommended) { + model = recommended; + if (defaultThinkingLevel) { + thinkingLevel = defaultThinkingLevel; + } + return { model, thinkingLevel, fallbackMessage: undefined }; } - return { model, thinkingLevel, fallbackMessage: undefined }; } + model = found; + if (defaultThinkingLevel) { + thinkingLevel = defaultThinkingLevel; + } + return { model, thinkingLevel, fallbackMessage: undefined }; } - model = found; - if (defaultThinkingLevel) { - thinkingLevel = defaultThinkingLevel; - } - return { model, thinkingLevel, fallbackMessage: undefined }; } }