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/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` so existing
167
- * type-based handling keeps working on a fallback-exhausted error too.
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, void 0, "fallback_exhausted");
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.status === 429 && error.code === void 0) error.code = "provider_rate_limited";
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 or
1736
- * reusing a call id isn't the provider being unhealthy, it's a model
1737
- * response defect that will very likely recur regardless of provider
1738
- * health, so it shouldn't push a healthy provider's circuit toward
1739
- * opening. Mirrors the same reasoning `shouldRetry` already applies to
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
- this.available = Math.min(this.capacity, this.available + (now - this.lastRefill) * this.refillPerMs);
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 { reserveUsage, refundUsage,...restCallParams } = callParams;
2227
- if (reserveUsage || refundUsage) this.logger.warn("[VernLLM] reserveUsage/refundUsage on `call` are ignored by cachedCall; set them at the top level instead.");
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, 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 };
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