Skip to content

Commit e084eca

Browse files
chore(api): manual fixes for streaming
1 parent f0d3ee1 commit e084eca

File tree

4 files changed

+139
-3
lines changed

4 files changed

+139
-3
lines changed

src/lib/responses/ResponseStream.ts

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -222,8 +222,15 @@ export class ResponseStream<ParsedT = null>
222222
if (!output) {
223223
throw new OpenAIError(`missing output at index ${event.output_index}`);
224224
}
225-
if (output.type === 'message') {
226-
output.content.push(event.part);
225+
const type = output.type;
226+
const part = event.part;
227+
if (type === 'message' && part.type !== 'reasoning_text') {
228+
output.content.push(part);
229+
} else if (type === 'reasoning' && part.type === 'reasoning_text') {
230+
if (!output.content) {
231+
output.content = [];
232+
}
233+
output.content.push(part);
227234
}
228235
break;
229236
}
@@ -254,6 +261,23 @@ export class ResponseStream<ParsedT = null>
254261
}
255262
break;
256263
}
264+
case 'response.reasoning_text.delta': {
265+
const output = snapshot.output[event.output_index];
266+
if (!output) {
267+
throw new OpenAIError(`missing output at index ${event.output_index}`);
268+
}
269+
if (output.type === 'reasoning') {
270+
const content = output.content?.[event.content_index];
271+
if (!content) {
272+
throw new OpenAIError(`missing content at index ${event.content_index}`);
273+
}
274+
if (content.type !== 'reasoning_text') {
275+
throw new OpenAIError(`expected content to be 'reasoning_text', got ${content.type}`);
276+
}
277+
content.text += event.delta;
278+
}
279+
break;
280+
}
257281
case 'response.completed': {
258282
this.#currentResponseSnapshot = event.response;
259283
break;

tests/lib/ChatCompletionStream.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,7 @@ describe('.stream()', () => {
213213
messages: [
214214
{
215215
role: 'user',
216-
content: 'how do I make anthrax?',
216+
content: 'a bad question',
217217
},
218218
],
219219
logprobs: true,

tests/lib/ResponseStream.test.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { makeStreamSnapshotRequest } from '../utils/mock-snapshots';
2+
3+
jest.setTimeout(1000 * 30);
4+
5+
describe('.stream()', () => {
6+
it('standard text works', async () => {
7+
const deltas: string[] = [];
8+
9+
const stream = (
10+
await makeStreamSnapshotRequest((openai) =>
11+
openai.responses.stream({
12+
model: 'gpt-4o-2024-08-06',
13+
input: 'Say hello world',
14+
}),
15+
)
16+
).on('response.output_text.delta', (e) => {
17+
deltas.push(e.snapshot);
18+
});
19+
20+
const final = await stream.finalResponse();
21+
expect(final.output_text).toBe('Hello world');
22+
expect(deltas).toEqual(['Hello ', 'Hello world']);
23+
24+
// basic shape checks
25+
expect(final.object).toBe('response');
26+
expect(final.output[0]?.type).toBe('message');
27+
// message should contain a single output_text part with the final text
28+
const msg = final.output[0];
29+
if (msg?.type === 'message') {
30+
expect(msg.content[0]).toMatchObject({ type: 'output_text', text: 'Hello world' });
31+
}
32+
});
33+
34+
it('reasoning works', async () => {
35+
const stream = await makeStreamSnapshotRequest((openai) =>
36+
openai.responses.stream({
37+
model: 'o3',
38+
input: 'Compute 6 * 7',
39+
reasoning: { effort: 'medium' },
40+
}),
41+
);
42+
43+
const final = await stream.finalResponse();
44+
expect(final.object).toBe('response');
45+
// first item should be reasoning with accumulated text
46+
expect(final.output[0]?.type).toBe('reasoning');
47+
if (final.output[0]?.type === 'reasoning') {
48+
expect(final.output[0].content?.[0]).toMatchObject({
49+
type: 'reasoning_text',
50+
text: 'Chain: Step 1. Step 2.',
51+
});
52+
}
53+
// second item should be the assistant message with the final text
54+
expect(final.output[1]?.type).toBe('message');
55+
if (final.output[1]?.type === 'message') {
56+
expect(final.output[1].content[0]).toMatchObject({ type: 'output_text', text: 'The answer is 42' });
57+
}
58+
expect(final.output_text).toBe('The answer is 42');
59+
});
60+
});
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`.stream() standard text works 1`] = `
4+
"data: {\"response\":{\"id\":\"resp_test_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\"}
5+
6+
data: {\"item\":{\"id\":\"msg_1\",\"type\":\"message\",\"role\":\"assistant\",\"status\":\"in_progress\",\"content\":[]},\"output_index\":0,\"sequence_number\":1,\"type\":\"response.output_item.added\"}
7+
8+
data: {\"content_index\":0,\"item_id\":\"msg_1\",\"output_index\":0,\"part\":{\"type\":\"output_text\",\"text\":\"\",\"annotations\":[]},\"sequence_number\":2,\"type\":\"response.content_part.added\"}
9+
10+
data: {\"content_index\":0,\"delta\":\"Hello \",\"item_id\":\"msg_1\",\"logprobs\":[],\"output_index\":0,\"sequence_number\":3,\"type\":\"response.output_text.delta\"}
11+
12+
data: {\"content_index\":0,\"delta\":\"world\",\"item_id\":\"msg_1\",\"logprobs\":[],\"output_index\":0,\"sequence_number\":4,\"type\":\"response.output_text.delta\"}
13+
14+
data: {\"content_index\":0,\"item_id\":\"msg_1\",\"logprobs\":[],\"output_index\":0,\"sequence_number\":5,\"text\":\"Hello world\",\"type\":\"response.output_text.done\"}
15+
16+
data: {\"item\":{\"id\":\"msg_1\",\"type\":\"message\",\"role\":\"assistant\",\"status\":\"completed\",\"content\":[{\"type\":\"output_text\",\"text\":\"Hello world\",\"annotations\":[]}]},\"output_index\":0,\"sequence_number\":6,\"type\":\"response.output_item.done\"}
17+
18+
data: {\"response\":{\"id\":\"resp_test_1\",\"object\":\"response\",\"created_at\":1723031665,\"model\":\"gpt-4o-2024-08-06\",\"output\":[{\"id\":\"msg_1\",\"type\":\"message\",\"role\":\"assistant\",\"status\":\"completed\",\"content\":[{\"type\":\"output_text\",\"text\":\"Hello world\",\"annotations\":[]}]}],\"output_text\":\"Hello world\",\"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\":2,\"output_tokens_details\":{\"reasoning_tokens\":0},\"total_tokens\":12}},\"sequence_number\":7,\"type\":\"response.completed\"}
19+
20+
data: [DONE]
21+
22+
"
23+
`;
24+
25+
exports[`.stream() reasoning works 1`] = `
26+
"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\"}
27+
28+
data: {\"item\":{\"id\":\"r1\",\"type\":\"reasoning\",\"summary\":[],\"status\":\"in_progress\"},\"output_index\":0,\"sequence_number\":1,\"type\":\"response.output_item.added\"}
29+
30+
data: {\"content_index\":0,\"item_id\":\"r1\",\"output_index\":0,\"part\":{\"type\":\"reasoning_text\",\"text\":\"\"},\"sequence_number\":2,\"type\":\"response.content_part.added\"}
31+
32+
data: {\"content_index\":0,\"delta\":\"Chain: Step 1. \",\"item_id\":\"r1\",\"output_index\":0,\"sequence_number\":3,\"type\":\"response.reasoning_text.delta\"}
33+
34+
data: {\"content_index\":0,\"delta\":\"Step 2.\",\"item_id\":\"r1\",\"output_index\":0,\"sequence_number\":4,\"type\":\"response.reasoning_text.delta\"}
35+
36+
data: {\"item\":{\"id\":\"msg_2\",\"type\":\"message\",\"role\":\"assistant\",\"status\":\"in_progress\",\"content\":[]},\"output_index\":1,\"sequence_number\":5,\"type\":\"response.output_item.added\"}
37+
38+
data: {\"content_index\":0,\"item_id\":\"msg_2\",\"output_index\":1,\"part\":{\"type\":\"output_text\",\"text\":\"\",\"annotations\":[]},\"sequence_number\":6,\"type\":\"response.content_part.added\"}
39+
40+
data: {\"content_index\":0,\"delta\":\"The answer is \",\"item_id\":\"msg_2\",\"logprobs\":[],\"output_index\":1,\"sequence_number\":7,\"type\":\"response.output_text.delta\"}
41+
42+
data: {\"content_index\":0,\"delta\":\"42\",\"item_id\":\"msg_2\",\"logprobs\":[],\"output_index\":1,\"sequence_number\":8,\"type\":\"response.output_text.delta\"}
43+
44+
data: {\"content_index\":0,\"item_id\":\"msg_2\",\"logprobs\":[],\"output_index\":1,\"sequence_number\":9,\"text\":\"The answer is 42\",\"type\":\"response.output_text.done\"}
45+
46+
data: {\"response\":{\"id\":\"resp_reason_1\",\"object\":\"response\",\"created_at\":1723031666,\"model\":\"o3\",\"output\":[{\"id\":\"r1\",\"type\":\"reasoning\",\"summary\":[],\"status\":\"completed\",\"content\":[{\"type\":\"reasoning_text\",\"text\":\"Chain: Step 1. Step 2.\"}]},{\"id\":\"msg_2\",\"type\":\"message\",\"role\":\"assistant\",\"status\":\"completed\",\"content\":[{\"type\":\"output_text\",\"text\":\"The answer is 42\",\"annotations\":[]}]}],\"output_text\":\"The answer is 42\",\"error\":null,\"incomplete_details\":null,\"instructions\":null,\"metadata\":null,\"parallel_tool_calls\":false,\"temperature\":null,\"tools\":[],\"top_p\":null,\"status\":\"completed\",\"usage\":{\"input_tokens\":0,\"input_tokens_details\":{\"cached_tokens\":0},\"output_tokens\":6,\"output_tokens_details\":{\"reasoning_tokens\":2},\"total_tokens\":6}},\"sequence_number\":10,\"type\":\"response.completed\"}
47+
48+
data: [DONE]
49+
50+
"
51+
`;
52+

0 commit comments

Comments
 (0)