lex-llm-vllm 0.3.11 → 0.3.14

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: 66c6939650ded871419a3833d9d0a89e5c31a104f3acccbb065af86af17709cb
4
- data.tar.gz: e639e39a60ff4c03838100bd361048997fecfe38e92fdd9f1763809958a43f8e
3
+ metadata.gz: '0459c715b28893afc32019e6f2f831ee08824cd3a0224c17fa25164f5f14a9fc'
4
+ data.tar.gz: 72629182ec192e73316f29d1b491b471a1c8cfe6d87e348f2ce00a16f9df8a1b
5
5
  SHA512:
6
- metadata.gz: cf346327446a5e3c7b8666c8ea04269d7fea32b7bbe89b9a0c11b7578fe3ca1a8f948cf4fc639265bfcfa6f0a65bca7007f24ce90146f6234f1589fcb6fe5c31
7
- data.tar.gz: 1e324da7463bd1a63b14b5fde9b3f6030be0f8cdd933d802d350066beaf40fe8208998a298a848b00126bf5d1d68868b8384c2ed719fb863c825179aa8c92ced
6
+ metadata.gz: 60b334c314c6f29257ae7c9a073dfc034536f50f4c58e620137d49d88fb682d12f1abb5882ae792e7a99faffabd3cb01ba990f053f341d10b5478952f527e6bd
7
+ data.tar.gz: 3b602d7548c15a76068645d27da5a94619d7ca1954a2620c8291b2eb4af93384b2a4fe5f241b15487b3b8f7bb99a8294cd474ddac7f94ee1439d9e2d5f76ee08
data/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.3.14] - 2026-07-24
4
+
5
+ ### Fixed
6
+ - **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.
7
+ - **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.
8
+
9
+ ## [0.3.13] - 2026-07-14
10
+
11
+ ### Fixed
12
+ - **Streaming dead stop on Qwen with thinking enabled.** `empty_delta?` only checked `delta['reasoning_content']`, missing vLLM's `delta['reasoning']` field. When the final chunk carried `finish_reason: "stop"` alongside a `reasoning` delta, the translator incorrectly treated it as an empty delta and fired the done chunk early — the content phase never started, producing an empty streaming response. Now checks both `reasoning_content` and `reasoning`. Also fixed `wire_metadata` to read from either field for sync responses.
13
+
14
+ ## [0.3.12] - 2026-07-05
15
+
16
+ ### Changed
17
+ - Stop_reason mapping now uses the shared `Legion::Extensions::Llm::StopReasonMapping` mixin from lex-llm (>= 0.6.9) instead of a local `VLLM_STOP_REASON_MAP`. The local 3-entry map only recognized `tool_use` and silently fell back to `:end_turn` on the `tool_calls` finish_reason that OpenAI-compatible vLLM actually emits on tool turns — so vLLM tool completions were mis-reported as `end_turn`. The shared vocabulary maps both `tool_calls` and `tool_use` to `:tool_use` (plus `stop`/`end_turn`/`eos`, `length`/`max_tokens`, `stop_sequence`, `content_filter`), and is inherited by every provider so it no longer drifts per gem.
18
+
3
19
  ## [0.3.11] - 2026-06-20
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.0'
30
+ spec.add_dependency 'lex-llm', '>= 0.6.9'
31
31
  end
@@ -471,10 +471,14 @@ module Legion
471
471
 
472
472
  # Override: delegate SSE chunk parsing to the canonical translator.
473
473
  def build_chunk(data)
474
- canonical_chunk = translator.parse_chunk(data)
475
- return nil if canonical_chunk.nil?
474
+ result = translator.parse_chunk(data)
475
+ return nil if result.nil?
476
476
 
477
- to_legacy_chunk(canonical_chunk, data)
477
+ if result.is_a?(Array)
478
+ result.map { |c| to_legacy_chunk(c, data) }
479
+ else
480
+ to_legacy_chunk(result, data)
481
+ end
478
482
  end
479
483
 
480
484
  def parse_list_models_response(response, provider, capabilities)
@@ -2,6 +2,7 @@
2
2
 
3
3
  require 'legion/extensions/llm/canonical'
4
4
  require 'legion/extensions/llm/responses/thinking_extractor'
5
+ require 'legion/extensions/llm/stop_reason_mapping'
5
6
  require 'legion/json'
6
7
  require 'legion/logging'
7
8
 
@@ -25,13 +26,10 @@ module Legion
25
26
  # rubocop:disable Metrics/ClassLength, Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity -- translator implementation
26
27
  class Translator
27
28
  include Legion::Logging::Helper
29
+ # Shared stop_reason vocabulary (tool_calls/tool_use/stop/length/...).
30
+ # vLLM is OpenAI-compatible and needs no additions; inherits the common map.
31
+ include Legion::Extensions::Llm::StopReasonMapping
28
32
 
29
- # vLLM-specific stop_reason mapping (per conformance fixture stop_reason_matrix).
30
- VLLM_STOP_REASON_MAP = {
31
- 'stop' => :end_turn,
32
- 'tool_use' => :tool_use,
33
- 'length' => :max_tokens
34
- }.freeze
35
33
  FALLBACK_STOP_REASON = :end_turn
36
34
 
37
35
  # G18 parameter mapping: supported canonical params.
@@ -165,23 +163,43 @@ module Legion
165
163
  )
166
164
  end
167
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
+
168
169
  tool_calls = Array(delta['tool_calls'])
169
- 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
+ return build_tool_call_delta_chunk(tool_calls.first, request_id,
176
+ stop_reason: chunk_stop_reason, usage: chunk_usage)
177
+ end
170
178
 
171
179
  # Thinking delta from reasoning_content
172
180
  reasoning_content = delta['reasoning_content'] || delta['reasoning']
181
+ content = delta['content']
173
182
  unless reasoning_content.to_s.empty?
174
- return Canonical::Chunk.thinking_delta(
183
+ thinking_chunk = Canonical::Chunk.thinking_delta(
175
184
  delta: reasoning_content,
176
185
  request_id: request_id,
177
186
  block_index: delta.dig('content_block', 'index'),
178
- item_id: delta['content_block_start']&.dig('id')
187
+ item_id: delta['content_block_start']&.dig('id'),
188
+ stop_reason: content.to_s.empty? ? chunk_stop_reason : nil,
189
+ usage: content.to_s.empty? ? chunk_usage : nil
179
190
  )
191
+ return thinking_chunk if content.to_s.empty?
192
+
193
+ content_chunk = parse_text_delta_with_thinking(content, request_id, data,
194
+ stop_reason: chunk_stop_reason, usage: chunk_usage)
195
+ return [thinking_chunk, content_chunk]
180
196
  end
181
197
 
182
198
  # Text delta — check for embedded think tags
183
- content = delta['content']
184
- return parse_text_delta_with_thinking(content, request_id, data) unless content.to_s.empty?
199
+ unless content.to_s.empty?
200
+ return parse_text_delta_with_thinking(content, request_id, data,
201
+ stop_reason: chunk_stop_reason, usage: chunk_usage)
202
+ end
185
203
 
186
204
  nil
187
205
  rescue Legion::JSON::ParseError => e
@@ -200,7 +218,7 @@ module Legion
200
218
  tool_calls_as_text: true,
201
219
  forced_tool_choice: true,
202
220
  thinking_tags: %w[think thinking],
203
- stop_reason_map: VLLM_STOP_REASON_MAP,
221
+ stop_reason_map: stop_reason_map,
204
222
  streaming_token_usage: true
205
223
  }.freeze
206
224
  end
@@ -613,7 +631,8 @@ module Legion
613
631
 
614
632
  def wire_metadata(wire, message, _thinking_meta)
615
633
  meta = {}
616
- meta[:reasoning_content] = message['reasoning_content'] if message['reasoning_content']
634
+ reasoning = message['reasoning_content'] || message['reasoning']
635
+ meta[:reasoning_content] = reasoning if reasoning
617
636
  raw_usage = wire['usage']
618
637
  if raw_usage.is_a?(Hash) && raw_usage['completion_tokens_details']
619
638
  meta[:completion_tokens_details] = raw_usage['completion_tokens_details']
@@ -636,7 +655,7 @@ module Legion
636
655
  # fragments carry id: nil and a raw partial-JSON arguments string.
637
656
  # The StreamAccumulator keys off a nil id to append fragments to the
638
657
  # current tool call, so the id must NOT be synthesized here.
639
- def build_tool_call_delta_chunk(first_call, request_id)
658
+ def build_tool_call_delta_chunk(first_call, request_id, stop_reason: nil, usage: nil)
640
659
  function = first_call.fetch('function', {})
641
660
 
642
661
  tc = Canonical::ToolCall.new(
@@ -650,14 +669,17 @@ module Legion
650
669
  Canonical::Chunk.tool_call_delta(
651
670
  tool_call: tc,
652
671
  request_id: request_id,
653
- block_index: first_call['index']
672
+ block_index: first_call['index'],
673
+ stop_reason: stop_reason,
674
+ usage: usage
654
675
  )
655
676
  end
656
677
 
657
678
  def empty_delta?(delta)
658
679
  (delta['content'].nil? || delta['content'].to_s.empty?) &&
659
680
  (delta['tool_calls'].nil? || Array(delta['tool_calls']).empty?) &&
660
- (delta['reasoning_content'].nil? || delta['reasoning_content'].to_s.empty?)
681
+ (delta['reasoning_content'].nil? || delta['reasoning_content'].to_s.empty?) &&
682
+ (delta['reasoning'].nil? || delta['reasoning'].to_s.empty?)
661
683
  end
662
684
 
663
685
  # Per-chunk think-tag extraction is structurally impossible while streaming:
@@ -667,11 +689,13 @@ module Legion
667
689
  # (Previously called ThinkingExtractor.extract_from_content, which is
668
690
  # private_class_method in lex-llm >= 0.5.0 and raised NoMethodError on
669
691
  # every streamed text delta, silently killing all vLLM streaming.)
670
- def parse_text_delta_with_thinking(content, request_id, data)
692
+ def parse_text_delta_with_thinking(content, request_id, data, stop_reason: nil, usage: nil)
671
693
  Canonical::Chunk.text_delta(
672
694
  delta: content,
673
695
  request_id: request_id,
674
- index: data['index']
696
+ index: data['index'],
697
+ stop_reason: stop_reason,
698
+ usage: usage
675
699
  )
676
700
  end
677
701
 
@@ -694,7 +718,7 @@ module Legion
694
718
  def map_stop_reason(raw)
695
719
  return FALLBACK_STOP_REASON if raw.nil? || raw.to_s.empty?
696
720
 
697
- VLLM_STOP_REASON_MAP.fetch(raw.to_s, FALLBACK_STOP_REASON)
721
+ stop_reason_lookup(raw) || FALLBACK_STOP_REASON
698
722
  end
699
723
 
700
724
  # ── JSON helpers ──
@@ -4,7 +4,7 @@ module Legion
4
4
  module Extensions
5
5
  module Llm
6
6
  module Vllm
7
- VERSION = '0.3.11'
7
+ VERSION = '0.3.14'
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.11
4
+ version: 0.3.14
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.0
74
+ version: 0.6.9
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.0
81
+ version: 0.6.9
82
82
  description: vLLM provider integration for the LegionIO LLM routing framework.
83
83
  email:
84
84
  - matthewdiverson@gmail.com