diff --git a/README.md b/README.md index 9580799..7752cc0 100644 --- a/README.md +++ b/README.md @@ -716,6 +716,7 @@ Any tool use happens server-side, under the `--allowedTools` set configured on t | `claude-opus-4-8` | Most capable (default for `opus` alias) | | `claude-opus-4-7` | Previous Opus, retained for pinning | | `claude-opus-4-6` | Older Opus, retained for pinning | +| `claude-sonnet-5` | Latest Sonnet (available by full ID; `sonnet` alias repoint tracked separately) | | `claude-sonnet-4-6` | Good balance of speed/quality (default for `sonnet` alias) | | `claude-haiku-4-5-20251001` | Fastest, lightweight (default for `haiku` alias) | diff --git a/models.json b/models.json index fe119e4..0f417f1 100644 --- a/models.json +++ b/models.json @@ -26,6 +26,14 @@ "contextWindow": 200000, "maxTokens": 16384 }, + { + "id": "claude-sonnet-5", + "displayName": "Claude Sonnet 5", + "openclawName": "Claude Sonnet 5 (via CLI)", + "reasoning": true, + "contextWindow": 200000, + "maxTokens": 16384 + }, { "id": "claude-sonnet-4-6", "displayName": "Claude Sonnet 4.6", diff --git a/ocp-connect b/ocp-connect index de268fa..1cc31d7 100755 --- a/ocp-connect +++ b/ocp-connect @@ -122,11 +122,17 @@ provider = { "models": [] } -# Model metadata mapping (prefix match for versioned IDs like claude-haiku-4-5-20251001) +# Model metadata mapping. Prefix match on the model FAMILY (claude-opus / -sonnet / +# -haiku), not a pinned version. A version-pinned prefix like "claude-sonnet-4" +# silently misses "claude-sonnet-5" and falls through to the non-reasoning / +# 8k-output default (PR #152 review) — every future Sonnet/Opus/Haiku bump would +# 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.) model_meta = { - "claude-opus-4": {"name": "Claude Opus (OCP)", "reasoning": True, "maxTokens": 16384}, - "claude-sonnet-4": {"name": "Claude Sonnet (OCP)", "reasoning": True, "maxTokens": 16384}, - "claude-haiku-4": {"name": "Claude Haiku (OCP)", "reasoning": False, "maxTokens": 8192}, + "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}, } def get_model_meta(mid): @@ -178,11 +184,11 @@ config.setdefault("agents", {}) config["agents"].setdefault("defaults", {}) config["agents"]["defaults"].setdefault("models", {}) -# Build alias map (prefix match) +# Build alias map (family prefix match — version-agnostic, see model_meta note) alias_prefixes = { - "claude-opus-4": "Claude Opus", - "claude-sonnet-4": "Claude Sonnet", - "claude-haiku-4": "Claude Haiku", + "claude-opus": "Claude Opus", + "claude-sonnet": "Claude Sonnet", + "claude-haiku": "Claude Haiku", } for mid in model_ids: diff --git a/test-features.mjs b/test-features.mjs index e190c26..0571b74 100644 --- a/test-features.mjs +++ b/test-features.mjs @@ -3326,6 +3326,31 @@ test("models.json aliases.sonnet === 'claude-sonnet-4-6' (default-request-model assert.equal(_spotModels.aliases.sonnet, "claude-sonnet-4-6"); }); +// ── 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 +// models[]. A one-line slip (edit an alias, forget the models[] entry) would leave +// /v1/models missing the model while every `model: ""` request passes +// validation and then fails at CLI spawn. VALID_MODELS keys on alias *names*, so +// nothing else checks alias *targets*. This is the guard with teeth. +const _spotModelIds = new Set(_spotModels.models.map(m => m.id)); + +test("models.json: claude-sonnet-5 is present in models[] (the entry this PR adds)", () => { + assert.ok(_spotModelIds.has("claude-sonnet-5"), "claude-sonnet-5 must exist as a models[].id"); +}); + +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)`); + } +}); + +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)`); + } +}); + // ── escapeHtml + key-name validator (issue #114) ──────────────────────────── // Replicated verbatim from dashboard.html so tests run without a browser. function escapeHtml(s) {