vern-llm 2.2.0 → 2.3.0

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/index.js CHANGED
@@ -1,23 +1,205 @@
1
1
  import { randomUUID } from "crypto";
2
2
 
3
3
  //#region src/types/errors.ts
4
+ /**
5
+ * Tool contract codes: a model or provider response defect, not a
6
+ * transient provider fault. Deterministic on the wire request, so
7
+ * retrying can't change the outcome and it shouldn't count toward the
8
+ * circuit breaker either. Shared by `LLMError.retryable` below and by
9
+ * `CallExecutor`'s own retry/breaker accounting, so the two can't drift
10
+ * apart.
11
+ */
12
+ const NON_RETRYABLE_TOOL_CONTRACT_CODES = new Set([
13
+ "unknown_tool",
14
+ "duplicate_tool_call_id",
15
+ "tool_choice_none_violated",
16
+ "unexpected_tool_calls"
17
+ ]);
18
+ /**
19
+ * Local rate-limit codes: the call never reached the provider, so it says
20
+ * nothing about the provider's health, and retrying either just requeues
21
+ * behind the same limit (the two queue codes) or can never succeed at all
22
+ * (`rate_limit_capacity_exceeded`). Shared for the same reason as
23
+ * {@link NON_RETRYABLE_TOOL_CONTRACT_CODES}.
24
+ */
25
+ const LOCAL_RATE_LIMIT_CODES = new Set([
26
+ "rate_limit_queue_full",
27
+ "rate_limit_queue_timeout",
28
+ "rate_limit_capacity_exceeded"
29
+ ]);
30
+ /**
31
+ * Types that are never worth retrying on their own: deterministic
32
+ * caller-input, model-response, or cancellation failures rather than a
33
+ * transient provider fault.
34
+ */
35
+ const NON_RETRYABLE_TYPES = new Set([
36
+ "parse",
37
+ "validation",
38
+ "invalid_params",
39
+ "aborted"
40
+ ]);
41
+ /**
42
+ * Shared retryability rule behind both `LLMError.retryable` and
43
+ * `LLMErrorSnapshot.retryable`. Pulled out so the two can't drift apart:
44
+ * a snapshot is a point-in-time copy of an error's fields, and this is
45
+ * one of them, so it has to be computed the same way in both places.
46
+ */
47
+ function computeRetryable(type, code) {
48
+ if (NON_RETRYABLE_TYPES.has(type)) return false;
49
+ if (code && NON_RETRYABLE_TOOL_CONTRACT_CODES.has(code)) return false;
50
+ if (code && LOCAL_RATE_LIMIT_CODES.has(code)) return false;
51
+ return true;
52
+ }
53
+ /**
54
+ * Returns `issues` unchanged when it can survive `JSON.stringify`.
55
+ * Most `issues` values are VernLLM's own structured shapes (see
56
+ * `LLMErrorIssuesByCode`) and always safe. The one exception is a
57
+ * schema validation failure, where `issues` is a caller supplied
58
+ * `SchemaLike` validator's own `error: unknown`, not controlled by
59
+ * VernLLM and not guaranteed to be circular free. Rather than silently
60
+ * dropping it in that case, this returns a marker string so a reader
61
+ * of serialized output can tell "no issues data" apart from "issues
62
+ * existed but could not be shown".
63
+ */
64
+ function safeIssues(issues) {
65
+ if (issues === void 0) return void 0;
66
+ try {
67
+ JSON.stringify(issues);
68
+ return issues;
69
+ } catch {
70
+ return "[Unserializable: issues contained a circular reference]";
71
+ }
72
+ }
73
+ /**
74
+ * Depth cap for `safeAttempts`, guarding against a pathological,
75
+ * self referential `attempts` array. `attempts` is a public
76
+ * `LLMErrorOptions` field, so a caller can construct one by hand; this
77
+ * keeps that path bounded the same way a circular `issues` value is
78
+ * bounded, rather than assuming well formed input.
79
+ */
80
+ const MAX_ATTEMPTS_DEPTH = 20;
81
+ /**
82
+ * Returns a copy of `attempts` with every nested snapshot's `issues`
83
+ * re-checked through `safeIssues`, recursively through each snapshot's
84
+ * own `attempts`. Needed for two reasons: `safeIssues` returns a safe
85
+ * `issues` value by reference, so a shared object can be mutated into a
86
+ * circular one after the snapshot was created, and `attempts` is a
87
+ * public constructor option, so a caller can hand build a `RetryAttempt`
88
+ * (or a whole `LLMErrorSnapshot`) with a circular `issues` and pass it
89
+ * in directly, never touching `toSnapshot()` at all. Extra fields on an
90
+ * attempt (e.g. `FallbackAttempt`'s `provider`/`model`) are preserved.
91
+ */
92
+ function safeAttempts(attempts, depth = 0) {
93
+ if (attempts === void 0) return void 0;
94
+ if (depth >= MAX_ATTEMPTS_DEPTH) return [];
95
+ return attempts.map((attempt) => ({
96
+ ...attempt,
97
+ error: {
98
+ ...attempt.error,
99
+ issues: safeIssues(attempt.error.issues),
100
+ attempts: safeAttempts(attempt.error.attempts, depth + 1)
101
+ }
102
+ }));
103
+ }
4
104
  var LLMError = class extends Error {
5
- constructor(message, type, status, issues, cause, retryAfterMs, code) {
105
+ status;
106
+ issues;
107
+ cause;
108
+ retryAfterMs;
109
+ /** Stable discriminator within `type`. Absent on errors predating it. */
110
+ code;
111
+ /** Every attempt made before this error was thrown, in order. Absent when nothing was retried. */
112
+ attempts;
113
+ constructor(message, type, options = {}) {
6
114
  super(message);
7
115
  this.type = type;
8
- this.status = status;
9
- this.issues = issues;
10
- this.cause = cause;
11
- this.retryAfterMs = retryAfterMs;
12
- this.code = code;
13
116
  this.name = "LLMError";
117
+ this.status = options.status;
118
+ this.issues = options.issues;
119
+ this.cause = options.cause;
120
+ this.retryAfterMs = options.retryAfterMs;
121
+ this.code = options.code;
122
+ this.attempts = options.attempts;
123
+ }
124
+ /**
125
+ * Computed purely from `type`/`code`, independent of any specific call's
126
+ * `nonRetryableStatus` list. False for `parse`/`validation`/
127
+ * `invalid_params`/`aborted` types (the caller's own input, the model's
128
+ * own response, or intentional cancellation, none of which are the
129
+ * provider being unhealthy), the tool contract codes, and the local
130
+ * rate limit codes. Subclasses (see `FallbackExhaustedError`) may
131
+ * override this when `type` alone carries no retry signal.
132
+ */
133
+ get retryable() {
134
+ return computeRetryable(this.type, this.code);
135
+ }
136
+ /**
137
+ * Copies this error's fields into an {@link LLMErrorSnapshot}, for
138
+ * recording as a `RetryAttempt`/`FallbackAttempt`. `retryable` is
139
+ * captured here since a snapshot has no getter of its own. `cause` is
140
+ * not copied, see `LLMErrorSnapshot`'s own doc. `issues` and every
141
+ * nested `attempts` entry's own `issues` go through `safeAttempts`,
142
+ * since a schema validation failure's `issues` is a caller supplied
143
+ * value, not controlled by VernLLM, and `attempts` is itself a public
144
+ * constructor option a caller can hand build.
145
+ */
146
+ toSnapshot() {
147
+ return {
148
+ message: this.message,
149
+ type: this.type,
150
+ status: this.status,
151
+ issues: safeIssues(this.issues),
152
+ retryAfterMs: this.retryAfterMs,
153
+ code: this.code,
154
+ retryable: this.retryable,
155
+ attempts: safeAttempts(this.attempts)
156
+ };
157
+ }
158
+ /**
159
+ * Controls what `JSON.stringify(err)` produces. Omits `cause` for the
160
+ * same reason `toSnapshot()` does: `cause` is `unknown` and never
161
+ * validated by VernLLM, and some SDK errors carry circular structures
162
+ * `JSON.stringify` cannot serialize at all. Read `err.cause` directly
163
+ * instead. `issues`, including every nested `attempts` entry's own
164
+ * `issues`, goes through `safeAttempts` for the same reason: a schema
165
+ * validation failure's `issues` is caller supplied and not guaranteed
166
+ * circular free. Also includes `message` and `retryable`, which a
167
+ * plain property walk would otherwise miss: `message` is
168
+ * non-enumerable on `Error`, and `retryable` is a getter, not an own
169
+ * property.
170
+ */
171
+ toJSON() {
172
+ return {
173
+ name: this.name,
174
+ message: this.message,
175
+ type: this.type,
176
+ status: this.status,
177
+ issues: safeIssues(this.issues),
178
+ retryAfterMs: this.retryAfterMs,
179
+ code: this.code,
180
+ retryable: this.retryable,
181
+ attempts: safeAttempts(this.attempts)
182
+ };
14
183
  }
15
- /** Every tool contract failure in one response, when there is more than one. */
16
- toolIssues;
17
184
  };
18
185
  function isLLMError(err) {
19
186
  return err instanceof LLMError;
20
187
  }
188
+ /**
189
+ * Narrows `err.issues` to the exact shape {@link LLMErrorIssuesByCode} maps
190
+ * `code` to, for any code listed there. `code` stays the only discriminator
191
+ * VernLLM uses; this just gives that existing check a typed return instead
192
+ * of requiring a manual cast of `issues`:
193
+ *
194
+ * ```ts
195
+ * if (isLLMError(err) && hasIssues(err, 'duplicate_tool_names')) {
196
+ * console.log(err.issues.names); // string[], no cast needed
197
+ * }
198
+ * ```
199
+ */
200
+ function hasIssues(err, code) {
201
+ return err.code === code && err.issues !== void 0;
202
+ }
21
203
 
22
204
  //#endregion
23
205
  //#region src/types/cache.ts
@@ -147,7 +329,12 @@ function isToolCallResult(result) {
147
329
  //#endregion
148
330
  //#region src/types/fallback.ts
149
331
  /** Tool contract failures are the model ignoring the request, not a sick provider: repeating it elsewhere can't help. */
150
- const TOOL_CONTRACT_CODES = new Set(["unknown_tool", "duplicate_tool_call_id"]);
332
+ const TOOL_CONTRACT_CODES = new Set([
333
+ "unknown_tool",
334
+ "duplicate_tool_call_id",
335
+ "tool_choice_none_violated",
336
+ "unexpected_tool_calls"
337
+ ]);
151
338
  /**
152
339
  * The default `fallbackOn` policy. Exported so a caller can wrap rather
153
340
  * than replace it, e.g. `fallbackOn: (e, ctx) => myCheck(e) ? 'stop' : defaultFallbackOn(e, ctx)`.
@@ -170,10 +357,30 @@ const defaultFallbackOn = (error) => {
170
357
  var FallbackExhaustedError = class extends LLMError {
171
358
  constructor(attempts) {
172
359
  const last = attempts[attempts.length - 1]?.error;
173
- super(`${attempts.length} provider${attempts.length === 1 ? "" : "s"} attempted and failed: ${attempts.map((a) => `${a.provider}(${a.error.type})`).join(" then ")}`, last?.type ?? "unknown", last?.status, void 0, last, last?.retryAfterMs, "fallback_exhausted");
360
+ super(`${attempts.length} provider${attempts.length === 1 ? "" : "s"} attempted and failed: ${attempts.map((a) => `${a.provider}(${a.error.type})`).join(" then ")}`, "fallback_exhausted", {
361
+ status: last?.status,
362
+ cause: last,
363
+ retryAfterMs: last?.retryAfterMs,
364
+ code: "fallback_exhausted",
365
+ attempts
366
+ });
174
367
  this.attempts = attempts;
175
368
  }
369
+ /**
370
+ * `type: 'fallback_exhausted'` by itself says nothing about whether
371
+ * retrying could help; the reason the last target failed does. Defers to
372
+ * that attempt's own `retryable` instead of anything about this class's
373
+ * own type.
374
+ */
375
+ get retryable() {
376
+ const last = this.attempts[this.attempts.length - 1]?.error;
377
+ return last ? last.retryable : super.retryable;
378
+ }
176
379
  };
380
+ /** Narrows `err` to {@link FallbackExhaustedError}, for direct access to its `attempts` (`provider`/`model` per failed target) without a manual `instanceof` check. */
381
+ function isFallbackExhaustedError(err) {
382
+ return err instanceof FallbackExhaustedError;
383
+ }
177
384
 
178
385
  //#endregion
179
386
  //#region src/internal/execution/usage.utils.ts
@@ -195,7 +402,7 @@ async function reserve(params, coalesced, signal) {
195
402
  return true;
196
403
  } catch (error) {
197
404
  if (signal?.aborted) throw new LLMError("LLM request aborted", "aborted");
198
- throw new LLMError(error instanceof Error ? error.message : "Usage reservation failed", "quota_exceeded", void 0, void 0, error);
405
+ throw new LLMError(error instanceof Error ? error.message : "Usage reservation failed", "quota_exceeded", { cause: error });
199
406
  }
200
407
  }
201
408
  /**
@@ -395,13 +602,30 @@ var CacheOrchestrator = class {
395
602
  */
396
603
  async deleteCache(key) {
397
604
  if (!this.cache.delete) return;
398
- await this.cache.delete(await this.resolveCacheKey(key));
605
+ try {
606
+ await this.cache.delete(await this.resolveCacheKey(key));
607
+ } catch (error) {
608
+ this.logger.warn(`[VernLLM] cache delete failed: ${error instanceof Error ? error.message : "unknown"}`);
609
+ }
399
610
  }
400
611
  /** Logs a failed refundUsage attempt via the configured logger. */
401
612
  logRefundError(logMessage, error) {
402
613
  this.logger.error(logMessage, { message: error instanceof Error ? error.message : "unknown" });
403
614
  }
404
615
  /**
616
+ * Reads from the cache, treating a failed adapter read as a miss rather
617
+ * than letting it fail the call. The request still falls through to a
618
+ * real provider call, but that fallback is now logged instead of silent.
619
+ */
620
+ async getCached(key) {
621
+ try {
622
+ return await this.cache.get(key);
623
+ } catch (error) {
624
+ this.logger.warn(`[VernLLM] cache read failed: ${error instanceof Error ? error.message : "unknown"}`);
625
+ return { hit: false };
626
+ }
627
+ }
628
+ /**
405
629
  * Internal cache primitive around caller-supplied logic. Concurrent misses
406
630
  * for the same `cacheKey` share a single in-flight call, avoiding cache
407
631
  * stampedes.
@@ -421,7 +645,7 @@ var CacheOrchestrator = class {
421
645
  ...params,
422
646
  cacheKey: resolvedKey
423
647
  };
424
- const cached = await this.cache.get(resolvedKey);
648
+ const cached = await this.getCached(resolvedKey);
425
649
  if (cached.hit) return cached.value;
426
650
  const existing = this.inFlight.get(resolvedKey);
427
651
  if (existing) return withReservedUsage(resolvedParams, true, () => existing, params.signal, (logMessage, error) => this.logRefundError(logMessage, error));
@@ -442,7 +666,7 @@ var CacheOrchestrator = class {
442
666
  try {
443
667
  await this.cache.set(params.cacheKey, result, params.ttl);
444
668
  } catch (error) {
445
- this.logger.error("[VernLLM] cache write failed", { message: error instanceof Error ? error.message : "unknown" });
669
+ this.logger.warn(`[VernLLM] cache write failed: ${error instanceof Error ? error.message : "unknown"}`);
446
670
  }
447
671
  return result;
448
672
  }
@@ -471,7 +695,7 @@ var CacheOrchestrator = class {
471
695
  ...params,
472
696
  cacheKey: resolvedKey
473
697
  };
474
- const cached = await this.cache.get(resolvedKey);
698
+ const cached = await this.getCached(resolvedKey);
475
699
  if (cached.hit) {
476
700
  const value = cached.value;
477
701
  return {
@@ -520,7 +744,7 @@ var CacheOrchestrator = class {
520
744
  try {
521
745
  await this.cache.set(params.cacheKey, value, params.ttl);
522
746
  } catch (error) {
523
- this.logger.error("[VernLLM] cache write failed", { message: error instanceof Error ? error.message : "unknown" });
747
+ this.logger.warn(`[VernLLM] cache write failed: ${error instanceof Error ? error.message : "unknown"}`);
524
748
  }
525
749
  return value;
526
750
  }, (error) => {
@@ -562,6 +786,7 @@ var CircuitBreaker = class {
562
786
  threshold;
563
787
  cooldownMs;
564
788
  onStateChange;
789
+ /** Whether this breaker tracks failures per model instead of one shared circuit. Read by `CallExecutor`/`VernLLM` to report per-target in `getCircuitStates`. */
565
790
  isolateByModel;
566
791
  sharedBucket = newBucket();
567
792
  bucketsByModel = new Map();
@@ -607,12 +832,12 @@ var CircuitBreaker = class {
607
832
  if (bucket.state === "closed") return;
608
833
  if (bucket.state === "open") {
609
834
  const elapsed = Date.now() - bucket.openedAt;
610
- if (elapsed < this.cooldownMs) throw new LLMError(`Circuit open, provider has failed ${bucket.consecutiveFailures} times in a row. Retry in ${Math.ceil((this.cooldownMs - elapsed) / 1e3)}s.`, "circuit_open");
835
+ if (elapsed < this.cooldownMs) throw new LLMError(`Circuit open, provider has failed ${bucket.consecutiveFailures} times in a row. Retry in ${Math.ceil((this.cooldownMs - elapsed) / 1e3)}s.`, "circuit_open", { code: "circuit_cooling_down" });
611
836
  bucket.trialInFlight = true;
612
837
  this.transition(bucket, "half-open", model);
613
838
  return;
614
839
  }
615
- if (bucket.trialInFlight) throw new LLMError("Circuit half-open. A trial request is already in flight. Try again shortly.", "circuit_open");
840
+ if (bucket.trialInFlight) throw new LLMError("Circuit half-open. A trial request is already in flight. Try again shortly.", "circuit_open", { code: "circuit_trial_in_flight" });
616
841
  bucket.trialInFlight = true;
617
842
  }
618
843
  recordSuccess(model) {
@@ -647,6 +872,33 @@ var CircuitBreaker = class {
647
872
  getState(model) {
648
873
  return this.lookupBucket(model)?.state ?? "closed";
649
874
  }
875
+ /**
876
+ * Manually opens the circuit, as if `threshold` consecutive failures had
877
+ * just happened, e.g. to pull a provider out of rotation ahead of known
878
+ * maintenance. Resets the cooldown window from now, same as a real
879
+ * threshold-crossing failure would, and clears any in-flight half-open
880
+ * trial since it no longer applies once the circuit is (re)opened.
881
+ */
882
+ open(model) {
883
+ const bucket = this.ensureBucketFor(model);
884
+ bucket.openedAt = Date.now();
885
+ bucket.trialInFlight = false;
886
+ this.transition(bucket, "open", model);
887
+ }
888
+ /**
889
+ * Manually closes the circuit and resets its failure count, e.g. once a
890
+ * provider is confirmed healthy again without waiting out the cooldown.
891
+ * Mirrors `recordSuccess`'s bookkeeping (including dropping the
892
+ * per-model bucket under `isolateByModel`, once idle) but without
893
+ * requiring an actual successful call first.
894
+ */
895
+ close(model) {
896
+ const bucket = this.ensureBucketFor(model);
897
+ bucket.consecutiveFailures = 0;
898
+ bucket.trialInFlight = false;
899
+ this.transition(bucket, "closed", model);
900
+ if (this.isolateByModel && bucket.state === "closed" && bucket.consecutiveFailures === 0) this.bucketsByModel.delete(model ?? UNLABELED_MODEL);
901
+ }
650
902
  };
651
903
 
652
904
  //#endregion
@@ -765,7 +1017,7 @@ async function withTimeout(fn, timeoutMs, externalSignal) {
765
1017
  try {
766
1018
  return await fn(signal);
767
1019
  } catch (err) {
768
- if (controller.signal.aborted && !externalSignal?.aborted && err instanceof DOMException && err.name === "AbortError") throw new LLMError("Request timed out", "timeout");
1020
+ if (controller.signal.aborted && !externalSignal?.aborted && err instanceof DOMException && err.name === "AbortError") throw new LLMError("Request timed out", "timeout", { code: "request_timeout" });
769
1021
  throw err;
770
1022
  } finally {
771
1023
  clearTimeout(timer);
@@ -798,7 +1050,7 @@ function withChunkIdleTimeout(next, timeoutMs, onIdle, logger) {
798
1050
  const timer = setTimeout(() => {
799
1051
  settled = true;
800
1052
  onIdle?.();
801
- reject(new LLMError(`No stream chunk received for ${activeTimeoutMs}ms (idle timeout)`, "timeout"));
1053
+ reject(new LLMError(`No stream chunk received for ${activeTimeoutMs}ms (idle timeout)`, "timeout", { code: "idle_timeout" }));
802
1054
  }, clampTimeoutMs(activeTimeoutMs));
803
1055
  next().then((result) => {
804
1056
  clearTimeout(timer);
@@ -963,20 +1215,58 @@ function describeError(err) {
963
1215
  } catch {}
964
1216
  return formatSafely(err);
965
1217
  }
966
- /** Converts any thrown value into a well-typed LLMError. */
967
- function normalizeError(error, signal) {
968
- if (signal?.aborted) return new LLMError("LLM request aborted", "aborted");
1218
+ /**
1219
+ * Maps an HTTP status to its corresponding `LLMErrorCode`, derived purely
1220
+ * from the status itself so it applies the same way regardless of which
1221
+ * adapter or client raised the error. Used both when building a fresh
1222
+ * `LLMError` and when filling in a `code` on an already-normalized one
1223
+ * that doesn't have one yet, so the two paths can't drift apart.
1224
+ */
1225
+ function codeForStatus(status) {
1226
+ switch (status) {
1227
+ case 429: return "provider_rate_limited";
1228
+ case 401: return "authentication";
1229
+ case 403: return "authorization";
1230
+ case 404: return "not_found";
1231
+ case 413: return "payload_too_large";
1232
+ default: return status >= 500 ? "server_error" : void 0;
1233
+ }
1234
+ }
1235
+ /**
1236
+ * Converts any thrown value into a well-typed LLMError. `attempts`, when
1237
+ * given, is the accumulated record of every attempt made before `error`
1238
+ * was thrown; it's passed straight into the constructed error's options
1239
+ * rather than assigned onto the error afterward, so `attempts` is always
1240
+ * settled once, through the constructor, like every other field on
1241
+ * `LLMError`.
1242
+ */
1243
+ function normalizeError(error, signal, attempts) {
1244
+ if (signal?.aborted) return new LLMError("LLM request aborted", "aborted", { attempts });
969
1245
  if (error instanceof LLMError) {
970
- if (error.code === void 0) {
971
- if (error.status === 429) error.code = "provider_rate_limited";
972
- else if (error.status === 401 || error.status === 403) error.code = "invalid_credentials";
973
- }
1246
+ if (error.code === void 0 && error.status !== void 0) error.code = codeForStatus(error.status);
1247
+ if (error.attempts === void 0 && attempts !== void 0) error.attempts = attempts;
974
1248
  return error;
975
1249
  }
976
1250
  const status = extractStatus(error);
977
1251
  const retryAfterMs = extractRetryAfterMs(error);
978
- if (status !== void 0) return new LLMError("LLM request failed", "api", status, void 0, error, retryAfterMs, status === 429 ? "provider_rate_limited" : status === 401 || status === 403 ? "invalid_credentials" : void 0);
979
- return new LLMError("LLM request failed", "unknown", void 0, void 0, error, retryAfterMs, isNetworkError(error) ? "connection_failed" : void 0);
1252
+ if (status !== void 0) return new LLMError("LLM request failed", "api", {
1253
+ status,
1254
+ cause: error,
1255
+ retryAfterMs,
1256
+ code: codeForStatus(status),
1257
+ attempts
1258
+ });
1259
+ if (isNetworkError(error)) return new LLMError("LLM request failed", "network", {
1260
+ cause: error,
1261
+ retryAfterMs,
1262
+ code: "connection_failed",
1263
+ attempts
1264
+ });
1265
+ return new LLMError("LLM request failed", "unknown", {
1266
+ cause: error,
1267
+ retryAfterMs,
1268
+ attempts
1269
+ });
980
1270
  }
981
1271
 
982
1272
  //#endregion
@@ -1025,7 +1315,7 @@ function parseWireToolCalls(wireToolCalls) {
1025
1315
  try {
1026
1316
  parsedArgs = wc.function.arguments.trim() ? JSON.parse(wc.function.arguments) : {};
1027
1317
  } catch {
1028
- throw new LLMError(`Invalid JSON arguments for tool call "${wc.function.name}"`, "parse");
1318
+ throw new LLMError(`Invalid JSON arguments for tool call "${wc.function.name}"`, "parse", { code: "tool_arguments_parse_failed" });
1029
1319
  }
1030
1320
  return {
1031
1321
  id: wc.id,
@@ -1040,9 +1330,13 @@ function parseWireToolCalls(wireToolCalls) {
1040
1330
  /**
1041
1331
  * Builds the wire request object for one call, applying per-instance
1042
1332
  * defaults (model, max tokens, temperature) and per-call overrides.
1043
- * Owns every validation that depends only on shape, not on execution:
1044
- * history alternation, duplicate/empty tool lists, `toolChoice` naming a
1045
- * real tool. Has no knowledge of retry, timeouts, or the breaker, only
1333
+ * Owns every check that depends only on the caller's own input shape, not
1334
+ * on execution: history alternation, duplicate/empty tool lists,
1335
+ * `toolChoice` naming a real tool. All deterministic on the call site's
1336
+ * own input and never touch the network, so every throw here is
1337
+ * `type: 'invalid_params'`, not `'validation'` (which is reserved for the
1338
+ * model/provider's own response failing a contract check). Has no
1339
+ * knowledge of retry, timeouts, or the breaker, only
1046
1340
  * the three defaults a `FallbackTarget` can override per-target (see the
1047
1341
  * `defaultMaxTokens`/`defaultTemperature` overrides in the fallback
1048
1342
  * design), which is what keeps it separable from `CallExecutor`.
@@ -1060,7 +1354,7 @@ var RequestBuilder = class {
1060
1354
  build(params) {
1061
1355
  const { systemPrompt, userContent, history = [], maxTokens = this.defaultMaxTokens, model = this.model, reasoningEffort, jsonSchema, tools, toolChoice } = params;
1062
1356
  const temperature = params.temperature === void 0 ? this.defaultTemperature : params.temperature;
1063
- if (tools && tools.length === 0) throw new LLMError("`tools` was an empty array. This is almost always a bug (e.g. a filtered tool list that ended up empty). An empty `tools` array still switches on tool-call mode (response shape, jsonMode default, wire format) with nothing for the model to call. Omit `tools` entirely for a normal call, or make sure the array is non-empty.", "validation");
1357
+ if (tools && tools.length === 0) throw new LLMError("`tools` was an empty array. This is almost always a bug (e.g. a filtered tool list that ended up empty). An empty `tools` array still switches on tool-call mode (response shape, jsonMode default, wire format) with nothing for the model to call. Omit `tools` entirely for a normal call, or make sure the array is non-empty.", "invalid_params");
1064
1358
  if (tools) {
1065
1359
  const seen = new Set();
1066
1360
  const duplicates = new Set();
@@ -1068,13 +1362,22 @@ var RequestBuilder = class {
1068
1362
  if (seen.has(tool.name)) duplicates.add(tool.name);
1069
1363
  seen.add(tool.name);
1070
1364
  }
1071
- if (duplicates.size) throw new LLMError(`\`tools\` has duplicate name(s): [${[...duplicates].join(", ")}]. Tool names must be unique.`, "validation");
1365
+ if (duplicates.size) throw new LLMError(`\`tools\` has duplicate name(s): [${[...duplicates].join(", ")}]. Tool names must be unique.`, "invalid_params", {
1366
+ code: "duplicate_tool_names",
1367
+ issues: { names: [...duplicates] }
1368
+ });
1072
1369
  }
1073
- if (toolChoice && !tools) throw new LLMError("`toolChoice` was set without `tools`. There is nothing for it to choose between. Set `tools`, or remove `toolChoice`.", "validation");
1074
- if (tools && typeof toolChoice === "object" && !tools.some((t) => t.name === toolChoice.name)) throw new LLMError(`toolChoice names "${toolChoice.name}", which is not in \`tools\` ([${tools.map((t) => t.name).join(", ")}]).`, "validation");
1370
+ if (toolChoice && !tools) throw new LLMError("`toolChoice` was set without `tools`. There is nothing for it to choose between. Set `tools`, or remove `toolChoice`.", "invalid_params");
1371
+ if (tools && typeof toolChoice === "object" && !tools.some((t) => t.name === toolChoice.name)) throw new LLMError(`toolChoice names "${toolChoice.name}", which is not in \`tools\` ([${tools.map((t) => t.name).join(", ")}]).`, "invalid_params", {
1372
+ code: "unknown_tool_choice",
1373
+ issues: {
1374
+ requested: toolChoice.name,
1375
+ available: tools.map((t) => t.name)
1376
+ }
1377
+ });
1075
1378
  const jsonMode = params.jsonMode ?? (tools ? false : true);
1076
1379
  const useJson = jsonMode || Boolean(jsonSchema);
1077
- if (params.schema && !useJson) throw new LLMError("schema was provided but jsonMode: false disables JSON parsing, so nothing would validate it. Remove jsonMode: false, set jsonSchema, or remove schema.", "validation");
1380
+ if (params.schema && !useJson) throw new LLMError("schema was provided but jsonMode: false disables JSON parsing, so nothing would validate it. Remove jsonMode: false, set jsonSchema, or remove schema.", "invalid_params");
1078
1381
  const responseFormat = this.buildResponseFormat(jsonSchema, useJson);
1079
1382
  this.validateHistory(history);
1080
1383
  const request = {
@@ -1111,29 +1414,47 @@ var RequestBuilder = class {
1111
1414
  let previousTurn;
1112
1415
  for (const [index, turn] of history.entries()) {
1113
1416
  if (turn.role === "tool") {
1114
- if (previousTurn?.role !== "assistant" || !previousTurn.toolCalls?.length) throw new LLMError(`history[${index}] is a "tool" turn, but must immediately follow an "assistant" turn that requested tools`, "validation");
1115
- if (!turn.toolResults?.length) throw new LLMError(`history[${index}] is a "tool" turn but has no toolResults`, "validation");
1417
+ if (previousTurn?.role !== "assistant" || !previousTurn.toolCalls?.length) throw new LLMError(`history[${index}] is a "tool" turn, but must immediately follow an "assistant" turn that requested tools`, "invalid_params");
1418
+ if (!turn.toolResults?.length) throw new LLMError(`history[${index}] is a "tool" turn but has no toolResults`, "invalid_params");
1116
1419
  const requestedIds = new Set(previousTurn.toolCalls.map((tc) => tc.id));
1117
1420
  const resultIds = turn.toolResults.map((tr) => tr.toolCallId);
1118
1421
  const unknownIds = resultIds.filter((id) => !requestedIds.has(id));
1119
- if (unknownIds.length) throw new LLMError(`history[${index}].toolResults references unknown toolCallId(s) [${unknownIds.join(", ")}]`, "validation");
1422
+ if (unknownIds.length) throw new LLMError(`history[${index}].toolResults references unknown toolCallId(s) [${unknownIds.join(", ")}]`, "invalid_params", {
1423
+ code: "unknown_tool_result_ids",
1424
+ issues: {
1425
+ historyIndex: index,
1426
+ ids: unknownIds
1427
+ }
1428
+ });
1120
1429
  const seenIds = new Set();
1121
1430
  const duplicateIds = new Set();
1122
1431
  for (const id of resultIds) {
1123
1432
  if (seenIds.has(id)) duplicateIds.add(id);
1124
1433
  seenIds.add(id);
1125
1434
  }
1126
- if (duplicateIds.size) throw new LLMError(`history[${index}].toolResults has duplicate toolCallId(s) [${[...duplicateIds].join(", ")}]`, "validation");
1435
+ if (duplicateIds.size) throw new LLMError(`history[${index}].toolResults has duplicate toolCallId(s) [${[...duplicateIds].join(", ")}]`, "invalid_params", {
1436
+ code: "duplicate_tool_result_ids",
1437
+ issues: {
1438
+ historyIndex: index,
1439
+ ids: [...duplicateIds]
1440
+ }
1441
+ });
1127
1442
  const missingIds = [...requestedIds].filter((id) => !resultIds.includes(id));
1128
- if (missingIds.length) throw new LLMError(`history[${index}] is missing toolResults for toolCallId(s) [${missingIds.join(", ")}]`, "validation");
1443
+ if (missingIds.length) throw new LLMError(`history[${index}] is missing toolResults for toolCallId(s) [${missingIds.join(", ")}]`, "invalid_params", {
1444
+ code: "missing_tool_results",
1445
+ issues: {
1446
+ historyIndex: index,
1447
+ ids: missingIds
1448
+ }
1449
+ });
1129
1450
  } else {
1130
- if (turn.role === previousTurn?.role) throw new LLMError(`history must alternate user/assistant turns: consecutive "${turn.role}" turns at history[${index - 1}] and history[${index}]`, "validation");
1131
- if (previousTurn?.role === "assistant" && previousTurn.toolCalls?.length) throw new LLMError(`history[${index}] follows an assistant tool request without tool results`, "validation");
1451
+ if (turn.role === previousTurn?.role) throw new LLMError(`history must alternate user/assistant turns: consecutive "${turn.role}" turns at history[${index - 1}] and history[${index}]`, "invalid_params");
1452
+ if (previousTurn?.role === "assistant" && previousTurn.toolCalls?.length) throw new LLMError(`history[${index}] follows an assistant tool request without tool results`, "invalid_params");
1132
1453
  }
1133
1454
  previousTurn = turn;
1134
1455
  }
1135
- if (previousTurn?.role === "assistant" && previousTurn.toolCalls?.length) throw new LLMError("The last entry in history is an assistant tool request without tool results", "validation");
1136
- if (previousTurn?.role === "user") throw new LLMError("The last entry in history is a \"user\" turn, which would collide with the current userContent turn.", "validation");
1456
+ if (previousTurn?.role === "assistant" && previousTurn.toolCalls?.length) throw new LLMError("The last entry in history is an assistant tool request without tool results", "invalid_params");
1457
+ if (previousTurn?.role === "user") throw new LLMError("The last entry in history is a \"user\" turn, which would collide with the current userContent turn.", "invalid_params");
1137
1458
  }
1138
1459
  /** Maps VernLLM's app-facing `ToolChoice` onto the OpenAI-shaped wire `tool_choice`. */
1139
1460
  buildWireToolChoice(toolChoice) {
@@ -1401,6 +1722,18 @@ var CallExecutor = class {
1401
1722
  getCircuitState(model) {
1402
1723
  return this.breaker?.getState(model);
1403
1724
  }
1725
+ /** Whether this target's breaker tracks failures per model. `false` if no breaker is configured. */
1726
+ get isolateByModel() {
1727
+ return this.breaker?.isolateByModel ?? false;
1728
+ }
1729
+ /** Manually opens this target's circuit breaker, if one is configured. No-op otherwise. */
1730
+ openCircuit(model) {
1731
+ this.breaker?.open(model);
1732
+ }
1733
+ /** Manually closes this target's circuit breaker, if one is configured. No-op otherwise. */
1734
+ closeCircuit(model) {
1735
+ this.breaker?.close(model);
1736
+ }
1404
1737
  /**
1405
1738
  * Throws if the breaker is open for this target/model, exactly like the
1406
1739
  * check `run`/`runStream` used to make internally. Exposed so `VernLLM`
@@ -1422,10 +1755,11 @@ var CallExecutor = class {
1422
1755
  */
1423
1756
  async run(params, requestId, onAttempt) {
1424
1757
  const model = params.model ?? this.model;
1758
+ const attempts = [];
1425
1759
  try {
1426
- return await this.retryWithBackoff((attempt) => this.executeCall(params, requestId, attempt), requestId, model, params.signal, onAttempt);
1760
+ return await this.retryWithBackoff((attempt) => this.executeCall(params, requestId, attempt), requestId, model, params.signal, onAttempt, attempts);
1427
1761
  } catch (error) {
1428
- const normalized = normalizeError(error, params.signal);
1762
+ const normalized = normalizeError(error, params.signal, attempts.length > 0 ? attempts : void 0);
1429
1763
  if (this.countsTowardBreaker(normalized)) this.breaker?.recordFailure(model);
1430
1764
  this.logger.debug(`[VernLLM:${requestId}] error:\n${this.redactText(describeError(error))}`);
1431
1765
  throw normalized;
@@ -1434,10 +1768,11 @@ var CallExecutor = class {
1434
1768
  /** Streaming counterpart to `run`. Mirrors the old streaming branch of `VernLLM.call`. */
1435
1769
  async runStream(params, requestId, onAttempt) {
1436
1770
  const model = params.model ?? this.model;
1771
+ const attempts = [];
1437
1772
  try {
1438
- return await this.retryWithBackoff((attempt) => this.executeStreamCall(params, requestId, attempt), requestId, model, params.signal, onAttempt);
1773
+ return await this.retryWithBackoff((attempt) => this.executeStreamCall(params, requestId, attempt), requestId, model, params.signal, onAttempt, attempts);
1439
1774
  } catch (error) {
1440
- const normalized = normalizeError(error, params.signal);
1775
+ const normalized = normalizeError(error, params.signal, attempts.length > 0 ? attempts : void 0);
1441
1776
  if (this.countsTowardBreaker(normalized)) this.breaker?.recordFailure(model);
1442
1777
  this.logger.debug(`[VernLLM:${requestId}] stream-open error:\n${this.redactText(describeError(error))}`);
1443
1778
  throw normalized;
@@ -1505,11 +1840,11 @@ var CallExecutor = class {
1505
1840
  finalizeResponse(rawContent, wireToolCalls, params, useJson, model, usage, requestId, attempt) {
1506
1841
  try {
1507
1842
  const content = rawContent?.trim();
1508
- if (!content && !wireToolCalls?.length) throw new LLMError("Empty LLM response", "api");
1843
+ if (!content && !wireToolCalls?.length) throw new LLMError("Empty LLM response", "api", { code: "empty_response" });
1509
1844
  this.logger.debug(`[VernLLM:${requestId}] output:\n${this.redactedOutput(content, wireToolCalls).slice(0, 800)}`);
1510
1845
  if (wireToolCalls?.length) {
1511
- if (!params.tools) throw new LLMError("Provider returned tool_calls but no `tools` were sent with this call.", "api");
1512
- if (params.toolChoice === "none") throw new LLMError("Provider returned tool_calls despite toolChoice: 'none'.", "api", void 0, void 0, void 0, void 0, "tool_choice_none_violated");
1846
+ if (!params.tools) throw new LLMError("Provider returned tool_calls but no `tools` were sent with this call.", "validation", { code: "unexpected_tool_calls" });
1847
+ if (params.toolChoice === "none") throw new LLMError("Provider returned tool_calls despite toolChoice: 'none'.", "validation", { code: "tool_choice_none_violated" });
1513
1848
  const toolCalls = parseWireToolCalls(wireToolCalls);
1514
1849
  this.validateToolCallArguments(toolCalls, params.tools);
1515
1850
  this.breaker?.recordSuccess(model);
@@ -1562,7 +1897,10 @@ var CallExecutor = class {
1562
1897
  async executeStreamCall(params, requestId, attempt) {
1563
1898
  const { useJson, model, request } = this.requestBuilder.build(params);
1564
1899
  const completions = this.client.chat.completions;
1565
- if (!completions.createStream) throw new LLMError("stream: true requires a client/adapter with createStream", "validation");
1900
+ if (!completions.createStream) throw new LLMError("stream: true requires a client/adapter with createStream", "invalid_params", {
1901
+ code: "unsupported_capability",
1902
+ issues: { capability: "createStream" }
1903
+ });
1566
1904
  const createStream = completions.createStream.bind(completions);
1567
1905
  let release;
1568
1906
  if (this.limiter) {
@@ -1623,13 +1961,13 @@ var CallExecutor = class {
1623
1961
  * `argumentsSchema`, if present.
1624
1962
  *
1625
1963
  * Contract failures (unknown name, duplicate id) are collected across
1626
- * every call and thrown together, since retrying a request that already
1627
- * has these errors cannot help (`shouldRetry` excludes them by `code`)
1628
- * and a caller fixing them wants to see every one, not just the first.
1629
- * Schema failures keep the original single-error, `type: 'validation'`
1630
- * shape rather than being folded into the aggregate, since they're a
1631
- * distinct failure kind from the contract failures above (also excluded
1632
- * from retry, by `type` rather than `code`; see `shouldRetry`).
1964
+ * every call and thrown together as one `type: 'validation'` error with
1965
+ * `issues: ToolIssue[]`, since retrying a request that already has these
1966
+ * errors cannot help (excluded from retry by `type`) and a caller fixing
1967
+ * them wants to see every one, not just the first. Schema failures keep
1968
+ * the original single-error, `type: 'validation'` shape rather than being
1969
+ * folded into the aggregate, since they're a distinct failure kind from
1970
+ * the contract failures above.
1633
1971
  */
1634
1972
  validateToolCallArguments(toolCalls, tools) {
1635
1973
  const known = new Map(tools.map((t) => [t.name, t]));
@@ -1651,20 +1989,31 @@ var CallExecutor = class {
1651
1989
  if (toolIssues.length > 0) {
1652
1990
  const unknownTool = toolIssues.find((i) => i.code === "unknown_tool");
1653
1991
  const primary = unknownTool ? `Model requested tool "${unknownTool.name}", which was not in the tools offered ([${[...known.keys()].join(", ")}]).` : `Duplicate tool call id "${toolIssues[0].toolCallId}" in the model's response.`;
1654
- const message = toolIssues.length > 1 ? `${primary} (${toolIssues.length} tool call issues total, see toolIssues.)` : primary;
1655
- const error = new LLMError(message, "api", void 0, void 0, void 0, void 0, unknownTool ? "unknown_tool" : "duplicate_tool_call_id");
1656
- error.toolIssues = toolIssues;
1657
- throw error;
1992
+ const message = toolIssues.length > 1 ? `${primary} (${toolIssues.length} tool call issues total, see error.issues.)` : primary;
1993
+ throw new LLMError(message, "validation", {
1994
+ code: unknownTool ? "unknown_tool" : "duplicate_tool_call_id",
1995
+ issues: toolIssues
1996
+ });
1658
1997
  }
1659
1998
  for (const call of toolCalls) {
1660
1999
  const definition = known.get(call.name);
1661
2000
  if (!definition?.argumentsSchema) continue;
1662
2001
  const result = definition.argumentsSchema.safeParse(call.arguments);
1663
- if (!result.success) throw new LLMError(`Arguments for tool call "${call.name}" failed validation`, "validation", void 0, result.error);
2002
+ if (!result.success) throw new LLMError(`Arguments for tool call "${call.name}" failed validation`, "validation", { issues: result.error });
1664
2003
  }
1665
2004
  }
1666
- /** Runs `fn`, retrying with backoff according to `shouldRetry`. */
1667
- async retryWithBackoff(fn, requestId, model, signal, onAttempt) {
2005
+ /**
2006
+ * Runs `fn`, retrying with backoff according to `shouldRetry`. When
2007
+ * `attempts` is given, every failed attempt that is actually followed by
2008
+ * a retry is recorded, in order. This mirrors `LLMError.attempts`'s
2009
+ * contract: every attempt made before this error was thrown. The
2010
+ * terminal failure is never pushed since it isn't a prior attempt, it
2011
+ * is the error being thrown. `attempts` stays empty when nothing was
2012
+ * retried, so no separate bookkeeping is needed at the call sites.
2013
+ * Each failure is recorded as a snapshot (`LLMError.toSnapshot()`),
2014
+ * not the live `LLMError`, per `RetryAttempt`'s contract.
2015
+ */
2016
+ async retryWithBackoff(fn, requestId, model, signal, onAttempt, attempts) {
1668
2017
  let lastError;
1669
2018
  for (let attempt = 0; attempt <= this.maxRetries; attempt++) try {
1670
2019
  if (attempt > 0) await this.recoverDelay(requestId, model, attempt, lastError, signal);
@@ -1672,7 +2021,12 @@ var CallExecutor = class {
1672
2021
  return await fn(attempt);
1673
2022
  } catch (error) {
1674
2023
  lastError = error;
1675
- if (!this.shouldRetry(error, signal)) break;
2024
+ const willRetry = attempt < this.maxRetries && this.shouldRetry(error, signal);
2025
+ if (!willRetry) break;
2026
+ attempts?.push({
2027
+ index: attempt,
2028
+ error: normalizeError(error, signal).toSnapshot()
2029
+ });
1676
2030
  }
1677
2031
  throw lastError;
1678
2032
  }
@@ -1742,7 +2096,7 @@ var CallExecutor = class {
1742
2096
  if (parsed === null || parsed === void 0) throw new LLMError("Invalid JSON response", "parse");
1743
2097
  if (!schema) return parsed;
1744
2098
  const result = schema.safeParse(parsed);
1745
- if (!result.success) throw new LLMError("Schema validation failed", "validation", void 0, result.error);
2099
+ if (!result.success) throw new LLMError("Schema validation failed", "validation", { issues: result.error });
1746
2100
  return result.data;
1747
2101
  }
1748
2102
  /**
@@ -1755,7 +2109,7 @@ var CallExecutor = class {
1755
2109
  const retryAfterMs = extractRetryAfterMs(error);
1756
2110
  const delay = retryAfterMs ?? getBackoffDelay(this.baseDelayMs, attempt);
1757
2111
  const retryAfterHonored = retryAfterMs !== void 0;
1758
- this.logger.warn(`[VernLLM:${requestId}] recovery attempt ${attempt}/${this.maxRetries}, waiting ${delay}ms` + (retryAfterHonored ? " (honoring Retry-After)" : ""));
2112
+ this.logger.warn(`[VernLLM:${requestId}] recovery attempt ${attempt}/${this.maxRetries}, waiting ${Math.ceil(delay)}ms` + (retryAfterHonored ? " (honoring Retry-After)" : ""));
1759
2113
  this.reportEvent({
1760
2114
  kind: "retry",
1761
2115
  requestId,
@@ -1769,15 +2123,10 @@ var CallExecutor = class {
1769
2123
  });
1770
2124
  await waitForRetry(delay, signal);
1771
2125
  }
1772
- isNonRetryableToolContractError(error) {
1773
- return error instanceof LLMError && (error.code === "unknown_tool" || error.code === "duplicate_tool_call_id" || error.code === "tool_choice_none_violated");
1774
- }
1775
2126
  /** Decides whether a failed attempt is worth retrying. */
1776
2127
  shouldRetry(error, signal) {
1777
2128
  if (signal?.aborted) return false;
1778
- if (error instanceof LLMError && (error.type === "parse" || error.type === "validation")) return false;
1779
- if (error instanceof LLMError && error.code === "local_rate_limit") return false;
1780
- if (this.isNonRetryableToolContractError(error)) return false;
2129
+ if (error instanceof LLMError && !error.retryable) return false;
1781
2130
  const status = extractStatus(error);
1782
2131
  return !(status !== void 0 && this.nonRetryableStatus.includes(status));
1783
2132
  }
@@ -1787,16 +2136,48 @@ var CallExecutor = class {
1787
2136
  * reusing a call id, or a provider ignoring `toolChoice: 'none'` isn't
1788
2137
  * the provider being unhealthy, it's a model/provider response defect
1789
2138
  * that will very likely recur regardless of provider health, so it
1790
- * shouldn't push a healthy provider's circuit toward opening. Mirrors
1791
- * the same reasoning `shouldRetry` already applies to
1792
- * `parse`/`validation`/these same tool-contract codes.
2139
+ * shouldn't push a healthy provider's circuit toward opening. Same for
2140
+ * a caller-input bug or a local rate-limit rejection: neither ever
2141
+ * reached the provider at all. This is exactly what `LLMError.retryable`
2142
+ * already excludes, so this defers to it directly.
1793
2143
  */
1794
2144
  countsTowardBreaker(error) {
1795
- if (error.type === "validation" || error.type === "parse" || error.type === "aborted" || error.code === "local_rate_limit" || this.isNonRetryableToolContractError(error)) return false;
1796
- return true;
2145
+ return error.retryable;
1797
2146
  }
1798
2147
  };
1799
2148
 
2149
+ //#endregion
2150
+ //#region src/internal/logger.utils.ts
2151
+ /**
2152
+ * Wraps a `Logger` so a throwing implementation can never break the call
2153
+ * it's trying to describe. `logger` is user-supplied (`VernLLMOptions.logger`),
2154
+ * so a custom logger that ships to a file, Datadog, etc. can throw for
2155
+ * reasons unrelated to VernLLM. Wrap once at construction so every
2156
+ * downstream `this.logger.warn(...)` call stays as-is and is safe by
2157
+ * construction, instead of guarding each call site individually.
2158
+ */
2159
+ function createSafeLogger(logger) {
2160
+ return {
2161
+ debug: safe(logger, "debug"),
2162
+ warn: safe(logger, "warn"),
2163
+ error: safe(logger, "error")
2164
+ };
2165
+ }
2166
+ function safe(logger, method) {
2167
+ const fn = logger[method].bind(logger);
2168
+ return (...args) => {
2169
+ try {
2170
+ swallowRejection(fn(...args));
2171
+ } catch {}
2172
+ };
2173
+ }
2174
+ function isPromiseLike(value) {
2175
+ return typeof value?.then === "function";
2176
+ }
2177
+ function swallowRejection(result) {
2178
+ if (isPromiseLike(result)) Promise.resolve(result).catch(() => {});
2179
+ }
2180
+
1800
2181
  //#endregion
1801
2182
  //#region src/logger.ts
1802
2183
  /**
@@ -1937,8 +2318,8 @@ var RateLimiter = class {
1937
2318
  */
1938
2319
  async acquire(estimatedTokens, signal) {
1939
2320
  if (signal?.aborted) throw new LLMError("LLM request aborted", "aborted");
1940
- if (!Number.isFinite(estimatedTokens) || estimatedTokens < 0) throw new LLMError(`estimatedTokens must be a finite, non-negative number, got ${String(estimatedTokens)}`, "validation");
1941
- if (this.tokens && estimatedTokens > this.tokens.getCapacity()) throw new LLMError(`estimatedTokens (${estimatedTokens}) exceeds the configured tokensPerMinute capacity (${this.tokens.getCapacity()}); this call could never acquire capacity.`, "quota_exceeded", void 0, void 0, void 0, void 0, "local_rate_limit");
2321
+ if (!Number.isFinite(estimatedTokens) || estimatedTokens < 0) throw new LLMError(`estimatedTokens must be a finite, non-negative number, got ${String(estimatedTokens)}`, "invalid_params");
2322
+ if (this.tokens && estimatedTokens > this.tokens.getCapacity()) throw new LLMError(`estimatedTokens (${estimatedTokens}) exceeds the configured tokensPerMinute capacity (${this.tokens.getCapacity()}); this call could never acquire capacity.`, "rate_limited", { code: "rate_limit_capacity_exceeded" });
1942
2323
  if (this.queue.length === 0) {
1943
2324
  const attempt = this.tryAcquireBuckets(estimatedTokens);
1944
2325
  if (attempt.ok) return {
@@ -1951,7 +2332,7 @@ var RateLimiter = class {
1951
2332
  return this.enqueue(estimatedTokens, void 0, signal);
1952
2333
  }
1953
2334
  queueFullError() {
1954
- return new LLMError("Rate limit queue is full", "quota_exceeded", void 0, void 0, void 0, void 0, "local_rate_limit");
2335
+ return new LLMError("Rate limit queue is full", "rate_limited", { code: "rate_limit_queue_full" });
1955
2336
  }
1956
2337
  enqueue(estimatedTokens, initialReason, signal) {
1957
2338
  return new Promise((resolvePromise, rejectPromise) => {
@@ -1984,7 +2365,7 @@ var RateLimiter = class {
1984
2365
  if (index !== -1) this.queue.splice(index, 1);
1985
2366
  };
1986
2367
  if (this.maxQueueMs > 0) queueTimer = setTimeout(() => {
1987
- waiter.reject(new LLMError("Rate limit queue timed out before capacity was available", "quota_exceeded", void 0, void 0, void 0, void 0, "local_rate_limit"));
2368
+ waiter.reject(new LLMError("Rate limit queue timed out before capacity was available", "rate_limited", { code: "rate_limit_queue_timeout" }));
1988
2369
  }, this.maxQueueMs);
1989
2370
  signal?.addEventListener("abort", onAbort, { once: true });
1990
2371
  this.queue.push(waiter);
@@ -2118,7 +2499,7 @@ var VernLLM = class {
2118
2499
  * `nonRetryableStatus` `[400, 401, 403, 404, 422]`, `debug` false.
2119
2500
  */
2120
2501
  constructor(options) {
2121
- this.logger = options.logger ?? new ConsoleLogger(options.debug ?? false);
2502
+ this.logger = createSafeLogger(options.logger ?? new ConsoleLogger(options.debug ?? false));
2122
2503
  const providerName = options.name ?? "primary";
2123
2504
  this.cacheOrchestrator = new CacheOrchestrator(options.cache ?? new InMemoryCacheAdapter(), this.logger);
2124
2505
  this.fallbackOn = options.fallbackOn ?? defaultFallbackOn;
@@ -2211,7 +2592,7 @@ var VernLLM = class {
2211
2592
  index: i - 1,
2212
2593
  provider: executor.providerName,
2213
2594
  model: params.model ?? executor.model,
2214
- error: normalized
2595
+ error: normalized.toSnapshot()
2215
2596
  });
2216
2597
  const isLast = i === this.executors.length - 1;
2217
2598
  const policyDecision = this.fallbackOn(normalized, { isLastTarget: isLast });
@@ -2230,7 +2611,7 @@ var VernLLM = class {
2230
2611
  });
2231
2612
  }
2232
2613
  }
2233
- throw new LLMError("No provider targets configured", "unknown");
2614
+ throw new LLMError("No provider targets configured", "invalid_params");
2234
2615
  }
2235
2616
  async call(params) {
2236
2617
  if (params.signal?.aborted) throw new LLMError("LLM request aborted", "aborted");
@@ -2276,7 +2657,7 @@ var VernLLM = class {
2276
2657
  async cachedCall(params) {
2277
2658
  const { call: callParams,...cacheParams } = params;
2278
2659
  const restCallParams = callParams;
2279
- if (restCallParams.reserveUsage || restCallParams.refundUsage) throw new LLMError("`reserveUsage`/`refundUsage` were set inside `call`, where cachedCall ignores them. Move them to the top level of the cachedCall() params, alongside cacheKey/ttl, instead.", "validation");
2660
+ if (restCallParams.reserveUsage || restCallParams.refundUsage) throw new LLMError("`reserveUsage`/`refundUsage` were set inside `call`, where cachedCall ignores them. Move them to the top level of the cachedCall() params, alongside cacheKey/ttl, instead.", "invalid_params");
2280
2661
  if (restCallParams.stream) {
2281
2662
  const streamParams = restCallParams;
2282
2663
  return this.cacheOrchestrator.runCachedStream({
@@ -2290,35 +2671,67 @@ var VernLLM = class {
2290
2671
  });
2291
2672
  }
2292
2673
  /**
2293
- * @param model With `circuitBreaker.isolateByModel` on, returns that
2294
- * model's own circuit state instead of the shared one. Ignored
2295
- * otherwise. Omit for the shared circuit (the default) or, under
2296
- * isolation, the state of calls that didn't resolve a model.
2297
- * @returns The current circuit breaker state (`'closed' | 'open' |
2298
- * 'half-open'`), or undefined if no circuit breaker was configured.
2674
+ * @param target.index Which target to read. Defaults to the primary.
2675
+ * @param target.model Which model bucket to read, if the target isolates by model.
2676
+ * @returns The breaker state, or `undefined` if that target has no breaker.
2677
+ * @throws {RangeError} If `target.index` names no target. Lets a real
2678
+ * target with no breaker (`undefined`) stay distinguishable from a
2679
+ * target that doesn't exist.
2299
2680
  */
2300
- getCircuitState(model) {
2301
- return this.executors[0].getCircuitState(model);
2681
+ getCircuitState(target) {
2682
+ const executor = this.resolveExecutor(target?.index ?? 0, "getCircuitState");
2683
+ this.warnIfModelUnsupported(executor.isolateByModel, target?.model, "getCircuitState");
2684
+ return executor.getCircuitState(target?.model ?? executor.model);
2302
2685
  }
2303
2686
  /**
2304
- * @param model With `circuitBreaker.isolateByModel` on, returns each
2305
- * target's circuit state for that model instead of its shared state.
2306
- * Ignored otherwise. Omit for the shared circuit (the default) or, under
2307
- * isolation, the state of calls that didn't resolve a model.
2308
- * @returns The current circuit state for every target in declaration
2309
- * order, including the primary and all fallback targets. Each entry
2310
- * includes the target's provider name, chain index, whether it is a
2311
- * fallback, and its circuit state, or undefined if that target has no
2312
- * circuit breaker configured.
2687
+ * @param model Which model bucket to read, for targets that isolate by model.
2688
+ * @returns Every target's state, in chain order.
2313
2689
  */
2314
2690
  getCircuitStates(model) {
2315
2691
  return this.executors.map((executor, index) => ({
2316
2692
  provider: executor.providerName,
2317
2693
  index,
2318
2694
  isFallback: index > 0,
2319
- state: executor.getCircuitState(model)
2695
+ isolateByModel: executor.isolateByModel,
2696
+ state: executor.getCircuitState(model ?? executor.model)
2320
2697
  }));
2321
2698
  }
2699
+ /**
2700
+ * Manually opens a target's breaker, e.g. to pull a provider out of
2701
+ * rotation ahead of known maintenance instead of waiting for it to fail.
2702
+ *
2703
+ * @param target.index Which target to open. Defaults to the primary.
2704
+ * @param target.model Which model bucket to open, if the target isolates by model.
2705
+ * @throws {RangeError} If `target.index` names no target.
2706
+ */
2707
+ openCircuit(target) {
2708
+ const executor = this.resolveExecutor(target?.index ?? 0, "openCircuit");
2709
+ this.warnIfModelUnsupported(executor.isolateByModel, target?.model, "openCircuit");
2710
+ executor.openCircuit(target?.model ?? executor.model);
2711
+ }
2712
+ /**
2713
+ * Manually closes a target's breaker, e.g. once a provider is confirmed
2714
+ * healthy again without waiting out the cooldown.
2715
+ *
2716
+ * @param target.index Which target to close. Defaults to the primary.
2717
+ * @param target.model Which model bucket to close, if the target isolates by model.
2718
+ * @throws {RangeError} If `target.index` names no target.
2719
+ */
2720
+ closeCircuit(target) {
2721
+ const executor = this.resolveExecutor(target?.index ?? 0, "closeCircuit");
2722
+ this.warnIfModelUnsupported(executor.isolateByModel, target?.model, "closeCircuit");
2723
+ executor.closeCircuit(target?.model ?? executor.model);
2724
+ }
2725
+ /** Resolves a target index so every circuit-breaker method agrees on what counts as valid. */
2726
+ resolveExecutor(index, caller) {
2727
+ const executor = this.executors[index];
2728
+ if (!executor) throw new RangeError(`${caller}: no target at index ${index} (chain has ${this.executors.length} target${this.executors.length === 1 ? "" : "s"})`);
2729
+ return executor;
2730
+ }
2731
+ /** Warns when `model` can't do anything on this target, so it's never silently ignored. */
2732
+ warnIfModelUnsupported(isolateByModel, model, caller) {
2733
+ if (model !== void 0 && !isolateByModel) this.logger.warn(`[VernLLM] ${caller}: \`model: '${model}'\` has no effect here. This target's circuitBreaker doesn't have isolateByModel on, so it only tracks one shared circuit regardless of \`model\`. Omit \`model\`, or set \`circuitBreaker.isolateByModel: true\` on this target if per-model tracking is what you want.`);
2734
+ }
2322
2735
  };
2323
2736
 
2324
2737
  //#endregion
@@ -2358,7 +2771,7 @@ async function* parseSseStream(source) {
2358
2771
  try {
2359
2772
  text = typeof chunk === "string" ? chunk : decoder.decode(chunk, { stream: true });
2360
2773
  } catch (cause) {
2361
- throw new LLMError("Invalid UTF-8 in SSE stream", "parse", void 0, void 0, cause);
2774
+ throw new LLMError("Invalid UTF-8 in SSE stream", "parse", { cause });
2362
2775
  }
2363
2776
  buffer = (buffer + text).replace(/\r\n/g, "\n").replace(/\r(?!$)/g, "\n");
2364
2777
  let boundary$1 = buffer.indexOf("\n\n");
@@ -2374,7 +2787,7 @@ async function* parseSseStream(source) {
2374
2787
  try {
2375
2788
  buffer += decoder.decode();
2376
2789
  } catch (cause) {
2377
- throw new LLMError("Invalid UTF-8 in SSE stream", "parse", void 0, void 0, cause);
2790
+ throw new LLMError("Invalid UTF-8 in SSE stream", "parse", { cause });
2378
2791
  }
2379
2792
  buffer = buffer.replace(/\r$/, "\n");
2380
2793
  let boundary = buffer.indexOf("\n\n");
@@ -2419,7 +2832,10 @@ function parseSseFrame(frame) {
2419
2832
  try {
2420
2833
  return JSON.parse(data);
2421
2834
  } catch (cause) {
2422
- throw new LLMError(`Invalid JSON in SSE frame: ${data.slice(0, 200)}`, "parse", void 0, void 0, cause);
2835
+ throw new LLMError(`Invalid JSON in SSE frame: ${data.slice(0, 200)}`, "parse", {
2836
+ cause,
2837
+ code: "stream_frame_invalid"
2838
+ });
2423
2839
  }
2424
2840
  }
2425
2841
 
@@ -2439,13 +2855,14 @@ const SUPPORTED_IMAGE_MIME_TYPES = [
2439
2855
  ];
2440
2856
  /**
2441
2857
  * Validates an `ImageBlock.mimeType` against the shared supported set.
2442
- * Throws a non-retryable `LLMError('validation')`, since an unsupported
2443
- * mimeType is a permanent failure, retrying the same input can't fix it,
2444
- * the same way a schema-validation or JSON-parse failure isn't retried.
2858
+ * Throws a non-retryable `LLMError('invalid_params')`, since an unsupported
2859
+ * mimeType is a bug in the caller's own input, deterministic before any
2860
+ * request is built, the same class of failure as every other check in
2861
+ * `RequestBuilder`.
2445
2862
  */
2446
2863
  function assertSupportedImageMimeType(mimeType) {
2447
2864
  if (SUPPORTED_IMAGE_MIME_TYPES.includes(mimeType)) return mimeType;
2448
- throw new LLMError(`Unsupported image mimeType "${mimeType}": expected one of ${SUPPORTED_IMAGE_MIME_TYPES.join(", ")}`, "validation");
2865
+ throw new LLMError(`Unsupported image mimeType "${mimeType}": expected one of ${SUPPORTED_IMAGE_MIME_TYPES.join(", ")}`, "invalid_params");
2449
2866
  }
2450
2867
 
2451
2868
  //#endregion
@@ -2758,7 +3175,7 @@ function toAnthropicMessage(m) {
2758
3175
  try {
2759
3176
  input = tc.function.arguments.trim() ? JSON.parse(tc.function.arguments) : {};
2760
3177
  } catch (cause) {
2761
- throw new LLMError(`Assistant tool call "${tc.function.name}" (${tc.id}) has arguments that are not valid JSON.`, "validation", void 0, void 0, cause);
3178
+ throw new LLMError(`Assistant tool call "${tc.function.name}" (${tc.id}) has arguments that are not valid JSON.`, "validation", { cause });
2762
3179
  }
2763
3180
  if (input === null || Array.isArray(input) || typeof input !== "object") throw new LLMError(`Assistant tool call "${tc.function.name}" (${tc.id}) arguments must be a JSON object.`, "validation");
2764
3181
  blocks.push({
@@ -2839,7 +3256,10 @@ function parseToolArguments(text, toolName) {
2839
3256
  try {
2840
3257
  parsed = text.trim() ? JSON.parse(text) : {};
2841
3258
  } catch (cause) {
2842
- throw new LLMError(`Tool call "${toolName}" arguments are not valid JSON.`, "validation", void 0, void 0, cause);
3259
+ throw new LLMError(`Tool call "${toolName}" arguments are not valid JSON.`, "parse", {
3260
+ cause,
3261
+ code: "tool_arguments_parse_failed"
3262
+ });
2843
3263
  }
2844
3264
  if (!parsed || Array.isArray(parsed) || typeof parsed !== "object") throw new LLMError(`Tool call "${toolName}" arguments must be a JSON object.`, "validation");
2845
3265
  return parsed;
@@ -2973,7 +3393,10 @@ function fromGemini(geminiClient) {
2973
3393
  };
2974
3394
  },
2975
3395
  async *createStream(params, options) {
2976
- if (!geminiClient.generateContentStream) throw new LLMError("stream: true requires a Gemini client with generateContentStream", "validation");
3396
+ if (!geminiClient.generateContentStream) throw new LLMError("stream: true requires a Gemini client with generateContentStream", "invalid_params", {
3397
+ code: "unsupported_capability",
3398
+ issues: { capability: "generateContentStream" }
3399
+ });
2977
3400
  const request = buildGeminiRequest(params);
2978
3401
  request.config = {
2979
3402
  ...request.config,
@@ -3121,7 +3544,10 @@ function buildBedrockRequest(params, toolUseSupportedModels, nativeStructuredOut
3121
3544
  if (params.tools?.length && !toolName) toolConfig = buildBedrockToolConfig(params.tools, params.tool_choice);
3122
3545
  if (jsonSchema && toolConfig && toolUseSupportedModels) {
3123
3546
  const isSupported = Array.isArray(toolUseSupportedModels) ? toolUseSupportedModels.includes(params.model) : toolUseSupportedModels(params.model);
3124
- if (!isSupported) throw new LLMError(`Bedrock model "${params.model}" is not listed in toolUseSupportedModels, but this call requires Converse tool use (either jsonSchema emulated as a forced tool call, or real \`tools\` sent alongside native structured output).`, "validation");
3547
+ if (!isSupported) throw new LLMError(`Bedrock model "${params.model}" is not listed in toolUseSupportedModels, but this call requires Converse tool use (either jsonSchema emulated as a forced tool call, or real \`tools\` sent alongside native structured output).`, "invalid_params", {
3548
+ code: "unsupported_capability",
3549
+ issues: { capability: "toolUseSupportedModels" }
3550
+ });
3125
3551
  }
3126
3552
  const systemParts = [systemMessage?.content, jsonInstruction].filter((s) => Boolean(s));
3127
3553
  const request = {
@@ -3233,7 +3659,10 @@ function fromBedrock(bedrockClient, options) {
3233
3659
  };
3234
3660
  },
3235
3661
  async *createStream(params, requestOptions) {
3236
- if (!bedrockClient.converseStream) throw new LLMError("stream: true requires a Bedrock client with converseStream", "validation");
3662
+ if (!bedrockClient.converseStream) throw new LLMError("stream: true requires a Bedrock client with converseStream", "invalid_params", {
3663
+ code: "unsupported_capability",
3664
+ issues: { capability: "converseStream" }
3665
+ });
3237
3666
  const { request, toolName } = buildBedrockRequest(params, toolUseSupportedModels, nativeStructuredOutputModels);
3238
3667
  const { stream } = await bedrockClient.converseStream(request, requestOptions);
3239
3668
  const blockKinds = new Map();
@@ -3275,12 +3704,18 @@ function fromBedrock(bedrockClient, options) {
3275
3704
  total_tokens: event.metadata.usage.totalTokens
3276
3705
  }
3277
3706
  };
3278
- else if ("throttlingException" in event) throw new LLMError(event.throttlingException.message ?? "Bedrock throttled the request mid-stream", "api", 429);
3707
+ else if ("throttlingException" in event) throw new LLMError(event.throttlingException.message ?? "Bedrock throttled the request mid-stream", "api", {
3708
+ status: 429,
3709
+ code: "provider_rate_limited"
3710
+ });
3279
3711
  else if ("validationException" in event) throw new LLMError(event.validationException.message ?? "Bedrock rejected the request mid-stream", "validation");
3280
3712
  else if ("internalServerException" in event || "serviceUnavailableException" in event || "modelStreamErrorException" in event) {
3281
3713
  const detail = "internalServerException" in event && event.internalServerException.message || "serviceUnavailableException" in event && event.serviceUnavailableException.message || "modelStreamErrorException" in event && event.modelStreamErrorException.message || "Bedrock reported a mid-stream error";
3282
3714
  const status = "modelStreamErrorException" in event && event.modelStreamErrorException.originalStatusCode || "serviceUnavailableException" in event && 503 || 500;
3283
- throw new LLMError(detail, "api", status);
3715
+ throw new LLMError(detail, "api", {
3716
+ status,
3717
+ code: status >= 500 ? "server_error" : void 0
3718
+ });
3284
3719
  }
3285
3720
  }
3286
3721
  } } };
@@ -3289,7 +3724,10 @@ function fromBedrock(bedrockClient, options) {
3289
3724
  function toBedrockToolChoice(toolChoice) {
3290
3725
  if (!toolChoice || toolChoice === "auto") return { auto: {} };
3291
3726
  if (toolChoice === "required") return { any: {} };
3292
- if (toolChoice === "none") throw new LLMError("'none' is not supported by fromBedrock: Bedrock Converse has no `tool_choice` equivalent to forbidding tool use while tools are still offered. Omit `tools` entirely for this call instead.", "validation");
3727
+ if (toolChoice === "none") throw new LLMError("'none' is not supported by fromBedrock: Bedrock Converse has no `tool_choice` equivalent to forbidding tool use while tools are still offered. Omit `tools` entirely for this call instead.", "invalid_params", {
3728
+ code: "unsupported_capability",
3729
+ issues: { capability: "toolChoice: 'none'" }
3730
+ });
3293
3731
  return { tool: { name: toolChoice.function.name } };
3294
3732
  }
3295
3733
  /**
@@ -3314,7 +3752,7 @@ function toBedrockMessage(m) {
3314
3752
  else try {
3315
3753
  input = JSON.parse(tc.function.arguments);
3316
3754
  } catch (cause) {
3317
- throw new LLMError(`Assistant tool call "${tc.function.name}" (${tc.id}) has arguments that are not valid JSON.`, "validation", void 0, void 0, cause);
3755
+ throw new LLMError(`Assistant tool call "${tc.function.name}" (${tc.id}) has arguments that are not valid JSON.`, "validation", { cause });
3318
3756
  }
3319
3757
  blocks.push({ toolUse: {
3320
3758
  toolUseId: tc.id,
@@ -3485,8 +3923,14 @@ function fromFetch(config) {
3485
3923
  };
3486
3924
  },
3487
3925
  async *createStream(params, options) {
3488
- if (!config.mapStreamEvent) throw new LLMError("stream: true requires mapStreamEvent to be configured on fromFetch", "validation");
3489
- if (config.request && !config.requestStream) throw new LLMError("`stream: true` requires `requestStream` to be configured on fromFetch when a custom `request` transport is set. `requestStream` does not fall back to `request` (it needs an async-iterable byte stream, which `RequestLike`'s buffered `ResponseLike` has no way to provide), without it, `stream: true` would silently use plain native `fetch` instead of your configured transport. Add a `requestStream` that opens the same connection your `request` does, or omit `request` if native `fetch` is fine for both.", "validation");
3926
+ if (!config.mapStreamEvent) throw new LLMError("stream: true requires mapStreamEvent to be configured on fromFetch", "invalid_params", {
3927
+ code: "unsupported_capability",
3928
+ issues: { capability: "mapStreamEvent" }
3929
+ });
3930
+ if (config.request && !config.requestStream) throw new LLMError("`stream: true` requires `requestStream` to be configured on fromFetch when a custom `request` transport is set. `requestStream` does not fall back to `request` (it needs an async-iterable byte stream, which `RequestLike`'s buffered `ResponseLike` has no way to provide), without it, `stream: true` would silently use plain native `fetch` instead of your configured transport. Add a `requestStream` that opens the same connection your `request` does, or omit `request` if native `fetch` is fine for both.", "invalid_params", {
3931
+ code: "unsupported_capability",
3932
+ issues: { capability: "requestStream" }
3933
+ });
3490
3934
  const { url, method, headers, body } = await buildRequestInit(config, params, config.mapRequest(params));
3491
3935
  const requestStream = config.requestStream ?? defaultRequestStream;
3492
3936
  const parseFrames = config.parseStreamFrames ?? parseSseStream;
@@ -3706,5 +4150,5 @@ const fromAtlasCloud = fromOpenAICompatible;
3706
4150
  const from01AI = fromOpenAICompatible;
3707
4151
 
3708
4152
  //#endregion
3709
- export { CircuitBreaker, ConsoleLogger, FallbackExhaustedError, InMemoryCacheAdapter, LLMError, NormalizedCacheAdapter, RateLimiter, SSE_PING, TieredCacheAdapter, VernLLM, defaultEstimateTokens, defaultFallbackOn, from01AI, fromAnthropic, fromAnyscale, fromAtlasCloud, fromBaseten, fromBedrock, fromCerebras, fromCloudflareWorkersAI, fromDeepInfra, fromDeepSeek, fromFeatherless, fromFetch, fromFireworks, fromFriendli, fromGemini, fromGroq, fromHyperbolic, fromInferenceNet, fromInfermatic, fromLMStudio, fromLambdaLabs, fromLepton, fromMiniMax, fromMistral, fromMoonshot, fromNebius, fromNovita, fromNvidiaNIM, fromOllama, fromOpenAI, fromOpenAICompatible, fromOpenRouter, fromParasail, fromPerplexity, fromSambaNova, fromSiliconFlow, fromSnowflakeCortex, fromStepFun, fromTogether, fromVLLM, fromVercelAIGateway, fromXAI, fromZhipu, isLLMError, isToolCallResult, parseSseStream };
4153
+ export { CircuitBreaker, ConsoleLogger, FallbackExhaustedError, InMemoryCacheAdapter, LLMError, NormalizedCacheAdapter, RateLimiter, SSE_PING, TieredCacheAdapter, VernLLM, defaultEstimateTokens, defaultFallbackOn, from01AI, fromAnthropic, fromAnyscale, fromAtlasCloud, fromBaseten, fromBedrock, fromCerebras, fromCloudflareWorkersAI, fromDeepInfra, fromDeepSeek, fromFeatherless, fromFetch, fromFireworks, fromFriendli, fromGemini, fromGroq, fromHyperbolic, fromInferenceNet, fromInfermatic, fromLMStudio, fromLambdaLabs, fromLepton, fromMiniMax, fromMistral, fromMoonshot, fromNebius, fromNovita, fromNvidiaNIM, fromOllama, fromOpenAI, fromOpenAICompatible, fromOpenRouter, fromParasail, fromPerplexity, fromSambaNova, fromSiliconFlow, fromSnowflakeCortex, fromStepFun, fromTogether, fromVLLM, fromVercelAIGateway, fromXAI, fromZhipu, hasIssues, isFallbackExhaustedError, isLLMError, isToolCallResult, parseSseStream };
3710
4154
  //# sourceMappingURL=index.js.map