llm_cost_tracker 0.8.0 → 0.10.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/CHANGELOG.md +136 -0
- data/README.md +14 -6
- data/app/assets/llm_cost_tracker/application.css +65 -5
- data/app/controllers/llm_cost_tracker/application_controller.rb +25 -33
- data/app/controllers/llm_cost_tracker/assets_controller.rb +1 -1
- data/app/controllers/llm_cost_tracker/calls_controller.rb +21 -11
- data/app/controllers/llm_cost_tracker/data_quality_controller.rb +4 -0
- data/app/controllers/llm_cost_tracker/reconciliation_controller.rb +106 -0
- data/app/controllers/llm_cost_tracker/tags_controller.rb +15 -1
- data/app/helpers/llm_cost_tracker/application_helper.rb +11 -1
- data/app/helpers/llm_cost_tracker/inline_style_helper.rb +28 -0
- data/app/helpers/llm_cost_tracker/reconciliation_helper.rb +13 -0
- data/app/helpers/llm_cost_tracker/token_usage_helper.rb +5 -1
- data/app/models/llm_cost_tracker/call.rb +0 -3
- data/app/models/llm_cost_tracker/call_line_item.rb +1 -5
- data/app/models/llm_cost_tracker/call_rollup.rb +0 -3
- data/app/models/llm_cost_tracker/call_tag.rb +0 -4
- data/app/models/llm_cost_tracker/ingestion/inbox_entry.rb +0 -4
- data/app/models/llm_cost_tracker/ingestion/lease.rb +0 -3
- data/app/models/llm_cost_tracker/provider_invoice.rb +7 -3
- data/app/models/llm_cost_tracker/provider_invoice_import.rb +29 -0
- data/app/services/llm_cost_tracker/dashboard/data_quality.rb +33 -4
- data/app/services/llm_cost_tracker/dashboard/filter.rb +6 -4
- data/app/services/llm_cost_tracker/dashboard/setup_state.rb +110 -0
- data/app/views/layouts/llm_cost_tracker/application.html.erb +6 -1
- data/app/views/llm_cost_tracker/calls/show.html.erb +26 -41
- data/app/views/llm_cost_tracker/dashboard/index.html.erb +9 -9
- data/app/views/llm_cost_tracker/data_quality/index.html.erb +92 -53
- data/app/views/llm_cost_tracker/reconciliation/index.html.erb +183 -0
- data/app/views/llm_cost_tracker/shared/_bar.html.erb +1 -1
- data/app/views/llm_cost_tracker/shared/_filters.html.erb +3 -0
- data/app/views/llm_cost_tracker/shared/_metric_stack.html.erb +1 -1
- data/app/views/llm_cost_tracker/tags/show.html.erb +60 -0
- data/config/routes.rb +3 -2
- data/lib/llm_cost_tracker/billing/components.rb +45 -3
- data/lib/llm_cost_tracker/billing/components.yml +71 -0
- data/lib/llm_cost_tracker/billing/cost_status.rb +21 -25
- data/lib/llm_cost_tracker/billing/line_item.rb +16 -50
- data/lib/llm_cost_tracker/budget.rb +31 -7
- data/lib/llm_cost_tracker/capture/stream_collector.rb +113 -34
- data/lib/llm_cost_tracker/capture/stream_tracker.rb +40 -5
- data/lib/llm_cost_tracker/configuration.rb +72 -17
- data/lib/llm_cost_tracker/doctor/capture_verifier.rb +1 -1
- data/lib/llm_cost_tracker/doctor/cost_drift_check.rb +2 -0
- data/lib/llm_cost_tracker/doctor/ingestion_check.rb +30 -4
- data/lib/llm_cost_tracker/doctor/invoice_reconciliation_check.rb +164 -0
- data/lib/llm_cost_tracker/doctor/legacy_audit_check.rb +0 -2
- data/lib/llm_cost_tracker/doctor/legacy_billing_status_check.rb +0 -2
- data/lib/llm_cost_tracker/doctor/schema_check.rb +5 -2
- data/lib/llm_cost_tracker/doctor.rb +72 -14
- data/lib/llm_cost_tracker/engine.rb +8 -0
- data/lib/llm_cost_tracker/errors.rb +3 -2
- data/lib/llm_cost_tracker/event.rb +48 -1
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/async_ingestion_generator.rb +43 -0
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/call_rollups_generator.rb +43 -0
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/install_generator.rb +17 -26
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/reconciliation_generator.rb +34 -0
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/create_llm_cost_tracker_async_ingestion.rb.erb +29 -0
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/create_llm_cost_tracker_call_rollups.rb.erb +15 -0
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/create_llm_cost_tracker_calls.rb.erb +5 -58
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/create_llm_cost_tracker_reconciliation.rb.erb +60 -0
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/initializer.rb.erb +35 -25
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_call_rollups_provider.rb.erb +35 -0
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_call_tags_key_value_index.rb.erb +32 -0
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_image_tokens.rb.erb +18 -0
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_provider_invoice_imports_provider.rb.erb +32 -0
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_provider_invoices_metadata_index.rb.erb +25 -0
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/upgrade_call_rollups_provider_generator.rb +29 -0
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/upgrade_call_tags_key_value_index_generator.rb +30 -0
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/upgrade_image_tokens_generator.rb +29 -0
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/upgrade_provider_invoice_imports_provider_generator.rb +31 -0
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/upgrade_provider_invoices_metadata_index_generator.rb +31 -0
- data/lib/llm_cost_tracker/ingestion/batch.rb +5 -2
- data/lib/llm_cost_tracker/ingestion/inbox.rb +3 -25
- data/lib/llm_cost_tracker/ingestion/pool.rb +44 -0
- data/lib/llm_cost_tracker/ingestion/worker.rb +28 -34
- data/lib/llm_cost_tracker/ingestion.rb +48 -11
- data/lib/llm_cost_tracker/integrations/anthropic.rb +31 -26
- data/lib/llm_cost_tracker/integrations/base.rb +35 -15
- data/lib/llm_cost_tracker/integrations/openai.rb +345 -84
- data/lib/llm_cost_tracker/integrations/ruby_llm.rb +111 -14
- data/lib/llm_cost_tracker/integrations.rb +33 -14
- data/lib/llm_cost_tracker/ledger/period/totals.rb +25 -7
- data/lib/llm_cost_tracker/ledger/rollups.rb +22 -17
- data/lib/llm_cost_tracker/ledger/schema/call_line_items.rb +41 -1
- data/lib/llm_cost_tracker/ledger/schema/call_rollups.rb +16 -6
- data/lib/llm_cost_tracker/ledger/schema/call_tags.rb +28 -2
- data/lib/llm_cost_tracker/ledger/schema/calls.rb +2 -4
- data/lib/llm_cost_tracker/ledger/schema/ingestion_inbox_entries.rb +57 -0
- data/lib/llm_cost_tracker/ledger/schema/ingestion_leases.rb +52 -0
- data/lib/llm_cost_tracker/ledger/schema/provider_invoice_imports.rb +56 -0
- data/lib/llm_cost_tracker/ledger/schema/provider_invoices.rb +28 -13
- data/lib/llm_cost_tracker/ledger/store.rb +34 -31
- data/lib/llm_cost_tracker/ledger/tags/encoding.rb +37 -0
- data/lib/llm_cost_tracker/ledger/tags/query.rb +2 -2
- data/lib/llm_cost_tracker/ledger.rb +2 -1
- data/lib/llm_cost_tracker/logging.rb +0 -4
- data/lib/llm_cost_tracker/masking.rb +39 -0
- data/lib/llm_cost_tracker/middleware/faraday.rb +120 -33
- data/lib/llm_cost_tracker/parsers/anthropic.rb +36 -28
- data/lib/llm_cost_tracker/parsers/azure.rb +46 -0
- data/lib/llm_cost_tracker/parsers/base.rb +53 -43
- data/lib/llm_cost_tracker/parsers/gemini.rb +24 -22
- data/lib/llm_cost_tracker/parsers/openai.rb +20 -38
- data/lib/llm_cost_tracker/parsers/openai_compatible.rb +26 -39
- data/lib/llm_cost_tracker/parsers/openai_service_charges.rb +81 -13
- data/lib/llm_cost_tracker/parsers/openai_usage.rb +126 -59
- data/lib/llm_cost_tracker/parsers.rb +31 -4
- data/lib/llm_cost_tracker/prices.json +572 -493
- data/lib/llm_cost_tracker/pricing/backfill.rb +140 -0
- data/lib/llm_cost_tracker/pricing/effective_prices.rb +7 -40
- data/lib/llm_cost_tracker/pricing/estimator.rb +33 -0
- data/lib/llm_cost_tracker/pricing/explainer.rb +4 -1
- data/lib/llm_cost_tracker/pricing/lookup.rb +73 -5
- data/lib/llm_cost_tracker/pricing/mode.rb +76 -0
- data/lib/llm_cost_tracker/pricing/registry.rb +3 -8
- data/lib/llm_cost_tracker/pricing/service_charges.rb +14 -12
- data/lib/llm_cost_tracker/pricing/{sync_change_printer.rb → sync/change_printer.rb} +3 -3
- data/lib/llm_cost_tracker/pricing/sync/registry_writer.rb +62 -1
- data/lib/llm_cost_tracker/pricing/sync.rb +4 -10
- data/lib/llm_cost_tracker/pricing/unknown.rb +5 -2
- data/lib/llm_cost_tracker/pricing.rb +117 -44
- data/lib/llm_cost_tracker/providers/anthropic/tier_classification.rb +22 -0
- data/lib/llm_cost_tracker/providers/azure/hosts.rb +17 -0
- data/lib/llm_cost_tracker/providers/gemini/model_families.rb +17 -0
- data/lib/llm_cost_tracker/providers/openai/hosts.rb +35 -0
- data/lib/llm_cost_tracker/providers/openai/model_families.rb +51 -0
- data/lib/llm_cost_tracker/railtie.rb +8 -0
- data/lib/llm_cost_tracker/reconcile_tasks.rb +134 -0
- data/lib/llm_cost_tracker/reconciliation/diff.rb +409 -0
- data/lib/llm_cost_tracker/reconciliation/diff_result.rb +44 -0
- data/lib/llm_cost_tracker/reconciliation/import_result.rb +19 -0
- data/lib/llm_cost_tracker/reconciliation/importer.rb +254 -0
- data/lib/llm_cost_tracker/reconciliation/sources/anthropic_usage.rb +172 -0
- data/lib/llm_cost_tracker/reconciliation/sources/fingerprint.rb +20 -0
- data/lib/llm_cost_tracker/reconciliation/sources/openai_usage.rb +142 -0
- data/lib/llm_cost_tracker/reconciliation.rb +118 -0
- data/lib/llm_cost_tracker/report/data.rb +4 -1
- data/lib/llm_cost_tracker/report.rb +0 -4
- data/lib/llm_cost_tracker/retention.rb +31 -6
- data/lib/llm_cost_tracker/tags/context.rb +3 -4
- data/lib/llm_cost_tracker/tags/sanitizer.rb +73 -21
- data/lib/llm_cost_tracker/token_usage.rb +14 -2
- data/lib/llm_cost_tracker/tracker.rb +41 -55
- data/lib/llm_cost_tracker/version.rb +1 -1
- data/lib/llm_cost_tracker.rb +19 -14
- data/lib/tasks/llm_cost_tracker.rake +41 -4
- metadata +49 -3
- data/lib/llm_cost_tracker/usage_capture.rb +0 -58
|
@@ -1,18 +1,21 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "base"
|
|
4
|
+
require_relative "../providers/anthropic/tier_classification"
|
|
4
5
|
|
|
5
6
|
module LlmCostTracker
|
|
6
7
|
module Parsers
|
|
7
8
|
class Anthropic < Base
|
|
8
9
|
HOSTS = %w[api.anthropic.com].freeze
|
|
9
10
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
class << self
|
|
12
|
+
def match?(url)
|
|
13
|
+
match_uri?(url, hosts: HOSTS, path_includes: "/v1/messages")
|
|
14
|
+
end
|
|
13
15
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
+
def provider_names
|
|
17
|
+
%w[anthropic]
|
|
18
|
+
end
|
|
16
19
|
end
|
|
17
20
|
|
|
18
21
|
def parse(request_body:, response_status:, response_body:, **)
|
|
@@ -25,10 +28,10 @@ module LlmCostTracker
|
|
|
25
28
|
request = safe_json_parse(request_body)
|
|
26
29
|
cache_read = usage["cache_read_input_tokens"].to_i
|
|
27
30
|
|
|
28
|
-
|
|
31
|
+
Event.build(
|
|
29
32
|
provider: "anthropic",
|
|
30
33
|
provider_response_id: response["id"],
|
|
31
|
-
pricing_mode: pricing_mode(request: request,
|
|
34
|
+
pricing_mode: pricing_mode(request: request, usage: usage),
|
|
32
35
|
model: response["model"] || request["model"],
|
|
33
36
|
token_usage: token_usage(usage: usage, cache_read: cache_read),
|
|
34
37
|
usage_source: :response,
|
|
@@ -49,39 +52,43 @@ module LlmCostTracker
|
|
|
49
52
|
model: model,
|
|
50
53
|
usage: usage,
|
|
51
54
|
response_id: response_id,
|
|
52
|
-
pricing_mode: pricing_mode(request: request,
|
|
55
|
+
pricing_mode: pricing_mode(request: request, usage: usage)
|
|
53
56
|
)
|
|
54
57
|
else
|
|
55
58
|
build_unknown_stream_usage(
|
|
56
59
|
provider: "anthropic",
|
|
57
60
|
model: model,
|
|
58
61
|
provider_response_id: response_id,
|
|
59
|
-
pricing_mode: pricing_mode(request: request,
|
|
62
|
+
pricing_mode: pricing_mode(request: request, usage: usage)
|
|
60
63
|
)
|
|
61
64
|
end
|
|
62
65
|
end
|
|
63
66
|
|
|
67
|
+
def provider_for(_request_url)
|
|
68
|
+
"anthropic"
|
|
69
|
+
end
|
|
70
|
+
|
|
64
71
|
private
|
|
65
72
|
|
|
66
73
|
def stream_usage(events)
|
|
67
|
-
start_usage = find_event_value(events, reverse: true) do |data|
|
|
68
|
-
data.dig("message", "usage") if data["type"] == "message_start"
|
|
69
|
-
end
|
|
70
74
|
latest_delta = find_event_value(events, reverse: true) do |data|
|
|
71
75
|
data["usage"] if data["type"] == "message_delta" && data["usage"].is_a?(Hash)
|
|
72
76
|
end
|
|
77
|
+
return nil unless latest_delta
|
|
73
78
|
|
|
74
|
-
|
|
79
|
+
start_usage = find_event_value(events, reverse: true) do |data|
|
|
80
|
+
data.dig("message", "usage") if data["type"] == "message_start"
|
|
81
|
+
end
|
|
75
82
|
|
|
76
|
-
(start_usage || {}).merge(latest_delta
|
|
77
|
-
delta_val
|
|
83
|
+
(start_usage || {}).merge(latest_delta) do |_key, start_val, delta_val|
|
|
84
|
+
delta_val || start_val
|
|
78
85
|
end
|
|
79
86
|
end
|
|
80
87
|
|
|
81
88
|
def build_stream_result(model:, usage:, response_id:, pricing_mode:)
|
|
82
89
|
cache_read = usage["cache_read_input_tokens"].to_i
|
|
83
90
|
|
|
84
|
-
|
|
91
|
+
Event.build(
|
|
85
92
|
provider: "anthropic",
|
|
86
93
|
provider_response_id: response_id,
|
|
87
94
|
pricing_mode: pricing_mode,
|
|
@@ -103,6 +110,11 @@ module LlmCostTracker
|
|
|
103
110
|
quantity: server_tool_use["web_search_requests"],
|
|
104
111
|
provider_field: "usage.server_tool_use.web_search_requests"
|
|
105
112
|
),
|
|
113
|
+
service_line_item(
|
|
114
|
+
component_key: :web_fetch_request,
|
|
115
|
+
quantity: server_tool_use["web_fetch_requests"],
|
|
116
|
+
provider_field: "usage.server_tool_use.web_fetch_requests"
|
|
117
|
+
),
|
|
106
118
|
service_line_item(
|
|
107
119
|
component_key: :code_execution_request,
|
|
108
120
|
quantity: server_tool_use["code_execution_requests"],
|
|
@@ -158,27 +170,23 @@ module LlmCostTracker
|
|
|
158
170
|
Logging.warn("Anthropic usage.cache_creation has unexpected shape: #{cache_creation.class}")
|
|
159
171
|
end
|
|
160
172
|
|
|
161
|
-
def pricing_mode(request:,
|
|
173
|
+
def pricing_mode(request:, usage:)
|
|
162
174
|
modes = []
|
|
163
|
-
speed = usage&.fetch("speed", nil) ||
|
|
164
|
-
service_tier = usage&.fetch("service_tier", nil) ||
|
|
165
|
-
|
|
166
|
-
request["service_tier"]
|
|
175
|
+
speed = usage&.fetch("speed", nil) || request["speed"]
|
|
176
|
+
service_tier = usage&.fetch("service_tier", nil) || request["service_tier"]
|
|
177
|
+
service_tier = nil if Providers::Anthropic::TierClassification.standard_equivalent_tier?(service_tier)
|
|
167
178
|
|
|
168
179
|
modes << Pricing.normalize_mode(speed)
|
|
169
180
|
modes << Pricing.normalize_mode(service_tier)
|
|
170
|
-
|
|
181
|
+
geo = inference_geo(request: request, usage: usage).downcase
|
|
182
|
+
modes << "data_residency" if Providers::Anthropic::TierClassification.data_residency_geo?(geo)
|
|
171
183
|
|
|
172
184
|
modes = modes.compact.uniq
|
|
173
185
|
modes.empty? ? nil : modes.join("_")
|
|
174
186
|
end
|
|
175
187
|
|
|
176
|
-
def inference_geo(request:,
|
|
177
|
-
(
|
|
178
|
-
usage&.fetch("inference_geo", nil) ||
|
|
179
|
-
response&.fetch("inference_geo", nil) ||
|
|
180
|
-
request["inference_geo"]
|
|
181
|
-
).to_s
|
|
188
|
+
def inference_geo(request:, usage:)
|
|
189
|
+
(usage&.fetch("inference_geo", nil) || request["inference_geo"]).to_s
|
|
182
190
|
end
|
|
183
191
|
end
|
|
184
192
|
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base"
|
|
4
|
+
require_relative "openai_usage"
|
|
5
|
+
require_relative "../providers/azure/hosts"
|
|
6
|
+
|
|
7
|
+
module LlmCostTracker
|
|
8
|
+
module Parsers
|
|
9
|
+
class Azure < Base
|
|
10
|
+
include OpenaiUsage
|
|
11
|
+
|
|
12
|
+
TRACKED_ENDPOINTS = %w[
|
|
13
|
+
chat/completions completions embeddings moderations responses
|
|
14
|
+
audio/transcriptions audio/translations audio/speech
|
|
15
|
+
images/generations images/edits images/variations
|
|
16
|
+
].freeze
|
|
17
|
+
|
|
18
|
+
PATH_PATTERN = %r{\A/openai/(?:deployments/[^/]+|v1)/(?:#{TRACKED_ENDPOINTS.join('|')})\z}
|
|
19
|
+
|
|
20
|
+
class << self
|
|
21
|
+
def match?(url)
|
|
22
|
+
uri_matches?(url) do |uri|
|
|
23
|
+
LlmCostTracker::Providers::Azure::Hosts.openai?(uri.host) && uri.path.to_s.match?(PATH_PATTERN)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def provider_names
|
|
28
|
+
%w[azure_openai]
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def provider_for(_request_url)
|
|
33
|
+
"azure_openai"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def model_for(request_url, request_parsed)
|
|
37
|
+
body_model = super
|
|
38
|
+
return body_model if body_model
|
|
39
|
+
|
|
40
|
+
uri = parsed_uri(request_url)
|
|
41
|
+
match = uri&.path&.match(%r{/openai/deployments/([^/]+)/})
|
|
42
|
+
match && match[1]
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -6,47 +6,7 @@ require "uri"
|
|
|
6
6
|
|
|
7
7
|
module LlmCostTracker
|
|
8
8
|
module Parsers
|
|
9
|
-
|
|
10
|
-
def parse(**)
|
|
11
|
-
raise NotImplementedError
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
def provider_names
|
|
15
|
-
[]
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
def match?(url)
|
|
19
|
-
raise NotImplementedError
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
def streaming_request?(_request_url, request_body)
|
|
23
|
-
return false if request_body.blank?
|
|
24
|
-
|
|
25
|
-
body = request_body.to_s
|
|
26
|
-
|
|
27
|
-
request = safe_json_parse(body)
|
|
28
|
-
request.is_a?(Hash) && request["stream"] == true
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
def parse_stream(**)
|
|
32
|
-
nil
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
private
|
|
36
|
-
|
|
37
|
-
def safe_json_parse(body)
|
|
38
|
-
return {} if body.blank?
|
|
39
|
-
|
|
40
|
-
JSON.parse(body)
|
|
41
|
-
rescue JSON::ParserError
|
|
42
|
-
{}
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
def uri_matches?(url)
|
|
46
|
-
uri = parsed_uri(url)
|
|
47
|
-
uri ? yield(uri) : false
|
|
48
|
-
end
|
|
49
|
-
|
|
9
|
+
module UrlMatchers
|
|
50
10
|
def match_uri?(url, hosts: nil, exact_paths: nil, path_includes: nil, path_suffixes: nil, path_pattern: nil)
|
|
51
11
|
uri_matches?(url) do |uri|
|
|
52
12
|
host_match = hosts.nil? || hosts.include?(uri.host.to_s.downcase)
|
|
@@ -66,6 +26,11 @@ module LlmCostTracker
|
|
|
66
26
|
end
|
|
67
27
|
end
|
|
68
28
|
|
|
29
|
+
def uri_matches?(url)
|
|
30
|
+
uri = parsed_uri(url)
|
|
31
|
+
uri ? yield(uri) : false
|
|
32
|
+
end
|
|
33
|
+
|
|
69
34
|
def parsed_uri(url)
|
|
70
35
|
URI.parse(url.to_s)
|
|
71
36
|
rescue URI::InvalidURIError
|
|
@@ -84,6 +49,51 @@ module LlmCostTracker
|
|
|
84
49
|
|
|
85
50
|
matches
|
|
86
51
|
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
class Base
|
|
55
|
+
extend UrlMatchers
|
|
56
|
+
include UrlMatchers
|
|
57
|
+
|
|
58
|
+
class << self
|
|
59
|
+
def match?(_url)
|
|
60
|
+
raise NotImplementedError
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def provider_names
|
|
64
|
+
[]
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def parse(**)
|
|
69
|
+
raise NotImplementedError
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def streaming_request?(_request_url, request_parsed)
|
|
73
|
+
request_parsed.is_a?(Hash) && request_parsed["stream"] == true
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def model_for(_request_url, request_parsed)
|
|
77
|
+
request_parsed["model"] if request_parsed.is_a?(Hash)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def parse_stream(**)
|
|
81
|
+
nil
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def auto_enable_stream_usage?(_request_url)
|
|
85
|
+
false
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def safe_json_parse(body)
|
|
89
|
+
return {} if body.blank?
|
|
90
|
+
|
|
91
|
+
JSON.parse(body)
|
|
92
|
+
rescue JSON::ParserError
|
|
93
|
+
{}
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
private
|
|
87
97
|
|
|
88
98
|
def each_event_data(events, reverse: false)
|
|
89
99
|
enumerator = reverse ? events.reverse_each : events.each
|
|
@@ -105,11 +115,11 @@ module LlmCostTracker
|
|
|
105
115
|
|
|
106
116
|
def build_unknown_stream_usage(provider:, model:, provider_response_id:, pricing_mode: nil,
|
|
107
117
|
service_line_items: nil)
|
|
108
|
-
|
|
118
|
+
Event.build(
|
|
109
119
|
provider: provider,
|
|
110
120
|
provider_response_id: provider_response_id,
|
|
111
121
|
pricing_mode: pricing_mode,
|
|
112
|
-
model: model ||
|
|
122
|
+
model: model || Event::UNKNOWN_MODEL,
|
|
113
123
|
token_usage: TokenUsage.build(input_tokens: 0, output_tokens: 0, total_tokens: 0),
|
|
114
124
|
stream: true,
|
|
115
125
|
usage_source: :unknown,
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "../billing/line_item"
|
|
4
4
|
require_relative "base"
|
|
5
|
+
require_relative "../providers/gemini/model_families"
|
|
5
6
|
|
|
6
7
|
module LlmCostTracker
|
|
7
8
|
module Parsers
|
|
@@ -9,17 +10,18 @@ module LlmCostTracker
|
|
|
9
10
|
HOSTS = %w[generativelanguage.googleapis.com].freeze
|
|
10
11
|
TRACKED_PATH_PATTERN = %r{/models/[^/:]+:(?:generateContent|streamGenerateContent)\z}
|
|
11
12
|
STREAM_PATH_PATTERN = /:streamGenerateContent\z/
|
|
12
|
-
PER_QUERY_GROUNDING_MODEL_PATTERN = /\bgemini-(?:[3-9]|[1-9]\d)\b/i
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
class << self
|
|
15
|
+
def match?(url)
|
|
16
|
+
match_uri?(url, hosts: HOSTS, path_pattern: TRACKED_PATH_PATTERN)
|
|
17
|
+
end
|
|
17
18
|
|
|
18
|
-
|
|
19
|
-
|
|
19
|
+
def provider_names
|
|
20
|
+
%w[gemini]
|
|
21
|
+
end
|
|
20
22
|
end
|
|
21
23
|
|
|
22
|
-
def streaming_request?(request_url,
|
|
24
|
+
def streaming_request?(request_url, request_parsed)
|
|
23
25
|
return true if match_uri?(request_url, path_pattern: STREAM_PATH_PATTERN)
|
|
24
26
|
|
|
25
27
|
super
|
|
@@ -34,13 +36,13 @@ module LlmCostTracker
|
|
|
34
36
|
|
|
35
37
|
request = safe_json_parse(request_body)
|
|
36
38
|
model = extract_model_from_url(request_url)
|
|
37
|
-
|
|
39
|
+
build_event(
|
|
38
40
|
request_url: request_url,
|
|
39
41
|
usage: usage,
|
|
40
42
|
usage_source: :response,
|
|
41
43
|
provider_response_id: response["responseId"],
|
|
42
44
|
pricing_mode: pricing_mode(request: request, response_headers: response_headers),
|
|
43
|
-
service_line_items:
|
|
45
|
+
service_line_items: grounding_line_items(grounding_request_count(response["candidates"]), model: model)
|
|
44
46
|
)
|
|
45
47
|
end
|
|
46
48
|
|
|
@@ -55,7 +57,7 @@ module LlmCostTracker
|
|
|
55
57
|
service_line_items = grounding_line_items_for_stream(events, model: model)
|
|
56
58
|
|
|
57
59
|
if usage
|
|
58
|
-
|
|
60
|
+
build_event(
|
|
59
61
|
request_url: request_url,
|
|
60
62
|
usage: usage,
|
|
61
63
|
stream: true,
|
|
@@ -75,16 +77,24 @@ module LlmCostTracker
|
|
|
75
77
|
end
|
|
76
78
|
end
|
|
77
79
|
|
|
80
|
+
def model_for(request_url, _request_parsed)
|
|
81
|
+
extract_model_from_url(request_url)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def provider_for(_request_url)
|
|
85
|
+
"gemini"
|
|
86
|
+
end
|
|
87
|
+
|
|
78
88
|
private
|
|
79
89
|
|
|
80
|
-
def
|
|
81
|
-
|
|
90
|
+
def build_event(request_url:, usage:, usage_source:, stream: false, provider_response_id: nil,
|
|
91
|
+
pricing_mode: nil, service_line_items: nil)
|
|
82
92
|
cache_read = usage["cachedContentTokenCount"].to_i
|
|
83
93
|
tool_use_prompt = usage["toolUsePromptTokenCount"].to_i
|
|
84
94
|
audio_input = audio_input_tokens(usage)
|
|
85
95
|
audio_output = audio_output_tokens(usage)
|
|
86
96
|
|
|
87
|
-
|
|
97
|
+
Event.build(
|
|
88
98
|
provider: "gemini",
|
|
89
99
|
model: extract_model_from_url(request_url),
|
|
90
100
|
pricing_mode: pricing_mode,
|
|
@@ -180,10 +190,6 @@ module LlmCostTracker
|
|
|
180
190
|
headers.to_h.find { |key, _value| key.to_s.downcase == name }&.last
|
|
181
191
|
end
|
|
182
192
|
|
|
183
|
-
def grounding_line_items_for_response(response, model:)
|
|
184
|
-
grounding_line_items(grounding_request_count(response["candidates"]), model: model)
|
|
185
|
-
end
|
|
186
|
-
|
|
187
193
|
def grounding_line_items_for_stream(events, model:)
|
|
188
194
|
quantity = find_event_value(events, reverse: true) do |data|
|
|
189
195
|
count = grounding_request_count(data["candidates"])
|
|
@@ -219,11 +225,7 @@ module LlmCostTracker
|
|
|
219
225
|
end
|
|
220
226
|
|
|
221
227
|
def grounding_billed_quantity(query_count, model:)
|
|
222
|
-
|
|
223
|
-
end
|
|
224
|
-
|
|
225
|
-
def per_query_billing?(model)
|
|
226
|
-
model.to_s.match?(PER_QUERY_GROUNDING_MODEL_PATTERN)
|
|
228
|
+
LlmCostTracker::Providers::Gemini::ModelFamilies.per_query_grounding?(model) ? query_count : 1
|
|
227
229
|
end
|
|
228
230
|
end
|
|
229
231
|
end
|
|
@@ -2,55 +2,37 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "base"
|
|
4
4
|
require_relative "openai_usage"
|
|
5
|
+
require_relative "../providers/openai/hosts"
|
|
5
6
|
|
|
6
7
|
module LlmCostTracker
|
|
7
8
|
module Parsers
|
|
8
9
|
class Openai < Base
|
|
9
10
|
include OpenaiUsage
|
|
10
11
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
12
|
+
TRACKED_PATHS = %w[
|
|
13
|
+
/v1/chat/completions
|
|
14
|
+
/v1/completions
|
|
15
|
+
/v1/embeddings
|
|
16
|
+
/v1/responses
|
|
17
|
+
/v1/images/generations
|
|
18
|
+
/v1/images/edits
|
|
19
|
+
/v1/images/variations
|
|
20
|
+
/v1/audio/transcriptions
|
|
21
|
+
/v1/audio/translations
|
|
22
|
+
/v1/audio/speech
|
|
23
|
+
/v1/moderations
|
|
23
24
|
].freeze
|
|
24
|
-
TRACKED_PATHS = %w[/v1/chat/completions /v1/completions /v1/embeddings /v1/responses].freeze
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
def provider_names
|
|
31
|
-
%w[openai]
|
|
32
|
-
end
|
|
26
|
+
class << self
|
|
27
|
+
def match?(url)
|
|
28
|
+
match_uri?(url, hosts: Providers::Openai::Hosts::API_HOSTS, exact_paths: TRACKED_PATHS)
|
|
29
|
+
end
|
|
33
30
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
request_body: request_body,
|
|
38
|
-
response_status: response_status,
|
|
39
|
-
response_body: response_body
|
|
40
|
-
)
|
|
31
|
+
def provider_names
|
|
32
|
+
%w[openai]
|
|
33
|
+
end
|
|
41
34
|
end
|
|
42
35
|
|
|
43
|
-
def parse_stream(response_status:, request_url: nil, request_body: nil, events: [], **)
|
|
44
|
-
parse_openai_stream_usage(
|
|
45
|
-
request_url: request_url,
|
|
46
|
-
request_body: request_body,
|
|
47
|
-
response_status: response_status,
|
|
48
|
-
events: events
|
|
49
|
-
)
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
private
|
|
53
|
-
|
|
54
36
|
def provider_for(_request_url)
|
|
55
37
|
"openai"
|
|
56
38
|
end
|
|
@@ -10,54 +10,41 @@ module LlmCostTracker
|
|
|
10
10
|
|
|
11
11
|
TRACKED_PATH_SUFFIXES = %w[/chat/completions /completions /embeddings /responses].freeze
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
class << self
|
|
14
|
+
def match?(url)
|
|
15
|
+
match_uri?(url, path_suffixes: TRACKED_PATH_SUFFIXES) { |uri| provider_for_uri(uri) }
|
|
16
|
+
end
|
|
16
17
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
def provider_names
|
|
19
|
+
providers = LlmCostTracker.configuration.openai_compatible_providers
|
|
20
|
+
cached = @provider_names
|
|
21
|
+
return cached if cached && @provider_names_providers.equal?(providers)
|
|
21
22
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
names = [
|
|
24
|
+
"openai_compatible",
|
|
25
|
+
*providers.each_value.map { |provider| provider.to_s.downcase }
|
|
26
|
+
].uniq.freeze
|
|
27
|
+
return names unless providers.frozen?
|
|
27
28
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
@provider_names_providers = providers
|
|
30
|
+
@provider_names = names
|
|
31
|
+
end
|
|
31
32
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
request_body: request_body,
|
|
36
|
-
response_status: response_status,
|
|
37
|
-
response_body: response_body
|
|
38
|
-
)
|
|
39
|
-
end
|
|
33
|
+
def provider_for(request_url)
|
|
34
|
+
provider_for_uri(parsed_uri(request_url)) || "openai_compatible"
|
|
35
|
+
end
|
|
40
36
|
|
|
41
|
-
|
|
42
|
-
parse_openai_stream_usage(
|
|
43
|
-
request_url: request_url,
|
|
44
|
-
request_body: request_body,
|
|
45
|
-
response_status: response_status,
|
|
46
|
-
events: events
|
|
47
|
-
)
|
|
48
|
-
end
|
|
37
|
+
private
|
|
49
38
|
|
|
50
|
-
|
|
39
|
+
def provider_for_uri(uri)
|
|
40
|
+
return nil unless uri
|
|
51
41
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
provider_for_uri(uri) || "openai_compatible"
|
|
42
|
+
LlmCostTracker.configuration.openai_compatible_providers[uri.host.to_s.downcase]&.to_s
|
|
43
|
+
end
|
|
55
44
|
end
|
|
56
45
|
|
|
57
|
-
def
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
LlmCostTracker.configuration.openai_compatible_providers[uri.host.to_s.downcase]&.to_s
|
|
46
|
+
def provider_for(request_url)
|
|
47
|
+
self.class.provider_for(request_url)
|
|
61
48
|
end
|
|
62
49
|
end
|
|
63
50
|
end
|