lex-llm-gateway 0.2.8 → 0.2.9

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 893662bdc056fd78efe87c3fa3874746bc1284d436b3233a49bed368370506b2
4
- data.tar.gz: c62e301286b6690a66c12445990424bb811d9faef6783986a4e93dca4c015672
3
+ metadata.gz: bce7fb746d14ff63db46cb4fc87613a5a0834c61e5e067dc4e5bf6122dbd251a
4
+ data.tar.gz: 2d7c0d14bef999fece93d633dd74c700d2594e76d1aae216a2afd3b51f13a534
5
5
  SHA512:
6
- metadata.gz: 9ad0d8c1dcf0df6b447c1a12653bf7799ef95f6c8b1fba8167cd54652f7ef7aadc6414fe03397ecb3307564b48b06fd6b1ce7604d9d1b6d72886dfe4608b422a
7
- data.tar.gz: c9d3c90429a44efa0cdb15309161d67182f8136815a119e4a4f4d76b014d0e5982bc0e161bab3db8423d69c68950a5f6cf5ef30fec176e567db2bc5d4a87fab3
6
+ metadata.gz: 488014e8667b9782d18a56dee95250ce8869aff2f9d9e0a5254d77f3d6a5ffd9a1cea802c66766fa50da1e2fa0b5801fb6b1b17b460dfb39305ddd3eaa2aeb81
7
+ data.tar.gz: a65be2e6996f18b98cbdafb3f1e6145671a6eb4f37f63e7b6e500e272d8d16ba26e5414f32489cd5569c16c6881c17dab601833f1eae5edf2516c177077e54f3
data/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.2.9] - 2026-03-23
4
+
5
+ ### Added
6
+ - UsageReporter runner: summary, worker_usage, budget_check, top_consumers from metering_records
7
+ - UsageQueries helper: extracted query building, aggregation, and period calculation
8
+ - Budget enforcement with configurable monthly_usd and alert_threshold via settings
9
+ - 15 specs covering all reporter methods with in-memory SQLite
10
+
3
11
  ## [0.2.8] - 2026-03-23
4
12
 
5
13
  ### Added
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module LLM
6
+ module Gateway
7
+ module Helpers
8
+ module UsageQueries
9
+ module_function
10
+
11
+ def metering_records
12
+ Legion::Data.connection[:metering_records]
13
+ end
14
+
15
+ def metering_since(since)
16
+ metering_records.where { recorded_at >= since }
17
+ end
18
+
19
+ def aggregate_totals(dataset)
20
+ {
21
+ total_requests: dataset.count,
22
+ total_cost_usd: dataset.sum(:cost_usd).to_f.round(4),
23
+ total_input: dataset.sum(:input_tokens).to_i,
24
+ total_output: dataset.sum(:output_tokens).to_i
25
+ }
26
+ end
27
+
28
+ def grouped_rows(dataset, column)
29
+ grouped_query(dataset, column).limit(20).all.map do |row|
30
+ format_grouped_row(row, column, name_key: true)
31
+ end
32
+ end
33
+
34
+ def aggregate_by_column(dataset, column, limit)
35
+ grouped_query(dataset, column).limit(limit).all.map do |row|
36
+ format_grouped_row(row, column)
37
+ end
38
+ end
39
+
40
+ def grouped_query(dataset, column)
41
+ base = dataset.exclude(column => nil).group_and_count(column)
42
+ append_aggregates(base).order(::Sequel.desc(:total_cost))
43
+ end
44
+
45
+ def append_aggregates(query)
46
+ query.select_append { sum(cost_usd).as(total_cost) }
47
+ .select_append { sum(input_tokens).as(total_input) }
48
+ .select_append { sum(output_tokens).as(total_output) }
49
+ end
50
+
51
+ def format_grouped_row(row, column, name_key: false)
52
+ key = name_key ? :name : column
53
+ {
54
+ key => row[column],
55
+ total_cost: row[:total_cost].to_f.round(4),
56
+ total_input: row[:total_input].to_i,
57
+ total_output: row[:total_output].to_i,
58
+ requests: row[:count].to_i
59
+ }
60
+ end
61
+
62
+ def period_start(period)
63
+ now = Time.now.utc
64
+ case period.to_sym
65
+ when :hour then now - 3600
66
+ when :week then day_start(now) - ((now.wday % 7) * 86_400)
67
+ when :month then Time.utc(now.year, now.month, 1)
68
+ else day_start(now)
69
+ end
70
+ end
71
+
72
+ def day_start(time)
73
+ Time.utc(time.year, time.month, time.day)
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../helpers/usage_queries'
4
+
5
+ module Legion
6
+ module Extensions
7
+ module LLM
8
+ module Gateway
9
+ module Runners
10
+ module UsageReporter
11
+ DEFAULT_BUDGET_USD = 100.0
12
+ DEFAULT_ALERT_THRESHOLD = 0.8
13
+
14
+ module_function
15
+
16
+ def summary(since: nil, period: :day)
17
+ return unavailable_result unless data_connected?
18
+
19
+ since ||= queries.period_start(period)
20
+ dataset = queries.metering_since(since)
21
+ build_summary(dataset, period, since)
22
+ end
23
+
24
+ def worker_usage(worker_id:, since: nil, period: :day)
25
+ return unavailable_result unless data_connected?
26
+
27
+ since ||= queries.period_start(period)
28
+ dataset = queries.metering_since(since).where(worker_id: worker_id)
29
+ build_worker_summary(dataset, worker_id, period, since)
30
+ end
31
+
32
+ def budget_check(budget_usd: nil, threshold: nil, period: :month)
33
+ return unavailable_result unless data_connected?
34
+
35
+ budget = budget_usd || configured_budget
36
+ alert_at = threshold || configured_threshold
37
+ spent = queries.metering_since(queries.period_start(period)).sum(:cost_usd).to_f
38
+ build_budget_result(budget, spent, alert_at, period)
39
+ end
40
+
41
+ def top_consumers(since: nil, period: :day, limit: 10, group_by: :worker_id)
42
+ return unavailable_result unless data_connected?
43
+
44
+ since ||= queries.period_start(period)
45
+ queries.aggregate_by_column(queries.metering_since(since), group_by.to_sym, limit)
46
+ end
47
+
48
+ def queries
49
+ Helpers::UsageQueries
50
+ end
51
+
52
+ def data_connected?
53
+ MeteringWriter.data_connected?
54
+ end
55
+
56
+ def build_summary(dataset, period, since)
57
+ queries.aggregate_totals(dataset).merge(
58
+ period: period.to_s,
59
+ since: since.iso8601,
60
+ by_provider: queries.grouped_rows(dataset, :provider),
61
+ by_model: queries.grouped_rows(dataset, :model_id)
62
+ )
63
+ end
64
+
65
+ def build_worker_summary(dataset, worker_id, period, since)
66
+ queries.aggregate_totals(dataset).merge(
67
+ worker_id: worker_id,
68
+ period: period.to_s,
69
+ since: since.iso8601,
70
+ by_model: queries.grouped_rows(dataset, :model_id)
71
+ )
72
+ end
73
+
74
+ def build_budget_result(budget, spent, alert_at, period)
75
+ ratio = budget.positive? ? (spent / budget) : 0.0
76
+ {
77
+ period: period.to_s,
78
+ budget: budget.round(2),
79
+ spent: spent.round(4),
80
+ remaining: [(budget - spent), 0.0].max.round(4),
81
+ ratio: ratio.round(4),
82
+ status: budget_status(ratio, alert_at)
83
+ }
84
+ end
85
+
86
+ def budget_status(ratio, alert_at)
87
+ return :exceeded if ratio >= 1.0
88
+ return :warning if ratio >= alert_at
89
+
90
+ :ok
91
+ end
92
+
93
+ def configured_budget
94
+ llm_setting(:budget, :monthly_usd)&.to_f || DEFAULT_BUDGET_USD
95
+ end
96
+
97
+ def configured_threshold
98
+ llm_setting(:budget, :alert_threshold)&.to_f || DEFAULT_ALERT_THRESHOLD
99
+ end
100
+
101
+ def llm_setting(*keys)
102
+ return nil unless defined?(Legion::Settings)
103
+
104
+ settings = Legion::Settings[:llm] rescue nil # rubocop:disable Style/RescueModifier
105
+ return nil unless settings.is_a?(Hash)
106
+
107
+ settings.dig(*keys)
108
+ end
109
+
110
+ def unavailable_result
111
+ { success: false, error: 'data_not_connected' }
112
+ end
113
+ end
114
+ end
115
+ end
116
+ end
117
+ end
118
+ end
@@ -4,7 +4,7 @@ module Legion
4
4
  module Extensions
5
5
  module LLM
6
6
  module Gateway
7
- VERSION = '0.2.8'
7
+ VERSION = '0.2.9'
8
8
  end
9
9
  end
10
10
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lex-llm-gateway
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.8
4
+ version: 0.2.9
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity
@@ -185,11 +185,13 @@ files:
185
185
  - lib/legion/extensions/llm/gateway/helpers/cost_estimator.rb
186
186
  - lib/legion/extensions/llm/gateway/helpers/reply_dispatcher.rb
187
187
  - lib/legion/extensions/llm/gateway/helpers/rpc.rb
188
+ - lib/legion/extensions/llm/gateway/helpers/usage_queries.rb
188
189
  - lib/legion/extensions/llm/gateway/runners/fleet.rb
189
190
  - lib/legion/extensions/llm/gateway/runners/fleet_handler.rb
190
191
  - lib/legion/extensions/llm/gateway/runners/inference.rb
191
192
  - lib/legion/extensions/llm/gateway/runners/metering.rb
192
193
  - lib/legion/extensions/llm/gateway/runners/metering_writer.rb
194
+ - lib/legion/extensions/llm/gateway/runners/usage_reporter.rb
193
195
  - lib/legion/extensions/llm/gateway/transport/exchanges/inference.rb
194
196
  - lib/legion/extensions/llm/gateway/transport/exchanges/metering.rb
195
197
  - lib/legion/extensions/llm/gateway/transport/messages/inference_request.rb