Repository navigation
Releases: LakBud/vernLLM
Release list
vern-llm@3.0.0
3.0.0
Major Changes
-
9121676: Stopping a stream early cancels it, tool arguments come back as the
argumentsSchemaoutput, and invalid rate limits throw.Streaming: leaving a
for awaitoverchunksbefore the end, bybreak,return, a throw in the loop body, orreturn(), cancels the provider stream.finalResultrejects withLLMError('aborted')and the rate limiter slot is freed. A stream that already finished is left alone. On acachedCall()miss, only that caller leaves: the shared stream keeps running, and is cached, while another caller waits on it.Tools:
ToolCall.argumentsis now theargumentsSchemaoutput, so defaults, coercion and transforms apply. Tools without a schema are unchanged.Rate limiting:
RateLimiterthrowsLLMError('invalid_params')at construction for a negative,NaNorInfinitylimit, arequestsPerMinuteortokensPerMinutebetween 0 and 1, a fractionalmaxConcurrentormaxQueueSize, amaxQueueMspast2147483647, a badaimd.proactiveFloor, andaimdwithoutrequestsPerMinute.0still means unlimited. -
df1d501: Adapters import from
vern-llm/adapters,fromGeminitakes the top level client, andfromBedrockmoves to its own package,vern-llm-bedrock.Adapters: every
from*factory, their option types,ModelCapabilityOverride,parseSseStreamandSSE_PINGmoved off the root entry to the newvern-llm/adapterssubpath.LLMClient, the wire types and everything else stay onvern-llm. Both entries share oneLLMErrorclass, soinstanceofchecks keep working. The rules adapter packages share with the built-in adapters are exported there too:supportsNativeStructuredOutput,rejectsForcedToolChoice,assertForcedToolChoiceSupported,throwMissingForcedJsonSchemaTool,assertForcedJsonSchemaToolInputIsObject,assertSupportedImageMimeType, the Claude reasoning budget helpers, andplanClaudeStructuredOutputandresolveClaudeThinking, which apply Claude's structured output and thinking rules to a request.Gemini: pass
ai, theGoogleGenAIinstance.fromGemini(ai.models)throws a plainErrorat construction that says to passai, andGeminiClientnow describes the top level client, with the model methods undermodels.Bedrock:
fromBedrocknow ships invern-llm-bedrock. Install it with@aws-sdk/client-bedrock-runtime, importfromBedrockfrom it, and pass aBedrockRuntimeClient. The hand-written{ converse, converseStream }client form andBedrockConverseClientare gone.vern-llmno longer loads the AWS SDK at all. -
5ec7a11:
promptTokensis now every input token the provider processed, cache reads and writes included, andTokenUsagereports the cache split.Usage:
fromAnthropicaddscache_read_input_tokenstopromptTokensandtotalTokens, on top of the cache writes it already counted. Streams take the input counts from the finalmessage_deltawhen it sends them, since they are cumulative totals, as Anthropic's own SDK does. OpenAI compatible adapters andfromGeminialready worked this way, andfromFetchreports whatmapResponsereturns.TokenUsagegainscacheReadTokens,cacheWriteTokensandcacheWriteTokensByTtl, each a subset ofpromptTokensandundefinedwhen the provider doesn't report it. The wire usage gainsprompt_tokens_detailswithcached_tokens,cache_write_tokensandcache_write_tokens_by_ttl.fromOpenAICompatiblefillscached_tokensfrom DeepSeek'sprompt_cache_hit_tokens, andfromFetch'smapResponsecan return the three cache counts.Rate limiting:
LLMClientgainscacheReadsCountTowardRateLimit, defaulttrue.fromAnthropicsets it tofalse, since Anthropic's input limit skips cache reads, so the token rate limiter reconciles against the total lesscacheReadTokensand its numbers don't change.// Before: cache reads were left out of promptTokens on Anthropic const total = usage.promptTokens + (usage.cacheReadTokens ?? 0); // After: promptTokens already includes them const total = usage.promptTokens;
Existing code keeps compiling. At runtime,
promptTokensandtotalTokensare higher on Anthropic calls that read the prompt cache, inonUsage, the'usage'event and OpenTelemetry'sgen_ai.usage.input_tokens. Code that added cache reads topromptTokensby hand now counts them twice. See Migration Notes.
Minor Changes
-
d02c2a7: Caching, rate limiting, retry, fallback, middleware, and streaming fixes, plus a new
response_truncatederror code.Caching:
NormalizedCacheAdapterno longer strips punctuation or case, so"2+2"and"2-2"no longer share an answer. It only normalizes Unicode composition, line endings, and outer whitespace.InMemoryCacheAdaptercopies values on write and read, and doesn't cache a value it can't copy faithfully (a class instance or function anywhere inside it). A missing, NaN, zero, or negativettlstores nothing instead of an entry that never expires.cachedCallwarns whenttlis missing or NaN and skips the write.TieredCacheAdapterno longer serves a value after L2 has expired it.Rate limiting: the default token estimate counts each image as 1,600 tokens instead of reading its base64 as text. AIMD shrinks at most once per 60 seconds. A blank rate limit header is treated as missing, and all zero usage keeps the estimate. An error thrown by a limiter's
estimateoracquirenever counts toward the circuit breaker.Retries: a negative or NaN
maxRetriesmeans one attempt instead of none, with a warning. Output cut off atmax_tokensthat fails to parse throwsLLMError('parse')with the new coderesponse_truncated, which is retried and never counts toward the breaker.fromAnthropic,fromGemini, andfromBedrockreport when they stopped atmax_tokens. Custom clients can setfinish_reason: 'length'on a choice.Fallback: a per call
modeloverride applies to the primary target only. Fallback targets run their ownmodel, and every report of the model names the one each target ran.Middleware:
ctx.ownpersists for one middleware across all its hooks within a logical call. An unnamed middleware is labeled[i]everywhere, includingctx.registeredMiddlewareNames, and anamethat matches another entry's[i]label throws at construction. The response body in afromFetcherror passes throughredact. A circuit transition with no call behind it reaches middlewareonEvent.Streaming: a stream that fails before its first content chunk is retried and falls back, even after a keep-alive ping opened it.
call()still resolves on the first ping, so a long thinking phase stays outsidetimeoutMs.chunksandfinalResultfollow whichever attempt produces content, andmeta.currentis updated in place if a different target answers.Existing code keeps compiling unless it has an exhaustive
switchoverLLMErrorCode, which needs a case forresponse_truncated. At runtime: prompts that only matched through case or punctuation stop sharing a cache entry, code that matched an unnamed middleware inregisteredMiddlewareNamesby its bare index must use[i], and a failure between a ping and the first content chunk now retries instead of rejectingfinalResult. -
63c976f: A call can seed
ctx.statebefore any middleware runs, with the newstateparam andstateEntryhelper.const routeDecisionKey = createStateKey<RouteDecision>('router.decision'); await llm.call({ userContent, state: [stateEntry(routeDecisionKey, decision)] });
stateEntrychecks the value against the key's type. Raw[key, value]pairs also work, and a later entry wins over an earlier one with the same key.createMiddlewareStateBagaccepts the same entries. The seed is never sent to the provider, never reported on events or usage, and not part of thecachedCallcache key. WithcachedCall, each caller'swrapsees that caller'sstate, while the shared request sees the state of the caller that started it. Anything that isn't an array of[key, value]pairs throwsinvalid_paramswith the new codeinvalid_state, before any hook runs.Existing code keeps compiling, and nothing changes at runtime until a call passes
state. -
f3ab557: A call takes a
context, plain JSON that middleware, events and usage carry.await llm.call({ userContent, context: { tenantId: 't1', routing: { only: ['bedrock'] } }, });
Middleware reads it as
ctx.contextin every hook and stage, fallback attempts included. Every event the call reports has it asevent.context, and so does theTokenUsageinonUsageand the'usage'event, so a cost middleware can attribute spend to a tenant. It is a frozen copy made when the call starts, is never sent to the provider, and is not part of thecachedCallcache key. VernLLM defines no keys: whatever is built on it owns its own.Anything that isn't a plain JSON object throws
LLMErrorof typeinvalid_paramswith the new codeinvalid_context, before any hook runs or request is sent.Existing code keeps compiling unless it has an exhaustive
switchoverLLMErrorCode, which needs a case forinvalid_context, or buildsAttemptContext/PreDispatchContextobjects by hand, which needcontext. At runtime nothing changes until a call passescontext. -
f1ce144: Target names must be unique.
VernLLMnow throws at construction when two targets, the primary or a fallback, share a name. Defaults count, so a fallback namedprimarycollides with an unnamed primary. Usage and events identify a target by name, so a shared name merged their data. -
987189c: Middleware dispatch hook, adapter context, a configurable Retry-After cap, an opt-in reader stall timeout, Claude thinking blocks in tool loops, and request fixes for newer ...
vern-llm-redis@0.3.0
0.3.0
Minor Changes
-
76331c9: Circuit breaker and rate limiter fixes for many processes sharing one Redis, a
vern-llm-redis/clientssubpath, and stricter option validation.Circuit breaker:
onStateChangefires once per real change in each process, however many replies, messages or polls carry it, and a stale reply no longer rolls the local state back. A call let through never takes a second trial slot, a burst of rejected calls asks for one slot at a time,releaseTrialonly gives its slot back, and slots held for an abandoned trial are dropped. A running trial call renews its lease, so a slow trial is never handed over mid call.cooldownBackoffuses core's formula, so a cooldown is never shorter thancooldownMsunlessmaxMsis. WithisolateByModel, calls with no model usekeyPrefixitself, apart from a model nameddefault, and the startup scan no longer reads another adapter's keys under a longer prefix.Rate limiter:
maxQueueSizecounts waiters across every process. The AIMD ceiling lives in its own key,{keyPrefix:rpm}:aimd, which survives an idle bucket, starts no higher thanaimd.maxCapacity, and shrinks at most once a minute across every process, like core.Both: a Redis failure in
acquire,prepareorreadStaterejects withLLMError('network')and codeconnection_failed. Several adapters can share one subscriber, anddispose()removes its own listener. Scripts are sent by SHA1 after the first call.RedisClientgains optionalevalshaandRedisSubscriberoptionaloff, both already on ioredis and now on the node-redis wrappers.Requires
vern-llm3.0.0 as a peer. See the Redis migration notes.Breaking:
fromIoredis,fromIoredisSubscriber,fromNodeRedisandfromNodeRedisSubscriberare exported fromvern-llm-redis/clients, no longer from the root entry.redisRateLimitvalidates limits with core's rules and messages, andredisCircuitBreakerthrowsLLMError('invalid_params')for every bad option, including a rolling window mistake that used to throwRangeErrorand athresholdthat is not an integer of at least 1. Calls with no model underisolateByModel, and the AIMD ceiling, start fresh once after upgrading.
Patch Changes
- Updated dependencies: vern-llm@3.0.0
vern-llm-otel@0.2.0
0.2.0
Minor Changes
-
f3ab557: Cache token attributes and custom span events. Needs
vern-llm3.0.0.Attempt spans set
gen_ai.usage.cache_read.input_tokensandgen_ai.usage.cache_write.input_tokenswhen the provider reports them. Both are subsets ofgen_ai.usage.input_tokens, as the GenAI conventions define. The token usage metric still records input and output only.Each event a middleware reports through
ctx.emitbecomes a span event on the call span, named after the event, withvernllm.event.source. The event'sdatais whatever the middleware passed, so it is left out unless you opt in, and no metric is recorded for custom events.otelMiddleware({ customEvents: { data: true, maxLength: 2048 } }); // default: on, no data otelMiddleware({ customEvents: false }); // record none
Existing code keeps compiling. At runtime, spans gain the two cache attributes on calls that use the prompt cache, and a call whose middleware emits events gains span events. Set
customEvents: falseto keep spans as they were. -
82ffcb4: Attempt spans now follow the provider request, and time to first chunk skips keep-alive pings. Needs
vern-llm3.0.0.An attempt span starts when the request is sent, after the rate limiter and every
transform, and is the active span while it runs, so an HTTP client span nests under it. Its duration no longer includes rate limit waiting or later transforms. A call rejected before sending, by an open circuit, a full rate limit queue, a throwingtransform, or a capability the model lacks, gets no attempt span, no duration point, and no count invernllm.total_attempts.Time to first chunk runs from sending the request to the first content chunk. Pings, including those Claude sends while reasoning, no longer count, so reasoning models report a later, accurate value.
gen_ai.provider.nameuses the provider the adapter names before guessing from the model id.providerNamesstill wins.Input capture reads the request as sent, after every
transform, so it is never skipped for ordering andvernllm.content.skipped_reasonis gone. The defaultpriorityis-1000whether or not capture is on.Existing code keeps compiling. At runtime, dashboards see shorter attempt spans, fewer attempt spans, and later time to first chunk values for streams that ping first.
Patch Changes
-
8846fbb: Telemetry fixes.
gen_ai.provider.nameno longer uses the target label. Unmapped targets are inferred from the model id (Claude with an@versionsuffix isgcp.vertex_ai), and_OTHERis used when that fails. Dashboards filtering onprimaryorfallback[0]need updating, or setproviderNames.
Input capture is skipped when any middleware sorts after it, so a later redactor can never be bypassed. The span getsvernllm.content.skipped_reasonand a warning is logged once. Middleware without atransformcounts too, so giveotelMiddlewarethe highest priority when capturing.
maxLengthnow caps the whole attribute, JSON and marker included. Older messages that don't fit are dropped whole, not left as marker only parts. An attribute whose structure alone doesn't fit is left out instead of exceeding the limit.
ThenormalizeModelcache is bounded.
gen_ai.request.temperatureis omitted for Anthropic, and for Claude on Bedrock, when thinking is on, since it is never sent.
An attempt whose closing signal was missed ends withvernllm.attempt.outcomeset tounknowninstead of an error, and records no duration. -
Updated dependencies: vern-llm@3.0.0
vern-llm-bedrock@0.1.0
0.1.0
Minor Changes
-
5ec7a11:
promptTokensnow includes cache reads and writes, and the cache split is reported onTokenUsage.fromBedrockaddscacheReadInputTokensandcacheWriteInputTokensto Converse'sinputTokens, which excludes them. Before,promptTokenspluscompletionTokensfell short of Bedrock'stotalTokenswhenever prompt caching was on. Now they agree, andtotalTokensis still what Bedrock reports.TokenUsagegainscacheReadTokens,cacheWriteTokensandcacheWriteTokensByTtl, the last read fromcacheDetailsand keyed by its TTL label.// Before: inputTokens alone usage.promptTokens; // 10 // After: inputTokens plus cache reads and writes usage.promptTokens; // 10010 usage.cacheReadTokens; // 8000 usage.cacheWriteTokens; // 2000
Existing code keeps compiling. At runtime,
promptTokensis higher on calls that use the prompt cache, inonUsage, the'usage'event and OpenTelemetry'sgen_ai.usage.input_tokens. The token rate limiter is unchanged, since Bedrock's total already included cache tokens. See Migration Notes. -
df1d501: Add
vern-llm-bedrock, the AWS Bedrock Converse adapter forvern-llm, built on the AWS SDK'sBedrockRuntimeClient.@aws-sdk/client-bedrock-runtimeandvern-llm3.0.0 are peer dependencies.import { BedrockRuntimeClient } from '@aws-sdk/client-bedrock-runtime'; import { VernLLM } from 'vern-llm'; import { fromBedrock } from 'vern-llm-bedrock'; const llm = new VernLLM({ client: fromBedrock(new BedrockRuntimeClient({ region: 'us-east-1', maxAttempts: 1 })), model: 'anthropic.claude-sonnet-4-5-20250929-v1:0', });
fromBedrocktakes aBedrockRuntimeClientand sendsConverseCommandandConverseStreamCommandthrough itssend. Requests and responses are typed with the SDK's own types, and stream events the adapter doesn't model, including the SDK's generated$unknownmember, are skipped.It carries everything
fromBedrockdid invern-llm2.x: native structured output, forced tool calls forjsonSchemaelsewhere, theforcedToolChoiceUnsupportedModelsrule for newer Claude models, Claude reasoning kept in history for tool loops, keep-alive pings while reasoning streams, and a local error fortoolChoice: 'none'with tools. The adapter names itselfbedrockwith provideraws.bedrockonLLMClient.adapter.
Patch Changes
-
5a5ff4b: Warn when the AWS SDK retries on its own.
fromBedrocklogs awarnatVernLLMconstruction, once per process, when the client's resolvedmaxAttemptsis above 1, since those retries are hidden from VernLLM's retries, circuit breaker, rate limiter, and events. The client is never modified. PassmaxAttempts: 1to theBedrockRuntimeClientto silence it. An explicitretryStrategyoverridesmaxAttempts, so remove it or configure it for one attempt. The warning reads the attempt count of the SDK's ownStandardRetryStrategyandAdaptiveRetryStrategy, including one you pass in. A fully custom strategy exposes no count, so it can't be checked and gets no warning even if it still retries. -
Updated dependencies: vern-llm@3.0.0
vern-llm@2.9.1
2.9.1
Patch Changes
- 805908b: Fix circuit breaker recovery with the rolling policy: a successful half open trial now resets the tripping window, so old failures no longer reopen the circuit right after recovery. Fix
withTimeoutto race the call against the timer, so a client that ignores the abort signal or throws its own abort error type still produces atimeouterror.
vern-llm-redis@0.2.1
0.2.1
Patch Changes
vern-llm@2.9.0
What's Changed
- feat(event)!: usage by @LakBud in #161
- feat(test): improve coverage to 100% by @LakBud in #164
- fix(bedrock): validate legacy jsonSchema tool calls by @LakBud in #165
- feat(circuit-breaker): add pluggable adapter support by @LakBud in #166
- feat(packages): redis by @LakBud in #167
- fix(vern-llm): release stuck half-open breaker trial by @LakBud in #169
- feat(vern-llm-redis): crash safe shared state for the breaker, limiter and cache by @LakBud in #170
- fix(vern-llm): deliver middleware onEvent synchronously by @LakBud in #175
- feat: vern-llm-otel by @LakBud in #176
- chore(deps): Update aws bedrock provider by @renovate[bot] in #171
- chore(deps): Update dependency @anthropic-ai/sdk to ^0.127.0 by @renovate[bot] in #172
- chore(deps): Update dependency @google/genai to ^2.23.0 by @renovate[bot] in #173
- chore(deps): Update all non-major dependencies by @renovate[bot] in #168
- chore(deps): migrate Renovate config by @renovate[bot] in #177
- chore(deps): Update github actions by @renovate[bot] in #162
- chore: version packages by @github-actions[bot] in #163
Full Changelog: https://github.com/LakBud/vernLLM/compare/vern-llm@2.8.0...vern-llm@2.9.0
vern-llm-redis@0.2.0
What's Changed
- feat(event)!: usage by @LakBud in #161
- feat(test): improve coverage to 100% by @LakBud in #164
- fix(bedrock): validate legacy jsonSchema tool calls by @LakBud in #165
- feat(circuit-breaker): add pluggable adapter support by @LakBud in #166
- feat(packages): redis by @LakBud in #167
- fix(vern-llm): release stuck half-open breaker trial by @LakBud in #169
- feat(vern-llm-redis): crash safe shared state for the breaker, limiter and cache by @LakBud in #170
- fix(vern-llm): deliver middleware onEvent synchronously by @LakBud in #175
- feat: vern-llm-otel by @LakBud in #176
- chore(deps): Update aws bedrock provider by @renovate[bot] in #171
- chore(deps): Update dependency @anthropic-ai/sdk to ^0.127.0 by @renovate[bot] in #172
- chore(deps): Update dependency @google/genai to ^2.23.0 by @renovate[bot] in #173
- chore(deps): Update all non-major dependencies by @renovate[bot] in #168
- chore(deps): migrate Renovate config by @renovate[bot] in #177
- chore(deps): Update github actions by @renovate[bot] in #162
- chore: version packages by @github-actions[bot] in #163
Full Changelog: https://github.com/LakBud/vernLLM/compare/vern-llm@2.8.0...vern-llm-redis@0.2.0
vern-llm-otel@0.1.0
What's Changed
- feat(event)!: usage by @LakBud in #161
- feat(test): improve coverage to 100% by @LakBud in #164
- fix(bedrock): validate legacy jsonSchema tool calls by @LakBud in #165
- feat(circuit-breaker): add pluggable adapter support by @LakBud in #166
- feat(packages): redis by @LakBud in #167
- fix(vern-llm): release stuck half-open breaker trial by @LakBud in #169
- feat(vern-llm-redis): crash safe shared state for the breaker, limiter and cache by @LakBud in #170
- fix(vern-llm): deliver middleware onEvent synchronously by @LakBud in #175
- feat: vern-llm-otel by @LakBud in #176
- chore(deps): Update aws bedrock provider by @renovate[bot] in #171
- chore(deps): Update dependency @anthropic-ai/sdk to ^0.127.0 by @renovate[bot] in #172
- chore(deps): Update dependency @google/genai to ^2.23.0 by @renovate[bot] in #173
- chore(deps): Update all non-major dependencies by @renovate[bot] in #168
- chore(deps): migrate Renovate config by @renovate[bot] in #177
- chore(deps): Update github actions by @renovate[bot] in #162
- chore: version packages by @github-actions[bot] in #163
Full Changelog: https://github.com/LakBud/vernLLM/compare/vern-llm@2.8.0...vern-llm-otel@0.1.0
vern-llm@2.8.0
What's Changed
- feat(detectSoftFailure): usage data by @LakBud in #155
- feat(middleware)!: ref for runsBefore and runsAfter instead of name by @LakBud in #158
- feat(middleware): requireRef by @LakBud in #159
- chore(deps): Update typescript tooling by @renovate[bot] in #147
- chore: version packages by @github-actions[bot] in #156
Full Changelog: https://github.com/LakBud/vernLLM/compare/vern-llm@2.7.0...vern-llm@2.8.0