From fdd6dadf96c029aaa4204ee55caf16205ef67be0 Mon Sep 17 00:00:00 2001 From: dtzp555-max Date: Mon, 27 Jul 2026 07:37:31 +1000 Subject: [PATCH] feat(models): add Claude Opus 5 and repoint the `opus` alias to it (#192) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(models): add Claude Opus 5 and repoint the `opus` alias to it Adds `claude-opus-5` to models.json (the SPOT) and moves the `opus` alias from `claude-opus-4-8` to it. `/v1/models` goes 6 -> 7; OpenClaw picks the new entry up on the next `ocp update` via scripts/sync-openclaw.mjs. server.mjs is NOT modified by this PR — models.json is the single source of truth (ADR 0003) and every consumer (server.mjs MODEL_MAP, setup.mjs, sync-openclaw.mjs, ocp-connect) derives from it. ocp-connect needs no change either: its metadata table matches on the `claude-opus` FAMILY prefix, which `claude-opus-5` hits (the guard added in PR #152 review). Alignment evidence. The claim "the CLI itself defaults opus -> claude-opus-5" is verified from the compiled CLI binary 2.1.220 via `strings`, the same protocol used for #152/#168: latest_per_family:{fable:"claude-fable-5",opus:"claude-opus-5", sonnet:"claude-sonnet-5",haiku:"claude-haiku-4-5"} The same registry entry gives context:{window:1e6,native_1m:true}, max_output_tokens:{default:64000,upper:128000} and pricing:"tier_5_25" -- i.e. identical $5/$25 per MTok to Opus 4.8, so the alias repoint carries no cost change. Availability confirmed with a live subscription-pool completion (`claude -p --model claude-opus-5` -> "OK"). This mirrors #168 (sonnet -> sonnet-5), which established the precedent that following the CLI's own latest_per_family is the correct default behavior. contextWindow is deliberately 200000, not the native 1M: 1. MAX_PROMPT_CHARS is a SINGLE GLOBAL budget, not per-model. derivePromptCharBudget (lib/prompt.mjs) returns `max(floor, max(...contextWindows) * 3)` across ALL entries, so a 1M entry would lift the truncation ceiling to 3,000,000 chars for claude-haiku-4-5-20251001 as well -- genuinely a 200k-native model -- turning clean OCP-side truncation into an upstream API rejection. 2. OpenClaw scales its history budget linearly off this value: contextWindow * maxHistoryShare * SAFETY_MARGIN (= x0.6), plus an oversized-message threshold at x0.5 (compaction-planning, OpenClaw 2026.7.1). Its own bundled registry hardcodes 200000 for Claude models, and the upstream request to raise it to 1M (openclaw#22979) was closed "not planned" -- 1M needs a beta header its compaction path omits. Raising the window for real requires per-model budgets and is ADR-level; a new regression test pins the invariant so that change has to be deliberate. Tests: 452 passed, 0 failed. Three mutations verified to bite: - revert aliases.opus -> 4-8 => 1 failure (opus-alias SPOT) - delete the claude-opus-5 entry => 2 failures (presence + dangling alias) - set its contextWindow to 1000000 => 2 failures (invariant + budget SPOT) E2E on an isolated port (:3999, prod on :3456 untouched and verified so): /v1/models returns 7 ids with claude-opus-5 first; a request with model:"opus" logs event=claude_spawned model=claude-opus-5 tier=opus and returns a real completion. Derived MAX_PROMPT_CHARS stays 600000. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_017gbqUZ8HfBZpjjbzQ85oH8 * fix(docs,test): restore the dropped Opus 4.8 line; assert the window CEILING Two review findings from the independent Iron-Rule-10 reviewer, both confirmed before applying: 1. docs/lan-mode.md: the sample `ocp update` output lost `ocp/claude-opus-4-8` entirely — the edit substituted the id instead of inserting the new one, so the block listed 6 bullets while line 219 of the same document claims "7 models available". Self-contradictory. Restored; the block is 7 again. 2. test-features.mjs: "every contextWindow is 200000" was too rigid and contradicted ADR 0009, which states the budget "scales automatically — no code change". Asserting every entry turned that documented no-code-change path into a must-edit-tests path, and would have failed on a future entry with a legitimately SMALLER window. Now asserts the MAX instead: raising the ceiling (the actual hazard, since the budget is global) still fails, while a smaller-window model stays legal. Verified the reformulation is strictly better, not just different: - raise a window to 1000000 -> 2 failures (ceiling + derivePromptCharBudget SPOT) - add a legitimate 128k model -> 452 passed, 0 failed (old test would have failed here) Tests: 452 passed, 0 failed. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_017gbqUZ8HfBZpjjbzQ85oH8 --------- Co-authored-by: dtzp555 Co-authored-by: Claude Opus 5 (1M context) --- CHANGELOG.md | 7 +++++++ README.md | 9 +++++---- docs/lan-mode.md | 11 ++++++----- models.json | 10 +++++++++- test-features.mjs | 23 +++++++++++++++++++++++ 5 files changed, 50 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 54ef353..8213659 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## Unreleased + +### Added + +- **Claude Opus 5 (`claude-opus-5`), and the `opus` alias now resolves to it.** New `models.json` entry — `/v1/models` goes 6 → 7 and OpenClaw picks it up on the next `ocp update` (via `scripts/sync-openclaw.mjs`). Verified against the installed CLI rather than assumed: the compiled `claude` 2.1.220 bundle carries `latest_per_family:{fable:"claude-fable-5",opus:"claude-opus-5",sonnet:"claude-sonnet-5",haiku:"claude-haiku-4-5"}`, so repointing `opus` mirrors what the CLI itself defaults to — the same reasoning as #168 (sonnet → sonnet-5). Availability confirmed with a live `claude -p --model claude-opus-5` completion on the subscription pool. Pricing is unchanged from Opus 4.8 ($5/$25 per MTok, CLI registry `pricing:"tier_5_25"`), so the alias repoint carries no cost change. `claude-opus-4-8` is retained for pinning. +- `contextWindow` is deliberately **200000**, not Opus 5's native 1M. Two reasons, both verified: (1) `MAX_PROMPT_CHARS` is a **single global** budget — `derivePromptCharBudget` takes `max(contextWindow) × 3` across *all* entries (`lib/prompt.mjs`), so a 1M entry would raise the truncation ceiling to 3,000,000 chars for `claude-haiku-4-5` too, which is genuinely 200k native, converting clean OCP-side truncation into an upstream API rejection; (2) OpenClaw scales its history budget linearly off this value (`contextWindow × maxHistoryShare × SAFETY_MARGIN` = `× 0.6`, plus an oversized-message threshold at `× 0.5`, per `compaction-planning` in OpenClaw 2026.7.1), and its own bundled registry hardcodes 200000 for Claude — the upstream request to raise it to 1M ([openclaw#22979](https://github.com/openclaw/openclaw/issues/22979)) was closed *not planned*. A new regression test pins the invariant so a future 1M entry has to be a deliberate, reviewed change. Raising it for real needs per-model budgets — tracked separately, ADR-level. + ## v3.24.0 — 2026-07-21 Minor release. Headline: two long-requested **OpenAI-compat features** land — **multimodal vision** (`image_url` parts) and **structured outputs** (`response_format` / JSON schema). Also: the prompt-char budget now derives from the model SPOT instead of a hand-set constant, an agentic-turn bug that dropped the model's final answer is fixed, and `OCP_LOCAL_TOOLS` supports the OpenClaw-backend use case. Four of the six landed from external contributors (@vvlasy-openclaw). Every code PR carried a fresh-context reviewer (Iron Rule 10); no new endpoint, no new `cli.js` wire behavior. diff --git a/README.md b/README.md index 8c7850a..37ff24c 100644 --- a/README.md +++ b/README.md @@ -113,11 +113,11 @@ node setup.mjs `setup.mjs` verifies the Claude CLI, starts the proxy on port 3456, and installs auto-start (launchd on macOS, systemd on Linux). The `ocp` CLI lands at `~/ocp/ocp` — symlink it onto your PATH (`sudo ln -sf ~/ocp/ocp /usr/local/bin/ocp`, or `ln -sf ~/ocp/ocp ~/.local/bin/ocp`) or alias it (`alias ocp=~/ocp/ocp`); the rest of the docs assume `ocp` is on your PATH. -**Verify** — should list 6 models: +**Verify** — should list 7 models: ```bash curl http://127.0.0.1:3456/v1/models -# claude-opus-4-8, claude-opus-4-7, claude-opus-4-6, claude-sonnet-5, claude-sonnet-4-6, claude-haiku-4-5-20251001 +# claude-opus-5, claude-opus-4-8, claude-opus-4-7, claude-opus-4-6, claude-sonnet-5, claude-sonnet-4-6, claude-haiku-4-5-20251001 ``` **Connect one IDE** — point any OpenAI-compatible tool at the proxy, then reload your shell and start a tool (Cline / Continue / Cursor / OpenCode): @@ -169,8 +169,9 @@ Any tool use happens server-side, under the `--allowedTools` set configured on t | Model ID | Notes | |----------|-------| -| `claude-opus-4-8` | Most capable (default for `opus` alias) | -| `claude-opus-4-7` | Previous Opus, retained for pinning | +| `claude-opus-5` | Most capable (default for `opus` alias) | +| `claude-opus-4-8` | Previous Opus, retained for pinning | +| `claude-opus-4-7` | Older Opus, retained for pinning | | `claude-opus-4-6` | Older Opus, retained for pinning | | `claude-sonnet-5` | Latest Sonnet (default for `sonnet` alias) | | `claude-sonnet-4-6` | Previous Sonnet, retained for pinning | diff --git a/docs/lan-mode.md b/docs/lan-mode.md index 86dbde5..de19cc0 100644 --- a/docs/lan-mode.md +++ b/docs/lan-mode.md @@ -78,7 +78,7 @@ Run `ocp lan` to see your IP and ready-to-share instructions. **Verify:** ```bash curl http://127.0.0.1:3456/v1/models -# Returns: claude-opus-4-8, claude-opus-4-7, claude-opus-4-6, claude-sonnet-5, claude-sonnet-4-6, claude-haiku-4-5-20251001 +# Returns: claude-opus-5, claude-opus-4-8, claude-opus-4-7, claude-opus-4-6, claude-sonnet-5, claude-sonnet-4-6, claude-haiku-4-5-20251001 ``` ### Headless install notes @@ -114,7 +114,7 @@ Please follow https://github.com/dtzp555-max/ocp/blob/main/README.md installed and logged in (`claude auth status`). Install missing pieces using my system's package manager. 2. git clone the repo, cd in, and run `node setup.mjs`. -3. Verify with `curl http://127.0.0.1:3456/v1/models` (should list 6 models). +3. Verify with `curl http://127.0.0.1:3456/v1/models` (should list 7 models). 4. Add `export OPENAI_BASE_URL=http://127.0.0.1:3456/v1` to my shell rc. 5. Tell me to reload my shell and try a tool like Cline / Continue / Cursor. @@ -141,7 +141,7 @@ Please follow https://github.com/dtzp555-max/ocp/blob/main/docs/lan-mode.md 5. Add OCP_ADMIN_KEY to my shell rc (~/.zshrc or ~/.bashrc). 6. Run `ocp lan` to show me the LAN IP and connect command. 7. Optionally create example keys: `ocp keys add laptop`, `ocp keys add tablet`. -8. Verify: `curl http://127.0.0.1:3456/v1/models` returns 6 models. +8. Verify: `curl http://127.0.0.1:3456/v1/models` returns 7 models. Tell me each step before running it. On error, diagnose before retrying. ``` @@ -163,7 +163,7 @@ Please follow https://github.com/dtzp555-max/ocp/blob/main/docs/lan-mode.md chmod +x ocp-connect 2. Run `./ocp-connect ` (add `--key ` if you have one). 3. Follow any IDE-specific manual hints it prints. -4. Verify: `curl http://:3456/v1/models` returns 6 models. +4. Verify: `curl http://:3456/v1/models` returns 7 models. 5. Tell me to reload my shell + restart any IDE that was already running. Don't auto-retry on error. Tell me the failure mode first. @@ -216,7 +216,7 @@ OCP Connect v1.3.0 (set by admin via PROXY_ANONYMOUS_KEY; see issue #12 §14 Path A) Testing API access... - ✓ API accessible (6 models available) + ✓ API accessible (7 models available) Shell config: ✓ .bashrc @@ -246,6 +246,7 @@ OCP Connect v1.3.0 ✓ OpenClaw configured Provider: ocp Models: + • ocp/claude-opus-5 • ocp/claude-opus-4-8 • ocp/claude-opus-4-7 • ocp/claude-opus-4-6 diff --git a/models.json b/models.json index c311585..f3c13d6 100644 --- a/models.json +++ b/models.json @@ -2,6 +2,14 @@ "$schema": "./models.schema.json", "version": 1, "models": [ + { + "id": "claude-opus-5", + "displayName": "Claude Opus 5", + "openclawName": "Claude Opus 5 (via CLI)", + "reasoning": true, + "contextWindow": 200000, + "maxTokens": 16384 + }, { "id": "claude-opus-4-8", "displayName": "Claude Opus 4.8", @@ -52,7 +60,7 @@ } ], "aliases": { - "opus": "claude-opus-4-8", + "opus": "claude-opus-5", "sonnet": "claude-sonnet-5", "haiku": "claude-haiku-4-5-20251001" }, diff --git a/test-features.mjs b/test-features.mjs index f73813d..d2022b3 100644 --- a/test-features.mjs +++ b/test-features.mjs @@ -4167,6 +4167,10 @@ test("models.json aliases.sonnet === 'claude-sonnet-5' (default-request-model SP assert.equal(_spotModels.aliases.sonnet, "claude-sonnet-5"); }); +test("models.json aliases.opus === 'claude-opus-5' (opus-alias SPOT)", () => { + assert.equal(_spotModels.aliases.opus, "claude-opus-5"); +}); + // ── Referential integrity (PR #152 review) ────────────────────────────────── // The value-mirror assertions above only prove the alias equals a string literal — // they pass even if that literal points at a model that does not exist in @@ -4180,6 +4184,25 @@ test("models.json: claude-sonnet-5 is present in models[] (the entry this PR add assert.ok(_spotModelIds.has("claude-sonnet-5"), "claude-sonnet-5 must exist as a models[].id"); }); +test("models.json: claude-opus-5 is present in models[] (the entry this PR adds)", () => { + assert.ok(_spotModelIds.has("claude-opus-5"), "claude-opus-5 must exist as a models[].id"); +}); + +// The prompt-char budget is GLOBAL (max across every entry × 3 chars/token), not +// per-model — see lib/prompt.mjs derivePromptCharBudget. An entry declaring a native 1M +// window would therefore raise the truncation ceiling for claude-haiku-4-5 too (genuinely +// 200k), turning OCP-side truncation into an upstream API rejection. +// +// Asserts the MAX, deliberately, not every entry: ADR 0009 states the budget "scales +// automatically — no code change", so a future entry with a SMALLER window (say a 128k +// model) must stay legal and must not fail this suite. Only raising the ceiling is the +// hazard, and that is an ADR-level decision requiring per-model budgets first. +test("models.json: max contextWindow is 200000 (global prompt-budget ceiling)", () => { + const windows = _spotModels.models.map(m => m.contextWindow); + assert.equal(Math.max(...windows), 200000, + `max contextWindow re-scales MAX_PROMPT_CHARS for ALL models incl. the 200k-native haiku (see lib/prompt.mjs + ADR 0009)`); +}); + test("models.json: every aliases value resolves to a real models[].id (referential integrity)", () => { for (const [name, target] of Object.entries(_spotModels.aliases)) { assert.ok(_spotModelIds.has(target), `aliases.${name} -> '${target}' is a dangling alias (no matching models[].id)`);