diff --git a/src/api/providers/__tests__/openai.spec.ts b/src/api/providers/__tests__/openai.spec.ts index 72d219e02a..3e18f03a4c 100644 --- a/src/api/providers/__tests__/openai.spec.ts +++ b/src/api/providers/__tests__/openai.spec.ts @@ -6,6 +6,7 @@ import { Anthropic } from "@anthropic-ai/sdk" import OpenAI from "openai" import { openAiModelInfoSaneDefaults, DEEP_SEEK_DEFAULT_TEMPERATURE } from "@roo-code/types" import { Package } from "../../../shared/package" +import { asyncStreamFrom, collectStream } from "../../../test-utils/stream" import axios from "axios" vitest.mock("../utils/timeout-config", () => ({ @@ -43,32 +44,30 @@ vitest.mock("openai", () => { } } - return { - [Symbol.asyncIterator]: async function* () { - yield { - choices: [ - { - delta: { content: "Test response" }, - index: 0, - }, - ], - usage: null, - } - yield { - choices: [ - { - delta: {}, - index: 0, - }, - ], - usage: { - prompt_tokens: 10, - completion_tokens: 5, - total_tokens: 15, + return asyncStreamFrom([ + { + choices: [ + { + delta: { content: "Test response" }, + index: 0, }, - } + ], + usage: null, }, - } + { + choices: [ + { + delta: {}, + index: 0, + }, + ], + usage: { + prompt_tokens: 10, + completion_tokens: 5, + total_tokens: 15, + }, + }, + ]) }), }, }, @@ -149,10 +148,7 @@ describe("OpenAiHandler", () => { }) const stream = handler.createMessage(systemPrompt, messages) - const chunks: any[] = [] - for await (const chunk of stream) { - chunks.push(chunk) - } + const chunks = await collectStream(stream) expect(chunks.length).toBeGreaterThan(0) const textChunk = chunks.find((chunk) => chunk.type === "text") @@ -199,10 +195,7 @@ describe("OpenAiHandler", () => { }) const stream = handler.createMessage(systemPrompt, messages) - const chunks: any[] = [] - for await (const chunk of stream) { - chunks.push(chunk) - } + const chunks = await collectStream(stream) const toolCallChunks = chunks.filter((chunk) => chunk.type === "tool_call") expect(toolCallChunks).toHaveLength(1) @@ -216,10 +209,7 @@ describe("OpenAiHandler", () => { it("should handle streaming responses", async () => { const stream = handler.createMessage(systemPrompt, messages) - const chunks: any[] = [] - for await (const chunk of stream) { - chunks.push(chunk) - } + const chunks = await collectStream(stream) expect(chunks.length).toBeGreaterThan(0) const textChunks = chunks.filter((chunk) => chunk.type === "text") @@ -228,48 +218,42 @@ describe("OpenAiHandler", () => { }) it("streams reasoning chunks from delta.reasoning_content", async () => { - mockCreate.mockImplementationOnce(async () => ({ - [Symbol.asyncIterator]: async function* () { - yield { choices: [{ delta: { reasoning_content: "thinking..." }, index: 0 }] } - yield { choices: [{ delta: { content: "answer" }, index: 0 }] } - yield { + mockCreate.mockImplementationOnce(async () => + asyncStreamFrom([ + { choices: [{ delta: { reasoning_content: "thinking..." }, index: 0 }] }, + { choices: [{ delta: { content: "answer" }, index: 0 }] }, + { choices: [{ delta: {}, index: 0 }], usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 }, - } - }, - })) + }, + ]), + ) - const chunks: any[] = [] - for await (const chunk of handler.createMessage(systemPrompt, messages)) { - chunks.push(chunk) - } + const chunks = await collectStream(handler.createMessage(systemPrompt, messages)) expect(chunks).toContainEqual({ type: "reasoning", text: "thinking..." }) }) it("falls back to delta.reasoning when reasoning_content is absent", async () => { - mockCreate.mockImplementationOnce(async () => ({ - [Symbol.asyncIterator]: async function* () { - yield { choices: [{ delta: { reasoning: "router-style thought" }, index: 0 }] } - yield { + mockCreate.mockImplementationOnce(async () => + asyncStreamFrom([ + { choices: [{ delta: { reasoning: "router-style thought" }, index: 0 }] }, + { choices: [{ delta: {}, index: 0 }], usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 }, - } - }, - })) + }, + ]), + ) - const chunks: any[] = [] - for await (const chunk of handler.createMessage(systemPrompt, messages)) { - chunks.push(chunk) - } + const chunks = await collectStream(handler.createMessage(systemPrompt, messages)) expect(chunks).toContainEqual({ type: "reasoning", text: "router-style thought" }) }) it("prefers delta.reasoning_content over delta.reasoning when both are present", async () => { - mockCreate.mockImplementationOnce(async () => ({ - [Symbol.asyncIterator]: async function* () { - yield { + mockCreate.mockImplementationOnce(async () => + asyncStreamFrom([ + { choices: [ { delta: { @@ -279,19 +263,15 @@ describe("OpenAiHandler", () => { index: 0, }, ], - } - yield { + }, + { choices: [{ delta: {}, index: 0 }], usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 }, - } - }, - })) - - const chunks: any[] = [] + }, + ]), + ) - for await (const chunk of handler.createMessage(systemPrompt, messages)) { - chunks.push(chunk) - } + const chunks = await collectStream(handler.createMessage(systemPrompt, messages)) const reasoningChunks = chunks.filter((chunk) => chunk.type === "reasoning") @@ -299,54 +279,49 @@ describe("OpenAiHandler", () => { }) it("should handle tool calls in streaming responses", async () => { - mockCreate.mockImplementation(async (options) => { - return { - [Symbol.asyncIterator]: async function* () { - yield { - choices: [ - { - delta: { - tool_calls: [ - { - index: 0, - id: "call_1", - function: { name: "test_tool", arguments: "" }, - }, - ], - }, - finish_reason: null, + mockCreate.mockImplementation(async (options) => + asyncStreamFrom([ + { + choices: [ + { + delta: { + tool_calls: [ + { + index: 0, + id: "call_1", + function: { name: "test_tool", arguments: "" }, + }, + ], }, - ], - } - yield { - choices: [ - { - delta: { - tool_calls: [{ index: 0, function: { arguments: '{"arg":' } }], - }, - finish_reason: null, + finish_reason: null, + }, + ], + }, + { + choices: [ + { + delta: { + tool_calls: [{ index: 0, function: { arguments: '{"arg":' } }], }, - ], - } - yield { - choices: [ - { - delta: { - tool_calls: [{ index: 0, function: { arguments: '"value"}' } }], - }, - finish_reason: "tool_calls", + finish_reason: null, + }, + ], + }, + { + choices: [ + { + delta: { + tool_calls: [{ index: 0, function: { arguments: '"value"}' } }], }, - ], - } + finish_reason: "tool_calls", + }, + ], }, - } - }) + ]), + ) const stream = handler.createMessage(systemPrompt, messages) - const chunks: any[] = [] - for await (const chunk of stream) { - chunks.push(chunk) - } + const chunks = await collectStream(stream) // Provider now yields tool_call_partial chunks, NativeToolCallParser handles reassembly const toolCallPartialChunks = chunks.filter((chunk) => chunk.type === "tool_call_partial") @@ -381,43 +356,37 @@ describe("OpenAiHandler", () => { }) it("should yield tool calls even when finish_reason is not set (fallback behavior)", async () => { - mockCreate.mockImplementation(async (options) => { - return { - [Symbol.asyncIterator]: async function* () { - yield { - choices: [ - { - delta: { - tool_calls: [ - { - index: 0, - id: "call_fallback", - function: { name: "fallback_tool", arguments: '{"test":"fallback"}' }, - }, - ], - }, - finish_reason: null, - }, - ], - } - // Stream ends without finish_reason being set to "tool_calls" - yield { - choices: [ - { - delta: {}, - finish_reason: "stop", // Different finish reason + mockCreate.mockImplementation(async (options) => + asyncStreamFrom([ + { + choices: [ + { + delta: { + tool_calls: [ + { + index: 0, + id: "call_fallback", + function: { name: "fallback_tool", arguments: '{"test":"fallback"}' }, + }, + ], }, - ], - } + finish_reason: null, + }, + ], }, - } - }) + { + choices: [ + { + delta: {}, + finish_reason: "stop", + }, + ], + }, + ]), + ) const stream = handler.createMessage(systemPrompt, messages) - const chunks: any[] = [] - for await (const chunk of stream) { - chunks.push(chunk) - } + const chunks = await collectStream(stream) // Provider now yields tool_call_partial chunks, NativeToolCallParser handles reassembly const toolCallPartialChunks = chunks.filter((chunk) => chunk.type === "tool_call_partial") @@ -445,8 +414,7 @@ describe("OpenAiHandler", () => { const reasoningHandler = new OpenAiHandler(reasoningOptions) const stream = reasoningHandler.createMessage(systemPrompt, messages) // Consume the stream to trigger the API call - for await (const _chunk of stream) { - } + await collectStream(stream) // Assert the mockCreate was called with reasoning_effort expect(mockCreate).toHaveBeenCalled() const callArgs = mockCreate.mock.calls[0][0] @@ -466,8 +434,7 @@ describe("OpenAiHandler", () => { } const reasoningHandler = new OpenAiHandler(reasoningOptions) const stream = reasoningHandler.createMessage(systemPrompt, messages) - for await (const _chunk of stream) { - } + await collectStream(stream) expect(mockCreate).toHaveBeenCalled() const callArgs = mockCreate.mock.calls[0][0] @@ -483,8 +450,7 @@ describe("OpenAiHandler", () => { const noReasoningHandler = new OpenAiHandler(noReasoningOptions) const stream = noReasoningHandler.createMessage(systemPrompt, messages) // Consume the stream to trigger the API call - for await (const _chunk of stream) { - } + await collectStream(stream) // Assert the mockCreate was called without reasoning_effort expect(mockCreate).toHaveBeenCalled() const callArgs = mockCreate.mock.calls[0][0] @@ -502,8 +468,7 @@ describe("OpenAiHandler", () => { } const noTempHandler = new OpenAiHandler(noTempOptions) const stream = noTempHandler.createMessage(systemPrompt, messages) - for await (const _chunk of stream) { - } + await collectStream(stream) expect(mockCreate).toHaveBeenCalled() const callArgs = mockCreate.mock.calls[0][0] expect(callArgs).not.toHaveProperty("temperature") @@ -513,8 +478,7 @@ describe("OpenAiHandler", () => { // Option A: when "use custom temperature" is off (modelTemperature unset) and the model has no // required default, omit `temperature` so the server's own default applies instead of forcing 0. const stream = handler.createMessage(systemPrompt, messages) - for await (const _chunk of stream) { - } + await collectStream(stream) expect(mockCreate).toHaveBeenCalled() const callArgs = mockCreate.mock.calls[0][0] expect(callArgs).not.toHaveProperty("temperature") @@ -523,8 +487,7 @@ describe("OpenAiHandler", () => { it("should use the configured modelTemperature when supportsTemperature is not false", async () => { const customTempHandler = new OpenAiHandler({ ...mockOptions, modelTemperature: 0.5 }) const stream = customTempHandler.createMessage(systemPrompt, messages) - for await (const _chunk of stream) { - } + await collectStream(stream) expect(mockCreate).toHaveBeenCalled() const callArgs = mockCreate.mock.calls[0][0] expect(callArgs.temperature).toBe(0.5) @@ -533,8 +496,7 @@ describe("OpenAiHandler", () => { it("should default to DEEP_SEEK_DEFAULT_TEMPERATURE for deepseek-reasoner models", async () => { const deepseekHandler = new OpenAiHandler({ ...mockOptions, openAiModelId: "deepseek-reasoner" }) const stream = deepseekHandler.createMessage(systemPrompt, messages) - for await (const _chunk of stream) { - } + await collectStream(stream) expect(mockCreate).toHaveBeenCalled() const callArgs = mockCreate.mock.calls[0][0] expect(callArgs.temperature).toBe(DEEP_SEEK_DEFAULT_TEMPERATURE) @@ -544,8 +506,7 @@ describe("OpenAiHandler", () => { // A deliberate 0 must be distinguished from "unset" — it is sent, not omitted. const zeroTempHandler = new OpenAiHandler({ ...mockOptions, modelTemperature: 0 }) const stream = zeroTempHandler.createMessage(systemPrompt, messages) - for await (const _chunk of stream) { - } + await collectStream(stream) expect(mockCreate).toHaveBeenCalled() const callArgs = mockCreate.mock.calls[0][0] expect(callArgs.temperature).toBe(0) @@ -564,8 +525,7 @@ describe("OpenAiHandler", () => { const handlerWithMaxTokens = new OpenAiHandler(optionsWithMaxTokens) const stream = handlerWithMaxTokens.createMessage(systemPrompt, messages) // Consume the stream to trigger the API call - for await (const _chunk of stream) { - } + await collectStream(stream) // Assert the mockCreate was called with max_tokens expect(mockCreate).toHaveBeenCalled() const callArgs = mockCreate.mock.calls[0][0] @@ -585,8 +545,7 @@ describe("OpenAiHandler", () => { const handlerWithoutMaxTokens = new OpenAiHandler(optionsWithoutMaxTokens) const stream = handlerWithoutMaxTokens.createMessage(systemPrompt, messages) // Consume the stream to trigger the API call - for await (const _chunk of stream) { - } + await collectStream(stream) // Assert the mockCreate was called without max_tokens expect(mockCreate).toHaveBeenCalled() const callArgs = mockCreate.mock.calls[0][0] @@ -606,8 +565,7 @@ describe("OpenAiHandler", () => { const handlerWithDefaultMaxTokens = new OpenAiHandler(optionsWithUndefinedMaxTokens) const stream = handlerWithDefaultMaxTokens.createMessage(systemPrompt, messages) // Consume the stream to trigger the API call - for await (const _chunk of stream) { - } + await collectStream(stream) // Assert the mockCreate was called without max_tokens expect(mockCreate).toHaveBeenCalled() const callArgs = mockCreate.mock.calls[0][0] @@ -628,8 +586,7 @@ describe("OpenAiHandler", () => { const handlerWithUserMaxTokens = new OpenAiHandler(optionsWithUserMaxTokens) const stream = handlerWithUserMaxTokens.createMessage(systemPrompt, messages) // Consume the stream to trigger the API call - for await (const _chunk of stream) { - } + await collectStream(stream) // Assert the mockCreate was called with user-configured modelMaxTokens (32000), not model default maxTokens (4096) expect(mockCreate).toHaveBeenCalled() const callArgs = mockCreate.mock.calls[0][0] @@ -650,8 +607,7 @@ describe("OpenAiHandler", () => { const handlerWithoutUserMaxTokens = new OpenAiHandler(optionsWithoutUserMaxTokens) const stream = handlerWithoutUserMaxTokens.createMessage(systemPrompt, messages) // Consume the stream to trigger the API call - for await (const _chunk of stream) { - } + await collectStream(stream) // Assert the mockCreate was called with model default maxTokens (4096) as fallback expect(mockCreate).toHaveBeenCalled() const callArgs = mockCreate.mock.calls[0][0] @@ -660,47 +616,25 @@ describe("OpenAiHandler", () => { describe("TagMatcher reasoning tags", () => { it("should treat stray closing tag as plain text when no tag is open", async () => { - mockCreate.mockImplementationOnce(() => ({ - [Symbol.asyncIterator]: () => ({ - next: vi - .fn() - .mockResolvedValueOnce({ - done: false, - value: { choices: [{ delta: { content: "finaltext" } }] }, - }) - .mockResolvedValueOnce({ done: true }), - }), - })) + mockCreate.mockImplementationOnce(() => + asyncStreamFrom([{ choices: [{ delta: { content: "finaltext" } }] }]), + ) const stream = handler.createMessage(systemPrompt, messages) - const chunks: any[] = [] - for await (const chunk of stream) { - chunks.push(chunk) - } + const chunks = await collectStream(stream) expect(chunks).toEqual([{ type: "text", text: "finaltext" }]) }) it("should treat extra closing tag after a closed block as plain text", async () => { - mockCreate.mockImplementationOnce(() => ({ - [Symbol.asyncIterator]: () => ({ - next: vi - .fn() - .mockResolvedValueOnce({ - done: false, - value: { - choices: [{ delta: { content: "thinkingfinaltext" } }], - }, - }) - .mockResolvedValueOnce({ done: true }), - }), - })) + mockCreate.mockImplementationOnce(() => + asyncStreamFrom([ + { choices: [{ delta: { content: "thinkingfinaltext" } }] }, + ]), + ) const stream = handler.createMessage(systemPrompt, messages) - const chunks: any[] = [] - for await (const chunk of stream) { - chunks.push(chunk) - } + const chunks = await collectStream(stream) expect(chunks).toEqual([ { type: "reasoning", text: "thinking" }, @@ -709,35 +643,17 @@ describe("OpenAiHandler", () => { }) it("should handle nested mixed tags with correct closure matching", async () => { - mockCreate.mockImplementationOnce(() => ({ - [Symbol.asyncIterator]: () => ({ - next: vi - .fn() - .mockResolvedValueOnce({ - done: false, - value: { choices: [{ delta: { content: "outer" } }] }, - }) - .mockResolvedValueOnce({ - done: false, - value: { choices: [{ delta: { content: "inner" } }] }, - }) - .mockResolvedValueOnce({ - done: false, - value: { choices: [{ delta: { content: " middle" } }] }, - }) - .mockResolvedValueOnce({ - done: false, - value: { choices: [{ delta: { content: "final text" } }] }, - }) - .mockResolvedValueOnce({ done: true }), - }), - })) + mockCreate.mockImplementationOnce(() => + asyncStreamFrom([ + { choices: [{ delta: { content: "outer" } }] }, + { choices: [{ delta: { content: "inner" } }] }, + { choices: [{ delta: { content: " middle" } }] }, + { choices: [{ delta: { content: "final text" } }] }, + ]), + ) const stream = handler.createMessage(systemPrompt, messages) - const chunks: any[] = [] - for await (const chunk of stream) { - chunks.push(chunk) - } + const chunks = await collectStream(stream) // With the tag stack fix, closes inner tag, // and correctly closes the outer tag. @@ -751,35 +667,17 @@ describe("OpenAiHandler", () => { }) it("should handle nested tags with correct stack unwinding", async () => { - mockCreate.mockImplementationOnce(() => ({ - [Symbol.asyncIterator]: () => ({ - next: vi - .fn() - .mockResolvedValueOnce({ - done: false, - value: { choices: [{ delta: { content: "outer" } }] }, - }) - .mockResolvedValueOnce({ - done: false, - value: { choices: [{ delta: { content: "inner" } }] }, - }) - .mockResolvedValueOnce({ - done: false, - value: { choices: [{ delta: { content: " middle" } }] }, - }) - .mockResolvedValueOnce({ - done: false, - value: { choices: [{ delta: { content: "final text" } }] }, - }) - .mockResolvedValueOnce({ done: true }), - }), - })) + mockCreate.mockImplementationOnce(() => + asyncStreamFrom([ + { choices: [{ delta: { content: "outer" } }] }, + { choices: [{ delta: { content: "inner" } }] }, + { choices: [{ delta: { content: " middle" } }] }, + { choices: [{ delta: { content: "final text" } }] }, + ]), + ) const stream = handler.createMessage(systemPrompt, messages) - const chunks: any[] = [] - for await (const chunk of stream) { - chunks.push(chunk) - } + const chunks = await collectStream(stream) // With the tag stack fix, closes inner tag, // and correctly closes the outer tag. @@ -793,31 +691,16 @@ describe("OpenAiHandler", () => { }) it("should handle reasoning_content alongside tag matching", async () => { - mockCreate.mockImplementationOnce(() => ({ - [Symbol.asyncIterator]: () => ({ - next: vi - .fn() - .mockResolvedValueOnce({ - done: false, - value: { choices: [{ delta: { reasoning_content: "native reasoning" } }] }, - }) - .mockResolvedValueOnce({ - done: false, - value: { choices: [{ delta: { content: "tag based" } }] }, - }) - .mockResolvedValueOnce({ - done: false, - value: { choices: [{ delta: { content: " final output" } }] }, - }) - .mockResolvedValueOnce({ done: true }), - }), - })) + mockCreate.mockImplementationOnce(() => + asyncStreamFrom([ + { choices: [{ delta: { reasoning_content: "native reasoning" } }] }, + { choices: [{ delta: { content: "tag based" } }] }, + { choices: [{ delta: { content: " final output" } }] }, + ]), + ) const stream = handler.createMessage(systemPrompt, messages) - const chunks: any[] = [] - for await (const chunk of stream) { - chunks.push(chunk) - } + const chunks = await collectStream(stream) expect(chunks).toEqual([ { type: "reasoning", text: "native reasoning" }, @@ -856,8 +739,7 @@ describe("OpenAiHandler", () => { ] const stream = thinkingHandler.createMessage(systemPrompt, messagesWithReasoning) - for await (const _chunk of stream) { - } + await collectStream(stream) expect(mockCreate).toHaveBeenCalled() const sentMessages: any[] = mockCreate.mock.calls[0][0].messages @@ -886,9 +768,7 @@ describe("OpenAiHandler", () => { const stream = handler.createMessage("system prompt", testMessages) await expect(async () => { - for await (const _chunk of stream) { - // Should not reach here - } + await collectStream(stream) }).rejects.toThrow("API Error") }) @@ -901,9 +781,7 @@ describe("OpenAiHandler", () => { const stream = handler.createMessage("system prompt", testMessages) await expect(async () => { - for await (const _chunk of stream) { - // Should not reach here - } + await collectStream(stream) }).rejects.toThrow("Rate limit exceeded") }) }) @@ -986,10 +864,7 @@ describe("OpenAiHandler", () => { ] const stream = azureHandler.createMessage(systemPrompt, messages) - const chunks: any[] = [] - for await (const chunk of stream) { - chunks.push(chunk) - } + const chunks = await collectStream(stream) expect(chunks.length).toBeGreaterThan(0) const textChunks = chunks.filter((chunk) => chunk.type === "text") @@ -1033,10 +908,7 @@ describe("OpenAiHandler", () => { ] const stream = azureHandler.createMessage(systemPrompt, messages) - const chunks: any[] = [] - for await (const chunk of stream) { - chunks.push(chunk) - } + const chunks = await collectStream(stream) expect(chunks.length).toBeGreaterThan(0) const textChunk = chunks.find((chunk) => chunk.type === "text") @@ -1154,10 +1026,7 @@ describe("OpenAiHandler", () => { ] const stream = o3Handler.createMessage(systemPrompt, messages) - const chunks: any[] = [] - for await (const chunk of stream) { - chunks.push(chunk) - } + const chunks = await collectStream(stream) expect(mockCreate).toHaveBeenCalledWith( expect.objectContaining({ @@ -1183,44 +1052,39 @@ describe("OpenAiHandler", () => { it("should handle tool calls with O3 model in streaming mode", async () => { const o3Handler = new OpenAiHandler(o3Options) - mockCreate.mockImplementation(async (options) => { - return { - [Symbol.asyncIterator]: async function* () { - yield { - choices: [ - { - delta: { - tool_calls: [ - { - index: 0, - id: "call_1", - function: { name: "test_tool", arguments: "" }, - }, - ], - }, - finish_reason: null, + mockCreate.mockImplementation(async (options) => + asyncStreamFrom([ + { + choices: [ + { + delta: { + tool_calls: [ + { + index: 0, + id: "call_1", + function: { name: "test_tool", arguments: "" }, + }, + ], }, - ], - } - yield { - choices: [ - { - delta: { - tool_calls: [{ index: 0, function: { arguments: "{}" } }], - }, - finish_reason: "tool_calls", + finish_reason: null, + }, + ], + }, + { + choices: [ + { + delta: { + tool_calls: [{ index: 0, function: { arguments: "{}" } }], }, - ], - } + finish_reason: "tool_calls", + }, + ], }, - } - }) + ]), + ) const stream = o3Handler.createMessage("system", []) - const chunks: any[] = [] - for await (const chunk of stream) { - chunks.push(chunk) - } + const chunks = await collectStream(stream) // Provider now yields tool_call_partial chunks, NativeToolCallParser handles reassembly const toolCallPartialChunks = chunks.filter((chunk) => chunk.type === "tool_call_partial") @@ -1248,43 +1112,37 @@ describe("OpenAiHandler", () => { it("should yield tool calls for O3 model even when finish_reason is not set (fallback behavior)", async () => { const o3Handler = new OpenAiHandler(o3Options) - mockCreate.mockImplementation(async (options) => { - return { - [Symbol.asyncIterator]: async function* () { - yield { - choices: [ - { - delta: { - tool_calls: [ - { - index: 0, - id: "call_o3_fallback", - function: { name: "o3_fallback_tool", arguments: '{"o3":"test"}' }, - }, - ], - }, - finish_reason: null, - }, - ], - } - // Stream ends with different finish reason - yield { - choices: [ - { - delta: {}, - finish_reason: "length", // Different finish reason + mockCreate.mockImplementation(async (options) => + asyncStreamFrom([ + { + choices: [ + { + delta: { + tool_calls: [ + { + index: 0, + id: "call_o3_fallback", + function: { name: "o3_fallback_tool", arguments: '{"o3":"test"}' }, + }, + ], }, - ], - } + finish_reason: null, + }, + ], }, - } - }) + { + choices: [ + { + delta: {}, + finish_reason: "length", + }, + ], + }, + ]), + ) const stream = o3Handler.createMessage("system", []) - const chunks: any[] = [] - for await (const chunk of stream) { - chunks.push(chunk) - } + const chunks = await collectStream(stream) // Provider now yields tool_call_partial chunks, NativeToolCallParser handles reassembly const toolCallPartialChunks = chunks.filter((chunk) => chunk.type === "tool_call_partial") @@ -1313,10 +1171,7 @@ describe("OpenAiHandler", () => { ] const stream = o3Handler.createMessage(systemPrompt, messages) - const chunks: any[] = [] - for await (const chunk of stream) { - chunks.push(chunk) - } + const chunks = await collectStream(stream) expect(mockCreate).toHaveBeenCalledWith( expect.objectContaining({ @@ -1357,10 +1212,7 @@ describe("OpenAiHandler", () => { ] const stream = o3Handler.createMessage(systemPrompt, messages) - const chunks: any[] = [] - for await (const chunk of stream) { - chunks.push(chunk) - } + const chunks = await collectStream(stream) expect(mockCreate).toHaveBeenCalledWith( expect.objectContaining({ @@ -1419,10 +1271,7 @@ describe("OpenAiHandler", () => { }) const stream = o3Handler.createMessage("system", []) - const chunks: any[] = [] - for await (const chunk of stream) { - chunks.push(chunk) - } + const chunks = await collectStream(stream) const toolCallChunks = chunks.filter((chunk) => chunk.type === "tool_call") expect(toolCallChunks).toHaveLength(1) diff --git a/src/eslint-suppressions.json b/src/eslint-suppressions.json index ca07e0a6fa..a1a096e2ae 100644 --- a/src/eslint-suppressions.json +++ b/src/eslint-suppressions.json @@ -241,7 +241,7 @@ }, "api/providers/__tests__/openai.spec.ts": { "@typescript-eslint/no-explicit-any": { - "count": 25 + "count": 4 } }, "api/providers/__tests__/opencode-go.spec.ts": {