llm_cost_tracker 0.8.0 → 0.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/CHANGELOG.md +108 -0
- data/README.md +12 -5
- 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 +5 -7
- 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 +10 -0
- 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 +24 -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/views/layouts/llm_cost_tracker/application.html.erb +6 -1
- data/app/views/llm_cost_tracker/calls/show.html.erb +25 -40
- data/app/views/llm_cost_tracker/dashboard/index.html.erb +9 -9
- data/app/views/llm_cost_tracker/data_quality/index.html.erb +91 -52
- 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/line_item.rb +1 -1
- data/lib/llm_cost_tracker/budget.rb +4 -2
- data/lib/llm_cost_tracker/capture/stream_collector.rb +93 -20
- data/lib/llm_cost_tracker/capture/stream_tracker.rb +40 -5
- data/lib/llm_cost_tracker/configuration.rb +53 -1
- data/lib/llm_cost_tracker/dashboard_setup_state.rb +109 -0
- data/lib/llm_cost_tracker/doctor/cost_drift_check.rb +2 -0
- data/lib/llm_cost_tracker/doctor/ingestion_check.rb +26 -0
- data/lib/llm_cost_tracker/doctor/invoice_reconciliation_check.rb +164 -0
- data/lib/llm_cost_tracker/doctor/schema_check.rb +5 -2
- data/lib/llm_cost_tracker/doctor.rb +72 -3
- data/lib/llm_cost_tracker/engine.rb +9 -0
- data/lib/llm_cost_tracker/event.rb +1 -1
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/call_rollups_generator.rb +43 -0
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/durable_ingestion_generator.rb +43 -0
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/install_generator.rb +13 -3
- 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_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_durable_ingestion.rb.erb +29 -0
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/create_llm_cost_tracker_reconciliation.rb.erb +55 -0
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/initializer.rb.erb +28 -25
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_call_rollups_provider.rb.erb +20 -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/upgrade_call_rollups_provider_generator.rb +38 -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/ingestion/inbox.rb +0 -1
- data/lib/llm_cost_tracker/ingestion/inline.rb +22 -0
- data/lib/llm_cost_tracker/ingestion/worker.rb +10 -2
- data/lib/llm_cost_tracker/ingestion.rb +48 -10
- data/lib/llm_cost_tracker/integrations/anthropic.rb +24 -5
- data/lib/llm_cost_tracker/integrations/base.rb +22 -5
- data/lib/llm_cost_tracker/integrations/openai.rb +300 -66
- data/lib/llm_cost_tracker/integrations/ruby_llm.rb +105 -6
- data/lib/llm_cost_tracker/integrations.rb +19 -1
- data/lib/llm_cost_tracker/ledger/period/totals.rb +21 -5
- data/lib/llm_cost_tracker/ledger/rollups.rb +24 -10
- data/lib/llm_cost_tracker/ledger/schema/call_line_items.rb +30 -1
- data/lib/llm_cost_tracker/ledger/schema/call_rollups.rb +3 -3
- data/lib/llm_cost_tracker/ledger/schema/call_tags.rb +17 -2
- data/lib/llm_cost_tracker/ledger/schema/calls.rb +2 -0
- data/lib/llm_cost_tracker/ledger/schema/ingestion_inbox_entries.rb +47 -0
- data/lib/llm_cost_tracker/ledger/schema/ingestion_leases.rb +42 -0
- data/lib/llm_cost_tracker/ledger/schema/provider_invoice_imports.rb +46 -0
- data/lib/llm_cost_tracker/ledger/schema/provider_invoices.rb +2 -2
- data/lib/llm_cost_tracker/ledger/store.rb +14 -14
- data/lib/llm_cost_tracker/ledger/tags/encoding.rb +37 -0
- data/lib/llm_cost_tracker/ledger/tags/query.rb +2 -1
- data/lib/llm_cost_tracker/ledger.rb +2 -1
- data/lib/llm_cost_tracker/masking.rb +39 -0
- data/lib/llm_cost_tracker/middleware/faraday.rb +88 -29
- data/lib/llm_cost_tracker/parsers/anthropic.rb +22 -7
- data/lib/llm_cost_tracker/parsers/base.rb +5 -1
- data/lib/llm_cost_tracker/parsers/gemini.rb +4 -0
- data/lib/llm_cost_tracker/parsers/openai.rb +16 -2
- data/lib/llm_cost_tracker/parsers/openai_compatible.rb +5 -1
- data/lib/llm_cost_tracker/parsers/openai_service_charges.rb +49 -10
- data/lib/llm_cost_tracker/parsers/openai_usage.rb +124 -53
- data/lib/llm_cost_tracker/prices.json +110 -19
- data/lib/llm_cost_tracker/pricing/effective_prices.rb +5 -36
- data/lib/llm_cost_tracker/pricing/lookup.rb +36 -3
- data/lib/llm_cost_tracker/pricing/mode.rb +76 -0
- data/lib/llm_cost_tracker/pricing/registry.rb +3 -1
- data/lib/llm_cost_tracker/pricing/service_charges.rb +9 -3
- data/lib/llm_cost_tracker/pricing/sync/registry_writer.rb +50 -1
- data/lib/llm_cost_tracker/pricing/sync.rb +3 -1
- data/lib/llm_cost_tracker/pricing.rb +47 -19
- data/lib/llm_cost_tracker/railtie.rb +6 -0
- data/lib/llm_cost_tracker/reconcile_tasks.rb +134 -0
- data/lib/llm_cost_tracker/reconciliation/diff.rb +428 -0
- data/lib/llm_cost_tracker/reconciliation/diff_result.rb +48 -0
- data/lib/llm_cost_tracker/reconciliation/import_result.rb +19 -0
- data/lib/llm_cost_tracker/reconciliation/importer.rb +253 -0
- data/lib/llm_cost_tracker/reconciliation/sources/anthropic_usage.rb +171 -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/retention.rb +15 -2
- data/lib/llm_cost_tracker/tags/context.rb +3 -4
- data/lib/llm_cost_tracker/tags/sanitizer.rb +60 -4
- data/lib/llm_cost_tracker/token_usage.rb +10 -2
- data/lib/llm_cost_tracker/tracker.rb +45 -18
- data/lib/llm_cost_tracker/version.rb +1 -1
- data/lib/llm_cost_tracker.rb +9 -0
- data/lib/tasks/llm_cost_tracker.rake +25 -2
- metadata +36 -1
data/lib/llm_cost_tracker/generators/llm_cost_tracker/upgrade_call_tags_key_value_index_generator.rb
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators"
|
|
4
|
+
require "rails/generators/active_record"
|
|
5
|
+
|
|
6
|
+
module LlmCostTracker
|
|
7
|
+
module Generators
|
|
8
|
+
class UpgradeCallTagsKeyValueIndexGenerator < Rails::Generators::Base
|
|
9
|
+
include ActiveRecord::Generators::Migration
|
|
10
|
+
|
|
11
|
+
source_root File.expand_path("templates", __dir__)
|
|
12
|
+
|
|
13
|
+
desc "Adds a (key, value) composite index on llm_cost_tracker_call_tags " \
|
|
14
|
+
"so high-cardinality tag filters use an index lookup instead of a key-only scan."
|
|
15
|
+
|
|
16
|
+
def create_migration_file
|
|
17
|
+
migration_template(
|
|
18
|
+
"upgrade_call_tags_key_value_index.rb.erb",
|
|
19
|
+
"db/migrate/upgrade_llm_cost_tracker_call_tags_key_value_index.rb"
|
|
20
|
+
)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def migration_version
|
|
26
|
+
"[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]"
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators"
|
|
4
|
+
require "rails/generators/active_record"
|
|
5
|
+
|
|
6
|
+
module LlmCostTracker
|
|
7
|
+
module Generators
|
|
8
|
+
class UpgradeImageTokensGenerator < Rails::Generators::Base
|
|
9
|
+
include ActiveRecord::Generators::Migration
|
|
10
|
+
|
|
11
|
+
source_root File.expand_path("templates", __dir__)
|
|
12
|
+
|
|
13
|
+
desc "Adds image_input_tokens and image_output_tokens columns to llm_cost_tracker_calls."
|
|
14
|
+
|
|
15
|
+
def create_migration_file
|
|
16
|
+
migration_template(
|
|
17
|
+
"upgrade_image_tokens.rb.erb",
|
|
18
|
+
"db/migrate/upgrade_llm_cost_tracker_image_tokens.rb"
|
|
19
|
+
)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def migration_version
|
|
25
|
+
"[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]"
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -112,7 +112,6 @@ module LlmCostTracker
|
|
|
112
112
|
quoted_columns = columns.map { |column| connection.quote_column_name(column) }.join(", ")
|
|
113
113
|
quoted_values = columns.map { |column| connection.quote(row.fetch(column)) }.join(", ")
|
|
114
114
|
table = connection.quote_table_name(InboxEntry.table_name)
|
|
115
|
-
|
|
116
115
|
connection.execute("INSERT INTO #{table} (#{quoted_columns}) VALUES (#{quoted_values})")
|
|
117
116
|
end
|
|
118
117
|
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../ledger/store"
|
|
4
|
+
|
|
5
|
+
module LlmCostTracker
|
|
6
|
+
module Ingestion
|
|
7
|
+
module Inline
|
|
8
|
+
class << self
|
|
9
|
+
def save(event)
|
|
10
|
+
persist(event)
|
|
11
|
+
event
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
private
|
|
15
|
+
|
|
16
|
+
def persist(event)
|
|
17
|
+
Ledger::Store.insert_many([event])
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -18,6 +18,8 @@ module LlmCostTracker
|
|
|
18
18
|
FLUSH_TIMEOUT_SECONDS = 10
|
|
19
19
|
class << self
|
|
20
20
|
def ensure_started
|
|
21
|
+
return unless Ingestion.durable?
|
|
22
|
+
|
|
21
23
|
thread = mutex.synchronize do
|
|
22
24
|
reset_after_fork!
|
|
23
25
|
unless @thread&.alive?
|
|
@@ -34,6 +36,8 @@ module LlmCostTracker
|
|
|
34
36
|
end
|
|
35
37
|
|
|
36
38
|
def flush!(timeout: nil, require_lease: false)
|
|
39
|
+
return true unless Ingestion.durable?
|
|
40
|
+
|
|
37
41
|
Ingestion.ensure_current_schema!
|
|
38
42
|
|
|
39
43
|
deadline = Time.now.utc + flush_timeout_seconds(timeout)
|
|
@@ -52,6 +56,8 @@ module LlmCostTracker
|
|
|
52
56
|
end
|
|
53
57
|
|
|
54
58
|
def shutdown!(timeout: nil, drain: true)
|
|
59
|
+
return true unless Ingestion.durable?
|
|
60
|
+
|
|
55
61
|
timeout ||= FLUSH_TIMEOUT_SECONDS
|
|
56
62
|
thread = mutex.synchronize do
|
|
57
63
|
@stop_requested = true
|
|
@@ -59,13 +65,15 @@ module LlmCostTracker
|
|
|
59
65
|
@thread
|
|
60
66
|
end
|
|
61
67
|
wake_thread(thread)
|
|
62
|
-
thread&.join(
|
|
68
|
+
thread&.join(timeout)
|
|
63
69
|
drain ? flush!(timeout: timeout, require_lease: true) : true
|
|
64
70
|
rescue StandardError => e
|
|
65
71
|
handle_error(e)
|
|
66
72
|
false
|
|
67
73
|
ensure
|
|
68
|
-
mutex.synchronize
|
|
74
|
+
mutex.synchronize do
|
|
75
|
+
@thread = nil if @thread.equal?(thread) && !thread&.alive?
|
|
76
|
+
end
|
|
69
77
|
end
|
|
70
78
|
|
|
71
79
|
def reset!
|
|
@@ -5,6 +5,7 @@ require "securerandom"
|
|
|
5
5
|
require_relative "doctor/check"
|
|
6
6
|
require_relative "errors"
|
|
7
7
|
require_relative "ledger"
|
|
8
|
+
require_relative "ingestion/inline"
|
|
8
9
|
require_relative "ingestion/lease_claim"
|
|
9
10
|
require_relative "ingestion/inbox"
|
|
10
11
|
require_relative "ingestion/batch"
|
|
@@ -19,11 +20,17 @@ module LlmCostTracker
|
|
|
19
20
|
"llm_cost_tracker_ingestion_"
|
|
20
21
|
end
|
|
21
22
|
|
|
22
|
-
|
|
23
|
-
["llm_cost_tracker_calls",
|
|
24
|
-
["llm_cost_tracker_call_line_items",
|
|
25
|
-
["llm_cost_tracker_call_tags",
|
|
26
|
-
|
|
23
|
+
CORE_SCHEMA_GUARDS = [
|
|
24
|
+
["llm_cost_tracker_calls", Ledger::Schema::Calls],
|
|
25
|
+
["llm_cost_tracker_call_line_items", Ledger::Schema::CallLineItems],
|
|
26
|
+
["llm_cost_tracker_call_tags", Ledger::Schema::CallTags]
|
|
27
|
+
].freeze
|
|
28
|
+
|
|
29
|
+
ROLLUPS_SCHEMA_GUARD = ["llm_cost_tracker_call_rollups", Ledger::Schema::CallRollups].freeze
|
|
30
|
+
|
|
31
|
+
DURABLE_SCHEMA_GUARDS = [
|
|
32
|
+
["llm_cost_tracker_ingestion_inbox_entries", Ledger::Schema::IngestionInboxEntries],
|
|
33
|
+
["llm_cost_tracker_ingestion_leases", Ledger::Schema::IngestionLeases]
|
|
27
34
|
].freeze
|
|
28
35
|
|
|
29
36
|
def ensure_current_schema!
|
|
@@ -31,7 +38,7 @@ module LlmCostTracker
|
|
|
31
38
|
raise Error, "llm_cost_tracker_calls table is missing; run install generator and migrate"
|
|
32
39
|
end
|
|
33
40
|
|
|
34
|
-
|
|
41
|
+
guards_for_current_config.each do |table_name, schema_module|
|
|
35
42
|
errors = schema_module.current_schema_errors
|
|
36
43
|
next if errors.empty?
|
|
37
44
|
|
|
@@ -40,6 +47,21 @@ module LlmCostTracker
|
|
|
40
47
|
end
|
|
41
48
|
end
|
|
42
49
|
|
|
50
|
+
def durable?
|
|
51
|
+
LlmCostTracker.configuration.durable_ingestion
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def cache_rollups?
|
|
55
|
+
LlmCostTracker.configuration.cache_rollups
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def guards_for_current_config
|
|
59
|
+
guards = CORE_SCHEMA_GUARDS.dup
|
|
60
|
+
guards << ROLLUPS_SCHEMA_GUARD if cache_rollups?
|
|
61
|
+
guards += DURABLE_SCHEMA_GUARDS if durable?
|
|
62
|
+
guards
|
|
63
|
+
end
|
|
64
|
+
|
|
43
65
|
def verify
|
|
44
66
|
unless LlmCostTracker::Call.table_exists?
|
|
45
67
|
return [
|
|
@@ -71,7 +93,7 @@ module LlmCostTracker
|
|
|
71
93
|
provider_response_id: response_id,
|
|
72
94
|
tags: { feature: VERIFY_TAG }
|
|
73
95
|
)
|
|
74
|
-
LlmCostTracker::Ingestion::Worker.flush!
|
|
96
|
+
LlmCostTracker::Ingestion::Worker.flush! if durable?
|
|
75
97
|
persisted = LlmCostTracker::Call.where(provider_response_id: response_id).exists?
|
|
76
98
|
|
|
77
99
|
return capture_success if persisted && notifications.any?
|
|
@@ -89,7 +111,7 @@ module LlmCostTracker
|
|
|
89
111
|
LlmCostTracker::Doctor::Check.new(:error, "active_record capture", "#{e.class}: #{e.message}")
|
|
90
112
|
ensure
|
|
91
113
|
cleanup_verification_call(response_id) if response_id
|
|
92
|
-
|
|
114
|
+
cleanup_verification_inbox(event: event, response_id: response_id)
|
|
93
115
|
ActiveSupport::Notifications.unsubscribe(subscription) if subscription
|
|
94
116
|
end
|
|
95
117
|
|
|
@@ -100,10 +122,11 @@ module LlmCostTracker
|
|
|
100
122
|
end
|
|
101
123
|
|
|
102
124
|
def capture_success
|
|
125
|
+
path = durable? ? "durable inbox" : "inline writer"
|
|
103
126
|
LlmCostTracker::Doctor::Check.new(
|
|
104
127
|
:ok,
|
|
105
128
|
"active_record capture",
|
|
106
|
-
"manual event emitted and persisted through
|
|
129
|
+
"manual event emitted and persisted through #{path}"
|
|
107
130
|
)
|
|
108
131
|
end
|
|
109
132
|
|
|
@@ -116,13 +139,28 @@ module LlmCostTracker
|
|
|
116
139
|
|
|
117
140
|
def cleanup_verification_call(response_id)
|
|
118
141
|
relation = LlmCostTracker::Call.where(provider_response_id: response_id)
|
|
119
|
-
rows = relation.pluck(:id, :tracked_at, :total_cost, :pricing_snapshot)
|
|
142
|
+
rows = relation.pluck(:id, :tracked_at, :total_cost, :pricing_snapshot, :provider)
|
|
120
143
|
return if rows.empty?
|
|
121
144
|
|
|
122
145
|
relation.delete_all
|
|
146
|
+
return unless cache_rollups?
|
|
147
|
+
|
|
123
148
|
LlmCostTracker::Ledger::Rollups.decrement!(rows)
|
|
124
149
|
end
|
|
125
150
|
|
|
151
|
+
def cleanup_verification_inbox(event:, response_id:)
|
|
152
|
+
return unless durable? && LlmCostTracker::Ingestion::InboxEntry.table_exists?
|
|
153
|
+
|
|
154
|
+
if event
|
|
155
|
+
LlmCostTracker::Ingestion::InboxEntry.where(event_id: event.event_id).delete_all
|
|
156
|
+
elsif response_id
|
|
157
|
+
escaped = ActiveRecord::Base.sanitize_sql_like(response_id)
|
|
158
|
+
LlmCostTracker::Ingestion::InboxEntry
|
|
159
|
+
.where("payload LIKE ?", "%\"provider_response_id\":\"#{escaped}\"%")
|
|
160
|
+
.delete_all
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
126
164
|
def sample_priced_identity
|
|
127
165
|
key = LlmCostTracker::Pricing::Registry.builtin_prices.find do |model_id, prices|
|
|
128
166
|
model_id.include?("/") && prices[:input] && prices[:output]
|
|
@@ -66,13 +66,15 @@ module LlmCostTracker
|
|
|
66
66
|
[
|
|
67
67
|
line_item_for_server_tool(server_tool_use, :web_search_request, :web_search_requests,
|
|
68
68
|
"usage.server_tool_use.web_search_requests"),
|
|
69
|
+
line_item_for_server_tool(server_tool_use, :web_fetch_request, :web_fetch_requests,
|
|
70
|
+
"usage.server_tool_use.web_fetch_requests"),
|
|
69
71
|
line_item_for_server_tool(server_tool_use, :code_execution_request, :code_execution_requests,
|
|
70
72
|
"usage.server_tool_use.code_execution_requests")
|
|
71
73
|
].compact
|
|
72
74
|
end
|
|
73
75
|
|
|
74
76
|
def line_item_for_server_tool(server_tool_use, component_key, count_key, provider_field)
|
|
75
|
-
quantity =
|
|
77
|
+
quantity = server_tool_count(server_tool_use, count_key)
|
|
76
78
|
return nil if quantity.zero?
|
|
77
79
|
|
|
78
80
|
Billing::LineItem.build(
|
|
@@ -84,6 +86,14 @@ module LlmCostTracker
|
|
|
84
86
|
)
|
|
85
87
|
end
|
|
86
88
|
|
|
89
|
+
def server_tool_count(server_tool_use, count_key)
|
|
90
|
+
direct = object_value(server_tool_use, count_key).to_i
|
|
91
|
+
return direct if direct.positive?
|
|
92
|
+
return 0 unless server_tool_use.respond_to?(:to_h)
|
|
93
|
+
|
|
94
|
+
server_tool_use.to_h[count_key].to_i
|
|
95
|
+
end
|
|
96
|
+
|
|
87
97
|
def token_usage(usage:, input_tokens:, output_tokens:)
|
|
88
98
|
cache_creation = object_value(usage, :cache_creation)
|
|
89
99
|
if cache_creation
|
|
@@ -108,14 +118,23 @@ module LlmCostTracker
|
|
|
108
118
|
)
|
|
109
119
|
end
|
|
110
120
|
|
|
121
|
+
DATA_RESIDENCY_GEOS = %w[us].freeze
|
|
122
|
+
# Anthropic Priority Tier is committed throughput (tokens/min capacity), not a per-token
|
|
123
|
+
# surcharge. Treat it as standard pricing so cost_status doesn't fall to :unknown.
|
|
124
|
+
STANDARD_EQUIVALENT_SERVICE_TIERS = %w[standard standard_only priority].freeze
|
|
125
|
+
|
|
111
126
|
def pricing_mode(message:, request:, usage:)
|
|
127
|
+
service_tier = object_value(usage, :service_tier) ||
|
|
128
|
+
object_value(message, :service_tier) ||
|
|
129
|
+
request[:service_tier]
|
|
130
|
+
service_tier = nil if STANDARD_EQUIVALENT_SERVICE_TIERS.include?(service_tier.to_s)
|
|
131
|
+
|
|
112
132
|
modes = [
|
|
113
133
|
Pricing.normalize_mode(object_value(usage, :speed) || object_value(message, :speed) || request[:speed]),
|
|
114
|
-
Pricing.normalize_mode(
|
|
115
|
-
object_value(usage, :service_tier) || object_value(message, :service_tier) || request[:service_tier]
|
|
116
|
-
)
|
|
134
|
+
Pricing.normalize_mode(service_tier)
|
|
117
135
|
]
|
|
118
|
-
|
|
136
|
+
geo = inference_geo(message: message, request: request, usage: usage).to_s.downcase
|
|
137
|
+
modes << "data_residency" if DATA_RESIDENCY_GEOS.include?(geo)
|
|
119
138
|
modes = modes.compact.uniq
|
|
120
139
|
modes.empty? ? nil : modes.join("_")
|
|
121
140
|
end
|
|
@@ -57,8 +57,21 @@ module LlmCostTracker
|
|
|
57
57
|
end
|
|
58
58
|
|
|
59
59
|
def request_params(args, kwargs)
|
|
60
|
-
params =
|
|
60
|
+
params =
|
|
61
|
+
case args.first
|
|
62
|
+
when Hash then args.first
|
|
63
|
+
when nil then {}
|
|
64
|
+
else args.first.respond_to?(:to_h) ? args.first.to_h : {}
|
|
65
|
+
end
|
|
61
66
|
params.merge(kwargs).with_indifferent_access
|
|
67
|
+
rescue StandardError
|
|
68
|
+
kwargs.to_h.with_indifferent_access
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def normalize_sdk_args(args, kwargs)
|
|
72
|
+
return args if args.any? || kwargs.empty?
|
|
73
|
+
|
|
74
|
+
[kwargs]
|
|
62
75
|
end
|
|
63
76
|
|
|
64
77
|
def track_stream(stream, collector:)
|
|
@@ -68,7 +81,7 @@ module LlmCostTracker
|
|
|
68
81
|
stream: stream,
|
|
69
82
|
collector: collector,
|
|
70
83
|
active: -> { active? },
|
|
71
|
-
finish: ->(errored
|
|
84
|
+
finish: ->(errored) { record_safely { collector.finish!(errored: errored) } }
|
|
72
85
|
).wrap
|
|
73
86
|
end
|
|
74
87
|
|
|
@@ -76,7 +89,8 @@ module LlmCostTracker
|
|
|
76
89
|
LlmCostTracker::Capture::StreamCollector.new(
|
|
77
90
|
provider: integration_name.to_s,
|
|
78
91
|
model: request[:model],
|
|
79
|
-
pricing_mode: stream_pricing_mode(request)
|
|
92
|
+
pricing_mode: stream_pricing_mode(request),
|
|
93
|
+
request: request
|
|
80
94
|
)
|
|
81
95
|
end
|
|
82
96
|
|
|
@@ -106,12 +120,13 @@ module LlmCostTracker
|
|
|
106
120
|
|
|
107
121
|
def patch_targets = []
|
|
108
122
|
|
|
109
|
-
def patch_target(constant_name, with:, methods:, optional: false)
|
|
123
|
+
def patch_target(constant_name, with:, methods:, optional: false, skip_when_methods_missing: false)
|
|
110
124
|
{
|
|
111
125
|
constant_name: constant_name,
|
|
112
126
|
patch: with,
|
|
113
127
|
method_names: Array(methods),
|
|
114
|
-
optional: optional
|
|
128
|
+
optional: optional,
|
|
129
|
+
skip_when_methods_missing: skip_when_methods_missing
|
|
115
130
|
}
|
|
116
131
|
end
|
|
117
132
|
|
|
@@ -172,6 +187,8 @@ module LlmCostTracker
|
|
|
172
187
|
end
|
|
173
188
|
|
|
174
189
|
def missing_methods(target_class, target)
|
|
190
|
+
return [] if target[:skip_when_methods_missing]
|
|
191
|
+
|
|
175
192
|
target.fetch(:method_names).filter_map do |method_name|
|
|
176
193
|
next if target_class.method_defined?(method_name) || target_class.private_method_defined?(method_name)
|
|
177
194
|
|