llm-fns 1.0.18 → 1.0.20
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/createCachedFetcher.d.ts +1 -6
- package/dist/createCachedFetcher.js +34 -16
- package/dist/createJsonSchemaLlmClient.d.ts +10 -3
- package/dist/createJsonSchemaLlmClient.js +44 -25
- package/dist/createLlmClient.d.ts +55 -9
- package/dist/createLlmClient.js +62 -24
- package/dist/createLlmClient.spec.js +72 -0
- package/dist/createLlmRetryClient.d.ts +15 -10
- package/dist/createLlmRetryClient.js +25 -26
- package/dist/createZodLlmClient.js +10 -1
- package/dist/llmFactory.d.ts +9 -9
- package/package.json +2 -2
- package/readme.md +8 -2
|
@@ -14,12 +14,7 @@ export interface CreateFetcherDependencies {
|
|
|
14
14
|
prefix?: string;
|
|
15
15
|
/** Time-to-live for cache entries, in milliseconds. */
|
|
16
16
|
ttl?: number;
|
|
17
|
-
/** Request timeout in milliseconds. If not provided, no timeout is applied
|
|
18
|
-
|
|
19
|
-
I'm now generating the corrected version of `src/createCachedFetcher.ts`. The primary fix is removing the extraneous text from the `set` method signature within the `CacheLike` interface. I've ensured the syntax is correct, and I'm confident the test run should now pass. After this is output, I plan to assess its integration within the wider project.
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
*/
|
|
17
|
+
/** Request timeout in milliseconds. If not provided, no timeout is applied. */
|
|
23
18
|
timeout?: number;
|
|
24
19
|
/** User-Agent string for requests. */
|
|
25
20
|
userAgent?: string;
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
// src/createCachedFetcher.ts
|
|
3
2
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
4
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
5
4
|
};
|
|
@@ -7,20 +6,43 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
7
6
|
exports.CachedResponse = void 0;
|
|
8
7
|
exports.createCachedFetcher = createCachedFetcher;
|
|
9
8
|
const crypto_1 = __importDefault(require("crypto"));
|
|
10
|
-
// A custom Response class to correctly handle the `.url` property on cache HITs.
|
|
11
|
-
// This is an implementation detail and doesn't need to be exported.
|
|
12
9
|
class CachedResponse extends Response {
|
|
13
10
|
#finalUrl;
|
|
14
11
|
constructor(body, init, finalUrl) {
|
|
15
12
|
super(body, init);
|
|
16
13
|
this.#finalUrl = finalUrl;
|
|
17
14
|
}
|
|
18
|
-
// Override the read-only `url` property
|
|
19
15
|
get url() {
|
|
20
16
|
return this.#finalUrl;
|
|
21
17
|
}
|
|
22
18
|
}
|
|
23
19
|
exports.CachedResponse = CachedResponse;
|
|
20
|
+
/**
|
|
21
|
+
* Creates a deterministic hash of headers for cache key generation.
|
|
22
|
+
* Headers are sorted alphabetically to ensure consistency.
|
|
23
|
+
*/
|
|
24
|
+
function hashHeaders(headers) {
|
|
25
|
+
if (!headers)
|
|
26
|
+
return '';
|
|
27
|
+
let headerEntries;
|
|
28
|
+
if (headers instanceof Headers) {
|
|
29
|
+
headerEntries = Array.from(headers.entries());
|
|
30
|
+
}
|
|
31
|
+
else if (Array.isArray(headers)) {
|
|
32
|
+
headerEntries = headers;
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
headerEntries = Object.entries(headers);
|
|
36
|
+
}
|
|
37
|
+
if (headerEntries.length === 0)
|
|
38
|
+
return '';
|
|
39
|
+
// Sort alphabetically by key for deterministic ordering
|
|
40
|
+
headerEntries.sort((a, b) => a[0].localeCompare(b[0]));
|
|
41
|
+
const headerString = headerEntries
|
|
42
|
+
.map(([key, value]) => `${key}:${value}`)
|
|
43
|
+
.join('|');
|
|
44
|
+
return crypto_1.default.createHash('md5').update(headerString).digest('hex');
|
|
45
|
+
}
|
|
24
46
|
/**
|
|
25
47
|
* Factory function that creates a `fetch` replacement with a caching layer.
|
|
26
48
|
* @param deps - Dependencies including the cache instance, prefix, TTL, and timeout.
|
|
@@ -30,8 +52,6 @@ function createCachedFetcher(deps) {
|
|
|
30
52
|
const { cache, prefix = 'http-cache', ttl, timeout, userAgent, fetch: customFetch, shouldCache } = deps;
|
|
31
53
|
const fetchImpl = customFetch ?? fetch;
|
|
32
54
|
const fetchWithTimeout = async (url, options) => {
|
|
33
|
-
// Correctly merge headers using Headers API to handle various input formats (plain object, Headers instance, array)
|
|
34
|
-
// and avoid issues with spreading Headers objects which can lead to lost headers or Symbol errors.
|
|
35
55
|
const headers = new Headers(options?.headers);
|
|
36
56
|
if (userAgent) {
|
|
37
57
|
headers.set('User-Agent', userAgent);
|
|
@@ -70,10 +90,7 @@ function createCachedFetcher(deps) {
|
|
|
70
90
|
clearTimeout(timeoutId);
|
|
71
91
|
}
|
|
72
92
|
};
|
|
73
|
-
// This is the actual fetcher implementation, returned by the factory.
|
|
74
|
-
// It "closes over" the dependencies provided to the factory.
|
|
75
93
|
return async (url, options) => {
|
|
76
|
-
// Determine the request method. Default to GET for fetch.
|
|
77
94
|
let method = 'GET';
|
|
78
95
|
if (options?.method) {
|
|
79
96
|
method = options.method;
|
|
@@ -87,7 +104,7 @@ function createCachedFetcher(deps) {
|
|
|
87
104
|
return fetchWithTimeout(url, options);
|
|
88
105
|
}
|
|
89
106
|
let cacheKey = `${prefix}:${urlString}`;
|
|
90
|
-
//
|
|
107
|
+
// Hash body for POST requests
|
|
91
108
|
if (method.toUpperCase() === 'POST' && options?.body) {
|
|
92
109
|
let bodyStr = '';
|
|
93
110
|
if (typeof options.body === 'string') {
|
|
@@ -97,7 +114,6 @@ function createCachedFetcher(deps) {
|
|
|
97
114
|
bodyStr = options.body.toString();
|
|
98
115
|
}
|
|
99
116
|
else {
|
|
100
|
-
// Fallback for other types, though mostly we expect string/JSON here
|
|
101
117
|
try {
|
|
102
118
|
bodyStr = JSON.stringify(options.body);
|
|
103
119
|
}
|
|
@@ -105,13 +121,17 @@ function createCachedFetcher(deps) {
|
|
|
105
121
|
bodyStr = 'unserializable';
|
|
106
122
|
}
|
|
107
123
|
}
|
|
108
|
-
const
|
|
109
|
-
cacheKey +=
|
|
124
|
+
const bodyHash = crypto_1.default.createHash('md5').update(bodyStr).digest('hex');
|
|
125
|
+
cacheKey += `:body:${bodyHash}`;
|
|
126
|
+
}
|
|
127
|
+
// Hash all request headers into cache key
|
|
128
|
+
const headersHash = hashHeaders(options?.headers);
|
|
129
|
+
if (headersHash) {
|
|
130
|
+
cacheKey += `:headers:${headersHash}`;
|
|
110
131
|
}
|
|
111
132
|
// 1. Check the cache
|
|
112
133
|
const cachedItem = await cache.get(cacheKey);
|
|
113
134
|
if (cachedItem) {
|
|
114
|
-
// Decode the base64 body back into a Buffer.
|
|
115
135
|
const body = Buffer.from(cachedItem.bodyBase64, 'base64');
|
|
116
136
|
return new CachedResponse(body, {
|
|
117
137
|
status: cachedItem.status,
|
|
@@ -135,7 +155,6 @@ function createCachedFetcher(deps) {
|
|
|
135
155
|
}
|
|
136
156
|
}
|
|
137
157
|
else {
|
|
138
|
-
// Default behavior: check for .error in JSON responses
|
|
139
158
|
const contentType = response.headers.get('content-type');
|
|
140
159
|
if (contentType && contentType.includes('application/json')) {
|
|
141
160
|
const checkClone = response.clone();
|
|
@@ -154,7 +173,6 @@ function createCachedFetcher(deps) {
|
|
|
154
173
|
if (isCacheable) {
|
|
155
174
|
const responseClone = response.clone();
|
|
156
175
|
const bodyBuffer = await responseClone.arrayBuffer();
|
|
157
|
-
// Convert ArrayBuffer to a base64 string for safe JSON serialization.
|
|
158
176
|
const bodyBase64 = Buffer.from(bodyBuffer).toString('base64');
|
|
159
177
|
const headers = Object.fromEntries(response.headers.entries());
|
|
160
178
|
const itemToCache = {
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import OpenAI from 'openai';
|
|
2
|
-
import { PromptFunction,
|
|
3
|
-
export
|
|
2
|
+
import { PromptFunction, LlmCommonOptions } from "./createLlmClient.js";
|
|
3
|
+
export declare class SchemaValidationError extends Error {
|
|
4
|
+
constructor(message: string, options?: ErrorOptions);
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Options for JSON schema prompt functions.
|
|
8
|
+
* Extends common options with JSON-specific settings.
|
|
9
|
+
*/
|
|
10
|
+
export interface JsonSchemaLlmClientOptions extends LlmCommonOptions {
|
|
4
11
|
maxRetries?: number;
|
|
5
12
|
/**
|
|
6
13
|
* If true, passes `response_format: { type: 'json_object' }` to the model.
|
|
@@ -22,7 +29,7 @@ export type JsonSchemaLlmClientOptions = Omit<LlmPromptOptions, 'messages' | 're
|
|
|
22
29
|
* If not provided, an AJV-based validator will be used.
|
|
23
30
|
*/
|
|
24
31
|
validator?: (data: any) => any;
|
|
25
|
-
}
|
|
32
|
+
}
|
|
26
33
|
export interface CreateJsonSchemaLlmClientParams {
|
|
27
34
|
prompt: PromptFunction;
|
|
28
35
|
fallbackPrompt?: PromptFunction;
|
|
@@ -3,13 +3,21 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.SchemaValidationError = void 0;
|
|
6
7
|
exports.createJsonSchemaLlmClient = createJsonSchemaLlmClient;
|
|
7
8
|
const ajv_1 = __importDefault(require("ajv"));
|
|
8
9
|
const createLlmRetryClient_js_1 = require("./createLlmRetryClient.js");
|
|
10
|
+
class SchemaValidationError extends Error {
|
|
11
|
+
constructor(message, options) {
|
|
12
|
+
super(message, options);
|
|
13
|
+
this.name = 'SchemaValidationError';
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
exports.SchemaValidationError = SchemaValidationError;
|
|
9
17
|
function createJsonSchemaLlmClient(params) {
|
|
10
18
|
const { prompt, fallbackPrompt, disableJsonFixer = false } = params;
|
|
11
19
|
const llmRetryClient = (0, createLlmRetryClient_js_1.createLlmRetryClient)({ prompt, fallbackPrompt });
|
|
12
|
-
const ajv = new ajv_1.default({ strict: false });
|
|
20
|
+
const ajv = new ajv_1.default({ strict: false });
|
|
13
21
|
async function _tryToFixJson(brokenResponse, schemaJsonString, errorDetails, options) {
|
|
14
22
|
const fixupPrompt = `
|
|
15
23
|
An attempt to generate a JSON object resulted in the following output, which is either not valid JSON or does not conform to the required schema.
|
|
@@ -37,7 +45,7 @@ ${brokenResponse}
|
|
|
37
45
|
const response_format = useResponseFormat
|
|
38
46
|
? { type: 'json_object' }
|
|
39
47
|
: undefined;
|
|
40
|
-
const { maxRetries, useResponseFormat: _useResponseFormat, ...restOptions } = options || {};
|
|
48
|
+
const { maxRetries, useResponseFormat: _useResponseFormat, beforeValidation, validator, ...restOptions } = options || {};
|
|
41
49
|
const completion = await prompt({
|
|
42
50
|
messages,
|
|
43
51
|
response_format,
|
|
@@ -51,7 +59,6 @@ ${brokenResponse}
|
|
|
51
59
|
}
|
|
52
60
|
async function _parseOrFixJson(llmResponseString, schemaJsonString, options) {
|
|
53
61
|
let jsonDataToParse = llmResponseString.trim();
|
|
54
|
-
// Robust handling for responses wrapped in markdown code blocks
|
|
55
62
|
const codeBlockRegex = /```(?:json)?\s*([\s\S]*?)\s*```/;
|
|
56
63
|
const match = codeBlockRegex.exec(jsonDataToParse);
|
|
57
64
|
if (match && match[1]) {
|
|
@@ -64,10 +71,14 @@ ${brokenResponse}
|
|
|
64
71
|
return JSON.parse(jsonDataToParse);
|
|
65
72
|
}
|
|
66
73
|
catch (parseError) {
|
|
74
|
+
// Only attempt to fix SyntaxErrors (JSON parsing errors).
|
|
75
|
+
// Other errors (like runtime errors) should bubble up.
|
|
76
|
+
if (!(parseError instanceof SyntaxError)) {
|
|
77
|
+
throw parseError;
|
|
78
|
+
}
|
|
67
79
|
if (disableJsonFixer) {
|
|
68
|
-
throw parseError;
|
|
80
|
+
throw parseError;
|
|
69
81
|
}
|
|
70
|
-
// Attempt a one-time fix before failing.
|
|
71
82
|
const errorDetails = `JSON Parse Error: ${parseError.message}`;
|
|
72
83
|
const fixedResponse = await _tryToFixJson(jsonDataToParse, schemaJsonString, errorDetails, options);
|
|
73
84
|
if (fixedResponse) {
|
|
@@ -75,11 +86,10 @@ ${brokenResponse}
|
|
|
75
86
|
return JSON.parse(fixedResponse);
|
|
76
87
|
}
|
|
77
88
|
catch (e) {
|
|
78
|
-
// Fix-up failed, throw original error.
|
|
79
89
|
throw parseError;
|
|
80
90
|
}
|
|
81
91
|
}
|
|
82
|
-
throw parseError;
|
|
92
|
+
throw parseError;
|
|
83
93
|
}
|
|
84
94
|
}
|
|
85
95
|
async function _validateOrFix(jsonData, validator, schemaJsonString, options) {
|
|
@@ -90,10 +100,14 @@ ${brokenResponse}
|
|
|
90
100
|
return validator(jsonData);
|
|
91
101
|
}
|
|
92
102
|
catch (validationError) {
|
|
103
|
+
// Only attempt to fix known validation errors (SchemaValidationError).
|
|
104
|
+
// Arbitrary errors thrown by custom validators (e.g. "Database Error") should bubble up.
|
|
105
|
+
if (!(validationError instanceof SchemaValidationError)) {
|
|
106
|
+
throw validationError;
|
|
107
|
+
}
|
|
93
108
|
if (disableJsonFixer) {
|
|
94
109
|
throw validationError;
|
|
95
110
|
}
|
|
96
|
-
// Attempt a one-time fix for schema validation errors.
|
|
97
111
|
const errorDetails = `Schema Validation Error: ${validationError.message}`;
|
|
98
112
|
const fixedResponse = await _tryToFixJson(JSON.stringify(jsonData, null, 2), schemaJsonString, errorDetails, options);
|
|
99
113
|
if (fixedResponse) {
|
|
@@ -105,11 +119,10 @@ ${brokenResponse}
|
|
|
105
119
|
return validator(fixedJsonData);
|
|
106
120
|
}
|
|
107
121
|
catch (e) {
|
|
108
|
-
// Fix-up failed, throw original validation error
|
|
109
122
|
throw validationError;
|
|
110
123
|
}
|
|
111
124
|
}
|
|
112
|
-
throw validationError;
|
|
125
|
+
throw validationError;
|
|
113
126
|
}
|
|
114
127
|
}
|
|
115
128
|
function _getJsonPromptConfig(messages, schema, options) {
|
|
@@ -120,12 +133,9 @@ Do NOT include any other text, explanations, or markdown formatting (like \`\`\`
|
|
|
120
133
|
|
|
121
134
|
JSON schema:
|
|
122
135
|
${schemaJsonString}`;
|
|
123
|
-
// Clone messages to avoid mutating the input
|
|
124
136
|
const finalMessages = [...messages];
|
|
125
|
-
// Find the first system message to append instructions to
|
|
126
137
|
const systemMessageIndex = finalMessages.findIndex(m => m.role === 'system');
|
|
127
138
|
if (systemMessageIndex !== -1) {
|
|
128
|
-
// Append to existing system message
|
|
129
139
|
const existingContent = finalMessages[systemMessageIndex].content;
|
|
130
140
|
finalMessages[systemMessageIndex] = {
|
|
131
141
|
...finalMessages[systemMessageIndex],
|
|
@@ -133,7 +143,6 @@ ${schemaJsonString}`;
|
|
|
133
143
|
};
|
|
134
144
|
}
|
|
135
145
|
else {
|
|
136
|
-
// Prepend new system message
|
|
137
146
|
finalMessages.unshift({
|
|
138
147
|
role: 'system',
|
|
139
148
|
content: commonPromptFooter
|
|
@@ -146,14 +155,13 @@ ${schemaJsonString}`;
|
|
|
146
155
|
return { finalMessages, schemaJsonString, response_format };
|
|
147
156
|
}
|
|
148
157
|
async function promptJson(messages, schema, options) {
|
|
149
|
-
// Default validator using AJV
|
|
150
158
|
const defaultValidator = (data) => {
|
|
151
159
|
try {
|
|
152
160
|
const validate = ajv.compile(schema);
|
|
153
161
|
const valid = validate(data);
|
|
154
162
|
if (!valid) {
|
|
155
|
-
const errors = validate.errors
|
|
156
|
-
throw new
|
|
163
|
+
const errors = (validate.errors || []).map(e => `${e.instancePath} ${e.message}`).join(', ');
|
|
164
|
+
throw new SchemaValidationError(`AJV Validation Error: ${errors}`);
|
|
157
165
|
}
|
|
158
166
|
return data;
|
|
159
167
|
}
|
|
@@ -169,29 +177,40 @@ ${schemaJsonString}`;
|
|
|
169
177
|
jsonData = await _parseOrFixJson(llmResponseString, schemaJsonString, options);
|
|
170
178
|
}
|
|
171
179
|
catch (parseError) {
|
|
172
|
-
|
|
180
|
+
// Only wrap SyntaxErrors (JSON parse errors) for retry.
|
|
181
|
+
if (parseError instanceof SyntaxError) {
|
|
182
|
+
const errorMessage = `Your previous response resulted in an error.
|
|
173
183
|
Error Type: JSON_PARSE_ERROR
|
|
174
184
|
Error Details: ${parseError.message}
|
|
175
185
|
The response provided was not valid JSON. Please correct it.`;
|
|
176
|
-
|
|
186
|
+
throw new createLlmRetryClient_js_1.LlmRetryError(errorMessage, 'JSON_PARSE_ERROR', undefined, llmResponseString);
|
|
187
|
+
}
|
|
188
|
+
// Rethrow other errors (e.g. fatal errors, runtime errors)
|
|
189
|
+
throw parseError;
|
|
177
190
|
}
|
|
178
191
|
try {
|
|
179
192
|
const validatedData = await _validateOrFix(jsonData, validator, schemaJsonString, options);
|
|
180
193
|
return validatedData;
|
|
181
194
|
}
|
|
182
195
|
catch (validationError) {
|
|
183
|
-
//
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
196
|
+
// Only wrap known validation errors for retry.
|
|
197
|
+
if (validationError instanceof SchemaValidationError) {
|
|
198
|
+
const rawResponseForError = JSON.stringify(jsonData, null, 2);
|
|
199
|
+
const errorDetails = validationError.message;
|
|
200
|
+
const errorMessage = `Your previous response resulted in an error.
|
|
187
201
|
Error Type: SCHEMA_VALIDATION_ERROR
|
|
188
202
|
Error Details: ${errorDetails}
|
|
189
203
|
The response was valid JSON but did not conform to the required schema. Please review the errors and the schema to provide a corrected response.`;
|
|
190
|
-
|
|
204
|
+
throw new createLlmRetryClient_js_1.LlmRetryError(errorMessage, 'CUSTOM_ERROR', validationError, rawResponseForError);
|
|
205
|
+
}
|
|
206
|
+
// Rethrow other errors
|
|
207
|
+
throw validationError;
|
|
191
208
|
}
|
|
192
209
|
};
|
|
210
|
+
const { maxRetries, useResponseFormat: _useResponseFormat, beforeValidation, validator: _validator, ...restOptions } = options || {};
|
|
193
211
|
const retryOptions = {
|
|
194
|
-
...
|
|
212
|
+
...restOptions,
|
|
213
|
+
maxRetries,
|
|
195
214
|
messages: finalMessages,
|
|
196
215
|
response_format,
|
|
197
216
|
validate: processResponse
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import OpenAI from "openai";
|
|
2
2
|
import type PQueue from 'p-queue';
|
|
3
|
+
export declare class LlmFatalError extends Error {
|
|
4
|
+
readonly cause?: any | undefined;
|
|
5
|
+
constructor(message: string, cause?: any | undefined);
|
|
6
|
+
}
|
|
3
7
|
export declare function countChars(message: OpenAI.Chat.Completions.ChatCompletionMessageParam): number;
|
|
4
8
|
export declare function truncateSingleMessage(message: OpenAI.Chat.Completions.ChatCompletionMessageParam, charLimit: number): OpenAI.Chat.Completions.ChatCompletionMessageParam;
|
|
5
9
|
export declare function truncateMessages(messages: OpenAI.Chat.Completions.ChatCompletionMessageParam[], limit: number): OpenAI.Chat.Completions.ChatCompletionMessageParam[];
|
|
@@ -21,12 +25,24 @@ export type OpenRouterResponseFormat = {
|
|
|
21
25
|
};
|
|
22
26
|
};
|
|
23
27
|
/**
|
|
24
|
-
*
|
|
25
|
-
* These
|
|
26
|
-
* 'messages' is a required property, inherited from OpenAI.Chat.Completions.ChatCompletionCreateParamsNonStreaming.
|
|
28
|
+
* Request-level options passed to the OpenAI SDK.
|
|
29
|
+
* These are separate from the body parameters.
|
|
27
30
|
*/
|
|
28
|
-
export interface
|
|
29
|
-
|
|
31
|
+
export interface LlmRequestOptions {
|
|
32
|
+
headers?: Record<string, string>;
|
|
33
|
+
signal?: AbortSignal;
|
|
34
|
+
timeout?: number;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Merges two LlmRequestOptions objects.
|
|
38
|
+
* Headers are merged (override wins on conflict), other properties are replaced.
|
|
39
|
+
*/
|
|
40
|
+
export declare function mergeRequestOptions(base?: LlmRequestOptions, override?: LlmRequestOptions): LlmRequestOptions | undefined;
|
|
41
|
+
/**
|
|
42
|
+
* Common options shared by all prompt functions.
|
|
43
|
+
* Does NOT include messages - those are handled separately.
|
|
44
|
+
*/
|
|
45
|
+
export interface LlmCommonOptions {
|
|
30
46
|
model?: ModelConfig;
|
|
31
47
|
retries?: number;
|
|
32
48
|
/** @deprecated Use `reasoning` object instead. */
|
|
@@ -35,6 +51,31 @@ export interface LlmPromptOptions extends Omit<OpenAI.Chat.Completions.ChatCompl
|
|
|
35
51
|
image_config?: {
|
|
36
52
|
aspect_ratio?: string;
|
|
37
53
|
};
|
|
54
|
+
requestOptions?: LlmRequestOptions;
|
|
55
|
+
temperature?: number;
|
|
56
|
+
max_tokens?: number;
|
|
57
|
+
top_p?: number;
|
|
58
|
+
frequency_penalty?: number;
|
|
59
|
+
presence_penalty?: number;
|
|
60
|
+
stop?: string | string[];
|
|
61
|
+
reasoning_effort?: 'low' | 'medium' | 'high';
|
|
62
|
+
seed?: number;
|
|
63
|
+
user?: string;
|
|
64
|
+
tools?: OpenAI.Chat.Completions.ChatCompletionTool[];
|
|
65
|
+
tool_choice?: OpenAI.Chat.Completions.ChatCompletionToolChoiceOption;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Options for the individual "prompt" function calls.
|
|
69
|
+
* Allows messages as string or array for convenience.
|
|
70
|
+
*/
|
|
71
|
+
export interface LlmPromptOptions extends LlmCommonOptions {
|
|
72
|
+
messages: string | OpenAI.Chat.Completions.ChatCompletionMessageParam[];
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Internal normalized params - messages is always an array.
|
|
76
|
+
*/
|
|
77
|
+
export interface LlmPromptParams extends LlmCommonOptions {
|
|
78
|
+
messages: OpenAI.Chat.Completions.ChatCompletionMessageParam[];
|
|
38
79
|
}
|
|
39
80
|
/**
|
|
40
81
|
* Options required to create an instance of the LlmClient.
|
|
@@ -45,8 +86,13 @@ export interface CreateLlmClientParams {
|
|
|
45
86
|
defaultModel: ModelConfig;
|
|
46
87
|
maxConversationChars?: number;
|
|
47
88
|
queue?: PQueue;
|
|
89
|
+
defaultRequestOptions?: LlmRequestOptions;
|
|
48
90
|
}
|
|
49
|
-
|
|
91
|
+
/**
|
|
92
|
+
* Normalizes input arguments to LlmPromptParams.
|
|
93
|
+
* Handles string shorthand and messages-as-string.
|
|
94
|
+
*/
|
|
95
|
+
export declare function normalizeOptions(arg1: string | LlmPromptOptions, arg2?: LlmCommonOptions): LlmPromptParams;
|
|
50
96
|
/**
|
|
51
97
|
* Factory function that creates a GPT "prompt" function.
|
|
52
98
|
* @param params - The core dependencies (API key, base URL, default model).
|
|
@@ -54,15 +100,15 @@ export declare function normalizeOptions(arg1: string | LlmPromptOptions, arg2?:
|
|
|
54
100
|
*/
|
|
55
101
|
export declare function createLlmClient(params: CreateLlmClientParams): {
|
|
56
102
|
prompt: {
|
|
57
|
-
(content: string, options?:
|
|
103
|
+
(content: string, options?: LlmCommonOptions): Promise<OpenAI.Chat.Completions.ChatCompletion>;
|
|
58
104
|
(options: LlmPromptOptions): Promise<OpenAI.Chat.Completions.ChatCompletion>;
|
|
59
105
|
};
|
|
60
106
|
promptText: {
|
|
61
|
-
(content: string, options?:
|
|
107
|
+
(content: string, options?: LlmCommonOptions): Promise<string>;
|
|
62
108
|
(options: LlmPromptOptions): Promise<string>;
|
|
63
109
|
};
|
|
64
110
|
promptImage: {
|
|
65
|
-
(content: string, options?:
|
|
111
|
+
(content: string, options?: LlmCommonOptions): Promise<Buffer>;
|
|
66
112
|
(options: LlmPromptOptions): Promise<Buffer>;
|
|
67
113
|
};
|
|
68
114
|
};
|
package/dist/createLlmClient.js
CHANGED
|
@@ -1,11 +1,23 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.LlmFatalError = void 0;
|
|
3
4
|
exports.countChars = countChars;
|
|
4
5
|
exports.truncateSingleMessage = truncateSingleMessage;
|
|
5
6
|
exports.truncateMessages = truncateMessages;
|
|
7
|
+
exports.mergeRequestOptions = mergeRequestOptions;
|
|
6
8
|
exports.normalizeOptions = normalizeOptions;
|
|
7
9
|
exports.createLlmClient = createLlmClient;
|
|
8
10
|
const retryUtils_js_1 = require("./retryUtils.js");
|
|
11
|
+
class LlmFatalError extends Error {
|
|
12
|
+
cause;
|
|
13
|
+
constructor(message, cause) {
|
|
14
|
+
super(message);
|
|
15
|
+
this.cause = cause;
|
|
16
|
+
this.name = 'LlmFatalError';
|
|
17
|
+
this.cause = cause;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
exports.LlmFatalError = LlmFatalError;
|
|
9
21
|
function countChars(message) {
|
|
10
22
|
if (!message.content)
|
|
11
23
|
return 0;
|
|
@@ -49,14 +61,12 @@ function truncateSingleMessage(message, charLimit) {
|
|
|
49
61
|
return messageCopy;
|
|
50
62
|
}
|
|
51
63
|
if (Array.isArray(messageCopy.content)) {
|
|
52
|
-
// Complex case: multipart message.
|
|
53
|
-
// Strategy: consolidate text, remove images if needed, then truncate text.
|
|
54
64
|
const textParts = messageCopy.content.filter((p) => p.type === 'text');
|
|
55
65
|
const imageParts = messageCopy.content.filter((p) => p.type === 'image_url');
|
|
56
66
|
let combinedText = textParts.map((p) => p.text).join('\n');
|
|
57
67
|
let keptImages = [...imageParts];
|
|
58
68
|
while (combinedText.length + (keptImages.length * 2500) > charLimit && keptImages.length > 0) {
|
|
59
|
-
keptImages.pop();
|
|
69
|
+
keptImages.pop();
|
|
60
70
|
}
|
|
61
71
|
const imageChars = keptImages.length * 2500;
|
|
62
72
|
const textCharLimit = charLimit - imageChars;
|
|
@@ -89,7 +99,6 @@ function truncateMessages(messages, limit) {
|
|
|
89
99
|
}
|
|
90
100
|
const mutableOtherMessages = JSON.parse(JSON.stringify(otherMessages));
|
|
91
101
|
let excessChars = totalChars - limit;
|
|
92
|
-
// Truncate messages starting from the second one.
|
|
93
102
|
for (let i = 1; i < mutableOtherMessages.length; i++) {
|
|
94
103
|
if (excessChars <= 0)
|
|
95
104
|
break;
|
|
@@ -100,7 +109,6 @@ function truncateMessages(messages, limit) {
|
|
|
100
109
|
mutableOtherMessages[i] = truncateSingleMessage(message, newCharCount);
|
|
101
110
|
excessChars -= charsToCut;
|
|
102
111
|
}
|
|
103
|
-
// If still over limit, truncate the first message.
|
|
104
112
|
if (excessChars > 0) {
|
|
105
113
|
const firstMessage = mutableOtherMessages[0];
|
|
106
114
|
const firstMessageChars = countChars(firstMessage);
|
|
@@ -108,7 +116,6 @@ function truncateMessages(messages, limit) {
|
|
|
108
116
|
const newCharCount = firstMessageChars - charsToCut;
|
|
109
117
|
mutableOtherMessages[0] = truncateSingleMessage(firstMessage, newCharCount);
|
|
110
118
|
}
|
|
111
|
-
// Filter out empty messages (char count is 0)
|
|
112
119
|
const finalMessages = mutableOtherMessages.filter(msg => countChars(msg) > 0);
|
|
113
120
|
return systemMessage ? [systemMessage, ...finalMessages] : finalMessages;
|
|
114
121
|
}
|
|
@@ -135,7 +142,6 @@ function concatMessageText(messages) {
|
|
|
135
142
|
}
|
|
136
143
|
function getPromptSummary(messages) {
|
|
137
144
|
const fullText = concatMessageText(messages);
|
|
138
|
-
// Replace multiple whitespace chars with a single space and trim.
|
|
139
145
|
const cleanedText = fullText.replace(/\s+/g, ' ').trim();
|
|
140
146
|
if (cleanedText.length <= 50) {
|
|
141
147
|
return cleanedText;
|
|
@@ -149,6 +155,30 @@ function getPromptSummary(messages) {
|
|
|
149
155
|
const middle = cleanedText.substring(midStart, midEnd);
|
|
150
156
|
return `${start}...${middle}...${end}`;
|
|
151
157
|
}
|
|
158
|
+
/**
|
|
159
|
+
* Merges two LlmRequestOptions objects.
|
|
160
|
+
* Headers are merged (override wins on conflict), other properties are replaced.
|
|
161
|
+
*/
|
|
162
|
+
function mergeRequestOptions(base, override) {
|
|
163
|
+
if (!base && !override)
|
|
164
|
+
return undefined;
|
|
165
|
+
if (!base)
|
|
166
|
+
return override;
|
|
167
|
+
if (!override)
|
|
168
|
+
return base;
|
|
169
|
+
return {
|
|
170
|
+
...base,
|
|
171
|
+
...override,
|
|
172
|
+
headers: {
|
|
173
|
+
...base.headers,
|
|
174
|
+
...override.headers
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Normalizes input arguments to LlmPromptParams.
|
|
180
|
+
* Handles string shorthand and messages-as-string.
|
|
181
|
+
*/
|
|
152
182
|
function normalizeOptions(arg1, arg2) {
|
|
153
183
|
if (typeof arg1 === 'string') {
|
|
154
184
|
return {
|
|
@@ -171,14 +201,12 @@ function normalizeOptions(arg1, arg2) {
|
|
|
171
201
|
* @returns An async function `prompt` ready to make OpenAI calls.
|
|
172
202
|
*/
|
|
173
203
|
function createLlmClient(params) {
|
|
174
|
-
const { openai, defaultModel: factoryDefaultModel, maxConversationChars, queue } = params;
|
|
175
|
-
const getCompletionParams = (
|
|
176
|
-
const { model: callSpecificModel, messages,
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
? [{ role: 'user', content: messages }]
|
|
204
|
+
const { openai, defaultModel: factoryDefaultModel, maxConversationChars, queue, defaultRequestOptions } = params;
|
|
205
|
+
const getCompletionParams = (promptParams) => {
|
|
206
|
+
const { model: callSpecificModel, messages, retries, requestOptions, ...restApiOptions } = promptParams;
|
|
207
|
+
const finalMessages = maxConversationChars
|
|
208
|
+
? truncateMessages(messages, maxConversationChars)
|
|
180
209
|
: messages;
|
|
181
|
-
const finalMessages = maxConversationChars ? truncateMessages(messagesArray, maxConversationChars) : messagesArray;
|
|
182
210
|
const baseConfig = typeof factoryDefaultModel === 'object' && factoryDefaultModel !== null
|
|
183
211
|
? factoryDefaultModel
|
|
184
212
|
: (typeof factoryDefaultModel === 'string' ? { model: factoryDefaultModel } : {});
|
|
@@ -196,15 +224,24 @@ function createLlmClient(params) {
|
|
|
196
224
|
messages: finalMessages,
|
|
197
225
|
...restApiOptions,
|
|
198
226
|
};
|
|
199
|
-
|
|
227
|
+
const mergedRequestOptions = mergeRequestOptions(defaultRequestOptions, requestOptions);
|
|
228
|
+
return { completionParams, modelToUse, finalMessages, retries, requestOptions: mergedRequestOptions };
|
|
200
229
|
};
|
|
201
230
|
async function prompt(arg1, arg2) {
|
|
202
|
-
const
|
|
203
|
-
const { completionParams, finalMessages, retries } = getCompletionParams(
|
|
231
|
+
const promptParams = normalizeOptions(arg1, arg2);
|
|
232
|
+
const { completionParams, finalMessages, retries, requestOptions } = getCompletionParams(promptParams);
|
|
204
233
|
const promptSummary = getPromptSummary(finalMessages);
|
|
205
234
|
const apiCall = async () => {
|
|
206
235
|
const task = () => (0, retryUtils_js_1.executeWithRetry)(async () => {
|
|
207
|
-
|
|
236
|
+
try {
|
|
237
|
+
return await openai.chat.completions.create(completionParams, requestOptions);
|
|
238
|
+
}
|
|
239
|
+
catch (error) {
|
|
240
|
+
if (error?.status === 400 || error?.status === 401 || error?.status === 403) {
|
|
241
|
+
throw new LlmFatalError(error.message || 'Fatal API Error', error);
|
|
242
|
+
}
|
|
243
|
+
throw error;
|
|
244
|
+
}
|
|
208
245
|
}, async (completion) => {
|
|
209
246
|
if (completion.error) {
|
|
210
247
|
return {
|
|
@@ -213,8 +250,9 @@ function createLlmClient(params) {
|
|
|
213
250
|
}
|
|
214
251
|
return { isValid: true, data: completion };
|
|
215
252
|
}, retries ?? 3, undefined, (error) => {
|
|
216
|
-
|
|
217
|
-
|
|
253
|
+
if (error instanceof LlmFatalError)
|
|
254
|
+
return false;
|
|
255
|
+
if (error?.status === 400 || error?.status === 401 || error?.status === 403 || error?.code === 'invalid_api_key') {
|
|
218
256
|
return false;
|
|
219
257
|
}
|
|
220
258
|
return true;
|
|
@@ -225,8 +263,8 @@ function createLlmClient(params) {
|
|
|
225
263
|
return apiCall();
|
|
226
264
|
}
|
|
227
265
|
async function promptText(arg1, arg2) {
|
|
228
|
-
const
|
|
229
|
-
const response = await prompt(
|
|
266
|
+
const promptParams = normalizeOptions(arg1, arg2);
|
|
267
|
+
const response = await prompt(promptParams);
|
|
230
268
|
const content = response.choices[0]?.message?.content;
|
|
231
269
|
if (content === null || content === undefined) {
|
|
232
270
|
throw new Error("LLM returned no text content.");
|
|
@@ -234,8 +272,8 @@ function createLlmClient(params) {
|
|
|
234
272
|
return content;
|
|
235
273
|
}
|
|
236
274
|
async function promptImage(arg1, arg2) {
|
|
237
|
-
const
|
|
238
|
-
const response = await prompt(
|
|
275
|
+
const promptParams = normalizeOptions(arg1, arg2);
|
|
276
|
+
const response = await prompt(promptParams);
|
|
239
277
|
const message = response.choices[0]?.message;
|
|
240
278
|
if (message.images && Array.isArray(message.images) && message.images.length > 0) {
|
|
241
279
|
const imageUrl = message.images[0].image_url.url;
|
|
@@ -37,4 +37,76 @@ const createLlmClient_js_1 = require("./createLlmClient.js");
|
|
|
37
37
|
temperature: 0.7
|
|
38
38
|
});
|
|
39
39
|
});
|
|
40
|
+
(0, vitest_1.it)('should include requestOptions when provided', () => {
|
|
41
|
+
const result = (0, createLlmClient_js_1.normalizeOptions)('Hello world', {
|
|
42
|
+
temperature: 0.5,
|
|
43
|
+
requestOptions: {
|
|
44
|
+
headers: { 'X-Custom': 'value' },
|
|
45
|
+
timeout: 5000
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
(0, vitest_1.expect)(result).toEqual({
|
|
49
|
+
messages: [{ role: 'user', content: 'Hello world' }],
|
|
50
|
+
temperature: 0.5,
|
|
51
|
+
requestOptions: {
|
|
52
|
+
headers: { 'X-Custom': 'value' },
|
|
53
|
+
timeout: 5000
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
(0, vitest_1.describe)('mergeRequestOptions', () => {
|
|
59
|
+
(0, vitest_1.it)('should return undefined when both are undefined', () => {
|
|
60
|
+
const result = (0, createLlmClient_js_1.mergeRequestOptions)(undefined, undefined);
|
|
61
|
+
(0, vitest_1.expect)(result).toBeUndefined();
|
|
62
|
+
});
|
|
63
|
+
(0, vitest_1.it)('should return override when base is undefined', () => {
|
|
64
|
+
const override = { timeout: 5000 };
|
|
65
|
+
const result = (0, createLlmClient_js_1.mergeRequestOptions)(undefined, override);
|
|
66
|
+
(0, vitest_1.expect)(result).toBe(override);
|
|
67
|
+
});
|
|
68
|
+
(0, vitest_1.it)('should return base when override is undefined', () => {
|
|
69
|
+
const base = { timeout: 5000 };
|
|
70
|
+
const result = (0, createLlmClient_js_1.mergeRequestOptions)(base, undefined);
|
|
71
|
+
(0, vitest_1.expect)(result).toBe(base);
|
|
72
|
+
});
|
|
73
|
+
(0, vitest_1.it)('should merge headers from both', () => {
|
|
74
|
+
const base = {
|
|
75
|
+
headers: { 'X-Base': 'base-value' },
|
|
76
|
+
timeout: 5000
|
|
77
|
+
};
|
|
78
|
+
const override = {
|
|
79
|
+
headers: { 'X-Override': 'override-value' }
|
|
80
|
+
};
|
|
81
|
+
const result = (0, createLlmClient_js_1.mergeRequestOptions)(base, override);
|
|
82
|
+
(0, vitest_1.expect)(result).toEqual({
|
|
83
|
+
headers: {
|
|
84
|
+
'X-Base': 'base-value',
|
|
85
|
+
'X-Override': 'override-value'
|
|
86
|
+
},
|
|
87
|
+
timeout: 5000
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
(0, vitest_1.it)('should override scalar properties', () => {
|
|
91
|
+
const base = { timeout: 5000 };
|
|
92
|
+
const override = { timeout: 10000 };
|
|
93
|
+
const result = (0, createLlmClient_js_1.mergeRequestOptions)(base, override);
|
|
94
|
+
(0, vitest_1.expect)(result).toEqual({ timeout: 10000, headers: {} });
|
|
95
|
+
});
|
|
96
|
+
(0, vitest_1.it)('should override conflicting headers', () => {
|
|
97
|
+
const base = {
|
|
98
|
+
headers: { 'X-Shared': 'base-value', 'X-Base': 'base' }
|
|
99
|
+
};
|
|
100
|
+
const override = {
|
|
101
|
+
headers: { 'X-Shared': 'override-value', 'X-Override': 'override' }
|
|
102
|
+
};
|
|
103
|
+
const result = (0, createLlmClient_js_1.mergeRequestOptions)(base, override);
|
|
104
|
+
(0, vitest_1.expect)(result).toEqual({
|
|
105
|
+
headers: {
|
|
106
|
+
'X-Shared': 'override-value',
|
|
107
|
+
'X-Base': 'base',
|
|
108
|
+
'X-Override': 'override'
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
});
|
|
40
112
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import OpenAI from 'openai';
|
|
2
|
-
import { PromptFunction, LlmPromptOptions } from "./createLlmClient.js";
|
|
2
|
+
import { PromptFunction, LlmCommonOptions, LlmPromptOptions } from "./createLlmClient.js";
|
|
3
3
|
export declare class LlmRetryError extends Error {
|
|
4
4
|
readonly message: string;
|
|
5
5
|
readonly type: 'JSON_PARSE_ERROR' | 'CUSTOM_ERROR';
|
|
@@ -16,33 +16,38 @@ export declare class LlmRetryAttemptError extends Error {
|
|
|
16
16
|
readonly mode: 'main' | 'fallback';
|
|
17
17
|
readonly conversation: OpenAI.Chat.Completions.ChatCompletionMessageParam[];
|
|
18
18
|
readonly attemptNumber: number;
|
|
19
|
-
|
|
19
|
+
readonly error: Error;
|
|
20
|
+
constructor(message: string, mode: 'main' | 'fallback', conversation: OpenAI.Chat.Completions.ChatCompletionMessageParam[], attemptNumber: number, error: Error, options?: ErrorOptions);
|
|
20
21
|
}
|
|
21
22
|
export interface LlmRetryResponseInfo {
|
|
22
23
|
mode: 'main' | 'fallback';
|
|
23
24
|
conversation: OpenAI.Chat.Completions.ChatCompletionMessageParam[];
|
|
24
25
|
attemptNumber: number;
|
|
25
26
|
}
|
|
26
|
-
|
|
27
|
+
/**
|
|
28
|
+
* Options for retry prompt functions.
|
|
29
|
+
* Extends common options with retry-specific settings.
|
|
30
|
+
*/
|
|
31
|
+
export interface LlmRetryOptions<T = any> extends LlmCommonOptions {
|
|
27
32
|
maxRetries?: number;
|
|
28
33
|
validate?: (response: any, info: LlmRetryResponseInfo) => Promise<T>;
|
|
29
|
-
}
|
|
34
|
+
}
|
|
30
35
|
export interface CreateLlmRetryClientParams {
|
|
31
36
|
prompt: PromptFunction;
|
|
32
37
|
fallbackPrompt?: PromptFunction;
|
|
33
38
|
}
|
|
34
39
|
export declare function createLlmRetryClient(params: CreateLlmRetryClientParams): {
|
|
35
40
|
promptRetry: {
|
|
36
|
-
<T = OpenAI.Chat.Completions.ChatCompletion>(content: string, options?:
|
|
37
|
-
<T = OpenAI.Chat.Completions.ChatCompletion>(options: LlmRetryOptions<T>): Promise<T>;
|
|
41
|
+
<T = OpenAI.Chat.Completions.ChatCompletion>(content: string, options?: LlmRetryOptions<T>): Promise<T>;
|
|
42
|
+
<T = OpenAI.Chat.Completions.ChatCompletion>(options: LlmPromptOptions & LlmRetryOptions<T>): Promise<T>;
|
|
38
43
|
};
|
|
39
44
|
promptTextRetry: {
|
|
40
|
-
<T = string>(content: string, options?:
|
|
41
|
-
<T = string>(options: LlmRetryOptions<T>): Promise<T>;
|
|
45
|
+
<T = string>(content: string, options?: LlmRetryOptions<T>): Promise<T>;
|
|
46
|
+
<T = string>(options: LlmPromptOptions & LlmRetryOptions<T>): Promise<T>;
|
|
42
47
|
};
|
|
43
48
|
promptImageRetry: {
|
|
44
|
-
<T = Buffer<ArrayBufferLike>>(content: string, options?:
|
|
45
|
-
<T = Buffer<ArrayBufferLike>>(options: LlmRetryOptions<T>): Promise<T>;
|
|
49
|
+
<T = Buffer<ArrayBufferLike>>(content: string, options?: LlmRetryOptions<T>): Promise<T>;
|
|
50
|
+
<T = Buffer<ArrayBufferLike>>(options: LlmPromptOptions & LlmRetryOptions<T>): Promise<T>;
|
|
46
51
|
};
|
|
47
52
|
};
|
|
48
53
|
export type LlmRetryClient = ReturnType<typeof createLlmRetryClient>;
|
|
@@ -3,7 +3,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.LlmRetryAttemptError = exports.LlmRetryExhaustedError = exports.LlmRetryError = void 0;
|
|
4
4
|
exports.createLlmRetryClient = createLlmRetryClient;
|
|
5
5
|
const createLlmClient_js_1 = require("./createLlmClient.js");
|
|
6
|
-
// Custom error for the querier to handle, allowing retries with structured feedback.
|
|
7
6
|
class LlmRetryError extends Error {
|
|
8
7
|
message;
|
|
9
8
|
type;
|
|
@@ -28,33 +27,39 @@ class LlmRetryExhaustedError extends Error {
|
|
|
28
27
|
}
|
|
29
28
|
}
|
|
30
29
|
exports.LlmRetryExhaustedError = LlmRetryExhaustedError;
|
|
31
|
-
// This error is thrown by LlmRetryClient for each failed attempt.
|
|
32
|
-
// It wraps the underlying error (from API call or validation) and adds context.
|
|
33
30
|
class LlmRetryAttemptError extends Error {
|
|
34
31
|
message;
|
|
35
32
|
mode;
|
|
36
33
|
conversation;
|
|
37
34
|
attemptNumber;
|
|
38
|
-
|
|
35
|
+
error;
|
|
36
|
+
constructor(message, mode, conversation, attemptNumber, error, options) {
|
|
39
37
|
super(message, options);
|
|
40
38
|
this.message = message;
|
|
41
39
|
this.mode = mode;
|
|
42
40
|
this.conversation = conversation;
|
|
43
41
|
this.attemptNumber = attemptNumber;
|
|
42
|
+
this.error = error;
|
|
44
43
|
this.name = 'LlmRetryAttemptError';
|
|
45
44
|
}
|
|
46
45
|
}
|
|
47
46
|
exports.LlmRetryAttemptError = LlmRetryAttemptError;
|
|
47
|
+
function normalizeRetryOptions(arg1, arg2) {
|
|
48
|
+
const baseParams = (0, createLlmClient_js_1.normalizeOptions)(arg1, arg2);
|
|
49
|
+
return {
|
|
50
|
+
...baseParams,
|
|
51
|
+
...arg2,
|
|
52
|
+
messages: baseParams.messages
|
|
53
|
+
};
|
|
54
|
+
}
|
|
48
55
|
function constructLlmMessages(initialMessages, attemptNumber, previousError) {
|
|
49
56
|
if (attemptNumber === 0) {
|
|
50
|
-
// First attempt
|
|
51
57
|
return initialMessages;
|
|
52
58
|
}
|
|
53
59
|
if (!previousError) {
|
|
54
|
-
// Should not happen for attempt > 0, but as a safeguard...
|
|
55
60
|
throw new Error("Invariant violation: previousError is missing for a retry attempt.");
|
|
56
61
|
}
|
|
57
|
-
const cause = previousError.
|
|
62
|
+
const cause = previousError.error;
|
|
58
63
|
if (!(cause instanceof LlmRetryError)) {
|
|
59
64
|
throw Error('cause must be an instanceof LlmRetryError');
|
|
60
65
|
}
|
|
@@ -64,10 +69,8 @@ function constructLlmMessages(initialMessages, attemptNumber, previousError) {
|
|
|
64
69
|
}
|
|
65
70
|
function createLlmRetryClient(params) {
|
|
66
71
|
const { prompt, fallbackPrompt } = params;
|
|
67
|
-
async function runPromptLoop(
|
|
68
|
-
const { maxRetries = 3, validate, messages, ...restOptions } =
|
|
69
|
-
// Ensure messages is an array (normalizeOptions ensures this but types might be loose)
|
|
70
|
-
const initialMessages = messages;
|
|
72
|
+
async function runPromptLoop(retryParams, responseType) {
|
|
73
|
+
const { maxRetries = 3, validate, messages: initialMessages, ...restOptions } = retryParams;
|
|
71
74
|
let lastError;
|
|
72
75
|
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
73
76
|
const useFallback = !!fallbackPrompt && attempt > 0;
|
|
@@ -111,7 +114,6 @@ function createLlmRetryClient(params) {
|
|
|
111
114
|
throw new LlmRetryError("LLM returned no image.", 'CUSTOM_ERROR', undefined, JSON.stringify(completion));
|
|
112
115
|
}
|
|
113
116
|
}
|
|
114
|
-
// Construct conversation history for success or potential error reporting
|
|
115
117
|
const finalConversation = [...currentMessages];
|
|
116
118
|
if (assistantMessage) {
|
|
117
119
|
finalConversation.push(assistantMessage);
|
|
@@ -128,21 +130,18 @@ function createLlmRetryClient(params) {
|
|
|
128
130
|
return dataToProcess;
|
|
129
131
|
}
|
|
130
132
|
catch (error) {
|
|
133
|
+
if (error instanceof createLlmClient_js_1.LlmFatalError) {
|
|
134
|
+
const fatalAttemptError = new LlmRetryAttemptError(`Fatal error on attempt ${attempt + 1}: ${error.message}`, mode, currentMessages, attempt, error, { cause: lastError });
|
|
135
|
+
throw new LlmRetryExhaustedError(`Operation failed with fatal error on attempt ${attempt + 1}.`, { cause: fatalAttemptError });
|
|
136
|
+
}
|
|
131
137
|
if (error instanceof LlmRetryError) {
|
|
132
|
-
// This is a recoverable error, so we'll create a detailed attempt error and continue the loop.
|
|
133
138
|
const conversationForError = [...currentMessages];
|
|
134
|
-
// If the error contains the raw response (e.g. the invalid text), add it to history
|
|
135
|
-
// so the LLM knows what it generated previously.
|
|
136
139
|
if (error.rawResponse) {
|
|
137
140
|
conversationForError.push({ role: 'assistant', content: error.rawResponse });
|
|
138
141
|
}
|
|
139
|
-
|
|
140
|
-
// For raw mode, if we have details, maybe we can infer something, but usually rawResponse is key.
|
|
141
|
-
}
|
|
142
|
-
lastError = new LlmRetryAttemptError(`Attempt ${attempt + 1} failed.`, mode, conversationForError, attempt, { cause: error });
|
|
142
|
+
lastError = new LlmRetryAttemptError(`Attempt ${attempt + 1} failed: ${error.message}`, mode, conversationForError, attempt, error, { cause: lastError });
|
|
143
143
|
}
|
|
144
144
|
else {
|
|
145
|
-
// This is a non-recoverable error (e.g., network, API key), so we re-throw it immediately.
|
|
146
145
|
throw error;
|
|
147
146
|
}
|
|
148
147
|
}
|
|
@@ -150,16 +149,16 @@ function createLlmRetryClient(params) {
|
|
|
150
149
|
throw new LlmRetryExhaustedError(`Operation failed after ${maxRetries + 1} attempts.`, { cause: lastError });
|
|
151
150
|
}
|
|
152
151
|
async function promptRetry(arg1, arg2) {
|
|
153
|
-
const
|
|
154
|
-
return runPromptLoop(
|
|
152
|
+
const retryParams = normalizeRetryOptions(arg1, arg2);
|
|
153
|
+
return runPromptLoop(retryParams, 'raw');
|
|
155
154
|
}
|
|
156
155
|
async function promptTextRetry(arg1, arg2) {
|
|
157
|
-
const
|
|
158
|
-
return runPromptLoop(
|
|
156
|
+
const retryParams = normalizeRetryOptions(arg1, arg2);
|
|
157
|
+
return runPromptLoop(retryParams, 'text');
|
|
159
158
|
}
|
|
160
159
|
async function promptImageRetry(arg1, arg2) {
|
|
161
|
-
const
|
|
162
|
-
return runPromptLoop(
|
|
160
|
+
const retryParams = normalizeRetryOptions(arg1, arg2);
|
|
161
|
+
return runPromptLoop(retryParams, 'image');
|
|
163
162
|
}
|
|
164
163
|
return { promptRetry, promptTextRetry, promptImageRetry };
|
|
165
164
|
}
|
|
@@ -36,6 +36,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
36
36
|
exports.normalizeZodArgs = normalizeZodArgs;
|
|
37
37
|
exports.createZodLlmClient = createZodLlmClient;
|
|
38
38
|
const z = __importStar(require("zod"));
|
|
39
|
+
const createJsonSchemaLlmClient_js_1 = require("./createJsonSchemaLlmClient.js");
|
|
39
40
|
function isZodSchema(obj) {
|
|
40
41
|
return (typeof obj === 'object' &&
|
|
41
42
|
obj !== null &&
|
|
@@ -94,7 +95,15 @@ function createZodLlmClient(params) {
|
|
|
94
95
|
unrepresentable: 'any'
|
|
95
96
|
});
|
|
96
97
|
const zodValidator = (data) => {
|
|
97
|
-
|
|
98
|
+
try {
|
|
99
|
+
return dataExtractionSchema.parse(data);
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
if (error instanceof z.ZodError) {
|
|
103
|
+
throw new createJsonSchemaLlmClient_js_1.SchemaValidationError(error.toString(), { cause: error });
|
|
104
|
+
}
|
|
105
|
+
throw error;
|
|
106
|
+
}
|
|
98
107
|
};
|
|
99
108
|
const result = await jsonSchemaClient.promptJson(messages, schema, {
|
|
100
109
|
...options,
|
package/dist/llmFactory.d.ts
CHANGED
|
@@ -10,27 +10,27 @@ export declare function createLlm(params: CreateLlmFactoryParams): {
|
|
|
10
10
|
};
|
|
11
11
|
promptJson: <T>(messages: import("openai/resources/index.js").ChatCompletionMessageParam[], schema: Record<string, any>, options?: import("./createJsonSchemaLlmClient.js").JsonSchemaLlmClientOptions) => Promise<T>;
|
|
12
12
|
promptRetry: {
|
|
13
|
-
<T = import("openai/resources/index.js").ChatCompletion>(content: string, options?:
|
|
14
|
-
<T = import("openai/resources/index.js").ChatCompletion>(options: import("./createLlmRetryClient.js").LlmRetryOptions<T>): Promise<T>;
|
|
13
|
+
<T = import("openai/resources/index.js").ChatCompletion>(content: string, options?: import("./createLlmRetryClient.js").LlmRetryOptions<T>): Promise<T>;
|
|
14
|
+
<T = import("openai/resources/index.js").ChatCompletion>(options: import("./createLlmClient.js").LlmPromptOptions & import("./createLlmRetryClient.js").LlmRetryOptions<T>): Promise<T>;
|
|
15
15
|
};
|
|
16
16
|
promptTextRetry: {
|
|
17
|
-
<T = string>(content: string, options?:
|
|
18
|
-
<T = string>(options: import("./createLlmRetryClient.js").LlmRetryOptions<T>): Promise<T>;
|
|
17
|
+
<T = string>(content: string, options?: import("./createLlmRetryClient.js").LlmRetryOptions<T>): Promise<T>;
|
|
18
|
+
<T = string>(options: import("./createLlmClient.js").LlmPromptOptions & import("./createLlmRetryClient.js").LlmRetryOptions<T>): Promise<T>;
|
|
19
19
|
};
|
|
20
20
|
promptImageRetry: {
|
|
21
|
-
<T = Buffer<ArrayBufferLike>>(content: string, options?:
|
|
22
|
-
<T = Buffer<ArrayBufferLike>>(options: import("./createLlmRetryClient.js").LlmRetryOptions<T>): Promise<T>;
|
|
21
|
+
<T = Buffer<ArrayBufferLike>>(content: string, options?: import("./createLlmRetryClient.js").LlmRetryOptions<T>): Promise<T>;
|
|
22
|
+
<T = Buffer<ArrayBufferLike>>(options: import("./createLlmClient.js").LlmPromptOptions & import("./createLlmRetryClient.js").LlmRetryOptions<T>): Promise<T>;
|
|
23
23
|
};
|
|
24
24
|
prompt: {
|
|
25
|
-
(content: string, options?:
|
|
25
|
+
(content: string, options?: import("./createLlmClient.js").LlmCommonOptions): Promise<import("openai/resources/index.js").ChatCompletion>;
|
|
26
26
|
(options: import("./createLlmClient.js").LlmPromptOptions): Promise<import("openai/resources/index.js").ChatCompletion>;
|
|
27
27
|
};
|
|
28
28
|
promptText: {
|
|
29
|
-
(content: string, options?:
|
|
29
|
+
(content: string, options?: import("./createLlmClient.js").LlmCommonOptions): Promise<string>;
|
|
30
30
|
(options: import("./createLlmClient.js").LlmPromptOptions): Promise<string>;
|
|
31
31
|
};
|
|
32
32
|
promptImage: {
|
|
33
|
-
(content: string, options?:
|
|
33
|
+
(content: string, options?: import("./createLlmClient.js").LlmCommonOptions): Promise<Buffer>;
|
|
34
34
|
(options: import("./createLlmClient.js").LlmPromptOptions): Promise<Buffer>;
|
|
35
35
|
};
|
|
36
36
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "llm-fns",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.20",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"ajv": "^8.17.1",
|
|
15
15
|
"openai": "^6.9.1",
|
|
16
16
|
"undici": "^7.16.0",
|
|
17
|
-
"zod": "^4.1
|
|
17
|
+
"zod": "^4.2.1"
|
|
18
18
|
},
|
|
19
19
|
"devDependencies": {
|
|
20
20
|
"@keyv/sqlite": "^4.0.6",
|
package/readme.md
CHANGED
|
@@ -27,6 +27,7 @@ const llm = createLlm({
|
|
|
27
27
|
// cache: Cache instance (cache-manager)
|
|
28
28
|
// queue: PQueue instance for concurrency control
|
|
29
29
|
// maxConversationChars: number (auto-truncation)
|
|
30
|
+
// defaultRequestOptions: { headers, timeout, signal }
|
|
30
31
|
});
|
|
31
32
|
```
|
|
32
33
|
|
|
@@ -214,8 +215,14 @@ const res = await llm.prompt({
|
|
|
214
215
|
|
|
215
216
|
// Library Extensions
|
|
216
217
|
model: "gpt-4o", // Override default model for this call
|
|
217
|
-
ttl: 5000, // Cache this specific call for 5s (in ms)
|
|
218
218
|
retries: 5, // Retry network errors 5 times
|
|
219
|
+
|
|
220
|
+
// Request-level options (headers, timeout, abort signal)
|
|
221
|
+
requestOptions: {
|
|
222
|
+
headers: { 'X-Cache-Salt': 'v2' }, // Affects cache key
|
|
223
|
+
timeout: 60000,
|
|
224
|
+
signal: abortController.signal
|
|
225
|
+
}
|
|
219
226
|
});
|
|
220
227
|
```
|
|
221
228
|
|
|
@@ -299,7 +306,6 @@ const gameState = await llm.promptZod(
|
|
|
299
306
|
model: "google/gemini-flash-1.5",
|
|
300
307
|
disableJsonFixer: true, // Turn off the automatic JSON repair agent
|
|
301
308
|
maxRetries: 0, // Fail immediately on error
|
|
302
|
-
ttl: 60000 // Cache result
|
|
303
309
|
}
|
|
304
310
|
);
|
|
305
311
|
```
|