d3b5340583
* fix: 9 unaddressed log-audit / run-audit findings Co-authored-by: Cursor <cursoragent@cursor.com> #815 entryPost stdlib-only imports; #823 MCP timeoutMs on checkout_pr/shell; #816 FREE_FALLBACK → opencode/big-pickle; #822 chunk GraphQL nodes ≤100; #817/#821 vip_audit 404 skip paths; #813 longer serializable retries; #818 run-context handler-entered log; #805 audit severity template. * fix: update footer test for big-pickle fallback slug Co-authored-by: Cursor <cursoragent@cursor.com> * fix: anneal round 1 — ghaCore getState casing, post-hook timeout Match @actions/core STATE_ key semantics (no uppercasing), cap postApiFetch at 30s, trim serializable retries to stay under GitHub's 10s webhook window, log Clerk failures in getUserTokenByGithubLogin. Co-authored-by: Cursor <cursoragent@cursor.com> * revert: drop run-context handler log (#818 deferred) The #692 client-side fix is already on main; residual SyntaxError hits are old action pins. Per-request log added noise without fixing anything. Co-authored-by: Cursor <cursoragent@cursor.com> * document per-issue Closes syntax for audit PRs GitHub only auto-closes the first issue when numbers are comma-separated; /audits and AGENTS.md now require Closes before each issue number. Co-authored-by: Cursor <cursoragent@cursor.com> * fix: anneal round 2 — outreach privacy, alert resilience, vertex cleanup Filter private repos from VIP authority output, harden console alert lines against DB failures, drop spoofable changesets body check, and unset GOOGLE_APPLICATION_CREDENTIALS after vertex credential cleanup. Co-authored-by: Cursor <cursoragent@cursor.com> * refactor: drop codexHome re-export of detectCodexRefresh Import detectCodexRefresh directly from codexRefreshDetect.ts everywhere; rename the unit test file to match. codexHome.ts stays install-only. Co-authored-by: Cursor <cursoragent@cursor.com> * fix: drop deprecated minimax-m2.5-free; add paid minimax-m2.5 Remove the deprecated free MiniMax promo from the catalog, docs, and tests. BYOK fallback and picker copy stay on opencode/big-pickle. Add opencode/minimax-m2.5 and openrouter/minimax-m2.5 for Zen BYOK and Router. Pin #816 regressions with freeFallbackCatalog and runErrorRenderer unit tests. Co-authored-by: Cursor <cursoragent@cursor.com> * fix: hidden minimax-m2.5-free fallback for stored slugs Re-add opencode/minimax-m2.5-free as a hidden fallback alias to big-pickle so repos with the legacy slug still resolve as free. Drop live Zen API experiment tests in freeFallbackCatalog.test.ts. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
301 lines
11 KiB
TypeScript
301 lines
11 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import {
|
|
DEFAULT_PROXY_MODEL,
|
|
getModelEnvVars,
|
|
getModelProvider,
|
|
isBedrockAnthropicId,
|
|
isVertexAnthropicId,
|
|
modelAliases,
|
|
parseModel,
|
|
providers,
|
|
resolveCliModel,
|
|
resolveDisplayAlias,
|
|
resolveModelSlug,
|
|
resolveOpenRouterModel,
|
|
} from "./models.ts";
|
|
|
|
describe("parseModel", () => {
|
|
it("parses provider/model format", () => {
|
|
const result = parseModel("anthropic/claude-opus");
|
|
expect(result).toEqual({ provider: "anthropic", model: "claude-opus" });
|
|
});
|
|
|
|
it("handles nested slashes (openrouter format)", () => {
|
|
const result = parseModel("openrouter/anthropic/claude-opus-4.6");
|
|
expect(result).toEqual({ provider: "openrouter", model: "anthropic/claude-opus-4.6" });
|
|
});
|
|
|
|
it("throws on invalid slug without slash", () => {
|
|
expect(() => parseModel("invalid")).toThrow("invalid model slug");
|
|
});
|
|
});
|
|
|
|
describe("getModelProvider", () => {
|
|
it("extracts provider from slug", () => {
|
|
expect(getModelProvider("anthropic/claude-opus")).toBe("anthropic");
|
|
expect(getModelProvider("openai/gpt")).toBe("openai");
|
|
expect(getModelProvider("google/gemini-pro")).toBe("google");
|
|
});
|
|
});
|
|
|
|
describe("getModelEnvVars", () => {
|
|
it("returns correct env vars for anthropic", () => {
|
|
expect(getModelEnvVars("anthropic/claude-opus")).toEqual([
|
|
"ANTHROPIC_API_KEY",
|
|
"CLAUDE_CODE_OAUTH_TOKEN",
|
|
]);
|
|
});
|
|
|
|
it("returns correct env vars for google (multiple)", () => {
|
|
const envVars = getModelEnvVars("google/gemini-pro");
|
|
expect(envVars).toContain("GOOGLE_GENERATIVE_AI_API_KEY");
|
|
expect(envVars).toContain("GEMINI_API_KEY");
|
|
});
|
|
|
|
it("returns empty array for unknown provider", () => {
|
|
expect(getModelEnvVars("unknown/model")).toEqual([]);
|
|
});
|
|
|
|
it("returns empty env vars for free opencode models", () => {
|
|
expect(getModelEnvVars("opencode/big-pickle")).toEqual([]);
|
|
expect(getModelEnvVars("opencode/mimo-v2-pro-free")).toEqual([]);
|
|
expect(getModelEnvVars("opencode/minimax-m2.5-free")).toEqual([]);
|
|
});
|
|
|
|
it("still requires OPENCODE_API_KEY for non-free opencode models", () => {
|
|
expect(getModelEnvVars("opencode/claude-opus")).toEqual(["OPENCODE_API_KEY"]);
|
|
expect(getModelEnvVars("opencode/minimax-m2.5")).toEqual(["OPENCODE_API_KEY"]);
|
|
expect(getModelEnvVars("opencode/gpt-5-nano")).toEqual(["OPENCODE_API_KEY"]);
|
|
});
|
|
});
|
|
|
|
describe("resolveModelSlug", () => {
|
|
it("resolves known alias to concrete specifier", () => {
|
|
const resolved = resolveModelSlug("anthropic/claude-opus");
|
|
expect(resolved).toBe("anthropic/claude-opus-4-7");
|
|
});
|
|
|
|
it("resolves openai alias", () => {
|
|
const resolved = resolveModelSlug("openai/gpt");
|
|
expect(resolved).toBe("openai/gpt-5.5");
|
|
});
|
|
|
|
it("returns the raw resolve for deprecated aliases (does not walk fallback)", () => {
|
|
expect(resolveModelSlug("openai/gpt-codex")).toBe("openai/gpt-5.3-codex");
|
|
});
|
|
|
|
it("returns undefined for unknown slug", () => {
|
|
expect(resolveModelSlug("unknown/model")).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe("resolveCliModel", () => {
|
|
it("returns same as resolveModelSlug (models.dev specifier)", () => {
|
|
const slug = "anthropic/claude-opus";
|
|
expect(resolveCliModel(slug)).toBe(resolveModelSlug(slug));
|
|
});
|
|
|
|
it("returns undefined for unknown slug", () => {
|
|
expect(resolveCliModel("bogus/nope")).toBeUndefined();
|
|
});
|
|
|
|
it("walks fallback chain for deprecated deepseek aliases", () => {
|
|
expect(resolveCliModel("deepseek/deepseek-reasoner")).toBe("deepseek/deepseek-v4-pro");
|
|
expect(resolveCliModel("deepseek/deepseek-chat")).toBe("deepseek/deepseek-v4-flash");
|
|
});
|
|
|
|
it("walks fallback chain for deprecated openai codex aliases", () => {
|
|
expect(resolveCliModel("openai/gpt-codex")).toBe("openai/gpt-5.5");
|
|
expect(resolveCliModel("openai/gpt-codex-mini")).toBe("openai/gpt-5.4-mini");
|
|
expect(resolveCliModel("opencode/gpt-codex")).toBe("opencode/gpt-5.5");
|
|
expect(resolveCliModel("openrouter/gpt-codex")).toBe("openrouter/openai/gpt-5.5");
|
|
});
|
|
|
|
it("walks fallback chain for hidden deprecated minimax-m2.5-free", () => {
|
|
expect(resolveCliModel("opencode/minimax-m2.5-free")).toBe("opencode/big-pickle");
|
|
});
|
|
});
|
|
|
|
describe("resolveDisplayAlias", () => {
|
|
it("returns the alias itself for a non-deprecated slug", () => {
|
|
const alias = resolveDisplayAlias("anthropic/claude-opus");
|
|
expect(alias?.slug).toBe("anthropic/claude-opus");
|
|
expect(alias?.displayName).toBe("Claude Opus");
|
|
});
|
|
|
|
it("walks fallback chain to terminal alias for deprecated slug", () => {
|
|
const alias = resolveDisplayAlias("openai/gpt-codex");
|
|
expect(alias?.slug).toBe("openai/gpt");
|
|
expect(alias?.displayName).toBe("GPT");
|
|
});
|
|
|
|
it("walks fallback chain for deepseek-reasoner -> deepseek-pro", () => {
|
|
const alias = resolveDisplayAlias("deepseek/deepseek-reasoner");
|
|
expect(alias?.slug).toBe("deepseek/deepseek-pro");
|
|
expect(alias?.displayName).toBe("DeepSeek Pro");
|
|
});
|
|
|
|
it("returns undefined for unknown slug", () => {
|
|
expect(resolveDisplayAlias("bogus/nope")).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe("DEFAULT_PROXY_MODEL", () => {
|
|
it("tracks moonshotai/kimi-k2 openRouterResolve", () => {
|
|
expect(DEFAULT_PROXY_MODEL).toBe(resolveOpenRouterModel("moonshotai/kimi-k2"));
|
|
});
|
|
});
|
|
|
|
describe("resolveOpenRouterModel", () => {
|
|
it("returns the openrouter specifier for a non-deprecated alias", () => {
|
|
expect(resolveOpenRouterModel("anthropic/claude-opus")).toBe(
|
|
"openrouter/anthropic/claude-opus-4.7"
|
|
);
|
|
});
|
|
|
|
it("walks fallback chain for deprecated deepseek aliases", () => {
|
|
expect(resolveOpenRouterModel("deepseek/deepseek-reasoner")).toBe(
|
|
"openrouter/deepseek/deepseek-v4-pro"
|
|
);
|
|
expect(resolveOpenRouterModel("deepseek/deepseek-chat")).toBe(
|
|
"openrouter/deepseek/deepseek-v4-flash"
|
|
);
|
|
expect(resolveOpenRouterModel("openrouter/deepseek-chat")).toBe(
|
|
"openrouter/deepseek/deepseek-v4-flash"
|
|
);
|
|
});
|
|
|
|
it("walks fallback chain for deprecated openai codex aliases", () => {
|
|
expect(resolveOpenRouterModel("openai/gpt-codex")).toBe("openrouter/openai/gpt-5.5");
|
|
expect(resolveOpenRouterModel("openai/gpt-codex-mini")).toBe("openrouter/openai/gpt-5.4-mini");
|
|
});
|
|
|
|
it("returns undefined for free opencode models with no openrouter equivalent", () => {
|
|
expect(resolveOpenRouterModel("opencode/big-pickle")).toBeUndefined();
|
|
});
|
|
|
|
it("returns undefined for unknown slug", () => {
|
|
expect(resolveOpenRouterModel("bogus/nope")).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe("modelAliases registry", () => {
|
|
it("has at least one model per provider", () => {
|
|
for (const providerKey of Object.keys(providers)) {
|
|
const providerModels = modelAliases.filter((a) => a.provider === providerKey);
|
|
expect(providerModels.length).toBeGreaterThan(0);
|
|
}
|
|
});
|
|
|
|
it("has exactly one preferred model per provider", () => {
|
|
for (const providerKey of Object.keys(providers)) {
|
|
// routing-only providers (bedrock) deliberately have no preferred
|
|
// model — the user picks the actual model via a per-run env var, so
|
|
// there's no "preferred default" to surface to auto-select.
|
|
const aliases = modelAliases.filter((a) => a.provider === providerKey);
|
|
if (aliases.every((a) => a.routing)) continue;
|
|
const preferred = aliases.filter((a) => a.preferred);
|
|
expect(preferred.length, `${providerKey} should have exactly 1 preferred model`).toBe(1);
|
|
}
|
|
});
|
|
|
|
it("all slugs follow provider/model format", () => {
|
|
for (const alias of modelAliases) {
|
|
expect(alias.slug).toContain("/");
|
|
const parsed = parseModel(alias.slug);
|
|
expect(parsed.provider).toBe(alias.provider);
|
|
}
|
|
});
|
|
|
|
it("all resolve values follow provider/model format", () => {
|
|
for (const alias of modelAliases) {
|
|
// routing slugs use a sentinel `resolve` (e.g. "bedrock") that's never
|
|
// passed to a CLI directly — the harness reads a separate env var to
|
|
// get the real model ID. format check doesn't apply.
|
|
if (alias.routing) continue;
|
|
expect(alias.resolve).toContain("/");
|
|
}
|
|
});
|
|
|
|
it("slugs are unique", () => {
|
|
const slugs = modelAliases.map((a) => a.slug);
|
|
expect(new Set(slugs).size).toBe(slugs.length);
|
|
});
|
|
});
|
|
|
|
describe("isBedrockAnthropicId", () => {
|
|
it("matches geo-prefixed Anthropic foundation IDs", () => {
|
|
expect(isBedrockAnthropicId("us.anthropic.claude-opus-4-7")).toBe(true);
|
|
expect(isBedrockAnthropicId("eu.anthropic.claude-sonnet-4-6")).toBe(true);
|
|
expect(isBedrockAnthropicId("global.anthropic.claude-haiku-4-5-20251001-v1:0")).toBe(true);
|
|
});
|
|
|
|
it("matches in-region Anthropic foundation IDs", () => {
|
|
expect(isBedrockAnthropicId("anthropic.claude-opus-4-7")).toBe(true);
|
|
});
|
|
|
|
it("rejects non-Anthropic foundation IDs", () => {
|
|
expect(isBedrockAnthropicId("amazon.nova-pro-v1:0")).toBe(false);
|
|
expect(isBedrockAnthropicId("us.meta.llama4-scout-17b-instruct-v1:0")).toBe(false);
|
|
expect(isBedrockAnthropicId("deepseek.v3.2")).toBe(false);
|
|
});
|
|
|
|
// regression: PR #720 review caught that a substring-only match was
|
|
// fragile for inference-profile ARNs (which BEDROCK_MODEL_ID accepts per
|
|
// the AWS docs). ARN names are user-chosen — both directions of the
|
|
// heuristic could break depending on what name the operator picked.
|
|
// We anchor on a discrete dot-segment match (case-insensitive) instead.
|
|
it("ignores 'anthropic' substrings inside non-segment text", () => {
|
|
// ARN whose user-chosen profile name happens to contain "anthropic" as
|
|
// part of a longer word — would route to claude-code under naive
|
|
// includes("anthropic") even though the backing model is unknown.
|
|
expect(
|
|
isBedrockAnthropicId(
|
|
"arn:aws:bedrock:us-east-2:123456789012:application-inference-profile/my-anthropicish-profile"
|
|
)
|
|
).toBe(false);
|
|
});
|
|
|
|
it("matches when 'anthropic' appears as its own dot-segment in ARN", () => {
|
|
// ARN whose profile name embeds the foundation segment correctly —
|
|
// operator chose to surface the backing model in the name.
|
|
expect(
|
|
isBedrockAnthropicId(
|
|
"arn:aws:bedrock:us-east-2:123456789012:application-inference-profile/anthropic.claude-opus-4-7"
|
|
)
|
|
).toBe(true);
|
|
});
|
|
|
|
it("is case-insensitive", () => {
|
|
expect(isBedrockAnthropicId("US.ANTHROPIC.CLAUDE-OPUS-4-7")).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("isVertexAnthropicId", () => {
|
|
it("matches Claude Vertex IDs by anchored prefix", () => {
|
|
expect(isVertexAnthropicId("claude-opus-4-1@20250805")).toBe(true);
|
|
});
|
|
|
|
it("rejects Gemini IDs", () => {
|
|
expect(isVertexAnthropicId("gemini-2.5-pro")).toBe(false);
|
|
});
|
|
|
|
it("ignores Anthropic substrings outside the prefix", () => {
|
|
expect(isVertexAnthropicId("publishers/anthropic/models/claude-opus-4-1")).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("providers registry", () => {
|
|
it("every provider has envVars", () => {
|
|
for (const [key, config] of Object.entries(providers)) {
|
|
expect(config.envVars.length, `${key} should have env vars`).toBeGreaterThan(0);
|
|
}
|
|
});
|
|
|
|
it("every provider has a displayName", () => {
|
|
for (const [key, config] of Object.entries(providers)) {
|
|
expect(config.displayName, `${key} should have a displayName`).toBeTruthy();
|
|
}
|
|
});
|
|
});
|