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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/readme.md +208 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "llm-fns",
3
- "version": "1.0.21",
3
+ "version": "1.0.22",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
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**.