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/README.md
CHANGED
|
@@ -36,10 +36,10 @@ pnpm add vern-llm
|
|
|
36
36
|
```ts
|
|
37
37
|
import Anthropic from '@anthropic-ai/sdk';
|
|
38
38
|
import OpenAI from 'openai';
|
|
39
|
-
import { fromAnthropic, VernLLM } from 'vern-llm';
|
|
39
|
+
import { fromAnthropic, fromOpenAI, VernLLM } from 'vern-llm';
|
|
40
40
|
|
|
41
41
|
const llm = new VernLLM({
|
|
42
|
-
client: new OpenAI({ apiKey: process.env.OPENAI_API_KEY }),
|
|
42
|
+
client: fromOpenAI(new OpenAI({ apiKey: process.env.OPENAI_API_KEY })),
|
|
43
43
|
model: 'gpt-4o',
|
|
44
44
|
maxRetries: 3,
|
|
45
45
|
timeoutMs: 10_000,
|
package/dist/index.cjs
CHANGED
|
@@ -187,13 +187,14 @@ const defaultFallbackOn = (error) => {
|
|
|
187
187
|
* or `fallbackOn` chose to stop early. Carries each attempt in order so
|
|
188
188
|
* an outage across providers stays debuggable without reproducing it.
|
|
189
189
|
* Extends `LLMError` so `isLLMError` and any `instanceof LLMError` check
|
|
190
|
-
* still passes, inheriting the last failure's `type`
|
|
191
|
-
* type-based handling
|
|
190
|
+
* still passes, inheriting the last failure's `type`/`status`/`retryAfterMs`
|
|
191
|
+
* so existing type-based handling, including reading `retryAfterMs` on an
|
|
192
|
+
* `'api'`-typed error, keeps working on a fallback-exhausted error too.
|
|
192
193
|
*/
|
|
193
194
|
var FallbackExhaustedError = class extends LLMError {
|
|
194
195
|
constructor(attempts) {
|
|
195
196
|
const last = attempts[attempts.length - 1]?.error;
|
|
196
|
-
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,
|
|
197
|
+
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");
|
|
197
198
|
this.attempts = attempts;
|
|
198
199
|
}
|
|
199
200
|
};
|
|
@@ -914,6 +915,52 @@ function extractStatus(err) {
|
|
|
914
915
|
if (typeof error.$metadata?.httpStatusCode === "number") return error.$metadata.httpStatusCode;
|
|
915
916
|
return void 0;
|
|
916
917
|
}
|
|
918
|
+
/**
|
|
919
|
+
* POSIX/libuv error codes libuv (and so Node's `fetch`/undici) attaches to
|
|
920
|
+
* genuine transport-level failures: connection refused, DNS lookup
|
|
921
|
+
* failure, connection reset mid-request, a connect that never completed,
|
|
922
|
+
* DNS server unreachable, broken pipe, or host/network unreachable.
|
|
923
|
+
* Deliberately narrow: only codes that can only mean "the connection
|
|
924
|
+
* itself failed," not anything that could also indicate an application
|
|
925
|
+
* error.
|
|
926
|
+
*/
|
|
927
|
+
const NETWORK_ERROR_CODES = new Set([
|
|
928
|
+
"ECONNREFUSED",
|
|
929
|
+
"ENOTFOUND",
|
|
930
|
+
"ECONNRESET",
|
|
931
|
+
"ETIMEDOUT",
|
|
932
|
+
"EAI_AGAIN",
|
|
933
|
+
"EPIPE",
|
|
934
|
+
"ECONNABORTED",
|
|
935
|
+
"EHOSTUNREACH",
|
|
936
|
+
"ENETUNREACH"
|
|
937
|
+
]);
|
|
938
|
+
/** `fetch`'s own wording for a transport-level failure, across runtimes/browsers. */
|
|
939
|
+
const NETWORK_ERROR_MESSAGES = new Set([
|
|
940
|
+
"fetch failed",
|
|
941
|
+
"failed to fetch",
|
|
942
|
+
"load failed",
|
|
943
|
+
"networkerror when attempting to fetch resource."
|
|
944
|
+
]);
|
|
945
|
+
/**
|
|
946
|
+
* Whether `error` is, with reasonable confidence, a transport-level
|
|
947
|
+
* failure (never reached the provider, as opposed to the provider itself
|
|
948
|
+
* responding with an error) rather than some other unexpected exception.
|
|
949
|
+
* Checked via explicit, well-known signals only, so a genuinely unknown
|
|
950
|
+
* error never gets misclassified as a connection failure just because it
|
|
951
|
+
* also lacked an HTTP status.
|
|
952
|
+
*/
|
|
953
|
+
function isNetworkError(error) {
|
|
954
|
+
if (!error || typeof error !== "object") return false;
|
|
955
|
+
const err = error;
|
|
956
|
+
if (typeof err.code === "string" && NETWORK_ERROR_CODES.has(err.code)) return true;
|
|
957
|
+
if (typeof err.message === "string" && NETWORK_ERROR_MESSAGES.has(err.message.toLowerCase())) return true;
|
|
958
|
+
if (err.cause && typeof err.cause === "object") {
|
|
959
|
+
const cause = err.cause;
|
|
960
|
+
if (typeof cause.code === "string" && NETWORK_ERROR_CODES.has(cause.code)) return true;
|
|
961
|
+
}
|
|
962
|
+
return false;
|
|
963
|
+
}
|
|
917
964
|
function formatSafely(value) {
|
|
918
965
|
try {
|
|
919
966
|
return JSON.stringify(value, null, 2) ?? String(value);
|
|
@@ -944,13 +991,16 @@ function describeError(err) {
|
|
|
944
991
|
function normalizeError(error, signal) {
|
|
945
992
|
if (signal?.aborted) return new LLMError("LLM request aborted", "aborted");
|
|
946
993
|
if (error instanceof LLMError) {
|
|
947
|
-
if (error.
|
|
994
|
+
if (error.code === void 0) {
|
|
995
|
+
if (error.status === 429) error.code = "provider_rate_limited";
|
|
996
|
+
else if (error.status === 401 || error.status === 403) error.code = "invalid_credentials";
|
|
997
|
+
}
|
|
948
998
|
return error;
|
|
949
999
|
}
|
|
950
1000
|
const status = extractStatus(error);
|
|
951
1001
|
const retryAfterMs = extractRetryAfterMs(error);
|
|
952
|
-
if (status !== void 0) return new LLMError("LLM request failed", "api", status, void 0, error, retryAfterMs, status === 429 ? "provider_rate_limited" : void 0);
|
|
953
|
-
return new LLMError("LLM request failed", "unknown", void 0, void 0, error, retryAfterMs);
|
|
1002
|
+
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);
|
|
1003
|
+
return new LLMError("LLM request failed", "unknown", void 0, void 0, error, retryAfterMs, isNetworkError(error) ? "connection_failed" : void 0);
|
|
954
1004
|
}
|
|
955
1005
|
|
|
956
1006
|
//#endregion
|
|
@@ -1483,6 +1533,7 @@ var CallExecutor = class {
|
|
|
1483
1533
|
this.logger.debug(`[VernLLM:${requestId}] output:\n${this.redactedOutput(content, wireToolCalls).slice(0, 800)}`);
|
|
1484
1534
|
if (wireToolCalls?.length) {
|
|
1485
1535
|
if (!params.tools) throw new LLMError("Provider returned tool_calls but no `tools` were sent with this call.", "api");
|
|
1536
|
+
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");
|
|
1486
1537
|
const toolCalls = parseWireToolCalls(wireToolCalls);
|
|
1487
1538
|
this.validateToolCallArguments(toolCalls, params.tools);
|
|
1488
1539
|
this.breaker?.recordSuccess(model);
|
|
@@ -1743,7 +1794,7 @@ var CallExecutor = class {
|
|
|
1743
1794
|
await waitForRetry(delay, signal);
|
|
1744
1795
|
}
|
|
1745
1796
|
isNonRetryableToolContractError(error) {
|
|
1746
|
-
return error instanceof LLMError && (error.code === "unknown_tool" || error.code === "duplicate_tool_call_id");
|
|
1797
|
+
return error instanceof LLMError && (error.code === "unknown_tool" || error.code === "duplicate_tool_call_id" || error.code === "tool_choice_none_violated");
|
|
1747
1798
|
}
|
|
1748
1799
|
/** Decides whether a failed attempt is worth retrying. */
|
|
1749
1800
|
shouldRetry(error, signal) {
|
|
@@ -1756,11 +1807,12 @@ var CallExecutor = class {
|
|
|
1756
1807
|
}
|
|
1757
1808
|
/**
|
|
1758
1809
|
* Decides whether a failed attempt should count toward the circuit
|
|
1759
|
-
* breaker's failure threshold. A model hallucinating a tool name
|
|
1760
|
-
* reusing a call id
|
|
1761
|
-
*
|
|
1762
|
-
*
|
|
1763
|
-
*
|
|
1810
|
+
* breaker's failure threshold. A model hallucinating a tool name,
|
|
1811
|
+
* reusing a call id, or a provider ignoring `toolChoice: 'none'` isn't
|
|
1812
|
+
* the provider being unhealthy, it's a model/provider response defect
|
|
1813
|
+
* that will very likely recur regardless of provider health, so it
|
|
1814
|
+
* shouldn't push a healthy provider's circuit toward opening. Mirrors
|
|
1815
|
+
* the same reasoning `shouldRetry` already applies to
|
|
1764
1816
|
* `parse`/`validation`/these same tool-contract codes.
|
|
1765
1817
|
*/
|
|
1766
1818
|
countsTowardBreaker(error) {
|
|
@@ -1823,7 +1875,8 @@ var TokenBucket = class {
|
|
|
1823
1875
|
refill() {
|
|
1824
1876
|
if (this.refillPerMs === 0) return;
|
|
1825
1877
|
const now = Date.now();
|
|
1826
|
-
|
|
1878
|
+
const elapsedMs = now - this.lastRefill;
|
|
1879
|
+
this.available = Math.min(this.capacity, this.available + Math.max(0, elapsedMs) * this.refillPerMs);
|
|
1827
1880
|
this.lastRefill = now;
|
|
1828
1881
|
}
|
|
1829
1882
|
/** Refills, then takes `amount` if available. Leaves the bucket untouched if it can't. */
|
|
@@ -1916,7 +1969,6 @@ var RateLimiter = class {
|
|
|
1916
1969
|
release: this.makeRelease(estimatedTokens),
|
|
1917
1970
|
waitedMs: 0
|
|
1918
1971
|
};
|
|
1919
|
-
if (this.maxQueueSize > 0 && this.queue.length >= this.maxQueueSize) throw this.queueFullError();
|
|
1920
1972
|
return this.enqueue(estimatedTokens, attempt.reason, signal);
|
|
1921
1973
|
}
|
|
1922
1974
|
if (this.maxQueueSize > 0 && this.queue.length >= this.maxQueueSize) throw this.queueFullError();
|
|
@@ -2247,8 +2299,8 @@ var VernLLM = class {
|
|
|
2247
2299
|
}
|
|
2248
2300
|
async cachedCall(params) {
|
|
2249
2301
|
const { call: callParams,...cacheParams } = params;
|
|
2250
|
-
const
|
|
2251
|
-
if (reserveUsage || refundUsage)
|
|
2302
|
+
const restCallParams = callParams;
|
|
2303
|
+
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");
|
|
2252
2304
|
if (restCallParams.stream) {
|
|
2253
2305
|
const streamParams = restCallParams;
|
|
2254
2306
|
return this.cacheOrchestrator.runCachedStream({
|
|
@@ -3571,6 +3623,20 @@ function fromOpenAICompatible(client, options = {}) {
|
|
|
3571
3623
|
}
|
|
3572
3624
|
} } };
|
|
3573
3625
|
}
|
|
3626
|
+
/**
|
|
3627
|
+
* Named alias for the OpenAI SDK itself. A raw `new OpenAI(...)` instance
|
|
3628
|
+
* structurally matches most of `LLMClient`, but newer `openai` SDK major
|
|
3629
|
+
* versions have widened `ChatCompletionContentPart` (e.g. adding a `file`
|
|
3630
|
+
* variant) in ways that no longer structurally satisfy VernLLM's
|
|
3631
|
+
* provider-agnostic `ContentBlock[]` on `userContent`, so passing the SDK
|
|
3632
|
+
* instance directly can fail to typecheck depending on the installed
|
|
3633
|
+
* `openai` version. Wrapping with `fromOpenAI()` (a plain alias of
|
|
3634
|
+
* `fromOpenAICompatible()`) sidesteps that by translating through
|
|
3635
|
+
* `unknown` at the boundary, and also picks up multimodal image
|
|
3636
|
+
* translation and `createStream` wiring that a raw client doesn't have.
|
|
3637
|
+
* See Migration Notes for details.
|
|
3638
|
+
*/
|
|
3639
|
+
const fromOpenAI = fromOpenAICompatible;
|
|
3574
3640
|
/** Groqs SDK matches the OpenAI wire format */
|
|
3575
3641
|
const fromGroq = fromOpenAICompatible;
|
|
3576
3642
|
/**
|
|
@@ -3628,8 +3694,6 @@ const fromNvidiaNIM = fromOpenAICompatible;
|
|
|
3628
3694
|
const fromVercelAIGateway = fromOpenAICompatible;
|
|
3629
3695
|
/** Cloudflare Workers AI exposes an OpenAI-compatible endpoint */
|
|
3630
3696
|
const fromCloudflareWorkersAI = fromOpenAICompatible;
|
|
3631
|
-
/** GitHub Models is OpenAI-compatible */
|
|
3632
|
-
const fromGitHubModels = fromOpenAICompatible;
|
|
3633
3697
|
/** Nebius AI Studio is OpenAI-compatible */
|
|
3634
3698
|
const fromNebius = fromOpenAICompatible;
|
|
3635
3699
|
/** SambaNova Cloud's API is OpenAI-compatible */
|
|
@@ -3656,8 +3720,6 @@ const fromSnowflakeCortex = fromOpenAICompatible;
|
|
|
3656
3720
|
const fromAnyscale = fromOpenAICompatible;
|
|
3657
3721
|
/** Lepton AI's inference API is OpenAI-compatible */
|
|
3658
3722
|
const fromLepton = fromOpenAICompatible;
|
|
3659
|
-
/** kluster.ai's inference API is OpenAI-compatible */
|
|
3660
|
-
const fromKlusterAI = fromOpenAICompatible;
|
|
3661
3723
|
/** Inference.net's API is OpenAI-compatible */
|
|
3662
3724
|
const fromInferenceNet = fromOpenAICompatible;
|
|
3663
3725
|
/** Infermatic's API is OpenAI-compatible */
|
|
@@ -3695,12 +3757,10 @@ exports.fromFetch = fromFetch
|
|
|
3695
3757
|
exports.fromFireworks = fromFireworks
|
|
3696
3758
|
exports.fromFriendli = fromFriendli
|
|
3697
3759
|
exports.fromGemini = fromGemini
|
|
3698
|
-
exports.fromGitHubModels = fromGitHubModels
|
|
3699
3760
|
exports.fromGroq = fromGroq
|
|
3700
3761
|
exports.fromHyperbolic = fromHyperbolic
|
|
3701
3762
|
exports.fromInferenceNet = fromInferenceNet
|
|
3702
3763
|
exports.fromInfermatic = fromInfermatic
|
|
3703
|
-
exports.fromKlusterAI = fromKlusterAI
|
|
3704
3764
|
exports.fromLMStudio = fromLMStudio
|
|
3705
3765
|
exports.fromLambdaLabs = fromLambdaLabs
|
|
3706
3766
|
exports.fromLepton = fromLepton
|
|
@@ -3711,6 +3771,7 @@ exports.fromNebius = fromNebius
|
|
|
3711
3771
|
exports.fromNovita = fromNovita
|
|
3712
3772
|
exports.fromNvidiaNIM = fromNvidiaNIM
|
|
3713
3773
|
exports.fromOllama = fromOllama
|
|
3774
|
+
exports.fromOpenAI = fromOpenAI
|
|
3714
3775
|
exports.fromOpenAICompatible = fromOpenAICompatible
|
|
3715
3776
|
exports.fromOpenRouter = fromOpenRouter
|
|
3716
3777
|
exports.fromParasail = fromParasail
|