Add async iterable support to ChatClient.completeStreamingChat() and AudioClient.transcribeStreaming()#521
Add async iterable support to ChatClient.completeStreamingChat() and AudioClient.transcribeStreaming()#521
ChatClient.completeStreamingChat() and AudioClient.transcribeStreaming()#521Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Co-authored-by: baijumeswani <12852605+baijumeswani@users.noreply.github.com>
ChatClient.completeStreamingChat()
prathikr
left a comment
There was a problem hiding this comment.
does audio client also need this change?
sdk_v2/js/src/openai/chatClient.ts
Outdated
| * } | ||
| * ``` | ||
| * | ||
| * Or with the callback pattern (for backward compatibility): |
There was a problem hiding this comment.
we don't need to call out "backward compatibility" since it appears we are going to support both methods moving forward
There was a problem hiding this comment.
Done in b47f975 — removed all "backward-compatible" / "backward compatibility" language from the JSDoc overload descriptions in chatClient.ts and ChatClient.md.
| * @returns An async iterable that yields each chunk of the streaming response. | ||
| * @throws Error - If messages or tools are invalid, or streaming fails. | ||
| */ | ||
| public completeStreamingChat(messages: any[]): AsyncIterable<any>; |
There was a problem hiding this comment.
does c# need this change as well? will also need to note this for impending rust/python SDKs
There was a problem hiding this comment.
The C# SDK already returns IAsyncEnumerable<T> from CompleteChatStreamingAsync() (and CompleteAudioStreamingAsync()), which is C#'s equivalent of the async iterable pattern — so no changes needed there. Agreed that the Rust and Python SDKs should follow the same convention when they add streaming APIs.
…anguage Co-authored-by: baijumeswani <12852605+baijumeswani@users.noreply.github.com>
Yes — applied the same async iterable pattern to // async iterable
for await (const chunk of audioClient.transcribeStreaming(filePath)) { ... }
// callback
await audioClient.transcribeStreaming(filePath, (chunk) => { ... }); |
ChatClient.completeStreamingChat()ChatClient.completeStreamingChat() and AudioClient.transcribeStreaming()
There was a problem hiding this comment.
Pull request overview
Adds async-iterable support to the JS SDK streaming APIs so callers can use the familiar for await ... of pattern (matching the OpenAI SDK) while preserving the existing callback overloads.
Changes:
- Added async-iterable overloads for
ChatClient.completeStreamingChat()andAudioClient.transcribeStreaming(), backed by internal async generators bridging the native callback stream. - Refactored the prior callback implementations into
_completeStreamingChatWithCallback()/_transcribeStreamingWithCallback(). - Added/updated integration + validation tests, docs, and updated the JS sample app to demonstrate both patterns.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| sdk_v2/js/src/openai/chatClient.ts | Adds async-iterable overloads + _streamChat() generator and dispatch logic. |
| sdk_v2/js/src/openai/audioClient.ts | Adds async-iterable overload + _transcribeStream() generator and dispatch logic. |
| sdk_v2/js/test/openai/chatClient.test.ts | Adds async-iterable integration test; expands validation coverage. |
| sdk_v2/js/test/openai/audioClient.test.ts | Adds async-iterable integration test; expands validation coverage. |
| sdk_v2/js/docs/classes/ChatClient.md | Documents new overloads and usage examples. |
| sdk_v2/js/docs/classes/AudioClient.md | Documents new overload and usage examples. |
| samples/js/native-chat-completions/app.js | Updates sample to show async-iterable + callback usage. |
Comments suppressed due to low confidence (1)
samples/js/native-chat-completions/app.js:62
- The callback streaming example is extracting
chunk.choices?.[0]?.message?.content, but the SDK’s streaming chunks (and the async-iterable example above, plus tests) usechoices[0].delta.content. Usingmessage.contenthere will likely print nothing. Update the callback sample to read fromdelta.contentfor consistency.
await chatClient.completeStreamingChat(
[{ role: 'user', content: 'Write a short poem about nature.' }],
(chunk) => {
const content = chunk.choices?.[0]?.message?.content;
if (content) {
process.stdout.write(content);
}
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // Test with async iterable pattern | ||
| try { | ||
| const iter = audioClient.transcribeStreaming(''); | ||
| await iter[Symbol.asyncIterator]().next(); |
| const tools = Array.isArray(toolsOrCallback) ? toolsOrCallback : undefined; | ||
| const callback = (Array.isArray(toolsOrCallback) ? maybeCallback : toolsOrCallback) as ((chunk: any) => void) | undefined; | ||
| const callback = typeof toolsOrCallback === 'function' ? toolsOrCallback : maybeCallback; | ||
|
|
||
| this.validateMessages(messages); | ||
| this.validateTools(tools); | ||
|
|
||
| if (!callback || typeof callback !== 'function') { | ||
| throw new Error('Callback must be a valid function.'); | ||
| if (callback !== undefined) { | ||
| if (typeof callback !== 'function') { | ||
| throw new Error('Callback must be a valid function.'); | ||
| } | ||
| return this._completeStreamingChatWithCallback(messages, tools, callback); | ||
| } | ||
|
|
||
| return this._streamChat(messages, tools); |
| const chunks: any[] = []; | ||
| let streamDone = false; | ||
| let streamError: Error | null = null; | ||
| let notify: (() => void) | null = null; | ||
|
|
||
| const wakeConsumer = () => { | ||
| if (notify) { const n = notify; notify = null; n(); } | ||
| }; | ||
|
|
||
| const streamPromise = this.coreInterop.executeCommandStreaming( | ||
| 'chat_completions', | ||
| { Params: { OpenAICreateRequest: JSON.stringify(request) } }, | ||
| (chunkStr: string) => { | ||
| // Skip processing if we already encountered an error | ||
| if (streamError) return; | ||
|
|
||
| if (chunkStr) { | ||
| let chunk: any; | ||
| try { | ||
| chunk = JSON.parse(chunkStr); | ||
| } catch (e) { | ||
| // Don't throw from callback - store first error and stop processing | ||
| streamError = new Error( | ||
| `Failed to parse streaming chunk: ${e instanceof Error ? e.message : String(e)}`, | ||
| { cause: e } | ||
| ); | ||
| wakeConsumer(); | ||
| return; | ||
| } | ||
| chunks.push(chunk); | ||
| wakeConsumer(); | ||
| } | ||
| } | ||
| ).then(() => { | ||
| streamDone = true; | ||
| wakeConsumer(); | ||
| }).catch((err: unknown) => { | ||
| streamError = err instanceof Error ? err : new Error(String(err)); | ||
| streamDone = true; | ||
| wakeConsumer(); | ||
| }); | ||
|
|
||
| try { | ||
| while (!streamDone && !streamError) { | ||
| while (chunks.length > 0) { | ||
| yield chunks.shift()!; | ||
| } | ||
| if (!streamDone && !streamError) { | ||
| await new Promise<void>(resolve => { notify = resolve; }); | ||
| } | ||
| } | ||
| // Drain any remaining chunks that arrived before streamDone was observed | ||
| while (chunks.length > 0) { | ||
| yield chunks.shift()!; | ||
| } |
| const chunks: any[] = []; | ||
| let streamDone = false; | ||
| let streamError: Error | null = null; | ||
| let notify: (() => void) | null = null; | ||
|
|
||
| const wakeConsumer = () => { | ||
| if (notify) { const n = notify; notify = null; n(); } | ||
| }; | ||
|
|
||
| const streamPromise = this.coreInterop.executeCommandStreaming( | ||
| "audio_transcribe", | ||
| { Params: { OpenAICreateRequest: JSON.stringify(request) } }, | ||
| (chunkStr: string) => { | ||
| // Skip processing if we already encountered an error | ||
| if (streamError) return; | ||
|
|
||
| if (chunkStr) { | ||
| let chunk: any; | ||
| try { | ||
| chunk = JSON.parse(chunkStr); | ||
| } catch (e) { | ||
| // Don't throw from callback - store first error and stop processing | ||
| streamError = new Error( | ||
| `Failed to parse streaming chunk: ${e instanceof Error ? e.message : String(e)}`, | ||
| { cause: e } | ||
| ); | ||
| wakeConsumer(); | ||
| return; | ||
| } | ||
| chunks.push(chunk); | ||
| wakeConsumer(); | ||
| } | ||
| } | ||
| ).then(() => { | ||
| streamDone = true; | ||
| wakeConsumer(); | ||
| }).catch((err: unknown) => { | ||
| streamError = err instanceof Error ? err : new Error(String(err)); | ||
| streamDone = true; | ||
| wakeConsumer(); | ||
| }); | ||
|
|
||
| try { | ||
| while (!streamDone && !streamError) { | ||
| while (chunks.length > 0) { | ||
| yield chunks.shift()!; | ||
| } | ||
| if (!streamDone && !streamError) { | ||
| await new Promise<void>(resolve => { notify = resolve; }); | ||
| } | ||
| } | ||
| // Drain any remaining chunks that arrived before streamDone was observed | ||
| while (chunks.length > 0) { | ||
| yield chunks.shift()!; | ||
| } |
| // Trigger iteration to materialize the error | ||
| const iter = client.completeStreamingChat(invalidMessage); | ||
| await iter[Symbol.asyncIterator]().next(); |
completeStreamingChat()andtranscribeStreaming()only accepted callbacks, causing confusing runtime errors when developers used the OpenAI SDK's familiarfor awaitpattern. This adds async iterable support to both clients while keeping the callback overloads.API changes
ChatClient.completeStreamingChat()Two new overloads return
AsyncIterable<any>when no callback is passed:AudioClient.transcribeStreaming()Same pattern applied:
Implementation
_streamChat()— private async generator that bridgesexecuteCommandStreaming's native callback to an async iterable via a chunk queue +wakeConsumernotify pattern.await streamPromisein thefinallyblock ensures native resources are cleaned up even on early iterator exit._completeStreamingChatWithCallback()— extracted from the originalcompleteStreamingChat()implementation; behavior unchanged._transcribeStream()— same async generator pattern for audio streaming._transcribeStreamingWithCallback()— extracted from the originaltranscribeStreaming()implementation; behavior unchanged.Other
ChatClient.md,AudioClient.md) and sample app updated to reflect all overloads.IAsyncEnumerable<T>from its streaming methods — no changes needed there.Original prompt
💬 Send tasks to Copilot coding agent from Slack and Teams to turn conversations into code. Copilot posts an update in your thread when it's finished.