llm_cost_tracker 0.9.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +55 -0
- data/README.md +6 -2
- data/app/assets/llm_cost_tracker/application.css +782 -801
- data/app/controllers/llm_cost_tracker/application_controller.rb +15 -3
- data/app/controllers/llm_cost_tracker/calls_controller.rb +39 -20
- data/app/controllers/llm_cost_tracker/dashboard_controller.rb +0 -3
- data/app/controllers/llm_cost_tracker/models_controller.rb +3 -1
- data/app/controllers/llm_cost_tracker/pricing_controller.rb +16 -0
- data/app/controllers/llm_cost_tracker/reconciliation_controller.rb +13 -19
- data/app/controllers/llm_cost_tracker/tags_controller.rb +3 -1
- data/app/helpers/llm_cost_tracker/application_helper.rb +16 -4
- data/app/helpers/llm_cost_tracker/chart_helper.rb +22 -6
- data/app/helpers/llm_cost_tracker/sortable_table_helper.rb +41 -0
- data/app/models/llm_cost_tracker/provider_invoice_import.rb +9 -4
- data/app/services/llm_cost_tracker/dashboard/pricing_overview.rb +95 -0
- data/app/services/llm_cost_tracker/dashboard/setup_state.rb +104 -0
- data/app/services/llm_cost_tracker/dashboard/sort.rb +9 -0
- data/app/services/llm_cost_tracker/dashboard/tag_breakdown.rb +19 -5
- data/app/services/llm_cost_tracker/dashboard/top_models.rb +34 -19
- data/app/views/layouts/llm_cost_tracker/application.html.erb +80 -17
- data/app/views/llm_cost_tracker/calls/index.html.erb +69 -90
- data/app/views/llm_cost_tracker/calls/show.html.erb +119 -120
- data/app/views/llm_cost_tracker/dashboard/index.html.erb +119 -158
- data/app/views/llm_cost_tracker/data_quality/index.html.erb +109 -108
- data/app/views/llm_cost_tracker/errors/database.html.erb +2 -2
- data/app/views/llm_cost_tracker/models/index.html.erb +39 -59
- data/app/views/llm_cost_tracker/pricing/index.html.erb +93 -0
- data/app/views/llm_cost_tracker/reconciliation/index.html.erb +49 -58
- data/app/views/llm_cost_tracker/shared/_filter_pill_date.html.erb +19 -0
- data/app/views/llm_cost_tracker/shared/_filter_pill_model.html.erb +22 -0
- data/app/views/llm_cost_tracker/shared/_filter_pill_provider.html.erb +22 -0
- data/app/views/llm_cost_tracker/shared/_filter_pill_stream.html.erb +23 -0
- data/app/views/llm_cost_tracker/shared/_spend_chart.html.erb +3 -13
- data/app/views/llm_cost_tracker/shared/_tag_chips.html.erb +1 -1
- data/app/views/llm_cost_tracker/shared/setup_required.html.erb +16 -15
- data/app/views/llm_cost_tracker/tags/index.html.erb +27 -32
- data/app/views/llm_cost_tracker/tags/show.html.erb +83 -102
- data/config/routes.rb +1 -0
- data/lib/llm_cost_tracker/billing/cost_status.rb +21 -25
- data/lib/llm_cost_tracker/billing/line_item.rb +15 -49
- data/lib/llm_cost_tracker/budget.rb +29 -8
- data/lib/llm_cost_tracker/{parsers → capture}/sse.rb +1 -1
- data/lib/llm_cost_tracker/capture/stream_collector.rb +34 -42
- data/lib/llm_cost_tracker/capture/stream_tracker.rb +2 -6
- data/lib/llm_cost_tracker/configuration.rb +30 -44
- data/lib/llm_cost_tracker/doctor/capture_verifier.rb +1 -1
- data/lib/llm_cost_tracker/doctor/ingestion_check.rb +8 -8
- data/lib/llm_cost_tracker/doctor/legacy_audit_check.rb +0 -2
- data/lib/llm_cost_tracker/doctor/legacy_billing_status_check.rb +0 -2
- data/lib/llm_cost_tracker/doctor.rb +80 -25
- data/lib/llm_cost_tracker/engine.rb +1 -2
- data/lib/llm_cost_tracker/errors.rb +3 -2
- data/lib/llm_cost_tracker/event.rb +47 -0
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/{durable_ingestion_generator.rb → async_ingestion_generator.rb} +8 -8
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/install_generator.rb +4 -23
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/{create_llm_cost_tracker_durable_ingestion.rb.erb → create_llm_cost_tracker_async_ingestion.rb.erb} +3 -3
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/create_llm_cost_tracker_reconciliation.rb.erb +6 -1
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/initializer.rb.erb +14 -7
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_call_rollups_provider.rb.erb +27 -8
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_call_tags_key_value_index.rb.erb +5 -5
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_provider_invoice_imports_provider.rb.erb +36 -0
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_provider_invoices_metadata_index.rb.erb +27 -0
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/upgrade_call_rollups_provider_generator.rb +0 -9
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/upgrade_provider_invoice_imports_provider_generator.rb +31 -0
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/upgrade_provider_invoices_metadata_index_generator.rb +31 -0
- data/lib/llm_cost_tracker/ingestion/batch.rb +5 -2
- data/lib/llm_cost_tracker/ingestion/inbox.rb +4 -25
- data/lib/llm_cost_tracker/ingestion/pool.rb +44 -0
- data/lib/llm_cost_tracker/ingestion/worker.rb +22 -36
- data/lib/llm_cost_tracker/ingestion.rb +8 -9
- data/lib/llm_cost_tracker/integrations/anthropic.rb +46 -68
- data/lib/llm_cost_tracker/integrations/base.rb +14 -11
- data/lib/llm_cost_tracker/integrations/openai.rb +104 -131
- data/lib/llm_cost_tracker/integrations/ruby_llm.rb +27 -73
- data/lib/llm_cost_tracker/integrations.rb +14 -13
- data/lib/llm_cost_tracker/ledger/period/totals.rb +5 -3
- data/lib/llm_cost_tracker/ledger/rollups.rb +4 -13
- data/lib/llm_cost_tracker/ledger/schema/call_line_items.rb +11 -0
- data/lib/llm_cost_tracker/ledger/schema/call_rollups.rb +13 -3
- data/lib/llm_cost_tracker/ledger/schema/call_tags.rb +11 -0
- data/lib/llm_cost_tracker/ledger/schema/calls.rb +0 -4
- data/lib/llm_cost_tracker/ledger/schema/ingestion_inbox_entries.rb +13 -3
- data/lib/llm_cost_tracker/ledger/schema/ingestion_leases.rb +13 -3
- data/lib/llm_cost_tracker/ledger/schema/provider_invoice_imports.rb +19 -9
- data/lib/llm_cost_tracker/ledger/schema/provider_invoices.rb +26 -11
- data/lib/llm_cost_tracker/ledger/store.rb +21 -18
- data/lib/llm_cost_tracker/ledger/tags/query.rb +0 -1
- data/lib/llm_cost_tracker/ledger.rb +13 -0
- data/lib/llm_cost_tracker/logging.rb +0 -4
- data/lib/llm_cost_tracker/middleware/faraday.rb +46 -17
- data/lib/llm_cost_tracker/parsers/anthropic.rb +35 -59
- data/lib/llm_cost_tracker/parsers/azure.rb +46 -0
- data/lib/llm_cost_tracker/parsers/base.rb +53 -47
- data/lib/llm_cost_tracker/parsers/gemini.rb +23 -27
- data/lib/llm_cost_tracker/parsers/openai.rb +8 -40
- data/lib/llm_cost_tracker/parsers/openai_compatible.rb +26 -49
- data/lib/llm_cost_tracker/parsers/openai_usage.rb +19 -23
- data/lib/llm_cost_tracker/parsers.rb +29 -4
- data/lib/llm_cost_tracker/prices.json +567 -579
- data/lib/llm_cost_tracker/pricing/backfill.rb +140 -0
- data/lib/llm_cost_tracker/pricing/effective_prices.rb +2 -4
- data/lib/llm_cost_tracker/pricing/estimator.rb +33 -0
- data/lib/llm_cost_tracker/pricing/explainer.rb +5 -2
- data/lib/llm_cost_tracker/pricing/lookup.rb +37 -2
- data/lib/llm_cost_tracker/pricing/mode.rb +34 -4
- data/lib/llm_cost_tracker/pricing/registry.rb +0 -7
- data/lib/llm_cost_tracker/pricing/service_charges.rb +6 -10
- data/lib/llm_cost_tracker/pricing/{sync_change_printer.rb → sync/change_printer.rb} +3 -3
- data/lib/llm_cost_tracker/pricing/sync/registry_writer.rb +14 -2
- data/lib/llm_cost_tracker/pricing/sync.rb +1 -9
- data/lib/llm_cost_tracker/pricing/unknown.rb +5 -2
- data/lib/llm_cost_tracker/pricing.rb +71 -43
- data/lib/llm_cost_tracker/providers/anthropic/server_tools.rb +15 -0
- data/lib/llm_cost_tracker/providers/anthropic/tier_classification.rb +22 -0
- data/lib/llm_cost_tracker/providers/azure/hosts.rb +17 -0
- data/lib/llm_cost_tracker/providers/gemini/model_families.rb +17 -0
- data/lib/llm_cost_tracker/providers/openai/hosts.rb +35 -0
- data/lib/llm_cost_tracker/providers/openai/model_families.rb +51 -0
- data/lib/llm_cost_tracker/providers/openai/service_charges.rb +157 -0
- data/lib/llm_cost_tracker/railtie.rb +3 -5
- data/lib/llm_cost_tracker/reconcile_tasks.rb +18 -21
- data/lib/llm_cost_tracker/reconciliation/diff.rb +26 -45
- data/lib/llm_cost_tracker/reconciliation/diff_result.rb +0 -4
- data/lib/llm_cost_tracker/reconciliation/importer.rb +3 -7
- data/lib/llm_cost_tracker/reconciliation/sources/anthropic_usage.rb +10 -33
- data/lib/llm_cost_tracker/reconciliation/sources/coercion.rb +40 -0
- data/lib/llm_cost_tracker/reconciliation/sources/openai_usage.rb +7 -31
- data/lib/llm_cost_tracker/report/formatter.rb +32 -19
- data/lib/llm_cost_tracker/report.rb +0 -4
- data/lib/llm_cost_tracker/retention.rb +20 -8
- data/lib/llm_cost_tracker/tags/sanitizer.rb +13 -17
- data/lib/llm_cost_tracker/token_usage.rb +4 -0
- data/lib/llm_cost_tracker/tracker.rb +33 -74
- data/lib/llm_cost_tracker/version.rb +1 -1
- data/lib/llm_cost_tracker.rb +11 -15
- data/lib/tasks/llm_cost_tracker.rake +16 -2
- metadata +31 -12
- data/app/views/llm_cost_tracker/shared/_active_filters.html.erb +0 -16
- data/app/views/llm_cost_tracker/shared/_filters.html.erb +0 -66
- data/app/views/llm_cost_tracker/shared/_sort.html.erb +0 -13
- data/lib/llm_cost_tracker/dashboard_setup_state.rb +0 -109
- data/lib/llm_cost_tracker/ingestion/inline.rb +0 -22
- data/lib/llm_cost_tracker/parsers/openai_service_charges.rb +0 -126
- data/lib/llm_cost_tracker/usage_capture.rb +0 -58
|
@@ -1,36 +1,29 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative "
|
|
3
|
+
require_relative "../providers/openai/hosts"
|
|
4
|
+
require_relative "../providers/openai/model_families"
|
|
5
|
+
require_relative "../providers/openai/service_charges"
|
|
4
6
|
|
|
5
7
|
module LlmCostTracker
|
|
6
8
|
module Parsers
|
|
7
9
|
module OpenaiUsage
|
|
8
|
-
include
|
|
9
|
-
|
|
10
|
-
OPENAI_DATA_RESIDENCY_HOST_PATTERN = /\A[a-z]{2,3}\.api\.openai\.com\z/
|
|
10
|
+
include LlmCostTracker::Providers::Openai::ServiceCharges
|
|
11
11
|
|
|
12
12
|
class << self
|
|
13
13
|
def combined_pricing_mode(host:, model:, service_tier:)
|
|
14
|
-
modes = [Pricing.
|
|
14
|
+
modes = [Pricing::Mode.normalize(service_tier)]
|
|
15
15
|
modes << "data_residency" if regional_processing?(host: host, model: model)
|
|
16
16
|
modes = modes.compact.uniq
|
|
17
17
|
modes.empty? ? nil : modes.join("_")
|
|
18
18
|
end
|
|
19
19
|
|
|
20
20
|
def regional_processing?(host:, model:)
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
def data_residency_model?(model)
|
|
25
|
-
model.to_s.match?(
|
|
26
|
-
/\Agpt-5\.(?:4|5)(?:-(?:mini|nano|pro|codex(?:-mini|-max)?))?(?:-\d{4}-\d{2}-\d{2})?\z/
|
|
27
|
-
)
|
|
21
|
+
LlmCostTracker::Providers::Openai::Hosts.data_residency?(host) &&
|
|
22
|
+
LlmCostTracker::Providers::Openai::ModelFamilies.data_residency?(model)
|
|
28
23
|
end
|
|
29
24
|
end
|
|
30
25
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
def parse_openai_usage(request_url:, request_body:, response_status:, response_body:)
|
|
26
|
+
def parse(request_url:, request_body:, response_status:, response_body:, **)
|
|
34
27
|
return nil unless response_status == 200
|
|
35
28
|
|
|
36
29
|
response = safe_json_parse(response_body)
|
|
@@ -42,7 +35,7 @@ module LlmCostTracker
|
|
|
42
35
|
|
|
43
36
|
model = response["model"] || request["model"]
|
|
44
37
|
|
|
45
|
-
|
|
38
|
+
Event.build(
|
|
46
39
|
provider: provider_for(request_url),
|
|
47
40
|
provider_response_id: response["id"],
|
|
48
41
|
pricing_mode: pricing_mode(
|
|
@@ -53,11 +46,11 @@ module LlmCostTracker
|
|
|
53
46
|
model: model,
|
|
54
47
|
token_usage: token_usage(usage: usage, cache_read: cache_read, model: model),
|
|
55
48
|
usage_source: :response,
|
|
56
|
-
service_line_items:
|
|
49
|
+
service_line_items: service_line_items_for(response, request: request, model: response["model"])
|
|
57
50
|
)
|
|
58
51
|
end
|
|
59
52
|
|
|
60
|
-
def
|
|
53
|
+
def parse_stream(response_status:, request_url: nil, request_body: nil, events: [], **)
|
|
61
54
|
return nil unless response_status == 200
|
|
62
55
|
|
|
63
56
|
request = safe_json_parse(request_body)
|
|
@@ -70,6 +63,12 @@ module LlmCostTracker
|
|
|
70
63
|
build_unknown_stream_usage(**context)
|
|
71
64
|
end
|
|
72
65
|
|
|
66
|
+
def auto_enable_stream_usage?(request_url)
|
|
67
|
+
openai_chat_completions_url?(request_url)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
private
|
|
71
|
+
|
|
73
72
|
def stream_capture_context(events:, request:, request_url:)
|
|
74
73
|
model = find_event_value(events) do |data|
|
|
75
74
|
data["model"] || data.dig("response", "model") || data.dig("chunk", "model")
|
|
@@ -91,7 +90,7 @@ module LlmCostTracker
|
|
|
91
90
|
|
|
92
91
|
def build_known_stream_usage(usage:, provider:, model:, provider_response_id:, pricing_mode:, service_line_items:)
|
|
93
92
|
cache_read = cache_read_input_tokens(usage)
|
|
94
|
-
|
|
93
|
+
Event.build(
|
|
95
94
|
provider: provider,
|
|
96
95
|
provider_response_id: provider_response_id,
|
|
97
96
|
pricing_mode: pricing_mode,
|
|
@@ -137,9 +136,6 @@ module LlmCostTracker
|
|
|
137
136
|
OpenaiUsage.combined_pricing_mode(host: parsed_uri(request_url)&.host, model: model, service_tier: service_tier)
|
|
138
137
|
end
|
|
139
138
|
|
|
140
|
-
IMAGE_OUTPUT_MODEL_PATTERN = /\Agpt-image-/i
|
|
141
|
-
private_constant :IMAGE_OUTPUT_MODEL_PATTERN
|
|
142
|
-
|
|
143
139
|
def token_usage(usage:, cache_read:, model: nil)
|
|
144
140
|
audio_input = audio_input_tokens(usage)
|
|
145
141
|
audio_output = audio_output_tokens(usage)
|
|
@@ -150,7 +146,7 @@ module LlmCostTracker
|
|
|
150
146
|
image_output, regular_output_remainder = split_stream_image_output(
|
|
151
147
|
raw_output: raw_output, image_output_details: image_output_details,
|
|
152
148
|
text_output_details: text_output_details, audio_output: audio_output,
|
|
153
|
-
default_to_image:
|
|
149
|
+
default_to_image: LlmCostTracker::Providers::Openai::ModelFamilies.image_output?(model)
|
|
154
150
|
)
|
|
155
151
|
|
|
156
152
|
TokenUsage.build(
|
|
@@ -2,19 +2,44 @@
|
|
|
2
2
|
|
|
3
3
|
module LlmCostTracker
|
|
4
4
|
module Parsers
|
|
5
|
-
|
|
5
|
+
autoload :Base, "llm_cost_tracker/parsers/base"
|
|
6
|
+
autoload :OpenaiUsage, "llm_cost_tracker/parsers/openai_usage"
|
|
7
|
+
autoload :Openai, "llm_cost_tracker/parsers/openai"
|
|
8
|
+
autoload :Azure, "llm_cost_tracker/parsers/azure"
|
|
9
|
+
autoload :OpenaiCompatible, "llm_cost_tracker/parsers/openai_compatible"
|
|
10
|
+
autoload :Anthropic, "llm_cost_tracker/parsers/anthropic"
|
|
11
|
+
autoload :Gemini, "llm_cost_tracker/parsers/gemini"
|
|
12
|
+
|
|
13
|
+
MUTEX = Mutex.new
|
|
14
|
+
PARSER_CONSTANTS = %i[Openai Azure OpenaiCompatible Anthropic Gemini].freeze
|
|
6
15
|
|
|
7
16
|
module_function
|
|
8
17
|
|
|
9
18
|
def find_for(url)
|
|
10
|
-
|
|
19
|
+
PARSER_CONSTANTS.each do |name|
|
|
20
|
+
klass = const_get(name)
|
|
21
|
+
return instance_for(klass) if klass.match?(url)
|
|
22
|
+
end
|
|
23
|
+
nil
|
|
11
24
|
end
|
|
12
25
|
|
|
13
26
|
def find_for_provider(provider)
|
|
14
27
|
provider_name = provider.to_s.downcase
|
|
15
|
-
|
|
16
|
-
|
|
28
|
+
PARSER_CONSTANTS.each do |name|
|
|
29
|
+
klass = const_get(name)
|
|
30
|
+
return instance_for(klass) if klass.provider_names.include?(provider_name)
|
|
31
|
+
end
|
|
32
|
+
nil
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def instance_for(klass)
|
|
36
|
+
cached = (@instances ||= {})[klass]
|
|
37
|
+
return cached if cached
|
|
38
|
+
|
|
39
|
+
MUTEX.synchronize do
|
|
40
|
+
@instances[klass] ||= klass.new
|
|
17
41
|
end
|
|
18
42
|
end
|
|
43
|
+
private_class_method :instance_for
|
|
19
44
|
end
|
|
20
45
|
end
|