From 99cfb3dab26a278dd3ea89a413c4a37f971ece9d Mon Sep 17 00:00:00 2001 From: Robby Date: Sun, 22 Feb 2026 18:14:12 +0100 Subject: [PATCH] fix(openrouter): pass reasoning.effort based on thinking level (#14664) (#17236) * fix(openrouter): pass reasoning.effort to OpenRouter API (#14664) * Agents: pass thinkLevel to extra-params wrapper * Changelog: note fix/openrouter-reasoning-effort-14664 OpenRouter fix * Changelog: fix OpenRouter entry text --------- Co-authored-by: Vincent Koc --- CHANGELOG.md | 1 + src/agents/pi-embedded-runner/extra-params.ts | 57 +++++++++++++++++-- src/agents/pi-embedded-runner/run/attempt.ts | 1 + 3 files changed, 53 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fb3633ea9..d6f5eeb7d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ Docs: https://docs.openclaw.ai ### Fixes +- Providers/OpenRouter: map `/think` levels to `reasoning.effort` in embedded runs while preserving explicit `reasoning.max_tokens` payloads. (#17236) Thanks @robbyczgw-cla. - Gateway/OpenRouter: preserve stored session provider when model IDs are vendor-prefixed (for example, `anthropic/...`) so follow-up turns do not incorrectly route to direct provider APIs. (#22753) Thanks @dndodson. - Providers/OpenRouter: preserve the required `openrouter/` prefix for OpenRouter-native model IDs during model-ref normalization. (#12942) Thanks @omair445. - Providers/OpenRouter: pass through provider routing parameters from model params.provider to OpenRouter request payloads for provider selection controls. (#17148) Thanks @carrotRakko. diff --git a/src/agents/pi-embedded-runner/extra-params.ts b/src/agents/pi-embedded-runner/extra-params.ts index 9b5587b9d..df0812c67 100644 --- a/src/agents/pi-embedded-runner/extra-params.ts +++ b/src/agents/pi-embedded-runner/extra-params.ts @@ -1,6 +1,7 @@ import type { StreamFn } from "@mariozechner/pi-agent-core"; import type { SimpleStreamOptions } from "@mariozechner/pi-ai"; import { streamSimple } from "@mariozechner/pi-ai"; +import type { ThinkLevel } from "../../auto-reply/thinking.js"; import type { OpenClawConfig } from "../../config/config.js"; import { log } from "./logger.js"; @@ -290,19 +291,62 @@ function createAnthropicBetaHeadersWrapper( } /** - * Create a streamFn wrapper that adds OpenRouter app attribution headers. - * These headers allow OpenClaw to appear on OpenRouter's leaderboard. + * Map OpenClaw's ThinkLevel to OpenRouter's reasoning.effort values. + * "off" maps to "none"; all other levels pass through as-is. */ -function createOpenRouterHeadersWrapper(baseStreamFn: StreamFn | undefined): StreamFn { +function mapThinkingLevelToOpenRouterReasoningEffort( + thinkingLevel: ThinkLevel, +): "none" | "minimal" | "low" | "medium" | "high" | "xhigh" { + if (thinkingLevel === "off") { + return "none"; + } + return thinkingLevel; +} + +/** + * Create a streamFn wrapper that adds OpenRouter app attribution headers + * and injects reasoning.effort based on the configured thinking level. + */ +function createOpenRouterWrapper( + baseStreamFn: StreamFn | undefined, + thinkingLevel?: ThinkLevel, +): StreamFn { const underlying = baseStreamFn ?? streamSimple; - return (model, context, options) => - underlying(model, context, { + return (model, context, options) => { + const onPayload = options?.onPayload; + return underlying(model, context, { ...options, headers: { ...OPENROUTER_APP_HEADERS, ...options?.headers, }, + onPayload: (payload) => { + if (thinkingLevel && payload && typeof payload === "object") { + const payloadObj = payload as Record; + 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); + } + } else if (!existingReasoning) { + payloadObj.reasoning = { + effort: mapThinkingLevelToOpenRouterReasoningEffort(thinkingLevel), + }; + } + } + onPayload?.(payload); + }, }); + }; } /** @@ -350,6 +394,7 @@ export function applyExtraParamsToAgent( provider: string, modelId: string, extraParamsOverride?: Record, + thinkingLevel?: ThinkLevel, ): void { const extraParams = resolveExtraParams({ cfg, @@ -380,7 +425,7 @@ export function applyExtraParamsToAgent( if (provider === "openrouter") { log.debug(`applying OpenRouter app attribution headers for ${provider}/${modelId}`); - agent.streamFn = createOpenRouterHeadersWrapper(agent.streamFn); + agent.streamFn = createOpenRouterWrapper(agent.streamFn, thinkingLevel); } // Enable Z.AI tool_stream for real-time tool call streaming. diff --git a/src/agents/pi-embedded-runner/run/attempt.ts b/src/agents/pi-embedded-runner/run/attempt.ts index 383d810e7..d8ba48e15 100644 --- a/src/agents/pi-embedded-runner/run/attempt.ts +++ b/src/agents/pi-embedded-runner/run/attempt.ts @@ -736,6 +736,7 @@ export async function runEmbeddedAttempt( params.provider, params.modelId, params.streamParams, + params.thinkLevel, ); if (cacheTrace) {