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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3eec433896cf314b65ac64111f414a55dc7e9639db3d54cc76359c9fb01ed567
4
- data.tar.gz: 659857c6aa71573f6fbb795a655b7a59f2936672b0538db0bc14c18ae06ecfbd
3
+ metadata.gz: 911fae6cf0899e9ce7ed1fea909e023b411f3437ea0225d263bcc1cb95d6f9b6
4
+ data.tar.gz: e7f85c4c731a1e5fe8af13dfcc85f4497619256923b6eba39afc2d9e49574863
5
5
  SHA512:
6
- metadata.gz: 648b2bd80386141477bf5b77b387814f768d5f64447a921d34ef828dde6d9469a15e63211e63c4173e7188e0a61c5d60eb366415ccb68c43c031a23cd4889ac3
7
- data.tar.gz: 22f1a949a48470e5a6b543a683e2de4f9c86f44d1c21d706e704bb4a5f58672311ba632b03710647beb071987abed2947e6299e0ad69c06b73c72484398fb238
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(
@@ -221,6 +221,7 @@ module Legion
221
221
  tool_calls: tool_calls,
222
222
  stop_reason: stop_reason
223
223
  )
224
+ set_routing_response_headers(pipeline_response: pipeline_response)
224
225
 
225
226
  payload = {
226
227
  request_id: request_id,
@@ -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
- estimated = ContextAccounting.estimate_message_tokens(reduced)
216
- estimated += ContextAccounting.estimate_text_tokens(routing_estimate_injected_system)
217
- estimated += ContextAccounting.estimate_json_tokens(routing_estimate_tool_defs)
218
- estimated += ContextAccounting.estimate_json_tokens(@request.tool_choice) if @request.tool_choice
219
- estimated += ContextAccounting.estimate_json_tokens(@request.thinking) if @request.thinking.is_a?(Hash) && @request.thinking.any?
220
- estimated
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
- Legion::Settings::Extensions.filter_tools(deferred: false).each { |e| defs << e } if registry_tool_injection_requested?
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')
@@ -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
@@ -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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Legion
4
4
  module LLM
5
- VERSION = '0.14.16'
5
+ VERSION = '0.14.17'
6
6
  end
7
7
  end
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.14.16
4
+ version: 0.14.17
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity