From 7fdd2e0f2ab53d0758b949e0d349871357fba642 Mon Sep 17 00:00:00 2001 From: Dizzle <112548150+maxmad64bis@users.noreply.github.com> Date: Sun, 23 Aug 2026 19:21:27 +0200 Subject: [PATCH] fix(sse): learn accepted reasoning_effort sets and clamp downgrade-only (#11232) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Validated on the combined 12-PR batch board (worktree off origin/release/v3.8.50 @ 8f390eff): focused node:test suites 183/184 (the single red was a load-induced pollUntil flake — 11/11 when re-run isolated, this file included), typecheck:core clean, file-size/changelog/complexity/cognitive gates all within baseline, mutation-coverage gate no-drift. Learned effort sets now cost at most one 400 per provider+model. Thank you @maxmad64bis! --- open-sse/executors/base.ts | 27 ++-- open-sse/executors/base/reasoningEffort.ts | 42 +++--- .../services/learnedReasoningEffortCaps.ts | 100 +++++++++----- .../learned-reasoning-effort-caps.test.ts | 94 +++++++++++-- .../reasoning-effort-clamp-and-retry.test.ts | 127 +++++++++++++++++- ...easoning-effort-learned-capability.test.ts | 64 +++++++++ 6 files changed, 373 insertions(+), 81 deletions(-) diff --git a/open-sse/executors/base.ts b/open-sse/executors/base.ts index 1c13442aff..53800c8bc7 100644 --- a/open-sse/executors/base.ts +++ b/open-sse/executors/base.ts @@ -1559,22 +1559,31 @@ export class BaseExecutor { if (acceptedValues) { reasoningEffortClamped = true; const learned = recordLearnedReasoningEffort(this.provider, model, acceptedValues); - if (learned) { + if (learned && learned.size > 0) { + const beforeRetry = JSON.stringify(transformedBody); transformedBody = sanitizeReasoningEffortForProvider( transformedBody, this.provider, model, log ); - let retryBody = JSON.stringify(transformedBody); - if (usesClaudeCodeProtocol || this.provider === "claude") { - retryBody = await signRequestBody(retryBody); + const afterRetry = JSON.stringify(transformedBody); + if (beforeRetry === afterRetry) { + log?.info?.( + "REASONING_SANITIZE", + `Upstream ${response.status} rejected reasoning_effort on ${url} — learned ${[...learned].join(",")} but clamp was no-op for ${this.provider}/${model}, not retrying` + ); + } else { + let retryBody = JSON.stringify(transformedBody); + if (usesClaudeCodeProtocol || this.provider === "claude") { + retryBody = await signRequestBody(retryBody); + } + log?.info?.( + "REASONING_SANITIZE", + `Upstream ${response.status} rejected reasoning_effort on ${url} — clamped to ${[...learned].join(",")} and retrying (learned for ${this.provider}/${model})` + ); + response = await fetchWithStartTimeout(url, { ...fetchOptions, body: retryBody }); } - log?.info?.( - "REASONING_SANITIZE", - `Upstream ${response.status} rejected reasoning_effort on ${url} — clamped to ${learned} and retrying (learned for ${this.provider}/${model})` - ); - response = await fetchWithStartTimeout(url, { ...fetchOptions, body: retryBody }); } } } diff --git a/open-sse/executors/base/reasoningEffort.ts b/open-sse/executors/base/reasoningEffort.ts index 8dd99904fd..d6ca00ba55 100644 --- a/open-sse/executors/base/reasoningEffort.ts +++ b/open-sse/executors/base/reasoningEffort.ts @@ -10,7 +10,7 @@ import { } from "../../config/providerModels.ts"; import { getLearnedReasoningEffort, - REASONING_EFFORT_ORDER, + clampToLearned, } from "../../services/learnedReasoningEffortCaps.ts"; /** @@ -340,26 +340,29 @@ export function sanitizeReasoningEffortForProvider( return body; } + // Generic learned clamp (downgrade-only: greatest accepted <= demand). + // Sits AFTER the per-provider early returns by design: deepseek/command-code/ + // ollama-cloud have deliberate static translations that take precedence; the + // learned set governs every other provider and all effort values, before the + // xhigh/max static fallbacks below. + const learnedSet = getLearnedReasoningEffort(provider, modelStr); + if (learnedSet && learnedSet.size > 0 && !learnedSet.has(effortStr)) { + const clamped = clampToLearned(effortStr, learnedSet); + if (clamped && clamped !== effortStr) { + log?.info?.( + "REASONING_SANITIZE", + `${provider}/${modelStr}: clamped reasoning_effort ${effortStr} → ${clamped} (learned)` + ); + return writeEffortValue(b, clamped, c); + } + } + const supportsXHigh = supportsXHighEffort(provider, modelStr); const supportsMax = supportsMaxEffortForProvider(provider, modelStr); - // Highest value we've actually seen this provider+model accept in a real - // upstream 4xx (learnedReasoningEffortCaps.ts) — takes priority over the - // static registry (which defaults to "supports everything" when there's no - // entry, e.g. custom OpenAI-compatible connections) and over the hardcoded - // "high" fallback below (which isn't always valid either). - const learnedCap = getLearnedReasoningEffort(provider, modelStr); - const learnedRank = learnedCap ? REASONING_EFFORT_ORDER.indexOf(learnedCap) : -1; // ── xhigh handling ────────────────────────────────────────────────────── // xhigh is OmniRoute-internal. Map it to the best effort the model accepts. if (effortStr === "xhigh") { - if (learnedCap && learnedRank < REASONING_EFFORT_ORDER.indexOf("xhigh")) { - log?.info?.( - "REASONING_SANITIZE", - `${provider}/${modelStr}: clamped reasoning_effort xhigh → ${learnedCap} (learned)` - ); - return writeEffortValue(b, learnedCap, c); - } if (supportsXHigh) return body; // model accepts xhigh natively if (supportsMax) { log?.info?.( @@ -384,13 +387,6 @@ export function sanitizeReasoningEffortForProvider( // upstream, and if it 400s the user gets a clear signal. This prevents // new models from being unusable for weeks until they're whitelisted (#8057). if (effortStr === "max") { - if (learnedCap && learnedRank < REASONING_EFFORT_ORDER.indexOf("max")) { - log?.info?.( - "REASONING_SANITIZE", - `${provider}/${modelStr}: clamped reasoning_effort max → ${learnedCap} (learned)` - ); - return writeEffortValue(b, learnedCap, c); - } if (supportsMax) return body; // explicitly known to accept max // A model that explicitly advertises its accepted tiers is safe to normalize. @@ -407,7 +403,7 @@ export function sanitizeReasoningEffortForProvider( )?.supportedThinkingEfforts; const maxFallback = Array.isArray(explicitEfforts) && !explicitEfforts.includes("max") - ? ["xhigh", "high", "medium", "low"].find((tier) => explicitEfforts.includes(tier)) + ? ["ultra", "xhigh", "high", "medium", "low"].find((tier) => explicitEfforts.includes(tier)) : undefined; if (maxFallback) { log?.info?.( diff --git a/open-sse/services/learnedReasoningEffortCaps.ts b/open-sse/services/learnedReasoningEffortCaps.ts index b0125d8683..ef7e7b5f32 100644 --- a/open-sse/services/learnedReasoningEffortCaps.ts +++ b/open-sse/services/learnedReasoningEffortCaps.ts @@ -6,12 +6,14 @@ * Same shape as `learnedThinkingCaps.ts` (thinking_budget), generalized from a * numeric budget to an ordinal reasoning_effort scale: on a 4xx whose body * enumerates the accepted values, `base.ts`'s executor calls - * `recordLearnedReasoningEffort`, which stores the highest recognized value in a - * module-level Map keyed "provider:model" (lowercased). Subsequent requests for - * the same provider+model read the cap via `getLearnedReasoningEffort` (consulted - * by `sanitizeReasoningEffortForProvider` in `executors/base/reasoningEffort.ts`) + * `recordLearnedReasoningEffort`, which stores the accepted set in a module-level + * Map keyed "provider:model" (lowercased). Subsequent requests for the same + * provider+model read the set via `getLearnedReasoningEffort` (consulted by + * `sanitizeReasoningEffortForProvider` in `executors/base/reasoningEffort.ts`) * so the 4xx→retry round-trip is paid at most once per process per provider+model. * + * `clampToLearned` implements downgrade-only clamping: greatest accepted <= demand. + * * In-memory only (same operator-accepted tradeoff as the thinking-budget cache): * restart resets, the first request after a restart may re-learn at the cost of * one upstream 4xx. @@ -25,10 +27,11 @@ export const REASONING_EFFORT_ORDER: readonly string[] = [ "high", "xhigh", "max", + "ultra", ]; -// key: `${provider}:${model}` lowercased → highest value known to be accepted. -const learnedCaps = new Map(); +// key: `${provider}:${model}` lowercased → accepted set. +const learnedCaps = new Map>(); function buildKey(provider: string | null | undefined, model: string | null | undefined): string { const p = typeof provider === "string" ? provider.trim().toLowerCase() : ""; @@ -41,61 +44,89 @@ function rankOf(value: string): number { return REASONING_EFFORT_ORDER.indexOf(value); } +function isSubset(a: Set, b: Set): boolean { + for (const v of a) if (!b.has(v)) return false; + return true; +} + /** - * Return the learned cap for provider+model, or null when nothing has been - * learned yet (no upstream 4xx recorded). Keyed case-insensitively. + * Return the learned accepted set for provider+model, or null when nothing has + * been learned yet (no upstream 4xx recorded). Keyed case-insensitively. */ export function getLearnedReasoningEffort( provider: string | null | undefined, model: string | null | undefined -): string | null { +): Set | null { const key = buildKey(provider, model); if (!key) return null; - return learnedCaps.get(key) ?? null; + const v = learnedCaps.get(key); + return v ? new Set(v) : null; } /** * Record that `acceptedValues` is the enum the upstream advertised for - * provider+model, and store the highest recognized value as the learned cap. - * Returns the stored value, or null when `acceptedValues` contained no token - * from `REASONING_EFFORT_ORDER` (nothing usable to learn) or the key is unusable. + * provider+model, and store the accepted set. Returns the stored set, or null + * when `acceptedValues` contained no token from `REASONING_EFFORT_ORDER`. * - * Always monotonically decreases: if a cap already stored ranks lower than the - * newly computed highest, the stored (lower) value wins and is returned - * unchanged. This keeps a later, laxer-looking response (or a race between - * concurrent requests) from ratcheting the cap back up. + * Monotonically non-expanding: if existing ⊆ newSet, keep existing (never + * re-expand); if newSet ⊂ existing, replace (more restrictive); if neither + * subset, keep existing. */ export function recordLearnedReasoningEffort( provider: string | null | undefined, model: string | null | undefined, acceptedValues: string[] -): string | null { +): Set | null { const key = buildKey(provider, model); if (!key) return null; - let best: string | null = null; - let bestRank = -1; + const newSet = new Set(); for (const raw of acceptedValues) { - const rank = rankOf(raw); - if (rank > bestRank) { - bestRank = rank; - best = raw; - } + const lowered = typeof raw === "string" ? raw.trim().toLowerCase() : ""; + if (lowered && REASONING_EFFORT_ORDER.includes(lowered)) newSet.add(lowered); } - if (best === null) return null; + if (newSet.size === 0) return null; const existing = learnedCaps.get(key); - if (existing !== undefined && rankOf(existing) <= bestRank) { - return existing; // already learned an equal-or-lower cap; keep it + if (existing !== undefined) { + // Defensive copies: never hand out the live cached Set. + if (isSubset(existing, newSet)) return new Set(existing); + if (isSubset(newSet, existing)) { + learnedCaps.set(key, newSet); + return new Set(newSet); + } + return new Set(existing); + } + learnedCaps.set(key, newSet); + return new Set(newSet); +} + +/** + * Return the greatest accepted value <= effortStr (downgrade only), or null + * if effortStr is already accepted, below the minimum, or not in ORDER. + */ +export function clampToLearned(effortStr: string, accepted: Set): string | null { + if (!effortStr || accepted.has(effortStr)) return null; + const rank = rankOf(effortStr); + if (rank === -1) return null; + const minRank = Math.min(...[...accepted].map((v) => rankOf(v))); + if (rank < minRank) return null; + let best: string | null = null; + let bestRank = -1; + for (const v of accepted) { + const r = rankOf(v); + if (r <= rank && r > bestRank) { + bestRank = r; + best = v; + } } - learnedCaps.set(key, best); return best; } -// Matches both prose shapes observed: OVH's `@ai-sdk/openai-compatible` -// deserializer ("expected one of `a`, `b`") and a generic vendor prose form -// ("Supported types are a, b, and c"). -const LIST_INTRO = /(?:expected one of|supported (?:types|values) are)[:\s]*([^.]+)/i; +// Matches prose shapes: OVH's "@ai-sdk/openai-compatible" deserializer +// ("expected one of `a`, `b`"), generic ("Supported types are a, b, and c"), +// and "please use a, b, or c". +const LIST_INTRO = /(?:expected one of|supported (?:types|values) are|please use)[:\s]*([^.]+)/i; /** * Extract the upstream-advertised accepted reasoning_effort values from a 4xx @@ -108,13 +139,14 @@ export function parseReasoningEffortEnum(errText: unknown): string[] | null { const match = LIST_INTRO.exec(errText); if (!match) return null; const tokens = match[1] - .split(/,|\band\b|&/i) + .split(/,|\b(?:and|or)\b|&/i) .map((t) => t .replace(/`/g, "") .replace(/\([^)]*\)/g, "") .trim() .toLowerCase() + .replace(/^[^a-z]+|[^a-z]+$/g, "") ) .filter((t) => t.length > 0 && REASONING_EFFORT_ORDER.includes(t)); return tokens.length > 0 ? tokens : null; diff --git a/tests/unit/learned-reasoning-effort-caps.test.ts b/tests/unit/learned-reasoning-effort-caps.test.ts index 5d69a342c3..c98eb8fad0 100644 --- a/tests/unit/learned-reasoning-effort-caps.test.ts +++ b/tests/unit/learned-reasoning-effort-caps.test.ts @@ -18,7 +18,7 @@ after(() => { // ── REASONING_EFFORT_ORDER ────────────────────────────────────────────────── -test("REASONING_EFFORT_ORDER is none < minimal < low < medium < high < xhigh < max", () => { +test("REASONING_EFFORT_ORDER is none < minimal < low < medium < high < xhigh < max < ultra", () => { assert.deepEqual(REASONING_EFFORT_ORDER, [ "none", "minimal", @@ -27,6 +27,24 @@ test("REASONING_EFFORT_ORDER is none < minimal < low < medium < high < xhigh < m "high", "xhigh", "max", + "ultra", + ]); +}); + +test("REASONING_EFFORT_ORDER ends with ultra", () => { + assert.equal(REASONING_EFFORT_ORDER.at(-1), "ultra"); +}); +test("parseReasoningEffortEnum extracts please use low, high, or max", () => { + const err = + "This model always engages in thinking and cannot be disabled; please use low, high, or max"; + assert.deepEqual(parseReasoningEffortEnum(err), ["low", "high", "max"]); +}); +test("parseReasoningEffortEnum extracts please use with ultra", () => { + assert.deepEqual(parseReasoningEffortEnum("please use low, high, max, ultra"), [ + "low", + "high", + "max", + "ultra", ]); }); @@ -70,9 +88,10 @@ test("records the highest recognized value from the accepted list", () => { "medium", "low", "minimal", - ]); - assert.equal(learned, "high"); - assert.equal(getLearnedReasoningEffort("ovh", "qwen3-coder-30b-a3b-instruct"), "high"); + ]) as unknown as Set; + assert.ok(learned instanceof Set); + assert.ok(learned.has("high")); + assert.equal((getLearnedReasoningEffort("ovh", "qwen3-coder-30b-a3b-instruct") as unknown as Set).has("high"), true); }); test("returns null and stores nothing when acceptedValues has no recognized token", () => { @@ -89,26 +108,77 @@ test("monotonic decrease: a later, higher accepted-list never ratchets the cap b "medium", "high", "xhigh", - ]); - assert.equal(learned, "medium"); - assert.equal(getLearnedReasoningEffort("acme", "model-x"), "medium"); + ]) as unknown as Set; + assert.equal(learned.size, 3); + assert.ok(learned.has("medium")); + assert.equal((getLearnedReasoningEffort("acme", "model-x") as unknown as Set).size, 3); }); test("a later, lower accepted-list does ratchet the cap down", () => { recordLearnedReasoningEffort("acme", "model-x", ["none", "low", "medium", "high"]); - const learned = recordLearnedReasoningEffort("acme", "model-x", ["none", "low"]); - assert.equal(learned, "low"); - assert.equal(getLearnedReasoningEffort("acme", "model-x"), "low"); + const learned = recordLearnedReasoningEffort("acme", "model-x", ["none", "low"]) as unknown as Set; + assert.equal(learned.size, 2); + assert.ok(learned.has("low")); + assert.equal((getLearnedReasoningEffort("acme", "model-x") as unknown as Set).size, 2); }); +test("clampToLearned medium→low when accepted is low,high,max", async () => { + const { clampToLearned } = await import("../../open-sse/services/learnedReasoningEffortCaps.ts"); + assert.equal(clampToLearned("medium", new Set(["low", "high", "max"])), "low"); +}); +test("clampToLearned xhigh→high when accepted is low,high,max", async () => { + const { clampToLearned } = await import("../../open-sse/services/learnedReasoningEffortCaps.ts"); + assert.equal(clampToLearned("xhigh", new Set(["low", "high", "max"])), "high"); +}); +test("clampToLearned ultra→max when accepted is low,high,max", async () => { + const { clampToLearned } = await import("../../open-sse/services/learnedReasoningEffortCaps.ts"); + assert.equal(clampToLearned("ultra", new Set(["low", "high", "max"])), "max"); +}); +test("clampToLearned ultra→medium when accepted is low,medium", async () => { + const { clampToLearned } = await import("../../open-sse/services/learnedReasoningEffortCaps.ts"); + assert.equal(clampToLearned("ultra", new Set(["low", "medium"])), "medium"); +}); +test("clampToLearned high→medium when accepted is low,medium", async () => { + const { clampToLearned } = await import("../../open-sse/services/learnedReasoningEffortCaps.ts"); + assert.equal(clampToLearned("high", new Set(["low", "medium"])), "medium"); +}); +test("clampToLearned returns null when already accepted", async () => { + const { clampToLearned } = await import("../../open-sse/services/learnedReasoningEffortCaps.ts"); + assert.equal(clampToLearned("low", new Set(["low", "high", "max"])), null); +}); +test("clampToLearned returns null when effort < min (no upgrade)", async () => { + const { clampToLearned } = await import("../../open-sse/services/learnedReasoningEffortCaps.ts"); + assert.equal(clampToLearned("low", new Set(["high", "max"])), null); +}); +test("clampToLearned returns null for turbo (not in ORDER)", async () => { + const { clampToLearned } = await import("../../open-sse/services/learnedReasoningEffortCaps.ts"); + assert.equal(clampToLearned("turbo", new Set(["low", "high", "max"])), null); +}); +test("clampToLearned returns null when effort is none but accepted is low,high,max", async () => { + const { clampToLearned } = await import("../../open-sse/services/learnedReasoningEffortCaps.ts"); + assert.equal(clampToLearned("none", new Set(["low", "high", "max"])), null); +}); +test("recordLearned stores Set and getLearned returns Set", () => { + const s = recordLearnedReasoningEffort("acme", "m1", ["low", "high", "max"]); + assert.ok(s instanceof Set); + assert.deepEqual([...(s as unknown as Set)].sort(), ["high", "low", "max"]); + const g = getLearnedReasoningEffort("acme", "m1"); + assert.ok(g instanceof Set); +}); +test("monotonicity incomparable: keep existing when neither subset", () => { + recordLearnedReasoningEffort("acme", "m4", ["low", "high", "max"]); + const s4 = recordLearnedReasoningEffort("acme", "m4", ["low", "medium"]); + assert.equal((s4 as unknown as Set).size, 3); + assert.ok((s4 as unknown as Set).has("high")); +}); test("getLearnedReasoningEffort returns null for unknown provider+model", () => { assert.equal(getLearnedReasoningEffort("acme", "unknown-model"), null); }); test("getLearnedReasoningEffort is keyed case-insensitively on provider+model", () => { recordLearnedReasoningEffort("OVH", "Qwen3-Coder-30B", ["none", "high"]); - assert.equal(getLearnedReasoningEffort("ovh", "qwen3-coder-30b"), "high"); - assert.equal(getLearnedReasoningEffort("OVH", "QWEN3-CODER-30B"), "high"); + assert.ok((getLearnedReasoningEffort("ovh", "qwen3-coder-30b") as unknown as Set).has("high")); + assert.ok((getLearnedReasoningEffort("OVH", "QWEN3-CODER-30B") as unknown as Set).has("high")); }); test("different providers for the same model id have independent caps", () => { diff --git a/tests/unit/reasoning-effort-clamp-and-retry.test.ts b/tests/unit/reasoning-effort-clamp-and-retry.test.ts index a97ac16df0..fa925216c6 100644 --- a/tests/unit/reasoning-effort-clamp-and-retry.test.ts +++ b/tests/unit/reasoning-effort-clamp-and-retry.test.ts @@ -66,9 +66,8 @@ test("422 'unknown variant xhigh, expected one of ...' clamps reasoning_effort a assert.equal(capturedBodies.length, 2); assert.equal(capturedBodies[0].reasoning_effort, "xhigh"); assert.equal(capturedBodies[1].reasoning_effort, "high"); - assert.equal( - getLearnedReasoningEffort("openai-compatible-chat-eaff6869", "qwen3-coder-30b-a3b-instruct"), - "high" + assert.ok( + (getLearnedReasoningEffort("openai-compatible-chat-eaff6869", "qwen3-coder-30b-a3b-instruct") as unknown as Set).has("high") ); assert.equal(result.response.status, 200); } finally { @@ -108,3 +107,125 @@ test("a second request for the same provider+model sends the learned value on th globalThis.fetch = originalFetch; } }); + +test("400 please use low, high, or max clamps and retries once", async () => { + const executor = new SimpleExecutor(); + const originalFetch = globalThis.fetch; + const capturedBodies: Record[] = []; + const BODY_400_PLEASE_USE = JSON.stringify({ + error: { message: "This model always engages in thinking and cannot be disabled; please use low, high, or max" }, + }); + + globalThis.fetch = async (_url: string | URL | Request, init: RequestInit = {}) => { + const body = JSON.parse(String(init.body)); + capturedBodies.push(body); + if (capturedBodies.length === 1) { + return new Response(BODY_400_PLEASE_USE, { + status: 400, + headers: { "Content-Type": "application/json" }, + }); + } + return new Response(JSON.stringify({ ok: true }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + }; + + try { + const result = await executor.execute({ + model: "x-preview-f-free", + body: { reasoning_effort: "medium" }, + stream: false, + credentials: {}, + }); + assert.equal(capturedBodies.length, 2); + assert.equal(capturedBodies[0].reasoning_effort, "medium"); + assert.equal(capturedBodies[1].reasoning_effort, "low"); + const learned = getLearnedReasoningEffort("openai-compatible-chat-eaff6869", "x-preview-f-free") as unknown as Set; + assert.ok(learned instanceof Set); + assert.ok(learned.has("low")); + assert.ok(learned.has("high")); + assert.equal(result.response.status, 200); + } finally { + globalThis.fetch = originalFetch; + } +}); + +test("400 please use low, medium with ultra retries to medium", async () => { + const executor = new SimpleExecutor(); + const originalFetch = globalThis.fetch; + const capturedBodies: Record[] = []; + const BODY_400_ULTRA = JSON.stringify({ + error: { message: "please use low, medium" }, + }); + + globalThis.fetch = async (_url: string | URL | Request, init: RequestInit = {}) => { + const body = JSON.parse(String(init.body)); + capturedBodies.push(body); + if (capturedBodies.length === 1) { + return new Response(BODY_400_ULTRA, { + status: 400, + headers: { "Content-Type": "application/json" }, + }); + } + return new Response(JSON.stringify({ ok: true }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + }; + + try { + const result = await executor.execute({ + model: "x-preview-f-free-2", + body: { reasoning_effort: "ultra" }, + stream: false, + credentials: {}, + }); + assert.equal(capturedBodies.length, 2); + assert.equal(capturedBodies[0].reasoning_effort, "ultra"); + assert.equal(capturedBodies[1].reasoning_effort, "medium"); + assert.equal(result.response.status, 200); + } finally { + globalThis.fetch = originalFetch; + } +}); + +test("no-op clamp does not retry: learned {high,max} with low request stays single-fetch", async () => { + const executor = new SimpleExecutor(); + const originalFetch = globalThis.fetch; + const capturedBodies: Record[] = []; + const BODY_400_HIGH_MAX = JSON.stringify({ + error: { message: "please use high, or max" }, + }); + + globalThis.fetch = async (_url: string | URL | Request, init: RequestInit = {}) => { + const body = JSON.parse(String(init.body)); + capturedBodies.push(body); + if (capturedBodies.length === 1) { + return new Response(BODY_400_HIGH_MAX, { + status: 400, + headers: { "Content-Type": "application/json" }, + }); + } + return new Response(JSON.stringify({ ok: true }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + }; + + try { + // low is below the learned minimum {high,max}: downgrade-only passthrough, + // sanitizer leaves the body unchanged -> no identical-body retry. + const result = await executor.execute({ + model: "x-preview-f-free-3", + body: { reasoning_effort: "low" }, + stream: false, + credentials: {}, + }); + assert.equal(capturedBodies.length, 1); + assert.equal(capturedBodies[0].reasoning_effort, "low"); + assert.equal(result.response.status, 400); + } finally { + globalThis.fetch = originalFetch; + } +}); diff --git a/tests/unit/reasoning-effort-learned-capability.test.ts b/tests/unit/reasoning-effort-learned-capability.test.ts index 210a451341..9dab460d8f 100644 --- a/tests/unit/reasoning-effort-learned-capability.test.ts +++ b/tests/unit/reasoning-effort-learned-capability.test.ts @@ -89,3 +89,67 @@ test("deepseek's non-ordinal max<->xhigh translation is untouched by the learned // deepseek's special case returns early — xhigh -> max, never reaches the catch-all. assert.equal(result.reasoning_effort, "max"); }); + +test("proactive clamp: medium→low for learned {low,high,max}", () => { + recordLearnedReasoningEffort("opencode-zen-direct", "x-preview-f-free", ["low", "high", "max"]); + const out = sanitizeReasoningEffortForProvider( + { reasoning_effort: "medium", model: "x-preview-f-free" }, + "opencode-zen-direct", + "x-preview-f-free" + ) as { reasoning_effort: string }; + assert.equal(out.reasoning_effort, "low"); +}); +test("proactive clamp: xhigh→high for learned {low,high,max}", () => { + recordLearnedReasoningEffort("opencode-zen-direct", "x-preview-f-free-2", ["low", "high", "max"]); + const out = sanitizeReasoningEffortForProvider( + { reasoning_effort: "xhigh", model: "x-preview-f-free-2" }, + "opencode-zen-direct", + "x-preview-f-free-2" + ) as { reasoning_effort: string }; + assert.equal(out.reasoning_effort, "high"); +}); +test("proactive clamp: ultra→max for learned {low,high,max}", () => { + recordLearnedReasoningEffort("opencode-zen-direct", "x-preview-f-free-3", ["low", "high", "max"]); + const out = sanitizeReasoningEffortForProvider( + { reasoning_effort: "ultra", model: "x-preview-f-free-3" }, + "opencode-zen-direct", + "x-preview-f-free-3" + ) as { reasoning_effort: string }; + assert.equal(out.reasoning_effort, "max"); +}); +test("proactive clamp: ultra→medium for learned {low,medium}", () => { + recordLearnedReasoningEffort("acme", "m", ["low", "medium"]); + const out = sanitizeReasoningEffortForProvider( + { reasoning_effort: "ultra", model: "m" }, + "acme", + "m" + ) as { reasoning_effort: string }; + assert.equal(out.reasoning_effort, "medium"); +}); +test("proactive clamp: high→medium for learned {low,medium}", () => { + recordLearnedReasoningEffort("acme", "m2", ["low", "medium"]); + const out = sanitizeReasoningEffortForProvider( + { reasoning_effort: "high", model: "m2" }, + "acme", + "m2" + ) as { reasoning_effort: string }; + assert.equal(out.reasoning_effort, "medium"); +}); +test("no upgrade: low stays low for learned {high,max}", () => { + recordLearnedReasoningEffort("acme", "m3", ["high", "max"]); + const out = sanitizeReasoningEffortForProvider( + { reasoning_effort: "low", model: "m3" }, + "acme", + "m3" + ) as { reasoning_effort: string }; + assert.equal(out.reasoning_effort, "low"); +}); +test("custom model ultra→medium for learned {low,medium}", () => { + recordLearnedReasoningEffort("openai-compatible-chat-eaff6869", "qwen3-coder-30b-a3b-instruct-2", ["low", "medium"]); + const out = sanitizeReasoningEffortForProvider( + { reasoning_effort: "ultra", model: "qwen3-coder-30b-a3b-instruct-2" }, + "openai-compatible-chat-eaff6869", + "qwen3-coder-30b-a3b-instruct-2" + ) as { reasoning_effort: string }; + assert.equal(out.reasoning_effort, "medium"); +});