@jaypie/llm 1.3.10 → 1.3.11

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.
@@ -404,6 +404,36 @@ function determineModelProvider(input) {
404
404
  };
405
405
  }
406
406
 
407
+ /**
408
+ * Normalizes a `model` option that may be a single model name or a
409
+ * preference-ordered array of model names.
410
+ *
411
+ * A `string[]` is interpreted as a fallback chain: index `0` is the primary
412
+ * model, indices `1..` become fallback entries with an auto-detected provider
413
+ * (reusing `determineModelProvider`, exactly as the constructor's first
414
+ * argument does).
415
+ *
416
+ * Returns the primary model plus the derived fallback chain. A bare string
417
+ * yields an empty chain and is unchanged.
418
+ */
419
+ function resolveModelChain(model) {
420
+ if (!Array.isArray(model)) {
421
+ return { fallback: [], model };
422
+ }
423
+ if (model.length === 0) {
424
+ throw new errors.ConfigurationError("model array must contain at least one model name");
425
+ }
426
+ const [primary, ...rest] = model;
427
+ const fallback = rest.map((name) => {
428
+ const determined = determineModelProvider(name);
429
+ if (!determined.provider) {
430
+ throw new errors.ConfigurationError(`Unable to determine provider from model: ${name}`);
431
+ }
432
+ return { model: determined.model, provider: determined.provider };
433
+ });
434
+ return { fallback, model: primary };
435
+ }
436
+
407
437
  //
408
438
  //
409
439
  // Abstract Base Adapter
@@ -1489,17 +1519,23 @@ function tryParseNumber(input, options) {
1489
1519
  /**
1490
1520
  * Categories of errors for retry logic
1491
1521
  */
1492
- var ErrorCategory;
1522
+ exports.ErrorCategory = void 0;
1493
1523
  (function (ErrorCategory) {
1494
1524
  /** Error is transient and can be retried */
1495
1525
  ErrorCategory["Retryable"] = "retryable";
1496
- /** Error is due to rate limiting */
1526
+ /** Error is due to short-term rate limiting (retry after a delay) */
1497
1527
  ErrorCategory["RateLimit"] = "rate_limit";
1528
+ /**
1529
+ * Provider quota is exhausted or the account cannot be billed
1530
+ * (insufficient funds, daily quota, plan limit). Terminal and actionable —
1531
+ * retrying within the request budget will not help.
1532
+ */
1533
+ ErrorCategory["Quota"] = "quota";
1498
1534
  /** Error cannot be recovered from */
1499
1535
  ErrorCategory["Unrecoverable"] = "unrecoverable";
1500
1536
  /** Error type is unknown */
1501
1537
  ErrorCategory["Unknown"] = "unknown";
1502
- })(ErrorCategory || (ErrorCategory = {}));
1538
+ })(exports.ErrorCategory || (exports.ErrorCategory = {}));
1503
1539
 
1504
1540
  /**
1505
1541
  * Transient network error detection utility.
@@ -1585,6 +1621,99 @@ function isTransientNetworkError(error) {
1585
1621
  return false;
1586
1622
  }
1587
1623
 
1624
+ //
1625
+ //
1626
+ // Constants
1627
+ //
1628
+ /**
1629
+ * Transient structured-output compile failures. The provider caches the
1630
+ * compiled grammar after a successful compile, so an immediate retry of the
1631
+ * identical request typically succeeds (issue #422).
1632
+ */
1633
+ const RETRYABLE_MESSAGE_PATTERNS = [
1634
+ "grammar compilation timed out",
1635
+ "grammar compilation timeout",
1636
+ ];
1637
+ /** Billing / insufficient-funds signals — the account cannot be charged. */
1638
+ const BILLING_MESSAGE_PATTERNS = [
1639
+ "insufficient_quota",
1640
+ "insufficient funds",
1641
+ "insufficient credit",
1642
+ "credit balance",
1643
+ "billing",
1644
+ "payment required",
1645
+ "plan and billing",
1646
+ ];
1647
+ /** Exhausted usage quota (per-day / per-model limits). */
1648
+ const QUOTA_MESSAGE_PATTERNS = [
1649
+ "quota exceeded",
1650
+ "exceeded your current quota",
1651
+ "resource_exhausted",
1652
+ "quota_exceeded",
1653
+ ];
1654
+ //
1655
+ //
1656
+ // Helpers
1657
+ //
1658
+ function extractMessage(error) {
1659
+ if (error instanceof Error)
1660
+ return error.message.toLowerCase();
1661
+ if (typeof error === "string")
1662
+ return error.toLowerCase();
1663
+ if (error && typeof error === "object") {
1664
+ const message = error.message;
1665
+ if (typeof message === "string")
1666
+ return message.toLowerCase();
1667
+ }
1668
+ return "";
1669
+ }
1670
+ //
1671
+ //
1672
+ // Main
1673
+ //
1674
+ /**
1675
+ * Shared, provider-agnostic first pass over an error. Returns a
1676
+ * {@link ClassifiedError} when the message unambiguously identifies a
1677
+ * cross-provider condition (retryable structured-output timeout, exhausted
1678
+ * quota, or a billing failure), or `undefined` to defer to the adapter's own
1679
+ * status/name classification.
1680
+ *
1681
+ * Adapters call this before their existing logic so that, for example, a
1682
+ * `429` carrying a daily-quota message is classified as {@link
1683
+ * ErrorCategory.Quota} rather than {@link ErrorCategory.RateLimit}.
1684
+ */
1685
+ function classifyProviderError(error) {
1686
+ const message = extractMessage(error);
1687
+ if (!message)
1688
+ return undefined;
1689
+ for (const pattern of RETRYABLE_MESSAGE_PATTERNS) {
1690
+ if (message.includes(pattern)) {
1691
+ return { category: exports.ErrorCategory.Retryable, error, shouldRetry: true };
1692
+ }
1693
+ }
1694
+ for (const pattern of BILLING_MESSAGE_PATTERNS) {
1695
+ if (message.includes(pattern)) {
1696
+ return {
1697
+ category: exports.ErrorCategory.Quota,
1698
+ error,
1699
+ reason: "billing",
1700
+ shouldRetry: false,
1701
+ };
1702
+ }
1703
+ }
1704
+ for (const pattern of QUOTA_MESSAGE_PATTERNS) {
1705
+ if (message.includes(pattern)) {
1706
+ return {
1707
+ category: exports.ErrorCategory.Quota,
1708
+ error,
1709
+ reason: "quota",
1710
+ shouldRetry: false,
1711
+ };
1712
+ }
1713
+ }
1714
+ return undefined;
1715
+ }
1716
+
1588
1717
  //
1589
1718
  //
1590
1719
  // Constants
@@ -2358,12 +2487,17 @@ class AnthropicAdapter extends BaseProviderAdapter {
2358
2487
  // Error Classification
2359
2488
  //
2360
2489
  classifyError(error) {
2490
+ // Shared first pass: retryable structured-output timeouts (#422),
2491
+ // quota exhaustion, and billing failures classify the same across providers.
2492
+ const shared = classifyProviderError(error);
2493
+ if (shared)
2494
+ return shared;
2361
2495
  const errorName = error?.constructor?.name;
2362
2496
  // Check for rate limit error
2363
2497
  if (errorName === "RateLimitError") {
2364
2498
  return {
2365
2499
  error,
2366
- category: ErrorCategory.RateLimit,
2500
+ category: exports.ErrorCategory.RateLimit,
2367
2501
  shouldRetry: false,
2368
2502
  suggestedDelayMs: 60000,
2369
2503
  };
@@ -2372,7 +2506,7 @@ class AnthropicAdapter extends BaseProviderAdapter {
2372
2506
  if (RETRYABLE_ERROR_NAMES.includes(errorName)) {
2373
2507
  return {
2374
2508
  error,
2375
- category: ErrorCategory.Retryable,
2509
+ category: exports.ErrorCategory.Retryable,
2376
2510
  shouldRetry: true,
2377
2511
  };
2378
2512
  }
@@ -2380,7 +2514,7 @@ class AnthropicAdapter extends BaseProviderAdapter {
2380
2514
  if (NOT_RETRYABLE_ERROR_NAMES.includes(errorName)) {
2381
2515
  return {
2382
2516
  error,
2383
- category: ErrorCategory.Unrecoverable,
2517
+ category: exports.ErrorCategory.Unrecoverable,
2384
2518
  shouldRetry: false,
2385
2519
  };
2386
2520
  }
@@ -2388,14 +2522,14 @@ class AnthropicAdapter extends BaseProviderAdapter {
2388
2522
  if (isTransientNetworkError(error)) {
2389
2523
  return {
2390
2524
  error,
2391
- category: ErrorCategory.Retryable,
2525
+ category: exports.ErrorCategory.Retryable,
2392
2526
  shouldRetry: true,
2393
2527
  };
2394
2528
  }
2395
2529
  // Unknown error - treat as potentially retryable
2396
2530
  return {
2397
2531
  error,
2398
- category: ErrorCategory.Unknown,
2532
+ category: exports.ErrorCategory.Unknown,
2399
2533
  shouldRetry: true,
2400
2534
  };
2401
2535
  }
@@ -2966,6 +3100,11 @@ class BedrockAdapter extends BaseProviderAdapter {
2966
3100
  // Error Classification
2967
3101
  //
2968
3102
  classifyError(error) {
3103
+ // Shared first pass: retryable structured-output timeouts (#422),
3104
+ // quota exhaustion, and billing failures classify the same across providers.
3105
+ const shared = classifyProviderError(error);
3106
+ if (shared)
3107
+ return shared;
2969
3108
  const errorName = error?.constructor?.name;
2970
3109
  const errorMessage = error?.message ?? "";
2971
3110
  if (errorName === "ThrottlingException" ||
@@ -2973,7 +3112,7 @@ class BedrockAdapter extends BaseProviderAdapter {
2973
3112
  errorMessage.includes("Too Many Requests")) {
2974
3113
  return {
2975
3114
  error,
2976
- category: ErrorCategory.RateLimit,
3115
+ category: exports.ErrorCategory.RateLimit,
2977
3116
  shouldRetry: false,
2978
3117
  suggestedDelayMs: 60000,
2979
3118
  };
@@ -2984,7 +3123,7 @@ class BedrockAdapter extends BaseProviderAdapter {
2984
3123
  errorMessage.includes("InternalServerException")) {
2985
3124
  return {
2986
3125
  error,
2987
- category: ErrorCategory.Retryable,
3126
+ category: exports.ErrorCategory.Retryable,
2988
3127
  shouldRetry: true,
2989
3128
  };
2990
3129
  }
@@ -2995,20 +3134,20 @@ class BedrockAdapter extends BaseProviderAdapter {
2995
3134
  errorMessage.includes("ValidationException")) {
2996
3135
  return {
2997
3136
  error,
2998
- category: ErrorCategory.Unrecoverable,
3137
+ category: exports.ErrorCategory.Unrecoverable,
2999
3138
  shouldRetry: false,
3000
3139
  };
3001
3140
  }
3002
3141
  if (isTransientNetworkError(error)) {
3003
3142
  return {
3004
3143
  error,
3005
- category: ErrorCategory.Retryable,
3144
+ category: exports.ErrorCategory.Retryable,
3006
3145
  shouldRetry: true,
3007
3146
  };
3008
3147
  }
3009
3148
  return {
3010
3149
  error,
3011
- category: ErrorCategory.Unknown,
3150
+ category: exports.ErrorCategory.Unknown,
3012
3151
  shouldRetry: true,
3013
3152
  };
3014
3153
  }
@@ -3635,6 +3774,11 @@ class GoogleAdapter extends BaseProviderAdapter {
3635
3774
  // Error Classification
3636
3775
  //
3637
3776
  classifyError(error) {
3777
+ // Shared first pass: retryable structured-output timeouts (#422),
3778
+ // quota exhaustion, and billing failures classify the same across providers.
3779
+ const shared = classifyProviderError(error);
3780
+ if (shared)
3781
+ return shared;
3638
3782
  const geminiError = error;
3639
3783
  // Extract status code from error
3640
3784
  const statusCode = geminiError.status || geminiError.code;
@@ -3642,7 +3786,7 @@ class GoogleAdapter extends BaseProviderAdapter {
3642
3786
  if (statusCode === 429) {
3643
3787
  return {
3644
3788
  error,
3645
- category: ErrorCategory.RateLimit,
3789
+ category: exports.ErrorCategory.RateLimit,
3646
3790
  shouldRetry: false,
3647
3791
  suggestedDelayMs: 60000,
3648
3792
  };
@@ -3652,7 +3796,7 @@ class GoogleAdapter extends BaseProviderAdapter {
3652
3796
  RETRYABLE_STATUS_CODES$1.includes(statusCode)) {
3653
3797
  return {
3654
3798
  error,
3655
- category: ErrorCategory.Retryable,
3799
+ category: exports.ErrorCategory.Retryable,
3656
3800
  shouldRetry: true,
3657
3801
  };
3658
3802
  }
@@ -3661,7 +3805,7 @@ class GoogleAdapter extends BaseProviderAdapter {
3661
3805
  NOT_RETRYABLE_STATUS_CODES.includes(statusCode)) {
3662
3806
  return {
3663
3807
  error,
3664
- category: ErrorCategory.Unrecoverable,
3808
+ category: exports.ErrorCategory.Unrecoverable,
3665
3809
  shouldRetry: false,
3666
3810
  };
3667
3811
  }
@@ -3671,7 +3815,7 @@ class GoogleAdapter extends BaseProviderAdapter {
3671
3815
  errorMessage.includes("quota exceeded")) {
3672
3816
  return {
3673
3817
  error,
3674
- category: ErrorCategory.RateLimit,
3818
+ category: exports.ErrorCategory.RateLimit,
3675
3819
  shouldRetry: false,
3676
3820
  suggestedDelayMs: 60000,
3677
3821
  };
@@ -3681,7 +3825,7 @@ class GoogleAdapter extends BaseProviderAdapter {
3681
3825
  errorMessage.includes("ECONNREFUSED")) {
3682
3826
  return {
3683
3827
  error,
3684
- category: ErrorCategory.Retryable,
3828
+ category: exports.ErrorCategory.Retryable,
3685
3829
  shouldRetry: true,
3686
3830
  };
3687
3831
  }
@@ -3689,14 +3833,14 @@ class GoogleAdapter extends BaseProviderAdapter {
3689
3833
  if (isTransientNetworkError(error)) {
3690
3834
  return {
3691
3835
  error,
3692
- category: ErrorCategory.Retryable,
3836
+ category: exports.ErrorCategory.Retryable,
3693
3837
  shouldRetry: true,
3694
3838
  };
3695
3839
  }
3696
3840
  // Unknown error - treat as potentially retryable
3697
3841
  return {
3698
3842
  error,
3699
- category: ErrorCategory.Unknown,
3843
+ category: exports.ErrorCategory.Unknown,
3700
3844
  shouldRetry: true,
3701
3845
  };
3702
3846
  }
@@ -4583,11 +4727,16 @@ class OpenAiAdapter extends BaseProviderAdapter {
4583
4727
  // Error Classification
4584
4728
  //
4585
4729
  classifyError(error) {
4730
+ // Shared first pass: retryable structured-output timeouts (#422),
4731
+ // quota exhaustion, and billing failures classify the same across providers.
4732
+ const shared = classifyProviderError(error);
4733
+ if (shared)
4734
+ return shared;
4586
4735
  // Check for rate limit error
4587
4736
  if (error instanceof RateLimitError$1) {
4588
4737
  return {
4589
4738
  error,
4590
- category: ErrorCategory.RateLimit,
4739
+ category: exports.ErrorCategory.RateLimit,
4591
4740
  shouldRetry: false, // Rate limit requires waiting, not immediate retry
4592
4741
  suggestedDelayMs: 60000, // 1 minute default
4593
4742
  };
@@ -4597,7 +4746,7 @@ class OpenAiAdapter extends BaseProviderAdapter {
4597
4746
  if (error instanceof ErrorType) {
4598
4747
  return {
4599
4748
  error,
4600
- category: ErrorCategory.Retryable,
4749
+ category: exports.ErrorCategory.Retryable,
4601
4750
  shouldRetry: true,
4602
4751
  };
4603
4752
  }
@@ -4607,7 +4756,7 @@ class OpenAiAdapter extends BaseProviderAdapter {
4607
4756
  if (error instanceof ErrorType) {
4608
4757
  return {
4609
4758
  error,
4610
- category: ErrorCategory.Unrecoverable,
4759
+ category: exports.ErrorCategory.Unrecoverable,
4611
4760
  shouldRetry: false,
4612
4761
  };
4613
4762
  }
@@ -4616,14 +4765,14 @@ class OpenAiAdapter extends BaseProviderAdapter {
4616
4765
  if (isTransientNetworkError(error)) {
4617
4766
  return {
4618
4767
  error,
4619
- category: ErrorCategory.Retryable,
4768
+ category: exports.ErrorCategory.Retryable,
4620
4769
  shouldRetry: true,
4621
4770
  };
4622
4771
  }
4623
4772
  // Unknown error - treat as potentially retryable
4624
4773
  return {
4625
4774
  error,
4626
- category: ErrorCategory.Unknown,
4775
+ category: exports.ErrorCategory.Unknown,
4627
4776
  shouldRetry: true,
4628
4777
  };
4629
4778
  }
@@ -5351,6 +5500,11 @@ class OpenRouterAdapter extends BaseProviderAdapter {
5351
5500
  // Error Classification
5352
5501
  //
5353
5502
  classifyError(error) {
5503
+ // Shared first pass: retryable structured-output timeouts (#422),
5504
+ // quota exhaustion, and billing failures classify the same across providers.
5505
+ const shared = classifyProviderError(error);
5506
+ if (shared)
5507
+ return shared;
5354
5508
  // Check if error has a status code (HTTP error)
5355
5509
  const errorWithStatus = error;
5356
5510
  const statusCode = errorWithStatus.status || errorWithStatus.statusCode;
@@ -5359,7 +5513,7 @@ class OpenRouterAdapter extends BaseProviderAdapter {
5359
5513
  if (statusCode === RATE_LIMIT_STATUS_CODE) {
5360
5514
  return {
5361
5515
  error,
5362
- category: ErrorCategory.RateLimit,
5516
+ category: exports.ErrorCategory.RateLimit,
5363
5517
  shouldRetry: false,
5364
5518
  suggestedDelayMs: 60000,
5365
5519
  };
@@ -5368,7 +5522,7 @@ class OpenRouterAdapter extends BaseProviderAdapter {
5368
5522
  if (RETRYABLE_STATUS_CODES.includes(statusCode)) {
5369
5523
  return {
5370
5524
  error,
5371
- category: ErrorCategory.Retryable,
5525
+ category: exports.ErrorCategory.Retryable,
5372
5526
  shouldRetry: true,
5373
5527
  };
5374
5528
  }
@@ -5376,7 +5530,7 @@ class OpenRouterAdapter extends BaseProviderAdapter {
5376
5530
  if (statusCode >= 400 && statusCode < 500) {
5377
5531
  return {
5378
5532
  error,
5379
- category: ErrorCategory.Unrecoverable,
5533
+ category: exports.ErrorCategory.Unrecoverable,
5380
5534
  shouldRetry: false,
5381
5535
  };
5382
5536
  }
@@ -5387,7 +5541,7 @@ class OpenRouterAdapter extends BaseProviderAdapter {
5387
5541
  errorMessage.includes("too many requests")) {
5388
5542
  return {
5389
5543
  error,
5390
- category: ErrorCategory.RateLimit,
5544
+ category: exports.ErrorCategory.RateLimit,
5391
5545
  shouldRetry: false,
5392
5546
  suggestedDelayMs: 60000,
5393
5547
  };
@@ -5396,14 +5550,14 @@ class OpenRouterAdapter extends BaseProviderAdapter {
5396
5550
  if (isTransientNetworkError(error)) {
5397
5551
  return {
5398
5552
  error,
5399
- category: ErrorCategory.Retryable,
5553
+ category: exports.ErrorCategory.Retryable,
5400
5554
  shouldRetry: true,
5401
5555
  };
5402
5556
  }
5403
5557
  // Unknown error - treat as potentially retryable
5404
5558
  return {
5405
5559
  error,
5406
- category: ErrorCategory.Unknown,
5560
+ category: exports.ErrorCategory.Unknown,
5407
5561
  shouldRetry: true,
5408
5562
  };
5409
5563
  }
@@ -5627,7 +5781,7 @@ class XaiAdapter extends OpenAiAdapter {
5627
5781
  if (isTransientIngestError(error)) {
5628
5782
  return {
5629
5783
  error,
5630
- category: ErrorCategory.Retryable,
5784
+ category: exports.ErrorCategory.Retryable,
5631
5785
  shouldRetry: true,
5632
5786
  };
5633
5787
  }
@@ -6715,6 +6869,122 @@ function createStaleRejectionGuard() {
6715
6869
  };
6716
6870
  }
6717
6871
 
6872
+ //
6873
+ //
6874
+ // Base
6875
+ //
6876
+ /**
6877
+ * Normalized, provider-agnostic LLM error. Thrown by the operate/stream retry
6878
+ * layer when a request cannot be completed, so consumers can `catch` a stable
6879
+ * type regardless of which provider failed. Extends {@link JaypieError} so
6880
+ * `isJaypieError()` and `.status` continue to work; the original provider error
6881
+ * is preserved on `.cause`.
6882
+ */
6883
+ class LlmError extends errors.JaypieError {
6884
+ constructor(message, category, { status = 502, title = "Bad Gateway", provider, model, retryAfterMs, cause, } = {}) {
6885
+ super(message, { status, title }, { _type: "LlmError" });
6886
+ this.name = "LlmError";
6887
+ this.category = category;
6888
+ this.provider = provider;
6889
+ this.model = model;
6890
+ this.retryAfterMs = retryAfterMs;
6891
+ this.cause = cause;
6892
+ }
6893
+ }
6894
+ //
6895
+ //
6896
+ // Subclasses
6897
+ //
6898
+ /**
6899
+ * Short-term rate limiting (per-minute 429). Terminal within the request
6900
+ * budget; `retryAfterMs` carries the provider's suggested wait when available.
6901
+ */
6902
+ class LlmRateLimitError extends LlmError {
6903
+ constructor(message, options = {}) {
6904
+ super(message, exports.ErrorCategory.RateLimit, {
6905
+ status: 429,
6906
+ title: "Too Many Requests",
6907
+ ...options,
6908
+ });
6909
+ this.name = "LlmRateLimitError";
6910
+ }
6911
+ }
6912
+ /**
6913
+ * Provider quota is exhausted or the account cannot be billed. Terminal and
6914
+ * actionable. `reason` distinguishes an exhausted usage quota from insufficient
6915
+ * funds.
6916
+ */
6917
+ class LlmQuotaError extends LlmError {
6918
+ constructor(message, { reason = "quota", ...options } = {}) {
6919
+ super(message, exports.ErrorCategory.Quota, {
6920
+ status: 402,
6921
+ title: "Payment Required",
6922
+ ...options,
6923
+ });
6924
+ this.name = "LlmQuotaError";
6925
+ this.reason = reason;
6926
+ }
6927
+ }
6928
+ /**
6929
+ * The request cannot be recovered from (bad request, authentication,
6930
+ * permission, not found). Terminal.
6931
+ */
6932
+ class LlmUnrecoverableError extends LlmError {
6933
+ constructor(message, options = {}) {
6934
+ super(message, exports.ErrorCategory.Unrecoverable, {
6935
+ status: 502,
6936
+ title: "Bad Gateway",
6937
+ ...options,
6938
+ });
6939
+ this.name = "LlmUnrecoverableError";
6940
+ }
6941
+ }
6942
+ /**
6943
+ * A transient or unknown error survived the retry budget. Terminal only because
6944
+ * retries were exhausted; the underlying condition may succeed later.
6945
+ */
6946
+ class LlmTransientError extends LlmError {
6947
+ constructor(message, options = {}) {
6948
+ super(message, exports.ErrorCategory.Retryable, {
6949
+ status: 504,
6950
+ title: "Gateway Timeout",
6951
+ ...options,
6952
+ });
6953
+ this.name = "LlmTransientError";
6954
+ }
6955
+ }
6956
+
6957
+ /**
6958
+ * Map a {@link ClassifiedError} to the matching {@link LlmError} subclass,
6959
+ * preserving the original error as `cause`. Used by the retry layer to throw a
6960
+ * stable, catchable type instead of a bare gateway error.
6961
+ */
6962
+ function toLlmError(classified, context = {}) {
6963
+ const original = classified.error;
6964
+ const message = original instanceof Error ? original.message : String(original);
6965
+ const options = {
6966
+ cause: original,
6967
+ model: context.model,
6968
+ provider: context.provider,
6969
+ retryAfterMs: classified.suggestedDelayMs,
6970
+ };
6971
+ switch (classified.category) {
6972
+ case exports.ErrorCategory.RateLimit:
6973
+ return new LlmRateLimitError(message, options);
6974
+ case exports.ErrorCategory.Quota:
6975
+ return new LlmQuotaError(message, {
6976
+ ...options,
6977
+ reason: classified.reason ?? "quota",
6978
+ });
6979
+ case exports.ErrorCategory.Unrecoverable:
6980
+ return new LlmUnrecoverableError(message, options);
6981
+ case exports.ErrorCategory.Retryable:
6982
+ case exports.ErrorCategory.Unknown:
6983
+ default:
6984
+ return new LlmTransientError(message, options);
6985
+ }
6986
+ }
6987
+
6718
6988
  //
6719
6989
  //
6720
6990
  // Main
@@ -6729,6 +6999,18 @@ class RetryExecutor {
6729
6999
  this.hookRunner = config.hookRunner ?? hookRunner;
6730
7000
  this.errorClassifier = config.errorClassifier;
6731
7001
  }
7002
+ /**
7003
+ * Build the typed, provider-agnostic error thrown when a request cannot be
7004
+ * completed — classified (rate limit / quota / unrecoverable / transient)
7005
+ * and carrying the provider, model, and original error as `cause`.
7006
+ */
7007
+ toTerminalError(error, context) {
7008
+ const classified = this.errorClassifier.classify(error);
7009
+ return toLlmError(classified, {
7010
+ model: context.model,
7011
+ provider: context.provider,
7012
+ });
7013
+ }
6732
7014
  /**
6733
7015
  * Execute an operation with retry logic.
6734
7016
  * Each attempt receives an AbortSignal. On failure, the signal is aborted
@@ -6772,8 +7054,7 @@ class RetryExecutor {
6772
7054
  providerRequest: options.context.providerRequest,
6773
7055
  error,
6774
7056
  });
6775
- const errorMessage = error instanceof Error ? error.message : String(error);
6776
- throw new errors.BadGatewayError(errorMessage);
7057
+ throw this.toTerminalError(error, options.context);
6777
7058
  }
6778
7059
  // Check if error is not retryable
6779
7060
  if (!this.errorClassifier.isRetryable(error)) {
@@ -6785,8 +7066,7 @@ class RetryExecutor {
6785
7066
  providerRequest: options.context.providerRequest,
6786
7067
  error,
6787
7068
  });
6788
- const errorMessage = error instanceof Error ? error.message : String(error);
6789
- throw new errors.BadGatewayError(errorMessage);
7069
+ throw this.toTerminalError(error, options.context);
6790
7070
  }
6791
7071
  // Warn if this is an unknown error type
6792
7072
  if (!this.errorClassifier.isKnownError(error)) {
@@ -6829,6 +7109,7 @@ const MAX_CONSECUTIVE_TOOL_ERRORS = 6;
6829
7109
  */
6830
7110
  function createErrorClassifier(adapter) {
6831
7111
  return {
7112
+ classify: (error) => adapter.classifyError(error),
6832
7113
  isRetryable: (error) => adapter.isRetryableError(error),
6833
7114
  isKnownError: (error) => {
6834
7115
  const classified = adapter.classifyError(error);
@@ -7082,7 +7363,9 @@ class OperateLoop {
7082
7363
  const response = await retryExecutor.execute((signal) => this.adapter.executeRequest(this.client, providerRequest, signal), {
7083
7364
  context: {
7084
7365
  input: state.currentInput,
7366
+ model: options.model ?? this.adapter.defaultModel,
7085
7367
  options,
7368
+ provider: this.adapter.name,
7086
7369
  providerRequest,
7087
7370
  },
7088
7371
  hooks: hooksWithProgress,
@@ -7705,9 +7988,11 @@ class StreamLoop {
7705
7988
  !this.adapter.isRetryableError(error)) {
7706
7989
  log.error(`Stream request failed after ${this.retryPolicy.maxRetries} retries`);
7707
7990
  log.var({ error });
7708
- const errorMessage = error instanceof Error ? error.message : String(error);
7709
7991
  llmSpan?.finish();
7710
- throw new errors.BadGatewayError(errorMessage);
7992
+ throw toLlmError(this.adapter.classifyError(error), {
7993
+ model: options.model ?? this.adapter.defaultModel,
7994
+ provider: this.adapter.name,
7995
+ });
7711
7996
  }
7712
7997
  const delay = this.retryPolicy.getDelayForAttempt(attempt);
7713
7998
  log.warn(`Stream request failed. Retrying in ${delay}ms...`);
@@ -9178,7 +9463,14 @@ class XaiProvider {
9178
9463
 
9179
9464
  class Llm {
9180
9465
  constructor(providerName = DEFAULT.PROVIDER.NAME, options = {}) {
9181
- const { fallback, model } = options;
9466
+ const { fallback: fallbackOption, model: modelOption } = options;
9467
+ // A `model` array is sugar for a preference-ordered fallback chain:
9468
+ // index 0 is primary, the rest become fallback entries (provider
9469
+ // auto-detected). Any explicit `fallback` is appended after the chain.
9470
+ const { fallback: modelFallback, model } = resolveModelChain(modelOption);
9471
+ const fallback = modelFallback.length || fallbackOption
9472
+ ? [...modelFallback, ...(fallbackOption ?? [])]
9473
+ : undefined;
9182
9474
  let finalProvider = providerName;
9183
9475
  let finalModel = model;
9184
9476
  // Legacy: accept "gemini" but warn
@@ -9250,6 +9542,11 @@ class Llm {
9250
9542
  }
9251
9543
  }
9252
9544
  async send(message, options) {
9545
+ if (options && Array.isArray(options.model)) {
9546
+ // `send` has no fallback machinery; use the primary model only
9547
+ const { model } = resolveModelChain(options.model);
9548
+ return this._llm.send(message, { ...options, model });
9549
+ }
9253
9550
  return this._llm.send(message, options);
9254
9551
  }
9255
9552
  /**
@@ -9282,8 +9579,21 @@ class Llm {
9282
9579
  if (!this._llm.operate) {
9283
9580
  throw new errors.NotImplementedError(`Provider ${this._provider} does not support operate method`);
9284
9581
  }
9285
- const fallbackChain = this.resolveFallbackChain(options);
9286
- const optionsWithoutFallback = { ...options, fallback: false };
9582
+ // A per-call `model` array becomes primary + a derived fallback chain
9583
+ // prepended to any resolved chain.
9584
+ const { fallback: modelFallback, model: perCallModel } = resolveModelChain(options.model);
9585
+ const resolvedOptions = {
9586
+ ...options,
9587
+ model: perCallModel,
9588
+ };
9589
+ const fallbackChain = [
9590
+ ...modelFallback,
9591
+ ...this.resolveFallbackChain(resolvedOptions),
9592
+ ];
9593
+ const optionsWithoutFallback = {
9594
+ ...resolvedOptions,
9595
+ fallback: false,
9596
+ };
9287
9597
  let lastError;
9288
9598
  let attempts = 0;
9289
9599
  // Try primary provider first
@@ -9332,33 +9642,45 @@ class Llm {
9332
9642
  if (!this._llm.stream) {
9333
9643
  throw new errors.NotImplementedError(`Provider ${this._provider} does not support stream method`);
9334
9644
  }
9335
- yield* this._llm.stream(input, options);
9645
+ // `stream` has no instance-level fallback machinery; an array model uses
9646
+ // the primary and ignores the rest.
9647
+ const { model } = resolveModelChain(options.model);
9648
+ const streamOptions = { ...options, model };
9649
+ yield* this._llm.stream(input, streamOptions);
9336
9650
  }
9337
9651
  static async send(message, options) {
9338
9652
  const { llm, apiKey, model, ...messageOptions } = options || {};
9339
- const instance = new Llm(llm, { apiKey, model });
9653
+ // A `model` array derives a fallback chain; `send` has no fallback
9654
+ // machinery, so the primary model is used and the chain is ignored.
9655
+ const { model: primaryModel } = resolveModelChain(model);
9656
+ const instance = new Llm(llm, { apiKey, model: primaryModel });
9340
9657
  return instance.send(message, messageOptions);
9341
9658
  }
9342
9659
  static async operate(input, options) {
9343
9660
  const { apiKey, fallback, llm, model, ...operateOptions } = options || {};
9661
+ // A `model` array becomes primary + derived fallback chain
9662
+ const { fallback: modelFallback, model: primaryModel } = resolveModelChain(model);
9344
9663
  let finalLlm = llm;
9345
- let finalModel = model;
9346
- if (!llm && model) {
9347
- const determined = determineModelProvider(model);
9664
+ let finalModel = primaryModel;
9665
+ if (!llm && primaryModel) {
9666
+ const determined = determineModelProvider(primaryModel);
9348
9667
  if (determined.provider) {
9349
9668
  finalLlm = determined.provider;
9350
9669
  }
9351
9670
  }
9352
- else if (llm && model) {
9671
+ else if (llm && primaryModel) {
9353
9672
  // When both llm and model are provided, check if they conflict
9354
- const determined = determineModelProvider(model);
9673
+ const determined = determineModelProvider(primaryModel);
9355
9674
  if (determined.provider && determined.provider !== llm) {
9356
9675
  // Don't pass the conflicting model to the constructor
9357
9676
  finalModel = undefined;
9358
9677
  }
9359
9678
  }
9360
9679
  // Resolve fallback for static method: pass to instance if array, pass to operate options if false
9361
- const instanceFallback = Array.isArray(fallback) ? fallback : undefined;
9680
+ const explicitFallback = Array.isArray(fallback) ? fallback : [];
9681
+ const instanceFallback = modelFallback.length || explicitFallback.length
9682
+ ? [...modelFallback, ...explicitFallback]
9683
+ : undefined;
9362
9684
  const operateFallback = fallback === false ? false : undefined;
9363
9685
  const instance = new Llm(finalLlm, {
9364
9686
  apiKey,
@@ -9372,23 +9694,29 @@ class Llm {
9372
9694
  }
9373
9695
  static stream(input, options) {
9374
9696
  const { llm, apiKey, model, ...streamOptions } = options || {};
9697
+ // A `model` array becomes primary + derived fallback chain
9698
+ const { fallback: modelFallback, model: primaryModel } = resolveModelChain(model);
9375
9699
  let finalLlm = llm;
9376
- let finalModel = model;
9377
- if (!llm && model) {
9378
- const determined = determineModelProvider(model);
9700
+ let finalModel = primaryModel;
9701
+ if (!llm && primaryModel) {
9702
+ const determined = determineModelProvider(primaryModel);
9379
9703
  if (determined.provider) {
9380
9704
  finalLlm = determined.provider;
9381
9705
  }
9382
9706
  }
9383
- else if (llm && model) {
9707
+ else if (llm && primaryModel) {
9384
9708
  // When both llm and model are provided, check if they conflict
9385
- const determined = determineModelProvider(model);
9709
+ const determined = determineModelProvider(primaryModel);
9386
9710
  if (determined.provider && determined.provider !== llm) {
9387
9711
  // Don't pass the conflicting model to the constructor
9388
9712
  finalModel = undefined;
9389
9713
  }
9390
9714
  }
9391
- const instance = new Llm(finalLlm, { apiKey, model: finalModel });
9715
+ const instance = new Llm(finalLlm, {
9716
+ apiKey,
9717
+ fallback: modelFallback.length ? modelFallback : undefined,
9718
+ model: finalModel,
9719
+ });
9392
9720
  return instance.stream(input, streamOptions);
9393
9721
  }
9394
9722
  }
@@ -9692,6 +10020,11 @@ exports.GoogleProvider = GoogleProvider;
9692
10020
  exports.JaypieToolkit = JaypieToolkit;
9693
10021
  exports.LLM = constants;
9694
10022
  exports.Llm = Llm;
10023
+ exports.LlmError = LlmError;
10024
+ exports.LlmQuotaError = LlmQuotaError;
10025
+ exports.LlmRateLimitError = LlmRateLimitError;
10026
+ exports.LlmTransientError = LlmTransientError;
10027
+ exports.LlmUnrecoverableError = LlmUnrecoverableError;
9695
10028
  exports.OpenRouterProvider = OpenRouterProvider;
9696
10029
  exports.Toolkit = Toolkit;
9697
10030
  exports.XaiProvider = XaiProvider;