diff --git a/src/agents/pi-embedded-runner-extraparams.test.ts b/src/agents/pi-embedded-runner-extraparams.test.ts index a6d3e9191e8..3f5189a40ea 100644 --- a/src/agents/pi-embedded-runner-extraparams.test.ts +++ b/src/agents/pi-embedded-runner-extraparams.test.ts @@ -202,6 +202,58 @@ describe("applyExtraParamsToAgent", () => { return calls[0]?.headers; } + it("does not inject reasoning when thinkingLevel is off (default) for OpenRouter", () => { + // Regression: "off" is a truthy string, so the old code injected + // reasoning: { effort: "none" }, causing a 400 on models that require + // reasoning (e.g. deepseek/deepseek-r1). + const payloads: Record[] = []; + const baseStreamFn: StreamFn = (_model, _context, options) => { + const payload: Record = { model: "deepseek/deepseek-r1" }; + options?.onPayload?.(payload); + payloads.push(payload); + return {} as ReturnType; + }; + const agent = { streamFn: baseStreamFn }; + + applyExtraParamsToAgent(agent, undefined, "openrouter", "deepseek/deepseek-r1", undefined, "off"); + + const model = { + api: "openai-completions", + provider: "openrouter", + id: "deepseek/deepseek-r1", + } as Model<"openai-completions">; + const context: Context = { messages: [] }; + void agent.streamFn?.(model, context, {}); + + expect(payloads).toHaveLength(1); + expect(payloads[0]).not.toHaveProperty("reasoning"); + expect(payloads[0]).not.toHaveProperty("reasoning_effort"); + }); + + it("injects reasoning.effort when thinkingLevel is non-off for OpenRouter", () => { + const payloads: Record[] = []; + const baseStreamFn: StreamFn = (_model, _context, options) => { + const payload: Record = {}; + options?.onPayload?.(payload); + payloads.push(payload); + return {} as ReturnType; + }; + const agent = { streamFn: baseStreamFn }; + + applyExtraParamsToAgent(agent, undefined, "openrouter", "openrouter/auto", undefined, "low"); + + const model = { + api: "openai-completions", + provider: "openrouter", + id: "openrouter/auto", + } as Model<"openai-completions">; + const context: Context = { messages: [] }; + void agent.streamFn?.(model, context, {}); + + expect(payloads).toHaveLength(1); + expect(payloads[0]?.reasoning).toEqual({ effort: "low" }); + }); + it("adds OpenRouter attribution headers to stream options", () => { const { calls, agent } = createOptionsCaptureAgent(); diff --git a/src/agents/pi-embedded-runner/extra-params.ts b/src/agents/pi-embedded-runner/extra-params.ts index 8ebacf6df68..66b077af232 100644 --- a/src/agents/pi-embedded-runner/extra-params.ts +++ b/src/agents/pi-embedded-runner/extra-params.ts @@ -435,24 +435,31 @@ function createOpenRouterWrapper( // only the nested one is sent. delete payloadObj.reasoning_effort; - const existingReasoning = payloadObj.reasoning; + // When thinking is "off", do not inject reasoning at all. + // Some models (e.g. deepseek/deepseek-r1) require reasoning and reject + // { effort: "none" } with "Reasoning is mandatory for this endpoint and + // cannot be disabled." Omitting the field lets each model use its own + // default reasoning behavior. + if (thinkingLevel !== "off") { + const existingReasoning = payloadObj.reasoning; - // OpenRouter treats reasoning.effort and reasoning.max_tokens as - // alternative controls. If max_tokens is already present, do not - // inject effort and do not overwrite caller-supplied reasoning. - if ( - existingReasoning && - typeof existingReasoning === "object" && - !Array.isArray(existingReasoning) - ) { - const reasoningObj = existingReasoning as Record; - if (!("max_tokens" in reasoningObj) && !("effort" in reasoningObj)) { - reasoningObj.effort = mapThinkingLevelToOpenRouterReasoningEffort(thinkingLevel); + // OpenRouter treats reasoning.effort and reasoning.max_tokens as + // alternative controls. If max_tokens is already present, do not + // inject effort and do not overwrite caller-supplied reasoning. + if ( + existingReasoning && + typeof existingReasoning === "object" && + !Array.isArray(existingReasoning) + ) { + const reasoningObj = existingReasoning as Record; + if (!("max_tokens" in reasoningObj) && !("effort" in reasoningObj)) { + reasoningObj.effort = mapThinkingLevelToOpenRouterReasoningEffort(thinkingLevel); + } + } else if (!existingReasoning) { + payloadObj.reasoning = { + effort: mapThinkingLevelToOpenRouterReasoningEffort(thinkingLevel), + }; } - } else if (!existingReasoning) { - payloadObj.reasoning = { - effort: mapThinkingLevelToOpenRouterReasoningEffort(thinkingLevel), - }; } } onPayload?.(payload);