omniai-anthropic 3.0.1 → 3.1.1

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 122d1e2eb26278fa1fe10129363873f18143d8997ec248246919b1a5291c6596
4
- data.tar.gz: '0268f4f9c74dfb2705169d31d1ac870793d1e3b14316c897c5ee5def2aca2da9'
3
+ metadata.gz: 5077178a43cec44b1c90e0fc272ee5b1e6e8b025387ed6bb71c2238514187631
4
+ data.tar.gz: 416834202514d4acfa5e742a8dba508f8aecd9042a5cc5ecff16d22366406cd7
5
5
  SHA512:
6
- metadata.gz: e217d230e744747c151f844b5de5f1aaf080f99bf9fdb688eb3687ab38571488e9412dea9d7bd79bb884b1bee548ed6cb9e87a45a0448db042fbe770e03841a5
7
- data.tar.gz: efb235cbc97a968a4f915dbfe6b48fda1e34a4aa47771ee2f50831af33534648faa475dc02aca0f2147ee9a3e286ccdb336528c26ed2cc53c029439ac4e8517d
6
+ metadata.gz: bab092c2f66f0ac792f09574974afdaa0f8e6a575482661c168374089aa5380202feeaaa617a2259fd81718f738b6c68c0280bba438ba35de7ae66d154f0a555
7
+ data.tar.gz: 5e4b8e0d4db1e9769031d8c11e11eedd4d6ad4ff14c6f06d48ae9271138cbcd187ebf379ad9059907fdf5f92c79004f6973522bc58561ac0b1fc1c456d70e3c2
data/README.md CHANGED
@@ -130,3 +130,43 @@ completion = client.chat(tools: [computer]) do |prompt|
130
130
  prompt.user('Please signup for reddit')
131
131
  end
132
132
  ```
133
+
134
+ ### Extended Thinking
135
+
136
+ Extended thinking allows Claude to show its reasoning process. This is useful for complex problems where you want to see the model's thought process.
137
+
138
+ ```ruby
139
+ # Enable with default budget (10,000 tokens)
140
+ response = client.chat("What is 25 * 25?", model: "claude-sonnet-4-20250514", thinking: true)
141
+
142
+ # Or specify a custom budget
143
+ response = client.chat("Solve this complex problem...", model: "claude-sonnet-4-20250514", thinking: { budget_tokens: 20_000 })
144
+ ```
145
+
146
+ When thinking is enabled:
147
+ - Temperature is automatically set to 1 (required by Anthropic)
148
+ - `max_tokens` is automatically adjusted to be greater than `budget_tokens`
149
+
150
+ #### Accessing Thinking Content
151
+
152
+ ```ruby
153
+ response.choices.first.message.contents.each do |content|
154
+ case content
155
+ when OmniAI::Chat::Thinking
156
+ puts "Thinking: #{content.thinking}"
157
+ puts "Signature: #{content.metadata[:signature]}" # Anthropic includes a signature
158
+ when OmniAI::Chat::Text
159
+ puts "Response: #{content.text}"
160
+ end
161
+ end
162
+ ```
163
+
164
+ #### Streaming with Thinking
165
+
166
+ ```ruby
167
+ client.chat("What are the prime factors of 1234567?", model: "claude-sonnet-4-20250514", thinking: true, stream: $stdout)
168
+ ```
169
+
170
+ The thinking content will stream first, followed by the response.
171
+
172
+ [Anthropic API Reference `thinking`](https://docs.anthropic.com/en/docs/build-with-claude/thinking)
@@ -7,10 +7,11 @@ module OmniAI
7
7
  module ContentSerializer
8
8
  # @param data [Hash]
9
9
  # @param context [Context]
10
- # @return [OmniAI::Chat::Text, OmniAI::Chat::ToolCall]
10
+ # @return [OmniAI::Chat::Text, OmniAI::Chat::ToolCall, OmniAI::Chat::Thinking]
11
11
  def self.deserialize(data, context:)
12
12
  case data["type"]
13
13
  when "text" then OmniAI::Chat::Text.deserialize(data, context:)
14
+ when "thinking" then OmniAI::Chat::Thinking.deserialize(data, context:)
14
15
  when "tool_use" then OmniAI::Chat::ToolCall.deserialize(data, context:)
15
16
  end
16
17
  end
@@ -11,7 +11,7 @@ module OmniAI
11
11
  # @return [Hash]
12
12
  def self.serialize(response, context:)
13
13
  usage = response.usage.serialize(context:)
14
- choice = response.choice.serialize(context:)
14
+ choice = response.choices.first.serialize(context:)
15
15
 
16
16
  choice.merge({ usage: })
17
17
  end
@@ -17,11 +17,14 @@ module OmniAI
17
17
  module ContentBlockType
18
18
  TEXT = "text"
19
19
  TOOL_USE = "tool_use"
20
+ THINKING = "thinking"
20
21
  end
21
22
 
22
23
  module ContentBlockDeltaType
23
24
  TEXT_DELTA = "text_delta"
24
25
  INPUT_JSON_DELTA = "input_json_delta"
26
+ THINKING_DELTA = "thinking_delta"
27
+ SIGNATURE_DELTA = "signature_delta"
25
28
  end
26
29
 
27
30
  # @yield [delta]
@@ -96,7 +99,12 @@ module OmniAI
96
99
  # @param data [Hash]
97
100
  def content_block_start(data)
98
101
  index = data["index"]
99
- @data["content"][index] = data["content_block"]
102
+ content_block = data["content_block"]
103
+
104
+ # Initialize thinking content blocks with empty string for accumulation
105
+ content_block["thinking"] = "" if content_block["type"] == ContentBlockType::THINKING
106
+
107
+ @data["content"][index] = content_block
100
108
  end
101
109
 
102
110
  # Handler for Type::CONTENT_BLOCK_DELTA
@@ -109,8 +117,12 @@ module OmniAI
109
117
  case data["delta"]["type"]
110
118
  when ContentBlockDeltaType::TEXT_DELTA
111
119
  content_block_delta_for_text_delta(data, &)
120
+ when ContentBlockDeltaType::THINKING_DELTA
121
+ content_block_delta_for_thinking_delta(data, &)
112
122
  when ContentBlockDeltaType::INPUT_JSON_DELTA
113
123
  content_block_delta_for_input_json_delta(data, &)
124
+ when ContentBlockDeltaType::SIGNATURE_DELTA
125
+ content_block_delta_for_signature_delta(data)
114
126
  end
115
127
  end
116
128
 
@@ -125,11 +137,27 @@ module OmniAI
125
137
  text = data["delta"]["text"]
126
138
 
127
139
  content = @data["content"][index]
128
- content["text"] += data["delta"]["text"]
140
+ content["text"] += text
129
141
 
130
142
  block&.call(OmniAI::Chat::Delta.new(text:))
131
143
  end
132
144
 
145
+ # Handler for Type::CONTENT_BLOCK_DELTA w/ ContentBlockDeltaType::THINKING_DELTA
146
+ #
147
+ # @yield [delta]
148
+ # @yieldparam delta [OmniAI::Chat::Delta]
149
+ #
150
+ # @param data [Hash]
151
+ def content_block_delta_for_thinking_delta(data, &block)
152
+ index = data["index"]
153
+ thinking = data["delta"]["thinking"]
154
+
155
+ content = @data["content"][index]
156
+ content["thinking"] += thinking
157
+
158
+ block&.call(OmniAI::Chat::Delta.new(thinking:))
159
+ end
160
+
133
161
  # Handler for Type::CONTENT_BLOCK_DELTA w/ ContentBlockDeltaType::INPUT_JSON_DELTA
134
162
  #
135
163
  # @yield [delta]
@@ -142,6 +170,15 @@ module OmniAI
142
170
  content["partial_json"] += data["delta"]["partial_json"]
143
171
  end
144
172
 
173
+ # Handler for Type::CONTENT_BLOCK_DELTA w/ ContentBlockDeltaType::SIGNATURE_DELTA
174
+ #
175
+ # @param data [Hash]
176
+ def content_block_delta_for_signature_delta(data)
177
+ content = @data["content"][data["index"]]
178
+ content["signature"] ||= ""
179
+ content["signature"] += data["delta"]["signature"]
180
+ end
181
+
145
182
  # Handler for Type::CONTENT_BLOCK_STOP
146
183
  #
147
184
  # @param data [Hash]
@@ -149,6 +186,11 @@ module OmniAI
149
186
  index = data["index"]
150
187
  content = @data["content"][index]
151
188
 
189
+ # Capture signature for thinking blocks (required for tool call round-trips)
190
+ signature = data.dig("content_block", "signature")
191
+ content["signature"] = signature if content["type"] == ContentBlockType::THINKING && signature
192
+
193
+ # Handle partial JSON for tool use blocks
152
194
  return unless content["partial_json"]
153
195
  return if content["partial_json"].empty?
154
196
 
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OmniAI
4
+ module Anthropic
5
+ class Chat
6
+ # Overrides thinking serialize / deserialize.
7
+ module ThinkingSerializer
8
+ # @param data [Hash]
9
+ # @param context [Context]
10
+ #
11
+ # @return [OmniAI::Chat::Thinking]
12
+ def self.deserialize(data, context: nil) # rubocop:disable Lint/UnusedMethodArgument
13
+ OmniAI::Chat::Thinking.new(data["thinking"], metadata: { signature: data["signature"] })
14
+ end
15
+
16
+ # @param thinking [OmniAI::Chat::Thinking]
17
+ # @param context [Context]
18
+ #
19
+ # @return [Hash]
20
+ def self.serialize(thinking, context: nil) # rubocop:disable Lint/UnusedMethodArgument
21
+ {
22
+ type: "thinking",
23
+ thinking: thinking.thinking,
24
+ signature: thinking.metadata[:signature],
25
+ }.compact
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -76,18 +76,49 @@ module OmniAI
76
76
 
77
77
  context.deserializers[:content] = ContentSerializer.method(:deserialize)
78
78
  context.deserializers[:response] = ResponseSerializer.method(:deserialize)
79
+
80
+ context.serializers[:thinking] = ThinkingSerializer.method(:serialize)
81
+ context.deserializers[:thinking] = ThinkingSerializer.method(:deserialize)
79
82
  end
80
83
 
81
84
  # @return [Hash]
82
85
  def payload
83
- OmniAI::Anthropic.config.chat_options.merge({
86
+ data = OmniAI::Anthropic.config.chat_options.merge({
84
87
  model: @model,
85
88
  messages:,
86
89
  system:,
87
90
  stream: stream? || nil,
88
- temperature: @temperature,
91
+ temperature: thinking_config ? nil : @temperature, # Anthropic requires temperature=1 (default) when thinking
89
92
  tools: tools_payload,
93
+ thinking: thinking_config,
90
94
  }).compact
95
+
96
+ # When thinking is enabled, ensure max_tokens > budget_tokens
97
+ data[:max_tokens] = thinking_max_tokens if thinking_config
98
+
99
+ data
100
+ end
101
+
102
+ # Translates unified thinking option to Anthropic's native format.
103
+ # Example: `thinking: { budget_tokens: 10000 }` becomes `{ type: "enabled", budget_tokens: 10000 }`
104
+ # @return [Hash, nil]
105
+ def thinking_config
106
+ thinking = @options[:thinking]
107
+ return unless thinking
108
+
109
+ case thinking
110
+ when true then { type: "enabled", budget_tokens: 10_000 }
111
+ when Hash then { type: "enabled" }.merge(thinking)
112
+ end
113
+ end
114
+
115
+ # Returns max_tokens ensuring it's greater than budget_tokens when thinking is enabled.
116
+ # @return [Integer]
117
+ def thinking_max_tokens
118
+ budget = thinking_config[:budget_tokens]
119
+ base = @options[:max_tokens] || OmniAI::Anthropic.config.chat_options[:max_tokens] || 0
120
+ # Ensure max_tokens > budget_tokens (default to budget + 8000 for response)
121
+ [base, budget + 8_000].max
91
122
  end
92
123
 
93
124
  # @return [Array<Hash>]
@@ -67,8 +67,9 @@ module OmniAI
67
67
  # @yieldparam prompt [OmniAI::Chat::Prompt]
68
68
  #
69
69
  # @return [OmniAI::Chat::Completion]
70
- def chat(messages = nil, model: Chat::DEFAULT_MODEL, temperature: nil, format: nil, stream: nil, tools: nil, &)
71
- Chat.process!(messages, model:, temperature:, format:, stream:, tools:, client: self, &)
70
+ def chat(messages = nil, model: Chat::DEFAULT_MODEL, temperature: nil, format: nil, stream: nil, tools: nil, **,
71
+ &)
72
+ Chat.process!(messages, model:, temperature:, format:, stream:, tools:, client: self, **, &)
72
73
  end
73
74
  end
74
75
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module OmniAI
4
4
  module Anthropic
5
- VERSION = "3.0.1"
5
+ VERSION = "3.1.1"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: omniai-anthropic
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.0.1
4
+ version: 3.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kevin Sylvestre
@@ -84,6 +84,7 @@ files:
84
84
  - lib/omniai/anthropic/chat/response_serializer.rb
85
85
  - lib/omniai/anthropic/chat/stream.rb
86
86
  - lib/omniai/anthropic/chat/text_serializer.rb
87
+ - lib/omniai/anthropic/chat/thinking_serializer.rb
87
88
  - lib/omniai/anthropic/chat/tool_call_result_serializer.rb
88
89
  - lib/omniai/anthropic/chat/tool_call_serializer.rb
89
90
  - lib/omniai/anthropic/chat/tool_serializer.rb