omniai 3.7.1 → 3.9.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.
- checksums.yaml +4 -4
- data/README.md +46 -0
- data/lib/omniai/chat/response.rb +19 -9
- data/lib/omniai/chat/usage.rb +35 -10
- data/lib/omniai/chat.rb +7 -0
- data/lib/omniai/client.rb +4 -1
- data/lib/omniai/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 79d29d3011d03634e81f70e37b83af802c8881afb6920769a2b49e165f244735
|
|
4
|
+
data.tar.gz: 15b2e1cc6af34b9692f2d0628c8a94754f7d2dafc6b545bd739c2d4d88fe8141
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d3d3636fc343966d21f40c7b09b566bcc3a3375e5d52e3b700f9d9db6e69194cb6d657cc25f721216ece7f8fa3d775216ca1baaaeb527b865d16be9398aaee47
|
|
7
|
+
data.tar.gz: bb45ac481dd2b27edf8e62ba66667ed1d17e77b4c44ab7505b17af0b78db4e8c7550d25fd67b8e5556b97e8455a8c65db443fcfb52abc3a2c3c4130904fafb65
|
data/README.md
CHANGED
|
@@ -545,6 +545,28 @@ end
|
|
|
545
545
|
client.chat('What is the weather in "London" in Celsius and "Paris" in Fahrenheit?', tools: [WeatherTool.new])
|
|
546
546
|
```
|
|
547
547
|
|
|
548
|
+
#### Per-Round Usage
|
|
549
|
+
|
|
550
|
+
A tool-call chain makes one request per round, but only the final `Response` is returned. Pass `on_response:` to receive each round's `Response` as it completes, before that round's tool calls run. It fires for single-round chats too and is independent of `stream:`.
|
|
551
|
+
|
|
552
|
+
Usage then survives an abort. When a stream block or a tool raises mid-chain, `process!` unwinds without returning a `Response`, but the rounds already handed over are still accounted for:
|
|
553
|
+
|
|
554
|
+
```ruby
|
|
555
|
+
rounds = []
|
|
556
|
+
|
|
557
|
+
stream = proc do |chunk|
|
|
558
|
+
raise TooManyRounds if rounds.length >= 20
|
|
559
|
+
|
|
560
|
+
print(chunk.text)
|
|
561
|
+
end
|
|
562
|
+
|
|
563
|
+
begin
|
|
564
|
+
client.chat(prompt, tools:, stream:, on_response: proc { |round| rounds << round })
|
|
565
|
+
rescue TooManyRounds
|
|
566
|
+
rounds.filter_map(&:usage).sum { |usage| usage.input_tokens.to_i + usage.output_tokens.to_i }
|
|
567
|
+
end
|
|
568
|
+
```
|
|
569
|
+
|
|
548
570
|
#### Extended Thinking / Reasoning
|
|
549
571
|
|
|
550
572
|
Some models support extended thinking or reasoning capabilities. OmniAI provides a unified `thinking:` option that works across all supported providers:
|
|
@@ -578,6 +600,30 @@ client.chat("Solve this step by step: What is 123 * 456?", thinking: true, strea
|
|
|
578
600
|
| Google | `thinking: true` | Requires Gemini 2.0+ with thinking enabled |
|
|
579
601
|
| OpenAI | `thinking: true` or `thinking: { effort: "high" }` | Requires o1/o3 models |
|
|
580
602
|
|
|
603
|
+
#### Thinking Token Accounting
|
|
604
|
+
|
|
605
|
+
Reasoning tokens are billable. `OmniAI::Chat::Usage#thinking_tokens` reports how many of a response's output tokens were internal reasoning:
|
|
606
|
+
|
|
607
|
+
```ruby
|
|
608
|
+
response = client.chat("What is 25 * 25?", thinking: true)
|
|
609
|
+
|
|
610
|
+
response.usage.output_tokens # => 1424 — billable output, reasoning included
|
|
611
|
+
response.usage.thinking_tokens # => 840 — the reasoning subset of the above
|
|
612
|
+
```
|
|
613
|
+
|
|
614
|
+
`thinking_tokens` is always a **subset** of `output_tokens`, never an addition to it. Adding the two together double counts.
|
|
615
|
+
|
|
616
|
+
It is `nil` when the provider reported no breakdown, which is deliberately distinct from `0` (the provider reported that no reasoning occurred). Each provider gem reads its own vocabulary:
|
|
617
|
+
|
|
618
|
+
| Provider | Populated | Read from |
|
|
619
|
+
|----------|-----------|-----------|
|
|
620
|
+
| omniai-google >= 3.12 | yes | `thoughtsTokenCount` |
|
|
621
|
+
| omniai-anthropic >= 3.6 | yes | `usage.output_tokens_details.thinking_tokens` |
|
|
622
|
+
| omniai-openai >= 3.2 | yes | `usage.output_tokens_details.reasoning_tokens` (Responses API) |
|
|
623
|
+
| omniai-mistral | no | no breakdown reported |
|
|
624
|
+
|
|
625
|
+
Note also that `total_tokens` may exceed `input_tokens + output_tokens` — providers count buckets OmniAI does not model, such as cached input and tool-use prompts — and may be `nil` where a provider reports no total. The reported total is authoritative and is never recomputed from the parts.
|
|
626
|
+
|
|
581
627
|
### 🎤 Speech to Text
|
|
582
628
|
|
|
583
629
|
Clients that support transcribe (e.g. OpenAI w/ "Whisper") convert recordings to text via the following calls:
|
data/lib/omniai/chat/response.rb
CHANGED
|
@@ -153,20 +153,30 @@ module OmniAI
|
|
|
153
153
|
# Returns aggregated usage across all responses in the chain.
|
|
154
154
|
# Walks the parent chain and sums all token counts.
|
|
155
155
|
#
|
|
156
|
+
# `total_tokens` prefers each response's provider-reported total and only falls back to `input + output` for
|
|
157
|
+
# responses where the provider reported none. Summing the reported totals matters wherever a provider counts
|
|
158
|
+
# tokens that are neither input nor output — Google's `totalTokenCount` includes thinking tokens, so
|
|
159
|
+
# recomputing unconditionally would discard them.
|
|
160
|
+
#
|
|
161
|
+
# Known limitation: Anthropic reports no total at all, so its contribution is always the derived
|
|
162
|
+
# `input + output`, which excludes `cache_creation_input_tokens` and `cache_read_input_tokens`. An aggregate
|
|
163
|
+
# spanning Anthropic responses therefore understates cache-heavy conversations.
|
|
164
|
+
#
|
|
156
165
|
# @return [Usage, nil]
|
|
157
166
|
def total_usage
|
|
158
|
-
|
|
159
|
-
usages = chain.map(&:usage).compact
|
|
167
|
+
usages = response_chain.map(&:usage).compact
|
|
160
168
|
return nil if usages.empty?
|
|
161
169
|
|
|
162
|
-
input_tokens = usages.sum { |
|
|
163
|
-
output_tokens = usages.sum { |
|
|
170
|
+
input_tokens = usages.sum { |usage| usage.input_tokens || 0 }
|
|
171
|
+
output_tokens = usages.sum { |usage| usage.output_tokens || 0 }
|
|
172
|
+
total_tokens = usages.sum do |usage|
|
|
173
|
+
usage.total_tokens || ((usage.input_tokens || 0) + (usage.output_tokens || 0))
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
thinking = usages.filter_map(&:thinking_tokens)
|
|
177
|
+
thinking_tokens = thinking.sum unless thinking.empty?
|
|
164
178
|
|
|
165
|
-
Usage.new(
|
|
166
|
-
input_tokens:,
|
|
167
|
-
output_tokens:,
|
|
168
|
-
total_tokens: input_tokens + output_tokens
|
|
169
|
-
)
|
|
179
|
+
Usage.new(input_tokens:, output_tokens:, total_tokens:, thinking_tokens:)
|
|
170
180
|
end
|
|
171
181
|
end
|
|
172
182
|
end
|
data/lib/omniai/chat/usage.rb
CHANGED
|
@@ -3,28 +3,52 @@
|
|
|
3
3
|
module OmniAI
|
|
4
4
|
class Chat
|
|
5
5
|
# The usage of a chat in terms of tokens (input / output / total).
|
|
6
|
+
#
|
|
7
|
+
# Two invariants hold across every provider:
|
|
8
|
+
#
|
|
9
|
+
# - `thinking_tokens` is a *subset* of `output_tokens`, never an addition to it. Providers either fold reasoning
|
|
10
|
+
# into their output count already (reporting the breakdown separately) or report it separately and have it
|
|
11
|
+
# added in by their own serializer. Adding `thinking_tokens` to `output_tokens` double counts.
|
|
12
|
+
# - `total_tokens` may exceed `input_tokens + output_tokens`. Providers count buckets this class does not model
|
|
13
|
+
# — cached input, tool-use prompts — so the reported total is authoritative and is never recomputed from the
|
|
14
|
+
# parts. It may also be `nil`: some providers report no total at all.
|
|
15
|
+
#
|
|
16
|
+
# Provider-specific vocabulary is read by that provider's own `:usage` deserializer, not here. This class reads
|
|
17
|
+
# only its own keys and the flat OpenAI-compatible aliases the base client speaks.
|
|
6
18
|
class Usage
|
|
7
|
-
# @return [Integer]
|
|
19
|
+
# @return [Integer, nil]
|
|
8
20
|
attr_accessor :input_tokens
|
|
9
21
|
|
|
10
|
-
# @return [Integer]
|
|
22
|
+
# @return [Integer, nil]
|
|
11
23
|
attr_accessor :output_tokens
|
|
12
24
|
|
|
13
|
-
# @return [Integer]
|
|
25
|
+
# @return [Integer, nil]
|
|
14
26
|
attr_accessor :total_tokens
|
|
15
27
|
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
|
|
28
|
+
# The subset of `output_tokens` a provider attributes to internal reasoning ("thinking"). `nil` when the
|
|
29
|
+
# provider does not report a breakdown — which is distinct from `0`, meaning the provider reported that no
|
|
30
|
+
# reasoning occurred.
|
|
31
|
+
#
|
|
32
|
+
# @return [Integer, nil]
|
|
33
|
+
attr_accessor :thinking_tokens
|
|
34
|
+
|
|
35
|
+
# @param input_tokens [Integer, nil]
|
|
36
|
+
# @param output_tokens [Integer, nil]
|
|
37
|
+
# @param total_tokens [Integer, nil]
|
|
38
|
+
# @param thinking_tokens [Integer, nil] optional
|
|
39
|
+
def initialize(input_tokens:, output_tokens:, total_tokens:, thinking_tokens: nil)
|
|
20
40
|
@input_tokens = input_tokens
|
|
21
41
|
@output_tokens = output_tokens
|
|
22
42
|
@total_tokens = total_tokens
|
|
43
|
+
@thinking_tokens = thinking_tokens
|
|
23
44
|
end
|
|
24
45
|
|
|
25
46
|
# @return [String]
|
|
26
47
|
def inspect
|
|
27
|
-
"#<#{self.class.name} input_tokens=#{input_tokens} output_tokens=#{output_tokens}
|
|
48
|
+
text = "#<#{self.class.name} input_tokens=#{input_tokens} output_tokens=#{output_tokens} " \
|
|
49
|
+
"total_tokens=#{total_tokens}"
|
|
50
|
+
text += " thinking_tokens=#{thinking_tokens}" unless thinking_tokens.nil?
|
|
51
|
+
"#{text}>"
|
|
28
52
|
end
|
|
29
53
|
|
|
30
54
|
# @param data [Hash]
|
|
@@ -38,8 +62,9 @@ module OmniAI
|
|
|
38
62
|
input_tokens = data["input_tokens"] || data["prompt_tokens"]
|
|
39
63
|
output_tokens = data["output_tokens"] || data["completion_tokens"]
|
|
40
64
|
total_tokens = data["total_tokens"]
|
|
65
|
+
thinking_tokens = data["thinking_tokens"]
|
|
41
66
|
|
|
42
|
-
new(input_tokens:, output_tokens:, total_tokens:)
|
|
67
|
+
new(input_tokens:, output_tokens:, total_tokens:, thinking_tokens:)
|
|
43
68
|
end
|
|
44
69
|
|
|
45
70
|
# @param context [OmniAI::Context] optional
|
|
@@ -53,7 +78,7 @@ module OmniAI
|
|
|
53
78
|
input_tokens:,
|
|
54
79
|
output_tokens:,
|
|
55
80
|
total_tokens:,
|
|
56
|
-
}
|
|
81
|
+
}.tap { |data| data[:thinking_tokens] = thinking_tokens unless thinking_tokens.nil? }
|
|
57
82
|
end
|
|
58
83
|
end
|
|
59
84
|
end
|
data/lib/omniai/chat.rb
CHANGED
|
@@ -66,6 +66,8 @@ module OmniAI
|
|
|
66
66
|
# @param stream [Proc, IO, nil] optional
|
|
67
67
|
# @param tools [Array<OmniAI::Tool>, nil] optional
|
|
68
68
|
# @param format [:json, :text, OmniAI::Schema::Object, nil] optional
|
|
69
|
+
# @param on_response [Proc, nil] optional - called with each completed round's `Response` before its tool
|
|
70
|
+
# calls run
|
|
69
71
|
# @param options [Hash] optional (used for vendor specific options)
|
|
70
72
|
#
|
|
71
73
|
# @yield [prompt] optional
|
|
@@ -80,6 +82,7 @@ module OmniAI
|
|
|
80
82
|
stream: nil,
|
|
81
83
|
tools: nil,
|
|
82
84
|
format: nil,
|
|
85
|
+
on_response: nil,
|
|
83
86
|
**options,
|
|
84
87
|
&block
|
|
85
88
|
)
|
|
@@ -94,6 +97,7 @@ module OmniAI
|
|
|
94
97
|
@stream = stream
|
|
95
98
|
@tools = tools
|
|
96
99
|
@format = format
|
|
100
|
+
@on_response = on_response
|
|
97
101
|
@options = options || {}
|
|
98
102
|
end
|
|
99
103
|
|
|
@@ -112,6 +116,8 @@ module OmniAI
|
|
|
112
116
|
raise SSLError, e.message, cause: e
|
|
113
117
|
end
|
|
114
118
|
|
|
119
|
+
@on_response&.call(completion)
|
|
120
|
+
|
|
115
121
|
if tools? && completion.tool_call_list?
|
|
116
122
|
next_completion = spawn!(
|
|
117
123
|
@prompt.dup.tap do |prompt|
|
|
@@ -165,6 +171,7 @@ module OmniAI
|
|
|
165
171
|
stream: @stream,
|
|
166
172
|
tools: @tools,
|
|
167
173
|
format: @format,
|
|
174
|
+
on_response: @on_response,
|
|
168
175
|
**@options
|
|
169
176
|
)
|
|
170
177
|
end
|
data/lib/omniai/client.rb
CHANGED
|
@@ -191,13 +191,16 @@ module OmniAI
|
|
|
191
191
|
# @param temperature [Float, nil] optional
|
|
192
192
|
# @param stream [Proc, nil] optional
|
|
193
193
|
# @param tools [Array<OmniAI::Tool>] optional
|
|
194
|
+
# @param on_response [Proc, nil] optional - called with each completed round's `Response` before its tool
|
|
195
|
+
# calls run
|
|
194
196
|
# @param options [Hash, nil] optional
|
|
195
197
|
#
|
|
196
198
|
# @yield [prompt] optional
|
|
197
199
|
# @yieldparam prompt [OmniAI::Chat::Prompt]
|
|
198
200
|
#
|
|
199
201
|
# @return [OmniAI::Chat::Response]
|
|
200
|
-
def chat(prompt = nil, model:, temperature: nil, format: nil, stream: nil, tools: nil,
|
|
202
|
+
def chat(prompt = nil, model:, temperature: nil, format: nil, stream: nil, tools: nil, on_response: nil,
|
|
203
|
+
options: {}, &)
|
|
201
204
|
raise NotImplementedError, "#{self.class.name}#chat undefined"
|
|
202
205
|
end
|
|
203
206
|
|
data/lib/omniai/version.rb
CHANGED