anygate 0.6.2 → 0.6.3
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/dist/{chunk-EMBABL33.js → chunk-NU4KMZIM.js} +911 -239
- package/dist/chunk-NU4KMZIM.js.map +1 -0
- package/dist/{chunk-S5WL3M5G.js → chunk-RNW2MGKL.js} +4 -2
- package/dist/chunk-RNW2MGKL.js.map +1 -0
- package/dist/cli.js +17 -15
- package/dist/cli.js.map +1 -1
- package/dist/{command-N3R2ZVVS.js → command-JOEFUFLD.js} +291 -116
- package/dist/command-JOEFUFLD.js.map +1 -0
- package/dist/{constants-U356SKNS.js → constants-7DBGCJ34.js} +4 -2
- package/dist/registry/data/templates/claude-code.json +1 -3
- package/dist/registry/data/templates/openai.json +13 -0
- package/dist/ui/dist/assets/index-CsUw6OG3.js +7 -0
- package/dist/ui/dist/index.html +1 -1
- package/package.json +1 -1
- package/dist/chunk-EMBABL33.js.map +0 -1
- package/dist/chunk-S5WL3M5G.js.map +0 -1
- package/dist/command-N3R2ZVVS.js.map +0 -1
- package/dist/ui/dist/assets/index-q6tXB_gH.js +0 -7
- /package/dist/{constants-U356SKNS.js.map → constants-7DBGCJ34.js.map} +0 -0
|
@@ -10,10 +10,11 @@ import {
|
|
|
10
10
|
OPENCODE_CACHE_PATH,
|
|
11
11
|
RATE_LIMIT_MAX_REQUESTS,
|
|
12
12
|
RATE_LIMIT_WINDOW_MS,
|
|
13
|
+
REQUEST_TIMEOUT_MS,
|
|
13
14
|
VERSION,
|
|
14
15
|
VERTEX_ANTHROPIC_NPM,
|
|
15
16
|
classifyModelFormat
|
|
16
|
-
} from "./chunk-
|
|
17
|
+
} from "./chunk-RNW2MGKL.js";
|
|
17
18
|
import {
|
|
18
19
|
getTemplateById,
|
|
19
20
|
listAddableTemplates
|
|
@@ -164,7 +165,18 @@ async function pollOpenAiDeviceCodeToken(deviceData, opts) {
|
|
|
164
165
|
const tokens = await tokenResponse.json();
|
|
165
166
|
return { tokens, accountId: extractOpenAiAccountId(tokens) };
|
|
166
167
|
}
|
|
167
|
-
if (response.status
|
|
168
|
+
if (response.status === 403) {
|
|
169
|
+
const detail = await response.text().catch(() => "");
|
|
170
|
+
try {
|
|
171
|
+
const parsed = JSON.parse(detail);
|
|
172
|
+
if (parsed.error?.code === "deviceauth_authorization_pending") break;
|
|
173
|
+
} catch {
|
|
174
|
+
}
|
|
175
|
+
if (detail.trim()) {
|
|
176
|
+
const msg = detail.replace(/\s+/g, " ").trim().slice(0, 300);
|
|
177
|
+
throw new Error(`OpenAI device authorization failed (403): ${msg}`);
|
|
178
|
+
}
|
|
179
|
+
} else if (response.status !== 404) {
|
|
168
180
|
const detail = await response.text().catch(() => "");
|
|
169
181
|
throw new Error(
|
|
170
182
|
`OpenAI device authorization failed (${response.status})${detail.trim() ? `: ${detail.replace(/\s+/g, " ").trim().slice(0, 200)}` : ""}`
|
|
@@ -174,7 +186,9 @@ async function pollOpenAiDeviceCodeToken(deviceData, opts) {
|
|
|
174
186
|
Math.min(intervalMs + OAUTH_POLLING_SAFETY_MARGIN_MS, Math.max(0, deadline - now()))
|
|
175
187
|
);
|
|
176
188
|
}
|
|
177
|
-
throw new Error(
|
|
189
|
+
throw new Error(
|
|
190
|
+
"OpenAI device authorization timed out \u2014 your account may not have device code authorization enabled. Enable it in ChatGPT Security Settings and try again, or use the CLI: anygate providers auth openai"
|
|
191
|
+
);
|
|
178
192
|
}
|
|
179
193
|
async function refreshOpenAiAccessToken(refreshToken) {
|
|
180
194
|
return postOAuthRefresh(
|
|
@@ -196,6 +210,47 @@ async function runOpenAiDeviceCodeFlow(onDeviceCode, opts) {
|
|
|
196
210
|
onDeviceCode({ url: openAiDeviceCodeUrl(), userCode: deviceData.user_code });
|
|
197
211
|
return pollOpenAiDeviceCodeToken(deviceData, opts);
|
|
198
212
|
}
|
|
213
|
+
async function buildOpenAiBrowserAuthUrl(redirectUri) {
|
|
214
|
+
const { verifier, challenge } = await generatePkce();
|
|
215
|
+
const state = generateOAuthState();
|
|
216
|
+
const params = new URLSearchParams({
|
|
217
|
+
client_id: CLIENT_ID,
|
|
218
|
+
response_type: "code",
|
|
219
|
+
redirect_uri: redirectUri,
|
|
220
|
+
scope: "openid profile email",
|
|
221
|
+
state,
|
|
222
|
+
code_challenge: challenge,
|
|
223
|
+
code_challenge_method: "S256",
|
|
224
|
+
// Prompt to force fresh sign-in if needed
|
|
225
|
+
prompt: "login"
|
|
226
|
+
});
|
|
227
|
+
return {
|
|
228
|
+
authUrl: `${ISSUER}/v1/authorize?${params}`,
|
|
229
|
+
codeVerifier: verifier,
|
|
230
|
+
oauthState: state
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
async function exchangeOpenAiBrowserToken(code, codeVerifier) {
|
|
234
|
+
const tokenResponse = await fetch(`${ISSUER}/auth/token`, {
|
|
235
|
+
method: "POST",
|
|
236
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
237
|
+
body: new URLSearchParams({
|
|
238
|
+
grant_type: "authorization_code",
|
|
239
|
+
code,
|
|
240
|
+
redirect_uri: `${ISSUER}/deviceauth/callback`,
|
|
241
|
+
client_id: CLIENT_ID,
|
|
242
|
+
code_verifier: codeVerifier
|
|
243
|
+
}).toString()
|
|
244
|
+
});
|
|
245
|
+
if (!tokenResponse.ok) {
|
|
246
|
+
const detail = await tokenResponse.text().catch(() => "");
|
|
247
|
+
throw new Error(
|
|
248
|
+
`OpenAI token exchange failed (${tokenResponse.status})${detail.trim() ? `: ${detail.replace(/\s+/g, " ").trim().slice(0, 200)}` : ""}`
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
const tokens = await tokenResponse.json();
|
|
252
|
+
return { tokens, accountId: extractOpenAiAccountId(tokens) };
|
|
253
|
+
}
|
|
199
254
|
|
|
200
255
|
// src/auth/responses-websocket.ts
|
|
201
256
|
var RESPONSES_LITE_HEADER = "x-openai-internal-codex-responses-lite";
|
|
@@ -474,6 +529,178 @@ function injectClaudeIdentity(body, providerData, seed) {
|
|
|
474
529
|
return { sessionId, userId };
|
|
475
530
|
}
|
|
476
531
|
|
|
532
|
+
// src/gateway/providers/stream-sanitizer.ts
|
|
533
|
+
function createNullChunkStripper() {
|
|
534
|
+
const decoder = new TextDecoder();
|
|
535
|
+
const encoder = new TextEncoder();
|
|
536
|
+
let buffer = "";
|
|
537
|
+
const isNullDataLine = (line) => {
|
|
538
|
+
const trimmed = line.trimStart();
|
|
539
|
+
if (!trimmed.startsWith("data:")) return false;
|
|
540
|
+
return trimmed.slice(5).trim() === "null";
|
|
541
|
+
};
|
|
542
|
+
return new TransformStream({
|
|
543
|
+
transform(chunk, controller) {
|
|
544
|
+
buffer += decoder.decode(chunk, { stream: true });
|
|
545
|
+
const newlineIdx = buffer.lastIndexOf("\n");
|
|
546
|
+
if (newlineIdx === -1) return;
|
|
547
|
+
const complete = buffer.slice(0, newlineIdx + 1);
|
|
548
|
+
buffer = buffer.slice(newlineIdx + 1);
|
|
549
|
+
const kept = complete.split("\n").filter((line) => !isNullDataLine(line)).join("\n");
|
|
550
|
+
if (kept) controller.enqueue(encoder.encode(kept));
|
|
551
|
+
},
|
|
552
|
+
flush(controller) {
|
|
553
|
+
const tail = buffer + decoder.decode();
|
|
554
|
+
if (tail && !isNullDataLine(tail)) controller.enqueue(encoder.encode(tail));
|
|
555
|
+
}
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
function flattenContent(content) {
|
|
559
|
+
const text4 = [];
|
|
560
|
+
const reasoning = [];
|
|
561
|
+
const blocks = Array.isArray(content) ? content : [content];
|
|
562
|
+
for (const block of blocks) {
|
|
563
|
+
if (block == null) continue;
|
|
564
|
+
if (typeof block === "string") {
|
|
565
|
+
text4.push(block);
|
|
566
|
+
continue;
|
|
567
|
+
}
|
|
568
|
+
if (typeof block !== "object") {
|
|
569
|
+
text4.push(String(block));
|
|
570
|
+
continue;
|
|
571
|
+
}
|
|
572
|
+
const b = block;
|
|
573
|
+
const isReasoning = b.type === "thinking" || b.type === "reasoning" || b.thinking != null;
|
|
574
|
+
if (isReasoning) {
|
|
575
|
+
const t = b.thinking;
|
|
576
|
+
if (Array.isArray(t)) {
|
|
577
|
+
for (const item of t) {
|
|
578
|
+
if (typeof item === "string") reasoning.push(item);
|
|
579
|
+
else if (item && typeof item.text === "string") reasoning.push(item.text);
|
|
580
|
+
}
|
|
581
|
+
} else if (typeof t === "string") {
|
|
582
|
+
reasoning.push(t);
|
|
583
|
+
}
|
|
584
|
+
if (typeof b.text === "string") reasoning.push(b.text);
|
|
585
|
+
} else if (typeof b.text === "string") {
|
|
586
|
+
text4.push(b.text);
|
|
587
|
+
} else if (typeof b.content === "string") {
|
|
588
|
+
text4.push(b.content);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
return { text: text4.join(""), reasoning: reasoning.join("") };
|
|
592
|
+
}
|
|
593
|
+
function normalizePart(part, idMap, preserveThinking = false) {
|
|
594
|
+
if (!part || typeof part !== "object") return;
|
|
595
|
+
if (Array.isArray(part.content) && !preserveThinking) {
|
|
596
|
+
const { text: text4, reasoning } = flattenContent(part.content);
|
|
597
|
+
part.content = text4;
|
|
598
|
+
if (reasoning) {
|
|
599
|
+
part.reasoning_content = (typeof part.reasoning_content === "string" ? part.reasoning_content : "") + reasoning;
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
if (Array.isArray(part.tool_calls)) {
|
|
603
|
+
part.tool_calls.forEach((tc, i) => {
|
|
604
|
+
if (!tc || typeof tc !== "object") return;
|
|
605
|
+
const tcIdx = typeof tc.index === "number" ? tc.index : i;
|
|
606
|
+
if (typeof tc.id !== "string" || tc.id === "") {
|
|
607
|
+
let existing = idMap.get(tcIdx);
|
|
608
|
+
if (!existing) {
|
|
609
|
+
existing = `call_${tcIdx}_${Math.random().toString(36).slice(2, 10)}`;
|
|
610
|
+
idMap.set(tcIdx, existing);
|
|
611
|
+
}
|
|
612
|
+
tc.id = existing;
|
|
613
|
+
}
|
|
614
|
+
if (!tc.function || typeof tc.function !== "object") tc.function = {};
|
|
615
|
+
if (typeof tc.function.name !== "string") tc.function.name = "";
|
|
616
|
+
if (typeof tc.function.arguments !== "string") tc.function.arguments = "";
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
function normalizeChunk(raw, ids, preserveThinking = false) {
|
|
621
|
+
if (!raw || typeof raw !== "object" || !Array.isArray(raw.choices)) return raw;
|
|
622
|
+
for (const choice of raw.choices) {
|
|
623
|
+
if (!choice || typeof choice !== "object") continue;
|
|
624
|
+
const choiceIdx = typeof choice.index === "number" ? choice.index : 0;
|
|
625
|
+
let idMap = ids.get(choiceIdx);
|
|
626
|
+
if (!idMap) {
|
|
627
|
+
idMap = /* @__PURE__ */ new Map();
|
|
628
|
+
ids.set(choiceIdx, idMap);
|
|
629
|
+
}
|
|
630
|
+
normalizePart(choice.delta, idMap, preserveThinking);
|
|
631
|
+
normalizePart(choice.message, idMap, preserveThinking);
|
|
632
|
+
}
|
|
633
|
+
return raw;
|
|
634
|
+
}
|
|
635
|
+
function createGlmChunkSanitizer(preserveThinking = false) {
|
|
636
|
+
const dec = new TextDecoder();
|
|
637
|
+
const enc = new TextEncoder();
|
|
638
|
+
let buffer = "";
|
|
639
|
+
const ids = /* @__PURE__ */ new Map();
|
|
640
|
+
const normalizeLine = (line) => {
|
|
641
|
+
if (!line.startsWith("data:")) return line;
|
|
642
|
+
const payload = line.slice(5).replace(/^\s+/, "");
|
|
643
|
+
if (payload === "" || payload === "[DONE]" || payload === "null") return line;
|
|
644
|
+
try {
|
|
645
|
+
const parsed = JSON.parse(payload);
|
|
646
|
+
if (parsed && typeof parsed === "object") {
|
|
647
|
+
return `data: ${JSON.stringify(normalizeChunk(parsed, ids, preserveThinking))}`;
|
|
648
|
+
}
|
|
649
|
+
} catch {
|
|
650
|
+
}
|
|
651
|
+
return line;
|
|
652
|
+
};
|
|
653
|
+
return new TransformStream({
|
|
654
|
+
transform(chunk, controller) {
|
|
655
|
+
buffer += dec.decode(chunk, { stream: true });
|
|
656
|
+
let nl;
|
|
657
|
+
while ((nl = buffer.indexOf("\n")) !== -1) {
|
|
658
|
+
const line = buffer.slice(0, nl);
|
|
659
|
+
buffer = buffer.slice(nl + 1);
|
|
660
|
+
controller.enqueue(enc.encode(normalizeLine(line) + "\n"));
|
|
661
|
+
}
|
|
662
|
+
},
|
|
663
|
+
flush(controller) {
|
|
664
|
+
const tail = buffer + dec.decode();
|
|
665
|
+
if (tail.length > 0) controller.enqueue(enc.encode(normalizeLine(tail)));
|
|
666
|
+
}
|
|
667
|
+
});
|
|
668
|
+
}
|
|
669
|
+
async function sanitizeJsonResponse(resp, preserveThinking = false) {
|
|
670
|
+
const text4 = await resp.clone().text();
|
|
671
|
+
try {
|
|
672
|
+
const parsed = JSON.parse(text4);
|
|
673
|
+
if (parsed && typeof parsed === "object") {
|
|
674
|
+
const fixed = normalizeChunk(parsed, /* @__PURE__ */ new Map(), preserveThinking);
|
|
675
|
+
return new Response(JSON.stringify(fixed), {
|
|
676
|
+
status: resp.status,
|
|
677
|
+
statusText: resp.statusText,
|
|
678
|
+
headers: resp.headers
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
} catch {
|
|
682
|
+
}
|
|
683
|
+
return resp;
|
|
684
|
+
}
|
|
685
|
+
function makeSanitizingFetch(upstream = fetch, preserveThinking = false) {
|
|
686
|
+
return (async (input, init) => {
|
|
687
|
+
const resp = await upstream(input, init);
|
|
688
|
+
const contentType = resp.headers.get("content-type") ?? "";
|
|
689
|
+
if (resp.body && contentType.includes("text/event-stream")) {
|
|
690
|
+
const sanitized = resp.body.pipeThrough(createNullChunkStripper()).pipeThrough(createGlmChunkSanitizer(preserveThinking));
|
|
691
|
+
return new Response(sanitized, {
|
|
692
|
+
status: resp.status,
|
|
693
|
+
statusText: resp.statusText,
|
|
694
|
+
headers: resp.headers
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
if (resp.body && contentType.includes("application/json")) {
|
|
698
|
+
return sanitizeJsonResponse(resp);
|
|
699
|
+
}
|
|
700
|
+
return resp;
|
|
701
|
+
});
|
|
702
|
+
}
|
|
703
|
+
|
|
477
704
|
// src/gateway/providers/provider-reasoning.ts
|
|
478
705
|
var ANTHROPIC_EFFORT_LEVELS = ["low", "medium", "high"];
|
|
479
706
|
var OPENAI_EFFORT_LEVELS = ["low", "medium", "high", "xhigh"];
|
|
@@ -954,37 +1181,49 @@ function thinkingProviderOptions(npm) {
|
|
|
954
1181
|
}
|
|
955
1182
|
};
|
|
956
1183
|
}
|
|
1184
|
+
if (npm === "@ai-sdk/mistral") {
|
|
1185
|
+
return { mistral: { reasoningEffort: "high" } };
|
|
1186
|
+
}
|
|
957
1187
|
return void 0;
|
|
958
1188
|
}
|
|
959
1189
|
|
|
960
1190
|
// src/gateway/providers/provider-factory.ts
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
1191
|
+
function buildUpstreamFetch(spec, preserveThinking = false) {
|
|
1192
|
+
return makeSanitizingFetch(
|
|
1193
|
+
async (input, init) => {
|
|
1194
|
+
const url = typeof input === "string" ? input : input?.url ?? String(input);
|
|
1195
|
+
const hdrs = new Headers(init?.headers ?? {});
|
|
1196
|
+
if (spec.headers) {
|
|
1197
|
+
for (const [k, v] of Object.entries(spec.headers)) hdrs.set(k, v);
|
|
1198
|
+
}
|
|
1199
|
+
const nextInit = { ...init, headers: hdrs };
|
|
1200
|
+
if (process.env.ANYGATE_TRACE === "1") {
|
|
1201
|
+
const shown = [];
|
|
1202
|
+
hdrs.forEach((v, k) => {
|
|
1203
|
+
shown.push(`${k}: ${/authorization|api-key/i.test(k) ? v.slice(0, 12) + "\u2026" : v}`);
|
|
1204
|
+
});
|
|
1205
|
+
spec.onDebug?.(`[sdk-fetch] POST ${url}
|
|
1206
|
+
[sdk-fetch] headers:
|
|
1207
|
+
${shown.join("\n ")}`);
|
|
1208
|
+
if (typeof init?.body === "string") {
|
|
1209
|
+
spec.onDebug?.(`[sdk-fetch] \u2192 body: ${init.body.slice(0, 8e3)}`);
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
const resp = await fetch(input, nextInit);
|
|
1213
|
+
if (process.env.ANYGATE_TRACE === "1") {
|
|
1214
|
+
spec.onDebug?.(`[sdk-fetch] \u2190 HTTP ${resp.status}`);
|
|
1215
|
+
if (resp.status >= 400) {
|
|
1216
|
+
const body = await resp.clone().text().catch(() => "");
|
|
1217
|
+
spec.onDebug?.(`[sdk-fetch] \u2190 body: ${body.slice(0, 500)}`);
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
return resp;
|
|
980
1221
|
},
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
if (tail && !isNullDataLine(tail)) controller.enqueue(encoder.encode(tail));
|
|
984
|
-
}
|
|
985
|
-
});
|
|
1222
|
+
preserveThinking
|
|
1223
|
+
);
|
|
986
1224
|
}
|
|
987
1225
|
var factoryCache = /* @__PURE__ */ new Map();
|
|
1226
|
+
var RESPONSES_ONLY_PREFIXES = ["gpt-5-codex", "gpt-5-pro", "gpt-5.2-pro", "o3", "o4"];
|
|
988
1227
|
function modelPrefersResponsesApi(modelId) {
|
|
989
1228
|
const lower = modelId.toLowerCase();
|
|
990
1229
|
if (RESPONSES_ONLY_PREFIXES.some((prefix) => lower === prefix || lower.startsWith(`${prefix}-`))) {
|
|
@@ -1118,51 +1357,10 @@ async function createLanguageModel(spec) {
|
|
|
1118
1357
|
// Custom headers like User-Agent must be forced at the fetch layer: the AI
|
|
1119
1358
|
// SDK builds its own User-Agent and overwrites a header-option value on some
|
|
1120
1359
|
// call paths (streaming vs non-streaming differ), which breaks providers that
|
|
1121
|
-
// gate on client identity. This wrapper
|
|
1122
|
-
//
|
|
1123
|
-
//
|
|
1124
|
-
|
|
1125
|
-
fetch: (async (input, init) => {
|
|
1126
|
-
const url = typeof input === "string" ? input : input?.url ?? String(input);
|
|
1127
|
-
const hdrs = new Headers(init?.headers ?? {});
|
|
1128
|
-
if (spec.headers) {
|
|
1129
|
-
for (const [k, v] of Object.entries(spec.headers)) hdrs.set(k, v);
|
|
1130
|
-
}
|
|
1131
|
-
const nextInit = { ...init, headers: hdrs };
|
|
1132
|
-
if (process.env.ANYGATE_TRACE === "1") {
|
|
1133
|
-
const shown = [];
|
|
1134
|
-
hdrs.forEach((v, k) => {
|
|
1135
|
-
shown.push(`${k}: ${/authorization|api-key/i.test(k) ? v.slice(0, 12) + "\u2026" : v}`);
|
|
1136
|
-
});
|
|
1137
|
-
spec.onDebug?.(
|
|
1138
|
-
`[sdk-fetch] POST ${url}
|
|
1139
|
-
[sdk-fetch] headers:
|
|
1140
|
-
${shown.join("\n ")}`
|
|
1141
|
-
);
|
|
1142
|
-
if (typeof init?.body === "string") {
|
|
1143
|
-
spec.onDebug?.(`[sdk-fetch] \u2192 body: ${init.body.slice(0, 8e3)}`);
|
|
1144
|
-
}
|
|
1145
|
-
}
|
|
1146
|
-
const resp = await fetch(input, nextInit);
|
|
1147
|
-
if (process.env.ANYGATE_TRACE === "1") {
|
|
1148
|
-
spec.onDebug?.(`[sdk-fetch] \u2190 HTTP ${resp.status}`);
|
|
1149
|
-
if (resp.status >= 400) {
|
|
1150
|
-
const body = await resp.clone().text().catch(() => "");
|
|
1151
|
-
spec.onDebug?.(`[sdk-fetch] \u2190 body: ${body.slice(0, 500)}`);
|
|
1152
|
-
}
|
|
1153
|
-
}
|
|
1154
|
-
const contentType = resp.headers.get("content-type") ?? "";
|
|
1155
|
-
if (resp.body && contentType.includes("text/event-stream")) {
|
|
1156
|
-
const sanitized = resp.body.pipeThrough(createNullChunkStripper());
|
|
1157
|
-
return new Response(sanitized, {
|
|
1158
|
-
status: resp.status,
|
|
1159
|
-
statusText: resp.statusText,
|
|
1160
|
-
headers: resp.headers
|
|
1161
|
-
});
|
|
1162
|
-
}
|
|
1163
|
-
return resp;
|
|
1164
|
-
})
|
|
1165
|
-
} : {}
|
|
1360
|
+
// gate on client identity. This wrapper also normalizes non-spec-compliant
|
|
1361
|
+
// upstream streams (array `content`, missing tool-call `id`, `data: null`
|
|
1362
|
+
// keepalives) so third-party models such as GLM-5.2 stream through cleanly.
|
|
1363
|
+
fetch: buildUpstreamFetch(spec)
|
|
1166
1364
|
};
|
|
1167
1365
|
model = createOpenAICompatible({
|
|
1168
1366
|
...options
|
|
@@ -1179,11 +1377,12 @@ async function createLanguageModel(spec) {
|
|
|
1179
1377
|
const provider = create({
|
|
1180
1378
|
apiKey,
|
|
1181
1379
|
...baseURL ? { baseURL } : {},
|
|
1182
|
-
...spec.headers ? { headers: spec.headers } : {}
|
|
1380
|
+
...spec.headers ? { headers: spec.headers } : {},
|
|
1381
|
+
fetch: buildUpstreamFetch(spec, npm === "@ai-sdk/mistral")
|
|
1183
1382
|
});
|
|
1184
1383
|
model = provider(modelId);
|
|
1185
1384
|
}
|
|
1186
|
-
const isReasoning = modelId.toLowerCase().match(/deepseek-r1|think|reasoning|qwq/);
|
|
1385
|
+
const isReasoning = modelId.toLowerCase().match(/deepseek-(?:r1|v4)|think|reasoning|qwq/);
|
|
1187
1386
|
if (isReasoning) {
|
|
1188
1387
|
return wrapLanguageModel({
|
|
1189
1388
|
model,
|
|
@@ -2398,6 +2597,12 @@ function shouldHideByModelsDevCapabilities(entry) {
|
|
|
2398
2597
|
if (entry.interactions === true && entry.chat === false) return true;
|
|
2399
2598
|
return false;
|
|
2400
2599
|
}
|
|
2600
|
+
function resolveReasoning(providerId, modelId, cache = loadModelsDevCache()) {
|
|
2601
|
+
const entry = findModelsDevModel(providerId, modelId, cache);
|
|
2602
|
+
if (!entry) return void 0;
|
|
2603
|
+
if (typeof entry.reasoning === "boolean") return entry.reasoning;
|
|
2604
|
+
return void 0;
|
|
2605
|
+
}
|
|
2401
2606
|
|
|
2402
2607
|
// src/apps/shared/trace-log.ts
|
|
2403
2608
|
import { chmodSync as chmodSync5, existsSync as existsSync4, mkdirSync as mkdirSync5, readFileSync as readFileSync5, unlinkSync as unlinkSync2, writeFileSync as writeFileSync4 } from "fs";
|
|
@@ -2649,46 +2854,33 @@ async function getServerPasswordKeyring() {
|
|
|
2649
2854
|
}
|
|
2650
2855
|
}
|
|
2651
2856
|
async function getSavedServerPassword() {
|
|
2652
|
-
const config = readConfig();
|
|
2653
|
-
if (config.server?.savedPassword) {
|
|
2654
|
-
const pwd = config.server.savedPassword;
|
|
2655
|
-
const keyring2 = await getServerPasswordKeyring();
|
|
2656
|
-
if (keyring2) {
|
|
2657
|
-
try {
|
|
2658
|
-
await keyring2.setPassword(pwd);
|
|
2659
|
-
delete config.server.savedPassword;
|
|
2660
|
-
if (Object.keys(config.server).length === 0) delete config.server;
|
|
2661
|
-
writeConfig(config);
|
|
2662
|
-
} catch {
|
|
2663
|
-
}
|
|
2664
|
-
}
|
|
2665
|
-
return pwd;
|
|
2666
|
-
}
|
|
2667
2857
|
const keyring = await getServerPasswordKeyring();
|
|
2668
|
-
|
|
2858
|
+
const config = readConfig();
|
|
2859
|
+
const legacy = config.server?.savedPassword;
|
|
2860
|
+
if (legacy && keyring) {
|
|
2669
2861
|
try {
|
|
2670
|
-
|
|
2862
|
+
await keyring.setPassword(legacy);
|
|
2863
|
+
delete config.server.savedPassword;
|
|
2864
|
+
if (Object.keys(config.server).length === 0) delete config.server;
|
|
2865
|
+
writeConfig(config);
|
|
2671
2866
|
} catch {
|
|
2672
|
-
return null;
|
|
2673
2867
|
}
|
|
2674
2868
|
}
|
|
2675
|
-
return null;
|
|
2869
|
+
if (!keyring) return null;
|
|
2870
|
+
try {
|
|
2871
|
+
return await keyring.getPassword();
|
|
2872
|
+
} catch {
|
|
2873
|
+
return null;
|
|
2874
|
+
}
|
|
2676
2875
|
}
|
|
2677
2876
|
async function setSavedServerPassword(password) {
|
|
2678
2877
|
const keyring = await getServerPasswordKeyring();
|
|
2679
|
-
if (keyring) {
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
} catch {
|
|
2684
|
-
}
|
|
2878
|
+
if (!keyring) {
|
|
2879
|
+
throw new Error(
|
|
2880
|
+
"Network-mode server password requires OS keychain support (@napi-rs/keyring). Install the optional dependency or start the server with `--password <value>` for this run only."
|
|
2881
|
+
);
|
|
2685
2882
|
}
|
|
2686
|
-
|
|
2687
|
-
config.server = {
|
|
2688
|
-
...config.server ?? {},
|
|
2689
|
-
savedPassword: password
|
|
2690
|
-
};
|
|
2691
|
-
writeConfig(config);
|
|
2883
|
+
await keyring.setPassword(password);
|
|
2692
2884
|
}
|
|
2693
2885
|
function getServerExposedProviders() {
|
|
2694
2886
|
const list = readConfig().server?.exposedProviders;
|
|
@@ -2746,6 +2938,9 @@ function setServerListenMode(listenMode) {
|
|
|
2746
2938
|
};
|
|
2747
2939
|
writeConfig(config);
|
|
2748
2940
|
}
|
|
2941
|
+
function getServerBudget() {
|
|
2942
|
+
return readConfig().server?.budget ?? {};
|
|
2943
|
+
}
|
|
2749
2944
|
|
|
2750
2945
|
// src/apps/shared/context-window.ts
|
|
2751
2946
|
import { readFileSync as readFileSync7 } from "fs";
|
|
@@ -3345,12 +3540,6 @@ function guiCallbackRedirectUri(host) {
|
|
|
3345
3540
|
return `http://${host}/auth/callback`;
|
|
3346
3541
|
}
|
|
3347
3542
|
|
|
3348
|
-
// src/auth/antigravity-oauth.ts
|
|
3349
|
-
import open2 from "open";
|
|
3350
|
-
import { readFileSync as readFileSync8 } from "fs";
|
|
3351
|
-
import { homedir as homedir2 } from "os";
|
|
3352
|
-
import { join as pathJoin } from "path";
|
|
3353
|
-
|
|
3354
3543
|
// src/auth/callback-server.ts
|
|
3355
3544
|
import http from "http";
|
|
3356
3545
|
var SUCCESS_HTML = `<!DOCTYPE html><html><head><meta charset="utf-8"><title>Authorized</title></head>
|
|
@@ -3405,6 +3594,10 @@ function startCallbackServer() {
|
|
|
3405
3594
|
}
|
|
3406
3595
|
|
|
3407
3596
|
// src/auth/antigravity-oauth.ts
|
|
3597
|
+
import open2 from "open";
|
|
3598
|
+
import { readFileSync as readFileSync8 } from "fs";
|
|
3599
|
+
import { homedir as homedir2 } from "os";
|
|
3600
|
+
import { join as pathJoin } from "path";
|
|
3408
3601
|
var DEFAULT_ANTIGRAVITY_CLIENT_ID = [
|
|
3409
3602
|
"107100606059",
|
|
3410
3603
|
"1-tmhssin2h2",
|
|
@@ -5054,6 +5247,16 @@ function parseModelList(body, npm, providerId) {
|
|
|
5054
5247
|
const freeStatus = classifyFreeStatus({
|
|
5055
5248
|
model: { cost, isFree: row.isFree }
|
|
5056
5249
|
});
|
|
5250
|
+
let reasoning;
|
|
5251
|
+
const caps = row.capabilities;
|
|
5252
|
+
if (caps && typeof caps === "object") {
|
|
5253
|
+
const capsObj = caps;
|
|
5254
|
+
const r = capsObj.reasoning;
|
|
5255
|
+
if (typeof r === "boolean") reasoning = r;
|
|
5256
|
+
}
|
|
5257
|
+
if (reasoning === void 0 && typeof row.reasoning === "boolean") {
|
|
5258
|
+
reasoning = row.reasoning;
|
|
5259
|
+
}
|
|
5057
5260
|
const contextWindow = row.context_length ?? row.contextWindow ?? row.context_window ?? resolveContextWindow(id, void 0, providerId);
|
|
5058
5261
|
models.push({
|
|
5059
5262
|
id,
|
|
@@ -5070,7 +5273,8 @@ function parseModelList(body, npm, providerId) {
|
|
|
5070
5273
|
providerId,
|
|
5071
5274
|
supportedParameters: Array.isArray(row.supported_parameters) ? row.supported_parameters : void 0,
|
|
5072
5275
|
useResponsesLite: typeof row.use_responses_lite === "boolean" ? row.use_responses_lite : void 0,
|
|
5073
|
-
preferWebSockets: typeof row.prefer_websockets === "boolean" ? row.prefer_websockets : void 0
|
|
5276
|
+
preferWebSockets: typeof row.prefer_websockets === "boolean" ? row.prefer_websockets : void 0,
|
|
5277
|
+
reasoning: reasoning === void 0 ? void 0 : reasoning
|
|
5074
5278
|
});
|
|
5075
5279
|
}
|
|
5076
5280
|
return models;
|
|
@@ -5321,47 +5525,102 @@ async function addProviderFromTemplate(template, apiKey, opts) {
|
|
|
5321
5525
|
|
|
5322
5526
|
// src/shared/http.ts
|
|
5323
5527
|
import * as zlib from "zlib";
|
|
5324
|
-
|
|
5325
|
-
|
|
5326
|
-
|
|
5327
|
-
|
|
5528
|
+
var RequestBodyTooLargeError = class extends Error {
|
|
5529
|
+
constructor(context) {
|
|
5530
|
+
super(`Request body too large (${context}, limit ${MAX_REQUEST_BODY_BYTES} bytes)`);
|
|
5531
|
+
this.name = "RequestBodyTooLargeError";
|
|
5532
|
+
}
|
|
5533
|
+
};
|
|
5534
|
+
function decodeBoundedChunk(createTransform, chunk, maxBytes) {
|
|
5535
|
+
return new Promise((resolve, reject) => {
|
|
5536
|
+
const transform = createTransform();
|
|
5537
|
+
const outChunks = [];
|
|
5538
|
+
let total = 0;
|
|
5539
|
+
let settled = false;
|
|
5540
|
+
const finish = (err) => {
|
|
5541
|
+
if (settled) return;
|
|
5542
|
+
settled = true;
|
|
5543
|
+
if (err) reject(err);
|
|
5544
|
+
else resolve(Buffer.concat(outChunks).toString());
|
|
5545
|
+
};
|
|
5546
|
+
transform.on("data", (out) => {
|
|
5547
|
+
total += out.length;
|
|
5548
|
+
if (total > maxBytes) {
|
|
5549
|
+
finish(new RequestBodyTooLargeError("decompressed"));
|
|
5550
|
+
transform.destroy();
|
|
5551
|
+
return;
|
|
5552
|
+
}
|
|
5553
|
+
outChunks.push(out);
|
|
5554
|
+
});
|
|
5555
|
+
transform.on("error", (err) => finish(err));
|
|
5556
|
+
transform.on("end", () => finish(null));
|
|
5557
|
+
transform.end(chunk);
|
|
5558
|
+
});
|
|
5559
|
+
}
|
|
5560
|
+
function decoderFor(encoding) {
|
|
5561
|
+
switch (encoding) {
|
|
5328
5562
|
case "gzip":
|
|
5329
5563
|
case "x-gzip":
|
|
5330
|
-
return
|
|
5564
|
+
return { label: encoding, createTransform: () => zlib.createGunzip() };
|
|
5331
5565
|
case "deflate":
|
|
5332
|
-
return
|
|
5566
|
+
return { label: encoding, createTransform: () => zlib.createInflate() };
|
|
5333
5567
|
case "br":
|
|
5334
|
-
return
|
|
5568
|
+
return { label: encoding, createTransform: () => zlib.createBrotliDecompress() };
|
|
5335
5569
|
case "zstd":
|
|
5336
|
-
if (typeof zlib.
|
|
5570
|
+
if (typeof zlib.createZstdDecompress !== "function") {
|
|
5337
5571
|
throw new Error("zstd request encoding requires Node >= 22.15");
|
|
5338
5572
|
}
|
|
5339
|
-
return
|
|
5573
|
+
return { label: encoding, createTransform: () => zlib.createZstdDecompress() };
|
|
5340
5574
|
default:
|
|
5341
|
-
return
|
|
5575
|
+
return { label: `${encoding || "identity"} (passthrough)` };
|
|
5576
|
+
}
|
|
5577
|
+
}
|
|
5578
|
+
async function inflateBounded(raw, spec, maxBytes) {
|
|
5579
|
+
if (!spec.createTransform) return raw.toString();
|
|
5580
|
+
const SLICE = 64 * 1024;
|
|
5581
|
+
let text4 = "";
|
|
5582
|
+
for (let offset = 0; offset < raw.length || offset === 0; offset += SLICE) {
|
|
5583
|
+
const slice = raw.subarray(offset, Math.min(offset + SLICE, raw.length));
|
|
5584
|
+
const part = await decodeBoundedChunk(spec.createTransform, slice, maxBytes - Buffer.byteLength(text4));
|
|
5585
|
+
text4 += part;
|
|
5586
|
+
if (Buffer.byteLength(text4) > maxBytes) {
|
|
5587
|
+
throw new RequestBodyTooLargeError(`decompressed (${spec.label})`);
|
|
5588
|
+
}
|
|
5589
|
+
if (offset + SLICE >= raw.length) break;
|
|
5342
5590
|
}
|
|
5591
|
+
return text4;
|
|
5592
|
+
}
|
|
5593
|
+
async function decodeRequestBody(raw, encoding) {
|
|
5594
|
+
const enc = (Array.isArray(encoding) ? encoding.join(",") : encoding ?? "").toLowerCase().trim();
|
|
5595
|
+
if (!enc || enc === "identity") return raw.toString();
|
|
5596
|
+
const spec = decoderFor(enc);
|
|
5597
|
+
return inflateBounded(raw, spec, MAX_REQUEST_BODY_BYTES);
|
|
5343
5598
|
}
|
|
5344
5599
|
function readBody(req) {
|
|
5345
5600
|
return new Promise((resolve, reject) => {
|
|
5346
5601
|
const chunks = [];
|
|
5347
5602
|
let totalSize = 0;
|
|
5603
|
+
let settled = false;
|
|
5604
|
+
const fail = (err) => {
|
|
5605
|
+
if (settled) return;
|
|
5606
|
+
settled = true;
|
|
5607
|
+
reject(err);
|
|
5608
|
+
req.destroy();
|
|
5609
|
+
};
|
|
5348
5610
|
req.on("data", (c) => {
|
|
5349
5611
|
totalSize += c.length;
|
|
5350
|
-
if (totalSize >
|
|
5351
|
-
|
|
5352
|
-
req.destroy();
|
|
5612
|
+
if (totalSize > MAX_REQUEST_BODY_BYTES) {
|
|
5613
|
+
fail(new RequestBodyTooLargeError("compressed"));
|
|
5353
5614
|
return;
|
|
5354
5615
|
}
|
|
5355
5616
|
chunks.push(c);
|
|
5356
5617
|
});
|
|
5357
5618
|
req.on("end", () => {
|
|
5358
|
-
|
|
5359
|
-
|
|
5360
|
-
|
|
5361
|
-
reject(err);
|
|
5362
|
-
}
|
|
5619
|
+
if (settled) return;
|
|
5620
|
+
settled = true;
|
|
5621
|
+
decodeRequestBody(Buffer.concat(chunks), req.headers["content-encoding"]).then(resolve, reject);
|
|
5363
5622
|
});
|
|
5364
|
-
req.on("error",
|
|
5623
|
+
req.on("error", fail);
|
|
5365
5624
|
});
|
|
5366
5625
|
}
|
|
5367
5626
|
function extractApiKey(req) {
|
|
@@ -5384,7 +5643,22 @@ function sendJson(res, status, body, extraHeaders) {
|
|
|
5384
5643
|
res.end(json);
|
|
5385
5644
|
}
|
|
5386
5645
|
var rateLimitStore = /* @__PURE__ */ new Map();
|
|
5646
|
+
var RATE_LIMIT_PRUNE_THRESHOLD = 1e4;
|
|
5647
|
+
function pruneRateLimits(now = Date.now()) {
|
|
5648
|
+
const staleBefore = now - 2 * RATE_LIMIT_WINDOW_MS;
|
|
5649
|
+
let pruned = 0;
|
|
5650
|
+
for (const [key, entry] of rateLimitStore) {
|
|
5651
|
+
if (entry.resetAt < staleBefore) {
|
|
5652
|
+
rateLimitStore.delete(key);
|
|
5653
|
+
pruned++;
|
|
5654
|
+
}
|
|
5655
|
+
}
|
|
5656
|
+
return pruned;
|
|
5657
|
+
}
|
|
5387
5658
|
function checkRateLimit(clientId) {
|
|
5659
|
+
if (rateLimitStore.size > RATE_LIMIT_PRUNE_THRESHOLD) {
|
|
5660
|
+
pruneRateLimits();
|
|
5661
|
+
}
|
|
5388
5662
|
const now = Date.now();
|
|
5389
5663
|
const entry = rateLimitStore.get(clientId);
|
|
5390
5664
|
if (!entry || now > entry.resetAt) {
|
|
@@ -5410,16 +5684,24 @@ import { appendFileSync as appendFileSync2, openSync as openSync4, writeSync as
|
|
|
5410
5684
|
import { Readable } from "stream";
|
|
5411
5685
|
|
|
5412
5686
|
// src/gateway/server/auth.ts
|
|
5687
|
+
import { timingSafeEqual } from "crypto";
|
|
5688
|
+
import { createHash as createHash2 } from "crypto";
|
|
5413
5689
|
function sanitizeCredential(value) {
|
|
5414
5690
|
if (!value) return null;
|
|
5415
5691
|
const firstLine = value.trim().split(/\r?\n/)[0]?.trim();
|
|
5416
5692
|
return firstLine || null;
|
|
5417
5693
|
}
|
|
5694
|
+
function timingSafeEqualStrings(a, b) {
|
|
5695
|
+
const da = createHash2("sha256").update(a, "utf8").digest();
|
|
5696
|
+
const db = createHash2("sha256").update(b, "utf8").digest();
|
|
5697
|
+
return timingSafeEqual(da, db);
|
|
5698
|
+
}
|
|
5418
5699
|
function isAuthorized(request, serverPassword) {
|
|
5419
5700
|
if (serverPassword === null) return true;
|
|
5420
5701
|
const bearerToken = extractBearerToken(request.headers.get("authorization"));
|
|
5421
|
-
if (bearerToken
|
|
5422
|
-
|
|
5702
|
+
if (bearerToken !== null && timingSafeEqualStrings(bearerToken, serverPassword)) return true;
|
|
5703
|
+
const apiKey = sanitizeCredential(request.headers.get("x-api-key"));
|
|
5704
|
+
return apiKey !== null && timingSafeEqualStrings(apiKey, serverPassword);
|
|
5423
5705
|
}
|
|
5424
5706
|
function extractBearerToken(value) {
|
|
5425
5707
|
if (!value) return null;
|
|
@@ -5584,73 +5866,169 @@ async function fetchWithOAuthRetry(apiKey, request, refreshToken) {
|
|
|
5584
5866
|
response = await request(refreshed);
|
|
5585
5867
|
return { response, apiKey: refreshed, refreshed: true };
|
|
5586
5868
|
}
|
|
5587
|
-
|
|
5588
|
-
const
|
|
5589
|
-
|
|
5590
|
-
|
|
5591
|
-
|
|
5592
|
-
|
|
5593
|
-
|
|
5594
|
-
|
|
5595
|
-
|
|
5596
|
-
|
|
5597
|
-
|
|
5598
|
-
|
|
5599
|
-
|
|
5600
|
-
|
|
5601
|
-
|
|
5602
|
-
|
|
5603
|
-
|
|
5604
|
-
|
|
5605
|
-
|
|
5606
|
-
throw new UpstreamUnreachableError(err);
|
|
5869
|
+
function scanAnthropicSseUsage(chunkText, usage) {
|
|
5870
|
+
for (const line of chunkText.split("\n")) {
|
|
5871
|
+
const t = line.startsWith("data:") ? line.slice(5).trim() : line.trim();
|
|
5872
|
+
if (!t || t === "[DONE]") continue;
|
|
5873
|
+
try {
|
|
5874
|
+
const obj = JSON.parse(t);
|
|
5875
|
+
const startUsage = obj.message?.usage;
|
|
5876
|
+
if (startUsage) {
|
|
5877
|
+
usage.inputTokens = startUsage.input_tokens ?? usage.inputTokens;
|
|
5878
|
+
if (typeof startUsage.output_tokens === "number")
|
|
5879
|
+
usage.outputTokens = startUsage.output_tokens;
|
|
5880
|
+
continue;
|
|
5881
|
+
}
|
|
5882
|
+
const deltaUsage = obj.usage;
|
|
5883
|
+
if (deltaUsage && typeof deltaUsage.output_tokens === "number") {
|
|
5884
|
+
usage.outputTokens = deltaUsage.output_tokens;
|
|
5885
|
+
}
|
|
5886
|
+
} catch {
|
|
5887
|
+
}
|
|
5607
5888
|
}
|
|
5608
|
-
|
|
5609
|
-
|
|
5610
|
-
|
|
5611
|
-
|
|
5612
|
-
|
|
5889
|
+
}
|
|
5890
|
+
async function forwardAnthropicMessages(res, messagesUrl, body, apiKey, clientWantsStream, inboundBeta, authType, log6, claudeCodeSessionId, extraHeaders, refreshToken, onTokenRefreshed, timeoutMs = REQUEST_TIMEOUT_MS) {
|
|
5891
|
+
const usage = { inputTokens: 0, outputTokens: 0 };
|
|
5892
|
+
const disconnectController = new AbortController();
|
|
5893
|
+
const onClientGone = () => disconnectController.abort();
|
|
5894
|
+
res.on("close", onClientGone);
|
|
5895
|
+
res.on("error", onClientGone);
|
|
5896
|
+
const timeoutController = new AbortController();
|
|
5897
|
+
const timer = setTimeout(() => timeoutController.abort(), timeoutMs);
|
|
5898
|
+
const combinedSignal = disconnectController.signal.aborted ? disconnectController.signal : connectAbortSignals(disconnectController.signal, timeoutController.signal);
|
|
5899
|
+
try {
|
|
5900
|
+
return await forwardInner(combinedSignal);
|
|
5901
|
+
} finally {
|
|
5902
|
+
clearTimeout(timer);
|
|
5903
|
+
res.off("close", onClientGone);
|
|
5904
|
+
res.off("error", onClientGone);
|
|
5905
|
+
}
|
|
5906
|
+
function connectAbortSignals(a, b) {
|
|
5907
|
+
const merged = new AbortController();
|
|
5908
|
+
const relay = () => merged.abort();
|
|
5909
|
+
if (a.aborted || b.aborted) {
|
|
5910
|
+
merged.abort();
|
|
5911
|
+
return merged.signal;
|
|
5912
|
+
}
|
|
5913
|
+
a.addEventListener("abort", relay, { once: true });
|
|
5914
|
+
b.addEventListener("abort", relay, { once: true });
|
|
5915
|
+
return merged.signal;
|
|
5916
|
+
}
|
|
5917
|
+
async function forwardInner(signal) {
|
|
5918
|
+
const doFetch = (key) => fetch(messagesUrl, {
|
|
5919
|
+
method: "POST",
|
|
5920
|
+
headers: anthropicUpstreamHeaders(
|
|
5921
|
+
key,
|
|
5922
|
+
clientWantsStream,
|
|
5923
|
+
inboundBeta,
|
|
5924
|
+
authType,
|
|
5925
|
+
claudeCodeSessionId,
|
|
5926
|
+
extraHeaders
|
|
5927
|
+
),
|
|
5928
|
+
body: JSON.stringify(body),
|
|
5929
|
+
signal
|
|
5613
5930
|
});
|
|
5614
|
-
|
|
5615
|
-
|
|
5616
|
-
|
|
5617
|
-
|
|
5931
|
+
let upstreamRes;
|
|
5932
|
+
try {
|
|
5933
|
+
const retryResult = await fetchWithOAuthRetry(apiKey, doFetch, refreshToken);
|
|
5934
|
+
upstreamRes = retryResult.response;
|
|
5935
|
+
if (retryResult.refreshed) onTokenRefreshed?.(retryResult.apiKey);
|
|
5936
|
+
} catch (err) {
|
|
5937
|
+
if (signal.aborted) {
|
|
5938
|
+
emitAborted(res, clientWantsStream);
|
|
5939
|
+
return usage;
|
|
5940
|
+
}
|
|
5941
|
+
throw new UpstreamUnreachableError(err);
|
|
5942
|
+
}
|
|
5943
|
+
if (!upstreamRes.ok) {
|
|
5944
|
+
const errBody = await upstreamRes.text();
|
|
5945
|
+
log6?.(`anthropic upstream ${upstreamRes.status}: ${errBody}`);
|
|
5946
|
+
res.writeHead(upstreamRes.status, {
|
|
5947
|
+
"Content-Type": upstreamRes.headers.get("content-type") || "application/json"
|
|
5948
|
+
});
|
|
5949
|
+
res.end(errBody);
|
|
5950
|
+
return usage;
|
|
5951
|
+
}
|
|
5952
|
+
if (clientWantsStream && upstreamRes.body) {
|
|
5953
|
+
res.writeHead(200, {
|
|
5954
|
+
"Content-Type": "text/event-stream",
|
|
5955
|
+
"Cache-Control": "no-cache",
|
|
5956
|
+
Connection: "keep-alive"
|
|
5957
|
+
});
|
|
5958
|
+
await new Promise((resolve, reject) => {
|
|
5959
|
+
Readable.fromWeb(upstreamRes.body).on("data", (chunk) => scanAnthropicSseUsage(chunk.toString("utf8"), usage)).on("error", (err) => {
|
|
5960
|
+
res.destroy();
|
|
5961
|
+
reject(err);
|
|
5962
|
+
}).on("end", () => resolve()).pipe(res);
|
|
5963
|
+
}).catch(() => {
|
|
5964
|
+
});
|
|
5965
|
+
return usage;
|
|
5966
|
+
}
|
|
5967
|
+
if (!upstreamRes.body) {
|
|
5968
|
+
res.writeHead(502, { "Content-Type": "application/json" });
|
|
5969
|
+
res.end(
|
|
5970
|
+
JSON.stringify({
|
|
5971
|
+
type: "error",
|
|
5972
|
+
error: { type: "api_error", message: "Upstream returned empty response body" }
|
|
5973
|
+
})
|
|
5974
|
+
);
|
|
5975
|
+
return usage;
|
|
5976
|
+
}
|
|
5977
|
+
let text4;
|
|
5978
|
+
try {
|
|
5979
|
+
text4 = await upstreamRes.text();
|
|
5980
|
+
} catch {
|
|
5981
|
+
emitAborted(res, clientWantsStream);
|
|
5982
|
+
return usage;
|
|
5983
|
+
}
|
|
5984
|
+
let parsed;
|
|
5985
|
+
try {
|
|
5986
|
+
parsed = JSON.parse(text4);
|
|
5987
|
+
} catch {
|
|
5988
|
+
res.writeHead(502, { "Content-Type": "application/json" });
|
|
5989
|
+
res.end(
|
|
5990
|
+
JSON.stringify({
|
|
5991
|
+
type: "error",
|
|
5992
|
+
error: { type: "api_error", message: "Upstream response was not valid JSON" }
|
|
5993
|
+
})
|
|
5994
|
+
);
|
|
5995
|
+
return usage;
|
|
5996
|
+
}
|
|
5997
|
+
const parsedUsage = parsed.usage;
|
|
5998
|
+
if (parsedUsage) {
|
|
5999
|
+
usage.inputTokens = parsedUsage.input_tokens ?? 0;
|
|
6000
|
+
usage.outputTokens = parsedUsage.output_tokens ?? 0;
|
|
6001
|
+
}
|
|
5618
6002
|
res.writeHead(200, {
|
|
5619
|
-
"Content-Type": "
|
|
5620
|
-
"
|
|
5621
|
-
Connection: "keep-alive"
|
|
6003
|
+
"Content-Type": "application/json",
|
|
6004
|
+
"Content-Length": Buffer.byteLength(text4).toString()
|
|
5622
6005
|
});
|
|
5623
|
-
|
|
5624
|
-
return;
|
|
6006
|
+
res.end(text4);
|
|
6007
|
+
return usage;
|
|
5625
6008
|
}
|
|
5626
|
-
|
|
5627
|
-
|
|
5628
|
-
|
|
5629
|
-
|
|
5630
|
-
type: "error",
|
|
5631
|
-
error: { type: "api_error", message: "Upstream returned empty response body" }
|
|
5632
|
-
})
|
|
5633
|
-
);
|
|
6009
|
+
}
|
|
6010
|
+
function emitAborted(res, clientWantsStream) {
|
|
6011
|
+
if (res.headersSent || res.writableEnded || res.destroyed) {
|
|
6012
|
+
res.destroy();
|
|
5634
6013
|
return;
|
|
5635
6014
|
}
|
|
5636
|
-
|
|
5637
|
-
|
|
5638
|
-
JSON.parse(text4);
|
|
5639
|
-
} catch {
|
|
5640
|
-
res.writeHead(502, { "Content-Type": "application/json" });
|
|
6015
|
+
if (clientWantsStream) {
|
|
6016
|
+
res.writeHead(504, { "Content-Type": "application/json" });
|
|
5641
6017
|
res.end(
|
|
5642
6018
|
JSON.stringify({
|
|
5643
6019
|
type: "error",
|
|
5644
|
-
error: { type: "
|
|
6020
|
+
error: { type: "timeout", message: "Upstream request timed out or was aborted." }
|
|
5645
6021
|
})
|
|
5646
6022
|
);
|
|
5647
6023
|
return;
|
|
5648
6024
|
}
|
|
5649
|
-
res.writeHead(
|
|
5650
|
-
|
|
5651
|
-
|
|
5652
|
-
|
|
5653
|
-
|
|
6025
|
+
res.writeHead(504, { "Content-Type": "application/json" });
|
|
6026
|
+
res.end(
|
|
6027
|
+
JSON.stringify({
|
|
6028
|
+
type: "error",
|
|
6029
|
+
error: { type: "timeout", message: "Upstream request timed out or was aborted." }
|
|
6030
|
+
})
|
|
6031
|
+
);
|
|
5654
6032
|
}
|
|
5655
6033
|
|
|
5656
6034
|
// src/gateway/antigravity/anthropic-to-cloudcode.ts
|
|
@@ -6443,7 +6821,7 @@ async function collectCloudCodeToAnthropic(upstreamRes, model, log6) {
|
|
|
6443
6821
|
import { randomUUID as randomUUID5 } from "crypto";
|
|
6444
6822
|
|
|
6445
6823
|
// src/gateway/adapters/sdk-adapter.ts
|
|
6446
|
-
import { streamText,
|
|
6824
|
+
import { streamText, tool as tool3, jsonSchema as jsonSchema3, stepCountIs } from "ai";
|
|
6447
6825
|
|
|
6448
6826
|
// src/apps/shared/tool-search.ts
|
|
6449
6827
|
var TOOL_SEARCH_TYPE_PREFIX = "tool_search_tool";
|
|
@@ -6832,15 +7210,21 @@ function annotateToolNames(messages) {
|
|
|
6832
7210
|
}
|
|
6833
7211
|
function thinkingToSdkPart(block, npm) {
|
|
6834
7212
|
const text4 = block.thinking ?? "";
|
|
6835
|
-
if (npm === "@ai-sdk/openai" && !block.signature && !text4.trim())
|
|
7213
|
+
if ((npm === "@ai-sdk/openai" || npm === "@ai-sdk/openai-compatible") && !block.signature && !text4.trim())
|
|
7214
|
+
return null;
|
|
6836
7215
|
const part = { type: "reasoning", text: text4 };
|
|
6837
7216
|
if (block.signature) {
|
|
6838
7217
|
if (npm === "@ai-sdk/google") {
|
|
6839
7218
|
part.providerOptions = { google: { thoughtSignature: block.signature } };
|
|
6840
|
-
} else if (npm === "@ai-sdk/openai"
|
|
7219
|
+
} else if (npm === "@ai-sdk/openai") {
|
|
6841
7220
|
part.providerOptions = { openai: { reasoningEncryptedContent: block.signature } };
|
|
7221
|
+
} else if (npm === "@ai-sdk/openai-compatible") {
|
|
7222
|
+
part.providerOptions = { openaiCompatible: { reasoningEncryptedContent: block.signature } };
|
|
6842
7223
|
}
|
|
6843
7224
|
}
|
|
7225
|
+
if (npm === "@ai-sdk/openai-compatible" && text4) {
|
|
7226
|
+
part.reasoning_content = text4;
|
|
7227
|
+
}
|
|
6844
7228
|
return part;
|
|
6845
7229
|
}
|
|
6846
7230
|
function translateMessages(messages, npm) {
|
|
@@ -7103,14 +7487,18 @@ async function writeAnthropicStream(fullStream, modelId, write, log6, hiddenTool
|
|
|
7103
7487
|
hiddenIds.add(part.id ?? "");
|
|
7104
7488
|
break;
|
|
7105
7489
|
}
|
|
7490
|
+
const toolKey = part.id ?? "";
|
|
7491
|
+
if (idToBlock.has(toolKey)) {
|
|
7492
|
+
break;
|
|
7493
|
+
}
|
|
7106
7494
|
const sig = grabRoundTripSignature(part);
|
|
7107
7495
|
openBlock("tool", {
|
|
7108
7496
|
type: "tool_use",
|
|
7109
|
-
id: encodeToolUseId(
|
|
7497
|
+
id: encodeToolUseId(toolKey, sig),
|
|
7110
7498
|
name: part.toolName,
|
|
7111
7499
|
input: {}
|
|
7112
7500
|
});
|
|
7113
|
-
idToBlock.set(
|
|
7501
|
+
idToBlock.set(toolKey, blockIndex);
|
|
7114
7502
|
break;
|
|
7115
7503
|
}
|
|
7116
7504
|
case "tool-input-delta":
|
|
@@ -7216,29 +7604,32 @@ async function generateAnthropicResponse(model, params, modelId, options) {
|
|
|
7216
7604
|
let toolCalls;
|
|
7217
7605
|
let finishReason;
|
|
7218
7606
|
let usage;
|
|
7607
|
+
let reasoningText = "";
|
|
7219
7608
|
const { webSearchToolName, ...callParams } = params;
|
|
7220
7609
|
const stopWhen = webSearchToolName ? stepCountIs(MAX_WEB_SEARCH_STEPS) : void 0;
|
|
7221
|
-
|
|
7222
|
-
|
|
7223
|
-
|
|
7224
|
-
|
|
7225
|
-
|
|
7226
|
-
|
|
7227
|
-
|
|
7228
|
-
|
|
7229
|
-
r.finishReason,
|
|
7230
|
-
r.usage
|
|
7231
|
-
]);
|
|
7232
|
-
} else {
|
|
7233
|
-
const r = await generateText({ model, ...callParams, stopWhen });
|
|
7234
|
-
({ text: text4, toolCalls, finishReason, usage } = r);
|
|
7610
|
+
const r = streamText({ model, ...callParams, stopWhen, onError: () => {
|
|
7611
|
+
} });
|
|
7612
|
+
Promise.resolve(r.toolResults).catch(() => {
|
|
7613
|
+
});
|
|
7614
|
+
for await (const part of r.fullStream) {
|
|
7615
|
+
if (part.type === "reasoning-delta") {
|
|
7616
|
+
reasoningText += part.text ?? "";
|
|
7617
|
+
}
|
|
7235
7618
|
}
|
|
7619
|
+
;
|
|
7620
|
+
[text4, toolCalls, finishReason, usage] = await Promise.all([
|
|
7621
|
+
r.text,
|
|
7622
|
+
r.toolCalls,
|
|
7623
|
+
r.finishReason,
|
|
7624
|
+
r.usage
|
|
7625
|
+
]);
|
|
7236
7626
|
return {
|
|
7237
7627
|
id: "msg_" + Date.now(),
|
|
7238
7628
|
type: "message",
|
|
7239
7629
|
role: "assistant",
|
|
7240
7630
|
model: modelId,
|
|
7241
7631
|
content: [
|
|
7632
|
+
...reasoningText ? [{ type: "thinking", thinking: reasoningText, signature: "" }] : [],
|
|
7242
7633
|
...text4 ? [{ type: "text", text: text4 }] : [],
|
|
7243
7634
|
...toolCalls.filter((tc) => tc.toolName !== webSearchToolName).map((tc) => ({
|
|
7244
7635
|
type: "tool_use",
|
|
@@ -7673,10 +8064,10 @@ function startProxyCatalog(routes, defaultAliasId, debug = false, opts) {
|
|
|
7673
8064
|
const originalModel = anthropicBody.model;
|
|
7674
8065
|
const clientWantsStream = Boolean(anthropicBody.stream);
|
|
7675
8066
|
const route = resolveRoute(byAlias, originalModel, defaultRoute, plog);
|
|
8067
|
+
let aliasWarning = null;
|
|
7676
8068
|
if (route === defaultRoute && originalModel !== defaultRoute.aliasId) {
|
|
7677
|
-
|
|
7678
|
-
|
|
7679
|
-
);
|
|
8069
|
+
aliasWarning = `model alias '${originalModel}' not found; using '${defaultRoute.aliasId}'`;
|
|
8070
|
+
quietErrorLog(`POST /v1/messages - ${aliasWarning}`);
|
|
7680
8071
|
}
|
|
7681
8072
|
const apiKey = route.apiKey;
|
|
7682
8073
|
const upstreamUrl = route.upstreamUrl;
|
|
@@ -7772,8 +8163,12 @@ function startProxyCatalog(routes, defaultAliasId, debug = false, opts) {
|
|
|
7772
8163
|
res.writeHead(200, {
|
|
7773
8164
|
"Content-Type": "text/event-stream",
|
|
7774
8165
|
"Cache-Control": "no-cache",
|
|
7775
|
-
Connection: "keep-alive"
|
|
8166
|
+
Connection: "keep-alive",
|
|
8167
|
+
...aliasWarning ? { "anthropic-warning": aliasWarning } : {}
|
|
7776
8168
|
});
|
|
8169
|
+
if (aliasWarning) res.write(`: ${aliasWarning}
|
|
8170
|
+
|
|
8171
|
+
`);
|
|
7777
8172
|
const usage = await streamAnthropicResponse(
|
|
7778
8173
|
model,
|
|
7779
8174
|
params,
|
|
@@ -7808,7 +8203,12 @@ function startProxyCatalog(routes, defaultAliasId, debug = false, opts) {
|
|
|
7808
8203
|
inputTokens: u?.inputTokens ?? 0,
|
|
7809
8204
|
outputTokens: u?.outputTokens ?? 0
|
|
7810
8205
|
});
|
|
7811
|
-
sendJson(
|
|
8206
|
+
sendJson(
|
|
8207
|
+
res,
|
|
8208
|
+
200,
|
|
8209
|
+
anthropicResponse,
|
|
8210
|
+
aliasWarning ? { "anthropic-warning": aliasWarning } : void 0
|
|
8211
|
+
);
|
|
7812
8212
|
}
|
|
7813
8213
|
} catch (err) {
|
|
7814
8214
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -10913,6 +11313,206 @@ async function streamOpenAiResponse(model, params, responseModelId, onChunk) {
|
|
|
10913
11313
|
return usage;
|
|
10914
11314
|
}
|
|
10915
11315
|
|
|
11316
|
+
// src/gateway/server/budget.ts
|
|
11317
|
+
import { existsSync as existsSync15, mkdirSync as mkdirSync9, openSync as openSync5, readFileSync as readFileSync14, writeSync as writeSync5, closeSync as closeSync5 } from "fs";
|
|
11318
|
+
import { dirname as dirname7, join as join14 } from "path";
|
|
11319
|
+
var STATE_FILENAME = "budget-state.json";
|
|
11320
|
+
function utcDayKey(now = /* @__PURE__ */ new Date()) {
|
|
11321
|
+
return now.toISOString().slice(0, 10);
|
|
11322
|
+
}
|
|
11323
|
+
function utcMonthKey(now = /* @__PURE__ */ new Date()) {
|
|
11324
|
+
return now.toISOString().slice(0, 7);
|
|
11325
|
+
}
|
|
11326
|
+
function statePath() {
|
|
11327
|
+
return join14(getAppHome(), STATE_FILENAME);
|
|
11328
|
+
}
|
|
11329
|
+
function emptyState(now = /* @__PURE__ */ new Date()) {
|
|
11330
|
+
return { day: utcDayKey(now), dayTokens: 0, month: utcMonthKey(now), monthTokens: 0 };
|
|
11331
|
+
}
|
|
11332
|
+
function readStateFile() {
|
|
11333
|
+
const path = statePath();
|
|
11334
|
+
if (!existsSync15(path)) return null;
|
|
11335
|
+
try {
|
|
11336
|
+
const raw = JSON.parse(readFileSync14(path, "utf8"));
|
|
11337
|
+
if (typeof raw.day !== "string" || typeof raw.month !== "string" || typeof raw.dayTokens !== "number" || typeof raw.monthTokens !== "number") {
|
|
11338
|
+
return null;
|
|
11339
|
+
}
|
|
11340
|
+
return raw;
|
|
11341
|
+
} catch {
|
|
11342
|
+
return null;
|
|
11343
|
+
}
|
|
11344
|
+
}
|
|
11345
|
+
function writeState(state) {
|
|
11346
|
+
const path = statePath();
|
|
11347
|
+
try {
|
|
11348
|
+
mkdirSync9(dirname7(path), { recursive: true, mode: 448 });
|
|
11349
|
+
} catch {
|
|
11350
|
+
}
|
|
11351
|
+
try {
|
|
11352
|
+
const json = `${JSON.stringify(state)}
|
|
11353
|
+
`;
|
|
11354
|
+
const fd = openSync5(path, "w", 384);
|
|
11355
|
+
try {
|
|
11356
|
+
writeSync5(fd, json);
|
|
11357
|
+
} finally {
|
|
11358
|
+
closeSync5(fd);
|
|
11359
|
+
}
|
|
11360
|
+
} catch {
|
|
11361
|
+
}
|
|
11362
|
+
}
|
|
11363
|
+
var BudgetTracker = class {
|
|
11364
|
+
constructor(config, now = /* @__PURE__ */ new Date()) {
|
|
11365
|
+
this.config = config;
|
|
11366
|
+
this.state = this.loadRollover(now);
|
|
11367
|
+
}
|
|
11368
|
+
config;
|
|
11369
|
+
state;
|
|
11370
|
+
loadRollover(now) {
|
|
11371
|
+
const persisted = readStateFile();
|
|
11372
|
+
const day = utcDayKey(now);
|
|
11373
|
+
const month = utcMonthKey(now);
|
|
11374
|
+
if (!persisted) return emptyState(now);
|
|
11375
|
+
return {
|
|
11376
|
+
day,
|
|
11377
|
+
// Day/month rollover: stale periods reset to zero automatically.
|
|
11378
|
+
dayTokens: persisted.day === day ? persisted.dayTokens : 0,
|
|
11379
|
+
month,
|
|
11380
|
+
monthTokens: persisted.month === month ? persisted.monthTokens : 0
|
|
11381
|
+
};
|
|
11382
|
+
}
|
|
11383
|
+
get enabled() {
|
|
11384
|
+
return this.config.enabled === true;
|
|
11385
|
+
}
|
|
11386
|
+
dayExceeded() {
|
|
11387
|
+
const max = this.config.maxTokensPerDay;
|
|
11388
|
+
return typeof max === "number" && max > 0 && this.state.dayTokens >= max;
|
|
11389
|
+
}
|
|
11390
|
+
monthExceeded() {
|
|
11391
|
+
const max = this.config.maxTokensPerMonth;
|
|
11392
|
+
return typeof max === "number" && max > 0 && this.state.monthTokens >= max;
|
|
11393
|
+
}
|
|
11394
|
+
/** Check whether requests may proceed under the configured budgets. */
|
|
11395
|
+
check() {
|
|
11396
|
+
if (!this.enabled) return { allowed: true };
|
|
11397
|
+
const now = /* @__PURE__ */ new Date();
|
|
11398
|
+
if (this.state.day !== utcDayKey(now)) this.state.dayTokens = 0;
|
|
11399
|
+
if (this.state.month !== utcMonthKey(now)) this.state.monthTokens = 0;
|
|
11400
|
+
if (this.dayExceeded()) {
|
|
11401
|
+
return {
|
|
11402
|
+
allowed: false,
|
|
11403
|
+
reason: `Daily token budget exceeded (${this.state.dayTokens}/${this.config.maxTokensPerDay}).`
|
|
11404
|
+
};
|
|
11405
|
+
}
|
|
11406
|
+
if (this.monthExceeded()) {
|
|
11407
|
+
return {
|
|
11408
|
+
allowed: false,
|
|
11409
|
+
reason: `Monthly token budget exceeded (${this.state.monthTokens}/${this.config.maxTokensPerMonth}).`
|
|
11410
|
+
};
|
|
11411
|
+
}
|
|
11412
|
+
return { allowed: true };
|
|
11413
|
+
}
|
|
11414
|
+
/** Record actual usage after a completed response. */
|
|
11415
|
+
add(inputTokens, outputTokens) {
|
|
11416
|
+
const total = Math.max(0, inputTokens) + Math.max(0, outputTokens);
|
|
11417
|
+
if (total === 0) return;
|
|
11418
|
+
const now = /* @__PURE__ */ new Date();
|
|
11419
|
+
if (this.state.day !== utcDayKey(now)) {
|
|
11420
|
+
this.state.day = utcDayKey(now);
|
|
11421
|
+
this.state.dayTokens = 0;
|
|
11422
|
+
}
|
|
11423
|
+
if (this.state.month !== utcMonthKey(now)) {
|
|
11424
|
+
this.state.month = utcMonthKey(now);
|
|
11425
|
+
this.state.monthTokens = 0;
|
|
11426
|
+
}
|
|
11427
|
+
this.state.dayTokens += total;
|
|
11428
|
+
this.state.monthTokens += total;
|
|
11429
|
+
writeState(this.state);
|
|
11430
|
+
}
|
|
11431
|
+
/** Current counters (for dashboards/tests). */
|
|
11432
|
+
snapshot() {
|
|
11433
|
+
return { ...this.state };
|
|
11434
|
+
}
|
|
11435
|
+
};
|
|
11436
|
+
|
|
11437
|
+
// src/services/provider-health.ts
|
|
11438
|
+
var healthMap = /* @__PURE__ */ new Map();
|
|
11439
|
+
function updateProviderHealth(status) {
|
|
11440
|
+
healthMap.set(status.providerId, status);
|
|
11441
|
+
}
|
|
11442
|
+
|
|
11443
|
+
// src/gateway/server/upstream-resilience.ts
|
|
11444
|
+
var MAX_UPSTREAM_RETRIES = 2;
|
|
11445
|
+
var UNHEALTHY_WINDOW_MS = 6e4;
|
|
11446
|
+
var RETRYABLE_STATUS = /* @__PURE__ */ new Set([429, 500, 502, 503, 504]);
|
|
11447
|
+
function isRetryableUpstreamError(err) {
|
|
11448
|
+
if (!err || typeof err !== "object") return false;
|
|
11449
|
+
const rec = err;
|
|
11450
|
+
for (const candidate of [rec.statusCode, rec.httpStatus]) {
|
|
11451
|
+
if (typeof candidate === "number" && RETRYABLE_STATUS.has(candidate)) return true;
|
|
11452
|
+
}
|
|
11453
|
+
return false;
|
|
11454
|
+
}
|
|
11455
|
+
function backoffDelayMs(attempt, retryAfterMs) {
|
|
11456
|
+
if (retryAfterMs != null) return retryAfterMs;
|
|
11457
|
+
return 500 * 2 ** attempt;
|
|
11458
|
+
}
|
|
11459
|
+
async function withUpstreamRetry(fn, onRetry, sleep = defaultSleep) {
|
|
11460
|
+
let lastErr;
|
|
11461
|
+
for (let attempt = 0; attempt <= MAX_UPSTREAM_RETRIES; attempt++) {
|
|
11462
|
+
try {
|
|
11463
|
+
return await fn();
|
|
11464
|
+
} catch (err) {
|
|
11465
|
+
lastErr = err;
|
|
11466
|
+
if (attempt === MAX_UPSTREAM_RETRIES || !isRetryableUpstreamError(err)) throw err;
|
|
11467
|
+
const delay = backoffDelayMs(attempt);
|
|
11468
|
+
await onRetry?.(attempt + 1, delay, err);
|
|
11469
|
+
await sleep(delay);
|
|
11470
|
+
}
|
|
11471
|
+
}
|
|
11472
|
+
throw lastErr;
|
|
11473
|
+
}
|
|
11474
|
+
function defaultSleep(ms) {
|
|
11475
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
11476
|
+
}
|
|
11477
|
+
var unhealthyUntil = /* @__PURE__ */ new Map();
|
|
11478
|
+
function recordProviderFailure(providerId, now = Date.now()) {
|
|
11479
|
+
const existing = failCounts.get(providerId) ?? { count: 0, firstAt: now };
|
|
11480
|
+
const count = now - existing.firstAt > UNHEALTHY_WINDOW_MS ? 1 : existing.count + 1;
|
|
11481
|
+
failCounts.set(providerId, { count, firstAt: count === 1 ? now : existing.firstAt });
|
|
11482
|
+
if (count >= 2) {
|
|
11483
|
+
unhealthyUntil.set(providerId, now + UNHEALTHY_WINDOW_MS);
|
|
11484
|
+
updateProviderHealthSafe(providerId, false, now);
|
|
11485
|
+
}
|
|
11486
|
+
}
|
|
11487
|
+
function recordProviderSuccess(providerId, now = Date.now()) {
|
|
11488
|
+
failCounts.delete(providerId);
|
|
11489
|
+
unhealthyUntil.delete(providerId);
|
|
11490
|
+
updateProviderHealthSafe(providerId, true, now);
|
|
11491
|
+
}
|
|
11492
|
+
function isProviderUnavailable(providerId, now = Date.now()) {
|
|
11493
|
+
const until = unhealthyUntil.get(providerId);
|
|
11494
|
+
if (until === void 0) return false;
|
|
11495
|
+
if (now >= until) {
|
|
11496
|
+
unhealthyUntil.delete(providerId);
|
|
11497
|
+
failCounts.delete(providerId);
|
|
11498
|
+
updateProviderHealthSafe(providerId, true, now);
|
|
11499
|
+
return false;
|
|
11500
|
+
}
|
|
11501
|
+
return true;
|
|
11502
|
+
}
|
|
11503
|
+
var failCounts = /* @__PURE__ */ new Map();
|
|
11504
|
+
function updateProviderHealthSafe(providerId, healthy, now) {
|
|
11505
|
+
try {
|
|
11506
|
+
updateProviderHealth({
|
|
11507
|
+
providerId,
|
|
11508
|
+
healthy,
|
|
11509
|
+
lastChecked: now,
|
|
11510
|
+
...healthy ? {} : { error: "repeated upstream failures" }
|
|
11511
|
+
});
|
|
11512
|
+
} catch {
|
|
11513
|
+
}
|
|
11514
|
+
}
|
|
11515
|
+
|
|
10916
11516
|
// src/gateway/server/router.ts
|
|
10917
11517
|
function makeServerLog(debugLogPath) {
|
|
10918
11518
|
if (!debugLogPath) return () => {
|
|
@@ -10924,8 +11524,10 @@ async function startServer(options) {
|
|
|
10924
11524
|
silenceSdkWarnings();
|
|
10925
11525
|
const languageModelCache = /* @__PURE__ */ new Map();
|
|
10926
11526
|
const plog = makeServerLog(options.debugLogPath);
|
|
11527
|
+
const budgetConfig = options.budget ?? getServerBudget();
|
|
11528
|
+
const budget = new BudgetTracker(budgetConfig);
|
|
10927
11529
|
const server = createServer2((req, res) => {
|
|
10928
|
-
void routeRequest(req, res, options, languageModelCache, plog);
|
|
11530
|
+
void routeRequest(req, res, options, languageModelCache, plog, budget);
|
|
10929
11531
|
});
|
|
10930
11532
|
await new Promise((resolve, reject) => {
|
|
10931
11533
|
server.once("error", reject);
|
|
@@ -10948,7 +11550,7 @@ async function startServer(options) {
|
|
|
10948
11550
|
})
|
|
10949
11551
|
};
|
|
10950
11552
|
}
|
|
10951
|
-
async function routeRequest(req, res, options, modelCache, plog) {
|
|
11553
|
+
async function routeRequest(req, res, options, modelCache, plog, budget) {
|
|
10952
11554
|
try {
|
|
10953
11555
|
const pathname = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`).pathname;
|
|
10954
11556
|
plog(`${req.method} ${pathname}`);
|
|
@@ -10960,7 +11562,7 @@ async function routeRequest(req, res, options, modelCache, plog) {
|
|
|
10960
11562
|
sendJson(res, 401, { error: { message: "Unauthorized" } });
|
|
10961
11563
|
return;
|
|
10962
11564
|
}
|
|
10963
|
-
const clientId = req.
|
|
11565
|
+
const clientId = req.socket.remoteAddress ?? "unknown";
|
|
10964
11566
|
const rateLimit = checkRateLimit(String(clientId));
|
|
10965
11567
|
if (!rateLimit.allowed) {
|
|
10966
11568
|
sendJson(
|
|
@@ -10988,11 +11590,11 @@ async function routeRequest(req, res, options, modelCache, plog) {
|
|
|
10988
11590
|
return;
|
|
10989
11591
|
}
|
|
10990
11592
|
if (req.method === "POST" && pathname === "/anthropic/v1/messages") {
|
|
10991
|
-
await handleAnthropicMessages(req, res, options, modelCache, plog);
|
|
11593
|
+
await handleAnthropicMessages(req, res, options, modelCache, plog, budget);
|
|
10992
11594
|
return;
|
|
10993
11595
|
}
|
|
10994
11596
|
if (req.method === "POST" && pathname === "/openai/v1/chat/completions") {
|
|
10995
|
-
await handleOpenAIChatCompletions(req, res, options, modelCache, plog);
|
|
11597
|
+
await handleOpenAIChatCompletions(req, res, options, modelCache, plog, budget);
|
|
10996
11598
|
return;
|
|
10997
11599
|
}
|
|
10998
11600
|
sendJson(res, 404, { error: { message: "Not found" } });
|
|
@@ -11007,12 +11609,19 @@ async function routeRequest(req, res, options, modelCache, plog) {
|
|
|
11007
11609
|
}
|
|
11008
11610
|
}
|
|
11009
11611
|
}
|
|
11010
|
-
async function handleAnthropicMessages(req, res, options, modelCache, plog) {
|
|
11612
|
+
async function handleAnthropicMessages(req, res, options, modelCache, plog, budget) {
|
|
11011
11613
|
const body = await readJson(req);
|
|
11012
11614
|
if (!body) {
|
|
11013
11615
|
sendJson(res, 400, { error: { message: "Invalid JSON body" } });
|
|
11014
11616
|
return;
|
|
11015
11617
|
}
|
|
11618
|
+
const budgetCheck = budget.check();
|
|
11619
|
+
if (!budgetCheck.allowed) {
|
|
11620
|
+
sendJson(res, 429, {
|
|
11621
|
+
error: { type: "budget_exceeded", message: budgetCheck.reason ?? "Token budget exceeded" }
|
|
11622
|
+
});
|
|
11623
|
+
return;
|
|
11624
|
+
}
|
|
11016
11625
|
const model = lookupModel(res, options.catalog, body.model, plog);
|
|
11017
11626
|
if (!model) {
|
|
11018
11627
|
plog(`model not found: ${body.model}`);
|
|
@@ -11048,7 +11657,7 @@ async function handleAnthropicMessages(req, res, options, modelCache, plog) {
|
|
|
11048
11657
|
plog(
|
|
11049
11658
|
() => `anthropic-passthrough \u2192 ${messagesUrl} oauth=${isOAuth} stream=${clientWantsStream}`
|
|
11050
11659
|
);
|
|
11051
|
-
await forwardAnthropicMessages(
|
|
11660
|
+
const usage = await forwardAnthropicMessages(
|
|
11052
11661
|
res,
|
|
11053
11662
|
messagesUrl,
|
|
11054
11663
|
forwardBody,
|
|
@@ -11064,6 +11673,16 @@ async function handleAnthropicMessages(req, res, options, modelCache, plog) {
|
|
|
11064
11673
|
model.apiKey = refreshed;
|
|
11065
11674
|
}
|
|
11066
11675
|
);
|
|
11676
|
+
recordUsage({
|
|
11677
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
11678
|
+
modelId: getResponseModelId(body.model, model, options),
|
|
11679
|
+
npm: void 0,
|
|
11680
|
+
providerId: model.providerId,
|
|
11681
|
+
app: options.app ?? "gateway",
|
|
11682
|
+
inputTokens: usage.inputTokens,
|
|
11683
|
+
outputTokens: usage.outputTokens
|
|
11684
|
+
});
|
|
11685
|
+
budget.add(usage.inputTokens, usage.outputTokens);
|
|
11067
11686
|
return;
|
|
11068
11687
|
}
|
|
11069
11688
|
if (model.modelFormat === "openai") {
|
|
@@ -11083,8 +11702,10 @@ async function handleAnthropicMessages(req, res, options, modelCache, plog) {
|
|
|
11083
11702
|
);
|
|
11084
11703
|
const npmMaxTools = maxToolsForNpm(model.npm);
|
|
11085
11704
|
const toolCount = Array.isArray(body.tools) ? body.tools.length : 0;
|
|
11705
|
+
let toolTruncationWarning = null;
|
|
11086
11706
|
if (npmMaxTools !== void 0 && toolCount > npmMaxTools) {
|
|
11087
|
-
|
|
11707
|
+
toolTruncationWarning = `tools truncated: ${toolCount} \u2192 ${npmMaxTools} (provider limit)`;
|
|
11708
|
+
plog(toolTruncationWarning);
|
|
11088
11709
|
}
|
|
11089
11710
|
const params = translateRequest2(body, model.npm, {
|
|
11090
11711
|
defaultEffort: anthropicEffortFromRequest(body) ? void 0 : model.defaultEffort,
|
|
@@ -11105,13 +11726,25 @@ async function handleAnthropicMessages(req, res, options, modelCache, plog) {
|
|
|
11105
11726
|
plog(
|
|
11106
11727
|
() => `sdk npm=${model.npm} upstream=${upstreamModelId(model)} responseModel=${responseModelId} stream=${clientWantsStream}`
|
|
11107
11728
|
);
|
|
11729
|
+
if (!clientWantsStream && model.providerId && isProviderUnavailable(model.providerId)) {
|
|
11730
|
+
sendJson(res, 502, {
|
|
11731
|
+
error: {
|
|
11732
|
+
message: `Provider ${model.providerId} temporarily unavailable (recent failures) \u2014 retry shortly.`
|
|
11733
|
+
}
|
|
11734
|
+
});
|
|
11735
|
+
return;
|
|
11736
|
+
}
|
|
11108
11737
|
try {
|
|
11109
11738
|
if (clientWantsStream) {
|
|
11110
11739
|
res.writeHead(200, {
|
|
11111
11740
|
"Content-Type": "text/event-stream",
|
|
11112
11741
|
"Cache-Control": "no-cache",
|
|
11113
|
-
Connection: "keep-alive"
|
|
11742
|
+
Connection: "keep-alive",
|
|
11743
|
+
...toolTruncationWarning ? { "anthropic-warning": toolTruncationWarning } : {}
|
|
11114
11744
|
});
|
|
11745
|
+
if (toolTruncationWarning) res.write(`: ${toolTruncationWarning}
|
|
11746
|
+
|
|
11747
|
+
`);
|
|
11115
11748
|
const usage = await streamAnthropicResponse(
|
|
11116
11749
|
languageModel,
|
|
11117
11750
|
params,
|
|
@@ -11127,32 +11760,46 @@ async function handleAnthropicMessages(req, res, options, modelCache, plog) {
|
|
|
11127
11760
|
inputTokens: usage.inputTokens,
|
|
11128
11761
|
outputTokens: usage.outputTokens
|
|
11129
11762
|
});
|
|
11763
|
+
budget.add(usage.inputTokens, usage.outputTokens);
|
|
11130
11764
|
res.end();
|
|
11131
11765
|
} else {
|
|
11132
|
-
const anthropicResponse = await
|
|
11133
|
-
languageModel,
|
|
11134
|
-
params,
|
|
11135
|
-
responseModelId
|
|
11766
|
+
const anthropicResponse = await withUpstreamRetry(
|
|
11767
|
+
() => generateAnthropicResponse(languageModel, params, responseModelId)
|
|
11136
11768
|
);
|
|
11769
|
+
recordProviderSuccess(model.providerId ?? upstreamModelId(model));
|
|
11770
|
+
const genUsage = anthropicResponse._usage ?? {
|
|
11771
|
+
inputTokens: 0,
|
|
11772
|
+
outputTokens: 0
|
|
11773
|
+
};
|
|
11137
11774
|
recordUsage({
|
|
11138
11775
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
11139
11776
|
modelId: responseModelId,
|
|
11140
11777
|
npm: model.npm,
|
|
11141
11778
|
providerId: model.providerId,
|
|
11142
11779
|
app: options.app ?? "gateway",
|
|
11143
|
-
inputTokens:
|
|
11144
|
-
outputTokens:
|
|
11780
|
+
inputTokens: genUsage.inputTokens ?? 0,
|
|
11781
|
+
outputTokens: genUsage.outputTokens ?? 0
|
|
11145
11782
|
});
|
|
11146
|
-
|
|
11783
|
+
budget.add(genUsage.inputTokens ?? 0, genUsage.outputTokens ?? 0);
|
|
11784
|
+
sendJson(
|
|
11785
|
+
res,
|
|
11786
|
+
200,
|
|
11787
|
+
anthropicResponse,
|
|
11788
|
+
toolTruncationWarning ? { "anthropic-warning": toolTruncationWarning } : void 0
|
|
11789
|
+
);
|
|
11147
11790
|
}
|
|
11148
11791
|
} catch (err) {
|
|
11149
11792
|
const message = formatUpstreamError(err);
|
|
11150
11793
|
plog(`sdk error npm=${model.npm} upstream=${upstreamModelId(model)}: ${message}`);
|
|
11794
|
+
const status = upstreamHttpStatus(err);
|
|
11795
|
+
if ((status === 429 || status >= 500) && model.providerId) {
|
|
11796
|
+
recordProviderFailure(model.providerId);
|
|
11797
|
+
plog(`provider ${model.providerId} failure recorded (status ${status})`);
|
|
11798
|
+
}
|
|
11151
11799
|
if (!res.headersSent) {
|
|
11152
|
-
const status = upstreamHttpStatus(err);
|
|
11153
11800
|
sendJson(res, status === 500 ? 502 : status, { error: { message } });
|
|
11154
11801
|
} else {
|
|
11155
|
-
const errorType = anthropicErrorType(
|
|
11802
|
+
const errorType = anthropicErrorType(status);
|
|
11156
11803
|
res.write(
|
|
11157
11804
|
`event: error
|
|
11158
11805
|
data: ${JSON.stringify({ type: "error", error: { type: errorType, message } })}
|
|
@@ -11166,12 +11813,19 @@ data: ${JSON.stringify({ type: "error", error: { type: errorType, message } })}
|
|
|
11166
11813
|
}
|
|
11167
11814
|
sendJson(res, 400, { error: { message: `Unsupported model format: ${model.modelFormat}` } });
|
|
11168
11815
|
}
|
|
11169
|
-
async function handleOpenAIChatCompletions(req, res, options, modelCache, plog) {
|
|
11816
|
+
async function handleOpenAIChatCompletions(req, res, options, modelCache, plog, budget) {
|
|
11170
11817
|
const body = await readJson(req);
|
|
11171
11818
|
if (!body) {
|
|
11172
11819
|
sendJson(res, 400, { error: { message: "Invalid JSON body" } });
|
|
11173
11820
|
return;
|
|
11174
11821
|
}
|
|
11822
|
+
const budgetCheck = budget.check();
|
|
11823
|
+
if (!budgetCheck.allowed) {
|
|
11824
|
+
sendJson(res, 429, {
|
|
11825
|
+
error: { type: "budget_exceeded", message: budgetCheck.reason ?? "Token budget exceeded" }
|
|
11826
|
+
});
|
|
11827
|
+
return;
|
|
11828
|
+
}
|
|
11175
11829
|
const model = lookupModel(res, options.catalog, body.model, plog);
|
|
11176
11830
|
if (!model) return;
|
|
11177
11831
|
if (supportsDirectOpenAIChatCompletions(model)) {
|
|
@@ -11207,6 +11861,14 @@ async function handleOpenAIChatCompletions(req, res, options, modelCache, plog)
|
|
|
11207
11861
|
plog(
|
|
11208
11862
|
() => `sdk-openai npm=${npm} upstream=${upstreamModelId(model)} responseModel=${responseModelId} stream=${clientWantsStream}`
|
|
11209
11863
|
);
|
|
11864
|
+
if (!clientWantsStream && model.providerId && isProviderUnavailable(model.providerId)) {
|
|
11865
|
+
sendJson(res, 502, {
|
|
11866
|
+
error: {
|
|
11867
|
+
message: `Provider ${model.providerId} temporarily unavailable (recent failures) \u2014 retry shortly.`
|
|
11868
|
+
}
|
|
11869
|
+
});
|
|
11870
|
+
return;
|
|
11871
|
+
}
|
|
11210
11872
|
try {
|
|
11211
11873
|
if (clientWantsStream) {
|
|
11212
11874
|
res.writeHead(200, {
|
|
@@ -11229,13 +11891,13 @@ async function handleOpenAIChatCompletions(req, res, options, modelCache, plog)
|
|
|
11229
11891
|
inputTokens: usage.inputTokens,
|
|
11230
11892
|
outputTokens: usage.outputTokens
|
|
11231
11893
|
});
|
|
11894
|
+
budget.add(usage.inputTokens, usage.outputTokens);
|
|
11232
11895
|
res.end();
|
|
11233
11896
|
} else {
|
|
11234
|
-
const { response, usage } = await
|
|
11235
|
-
languageModel,
|
|
11236
|
-
params,
|
|
11237
|
-
responseModelId
|
|
11897
|
+
const { response, usage } = await withUpstreamRetry(
|
|
11898
|
+
() => generateOpenAiResponse(languageModel, params, responseModelId)
|
|
11238
11899
|
);
|
|
11900
|
+
recordProviderSuccess(model.providerId ?? upstreamModelId(model));
|
|
11239
11901
|
recordUsage({
|
|
11240
11902
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
11241
11903
|
modelId: responseModelId,
|
|
@@ -11245,16 +11907,21 @@ async function handleOpenAIChatCompletions(req, res, options, modelCache, plog)
|
|
|
11245
11907
|
inputTokens: usage.inputTokens,
|
|
11246
11908
|
outputTokens: usage.outputTokens
|
|
11247
11909
|
});
|
|
11910
|
+
budget.add(usage.inputTokens, usage.outputTokens);
|
|
11248
11911
|
sendJson(res, 200, response);
|
|
11249
11912
|
}
|
|
11250
11913
|
} catch (err) {
|
|
11251
11914
|
const message = formatUpstreamError(err);
|
|
11252
|
-
plog(`sdk error npm=${
|
|
11915
|
+
plog(`sdk error npm=${npm} upstream=${upstreamModelId(model)}: ${message}`);
|
|
11916
|
+
const status = upstreamHttpStatus(err);
|
|
11917
|
+
if ((status === 429 || status >= 500) && model.providerId) {
|
|
11918
|
+
recordProviderFailure(model.providerId);
|
|
11919
|
+
plog(`provider ${model.providerId} failure recorded (status ${status})`);
|
|
11920
|
+
}
|
|
11253
11921
|
if (!res.headersSent) {
|
|
11254
|
-
const status = upstreamHttpStatus(err);
|
|
11255
11922
|
sendJson(res, status === 500 ? 502 : status, { error: { message } });
|
|
11256
11923
|
} else {
|
|
11257
|
-
const errorType = anthropicErrorType(
|
|
11924
|
+
const errorType = anthropicErrorType(status);
|
|
11258
11925
|
res.write(
|
|
11259
11926
|
`event: error
|
|
11260
11927
|
data: ${JSON.stringify({ type: "error", error: { type: errorType, message } })}
|
|
@@ -11375,7 +12042,7 @@ function summarizeServerProviders(models) {
|
|
|
11375
12042
|
}
|
|
11376
12043
|
|
|
11377
12044
|
// src/apps/claude/desktop-launch.ts
|
|
11378
|
-
import { existsSync as
|
|
12045
|
+
import { existsSync as existsSync16 } from "fs";
|
|
11379
12046
|
var CLAUDE_BUNDLE_ID = "com.anthropic.claudefordesktop";
|
|
11380
12047
|
var ClaudeAppLauncher = class extends AppLauncher {
|
|
11381
12048
|
appName = "Claude Desktop";
|
|
@@ -11390,7 +12057,7 @@ var ClaudeAppLauncher = class extends AppLauncher {
|
|
|
11390
12057
|
try {
|
|
11391
12058
|
const out = this.run(`mdfind "kMDItemCFBundleIdentifier == '${CLAUDE_BUNDLE_ID}'"`);
|
|
11392
12059
|
const first = out.split("\n").map((l) => l.trim()).find(Boolean);
|
|
11393
|
-
return first &&
|
|
12060
|
+
return first && existsSync16(first) ? first : null;
|
|
11394
12061
|
} catch {
|
|
11395
12062
|
return null;
|
|
11396
12063
|
}
|
|
@@ -12098,9 +12765,12 @@ function favoriteProviderDisplayName(provider) {
|
|
|
12098
12765
|
}
|
|
12099
12766
|
|
|
12100
12767
|
export {
|
|
12768
|
+
ISSUER,
|
|
12101
12769
|
requestOpenAiDeviceCode,
|
|
12102
12770
|
openAiDeviceCodeUrl,
|
|
12103
12771
|
pollOpenAiDeviceCodeToken,
|
|
12772
|
+
buildOpenAiBrowserAuthUrl,
|
|
12773
|
+
exchangeOpenAiBrowserToken,
|
|
12104
12774
|
buildClaudeCodeBillingSystemLine,
|
|
12105
12775
|
selectBetaFlags,
|
|
12106
12776
|
injectClaudeIdentity,
|
|
@@ -12163,6 +12833,7 @@ export {
|
|
|
12163
12833
|
freeStatusLabel,
|
|
12164
12834
|
refreshModelsDevCacheAsync,
|
|
12165
12835
|
resolveInputTypes,
|
|
12836
|
+
resolveReasoning,
|
|
12166
12837
|
loadPreferences,
|
|
12167
12838
|
savePreferences,
|
|
12168
12839
|
loadLaunchPresets,
|
|
@@ -12194,6 +12865,7 @@ export {
|
|
|
12194
12865
|
requestXaiDeviceCode,
|
|
12195
12866
|
pollXaiDeviceCodeToken,
|
|
12196
12867
|
guiCallbackRedirectUri,
|
|
12868
|
+
startCallbackServer,
|
|
12197
12869
|
ANTIGRAVITY_BASE_URLS,
|
|
12198
12870
|
buildAntigravityAuthUrl,
|
|
12199
12871
|
completeAntigravityExchange,
|
|
@@ -12315,4 +12987,4 @@ export {
|
|
|
12315
12987
|
runServerCommand,
|
|
12316
12988
|
favoriteProviderDisplayName
|
|
12317
12989
|
};
|
|
12318
|
-
//# sourceMappingURL=chunk-
|
|
12990
|
+
//# sourceMappingURL=chunk-NU4KMZIM.js.map
|