vern-llm 2.1.0 → 2.2.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/README.md +2 -2
- package/dist/index.cjs +83 -22
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +63 -17
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +63 -17
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +83 -21
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -163,13 +163,14 @@ const defaultFallbackOn = (error) => {
|
|
|
163
163
|
* or `fallbackOn` chose to stop early. Carries each attempt in order so
|
|
164
164
|
* an outage across providers stays debuggable without reproducing it.
|
|
165
165
|
* Extends `LLMError` so `isLLMError` and any `instanceof LLMError` check
|
|
166
|
-
* still passes, inheriting the last failure's `type`
|
|
167
|
-
* type-based handling
|
|
166
|
+
* still passes, inheriting the last failure's `type`/`status`/`retryAfterMs`
|
|
167
|
+
* so existing type-based handling, including reading `retryAfterMs` on an
|
|
168
|
+
* `'api'`-typed error, keeps working on a fallback-exhausted error too.
|
|
168
169
|
*/
|
|
169
170
|
var FallbackExhaustedError = class extends LLMError {
|
|
170
171
|
constructor(attempts) {
|
|
171
172
|
const last = attempts[attempts.length - 1]?.error;
|
|
172
|
-
super(`${attempts.length} provider${attempts.length === 1 ? "" : "s"} attempted and failed: ${attempts.map((a) => `${a.provider}(${a.error.type})`).join(" then ")}`, last?.type ?? "unknown", last?.status, void 0, last,
|
|
173
|
+
super(`${attempts.length} provider${attempts.length === 1 ? "" : "s"} attempted and failed: ${attempts.map((a) => `${a.provider}(${a.error.type})`).join(" then ")}`, last?.type ?? "unknown", last?.status, void 0, last, last?.retryAfterMs, "fallback_exhausted");
|
|
173
174
|
this.attempts = attempts;
|
|
174
175
|
}
|
|
175
176
|
};
|
|
@@ -890,6 +891,52 @@ function extractStatus(err) {
|
|
|
890
891
|
if (typeof error.$metadata?.httpStatusCode === "number") return error.$metadata.httpStatusCode;
|
|
891
892
|
return void 0;
|
|
892
893
|
}
|
|
894
|
+
/**
|
|
895
|
+
* POSIX/libuv error codes libuv (and so Node's `fetch`/undici) attaches to
|
|
896
|
+
* genuine transport-level failures: connection refused, DNS lookup
|
|
897
|
+
* failure, connection reset mid-request, a connect that never completed,
|
|
898
|
+
* DNS server unreachable, broken pipe, or host/network unreachable.
|
|
899
|
+
* Deliberately narrow: only codes that can only mean "the connection
|
|
900
|
+
* itself failed," not anything that could also indicate an application
|
|
901
|
+
* error.
|
|
902
|
+
*/
|
|
903
|
+
const NETWORK_ERROR_CODES = new Set([
|
|
904
|
+
"ECONNREFUSED",
|
|
905
|
+
"ENOTFOUND",
|
|
906
|
+
"ECONNRESET",
|
|
907
|
+
"ETIMEDOUT",
|
|
908
|
+
"EAI_AGAIN",
|
|
909
|
+
"EPIPE",
|
|
910
|
+
"ECONNABORTED",
|
|
911
|
+
"EHOSTUNREACH",
|
|
912
|
+
"ENETUNREACH"
|
|
913
|
+
]);
|
|
914
|
+
/** `fetch`'s own wording for a transport-level failure, across runtimes/browsers. */
|
|
915
|
+
const NETWORK_ERROR_MESSAGES = new Set([
|
|
916
|
+
"fetch failed",
|
|
917
|
+
"failed to fetch",
|
|
918
|
+
"load failed",
|
|
919
|
+
"networkerror when attempting to fetch resource."
|
|
920
|
+
]);
|
|
921
|
+
/**
|
|
922
|
+
* Whether `error` is, with reasonable confidence, a transport-level
|
|
923
|
+
* failure (never reached the provider, as opposed to the provider itself
|
|
924
|
+
* responding with an error) rather than some other unexpected exception.
|
|
925
|
+
* Checked via explicit, well-known signals only, so a genuinely unknown
|
|
926
|
+
* error never gets misclassified as a connection failure just because it
|
|
927
|
+
* also lacked an HTTP status.
|
|
928
|
+
*/
|
|
929
|
+
function isNetworkError(error) {
|
|
930
|
+
if (!error || typeof error !== "object") return false;
|
|
931
|
+
const err = error;
|
|
932
|
+
if (typeof err.code === "string" && NETWORK_ERROR_CODES.has(err.code)) return true;
|
|
933
|
+
if (typeof err.message === "string" && NETWORK_ERROR_MESSAGES.has(err.message.toLowerCase())) return true;
|
|
934
|
+
if (err.cause && typeof err.cause === "object") {
|
|
935
|
+
const cause = err.cause;
|
|
936
|
+
if (typeof cause.code === "string" && NETWORK_ERROR_CODES.has(cause.code)) return true;
|
|
937
|
+
}
|
|
938
|
+
return false;
|
|
939
|
+
}
|
|
893
940
|
function formatSafely(value) {
|
|
894
941
|
try {
|
|
895
942
|
return JSON.stringify(value, null, 2) ?? String(value);
|
|
@@ -920,13 +967,16 @@ function describeError(err) {
|
|
|
920
967
|
function normalizeError(error, signal) {
|
|
921
968
|
if (signal?.aborted) return new LLMError("LLM request aborted", "aborted");
|
|
922
969
|
if (error instanceof LLMError) {
|
|
923
|
-
if (error.
|
|
970
|
+
if (error.code === void 0) {
|
|
971
|
+
if (error.status === 429) error.code = "provider_rate_limited";
|
|
972
|
+
else if (error.status === 401 || error.status === 403) error.code = "invalid_credentials";
|
|
973
|
+
}
|
|
924
974
|
return error;
|
|
925
975
|
}
|
|
926
976
|
const status = extractStatus(error);
|
|
927
977
|
const retryAfterMs = extractRetryAfterMs(error);
|
|
928
|
-
if (status !== void 0) return new LLMError("LLM request failed", "api", status, void 0, error, retryAfterMs, status === 429 ? "provider_rate_limited" : void 0);
|
|
929
|
-
return new LLMError("LLM request failed", "unknown", void 0, void 0, error, retryAfterMs);
|
|
978
|
+
if (status !== void 0) return new LLMError("LLM request failed", "api", status, void 0, error, retryAfterMs, status === 429 ? "provider_rate_limited" : status === 401 || status === 403 ? "invalid_credentials" : void 0);
|
|
979
|
+
return new LLMError("LLM request failed", "unknown", void 0, void 0, error, retryAfterMs, isNetworkError(error) ? "connection_failed" : void 0);
|
|
930
980
|
}
|
|
931
981
|
|
|
932
982
|
//#endregion
|
|
@@ -1459,6 +1509,7 @@ var CallExecutor = class {
|
|
|
1459
1509
|
this.logger.debug(`[VernLLM:${requestId}] output:\n${this.redactedOutput(content, wireToolCalls).slice(0, 800)}`);
|
|
1460
1510
|
if (wireToolCalls?.length) {
|
|
1461
1511
|
if (!params.tools) throw new LLMError("Provider returned tool_calls but no `tools` were sent with this call.", "api");
|
|
1512
|
+
if (params.toolChoice === "none") throw new LLMError("Provider returned tool_calls despite toolChoice: 'none'.", "api", void 0, void 0, void 0, void 0, "tool_choice_none_violated");
|
|
1462
1513
|
const toolCalls = parseWireToolCalls(wireToolCalls);
|
|
1463
1514
|
this.validateToolCallArguments(toolCalls, params.tools);
|
|
1464
1515
|
this.breaker?.recordSuccess(model);
|
|
@@ -1719,7 +1770,7 @@ var CallExecutor = class {
|
|
|
1719
1770
|
await waitForRetry(delay, signal);
|
|
1720
1771
|
}
|
|
1721
1772
|
isNonRetryableToolContractError(error) {
|
|
1722
|
-
return error instanceof LLMError && (error.code === "unknown_tool" || error.code === "duplicate_tool_call_id");
|
|
1773
|
+
return error instanceof LLMError && (error.code === "unknown_tool" || error.code === "duplicate_tool_call_id" || error.code === "tool_choice_none_violated");
|
|
1723
1774
|
}
|
|
1724
1775
|
/** Decides whether a failed attempt is worth retrying. */
|
|
1725
1776
|
shouldRetry(error, signal) {
|
|
@@ -1732,11 +1783,12 @@ var CallExecutor = class {
|
|
|
1732
1783
|
}
|
|
1733
1784
|
/**
|
|
1734
1785
|
* Decides whether a failed attempt should count toward the circuit
|
|
1735
|
-
* breaker's failure threshold. A model hallucinating a tool name
|
|
1736
|
-
* reusing a call id
|
|
1737
|
-
*
|
|
1738
|
-
*
|
|
1739
|
-
*
|
|
1786
|
+
* breaker's failure threshold. A model hallucinating a tool name,
|
|
1787
|
+
* reusing a call id, or a provider ignoring `toolChoice: 'none'` isn't
|
|
1788
|
+
* the provider being unhealthy, it's a model/provider response defect
|
|
1789
|
+
* that will very likely recur regardless of provider health, so it
|
|
1790
|
+
* shouldn't push a healthy provider's circuit toward opening. Mirrors
|
|
1791
|
+
* the same reasoning `shouldRetry` already applies to
|
|
1740
1792
|
* `parse`/`validation`/these same tool-contract codes.
|
|
1741
1793
|
*/
|
|
1742
1794
|
countsTowardBreaker(error) {
|
|
@@ -1799,7 +1851,8 @@ var TokenBucket = class {
|
|
|
1799
1851
|
refill() {
|
|
1800
1852
|
if (this.refillPerMs === 0) return;
|
|
1801
1853
|
const now = Date.now();
|
|
1802
|
-
|
|
1854
|
+
const elapsedMs = now - this.lastRefill;
|
|
1855
|
+
this.available = Math.min(this.capacity, this.available + Math.max(0, elapsedMs) * this.refillPerMs);
|
|
1803
1856
|
this.lastRefill = now;
|
|
1804
1857
|
}
|
|
1805
1858
|
/** Refills, then takes `amount` if available. Leaves the bucket untouched if it can't. */
|
|
@@ -1892,7 +1945,6 @@ var RateLimiter = class {
|
|
|
1892
1945
|
release: this.makeRelease(estimatedTokens),
|
|
1893
1946
|
waitedMs: 0
|
|
1894
1947
|
};
|
|
1895
|
-
if (this.maxQueueSize > 0 && this.queue.length >= this.maxQueueSize) throw this.queueFullError();
|
|
1896
1948
|
return this.enqueue(estimatedTokens, attempt.reason, signal);
|
|
1897
1949
|
}
|
|
1898
1950
|
if (this.maxQueueSize > 0 && this.queue.length >= this.maxQueueSize) throw this.queueFullError();
|
|
@@ -2223,8 +2275,8 @@ var VernLLM = class {
|
|
|
2223
2275
|
}
|
|
2224
2276
|
async cachedCall(params) {
|
|
2225
2277
|
const { call: callParams,...cacheParams } = params;
|
|
2226
|
-
const
|
|
2227
|
-
if (reserveUsage || refundUsage)
|
|
2278
|
+
const restCallParams = callParams;
|
|
2279
|
+
if (restCallParams.reserveUsage || restCallParams.refundUsage) throw new LLMError("`reserveUsage`/`refundUsage` were set inside `call`, where cachedCall ignores them. Move them to the top level of the cachedCall() params, alongside cacheKey/ttl, instead.", "validation");
|
|
2228
2280
|
if (restCallParams.stream) {
|
|
2229
2281
|
const streamParams = restCallParams;
|
|
2230
2282
|
return this.cacheOrchestrator.runCachedStream({
|
|
@@ -3547,6 +3599,20 @@ function fromOpenAICompatible(client, options = {}) {
|
|
|
3547
3599
|
}
|
|
3548
3600
|
} } };
|
|
3549
3601
|
}
|
|
3602
|
+
/**
|
|
3603
|
+
* Named alias for the OpenAI SDK itself. A raw `new OpenAI(...)` instance
|
|
3604
|
+
* structurally matches most of `LLMClient`, but newer `openai` SDK major
|
|
3605
|
+
* versions have widened `ChatCompletionContentPart` (e.g. adding a `file`
|
|
3606
|
+
* variant) in ways that no longer structurally satisfy VernLLM's
|
|
3607
|
+
* provider-agnostic `ContentBlock[]` on `userContent`, so passing the SDK
|
|
3608
|
+
* instance directly can fail to typecheck depending on the installed
|
|
3609
|
+
* `openai` version. Wrapping with `fromOpenAI()` (a plain alias of
|
|
3610
|
+
* `fromOpenAICompatible()`) sidesteps that by translating through
|
|
3611
|
+
* `unknown` at the boundary, and also picks up multimodal image
|
|
3612
|
+
* translation and `createStream` wiring that a raw client doesn't have.
|
|
3613
|
+
* See Migration Notes for details.
|
|
3614
|
+
*/
|
|
3615
|
+
const fromOpenAI = fromOpenAICompatible;
|
|
3550
3616
|
/** Groqs SDK matches the OpenAI wire format */
|
|
3551
3617
|
const fromGroq = fromOpenAICompatible;
|
|
3552
3618
|
/**
|
|
@@ -3604,8 +3670,6 @@ const fromNvidiaNIM = fromOpenAICompatible;
|
|
|
3604
3670
|
const fromVercelAIGateway = fromOpenAICompatible;
|
|
3605
3671
|
/** Cloudflare Workers AI exposes an OpenAI-compatible endpoint */
|
|
3606
3672
|
const fromCloudflareWorkersAI = fromOpenAICompatible;
|
|
3607
|
-
/** GitHub Models is OpenAI-compatible */
|
|
3608
|
-
const fromGitHubModels = fromOpenAICompatible;
|
|
3609
3673
|
/** Nebius AI Studio is OpenAI-compatible */
|
|
3610
3674
|
const fromNebius = fromOpenAICompatible;
|
|
3611
3675
|
/** SambaNova Cloud's API is OpenAI-compatible */
|
|
@@ -3632,8 +3696,6 @@ const fromSnowflakeCortex = fromOpenAICompatible;
|
|
|
3632
3696
|
const fromAnyscale = fromOpenAICompatible;
|
|
3633
3697
|
/** Lepton AI's inference API is OpenAI-compatible */
|
|
3634
3698
|
const fromLepton = fromOpenAICompatible;
|
|
3635
|
-
/** kluster.ai's inference API is OpenAI-compatible */
|
|
3636
|
-
const fromKlusterAI = fromOpenAICompatible;
|
|
3637
3699
|
/** Inference.net's API is OpenAI-compatible */
|
|
3638
3700
|
const fromInferenceNet = fromOpenAICompatible;
|
|
3639
3701
|
/** Infermatic's API is OpenAI-compatible */
|
|
@@ -3644,5 +3706,5 @@ const fromAtlasCloud = fromOpenAICompatible;
|
|
|
3644
3706
|
const from01AI = fromOpenAICompatible;
|
|
3645
3707
|
|
|
3646
3708
|
//#endregion
|
|
3647
|
-
export { CircuitBreaker, ConsoleLogger, FallbackExhaustedError, InMemoryCacheAdapter, LLMError, NormalizedCacheAdapter, RateLimiter, SSE_PING, TieredCacheAdapter, VernLLM, defaultEstimateTokens, defaultFallbackOn, from01AI, fromAnthropic, fromAnyscale, fromAtlasCloud, fromBaseten, fromBedrock, fromCerebras, fromCloudflareWorkersAI, fromDeepInfra, fromDeepSeek, fromFeatherless, fromFetch, fromFireworks, fromFriendli, fromGemini,
|
|
3709
|
+
export { CircuitBreaker, ConsoleLogger, FallbackExhaustedError, InMemoryCacheAdapter, LLMError, NormalizedCacheAdapter, RateLimiter, SSE_PING, TieredCacheAdapter, VernLLM, defaultEstimateTokens, defaultFallbackOn, from01AI, fromAnthropic, fromAnyscale, fromAtlasCloud, fromBaseten, fromBedrock, fromCerebras, fromCloudflareWorkersAI, fromDeepInfra, fromDeepSeek, fromFeatherless, fromFetch, fromFireworks, fromFriendli, fromGemini, fromGroq, fromHyperbolic, fromInferenceNet, fromInfermatic, fromLMStudio, fromLambdaLabs, fromLepton, fromMiniMax, fromMistral, fromMoonshot, fromNebius, fromNovita, fromNvidiaNIM, fromOllama, fromOpenAI, fromOpenAICompatible, fromOpenRouter, fromParasail, fromPerplexity, fromSambaNova, fromSiliconFlow, fromSnowflakeCortex, fromStepFun, fromTogether, fromVLLM, fromVercelAIGateway, fromXAI, fromZhipu, isLLMError, isToolCallResult, parseSseStream };
|
|
3648
3710
|
//# sourceMappingURL=index.js.map
|