myapikey 0.36.0 → 0.36.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "myapikey",
3
- "version": "0.36.0",
3
+ "version": "0.36.2",
4
4
  "type": "module",
5
5
  "description": "Personal LLM API gateway & proxy — one address + one API key for all your models. Forwards OpenAI & Anthropic calls to your backends with failover and a circuit breaker. Pure passthrough, no format translation. Self-hosted (CLI + web UI).",
6
6
  "keywords": [
@@ -6,8 +6,43 @@ import { UsageCollector } from "./tokens";
6
6
 
7
7
  /** HTTP statuses that should trigger failover to the next provider. 401/403
8
8
  * included: a banned/invalid credential (e.g. "User has been banned") is dead
9
- * for THIS source only - the same request may be fine on the next one. */
10
- const RETRYABLE = new Set([401, 403, 408, 425, 429, 500, 502, 503, 504]);
9
+ * for THIS source only - the same request may be fine on the next one. 402 is
10
+ * always account-level (exhausted balance). */
11
+ const RETRYABLE = new Set([401, 402, 403, 408, 425, 429, 500, 502, 503, 504]);
12
+
13
+ /** Error-body markers that turn a NON-retryable 4xx into an account-level
14
+ * failure worth failing over: dead credential / expired subscription / no
15
+ * balance, which some vendors smuggle under a 400 instead of 401/403
16
+ * (Volcengine Ark's expired coding-plan subscription is the case that bit
17
+ * us). Deliberately narrow — a genuine request-shape 400 (bad params, context
18
+ * overflow, "max_tokens too large") still returns to the client as-is, since
19
+ * every other source would 400 identically. Matched case-insensitively. */
20
+ const ACCOUNT_HINTS = [
21
+ "subscription",
22
+ "unauthorized",
23
+ "invalid_api_key",
24
+ "invalid api key",
25
+ "incorrect api key",
26
+ "quota",
27
+ "insufficient",
28
+ "balance",
29
+ "credit",
30
+ "expired",
31
+ "令牌",
32
+ "订阅",
33
+ "额度",
34
+ "余额",
35
+ "欠费",
36
+ "未开通",
37
+ ];
38
+
39
+ /** True when `status`/`bodyText` describe an account-level failure on an
40
+ * otherwise non-retryable 4xx (see ACCOUNT_HINTS). */
41
+ function isAccountLevelError(status: number, bodyText: string): boolean {
42
+ if (RETRYABLE.has(status) || status < 400 || status >= 500) return false;
43
+ const t = bodyText.toLowerCase();
44
+ return ACCOUNT_HINTS.some((h) => t.includes(h));
45
+ }
11
46
 
12
47
  /** Even pacing (per-model `paceRpm`) message constants. The queue itself lives
13
48
  * in Store.paceClaim - 60s wait horizon, one release every 60/rpm seconds. */
@@ -259,11 +294,6 @@ function downHeaders(upstream: Response, servedBy?: string): Headers {
259
294
  return headers;
260
295
  }
261
296
 
262
- function passThrough(upstream: Response, servedBy?: string): Response {
263
- // Stream the upstream body straight through (handles SSE + normal JSON).
264
- return new Response(upstream.body, { status: upstream.status, headers: downHeaders(upstream, servedBy) });
265
- }
266
-
267
297
  /** Pull a short human-readable message out of an upstream error body. */
268
298
  export function shortError(text: string): string {
269
299
  try {
@@ -712,13 +742,18 @@ export function proxyApi(
712
742
  });
713
743
  return new Response(out, { status: upstream.status, headers: downHeaders(upstream, isProbe ? provider.name : undefined) });
714
744
  }
715
- if (RETRYABLE.has(upstream.status)) {
716
- lastStatus = upstream.status;
717
- // Drain so the connection can be reused, then move on; capture the
718
- // reason for the log (this branch never streams back to the client).
719
- const txt = await upstream.text().catch(() => "");
720
- lastErr = shortError(txt) || `HTTP ${upstream.status}`;
721
- capture(upstream.status, txt, false, lastErr);
745
+ // Error from the upstream. Read the body ONCE (drains the connection
746
+ // for reuse), then either fail over or pass the error back verbatim —
747
+ // both branches share this text (log row + debug capture).
748
+ const txt = await upstream.text().catch(() => "");
749
+ lastStatus = upstream.status;
750
+ lastErr = shortError(txt) || `HTTP ${upstream.status}`;
751
+ capture(upstream.status, txt, false, lastErr);
752
+ // Fail over on a retryable status OR an account-level failure smuggled
753
+ // under a non-retryable 4xx (expired subscription / dead key / no
754
+ // balance returned as 400) — the source is dead for THIS request only,
755
+ // exactly like a 401/403.
756
+ if (RETRYABLE.has(upstream.status) || isAccountLevelError(upstream.status, txt)) {
722
757
  if (pinIndex != null) break; // per-source probe: fail fast, no circuit impact.
723
758
  sayFailover(provider, `HTTP ${lastStatus} (${lastErr})`);
724
759
  // A 429/overloaded upstream usually carries Retry-After; honoring it
@@ -735,12 +770,9 @@ export function proxyApi(
735
770
  }
736
771
  continue;
737
772
  }
738
- // Non-retryable client error: return it to the caller as-is. Read the
739
- // error text off a CLONE so the original body still streams back.
740
- const errText = await upstream.clone().text().catch(() => "");
741
- store.pushLog({ ts: Date.now(), model, upstreamModel, provider: provider.name, providerId: provider.id, format: wire, status: upstream.status, ms: Date.now() - start, stream, thinking: think, error: shortError(errText) || `HTTP ${upstream.status}` });
742
- capture(upstream.status, errText, false, shortError(errText) || `HTTP ${upstream.status}`);
743
- return passThrough(upstream, isProbe ? provider.name : undefined);
773
+ // Non-retryable client error: return it to the caller as-is.
774
+ store.pushLog({ ts: Date.now(), model, upstreamModel, provider: provider.name, providerId: provider.id, format: wire, status: upstream.status, ms: Date.now() - start, stream, thinking: think, error: lastErr });
775
+ return new Response(txt, { status: upstream.status, headers: downHeaders(upstream, isProbe ? provider.name : undefined) });
744
776
  }
745
777
 
746
778
  const last = order[order.length - 1];