Configuration
Every VernLLM constructor option and per call option
Everything below is set once on the VernLLM instance and applies to every call() unless overridden per request. Nothing is required beyond client and model; every other option has a sensible default.
import { InMemoryCacheAdapter } from 'vern-llm';
const llm = new VernLLM({
client: openai,
model: 'gpt-4o',
name: 'primary',
maxRetries: 1,
timeoutMs: 25_000,
chunkIdleTimeoutMs: 30_000,
baseDelayMs: 500,
defaultMaxTokens: 1000,
nonRetryableStatus: [400, 401, 403, 404, 422],
circuitBreaker: true,
rateLimit: { requestsPerMinute: 500, tokensPerMinute: 200_000, maxConcurrent: 20 },
fallback: { client: anthropicClient, model: 'claude-sonnet-5', circuitBreaker: true },
debug: false,
cache: new InMemoryCacheAdapter(),
parseJson: (content) => {
try {
return JSON.parse(content);
} catch {
return undefined;
}
},
onUsage: (usage) => billing.record(usage),
onUsageFailure: (usage, error) => billing.record(usage),
onEvent: (event) => observability.record(event),
logger: myLogger,
});Required
client
An LLMClient, an OpenAI SDK instance, or the result of wrapping another provider with an
adapter (fromAnthropic, fromGemini, fromBedrock, fromFetch, etc). See Adapters.
model
The default model ID for calls that do not override it via call({ model: '...' }).
Provider identity
| Option | Default | Notes |
|---|---|---|
name | 'primary' | Optional application-level label for this VernLLM instance. It is included as provider in usage and observability events. |
name does not affect request routing or the model sent to the provider. It is an application-level
label that lets you distinguish multiple VernLLM instances in shared billing and observability
pipelines.
Retry & timeout
| Option | Default | Notes |
|---|---|---|
maxRetries | 1 | Retries after the first attempt, so 2 attempts total by default. maxRetries: 3 means up to 4 attempts. |
timeoutMs | 25000 | Per-attempt timeout, not a total call timeout. Each retry gets its own fresh timeoutMs window. For stream: true, this bounds opening the stream and its first chunk; see chunkIdleTimeoutMs. |
chunkIdleTimeoutMs | 30000 | stream: true only. Max gap between chunks once the stream has opened. Resets on every chunk, including provider keep-alive pings. See Per-chunk idle timeout. |
baseDelayMs | 500 | Base for exponential backoff between retries. Actual delay grows per attempt with jitter. A Retry-After header overrides the computed delay when present. |
nonRetryableStatus | [400, 401, 403, 404, 422] | HTTP status codes that fail immediately instead of retrying. |
detectSoftFailure | none | (result, meta) => LLMErrorCode | undefined. Reclassifies a response that parsed and validated fine as a failure. undefined leaves it a success. See Soft Failure Detection. |
Request defaults
| Option | Default | Notes |
|---|---|---|
defaultMaxTokens | 1000 | Used for any call that does not set maxTokens itself. |
defaultTemperature | 0.2 | Used for any call that does not set temperature itself. Pass null to omit temperature by default. Claude Opus 4.7 and later, and every Claude 5 tier model (Sonnet 5, Fable 5, Mythos 5), reject a non-default temperature with a 400 error; set this to null for an instance pointed at one of those models. Separately, fromAnthropic/fromBedrock omit temperature automatically whenever budgetTokens/reasoningEffort is set, on any model, since Anthropic rejects temperature alongside any thinking mode. |
defaultReasoningEffort | Not sent | Used for any call that does not set reasoningEffort itself. See Call Params. |
defaultBudgetTokens | Not sent | Used for any call that does not set budgetTokens itself. See Call Params. |
parseJson | defaultParseJson | Custom parser for non-standard JSON responses. Must return undefined or null on failure, either is treated the same way, and must not throw. |
Caching
The cache option controls the adapter used by cachedCall.
By default, VernLLM creates an InMemoryCacheAdapter. This stores cached responses in the current
process memory using a bounded in-memory map. It is useful for local development and single-process
deployments, but entries are not shared between application instances.
For distributed deployments, provide your own adapter backed by shared storage such as Redis or Upstash.
InMemoryCacheAdapter accepts an optional maxSize argument to limit the number of entries kept in
memory. When the limit is reached, it evicts by 'fifo' (default), removing the oldest inserted
entry, or 'lru', removing the least recently used entry. See
Eviction.
Two additional built-in adapters compose on top of any CacheAdapter: NormalizedCacheAdapter
(normalizes keys before matching, including case, whitespace, and punctuation) and
TieredCacheAdapter (checks a fast local adapter first, falling back to a shared one). See Caching for usage.
| Option | Default | Notes |
|---|---|---|
cache | new InMemoryCacheAdapter() | Used only by cachedCall, not plain call(). { maxSize, eviction } configures the built-in adapter (eviction: 'fifo' default or 'lru'), or pass a CacheAdapter directly for a shared backend. See Eviction. |
Observability
| Option | Default | Notes |
|---|---|---|
onUsage | none | Called after every successful call with token counts when the provider returned usage data. The usage object includes provider, which is the name configured on the target. |
onUsageFailure | none | Called when a provider response arrived with usage data but VernLLM's post-processing then failed. Fires once per failed attempt with extractable usage. See Usage Tracking. |
onEvent | none | Receives events for retries, circuit state transitions, rate-limit waits, and fallback transitions. Fire and forget; cannot change the call result or retry behavior. See Observability. |
logger | console-based ConsoleLogger | Implement the Logger interface to route output elsewhere. See Pluggable Logger. |
debug | false | Controls diagnostic logger.debug calls on the default console logger only. With a custom logger, its own debug() decides whether to emit. warn and error logs are not controlled by debug. |
redact | none | Applied before every internal logger.debug() call, output on success and the provider error on failure, regardless of whether that call ends up emitting anything. See Redacting debug output. |
See Observability for the full onEvent union and event
shapes.
Circuit breaker
| Option | Default | Notes |
|---|---|---|
circuitBreaker | disabled | Pass true for defaults (threshold: 5, cooldownMs: 30_000), or { threshold, cooldownMs, isolateByModel, halfOpenProbes, halfOpenSuccessRatio, cooldownBackoff, tripping, onStateChange }. See Circuit Breaker. |
The circuit breaker tracks provider and transport-level health. Response parsing and validation
errors do not count as provider failures. unknown_tool and duplicate_tool_call_id tool-contract
errors also do not count.
With isolateByModel: false (the default), one circuit is shared across models used by the same
target. With isolateByModel: true, each resolved model gets its own circuit and
getCircuitState({ model }) can be used to inspect that model's state. Omitting model reads or
acts on the target's own configured model rather than an unlabeled bucket.
halfOpenProbes (default 1) and halfOpenSuccessRatio (default 1) control how many trial calls
run during the half-open window and what fraction must succeed to close the circuit again.
cooldownBackoff accepts ExponentialBackoffOptions | CooldownBackoff:
ExponentialBackoffOptions:{ multiplier: number; maxMs?: number }. ScalescooldownMsbymultiplieron each repeat open, capped atmaxMs(defaultInfinity). Always applies full jitter.CooldownBackoff:(reopenCount: number, baseCooldownMs: number) => number, an escape hatch for anything beyond exponential growth, or an exact deterministic value. Never jittered automatically.
Omitted (the default), cooldownMs stays fixed.
tripping accepts { kind: 'consecutive', threshold } | { kind: 'rolling', windowMs, minCalls, failureRatio } | TrippingPolicy:
{ kind: 'consecutive', threshold }: the default, matchingthresholdon its own. Opens after that many failures in a row.{ kind: 'rolling', windowMs, minCalls, failureRatio }: opens once at leastminCallscalls have landed in the trailingwindowMsand the failure ratio among them reachesfailureRatio.TrippingPolicy:{ onSuccess(), onFailure(): boolean, reset() }, an escape hatch for anything else. No class required, a plain object satisfying the interface works.
onStateChange, the circuit_state event, and the open-circuit error message always report a true
consecutive-failure count, tracked independently of tripping.
Omitted (the default), tripping is { kind: 'consecutive', threshold }.
See Circuit Breaker for failure conditions, half-open behavior, cooldown backoff, tripping policy, failure attribution, per-model isolation, and state inspection.
Rate limiting
| Option | Default | Notes |
|---|---|---|
rateLimit | unlimited | { requestsPerMinute, tokensPerMinute, maxConcurrent, maxQueueMs, maxQueueSize, estimateTokens }, or a RateLimiterAdapter instance for cross process coordination. Queues calls locally to stay under configured limits. Every field is independent and optional. See Rate Limiting. |
A local rate-limit rejection fails immediately with LLMError('rate_limited') and
code: 'rate_limit_queue_timeout', 'rate_limit_queue_full', or 'rate_limit_capacity_exceeded'.
It is never retried and does not count as a circuit-breaker failure.
A provider 429 carries code: 'provider_rate_limited' and is handled separately from local
rate-limit rejections. See Error codes.
Retry budget
| Option | Default | Notes |
|---|---|---|
retryBudget | disabled | { windowMs, minCalls, retryRatio }. Caps how much of a target's recent traffic is allowed to be retries, independent of circuitBreaker. See Retry Budget. |
Once at least minCalls calls have landed in the trailing windowMs and the retry ratio among
them reaches retryRatio, further retries against that target fail immediately with
LLMError('rate_limited') and code: 'retry_budget_exhausted'. It is never retried and does not
count as a circuit-breaker failure. Like circuitBreaker and rateLimit, a target's own
retryBudget is never inherited: each target needs its own if it wants one.
Fallback
| Option | Default | Notes |
|---|---|---|
fallback | none | A FallbackTarget or FallbackTarget[], tried in declaration order after the primary and after each earlier target's retries are exhausted or abandoned. See Provider Fallback. |
fallbackOn | defaultFallbackOn | (error, { isLastTarget }) => 'next' | 'stop'. Decides whether a failed target is followed by the next one. |
Each FallbackTarget needs its own client and model. name, maxRetries, timeoutMs,
chunkIdleTimeoutMs, baseDelayMs, defaultMaxTokens, defaultTemperature,
nonRetryableStatus, and detectSoftFailure inherit the parent instance's resolved value when
omitted.
circuitBreaker, rateLimit, and retryBudget are never inherited. Each target has its own
independent breaker, rate limiter, and retry budget when configured.
An open target breaker is treated as a target failure and can cause the chain to move to the next
target according to fallbackOn.
When every target fails, call() throws FallbackExhaustedError containing the normalized error
from each attempted target in declaration order. See FallbackExhaustedError.
Per call defaults and overrides
A handful of options are set per call() rather than on the instance. The following options have
their own defaults worth knowing:
| Option | Default | Notes |
|---|---|---|
temperature | 0.2, or the instance's defaultTemperature | Low by design. Override per call as needed. Pass null to omit temperature and use the provider's own default. Claude Opus 4.7 and later, and every Claude 5 tier model, reject a non-default value with a 400 error. |
jsonMode | true, except false if tools is set | Responses are parsed as JSON by default. Set jsonMode: false to get the raw string back. |
systemPrompt | none | Optional. Omitting it skips the system message entirely. |
toolChoice | 'auto' when tools is set | Controls whether and which tool the model must call. See Tool Calling. |
Usage metering
reserveUsage and refundUsage are provided per call, not on the VernLLM instance.
| Option | Default | Notes |
|---|---|---|
reserveUsage | none | Runs before the provider request. If it throws, the call fails with LLMError('quota_exceeded') and no request is sent. |
refundUsage | none | Runs after a successful reservation if the call ultimately fails. It is also called when an already-reserved request is aborted before the wrapped operation starts. |
These hooks are application controlled. VernLLM only manages the reservation lifecycle; it does not decide quota rules or billing policy.
userContent, model, maxTokens, and reasoningEffort have no call-level default beyond
falling back to the corresponding instance-level field, or being undefined when omitted.
schema, jsonSchema, tools, signal, and requestId have no instance-level equivalent; they
are undefined unless set on the call itself.
requestId is an optional identifier you can provide per call:
const result = await llm.call({
userContent: 'Generate a summary',
requestId: `document-summary:${documentId}`,
});Multimodal userContent
ContentBlock is the shape used when userContent is passed as an array instead of a plain string:
interface TextBlock {
type: 'text';
text: string;
}
interface ImageBlock {
type: 'image';
data: string; // base64-encoded bytes, no `data:` prefix
mimeType: string; // e.g. 'image/png', 'image/jpeg', 'image/webp', 'image/gif'
}
type ContentBlock = TextBlock | ImageBlock;Image blocks validate mimeType against VernLLM's supported image set before a provider request
is created. Supported values are image/png, image/jpeg, image/gif, and image/webp.
Unsupported values fail with LLMError('validation').