llm-fns 1.0.24 → 1.0.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,6 @@
1
- import crypto from 'crypto';
2
1
  export function createCandidateSelector(params) {
3
2
  const { candidateCount, generate, judge, onCandidateError } = params;
4
- async function run(input, baseSalt = crypto.randomUUID()) {
3
+ async function run(input, baseSalt = 'candidate') {
5
4
  // 1. Generate Candidates in Parallel
6
5
  const promises = [];
7
6
  for (let i = 0; i < candidateCount; i++) {
@@ -23,6 +23,7 @@ export type OpenRouterResponseFormat = {
23
23
  schema: object;
24
24
  };
25
25
  };
26
+ export type LlmAudioTransport = 'auto' | 'chat' | 'chat-stream';
26
27
  /**
27
28
  * Request-level options passed to the OpenAI SDK.
28
29
  * These are separate from the body parameters.
@@ -49,6 +50,12 @@ export interface LlmCommonOptions {
49
50
  response_format?: OpenRouterResponseFormat;
50
51
  modalities?: string[];
51
52
  audio?: OpenAI.Chat.Completions.ChatCompletionAudioParam;
53
+ /**
54
+ * Selects how audio output is requested. `auto` prefers streaming because
55
+ * OpenRouter audio output requires it, then falls back to non-streaming chat
56
+ * if streaming is rejected by the provider.
57
+ */
58
+ audioTransport?: LlmAudioTransport;
52
59
  image_config?: {
53
60
  aspect_ratio?: string;
54
61
  };
@@ -64,6 +71,7 @@ export interface LlmCommonOptions {
64
71
  user?: string;
65
72
  tools?: OpenAI.Chat.Completions.ChatCompletionTool[];
66
73
  tool_choice?: OpenAI.Chat.Completions.ChatCompletionToolChoiceOption;
74
+ stream_options?: OpenAI.Chat.Completions.ChatCompletionStreamOptions;
67
75
  }
68
76
  /**
69
77
  * Options for the individual "prompt" function calls.
@@ -119,8 +127,13 @@ export declare function createLlmClient(params: CreateLlmClientParams): {
119
127
  (content: string, options?: LlmCommonOptions): Promise<Buffer>;
120
128
  (options: LlmPromptOptions): Promise<Buffer>;
121
129
  };
130
+ promptAudioStream: {
131
+ (content: string, options?: LlmCommonOptions): AsyncIterable<Buffer>;
132
+ (options: LlmPromptOptions): AsyncIterable<Buffer>;
133
+ };
122
134
  };
123
135
  export type PromptFunction = ReturnType<typeof createLlmClient>['prompt'];
124
136
  export type PromptTextFunction = ReturnType<typeof createLlmClient>['promptText'];
125
137
  export type PromptImageFunction = ReturnType<typeof createLlmClient>['promptImage'];
126
138
  export type PromptAudioFunction = ReturnType<typeof createLlmClient>['promptAudio'];
139
+ export type PromptAudioStreamFunction = ReturnType<typeof createLlmClient>['promptAudioStream'];
@@ -1,6 +1,6 @@
1
1
  import { executeWithRetry } from './retryUtils.js';
2
2
  import { truncateMessages, getPromptSummary } from './util.js';
3
- import { extractImageBuffer, extractAudioBuffer } from './extractBinary.js';
3
+ import { extractImageBuffer, extractAudioBuffer, extractAudioDeltaData } from './extractBinary.js';
4
4
  import { createDnsFetcher } from './createDnsFetcher.js';
5
5
  export class LlmFatalError extends Error {
6
6
  cause;
@@ -63,8 +63,47 @@ export function normalizeOptions(arg1, arg2) {
63
63
  export function createLlmClient(params) {
64
64
  const { openai, defaultModel: factoryDefaultModel, maxConversationChars, queue, defaultRequestOptions, retryBaseDelay: factoryRetryBaseDelay = 1000, fetch: factoryFetch } = params;
65
65
  const fetchImpl = factoryFetch ?? createDnsFetcher();
66
+ const getErrorMessage = (error) => [
67
+ error?.message,
68
+ error?.cause?.message,
69
+ error?.error?.message,
70
+ error?.cause?.error?.message,
71
+ error?.response?.data?.error?.message
72
+ ].filter(Boolean).join('\n');
73
+ const isStreamingUnsupportedError = (error) => {
74
+ const message = getErrorMessage(error);
75
+ return /stream(ing)?\s+(is\s+)?not\s+supported|does\s+not\s+support\s+stream|stream\s+unsupported/i.test(message);
76
+ };
77
+ const withAudioOutputDefaults = (promptParams, defaultFormat) => {
78
+ const factoryModelConfig = typeof factoryDefaultModel === 'object' && factoryDefaultModel !== null
79
+ ? factoryDefaultModel
80
+ : {};
81
+ const callModelConfig = typeof promptParams.model === 'object' && promptParams.model !== null
82
+ ? promptParams.model
83
+ : {};
84
+ const nextModalities = [
85
+ ...(factoryModelConfig.modalities ?? []),
86
+ ...(callModelConfig.modalities ?? []),
87
+ ...(promptParams.modalities ?? [])
88
+ ];
89
+ if (!nextModalities.includes('text'))
90
+ nextModalities.push('text');
91
+ if (!nextModalities.includes('audio'))
92
+ nextModalities.push('audio');
93
+ return {
94
+ ...promptParams,
95
+ modalities: [...new Set(nextModalities)],
96
+ audio: {
97
+ voice: 'alloy',
98
+ format: defaultFormat,
99
+ ...(factoryModelConfig.audio ?? {}),
100
+ ...(callModelConfig.audio ?? {}),
101
+ ...(promptParams.audio ?? {})
102
+ }
103
+ };
104
+ };
66
105
  const getCompletionParams = (promptParams) => {
67
- const { model: callSpecificModel, messages, retries, retryBaseDelay: callSpecificRetryBaseDelay, requestOptions, ...restApiOptions } = promptParams;
106
+ const { model: callSpecificModel, messages, retries, retryBaseDelay: callSpecificRetryBaseDelay, requestOptions, audioTransport: _audioTransport, ...restApiOptions } = promptParams;
68
107
  const finalMessages = maxConversationChars
69
108
  ? truncateMessages(messages, maxConversationChars)
70
109
  : messages;
@@ -146,11 +185,115 @@ export function createLlmClient(params) {
146
185
  return extractImageBuffer(response, fetchImpl);
147
186
  }
148
187
  async function promptAudio(arg1, arg2) {
149
- const promptParams = normalizeOptions(arg1, arg2);
150
- // Ensure modalities includes audio if not explicitly set, though user should ideally provide it.
151
- // We won't force it here to avoid overriding user intent, but promptAudio implies audio output.
152
- const response = await prompt(promptParams);
153
- return extractAudioBuffer(response);
188
+ const normalizedParams = normalizeOptions(arg1, arg2);
189
+ const transport = normalizedParams.audioTransport ?? 'auto';
190
+ if (transport === 'chat') {
191
+ const response = await prompt(withAudioOutputDefaults(normalizedParams, 'mp3'));
192
+ return extractAudioBuffer(response);
193
+ }
194
+ const streamParams = withAudioOutputDefaults(normalizedParams, 'pcm16');
195
+ try {
196
+ return await promptAudioViaChatStream(streamParams);
197
+ }
198
+ catch (error) {
199
+ if (transport === 'auto' && isStreamingUnsupportedError(error)) {
200
+ const response = await prompt(withAudioOutputDefaults(normalizedParams, 'mp3'));
201
+ return extractAudioBuffer(response);
202
+ }
203
+ throw error;
204
+ }
205
+ }
206
+ async function* promptAudioStream(arg1, arg2) {
207
+ const promptParams = withAudioOutputDefaults(normalizeOptions(arg1, arg2), 'pcm16');
208
+ const { completionParams, finalMessages, retries, requestOptions, retryBaseDelay: baseDelay } = getCompletionParams(promptParams);
209
+ const promptSummary = getPromptSummary(finalMessages);
210
+ const streamParams = {
211
+ ...completionParams,
212
+ stream: true
213
+ };
214
+ const streamEvents = [];
215
+ let wakeConsumer;
216
+ const pushStreamEvent = (event) => {
217
+ streamEvents.push(event);
218
+ wakeConsumer?.();
219
+ wakeConsumer = undefined;
220
+ };
221
+ const nextStreamEvent = async () => {
222
+ while (streamEvents.length === 0) {
223
+ await new Promise(resolve => {
224
+ wakeConsumer = resolve;
225
+ });
226
+ }
227
+ return streamEvents.shift();
228
+ };
229
+ let emittedChunkCount = 0;
230
+ async function readStream() {
231
+ let chunkCount = 0;
232
+ try {
233
+ const stream = await openai.chat.completions.create(streamParams, requestOptions);
234
+ for await (const chunk of stream) {
235
+ const audioData = extractAudioDeltaData(chunk);
236
+ if (audioData) {
237
+ chunkCount += 1;
238
+ emittedChunkCount += 1;
239
+ pushStreamEvent({ type: 'chunk', chunk: Buffer.from(audioData, 'base64') });
240
+ }
241
+ }
242
+ return chunkCount;
243
+ }
244
+ catch (error) {
245
+ if (error?.status === 400 || error?.status === 401 || error?.status === 403) {
246
+ throw new LlmFatalError(error.message || 'Fatal API Error', error, finalMessages);
247
+ }
248
+ throw error;
249
+ }
250
+ }
251
+ const task = () => executeWithRetry(() => readStream(), async (chunkCount) => {
252
+ if (chunkCount === 0) {
253
+ return {
254
+ isValid: false,
255
+ feedbackForNextAttempt: {
256
+ type: 'EMPTY_AUDIO_STREAM',
257
+ message: 'LLM returned no streamed audio content.'
258
+ }
259
+ };
260
+ }
261
+ return { isValid: true, data: chunkCount };
262
+ }, retries ?? 3, undefined, (error) => {
263
+ if (emittedChunkCount > 0)
264
+ return false;
265
+ if (error instanceof LlmFatalError)
266
+ return false;
267
+ if (error?.status === 400 || error?.status === 401 || error?.status === 403 || error?.code === 'invalid_api_key') {
268
+ return false;
269
+ }
270
+ return true;
271
+ }, baseDelay);
272
+ const producer = (queue
273
+ ? queue.add(task, { id: promptSummary, messages: finalMessages })
274
+ : task());
275
+ producer
276
+ .then(() => pushStreamEvent({ type: 'done' }))
277
+ .catch(error => pushStreamEvent({ type: 'error', error }));
278
+ while (true) {
279
+ const event = await nextStreamEvent();
280
+ if (event.type === 'chunk') {
281
+ yield event.chunk;
282
+ }
283
+ else if (event.type === 'error') {
284
+ throw event.error;
285
+ }
286
+ else {
287
+ break;
288
+ }
289
+ }
290
+ }
291
+ async function promptAudioViaChatStream(promptParams) {
292
+ const chunks = [];
293
+ for await (const chunk of promptAudioStream(promptParams)) {
294
+ chunks.push(chunk);
295
+ }
296
+ return Buffer.concat(chunks);
154
297
  }
155
- return { prompt, promptText, promptImage, promptAudio };
298
+ return { prompt, promptText, promptImage, promptAudio, promptAudioStream };
156
299
  }
@@ -1,5 +1,5 @@
1
1
  import OpenAI from 'openai';
2
- import { PromptFunction, LlmCommonOptions, LlmPromptOptions } from "./createLlmClient.js";
2
+ import { PromptFunction, PromptAudioFunction, LlmCommonOptions, LlmPromptOptions } from "./createLlmClient.js";
3
3
  import { ConversationState } from './createConversation.js';
4
4
  export declare class LlmRetryError extends Error {
5
5
  readonly message: string;
@@ -38,6 +38,7 @@ export interface LlmRetryOptions<T = any> extends LlmCommonOptions {
38
38
  }
39
39
  export interface CreateLlmRetryClientParams {
40
40
  prompt: PromptFunction;
41
+ promptAudio?: PromptAudioFunction;
41
42
  fallbackPrompt?: PromptFunction;
42
43
  retryBaseDelay?: number;
43
44
  /** Optional custom fetch implementation for binary extraction */
@@ -68,7 +68,7 @@ function constructLlmMessages(initialMessages, attemptNumber, previousError) {
68
68
  return messages;
69
69
  }
70
70
  export function createLlmRetryClient(params) {
71
- const { prompt, fallbackPrompt, retryBaseDelay: factoryRetryBaseDelay = 0, fetch: factoryFetch } = params;
71
+ const { prompt, promptAudio, fallbackPrompt, retryBaseDelay: factoryRetryBaseDelay = 0, fetch: factoryFetch } = params;
72
72
  const fetchImpl = factoryFetch ?? createDnsFetcher();
73
73
  async function runPromptLoop(retryParams) {
74
74
  const { maxRetries = 3, validate, messages: initialMessages, retryBaseDelay = factoryRetryBaseDelay, state, ...restOptions } = retryParams;
@@ -192,6 +192,13 @@ export function createLlmRetryClient(params) {
192
192
  async function promptAudioRetry(arg1, arg2) {
193
193
  const retryParams = normalizeRetryOptions(arg1, arg2);
194
194
  const userValidate = retryParams.validate;
195
+ if (promptAudio && !userValidate && !retryParams.state && !fallbackPrompt) {
196
+ const { maxRetries, validate, state, ...promptOptions } = retryParams;
197
+ return await promptAudio({
198
+ ...promptOptions,
199
+ retries: maxRetries ?? promptOptions.retries
200
+ });
201
+ }
195
202
  retryParams.validate = async (completion, info) => {
196
203
  try {
197
204
  const buffer = extractAudioBuffer(completion);
@@ -1,59 +1,59 @@
1
1
  import { getPromptSummary } from "./util.js";
2
2
  import { LlmFatalError } from "./createLlmClient.js";
3
- import { vi } from "vitest";
4
3
  export function createMockOpenAI(responses) {
5
4
  let callCount = 0;
5
+ const createFn = async (params) => {
6
+ let response;
7
+ const currentCall = callCount + 1;
8
+ const summary = getPromptSummary(params.messages);
9
+ console.log(`\n--- [Mock OpenAI] Call #${currentCall} ---`);
10
+ console.log(`Prompt: ${summary}`);
11
+ if (typeof responses === 'function') {
12
+ response = responses(params.messages);
13
+ if (response === undefined || response === null) {
14
+ throw new LlmFatalError(`[Mock OpenAI] Resolver function returned null/undefined.\n` +
15
+ `Requested Call: #${currentCall}\n` +
16
+ `Prompt Summary: ${summary}\n\n` +
17
+ `Check your mock resolver logic to ensure it returns a valid response for this prompt.`, undefined, params.messages);
18
+ }
19
+ console.log(`Response (Resolver): ${typeof response === 'string' ? response : JSON.stringify(response, null, 2)}`);
20
+ callCount++;
21
+ }
22
+ else {
23
+ if (callCount >= responses.length) {
24
+ throw new LlmFatalError(`[Mock OpenAI] No more responses configured.\n` +
25
+ `Requested Call: #${currentCall}\n` +
26
+ `Configured Responses: ${responses.length}\n` +
27
+ `Prompt Summary: ${summary}\n\n` +
28
+ `Please check responses in your mock configuration array.`, undefined, params.messages);
29
+ }
30
+ response = responses[callCount];
31
+ console.log(`Response (Array[${callCount}]): ${typeof response === 'string' ? response : JSON.stringify(response, null, 2)}`);
32
+ callCount++;
33
+ }
34
+ console.log(`---------------------------------------\n`);
35
+ // If response is a string, wrap it in a standard text message
36
+ if (typeof response === 'string') {
37
+ return {
38
+ id: 'mock-id',
39
+ choices: [{
40
+ message: { content: response }
41
+ }]
42
+ };
43
+ }
44
+ // If response is an object, assume it's a full message object (e.g. for images/audio)
45
+ // or a partial choice object
46
+ return {
47
+ id: 'mock-id',
48
+ choices: [{
49
+ message: response
50
+ }]
51
+ };
52
+ };
6
53
  return {
7
54
  chat: {
8
55
  completions: {
9
- create: vi.fn(async (params) => {
10
- let response;
11
- const currentCall = callCount + 1;
12
- const summary = getPromptSummary(params.messages);
13
- console.log(`\n--- [Mock OpenAI] Call #${currentCall} ---`);
14
- console.log(`Prompt: ${summary}`);
15
- if (typeof responses === 'function') {
16
- response = responses(params.messages);
17
- if (response === undefined || response === null) {
18
- throw new LlmFatalError(`[Mock OpenAI] Resolver function returned null/undefined.\n` +
19
- `Requested Call: #${currentCall}\n` +
20
- `Prompt Summary: ${summary}\n\n` +
21
- `Check your mock resolver logic to ensure it returns a valid response for this prompt.`, undefined, params.messages);
22
- }
23
- console.log(`Response (Resolver): ${typeof response === 'string' ? response : JSON.stringify(response, null, 2)}`);
24
- callCount++;
25
- }
26
- else {
27
- if (callCount >= responses.length) {
28
- throw new LlmFatalError(`[Mock OpenAI] No more responses configured.\n` +
29
- `Requested Call: #${currentCall}\n` +
30
- `Configured Responses: ${responses.length}\n` +
31
- `Prompt Summary: ${summary}\n\n` +
32
- `Please check responses in your mock configuration array.`, undefined, params.messages);
33
- }
34
- response = responses[callCount];
35
- console.log(`Response (Array[${callCount}]): ${typeof response === 'string' ? response : JSON.stringify(response, null, 2)}`);
36
- callCount++;
37
- }
38
- console.log(`---------------------------------------\n`);
39
- // If response is a string, wrap it in a standard text message
40
- if (typeof response === 'string') {
41
- return {
42
- id: 'mock-id',
43
- choices: [{
44
- message: { content: response }
45
- }]
46
- };
47
- }
48
- // If response is an object, assume it's a full message object (e.g. for images/audio)
49
- // or a partial choice object
50
- return {
51
- id: 'mock-id',
52
- choices: [{
53
- message: response
54
- }]
55
- };
56
- })
56
+ create: createFn
57
57
  }
58
58
  }
59
59
  };
@@ -19,3 +19,10 @@ export declare function extractImageBuffer(completion: OpenAI.Chat.Completions.C
19
19
  * Extracts an audio buffer from a ChatCompletion.
20
20
  */
21
21
  export declare function extractAudioBuffer(completion: OpenAI.Chat.Completions.ChatCompletion): Buffer;
22
+ /**
23
+ * Extracts Base64 audio payloads from streamed Chat Completion chunks.
24
+ * OpenRouter delivers audio output at `choices[0].delta.audio.data`.
25
+ * Some OpenAI-compatible providers may use adjacent fields, so this accepts the
26
+ * common variants without widening the public client API.
27
+ */
28
+ export declare function extractAudioDeltaData(chunk: unknown): string | undefined;
@@ -18,7 +18,7 @@ export function isAudioResponse(completion) {
18
18
  if (message?.audio)
19
19
  return true;
20
20
  if (Array.isArray(message?.content)) {
21
- return message.content.some((part) => part.type === 'input_audio');
21
+ return message.content.some((part) => part.type === 'input_audio' || part.type === 'audio');
22
22
  }
23
23
  return false;
24
24
  }
@@ -66,11 +66,30 @@ export function extractAudioBuffer(completion) {
66
66
  }
67
67
  // 2. Check standard content parts
68
68
  else if (Array.isArray(message?.content)) {
69
- const part = message.content.find((p) => p.type === 'input_audio');
70
- audioData = part?.input_audio?.data;
69
+ const part = message.content.find((p) => p.type === 'input_audio' || p.type === 'audio');
70
+ audioData = part?.input_audio?.data || part?.audio?.data || part?.data;
71
71
  }
72
72
  if (audioData) {
73
73
  return Buffer.from(audioData, 'base64');
74
74
  }
75
75
  throw new Error("LLM returned no audio content.");
76
76
  }
77
+ /**
78
+ * Extracts Base64 audio payloads from streamed Chat Completion chunks.
79
+ * OpenRouter delivers audio output at `choices[0].delta.audio.data`.
80
+ * Some OpenAI-compatible providers may use adjacent fields, so this accepts the
81
+ * common variants without widening the public client API.
82
+ */
83
+ export function extractAudioDeltaData(chunk) {
84
+ const delta = chunk?.choices?.[0]?.delta;
85
+ const audio = delta?.audio || delta?.output_audio;
86
+ if (typeof audio === 'string')
87
+ return audio;
88
+ if (audio?.data)
89
+ return audio.data;
90
+ if (audio?.delta)
91
+ return audio.delta;
92
+ if (delta?.audio_data)
93
+ return delta.audio_data;
94
+ return undefined;
95
+ }
@@ -47,5 +47,9 @@ export declare function createLlm(params: CreateLlmFactoryParams): {
47
47
  (content: string, options?: import("./createLlmClient.js").LlmCommonOptions): Promise<Buffer>;
48
48
  (options: import("./createLlmClient.js").LlmPromptOptions): Promise<Buffer>;
49
49
  };
50
+ promptAudioStream: {
51
+ (content: string, options?: import("./createLlmClient.js").LlmCommonOptions): AsyncIterable<Buffer>;
52
+ (options: import("./createLlmClient.js").LlmPromptOptions): AsyncIterable<Buffer>;
53
+ };
50
54
  };
51
55
  export type LlmClient = ReturnType<typeof createLlm>;
@@ -7,6 +7,7 @@ export function createLlm(params) {
7
7
  const baseClient = createLlmClient(params);
8
8
  const retryClient = createLlmRetryClient({
9
9
  prompt: baseClient.prompt,
10
+ promptAudio: baseClient.promptAudio,
10
11
  retryBaseDelay: params.retryBaseDelay,
11
12
  fetch: params.fetch
12
13
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "llm-fns",
3
- "version": "1.0.24",
3
+ "version": "1.0.26",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -8,9 +8,16 @@
8
8
  "files": [
9
9
  "dist"
10
10
  ],
11
+ "scripts": {
12
+ "test": "vitest run",
13
+ "release": "scripts/release.sh",
14
+ "build": "tsc",
15
+ "watch": "tsc -b -w"
16
+ },
11
17
  "keywords": [],
12
18
  "author": "",
13
19
  "license": "MIT",
20
+ "packageManager": "pnpm@10.6.5",
14
21
  "dependencies": {
15
22
  "ajv": "^8.17.1",
16
23
  "openai": "^6.15.0",
@@ -26,11 +33,5 @@
26
33
  "p-queue": "^9.0.1",
27
34
  "typescript": "^5.9.3",
28
35
  "vitest": "^1.6.1"
29
- },
30
- "scripts": {
31
- "test": "vitest run",
32
- "release": "scripts/release.sh",
33
- "build": "tsc",
34
- "watch": "tsc -b -w"
35
36
  }
36
- }
37
+ }
package/readme.md CHANGED
@@ -22,7 +22,7 @@ const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
22
22
 
23
23
  const llm = createLlm({
24
24
  openai,
25
- defaultModel: 'google/gemini-3-pro-preview',
25
+ defaultModel: '~google/gemini-pro-latest',
26
26
  // optional:
27
27
  // cache: Cache instance (cache-manager)
28
28
  // queue: PQueue instance for concurrency control
@@ -251,7 +251,50 @@ const buffer2 = await llm.promptImage({
251
251
 
252
252
  ---
253
253
 
254
- # Use Case 3: Structured Data (`llm.promptJson` & `llm.promptZod`)
254
+ # Use Case 3: Audio (`llm.promptAudio` / `llm.promptAudioStream`)
255
+
256
+ Generates audio and returns it as a `Buffer`, or yields audio chunks as they arrive. This supports providers that only expose audio output through streaming Chat Completions, including OpenRouter routes to audio-capable OpenAI models.
257
+
258
+ **Return Type:** `Promise<Buffer>` for `promptAudio`, `AsyncIterable<Buffer>` for `promptAudioStream`
259
+
260
+ ```typescript
261
+ import fs from 'node:fs/promises';
262
+
263
+ const audio = await llm.promptAudio({
264
+ model: 'openai/gpt-audio-mini',
265
+ messages: 'Say exactly: BatchPrompt audio test.',
266
+ modalities: ['text', 'audio'],
267
+ audio: {
268
+ voice: 'alloy',
269
+ format: 'pcm16'
270
+ },
271
+ audioTransport: 'chat-stream'
272
+ });
273
+
274
+ await fs.writeFile('batchprompt-audio-test.pcm', audio);
275
+ ```
276
+
277
+ For callers that want to process chunks as they arrive, use `promptAudioStream`:
278
+
279
+ ```typescript
280
+ const chunks: Buffer[] = [];
281
+
282
+ for await (const chunk of llm.promptAudioStream({
283
+ model: 'openai/gpt-audio-mini',
284
+ messages: 'Read this sentence aloud.',
285
+ audio: { voice: 'alloy', format: 'pcm16' }
286
+ })) {
287
+ chunks.push(chunk);
288
+ }
289
+
290
+ await fs.writeFile('streamed-audio.pcm', Buffer.concat(chunks));
291
+ ```
292
+
293
+ `promptAudio` defaults to streaming transport because some providers require it for audio output. When streaming, the default audio format is `pcm16`; OpenAI rejects `mp3` for streamed `gpt-audio-mini` responses. If you need non-streaming Chat Completions audio, pass `audioTransport: 'chat'`, where the default format is `mp3`.
294
+
295
+ ---
296
+
297
+ # Use Case 4: Structured Data (`llm.promptJson` & `llm.promptZod`)
255
298
 
256
299
  This is a high-level wrapper that employs a **Re-asking Loop**. If the LLM outputs invalid JSON or data that fails the schema validation, the client automatically feeds the error back to the LLM and asks it to fix it (up to `maxRetries`).
257
300
 
@@ -303,7 +346,7 @@ const gameState = await llm.promptZod(
303
346
  history, // Arg 1: Context
304
347
  GameStateSchema, // Arg 2: Schema
305
348
  { // Arg 3: Options Override
306
- model: "google/gemini-flash-1.5",
349
+ model: "~google/gemini-flash-latest",
307
350
  disableJsonFixer: true, // Turn off the automatic JSON repair agent
308
351
  maxRetries: 0, // Fail immediately on error
309
352
  }
@@ -392,7 +435,7 @@ const SafeSchema = z.object({
392
435
 
393
436
  ---
394
437
 
395
- # Use Case 4: Agentic Retry Loops (`llm.promptTextRetry`)
438
+ # Use Case 5: Agentic Retry Loops (`llm.promptTextRetry`)
396
439
 
397
440
  The library exposes the "Conversational Retry" engine used internally by `promptZod`. You can provide a `validate` function. If it throws a `LlmRetryError`, the error message is fed back to the LLM, and it tries again.
398
441
 
@@ -420,7 +463,7 @@ const poem = await llm.promptTextRetry({
420
463
 
421
464
  ---
422
465
 
423
- # Use Case 5: Iterative Refinement (`createIterativeRefiner`)
466
+ # Use Case 6: Iterative Refinement (`createIterativeRefiner`)
424
467
 
425
468
  For complex tasks where an LLM needs to "try, check, and fix" its own output (like code generation or complex configuration), use the `IterativeRefiner`.
426
469
 
@@ -615,7 +658,7 @@ function getAllResponses(error: LlmRetryExhaustedError): string[] {
615
658
 
616
659
  ---
617
660
 
618
- # Use Case 6: Architecture & Composition
661
+ # Use Case 7: Architecture & Composition
619
662
 
620
663
  How to build the client manually to enable **Fallback Chains** and **Smart Routing**.
621
664
 
@@ -628,13 +671,13 @@ import { createLlmClient } from './src';
628
671
  // 1. Define a CHEAP model
629
672
  const cheapClient = createLlmClient({
630
673
  openai,
631
- defaultModel: 'google/gemini-flash-1.5'
674
+ defaultModel: '~google/gemini-flash-latest'
632
675
  });
633
676
 
634
677
  // 2. Define a STRONG model
635
678
  const strongClient = createLlmClient({
636
679
  openai,
637
- defaultModel: 'google/gemini-3-pro-preview'
680
+ defaultModel: '~google/gemini-pro-latest'
638
681
  });
639
682
  ```
640
683