myapikey 0.22.0 → 0.23.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/server/admin.ts +51 -1
- package/packages/core/src/server/proxy.ts +5 -3
- package/packages/web/dist/assets/index-CL6-7p6i.js +314 -0
- package/packages/web/dist/assets/index-D_95zRrv.css +1 -0
- package/packages/web/dist/index.html +2 -2
- package/packages/web/dist/assets/index-B9ZYbCY9.css +0 -1
- package/packages/web/dist/assets/index-DTibXbE8.js +0 -304
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "myapikey",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.23.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": [
|
|
@@ -2,7 +2,7 @@ import { Hono, type MiddlewareHandler } from "hono";
|
|
|
2
2
|
import { newProviderId, newApiKey, trimBase } from "../shared/config";
|
|
3
3
|
import type { ChainSlot, Format, FormatEntry, ModelEntry, Provider, RouteKey } from "../shared/types";
|
|
4
4
|
import type { Store } from "./store";
|
|
5
|
-
import { shortError, anthropicAuthHeaders } from "./proxy";
|
|
5
|
+
import { shortError, anthropicAuthHeaders, upstreamTarget, upstreamHeaders } from "./proxy";
|
|
6
6
|
import { networkInterfaces } from "node:os";
|
|
7
7
|
|
|
8
8
|
/** Best-effort LAN IPv4 of this host — the address an agent on another machine
|
|
@@ -323,6 +323,56 @@ export function adminApi(store: Store, auth: MiddlewareHandler, openai: Hono, an
|
|
|
323
323
|
}
|
|
324
324
|
});
|
|
325
325
|
|
|
326
|
+
// Test a SOURCE directly: one minimal ping per selected protocol, straight to
|
|
327
|
+
// the upstream with this source's own key + base URL. Unlike the model tests
|
|
328
|
+
// (which loop back through dispatch) this needs no routing config, so an
|
|
329
|
+
// unrouted source can be checked too — and it takes no routing side-effects
|
|
330
|
+
// (no logs, no circuit, no pacing). ?model= is the upstream name sent
|
|
331
|
+
// verbatim; ?format= (repeatable) narrows the protocols, defaulting to every
|
|
332
|
+
// one the source supports (responses only when supportsResponses).
|
|
333
|
+
app.post("/providers/:id/test", async (c) => {
|
|
334
|
+
const p = store.get().providers.find((x) => x.id === c.req.param("id"));
|
|
335
|
+
if (!p) return c.json({ error: { message: "provider not found" } }, 404);
|
|
336
|
+
const model = (c.req.query("model") ?? "").trim();
|
|
337
|
+
if (!model) return c.json({ error: { message: "?model= (upstream model name) is required" } }, 400);
|
|
338
|
+
const wanted = (c.req.queries("format") ?? []).filter(
|
|
339
|
+
(f): f is RouteKey => f === "openai" || f === "anthropic" || f === "responses",
|
|
340
|
+
);
|
|
341
|
+
const formats: RouteKey[] = wanted.length
|
|
342
|
+
? wanted
|
|
343
|
+
: [...p.formats, ...(p.supportsResponses ? ["responses" as const] : [])];
|
|
344
|
+
const results = await Promise.all(
|
|
345
|
+
formats.map(async (format) => {
|
|
346
|
+
if (format === "responses" ? !p.supportsResponses : !p.formats.includes(format as Format)) {
|
|
347
|
+
return { format, ok: false, status: 0, ms: 0, error: "source does not speak this format" };
|
|
348
|
+
}
|
|
349
|
+
const start = Date.now();
|
|
350
|
+
try {
|
|
351
|
+
// /responses takes `input`, not `messages` (same probe bodies as the
|
|
352
|
+
// model tests); max_tokens: 1 keeps the ping cheap.
|
|
353
|
+
const body =
|
|
354
|
+
format === "responses"
|
|
355
|
+
? { model, input: "ping", stream: false }
|
|
356
|
+
: { model, messages: [{ role: "user", content: "ping" }], max_tokens: 1, stream: false };
|
|
357
|
+
const res = await fetch(upstreamTarget(p, format).url, {
|
|
358
|
+
method: "POST",
|
|
359
|
+
headers: upstreamHeaders(p, format === "responses" ? "openai" : format),
|
|
360
|
+
body: JSON.stringify(body),
|
|
361
|
+
});
|
|
362
|
+
const ms = Date.now() - start;
|
|
363
|
+
// Drain so the connection is released; the text feeds the error line.
|
|
364
|
+
const txt = await res.text().catch(() => "");
|
|
365
|
+
return res.ok
|
|
366
|
+
? { format, ok: true, status: res.status, ms }
|
|
367
|
+
: { format, ok: false, status: res.status, ms, error: shortError(txt) || `HTTP ${res.status}` };
|
|
368
|
+
} catch (e) {
|
|
369
|
+
return { format, ok: false, status: 0, ms: Date.now() - start, error: `network error: ${(e as Error).message}` };
|
|
370
|
+
}
|
|
371
|
+
}),
|
|
372
|
+
);
|
|
373
|
+
return c.json({ results });
|
|
374
|
+
});
|
|
375
|
+
|
|
326
376
|
// --- models ---
|
|
327
377
|
app.get("/models", (c) => {
|
|
328
378
|
const d = store.get();
|
|
@@ -219,7 +219,8 @@ export function anthropicAuthHeaders(apiKey: string, version: string): Record<st
|
|
|
219
219
|
return { "x-api-key": apiKey, authorization: `Bearer ${apiKey}`, "anthropic-version": version };
|
|
220
220
|
}
|
|
221
221
|
|
|
222
|
-
|
|
222
|
+
/** Exported for the admin source-test (direct upstream ping, no routing). */
|
|
223
|
+
export function upstreamHeaders(provider: Provider, format: Format, clientVersion?: string): Record<string, string> {
|
|
223
224
|
const h: Record<string, string> = { "content-type": "application/json" };
|
|
224
225
|
if (format === "openai") h.authorization = `Bearer ${provider.apiKey}`;
|
|
225
226
|
else Object.assign(h, anthropicAuthHeaders(provider.apiKey, clientVersion || "2023-06-01"));
|
|
@@ -228,8 +229,9 @@ function upstreamHeaders(provider: Provider, format: Format, clientVersion?: str
|
|
|
228
229
|
|
|
229
230
|
/** Resolve the upstream URL + wire format for a routing slot. The OpenAI base
|
|
230
231
|
* includes the version segment (we append the bare resource); the Anthropic
|
|
231
|
-
* base excludes /v1 (we append v1/messages). /responses reuses the OpenAI base.
|
|
232
|
-
|
|
232
|
+
* base excludes /v1 (we append v1/messages). /responses reuses the OpenAI base.
|
|
233
|
+
* Exported for the admin source-test (direct upstream ping, no routing). */
|
|
234
|
+
export function upstreamTarget(p: Provider, key: RouteKey): { url: string; wire: Format } {
|
|
233
235
|
if (key === "anthropic") {
|
|
234
236
|
return { url: `${trimBase(p.baseUrlAnthropic)}/v1/messages`, wire: "anthropic" };
|
|
235
237
|
}
|