mirror of
https://github.com/we-promise/sure.git
synced 2026-08-05 08:32:15 +00:00
* 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>
77 lines
1.9 KiB
Ruby
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
|