vern-llm 2.1.1 → 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.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` so existing
191
- * type-based handling keeps working on a fallback-exhausted error too.
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, void 0, "fallback_exhausted");
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.status === 429 && error.code === void 0) error.code = "provider_rate_limited";
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 or
1760
- * reusing a call id isn't the provider being unhealthy, it's a model
1761
- * response defect that will very likely recur regardless of provider
1762
- * health, so it shouldn't push a healthy provider's circuit toward
1763
- * opening. Mirrors the same reasoning `shouldRetry` already applies to
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
- this.available = Math.min(this.capacity, this.available + (now - this.lastRefill) * this.refillPerMs);
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 { reserveUsage, refundUsage,...restCallParams } = callParams;
2251
- if (reserveUsage || refundUsage) this.logger.warn("[VernLLM] reserveUsage/refundUsage on `call` are ignored by cachedCall; set them at the top level instead.");
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({
@@ -3642,8 +3694,6 @@ const fromNvidiaNIM = fromOpenAICompatible;
3642
3694
  const fromVercelAIGateway = fromOpenAICompatible;
3643
3695
  /** Cloudflare Workers AI exposes an OpenAI-compatible endpoint */
3644
3696
  const fromCloudflareWorkersAI = fromOpenAICompatible;
3645
- /** GitHub Models is OpenAI-compatible */
3646
- const fromGitHubModels = fromOpenAICompatible;
3647
3697
  /** Nebius AI Studio is OpenAI-compatible */
3648
3698
  const fromNebius = fromOpenAICompatible;
3649
3699
  /** SambaNova Cloud's API is OpenAI-compatible */
@@ -3670,8 +3720,6 @@ const fromSnowflakeCortex = fromOpenAICompatible;
3670
3720
  const fromAnyscale = fromOpenAICompatible;
3671
3721
  /** Lepton AI's inference API is OpenAI-compatible */
3672
3722
  const fromLepton = fromOpenAICompatible;
3673
- /** kluster.ai's inference API is OpenAI-compatible */
3674
- const fromKlusterAI = fromOpenAICompatible;
3675
3723
  /** Inference.net's API is OpenAI-compatible */
3676
3724
  const fromInferenceNet = fromOpenAICompatible;
3677
3725
  /** Infermatic's API is OpenAI-compatible */
@@ -3709,12 +3757,10 @@ exports.fromFetch = fromFetch
3709
3757
  exports.fromFireworks = fromFireworks
3710
3758
  exports.fromFriendli = fromFriendli
3711
3759
  exports.fromGemini = fromGemini
3712
- exports.fromGitHubModels = fromGitHubModels
3713
3760
  exports.fromGroq = fromGroq
3714
3761
  exports.fromHyperbolic = fromHyperbolic
3715
3762
  exports.fromInferenceNet = fromInferenceNet
3716
3763
  exports.fromInfermatic = fromInfermatic
3717
- exports.fromKlusterAI = fromKlusterAI
3718
3764
  exports.fromLMStudio = fromLMStudio
3719
3765
  exports.fromLambdaLabs = fromLambdaLabs
3720
3766
  exports.fromLepton = fromLepton