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:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: bce7fb746d14ff63db46cb4fc87613a5a0834c61e5e067dc4e5bf6122dbd251a
|
|
4
|
+
data.tar.gz: 2d7c0d14bef999fece93d633dd74c700d2594e76d1aae216a2afd3b51f13a534
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
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.
|
|
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
|