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
|
@@ -2,10 +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
7
|
require_relative "ingestion/lease_claim"
|
|
8
|
+
require_relative "ingestion/pool"
|
|
9
9
|
require_relative "ingestion/inbox"
|
|
10
10
|
require_relative "ingestion/batch"
|
|
11
11
|
require_relative "ingestion/worker"
|
|
@@ -19,11 +19,17 @@ module LlmCostTracker
|
|
|
19
19
|
"llm_cost_tracker_ingestion_"
|
|
20
20
|
end
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
["llm_cost_tracker_calls",
|
|
24
|
-
["llm_cost_tracker_call_line_items",
|
|
25
|
-
["llm_cost_tracker_call_tags",
|
|
26
|
-
|
|
22
|
+
CORE_SCHEMA_GUARDS = [
|
|
23
|
+
["llm_cost_tracker_calls", Ledger::Schema::Calls],
|
|
24
|
+
["llm_cost_tracker_call_line_items", Ledger::Schema::CallLineItems],
|
|
25
|
+
["llm_cost_tracker_call_tags", Ledger::Schema::CallTags]
|
|
26
|
+
].freeze
|
|
27
|
+
|
|
28
|
+
ROLLUPS_SCHEMA_GUARD = ["llm_cost_tracker_call_rollups", Ledger::Schema::CallRollups].freeze
|
|
29
|
+
|
|
30
|
+
ASYNC_SCHEMA_GUARDS = [
|
|
31
|
+
["llm_cost_tracker_ingestion_inbox_entries", Ledger::Schema::IngestionInboxEntries],
|
|
32
|
+
["llm_cost_tracker_ingestion_leases", Ledger::Schema::IngestionLeases]
|
|
27
33
|
].freeze
|
|
28
34
|
|
|
29
35
|
def ensure_current_schema!
|
|
@@ -31,7 +37,7 @@ module LlmCostTracker
|
|
|
31
37
|
raise Error, "llm_cost_tracker_calls table is missing; run install generator and migrate"
|
|
32
38
|
end
|
|
33
39
|
|
|
34
|
-
|
|
40
|
+
guards_for_current_config.each do |table_name, schema_module|
|
|
35
41
|
errors = schema_module.current_schema_errors
|
|
36
42
|
next if errors.empty?
|
|
37
43
|
|
|
@@ -40,6 +46,21 @@ module LlmCostTracker
|
|
|
40
46
|
end
|
|
41
47
|
end
|
|
42
48
|
|
|
49
|
+
def async?
|
|
50
|
+
LlmCostTracker.configuration.ingestion == :async
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def cache_rollups?
|
|
54
|
+
LlmCostTracker.configuration.cache_rollups
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def guards_for_current_config
|
|
58
|
+
guards = CORE_SCHEMA_GUARDS.dup
|
|
59
|
+
guards << ROLLUPS_SCHEMA_GUARD if cache_rollups?
|
|
60
|
+
guards += ASYNC_SCHEMA_GUARDS if async?
|
|
61
|
+
guards
|
|
62
|
+
end
|
|
63
|
+
|
|
43
64
|
def verify
|
|
44
65
|
unless LlmCostTracker::Call.table_exists?
|
|
45
66
|
return [
|
|
@@ -71,7 +92,7 @@ module LlmCostTracker
|
|
|
71
92
|
provider_response_id: response_id,
|
|
72
93
|
tags: { feature: VERIFY_TAG }
|
|
73
94
|
)
|
|
74
|
-
LlmCostTracker::Ingestion::Worker.flush!
|
|
95
|
+
LlmCostTracker::Ingestion::Worker.flush! if async?
|
|
75
96
|
persisted = LlmCostTracker::Call.where(provider_response_id: response_id).exists?
|
|
76
97
|
|
|
77
98
|
return capture_success if persisted && notifications.any?
|
|
@@ -89,7 +110,7 @@ module LlmCostTracker
|
|
|
89
110
|
LlmCostTracker::Doctor::Check.new(:error, "active_record capture", "#{e.class}: #{e.message}")
|
|
90
111
|
ensure
|
|
91
112
|
cleanup_verification_call(response_id) if response_id
|
|
92
|
-
|
|
113
|
+
cleanup_verification_inbox(event: event, response_id: response_id)
|
|
93
114
|
ActiveSupport::Notifications.unsubscribe(subscription) if subscription
|
|
94
115
|
end
|
|
95
116
|
|
|
@@ -100,10 +121,11 @@ module LlmCostTracker
|
|
|
100
121
|
end
|
|
101
122
|
|
|
102
123
|
def capture_success
|
|
124
|
+
path = async? ? "async inbox" : "inline writer"
|
|
103
125
|
LlmCostTracker::Doctor::Check.new(
|
|
104
126
|
:ok,
|
|
105
127
|
"active_record capture",
|
|
106
|
-
"manual event emitted and persisted through
|
|
128
|
+
"manual event emitted and persisted through #{path}"
|
|
107
129
|
)
|
|
108
130
|
end
|
|
109
131
|
|
|
@@ -116,13 +138,28 @@ module LlmCostTracker
|
|
|
116
138
|
|
|
117
139
|
def cleanup_verification_call(response_id)
|
|
118
140
|
relation = LlmCostTracker::Call.where(provider_response_id: response_id)
|
|
119
|
-
rows = relation.pluck(:id, :tracked_at, :total_cost, :pricing_snapshot)
|
|
141
|
+
rows = relation.pluck(:id, :tracked_at, :total_cost, :pricing_snapshot, :provider)
|
|
120
142
|
return if rows.empty?
|
|
121
143
|
|
|
122
144
|
relation.delete_all
|
|
145
|
+
return unless cache_rollups?
|
|
146
|
+
|
|
123
147
|
LlmCostTracker::Ledger::Rollups.decrement!(rows)
|
|
124
148
|
end
|
|
125
149
|
|
|
150
|
+
def cleanup_verification_inbox(event:, response_id:)
|
|
151
|
+
return unless async? && LlmCostTracker::Ingestion::InboxEntry.table_exists?
|
|
152
|
+
|
|
153
|
+
if event
|
|
154
|
+
LlmCostTracker::Ingestion::InboxEntry.where(event_id: event.event_id).delete_all
|
|
155
|
+
elsif response_id
|
|
156
|
+
escaped = ActiveRecord::Base.sanitize_sql_like(response_id)
|
|
157
|
+
LlmCostTracker::Ingestion::InboxEntry
|
|
158
|
+
.where("payload LIKE ?", "%\"provider_response_id\":\"#{escaped}\"%")
|
|
159
|
+
.delete_all
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
126
163
|
def sample_priced_identity
|
|
127
164
|
key = LlmCostTracker::Pricing::Registry.builtin_prices.find do |model_id, prices|
|
|
128
165
|
model_id.include?("/") && prices[:input] && prices[:output]
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "base"
|
|
4
4
|
require_relative "../billing/line_item"
|
|
5
|
+
require_relative "../providers/anthropic/tier_classification"
|
|
5
6
|
|
|
6
7
|
module LlmCostTracker
|
|
7
8
|
module Integrations
|
|
@@ -45,10 +46,10 @@ module LlmCostTracker
|
|
|
45
46
|
next if input_tokens.nil? && output_tokens.nil?
|
|
46
47
|
|
|
47
48
|
LlmCostTracker::Tracker.record(
|
|
48
|
-
|
|
49
|
+
event: Event.build(
|
|
49
50
|
provider: "anthropic",
|
|
50
51
|
model: object_value(message, :model) || request[:model],
|
|
51
|
-
pricing_mode: pricing_mode(
|
|
52
|
+
pricing_mode: pricing_mode(request: request, usage: usage),
|
|
52
53
|
token_usage: token_usage(usage: usage, input_tokens: input_tokens, output_tokens: output_tokens),
|
|
53
54
|
usage_source: :sdk_response,
|
|
54
55
|
provider_response_id: object_value(message, :id),
|
|
@@ -66,6 +67,8 @@ module LlmCostTracker
|
|
|
66
67
|
[
|
|
67
68
|
line_item_for_server_tool(server_tool_use, :web_search_request, :web_search_requests,
|
|
68
69
|
"usage.server_tool_use.web_search_requests"),
|
|
70
|
+
line_item_for_server_tool(server_tool_use, :web_fetch_request, :web_fetch_requests,
|
|
71
|
+
"usage.server_tool_use.web_fetch_requests"),
|
|
69
72
|
line_item_for_server_tool(server_tool_use, :code_execution_request, :code_execution_requests,
|
|
70
73
|
"usage.server_tool_use.code_execution_requests")
|
|
71
74
|
].compact
|
|
@@ -108,56 +111,58 @@ module LlmCostTracker
|
|
|
108
111
|
)
|
|
109
112
|
end
|
|
110
113
|
|
|
111
|
-
def pricing_mode(
|
|
114
|
+
def pricing_mode(request:, usage:)
|
|
115
|
+
service_tier = object_value(usage, :service_tier) || request[:service_tier]
|
|
116
|
+
tier = Providers::Anthropic::TierClassification
|
|
117
|
+
service_tier = nil if tier.standard_equivalent_tier?(service_tier)
|
|
118
|
+
|
|
112
119
|
modes = [
|
|
113
|
-
Pricing.normalize_mode(object_value(usage, :speed) ||
|
|
114
|
-
Pricing.normalize_mode(
|
|
115
|
-
object_value(usage, :service_tier) || object_value(message, :service_tier) || request[:service_tier]
|
|
116
|
-
)
|
|
120
|
+
Pricing.normalize_mode(object_value(usage, :speed) || request[:speed]),
|
|
121
|
+
Pricing.normalize_mode(service_tier)
|
|
117
122
|
]
|
|
118
|
-
|
|
123
|
+
geo = inference_geo(request: request, usage: usage).to_s.downcase
|
|
124
|
+
modes << "data_residency" if tier.data_residency_geo?(geo)
|
|
119
125
|
modes = modes.compact.uniq
|
|
120
126
|
modes.empty? ? nil : modes.join("_")
|
|
121
127
|
end
|
|
122
128
|
|
|
123
129
|
def stream_pricing_mode(request)
|
|
124
|
-
pricing_mode(
|
|
130
|
+
pricing_mode(request: request || {}, usage: nil)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def inference_geo(request:, usage:)
|
|
134
|
+
object_value(usage, :inference_geo) || request[:inference_geo]
|
|
125
135
|
end
|
|
126
136
|
|
|
127
|
-
def
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
137
|
+
def wrap_stream_call(args, kwargs)
|
|
138
|
+
request = request_params(args, kwargs)
|
|
139
|
+
enforce_budget!(request: request)
|
|
140
|
+
collector = stream_collector(request)
|
|
141
|
+
stream = yield
|
|
142
|
+
track_stream(stream, collector: collector)
|
|
131
143
|
end
|
|
132
144
|
end
|
|
133
145
|
|
|
134
146
|
module MessagesPatch
|
|
135
147
|
def create(*args, **kwargs)
|
|
136
|
-
LlmCostTracker::Integrations::Anthropic.
|
|
148
|
+
request = LlmCostTracker::Integrations::Anthropic.request_params(args, kwargs)
|
|
149
|
+
LlmCostTracker::Integrations::Anthropic.enforce_budget!(request: request)
|
|
137
150
|
started_at = LlmCostTracker::Timing.now_monotonic
|
|
138
151
|
message = super
|
|
139
152
|
LlmCostTracker::Integrations::Anthropic.record_message(
|
|
140
153
|
message,
|
|
141
|
-
request:
|
|
142
|
-
latency_ms: LlmCostTracker::
|
|
154
|
+
request: request,
|
|
155
|
+
latency_ms: LlmCostTracker::Timing.elapsed_ms(started_at)
|
|
143
156
|
)
|
|
144
157
|
message
|
|
145
158
|
end
|
|
146
159
|
|
|
147
160
|
def stream(*args, **kwargs)
|
|
148
|
-
|
|
149
|
-
LlmCostTracker::Integrations::Anthropic.enforce_budget!
|
|
150
|
-
collector = LlmCostTracker::Integrations::Anthropic.stream_collector(request)
|
|
151
|
-
stream = super
|
|
152
|
-
LlmCostTracker::Integrations::Anthropic.track_stream(stream, collector: collector)
|
|
161
|
+
LlmCostTracker::Integrations::Anthropic.wrap_stream_call(args, kwargs) { super }
|
|
153
162
|
end
|
|
154
163
|
|
|
155
164
|
def stream_raw(*args, **kwargs)
|
|
156
|
-
|
|
157
|
-
LlmCostTracker::Integrations::Anthropic.enforce_budget!
|
|
158
|
-
collector = LlmCostTracker::Integrations::Anthropic.stream_collector(request)
|
|
159
|
-
stream = super
|
|
160
|
-
LlmCostTracker::Integrations::Anthropic.track_stream(stream, collector: collector)
|
|
165
|
+
LlmCostTracker::Integrations::Anthropic.wrap_stream_call(args, kwargs) { super }
|
|
161
166
|
end
|
|
162
167
|
end
|
|
163
168
|
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
|
|
@@ -57,8 +60,21 @@ module LlmCostTracker
|
|
|
57
60
|
end
|
|
58
61
|
|
|
59
62
|
def request_params(args, kwargs)
|
|
60
|
-
params =
|
|
63
|
+
params =
|
|
64
|
+
case args.first
|
|
65
|
+
when Hash then args.first
|
|
66
|
+
when nil then {}
|
|
67
|
+
else args.first.to_h
|
|
68
|
+
end
|
|
61
69
|
params.merge(kwargs).with_indifferent_access
|
|
70
|
+
rescue StandardError
|
|
71
|
+
kwargs.to_h.with_indifferent_access
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def normalize_sdk_args(args, kwargs)
|
|
75
|
+
return args if args.any? || kwargs.empty?
|
|
76
|
+
|
|
77
|
+
[kwargs]
|
|
62
78
|
end
|
|
63
79
|
|
|
64
80
|
def track_stream(stream, collector:)
|
|
@@ -68,7 +84,7 @@ module LlmCostTracker
|
|
|
68
84
|
stream: stream,
|
|
69
85
|
collector: collector,
|
|
70
86
|
active: -> { active? },
|
|
71
|
-
finish: ->(errored
|
|
87
|
+
finish: ->(errored) { record_safely { collector.finish!(errored: errored) } }
|
|
72
88
|
).wrap
|
|
73
89
|
end
|
|
74
90
|
|
|
@@ -76,7 +92,8 @@ module LlmCostTracker
|
|
|
76
92
|
LlmCostTracker::Capture::StreamCollector.new(
|
|
77
93
|
provider: integration_name.to_s,
|
|
78
94
|
model: request[:model],
|
|
79
|
-
pricing_mode: stream_pricing_mode(request)
|
|
95
|
+
pricing_mode: stream_pricing_mode(request),
|
|
96
|
+
request: request
|
|
80
97
|
)
|
|
81
98
|
end
|
|
82
99
|
|
|
@@ -106,12 +123,13 @@ module LlmCostTracker
|
|
|
106
123
|
|
|
107
124
|
def patch_targets = []
|
|
108
125
|
|
|
109
|
-
def patch_target(constant_name, with:, methods:, optional: false)
|
|
126
|
+
def patch_target(constant_name, with:, methods:, optional: false, skip_when_methods_missing: false)
|
|
110
127
|
{
|
|
111
128
|
constant_name: constant_name,
|
|
112
129
|
patch: with,
|
|
113
130
|
method_names: Array(methods),
|
|
114
|
-
optional: optional
|
|
131
|
+
optional: optional,
|
|
132
|
+
skip_when_methods_missing: skip_when_methods_missing
|
|
115
133
|
}
|
|
116
134
|
end
|
|
117
135
|
|
|
@@ -172,6 +190,8 @@ module LlmCostTracker
|
|
|
172
190
|
end
|
|
173
191
|
|
|
174
192
|
def missing_methods(target_class, target)
|
|
193
|
+
return [] if target[:skip_when_methods_missing]
|
|
194
|
+
|
|
175
195
|
target.fetch(:method_names).filter_map do |method_name|
|
|
176
196
|
next if target_class.method_defined?(method_name) || target_class.private_method_defined?(method_name)
|
|
177
197
|
|