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.
@@ -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-S5WL3M5G.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",
@@ -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
- function decodeRequestBody(raw, encoding) {
5325
- const enc = (Array.isArray(encoding) ? encoding.join(",") : encoding ?? "").toLowerCase().trim();
5326
- if (!enc || enc === "identity") return raw.toString();
5327
- 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) {
5328
5562
  case "gzip":
5329
5563
  case "x-gzip":
5330
- return zlib.gunzipSync(raw).toString();
5564
+ return { label: encoding, createTransform: () => zlib.createGunzip() };
5331
5565
  case "deflate":
5332
- return zlib.inflateSync(raw).toString();
5566
+ return { label: encoding, createTransform: () => zlib.createInflate() };
5333
5567
  case "br":
5334
- return zlib.brotliDecompressSync(raw).toString();
5568
+ return { label: encoding, createTransform: () => zlib.createBrotliDecompress() };
5335
5569
  case "zstd":
5336
- if (typeof zlib.zstdDecompressSync !== "function") {
5570
+ if (typeof zlib.createZstdDecompress !== "function") {
5337
5571
  throw new Error("zstd request encoding requires Node >= 22.15");
5338
5572
  }
5339
- return zlib.zstdDecompressSync(raw).toString();
5573
+ return { label: encoding, createTransform: () => zlib.createZstdDecompress() };
5340
5574
  default:
5341
- 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;
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 > 50 * 1024 * 1024) {
5351
- reject(new Error("Request body too large"));
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
- try {
5359
- resolve(decodeRequestBody(Buffer.concat(chunks), req.headers["content-encoding"]));
5360
- } catch (err) {
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", reject);
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 === serverPassword) return true;
5422
- 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);
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
- async function forwardAnthropicMessages(res, messagesUrl, body, apiKey, clientWantsStream, inboundBeta, authType, log6, claudeCodeSessionId, extraHeaders, refreshToken, onTokenRefreshed) {
5588
- const doFetch = (key) => fetch(messagesUrl, {
5589
- method: "POST",
5590
- headers: anthropicUpstreamHeaders(
5591
- key,
5592
- clientWantsStream,
5593
- inboundBeta,
5594
- authType,
5595
- claudeCodeSessionId,
5596
- extraHeaders
5597
- ),
5598
- body: JSON.stringify(body)
5599
- });
5600
- let upstreamRes;
5601
- try {
5602
- const retryResult = await fetchWithOAuthRetry(apiKey, doFetch, refreshToken);
5603
- upstreamRes = retryResult.response;
5604
- if (retryResult.refreshed) onTokenRefreshed?.(retryResult.apiKey);
5605
- } catch (err) {
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
- if (!upstreamRes.ok) {
5609
- const errBody = await upstreamRes.text();
5610
- log6?.(`anthropic upstream ${upstreamRes.status}: ${errBody}`);
5611
- res.writeHead(upstreamRes.status, {
5612
- "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
5613
5930
  });
5614
- res.end(errBody);
5615
- return;
5616
- }
5617
- 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
+ }
5618
6002
  res.writeHead(200, {
5619
- "Content-Type": "text/event-stream",
5620
- "Cache-Control": "no-cache",
5621
- Connection: "keep-alive"
6003
+ "Content-Type": "application/json",
6004
+ "Content-Length": Buffer.byteLength(text4).toString()
5622
6005
  });
5623
- Readable.fromWeb(upstreamRes.body).on("error", () => res.destroy()).pipe(res);
5624
- return;
6006
+ res.end(text4);
6007
+ return usage;
5625
6008
  }
5626
- if (!upstreamRes.body) {
5627
- res.writeHead(502, { "Content-Type": "application/json" });
5628
- res.end(
5629
- JSON.stringify({
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
- const text4 = await upstreamRes.text();
5637
- try {
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: "api_error", message: "Upstream response was not valid JSON" }
6020
+ error: { type: "timeout", message: "Upstream request timed out or was aborted." }
5645
6021
  })
5646
6022
  );
5647
6023
  return;
5648
6024
  }
5649
- res.writeHead(200, {
5650
- "Content-Type": "application/json",
5651
- "Content-Length": Buffer.byteLength(text4).toString()
5652
- });
5653
- 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
+ );
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, generateText, tool as tool3, jsonSchema as jsonSchema3, stepCountIs } from "ai";
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()) return null;
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" || npm === "@ai-sdk/openai-compatible") {
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(part.id ?? "", sig),
7497
+ id: encodeToolUseId(toolKey, sig),
7110
7498
  name: part.toolName,
7111
7499
  input: {}
7112
7500
  });
7113
- idToBlock.set(part.id ?? "", blockIndex);
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
- if (options?.forceStream) {
7222
- const r = streamText({ model, ...callParams, stopWhen, onError: () => {
7223
- } });
7224
- Promise.resolve(r.toolResults).catch(() => {
7225
- });
7226
- [text4, toolCalls, finishReason, usage] = await Promise.all([
7227
- r.text,
7228
- r.toolCalls,
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
- quietErrorLog(
7678
- `POST /v1/messages - model alias '${originalModel}' not found, falling back to default route '${defaultRoute.aliasId}'`
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(res, 200, anthropicResponse);
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.headers["x-api-key"] ?? req.socket.remoteAddress ?? "unknown";
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
- plog(`tools truncated: ${toolCount} \u2192 ${npmMaxTools} (provider limit)`);
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 generateAnthropicResponse(
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: anthropicResponse._usage?.inputTokens ?? 0,
11144
- outputTokens: anthropicResponse._usage?.outputTokens ?? 0
11780
+ inputTokens: genUsage.inputTokens ?? 0,
11781
+ outputTokens: genUsage.outputTokens ?? 0
11145
11782
  });
11146
- 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
+ );
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(upstreamHttpStatus(err));
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 generateOpenAiResponse(
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=${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
+ }
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(upstreamHttpStatus(err));
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 existsSync15 } from "fs";
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 && existsSync15(first) ? first : null;
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-EMBABL33.js.map
12990
+ //# sourceMappingURL=chunk-NU4KMZIM.js.map