llm_cost_tracker 0.8.0 → 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/CHANGELOG.md +108 -0
- data/README.md +12 -5
- data/app/assets/llm_cost_tracker/application.css +65 -5
- data/app/controllers/llm_cost_tracker/application_controller.rb +25 -33
- data/app/controllers/llm_cost_tracker/assets_controller.rb +1 -1
- data/app/controllers/llm_cost_tracker/calls_controller.rb +5 -7
- data/app/controllers/llm_cost_tracker/data_quality_controller.rb +4 -0
- 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 +10 -0
- 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 +5 -1
- data/app/models/llm_cost_tracker/call.rb +0 -3
- data/app/models/llm_cost_tracker/call_line_item.rb +1 -5
- data/app/models/llm_cost_tracker/call_rollup.rb +0 -3
- data/app/models/llm_cost_tracker/call_tag.rb +0 -4
- data/app/models/llm_cost_tracker/ingestion/inbox_entry.rb +0 -4
- data/app/models/llm_cost_tracker/ingestion/lease.rb +0 -3
- data/app/models/llm_cost_tracker/provider_invoice.rb +7 -3
- data/app/models/llm_cost_tracker/provider_invoice_import.rb +24 -0
- data/app/services/llm_cost_tracker/dashboard/data_quality.rb +33 -4
- data/app/services/llm_cost_tracker/dashboard/filter.rb +6 -4
- data/app/views/layouts/llm_cost_tracker/application.html.erb +6 -1
- data/app/views/llm_cost_tracker/calls/show.html.erb +25 -40
- data/app/views/llm_cost_tracker/dashboard/index.html.erb +9 -9
- data/app/views/llm_cost_tracker/data_quality/index.html.erb +91 -52
- 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 +3 -0
- data/app/views/llm_cost_tracker/shared/_metric_stack.html.erb +1 -1
- data/app/views/llm_cost_tracker/tags/show.html.erb +60 -0
- data/config/routes.rb +3 -2
- data/lib/llm_cost_tracker/billing/components.rb +45 -3
- data/lib/llm_cost_tracker/billing/components.yml +71 -0
- data/lib/llm_cost_tracker/billing/line_item.rb +1 -1
- data/lib/llm_cost_tracker/budget.rb +4 -2
- data/lib/llm_cost_tracker/capture/stream_collector.rb +93 -20
- data/lib/llm_cost_tracker/capture/stream_tracker.rb +40 -5
- data/lib/llm_cost_tracker/configuration.rb +53 -1
- data/lib/llm_cost_tracker/dashboard_setup_state.rb +109 -0
- data/lib/llm_cost_tracker/doctor/cost_drift_check.rb +2 -0
- data/lib/llm_cost_tracker/doctor/ingestion_check.rb +26 -0
- data/lib/llm_cost_tracker/doctor/invoice_reconciliation_check.rb +164 -0
- data/lib/llm_cost_tracker/doctor/schema_check.rb +5 -2
- data/lib/llm_cost_tracker/doctor.rb +72 -3
- data/lib/llm_cost_tracker/engine.rb +9 -0
- data/lib/llm_cost_tracker/event.rb +1 -1
- 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 +13 -3
- 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 +5 -58
- 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/upgrade_call_tags_key_value_index_generator.rb +30 -0
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/upgrade_image_tokens_generator.rb +29 -0
- data/lib/llm_cost_tracker/ingestion/inbox.rb +0 -1
- data/lib/llm_cost_tracker/ingestion/inline.rb +22 -0
- data/lib/llm_cost_tracker/ingestion/worker.rb +10 -2
- data/lib/llm_cost_tracker/ingestion.rb +48 -10
- data/lib/llm_cost_tracker/integrations/anthropic.rb +24 -5
- data/lib/llm_cost_tracker/integrations/base.rb +22 -5
- data/lib/llm_cost_tracker/integrations/openai.rb +300 -66
- data/lib/llm_cost_tracker/integrations/ruby_llm.rb +105 -6
- data/lib/llm_cost_tracker/integrations.rb +19 -1
- data/lib/llm_cost_tracker/ledger/period/totals.rb +21 -5
- data/lib/llm_cost_tracker/ledger/rollups.rb +24 -10
- data/lib/llm_cost_tracker/ledger/schema/call_line_items.rb +30 -1
- data/lib/llm_cost_tracker/ledger/schema/call_rollups.rb +3 -3
- data/lib/llm_cost_tracker/ledger/schema/call_tags.rb +17 -2
- data/lib/llm_cost_tracker/ledger/schema/calls.rb +2 -0
- 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 +2 -2
- data/lib/llm_cost_tracker/ledger/store.rb +14 -14
- data/lib/llm_cost_tracker/ledger/tags/encoding.rb +37 -0
- data/lib/llm_cost_tracker/ledger/tags/query.rb +2 -1
- data/lib/llm_cost_tracker/ledger.rb +2 -1
- data/lib/llm_cost_tracker/masking.rb +39 -0
- data/lib/llm_cost_tracker/middleware/faraday.rb +88 -29
- data/lib/llm_cost_tracker/parsers/anthropic.rb +22 -7
- data/lib/llm_cost_tracker/parsers/base.rb +5 -1
- data/lib/llm_cost_tracker/parsers/gemini.rb +4 -0
- data/lib/llm_cost_tracker/parsers/openai.rb +16 -2
- data/lib/llm_cost_tracker/parsers/openai_compatible.rb +5 -1
- data/lib/llm_cost_tracker/parsers/openai_service_charges.rb +49 -10
- data/lib/llm_cost_tracker/parsers/openai_usage.rb +124 -53
- data/lib/llm_cost_tracker/prices.json +110 -19
- data/lib/llm_cost_tracker/pricing/effective_prices.rb +5 -36
- data/lib/llm_cost_tracker/pricing/lookup.rb +36 -3
- data/lib/llm_cost_tracker/pricing/mode.rb +76 -0
- data/lib/llm_cost_tracker/pricing/registry.rb +3 -1
- data/lib/llm_cost_tracker/pricing/service_charges.rb +9 -3
- data/lib/llm_cost_tracker/pricing/sync/registry_writer.rb +50 -1
- data/lib/llm_cost_tracker/pricing/sync.rb +3 -1
- data/lib/llm_cost_tracker/pricing.rb +47 -19
- data/lib/llm_cost_tracker/railtie.rb +6 -0
- 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 +4 -1
- data/lib/llm_cost_tracker/retention.rb +15 -2
- data/lib/llm_cost_tracker/tags/context.rb +3 -4
- data/lib/llm_cost_tracker/tags/sanitizer.rb +60 -4
- data/lib/llm_cost_tracker/token_usage.rb +10 -2
- data/lib/llm_cost_tracker/tracker.rb +45 -18
- data/lib/llm_cost_tracker/version.rb +1 -1
- data/lib/llm_cost_tracker.rb +9 -0
- data/lib/tasks/llm_cost_tracker.rake +25 -2
- metadata +36 -1
|
@@ -8,8 +8,10 @@ module LlmCostTracker
|
|
|
8
8
|
cache_write_input_tokens: "Cache write",
|
|
9
9
|
cache_write_extended_input_tokens: "Extended cache write",
|
|
10
10
|
audio_input_tokens: "Audio input",
|
|
11
|
+
image_input_tokens: "Image input",
|
|
11
12
|
output_tokens: "Output",
|
|
12
13
|
audio_output_tokens: "Audio output",
|
|
14
|
+
image_output_tokens: "Image output",
|
|
13
15
|
hidden_output_tokens: "Hidden output"
|
|
14
16
|
}.freeze
|
|
15
17
|
QUALITY_LABELS = COMPONENT_LABELS.merge(
|
|
@@ -24,8 +26,10 @@ module LlmCostTracker
|
|
|
24
26
|
cache_write_input_tokens: "lct-stack-fill-cache-write",
|
|
25
27
|
cache_write_extended_input_tokens: "lct-stack-fill-cache-write-extended",
|
|
26
28
|
audio_input_tokens: "lct-stack-fill-audio-input",
|
|
29
|
+
image_input_tokens: "lct-stack-fill-image-input",
|
|
27
30
|
output_tokens: "lct-stack-fill-output",
|
|
28
|
-
audio_output_tokens: "lct-stack-fill-audio-output"
|
|
31
|
+
audio_output_tokens: "lct-stack-fill-audio-output",
|
|
32
|
+
image_output_tokens: "lct-stack-fill-image-output"
|
|
29
33
|
}.freeze
|
|
30
34
|
|
|
31
35
|
def token_usage_stack_components
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "active_record"
|
|
4
3
|
require "securerandom"
|
|
5
4
|
|
|
6
5
|
require "llm_cost_tracker/billing/cost_status"
|
|
@@ -9,8 +8,6 @@ require "llm_cost_tracker/ledger/tags/sql"
|
|
|
9
8
|
|
|
10
9
|
module LlmCostTracker
|
|
11
10
|
class Call < ActiveRecord::Base
|
|
12
|
-
self.table_name = "llm_cost_tracker_calls"
|
|
13
|
-
|
|
14
11
|
before_validation :assign_event_id
|
|
15
12
|
|
|
16
13
|
PERIOD_FORMATS = {
|
|
@@ -1,17 +1,13 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "active_record"
|
|
4
|
-
|
|
5
3
|
module LlmCostTracker
|
|
6
4
|
class CallLineItem < ActiveRecord::Base
|
|
7
|
-
self.table_name = "llm_cost_tracker_call_line_items"
|
|
8
|
-
|
|
9
5
|
belongs_to :call,
|
|
10
6
|
class_name: "LlmCostTracker::Call",
|
|
11
7
|
foreign_key: :llm_cost_tracker_call_id,
|
|
12
8
|
inverse_of: :line_items
|
|
13
9
|
|
|
14
|
-
scope :tokens, -> { where(
|
|
10
|
+
scope :tokens, -> { where(unit: "token") }
|
|
15
11
|
scope :by_kind, ->(kind) { where(kind: kind.to_s) }
|
|
16
12
|
scope :by_direction, ->(direction) { where(direction: direction.to_s) }
|
|
17
13
|
scope :by_modality, ->(modality) { where(modality: modality.to_s) }
|
|
@@ -1,11 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "active_record"
|
|
4
|
-
|
|
5
3
|
module LlmCostTracker
|
|
6
4
|
class CallTag < ActiveRecord::Base
|
|
7
|
-
self.table_name = "llm_cost_tracker_call_tags"
|
|
8
|
-
|
|
9
5
|
belongs_to :call,
|
|
10
6
|
class_name: "LlmCostTracker::Call",
|
|
11
7
|
foreign_key: :llm_cost_tracker_call_id,
|
|
@@ -1,12 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "active_record"
|
|
4
|
-
|
|
5
3
|
module LlmCostTracker
|
|
6
4
|
module Ingestion
|
|
7
5
|
class InboxEntry < ActiveRecord::Base
|
|
8
|
-
self.table_name = "llm_cost_tracker_ingestion_inbox_entries"
|
|
9
|
-
|
|
10
6
|
MAX_ATTEMPTS_BEFORE_QUARANTINE = 5
|
|
11
7
|
end
|
|
12
8
|
end
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "active_record"
|
|
4
|
-
|
|
5
3
|
module LlmCostTracker
|
|
6
4
|
class ProviderInvoice < ActiveRecord::Base
|
|
7
|
-
|
|
5
|
+
before_validation :normalize_currency
|
|
6
|
+
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
def normalize_currency
|
|
10
|
+
self.currency = currency.to_s.upcase if currency.present?
|
|
11
|
+
end
|
|
8
12
|
end
|
|
9
13
|
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LlmCostTracker
|
|
4
|
+
class ProviderInvoiceImport < ActiveRecord::Base
|
|
5
|
+
STATE_RUNNING = "running"
|
|
6
|
+
STATE_COMPLETED = "completed"
|
|
7
|
+
STATE_FAILED = "failed"
|
|
8
|
+
STATES = [STATE_RUNNING, STATE_COMPLETED, STATE_FAILED].freeze
|
|
9
|
+
|
|
10
|
+
scope :for_source, ->(source) { where(source: source.to_s) }
|
|
11
|
+
scope :running, -> { where(state: STATE_RUNNING) }
|
|
12
|
+
scope :completed, -> { where(state: STATE_COMPLETED) }
|
|
13
|
+
scope :failed, -> { where(state: STATE_FAILED) }
|
|
14
|
+
scope :latest, -> { order(started_at: :desc, id: :desc) }
|
|
15
|
+
|
|
16
|
+
def self.resume_cursor_for(source)
|
|
17
|
+
for_source(source).latest.limit(1).pick(:cursor)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def self.last_completed_window_for(source)
|
|
21
|
+
for_source(source).completed.latest.limit(1).pick(:window_start, :window_end)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -6,7 +6,8 @@ require "llm_cost_tracker/ledger/schema/adapter"
|
|
|
6
6
|
module LlmCostTracker
|
|
7
7
|
module Dashboard
|
|
8
8
|
class DataQuality
|
|
9
|
-
UnknownPricingRow = ::Data.define(:model, :calls, :share_percent)
|
|
9
|
+
UnknownPricingRow = ::Data.define(:provider, :model, :calls, :share_percent)
|
|
10
|
+
StreamingHealthRow = ::Data.define(:provider, :streams, :with_usage, :unknown, :unknown_share)
|
|
10
11
|
Summary = ::Data.define(:total, :unknown_pricing_count, :untagged_calls_count, :missing_latency_count,
|
|
11
12
|
:streaming_count, :streaming_missing_usage, :missing_provider_response_id_count,
|
|
12
13
|
:calls_with_pricing, :tagged_calls, :calls_with_latency, :streams_with_usage,
|
|
@@ -48,13 +49,14 @@ module LlmCostTracker
|
|
|
48
49
|
|
|
49
50
|
def unknown_pricing_by_model(scope, total_calls:)
|
|
50
51
|
scope.unknown_pricing
|
|
51
|
-
.group(:model)
|
|
52
|
+
.group(:provider, :model)
|
|
52
53
|
.order(Arel.sql("COUNT(*) DESC"))
|
|
53
|
-
.select("model, COUNT(*) AS calls")
|
|
54
|
+
.select("provider, model, COUNT(*) AS calls")
|
|
54
55
|
.limit(10)
|
|
55
56
|
.map do |row|
|
|
56
57
|
calls = row.calls.to_i
|
|
57
|
-
UnknownPricingRow.new(
|
|
58
|
+
UnknownPricingRow.new(provider: row.provider, model: row.model, calls: calls,
|
|
59
|
+
share_percent: percentage(calls, total_calls))
|
|
58
60
|
end
|
|
59
61
|
end
|
|
60
62
|
|
|
@@ -125,6 +127,33 @@ module LlmCostTracker
|
|
|
125
127
|
index_costs_by_component(rows)
|
|
126
128
|
end
|
|
127
129
|
|
|
130
|
+
def streaming_health_rows(scope, total_streaming:)
|
|
131
|
+
return [] unless total_streaming.positive?
|
|
132
|
+
|
|
133
|
+
unknown_predicate = "usage_source = 'unknown' OR usage_source IS NULL"
|
|
134
|
+
rows = scope.unscope(:select, :order, :group)
|
|
135
|
+
.where(stream: true)
|
|
136
|
+
.group(:provider)
|
|
137
|
+
.order(Arel.sql("COUNT(*) DESC"), :provider)
|
|
138
|
+
.pluck(
|
|
139
|
+
:provider,
|
|
140
|
+
Arel.sql("COUNT(*)"),
|
|
141
|
+
Arel.sql("SUM(CASE WHEN #{unknown_predicate} THEN 1 ELSE 0 END)")
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
rows.map do |provider, streams, unknown|
|
|
145
|
+
streams_count = streams.to_i
|
|
146
|
+
unknown_count = unknown.to_i
|
|
147
|
+
StreamingHealthRow.new(
|
|
148
|
+
provider: provider,
|
|
149
|
+
streams: streams_count,
|
|
150
|
+
with_usage: streams_count - unknown_count,
|
|
151
|
+
unknown: unknown_count,
|
|
152
|
+
unknown_share: percentage(unknown_count, streams_count)
|
|
153
|
+
)
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
128
157
|
def hidden_output_summary(stats)
|
|
129
158
|
output_tokens = stats.output_tokens.to_i
|
|
130
159
|
return unless output_tokens.positive?
|
|
@@ -35,11 +35,13 @@ module LlmCostTracker
|
|
|
35
35
|
to_date = Dashboard::DateRange.parse(params, :to)
|
|
36
36
|
Dashboard::DateRange.validate!(from: from_date, to: to_date)
|
|
37
37
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
38
|
+
default_range = Dashboard::DateRange.call(params: params)
|
|
39
|
+
from_date ||= default_range.from
|
|
40
|
+
to_date ||= default_range.to
|
|
41
|
+
|
|
42
42
|
relation
|
|
43
|
+
.where(tracked_at: from_date.beginning_of_day..)
|
|
44
|
+
.where(tracked_at: ..to_date.end_of_day)
|
|
43
45
|
end
|
|
44
46
|
|
|
45
47
|
def apply_exact_filter(relation, key)
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
<% body = capture { yield } %>
|
|
1
2
|
<!DOCTYPE html>
|
|
2
3
|
<html lang="en">
|
|
3
4
|
<head>
|
|
@@ -5,6 +6,7 @@
|
|
|
5
6
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
7
|
<title>LLM Cost Tracker</title>
|
|
7
8
|
<%= stylesheet_link_tag stylesheet_path %>
|
|
9
|
+
<%= inline_style_block %>
|
|
8
10
|
</head>
|
|
9
11
|
<body class="lct-body">
|
|
10
12
|
<div class="lct-app">
|
|
@@ -19,10 +21,13 @@
|
|
|
19
21
|
<%= link_to "Calls", calls_path, class: ("lct-active" if request.path.start_with?(calls_path)) %>
|
|
20
22
|
<%= link_to "Tags", tags_path, class: ("lct-active" if request.path.start_with?(tags_path)) %>
|
|
21
23
|
<%= link_to "Data Quality", data_quality_path, class: ("lct-active" if request.path.start_with?(data_quality_path)) %>
|
|
24
|
+
<% if LlmCostTracker.reconciliation_enabled? %>
|
|
25
|
+
<%= link_to "Reconciliation", reconciliation_path, class: ("lct-active" if request.path.start_with?(reconciliation_path)) %>
|
|
26
|
+
<% end %>
|
|
22
27
|
</nav>
|
|
23
28
|
</header>
|
|
24
29
|
|
|
25
|
-
<%=
|
|
30
|
+
<%= body %>
|
|
26
31
|
</main>
|
|
27
32
|
</div>
|
|
28
33
|
</body>
|
|
@@ -14,15 +14,19 @@ end %>
|
|
|
14
14
|
<% end %>
|
|
15
15
|
|
|
16
16
|
<section class="lct-panel">
|
|
17
|
-
<
|
|
17
|
+
<nav class="lct-breadcrumb" aria-label="Breadcrumb">
|
|
18
|
+
<%= link_to "Calls", calls_path, class: "lct-breadcrumb-link" %>
|
|
19
|
+
<span class="lct-breadcrumb-sep" aria-hidden="true">›</span>
|
|
20
|
+
<span class="lct-breadcrumb-current">#<%= @call.id %></span>
|
|
21
|
+
</nav>
|
|
18
22
|
<div class="lct-call-hero">
|
|
19
23
|
<div>
|
|
20
|
-
<h2 class="lct-section-title lct-call-title">
|
|
24
|
+
<h2 class="lct-section-title lct-call-title">
|
|
25
|
+
<code class="lct-code"><%= @call.model %></code>
|
|
26
|
+
</h2>
|
|
21
27
|
<p class="lct-call-subtitle">
|
|
22
28
|
<code class="lct-code"><%= @call.provider %></code>
|
|
23
29
|
<span>·</span>
|
|
24
|
-
<code class="lct-code"><%= @call.model %></code>
|
|
25
|
-
<span>·</span>
|
|
26
30
|
<span><%= format_date(@call.tracked_at) %></span>
|
|
27
31
|
</p>
|
|
28
32
|
</div>
|
|
@@ -65,45 +69,26 @@ end %>
|
|
|
65
69
|
|
|
66
70
|
<div class="lct-detail-grid">
|
|
67
71
|
<dl class="lct-dl">
|
|
68
|
-
<dt>Tracked At</dt>
|
|
69
|
-
<dd><%= format_date(@call.tracked_at) %></dd>
|
|
70
|
-
|
|
71
|
-
<dt>Provider</dt>
|
|
72
|
-
<dd><%= @call.provider %></dd>
|
|
73
|
-
|
|
74
|
-
<dt>Model</dt>
|
|
75
|
-
<dd><%= @call.model %></dd>
|
|
76
|
-
|
|
77
|
-
<dt>Pricing Status</dt>
|
|
78
|
-
<dd><%= pricing_status(@call) %></dd>
|
|
79
|
-
|
|
80
72
|
<dt>Cost Status</dt>
|
|
81
73
|
<dd><%= @call.cost_status.presence || "n/a" %></dd>
|
|
82
74
|
|
|
83
|
-
<dt>
|
|
84
|
-
<dd><%= @call.
|
|
85
|
-
|
|
86
|
-
<dt>Provider Project ID</dt>
|
|
87
|
-
<dd><%= @call.provider_project_id.presence || "n/a" %></dd>
|
|
88
|
-
|
|
89
|
-
<dt>Provider API Key ID</dt>
|
|
90
|
-
<dd><%= @call.provider_api_key_id.presence || "n/a" %></dd>
|
|
91
|
-
|
|
92
|
-
<dt>Provider Workspace ID</dt>
|
|
93
|
-
<dd><%= @call.provider_workspace_id.presence || "n/a" %></dd>
|
|
75
|
+
<dt>Latency</dt>
|
|
76
|
+
<dd><%= @call.latency_ms ? "#{optional_number(@call.latency_ms)}ms" : "n/a" %></dd>
|
|
94
77
|
|
|
95
78
|
<dt>Batch</dt>
|
|
96
79
|
<dd><%= @call.batch? ? "yes" : "no" %></dd>
|
|
97
80
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
<dd><%= format_date(@call.created_at) %></dd>
|
|
101
|
-
<% end %>
|
|
81
|
+
<dt>Response ID</dt>
|
|
82
|
+
<dd><%= @call.provider_response_id.presence || "n/a" %></dd>
|
|
102
83
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
84
|
+
<dt>Project ID</dt>
|
|
85
|
+
<dd><%= @call.provider_project_id.present? ? LlmCostTracker::Masking.mask_value(:provider_project_id, @call.provider_project_id) : "n/a" %></dd>
|
|
86
|
+
|
|
87
|
+
<dt>API Key ID</dt>
|
|
88
|
+
<dd><%= @call.provider_api_key_id.present? ? LlmCostTracker::Masking.mask_value(:provider_api_key_id, @call.provider_api_key_id) : "n/a" %></dd>
|
|
89
|
+
|
|
90
|
+
<dt>Workspace ID</dt>
|
|
91
|
+
<dd><%= @call.provider_workspace_id.present? ? LlmCostTracker::Masking.mask_value(:provider_workspace_id, @call.provider_workspace_id) : "n/a" %></dd>
|
|
107
92
|
</dl>
|
|
108
93
|
|
|
109
94
|
<dl class="lct-dl">
|
|
@@ -114,7 +99,9 @@ end %>
|
|
|
114
99
|
|
|
115
100
|
<dt>Total Tokens</dt>
|
|
116
101
|
<dd><%= number(@call.total_tokens) %></dd>
|
|
102
|
+
</dl>
|
|
117
103
|
|
|
104
|
+
<dl class="lct-dl">
|
|
118
105
|
<% priced_components.each do |component| %>
|
|
119
106
|
<dt><%= component.fetch(:label).titleize %> Cost</dt>
|
|
120
107
|
<dd><%= optional_money(line_item_costs_by_component[component.fetch(:price_key)]) %></dd>
|
|
@@ -122,9 +109,6 @@ end %>
|
|
|
122
109
|
|
|
123
110
|
<dt>Total Cost</dt>
|
|
124
111
|
<dd><%= optional_money(@call.total_cost) %></dd>
|
|
125
|
-
|
|
126
|
-
<dt>Latency</dt>
|
|
127
|
-
<dd><%= @call.latency_ms ? "#{optional_number(@call.latency_ms)}ms" : "n/a" %></dd>
|
|
128
112
|
</dl>
|
|
129
113
|
</div>
|
|
130
114
|
</section>
|
|
@@ -152,7 +136,8 @@ end %>
|
|
|
152
136
|
<td><%= line_item.unit %></td>
|
|
153
137
|
<td class="lct-num"><%= line_item.quantity %></td>
|
|
154
138
|
<td class="lct-num"><%= line_item.rate_amount ? "#{optional_money(line_item.rate_amount)} / #{line_item.rate_quantity}" : "n/a" %></td>
|
|
155
|
-
|
|
139
|
+
<% unknown_cost = line_item.cost.nil? || line_item.cost_status.to_s == LlmCostTracker::Billing::CostStatus::UNKNOWN %>
|
|
140
|
+
<td class="lct-num<%= ' lct-num-muted' if unknown_cost %>"><%= unknown_cost ? "n/a" : money(line_item.cost) %></td>
|
|
156
141
|
<td><%= line_item.cost_status %></td>
|
|
157
142
|
</tr>
|
|
158
143
|
<% end %>
|
|
@@ -177,6 +162,6 @@ end %>
|
|
|
177
162
|
<% if @call.has_attribute?("metadata") %>
|
|
178
163
|
<section class="lct-panel">
|
|
179
164
|
<h2 class="lct-section-title">Metadata</h2>
|
|
180
|
-
<pre class="lct-pre"><%= safe_json(@call.read_attribute("metadata")) %></pre>
|
|
165
|
+
<pre class="lct-pre"><%= safe_json(LlmCostTracker::Masking.mask_hash(masked_metadata_hash(@call.read_attribute("metadata")))) %></pre>
|
|
181
166
|
</section>
|
|
182
167
|
<% end %>
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
<h2 class="lct-state-title">No LLM calls yet</h2>
|
|
15
15
|
<p class="lct-state-copy">Tracked requests will appear here after your application records its first LLM API call.</p>
|
|
16
16
|
<div class="lct-state-actions">
|
|
17
|
-
<%= link_to "
|
|
17
|
+
<%= link_to "Calls", calls_path, class: "lct-button lct-button-secondary" %>
|
|
18
18
|
</div>
|
|
19
19
|
</section>
|
|
20
20
|
<% else %>
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
<% end %>
|
|
47
47
|
</p>
|
|
48
48
|
</div>
|
|
49
|
-
<%= link_to "
|
|
49
|
+
<%= link_to "Calls",
|
|
50
50
|
calls_path(current_query(provider: @spend_anomaly.fetch(:provider), model: @spend_anomaly.fetch(:model), from: @to_date.iso8601, to: @to_date.iso8601, page: nil, per: nil, format: nil)),
|
|
51
51
|
class: "lct-button lct-button-secondary" %>
|
|
52
52
|
</aside>
|
|
@@ -64,6 +64,11 @@
|
|
|
64
64
|
|
|
65
65
|
<div class="lct-hero-side">
|
|
66
66
|
<div class="lct-stat-grid">
|
|
67
|
+
<article class="lct-stat">
|
|
68
|
+
<p class="lct-stat-label">Avg cost / call</p>
|
|
69
|
+
<p class="lct-stat-value"><%= money(@stats.average_cost_per_call) %></p>
|
|
70
|
+
</article>
|
|
71
|
+
|
|
67
72
|
<article class="lct-stat">
|
|
68
73
|
<p class="lct-stat-label">Calls</p>
|
|
69
74
|
<p class="lct-stat-value"><%= number(@stats.total_calls) %></p>
|
|
@@ -71,11 +76,6 @@
|
|
|
71
76
|
<span class="<%= badge[:css_class] %>"><span class="lct-delta-dot" aria-hidden="true"></span><%= badge[:text] %></span>
|
|
72
77
|
</article>
|
|
73
78
|
|
|
74
|
-
<article class="lct-stat">
|
|
75
|
-
<p class="lct-stat-label">Avg cost / call</p>
|
|
76
|
-
<p class="lct-stat-value"><%= money(@stats.average_cost_per_call) %></p>
|
|
77
|
-
</article>
|
|
78
|
-
|
|
79
79
|
<% if @stats.average_latency_ms %>
|
|
80
80
|
<article class="lct-stat">
|
|
81
81
|
<p class="lct-stat-label">Avg latency</p>
|
|
@@ -103,9 +103,9 @@
|
|
|
103
103
|
<span class="lct-budget-percent"><%= percent(budget[:percent_used]) %></span>
|
|
104
104
|
</div>
|
|
105
105
|
<div class="lct-budget-track" role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow="<%= budget[:progress_percent].round %>">
|
|
106
|
-
<div
|
|
106
|
+
<div data-lct-style="<%= inline_style("width: #{budget[:progress_percent]}%") %>" class="lct-budget-fill <%= budget[:fill_modifier] %>"></div>
|
|
107
107
|
<% if budget[:projected_spent].positive? %>
|
|
108
|
-
<span
|
|
108
|
+
<span data-lct-style="<%= inline_style("left: calc(#{budget[:projected_marker_percent]}% - 1px)") %>" class="lct-budget-marker" aria-hidden="true"></span>
|
|
109
109
|
<% end %>
|
|
110
110
|
</div>
|
|
111
111
|
<p class="lct-budget-projection">
|
|
@@ -72,6 +72,56 @@
|
|
|
72
72
|
</section>
|
|
73
73
|
|
|
74
74
|
<section class="lct-grid lct-two-col">
|
|
75
|
+
<section class="lct-panel">
|
|
76
|
+
<div class="lct-section-head">
|
|
77
|
+
<div>
|
|
78
|
+
<h2 class="lct-section-title">Next actions</h2>
|
|
79
|
+
<p class="lct-section-copy">Use these fixes to improve the trustworthiness of downstream cost reports.</p>
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
|
|
83
|
+
<table class="lct-table lct-table-compact">
|
|
84
|
+
<thead>
|
|
85
|
+
<tr>
|
|
86
|
+
<th>Issue</th>
|
|
87
|
+
<th>Why it matters</th>
|
|
88
|
+
<th>Suggested action</th>
|
|
89
|
+
</tr>
|
|
90
|
+
</thead>
|
|
91
|
+
<tbody>
|
|
92
|
+
<tr>
|
|
93
|
+
<td>Unknown pricing</td>
|
|
94
|
+
<td>Cost stays <code class="lct-code">nil</code>, so totals undercount.</td>
|
|
95
|
+
<td>Update <code class="lct-code">pricing_overrides</code> or <code class="lct-code">prices_file</code>.</td>
|
|
96
|
+
</tr>
|
|
97
|
+
<tr>
|
|
98
|
+
<td>Missing tags</td>
|
|
99
|
+
<td>Attribution by tenant, user, or feature becomes less useful.</td>
|
|
100
|
+
<td>Pass <code class="lct-code">tags:</code> from middleware using request context.</td>
|
|
101
|
+
</tr>
|
|
102
|
+
<tr>
|
|
103
|
+
<td>Missing latency</td>
|
|
104
|
+
<td>Slow requests become harder to isolate on the calls page.</td>
|
|
105
|
+
<td>Make sure latency capture is enabled on every tracked request.</td>
|
|
106
|
+
</tr>
|
|
107
|
+
<% if @summary.streaming_missing_usage.positive? %>
|
|
108
|
+
<tr>
|
|
109
|
+
<td>Streams without usage</td>
|
|
110
|
+
<td>Token totals undercount when streaming responses drop the final usage event.</td>
|
|
111
|
+
<td>Send OpenAI requests with <code class="lct-code">stream_options: { include_usage: true }</code>, or wrap custom clients with <code class="lct-code">LlmCostTracker.track_stream</code>.</td>
|
|
112
|
+
</tr>
|
|
113
|
+
<% end %>
|
|
114
|
+
<% if @summary.missing_provider_response_id_count.positive? %>
|
|
115
|
+
<tr>
|
|
116
|
+
<td>Missing provider response IDs</td>
|
|
117
|
+
<td>Proof of provider-issued responses is weaker when calls cannot be tied back to provider objects.</td>
|
|
118
|
+
<td>Upgrade to the latest parser coverage and pass <code class="lct-code">provider_response_id:</code> for custom clients when the provider exposes one.</td>
|
|
119
|
+
</tr>
|
|
120
|
+
<% end %>
|
|
121
|
+
</tbody>
|
|
122
|
+
</table>
|
|
123
|
+
</section>
|
|
124
|
+
|
|
75
125
|
<section class="lct-panel">
|
|
76
126
|
<div class="lct-section-head">
|
|
77
127
|
<div>
|
|
@@ -129,56 +179,6 @@
|
|
|
129
179
|
</tbody>
|
|
130
180
|
</table>
|
|
131
181
|
</section>
|
|
132
|
-
|
|
133
|
-
<section class="lct-panel">
|
|
134
|
-
<div class="lct-section-head">
|
|
135
|
-
<div>
|
|
136
|
-
<h2 class="lct-section-title">Next actions</h2>
|
|
137
|
-
<p class="lct-section-copy">Use these fixes to improve the trustworthiness of downstream cost reports.</p>
|
|
138
|
-
</div>
|
|
139
|
-
</div>
|
|
140
|
-
|
|
141
|
-
<table class="lct-table lct-table-compact">
|
|
142
|
-
<thead>
|
|
143
|
-
<tr>
|
|
144
|
-
<th>Issue</th>
|
|
145
|
-
<th>Why it matters</th>
|
|
146
|
-
<th>Suggested action</th>
|
|
147
|
-
</tr>
|
|
148
|
-
</thead>
|
|
149
|
-
<tbody>
|
|
150
|
-
<tr>
|
|
151
|
-
<td>Unknown pricing</td>
|
|
152
|
-
<td>Cost stays <code class="lct-code">nil</code>, so totals undercount.</td>
|
|
153
|
-
<td>Update <code class="lct-code">pricing_overrides</code> or <code class="lct-code">prices_file</code>.</td>
|
|
154
|
-
</tr>
|
|
155
|
-
<tr>
|
|
156
|
-
<td>Missing tags</td>
|
|
157
|
-
<td>Attribution by tenant, user, or feature becomes less useful.</td>
|
|
158
|
-
<td>Pass <code class="lct-code">tags:</code> from middleware using request context.</td>
|
|
159
|
-
</tr>
|
|
160
|
-
<tr>
|
|
161
|
-
<td>Missing latency</td>
|
|
162
|
-
<td>Slow requests become harder to isolate on the calls page.</td>
|
|
163
|
-
<td>Make sure latency capture is enabled on every tracked request.</td>
|
|
164
|
-
</tr>
|
|
165
|
-
<% if @summary.streaming_missing_usage.positive? %>
|
|
166
|
-
<tr>
|
|
167
|
-
<td>Streams without usage</td>
|
|
168
|
-
<td>Token totals undercount when streaming responses drop the final usage event.</td>
|
|
169
|
-
<td>Send OpenAI requests with <code class="lct-code">stream_options: { include_usage: true }</code>, or wrap custom clients with <code class="lct-code">LlmCostTracker.track_stream</code>.</td>
|
|
170
|
-
</tr>
|
|
171
|
-
<% end %>
|
|
172
|
-
<% if @summary.missing_provider_response_id_count.positive? %>
|
|
173
|
-
<tr>
|
|
174
|
-
<td>Missing provider response IDs</td>
|
|
175
|
-
<td>Proof of provider-issued responses is weaker when calls cannot be tied back to provider objects.</td>
|
|
176
|
-
<td>Upgrade to the latest parser coverage and pass <code class="lct-code">provider_response_id:</code> for custom clients when the provider exposes one.</td>
|
|
177
|
-
</tr>
|
|
178
|
-
<% end %>
|
|
179
|
-
</tbody>
|
|
180
|
-
</table>
|
|
181
|
-
</section>
|
|
182
182
|
</section>
|
|
183
183
|
|
|
184
184
|
<section class="lct-panel">
|
|
@@ -243,13 +243,50 @@
|
|
|
243
243
|
</thead>
|
|
244
244
|
<tbody>
|
|
245
245
|
<% @service_charge_rows.each do |row| %>
|
|
246
|
+
<% unknown_cost = row.cost_status.to_s == LlmCostTracker::Billing::CostStatus::UNKNOWN %>
|
|
246
247
|
<tr>
|
|
247
248
|
<td><code class="lct-code"><%= row.provider %></code></td>
|
|
248
249
|
<td><code class="lct-code"><%= row.component %></code></td>
|
|
249
250
|
<td><%= row.cost_status %></td>
|
|
250
251
|
<td class="lct-num"><%= number(row.charges_count) %></td>
|
|
251
252
|
<td class="lct-num"><%= number(row.quantity) %></td>
|
|
252
|
-
<td class="lct-num"><%=
|
|
253
|
+
<td class="lct-num<%= ' lct-num-muted' if unknown_cost %>"><%= unknown_cost ? "n/a" : money(row.total_cost) %></td>
|
|
254
|
+
</tr>
|
|
255
|
+
<% end %>
|
|
256
|
+
</tbody>
|
|
257
|
+
</table>
|
|
258
|
+
</div>
|
|
259
|
+
</section>
|
|
260
|
+
<% end %>
|
|
261
|
+
|
|
262
|
+
<% if @streaming_health_rows.any? %>
|
|
263
|
+
<section class="lct-panel">
|
|
264
|
+
<div class="lct-section-head">
|
|
265
|
+
<div>
|
|
266
|
+
<h2 class="lct-section-title">Streaming health by provider</h2>
|
|
267
|
+
<p class="lct-section-copy">Streams without a final usage chunk land as <code class="lct-code">usage_source: unknown</code> and undercount tokens. A high unknown share for an OpenAI-compatible provider usually means <code class="lct-code">stream_options: { include_usage: true }</code> is not being injected for that host.</p>
|
|
268
|
+
</div>
|
|
269
|
+
</div>
|
|
270
|
+
|
|
271
|
+
<div class="lct-table-wrap">
|
|
272
|
+
<table class="lct-table lct-table-compact">
|
|
273
|
+
<thead>
|
|
274
|
+
<tr>
|
|
275
|
+
<th>Provider</th>
|
|
276
|
+
<th class="lct-num">Streams</th>
|
|
277
|
+
<th class="lct-num">With usage</th>
|
|
278
|
+
<th class="lct-num">Unknown</th>
|
|
279
|
+
<th class="lct-num">Unknown share</th>
|
|
280
|
+
</tr>
|
|
281
|
+
</thead>
|
|
282
|
+
<tbody>
|
|
283
|
+
<% @streaming_health_rows.each do |row| %>
|
|
284
|
+
<tr>
|
|
285
|
+
<td><code class="lct-code"><%= row.provider %></code></td>
|
|
286
|
+
<td class="lct-num"><%= number(row.streams) %></td>
|
|
287
|
+
<td class="lct-num"><%= number(row.with_usage) %></td>
|
|
288
|
+
<td class="lct-num"><%= number(row.unknown) %></td>
|
|
289
|
+
<td class="lct-num"><%= percent(row.unknown_share) %></td>
|
|
253
290
|
</tr>
|
|
254
291
|
<% end %>
|
|
255
292
|
</tbody>
|
|
@@ -265,13 +302,14 @@
|
|
|
265
302
|
<h2 class="lct-section-title">Unknown pricing by model</h2>
|
|
266
303
|
<p class="lct-section-copy">These models have token counts but no configured rates, so cost stays unknown.</p>
|
|
267
304
|
</div>
|
|
268
|
-
<%= link_to "
|
|
305
|
+
<%= link_to "Calls", calls_path(current_query(sort: "unknown_pricing")), class: "lct-button lct-button-secondary" %>
|
|
269
306
|
</div>
|
|
270
307
|
|
|
271
308
|
<div class="lct-table-wrap">
|
|
272
309
|
<table class="lct-table lct-table-compact">
|
|
273
310
|
<thead>
|
|
274
311
|
<tr>
|
|
312
|
+
<th>Provider</th>
|
|
275
313
|
<th>Model</th>
|
|
276
314
|
<th class="lct-num">Calls without cost</th>
|
|
277
315
|
<th class="lct-num">Share of total</th>
|
|
@@ -280,6 +318,7 @@
|
|
|
280
318
|
<tbody>
|
|
281
319
|
<% @unknown_pricing_by_model.each do |row| %>
|
|
282
320
|
<tr>
|
|
321
|
+
<td><code class="lct-code"><%= row.provider %></code></td>
|
|
283
322
|
<td><code class="lct-code"><%= row.model %></code></td>
|
|
284
323
|
<td class="lct-num"><%= number(row.calls) %></td>
|
|
285
324
|
<td class="lct-num"><%= percent(row.share_percent) %></td>
|