fix(providers): clamp reasoning_effort to the declared vocabulary of models that expose one (#11274)

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.
This commit is contained in:
Harvey Doan
2026-08-24 04:10:21 +07:00
committed by GitHub
parent 10276821cd
commit 9adeb3b673
3 changed files with 171 additions and 0 deletions

View File

@@ -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"],
},
],
};

View File

@@ -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);

View File

@@ -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<string, unknown>).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<string, unknown>).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<string, unknown>).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<string, unknown>).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<string, unknown>).reasoning_effort, "medium");
});