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,22 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative "../ledger/store"
|
|
4
|
-
|
|
5
|
-
module LlmCostTracker
|
|
6
|
-
module Ingestion
|
|
7
|
-
module Inline
|
|
8
|
-
class << self
|
|
9
|
-
def save(event)
|
|
10
|
-
persist(event)
|
|
11
|
-
event
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
private
|
|
15
|
-
|
|
16
|
-
def persist(event)
|
|
17
|
-
Ledger::Store.insert_many([event])
|
|
18
|
-
end
|
|
19
|
-
end
|
|
20
|
-
end
|
|
21
|
-
end
|
|
22
|
-
end
|
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative "../billing/line_item"
|
|
4
|
-
|
|
5
|
-
module LlmCostTracker
|
|
6
|
-
module Parsers
|
|
7
|
-
module OpenaiServiceCharges
|
|
8
|
-
RESPONSE_OUTPUT_COMPONENTS = {
|
|
9
|
-
"web_search_call" => :web_search_request,
|
|
10
|
-
"file_search_call" => :file_search_call,
|
|
11
|
-
"code_interpreter_call" => :container_session,
|
|
12
|
-
"mcp_call" => :mcp_call
|
|
13
|
-
}.freeze
|
|
14
|
-
|
|
15
|
-
REASONING_MODEL_PATTERNS = [
|
|
16
|
-
/\Agpt-5(\b|[\d.-])/i,
|
|
17
|
-
/\Ao\d+(\b|[\d.-])/i
|
|
18
|
-
].freeze
|
|
19
|
-
NON_REASONING_GPT5_PATTERN = /\Agpt-5(?:\.\d+)?-chat\b/i
|
|
20
|
-
private_constant :NON_REASONING_GPT5_PATTERN
|
|
21
|
-
|
|
22
|
-
module_function
|
|
23
|
-
|
|
24
|
-
def line_items_from_output(output_items, request: nil, model: nil)
|
|
25
|
-
deduped = {}
|
|
26
|
-
Array(output_items).each { |item| store_output_item(deduped, item) }
|
|
27
|
-
deduped.values
|
|
28
|
-
.select { |item| billable?(item) }
|
|
29
|
-
.filter_map { |item| build_line_item(item, request: request, model: model) }
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
def billable?(item)
|
|
33
|
-
return false unless item.is_a?(Hash)
|
|
34
|
-
|
|
35
|
-
component = RESPONSE_OUTPUT_COMPONENTS[item["type"]]
|
|
36
|
-
return false unless component
|
|
37
|
-
return true unless component == :web_search_request
|
|
38
|
-
|
|
39
|
-
action_type = item.dig("action", "type")
|
|
40
|
-
action_type.nil? || action_type == "search"
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
def store_output_item(output_items, item)
|
|
44
|
-
return unless item.is_a?(Hash) && RESPONSE_OUTPUT_COMPONENTS.key?(item["type"])
|
|
45
|
-
|
|
46
|
-
component = RESPONSE_OUTPUT_COMPONENTS[item["type"]]
|
|
47
|
-
key = if component == :container_session && item["container_id"]
|
|
48
|
-
"#{component}:#{item['container_id']}"
|
|
49
|
-
else
|
|
50
|
-
item["id"] || "#{item['type']}:#{output_items.length}"
|
|
51
|
-
end
|
|
52
|
-
output_items[key] = item
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
def build_line_item(item, request: nil, model: nil)
|
|
56
|
-
return nil unless item.is_a?(Hash)
|
|
57
|
-
|
|
58
|
-
component_key = component_key_for(item, request: request, model: model)
|
|
59
|
-
return nil unless component_key
|
|
60
|
-
|
|
61
|
-
provider_item_id = if component_key == :container_session
|
|
62
|
-
item["container_id"] || item["id"]
|
|
63
|
-
else
|
|
64
|
-
item["id"]
|
|
65
|
-
end
|
|
66
|
-
Billing::LineItem.build(
|
|
67
|
-
component_key: component_key,
|
|
68
|
-
quantity: 1,
|
|
69
|
-
cost_status: Billing::CostStatus::UNKNOWN,
|
|
70
|
-
pricing_basis: :provider_usage,
|
|
71
|
-
provider_field: "response.output.#{item['type']}",
|
|
72
|
-
provider_item_id: provider_item_id,
|
|
73
|
-
details: line_item_details(item)
|
|
74
|
-
)
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
def component_key_for(item, request:, model:)
|
|
78
|
-
component = RESPONSE_OUTPUT_COMPONENTS[item["type"]]
|
|
79
|
-
return component unless component == :web_search_request
|
|
80
|
-
return component unless web_search_preview_used?(request)
|
|
81
|
-
|
|
82
|
-
reasoning_model?(model) ? :web_search_preview_request_reasoning : :web_search_preview_request_non_reasoning
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
def web_search_preview_used?(request)
|
|
86
|
-
tools = request && (request[:tools] || request["tools"])
|
|
87
|
-
return false unless tools.respond_to?(:each)
|
|
88
|
-
|
|
89
|
-
tools.any? do |tool|
|
|
90
|
-
type = tool.is_a?(Hash) ? (tool[:type] || tool["type"]) : tool
|
|
91
|
-
type.to_s.include?("web_search_preview")
|
|
92
|
-
end
|
|
93
|
-
end
|
|
94
|
-
|
|
95
|
-
def reasoning_model?(model)
|
|
96
|
-
return false unless model
|
|
97
|
-
|
|
98
|
-
name = model.to_s.split("/", 2).last
|
|
99
|
-
return false if NON_REASONING_GPT5_PATTERN.match?(name)
|
|
100
|
-
|
|
101
|
-
REASONING_MODEL_PATTERNS.any? { |pattern| pattern.match?(name) }
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
def line_item_details(item)
|
|
105
|
-
{
|
|
106
|
-
"status" => item["status"],
|
|
107
|
-
"action_type" => item.dig("action", "type"),
|
|
108
|
-
"container_id" => item["container_id"]
|
|
109
|
-
}.compact
|
|
110
|
-
end
|
|
111
|
-
|
|
112
|
-
def openai_service_line_items(response, request: nil)
|
|
113
|
-
line_items_from_output(response["output"], request: request, model: response["model"])
|
|
114
|
-
end
|
|
115
|
-
|
|
116
|
-
def openai_stream_service_line_items(events, request: nil, model: nil)
|
|
117
|
-
output_items = []
|
|
118
|
-
each_event_data(events) do |data|
|
|
119
|
-
output_items.concat(Array(data.dig("response", "output")))
|
|
120
|
-
output_items << data["item"] if data["item"]
|
|
121
|
-
end
|
|
122
|
-
line_items_from_output(output_items, request: request, model: model)
|
|
123
|
-
end
|
|
124
|
-
end
|
|
125
|
-
end
|
|
126
|
-
end
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "active_support/core_ext/object/blank"
|
|
4
|
-
|
|
5
|
-
require_relative "pricing"
|
|
6
|
-
require_relative "billing/line_item"
|
|
7
|
-
|
|
8
|
-
module LlmCostTracker
|
|
9
|
-
UsageCapture = Data.define(
|
|
10
|
-
:provider,
|
|
11
|
-
:model,
|
|
12
|
-
:token_usage,
|
|
13
|
-
:stream,
|
|
14
|
-
:usage_source,
|
|
15
|
-
:provider_response_id,
|
|
16
|
-
:provider_project_id,
|
|
17
|
-
:provider_api_key_id,
|
|
18
|
-
:provider_workspace_id,
|
|
19
|
-
:batch,
|
|
20
|
-
:pricing_mode,
|
|
21
|
-
:line_items
|
|
22
|
-
)
|
|
23
|
-
|
|
24
|
-
class UsageCapture
|
|
25
|
-
UNKNOWN_MODEL = "unknown"
|
|
26
|
-
|
|
27
|
-
def self.batch_from_pricing_mode?(pricing_mode)
|
|
28
|
-
pricing_mode.to_s.split("_").include?("batch")
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
def self.build(**attributes)
|
|
32
|
-
pricing_mode = Pricing.normalize_mode(attributes[:pricing_mode])
|
|
33
|
-
batch = attributes[:batch]
|
|
34
|
-
batch = batch_from_pricing_mode?(pricing_mode) if batch.nil?
|
|
35
|
-
|
|
36
|
-
token_usage = attributes.fetch(:token_usage)
|
|
37
|
-
service_line_items = Array(attributes[:service_line_items]).map do |item|
|
|
38
|
-
item.is_a?(Billing::LineItem) ? item : Billing::LineItem.build(item)
|
|
39
|
-
end
|
|
40
|
-
line_items = attributes[:line_items] || (Billing::LineItem.from_token_usage(token_usage) + service_line_items)
|
|
41
|
-
|
|
42
|
-
new(
|
|
43
|
-
provider: attributes.fetch(:provider).to_s,
|
|
44
|
-
model: attributes.fetch(:model).to_s.strip.presence || UNKNOWN_MODEL,
|
|
45
|
-
token_usage: token_usage,
|
|
46
|
-
stream: attributes[:stream] || false,
|
|
47
|
-
usage_source: attributes[:usage_source],
|
|
48
|
-
provider_response_id: attributes[:provider_response_id].to_s.strip.presence,
|
|
49
|
-
provider_project_id: attributes[:provider_project_id].to_s.strip.presence,
|
|
50
|
-
provider_api_key_id: attributes[:provider_api_key_id].to_s.strip.presence,
|
|
51
|
-
provider_workspace_id: attributes[:provider_workspace_id].to_s.strip.presence,
|
|
52
|
-
batch: batch,
|
|
53
|
-
pricing_mode: pricing_mode,
|
|
54
|
-
line_items: line_items
|
|
55
|
-
)
|
|
56
|
-
end
|
|
57
|
-
end
|
|
58
|
-
end
|