anygate 0.6.1 → 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.
@@ -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-UE2I2ETX.js";
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 !== 403 && response.status !== 404) {
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("OpenAI device authorization timed out");
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
- var RESPONSES_ONLY_PREFIXES = ["gpt-5-codex", "gpt-5-pro", "gpt-5.2-pro", "o3", "o4"];
962
- function createNullChunkStripper() {
963
- const decoder = new TextDecoder();
964
- const encoder = new TextEncoder();
965
- let buffer = "";
966
- const isNullDataLine = (line) => {
967
- const trimmed = line.trimStart();
968
- if (!trimmed.startsWith("data:")) return false;
969
- return trimmed.slice(5).trim() === "null";
970
- };
971
- return new TransformStream({
972
- transform(chunk, controller) {
973
- buffer += decoder.decode(chunk, { stream: true });
974
- const newlineIdx = buffer.lastIndexOf("\n");
975
- if (newlineIdx === -1) return;
976
- const complete = buffer.slice(0, newlineIdx + 1);
977
- buffer = buffer.slice(newlineIdx + 1);
978
- const kept = complete.split("\n").filter((line) => !isNullDataLine(line)).join("\n");
979
- if (kept) controller.enqueue(encoder.encode(kept));
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
- flush(controller) {
982
- const tail = buffer + decoder.decode();
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 pins spec.headers onto every request
1122
- // regardless of path, and (when tracing) logs the exact wire headers + any
1123
- // error-response body so failures can be diagnosed from ground truth.
1124
- ...spec.headers || process.env.ANYGATE_TRACE === "1" ? {
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
- if (keyring) {
2858
+ const config = readConfig();
2859
+ const legacy = config.server?.savedPassword;
2860
+ if (legacy && keyring) {
2669
2861
  try {
2670
- return await keyring.getPassword();
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
- try {
2681
- await keyring.setPassword(password);
2682
- return;
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
- const config = readConfig();
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",
@@ -3868,6 +4061,7 @@ function buildChildEnv(baseUrl, model, apiKey, proxyPort, contextWindow, enableG
3868
4061
  }
3869
4062
  env["ANTHROPIC_BASE_URL"] = proxyPort ? `http://127.0.0.1:${proxyPort}` : baseUrl;
3870
4063
  env["ANTHROPIC_API_KEY"] = apiKey;
4064
+ env["ANTHROPIC_AUTH_TOKEN"] = apiKey;
3871
4065
  const bareModel = stripOneMContextSuffix(model);
3872
4066
  env["ANTHROPIC_MODEL"] = claudeCodeClientModelId(model, contextWindow);
3873
4067
  env["CLAUDE_CODE_MAX_CONTEXT_TOKENS"] = String(resolveContextWindow(bareModel, contextWindow));
@@ -5053,6 +5247,16 @@ function parseModelList(body, npm, providerId) {
5053
5247
  const freeStatus = classifyFreeStatus({
5054
5248
  model: { cost, isFree: row.isFree }
5055
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
+ }
5056
5260
  const contextWindow = row.context_length ?? row.contextWindow ?? row.context_window ?? resolveContextWindow(id, void 0, providerId);
5057
5261
  models.push({
5058
5262
  id,
@@ -5069,7 +5273,8 @@ function parseModelList(body, npm, providerId) {
5069
5273
  providerId,
5070
5274
  supportedParameters: Array.isArray(row.supported_parameters) ? row.supported_parameters : void 0,
5071
5275
  useResponsesLite: typeof row.use_responses_lite === "boolean" ? row.use_responses_lite : void 0,
5072
- 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
5073
5278
  });
5074
5279
  }
5075
5280
  return models;
@@ -5320,47 +5525,102 @@ async function addProviderFromTemplate(template, apiKey, opts) {
5320
5525
 
5321
5526
  // src/shared/http.ts
5322
5527
  import * as zlib from "zlib";
5323
- function decodeRequestBody(raw, encoding) {
5324
- const enc = (Array.isArray(encoding) ? encoding.join(",") : encoding ?? "").toLowerCase().trim();
5325
- if (!enc || enc === "identity") return raw.toString();
5326
- switch (enc) {
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) {
5327
5562
  case "gzip":
5328
5563
  case "x-gzip":
5329
- return zlib.gunzipSync(raw).toString();
5564
+ return { label: encoding, createTransform: () => zlib.createGunzip() };
5330
5565
  case "deflate":
5331
- return zlib.inflateSync(raw).toString();
5566
+ return { label: encoding, createTransform: () => zlib.createInflate() };
5332
5567
  case "br":
5333
- return zlib.brotliDecompressSync(raw).toString();
5568
+ return { label: encoding, createTransform: () => zlib.createBrotliDecompress() };
5334
5569
  case "zstd":
5335
- if (typeof zlib.zstdDecompressSync !== "function") {
5570
+ if (typeof zlib.createZstdDecompress !== "function") {
5336
5571
  throw new Error("zstd request encoding requires Node >= 22.15");
5337
5572
  }
5338
- return zlib.zstdDecompressSync(raw).toString();
5573
+ return { label: encoding, createTransform: () => zlib.createZstdDecompress() };
5339
5574
  default:
5340
- return raw.toString();
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;
5341
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);
5342
5598
  }
5343
5599
  function readBody(req) {
5344
5600
  return new Promise((resolve, reject) => {
5345
5601
  const chunks = [];
5346
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
+ };
5347
5610
  req.on("data", (c) => {
5348
5611
  totalSize += c.length;
5349
- if (totalSize > 50 * 1024 * 1024) {
5350
- reject(new Error("Request body too large"));
5351
- req.destroy();
5612
+ if (totalSize > MAX_REQUEST_BODY_BYTES) {
5613
+ fail(new RequestBodyTooLargeError("compressed"));
5352
5614
  return;
5353
5615
  }
5354
5616
  chunks.push(c);
5355
5617
  });
5356
5618
  req.on("end", () => {
5357
- try {
5358
- resolve(decodeRequestBody(Buffer.concat(chunks), req.headers["content-encoding"]));
5359
- } catch (err) {
5360
- reject(err);
5361
- }
5619
+ if (settled) return;
5620
+ settled = true;
5621
+ decodeRequestBody(Buffer.concat(chunks), req.headers["content-encoding"]).then(resolve, reject);
5362
5622
  });
5363
- req.on("error", reject);
5623
+ req.on("error", fail);
5364
5624
  });
5365
5625
  }
5366
5626
  function extractApiKey(req) {
@@ -5383,7 +5643,22 @@ function sendJson(res, status, body, extraHeaders) {
5383
5643
  res.end(json);
5384
5644
  }
5385
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
+ }
5386
5658
  function checkRateLimit(clientId) {
5659
+ if (rateLimitStore.size > RATE_LIMIT_PRUNE_THRESHOLD) {
5660
+ pruneRateLimits();
5661
+ }
5387
5662
  const now = Date.now();
5388
5663
  const entry = rateLimitStore.get(clientId);
5389
5664
  if (!entry || now > entry.resetAt) {
@@ -5409,16 +5684,24 @@ import { appendFileSync as appendFileSync2, openSync as openSync4, writeSync as
5409
5684
  import { Readable } from "stream";
5410
5685
 
5411
5686
  // src/gateway/server/auth.ts
5687
+ import { timingSafeEqual } from "crypto";
5688
+ import { createHash as createHash2 } from "crypto";
5412
5689
  function sanitizeCredential(value) {
5413
5690
  if (!value) return null;
5414
5691
  const firstLine = value.trim().split(/\r?\n/)[0]?.trim();
5415
5692
  return firstLine || null;
5416
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
+ }
5417
5699
  function isAuthorized(request, serverPassword) {
5418
5700
  if (serverPassword === null) return true;
5419
5701
  const bearerToken = extractBearerToken(request.headers.get("authorization"));
5420
- if (bearerToken === serverPassword) return true;
5421
- return sanitizeCredential(request.headers.get("x-api-key")) === serverPassword;
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);
5422
5705
  }
5423
5706
  function extractBearerToken(value) {
5424
5707
  if (!value) return null;
@@ -5583,73 +5866,169 @@ async function fetchWithOAuthRetry(apiKey, request, refreshToken) {
5583
5866
  response = await request(refreshed);
5584
5867
  return { response, apiKey: refreshed, refreshed: true };
5585
5868
  }
5586
- async function forwardAnthropicMessages(res, messagesUrl, body, apiKey, clientWantsStream, inboundBeta, authType, log6, claudeCodeSessionId, extraHeaders, refreshToken, onTokenRefreshed) {
5587
- const doFetch = (key) => fetch(messagesUrl, {
5588
- method: "POST",
5589
- headers: anthropicUpstreamHeaders(
5590
- key,
5591
- clientWantsStream,
5592
- inboundBeta,
5593
- authType,
5594
- claudeCodeSessionId,
5595
- extraHeaders
5596
- ),
5597
- body: JSON.stringify(body)
5598
- });
5599
- let upstreamRes;
5600
- try {
5601
- const retryResult = await fetchWithOAuthRetry(apiKey, doFetch, refreshToken);
5602
- upstreamRes = retryResult.response;
5603
- if (retryResult.refreshed) onTokenRefreshed?.(retryResult.apiKey);
5604
- } catch (err) {
5605
- 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
+ }
5606
5888
  }
5607
- if (!upstreamRes.ok) {
5608
- const errBody = await upstreamRes.text();
5609
- log6?.(`anthropic upstream ${upstreamRes.status}: ${errBody}`);
5610
- res.writeHead(upstreamRes.status, {
5611
- "Content-Type": upstreamRes.headers.get("content-type") || "application/json"
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
5612
5930
  });
5613
- res.end(errBody);
5614
- return;
5615
- }
5616
- if (clientWantsStream && upstreamRes.body) {
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
+ }
5617
6002
  res.writeHead(200, {
5618
- "Content-Type": "text/event-stream",
5619
- "Cache-Control": "no-cache",
5620
- Connection: "keep-alive"
6003
+ "Content-Type": "application/json",
6004
+ "Content-Length": Buffer.byteLength(text4).toString()
5621
6005
  });
5622
- Readable.fromWeb(upstreamRes.body).on("error", () => res.destroy()).pipe(res);
5623
- return;
6006
+ res.end(text4);
6007
+ return usage;
5624
6008
  }
5625
- if (!upstreamRes.body) {
5626
- res.writeHead(502, { "Content-Type": "application/json" });
5627
- res.end(
5628
- JSON.stringify({
5629
- type: "error",
5630
- error: { type: "api_error", message: "Upstream returned empty response body" }
5631
- })
5632
- );
6009
+ }
6010
+ function emitAborted(res, clientWantsStream) {
6011
+ if (res.headersSent || res.writableEnded || res.destroyed) {
6012
+ res.destroy();
5633
6013
  return;
5634
6014
  }
5635
- const text4 = await upstreamRes.text();
5636
- try {
5637
- JSON.parse(text4);
5638
- } catch {
5639
- res.writeHead(502, { "Content-Type": "application/json" });
6015
+ if (clientWantsStream) {
6016
+ res.writeHead(504, { "Content-Type": "application/json" });
5640
6017
  res.end(
5641
6018
  JSON.stringify({
5642
6019
  type: "error",
5643
- error: { type: "api_error", message: "Upstream response was not valid JSON" }
6020
+ error: { type: "timeout", message: "Upstream request timed out or was aborted." }
5644
6021
  })
5645
6022
  );
5646
6023
  return;
5647
6024
  }
5648
- res.writeHead(200, {
5649
- "Content-Type": "application/json",
5650
- "Content-Length": Buffer.byteLength(text4).toString()
5651
- });
5652
- res.end(text4);
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
+ );
5653
6032
  }
5654
6033
 
5655
6034
  // src/gateway/antigravity/anthropic-to-cloudcode.ts
@@ -6442,7 +6821,7 @@ async function collectCloudCodeToAnthropic(upstreamRes, model, log6) {
6442
6821
  import { randomUUID as randomUUID5 } from "crypto";
6443
6822
 
6444
6823
  // src/gateway/adapters/sdk-adapter.ts
6445
- import { streamText, generateText, tool as tool3, jsonSchema as jsonSchema3, stepCountIs } from "ai";
6824
+ import { streamText, tool as tool3, jsonSchema as jsonSchema3, stepCountIs } from "ai";
6446
6825
 
6447
6826
  // src/apps/shared/tool-search.ts
6448
6827
  var TOOL_SEARCH_TYPE_PREFIX = "tool_search_tool";
@@ -6831,15 +7210,21 @@ function annotateToolNames(messages) {
6831
7210
  }
6832
7211
  function thinkingToSdkPart(block, npm) {
6833
7212
  const text4 = block.thinking ?? "";
6834
- if (npm === "@ai-sdk/openai" && !block.signature && !text4.trim()) return null;
7213
+ if ((npm === "@ai-sdk/openai" || npm === "@ai-sdk/openai-compatible") && !block.signature && !text4.trim())
7214
+ return null;
6835
7215
  const part = { type: "reasoning", text: text4 };
6836
7216
  if (block.signature) {
6837
7217
  if (npm === "@ai-sdk/google") {
6838
7218
  part.providerOptions = { google: { thoughtSignature: block.signature } };
6839
- } else if (npm === "@ai-sdk/openai" || npm === "@ai-sdk/openai-compatible") {
7219
+ } else if (npm === "@ai-sdk/openai") {
6840
7220
  part.providerOptions = { openai: { reasoningEncryptedContent: block.signature } };
7221
+ } else if (npm === "@ai-sdk/openai-compatible") {
7222
+ part.providerOptions = { openaiCompatible: { reasoningEncryptedContent: block.signature } };
6841
7223
  }
6842
7224
  }
7225
+ if (npm === "@ai-sdk/openai-compatible" && text4) {
7226
+ part.reasoning_content = text4;
7227
+ }
6843
7228
  return part;
6844
7229
  }
6845
7230
  function translateMessages(messages, npm) {
@@ -7102,14 +7487,18 @@ async function writeAnthropicStream(fullStream, modelId, write, log6, hiddenTool
7102
7487
  hiddenIds.add(part.id ?? "");
7103
7488
  break;
7104
7489
  }
7490
+ const toolKey = part.id ?? "";
7491
+ if (idToBlock.has(toolKey)) {
7492
+ break;
7493
+ }
7105
7494
  const sig = grabRoundTripSignature(part);
7106
7495
  openBlock("tool", {
7107
7496
  type: "tool_use",
7108
- id: encodeToolUseId(part.id ?? "", sig),
7497
+ id: encodeToolUseId(toolKey, sig),
7109
7498
  name: part.toolName,
7110
7499
  input: {}
7111
7500
  });
7112
- idToBlock.set(part.id ?? "", blockIndex);
7501
+ idToBlock.set(toolKey, blockIndex);
7113
7502
  break;
7114
7503
  }
7115
7504
  case "tool-input-delta":
@@ -7215,29 +7604,32 @@ async function generateAnthropicResponse(model, params, modelId, options) {
7215
7604
  let toolCalls;
7216
7605
  let finishReason;
7217
7606
  let usage;
7607
+ let reasoningText = "";
7218
7608
  const { webSearchToolName, ...callParams } = params;
7219
7609
  const stopWhen = webSearchToolName ? stepCountIs(MAX_WEB_SEARCH_STEPS) : void 0;
7220
- if (options?.forceStream) {
7221
- const r = streamText({ model, ...callParams, stopWhen, onError: () => {
7222
- } });
7223
- Promise.resolve(r.toolResults).catch(() => {
7224
- });
7225
- [text4, toolCalls, finishReason, usage] = await Promise.all([
7226
- r.text,
7227
- r.toolCalls,
7228
- r.finishReason,
7229
- r.usage
7230
- ]);
7231
- } else {
7232
- const r = await generateText({ model, ...callParams, stopWhen });
7233
- ({ 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
+ }
7234
7618
  }
7619
+ ;
7620
+ [text4, toolCalls, finishReason, usage] = await Promise.all([
7621
+ r.text,
7622
+ r.toolCalls,
7623
+ r.finishReason,
7624
+ r.usage
7625
+ ]);
7235
7626
  return {
7236
7627
  id: "msg_" + Date.now(),
7237
7628
  type: "message",
7238
7629
  role: "assistant",
7239
7630
  model: modelId,
7240
7631
  content: [
7632
+ ...reasoningText ? [{ type: "thinking", thinking: reasoningText, signature: "" }] : [],
7241
7633
  ...text4 ? [{ type: "text", text: text4 }] : [],
7242
7634
  ...toolCalls.filter((tc) => tc.toolName !== webSearchToolName).map((tc) => ({
7243
7635
  type: "tool_use",
@@ -7672,10 +8064,10 @@ function startProxyCatalog(routes, defaultAliasId, debug = false, opts) {
7672
8064
  const originalModel = anthropicBody.model;
7673
8065
  const clientWantsStream = Boolean(anthropicBody.stream);
7674
8066
  const route = resolveRoute(byAlias, originalModel, defaultRoute, plog);
8067
+ let aliasWarning = null;
7675
8068
  if (route === defaultRoute && originalModel !== defaultRoute.aliasId) {
7676
- quietErrorLog(
7677
- `POST /v1/messages - model alias '${originalModel}' not found, falling back to default route '${defaultRoute.aliasId}'`
7678
- );
8069
+ aliasWarning = `model alias '${originalModel}' not found; using '${defaultRoute.aliasId}'`;
8070
+ quietErrorLog(`POST /v1/messages - ${aliasWarning}`);
7679
8071
  }
7680
8072
  const apiKey = route.apiKey;
7681
8073
  const upstreamUrl = route.upstreamUrl;
@@ -7771,8 +8163,12 @@ function startProxyCatalog(routes, defaultAliasId, debug = false, opts) {
7771
8163
  res.writeHead(200, {
7772
8164
  "Content-Type": "text/event-stream",
7773
8165
  "Cache-Control": "no-cache",
7774
- Connection: "keep-alive"
8166
+ Connection: "keep-alive",
8167
+ ...aliasWarning ? { "anthropic-warning": aliasWarning } : {}
7775
8168
  });
8169
+ if (aliasWarning) res.write(`: ${aliasWarning}
8170
+
8171
+ `);
7776
8172
  const usage = await streamAnthropicResponse(
7777
8173
  model,
7778
8174
  params,
@@ -7807,7 +8203,12 @@ function startProxyCatalog(routes, defaultAliasId, debug = false, opts) {
7807
8203
  inputTokens: u?.inputTokens ?? 0,
7808
8204
  outputTokens: u?.outputTokens ?? 0
7809
8205
  });
7810
- sendJson(res, 200, anthropicResponse);
8206
+ sendJson(
8207
+ res,
8208
+ 200,
8209
+ anthropicResponse,
8210
+ aliasWarning ? { "anthropic-warning": aliasWarning } : void 0
8211
+ );
7811
8212
  }
7812
8213
  } catch (err) {
7813
8214
  const message = err instanceof Error ? err.message : String(err);
@@ -7963,6 +8364,7 @@ function startProxy(completionsUrl, modelId, debug = false, contextWindow, sdk,
7963
8364
  interleavedReasoningField: sdk?.interleavedReasoningField,
7964
8365
  useResponsesLite: sdk?.useResponsesLite,
7965
8366
  preferWebSockets: sdk?.preferWebSockets,
8367
+ headers: sdk?.headers,
7966
8368
  app: sdk?.app
7967
8369
  }
7968
8370
  ],
@@ -10911,6 +11313,206 @@ async function streamOpenAiResponse(model, params, responseModelId, onChunk) {
10911
11313
  return usage;
10912
11314
  }
10913
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
+
10914
11516
  // src/gateway/server/router.ts
10915
11517
  function makeServerLog(debugLogPath) {
10916
11518
  if (!debugLogPath) return () => {
@@ -10922,8 +11524,10 @@ async function startServer(options) {
10922
11524
  silenceSdkWarnings();
10923
11525
  const languageModelCache = /* @__PURE__ */ new Map();
10924
11526
  const plog = makeServerLog(options.debugLogPath);
11527
+ const budgetConfig = options.budget ?? getServerBudget();
11528
+ const budget = new BudgetTracker(budgetConfig);
10925
11529
  const server = createServer2((req, res) => {
10926
- void routeRequest(req, res, options, languageModelCache, plog);
11530
+ void routeRequest(req, res, options, languageModelCache, plog, budget);
10927
11531
  });
10928
11532
  await new Promise((resolve, reject) => {
10929
11533
  server.once("error", reject);
@@ -10946,7 +11550,7 @@ async function startServer(options) {
10946
11550
  })
10947
11551
  };
10948
11552
  }
10949
- async function routeRequest(req, res, options, modelCache, plog) {
11553
+ async function routeRequest(req, res, options, modelCache, plog, budget) {
10950
11554
  try {
10951
11555
  const pathname = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`).pathname;
10952
11556
  plog(`${req.method} ${pathname}`);
@@ -10958,7 +11562,7 @@ async function routeRequest(req, res, options, modelCache, plog) {
10958
11562
  sendJson(res, 401, { error: { message: "Unauthorized" } });
10959
11563
  return;
10960
11564
  }
10961
- const clientId = req.headers["x-api-key"] ?? req.socket.remoteAddress ?? "unknown";
11565
+ const clientId = req.socket.remoteAddress ?? "unknown";
10962
11566
  const rateLimit = checkRateLimit(String(clientId));
10963
11567
  if (!rateLimit.allowed) {
10964
11568
  sendJson(
@@ -10986,11 +11590,11 @@ async function routeRequest(req, res, options, modelCache, plog) {
10986
11590
  return;
10987
11591
  }
10988
11592
  if (req.method === "POST" && pathname === "/anthropic/v1/messages") {
10989
- await handleAnthropicMessages(req, res, options, modelCache, plog);
11593
+ await handleAnthropicMessages(req, res, options, modelCache, plog, budget);
10990
11594
  return;
10991
11595
  }
10992
11596
  if (req.method === "POST" && pathname === "/openai/v1/chat/completions") {
10993
- await handleOpenAIChatCompletions(req, res, options, modelCache, plog);
11597
+ await handleOpenAIChatCompletions(req, res, options, modelCache, plog, budget);
10994
11598
  return;
10995
11599
  }
10996
11600
  sendJson(res, 404, { error: { message: "Not found" } });
@@ -11005,12 +11609,19 @@ async function routeRequest(req, res, options, modelCache, plog) {
11005
11609
  }
11006
11610
  }
11007
11611
  }
11008
- async function handleAnthropicMessages(req, res, options, modelCache, plog) {
11612
+ async function handleAnthropicMessages(req, res, options, modelCache, plog, budget) {
11009
11613
  const body = await readJson(req);
11010
11614
  if (!body) {
11011
11615
  sendJson(res, 400, { error: { message: "Invalid JSON body" } });
11012
11616
  return;
11013
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
+ }
11014
11625
  const model = lookupModel(res, options.catalog, body.model, plog);
11015
11626
  if (!model) {
11016
11627
  plog(`model not found: ${body.model}`);
@@ -11046,7 +11657,7 @@ async function handleAnthropicMessages(req, res, options, modelCache, plog) {
11046
11657
  plog(
11047
11658
  () => `anthropic-passthrough \u2192 ${messagesUrl} oauth=${isOAuth} stream=${clientWantsStream}`
11048
11659
  );
11049
- await forwardAnthropicMessages(
11660
+ const usage = await forwardAnthropicMessages(
11050
11661
  res,
11051
11662
  messagesUrl,
11052
11663
  forwardBody,
@@ -11062,6 +11673,16 @@ async function handleAnthropicMessages(req, res, options, modelCache, plog) {
11062
11673
  model.apiKey = refreshed;
11063
11674
  }
11064
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);
11065
11686
  return;
11066
11687
  }
11067
11688
  if (model.modelFormat === "openai") {
@@ -11081,8 +11702,10 @@ async function handleAnthropicMessages(req, res, options, modelCache, plog) {
11081
11702
  );
11082
11703
  const npmMaxTools = maxToolsForNpm(model.npm);
11083
11704
  const toolCount = Array.isArray(body.tools) ? body.tools.length : 0;
11705
+ let toolTruncationWarning = null;
11084
11706
  if (npmMaxTools !== void 0 && toolCount > npmMaxTools) {
11085
- plog(`tools truncated: ${toolCount} \u2192 ${npmMaxTools} (provider limit)`);
11707
+ toolTruncationWarning = `tools truncated: ${toolCount} \u2192 ${npmMaxTools} (provider limit)`;
11708
+ plog(toolTruncationWarning);
11086
11709
  }
11087
11710
  const params = translateRequest2(body, model.npm, {
11088
11711
  defaultEffort: anthropicEffortFromRequest(body) ? void 0 : model.defaultEffort,
@@ -11103,13 +11726,25 @@ async function handleAnthropicMessages(req, res, options, modelCache, plog) {
11103
11726
  plog(
11104
11727
  () => `sdk npm=${model.npm} upstream=${upstreamModelId(model)} responseModel=${responseModelId} stream=${clientWantsStream}`
11105
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
+ }
11106
11737
  try {
11107
11738
  if (clientWantsStream) {
11108
11739
  res.writeHead(200, {
11109
11740
  "Content-Type": "text/event-stream",
11110
11741
  "Cache-Control": "no-cache",
11111
- Connection: "keep-alive"
11742
+ Connection: "keep-alive",
11743
+ ...toolTruncationWarning ? { "anthropic-warning": toolTruncationWarning } : {}
11112
11744
  });
11745
+ if (toolTruncationWarning) res.write(`: ${toolTruncationWarning}
11746
+
11747
+ `);
11113
11748
  const usage = await streamAnthropicResponse(
11114
11749
  languageModel,
11115
11750
  params,
@@ -11125,32 +11760,46 @@ async function handleAnthropicMessages(req, res, options, modelCache, plog) {
11125
11760
  inputTokens: usage.inputTokens,
11126
11761
  outputTokens: usage.outputTokens
11127
11762
  });
11763
+ budget.add(usage.inputTokens, usage.outputTokens);
11128
11764
  res.end();
11129
11765
  } else {
11130
- const anthropicResponse = await generateAnthropicResponse(
11131
- languageModel,
11132
- params,
11133
- responseModelId
11766
+ const anthropicResponse = await withUpstreamRetry(
11767
+ () => generateAnthropicResponse(languageModel, params, responseModelId)
11134
11768
  );
11769
+ recordProviderSuccess(model.providerId ?? upstreamModelId(model));
11770
+ const genUsage = anthropicResponse._usage ?? {
11771
+ inputTokens: 0,
11772
+ outputTokens: 0
11773
+ };
11135
11774
  recordUsage({
11136
11775
  ts: (/* @__PURE__ */ new Date()).toISOString(),
11137
11776
  modelId: responseModelId,
11138
11777
  npm: model.npm,
11139
11778
  providerId: model.providerId,
11140
11779
  app: options.app ?? "gateway",
11141
- inputTokens: anthropicResponse._usage?.inputTokens ?? 0,
11142
- outputTokens: anthropicResponse._usage?.outputTokens ?? 0
11780
+ inputTokens: genUsage.inputTokens ?? 0,
11781
+ outputTokens: genUsage.outputTokens ?? 0
11143
11782
  });
11144
- sendJson(res, 200, anthropicResponse);
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
+ );
11145
11790
  }
11146
11791
  } catch (err) {
11147
11792
  const message = formatUpstreamError(err);
11148
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
+ }
11149
11799
  if (!res.headersSent) {
11150
- const status = upstreamHttpStatus(err);
11151
11800
  sendJson(res, status === 500 ? 502 : status, { error: { message } });
11152
11801
  } else {
11153
- const errorType = anthropicErrorType(upstreamHttpStatus(err));
11802
+ const errorType = anthropicErrorType(status);
11154
11803
  res.write(
11155
11804
  `event: error
11156
11805
  data: ${JSON.stringify({ type: "error", error: { type: errorType, message } })}
@@ -11164,12 +11813,19 @@ data: ${JSON.stringify({ type: "error", error: { type: errorType, message } })}
11164
11813
  }
11165
11814
  sendJson(res, 400, { error: { message: `Unsupported model format: ${model.modelFormat}` } });
11166
11815
  }
11167
- async function handleOpenAIChatCompletions(req, res, options, modelCache, plog) {
11816
+ async function handleOpenAIChatCompletions(req, res, options, modelCache, plog, budget) {
11168
11817
  const body = await readJson(req);
11169
11818
  if (!body) {
11170
11819
  sendJson(res, 400, { error: { message: "Invalid JSON body" } });
11171
11820
  return;
11172
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
+ }
11173
11829
  const model = lookupModel(res, options.catalog, body.model, plog);
11174
11830
  if (!model) return;
11175
11831
  if (supportsDirectOpenAIChatCompletions(model)) {
@@ -11205,6 +11861,14 @@ async function handleOpenAIChatCompletions(req, res, options, modelCache, plog)
11205
11861
  plog(
11206
11862
  () => `sdk-openai npm=${npm} upstream=${upstreamModelId(model)} responseModel=${responseModelId} stream=${clientWantsStream}`
11207
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
+ }
11208
11872
  try {
11209
11873
  if (clientWantsStream) {
11210
11874
  res.writeHead(200, {
@@ -11227,13 +11891,13 @@ async function handleOpenAIChatCompletions(req, res, options, modelCache, plog)
11227
11891
  inputTokens: usage.inputTokens,
11228
11892
  outputTokens: usage.outputTokens
11229
11893
  });
11894
+ budget.add(usage.inputTokens, usage.outputTokens);
11230
11895
  res.end();
11231
11896
  } else {
11232
- const { response, usage } = await generateOpenAiResponse(
11233
- languageModel,
11234
- params,
11235
- responseModelId
11897
+ const { response, usage } = await withUpstreamRetry(
11898
+ () => generateOpenAiResponse(languageModel, params, responseModelId)
11236
11899
  );
11900
+ recordProviderSuccess(model.providerId ?? upstreamModelId(model));
11237
11901
  recordUsage({
11238
11902
  ts: (/* @__PURE__ */ new Date()).toISOString(),
11239
11903
  modelId: responseModelId,
@@ -11243,16 +11907,21 @@ async function handleOpenAIChatCompletions(req, res, options, modelCache, plog)
11243
11907
  inputTokens: usage.inputTokens,
11244
11908
  outputTokens: usage.outputTokens
11245
11909
  });
11910
+ budget.add(usage.inputTokens, usage.outputTokens);
11246
11911
  sendJson(res, 200, response);
11247
11912
  }
11248
11913
  } catch (err) {
11249
11914
  const message = formatUpstreamError(err);
11250
- plog(`sdk error npm=${model.npm} upstream=${upstreamModelId(model)}: ${message}`);
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
+ }
11251
11921
  if (!res.headersSent) {
11252
- const status = upstreamHttpStatus(err);
11253
11922
  sendJson(res, status === 500 ? 502 : status, { error: { message } });
11254
11923
  } else {
11255
- const errorType = anthropicErrorType(upstreamHttpStatus(err));
11924
+ const errorType = anthropicErrorType(status);
11256
11925
  res.write(
11257
11926
  `event: error
11258
11927
  data: ${JSON.stringify({ type: "error", error: { type: errorType, message } })}
@@ -11373,7 +12042,7 @@ function summarizeServerProviders(models) {
11373
12042
  }
11374
12043
 
11375
12044
  // src/apps/claude/desktop-launch.ts
11376
- import { existsSync as existsSync15 } from "fs";
12045
+ import { existsSync as existsSync16 } from "fs";
11377
12046
  var CLAUDE_BUNDLE_ID = "com.anthropic.claudefordesktop";
11378
12047
  var ClaudeAppLauncher = class extends AppLauncher {
11379
12048
  appName = "Claude Desktop";
@@ -11388,7 +12057,7 @@ var ClaudeAppLauncher = class extends AppLauncher {
11388
12057
  try {
11389
12058
  const out = this.run(`mdfind "kMDItemCFBundleIdentifier == '${CLAUDE_BUNDLE_ID}'"`);
11390
12059
  const first = out.split("\n").map((l) => l.trim()).find(Boolean);
11391
- return first && existsSync15(first) ? first : null;
12060
+ return first && existsSync16(first) ? first : null;
11392
12061
  } catch {
11393
12062
  return null;
11394
12063
  }
@@ -12096,9 +12765,12 @@ function favoriteProviderDisplayName(provider) {
12096
12765
  }
12097
12766
 
12098
12767
  export {
12768
+ ISSUER,
12099
12769
  requestOpenAiDeviceCode,
12100
12770
  openAiDeviceCodeUrl,
12101
12771
  pollOpenAiDeviceCodeToken,
12772
+ buildOpenAiBrowserAuthUrl,
12773
+ exchangeOpenAiBrowserToken,
12102
12774
  buildClaudeCodeBillingSystemLine,
12103
12775
  selectBetaFlags,
12104
12776
  injectClaudeIdentity,
@@ -12161,6 +12833,7 @@ export {
12161
12833
  freeStatusLabel,
12162
12834
  refreshModelsDevCacheAsync,
12163
12835
  resolveInputTypes,
12836
+ resolveReasoning,
12164
12837
  loadPreferences,
12165
12838
  savePreferences,
12166
12839
  loadLaunchPresets,
@@ -12192,6 +12865,7 @@ export {
12192
12865
  requestXaiDeviceCode,
12193
12866
  pollXaiDeviceCodeToken,
12194
12867
  guiCallbackRedirectUri,
12868
+ startCallbackServer,
12195
12869
  ANTIGRAVITY_BASE_URLS,
12196
12870
  buildAntigravityAuthUrl,
12197
12871
  completeAntigravityExchange,
@@ -12249,6 +12923,7 @@ export {
12249
12923
  aliasModelId,
12250
12924
  startProxyCatalog,
12251
12925
  startProxy,
12926
+ dedupeByKey,
12252
12927
  routableModelsForTarget,
12253
12928
  providersForTarget,
12254
12929
  fetchProviderCatalog,
@@ -12312,4 +12987,4 @@ export {
12312
12987
  runServerCommand,
12313
12988
  favoriteProviderDisplayName
12314
12989
  };
12315
- //# sourceMappingURL=chunk-BY4AKT2X.js.map
12990
+ //# sourceMappingURL=chunk-NU4KMZIM.js.map