diff --git a/packages/coding-agent/src/cli/bench-cli.ts b/packages/coding-agent/src/cli/bench-cli.ts index 60b880083..485241b58 100644 --- a/packages/coding-agent/src/cli/bench-cli.ts +++ b/packages/coding-agent/src/cli/bench-cli.ts @@ -76,6 +76,7 @@ export interface BenchCommandArgs { export interface BenchModelRegistry { getAll(): Model[]; + getAvailable(): Model[]; getApiKey(model: Model, sessionId?: string): Promise; resolver(model: ApiKeyResolverModel, sessionId?: string): ApiKeyResolver; hasConfiguredAuth?(model: Model): boolean; diff --git a/packages/coding-agent/src/config/model-resolver.ts b/packages/coding-agent/src/config/model-resolver.ts index 348d06639..473fbd13c 100644 --- a/packages/coding-agent/src/config/model-resolver.ts +++ b/packages/coding-agent/src/config/model-resolver.ts @@ -466,7 +466,7 @@ export interface ModelMatchPreferences { } export type ModelLookupRegistry = Pick; -type CliModelRegistry = Pick; +type CliModelRegistry = Pick; type InitialModelRegistry = Pick; type RestorableModelRegistry = Pick; @@ -1652,7 +1652,7 @@ export function resolveCliModel(options: { cliProvider?: string; cliModel?: string; modelRegistry: CliModelRegistry; - /** Authenticated models to prefer for unqualified selectors; omit to preserve catalog-order behavior. */ + /** Authenticated models to prefer for unqualified selectors; defaults to the registry's authenticated set. */ availableModels?: Model[]; settings?: Settings; preferences?: ModelMatchPreferences; @@ -1673,7 +1673,7 @@ export function resolveCliModel(options: { }; } - const availableModels = preferredModels ?? allModels; + const availableModels = preferredModels ?? modelRegistry.getAvailable(); const providerMap = new Map(); for (const model of allModels) { providerMap.set(model.provider.toLowerCase(), model.provider); diff --git a/packages/coding-agent/test/bench-auth-fallback.test.ts b/packages/coding-agent/test/bench-auth-fallback.test.ts index 69c067ec3..87e3605f4 100644 --- a/packages/coding-agent/test/bench-auth-fallback.test.ts +++ b/packages/coding-agent/test/bench-auth-fallback.test.ts @@ -65,6 +65,7 @@ function fakeRegistry(opts: FakeRegistryOptions): BenchModelRegistry { const authed = new Set(opts.authedProviders); return { getAll: () => opts.models, + getAvailable: () => opts.models.filter(model => authed.has(model.provider)), hasConfiguredAuth: model => authed.has(model.provider), getApiKey: async model => (authed.has(model.provider) ? "sk-test" : undefined), resolver: () => (() => Promise.resolve("sk-test")) as unknown as ApiKeyResolver, diff --git a/packages/coding-agent/test/bench-cache.test.ts b/packages/coding-agent/test/bench-cache.test.ts index 9bbb91af6..02984fd96 100644 --- a/packages/coding-agent/test/bench-cache.test.ts +++ b/packages/coding-agent/test/bench-cache.test.ts @@ -38,6 +38,7 @@ const codexModel = { const registry: BenchModelRegistry = { getAll: () => [model], + getAvailable: () => [model], getApiKey: async () => "sk-test", resolver: () => (() => Promise.resolve("sk-test")) as unknown as ApiKeyResolver, }; diff --git a/packages/coding-agent/test/issue-980-bedrock-priority.test.ts b/packages/coding-agent/test/issue-980-bedrock-priority.test.ts index 8f208dbbb..135d703e1 100644 --- a/packages/coding-agent/test/issue-980-bedrock-priority.test.ts +++ b/packages/coding-agent/test/issue-980-bedrock-priority.test.ts @@ -52,6 +52,7 @@ describe("issue #980 provider-qualified model resolution", () => { cliModel: "anthropic/claude-3-7-sonnet", modelRegistry: { getAll: () => availableModels, + getAvailable: () => availableModels, }, }); expect(cliResolved.model).toBeUndefined(); diff --git a/packages/coding-agent/test/model-resolver.test.ts b/packages/coding-agent/test/model-resolver.test.ts index 55ff9af16..19f1d8d84 100644 --- a/packages/coding-agent/test/model-resolver.test.ts +++ b/packages/coding-agent/test/model-resolver.test.ts @@ -992,7 +992,9 @@ describe("resolveModelOverride", () => { }); describe("resolveCliModel", () => { test("resolves --model provider/id without --provider", () => { - const registry = { getAll: () => allModels } as unknown as Parameters[0]["modelRegistry"]; + const registry = { getAll: () => allModels, getAvailable: () => allModels } as unknown as Parameters< + typeof resolveCliModel + >[0]["modelRegistry"]; const result = resolveCliModel({ cliModel: "openai/gpt-4o", @@ -1006,9 +1008,7 @@ describe("resolveCliModel", () => { test("prefers an authenticated provider for an unqualified exact model id", () => { const availableModels = openaiGpt55Models.filter(model => model.provider === "openai-codex"); - const registry = { - getAll: () => openaiGpt55Models, - }; + const registry = { getAll: () => openaiGpt55Models, getAvailable: () => openaiGpt55Models }; const result = resolveCliModel({ cliModel: "gpt-5.5", @@ -1043,7 +1043,7 @@ describe("resolveCliModel", () => { const result = resolveCliModel({ cliModel: "openai/gpt-oss-120b", - modelRegistry: { getAll: () => catalog }, + modelRegistry: { getAll: () => catalog, getAvailable: () => [authenticated] }, availableModels: [authenticated], }); @@ -1053,7 +1053,7 @@ describe("resolveCliModel", () => { }); test("resolves bare configured role names from --model", () => { - const registry = { getAll: () => allModels }; + const registry = { getAll: () => allModels, getAvailable: () => allModels }; const settings = Settings.isolated({ modelRoles: { task: "openai/gpt-4o" }, }); @@ -1070,7 +1070,7 @@ describe("resolveCliModel", () => { }); test("resolves bare configured role names with thinking suffixes", () => { - const registry = { getAll: () => allModels }; + const registry = { getAll: () => allModels, getAvailable: () => allModels }; const settings = Settings.isolated({ modelRoles: { task: "anthropic/claude-sonnet-4-5" }, }); @@ -1088,7 +1088,7 @@ describe("resolveCliModel", () => { }); test("preserves configured role fallback selectors for deferred resolution", () => { - const registry = { getAll: () => allModels }; + const registry = { getAll: () => allModels, getAvailable: () => allModels }; const settings = Settings.isolated({ modelRoles: { task: "openrouter/z-ai/glm-4.7@cerebras,anthropic/claude-sonnet-4-5", @@ -1105,7 +1105,7 @@ describe("resolveCliModel", () => { }); test("reports when a configured role matches after unresolved candidates", () => { - const registry = { getAll: () => allModels }; + const registry = { getAll: () => allModels, getAvailable: () => allModels }; const settings = Settings.isolated({ modelRoles: { task: "runtime-provider/runtime-model,anthropic/claude-sonnet-4-5", @@ -1124,7 +1124,7 @@ describe("resolveCliModel", () => { }); test("does not fuzzy-match unresolved configured roles", () => { - const registry = { getAll: () => allModels }; + const registry = { getAll: () => allModels, getAvailable: () => allModels }; const settings = Settings.isolated({ modelRoles: { sonnet: "runtime-provider/runtime-model" }, }); @@ -1141,7 +1141,7 @@ describe("resolveCliModel", () => { }); test("keeps unknown --model names on the not-found path", () => { - const registry = { getAll: () => allModels }; + const registry = { getAll: () => allModels, getAvailable: () => allModels }; const result = resolveCliModel({ cliModel: "not-a-model", @@ -1165,7 +1165,7 @@ describe("resolveCliModel", () => { contextWindow: 128000, maxTokens: 4096, }); - const registry = { getAll: () => [...allModels, exactModel] }; + const registry = { getAll: () => [...allModels, exactModel], getAvailable: () => [...allModels, exactModel] }; const settings = Settings.isolated({ modelRoles: { task: "anthropic/claude-sonnet-4-5" }, }); @@ -1206,7 +1206,7 @@ describe("resolveCliModel", () => { contextWindow: 128000, maxTokens: 4096, }); - const registry = { getAll: () => [...allModels, cursorDefault] }; + const registry = { getAll: () => [...allModels, cursorDefault], getAvailable: () => allModels }; const settings = Settings.isolated({ modelRoles: { default: "openai/gpt-4o" }, }); @@ -1215,8 +1215,6 @@ describe("resolveCliModel", () => { cliModel: "default", modelRegistry: registry, settings, - // Authenticated set omits the catalog-only Cursor model. - availableModels: allModels, }); expect(result.error).toBeUndefined(); @@ -1240,14 +1238,13 @@ describe("resolveCliModel", () => { contextWindow: 128000, maxTokens: 4096, }); - const registry = { getAll: () => [...allModels, cursorDefault] }; + const registry = { getAll: () => [...allModels, cursorDefault], getAvailable: () => allModels }; const settings = Settings.isolated({ modelRoles: {} }); const result = resolveCliModel({ cliModel: "default", modelRegistry: registry, settings, - availableModels: allModels, }); expect(result.model?.provider).toBe("cursor"); @@ -1255,7 +1252,7 @@ describe("resolveCliModel", () => { }); test("resolves configured custom, legacy, and default role aliases from --model", () => { - const registry = { getAll: () => allModels }; + const registry = { getAll: () => allModels, getAvailable: () => allModels }; const settings = Settings.isolated({ modelRoles: { default: "openai/gpt-4o", @@ -1289,7 +1286,7 @@ describe("resolveCliModel", () => { }); test("splits thinking suffixes and abbreviations off the * default alias", () => { - const registry = { getAll: () => allModels }; + const registry = { getAll: () => allModels, getAvailable: () => allModels }; const settings = Settings.isolated({ modelRoles: { default: "anthropic/claude-sonnet-4-5" }, }); @@ -1315,7 +1312,9 @@ describe("resolveCliModel", () => { }); test("resolves fuzzy patterns within an explicit provider", () => { - const registry = { getAll: () => allModels } as unknown as Parameters[0]["modelRegistry"]; + const registry = { getAll: () => allModels, getAvailable: () => allModels } as unknown as Parameters< + typeof resolveCliModel + >[0]["modelRegistry"]; const result = resolveCliModel({ cliProvider: "openai", @@ -1329,7 +1328,9 @@ describe("resolveCliModel", () => { }); test("supports --model : (without explicit --thinking)", () => { - const registry = { getAll: () => allModels } as unknown as Parameters[0]["modelRegistry"]; + const registry = { getAll: () => allModels, getAvailable: () => allModels } as unknown as Parameters< + typeof resolveCliModel + >[0]["modelRegistry"]; const result = resolveCliModel({ cliModel: "sonnet:high", @@ -1342,7 +1343,9 @@ describe("resolveCliModel", () => { }); test("prefers exact model id match over provider inference (OpenRouter-style ids)", () => { - const registry = { getAll: () => allModels } as unknown as Parameters[0]["modelRegistry"]; + const registry = { getAll: () => allModels, getAvailable: () => allModels } as unknown as Parameters< + typeof resolveCliModel + >[0]["modelRegistry"]; const result = resolveCliModel({ cliModel: "openai/gpt-4o:extended", @@ -1355,7 +1358,9 @@ describe("resolveCliModel", () => { }); test("does not strip invalid :suffix as thinking level in --model (fail fast)", () => { - const registry = { getAll: () => allModels } as unknown as Parameters[0]["modelRegistry"]; + const registry = { getAll: () => allModels, getAvailable: () => allModels } as unknown as Parameters< + typeof resolveCliModel + >[0]["modelRegistry"]; const result = resolveCliModel({ cliProvider: "openai", @@ -1368,7 +1373,9 @@ describe("resolveCliModel", () => { }); test("supports provider-prefixed OpenRouter route suffixes even when the base model is cataloged without them", () => { - const registry = { getAll: () => allModels } as unknown as Parameters[0]["modelRegistry"]; + const registry = { getAll: () => allModels, getAvailable: () => allModels } as unknown as Parameters< + typeof resolveCliModel + >[0]["modelRegistry"]; const result = resolveCliModel({ cliModel: "openrouter/z-ai/glm-4.7-20251222:nitro", @@ -1381,7 +1388,9 @@ describe("resolveCliModel", () => { }); test("supports explicit OpenRouter provider with route suffixes that are not in the catalog", () => { - const registry = { getAll: () => allModels } as unknown as Parameters[0]["modelRegistry"]; + const registry = { getAll: () => allModels, getAvailable: () => allModels } as unknown as Parameters< + typeof resolveCliModel + >[0]["modelRegistry"]; const result = resolveCliModel({ cliProvider: "openrouter", @@ -1401,12 +1410,12 @@ describe("resolveCliModel", () => { const baseResult = resolveCliModel({ cliProvider: "amazon-bedrock", cliModel: profileArn, - modelRegistry: { getAll: () => [defaultBedrockModel] }, + modelRegistry: { getAll: () => [defaultBedrockModel], getAvailable: () => [defaultBedrockModel] }, }); const offResult = resolveCliModel({ cliProvider: "amazon-bedrock", cliModel: `${profileArn}:off`, - modelRegistry: { getAll: () => [defaultBedrockModel] }, + modelRegistry: { getAll: () => [defaultBedrockModel], getAvailable: () => [defaultBedrockModel] }, }); expect(baseResult.error).toBeUndefined(); @@ -1425,7 +1434,9 @@ describe("resolveCliModel", () => { }); test("returns a clear error when there are no models", () => { - const registry = { getAll: () => [] } as unknown as Parameters[0]["modelRegistry"]; + const registry = { getAll: () => [], getAvailable: () => [] } as unknown as Parameters< + typeof resolveCliModel + >[0]["modelRegistry"]; const result = resolveCliModel({ cliProvider: "openai", @@ -1438,7 +1449,9 @@ describe("resolveCliModel", () => { }); test("resolves provider-prefixed fuzzy patterns (openrouter/qwen -> openrouter model)", () => { - const registry = { getAll: () => allModels } as unknown as Parameters[0]["modelRegistry"]; + const registry = { getAll: () => allModels, getAvailable: () => allModels } as unknown as Parameters< + typeof resolveCliModel + >[0]["modelRegistry"]; const result = resolveCliModel({ cliModel: "openrouter/qwen", @@ -1479,7 +1492,7 @@ describe("resolveCliModel", () => { maxTokens: 4096, }), ]; - const registry = { getAll: () => ambiguousModels } as unknown as Parameters< + const registry = { getAll: () => ambiguousModels, getAvailable: () => ambiguousModels } as unknown as Parameters< typeof resolveCliModel >[0]["modelRegistry"]; @@ -1843,7 +1856,9 @@ describe("provider routing selector (@upstream)", () => { }); test("resolveCliModel round-trips @upstream in the selector and carries compat", () => { - const registry = { getAll: () => allModels } as unknown as Parameters[0]["modelRegistry"]; + const registry = { getAll: () => allModels, getAvailable: () => allModels } as unknown as Parameters< + typeof resolveCliModel + >[0]["modelRegistry"]; const result = resolveCliModel({ cliModel: "openrouter/z-ai/glm-4.7@cerebras", modelRegistry: registry }); expect(result.model?.id).toBe("z-ai/glm-4.7"); expect(result.selector).toBe("openrouter/z-ai/glm-4.7@cerebras");