llm_cost_tracker 0.7.3 → 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/.ruby-version +1 -0
- data/CHANGELOG.md +173 -0
- data/README.md +60 -220
- data/app/assets/llm_cost_tracker/application.css +282 -45
- data/app/controllers/llm_cost_tracker/application_controller.rb +25 -20
- data/app/controllers/llm_cost_tracker/assets_controller.rb +11 -1
- data/app/controllers/llm_cost_tracker/calls_controller.rb +22 -19
- data/app/controllers/llm_cost_tracker/data_quality_controller.rb +14 -2
- 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 +18 -21
- data/app/helpers/llm_cost_tracker/dashboard_filter_helper.rb +3 -21
- data/app/helpers/llm_cost_tracker/dashboard_filter_options_helper.rb +4 -4
- data/app/helpers/llm_cost_tracker/dashboard_query_helper.rb +1 -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 +24 -7
- data/app/models/llm_cost_tracker/call.rb +166 -0
- data/app/models/llm_cost_tracker/call_line_item.rb +18 -0
- data/app/models/llm_cost_tracker/call_rollup.rb +6 -0
- data/app/models/llm_cost_tracker/call_tag.rb +12 -0
- data/app/models/llm_cost_tracker/ingestion/inbox_entry.rb +9 -0
- data/app/models/llm_cost_tracker/ingestion/lease.rb +0 -3
- data/app/models/llm_cost_tracker/provider_invoice.rb +13 -0
- data/app/models/llm_cost_tracker/provider_invoice_import.rb +24 -0
- data/app/services/llm_cost_tracker/dashboard/data_quality.rb +152 -32
- data/app/services/llm_cost_tracker/dashboard/date_range.rb +1 -1
- data/app/services/llm_cost_tracker/dashboard/filter.rb +8 -6
- data/app/services/llm_cost_tracker/dashboard/overview_stats.rb +74 -21
- data/app/services/llm_cost_tracker/dashboard/pagination.rb +6 -4
- data/app/services/llm_cost_tracker/dashboard/params.rb +8 -2
- data/app/services/llm_cost_tracker/dashboard/provider_breakdown.rb +1 -1
- data/app/services/llm_cost_tracker/dashboard/spend_anomaly.rb +4 -3
- data/app/services/llm_cost_tracker/dashboard/tag_breakdown.rb +42 -9
- data/app/services/llm_cost_tracker/dashboard/tag_key_explorer.rb +14 -37
- data/app/services/llm_cost_tracker/dashboard/time_series.rb +1 -1
- data/app/services/llm_cost_tracker/dashboard/top_models.rb +1 -1
- data/app/views/layouts/llm_cost_tracker/application.html.erb +6 -1
- data/app/views/llm_cost_tracker/calls/index.html.erb +33 -75
- data/app/views/llm_cost_tracker/calls/show.html.erb +73 -33
- data/app/views/llm_cost_tracker/dashboard/index.html.erb +16 -57
- data/app/views/llm_cost_tracker/data_quality/index.html.erb +183 -167
- data/app/views/llm_cost_tracker/errors/database.html.erb +1 -1
- data/app/views/llm_cost_tracker/models/index.html.erb +18 -50
- 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 +66 -0
- data/app/views/llm_cost_tracker/shared/_metric_stack.html.erb +1 -1
- data/app/views/llm_cost_tracker/shared/_sort.html.erb +13 -0
- data/app/views/llm_cost_tracker/shared/setup_required.html.erb +1 -1
- data/app/views/llm_cost_tracker/tags/index.html.erb +3 -34
- data/app/views/llm_cost_tracker/tags/show.html.erb +64 -36
- data/config/routes.rb +3 -2
- data/lib/llm_cost_tracker/billing/components.rb +95 -0
- data/lib/llm_cost_tracker/billing/components.yml +188 -0
- data/lib/llm_cost_tracker/billing/cost_status.rb +45 -0
- data/lib/llm_cost_tracker/billing/line_item.rb +189 -0
- data/lib/llm_cost_tracker/budget.rb +26 -36
- data/lib/llm_cost_tracker/capture/stream_collector.rb +125 -38
- data/lib/llm_cost_tracker/capture/stream_tracker.rb +40 -5
- data/lib/llm_cost_tracker/configuration.rb +86 -17
- data/lib/llm_cost_tracker/dashboard_setup_state.rb +109 -0
- data/lib/llm_cost_tracker/doctor/cost_drift_check.rb +56 -0
- data/lib/llm_cost_tracker/doctor/ingestion_check.rb +48 -30
- data/lib/llm_cost_tracker/doctor/invoice_reconciliation_check.rb +164 -0
- data/lib/llm_cost_tracker/doctor/legacy_audit_check.rb +36 -0
- data/lib/llm_cost_tracker/doctor/legacy_billing_status_check.rb +22 -0
- data/lib/llm_cost_tracker/doctor/price_check.rb +2 -2
- data/lib/llm_cost_tracker/doctor/pricing_snapshot_drift_check.rb +85 -0
- data/lib/llm_cost_tracker/doctor/probe.rb +17 -0
- data/lib/llm_cost_tracker/doctor/schema_check.rb +34 -0
- data/lib/llm_cost_tracker/doctor.rb +111 -44
- data/lib/llm_cost_tracker/engine.rb +9 -0
- data/lib/llm_cost_tracker/errors.rb +5 -19
- data/lib/llm_cost_tracker/event.rb +11 -3
- 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 +17 -5
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/prices_generator.rb +2 -6
- 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 +104 -0
- 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/{add_provider_response_id_generator.rb → upgrade_call_tags_key_value_index_generator.rb} +5 -4
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/{add_streaming_generator.rb → upgrade_image_tokens_generator.rb} +4 -4
- data/lib/llm_cost_tracker/ingestion/batch.rb +11 -12
- data/lib/llm_cost_tracker/ingestion/inbox.rb +39 -24
- data/lib/llm_cost_tracker/ingestion/inline.rb +22 -0
- data/lib/llm_cost_tracker/ingestion/worker.rb +24 -7
- data/lib/llm_cost_tracker/ingestion.rb +66 -22
- data/lib/llm_cost_tracker/integrations/anthropic.rb +68 -42
- data/lib/llm_cost_tracker/integrations/base.rb +56 -32
- data/lib/llm_cost_tracker/integrations/openai.rb +342 -63
- data/lib/llm_cost_tracker/integrations/ruby_llm.rb +110 -11
- data/lib/llm_cost_tracker/integrations.rb +21 -3
- data/lib/llm_cost_tracker/ledger/period/totals.rb +30 -11
- data/lib/llm_cost_tracker/ledger/period.rb +5 -5
- data/lib/llm_cost_tracker/ledger/rollups/upsert_sql.rb +2 -2
- data/lib/llm_cost_tracker/ledger/rollups.rb +90 -25
- data/lib/llm_cost_tracker/ledger/schema/adapter.rb +18 -0
- data/lib/llm_cost_tracker/ledger/schema/call_line_items.rb +79 -0
- data/lib/llm_cost_tracker/ledger/schema/call_rollups.rb +37 -0
- data/lib/llm_cost_tracker/ledger/schema/call_tags.rb +41 -0
- data/lib/llm_cost_tracker/ledger/schema/calls.rb +36 -23
- 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 +57 -0
- data/lib/llm_cost_tracker/ledger/store.rb +103 -20
- data/lib/llm_cost_tracker/ledger/tags/encoding.rb +37 -0
- data/lib/llm_cost_tracker/ledger/tags/query.rb +6 -11
- data/lib/llm_cost_tracker/ledger/tags/sql.rb +27 -15
- data/lib/llm_cost_tracker/ledger.rb +5 -2
- data/lib/llm_cost_tracker/logging.rb +2 -5
- data/lib/llm_cost_tracker/masking.rb +39 -0
- data/lib/llm_cost_tracker/middleware/faraday.rb +95 -35
- data/lib/llm_cost_tracker/parsers/anthropic.rb +74 -14
- data/lib/llm_cost_tracker/parsers/base.rb +13 -4
- data/lib/llm_cost_tracker/parsers/gemini.rb +105 -15
- data/lib/llm_cost_tracker/parsers/openai.rb +16 -2
- data/lib/llm_cost_tracker/parsers/openai_compatible.rb +15 -3
- data/lib/llm_cost_tracker/parsers/openai_service_charges.rb +126 -0
- data/lib/llm_cost_tracker/parsers/openai_usage.rb +157 -59
- data/lib/llm_cost_tracker/parsers/sse.rb +1 -1
- data/lib/llm_cost_tracker/parsers.rb +1 -1
- data/lib/llm_cost_tracker/prices.json +198 -22
- data/lib/llm_cost_tracker/pricing/effective_prices.rb +28 -21
- data/lib/llm_cost_tracker/pricing/explainer.rb +4 -5
- data/lib/llm_cost_tracker/pricing/lookup.rb +73 -36
- data/lib/llm_cost_tracker/pricing/mode.rb +76 -0
- data/lib/llm_cost_tracker/pricing/registry.rb +67 -45
- data/lib/llm_cost_tracker/pricing/service_charges.rb +210 -0
- data/lib/llm_cost_tracker/pricing/sync/fetcher.rb +26 -17
- data/lib/llm_cost_tracker/pricing/sync/registry_diff.rb +6 -15
- data/lib/llm_cost_tracker/pricing/sync/registry_writer.rb +50 -1
- data/lib/llm_cost_tracker/pricing/sync.rb +59 -10
- data/lib/llm_cost_tracker/pricing/sync_change_printer.rb +32 -0
- data/lib/llm_cost_tracker/pricing.rb +220 -28
- data/lib/llm_cost_tracker/railtie.rb +6 -8
- 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 +19 -8
- data/lib/llm_cost_tracker/report.rb +0 -4
- data/lib/llm_cost_tracker/retention.rb +22 -9
- data/lib/llm_cost_tracker/tags/context.rb +2 -5
- data/lib/llm_cost_tracker/tags/key.rb +4 -0
- data/lib/llm_cost_tracker/tags/sanitizer.rb +71 -20
- data/lib/llm_cost_tracker/timing.rb +15 -0
- data/lib/llm_cost_tracker/token_usage.rb +64 -42
- data/lib/llm_cost_tracker/tracker.rb +97 -27
- data/lib/llm_cost_tracker/usage_capture.rb +29 -8
- data/lib/llm_cost_tracker/version.rb +1 -1
- data/lib/llm_cost_tracker.rb +45 -35
- data/lib/tasks/llm_cost_tracker.rake +45 -17
- metadata +71 -41
- data/app/models/llm_cost_tracker/ingestion/event.rb +0 -13
- data/app/models/llm_cost_tracker/ledger/call.rb +0 -45
- data/app/models/llm_cost_tracker/ledger/call_metrics.rb +0 -66
- data/app/models/llm_cost_tracker/ledger/period/grouping.rb +0 -71
- data/app/models/llm_cost_tracker/ledger/period/total.rb +0 -13
- data/app/models/llm_cost_tracker/ledger/tags/accessors.rb +0 -19
- data/lib/llm_cost_tracker/configuration/instrumentation.rb +0 -33
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/add_ingestion_generator.rb +0 -29
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/add_latency_ms_generator.rb +0 -29
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/add_period_totals_generator.rb +0 -29
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/add_token_usage_generator.rb +0 -42
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/add_ingestion_to_llm_cost_tracker.rb.erb +0 -33
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/add_latency_ms_to_llm_api_calls.rb.erb +0 -9
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/add_period_totals_to_llm_cost_tracker.rb.erb +0 -104
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/add_provider_response_id_to_llm_api_calls.rb.erb +0 -15
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/add_streaming_to_llm_api_calls.rb.erb +0 -21
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/add_token_usage_to_llm_api_calls.rb.erb +0 -22
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/create_llm_api_calls.rb.erb +0 -83
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_llm_api_call_cost_precision.rb.erb +0 -26
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_llm_api_call_tags_to_jsonb.rb.erb +0 -44
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/upgrade_cost_precision_generator.rb +0 -29
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/upgrade_tags_to_jsonb_generator.rb +0 -29
- data/lib/llm_cost_tracker/ledger/rollups/batch.rb +0 -43
- data/lib/llm_cost_tracker/ledger/schema/period_totals.rb +0 -32
- data/lib/llm_cost_tracker/pricing/components.rb +0 -37
- data/lib/llm_cost_tracker/pricing/sync/registry_loader.rb +0 -63
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"metadata": {
|
|
3
|
-
"updated_at": "2026-05-
|
|
3
|
+
"updated_at": "2026-05-08",
|
|
4
4
|
"currency": "USD",
|
|
5
5
|
"unit": "1M tokens",
|
|
6
6
|
"source_urls": [
|
|
@@ -15,13 +15,26 @@
|
|
|
15
15
|
"schema_version": 1,
|
|
16
16
|
"min_gem_version": "0.4.0"
|
|
17
17
|
},
|
|
18
|
+
"service_charges": {
|
|
19
|
+
"anthropic": {
|
|
20
|
+
"web_search_request": 10.0,
|
|
21
|
+
"web_fetch_request": 0.0,
|
|
22
|
+
"code_execution_hour": 0.05
|
|
23
|
+
},
|
|
24
|
+
"openai": {
|
|
25
|
+
"web_search_request": 10.0,
|
|
26
|
+
"web_search_preview_request_reasoning": 10.0,
|
|
27
|
+
"web_search_preview_request_non_reasoning": 25.0,
|
|
28
|
+
"file_search_call": 2.5
|
|
29
|
+
}
|
|
30
|
+
},
|
|
18
31
|
"models": {
|
|
19
32
|
"anthropic/claude-haiku-4-5": {
|
|
20
33
|
"input": 1.0,
|
|
21
34
|
"output": 5.0,
|
|
22
35
|
"cache_read_input": 0.1,
|
|
23
36
|
"cache_write_input": 1.25,
|
|
24
|
-
"
|
|
37
|
+
"cache_write_extended_input": 2.0,
|
|
25
38
|
"batch_input": 0.5,
|
|
26
39
|
"batch_output": 2.5
|
|
27
40
|
},
|
|
@@ -30,7 +43,7 @@
|
|
|
30
43
|
"output": 75.0,
|
|
31
44
|
"cache_read_input": 1.5,
|
|
32
45
|
"cache_write_input": 18.75,
|
|
33
|
-
"
|
|
46
|
+
"cache_write_extended_input": 30.0,
|
|
34
47
|
"batch_input": 7.5,
|
|
35
48
|
"batch_output": 37.5
|
|
36
49
|
},
|
|
@@ -39,7 +52,7 @@
|
|
|
39
52
|
"output": 75.0,
|
|
40
53
|
"cache_read_input": 1.5,
|
|
41
54
|
"cache_write_input": 18.75,
|
|
42
|
-
"
|
|
55
|
+
"cache_write_extended_input": 30.0,
|
|
43
56
|
"batch_input": 7.5,
|
|
44
57
|
"batch_output": 37.5
|
|
45
58
|
},
|
|
@@ -48,7 +61,7 @@
|
|
|
48
61
|
"output": 25.0,
|
|
49
62
|
"cache_read_input": 0.5,
|
|
50
63
|
"cache_write_input": 6.25,
|
|
51
|
-
"
|
|
64
|
+
"cache_write_extended_input": 10.0,
|
|
52
65
|
"batch_input": 2.5,
|
|
53
66
|
"batch_output": 12.5
|
|
54
67
|
},
|
|
@@ -57,24 +70,24 @@
|
|
|
57
70
|
"output": 25.0,
|
|
58
71
|
"cache_read_input": 0.5,
|
|
59
72
|
"cache_write_input": 6.25,
|
|
60
|
-
"
|
|
73
|
+
"cache_write_extended_input": 10.0,
|
|
61
74
|
"batch_input": 2.5,
|
|
62
75
|
"batch_output": 12.5,
|
|
63
76
|
"data_residency_input": 5.5,
|
|
64
77
|
"data_residency_cache_write_input": 6.875,
|
|
65
|
-
"
|
|
78
|
+
"data_residency_cache_write_extended_input": 11.0,
|
|
66
79
|
"data_residency_cache_read_input": 0.55,
|
|
67
80
|
"data_residency_output": 27.5,
|
|
68
81
|
"data_residency_batch_input": 2.75,
|
|
69
82
|
"data_residency_batch_output": 13.75,
|
|
70
83
|
"fast_input": 30.0,
|
|
71
84
|
"fast_cache_write_input": 37.5,
|
|
72
|
-
"
|
|
85
|
+
"fast_cache_write_extended_input": 60.0,
|
|
73
86
|
"fast_cache_read_input": 3.0,
|
|
74
87
|
"fast_output": 150.0,
|
|
75
88
|
"fast_data_residency_input": 33.0,
|
|
76
89
|
"fast_data_residency_cache_write_input": 41.25,
|
|
77
|
-
"
|
|
90
|
+
"fast_data_residency_cache_write_extended_input": 66.0,
|
|
78
91
|
"fast_data_residency_cache_read_input": 3.3,
|
|
79
92
|
"fast_data_residency_output": 165.0
|
|
80
93
|
},
|
|
@@ -83,12 +96,12 @@
|
|
|
83
96
|
"output": 25.0,
|
|
84
97
|
"cache_read_input": 0.5,
|
|
85
98
|
"cache_write_input": 6.25,
|
|
86
|
-
"
|
|
99
|
+
"cache_write_extended_input": 10.0,
|
|
87
100
|
"batch_input": 2.5,
|
|
88
101
|
"batch_output": 12.5,
|
|
89
102
|
"data_residency_input": 5.5,
|
|
90
103
|
"data_residency_cache_write_input": 6.875,
|
|
91
|
-
"
|
|
104
|
+
"data_residency_cache_write_extended_input": 11.0,
|
|
92
105
|
"data_residency_cache_read_input": 0.55,
|
|
93
106
|
"data_residency_output": 27.5,
|
|
94
107
|
"data_residency_batch_input": 2.75,
|
|
@@ -99,7 +112,7 @@
|
|
|
99
112
|
"output": 15.0,
|
|
100
113
|
"cache_read_input": 0.3,
|
|
101
114
|
"cache_write_input": 3.75,
|
|
102
|
-
"
|
|
115
|
+
"cache_write_extended_input": 6.0,
|
|
103
116
|
"batch_input": 1.5,
|
|
104
117
|
"batch_output": 7.5
|
|
105
118
|
},
|
|
@@ -108,7 +121,7 @@
|
|
|
108
121
|
"output": 15.0,
|
|
109
122
|
"cache_read_input": 0.3,
|
|
110
123
|
"cache_write_input": 3.75,
|
|
111
|
-
"
|
|
124
|
+
"cache_write_extended_input": 6.0,
|
|
112
125
|
"batch_input": 1.5,
|
|
113
126
|
"batch_output": 7.5
|
|
114
127
|
},
|
|
@@ -117,12 +130,12 @@
|
|
|
117
130
|
"output": 15.0,
|
|
118
131
|
"cache_read_input": 0.3,
|
|
119
132
|
"cache_write_input": 3.75,
|
|
120
|
-
"
|
|
133
|
+
"cache_write_extended_input": 6.0,
|
|
121
134
|
"batch_input": 1.5,
|
|
122
135
|
"batch_output": 7.5,
|
|
123
136
|
"data_residency_input": 3.3,
|
|
124
137
|
"data_residency_cache_write_input": 4.125,
|
|
125
|
-
"
|
|
138
|
+
"data_residency_cache_write_extended_input": 6.6,
|
|
126
139
|
"data_residency_cache_read_input": 0.33,
|
|
127
140
|
"data_residency_output": 16.5,
|
|
128
141
|
"data_residency_batch_input": 1.65,
|
|
@@ -130,10 +143,12 @@
|
|
|
130
143
|
},
|
|
131
144
|
"gemini/gemini-2.0-flash": {
|
|
132
145
|
"input": 0.1,
|
|
133
|
-
"cache_read_input": 0.025,
|
|
134
146
|
"output": 0.4,
|
|
147
|
+
"audio_input": 0.7,
|
|
148
|
+
"cache_read_input": 0.025,
|
|
135
149
|
"batch_input": 0.05,
|
|
136
150
|
"batch_output": 0.2,
|
|
151
|
+
"batch_audio_input": 0.35,
|
|
137
152
|
"batch_cache_read_input": 0.025
|
|
138
153
|
},
|
|
139
154
|
"gemini/gemini-2.0-flash-lite": {
|
|
@@ -145,44 +160,52 @@
|
|
|
145
160
|
"gemini/gemini-2.5-flash": {
|
|
146
161
|
"input": 0.3,
|
|
147
162
|
"output": 2.5,
|
|
163
|
+
"audio_input": 1.0,
|
|
148
164
|
"cache_read_input": 0.03,
|
|
149
165
|
"batch_input": 0.15,
|
|
150
166
|
"batch_output": 1.25,
|
|
167
|
+
"batch_audio_input": 0.5,
|
|
151
168
|
"batch_cache_read_input": 0.03,
|
|
152
169
|
"flex_input": 0.15,
|
|
153
170
|
"flex_output": 1.25,
|
|
171
|
+
"flex_audio_input": 0.5,
|
|
154
172
|
"flex_cache_read_input": 0.03,
|
|
155
173
|
"priority_input": 0.54,
|
|
156
174
|
"priority_output": 4.5,
|
|
175
|
+
"priority_audio_input": 1.8,
|
|
157
176
|
"priority_cache_read_input": 0.054
|
|
158
177
|
},
|
|
159
178
|
"gemini/gemini-2.5-flash-lite": {
|
|
160
179
|
"input": 0.1,
|
|
161
180
|
"output": 0.4,
|
|
181
|
+
"audio_input": 0.3,
|
|
162
182
|
"cache_read_input": 0.01,
|
|
163
183
|
"batch_input": 0.05,
|
|
164
184
|
"batch_output": 0.2,
|
|
185
|
+
"batch_audio_input": 0.15,
|
|
165
186
|
"batch_cache_read_input": 0.01,
|
|
166
187
|
"flex_input": 0.05,
|
|
167
188
|
"flex_output": 0.2,
|
|
189
|
+
"flex_audio_input": 0.15,
|
|
168
190
|
"flex_cache_read_input": 0.01,
|
|
169
191
|
"priority_input": 0.18,
|
|
170
192
|
"priority_output": 0.72,
|
|
193
|
+
"priority_audio_input": 0.54,
|
|
171
194
|
"priority_cache_read_input": 0.018
|
|
172
195
|
},
|
|
173
196
|
"gemini/gemini-2.5-pro": {
|
|
174
197
|
"input": 1.25,
|
|
175
198
|
"output": 10.0,
|
|
176
|
-
"cache_read_input": 0.125,
|
|
177
|
-
"batch_input": 0.625,
|
|
178
|
-
"batch_output": 5.0,
|
|
179
|
-
"batch_cache_read_input": 0.125,
|
|
180
199
|
"_context_price_threshold_tokens": 200000,
|
|
181
200
|
"above_context_input": 2.5,
|
|
182
201
|
"above_context_output": 15.0,
|
|
202
|
+
"cache_read_input": 0.125,
|
|
183
203
|
"above_context_cache_read_input": 0.25,
|
|
204
|
+
"batch_input": 0.625,
|
|
205
|
+
"batch_output": 5.0,
|
|
184
206
|
"above_context_batch_input": 1.25,
|
|
185
207
|
"above_context_batch_output": 7.5,
|
|
208
|
+
"batch_cache_read_input": 0.125,
|
|
186
209
|
"above_context_batch_cache_read_input": 0.25,
|
|
187
210
|
"flex_input": 0.625,
|
|
188
211
|
"flex_output": 5.0,
|
|
@@ -309,6 +332,141 @@
|
|
|
309
332
|
"priority_output": 1.0,
|
|
310
333
|
"priority_cache_read_input": 0.125
|
|
311
334
|
},
|
|
335
|
+
"openai/gpt-4o-realtime-preview": {
|
|
336
|
+
"input": 5.0,
|
|
337
|
+
"cache_read_input": 2.5,
|
|
338
|
+
"audio_input": 40.0,
|
|
339
|
+
"output": 20.0,
|
|
340
|
+
"audio_output": 80.0
|
|
341
|
+
},
|
|
342
|
+
"openai/gpt-4o-mini-realtime-preview": {
|
|
343
|
+
"input": 0.6,
|
|
344
|
+
"cache_read_input": 0.3,
|
|
345
|
+
"audio_input": 10.0,
|
|
346
|
+
"output": 2.4,
|
|
347
|
+
"audio_output": 20.0
|
|
348
|
+
},
|
|
349
|
+
"openai/gpt-realtime": {
|
|
350
|
+
"input": 4.0,
|
|
351
|
+
"cache_read_input": 0.4,
|
|
352
|
+
"audio_input": 32.0,
|
|
353
|
+
"output": 16.0,
|
|
354
|
+
"audio_output": 64.0
|
|
355
|
+
},
|
|
356
|
+
"openai/gpt-realtime-1.5": {
|
|
357
|
+
"input": 4.0,
|
|
358
|
+
"cache_read_input": 0.4,
|
|
359
|
+
"audio_input": 32.0,
|
|
360
|
+
"output": 16.0,
|
|
361
|
+
"audio_output": 64.0
|
|
362
|
+
},
|
|
363
|
+
"openai/gpt-realtime-mini": {
|
|
364
|
+
"input": 0.6,
|
|
365
|
+
"cache_read_input": 0.06,
|
|
366
|
+
"audio_input": 10.0,
|
|
367
|
+
"output": 2.4,
|
|
368
|
+
"audio_output": 20.0
|
|
369
|
+
},
|
|
370
|
+
"openai/gpt-audio-1.5": {
|
|
371
|
+
"input": 2.5,
|
|
372
|
+
"audio_input": 32.0,
|
|
373
|
+
"output": 10.0,
|
|
374
|
+
"audio_output": 64.0
|
|
375
|
+
},
|
|
376
|
+
"openai/gpt-audio-mini": {
|
|
377
|
+
"input": 0.6,
|
|
378
|
+
"audio_input": 10.0,
|
|
379
|
+
"output": 2.4,
|
|
380
|
+
"audio_output": 20.0
|
|
381
|
+
},
|
|
382
|
+
"openai/gpt-audio": {
|
|
383
|
+
"input": 2.5,
|
|
384
|
+
"audio_input": 32.0,
|
|
385
|
+
"output": 10.0,
|
|
386
|
+
"audio_output": 64.0
|
|
387
|
+
},
|
|
388
|
+
"openai/gpt-4o-audio-preview": {
|
|
389
|
+
"input": 2.5,
|
|
390
|
+
"audio_input": 40.0,
|
|
391
|
+
"output": 10.0,
|
|
392
|
+
"audio_output": 80.0
|
|
393
|
+
},
|
|
394
|
+
"openai/gpt-4o-mini-audio-preview": {
|
|
395
|
+
"input": 0.15,
|
|
396
|
+
"audio_input": 10.0,
|
|
397
|
+
"output": 0.6,
|
|
398
|
+
"audio_output": 20.0
|
|
399
|
+
},
|
|
400
|
+
"openai/text-embedding-3-small": {
|
|
401
|
+
"input": 0.02,
|
|
402
|
+
"batch_input": 0.01
|
|
403
|
+
},
|
|
404
|
+
"openai/text-embedding-3-large": {
|
|
405
|
+
"input": 0.13,
|
|
406
|
+
"batch_input": 0.065
|
|
407
|
+
},
|
|
408
|
+
"openai/text-embedding-ada-002": {
|
|
409
|
+
"input": 0.10,
|
|
410
|
+
"batch_input": 0.05
|
|
411
|
+
},
|
|
412
|
+
"openai/gpt-4o-transcribe": {
|
|
413
|
+
"input": 2.5,
|
|
414
|
+
"audio_input": 6.0,
|
|
415
|
+
"output": 10.0
|
|
416
|
+
},
|
|
417
|
+
"openai/gpt-4o-mini-transcribe": {
|
|
418
|
+
"input": 1.25,
|
|
419
|
+
"audio_input": 3.0,
|
|
420
|
+
"output": 5.0
|
|
421
|
+
},
|
|
422
|
+
"openai/tts-1": {
|
|
423
|
+
"text_to_speech_character": 15.0
|
|
424
|
+
},
|
|
425
|
+
"openai/tts-1-hd": {
|
|
426
|
+
"text_to_speech_character": 30.0
|
|
427
|
+
},
|
|
428
|
+
"openai/gpt-image-1": {
|
|
429
|
+
"input": 5.0,
|
|
430
|
+
"cache_read_input": 1.25,
|
|
431
|
+
"image_input": 10.0,
|
|
432
|
+
"image_output": 40.0,
|
|
433
|
+
"batch_input": 2.5,
|
|
434
|
+
"batch_cache_read_input": 0.63,
|
|
435
|
+
"batch_image_input": 5.0,
|
|
436
|
+
"batch_image_output": 20.0
|
|
437
|
+
},
|
|
438
|
+
"openai/gpt-image-1-mini": {
|
|
439
|
+
"input": 2.0,
|
|
440
|
+
"cache_read_input": 0.2,
|
|
441
|
+
"image_input": 2.5,
|
|
442
|
+
"image_output": 8.0,
|
|
443
|
+
"batch_input": 1.0,
|
|
444
|
+
"batch_cache_read_input": 0.1,
|
|
445
|
+
"batch_image_input": 1.25,
|
|
446
|
+
"batch_image_output": 4.0
|
|
447
|
+
},
|
|
448
|
+
"openai/gpt-image-1.5": {
|
|
449
|
+
"input": 5.0,
|
|
450
|
+
"cache_read_input": 1.25,
|
|
451
|
+
"output": 10.0,
|
|
452
|
+
"image_input": 8.0,
|
|
453
|
+
"image_output": 32.0,
|
|
454
|
+
"batch_input": 2.5,
|
|
455
|
+
"batch_cache_read_input": 0.63,
|
|
456
|
+
"batch_output": 5.0,
|
|
457
|
+
"batch_image_input": 4.0,
|
|
458
|
+
"batch_image_output": 16.0
|
|
459
|
+
},
|
|
460
|
+
"openai/gpt-image-2": {
|
|
461
|
+
"input": 5.0,
|
|
462
|
+
"cache_read_input": 1.25,
|
|
463
|
+
"image_input": 8.0,
|
|
464
|
+
"image_output": 30.0,
|
|
465
|
+
"batch_input": 2.5,
|
|
466
|
+
"batch_cache_read_input": 0.625,
|
|
467
|
+
"batch_image_input": 4.0,
|
|
468
|
+
"batch_image_output": 15.0
|
|
469
|
+
},
|
|
312
470
|
"openai/gpt-5": {
|
|
313
471
|
"input": 1.25,
|
|
314
472
|
"output": 10.0,
|
|
@@ -672,7 +830,7 @@
|
|
|
672
830
|
"anthropic/claude-haiku-3-5": {
|
|
673
831
|
"input": 0.8,
|
|
674
832
|
"cache_write_input": 1.0,
|
|
675
|
-
"
|
|
833
|
+
"cache_write_extended_input": 1.6,
|
|
676
834
|
"cache_read_input": 0.08,
|
|
677
835
|
"output": 4.0,
|
|
678
836
|
"batch_input": 0.4,
|
|
@@ -681,7 +839,7 @@
|
|
|
681
839
|
"anthropic/claude-haiku-3": {
|
|
682
840
|
"input": 0.25,
|
|
683
841
|
"cache_write_input": 0.3,
|
|
684
|
-
"
|
|
842
|
+
"cache_write_extended_input": 0.5,
|
|
685
843
|
"cache_read_input": 0.03,
|
|
686
844
|
"output": 1.25,
|
|
687
845
|
"batch_input": 0.125,
|
|
@@ -720,6 +878,24 @@
|
|
|
720
878
|
"input": 1.5,
|
|
721
879
|
"output": 6.0,
|
|
722
880
|
"cache_read_input": 0.375
|
|
881
|
+
},
|
|
882
|
+
"gemini/gemini-3.1-flash-lite": {
|
|
883
|
+
"input": 0.25,
|
|
884
|
+
"output": 1.5,
|
|
885
|
+
"audio_input": 0.5,
|
|
886
|
+
"cache_read_input": 0.025,
|
|
887
|
+
"batch_input": 0.125,
|
|
888
|
+
"batch_output": 0.75,
|
|
889
|
+
"batch_audio_input": 0.25,
|
|
890
|
+
"batch_cache_read_input": 0.0125,
|
|
891
|
+
"flex_input": 0.125,
|
|
892
|
+
"flex_output": 0.75,
|
|
893
|
+
"flex_audio_input": 0.25,
|
|
894
|
+
"flex_cache_read_input": 0.0125,
|
|
895
|
+
"priority_input": 0.45,
|
|
896
|
+
"priority_output": 2.7,
|
|
897
|
+
"priority_audio_input": 0.9,
|
|
898
|
+
"priority_cache_read_input": 0.045
|
|
723
899
|
}
|
|
724
900
|
}
|
|
725
901
|
}
|
|
@@ -1,23 +1,24 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative "components"
|
|
3
|
+
require_relative "../billing/components"
|
|
4
|
+
require_relative "mode"
|
|
4
5
|
|
|
5
6
|
module LlmCostTracker
|
|
6
7
|
module Pricing
|
|
7
8
|
module EffectivePrices
|
|
8
9
|
class << self
|
|
9
10
|
def call(usage:, prices:, pricing_mode:)
|
|
10
|
-
quantities = usage.price_quantities
|
|
11
11
|
context_tier = context_tier?(usage: usage, prices: prices)
|
|
12
|
+
orderings = pricing_mode && Mode.parse(pricing_mode).permutations
|
|
12
13
|
|
|
13
|
-
|
|
14
|
-
price_key = component.
|
|
15
|
-
tokens =
|
|
14
|
+
Billing::Components::TOKEN_PRICED.to_h do |component|
|
|
15
|
+
price_key = component.key
|
|
16
|
+
tokens = usage.public_send(component.token_key)
|
|
16
17
|
price = if tokens.positive?
|
|
17
18
|
price_for(
|
|
18
19
|
prices: prices,
|
|
19
20
|
key: price_key,
|
|
20
|
-
|
|
21
|
+
orderings: orderings,
|
|
21
22
|
context_tier: context_tier
|
|
22
23
|
)
|
|
23
24
|
else
|
|
@@ -29,12 +30,16 @@ module LlmCostTracker
|
|
|
29
30
|
|
|
30
31
|
private
|
|
31
32
|
|
|
32
|
-
def price_for(prices:, key:,
|
|
33
|
-
|
|
34
|
-
return contextual_price(prices: prices, key: key, context_tier: context_tier) unless mode
|
|
33
|
+
def price_for(prices:, key:, orderings:, context_tier:)
|
|
34
|
+
return contextual_price(prices: prices, key: key, context_tier: context_tier) unless orderings
|
|
35
35
|
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
orderings.each do |mode|
|
|
37
|
+
direct = contextual_price(prices: prices, key: :"#{mode}_#{key}", context_tier: context_tier)
|
|
38
|
+
return direct if direct
|
|
39
|
+
end
|
|
40
|
+
return nil if %i[input output].include?(key)
|
|
41
|
+
|
|
42
|
+
derived_mode_price(prices: prices, key: key, modes: orderings, context_tier: context_tier)
|
|
38
43
|
end
|
|
39
44
|
|
|
40
45
|
def contextual_price(prices:, key:, context_tier:)
|
|
@@ -43,16 +48,17 @@ module LlmCostTracker
|
|
|
43
48
|
prices[:"above_context_#{key}"]
|
|
44
49
|
end
|
|
45
50
|
|
|
46
|
-
def derived_mode_price(prices:, key:,
|
|
51
|
+
def derived_mode_price(prices:, key:, modes:, context_tier:)
|
|
47
52
|
standard_price = contextual_price(prices: prices, key: key, context_tier: context_tier)
|
|
48
|
-
|
|
53
|
+
base_price = contextual_price(prices: prices, key: :input, context_tier: context_tier)
|
|
54
|
+
return nil unless standard_price && base_price
|
|
55
|
+
return nil if base_price.zero?
|
|
49
56
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
standard_price * (mode_base_price.to_f / base_price)
|
|
57
|
+
modes.each do |mode|
|
|
58
|
+
mode_base_price = contextual_price(prices: prices, key: :"#{mode}_input", context_tier: context_tier)
|
|
59
|
+
return standard_price * (mode_base_price / base_price) if mode_base_price
|
|
60
|
+
end
|
|
61
|
+
nil
|
|
56
62
|
end
|
|
57
63
|
|
|
58
64
|
def context_tier?(usage:, prices:)
|
|
@@ -62,8 +68,9 @@ module LlmCostTracker
|
|
|
62
68
|
input_tokens = usage.input_tokens +
|
|
63
69
|
usage.cache_read_input_tokens +
|
|
64
70
|
usage.cache_write_input_tokens +
|
|
65
|
-
usage.
|
|
66
|
-
|
|
71
|
+
usage.cache_write_extended_input_tokens +
|
|
72
|
+
usage.audio_input_tokens
|
|
73
|
+
input_tokens > threshold
|
|
67
74
|
end
|
|
68
75
|
end
|
|
69
76
|
end
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "../token_usage"
|
|
3
4
|
require_relative "effective_prices"
|
|
4
5
|
|
|
5
6
|
module LlmCostTracker
|
|
@@ -33,7 +34,7 @@ module LlmCostTracker
|
|
|
33
34
|
|
|
34
35
|
module Explainer
|
|
35
36
|
class << self
|
|
36
|
-
def call(provider:, model:,
|
|
37
|
+
def call(provider:, model:, tokens:, pricing_mode: nil)
|
|
37
38
|
match = Lookup.call(provider: provider, model: model)
|
|
38
39
|
|
|
39
40
|
explanation(
|
|
@@ -41,7 +42,7 @@ module LlmCostTracker
|
|
|
41
42
|
model: model,
|
|
42
43
|
pricing_mode: pricing_mode,
|
|
43
44
|
match: match,
|
|
44
|
-
usage:
|
|
45
|
+
usage: TokenUsage.build_from_tokens(tokens)
|
|
45
46
|
)
|
|
46
47
|
end
|
|
47
48
|
|
|
@@ -50,9 +51,7 @@ module LlmCostTracker
|
|
|
50
51
|
def explanation(provider:, model:, pricing_mode:, match:, usage:)
|
|
51
52
|
prices = match&.prices
|
|
52
53
|
pricing_mode = Pricing.normalize_mode(pricing_mode)
|
|
53
|
-
effective =
|
|
54
|
-
EffectivePrices.call(usage: usage, prices: prices, pricing_mode: pricing_mode)
|
|
55
|
-
end
|
|
54
|
+
effective = EffectivePrices.call(usage: usage, prices: prices, pricing_mode: pricing_mode) if prices
|
|
56
55
|
|
|
57
56
|
Explanation.new(
|
|
58
57
|
provider: provider.to_s,
|
|
@@ -7,55 +7,89 @@ module LlmCostTracker
|
|
|
7
7
|
MUTEX = Mutex.new
|
|
8
8
|
CACHE_MISS = Object.new.freeze
|
|
9
9
|
NO_MATCH = Object.new.freeze
|
|
10
|
-
|
|
10
|
+
LOOKUP_CACHE_LIMIT = 2_048
|
|
11
|
+
PRICE_FILE_RECHECK_INTERVAL = 1.0
|
|
12
|
+
private_constant :PRICE_FILE_RECHECK_INTERVAL
|
|
11
13
|
|
|
12
14
|
class << self
|
|
13
15
|
def call(provider:, model:)
|
|
14
16
|
provider_name = provider.to_s.presence
|
|
15
17
|
model_name = model.to_s
|
|
18
|
+
return nil if model_name.empty?
|
|
19
|
+
|
|
20
|
+
invalidate_cache_if_prices_file_changed!
|
|
21
|
+
|
|
16
22
|
cache_key = [provider_name, model_name]
|
|
17
23
|
cached = cached_lookup(cache_key)
|
|
18
24
|
return cached unless cached.equal?(CACHE_MISS)
|
|
19
25
|
|
|
26
|
+
match = lookup_match(provider_name: provider_name, model_name: model_name)
|
|
27
|
+
cache_lookup(cache_key, match)
|
|
28
|
+
match
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def reset!
|
|
32
|
+
MUTEX.synchronize do
|
|
33
|
+
reset_prices_caches!(signature: nil)
|
|
34
|
+
@prices_file_last_check_at = nil
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def invalidate_cache_if_prices_file_changed!
|
|
41
|
+
path = LlmCostTracker.configuration.prices_file
|
|
42
|
+
|
|
43
|
+
unless path
|
|
44
|
+
return if @prices_file_signature.nil?
|
|
45
|
+
|
|
46
|
+
MUTEX.synchronize { reset_prices_caches!(signature: nil) unless @prices_file_signature.nil? }
|
|
47
|
+
return
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
51
|
+
last_check = @prices_file_last_check_at
|
|
52
|
+
return if last_check && (now - last_check) < PRICE_FILE_RECHECK_INTERVAL
|
|
53
|
+
|
|
54
|
+
signature = File.exist?(path) ? File.mtime(path) : nil
|
|
55
|
+
MUTEX.synchronize do
|
|
56
|
+
@prices_file_last_check_at = now
|
|
57
|
+
reset_prices_caches!(signature: signature) if @prices_file_signature != signature
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def reset_prices_caches!(signature:)
|
|
62
|
+
@prices_cache = nil
|
|
63
|
+
@lookup_cache = nil
|
|
64
|
+
@sorted_price_keys_cache = nil
|
|
65
|
+
@prices_file_signature = signature
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def lookup_match(provider_name:, model_name:)
|
|
20
69
|
provider_model = provider_name ? "#{provider_name}/#{model_name}" : model_name
|
|
21
70
|
normalized_model = normalize_model_name(model_name)
|
|
22
71
|
current = current_price_tables
|
|
23
72
|
|
|
24
|
-
|
|
25
|
-
explain_table(
|
|
26
|
-
table:
|
|
27
|
-
source:
|
|
28
|
-
provider_model: provider_model,
|
|
29
|
-
model_name: model_name,
|
|
30
|
-
normalized_model: normalized_model
|
|
31
|
-
) ||
|
|
32
|
-
explain_table(
|
|
33
|
-
table: current.fetch(:file_prices),
|
|
34
|
-
source: :prices_file,
|
|
35
|
-
provider_model: provider_model,
|
|
36
|
-
model_name: model_name,
|
|
37
|
-
normalized_model: normalized_model
|
|
38
|
-
) ||
|
|
39
|
-
explain_table(
|
|
40
|
-
table: Registry.builtin_prices,
|
|
41
|
-
source: :bundled,
|
|
73
|
+
ordered_table_lookups(current).each do |source, table|
|
|
74
|
+
match = explain_table(
|
|
75
|
+
table: table,
|
|
76
|
+
source: source,
|
|
42
77
|
provider_model: provider_model,
|
|
43
78
|
model_name: model_name,
|
|
44
79
|
normalized_model: normalized_model
|
|
45
80
|
)
|
|
46
|
-
|
|
47
|
-
match
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
def reset!
|
|
51
|
-
MUTEX.synchronize do
|
|
52
|
-
@prices_cache = nil
|
|
53
|
-
@lookup_cache = nil
|
|
54
|
-
@sorted_price_keys_cache = nil
|
|
81
|
+
return match if match
|
|
55
82
|
end
|
|
83
|
+
nil
|
|
56
84
|
end
|
|
57
85
|
|
|
58
|
-
|
|
86
|
+
def ordered_table_lookups(current)
|
|
87
|
+
[
|
|
88
|
+
[:pricing_overrides, current.fetch(:pricing_overrides)],
|
|
89
|
+
[:prices_file, current.fetch(:file_prices)],
|
|
90
|
+
[:bundled, Registry.builtin_prices]
|
|
91
|
+
]
|
|
92
|
+
end
|
|
59
93
|
|
|
60
94
|
def current_price_tables
|
|
61
95
|
cached = @prices_cache
|
|
@@ -67,8 +101,7 @@ module LlmCostTracker
|
|
|
67
101
|
|
|
68
102
|
config = LlmCostTracker.configuration
|
|
69
103
|
file_prices = Registry.file_prices(config.prices_file)
|
|
70
|
-
|
|
71
|
-
value = { pricing_overrides: overrides, file_prices: file_prices }.freeze
|
|
104
|
+
value = { pricing_overrides: config.pricing_overrides, file_prices: file_prices }.freeze
|
|
72
105
|
@prices_cache = value
|
|
73
106
|
value
|
|
74
107
|
end
|
|
@@ -85,7 +118,7 @@ module LlmCostTracker
|
|
|
85
118
|
def cache_lookup(cache_key, match)
|
|
86
119
|
MUTEX.synchronize do
|
|
87
120
|
values = (@lookup_cache || {}).dup
|
|
88
|
-
values.
|
|
121
|
+
values.shift while values.size >= LOOKUP_CACHE_LIMIT
|
|
89
122
|
values[cache_key] = match || NO_MATCH
|
|
90
123
|
@lookup_cache = values.freeze
|
|
91
124
|
end
|
|
@@ -135,7 +168,7 @@ module LlmCostTracker
|
|
|
135
168
|
end
|
|
136
169
|
|
|
137
170
|
def match(table:, source:, key:, matched_by:)
|
|
138
|
-
Match.new(source: source
|
|
171
|
+
Match.new(source: source, key: key, prices: table[key], matched_by: matched_by)
|
|
139
172
|
end
|
|
140
173
|
|
|
141
174
|
def snapshot_variant?(model, key)
|
|
@@ -147,14 +180,18 @@ module LlmCostTracker
|
|
|
147
180
|
|
|
148
181
|
def sorted_price_keys(table)
|
|
149
182
|
cached = @sorted_price_keys_cache
|
|
150
|
-
|
|
183
|
+
existing = cached && cached[table]
|
|
184
|
+
return existing if existing
|
|
151
185
|
|
|
152
186
|
MUTEX.synchronize do
|
|
153
187
|
cached = @sorted_price_keys_cache
|
|
154
|
-
|
|
188
|
+
existing = cached && cached[table]
|
|
189
|
+
return existing if existing
|
|
155
190
|
|
|
156
191
|
keys = table.keys.sort_by { |key| -key.length }
|
|
157
|
-
|
|
192
|
+
next_cache = cached ? cached.dup : {}.compare_by_identity
|
|
193
|
+
next_cache[table] = keys
|
|
194
|
+
@sorted_price_keys_cache = next_cache.freeze
|
|
158
195
|
keys
|
|
159
196
|
end
|
|
160
197
|
end
|