Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions src/lib/responses/ResponseStream.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
} from '../../resources/responses/responses';
import { RequestOptions } from '../../internal/request-options';
import { type ReadableStream } from '../../internal/shim-types';
import { APIUserAbortError, OpenAIError } from '../../error';
import { APIError, APIUserAbortError, OpenAIError } from '../../error';
import OpenAI from '../../index';
import { type BaseEvents, EventStream } from '../EventStream';
import { type ResponseFunctionCallArgumentsDeltaEvent, type ResponseTextDeltaEvent } from './EventTypes';
Expand Down Expand Up @@ -54,7 +54,7 @@ type ResponseEvents = BaseEvents &
{
[K in ResponseStreamEvent['type']]: (event: Extract<ResponseStreamEvent, { type: K }>) => void;
},
'response.output_text.delta' | 'response.function_call_arguments.delta'
'response.output_text.delta' | 'response.function_call_arguments.delta' | 'error'
> & {
event: (event: ResponseStreamEvent) => void;
'response.output_text.delta': (event: ResponseTextDeltaEvent) => void;
Expand Down Expand Up @@ -152,6 +152,12 @@ export class ResponseStream<ParsedT = null>
}
break;
}
case 'error': {
// The API reports failures with an `error` event instead of a non-2xx response, so
// convert it here. Throwing hands it to `EventStream`'s error path, which rejects
// `finalResponse()` and notifies `error` listeners with the converted error.
throw new APIError(undefined, event, event.message, undefined);
Comment on lines +155 to +159

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Convert initial error events before snapshot accumulation

When the server sends an error frame before response.created, this branch is never reached because accumulateResponse() first throws its generic “expected 'response.created'” OpenAIError. Consequently finalResponse() and error listeners lose the server's message, code, and parameter and do not receive the promised APIError. Handle event.type === 'error' before calling the accumulator.

Useful? React with 👍 / 👎.

Comment on lines +155 to +159

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Retain converted errors for the async iterator

When a for await consumer is still processing the raw error frame before requesting its next item, this throw reaches the iterator's error listener while readQueue is empty. That listener marks the iterator done but does not retain the failure, so the subsequent next() returns cleanly with done: true instead of rejecting with this APIError; an await in the loop body makes this especially likely. Convert the frame before emitting it through event, or store the failure so the next iterator read rejects.

Useful? React with 👍 / 👎.

}
default:
maybeEmit(event.type, event);
break;
Expand Down
37 changes: 36 additions & 1 deletion tests/lib/ResponseStream.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import OpenAI, { APIUserAbortError } from 'openai';
import OpenAI, { APIError, APIUserAbortError, OpenAIError } from 'openai';
import { ReadableStreamFrom } from 'openai/internal/shims';
import { ResponseStream } from 'openai/lib/responses/ResponseStream';
import type { Response, ResponseStreamEvent } from 'openai/resources/responses/responses';
Expand Down Expand Up @@ -155,6 +155,41 @@ describe('.stream()', () => {
});
});

it('converts an error event into an APIError', async () => {
const events: ResponseStreamEvent[] = [
{
type: 'response.created',
sequence_number: 0,
response: makeResponse(),
},
{
type: 'error',
sequence_number: 1,
code: 'server_error',
message: 'The server had an error while processing your request.',
param: null,
},
];
const stream = ResponseStream.fromReadableStream(readableStreamFromEvents(events));
const listenerErrors: OpenAIError[] = [];
stream.on('error', (error) => listenerErrors.push(error));

const rejection = await stream.finalResponse().then(
() => {
throw new Error('expected finalResponse() to reject');
},
(error: unknown) => error,
);

expect(rejection).toBeInstanceOf(OpenAIError);
expect(rejection).toBeInstanceOf(APIError);
expect((rejection as APIError).message).toBe('The server had an error while processing your request.');
expect((rejection as APIError).code).toBe('server_error');
// `.on('error')` must observe the converted error, not the raw stream frame.
expect(listenerErrors).toHaveLength(1);
expect(listenerErrors[0]).toBe(rejection);
});

it('cancels a stalled readable stream when aborted', async () => {
const encoder = new TextEncoder();
const created = {
Expand Down