Files
Michael Ramos b752080204 feat(ai): add Claude Opus 5 across providers and review agents (#1151)
* feat(ai): add Claude Opus 5 to the model catalogs

Adds `claude-opus-5` to both static Claude model lists:

- `packages/ai/providers/claude-agent-sdk.ts` — the Ask AI picker
- `packages/ui/components/AgentsTab.tsx` — the review-agent picker, which
  `TOUR_CLAUDE_MODELS` spreads, so tour and guide pick it up too

No `claude-opus-5[1m]` variant: the `[1m]` suffix opts the 4.x models into
the larger context window, and the 5-series models are already 1M by default
— which is why the existing `claude-fable-5` and `claude-sonnet-5` entries
carry no variant either. A test pins that invariant.

Guide, tour, and marker review need no change — they pass the picked model
straight through to the CLI with no server-side allowlist.

Tests cover the Bedrock/Vertex family matcher accepting the new bare alias
(a matcher keyed on "opus-4" would have silently dropped it) and pin the
existing Fable fallthrough, which has no ANTHROPIC_DEFAULT_*_MODEL of its own.

Also refreshes the marketing Ask AI model list, which had gone stale at
Sonnet 4.6 / Opus 4.6 / Haiku 4.5.

* feat(ai): default Claude review agents to Opus 5

Bumps the two Claude review-agent defaults from Opus 4.7 to Opus 5:

- `DEFAULT_CLAUDE_MODEL` (packages/ui/hooks/useAgentSettings.ts) — the
  review-agent picker's default for users with no saved cookie preference
- `buildClaudeCommand`'s default parameter (packages/server/claude-review.ts)
  — the server-side fallback when a job arrives with no model

BEHAVIOR CHANGE: users who have never picked a Claude review model move from
Opus 4.7 to Opus 5. Saved picks are untouched — the cookie already holds an
explicit model for anyone who has chosen one.

Kept deliberately separate from the catalog commit so it can be reverted on
its own if the maintainer prefers to ship the new models as options only.

Ask AI's default (`DEFAULT_MODEL` in claude-agent-sdk.ts) stays on Sonnet 5,
and the tour/guide defaults stay on the `sonnet` latest-alias — those are
interactive, latency-sensitive surfaces where the cheaper tier is the
intentional choice.
2026-07-29 08:51:33 -07:00

114 lines
4.3 KiB
TypeScript

import { describe, expect, test } from "bun:test";
import { CLAUDE_MODELS, CODEX_MODELS, TOUR_CLAUDE_MODELS, codexReasoningOptions } from "./AgentsTab";
const catalogEntry = (value: string) => CODEX_MODELS.find((m) => m.value === value);
describe("CLAUDE_MODELS catalog", () => {
test("offers the current 5-series flagships", () => {
for (const value of ["claude-fable-5", "claude-opus-5", "claude-sonnet-5"]) {
expect(CLAUDE_MODELS.some((m) => m.value === value)).toBe(true);
}
expect(CLAUDE_MODELS.find((m) => m.value === "claude-opus-5")?.label).toBe("Opus 5");
});
// The 5-series models are 1M-context by default, so they carry no `[1m]`
// variant — that suffix exists only to opt the older 4.x models into the
// larger window. Pinned so a future entry doesn't invent `claude-opus-5[1m]`.
test("5-series entries have no [1m] context variant", () => {
for (const { value } of CLAUDE_MODELS) {
if (/-5(\[|$)/.test(value)) expect(value).not.toContain("[1m]");
}
});
test("tour/guide catalog inherits every review model plus the latest aliases", () => {
for (const { value } of CLAUDE_MODELS) {
expect(TOUR_CLAUDE_MODELS.some((m) => m.value === value)).toBe(true);
}
for (const alias of ["sonnet", "opus", "fable"]) {
expect(TOUR_CLAUDE_MODELS.some((m) => m.value === alias)).toBe(true);
}
});
});
test("uses the canonical GPT-5.6 Sol model ID", () => {
expect(catalogEntry("gpt-5.6-sol")?.label).toBe("GPT-5.6 Sol");
expect(CODEX_MODELS.some(({ value }) => value === "gpt-5.6")).toBe(false);
});
describe("CODEX_MODELS catalog", () => {
test("omits models rejected or API-shut-down for every auth mode", () => {
// gpt-5.3-codex: ChatGPT-account Codex rejects it outright. The other
// three: API-level shutdown 2026-07-23 per OpenAI's deprecations page.
for (const value of ["gpt-5.3-codex", "gpt-5.2-codex", "gpt-5.1-codex-max", "gpt-5.1-codex-mini"]) {
expect(CODEX_MODELS.some((m) => m.value === value)).toBe(false);
}
});
test("keeps gpt-5.2 (still API-valid) with the historical effort set", () => {
expect(catalogEntry("gpt-5.2")?.efforts).toEqual(["low", "medium", "high", "xhigh"]);
});
test("GPT-5.6 family carries the CLI catalog's efforts and defaults", () => {
expect(catalogEntry("gpt-5.6-sol")).toEqual({
value: "gpt-5.6-sol",
label: "GPT-5.6 Sol",
efforts: ["low", "medium", "high", "xhigh", "max", "ultra"],
defaultEffort: "low",
});
expect(catalogEntry("gpt-5.6-terra")).toEqual({
value: "gpt-5.6-terra",
label: "GPT-5.6 Terra",
efforts: ["low", "medium", "high", "xhigh", "max", "ultra"],
defaultEffort: "medium",
});
expect(catalogEntry("gpt-5.6-luna")).toEqual({
value: "gpt-5.6-luna",
label: "GPT-5.6 Luna",
efforts: ["low", "medium", "high", "xhigh", "max"],
defaultEffort: "medium",
});
});
test("spark defaults to high; 5.5/5.4 default to medium", () => {
expect(catalogEntry("gpt-5.3-codex-spark")?.defaultEffort).toBe("high");
expect(catalogEntry("gpt-5.5")?.defaultEffort).toBe("medium");
expect(catalogEntry("gpt-5.4")?.defaultEffort).toBe("medium");
expect(catalogEntry("gpt-5.4-mini")?.defaultEffort).toBe("medium");
});
test("no model offers minimal, and every default effort is supported", () => {
for (const model of CODEX_MODELS) {
expect(model.efforts).not.toContain("minimal");
expect(model.efforts).toContain(model.defaultEffort);
}
});
});
describe("codexReasoningOptions", () => {
test("offers only the selected model's efforts, with Max/Ultra labels", () => {
expect(codexReasoningOptions("gpt-5.6-sol")).toEqual([
{ value: "low", label: "Low" },
{ value: "medium", label: "Medium" },
{ value: "high", label: "High" },
{ value: "xhigh", label: "XHigh" },
{ value: "max", label: "Max" },
{ value: "ultra", label: "Ultra" },
]);
expect(codexReasoningOptions("gpt-5.5").map(({ value }) => value)).toEqual([
"low",
"medium",
"high",
"xhigh",
]);
});
test("falls back to the low..xhigh set for unknown models", () => {
expect(codexReasoningOptions("future-codex-model").map(({ value }) => value)).toEqual([
"low",
"medium",
"high",
"xhigh",
]);
});
});