Found while adding SSE tool-call coverage in #166, and pinned there as a test rather than changed — closing it is a behavioural decision, not a test fix.
The divergence
Two providers disagree about what to do when a tool call's arguments cannot be parsed.
anthropic.rs fails the turn, and states why at the sweep:
a tool handed {"__partial_json": ...} runs on its defaults instead of what the model asked for
So the turn returns StopReason::Error, the agent loop returns before extracting tool calls, and the tool never runs.
openai_compat.rs falls back to an empty object:
let args = serde_json::from_str(&buf.arguments).unwrap_or_else(|e| {
if !buf.arguments.is_empty() {
warn!(tool = %buf.name, len = buf.arguments.len(),
"tool-call arguments failed to parse ({e}); using empty object");
}
serde_json::Value::Object(Default::default())
});
The call proceeds with {} — which is exactly the hazard Anthropic's path exists to prevent, and it is the default on 15+ providers: OpenAI, Groq, Together, DeepSeek, Fireworks, Mistral, xAI.
Pinned by truncated_tool_arguments_fall_back_to_empty_unlike_anthropic in tests/openai_compat_stream_test.rs.
Why it matters
A truncated stream is not exotic — a dropped connection mid-arguments is the ordinary case. The failure is silent apart from a warn!, and a library cannot assume its host installed a subscriber.
Concretely: delete_files({"paths": ["/tmp/scratch"]}) truncated after {"paths": becomes delete_files({}). What that does depends entirely on the tool's own defaults, and the model's actual intent is gone. The agent then sees a plausible-looking result and continues.
The empty case is not affected and should stay as-is: a zero-argument tool legitimately streams "", and {} is the right answer there. Both are now pinned, so any fix must keep them distinct — which is the same empty-vs-malformed distinction that shipped the Anthropic bug.
Options
- Match Anthropic — fail the turn on a non-empty, unparseable buffer. Consistent, and the loop already handles
StopReason::Error. Behavioural change for 15+ providers.
- Surface it as an error tool result — let the model see "your arguments did not parse" and retry. Fits the crate's "tools return stderr so the LLM can self-correct" convention, and is less abrupt than failing the turn.
- Leave it, document it. Defensible if the truncation rate is genuinely negligible, but nothing measures that today.
Option 2 looks best on the crate's own conventions, but it is a real design call rather than a bug fix.
Acceptance
Found while adding SSE tool-call coverage in #166, and pinned there as a test rather than changed — closing it is a behavioural decision, not a test fix.
The divergence
Two providers disagree about what to do when a tool call's arguments cannot be parsed.
anthropic.rsfails the turn, and states why at the sweep:So the turn returns
StopReason::Error, the agent loop returns before extracting tool calls, and the tool never runs.openai_compat.rsfalls back to an empty object:The call proceeds with
{}— which is exactly the hazard Anthropic's path exists to prevent, and it is the default on 15+ providers: OpenAI, Groq, Together, DeepSeek, Fireworks, Mistral, xAI.Pinned by
truncated_tool_arguments_fall_back_to_empty_unlike_anthropicintests/openai_compat_stream_test.rs.Why it matters
A truncated stream is not exotic — a dropped connection mid-
argumentsis the ordinary case. The failure is silent apart from awarn!, and a library cannot assume its host installed a subscriber.Concretely:
delete_files({"paths": ["/tmp/scratch"]})truncated after{"paths":becomesdelete_files({}). What that does depends entirely on the tool's own defaults, and the model's actual intent is gone. The agent then sees a plausible-looking result and continues.The empty case is not affected and should stay as-is: a zero-argument tool legitimately streams
"", and{}is the right answer there. Both are now pinned, so any fix must keep them distinct — which is the same empty-vs-malformed distinction that shipped the Anthropic bug.Options
StopReason::Error. Behavioural change for 15+ providers.Option 2 looks best on the crate's own conventions, but it is a real design call rather than a bug fix.
Acceptance
truncated_tool_arguments_fall_back_to_empty_unlike_anthropicis updated rather than deleted, so the change is visible in the diff.google.rs,bedrock.rs,openai_responses.rsandazure_openai.rshave a third behaviour — only Anthropic and openai_compat were examined.