myapikey 0.11.1 → 0.12.1

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
@@ -124,7 +124,7 @@ myapikey model prioritize gpt-4o-mini openai-direct backup --format openai # l
124
124
  myapikey model list # see the routing table
125
125
  ```
126
126
 
127
- > `--format` selects the routing **slot**: `openai` (for `/v1/chat/completions`), `anthropic` (for `/v1/messages`), or `responses` (for `/v1/responses`). The `responses` slot only accepts backends you've marked **supportsResponses** — set that toggle in the web UI (the CLI doesn't expose it yet).
127
+ > `--format` selects the routing **slot**: `openai` (for `/openai/v1/chat/completions`), `anthropic` (for `/anthropic/v1/messages`), or `responses` (for `/openai/v1/responses`). The `responses` slot only accepts backends you've marked **supportsResponses** — set that toggle in the web UI (the CLI doesn't expose it yet).
128
128
 
129
129
  ---
130
130
 
@@ -139,14 +139,14 @@ myapikey whoami # prints base url + api key + ready-to-paste env lines
139
139
  For an **OpenAI-compatible** tool (covers `/chat/completions` *and* `/responses`):
140
140
 
141
141
  ```bash
142
- export OPENAI_BASE_URL=http://localhost:7800/v1
142
+ export OPENAI_BASE_URL=http://localhost:7800/openai/v1
143
143
  export OPENAI_API_KEY=<gateway api key>
144
144
  ```
145
145
 
146
146
  For **Anthropic / Claude Code**:
147
147
 
148
148
  ```bash
149
- export ANTHROPIC_BASE_URL=http://localhost:7800
149
+ export ANTHROPIC_BASE_URL=http://localhost:7800/anthropic
150
150
  export ANTHROPIC_API_KEY=<gateway api key>
151
151
  ```
152
152
 
@@ -158,6 +158,22 @@ Quick smoke test without any tool:
158
158
  myapikey call gpt-4o-mini "Say hello in one sentence."
159
159
  ```
160
160
 
161
+ **Why two base URLs?** The gateway exposes two separate agent surfaces — `/openai/v1` and `/anthropic/v1` — each with its own `GET /models` (an OpenAI client discovers only openai-enabled models, an Anthropic client only anthropic-enabled ones). Each ecosystem's SDK appends its own paths, so the OpenAI SDK points at `…/openai/v1` (it appends `/chat/completions`, `/responses`, `/models`) and the Anthropic SDK / Claude Code points at `…/anthropic` (it appends `/v1/messages`, `/v1/models`).
162
+
163
+ **Raw HTTP** (no SDK) — hit either surface directly with the gateway API key as a `Bearer` token:
164
+
165
+ ```bash
166
+ # OpenAI family
167
+ curl http://localhost:7800/openai/v1/chat/completions \
168
+ -H "Authorization: Bearer <gateway api key>" -H "Content-Type: application/json" \
169
+ -d '{"model":"<model>","messages":[{"role":"user","content":"hi"}]}'
170
+
171
+ # Anthropic family
172
+ curl http://localhost:7800/anthropic/v1/messages \
173
+ -H "Authorization: Bearer <gateway api key>" -H "Content-Type: application/json" \
174
+ -d '{"model":"<model>","max_tokens":16,"messages":[{"role":"user","content":"hi"}]}'
175
+ ```
176
+
161
177
  Whichever endpoint you point a tool at, the gateway forwards in that format and never translates — so make sure each model you call is backed by at least one source on the matching slot ([see below](#how-routing-works)).
162
178
 
163
179
  ---
@@ -166,7 +182,7 @@ Whichever endpoint you point a tool at, the gateway forwards in that format and
166
182
 
167
183
  The gateway is a **directional forwarder, not a translator**. Four rules explain everything:
168
184
 
169
- 1. **The endpoint picks the slot.** `/v1/chat/completions` → the **openai** slot. `/v1/responses` → the **responses** slot. `/v1/messages` → the **anthropic** slot. Each is a distinct wire format, and the body is forwarded verbatim — nothing is converted.
185
+ 1. **The endpoint picks the slot.** `/openai/v1/chat/completions` → the **openai** slot. `/openai/v1/responses` → the **responses** slot. `/anthropic/v1/messages` → the **anthropic** slot. Each is a distinct wire format, and the body is forwarded verbatim — nothing is converted.
170
186
 
171
187
  2. **Each model has three independent slots.** For one model you can enable `openai`, `responses`, and `anthropic` separately, and each has its own ordered source chain. Enable a model only on the slots its backends actually speak.
172
188
 
@@ -235,12 +251,13 @@ It's the same admin API the CLI uses — configure either way.
235
251
 
236
252
  ## API surface
237
253
 
238
- **Agent-facing (`/v1`, API key — `Authorization: Bearer` or `x-api-key`):**
254
+ **Agent-facing (two surfaces, one API key — `Authorization: Bearer` or `x-api-key`):**
239
255
 
240
- - `POST /v1/chat/completions` — OpenAI-format proxy
241
- - `POST /v1/responses` — OpenAI Responses API (only sources marked `supportsResponses`)
242
- - `POST /v1/messages` — Anthropic-format proxy
243
- - `GET /v1/models` — models enabled on the **openai** slot, OpenAI list shape (Anthropic-only models aren't listed here — call them via `/v1/messages`)
256
+ - `POST /openai/v1/chat/completions` — OpenAI-format proxy
257
+ - `POST /openai/v1/responses` — OpenAI Responses API (only sources marked `supportsResponses`)
258
+ - `GET /openai/v1/models` — models enabled on the **openai** slot, OpenAI list shape
259
+ - `POST /anthropic/v1/messages` — Anthropic-format proxy
260
+ - `GET /anthropic/v1/models` — models enabled on the **anthropic** slot, OpenAI list shape
244
261
  - `GET /health` — public liveness check
245
262
 
246
263
  **Admin (`/admin`, account password — HTTP Basic):**
@@ -279,7 +296,7 @@ npm install
279
296
  npm run build:web # build the Vue UI into packages/web/dist (one time, and after UI changes)
280
297
  npm start # tsx ... serve (gateway on :7800)
281
298
  npm run dev # gateway, with watch reload
282
- npm run dev:web # vite dev server (proxies /v1 and /admin to :7800)
299
+ npm run dev:web # vite dev server (proxies /openai, /anthropic, and /admin to :7800)
283
300
  npm run typecheck # tsc (core) + vue-tsc (web)
284
301
  ```
285
302
 
package/README.zh-CN.md CHANGED
@@ -124,7 +124,7 @@ myapikey model prioritize gpt-4o-mini openai-direct backup --format openai #
124
124
  myapikey model list # 查看路由表
125
125
  ```
126
126
 
127
- > `--format` 选择路由**槽位**:`openai`(对应 `/v1/chat/completions`)、`anthropic`(对应 `/v1/messages`)、或 `responses`(对应 `/v1/responses`)。`responses` 槽位只接受你标记了 **supportsResponses** 的后端——在网页界面里勾选那个开关(CLI 暂未暴露它)。
127
+ > `--format` 选择路由**槽位**:`openai`(对应 `/openai/v1/chat/completions`)、`anthropic`(对应 `/anthropic/v1/messages`)、或 `responses`(对应 `/openai/v1/responses`)。`responses` 槽位只接受你标记了 **supportsResponses** 的后端——在网页界面里勾选那个开关(CLI 暂未暴露它)。
128
128
 
129
129
  ---
130
130
 
@@ -139,14 +139,14 @@ myapikey whoami # 打印 base url + api key + 可直接粘贴的环境变
139
139
  **兼容 OpenAI** 的工具(覆盖 `/chat/completions` *和* `/responses`):
140
140
 
141
141
  ```bash
142
- export OPENAI_BASE_URL=http://localhost:7800/v1
142
+ export OPENAI_BASE_URL=http://localhost:7800/openai/v1
143
143
  export OPENAI_API_KEY=<网关 API Key>
144
144
  ```
145
145
 
146
146
  **Anthropic / Claude Code**:
147
147
 
148
148
  ```bash
149
- export ANTHROPIC_BASE_URL=http://localhost:7800
149
+ export ANTHROPIC_BASE_URL=http://localhost:7800/anthropic
150
150
  export ANTHROPIC_API_KEY=<网关 API Key>
151
151
  ```
152
152
 
@@ -158,6 +158,22 @@ export ANTHROPIC_API_KEY=<网关 API Key>
158
158
  myapikey call gpt-4o-mini "用一句话打个招呼。"
159
159
  ```
160
160
 
161
+ **为什么有两个 Base URL?** 网关暴露了两个互相独立的 agent 面——`/openai/v1` 和 `/anthropic/v1`,各自带自己的 `GET /models`(OpenAI 客户端只发现 openai 槽位启用的模型,Anthropic 客户端只发现 anthropic 槽位启用的模型)。两套生态的 SDK 各自拼接自己的路径:OpenAI SDK 指向 `…/openai/v1`(它自己补 `/chat/completions`、`/responses`、`/models`),Anthropic SDK / Claude Code 指向 `…/anthropic`(它自己补 `/v1/messages`、`/v1/models`)。
162
+
163
+ **直接用 HTTP**(不走 SDK)——带上网关 API Key(`Bearer`)直接打这两个面之一:
164
+
165
+ ```bash
166
+ # OpenAI 系
167
+ curl http://localhost:7800/openai/v1/chat/completions \
168
+ -H "Authorization: Bearer <网关 API Key>" -H "Content-Type: application/json" \
169
+ -d '{"model":"<模型名>","messages":[{"role":"user","content":"hi"}]}'
170
+
171
+ # Anthropic 系
172
+ curl http://localhost:7800/anthropic/v1/messages \
173
+ -H "Authorization: Bearer <网关 API Key>" -H "Content-Type: application/json" \
174
+ -d '{"model":"<模型名>","max_tokens":16,"messages":[{"role":"user","content":"hi"}]}'
175
+ ```
176
+
161
177
  不管你把工具指向哪个端点,网关都按那个格式转发、绝不翻译——所以确保你调用的每个模型,在对应的槽位上至少有一个来源在服务它([见下文](#路由是怎么工作的))。
162
178
 
163
179
  ---
@@ -166,7 +182,7 @@ myapikey call gpt-4o-mini "用一句话打个招呼。"
166
182
 
167
183
  网关是一个**定向转发器,而非翻译器**。四条规则解释一切:
168
184
 
169
- 1. **端点决定槽位。** `/v1/chat/completions` → **openai** 槽位;`/v1/responses` → **responses** 槽位;`/v1/messages` → **anthropic** 槽位。每种都是独立的线材格式,请求体原样转发,不做任何转换。
185
+ 1. **端点决定槽位。** `/openai/v1/chat/completions` → **openai** 槽位;`/openai/v1/responses` → **responses** 槽位;`/anthropic/v1/messages` → **anthropic** 槽位。每种都是独立的线材格式,请求体原样转发,不做任何转换。
170
186
 
171
187
  2. **每个模型有三个互相独立的槽位。** 对同一个模型,你可以分别启用 `openai`、`responses`、`anthropic`,每个槽位都有自己的、按优先级排序的来源链。只在它的后端真正支持的槽位上启用即可。
172
188
 
@@ -235,12 +251,13 @@ myapikey call gpt-4o-mini "用一句话打个招呼。"
235
251
 
236
252
  ## 接口一览
237
253
 
238
- **面向 agent(`/v1`,API Key —— `Authorization: Bearer` 或 `x-api-key`):**
254
+ **面向 agent(两个面,共用一把 API Key —— `Authorization: Bearer` 或 `x-api-key`):**
239
255
 
240
- - `POST /v1/chat/completions` — OpenAI 格式代理
241
- - `POST /v1/responses` — OpenAI Responses API(仅限标记了 `supportsResponses` 的来源)
242
- - `POST /v1/messages` — Anthropic 格式代理
243
- - `GET /v1/models` — 启用在 **openai** 槽位的模型,OpenAI 列表格式(纯 Anthropic 模型不会出现在这里——请通过 `/v1/messages` 调用)
256
+ - `POST /openai/v1/chat/completions` — OpenAI 格式代理
257
+ - `POST /openai/v1/responses` — OpenAI Responses API(仅限标记了 `supportsResponses` 的来源)
258
+ - `GET /openai/v1/models` — 启用在 **openai** 槽位的模型,OpenAI 列表格式
259
+ - `POST /anthropic/v1/messages` — Anthropic 格式代理
260
+ - `GET /anthropic/v1/models` — 启用在 **anthropic** 槽位的模型,OpenAI 列表格式
244
261
  - `GET /health` — 公开存活检查
245
262
 
246
263
  **管理(`/admin`,账号密码 —— HTTP Basic):**
@@ -279,7 +296,7 @@ npm install
279
296
  npm run build:web # 把 Vue 界面构建到 packages/web/dist(首次,以及每次改完界面后)
280
297
  npm start # tsx ... serve(网关在 :7800)
281
298
  npm run dev # 网关,带 watch 热重载
282
- npm run dev:web # vite 开发服务器(把 /v1 和 /admin 代理到 :7800)
299
+ npm run dev:web # vite 开发服务器(把 /openai、/anthropic、/admin 代理到 :7800)
283
300
  npm run typecheck # tsc(core)+ vue-tsc(web)
284
301
  ```
285
302
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "myapikey",
3
- "version": "0.11.1",
3
+ "version": "0.12.1",
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 /v1
12
+ apiKey?: string; // Bearer token for /openai/v1 + /anthropic/v1
13
13
  }
14
14
 
15
15
  interface Opts {
@@ -34,18 +34,19 @@ export async function api<T = unknown>(
34
34
  path: string,
35
35
  body?: unknown,
36
36
  ): Promise<T> {
37
- const isV1 = path.startsWith("/v1");
38
- // /v1 takes the API key (Bearer); everything else (/admin) takes account Basic.
39
- if (isV1 && !ctx.apiKey) {
40
- throw new Error("No API key for /v1. Run `myapikey serve`, set MYAPIKEY_API_KEY, or pass --api-key.");
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/");
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
42
  }
42
- if (!isV1 && !ctx.auth) {
43
+ if (!isProxy && !ctx.auth) {
43
44
  throw new Error("No account credentials for /admin. Run `myapikey serve`, set MYAPIKEY_USER/MYAPIKEY_PASS, or pass --user/--pass.");
44
45
  }
45
46
  const res = await fetch(`${ctx.url}${path}`, {
46
47
  method,
47
48
  headers: {
48
- authorization: isV1 ? `Bearer ${ctx.apiKey}` : ctx.auth,
49
+ authorization: isProxy ? `Bearer ${ctx.apiKey}` : ctx.auth,
49
50
  ...(body ? { "content-type": "application/json" } : {}),
50
51
  },
51
52
  body: body ? JSON.stringify(body) : undefined,
@@ -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 /v1 (agent calls)")
26
+ .option("--api-key <key>", "api key for /openai/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}/v1/chat/completions (OpenAI)`);
55
- console.log(` ${url}/v1/responses (OpenAI Responses)`);
56
- console.log(` ${url}/v1/messages (Anthropic)`);
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)`);
57
57
  console.log(` data: ${dataDir} (override with --data-dir or MYAPIKEY_DATA_DIR)\n`);
58
58
 
59
59
  if (firstRun) {
@@ -93,9 +93,9 @@ program
93
93
  console.log(` login : ${profile.username} / ${profile.password} ← only for the web UI\n`);
94
94
  if (apiKey) {
95
95
  console.log("Example (OpenAI SDK):");
96
- console.log(` OPENAI_BASE_URL=${profile.url}/v1 OPENAI_API_KEY=${apiKey}`);
96
+ console.log(` OPENAI_BASE_URL=${profile.url}/openai/v1 OPENAI_API_KEY=${apiKey}`);
97
97
  console.log("\nExample (Claude Code / Anthropic):");
98
- console.log(` ANTHROPIC_BASE_URL=${profile.url} ANTHROPIC_API_KEY=${apiKey}`);
98
+ console.log(` ANTHROPIC_BASE_URL=${profile.url}/anthropic ANTHROPIC_API_KEY=${apiKey}`);
99
99
  }
100
100
  });
101
101
 
@@ -267,7 +267,7 @@ program
267
267
  const prompt = promptParts.join(" ").trim();
268
268
  const input = prompt || (await readStdin());
269
269
  if (!input) return console.log("Provide a prompt: myapikey call <model> hello");
270
- const r = (await api(ctx(), "POST", "/v1/chat/completions", {
270
+ const r = (await api(ctx(), "POST", "/openai/v1/chat/completions", {
271
271
  model: modelName,
272
272
  messages: [{ role: "user", content: input }],
273
273
  })) as any;
@@ -141,7 +141,7 @@ async function refreshDiscovery(store: Store, id: string): Promise<string[]> {
141
141
  return failed ? (store.get().providers.find((x) => x.id === id)?.discoveredModels ?? []) : models;
142
142
  }
143
143
 
144
- export function adminApi(store: Store, auth: MiddlewareHandler, v1: Hono): Hono {
144
+ export function adminApi(store: Store, auth: MiddlewareHandler, openai: Hono, anthropic: Hono): Hono {
145
145
  const app = new Hono();
146
146
  app.use("*", auth);
147
147
 
@@ -526,6 +526,10 @@ export function adminApi(store: Store, auth: MiddlewareHandler, v1: Hono): Hono
526
526
  if (!format) {
527
527
  return c.json({ result: { ok: false, status: 0, format: "openai", error: "model not enabled on any routing slot" } });
528
528
  }
529
+ // Route the loopback to the matching surface: anthropic → the anthropic
530
+ // sub-app (/messages); openai/responses → the openai sub-app (the openai
531
+ // family lives there, including /responses).
532
+ const sub = format === "anthropic" ? anthropic : openai;
529
533
  const path = format === "anthropic" ? "/messages" : format === "responses" ? "/responses" : "/chat/completions";
530
534
  // /responses is the OpenAI Responses API — it takes `input`, not `messages`.
531
535
  const body =
@@ -534,7 +538,7 @@ export function adminApi(store: Store, auth: MiddlewareHandler, v1: Hono): Hono
534
538
  : { model: name, messages: [{ role: "user", content: "ping" }], max_tokens: 1, stream: false };
535
539
  let res: Response;
536
540
  try {
537
- res = await v1.request(path, {
541
+ res = await sub.request(path, {
538
542
  method: "POST",
539
543
  headers: { "content-type": "application/json", authorization: `Bearer ${cfg.apiKey}`, "x-myapikey-probe": "1" },
540
544
  body: JSON.stringify(body),
@@ -584,6 +588,7 @@ export function adminApi(store: Store, auth: MiddlewareHandler, v1: Hono): Hono
584
588
  if (!provider || !providerSpeaks(provider, format)) {
585
589
  return c.json({ result: { ok: false, status: 0, format, error: "source does not speak this format" } });
586
590
  }
591
+ const sub = format === "anthropic" ? anthropic : openai;
587
592
  const path = format === "anthropic" ? "/messages" : format === "responses" ? "/responses" : "/chat/completions";
588
593
  // /responses is the OpenAI Responses API — it takes `input`, not `messages`.
589
594
  const body =
@@ -592,7 +597,7 @@ export function adminApi(store: Store, auth: MiddlewareHandler, v1: Hono): Hono
592
597
  : { model: name, messages: [{ role: "user", content: "ping" }], max_tokens: 1, stream: false };
593
598
  let res: Response;
594
599
  try {
595
- res = await v1.request(path, {
600
+ res = await sub.request(path, {
596
601
  method: "POST",
597
602
  headers: { "content-type": "application/json", authorization: `Bearer ${cfg.apiKey}`, "x-myapikey-probe": "1", "x-myapikey-probe-slot": String(index) },
598
603
  body: JSON.stringify(body),
@@ -26,10 +26,13 @@ export function createApp(store: Store, opts: AppOptions = {}): Hono {
26
26
  const apiKeyAuth = apiKeyMiddleware(() => store.get().apiKey);
27
27
 
28
28
  // Both sub-apps require auth, applied inside each sub-app (before routes).
29
- const v1 = proxyApi(store, apiKeyAuth);
30
- const admin = adminApi(store, accountAuth, v1);
29
+ // Two agent surfaces, each with its own /models: /openai/v1 (openai family)
30
+ // and /anthropic/v1 (anthropic family). Both gate on the same API key.
31
+ const { openai, anthropic } = proxyApi(store, apiKeyAuth);
32
+ const admin = adminApi(store, accountAuth, openai, anthropic);
31
33
 
32
- app.route("/v1", v1);
34
+ app.route("/openai/v1", openai);
35
+ app.route("/anthropic/v1", anthropic);
33
36
  app.route("/admin", admin);
34
37
 
35
38
  // Web UI: serve built SPA when available.
@@ -45,7 +48,7 @@ export function createApp(store: Store, opts: AppOptions = {}): Hono {
45
48
  } else {
46
49
  app.get("*", (c) =>
47
50
  c.text(
48
- "MyAPIKey is running. Web UI not built — run `npm run build:web`. API at /v1 (proxy) and /admin (config).",
51
+ "MyAPIKey is running. Web UI not built — run `npm run build:web`. API at /openai/v1 + /anthropic/v1 (proxy) and /admin (config).",
49
52
  404,
50
53
  ),
51
54
  );
@@ -293,9 +293,35 @@ function observedBody(
293
293
  });
294
294
  }
295
295
 
296
- export function proxyApi(store: Store, auth: MiddlewareHandler): Hono {
297
- const app = new Hono();
298
- app.use("*", auth);
296
+ /** OpenAI-style model list of the models enabled on ONE routing family's slot.
297
+ * Each agent surface gets its own `/models` so a client listing models never
298
+ * picks an id that 404s on that surface's call endpoint: `/openai/v1/models`
299
+ * advertises the openai slot, `/anthropic/v1/models` the anthropic slot. */
300
+ function modelsList(c: Context, store: Store, fmt: "openai" | "anthropic") {
301
+ const d = store.get();
302
+ const byId = new Map(d.providers.map((p) => [p.id, p]));
303
+ const data = Object.entries(d.models)
304
+ .filter(([, e]) => e[fmt].enabled)
305
+ .map(([id, e]) => ({
306
+ id,
307
+ object: "model",
308
+ created: 0,
309
+ owned_by: byId.get(e[fmt].providers[0]?.id ?? "")?.name || "MyAPIKey",
310
+ }));
311
+ return c.json({ object: "list", data });
312
+ }
313
+
314
+ /** The two agent surfaces as separate sub-apps, so each carries its own
315
+ * `/models` (openai list vs anthropic list) under its own prefix. `dispatch`
316
+ * is shared — it's keyed by RouteKey, surface-agnostic. */
317
+ export function proxyApi(
318
+ store: Store,
319
+ auth: MiddlewareHandler,
320
+ ): { openai: Hono; anthropic: Hono } {
321
+ const openai = new Hono();
322
+ const anthropic = new Hono();
323
+ openai.use("*", auth);
324
+ anthropic.use("*", auth);
299
325
 
300
326
  /** Shared dispatch with failover. `key` selects the routing slot (and thus the
301
327
  * candidate chain); `wire`/`path` derive from it for the upstream call. */
@@ -473,28 +499,18 @@ export function proxyApi(store: Store, auth: MiddlewareHandler): Hono {
473
499
  );
474
500
  };
475
501
 
476
- app.post("/chat/completions", (c) => dispatch(c, "openai"));
477
- app.post("/messages", (c) => dispatch(c, "anthropic"));
502
+ // OpenAI surface: chat/completions + responses, plus its own (openai-slot)
503
+ // model list.
504
+ openai.post("/chat/completions", (c) => dispatch(c, "openai"));
478
505
  // OpenAI Responses API — its own routing slot (sources must be supportsResponses).
479
- app.post("/responses", (c) => dispatch(c, "responses"));
506
+ openai.post("/responses", (c) => dispatch(c, "responses"));
507
+ openai.get("/models", (c) => modelsList(c, store, "openai"));
480
508
 
481
- // OpenAI-style model list of everything routable on the OpenAI path. This is
482
- // the OpenAI list endpoint advertise only models whose openai slot is
483
- // enabled, so an agent that picks an id here can actually call it on
484
- // /chat/completions. Anthropic-only models are intentionally omitted.
485
- app.get("/models", (c) => {
486
- const d = store.get();
487
- const byId = new Map(d.providers.map((p) => [p.id, p]));
488
- const data = Object.entries(d.models)
489
- .filter(([, e]) => e.openai.enabled)
490
- .map(([id, e]) => ({
491
- id,
492
- object: "model",
493
- created: 0,
494
- owned_by: byId.get(e.openai.providers[0]?.id ?? "")?.name || "MyAPIKey",
495
- }));
496
- return c.json({ object: "list", data });
497
- });
509
+ // Anthropic surface: messages, plus its own (anthropic-slot) model list so
510
+ // an Anthropic client can discover models enabled only on the anthropic slot,
511
+ // which the shared-/v1 design couldn't surface.
512
+ anthropic.post("/messages", (c) => dispatch(c, "anthropic"));
513
+ anthropic.get("/models", (c) => modelsList(c, store, "anthropic"));
498
514
 
499
- return app;
515
+ return { openai, anthropic };
500
516
  }
@@ -219,7 +219,7 @@ export class Store {
219
219
  ` username: ${account.username}`,
220
220
  ` password: ${account.password}`,
221
221
  "",
222
- "API key (for agents calling /v1):",
222
+ "API key (for agents calling /openai/v1 + /anthropic/v1):",
223
223
  ` ${apiKey}`,
224
224
  "",
225
225
  "Regenerated on each startup. If you change the password in Settings, this",