mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-24 08:02:14 +03:00
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:
@@ -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"],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
120
tests/unit/opencode-go-console-go-effort-clamp.test.ts
Normal file
120
tests/unit/opencode-go-console-go-effort-clamp.test.ts
Normal 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");
|
||||
});
|
||||
Reference in New Issue
Block a user