Skip to content

Commit e3376ee

Browse files
authored
feat(node): Instrument stream responses for openai (#17110)
This adds support for OpenAI streaming responses in the Node.js SDK What's new - Streaming Chat Completions: Support for stream: true in chat.completions.create() - Streaming Responses API: Support for streaming in the new OpenAI Responses API ``` // Streaming chat completions - automatically instrumented const stream = await openai.chat.completions.create({ model: 'gpt-4', messages: [...], stream: true }); // Streaming responses API - automatically instrumented const responseStream = await openai.responses.create({ model: 'gpt-4', input: 'Hello', stream: true }); ```
1 parent 87b4e30 commit e3376ee

File tree

9 files changed

+926
-92
lines changed

9 files changed

+926
-92
lines changed

dev-packages/node-integration-tests/suites/tracing/openai/scenario.mjs

Lines changed: 215 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@ class MockOpenAI {
1818
throw error;
1919
}
2020

21+
// If stream is requested, return an async generator
22+
if (params.stream) {
23+
return this._createChatCompletionStream(params);
24+
}
25+
2126
return {
2227
id: 'chatcmpl-mock123',
2328
object: 'chat.completion',
@@ -48,14 +53,19 @@ class MockOpenAI {
4853
create: async params => {
4954
await new Promise(resolve => setTimeout(resolve, 10));
5055

56+
// If stream is requested, return an async generator
57+
if (params.stream) {
58+
return this._createResponsesApiStream(params);
59+
}
60+
5161
return {
5262
id: 'resp_mock456',
5363
object: 'response',
54-
created: 1677652290,
64+
created_at: 1677652290,
5565
model: params.model,
5666
input_text: params.input,
5767
output_text: `Response to: ${params.input}`,
58-
finish_reason: 'stop',
68+
status: 'completed',
5969
usage: {
6070
input_tokens: 5,
6171
output_tokens: 8,
@@ -65,6 +75,163 @@ class MockOpenAI {
6575
},
6676
};
6777
}
78+
79+
// Create a mock streaming response for chat completions
80+
async *_createChatCompletionStream(params) {
81+
// First chunk with basic info
82+
yield {
83+
id: 'chatcmpl-stream-123',
84+
object: 'chat.completion.chunk',
85+
created: 1677652300,
86+
model: params.model,
87+
system_fingerprint: 'fp_stream_123',
88+
choices: [
89+
{
90+
index: 0,
91+
delta: {
92+
role: 'assistant',
93+
content: 'Hello',
94+
},
95+
finish_reason: null,
96+
},
97+
],
98+
};
99+
100+
// Second chunk with more content
101+
yield {
102+
id: 'chatcmpl-stream-123',
103+
object: 'chat.completion.chunk',
104+
created: 1677652300,
105+
model: params.model,
106+
system_fingerprint: 'fp_stream_123',
107+
choices: [
108+
{
109+
index: 0,
110+
delta: {
111+
content: ' from OpenAI streaming!',
112+
},
113+
finish_reason: 'stop',
114+
},
115+
],
116+
usage: {
117+
prompt_tokens: 12,
118+
completion_tokens: 18,
119+
total_tokens: 30,
120+
completion_tokens_details: {
121+
accepted_prediction_tokens: 0,
122+
audio_tokens: 0,
123+
reasoning_tokens: 0,
124+
rejected_prediction_tokens: 0,
125+
},
126+
prompt_tokens_details: {
127+
audio_tokens: 0,
128+
cached_tokens: 0,
129+
},
130+
},
131+
};
132+
}
133+
134+
// Create a mock streaming response for responses API
135+
async *_createResponsesApiStream(params) {
136+
// Response created event
137+
yield {
138+
type: 'response.created',
139+
response: {
140+
id: 'resp_stream_456',
141+
object: 'response',
142+
created_at: 1677652310,
143+
model: params.model,
144+
status: 'in_progress',
145+
error: null,
146+
incomplete_details: null,
147+
instructions: params.instructions,
148+
max_output_tokens: 1000,
149+
parallel_tool_calls: false,
150+
previous_response_id: null,
151+
reasoning: {
152+
effort: null,
153+
summary: null,
154+
},
155+
store: false,
156+
temperature: 0.7,
157+
text: {
158+
format: {
159+
type: 'text',
160+
},
161+
},
162+
tool_choice: 'auto',
163+
tools: [],
164+
top_p: 1.0,
165+
truncation: 'disabled',
166+
user: null,
167+
metadata: {},
168+
output: [],
169+
output_text: '',
170+
usage: {
171+
input_tokens: 0,
172+
output_tokens: 0,
173+
total_tokens: 0,
174+
},
175+
},
176+
sequence_number: 1,
177+
};
178+
179+
// Response in progress with output text delta
180+
yield {
181+
type: 'response.output_text.delta',
182+
delta: 'Streaming response to: ',
183+
sequence_number: 2,
184+
};
185+
186+
yield {
187+
type: 'response.output_text.delta',
188+
delta: params.input,
189+
sequence_number: 3,
190+
};
191+
192+
// Response completed event
193+
yield {
194+
type: 'response.completed',
195+
response: {
196+
id: 'resp_stream_456',
197+
object: 'response',
198+
created_at: 1677652310,
199+
model: params.model,
200+
status: 'completed',
201+
error: null,
202+
incomplete_details: null,
203+
instructions: params.instructions,
204+
max_output_tokens: 1000,
205+
parallel_tool_calls: false,
206+
previous_response_id: null,
207+
reasoning: {
208+
effort: null,
209+
summary: null,
210+
},
211+
store: false,
212+
temperature: 0.7,
213+
text: {
214+
format: {
215+
type: 'text',
216+
},
217+
},
218+
tool_choice: 'auto',
219+
tools: [],
220+
top_p: 1.0,
221+
truncation: 'disabled',
222+
user: null,
223+
metadata: {},
224+
output: [],
225+
output_text: params.input,
226+
usage: {
227+
input_tokens: 6,
228+
output_tokens: 10,
229+
total_tokens: 16,
230+
},
231+
},
232+
sequence_number: 4,
233+
};
234+
}
68235
}
69236

70237
async function run() {
@@ -93,7 +260,7 @@ async function run() {
93260
instructions: 'You are a translator',
94261
});
95262

96-
// Third test: error handling
263+
// Third test: error handling in chat completions
97264
try {
98265
await client.chat.completions.create({
99266
model: 'error-model',
@@ -102,6 +269,51 @@ async function run() {
102269
} catch {
103270
// Error is expected and handled
104271
}
272+
273+
// Fourth test: chat completions streaming
274+
const stream1 = await client.chat.completions.create({
275+
model: 'gpt-4',
276+
messages: [
277+
{ role: 'system', content: 'You are a helpful assistant.' },
278+
{ role: 'user', content: 'Tell me about streaming' },
279+
],
280+
stream: true,
281+
temperature: 0.8,
282+
});
283+
284+
// Consume the stream to trigger span instrumentation
285+
for await (const chunk of stream1) {
286+
// Stream chunks are processed automatically by instrumentation
287+
void chunk; // Prevent unused variable warning
288+
}
289+
290+
// Fifth test: responses API streaming
291+
const stream2 = await client.responses.create({
292+
model: 'gpt-4',
293+
input: 'Test streaming responses API',
294+
instructions: 'You are a streaming assistant',
295+
stream: true,
296+
});
297+
298+
for await (const chunk of stream2) {
299+
void chunk;
300+
}
301+
302+
// Sixth test: error handling in streaming context
303+
try {
304+
const errorStream = await client.chat.completions.create({
305+
model: 'error-model',
306+
messages: [{ role: 'user', content: 'This will fail' }],
307+
stream: true,
308+
});
309+
310+
// Try to consume the stream (this should not execute)
311+
for await (const chunk of errorStream) {
312+
void chunk;
313+
}
314+
} catch {
315+
// Error is expected and handled
316+
}
105317
});
106318
}
107319

0 commit comments

Comments
 (0)