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 +4 -4
- data/CHANGELOG.md +38 -0
- data/legion-llm.gemspec +1 -1
- data/lib/legion/llm/api/client_translators/openai_responses.rb +4 -6
- data/lib/legion/llm/call/lex_llm_adapter.rb +1 -1
- data/lib/legion/llm/inference/executor/context_window.rb +2 -2
- data/lib/legion/llm/inference/executor/routing.rb +6 -9
- data/lib/legion/llm/inference/executor.rb +1 -1
- data/lib/legion/llm/inference.rb +16 -9
- data/lib/legion/llm/types/message.rb +1 -1
- data/lib/legion/llm/version.rb +1 -1
- metadata +3 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f67a1a83e7213a2e50437b07e71afd74302c2b1da8229708ec8d99506d45354e
|
|
4
|
+
data.tar.gz: fa1dcc7ae0d4b64dd2716271dd5c50778ff178fa3987704b16302c00b75029ab
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
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
|
-
|
|
156
|
-
|
|
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
|
|
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
|
-
|
|
87
|
-
messages
|
|
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
|
data/lib/legion/llm/inference.rb
CHANGED
|
@@ -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
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
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?(:
|
|
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
|
|
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
|
data/lib/legion/llm/version.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.14.
|
|
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.
|
|
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.
|
|
151
|
+
version: 0.6.3
|
|
152
152
|
- !ruby/object:Gem::Dependency
|
|
153
153
|
name: pdf-reader
|
|
154
154
|
requirement: !ruby/object:Gem::Requirement
|