lex-llm 0.4.5 → 0.4.7
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 +10 -0
- data/lib/legion/extensions/llm/fleet/worker_execution.rb +9 -0
- data/lib/legion/extensions/llm/provider/open_ai_compatible.rb +17 -2
- data/lib/legion/extensions/llm/stream_accumulator.rb +46 -29
- data/lib/legion/extensions/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: b7b27c6f6a3d3166cdf483b997908532b2a511e57496fc13bd9c1d854d0daee7
|
|
4
|
+
data.tar.gz: b21fda8924c6e108905b46ab962f62e67095f2a801dda19e284b639c7152fdd2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: cd6836a39da034186d4987b91066ecb4506c7dab624bdcc54c623657a5fa3f76a3a8645f4541593f5e9b32de364677542960bdacf1c8140d4605db1b5e58d79f
|
|
7
|
+
data.tar.gz: 163cc40607ab68e5014460e426558e4b6be9a3ccf3cf25ab7b5414edeed430a5e293a8a9f9cd9cd0ac6d636e07d806223142881233836bb50343f47b64a12628
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.7 - 2026-05-08
|
|
4
|
+
|
|
5
|
+
- Unpack legacy nested fleet `options` before provider dispatch so `system` and `tools` arrive as normal provider keyword arguments.
|
|
6
|
+
|
|
7
|
+
## 0.4.6 - 2026-05-07
|
|
8
|
+
|
|
9
|
+
- Render OpenAI-compatible embedding payloads with the canonical model id when callers pass `Model::Info` objects.
|
|
10
|
+
- Preserve streamed OpenAI-compatible tool-call argument fragments until the accumulator can assemble and parse the full JSON payload.
|
|
11
|
+
- Treat malformed accumulated streaming tool arguments as handled provider output and return empty arguments instead of raising.
|
|
12
|
+
|
|
3
13
|
## 0.4.5 - 2026-05-07
|
|
4
14
|
|
|
5
15
|
- Add `ProviderSettings.infer_tier_from_endpoint(url)` shared utility: returns `:local` for localhost/loopback endpoints, `:direct` for all other hosts. Handles `URI::InvalidURIError` and nil safely.
|
|
@@ -64,6 +64,7 @@ module Legion
|
|
|
64
64
|
provider = provider.call(envelope) if provider.respond_to?(:call) && !provider.respond_to?(:chat)
|
|
65
65
|
operation = envelope_value(envelope, :operation).to_sym
|
|
66
66
|
params = normalize_hash(envelope_value(envelope, :params) || {})
|
|
67
|
+
params = unpack_legacy_options(params)
|
|
67
68
|
model = envelope_value(envelope, :model)
|
|
68
69
|
|
|
69
70
|
case operation
|
|
@@ -80,6 +81,14 @@ module Legion
|
|
|
80
81
|
end
|
|
81
82
|
end
|
|
82
83
|
|
|
84
|
+
def unpack_legacy_options(params)
|
|
85
|
+
options = params.delete(:options)
|
|
86
|
+
return params unless options.is_a?(Hash)
|
|
87
|
+
|
|
88
|
+
normalize_hash(options).each { |key, value| params[key] = value unless params.key?(key) }
|
|
89
|
+
params
|
|
90
|
+
end
|
|
91
|
+
|
|
83
92
|
def reset_idempotency_cache!
|
|
84
93
|
@idempotency_keys = Concurrent::Map.new
|
|
85
94
|
@idempotency_mutex = Mutex.new
|
|
@@ -176,7 +176,7 @@ module Legion
|
|
|
176
176
|
role: :assistant,
|
|
177
177
|
content: content,
|
|
178
178
|
model_id: data['model'],
|
|
179
|
-
tool_calls:
|
|
179
|
+
tool_calls: parse_streaming_tool_calls(delta['tool_calls']),
|
|
180
180
|
thinking: thinking,
|
|
181
181
|
input_tokens: usage['prompt_tokens'],
|
|
182
182
|
output_tokens: usage['completion_tokens'],
|
|
@@ -184,6 +184,21 @@ module Legion
|
|
|
184
184
|
)
|
|
185
185
|
end
|
|
186
186
|
|
|
187
|
+
def parse_streaming_tool_calls(tool_calls)
|
|
188
|
+
return nil unless tool_calls&.any?
|
|
189
|
+
|
|
190
|
+
tool_calls.to_h do |call|
|
|
191
|
+
function = call.fetch('function', {})
|
|
192
|
+
name = function['name']
|
|
193
|
+
id = call['id']
|
|
194
|
+
key = (name || id || call['index']).to_s.to_sym
|
|
195
|
+
[
|
|
196
|
+
key,
|
|
197
|
+
Legion::Extensions::Llm::ToolCall.new(id: id, name: name, arguments: function['arguments'] || '')
|
|
198
|
+
]
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
|
|
187
202
|
def extract_thinking_from_chunk(delta)
|
|
188
203
|
reasoning = delta['reasoning_content'] || delta['reasoning']
|
|
189
204
|
content = delta['content']
|
|
@@ -273,7 +288,7 @@ module Legion
|
|
|
273
288
|
end
|
|
274
289
|
|
|
275
290
|
def render_embedding_payload(text, model:, dimensions:)
|
|
276
|
-
{ model: model, input: text, dimensions: dimensions }.compact
|
|
291
|
+
{ model: model.respond_to?(:id) ? model.id : model, input: text, dimensions: dimensions }.compact
|
|
277
292
|
end
|
|
278
293
|
|
|
279
294
|
def parse_embedding_response(response, model:, text:)
|
|
@@ -5,6 +5,8 @@ module Legion
|
|
|
5
5
|
module Llm
|
|
6
6
|
# Assembles streaming responses from LLMs into complete messages.
|
|
7
7
|
class StreamAccumulator
|
|
8
|
+
include Legion::Logging::Helper
|
|
9
|
+
|
|
8
10
|
attr_reader :content, :model_id, :tool_calls
|
|
9
11
|
|
|
10
12
|
def initialize
|
|
@@ -77,13 +79,7 @@ module Legion
|
|
|
77
79
|
|
|
78
80
|
def tool_calls_from_stream
|
|
79
81
|
tool_calls.transform_values do |tc|
|
|
80
|
-
arguments =
|
|
81
|
-
Legion::JSON.parse(tc.arguments, symbolize_names: false)
|
|
82
|
-
elsif tc.arguments.is_a?(String)
|
|
83
|
-
{}
|
|
84
|
-
else
|
|
85
|
-
tc.arguments
|
|
86
|
-
end
|
|
82
|
+
arguments = parse_accumulated_arguments(tc.arguments)
|
|
87
83
|
|
|
88
84
|
ToolCall.new(
|
|
89
85
|
id: tc.id,
|
|
@@ -94,38 +90,59 @@ module Legion
|
|
|
94
90
|
end
|
|
95
91
|
end
|
|
96
92
|
|
|
97
|
-
def
|
|
93
|
+
def parse_accumulated_arguments(arguments)
|
|
94
|
+
return arguments unless arguments.is_a?(String)
|
|
95
|
+
return {} if arguments.empty?
|
|
96
|
+
|
|
97
|
+
Legion::JSON.parse(arguments, symbolize_names: false)
|
|
98
|
+
rescue Legion::JSON::ParseError => e
|
|
99
|
+
handle_exception(e, level: :warn, handled: true, operation: 'llm.stream.parse_tool_arguments')
|
|
100
|
+
{}
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def accumulate_tool_calls(new_tool_calls)
|
|
98
104
|
if Legion::Extensions::Llm.config.log_stream_debug
|
|
99
105
|
Legion::Extensions::Llm.logger.debug { "Accumulating tool calls: #{new_tool_calls}" }
|
|
100
106
|
end
|
|
101
107
|
new_tool_calls.each_value do |tool_call|
|
|
102
108
|
if tool_call.id
|
|
103
|
-
|
|
104
|
-
tool_call_arguments = tool_call.arguments
|
|
105
|
-
if tool_call_arguments.nil? || (tool_call_arguments.respond_to?(:empty?) && tool_call_arguments.empty?)
|
|
106
|
-
tool_call_arguments = +''
|
|
107
|
-
end
|
|
108
|
-
@tool_calls[tool_call.id] = ToolCall.new(
|
|
109
|
-
id: tool_call_id,
|
|
110
|
-
name: tool_call.name,
|
|
111
|
-
arguments: tool_call_arguments,
|
|
112
|
-
thought_signature: tool_call.thought_signature
|
|
113
|
-
)
|
|
114
|
-
@latest_tool_call_id = tool_call.id
|
|
109
|
+
start_tool_call(tool_call)
|
|
115
110
|
else
|
|
116
|
-
|
|
117
|
-
if existing
|
|
118
|
-
fragment = tool_call.arguments
|
|
119
|
-
fragment = '' if fragment.nil?
|
|
120
|
-
existing.arguments << fragment
|
|
121
|
-
if tool_call.thought_signature && existing.thought_signature.nil?
|
|
122
|
-
existing.thought_signature = tool_call.thought_signature
|
|
123
|
-
end
|
|
124
|
-
end
|
|
111
|
+
append_tool_call_fragment(tool_call)
|
|
125
112
|
end
|
|
126
113
|
end
|
|
127
114
|
end
|
|
128
115
|
|
|
116
|
+
def start_tool_call(tool_call)
|
|
117
|
+
@tool_calls[tool_call.id] = ToolCall.new(
|
|
118
|
+
id: tool_call.id.empty? ? SecureRandom.uuid : tool_call.id,
|
|
119
|
+
name: tool_call.name,
|
|
120
|
+
arguments: mutable_tool_arguments(tool_call.arguments),
|
|
121
|
+
thought_signature: tool_call.thought_signature
|
|
122
|
+
)
|
|
123
|
+
@latest_tool_call_id = tool_call.id
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def mutable_tool_arguments(arguments)
|
|
127
|
+
if arguments.nil? || (arguments.respond_to?(:empty?) && arguments.empty?)
|
|
128
|
+
+''
|
|
129
|
+
elsif arguments.is_a?(String)
|
|
130
|
+
+arguments
|
|
131
|
+
else
|
|
132
|
+
arguments
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def append_tool_call_fragment(tool_call)
|
|
137
|
+
existing = @tool_calls[@latest_tool_call_id]
|
|
138
|
+
return unless existing
|
|
139
|
+
|
|
140
|
+
existing.arguments << tool_call.arguments.to_s
|
|
141
|
+
return unless tool_call.thought_signature && existing.thought_signature.nil?
|
|
142
|
+
|
|
143
|
+
existing.thought_signature = tool_call.thought_signature
|
|
144
|
+
end
|
|
145
|
+
|
|
129
146
|
def find_tool_call(tool_call_id)
|
|
130
147
|
if tool_call_id.nil?
|
|
131
148
|
@tool_calls[@latest_tool_call]
|