Files
sure/test/models/provider/anthropic/chat_parser_test.rb
kai392 6e2cbb727a fix(ai): prevent Anthropic chat crash on no-argument tool calls (#2755)
* fix: prevent Anthropic chat crash on no-argument tool calls

When the model streams a tool_use block with no arguments (e.g.
get_categories), the accumulated input arrives as an empty string.
The Anthropic chat parser passed that empty string straight through as
function_args, and Assistant::FunctionToolCaller then called
JSON.parse("") — which raises "unexpected end of input", surfaces as a
Provider::Anthropic::Error, and kills the whole assistant turn.

Fix at the source by normalizing empty/nil tool input to an empty JSON
object in the parser, plus a defensive guard in FunctionToolCaller so
any provider that emits blank arguments cannot crash a turn.

Fixes #2722

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test: assert nil-args result as Hash to match jsonb function_result

Addresses Codex P1 / CodeRabbit review: EchoFunction returns the parsed
params Hash and ToolCall::Function stores it in a jsonb column, so the
nil-arguments test must assert the Hash directly instead of JSON.parse.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-26 04:47:15 +02:00

117 lines
3.4 KiB
Ruby

require "test_helper"
require "ostruct"
class Provider::Anthropic::ChatParserTest < ActiveSupport::TestCase
test "parses text-only message into ChatResponse with single output_text" do
raw = build_message(
id: "msg_1",
model: "claude-sonnet-4-6",
content: [
OpenStruct.new(type: :text, text: "Hello"),
OpenStruct.new(type: :text, text: "world")
]
)
parsed = Provider::Anthropic::ChatParser.new(raw).parsed
assert_equal "msg_1", parsed.id
assert_equal "claude-sonnet-4-6", parsed.model
assert_equal 1, parsed.messages.size
assert_equal "Hello\nworld", parsed.messages.first.output_text
assert_empty parsed.function_requests
end
test "parses tool_use blocks into ChatFunctionRequest" do
raw = build_message(
id: "msg_2",
model: "claude-sonnet-4-6",
content: [
OpenStruct.new(
type: :tool_use,
id: "toolu_abc",
name: "get_transactions",
input: { "page" => 1, "order" => "asc" }
)
]
)
parsed = Provider::Anthropic::ChatParser.new(raw).parsed
assert_empty parsed.messages
assert_equal 1, parsed.function_requests.size
req = parsed.function_requests.first
assert_equal "toolu_abc", req.id
assert_equal "toolu_abc", req.call_id
assert_equal "get_transactions", req.function_name
assert_equal({ "page" => 1, "order" => "asc" }.to_json, req.function_args)
end
test "parses mixed content blocks" do
raw = build_message(
id: "msg_3",
model: "claude-sonnet-4-6",
content: [
OpenStruct.new(type: :text, text: "Looking up your transactions..."),
OpenStruct.new(type: :tool_use, id: "toolu_42", name: "get_transactions", input: {})
]
)
parsed = Provider::Anthropic::ChatParser.new(raw).parsed
assert_equal 1, parsed.messages.size
assert_equal "Looking up your transactions...", parsed.messages.first.output_text
assert_equal 1, parsed.function_requests.size
assert_equal "toolu_42", parsed.function_requests.first.call_id
end
test "normalizes empty-string tool input to an empty JSON object" do
raw = build_message(
id: "msg_5",
model: "claude-sonnet-4-6",
content: [
OpenStruct.new(type: :tool_use, id: "toolu_noargs", name: "get_categories", input: "")
]
)
parsed = Provider::Anthropic::ChatParser.new(raw).parsed
req = parsed.function_requests.first
assert_equal "{}", req.function_args
# Must survive the downstream JSON.parse that fulfills the tool call.
assert_equal({}, JSON.parse(req.function_args))
end
test "normalizes nil tool input to an empty JSON object" do
raw = build_message(
id: "msg_6",
model: "claude-sonnet-4-6",
content: [
OpenStruct.new(type: :tool_use, id: "toolu_nil", name: "get_categories", input: nil)
]
)
parsed = Provider::Anthropic::ChatParser.new(raw).parsed
assert_equal "{}", parsed.function_requests.first.function_args
end
test "accepts hash-shaped content blocks" do
raw = OpenStruct.new(
id: "msg_4",
model: "claude-sonnet-4-6",
content: [
{ type: :text, text: "from hash" }
]
)
parsed = Provider::Anthropic::ChatParser.new(raw).parsed
assert_equal "from hash", parsed.messages.first.output_text
end
private
def build_message(id:, model:, content:)
OpenStruct.new(id: id, model: model, content: content)
end
end