lex-llm 0.6.14 → 0.6.15
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: 07f6e605230a187e17667abdf947db738dab8b823783cde124766278159c49f1
|
|
4
|
+
data.tar.gz: e1858e04a213ed78175c8647c74f05a5c300db4a8a54caadaba523cb3a1e71ef
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 10216387247f307ee05fe4e06a199cbf6cc762f6869636b16043adb18e1fdae355127f99e433e67614580e3c85b3d18b9d6199291d2703167184ee93e8810745
|
|
7
|
+
data.tar.gz: f010c914eddd807fc13593692a6fc20353ea1c12487838eed859493088b908a64a6459ee1866d5ffaaa9e4b3924e521b8ed3e79fce9e94fe27364283fb7be62e
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.6.15 - 2026-07-31
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
- **Remove dead `find_tool_call` method from StreamAccumulator.** Zero callers anywhere in lex-llm (confirmed via grep). The method referenced an undefined ivar `@latest_tool_call` (the actual ivar is `@latest_tool_call_id`) — it was broken dead code that could only ever return nil.
|
|
7
|
+
- **StreamAccumulator no longer drops tool_call opening fragments that arrive with `id: nil`.** When a tool_call chunk has no `id` but carries a `name` (indicating it's an opening fragment, not a continuation), the accumulator now generates a UUID and starts the call instead of silently dropping it via `append_tool_call_fragment`. This is defensive hardening — normal OpenAI-compatible streaming always assigns an id to the opening fragment, but non-conforming backends or edge cases could produce id-less openers. Existing id-based correlation is unchanged.
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- **Contract specs asserting streaming `stop_reason` propagates through the StreamAccumulator.** Three regression specs encoding the accumulator's stop_reason behavior: single stop_reason captured from a chunk and propagated to the assembled Message, last-wins semantics when multiple chunks carry non-nil stop_reason, and nil-when-absent (no chunks carry stop_reason). This is the lex-llm half of the cross-gem streaming finish_reason fix — paired with lex-llm-vllm v0.3.15 (#16) which wires `stop_reason: canonical.stop_reason` through `to_legacy_chunk`.
|
|
11
|
+
|
|
3
12
|
## 0.6.14 - 2026-07-31
|
|
4
13
|
|
|
5
14
|
### Added
|
|
@@ -149,6 +149,8 @@ module Legion
|
|
|
149
149
|
new_tool_calls.each_value do |tool_call|
|
|
150
150
|
if tool_call.id
|
|
151
151
|
start_tool_call(tool_call)
|
|
152
|
+
elsif tool_call.name && @latest_tool_call_id.nil?
|
|
153
|
+
start_tool_call_without_id(tool_call)
|
|
152
154
|
else
|
|
153
155
|
append_tool_call_fragment(tool_call)
|
|
154
156
|
end
|
|
@@ -175,6 +177,17 @@ module Legion
|
|
|
175
177
|
end
|
|
176
178
|
end
|
|
177
179
|
|
|
180
|
+
def start_tool_call_without_id(tool_call)
|
|
181
|
+
generated_id = SecureRandom.uuid
|
|
182
|
+
@tool_calls[generated_id] = ToolCall.new(
|
|
183
|
+
id: generated_id,
|
|
184
|
+
name: tool_call.name,
|
|
185
|
+
arguments: mutable_tool_arguments(tool_call.arguments),
|
|
186
|
+
thought_signature: tool_call.thought_signature
|
|
187
|
+
)
|
|
188
|
+
@latest_tool_call_id = generated_id
|
|
189
|
+
end
|
|
190
|
+
|
|
178
191
|
def append_tool_call_fragment(tool_call)
|
|
179
192
|
existing = @tool_calls[@latest_tool_call_id]
|
|
180
193
|
return unless existing
|
|
@@ -185,15 +198,6 @@ module Legion
|
|
|
185
198
|
existing.thought_signature = tool_call.thought_signature
|
|
186
199
|
end
|
|
187
200
|
|
|
188
|
-
def find_tool_call(tool_call_id)
|
|
189
|
-
if tool_call_id.nil?
|
|
190
|
-
@tool_calls[@latest_tool_call]
|
|
191
|
-
else
|
|
192
|
-
@latest_tool_call_id = tool_call_id
|
|
193
|
-
@tool_calls[tool_call_id]
|
|
194
|
-
end
|
|
195
|
-
end
|
|
196
|
-
|
|
197
201
|
def count_tokens(chunk)
|
|
198
202
|
@input_tokens = chunk.input_tokens if chunk.input_tokens
|
|
199
203
|
@output_tokens = chunk.output_tokens if chunk.output_tokens
|
|
@@ -99,6 +99,62 @@ RSpec.describe Legion::Extensions::Llm::StreamAccumulator do
|
|
|
99
99
|
expect(message.content).to eq(expected)
|
|
100
100
|
expect(message.thinking).to be_nil
|
|
101
101
|
end
|
|
102
|
+
|
|
103
|
+
it 'captures stop_reason from a chunk and propagates it to the assembled Message' do
|
|
104
|
+
accumulator = described_class.new
|
|
105
|
+
chunk = Legion::Extensions::Llm::Chunk.new(
|
|
106
|
+
role: :assistant, content: 'partial', model_id: 'm', stop_reason: :max_tokens
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
accumulator.add(chunk)
|
|
110
|
+
expect(accumulator.stop_reason).to eq(:max_tokens)
|
|
111
|
+
|
|
112
|
+
message = accumulator.to_message(nil)
|
|
113
|
+
expect(message.stop_reason).to eq(:max_tokens)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
it 'uses the last non-nil stop_reason when multiple chunks carry one' do
|
|
117
|
+
accumulator = described_class.new
|
|
118
|
+
accumulator.add(Legion::Extensions::Llm::Chunk.new(
|
|
119
|
+
role: :assistant, content: 'hello', model_id: 'm', stop_reason: :end_turn
|
|
120
|
+
))
|
|
121
|
+
accumulator.add(Legion::Extensions::Llm::Chunk.new(
|
|
122
|
+
role: :assistant, content: nil, model_id: 'm', stop_reason: :max_tokens
|
|
123
|
+
))
|
|
124
|
+
|
|
125
|
+
expect(accumulator.stop_reason).to eq(:max_tokens)
|
|
126
|
+
expect(accumulator.to_message(nil).stop_reason).to eq(:max_tokens)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
it 'leaves stop_reason nil when no chunk carries one' do
|
|
130
|
+
accumulator = described_class.new
|
|
131
|
+
accumulator.add(Legion::Extensions::Llm::Chunk.new(role: :assistant, content: 'hi', model_id: 'm'))
|
|
132
|
+
|
|
133
|
+
expect(accumulator.stop_reason).to be_nil
|
|
134
|
+
expect(accumulator.to_message(nil).stop_reason).to be_nil
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
it 'starts a tool call from an opening fragment with id nil but name present' do
|
|
138
|
+
accumulator = described_class.new
|
|
139
|
+
tool_call = Legion::Extensions::Llm::ToolCall.new(id: nil, name: 'weather', arguments: '')
|
|
140
|
+
chunk = Legion::Extensions::Llm::Chunk.new(
|
|
141
|
+
role: :assistant, content: nil, tool_calls: { weather: tool_call }
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
accumulator.add(chunk)
|
|
145
|
+
|
|
146
|
+
frag_call = Legion::Extensions::Llm::ToolCall.new(id: nil, name: nil, arguments: '{"city":"SF"}')
|
|
147
|
+
frag_chunk = Legion::Extensions::Llm::Chunk.new(
|
|
148
|
+
role: :assistant, content: nil, tool_calls: { fragment: frag_call }
|
|
149
|
+
)
|
|
150
|
+
accumulator.add(frag_chunk)
|
|
151
|
+
|
|
152
|
+
message = accumulator.to_message(nil)
|
|
153
|
+
expect(message.tool_calls.size).to eq(1)
|
|
154
|
+
tc = message.tool_calls.values.first
|
|
155
|
+
expect(tc.name).to eq('weather')
|
|
156
|
+
expect(tc.arguments).to eq({ 'city' => 'SF' })
|
|
157
|
+
end
|
|
102
158
|
end
|
|
103
159
|
|
|
104
160
|
describe '#filtered_chunk' do
|