@bman654/clodex 2.11.1 → 2.11.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -382,7 +382,7 @@ import { join } from "path";
382
382
  // package.json
383
383
  var package_default = {
384
384
  name: "@bman654/clodex",
385
- version: "2.11.1",
385
+ version: "2.11.3",
386
386
  publishConfig: {
387
387
  access: "public"
388
388
  },
@@ -856,21 +856,31 @@ async function sleepMs(ms) {
856
856
  }
857
857
 
858
858
  // src/oauth/refresh-http.ts
859
- var OAUTH_REFRESH_TIMEOUT_MS = 3e4;
860
- async function postOAuthRefresh(url, body, options) {
861
- const isJson = options.contentType === "json";
859
+ var OAUTH_REQUEST_TIMEOUT_MS = 3e4;
860
+ async function withOAuthRequestTimeout(operation, timeoutMs = OAUTH_REQUEST_TIMEOUT_MS) {
862
861
  const abortController = new AbortController();
863
862
  const timeout = setTimeout(() => {
864
863
  abortController.abort(new DOMException(
865
864
  "The operation was aborted due to timeout",
866
865
  "TimeoutError"
867
866
  ));
868
- }, OAUTH_REFRESH_TIMEOUT_MS);
867
+ }, timeoutMs);
869
868
  timeout.unref();
870
869
  try {
870
+ return await operation(abortController.signal);
871
+ } finally {
872
+ clearTimeout(timeout);
873
+ if (!abortController.signal.aborted) {
874
+ abortController.abort(new Error("OAuth request completed"));
875
+ }
876
+ }
877
+ }
878
+ async function postOAuthRefresh(url, body, options) {
879
+ const isJson = options.contentType === "json";
880
+ return withOAuthRequestTimeout(async (signal) => {
871
881
  const response = await fetch(url, {
872
882
  method: "POST",
873
- signal: abortController.signal,
883
+ signal,
874
884
  headers: {
875
885
  "Content-Type": isJson ? "application/json" : "application/x-www-form-urlencoded",
876
886
  Accept: "application/json",
@@ -892,9 +902,7 @@ async function postOAuthRefresh(url, body, options) {
892
902
  throw new Error(`${options.errorPrefix}${status}${detail ? `: ${detail}` : ""}`);
893
903
  }
894
904
  return await response.json();
895
- } finally {
896
- clearTimeout(timeout);
897
- }
905
+ });
898
906
  }
899
907
 
900
908
  // src/oauth/callback-server.ts
@@ -1007,6 +1015,9 @@ var OAUTH_POLLING_SAFETY_MARGIN_MS = 3e3;
1007
1015
  var DEVICE_CODE_DEFAULT_EXPIRES_MS = 5 * 60 * 1e3;
1008
1016
  var BROWSER_CALLBACK_PORTS = [1455, 1457];
1009
1017
  var BROWSER_CALLBACK_PATH = "/auth/callback";
1018
+ function isTimeoutError(error) {
1019
+ return error instanceof Error && error.name === "TimeoutError";
1020
+ }
1010
1021
  function extractOpenAiAccountId(tokens) {
1011
1022
  const token = tokens.id_token ?? tokens.access_token;
1012
1023
  if (!token) return void 0;
@@ -1020,18 +1031,30 @@ function extractOpenAiAccountId(tokens) {
1020
1031
  }
1021
1032
  }
1022
1033
  async function requestOpenAiDeviceCode() {
1023
- const response = await fetch(`${ISSUER}/api/accounts/deviceauth/usercode`, {
1024
- method: "POST",
1025
- headers: {
1026
- "Content-Type": "application/json",
1027
- "User-Agent": `clodex/${VERSION}`
1028
- },
1029
- body: JSON.stringify({ client_id: CLIENT_ID })
1030
- });
1031
- if (!response.ok) {
1032
- throw new Error("Failed to initiate OpenAI device authorization");
1034
+ try {
1035
+ return await withOAuthRequestTimeout(async (signal) => {
1036
+ const response = await fetch(`${ISSUER}/api/accounts/deviceauth/usercode`, {
1037
+ method: "POST",
1038
+ headers: {
1039
+ "Content-Type": "application/json",
1040
+ "User-Agent": `clodex/${VERSION}`
1041
+ },
1042
+ body: JSON.stringify({ client_id: CLIENT_ID }),
1043
+ signal
1044
+ });
1045
+ if (!response.ok) {
1046
+ throw new Error("Failed to initiate OpenAI device authorization");
1047
+ }
1048
+ return await response.json();
1049
+ });
1050
+ } catch (error) {
1051
+ if (isTimeoutError(error)) {
1052
+ throw new Error("OpenAI device authorization timed out while requesting a sign-in code", {
1053
+ cause: error
1054
+ });
1055
+ }
1056
+ throw error;
1033
1057
  }
1034
- return response.json();
1035
1058
  }
1036
1059
  function openAiDeviceCodeUrl() {
1037
1060
  return `${ISSUER}/codex/device`;
@@ -1041,41 +1064,65 @@ async function pollOpenAiDeviceCodeToken(deviceData, opts) {
1041
1064
  const now = opts?.now ?? (() => Date.now());
1042
1065
  const intervalMs = Math.max(parseInt(deviceData.interval, 10) || 5, 1) * 1e3;
1043
1066
  const deadline = now() + positiveSecondsToMs(deviceData.expires_in, DEVICE_CODE_DEFAULT_EXPIRES_MS);
1067
+ const remainingPollTimeoutMs = () => Math.min(OAUTH_REQUEST_TIMEOUT_MS, Math.max(0, deadline - now()));
1068
+ const waitForNextPoll = () => sleep(Math.min(intervalMs + OAUTH_POLLING_SAFETY_MARGIN_MS, Math.max(0, deadline - now())));
1044
1069
  while (now() < deadline) {
1045
- const response = await fetch(`${ISSUER}/api/accounts/deviceauth/token`, {
1046
- method: "POST",
1047
- headers: {
1048
- "Content-Type": "application/json",
1049
- "User-Agent": `clodex/${VERSION}`
1050
- },
1051
- body: JSON.stringify({
1052
- device_auth_id: deviceData.device_auth_id,
1053
- user_code: deviceData.user_code
1054
- })
1055
- });
1056
- if (response.ok) {
1057
- const data = await response.json();
1058
- const tokenResponse = await fetch(`${ISSUER}/oauth/token`, {
1059
- method: "POST",
1060
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
1061
- body: new URLSearchParams({
1062
- grant_type: "authorization_code",
1063
- code: data.authorization_code,
1064
- redirect_uri: `${ISSUER}/deviceauth/callback`,
1065
- client_id: CLIENT_ID,
1066
- code_verifier: data.code_verifier
1067
- }).toString()
1068
- });
1069
- if (!tokenResponse.ok) {
1070
- throw new Error(`OpenAI token exchange failed (${tokenResponse.status})`);
1070
+ const pollTimeoutMs = remainingPollTimeoutMs();
1071
+ if (pollTimeoutMs <= 0) break;
1072
+ let result;
1073
+ try {
1074
+ result = await withOAuthRequestTimeout(async (signal) => {
1075
+ const response = await fetch(`${ISSUER}/api/accounts/deviceauth/token`, {
1076
+ method: "POST",
1077
+ headers: {
1078
+ "Content-Type": "application/json",
1079
+ "User-Agent": `clodex/${VERSION}`
1080
+ },
1081
+ body: JSON.stringify({
1082
+ device_auth_id: deviceData.device_auth_id,
1083
+ user_code: deviceData.user_code
1084
+ }),
1085
+ signal
1086
+ });
1087
+ const data = response.ok ? await response.json() : void 0;
1088
+ return { status: response.status, data };
1089
+ }, pollTimeoutMs);
1090
+ } catch (error) {
1091
+ if (!isTimeoutError(error)) throw error;
1092
+ if (now() >= deadline) break;
1093
+ await waitForNextPoll();
1094
+ continue;
1095
+ }
1096
+ if (result.data) {
1097
+ let tokens;
1098
+ try {
1099
+ tokens = await postOAuthRefresh(
1100
+ `${ISSUER}/oauth/token`,
1101
+ new URLSearchParams({
1102
+ grant_type: "authorization_code",
1103
+ code: result.data.authorization_code,
1104
+ redirect_uri: `${ISSUER}/deviceauth/callback`,
1105
+ client_id: CLIENT_ID,
1106
+ code_verifier: result.data.code_verifier
1107
+ }),
1108
+ {
1109
+ contentType: "form",
1110
+ errorPrefix: "OpenAI token exchange failed",
1111
+ includeStatus: true
1112
+ }
1113
+ );
1114
+ } catch (error) {
1115
+ if (isTimeoutError(error)) {
1116
+ throw new Error("OpenAI token exchange timed out", { cause: error });
1117
+ }
1118
+ throw error;
1071
1119
  }
1072
- const tokens = await tokenResponse.json();
1073
1120
  return { tokens, accountId: extractOpenAiAccountId(tokens) };
1074
1121
  }
1075
- if (response.status !== 403 && response.status !== 404) {
1076
- throw new Error(`OpenAI device authorization failed (${response.status})`);
1122
+ if (result.status !== 403 && result.status !== 404) {
1123
+ throw new Error(`OpenAI device authorization failed (${result.status})`);
1077
1124
  }
1078
- await sleep(Math.min(intervalMs + OAUTH_POLLING_SAFETY_MARGIN_MS, Math.max(0, deadline - now())));
1125
+ await waitForNextPoll();
1079
1126
  }
1080
1127
  throw new Error("OpenAI device authorization timed out");
1081
1128
  }
@@ -7792,6 +7839,23 @@ function clampRetryAfterSeconds(value) {
7792
7839
  }
7793
7840
  return Math.min(Math.round(value), MAX_RETRY_AFTER_SECONDS);
7794
7841
  }
7842
+ function clampAiSdkRetryAfterSeconds(value) {
7843
+ return Math.min(clampRetryAfterSeconds(value), MAX_RETRY_AFTER_SECONDS - 1);
7844
+ }
7845
+ var RETRY_AFTER_PARAM_PREFIX = "clodex_retry_after:";
7846
+ function retryAfterProvenanceParam(provenance) {
7847
+ return provenance.source === "default" ? `${RETRY_AFTER_PARAM_PREFIX}default` : `${RETRY_AFTER_PARAM_PREFIX}upstream:${String(provenance.rawSeconds)}`;
7848
+ }
7849
+ function retryAfterProvenanceFromParam(value) {
7850
+ if (value === `${RETRY_AFTER_PARAM_PREFIX}default`) return { source: "default" };
7851
+ if (typeof value !== "string" || !value.startsWith(`${RETRY_AFTER_PARAM_PREFIX}upstream:`)) {
7852
+ return void 0;
7853
+ }
7854
+ const rawValue = value.slice(`${RETRY_AFTER_PARAM_PREFIX}upstream:`.length);
7855
+ if (rawValue.length === 0) return void 0;
7856
+ const rawSeconds = Number(rawValue);
7857
+ return Number.isFinite(rawSeconds) ? { source: "upstream", rawSeconds } : void 0;
7858
+ }
7795
7859
  function retryAfterFromText(message) {
7796
7860
  if (typeof message !== "string") return void 0;
7797
7861
  const match = /retry after (\d+)s\b/i.exec(message);
@@ -7993,7 +8057,8 @@ function upstreamHttpStatus(err, message) {
7993
8057
  if (message.includes("HTTP 400")) return 400;
7994
8058
  return 500;
7995
8059
  }
7996
- function anthropicErrorType(status) {
8060
+ function anthropicErrorType(status, transportCode) {
8061
+ if (transportCode === "websocket_transport_error") return "overloaded_error";
7997
8062
  switch (status) {
7998
8063
  case 400:
7999
8064
  return "invalid_request_error";
@@ -8291,8 +8356,11 @@ var WsUpgradePacer = class {
8291
8356
  }
8292
8357
  /**
8293
8358
  * Resolves when this request may open a new connection, or resolves to a
8294
- * refusal the caller must report as a retryable rate limit. Callers that
8295
- * reuse an existing connection must not call this at all.
8359
+ * refusal the caller must report as a retryable rate limit. A caller that can
8360
+ * already reuse an existing connection must not call this at all; one that
8361
+ * discovers a reusable connection only after being admitted must hand the
8362
+ * token back through `release` instead of holding it for a socket it never
8363
+ * opened.
8296
8364
  */
8297
8365
  async admit(signal) {
8298
8366
  if (!this.enabled) return { kind: "admitted", waitedMs: 0 };
@@ -8310,7 +8378,14 @@ var WsUpgradePacer = class {
8310
8378
  )
8311
8379
  };
8312
8380
  }
8313
- if (reservation.waitMs <= 0) return { kind: "admitted", waitedMs: 0 };
8381
+ if (reservation.waitMs <= 0) {
8382
+ return {
8383
+ kind: "admitted",
8384
+ waitedMs: 0,
8385
+ queued: false,
8386
+ release: this.releaser(reservation.consumed)
8387
+ };
8388
+ }
8314
8389
  const startedAt = this.now();
8315
8390
  try {
8316
8391
  await this.sleep(reservation.waitMs, signal);
@@ -8318,7 +8393,28 @@ var WsUpgradePacer = class {
8318
8393
  this.refund(reservation.consumed);
8319
8394
  throw error;
8320
8395
  }
8321
- return { kind: "admitted", waitedMs: Math.max(0, this.now() - startedAt) };
8396
+ return {
8397
+ kind: "admitted",
8398
+ // A clock moved backwards during the wait would otherwise report this
8399
+ // request as never queued at all.
8400
+ waitedMs: Math.max(0, this.now() - startedAt),
8401
+ queued: true,
8402
+ release: this.releaser(reservation.consumed)
8403
+ };
8404
+ }
8405
+ /**
8406
+ * One-shot refund for an admission whose caller opened nothing. Guarded
8407
+ * because a second call would mint a token the bucket never charged, which
8408
+ * is the one way a refund can raise the sustained rate instead of correcting
8409
+ * it.
8410
+ */
8411
+ releaser(consumed) {
8412
+ let released = false;
8413
+ return () => {
8414
+ if (released) return;
8415
+ released = true;
8416
+ this.refund(consumed);
8417
+ };
8322
8418
  }
8323
8419
  reserve(now) {
8324
8420
  const elapsed = Math.max(0, now - this.lastRefillAt);
@@ -8691,7 +8787,11 @@ function warnToolArgumentNormalizationGap(gap, log12) {
8691
8787
  emitParentNotice("clodex: warning: further tool-argument normalization warnings suppressed.");
8692
8788
  }
8693
8789
  }
8694
- function continuationMismatchDetails(entry, payload, log12, warnOnGap = false) {
8790
+ function continuationMismatchDetails(entry, payload, log12, warnOnGap = false, deferWarnings) {
8791
+ const raise = (warn) => {
8792
+ if (deferWarnings) deferWarnings.push(warn);
8793
+ else warn();
8794
+ };
8695
8795
  const full = inputArray(payload);
8696
8796
  const prefix = [...entry.requestInput ?? [], ...entry.expectedAssistant ?? []];
8697
8797
  const comparable = Math.min(full.length, prefix.length);
@@ -8705,7 +8805,7 @@ function continuationMismatchDetails(entry, payload, log12, warnOnGap = false) {
8705
8805
  const expected = mismatch < prefix.length ? prefix[mismatch] : void 0;
8706
8806
  const actual = mismatch < full.length ? full[mismatch] : void 0;
8707
8807
  const reasoningGap = reasoningNormalizationGap(expected, actual);
8708
- if (reasoningGap && warnOnGap) warnReasoningNormalizationGap(reasoningGap, log12);
8808
+ if (reasoningGap && warnOnGap) raise(() => warnReasoningNormalizationGap(reasoningGap, log12));
8709
8809
  let gapExpected = expected;
8710
8810
  if (conversationItemKind(expected) === "reasoning" && conversationItemKind(actual) === "function_call") {
8711
8811
  for (let index = mismatch; index < prefix.length; index += 1) {
@@ -8727,7 +8827,8 @@ function continuationMismatchDetails(entry, payload, log12, warnOnGap = false) {
8727
8827
  } catch {
8728
8828
  }
8729
8829
  if (toolArgumentGap?.equalAfterStrip === true) {
8730
- if (warnOnGap) warnToolArgumentNormalizationGap(toolArgumentGap, log12);
8830
+ const gap = toolArgumentGap;
8831
+ if (warnOnGap) raise(() => warnToolArgumentNormalizationGap(gap, log12));
8731
8832
  } else if (toolArgumentGap && warnOnGap) {
8732
8833
  try {
8733
8834
  log12?.(`tool argument mismatch beyond the strip rule: ${String(toolArgumentGap.tool)}`);
@@ -9176,11 +9277,16 @@ function deleteEntry(entry, closeSocket = true) {
9176
9277
  }
9177
9278
  }
9178
9279
  }
9179
- function failContext(entry, ctx, message, diagnosticDetails, statusCode, retryAfterSeconds) {
9280
+ function failContext(entry, ctx, message, diagnosticDetails, statusCode, retryAfter) {
9180
9281
  if (ctx.closed || entry.current !== ctx) return;
9282
+ const retryAfterSeconds = retryAfter === void 0 ? void 0 : clampRetryAfterSeconds(retryAfter.seconds);
9181
9283
  entry.debug(`fail: ${message}`);
9182
9284
  emitResponseErrorDiagnostic(entry, ctx, {
9183
9285
  ...diagnosticDetails,
9286
+ ...retryAfter !== void 0 ? {
9287
+ retryAfterSource: retryAfter.source,
9288
+ ..."rawSeconds" in retryAfter ? { rawRetryAfterSeconds: retryAfter.rawSeconds } : {}
9289
+ } : {},
9184
9290
  ...diagnosticTextFingerprint("errorMessage", message)
9185
9291
  });
9186
9292
  flushPending(ctx);
@@ -9191,7 +9297,7 @@ function failContext(entry, ctx, message, diagnosticDetails, statusCode, retryAf
9191
9297
  type: statusCode === void 0 ? "transport_error" : anthropicErrorType(statusCode),
9192
9298
  code: statusCode === void 0 ? "websocket_transport_error" : String(statusCode),
9193
9299
  message,
9194
- param: null,
9300
+ param: retryAfter === void 0 ? null : retryAfterProvenanceParam(retryAfter),
9195
9301
  ...retryAfterSeconds !== void 0 ? { retry_after_seconds: retryAfterSeconds } : {}
9196
9302
  }
9197
9303
  });
@@ -9390,7 +9496,8 @@ function handleSocketMessage(entry, data) {
9390
9496
  const previousMissing = errorCode === "previous_response_not_found";
9391
9497
  const willRetry = previousMissing && ctx.continued && !ctx.retried && !ctx.emittedModelData;
9392
9498
  if (errorCode === "websocket_connection_limit_reached" && !ctx.emittedModelData) {
9393
- const retryAfterSeconds = clampRetryAfterSeconds(responseRetryAfterSeconds(event));
9499
+ const rawRetryAfterSeconds = responseRetryAfterSeconds(event);
9500
+ const retryAfterSeconds = clampRetryAfterSeconds(rawRetryAfterSeconds);
9394
9501
  failContext(
9395
9502
  entry,
9396
9503
  ctx,
@@ -9402,7 +9509,7 @@ function handleSocketMessage(entry, data) {
9402
9509
  retryAfterSeconds
9403
9510
  },
9404
9511
  429,
9405
- retryAfterSeconds
9512
+ rawRetryAfterSeconds === void 0 ? { seconds: retryAfterSeconds, source: "default" } : { seconds: retryAfterSeconds, source: "upstream", rawSeconds: rawRetryAfterSeconds }
9406
9513
  );
9407
9514
  return;
9408
9515
  }
@@ -9427,7 +9534,12 @@ function handleSocketMessage(entry, data) {
9427
9534
  }
9428
9535
  if (errorStatus !== void 0) {
9429
9536
  const statedRetryAfter = errorStatus === 429 ? responseRetryAfterSeconds(event) : void 0;
9430
- const retryAfterSeconds = statedRetryAfter === void 0 ? void 0 : clampRetryAfterSeconds(statedRetryAfter);
9537
+ const retryAfter = statedRetryAfter === void 0 ? void 0 : {
9538
+ seconds: clampRetryAfterSeconds(statedRetryAfter),
9539
+ source: "upstream",
9540
+ rawSeconds: statedRetryAfter
9541
+ };
9542
+ const retryAfterSeconds = retryAfter?.seconds;
9431
9543
  const reason = responseErrorMessage(event) ?? `OpenAI rejected the request (HTTP ${errorStatus})`;
9432
9544
  failContext(
9433
9545
  entry,
@@ -9447,7 +9559,7 @@ function handleSocketMessage(entry, data) {
9447
9559
  ...retryAfterSeconds !== void 0 ? { retryAfterSeconds } : {}
9448
9560
  },
9449
9561
  errorStatus,
9450
- retryAfterSeconds
9562
+ retryAfter
9451
9563
  );
9452
9564
  return;
9453
9565
  }
@@ -9461,7 +9573,13 @@ function handleSocketMessage(entry, data) {
9461
9573
  const classified = numericOrNamed !== void 0 || discriminator ? frameStatusCode(numericOrNamed, discriminator) : 500;
9462
9574
  const statusCode = classified !== 500 ? classified : settledReason ? 400 : 502;
9463
9575
  const usageLimited = statusCode === 429;
9464
- const retryAfterSeconds = usageLimited && responseRetryAfterSeconds(event) !== void 0 ? clampRetryAfterSeconds(responseRetryAfterSeconds(event)) : void 0;
9576
+ const rawRetryAfterSeconds = usageLimited ? responseRetryAfterSeconds(event) : void 0;
9577
+ const retryAfter = rawRetryAfterSeconds === void 0 ? void 0 : {
9578
+ seconds: clampRetryAfterSeconds(rawRetryAfterSeconds),
9579
+ source: "upstream",
9580
+ rawSeconds: rawRetryAfterSeconds
9581
+ };
9582
+ const retryAfterSeconds = retryAfter?.seconds;
9465
9583
  failContext(
9466
9584
  entry,
9467
9585
  ctx,
@@ -9480,7 +9598,7 @@ function handleSocketMessage(entry, data) {
9480
9598
  ...diagnosticTextFingerprint("upstreamMessage", responseErrorMessage(event))
9481
9599
  },
9482
9600
  statusCode,
9483
- retryAfterSeconds
9601
+ retryAfter
9484
9602
  );
9485
9603
  return;
9486
9604
  }
@@ -9586,15 +9704,14 @@ function createConnection(WebSocket, wsUrl, headers, persistent, key, options, d
9586
9704
  return;
9587
9705
  }
9588
9706
  if (statusCode === 403) {
9589
- const retryAfterSeconds = clampRetryAfterSeconds(
9590
- numericRetryAfterHeader(response.headers["retry-after"])
9591
- );
9707
+ const rawRetryAfterSeconds = numericRetryAfterHeader(response.headers["retry-after"]);
9708
+ const retryAfterSeconds = clampRetryAfterSeconds(rawRetryAfterSeconds);
9592
9709
  failContext(entry, ctx, `OpenAI edge throttled the Responses WebSocket upgrade (HTTP 403); retry after ${retryAfterSeconds}s`, {
9593
9710
  source: "unexpected_response",
9594
9711
  httpStatusCode: statusCode,
9595
9712
  mappedStatusCode: 429,
9596
9713
  retryAfterSeconds
9597
- }, 429, retryAfterSeconds);
9714
+ }, 429, rawRetryAfterSeconds === void 0 ? { seconds: retryAfterSeconds, source: "default" } : { seconds: retryAfterSeconds, source: "upstream", rawSeconds: rawRetryAfterSeconds });
9598
9715
  return;
9599
9716
  }
9600
9717
  failContext(entry, ctx, `WebSocket upgrade failed (HTTP ${statusCode})`, {
@@ -9687,18 +9804,25 @@ function createResponsesWebSocketFetch(wsUrl, log12, options = {}) {
9687
9804
  const diagnosticCorrelation = diagnosticContext.getStore();
9688
9805
  let now = resolvedOptions.now();
9689
9806
  const evictions = cleanupExpiredConnections(now);
9690
- const candidates = partitionKey ? connectionEntries(partitionKey) : [];
9691
- const idleCandidates = candidates.filter((entry) => !entry.inFlight);
9692
- const clientItems = idleCandidates.length ? canonicalItemStrings(inputArray(payload)) : [];
9693
- const matches = idleCandidates.map((entry) => ({ entry, match: continuationMatch(entry, payload, clientItems) })).filter((candidate) => candidate.match !== void 0).sort((left, right) => left.match.delta.length - right.match.delta.length || (left.match.mode === right.match.mode ? 0 : left.match.mode === "exact" ? -1 : 1));
9807
+ const scanForHeads = () => {
9808
+ const scanned = partitionKey ? connectionEntries(partitionKey) : [];
9809
+ const idle = scanned.filter((entry) => !entry.inFlight);
9810
+ const clientItems = idle.length ? canonicalItemStrings(inputArray(payload)) : [];
9811
+ return {
9812
+ candidates: scanned,
9813
+ idleCandidates: idle,
9814
+ matches: idle.map((entry) => ({ entry, match: continuationMatch(entry, payload, clientItems) })).filter((candidate) => candidate.match !== void 0).sort((left, right) => left.match.delta.length - right.match.delta.length || (left.match.mode === right.match.mode ? 0 : left.match.mode === "exact" ? -1 : 1))
9815
+ };
9816
+ };
9817
+ let { candidates, idleCandidates, matches } = scanForHeads();
9694
9818
  let selected = matches[0]?.entry;
9695
- const selectedMatch = matches[0]?.match;
9696
- const selectedDelta = selectedMatch?.delta;
9819
+ let selectedMatch = matches[0]?.match;
9820
+ let selectedDelta = selectedMatch?.delta;
9697
9821
  const diagnosticEntry = selected ?? [...idleCandidates].sort((left, right) => right.lastUsedAt - left.lastUsedAt)[0] ?? candidates[0];
9698
9822
  debug(
9699
9823
  `lookup key=${debugKey(partitionKey)} prompt=${debugKey(promptFingerprint)} hit=${candidates.length > 0} heads=${candidates.length} active_connections=${connectionCount()}`
9700
9824
  );
9701
- const promptChanges = changedPromptFields(diagnosticEntry?.promptFieldHashes, promptFieldHashes);
9825
+ let promptChanges = changedPromptFields(diagnosticEntry?.promptFieldHashes, promptFieldHashes);
9702
9826
  if (promptChanges.length) debug(`prompt fields changed: ${promptChanges.join(",")}`);
9703
9827
  if (promptChanges.includes("instructions")) {
9704
9828
  const summary = instructionChangeSummary(diagnosticEntry?.instructionsSnapshot, instructionsSnapshot);
@@ -9710,35 +9834,49 @@ function createResponsesWebSocketFetch(wsUrl, log12, options = {}) {
9710
9834
  let promotedConnectionId;
9711
9835
  let decision;
9712
9836
  let candidateMismatchDetails;
9713
- if (selected && selectedDelta) {
9714
- sendPayload = { ...payload, input: selectedDelta, previous_response_id: selected.responseId };
9837
+ const deferredMismatchWarnings = [];
9838
+ const flushMismatchWarnings = () => {
9839
+ for (const warn of deferredMismatchWarnings) warn();
9840
+ deferredMismatchWarnings.length = 0;
9841
+ };
9842
+ const continueOnHead = (entry, match) => {
9843
+ sendPayload = { ...payload, input: match.delta, previous_response_id: entry.responseId };
9715
9844
  continued = true;
9716
- if (selected.generation === "nursery") {
9845
+ if (entry.generation === "nursery") {
9717
9846
  evictions.push(...evictOldestIdleGeneration(
9718
9847
  "established",
9719
9848
  resolvedOptions.maxConnections,
9720
9849
  "established_lru_cap"
9721
9850
  ));
9722
- selected.generation = "established";
9723
- promotedConnectionId = selected.debugId;
9851
+ entry.generation = "established";
9852
+ promotedConnectionId = entry.debugId;
9724
9853
  }
9725
- decision = "continuation";
9726
9854
  debug(
9727
- `continuing chain with ${selectedDelta.length} incremental input item(s)` + (selectedMatch.mode === "omitted_reasoning" ? " after accepting omitted reasoning" : "")
9855
+ `continuing chain with ${match.delta.length} incremental input item(s)` + (match.mode === "omitted_reasoning" ? " after accepting omitted reasoning" : "")
9728
9856
  );
9857
+ return "continuation";
9858
+ };
9859
+ if (selected && selectedDelta) {
9860
+ decision = continueOnHead(selected, selectedMatch);
9729
9861
  } else if (candidates.some((entry) => entry.inFlight)) {
9730
9862
  selected = void 0;
9731
9863
  persistent = false;
9732
9864
  decision = "parallel_isolated";
9733
9865
  debug("parallel request using an isolated socket");
9734
9866
  } else if (diagnosticEntry) {
9735
- const diagnosticMismatch = continuationMismatchDetails(diagnosticEntry, payload, debug, true);
9867
+ const diagnosticMismatch = continuationMismatchDetails(
9868
+ diagnosticEntry,
9869
+ payload,
9870
+ debug,
9871
+ true,
9872
+ deferredMismatchWarnings
9873
+ );
9736
9874
  candidateMismatchDetails = /* @__PURE__ */ new Map([[diagnosticEntry, diagnosticMismatch]]);
9737
9875
  for (const candidate of candidates) {
9738
9876
  if (candidate === diagnosticEntry) continue;
9739
9877
  candidateMismatchDetails.set(
9740
9878
  candidate,
9741
- continuationMismatchDetails(candidate, payload, debug, true)
9879
+ continuationMismatchDetails(candidate, payload, debug, true, deferredMismatchWarnings)
9742
9880
  );
9743
9881
  }
9744
9882
  debug(
@@ -9757,6 +9895,7 @@ function createResponsesWebSocketFetch(wsUrl, log12, options = {}) {
9757
9895
  decision = "unpartitioned_socket";
9758
9896
  }
9759
9897
  let pacingWaitedMs;
9898
+ let pacingRescanOutcome;
9760
9899
  if (!selected) {
9761
9900
  const pacer = options.pacer ?? sharedWsUpgradePacer();
9762
9901
  const pacingStartedAt = resolvedOptions.now();
@@ -9770,6 +9909,7 @@ function createResponsesWebSocketFetch(wsUrl, log12, options = {}) {
9770
9909
  decision,
9771
9910
  waitedMs: Math.max(0, resolvedOptions.now() - pacingStartedAt)
9772
9911
  }, diagnosticCorrelation);
9912
+ flushMismatchWarnings();
9773
9913
  throw error;
9774
9914
  }
9775
9915
  if (admission.kind === "refused") {
@@ -9783,6 +9923,7 @@ function createResponsesWebSocketFetch(wsUrl, log12, options = {}) {
9783
9923
  requiredWaitMs: admission.requiredWaitMs,
9784
9924
  retryAfterSeconds: admission.retryAfterSeconds
9785
9925
  }, diagnosticCorrelation);
9926
+ flushMismatchWarnings();
9786
9927
  return pacedRefusalResponse(admission.retryAfterSeconds);
9787
9928
  }
9788
9929
  if (admission.waitedMs > 0) {
@@ -9797,12 +9938,42 @@ function createResponsesWebSocketFetch(wsUrl, log12, options = {}) {
9797
9938
  }
9798
9939
  now = resolvedOptions.now();
9799
9940
  evictions.push(...cleanupExpiredConnections(now));
9800
- if (persistent && partitionKey && connectionEntries(partitionKey).some((entry) => entry.inFlight)) {
9941
+ if (admission.queued ?? admission.waitedMs > 0) {
9942
+ const rescan = scanForHeads();
9943
+ const rematched = rescan.matches[0];
9944
+ if (rematched) {
9945
+ ({ candidates, idleCandidates, matches } = rescan);
9946
+ selected = rematched.entry;
9947
+ selectedMatch = rematched.match;
9948
+ selectedDelta = rematched.match.delta;
9949
+ persistent = Boolean(partitionKey);
9950
+ promptChanges = changedPromptFields(rematched.entry.promptFieldHashes, promptFieldHashes);
9951
+ decision = continueOnHead(rematched.entry, rematched.match);
9952
+ pacingRescanOutcome = "continuation";
9953
+ debug("continuing a chain head that freed up during the pacing wait");
9954
+ if (promptChanges.length) debug(`prompt fields changed: ${promptChanges.join(",")}`);
9955
+ admission.release?.();
9956
+ } else {
9957
+ pacingRescanOutcome = "no_change";
9958
+ }
9959
+ }
9960
+ if (!selected && persistent && partitionKey && connectionEntries(partitionKey).some((entry) => entry.inFlight)) {
9801
9961
  persistent = false;
9802
9962
  decision = "parallel_isolated";
9803
9963
  debug("parallel request using an isolated socket after pacing");
9964
+ if (pacingRescanOutcome === "no_change") pacingRescanOutcome = "parallel_isolated";
9804
9965
  }
9805
9966
  }
9967
+ let suppressedMismatchWarnings;
9968
+ if (selected && deferredMismatchWarnings.length) {
9969
+ suppressedMismatchWarnings = deferredMismatchWarnings.length;
9970
+ debug(
9971
+ `suppressed ${suppressedMismatchWarnings} arrival mismatch warning(s) after continuing a head that freed up during the pacing wait`
9972
+ );
9973
+ deferredMismatchWarnings.length = 0;
9974
+ } else {
9975
+ flushMismatchWarnings();
9976
+ }
9806
9977
  if (!selected && persistent) {
9807
9978
  evictions.push(...evictOldestIdleGeneration(
9808
9979
  "nursery",
@@ -9845,6 +10016,8 @@ function createResponsesWebSocketFetch(wsUrl, log12, options = {}) {
9845
10016
  promotedConnectionId,
9846
10017
  createdConnectionId: selected ? void 0 : nextConnectionDebugId,
9847
10018
  ...pacingWaitedMs !== void 0 ? { pacingWaitedMs } : {},
10019
+ ...pacingRescanOutcome !== void 0 ? { pacingRescanOutcome } : {},
10020
+ ...suppressedMismatchWarnings !== void 0 ? { suppressedMismatchWarnings } : {},
9848
10021
  createdGeneration: selected ? void 0 : persistent ? "nursery" : "isolated",
9849
10022
  incrementalInputItems: selectedDelta?.length,
9850
10023
  heads: candidates.map((entry) => ({
@@ -10928,6 +11101,28 @@ function sendJson(res, status, body) {
10928
11101
  res.writeHead(status, { "Content-Type": "application/json" });
10929
11102
  res.end(json);
10930
11103
  }
11104
+ var RESPONSE_COMPLETED = /* @__PURE__ */ Symbol.for("clodex.responseCompleted");
11105
+ var ResponseCompleted = class extends Error {
11106
+ [RESPONSE_COMPLETED] = true;
11107
+ constructor() {
11108
+ super("Response completed");
11109
+ this.name = "ResponseCompleted";
11110
+ }
11111
+ };
11112
+ function clientDisconnected(signal) {
11113
+ if (!signal.aborted) return false;
11114
+ const reason = signal.reason;
11115
+ return !(typeof reason === "object" && reason !== null && RESPONSE_COMPLETED in reason);
11116
+ }
11117
+ function watchClientDisconnect(res) {
11118
+ const controller = new AbortController();
11119
+ res.once("close", () => {
11120
+ controller.abort(
11121
+ res.writableFinished ? new ResponseCompleted() : new Error("Client disconnected")
11122
+ );
11123
+ });
11124
+ return controller;
11125
+ }
10931
11126
 
10932
11127
  // src/server/vendor-mask.ts
10933
11128
  function reverseSegment(value) {
@@ -11412,7 +11607,23 @@ function resolveUpstreamTools(tools, messages) {
11412
11607
  }
11413
11608
 
11414
11609
  // src/upstream-attempts.ts
11415
- import { RetryError as RetryError2, wrapLanguageModel as wrapLanguageModel2 } from "ai";
11610
+ import { APICallError as APICallError2, RetryError as RetryError2, wrapLanguageModel as wrapLanguageModel2 } from "ai";
11611
+ function restoreSyntheticRetryAfterHeader(error) {
11612
+ if (!APICallError2.isInstance(error) || error.statusCode !== 429) return;
11613
+ const headers = error.responseHeaders;
11614
+ if (!headers || headers["retry-after"] !== void 0 || headers["retry-after-ms"] !== void 0) return;
11615
+ const rawFrame = error.data;
11616
+ if (!rawFrame || typeof rawFrame !== "object" || rawFrame.type !== "error") return;
11617
+ const frame = providerErrorFrame(rawFrame);
11618
+ if (frame?.retryAfterSeconds === void 0) return;
11619
+ const nestedError = rawFrame.error;
11620
+ const provenance = retryAfterProvenanceFromParam(
11621
+ nestedError && typeof nestedError === "object" ? nestedError.param : void 0
11622
+ );
11623
+ if (provenance?.source !== "upstream") return;
11624
+ if (provenance.rawSeconds < 0 || provenance.rawSeconds > MAX_RETRY_AFTER_SECONDS) return;
11625
+ headers["retry-after"] = String(clampAiSdkRetryAfterSeconds(provenance.rawSeconds));
11626
+ }
11416
11627
  function trackUpstreamAttempts(model) {
11417
11628
  if (typeof model === "string") {
11418
11629
  return { model, deadlineError: (timeoutError) => timeoutError };
@@ -11426,6 +11637,7 @@ function trackUpstreamAttempts(model) {
11426
11637
  failedAttempts.length = 0;
11427
11638
  return result;
11428
11639
  } catch (error) {
11640
+ restoreSyntheticRetryAfterHeader(error);
11429
11641
  failedAttempts.push(error);
11430
11642
  waitingToRetry = true;
11431
11643
  throw error;
@@ -12082,9 +12294,10 @@ async function writeAnthropicStream(stream, modelId, write, log12, observer, too
12082
12294
  case "error": {
12083
12295
  const e = part.error;
12084
12296
  const errMsg = e?.message || (typeof part.error === "string" ? part.error : JSON.stringify(e?.data ?? part.error));
12085
- const errorType = anthropicErrorType(upstreamHttpStatus(part.error, errMsg));
12297
+ const transportCode = sdkUpstreamErrorDetails(part.error)?.transportCode;
12298
+ const errorType = anthropicErrorType(upstreamHttpStatus(part.error, errMsg), transportCode);
12086
12299
  log12?.(() => `sdk stream error (${errorType}): ${errMsg}`);
12087
- closeOpen();
12300
+ if (!(transportCode === "websocket_transport_error" && openType === "thinking")) closeOpen();
12088
12301
  throw part.error instanceof Error || part.error && typeof part.error === "object" ? part.error : new Error(errMsg);
12089
12302
  }
12090
12303
  default:
@@ -12526,14 +12739,7 @@ async function startProxyCatalog(routes, defaultAliasId, debug = false, inferenc
12526
12739
  anthropicError(res, 401, "Invalid proxy token");
12527
12740
  return;
12528
12741
  }
12529
- const clientAbort = new AbortController();
12530
- const abortForClientDisconnect = () => {
12531
- if (!clientAbort.signal.aborted) clientAbort.abort(new Error("Client disconnected"));
12532
- };
12533
- req.once("aborted", abortForClientDisconnect);
12534
- res.once("close", () => {
12535
- if (!res.writableFinished) abortForClientDisconnect();
12536
- });
12742
+ const clientAbort = watchClientDisconnect(res);
12537
12743
  let anthropicBody;
12538
12744
  try {
12539
12745
  const raw = await readBody(req);
@@ -12613,7 +12819,7 @@ async function startProxyCatalog(routes, defaultAliasId, debug = false, inferenc
12613
12819
  signal: clientAbort.signal
12614
12820
  });
12615
12821
  } catch (err) {
12616
- if (clientAbort.signal.aborted) return;
12822
+ if (clientDisconnected(clientAbort.signal)) return;
12617
12823
  const message = err instanceof UpstreamUnreachableError ? err.message : String(err);
12618
12824
  plog(() => `anthropic token-count error: ${message}`);
12619
12825
  anthropicError(res, 502, message);
@@ -12674,7 +12880,7 @@ async function startProxyCatalog(routes, defaultAliasId, debug = false, inferenc
12674
12880
  }) : void 0
12675
12881
  });
12676
12882
  } catch (err) {
12677
- if (clientAbort.signal.aborted) return;
12883
+ if (clientDisconnected(clientAbort.signal)) return;
12678
12884
  const message = err instanceof UpstreamUnreachableError ? err.message : String(err);
12679
12885
  plog(() => `anthropic-passthrough error: ${message}`);
12680
12886
  anthropicError(res, 502, message);
@@ -12814,7 +13020,7 @@ async function startProxyCatalog(routes, defaultAliasId, debug = false, inferenc
12814
13020
  };
12815
13021
  let sdkAttempt = 0;
12816
13022
  const handleSdkError = async (err) => {
12817
- if (clientAbort.signal.aborted) {
13023
+ if (clientDisconnected(clientAbort.signal)) {
12818
13024
  translationLifecycle?.cancel();
12819
13025
  return "cancelled";
12820
13026
  }
@@ -12870,7 +13076,7 @@ async function startProxyCatalog(routes, defaultAliasId, debug = false, inferenc
12870
13076
  contextLengthExceeded ? relayRequestId ?? randomUUID3() : void 0
12871
13077
  );
12872
13078
  } else {
12873
- const errorType = anthropicErrorType(upstreamStatus);
13079
+ const errorType = anthropicErrorType(upstreamStatus, details?.transportCode);
12874
13080
  res.write(`event: error
12875
13081
  data: ${JSON.stringify({
12876
13082
  type: "error",
@@ -15119,9 +15325,9 @@ function buildDynamicOAuthModel(entry, seedById, codexCatalog) {
15119
15325
  };
15120
15326
  }
15121
15327
  async function fetchJsonWithAuth(url, accessToken, timeoutMs) {
15328
+ const controller = new AbortController();
15329
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
15122
15330
  try {
15123
- const controller = new AbortController();
15124
- const timer = setTimeout(() => controller.abort(), timeoutMs);
15125
15331
  const response = await fetch(url, {
15126
15332
  headers: {
15127
15333
  Accept: "application/json",
@@ -15129,7 +15335,7 @@ async function fetchJsonWithAuth(url, accessToken, timeoutMs) {
15129
15335
  "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
15130
15336
  },
15131
15337
  signal: controller.signal
15132
- }).finally(() => clearTimeout(timer));
15338
+ });
15133
15339
  if (!response.ok) {
15134
15340
  const detail = await response.text().then((t) => t.slice(0, 200)).catch(() => "");
15135
15341
  return { body: null, error: `HTTP ${response.status}${detail ? `: ${detail}` : ""}` };
@@ -15137,6 +15343,11 @@ async function fetchJsonWithAuth(url, accessToken, timeoutMs) {
15137
15343
  return { body: await response.json() };
15138
15344
  } catch (err) {
15139
15345
  return { body: null, error: err instanceof Error ? err.message : String(err) };
15346
+ } finally {
15347
+ clearTimeout(timer);
15348
+ if (!controller.signal.aborted) {
15349
+ controller.abort(new Error("OpenAI catalog request completed"));
15350
+ }
15140
15351
  }
15141
15352
  }
15142
15353
  async function refreshOpenAiOAuthModels(accessToken) {
@@ -17224,10 +17435,11 @@ async function collectOpenAiStream(stream) {
17224
17435
  }
17225
17436
  return collected;
17226
17437
  }
17227
- function startUpstreamBudget(model, streaming) {
17438
+ function startUpstreamBudget(model, streaming, clientSignal) {
17228
17439
  const { idleTimeoutMs, totalTimeoutMs, maxRetries } = upstreamRequestBudget();
17229
17440
  const attempts = trackUpstreamAttempts(model);
17230
17441
  const abort = new AbortController();
17442
+ const stopForwardingAbort = forwardAbortSignal(clientSignal, abort);
17231
17443
  const idleError = () => attempts.deadlineError(new Error(
17232
17444
  `no data received from provider for ${Math.round(idleTimeoutMs / 1e3)}s`
17233
17445
  ));
@@ -17248,6 +17460,7 @@ function startUpstreamBudget(model, streaming) {
17248
17460
  idleTimer = setTimeout(() => abort.abort(idleError()), idleTimeoutMs);
17249
17461
  },
17250
17462
  close: () => {
17463
+ stopForwardingAbort();
17251
17464
  if (idleTimer !== void 0) clearTimeout(idleTimer);
17252
17465
  clearTimeout(totalTimer);
17253
17466
  if (!abort.signal.aborted) abort.abort();
@@ -17269,7 +17482,7 @@ async function* watchOpenAiStream(stream, budget) {
17269
17482
  async function generateOpenAiResponse(model, params, responseModelId, options) {
17270
17483
  let result;
17271
17484
  const streaming = options?.forceStream === true;
17272
- const budget = startUpstreamBudget(model, streaming);
17485
+ const budget = startUpstreamBudget(model, streaming, options?.abortSignal);
17273
17486
  try {
17274
17487
  if (streaming) {
17275
17488
  const { stream } = streamText2({
@@ -17322,8 +17535,8 @@ async function generateOpenAiResponse(model, params, responseModelId, options) {
17322
17535
  }
17323
17536
  };
17324
17537
  }
17325
- async function streamOpenAiResponse(model, params, responseModelId, onChunk) {
17326
- const budget = startUpstreamBudget(model, true);
17538
+ async function streamOpenAiResponse(model, params, responseModelId, onChunk, options) {
17539
+ const budget = startUpstreamBudget(model, true, options?.abortSignal);
17327
17540
  try {
17328
17541
  const { stream } = streamText2({
17329
17542
  model: budget.model,
@@ -17499,6 +17712,7 @@ async function routeRequest(req, res, options, modelCache, plog) {
17499
17712
  }
17500
17713
  }
17501
17714
  async function handleAnthropicMessages(req, res, options, modelCache, plog) {
17715
+ const clientAbort = watchClientDisconnect(res);
17502
17716
  const body = await readJson(req);
17503
17717
  if (!body) {
17504
17718
  sendJson(res, 400, { error: { message: "Invalid JSON body" } });
@@ -17572,28 +17786,34 @@ async function handleAnthropicMessages(req, res, options, modelCache, plog) {
17572
17786
  rejectedAccessToken
17573
17787
  ) : void 0;
17574
17788
  plog(() => `anthropic-passthrough \u2192 ${messagesUrl} oauth=${isOAuth} stream=${clientWantsStream}`);
17575
- await relayAnthropicMessages(res, messagesUrl, forwardBody, apiKey, clientWantsStream, {
17576
- inboundBeta: effectiveBeta,
17577
- authType,
17578
- log: (message) => plog(message),
17579
- claudeCodeSessionId,
17580
- extraHeaders: model.headers,
17581
- refreshToken,
17582
- onTokenRefreshed: (refreshed) => {
17583
- model.apiKey = refreshed;
17584
- },
17585
- // Echo the exact requested id when it differs from the upstream id, so
17586
- // clients that key context windows on the response model still resolve.
17587
- responseModelOverride: typeof body.model === "string" && body.model !== upstreamModelId(model) ? body.model : void 0,
17588
- onUpstreamError: options.inferenceLogPath ? (statusCode, errorContent) => writeInferenceResponseErrorLog(options.inferenceLogPath, {
17589
- requestId,
17590
- modelId: body.model,
17591
- provider: inferenceProvider(model),
17592
- route: "passthrough",
17593
- statusCode,
17594
- errorContent
17595
- }) : void 0
17596
- });
17789
+ try {
17790
+ await relayAnthropicMessages(res, messagesUrl, forwardBody, apiKey, clientWantsStream, {
17791
+ inboundBeta: effectiveBeta,
17792
+ authType,
17793
+ log: (message) => plog(message),
17794
+ claudeCodeSessionId,
17795
+ extraHeaders: model.headers,
17796
+ refreshToken,
17797
+ onTokenRefreshed: (refreshed) => {
17798
+ model.apiKey = refreshed;
17799
+ },
17800
+ signal: clientAbort.signal,
17801
+ // Echo the exact requested id when it differs from the upstream id, so
17802
+ // clients that key context windows on the response model still resolve.
17803
+ responseModelOverride: typeof body.model === "string" && body.model !== upstreamModelId(model) ? body.model : void 0,
17804
+ onUpstreamError: options.inferenceLogPath ? (statusCode, errorContent) => writeInferenceResponseErrorLog(options.inferenceLogPath, {
17805
+ requestId,
17806
+ modelId: body.model,
17807
+ provider: inferenceProvider(model),
17808
+ route: "passthrough",
17809
+ statusCode,
17810
+ errorContent
17811
+ }) : void 0
17812
+ });
17813
+ } catch (err) {
17814
+ if (clientDisconnected(clientAbort.signal)) return;
17815
+ throw err;
17816
+ }
17597
17817
  return;
17598
17818
  }
17599
17819
  if (model.modelFormat === "openai") {
@@ -17672,6 +17892,7 @@ async function handleAnthropicMessages(req, res, options, modelCache, plog) {
17672
17892
  await withResponsesWebSocketDiagnosticContext(
17673
17893
  { requestId, claudeSessionId },
17674
17894
  () => streamAnthropicResponse(languageModel, params, responseModelId, writeStreamChunk, void 0, {
17895
+ abortSignal: clientAbort.signal,
17675
17896
  initialInputTokens: estimateAnthropicInputTokens(body),
17676
17897
  onPromptTokens: (total) => reportPricingBoundaryCrossing({
17677
17898
  modelKey: model.id,
@@ -17688,6 +17909,7 @@ async function handleAnthropicMessages(req, res, options, modelCache, plog) {
17688
17909
  { requestId, claudeSessionId },
17689
17910
  () => generateAnthropicResponse(languageModel, params, responseModelId, {
17690
17911
  forceStream: openAiOAuth,
17912
+ abortSignal: clientAbort.signal,
17691
17913
  onPromptTokens: (total) => reportPricingBoundaryCrossing({
17692
17914
  modelKey: model.id,
17693
17915
  modelLabel: model.name || model.id,
@@ -17700,6 +17922,7 @@ async function handleAnthropicMessages(req, res, options, modelCache, plog) {
17700
17922
  }
17701
17923
  break;
17702
17924
  } catch (err) {
17925
+ if (clientDisconnected(clientAbort.signal)) break;
17703
17926
  const message = formatUpstreamError(err);
17704
17927
  const details = sdkUpstreamErrorDetails(err);
17705
17928
  const candidateStatus = details?.statusCode ?? upstreamHttpStatus(err, message);
@@ -17736,7 +17959,7 @@ async function handleAnthropicMessages(req, res, options, modelCache, plog) {
17736
17959
  sendJson(res, status === 500 ? 502 : status, { error: { message: clientMessage } });
17737
17960
  }
17738
17961
  } else {
17739
- const errorType = anthropicErrorType(status);
17962
+ const errorType = anthropicErrorType(status, details?.transportCode);
17740
17963
  res.write(`event: error
17741
17964
  data: ${JSON.stringify({
17742
17965
  type: "error",
@@ -17755,6 +17978,7 @@ data: ${JSON.stringify({
17755
17978
  sendJson(res, 400, { error: { message: `Unsupported model format: ${model.modelFormat}` } });
17756
17979
  }
17757
17980
  async function handleAnthropicCountTokens(req, res, options, plog) {
17981
+ const clientAbort = watchClientDisconnect(res);
17758
17982
  const body = await readJson(req);
17759
17983
  if (!body) {
17760
17984
  sendJson(res, 400, { error: { message: "Invalid JSON body" } });
@@ -17805,18 +18029,25 @@ async function handleAnthropicCountTokens(req, res, options, plog) {
17805
18029
  ) : void 0;
17806
18030
  const countTokensUrl = `${model.baseUrl}/v1/messages/count_tokens`;
17807
18031
  plog(() => `anthropic-count-tokens \u2192 ${countTokensUrl} oauth=${isOAuth}`);
17808
- await relayAnthropicMessages(res, countTokensUrl, forwardBody, apiKey, false, {
17809
- inboundBeta,
17810
- authType,
17811
- log: (message) => plog(message),
17812
- extraHeaders: model.headers,
17813
- refreshToken,
17814
- onTokenRefreshed: (refreshed) => {
17815
- model.apiKey = refreshed;
17816
- }
17817
- });
18032
+ try {
18033
+ await relayAnthropicMessages(res, countTokensUrl, forwardBody, apiKey, false, {
18034
+ inboundBeta,
18035
+ authType,
18036
+ log: (message) => plog(message),
18037
+ extraHeaders: model.headers,
18038
+ refreshToken,
18039
+ onTokenRefreshed: (refreshed) => {
18040
+ model.apiKey = refreshed;
18041
+ },
18042
+ signal: clientAbort.signal
18043
+ });
18044
+ } catch (err) {
18045
+ if (clientDisconnected(clientAbort.signal)) return;
18046
+ throw err;
18047
+ }
17818
18048
  }
17819
18049
  async function handleOpenAIChatCompletions(req, res, options, modelCache, plog) {
18050
+ const clientAbort = watchClientDisconnect(res);
17820
18051
  const body = await readJson(req);
17821
18052
  if (!body) {
17822
18053
  sendJson(res, 400, { error: { message: "Invalid JSON body" } });
@@ -17857,21 +18088,27 @@ async function handleOpenAIChatCompletions(req, res, options, modelCache, plog)
17857
18088
  options.apiKey,
17858
18089
  rejectedAccessToken
17859
18090
  ) : void 0;
17860
- await relayAnthropicMessages(res, completionsUrl, forwardBody, apiKey2, Boolean(body.stream), {
17861
- authType: model.authType ?? "api",
17862
- extraHeaders: model.headers,
17863
- refreshToken,
17864
- onTokenRefreshed: (refreshed) => {
17865
- model.apiKey = refreshed;
17866
- },
17867
- onUpstreamError: options.inferenceLogPath ? (statusCode, errorContent) => writeInferenceResponseErrorLog(options.inferenceLogPath, {
17868
- modelId: body.model,
17869
- provider: inferenceProvider(model),
17870
- route: "passthrough",
17871
- statusCode,
17872
- errorContent
17873
- }) : void 0
17874
- });
18091
+ try {
18092
+ await relayAnthropicMessages(res, completionsUrl, forwardBody, apiKey2, Boolean(body.stream), {
18093
+ authType: model.authType ?? "api",
18094
+ extraHeaders: model.headers,
18095
+ refreshToken,
18096
+ onTokenRefreshed: (refreshed) => {
18097
+ model.apiKey = refreshed;
18098
+ },
18099
+ signal: clientAbort.signal,
18100
+ onUpstreamError: options.inferenceLogPath ? (statusCode, errorContent) => writeInferenceResponseErrorLog(options.inferenceLogPath, {
18101
+ modelId: body.model,
18102
+ provider: inferenceProvider(model),
18103
+ route: "passthrough",
18104
+ statusCode,
18105
+ errorContent
18106
+ }) : void 0
18107
+ });
18108
+ } catch (err) {
18109
+ if (clientDisconnected(clientAbort.signal)) return;
18110
+ throw err;
18111
+ }
17875
18112
  return;
17876
18113
  }
17877
18114
  const npm = model.npm || (model.modelFormat === "anthropic" ? "@ai-sdk/anthropic" : void 0);
@@ -17923,15 +18160,21 @@ async function handleOpenAIChatCompletions(req, res, options, modelCache, plog)
17923
18160
  }
17924
18161
  res.write(chunk);
17925
18162
  };
17926
- await streamOpenAiResponse(languageModel, params, responseModelId, writeStreamChunk);
18163
+ await streamOpenAiResponse(languageModel, params, responseModelId, writeStreamChunk, {
18164
+ abortSignal: clientAbort.signal
18165
+ });
17927
18166
  if (!res.headersSent) writeStreamChunk("");
17928
18167
  res.end();
17929
18168
  } else {
17930
- const response = await generateOpenAiResponse(languageModel, params, responseModelId, { forceStream: openAiOAuth });
18169
+ const response = await generateOpenAiResponse(languageModel, params, responseModelId, {
18170
+ forceStream: openAiOAuth,
18171
+ abortSignal: clientAbort.signal
18172
+ });
17931
18173
  sendJson(res, 200, response);
17932
18174
  }
17933
18175
  break;
17934
18176
  } catch (err) {
18177
+ if (clientDisconnected(clientAbort.signal)) break;
17935
18178
  const message = formatUpstreamError(err);
17936
18179
  const details = sdkUpstreamErrorDetails(err);
17937
18180
  const candidateStatus = details?.statusCode ?? upstreamHttpStatus(err, message);
@@ -18146,6 +18389,7 @@ import * as p9 from "@clack/prompts";
18146
18389
  import * as http2 from "http";
18147
18390
  import * as https from "https";
18148
18391
  import * as net from "net";
18392
+ import { PassThrough } from "stream";
18149
18393
  import { randomUUID as randomUUID6 } from "crypto";
18150
18394
  import { URL as URL2 } from "url";
18151
18395
  import { createBrotliDecompress, createGunzip, createInflate } from "zlib";
@@ -18456,7 +18700,7 @@ function forwardRawAnthropicRequest(req, res, rawBody, origin, rejectUnauthorize
18456
18700
  let settled = false;
18457
18701
  let responseEnded = false;
18458
18702
  let failed = false;
18459
- let clientDisconnected = false;
18703
+ let clientDisconnected2 = false;
18460
18704
  const writeLifecycle = (event, extra = {}) => {
18461
18705
  if (!lifecycle) return;
18462
18706
  writeInferenceResponseLifecycleLog(lifecycle.logPath, {
@@ -18498,7 +18742,7 @@ function forwardRawAnthropicRequest(req, res, rawBody, origin, rejectUnauthorize
18498
18742
  const errorType = (err) => err.code ?? err.name;
18499
18743
  let upstream;
18500
18744
  let attempt = 0;
18501
- const isRetryableUpstreamFailure = (err, request3) => attempt <= retryBudget && !headersReceived && !failed && !clientDisconnected && !isLocalShutdown() && request3.reusedSocket === true && err.code === RETRYABLE_PASSTHROUGH_CODE;
18745
+ const isRetryableUpstreamFailure = (err, request3) => attempt <= retryBudget && !headersReceived && !failed && !clientDisconnected2 && !isLocalShutdown() && request3.reusedSocket === true && err.code === RETRYABLE_PASSTHROUGH_CODE;
18502
18746
  const sendAttempt = () => {
18503
18747
  attempt += 1;
18504
18748
  const request3 = https.request({
@@ -18537,7 +18781,7 @@ function forwardRawAnthropicRequest(req, res, rawBody, origin, rejectUnauthorize
18537
18781
  done();
18538
18782
  });
18539
18783
  upstreamRes.once("error", (err) => {
18540
- if (clientDisconnected || failed) {
18784
+ if (clientDisconnected2 || failed) {
18541
18785
  done();
18542
18786
  return;
18543
18787
  }
@@ -18561,7 +18805,7 @@ function forwardRawAnthropicRequest(req, res, rawBody, origin, rejectUnauthorize
18561
18805
  });
18562
18806
  upstream = request3;
18563
18807
  request3.once("error", (err) => {
18564
- if (clientDisconnected) {
18808
+ if (clientDisconnected2) {
18565
18809
  done();
18566
18810
  return;
18567
18811
  }
@@ -18609,7 +18853,7 @@ function forwardRawAnthropicRequest(req, res, rawBody, origin, rejectUnauthorize
18609
18853
  };
18610
18854
  res.once("finish", () => {
18611
18855
  stopProgress();
18612
- if (failed || clientDisconnected) return;
18856
+ if (failed || clientDisconnected2) return;
18613
18857
  const now = Date.now();
18614
18858
  writeLifecycle("response_completed", {
18615
18859
  statusCode,
@@ -18623,7 +18867,7 @@ function forwardRawAnthropicRequest(req, res, rawBody, origin, rejectUnauthorize
18623
18867
  res.once("close", () => {
18624
18868
  stopProgress();
18625
18869
  if (res.writableFinished || failed) return;
18626
- clientDisconnected = true;
18870
+ clientDisconnected2 = true;
18627
18871
  const now = Date.now();
18628
18872
  writeLifecycle("response_client_disconnected", {
18629
18873
  statusCode,
@@ -18652,7 +18896,7 @@ function forwardToAdapter(req, res, rawBody, adapter, adapterRequest = http2.req
18652
18896
  let chunks = 0;
18653
18897
  let adapterEnded = false;
18654
18898
  let failed = false;
18655
- let clientDisconnected = false;
18899
+ let clientDisconnected2 = false;
18656
18900
  let adapterResponse;
18657
18901
  let upstream;
18658
18902
  const writeLifecycle = (event, extra = {}) => {
@@ -18690,7 +18934,7 @@ function forwardToAdapter(req, res, rawBody, adapter, adapterRequest = http2.req
18690
18934
  };
18691
18935
  res.once("finish", () => {
18692
18936
  stopProgress();
18693
- if (failed || clientDisconnected) return;
18937
+ if (failed || clientDisconnected2) return;
18694
18938
  const now = Date.now();
18695
18939
  writeLifecycle("response_completed", {
18696
18940
  statusCode,
@@ -18703,7 +18947,7 @@ function forwardToAdapter(req, res, rawBody, adapter, adapterRequest = http2.req
18703
18947
  res.once("close", () => {
18704
18948
  stopProgress();
18705
18949
  if (res.writableFinished || failed) return;
18706
- clientDisconnected = true;
18950
+ clientDisconnected2 = true;
18707
18951
  const now = Date.now();
18708
18952
  writeLifecycle("response_client_disconnected", {
18709
18953
  statusCode,
@@ -18720,7 +18964,7 @@ function forwardToAdapter(req, res, rawBody, adapter, adapterRequest = http2.req
18720
18964
  resolve3();
18721
18965
  });
18722
18966
  const failAdapterRequest = (err, failureSource) => {
18723
- if (clientDisconnected) {
18967
+ if (clientDisconnected2) {
18724
18968
  resolve3();
18725
18969
  return;
18726
18970
  }
@@ -18778,7 +19022,7 @@ function forwardToAdapter(req, res, rawBody, adapter, adapterRequest = http2.req
18778
19022
  });
18779
19023
  copyResponse(upstreamRes, res, void 0, lifecycle ? (usage) => writeLifecycle("response_usage", usage) : void 0);
18780
19024
  const failAdapterResponse = (err, failureSource) => {
18781
- if (clientDisconnected) {
19025
+ if (clientDisconnected2) {
18782
19026
  resolve3();
18783
19027
  return;
18784
19028
  }
@@ -18833,6 +19077,97 @@ function forwardToAdapter(req, res, rawBody, adapter, adapterRequest = http2.req
18833
19077
  upstream.end(rawBody);
18834
19078
  });
18835
19079
  }
19080
+ function forwardAnthropicUpgrade(req, clientSocket, head, origin, rejectUnauthorized, agent, sockets) {
19081
+ if (clientSocket._httpMessage) {
19082
+ clientSocket.destroy();
19083
+ return;
19084
+ }
19085
+ const clientData = new PassThrough();
19086
+ clientSocket.pipe(clientData);
19087
+ let responseStarted = false;
19088
+ let upgradedSocket;
19089
+ const upstream = https.request({
19090
+ protocol: "https:",
19091
+ hostname: origin.hostname,
19092
+ port: origin.port || 443,
19093
+ method: req.method,
19094
+ path: req.url,
19095
+ headers: requestHeadersWithoutProxyHeaders(req),
19096
+ servername: net.isIP(origin.hostname) ? void 0 : origin.hostname,
19097
+ rejectUnauthorized,
19098
+ agent
19099
+ });
19100
+ const fail = () => {
19101
+ if (clientSocket.destroyed) return;
19102
+ if (responseStarted) {
19103
+ clientSocket.destroy();
19104
+ return;
19105
+ }
19106
+ responseStarted = true;
19107
+ clientSocket.end(
19108
+ "HTTP/1.1 502 Bad Gateway\r\nConnection: close\r\nContent-Length: 0\r\n\r\n",
19109
+ () => clientSocket.destroy()
19110
+ );
19111
+ };
19112
+ clientSocket.once("error", () => clientSocket.destroy());
19113
+ clientSocket.once("end", () => {
19114
+ if (!responseStarted) clientSocket.destroy();
19115
+ });
19116
+ clientSocket.once("close", () => {
19117
+ clientData.destroy();
19118
+ upstream.destroy();
19119
+ upgradedSocket?.destroy();
19120
+ });
19121
+ upstream.once("socket", (socket) => {
19122
+ if (clientSocket.destroyed) socket.destroy();
19123
+ });
19124
+ upstream.once("error", fail);
19125
+ upstream.once("close", () => {
19126
+ if (!responseStarted) fail();
19127
+ });
19128
+ upstream.once("response", (upstreamRes) => {
19129
+ if (clientSocket.destroyed) {
19130
+ upstreamRes.destroy();
19131
+ return;
19132
+ }
19133
+ responseStarted = true;
19134
+ const response = new http2.ServerResponse(req);
19135
+ response.shouldKeepAlive = false;
19136
+ response.assignSocket(clientSocket);
19137
+ clientSocket.on("drain", () => response.emit("drain"));
19138
+ response.once("error", () => clientSocket.destroy());
19139
+ response.once("finish", () => clientSocket.end(() => clientSocket.destroy()));
19140
+ copyResponse(upstreamRes, response);
19141
+ });
19142
+ upstream.once("upgrade", (upstreamRes, socket, upstreamHead) => {
19143
+ if (clientSocket.destroyed) {
19144
+ socket.destroy();
19145
+ return;
19146
+ }
19147
+ responseStarted = true;
19148
+ upgradedSocket = socket;
19149
+ sockets.add(socket);
19150
+ socket.once("error", () => clientSocket.destroy());
19151
+ socket.once("close", () => {
19152
+ sockets.delete(socket);
19153
+ clientSocket.destroy();
19154
+ });
19155
+ const headers = [
19156
+ `HTTP/${upstreamRes.httpVersion} ${upstreamRes.statusCode} ${upstreamRes.statusMessage}`
19157
+ ];
19158
+ for (let i = 0; i < upstreamRes.rawHeaders.length; i += 2) {
19159
+ headers.push(`${upstreamRes.rawHeaders[i]}: ${upstreamRes.rawHeaders[i + 1]}`);
19160
+ }
19161
+ clientSocket.write(Buffer.from(`${headers.join("\r\n")}\r
19162
+ \r
19163
+ `, "latin1"));
19164
+ if (upstreamHead.length > 0) clientSocket.write(upstreamHead);
19165
+ if (head.length > 0) socket.write(head);
19166
+ clientData.pipe(socket);
19167
+ socket.pipe(clientSocket);
19168
+ });
19169
+ upstream.end();
19170
+ }
18836
19171
  function forwardPlainHttp(req, res) {
18837
19172
  let target;
18838
19173
  try {
@@ -19049,6 +19384,17 @@ async function startHttpProxy(options) {
19049
19384
  );
19050
19385
  });
19051
19386
  const sockets = /* @__PURE__ */ new Set();
19387
+ mitmServer.on("upgrade", (req, socket, head) => {
19388
+ forwardAnthropicUpgrade(
19389
+ req,
19390
+ socket,
19391
+ head,
19392
+ anthropicOrigin,
19393
+ options.anthropicRejectUnauthorized ?? true,
19394
+ anthropicAgent,
19395
+ sockets
19396
+ );
19397
+ });
19052
19398
  const proxyServer = http2.createServer(forwardPlainHttp);
19053
19399
  proxyServer.on("connection", (socket) => {
19054
19400
  sockets.add(socket);