From 3d1b00fc2b4bee13d9eef8a10a54169834ae9d96 Mon Sep 17 00:00:00 2001 From: Roomote Date: Sat, 1 Aug 2026 13:22:27 +0000 Subject: [PATCH] refactor: roll out stream helpers to requesty spec --- src/api/providers/__tests__/requesty.spec.ts | 185 ++++++++----------- src/eslint-suppressions.json | 5 - src/test-utils/__tests__/stream.spec.ts | 20 ++ 3 files changed, 99 insertions(+), 111 deletions(-) create mode 100644 src/test-utils/__tests__/stream.spec.ts diff --git a/src/api/providers/__tests__/requesty.spec.ts b/src/api/providers/__tests__/requesty.spec.ts index 38e0c33d95..77adb8724f 100644 --- a/src/api/providers/__tests__/requesty.spec.ts +++ b/src/api/providers/__tests__/requesty.spec.ts @@ -13,6 +13,7 @@ import { RequestyHandler } from "../requesty" import { ApiHandlerOptions } from "../../../shared/api" import { Package } from "../../../shared/package" import { ApiHandlerCreateMessageMetadata } from "../../index" +import { asyncStreamFrom, collectStream } from "../../../test-utils/stream" const mockCreate = vitest.fn() @@ -182,38 +183,31 @@ describe("RequestyHandler", () => { it("generates correct stream chunks", async () => { const handler = new RequestyHandler(mockOptions) - const mockStream = { - async *[Symbol.asyncIterator]() { - yield { - id: mockOptions.requestyModelId, - choices: [{ delta: { content: "test response" } }], - } - yield { - id: "test-id", - choices: [{ delta: {} }], - usage: { - prompt_tokens: 10, - completion_tokens: 20, - prompt_tokens_details: { - caching_tokens: 5, - cached_tokens: 2, - }, + const mockStream = asyncStreamFrom([ + { + id: mockOptions.requestyModelId, + choices: [{ delta: { content: "test response" } }], + }, + { + id: "test-id", + choices: [{ delta: {} }], + usage: { + prompt_tokens: 10, + completion_tokens: 20, + prompt_tokens_details: { + caching_tokens: 5, + cached_tokens: 2, }, - } + }, }, - } + ]) mockCreate.mockResolvedValue(mockStream) const systemPrompt = "test system prompt" const messages: Anthropic.Messages.MessageParam[] = [{ role: "user" as const, content: "test message" }] - const generator = handler.createMessage(systemPrompt, messages) - const chunks = [] - - for await (const chunk of generator) { - chunks.push(chunk) - } + const chunks = await collectStream(handler.createMessage(systemPrompt, messages)) // Verify stream chunks expect(chunks).toHaveLength(2) // One text chunk and one usage chunk @@ -257,15 +251,13 @@ describe("RequestyHandler", () => { modelMaxTokens: 32768, }) - const mockStream = { - async *[Symbol.asyncIterator]() { - yield { - id: "test-id", - choices: [{ delta: {} }], - usage: { prompt_tokens: 10, completion_tokens: 20 }, - } + const mockStream = asyncStreamFrom([ + { + id: "test-id", + choices: [{ delta: {} }], + usage: { prompt_tokens: 10, completion_tokens: 20 }, }, - } + ]) mockCreate.mockResolvedValue(mockStream) @@ -290,15 +282,13 @@ describe("RequestyHandler", () => { modelMaxTokens: 32768, }) - const mockStream = { - async *[Symbol.asyncIterator]() { - yield { - id: "test-id", - choices: [{ delta: {} }], - usage: { prompt_tokens: 10, completion_tokens: 20 }, - } + const mockStream = asyncStreamFrom([ + { + id: "test-id", + choices: [{ delta: {} }], + usage: { prompt_tokens: 10, completion_tokens: 20 }, }, - } + ]) mockCreate.mockResolvedValue(mockStream) @@ -323,15 +313,13 @@ describe("RequestyHandler", () => { modelMaxTokens: 32768, }) - const mockStream = { - async *[Symbol.asyncIterator]() { - yield { - id: "test-id", - choices: [{ delta: {} }], - usage: { prompt_tokens: 10, completion_tokens: 20 }, - } + const mockStream = asyncStreamFrom([ + { + id: "test-id", + choices: [{ delta: {} }], + usage: { prompt_tokens: 10, completion_tokens: 20 }, }, - } + ]) mockCreate.mockResolvedValue(mockStream) @@ -359,43 +347,37 @@ describe("RequestyHandler", () => { it("streams reasoning chunks from delta.reasoning_content", async () => { const handler = new RequestyHandler(mockOptions) - mockCreate.mockResolvedValue({ - async *[Symbol.asyncIterator]() { - yield { id: "1", choices: [{ delta: { reasoning_content: "thinking..." } }] } - yield { id: "1", choices: [{ delta: { content: "answer" } }] } - yield { + mockCreate.mockResolvedValue( + asyncStreamFrom([ + { id: "1", choices: [{ delta: { reasoning_content: "thinking..." } }] }, + { id: "1", choices: [{ delta: { content: "answer" } }] }, + { id: "1", choices: [{ delta: {} }], usage: { prompt_tokens: 1, completion_tokens: 1 }, - } - }, - }) + }, + ]), + ) - const chunks: any[] = [] - for await (const chunk of handler.createMessage("sys", [{ role: "user", content: "hi" }])) { - chunks.push(chunk) - } + const chunks = await collectStream(handler.createMessage("sys", [{ role: "user", content: "hi" }])) expect(chunks).toContainEqual({ type: "reasoning", text: "thinking..." }) }) it("falls back to delta.reasoning when reasoning_content is absent", async () => { const handler = new RequestyHandler(mockOptions) - mockCreate.mockResolvedValue({ - async *[Symbol.asyncIterator]() { - yield { id: "1", choices: [{ delta: { reasoning: "router-style thought" } }] } - yield { + mockCreate.mockResolvedValue( + asyncStreamFrom([ + { id: "1", choices: [{ delta: { reasoning: "router-style thought" } }] }, + { id: "1", choices: [{ delta: {} }], usage: { prompt_tokens: 1, completion_tokens: 1 }, - } - }, - }) + }, + ]), + ) - const chunks: any[] = [] - for await (const chunk of handler.createMessage("sys", [{ role: "user", content: "hi" }])) { - chunks.push(chunk) - } + const chunks = await collectStream(handler.createMessage("sys", [{ role: "user", content: "hi" }])) expect(chunks).toContainEqual({ type: "reasoning", text: "router-style thought" }) }) @@ -403,9 +385,9 @@ describe("RequestyHandler", () => { it("prefers delta.reasoning_content over delta.reasoning when both are present", async () => { const handler = new RequestyHandler(mockOptions) - mockCreate.mockResolvedValue({ - async *[Symbol.asyncIterator]() { - yield { + mockCreate.mockResolvedValue( + asyncStreamFrom([ + { id: "1", choices: [ { @@ -415,20 +397,16 @@ describe("RequestyHandler", () => { }, }, ], - } - yield { + }, + { id: "1", choices: [{ delta: {} }], usage: { prompt_tokens: 1, completion_tokens: 1 }, - } - }, - }) - - const chunks: any[] = [] + }, + ]), + ) - for await (const chunk of handler.createMessage("sys", [{ role: "user", content: "hi" }])) { - chunks.push(chunk) - } + const chunks = await collectStream(handler.createMessage("sys", [{ role: "user", content: "hi" }])) const reasoningChunks = chunks.filter((chunk) => chunk.type === "reasoning") @@ -459,15 +437,14 @@ describe("RequestyHandler", () => { ] beforeEach(() => { - const mockStream = { - async *[Symbol.asyncIterator]() { - yield { + mockCreate.mockResolvedValue( + asyncStreamFrom([ + { id: "test-id", choices: [{ delta: { content: "test response" } }], - } - }, - } - mockCreate.mockResolvedValue(mockStream) + }, + ]), + ) }) it("should include tools in request when tools are provided", async () => { @@ -498,9 +475,9 @@ describe("RequestyHandler", () => { }) it("should handle tool_call_partial chunks in streaming response", async () => { - const mockStreamWithToolCalls = { - async *[Symbol.asyncIterator]() { - yield { + mockCreate.mockResolvedValue( + asyncStreamFrom([ + { id: "test-id", choices: [ { @@ -518,8 +495,8 @@ describe("RequestyHandler", () => { }, }, ], - } - yield { + }, + { id: "test-id", choices: [ { @@ -535,15 +512,14 @@ describe("RequestyHandler", () => { }, }, ], - } - yield { + }, + { id: "test-id", choices: [{ delta: {} }], usage: { prompt_tokens: 10, completion_tokens: 20 }, - } - }, - } - mockCreate.mockResolvedValue(mockStreamWithToolCalls) + }, + ]), + ) const metadata: ApiHandlerCreateMessageMetadata = { taskId: "test-task", @@ -551,10 +527,7 @@ describe("RequestyHandler", () => { } const handler = new RequestyHandler(mockOptions) - const chunks = [] - for await (const chunk of handler.createMessage(systemPrompt, messages, metadata)) { - chunks.push(chunk) - } + const chunks = await collectStream(handler.createMessage(systemPrompt, messages, metadata)) // Expect two tool_call_partial chunks and one usage chunk expect(chunks).toHaveLength(3) diff --git a/src/eslint-suppressions.json b/src/eslint-suppressions.json index 75b9342a24..aefee7a060 100644 --- a/src/eslint-suppressions.json +++ b/src/eslint-suppressions.json @@ -264,11 +264,6 @@ "count": 5 } }, - "api/providers/__tests__/requesty.spec.ts": { - "@typescript-eslint/no-explicit-any": { - "count": 3 - } - }, "api/providers/__tests__/sambanova.spec.ts": { "@typescript-eslint/no-explicit-any": { "count": 2 diff --git a/src/test-utils/__tests__/stream.spec.ts b/src/test-utils/__tests__/stream.spec.ts new file mode 100644 index 0000000000..c5d3fca408 --- /dev/null +++ b/src/test-utils/__tests__/stream.spec.ts @@ -0,0 +1,20 @@ +import { asyncStreamFrom, collectStream } from "../stream" + +describe("stream test utils", () => { + it("collects chunks in order", async () => { + await expect(collectStream(asyncStreamFrom([1, 2, 3]))).resolves.toEqual([1, 2, 3]) + }) + + it("collects empty streams", async () => { + await expect(collectStream(asyncStreamFrom([]))).resolves.toEqual([]) + }) + + it("propagates stream errors", async () => { + async function* failingStream() { + yield 1 + throw new Error("boom") + } + + await expect(collectStream(failingStream())).rejects.toThrow("boom") + }) +})