llm-fns 1.0.21 → 1.0.22
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/package.json +1 -1
- package/readme.md +208 -0
package/package.json
CHANGED
package/readme.md
CHANGED
|
@@ -328,6 +328,68 @@ const result = await llm.promptZod(MySchema, {
|
|
|
328
328
|
});
|
|
329
329
|
```
|
|
330
330
|
|
|
331
|
+
### Level 4: Retryable Errors in Zod Transforms
|
|
332
|
+
|
|
333
|
+
You can throw `SchemaValidationError` inside Zod `.transform()` or `.refine()` to trigger the retry loop. This is useful for complex validation logic that can't be expressed in the schema itself.
|
|
334
|
+
|
|
335
|
+
```typescript
|
|
336
|
+
import { z } from 'zod';
|
|
337
|
+
import { SchemaValidationError } from './src';
|
|
338
|
+
|
|
339
|
+
const ProductSchema = z.object({
|
|
340
|
+
name: z.string(),
|
|
341
|
+
price: z.number(),
|
|
342
|
+
currency: z.string()
|
|
343
|
+
}).transform((data) => {
|
|
344
|
+
// Custom validation that triggers retry
|
|
345
|
+
if (data.price < 0) {
|
|
346
|
+
throw new SchemaValidationError(
|
|
347
|
+
`Price cannot be negative. Got: ${data.price}. Please provide a valid positive price.`
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Normalize currency
|
|
352
|
+
const validCurrencies = ['USD', 'EUR', 'GBP'];
|
|
353
|
+
if (!validCurrencies.includes(data.currency.toUpperCase())) {
|
|
354
|
+
throw new SchemaValidationError(
|
|
355
|
+
`Invalid currency "${data.currency}". Must be one of: ${validCurrencies.join(', ')}`
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
return {
|
|
360
|
+
...data,
|
|
361
|
+
currency: data.currency.toUpperCase()
|
|
362
|
+
};
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
// If the LLM returns { price: -10, ... }, the error message is sent back
|
|
366
|
+
// and the LLM gets another chance to fix it
|
|
367
|
+
const product = await llm.promptZod("Extract product info from: ...", ProductSchema);
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
**Important:** Only `SchemaValidationError` triggers the retry loop. Other errors (like `TypeError`, database errors, etc.) will bubble up immediately without retry. This prevents infinite loops when there's a bug in your transform logic.
|
|
371
|
+
|
|
372
|
+
```typescript
|
|
373
|
+
const SafeSchema = z.object({
|
|
374
|
+
userId: z.string()
|
|
375
|
+
}).transform(async (data) => {
|
|
376
|
+
// This error WILL trigger retry (user can fix the input)
|
|
377
|
+
if (!data.userId.match(/^[a-z0-9]+$/)) {
|
|
378
|
+
throw new SchemaValidationError(
|
|
379
|
+
`Invalid userId format "${data.userId}". Must be lowercase alphanumeric.`
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// This error will NOT trigger retry (it's a system error)
|
|
384
|
+
const user = await db.findUser(data.userId);
|
|
385
|
+
if (!user) {
|
|
386
|
+
throw new Error(`User not found: ${data.userId}`); // Bubbles up immediately
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
return { ...data, user };
|
|
390
|
+
});
|
|
391
|
+
```
|
|
392
|
+
|
|
331
393
|
---
|
|
332
394
|
|
|
333
395
|
# Use Case 4: Agentic Retry Loops (`llm.promptTextRetry`)
|
|
@@ -358,6 +420,152 @@ const poem = await llm.promptTextRetry({
|
|
|
358
420
|
|
|
359
421
|
---
|
|
360
422
|
|
|
423
|
+
# Error Handling
|
|
424
|
+
|
|
425
|
+
The library provides a structured error hierarchy that preserves the full context of failures across retry attempts.
|
|
426
|
+
|
|
427
|
+
## Error Types
|
|
428
|
+
|
|
429
|
+
### `LlmRetryError`
|
|
430
|
+
Thrown to signal that the current attempt failed but can be retried. The error message is sent back to the LLM.
|
|
431
|
+
|
|
432
|
+
```typescript
|
|
433
|
+
import { LlmRetryError } from './src';
|
|
434
|
+
|
|
435
|
+
throw new LlmRetryError(
|
|
436
|
+
"The response must include a title field.", // Message sent to LLM
|
|
437
|
+
'CUSTOM_ERROR', // Type: 'JSON_PARSE_ERROR' | 'CUSTOM_ERROR'
|
|
438
|
+
{ field: 'title' }, // Optional details
|
|
439
|
+
'{"name": "test"}' // Optional raw response
|
|
440
|
+
);
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
### `SchemaValidationError`
|
|
444
|
+
A specialized error for schema validation failures. Use this in Zod transforms to trigger retries.
|
|
445
|
+
|
|
446
|
+
```typescript
|
|
447
|
+
import { SchemaValidationError } from './src';
|
|
448
|
+
|
|
449
|
+
throw new SchemaValidationError("Age must be a positive number");
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
### `LlmRetryAttemptError`
|
|
453
|
+
Wraps each failed attempt with full context. These are chained together via `.cause`.
|
|
454
|
+
|
|
455
|
+
```typescript
|
|
456
|
+
interface LlmRetryAttemptError {
|
|
457
|
+
message: string;
|
|
458
|
+
mode: 'main' | 'fallback'; // Which prompt was used
|
|
459
|
+
conversation: ChatCompletionMessageParam[]; // Full message history
|
|
460
|
+
attemptNumber: number; // 0-indexed attempt number
|
|
461
|
+
error: Error; // The original error (LlmRetryError, etc.)
|
|
462
|
+
rawResponse?: string | null; // The raw LLM response
|
|
463
|
+
cause?: LlmRetryAttemptError; // Previous attempt's error (chain)
|
|
464
|
+
}
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
### `LlmRetryExhaustedError`
|
|
468
|
+
Thrown when all retry attempts have been exhausted. Contains the full chain of attempt errors.
|
|
469
|
+
|
|
470
|
+
```typescript
|
|
471
|
+
interface LlmRetryExhaustedError {
|
|
472
|
+
message: string;
|
|
473
|
+
cause: LlmRetryAttemptError; // The last attempt error (with chain to previous)
|
|
474
|
+
}
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
### `LlmFatalError`
|
|
478
|
+
Thrown for unrecoverable errors (e.g., 401 Unauthorized, 403 Forbidden). These bypass the retry loop entirely.
|
|
479
|
+
|
|
480
|
+
```typescript
|
|
481
|
+
interface LlmFatalError {
|
|
482
|
+
message: string;
|
|
483
|
+
cause?: any; // Original error
|
|
484
|
+
messages?: ChatCompletionMessageParam[]; // The messages that caused the error
|
|
485
|
+
rawResponse?: string | null; // Raw response if available
|
|
486
|
+
}
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
## Error Chain Structure
|
|
490
|
+
|
|
491
|
+
When retries are exhausted, the error chain looks like this:
|
|
492
|
+
|
|
493
|
+
```
|
|
494
|
+
LlmRetryExhaustedError
|
|
495
|
+
└── cause: LlmRetryAttemptError (Attempt 3)
|
|
496
|
+
├── error: LlmRetryError (the validation error)
|
|
497
|
+
├── conversation: [...] (full message history)
|
|
498
|
+
├── rawResponse: '{"age": "wrong3"}'
|
|
499
|
+
└── cause: LlmRetryAttemptError (Attempt 2)
|
|
500
|
+
├── error: LlmRetryError
|
|
501
|
+
├── conversation: [...]
|
|
502
|
+
├── rawResponse: '{"age": "wrong2"}'
|
|
503
|
+
└── cause: LlmRetryAttemptError (Attempt 1)
|
|
504
|
+
├── error: LlmRetryError
|
|
505
|
+
├── conversation: [...]
|
|
506
|
+
├── rawResponse: '{"age": "wrong1"}'
|
|
507
|
+
└── cause: undefined
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
## Handling Errors
|
|
511
|
+
|
|
512
|
+
```typescript
|
|
513
|
+
import {
|
|
514
|
+
LlmRetryExhaustedError,
|
|
515
|
+
LlmRetryAttemptError,
|
|
516
|
+
LlmFatalError
|
|
517
|
+
} from './src';
|
|
518
|
+
|
|
519
|
+
try {
|
|
520
|
+
const result = await llm.promptZod(MySchema);
|
|
521
|
+
} catch (error) {
|
|
522
|
+
if (error instanceof LlmRetryExhaustedError) {
|
|
523
|
+
console.log('All retries failed');
|
|
524
|
+
|
|
525
|
+
// Walk the error chain
|
|
526
|
+
let attempt = error.cause;
|
|
527
|
+
while (attempt) {
|
|
528
|
+
console.log(`Attempt ${attempt.attemptNumber + 1}:`);
|
|
529
|
+
console.log(` Mode: ${attempt.mode}`);
|
|
530
|
+
console.log(` Error: ${attempt.error.message}`);
|
|
531
|
+
console.log(` Raw Response: ${attempt.rawResponse}`);
|
|
532
|
+
console.log(` Conversation length: ${attempt.conversation.length}`);
|
|
533
|
+
|
|
534
|
+
attempt = attempt.cause as LlmRetryAttemptError | undefined;
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
if (error instanceof LlmFatalError) {
|
|
539
|
+
console.log('Fatal error (no retry):', error.message);
|
|
540
|
+
console.log('Original messages:', error.messages);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
```
|
|
544
|
+
|
|
545
|
+
## Extracting the Last Response
|
|
546
|
+
|
|
547
|
+
A common pattern is to extract the last LLM response from a failed operation:
|
|
548
|
+
|
|
549
|
+
```typescript
|
|
550
|
+
function getLastResponse(error: LlmRetryExhaustedError): string | null {
|
|
551
|
+
return error.cause?.rawResponse ?? null;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function getAllResponses(error: LlmRetryExhaustedError): string[] {
|
|
555
|
+
const responses: string[] = [];
|
|
556
|
+
let attempt = error.cause;
|
|
557
|
+
while (attempt) {
|
|
558
|
+
if (attempt.rawResponse) {
|
|
559
|
+
responses.unshift(attempt.rawResponse); // Add to front (chronological order)
|
|
560
|
+
}
|
|
561
|
+
attempt = attempt.cause as LlmRetryAttemptError | undefined;
|
|
562
|
+
}
|
|
563
|
+
return responses;
|
|
564
|
+
}
|
|
565
|
+
```
|
|
566
|
+
|
|
567
|
+
---
|
|
568
|
+
|
|
361
569
|
# Use Case 5: Architecture & Composition
|
|
362
570
|
|
|
363
571
|
How to build the client manually to enable **Fallback Chains** and **Smart Routing**.
|