llm_cost_tracker 0.14.1 → 0.14.2
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 +82 -0
- data/README.md +16 -5
- data/app/assets/llm_cost_tracker/application.css +16 -0
- data/app/controllers/llm_cost_tracker/calls_controller.rb +1 -3
- data/app/controllers/llm_cost_tracker/models_controller.rb +2 -4
- data/app/controllers/llm_cost_tracker/tags_controller.rb +5 -4
- data/app/helpers/llm_cost_tracker/application_helper.rb +4 -25
- data/app/helpers/llm_cost_tracker/chart_helper.rb +6 -10
- data/app/helpers/llm_cost_tracker/token_usage_helper.rb +8 -28
- data/app/models/llm_cost_tracker/call.rb +10 -14
- data/app/services/llm_cost_tracker/dashboard/data_quality.rb +4 -15
- data/app/services/llm_cost_tracker/dashboard/filter.rb +5 -9
- data/app/services/llm_cost_tracker/dashboard/pagination.rb +2 -5
- data/app/services/llm_cost_tracker/dashboard/pricing_overview.rb +2 -1
- data/app/services/llm_cost_tracker/dashboard/spend_anomaly.rb +1 -1
- data/app/services/llm_cost_tracker/dashboard/tag_breakdown.rb +2 -3
- data/app/services/llm_cost_tracker/dashboard/tag_key_explorer.rb +1 -0
- data/app/services/llm_cost_tracker/dashboard/time_series.rb +3 -5
- data/app/services/llm_cost_tracker/dashboard/top_models.rb +1 -2
- data/app/views/llm_cost_tracker/calls/show.html.erb +8 -22
- data/app/views/llm_cost_tracker/data_quality/index.html.erb +1 -1
- data/app/views/llm_cost_tracker/shared/_bar.html.erb +1 -3
- data/app/views/llm_cost_tracker/shared/_filter_pill_date.html.erb +1 -4
- data/app/views/llm_cost_tracker/shared/_filter_pill_model.html.erb +1 -4
- data/app/views/llm_cost_tracker/shared/_filter_pill_provider.html.erb +1 -4
- data/app/views/llm_cost_tracker/shared/_filter_pill_stream.html.erb +1 -4
- data/app/views/llm_cost_tracker/tags/show.html.erb +5 -5
- data/lib/llm_cost_tracker/budget/per_tag.rb +15 -9
- data/lib/llm_cost_tracker/budget.rb +44 -55
- data/lib/llm_cost_tracker/capture/event_window.rb +3 -2
- data/lib/llm_cost_tracker/capture/sdk_payload.rb +5 -1
- data/lib/llm_cost_tracker/capture/stream_collector.rb +25 -26
- data/lib/llm_cost_tracker/capture/stream_tracker.rb +9 -23
- data/lib/llm_cost_tracker/capture_verifier.rb +1 -7
- data/lib/llm_cost_tracker/charges/line_item.rb +5 -1
- data/lib/llm_cost_tracker/configuration/budgets.rb +1 -1
- data/lib/llm_cost_tracker/configuration/pricing.rb +1 -1
- data/lib/llm_cost_tracker/configuration.rb +5 -9
- data/lib/llm_cost_tracker/doctor/price_check.rb +13 -3
- data/lib/llm_cost_tracker/doctor/schema_check.rb +1 -2
- data/lib/llm_cost_tracker/doctor.rb +19 -2
- data/lib/llm_cost_tracker/engine.rb +0 -1
- data/lib/llm_cost_tracker/event.rb +8 -0
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/async_ingestion_generator.rb +0 -6
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/call_rollups_generator.rb +5 -8
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/install_generator.rb +0 -6
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/create_llm_cost_tracker_async_ingestion.rb.erb +1 -1
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/create_llm_cost_tracker_call_rollups.rb.erb +1 -1
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/create_llm_cost_tracker_calls.rb.erb +1 -1
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/initializer.rb.erb +11 -9
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_call_rollups_provider.rb.erb +1 -1
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_call_tags_key_value_index.rb.erb +1 -1
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_image_tokens.rb.erb +1 -1
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_indexes.rb.erb +1 -1
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_per_tag_budgets.rb.erb +1 -1
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/upgrade_call_rollups_provider_generator.rb +0 -6
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/upgrade_call_tags_key_value_index_generator.rb +0 -6
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/upgrade_image_tokens_generator.rb +0 -6
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/upgrade_indexes_generator.rb +3 -8
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/upgrade_per_tag_budgets_generator.rb +3 -8
- data/lib/llm_cost_tracker/ingestion/inbox.rb +2 -8
- data/lib/llm_cost_tracker/ingestion.rb +3 -1
- data/lib/llm_cost_tracker/integrations/anthropic.rb +63 -10
- data/lib/llm_cost_tracker/integrations/base.rb +39 -18
- data/lib/llm_cost_tracker/integrations/openai/batch_capture.rb +16 -13
- data/lib/llm_cost_tracker/integrations/openai/patches.rb +8 -0
- data/lib/llm_cost_tracker/integrations/openai.rb +42 -59
- data/lib/llm_cost_tracker/integrations/ruby_llm.rb +241 -76
- data/lib/llm_cost_tracker/integrations.rb +1 -20
- data/lib/llm_cost_tracker/ledger/isolation.rb +0 -7
- data/lib/llm_cost_tracker/ledger/period/totals.rb +14 -10
- data/lib/llm_cost_tracker/ledger/rollups.rb +14 -14
- data/lib/llm_cost_tracker/ledger/schema/adapter.rb +14 -3
- data/lib/llm_cost_tracker/ledger/schema/base.rb +5 -4
- data/lib/llm_cost_tracker/ledger/store.rb +2 -2
- data/lib/llm_cost_tracker/ledger/tags/breakdown.rb +1 -5
- data/lib/llm_cost_tracker/ledger/tags/encoding.rb +3 -12
- data/lib/llm_cost_tracker/ledger/tags/query.rb +0 -2
- data/lib/llm_cost_tracker/middleware/faraday.rb +36 -19
- data/lib/llm_cost_tracker/parsers.rb +7 -40
- data/lib/llm_cost_tracker/prices.json +2237 -154
- data/lib/llm_cost_tracker/pricing/backfill.rb +46 -13
- data/lib/llm_cost_tracker/pricing/calculation.rb +118 -75
- data/lib/llm_cost_tracker/pricing/effective_prices.rb +11 -10
- data/lib/llm_cost_tracker/pricing/estimator.rb +5 -2
- data/lib/llm_cost_tracker/pricing/matcher.rb +36 -22
- data/lib/llm_cost_tracker/pricing/mode.rb +2 -13
- data/lib/llm_cost_tracker/pricing/price_key.rb +5 -3
- data/lib/llm_cost_tracker/pricing/rate.rb +1 -0
- data/lib/llm_cost_tracker/pricing/registry.rb +5 -16
- data/lib/llm_cost_tracker/pricing/service_rates.rb +1 -8
- data/lib/llm_cost_tracker/pricing/sync/registry_diff.rb +13 -9
- data/lib/llm_cost_tracker/pricing/sync.rb +22 -71
- data/lib/llm_cost_tracker/providers/anthropic/parser.rb +33 -3
- data/lib/llm_cost_tracker/providers/anthropic/response_parser.rb +11 -5
- data/lib/llm_cost_tracker/providers/anthropic/usage_extractor.rb +81 -12
- data/lib/llm_cost_tracker/providers/azure/parser.rb +3 -7
- data/lib/llm_cost_tracker/providers/gemini/parser.rb +172 -48
- data/lib/llm_cost_tracker/providers/gemini/usage_extractor.rb +13 -22
- data/lib/llm_cost_tracker/providers/openai/hosts.rb +1 -1
- data/lib/llm_cost_tracker/providers/openai/parser.rb +11 -13
- data/lib/llm_cost_tracker/providers/openai/response_parser.rb +96 -26
- data/lib/llm_cost_tracker/providers/openai/service_charges.rb +57 -40
- data/lib/llm_cost_tracker/providers/openai/usage_extractor.rb +14 -1
- data/lib/llm_cost_tracker/providers/openai_compatible/parser.rb +3 -1
- data/lib/llm_cost_tracker/report/data.rb +1 -1
- data/lib/llm_cost_tracker/retention.rb +1 -3
- data/lib/llm_cost_tracker/tracker.rb +17 -9
- data/lib/llm_cost_tracker/usage/catalog.rb +13 -4
- data/lib/llm_cost_tracker/usage/dimension.rb +1 -1
- data/lib/llm_cost_tracker/usage/dimensions.yml +61 -0
- data/lib/llm_cost_tracker/version.rb +1 -1
- data/lib/tasks/llm_cost_tracker.rake +24 -21
- data/llm_cost_tracker.gemspec +63 -0
- metadata +8 -7
|
@@ -16,13 +16,10 @@ module LlmCostTracker
|
|
|
16
16
|
@integration_name ||= name.demodulize.underscore.to_sym
|
|
17
17
|
end
|
|
18
18
|
|
|
19
|
-
def provider
|
|
20
|
-
@provider = value.to_s if value
|
|
21
|
-
@provider ||= integration_name.to_s
|
|
22
|
-
end
|
|
19
|
+
def provider = integration_name.to_s
|
|
23
20
|
|
|
24
21
|
def active?
|
|
25
|
-
LlmCostTracker.configuration.instrumented?(integration_name)
|
|
22
|
+
LlmCostTracker.configuration.enabled && LlmCostTracker.configuration.instrumented?(integration_name)
|
|
26
23
|
end
|
|
27
24
|
|
|
28
25
|
def install
|
|
@@ -30,7 +27,8 @@ module LlmCostTracker
|
|
|
30
27
|
Logging.warn(untested_version_message) if untested_version?
|
|
31
28
|
patch_targets.each do |target|
|
|
32
29
|
target_class = target.fetch(:constant_name).to_s.safe_constantize
|
|
33
|
-
|
|
30
|
+
patch = target.fetch(:patch)
|
|
31
|
+
target_class.prepend(patch) if target_class && !target_class.ancestors.include?(patch)
|
|
34
32
|
end
|
|
35
33
|
end
|
|
36
34
|
|
|
@@ -68,6 +66,40 @@ module LlmCostTracker
|
|
|
68
66
|
Logging.warn("#{integration_name} integration failed to record usage: #{e.class}: #{e.message}")
|
|
69
67
|
end
|
|
70
68
|
|
|
69
|
+
def record_passthrough(provider:,
|
|
70
|
+
model:,
|
|
71
|
+
response:,
|
|
72
|
+
latency_ms:,
|
|
73
|
+
service_line_items: [],
|
|
74
|
+
usage_source: LlmCostTracker::Usage::Source::SDK_RESPONSE,
|
|
75
|
+
pricing_mode: nil,
|
|
76
|
+
**token_attributes)
|
|
77
|
+
return unless active?
|
|
78
|
+
|
|
79
|
+
record_safely do
|
|
80
|
+
LlmCostTracker::Tracker.record(
|
|
81
|
+
event: LlmCostTracker::Event.build(
|
|
82
|
+
provider: provider,
|
|
83
|
+
model: model,
|
|
84
|
+
token_usage: LlmCostTracker::Usage::TokenUsage.build(**token_attributes),
|
|
85
|
+
usage_source: usage_source,
|
|
86
|
+
pricing_mode: pricing_mode,
|
|
87
|
+
provider_response_id: provider_response_id_for(response),
|
|
88
|
+
service_line_items: service_line_items
|
|
89
|
+
),
|
|
90
|
+
latency_ms: latency_ms
|
|
91
|
+
)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def record_once(event)
|
|
96
|
+
LlmCostTracker::Tracker.record(event: event.keyed_by_response_id)
|
|
97
|
+
rescue ActiveRecord::RecordNotUnique
|
|
98
|
+
nil
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def provider_response_id_for(response) = response&.try(:id)
|
|
102
|
+
|
|
71
103
|
def request_params(args, kwargs)
|
|
72
104
|
params =
|
|
73
105
|
case args.first
|
|
@@ -108,19 +140,14 @@ module LlmCostTracker
|
|
|
108
140
|
).wrap
|
|
109
141
|
end
|
|
110
142
|
|
|
111
|
-
def stream_collector(request
|
|
143
|
+
def stream_collector(request)
|
|
112
144
|
LlmCostTracker::Capture::StreamCollector.new(
|
|
113
145
|
provider: provider,
|
|
114
146
|
model: request[:model],
|
|
115
|
-
pricing_mode: stream_pricing_mode(request),
|
|
116
147
|
request: request
|
|
117
148
|
)
|
|
118
149
|
end
|
|
119
150
|
|
|
120
|
-
def stream_pricing_mode(_request)
|
|
121
|
-
nil
|
|
122
|
-
end
|
|
123
|
-
|
|
124
151
|
def minimum_version(value = nil)
|
|
125
152
|
@minimum_version = value if value
|
|
126
153
|
@minimum_version
|
|
@@ -196,12 +223,6 @@ module LlmCostTracker
|
|
|
196
223
|
"#{target.fetch(:constant_name)}##{method_name} is not available"
|
|
197
224
|
end
|
|
198
225
|
end
|
|
199
|
-
|
|
200
|
-
def install_patch(target, patch)
|
|
201
|
-
return if target&.ancestors&.include?(patch)
|
|
202
|
-
|
|
203
|
-
target.prepend(patch)
|
|
204
|
-
end
|
|
205
226
|
end
|
|
206
227
|
end
|
|
207
228
|
end
|
|
@@ -13,15 +13,15 @@ module LlmCostTracker
|
|
|
13
13
|
class << self
|
|
14
14
|
def maybe_capture(batch, resource:)
|
|
15
15
|
return unless Openai.active?
|
|
16
|
-
return unless batch.status.to_s
|
|
16
|
+
return unless %w[completed expired cancelled].include?(batch.status.to_s)
|
|
17
17
|
return unless batch.output_file_id && batch.id
|
|
18
18
|
return if captured?(batch.id)
|
|
19
19
|
|
|
20
20
|
client = resource.instance_variable_get(:@client)
|
|
21
21
|
host = Openai.client_host_for(resource)
|
|
22
22
|
Openai.record_safely do
|
|
23
|
-
|
|
24
|
-
deferred = capture_jsonl(
|
|
23
|
+
jsonl = client.files.content(batch.output_file_id).read
|
|
24
|
+
deferred = capture_jsonl(jsonl, host: host, model: batch.model)
|
|
25
25
|
mark_captured(batch.id)
|
|
26
26
|
raise deferred if deferred
|
|
27
27
|
end
|
|
@@ -41,19 +41,16 @@ module LlmCostTracker
|
|
|
41
41
|
end
|
|
42
42
|
end
|
|
43
43
|
|
|
44
|
-
def capture_jsonl(jsonl, host:)
|
|
44
|
+
def capture_jsonl(jsonl, host:, model:)
|
|
45
45
|
deferred = nil
|
|
46
46
|
jsonl.each_line do |line|
|
|
47
|
-
line = line.strip
|
|
48
|
-
next if line.empty?
|
|
49
|
-
|
|
50
47
|
entry = parse_line(line)
|
|
51
48
|
next unless entry
|
|
52
49
|
|
|
53
50
|
response = entry.dig("response", "body")
|
|
54
51
|
next unless response.is_a?(Hash) && response["usage"]
|
|
55
52
|
|
|
56
|
-
record_result(response, host: host)
|
|
53
|
+
record_result({ "id" => entry["id"] }.merge(response), host: host, model: model)
|
|
57
54
|
rescue LlmCostTracker::BudgetExceededError, LlmCostTracker::UnknownPricingError => e
|
|
58
55
|
deferred ||= e
|
|
59
56
|
end
|
|
@@ -66,19 +63,25 @@ module LlmCostTracker
|
|
|
66
63
|
nil
|
|
67
64
|
end
|
|
68
65
|
|
|
69
|
-
def record_result(response, host:)
|
|
66
|
+
def record_result(response, host:, model:)
|
|
70
67
|
provider = Openai.provider_for_host(host)
|
|
71
68
|
return if LlmCostTracker::Call.already_recorded?(provider: provider, provider_response_id: response["id"])
|
|
72
69
|
|
|
73
|
-
|
|
70
|
+
parser = LlmCostTracker::Providers::Openai::ResponseParser
|
|
71
|
+
event = parser.event_from_response(
|
|
74
72
|
response: response,
|
|
75
|
-
request: {},
|
|
73
|
+
request: { "model" => model },
|
|
76
74
|
provider: provider,
|
|
77
75
|
host: host,
|
|
78
76
|
usage_source: LlmCostTracker::Usage::Source::SDK_BATCH_RESULT,
|
|
79
|
-
|
|
77
|
+
# /v1/batches runs regional processing only on the us and eu hosts.
|
|
78
|
+
pricing_mode: parser.combined_pricing_mode(
|
|
79
|
+
host: (host if host.to_s.match?(/\A(?:us|eu)\./i)),
|
|
80
|
+
model: response["model"] || model,
|
|
81
|
+
service_tier: "batch"
|
|
82
|
+
)
|
|
80
83
|
)
|
|
81
|
-
|
|
84
|
+
Openai.record_once(event)
|
|
82
85
|
end
|
|
83
86
|
end
|
|
84
87
|
end
|
|
@@ -45,6 +45,14 @@ module LlmCostTracker
|
|
|
45
45
|
include PatchBuilder.build(record_method: :record_response, methods: %i[create])
|
|
46
46
|
include PatchBuilder.build_stream(methods: %i[stream stream_raw])
|
|
47
47
|
|
|
48
|
+
def retrieve(response_id, *args, **kwargs)
|
|
49
|
+
response = super
|
|
50
|
+
LlmCostTracker::Integrations::Openai.record_retrieved_response(
|
|
51
|
+
response, host: LlmCostTracker::Integrations::Openai.client_host_for(self)
|
|
52
|
+
)
|
|
53
|
+
response
|
|
54
|
+
end
|
|
55
|
+
|
|
48
56
|
def retrieve_streaming(response_id, *args, **kwargs)
|
|
49
57
|
LlmCostTracker::Integrations::Openai.wrap_stream(
|
|
50
58
|
args, kwargs, **LlmCostTracker::Integrations::Openai.stream_seam(self)
|
|
@@ -18,19 +18,12 @@ module LlmCostTracker
|
|
|
18
18
|
minimum_version "0.59.0"
|
|
19
19
|
|
|
20
20
|
class << self
|
|
21
|
-
def stream_pricing_mode(request, host: nil)
|
|
22
|
-
LlmCostTracker::Providers::Openai::ResponseParser.combined_pricing_mode(
|
|
23
|
-
host: host,
|
|
24
|
-
model: (request || {})[:model],
|
|
25
|
-
service_tier: (request || {})[:service_tier]
|
|
26
|
-
)
|
|
27
|
-
end
|
|
28
|
-
|
|
29
21
|
def stream_collector(request, host: nil)
|
|
30
22
|
LlmCostTracker::Capture::StreamCollector.new(
|
|
31
23
|
provider: provider_for_host(host),
|
|
32
24
|
model: request[:model],
|
|
33
|
-
|
|
25
|
+
# Host part only: the parser prefers the served tier over the requested one, e.g. after a downgrade.
|
|
26
|
+
pricing_mode: host_pricing_mode(host, request),
|
|
34
27
|
request: request
|
|
35
28
|
)
|
|
36
29
|
end
|
|
@@ -53,29 +46,25 @@ module LlmCostTracker
|
|
|
53
46
|
end
|
|
54
47
|
|
|
55
48
|
def provider_for_host(host)
|
|
56
|
-
LlmCostTracker::Providers::Azure::Hosts.openai?(host)
|
|
49
|
+
return "azure_openai" if LlmCostTracker::Providers::Azure::Hosts.openai?(host)
|
|
50
|
+
|
|
51
|
+
LlmCostTracker.configuration.capture.openai_compatible_providers[host.to_s.downcase] || "openai"
|
|
57
52
|
end
|
|
58
53
|
|
|
59
54
|
def patch_targets
|
|
60
55
|
[
|
|
61
56
|
patch_target("OpenAI::Resources::Responses", with: ResponsesPatch),
|
|
62
57
|
patch_target("OpenAI::Resources::Chat::Completions", with: ChatCompletionsPatch),
|
|
63
|
-
*auxiliary_patch_targets
|
|
64
|
-
]
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
def auxiliary_patch_targets
|
|
68
|
-
[
|
|
69
58
|
patch_target("OpenAI::Resources::Embeddings", with: EmbeddingsPatch, optional: true),
|
|
70
59
|
patch_target("OpenAI::Resources::Images", with: ImagesPatch, optional: true),
|
|
71
60
|
patch_target("OpenAI::Resources::Images",
|
|
72
61
|
with: StreamingImagesPatch,
|
|
73
|
-
|
|
62
|
+
optional: true,
|
|
74
63
|
skip_when_methods_missing: true),
|
|
75
64
|
patch_target("OpenAI::Resources::Audio::Transcriptions", with: TranscriptionsPatch, optional: true),
|
|
76
65
|
patch_target("OpenAI::Resources::Audio::Transcriptions",
|
|
77
66
|
with: StreamingTranscriptionsPatch,
|
|
78
|
-
|
|
67
|
+
optional: true,
|
|
79
68
|
skip_when_methods_missing: true),
|
|
80
69
|
patch_target("OpenAI::Resources::Audio::Translations", with: TranslationsPatch, optional: true),
|
|
81
70
|
patch_target("OpenAI::Resources::Audio::Speech", with: SpeechPatch, optional: true),
|
|
@@ -89,11 +78,14 @@ module LlmCostTracker
|
|
|
89
78
|
|
|
90
79
|
record_safely do
|
|
91
80
|
normalized = LlmCostTracker::Capture::SdkPayload.normalize(response)
|
|
92
|
-
usage = normalized["usage"]
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
81
|
+
usage = normalized["usage"] || {}
|
|
82
|
+
input_tokens = usage["input_tokens"] || usage["prompt_tokens"]
|
|
83
|
+
output_tokens = usage["output_tokens"] || usage["completion_tokens"]
|
|
84
|
+
if input_tokens.nil? && output_tokens.nil?
|
|
85
|
+
unless normalized["background"]
|
|
86
|
+
Logging.warn("OpenAI response #{normalized['id']} has no usage; not recorded")
|
|
87
|
+
end
|
|
88
|
+
next
|
|
97
89
|
end
|
|
98
90
|
|
|
99
91
|
event = LlmCostTracker::Providers::Openai::ResponseParser.event_from_response(
|
|
@@ -103,7 +95,21 @@ module LlmCostTracker
|
|
|
103
95
|
host: host,
|
|
104
96
|
usage_source: LlmCostTracker::Usage::Source::SDK_RESPONSE
|
|
105
97
|
)
|
|
106
|
-
LlmCostTracker::Tracker.record(event: event, latency_ms: latency_ms)
|
|
98
|
+
LlmCostTracker::Tracker.record(event: event, latency_ms: latency_ms)
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def record_retrieved_response(response, host:)
|
|
103
|
+
return unless active?
|
|
104
|
+
|
|
105
|
+
record_safely do
|
|
106
|
+
event = LlmCostTracker::Providers::Openai::ResponseParser.retrieved_event(
|
|
107
|
+
response: LlmCostTracker::Capture::SdkPayload.normalize(response),
|
|
108
|
+
provider: provider_for_host(host),
|
|
109
|
+
host: host,
|
|
110
|
+
usage_source: LlmCostTracker::Usage::Source::SDK_RESPONSE
|
|
111
|
+
)
|
|
112
|
+
record_once(event) if event
|
|
107
113
|
end
|
|
108
114
|
end
|
|
109
115
|
|
|
@@ -123,7 +129,8 @@ module LlmCostTracker
|
|
|
123
129
|
model: request[:model],
|
|
124
130
|
response: response,
|
|
125
131
|
latency_ms: latency_ms,
|
|
126
|
-
|
|
132
|
+
provider: provider_for_host(host),
|
|
133
|
+
pricing_mode: host_pricing_mode(host, request),
|
|
127
134
|
input_tokens: [raw_input - image_input - cache_read, 0].max,
|
|
128
135
|
image_input_tokens: image_input,
|
|
129
136
|
output_tokens: text_output,
|
|
@@ -138,8 +145,10 @@ module LlmCostTracker
|
|
|
138
145
|
model: request[:model],
|
|
139
146
|
response: response,
|
|
140
147
|
latency_ms: latency_ms,
|
|
141
|
-
|
|
148
|
+
provider: provider_for_host(host),
|
|
149
|
+
pricing_mode: host_pricing_mode(host, request),
|
|
142
150
|
service_line_items: LlmCostTracker::Providers::Openai::ServiceCharges.transcription_line_items(usage),
|
|
151
|
+
usage_source: usage ? LlmCostTracker::Usage::Source::SDK_RESPONSE : LlmCostTracker::Usage::Source::UNKNOWN,
|
|
143
152
|
**transcription_token_attributes(usage)
|
|
144
153
|
)
|
|
145
154
|
end
|
|
@@ -161,54 +170,28 @@ module LlmCostTracker
|
|
|
161
170
|
model: request[:model],
|
|
162
171
|
response: nil,
|
|
163
172
|
latency_ms: latency_ms,
|
|
164
|
-
|
|
173
|
+
provider: provider_for_host(host),
|
|
165
174
|
input_tokens: 0,
|
|
166
175
|
output_tokens: 0,
|
|
167
|
-
service_line_items: speech_line_items(request)
|
|
176
|
+
service_line_items: LlmCostTracker::Providers::Openai::ServiceCharges.speech_line_items(request)
|
|
168
177
|
)
|
|
169
178
|
end
|
|
170
179
|
|
|
171
|
-
def speech_line_items(request)
|
|
172
|
-
input = request[:input]
|
|
173
|
-
return [] unless input.is_a?(String)
|
|
174
|
-
return [] unless LlmCostTracker::Providers::Openai::ModelFamilies.character_billed_tts?(request[:model])
|
|
175
|
-
|
|
176
|
-
[LlmCostTracker::Charges::LineItem.build(
|
|
177
|
-
dimension_key: "text_to_speech_character",
|
|
178
|
-
quantity: input.length,
|
|
179
|
-
cost_status: LlmCostTracker::Charges::CostStatus::UNKNOWN,
|
|
180
|
-
pricing_basis: "provider_usage",
|
|
181
|
-
provider_field: "request.input"
|
|
182
|
-
)]
|
|
183
|
-
end
|
|
184
|
-
|
|
185
180
|
def record_moderation(response, request:, latency_ms:, host: nil)
|
|
186
181
|
record_passthrough(
|
|
187
182
|
model: response.model || request[:model],
|
|
188
183
|
response: response,
|
|
189
184
|
latency_ms: latency_ms,
|
|
190
|
-
|
|
185
|
+
provider: provider_for_host(host),
|
|
191
186
|
input_tokens: 0,
|
|
192
187
|
output_tokens: 0
|
|
193
188
|
)
|
|
194
189
|
end
|
|
195
190
|
|
|
196
|
-
def
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
LlmCostTracker::Tracker.record(
|
|
201
|
-
event: Event.build(
|
|
202
|
-
provider: provider_for_host(host),
|
|
203
|
-
model: model,
|
|
204
|
-
token_usage: Usage::TokenUsage.build(**token_attributes),
|
|
205
|
-
usage_source: LlmCostTracker::Usage::Source::SDK_RESPONSE,
|
|
206
|
-
provider_response_id: response&.try(:id),
|
|
207
|
-
service_line_items: service_line_items
|
|
208
|
-
),
|
|
209
|
-
latency_ms: latency_ms
|
|
210
|
-
)
|
|
211
|
-
end
|
|
191
|
+
def host_pricing_mode(host, request)
|
|
192
|
+
LlmCostTracker::Providers::Openai::ResponseParser.combined_pricing_mode(
|
|
193
|
+
provider: provider_for_host(host), host: host, model: request[:model], service_tier: nil
|
|
194
|
+
)
|
|
212
195
|
end
|
|
213
196
|
|
|
214
197
|
def usage_hash_from(response)
|