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 +4 -4
- data/README.md +40 -0
- data/lib/omniai/anthropic/chat/content_serializer.rb +2 -1
- data/lib/omniai/anthropic/chat/response_serializer.rb +1 -1
- data/lib/omniai/anthropic/chat/stream.rb +44 -2
- data/lib/omniai/anthropic/chat/thinking_serializer.rb +30 -0
- data/lib/omniai/anthropic/chat.rb +33 -2
- data/lib/omniai/anthropic/client.rb +3 -2
- data/lib/omniai/anthropic/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5077178a43cec44b1c90e0fc272ee5b1e6e8b025387ed6bb71c2238514187631
|
|
4
|
+
data.tar.gz: 416834202514d4acfa5e742a8dba508f8aecd9042a5cc5ecff16d22366406cd7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
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
|
-
|
|
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"] +=
|
|
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
|
-
|
|
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
|
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.
|
|
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
|