legion-llm 0.3.20 → 0.3.22
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 +4 -4
- data/CHANGELOG.md +18 -0
- data/lib/legion/llm/batch.rb +19 -2
- data/lib/legion/llm/cost_tracker.rb +1 -1
- data/lib/legion/llm/off_peak.rb +6 -25
- data/lib/legion/llm/scheduling.rb +10 -10
- data/lib/legion/llm/shadow_eval.rb +3 -3
- data/lib/legion/llm/tool_registry.rb +26 -0
- data/lib/legion/llm/version.rb +1 -1
- data/lib/legion/llm.rb +18 -2
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: db13bc01a538ce15c213a0ee49dae011b79c0bdf0148ebb940dad6b54cc769c4
|
|
4
|
+
data.tar.gz: a68d77b17f0eeff3e841620cc43bae9601e9a9069b555ca583b507ab258677db
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ab9351b4781dcf146d552f555d0da7eaa444a94d15125af387af33b5ae3741863fccccaafd1fd981faca6e3781589fd6f5a4a273b9a02bf940d389763c55150c
|
|
7
|
+
data.tar.gz: 4e17454656a9baf87b78a75e99bfe5cc6215a48d1135cc053abab4e85b8b300e3ece10ab5e6628dabbd7a22cbc167d1d3e1d5c63d87da226a8092db6f1ed3b64
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# Legion LLM Changelog
|
|
2
2
|
|
|
3
|
+
## [0.3.22] - 2026-03-23
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
- `Batch.submit_single` now calls `Legion::LLM.chat_direct` instead of returning a stub response
|
|
7
|
+
- Batch flush returns `status: :completed` on success or `status: :failed` with error on exception
|
|
8
|
+
- `OffPeak` module now delegates to `Scheduling` (consolidated duplicate peak-hour logic)
|
|
9
|
+
- `Scheduling.peak_hours?` and `Scheduling.next_off_peak` accept optional `time` parameter
|
|
10
|
+
|
|
11
|
+
## [0.3.21] - 2026-03-23
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
- `Legion::LLM::ToolRegistry` thread-safe tool class registry for auto-attaching tools to chat sessions
|
|
15
|
+
- Wire ToolRegistry into `chat_single` so globally registered tools are available in every session
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
- Fix `CostTracker.settings_pricing` reading from wrong settings key (`:'legion-llm'` instead of `:llm`)
|
|
19
|
+
- Fix `ShadowEval.evaluate` not passing `messages:` to shadow model (shadow got no context to respond to)
|
|
20
|
+
|
|
3
21
|
## [0.3.20] - 2026-03-22
|
|
4
22
|
|
|
5
23
|
### Changed
|
data/lib/legion/llm/batch.rb
CHANGED
|
@@ -101,13 +101,30 @@ module Legion
|
|
|
101
101
|
end
|
|
102
102
|
|
|
103
103
|
def submit_single(entry, provider:, model:)
|
|
104
|
+
response = Legion::LLM.chat_direct(
|
|
105
|
+
messages: entry[:messages],
|
|
106
|
+
model: model,
|
|
107
|
+
**entry[:opts]
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
{
|
|
111
|
+
status: :completed,
|
|
112
|
+
model: model,
|
|
113
|
+
provider: provider,
|
|
114
|
+
id: entry[:id],
|
|
115
|
+
response: response,
|
|
116
|
+
meta: { batched: true, queued_at: entry[:queued_at], completed_at: Time.now.utc }
|
|
117
|
+
}
|
|
118
|
+
rescue StandardError => e
|
|
119
|
+
Legion::Logging.warn("Batch submit_single failed for #{entry[:id]}: #{e.message}") if defined?(Legion::Logging)
|
|
104
120
|
{
|
|
105
|
-
status: :
|
|
121
|
+
status: :failed,
|
|
106
122
|
model: model,
|
|
107
123
|
provider: provider,
|
|
108
124
|
id: entry[:id],
|
|
109
125
|
response: nil,
|
|
110
|
-
|
|
126
|
+
error: e.message,
|
|
127
|
+
meta: { batched: true, queued_at: entry[:queued_at], failed_at: Time.now.utc }
|
|
111
128
|
}
|
|
112
129
|
end
|
|
113
130
|
end
|
|
@@ -84,7 +84,7 @@ module Legion
|
|
|
84
84
|
def settings_pricing
|
|
85
85
|
return {} unless defined?(Legion::Settings)
|
|
86
86
|
|
|
87
|
-
pricing = Legion::Settings.dig(:
|
|
87
|
+
pricing = Legion::Settings.dig(:llm, :pricing)
|
|
88
88
|
pricing.is_a?(Hash) ? pricing : {}
|
|
89
89
|
rescue StandardError => e
|
|
90
90
|
Legion::Logging.warn("CostTracker settings unavailable: #{e.message}") if defined?(Legion::Logging)
|
data/lib/legion/llm/off_peak.rb
CHANGED
|
@@ -1,44 +1,25 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative 'scheduling'
|
|
4
|
+
|
|
3
5
|
module Legion
|
|
4
6
|
module LLM
|
|
7
|
+
# Simplified peak-hour interface delegating to Scheduling.
|
|
8
|
+
# Preserved for backward compatibility.
|
|
5
9
|
module OffPeak
|
|
6
|
-
# Peak hours in UTC: 14:00-22:00 (9 AM - 5 PM CT)
|
|
7
|
-
PEAK_HOURS = (14..22)
|
|
8
|
-
|
|
9
10
|
class << self
|
|
10
|
-
# Returns true if the given time falls within peak hours.
|
|
11
|
-
#
|
|
12
|
-
# @param time [Time] time to check (defaults to now)
|
|
13
|
-
# @return [Boolean]
|
|
14
11
|
def peak_hour?(time = Time.now.utc)
|
|
15
|
-
|
|
16
|
-
Legion::Logging.debug("OffPeak peak_hour check hour=#{time.hour} peak=#{result}") if defined?(Legion::Logging)
|
|
17
|
-
result
|
|
12
|
+
Scheduling.peak_hours?(time)
|
|
18
13
|
end
|
|
19
14
|
|
|
20
|
-
# Returns true when a non-urgent request should be deferred to off-peak.
|
|
21
|
-
#
|
|
22
|
-
# @param priority [Symbol] :urgent bypasses deferral; :normal and :low defer during peak
|
|
23
|
-
# @return [Boolean]
|
|
24
15
|
def should_defer?(priority: :normal)
|
|
25
16
|
return false if priority.to_sym == :urgent
|
|
26
17
|
|
|
27
18
|
peak_hour?
|
|
28
19
|
end
|
|
29
20
|
|
|
30
|
-
# Returns the next off-peak Time (UTC).
|
|
31
|
-
# If already off-peak, returns the current time.
|
|
32
|
-
# Off-peak begins at the hour after the peak window ends (23:00 UTC).
|
|
33
|
-
#
|
|
34
|
-
# @param time [Time] reference time (defaults to now)
|
|
35
|
-
# @return [Time]
|
|
36
21
|
def next_off_peak(time = Time.now.utc)
|
|
37
|
-
|
|
38
|
-
time
|
|
39
|
-
else
|
|
40
|
-
Time.utc(time.year, time.month, time.day, PEAK_HOURS.last, 0, 0)
|
|
41
|
-
end
|
|
22
|
+
Scheduling.next_off_peak(time)
|
|
42
23
|
end
|
|
43
24
|
end
|
|
44
25
|
end
|
|
@@ -29,9 +29,9 @@ module Legion
|
|
|
29
29
|
result
|
|
30
30
|
end
|
|
31
31
|
|
|
32
|
-
# Returns true if the
|
|
33
|
-
def peak_hours?
|
|
34
|
-
hour = Time.now.utc.hour
|
|
32
|
+
# Returns true if the given UTC hour falls within the configured peak window.
|
|
33
|
+
def peak_hours?(time = Time.now.utc)
|
|
34
|
+
hour = time.is_a?(Time) ? time.hour : Time.now.utc.hour
|
|
35
35
|
peak_range.cover?(hour)
|
|
36
36
|
end
|
|
37
37
|
|
|
@@ -39,19 +39,19 @@ module Legion
|
|
|
39
39
|
# Off-peak begins at the hour after the peak window ends.
|
|
40
40
|
#
|
|
41
41
|
# @return [Time] next off-peak start time
|
|
42
|
-
def next_off_peak
|
|
43
|
-
now = Time.now.utc
|
|
42
|
+
def next_off_peak(time = Time.now.utc)
|
|
43
|
+
now = time.is_a?(Time) ? time : Time.now.utc
|
|
44
44
|
peak_end = peak_range.last
|
|
45
45
|
max_defer = settings.fetch(:max_defer_hours, 8)
|
|
46
46
|
|
|
47
|
-
next_time = if now
|
|
48
|
-
#
|
|
49
|
-
now
|
|
50
|
-
else
|
|
51
|
-
# During or after peak — next off-peak is at peak_end + 1
|
|
47
|
+
next_time = if peak_hours?(now)
|
|
48
|
+
# During peak — next off-peak is at peak_end + 1
|
|
52
49
|
candidate = Time.utc(now.year, now.month, now.day, peak_end + 1, 0, 0)
|
|
53
50
|
candidate += 86_400 if candidate <= now
|
|
54
51
|
candidate
|
|
52
|
+
else
|
|
53
|
+
# Already off-peak — return now
|
|
54
|
+
now
|
|
55
55
|
end
|
|
56
56
|
|
|
57
57
|
# Cap at max_defer_hours from now
|
|
@@ -15,14 +15,14 @@ module Legion
|
|
|
15
15
|
rand < rate
|
|
16
16
|
end
|
|
17
17
|
|
|
18
|
-
def evaluate(primary_response:, messages: nil, shadow_model: nil)
|
|
18
|
+
def evaluate(primary_response:, messages: nil, shadow_model: nil)
|
|
19
19
|
shadow_model ||= Legion::Settings.dig(:llm, :shadow, :model) || 'gpt-4o-mini'
|
|
20
20
|
Legion::Logging.debug("ShadowEval triggered primary_model=#{primary_response[:model]} shadow_model=#{shadow_model}") if defined?(Legion::Logging)
|
|
21
21
|
|
|
22
22
|
shadow_response = Legion::LLM.send(:chat_single,
|
|
23
23
|
model: shadow_model, provider: nil,
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
messages: messages, intent: nil,
|
|
25
|
+
tier: nil)
|
|
26
26
|
|
|
27
27
|
comparison = compare(primary_response, shadow_response, shadow_model)
|
|
28
28
|
Legion::Events.emit('llm.shadow_eval', comparison) if defined?(Legion::Events)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module LLM
|
|
5
|
+
module ToolRegistry
|
|
6
|
+
@tools = []
|
|
7
|
+
@mutex = Mutex.new
|
|
8
|
+
|
|
9
|
+
class << self
|
|
10
|
+
def register(tool_class)
|
|
11
|
+
@mutex.synchronize do
|
|
12
|
+
@tools << tool_class unless @tools.include?(tool_class)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def tools
|
|
17
|
+
@mutex.synchronize { @tools.dup }
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def clear
|
|
21
|
+
@mutex.synchronize { @tools.clear }
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
data/lib/legion/llm/version.rb
CHANGED
data/lib/legion/llm.rb
CHANGED
|
@@ -17,6 +17,7 @@ require_relative 'llm/batch'
|
|
|
17
17
|
require_relative 'llm/scheduling'
|
|
18
18
|
require_relative 'llm/off_peak'
|
|
19
19
|
require_relative 'llm/cost_tracker'
|
|
20
|
+
require_relative 'llm/tool_registry'
|
|
20
21
|
|
|
21
22
|
begin
|
|
22
23
|
require 'legion/extensions/llm/gateway'
|
|
@@ -119,6 +120,9 @@ module Legion
|
|
|
119
120
|
end
|
|
120
121
|
end
|
|
121
122
|
|
|
123
|
+
if defined?(Legion::Logging)
|
|
124
|
+
Legion::Logging.debug "[LLM] chat_direct escalate=#{escalate} message_present=#{!message.nil?} model=#{model} provider=#{provider}"
|
|
125
|
+
end
|
|
122
126
|
result = if escalate && message
|
|
123
127
|
chat_with_escalation(
|
|
124
128
|
model: model, provider: provider, intent: intent, tier: tier,
|
|
@@ -129,6 +133,7 @@ module Legion
|
|
|
129
133
|
chat_single(model: model, provider: provider, intent: intent, tier: tier,
|
|
130
134
|
temperature: temperature, message: message, **kwargs)
|
|
131
135
|
end
|
|
136
|
+
Legion::Logging.debug "[LLM] chat_direct result_class=#{result.class} result_nil=#{result.nil?}" if defined?(Legion::Logging)
|
|
132
137
|
|
|
133
138
|
if cache_key && result.is_a?(Hash)
|
|
134
139
|
ttl = settings.dig(:prompt_caching, :response_cache, :ttl_seconds) || Cache::DEFAULT_TTL
|
|
@@ -283,7 +288,11 @@ module Legion
|
|
|
283
288
|
Legion::Extensions::LLM::Gateway::Runners::Inference.chat(**)
|
|
284
289
|
end
|
|
285
290
|
|
|
286
|
-
def chat_single(model:, provider:, intent:, tier:, message: nil, **kwargs)
|
|
291
|
+
def chat_single(model:, provider:, intent:, tier:, message: nil, **kwargs) # rubocop:disable Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
|
|
292
|
+
explicit_tools = kwargs.delete(:tools)
|
|
293
|
+
tools = explicit_tools || ToolRegistry.tools
|
|
294
|
+
tools = nil if tools.empty?
|
|
295
|
+
|
|
287
296
|
if (intent || tier) && Router.routing_enabled?
|
|
288
297
|
resolution = Router.resolve(intent: intent, tier: tier, model: model, provider: provider)
|
|
289
298
|
if resolution
|
|
@@ -307,10 +316,17 @@ module Legion
|
|
|
307
316
|
|
|
308
317
|
inject_anthropic_cache_control!(opts, provider)
|
|
309
318
|
|
|
319
|
+
if defined?(Legion::Logging)
|
|
320
|
+
Legion::Logging.debug "[LLM] chat_single model=#{opts[:model]} provider=#{opts[:provider]} message_present=#{!message.nil?} tools=#{tools&.size || 0}"
|
|
321
|
+
end
|
|
310
322
|
session = RubyLLM.chat(**opts)
|
|
323
|
+
tools&.each { |tool| session.with_tool(tool) }
|
|
311
324
|
return session unless message
|
|
312
325
|
|
|
313
|
-
session.ask(
|
|
326
|
+
Legion::Logging.debug '[LLM] chat_single calling session.ask' if defined?(Legion::Logging)
|
|
327
|
+
response = session.ask(message)
|
|
328
|
+
Legion::Logging.debug "[LLM] chat_single response_class=#{response.class} response_nil=#{response.nil?}" if defined?(Legion::Logging)
|
|
329
|
+
response
|
|
314
330
|
end
|
|
315
331
|
|
|
316
332
|
def chat_with_escalation(model:, provider:, intent:, tier:, max_escalations:, quality_check:, message:, **kwargs)
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: legion-llm
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.3.
|
|
4
|
+
version: 0.3.22
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Esity
|
|
@@ -161,6 +161,7 @@ files:
|
|
|
161
161
|
- lib/legion/llm/settings.rb
|
|
162
162
|
- lib/legion/llm/shadow_eval.rb
|
|
163
163
|
- lib/legion/llm/structured_output.rb
|
|
164
|
+
- lib/legion/llm/tool_registry.rb
|
|
164
165
|
- lib/legion/llm/transport/exchanges/escalation.rb
|
|
165
166
|
- lib/legion/llm/transport/messages/escalation_event.rb
|
|
166
167
|
- lib/legion/llm/version.rb
|