llm-fns 1.0.2
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/LICENSE +21 -0
- package/package.json +27 -0
- package/readme.md +299 -0
- package/scripts/release.sh +32 -0
- package/src/createLlmClient.spec.ts +42 -0
- package/src/createLlmClient.ts +389 -0
- package/src/createLlmRetryClient.ts +244 -0
- package/src/createZodLlmClient.spec.ts +76 -0
- package/src/createZodLlmClient.ts +378 -0
- package/src/index.ts +5 -0
- package/src/llmFactory.ts +26 -0
- package/src/retryUtils.ts +91 -0
- package/tests/basic.test.ts +47 -0
- package/tests/env.ts +16 -0
- package/tests/setup.ts +24 -0
- package/tests/zod.test.ts +178 -0
- package/tsconfig.json +22 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Tobias Anhalt
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/package.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "llm-fns",
|
|
3
|
+
"version": "1.0.2",
|
|
4
|
+
"description": "",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"keywords": [],
|
|
7
|
+
"author": "",
|
|
8
|
+
"license": "MIT",
|
|
9
|
+
"dependencies": {
|
|
10
|
+
"openai": "^6.9.1",
|
|
11
|
+
"zod": "^4.1.13"
|
|
12
|
+
},
|
|
13
|
+
"devDependencies": {
|
|
14
|
+
"@keyv/sqlite": "^4.0.6",
|
|
15
|
+
"@types/node": "^20.11.0",
|
|
16
|
+
"cache-manager": "^7.2.5",
|
|
17
|
+
"dotenv": "^16.6.1",
|
|
18
|
+
"p-queue": "^9.0.1",
|
|
19
|
+
"typescript": "^5.9.3",
|
|
20
|
+
"vitest": "^1.2.1"
|
|
21
|
+
},
|
|
22
|
+
"scripts": {
|
|
23
|
+
"test": "vitest run",
|
|
24
|
+
"release": "scripts/release.sh",
|
|
25
|
+
"build": "tsc"
|
|
26
|
+
}
|
|
27
|
+
}
|
package/readme.md
ADDED
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
# LLM Client Wrapper for OpenAI
|
|
2
|
+
|
|
3
|
+
A generic, type-safe wrapper around the OpenAI API. It abstracts away the boilerplate (parsing, retries, caching, logging) while allowing raw access when needed.
|
|
4
|
+
|
|
5
|
+
Designed for power users who need to switch between simple string prompts and complex, resilient agentic workflows.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install openai zod cache-manager p-queue
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quick Start (Factory)
|
|
14
|
+
|
|
15
|
+
The `createLlm` factory bundles all functionality (Basic, Retry, Zod) into a single client.
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
import OpenAI from 'openai';
|
|
19
|
+
import { createLlm } from './src';
|
|
20
|
+
|
|
21
|
+
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
|
|
22
|
+
|
|
23
|
+
const llm = createLlm({
|
|
24
|
+
openai,
|
|
25
|
+
defaultModel: 'google/gemini-3-pro-preview',
|
|
26
|
+
// optional:
|
|
27
|
+
// cache: Cache instance (cache-manager)
|
|
28
|
+
// queue: PQueue instance for concurrency control
|
|
29
|
+
// maxConversationChars: number (auto-truncation)
|
|
30
|
+
});
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
# Use Case 1: Text & Chat (`llm.prompt` / `llm.promptText`)
|
|
36
|
+
|
|
37
|
+
### Level 1: The Easy Way (String Output)
|
|
38
|
+
Use `promptText` when you just want the answer as a string.
|
|
39
|
+
|
|
40
|
+
**Return Type:** `Promise<string>`
|
|
41
|
+
|
|
42
|
+
```typescript
|
|
43
|
+
// 1. Simple User Question
|
|
44
|
+
const ans1 = await llm.promptText("Why is the sky blue?");
|
|
45
|
+
|
|
46
|
+
// 2. System Instruction + User Question
|
|
47
|
+
const ans2 = await llm.promptText("You are a poet", "Describe the sea");
|
|
48
|
+
|
|
49
|
+
// 3. Conversation History (Chat Bots)
|
|
50
|
+
const ans3 = await llm.promptText([
|
|
51
|
+
{ role: "user", content: "Hi" },
|
|
52
|
+
{ role: "assistant", content: "Ho" }
|
|
53
|
+
]);
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Level 2: The Raw Object (Shortcuts)
|
|
57
|
+
Use `prompt` when you need the **Full OpenAI Response** (`usage`, `id`, `choices`, `finish_reason`) but want to use the **Simple Inputs** from Level 1.
|
|
58
|
+
|
|
59
|
+
**Return Type:** `Promise<OpenAI.Chat.Completions.ChatCompletion>`
|
|
60
|
+
|
|
61
|
+
```typescript
|
|
62
|
+
// Shortcut A: Single String -> User Message
|
|
63
|
+
const res1 = await llm.prompt("Why is the sky blue?");
|
|
64
|
+
console.log(res1.usage.total_tokens); // Access generic OpenAI properties
|
|
65
|
+
|
|
66
|
+
// Shortcut B: Two Strings -> System + User
|
|
67
|
+
const res2 = await llm.prompt(
|
|
68
|
+
"You are a SQL Expert.", // System
|
|
69
|
+
"Write a query for users." // User
|
|
70
|
+
);
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Level 3: Full Control (Config Object)
|
|
74
|
+
Use the **Config Object** overload for absolute control. This allows you to mix Standard OpenAI flags with Library flags.
|
|
75
|
+
|
|
76
|
+
**Input Type:** `LlmPromptOptions`
|
|
77
|
+
|
|
78
|
+
```typescript
|
|
79
|
+
const res = await llm.prompt({
|
|
80
|
+
// Standard OpenAI params
|
|
81
|
+
messages: [{ role: "user", content: "Hello" }],
|
|
82
|
+
temperature: 1.5,
|
|
83
|
+
frequency_penalty: 0.2,
|
|
84
|
+
max_tokens: 100,
|
|
85
|
+
|
|
86
|
+
// Library Extensions
|
|
87
|
+
model: "gpt-4o", // Override default model for this call
|
|
88
|
+
ttl: 5000, // Cache this specific call for 5s (in ms)
|
|
89
|
+
retries: 5, // Retry network errors 5 times
|
|
90
|
+
});
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
# Use Case 2: Images (`llm.promptImage`)
|
|
96
|
+
|
|
97
|
+
Generates an image and returns it as a `Buffer`. This handles the fetching of the URL or Base64 decoding automatically.
|
|
98
|
+
|
|
99
|
+
**Return Type:** `Promise<Buffer>`
|
|
100
|
+
|
|
101
|
+
```typescript
|
|
102
|
+
// 1. Simple Generation
|
|
103
|
+
const buffer1 = await llm.promptImage("A cyberpunk cat");
|
|
104
|
+
|
|
105
|
+
// 2. Advanced Configuration (Model & Aspect Ratio)
|
|
106
|
+
const buffer2 = await llm.promptImage({
|
|
107
|
+
messages: "A cyberpunk cat",
|
|
108
|
+
model: "dall-e-3", // Override default model
|
|
109
|
+
size: "1024x1024", // OpenAI specific params pass through
|
|
110
|
+
quality: "hd"
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// fs.writeFileSync('cat.png', buffer2);
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
# Use Case 3: Structured Data (`llm.promptZod`)
|
|
119
|
+
|
|
120
|
+
This is a high-level wrapper that employs a **Re-asking Loop**. If the LLM outputs invalid JSON or data that fails the Zod schema validation, the client automatically feeds the error back to the LLM and asks it to fix it (up to `maxRetries`).
|
|
121
|
+
|
|
122
|
+
**Return Type:** `Promise<z.infer<typeof Schema>>`
|
|
123
|
+
|
|
124
|
+
### Level 1: Generation (Schema Only)
|
|
125
|
+
The client "hallucinates" data matching the shape.
|
|
126
|
+
|
|
127
|
+
```typescript
|
|
128
|
+
import { z } from 'zod';
|
|
129
|
+
const UserSchema = z.object({ name: z.string(), age: z.number() });
|
|
130
|
+
|
|
131
|
+
// Input: Schema only
|
|
132
|
+
const user = await llm.promptZod(UserSchema);
|
|
133
|
+
// Output: { name: "Alice", age: 32 }
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### Level 2: Extraction (Injection Shortcuts)
|
|
137
|
+
Pass context alongside the schema. This automates the "System Prompt JSON Injection".
|
|
138
|
+
|
|
139
|
+
```typescript
|
|
140
|
+
// 1. Extract from String
|
|
141
|
+
const email = "Meeting at 2 PM with Bob.";
|
|
142
|
+
const event = await llm.promptZod(email, z.object({ time: z.string(), who: z.string() }));
|
|
143
|
+
|
|
144
|
+
// 2. Strict Separation (System, User, Schema)
|
|
145
|
+
// Useful for auditing code or translations where instructions must not bleed into data.
|
|
146
|
+
const analysis = await llm.promptZod(
|
|
147
|
+
"You are a security auditor.", // Arg 1: System
|
|
148
|
+
"function dangerous() {}", // Arg 2: User Data
|
|
149
|
+
SecuritySchema // Arg 3: Schema
|
|
150
|
+
);
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### Level 3: State & Options (History + Config)
|
|
154
|
+
Process full chat history into state, and use the **Options Object (4th Argument)** to control the internals (Models, Retries, Caching).
|
|
155
|
+
|
|
156
|
+
**Input Type:** `ZodLlmClientOptions`
|
|
157
|
+
|
|
158
|
+
```typescript
|
|
159
|
+
const history = [
|
|
160
|
+
{ role: "user", content: "I cast Fireball." },
|
|
161
|
+
{ role: "assistant", content: "It misses." }
|
|
162
|
+
];
|
|
163
|
+
|
|
164
|
+
const gameState = await llm.promptZod(
|
|
165
|
+
history, // Arg 1: Context
|
|
166
|
+
GameStateSchema, // Arg 2: Schema
|
|
167
|
+
{ // Arg 3: Options Override
|
|
168
|
+
model: "google/gemini-flash-1.5",
|
|
169
|
+
disableJsonFixer: true, // Turn off the automatic JSON repair agent
|
|
170
|
+
maxRetries: 0, // Fail immediately on error
|
|
171
|
+
ttl: 60000 // Cache result
|
|
172
|
+
}
|
|
173
|
+
);
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### Level 4: Hooks & Pre-processing
|
|
177
|
+
Sometimes LLMs output data that is *almost* correct (e.g., strings for numbers). You can sanitize data before Zod validation runs.
|
|
178
|
+
|
|
179
|
+
```typescript
|
|
180
|
+
const result = await llm.promptZod(MySchema, {
|
|
181
|
+
// Transform JSON before Zod validation runs
|
|
182
|
+
beforeValidation: (data) => {
|
|
183
|
+
if (data.price && typeof data.price === 'string') {
|
|
184
|
+
return { ...data, price: parseFloat(data.price) };
|
|
185
|
+
}
|
|
186
|
+
return data;
|
|
187
|
+
},
|
|
188
|
+
|
|
189
|
+
// Toggle usage of 'response_format: { type: "json_object" }'
|
|
190
|
+
// Sometimes strict JSON mode is too restrictive for creative tasks
|
|
191
|
+
useResponseFormat: false
|
|
192
|
+
});
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
---
|
|
196
|
+
|
|
197
|
+
# Use Case 4: Agentic Retry Loops (`llm.promptTextRetry`)
|
|
198
|
+
|
|
199
|
+
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.
|
|
200
|
+
|
|
201
|
+
**Return Type:** `Promise<string>` (or generic `<T>`)
|
|
202
|
+
|
|
203
|
+
```typescript
|
|
204
|
+
import { LlmRetryError } from './src';
|
|
205
|
+
|
|
206
|
+
const poem = await llm.promptTextRetry({
|
|
207
|
+
messages: "Write a haiku about coding.",
|
|
208
|
+
maxRetries: 3,
|
|
209
|
+
validate: async (text, info) => {
|
|
210
|
+
// 'info' contains history and attempt number
|
|
211
|
+
// info: { attemptNumber: number, conversation: [...], mode: 'main'|'fallback' }
|
|
212
|
+
|
|
213
|
+
if (!text.toLowerCase().includes("bug")) {
|
|
214
|
+
// This message goes back to the LLM:
|
|
215
|
+
// User: "Please include the word 'bug'."
|
|
216
|
+
throw new LlmRetryError("Please include the word 'bug'.", 'CUSTOM_ERROR');
|
|
217
|
+
}
|
|
218
|
+
return text;
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
---
|
|
224
|
+
|
|
225
|
+
# Use Case 5: Architecture & Composition
|
|
226
|
+
|
|
227
|
+
How to build the client manually to enable **Fallback Chains** and **Smart Routing**.
|
|
228
|
+
|
|
229
|
+
### Level 1: The Base Client (`createLlmClient`)
|
|
230
|
+
This creates the underlying engine that generates Text and Images. It handles Caching and Queuing but *not* Zod or Retry Loops.
|
|
231
|
+
|
|
232
|
+
```typescript
|
|
233
|
+
import { createLlmClient } from './src';
|
|
234
|
+
|
|
235
|
+
// 1. Define a CHEAP model
|
|
236
|
+
const cheapClient = createLlmClient({
|
|
237
|
+
openai,
|
|
238
|
+
defaultModel: 'google/gemini-flash-1.5'
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
// 2. Define a STRONG model
|
|
242
|
+
const strongClient = createLlmClient({
|
|
243
|
+
openai,
|
|
244
|
+
defaultModel: 'google/gemini-3-pro-preview'
|
|
245
|
+
});
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
### Level 2: The Zod Client (`createZodLlmClient`)
|
|
249
|
+
This wraps a Base Client with the "Fixer" logic. You inject the `prompt` function you want it to use.
|
|
250
|
+
|
|
251
|
+
```typescript
|
|
252
|
+
import { createZodLlmClient } from './src';
|
|
253
|
+
|
|
254
|
+
// A standard Zod client using only the strong model
|
|
255
|
+
const zodClient = createZodLlmClient({
|
|
256
|
+
prompt: strongClient.prompt,
|
|
257
|
+
isPromptCached: strongClient.isPromptCached
|
|
258
|
+
});
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
### Level 3: The Fallback Chain (Smart Routing)
|
|
262
|
+
Link two clients together. If the `prompt` function of the first client fails (retries exhausted, refusal, or unfixable JSON), it switches to the `fallbackPrompt`.
|
|
263
|
+
|
|
264
|
+
```typescript
|
|
265
|
+
const smartClient = createZodLlmClient({
|
|
266
|
+
// Primary Strategy: Try Cheap/Fast
|
|
267
|
+
prompt: cheapClient.prompt,
|
|
268
|
+
isPromptCached: cheapClient.isPromptCached,
|
|
269
|
+
|
|
270
|
+
// Fallback Strategy: Switch to Strong/Expensive
|
|
271
|
+
// This is triggered if the Primary Strategy exhausts its retries or validation fails
|
|
272
|
+
fallbackPrompt: strongClient.prompt,
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
// Usage acts exactly like the standard client
|
|
276
|
+
await smartClient.promptZod(MySchema);
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
---
|
|
280
|
+
|
|
281
|
+
# Utilities: Cache Inspection
|
|
282
|
+
|
|
283
|
+
Check if a specific prompt is already cached without making an API call (or partial cache check for Zod calls).
|
|
284
|
+
|
|
285
|
+
**Return Type:** `Promise<boolean>`
|
|
286
|
+
|
|
287
|
+
```typescript
|
|
288
|
+
const options = { messages: "Compare 5000 files..." };
|
|
289
|
+
|
|
290
|
+
// 1. Check Standard Call
|
|
291
|
+
if (await llm.isPromptCached(options)) {
|
|
292
|
+
console.log("Zero latency result available!");
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// 2. Check Zod Call (checks exact schema + prompt combo)
|
|
296
|
+
if (await llm.isPromptZodCached(options, MySchema)) {
|
|
297
|
+
// ...
|
|
298
|
+
}
|
|
299
|
+
```
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
set -e
|
|
3
|
+
set -x
|
|
4
|
+
|
|
5
|
+
VERSION_TYPE=${1:-patch}
|
|
6
|
+
|
|
7
|
+
echo "🚀 Starting release process..."
|
|
8
|
+
echo "📦 Version bump type: $VERSION_TYPE"
|
|
9
|
+
|
|
10
|
+
# 1. Build
|
|
11
|
+
echo ""
|
|
12
|
+
echo "🔨 Step 1: Building..."
|
|
13
|
+
pnpm run build
|
|
14
|
+
|
|
15
|
+
# 2. Bump Version (updates package.json, creates git commit and tag)
|
|
16
|
+
echo ""
|
|
17
|
+
echo "📈 Step 2: Bumping version ($VERSION_TYPE)..."
|
|
18
|
+
pnpm version $VERSION_TYPE
|
|
19
|
+
|
|
20
|
+
# 3. Push Changes and Tags
|
|
21
|
+
echo ""
|
|
22
|
+
echo "⬆️ Step 3: Pushing to git..."
|
|
23
|
+
git push --follow-tags
|
|
24
|
+
|
|
25
|
+
# 4. Publish to Registry
|
|
26
|
+
echo ""
|
|
27
|
+
echo "📢 Step 4: Publishing to registry..."
|
|
28
|
+
# --no-git-checks avoids errors if pnpm thinks the repo is dirty (though npm version should have committed everything)
|
|
29
|
+
pnpm publish --no-git-checks
|
|
30
|
+
|
|
31
|
+
echo ""
|
|
32
|
+
echo "✅ Release completed successfully!"
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { normalizeOptions } from './createLlmClient.js';
|
|
3
|
+
|
|
4
|
+
describe('normalizeOptions', () => {
|
|
5
|
+
it('should normalize a simple string prompt', () => {
|
|
6
|
+
const result = normalizeOptions('Hello world');
|
|
7
|
+
expect(result).toEqual({
|
|
8
|
+
messages: [{ role: 'user', content: 'Hello world' }]
|
|
9
|
+
});
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('should normalize a string prompt with options', () => {
|
|
13
|
+
const result = normalizeOptions('Hello world', { temperature: 0.5 });
|
|
14
|
+
expect(result).toEqual({
|
|
15
|
+
messages: [{ role: 'user', content: 'Hello world' }],
|
|
16
|
+
temperature: 0.5
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('should normalize an options object with string messages', () => {
|
|
21
|
+
const result = normalizeOptions({
|
|
22
|
+
messages: 'Hello world',
|
|
23
|
+
temperature: 0.7
|
|
24
|
+
});
|
|
25
|
+
expect(result).toEqual({
|
|
26
|
+
messages: [{ role: 'user', content: 'Hello world' }],
|
|
27
|
+
temperature: 0.7
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('should pass through an options object with array messages', () => {
|
|
32
|
+
const messages = [{ role: 'user', content: 'Hello' }] as any;
|
|
33
|
+
const result = normalizeOptions({
|
|
34
|
+
messages,
|
|
35
|
+
temperature: 0.7
|
|
36
|
+
});
|
|
37
|
+
expect(result).toEqual({
|
|
38
|
+
messages,
|
|
39
|
+
temperature: 0.7
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
});
|