llm_cost_tracker 0.15.2 → 0.15.3
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/CHANGELOG.md +13 -0
- data/lib/llm_cost_tracker/integrations/openai/patches.rb +6 -0
- data/lib/llm_cost_tracker/integrations/openai.rb +5 -0
- data/lib/llm_cost_tracker/integrations/ruby_llm/v2/attempt/token_counts.rb +2 -1
- data/lib/llm_cost_tracker/integrations/ruby_llm/v2/attempt.rb +15 -5
- data/lib/llm_cost_tracker/integrations/ruby_llm/v2/frame.rb +11 -0
- data/lib/llm_cost_tracker/integrations/ruby_llm/v2/seams.rb +17 -1
- data/lib/llm_cost_tracker/prices.json +536 -224
- data/lib/llm_cost_tracker/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: 2fcb73e485dc1b20fed09f36b2095911d5268e16e633645329136acbe5ef3d3f
|
|
4
|
+
data.tar.gz: 4d3622d57bd89110efa3c824fcf87bc5b5a7f785ee371b7bc42a8351219f8df5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0ac48c68f138cfe1c7707b7ed1576c8c033a0ccbe0930f500bcc57d0c458c41656889095ec20cfc66b9913c1468fedfae37fac959f5696de678779d6e09fe1cd
|
|
7
|
+
data.tar.gz: e9fc0c48df5d63382e2c98f150ae2ed25130004a80514d540a1e83a06b9b2cbbf838f4ca29ef931609d0d7cf3c95f24252b8be0bf50f03945a99bae95611d4e8
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Versioning: [SemVer](https://semver.org/spec/v2.0.0.html).
|
|
4
4
|
|
|
5
|
+
## [0.15.3] - 2026-11-11
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
|
|
9
|
+
- Bundled prices add Claude Fable 5.2 and Mythos 5.2, Gemini 4 Carbon, Gemini Omni 1.2 Flash, GPT-6-Cyber, GPT-6 Rosalind and GPT-Image-2.6, and follow Mistral's Large 4 price cut.
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- openai gem 0.115+ `connection.response.create` on a Responses WebSocket keeps its tags and `:block_requests` budget check.
|
|
14
|
+
- RubyLLM 2.2: an attempt the provider answered with a 5xx before any output is recorded again, with unknown cost.
|
|
15
|
+
- RubyLLM 2.2: transcription without usage is priced by its duration again, and speech without usage is no longer recorded as free.
|
|
16
|
+
- RubyLLM 2.2: Claude on Bedrock, now called through InvokeModel, keeps its inference profile, data residency and service tier.
|
|
17
|
+
|
|
5
18
|
## [0.15.2] - 2026-10-29
|
|
6
19
|
|
|
7
20
|
### Changed
|
|
@@ -71,6 +71,12 @@ module LlmCostTracker
|
|
|
71
71
|
end
|
|
72
72
|
end
|
|
73
73
|
|
|
74
|
+
module ResponsesConnectionCreatePatch
|
|
75
|
+
def send_response_create(event)
|
|
76
|
+
llm_cost_tracker_capture.sending(event) { super }
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
74
80
|
module ChatCompletionsPatch
|
|
75
81
|
include PatchBuilder.build(record_method: :record_response, methods: %i[create])
|
|
76
82
|
include PatchBuilder.build_stream(methods: %i[stream stream_raw])
|
|
@@ -31,6 +31,11 @@ module LlmCostTracker
|
|
|
31
31
|
with: ResponsesConnectionPatch,
|
|
32
32
|
optional: true,
|
|
33
33
|
since: "0.86.0"),
|
|
34
|
+
patch_target("OpenAI::Responses::Connection",
|
|
35
|
+
with: ResponsesConnectionCreatePatch,
|
|
36
|
+
optional: true,
|
|
37
|
+
skip_when_methods_missing: true,
|
|
38
|
+
since: "0.86.0"),
|
|
34
39
|
patch_target("OpenAI::Resources::Chat::Completions", with: ChatCompletionsPatch),
|
|
35
40
|
patch_target("OpenAI::Resources::Completions", with: CreatePatch, optional: true),
|
|
36
41
|
patch_target("OpenAI::Resources::Embeddings", with: CreatePatch, optional: true),
|
|
@@ -7,6 +7,7 @@ module LlmCostTracker
|
|
|
7
7
|
class Attempt
|
|
8
8
|
class TokenCounts
|
|
9
9
|
REFUSED = { input_tokens: 0, output_tokens: 0 }.freeze
|
|
10
|
+
NON_TOKEN_COUNTS = %i[characters audio_seconds search_units].freeze
|
|
10
11
|
|
|
11
12
|
def initialize(usage, payload, raw:, request:)
|
|
12
13
|
@usage = usage
|
|
@@ -62,7 +63,7 @@ module LlmCostTracker
|
|
|
62
63
|
|
|
63
64
|
private
|
|
64
65
|
|
|
65
|
-
def known? = @tokens.to_h.any? || !@tokens.reported_cost.nil?
|
|
66
|
+
def known? = @tokens.to_h.except(*NON_TOKEN_COUNTS).any? || !@tokens.reported_cost.nil?
|
|
66
67
|
|
|
67
68
|
def usage_source(known, result)
|
|
68
69
|
return Usage::Source::SDK_RESPONSE if known
|
|
@@ -8,6 +8,8 @@ module LlmCostTracker
|
|
|
8
8
|
module RubyLlm
|
|
9
9
|
module V2
|
|
10
10
|
class Attempt
|
|
11
|
+
BEDROCK_CLAUDE = /(?:\A|\.)anthropic\./
|
|
12
|
+
|
|
11
13
|
attr_reader :usage, :latency_ms
|
|
12
14
|
|
|
13
15
|
class << self
|
|
@@ -98,9 +100,9 @@ module LlmCostTracker
|
|
|
98
100
|
return unless @events
|
|
99
101
|
|
|
100
102
|
context = raw_context(raw, request).merge(response_status: 200, events: @events)
|
|
101
|
-
Attempt.stream_parsers.lazy.filter_map { |parser| parser.parse_stream(**context) }
|
|
102
|
-
|
|
103
|
-
|
|
103
|
+
event = Attempt.stream_parsers.lazy.filter_map { |parser| parser.parse_stream(**context) }
|
|
104
|
+
.find { |parsed| parsed.usage_source != Usage::Source::UNKNOWN }
|
|
105
|
+
sdk_event(event, usage, raw)
|
|
104
106
|
end
|
|
105
107
|
|
|
106
108
|
def parsed_event(usage, own, request)
|
|
@@ -108,8 +110,16 @@ module LlmCostTracker
|
|
|
108
110
|
return unless body.is_a?(Hash)
|
|
109
111
|
|
|
110
112
|
response = raw_context(own, request).merge(response_status: own.status, response_body: body)
|
|
111
|
-
|
|
112
|
-
|
|
113
|
+
sdk_event(body_event(usage, own, request, response), usage, own)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def sdk_event(event, usage, raw)
|
|
117
|
+
event = event&.with(provider: usage[:provider], usage_source: Usage::Source::SDK_RESPONSE)
|
|
118
|
+
return event unless event && usage[:provider] == "bedrock" && usage[:model].to_s.match?(BEDROCK_CLAUDE)
|
|
119
|
+
|
|
120
|
+
request = { model: usage[:model], service_tier: raw.try(:headers).try(:[], "x-amzn-bedrock-service-tier") }
|
|
121
|
+
event.with(model: usage[:model],
|
|
122
|
+
pricing_mode: Providers::Anthropic::UsageExtractor.pricing_mode(request: request, usage: nil))
|
|
113
123
|
end
|
|
114
124
|
|
|
115
125
|
def body_event(usage, own, request, response)
|
|
@@ -36,6 +36,8 @@ module LlmCostTracker
|
|
|
36
36
|
end
|
|
37
37
|
|
|
38
38
|
def observe(seam, value, provider)
|
|
39
|
+
return @error_status = value.try(:status) if seam == :parse_error
|
|
40
|
+
|
|
39
41
|
@provider = provider
|
|
40
42
|
case seam
|
|
41
43
|
when :parse_completion_body then @raw = value unless value.is_a?(Hash)
|
|
@@ -55,6 +57,8 @@ module LlmCostTracker
|
|
|
55
57
|
end
|
|
56
58
|
|
|
57
59
|
def add_attempt(usage)
|
|
60
|
+
usage = usage.merge(tokens: RubyLLM::Tokens.new) if maybe_billed?(usage)
|
|
61
|
+
@error_status = nil
|
|
58
62
|
raw = @raw unless usage[:status] == :succeeded && usage[:tokens].to_h.empty?
|
|
59
63
|
@reported << [usage, { latency_ms: @latency_ms, events: @window&.events, raw: raw }]
|
|
60
64
|
@window = nil
|
|
@@ -68,6 +72,13 @@ module LlmCostTracker
|
|
|
68
72
|
Attempt.new(usage, payload, final: index == final, response: @response, provider: @provider, **reported)
|
|
69
73
|
end
|
|
70
74
|
end
|
|
75
|
+
|
|
76
|
+
private
|
|
77
|
+
|
|
78
|
+
def maybe_billed?(usage)
|
|
79
|
+
usage[:status] != :succeeded && (500..599).cover?(@error_status.to_i) &&
|
|
80
|
+
usage[:tokens].to_h == Attempt::TokenCounts::REFUSED
|
|
81
|
+
end
|
|
71
82
|
end
|
|
72
83
|
end
|
|
73
84
|
end
|
|
@@ -40,6 +40,12 @@ module LlmCostTracker
|
|
|
40
40
|
BatchBridge = Module.new do
|
|
41
41
|
%i[messages results].each { |name| define_method(name) { V2.collect(self) { super() } } }
|
|
42
42
|
end
|
|
43
|
+
ErrorBridge = Module.new do
|
|
44
|
+
def parse_error(*, **options, &)
|
|
45
|
+
V2.observe(:parse_error, options[:response])
|
|
46
|
+
super
|
|
47
|
+
end
|
|
48
|
+
end
|
|
43
49
|
BRIDGES = (SEAMS.keys - %i[stream_transcription transcribe messages results current_workflow]).to_h do |seam|
|
|
44
50
|
bridge = Module.new do
|
|
45
51
|
define_method(seam) do |value, *args, **options, &block|
|
|
@@ -63,16 +69,26 @@ module LlmCostTracker
|
|
|
63
69
|
owner.prepend(bridge) unless owner == bridge
|
|
64
70
|
end
|
|
65
71
|
end
|
|
72
|
+
middleware = error_middleware
|
|
73
|
+
return unless middleware
|
|
74
|
+
|
|
75
|
+
middleware.singleton_class.prepend(ErrorBridge)
|
|
66
76
|
end
|
|
67
77
|
|
|
68
78
|
def missing
|
|
69
|
-
SEAMS.flat_map do |seam, targets|
|
|
79
|
+
seams = SEAMS.flat_map do |seam, targets|
|
|
70
80
|
targets.reject { |target| owner(target, seam) }.map { |target| "RubyLLM::#{target}##{seam}" }
|
|
71
81
|
end
|
|
82
|
+
error_middleware ? seams : [*seams, "RubyLLM::Transport::ErrorMiddleware.parse_error"]
|
|
72
83
|
end
|
|
73
84
|
|
|
74
85
|
private
|
|
75
86
|
|
|
87
|
+
def error_middleware
|
|
88
|
+
middleware = "RubyLLM::Transport::ErrorMiddleware".safe_constantize
|
|
89
|
+
middleware if middleware.respond_to?(:parse_error)
|
|
90
|
+
end
|
|
91
|
+
|
|
76
92
|
def owner(target, seam)
|
|
77
93
|
"RubyLLM::#{target}".safe_constantize&.instance_method(seam)&.owner
|
|
78
94
|
rescue NameError
|