lex-llm 0.4.3 → 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: b266813f29f9a144b2a57408f39fe98bc27b7a53e59b13871ca22c0dc8cf6127
4
- data.tar.gz: 80cb7a8866d4cd2b9c150dd4567f99aa09d12208f8acfa38df3dd578c7c93831
3
+ metadata.gz: b7b27c6f6a3d3166cdf483b997908532b2a511e57496fc13bd9c1d854d0daee7
4
+ data.tar.gz: b21fda8924c6e108905b46ab962f62e67095f2a801dda19e284b639c7152fdd2
5
5
  SHA512:
6
- metadata.gz: d43d28ab982b938f012a66000f73ee7cb4b9cae34ae31cbb6c11794d87845280ae919e2b91b81b594f76f6e11b95e9c57ff796c46d1ce595a74962b6d4a91800
7
- data.tar.gz: 1976f2adfd60d698e547e92b00f7d779ab28b5c75c975a7245bc58ecc94dbb0d81b767c76552b3d1cee24a53fdd8d2bb98d1e3cb204816e27963491326daee50
6
+ metadata.gz: cd6836a39da034186d4987b91066ecb4506c7dab624bdcc54c623657a5fa3f76a3a8645f4541593f5e9b32de364677542960bdacf1c8140d4605db1b5e58d79f
7
+ data.tar.gz: 163cc40607ab68e5014460e426558e4b6be9a3ccf3cf25ab7b5414edeed430a5e293a8a9f9cd9cd0ac6d636e07d806223142881233836bb50343f47b64a12628
data/CHANGELOG.md CHANGED
@@ -1,5 +1,24 @@
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
+
13
+ ## 0.4.5 - 2026-05-07
14
+
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.
16
+
17
+ ## 0.4.4 - 2026-05-07
18
+
19
+ - Fix `confirm_publish` to call `wait_for_confirms` with no arguments, matching bunny 3.1.0 API which removed the timeout parameter.
20
+ - Fix `prepare_publisher_confirms` to pass `confirm_timeout:` to `confirm_select` when `publish_confirm_timeout_ms` is set.
21
+
3
22
  ## 0.4.3 - 2026-05-06
4
23
 
5
24
  - Move provider-owned fleet responder execution into `lex-llm` so provider gems no longer depend on `legion-llm`.
@@ -57,7 +57,14 @@ module Legion
57
57
  return unless options[:publisher_confirm] == true
58
58
 
59
59
  confirm_channel = publish_channel(exchange_dest)
60
- confirm_channel.confirm_select if confirm_channel.respond_to?(:confirm_select)
60
+ return unless confirm_channel.respond_to?(:confirm_select)
61
+
62
+ timeout_ms = options[:publish_confirm_timeout_ms]
63
+ if timeout_ms
64
+ confirm_channel.confirm_select(confirm_timeout: timeout_ms.to_i)
65
+ else
66
+ confirm_channel.confirm_select
67
+ end
61
68
  end
62
69
 
63
70
  def publish_result(exchange_dest, options, return_state)
@@ -93,12 +100,7 @@ module Legion
93
100
  confirm_channel = publish_channel(exchange_dest)
94
101
  return :accepted unless confirm_channel.respond_to?(:wait_for_confirms)
95
102
 
96
- timeout = options[:publish_confirm_timeout_ms]
97
- confirmed = if timeout
98
- confirm_channel.wait_for_confirms(timeout.to_f / 1000.0)
99
- else
100
- confirm_channel.wait_for_confirms
101
- end
103
+ confirmed = confirm_channel.wait_for_confirms
102
104
  confirmed == false ? :nacked : :accepted
103
105
  rescue Timeout::Error => e
104
106
  handle_exception(e, level: :warn, handled: true, operation: 'llm.fleet.publish.confirm')
@@ -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:)
@@ -60,6 +60,16 @@ module Legion
60
60
  left_value.is_a?(Hash) && right_value.is_a?(Hash) ? deep_merge(left_value, right_value) : right_value
61
61
  end
62
62
  end
63
+
64
+ def infer_tier_from_endpoint(url)
65
+ return :direct if url.nil? || url.to_s.empty?
66
+
67
+ require 'uri'
68
+ host = URI.parse(url.to_s).host.to_s.downcase
69
+ %w[localhost 127.0.0.1 ::1 [::1]].include?(host) ? :local : :direct
70
+ rescue URI::InvalidURIError
71
+ :direct
72
+ end
63
73
  end
64
74
  end
65
75
  end
@@ -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.3'
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.3
4
+ version: 0.4.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - LegionIO