Skip to content
Closed
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
23 changes: 22 additions & 1 deletion src/lib/responses/ResponseStream.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@ import { RequestOptions } from '../../internal/request-options';
import { APIUserAbortError, OpenAIError } from '../../error';
import OpenAI from '../../index';
import { type BaseEvents, EventStream } from '../EventStream';
import { type ResponseFunctionCallArgumentsDeltaEvent, type ResponseTextDeltaEvent } from './EventTypes';
import {
type ResponseFunctionCallArgumentsDeltaEvent,
type ResponseRefusalDeltaEvent,
type ResponseTextDeltaEvent,
Comment on lines +13 to +16

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Import refusal delta from an exported module

In builds that type-check this file, this import fails because ResponseRefusalDeltaEvent is only a local import inside ./EventTypes and is not exported from that module; the newly added binding is also unused. Import it from ../../resources/responses/responses, export an augmented type from EventTypes, or remove it if no special event typing is needed.

Useful? React with 👍 / 👎.

} from './EventTypes';
import { maybeParseResponse, ParseableToolsParams } from '../ResponsesParser';
import { Stream } from '../../streaming';

Expand Down Expand Up @@ -278,6 +282,23 @@ export class ResponseStream<ParsedT = null>
}
break;
}
case 'response.refusal.delta': {
const output = snapshot.output[event.output_index];
if (!output) {
throw new OpenAIError(`missing output at index ${event.output_index}`);
}
if (output.type === 'message') {
const content = output.content[event.content_index];
if (!content) {
throw new OpenAIError(`missing content at index ${event.content_index}`);
}
if (content.type !== 'refusal') {
throw new OpenAIError(`expected content to be 'refusal', got ${content.type}`);
}
content.refusal += event.delta;
}
break;
}
case 'response.completed': {
this.#currentResponseSnapshot = event.response;
break;
Expand Down
25 changes: 25 additions & 0 deletions tests/lib/ResponseStream.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,31 @@ describe('.stream()', () => {
}
});

it('refusal accumulates correctly', async () => {
const deltas: string[] = [];

const stream = (
await makeStreamSnapshotRequest((openai) =>
openai.responses.stream({
model: 'gpt-4o-2024-08-06',
input: 'Do something harmful',
}),
)
).on('response.refusal.delta', (e) => {
deltas.push(e.delta);
});

const final = await stream.finalResponse();
// The three deltas must be concatenated into the snapshot's refusal content
expect(deltas).toEqual(['I cannot ', 'help with ', 'that.']);

const msg = final.output[0];
expect(msg?.type).toBe('message');
if (msg?.type === 'message') {
expect(msg.content[0]).toMatchObject({ type: 'refusal', refusal: 'I cannot help with that.' });
}
});

it('reasoning works', async () => {
const stream = await makeStreamSnapshotRequest((openai) =>
openai.responses.stream({
Expand Down
24 changes: 24 additions & 0 deletions tests/lib/__snapshots__/ResponseStream.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,30 @@ data: [DONE]
"
`;

exports[`.stream() refusal accumulates correctly 1`] = `
"data: {\"response\":{\"id\":\"resp_refusal_1\",\"object\":\"response\",\"created_at\":1723031665,\"model\":\"gpt-4o-2024-08-06\",\"output\":[],\"output_text\":\"\",\"error\":null,\"incomplete_details\":null,\"instructions\":null,\"metadata\":null,\"parallel_tool_calls\":false,\"temperature\":null,\"tools\":[],\"top_p\":null,\"status\":\"in_progress\",\"usage\":{\"input_tokens\":0,\"input_tokens_details\":{\"cached_tokens\":0},\"output_tokens\":0,\"output_tokens_details\":{\"reasoning_tokens\":0},\"total_tokens\":0}},\"sequence_number\":0,\"type\":\"response.created\"}

data: {\"item\":{\"id\":\"msg_ref_1\",\"type\":\"message\",\"role\":\"assistant\",\"status\":\"in_progress\",\"content\":[]},\"output_index\":0,\"sequence_number\":1,\"type\":\"response.output_item.added\"}

data: {\"content_index\":0,\"item_id\":\"msg_ref_1\",\"output_index\":0,\"part\":{\"type\":\"refusal\",\"refusal\":\"\"},\"sequence_number\":2,\"type\":\"response.content_part.added\"}

data: {\"content_index\":0,\"delta\":\"I cannot \",\"item_id\":\"msg_ref_1\",\"output_index\":0,\"sequence_number\":3,\"type\":\"response.refusal.delta\"}

data: {\"content_index\":0,\"delta\":\"help with \",\"item_id\":\"msg_ref_1\",\"output_index\":0,\"sequence_number\":4,\"type\":\"response.refusal.delta\"}

data: {\"content_index\":0,\"delta\":\"that.\",\"item_id\":\"msg_ref_1\",\"output_index\":0,\"sequence_number\":5,\"type\":\"response.refusal.delta\"}

data: {\"content_index\":0,\"item_id\":\"msg_ref_1\",\"output_index\":0,\"refusal\":\"I cannot help with that.\",\"sequence_number\":6,\"type\":\"response.refusal.done\"}

data: {\"item\":{\"id\":\"msg_ref_1\",\"type\":\"message\",\"role\":\"assistant\",\"status\":\"completed\",\"content\":[{\"type\":\"refusal\",\"refusal\":\"I cannot help with that.\"}]},\"output_index\":0,\"sequence_number\":7,\"type\":\"response.output_item.done\"}

data: {\"response\":{\"id\":\"resp_refusal_1\",\"object\":\"response\",\"created_at\":1723031665,\"model\":\"gpt-4o-2024-08-06\",\"output\":[{\"id\":\"msg_ref_1\",\"type\":\"message\",\"role\":\"assistant\",\"status\":\"completed\",\"content\":[{\"type\":\"refusal\",\"refusal\":\"I cannot help with that.\"}]}],\"output_text\":\"\",\"error\":null,\"incomplete_details\":null,\"instructions\":null,\"metadata\":null,\"parallel_tool_calls\":false,\"temperature\":null,\"tools\":[],\"top_p\":null,\"status\":\"completed\",\"usage\":{\"input_tokens\":10,\"input_tokens_details\":{\"cached_tokens\":0},\"output_tokens\":5,\"output_tokens_details\":{\"reasoning_tokens\":0},\"total_tokens\":15}},\"sequence_number\":8,\"type\":\"response.completed\"}

data: [DONE]

"
`;

exports[`.stream() reasoning works 1`] = `
"data: {\"response\":{\"id\":\"resp_reason_1\",\"object\":\"response\",\"created_at\":1723031666,\"model\":\"o3\",\"output\":[],\"output_text\":\"\",\"error\":null,\"incomplete_details\":null,\"instructions\":null,\"metadata\":null,\"parallel_tool_calls\":false,\"temperature\":null,\"tools\":[],\"top_p\":null,\"status\":\"in_progress\",\"usage\":{\"input_tokens\":0,\"input_tokens_details\":{\"cached_tokens\":0},\"output_tokens\":0,\"output_tokens_details\":{\"reasoning_tokens\":0},\"total_tokens\":0}},\"sequence_number\":0,\"type\":\"response.created\"}

Expand Down