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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 33dcc097174b40983896f2c6676ca20e055f262e9f85d27df6a4b1e6f27e9632
4
- data.tar.gz: 0abfa31c8b8d3d034f0c0d87e865bc80c5d6f8d5d5232a94ce614da0403b6868
3
+ metadata.gz: b7b27c6f6a3d3166cdf483b997908532b2a511e57496fc13bd9c1d854d0daee7
4
+ data.tar.gz: b21fda8924c6e108905b46ab962f62e67095f2a801dda19e284b639c7152fdd2
5
5
  SHA512:
6
- metadata.gz: 27dc2378bfb280e77267f15f2ef6945d2453f1ac74c0437815e5c474e1310c951f3b4d80167f182f4465003c1699d97e200f7f1a8f2d479defe94a90dce35ddd
7
- data.tar.gz: edf02a16a1a1373ac3dc97807af981314bd6fefd4b175bd60b6418e2526b8701f3726b657a6c4b0fb80a64765c2629614601ffe7abef847a6015b1a04ef91275
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: parse_tool_calls(delta['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 = if tc.arguments.is_a?(String) && !tc.arguments.empty?
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 accumulate_tool_calls(new_tool_calls) # rubocop:disable Metrics/PerceivedComplexity
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
- tool_call_id = tool_call.id.empty? ? SecureRandom.uuid : tool_call.id
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
- existing = @tool_calls[@latest_tool_call_id]
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]
@@ -3,7 +3,7 @@
3
3
  module Legion
4
4
  module Extensions
5
5
  module Llm
6
- VERSION = '0.4.5'
6
+ VERSION = '0.4.7'
7
7
  end
8
8
  end
9
9
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lex-llm
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.5
4
+ version: 0.4.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - LegionIO