omniai 3.7.1 → 3.8.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e22534811cc346b74927ec47382420e86faee449ff730cbb0665d30a714cd872
4
- data.tar.gz: 1c3e58ad36506a2564563c46a700e8be04731b7266d8008bbd63bcc22d9cfa64
3
+ metadata.gz: 444922e4ffb4b54da232b01faab2b171f39ec2547830a7c02a4dc607173acae9
4
+ data.tar.gz: 221d5e646b968f9cb422c64547966855518351038c9f1d7248961778785dd8fc
5
5
  SHA512:
6
- metadata.gz: ea3efdf811897d5fff8f55b0e4051b71f151d077230518b139fde0ef38656c6b784f9aafcfccb43c692fe85a102b47d0ca41c8696af767294b1a2270dfeee5fa
7
- data.tar.gz: f3e87e1dd51ca72ec1d8706d3833a06c10b65947fdaf1e9a375518ca2b3d78f03a43af1c2869de1e84fb1c5cd77226ecbd5aa45fa7092f84691f0ea5f5afd183
6
+ metadata.gz: 30da07946fab93796f28c4d831ef00f0b609051e664a1cdc01373af8e6079d857239bf26be88d5cbeb6e9d25c7c2bcc08588979e3393da661bc73057e4c34bcb
7
+ data.tar.gz: '09907c95ccd31c1f672f2fcc324a11a23b63799916db570a4a4fcaee4677249f430e15071a3f5e5ca34e72d867dedfc039a52527aa2dc9d6ffe7734b4f7d7dc7'
data/README.md CHANGED
@@ -578,6 +578,30 @@ client.chat("Solve this step by step: What is 123 * 456?", thinking: true, strea
578
578
  | Google | `thinking: true` | Requires Gemini 2.0+ with thinking enabled |
579
579
  | OpenAI | `thinking: true` or `thinking: { effort: "high" }` | Requires o1/o3 models |
580
580
 
581
+ #### Thinking Token Accounting
582
+
583
+ Reasoning tokens are billable. `OmniAI::Chat::Usage#thinking_tokens` reports how many of a response's output tokens were internal reasoning:
584
+
585
+ ```ruby
586
+ response = client.chat("What is 25 * 25?", thinking: true)
587
+
588
+ response.usage.output_tokens # => 1424 — billable output, reasoning included
589
+ response.usage.thinking_tokens # => 840 — the reasoning subset of the above
590
+ ```
591
+
592
+ `thinking_tokens` is always a **subset** of `output_tokens`, never an addition to it. Adding the two together double counts.
593
+
594
+ 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:
595
+
596
+ | Provider | Populated | Read from |
597
+ |----------|-----------|-----------|
598
+ | omniai-google >= 3.12 | yes | `thoughtsTokenCount` |
599
+ | omniai-anthropic >= 3.6 | yes | `usage.output_tokens_details.thinking_tokens` |
600
+ | omniai-openai >= 3.2 | yes | `usage.output_tokens_details.reasoning_tokens` (Responses API) |
601
+ | omniai-mistral | no | no breakdown reported |
602
+
603
+ 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.
604
+
581
605
  ### 🎤 Speech to Text
582
606
 
583
607
  Clients that support transcribe (e.g. OpenAI w/ "Whisper") convert recordings to text via the following calls:
@@ -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
- chain = response_chain
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 { |u| u.input_tokens || 0 }
163
- output_tokens = usages.sum { |u| u.output_tokens || 0 }
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
@@ -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
- # @param input_tokens [Integer]
17
- # @param output_tokens [Integer]
18
- # @param total_tokens [Integer]
19
- def initialize(input_tokens:, output_tokens:, total_tokens:)
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} total_tokens=#{total_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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module OmniAI
4
- VERSION = "3.7.1"
4
+ VERSION = "3.8.0"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: omniai
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.7.1
4
+ version: 3.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kevin Sylvestre