fix(coding-agent): used authenticated registry models by default

Required CLI model registries to expose getAvailable() and used that authenticated
set whenever callers omit availableModels. Deferred SDK and bench/dry-balance
resolution now lets configured roles beat unauthenticated catalog id collisions.

Updated resolver test registries and made the #6508 regression omit the explicit
availableModels option, covering the deferred-caller path from the review.

Fixes #6508
This commit is contained in:
roboomp
2026-07-24 11:40:38 +00:00
parent df0e77c584
commit 8191cf41d3
6 changed files with 53 additions and 34 deletions
@@ -76,6 +76,7 @@ export interface BenchCommandArgs {
export interface BenchModelRegistry {
getAll(): Model<Api>[];
getAvailable(): Model<Api>[];
getApiKey(model: Model<Api>, sessionId?: string): Promise<string | undefined>;
resolver(model: ApiKeyResolverModel, sessionId?: string): ApiKeyResolver;
hasConfiguredAuth?(model: Model<Api>): boolean;
@@ -466,7 +466,7 @@ export interface ModelMatchPreferences {
}
export type ModelLookupRegistry = Pick<ModelRegistry, "getAvailable">;
type CliModelRegistry = Pick<ModelRegistry, "getAll">;
type CliModelRegistry = Pick<ModelRegistry, "getAll" | "getAvailable">;
type InitialModelRegistry = Pick<ModelRegistry, "getAvailable" | "find">;
type RestorableModelRegistry = Pick<ModelRegistry, "getAvailable" | "find" | "getApiKey">;
@@ -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<Api>[];
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<string, string>();
for (const model of allModels) {
providerMap.set(model.provider.toLowerCase(), model.provider);
@@ -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,
@@ -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,
};
@@ -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();
@@ -992,7 +992,9 @@ describe("resolveModelOverride", () => {
});
describe("resolveCliModel", () => {
test("resolves --model provider/id without --provider", () => {
const registry = { getAll: () => allModels } as unknown as Parameters<typeof resolveCliModel>[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<typeof resolveCliModel>[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 <pattern>:<thinking> (without explicit --thinking)", () => {
const registry = { getAll: () => allModels } as unknown as Parameters<typeof resolveCliModel>[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<typeof resolveCliModel>[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<typeof resolveCliModel>[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<typeof resolveCliModel>[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<typeof resolveCliModel>[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<typeof resolveCliModel>[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<typeof resolveCliModel>[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<typeof resolveCliModel>[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");