llm_cost_tracker 0.11.0 → 0.13.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 +73 -0
- data/README.md +7 -4
- data/app/assets/llm_cost_tracker/application.css +8 -7
- data/app/controllers/llm_cost_tracker/calls_controller.rb +5 -5
- data/app/controllers/llm_cost_tracker/dashboard_controller.rb +1 -1
- data/app/controllers/llm_cost_tracker/data_quality_controller.rb +1 -0
- data/app/controllers/llm_cost_tracker/pricing_controller.rb +1 -1
- data/app/helpers/llm_cost_tracker/application_helper.rb +12 -19
- data/app/helpers/llm_cost_tracker/dashboard_filter_options_helper.rb +1 -11
- data/app/helpers/llm_cost_tracker/sortable_table_helper.rb +4 -4
- data/app/helpers/llm_cost_tracker/token_usage_helper.rb +4 -6
- data/app/models/llm_cost_tracker/call.rb +28 -63
- data/app/models/llm_cost_tracker/call_line_item.rb +2 -2
- data/app/models/llm_cost_tracker/call_rollup.rb +38 -0
- data/app/models/llm_cost_tracker/call_tag.rb +0 -2
- data/app/models/llm_cost_tracker/ingestion/inbox_entry.rb +3 -0
- data/app/services/llm_cost_tracker/dashboard/data_quality.rb +76 -43
- data/app/services/llm_cost_tracker/dashboard/filter.rb +5 -0
- data/app/services/llm_cost_tracker/dashboard/masking.rb +31 -0
- data/app/services/llm_cost_tracker/dashboard/monthly_budget.rb +63 -0
- data/app/services/llm_cost_tracker/dashboard/overview_stats.rb +5 -71
- data/app/services/llm_cost_tracker/dashboard/pagination.rb +2 -5
- data/app/services/llm_cost_tracker/dashboard/pricing_overview.rb +30 -44
- data/app/services/llm_cost_tracker/dashboard/setup_state.rb +4 -60
- data/app/services/llm_cost_tracker/dashboard/tag_breakdown.rb +1 -7
- data/app/services/llm_cost_tracker/dashboard/tag_key_explorer.rb +1 -1
- data/app/views/layouts/llm_cost_tracker/application.html.erb +0 -6
- data/app/views/llm_cost_tracker/calls/index.html.erb +8 -8
- data/app/views/llm_cost_tracker/calls/show.html.erb +33 -25
- data/app/views/llm_cost_tracker/dashboard/index.html.erb +8 -8
- data/app/views/llm_cost_tracker/data_quality/index.html.erb +73 -117
- data/app/views/llm_cost_tracker/models/index.html.erb +5 -5
- data/app/views/llm_cost_tracker/pricing/index.html.erb +3 -3
- data/app/views/llm_cost_tracker/shared/_filter_pill_model.html.erb +1 -1
- data/app/views/llm_cost_tracker/shared/_filter_pill_provider.html.erb +1 -1
- data/app/views/llm_cost_tracker/shared/_filter_pill_stream.html.erb +1 -1
- data/app/views/llm_cost_tracker/tags/index.html.erb +3 -3
- data/app/views/llm_cost_tracker/tags/show.html.erb +10 -10
- data/config/routes.rb +2 -3
- data/lib/llm_cost_tracker/budget.rb +24 -26
- data/lib/llm_cost_tracker/capture/sdk_payload.rb +34 -0
- data/lib/llm_cost_tracker/capture/sse.rb +1 -0
- data/lib/llm_cost_tracker/capture/stream_collector.rb +32 -39
- data/lib/llm_cost_tracker/capture/stream_tracker.rb +17 -28
- data/lib/llm_cost_tracker/capture_verifier.rb +59 -0
- data/lib/llm_cost_tracker/charges/cost.rb +27 -0
- data/lib/llm_cost_tracker/{billing → charges}/cost_status.rb +14 -4
- data/lib/llm_cost_tracker/{billing → charges}/line_item.rb +41 -44
- data/lib/llm_cost_tracker/check.rb +5 -0
- data/lib/llm_cost_tracker/configuration.rb +13 -44
- data/lib/llm_cost_tracker/currency.rb +5 -0
- data/lib/llm_cost_tracker/doctor/ingestion_check.rb +15 -49
- data/lib/llm_cost_tracker/doctor/price_check.rb +1 -1
- data/lib/llm_cost_tracker/doctor/probe.rb +3 -4
- data/lib/llm_cost_tracker/doctor/schema_check.rb +3 -6
- data/lib/llm_cost_tracker/doctor.rb +5 -69
- data/lib/llm_cost_tracker/engine.rb +4 -4
- data/lib/llm_cost_tracker/event.rb +12 -18
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/install_generator.rb +2 -3
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/prices_generator.rb +5 -2
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/create_llm_cost_tracker_calls.rb.erb +4 -5
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/initializer.rb.erb +4 -3
- data/lib/llm_cost_tracker/ingestion/batch.rb +61 -12
- data/lib/llm_cost_tracker/ingestion/inbox.rb +7 -8
- data/lib/llm_cost_tracker/ingestion/pool.rb +3 -11
- data/lib/llm_cost_tracker/ingestion/worker.rb +7 -17
- data/lib/llm_cost_tracker/ingestion.rb +24 -36
- data/lib/llm_cost_tracker/integrations/anthropic.rb +93 -106
- data/lib/llm_cost_tracker/integrations/base.rb +39 -57
- data/lib/llm_cost_tracker/integrations/openai/batch_capture.rb +84 -0
- data/lib/llm_cost_tracker/integrations/openai/patches.rb +81 -0
- data/lib/llm_cost_tracker/integrations/openai.rb +70 -276
- data/lib/llm_cost_tracker/integrations/ruby_llm.rb +87 -99
- data/lib/llm_cost_tracker/integrations.rb +32 -25
- data/lib/llm_cost_tracker/ledger/period/totals.rb +27 -42
- data/lib/llm_cost_tracker/ledger/period.rb +5 -10
- data/lib/llm_cost_tracker/ledger/rollups.rb +67 -98
- data/lib/llm_cost_tracker/ledger/schema/adapter.rb +12 -13
- data/lib/llm_cost_tracker/ledger/schema/base.rb +51 -0
- data/lib/llm_cost_tracker/ledger/schema/call_line_items.rb +24 -79
- data/lib/llm_cost_tracker/ledger/schema/call_rollups.rb +3 -35
- data/lib/llm_cost_tracker/ledger/schema/call_tags.rb +4 -41
- data/lib/llm_cost_tracker/ledger/schema/calls.rb +30 -99
- data/lib/llm_cost_tracker/ledger/schema/ingestion/inbox_entries.rb +26 -0
- data/lib/llm_cost_tracker/ledger/schema/ingestion/leases.rb +17 -0
- data/lib/llm_cost_tracker/ledger/schema.rb +26 -0
- data/lib/llm_cost_tracker/ledger/store.rb +24 -41
- data/lib/llm_cost_tracker/ledger/tags/{sql.rb → breakdown.rb} +1 -1
- data/lib/llm_cost_tracker/ledger/tags/encoding.rb +4 -6
- data/lib/llm_cost_tracker/ledger.rb +8 -18
- data/lib/llm_cost_tracker/logging.rb +4 -21
- data/lib/llm_cost_tracker/middleware/faraday.rb +61 -50
- data/lib/llm_cost_tracker/parsers.rb +139 -26
- data/lib/llm_cost_tracker/prices.json +1821 -13
- data/lib/llm_cost_tracker/pricing/backfill.rb +52 -80
- data/lib/llm_cost_tracker/pricing/calculation.rb +279 -0
- data/lib/llm_cost_tracker/pricing/effective_prices.rb +17 -18
- data/lib/llm_cost_tracker/pricing/estimator.rb +2 -2
- data/lib/llm_cost_tracker/pricing/matcher.rb +98 -0
- data/lib/llm_cost_tracker/pricing/mode.rb +44 -56
- data/lib/llm_cost_tracker/pricing/price_key.rb +56 -0
- data/lib/llm_cost_tracker/pricing/rate.rb +18 -0
- data/lib/llm_cost_tracker/pricing/registry.rb +189 -100
- data/lib/llm_cost_tracker/pricing/service_rates.rb +69 -0
- data/lib/llm_cost_tracker/pricing/source.rb +7 -0
- data/lib/llm_cost_tracker/pricing/sync/fetcher.rb +2 -3
- data/lib/llm_cost_tracker/pricing/sync/registry_diff.rb +4 -10
- data/lib/llm_cost_tracker/pricing/sync/registry_writer.rb +10 -3
- data/lib/llm_cost_tracker/pricing/sync.rb +9 -11
- data/lib/llm_cost_tracker/pricing/unknown.rb +1 -5
- data/lib/llm_cost_tracker/pricing.rb +10 -278
- data/lib/llm_cost_tracker/providers/anthropic/parser.rb +93 -0
- data/lib/llm_cost_tracker/providers/anthropic/response_parser.rb +30 -0
- data/lib/llm_cost_tracker/providers/anthropic/usage_extractor.rb +76 -0
- data/lib/llm_cost_tracker/providers/azure/hosts.rb +1 -4
- data/lib/llm_cost_tracker/providers/azure/parser.rb +44 -0
- data/lib/llm_cost_tracker/providers/gemini/model_families.rb +1 -4
- data/lib/llm_cost_tracker/providers/gemini/parser.rb +177 -0
- data/lib/llm_cost_tracker/providers/gemini/usage_extractor.rb +76 -0
- data/lib/llm_cost_tracker/providers/openai/hosts.rb +1 -7
- data/lib/llm_cost_tracker/providers/openai/model_families.rb +5 -8
- data/lib/llm_cost_tracker/providers/openai/parser.rb +39 -0
- data/lib/llm_cost_tracker/providers/openai/response_parser.rb +152 -0
- data/lib/llm_cost_tracker/providers/openai/service_charges.rb +63 -39
- data/lib/llm_cost_tracker/providers/openai/usage_extractor.rb +72 -0
- data/lib/llm_cost_tracker/providers/openai_compatible/parser.rb +36 -0
- data/lib/llm_cost_tracker/providers.rb +35 -0
- data/lib/llm_cost_tracker/railtie.rb +0 -3
- data/lib/llm_cost_tracker/report/data.rb +3 -4
- data/lib/llm_cost_tracker/report/formatter.rb +1 -1
- data/lib/llm_cost_tracker/report.rb +1 -1
- data/lib/llm_cost_tracker/retention.rb +6 -19
- data/lib/llm_cost_tracker/tags/context.rb +9 -6
- data/lib/llm_cost_tracker/tags/sanitizer.rb +10 -0
- data/lib/llm_cost_tracker/timing.rb +2 -4
- data/lib/llm_cost_tracker/tracker.rb +23 -36
- data/lib/llm_cost_tracker/usage/catalog.rb +69 -0
- data/lib/llm_cost_tracker/usage/dimension.rb +21 -0
- data/lib/llm_cost_tracker/{billing/components.yml → usage/dimensions.yml} +6 -52
- data/lib/llm_cost_tracker/usage/source.rb +14 -0
- data/lib/llm_cost_tracker/usage/token_usage.rb +45 -0
- data/lib/llm_cost_tracker/version.rb +1 -1
- data/lib/llm_cost_tracker.rb +43 -52
- data/lib/tasks/llm_cost_tracker.rake +14 -73
- metadata +81 -55
- data/app/controllers/llm_cost_tracker/reconciliation_controller.rb +0 -100
- data/app/helpers/llm_cost_tracker/dashboard_filter_helper.rb +0 -28
- data/app/helpers/llm_cost_tracker/reconciliation_helper.rb +0 -13
- data/app/models/llm_cost_tracker/provider_invoice.rb +0 -13
- data/app/models/llm_cost_tracker/provider_invoice_import.rb +0 -29
- data/app/views/llm_cost_tracker/reconciliation/index.html.erb +0 -174
- data/lib/llm_cost_tracker/billing/components.rb +0 -95
- data/lib/llm_cost_tracker/capture/stream.rb +0 -9
- data/lib/llm_cost_tracker/doctor/capture_verifier.rb +0 -61
- data/lib/llm_cost_tracker/doctor/check.rb +0 -7
- data/lib/llm_cost_tracker/doctor/cost_drift_check.rb +0 -56
- data/lib/llm_cost_tracker/doctor/invoice_reconciliation_check.rb +0 -164
- data/lib/llm_cost_tracker/doctor/legacy_audit_check.rb +0 -34
- data/lib/llm_cost_tracker/doctor/legacy_billing_status_check.rb +0 -20
- data/lib/llm_cost_tracker/doctor/pricing_snapshot_drift_check.rb +0 -85
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/reconciliation_generator.rb +0 -34
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/create_llm_cost_tracker_reconciliation.rb.erb +0 -60
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_provider_invoice_imports_provider.rb.erb +0 -36
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_provider_invoices_metadata_index.rb.erb +0 -27
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/upgrade_provider_invoice_imports_provider_generator.rb +0 -31
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/upgrade_provider_invoices_metadata_index_generator.rb +0 -31
- data/lib/llm_cost_tracker/ledger/rollups/upsert_sql.rb +0 -40
- data/lib/llm_cost_tracker/ledger/schema/ingestion_inbox_entries.rb +0 -57
- data/lib/llm_cost_tracker/ledger/schema/ingestion_leases.rb +0 -52
- data/lib/llm_cost_tracker/ledger/schema/provider_invoice_imports.rb +0 -56
- data/lib/llm_cost_tracker/ledger/schema/provider_invoices.rb +0 -72
- data/lib/llm_cost_tracker/masking.rb +0 -39
- data/lib/llm_cost_tracker/parsers/anthropic.rb +0 -176
- data/lib/llm_cost_tracker/parsers/azure.rb +0 -46
- data/lib/llm_cost_tracker/parsers/base.rb +0 -131
- data/lib/llm_cost_tracker/parsers/gemini.rb +0 -230
- data/lib/llm_cost_tracker/parsers/openai.rb +0 -41
- data/lib/llm_cost_tracker/parsers/openai_compatible.rb +0 -45
- data/lib/llm_cost_tracker/parsers/openai_usage.rb +0 -228
- data/lib/llm_cost_tracker/pricing/explainer.rb +0 -74
- data/lib/llm_cost_tracker/pricing/lookup.rb +0 -236
- data/lib/llm_cost_tracker/pricing/service_charges.rb +0 -206
- data/lib/llm_cost_tracker/providers/anthropic/server_tools.rb +0 -15
- data/lib/llm_cost_tracker/providers/anthropic/tier_classification.rb +0 -22
- data/lib/llm_cost_tracker/reconcile_tasks.rb +0 -131
- data/lib/llm_cost_tracker/reconciliation/diff.rb +0 -409
- data/lib/llm_cost_tracker/reconciliation/diff_result.rb +0 -44
- data/lib/llm_cost_tracker/reconciliation/import_result.rb +0 -19
- data/lib/llm_cost_tracker/reconciliation/importer.rb +0 -249
- data/lib/llm_cost_tracker/reconciliation/sources/anthropic_usage.rb +0 -148
- data/lib/llm_cost_tracker/reconciliation/sources/coercion.rb +0 -40
- data/lib/llm_cost_tracker/reconciliation/sources/fingerprint.rb +0 -20
- data/lib/llm_cost_tracker/reconciliation/sources/openai_usage.rb +0 -118
- data/lib/llm_cost_tracker/reconciliation.rb +0 -118
- data/lib/llm_cost_tracker/token_usage.rb +0 -93
|
@@ -1,37 +1,23 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "base"
|
|
4
|
-
require_relative "../
|
|
5
|
-
require_relative "../providers/anthropic/
|
|
6
|
-
require_relative "../providers/anthropic/tier_classification"
|
|
4
|
+
require_relative "../providers/anthropic/usage_extractor"
|
|
5
|
+
require_relative "../providers/anthropic/response_parser"
|
|
7
6
|
|
|
8
7
|
module LlmCostTracker
|
|
9
8
|
module Integrations
|
|
10
9
|
module Anthropic
|
|
11
10
|
extend Base
|
|
12
11
|
|
|
13
|
-
|
|
14
|
-
def integration_name
|
|
15
|
-
:anthropic
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
def minimum_version
|
|
19
|
-
"1.36.0"
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
def version_constant
|
|
23
|
-
"Anthropic::VERSION"
|
|
24
|
-
end
|
|
12
|
+
minimum_version "1.36.0"
|
|
25
13
|
|
|
14
|
+
class << self
|
|
26
15
|
def patch_targets
|
|
27
16
|
[
|
|
28
|
-
patch_target("Anthropic::Resources::Messages", with: MessagesPatch
|
|
29
|
-
patch_target(
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
methods: %i[create stream stream_raw],
|
|
33
|
-
optional: true
|
|
34
|
-
)
|
|
17
|
+
patch_target("Anthropic::Resources::Messages", with: MessagesPatch),
|
|
18
|
+
patch_target("Anthropic::Resources::Beta::Messages", with: MessagesPatch, optional: true),
|
|
19
|
+
patch_target("Anthropic::Resources::Messages::Batches", with: BatchesPatch, optional: true),
|
|
20
|
+
patch_target("Anthropic::Resources::Beta::Messages::Batches", with: BatchesPatch, optional: true)
|
|
35
21
|
]
|
|
36
22
|
end
|
|
37
23
|
|
|
@@ -39,122 +25,123 @@ module LlmCostTracker
|
|
|
39
25
|
return unless active?
|
|
40
26
|
|
|
41
27
|
record_safely do
|
|
42
|
-
usage =
|
|
28
|
+
usage = message.usage
|
|
43
29
|
next unless usage
|
|
30
|
+
next if usage.input_tokens.nil? && usage.output_tokens.nil?
|
|
44
31
|
|
|
45
|
-
|
|
46
|
-
output_tokens = object_value(usage, :output_tokens)
|
|
47
|
-
next if input_tokens.nil? && output_tokens.nil?
|
|
32
|
+
usage_hash = usage.deep_to_h
|
|
48
33
|
|
|
49
34
|
LlmCostTracker::Tracker.record(
|
|
50
|
-
event:
|
|
51
|
-
|
|
52
|
-
model:
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
provider_response_id: object_value(message, :id),
|
|
57
|
-
service_line_items: service_line_items_from(usage)
|
|
35
|
+
event: Providers::Anthropic::ResponseParser.event_from_usage(
|
|
36
|
+
usage: usage_hash,
|
|
37
|
+
model: message.model || request[:model],
|
|
38
|
+
provider_response_id: message.id,
|
|
39
|
+
usage_source: Usage::Source::SDK_RESPONSE,
|
|
40
|
+
request: request
|
|
58
41
|
),
|
|
59
42
|
latency_ms: latency_ms
|
|
60
43
|
)
|
|
61
44
|
end
|
|
62
45
|
end
|
|
63
46
|
|
|
64
|
-
def
|
|
65
|
-
|
|
66
|
-
return [] unless server_tool_use
|
|
47
|
+
def record_batch_result(response)
|
|
48
|
+
return unless active?
|
|
67
49
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
next if quantity.zero?
|
|
50
|
+
record_safely do
|
|
51
|
+
next unless response.respond_to?(:result) && response.result
|
|
71
52
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
53
|
+
result = response.result
|
|
54
|
+
next unless result.respond_to?(:type) && result.type.to_s == "succeeded"
|
|
55
|
+
|
|
56
|
+
message = result.respond_to?(:message) ? result.message : nil
|
|
57
|
+
next unless message
|
|
58
|
+
next if LlmCostTracker::Call.already_recorded?(provider: "anthropic", provider_response_id: message.id)
|
|
59
|
+
|
|
60
|
+
usage = message.usage
|
|
61
|
+
next unless usage
|
|
62
|
+
next if usage.input_tokens.nil? && usage.output_tokens.nil?
|
|
63
|
+
|
|
64
|
+
usage_hash = usage.deep_to_h
|
|
65
|
+
LlmCostTracker::Tracker.record(
|
|
66
|
+
event: Providers::Anthropic::ResponseParser.event_from_usage(
|
|
67
|
+
usage: usage_hash,
|
|
68
|
+
model: message.model,
|
|
69
|
+
provider_response_id: message.id,
|
|
70
|
+
usage_source: Usage::Source::SDK_BATCH_RESULT,
|
|
71
|
+
pricing_mode: "batch"
|
|
72
|
+
)
|
|
78
73
|
)
|
|
79
74
|
end
|
|
80
75
|
end
|
|
81
76
|
|
|
82
|
-
def
|
|
83
|
-
|
|
84
|
-
if cache_creation
|
|
85
|
-
cache_write_default = object_value(cache_creation, :ephemeral_5m_input_tokens).to_i
|
|
86
|
-
cache_write_extended = object_value(cache_creation, :ephemeral_1h_input_tokens).to_i
|
|
87
|
-
else
|
|
88
|
-
cache_write_default = object_value(usage, :cache_creation_input_tokens).to_i
|
|
89
|
-
cache_write_extended = 0
|
|
90
|
-
end
|
|
91
|
-
hidden_output = (
|
|
92
|
-
object_value(usage, :thinking_tokens, :thinking_output_tokens) ||
|
|
93
|
-
object_dig(usage, :output_tokens_details, :reasoning_tokens)
|
|
94
|
-
).to_i
|
|
95
|
-
|
|
96
|
-
TokenUsage.build(
|
|
97
|
-
input_tokens: input_tokens.to_i,
|
|
98
|
-
output_tokens: output_tokens.to_i,
|
|
99
|
-
cache_read_input_tokens: object_value(usage, :cache_read_input_tokens).to_i,
|
|
100
|
-
cache_write_input_tokens: cache_write_default,
|
|
101
|
-
cache_write_extended_input_tokens: cache_write_extended,
|
|
102
|
-
hidden_output_tokens: hidden_output
|
|
103
|
-
)
|
|
77
|
+
def stream_pricing_mode(request)
|
|
78
|
+
Providers::Anthropic::UsageExtractor.pricing_mode(request: request || {}, usage: nil)
|
|
104
79
|
end
|
|
80
|
+
end
|
|
105
81
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
modes = modes.compact.uniq
|
|
118
|
-
modes.empty? ? nil : modes.join("_")
|
|
82
|
+
module MessagesPatch
|
|
83
|
+
def create(*args, **kwargs)
|
|
84
|
+
LlmCostTracker::Integrations::Anthropic.wrap_blocking(
|
|
85
|
+
args,
|
|
86
|
+
kwargs,
|
|
87
|
+
record: lambda do |message, request, latency_ms|
|
|
88
|
+
LlmCostTracker::Integrations::Anthropic.record_message(
|
|
89
|
+
message, request: request, latency_ms: latency_ms
|
|
90
|
+
)
|
|
91
|
+
end
|
|
92
|
+
) { super }
|
|
119
93
|
end
|
|
120
94
|
|
|
121
|
-
def
|
|
122
|
-
|
|
95
|
+
def stream(*args, **kwargs)
|
|
96
|
+
LlmCostTracker::Integrations::Anthropic.wrap_stream(
|
|
97
|
+
args,
|
|
98
|
+
kwargs,
|
|
99
|
+
collector: ->(request) { LlmCostTracker::Integrations::Anthropic.stream_collector(request) }
|
|
100
|
+
) { super }
|
|
123
101
|
end
|
|
124
102
|
|
|
125
|
-
def
|
|
126
|
-
|
|
103
|
+
def stream_raw(*args, **kwargs)
|
|
104
|
+
LlmCostTracker::Integrations::Anthropic.wrap_stream(
|
|
105
|
+
args,
|
|
106
|
+
kwargs,
|
|
107
|
+
collector: ->(request) { LlmCostTracker::Integrations::Anthropic.stream_collector(request) }
|
|
108
|
+
) { super }
|
|
127
109
|
end
|
|
110
|
+
end
|
|
128
111
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
stream = yield
|
|
134
|
-
track_stream(stream, collector: collector)
|
|
135
|
-
end
|
|
112
|
+
module BatchesPatch
|
|
113
|
+
def results_streaming(*args, **kwargs)
|
|
114
|
+
raw = super
|
|
115
|
+
return raw unless LlmCostTracker::Integrations::Anthropic.active?
|
|
136
116
|
|
|
137
|
-
|
|
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
|
|
117
|
+
BatchResultsCapture.new(raw)
|
|
144
118
|
end
|
|
145
119
|
end
|
|
146
120
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
121
|
+
class BatchResultsCapture
|
|
122
|
+
include Enumerable
|
|
123
|
+
|
|
124
|
+
def initialize(raw_stream)
|
|
125
|
+
@raw_stream = raw_stream
|
|
150
126
|
end
|
|
151
127
|
|
|
152
|
-
def
|
|
153
|
-
|
|
128
|
+
def each(&block)
|
|
129
|
+
return enum_for(:each) unless block
|
|
130
|
+
|
|
131
|
+
@raw_stream.each do |response|
|
|
132
|
+
LlmCostTracker::Integrations::Anthropic.record_batch_result(response)
|
|
133
|
+
block.call(response)
|
|
134
|
+
end
|
|
154
135
|
end
|
|
155
136
|
|
|
156
|
-
def
|
|
157
|
-
|
|
137
|
+
def respond_to_missing?(name, include_private = false)
|
|
138
|
+
@raw_stream.respond_to?(name, include_private) || super
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def method_missing(name, ...)
|
|
142
|
+
return super unless @raw_stream.respond_to?(name)
|
|
143
|
+
|
|
144
|
+
@raw_stream.public_send(name, ...)
|
|
158
145
|
end
|
|
159
146
|
end
|
|
160
147
|
end
|
|
@@ -3,7 +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 "../
|
|
6
|
+
require_relative "../check"
|
|
7
7
|
require_relative "../logging"
|
|
8
8
|
require_relative "../timing"
|
|
9
9
|
require_relative "../capture/stream_collector"
|
|
@@ -12,7 +12,14 @@ require_relative "../capture/stream_tracker"
|
|
|
12
12
|
module LlmCostTracker
|
|
13
13
|
module Integrations
|
|
14
14
|
module Base
|
|
15
|
-
|
|
15
|
+
def integration_name
|
|
16
|
+
@integration_name ||= name.demodulize.underscore.to_sym
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def provider(value = nil)
|
|
20
|
+
@provider = value.to_s if value
|
|
21
|
+
@provider ||= integration_name.to_s
|
|
22
|
+
end
|
|
16
23
|
|
|
17
24
|
def active?
|
|
18
25
|
LlmCostTracker.configuration.instrumented?(integration_name)
|
|
@@ -30,22 +37,22 @@ module LlmCostTracker
|
|
|
30
37
|
name = integration_name.to_s
|
|
31
38
|
problems = version_problems + target_problems
|
|
32
39
|
if problems.any?
|
|
33
|
-
return
|
|
40
|
+
return Check.new(:warn, name, "#{name} integration cannot be installed: #{problems.join('; ')}")
|
|
34
41
|
end
|
|
35
42
|
|
|
36
43
|
installed = patch_targets.reject { |target| target.fetch(:optional) }.all? do |target|
|
|
37
44
|
target.fetch(:constant_name).to_s.safe_constantize&.ancestors&.include?(target.fetch(:patch))
|
|
38
45
|
end
|
|
39
|
-
return
|
|
46
|
+
return Check.new(:ok, name, "#{name} integration installed") if installed
|
|
40
47
|
|
|
41
|
-
|
|
48
|
+
Check.new(:warn, name, "#{name} integration is enabled but not installed")
|
|
42
49
|
end
|
|
43
50
|
|
|
44
|
-
def enforce_budget!(request:)
|
|
51
|
+
def enforce_budget!(request:, provider: self.provider)
|
|
45
52
|
return unless active?
|
|
46
53
|
|
|
47
|
-
LlmCostTracker::
|
|
48
|
-
provider:
|
|
54
|
+
LlmCostTracker::Budget.enforce!(
|
|
55
|
+
provider: provider,
|
|
49
56
|
model: request[:model],
|
|
50
57
|
request: request
|
|
51
58
|
)
|
|
@@ -71,10 +78,21 @@ module LlmCostTracker
|
|
|
71
78
|
kwargs.to_h.with_indifferent_access
|
|
72
79
|
end
|
|
73
80
|
|
|
74
|
-
def
|
|
75
|
-
|
|
81
|
+
def wrap_blocking(args, kwargs, record:, provider: self.provider)
|
|
82
|
+
request = request_params(args, kwargs)
|
|
83
|
+
enforce_budget!(request: request, provider: provider)
|
|
84
|
+
started_at = LlmCostTracker::Timing.now_monotonic
|
|
85
|
+
response = yield
|
|
86
|
+
record_safely { record.call(response, request, LlmCostTracker::Timing.elapsed_ms(started_at)) }
|
|
87
|
+
response
|
|
88
|
+
end
|
|
76
89
|
|
|
77
|
-
|
|
90
|
+
def wrap_stream(args, kwargs, collector:, provider: self.provider)
|
|
91
|
+
request = request_params(args, kwargs)
|
|
92
|
+
enforce_budget!(request: request, provider: provider)
|
|
93
|
+
built = collector.call(request)
|
|
94
|
+
stream = yield(built)
|
|
95
|
+
track_stream(stream, collector: built)
|
|
78
96
|
end
|
|
79
97
|
|
|
80
98
|
def track_stream(stream, collector:)
|
|
@@ -88,9 +106,9 @@ module LlmCostTracker
|
|
|
88
106
|
).wrap
|
|
89
107
|
end
|
|
90
108
|
|
|
91
|
-
def stream_collector(request)
|
|
109
|
+
def stream_collector(request, provider: self.provider)
|
|
92
110
|
LlmCostTracker::Capture::StreamCollector.new(
|
|
93
|
-
provider:
|
|
111
|
+
provider: provider,
|
|
94
112
|
model: request[:model],
|
|
95
113
|
pricing_mode: stream_pricing_mode(request),
|
|
96
114
|
request: request
|
|
@@ -101,56 +119,29 @@ module LlmCostTracker
|
|
|
101
119
|
nil
|
|
102
120
|
end
|
|
103
121
|
|
|
104
|
-
def
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
return value unless value.nil?
|
|
108
|
-
end
|
|
109
|
-
nil
|
|
122
|
+
def minimum_version(value = nil)
|
|
123
|
+
@minimum_version = value if value
|
|
124
|
+
@minimum_version
|
|
110
125
|
end
|
|
111
126
|
|
|
112
|
-
def
|
|
113
|
-
|
|
114
|
-
return nil if current.nil?
|
|
115
|
-
|
|
116
|
-
read_object_value(current, key)
|
|
117
|
-
end
|
|
127
|
+
def gem_version
|
|
128
|
+
Gem.loaded_specs[integration_name.to_s]&.version
|
|
118
129
|
end
|
|
119
130
|
|
|
120
|
-
def minimum_version = nil
|
|
121
|
-
|
|
122
|
-
def version_constant = nil
|
|
123
|
-
|
|
124
131
|
def patch_targets = []
|
|
125
132
|
|
|
126
|
-
def patch_target(constant_name, with:,
|
|
133
|
+
def patch_target(constant_name, with:, optional: false, skip_when_methods_missing: false)
|
|
127
134
|
{
|
|
128
135
|
constant_name: constant_name,
|
|
129
136
|
patch: with,
|
|
130
|
-
method_names:
|
|
137
|
+
method_names: with.instance_methods,
|
|
131
138
|
optional: optional,
|
|
132
139
|
skip_when_methods_missing: skip_when_methods_missing
|
|
133
140
|
}
|
|
134
141
|
end
|
|
135
142
|
|
|
136
|
-
module_function :object_value, :object_dig
|
|
137
|
-
|
|
138
143
|
private
|
|
139
144
|
|
|
140
|
-
def read_object_value(object, key)
|
|
141
|
-
return nil if object.nil?
|
|
142
|
-
|
|
143
|
-
if object.is_a?(Hash)
|
|
144
|
-
return object[key] if object.key?(key)
|
|
145
|
-
return object[key.name] if key.is_a?(Symbol) && object.key?(key.name)
|
|
146
|
-
end
|
|
147
|
-
|
|
148
|
-
object.public_send(key) if object.respond_to?(key)
|
|
149
|
-
end
|
|
150
|
-
|
|
151
|
-
module_function :read_object_value
|
|
152
|
-
private_class_method :read_object_value
|
|
153
|
-
|
|
154
145
|
def validate_contract!
|
|
155
146
|
problems = version_problems + target_problems
|
|
156
147
|
return if problems.empty?
|
|
@@ -162,22 +153,13 @@ module LlmCostTracker
|
|
|
162
153
|
return [] unless minimum_version
|
|
163
154
|
|
|
164
155
|
name = integration_name.to_s
|
|
165
|
-
version =
|
|
156
|
+
version = gem_version
|
|
166
157
|
return ["#{name} >= #{minimum_version} is required, but #{name} is not loaded"] unless version
|
|
167
158
|
return [] if version >= Gem::Version.new(minimum_version)
|
|
168
159
|
|
|
169
160
|
["#{name} >= #{minimum_version} is required, detected #{version}"]
|
|
170
161
|
end
|
|
171
162
|
|
|
172
|
-
def constant_version
|
|
173
|
-
return nil unless version_constant
|
|
174
|
-
|
|
175
|
-
value = version_constant.to_s.safe_constantize
|
|
176
|
-
value ? Gem::Version.new(value.to_s) : nil
|
|
177
|
-
rescue ArgumentError
|
|
178
|
-
nil
|
|
179
|
-
end
|
|
180
|
-
|
|
181
163
|
def target_problems
|
|
182
164
|
patch_targets.flat_map do |target|
|
|
183
165
|
constant_name = target.fetch(:constant_name)
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module LlmCostTracker
|
|
6
|
+
module Integrations
|
|
7
|
+
module Openai
|
|
8
|
+
module BatchCapture
|
|
9
|
+
DEDUP_LIMIT = 1024
|
|
10
|
+
MUTEX = Mutex.new
|
|
11
|
+
private_constant :DEDUP_LIMIT, :MUTEX
|
|
12
|
+
|
|
13
|
+
class << self
|
|
14
|
+
def maybe_capture(batch, resource:)
|
|
15
|
+
return unless Openai.active?
|
|
16
|
+
return unless batch.respond_to?(:status) && batch.status.to_s == "completed"
|
|
17
|
+
|
|
18
|
+
output_file_id = batch.respond_to?(:output_file_id) ? batch.output_file_id : nil
|
|
19
|
+
return unless output_file_id
|
|
20
|
+
|
|
21
|
+
batch_id = batch.respond_to?(:id) ? batch.id : nil
|
|
22
|
+
return unless batch_id && claim(batch_id)
|
|
23
|
+
|
|
24
|
+
client = resource.instance_variable_get(:@client)
|
|
25
|
+
host = Openai.client_host_for(resource)
|
|
26
|
+
Openai.record_safely do
|
|
27
|
+
io = client.files.content(output_file_id)
|
|
28
|
+
capture_jsonl(io.respond_to?(:read) ? io.read : io.to_s, host: host)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def claim(batch_id)
|
|
35
|
+
MUTEX.synchronize do
|
|
36
|
+
@dedup ||= Set.new
|
|
37
|
+
next false if @dedup.include?(batch_id)
|
|
38
|
+
|
|
39
|
+
@dedup.clear if @dedup.size >= DEDUP_LIMIT
|
|
40
|
+
@dedup.add(batch_id)
|
|
41
|
+
true
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def capture_jsonl(jsonl, host:)
|
|
46
|
+
jsonl.each_line do |line|
|
|
47
|
+
line = line.strip
|
|
48
|
+
next if line.empty?
|
|
49
|
+
|
|
50
|
+
entry = parse_line(line)
|
|
51
|
+
next unless entry
|
|
52
|
+
|
|
53
|
+
response = entry.dig("response", "body")
|
|
54
|
+
next unless response.is_a?(Hash) && response["usage"]
|
|
55
|
+
|
|
56
|
+
record_result(response, host: host)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def parse_line(line)
|
|
61
|
+
JSON.parse(line)
|
|
62
|
+
rescue JSON::ParserError
|
|
63
|
+
nil
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def record_result(response, host:)
|
|
67
|
+
provider = Openai.provider_for_host(host)
|
|
68
|
+
return if LlmCostTracker::Call.already_recorded?(provider: provider, provider_response_id: response["id"])
|
|
69
|
+
|
|
70
|
+
event = LlmCostTracker::Providers::Openai::ResponseParser.event_from_response(
|
|
71
|
+
response: response,
|
|
72
|
+
request: {},
|
|
73
|
+
provider: provider,
|
|
74
|
+
host: host,
|
|
75
|
+
usage_source: LlmCostTracker::Usage::Source::SDK_BATCH_RESULT,
|
|
76
|
+
pricing_mode: "batch"
|
|
77
|
+
)
|
|
78
|
+
LlmCostTracker::Tracker.record(event: event) if event
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LlmCostTracker
|
|
4
|
+
module Integrations
|
|
5
|
+
module Openai
|
|
6
|
+
module PatchBuilder
|
|
7
|
+
def self.build(record_method:, methods:)
|
|
8
|
+
Module.new.tap do |mod|
|
|
9
|
+
methods.each { |method_name| define_blocking_method(mod, method_name, record_method) }
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def self.build_stream(methods:)
|
|
14
|
+
Module.new.tap do |mod|
|
|
15
|
+
methods.each { |method_name| define_stream_method(mod, method_name) }
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def self.define_blocking_method(mod, method_name, record_method)
|
|
20
|
+
mod.define_method(method_name) do |*args, **kwargs, &block|
|
|
21
|
+
host = LlmCostTracker::Integrations::Openai.client_host_for(self)
|
|
22
|
+
LlmCostTracker::Integrations::Openai.wrap_blocking(
|
|
23
|
+
args,
|
|
24
|
+
kwargs,
|
|
25
|
+
provider: LlmCostTracker::Integrations::Openai.provider_for_host(host),
|
|
26
|
+
record: lambda do |response, request, latency_ms|
|
|
27
|
+
LlmCostTracker::Integrations::Openai.public_send(
|
|
28
|
+
record_method, response, request: request, latency_ms: latency_ms, host: host
|
|
29
|
+
)
|
|
30
|
+
end
|
|
31
|
+
) { super(*args, **kwargs, &block) }
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def self.define_stream_method(mod, method_name)
|
|
36
|
+
mod.define_method(method_name) do |*args, **kwargs|
|
|
37
|
+
LlmCostTracker::Integrations::Openai.wrap_stream(
|
|
38
|
+
args, kwargs, **LlmCostTracker::Integrations::Openai.stream_seam(self)
|
|
39
|
+
) { super(*args, **kwargs) }
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
module ResponsesPatch
|
|
45
|
+
include PatchBuilder.build(record_method: :record_response, methods: %i[create])
|
|
46
|
+
include PatchBuilder.build_stream(methods: %i[stream stream_raw])
|
|
47
|
+
|
|
48
|
+
def retrieve_streaming(response_id, *args, **kwargs)
|
|
49
|
+
LlmCostTracker::Integrations::Openai.wrap_stream(
|
|
50
|
+
args, kwargs, **LlmCostTracker::Integrations::Openai.stream_seam(self)
|
|
51
|
+
) do |collector|
|
|
52
|
+
collector.provider_response_id = response_id
|
|
53
|
+
super(response_id, *args, **kwargs)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
module ChatCompletionsPatch
|
|
59
|
+
include PatchBuilder.build(record_method: :record_response, methods: %i[create])
|
|
60
|
+
include PatchBuilder.build_stream(methods: %i[stream stream_raw])
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
EmbeddingsPatch = PatchBuilder.build(record_method: :record_response, methods: %i[create])
|
|
64
|
+
ImagesPatch = PatchBuilder.build(record_method: :record_image, methods: %i[generate edit create_variation])
|
|
65
|
+
TranscriptionsPatch = PatchBuilder.build(record_method: :record_transcription, methods: %i[create])
|
|
66
|
+
TranslationsPatch = PatchBuilder.build(record_method: :record_transcription, methods: %i[create])
|
|
67
|
+
SpeechPatch = PatchBuilder.build(record_method: :record_speech, methods: %i[create])
|
|
68
|
+
ModerationsPatch = PatchBuilder.build(record_method: :record_moderation, methods: %i[create])
|
|
69
|
+
StreamingImagesPatch = PatchBuilder.build_stream(methods: %i[generate_stream_raw edit_stream_raw])
|
|
70
|
+
StreamingTranscriptionsPatch = PatchBuilder.build_stream(methods: %i[create_streaming])
|
|
71
|
+
|
|
72
|
+
module BatchesPatch
|
|
73
|
+
def retrieve(batch_id, *args, **kwargs)
|
|
74
|
+
batch = super
|
|
75
|
+
LlmCostTracker::Integrations::Openai::BatchCapture.maybe_capture(batch, resource: self)
|
|
76
|
+
batch
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|