chore(models): move the opus alias to claude-opus-5

Claude Opus 5 released today — same $5/$25 sticker, 1M context; Opus 4.8
moves to legacy. The pricing table gains a dedicated claude-opus-5
fragment (the claude-opus-4 substring doesn't cover it, so without the
row the fleet's Opus usage would silently cost-track as $0 — exactly
what test_opus_is_priced now guards).
This commit is contained in:
Renn F
2026-07-25 00:41:13 +02:00
parent 9b407d764d
commit 3c87596685
17 changed files with 29 additions and 26 deletions
+1 -1
View File
@@ -30,7 +30,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
### Changed
- **The `opus` alias upgrades from Claude Opus 4.6 to Claude Opus 4.8.** `MODEL_MAP["opus"]` now resolves to `claude-opus-4-8` — the newest Opus tier, at the same price as 4.6 — so every Opus-routed role (PR reviewer, Auditor, Board, CEO, Intake, Secretary) rides it at next spawn. Pricing already covered the id via the `claude-opus-4` fragment; a new `test_opus_is_priced` guard keeps the alias from ever pointing at an unpriced model.
- **The `opus` alias upgrades from Claude Opus 4.6 to Claude Opus 5.** `MODEL_MAP["opus"]` now resolves to `claude-opus-5` — the newest Opus tier, released 2026-07-25 at the same $5/$25 price — so every Opus-routed role (PR reviewer, Auditor, Board, CEO, Intake, Secretary) rides it at next spawn. The pricing table gains a dedicated `claude-opus-5` fragment (the `claude-opus-4` substring doesn't cover it — without the row, Opus fleet usage would silently cost-track as $0), and a new `test_opus_is_priced` guard keeps the alias from ever pointing at an unpriced model.
- **Panel dialogs converge on one shape: one disclosure primitive, DialogFooter everywhere, three dialog widths.** `collapsible-section` becomes the single sectioned-disclosure primitive — task dialogs' raw `Collapsible` and create-project's ad-hoc `showAdvanced` both move onto it; every hand-rolled dialog footer becomes `DialogFooter`; dialog widths collapse from ten ad-hoc classes to three named sizes, with deliberate outliers annotated. No behavioral change.
### Fixed
+1 -1
View File
@@ -79,7 +79,7 @@ pnpm test
| RAG Engine | in-house (asyncpg + pgvector, hybrid retrieval) |
| Cache/Queue | Redis |
| Container Runtime | Docker + Docker Compose |
| Cloud LLM | Claude API (claude-opus-4-8) + xAI Grok (official `grok` CLI, SuperGrok subscription) + OpenAI (official `codex` CLI, ChatGPT subscription) + Google Gemini (official `gemini` CLI, OAuth login) |
| Cloud LLM | Claude API (claude-opus-5) + xAI Grok (official `grok` CLI, SuperGrok subscription) + OpenAI (official `codex` CLI, ChatGPT subscription) + Google Gemini (official `gemini` CLI, OAuth login) |
| Local LLM | Ollama (glm-5.2:cloud for RAG/hybrid retrieval) |
| Embeddings | qwen3-embedding:0.6b (1024 dim) |
| Frontend | Next.js 16 + TypeScript + Tailwind + Radix UI (in `panel/`) |
+1 -1
View File
@@ -5134,7 +5134,7 @@ The Pydantic/dataclass domain surface of RoboCo — the typed contract the API,
| `OrchestratorAgentState` | StrEnum | runtime.py:15 | offline/starting/active/waiting_short/waiting_long/idle/stopping |
| `AgentInstance` | dataclass | runtime.py:70 | Running Claude Code container record + `usage_session_id` |
| `SpawnGitContext` | dataclass | runtime.py:28 | Git context for spawn (project_slug, branch, `task_short_id` for worktree) |
| `MODEL_MAP` | dict | runtime.py:106 | short-name→full Claude id (opus→claude-opus-4-8, sonnet→claude-sonnet-5, haiku→…) |
| `MODEL_MAP` | dict | runtime.py:106 | short-name→full Claude id (opus→claude-opus-5, sonnet→claude-sonnet-5, haiku→…) |
| `ROLE_MODEL_MAP` | dict | runtime.py:120 | per-role default tier: developer/qa/documenter/cell_pm/main_pm→sonnet, pr_reviewer/auditor/board/ceo/prompter/secretary→opus. **Haiku retired from every delivery-lifecycle role (#680, 2026-07-24)**: qa/documenter moved haiku→sonnet — haiku can't reliably emit the structured envelopes the lifecycle runs on (`pass_review`'s per-AC `criteria_verified`, `delegate`'s `covers_parent_criteria`, the findings ledger), so a haiku QA/PM claims, gets validation-rejected, idles, respawns, and loops without progress. A structured-verb capability floor (`_below_capability_floor`/`_floor_below_capability`, `roboco/services/llm.py`) additionally upgrades ANY below-floor Anthropic assignment to sonnet at resolution time — from a pin, a plain ROLE row, or a future map edit — so the floor can't be silently reintroduced. Non-Anthropic providers are untouched (an Anthropic-tier floor, not a provider policy). |
| `ROLE_EFFORT_MAP` | dict | runtime.py | per-role `CLAUDE_CODE_EFFORT_LEVEL` override injected at spawn; **empty/inert by default** (opt-in per role after verifying the level moves usage) |
| `MODEL_CATALOG` | tuple | llm_catalog.py:67 | Settings-dropdown source of truth; Anthropic entries derived from `MODEL_MAP` |
+1 -1
View File
@@ -83,7 +83,7 @@ The Pydantic/dataclass domain surface of RoboCo — the typed contract the API,
| `OrchestratorAgentState` | StrEnum | runtime.py:15 | offline/starting/active/waiting_short/waiting_long/idle/stopping |
| `AgentInstance` | dataclass | runtime.py:70 | Running Claude Code container record + `usage_session_id` |
| `SpawnGitContext` | dataclass | runtime.py:28 | Git context for spawn (project_slug, branch, `task_short_id` for worktree) |
| `MODEL_MAP` | dict | runtime.py:106 | short-name→full Claude id (opus→claude-opus-4-8, sonnet→claude-sonnet-5, haiku→…) |
| `MODEL_MAP` | dict | runtime.py:106 | short-name→full Claude id (opus→claude-opus-5, sonnet→claude-sonnet-5, haiku→…) |
| `ROLE_MODEL_MAP` | dict | runtime.py:120 | per-role default tier: developer/qa/documenter/cell_pm/main_pm→sonnet, pr_reviewer/auditor/board/ceo/prompter/secretary→opus. **Haiku retired from every delivery-lifecycle role (#680, 2026-07-24)**: qa/documenter moved haiku→sonnet — haiku can't reliably emit the structured envelopes the lifecycle runs on (`pass_review`'s per-AC `criteria_verified`, `delegate`'s `covers_parent_criteria`, the findings ledger), so a haiku QA/PM claims, gets validation-rejected, idles, respawns, and loops without progress. A structured-verb capability floor (`_below_capability_floor`/`_floor_below_capability`, `roboco/services/llm.py`) additionally upgrades ANY below-floor Anthropic assignment to sonnet at resolution time — from a pin, a plain ROLE row, or a future map edit — so the floor can't be silently reintroduced. Non-Anthropic providers are untouched (an Anthropic-tier floor, not a provider policy). |
| `ROLE_EFFORT_MAP` | dict | runtime.py | per-role `CLAUDE_CODE_EFFORT_LEVEL` override injected at spawn; **empty/inert by default** (opt-in per role after verifying the level moves usage) |
| `MODEL_CATALOG` | tuple | llm_catalog.py:67 | Settings-dropdown source of truth; Anthropic entries derived from `MODEL_MAP` |
@@ -33,9 +33,9 @@ const {
} = vi.hoisted(() => ({
catalog: vi.fn(async () => [
{
model_name: "claude-opus-4-8",
model_name: "claude-opus-5",
provider_type: "anthropic",
display_name: "Claude Opus 4.8",
display_name: "Claude Opus 5",
},
{
model_name: "grok-build-0.1",
@@ -719,14 +719,14 @@ describe("AIRoutingCard", () => {
const devLow = screen.getByTestId("complexity-select-developer-low");
fireEvent.click(
await within(devLow).findByRole("option", { name: "Claude Opus 4.8" }),
await within(devLow).findByRole("option", { name: "Claude Opus 5" }),
);
await waitFor(() =>
expect(setComplexityOverride).toHaveBeenCalledWith({
role: "developer",
complexity: "low",
model_name: "claude-opus-4-8",
model_name: "claude-opus-5",
}),
);
});
@@ -764,7 +764,7 @@ describe("AIRoutingCard", () => {
const devLow = screen.getByTestId("complexity-select-developer-low");
fireEvent.click(
await within(devLow).findByRole("option", { name: "Claude Opus 4.8" }),
await within(devLow).findByRole("option", { name: "Claude Opus 5" }),
);
await waitFor(() => expect(setComplexityOverride).toHaveBeenCalled());
+1 -1
View File
@@ -105,7 +105,7 @@ export const DEMO_TEAM_USAGE: TeamUsageRow[] = (
export const DEMO_MODEL_USAGE: ModelUsageSlice[] = (
[
["claude-opus-4-8", 42.1],
["claude-opus-5", 42.1],
["glm-5.2:cloud", 16.8],
["grok-build", 7.64],
] as Array<[string, number]>
+4 -1
View File
@@ -57,11 +57,14 @@ _ANTHROPIC_FRAGMENTS = ("claude", "opus", "sonnet", "haiku")
# Cache read is charged at ~10 % of the input price.
# Cache write is charged at ~25 % of the input price.
#
# Match on *substring* of model name so "claude-opus-4-8" and "opus" both
# Match on *substring* of model name so "claude-opus-5" and "opus" both
# resolve to the same tier.
# ---------------------------------------------------------------------------
_PRICING: list[tuple[str, float, float, float, float]] = [
# (fragment, input/1M, output/1M, cache_read/1M, cache_write/1M)
# Opus 5 — same sticker as the Opus 4 family; needs its own fragment
# because "claude-opus-4" doesn't substring-match it.
("claude-opus-5", 5.00, 25.00, 0.50, 6.25),
# Opus 4 family
("claude-opus-4", 5.00, 25.00, 0.50, 6.25),
# Sonnet 4 / 3.7 / 3.5 family
+2 -2
View File
@@ -7,7 +7,7 @@ the router maps back to the correct pre-seeded provider row.
**Anthropic entries derive from `runtime.MODEL_MAP`** that's the single
source of truth for which Claude versions are supported. Bumping a model
version there (e.g. `claude-opus-4-8` a newer id) updates the
version there (e.g. `claude-opus-5` a newer id) updates the
catalog automatically without a second edit here.
Ollama Cloud entries are hand-maintained because Ollama's cloud tags
@@ -46,7 +46,7 @@ def _build_anthropic_entries() -> tuple[CatalogEntry, ...]:
Keeps the UI honest: a user picking "Claude Opus" sees exactly which
underlying Claude Code model id will be used at spawn
(e.g. "Claude Opus · claude-opus-4-8"), so version bumps in
(e.g. "Claude Opus · claude-opus-5"), so version bumps in
`runtime.MODEL_MAP` are immediately visible.
"""
entries: list[CatalogEntry] = []
+1 -1
View File
@@ -110,7 +110,7 @@ class WaitingRecord:
# Model mapping for cost optimization
MODEL_MAP: dict[str, str] = {
"opus": "claude-opus-4-8",
"opus": "claude-opus-5",
"sonnet": "claude-sonnet-5",
"haiku": "claude-haiku-4-5-20251001",
}
+1
View File
@@ -565,6 +565,7 @@ class TestProviderAwareness:
for name in (
"claude-opus-4-6",
"claude-opus-4-8",
"claude-opus-5",
"claude-fable-5",
"opus",
"sonnet",
+1 -1
View File
@@ -106,7 +106,7 @@ def test_build_anthropic_entries_skips_missing_models() -> None:
# Patch MODEL_MAP to be missing one entry → loop hits the `continue` branch.
with patch(
"roboco.models.llm_catalog.MODEL_MAP",
{"opus": "claude-opus-4-8"}, # sonnet+haiku missing
{"opus": "claude-opus-5"}, # sonnet+haiku missing
):
entries = _build_anthropic_entries()
# Only one entry survives — opus.
+3 -4
View File
@@ -12,10 +12,9 @@ def test_sonnet_alias_resolves_to_sonnet_5() -> None:
assert MODEL_MAP["sonnet"] == "claude-sonnet-5"
def test_opus_alias_resolves_to_opus_4_8() -> None:
# CEO upgrade to the newest Opus tier ("Opus 5" does not exist — the
# Claude 5 family is Fable/Mythos/Sonnet; Opus tops out at 4.8).
assert MODEL_MAP["opus"] == "claude-opus-4-8"
def test_opus_alias_resolves_to_opus_5() -> None:
# CEO upgrade to the newest Opus tier (Opus 5, released 2026-07-25).
assert MODEL_MAP["opus"] == "claude-opus-5"
def test_sonnet_5_is_priced() -> None:
+2 -2
View File
@@ -56,7 +56,7 @@ def _spec(**overrides: Any) -> _IntakeRunSpec:
},
"session_id": "sess-abc",
"cwd": "/data/workspaces/roboco/board/intake-1",
"cli_model": "claude-opus-4-8",
"cli_model": "claude-opus-5",
"api_url": "http://roboco-orchestrator:8000",
"provider_base_url": None,
"provider_auth_token": None,
@@ -108,7 +108,7 @@ class TestBuildIntakeRunCmd:
assert "ROBOCO_WORKSPACE=/data/workspaces/roboco/board/intake-1" in cmd
assert "ROBOCO_API_URL=http://roboco-orchestrator:8000" in cmd
assert "ROBOCO_AGENT_ID=intake-1" in cmd
assert "CLAUDE_CODE_SUBAGENT_MODEL=claude-opus-4-8" in cmd
assert "CLAUDE_CODE_SUBAGENT_MODEL=claude-opus-5" in cmd
def test_mounts_prompt_and_workspaces(self) -> None:
cmd = AgentOrchestrator._build_intake_run_cmd(_spec())
@@ -99,7 +99,7 @@ class TestBuildManifestForAgent:
mock_settings.manifest_host_dir = str(tmp_path)
mock_settings.workspaces_root = str(tmp_path / "workspaces")
result = _build_manifest_for_agent("be-dev-1", "claude-opus-4-8")
result = _build_manifest_for_agent("be-dev-1", "claude-opus-5")
assert result is not None
data = json.loads(result.read_text())
+1 -1
View File
@@ -87,7 +87,7 @@ class TestBuildForRole:
role=role,
team="board",
workspace_path=Path("/tmp/x"),
agent_model="claude-opus-4-8",
agent_model="claude-opus-5",
)
)
assert "read_messages" in m.do_tools
+1 -1
View File
@@ -182,7 +182,7 @@ async def test_log_agent_event_resolves_slug_to_uuid(
event_type="agent.spawned",
agent_slug=slug,
task_id=task_id,
details={"container_id": "abc123def456", "model": "claude-opus-4-8"},
details={"container_id": "abc123def456", "model": "claude-opus-5"},
)
# Read back the row.
+2 -2
View File
@@ -945,7 +945,7 @@ class TestGetRecentSessions:
row = MagicMock()
row.id = sid
row.agent_slug = "product-owner"
row.model = "claude-opus-4-8"
row.model = "claude-opus-5"
row.started_at = datetime.datetime(2026, 6, 11, 20, 41, tzinfo=datetime.UTC)
row.ended_at = datetime.datetime(2026, 6, 11, 20, 42, tzinfo=datetime.UTC)
row.tokens_input = exp_in
@@ -966,7 +966,7 @@ class TestGetRecentSessions:
s = out[0]
assert s["id"] == str(sid)
assert s["agent_slug"] == "product-owner"
assert s["model"] == "claude-opus-4-8"
assert s["model"] == "claude-opus-5"
assert s["tokens_input"] == exp_in
assert s["tokens_output"] == exp_out
assert s["tokens_cache"] == exp_cr + exp_cw