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.
Files changed (145) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +55 -0
  3. data/README.md +6 -2
  4. data/app/assets/llm_cost_tracker/application.css +782 -801
  5. data/app/controllers/llm_cost_tracker/application_controller.rb +15 -3
  6. data/app/controllers/llm_cost_tracker/calls_controller.rb +39 -20
  7. data/app/controllers/llm_cost_tracker/dashboard_controller.rb +0 -3
  8. data/app/controllers/llm_cost_tracker/models_controller.rb +3 -1
  9. data/app/controllers/llm_cost_tracker/pricing_controller.rb +16 -0
  10. data/app/controllers/llm_cost_tracker/reconciliation_controller.rb +13 -19
  11. data/app/controllers/llm_cost_tracker/tags_controller.rb +3 -1
  12. data/app/helpers/llm_cost_tracker/application_helper.rb +16 -4
  13. data/app/helpers/llm_cost_tracker/chart_helper.rb +22 -6
  14. data/app/helpers/llm_cost_tracker/sortable_table_helper.rb +41 -0
  15. data/app/models/llm_cost_tracker/provider_invoice_import.rb +9 -4
  16. data/app/services/llm_cost_tracker/dashboard/pricing_overview.rb +95 -0
  17. data/app/services/llm_cost_tracker/dashboard/setup_state.rb +104 -0
  18. data/app/services/llm_cost_tracker/dashboard/sort.rb +9 -0
  19. data/app/services/llm_cost_tracker/dashboard/tag_breakdown.rb +19 -5
  20. data/app/services/llm_cost_tracker/dashboard/top_models.rb +34 -19
  21. data/app/views/layouts/llm_cost_tracker/application.html.erb +80 -17
  22. data/app/views/llm_cost_tracker/calls/index.html.erb +69 -90
  23. data/app/views/llm_cost_tracker/calls/show.html.erb +119 -120
  24. data/app/views/llm_cost_tracker/dashboard/index.html.erb +119 -158
  25. data/app/views/llm_cost_tracker/data_quality/index.html.erb +109 -108
  26. data/app/views/llm_cost_tracker/errors/database.html.erb +2 -2
  27. data/app/views/llm_cost_tracker/models/index.html.erb +39 -59
  28. data/app/views/llm_cost_tracker/pricing/index.html.erb +93 -0
  29. data/app/views/llm_cost_tracker/reconciliation/index.html.erb +49 -58
  30. data/app/views/llm_cost_tracker/shared/_filter_pill_date.html.erb +19 -0
  31. data/app/views/llm_cost_tracker/shared/_filter_pill_model.html.erb +22 -0
  32. data/app/views/llm_cost_tracker/shared/_filter_pill_provider.html.erb +22 -0
  33. data/app/views/llm_cost_tracker/shared/_filter_pill_stream.html.erb +23 -0
  34. data/app/views/llm_cost_tracker/shared/_spend_chart.html.erb +3 -13
  35. data/app/views/llm_cost_tracker/shared/_tag_chips.html.erb +1 -1
  36. data/app/views/llm_cost_tracker/shared/setup_required.html.erb +16 -15
  37. data/app/views/llm_cost_tracker/tags/index.html.erb +27 -32
  38. data/app/views/llm_cost_tracker/tags/show.html.erb +83 -102
  39. data/config/routes.rb +1 -0
  40. data/lib/llm_cost_tracker/billing/cost_status.rb +21 -25
  41. data/lib/llm_cost_tracker/billing/line_item.rb +15 -49
  42. data/lib/llm_cost_tracker/budget.rb +29 -8
  43. data/lib/llm_cost_tracker/{parsers → capture}/sse.rb +1 -1
  44. data/lib/llm_cost_tracker/capture/stream_collector.rb +34 -42
  45. data/lib/llm_cost_tracker/capture/stream_tracker.rb +2 -6
  46. data/lib/llm_cost_tracker/configuration.rb +30 -44
  47. data/lib/llm_cost_tracker/doctor/capture_verifier.rb +1 -1
  48. data/lib/llm_cost_tracker/doctor/ingestion_check.rb +8 -8
  49. data/lib/llm_cost_tracker/doctor/legacy_audit_check.rb +0 -2
  50. data/lib/llm_cost_tracker/doctor/legacy_billing_status_check.rb +0 -2
  51. data/lib/llm_cost_tracker/doctor.rb +80 -25
  52. data/lib/llm_cost_tracker/engine.rb +1 -2
  53. data/lib/llm_cost_tracker/errors.rb +3 -2
  54. data/lib/llm_cost_tracker/event.rb +47 -0
  55. data/lib/llm_cost_tracker/generators/llm_cost_tracker/{durable_ingestion_generator.rb → async_ingestion_generator.rb} +8 -8
  56. data/lib/llm_cost_tracker/generators/llm_cost_tracker/install_generator.rb +4 -23
  57. 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
  58. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/create_llm_cost_tracker_reconciliation.rb.erb +6 -1
  59. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/initializer.rb.erb +14 -7
  60. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_call_rollups_provider.rb.erb +27 -8
  61. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_call_tags_key_value_index.rb.erb +5 -5
  62. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_provider_invoice_imports_provider.rb.erb +36 -0
  63. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_provider_invoices_metadata_index.rb.erb +27 -0
  64. data/lib/llm_cost_tracker/generators/llm_cost_tracker/upgrade_call_rollups_provider_generator.rb +0 -9
  65. data/lib/llm_cost_tracker/generators/llm_cost_tracker/upgrade_provider_invoice_imports_provider_generator.rb +31 -0
  66. data/lib/llm_cost_tracker/generators/llm_cost_tracker/upgrade_provider_invoices_metadata_index_generator.rb +31 -0
  67. data/lib/llm_cost_tracker/ingestion/batch.rb +5 -2
  68. data/lib/llm_cost_tracker/ingestion/inbox.rb +4 -25
  69. data/lib/llm_cost_tracker/ingestion/pool.rb +44 -0
  70. data/lib/llm_cost_tracker/ingestion/worker.rb +22 -36
  71. data/lib/llm_cost_tracker/ingestion.rb +8 -9
  72. data/lib/llm_cost_tracker/integrations/anthropic.rb +46 -68
  73. data/lib/llm_cost_tracker/integrations/base.rb +14 -11
  74. data/lib/llm_cost_tracker/integrations/openai.rb +104 -131
  75. data/lib/llm_cost_tracker/integrations/ruby_llm.rb +27 -73
  76. data/lib/llm_cost_tracker/integrations.rb +14 -13
  77. data/lib/llm_cost_tracker/ledger/period/totals.rb +5 -3
  78. data/lib/llm_cost_tracker/ledger/rollups.rb +4 -13
  79. data/lib/llm_cost_tracker/ledger/schema/call_line_items.rb +11 -0
  80. data/lib/llm_cost_tracker/ledger/schema/call_rollups.rb +13 -3
  81. data/lib/llm_cost_tracker/ledger/schema/call_tags.rb +11 -0
  82. data/lib/llm_cost_tracker/ledger/schema/calls.rb +0 -4
  83. data/lib/llm_cost_tracker/ledger/schema/ingestion_inbox_entries.rb +13 -3
  84. data/lib/llm_cost_tracker/ledger/schema/ingestion_leases.rb +13 -3
  85. data/lib/llm_cost_tracker/ledger/schema/provider_invoice_imports.rb +19 -9
  86. data/lib/llm_cost_tracker/ledger/schema/provider_invoices.rb +26 -11
  87. data/lib/llm_cost_tracker/ledger/store.rb +21 -18
  88. data/lib/llm_cost_tracker/ledger/tags/query.rb +0 -1
  89. data/lib/llm_cost_tracker/ledger.rb +13 -0
  90. data/lib/llm_cost_tracker/logging.rb +0 -4
  91. data/lib/llm_cost_tracker/middleware/faraday.rb +46 -17
  92. data/lib/llm_cost_tracker/parsers/anthropic.rb +35 -59
  93. data/lib/llm_cost_tracker/parsers/azure.rb +46 -0
  94. data/lib/llm_cost_tracker/parsers/base.rb +53 -47
  95. data/lib/llm_cost_tracker/parsers/gemini.rb +23 -27
  96. data/lib/llm_cost_tracker/parsers/openai.rb +8 -40
  97. data/lib/llm_cost_tracker/parsers/openai_compatible.rb +26 -49
  98. data/lib/llm_cost_tracker/parsers/openai_usage.rb +19 -23
  99. data/lib/llm_cost_tracker/parsers.rb +29 -4
  100. data/lib/llm_cost_tracker/prices.json +567 -579
  101. data/lib/llm_cost_tracker/pricing/backfill.rb +140 -0
  102. data/lib/llm_cost_tracker/pricing/effective_prices.rb +2 -4
  103. data/lib/llm_cost_tracker/pricing/estimator.rb +33 -0
  104. data/lib/llm_cost_tracker/pricing/explainer.rb +5 -2
  105. data/lib/llm_cost_tracker/pricing/lookup.rb +37 -2
  106. data/lib/llm_cost_tracker/pricing/mode.rb +34 -4
  107. data/lib/llm_cost_tracker/pricing/registry.rb +0 -7
  108. data/lib/llm_cost_tracker/pricing/service_charges.rb +6 -10
  109. data/lib/llm_cost_tracker/pricing/{sync_change_printer.rb → sync/change_printer.rb} +3 -3
  110. data/lib/llm_cost_tracker/pricing/sync/registry_writer.rb +14 -2
  111. data/lib/llm_cost_tracker/pricing/sync.rb +1 -9
  112. data/lib/llm_cost_tracker/pricing/unknown.rb +5 -2
  113. data/lib/llm_cost_tracker/pricing.rb +71 -43
  114. data/lib/llm_cost_tracker/providers/anthropic/server_tools.rb +15 -0
  115. data/lib/llm_cost_tracker/providers/anthropic/tier_classification.rb +22 -0
  116. data/lib/llm_cost_tracker/providers/azure/hosts.rb +17 -0
  117. data/lib/llm_cost_tracker/providers/gemini/model_families.rb +17 -0
  118. data/lib/llm_cost_tracker/providers/openai/hosts.rb +35 -0
  119. data/lib/llm_cost_tracker/providers/openai/model_families.rb +51 -0
  120. data/lib/llm_cost_tracker/providers/openai/service_charges.rb +157 -0
  121. data/lib/llm_cost_tracker/railtie.rb +3 -5
  122. data/lib/llm_cost_tracker/reconcile_tasks.rb +18 -21
  123. data/lib/llm_cost_tracker/reconciliation/diff.rb +26 -45
  124. data/lib/llm_cost_tracker/reconciliation/diff_result.rb +0 -4
  125. data/lib/llm_cost_tracker/reconciliation/importer.rb +3 -7
  126. data/lib/llm_cost_tracker/reconciliation/sources/anthropic_usage.rb +10 -33
  127. data/lib/llm_cost_tracker/reconciliation/sources/coercion.rb +40 -0
  128. data/lib/llm_cost_tracker/reconciliation/sources/openai_usage.rb +7 -31
  129. data/lib/llm_cost_tracker/report/formatter.rb +32 -19
  130. data/lib/llm_cost_tracker/report.rb +0 -4
  131. data/lib/llm_cost_tracker/retention.rb +20 -8
  132. data/lib/llm_cost_tracker/tags/sanitizer.rb +13 -17
  133. data/lib/llm_cost_tracker/token_usage.rb +4 -0
  134. data/lib/llm_cost_tracker/tracker.rb +33 -74
  135. data/lib/llm_cost_tracker/version.rb +1 -1
  136. data/lib/llm_cost_tracker.rb +11 -15
  137. data/lib/tasks/llm_cost_tracker.rake +16 -2
  138. metadata +31 -12
  139. data/app/views/llm_cost_tracker/shared/_active_filters.html.erb +0 -16
  140. data/app/views/llm_cost_tracker/shared/_filters.html.erb +0 -66
  141. data/app/views/llm_cost_tracker/shared/_sort.html.erb +0 -13
  142. data/lib/llm_cost_tracker/dashboard_setup_state.rb +0 -109
  143. data/lib/llm_cost_tracker/ingestion/inline.rb +0 -22
  144. data/lib/llm_cost_tracker/parsers/openai_service_charges.rb +0 -126
  145. 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