llm_cost_tracker 0.9.0 → 0.11.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 +55 -0
- data/README.md +6 -2
- data/app/assets/llm_cost_tracker/application.css +782 -801
- data/app/controllers/llm_cost_tracker/application_controller.rb +15 -3
- data/app/controllers/llm_cost_tracker/calls_controller.rb +39 -20
- data/app/controllers/llm_cost_tracker/dashboard_controller.rb +0 -3
- data/app/controllers/llm_cost_tracker/models_controller.rb +3 -1
- data/app/controllers/llm_cost_tracker/pricing_controller.rb +16 -0
- data/app/controllers/llm_cost_tracker/reconciliation_controller.rb +13 -19
- data/app/controllers/llm_cost_tracker/tags_controller.rb +3 -1
- data/app/helpers/llm_cost_tracker/application_helper.rb +16 -4
- data/app/helpers/llm_cost_tracker/chart_helper.rb +22 -6
- data/app/helpers/llm_cost_tracker/sortable_table_helper.rb +41 -0
- data/app/models/llm_cost_tracker/provider_invoice_import.rb +9 -4
- data/app/services/llm_cost_tracker/dashboard/pricing_overview.rb +95 -0
- data/app/services/llm_cost_tracker/dashboard/setup_state.rb +104 -0
- data/app/services/llm_cost_tracker/dashboard/sort.rb +9 -0
- data/app/services/llm_cost_tracker/dashboard/tag_breakdown.rb +19 -5
- data/app/services/llm_cost_tracker/dashboard/top_models.rb +34 -19
- data/app/views/layouts/llm_cost_tracker/application.html.erb +80 -17
- data/app/views/llm_cost_tracker/calls/index.html.erb +69 -90
- data/app/views/llm_cost_tracker/calls/show.html.erb +119 -120
- data/app/views/llm_cost_tracker/dashboard/index.html.erb +119 -158
- data/app/views/llm_cost_tracker/data_quality/index.html.erb +109 -108
- data/app/views/llm_cost_tracker/errors/database.html.erb +2 -2
- data/app/views/llm_cost_tracker/models/index.html.erb +39 -59
- data/app/views/llm_cost_tracker/pricing/index.html.erb +93 -0
- data/app/views/llm_cost_tracker/reconciliation/index.html.erb +49 -58
- data/app/views/llm_cost_tracker/shared/_filter_pill_date.html.erb +19 -0
- data/app/views/llm_cost_tracker/shared/_filter_pill_model.html.erb +22 -0
- data/app/views/llm_cost_tracker/shared/_filter_pill_provider.html.erb +22 -0
- data/app/views/llm_cost_tracker/shared/_filter_pill_stream.html.erb +23 -0
- data/app/views/llm_cost_tracker/shared/_spend_chart.html.erb +3 -13
- data/app/views/llm_cost_tracker/shared/_tag_chips.html.erb +1 -1
- data/app/views/llm_cost_tracker/shared/setup_required.html.erb +16 -15
- data/app/views/llm_cost_tracker/tags/index.html.erb +27 -32
- data/app/views/llm_cost_tracker/tags/show.html.erb +83 -102
- data/config/routes.rb +1 -0
- data/lib/llm_cost_tracker/billing/cost_status.rb +21 -25
- data/lib/llm_cost_tracker/billing/line_item.rb +15 -49
- data/lib/llm_cost_tracker/budget.rb +29 -8
- data/lib/llm_cost_tracker/{parsers → capture}/sse.rb +1 -1
- data/lib/llm_cost_tracker/capture/stream_collector.rb +34 -42
- data/lib/llm_cost_tracker/capture/stream_tracker.rb +2 -6
- data/lib/llm_cost_tracker/configuration.rb +30 -44
- data/lib/llm_cost_tracker/doctor/capture_verifier.rb +1 -1
- data/lib/llm_cost_tracker/doctor/ingestion_check.rb +8 -8
- 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.rb +80 -25
- data/lib/llm_cost_tracker/engine.rb +1 -2
- data/lib/llm_cost_tracker/errors.rb +3 -2
- data/lib/llm_cost_tracker/event.rb +47 -0
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/{durable_ingestion_generator.rb → async_ingestion_generator.rb} +8 -8
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/install_generator.rb +4 -23
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/{create_llm_cost_tracker_durable_ingestion.rb.erb → create_llm_cost_tracker_async_ingestion.rb.erb} +3 -3
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/create_llm_cost_tracker_reconciliation.rb.erb +6 -1
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/initializer.rb.erb +14 -7
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_call_rollups_provider.rb.erb +27 -8
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_call_tags_key_value_index.rb.erb +5 -5
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_provider_invoice_imports_provider.rb.erb +36 -0
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_provider_invoices_metadata_index.rb.erb +27 -0
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/upgrade_call_rollups_provider_generator.rb +0 -9
- 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 +4 -25
- data/lib/llm_cost_tracker/ingestion/pool.rb +44 -0
- data/lib/llm_cost_tracker/ingestion/worker.rb +22 -36
- data/lib/llm_cost_tracker/ingestion.rb +8 -9
- data/lib/llm_cost_tracker/integrations/anthropic.rb +46 -68
- data/lib/llm_cost_tracker/integrations/base.rb +14 -11
- data/lib/llm_cost_tracker/integrations/openai.rb +104 -131
- data/lib/llm_cost_tracker/integrations/ruby_llm.rb +27 -73
- data/lib/llm_cost_tracker/integrations.rb +14 -13
- data/lib/llm_cost_tracker/ledger/period/totals.rb +5 -3
- data/lib/llm_cost_tracker/ledger/rollups.rb +4 -13
- data/lib/llm_cost_tracker/ledger/schema/call_line_items.rb +11 -0
- data/lib/llm_cost_tracker/ledger/schema/call_rollups.rb +13 -3
- data/lib/llm_cost_tracker/ledger/schema/call_tags.rb +11 -0
- data/lib/llm_cost_tracker/ledger/schema/calls.rb +0 -4
- data/lib/llm_cost_tracker/ledger/schema/ingestion_inbox_entries.rb +13 -3
- data/lib/llm_cost_tracker/ledger/schema/ingestion_leases.rb +13 -3
- data/lib/llm_cost_tracker/ledger/schema/provider_invoice_imports.rb +19 -9
- data/lib/llm_cost_tracker/ledger/schema/provider_invoices.rb +26 -11
- data/lib/llm_cost_tracker/ledger/store.rb +21 -18
- data/lib/llm_cost_tracker/ledger/tags/query.rb +0 -1
- data/lib/llm_cost_tracker/ledger.rb +13 -0
- data/lib/llm_cost_tracker/logging.rb +0 -4
- data/lib/llm_cost_tracker/middleware/faraday.rb +46 -17
- data/lib/llm_cost_tracker/parsers/anthropic.rb +35 -59
- data/lib/llm_cost_tracker/parsers/azure.rb +46 -0
- data/lib/llm_cost_tracker/parsers/base.rb +53 -47
- data/lib/llm_cost_tracker/parsers/gemini.rb +23 -27
- data/lib/llm_cost_tracker/parsers/openai.rb +8 -40
- data/lib/llm_cost_tracker/parsers/openai_compatible.rb +26 -49
- data/lib/llm_cost_tracker/parsers/openai_usage.rb +19 -23
- data/lib/llm_cost_tracker/parsers.rb +29 -4
- data/lib/llm_cost_tracker/prices.json +567 -579
- data/lib/llm_cost_tracker/pricing/backfill.rb +140 -0
- data/lib/llm_cost_tracker/pricing/effective_prices.rb +2 -4
- data/lib/llm_cost_tracker/pricing/estimator.rb +33 -0
- data/lib/llm_cost_tracker/pricing/explainer.rb +5 -2
- data/lib/llm_cost_tracker/pricing/lookup.rb +37 -2
- data/lib/llm_cost_tracker/pricing/mode.rb +34 -4
- data/lib/llm_cost_tracker/pricing/registry.rb +0 -7
- data/lib/llm_cost_tracker/pricing/service_charges.rb +6 -10
- 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 +14 -2
- data/lib/llm_cost_tracker/pricing/sync.rb +1 -9
- data/lib/llm_cost_tracker/pricing/unknown.rb +5 -2
- data/lib/llm_cost_tracker/pricing.rb +71 -43
- data/lib/llm_cost_tracker/providers/anthropic/server_tools.rb +15 -0
- 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/providers/openai/service_charges.rb +157 -0
- data/lib/llm_cost_tracker/railtie.rb +3 -5
- data/lib/llm_cost_tracker/reconcile_tasks.rb +18 -21
- data/lib/llm_cost_tracker/reconciliation/diff.rb +26 -45
- data/lib/llm_cost_tracker/reconciliation/diff_result.rb +0 -4
- data/lib/llm_cost_tracker/reconciliation/importer.rb +3 -7
- data/lib/llm_cost_tracker/reconciliation/sources/anthropic_usage.rb +10 -33
- data/lib/llm_cost_tracker/reconciliation/sources/coercion.rb +40 -0
- data/lib/llm_cost_tracker/reconciliation/sources/openai_usage.rb +7 -31
- data/lib/llm_cost_tracker/report/formatter.rb +32 -19
- data/lib/llm_cost_tracker/report.rb +0 -4
- data/lib/llm_cost_tracker/retention.rb +20 -8
- data/lib/llm_cost_tracker/tags/sanitizer.rb +13 -17
- data/lib/llm_cost_tracker/token_usage.rb +4 -0
- data/lib/llm_cost_tracker/tracker.rb +33 -74
- data/lib/llm_cost_tracker/version.rb +1 -1
- data/lib/llm_cost_tracker.rb +11 -15
- data/lib/tasks/llm_cost_tracker.rake +16 -2
- metadata +31 -12
- data/app/views/llm_cost_tracker/shared/_active_filters.html.erb +0 -16
- data/app/views/llm_cost_tracker/shared/_filters.html.erb +0 -66
- data/app/views/llm_cost_tracker/shared/_sort.html.erb +0 -13
- data/lib/llm_cost_tracker/dashboard_setup_state.rb +0 -109
- data/lib/llm_cost_tracker/ingestion/inline.rb +0 -22
- data/lib/llm_cost_tracker/parsers/openai_service_charges.rb +0 -126
- data/lib/llm_cost_tracker/usage_capture.rb +0 -58
|
@@ -16,29 +16,31 @@ module LlmCostTracker
|
|
|
16
16
|
MAX_IDLE_INTERVAL_SECONDS = 5.0
|
|
17
17
|
LEASE_SECONDS = 10
|
|
18
18
|
FLUSH_TIMEOUT_SECONDS = 10
|
|
19
|
+
MUTEX = Mutex.new
|
|
20
|
+
|
|
19
21
|
class << self
|
|
20
22
|
def ensure_started
|
|
21
|
-
return unless Ingestion.
|
|
23
|
+
return unless Ingestion.async?
|
|
22
24
|
|
|
23
|
-
thread =
|
|
25
|
+
thread = MUTEX.synchronize do
|
|
24
26
|
reset_after_fork!
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
end
|
|
27
|
+
break @thread if @stop_requested || @thread&.alive?
|
|
28
|
+
|
|
29
|
+
@generation = @generation.to_i + 1
|
|
30
|
+
generation = @generation
|
|
31
|
+
@thread = Thread.new { run(generation) }
|
|
32
|
+
@thread.name = "llm_cost_tracker_ingestor"
|
|
33
|
+
@thread.report_on_exception = false
|
|
33
34
|
@thread
|
|
34
35
|
end
|
|
35
36
|
wake_thread(thread)
|
|
36
37
|
end
|
|
37
38
|
|
|
38
39
|
def flush!(timeout: nil, require_lease: false)
|
|
39
|
-
return true unless Ingestion.
|
|
40
|
+
return true unless Ingestion.async?
|
|
40
41
|
|
|
41
42
|
Ingestion.ensure_current_schema!
|
|
43
|
+
MUTEX.synchronize { reset_after_fork! }
|
|
42
44
|
|
|
43
45
|
deadline = Time.now.utc + flush_timeout_seconds(timeout)
|
|
44
46
|
loop do
|
|
@@ -56,10 +58,10 @@ module LlmCostTracker
|
|
|
56
58
|
end
|
|
57
59
|
|
|
58
60
|
def shutdown!(timeout: nil, drain: true)
|
|
59
|
-
return true unless Ingestion.
|
|
61
|
+
return true unless Ingestion.async?
|
|
60
62
|
|
|
61
63
|
timeout ||= FLUSH_TIMEOUT_SECONDS
|
|
62
|
-
thread =
|
|
64
|
+
thread = MUTEX.synchronize do
|
|
63
65
|
@stop_requested = true
|
|
64
66
|
@generation = @generation.to_i + 1
|
|
65
67
|
@thread
|
|
@@ -71,14 +73,14 @@ module LlmCostTracker
|
|
|
71
73
|
handle_error(e)
|
|
72
74
|
false
|
|
73
75
|
ensure
|
|
74
|
-
|
|
76
|
+
MUTEX.synchronize do
|
|
75
77
|
@thread = nil if @thread.equal?(thread) && !thread&.alive?
|
|
76
78
|
end
|
|
77
79
|
end
|
|
78
80
|
|
|
79
81
|
def reset!
|
|
80
|
-
thread =
|
|
81
|
-
@stop_requested =
|
|
82
|
+
thread = MUTEX.synchronize do
|
|
83
|
+
@stop_requested = false
|
|
82
84
|
@generation = @generation.to_i + 1
|
|
83
85
|
thread = @thread
|
|
84
86
|
@thread = nil
|
|
@@ -98,6 +100,7 @@ module LlmCostTracker
|
|
|
98
100
|
|
|
99
101
|
def ingest_once(require_lease: true)
|
|
100
102
|
Ingestion.ensure_current_schema!
|
|
103
|
+
MUTEX.synchronize { reset_after_fork! }
|
|
101
104
|
batch = Ingestion::Batch.new(identity: identity)
|
|
102
105
|
return 0 unless batch.claimable?
|
|
103
106
|
return 0 if require_lease && !Ingestion::LeaseClaim.new(identity: identity, seconds: LEASE_SECONDS).acquire
|
|
@@ -110,16 +113,12 @@ module LlmCostTracker
|
|
|
110
113
|
|
|
111
114
|
private
|
|
112
115
|
|
|
113
|
-
def mutex
|
|
114
|
-
@mutex ||= Mutex.new
|
|
115
|
-
end
|
|
116
|
-
|
|
117
116
|
def run(generation)
|
|
118
117
|
idle_interval = IDLE_INTERVAL_SECONDS
|
|
119
118
|
loop do
|
|
120
|
-
break if
|
|
119
|
+
break if MUTEX.synchronize { @stop_requested || generation != @generation }
|
|
121
120
|
|
|
122
|
-
processed =
|
|
121
|
+
processed = Rails.application.executor.wrap { ingest_once }
|
|
123
122
|
release_connection!
|
|
124
123
|
if processed.zero?
|
|
125
124
|
sleep(idle_interval)
|
|
@@ -134,7 +133,7 @@ module LlmCostTracker
|
|
|
134
133
|
end
|
|
135
134
|
ensure
|
|
136
135
|
release_connection!
|
|
137
|
-
|
|
136
|
+
MUTEX.synchronize { @thread = nil if @thread.equal?(Thread.current) }
|
|
138
137
|
end
|
|
139
138
|
|
|
140
139
|
def reset_after_fork!
|
|
@@ -151,19 +150,6 @@ module LlmCostTracker
|
|
|
151
150
|
nil
|
|
152
151
|
end
|
|
153
152
|
|
|
154
|
-
def executor_wrap(&)
|
|
155
|
-
executor = rails_executor
|
|
156
|
-
return yield unless executor
|
|
157
|
-
|
|
158
|
-
executor.wrap(&)
|
|
159
|
-
end
|
|
160
|
-
|
|
161
|
-
def rails_executor
|
|
162
|
-
Rails.application.try(:executor)
|
|
163
|
-
rescue StandardError
|
|
164
|
-
nil
|
|
165
|
-
end
|
|
166
|
-
|
|
167
153
|
def identity
|
|
168
154
|
@identity ||= "pid-#{Process.pid}-#{SecureRandom.hex(6)}"
|
|
169
155
|
end
|
|
@@ -2,11 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
require "securerandom"
|
|
4
4
|
|
|
5
|
-
require_relative "doctor/check"
|
|
6
5
|
require_relative "errors"
|
|
7
6
|
require_relative "ledger"
|
|
8
|
-
require_relative "ingestion/inline"
|
|
9
7
|
require_relative "ingestion/lease_claim"
|
|
8
|
+
require_relative "ingestion/pool"
|
|
10
9
|
require_relative "ingestion/inbox"
|
|
11
10
|
require_relative "ingestion/batch"
|
|
12
11
|
require_relative "ingestion/worker"
|
|
@@ -28,7 +27,7 @@ module LlmCostTracker
|
|
|
28
27
|
|
|
29
28
|
ROLLUPS_SCHEMA_GUARD = ["llm_cost_tracker_call_rollups", Ledger::Schema::CallRollups].freeze
|
|
30
29
|
|
|
31
|
-
|
|
30
|
+
ASYNC_SCHEMA_GUARDS = [
|
|
32
31
|
["llm_cost_tracker_ingestion_inbox_entries", Ledger::Schema::IngestionInboxEntries],
|
|
33
32
|
["llm_cost_tracker_ingestion_leases", Ledger::Schema::IngestionLeases]
|
|
34
33
|
].freeze
|
|
@@ -47,8 +46,8 @@ module LlmCostTracker
|
|
|
47
46
|
end
|
|
48
47
|
end
|
|
49
48
|
|
|
50
|
-
def
|
|
51
|
-
LlmCostTracker.configuration.
|
|
49
|
+
def async?
|
|
50
|
+
LlmCostTracker.configuration.ingestion == :async
|
|
52
51
|
end
|
|
53
52
|
|
|
54
53
|
def cache_rollups?
|
|
@@ -58,7 +57,7 @@ module LlmCostTracker
|
|
|
58
57
|
def guards_for_current_config
|
|
59
58
|
guards = CORE_SCHEMA_GUARDS.dup
|
|
60
59
|
guards << ROLLUPS_SCHEMA_GUARD if cache_rollups?
|
|
61
|
-
guards +=
|
|
60
|
+
guards += ASYNC_SCHEMA_GUARDS if async?
|
|
62
61
|
guards
|
|
63
62
|
end
|
|
64
63
|
|
|
@@ -93,7 +92,7 @@ module LlmCostTracker
|
|
|
93
92
|
provider_response_id: response_id,
|
|
94
93
|
tags: { feature: VERIFY_TAG }
|
|
95
94
|
)
|
|
96
|
-
LlmCostTracker::Ingestion::Worker.flush! if
|
|
95
|
+
LlmCostTracker::Ingestion::Worker.flush! if async?
|
|
97
96
|
persisted = LlmCostTracker::Call.where(provider_response_id: response_id).exists?
|
|
98
97
|
|
|
99
98
|
return capture_success if persisted && notifications.any?
|
|
@@ -122,7 +121,7 @@ module LlmCostTracker
|
|
|
122
121
|
end
|
|
123
122
|
|
|
124
123
|
def capture_success
|
|
125
|
-
path =
|
|
124
|
+
path = async? ? "async inbox" : "inline writer"
|
|
126
125
|
LlmCostTracker::Doctor::Check.new(
|
|
127
126
|
:ok,
|
|
128
127
|
"active_record capture",
|
|
@@ -149,7 +148,7 @@ module LlmCostTracker
|
|
|
149
148
|
end
|
|
150
149
|
|
|
151
150
|
def cleanup_verification_inbox(event:, response_id:)
|
|
152
|
-
return unless
|
|
151
|
+
return unless async? && LlmCostTracker::Ingestion::InboxEntry.table_exists?
|
|
153
152
|
|
|
154
153
|
if event
|
|
155
154
|
LlmCostTracker::Ingestion::InboxEntry.where(event_id: event.event_id).delete_all
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "base"
|
|
4
4
|
require_relative "../billing/line_item"
|
|
5
|
+
require_relative "../providers/anthropic/server_tools"
|
|
6
|
+
require_relative "../providers/anthropic/tier_classification"
|
|
5
7
|
|
|
6
8
|
module LlmCostTracker
|
|
7
9
|
module Integrations
|
|
@@ -45,10 +47,10 @@ module LlmCostTracker
|
|
|
45
47
|
next if input_tokens.nil? && output_tokens.nil?
|
|
46
48
|
|
|
47
49
|
LlmCostTracker::Tracker.record(
|
|
48
|
-
|
|
50
|
+
event: Event.build(
|
|
49
51
|
provider: "anthropic",
|
|
50
52
|
model: object_value(message, :model) || request[:model],
|
|
51
|
-
pricing_mode: pricing_mode(
|
|
53
|
+
pricing_mode: pricing_mode(request: request, usage: usage),
|
|
52
54
|
token_usage: token_usage(usage: usage, input_tokens: input_tokens, output_tokens: output_tokens),
|
|
53
55
|
usage_source: :sdk_response,
|
|
54
56
|
provider_response_id: object_value(message, :id),
|
|
@@ -63,35 +65,18 @@ module LlmCostTracker
|
|
|
63
65
|
server_tool_use = object_value(usage, :server_tool_use)
|
|
64
66
|
return [] unless server_tool_use
|
|
65
67
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
line_item_for_server_tool(server_tool_use, :web_fetch_request, :web_fetch_requests,
|
|
70
|
-
"usage.server_tool_use.web_fetch_requests"),
|
|
71
|
-
line_item_for_server_tool(server_tool_use, :code_execution_request, :code_execution_requests,
|
|
72
|
-
"usage.server_tool_use.code_execution_requests")
|
|
73
|
-
].compact
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
def line_item_for_server_tool(server_tool_use, component_key, count_key, provider_field)
|
|
77
|
-
quantity = server_tool_count(server_tool_use, count_key)
|
|
78
|
-
return nil if quantity.zero?
|
|
79
|
-
|
|
80
|
-
Billing::LineItem.build(
|
|
81
|
-
component_key: component_key,
|
|
82
|
-
quantity: quantity,
|
|
83
|
-
cost_status: Billing::CostStatus::UNKNOWN,
|
|
84
|
-
pricing_basis: :provider_usage,
|
|
85
|
-
provider_field: provider_field
|
|
86
|
-
)
|
|
87
|
-
end
|
|
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)
|
|
68
|
+
Providers::Anthropic::ServerTools::LINE_ITEMS.filter_map do |component_key, count_key|
|
|
69
|
+
quantity = object_value(server_tool_use, count_key).to_i
|
|
70
|
+
next if quantity.zero?
|
|
93
71
|
|
|
94
|
-
|
|
72
|
+
Billing::LineItem.build(
|
|
73
|
+
component_key: component_key,
|
|
74
|
+
quantity: quantity,
|
|
75
|
+
cost_status: Billing::CostStatus::UNKNOWN,
|
|
76
|
+
pricing_basis: :provider_usage,
|
|
77
|
+
provider_field: "usage.server_tool_use.#{count_key}"
|
|
78
|
+
)
|
|
79
|
+
end
|
|
95
80
|
end
|
|
96
81
|
|
|
97
82
|
def token_usage(usage:, input_tokens:, output_tokens:)
|
|
@@ -118,65 +103,58 @@ module LlmCostTracker
|
|
|
118
103
|
)
|
|
119
104
|
end
|
|
120
105
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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)
|
|
106
|
+
def pricing_mode(request:, usage:)
|
|
107
|
+
service_tier = object_value(usage, :service_tier) || request[:service_tier]
|
|
108
|
+
tier = Providers::Anthropic::TierClassification
|
|
109
|
+
service_tier = nil if tier.standard_equivalent_tier?(service_tier)
|
|
131
110
|
|
|
132
111
|
modes = [
|
|
133
|
-
Pricing.
|
|
134
|
-
Pricing.
|
|
112
|
+
Pricing::Mode.normalize(object_value(usage, :speed) || request[:speed]),
|
|
113
|
+
Pricing::Mode.normalize(service_tier)
|
|
135
114
|
]
|
|
136
|
-
geo = inference_geo(
|
|
137
|
-
modes << "data_residency" if
|
|
115
|
+
geo = inference_geo(request: request, usage: usage).to_s.downcase
|
|
116
|
+
modes << "data_residency" if tier.data_residency_geo?(geo)
|
|
138
117
|
modes = modes.compact.uniq
|
|
139
118
|
modes.empty? ? nil : modes.join("_")
|
|
140
119
|
end
|
|
141
120
|
|
|
142
121
|
def stream_pricing_mode(request)
|
|
143
|
-
pricing_mode(
|
|
122
|
+
pricing_mode(request: request || {}, usage: nil)
|
|
144
123
|
end
|
|
145
124
|
|
|
146
|
-
def inference_geo(
|
|
147
|
-
object_value(usage, :inference_geo) ||
|
|
148
|
-
|
|
149
|
-
|
|
125
|
+
def inference_geo(request:, usage:)
|
|
126
|
+
object_value(usage, :inference_geo) || request[:inference_geo]
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def wrap_stream_call(args, kwargs)
|
|
130
|
+
request = request_params(args, kwargs)
|
|
131
|
+
enforce_budget!(request: request)
|
|
132
|
+
collector = stream_collector(request)
|
|
133
|
+
stream = yield
|
|
134
|
+
track_stream(stream, collector: collector)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def wrap_blocking_call(args, kwargs)
|
|
138
|
+
request = request_params(args, kwargs)
|
|
139
|
+
enforce_budget!(request: request)
|
|
140
|
+
started_at = LlmCostTracker::Timing.now_monotonic
|
|
141
|
+
message = yield
|
|
142
|
+
record_message(message, request: request, latency_ms: LlmCostTracker::Timing.elapsed_ms(started_at))
|
|
143
|
+
message
|
|
150
144
|
end
|
|
151
145
|
end
|
|
152
146
|
|
|
153
147
|
module MessagesPatch
|
|
154
148
|
def create(*args, **kwargs)
|
|
155
|
-
LlmCostTracker::Integrations::Anthropic.
|
|
156
|
-
started_at = LlmCostTracker::Timing.now_monotonic
|
|
157
|
-
message = super
|
|
158
|
-
LlmCostTracker::Integrations::Anthropic.record_message(
|
|
159
|
-
message,
|
|
160
|
-
request: LlmCostTracker::Integrations::Anthropic.request_params(args, kwargs),
|
|
161
|
-
latency_ms: LlmCostTracker::Integrations::Anthropic.elapsed_ms(started_at)
|
|
162
|
-
)
|
|
163
|
-
message
|
|
149
|
+
LlmCostTracker::Integrations::Anthropic.wrap_blocking_call(args, kwargs) { super }
|
|
164
150
|
end
|
|
165
151
|
|
|
166
152
|
def stream(*args, **kwargs)
|
|
167
|
-
|
|
168
|
-
LlmCostTracker::Integrations::Anthropic.enforce_budget!
|
|
169
|
-
collector = LlmCostTracker::Integrations::Anthropic.stream_collector(request)
|
|
170
|
-
stream = super
|
|
171
|
-
LlmCostTracker::Integrations::Anthropic.track_stream(stream, collector: collector)
|
|
153
|
+
LlmCostTracker::Integrations::Anthropic.wrap_stream_call(args, kwargs) { super }
|
|
172
154
|
end
|
|
173
155
|
|
|
174
156
|
def stream_raw(*args, **kwargs)
|
|
175
|
-
|
|
176
|
-
LlmCostTracker::Integrations::Anthropic.enforce_budget!
|
|
177
|
-
collector = LlmCostTracker::Integrations::Anthropic.stream_collector(request)
|
|
178
|
-
stream = super
|
|
179
|
-
LlmCostTracker::Integrations::Anthropic.track_stream(stream, collector: collector)
|
|
157
|
+
LlmCostTracker::Integrations::Anthropic.wrap_stream_call(args, kwargs) { super }
|
|
180
158
|
end
|
|
181
159
|
end
|
|
182
160
|
end
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require "active_support/core_ext/hash/indifferent_access"
|
|
4
4
|
require "active_support/core_ext/string/inflections"
|
|
5
5
|
|
|
6
|
+
require_relative "../doctor/check"
|
|
6
7
|
require_relative "../logging"
|
|
7
8
|
require_relative "../timing"
|
|
8
9
|
require_relative "../capture/stream_collector"
|
|
@@ -11,7 +12,7 @@ require_relative "../capture/stream_tracker"
|
|
|
11
12
|
module LlmCostTracker
|
|
12
13
|
module Integrations
|
|
13
14
|
module Base
|
|
14
|
-
Result =
|
|
15
|
+
Result = LlmCostTracker::Doctor::Check
|
|
15
16
|
|
|
16
17
|
def active?
|
|
17
18
|
LlmCostTracker.configuration.instrumented?(integration_name)
|
|
@@ -26,26 +27,28 @@ module LlmCostTracker
|
|
|
26
27
|
end
|
|
27
28
|
|
|
28
29
|
def status
|
|
29
|
-
name = integration_name
|
|
30
|
+
name = integration_name.to_s
|
|
30
31
|
problems = version_problems + target_problems
|
|
31
32
|
if problems.any?
|
|
32
|
-
return Result.new(
|
|
33
|
+
return Result.new(:warn, name, "#{name} integration cannot be installed: #{problems.join('; ')}")
|
|
33
34
|
end
|
|
34
35
|
|
|
35
36
|
installed = patch_targets.reject { |target| target.fetch(:optional) }.all? do |target|
|
|
36
37
|
target.fetch(:constant_name).to_s.safe_constantize&.ancestors&.include?(target.fetch(:patch))
|
|
37
38
|
end
|
|
38
|
-
return Result.new(
|
|
39
|
+
return Result.new(:ok, name, "#{name} integration installed") if installed
|
|
39
40
|
|
|
40
|
-
Result.new(
|
|
41
|
+
Result.new(:warn, name, "#{name} integration is enabled but not installed")
|
|
41
42
|
end
|
|
42
43
|
|
|
43
|
-
def
|
|
44
|
-
|
|
45
|
-
end
|
|
44
|
+
def enforce_budget!(request:)
|
|
45
|
+
return unless active?
|
|
46
46
|
|
|
47
|
-
|
|
48
|
-
|
|
47
|
+
LlmCostTracker::Tracker.enforce_budget!(
|
|
48
|
+
provider: integration_name.to_s,
|
|
49
|
+
model: request[:model],
|
|
50
|
+
request: request
|
|
51
|
+
)
|
|
49
52
|
end
|
|
50
53
|
|
|
51
54
|
def record_safely
|
|
@@ -61,7 +64,7 @@ module LlmCostTracker
|
|
|
61
64
|
case args.first
|
|
62
65
|
when Hash then args.first
|
|
63
66
|
when nil then {}
|
|
64
|
-
else args.first.
|
|
67
|
+
else args.first.to_h
|
|
65
68
|
end
|
|
66
69
|
params.merge(kwargs).with_indifferent_access
|
|
67
70
|
rescue StandardError
|