#181

Anthropic adaptive thinking mode fails with `content[].thinking must be passed back to the API` after tool roundtrip

Openp091opened 8h ago

Describe the bug

When running Artex worker with Anthropic Claude adaptive thinking enabled + tool use (multi-turn agent loop), the second API call after returning tool_result gets a 400 invalid_request_error:

The content[].thinking in the thinking mode must be passed back to the API.

The worker marks intent as model_error / terminal state, exhausts retries and aborts the agent flow.

Full error log

(no text summary; terminal state model_error: the model or API call failed (network, auth, rate limiting, provider 5xx, etc.) …)

  • Terminal state: model_error - the model or API call failed (network, auth, rate limiting, provider 5xx, etc.) and retries were exhausted, so the intent is marked blocked — a transport-layer failure means this intent was barely explored; check its execution (get_worker_trace) before deciding whether to redispatch or switch tactics
  • Underlying error: anthropic: status 422: {"error":{"type":"invalid_request_error","message":"Failed to deserialize the JSON body into the target type: messages[1].content: missing field thinking at line 1 column 169788 (request_id: ec5fdb0e-31d1-416c-87de-a38430e7c9d2) (request id: 202611111432293001052488268d9d6wX0A3k9e)"},"type":"error"}
  • Executed: 1 model turns
  • Run duration: 28.2s
  • Tool calls: the run ended before any tool call was made

Activity

3
github-actions[bot]8h ago

Thanks for the detailed log. This looks like a bug in how Anthropic thinking blocks are round-tripped, but the report is missing what we need to reproduce it:

  1. Exact Artex version/commit and build (Docker image tag or git rev-parse HEAD).
  2. Go version and the Anthropic model ID + thinking budget/adaptive settings from your config.
  3. Minimal reproduction steps: the prompt, tool definitions, and whether extended thinking is enabled per-request or via env/config.
  4. The actual request body sent on the failing second call (redact secrets) — specifically whether thinking/redacted_thinking blocks from assistant turns are preserved in messages[] alongside tool_result, per Anthropic's requirement that thinking blocks be echoed back unmodified.

Also, the report is internally inconsistent: the summary says the failure occurs after a tool roundtrip, but the log states "the run ended before any tool call was made," and it cites both a 400 and a 422 (missing field thinking). Clarifying which is accurate would help. Meanwhile, check agent/provider.go and agent/chat.go for where assistant content blocks are serialized into history.


🤖 automated reply from the repo bot. A human maintainer reads everything. Ping me in any comment with @artex-bot.

BaaRai7h ago

Thanks for the report. We traced it, and the bug is in Norma, the LLM client library ARTEX uses. It is not in ARTEX itself.

Diagnosis

When thinking display is omitted (the default for adaptive thinking on recent Claude models), the Messages API returns thinking blocks with an empty thinking string and a signature:

{"type": "thinking", "thinking": "", "signature": "..."}

Norma's Anthropic client parsed this block correctly. When it sent the block back on the next request, though, it dropped the empty thinking field because the field was serialized with omitempty. The replayed block was therefore:

{"type": "thinking", "signature": "..."}

The backend rejects that block with messages[1].content: missing field 'thinking'. This explains why the failure always hits messages[1] (the first assistant turn) on the second model call. It happens right after the first turn's tool_use, before any tool result comes back, which is why the log says "the run ended before any tool call was made".

There was a second gap in the same code: redacted_thinking blocks were ignored when the response was parsed, so they were never sent back either.

A fix is up for review in Norma: https://github.com/BaaRai/Norma/pull/4. With it, thinking blocks are always replayed with their thinking field, and redacted_thinking is parsed and replayed verbatim, in both the streaming and non-streaming paths.

A separate, smaller gap remains: ARTEX's context compression view does not yet keep redacted_thinking blocks, so a compressed view can still drop them. We are tracking that as its own follow-up.

Two things would help us confirm this is your exact case

  1. The raw body of the first assistant response, i.e. the JSON returned by the first /v1/messages call, before the failing second call. Please redact anything sensitive, but keep the content[] array as it is, including any thinking / redacted_thinking blocks and their fields (you can shorten the signature/data values). We want to see whether thinking arrives empty, missing, or non-empty.

  2. What sits between ARTEX and Anthropic. Are you calling api.anthropic.com directly, or going through a gateway/proxy (one-api / new-api style relay, LiteLLM, OpenRouter, a cloud provider endpoint, a corporate proxy...)? Please include its name and version if you can. The 422 Failed to deserialize the JSON body into the target type and the second (request id: ...) suffix do not look like Anthropic's own error format, which returns 400 invalid_request_error. They point to an intermediate service with strict schema validation. The 400 in the title (content[].thinking ... must be passed back) also reads like a gateway or another backend's wording. Knowing the gateway tells us whether it also rewrites or strips thinking blocks on its own.

p0916h ago

Hi,BaaRai I haven’t yet figured out how to capture the raw 200 response body of the first /v1/messages call. I will try to get this payload later and send it over once I have it.

For the second question: ARTEX is not calling api.anthropic.com directly. It goes through a third-party site running the latest version of newapi as the relay gateway.