From 9adeb3b673a2adadf8c924b543f87ce42ebb980f Mon Sep 17 00:00:00 2001 From: Harvey Doan Date: Mon, 24 Aug 2026 04:10:21 +0700 Subject: [PATCH] fix(providers): clamp reasoning_effort to the declared vocabulary of models that expose one (#11274) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Validated on the combined 8-PR board: the declared-vocabulary clamp integrates with #11232's learned-set clamp (both fire in sequence; REASONING_EFFORT_ORDER import added), reasoning quartet 57/57 including the new opencode-go suite 7/7, 88/88 across the board's focused suites, typecheck:core + dashboard-typecheck clean, all static gates within baseline. Retargeted main→release/v3.8.50 and rebased onto the release tip (authorship preserved; the branch carried main history, so only the real commit was carried). The generic explicit-capability clamp closes the Console Go 400 [1210] loop for models with a declared effort vocabulary. Thank you @linhdmn — the live-probe table in the PR body is exactly the evidence standard we want. --- .../providers/registry/opencode/go/index.ts | 13 ++ open-sse/executors/base/reasoningEffort.ts | 38 ++++++ ...pencode-go-console-go-effort-clamp.test.ts | 120 ++++++++++++++++++ 3 files changed, 171 insertions(+) create mode 100644 tests/unit/opencode-go-console-go-effort-clamp.test.ts diff --git a/open-sse/config/providers/registry/opencode/go/index.ts b/open-sse/config/providers/registry/opencode/go/index.ts index abebd92c0f..21170b5b16 100644 --- a/open-sse/config/providers/registry/opencode/go/index.ts +++ b/open-sse/config/providers/registry/opencode/go/index.ts @@ -219,5 +219,18 @@ export const opencode_goProvider: RegistryEntry = { supportedThinkingEfforts: ["none", "low", "high", "max"], targetFormat: "openai-responses", }, + // Console Go free GLM-tier model (live-verified 2026-08-23): the upstream + // rejects every reasoning_effort outside {low, high, max} whenever tools + // are present — "[1210] This model always engages in thinking and cannot + // be disabled; please use low, high, or max" — which broke clients that + // default to reasoning_effort:"medium" (Hermes). Declaring the exact + // vocabulary lets sanitizeReasoningEffortForProvider clamp off-vocabulary + // requests to the nearest accepted tier instead of burning a 400. + { + id: "ox-alpha-free", + name: "ox-alpha (free)", + supportsReasoning: true, + supportedThinkingEfforts: ["low", "high", "max"], + }, ], }; diff --git a/open-sse/executors/base/reasoningEffort.ts b/open-sse/executors/base/reasoningEffort.ts index d6ca00ba55..ee520bbc2c 100644 --- a/open-sse/executors/base/reasoningEffort.ts +++ b/open-sse/executors/base/reasoningEffort.ts @@ -11,6 +11,7 @@ import { import { getLearnedReasoningEffort, clampToLearned, + REASONING_EFFORT_ORDER, } from "../../services/learnedReasoningEffortCaps.ts"; /** @@ -357,6 +358,43 @@ export function sanitizeReasoningEffortForProvider( } } + // ── explicit per-model capability clamp ────────────────────────────────── + // When the registry declares supportedThinkingEfforts for this exact model + // and the requested effort falls outside that vocabulary, remap to the + // nearest declared tier: the smallest ranked value ≥ the request, else the + // highest declared (a request above the ceiling lands on the ceiling). + // Live case: opencode-go/ox-alpha-free (Console Go) only accepts + // {low, high, max} — a client's reasoning_effort:"medium" reached the + // upstream verbatim and 400'd every turn ("[1210] This model always engages + // in thinking and cannot be disabled; please use low, high, or max"). The + // learned-caps path can't help here (it only clamps down from xhigh/max, + // and this error text isn't a parseable enum), so the declaration is the + // only source of truth. Models without an explicit declaration keep + // #8057's trust-the-upstream pass-through. + const providerModelIdForClamp = modelStr.startsWith(`${provider}/`) + ? modelStr.slice(provider.length + 1) + : modelStr; + const declaredEfforts = getProviderModels(provider).find( + (entry) => entry.id === providerModelIdForClamp || entry.aliases?.includes(providerModelIdForClamp) + )?.supportedThinkingEfforts; + const declaredRanked = ( + Array.isArray(declaredEfforts) ? declaredEfforts : [] + ) + .map((tier) => ({ tier, rank: REASONING_EFFORT_ORDER.indexOf(tier) })) + .filter((x) => x.rank >= 0) + .sort((a, b) => a.rank - b.rank); + if (declaredRanked.length > 0 && !declaredEfforts!.includes(effortStr)) { + const requestedRank = REASONING_EFFORT_ORDER.indexOf(effortStr); + const nearest = + declaredRanked.find((x) => x.rank >= requestedRank) ?? + declaredRanked[declaredRanked.length - 1]; + log?.info?.( + "REASONING_SANITIZE", + `${provider}/${modelStr}: mapped reasoning_effort ${effortStr} → ${nearest.tier} (model accepts ${declaredEfforts!.join("/")})` + ); + return writeEffortValue(b, nearest.tier, c); + } + const supportsXHigh = supportsXHighEffort(provider, modelStr); const supportsMax = supportsMaxEffortForProvider(provider, modelStr); diff --git a/tests/unit/opencode-go-console-go-effort-clamp.test.ts b/tests/unit/opencode-go-console-go-effort-clamp.test.ts new file mode 100644 index 0000000000..2009d11a39 --- /dev/null +++ b/tests/unit/opencode-go-console-go-effort-clamp.test.ts @@ -0,0 +1,120 @@ +/** + * Console Go (opencode.ai/zen/go/v1) reasoning-effort vocabulary clamp. + * + * Live-reproduced 2026-08-23 via the Hermes Telegram bot → /v1/chat/completions: + * `opencode-go/ox-alpha-free` rejects every reasoning_effort except + * {low, high, max} whenever the request carries tools — + * + * [400] Error from provider (Console Go): Upstream request failed: [1210] + * This model always engages in thinking and cannot be disabled; please use + * low, high, or max + * + * Hermes sends reasoning_effort:"medium" with 24 tools and died on every turn. + * Two gaps let the bad value reach the upstream verbatim: + * 1. `ox-alpha-free` is a discovery-synced model with no static registry + * entry declaring its effort vocabulary. + * 2. sanitizeReasoningEffortForProvider only consults declared + * supportedThinkingEfforts in the `max` branch (max fallback); other + * out-of-vocabulary values pass through untouched. + * + * Fix under test: declare ["low","high","max"] on the registry entry and add a + * generic explicit-capability clamp that remaps any out-of-vocabulary effort to + * the nearest declared tier (smallest ranked ≥ requested, else the highest). + * Models without a declaration keep today's pass-through behavior (#8057). + */ +import test from "node:test"; +import assert from "node:assert/strict"; + +const { sanitizeReasoningEffortForProvider } = await import("../../open-sse/executors/base.ts"); +const { REGISTRY } = await import("../../open-sse/config/providerRegistry.ts"); + +function makeLog() { + const messages: Array<[string, string]> = []; + return { + info: (tag: string, msg: string) => messages.push([tag, msg]), + messages, + }; +} + +const HERMES_BODY = { + model: "ox-alpha-free", + max_tokens: 65536, + stream_options: { include_usage: true }, + messages: [{ role: "user", content: "Start telegram bot" }], + tools: [ + { + type: "function", + function: { name: "clarify", description: "ask", parameters: { type: "object" } }, + }, + ], +}; + +test("registry: opencode-go declares ox-alpha-free with the live-verified Console Go effort set", () => { + const entry = REGISTRY["opencode-go"]; + assert.ok(entry, "opencode-go registry entry must exist"); + const model = entry.models.find((m) => m.id === "ox-alpha-free"); + assert.ok(model, "ox-alpha-free must be registered on opencode-go"); + assert.deepEqual(model.supportedThinkingEfforts, ["low", "high", "max"]); +}); + +test("clamp: medium → high for ox-alpha-free (the exact Hermes failure)", () => { + const log = makeLog(); + const body = { ...HERMES_BODY, reasoning_effort: "medium" }; + const result = sanitizeReasoningEffortForProvider(body, "opencode-go", "ox-alpha-free", log); + assert.notEqual(result, body, "must return a new object when mutating"); + assert.equal((result as Record).reasoning_effort, "high"); + assert.ok( + log.messages.some(([tag, m]) => tag === "REASONING_SANITIZE" && /medium → high/.test(m)), + "logs the mapping" + ); +}); + +test("clamp: disable-shaped efforts map to low (upstream refuses to stop thinking)", () => { + for (const effort of ["none", "minimal"]) { + const body = { ...HERMES_BODY, reasoning_effort: effort }; + const result = sanitizeReasoningEffortForProvider(body, "opencode-go", "ox-alpha-free", null); + assert.equal( + (result as Record).reasoning_effort, + "low", + `${effort} → low` + ); + } +}); + +test("clamp: xhigh → max for ox-alpha-free", () => { + const body = { ...HERMES_BODY, reasoning_effort: "xhigh" }; + const result = sanitizeReasoningEffortForProvider(body, "opencode-go", "ox-alpha-free", null); + assert.equal((result as Record).reasoning_effort, "max"); +}); + +test("clamp: in-vocabulary efforts pass through untouched", () => { + for (const effort of ["low", "high", "max"]) { + const body = { ...HERMES_BODY, reasoning_effort: effort }; + const result = sanitizeReasoningEffortForProvider(body, "opencode-go", "ox-alpha-free", null); + assert.equal(result, body, `${effort} must not be rewritten`); + assert.equal((result as Record).reasoning_effort, effort); + } +}); + +test("clamp writes back to every carrier present (top-level + reasoning.effort + output_config.effort)", () => { + const body = { + ...HERMES_BODY, + reasoning_effort: "medium", + reasoning: { effort: "medium" }, + output_config: { effort: "medium" }, + }; + const result = sanitizeReasoningEffortForProvider(body, "opencode-go", "ox-alpha-free", null) as Record< + string, + unknown + >; + assert.equal(result.reasoning_effort, "high"); + assert.deepEqual(result.reasoning, { effort: "high" }); + assert.deepEqual(result.output_config, { effort: "high" }); +}); + +test("no declaration → pass-through unchanged (#8057 policy for unlisted models)", () => { + const body = { ...HERMES_BODY, model: "some-unregistered-model", reasoning_effort: "medium" }; + const result = sanitizeReasoningEffortForProvider(body, "opencode-go", "some-unregistered-model", null); + assert.equal(result, body, "undeclared models keep today's trust-the-upstream behavior"); + assert.equal((result as Record).reasoning_effort, "medium"); +});