anthropic 1.13.0 → 1.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +19 -0
  3. data/README.md +31 -1
  4. data/lib/anthropic/helpers/input_schema/base_model.rb +6 -3
  5. data/lib/anthropic/helpers/input_schema/json_schema_converter.rb +9 -3
  6. data/lib/anthropic/helpers/input_schema/supported_schemas.rb +106 -0
  7. data/lib/anthropic/helpers/input_schema/union_of.rb +3 -1
  8. data/lib/anthropic/helpers/messages.rb +107 -0
  9. data/lib/anthropic/helpers/streaming/message_stream.rb +54 -43
  10. data/lib/anthropic/helpers/tools/base_tool.rb +82 -0
  11. data/lib/anthropic/helpers/tools/runner.rb +156 -0
  12. data/lib/anthropic/helpers/tools.rb +5 -0
  13. data/lib/anthropic/internal/transport/base_client.rb +7 -1
  14. data/lib/anthropic/internal/transport/pooled_net_requester.rb +6 -2
  15. data/lib/anthropic/models/beta/beta_tool_use_block.rb +14 -0
  16. data/lib/anthropic/models/tool_use_block.rb +6 -6
  17. data/lib/anthropic/resources/beta/messages.rb +23 -5
  18. data/lib/anthropic/resources/messages.rb +7 -81
  19. data/lib/anthropic/version.rb +1 -1
  20. data/lib/anthropic.rb +15 -10
  21. data/manifest.yaml +1 -0
  22. data/rbi/anthropic/helpers/input_schema/base_model.rbi +7 -2
  23. data/rbi/anthropic/helpers/tools/base_tool.rbi +51 -0
  24. data/rbi/anthropic/helpers/tools/runner.rbi +40 -0
  25. data/rbi/anthropic/helpers/tools.rbi +5 -0
  26. data/rbi/anthropic/internal/transport/base_client.rbi +5 -0
  27. data/rbi/anthropic/internal/transport/pooled_net_requester.rbi +6 -2
  28. data/rbi/anthropic/internal/type/base_model.rbi +8 -4
  29. data/rbi/anthropic/models/tool_use_block.rbi +3 -0
  30. data/rbi/anthropic/resources/beta/messages.rbi +296 -0
  31. data/sig/anthropic/internal/transport/base_client.rbs +2 -0
  32. data/sig/anthropic/internal/transport/pooled_net_requester.rbs +4 -1
  33. metadata +11 -4
  34. data/lib/anthropic/helpers/input_schema/property_mapping.rb +0 -47
  35. /data/rbi/anthropic/helpers/{structured_output.rbi → input_schema.rbi} +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 402cad94c15427670184c0922d15aa743631c9cf0178a2bd7d1a54fd515e138a
4
- data.tar.gz: 89e7ff9f5a7072a3380a749286be91d0da71eb0067310ab558086496cd9264f3
3
+ metadata.gz: bd00e27834064b69a4c340776b3a6dcc50be9cde619c86fca93663c421cb7dff
4
+ data.tar.gz: 8a07090aaa4fc943673bba3a67411ef3d2dede001e8a597487c6be61b17a6d36
5
5
  SHA512:
6
- metadata.gz: 9439dd79ab1f200590ea63e60e27f444a51566fb75988b8b6a32cde0d20d404dd7e85b5bd77d94c21ec0ff77684df53f59d610063bb834e222ec1d267490d0f1
7
- data.tar.gz: 998aca515a29223d30c4bd7f3c240e99c476a354eb40a3b2131feb62404893e3428ecf47d72bb188ff847af5f56125b87ec27bce90b03f69fb2cc8dc5d6d0ac5
6
+ metadata.gz: f50ad464f5deeaf0be1635ca3fe270b82efd88332c145472ca653048b3e970925b531f9e652401d8f5cfe6e055083e540aadd2f3f6ae18b743acb2e58c0a513c
7
+ data.tar.gz: 0706ceb70895b9cfedf683a8edbc183a73b45234c62531c15a281860891de1ed2160245f104a3f4f752eb7d82235de0d6ade340bdf66e1a5de99801cf755a951
data/CHANGELOG.md CHANGED
@@ -1,5 +1,24 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.14.0 (2025-11-07)
4
+
5
+ Full Changelog: [v1.13.0...v1.14.0](https://github.com/anthropics/anthropic-sdk-ruby/compare/v1.13.0...v1.14.0)
6
+
7
+ ### Features
8
+
9
+ * run-tools implementation ([#714](https://github.com/anthropics/anthropic-sdk-ruby/issues/714)) ([5cf7298](https://github.com/anthropics/anthropic-sdk-ruby/commit/5cf72982d25f97052cb87270293434e16a330818))
10
+
11
+
12
+ ### Bug Fixes
13
+
14
+ * better thread safety via early initializing SSL store during HTTP client creation ([3a9531c](https://github.com/anthropics/anthropic-sdk-ruby/commit/3a9531c5b46da4d77647b14a3e8b20e3713c6da3))
15
+
16
+
17
+ ### Chores
18
+
19
+ * **client:** send user-agent header ([57b22be](https://github.com/anthropics/anthropic-sdk-ruby/commit/57b22bebbc1b5c1bf676103c997e0aabdc065838))
20
+ * **internal:** codegen related update ([15188e3](https://github.com/anthropics/anthropic-sdk-ruby/commit/15188e3963c05e4594d449a0ef871f9ae6b41ed3))
21
+
3
22
  ## 1.13.0 (2025-10-29)
4
23
 
5
24
  Full Changelog: [v1.12.0...v1.13.0](https://github.com/anthropics/anthropic-sdk-ruby/compare/v1.12.0...v1.13.0)
data/README.md CHANGED
@@ -15,7 +15,7 @@ To use this gem, install via Bundler by adding the following to your application
15
15
  <!-- x-release-please-start-version -->
16
16
 
17
17
  ```ruby
18
- gem "anthropic", "~> 1.13.0"
18
+ gem "anthropic", "~> 1.14.0"
19
19
  ```
20
20
 
21
21
  <!-- x-release-please-end -->
@@ -79,6 +79,36 @@ end
79
79
 
80
80
  Streaming with `anthropic.messages.stream(...)` exposes [various helpers for your convenience](helpers.md) including accumulation & SDK-specific events.
81
81
 
82
+ ### Input Schema & Tool Calling
83
+
84
+ We have helper mechanisms to define structured data classes for tools and let Claude automatically execute them.
85
+
86
+ Please refer to [helpers.md](helpers.md) for more detailed usage information.
87
+
88
+ ```ruby
89
+ class CalculatorInput < Anthropic::BaseModel
90
+ required :lhs, Float
91
+ required :rhs, Float
92
+ required :operator, Anthropic::InputSchema::EnumOf[:+, :-, :*, :/]
93
+ end
94
+
95
+ class Calculator < Anthropic::BaseTool
96
+ input_schema CalculatorInput
97
+
98
+ def call(expr)
99
+ expr.lhs.public_send(expr.operator, expr.rhs)
100
+ end
101
+ end
102
+
103
+ # Automatically handles tool execution loop
104
+ client.beta.messages.tool_runner(
105
+ model: "claude-sonnet-4-5-20250929",
106
+ max_tokens: 1024,
107
+ messages: [{role: "user", content: "What's 15 * 7?"}],
108
+ tools: [Calculator.new]
109
+ ).each_message { puts _1.content }
110
+ ```
111
+
82
112
  ### Pagination
83
113
 
84
114
  List methods in the Anthropic API are paginated.
@@ -68,12 +68,15 @@ module Anthropic
68
68
  super(name_sym, type_info, spec)
69
69
  end
70
70
 
71
+ # @return [String]
71
72
  attr_reader :doc_string
72
73
 
74
+ # @api public
75
+ #
73
76
  # @param description [String]
74
- def doc(description)
75
- @doc_string = description
76
- end
77
+ def description(description) = (@doc_string = description)
78
+
79
+ alias_method :doc, :description
77
80
 
78
81
  private def process_field_args(args)
79
82
  # Only accept hash format for descriptions.
@@ -51,7 +51,7 @@ module Anthropic
51
51
  {anyOf: [schema, {type: null}]}
52
52
  in {anyOf: schemas}
53
53
  null = {type: null}
54
- schemas.any? { _1 == null || _1 == {type: ["null"]} } ? schema : {anyOf: [*schemas, null]}
54
+ schemas.any? { _1 == null || _1 == {type: [null]} } ? schema : {anyOf: [*schemas, null]}
55
55
  in {type: String => type}
56
56
  type == null ? schema : schema.update(type: [type, null])
57
57
  in {type: Array => types}
@@ -63,7 +63,12 @@ module Anthropic
63
63
  #
64
64
  # @param schema [Hash{Symbol=>Object}]
65
65
  def assoc_meta!(schema, meta:)
66
- xformed = meta.transform_keys(Anthropic::Helpers::InputSchema::PROPERTY_MAPPING)
66
+ xformed = meta.transform_keys(doc: :description) do
67
+ _1.to_s.gsub(/_\w/, &:upcase).tr("_", "").to_sym
68
+ end
69
+ if schema.key?(:$ref) && !xformed.empty?
70
+ schema.merge!(Anthropic::Helpers::InputSchema::JsonSchemaConverter::NO_REF => true)
71
+ end
67
72
  schema.merge!(xformed)
68
73
  end
69
74
 
@@ -138,7 +143,8 @@ module Anthropic
138
143
  end
139
144
 
140
145
  xformed = reused_defs.transform_keys { _1.delete_prefix("#/$defs/") }
141
- xformed.empty? ? schema : {"$defs": xformed}.update(schema)
146
+ unconformed = xformed.empty? ? schema : {"$defs": xformed}.update(schema)
147
+ unconformed.tap { Anthropic::Helpers::InputSchema::SupportedSchemas.transform_schema!(_1) }
142
148
  end
143
149
 
144
150
  # @api private
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Anthropic
4
+ module Helpers
5
+ module InputSchema
6
+ module SupportedSchemas
7
+ class << self
8
+ # @api private
9
+ #
10
+ # @param x [Object]
11
+ #
12
+ # @yieldparam [Object]
13
+ private def walk_once(x, &blk)
14
+ seen = Set.new
15
+ rec = ->(x) do
16
+ return unless seen.add?(x)
17
+
18
+ blk.call(x)
19
+ case x
20
+ in Hash
21
+ x.each_value(&rec)
22
+ in Array
23
+ x.each(&rec)
24
+ else
25
+ end
26
+ end
27
+
28
+ rec.call(x)
29
+ end
30
+
31
+ # @api private
32
+ #
33
+ # @param schema [Hash{Symbol=>Object}]
34
+ #
35
+ # @return [String]
36
+ private def describe!(schema, unsupported:)
37
+ [:$defs, :default].each { unsupported.delete(_1) }
38
+
39
+ doc = unsupported.delete(:description)
40
+ return if unsupported.empty?
41
+
42
+ addendum = unsupported.map { "#{_1}=#{_2.to_json}" }.join(",")
43
+ unsupported.each_key { schema.delete(_1) }
44
+
45
+ schema[:description] =
46
+ [
47
+ doc,
48
+ "Please also conform to these set of constraints: #{addendum}"
49
+ ].compact.join("\n")
50
+ end
51
+
52
+ # @api private
53
+ #
54
+ # @param schema [Hash{Symbol=>Object}]
55
+ #
56
+ # @return [Hash{Symbol=>Object}]
57
+ def transform_schema!(schema)
58
+ defs = schema[:$defs].to_h
59
+
60
+ # rubocop:disable Metrics/BlockLength
61
+ xform = ->(s) do
62
+ case s
63
+ in {type: "string" | ["string", "null"], format: "date-time" | "time" | "date" | "duration" | "email" | "hostname" | "uri" | "ipv4" | "ipv6" | "uuid"} | {type: "array", minItems: 0 | 1}
64
+ # these are the currently supported cases
65
+ next
66
+
67
+ in {oneOf: Array => schemas, **rest}
68
+ {anyOf: schemas, **rest}
69
+
70
+ in {allOf: Array => schemas}
71
+ derefed = schemas.lazy.grep(Hash).map do
72
+ ref = _1[:$ref].to_s.delete_prefix("#/$defs/")
73
+ defs.fetch(ref, _1)
74
+ end
75
+ merged = {}.merge!(*derefed)
76
+ if (doc = xform.call(merged))
77
+ merged.store(:$ref, true)
78
+
79
+ schemas.reject! do |s|
80
+ next unless s.is_a?(Hash)
81
+ s.delete(:description)
82
+ s.select! { merged.key?(_1) }
83
+ s.empty?
84
+ end
85
+ schemas << {description: doc}
86
+ end
87
+ in {type: "integer" | "number" | "string" | ["integer", "null"] | ["number", "null"] | ["string", "null"], **unsupported}
88
+ describe!(s, unsupported: unsupported.except(:enum))
89
+ in {type: "array", **unsupported}
90
+ describe!(s, unsupported: unsupported.except(:items))
91
+ in {type: "object", **unsupported}
92
+ unsupported.delete(:additionalProperties) if unsupported[:additionalProperties] == false
93
+ describe!(s, unsupported: unsupported.except(:required, :properties))
94
+ else
95
+ end
96
+ end
97
+ # rubocop:enable Metrics/BlockLength
98
+
99
+ walk_once(schema, &xform)
100
+ schema
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end
106
+ end
@@ -33,7 +33,9 @@ module Anthropic
33
33
  end
34
34
 
35
35
  schemas.each do |schema|
36
- mergeable_keys.each_key { mergeable_keys[_1] += 1 if schema.keys == _1 }
36
+ mergeable_keys.each_key do
37
+ mergeable_keys[_1] += 1 if schema.keys == _1 && schema[_1].is_a?(Array)
38
+ end
37
39
  end
38
40
  mergeable = mergeable_keys.any? { _1.last == schemas.length }
39
41
  mergeable ? Anthropic::Internal::Util.deep_merge(*schemas, concat: true) : {anyOf: schemas}
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Anthropic
4
+ module Helpers
5
+ module Messages
6
+ class << self
7
+ # @api private
8
+ #
9
+ # Extract tool models from the request and convert them to JSON Schema
10
+ # Returns a hash mapping tool name to Ruby model.
11
+ #
12
+ # @param data [Hash{Sybmol=>Object}]
13
+ #
14
+ # @param strict [Boolean, nil]
15
+ #
16
+ # @return [Hash{String=>Class}, Hash{String=>Class}]
17
+ def distill_input_schema_models!(data, strict:)
18
+ tools = {}
19
+ models = {}
20
+
21
+ case data
22
+ in {tools: Array => tool_array}
23
+ # rubocop:disable Metrics/BlockLength
24
+ mapped = tool_array.map do |tool|
25
+ case tool
26
+ # Direct tool class:
27
+ in Anthropic::Helpers::InputSchema::JsonSchemaConverter
28
+ classname = tool.is_a?(Anthropic::Helpers::Tools::BaseTool) ? tool.class.name : tool.name
29
+ name = classname
30
+ .split("::")
31
+ .last
32
+ .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
33
+ .gsub(/([a-z\d])([A-Z])/, '\1_\2')
34
+ .downcase
35
+ description =
36
+ case tool
37
+ in Anthropic::Helpers::Tools::BaseTool
38
+ tool.class.doc_string || name
39
+ in Class if tool <= Anthropic::Helpers::InputSchema::BaseModel
40
+ tool.doc_string || name
41
+ else
42
+ name
43
+ end
44
+
45
+ tools.store(name, tool)
46
+ input_schema = Anthropic::Helpers::InputSchema::JsonSchemaConverter.to_json_schema(tool)
47
+ {name:, description:, input_schema:}.tap { _1.update(strict:) if strict }
48
+ # Tool with explicit name/description and BaseModel as input_schema:
49
+ in {name: String => name,
50
+ input_schema: Anthropic::Helpers::InputSchema::JsonSchemaConverter => tool,
51
+ **rest}
52
+ tools.store(name, tool)
53
+ input_schema = Anthropic::Helpers::InputSchema::JsonSchemaConverter.to_json_schema(tool)
54
+ rest.merge(name:, input_schema:).tap { _1.update(strict:) if strict }
55
+ else
56
+ # Any other format (pass through unchanged)
57
+ # This includes raw JSON schemas and any other tool definitions.
58
+ tool
59
+ end
60
+ end
61
+ tool_array.replace(mapped)
62
+ else
63
+ end
64
+ # rubocop:enable Metrics/BlockLength
65
+
66
+ [tools, models]
67
+ end
68
+
69
+ # @api private
70
+ #
71
+ # @param raw [Hash{Sybmol=>Object}]
72
+ #
73
+ # @param tools [Hash{String=>Class}]
74
+ #
75
+ # @param models [Hash{String=>Class}]
76
+ #
77
+ # @return [Hash{Sybmol=>Object}]
78
+ def parse_input_schemas!(raw, tools:, models:)
79
+ raw[:content]&.each do |content|
80
+ case content
81
+ in {type: "tool_use", name:, input:} if (tool = tools[name])
82
+ begin
83
+ coerced = Anthropic::Internal::Type::Converter.coerce(tool, input)
84
+
85
+ content.store(:parsed, coerced)
86
+ rescue StandardError => e
87
+ content.store(:parsed, {error: e.message})
88
+ end
89
+ in {type: "text", text:} if (model = models.first&.last)
90
+ begin
91
+ json = JSON.parse(text, symbolize_names: true)
92
+ coerced = Anthropic::Internal::Type::Converter.coerce(model, json)
93
+
94
+ content.store(:parsed, coerced)
95
+ rescue StandardError => e
96
+ content.store(:parsed, {error: e.message})
97
+ end
98
+ else
99
+ end
100
+ end
101
+
102
+ raw
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end
@@ -60,10 +60,10 @@ module Anthropic
60
60
  #
61
61
  # Returns the complete accumulated Message object after stream completion.
62
62
  #
63
- # @return [Anthropic::Models::Message]
63
+ # @return [Anthropic::Models::Message, Anthropic::Models::Beta::BetaMessage]
64
64
  def accumulated_message
65
65
  until_done
66
- parse_tool_uses!(@accumated_message_snapshot) if @tool_models.any?
66
+ parse_tool_uses!(@accumated_message_snapshot)
67
67
  @accumated_message_snapshot
68
68
  end
69
69
 
@@ -76,18 +76,7 @@ module Anthropic
76
76
  # @return [String]
77
77
  def accumulated_text
78
78
  message = accumulated_message
79
- text_blocks = []
80
- message.content.each do |block|
81
- if block.type == :text
82
- text_blocks << block.text
83
- end
84
- end
85
-
86
- if text_blocks.empty?
87
- raise RuntimeError.new("Expected to have received at least 1 text block")
88
- end
89
-
90
- text_blocks.join
79
+ message.content.map { _1.type == :text ? _1.text : nil }.join
91
80
  end
92
81
 
93
82
  # @api private
@@ -99,7 +88,10 @@ module Anthropic
99
88
  #
100
89
  # @return [Anthropic::Models::Message] updated message snapshot with event applied
101
90
  private def accumulate_event(event:, current_snapshot:)
102
- unless event in Anthropic::Models::RawMessageStreamEvent
91
+ case event
92
+ in Anthropic::Models::RawMessageStreamEvent | Anthropic::Models::BetaRawMessageStreamEvent
93
+ nil
94
+ else
103
95
  message = "Expected event to be a variant of RawMessageStreamEvent, got #{event.class}"
104
96
  raise ArgumentError.new(message)
105
97
  end
@@ -112,36 +104,37 @@ module Anthropic
112
104
  end
113
105
 
114
106
  case event
115
- in Anthropic::Models::RawMessageStartEvent
116
- # Use the converter to create a new, isolated copy of the message object.
107
+ in Anthropic::Models::RawMessageStartEvent # Use the converter to create a new, isolated copy of the message object.
117
108
  # This ensures proper type validation and prevents shared object references
118
109
  # that could lead to unintended mutations during streaming accumulation.
119
110
  # Matches the Python SDK's approach of explicitly constructing Message objects.
120
111
  return Anthropic::Internal::Type::Converter.coerce(Anthropic::Models::Message, event.message)
121
- in Anthropic::Models::RawContentBlockStartEvent
112
+ in Anthropic::Models::BetaRawMessageStartEvent
113
+ return Anthropic::Internal::Type::Converter.coerce(Anthropic::Models::BetaMessage, event.message)
114
+ in Anthropic::Models::RawContentBlockStartEvent | Anthropic::Models::BetaRawContentBlockStartEvent
122
115
  current_snapshot.content = (current_snapshot.content || []) + [event.content_block]
123
- in Anthropic::Models::RawContentBlockDeltaEvent
116
+ in Anthropic::Models::RawContentBlockDeltaEvent | Anthropic::Models::BetaRawContentBlockDeltaEvent
124
117
  content = current_snapshot.content[event.index]
125
118
 
126
119
  case (delta = event.delta)
127
- in Anthropic::Models::TextDelta if content.type == :text
120
+ in Anthropic::Models::TextDelta | Anthropic::Models::BetaTextDelta if content.type == :text
128
121
  content.text += delta.text
129
- in Anthropic::Models::InputJSONDelta if content.type == :tool_use
130
- json_buf = content.json_buf.to_s
122
+ in Anthropic::Models::InputJSONDelta | Anthropic::Models::BetaInputJSONDelta if content.type == :tool_use
123
+ json_buf = content._json_buf.to_s
131
124
  json_buf += delta.partial_json
132
125
 
133
126
  content.input = json_buf
134
- content.json_buf = json_buf
135
- in Anthropic::Models::CitationsDelta if content.type == :text
127
+ content._json_buf = json_buf
128
+ in Anthropic::Models::CitationsDelta | Anthropic::Models::BetaCitationsDelta if content.type == :text
136
129
  content.citations ||= []
137
130
  content.citations << delta.citation
138
- in Anthropic::Models::ThinkingDelta if content.type == :thinking
131
+ in Anthropic::Models::ThinkingDelta | Anthropic::Models::BetaThinkingDelta if content.type == :thinking
139
132
  content.thinking += delta.thinking
140
- in Anthropic::Models::SignatureDelta if content.type == :thinking
133
+ in Anthropic::Models::SignatureDelta | Anthropic::Models::BetaSignatureDelta if content.type == :thinking
141
134
  content.signature = delta.signature
142
135
  else
143
136
  end
144
- in Anthropic::Models::RawMessageDeltaEvent
137
+ in Anthropic::Models::RawMessageDeltaEvent | Anthropic::Models::BetaRawMessageDeltaEvent
145
138
  current_snapshot.stop_reason = event.delta.stop_reason
146
139
  current_snapshot.stop_sequence = event.delta.stop_sequence
147
140
  current_snapshot.usage.output_tokens = event.usage.output_tokens
@@ -166,51 +159,51 @@ module Anthropic
166
159
  events_to_yield = []
167
160
 
168
161
  case event
169
- in Anthropic::Models::RawMessageStopEvent
162
+ in Anthropic::Models::RawMessageStopEvent | Anthropic::Models::BetaRawMessageStopEvent
170
163
  events_to_yield << MessageStopEvent.new(
171
164
  type: :message_stop,
172
165
  message: message_snapshot
173
166
  )
174
- in Anthropic::Models::RawContentBlockDeltaEvent
167
+ in Anthropic::Models::RawContentBlockDeltaEvent | Anthropic::Models::BetaRawContentBlockDeltaEvent
175
168
  events_to_yield << event
176
169
  content_block = message_snapshot.content[event.index]
177
170
 
178
171
  case (delta = event.delta)
179
- in Anthropic::Models::TextDelta if content_block.type == :text
172
+ in Anthropic::Models::TextDelta | Anthropic::Models::BetaTextDelta if content_block.type == :text
180
173
  events_to_yield << Anthropic::Streaming::TextEvent.new(
181
174
  type: :text,
182
175
  text: delta.text,
183
176
  snapshot: content_block.text
184
177
  )
185
- in Anthropic::Models::InputJSONDelta if content_block.type == :tool_use
178
+ in Anthropic::Models::InputJSONDelta | Anthropic::Models::BetaInputJSONDelta if content_block.type == :tool_use
186
179
  events_to_yield << Anthropic::Streaming::InputJsonEvent.new(
187
180
  type: :input_json,
188
181
  partial_json: delta.partial_json,
189
182
  snapshot: content_block.input
190
183
  )
191
- in Anthropic::Models::CitationsDelta if content_block.type == :text
184
+ in Anthropic::Models::CitationsDelta | Anthropic::Models::BetaCitationsDelta if content_block.type == :text
192
185
  events_to_yield << Anthropic::Streaming::CitationEvent.new(
193
186
  type: :citation,
194
187
  citation: delta.citation,
195
188
  snapshot: content_block.citations || []
196
189
  )
197
- in Anthropic::Models::ThinkingDelta if content_block.type == :thinking
190
+ in Anthropic::Models::ThinkingDelta | Anthropic::Models::BetaThinkingDelta if content_block.type == :thinking
198
191
  events_to_yield << Anthropic::Streaming::ThinkingEvent.new(
199
192
  type: :thinking,
200
193
  thinking: delta.thinking,
201
194
  snapshot: content_block.thinking
202
195
  )
203
- in Anthropic::Models::SignatureDelta if content_block.type == :thinking
196
+ in Anthropic::Models::SignatureDelta | Anthropic::Models::BetaSignatureDelta if content_block.type == :thinking
204
197
  events_to_yield << Anthropic::Streaming::SignatureEvent.new(
205
198
  type: :signature,
206
199
  signature: content_block.signature
207
200
  )
208
201
  else
209
202
  end
210
- in Anthropic::Models::RawContentBlockStopEvent
203
+ in Anthropic::Models::RawContentBlockStopEvent | Anthropic::Models::BetaRawContentBlockStopEvent
211
204
  content_block = message_snapshot.content[event.index]
212
205
 
213
- events_to_yield << ContentBlockStopEvent.new(
206
+ events_to_yield << Anthropic::Streaming::ContentBlockStopEvent.new(
214
207
  type: :content_block_stop,
215
208
  index: event.index,
216
209
  content_block: content_block
@@ -232,21 +225,33 @@ module Anthropic
232
225
  private def parse_tool_uses!(message)
233
226
  return message unless message&.content
234
227
 
228
+ # rubocop:disable Metrics/BlockLength
235
229
  message.content.each_with_index do |content, index|
236
230
  next unless content.type == :tool_use
237
231
 
238
- next unless (model = @tool_models[content.name])
232
+ next unless (tool = @tools[content.name])
239
233
 
240
234
  parsed =
241
235
  begin
242
- parsed_input = content.input.is_a?(String) ? JSON.parse(content.input) : content.input
236
+ parsed_input = if content.input.is_a?(String)
237
+ JSON.parse(content.input, symbolize_names: true)
238
+ else
239
+ content.input
240
+ end
243
241
 
244
- Anthropic::Internal::Type::Converter.coerce(model, parsed_input)
242
+ Anthropic::Internal::Type::Converter.coerce(tool, parsed_input)
245
243
  rescue StandardError => e
246
244
  e
247
245
  end
248
246
 
249
- message.content[index] = Anthropic::Models::ToolUseBlock.new(
247
+ cls =
248
+ case content
249
+ in Anthropic::ContentBlock
250
+ Anthropic::Models::ToolUseBlock
251
+ else
252
+ Anthropic::Models::BetaToolUseBlock
253
+ end
254
+ message.content[index] = cls.new(
250
255
  id: content.id,
251
256
  input: content.input,
252
257
  name: content.name,
@@ -254,6 +259,7 @@ module Anthropic
254
259
  parsed: parsed
255
260
  )
256
261
  end
262
+ # rubocop:enable Metrics/BlockLength
257
263
 
258
264
  message
259
265
  end
@@ -261,14 +267,19 @@ module Anthropic
261
267
  # @api private
262
268
  #
263
269
  # @param raw_stream [Anthropic::Internal::Type::BaseStream]
264
- # @param tool_models [Hash{String=>Class}] Mapping of tool names to model classes
265
- def initialize(raw_stream:, tool_models: {})
270
+ #
271
+ # @param tools [Hash{String=>Class}] Mapping of tool names to model classes
272
+ #
273
+ # @param models [Hash{String=>Class}] Mapping of tool names to model classes
274
+ def initialize(raw_stream:, tools: {}, models: {})
266
275
  # The underlying Server-Sent Event stream from the Anthropic API.
267
276
  @raw_stream = raw_stream
268
277
  # Accumulated message state that builds up as events are processed.
269
278
  @accumated_message_snapshot = nil
270
279
  # Mapping of tool names to model classes for parsing.
271
- @tool_models = tool_models
280
+ @tools = tools
281
+ # Mapping of tool names to model classes for parsing.
282
+ @models = models
272
283
  # Lazy enumerable that transforms raw events into consumable events.
273
284
  @iterator = iterator
274
285
  @status = raw_stream.status
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Anthropic
4
+ module Helpers
5
+ module Tools
6
+ class BaseTool
7
+ include Anthropic::Internal::Type::Converter
8
+ include Anthropic::Helpers::InputSchema::JsonSchemaConverter
9
+
10
+ class << self
11
+ # @api public
12
+ #
13
+ # @return [Class<Anthropic::Helpers::InputSchema::BaseModel>]
14
+ attr_reader :model
15
+
16
+ # @return [String]
17
+ attr_reader :doc_string
18
+
19
+ # @api public
20
+ #
21
+ # @param description [String]
22
+ def description(description) = (@doc_string = description)
23
+
24
+ alias_method :doc, :description
25
+
26
+ # @api public
27
+ #
28
+ # @model [Class<Anthropic::Helpers::InputSchema::BaseModel>]
29
+ def input_schema(model) = (@model = model)
30
+
31
+ # @api private
32
+ #
33
+ # @param depth [Integer]
34
+ def inspect(depth: 0) = "#{name}[#{model.inspect(depth:)}]"
35
+ end
36
+
37
+ # @api private
38
+ #
39
+ def to_json_schema_inner(state:) = self.class.model&.to_json_schema_inner(state:)
40
+
41
+ # @api private
42
+ #
43
+ def to_json_schema = self.class.model&.to_json_schema
44
+
45
+ # @api private
46
+ #
47
+ def dump(value, state:)
48
+ Anthropic::Internal::Type::Converter.dump(self.class.model, value, state:)
49
+ end
50
+
51
+ # @api private
52
+ #
53
+ def coerce(value, state:)
54
+ parsed = parse(value)
55
+ Anthropic::Internal::Type::Converter.coerce(self.class.model, parsed, state:)
56
+ end
57
+
58
+ # rubocop:disable Lint/UnusedMethodArgument
59
+
60
+ # @api public
61
+ #
62
+ # Override the `#parse` method to customize the pre-processing of the tool call argument
63
+ #
64
+ # @param value [Object]
65
+ #
66
+ # @return [Object]
67
+ def parse(value) = value
68
+
69
+ # @api public
70
+ #
71
+ # @param parsed [Anthropic::Helpers::InputSchema::BaseModel]
72
+ def call(parsed) = raise NotImplementedError.new
73
+
74
+ # rubocop:enable Lint/UnusedMethodArgument
75
+
76
+ # @api private
77
+ #
78
+ def inspect = "#<#{self.class.inspect(depth: 1)}:0x#{object_id.to_s(16)}>"
79
+ end
80
+ end
81
+ end
82
+ end