myapikey 0.18.0 → 0.19.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/index.ts +15 -1
- package/packages/core/src/server/admin.ts +48 -0
- package/packages/core/src/server/proxy.ts +131 -10
- package/packages/core/src/shared/types.ts +14 -0
- package/packages/web/dist/assets/index-DHemqm93.js +298 -0
- package/packages/web/dist/assets/index-L2OOJ8_e.css +1 -0
- package/packages/web/dist/index.html +2 -2
- package/packages/web/dist/assets/index-88CUfFdq.css +0 -1
- package/packages/web/dist/assets/index-DhA5duaA.js +0 -293
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "myapikey",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.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": [
|
|
@@ -181,7 +181,7 @@ model.command("list").action(async () => {
|
|
|
181
181
|
console.log(m.name);
|
|
182
182
|
for (const f of fmts) {
|
|
183
183
|
const fe = m[f];
|
|
184
|
-
const chain = fe.providers.map((p: any) =>
|
|
184
|
+
const chain = fe.providers.map((p: any) => `${p.model ? `${p.name}→${p.model}` : p.name}${p.thinking ? ` thinking=${p.thinking}` : ""}`).join(" → ") || "(none)";
|
|
185
185
|
console.log(` ${f.padEnd(9)} ${fe.enabled ? "✓" : "·"} ${chain}`);
|
|
186
186
|
}
|
|
187
187
|
if (m.paceRpm) console.log(` pace ${m.paceRpm}/min (one every ${Math.round(60 / m.paceRpm)}s)`);
|
|
@@ -264,6 +264,20 @@ model
|
|
|
264
264
|
console.log(r.paceRpm ? `Pacing ${name} at ${r.paceRpm}/min (one every ${Math.round(60 / r.paceRpm)}s).` : `Pacing cleared for ${name} (unlimited).`);
|
|
265
265
|
});
|
|
266
266
|
|
|
267
|
+
model
|
|
268
|
+
.command("thinking <name> <index> [value]")
|
|
269
|
+
.description("set/clear the default thinking level of one chain slot (effort on openai/responses, budget tokens on anthropic; overrides the request's own setting; empty = clear)")
|
|
270
|
+
.addOption(fmtOption())
|
|
271
|
+
.action(async (name: string, indexRaw: string, value: string | undefined, opts: { format: "openai" | "anthropic" | "responses" }) => {
|
|
272
|
+
const index = Number(indexRaw);
|
|
273
|
+
const r = (await api(ctx(), "PUT", `/admin/models/${encodeURIComponent(name)}/thinking`, { format: opts.format, index, thinking: value ?? "" })) as { thinking?: string };
|
|
274
|
+
console.log(
|
|
275
|
+
r.thinking
|
|
276
|
+
? `Default thinking for ${name} [${opts.format}] slot ${index}: ${r.thinking}.`
|
|
277
|
+
: `Default thinking cleared for ${name} [${opts.format}] slot ${index}.`,
|
|
278
|
+
);
|
|
279
|
+
});
|
|
280
|
+
|
|
267
281
|
model.command("remove <name>").description("remove a model entirely (both formats)").action(async (name: string) => {
|
|
268
282
|
await api(ctx(), "DELETE", `/admin/models/${encodeURIComponent(name)}`);
|
|
269
283
|
console.log(`Removed ${name}.`);
|
|
@@ -312,6 +312,8 @@ export function adminApi(store: Store, auth: MiddlewareHandler, openai: Hono, an
|
|
|
312
312
|
// send the public model name verbatim). Carried inline on each slot so
|
|
313
313
|
// a provider can appear more than once with different upstream names.
|
|
314
314
|
model: s.model,
|
|
315
|
+
// Default thinking level for this slot (undefined = pure passthrough).
|
|
316
|
+
thinking: s.thinking,
|
|
315
317
|
})),
|
|
316
318
|
});
|
|
317
319
|
const models = Object.entries(d.models).map(([name, e]) => ({
|
|
@@ -540,6 +542,52 @@ export function adminApi(store: Store, auth: MiddlewareHandler, openai: Hono, an
|
|
|
540
542
|
return c.json({ ok: true });
|
|
541
543
|
});
|
|
542
544
|
|
|
545
|
+
// Set (or clear) the default thinking level for ONE chain slot (addressed by
|
|
546
|
+
// `index`, like /map above). The value is whatever the slot's wire format
|
|
547
|
+
// takes natively: an effort token ("low"/"medium"/"high"/…) on openai/
|
|
548
|
+
// responses slots; a thinking budget in TOKENS (positive integer) on
|
|
549
|
+
// anthropic slots (Anthropic has no named levels). When set, dispatch
|
|
550
|
+
// applies it IN PLACE OF whatever thinking parameters the request carried
|
|
551
|
+
// (the gateway's level wins); an empty value clears it (pure passthrough).
|
|
552
|
+
app.put("/models/:name/thinking", async (c) => {
|
|
553
|
+
const name = c.req.param("name");
|
|
554
|
+
const body = await readJson<{ format?: RouteKey; index?: number; thinking?: string | number }>(c.req.raw);
|
|
555
|
+
if (!body?.format) return c.json({ error: { message: "format is required" } }, 400);
|
|
556
|
+
if (!Number.isInteger(body?.index) || (body?.index ?? -1) < 0)
|
|
557
|
+
return c.json({ error: { message: "index (non-negative integer) is required" } }, 400);
|
|
558
|
+
const format = body.format;
|
|
559
|
+
const index = body.index!;
|
|
560
|
+
const raw = body.thinking === undefined ? "" : String(body.thinking).trim();
|
|
561
|
+
if (raw && format === "anthropic" && (!/^\d+$/.test(raw) || Number(raw) < 1)) {
|
|
562
|
+
return c.json({ error: { message: "anthropic thinking default must be a positive integer (thinking budget tokens, e.g. 8192)" } }, 400);
|
|
563
|
+
}
|
|
564
|
+
if (raw && format !== "anthropic" && raw.length > 32) {
|
|
565
|
+
return c.json({ error: { message: "thinking default too long (max 32 chars)" } }, 400);
|
|
566
|
+
}
|
|
567
|
+
const value = raw && format === "anthropic" ? String(Number(raw)) : raw;
|
|
568
|
+
let errStatus = 0;
|
|
569
|
+
let errMsg = "";
|
|
570
|
+
await store.update((d) => {
|
|
571
|
+
const entry = d.models[name];
|
|
572
|
+
if (!entry) {
|
|
573
|
+
errStatus = 404;
|
|
574
|
+
errMsg = "model not found";
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
const fe = entry[format];
|
|
578
|
+
if (index >= fe.providers.length) {
|
|
579
|
+
errStatus = 400;
|
|
580
|
+
errMsg = "index out of range for this model's chain";
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
if (value) fe.providers[index].thinking = value;
|
|
584
|
+
else delete fe.providers[index].thinking;
|
|
585
|
+
});
|
|
586
|
+
if (errStatus === 404) return c.json({ error: { message: errMsg } }, 404);
|
|
587
|
+
if (errStatus === 400) return c.json({ error: { message: errMsg } }, 400);
|
|
588
|
+
return c.json({ ok: true, thinking: value || undefined });
|
|
589
|
+
});
|
|
590
|
+
|
|
543
591
|
app.post("/models/:name/disable", async (c) => {
|
|
544
592
|
const name = c.req.param("name");
|
|
545
593
|
const body = await readJson<{ format?: RouteKey }>(c.req.raw);
|
|
@@ -62,12 +62,14 @@ function parseResetFromBody(text: string): number | undefined {
|
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
/** One resolved routing slot: the provider to forward to plus THIS slot's
|
|
65
|
-
* optional upstream model name (absent = send the public model name)
|
|
66
|
-
* same provider may occupy several slots in a
|
|
67
|
-
* failover slot carrying its own upstream
|
|
65
|
+
* optional upstream model name (absent = send the public model name) and
|
|
66
|
+
* default thinking level. The same provider may occupy several slots in a
|
|
67
|
+
* chain — each is an independent failover slot carrying its own upstream
|
|
68
|
+
* model. */
|
|
68
69
|
interface CandidateSlot {
|
|
69
70
|
provider: Provider;
|
|
70
71
|
model?: string;
|
|
72
|
+
thinking?: string;
|
|
71
73
|
}
|
|
72
74
|
|
|
73
75
|
/** Resolve the ordered, compatible provider slots for a model on a routing slot. */
|
|
@@ -83,7 +85,7 @@ function candidates(store: Store, model: string, key: RouteKey): CandidateSlot[]
|
|
|
83
85
|
return fe.providers
|
|
84
86
|
.map((s): CandidateSlot | null => {
|
|
85
87
|
const p = byId.get(s.id);
|
|
86
|
-
return p ? { provider: p, model: s.model } : null;
|
|
88
|
+
return p ? { provider: p, model: s.model, thinking: s.thinking } : null;
|
|
87
89
|
})
|
|
88
90
|
.filter((slot): slot is CandidateSlot => {
|
|
89
91
|
if (!slot) return false;
|
|
@@ -104,6 +106,109 @@ function notFound(c: Context, model: string) {
|
|
|
104
106
|
);
|
|
105
107
|
}
|
|
106
108
|
|
|
109
|
+
// --- per-slot default thinking level -----------------------------------------
|
|
110
|
+
// Each routing slot can carry a default thinking level. When a slot HAS one it
|
|
111
|
+
// takes precedence over EVERYTHING the request carried — the gateway's own
|
|
112
|
+
// config is the authority, so the default replaces the request's thinking
|
|
113
|
+
// parameters outright (all of them, including compat-backend switches, leaving
|
|
114
|
+
// exactly one thinking instruction in the body). A slot WITHOUT a default is
|
|
115
|
+
// pure passthrough: whatever the request carried goes through untouched —
|
|
116
|
+
// restored verbatim if an earlier failover slot overrode it. Like the model
|
|
117
|
+
// rewrite, the default is written in the wire's own dialect, never translated
|
|
118
|
+
// across formats:
|
|
119
|
+
// openai /chat/completions → `reasoning_effort: "<token>"` (low/medium/high/…)
|
|
120
|
+
// /responses → `reasoning: { effort: "<token>" }`
|
|
121
|
+
// anthropic /v1/messages → `thinking: { type: "enabled", budget_tokens: N }`
|
|
122
|
+
// (Anthropic has no named levels — the stored default IS the budget, a
|
|
123
|
+
// positive integer. The API also demands max_tokens > budget_tokens, so an
|
|
124
|
+
// at-or-below cap is lifted to budget+1 (and restored on a passthrough
|
|
125
|
+
// slot); otherwise the call would 400 — including our own max_tokens:1
|
|
126
|
+
// probe.)
|
|
127
|
+
|
|
128
|
+
/** The body fields that steer thinking on a slot. The FIRST is the wire's
|
|
129
|
+
* canonical parameter — the one a default is written into; the rest are
|
|
130
|
+
* compat-backend switches (`thinking`, `enable_thinking` on chat/completions)
|
|
131
|
+
* that an override clears so the forced level is the only instruction left. */
|
|
132
|
+
function thinkingFields(key: RouteKey): string[] {
|
|
133
|
+
if (key === "anthropic") return ["thinking"];
|
|
134
|
+
if (key === "responses") return ["reasoning"];
|
|
135
|
+
return ["reasoning_effort", "reasoning", "thinking", "enable_thinking"];
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Best-effort display of what a body's own thinking switches ask for (for the
|
|
139
|
+
* log row on a passthrough slot, where the request's setting is what ran). */
|
|
140
|
+
function clientThinking(body: Record<string, unknown>, key: RouteKey): { set: boolean; value?: string } {
|
|
141
|
+
const show = (v: unknown): string | undefined => (typeof v === "object" && v !== null ? JSON.stringify(v) : String(v));
|
|
142
|
+
if (key === "responses") {
|
|
143
|
+
const r = body.reasoning;
|
|
144
|
+
if (r === undefined || r === null) return { set: false };
|
|
145
|
+
const effort = (r as { effort?: unknown }).effort;
|
|
146
|
+
return { set: true, value: effort === undefined ? show(r) : show(effort) };
|
|
147
|
+
}
|
|
148
|
+
if (key === "anthropic") {
|
|
149
|
+
const t = body.thinking;
|
|
150
|
+
if (t === undefined || t === null) return { set: false };
|
|
151
|
+
const budget = (t as { budget_tokens?: unknown }).budget_tokens;
|
|
152
|
+
return { set: true, value: budget === undefined ? show(t) : show(budget) };
|
|
153
|
+
}
|
|
154
|
+
const found = [
|
|
155
|
+
body.reasoning_effort,
|
|
156
|
+
(body.reasoning as { effort?: unknown } | undefined)?.effort,
|
|
157
|
+
(body.thinking as { budget_tokens?: unknown } | undefined)?.budget_tokens,
|
|
158
|
+
(body.thinking as { type?: unknown } | undefined)?.type,
|
|
159
|
+
body.enable_thinking,
|
|
160
|
+
].find((v) => v !== undefined && v !== null);
|
|
161
|
+
return { set: found !== undefined, value: found === undefined ? undefined : show(found) };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Snapshot of the request's own thinking switches, taken once before the
|
|
165
|
+
* failover loop — every attempt mutates the shared body, so a passthrough
|
|
166
|
+
* slot needs the originals kept aside to restore. */
|
|
167
|
+
interface ThinkingOrig {
|
|
168
|
+
fields: Record<string, unknown>;
|
|
169
|
+
/** anthropic only: the request's original max_tokens (restored alongside,
|
|
170
|
+
* since an override may have lifted it above the forced budget). */
|
|
171
|
+
maxTokens: unknown;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Apply THIS slot's thinking level to the body (mutating it), and return the
|
|
175
|
+
* log row's `thinking` field: the level that ran and where it came from.
|
|
176
|
+
* Undefined = nothing applied. A slot WITH a default overrides the request's
|
|
177
|
+
* own parameters entirely; a slot WITHOUT one restores them (undoing any
|
|
178
|
+
* override an earlier failover slot applied — same recompute-per-attempt
|
|
179
|
+
* discipline as the model rewrite, so slot A's level never leaks into B). */
|
|
180
|
+
function applySlotThinking(
|
|
181
|
+
body: Record<string, unknown>,
|
|
182
|
+
key: RouteKey,
|
|
183
|
+
def: string | undefined,
|
|
184
|
+
orig: ThinkingOrig,
|
|
185
|
+
): { value: string; from: "client" | "default" } | undefined {
|
|
186
|
+
const fields = thinkingFields(key);
|
|
187
|
+
if (!def) {
|
|
188
|
+
for (const f of fields) {
|
|
189
|
+
if (orig.fields[f] === undefined) delete body[f];
|
|
190
|
+
else body[f] = orig.fields[f];
|
|
191
|
+
}
|
|
192
|
+
if (key === "anthropic") {
|
|
193
|
+
if (orig.maxTokens === undefined) delete body.max_tokens;
|
|
194
|
+
else body.max_tokens = orig.maxTokens;
|
|
195
|
+
}
|
|
196
|
+
const own = clientThinking(orig.fields, key);
|
|
197
|
+
return own.set ? { value: own.value ?? "", from: "client" } : undefined;
|
|
198
|
+
}
|
|
199
|
+
for (const f of fields) delete body[f];
|
|
200
|
+
if (key === "anthropic") {
|
|
201
|
+
const budget = Number(def);
|
|
202
|
+
body.thinking = { type: "enabled", budget_tokens: budget };
|
|
203
|
+
if (typeof body.max_tokens === "number" && body.max_tokens <= budget) body.max_tokens = budget + 1;
|
|
204
|
+
} else if (key === "responses") {
|
|
205
|
+
body.reasoning = { effort: def };
|
|
206
|
+
} else {
|
|
207
|
+
body.reasoning_effort = def;
|
|
208
|
+
}
|
|
209
|
+
return { value: def, from: "default" };
|
|
210
|
+
}
|
|
211
|
+
|
|
107
212
|
/** Auth headers for the Anthropic wire format. Sends BOTH x-api-key and
|
|
108
213
|
* Authorization: Bearer (same key). Native Anthropic (api.anthropic.com)
|
|
109
214
|
* accepts either; anthropic- COMPATIBLE surfaces (sensenova, Volcengine Ark,
|
|
@@ -353,6 +458,13 @@ export function proxyApi(
|
|
|
353
458
|
const model: string = body.model;
|
|
354
459
|
const wire: Format = key === "anthropic" ? "anthropic" : "openai";
|
|
355
460
|
const stream = body.stream === true;
|
|
461
|
+
// The request's own thinking switches, snapshotted BEFORE the failover
|
|
462
|
+
// loop — each attempt mutates the shared body, and a passthrough slot
|
|
463
|
+
// (no default) must restore these originals.
|
|
464
|
+
const thinkOrig: ThinkingOrig = {
|
|
465
|
+
fields: Object.fromEntries(thinkingFields(key).map((f) => [f, body[f]])),
|
|
466
|
+
maxTokens: body.max_tokens,
|
|
467
|
+
};
|
|
356
468
|
// The model-page "test" button drives dispatch via an in-process loopback
|
|
357
469
|
// (adminApi calls v1.request). The probe is a real call in every respect —
|
|
358
470
|
// including being logged — so we only tag it to report WHICH provider
|
|
@@ -393,6 +505,9 @@ export function proxyApi(
|
|
|
393
505
|
const start = Date.now();
|
|
394
506
|
let lastStatus = 502;
|
|
395
507
|
let lastErr = "";
|
|
508
|
+
// Thinking level of the most recent attempt (for the all-failed row after
|
|
509
|
+
// the rounds loop; per-attempt rows capture the loop's own `think` const).
|
|
510
|
+
let lastThink: { value: string; from: "client" | "default" } | undefined;
|
|
396
511
|
// Runtime log (console + server.log). Errors and notable events only — the
|
|
397
512
|
// per-call history these lines summarize goes to pushLog/logs.jsonl anyway.
|
|
398
513
|
// UI-triggered probes are excluded: their outcome is shown inline already.
|
|
@@ -482,6 +597,12 @@ export function proxyApi(
|
|
|
482
597
|
// when the public name went through verbatim — JSON.stringify drops it, so
|
|
483
598
|
// identity + legacy rows stay clean.
|
|
484
599
|
const upstreamModel = slot.model && slot.model !== model ? slot.model : undefined;
|
|
600
|
+
// Per-slot thinking level: re-read from the slot and re-applied every
|
|
601
|
+
// attempt. A configured default OVERRIDES whatever the request carried;
|
|
602
|
+
// a slot without one restores the request's own switches (see
|
|
603
|
+
// applySlotThinking), so slot A's level never leaks into slot B.
|
|
604
|
+
const think = applySlotThinking(body, key, slot.thinking, thinkOrig);
|
|
605
|
+
lastThink = think;
|
|
485
606
|
// Count this attempt toward the source's RPM window — but not for a pinned
|
|
486
607
|
// probe, which (like circuit state) takes no routing side-effects.
|
|
487
608
|
if (pinIndex == null) store.recordDispatch(provider.id);
|
|
@@ -500,7 +621,7 @@ export function proxyApi(
|
|
|
500
621
|
sayFailover(provider, "network error");
|
|
501
622
|
const r = store.recordCircuitFailure(provider.id, lastStatus, lastErr);
|
|
502
623
|
if (r.entered) {
|
|
503
|
-
store.pushLog({ ts: Date.now(), model, upstreamModel, provider: provider.name, providerId: provider.id, format: wire, status: lastStatus, ms: Date.now() - start, stream, kind: "cooldown", cooldownMs: r.cooldownMs, fails: r.fails, error: lastErr });
|
|
624
|
+
store.pushLog({ ts: Date.now(), model, upstreamModel, provider: provider.name, providerId: provider.id, format: wire, status: lastStatus, ms: Date.now() - start, stream, thinking: think, kind: "cooldown", cooldownMs: r.cooldownMs, fails: r.fails, error: lastErr });
|
|
504
625
|
sayCooldown(provider, r);
|
|
505
626
|
}
|
|
506
627
|
continue;
|
|
@@ -526,13 +647,13 @@ export function proxyApi(
|
|
|
526
647
|
onSettle: (info) => {
|
|
527
648
|
if (info.ok) {
|
|
528
649
|
store.recordCircuitSuccess(provider.id);
|
|
529
|
-
store.pushLog({ ts: Date.now(), model, upstreamModel, provider: provider.name, providerId: provider.id, format: wire, status: 200, ms: ttfb, stream, usage: info.usage });
|
|
650
|
+
store.pushLog({ ts: Date.now(), model, upstreamModel, provider: provider.name, providerId: provider.id, format: wire, status: 200, ms: ttfb, stream, thinking: think, usage: info.usage });
|
|
530
651
|
} else {
|
|
531
652
|
// A pinned per-source probe takes no circuit side-effects (a manual
|
|
532
653
|
// test must not trip the breaker) — mirrors the retryable branch.
|
|
533
654
|
if (pinIndex == null) store.recordCircuitFailure(provider.id, info.status, info.error || "stream failed");
|
|
534
655
|
if (!isProbe) rt.warn(`proxy stream failed: provider '${provider.name}' status=${info.status} (${info.error || "stream failed"})`);
|
|
535
|
-
store.pushLog({ ts: Date.now(), model, upstreamModel, provider: provider.name, providerId: provider.id, format: wire, status: info.status, ms: ttfb, stream, error: info.error });
|
|
656
|
+
store.pushLog({ ts: Date.now(), model, upstreamModel, provider: provider.name, providerId: provider.id, format: wire, status: info.status, ms: ttfb, stream, thinking: think, error: info.error });
|
|
536
657
|
}
|
|
537
658
|
},
|
|
538
659
|
});
|
|
@@ -555,7 +676,7 @@ export function proxyApi(
|
|
|
555
676
|
const resetInMs = retryAfterMs ? undefined : parseResetFromBody(txt);
|
|
556
677
|
const r = store.recordCircuitFailure(provider.id, lastStatus, lastErr, retryAfterMs ?? resetInMs, !!resetInMs);
|
|
557
678
|
if (r.entered) {
|
|
558
|
-
store.pushLog({ ts: Date.now(), model, upstreamModel, provider: provider.name, providerId: provider.id, format: wire, status: lastStatus, ms: Date.now() - start, stream, kind: "cooldown", cooldownMs: r.cooldownMs, fails: r.fails, error: lastErr });
|
|
679
|
+
store.pushLog({ ts: Date.now(), model, upstreamModel, provider: provider.name, providerId: provider.id, format: wire, status: lastStatus, ms: Date.now() - start, stream, thinking: think, kind: "cooldown", cooldownMs: r.cooldownMs, fails: r.fails, error: lastErr });
|
|
559
680
|
sayCooldown(provider, r);
|
|
560
681
|
}
|
|
561
682
|
continue;
|
|
@@ -563,7 +684,7 @@ export function proxyApi(
|
|
|
563
684
|
// Non-retryable client error: return it to the caller as-is. Read the
|
|
564
685
|
// error text off a CLONE so the original body still streams back.
|
|
565
686
|
const errText = await upstream.clone().text().catch(() => "");
|
|
566
|
-
store.pushLog({ ts: Date.now(), model, upstreamModel, provider: provider.name, providerId: provider.id, format: wire, status: upstream.status, ms: Date.now() - start, stream, error: shortError(errText) || `HTTP ${upstream.status}` });
|
|
687
|
+
store.pushLog({ ts: Date.now(), model, upstreamModel, provider: provider.name, providerId: provider.id, format: wire, status: upstream.status, ms: Date.now() - start, stream, thinking: think, error: shortError(errText) || `HTTP ${upstream.status}` });
|
|
567
688
|
return passThrough(upstream, isProbe ? provider.name : undefined);
|
|
568
689
|
}
|
|
569
690
|
|
|
@@ -576,7 +697,7 @@ export function proxyApi(
|
|
|
576
697
|
return new Response(null, { status: 499 });
|
|
577
698
|
}
|
|
578
699
|
if (!isProbe) rt.error(`proxy all providers failed model=${model} (last status ${lastStatus})`);
|
|
579
|
-
store.pushLog({ ts: Date.now(), model, upstreamModel: lastUpstreamModel, provider: last.provider.name, providerId: last.provider.id, format: wire, status: lastStatus, ms: Date.now() - start, stream, error: lastErr || `all providers failed (last status ${lastStatus})` });
|
|
700
|
+
store.pushLog({ ts: Date.now(), model, upstreamModel: lastUpstreamModel, provider: last.provider.name, providerId: last.provider.id, format: wire, status: lastStatus, ms: Date.now() - start, stream, thinking: lastThink, error: lastErr || `all providers failed (last status ${lastStatus})` });
|
|
580
701
|
// A pinned (per-source) probe failed: surface the REAL upstream status the
|
|
581
702
|
// one slot returned (429/500/…), not a collapsed 502, and tag it with
|
|
582
703
|
// x-myapikey-provider so the source-row badge names the tested source.
|
|
@@ -68,6 +68,14 @@ export interface ChainSlot {
|
|
|
68
68
|
/** Upstream model name to send to this provider. Absent = forward the public
|
|
69
69
|
* model name (the key in GateConfig.models) unchanged. */
|
|
70
70
|
model?: string;
|
|
71
|
+
/** Default thinking level for this slot. When set it takes PRECEDENCE over
|
|
72
|
+
* the request's own thinking parameters — dispatch replaces them outright
|
|
73
|
+
* (the gateway's configured level is the authority). When absent, the
|
|
74
|
+
* request's parameters pass through untouched. The value is whatever the
|
|
75
|
+
* slot's wire format takes natively — no translation: an effort token
|
|
76
|
+
* ("low"/"medium"/"high"/…) on openai/responses slots, a thinking budget
|
|
77
|
+
* in tokens (positive integer) on anthropic slots. */
|
|
78
|
+
thinking?: string;
|
|
71
79
|
}
|
|
72
80
|
|
|
73
81
|
/** A model's routing dimensions — one per forwarding endpoint. /chat/completions
|
|
@@ -148,6 +156,12 @@ export interface LogEntry {
|
|
|
148
156
|
usage?: Usage;
|
|
149
157
|
/** Short upstream error text on non-2xx (omitted on success). */
|
|
150
158
|
error?: string;
|
|
159
|
+
/** The thinking level this call ran with, and where it came from: "client" =
|
|
160
|
+
* the answering slot had no default, so the request's own thinking setting
|
|
161
|
+
* ran (forwarded untouched); "default" = the slot's configured `thinking`
|
|
162
|
+
* ran, OVERRIDING whatever the request carried. Absent when neither
|
|
163
|
+
* applied (no default configured, request carried nothing). */
|
|
164
|
+
thinking?: { value: string; from: "client" | "default" };
|
|
151
165
|
/** Row kind. Absent on legacy lines → treated as a normal call. "cooldown"
|
|
152
166
|
* marks a circuit-breaker event (a provider just entered cooldown), shown
|
|
153
167
|
* distinctly in the timeline alongside the failures that caused it. */
|