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: 13443e923c7850c17785f0e750d1bdb995575da237592758c7578de30c321081
4
- data.tar.gz: e054cb9fd8222b5d0616f82afb46669541fccfa54c9de4dbd7f83360cb4e7967
3
+ metadata.gz: f0014a77ce8b15e51cc4f1ae2edc8804fdd22fc85042fdabe7a556c275ed9d3c
4
+ data.tar.gz: 26b9fd1245b65d5ae61ec9832c2ec3c80fe857ebb13ec28526d828f873ebb33f
5
5
  SHA512:
6
- metadata.gz: 561748036537315786f810e594f005f58de6f4b2793ec497967dd00ae7488ac7c75a7fd4f7ddfeeea0f5cdd2da9442b3ffe434f4a419d3f083c63c3cea958a47
7
- data.tar.gz: 0dfdb4731304dadc64f56c02e3c71edd939137026b51f057cf5c181412edc4b4b23c02d7bae8ae65451ce8e41c222bd5bbc5a9944991bec3c3dc282ee08c4b21
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
@@ -3,7 +3,7 @@
3
3
  module Legion
4
4
  module Extensions
5
5
  module Metering
6
- VERSION = '0.1.5'
6
+ VERSION = '0.1.6'
7
7
  end
8
8
  end
9
9
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'legion/extensions/metering/version'
4
+ require 'legion/extensions/metering/runners/cost_optimizer'
4
5
 
5
6
  module Legion
6
7
  module Extensions
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.5
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