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:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: '0459c715b28893afc32019e6f2f831ee08824cd3a0224c17fa25164f5f14a9fc'
|
|
4
|
+
data.tar.gz: 72629182ec192e73316f29d1b491b471a1c8cfe6d87e348f2ce00a16f9df8a1b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
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
|
-
|
|
475
|
-
return nil if
|
|
474
|
+
result = translator.parse_chunk(data)
|
|
475
|
+
return nil if result.nil?
|
|
476
476
|
|
|
477
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
184
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
721
|
+
stop_reason_lookup(raw) || FALLBACK_STOP_REASON
|
|
698
722
|
end
|
|
699
723
|
|
|
700
724
|
# ── JSON helpers ──
|
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.
|
|
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.
|
|
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.
|
|
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
|