chore(vendor): import pi 1.3.0

Imported as a linear vendor commit so forge rebases do not replay the upstream root tree.

git-subtree-dir: third_party/pi

git-subtree-split: 71023aec1546d6d677a65669c5665c0927ce0dd0
This commit is contained in:
2026-10-15 17:58:48 -04:00
parent 719370c8f7
commit 6081e3e325
1743 changed files with 205460 additions and 131705 deletions

View File

@@ -2,7 +2,7 @@ import { mkdirSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { Agent } from "@earendil-works/pi-agent-core";
import type { Model } from "@earendil-works/pi-ai";
import type { Api, Model } from "@earendil-works/pi-ai";
import { getModel, streamSimple } from "@earendil-works/pi-ai/compat";
import { getBuiltinModels, getBuiltinProviders } from "@earendil-works/pi-ai/providers/all";
import { afterEach, describe, expect, test, vi } from "vitest";
@@ -11,8 +11,9 @@ import { AuthStorage } from "../src/core/auth-storage.ts";
import {
defaultModelPerProvider,
findInitialModel,
parseModelPattern,
type ModelReferenceRequest,
resolveCliModel,
resolveModelReference,
resolveModelScope,
resolveModelScopeWithDiagnostics,
} from "../src/core/model-resolver.ts";
@@ -79,144 +80,258 @@ const mockOpenRouterModels: Model<"anthropic-messages">[] = [
const allModels = [...mockModels, ...mockOpenRouterModels];
describe("parseModelPattern", () => {
describe("simple patterns without colons", () => {
test("exact match returns model with undefined thinking level", () => {
const result = parseModelPattern("claude-sonnet-4-5", allModels);
expect(result.model?.id).toBe("claude-sonnet-4-5");
expect(result.thinkingLevel).toBeUndefined();
expect(result.warning).toBeUndefined();
});
function referenceModel(provider: string, id: string): Model<Api> {
return {
id,
name: id,
api: "openai-completions",
provider,
baseUrl: "https://example.invalid",
reasoning: true,
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 128000,
maxTokens: 8192,
};
}
test("partial match returns best model with undefined thinking level", () => {
const result = parseModelPattern("sonnet", allModels);
expect(result.model?.id).toBe("claude-sonnet-4-5");
expect(result.thinkingLevel).toBeUndefined();
expect(result.warning).toBeUndefined();
});
const referenceCatalog = [
referenceModel("anthropic", "claude-sonnet-4-5"),
referenceModel("anthropic", "claude-sonnet-4-5-20250929"),
referenceModel("anthropic", "claude-opus-4-8"),
referenceModel("anthropic", "exact"),
// Fuzzy-matches "shared"; an ambiguous exact "shared" must not fall through to it.
referenceModel("anthropic", "shared-pro"),
referenceModel("openai", "gpt-4o"),
referenceModel("openrouter", "qwen/qwen3-coder:exacto"),
referenceModel("openrouter", "openai/gpt-4o:extended"),
referenceModel("gw-a", "shared"),
referenceModel("gw-a", "anthropic/future"),
referenceModel("gw-a", "anthropic/exact"),
referenceModel("gw-a", "anthropic/claude-opus-4.8"),
referenceModel("gw-a", "anthropic/claude-opus"),
referenceModel("gw-b", "shared"),
referenceModel("gw-b", "anthropic/future"),
referenceModel("gw-b", "claude-opus-4-8"),
referenceModel("gw-b", "gw-b/free"),
referenceModel("gw-b", "zz:free"),
];
test("no match returns undefined model and thinking level", () => {
const result = parseModelPattern("nonexistent", allModels);
expect(result.model).toBeUndefined();
expect(result.thinkingLevel).toBeUndefined();
expect(result.warning).toBeUndefined();
});
});
type ReferenceExpected =
| { model: string; thinkingLevel?: string; warning?: string }
| { ambiguous: string[]; usableCount: number }
| "not-found";
describe("patterns with valid thinking levels", () => {
test("sonnet:high returns sonnet with high thinking level", () => {
const result = parseModelPattern("sonnet:high", allModels);
expect(result.model?.id).toBe("claude-sonnet-4-5");
expect(result.thinkingLevel).toBe("high");
expect(result.warning).toBeUndefined();
});
interface ReferenceCase {
name: string;
text: string;
expected: ReferenceExpected;
usable?: string[];
provider?: string;
thinkingSuffix?: ModelReferenceRequest["thinkingSuffix"];
fuzzy?: boolean;
}
test("gpt-4o:medium returns gpt-4o with medium thinking level", () => {
const result = parseModelPattern("gpt-4o:medium", allModels);
expect(result.model?.id).toBe("gpt-4o");
expect(result.thinkingLevel).toBe("medium");
expect(result.warning).toBeUndefined();
});
const referenceCases: ReferenceCase[] = [
// Reference syntax
{ name: "exact id", text: "claude-sonnet-4-5", expected: { model: "anthropic/claude-sonnet-4-5" } },
{ name: "exact id is case-insensitive", text: "GPT-4O", expected: { model: "openai/gpt-4o" } },
{ name: "fuzzy prefers aliases over dated ids", text: "sonnet", expected: { model: "anthropic/claude-sonnet-4-5" } },
{
name: "thinking suffix",
text: "sonnet:high",
expected: { model: "anthropic/claude-sonnet-4-5", thinkingLevel: "high" },
},
{
name: "colon in a literal id is not a thinking suffix",
text: "qwen/qwen3-coder:exacto",
expected: { model: "openrouter/qwen/qwen3-coder:exacto" },
},
{
name: "provider prefix, colon id, and thinking suffix",
text: "openrouter/qwen/qwen3-coder:exacto:high",
expected: { model: "openrouter/qwen/qwen3-coder:exacto", thinkingLevel: "high" },
},
{
name: "raw id wins when the provider prefix names no matching model",
text: "openai/gpt-4o:extended",
expected: { model: "openrouter/openai/gpt-4o:extended" },
},
{
name: "fuzzy searches the prefixed provider first",
text: "openrouter/qwen",
expected: { model: "openrouter/qwen/qwen3-coder:exacto" },
},
{ name: "strict keeps an invalid suffix in the id", text: "gpt-4o:random", expected: "not-found" },
{
name: "lenient drops an invalid suffix with a warning",
text: "gpt-4o:random",
thinkingSuffix: "lenient",
expected: { model: "openai/gpt-4o", warning: 'Invalid thinking level "random"' },
},
{
name: "lenient invalid outer suffix discards the inner thinking level",
text: "qwen/qwen3-coder:exacto:high:random",
thinkingSuffix: "lenient",
expected: { model: "openrouter/qwen/qwen3-coder:exacto", warning: 'Invalid thinking level "random"' },
},
{ name: "none keeps the suffix in the id", text: "gpt-4o:high", thinkingSuffix: "none", expected: "not-found" },
{ name: "exact-only skips fuzzy matches", text: "sonnet", fuzzy: false, expected: "not-found" },
{ name: "empty text", text: " ", expected: "not-found" },
test("all valid thinking levels work", () => {
for (const level of ["off", "minimal", "low", "medium", "high", "xhigh", "max"]) {
const result = parseModelPattern(`sonnet:${level}`, allModels);
expect(result.model?.id).toBe("claude-sonnet-4-5");
expect(result.thinkingLevel).toBe(level);
expect(result.warning).toBeUndefined();
// Exact matches across providers
// #7327
{ name: "sole usable provider wins", text: "shared", usable: ["gw-b"], expected: { model: "gw-b/shared" } },
{
name: "no usable provider is ambiguous instead of falling through to fuzzy",
text: "shared",
expected: { ambiguous: ["gw-a/shared", "gw-b/shared"], usableCount: 0 },
},
{
name: "several usable providers are ambiguous",
text: "shared",
usable: ["gw-a", "gw-b"],
expected: { ambiguous: ["gw-a/shared", "gw-b/shared"], usableCount: 2 },
},
{
name: "thinking suffix does not change provider selection",
text: "shared:high",
usable: ["gw-b"],
expected: { model: "gw-b/shared", thinkingLevel: "high" },
},
{
name: "thinking suffix does not bypass ambiguity",
text: "shared:high",
usable: ["gw-a", "gw-b"],
expected: { ambiguous: ["gw-a/shared", "gw-b/shared"], usableCount: 2 },
},
{
// #10160
name: "provider prefix without a direct model selects the usable raw id",
text: "anthropic/future",
usable: ["gw-b"],
expected: { model: "gw-b/anthropic/future" },
},
{
// #5823
name: "usable qualified match beats a usable raw id",
text: "anthropic/exact",
usable: ["anthropic", "gw-a"],
expected: { model: "anthropic/exact" },
},
{
// #5643
name: "usable raw id beats an unusable qualified match",
text: "anthropic/exact",
usable: ["gw-a"],
expected: { model: "gw-a/anthropic/exact" },
},
{
name: "unusable qualified match is kept when nothing is usable",
text: "anthropic/exact",
expected: { model: "anthropic/exact" },
},
{
// Like `mistral/codestral` -> mistral/codestral-latest despite gateway id `mistral/codestral`
name: "named provider's usable fuzzy match beats a raw id elsewhere",
text: "anthropic/claude-opus",
usable: ["anthropic"],
expected: { model: "anthropic/claude-opus-4-8" },
},
{
name: "usable raw id beats the named provider's unusable fuzzy match",
text: "anthropic/claude-opus",
usable: ["gw-a"],
expected: { model: "gw-a/anthropic/claude-opus" },
},
{
name: "named provider's fuzzy match is kept when nothing is usable",
text: "anthropic/claude-opus",
expected: { model: "anthropic/claude-opus-4-8" },
},
{
name: "named provider's literal id beats its fuzzy matches",
text: "gw-b/free",
expected: { model: "gw-b/gw-b/free" },
},
// Fuzzy matches across providers
{
name: "fuzzy prefers usable providers",
text: "opus",
usable: ["gw-a"],
expected: { model: "gw-a/anthropic/claude-opus-4.8" },
},
{
name: "fuzzy id tie across usable providers is ambiguous",
text: "opus",
usable: ["anthropic", "gw-a", "gw-b"],
expected: { ambiguous: ["anthropic/claude-opus-4-8", "gw-b/claude-opus-4-8"], usableCount: 2 },
},
{
name: "fuzzy id tie resolves to the sole usable provider",
text: "opus",
usable: ["anthropic"],
expected: { model: "anthropic/claude-opus-4-8" },
},
// Explicit provider constraint
{
name: "explicit provider ignores usable providers elsewhere",
text: "shared",
provider: "gw-a",
usable: ["gw-b"],
expected: { model: "gw-a/shared" },
},
{
name: "explicit provider accepts its own prefix",
text: "GW-B/shared",
provider: "gw-b",
expected: { model: "gw-b/shared" },
},
{
name: "explicit provider allows fuzzy matches",
text: "4o",
provider: "openai",
expected: { model: "openai/gpt-4o" },
},
{ name: "explicit provider never leaves the provider", text: "gpt-4o", provider: "gw-a", expected: "not-found" },
];
function modelRef(m: Model<Api>): string {
return `${m.provider}/${m.id}`;
}
describe("resolveModelReference", () => {
test.each(referenceCases)("$name", (testCase) => {
const usable = new Set(testCase.usable);
const request: ModelReferenceRequest = {
text: testCase.text,
provider: testCase.provider,
thinkingSuffix: testCase.thinkingSuffix ?? "strict",
fuzzy: testCase.fuzzy ?? true,
};
// Catalog order must never decide the result.
for (const models of [referenceCatalog, [...referenceCatalog].reverse()]) {
const result = resolveModelReference(request, models, (provider) => usable.has(provider));
const expected = testCase.expected;
if (expected === "not-found") {
expect(result.status).toBe("not-found");
} else if ("ambiguous" in expected) {
expect(result.status).toBe("ambiguous");
if (result.status !== "ambiguous") continue;
expect(result.candidates.map(modelRef).sort()).toEqual(expected.ambiguous);
expect(result.usableCount).toBe(expected.usableCount);
} else {
expect(result.status).toBe("resolved");
if (result.status !== "resolved") continue;
expect(modelRef(result.model)).toBe(expected.model);
expect(result.thinkingLevel).toBe(expected.thinkingLevel);
if (expected.warning) expect(result.warning).toContain(expected.warning);
else expect(result.warning).toBeUndefined();
}
});
});
describe("patterns with invalid thinking levels", () => {
test("sonnet:random returns sonnet with undefined thinking level and warning", () => {
const result = parseModelPattern("sonnet:random", allModels);
expect(result.model?.id).toBe("claude-sonnet-4-5");
expect(result.thinkingLevel).toBeUndefined();
expect(result.warning).toContain("Invalid thinking level");
expect(result.warning).toContain("random");
});
test("gpt-4o:invalid returns gpt-4o with undefined thinking level and warning", () => {
const result = parseModelPattern("gpt-4o:invalid", allModels);
expect(result.model?.id).toBe("gpt-4o");
expect(result.thinkingLevel).toBeUndefined();
expect(result.warning).toContain("Invalid thinking level");
});
});
describe("OpenRouter models with colons in IDs", () => {
test("qwen3-coder:exacto matches the model with undefined thinking level", () => {
const result = parseModelPattern("qwen/qwen3-coder:exacto", allModels);
expect(result.model?.id).toBe("qwen/qwen3-coder:exacto");
expect(result.thinkingLevel).toBeUndefined();
expect(result.warning).toBeUndefined();
});
test("openrouter/qwen/qwen3-coder:exacto matches with provider prefix", () => {
const result = parseModelPattern("openrouter/qwen/qwen3-coder:exacto", allModels);
expect(result.model?.id).toBe("qwen/qwen3-coder:exacto");
expect(result.model?.provider).toBe("openrouter");
expect(result.thinkingLevel).toBeUndefined();
expect(result.warning).toBeUndefined();
});
test("qwen3-coder:exacto:high matches model with high thinking level", () => {
const result = parseModelPattern("qwen/qwen3-coder:exacto:high", allModels);
expect(result.model?.id).toBe("qwen/qwen3-coder:exacto");
expect(result.thinkingLevel).toBe("high");
expect(result.warning).toBeUndefined();
});
test("openrouter/qwen/qwen3-coder:exacto:high matches with provider and thinking level", () => {
const result = parseModelPattern("openrouter/qwen/qwen3-coder:exacto:high", allModels);
expect(result.model?.id).toBe("qwen/qwen3-coder:exacto");
expect(result.model?.provider).toBe("openrouter");
expect(result.thinkingLevel).toBe("high");
expect(result.warning).toBeUndefined();
});
test("gpt-4o:extended matches the extended model with undefined thinking level", () => {
const result = parseModelPattern("openai/gpt-4o:extended", allModels);
expect(result.model?.id).toBe("openai/gpt-4o:extended");
expect(result.thinkingLevel).toBeUndefined();
expect(result.warning).toBeUndefined();
});
});
describe("invalid thinking levels with OpenRouter models", () => {
test("qwen3-coder:exacto:random returns model with undefined thinking level and warning", () => {
const result = parseModelPattern("qwen/qwen3-coder:exacto:random", allModels);
expect(result.model?.id).toBe("qwen/qwen3-coder:exacto");
expect(result.thinkingLevel).toBeUndefined();
expect(result.warning).toContain("Invalid thinking level");
expect(result.warning).toContain("random");
});
test("qwen3-coder:exacto:high:random returns model with undefined thinking level and warning", () => {
const result = parseModelPattern("qwen/qwen3-coder:exacto:high:random", allModels);
expect(result.model?.id).toBe("qwen/qwen3-coder:exacto");
expect(result.thinkingLevel).toBeUndefined();
expect(result.warning).toContain("Invalid thinking level");
expect(result.warning).toContain("random");
});
});
describe("edge cases", () => {
test("empty pattern matches via partial matching", () => {
// Empty string is included in all model IDs, so partial matching finds a match
const result = parseModelPattern("", allModels);
expect(result.model).not.toBeNull();
expect(result.thinkingLevel).toBeUndefined();
});
test("pattern ending with colon treats empty suffix as invalid", () => {
const result = parseModelPattern("sonnet:", allModels);
// Empty string after colon is not a valid thinking level
// So it tries to match "sonnet:" which won't match, then tries "sonnet"
expect(result.model?.id).toBe("claude-sonnet-4-5");
expect(result.warning).toContain("Invalid thinking level");
});
}
});
});
@@ -270,6 +385,41 @@ describe("resolveModelScopeWithDiagnostics", () => {
}
});
test("adds every provider for a pattern matching the same id on several providers, with a warning", async () => {
const registry = {
getAvailable: () => [
...allModels,
{ ...mockModels[1], provider: "azure" },
{ ...mockModels[1], provider: "openai-codex", id: "gpt-4o-mini" },
],
} as unknown as Parameters<typeof resolveModelScopeWithDiagnostics>[1];
for (const [pattern, thinkingLevel] of [
["gpt-4o:high", "high"],
["GPT-4o", undefined],
] as const) {
const result = await resolveModelScopeWithDiagnostics([pattern], registry);
expect(
result.scopedModels.map((scoped) => [`${scoped.model.provider}/${scoped.model.id}`, scoped.thinkingLevel]),
).toEqual(
expect.arrayContaining([
["azure/gpt-4o", thinkingLevel],
["openai/gpt-4o", thinkingLevel],
]),
);
expect(result.scopedModels).toHaveLength(2);
expect(result.diagnostics).toEqual([
{
type: "warning",
code: "ambiguous",
message: `Pattern "${pattern}" matches several providers: azure, openai. Use provider/id to choose one.`,
pattern,
},
]);
}
});
test("resolves bracketed model ids as exact references before glob matching", async () => {
const bracketedModel: Model<"anthropic-messages"> = {
id: "bracketed-model[1m]",
@@ -319,67 +469,6 @@ describe("resolveModelScopeWithDiagnostics", () => {
});
describe("resolveCliModel", () => {
test("resolves --model provider/id without --provider", () => {
const registry = {
getModels: () => allModels,
} as unknown as Parameters<typeof resolveCliModel>[0]["modelRuntime"];
const result = resolveCliModel({
cliModel: "openai/gpt-4o",
modelRuntime: registry,
});
expect(result.error).toBeUndefined();
expect(result.model?.provider).toBe("openai");
expect(result.model?.id).toBe("gpt-4o");
});
test("resolves fuzzy patterns within an explicit provider", () => {
const registry = {
getModels: () => allModels,
} as unknown as Parameters<typeof resolveCliModel>[0]["modelRuntime"];
const result = resolveCliModel({
cliProvider: "openai",
cliModel: "4o",
modelRuntime: registry,
});
expect(result.error).toBeUndefined();
expect(result.model?.provider).toBe("openai");
expect(result.model?.id).toBe("gpt-4o");
});
test("supports --model <pattern>:<thinking> (without explicit --thinking)", () => {
const registry = {
getModels: () => allModels,
} as unknown as Parameters<typeof resolveCliModel>[0]["modelRuntime"];
const result = resolveCliModel({
cliModel: "sonnet:high",
modelRuntime: registry,
});
expect(result.error).toBeUndefined();
expect(result.model?.id).toBe("claude-sonnet-4-5");
expect(result.thinkingLevel).toBe("high");
});
test("prefers exact model id match over provider inference (OpenRouter-style ids)", () => {
const registry = {
getModels: () => allModels,
} as unknown as Parameters<typeof resolveCliModel>[0]["modelRuntime"];
const result = resolveCliModel({
cliModel: "openai/gpt-4o:extended",
modelRuntime: registry,
});
expect(result.error).toBeUndefined();
expect(result.model?.provider).toBe("openrouter");
expect(result.model?.id).toBe("openai/gpt-4o:extended");
});
test("does not strip invalid :suffix as thinking level in --model (treat as raw id)", () => {
const registry = {
getModels: () => allModels,
@@ -427,159 +516,49 @@ describe("resolveCliModel", () => {
expect(result.error).toContain("No models available");
});
test("prefers the sole authenticated provider for an ambiguous bare exact model id", () => {
const azureModel: Model<"anthropic-messages"> = {
...mockModels[1],
id: "gpt-5.6-sol",
name: "GPT 5.6 Sol",
provider: "azure-openai-responses",
};
const codexModel: Model<"anthropic-messages"> = {
...mockModels[1],
id: "gpt-5.6-sol",
name: "GPT 5.6 Sol",
provider: "openai-codex",
};
const registry = {
getModels: () => [azureModel, codexModel],
hasConfiguredAuth: (provider: string) => provider === "openai-codex",
} as unknown as Parameters<typeof resolveCliModel>[0]["modelRuntime"];
describe("duplicate raw ids behind a known provider prefix (#10160)", () => {
const direct = { ...mockModels[0], id: "claude-sonnet-4-5", provider: "anthropic" };
const openrouter = { ...mockModels[0], id: "anthropic/claude-opus-5.5", provider: "openrouter" };
const vercel = { ...mockModels[0], id: "anthropic/claude-opus-5.5", provider: "vercel-ai-gateway" };
const registry = (configured: string[]) =>
({
getModels: () => [direct, openrouter, vercel],
hasConfiguredAuth: (provider: string) => configured.includes(provider),
}) as unknown as Parameters<typeof resolveCliModel>[0]["modelRuntime"];
const result = resolveCliModel({
cliModel: "gpt-5.6-sol",
modelRuntime: registry,
test("selects the authenticated gateway, with or without a thinking suffix", () => {
for (const [cliModel, thinkingLevel] of [
["anthropic/claude-opus-5.5", undefined],
["anthropic/claude-opus-5.5:high", "high"],
] as const) {
const result = resolveCliModel({ cliModel, modelRuntime: registry(["vercel-ai-gateway"]) });
expect(result.error).toBeUndefined();
expect(result.model?.provider).toBe("vercel-ai-gateway");
expect(result.thinkingLevel).toBe(thinkingLevel);
}
});
expect(result.error).toBeUndefined();
expect(result.model?.provider).toBe("openai-codex");
expect(result.model?.id).toBe("gpt-5.6-sol");
});
test("reports ambiguity instead of choosing by catalog order", () => {
const result = resolveCliModel({ cliModel: "anthropic/claude-opus-5.5", modelRuntime: registry([]) });
test("requires an explicit provider for an ambiguous bare exact model id without a unique authenticated provider", () => {
const azureModel: Model<"anthropic-messages"> = {
...mockModels[1],
id: "gpt-5.6-sol",
name: "GPT 5.6 Sol",
provider: "azure-openai-responses",
};
const codexModel: Model<"anthropic-messages"> = {
...mockModels[1],
id: "gpt-5.6-sol",
name: "GPT 5.6 Sol",
provider: "openai-codex",
};
const registry = {
getModels: () => [azureModel, codexModel],
hasConfiguredAuth: () => false,
} as unknown as Parameters<typeof resolveCliModel>[0]["modelRuntime"];
const result = resolveCliModel({
cliModel: "gpt-5.6-sol",
modelRuntime: registry,
expect(result.model).toBeUndefined();
expect(result.error).toContain(
"openrouter/anthropic/claude-opus-5.5, vercel-ai-gateway/anthropic/claude-opus-5.5",
);
expect(result.error).toContain("No matching provider is authenticated. Use --provider or provider/model.");
});
expect(result.model).toBeUndefined();
expect(result.error).toContain('Model "gpt-5.6-sol" is ambiguous across providers');
expect(result.error).toContain("azure-openai-responses/gpt-5.6-sol");
expect(result.error).toContain("openai-codex/gpt-5.6-sol");
expect(result.error).toContain("Use --provider or provider/model");
});
test("does not let stored credentials choose the provider for --api-key", () => {
const result = resolveCliModel({
cliModel: "anthropic/claude-opus-5.5",
inlineApiKey: true,
modelRuntime: registry(["vercel-ai-gateway"]),
});
test("prefers provider/model split over gateway model with matching id", () => {
// When a user writes "zai/glm-5", and both a zai provider model (id: "glm-5")
// and a gateway model (id: "zai/glm-5") exist, prefer the zai provider model.
const zaiModel: Model<"anthropic-messages"> = {
id: "glm-5",
name: "GLM-5",
api: "anthropic-messages",
provider: "zai",
baseUrl: "https://open.bigmodel.cn/api/paas/v4",
reasoning: true,
input: ["text"],
cost: { input: 1, output: 2, cacheRead: 0.1, cacheWrite: 1 },
contextWindow: 128000,
maxTokens: 8192,
};
const gatewayModel: Model<"anthropic-messages"> = {
id: "zai/glm-5",
name: "GLM-5",
api: "anthropic-messages",
provider: "vercel-ai-gateway",
baseUrl: "https://ai-gateway.vercel.sh",
reasoning: true,
input: ["text"],
cost: { input: 1, output: 2, cacheRead: 0.1, cacheWrite: 1 },
contextWindow: 128000,
maxTokens: 8192,
};
const registry = {
getModels: () => [...allModels, zaiModel, gatewayModel],
hasConfiguredAuth: () => true,
} as unknown as Parameters<typeof resolveCliModel>[0]["modelRuntime"];
const result = resolveCliModel({
cliModel: "zai/glm-5",
modelRuntime: registry,
expect(result.model).toBeUndefined();
expect(result.error).toContain("--api-key does not choose a provider.");
});
expect(result.error).toBeUndefined();
expect(result.model?.provider).toBe("zai");
expect(result.model?.id).toBe("glm-5");
});
test("prefers an authenticated exact raw model id over an unauthenticated inferred provider", () => {
const commandcodeModel: Model<"anthropic-messages"> = {
id: "xiaomi/mimo-v2.5-pro",
name: "Xiaomi MiMo via Commandcode",
api: "anthropic-messages",
provider: "commandcode",
baseUrl: "https://example.invalid",
reasoning: false,
input: ["text"],
cost: { input: 1, output: 2, cacheRead: 0.1, cacheWrite: 1 },
contextWindow: 128000,
maxTokens: 8192,
};
const xiaomiModel: Model<"anthropic-messages"> = {
id: "mimo-v2.5-pro",
name: "Xiaomi MiMo",
api: "anthropic-messages",
provider: "xiaomi",
baseUrl: "https://api.xiaomimimo.com",
reasoning: false,
input: ["text"],
cost: { input: 1, output: 2, cacheRead: 0.1, cacheWrite: 1 },
contextWindow: 128000,
maxTokens: 8192,
};
const registry = {
getModels: () => [...allModels, commandcodeModel, xiaomiModel],
hasConfiguredAuth: (provider: string) => provider === "commandcode",
} as unknown as Parameters<typeof resolveCliModel>[0]["modelRuntime"];
const result = resolveCliModel({
cliModel: "xiaomi/mimo-v2.5-pro",
modelRuntime: registry,
});
expect(result.error).toBeUndefined();
expect(result.model?.provider).toBe("commandcode");
expect(result.model?.id).toBe("xiaomi/mimo-v2.5-pro");
});
test("resolves provider-prefixed fuzzy patterns (openrouter/qwen -> openrouter model)", () => {
const registry = {
getModels: () => allModels,
} as unknown as Parameters<typeof resolveCliModel>[0]["modelRuntime"];
const result = resolveCliModel({
cliModel: "openrouter/qwen",
modelRuntime: registry,
});
expect(result.error).toBeUndefined();
expect(result.model?.provider).toBe("openrouter");
expect(result.model?.id).toBe("qwen/qwen3-coder:exacto");
});
describe("custom model fallback with :thinking suffix (#5552)", () => {
@@ -708,7 +687,7 @@ describe("resolveCliModel", () => {
describe("default model selection", () => {
test("openai defaults track current models", () => {
expect(defaultModelPerProvider.openai).toBe("gpt-5.5");
expect(defaultModelPerProvider["openai-codex"]).toBe("gpt-5.5");
expect(defaultModelPerProvider["openai-codex"]).toBe("gpt-6.1-sol");
});
test("zai, minimax, cerebras, and ant-ling defaults track current models", () => {
@@ -720,11 +699,16 @@ describe("default model selection", () => {
expect(defaultModelPerProvider["ant-ling"]).toBe("Ring-2.6-1T");
});
test("built-in defaults exist in generated provider catalogs", () => {
test("built-in chat providers have defaults in their generated catalogs", () => {
for (const provider of getBuiltinProviders()) {
const chatModels = getBuiltinModels(provider);
const defaultId = defaultModelPerProvider[provider];
if (chatModels.length === 0) {
expect(defaultId, `${provider} has no chat models and should have no chat default`).toBeUndefined();
continue;
}
expect(
getBuiltinModels(provider).some((model) => model.id === defaultId),
chatModels.some((model) => model.id === defaultId),
`${provider} default ${defaultId} should exist in its generated catalog`,
).toBe(true);
}
@@ -735,30 +719,13 @@ describe("default model selection", () => {
});
test("xai default tracks current model", () => {
expect(defaultModelPerProvider.xai).toBe("grok-4.6");
expect(defaultModelPerProvider.xai).toBe("grok-4.7");
});
test("qwen token plan individual default tracks current model", () => {
expect(defaultModelPerProvider["qwen-token-plan-individual"]).toBe("qwen3.8-max");
});
test("findInitialModel accepts explicit provider custom model ids", async () => {
const registry = {
getModels: () => allModels,
} as unknown as Parameters<typeof findInitialModel>[0]["modelRuntime"];
const result = await findInitialModel({
cliProvider: "openrouter",
cliModel: "openrouter/openai/ghost-model",
scopedModels: [],
isContinuing: false,
modelRuntime: registry,
});
expect(result.model?.provider).toBe("openrouter");
expect(result.model?.id).toBe("openai/ghost-model");
});
test("findInitialModel selects ai-gateway default when available", async () => {
const aiGatewayModel: Model<"anthropic-messages"> = {
id: "anthropic/claude-opus-4-6",