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.
- package/dist/createCandidateSelector.js +1 -2
- package/dist/createLlmClient.d.ts +13 -0
- package/dist/createLlmClient.js +151 -8
- package/dist/createLlmRetryClient.d.ts +2 -1
- package/dist/createLlmRetryClient.js +8 -1
- package/dist/createMockOpenAi.js +49 -49
- package/dist/extractBinary.d.ts +7 -0
- package/dist/extractBinary.js +22 -3
- package/dist/llmFactory.d.ts +4 -0
- package/dist/llmFactory.js +1 -0
- package/package.json +9 -8
- package/readme.md +51 -8
|
@@ -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 =
|
|
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'];
|
package/dist/createLlmClient.js
CHANGED
|
@@ -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
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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);
|
package/dist/createMockOpenAi.js
CHANGED
|
@@ -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:
|
|
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
|
};
|
package/dist/extractBinary.d.ts
CHANGED
|
@@ -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;
|
package/dist/extractBinary.js
CHANGED
|
@@ -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
|
+
}
|
package/dist/llmFactory.d.ts
CHANGED
|
@@ -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>;
|
package/dist/llmFactory.js
CHANGED
|
@@ -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.
|
|
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-
|
|
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:
|
|
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-
|
|
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
|
|
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
|
|
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
|
|
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-
|
|
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-
|
|
680
|
+
defaultModel: '~google/gemini-pro-latest'
|
|
638
681
|
});
|
|
639
682
|
```
|
|
640
683
|
|