From 596726ca44d338ac4a8e916e3b28c50071ae1410 Mon Sep 17 00:00:00 2001 From: Dennis Date: Fri, 1 May 2026 06:12:21 -0200 Subject: [PATCH 1/2] Use getReader for stream shape checks --- packages/agent/src/lib/model-result.ts | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/packages/agent/src/lib/model-result.ts b/packages/agent/src/lib/model-result.ts index c187f72..0f782de 100644 --- a/packages/agent/src/lib/model-result.ts +++ b/packages/agent/src/lib/model-result.ts @@ -92,8 +92,8 @@ function isRecord(value: unknown): value is Record { } /** - * Type guard for stream event with toReadableStream method - * Checks constructor name, prototype, and method availability + * Type guard for stream event responses + * Checks constructor name and readable stream behavior */ function isEventStream(value: unknown): value is EventStream { if (value === null || typeof value !== 'object') { @@ -106,11 +106,10 @@ function isEventStream(value: unknown): value is EventStream Date: Fri, 1 May 2026 18:24:29 -0200 Subject: [PATCH 2/2] Add stream detection regression coverage --- .changeset/curvy-streams-watch.md | 5 + packages/agent/src/lib/model-result.ts | 4 +- .../model-result-stream-detection.test.ts | 111 ++++++++++++++++++ 3 files changed, 117 insertions(+), 3 deletions(-) create mode 100644 .changeset/curvy-streams-watch.md create mode 100644 packages/agent/tests/unit/model-result-stream-detection.test.ts diff --git a/.changeset/curvy-streams-watch.md b/.changeset/curvy-streams-watch.md new file mode 100644 index 0000000..17d5e16 --- /dev/null +++ b/.changeset/curvy-streams-watch.md @@ -0,0 +1,5 @@ +--- +"@openrouter/agent": patch +--- + +Detect streamed Responses API results by readable stream behavior instead of constructor names or unsupported adapters. diff --git a/packages/agent/src/lib/model-result.ts b/packages/agent/src/lib/model-result.ts index 0f782de..e706fe0 100644 --- a/packages/agent/src/lib/model-result.ts +++ b/packages/agent/src/lib/model-result.ts @@ -100,9 +100,7 @@ function isEventStream(value: unknown): value is EventStream vi.fn()); + +vi.mock('@openrouter/sdk/funcs/betaResponsesSend', () => ({ + betaResponsesSend: mockBetaResponsesSend, +})); + +import { ModelResult } from '../../src/lib/model-result.js'; + +function makeResponse(): models.OpenResponsesResult { + return { + id: 'resp_test_stream_detection', + object: 'response', + createdAt: 0, + model: 'test-model', + status: 'completed', + completedAt: 0, + output: [ + { + id: 'msg_test_stream_detection', + type: 'message', + role: 'assistant', + status: 'completed', + content: [ + { + type: 'output_text', + text: 'ok', + annotations: [], + }, + ], + }, + ], + error: null, + incompleteDetails: null, + temperature: null, + topP: null, + presencePenalty: null, + frequencyPenalty: null, + metadata: null, + instructions: null, + tools: [], + toolChoice: 'auto', + parallelToolCalls: false, + } as models.OpenResponsesResult; +} + +function makeCompletedStream( + response: models.OpenResponsesResult, +): ReadableStream { + return new ReadableStream({ + start(controller) { + controller.enqueue({ + type: 'response.completed', + response, + sequenceNumber: 0, + } as models.StreamEvents); + controller.close(); + }, + }); +} + +function makeResult(): ModelResult<[]> { + return new ModelResult({ + request: { + model: 'test-model', + input: 'hello', + }, + client: {} as OpenRouterCore, + tools: [], + }); +} + +describe('ModelResult stream detection', () => { + beforeEach(() => { + mockBetaResponsesSend.mockReset(); + }); + + it('accepts ReadableStream responses whose constructor name is not EventStream', async () => { + const response = makeResponse(); + const stream = makeCompletedStream(response); + + expect(Object.getPrototypeOf(stream)?.constructor?.name).not.toBe('EventStream'); + expect(stream).toBeInstanceOf(ReadableStream); + + mockBetaResponsesSend.mockResolvedValue({ + ok: true, + value: stream, + }); + + await expect(makeResult().getResponse()).resolves.toEqual(response); + }); + + it('rejects toReadableStream-only adapters before ReusableReadableStream reads them', async () => { + const response = makeResponse(); + const adapter = { + toReadableStream: () => makeCompletedStream(response), + }; + + expect('getReader' in adapter).toBe(false); + + mockBetaResponsesSend.mockResolvedValue({ + ok: true, + value: adapter, + }); + + await expect(makeResult().getResponse()).rejects.toThrow('Unexpected response type from API'); + }); +});