Skip to content

fix(agent): stop replaying reasoning_content to strict openai-compatible providers - #2922

Open
Dani Akash (DaniAkash) wants to merge 2 commits into
mainfrom
fix/reasoning-content-replay-strict-openai-compatible
Open

Dani Akash (DaniAkash) wants to merge 2 commits into
mainfrom
fix/reasoning-content-replay-strict-openai-compatible

Conversation

@DaniAkash

Copy link
Copy Markdown
Contributor

Summary

Fixes #2921. A reasoning model behind an OpenAI-compatible gateway (for example GLM served through api.mistral.ai) fails on the second LLM call of every agent run with HTTP 422 extra_forbidden at messages[N].assistant.reasoning_content.

Root cause

The agent runs on the AI SDK. For a provider of type "OpenAI Compatible" the request is serialized by @ai-sdk/openai-compatible, whose assistant-message serializer re-emits a reasoning part as a top-level reasoning_content field. That field is response-only in the chat-completions schema, and strict gateways (Mistral, Cerebras, Fireworks) validate with extra="forbid" and reject it. Step one succeeds; the assistant turn it produces (reasoning plus tool call) is replayed on the next step, which 422s. The existing stripReasoningParts only cleans the pre-run history copy, so it never touches the turn the multi-step tool loop generates mid-run.

Fix

Strip reasoning from the model messages inside prepareStep, which runs on every step, for the providers that serialize through @ai-sdk/openai-compatible (OpenAI Compatible, LM Studio, Ollama, Moonshot, Qwen Code, GitHub Copilot). Reasoning stays in the durable, client-displayed history; only the replayed request copy is cleaned, and the model generates fresh reasoning each step. Native Anthropic, OpenAI, Google, Azure and Bedrock providers carry signed or encrypted reasoning that must be replayed intact, so they are deliberately left untouched, as is OpenRouter (its own provider).

Tests

  • New unit coverage for the model-message strip: drops the reasoning part while keeping the tool call, keeps assistant text, drops a reasoning-only turn, and leaves user, tool, system and string-content messages untouched.
  • New coverage locking the provider scope: the openai-compatible family returns true, native and OpenRouter providers return false.
  • bun run lint, turbo typecheck --affected and bun fallow pass; the touched agent unit suites are green.

Out of scope

The issue also notes the reasoning-effort dropdown offers "Medium" for a GLM model that only accepts low, high and max. That is a separate model-catalog and UI concern, and the effort value is not forwarded for openai-compatible providers anyway, so it is left for a follow-up.

…ble providers

A reasoning model behind an openai-compatible gateway (for example GLM served through Mistral) returns reasoning that the @ai-sdk/openai-compatible serializer re-sends as a top-level reasoning_content field on the next request. reasoning_content is response-only in the chat-completions schema, so strict gateways (Mistral, Cerebras, Fireworks) reject the replay with 422 and every multi-step agent run fails on its second call. stripReasoningParts already cleans the pre-run history, but the assistant turn the multi-step tool loop generates mid-run never passes through that path, so strip reasoning from the model messages inside prepareStep for providers that serialize through openai-compatible. Native Anthropic, OpenAI, Google, Azure and Bedrock providers carry signed or encrypted reasoning that must be replayed, so they are left untouched.
@github-actions github-actions Bot added the fix label Oct 15, 2026
@github-actions

github-actions Bot commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

✅ Tests passed: 1409/1412

Ran 8 of 16 suites (8 not affected by this change).

Suite Passed Failed Skipped
✅ server-agent 235/235 0 0
✅ server-api 289/289 0 0
✅ server-tools 256/256 0 0
✅ server-browser 10/10 0 0
✅ server-integration 10/10 0 0
✅ server-lib 199/199 0 0
✅ server-root 38/41 0 3
✅ agent 372/372 0 0
⏩ claw-app n/a n/a not affected
⏩ claw-onboard n/a n/a not affected
⏩ app-onboard n/a n/a not affected
⏩ build n/a n/a not affected
⏩ release n/a n/a not affected
⏩ claw-server n/a n/a not affected
⏩ claw-server-quality n/a n/a not affected
⏩ claw-mcp n/a n/a not affected

View workflow run

@greptile-apps

greptile-apps Bot commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerView in GreptileConfidence Score: 4/5

[Medium impact] Filters reasoning from replayed messages for some LLM providers.

The PR appears safe to merge, with a non-blocking gap in coverage of the two-step agent flow.

Findings

  1. P2 Per-step filtering goes untested ▶
Fix with agent prompt
### Issue 1
packages/browseros-agent/apps/server/tests/agent/message-normalization.test.ts:228
The new tests call `stripReasoningFromModelMessages` and `serializesReasoningContent` separately, so they would still pass if `prepareStep` stopped applying the filter. That leaves the reported second-call failure unprotected.

Add a two-step test that returns reasoning plus a tool call on step one, then checks that step two sends the tool call without reasoning for an OpenAI-compatible provider. Also check that a native provider keeps its reasoning.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Summary

Removes assistant reasoning from replayed requests on every agent step for the six providers that use createOpenAICompatible.

  • Compatible-provider replay messages leave out assistant reasoning.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[Messages for next step] --> B[Normalize messages]
  B --> C{Uses compatible serializer?}
  C -->|Yes| D[Remove assistant reasoning]
  C -->|No| E[Keep reasoning]
  D --> F[Compact if needed]
  E --> F
  F --> G[Next model request]
Loading

Reviews (1) · Last reviewed commit: "fix(agent): stop replaying reasoning_con..." · Reviewed by Greptile

@graphify-labs graphify-labs Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Graphify reviewed this change.

No issues found by static checks — no coupling regressions or blocking issues in the code graph. That is not the same as safe to merge: see what was not checked.

Not checked: tests were not run; no formal proof of the changed code.


Graphify review — findings

Strips assistant reasoning parts from the model messages replayed on each step of a multi-step tool loop, for providers that go through the @ai-sdk/openai-compatible serializer. That serializer turns reasoning into a top-level reasoning_content request field, which strict gateways such as Mistral reject with 400/422. The new stripReasoningFromModelMessages removes reasoning parts and drops assistant turns that would be left empty, while keeping text and tool calls. Reasoning stays in the stored, client-displayed history. A new serializesReasoningContent check in the provider factory picks the affected providers (OpenAI-compatible, LM Studio, Ollama, Moonshot, Qwen Code, GitHub Copilot), and prepareStep in the agent applies the strip only for those. Native Anthropic, OpenAI, Google, Azure, Bedrock and OpenRouter are left alone, since they replay signed or encrypted reasoning that must stay intact.

No blocking issues surfaced. 2 lower-confidence candidates did not survive cross-model review.

Analysis details — impact, health, verification

Impact & health

Graphify review

Impact — 81 functions depend on the 58 functions this change touches.

Health — grade A; 4 existing hotspot(s) in the area this change touches (pre-existing, not introduced here):

  • createChatRoutes() — 6 callers, 14 callees (high)
  • createApiRoutes() — 3 callers, 20 callees (high)
  • createHttpServer() — 2 callers, 6 callees (medium)
  • createLanguageModel() — 4 callers, 3 callees (medium)

Verification — 81 functions in the blast radius were not formally verified this run (proofs are advisory here).

Gate & verification

graphify gate

PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.

Advisory (not blocking):

  • verification_scope: 65 function(s) in the blast radius were not formally verified this run

… vs native providers

Add a two-step pipeline test that reproduces the reported second-call failure: step one returns reasoning plus a tool call, and step two must send the tool call without reasoning to an openai-compatible provider while a native provider keeps its reasoning for replay. The unit tests already cover stripReasoningFromModelMessages and serializesReasoningContent in isolation; this exercises them composed the way prepareStep applies them on every step.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Agent fails with HTTP 422 when replaying reasoning_content to Mistral

1 participant