diff --git a/README.md b/README.md index 14c53229..07bc9b4d 100644 --- a/README.md +++ b/README.md @@ -232,9 +232,9 @@ const result = await audioClient.transcribe('recording.wav'); console.log('Transcription:', result.text); // Or stream in real-time -await audioClient.transcribeStreaming('recording.wav', (chunk) => { +for await (const chunk of audioClient.transcribeStreaming('recording.wav')) { process.stdout.write(chunk.text); -}); +} await whisperModel.unload(); ``` diff --git a/samples/js/audio-transcription-example/app.js b/samples/js/audio-transcription-example/app.js index fe441d1b..78efc8af 100644 --- a/samples/js/audio-transcription-example/app.js +++ b/samples/js/audio-transcription-example/app.js @@ -39,12 +39,12 @@ console.log('\nAudio transcription result:'); console.log(transcription.text); console.log('✓ Audio transcription completed'); -// Same example but with streaming transcription using callback +// Same example but with streaming transcription using async iteration console.log('\nTesting streaming audio transcription...'); -await audioClient.transcribeStreaming('./Recording.mp3', (result) => { +for await (const result of audioClient.transcribeStreaming('./Recording.mp3')) { // Output the intermediate transcription results as they are received without line ending process.stdout.write(result.text); -}); +} console.log('\n✓ Streaming transcription completed'); // Unload the model diff --git a/samples/js/chat-and-audio-foundry-local/src/app.js b/samples/js/chat-and-audio-foundry-local/src/app.js index b3084816..49ce199c 100644 --- a/samples/js/chat-and-audio-foundry-local/src/app.js +++ b/samples/js/chat-and-audio-foundry-local/src/app.js @@ -76,22 +76,19 @@ async function main() { // Summarize the transcription console.log("Generating summary...\n"); - await chatClient.completeStreamingChat( - [ - { - role: "system", - content: - "You are a helpful assistant. Summarize the following transcribed audio and extract key themes and action items.", - }, - { role: "user", content: transcription.text }, - ], - (chunk) => { - const content = chunk.choices?.[0]?.message?.content; - if (content) { - process.stdout.write(content); - } + for await (const chunk of chatClient.completeStreamingChat([ + { + role: "system", + content: + "You are a helpful assistant. Summarize the following transcribed audio and extract key themes and action items.", + }, + { role: "user", content: transcription.text }, + ])) { + const content = chunk.choices?.[0]?.message?.content; + if (content) { + process.stdout.write(content); } - ); + } console.log("\n"); // --- Clean up --- diff --git a/samples/js/native-chat-completions/app.js b/samples/js/native-chat-completions/app.js index af566ef7..67348e8c 100644 --- a/samples/js/native-chat-completions/app.js +++ b/samples/js/native-chat-completions/app.js @@ -41,15 +41,14 @@ console.log(completion.choices[0]?.message?.content); // Example streaming completion console.log('\nTesting streaming completion...'); -await chatClient.completeStreamingChat( - [{ role: 'user', content: 'Write a short poem about programming.' }], - (chunk) => { - const content = chunk.choices?.[0]?.message?.content; - if (content) { - process.stdout.write(content); - } +for await (const chunk of chatClient.completeStreamingChat( + [{ role: 'user', content: 'Write a short poem about programming.' }] +)) { + const content = chunk.choices?.[0]?.message?.content; + if (content) { + process.stdout.write(content); } -); +} console.log('\n'); // Unload the model diff --git a/sdk/js/README.md b/sdk/js/README.md index 3308c9d8..9b08f9ac 100644 --- a/sdk/js/README.md +++ b/sdk/js/README.md @@ -69,15 +69,14 @@ console.log(completion.choices[0]?.message?.content); // Example streaming completion console.log('\nTesting streaming completion...'); -await chatClient.completeStreamingChat( - [{ role: 'user', content: 'Write a short poem about programming.' }], - (chunk) => { - const content = chunk.choices?.[0]?.message?.content; - if (content) { - process.stdout.write(content); - } +for await (const chunk of chatClient.completeStreamingChat( + [{ role: 'user', content: 'Write a short poem about programming.' }] +)) { + const content = chunk.choices?.[0]?.message?.content; + if (content) { + process.stdout.write(content); } -); +} console.log('\n'); // Unload the model @@ -157,15 +156,14 @@ console.log(response.choices[0].message.content); For real-time output, use streaming: ```typescript -await chatClient.completeStreamingChat( - [{ role: 'user', content: 'Write a short poem about programming.' }], - (chunk) => { - const content = chunk.choices?.[0]?.message?.content; - if (content) { - process.stdout.write(content); - } +for await (const chunk of chatClient.completeStreamingChat( + [{ role: 'user', content: 'Write a short poem about programming.' }] +)) { + const content = chunk.choices?.[0]?.message?.content; + if (content) { + process.stdout.write(content); } -); +} ``` ### Audio Transcription @@ -180,9 +178,9 @@ audioClient.settings.language = 'en'; const result = await audioClient.transcribe('/path/to/audio.wav'); // Streaming transcription -await audioClient.transcribeStreaming('/path/to/audio.wav', (chunk) => { +for await (const chunk of audioClient.transcribeStreaming('/path/to/audio.wav')) { console.log(chunk); -}); +} ``` ### Embedded Web Service diff --git a/sdk/js/docs/README.md b/sdk/js/docs/README.md index e79be84d..58218628 100644 --- a/sdk/js/docs/README.md +++ b/sdk/js/docs/README.md @@ -163,7 +163,7 @@ Use a plain object with these properties to configure the SDK. ##### additionalSettings? ```ts -optional additionalSettings: { +optional additionalSettings?: { [key: string]: string; }; ``` @@ -180,7 +180,7 @@ Optional. Internal use only. ##### appDataDir? ```ts -optional appDataDir: string; +optional appDataDir?: string; ``` The directory where application data should be stored. @@ -198,7 +198,7 @@ Used for identifying the application in logs and telemetry. ##### libraryPath? ```ts -optional libraryPath: string; +optional libraryPath?: string; ``` The path to the directory containing the native Foundry Local Core libraries. @@ -208,7 +208,7 @@ If not provided, the SDK attempts to discover them in standard locations. ##### logLevel? ```ts -optional logLevel: "trace" | "debug" | "info" | "warn" | "error" | "fatal"; +optional logLevel?: "trace" | "debug" | "info" | "warn" | "error" | "fatal"; ``` The logging level for the SDK. @@ -218,7 +218,7 @@ Defaults to 'warn'. ##### logsDir? ```ts -optional logsDir: string; +optional logsDir?: string; ``` The directory where log files are written. @@ -227,7 +227,7 @@ Optional. Defaults to `{appDataDir}/logs`. ##### modelCacheDir? ```ts -optional modelCacheDir: string; +optional modelCacheDir?: string; ``` The directory where models are downloaded and cached. @@ -236,7 +236,7 @@ Optional. Defaults to `{appDataDir}/cache/models`. ##### serviceEndpoint? ```ts -optional serviceEndpoint: string; +optional serviceEndpoint?: string; ``` The external URL if the web service is running in a separate process. @@ -245,7 +245,7 @@ Optional. This is used to connect to an existing service instance. ##### webServiceUrls? ```ts -optional webServiceUrls: string; +optional webServiceUrls?: string; ``` The URL(s) for the local web service to bind to. @@ -351,7 +351,7 @@ call_id: string; ##### id? ```ts -optional id: string; +optional id?: string; ``` ##### name @@ -363,7 +363,7 @@ name: string; ##### status? ```ts -optional status: ResponseItemStatus; +optional status?: ResponseItemStatus; ``` ##### type @@ -387,7 +387,7 @@ call_id: string; ##### id? ```ts -optional id: string; +optional id?: string; ``` ##### output @@ -399,7 +399,7 @@ output: string | ContentPart[]; ##### status? ```ts -optional status: ResponseItemStatus; +optional status?: ResponseItemStatus; ``` ##### type @@ -417,7 +417,7 @@ type: "function_call_output"; ##### description? ```ts -optional description: string; +optional description?: string; ``` ##### name @@ -429,13 +429,13 @@ name: string; ##### parameters? ```ts -optional parameters: Record; +optional parameters?: Record; ``` ##### strict? ```ts -optional strict: boolean; +optional strict?: boolean; ``` ##### type @@ -671,7 +671,7 @@ type: "item_reference"; ##### bytes? ```ts -optional bytes: number[]; +optional bytes?: number[]; ``` ##### logprob @@ -701,7 +701,7 @@ content: string | ContentPart[]; ##### id? ```ts -optional id: string; +optional id?: string; ``` ##### role @@ -713,7 +713,7 @@ role: MessageRole; ##### status? ```ts -optional status: ResponseItemStatus; +optional status?: ResponseItemStatus; ``` ##### type @@ -749,13 +749,13 @@ createdAtUnix: number; ##### displayName? ```ts -optional displayName: string | null; +optional displayName?: string | null; ``` ##### fileSizeMb? ```ts -optional fileSizeMb: number | null; +optional fileSizeMb?: number | null; ``` ##### id @@ -767,31 +767,31 @@ id: string; ##### license? ```ts -optional license: string | null; +optional license?: string | null; ``` ##### licenseDescription? ```ts -optional licenseDescription: string | null; +optional licenseDescription?: string | null; ``` ##### maxOutputTokens? ```ts -optional maxOutputTokens: number | null; +optional maxOutputTokens?: number | null; ``` ##### minFLVersion? ```ts -optional minFLVersion: string | null; +optional minFLVersion?: string | null; ``` ##### modelSettings? ```ts -optional modelSettings: ModelSettings | null; +optional modelSettings?: ModelSettings | null; ``` ##### modelType @@ -809,7 +809,7 @@ name: string; ##### promptTemplate? ```ts -optional promptTemplate: PromptTemplate | null; +optional promptTemplate?: PromptTemplate | null; ``` ##### providerType @@ -821,25 +821,25 @@ providerType: string; ##### publisher? ```ts -optional publisher: string | null; +optional publisher?: string | null; ``` ##### runtime? ```ts -optional runtime: Runtime | null; +optional runtime?: Runtime | null; ``` ##### supportsToolCalling? ```ts -optional supportsToolCalling: boolean | null; +optional supportsToolCalling?: boolean | null; ``` ##### task? ```ts -optional task: string | null; +optional task?: string | null; ``` ##### uri @@ -863,7 +863,7 @@ version: number; ##### parameters? ```ts -optional parameters: Parameter[] | null; +optional parameters?: Parameter[] | null; ``` *** @@ -947,13 +947,13 @@ type: "response.output_item.done"; ##### annotations? ```ts -optional annotations: Annotation[]; +optional annotations?: Annotation[]; ``` ##### logprobs? ```ts -optional logprobs: LogProb[]; +optional logprobs?: LogProb[]; ``` ##### text @@ -1067,7 +1067,7 @@ name: string; ##### value? ```ts -optional value: string | null; +optional value?: string | null; ``` *** @@ -1091,13 +1091,13 @@ prompt: string; ##### system? ```ts -optional system: string | null; +optional system?: string | null; ``` ##### user? ```ts -optional user: string | null; +optional user?: string | null; ``` *** @@ -1109,13 +1109,13 @@ optional user: string | null; ##### effort? ```ts -optional effort: string; +optional effort?: string; ``` ##### summary? ```ts -optional summary: string; +optional summary?: string; ``` *** @@ -1127,31 +1127,31 @@ optional summary: string; ##### content? ```ts -optional content: ContentPart[]; +optional content?: ContentPart[]; ``` ##### encrypted\_content? ```ts -optional encrypted_content: string; +optional encrypted_content?: string; ``` ##### id? ```ts -optional id: string; +optional id?: string; ``` ##### status? ```ts -optional status: ResponseItemStatus; +optional status?: ResponseItemStatus; ``` ##### summary? ```ts -optional summary: string; +optional summary?: string; ``` ##### type @@ -1259,121 +1259,121 @@ type: "response.refusal.done"; ##### frequency\_penalty? ```ts -optional frequency_penalty: number; +optional frequency_penalty?: number; ``` ##### input? ```ts -optional input: string | ResponseInputItem[]; +optional input?: string | ResponseInputItem[]; ``` ##### instructions? ```ts -optional instructions: string; +optional instructions?: string; ``` ##### max\_output\_tokens? ```ts -optional max_output_tokens: number; +optional max_output_tokens?: number; ``` ##### metadata? ```ts -optional metadata: Record; +optional metadata?: Record; ``` ##### model? ```ts -optional model: string; +optional model?: string; ``` ##### parallel\_tool\_calls? ```ts -optional parallel_tool_calls: boolean; +optional parallel_tool_calls?: boolean; ``` ##### presence\_penalty? ```ts -optional presence_penalty: number; +optional presence_penalty?: number; ``` ##### previous\_response\_id? ```ts -optional previous_response_id: string; +optional previous_response_id?: string; ``` ##### reasoning? ```ts -optional reasoning: ReasoningConfig; +optional reasoning?: ReasoningConfig; ``` ##### seed? ```ts -optional seed: number; +optional seed?: number; ``` ##### store? ```ts -optional store: boolean; +optional store?: boolean; ``` ##### stream? ```ts -optional stream: boolean; +optional stream?: boolean; ``` ##### temperature? ```ts -optional temperature: number; +optional temperature?: number; ``` ##### text? ```ts -optional text: TextConfig; +optional text?: TextConfig; ``` ##### tool\_choice? ```ts -optional tool_choice: ResponseToolChoice; +optional tool_choice?: ResponseToolChoice; ``` ##### tools? ```ts -optional tools: FunctionToolDefinition[]; +optional tools?: FunctionToolDefinition[]; ``` ##### top\_p? ```ts -optional top_p: number; +optional top_p?: number; ``` ##### truncation? ```ts -optional truncation: TruncationStrategy; +optional truncation?: TruncationStrategy; ``` ##### user? ```ts -optional user: string; +optional user?: string; ``` *** @@ -1403,13 +1403,13 @@ message: string; ##### jsonSchema? ```ts -optional jsonSchema: string; +optional jsonSchema?: string; ``` ##### larkGrammar? ```ts -optional larkGrammar: string; +optional larkGrammar?: string; ``` ##### type @@ -1457,13 +1457,13 @@ type: ##### cancelled\_at? ```ts -optional cancelled_at: number | null; +optional cancelled_at?: number | null; ``` ##### completed\_at? ```ts -optional completed_at: number | null; +optional completed_at?: number | null; ``` ##### created\_at @@ -1475,13 +1475,13 @@ created_at: number; ##### error? ```ts -optional error: ResponseError | null; +optional error?: ResponseError | null; ``` ##### failed\_at? ```ts -optional failed_at: number | null; +optional failed_at?: number | null; ``` ##### frequency\_penalty @@ -1499,25 +1499,25 @@ id: string; ##### incomplete\_details? ```ts -optional incomplete_details: IncompleteDetails | null; +optional incomplete_details?: IncompleteDetails | null; ``` ##### instructions? ```ts -optional instructions: string | null; +optional instructions?: string | null; ``` ##### max\_output\_tokens? ```ts -optional max_output_tokens: number | null; +optional max_output_tokens?: number | null; ``` ##### metadata? ```ts -optional metadata: Record | null; +optional metadata?: Record | null; ``` ##### model @@ -1553,13 +1553,13 @@ presence_penalty: number; ##### previous\_response\_id? ```ts -optional previous_response_id: string | null; +optional previous_response_id?: string | null; ``` ##### reasoning? ```ts -optional reasoning: ReasoningConfig | null; +optional reasoning?: ReasoningConfig | null; ``` ##### status @@ -1613,13 +1613,13 @@ truncation: TruncationStrategy; ##### usage? ```ts -optional usage: ResponseUsage | null; +optional usage?: ResponseUsage | null; ``` ##### user? ```ts -optional user: string | null; +optional user?: string | null; ``` *** @@ -1655,7 +1655,7 @@ input_tokens: number; ##### input\_tokens\_details? ```ts -optional input_tokens_details: { +optional input_tokens_details?: { cached_tokens: number; }; ``` @@ -1675,7 +1675,7 @@ output_tokens: number; ##### output\_tokens\_details? ```ts -optional output_tokens_details: { +optional output_tokens_details?: { reasoning_tokens: number; }; ``` @@ -1719,19 +1719,19 @@ executionProvider: string; ##### code? ```ts -optional code: string; +optional code?: string; ``` ##### message? ```ts -optional message: string; +optional message?: string; ``` ##### param? ```ts -optional param: string; +optional param?: string; ``` ##### sequence\_number @@ -1755,13 +1755,13 @@ type: "error"; ##### format? ```ts -optional format: TextFormat; +optional format?: TextFormat; ``` ##### verbosity? ```ts -optional verbosity: string; +optional verbosity?: string; ``` *** @@ -1773,25 +1773,25 @@ optional verbosity: string; ##### description? ```ts -optional description: string; +optional description?: string; ``` ##### name? ```ts -optional name: string; +optional name?: string; ``` ##### schema? ```ts -optional schema: unknown; +optional schema?: unknown; ``` ##### strict? ```ts -optional strict: boolean; +optional strict?: boolean; ``` ##### type @@ -1809,7 +1809,7 @@ type: string; ##### name? ```ts -optional name: string; +optional name?: string; ``` ##### type diff --git a/sdk/js/docs/classes/AudioClient.md b/sdk/js/docs/classes/AudioClient.md index 7fd13bd8..12e79de5 100644 --- a/sdk/js/docs/classes/AudioClient.md +++ b/sdk/js/docs/classes/AudioClient.md @@ -46,24 +46,31 @@ Error - If audioFilePath is invalid or transcription fails. ### transcribeStreaming() ```ts -transcribeStreaming(audioFilePath, callback): Promise; +transcribeStreaming(audioFilePath): AsyncIterable; ``` -Transcribes audio into the input language using streaming. +Transcribes audio into the input language using streaming, returning an async iterable of chunks. #### Parameters | Parameter | Type | Description | | ------ | ------ | ------ | | `audioFilePath` | `string` | Path to the audio file to transcribe. | -| `callback` | (`chunk`) => `void` | A callback function that receives each chunk of the streaming response. | #### Returns -`Promise`\<`void`\> +`AsyncIterable`\<`any`\> -A promise that resolves when the stream is complete. +An async iterable that yields parsed streaming transcription chunks. #### Throws -Error - If audioFilePath or callback are invalid, or streaming fails. +Error - If audioFilePath is invalid, or streaming fails. + +#### Example + +```typescript +for await (const chunk of audioClient.transcribeStreaming('recording.wav')) { + process.stdout.write(chunk.text); +} +``` diff --git a/sdk/js/docs/classes/AudioClientSettings.md b/sdk/js/docs/classes/AudioClientSettings.md index 619c526b..dae7cbbe 100644 --- a/sdk/js/docs/classes/AudioClientSettings.md +++ b/sdk/js/docs/classes/AudioClientSettings.md @@ -19,7 +19,7 @@ new AudioClientSettings(): AudioClientSettings; ### language? ```ts -optional language: string; +optional language?: string; ``` *** @@ -27,5 +27,5 @@ optional language: string; ### temperature? ```ts -optional temperature: number; +optional temperature?: number; ``` diff --git a/sdk/js/docs/classes/ChatClient.md b/sdk/js/docs/classes/ChatClient.md index 91e877aa..c3120f0b 100644 --- a/sdk/js/docs/classes/ChatClient.md +++ b/sdk/js/docs/classes/ChatClient.md @@ -75,53 +75,80 @@ Error - If messages or tools are invalid or completion fails. #### Call Signature ```ts -completeStreamingChat(messages, callback): Promise; +completeStreamingChat(messages): AsyncIterable; ``` -Performs a streaming chat completion. +Performs a streaming chat completion, returning an async iterable of chunks. ##### Parameters | Parameter | Type | Description | | ------ | ------ | ------ | | `messages` | `any`[] | An array of message objects. | -| `callback` | (`chunk`) => `void` | A callback function that receives each chunk of the streaming response. | ##### Returns -`Promise`\<`void`\> +`AsyncIterable`\<`any`\> -A promise that resolves when the stream is complete. +An async iterable that yields parsed streaming response chunks. ##### Throws -Error - If messages, tools, or callback are invalid, or streaming fails. +Error - If messages or tools are invalid, or streaming fails. + +##### Example + +```typescript +// Without tools: +for await (const chunk of chatClient.completeStreamingChat(messages)) { + const content = chunk.choices?.[0]?.delta?.content; + if (content) process.stdout.write(content); +} + +// With tools: +for await (const chunk of chatClient.completeStreamingChat(messages, tools)) { + const content = chunk.choices?.[0]?.delta?.content; + if (content) process.stdout.write(content); +} +``` #### Call Signature ```ts -completeStreamingChat( - messages, - tools, -callback): Promise; +completeStreamingChat(messages, tools): AsyncIterable; ``` -Performs a streaming chat completion. +Performs a streaming chat completion, returning an async iterable of chunks. ##### Parameters | Parameter | Type | Description | | ------ | ------ | ------ | | `messages` | `any`[] | An array of message objects. | -| `tools` | `any`[] | An array of tool objects. | -| `callback` | (`chunk`) => `void` | A callback function that receives each chunk of the streaming response. | +| `tools` | `any`[] | An optional array of tool objects. | ##### Returns -`Promise`\<`void`\> +`AsyncIterable`\<`any`\> -A promise that resolves when the stream is complete. +An async iterable that yields parsed streaming response chunks. ##### Throws -Error - If messages, tools, or callback are invalid, or streaming fails. +Error - If messages or tools are invalid, or streaming fails. + +##### Example + +```typescript +// Without tools: +for await (const chunk of chatClient.completeStreamingChat(messages)) { + const content = chunk.choices?.[0]?.delta?.content; + if (content) process.stdout.write(content); +} + +// With tools: +for await (const chunk of chatClient.completeStreamingChat(messages, tools)) { + const content = chunk.choices?.[0]?.delta?.content; + if (content) process.stdout.write(content); +} +``` diff --git a/sdk/js/docs/classes/ChatClientSettings.md b/sdk/js/docs/classes/ChatClientSettings.md index 7fed8a46..7d48bcca 100644 --- a/sdk/js/docs/classes/ChatClientSettings.md +++ b/sdk/js/docs/classes/ChatClientSettings.md @@ -19,7 +19,7 @@ new ChatClientSettings(): ChatClientSettings; ### frequencyPenalty? ```ts -optional frequencyPenalty: number; +optional frequencyPenalty?: number; ``` *** @@ -27,7 +27,7 @@ optional frequencyPenalty: number; ### maxTokens? ```ts -optional maxTokens: number; +optional maxTokens?: number; ``` *** @@ -35,7 +35,7 @@ optional maxTokens: number; ### n? ```ts -optional n: number; +optional n?: number; ``` *** @@ -43,7 +43,7 @@ optional n: number; ### presencePenalty? ```ts -optional presencePenalty: number; +optional presencePenalty?: number; ``` *** @@ -51,7 +51,7 @@ optional presencePenalty: number; ### randomSeed? ```ts -optional randomSeed: number; +optional randomSeed?: number; ``` *** @@ -59,7 +59,7 @@ optional randomSeed: number; ### responseFormat? ```ts -optional responseFormat: ResponseFormat; +optional responseFormat?: ResponseFormat; ``` *** @@ -67,7 +67,7 @@ optional responseFormat: ResponseFormat; ### temperature? ```ts -optional temperature: number; +optional temperature?: number; ``` *** @@ -75,7 +75,7 @@ optional temperature: number; ### toolChoice? ```ts -optional toolChoice: ToolChoice; +optional toolChoice?: ToolChoice; ``` *** @@ -83,7 +83,7 @@ optional toolChoice: ToolChoice; ### topK? ```ts -optional topK: number; +optional topK?: number; ``` *** @@ -91,5 +91,5 @@ optional topK: number; ### topP? ```ts -optional topP: number; +optional topP?: number; ``` diff --git a/sdk/js/docs/classes/Model.md b/sdk/js/docs/classes/Model.md index 48340dae..424d673b 100644 --- a/sdk/js/docs/classes/Model.md +++ b/sdk/js/docs/classes/Model.md @@ -156,7 +156,7 @@ Automatically selects the new variant if it is cached and the current one is not #### Throws -Error - If the variant's alias does not match the model's alias. +Error - If the argument is not a ModelVariant object, or if the variant's alias does not match the model's alias. *** diff --git a/sdk/js/docs/classes/ResponsesClientSettings.md b/sdk/js/docs/classes/ResponsesClientSettings.md index 08b9ea94..8401faf1 100644 --- a/sdk/js/docs/classes/ResponsesClientSettings.md +++ b/sdk/js/docs/classes/ResponsesClientSettings.md @@ -22,7 +22,7 @@ new ResponsesClientSettings(): ResponsesClientSettings; ### frequencyPenalty? ```ts -optional frequencyPenalty: number; +optional frequencyPenalty?: number; ``` *** @@ -30,7 +30,7 @@ optional frequencyPenalty: number; ### instructions? ```ts -optional instructions: string; +optional instructions?: string; ``` System-level instructions to guide the model. @@ -40,7 +40,7 @@ System-level instructions to guide the model. ### maxOutputTokens? ```ts -optional maxOutputTokens: number; +optional maxOutputTokens?: number; ``` *** @@ -48,7 +48,7 @@ optional maxOutputTokens: number; ### metadata? ```ts -optional metadata: Record; +optional metadata?: Record; ``` *** @@ -56,7 +56,7 @@ optional metadata: Record; ### parallelToolCalls? ```ts -optional parallelToolCalls: boolean; +optional parallelToolCalls?: boolean; ``` *** @@ -64,7 +64,7 @@ optional parallelToolCalls: boolean; ### presencePenalty? ```ts -optional presencePenalty: number; +optional presencePenalty?: number; ``` *** @@ -72,7 +72,7 @@ optional presencePenalty: number; ### reasoning? ```ts -optional reasoning: ReasoningConfig; +optional reasoning?: ReasoningConfig; ``` *** @@ -80,7 +80,7 @@ optional reasoning: ReasoningConfig; ### seed? ```ts -optional seed: number; +optional seed?: number; ``` *** @@ -88,7 +88,7 @@ optional seed: number; ### store? ```ts -optional store: boolean; +optional store?: boolean; ``` *** @@ -96,7 +96,7 @@ optional store: boolean; ### temperature? ```ts -optional temperature: number; +optional temperature?: number; ``` *** @@ -104,7 +104,7 @@ optional temperature: number; ### text? ```ts -optional text: TextConfig; +optional text?: TextConfig; ``` *** @@ -112,7 +112,7 @@ optional text: TextConfig; ### toolChoice? ```ts -optional toolChoice: ResponseToolChoice; +optional toolChoice?: ResponseToolChoice; ``` *** @@ -120,7 +120,7 @@ optional toolChoice: ResponseToolChoice; ### topP? ```ts -optional topP: number; +optional topP?: number; ``` *** @@ -128,5 +128,5 @@ optional topP: number; ### truncation? ```ts -optional truncation: TruncationStrategy; +optional truncation?: TruncationStrategy; ``` diff --git a/sdk/js/examples/audio-transcription.ts b/sdk/js/examples/audio-transcription.ts index 7fddf2d8..4e4fc2d4 100644 --- a/sdk/js/examples/audio-transcription.ts +++ b/sdk/js/examples/audio-transcription.ts @@ -72,9 +72,9 @@ async function main() { // Example: Streaming transcription console.log('\nTesting streaming transcription...'); - await audioClient.transcribeStreaming(audioFilePath, (chunk: any) => { + for await (const chunk of audioClient.transcribeStreaming(audioFilePath)) { process.stdout.write(chunk.text); - }); + } console.log('\n'); // Unload the model diff --git a/sdk/js/examples/chat-completion.ts b/sdk/js/examples/chat-completion.ts index 2c283e23..a9e2d59a 100644 --- a/sdk/js/examples/chat-completion.ts +++ b/sdk/js/examples/chat-completion.ts @@ -70,15 +70,14 @@ async function main() { // Example streaming completion console.log('\nTesting streaming completion...'); - await chatClient.completeStreamingChat( - [{ role: 'user', content: 'Write a short poem about programming.' }], - (chunk) => { - const content = chunk.choices?.[0]?.message?.content; - if (content) { - process.stdout.write(content); - } + for await (const chunk of chatClient.completeStreamingChat( + [{ role: 'user', content: 'Write a short poem about programming.' }] + )) { + const content = chunk.choices?.[0]?.message?.content; + if (content) { + process.stdout.write(content); } - ); + } console.log('\n'); // Model management example diff --git a/sdk/js/examples/tool-calling.ts b/sdk/js/examples/tool-calling.ts index bb4ed541..c3640a8f 100644 --- a/sdk/js/examples/tool-calling.ts +++ b/sdk/js/examples/tool-calling.ts @@ -109,22 +109,18 @@ async function main() { let toolCallData: any = null; console.log('Chat completion response:'); - await chatClient.completeStreamingChat( - messages, - tools, - (chunk: any) => { - const content = chunk.choices?.[0]?.message?.content; - if (content) { - process.stdout.write(content); - } - - // Capture tool call data - const toolCalls = chunk.choices?.[0]?.message?.tool_calls; - if (toolCalls && toolCalls.length > 0) { - toolCallData = toolCalls[0]; - } + for await (const chunk of chatClient.completeStreamingChat(messages, tools)) { + const content = chunk.choices?.[0]?.message?.content; + if (content) { + process.stdout.write(content); + } + + // Capture tool call data + const toolCalls = chunk.choices?.[0]?.message?.tool_calls; + if (toolCalls && toolCalls.length > 0) { + toolCallData = toolCalls[0]; } - ); + } console.log('\n'); // Handle tool invocation @@ -159,16 +155,12 @@ async function main() { }; console.log('Chat completion response:'); - await chatClient.completeStreamingChat( - messages, - tools, - (chunk: any) => { - const content = chunk.choices?.[0]?.message?.content; - if (content) { - process.stdout.write(content); - } + for await (const chunk of chatClient.completeStreamingChat(messages, tools)) { + const content = chunk.choices?.[0]?.message?.content; + if (content) { + process.stdout.write(content); } - ); + } console.log('\n'); console.log('\n✓ Example completed successfully'); diff --git a/sdk/js/src/openai/audioClient.ts b/sdk/js/src/openai/audioClient.ts index 59267015..7b174924 100644 --- a/sdk/js/src/openai/audioClient.ts +++ b/sdk/js/src/openai/audioClient.ts @@ -89,66 +89,153 @@ export class AudioClient { } /** - * Transcribes audio into the input language using streaming. + * Transcribes audio into the input language using streaming, returning an async iterable of chunks. * @param audioFilePath - Path to the audio file to transcribe. - * @param callback - A callback function that receives each chunk of the streaming response. - * @returns A promise that resolves when the stream is complete. - * @throws Error - If audioFilePath or callback are invalid, or streaming fails. + * @returns An async iterable that yields parsed streaming transcription chunks. + * @throws Error - If audioFilePath is invalid, or streaming fails. + * + * @example + * ```typescript + * for await (const chunk of audioClient.transcribeStreaming('recording.wav')) { + * process.stdout.write(chunk.text); + * } + * ``` */ - public async transcribeStreaming(audioFilePath: string, callback: (chunk: any) => void): Promise { + public transcribeStreaming(audioFilePath: string): AsyncIterable { this.validateAudioFilePath(audioFilePath); - if (!callback || typeof callback !== 'function') { - throw new Error('Callback must be a valid function.'); - } + const request = { Model: this.modelId, FileName: audioFilePath, ...this.settings._serialize() }; - - let error: Error | null = null; - try { - await this.coreInterop.executeCommandStreaming( - "audio_transcribe", - { Params: { OpenAICreateRequest: JSON.stringify(request) } }, - (chunkStr: string) => { - // Skip processing if we already encountered an error - if (error) { - return; - } - - if (chunkStr) { - let chunk: any; - try { - chunk = JSON.parse(chunkStr); - } catch (e) { - // Don't throw from callback - store first error and stop processing - error = new Error(`Failed to parse streaming chunk: ${e instanceof Error ? e.message : String(e)}`, { cause: e }); - return; + // Capture instance properties to local variables because `this` is not + // accessible inside the [Symbol.asyncIterator]() method below — it's a + // regular method on the returned object literal, not on the AudioClient. + const coreInterop = this.coreInterop; + const modelId = this.modelId; + + // Return an AsyncIterable object. The [Symbol.asyncIterator]() factory + // is called once when the consumer starts a `for await` loop, and it + // returns the AsyncIterator (with next() / return() methods). + return { + [Symbol.asyncIterator](): AsyncIterator { + // Buffer for chunks received from the native callback. + // Uses a head index for O(1) dequeue instead of Array.shift() which is O(n). + // JavaScript's single-threaded event loop ensures no race conditions + // between the callback pushing chunks and next() consuming them. + const chunks: any[] = []; + let head = 0; + let done = false; + let cancelled = false; + let error: Error | null = null; + let resolve: (() => void) | null = null; + let nextInFlight = false; + + const streamingPromise = coreInterop.executeCommandStreaming( + "audio_transcribe", + { Params: { OpenAICreateRequest: JSON.stringify(request) } }, + (chunkStr: string) => { + if (cancelled || error) return; + if (chunkStr) { + try { + const chunk = JSON.parse(chunkStr); + chunks.push(chunk); + } catch (e) { + if (!error) { + error = new Error( + `Failed to parse streaming chunk: ${e instanceof Error ? e.message : String(e)}`, + { cause: e } + ); + } + } + } + // Wake up any waiting next() call + if (resolve) { + const r = resolve; + resolve = null; + r(); } + } + // When the native stream completes, mark done and wake up any + // pending next() call so it can see that iteration has ended. + ).then(() => { + done = true; + if (resolve) { + const r = resolve; + resolve = null; + r(); // resolve the pending next() promise + } + }).catch((err) => { + if (!error) { + const underlyingError = err instanceof Error ? err : new Error(String(err)); + error = new Error( + `Streaming audio transcription failed for model '${modelId}': ${underlyingError.message}`, + { cause: underlyingError } + ); + } + done = true; + if (resolve) { + const r = resolve; + resolve = null; + r(); + } + }); + // Return the AsyncIterator object consumed by `for await`. + // next() yields buffered chunks one at a time; return() is + // called automatically when the consumer breaks out early. + return { + async next(): Promise> { + if (nextInFlight) { + throw new Error('next() called concurrently on streaming iterator; await each call before invoking next().'); + } + nextInFlight = true; try { - callback(chunk); - } catch (e) { - // Don't throw from callback - store first error and stop processing - error = new Error(`User callback threw an error: ${e instanceof Error ? e.message : String(e)}`, { cause: e }); - return; + while (true) { + if (head < chunks.length) { + const value = chunks[head]; + chunks[head] = undefined; // allow GC + head++; + // Compact the array when all buffered chunks have been consumed + if (head === chunks.length) { + chunks.length = 0; + head = 0; + } + return { value, done: false }; + } + if (error) { + throw error; + } + if (done || cancelled) { + return { value: undefined, done: true }; + } + // Wait for the next chunk or completion + await new Promise((r) => { resolve = r; }); + } + } finally { + nextInFlight = false; } + }, + async return(): Promise> { + // Mark cancelled so the callback stops buffering. + // Note: the underlying native stream cannot be cancelled + // (CoreInterop.executeCommandStreaming has no abort support), + // so the koffi callback may still fire but will no-op due + // to the cancelled guard above. + cancelled = true; + chunks.length = 0; + head = 0; + if (resolve) { + const r = resolve; + resolve = null; + r(); + } + return { value: undefined, done: true }; } - } - ); - - // If we encountered an error during streaming, reject now - if (error) { - throw error; + }; } - } catch (err) { - const underlyingError = err instanceof Error ? err : new Error(String(err)); - throw new Error( - `Streaming audio transcription failed for model '${this.modelId}': ${underlyingError.message}`, - { cause: underlyingError } - ); - } + }; } } diff --git a/sdk/js/src/openai/chatClient.ts b/sdk/js/src/openai/chatClient.ts index 7aa77170..f844da41 100644 --- a/sdk/js/src/openai/chatClient.ts +++ b/sdk/js/src/openai/chatClient.ts @@ -211,26 +211,33 @@ export class ChatClient { } /** - * Performs a streaming chat completion. + * Performs a streaming chat completion, returning an async iterable of chunks. * @param messages - An array of message objects. - * @param tools - An array of tool objects. - * @param callback - A callback function that receives each chunk of the streaming response. - * @returns A promise that resolves when the stream is complete. - * @throws Error - If messages, tools, or callback are invalid, or streaming fails. + * @param tools - An optional array of tool objects. + * @returns An async iterable that yields parsed streaming response chunks. + * @throws Error - If messages or tools are invalid, or streaming fails. + * + * @example + * ```typescript + * // Without tools: + * for await (const chunk of chatClient.completeStreamingChat(messages)) { + * const content = chunk.choices?.[0]?.delta?.content; + * if (content) process.stdout.write(content); + * } + * + * // With tools: + * for await (const chunk of chatClient.completeStreamingChat(messages, tools)) { + * const content = chunk.choices?.[0]?.delta?.content; + * if (content) process.stdout.write(content); + * } + * ``` */ - public async completeStreamingChat(messages: any[], callback: (chunk: any) => void): Promise; - public async completeStreamingChat(messages: any[], tools: any[], callback: (chunk: any) => void): Promise; - public async completeStreamingChat(messages: any[], toolsOrCallback: any[] | ((chunk: any) => void), maybeCallback?: (chunk: any) => void): Promise { - const tools = Array.isArray(toolsOrCallback) ? toolsOrCallback : undefined; - const callback = (Array.isArray(toolsOrCallback) ? maybeCallback : toolsOrCallback) as ((chunk: any) => void) | undefined; - + public completeStreamingChat(messages: any[]): AsyncIterable; + public completeStreamingChat(messages: any[], tools: any[]): AsyncIterable; + public completeStreamingChat(messages: any[], tools?: any[]): AsyncIterable { this.validateMessages(messages); this.validateTools(tools); - if (!callback || typeof callback !== 'function') { - throw new Error('Callback must be a valid function.'); - } - const request = { model: this.modelId, messages, @@ -239,49 +246,132 @@ export class ChatClient { ...this.settings._serialize() }; - let error: Error | null = null; + // Capture instance properties to local variables because `this` is not + // accessible inside the [Symbol.asyncIterator]() method below — it's a + // regular method on the returned object literal, not on the ChatClient. + const coreInterop = this.coreInterop; + const modelId = this.modelId; - try { - await this.coreInterop.executeCommandStreaming( - 'chat_completions', - { Params: { OpenAICreateRequest: JSON.stringify(request) } }, - (chunkStr: string) => { - // Skip processing if we already encountered an error - if (error) return; + // Return an AsyncIterable object. The [Symbol.asyncIterator]() factory + // is called once when the consumer starts a `for await` loop, and it + // returns the AsyncIterator (with next() / return() methods). + return { + [Symbol.asyncIterator](): AsyncIterator { + // Buffer for chunks received from the native callback. + // Uses a head index for O(1) dequeue instead of Array.shift() which is O(n). + // JavaScript's single-threaded event loop ensures no race conditions + // between the callback pushing chunks and next() consuming them. + const chunks: any[] = []; + let head = 0; + let done = false; + let cancelled = false; + let error: Error | null = null; + let resolve: (() => void) | null = null; + let nextInFlight = false; - if (chunkStr) { - let chunk: any; - try { - chunk = JSON.parse(chunkStr); - } catch (e) { - // Don't throw from callback - store first error and stop processing - error = new Error( - `Failed to parse streaming chunk: ${e instanceof Error ? e.message : String(e)}`, - { cause: e } - ); - return; + const streamingPromise = coreInterop.executeCommandStreaming( + 'chat_completions', + { Params: { OpenAICreateRequest: JSON.stringify(request) } }, + (chunkStr: string) => { + if (cancelled || error) return; + if (chunkStr) { + try { + const chunk = JSON.parse(chunkStr); + chunks.push(chunk); + } catch (e) { + if (!error) { + error = new Error( + `Failed to parse streaming chunk: ${e instanceof Error ? e.message : String(e)}`, + { cause: e } + ); + } + } } + // Wake up any waiting next() call + if (resolve) { + const r = resolve; + resolve = null; + r(); + } + } + // When the native stream completes, mark done and wake up any + // pending next() call so it can see that iteration has ended. + ).then(() => { + done = true; + if (resolve) { + const r = resolve; + resolve = null; + r(); // resolve the pending next() promise + } + }).catch((err) => { + if (!error) { + const underlyingError = err instanceof Error ? err : new Error(String(err)); + error = new Error( + `Streaming chat completion failed for model '${modelId}': ${underlyingError.message}`, + { cause: underlyingError } + ); + } + done = true; + if (resolve) { + const r = resolve; + resolve = null; + r(); + } + }); + // Return the AsyncIterator object consumed by `for await`. + // next() yields buffered chunks one at a time; return() is + // called automatically when the consumer breaks out early. + return { + async next(): Promise> { + if (nextInFlight) { + throw new Error('next() called concurrently on streaming iterator; await each call before invoking next().'); + } + nextInFlight = true; try { - callback(chunk); - } catch (e) { - // Don't throw from callback - store first error and stop processing - error = new Error( - `User callback threw an error: ${e instanceof Error ? e.message : String(e)}`, - { cause: e } - ); + while (true) { + if (head < chunks.length) { + const value = chunks[head]; + chunks[head] = undefined; // allow GC + head++; + // Compact the array when all buffered chunks have been consumed + if (head === chunks.length) { + chunks.length = 0; + head = 0; + } + return { value, done: false }; + } + if (error) { + throw error; + } + if (done || cancelled) { + return { value: undefined, done: true }; + } + // Wait for the next chunk or completion + await new Promise((r) => { resolve = r; }); + } + } finally { + nextInFlight = false; + } + }, + async return(): Promise> { + // Mark cancelled so the callback stops buffering. + // Note: the underlying native stream cannot be cancelled + // (CoreInterop.executeCommandStreaming has no abort support), + // so the koffi callback may still fire but will no-op due + // to the cancelled guard above. + cancelled = true; + chunks.length = 0; + head = 0; + if (resolve) { + const r = resolve; + resolve = null; + r(); } + return { value: undefined, done: true }; } - } - ); - - // If we encountered an error during streaming, reject now - if (error) throw error; - } catch (err) { - const underlyingError = err instanceof Error ? err : new Error(String(err)); - throw new Error(`Streaming chat completion failed for model '${this.modelId}': ${underlyingError.message}`, { - cause: underlyingError - }); - } + }; + } + }; } } diff --git a/sdk/js/test/openai/audioClient.test.ts b/sdk/js/test/openai/audioClient.test.ts index a57c02e5..10da05be 100644 --- a/sdk/js/test/openai/audioClient.test.ts +++ b/sdk/js/test/openai/audioClient.test.ts @@ -110,13 +110,13 @@ describe('Audio Client Tests', () => { audioClient.settings.temperature = 0.0; // for deterministic results let fullResponse = ''; - await audioClient.transcribeStreaming(AUDIO_FILE_PATH, (chunk) => { + for await (const chunk of audioClient.transcribeStreaming(AUDIO_FILE_PATH)) { expect(chunk).to.not.be.undefined; expect(chunk.text).to.not.be.undefined; expect(chunk.text).to.be.a('string'); expect(chunk.text.length).to.be.greaterThan(0); fullResponse += chunk.text; - }); + } console.log(`Full response: ${fullResponse}`); expect(fullResponse).to.equal(EXPECTED_TEXT); @@ -151,13 +151,13 @@ describe('Audio Client Tests', () => { audioClient.settings.temperature = 0.0; // for deterministic results let fullResponse = ''; - await audioClient.transcribeStreaming(AUDIO_FILE_PATH, (chunk) => { + for await (const chunk of audioClient.transcribeStreaming(AUDIO_FILE_PATH)) { expect(chunk).to.not.be.undefined; expect(chunk.text).to.not.be.undefined; expect(chunk.text).to.be.a('string'); expect(chunk.text.length).to.be.greaterThan(0); fullResponse += chunk.text; - }); + } console.log(`Full response: ${fullResponse}`); expect(fullResponse).to.equal(EXPECTED_TEXT); @@ -190,27 +190,12 @@ describe('Audio Client Tests', () => { const audioClient = model.createAudioClient(); try { - await audioClient.transcribeStreaming('', () => {}); + // transcribeStreaming validates synchronously before returning the AsyncIterable + audioClient.transcribeStreaming(''); expect.fail('Should have thrown an error for empty audio file path'); } catch (error) { expect(error).to.be.instanceOf(Error); expect((error as Error).message).to.include('Audio file path must be a non-empty string'); } }); - - it('should throw when transcribing streaming with invalid callback', async function() { - const manager = getTestManager(); - const catalog = manager.catalog; - const model = await catalog.getModel(WHISPER_MODEL_ALIAS); - const audioClient = model.createAudioClient(); - const invalidCallbacks: any[] = [null, undefined, 42, {}, 'not-a-function']; - for (const invalidCallback of invalidCallbacks) { - try { - await audioClient.transcribeStreaming(AUDIO_FILE_PATH, invalidCallback as any); - expect.fail('Should have thrown an error for invalid callback'); - } catch (error) { - expect(error).to.be.instanceOf(Error); - } - } - }); }); \ No newline at end of file diff --git a/sdk/js/test/openai/chatClient.test.ts b/sdk/js/test/openai/chatClient.test.ts index 5f612845..7be190ce 100644 --- a/sdk/js/test/openai/chatClient.test.ts +++ b/sdk/js/test/openai/chatClient.test.ts @@ -81,13 +81,13 @@ describe('Chat Client Tests', () => { let fullContent = ''; let chunkCount = 0; - await client.completeStreamingChat(messages, (chunk: any) => { + for await (const chunk of client.completeStreamingChat(messages)) { chunkCount++; const content = chunk.choices?.[0]?.delta?.content; if (content) { fullContent += content; } - }); + } expect(chunkCount).to.be.greaterThan(0); expect(fullContent).to.be.a('string'); @@ -102,13 +102,13 @@ describe('Chat Client Tests', () => { fullContent = ''; chunkCount = 0; - await client.completeStreamingChat(messages, (chunk: any) => { + for await (const chunk of client.completeStreamingChat(messages)) { chunkCount++; const content = chunk.choices?.[0]?.delta?.content; if (content) { fullContent += content; } - }); + } expect(chunkCount).to.be.greaterThan(0); expect(fullContent).to.be.a('string'); @@ -172,7 +172,8 @@ describe('Chat Client Tests', () => { const invalidMessages: any[] = [[], null, undefined]; for (const invalidMessage of invalidMessages) { try { - await client.completeStreamingChat(invalidMessage, () => {}); + // completeStreamingChat validates synchronously before returning the AsyncIterable + client.completeStreamingChat(invalidMessage); expect.fail(`Should have thrown an error for ${Array.isArray(invalidMessage) ? 'empty' : invalidMessage} messages`); } catch (error) { expect(error).to.be.instanceOf(Error); @@ -181,23 +182,6 @@ describe('Chat Client Tests', () => { } }); - it('should throw when completing streaming chat with invalid callback', async function() { - const manager = getTestManager(); - const catalog = manager.catalog; - const model = await catalog.getModel(TEST_MODEL_ALIAS); - const client = model.createChatClient(); - const messages = [{ role: 'user', content: 'Hello' }]; - const invalidCallbacks: any[] = [null, undefined, {} as any, 'not a function' as any]; - for (const invalidCallback of invalidCallbacks) { - try { - await client.completeStreamingChat(messages as any, invalidCallback as any); - expect.fail('Should have thrown an error for invalid callback'); - } catch (error) { - expect(error).to.be.instanceOf(Error); - } - } - }); - it('should perform tool calling chat completion (non-streaming)', async function() { this.timeout(20000); const manager = getTestManager(); @@ -305,7 +289,7 @@ describe('Chat Client Tests', () => { let lastToolCallChunk: any = null; // Check that each response chunk contains the expected information - await client.completeStreamingChat(messages, tools, (chunk: any) => { + for await (const chunk of client.completeStreamingChat(messages, tools)) { const content = chunk.choices?.[0]?.message?.content ?? chunk.choices?.[0]?.delta?.content; if (content) { fullResponse += content; @@ -314,7 +298,7 @@ describe('Chat Client Tests', () => { if (toolCalls && toolCalls.length > 0) { lastToolCallChunk = chunk; } - }); + } expect(fullResponse).to.be.a('string').and.not.equal(''); expect(lastToolCallChunk).to.not.be.null; @@ -341,12 +325,12 @@ describe('Chat Client Tests', () => { // Run the next turn of the conversation fullResponse = ''; - await client.completeStreamingChat(messages, tools, (chunk: any) => { + for await (const chunk of client.completeStreamingChat(messages, tools)) { const content = chunk.choices?.[0]?.message?.content ?? chunk.choices?.[0]?.delta?.content; if (content) { fullResponse += content; } - }); + } // Check that the conversation continued expect(fullResponse).to.be.a('string').and.not.equal('');