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 +27 -10
- package/README.zh-CN.md +27 -10
- package/package.json +1 -1
- package/packages/core/src/cli/client.ts +8 -7
- package/packages/core/src/cli/index.ts +7 -7
- package/packages/core/src/server/admin.ts +8 -3
- package/packages/core/src/server/app.ts +7 -4
- package/packages/core/src/server/proxy.ts +40 -24
- package/packages/core/src/server/store.ts +1 -1
- package/packages/web/dist/assets/{index-KSgkYqUT.js → index-B8MSV1WY.js} +26 -23
- package/packages/web/dist/index.html +1 -1
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 (
|
|
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
|
-
- `
|
|
243
|
-
- `
|
|
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 /
|
|
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(
|
|
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
|
-
- `
|
|
243
|
-
- `
|
|
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 开发服务器(把 /
|
|
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.
|
|
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
|
-
|
|
38
|
-
//
|
|
39
|
-
|
|
40
|
-
|
|
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 (!
|
|
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:
|
|
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
|
|
55
|
-
console.log(` ${url}/v1/responses (OpenAI Responses)`);
|
|
56
|
-
console.log(` ${url}/v1/messages
|
|
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,
|
|
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
|
|
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
|
|
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
|
-
|
|
30
|
-
|
|
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",
|
|
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
|
-
|
|
297
|
-
|
|
298
|
-
|
|
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
|
-
|
|
477
|
-
|
|
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
|
-
|
|
506
|
+
openai.post("/responses", (c) => dispatch(c, "responses"));
|
|
507
|
+
openai.get("/models", (c) => modelsList(c, store, "openai"));
|
|
480
508
|
|
|
481
|
-
//
|
|
482
|
-
//
|
|
483
|
-
//
|
|
484
|
-
|
|
485
|
-
|
|
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
|
|
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",
|