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": {