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 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` 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({
@@ -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