myapikey 0.9.0 → 0.10.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "myapikey",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
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": [
@@ -54,7 +54,8 @@
54
54
  "scripts": {
55
55
  "dev": "tsx watch packages/core/src/cli/index.ts serve",
56
56
  "dev:web": "npm run dev -w @myapikey/web",
57
- "start": "tsx packages/core/src/cli/index.ts serve",
57
+ "start": "npm run build:web && npm run serve",
58
+ "serve": "tsx packages/core/src/cli/index.ts serve",
58
59
  "typecheck": "tsc -p packages/core/tsconfig.json --noEmit && tsc -p packages/core/tsconfig.test.json --noEmit && vue-tsc --noEmit -p packages/web/tsconfig.json",
59
60
  "build:web": "npm run build -w @myapikey/web",
60
61
  "test": "vitest run",
@@ -367,6 +367,12 @@ export function proxyApi(store: Store, auth: MiddlewareHandler): Hono {
367
367
  // the slot each iteration, so failover never carries the previous slot's
368
368
  // upstream name.
369
369
  body.model = slot.model ?? model;
370
+ // The actual upstream model forwarded this attempt (after the per-slot
371
+ // rewrite). Recorded on the log row so history shows which real model a
372
+ // routed call landed on when a source remaps the public name. `undefined`
373
+ // when the public name went through verbatim — JSON.stringify drops it, so
374
+ // identity + legacy rows stay clean.
375
+ const upstreamModel = slot.model && slot.model !== model ? slot.model : undefined;
370
376
  // Count this attempt toward the source's RPM window — but not for a pinned
371
377
  // probe, which (like circuit state) takes no routing side-effects.
372
378
  if (pinIndex == null) store.recordDispatch(provider.id);
@@ -384,7 +390,7 @@ export function proxyApi(store: Store, auth: MiddlewareHandler): Hono {
384
390
  if (pinIndex != null) break; // per-source probe: fail fast, no circuit impact.
385
391
  const r = store.recordCircuitFailure(provider.id, lastStatus, lastErr);
386
392
  if (r.entered) {
387
- store.pushLog({ ts: Date.now(), model, provider: provider.name, providerId: provider.id, format: wire, status: lastStatus, ms: Date.now() - start, stream, kind: "cooldown", cooldownMs: r.cooldownMs, fails: r.fails, error: lastErr });
393
+ store.pushLog({ ts: Date.now(), model, upstreamModel, provider: provider.name, providerId: provider.id, format: wire, status: lastStatus, ms: Date.now() - start, stream, kind: "cooldown", cooldownMs: r.cooldownMs, fails: r.fails, error: lastErr });
388
394
  }
389
395
  continue;
390
396
  }
@@ -409,12 +415,12 @@ export function proxyApi(store: Store, auth: MiddlewareHandler): Hono {
409
415
  onSettle: (info) => {
410
416
  if (info.ok) {
411
417
  store.recordCircuitSuccess(provider.id);
412
- store.pushLog({ ts: Date.now(), model, provider: provider.name, providerId: provider.id, format: wire, status: 200, ms: ttfb, stream, usage: info.usage });
418
+ store.pushLog({ ts: Date.now(), model, upstreamModel, provider: provider.name, providerId: provider.id, format: wire, status: 200, ms: ttfb, stream, usage: info.usage });
413
419
  } else {
414
420
  // A pinned per-source probe takes no circuit side-effects (a manual
415
421
  // test must not trip the breaker) — mirrors the retryable branch.
416
422
  if (pinIndex == null) store.recordCircuitFailure(provider.id, info.status, info.error || "stream failed");
417
- store.pushLog({ ts: Date.now(), model, provider: provider.name, providerId: provider.id, format: wire, status: info.status, ms: ttfb, stream, error: info.error });
423
+ store.pushLog({ ts: Date.now(), model, upstreamModel, provider: provider.name, providerId: provider.id, format: wire, status: info.status, ms: ttfb, stream, error: info.error });
418
424
  }
419
425
  },
420
426
  });
@@ -436,19 +442,20 @@ export function proxyApi(store: Store, auth: MiddlewareHandler): Hono {
436
442
  const resetInMs = retryAfterMs ? undefined : parseResetFromBody(txt);
437
443
  const r = store.recordCircuitFailure(provider.id, lastStatus, lastErr, retryAfterMs ?? resetInMs, !!resetInMs);
438
444
  if (r.entered) {
439
- store.pushLog({ ts: Date.now(), model, provider: provider.name, providerId: provider.id, format: wire, status: lastStatus, ms: Date.now() - start, stream, kind: "cooldown", cooldownMs: r.cooldownMs, fails: r.fails, error: lastErr });
445
+ store.pushLog({ ts: Date.now(), model, upstreamModel, provider: provider.name, providerId: provider.id, format: wire, status: lastStatus, ms: Date.now() - start, stream, kind: "cooldown", cooldownMs: r.cooldownMs, fails: r.fails, error: lastErr });
440
446
  }
441
447
  continue;
442
448
  }
443
449
  // Non-retryable client error: return it to the caller as-is. Read the
444
450
  // error text off a CLONE so the original body still streams back.
445
451
  const errText = await upstream.clone().text().catch(() => "");
446
- store.pushLog({ ts: Date.now(), model, provider: provider.name, providerId: provider.id, format: wire, status: upstream.status, ms: Date.now() - start, stream, error: shortError(errText) || `HTTP ${upstream.status}` });
452
+ store.pushLog({ ts: Date.now(), model, upstreamModel, provider: provider.name, providerId: provider.id, format: wire, status: upstream.status, ms: Date.now() - start, stream, error: shortError(errText) || `HTTP ${upstream.status}` });
447
453
  return passThrough(upstream, isProbe ? provider.name : undefined);
448
454
  }
449
455
 
450
456
  const last = order[order.length - 1];
451
- store.pushLog({ ts: Date.now(), model, provider: last.provider.name, providerId: last.provider.id, format: wire, status: lastStatus, ms: Date.now() - start, stream, error: lastErr || `all providers failed (last status ${lastStatus})` });
457
+ const lastUpstreamModel = last.model && last.model !== model ? last.model : undefined;
458
+ store.pushLog({ ts: Date.now(), model, upstreamModel: lastUpstreamModel, provider: last.provider.name, providerId: last.provider.id, format: wire, status: lastStatus, ms: Date.now() - start, stream, error: lastErr || `all providers failed (last status ${lastStatus})` });
452
459
  // A pinned (per-source) probe failed: surface the REAL upstream status the
453
460
  // one slot returned (429/500/…), not a collapsed 502, and tag it with
454
461
  // x-myapikey-provider so the source-row badge names the tested source.
@@ -118,6 +118,13 @@ export interface Usage {
118
118
  export interface LogEntry {
119
119
  ts: number;
120
120
  model: string;
121
+ /** The actual upstream model name forwarded to the provider, when a per-slot
122
+ * rewrite changed it (a ChainSlot `model` that differs from the public name —
123
+ * an alias, a model swap, or one of several models on a repeated source).
124
+ * Absent when the public name was forwarded verbatim, so identity + legacy
125
+ * rows stay clean. Lets the log show which real model answered when a source
126
+ * remaps the public name. */
127
+ upstreamModel?: string;
121
128
  provider: string;
122
129
  /** Stable provider id (newer entries). Stats group by this so renaming a
123
130
  * provider doesn't split its history; the display name is resolved from the