legion-llm 0.14.4 → 0.14.9

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: 433b9bf3832cc3c6fcb25ab53d135306475b4147a242af49a3df5fe52eab9946
4
- data.tar.gz: 94873f17f47ef2c08cab12e8ad6de884af7bb108bdb7967cf30df5b32c21234e
3
+ metadata.gz: f67a1a83e7213a2e50437b07e71afd74302c2b1da8229708ec8d99506d45354e
4
+ data.tar.gz: fa1dcc7ae0d4b64dd2716271dd5c50778ff178fa3987704b16302c00b75029ab
5
5
  SHA512:
6
- metadata.gz: 8c96394a77c183d9441d3b56461c0d1303dc93823e2ca87bb68e8e21e6903a484fc36e7242ef297ea9312e4de9d36ec5abfcbd36a8b7966566d98d123ed85085
7
- data.tar.gz: b3bb6181d62ef27e6c812d34f3c9efc6b540e041025324e92fe7d199cfe28f3a619baa20a4dd5bde1eea7153afa0420d98332bcf96c941923e0d15afe563c722
6
+ metadata.gz: b3c1802c7bc32daa15d15c0fc8b16923d8fac48531adc5bcfb2997eec6234534166b85c43e9262774dedb63e28a7a62c148614bdac53e00c15fe44598770823b
7
+ data.tar.gz: cc6ae96b0bfc1e1e54298b028334d1bd1f19e18147ac0d7a7d61bb849c86e157ff26b986082be3027ba4199570481d91b960ce6eb1d82b2ec5aa66d787d3a9b7
data/CHANGELOG.md CHANGED
@@ -1,5 +1,43 @@
1
1
  # Legion LLM Changelog
2
2
 
3
+ ## [0.14.9] - 2026-06-25
4
+
5
+ ### Fixed
6
+
7
+ - Routing token estimate (`estimate_request_tokens`) now includes system prompt, tool definitions, and thinking config — matching what `enforce_final_context_budget!` checks at dispatch time. Previously only counted messages, causing large-context requests (e.g. Copilot Chat with 81 tools + system prompt) to route to small-context models and fail with ContextOverflow at dispatch.
8
+ - `resolve_routing_state` always consults `request_lane` when Inventory has lanes, removing the gate that required explicit routing flags. Enables weight-based lane selection for all callers including `Legion::LLM.chat`.
9
+
10
+ ### Added
11
+
12
+ - `:subcall` profile in `Inference::Profile` — skips all pipeline steps except routing, provider_call, and metering. Used for in-pipeline LLM sub-calls (debate, lex-knowledge, lex-apollo) to avoid recursive full-pipeline execution.
13
+ - `Executor.new(request, skip_pipeline: true)` kwarg forces `:subcall` profile without requiring caller identity manipulation.
14
+ - `dispatch_chat` unified path — builds a `Request` via `from_chat_args` and runs the Executor directly, detecting in-pipeline context via `Thread.current[:legion_llm_in_pipeline]`.
15
+
16
+ ## [0.14.8] - 2026-06-25
17
+
18
+ ### Fixed
19
+
20
+ - Responses translator (`format_response`) no longer emits a message item with empty `output_text` when the response is pure tool_use (function_call items present). The empty text triggered Codex's `[Your previous response had no visible output]` injection on every tool turn.
21
+
22
+ ## [0.14.7] - 2026-06-25
23
+
24
+ ### Fixed
25
+
26
+ - `step_context_store` stores `typed_msg.text` (extracted string) instead of `typed_msg.content` (raw Array), preventing ContentBlock objects from being serialized as `#inspect` strings in conversation history.
27
+ - `Types::Message#text_from_block` recognizes `output_text`/`input_text` content block types so token estimation and text extraction work for Responses API content.
28
+ - `lex_llm_adapter.rb` `text_part_content` Data struct branch handles `output_text`/`input_text` types (previously only matched `type == 'text'`).
29
+ - `context_window.rb` `compact_to_fit` loops halving until messages fit the target token budget instead of a single halve that leaves payloads 2x over threshold for very large conversations.
30
+ - `emit_non_pipeline_metering` reads tokens from `response.usage.input_tokens` (correct) instead of `response.input_tokens` (non-existent on Canonical::Response), fixing zero-token metering for internal extension LLM calls (lex-apollo, legion-gaia, lex-knowledge, etc.).
31
+ - `emit_non_pipeline_metering` reads `response.metadata` instead of `response.meta`, passes `messages` and `response_content` to metering events.
32
+
33
+ ## [0.14.5] - 2026-06-24
34
+
35
+ ### Fixed
36
+
37
+ - Non-pipeline metering (`emit_non_pipeline_metering`) now correctly reads token counts from `response.usage` instead of the non-existent top-level `input_tokens`/`output_tokens` on `Canonical::Response`. Previously all internal LLM calls (via `Legion::LLM.structured`, `chat_single_native`) emitted zero-token metering events.
38
+ - Non-pipeline metering now includes `messages` and `response_content` fields, fixing null request/response JSON columns in the ledger for internal extension calls (lex-apollo, lex-agentic-self, legion-gaia, etc.).
39
+ - Non-pipeline metering reads `response.metadata` (correct field) instead of `response.meta` (does not exist on `Canonical::Response`) for latency/timing extraction.
40
+
3
41
  ## [0.14.4] - 2026-06-23
4
42
 
5
43
  ### Fixed
data/legion-llm.gemspec CHANGED
@@ -34,7 +34,7 @@ Gem::Specification.new do |spec|
34
34
  spec.add_dependency 'legion-settings', '>= 1.4.0'
35
35
  spec.add_dependency 'legion-transport', '>= 1.4.14'
36
36
  spec.add_dependency 'lex-knowledge'
37
- spec.add_dependency 'lex-llm', '>= 0.6.0'
37
+ spec.add_dependency 'lex-llm', '>= 0.6.3'
38
38
  spec.add_dependency 'pdf-reader'
39
39
  spec.add_dependency 'sinatra-contrib', '>= 2.0'
40
40
  spec.add_dependency 'tzinfo', '>= 2.0'
@@ -151,18 +151,16 @@ module Legion
151
151
  server_tool_items = build_output_server_tool_items(pipeline_response)
152
152
  reasoning = build_output_reasoning(pipeline_response)
153
153
 
154
- output = [
155
- *reasoning,
156
- *server_tool_items,
157
- *actionable_tool_calls,
158
- {
154
+ output = [*reasoning, *server_tool_items, *actionable_tool_calls]
155
+ unless content.to_s.strip.empty? && (actionable_tool_calls.any? || server_tool_items.any?)
156
+ output << {
159
157
  type: 'message',
160
158
  id: "msg_#{SecureRandom.hex(12)}",
161
159
  role: 'assistant',
162
160
  content: [{ type: 'output_text', text: content }],
163
161
  status: 'completed'
164
162
  }
165
- ]
163
+ end
166
164
 
167
165
  # Responses protocol: a turn is always status `completed`. Both
168
166
  # client-callable calls (actionable_tool_calls) and LegionIO-run
@@ -753,7 +753,7 @@ module Legion
753
753
  if part.respond_to?(:type) || part.respond_to?(:text)
754
754
  type = (part.respond_to?(:type) ? part.type.to_s : '')
755
755
  text = part.respond_to?(:text) ? part.text : nil
756
- return text.to_s if type == 'text' || (type.empty? && !text.nil?)
756
+ return text.to_s if %w[text output_text input_text].include?(type) || (type.empty? && !text.nil?)
757
757
 
758
758
  return nil
759
759
  end
@@ -83,8 +83,8 @@ module Legion
83
83
 
84
84
  return messages if estimate_message_tokens(messages) <= target_tokens
85
85
 
86
- half = messages.size / 2
87
- messages.last(half)
86
+ messages = messages.last(messages.size / 2) while messages.size > 2 && estimate_message_tokens(messages) > target_tokens
87
+ messages
88
88
  end
89
89
 
90
90
  def resolved_context_window
@@ -199,14 +199,15 @@ module Legion
199
199
  end
200
200
 
201
201
  def estimate_request_tokens
202
- # Estimate total tokens from current request messages + conversation history.
203
- # This is used by the router to exclude models whose context window can't fit.
204
202
  all_messages = []
205
203
  all_messages.concat(@enrichments['context:conversation_history'] || [])
206
204
  all_messages.concat(@request.messages || [])
207
- return 0 if all_messages.empty?
208
205
 
209
- estimate_message_tokens(all_messages)
206
+ estimated = all_messages.empty? ? 0 : estimate_message_tokens(all_messages)
207
+ estimated += ((@request.system || '').length / 4.0).ceil
208
+ estimated += estimate_tool_token_budget if @request.tools&.any?
209
+ estimated += (Legion::JSON.dump(@request.thinking).length / 3.5).ceil if @request.thinking.is_a?(Hash) && @request.thinking.any?
210
+ estimated
210
211
  end
211
212
 
212
213
  def chain_required_capabilities
@@ -367,11 +368,7 @@ module Legion
367
368
 
368
369
  def resolve_routing_state(state)
369
370
  return state unless defined?(Router)
370
-
371
- explicit_route = state[:provider_explicit] || state[:instance_explicit] || state[:tier_explicit]
372
- auto_route = state[:auto_route] == true
373
- intent_route = state[:intent_explicit] && state[:intent] && Router.routing_enabled?
374
- return state unless explicit_route || auto_route || intent_route
371
+ return state unless Legion::LLM::Inventory.lanes.any?
375
372
 
376
373
  resolution = routing_resolution_for(state)
377
374
  return state unless resolution
@@ -947,7 +947,7 @@ module Legion
947
947
 
948
948
  attrs = {
949
949
  role: typed_msg.role,
950
- content: typed_msg.content,
950
+ content: typed_msg.text,
951
951
  conversation_id: conv_id,
952
952
  task_id: typed_msg.task_id
953
953
  }
@@ -605,7 +605,7 @@ module Legion
605
605
  Call::Dispatch.call(provider: provider, instance: instance, capability: :chat, model: model,
606
606
  messages: messages, **)
607
607
  end
608
- emit_non_pipeline_metering(result, model: model, provider: provider, caller: caller)
608
+ emit_non_pipeline_metering(result, model: model, provider: provider, caller: caller, messages: messages)
609
609
  result
610
610
  end
611
611
 
@@ -890,14 +890,13 @@ module Legion
890
890
  esc[:quality_threshold] || 50
891
891
  end
892
892
 
893
- def emit_non_pipeline_metering(response, model:, provider:, caller: nil)
893
+ def emit_non_pipeline_metering(response, model:, provider:, caller: nil, messages: nil)
894
894
  return unless response
895
895
 
896
- input = response.respond_to?(:input_tokens) ? response.input_tokens.to_i : 0
897
- output = response.respond_to?(:output_tokens) ? response.output_tokens.to_i : 0
898
- usage = response.respond_to?(:usage) ? response.usage : {}
899
- usage_hash = usage.is_a?(Hash) ? usage : {}
900
- thinking = (usage_hash[:thinking_tokens] || usage_hash[:thinking] || 0).to_i
896
+ usage = response.respond_to?(:usage) ? response.usage : nil
897
+ input = usage.respond_to?(:input_tokens) ? usage.input_tokens.to_i : 0
898
+ output = usage.respond_to?(:output_tokens) ? usage.output_tokens.to_i : 0
899
+ thinking = usage.respond_to?(:thinking_tokens) ? usage.thinking_tokens.to_i : 0
901
900
 
902
901
  finish = nil
903
902
  if response.respond_to?(:stop_reason)
@@ -906,11 +905,17 @@ module Legion
906
905
  finish = response.stop[:reason]&.to_s
907
906
  end
908
907
 
909
- meta = response.respond_to?(:meta) ? response.meta : {}
908
+ meta = response.respond_to?(:metadata) ? response.metadata : {}
910
909
  meta_hash = meta.is_a?(Hash) ? meta : {}
911
910
  latency = (meta_hash[:latency_ms] || meta_hash.dig(:timing, :latency_ms) || 0).to_i
912
911
  wall_clock = (meta_hash[:wall_clock_ms] || meta_hash.dig(:timing, :wall_clock_ms) || 0).to_i
913
912
 
913
+ response_content = if response.respond_to?(:text)
914
+ response.text
915
+ elsif response.respond_to?(:content)
916
+ response.content
917
+ end
918
+
914
919
  Legion::LLM::Metering.emit(
915
920
  provider: provider,
916
921
  model_id: model,
@@ -926,7 +931,9 @@ module Legion
926
931
  wall_clock_ms: wall_clock,
927
932
  caller: caller,
928
933
  event_type: 'llm_completion',
929
- status: 'success'
934
+ status: 'success',
935
+ messages: messages,
936
+ response_content: response_content
930
937
  )
931
938
  rescue StandardError => e
932
939
  handle_exception(e, level: :warn, operation: 'llm.inference.non_pipeline_metering')
@@ -93,7 +93,7 @@ module Legion
93
93
  return nil unless block.is_a?(Hash)
94
94
 
95
95
  type = block[:type] || block['type']
96
- return nil unless type.nil? || type.to_s == 'text'
96
+ return nil unless type.nil? || %w[text output_text input_text].include?(type.to_s)
97
97
 
98
98
  block[:text] || block['text'] || block[:content] || block['content']
99
99
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Legion
4
4
  module LLM
5
- VERSION = '0.14.4'
5
+ VERSION = '0.14.9'
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.4
4
+ version: 0.14.9
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity
@@ -141,14 +141,14 @@ dependencies:
141
141
  requirements:
142
142
  - - ">="
143
143
  - !ruby/object:Gem::Version
144
- version: 0.6.0
144
+ version: 0.6.3
145
145
  type: :runtime
146
146
  prerelease: false
147
147
  version_requirements: !ruby/object:Gem::Requirement
148
148
  requirements:
149
149
  - - ">="
150
150
  - !ruby/object:Gem::Version
151
- version: 0.6.0
151
+ version: 0.6.3
152
152
  - !ruby/object:Gem::Dependency
153
153
  name: pdf-reader
154
154
  requirement: !ruby/object:Gem::Requirement