lex-metering 0.1.5 → 0.1.6
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: f0014a77ce8b15e51cc4f1ae2edc8804fdd22fc85042fdabe7a556c275ed9d3c
|
|
4
|
+
data.tar.gz: 26b9fd1245b65d5ae61ec9832c2ec3c80fe857ebb13ec28526d828f873ebb33f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ca6352cb2ff5ef6b9067f962561679e03042ec6ee9780b2efda08f2d1198cb4bc8013884bc6756e713221193bb4ca949ddcc979b8bd09e2f094800535d170d63
|
|
7
|
+
data.tar.gz: 30afaffe29668eb64d0a17d52c566c6e87a98cd8d92b08ffc54f7e9552ffc46b5a06d2302558382cf712dcba06a3081282aa78414185bf55bea29a2ceeaabc8f
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.1.6] - 2026-03-21
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- `Runners::CostOptimizer` module with `analyze_costs` method for weekly LLM cost analysis
|
|
7
|
+
- `Actor::CostOptimizer` periodic actor (runs weekly) to trigger cost analysis
|
|
8
|
+
- Cost rate tables for Anthropic, OpenAI, Bedrock, and Azure AI models
|
|
9
|
+
- LLM-powered recommendation generation for model rightsizing
|
|
10
|
+
|
|
3
11
|
## [0.1.5] - 2026-03-20
|
|
4
12
|
|
|
5
13
|
### Added
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module Metering
|
|
6
|
+
module Actor
|
|
7
|
+
class CostOptimizer < Legion::Extensions::Actors::Every
|
|
8
|
+
def runner_class
|
|
9
|
+
'Legion::Extensions::Metering::Runners::CostOptimizer'
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def runner_function
|
|
13
|
+
'analyze_costs'
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def time
|
|
17
|
+
604_800 # once per week
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def run_now?
|
|
21
|
+
false
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def use_runner?
|
|
25
|
+
false
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def check_subtask?
|
|
29
|
+
false
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def generate_task?
|
|
33
|
+
false
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module Metering
|
|
6
|
+
module Runners
|
|
7
|
+
module CostOptimizer
|
|
8
|
+
def analyze_costs(window_days: 7, top_n: 10)
|
|
9
|
+
drivers = collect_cost_data(window_days: window_days)
|
|
10
|
+
return { status: 'no_data', window_days: window_days, cost_drivers: [], recommendations: [] } if drivers.empty?
|
|
11
|
+
|
|
12
|
+
top_drivers = drivers.sort_by { |d| -(d[:total_cost] || 0) }.first(top_n)
|
|
13
|
+
recommendations = generate_recommendations(top_drivers)
|
|
14
|
+
|
|
15
|
+
{
|
|
16
|
+
status: 'analyzed',
|
|
17
|
+
window_days: window_days,
|
|
18
|
+
cost_drivers: top_drivers,
|
|
19
|
+
recommendations: recommendations[:recommendations] || []
|
|
20
|
+
}
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def collect_cost_data(window_days:)
|
|
26
|
+
return [] unless defined?(Legion::Data) && Legion::Data.respond_to?(:connection) && Legion::Data.connection
|
|
27
|
+
|
|
28
|
+
cutoff = Time.now.utc - (window_days * 86_400)
|
|
29
|
+
ds = Legion::Data.connection[:metering_records]
|
|
30
|
+
.where(::Sequel.lit('recorded_at >= ?', cutoff))
|
|
31
|
+
|
|
32
|
+
grouped = ds.group(:provider, :model_id)
|
|
33
|
+
.select_append do
|
|
34
|
+
[sum(total_tokens).as(total_tokens),
|
|
35
|
+
sum(input_tokens).as(input_tokens),
|
|
36
|
+
sum(output_tokens).as(output_tokens),
|
|
37
|
+
count(Sequel.lit('*')).as(call_count)]
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
grouped.all.map do |row|
|
|
41
|
+
{
|
|
42
|
+
extension: row[:provider],
|
|
43
|
+
model: row[:model_id],
|
|
44
|
+
total_tokens: row[:total_tokens] || 0,
|
|
45
|
+
total_cost: estimate_cost(row[:provider], row[:model_id], row[:total_tokens] || 0),
|
|
46
|
+
call_count: row[:call_count] || 0
|
|
47
|
+
}
|
|
48
|
+
end
|
|
49
|
+
rescue StandardError
|
|
50
|
+
[]
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def estimate_cost(provider, model, total_tokens)
|
|
54
|
+
rate = cost_rate(provider, model)
|
|
55
|
+
(total_tokens * rate / 1_000_000.0).round(4)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def cost_rate(provider, model)
|
|
59
|
+
rates = {
|
|
60
|
+
'anthropic' => { 'claude-opus-4-6' => 15.0, 'claude-sonnet-4-6' => 3.0, 'claude-haiku-4-5' => 0.25 },
|
|
61
|
+
'openai' => { 'gpt-4o' => 5.0, 'gpt-4o-mini' => 0.15, 'gpt-4.1' => 2.0 },
|
|
62
|
+
'bedrock' => { 'default' => 3.0 },
|
|
63
|
+
'azure-ai' => { 'default' => 3.0 }
|
|
64
|
+
}
|
|
65
|
+
provider_rates = rates[provider&.to_s] || {}
|
|
66
|
+
provider_rates[model&.to_s] || provider_rates['default'] || 1.0
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def generate_recommendations(drivers)
|
|
70
|
+
return { recommendations: [] } unless defined?(Legion::LLM)
|
|
71
|
+
|
|
72
|
+
prompt = build_recommendation_prompt(drivers)
|
|
73
|
+
result = Legion::LLM.chat(message: prompt)
|
|
74
|
+
::JSON.parse(result[:content] || '{}', symbolize_names: true)
|
|
75
|
+
rescue StandardError
|
|
76
|
+
{ recommendations: [] }
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def build_recommendation_prompt(drivers)
|
|
80
|
+
lines = drivers.map do |d|
|
|
81
|
+
"#{d[:extension]}/#{d[:model]}: #{d[:total_tokens]} tokens, $#{d[:total_cost]}, #{d[:call_count]} calls"
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
<<~PROMPT
|
|
85
|
+
Analyze these LLM cost drivers from the past week and recommend model rightsizing.
|
|
86
|
+
Focus on cases where a cheaper model could handle the workload.
|
|
87
|
+
|
|
88
|
+
#{lines.join("\n")}
|
|
89
|
+
|
|
90
|
+
Return JSON: { "recommendations": [{ "extension": "...", "current_model": "...", "suggested_model": "...", "rationale": "...", "estimated_savings_pct": N }] }
|
|
91
|
+
PROMPT
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: lex-metering
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.6
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Esity
|
|
@@ -113,8 +113,10 @@ files:
|
|
|
113
113
|
- lex-metering.gemspec
|
|
114
114
|
- lib/legion/extensions/metering.rb
|
|
115
115
|
- lib/legion/extensions/metering/actors/cleanup.rb
|
|
116
|
+
- lib/legion/extensions/metering/actors/cost_optimizer.rb
|
|
116
117
|
- lib/legion/extensions/metering/data/migrations/001_add_metering_records.rb
|
|
117
118
|
- lib/legion/extensions/metering/helpers/economics.rb
|
|
119
|
+
- lib/legion/extensions/metering/runners/cost_optimizer.rb
|
|
118
120
|
- lib/legion/extensions/metering/runners/metering.rb
|
|
119
121
|
- lib/legion/extensions/metering/version.rb
|
|
120
122
|
homepage: https://github.com/LegionIO/lex-metering
|