myapikey 0.46.0 → 0.47.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.46.0",
3
+ "version": "0.47.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": [
@@ -9,7 +9,7 @@ export class ApiError extends Error {
9
9
  export interface Ctx {
10
10
  url: string;
11
11
  auth: string; // Basic header value ("" if no account creds) — for /admin
12
- apiKey?: string; // Bearer token for /openai/v1 + /anthropic/v1
12
+ apiKey?: string; // Bearer token for /openai-chat/v1 + /openai-responses/v1 + /anthropic/v1
13
13
  }
14
14
 
15
15
  interface Opts {
@@ -34,11 +34,11 @@ export async function api<T = unknown>(
34
34
  path: string,
35
35
  body?: unknown,
36
36
  ): Promise<T> {
37
- // The two agent surfaces (/openai/v1, /anthropic/v1) take the API key
38
- // (Bearer); everything else (/admin) takes account Basic.
39
- const isProxy = path.startsWith("/openai/") || path.startsWith("/anthropic/");
37
+ // The agent surfaces (/openai-chat/v1, /openai-responses/v1, /anthropic/v1)
38
+ // take the API key (Bearer); everything else (/admin) takes account Basic.
39
+ const isProxy = path.startsWith("/openai-chat/") || path.startsWith("/openai-responses/") || path.startsWith("/anthropic/");
40
40
  if (isProxy && !ctx.apiKey) {
41
- throw new Error("No API key for /openai/v1 or /anthropic/v1. Run `myapikey serve`, set MYAPIKEY_API_KEY, or pass --api-key.");
41
+ throw new Error("No API key for /openai-chat/v1, /openai-responses/v1 or /anthropic/v1. Run `myapikey serve`, set MYAPIKEY_API_KEY, or pass --api-key.");
42
42
  }
43
43
  if (!isProxy && !ctx.auth) {
44
44
  throw new Error("No account credentials for /admin. Run `myapikey serve`, set MYAPIKEY_USER/MYAPIKEY_PASS, or pass --user/--pass.");
@@ -23,7 +23,7 @@ program
23
23
  .option("-u, --url <url>", "gateway base URL")
24
24
  .option("--user <user>", "account username")
25
25
  .option("--pass <pass>", "account password")
26
- .option("--api-key <key>", "api key for /openai/v1 + /anthropic/v1 (agent calls)")
26
+ .option("--api-key <key>", "api key for /openai-chat/v1 + /openai-responses/v1 + /anthropic/v1 (agent calls)")
27
27
  .hook("preAction", () => undefined);
28
28
 
29
29
  const ctx = (): ReturnType<typeof makeCtx> => makeCtx(program.opts<Globals>());
@@ -51,9 +51,9 @@ program
51
51
  console.log(`\n MyAPIKey listening on ${url}`);
52
52
  if (webDir) console.log(` web UI: ${url}`);
53
53
  else console.log(` web UI: not built (run: npm run build:web)`);
54
- console.log(` proxy: ${url}/openai/v1/chat/completions (OpenAI)`);
55
- console.log(` ${url}/openai/v1/responses (OpenAI Responses)`);
56
- console.log(` ${url}/anthropic/v1/messages (Anthropic)`);
54
+ console.log(` proxy: ${url}/openai-chat/v1/chat/completions (OpenAI chat)`);
55
+ console.log(` ${url}/openai-responses/v1/responses (OpenAI Responses)`);
56
+ console.log(` ${url}/anthropic/v1/messages (Anthropic)`);
57
57
  console.log(` data: ${dataDir} (override with --data-dir or MYAPIKEY_DATA_DIR)`);
58
58
  console.log(` log: ${store.getPaths().serverLogFile} (errors + failover/cooldown events; level via MYAPIKEY_LOG_LEVEL)\n`);
59
59
  store.getLogger().info(`gateway started on port ${info.port}, data=${dataDir}`);
@@ -95,7 +95,7 @@ program
95
95
  console.log(` login : ${profile.username} / ${profile.password} ← only for the web UI\n`);
96
96
  if (apiKey) {
97
97
  console.log("Example (OpenAI SDK):");
98
- console.log(` OPENAI_BASE_URL=${profile.url}/openai/v1 OPENAI_API_KEY=${apiKey}`);
98
+ console.log(` OPENAI_BASE_URL=${profile.url}/openai-chat/v1 OPENAI_API_KEY=${apiKey}`);
99
99
  console.log("\nExample (Claude Code / Anthropic):");
100
100
  console.log(` ANTHROPIC_BASE_URL=${profile.url}/anthropic ANTHROPIC_API_KEY=${apiKey}`);
101
101
  }
@@ -378,7 +378,7 @@ program
378
378
  const prompt = promptParts.join(" ").trim();
379
379
  const input = prompt || (await readStdin());
380
380
  if (!input) return console.log("Provide a prompt: myapikey call <model> hello");
381
- const r = (await api(ctx(), "POST", "/openai/v1/chat/completions", {
381
+ const r = (await api(ctx(), "POST", "/openai-chat/v1/chat/completions", {
382
382
  model: modelName,
383
383
  messages: [{ role: "user", content: input }],
384
384
  })) as any;
@@ -194,7 +194,7 @@ function projectModel(name: string, e: ModelEntry, byId: Map<string, Provider>)
194
194
  };
195
195
  }
196
196
 
197
- export function adminApi(store: Store, auth: MiddlewareHandler, openai: Hono, anthropic: Hono): Hono {
197
+ export function adminApi(store: Store, auth: MiddlewareHandler, chat: Hono, responses: Hono, anthropic: Hono): Hono {
198
198
  const app = new Hono();
199
199
  app.use("*", auth);
200
200
 
@@ -927,10 +927,10 @@ app.delete("/models/:name/debug/fails", (c) => {
927
927
  if (!format) {
928
928
  return c.json({ result: { ok: false, status: 0, format: "openai", error: "model not enabled on any routing slot" } });
929
929
  }
930
- // Route the loopback to the matching surface: anthropic the anthropic
931
- // sub-app (/messages); openai/responses → the openai sub-app (the openai
932
- // family lives there, including /responses).
933
- const sub = format === "anthropic" ? anthropic : openai;
930
+ // Route the loopback to the matching surface sub-app: each protocol has
931
+ // its own surface now (anthropic → /messages, responses → /responses,
932
+ // openai /chat/completions).
933
+ const sub = format === "anthropic" ? anthropic : format === "responses" ? responses : chat;
934
934
  const path = format === "anthropic" ? "/messages" : format === "responses" ? "/responses" : "/chat/completions";
935
935
  // /responses is the OpenAI Responses API — it takes `input`, not `messages`.
936
936
  const body =
@@ -989,7 +989,7 @@ app.delete("/models/:name/debug/fails", (c) => {
989
989
  if (!provider || !providerSpeaks(provider, format)) {
990
990
  return c.json({ result: { ok: false, status: 0, format, error: "source does not speak this format" } });
991
991
  }
992
- const sub = format === "anthropic" ? anthropic : openai;
992
+ const sub = format === "anthropic" ? anthropic : format === "responses" ? responses : chat;
993
993
  const path = format === "anthropic" ? "/messages" : format === "responses" ? "/responses" : "/chat/completions";
994
994
  // /responses is the OpenAI Responses API — it takes `input`, not `messages`.
995
995
  const body =
@@ -34,12 +34,14 @@ export function createApp(store: Store, opts: AppOptions = {}): Hono {
34
34
  const apiKeyAuth = apiKeyMiddleware(() => store.get().apiKey, logger);
35
35
 
36
36
  // Both sub-apps require auth, applied inside each sub-app (before routes).
37
- // Two agent surfaces, each with its own /models: /openai/v1 (openai family)
38
- // and /anthropic/v1 (anthropic family). Both gate on the same API key.
39
- const { openai, anthropic } = proxyApi(store, apiKeyAuth);
40
- const admin = adminApi(store, accountAuth, openai, anthropic);
37
+ // Three agent surfaces — one per protocol, each with its own /models:
38
+ // /openai-chat/v1 (openai chat/completions), /openai-responses/v1 (openai
39
+ // responses), /anthropic/v1 (anthropic messages). All gate on the same API key.
40
+ const { chat, responses, anthropic } = proxyApi(store, apiKeyAuth);
41
+ const admin = adminApi(store, accountAuth, chat, responses, anthropic);
41
42
 
42
- app.route("/openai/v1", openai);
43
+ app.route("/openai-chat/v1", chat);
44
+ app.route("/openai-responses/v1", responses);
43
45
  app.route("/anthropic/v1", anthropic);
44
46
  app.route("/admin", admin);
45
47
 
@@ -49,13 +51,18 @@ export function createApp(store: Store, opts: AppOptions = {}): Hono {
49
51
  // as "Unexpected token '<'" (this bit pi's model refresh once).
50
52
  const apiMiss = (c: Context) =>
51
53
  c.json({ error: { message: `no such endpoint: ${c.req.method} ${c.req.path}`, type: "invalid_request_error" } }, 404);
52
- app.all("/openai/*", apiMiss);
54
+ app.all("/openai-chat/*", apiMiss);
55
+ app.all("/openai-responses/*", apiMiss);
53
56
  app.all("/anthropic/*", apiMiss);
54
57
  app.all("/admin/*", apiMiss);
55
- // Legacy pre-0.12 surface: gone since the split point the caller at the two
58
+ // Legacy pre-0.47 openai surface: the two openai families now have their own
59
+ // prefixes — point the caller there instead of a bare 404.
60
+ app.all("/openai/*", (c) =>
61
+ c.json({ error: { message: "the /openai/v1 surface was split — use /openai-chat/v1 or /openai-responses/v1", type: "invalid_request_error" } }, 404));
62
+ // Legacy pre-0.12 surface: gone since the split — point the caller at the
56
63
  // current surfaces instead of a bare 404.
57
64
  app.all("/v1/*", (c) =>
58
- c.json({ error: { message: "the /v1 surface was split in v0.12.0 — use /openai/v1 or /anthropic/v1", type: "invalid_request_error" } }, 404));
65
+ c.json({ error: { message: "the /v1 surface was split in v0.12.0 — use /openai-chat/v1, /openai-responses/v1 or /anthropic/v1", type: "invalid_request_error" } }, 404));
59
66
 
60
67
  // Web UI: serve built SPA when available.
61
68
  if (opts.webDir && existsSync(opts.webDir)) {
@@ -83,7 +90,7 @@ export function createApp(store: Store, opts: AppOptions = {}): Hono {
83
90
  } else {
84
91
  app.get("*", (c) =>
85
92
  c.text(
86
- "MyAPIKey is running. Web UI not built — run `npm run build:web`. API at /openai/v1 + /anthropic/v1 (proxy) and /admin (config).",
93
+ "MyAPIKey is running. Web UI not built — run `npm run build:web`. API at /openai-chat/v1 + /openai-responses/v1 + /anthropic/v1 (proxy) and /admin (config).",
87
94
  404,
88
95
  ),
89
96
  );
@@ -1,6 +1,6 @@
1
1
  import { Hono, type Context, type MiddlewareHandler } from "hono";
2
2
  import { trimBase } from "../shared/config";
3
- import type { DebugCapture, Format, Provider, RouteKey, Usage } from "../shared/types";
3
+ import type { DebugCapture, Format, FormatEntry, Provider, RouteKey, Usage } from "../shared/types";
4
4
  import { CAPTURE_BODY_MAX, type Store } from "./store";
5
5
  import { UsageCollector } from "./tokens";
6
6
 
@@ -494,44 +494,58 @@ function observedBody(
494
494
  /** Model list of the models enabled on ONE routing family's slot. Each agent
495
495
  * surface gets its own `/models` so a client listing models never picks an id
496
496
  * that 404s on that surface's call endpoint — and each answers in its own
497
- * ecosystem's list shape: openai `{object:"list", data:[{id, owned_by}]}` vs
498
- * anthropic `{data:[{id, display_name}], first_id, last_id, has_more}`. We
499
- * can't know real created_at / capabilities, so the anthropic shape carries
500
- * only the honest minimal fields rather than fabricating them. */
501
- function modelsList(c: Context, store: Store, fmt: "openai" | "anthropic") {
497
+ * ecosystem's list shape: the two openai-family surfaces `{object:"list",
498
+ * data:[{id, owned_by}]}` vs anthropic `{data:[{id, display_name}],
499
+ * first_id, last_id, has_more}`. We can't know real created_at / capabilities,
500
+ * so the anthropic shape carries only the honest minimal fields rather than
501
+ * fabricating them. Each surface lists exactly what dispatch would route on
502
+ * it: the entry's chain for that RouteKey must be enabled (and, for
503
+ * responses, keep at least one slot on a supportsResponses source). */
504
+ function modelsList(c: Context, store: Store, key: RouteKey) {
502
505
  const d = store.get();
503
506
  const byId = new Map(d.providers.map((p) => [p.id, p]));
504
- const enabled = Object.entries(d.models).filter(([, e]) => e[fmt].enabled);
505
- if (fmt === "anthropic") {
507
+ const enabled = Object.entries(d.models).filter(([, e]) => e[key]?.enabled);
508
+ if (key === "anthropic") {
506
509
  const data = enabled.map(([id]) => ({ id, display_name: id, created_at: "1970-01-01T00:00:00Z", type: "model" }));
507
510
  return c.json({ data, first_id: data[0]?.id ?? null, last_id: data.at(-1)?.id ?? null, has_more: false });
508
511
  }
509
- const data = enabled.map(([id, e]) => ({
510
- id,
511
- object: "model",
512
- created: 0,
513
- owned_by: byId.get(e.openai.providers[0]?.id ?? "")?.name || "MyAPIKey",
514
- }));
512
+ const routable = (e: FormatEntry) =>
513
+ key === "responses" ? e.providers.some((s) => byId.get(s.id)?.supportsResponses) : true;
514
+ // owned_by follows the first slot dispatch would actually use on this surface.
515
+ const firstSlot = (e: FormatEntry) =>
516
+ key === "responses" ? e.providers.find((s) => byId.get(s.id)?.supportsResponses) : e.providers[0];
517
+ const data = enabled
518
+ .filter(([, e]) => e[key] && routable(e[key]))
519
+ .map(([id, e]) => ({
520
+ id,
521
+ object: "model",
522
+ created: 0,
523
+ owned_by: byId.get(firstSlot(e[key])?.id ?? "")?.name || "MyAPIKey",
524
+ }));
515
525
  return c.json({ object: "list", data });
516
526
  }
517
527
 
518
- /** The two agent surfaces as separate sub-apps, so each carries its own
519
- * `/models` (openai list vs anthropic list) under its own prefix. `dispatch`
520
- * is shared it's keyed by RouteKey, surface-agnostic. */
528
+ /** The three agent surfaces as separate sub-apps (one per protocol family:
529
+ * openai chat/completions, openai responses, anthropic messages), so each
530
+ * carries its own `/models` under its own prefix. `dispatch` is shared —
531
+ * it's keyed by RouteKey, surface-agnostic. */
521
532
  export function proxyApi(
522
533
  store: Store,
523
534
  auth: MiddlewareHandler,
524
- ): { openai: Hono; anthropic: Hono } {
525
- const openai = new Hono();
535
+ ): { chat: Hono; responses: Hono; anthropic: Hono } {
536
+ const chat = new Hono();
537
+ const responses = new Hono();
526
538
  const anthropic = new Hono();
527
539
  // GET /models is a PUBLIC discovery read — no api key required. It returns only
528
540
  // the enabled model names (like /health), so an agent or a quick curl can see
529
541
  // what each surface offers before wiring up auth. Registered BEFORE the auth
530
542
  // middleware so it isn't gated: Hono only runs middleware on routes registered
531
543
  // after it.
532
- openai.get("/models", (c) => modelsList(c, store, "openai"));
544
+ chat.get("/models", (c) => modelsList(c, store, "openai"));
545
+ responses.get("/models", (c) => modelsList(c, store, "responses"));
533
546
  anthropic.get("/models", (c) => modelsList(c, store, "anthropic"));
534
- openai.use("*", auth);
547
+ chat.use("*", auth);
548
+ responses.use("*", auth);
535
549
  anthropic.use("*", auth);
536
550
 
537
551
  /** Shared dispatch with failover. `key` selects the routing slot (and thus the
@@ -581,7 +595,7 @@ export function proxyApi(
581
595
  return c.json(
582
596
  {
583
597
  error: {
584
- message: `model '${model}' is not enabled for /responses — enable it on a source marked "supports responses"`,
598
+ message: `model '${model}' is not enabled for /openai-responses/v1/responses — enable it on a source marked "supports responses"`,
585
599
  type: "invalid_request_error",
586
600
  code: "model_not_found",
587
601
  },
@@ -865,14 +879,14 @@ export function proxyApi(
865
879
  }
866
880
  };
867
881
 
868
- // OpenAI surface: chat/completions + responses (/models is registered above,
869
- // before the auth middleware, so it stays public).
870
- openai.post("/chat/completions", (c) => dispatch(c, "openai"));
882
+ // One call endpoint per surface (/models is registered above, before the
883
+ // auth middleware, so it stays public).
884
+ chat.post("/chat/completions", (c) => dispatch(c, "openai"));
871
885
  // OpenAI Responses API — its own routing slot (sources must be supportsResponses).
872
- openai.post("/responses", (c) => dispatch(c, "responses"));
886
+ responses.post("/responses", (c) => dispatch(c, "responses"));
873
887
 
874
- // Anthropic surface: messages (/models likewise registered above, public).
888
+ // Anthropic surface: messages.
875
889
  anthropic.post("/messages", (c) => dispatch(c, "anthropic"));
876
890
 
877
- return { openai, anthropic };
891
+ return { chat, responses, anthropic };
878
892
  }