Skip to content

fix(anthropic): preserve tool_use when ToolCallPart.Input is empty or malformed#219

Merged
andreynering merged 1 commit intocharmbracelet:mainfrom
ljuti:fix/anthropic-empty-tool-input
Apr 29, 2026
Merged

fix(anthropic): preserve tool_use when ToolCallPart.Input is empty or malformed#219
andreynering merged 1 commit intocharmbracelet:mainfrom
ljuti:fix/anthropic-empty-tool-input

Conversation

@ljuti
Copy link
Copy Markdown
Contributor

@ljuti ljuti commented Apr 29, 2026

Summary

When ToolCallPart.Input is empty (""), null, or otherwise malformed JSON, toPrompt previously dropped the corresponding tool_use block silently. The agent loop has already executed the tool by that point and queued a ToolResultPart for the next user message, so the result survives without its parent. Anthropic's API enforces that every tool_result block have a matching tool_use in the previous message and rejects the next request with HTTP 400:

unexpected `messages.N.content.0: tool_use_id` found in `tool_result` blocks: <id>.
Each `tool_result` block must have a corresponding `tool_use` block in the previous message.

Reproduced via DeepSeek's anthropic-compat endpoint (https://api.deepseek.com/anthropic/v1/messages), where the model emits parameterless tool_use blocks with empty input. The bug is provider-agnostic — any anthropic-compat endpoint that emits empty/malformed input triggers it.

Fix

Two new helpers (decodeToolCallInputMap, decodeToolCallInputAny) handle the empty/malformed cases:

  • Empty/whitespace input → empty map (or nil for ServerToolUseBlockParam.Input any). No warning — empty input is a valid round-trip.
  • Non-empty but malformed input → empty map + a CallWarning naming the tool call ID and the parse error. The block is still emitted so tool_use ↔ tool_result pairing is preserved.

Even malformed input is better surfaced as {} plus a warning than as a silently dropped block, because a dropped block guarantees a 400 on the next round-trip.

Tests

  • The previous test should drop assistant messages with invalid tool input pinned the drop as intended. Renamed and rewritten as should preserve tool_use block when input JSON is malformed.
  • New test should preserve tool_use block when input is empty.

Test plan

  • go test ./providers/anthropic/... passes
  • go vet ./providers/anthropic/... passes
  • gofumpt -l clean on modified files
  • Manually verify against DeepSeek's anthropic-compat endpoint with a parameterless tool call

… malformed

json.Unmarshal("", &m) errors and previously caused the tool_use to be
silently dropped in toPrompt, leaving the matching tool_result orphaned
in the next user message. The Anthropic API then rejects the request
with "tool_result must have a corresponding tool_use in the previous
message". Default empty/whitespace input to {} (and nil for the
server_tool_use branch); on malformed-but-non-empty input, emit {} +
a CallWarning rather than dropping the block.

Hit in the wild via DeepSeek's anthropic-compat endpoint, which emits
empty input for parameterless tool calls.
Copy link
Copy Markdown
Member

@andreynering andreynering left a comment

Choose a reason for hiding this comment

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

Looks good, thank you!

@andreynering andreynering merged commit d73b308 into charmbracelet:main Apr 29, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants