Skip to content

anthropic: intermittent 400 "tool_use.input: Input should be an object" — tool input sent as raw string right after tool result #53865

Closed as not planned

Description

@huangyoudashi

Description

Intermittently a request fails with Anthropic 400 invalid_request_error: messages.N.content.M.tool_use.input: Input should be an object. Happened twice in one long tool-heavy session (04:44:49Z and 04:45:13Z; request_ids req_011CfpAGFPT8Vh6jWAmPTY15 / req_011CfpAJ1EiACT7zjg5zAtsM). Each failed attempt is persisted as an empty assistant message with error.type="provider.invalid-request"; the next retry a few seconds later succeeds unchanged (self-heals).

Evidence points to a race: the follow-up request is fired right after a tool result is ready but before the previous assistant message is finalized.

  • Capturing the outgoing request body shows exactly one tool_use per failing request whose input is a raw JSON string instead of an object — always the freshest tool call (first a write, then a shell). All other tool_use blocks in the same request are proper objects.
  • All tool inputs in the persisted session store are objects, so history is clean; the corruption exists only in the outgoing payload.
  • The assistant rows in question were finalized (time_updated) a few seconds after the failing request was sent (created 04:44:45Z / 04:45:10Z, failed request 04:44:49Z / 04:45:13Z, finalized 04:45:00Z / 04:45:16Z). The tool_result was already present in the request.
  • The bad index moved from messages.58 to messages.69, tracking history growth — so it is the newest tool call, not a fixed poisoned message.

The malformed input was already present in the request body as sent by opencode (captured at my self-hosted relay's inbound log; the relay is pure pass-through for this path).

Plugins

opencode.browser, openchamber-agent-tool

OpenCode version

2.0.22

Steps to reproduce

  1. Long session with many tool calls, Anthropic provider.
  2. A step ends with a tool call (observed with write and shell).
  3. The next request is dispatched immediately after the tool result is ready.
  4. Occasionally the request goes out with the previous tool_use.input still as a raw JSON string -> API 400. Not deterministic (timing race); observed 2x within one minute in one session.

Possibly related (same family, different layer/symptom): #36766, #36421, #47902.

Screenshot and/or share link

N/A — exact error text and request_ids included in the description above.

Operating System

Windows 11 (10.0.26200)

Terminal

OpenChamber desktop wrapper (no TTY env vars)

Activity

  1. opencode-agent commented on Oct 8, 2026

    @opencode-agent
    Contributor

    Thanks for the detailed report and the request IDs. I tried to reproduce this on 2.0.22 and couldn't.

    I pointed the built-in Anthropic provider at a local mock of the Anthropic Messages streaming API. The mock logs every request body and rejects any tool_use.input that isn't an object, with the same 400 you saw. Then I ran a session with over 60 back-to-back steps alternating write and shell tool calls, each request sent right after the previous tool result. It covered tool input streamed in small pieces, input sent all at once, and two tool calls in a single step. Every outgoing tool_use.input was an object.

    From the code, by the time a tool has a tool_result, its saved input is always an object (a non-object would be saved wrapped as { "value": ... }). That matches what you found in your session store. So a request with a tool_result but a raw-string input doesn't look like something opencode builds by itself. One thing that can change the outgoing messages is plugins: they can rewrite the message list just before the request is sent.

    These would help narrow it down:

    • Does it still happen with opencode.browser and openchamber-agent-tool disabled? In particular, does either plugin register a session context hook?
    • Does it happen when connecting straight to Anthropic, without the relay, or with plain opencode instead of the OpenChamber wrapper?
    • If you can capture it again, please share the full failing request (redacted), not just the bad block. Server logs from around that time at OPENCODE_LOG_LEVEL=DEBUG would also help.

    Possibly related: #51452 (also a string sent where an object is expected, though on the Mistral provider when a pending step is replayed after a service restart).

  2. opencode-agent commented on Oct 8, 2026

    @opencode-agent
    Contributor

    Could you retest on 2.0.25? It includes #53685. In 2.0.22, a tool call whose arguments never finished could stay in history with its raw argument text, and the next request would send that text as a string tool_use.input. That gives exactly this 400. In 2.0.25, those calls are closed out as failed, with input: {} and an error tool result.

    If it still happens on 2.0.25, please share:

    • the full failing request body (redacted), especially whether the bad input is empty or partial JSON, and whether there's a tool_result with the same id
    • whether more than one OpenCode server or client was attached to that session at the time
  3. opencode-agent commented on Oct 11, 2026

    @opencode-agent
    Contributor

    I'm closing this for now because there hasn't been a reply to the questions above. I couldn't reproduce the 400 tool_use.input: Input should be an object on 2.0.22. Across 60+ consecutive write/shell steps against an Anthropic mock, every tool_use.input went out as an object. Without more details, I can't tell whether the string input comes from opencode, a plugin or the relay.

    If it happens again, please comment here (or open a new issue) with:

    • whether it still happens with opencode.browser and openchamber-agent-tool disabled
    • whether it happens when connecting straight to Anthropic (no relay), or with plain opencode instead of the OpenChamber wrapper
    • the full failing request (redacted), and OPENCODE_LOG_LEVEL=DEBUG server logs from around that time

    Thanks again for the detailed report.

  4. TheBurgerShot commented on Oct 14, 2026

    @TheBurgerShot

    Retested as asked: still happens on 2.0.29.

    Summary

    Still happening on 2.0.29 (includes #53685): Anthropic 400 messages.N.content.1.tool_use.input: Input should be an object. Unlike the original report it does not self-heal here: once a subagent session hits it, every following request in that session fails the same way, so the session is dead and has to be replaced. It hit three background subagent sessions within 15 minutes, including one created fresh after the others had failed.

    Environment

    • opencode version: server 2.0.29 (latest); opencode CLI on PATH reports 1.18.30 (Homebrew), the server is the 2.0.29 build
    • OS: Darwin 25.5.0 arm64 (macOS)
    • Terminal: n/a (sessions driven from the OpenChamber web UI)
    • Shell: /bin/zsh
    • Install/channel: latest
    • Active plugins: opencode.browser, openchamber-agent-tool, two local plugins (employer-layer, session-keepalive, session-title.ts), @ex-machina/[email protected]
    • Model: anthropic/claude-opus-5-5 via the anthropic-auth plugin

    Reproduction

    Not minimal yet. Observed pattern:

    1. A parent session spawns several background subagents (general) that run long shell commands (loops with sleep 60, timeouts up to 50 min) while other subagents run in parallel.
    2. The opencode server restarts once (2026-10-14 ~11:47Z).
    3. Within minutes, subagent requests fail with the 400 above. Resuming the same session (new prompt) fails again immediately with the same error (index moves with history).
    4. A brand-new subagent session started after the restart failed the same way after ~20 messages.

    Expected Behavior

    Tool inputs are always sent as objects; a single malformed tool call does not poison the session.

    Actual Behavior

    Log (~/.local/share/opencode/log/opencode.log), level=ERROR Failed to drain Session:

    11:54:22Z  messages.94.content.1.tool_use.input: Input should be an object   (session A)
    11:57:19Z  messages.94 …                                                      (session A, after resume)
    11:58:29Z / 12:00:06Z  messages.152 / messages.116 …                          (session B, after resume)
    12:05:21Z / 12:05:55Z  messages.42 …                                          (session C, created 11:55Z, fresh)
    12:08:37Z  …
    

    The last persisted part in session C is a shell tool call with "executed":false, "status":"running" and a proper object input (a 40× sleep 60 polling loop with timeout: 3000000). The persisted history contains no non-object input, so — as in the original report — the string seems to exist only in the outgoing payload. I did not capture the outgoing request body.

    Additional Context

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

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions