vern-llm 2.0.0 → 2.1.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.d.cts CHANGED
@@ -1,14 +1,109 @@
1
+ //#region src/circuitBreaker.d.ts
2
+ interface CircuitBreakerOptions {
3
+ /** Consecutive failures before the circuit opens, default 5 */
4
+ threshold?: number;
5
+ /** How long the circuit stays open before allowing a trial request, in ms. Default 30000 */
6
+ cooldownMs?: number;
7
+ /**
8
+ * Called after every real state change, never for a no-op transition
9
+ * (e.g. open to open). `model` is the resolved model of whichever call
10
+ * triggered this specific transition (the `model` passed to whichever
11
+ * of `assertClosed`/`recordSuccess`/`recordFailure` caused it).
12
+ *
13
+ * With `isolateByModel` off (the default), this is a label only: the
14
+ * breaker still counts failures across every model together, so a
15
+ * threshold crossing can be the sum of several different models'
16
+ * failures even though only the triggering call's `model` is reported
17
+ * here. With `isolateByModel` on, it's exact: each model has its own
18
+ * counter, so the transition really was caused solely by that model.
19
+ */
20
+ onStateChange?: (from: CircuitState, to: CircuitState, consecutiveFailures: number, model?: string) => void;
21
+ /**
22
+ * Track a separate circuit per resolved model instead of one shared
23
+ * circuit for the whole instance. A failure on one model then never
24
+ * opens another model's circuit, at the cost of slower detection for
25
+ * an outage spread across many distinct models (each model's counter
26
+ * must independently cross `threshold`). Default false: one shared
27
+ * circuit, matching every version before this option existed.
28
+ *
29
+ * A call that omits `model` (only possible calling `CircuitBreaker`
30
+ * directly, `VernLLM` always passes one) falls into one shared bucket
31
+ * alongside every other call that also omits it.
32
+ */
33
+ isolateByModel?: boolean;
34
+ }
35
+ type CircuitState = 'closed' | 'open' | 'half-open';
36
+ /**
37
+ * Per retry VernLLM-instance circuit breaker. Tracks consecutive failures across
38
+ * calls. Once the threshold is hit, short-circuits new calls with an
39
+ * LLMError('circuit_open') instead of hitting the provider, until the
40
+ * cooldown elapses and a single trial call is allowed through
41
+ */
42
+ declare class CircuitBreaker {
43
+ private readonly threshold;
44
+ private readonly cooldownMs;
45
+ private readonly onStateChange?;
46
+ private readonly isolateByModel;
47
+ private readonly sharedBucket;
48
+ private readonly bucketsByModel;
49
+ constructor(options?: CircuitBreakerOptions);
50
+ /** Returns the bucket for a model if one already exists, without allocating. */
51
+ private lookupBucket;
52
+ /** Creates and stores a bucket for a model when the first mutation needs one. */
53
+ private ensureBucketFor;
54
+ /** Every state mutation routes through here, so `onStateChange` fires exactly once per real change. */
55
+ private transition;
56
+ /**
57
+ * Throws if the circuit is open and the cooldown hasn't elapsed, or if
58
+ * the circuit is half-open and a trial call is already in flight.
59
+ * Otherwise, if the circuit just became eligible for a trial (cooldown
60
+ * elapsed, or half-open with no trial currently running), this call
61
+ * becomes that trial
62
+ */
63
+ assertClosed(model?: string): void;
64
+ recordSuccess(model?: string): void;
65
+ recordFailure(model?: string): void;
66
+ /**
67
+ * With `isolateByModel` off (the default), `model` is ignored and the
68
+ * one shared circuit's state is returned, unchanged from every version
69
+ * before this option existed. With `isolateByModel` on, returns that
70
+ * model's own state, `'closed'` for a model never seen yet, same as a
71
+ * fresh breaker.
72
+ */
73
+ getState(model?: string): CircuitState;
74
+ } //#endregion
1
75
  //#region src/types/errors.d.ts
76
+
77
+ //# sourceMappingURL=circuitBreaker.d.ts.map
2
78
  type LLMErrorType = 'timeout' | 'api' | 'parse' | 'validation' | 'circuit_open' | 'quota_exceeded' | 'unknown' | 'aborted';
79
+ /**
80
+ * Machine readable discriminator within a `type`, for cases where `type`
81
+ * alone is too coarse to act on. Optional and additive: errors thrown
82
+ * before a given code existed simply omit it.
83
+ */
84
+ type LLMErrorCode = 'unknown_tool' | 'duplicate_tool_call_id' | 'local_rate_limit' | 'provider_rate_limited' | 'fallback_exhausted';
85
+ /** One tool call's contract failure, used to report every bad call in a response at once. */
86
+ interface ToolIssue {
87
+ name: string;
88
+ toolCallId: string;
89
+ code: LLMErrorCode;
90
+ detail?: unknown;
91
+ }
3
92
  declare class LLMError extends Error {
4
93
  type: LLMErrorType;
5
94
  status?: number | undefined;
6
95
  issues?: unknown | undefined;
7
96
  cause?: unknown | undefined;
8
97
  retryAfterMs?: number | undefined;
9
- constructor(message: string, type: LLMErrorType, status?: number | undefined, issues?: unknown | undefined, cause?: unknown | undefined, retryAfterMs?: number | undefined);
98
+ /** Stable discriminator within `type`. Absent on errors predating it. */
99
+ code?: LLMErrorCode | undefined;
100
+ constructor(message: string, type: LLMErrorType, status?: number | undefined, issues?: unknown | undefined, cause?: unknown | undefined, retryAfterMs?: number | undefined, /** Stable discriminator within `type`. Absent on errors predating it. */
101
+ code?: LLMErrorCode | undefined);
102
+ /** Every tool contract failure in one response, when there is more than one. */
103
+ toolIssues?: ToolIssue[];
10
104
  }
11
105
  declare function isLLMError(err: unknown): err is LLMError;
106
+
12
107
  //#endregion
13
108
  //#region src/types/cache.d.ts
14
109
  //# sourceMappingURL=errors.d.ts.map
@@ -77,8 +172,206 @@ declare class TieredCacheAdapter<T = unknown> implements CacheAdapter<T> {
77
172
  }
78
173
 
79
174
  //#endregion
80
- //#region src/types/schema.d.ts
175
+ //#region src/rateLimit.d.ts
81
176
  //# sourceMappingURL=cache.d.ts.map
177
+ /** The request shape sent to `LLMClient['chat']['completions']['create']`, used for token estimation. */
178
+ type WireRequest = Parameters<LLMClient['chat']['completions']['create']>[0];
179
+ /** Which configured bucket is currently blocking a call. */
180
+ type RateLimitReason = 'concurrency' | 'rpm' | 'tpm';
181
+ interface RateLimitOptions {
182
+ /** Max requests per minute. Omit for unlimited. */
183
+ requestsPerMinute?: number;
184
+ /**
185
+ * Max tokens per minute. Enforced against a pre-flight estimate, then
186
+ * reconciled against reported usage once the call completes. Omit for
187
+ * unlimited.
188
+ */
189
+ tokensPerMinute?: number;
190
+ /** Max requests in flight at once. Default 0, meaning unlimited. */
191
+ maxConcurrent?: number;
192
+ /**
193
+ * Max time a call may sit queued waiting for capacity, in ms. Exceeding
194
+ * it throws rather than hanging forever. Default 30000. Pass 0 to wait
195
+ * indefinitely.
196
+ */
197
+ maxQueueMs?: number;
198
+ /** Max queued calls before new ones reject immediately instead of queueing. Default 0, unbounded. */
199
+ maxQueueSize?: number;
200
+ /**
201
+ * Pre-flight token estimate for `tokensPerMinute`. Defaults to a
202
+ * chars/4 heuristic over message content plus `max_tokens`.
203
+ */
204
+ estimateTokens?: (request: WireRequest) => number;
205
+ }
206
+ interface RateLimitAcquireResult {
207
+ /**
208
+ * Releases the concurrency slot this attempt held and reconciles the
209
+ * token bucket against real usage, when `actualTokens` is supplied.
210
+ * Idempotent: only the first call does anything. Must run in a
211
+ * `finally` block so a slot is never leaked on a failed attempt.
212
+ */
213
+ release: (actualTokens?: number) => void;
214
+ /** How long this attempt waited in queue before capacity was available. */
215
+ waitedMs: number;
216
+ /** Which bucket was blocking this attempt just before it cleared, if any wait happened. */
217
+ reason?: RateLimitReason;
218
+ }
219
+ /** Default `estimateTokens`: chars/4 over every message's content, plus the requested `max_tokens`. */
220
+ declare function defaultEstimateTokens(request: WireRequest): number;
221
+ /**
222
+ * Per-target rate limiter. Up to three buckets (requests/min, tokens/min,
223
+ * concurrency) behind one FIFO queue, so a large call isn't starved by a
224
+ * stream of small ones. Any bucket omitted from `options` has infinite
225
+ * capacity and never blocks.
226
+ */
227
+ declare class RateLimiter {
228
+ private readonly requests?;
229
+ private readonly tokens?;
230
+ private readonly concurrency?;
231
+ private readonly maxQueueMs;
232
+ private readonly maxQueueSize;
233
+ private readonly estimateTokensFn;
234
+ private readonly queue;
235
+ /**
236
+ * A single scheduled re-check for the head of the queue when it's
237
+ * blocked on a bucket that refills on its own clock (rpm/tpm), so a
238
+ * queue that nobody calls `acquire`/`release` on again isn't stuck
239
+ * forever waiting for an external trigger to re-drain it. Not needed
240
+ * for a concurrency block, which only clears via `release`.
241
+ */
242
+ private wakeTimer?;
243
+ constructor(options: RateLimitOptions);
244
+ /** Pre-flight token estimate for a request, per the configured (or default) heuristic. */
245
+ estimate(request: WireRequest): number;
246
+ /**
247
+ * Waits for capacity in every configured bucket, then takes from each.
248
+ * The returned `release` gives the concurrency slot back and reconciles
249
+ * the token bucket against real usage; it must run in a `finally` block.
250
+ */
251
+ acquire(estimatedTokens: number, signal?: AbortSignal): Promise<RateLimitAcquireResult>;
252
+ private queueFullError;
253
+ private enqueue;
254
+ /**
255
+ * Checks and takes from every configured bucket as one atomic unit: if
256
+ * any bucket lacks capacity, whatever was already taken from the
257
+ * earlier ones in this attempt is rolled back before reporting which
258
+ * bucket blocked.
259
+ */
260
+ private tryAcquireBuckets;
261
+ /** Drains the queue head first. Stops at the first waiter that still can't proceed, so no one is starved out of turn. */
262
+ private drain;
263
+ /**
264
+ * Schedules a one-shot re-check of the queue for whenever the bucket
265
+ * that's currently blocking the head waiter should next have enough
266
+ * capacity. A no-op for a concurrency block (only `release` can clear
267
+ * that) or while a wake is already pending.
268
+ */
269
+ private scheduleWake;
270
+ /**
271
+ * Builds the one-shot release closure for an acquired slot. Only the
272
+ * concurrency bucket is given back on release; the requests-per-minute
273
+ * bucket is a real spend that only recovers via its own refill, and the
274
+ * tokens bucket is reconciled against `actualTokens` rather than fully
275
+ * refunded, since real tokens really were spent.
276
+ */
277
+ private makeRelease;
278
+ }
279
+
280
+ //#endregion
281
+ //#region src/types/fallback.d.ts
282
+ //# sourceMappingURL=rateLimit.d.ts.map
283
+ /**
284
+ * One provider to try after the primary (or after an earlier fallback
285
+ * target) fails. Order is the policy: VernLLM never reorders, scores, or
286
+ * selects a target, it only walks the list as given.
287
+ *
288
+ * Most per-target overrides fall back to the parent `VernLLM` instance's
289
+ * own option when omitted, so a target only needs to specify what's
290
+ * actually different about it (a different client/model is the common
291
+ * case). `circuitBreaker` and `rateLimit` are the exception: they are
292
+ * never inherited from the parent, since a breaker or limiter tuned for
293
+ * the primary provider's limits is rarely right for a fallback's. Leave
294
+ * them unset on a target to run it without one, even if the parent has
295
+ * one configured.
296
+ */
297
+ interface FallbackTarget {
298
+ client: LLMClient;
299
+ model: string;
300
+ /** Label for events, errors, and `TokenUsage.provider`. Default `` `fallback[${index}]` ``. */
301
+ name?: string;
302
+ maxRetries?: number;
303
+ timeoutMs?: number;
304
+ chunkIdleTimeoutMs?: number;
305
+ baseDelayMs?: number;
306
+ defaultMaxTokens?: number;
307
+ defaultTemperature?: number | null;
308
+ nonRetryableStatus?: number[];
309
+ /** This target's own circuit breaker, independent of every other target's. Not inherited from the parent's `circuitBreaker`. */
310
+ circuitBreaker?: boolean | CircuitBreakerOptions;
311
+ /** This target's own rate limiter, independent of every other target's. Not inherited from the parent's `rateLimit`. */
312
+ rateLimit?: RateLimitOptions;
313
+ }
314
+ /**
315
+ * Written into `CallParams['meta']` once `call()` resolves, so a caller
316
+ * who wants provider identity on the same line as the result doesn't need
317
+ * to read it back out of `onUsage`.
318
+ */
319
+ interface CallMeta {
320
+ provider: string;
321
+ model: string;
322
+ /** `-1` if the primary target answered, otherwise the index into `fallback`. */
323
+ fallbackIndex: number;
324
+ usedFallback: boolean;
325
+ /** Attempts made against the target that ultimately answered, including the successful one. */
326
+ attempts: number;
327
+ }
328
+ /** One target's circuit state, as returned by `VernLLM.getCircuitStates()`. */
329
+ interface TargetCircuitState {
330
+ provider: string;
331
+ /** Position in the chain: `0` for the primary, `1`+ for fallback targets. */
332
+ index: number;
333
+ isFallback: boolean;
334
+ /** `undefined` if that target has no circuit breaker configured. */
335
+ state: CircuitState | undefined;
336
+ }
337
+ /** One target's failure, recorded on the way to either the next target or `FallbackExhaustedError`. */
338
+ interface FallbackAttempt {
339
+ /** `-1` for the primary target. */
340
+ index: number;
341
+ provider: string;
342
+ model: string;
343
+ error: LLMError;
344
+ }
345
+ /**
346
+ * Decides what happens after a target's own retries are exhausted or
347
+ * abandoned early. Called once per failed target. `'retry'` is not a
348
+ * valid return here: retrying already happened inside the target, this
349
+ * only decides whether to move on to the next one or stop.
350
+ */
351
+ type FallbackOn = (error: LLMError, context: {
352
+ isLastTarget: boolean;
353
+ }) => 'next' | 'stop';
354
+ /**
355
+ * The default `fallbackOn` policy. Exported so a caller can wrap rather
356
+ * than replace it, e.g. `fallbackOn: (e, ctx) => myCheck(e) ? 'stop' : defaultFallbackOn(e, ctx)`.
357
+ */
358
+ declare const defaultFallbackOn: FallbackOn;
359
+ /**
360
+ * Thrown when the chain gives up, whether because the last target failed
361
+ * or `fallbackOn` chose to stop early. Carries each attempt in order so
362
+ * an outage across providers stays debuggable without reproducing it.
363
+ * Extends `LLMError` so `isLLMError` and any `instanceof LLMError` check
364
+ * still passes, inheriting the last failure's `type` so existing
365
+ * type-based handling keeps working on a fallback-exhausted error too.
366
+ */
367
+ declare class FallbackExhaustedError extends LLMError {
368
+ readonly attempts: FallbackAttempt[];
369
+ constructor(attempts: FallbackAttempt[]);
370
+ }
371
+
372
+ //#endregion
373
+ //#region src/types/schema.d.ts
374
+ //# sourceMappingURL=fallback.d.ts.map
82
375
  /**
83
376
  * Minimal structural type for a Zod-like schema, so this package doesnt need
84
377
  * a hard dependency on a specific Zod major version. Any object exposing
@@ -213,6 +506,20 @@ interface TokenUsage {
213
506
  totalTokens: number;
214
507
  requestId: string;
215
508
  model: string;
509
+ /**
510
+ * The provider target that produced this usage. See `VernLLMOptions['name']`,
511
+ * default `'primary'`. Optional so consumers constructing a `TokenUsage`
512
+ * themselves (e.g. in tests) aren't forced to supply it; `VernLLM` always
513
+ * populates it. Absent means the same as `'primary'` if you need a value.
514
+ */
515
+ provider?: string;
516
+ /**
517
+ * Whether this usage came from a fallback target rather than the
518
+ * primary. Optional for the same reason `provider` is: `VernLLM`
519
+ * always populates it, a hand-constructed `TokenUsage` (e.g. in tests)
520
+ * isn't forced to.
521
+ */
522
+ usedFallback?: boolean;
216
523
  }
217
524
  type OnUsage = (usage: TokenUsage) => void;
218
525
  /**
@@ -316,10 +623,18 @@ interface CallParams<T = unknown> extends UsageHooks {
316
623
  * (see `CallWithToolsResult`), a breaking-change point: omitting `tools`
317
624
  * keeps `call()`'s old `Promise<T>` behavior exactly.
318
625
  *
319
- * Mutually exclusive with `jsonSchema`/`schema`: on Anthropic and
320
- * Bedrock, `jsonSchema` is implemented internally as a forced single-tool
321
- * call, which would collide with real tools. Setting both throws
322
- * `LLMError('validation')`.
626
+ * Can be combined with `jsonSchema` on Gemini and OpenAI-compatible
627
+ * clients unconditionally (neither ever restricted the combination:
628
+ * Gemini builds `responseSchema`/`tools` as independent fields, OpenAI-
629
+ * compatible clients pass both straight through). On Anthropic and
630
+ * Bedrock, combining the two is opt-in per call site, via each
631
+ * adapter's `nativeStructuredOutputModels` option: models not covered
632
+ * by it still throw `LLMError('validation')`, since `jsonSchema` falls
633
+ * back to a forced single-tool call there, which would collide with
634
+ * real tools. See `fromAnthropic`/`fromBedrock`.
635
+ *
636
+ * `schema` (client-side validation, distinct from `jsonSchema`) was
637
+ * never restricted from combining with `tools` on any provider.
323
638
  */
324
639
  tools?: ToolDefinition[];
325
640
  /** Defaults to `'auto'` when `tools` is set. */
@@ -338,6 +653,18 @@ interface CallParams<T = unknown> extends UsageHooks {
338
653
  * `StreamCallResult`.
339
654
  */
340
655
  stream?: boolean;
656
+ /**
657
+ * Optional out-parameter for provider identity. Pass `{}` (or any object
658
+ * with a mutable `current` property) and `call()` writes a `CallMeta`
659
+ * into `meta.current` before returning, alongside whatever `onUsage`
660
+ * already reports. Ignored for `stream: true`, since `call()` returns
661
+ * before the outcome (and so the target that answered) is known; read
662
+ * `TokenUsage.provider`/`usedFallback` from `onUsage` for streaming
663
+ * calls instead.
664
+ */
665
+ meta?: {
666
+ current?: CallMeta;
667
+ };
341
668
  }
342
669
  /**
343
670
  * A `CallParams` variant where tool calling is explicitly enabled.
@@ -609,50 +936,9 @@ interface LLMClient {
609
936
  };
610
937
  }
611
938
 
612
- //#endregion
613
- //#region src/circuitBreaker.d.ts
614
- //# sourceMappingURL=client.d.ts.map
615
- interface CircuitBreakerOptions {
616
- /** Consecutive failures before the circuit opens, default 5 */
617
- threshold?: number;
618
- /** How long the circuit stays open before allowing a trial request, in ms. Default 30000 */
619
- cooldownMs?: number;
620
- }
621
- type CircuitState = 'closed' | 'open' | 'half-open';
622
- /**
623
- * Per retry VernLLM-instance circuit breaker. Tracks consecutive failures across
624
- * calls. Once the threshold is hit, short-circuits new calls with an
625
- * LLMError('circuit_open') instead of hitting the provider, until the
626
- * cooldown elapses and a single trial call is allowed through
627
- */
628
- declare class CircuitBreaker {
629
- private state;
630
- private consecutiveFailures;
631
- private openedAt;
632
- private threshold;
633
- private cooldownMs;
634
- /**
635
- * True while a single half-open trial call is in flight. Guards against
636
- * multiple concurrent callers all treating themselves as "the" trial once
637
- * the cooldown elapses
638
- */
639
- private trialInFlight;
640
- constructor(options?: CircuitBreakerOptions);
641
- /**
642
- * Throws if the circuit is open and the cooldown hasn't elapsed, or if
643
- * the circuit is half-open and a trial call is already in flight.
644
- * Otherwise, if the circuit just became eligible for a trial (cooldown
645
- * elapsed, or half-open with no trial currently running), this call
646
- * becomes that trial
647
- */
648
- assertClosed(): void;
649
- recordSuccess(): void;
650
- recordFailure(): void;
651
- getState(): CircuitState;
652
- }
653
-
654
939
  //#endregion
655
940
  //#region src/logger.d.ts
941
+ //# sourceMappingURL=client.d.ts.map
656
942
  interface Logger {
657
943
  debug(message: string): void;
658
944
  warn(message: string): void;
@@ -671,11 +957,79 @@ declare class ConsoleLogger implements Logger {
671
957
  }
672
958
 
673
959
  //#endregion
674
- //#region src/types/options.d.ts
960
+ //#region src/types/events.d.ts
675
961
  //# sourceMappingURL=logger.d.ts.map
962
+ /**
963
+ * Reports what happened during a call. Fire and forget, mirroring
964
+ * `onUsage`: the return value is never read and a throwing handler cannot
965
+ * change what the call does, only what gets reported about it.
966
+ */
967
+ type VernLLMEvent = {
968
+ kind: 'retry';
969
+ requestId: string;
970
+ provider: string;
971
+ /** The model actually resolved for this call (honors a per-call `model` override). */
972
+ model: string;
973
+ /** The 1-based retry ordinal (the 1st retry is `1`, not the overall attempt count). */
974
+ attempt: number;
975
+ maxRetries: number;
976
+ delayMs: number;
977
+ retryAfterHonored: boolean;
978
+ error: LLMError;
979
+ } | {
980
+ kind: 'circuit_state';
981
+ provider: string;
982
+ /**
983
+ * The model of the call that triggered this specific transition
984
+ * (whatever was passed to the `assertClosed`/`recordSuccess`/
985
+ * `recordFailure` call that caused it), not a property of the
986
+ * circuit itself: the breaker still counts failures across every
987
+ * model together, so a threshold crossing can be the sum of
988
+ * several different models' failures even though only the
989
+ * triggering call's `model` is reported here.
990
+ */
991
+ model: string;
992
+ from: CircuitState;
993
+ to: CircuitState;
994
+ consecutiveFailures: number;
995
+ } | {
996
+ kind: 'fallback';
997
+ requestId: string;
998
+ /** Provider name of the target that just failed. */
999
+ from: string;
1000
+ /** Provider name of the target about to be tried next. */
1001
+ to: string;
1002
+ /** `-1` for the primary target, otherwise the index into `fallback`. */
1003
+ fromIndex: number;
1004
+ toIndex: number;
1005
+ /** The normalized error that caused `from` to be abandoned. */
1006
+ error: LLMError;
1007
+ /** Time spent on `from`, including its own retries, before giving up. */
1008
+ elapsedMs: number;
1009
+ } | {
1010
+ kind: 'rate_limited';
1011
+ requestId: string;
1012
+ provider: string;
1013
+ /** The model actually resolved for this call (honors a per-call `model` override). */
1014
+ model: string;
1015
+ /** How long this attempt sat queued for capacity before it was let through. */
1016
+ waitedMs: number;
1017
+ /** Which configured bucket was blocking this attempt just before it cleared. */
1018
+ reason: 'concurrency' | 'rpm' | 'tpm';
1019
+ };
1020
+ type OnEvent = (event: VernLLMEvent) => void;
1021
+
1022
+ //#endregion
1023
+ //#region src/types/options.d.ts
1024
+ //# sourceMappingURL=events.d.ts.map
676
1025
  interface VernLLMOptions {
677
1026
  client: LLMClient;
678
1027
  model: string;
1028
+ /**
1029
+ * Label for this provider in usage (`TokenUsage.provider`) and events.
1030
+ * Default `'primary'`.
1031
+ */
1032
+ name?: string;
679
1033
  /** Max retries after the first attempt. Default 1 (2 attempts total) */
680
1034
  maxRetries?: number;
681
1035
  /** Per-attempt timeout in ms. Default 25000 */
@@ -700,9 +1054,28 @@ interface VernLLMOptions {
700
1054
  * request entirely, so the provider applies its own default instead.
701
1055
  */
702
1056
  defaultTemperature?: number | null;
703
- /** Enables debug logging of raw model output (logs up to 800 chars of each
704
- * response). Off by default */
1057
+ /**
1058
+ * Enables debug logging of raw model output (logs up to 800 chars of each
1059
+ * response) and provider errors. Off by default. Only controls the
1060
+ * default `ConsoleLogger`: when a custom `logger` is supplied instead,
1061
+ * that logger's own `debug()` implementation decides whether messages
1062
+ * are emitted, and this option has no effect on it.
1063
+ */
705
1064
  debug?: boolean;
1065
+ /**
1066
+ * Applied before every internal `logger.debug()` call: the raw output
1067
+ * logged on success, and the provider error logged on a failed call or
1068
+ * a failed stream open. This is the one piece of logging an app can't
1069
+ * intercept itself, since it's a direct call into `logger.debug`
1070
+ * rather than something routed through `onEvent`/`onUsage`; anything
1071
+ * caught elsewhere (events, `LLMError.cause`) already passes through
1072
+ * the app's own callback and can be redacted there instead. Runs
1073
+ * before `logger.debug()` regardless of whether that call ends up
1074
+ * emitting anything, so with a custom `logger`, `redact` still applies
1075
+ * even without `debug: true`; see `debug` for why. Default: identity
1076
+ * (no redaction).
1077
+ */
1078
+ redact?: (text: string) => string;
706
1079
  /** Cache adapter for cachedCall. Defaults to an in-memory adapter */
707
1080
  cache?: CacheAdapter;
708
1081
  /** HTTP status codes that should fail fast without retrying. Default [400, 401, 403, 404, 422] */
@@ -731,6 +1104,41 @@ interface VernLLMOptions {
731
1104
  * Pass `true` for defaults, or an options object to tune threshold/cooldown
732
1105
  */
733
1106
  circuitBreaker?: boolean | CircuitBreakerOptions;
1107
+ /**
1108
+ * Reports retries and circuit-breaker state transitions as they happen.
1109
+ * Fire and forget: a throwing handler is caught and logged, and its
1110
+ * return value is never read, so it cannot influence the call.
1111
+ */
1112
+ onEvent?: OnEvent;
1113
+ /**
1114
+ * Client-side rate limiting. Queues calls locally to stay under the
1115
+ * configured requests/tokens-per-minute or concurrency caps, instead of
1116
+ * letting the provider reject them. Independent of the `Retry-After`
1117
+ * handling already applied to a provider 429: this avoids tripping the
1118
+ * limit in the first place. Omit for unlimited (the default).
1119
+ */
1120
+ rateLimit?: RateLimitOptions;
1121
+ /**
1122
+ * Ordered targets tried after the primary, in order, once it (and its
1123
+ * own retries) is exhausted or abandoned. Order is the policy: VernLLM
1124
+ * never reorders, scores, or selects between targets. Each target keeps
1125
+ * its own retry state, circuit breaker, and rate limiter, independent
1126
+ * of every other target's. A single `FallbackTarget` is equivalent to
1127
+ * `[target]`.
1128
+ */
1129
+ fallback?: FallbackTarget | FallbackTarget[];
1130
+ /**
1131
+ * Decides what happens after a target fails: `'next'` to move on to
1132
+ * the following target (or throw, if it was the last one), `'stop'` to
1133
+ * give up immediately without trying any remaining targets. Called
1134
+ * once per failed target, after that target's own retries are
1135
+ * exhausted or abandoned early, so `'retry'` is never a valid return
1136
+ * here. Defaults to `defaultFallbackOn`, which stops on
1137
+ * parse/validation/aborted/quota errors and on tool-contract failures
1138
+ * (the model ignoring the request, not the provider being unhealthy),
1139
+ * and moves on for everything else.
1140
+ */
1141
+ fallbackOn?: FallbackOn;
734
1142
  }
735
1143
 
736
1144
  //#endregion
@@ -745,22 +1153,25 @@ interface VernLLMOptions {
745
1153
  * beyond sensible defaults.
746
1154
  */
747
1155
  declare class VernLLM {
748
- private readonly client;
749
- private readonly model;
750
- private readonly maxRetries;
751
- private readonly timeoutMs;
752
- private readonly chunkIdleTimeoutMs;
753
- private readonly baseDelayMs;
754
- private readonly defaultMaxTokens;
755
- private readonly defaultTemperature;
756
- private readonly cache;
757
- private readonly nonRetryableStatus;
758
- private readonly inFlight;
759
- private readonly parseJson;
760
- private readonly onUsage?;
761
- private readonly onUsageFailure?;
762
1156
  private readonly logger;
763
- private readonly breaker?;
1157
+ /**
1158
+ * One `CallExecutor` per provider target: index 0 is the primary,
1159
+ * everything after it is a `fallback` target, in the order declared.
1160
+ * Each owns its own request building, retry/timeout, circuit breaker,
1161
+ * and rate limiter. `call()` walks this array in `runFallbackChain`,
1162
+ * moving to the next entry only when `fallbackOn` says to.
1163
+ */
1164
+ private readonly executors;
1165
+ /** Decides whether a failed target is followed by the next one or the chain stops. See `VernLLMOptions['fallbackOn']`. */
1166
+ private readonly fallbackOn;
1167
+ /** Reports a `'fallback'` event when the chain moves to the next target. Shared `onEvent` plumbing, same as every executor's. */
1168
+ private readonly reportEvent;
1169
+ /**
1170
+ * Owns cache key resolution, cache reads/writes, and in-flight
1171
+ * coalescing for `cachedCall()`. Independent of `executor`: it only
1172
+ * ever calls back into `this.call()` as an opaque function.
1173
+ */
1174
+ private readonly cacheOrchestrator;
764
1175
  /**
765
1176
  * @param options Client, model, and tunables. Defaults: `maxRetries` 1,
766
1177
  * `timeoutMs` 25000, `baseDelayMs` 500, `defaultMaxTokens` 1000,
@@ -768,8 +1179,23 @@ declare class VernLLM {
768
1179
  * `nonRetryableStatus` `[400, 401, 403, 404, 422]`, `debug` false.
769
1180
  */
770
1181
  constructor(options: VernLLMOptions);
771
- /** Resolves a cache key through the adapter when it supports normalization. */
772
- private resolveCacheKey;
1182
+ /** Logs a failed refundUsage attempt via the configured logger. */
1183
+ private logRefundError;
1184
+ /**
1185
+ * Walks `this.executors` in order, running `attempt` against each until
1186
+ * one succeeds or every target has failed. `run` on a lone target
1187
+ * (no `fallback` configured) throws exactly what it throws today: the
1188
+ * loop's single iteration path is unchanged from pre-fallback behavior.
1189
+ *
1190
+ * For streaming, `attempt` is `executor.runStream`, whose own retries
1191
+ * only cover *opening* the stream (see `CallExecutor.runStream`). A
1192
+ * mid-stream failure surfaces through `finalResult` after this function
1193
+ * has already returned, so it's never seen here and never falls over,
1194
+ * per the streaming limitation: splicing a second model's output into a
1195
+ * response the consumer has already partially rendered would corrupt
1196
+ * it.
1197
+ */
1198
+ private runFallbackChain;
773
1199
  /**
774
1200
  * Makes a single logical LLM call, retrying on failure per the configured
775
1201
  * policy. Fails fast if the breaker is open or the signal is already
@@ -807,121 +1233,12 @@ declare class VernLLM {
807
1233
  call<T = unknown>(params: ToolEnabledCallParams<T>): Promise<CallWithToolsResult<T>>;
808
1234
  call<T = unknown>(params: CallParams<T>): Promise<T>;
809
1235
  /**
810
- * Performs a single attempt: builds the request (translating `tools` to
811
- * wire shape when present), dispatches it with a timeout, and shapes the
812
- * response into `T` or a `CallWithToolsResult<T>` when `params.tools` was
813
- * set. Throws on an empty response (no text and no tool_calls) so the
814
- * retry loop treats it like any other transient failure.
1236
+ * Thin delegator kept private on `VernLLM` (rather than only existing on
1237
+ * `CacheOrchestrator`) since it's the one caching primitive exercised
1238
+ * directly by white-box tests, independent of the public `cachedCall()`
1239
+ * surface.
815
1240
  */
816
- private executeCall;
817
- /**
818
- * Shapes a fully-arrived response (content and/or tool_calls, already
819
- * extracted from the provider's payload) into `T` or a
820
- * `CallWithToolsResult<T>`. Reused by the streaming path once it has
821
- * buffered the full text/tool-call deltas, so there's no separate
822
- * parsing/validation logic for streaming.
823
- *
824
- * Normalizes and reports usage failure on error itself, so every caller
825
- * gets identical error handling without duplicating it.
826
- */
827
- private finalizeResponse;
828
- /**
829
- * Opens a stream for a single attempt: builds the request exactly like
830
- * `executeCall`, then requires `createStream` on the client (a clear
831
- * `validation` error if the adapter doesn't support it). The timeout
832
- * wraps stream construction and the first `.next()` together, not just
833
- * construction: calling an `async function*` returns an iterator
834
- * synchronously without running its body until `.next()` is first
835
- * invoked, so timing only construction would time an operation that's
836
- * always instant, not the actual connection. Both are folded into a
837
- * single `withTimeout` so the same abort signal reaches whatever the
838
- * adapter's `createStream` uses internally for its first network
839
- * round-trip.
840
- *
841
- * Circuit-breaker success is recorded once the stream fully completes,
842
- * not on the first chunk arriving, so a connection that opens but then
843
- * dies mid-stream isn't masked as a success (see `buildStreamResult`).
844
- */
845
- private executeStreamCall;
846
- /**
847
- * The streaming accumulator: wraps the raw `WireStreamChunk` iterator in
848
- * an async generator that yields translated `StreamChunk`s to the caller
849
- * live, as they arrive, with no per-chunk timeout and no bound on total
850
- * duration, and accumulates text/tool-call deltas internally so that
851
- * `finalizeResponse` can produce `finalResult` once the stream completes.
852
- *
853
- * Two separate try/catches: the iteration loop's catch handles errors
854
- * the transport itself throws, which aren't normalized yet, so that
855
- * happens here along with the one `reportUsageFailure` call for them.
856
- * The second catch, around `finalizeResponse`, does not re-normalize or
857
- * re-report since `finalizeResponse` already does both internally.
858
- * Circuit-breaker success is only recorded once the stream fully
859
- * completes, not when the first chunk arrives, so a connection that
860
- * opens and then dies mid-way still counts as a failure below instead
861
- * of masking it.
862
- */
863
- private buildStreamResult;
864
- /**
865
- * Checks every `ToolCall` against the `tools` that were offered, catching
866
- * a hallucinated tool name early instead of letting it reach the
867
- * application's dispatch table. Then runs each tool's `argumentsSchema`,
868
- * if present, throwing `LLMError('validation')` on failure.
869
- */
870
- private validateToolCallArguments;
871
- /** Runs `fn`, retrying with backoff according to `shouldRetry`. */
872
- private retryWithBackoff;
873
- /**
874
- * Validates `history` alternates user/assistant turns, since providers
875
- * like Anthropic/Gemini reject or mishandle consecutive same-role turns.
876
- */
877
- private validateHistory;
878
- /** Applies per-call defaults and shapes params into the client's request object. */
879
- private buildRequestPayload;
880
- /** Maps VernLLM's app-facing `ToolChoice` onto the OpenAI-shaped wire `tool_choice`. */
881
- private buildWireToolChoice;
882
- /**
883
- * Expands one `ConversationTurn` into one or more wire messages. Plain
884
- * user/assistant turns map 1:1. An assistant turn with `toolCalls` maps
885
- * to an assistant message carrying wire-shaped `tool_calls`. A `'tool'`
886
- * turn expands into one wire `tool` message per `toolResult`, since
887
- * OpenAI-shaped wire format wants one message per tool_call_id.
888
- */
889
- private turnToWireMessages;
890
- /**
891
- * Chooses the response format: a provider-native `jsonSchema` takes
892
- * priority when supplied (constrains generation directly), otherwise
893
- * falls back to the looser `json_object` mode when JSON output is
894
- * requested, or no format at all for plain text responses.
895
- */
896
- private buildResponseFormat;
897
- /**
898
- * Pulls `TokenUsage` out of a raw response, if the provider reported it.
899
- * Extraction doesn't depend on what happens to the response afterward, so
900
- * a malformed body can still yield usage if the provider's usage block
901
- * itself came through intact.
902
- */
903
- private extractUsage;
904
- /** Reports token usage for a successful call, swallowing and logging any error `onUsage` throws. */
905
- private reportUsage;
906
- /**
907
- * Reports token usage spent on an attempt that then failed, so it isn't
908
- * dropped alongside the error. Covers any error thrown after usage
909
- * extraction, since all of them happen only after a response (real
910
- * spend) already arrived. Swallows and logs any error `onUsageFailure`
911
- * itself throws.
912
- */
913
- private reportUsageFailure;
914
- /** Parses response content as JSON and validates it against `schema` when supplied. */
915
- private parseAndValidate;
916
- /**
917
- * Waits out the backoff delay for a retry attempt, honoring a
918
- * Retry-After header on the failed attempt's error when present.
919
- * Both Retry-After and plain exponential backoff are capped at the same
920
- * max delay (see `DEFAULT_MAX_DELAY_MS` in `vernLLM.utils.ts`).
921
- */
922
- private recoverDelay;
923
- /** Decides whether a failed attempt is worth retrying. */
924
- private shouldRetry;
1241
+ private runCached;
925
1242
  /**
926
1243
  * Removes a cached response by key when the configured cache adapter
927
1244
  * supports deletion. Cache invalidation is the caller's responsibility;
@@ -931,61 +1248,6 @@ declare class VernLLM {
931
1248
  * `resolveKey`, if any, before deletion).
932
1249
  */
933
1250
  deleteCache(key: string): Promise<void>;
934
- /**
935
- * Internal cache primitive around caller-supplied logic. Concurrent misses
936
- * for the same `cacheKey` share a single in-flight call, avoiding cache
937
- * stampedes.
938
- *
939
- * Not part of the public API. Backs the public `cachedCall()`, which
940
- * always composes this with `call()` so cached results get the same
941
- * retry/timeout/circuit-breaker guarantees as any other LLM call.
942
- *
943
- * @param params `cacheKey`, `ttl`, `fn` (the work to run on a cache
944
- * miss, typically `() => this.call(...)`), and optional
945
- * `reserveUsage`/`refundUsage`/`signal`. See `InternalCacheParams`.
946
- * @returns The cached value on a hit, or the result of `fn()` on a miss.
947
- */
948
- private runCached;
949
- /** Starts the shared fn() call for a cache miss and tracks it in the in-flight map until it settles. */
950
- private registerTrigger;
951
- /** Runs `fn` and writes its result to the cache. */
952
- private runAndCache;
953
- /**
954
- * Streaming counterpart to `runCached`. Three cases:
955
- *
956
- * - Hit: no live generation to relay. Returns immediately with
957
- * `finalResult` resolved to the cached value and a one-shot `chunks`
958
- * replay built from it, so `for await (const c of chunks)` call sites
959
- * work identically on a hit or a miss. No usage hooks fire, since
960
- * nothing was actually spent.
961
- * - Miss, nothing else in flight for this key: delegates to
962
- * `registerStreamTrigger`, which opens the stream and relays its
963
- * `chunks` live.
964
- * - Miss, but another call for the same key is already in flight: this
965
- * call has no live chunks of its own to relay, so it's treated like a
966
- * delayed hit. `finalResult` shares the trigger's in-flight promise
967
- * (the same `this.inFlight` map non-streaming `runCached` uses, so
968
- * streaming and non-streaming `cachedCall`s for the same key coalesce
969
- * against each other too), and `chunks` is a one-shot replay built
970
- * once that promise resolves.
971
- */
972
- private runCachedStream;
973
- /**
974
- * Opens the shared stream for a cache miss and tracks its settled value
975
- * in `this.inFlight` until it resolves or rejects. Writes to the cache
976
- * on success only, matching `runAndCache`.
977
- *
978
- * Registers the in-flight promise synchronously, before anything async
979
- * runs, so a concurrent `cachedCall` for the same key always sees it in
980
- * time to join instead of triggering its own stream. Settlement is
981
- * wired onto the whole `withReservedUsageForStream` call rather than a
982
- * line inside its callback, so any failure point (reserving usage,
983
- * opening the stream, or the stream itself) reliably settles the
984
- * in-flight entry instead of leaving it stuck.
985
- */
986
- private registerStreamTrigger;
987
- /** Logs a failed refundUsage attempt via the configured logger. */
988
- private logRefundError;
989
1251
  /**
990
1252
  * Cache wrapper composing `call` + caching, so cached LLM calls
991
1253
  * automatically get retry/timeout/circuit-breaker behavior. `reserveUsage`/
@@ -1019,14 +1281,30 @@ declare class VernLLM {
1019
1281
  cachedCall<T>(params: CachedToolCallParams<T>): Promise<CallWithToolsResult<T>>;
1020
1282
  cachedCall<T>(params: CachedCallParams<T>): Promise<T>;
1021
1283
  /**
1284
+ * @param model With `circuitBreaker.isolateByModel` on, returns that
1285
+ * model's own circuit state instead of the shared one. Ignored
1286
+ * otherwise. Omit for the shared circuit (the default) or, under
1287
+ * isolation, the state of calls that didn't resolve a model.
1022
1288
  * @returns The current circuit breaker state (`'closed' | 'open' |
1023
1289
  * 'half-open'`), or undefined if no circuit breaker was configured.
1024
1290
  */
1025
- getCircuitState(): ("closed" | "open" | "half-open") | undefined;
1291
+ getCircuitState(model?: string): CircuitState | undefined;
1292
+ /**
1293
+ * @param model With `circuitBreaker.isolateByModel` on, returns each
1294
+ * target's circuit state for that model instead of its shared state.
1295
+ * Ignored otherwise. Omit for the shared circuit (the default) or, under
1296
+ * isolation, the state of calls that didn't resolve a model.
1297
+ * @returns The current circuit state for every target in declaration
1298
+ * order, including the primary and all fallback targets. Each entry
1299
+ * includes the target's provider name, chain index, whether it is a
1300
+ * fallback, and its circuit state, or undefined if that target has no
1301
+ * circuit breaker configured.
1302
+ */
1303
+ getCircuitStates(model?: string): TargetCircuitState[];
1026
1304
  }
1027
1305
 
1028
1306
  //#endregion
1029
- //#region src/internal/sse.d.ts
1307
+ //#region src/adapters/internal/sse.d.ts
1030
1308
  //# sourceMappingURL=vernLLM.d.ts.map
1031
1309
  /**
1032
1310
  * Parses a Server-Sent-Events byte/text stream into the JSON payload of
@@ -1065,7 +1343,7 @@ declare function parseSseStream(source: AsyncIterable<Uint8Array | string>): Asy
1065
1343
  declare const SSE_PING: unique symbol;
1066
1344
 
1067
1345
  //#endregion
1068
- //#region src/internal/imageFormat.d.ts
1346
+ //#region src/adapters/internal/imageFormat.d.ts
1069
1347
  //# sourceMappingURL=sse.d.ts.map
1070
1348
  /**
1071
1349
  * MIME types accepted for `ImageBlock.mimeType` across all adapters. This is
@@ -1077,13 +1355,38 @@ declare const SUPPORTED_IMAGE_MIME_TYPES: readonly ["image/png", "image/jpeg", "
1077
1355
  type SupportedImageMimeType = (typeof SUPPORTED_IMAGE_MIME_TYPES)[number];
1078
1356
 
1079
1357
  //#endregion
1080
- //#region src/adapters/anthropic.d.ts
1358
+ //#region src/adapters/internal/nativeStructuredOutput.d.ts
1081
1359
  /**
1082
1360
  * Validates an `ImageBlock.mimeType` against the shared supported set.
1083
1361
  * Throws a non-retryable `LLMError('validation')`, since an unsupported
1084
1362
  * mimeType is a permanent failure, retrying the same input can't fix it,
1085
1363
  * the same way a schema-validation or JSON-parse failure isn't retried.
1086
1364
  */
1365
+
1366
+ /**
1367
+ * A static allow-list or predicate naming which models support native,
1368
+ * schema-constrained output as its own request field — Anthropic's
1369
+ * `output_config.format`, Bedrock's `outputConfig.textFormat` — separate
1370
+ * from `tools`/`tool_choice`, so it can be combined with real,
1371
+ * caller-supplied `tools` in the same request.
1372
+ *
1373
+ * There is no built-in default list here. Which models support this is
1374
+ * Anthropic's and Bedrock's call to make, not this package's, and it
1375
+ * changes over time; hardcoding a guessed list would risk silently
1376
+ * routing a request onto a field a given model doesn't actually support,
1377
+ * trading a clear `LLMError('validation')` for a confusing error from the
1378
+ * provider instead. So this is opt-in: pass the model IDs you've verified
1379
+ * against the provider's own docs (or a predicate). Left unset, no model
1380
+ * is treated as native-capable, `jsonSchema` keeps using the older
1381
+ * forced-single-tool-call emulation, and `tools` + `jsonSchema` together
1382
+ * is rejected, exactly this package's behavior before native support was
1383
+ * added.
1384
+ */
1385
+ type ModelCapabilityOverride = string[] | ((model: string) => boolean);
1386
+
1387
+ //#endregion
1388
+ //#region src/adapters/anthropic.d.ts
1389
+ /** Resolves whether `model` is covered by a caller-supplied allow-list/predicate. */
1087
1390
  /** Anthropic's native per-block content shape for a message. */
1088
1391
  type AnthropicContentBlock = {
1089
1392
  type: 'text';
@@ -1137,6 +1440,28 @@ interface AnthropicClient {
1137
1440
  type: 'tool';
1138
1441
  name: string;
1139
1442
  };
1443
+ /**
1444
+ * Native, schema-constrained output: a separate request field from
1445
+ * `tools`/`tool_choice`, so it can be sent alongside real tool
1446
+ * calls. Only built by this adapter for models covered by
1447
+ * `nativeStructuredOutputModels` (opt-in, see
1448
+ * `AnthropicAdapterOptions`); other models keep getting
1449
+ * `jsonSchema` emulated as a forced single tool call, the
1450
+ * pre-existing behavior.
1451
+ *
1452
+ * Matches the real Anthropic API's `output_config.format` shape
1453
+ * exactly: just `type` and `schema`, no `name`/`description`/
1454
+ * `strict`. Those three exist on VernLLM's own `jsonSchema` API
1455
+ * (and are still forwarded on the legacy forced-tool-call path,
1456
+ * where they're real `Tool` fields), but the native structured-
1457
+ * output endpoint has no equivalent for any of them.
1458
+ */
1459
+ output_config?: {
1460
+ format: {
1461
+ type: 'json_schema';
1462
+ schema: Record<string, unknown>;
1463
+ };
1464
+ };
1140
1465
  }, options: {
1141
1466
  signal: AbortSignal;
1142
1467
  }): Promise<{
@@ -1154,21 +1479,50 @@ interface AnthropicClient {
1154
1479
  }>;
1155
1480
  };
1156
1481
  }
1482
+ /** Optional configuration for `fromAnthropic`. */
1483
+ interface AnthropicAdapterOptions {
1484
+ /**
1485
+ * Which models support native, schema-constrained output
1486
+ * (`output_config.format`), independent of `tools`/`tool_choice`, so it
1487
+ * can be combined with real `tools` in one request. Pass a static list
1488
+ * of model IDs (verified against Anthropic's own docs) or a predicate.
1489
+ *
1490
+ * There is no built-in default here (see `supportsNativeStructuredOutput`
1491
+ * for why). Left unset, every model uses the older forced-single-tool-
1492
+ * call emulation, and `tools` + `jsonSchema` together is rejected,
1493
+ * exactly this adapter's behavior before native support was added.
1494
+ */
1495
+ nativeStructuredOutputModels?: ModelCapabilityOverride;
1496
+ }
1157
1497
  /**
1158
1498
  * Wraps an Anthropic SDK client so it satisfies the same `LLMClient`
1159
1499
  * interface VernLLM uses for OpenAI/Groq.
1160
1500
  *
1161
- * `response_format: json_schema` is mapped to Anthropic's forced tool-use:
1162
- * a single tool is defined with `input_schema` set to the caller's schema,
1163
- * `description` forwarded when provided, and `strict` forwarded when set.
1164
- * `tool_choice` forces the model to call it. Provider-constrained schema
1165
- * matching applies only when `strict: true` is forwarded and supported.
1501
+ * `response_format: json_schema`, on a model covered by
1502
+ * `options.nativeStructuredOutputModels`, is sent as `output_config.format`,
1503
+ * its own request field, independent of `tools`/`tool_choice`, so it can be
1504
+ * combined with real, caller-supplied `tools` in the same request. Only
1505
+ * `type` and `schema` are sent on this path, the real Anthropic API's
1506
+ * `output_config.format` has no `name`/`description`/`strict` fields.
1507
+ *
1508
+ * On any other model (the default, since `nativeStructuredOutputModels` is
1509
+ * opt-in), `response_format: json_schema` is mapped to Anthropic's forced
1510
+ * tool-use instead: a single tool is defined with `input_schema` set to
1511
+ * the caller's schema, `description` forwarded when provided, and `strict`
1512
+ * forwarded when set, and `tool_choice` forces the model to call it. This
1513
+ * legacy path cannot be combined with real `tools` (both would need the
1514
+ * same `tools`/`tool_choice` field), and a call that tries throws
1515
+ * `LLMError('validation')` before reaching the API. Provider-constrained
1516
+ * schema matching applies only when `strict: true` is forwarded and
1517
+ * supported.
1166
1518
  *
1167
1519
  * `response_format: json_object` (no schema to build a tool from) falls
1168
1520
  * back to a system-prompt instruction, since there's nothing to constrain
1169
- * generation against.
1521
+ * generation against. Unlike `jsonSchema`, this combines with real `tools`
1522
+ * freely on every model: it's a prompt nudge, not a request field, so
1523
+ * there's nothing for it to collide with.
1170
1524
  */
1171
- declare function fromAnthropic(anthropicClient: AnthropicClient): LLMClient;
1525
+ declare function fromAnthropic(anthropicClient: AnthropicClient, options?: AnthropicAdapterOptions): LLMClient;
1172
1526
 
1173
1527
  //#endregion
1174
1528
  //#region src/adapters/gemini.d.ts
@@ -1294,10 +1648,10 @@ interface GeminiClient {
1294
1648
  * Anthropic.
1295
1649
  *
1296
1650
  * `tools` maps to Gemini's native `functionDeclarations`/`functionCall`;
1297
- * `tool_choice` maps to `toolConfig.functionCallingConfig`. `jsonSchema`
1298
- * and `tools` are mutually exclusive by the time a call reaches here
1299
- * (enforced in vernLLM.ts), so `responseSchema` and `tools` never
1300
- * both apply.
1651
+ * `tool_choice` maps to `toolConfig.functionCallingConfig`. Gemini accepts
1652
+ * `responseSchema` and `tools` in the same request natively, so both are
1653
+ * set independently here and no special-casing is needed for the
1654
+ * combination, unlike `fromAnthropic`/`fromBedrock`.
1301
1655
  *
1302
1656
  * `createStream` calls `generateContentStream` (optional on `GeminiClient`
1303
1657
  *, required only if the caller sets `stream: true`) and translates each
@@ -1394,6 +1748,34 @@ interface BedrockConverseClient {
1394
1748
  any: Record<string, never>;
1395
1749
  };
1396
1750
  };
1751
+ /**
1752
+ * Native, schema-constrained output: a separate request field from
1753
+ * `toolConfig`, so it can be sent alongside real tool calls. Only
1754
+ * built by this adapter for models covered by
1755
+ * `nativeStructuredOutputModels` (opt-in, see
1756
+ * `BedrockAdapterOptions`); other models keep getting `jsonSchema`
1757
+ * emulated as a forced single tool call via `toolConfig`, the
1758
+ * pre-existing behavior.
1759
+ *
1760
+ * Matches the real Bedrock Converse API's `outputConfig.textFormat`
1761
+ * shape exactly: the schema itself is nested one level deeper, under
1762
+ * `structure.jsonSchema`, not flat on `textFormat`, and `schema` is
1763
+ * a JSON-encoded *string*, not a parsed object, unlike every other
1764
+ * schema field this adapter builds (`toolSpec.inputSchema.json`
1765
+ * included). There is no `strict` field here, unlike `toolSpec`.
1766
+ */
1767
+ outputConfig?: {
1768
+ textFormat: {
1769
+ type: 'json_schema';
1770
+ structure: {
1771
+ jsonSchema: {
1772
+ schema: string;
1773
+ name?: string;
1774
+ description?: string;
1775
+ };
1776
+ };
1777
+ };
1778
+ };
1397
1779
  }, options: {
1398
1780
  signal: AbortSignal;
1399
1781
  }): Promise<{
@@ -1503,18 +1885,34 @@ type BedrockConverseStreamEvent = {
1503
1885
  */
1504
1886
  interface BedrockAdapterOptions {
1505
1887
  /**
1506
- * Optional preflight check for tool-use support, needed for `jsonSchema`
1507
- * structured output. VernLLM never guesses capability from a failed
1508
- * call's error message (AWS's error text isn't a documented, stable
1509
- * contract), so this is opt-in: pass either a static list of tool-use
1510
- * -capable model IDs, or a predicate function, and VernLLM will reject
1511
- * unsupported models with a clear `LLMError('validation')` *before*
1512
- * dispatching the request, instead of on the wire.
1888
+ * Optional preflight check for tool-use support, needed whenever a
1889
+ * `jsonSchema` call ends up sending Converse `toolConfig` either the
1890
+ * legacy forced-single-tool-call emulation, or real `tools` sent
1891
+ * alongside native structured output (`outputConfig`). VernLLM never
1892
+ * guesses capability from a failed call's error message (AWS's error
1893
+ * text isn't a documented, stable contract), so this is opt-in: pass
1894
+ * either a static list of tool-use-capable model IDs, or a predicate
1895
+ * function, and VernLLM will reject unsupported models with a clear
1896
+ * `LLMError('validation')` *before* dispatching the request, instead of
1897
+ * on the wire.
1513
1898
  *
1514
1899
  * Left unset (default), no preflight check runs, and a `jsonSchema` call
1515
1900
  * to an unsupported model surfaces Bedrock's raw `converse` error as-is.
1516
1901
  */
1517
1902
  toolUseSupportedModels?: string[] | ((modelId: string) => boolean);
1903
+ /**
1904
+ * Which models support native, schema-constrained output
1905
+ * (`outputConfig.textFormat`), independent of `toolConfig`, so it can be
1906
+ * combined with real `tools` in one request. Pass a static list of
1907
+ * model IDs (verified against Bedrock's own docs) or a predicate.
1908
+ *
1909
+ * There is no built-in default here (see `supportsNativeStructuredOutput`
1910
+ * for why). Left unset, every model uses the older forced-single-tool-
1911
+ * call emulation via `toolConfig`, and `tools` + `jsonSchema` together is
1912
+ * rejected, exactly this adapter's behavior before native support was
1913
+ * added.
1914
+ */
1915
+ nativeStructuredOutputModels?: ModelCapabilityOverride;
1518
1916
  }
1519
1917
  /**
1520
1918
  * Wraps a Bedrock Converse-API client so it satisfies the `LLMClient`
@@ -1524,22 +1922,36 @@ interface BedrockAdapterOptions {
1524
1922
  * regardless of which underlying model `modelId` points at, as long as
1525
1923
  * that model supports Converse (most current-generation ones do)
1526
1924
  *
1527
- * `response_format: json_schema` is mapped to Converse's `toolConfig`: a
1528
- * single tool is defined from the schema, description, and strictness settings,
1529
- * and `toolChoice` forces the model to call it. Provider-constrained schema
1530
- * matching applies only when `strict: true` is forwarded and supported.
1531
- * Native tool support varies by model family; pass
1532
- * `toolUseSupportedModels` to preflight-check it (see
1925
+ * `response_format: json_schema`, on a model covered by
1926
+ * `options.nativeStructuredOutputModels` (opt-in, unset by default), is
1927
+ * sent as `outputConfig.textFormat`, its own request field, independent of
1928
+ * `toolConfig`, so it can be combined with real, caller-supplied `tools`
1929
+ * in the same request. Matches the real Converse API's shape exactly: the
1930
+ * schema is nested under `structure.jsonSchema` and JSON-encoded as a
1931
+ * string, not the parsed object `toolConfig`'s tool schemas use, and there
1932
+ * is no `strict` field on this path.
1933
+ *
1934
+ * On any other model (the default), `response_format: json_schema` is
1935
+ * mapped to Converse's `toolConfig` instead: a single tool is defined from
1936
+ * the schema, description, and strictness settings, and `toolChoice`
1937
+ * forces the model to call it. This legacy path cannot be combined with
1938
+ * real `tools` (both would need the same `toolConfig`), and a call that
1939
+ * tries throws `LLMError('validation')` before reaching the API.
1940
+ * Provider-constrained schema matching applies only when `strict: true` is
1941
+ * forwarded and supported. Native tool support varies by model family;
1942
+ * pass `toolUseSupportedModels` to preflight-check it (see
1533
1943
  * `BedrockAdapterOptions`), otherwise a `jsonSchema` call to an
1534
1944
  * unsupported model surfaces Bedrock's raw error unchanged.
1535
1945
  *
1536
1946
  * `response_format: json_object` (no schema to build a tool from) and
1537
1947
  * `reasoning_effort` (no Converse equivalent) fall back to a system-prompt
1538
- * instruction and are dropped respectively.
1948
+ * instruction and are dropped respectively. Unlike `jsonSchema`,
1949
+ * `json_object` combines with real `tools` freely on every model: it's a
1950
+ * prompt nudge, not a request field, so there's nothing for it to collide
1951
+ * with.
1539
1952
  *
1540
- * `tools` maps to Converse's native `toolConfig`/`toolUse`/`toolResult`;
1541
- * `tool_choice` maps to `toolConfig.toolChoice`. Mutually exclusive with
1542
- * `jsonSchema` by the time a call reaches here (enforced in vernLLM.ts).
1953
+ * `tools` alone maps to Converse's native `toolConfig`/`toolUse`/
1954
+ * `toolResult`; `tool_choice` maps to `toolConfig.toolChoice`.
1543
1955
  *
1544
1956
  * `createStream` calls `converseStream` (optional on `BedrockConverseClient`
1545
1957
  *, required only if the caller sets `stream: true`) and translates its
@@ -1865,5 +2277,5 @@ declare const from01AI: typeof fromOpenAICompatible;
1865
2277
  //#endregion
1866
2278
  //# sourceMappingURL=openaiCompatible.d.ts.map
1867
2279
 
1868
- export { AnthropicClient, BedrockConverseClient, CacheAdapter, CachedCallParams, CachedStreamCallParams, CachedStreamToolCallParams, CachedToolCallParams, CallParams, CallWithToolsResult, CircuitBreaker, CircuitBreakerOptions, ConsoleLogger, ContentBlock, ContentResult, ConversationTurn, FetchAdapterConfig, GeminiClient, ImageBlock, InMemoryCacheAdapter, JsonSchemaSpec, LLMClient, LLMError, LLMErrorType, Logger, NormalizedCacheAdapter, OnUsage, RefundUsage, ReserveUsage, SSE_PING, SchemaLike, StreamCallResult, StreamChunk, StreamEnabledCallParams, TextBlock, TieredCacheAdapter, TokenUsage, ToolCall, ToolCallResult, ToolChoice, ToolDefinition, ToolEnabledCallParams, ToolResult, VernLLM, VernLLMOptions, WireMessage, WireStreamChunk, WireToolCall, WireToolChoice, from01AI, fromAnthropic, fromAnyscale, fromAtlasCloud, fromBaseten, fromBedrock, fromCerebras, fromCloudflareWorkersAI, fromDeepInfra, fromDeepSeek, fromFeatherless, fromFetch, fromFireworks, fromFriendli, fromGemini, fromGitHubModels, fromGroq, fromHyperbolic, fromInferenceNet, fromInfermatic, fromKlusterAI, fromLMStudio, fromLambdaLabs, fromLepton, fromMiniMax, fromMistral, fromMoonshot, fromNebius, fromNovita, fromNvidiaNIM, fromOllama, fromOpenAICompatible, fromOpenRouter, fromParasail, fromPerplexity, fromSambaNova, fromSiliconFlow, fromSnowflakeCortex, fromStepFun, fromTogether, fromVLLM, fromVercelAIGateway, fromXAI, fromZhipu, isLLMError, isToolCallResult, parseSseStream };
2280
+ export { AnthropicClient, BedrockConverseClient, CacheAdapter, CachedCallParams, CachedStreamCallParams, CachedStreamToolCallParams, CachedToolCallParams, CallMeta, CallParams, CallWithToolsResult, CircuitBreaker, CircuitBreakerOptions, CircuitState, ConsoleLogger, ContentBlock, ContentResult, ConversationTurn, FallbackAttempt, FallbackExhaustedError, FallbackOn, FallbackTarget, FetchAdapterConfig, GeminiClient, ImageBlock, InMemoryCacheAdapter, JsonSchemaSpec, LLMClient, LLMError, LLMErrorCode, LLMErrorType, Logger, NormalizedCacheAdapter, OnEvent, OnUsage, RateLimitAcquireResult, RateLimitOptions, RateLimitReason, RateLimiter, RefundUsage, ReserveUsage, SSE_PING, SchemaLike, StreamCallResult, StreamChunk, StreamEnabledCallParams, TargetCircuitState, TextBlock, TieredCacheAdapter, TokenUsage, ToolCall, ToolCallResult, ToolChoice, ToolDefinition, ToolEnabledCallParams, ToolIssue, ToolResult, VernLLM, VernLLMEvent, VernLLMOptions, WireMessage, WireRequest, WireStreamChunk, WireToolCall, WireToolChoice, defaultEstimateTokens, defaultFallbackOn, from01AI, fromAnthropic, fromAnyscale, fromAtlasCloud, fromBaseten, fromBedrock, fromCerebras, fromCloudflareWorkersAI, fromDeepInfra, fromDeepSeek, fromFeatherless, fromFetch, fromFireworks, fromFriendli, fromGemini, fromGitHubModels, fromGroq, fromHyperbolic, fromInferenceNet, fromInfermatic, fromKlusterAI, fromLMStudio, fromLambdaLabs, fromLepton, fromMiniMax, fromMistral, fromMoonshot, fromNebius, fromNovita, fromNvidiaNIM, fromOllama, fromOpenAICompatible, fromOpenRouter, fromParasail, fromPerplexity, fromSambaNova, fromSiliconFlow, fromSnowflakeCortex, fromStepFun, fromTogether, fromVLLM, fromVercelAIGateway, fromXAI, fromZhipu, isLLMError, isToolCallResult, parseSseStream };
1869
2281
  //# sourceMappingURL=index.d.cts.map