Skip to content

Commit 7a56e46

Browse files
mattwebbioclaude
andauthored
Fix Anthropic streaming and empty tool input errors (#299)
* Fix Anthropic streaming error for higher-level gem events The anthropic gem (1.12+) emits higher-level convenience events (TextEvent, ThinkingEvent, SignatureEvent, etc.) in addition to raw streaming events when using MessageStream. The :signature event was causing "Unexpected chunk type: signature" errors because it lacks a :snapshot method, causing it to fall through to the error handler. Added explicit handling for all higher-level convenience events: :text, :input_json, :citation, :thinking, :signature These are safely ignored since the actual content is already processed via the raw :content_block_delta events. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Fix empty tool input JSON parsing for parameterless tools For tools with no parameters, the streaming API may send empty partial_json, resulting in an empty string input that would fail JSON.parse with a syntax error. Changes: - Use json_buf.present? instead of just json_buf to skip empty strings - For string inputs, check .present? before parsing and default to {} Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent ee41d3a commit 7a56e46

File tree

3 files changed

+107
-4
lines changed

3 files changed

+107
-4
lines changed

lib/active_agent/providers/anthropic_provider.rb

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -189,9 +189,14 @@ def process_stream_chunk(api_response_chunk)
189189
when :ping
190190
# No-Op Keep Awake
191191
when :overloaded_error
192-
# TODO: https://docs.claude.com/en/docs/build-with-claude/streaming#error-events
192+
# TODO: https://docs.claude.com/en/docs/build-with-claude/streaming#error-events
193+
194+
# Higher-level convenience events from anthropic gem's MessageStream
195+
when :text, :input_json, :citation, :thinking, :signature
196+
# No-Op; Already handled via :content_block_delta
197+
193198
else
194-
# No-Op: Looks like internal tracking from gem wrapper
199+
# No-Op; Internal tracking from gem wrapper
195200
return if api_response_chunk.respond_to?(:snapshot)
196201
raise "Unexpected chunk type: #{api_response_chunk.type}"
197202
end
@@ -296,11 +301,16 @@ def process_prompt_finished_extract_messages(api_response)
296301
def process_prompt_finished_extract_function_calls
297302
message_stack.pluck(:content).flatten.select { _1 in { type: "tool_use" } }.map do |api_function_call|
298303
json_buf = api_function_call.delete(:json_buf)
299-
api_function_call[:input] = JSON.parse(json_buf, symbolize_names: true) if json_buf
304+
api_function_call[:input] = JSON.parse(json_buf, symbolize_names: true) if json_buf.present?
300305

301306
# Handle case where :input is still a JSON string (gem >= 1.14.0)
307+
# For tools with no parameters, input may be an empty string
302308
if api_function_call[:input].is_a?(String)
303-
api_function_call[:input] = JSON.parse(api_function_call[:input], symbolize_names: true)
309+
if api_function_call[:input].present?
310+
api_function_call[:input] = JSON.parse(api_function_call[:input], symbolize_names: true)
311+
else
312+
api_function_call[:input] = {}
313+
end
304314
end
305315

306316
api_function_call
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# frozen_string_literal: true
2+
3+
require "test_helper"
4+
require_relative "../../../lib/active_agent/providers/anthropic_provider"
5+
6+
module Providers
7+
module Anthropic
8+
class EmptyToolInputTest < ActiveSupport::TestCase
9+
setup do
10+
@provider = ActiveAgent::Providers::AnthropicProvider.new(
11+
service: "Anthropic",
12+
model: "claude-sonnet-4-5",
13+
messages: [ { role: "user", content: "Hello" } ],
14+
stream_broadcaster: ->(message, delta, event_type) { }
15+
)
16+
end
17+
18+
test "handles empty string input for tools with no parameters" do
19+
@provider.send(:message_stack).push({
20+
role: "assistant",
21+
content: [
22+
{ type: "tool_use", id: "tool_123", name: "no_param_tool", input: "" }
23+
]
24+
})
25+
26+
result = @provider.send(:process_prompt_finished_extract_function_calls)
27+
28+
assert_equal 1, result.size
29+
assert_equal({}, result.first[:input])
30+
end
31+
32+
test "handles empty json_buf gracefully" do
33+
@provider.send(:message_stack).push({
34+
role: "assistant",
35+
content: [
36+
{ type: "tool_use", id: "tool_123", name: "no_param_tool", json_buf: "", input: nil }
37+
]
38+
})
39+
40+
result = @provider.send(:process_prompt_finished_extract_function_calls)
41+
42+
assert_equal 1, result.size
43+
assert_nil result.first[:input]
44+
end
45+
end
46+
end
47+
end
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# frozen_string_literal: true
2+
3+
require "test_helper"
4+
require_relative "../../../lib/active_agent/providers/anthropic_provider"
5+
6+
module Providers
7+
module Anthropic
8+
class StreamingEventsTest < ActiveSupport::TestCase
9+
setup do
10+
@provider = ActiveAgent::Providers::AnthropicProvider.new(
11+
service: "Anthropic",
12+
model: "claude-sonnet-4-5",
13+
messages: [ { role: "user", content: "Hello" } ],
14+
stream_broadcaster: ->(message, delta, event_type) { }
15+
)
16+
17+
@provider.send(:message_stack).push({
18+
role: "assistant",
19+
content: [ { type: "text", text: "" } ]
20+
})
21+
end
22+
23+
MockEvent = Struct.new(:type, keyword_init: true) do
24+
def [](key)
25+
send(key) if respond_to?(key)
26+
end
27+
end
28+
29+
test "handles :signature event without raising" do
30+
event = MockEvent.new(type: :signature)
31+
32+
assert_nothing_raised do
33+
@provider.send(:process_stream_chunk, event)
34+
end
35+
end
36+
37+
test "handles :thinking event without raising" do
38+
event = MockEvent.new(type: :thinking)
39+
40+
assert_nothing_raised do
41+
@provider.send(:process_stream_chunk, event)
42+
end
43+
end
44+
end
45+
end
46+
end

0 commit comments

Comments
 (0)