myapikey 0.35.0 → 0.36.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 +1 -1
- package/packages/core/src/cli/index.ts +23 -16
- package/packages/core/src/server/admin.ts +16 -3
- package/packages/core/src/server/proxy.ts +22 -24
- package/packages/core/src/server/store.ts +51 -18
- package/packages/core/src/shared/types.ts +3 -1
- package/packages/web/dist/assets/index-DBb7JCKv.js +334 -0
- package/packages/web/dist/index.html +1 -1
- package/packages/web/dist/assets/index-cZhEr_b8.js +0 -334
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "myapikey",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.36.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": [
|
|
@@ -285,7 +285,7 @@ model.command("remove <name>").description("remove a model entirely (both format
|
|
|
285
285
|
|
|
286
286
|
model
|
|
287
287
|
.command("debug <name> [action] [index]")
|
|
288
|
-
.description("debug capture: on|off toggles recording of the last 50 actual
|
|
288
|
+
.description("debug capture: on|off toggles recording of the last 50 actual requests/responses (failed attempts are ALWAYS recorded in a global net, last 50); show lists everything (add an index to dump one in full)")
|
|
289
289
|
.action(async (name: string, action = "show", indexRaw?: string) => {
|
|
290
290
|
const path = `/admin/models/${encodeURIComponent(name)}/debug`;
|
|
291
291
|
if (action === "on" || action === "off") {
|
|
@@ -293,32 +293,39 @@ model
|
|
|
293
293
|
console.log(
|
|
294
294
|
r.enabled
|
|
295
295
|
? `Debug capture ON for ${name} — the last 50 upstream attempts (request + response) are recorded in memory; turn off to clear.`
|
|
296
|
-
: `Debug capture OFF for ${name}; captured content cleared.`,
|
|
296
|
+
: `Debug capture OFF for ${name}; captured content cleared (auto-recorded failures stay until they age out of the net).`,
|
|
297
297
|
);
|
|
298
298
|
return;
|
|
299
299
|
}
|
|
300
300
|
if (action !== "show") throw new Error(`Unknown action '${action}' (use on | off | show).`);
|
|
301
|
-
const r = (await api(ctx(), "GET", path)) as { enabled: boolean; captures: any[] };
|
|
302
|
-
if (!r.enabled) console.log(`Debug capture is OFF for ${name}.`);
|
|
303
|
-
const
|
|
301
|
+
const r = (await api(ctx(), "GET", path)) as { enabled: boolean; captures: any[]; failures: any[] };
|
|
302
|
+
if (!r.enabled) console.log(`Debug capture is OFF for ${name} (failed calls are still auto-recorded).`);
|
|
303
|
+
const isFail = (c: any) => c.status >= 400 || c.status === 0;
|
|
304
|
+
// One newest-first timeline; a failure made while the switch was on sits
|
|
305
|
+
// in both server buffers — show it once, from the net, tagged AUTO.
|
|
306
|
+
const rows = [
|
|
307
|
+
...(r.failures ?? []).map((c) => ({ c, auto: true })),
|
|
308
|
+
...(r.captures ?? []).filter((c) => !isFail(c)).map((c) => ({ c, auto: false })),
|
|
309
|
+
].sort((a, b) => b.c.ts - a.c.ts);
|
|
304
310
|
if (indexRaw !== undefined) {
|
|
305
|
-
const row =
|
|
306
|
-
if (!row) throw new Error(`No capture #${indexRaw} (list is newest-first, ${
|
|
307
|
-
|
|
311
|
+
const row = rows[Number(indexRaw) - 1];
|
|
312
|
+
if (!row) throw new Error(`No capture #${indexRaw} (list is newest-first, ${rows.length} recorded).`);
|
|
313
|
+
const c = row.c;
|
|
314
|
+
console.log(`#${Number(indexRaw)}${row.auto ? " (auto)" : ""} ${new Date(c.ts).toLocaleTimeString("en-GB", { hour12: false })} ${c.provider} [${c.format}] status=${c.status} ${c.ms}ms${c.upstreamModel ? ` model=${c.upstreamModel}` : ""}${c.error ? ` error=${c.error}` : ""}${c.truncated ? " (truncated)" : ""}`);
|
|
308
315
|
console.log("\n--- request (forwarded verbatim) ---");
|
|
309
|
-
console.log(prettyJson(
|
|
310
|
-
if (
|
|
316
|
+
console.log(prettyJson(c.request));
|
|
317
|
+
if (c.response !== undefined) {
|
|
311
318
|
console.log("\n--- response (upstream) ---");
|
|
312
|
-
console.log(prettyJson(
|
|
319
|
+
console.log(prettyJson(c.response));
|
|
313
320
|
}
|
|
314
321
|
return;
|
|
315
322
|
}
|
|
316
|
-
if (!
|
|
317
|
-
console.log(`${name}: ${
|
|
318
|
-
for (let i = 0; i <
|
|
319
|
-
const c =
|
|
323
|
+
if (!rows.length) return console.log("Nothing recorded yet — call the model, then `show` again (or pass an index to dump one).");
|
|
324
|
+
console.log(`${name}: ${rows.length} recorded (newest first; AUTO = failed attempt, always recorded)`);
|
|
325
|
+
for (let i = 0; i < rows.length; i++) {
|
|
326
|
+
const { c, auto } = rows[i];
|
|
320
327
|
console.log(
|
|
321
|
-
` #${i + 1} ${new Date(c.ts).toLocaleTimeString("en-GB", { hour12: false })} ${c.provider} [${c.format}] status=${c.status} ${c.ms}ms ${fmtBytes(c.request)} req / ${fmtBytes(c.response ?? "")} resp${c.upstreamModel ? ` model=${c.upstreamModel}` : ""}${c.error ? ` ${c.error}` : ""}`,
|
|
328
|
+
` #${i + 1}${auto ? " AUTO" : " "} ${new Date(c.ts).toLocaleTimeString("en-GB", { hour12: false })} ${c.provider} [${c.format}] status=${c.status} ${c.ms}ms ${fmtBytes(c.request)} req / ${fmtBytes(c.response ?? "")} resp${c.upstreamModel ? ` model=${c.upstreamModel}` : ""}${c.error ? ` ${c.error}` : ""}`,
|
|
322
329
|
);
|
|
323
330
|
}
|
|
324
331
|
console.log("\nDump one: myapikey model debug <name> show <index>");
|
|
@@ -705,12 +705,15 @@ export function adminApi(store: Store, auth: MiddlewareHandler, openai: Hono, an
|
|
|
705
705
|
// ON: dispatch records every upstream attempt for this model (exact forwarded
|
|
706
706
|
// request + response bodies) into an in-memory ring buffer, last 50. OFF: the
|
|
707
707
|
// buffer is cleared immediately — captured conversations shouldn't outlive the
|
|
708
|
-
// debugging session.
|
|
709
|
-
//
|
|
708
|
+
// debugging session. Independently, FAILED attempts always land in a global
|
|
709
|
+
// in-memory net (last 50 across all models) so an error can be inspected even
|
|
710
|
+
// when the switch was never on; `failures` here is that net filtered to this
|
|
711
|
+
// model. The switch is config state (persists in data.json); the captured
|
|
712
|
+
// bodies do not (in-memory only, restart clears them too).
|
|
710
713
|
app.get("/models/:name/debug", (c) => {
|
|
711
714
|
const name = c.req.param("name");
|
|
712
715
|
if (!store.get().models[name]) return c.json({ error: { message: "model not found" } }, 404);
|
|
713
|
-
return c.json({ enabled: store.isDebug(name), captures: store.getCaptures(name) });
|
|
716
|
+
return c.json({ enabled: store.isDebug(name), captures: store.getCaptures(name), failures: store.getFailCaptures(name) });
|
|
714
717
|
});
|
|
715
718
|
|
|
716
719
|
app.put("/models/:name/debug", async (c) => {
|
|
@@ -732,6 +735,16 @@ app.put("/models/:name/debug", async (c) => {
|
|
|
732
735
|
return c.json({ ok: true, enabled });
|
|
733
736
|
});
|
|
734
737
|
|
|
738
|
+
// Manual scrub of this model's rows in the always-on failure net (the switch
|
|
739
|
+
// only governs the per-model buffer; failures age out of the global ring on
|
|
740
|
+
// their own — this drops them now).
|
|
741
|
+
app.delete("/models/:name/debug/fails", (c) => {
|
|
742
|
+
const name = c.req.param("name");
|
|
743
|
+
if (!store.get().models[name]) return c.json({ error: { message: "model not found" } }, 404);
|
|
744
|
+
store.clearFailCaptures(name);
|
|
745
|
+
return c.json({ ok: true });
|
|
746
|
+
});
|
|
747
|
+
|
|
735
748
|
// Set (or clear) the upstream-model mapping for ONE chain slot (addressed by
|
|
736
749
|
// `index`). An empty `model` clears it (back to identity — send the public
|
|
737
750
|
// name). Index-addressed because a provider may occupy several slots, each
|
|
@@ -613,17 +613,19 @@ export function proxyApi(
|
|
|
613
613
|
// Count this attempt toward the source's RPM window — but not for a pinned
|
|
614
614
|
// probe, which (like circuit state) takes no routing side-effects.
|
|
615
615
|
if (pinIndex == null) store.recordDispatch(provider.id);
|
|
616
|
-
// Debug capture
|
|
617
|
-
//
|
|
618
|
-
//
|
|
619
|
-
//
|
|
620
|
-
//
|
|
621
|
-
//
|
|
622
|
-
|
|
616
|
+
// Debug capture: EVERY upstream attempt is offered to the store —
|
|
617
|
+
// failed attempts always land in the global failure net (the safety
|
|
618
|
+
// net for after-the-fact debugging), everything lands in the model's
|
|
619
|
+
// own buffer while its switch is on. The row carries the exact
|
|
620
|
+
// forwarded body (this slot's model rewrite + thinking injection are
|
|
621
|
+
// already applied) and the response as it flowed. UI probes are
|
|
622
|
+
// excluded (not real conversations). One entry per attempt: a
|
|
623
|
+
// failover chain writes several, each showing what THAT source
|
|
624
|
+
// actually received.
|
|
623
625
|
const attemptStart = Date.now();
|
|
624
626
|
const reqText = JSON.stringify(body);
|
|
625
627
|
const capture = (status: number, response: string | undefined, truncated: boolean, error?: string) => {
|
|
626
|
-
if (
|
|
628
|
+
if (isProbe) return;
|
|
627
629
|
const row: DebugCapture = {
|
|
628
630
|
ts: Date.now(),
|
|
629
631
|
model,
|
|
@@ -679,36 +681,32 @@ export function proxyApi(
|
|
|
679
681
|
const ttfb = Date.now() - start;
|
|
680
682
|
// Debug capture tee: accumulate the decoded chunks into a bounded
|
|
681
683
|
// string (the proxy keeps forwarding bytes verbatim regardless).
|
|
682
|
-
const capAcc =
|
|
684
|
+
const capAcc = { text: "", truncated: false };
|
|
683
685
|
const out = observedBody(upstream, {
|
|
684
686
|
stream,
|
|
685
687
|
key,
|
|
686
688
|
requestMessages: body.messages,
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
capAcc.text += txt.slice(0, room);
|
|
697
|
-
},
|
|
698
|
-
}
|
|
699
|
-
: {}),
|
|
689
|
+
onText: (txt: string) => {
|
|
690
|
+
const room = CAPTURE_BODY_MAX - capAcc.text.length;
|
|
691
|
+
if (room <= 0) {
|
|
692
|
+
capAcc.truncated = true;
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
695
|
+
if (txt.length > room) capAcc.truncated = true;
|
|
696
|
+
capAcc.text += txt.slice(0, room);
|
|
697
|
+
},
|
|
700
698
|
onSettle: (info) => {
|
|
701
699
|
if (info.ok) {
|
|
702
700
|
store.recordCircuitSuccess(provider.id);
|
|
703
701
|
store.pushLog({ ts: Date.now(), model, upstreamModel, provider: provider.name, providerId: provider.id, format: wire, status: 200, ms: ttfb, stream, thinking: think, usage: info.usage });
|
|
704
|
-
capture(200, capAcc
|
|
702
|
+
capture(200, capAcc.text, capAcc.truncated);
|
|
705
703
|
} else {
|
|
706
704
|
// A pinned per-source probe takes no circuit side-effects (a manual
|
|
707
705
|
// test must not trip the breaker) — mirrors the retryable branch.
|
|
708
706
|
if (pinIndex == null) store.recordCircuitFailure(provider.id, info.status, info.error || "stream failed");
|
|
709
707
|
if (!isProbe) rt.warn(`proxy stream failed: provider '${provider.name}' status=${info.status} (${info.error || "stream failed"})`);
|
|
710
708
|
store.pushLog({ ts: Date.now(), model, upstreamModel, provider: provider.name, providerId: provider.id, format: wire, status: info.status, ms: ttfb, stream, thinking: think, error: info.error });
|
|
711
|
-
capture(info.status, capAcc
|
|
709
|
+
capture(info.status, capAcc.text, capAcc.truncated, info.error);
|
|
712
710
|
}
|
|
713
711
|
},
|
|
714
712
|
});
|
|
@@ -45,12 +45,18 @@ const RPM_WINDOW_MS = 60_000;
|
|
|
45
45
|
* time and the queue depth (at N rpm / 60s wait, at most ~N requests queue). */
|
|
46
46
|
const PACE_MAX_WAIT_MS = 60_000;
|
|
47
47
|
|
|
48
|
-
/** Debug capture (
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
48
|
+
/** Debug capture (in-memory ring buffers, never persisted). Two tiers:
|
|
49
|
+
* - FAIL_NET (global): the last CAPTURE_FAIL_MAX FAILED upstream attempts
|
|
50
|
+
* across ALL models, recorded ALWAYS (that's the safety net — errors are
|
|
51
|
+
* debugged after the fact, when nobody pre-armed a switch). 50 × 2 × 256KB
|
|
52
|
+
* ≈ 25MB worst case, typical far less (failed responses are tiny).
|
|
53
|
+
* - per-model switch buffer: last CAPTURE_MAX attempts of a model whose
|
|
54
|
+
* ModelEntry.debugCapture is on — success AND failure.
|
|
55
|
+
* Bodies (conversations) are sensitive: everything lives only in memory,
|
|
56
|
+
* never touches disk, ages out of the rings, and the switch-off / clear
|
|
57
|
+
* endpoints drop it on demand. */
|
|
53
58
|
export const CAPTURE_MAX = 50;
|
|
59
|
+
export const CAPTURE_FAIL_MAX = 50;
|
|
54
60
|
export const CAPTURE_BODY_MAX = 256 * 1024;
|
|
55
61
|
|
|
56
62
|
/** Per-provider circuit state (in-memory, never persisted). */
|
|
@@ -200,10 +206,14 @@ export class Store {
|
|
|
200
206
|
* release slot. In-memory, NOT persisted (resets on restart). */
|
|
201
207
|
private pace = new Map<string, number>();
|
|
202
208
|
/** Per-model debug-capture ring buffers: model name -> last CAPTURE_MAX
|
|
203
|
-
* upstream attempts (oldest first)
|
|
204
|
-
* conversation content, so they live
|
|
205
|
-
* is on and evaporate on restart or toggle-off. */
|
|
209
|
+
* upstream attempts (oldest first) while the model's switch is on.
|
|
210
|
+
* In-memory, NOT persisted — bodies are conversation content, so they live
|
|
211
|
+
* only while the switch is on and evaporate on restart or toggle-off. */
|
|
206
212
|
private captures = new Map<string, DebugCapture[]>();
|
|
213
|
+
/** The always-on failure net: last CAPTURE_FAIL_MAX FAILED upstream attempts
|
|
214
|
+
* across all models (oldest first), regardless of any switch. In-memory,
|
|
215
|
+
* NOT persisted; entries age out as new failures push them off. */
|
|
216
|
+
private failCaptures: DebugCapture[] = [];
|
|
207
217
|
|
|
208
218
|
constructor(dataDir: string, opts: { logger?: Logger } = {}) {
|
|
209
219
|
this.dataDir = dataDir;
|
|
@@ -669,12 +679,27 @@ export class Store {
|
|
|
669
679
|
return !!this.data.models[model]?.debugCapture;
|
|
670
680
|
}
|
|
671
681
|
|
|
672
|
-
/** Record one upstream attempt
|
|
673
|
-
*
|
|
674
|
-
*
|
|
675
|
-
*
|
|
682
|
+
/** Record one upstream attempt. Routing: FAILED attempts (4xx/5xx/network)
|
|
683
|
+
* always land in the global failure net (the safety net for after-the-fact
|
|
684
|
+
* debugging); EVERYTHING lands in the model's own buffer while its switch
|
|
685
|
+
* is on. When the switch is off, successes are dropped. Bodies are capped
|
|
686
|
+
* at CAPTURE_BODY_MAX chars each (flagging `truncated`). */
|
|
676
687
|
pushCapture(model: string, entry: DebugCapture): void {
|
|
688
|
+
const e = this.capped({ ...entry, model }); // row's model = the routing key
|
|
689
|
+
const failed = e.status >= 400 || e.status === 0;
|
|
690
|
+
if (failed) {
|
|
691
|
+
this.failCaptures.push(e);
|
|
692
|
+
if (this.failCaptures.length > CAPTURE_FAIL_MAX) this.failCaptures.splice(0, this.failCaptures.length - CAPTURE_FAIL_MAX);
|
|
693
|
+
}
|
|
677
694
|
if (!this.isDebug(model)) return;
|
|
695
|
+
let arr = this.captures.get(model);
|
|
696
|
+
if (!arr) this.captures.set(model, (arr = []));
|
|
697
|
+
arr.push(e);
|
|
698
|
+
if (arr.length > CAPTURE_MAX) arr.splice(0, arr.length - CAPTURE_MAX);
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
/** Copy of an entry with both bodies clamped to CAPTURE_BODY_MAX. */
|
|
702
|
+
private capped(entry: DebugCapture): DebugCapture {
|
|
678
703
|
const e = { ...entry };
|
|
679
704
|
if (e.request.length > CAPTURE_BODY_MAX) {
|
|
680
705
|
e.request = e.request.slice(0, CAPTURE_BODY_MAX);
|
|
@@ -684,23 +709,31 @@ export class Store {
|
|
|
684
709
|
e.response = e.response.slice(0, CAPTURE_BODY_MAX);
|
|
685
710
|
e.truncated = true;
|
|
686
711
|
}
|
|
687
|
-
|
|
688
|
-
if (!arr) this.captures.set(model, (arr = []));
|
|
689
|
-
arr.push(e);
|
|
690
|
-
if (arr.length > CAPTURE_MAX) arr.splice(0, arr.length - CAPTURE_MAX);
|
|
712
|
+
return e;
|
|
691
713
|
}
|
|
692
714
|
|
|
693
|
-
/** The model's captured attempts, newest first. */
|
|
715
|
+
/** The model's switch-captured attempts, newest first. */
|
|
694
716
|
getCaptures(model: string): DebugCapture[] {
|
|
695
717
|
const arr = this.captures.get(model);
|
|
696
718
|
return arr ? [...arr].reverse() : [];
|
|
697
719
|
}
|
|
698
720
|
|
|
699
|
-
/**
|
|
721
|
+
/** The model's always-recorded failed attempts (from the global net),
|
|
722
|
+
* newest first. */
|
|
723
|
+
getFailCaptures(model: string): DebugCapture[] {
|
|
724
|
+
return this.failCaptures.filter((c) => c.model === model).reverse();
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
/** Drop a model's switch-capture buffer (toggle-off, model delete/rename). */
|
|
700
728
|
clearCaptures(model: string): void {
|
|
701
729
|
this.captures.delete(model);
|
|
702
730
|
}
|
|
703
731
|
|
|
732
|
+
/** Scrub a model's entries from the global failure net (manual clear). */
|
|
733
|
+
clearFailCaptures(model: string): void {
|
|
734
|
+
this.failCaptures = this.failCaptures.filter((c) => c.model !== model);
|
|
735
|
+
}
|
|
736
|
+
|
|
704
737
|
/** Snapshot of every configured provider's circuit state for GET /admin/circuit.
|
|
705
738
|
* Healthy providers appear as state "open"; a provider deleted while cooling
|
|
706
739
|
* simply drops out (we iterate the live config, not the map). */
|
|
@@ -104,7 +104,9 @@ export interface ModelEntry {
|
|
|
104
104
|
* records every UPSTREAM ATTEMPT for this model — the exact forwarded request
|
|
105
105
|
* body and the response body — into an in-memory ring buffer (last 50, see
|
|
106
106
|
* Store.pushCapture). Turning it off clears the buffer; restarts clear it too.
|
|
107
|
-
*
|
|
107
|
+
* Independently of this switch, FAILED attempts always land in a global
|
|
108
|
+
* in-memory net (last 50 across all models) so an error can be inspected
|
|
109
|
+
* after the fact. Nothing here ever touches logs.jsonl. */
|
|
108
110
|
debugCapture?: boolean;
|
|
109
111
|
}
|
|
110
112
|
|