lex-llm-vllm 0.3.13 → 0.3.16

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: a62d4fc4e5b1020ccf9a4bcb5ed9c03096923075c7111357648cc1b360dd804b
4
- data.tar.gz: 1cb2fe7f84f9fcd44b0c04407e62310e5fa4fad59bdfd2bc65889bfa8b9bd213
3
+ metadata.gz: 8a53b31d5fe4efcc8b9451b0934c52bcbf71a4af70e493cfe82d7ba75f40ce68
4
+ data.tar.gz: a567a34e2f14e2cf0497b98926527fa9895040981e19e0340006bdf143640ef4
5
5
  SHA512:
6
- metadata.gz: 0a68362c7660f1474b943d8d67948e71b3d2b23840b39835937839bf5d2542d0baaa05b569764f77e5268cde1848dfca20e2e16274de6f112756a8eaa1b79ccc
7
- data.tar.gz: de8011c0d1468c8d3f35215f01b7ee3ffa182d47c56dc89ac5fd983dcbf6f9de52ef167fc04d7514b984423081b65f4a8a6f5b463dc6d4372bbcbcdd90d7aa75
6
+ metadata.gz: 0a64470ff262c97f5cd99b02c066cf24ffb2872ae7dcde10597e9869f3cfc2be89ffc4d07bf9bb43bd83b83d161cfc8d987f25121f87e9cf4f8770abe66fb358
7
+ data.tar.gz: cd4b89c6a0297f70232cf882480b24f903517caec5ed9f7129a61e0dc0d65bb599126cc1fa7cceccd388c8411035db74d737ff15351d7c3ad4c9433823fd5e37
data/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.3.16] - 2026-07-31
4
+
5
+ ### Fixed
6
+ - **`parse_chunk` now handles multiple tool_calls batched in a single SSE delta.** vLLM can batch several parallel tool_calls into one `choices[0].delta.tool_calls` array. Previously only `tool_calls.first` was processed — the 2nd+ tool calls were silently dropped, causing parallel tool invocations to lose calls. Now returns an array of `tool_call_delta` chunks (one per tool_call), which the streaming handler already iterates (per lex-llm 0.6.13). Single tool_call deltas still return a single chunk (not wrapped in an array) for backward compatibility.
7
+
8
+ ## [0.3.15] - 2026-07-31
9
+
10
+ ### Fixed
11
+ - **`to_legacy_chunk` now propagates `stop_reason` from canonical chunks.** The canonical translator correctly parsed vLLM's `finish_reason` into `Canonical::Chunk.stop_reason`, but `to_legacy_chunk` never passed it into the legacy `Legion::Extensions::Llm::Chunk` constructor. The `StreamAccumulator` already reads `chunk.stop_reason` (line 40), so the field was silently nil on every streamed chunk — downstream always defaulted to `:end_turn` regardless of what the provider actually said. Truncated responses (`:max_tokens`) and content-filtered responses (`:content_filter`) were indistinguishable from clean completions. Now the real finish_reason flows through: canonical chunk → legacy chunk → accumulator → assembled Message.
12
+
13
+ ## [0.3.14] - 2026-07-24
14
+
15
+ ### Fixed
16
+ - **Translator passes `finish_reason` and `usage` through on all streaming chunk types.** vLLM sends `finish_reason` on the same chunk as the last content token. Previously the translator dropped it because the early-return pattern only emitted finish_reason when the delta was empty. Now `text_delta`, `tool_call_delta`, and `thinking_delta` chunks carry `stop_reason` and `usage` when present on the SSE event, enabling the accumulator to capture the real provider signal.
17
+ - **Translator returns both thinking and content when vLLM sends both on the same SSE chunk.** vLLM emits `reasoning` and `content` simultaneously on the boundary between thinking and visible output. Previously the first-branch-wins pattern dropped content when reasoning was present, causing visible characters to disappear from responses (e.g. numbered list items losing their leading digit). `parse_chunk` now returns an array `[thinking_delta, text_delta]` when both fields are present, and the streaming handler iterates them.
18
+
3
19
  ## [0.3.13] - 2026-07-14
4
20
 
5
21
  ### Fixed
data/lex-llm-vllm.gemspec CHANGED
@@ -27,5 +27,5 @@ Gem::Specification.new do |spec|
27
27
  spec.add_dependency 'legion-logging', '>= 1.3.2'
28
28
  spec.add_dependency 'legion-settings', '>= 1.3.14'
29
29
  spec.add_dependency 'legion-transport', '>= 1.4.14'
30
- spec.add_dependency 'lex-llm', '>= 0.6.9'
30
+ spec.add_dependency 'lex-llm', '>= 0.6.13'
31
31
  end
@@ -363,6 +363,7 @@ module Legion
363
363
  thinking: thinking,
364
364
  input_tokens: usage.respond_to?(:input_tokens) ? usage.input_tokens : nil,
365
365
  output_tokens: usage.respond_to?(:output_tokens) ? usage.output_tokens : nil,
366
+ stop_reason: canonical.stop_reason,
366
367
  raw: raw_data
367
368
  )
368
369
  end
@@ -471,10 +472,14 @@ module Legion
471
472
 
472
473
  # Override: delegate SSE chunk parsing to the canonical translator.
473
474
  def build_chunk(data)
474
- canonical_chunk = translator.parse_chunk(data)
475
- return nil if canonical_chunk.nil?
475
+ result = translator.parse_chunk(data)
476
+ return nil if result.nil?
476
477
 
477
- to_legacy_chunk(canonical_chunk, data)
478
+ if result.is_a?(Array)
479
+ result.map { |c| to_legacy_chunk(c, data) }
480
+ else
481
+ to_legacy_chunk(result, data)
482
+ end
478
483
  end
479
484
 
480
485
  def parse_list_models_response(response, provider, capabilities)
@@ -163,23 +163,46 @@ module Legion
163
163
  )
164
164
  end
165
165
 
166
+ chunk_stop_reason = finish_reason ? map_stop_reason(finish_reason) : nil
167
+ chunk_usage = finish_reason && data['usage'] ? Canonical::Usage.from_hash(data['usage']) : nil
168
+
166
169
  tool_calls = Array(delta['tool_calls'])
167
- return build_tool_call_delta_chunk(tool_calls.first, request_id) unless tool_calls.empty?
170
+ unless tool_calls.empty?
171
+ if delta['content'] && !delta['content'].to_s.empty?
172
+ log.debug '[vllm][translator] action=content_dropped_with_tool_call ' \
173
+ "content=#{delta['content'][0, 100].inspect} request_id=#{request_id}"
174
+ end
175
+ chunks = tool_calls.map do |tc|
176
+ build_tool_call_delta_chunk(tc, request_id,
177
+ stop_reason: chunk_stop_reason, usage: chunk_usage)
178
+ end
179
+ return chunks.size == 1 ? chunks.first : chunks
180
+ end
168
181
 
169
182
  # Thinking delta from reasoning_content
170
183
  reasoning_content = delta['reasoning_content'] || delta['reasoning']
184
+ content = delta['content']
171
185
  unless reasoning_content.to_s.empty?
172
- return Canonical::Chunk.thinking_delta(
186
+ thinking_chunk = Canonical::Chunk.thinking_delta(
173
187
  delta: reasoning_content,
174
188
  request_id: request_id,
175
189
  block_index: delta.dig('content_block', 'index'),
176
- item_id: delta['content_block_start']&.dig('id')
190
+ item_id: delta['content_block_start']&.dig('id'),
191
+ stop_reason: content.to_s.empty? ? chunk_stop_reason : nil,
192
+ usage: content.to_s.empty? ? chunk_usage : nil
177
193
  )
194
+ return thinking_chunk if content.to_s.empty?
195
+
196
+ content_chunk = parse_text_delta_with_thinking(content, request_id, data,
197
+ stop_reason: chunk_stop_reason, usage: chunk_usage)
198
+ return [thinking_chunk, content_chunk]
178
199
  end
179
200
 
180
201
  # Text delta — check for embedded think tags
181
- content = delta['content']
182
- return parse_text_delta_with_thinking(content, request_id, data) unless content.to_s.empty?
202
+ unless content.to_s.empty?
203
+ return parse_text_delta_with_thinking(content, request_id, data,
204
+ stop_reason: chunk_stop_reason, usage: chunk_usage)
205
+ end
183
206
 
184
207
  nil
185
208
  rescue Legion::JSON::ParseError => e
@@ -635,7 +658,7 @@ module Legion
635
658
  # fragments carry id: nil and a raw partial-JSON arguments string.
636
659
  # The StreamAccumulator keys off a nil id to append fragments to the
637
660
  # current tool call, so the id must NOT be synthesized here.
638
- def build_tool_call_delta_chunk(first_call, request_id)
661
+ def build_tool_call_delta_chunk(first_call, request_id, stop_reason: nil, usage: nil)
639
662
  function = first_call.fetch('function', {})
640
663
 
641
664
  tc = Canonical::ToolCall.new(
@@ -649,7 +672,9 @@ module Legion
649
672
  Canonical::Chunk.tool_call_delta(
650
673
  tool_call: tc,
651
674
  request_id: request_id,
652
- block_index: first_call['index']
675
+ block_index: first_call['index'],
676
+ stop_reason: stop_reason,
677
+ usage: usage
653
678
  )
654
679
  end
655
680
 
@@ -667,11 +692,13 @@ module Legion
667
692
  # (Previously called ThinkingExtractor.extract_from_content, which is
668
693
  # private_class_method in lex-llm >= 0.5.0 and raised NoMethodError on
669
694
  # every streamed text delta, silently killing all vLLM streaming.)
670
- def parse_text_delta_with_thinking(content, request_id, data)
695
+ def parse_text_delta_with_thinking(content, request_id, data, stop_reason: nil, usage: nil)
671
696
  Canonical::Chunk.text_delta(
672
697
  delta: content,
673
698
  request_id: request_id,
674
- index: data['index']
699
+ index: data['index'],
700
+ stop_reason: stop_reason,
701
+ usage: usage
675
702
  )
676
703
  end
677
704
 
@@ -4,7 +4,7 @@ module Legion
4
4
  module Extensions
5
5
  module Llm
6
6
  module Vllm
7
- VERSION = '0.3.13'
7
+ VERSION = '0.3.16'
8
8
  end
9
9
  end
10
10
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lex-llm-vllm
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.13
4
+ version: 0.3.16
5
5
  platform: ruby
6
6
  authors:
7
7
  - LegionIO
@@ -71,14 +71,14 @@ dependencies:
71
71
  requirements:
72
72
  - - ">="
73
73
  - !ruby/object:Gem::Version
74
- version: 0.6.9
74
+ version: 0.6.13
75
75
  type: :runtime
76
76
  prerelease: false
77
77
  version_requirements: !ruby/object:Gem::Requirement
78
78
  requirements:
79
79
  - - ">="
80
80
  - !ruby/object:Gem::Version
81
- version: 0.6.9
81
+ version: 0.6.13
82
82
  description: vLLM provider integration for the LegionIO LLM routing framework.
83
83
  email:
84
84
  - matthewdiverson@gmail.com