OpenRouter reasoning models: chat-completions path never sends the `reasoning` param and drops `reasoning_details`

Author: sergiobe31Created Jun 30, 2026Updated Jun 30, 2026

Summary

On the chat-completions path, PAL never sends OpenRouter's reasoning request field, so reasoning effort can't be controlled via thinking_mode for OpenRouter reasoning models (e.g. z-ai/glm-5.2). Independently, the returned reasoning_details is silently dropped — the handler reads only message.content. The Responses-API path does send the reasoning param, but it also drops the trace, so reasoning output is surfaced on neither path.

Where (upstream 7afc7c1, v9.8.2, providers/openai_compatible.py)

  • _generate_with_responses_endpoint: reads default_reasoning_effort and sends "reasoning": {"effort": …} (≈L416–423); reached only when use_openai_response_api is set.
  • generate_content (chat-completions): completion_params (≈L586–610) never includes a reasoning field; only temperature, max_tokens and a fixed kwargs whitelist (top_p, frequency_penalty, presence_penalty, seed, stop, stream) are forwarded.
  • chat-completions _attempt() reads only response.choices[0].message.content (≈L643); message.reasoning / reasoning_details are never read (the Responses path likewise extracts only output_text).
  • The effort field already exists: default_reasoning_effort (providers/shared/model_capabilities.py:56) — it's just not applied on this path.
  • Note: OpenRouter's reasoning is a nested body object (reasoning: {effort|enabled|max_tokens}), distinct from OpenAI's top-level reasoning_effort — hence OpenRouter-specific, passed via the SDK's extra_body.

Repro (live, z-ai/glm-5.2)

Same prompt, only the reasoning field toggled:

reasoning:{enabled:true}  → message.reasoning_details present · usage…reasoning_tokens = 156
reasoning:{enabled:false} → no reasoning_details · reasoning_tokens = 0

So OpenRouter returns a real reasoning trace when asked — but it never reaches the caller, because the chat-completions handler reads only message.content. And without reasoning:{…} in the body, effort can't be set at all.

Proposed direction (small, reuses existing pieces)

On the chat-completions path, when the provider is OpenRouter and the model is reasoning-capable (gate on supports_extended_thinking), send reasoning via extra_body — effort from default_reasoning_effort (the field the Responses path already uses), optionally overridden by the per-call thinking_mode. Effort vocabulary can be model-specific (e.g. GLM-5.2 accepts xhigh), so the general mapping should stay conservative. Surface the returned reasoning_details on ModelResponse.metadata, consistent with the existing pattern; whether to also expose it in content/history is a design call I'd defer to maintainers.

Gated so non-reasoning models and non-OpenRouter (OpenAI-compatible) providers are untouched.

I have a working PoC doing exactly this (verified live: GLM-5.2 returns its reasoning trace at a controllable effort; non-flagged models unchanged) — a one-file diff vs 7afc7c1: https://github.com/BeehiveInnovations/pal-mcp-server/compare/7afc7c1...sergiobe31:pal-mcp-server:openrouter-reasoning

Happy to open a PR if this is a direction you'd accept — wanted to check first.

Source: BeehiveInnovations/pal-mcp-server