llm_cost_tracker 0.14.1 → 0.14.2
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 +82 -0
- data/README.md +16 -5
- data/app/assets/llm_cost_tracker/application.css +16 -0
- data/app/controllers/llm_cost_tracker/calls_controller.rb +1 -3
- data/app/controllers/llm_cost_tracker/models_controller.rb +2 -4
- data/app/controllers/llm_cost_tracker/tags_controller.rb +5 -4
- data/app/helpers/llm_cost_tracker/application_helper.rb +4 -25
- data/app/helpers/llm_cost_tracker/chart_helper.rb +6 -10
- data/app/helpers/llm_cost_tracker/token_usage_helper.rb +8 -28
- data/app/models/llm_cost_tracker/call.rb +10 -14
- data/app/services/llm_cost_tracker/dashboard/data_quality.rb +4 -15
- data/app/services/llm_cost_tracker/dashboard/filter.rb +5 -9
- data/app/services/llm_cost_tracker/dashboard/pagination.rb +2 -5
- data/app/services/llm_cost_tracker/dashboard/pricing_overview.rb +2 -1
- data/app/services/llm_cost_tracker/dashboard/spend_anomaly.rb +1 -1
- data/app/services/llm_cost_tracker/dashboard/tag_breakdown.rb +2 -3
- data/app/services/llm_cost_tracker/dashboard/tag_key_explorer.rb +1 -0
- data/app/services/llm_cost_tracker/dashboard/time_series.rb +3 -5
- data/app/services/llm_cost_tracker/dashboard/top_models.rb +1 -2
- data/app/views/llm_cost_tracker/calls/show.html.erb +8 -22
- data/app/views/llm_cost_tracker/data_quality/index.html.erb +1 -1
- data/app/views/llm_cost_tracker/shared/_bar.html.erb +1 -3
- data/app/views/llm_cost_tracker/shared/_filter_pill_date.html.erb +1 -4
- data/app/views/llm_cost_tracker/shared/_filter_pill_model.html.erb +1 -4
- data/app/views/llm_cost_tracker/shared/_filter_pill_provider.html.erb +1 -4
- data/app/views/llm_cost_tracker/shared/_filter_pill_stream.html.erb +1 -4
- data/app/views/llm_cost_tracker/tags/show.html.erb +5 -5
- data/lib/llm_cost_tracker/budget/per_tag.rb +15 -9
- data/lib/llm_cost_tracker/budget.rb +44 -55
- data/lib/llm_cost_tracker/capture/event_window.rb +3 -2
- data/lib/llm_cost_tracker/capture/sdk_payload.rb +5 -1
- data/lib/llm_cost_tracker/capture/stream_collector.rb +25 -26
- data/lib/llm_cost_tracker/capture/stream_tracker.rb +9 -23
- data/lib/llm_cost_tracker/capture_verifier.rb +1 -7
- data/lib/llm_cost_tracker/charges/line_item.rb +5 -1
- data/lib/llm_cost_tracker/configuration/budgets.rb +1 -1
- data/lib/llm_cost_tracker/configuration/pricing.rb +1 -1
- data/lib/llm_cost_tracker/configuration.rb +5 -9
- data/lib/llm_cost_tracker/doctor/price_check.rb +13 -3
- data/lib/llm_cost_tracker/doctor/schema_check.rb +1 -2
- data/lib/llm_cost_tracker/doctor.rb +19 -2
- data/lib/llm_cost_tracker/engine.rb +0 -1
- data/lib/llm_cost_tracker/event.rb +8 -0
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/async_ingestion_generator.rb +0 -6
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/call_rollups_generator.rb +5 -8
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/install_generator.rb +0 -6
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/create_llm_cost_tracker_async_ingestion.rb.erb +1 -1
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/create_llm_cost_tracker_call_rollups.rb.erb +1 -1
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/create_llm_cost_tracker_calls.rb.erb +1 -1
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/initializer.rb.erb +11 -9
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_call_rollups_provider.rb.erb +1 -1
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_call_tags_key_value_index.rb.erb +1 -1
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_image_tokens.rb.erb +1 -1
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_indexes.rb.erb +1 -1
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_per_tag_budgets.rb.erb +1 -1
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/upgrade_call_rollups_provider_generator.rb +0 -6
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/upgrade_call_tags_key_value_index_generator.rb +0 -6
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/upgrade_image_tokens_generator.rb +0 -6
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/upgrade_indexes_generator.rb +3 -8
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/upgrade_per_tag_budgets_generator.rb +3 -8
- data/lib/llm_cost_tracker/ingestion/inbox.rb +2 -8
- data/lib/llm_cost_tracker/ingestion.rb +3 -1
- data/lib/llm_cost_tracker/integrations/anthropic.rb +63 -10
- data/lib/llm_cost_tracker/integrations/base.rb +39 -18
- data/lib/llm_cost_tracker/integrations/openai/batch_capture.rb +16 -13
- data/lib/llm_cost_tracker/integrations/openai/patches.rb +8 -0
- data/lib/llm_cost_tracker/integrations/openai.rb +42 -59
- data/lib/llm_cost_tracker/integrations/ruby_llm.rb +241 -76
- data/lib/llm_cost_tracker/integrations.rb +1 -20
- data/lib/llm_cost_tracker/ledger/isolation.rb +0 -7
- data/lib/llm_cost_tracker/ledger/period/totals.rb +14 -10
- data/lib/llm_cost_tracker/ledger/rollups.rb +14 -14
- data/lib/llm_cost_tracker/ledger/schema/adapter.rb +14 -3
- data/lib/llm_cost_tracker/ledger/schema/base.rb +5 -4
- data/lib/llm_cost_tracker/ledger/store.rb +2 -2
- data/lib/llm_cost_tracker/ledger/tags/breakdown.rb +1 -5
- data/lib/llm_cost_tracker/ledger/tags/encoding.rb +3 -12
- data/lib/llm_cost_tracker/ledger/tags/query.rb +0 -2
- data/lib/llm_cost_tracker/middleware/faraday.rb +36 -19
- data/lib/llm_cost_tracker/parsers.rb +7 -40
- data/lib/llm_cost_tracker/prices.json +2237 -154
- data/lib/llm_cost_tracker/pricing/backfill.rb +46 -13
- data/lib/llm_cost_tracker/pricing/calculation.rb +118 -75
- data/lib/llm_cost_tracker/pricing/effective_prices.rb +11 -10
- data/lib/llm_cost_tracker/pricing/estimator.rb +5 -2
- data/lib/llm_cost_tracker/pricing/matcher.rb +36 -22
- data/lib/llm_cost_tracker/pricing/mode.rb +2 -13
- data/lib/llm_cost_tracker/pricing/price_key.rb +5 -3
- data/lib/llm_cost_tracker/pricing/rate.rb +1 -0
- data/lib/llm_cost_tracker/pricing/registry.rb +5 -16
- data/lib/llm_cost_tracker/pricing/service_rates.rb +1 -8
- data/lib/llm_cost_tracker/pricing/sync/registry_diff.rb +13 -9
- data/lib/llm_cost_tracker/pricing/sync.rb +22 -71
- data/lib/llm_cost_tracker/providers/anthropic/parser.rb +33 -3
- data/lib/llm_cost_tracker/providers/anthropic/response_parser.rb +11 -5
- data/lib/llm_cost_tracker/providers/anthropic/usage_extractor.rb +81 -12
- data/lib/llm_cost_tracker/providers/azure/parser.rb +3 -7
- data/lib/llm_cost_tracker/providers/gemini/parser.rb +172 -48
- data/lib/llm_cost_tracker/providers/gemini/usage_extractor.rb +13 -22
- data/lib/llm_cost_tracker/providers/openai/hosts.rb +1 -1
- data/lib/llm_cost_tracker/providers/openai/parser.rb +11 -13
- data/lib/llm_cost_tracker/providers/openai/response_parser.rb +96 -26
- data/lib/llm_cost_tracker/providers/openai/service_charges.rb +57 -40
- data/lib/llm_cost_tracker/providers/openai/usage_extractor.rb +14 -1
- data/lib/llm_cost_tracker/providers/openai_compatible/parser.rb +3 -1
- data/lib/llm_cost_tracker/report/data.rb +1 -1
- data/lib/llm_cost_tracker/retention.rb +1 -3
- data/lib/llm_cost_tracker/tracker.rb +17 -9
- data/lib/llm_cost_tracker/usage/catalog.rb +13 -4
- data/lib/llm_cost_tracker/usage/dimension.rb +1 -1
- data/lib/llm_cost_tracker/usage/dimensions.yml +61 -0
- data/lib/llm_cost_tracker/version.rb +1 -1
- data/lib/tasks/llm_cost_tracker.rake +24 -21
- data/llm_cost_tracker.gemspec +63 -0
- metadata +8 -7
|
@@ -4,7 +4,6 @@ module LlmCostTracker
|
|
|
4
4
|
module Dashboard
|
|
5
5
|
class TopModels
|
|
6
6
|
DEFAULT_LIMIT = 5
|
|
7
|
-
SORT_OPTIONS = %w[cost calls avg_cost latency tokens provider name].freeze
|
|
8
7
|
DEFAULT_SORT = "cost"
|
|
9
8
|
DEFAULT_DIRECTIONS = {
|
|
10
9
|
"provider" => "asc",
|
|
@@ -43,7 +42,7 @@ module LlmCostTracker
|
|
|
43
42
|
def initialize(scope:, limit:, sort: DEFAULT_SORT, direction: nil)
|
|
44
43
|
@scope = scope
|
|
45
44
|
@limit = limit
|
|
46
|
-
@sort =
|
|
45
|
+
@sort = DEFAULT_DIRECTIONS.key?(sort.to_s) ? sort.to_s : DEFAULT_SORT
|
|
47
46
|
@direction = Sort::DIRECTIONS.include?(direction.to_s) ? direction.to_s : DEFAULT_DIRECTIONS[@sort]
|
|
48
47
|
end
|
|
49
48
|
|
|
@@ -5,13 +5,10 @@
|
|
|
5
5
|
value = @call[token_key]
|
|
6
6
|
{ label: component.fetch(:label), value: value, formatted_value: number_with_delimiter(value), css_class: component.fetch(:css_class) }
|
|
7
7
|
end %>
|
|
8
|
-
<% cost_segments =
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
{ label: component.fetch(:label), value: value, formatted_value: optional_money(value), css_class: component.fetch(:css_class) }
|
|
13
|
-
end %>
|
|
14
|
-
<% end %>
|
|
8
|
+
<% cost_segments = priced_components.map do |component|
|
|
9
|
+
value = line_item_costs_by_component[component.fetch(:price_key)]
|
|
10
|
+
{ label: component.fetch(:label), value: value, formatted_value: optional_money(value), css_class: component.fetch(:css_class) }
|
|
11
|
+
end %>
|
|
15
12
|
<% service_line_items = @call.line_items.reject { |li| li.unit == "token" }.sort_by(&:position) %>
|
|
16
13
|
|
|
17
14
|
<p class="lct-breadcrumb-back"><%= link_to "← All calls", calls_path %></p>
|
|
@@ -84,22 +81,11 @@ end %>
|
|
|
84
81
|
<dd><%= content_tag(:code, @call.provider_response_id, class: "lct-code-id") %></dd>
|
|
85
82
|
</div>
|
|
86
83
|
<% end %>
|
|
87
|
-
<%
|
|
88
|
-
|
|
89
|
-
<dt>Project ID</dt>
|
|
90
|
-
<dd><%= content_tag(:code, LlmCostTracker::Dashboard::Masking.mask_value(:provider_project_id, @call.provider_project_id), class: "lct-code-id") %></dd>
|
|
91
|
-
</div>
|
|
92
|
-
<% end %>
|
|
93
|
-
<% if @call.provider_api_key_id.present? %>
|
|
94
|
-
<div class="lct-meta-strip-item">
|
|
95
|
-
<dt>API Key ID</dt>
|
|
96
|
-
<dd><%= content_tag(:code, LlmCostTracker::Dashboard::Masking.mask_value(:provider_api_key_id, @call.provider_api_key_id), class: "lct-code-id") %></dd>
|
|
97
|
-
</div>
|
|
98
|
-
<% end %>
|
|
99
|
-
<% if @call.provider_workspace_id.present? %>
|
|
84
|
+
<% { provider_project_id: "Project ID", provider_api_key_id: "API Key ID", provider_workspace_id: "Workspace ID" }.each do |attribute, label| %>
|
|
85
|
+
<% next if @call[attribute].blank? %>
|
|
100
86
|
<div class="lct-meta-strip-item">
|
|
101
|
-
<dt
|
|
102
|
-
<dd><%= content_tag(:code, LlmCostTracker::Dashboard::Masking.mask_value(
|
|
87
|
+
<dt><%= label %></dt>
|
|
88
|
+
<dd><%= content_tag(:code, LlmCostTracker::Dashboard::Masking.mask_value(attribute, @call[attribute]), class: "lct-code-id") %></dd>
|
|
103
89
|
</div>
|
|
104
90
|
<% end %>
|
|
105
91
|
</dl>
|
|
@@ -271,7 +271,7 @@
|
|
|
271
271
|
<h2 class="lct-panel-title">Incomplete pricing by model</h2>
|
|
272
272
|
<span class="lct-panel-meta"><%= link_to "View calls →", calls_path(current_query(cost_status: "incomplete", sort: nil)) %></span>
|
|
273
273
|
</div>
|
|
274
|
-
<p class="lct-panel-intro">These models have line items without configured rates, so totals undercount (a full row is missing all rates; a partial row has some line items priced and others not). After the next price refresh or a <code>pricing_overrides</code> update, run <code>bin/rails llm_cost_tracker:backfill_unknown_pricing</code> to recompute these calls.</p>
|
|
274
|
+
<p class="lct-panel-intro">These models have line items without configured rates, so totals undercount (a full row is missing all rates; a partial row has some line items priced and others not). After the next price refresh or a <code>pricing_overrides</code> update, run <code>bin/rails llm_cost_tracker:backfill_unknown_pricing</code> to recompute these calls. A partial call is left as recorded when a rate it already used has changed since.</p>
|
|
275
275
|
|
|
276
276
|
<div class="lct-table-wrap">
|
|
277
277
|
<table class="lct-tbl">
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
<% variant_class = local_assigns[:variant] == "budget" ? " lct-budget-fill" : "" %>
|
|
2
|
-
|
|
3
1
|
<div class="lct-bar-track" aria-hidden="true">
|
|
4
|
-
<div data-lct-style="<%= inline_style("width: #{bar_width(value, max)}") %>" class="lct-bar-fill
|
|
2
|
+
<div data-lct-style="<%= inline_style("width: #{bar_width(value, max)}") %>" class="lct-bar-fill"></div>
|
|
5
3
|
</div>
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
<% extra_hidden = local_assigns.fetch(:extra_hidden, {}) %>
|
|
2
|
-
<% extra_except = local_assigns.fetch(:extra_except, []) %>
|
|
3
1
|
<details class="lct-filter-pop" name="lct-filter">
|
|
4
2
|
<summary class="lct-filter-pill">
|
|
5
3
|
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="4" width="18" height="18" rx="2"/><path d="M16 2v4M8 2v4M3 10h18"/></svg>
|
|
@@ -8,8 +6,7 @@
|
|
|
8
6
|
<svg class="lct-chev" width="10" height="6" viewBox="0 0 10 6" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M1 1l4 4 4-4"/></svg>
|
|
9
7
|
</summary>
|
|
10
8
|
<%= form_with url: path, method: :get, local: true, html: { class: "lct-filter-pop-body" } do %>
|
|
11
|
-
|
|
12
|
-
<%= hidden_query_fields(current_query.except(:from, :to, :page, :per, *extra_except)) %>
|
|
9
|
+
<%= hidden_query_fields(current_query.except(:from, :to, :page, :per)) %>
|
|
13
10
|
<div class="lct-filter-pop-field"><label for="lct-filter-from">From</label><input type="date" name="from" id="lct-filter-from" value="<%= @from_date.iso8601 %>"></div>
|
|
14
11
|
<div class="lct-filter-pop-field"><label for="lct-filter-to">To</label><input type="date" name="to" id="lct-filter-to" value="<%= @to_date.iso8601 %>"></div>
|
|
15
12
|
<button type="submit" class="lct-button lct-button-primary">Apply</button>
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
<% extra_hidden = local_assigns.fetch(:extra_hidden, {}) %>
|
|
2
|
-
<% extra_except = local_assigns.fetch(:extra_except, []) %>
|
|
3
1
|
<% active = params[:model].presence %>
|
|
4
2
|
<details class="lct-filter-pop" name="lct-filter">
|
|
5
3
|
<summary class="lct-filter-pill <%= 'lct-active' if active %>">
|
|
@@ -9,8 +7,7 @@
|
|
|
9
7
|
<svg class="lct-chev" width="10" height="6" viewBox="0 0 10 6" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M1 1l4 4 4-4"/></svg>
|
|
10
8
|
</summary>
|
|
11
9
|
<%= form_with url: path, method: :get, local: true, html: { class: "lct-filter-pop-body" } do %>
|
|
12
|
-
|
|
13
|
-
<%= hidden_query_fields(current_query.except(:model, :page, :per, *extra_except)) %>
|
|
10
|
+
<%= hidden_query_fields(current_query.except(:model, :page, :per)) %>
|
|
14
11
|
<div class="lct-filter-pop-field">
|
|
15
12
|
<label for="lct-filter-model">Model</label>
|
|
16
13
|
<%= select_tag :model, options_for_select(filter_options_for(:model), active), include_blank: "All models", id: "lct-filter-model" %>
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
<% extra_hidden = local_assigns.fetch(:extra_hidden, {}) %>
|
|
2
|
-
<% extra_except = local_assigns.fetch(:extra_except, []) %>
|
|
3
1
|
<% active = params[:provider].presence %>
|
|
4
2
|
<details class="lct-filter-pop" name="lct-filter">
|
|
5
3
|
<summary class="lct-filter-pill <%= 'lct-active' if active %>">
|
|
@@ -9,8 +7,7 @@
|
|
|
9
7
|
<svg class="lct-chev" width="10" height="6" viewBox="0 0 10 6" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M1 1l4 4 4-4"/></svg>
|
|
10
8
|
</summary>
|
|
11
9
|
<%= form_with url: path, method: :get, local: true, html: { class: "lct-filter-pop-body" } do %>
|
|
12
|
-
|
|
13
|
-
<%= hidden_query_fields(current_query.except(:provider, :page, :per, *extra_except)) %>
|
|
10
|
+
<%= hidden_query_fields(current_query.except(:provider, :page, :per)) %>
|
|
14
11
|
<div class="lct-filter-pop-field">
|
|
15
12
|
<label for="lct-filter-provider">Provider</label>
|
|
16
13
|
<%= select_tag :provider, options_for_select(filter_options_for(:provider), active), include_blank: "All providers", id: "lct-filter-provider" %>
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
<% extra_hidden = local_assigns.fetch(:extra_hidden, {}) %>
|
|
2
|
-
<% extra_except = local_assigns.fetch(:extra_except, []) %>
|
|
3
1
|
<% active = params[:stream].presence %>
|
|
4
2
|
<% display_value = case active when "yes" then "Streaming" when "no" then "Non-streaming" else "All" end %>
|
|
5
3
|
<details class="lct-filter-pop" name="lct-filter">
|
|
@@ -10,8 +8,7 @@
|
|
|
10
8
|
<svg class="lct-chev" width="10" height="6" viewBox="0 0 10 6" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M1 1l4 4 4-4"/></svg>
|
|
11
9
|
</summary>
|
|
12
10
|
<%= form_with url: path, method: :get, local: true, html: { class: "lct-filter-pop-body" } do %>
|
|
13
|
-
|
|
14
|
-
<%= hidden_query_fields(current_query.except(:stream, :page, :per, *extra_except)) %>
|
|
11
|
+
<%= hidden_query_fields(current_query.except(:stream, :page, :per)) %>
|
|
15
12
|
<div class="lct-filter-pop-field">
|
|
16
13
|
<label for="lct-filter-stream">Stream</label>
|
|
17
14
|
<%= select_tag :stream, options_for_select(LlmCostTracker::Dashboard::Filter::STREAM_FILTER_OPTIONS, active), include_blank: "All calls", id: "lct-filter-stream" %>
|
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
<h2 class="lct-page-title">Tag: <code class="lct-code-id"><%= params[:key] %></code> = <code class="lct-code-id"><%= @value %></code></h2>
|
|
6
6
|
|
|
7
7
|
<div class="lct-filter-row">
|
|
8
|
-
<%= render "llm_cost_tracker/shared/filter_pill_date", path: form_url
|
|
9
|
-
<%= render "llm_cost_tracker/shared/filter_pill_provider", path: form_url
|
|
10
|
-
<%= render "llm_cost_tracker/shared/filter_pill_model", path: form_url
|
|
8
|
+
<%= render "llm_cost_tracker/shared/filter_pill_date", path: form_url %>
|
|
9
|
+
<%= render "llm_cost_tracker/shared/filter_pill_provider", path: form_url %>
|
|
10
|
+
<%= render "llm_cost_tracker/shared/filter_pill_model", path: form_url %>
|
|
11
11
|
|
|
12
12
|
<% if params[:provider].present? || params[:model].present? %>
|
|
13
13
|
<%= link_to "× Clear filters", tag_path(params[:key], current_query(provider: nil, model: nil, page: nil)), class: "lct-filter-clear" %>
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
</div>
|
|
36
36
|
<div class="lct-stat">
|
|
37
37
|
<div class="lct-stat-head"><p class="lct-stat-label">Avg cost / call</p></div>
|
|
38
|
-
<p class="lct-stat-value"><%= money(@
|
|
38
|
+
<p class="lct-stat-value"><%= money(@value_total_cost / @value_calls) %></p>
|
|
39
39
|
</div>
|
|
40
40
|
</div>
|
|
41
41
|
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
<h2 class="lct-panel-title">Spend over time</h2>
|
|
45
45
|
<span class="lct-panel-meta"><%= link_to "View calls →", calls_path(calls_query_for_tag(key: @key, value: @value)) %></span>
|
|
46
46
|
</div>
|
|
47
|
-
<%= render "llm_cost_tracker/shared/spend_chart", series: @value_points
|
|
47
|
+
<%= render "llm_cost_tracker/shared/spend_chart", series: @value_points %>
|
|
48
48
|
</section>
|
|
49
49
|
<% end %>
|
|
50
50
|
|
|
@@ -30,7 +30,7 @@ module LlmCostTracker
|
|
|
30
30
|
end
|
|
31
31
|
|
|
32
32
|
def blocking?
|
|
33
|
-
|
|
33
|
+
configured.each_value.any? { |entry| behavior_for(entry) == :block_requests } && active?
|
|
34
34
|
end
|
|
35
35
|
|
|
36
36
|
def rules_for(tags, blocking_only: false)
|
|
@@ -38,13 +38,13 @@ module LlmCostTracker
|
|
|
38
38
|
|
|
39
39
|
normalized = (tags || {}).to_h.transform_keys(&:to_s)
|
|
40
40
|
configured.filter_map do |key, entry|
|
|
41
|
-
|
|
42
|
-
next if
|
|
41
|
+
value = Ledger::Tags::Encoding.encode(normalized[key])
|
|
42
|
+
next if value.empty?
|
|
43
43
|
next if blocking_only && behavior_for(entry) != :block_requests
|
|
44
44
|
|
|
45
45
|
Rule.new(
|
|
46
46
|
key: key,
|
|
47
|
-
value:
|
|
47
|
+
value: value,
|
|
48
48
|
windows: entry[:windows],
|
|
49
49
|
behavior: behavior_for(entry),
|
|
50
50
|
on_exceeded: on_exceeded_for(entry)
|
|
@@ -53,21 +53,27 @@ module LlmCostTracker
|
|
|
53
53
|
end
|
|
54
54
|
|
|
55
55
|
def rules_for_events(events)
|
|
56
|
-
events.each_with_object({}) do |event, grouped|
|
|
56
|
+
events.select(&:total_cost).each_with_object({}) do |event, grouped|
|
|
57
57
|
rules_for(event.tags).each { |rule| (grouped[rule] ||= []) << event }
|
|
58
58
|
end
|
|
59
59
|
end
|
|
60
60
|
|
|
61
61
|
def spend(key, value, window, time:)
|
|
62
|
-
spend_by_value(key, [value], window, window_start(window, time)).fetch(value, 0).to_d
|
|
62
|
+
spend_by_value(key, [value], window, window_start(window, time)).fetch(value, [0]).first.to_d
|
|
63
63
|
end
|
|
64
64
|
|
|
65
|
-
def spend_by_value(key, values, window, bucket)
|
|
65
|
+
def spend_by_value(key, values, window, bucket, upto = nil)
|
|
66
66
|
started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
67
|
+
range = window_range(window, bucket)
|
|
68
|
+
upto_sum = LlmCostTracker::CallTag.sanitize_sql_array(
|
|
69
|
+
["SUM(CASE WHEN #{TIME_COLUMN} <= ? THEN #{COST_COLUMN} ELSE 0 END)", upto || range.end]
|
|
70
|
+
)
|
|
67
71
|
totals = Ledger::Isolation.guard(LlmCostTracker::CallTag) do
|
|
68
72
|
LlmCostTracker::CallTag
|
|
69
|
-
.where(key: key, value: values, TIME_COLUMN =>
|
|
70
|
-
.group(:value)
|
|
73
|
+
.where(key: key, value: values, TIME_COLUMN => range)
|
|
74
|
+
.group(:value)
|
|
75
|
+
.pluck(:value, Arel.sql("SUM(#{COST_COLUMN})"), Arel.sql(upto_sum))
|
|
76
|
+
.to_h { |value, *sums| [value, sums.map(&:to_d)] }
|
|
71
77
|
end
|
|
72
78
|
warn_slow_read(key, window, Process.clock_gettime(Process::CLOCK_MONOTONIC) - started_at)
|
|
73
79
|
totals
|
|
@@ -37,25 +37,40 @@ module LlmCostTracker
|
|
|
37
37
|
config = LlmCostTracker.configuration
|
|
38
38
|
return unless event.total_cost
|
|
39
39
|
|
|
40
|
-
check_per_call_budget(event, config, behavior_override)
|
|
40
|
+
errors = [check_per_call_budget(event, config, behavior_override)]
|
|
41
41
|
check_windowed({ daily: config.budgets.daily, monthly: config.budgets.monthly }.compact,
|
|
42
42
|
time: event.tracked_at) do |budget_type, total, budget|
|
|
43
|
-
handle_exceeded(budget_type: budget_type,
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
43
|
+
errors << handle_exceeded(budget_type: budget_type,
|
|
44
|
+
total: total,
|
|
45
|
+
budget: budget,
|
|
46
|
+
previous_total: total - event.total_cost,
|
|
47
|
+
last_event: event,
|
|
48
|
+
behavior: behavior_override)
|
|
49
49
|
end
|
|
50
|
+
errors.concat(persisted_errors([event], behavior_override)) unless Ingestion.async?
|
|
51
|
+
raise_first(errors)
|
|
50
52
|
end
|
|
51
53
|
|
|
52
54
|
def check_persisted!(events, behavior_override: nil)
|
|
53
|
-
|
|
55
|
+
raise_first(persisted_errors(events, behavior_override))
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def notify_persisted_safely!(events)
|
|
59
|
+
check_persisted!(events, behavior_override: :notify)
|
|
60
|
+
rescue StandardError => e
|
|
61
|
+
Logging.warn("Per-tag budget check failed after ingest: #{e.class}: #{e.message}")
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
private
|
|
65
|
+
|
|
66
|
+
def persisted_errors(events, behavior_override)
|
|
67
|
+
by_rule = PerTag.rules_for_events(events)
|
|
54
68
|
by_rule = by_rule.reject { |rule, _| rule.on_exceeded.nil? } if behavior_override == :notify
|
|
55
|
-
window_buckets(by_rule).
|
|
56
|
-
|
|
57
|
-
scored.
|
|
58
|
-
|
|
69
|
+
window_buckets(by_rule).flat_map do |(key, window, bucket), scored|
|
|
70
|
+
upto = scored.values.flatten.map(&:tracked_at).max unless Ingestion.async?
|
|
71
|
+
totals = PerTag.spend_by_value(key, scored.keys.map(&:value), window, bucket, upto)
|
|
72
|
+
scored.filter_map do |rule, recorded|
|
|
73
|
+
total, upto_total = totals.fetch(rule.value, [0, 0])
|
|
59
74
|
limit = rule.windows.fetch(window)
|
|
60
75
|
next if total < limit
|
|
61
76
|
|
|
@@ -63,7 +78,7 @@ module LlmCostTracker
|
|
|
63
78
|
budget_type: window,
|
|
64
79
|
total: total,
|
|
65
80
|
budget: limit,
|
|
66
|
-
previous_total: total - recorded.sum(&:total_cost),
|
|
81
|
+
previous_total: (upto_total >= limit ? upto_total : total) - recorded.sum(&:total_cost),
|
|
67
82
|
last_event: recorded.last,
|
|
68
83
|
scope: scope_for(rule),
|
|
69
84
|
behavior: behavior_override || rule.behavior,
|
|
@@ -73,14 +88,11 @@ module LlmCostTracker
|
|
|
73
88
|
end
|
|
74
89
|
end
|
|
75
90
|
|
|
76
|
-
def
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
Logging.warn("Per-tag budget check failed after ingest: #{e.class}: #{e.message}")
|
|
91
|
+
def raise_first(errors)
|
|
92
|
+
error = errors.compact.first
|
|
93
|
+
raise error if error
|
|
80
94
|
end
|
|
81
95
|
|
|
82
|
-
private
|
|
83
|
-
|
|
84
96
|
def window_buckets(by_rule)
|
|
85
97
|
by_rule.each_with_object({}) do |(rule, events), grouped|
|
|
86
98
|
rule.windows.each_key do |window|
|
|
@@ -98,14 +110,6 @@ module LlmCostTracker
|
|
|
98
110
|
Pricing::Estimator.call(provider: provider, model: model, request: request) || BigDecimal("0")
|
|
99
111
|
end
|
|
100
112
|
|
|
101
|
-
def raise_per_call_pre_send(estimate, budget)
|
|
102
|
-
return unless estimate >= budget
|
|
103
|
-
|
|
104
|
-
raise BudgetExceededError.new(**budget_payload(
|
|
105
|
-
budget_type: :per_call, total: estimate, budget: budget, last_event: nil, stage: :pre_send
|
|
106
|
-
))
|
|
107
|
-
end
|
|
108
|
-
|
|
109
113
|
def check_per_call_budget(event, config, behavior_override)
|
|
110
114
|
budget = config.budgets.per_call
|
|
111
115
|
return unless budget
|
|
@@ -122,7 +126,10 @@ module LlmCostTracker
|
|
|
122
126
|
end
|
|
123
127
|
|
|
124
128
|
def enforce_globally(config, estimate:, time:)
|
|
125
|
-
|
|
129
|
+
per_call = config.budgets.per_call
|
|
130
|
+
if per_call && estimate.positive? && estimate >= per_call
|
|
131
|
+
raise_pre_send(budget_type: :per_call, total: estimate, budget: per_call)
|
|
132
|
+
end
|
|
126
133
|
|
|
127
134
|
check_windowed({ monthly: config.budgets.monthly, daily: config.budgets.daily }.compact,
|
|
128
135
|
time: time,
|
|
@@ -132,9 +139,9 @@ module LlmCostTracker
|
|
|
132
139
|
end
|
|
133
140
|
|
|
134
141
|
def raise_pre_send(budget_type:, total:, budget:, scope: nil)
|
|
135
|
-
raise BudgetExceededError.new(
|
|
136
|
-
budget_type: budget_type, total: total, budget: budget,
|
|
137
|
-
)
|
|
142
|
+
raise BudgetExceededError.new(
|
|
143
|
+
budget_type: budget_type, total: total, budget: budget, stage: :pre_send, scope: scope
|
|
144
|
+
)
|
|
138
145
|
end
|
|
139
146
|
|
|
140
147
|
def check_per_tag(tags, time:, estimate: BigDecimal("0"), blocking_only: false)
|
|
@@ -153,21 +160,14 @@ module LlmCostTracker
|
|
|
153
160
|
def check_windowed(budgets, time:, estimate: BigDecimal("0"))
|
|
154
161
|
return if budgets.empty?
|
|
155
162
|
|
|
156
|
-
|
|
163
|
+
periods = budgets.keys.map { |type| BUDGET_TYPE_TO_PERIOD.fetch(type) }
|
|
164
|
+
totals = LlmCostTracker::Ledger::Period::Totals.call(periods, time: time)
|
|
157
165
|
budgets.each do |budget_type, budget|
|
|
158
|
-
total = totals.fetch(budget_type) + estimate
|
|
166
|
+
total = totals.fetch(BUDGET_TYPE_TO_PERIOD.fetch(budget_type)) + estimate
|
|
159
167
|
yield(budget_type, total, budget) if total >= budget
|
|
160
168
|
end
|
|
161
169
|
end
|
|
162
170
|
|
|
163
|
-
def totals_for(budget_types, time:)
|
|
164
|
-
return {} if budget_types.empty?
|
|
165
|
-
|
|
166
|
-
period_for = budget_types.to_h { |type| [type, BUDGET_TYPE_TO_PERIOD.fetch(type)] }
|
|
167
|
-
period_totals = LlmCostTracker::Ledger::Period::Totals.call(period_for.values, time: time)
|
|
168
|
-
period_for.transform_values { |period| period_totals.fetch(period) }
|
|
169
|
-
end
|
|
170
|
-
|
|
171
171
|
def handle_exceeded(budget_type:,
|
|
172
172
|
total:,
|
|
173
173
|
budget:,
|
|
@@ -179,28 +179,17 @@ module LlmCostTracker
|
|
|
179
179
|
config = LlmCostTracker.configuration
|
|
180
180
|
behavior ||= config.budgets.exceeded_behavior
|
|
181
181
|
on_exceeded ||= config.budgets.on_exceeded
|
|
182
|
-
payload =
|
|
182
|
+
payload = {
|
|
183
183
|
budget_type: budget_type,
|
|
184
184
|
total: total,
|
|
185
185
|
budget: budget,
|
|
186
186
|
last_event: last_event,
|
|
187
187
|
stage: :post_spend,
|
|
188
188
|
scope: scope
|
|
189
|
-
|
|
189
|
+
}
|
|
190
190
|
|
|
191
191
|
on_exceeded.call(payload) if on_exceeded && (previous_total.nil? || previous_total < budget)
|
|
192
|
-
|
|
193
|
-
end
|
|
194
|
-
|
|
195
|
-
def budget_payload(budget_type:, total:, budget:, last_event:, stage:, scope: nil)
|
|
196
|
-
{
|
|
197
|
-
budget_type: budget_type,
|
|
198
|
-
total: total,
|
|
199
|
-
budget: budget,
|
|
200
|
-
last_event: last_event,
|
|
201
|
-
stage: stage,
|
|
202
|
-
scope: scope
|
|
203
|
-
}
|
|
192
|
+
BudgetExceededError.new(**payload) if %i[raise block_requests].include?(behavior)
|
|
204
193
|
end
|
|
205
194
|
end
|
|
206
195
|
end
|
|
@@ -8,7 +8,7 @@ module LlmCostTracker
|
|
|
8
8
|
HEAD_EVENTS = 16
|
|
9
9
|
TAIL_EVENTS = 32
|
|
10
10
|
HEAVY_STRING_BYTES = 8 * 1024
|
|
11
|
-
IGNORED_PAYLOAD_KEYS = %w[b64_json partial_image_b64].freeze
|
|
11
|
+
IGNORED_PAYLOAD_KEYS = %w[b64_json partial_image_b64 snapshot logprobs].freeze
|
|
12
12
|
|
|
13
13
|
def initialize(notable: nil)
|
|
14
14
|
@notable = notable
|
|
@@ -20,7 +20,8 @@ module LlmCostTracker
|
|
|
20
20
|
end
|
|
21
21
|
|
|
22
22
|
def push(data, type: nil)
|
|
23
|
-
|
|
23
|
+
# The openai gem's chat stream helper resends all logprobs so far in logprobs.* events; none carry usage.
|
|
24
|
+
return if @overflowed || type&.start_with?("logprobs.")
|
|
24
25
|
|
|
25
26
|
event = { event: type, data: strip_heavy_payload(data) }
|
|
26
27
|
size = approximate_bytesize(event)
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
require "active_support/core_ext/object/deep_dup"
|
|
4
4
|
require "active_support/core_ext/object/try"
|
|
5
5
|
|
|
6
|
+
require_relative "event_window"
|
|
7
|
+
|
|
6
8
|
module LlmCostTracker
|
|
7
9
|
module Capture
|
|
8
10
|
module SdkPayload
|
|
@@ -11,7 +13,9 @@ module LlmCostTracker
|
|
|
11
13
|
def normalize(value)
|
|
12
14
|
case value
|
|
13
15
|
when Hash
|
|
14
|
-
value.each_with_object({})
|
|
16
|
+
value.each_with_object({}) do |(key, nested), out|
|
|
17
|
+
out[key.to_s] = normalize(nested) unless EventWindow::IGNORED_PAYLOAD_KEYS.include?(key.to_s)
|
|
18
|
+
end
|
|
15
19
|
when Array
|
|
16
20
|
value.map { |nested| normalize(nested) }
|
|
17
21
|
when Symbol
|
|
@@ -5,6 +5,7 @@ require "active_support/core_ext/object/deep_dup"
|
|
|
5
5
|
require "json"
|
|
6
6
|
|
|
7
7
|
require_relative "event_window"
|
|
8
|
+
require_relative "sdk_payload"
|
|
8
9
|
require_relative "../timing"
|
|
9
10
|
|
|
10
11
|
module LlmCostTracker
|
|
@@ -21,7 +22,6 @@ module LlmCostTracker
|
|
|
21
22
|
provider_workspace_id: nil,
|
|
22
23
|
pricing_mode: nil,
|
|
23
24
|
metadata: {},
|
|
24
|
-
context_tags: nil,
|
|
25
25
|
request: nil)
|
|
26
26
|
@provider = provider.to_s
|
|
27
27
|
@model = model
|
|
@@ -32,7 +32,7 @@ module LlmCostTracker
|
|
|
32
32
|
@provider_workspace_id = provider_workspace_id
|
|
33
33
|
@pricing_mode = pricing_mode
|
|
34
34
|
@metadata = (metadata || {}).deep_dup
|
|
35
|
-
@context_tags =
|
|
35
|
+
@context_tags = LlmCostTracker::Tags::Context.tags.deep_dup
|
|
36
36
|
@request = request
|
|
37
37
|
@window = EventWindow.new(notable: Parsers.find_for_provider(@provider)&.method(:retain_stream_event?))
|
|
38
38
|
@explicit_usage = nil
|
|
@@ -57,9 +57,10 @@ module LlmCostTracker
|
|
|
57
57
|
end
|
|
58
58
|
|
|
59
59
|
def event(data, type: nil)
|
|
60
|
+
data = SdkPayload.normalize(data) if data.is_a?(Hash)
|
|
60
61
|
@mutex.synchronize do
|
|
61
62
|
ensure_open!
|
|
62
|
-
@window.push(data, type: type) unless data.nil?
|
|
63
|
+
@window.push(data, type: type&.to_s) unless data.nil?
|
|
63
64
|
end
|
|
64
65
|
end
|
|
65
66
|
|
|
@@ -91,6 +92,8 @@ module LlmCostTracker
|
|
|
91
92
|
record_snapshot(snapshot, errored: errored)
|
|
92
93
|
rescue TransactionAbortedError
|
|
93
94
|
raise
|
|
95
|
+
rescue ActiveRecord::RecordNotUnique
|
|
96
|
+
nil
|
|
94
97
|
rescue StandardError => e
|
|
95
98
|
raise unless errored
|
|
96
99
|
|
|
@@ -165,20 +168,27 @@ module LlmCostTracker
|
|
|
165
168
|
end
|
|
166
169
|
|
|
167
170
|
def build_event(snapshot)
|
|
168
|
-
return
|
|
169
|
-
return build_unknown_usage(snapshot) if snapshot[:overflowed]
|
|
171
|
+
return build_unparsed_event(snapshot) if snapshot[:explicit_usage]
|
|
170
172
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
173
|
+
if snapshot[:overflowed]
|
|
174
|
+
Logging.warn("#{@provider} stream events exceeded #{SSE::LIMIT_BYTES} bytes; " \
|
|
175
|
+
"recording usage_source=#{Usage::Source::UNKNOWN}.")
|
|
176
|
+
return build_unparsed_event(snapshot)
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
request_body = request_body_for(snapshot[:request])
|
|
180
|
+
events = Parsers.all_for_provider(@provider).filter_map do |parser|
|
|
181
|
+
parser.parse_stream(
|
|
182
|
+
response_status: 200, events: snapshot[:events], request_body: request_body, model: snapshot[:model]
|
|
183
|
+
)
|
|
184
|
+
end
|
|
185
|
+
event = events.find { |parsed| parsed.usage_source != Usage::Source::UNKNOWN } || events.first
|
|
176
186
|
if event
|
|
177
187
|
model = present_model(event.model) || present_model(snapshot[:model]) || Event::UNKNOWN_MODEL
|
|
178
188
|
return event.with(provider: @provider, model: model, **snapshot.fetch(:capture_dimensions))
|
|
179
189
|
end
|
|
180
190
|
|
|
181
|
-
|
|
191
|
+
build_unparsed_event(snapshot)
|
|
182
192
|
end
|
|
183
193
|
|
|
184
194
|
def request_body_for(request)
|
|
@@ -194,25 +204,14 @@ module LlmCostTracker
|
|
|
194
204
|
string unless string == Event::UNKNOWN_MODEL
|
|
195
205
|
end
|
|
196
206
|
|
|
197
|
-
def
|
|
198
|
-
|
|
199
|
-
provider: @provider,
|
|
200
|
-
model: snapshot[:model] || Event::UNKNOWN_MODEL,
|
|
201
|
-
token_usage: snapshot[:explicit_usage],
|
|
202
|
-
stream: true,
|
|
203
|
-
usage_source: Usage::Source::MANUAL,
|
|
204
|
-
pricing_mode: snapshot[:pricing_mode],
|
|
205
|
-
**snapshot.fetch(:capture_dimensions)
|
|
206
|
-
)
|
|
207
|
-
end
|
|
208
|
-
|
|
209
|
-
def build_unknown_usage(snapshot)
|
|
207
|
+
def build_unparsed_event(snapshot)
|
|
208
|
+
explicit_usage = snapshot[:explicit_usage]
|
|
210
209
|
Event.build(
|
|
211
210
|
provider: @provider,
|
|
212
211
|
model: snapshot[:model] || Event::UNKNOWN_MODEL,
|
|
213
|
-
token_usage: Usage::TokenUsage.build(input_tokens: 0, output_tokens: 0, total_tokens: 0),
|
|
212
|
+
token_usage: explicit_usage || Usage::TokenUsage.build(input_tokens: 0, output_tokens: 0, total_tokens: 0),
|
|
214
213
|
stream: true,
|
|
215
|
-
usage_source: Usage::Source::UNKNOWN,
|
|
214
|
+
usage_source: explicit_usage ? Usage::Source::MANUAL : Usage::Source::UNKNOWN,
|
|
216
215
|
pricing_mode: snapshot[:pricing_mode],
|
|
217
216
|
**snapshot.fetch(:capture_dimensions)
|
|
218
217
|
)
|
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "active_support/core_ext/object/deep_dup"
|
|
4
3
|
require "active_support/core_ext/object/try"
|
|
5
4
|
|
|
6
|
-
require_relative "sdk_payload"
|
|
7
|
-
|
|
8
5
|
module LlmCostTracker
|
|
9
6
|
module Capture
|
|
10
7
|
class StreamTracker
|
|
@@ -21,23 +18,15 @@ module LlmCostTracker
|
|
|
21
18
|
def wrap
|
|
22
19
|
return @stream unless @stream
|
|
23
20
|
|
|
24
|
-
|
|
25
|
-
if
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
end)
|
|
32
|
-
iterator_wrapped = true
|
|
33
|
-
end
|
|
34
|
-
end
|
|
35
|
-
each_wrapped = false
|
|
36
|
-
if !iterator_wrapped && @stream.respond_to?(:each)
|
|
21
|
+
iterator = @stream.instance_variable_get(:@iterator)
|
|
22
|
+
if iterator.respond_to?(:each)
|
|
23
|
+
@stream.instance_variable_set(:@iterator,
|
|
24
|
+
Enumerator.new do |yielder|
|
|
25
|
+
each_from(iterator) { |event| yielder << event }
|
|
26
|
+
end)
|
|
27
|
+
elsif @stream.respond_to?(:each)
|
|
37
28
|
wrap_each
|
|
38
|
-
|
|
39
|
-
end
|
|
40
|
-
unless iterator_wrapped || each_wrapped
|
|
29
|
+
else
|
|
41
30
|
Logging.warn(
|
|
42
31
|
"stream integration found no wrappable iterator on #{@stream.class} " \
|
|
43
32
|
"(missing both `@iterator` ivar and `#each`); usage will not be captured"
|
|
@@ -83,10 +72,7 @@ module LlmCostTracker
|
|
|
83
72
|
end
|
|
84
73
|
|
|
85
74
|
def capture(event)
|
|
86
|
-
|
|
87
|
-
payload = SdkPayload.normalize(raw_payload)
|
|
88
|
-
type = event.try(:type) || payload["type"]
|
|
89
|
-
@collector.event(payload, type: type&.to_s)
|
|
75
|
+
@collector.event(event.try(:to_h) || {}, type: event.try(:type)&.to_s)
|
|
90
76
|
rescue StandardError => e
|
|
91
77
|
warn_capture_failure(e)
|
|
92
78
|
end
|
|
@@ -25,7 +25,7 @@ module LlmCostTracker
|
|
|
25
25
|
[
|
|
26
26
|
enabled_check,
|
|
27
27
|
*integration_checks,
|
|
28
|
-
*
|
|
28
|
+
*LlmCostTracker::Ingestion.verify
|
|
29
29
|
].compact
|
|
30
30
|
end
|
|
31
31
|
|
|
@@ -49,11 +49,5 @@ module LlmCostTracker
|
|
|
49
49
|
check.with(name: "sdk integration #{check.name}")
|
|
50
50
|
end
|
|
51
51
|
end
|
|
52
|
-
|
|
53
|
-
def storage_checks
|
|
54
|
-
LlmCostTracker::Ingestion.verify
|
|
55
|
-
rescue LlmCostTracker::Error => e
|
|
56
|
-
[Check.new(:error, "storage", e.message)]
|
|
57
|
-
end
|
|
58
52
|
end
|
|
59
53
|
end
|