Files
sure/app/models/provider/anthropic/chat_parser.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

77 lines
1.9 KiB
Ruby

class Provider::Anthropic::ChatParser
Error = Class.new(StandardError)
def initialize(message)
@message = message
end
def parsed
ChatResponse.new(
id: response_id,
model: response_model,
messages: messages,
function_requests: function_requests
)
end
private
ChatResponse = Provider::LlmConcept::ChatResponse
ChatMessage = Provider::LlmConcept::ChatMessage
ChatFunctionRequest = Provider::LlmConcept::ChatFunctionRequest
attr_reader :message
def response_id
message.id
end
def response_model
message.model.to_s
end
def messages
text_blocks = content_blocks.select { |block| block_type(block) == :text }
return [] if text_blocks.empty?
[
ChatMessage.new(
id: response_id,
output_text: text_blocks.map { |b| block_value(b, :text) }.compact.join("\n")
)
]
end
def function_requests
content_blocks
.select { |block| block_type(block) == :tool_use }
.map do |block|
input = block_value(block, :input)
ChatFunctionRequest.new(
id: block_value(block, :id),
call_id: block_value(block, :id),
function_name: block_value(block, :name),
# A tool_use block with no arguments streams in as an empty string.
# Normalize it to an empty JSON object so downstream JSON.parse succeeds.
function_args: input.is_a?(String) ? input.presence || "{}" : (input || {}).to_json
)
end
end
def content_blocks
Array(message.content)
end
def block_type(block)
raw = block.respond_to?(:type) ? block.type : block[:type] || block["type"]
raw.to_s.to_sym
end
def block_value(block, key)
if block.respond_to?(key)
block.public_send(key)
elsif block.is_a?(Hash)
block[key] || block[key.to_s]
end
end
end