Skip to content

Commit ae85d1b

Browse files
committed
fix: emit tool_call_end events in OpenAI handler when streaming ends
The OpenAiHandler was not emitting tool_call_end events when the API stream ended with finish_reason === 'tool_calls'. This could cause the extension to appear stuck waiting for more stream data. Changes: - Added tracking of active tool call IDs in createMessage() and handleStreamResponse() - Emit tool_call_end events when finish_reason === 'tool_calls' - Added test coverage for both regular and O3 family models Closes: #10275 Linear: ROO-269
1 parent f462eeb commit ae85d1b

File tree

2 files changed

+37
-0
lines changed

2 files changed

+37
-0
lines changed

src/api/providers/__tests__/openai.spec.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,10 @@ describe("OpenAiHandler", () => {
295295
name: undefined,
296296
arguments: '"value"}',
297297
})
298+
299+
// Verify tool_call_end event is emitted when finish_reason is "tool_calls"
300+
const toolCallEndChunks = chunks.filter((chunk) => chunk.type === "tool_call_end")
301+
expect(toolCallEndChunks).toHaveLength(1)
298302
})
299303

300304
it("should yield tool calls even when finish_reason is not set (fallback behavior)", async () => {
@@ -855,6 +859,10 @@ describe("OpenAiHandler", () => {
855859
name: undefined,
856860
arguments: "{}",
857861
})
862+
863+
// Verify tool_call_end event is emitted when finish_reason is "tool_calls"
864+
const toolCallEndChunks = chunks.filter((chunk) => chunk.type === "tool_call_end")
865+
expect(toolCallEndChunks).toHaveLength(1)
858866
})
859867

860868
it("should yield tool calls for O3 model even when finish_reason is not set (fallback behavior)", async () => {

src/api/providers/openai.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,9 +194,11 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl
194194
)
195195

196196
let lastUsage
197+
const activeToolCallIds = new Set<string>()
197198

198199
for await (const chunk of stream) {
199200
const delta = chunk.choices?.[0]?.delta ?? {}
201+
const finishReason = chunk.choices?.[0]?.finish_reason
200202

201203
if (delta.content) {
202204
for (const chunk of matcher.update(delta.content)) {
@@ -213,6 +215,9 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl
213215

214216
if (delta.tool_calls) {
215217
for (const toolCall of delta.tool_calls) {
218+
if (toolCall.id) {
219+
activeToolCallIds.add(toolCall.id)
220+
}
216221
yield {
217222
type: "tool_call_partial",
218223
index: toolCall.index,
@@ -223,6 +228,15 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl
223228
}
224229
}
225230

231+
// Emit tool_call_end events when finish_reason is "tool_calls"
232+
// This ensures tool calls are finalized even if the stream doesn't properly close
233+
if (finishReason === "tool_calls" && activeToolCallIds.size > 0) {
234+
for (const id of activeToolCallIds) {
235+
yield { type: "tool_call_end", id }
236+
}
237+
activeToolCallIds.clear()
238+
}
239+
226240
if (chunk.usage) {
227241
lastUsage = chunk.usage
228242
}
@@ -443,8 +457,11 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl
443457
}
444458

445459
private async *handleStreamResponse(stream: AsyncIterable<OpenAI.Chat.Completions.ChatCompletionChunk>): ApiStream {
460+
const activeToolCallIds = new Set<string>()
461+
446462
for await (const chunk of stream) {
447463
const delta = chunk.choices?.[0]?.delta
464+
const finishReason = chunk.choices?.[0]?.finish_reason
448465

449466
if (delta) {
450467
if (delta.content) {
@@ -457,6 +474,9 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl
457474
// Emit raw tool call chunks - NativeToolCallParser handles state management
458475
if (delta.tool_calls) {
459476
for (const toolCall of delta.tool_calls) {
477+
if (toolCall.id) {
478+
activeToolCallIds.add(toolCall.id)
479+
}
460480
yield {
461481
type: "tool_call_partial",
462482
index: toolCall.index,
@@ -468,6 +488,15 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl
468488
}
469489
}
470490

491+
// Emit tool_call_end events when finish_reason is "tool_calls"
492+
// This ensures tool calls are finalized even if the stream doesn't properly close
493+
if (finishReason === "tool_calls" && activeToolCallIds.size > 0) {
494+
for (const id of activeToolCallIds) {
495+
yield { type: "tool_call_end", id }
496+
}
497+
activeToolCallIds.clear()
498+
}
499+
471500
if (chunk.usage) {
472501
yield {
473502
type: "usage",

0 commit comments

Comments
 (0)