diff --git a/CHANGELOG.md b/CHANGELOG.md index 44eaa6a..2efe66f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ ### 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 declares in the compiled CLI 2.1.220 registry. This corrects **advertised metadata only**: `models.json` is the SPOT (ADR 0003) and every value in it should be the truth about the model. **It changes nothing about how OCP behaves.** OCP never enforces `maxTokens` — `buildCliArgs` passes no output-token flag to the CLI at all — and OpenClaw addresses a local OCP over `openai-completions`, whose request field (`max_completion_tokens`) appears nowhere in this repo. The value is consumed only by clients that choose to honour it, via `setup.mjs` / `scripts/sync-openclaw.mjs` / `ocp-connect`. **Expect no change in answer length or quota burn.** `ocp-connect`'s independent family table moves to the family floor (opus 64000, sonnet 32000, haiku 32000) since prefixes cannot distinguish versions. +- **`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 declares in the compiled CLI 2.1.220 registry. This corrects **advertised metadata only**: `models.json` is the SPOT (ADR 0003) and every value in it should be the truth about the model. **It changes nothing about how OCP behaves.** OCP never enforces `maxTokens` — `buildCliArgs` passes no output-token flag to the CLI at all — and OpenClaw addresses a local OCP over `openai-completions`, whose request field (`max_completion_tokens`) appears nowhere in this repo. The value is consumed only by clients that choose to honour it, via `setup.mjs` / `scripts/sync-openclaw.mjs` / `ocp-connect`. **Expect no change in answer length or quota burn.** `ocp-connect`'s independent family table moves to the floor over each family's current `models.json` members (opus 64000, sonnet 32000, haiku 32000), since prefixes cannot distinguish versions. Its unknown-id fallback stays at **8192** — the registry's global minimum, held by `claude-3-5-haiku` and `claude-3-5-sonnet`, which are the only real ids that reach it (`claude-3-5-*` matches no family prefix). ## v3.25.0 — 2026-07-27 diff --git a/ocp-connect b/ocp-connect index 15cc7b3..b71e093 100755 --- a/ocp-connect +++ b/ocp-connect @@ -129,11 +129,14 @@ 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. +# maxTokens values are the floor of the CLI registry's max_output_tokens.default over the +# family members CURRENTLY IN models.json (opus 64000; sonnet 32000, because sonnet-4-6 is +# 32000 while sonnet-5 is 64000; haiku 32000) — NOT over the whole registry family. The +# registry also holds claude-opus-4-0 / -4-5 at 32000, so should a legacy opus ever be added +# to models.json, the opus row here would over-advertise it and must drop to 32000. +# Family prefixes cannot distinguish versions, so a 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 = { @@ -147,11 +150,14 @@ def get_model_meta(mid): for prefix, meta in sorted(model_meta.items(), key=lambda x: -len(x[0])): if mid.startswith(prefix): return meta - # Unknown id = most likely a model newer than this script. 32000 is the LOWEST - # max_output_tokens.default in the CLI 2.1.220 registry (haiku/sonnet-4-6), so it is the - # safe floor: never over-advertises a real model, and no longer contradicts the rest of - # this table by sitting below every family in it. - return {"name": mid + " (OCP)", "reasoning": False, "maxTokens": 32000} + # Unknown id. 8192 is the GLOBAL MINIMUM max_output_tokens.default across all 17 records in + # the CLI 2.1.220 registry (distinct values: 8192 / 32000 / 64000). The two records holding + # it — claude-3-5-haiku and claude-3-5-sonnet, both default:8192, upper:8192 — are precisely + # the ones that reach this line, because "claude-3-5-*" matches none of the family prefixes + # above. Raising this to a family-table value would over-advertise those two by 4x. Keep 8192: + # under-advertising caps a client lower than a model allows, which is safe; over-advertising + # promises capacity that does not exist. + return {"name": mid + " (OCP)", "reasoning": False, "maxTokens": 8192} for mid in model_ids: meta = get_model_meta(mid) diff --git a/test-features.mjs b/test-features.mjs index 6eb9b69..3ff4e8b 100644 --- a/test-features.mjs +++ b/test-features.mjs @@ -4310,9 +4310,17 @@ test("models.json: every aliases value resolves to a real models[].id (referenti // value above the bar and still call itself "registry-aligned" — which is the actual claim. Adding // a model means adding a row here, and that is the point: the row is where you record what the // registry said when you checked. -const _spotRegistryMaxTokens = { // CLI 2.1.220, id-anchored max_output_tokens.default +// Keys are models.json ids; values are the CLI 2.1.220 registry's max_output_tokens.default, +// each extracted id-anchored (grep 'id:""' + the following bytes) — never by bare-string +// search, which matches cross-references inside OTHER models' records and silently attributes +// the wrong number. ONE KEY IS NOT A REGISTRY ID: models.json carries the dated haiku id, but +// the registry record is id:"claude-haiku-4-5" (the dated string appears only as that record's +// provider_ids.first_party). Anchor the haiku row on the SHORT id; anchoring on the dated one +// returns nothing, which is what tempts the next reader back into a bare-string search. +const _spotRegistryMaxTokens = { "claude-opus-5": 64000, "claude-opus-4-8": 64000, "claude-opus-4-7": 64000, "claude-opus-4-6": 64000, - "claude-sonnet-5": 64000, "claude-sonnet-4-6": 32000, "claude-haiku-4-5-20251001": 32000, + "claude-sonnet-5": 64000, "claude-sonnet-4-6": 32000, + "claude-haiku-4-5-20251001": 32000, // registry id: claude-haiku-4-5 }; test("models.json: every maxTokens equals the CLI registry's max_output_tokens.default (#195)", () => { for (const m of _spotModels.models) {