diff --git a/src/reasoning-effort.ts b/src/reasoning-effort.ts index ff168f4b7d..81b35b7af5 100644 --- a/src/reasoning-effort.ts +++ b/src/reasoning-effort.ts @@ -14,6 +14,16 @@ export const CODEX_REASONING_LEVELS: { effort: string; description: string }[] = const CODEX_REASONING_ORDER = CODEX_REASONING_LEVELS.map(l => l.effort); const CODEX_REASONING_SET = new Set(CODEX_REASONING_ORDER); +/** + * Sentinel wire value in reasoningEffortMap / modelReasoningEffortMap to explicitly + * omit the reasoning_effort field from the upstream wire request (issue #2356). + */ +export const REASONING_EFFORT_OMIT_SENTINEL = "__omit__"; + +export function isReasoningEffortOmitted(wireEffort: string | undefined): boolean { + return wireEffort === REASONING_EFFORT_OMIT_SENTINEL; +} + /** True when `effort` is a member of the Codex reasoning ladder (low..ultra). */ export function isCodexReasoningEffort(effort: string): boolean { return CODEX_REASONING_SET.has(effort); @@ -170,7 +180,10 @@ export function mapReasoningEffort(provider: OcxProviderConfig, modelId: string, const boundary = requested === "ultra" ? "max" : requested; const wireMap = reasoningEffortMapFor(provider, modelId); - if (wireMap && Object.prototype.hasOwnProperty.call(wireMap, boundary)) return wireMap[boundary]; + if (wireMap && Object.prototype.hasOwnProperty.call(wireMap, boundary)) { + const mapped = wireMap[boundary]; + return mapped === REASONING_EFFORT_OMIT_SENTINEL ? undefined : mapped; + } const supported = configuredReasoningEfforts(provider, modelId); const codexEffort = supported !== undefined ? clampToSupportedCodexEffort(boundary, supported) : requestToCodexEffort(boundary); @@ -178,6 +191,10 @@ export function mapReasoningEffort(provider: OcxProviderConfig, modelId: string, // Belt for the odd config where the supported ladder is ultra-only and the clamp lands on it. const wire = codexEffort === "ultra" ? "max" : codexEffort; - if (wireMap && Object.prototype.hasOwnProperty.call(wireMap, wire)) return wireMap[wire]; + if (wireMap && Object.prototype.hasOwnProperty.call(wireMap, wire)) { + const mapped = wireMap[wire]; + return mapped === REASONING_EFFORT_OMIT_SENTINEL ? undefined : mapped; + } + if (wire === REASONING_EFFORT_OMIT_SENTINEL) return undefined; return wire; } diff --git a/tests/reasoning-effort.test.ts b/tests/reasoning-effort.test.ts index 269da1a81a..cf34b8070b 100644 --- a/tests/reasoning-effort.test.ts +++ b/tests/reasoning-effort.test.ts @@ -973,4 +973,66 @@ describe("stale reasoning-ladder self-heal", () => { }; expect(configuredReasoningEfforts(prov, "model")).toEqual([]); }); + + test("per-effort omission sentinel (__omit__) drops reasoning_effort from the wire (#2356)", () => { + const ollamaProv: OcxProviderConfig = { + adapter: "openai-chat", + baseUrl: "http://localhost:11434/v1", + modelReasoningEfforts: { + "qwen3.8-uncensored:27b-q4": ["low", "medium", "high", "xhigh", "max"], + }, + modelReasoningEffortMap: { + "qwen3.8-uncensored:27b-q4": { + low: "low", + medium: "medium", + high: "__omit__", + xhigh: "__omit__", + max: "__omit__", + }, + }, + }; + + // High/xhigh/max/ultra map to undefined (omitted on wire) + expect(mapReasoningEffort(ollamaProv, "qwen3.8-uncensored:27b-q4", "high")).toBeUndefined(); + expect(mapReasoningEffort(ollamaProv, "qwen3.8-uncensored:27b-q4", "xhigh")).toBeUndefined(); + expect(mapReasoningEffort(ollamaProv, "qwen3.8-uncensored:27b-q4", "max")).toBeUndefined(); + expect(mapReasoningEffort(ollamaProv, "qwen3.8-uncensored:27b-q4", "ultra")).toBeUndefined(); + + // Low and medium map to explicit wire values + expect(mapReasoningEffort(ollamaProv, "qwen3.8-uncensored:27b-q4", "low")).toBe("low"); + expect(mapReasoningEffort(ollamaProv, "qwen3.8-uncensored:27b-q4", "medium")).toBe("medium"); + + const fallbackProv: OcxProviderConfig = { + ...ollamaProv, + modelReasoningEfforts: { + "qwen3.8-uncensored:27b-q4": ["low", "high"], + }, + modelReasoningEffortMap: { + "qwen3.8-uncensored:27b-q4": { high: "__omit__" }, + }, + }; + expect(mapReasoningEffort(fallbackProv, "qwen3.8-uncensored:27b-q4", "xhigh")).toBeUndefined(); + + // Verify in openai-chat adapter buildRequest: field is completely omitted when mapped to __omit__ + const adapter = createOpenAIChatAdapter(ollamaProv); + const reqMax = adapter.buildRequest({ + modelId: "qwen3.8-uncensored:27b-q4", + stream: false, + context: { messages: [{ role: "user", content: "deep thinking" }] }, + options: { reasoning: "max" }, + } as OcxParsedRequest); + const bodyMax = JSON.parse(reqMax.body as string); + expect(bodyMax.reasoning_effort).toBeUndefined(); + expect(bodyMax).not.toHaveProperty("reasoning_effort"); + + // Field is present when mapped to a real string + const reqLow = adapter.buildRequest({ + modelId: "qwen3.8-uncensored:27b-q4", + stream: false, + context: { messages: [{ role: "user", content: "fast turn" }] }, + options: { reasoning: "low" }, + } as OcxParsedRequest); + const bodyLow = JSON.parse(reqLow.body as string); + expect(bodyLow.reasoning_effort).toBe("low"); + }); });