myapikey 0.46.1 → 0.47.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/package.json +1 -1
- package/packages/core/src/cli/client.ts +5 -5
- package/packages/core/src/cli/index.ts +6 -6
- package/packages/core/src/server/admin.ts +6 -6
- package/packages/core/src/server/app.ts +16 -9
- package/packages/core/src/server/proxy.ts +42 -28
- package/packages/web/dist/assets/index-C6uBvekt.js +346 -0
- package/packages/web/dist/index.html +1 -1
- package/packages/web/dist/assets/index-C7p__OSN.js +0 -345
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "myapikey",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.47.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 /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
|
|
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
|
|
56
|
-
console.log(` ${url}/anthropic/v1/messages
|
|
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,
|
|
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:
|
|
931
|
-
//
|
|
932
|
-
//
|
|
933
|
-
const sub = format === "anthropic" ? anthropic :
|
|
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 :
|
|
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
|
-
//
|
|
38
|
-
//
|
|
39
|
-
|
|
40
|
-
const
|
|
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",
|
|
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.
|
|
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",
|
|
498
|
-
* anthropic `{data:[{id, display_name}],
|
|
499
|
-
* can't know real created_at / capabilities,
|
|
500
|
-
* only the honest minimal fields rather than
|
|
501
|
-
|
|
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[
|
|
505
|
-
if (
|
|
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
|
|
510
|
-
id
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
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
|
|
519
|
-
*
|
|
520
|
-
*
|
|
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
|
-
): {
|
|
525
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
869
|
-
//
|
|
870
|
-
|
|
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
|
-
|
|
886
|
+
responses.post("/responses", (c) => dispatch(c, "responses"));
|
|
873
887
|
|
|
874
|
-
// Anthropic surface: messages
|
|
888
|
+
// Anthropic surface: messages.
|
|
875
889
|
anthropic.post("/messages", (c) => dispatch(c, "anthropic"));
|
|
876
890
|
|
|
877
|
-
return {
|
|
891
|
+
return { chat, responses, anthropic };
|
|
878
892
|
}
|