llm_cost_tracker 0.13.0 → 0.14.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 +53 -16
- data/README.md +16 -28
- data/app/controllers/llm_cost_tracker/application_controller.rb +3 -2
- data/app/controllers/llm_cost_tracker/data_quality_controller.rb +1 -0
- data/app/controllers/llm_cost_tracker/models_controller.rb +3 -1
- data/app/helpers/llm_cost_tracker/dashboard_query_helper.rb +11 -0
- data/app/models/llm_cost_tracker/call.rb +9 -3
- data/app/models/llm_cost_tracker/call_rollup.rb +19 -3
- data/app/services/llm_cost_tracker/dashboard/data_quality.rb +13 -0
- data/app/services/llm_cost_tracker/dashboard/monthly_budget.rb +1 -1
- data/app/services/llm_cost_tracker/dashboard/pagination.rb +2 -1
- data/app/services/llm_cost_tracker/dashboard/pricing_overview.rb +3 -3
- data/app/services/llm_cost_tracker/dashboard/setup_state.rb +5 -3
- data/app/views/llm_cost_tracker/calls/show.html.erb +6 -8
- data/app/views/llm_cost_tracker/data_quality/index.html.erb +11 -0
- data/app/views/llm_cost_tracker/shared/_filter_pill_date.html.erb +1 -3
- data/app/views/llm_cost_tracker/shared/_filter_pill_model.html.erb +1 -3
- data/app/views/llm_cost_tracker/shared/_filter_pill_provider.html.erb +1 -3
- data/app/views/llm_cost_tracker/shared/_filter_pill_stream.html.erb +1 -3
- data/lib/llm_cost_tracker/budget/per_tag.rb +159 -0
- data/lib/llm_cost_tracker/budget.rb +120 -32
- data/lib/llm_cost_tracker/charges/cost_status.rb +4 -3
- data/lib/llm_cost_tracker/configuration/budgets.rb +93 -0
- data/lib/llm_cost_tracker/configuration/capture.rb +42 -0
- data/lib/llm_cost_tracker/configuration/ingestion.rb +20 -0
- data/lib/llm_cost_tracker/configuration/mutability.rb +33 -0
- data/lib/llm_cost_tracker/configuration/pricing.rb +36 -0
- data/lib/llm_cost_tracker/configuration/section.rb +59 -0
- data/lib/llm_cost_tracker/configuration/tags.rb +52 -0
- data/lib/llm_cost_tracker/configuration.rb +69 -125
- data/lib/llm_cost_tracker/deprecator.rb +9 -0
- data/lib/llm_cost_tracker/doctor/ingestion_check.rb +17 -8
- data/lib/llm_cost_tracker/doctor/price_check.rb +1 -1
- data/lib/llm_cost_tracker/doctor.rb +4 -4
- data/lib/llm_cost_tracker/engine.rb +4 -0
- data/lib/llm_cost_tracker/errors.rb +12 -3
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/async_ingestion_generator.rb +2 -2
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/call_rollups_generator.rb +2 -2
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/create_llm_cost_tracker_async_ingestion.rb.erb +0 -1
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/create_llm_cost_tracker_calls.rb.erb +8 -4
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/initializer.rb.erb +50 -30
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_indexes.rb.erb +40 -0
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_per_tag_budgets.rb.erb +41 -0
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/upgrade_indexes_generator.rb +30 -0
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/upgrade_per_tag_budgets_generator.rb +30 -0
- data/lib/llm_cost_tracker/ingestion/batch.rb +3 -1
- data/lib/llm_cost_tracker/ingestion/pool.rb +9 -2
- data/lib/llm_cost_tracker/ingestion.rb +3 -7
- data/lib/llm_cost_tracker/integrations/openai/batch_capture.rb +10 -12
- data/lib/llm_cost_tracker/ledger/period/totals.rb +1 -1
- data/lib/llm_cost_tracker/ledger/rollups.rb +34 -4
- data/lib/llm_cost_tracker/ledger/store.rb +9 -2
- data/lib/llm_cost_tracker/ledger/tags/encoding.rb +15 -5
- data/lib/llm_cost_tracker/logging.rb +3 -5
- data/lib/llm_cost_tracker/middleware/faraday.rb +1 -1
- data/lib/llm_cost_tracker/prices.json +1291 -313
- data/lib/llm_cost_tracker/pricing/backfill.rb +11 -1
- data/lib/llm_cost_tracker/pricing/calculation.rb +4 -4
- data/lib/llm_cost_tracker/pricing/effective_prices.rb +26 -15
- data/lib/llm_cost_tracker/pricing/matcher.rb +7 -0
- data/lib/llm_cost_tracker/pricing/rate.rb +1 -2
- data/lib/llm_cost_tracker/pricing/registry.rb +5 -5
- data/lib/llm_cost_tracker/pricing/sync.rb +1 -1
- data/lib/llm_cost_tracker/pricing/unknown.rb +11 -8
- data/lib/llm_cost_tracker/providers/anthropic/usage_extractor.rb +3 -2
- data/lib/llm_cost_tracker/providers/openai/model_families.rb +0 -7
- data/lib/llm_cost_tracker/providers/openai/response_parser.rb +5 -2
- data/lib/llm_cost_tracker/providers/openai/usage_extractor.rb +5 -4
- data/lib/llm_cost_tracker/providers/openai_compatible/parser.rb +2 -2
- data/lib/llm_cost_tracker/railtie.rb +3 -7
- data/lib/llm_cost_tracker/report/data.rb +2 -2
- data/lib/llm_cost_tracker/retention.rb +22 -10
- data/lib/llm_cost_tracker/tags/context.rb +3 -3
- data/lib/llm_cost_tracker/tags/sanitizer.rb +4 -4
- data/lib/llm_cost_tracker/tracker.rb +14 -23
- data/lib/llm_cost_tracker/usage/catalog.rb +1 -2
- data/lib/llm_cost_tracker/version.rb +1 -1
- data/lib/llm_cost_tracker.rb +6 -1
- data/lib/tasks/llm_cost_tracker.rake +23 -10
- metadata +22 -9
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
require "llm_cost_tracker/ledger/schema/adapter"
|
|
2
|
+
|
|
3
|
+
class UpgradeLlmCostTrackerIndexes < ActiveRecord::Migration<%= migration_version %>
|
|
4
|
+
disable_ddl_transaction!
|
|
5
|
+
|
|
6
|
+
CALLS = :llm_cost_tracker_calls
|
|
7
|
+
INBOX = :llm_cost_tracker_ingestion_inbox_entries
|
|
8
|
+
UNUSED_INBOX_INDEX = %i[locked_at id].freeze
|
|
9
|
+
UNPRICED_INDEX = :index_llm_cost_tracker_calls_on_unpriced
|
|
10
|
+
|
|
11
|
+
def up
|
|
12
|
+
add_index CALLS, :id, name: UNPRICED_INDEX, where: "total_cost IS NULL", **add_index_options
|
|
13
|
+
return unless table_exists?(INBOX)
|
|
14
|
+
|
|
15
|
+
remove_index INBOX, column: UNUSED_INBOX_INDEX, **remove_index_options
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def down
|
|
19
|
+
add_index INBOX, UNUSED_INBOX_INDEX, **add_index_options if table_exists?(INBOX)
|
|
20
|
+
remove_index CALLS, name: UNPRICED_INDEX, **remove_index_options
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def add_index_options
|
|
26
|
+
return { if_not_exists: true } unless postgresql?
|
|
27
|
+
|
|
28
|
+
{ if_not_exists: true, algorithm: :concurrently }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def remove_index_options
|
|
32
|
+
return { if_exists: true } unless postgresql?
|
|
33
|
+
|
|
34
|
+
{ if_exists: true, algorithm: :concurrently }
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def postgresql?
|
|
38
|
+
LlmCostTracker::Ledger::Schema::Adapter.postgresql?(connection)
|
|
39
|
+
end
|
|
40
|
+
end
|
data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_per_tag_budgets.rb.erb
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
require "llm_cost_tracker/ledger/schema/adapter"
|
|
2
|
+
|
|
3
|
+
class UpgradeLlmCostTrackerPerTagBudgets < ActiveRecord::Migration<%= migration_version %>
|
|
4
|
+
disable_ddl_transaction!
|
|
5
|
+
|
|
6
|
+
TABLE = :llm_cost_tracker_call_tags
|
|
7
|
+
INDEX = %i[key value tracked_at].freeze
|
|
8
|
+
REPLACED_INDEX = %i[key value].freeze
|
|
9
|
+
|
|
10
|
+
def up
|
|
11
|
+
add_column TABLE, :total_cost, :decimal, precision: 20, scale: 8 unless column_exists?(TABLE, :total_cost)
|
|
12
|
+
add_column TABLE, :tracked_at, :datetime unless column_exists?(TABLE, :tracked_at)
|
|
13
|
+
add_index TABLE, INDEX, **add_index_options
|
|
14
|
+
remove_index TABLE, column: REPLACED_INDEX, **remove_index_options
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def down
|
|
18
|
+
add_index TABLE, REPLACED_INDEX, **add_index_options
|
|
19
|
+
remove_index TABLE, column: INDEX, **remove_index_options
|
|
20
|
+
remove_column TABLE, :tracked_at, if_exists: true
|
|
21
|
+
remove_column TABLE, :total_cost, if_exists: true
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def add_index_options
|
|
27
|
+
return { if_not_exists: true, length: { value: 191 } } unless postgresql?
|
|
28
|
+
|
|
29
|
+
{ if_not_exists: true, algorithm: :concurrently }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def remove_index_options
|
|
33
|
+
return { if_exists: true } unless postgresql?
|
|
34
|
+
|
|
35
|
+
{ if_exists: true, algorithm: :concurrently }
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def postgresql?
|
|
39
|
+
LlmCostTracker::Ledger::Schema::Adapter.postgresql?(connection)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators"
|
|
4
|
+
require "rails/generators/active_record"
|
|
5
|
+
|
|
6
|
+
module LlmCostTracker
|
|
7
|
+
module Generators
|
|
8
|
+
class UpgradeIndexesGenerator < Rails::Generators::Base
|
|
9
|
+
include ActiveRecord::Generators::Migration
|
|
10
|
+
|
|
11
|
+
source_root File.expand_path("templates", __dir__)
|
|
12
|
+
|
|
13
|
+
desc "Adds a partial index over unpriced calls and drops the unused ingestion inbox lock index" \
|
|
14
|
+
"scanning the whole ledger."
|
|
15
|
+
|
|
16
|
+
def create_migration_file
|
|
17
|
+
migration_template(
|
|
18
|
+
"upgrade_indexes.rb.erb",
|
|
19
|
+
"db/migrate/upgrade_llm_cost_tracker_indexes.rb"
|
|
20
|
+
)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def migration_version
|
|
26
|
+
"[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]"
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators"
|
|
4
|
+
require "rails/generators/active_record"
|
|
5
|
+
|
|
6
|
+
module LlmCostTracker
|
|
7
|
+
module Generators
|
|
8
|
+
class UpgradePerTagBudgetsGenerator < Rails::Generators::Base
|
|
9
|
+
include ActiveRecord::Generators::Migration
|
|
10
|
+
|
|
11
|
+
source_root File.expand_path("templates", __dir__)
|
|
12
|
+
|
|
13
|
+
desc "Carries each call's cost and time onto its tag rows so per-tag budgets read " \
|
|
14
|
+
"llm_cost_tracker_call_tags without a join. Required when config.budgets.per_tag is set."
|
|
15
|
+
|
|
16
|
+
def create_migration_file
|
|
17
|
+
migration_template(
|
|
18
|
+
"upgrade_per_tag_budgets.rb.erb",
|
|
19
|
+
"db/migrate/upgrade_llm_cost_tracker_per_tag_budgets.rb"
|
|
20
|
+
)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def migration_version
|
|
26
|
+
"[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]"
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "inbox"
|
|
4
|
+
require_relative "../budget"
|
|
4
5
|
require_relative "../ledger/store"
|
|
5
6
|
|
|
6
7
|
module LlmCostTracker
|
|
@@ -121,7 +122,8 @@ module LlmCostTracker
|
|
|
121
122
|
Ledger::Store.persist_records(events)
|
|
122
123
|
Ingestion::InboxEntry.where(id: rows.map(&:id), locked_by: identity).delete_all
|
|
123
124
|
end
|
|
124
|
-
Ledger::Rollups.increment_safely!(events)
|
|
125
|
+
Ledger::Rollups.increment_safely!(events)
|
|
126
|
+
Budget.notify_persisted_safely!(events)
|
|
125
127
|
rescue ActiveRecord::RecordNotUnique
|
|
126
128
|
raise unless retry_on_conflict
|
|
127
129
|
|
|
@@ -12,7 +12,14 @@ module LlmCostTracker
|
|
|
12
12
|
delegate :with_connection, to: :pool
|
|
13
13
|
|
|
14
14
|
def pool
|
|
15
|
-
@pool
|
|
15
|
+
return @pool if @pool && @pid == Process.pid
|
|
16
|
+
|
|
17
|
+
MUTEX.synchronize do
|
|
18
|
+
next @pool if @pool && @pid == Process.pid
|
|
19
|
+
|
|
20
|
+
@pid = Process.pid
|
|
21
|
+
@pool = connect!
|
|
22
|
+
end
|
|
16
23
|
end
|
|
17
24
|
|
|
18
25
|
private
|
|
@@ -27,7 +34,7 @@ module LlmCostTracker
|
|
|
27
34
|
end
|
|
28
35
|
|
|
29
36
|
def pool_size
|
|
30
|
-
configured = LlmCostTracker.configuration.
|
|
37
|
+
configured = LlmCostTracker.configuration.ingestion.pool_size.to_i
|
|
31
38
|
configured.positive? ? configured : DEFAULT_POOL_SIZE
|
|
32
39
|
end
|
|
33
40
|
end
|
|
@@ -37,16 +37,12 @@ module LlmCostTracker
|
|
|
37
37
|
end
|
|
38
38
|
|
|
39
39
|
def async?
|
|
40
|
-
LlmCostTracker.configuration.ingestion == :async
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
def cache_rollups?
|
|
44
|
-
LlmCostTracker.configuration.cache_rollups
|
|
40
|
+
LlmCostTracker.configuration.ingestion.mode == :async
|
|
45
41
|
end
|
|
46
42
|
|
|
47
43
|
def guards_for_current_config
|
|
48
44
|
guards = Ledger::Schema::CORE_SCHEMAS.dup
|
|
49
|
-
guards << Ledger::Schema::CACHE_ROLLUPS_SCHEMA if
|
|
45
|
+
guards << Ledger::Schema::CACHE_ROLLUPS_SCHEMA if LlmCostTracker.configuration.budgets.totals_source == :cache
|
|
50
46
|
guards += Ledger::Schema::ASYNC_SCHEMAS if async?
|
|
51
47
|
guards
|
|
52
48
|
end
|
|
@@ -132,7 +128,7 @@ module LlmCostTracker
|
|
|
132
128
|
return if records.empty?
|
|
133
129
|
|
|
134
130
|
relation.delete_all
|
|
135
|
-
LlmCostTracker::Ledger::Rollups.decrement!(records)
|
|
131
|
+
LlmCostTracker::Ledger::Rollups.decrement!(records)
|
|
136
132
|
end
|
|
137
133
|
|
|
138
134
|
def cleanup_verification_inbox(event:, response_id:)
|
|
@@ -13,32 +13,30 @@ module LlmCostTracker
|
|
|
13
13
|
class << self
|
|
14
14
|
def maybe_capture(batch, resource:)
|
|
15
15
|
return unless Openai.active?
|
|
16
|
-
return unless batch.
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
return unless output_file_id
|
|
20
|
-
|
|
21
|
-
batch_id = batch.respond_to?(:id) ? batch.id : nil
|
|
22
|
-
return unless batch_id && claim(batch_id)
|
|
16
|
+
return unless batch.status.to_s == "completed"
|
|
17
|
+
return unless batch.output_file_id && batch.id
|
|
18
|
+
return if captured?(batch.id)
|
|
23
19
|
|
|
24
20
|
client = resource.instance_variable_get(:@client)
|
|
25
21
|
host = Openai.client_host_for(resource)
|
|
26
22
|
Openai.record_safely do
|
|
27
|
-
io = client.files.content(output_file_id)
|
|
23
|
+
io = client.files.content(batch.output_file_id)
|
|
28
24
|
capture_jsonl(io.respond_to?(:read) ? io.read : io.to_s, host: host)
|
|
25
|
+
mark_captured(batch.id)
|
|
29
26
|
end
|
|
30
27
|
end
|
|
31
28
|
|
|
32
29
|
private
|
|
33
30
|
|
|
34
|
-
def
|
|
31
|
+
def captured?(batch_id)
|
|
32
|
+
MUTEX.synchronize { @dedup&.include?(batch_id) || false }
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def mark_captured(batch_id)
|
|
35
36
|
MUTEX.synchronize do
|
|
36
37
|
@dedup ||= Set.new
|
|
37
|
-
next false if @dedup.include?(batch_id)
|
|
38
|
-
|
|
39
38
|
@dedup.clear if @dedup.size >= DEDUP_LIMIT
|
|
40
39
|
@dedup.add(batch_id)
|
|
41
|
-
true
|
|
42
40
|
end
|
|
43
41
|
end
|
|
44
42
|
|
|
@@ -45,7 +45,7 @@ module LlmCostTracker
|
|
|
45
45
|
|
|
46
46
|
def recorded_sql(period, start)
|
|
47
47
|
calls = "COALESCE(#{sum_sql(LlmCostTracker::Call.between(start, time))}, 0)"
|
|
48
|
-
return calls unless
|
|
48
|
+
return calls unless Rollups.cache_active?
|
|
49
49
|
|
|
50
50
|
rollup = "COALESCE(#{sum_sql(rollup_scope(period))}, 0)"
|
|
51
51
|
"GREATEST(#{rollup}, #{calls})"
|
|
@@ -8,7 +8,17 @@ module LlmCostTracker
|
|
|
8
8
|
module Ledger
|
|
9
9
|
module Rollups
|
|
10
10
|
class << self
|
|
11
|
+
def cache_active?
|
|
12
|
+
return false unless LlmCostTracker.configuration.budgets.totals_source == :cache
|
|
13
|
+
return true if LlmCostTracker::CallRollup.table_exists?
|
|
14
|
+
|
|
15
|
+
warn_missing_table
|
|
16
|
+
false
|
|
17
|
+
end
|
|
18
|
+
|
|
11
19
|
def increment!(events)
|
|
20
|
+
return unless cache_active?
|
|
21
|
+
|
|
12
22
|
events = Array(events).select(&:total_cost)
|
|
13
23
|
return if events.empty?
|
|
14
24
|
|
|
@@ -18,7 +28,9 @@ module LlmCostTracker
|
|
|
18
28
|
ROLLUP_INCREMENT_ATTEMPTS = 3
|
|
19
29
|
ROLLUP_INCREMENT_BASE_DELAY_SECONDS = 0.05
|
|
20
30
|
REBUILD_INSERT_SLICE = 1_000
|
|
21
|
-
private_constant :ROLLUP_INCREMENT_ATTEMPTS,
|
|
31
|
+
private_constant :ROLLUP_INCREMENT_ATTEMPTS,
|
|
32
|
+
:ROLLUP_INCREMENT_BASE_DELAY_SECONDS,
|
|
33
|
+
:REBUILD_INSERT_SLICE
|
|
22
34
|
|
|
23
35
|
def increment_safely!(events)
|
|
24
36
|
attempt = 0
|
|
@@ -28,19 +40,26 @@ module LlmCostTracker
|
|
|
28
40
|
rescue StandardError => e
|
|
29
41
|
raise if LlmCostTracker::Call.connection.open_transactions.positive?
|
|
30
42
|
|
|
31
|
-
if attempt < ROLLUP_INCREMENT_ATTEMPTS
|
|
43
|
+
if attempt < ROLLUP_INCREMENT_ATTEMPTS && rolled_back?(e)
|
|
32
44
|
sleep(ROLLUP_INCREMENT_BASE_DELAY_SECONDS * (2**(attempt - 1)))
|
|
33
45
|
retry
|
|
34
46
|
end
|
|
35
47
|
|
|
36
48
|
LlmCostTracker::Logging.warn(
|
|
37
|
-
"Rollup increment failed for #{events.size} events after #{attempt}
|
|
38
|
-
"#{e.class}: #{e.message}"
|
|
49
|
+
"Rollup increment failed for #{events.size} events after #{attempt} attempt(s): " \
|
|
50
|
+
"#{e.class}: #{e.message}. Budget reads fall back to the calls ledger; " \
|
|
51
|
+
"run bin/rails llm_cost_tracker:rebuild_rollups to resync the cache."
|
|
39
52
|
)
|
|
40
53
|
end
|
|
41
54
|
end
|
|
42
55
|
|
|
56
|
+
def rolled_back?(error)
|
|
57
|
+
error.is_a?(ActiveRecord::Deadlocked) || error.is_a?(ActiveRecord::LockWaitTimeout)
|
|
58
|
+
end
|
|
59
|
+
|
|
43
60
|
def decrement!(records)
|
|
61
|
+
return unless cache_active?
|
|
62
|
+
|
|
44
63
|
buckets = bucket_totals(records)
|
|
45
64
|
return if buckets.empty?
|
|
46
65
|
|
|
@@ -60,6 +79,17 @@ module LlmCostTracker
|
|
|
60
79
|
|
|
61
80
|
private
|
|
62
81
|
|
|
82
|
+
def warn_missing_table
|
|
83
|
+
return if @missing_table_warned
|
|
84
|
+
|
|
85
|
+
@missing_table_warned = true
|
|
86
|
+
Logging.warn(
|
|
87
|
+
"config.budgets.totals_source = :cache is set but llm_cost_tracker_call_rollups is missing; " \
|
|
88
|
+
"budget and dashboard totals fall back to aggregating llm_cost_tracker_calls. " \
|
|
89
|
+
"Run the call_rollups generator and migrate, or set config.budgets.totals_source = :ledger."
|
|
90
|
+
)
|
|
91
|
+
end
|
|
92
|
+
|
|
63
93
|
def accumulate_priced_calls
|
|
64
94
|
accumulated = Hash.new { |totals, key| totals[key] = BigDecimal("0") }
|
|
65
95
|
priced_calls.in_batches do |batch|
|
|
@@ -4,6 +4,7 @@ require "json"
|
|
|
4
4
|
|
|
5
5
|
require_relative "../pricing"
|
|
6
6
|
require_relative "rollups"
|
|
7
|
+
require_relative "../budget/per_tag"
|
|
7
8
|
require_relative "tags/encoding"
|
|
8
9
|
|
|
9
10
|
module LlmCostTracker
|
|
@@ -15,7 +16,7 @@ module LlmCostTracker
|
|
|
15
16
|
return if events.empty?
|
|
16
17
|
|
|
17
18
|
persist_records(events)
|
|
18
|
-
Ledger::Rollups.increment_safely!(events)
|
|
19
|
+
Ledger::Rollups.increment_safely!(events)
|
|
19
20
|
end
|
|
20
21
|
|
|
21
22
|
def persist_records(events)
|
|
@@ -121,7 +122,7 @@ module LlmCostTracker
|
|
|
121
122
|
llm_cost_tracker_call_id: call_ids.fetch(event.event_id),
|
|
122
123
|
key: key.to_s,
|
|
123
124
|
value: Tags::Encoding.encode(value)
|
|
124
|
-
}
|
|
125
|
+
}.merge(budget_columns_for(event))
|
|
125
126
|
end
|
|
126
127
|
end
|
|
127
128
|
return if rows.empty?
|
|
@@ -129,6 +130,12 @@ module LlmCostTracker
|
|
|
129
130
|
LlmCostTracker::CallTag.insert_all!(rows, record_timestamps: false, returning: false)
|
|
130
131
|
end
|
|
131
132
|
|
|
133
|
+
def budget_columns_for(event)
|
|
134
|
+
return {} unless LlmCostTracker::Budget::PerTag.columns?
|
|
135
|
+
|
|
136
|
+
{ total_cost: event.cost&.total, tracked_at: event.tracked_at }
|
|
137
|
+
end
|
|
138
|
+
|
|
132
139
|
def stored_details(details)
|
|
133
140
|
(details || {}).transform_keys(&:to_s).transform_values { |value| Tags::Encoding.normalize_value(value) }
|
|
134
141
|
end
|
|
@@ -6,12 +6,22 @@ module LlmCostTracker
|
|
|
6
6
|
module Ledger
|
|
7
7
|
module Tags
|
|
8
8
|
module Encoding
|
|
9
|
+
INDEXABLE_BYTES = 2_048
|
|
10
|
+
|
|
9
11
|
def self.encode(value)
|
|
10
|
-
case value
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
encoded = case value
|
|
13
|
+
when Hash then JSON.generate(normalize_hash(value))
|
|
14
|
+
when Array then JSON.generate(normalize_array(value))
|
|
15
|
+
else value.to_s
|
|
16
|
+
end
|
|
17
|
+
truncate(encoded)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def self.truncate(string)
|
|
21
|
+
limit = [LlmCostTracker.configuration.tags.max_value_bytesize.to_i, INDEXABLE_BYTES].min
|
|
22
|
+
return string if string.bytesize <= limit
|
|
23
|
+
|
|
24
|
+
string.byteslice(0, limit).encode("UTF-8", invalid: :replace, undef: :replace)
|
|
15
25
|
end
|
|
16
26
|
|
|
17
27
|
def self.normalize_hash(hash)
|
|
@@ -3,15 +3,13 @@
|
|
|
3
3
|
module LlmCostTracker
|
|
4
4
|
module Logging
|
|
5
5
|
class << self
|
|
6
|
-
def debug(message) =
|
|
6
|
+
def debug(message) = Rails.logger.debug(prefixed(message))
|
|
7
7
|
|
|
8
|
-
def warn(message) =
|
|
8
|
+
def warn(message) = Rails.logger.warn(prefixed(message))
|
|
9
9
|
|
|
10
10
|
private
|
|
11
11
|
|
|
12
|
-
def
|
|
13
|
-
Rails.logger.tagged(LlmCostTracker.name)
|
|
14
|
-
end
|
|
12
|
+
def prefixed(message) = "[#{LlmCostTracker.name}] #{message}"
|
|
15
13
|
end
|
|
16
14
|
end
|
|
17
15
|
end
|
|
@@ -94,7 +94,7 @@ module LlmCostTracker
|
|
|
94
94
|
end
|
|
95
95
|
|
|
96
96
|
def inject_stream_usage_flag(request_env, parser, request_url, request_parsed)
|
|
97
|
-
return nil unless LlmCostTracker.configuration.
|
|
97
|
+
return nil unless LlmCostTracker.configuration.capture.request_stream_usage
|
|
98
98
|
return nil unless parser&.auto_enable_stream_usage?(request_url)
|
|
99
99
|
|
|
100
100
|
stream_options = request_parsed["stream_options"]
|