legion-llm 0.14.16 → 0.14.17
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 +11 -0
- data/lib/legion/llm/api/namespaces/anthropic/messages.rb +1 -0
- data/lib/legion/llm/api/namespaces/native/inference.rb +1 -0
- data/lib/legion/llm/api/namespaces/openai/chat/completions.rb +1 -0
- data/lib/legion/llm/api/namespaces/openai/responses.rb +1 -0
- data/lib/legion/llm/api/shared_helpers.rb +11 -0
- data/lib/legion/llm/inference/executor/routing.rb +8 -19
- data/lib/legion/llm/inventory.rb +22 -0
- data/lib/legion/llm/router.rb +27 -1
- data/lib/legion/llm/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 911fae6cf0899e9ce7ed1fea909e023b411f3437ea0225d263bcc1cb95d6f9b6
|
|
4
|
+
data.tar.gz: e7f85c4c731a1e5fe8af13dfcc85f4497619256923b6eba39afc2d9e49574863
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: bd8368de549a561693308531c6a5efd64ba2cbf09ca8f5cc638c76c335944392686a44418204eb5895e9386d388160550b79426c4ed832931c759dec0eee2c4d
|
|
7
|
+
data.tar.gz: cd03fa5498c7783c3be2a587c3d6ec8543ab203b19ddb3e6e207388107e30ed54b66f08980f7a5c2b9e64147d4c4defe23bbb62ed46ebf33257a96f3ceeae9d8
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# Legion LLM Changelog
|
|
2
2
|
|
|
3
|
+
## [0.14.17] - 2026-07-22
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- Physical Binning (SSoT v2): Router Range Sieve steers requests to hardware based on workload size
|
|
7
|
+
- 3-tier priority: specific range match → generalist → full eligible fallback
|
|
8
|
+
- Boundaries are lower-inclusive, upper-exclusive (`[min, max)`)
|
|
9
|
+
- Escalation drains each tier naturally via `tried_lanes`
|
|
10
|
+
- Inventory lane enrichment: `preferred_min_context_tokens` / `preferred_max_context_tokens` injected from instance settings
|
|
11
|
+
- Works for ALL providers automatically (same settings path as instance weights)
|
|
12
|
+
- Settings path: `settings[:extensions][:llm][provider][:instances][id][:preferred_min/max_context_tokens]`
|
|
13
|
+
|
|
3
14
|
## [0.14.16] - 2026-07-14
|
|
4
15
|
|
|
5
16
|
### Fixed
|
|
@@ -95,6 +95,7 @@ module Legion
|
|
|
95
95
|
stream: false,
|
|
96
96
|
started_at: request_started_at
|
|
97
97
|
)
|
|
98
|
+
set_routing_response_headers(pipeline_response: pipeline_response)
|
|
98
99
|
|
|
99
100
|
if canonical_format
|
|
100
101
|
status_code, response_headers, body_string = Legion::LLM::API::DebugFormats.render_canonical_response(
|
|
@@ -106,6 +106,7 @@ module Legion
|
|
|
106
106
|
stream: false,
|
|
107
107
|
started_at: request_started_at
|
|
108
108
|
)
|
|
109
|
+
set_routing_response_headers(pipeline_response: pipeline_response)
|
|
109
110
|
|
|
110
111
|
if canonical_format
|
|
111
112
|
status_code, response_headers, body_string = Legion::LLM::API::DebugFormats.render_canonical_response(
|
|
@@ -104,6 +104,7 @@ module Legion
|
|
|
104
104
|
stream: false,
|
|
105
105
|
started_at: request_started_at
|
|
106
106
|
)
|
|
107
|
+
set_routing_response_headers(pipeline_response: pipeline_response)
|
|
107
108
|
|
|
108
109
|
if canonical_format
|
|
109
110
|
status_code, response_headers, body_string = Legion::LLM::API::DebugFormats.render_canonical_response(
|
|
@@ -211,6 +211,17 @@ module Legion
|
|
|
211
211
|
request_id: request_id)
|
|
212
212
|
end
|
|
213
213
|
|
|
214
|
+
def set_routing_response_headers(pipeline_response:)
|
|
215
|
+
routing = pipeline_response.respond_to?(:routing) ? pipeline_response.routing || {} : {}
|
|
216
|
+
provider_val = api_hash_value(routing, :provider)
|
|
217
|
+
instance_val = api_hash_value(routing, :instance)
|
|
218
|
+
model_val = api_hash_value(routing, :model)
|
|
219
|
+
|
|
220
|
+
headers 'X-Legion-Provider' => provider_val.to_s if provider_val
|
|
221
|
+
headers 'X-Legion-Instance' => instance_val.to_s if instance_val
|
|
222
|
+
headers 'X-Legion-Model' => model_val.to_s if model_val
|
|
223
|
+
end
|
|
224
|
+
|
|
214
225
|
def api_duration_ms(started_at)
|
|
215
226
|
return 0 unless started_at
|
|
216
227
|
|
|
@@ -204,20 +204,16 @@ module Legion
|
|
|
204
204
|
# dispatch. Use ContextAccounting (walks canonical structs / content
|
|
205
205
|
# blocks) for messages, estimate the INJECTED system (baseline + RAG +
|
|
206
206
|
# skills + prior-history text), and count client tools. Messages are the
|
|
207
|
-
# lane-independent-reduced set — what actually goes on the wire.
|
|
208
207
|
def estimate_request_tokens
|
|
209
|
-
# Mirror the dispatch shape EXACTLY: native_dispatch_messages sends
|
|
210
|
-
# @request.messages (lane-independent-reduced); the conversation
|
|
211
|
-
# history lives in the injected SYSTEM (via EnrichmentInjector), not
|
|
212
|
-
# in the messages array — counting it in both would double-count.
|
|
213
208
|
reduced = reduce_messages_for_dispatch(Array(@request.messages))
|
|
214
209
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
210
|
+
msg_tokens = ContextAccounting.estimate_message_tokens(reduced)
|
|
211
|
+
sys_tokens = ContextAccounting.estimate_text_tokens(routing_estimate_injected_system)
|
|
212
|
+
tool_tokens = ContextAccounting.estimate_json_tokens(routing_estimate_tool_defs)
|
|
213
|
+
choice_tokens = @request.tool_choice ? ContextAccounting.estimate_json_tokens(@request.tool_choice) : 0
|
|
214
|
+
thinking_tokens = @request.thinking.is_a?(Hash) && @request.thinking.any? ? ContextAccounting.estimate_json_tokens(@request.thinking) : 0
|
|
215
|
+
|
|
216
|
+
msg_tokens + sys_tokens + tool_tokens + choice_tokens + thinking_tokens
|
|
221
217
|
end
|
|
222
218
|
|
|
223
219
|
# The system prompt as it will be dispatched — baseline + GAIA + prior
|
|
@@ -227,17 +223,10 @@ module Legion
|
|
|
227
223
|
EnrichmentInjector.inject(system: @request.system, enrichments: @enrichments || {})
|
|
228
224
|
end
|
|
229
225
|
|
|
230
|
-
# The tool catalog dispatch will send (special + client + registry), NOT
|
|
231
|
-
# just @request.tools — the dispatch guard counts native_dispatch_tools,
|
|
232
|
-
# so routing must too or a heavy-tool request under-counts and picks a
|
|
233
|
-
# too-small lane. Built read-only here (never memoized) because
|
|
234
|
-
# native_tool_definitions caches and applies the local-provider cap
|
|
235
|
-
# against @resolved_provider, which is not set at routing time; an
|
|
236
|
-
# uncapped over-estimate is the safe direction for a "will it fit" filter.
|
|
237
226
|
def routing_estimate_tool_defs
|
|
238
227
|
defs = Tools::Special.pinned_definitions.map(&:to_h)
|
|
239
228
|
Array(@request.tools).each { |t| defs << (t.respond_to?(:to_h) ? t.to_h : t) }
|
|
240
|
-
|
|
229
|
+
Array(@discovered_tools).each { |t| defs << (t.respond_to?(:to_h) ? t.to_h : t) }
|
|
241
230
|
defs
|
|
242
231
|
rescue StandardError => e
|
|
243
232
|
handle_exception(e, level: :warn, handled: true, operation: 'llm.pipeline.routing_estimate_tool_defs')
|
data/lib/legion/llm/inventory.rb
CHANGED
|
@@ -25,6 +25,8 @@ module Legion
|
|
|
25
25
|
validate_lane!(lane: lane)
|
|
26
26
|
return policy_skip(lane: lane) if policy_denied?(lane: lane)
|
|
27
27
|
|
|
28
|
+
lane = enrich_lane_range(lane: lane)
|
|
29
|
+
|
|
28
30
|
existing = live_map[lane[:id]]
|
|
29
31
|
resolved_health = if health == :preserve
|
|
30
32
|
existing&.dig(:health) || default_health
|
|
@@ -180,6 +182,26 @@ module Legion
|
|
|
180
182
|
(base * health_mult).to_i
|
|
181
183
|
end
|
|
182
184
|
|
|
185
|
+
def enrich_lane_range(lane:)
|
|
186
|
+
provider_settings = Legion::Settings.dig(:extensions, :llm, lane[:provider_family].to_sym)
|
|
187
|
+
return lane unless provider_settings.is_a?(Hash)
|
|
188
|
+
|
|
189
|
+
instances = provider_settings[:instances]
|
|
190
|
+
return lane unless instances.is_a?(Hash)
|
|
191
|
+
|
|
192
|
+
inst_cfg = instances[lane[:instance_id]] || instances[lane[:instance_id]&.to_sym]
|
|
193
|
+
return lane unless inst_cfg.is_a?(Hash)
|
|
194
|
+
|
|
195
|
+
min_val = inst_cfg[:preferred_min_context_tokens]
|
|
196
|
+
max_val = inst_cfg[:preferred_max_context_tokens]
|
|
197
|
+
return lane if min_val.nil? && max_val.nil?
|
|
198
|
+
|
|
199
|
+
enriched = lane.dup
|
|
200
|
+
enriched[:preferred_min_context_tokens] = min_val if min_val
|
|
201
|
+
enriched[:preferred_max_context_tokens] = max_val if max_val
|
|
202
|
+
enriched
|
|
203
|
+
end
|
|
204
|
+
|
|
183
205
|
def lane_weights_from_settings(lane:)
|
|
184
206
|
tier_weights = Legion::Settings.dig(:llm, :routing, :tier_weights) || {}
|
|
185
207
|
provider_sym = lane[:provider_family].to_sym
|
data/lib/legion/llm/router.rb
CHANGED
|
@@ -76,7 +76,9 @@ module Legion
|
|
|
76
76
|
|
|
77
77
|
return nil if eligible.empty?
|
|
78
78
|
|
|
79
|
-
eligible
|
|
79
|
+
pool = range_sieve(eligible: eligible, estimated_context: estimated_context)
|
|
80
|
+
|
|
81
|
+
pool
|
|
80
82
|
.group_by { |lane| lane[:lane_weight] }
|
|
81
83
|
.max_by { |weight, _| weight }
|
|
82
84
|
.last
|
|
@@ -190,6 +192,30 @@ module Legion
|
|
|
190
192
|
|
|
191
193
|
private
|
|
192
194
|
|
|
195
|
+
def range_sieve(eligible:, estimated_context:)
|
|
196
|
+
return eligible if estimated_context.nil?
|
|
197
|
+
|
|
198
|
+
specific, generalist = eligible.partition { |lane| lane_has_range?(lane) }
|
|
199
|
+
matched = specific.select { |lane| lane_in_range?(lane: lane, estimated_context: estimated_context) }
|
|
200
|
+
|
|
201
|
+
return matched unless matched.empty?
|
|
202
|
+
return generalist unless generalist.empty?
|
|
203
|
+
|
|
204
|
+
eligible
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def lane_has_range?(lane)
|
|
208
|
+
!lane[:preferred_min_context_tokens].nil? || !lane[:preferred_max_context_tokens].nil?
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def lane_in_range?(lane:, estimated_context:)
|
|
212
|
+
lower = (lane[:preferred_min_context_tokens] || 0).to_i
|
|
213
|
+
upper = lane[:preferred_max_context_tokens]
|
|
214
|
+
upper = upper ? upper.to_i : Float::INFINITY
|
|
215
|
+
|
|
216
|
+
estimated_context >= lower && estimated_context < upper
|
|
217
|
+
end
|
|
218
|
+
|
|
193
219
|
def lane_passes_hard_filters?(lane:, type:, tiers:, providers:, instances:, models:,
|
|
194
220
|
capabilities:, thinking:, privacy:, estimated_context:, **)
|
|
195
221
|
return false if lane[:type] != type
|
data/lib/legion/llm/version.rb
CHANGED