llm-fns 1.0.3 → 1.0.5
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/createLlmClient.d.ts +79 -0
- package/{src/createLlmClient.ts → dist/createLlmClient.js} +82 -176
- package/dist/createLlmClient.spec.d.ts +1 -0
- package/dist/createLlmClient.spec.js +40 -0
- package/dist/createLlmRetryClient.d.ts +47 -0
- package/{src/createLlmRetryClient.ts → dist/createLlmRetryClient.js} +67 -146
- package/dist/createZodLlmClient.d.ts +45 -0
- package/{src/createZodLlmClient.ts → dist/createZodLlmClient.js} +85 -202
- package/dist/createZodLlmClient.spec.d.ts +1 -0
- package/dist/createZodLlmClient.spec.js +64 -0
- package/dist/index.js +21 -0
- package/dist/llmFactory.d.ts +43 -0
- package/dist/llmFactory.js +21 -0
- package/dist/retryUtils.d.ts +23 -0
- package/{src/retryUtils.ts → dist/retryUtils.js} +9 -32
- package/package.json +6 -2
- package/scripts/release.sh +0 -32
- package/src/createLlmClient.spec.ts +0 -42
- package/src/createZodLlmClient.spec.ts +0 -76
- package/src/llmFactory.ts +0 -26
- package/tests/basic.test.ts +0 -47
- package/tests/env.ts +0 -16
- package/tests/setup.ts +0 -24
- package/tests/zod.test.ts +0 -178
- package/tsconfig.json +0 -22
- /package/{src/index.ts → dist/index.d.ts} +0 -0
|
@@ -1,129 +1,95 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.LlmRetryAttemptError = exports.LlmRetryExhaustedError = exports.LlmRetryError = void 0;
|
|
4
|
+
exports.createLlmRetryClient = createLlmRetryClient;
|
|
5
|
+
const createLlmClient_js_1 = require("./createLlmClient.js");
|
|
4
6
|
// Custom error for the querier to handle, allowing retries with structured feedback.
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
) {
|
|
7
|
+
class LlmRetryError extends Error {
|
|
8
|
+
message;
|
|
9
|
+
type;
|
|
10
|
+
details;
|
|
11
|
+
rawResponse;
|
|
12
|
+
constructor(message, type, details, rawResponse) {
|
|
12
13
|
super(message);
|
|
14
|
+
this.message = message;
|
|
15
|
+
this.type = type;
|
|
16
|
+
this.details = details;
|
|
17
|
+
this.rawResponse = rawResponse;
|
|
13
18
|
this.name = 'LlmRetryError';
|
|
14
19
|
}
|
|
15
20
|
}
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
options?: ErrorOptions
|
|
21
|
-
) {
|
|
21
|
+
exports.LlmRetryError = LlmRetryError;
|
|
22
|
+
class LlmRetryExhaustedError extends Error {
|
|
23
|
+
message;
|
|
24
|
+
constructor(message, options) {
|
|
22
25
|
super(message, options);
|
|
26
|
+
this.message = message;
|
|
23
27
|
this.name = 'LlmRetryExhaustedError';
|
|
24
28
|
}
|
|
25
29
|
}
|
|
26
|
-
|
|
30
|
+
exports.LlmRetryExhaustedError = LlmRetryExhaustedError;
|
|
27
31
|
// This error is thrown by LlmRetryClient for each failed attempt.
|
|
28
32
|
// It wraps the underlying error (from API call or validation) and adds context.
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
options?: ErrorOptions
|
|
36
|
-
) {
|
|
33
|
+
class LlmRetryAttemptError extends Error {
|
|
34
|
+
message;
|
|
35
|
+
mode;
|
|
36
|
+
conversation;
|
|
37
|
+
attemptNumber;
|
|
38
|
+
constructor(message, mode, conversation, attemptNumber, options) {
|
|
37
39
|
super(message, options);
|
|
40
|
+
this.message = message;
|
|
41
|
+
this.mode = mode;
|
|
42
|
+
this.conversation = conversation;
|
|
43
|
+
this.attemptNumber = attemptNumber;
|
|
38
44
|
this.name = 'LlmRetryAttemptError';
|
|
39
45
|
}
|
|
40
46
|
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
mode: 'main' | 'fallback';
|
|
44
|
-
conversation: OpenAI.Chat.Completions.ChatCompletionMessageParam[];
|
|
45
|
-
attemptNumber: number;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export type LlmRetryOptions<T = any> = LlmPromptOptions & {
|
|
49
|
-
maxRetries?: number;
|
|
50
|
-
validate?: (response: any, info: LlmRetryResponseInfo) => Promise<T>;
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
export interface CreateLlmRetryClientParams {
|
|
54
|
-
prompt: PromptFunction;
|
|
55
|
-
fallbackPrompt?: PromptFunction;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function constructLlmMessages(
|
|
59
|
-
initialMessages: OpenAI.Chat.Completions.ChatCompletionMessageParam[],
|
|
60
|
-
attemptNumber: number,
|
|
61
|
-
previousError?: LlmRetryAttemptError
|
|
62
|
-
): OpenAI.Chat.Completions.ChatCompletionMessageParam[] {
|
|
47
|
+
exports.LlmRetryAttemptError = LlmRetryAttemptError;
|
|
48
|
+
function constructLlmMessages(initialMessages, attemptNumber, previousError) {
|
|
63
49
|
if (attemptNumber === 0) {
|
|
64
50
|
// First attempt
|
|
65
51
|
return initialMessages;
|
|
66
52
|
}
|
|
67
|
-
|
|
68
53
|
if (!previousError) {
|
|
69
54
|
// Should not happen for attempt > 0, but as a safeguard...
|
|
70
55
|
throw new Error("Invariant violation: previousError is missing for a retry attempt.");
|
|
71
56
|
}
|
|
72
57
|
const cause = previousError.cause;
|
|
73
|
-
|
|
74
58
|
if (!(cause instanceof LlmRetryError)) {
|
|
75
|
-
throw Error('cause must be an instanceof LlmRetryError')
|
|
59
|
+
throw Error('cause must be an instanceof LlmRetryError');
|
|
76
60
|
}
|
|
77
|
-
|
|
78
|
-
const messages: OpenAI.Chat.Completions.ChatCompletionMessageParam[] = [...previousError.conversation];
|
|
79
|
-
|
|
61
|
+
const messages = [...previousError.conversation];
|
|
80
62
|
messages.push({ role: "user", content: cause.message });
|
|
81
|
-
|
|
82
63
|
return messages;
|
|
83
64
|
}
|
|
84
|
-
|
|
85
|
-
export function createLlmRetryClient(params: CreateLlmRetryClientParams) {
|
|
65
|
+
function createLlmRetryClient(params) {
|
|
86
66
|
const { prompt, fallbackPrompt } = params;
|
|
87
|
-
|
|
88
|
-
async function runPromptLoop<T>(
|
|
89
|
-
options: LlmRetryOptions<T>,
|
|
90
|
-
responseType: 'raw' | 'text' | 'image'
|
|
91
|
-
): Promise<T> {
|
|
67
|
+
async function runPromptLoop(options, responseType) {
|
|
92
68
|
const { maxRetries = 3, validate, messages, ...restOptions } = options;
|
|
93
|
-
|
|
94
69
|
// Ensure messages is an array (normalizeOptions ensures this but types might be loose)
|
|
95
|
-
const initialMessages = messages
|
|
96
|
-
|
|
97
|
-
let lastError: LlmRetryAttemptError | undefined;
|
|
98
|
-
|
|
70
|
+
const initialMessages = messages;
|
|
71
|
+
let lastError;
|
|
99
72
|
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
100
73
|
const useFallback = !!fallbackPrompt && attempt > 0;
|
|
101
|
-
const currentPrompt = useFallback ? fallbackPrompt
|
|
74
|
+
const currentPrompt = useFallback ? fallbackPrompt : prompt;
|
|
102
75
|
const mode = useFallback ? 'fallback' : 'main';
|
|
103
|
-
|
|
104
|
-
const currentMessages = constructLlmMessages(
|
|
105
|
-
initialMessages,
|
|
106
|
-
attempt,
|
|
107
|
-
lastError
|
|
108
|
-
);
|
|
109
|
-
|
|
76
|
+
const currentMessages = constructLlmMessages(initialMessages, attempt, lastError);
|
|
110
77
|
try {
|
|
111
78
|
const completion = await currentPrompt({
|
|
112
79
|
messages: currentMessages,
|
|
113
80
|
...restOptions,
|
|
114
81
|
});
|
|
115
|
-
|
|
116
82
|
const assistantMessage = completion.choices[0]?.message;
|
|
117
|
-
let dataToProcess
|
|
118
|
-
|
|
83
|
+
let dataToProcess = completion;
|
|
119
84
|
if (responseType === 'text') {
|
|
120
85
|
const content = assistantMessage?.content;
|
|
121
86
|
if (content === null || content === undefined) {
|
|
122
87
|
throw new LlmRetryError("LLM returned no text content.", 'CUSTOM_ERROR', undefined, JSON.stringify(completion));
|
|
123
88
|
}
|
|
124
89
|
dataToProcess = content;
|
|
125
|
-
}
|
|
126
|
-
|
|
90
|
+
}
|
|
91
|
+
else if (responseType === 'image') {
|
|
92
|
+
const messageAny = assistantMessage;
|
|
127
93
|
if (messageAny.images && Array.isArray(messageAny.images) && messageAny.images.length > 0) {
|
|
128
94
|
const imageUrl = messageAny.images[0].image_url.url;
|
|
129
95
|
if (typeof imageUrl === 'string') {
|
|
@@ -131,114 +97,69 @@ export function createLlmRetryClient(params: CreateLlmRetryClientParams) {
|
|
|
131
97
|
const imgRes = await fetch(imageUrl);
|
|
132
98
|
const arrayBuffer = await imgRes.arrayBuffer();
|
|
133
99
|
dataToProcess = Buffer.from(arrayBuffer);
|
|
134
|
-
}
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
135
102
|
const base64Data = imageUrl.replace(/^data:image\/\w+;base64,/, "");
|
|
136
103
|
dataToProcess = Buffer.from(base64Data, 'base64');
|
|
137
104
|
}
|
|
138
|
-
}
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
139
107
|
throw new LlmRetryError("LLM returned invalid image URL.", 'CUSTOM_ERROR', undefined, JSON.stringify(completion));
|
|
140
108
|
}
|
|
141
|
-
}
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
142
111
|
throw new LlmRetryError("LLM returned no image.", 'CUSTOM_ERROR', undefined, JSON.stringify(completion));
|
|
143
112
|
}
|
|
144
113
|
}
|
|
145
|
-
|
|
146
114
|
// Construct conversation history for success or potential error reporting
|
|
147
115
|
const finalConversation = [...currentMessages];
|
|
148
116
|
if (assistantMessage) {
|
|
149
117
|
finalConversation.push(assistantMessage);
|
|
150
118
|
}
|
|
151
|
-
|
|
152
|
-
const info: LlmRetryResponseInfo = {
|
|
119
|
+
const info = {
|
|
153
120
|
mode,
|
|
154
121
|
conversation: finalConversation,
|
|
155
122
|
attemptNumber: attempt,
|
|
156
123
|
};
|
|
157
|
-
|
|
158
124
|
if (validate) {
|
|
159
125
|
const result = await validate(dataToProcess, info);
|
|
160
126
|
return result;
|
|
161
127
|
}
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
} catch (error: any) {
|
|
128
|
+
return dataToProcess;
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
166
131
|
if (error instanceof LlmRetryError) {
|
|
167
132
|
// This is a recoverable error, so we'll create a detailed attempt error and continue the loop.
|
|
168
133
|
const conversationForError = [...currentMessages];
|
|
169
|
-
|
|
170
134
|
// If the error contains the raw response (e.g. the invalid text), add it to history
|
|
171
135
|
// so the LLM knows what it generated previously.
|
|
172
136
|
if (error.rawResponse) {
|
|
173
137
|
conversationForError.push({ role: 'assistant', content: error.rawResponse });
|
|
174
|
-
}
|
|
138
|
+
}
|
|
139
|
+
else if (responseType === 'raw' && error.details) {
|
|
175
140
|
// For raw mode, if we have details, maybe we can infer something, but usually rawResponse is key.
|
|
176
141
|
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
mode,
|
|
181
|
-
conversationForError,
|
|
182
|
-
attempt,
|
|
183
|
-
{ cause: error }
|
|
184
|
-
);
|
|
185
|
-
} else {
|
|
142
|
+
lastError = new LlmRetryAttemptError(`Attempt ${attempt + 1} failed.`, mode, conversationForError, attempt, { cause: error });
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
186
145
|
// This is a non-recoverable error (e.g., network, API key), so we re-throw it immediately.
|
|
187
146
|
throw error;
|
|
188
147
|
}
|
|
189
148
|
}
|
|
190
149
|
}
|
|
191
|
-
|
|
192
|
-
throw new LlmRetryExhaustedError(
|
|
193
|
-
`Operation failed after ${maxRetries + 1} attempts.`,
|
|
194
|
-
{ cause: lastError }
|
|
195
|
-
);
|
|
150
|
+
throw new LlmRetryExhaustedError(`Operation failed after ${maxRetries + 1} attempts.`, { cause: lastError });
|
|
196
151
|
}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
content: string,
|
|
200
|
-
options?: Omit<LlmRetryOptions<T>, 'messages'>
|
|
201
|
-
): Promise<T>;
|
|
202
|
-
async function promptRetry<T = OpenAI.Chat.Completions.ChatCompletion>(
|
|
203
|
-
options: LlmRetryOptions<T>
|
|
204
|
-
): Promise<T>;
|
|
205
|
-
async function promptRetry<T = OpenAI.Chat.Completions.ChatCompletion>(
|
|
206
|
-
arg1: string | LlmRetryOptions<T>,
|
|
207
|
-
arg2?: Omit<LlmRetryOptions<T>, 'messages'>
|
|
208
|
-
): Promise<T> {
|
|
209
|
-
const options = normalizeOptions(arg1, arg2) as LlmRetryOptions<T>;
|
|
152
|
+
async function promptRetry(arg1, arg2) {
|
|
153
|
+
const options = (0, createLlmClient_js_1.normalizeOptions)(arg1, arg2);
|
|
210
154
|
return runPromptLoop(options, 'raw');
|
|
211
155
|
}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
content: string,
|
|
215
|
-
options?: Omit<LlmRetryOptions<T>, 'messages'>
|
|
216
|
-
): Promise<T>;
|
|
217
|
-
async function promptTextRetry<T = string>(
|
|
218
|
-
options: LlmRetryOptions<T>
|
|
219
|
-
): Promise<T>;
|
|
220
|
-
async function promptTextRetry<T = string>(
|
|
221
|
-
arg1: string | LlmRetryOptions<T>,
|
|
222
|
-
arg2?: Omit<LlmRetryOptions<T>, 'messages'>
|
|
223
|
-
): Promise<T> {
|
|
224
|
-
const options = normalizeOptions(arg1, arg2) as LlmRetryOptions<T>;
|
|
156
|
+
async function promptTextRetry(arg1, arg2) {
|
|
157
|
+
const options = (0, createLlmClient_js_1.normalizeOptions)(arg1, arg2);
|
|
225
158
|
return runPromptLoop(options, 'text');
|
|
226
159
|
}
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
content: string,
|
|
230
|
-
options?: Omit<LlmRetryOptions<T>, 'messages'>
|
|
231
|
-
): Promise<T>;
|
|
232
|
-
async function promptImageRetry<T = Buffer>(
|
|
233
|
-
options: LlmRetryOptions<T>
|
|
234
|
-
): Promise<T>;
|
|
235
|
-
async function promptImageRetry<T = Buffer>(
|
|
236
|
-
arg1: string | LlmRetryOptions<T>,
|
|
237
|
-
arg2?: Omit<LlmRetryOptions<T>, 'messages'>
|
|
238
|
-
): Promise<T> {
|
|
239
|
-
const options = normalizeOptions(arg1, arg2) as LlmRetryOptions<T>;
|
|
160
|
+
async function promptImageRetry(arg1, arg2) {
|
|
161
|
+
const options = (0, createLlmClient_js_1.normalizeOptions)(arg1, arg2);
|
|
240
162
|
return runPromptLoop(options, 'image');
|
|
241
163
|
}
|
|
242
|
-
|
|
243
164
|
return { promptRetry, promptTextRetry, promptImageRetry };
|
|
244
165
|
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import OpenAI from 'openai';
|
|
2
|
+
import * as z from "zod";
|
|
3
|
+
import { PromptFunction, LlmPromptOptions, IsPromptCachedFunction } from "./createLlmClient.js";
|
|
4
|
+
import { ZodTypeAny } from "zod";
|
|
5
|
+
export type ZodLlmClientOptions = Omit<LlmPromptOptions, 'messages' | 'response_format'> & {
|
|
6
|
+
maxRetries?: number;
|
|
7
|
+
/**
|
|
8
|
+
* If true, passes `response_format: { type: 'json_object' }` to the model.
|
|
9
|
+
* If false, only includes the schema in the system prompt.
|
|
10
|
+
* Defaults to true.
|
|
11
|
+
*/
|
|
12
|
+
useResponseFormat?: boolean;
|
|
13
|
+
/**
|
|
14
|
+
* A hook to process the parsed JSON data before it is validated against the Zod schema.
|
|
15
|
+
* This can be used to merge partial results or perform other transformations.
|
|
16
|
+
* @param data The parsed JSON data from the LLM response.
|
|
17
|
+
* @returns The processed data to be validated.
|
|
18
|
+
*/
|
|
19
|
+
beforeValidation?: (data: any) => any;
|
|
20
|
+
};
|
|
21
|
+
export interface CreateZodLlmClientParams {
|
|
22
|
+
prompt: PromptFunction;
|
|
23
|
+
isPromptCached: IsPromptCachedFunction;
|
|
24
|
+
fallbackPrompt?: PromptFunction;
|
|
25
|
+
disableJsonFixer?: boolean;
|
|
26
|
+
}
|
|
27
|
+
export interface NormalizedZodArgs<T extends ZodTypeAny> {
|
|
28
|
+
mainInstruction: string;
|
|
29
|
+
userMessagePayload: string | OpenAI.Chat.Completions.ChatCompletionContentPart[];
|
|
30
|
+
dataExtractionSchema: T;
|
|
31
|
+
options?: ZodLlmClientOptions;
|
|
32
|
+
}
|
|
33
|
+
export declare function normalizeZodArgs<T extends ZodTypeAny>(arg1: string | T, arg2?: string | OpenAI.Chat.Completions.ChatCompletionContentPart[] | T | ZodLlmClientOptions, arg3?: T | ZodLlmClientOptions, arg4?: ZodLlmClientOptions): NormalizedZodArgs<T>;
|
|
34
|
+
export declare function createZodLlmClient(params: CreateZodLlmClientParams): {
|
|
35
|
+
promptZod: {
|
|
36
|
+
<T extends ZodTypeAny>(schema: T, options?: ZodLlmClientOptions): Promise<z.infer<T>>;
|
|
37
|
+
<T extends ZodTypeAny>(prompt: string, schema: T, options?: ZodLlmClientOptions): Promise<z.infer<T>>;
|
|
38
|
+
<T extends ZodTypeAny>(mainInstruction: string, userMessagePayload: string | OpenAI.Chat.Completions.ChatCompletionContentPart[], dataExtractionSchema: T, options?: ZodLlmClientOptions): Promise<z.infer<T>>;
|
|
39
|
+
};
|
|
40
|
+
isPromptZodCached: {
|
|
41
|
+
<T extends ZodTypeAny>(schema: T, options?: ZodLlmClientOptions): Promise<boolean>;
|
|
42
|
+
<T extends ZodTypeAny>(prompt: string, schema: T, options?: ZodLlmClientOptions): Promise<boolean>;
|
|
43
|
+
<T extends ZodTypeAny>(mainInstruction: string, userMessagePayload: string | OpenAI.Chat.Completions.ChatCompletionContentPart[], dataExtractionSchema: T, options?: ZodLlmClientOptions): Promise<boolean>;
|
|
44
|
+
};
|
|
45
|
+
};
|