legion-llm 0.5.18 → 0.5.20
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/AGENTS.md +37 -0
- data/CHANGELOG.md +25 -0
- data/lib/legion/llm/codex_config_loader.rb +93 -0
- data/lib/legion/llm/helper.rb +132 -0
- data/lib/legion/llm/helpers/llm.rb +3 -50
- data/lib/legion/llm/version.rb +1 -1
- data/lib/legion/llm.rb +3 -0
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f51b096c53558665dbfc074fa203c8624aa79444324cb686759aee35cd568ca6
|
|
4
|
+
data.tar.gz: 95c6a41fa6839d476cff0d60a6b56a1fbc7ad5be4f914f290fc877e0198e50bc
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 33a939afde771b78203d7c84f3fdb9ccbe226907037bc97f86a5650324c089e1034e9de3251ea1deb72d634680b3de6e263a48bbb933648621586598d1244f16
|
|
7
|
+
data.tar.gz: d0f98e6c6dfbee9040953991e8ab3a0de88cd37ed32a352b2d6d0ef52163e120756238d7d5f96c6f00d028b25e778a2076c9012550879fba1b98b06339d5a63e
|
data/AGENTS.md
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# legion-llm Agent Notes
|
|
2
|
+
|
|
3
|
+
## Scope
|
|
4
|
+
|
|
5
|
+
`legion-llm` provides provider configuration, chat/embed/structured interfaces, dynamic routing, escalation, quality checks, and pipeline execution for Legion.
|
|
6
|
+
|
|
7
|
+
## Fast Start
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
bundle install
|
|
11
|
+
bundle exec rspec
|
|
12
|
+
bundle exec rubocop
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Primary Entry Points
|
|
16
|
+
|
|
17
|
+
- `lib/legion/llm.rb`
|
|
18
|
+
- `lib/legion/llm/providers.rb`
|
|
19
|
+
- `lib/legion/llm/router/`
|
|
20
|
+
- `lib/legion/llm/pipeline/`
|
|
21
|
+
- `lib/legion/llm/structured_output.rb`
|
|
22
|
+
- `lib/legion/llm/embeddings.rb`
|
|
23
|
+
- `lib/legion/llm/fleet/`
|
|
24
|
+
|
|
25
|
+
## Guardrails
|
|
26
|
+
|
|
27
|
+
- Keep typed error behavior and retry semantics stable (`ProviderDown`, `RateLimitError`, `EscalationExhausted`, etc.).
|
|
28
|
+
- Routing and escalation must remain deterministic given the same inputs/settings.
|
|
29
|
+
- Preserve pipeline feature-flag behavior; avoid forcing pipeline-only code paths.
|
|
30
|
+
- Keep provider credentials resolved through settings secret resolution flow; never hardcode secrets.
|
|
31
|
+
- Maintain compatibility with direct methods (`chat_direct`, `embed_direct`, `structured_direct`) and daemon-aware flows.
|
|
32
|
+
- Health tracker and rule scoring are contract-sensitive; changes require spec updates.
|
|
33
|
+
|
|
34
|
+
## Validation
|
|
35
|
+
|
|
36
|
+
- Run targeted specs for modified router/pipeline/provider code.
|
|
37
|
+
- Before handoff, run full `bundle exec rspec` and `bundle exec rubocop`.
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# Legion LLM Changelog
|
|
2
2
|
|
|
3
|
+
## [Unreleased]
|
|
4
|
+
|
|
5
|
+
## [0.5.20] - 2026-03-30
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- `CodexConfigLoader`: auto-imports OpenAI bearer token from `~/.codex/auth.json` when `auth_mode` is `chatgpt` and no existing OpenAI API key is configured (#15)
|
|
9
|
+
- JWT expiry validation — expired codex tokens are skipped with a debug log (#15)
|
|
10
|
+
- Non-JWT tokens (plain API keys) accepted without validation (#15)
|
|
11
|
+
- Falls back to vault/settings/env when codex auth file is absent or token is expired (#15)
|
|
12
|
+
- `Legion::LLM::Helper` module at `lib/legion/llm/helper.rb` — canonical helper following cache/transport pattern (#20)
|
|
13
|
+
- Layered defaults: `llm_default_model`, `llm_default_provider`, `llm_default_intent` (LEX-overridable) (#20)
|
|
14
|
+
- `llm_embed_batch` — batch embedding convenience (#20)
|
|
15
|
+
- `llm_structured` — structured JSON output convenience (#20)
|
|
16
|
+
- `llm_ask` — daemon-first single-shot convenience (#20)
|
|
17
|
+
- `llm_connected?` / `llm_can_embed?` / `llm_routing_enabled?` — status helpers (#20)
|
|
18
|
+
- `llm_cost_estimate` / `llm_cost_summary` / `llm_budget_remaining` — cost and budget helpers (#20)
|
|
19
|
+
- Layered model/provider/intent defaults applied to `llm_chat` and `llm_session` (#20)
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
- `lib/legion/llm/helpers/llm.rb` is now a backward-compat shim that includes `Legion::LLM::Helper` (#20)
|
|
23
|
+
|
|
3
24
|
## [0.5.18] - 2026-03-29
|
|
4
25
|
|
|
5
26
|
### Fixed
|
|
@@ -43,6 +64,10 @@
|
|
|
43
64
|
## [0.5.14] - 2026-03-27
|
|
44
65
|
|
|
45
66
|
### Added
|
|
67
|
+
- `CodexConfigLoader`: auto-imports OpenAI bearer token from `~/.codex/auth.json` when `auth_mode` is `chatgpt` and no existing OpenAI API key is configured
|
|
68
|
+
- JWT expiry validation — expired codex tokens are skipped with a debug log
|
|
69
|
+
- Non-JWT tokens (plain API keys) accepted without validation
|
|
70
|
+
- Falls back to vault/settings/env when codex auth file is absent or token is expired
|
|
46
71
|
- `DaemonClient.inference` method for conversation-level routing — accepts a full `messages:` array and optional `tools:`, `model:`, `provider:`, and `timeout:` keyword args, posts to `POST /api/llm/inference`, and returns a structured `{ status: :ok, data: { content:, tool_calls:, stop_reason:, model:, input_tokens:, output_tokens: } }` hash on success
|
|
47
72
|
- `http_post` now accepts an optional `timeout:` keyword argument (default `DEFAULT_TIMEOUT = 60`) so callers like `inference` can pass a longer timeout (120s) without affecting existing `chat` calls
|
|
48
73
|
- `interpret_inference_response` private helper that maps the `/api/llm/inference` HTTP response — 200 returns `:ok` with structured fields, 4xx/5xx follow the same error handling as `interpret_response`
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'base64'
|
|
4
|
+
require 'json'
|
|
5
|
+
|
|
6
|
+
module Legion
|
|
7
|
+
module LLM
|
|
8
|
+
module CodexConfigLoader
|
|
9
|
+
CODEX_AUTH = File.expand_path('~/.codex/auth.json')
|
|
10
|
+
|
|
11
|
+
module_function
|
|
12
|
+
|
|
13
|
+
def load
|
|
14
|
+
return unless File.exist?(CODEX_AUTH)
|
|
15
|
+
|
|
16
|
+
config = read_json(CODEX_AUTH)
|
|
17
|
+
return if config.empty?
|
|
18
|
+
|
|
19
|
+
apply_codex_config(config)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def read_json(path)
|
|
23
|
+
::JSON.parse(File.read(path), symbolize_names: true)
|
|
24
|
+
rescue StandardError => e
|
|
25
|
+
Legion::Logging.debug("CodexConfigLoader could not read #{path}: #{e.message}") if defined?(Legion::Logging)
|
|
26
|
+
{}
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def apply_codex_config(config)
|
|
30
|
+
return unless config[:auth_mode] == 'chatgpt'
|
|
31
|
+
|
|
32
|
+
token = config.dig(:tokens, :access_token)
|
|
33
|
+
return unless token.is_a?(String) && !token.empty?
|
|
34
|
+
|
|
35
|
+
unless token_valid?(token)
|
|
36
|
+
Legion::Logging.debug 'CodexConfigLoader: access token is expired, skipping' if defined?(Legion::Logging)
|
|
37
|
+
return
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
providers = Legion::LLM.settings[:providers]
|
|
41
|
+
existing_raw = providers.dig(:openai, :api_key)
|
|
42
|
+
resolved_existing = resolve_env_api_key(existing_raw)
|
|
43
|
+
return unless resolved_existing.nil? || (resolved_existing.respond_to?(:empty?) && resolved_existing.empty?)
|
|
44
|
+
|
|
45
|
+
providers[:openai][:api_key] = token
|
|
46
|
+
Legion::Logging.debug 'Imported OpenAI API key from Codex auth config' if defined?(Legion::Logging)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def resolve_env_api_key(value)
|
|
50
|
+
return nil if value.nil?
|
|
51
|
+
|
|
52
|
+
if value.is_a?(String)
|
|
53
|
+
return nil if value.empty?
|
|
54
|
+
|
|
55
|
+
if value.start_with?('env://')
|
|
56
|
+
env_name = value.sub('env://', '')
|
|
57
|
+
env_value = ENV.fetch(env_name, nil)
|
|
58
|
+
return nil if env_value.nil? || env_value.empty?
|
|
59
|
+
|
|
60
|
+
return env_value
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
return value
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
if value.is_a?(Array)
|
|
67
|
+
resolved = value.map { |v| resolve_env_api_key(v) }.compact
|
|
68
|
+
return nil if resolved.empty?
|
|
69
|
+
return resolved.first if resolved.length == 1
|
|
70
|
+
|
|
71
|
+
return resolved
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
value
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def token_valid?(token)
|
|
78
|
+
parts = token.split('.')
|
|
79
|
+
return true unless parts.length == 3
|
|
80
|
+
|
|
81
|
+
padded = parts[1] + ('=' * ((4 - (parts[1].length % 4)) % 4))
|
|
82
|
+
payload = ::JSON.parse(Base64.urlsafe_decode64(padded), symbolize_names: true)
|
|
83
|
+
exp = payload[:exp]
|
|
84
|
+
return true unless exp.is_a?(Integer)
|
|
85
|
+
|
|
86
|
+
exp > Time.now.to_i
|
|
87
|
+
rescue StandardError => e
|
|
88
|
+
Legion::Logging.debug("CodexConfigLoader: failed to parse access token for exp validation: #{e.class}: #{e.message}") if defined?(Legion::Logging)
|
|
89
|
+
true
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module LLM
|
|
5
|
+
module Helper
|
|
6
|
+
# --- Layered Defaults ---
|
|
7
|
+
# Override in your LEX to set extension-specific defaults.
|
|
8
|
+
# Resolution chain: per-call kwarg -> LEX override -> Settings -> nil (auto-detect)
|
|
9
|
+
|
|
10
|
+
def llm_default_model
|
|
11
|
+
return nil unless defined?(Legion::Settings)
|
|
12
|
+
|
|
13
|
+
Legion::Settings.dig(:llm, :default_model)
|
|
14
|
+
rescue StandardError
|
|
15
|
+
nil
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def llm_default_provider
|
|
19
|
+
return nil unless defined?(Legion::Settings)
|
|
20
|
+
|
|
21
|
+
Legion::Settings.dig(:llm, :default_provider)
|
|
22
|
+
rescue StandardError
|
|
23
|
+
nil
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def llm_default_intent
|
|
27
|
+
return nil unless defined?(Legion::Settings)
|
|
28
|
+
|
|
29
|
+
Legion::Settings.dig(:llm, :routing, :default_intent)
|
|
30
|
+
rescue StandardError
|
|
31
|
+
nil
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# --- Core Operations ---
|
|
35
|
+
|
|
36
|
+
def llm_chat(message, model: nil, provider: nil, intent: nil, tier: nil, tools: [], # rubocop:disable Metrics/ParameterLists
|
|
37
|
+
instructions: nil, compress: 0, escalate: nil, max_escalations: nil,
|
|
38
|
+
quality_check: nil, caller: nil, use_default_intent: false)
|
|
39
|
+
effective_model = model || llm_default_model
|
|
40
|
+
effective_provider = provider || llm_default_provider
|
|
41
|
+
effective_intent = intent || (use_default_intent ? llm_default_intent : nil)
|
|
42
|
+
|
|
43
|
+
if compress.positive?
|
|
44
|
+
message = Legion::LLM::Compressor.compress(message, level: compress)
|
|
45
|
+
instructions = Legion::LLM::Compressor.compress(instructions, level: compress) if instructions
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
if escalate
|
|
49
|
+
return Legion::LLM.chat(model: effective_model, provider: effective_provider,
|
|
50
|
+
intent: effective_intent, tier: tier,
|
|
51
|
+
escalate: true, max_escalations: max_escalations,
|
|
52
|
+
quality_check: quality_check, message: message, caller: caller)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
chat = Legion::LLM.chat(model: effective_model, provider: effective_provider,
|
|
56
|
+
intent: effective_intent, tier: tier,
|
|
57
|
+
escalate: false, caller: caller)
|
|
58
|
+
chat.with_instructions(instructions) if instructions
|
|
59
|
+
chat.with_tools(*tools) unless tools.empty?
|
|
60
|
+
chat.ask(message)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def llm_embed(text, **)
|
|
64
|
+
Legion::LLM.embed(text, **)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def llm_embed_batch(texts, **)
|
|
68
|
+
Legion::LLM.embed_batch(texts, **)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def llm_session(model: nil, provider: nil, intent: nil, tier: nil, caller: nil, use_default_intent: false)
|
|
72
|
+
effective_model = model || llm_default_model
|
|
73
|
+
effective_provider = provider || llm_default_provider
|
|
74
|
+
effective_intent = intent || (use_default_intent ? llm_default_intent : nil)
|
|
75
|
+
|
|
76
|
+
Legion::LLM.chat(model: effective_model, provider: effective_provider,
|
|
77
|
+
intent: effective_intent, tier: tier,
|
|
78
|
+
escalate: false, caller: caller)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def llm_structured(messages:, schema:, **)
|
|
82
|
+
Legion::LLM.structured(messages: messages, schema: schema, **)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def llm_ask(message:, **)
|
|
86
|
+
Legion::LLM.ask(message: message, **)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# --- Status ---
|
|
90
|
+
|
|
91
|
+
def llm_connected?
|
|
92
|
+
defined?(Legion::LLM) && Legion::LLM.started?
|
|
93
|
+
rescue StandardError
|
|
94
|
+
false
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def llm_can_embed?
|
|
98
|
+
llm_connected? && Legion::LLM.can_embed?
|
|
99
|
+
rescue StandardError
|
|
100
|
+
false
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def llm_routing_enabled?
|
|
104
|
+
llm_connected? && Legion::LLM::Router.routing_enabled?
|
|
105
|
+
rescue StandardError
|
|
106
|
+
false
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# --- Cost / Budget ---
|
|
110
|
+
|
|
111
|
+
def llm_cost_estimate(model: nil, input_tokens: 0, output_tokens: 0)
|
|
112
|
+
model ||= llm_default_model
|
|
113
|
+
Legion::LLM::CostEstimator.estimate(model_id: model, input_tokens: input_tokens,
|
|
114
|
+
output_tokens: output_tokens)
|
|
115
|
+
rescue StandardError
|
|
116
|
+
0.0
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def llm_cost_summary(since: nil)
|
|
120
|
+
Legion::LLM::CostTracker.summary(since: since)
|
|
121
|
+
rescue StandardError
|
|
122
|
+
{ total_cost_usd: 0.0, total_requests: 0, total_input_tokens: 0, total_output_tokens: 0, by_model: {} }
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def llm_budget_remaining
|
|
126
|
+
Legion::LLM::Hooks::BudgetGuard.remaining
|
|
127
|
+
rescue StandardError
|
|
128
|
+
Float::INFINITY
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
@@ -1,59 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'legion/llm/helper'
|
|
4
|
+
|
|
3
5
|
module Legion
|
|
4
6
|
module Extensions
|
|
5
7
|
module Helpers
|
|
6
8
|
module LLM
|
|
7
|
-
|
|
8
|
-
# @param message [String] the prompt
|
|
9
|
-
# @param model [String] optional model override
|
|
10
|
-
# @param provider [Symbol] optional provider override
|
|
11
|
-
# @param intent [Hash, nil] routing intent (capability, privacy, etc.)
|
|
12
|
-
# @param tier [Symbol, nil] explicit tier override
|
|
13
|
-
# @param tools [Array<Class>] optional RubyLLM::Tool subclasses
|
|
14
|
-
# @param instructions [String] optional system instructions
|
|
15
|
-
# @param escalate [Boolean, nil] enable model escalation on low-quality responses
|
|
16
|
-
# @param max_escalations [Integer, nil] max escalation attempts
|
|
17
|
-
# @param quality_check [Proc, nil] callable that returns true if response is acceptable
|
|
18
|
-
# @return [RubyLLM::Message] the assistant response
|
|
19
|
-
def llm_chat(message, model: nil, provider: nil, intent: nil, tier: nil, tools: [], instructions: nil, # rubocop:disable Metrics/ParameterLists
|
|
20
|
-
compress: 0, escalate: nil, max_escalations: nil, quality_check: nil, caller: nil)
|
|
21
|
-
if compress.positive?
|
|
22
|
-
message = Legion::LLM::Compressor.compress(message, level: compress)
|
|
23
|
-
instructions = Legion::LLM::Compressor.compress(instructions, level: compress) if instructions
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
# When escalation is active, chat() handles ask() internally via message: kwarg
|
|
27
|
-
if escalate
|
|
28
|
-
return Legion::LLM.chat(model: model, provider: provider, intent: intent, tier: tier,
|
|
29
|
-
escalate: true, max_escalations: max_escalations,
|
|
30
|
-
quality_check: quality_check, message: message, caller: caller)
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
chat = Legion::LLM.chat(model: model, provider: provider, intent: intent, tier: tier,
|
|
34
|
-
escalate: false, caller: caller)
|
|
35
|
-
chat.with_instructions(instructions) if instructions
|
|
36
|
-
chat.with_tools(*tools) unless tools.empty?
|
|
37
|
-
chat.ask(message)
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
# Quick embed from any extension runner
|
|
41
|
-
# @param text [String, Array<String>] text to embed
|
|
42
|
-
# @param model [String] optional model override
|
|
43
|
-
# @return [RubyLLM::Embedding]
|
|
44
|
-
def llm_embed(text, model: nil)
|
|
45
|
-
Legion::LLM.embed(text, model: model)
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
# Get a raw chat object for multi-turn conversations
|
|
49
|
-
# @param model [String] optional model override
|
|
50
|
-
# @param provider [Symbol] optional provider override
|
|
51
|
-
# @param intent [Hash, nil] routing intent (capability, privacy, etc.)
|
|
52
|
-
# @param tier [Symbol, nil] explicit tier override
|
|
53
|
-
# @return [RubyLLM::Chat]
|
|
54
|
-
def llm_session(model: nil, provider: nil, intent: nil, tier: nil, caller: nil)
|
|
55
|
-
Legion::LLM.chat(model: model, provider: provider, intent: intent, tier: tier, escalate: false, caller: caller)
|
|
56
|
-
end
|
|
9
|
+
include Legion::LLM::Helper
|
|
57
10
|
end
|
|
58
11
|
end
|
|
59
12
|
end
|
data/lib/legion/llm/version.rb
CHANGED
data/lib/legion/llm.rb
CHANGED
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.5.
|
|
4
|
+
version: 0.5.20
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Esity
|
|
@@ -179,6 +179,7 @@ files:
|
|
|
179
179
|
- ".github/workflows/ci.yml"
|
|
180
180
|
- ".gitignore"
|
|
181
181
|
- ".rubocop.yml"
|
|
182
|
+
- AGENTS.md
|
|
182
183
|
- CHANGELOG.md
|
|
183
184
|
- CLAUDE.md
|
|
184
185
|
- CODEOWNERS
|
|
@@ -213,6 +214,7 @@ files:
|
|
|
213
214
|
- lib/legion/llm/bedrock_bearer_auth.rb
|
|
214
215
|
- lib/legion/llm/cache.rb
|
|
215
216
|
- lib/legion/llm/claude_config_loader.rb
|
|
217
|
+
- lib/legion/llm/codex_config_loader.rb
|
|
216
218
|
- lib/legion/llm/compressor.rb
|
|
217
219
|
- lib/legion/llm/confidence_score.rb
|
|
218
220
|
- lib/legion/llm/confidence_scorer.rb
|
|
@@ -230,6 +232,7 @@ files:
|
|
|
230
232
|
- lib/legion/llm/fleet/dispatcher.rb
|
|
231
233
|
- lib/legion/llm/fleet/handler.rb
|
|
232
234
|
- lib/legion/llm/fleet/reply_dispatcher.rb
|
|
235
|
+
- lib/legion/llm/helper.rb
|
|
233
236
|
- lib/legion/llm/helpers/llm.rb
|
|
234
237
|
- lib/legion/llm/hooks.rb
|
|
235
238
|
- lib/legion/llm/hooks/budget_guard.rb
|