lex-metering 0.1.5 → 0.1.7

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: 175af45dd831905822683d8890bf498d10acb478310d0d7dd97a3359a74f6d48
4
+ data.tar.gz: dc29268590deb375eb3d45b46d1ab10361c48563bbda942c37d5def2b9f468ce
5
5
  SHA512:
6
- metadata.gz: 561748036537315786f810e594f005f58de6f4b2793ec497967dd00ae7488ac7c75a7fd4f7ddfeeea0f5cdd2da9442b3ffe434f4a419d3f083c63c3cea958a47
7
- data.tar.gz: 0dfdb4731304dadc64f56c02e3c71edd939137026b51f057cf5c181412edc4b4b23c02d7bae8ae65451ce8e41c222bd5bbc5a9944991bec3c3dc282ee08c4b21
6
+ metadata.gz: 6268304a7fe42da3b8fba5c5948c4f6da6c26109fbbd7691f52fed79e63505edd6896d1bb648d93c939d69f5e4319ae88c757618b848e71e47b9f71082eab3c7
7
+ data.tar.gz: 987863af819432d2dbd342c43c82359825274611020d330ab0a05616815d2d1db8622c88c179f3d03a06b8fd0fb8853e241d72f822b96a2ab03bcc7c5364d0f1
data/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.1.7] - 2026-03-22
4
+
5
+ ### Changed
6
+ - Updated `legionio` dependency constraint to `>= 1.4.123`
7
+
8
+ ## [0.1.6] - 2026-03-21
9
+
10
+ ### Added
11
+ - `Runners::CostOptimizer` module with `analyze_costs` method for weekly LLM cost analysis
12
+ - `Actor::CostOptimizer` periodic actor (runs weekly) to trigger cost analysis
13
+ - Cost rate tables for Anthropic, OpenAI, Bedrock, and Azure AI models
14
+ - LLM-powered recommendation generation for model rightsizing
15
+
3
16
  ## [0.1.5] - 2026-03-20
4
17
 
5
18
  ### Added
data/lex-metering.gemspec CHANGED
@@ -28,7 +28,7 @@ Gem::Specification.new do |spec|
28
28
  end
29
29
  spec.require_paths = ['lib']
30
30
 
31
- spec.add_dependency 'legionio'
31
+ spec.add_dependency 'legionio', '>= 1.4.123'
32
32
 
33
33
  spec.add_development_dependency 'rake'
34
34
  spec.add_development_dependency 'rspec'
@@ -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.7'
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.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity
@@ -15,14 +15,14 @@ dependencies:
15
15
  requirements:
16
16
  - - ">="
17
17
  - !ruby/object:Gem::Version
18
- version: '0'
18
+ version: 1.4.123
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
23
  - - ">="
24
24
  - !ruby/object:Gem::Version
25
- version: '0'
25
+ version: 1.4.123
26
26
  - !ruby/object:Gem::Dependency
27
27
  name: rake
28
28
  requirement: !ruby/object:Gem::Requirement
@@ -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