mirror of
https://github.com/dtzp555-max/ocp.git
synced 2026-07-27 07:55:07 +00:00
feat(models): align maxTokens with the CLI registry (#195)
Every Opus entry and claude-sonnet-5 go 16384 -> 64000; claude-sonnet-4-6 and
claude-haiku-4-5 go to 32000 (haiku was 8192). These are the
max_output_tokens.default each model declares in the compiled CLI 2.1.220
registry, extracted id-anchored:
claude-opus-5 max_output_tokens:{default:64000,upper:128000}
claude-opus-4-8 max_output_tokens:{default:64000,upper:128000}
claude-opus-4-7 max_output_tokens:{default:64000,upper:128000}
claude-opus-4-6 max_output_tokens:{default:64000,upper:128000}
claude-sonnet-5 max_output_tokens:{default:64000,upper:128000}
claude-sonnet-4-6 max_output_tokens:{default:32000,upper:128000}
claude-haiku-4-5 max_output_tokens:{default:32000,upper:64000}
This is not cosmetic metadata. OCP itself never enforces maxTokens — server.mjs
does not read it — but it is propagated to OpenClaw (setup.mjs,
scripts/sync-openclaw.mjs), and OpenClaw CAPS the outbound request with it:
maxTokens = Math.min(baseMaxTokens + thinkingBudget, modelMaxTokens)
with reasoning budgets of medium 8192 / high 16384, and a clamp
`if (maxTokens <= thinkingBudget) thinkingBudget = maxTokens - 1024`.
So at `high` reasoning against a model declared at 16384: maxTokens resolves to
16384, the clamp fires, thinking takes 15360, and roughly 1024 tokens are left
for the visible answer. That is what this repo shipped for every model until
now, and it is the actual harm — not the advertised-vs-real mismatch.
ocp-connect keeps its own family-prefix table (/v1/models does not expose
maxTokens, so it cannot derive them). Set to the family FLOOR — opus 64000,
sonnet 32000 because sonnet-4-6 is 32000 while sonnet-5 is 64000, haiku 32000 —
because prefixes cannot distinguish versions and under-advertising merely caps
a client lower than the model allows, whereas over-advertising would promise
capacity a family member does not have. Commented in place.
New test asserts the PRINCIPLE, not the numbers: every maxTokens must exceed
OpenClaw's `high` thinking budget so an answer still fits. A value-pinning test
would need editing on every model addition and would not explain itself.
Mutation-proven: reverting one entry to 16384 fails with
"claude-opus-5 declares maxTokens=16384 <= 16384: at OpenClaw's 'high'
reasoning level the thinking budget would consume all but ~1024 tokens".
Owner-approved decision (option: align with registry defaults). Tradeoff
recorded in CHANGELOG: longer answers, correspondingly higher per-request quota
burn on long generations.
Verified: bash -n on ocp-connect, and py_compile on its embedded python block
(lines 101-334) so the comment sits at a valid indentation.
Tests: 458 passed, 0 failed (457 + 1).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017gbqUZ8HfBZpjjbzQ85oH8
This commit is contained in:
@@ -1,5 +1,11 @@
|
||||
# Changelog
|
||||
|
||||
## Unreleased
|
||||
|
||||
### Changed
|
||||
|
||||
- **`maxTokens` now matches the CLI registry instead of a uniform 16384 (#195).** Every Opus entry and `claude-sonnet-5` go to **64000**, `claude-sonnet-4-6` and `claude-haiku-4-5` to **32000** — the `max_output_tokens.default` each model actually declares in the compiled CLI 2.1.220 registry. OCP never enforced this value; it is propagated to OpenClaw, where it **caps the outbound request**: `maxTokens = Math.min(baseMaxTokens + thinkingBudget, modelMaxTokens)`. OpenClaw's reasoning budgets are medium 8192 / high 16384, and when `maxTokens <= thinkingBudget` it clamps thinking to `maxTokens - 1024` — so a model declared at 16384 running at **high** reasoning spent ~15360 on thinking and left **~1024 tokens for the actual answer**. `ocp-connect`'s independent family table moves to the family floor (opus 64000, sonnet 32000, haiku 32000) since prefixes cannot distinguish versions. Expect longer answers and correspondingly higher per-request quota burn on long generations.
|
||||
|
||||
## v3.25.0 — 2026-07-27
|
||||
|
||||
Minor release. Headline: **Claude Opus 5** joins the model list and the `opus` alias now resolves to it. Alongside it, two `server.mjs` correctness fixes found by review rather than by users — a monotonic in-flight-counter leak, and cache keys that were hashing the alias string instead of the model it resolves to. The three TUI/prompt fixes that landed after v3.24.0 are included here too.
|
||||
|
||||
+7
-7
@@ -8,7 +8,7 @@
|
||||
"openclawName": "Claude Opus 5 (via CLI)",
|
||||
"reasoning": true,
|
||||
"contextWindow": 200000,
|
||||
"maxTokens": 16384
|
||||
"maxTokens": 64000
|
||||
},
|
||||
{
|
||||
"id": "claude-opus-4-8",
|
||||
@@ -16,7 +16,7 @@
|
||||
"openclawName": "Claude Opus 4.8 (via CLI)",
|
||||
"reasoning": true,
|
||||
"contextWindow": 200000,
|
||||
"maxTokens": 16384
|
||||
"maxTokens": 64000
|
||||
},
|
||||
{
|
||||
"id": "claude-opus-4-7",
|
||||
@@ -24,7 +24,7 @@
|
||||
"openclawName": "Claude Opus 4.7 (via CLI)",
|
||||
"reasoning": true,
|
||||
"contextWindow": 200000,
|
||||
"maxTokens": 16384
|
||||
"maxTokens": 64000
|
||||
},
|
||||
{
|
||||
"id": "claude-opus-4-6",
|
||||
@@ -32,7 +32,7 @@
|
||||
"openclawName": "Claude Opus 4.6 (via CLI)",
|
||||
"reasoning": true,
|
||||
"contextWindow": 200000,
|
||||
"maxTokens": 16384
|
||||
"maxTokens": 64000
|
||||
},
|
||||
{
|
||||
"id": "claude-sonnet-5",
|
||||
@@ -40,7 +40,7 @@
|
||||
"openclawName": "Claude Sonnet 5 (via CLI)",
|
||||
"reasoning": true,
|
||||
"contextWindow": 200000,
|
||||
"maxTokens": 16384
|
||||
"maxTokens": 64000
|
||||
},
|
||||
{
|
||||
"id": "claude-sonnet-4-6",
|
||||
@@ -48,7 +48,7 @@
|
||||
"openclawName": "Claude Sonnet 4.6 (via CLI)",
|
||||
"reasoning": true,
|
||||
"contextWindow": 200000,
|
||||
"maxTokens": 16384
|
||||
"maxTokens": 32000
|
||||
},
|
||||
{
|
||||
"id": "claude-haiku-4-5-20251001",
|
||||
@@ -56,7 +56,7 @@
|
||||
"openclawName": "Claude Haiku 4.5 (via CLI)",
|
||||
"reasoning": false,
|
||||
"contextWindow": 200000,
|
||||
"maxTokens": 8192
|
||||
"maxTokens": 32000
|
||||
}
|
||||
],
|
||||
"aliases": {
|
||||
|
||||
+10
-3
@@ -129,10 +129,17 @@ provider = {
|
||||
# re-trip it. Family prefixes classify any versioned ID correctly with no per-model
|
||||
# edit. (ADR 0003: models.json is the SPOT for model existence; /v1/models does not
|
||||
# expose reasoning/maxTokens, so family classification stays here.)
|
||||
# maxTokens values are the FAMILY FLOOR of the CLI registry max_output_tokens.default
|
||||
# (opus family 64000; sonnet 32000 because sonnet-4-6 is 32000 while sonnet-5 is 64000;
|
||||
# haiku 32000). Family prefixes cannot distinguish versions, so the floor is used
|
||||
# deliberately: under-advertising caps a client lower than the model allows, which is safe,
|
||||
# whereas over-advertising would promise capacity a member of the family does not have.
|
||||
# models.json is authoritative per ADR 0003; this table only exists because /v1/models does
|
||||
# not expose maxTokens.
|
||||
model_meta = {
|
||||
"claude-opus": {"name": "Claude Opus (OCP)", "reasoning": True, "maxTokens": 16384},
|
||||
"claude-sonnet": {"name": "Claude Sonnet (OCP)", "reasoning": True, "maxTokens": 16384},
|
||||
"claude-haiku": {"name": "Claude Haiku (OCP)", "reasoning": False, "maxTokens": 8192},
|
||||
"claude-opus": {"name": "Claude Opus (OCP)", "reasoning": True, "maxTokens": 64000},
|
||||
"claude-sonnet": {"name": "Claude Sonnet (OCP)", "reasoning": True, "maxTokens": 32000},
|
||||
"claude-haiku": {"name": "Claude Haiku (OCP)", "reasoning": False, "maxTokens": 32000},
|
||||
}
|
||||
|
||||
def get_model_meta(mid):
|
||||
|
||||
@@ -4299,6 +4299,24 @@ test("models.json: every aliases value resolves to a real models[].id (referenti
|
||||
}
|
||||
});
|
||||
|
||||
// maxTokens must leave room for a visible answer once a client's thinking budget is taken out
|
||||
// (#195). OCP never enforces maxTokens itself — it is propagated to OpenClaw (setup.mjs,
|
||||
// scripts/sync-openclaw.mjs) where it CAPS the outbound request:
|
||||
// maxTokens = Math.min(baseMaxTokens + thinkingBudget, modelMaxTokens)
|
||||
// OpenClaw's reasoning budgets are medium 8192 / high 16384, and when maxTokens <= thinkingBudget
|
||||
// it clamps thinking to maxTokens - 1024. So a model declared at 16384 running at `high` spent
|
||||
// ~15360 on thinking and left ~1024 for the actual answer — which is what this repo shipped until
|
||||
// v3.25.0. Asserting the PRINCIPLE rather than pinning per-model numbers: a value test would need
|
||||
// editing every time a model is added, and would not say why.
|
||||
const _spotHighThinkingBudget = 16384; // OpenClaw's `high` reasoning budget
|
||||
test("models.json: every maxTokens exceeds the high thinking budget, leaving room for output (#195)", () => {
|
||||
for (const m of _spotModels.models) {
|
||||
assert.ok(m.maxTokens > _spotHighThinkingBudget,
|
||||
`${m.id} declares maxTokens=${m.maxTokens} <= ${_spotHighThinkingBudget}: at OpenClaw's 'high' reasoning ` +
|
||||
`level the thinking budget would consume all but ~1024 tokens of the response`);
|
||||
}
|
||||
});
|
||||
|
||||
test("models.json: every legacyAliases value resolves to a real models[].id (referential integrity)", () => {
|
||||
for (const [name, target] of Object.entries(_spotModels.legacyAliases || {})) {
|
||||
assert.ok(_spotModelIds.has(target), `legacyAliases.${name} -> '${target}' is a dangling alias (no matching models[].id)`);
|
||||
|
||||
Reference in New Issue
Block a user