@bman654/clodex 2.11.0 → 2.11.2

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.0",
385
+ version: "2.11.2",
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
  }
@@ -3121,7 +3168,6 @@ async function deleteProviderCredential(authRef, diag) {
3121
3168
  }
3122
3169
 
3123
3170
  // src/context-modes.ts
3124
- var DEFAULT_EFFECTIVE_CONTEXT_PERCENT = 95;
3125
3171
  function positiveInteger(value) {
3126
3172
  return typeof value === "number" && Number.isSafeInteger(value) && value > 0 ? value : void 0;
3127
3173
  }
@@ -5919,6 +5965,10 @@ function openAiPricingMetadata(id) {
5919
5965
  pricingBoundaryNote: GPT_5_6_PRICING_NOTE
5920
5966
  };
5921
5967
  }
5968
+ var LEGACY_IMPOSED_CONTEXT_PERCENT = 95;
5969
+ function migratedEffectiveContextPercent(cached) {
5970
+ return cached === LEGACY_IMPOSED_CONTEXT_PERCENT ? void 0 : cached;
5971
+ }
5922
5972
  function applyOAuthSeedContextMetadata(models) {
5923
5973
  const seedById = new Map(buildOpenAiOAuthModels().map((model) => [model.id, model]));
5924
5974
  return models.map((model) => {
@@ -5927,7 +5977,7 @@ function applyOAuthSeedContextMetadata(models) {
5927
5977
  return {
5928
5978
  ...model,
5929
5979
  maxContextWindow: model.maxContextWindow ?? seed?.maxContextWindow,
5930
- effectiveContextPercent: model.effectiveContextPercent ?? seed?.effectiveContextPercent ?? DEFAULT_EFFECTIVE_CONTEXT_PERCENT,
5980
+ effectiveContextPercent: migratedEffectiveContextPercent(model.effectiveContextPercent),
5931
5981
  pricingBoundary: model.pricingBoundary ?? seed?.pricingBoundary ?? pricing.pricingBoundary,
5932
5982
  pricingBoundaryNote: model.pricingBoundaryNote ?? seed?.pricingBoundaryNote ?? pricing.pricingBoundaryNote,
5933
5983
  maxOutputTokens: model.maxOutputTokens ?? seed?.maxOutputTokens,
@@ -5947,7 +5997,7 @@ function buildOpenAiOAuthModels() {
5947
5997
  brand: deriveBrand(prefix),
5948
5998
  contextWindow: resolveContextWindow(seed.id, seed.contextWindow),
5949
5999
  maxContextWindow: seed.maxContextWindow,
5950
- effectiveContextPercent: seed.effectiveContextPercent ?? DEFAULT_EFFECTIVE_CONTEXT_PERCENT,
6000
+ effectiveContextPercent: seed.effectiveContextPercent,
5951
6001
  pricingBoundary: seed.pricingBoundary ?? pricing.pricingBoundary,
5952
6002
  pricingBoundaryNote: seed.pricingBoundaryNote ?? pricing.pricingBoundaryNote,
5953
6003
  maxOutputTokens: seed.maxOutputTokens,
@@ -7789,6 +7839,23 @@ function clampRetryAfterSeconds(value) {
7789
7839
  }
7790
7840
  return Math.min(Math.round(value), MAX_RETRY_AFTER_SECONDS);
7791
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
+ }
7792
7859
  function retryAfterFromText(message) {
7793
7860
  if (typeof message !== "string") return void 0;
7794
7861
  const match = /retry after (\d+)s\b/i.exec(message);
@@ -7990,7 +8057,8 @@ function upstreamHttpStatus(err, message) {
7990
8057
  if (message.includes("HTTP 400")) return 400;
7991
8058
  return 500;
7992
8059
  }
7993
- function anthropicErrorType(status) {
8060
+ function anthropicErrorType(status, transportCode) {
8061
+ if (transportCode === "websocket_transport_error") return "overloaded_error";
7994
8062
  switch (status) {
7995
8063
  case 400:
7996
8064
  return "invalid_request_error";
@@ -8288,8 +8356,11 @@ var WsUpgradePacer = class {
8288
8356
  }
8289
8357
  /**
8290
8358
  * Resolves when this request may open a new connection, or resolves to a
8291
- * refusal the caller must report as a retryable rate limit. Callers that
8292
- * 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.
8293
8364
  */
8294
8365
  async admit(signal) {
8295
8366
  if (!this.enabled) return { kind: "admitted", waitedMs: 0 };
@@ -8307,7 +8378,14 @@ var WsUpgradePacer = class {
8307
8378
  )
8308
8379
  };
8309
8380
  }
8310
- 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
+ }
8311
8389
  const startedAt = this.now();
8312
8390
  try {
8313
8391
  await this.sleep(reservation.waitMs, signal);
@@ -8315,7 +8393,28 @@ var WsUpgradePacer = class {
8315
8393
  this.refund(reservation.consumed);
8316
8394
  throw error;
8317
8395
  }
8318
- 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
+ };
8319
8418
  }
8320
8419
  reserve(now) {
8321
8420
  const elapsed = Math.max(0, now - this.lastRefillAt);
@@ -8688,7 +8787,11 @@ function warnToolArgumentNormalizationGap(gap, log12) {
8688
8787
  emitParentNotice("clodex: warning: further tool-argument normalization warnings suppressed.");
8689
8788
  }
8690
8789
  }
8691
- 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
+ };
8692
8795
  const full = inputArray(payload);
8693
8796
  const prefix = [...entry.requestInput ?? [], ...entry.expectedAssistant ?? []];
8694
8797
  const comparable = Math.min(full.length, prefix.length);
@@ -8702,7 +8805,7 @@ function continuationMismatchDetails(entry, payload, log12, warnOnGap = false) {
8702
8805
  const expected = mismatch < prefix.length ? prefix[mismatch] : void 0;
8703
8806
  const actual = mismatch < full.length ? full[mismatch] : void 0;
8704
8807
  const reasoningGap = reasoningNormalizationGap(expected, actual);
8705
- if (reasoningGap && warnOnGap) warnReasoningNormalizationGap(reasoningGap, log12);
8808
+ if (reasoningGap && warnOnGap) raise(() => warnReasoningNormalizationGap(reasoningGap, log12));
8706
8809
  let gapExpected = expected;
8707
8810
  if (conversationItemKind(expected) === "reasoning" && conversationItemKind(actual) === "function_call") {
8708
8811
  for (let index = mismatch; index < prefix.length; index += 1) {
@@ -8724,7 +8827,8 @@ function continuationMismatchDetails(entry, payload, log12, warnOnGap = false) {
8724
8827
  } catch {
8725
8828
  }
8726
8829
  if (toolArgumentGap?.equalAfterStrip === true) {
8727
- if (warnOnGap) warnToolArgumentNormalizationGap(toolArgumentGap, log12);
8830
+ const gap = toolArgumentGap;
8831
+ if (warnOnGap) raise(() => warnToolArgumentNormalizationGap(gap, log12));
8728
8832
  } else if (toolArgumentGap && warnOnGap) {
8729
8833
  try {
8730
8834
  log12?.(`tool argument mismatch beyond the strip rule: ${String(toolArgumentGap.tool)}`);
@@ -9173,11 +9277,16 @@ function deleteEntry(entry, closeSocket = true) {
9173
9277
  }
9174
9278
  }
9175
9279
  }
9176
- function failContext(entry, ctx, message, diagnosticDetails, statusCode, retryAfterSeconds) {
9280
+ function failContext(entry, ctx, message, diagnosticDetails, statusCode, retryAfter) {
9177
9281
  if (ctx.closed || entry.current !== ctx) return;
9282
+ const retryAfterSeconds = retryAfter === void 0 ? void 0 : clampRetryAfterSeconds(retryAfter.seconds);
9178
9283
  entry.debug(`fail: ${message}`);
9179
9284
  emitResponseErrorDiagnostic(entry, ctx, {
9180
9285
  ...diagnosticDetails,
9286
+ ...retryAfter !== void 0 ? {
9287
+ retryAfterSource: retryAfter.source,
9288
+ ..."rawSeconds" in retryAfter ? { rawRetryAfterSeconds: retryAfter.rawSeconds } : {}
9289
+ } : {},
9181
9290
  ...diagnosticTextFingerprint("errorMessage", message)
9182
9291
  });
9183
9292
  flushPending(ctx);
@@ -9188,7 +9297,7 @@ function failContext(entry, ctx, message, diagnosticDetails, statusCode, retryAf
9188
9297
  type: statusCode === void 0 ? "transport_error" : anthropicErrorType(statusCode),
9189
9298
  code: statusCode === void 0 ? "websocket_transport_error" : String(statusCode),
9190
9299
  message,
9191
- param: null,
9300
+ param: retryAfter === void 0 ? null : retryAfterProvenanceParam(retryAfter),
9192
9301
  ...retryAfterSeconds !== void 0 ? { retry_after_seconds: retryAfterSeconds } : {}
9193
9302
  }
9194
9303
  });
@@ -9387,7 +9496,8 @@ function handleSocketMessage(entry, data) {
9387
9496
  const previousMissing = errorCode === "previous_response_not_found";
9388
9497
  const willRetry = previousMissing && ctx.continued && !ctx.retried && !ctx.emittedModelData;
9389
9498
  if (errorCode === "websocket_connection_limit_reached" && !ctx.emittedModelData) {
9390
- const retryAfterSeconds = clampRetryAfterSeconds(responseRetryAfterSeconds(event));
9499
+ const rawRetryAfterSeconds = responseRetryAfterSeconds(event);
9500
+ const retryAfterSeconds = clampRetryAfterSeconds(rawRetryAfterSeconds);
9391
9501
  failContext(
9392
9502
  entry,
9393
9503
  ctx,
@@ -9399,7 +9509,7 @@ function handleSocketMessage(entry, data) {
9399
9509
  retryAfterSeconds
9400
9510
  },
9401
9511
  429,
9402
- retryAfterSeconds
9512
+ rawRetryAfterSeconds === void 0 ? { seconds: retryAfterSeconds, source: "default" } : { seconds: retryAfterSeconds, source: "upstream", rawSeconds: rawRetryAfterSeconds }
9403
9513
  );
9404
9514
  return;
9405
9515
  }
@@ -9424,7 +9534,12 @@ function handleSocketMessage(entry, data) {
9424
9534
  }
9425
9535
  if (errorStatus !== void 0) {
9426
9536
  const statedRetryAfter = errorStatus === 429 ? responseRetryAfterSeconds(event) : void 0;
9427
- 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;
9428
9543
  const reason = responseErrorMessage(event) ?? `OpenAI rejected the request (HTTP ${errorStatus})`;
9429
9544
  failContext(
9430
9545
  entry,
@@ -9444,7 +9559,7 @@ function handleSocketMessage(entry, data) {
9444
9559
  ...retryAfterSeconds !== void 0 ? { retryAfterSeconds } : {}
9445
9560
  },
9446
9561
  errorStatus,
9447
- retryAfterSeconds
9562
+ retryAfter
9448
9563
  );
9449
9564
  return;
9450
9565
  }
@@ -9458,7 +9573,13 @@ function handleSocketMessage(entry, data) {
9458
9573
  const classified = numericOrNamed !== void 0 || discriminator ? frameStatusCode(numericOrNamed, discriminator) : 500;
9459
9574
  const statusCode = classified !== 500 ? classified : settledReason ? 400 : 502;
9460
9575
  const usageLimited = statusCode === 429;
9461
- 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;
9462
9583
  failContext(
9463
9584
  entry,
9464
9585
  ctx,
@@ -9477,7 +9598,7 @@ function handleSocketMessage(entry, data) {
9477
9598
  ...diagnosticTextFingerprint("upstreamMessage", responseErrorMessage(event))
9478
9599
  },
9479
9600
  statusCode,
9480
- retryAfterSeconds
9601
+ retryAfter
9481
9602
  );
9482
9603
  return;
9483
9604
  }
@@ -9583,15 +9704,14 @@ function createConnection(WebSocket, wsUrl, headers, persistent, key, options, d
9583
9704
  return;
9584
9705
  }
9585
9706
  if (statusCode === 403) {
9586
- const retryAfterSeconds = clampRetryAfterSeconds(
9587
- numericRetryAfterHeader(response.headers["retry-after"])
9588
- );
9707
+ const rawRetryAfterSeconds = numericRetryAfterHeader(response.headers["retry-after"]);
9708
+ const retryAfterSeconds = clampRetryAfterSeconds(rawRetryAfterSeconds);
9589
9709
  failContext(entry, ctx, `OpenAI edge throttled the Responses WebSocket upgrade (HTTP 403); retry after ${retryAfterSeconds}s`, {
9590
9710
  source: "unexpected_response",
9591
9711
  httpStatusCode: statusCode,
9592
9712
  mappedStatusCode: 429,
9593
9713
  retryAfterSeconds
9594
- }, 429, retryAfterSeconds);
9714
+ }, 429, rawRetryAfterSeconds === void 0 ? { seconds: retryAfterSeconds, source: "default" } : { seconds: retryAfterSeconds, source: "upstream", rawSeconds: rawRetryAfterSeconds });
9595
9715
  return;
9596
9716
  }
9597
9717
  failContext(entry, ctx, `WebSocket upgrade failed (HTTP ${statusCode})`, {
@@ -9684,18 +9804,25 @@ function createResponsesWebSocketFetch(wsUrl, log12, options = {}) {
9684
9804
  const diagnosticCorrelation = diagnosticContext.getStore();
9685
9805
  let now = resolvedOptions.now();
9686
9806
  const evictions = cleanupExpiredConnections(now);
9687
- const candidates = partitionKey ? connectionEntries(partitionKey) : [];
9688
- const idleCandidates = candidates.filter((entry) => !entry.inFlight);
9689
- const clientItems = idleCandidates.length ? canonicalItemStrings(inputArray(payload)) : [];
9690
- 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();
9691
9818
  let selected = matches[0]?.entry;
9692
- const selectedMatch = matches[0]?.match;
9693
- const selectedDelta = selectedMatch?.delta;
9819
+ let selectedMatch = matches[0]?.match;
9820
+ let selectedDelta = selectedMatch?.delta;
9694
9821
  const diagnosticEntry = selected ?? [...idleCandidates].sort((left, right) => right.lastUsedAt - left.lastUsedAt)[0] ?? candidates[0];
9695
9822
  debug(
9696
9823
  `lookup key=${debugKey(partitionKey)} prompt=${debugKey(promptFingerprint)} hit=${candidates.length > 0} heads=${candidates.length} active_connections=${connectionCount()}`
9697
9824
  );
9698
- const promptChanges = changedPromptFields(diagnosticEntry?.promptFieldHashes, promptFieldHashes);
9825
+ let promptChanges = changedPromptFields(diagnosticEntry?.promptFieldHashes, promptFieldHashes);
9699
9826
  if (promptChanges.length) debug(`prompt fields changed: ${promptChanges.join(",")}`);
9700
9827
  if (promptChanges.includes("instructions")) {
9701
9828
  const summary = instructionChangeSummary(diagnosticEntry?.instructionsSnapshot, instructionsSnapshot);
@@ -9707,35 +9834,49 @@ function createResponsesWebSocketFetch(wsUrl, log12, options = {}) {
9707
9834
  let promotedConnectionId;
9708
9835
  let decision;
9709
9836
  let candidateMismatchDetails;
9710
- if (selected && selectedDelta) {
9711
- 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 };
9712
9844
  continued = true;
9713
- if (selected.generation === "nursery") {
9845
+ if (entry.generation === "nursery") {
9714
9846
  evictions.push(...evictOldestIdleGeneration(
9715
9847
  "established",
9716
9848
  resolvedOptions.maxConnections,
9717
9849
  "established_lru_cap"
9718
9850
  ));
9719
- selected.generation = "established";
9720
- promotedConnectionId = selected.debugId;
9851
+ entry.generation = "established";
9852
+ promotedConnectionId = entry.debugId;
9721
9853
  }
9722
- decision = "continuation";
9723
9854
  debug(
9724
- `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" : "")
9725
9856
  );
9857
+ return "continuation";
9858
+ };
9859
+ if (selected && selectedDelta) {
9860
+ decision = continueOnHead(selected, selectedMatch);
9726
9861
  } else if (candidates.some((entry) => entry.inFlight)) {
9727
9862
  selected = void 0;
9728
9863
  persistent = false;
9729
9864
  decision = "parallel_isolated";
9730
9865
  debug("parallel request using an isolated socket");
9731
9866
  } else if (diagnosticEntry) {
9732
- const diagnosticMismatch = continuationMismatchDetails(diagnosticEntry, payload, debug, true);
9867
+ const diagnosticMismatch = continuationMismatchDetails(
9868
+ diagnosticEntry,
9869
+ payload,
9870
+ debug,
9871
+ true,
9872
+ deferredMismatchWarnings
9873
+ );
9733
9874
  candidateMismatchDetails = /* @__PURE__ */ new Map([[diagnosticEntry, diagnosticMismatch]]);
9734
9875
  for (const candidate of candidates) {
9735
9876
  if (candidate === diagnosticEntry) continue;
9736
9877
  candidateMismatchDetails.set(
9737
9878
  candidate,
9738
- continuationMismatchDetails(candidate, payload, debug, true)
9879
+ continuationMismatchDetails(candidate, payload, debug, true, deferredMismatchWarnings)
9739
9880
  );
9740
9881
  }
9741
9882
  debug(
@@ -9754,6 +9895,7 @@ function createResponsesWebSocketFetch(wsUrl, log12, options = {}) {
9754
9895
  decision = "unpartitioned_socket";
9755
9896
  }
9756
9897
  let pacingWaitedMs;
9898
+ let pacingRescanOutcome;
9757
9899
  if (!selected) {
9758
9900
  const pacer = options.pacer ?? sharedWsUpgradePacer();
9759
9901
  const pacingStartedAt = resolvedOptions.now();
@@ -9767,6 +9909,7 @@ function createResponsesWebSocketFetch(wsUrl, log12, options = {}) {
9767
9909
  decision,
9768
9910
  waitedMs: Math.max(0, resolvedOptions.now() - pacingStartedAt)
9769
9911
  }, diagnosticCorrelation);
9912
+ flushMismatchWarnings();
9770
9913
  throw error;
9771
9914
  }
9772
9915
  if (admission.kind === "refused") {
@@ -9780,6 +9923,7 @@ function createResponsesWebSocketFetch(wsUrl, log12, options = {}) {
9780
9923
  requiredWaitMs: admission.requiredWaitMs,
9781
9924
  retryAfterSeconds: admission.retryAfterSeconds
9782
9925
  }, diagnosticCorrelation);
9926
+ flushMismatchWarnings();
9783
9927
  return pacedRefusalResponse(admission.retryAfterSeconds);
9784
9928
  }
9785
9929
  if (admission.waitedMs > 0) {
@@ -9794,12 +9938,42 @@ function createResponsesWebSocketFetch(wsUrl, log12, options = {}) {
9794
9938
  }
9795
9939
  now = resolvedOptions.now();
9796
9940
  evictions.push(...cleanupExpiredConnections(now));
9797
- 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)) {
9798
9961
  persistent = false;
9799
9962
  decision = "parallel_isolated";
9800
9963
  debug("parallel request using an isolated socket after pacing");
9964
+ if (pacingRescanOutcome === "no_change") pacingRescanOutcome = "parallel_isolated";
9801
9965
  }
9802
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
+ }
9803
9977
  if (!selected && persistent) {
9804
9978
  evictions.push(...evictOldestIdleGeneration(
9805
9979
  "nursery",
@@ -9842,6 +10016,8 @@ function createResponsesWebSocketFetch(wsUrl, log12, options = {}) {
9842
10016
  promotedConnectionId,
9843
10017
  createdConnectionId: selected ? void 0 : nextConnectionDebugId,
9844
10018
  ...pacingWaitedMs !== void 0 ? { pacingWaitedMs } : {},
10019
+ ...pacingRescanOutcome !== void 0 ? { pacingRescanOutcome } : {},
10020
+ ...suppressedMismatchWarnings !== void 0 ? { suppressedMismatchWarnings } : {},
9845
10021
  createdGeneration: selected ? void 0 : persistent ? "nursery" : "isolated",
9846
10022
  incrementalInputItems: selectedDelta?.length,
9847
10023
  heads: candidates.map((entry) => ({
@@ -10925,6 +11101,28 @@ function sendJson(res, status, body) {
10925
11101
  res.writeHead(status, { "Content-Type": "application/json" });
10926
11102
  res.end(json);
10927
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
+ }
10928
11126
 
10929
11127
  // src/server/vendor-mask.ts
10930
11128
  function reverseSegment(value) {
@@ -11409,7 +11607,23 @@ function resolveUpstreamTools(tools, messages) {
11409
11607
  }
11410
11608
 
11411
11609
  // src/upstream-attempts.ts
11412
- 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
+ }
11413
11627
  function trackUpstreamAttempts(model) {
11414
11628
  if (typeof model === "string") {
11415
11629
  return { model, deadlineError: (timeoutError) => timeoutError };
@@ -11423,6 +11637,7 @@ function trackUpstreamAttempts(model) {
11423
11637
  failedAttempts.length = 0;
11424
11638
  return result;
11425
11639
  } catch (error) {
11640
+ restoreSyntheticRetryAfterHeader(error);
11426
11641
  failedAttempts.push(error);
11427
11642
  waitingToRetry = true;
11428
11643
  throw error;
@@ -12079,9 +12294,10 @@ async function writeAnthropicStream(stream, modelId, write, log12, observer, too
12079
12294
  case "error": {
12080
12295
  const e = part.error;
12081
12296
  const errMsg = e?.message || (typeof part.error === "string" ? part.error : JSON.stringify(e?.data ?? part.error));
12082
- const errorType = anthropicErrorType(upstreamHttpStatus(part.error, errMsg));
12297
+ const transportCode = sdkUpstreamErrorDetails(part.error)?.transportCode;
12298
+ const errorType = anthropicErrorType(upstreamHttpStatus(part.error, errMsg), transportCode);
12083
12299
  log12?.(() => `sdk stream error (${errorType}): ${errMsg}`);
12084
- closeOpen();
12300
+ if (!(transportCode === "websocket_transport_error" && openType === "thinking")) closeOpen();
12085
12301
  throw part.error instanceof Error || part.error && typeof part.error === "object" ? part.error : new Error(errMsg);
12086
12302
  }
12087
12303
  default:
@@ -12523,14 +12739,7 @@ async function startProxyCatalog(routes, defaultAliasId, debug = false, inferenc
12523
12739
  anthropicError(res, 401, "Invalid proxy token");
12524
12740
  return;
12525
12741
  }
12526
- const clientAbort = new AbortController();
12527
- const abortForClientDisconnect = () => {
12528
- if (!clientAbort.signal.aborted) clientAbort.abort(new Error("Client disconnected"));
12529
- };
12530
- req.once("aborted", abortForClientDisconnect);
12531
- res.once("close", () => {
12532
- if (!res.writableFinished) abortForClientDisconnect();
12533
- });
12742
+ const clientAbort = watchClientDisconnect(res);
12534
12743
  let anthropicBody;
12535
12744
  try {
12536
12745
  const raw = await readBody(req);
@@ -12610,7 +12819,7 @@ async function startProxyCatalog(routes, defaultAliasId, debug = false, inferenc
12610
12819
  signal: clientAbort.signal
12611
12820
  });
12612
12821
  } catch (err) {
12613
- if (clientAbort.signal.aborted) return;
12822
+ if (clientDisconnected(clientAbort.signal)) return;
12614
12823
  const message = err instanceof UpstreamUnreachableError ? err.message : String(err);
12615
12824
  plog(() => `anthropic token-count error: ${message}`);
12616
12825
  anthropicError(res, 502, message);
@@ -12671,7 +12880,7 @@ async function startProxyCatalog(routes, defaultAliasId, debug = false, inferenc
12671
12880
  }) : void 0
12672
12881
  });
12673
12882
  } catch (err) {
12674
- if (clientAbort.signal.aborted) return;
12883
+ if (clientDisconnected(clientAbort.signal)) return;
12675
12884
  const message = err instanceof UpstreamUnreachableError ? err.message : String(err);
12676
12885
  plog(() => `anthropic-passthrough error: ${message}`);
12677
12886
  anthropicError(res, 502, message);
@@ -12811,7 +13020,7 @@ async function startProxyCatalog(routes, defaultAliasId, debug = false, inferenc
12811
13020
  };
12812
13021
  let sdkAttempt = 0;
12813
13022
  const handleSdkError = async (err) => {
12814
- if (clientAbort.signal.aborted) {
13023
+ if (clientDisconnected(clientAbort.signal)) {
12815
13024
  translationLifecycle?.cancel();
12816
13025
  return "cancelled";
12817
13026
  }
@@ -12867,7 +13076,7 @@ async function startProxyCatalog(routes, defaultAliasId, debug = false, inferenc
12867
13076
  contextLengthExceeded ? relayRequestId ?? randomUUID3() : void 0
12868
13077
  );
12869
13078
  } else {
12870
- const errorType = anthropicErrorType(upstreamStatus);
13079
+ const errorType = anthropicErrorType(upstreamStatus, details?.transportCode);
12871
13080
  res.write(`event: error
12872
13081
  data: ${JSON.stringify({
12873
13082
  type: "error",
@@ -15090,7 +15299,10 @@ function buildDynamicOAuthModel(entry, seedById, codexCatalog) {
15090
15299
  brand: deriveBrand(prefix),
15091
15300
  contextWindow: entry.context_window ?? resolveContextWindow(id),
15092
15301
  maxContextWindow: entry.max_context_window,
15093
- effectiveContextPercent: entry.effective_context_window_percent ?? DEFAULT_EFFECTIVE_CONTEXT_PERCENT,
15302
+ // Absent means no reduction. clodex reports the window the provider actually
15303
+ // gives; deciding how much of it to leave free is the client's job, and Claude
15304
+ // Code already reserves a flat 33,000 tokens below whatever it is told.
15305
+ effectiveContextPercent: entry.effective_context_window_percent,
15094
15306
  maxOutputTokens: entry.max_output_tokens,
15095
15307
  ...openAiPricingMetadata(id),
15096
15308
  modelFormat: "openai",
@@ -15113,9 +15325,9 @@ function buildDynamicOAuthModel(entry, seedById, codexCatalog) {
15113
15325
  };
15114
15326
  }
15115
15327
  async function fetchJsonWithAuth(url, accessToken, timeoutMs) {
15328
+ const controller = new AbortController();
15329
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
15116
15330
  try {
15117
- const controller = new AbortController();
15118
- const timer = setTimeout(() => controller.abort(), timeoutMs);
15119
15331
  const response = await fetch(url, {
15120
15332
  headers: {
15121
15333
  Accept: "application/json",
@@ -15123,7 +15335,7 @@ async function fetchJsonWithAuth(url, accessToken, timeoutMs) {
15123
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"
15124
15336
  },
15125
15337
  signal: controller.signal
15126
- }).finally(() => clearTimeout(timer));
15338
+ });
15127
15339
  if (!response.ok) {
15128
15340
  const detail = await response.text().then((t) => t.slice(0, 200)).catch(() => "");
15129
15341
  return { body: null, error: `HTTP ${response.status}${detail ? `: ${detail}` : ""}` };
@@ -15131,6 +15343,11 @@ async function fetchJsonWithAuth(url, accessToken, timeoutMs) {
15131
15343
  return { body: await response.json() };
15132
15344
  } catch (err) {
15133
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
+ }
15134
15351
  }
15135
15352
  }
15136
15353
  async function refreshOpenAiOAuthModels(accessToken) {
@@ -17218,10 +17435,11 @@ async function collectOpenAiStream(stream) {
17218
17435
  }
17219
17436
  return collected;
17220
17437
  }
17221
- function startUpstreamBudget(model, streaming) {
17438
+ function startUpstreamBudget(model, streaming, clientSignal) {
17222
17439
  const { idleTimeoutMs, totalTimeoutMs, maxRetries } = upstreamRequestBudget();
17223
17440
  const attempts = trackUpstreamAttempts(model);
17224
17441
  const abort = new AbortController();
17442
+ const stopForwardingAbort = forwardAbortSignal(clientSignal, abort);
17225
17443
  const idleError = () => attempts.deadlineError(new Error(
17226
17444
  `no data received from provider for ${Math.round(idleTimeoutMs / 1e3)}s`
17227
17445
  ));
@@ -17242,6 +17460,7 @@ function startUpstreamBudget(model, streaming) {
17242
17460
  idleTimer = setTimeout(() => abort.abort(idleError()), idleTimeoutMs);
17243
17461
  },
17244
17462
  close: () => {
17463
+ stopForwardingAbort();
17245
17464
  if (idleTimer !== void 0) clearTimeout(idleTimer);
17246
17465
  clearTimeout(totalTimer);
17247
17466
  if (!abort.signal.aborted) abort.abort();
@@ -17263,7 +17482,7 @@ async function* watchOpenAiStream(stream, budget) {
17263
17482
  async function generateOpenAiResponse(model, params, responseModelId, options) {
17264
17483
  let result;
17265
17484
  const streaming = options?.forceStream === true;
17266
- const budget = startUpstreamBudget(model, streaming);
17485
+ const budget = startUpstreamBudget(model, streaming, options?.abortSignal);
17267
17486
  try {
17268
17487
  if (streaming) {
17269
17488
  const { stream } = streamText2({
@@ -17316,8 +17535,8 @@ async function generateOpenAiResponse(model, params, responseModelId, options) {
17316
17535
  }
17317
17536
  };
17318
17537
  }
17319
- async function streamOpenAiResponse(model, params, responseModelId, onChunk) {
17320
- const budget = startUpstreamBudget(model, true);
17538
+ async function streamOpenAiResponse(model, params, responseModelId, onChunk, options) {
17539
+ const budget = startUpstreamBudget(model, true, options?.abortSignal);
17321
17540
  try {
17322
17541
  const { stream } = streamText2({
17323
17542
  model: budget.model,
@@ -17493,6 +17712,7 @@ async function routeRequest(req, res, options, modelCache, plog) {
17493
17712
  }
17494
17713
  }
17495
17714
  async function handleAnthropicMessages(req, res, options, modelCache, plog) {
17715
+ const clientAbort = watchClientDisconnect(res);
17496
17716
  const body = await readJson(req);
17497
17717
  if (!body) {
17498
17718
  sendJson(res, 400, { error: { message: "Invalid JSON body" } });
@@ -17566,28 +17786,34 @@ async function handleAnthropicMessages(req, res, options, modelCache, plog) {
17566
17786
  rejectedAccessToken
17567
17787
  ) : void 0;
17568
17788
  plog(() => `anthropic-passthrough \u2192 ${messagesUrl} oauth=${isOAuth} stream=${clientWantsStream}`);
17569
- await relayAnthropicMessages(res, messagesUrl, forwardBody, apiKey, clientWantsStream, {
17570
- inboundBeta: effectiveBeta,
17571
- authType,
17572
- log: (message) => plog(message),
17573
- claudeCodeSessionId,
17574
- extraHeaders: model.headers,
17575
- refreshToken,
17576
- onTokenRefreshed: (refreshed) => {
17577
- model.apiKey = refreshed;
17578
- },
17579
- // Echo the exact requested id when it differs from the upstream id, so
17580
- // clients that key context windows on the response model still resolve.
17581
- responseModelOverride: typeof body.model === "string" && body.model !== upstreamModelId(model) ? body.model : void 0,
17582
- onUpstreamError: options.inferenceLogPath ? (statusCode, errorContent) => writeInferenceResponseErrorLog(options.inferenceLogPath, {
17583
- requestId,
17584
- modelId: body.model,
17585
- provider: inferenceProvider(model),
17586
- route: "passthrough",
17587
- statusCode,
17588
- errorContent
17589
- }) : void 0
17590
- });
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
+ }
17591
17817
  return;
17592
17818
  }
17593
17819
  if (model.modelFormat === "openai") {
@@ -17666,6 +17892,7 @@ async function handleAnthropicMessages(req, res, options, modelCache, plog) {
17666
17892
  await withResponsesWebSocketDiagnosticContext(
17667
17893
  { requestId, claudeSessionId },
17668
17894
  () => streamAnthropicResponse(languageModel, params, responseModelId, writeStreamChunk, void 0, {
17895
+ abortSignal: clientAbort.signal,
17669
17896
  initialInputTokens: estimateAnthropicInputTokens(body),
17670
17897
  onPromptTokens: (total) => reportPricingBoundaryCrossing({
17671
17898
  modelKey: model.id,
@@ -17682,6 +17909,7 @@ async function handleAnthropicMessages(req, res, options, modelCache, plog) {
17682
17909
  { requestId, claudeSessionId },
17683
17910
  () => generateAnthropicResponse(languageModel, params, responseModelId, {
17684
17911
  forceStream: openAiOAuth,
17912
+ abortSignal: clientAbort.signal,
17685
17913
  onPromptTokens: (total) => reportPricingBoundaryCrossing({
17686
17914
  modelKey: model.id,
17687
17915
  modelLabel: model.name || model.id,
@@ -17694,6 +17922,7 @@ async function handleAnthropicMessages(req, res, options, modelCache, plog) {
17694
17922
  }
17695
17923
  break;
17696
17924
  } catch (err) {
17925
+ if (clientDisconnected(clientAbort.signal)) break;
17697
17926
  const message = formatUpstreamError(err);
17698
17927
  const details = sdkUpstreamErrorDetails(err);
17699
17928
  const candidateStatus = details?.statusCode ?? upstreamHttpStatus(err, message);
@@ -17730,7 +17959,7 @@ async function handleAnthropicMessages(req, res, options, modelCache, plog) {
17730
17959
  sendJson(res, status === 500 ? 502 : status, { error: { message: clientMessage } });
17731
17960
  }
17732
17961
  } else {
17733
- const errorType = anthropicErrorType(status);
17962
+ const errorType = anthropicErrorType(status, details?.transportCode);
17734
17963
  res.write(`event: error
17735
17964
  data: ${JSON.stringify({
17736
17965
  type: "error",
@@ -17749,6 +17978,7 @@ data: ${JSON.stringify({
17749
17978
  sendJson(res, 400, { error: { message: `Unsupported model format: ${model.modelFormat}` } });
17750
17979
  }
17751
17980
  async function handleAnthropicCountTokens(req, res, options, plog) {
17981
+ const clientAbort = watchClientDisconnect(res);
17752
17982
  const body = await readJson(req);
17753
17983
  if (!body) {
17754
17984
  sendJson(res, 400, { error: { message: "Invalid JSON body" } });
@@ -17799,18 +18029,25 @@ async function handleAnthropicCountTokens(req, res, options, plog) {
17799
18029
  ) : void 0;
17800
18030
  const countTokensUrl = `${model.baseUrl}/v1/messages/count_tokens`;
17801
18031
  plog(() => `anthropic-count-tokens \u2192 ${countTokensUrl} oauth=${isOAuth}`);
17802
- await relayAnthropicMessages(res, countTokensUrl, forwardBody, apiKey, false, {
17803
- inboundBeta,
17804
- authType,
17805
- log: (message) => plog(message),
17806
- extraHeaders: model.headers,
17807
- refreshToken,
17808
- onTokenRefreshed: (refreshed) => {
17809
- model.apiKey = refreshed;
17810
- }
17811
- });
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
+ }
17812
18048
  }
17813
18049
  async function handleOpenAIChatCompletions(req, res, options, modelCache, plog) {
18050
+ const clientAbort = watchClientDisconnect(res);
17814
18051
  const body = await readJson(req);
17815
18052
  if (!body) {
17816
18053
  sendJson(res, 400, { error: { message: "Invalid JSON body" } });
@@ -17851,21 +18088,27 @@ async function handleOpenAIChatCompletions(req, res, options, modelCache, plog)
17851
18088
  options.apiKey,
17852
18089
  rejectedAccessToken
17853
18090
  ) : void 0;
17854
- await relayAnthropicMessages(res, completionsUrl, forwardBody, apiKey2, Boolean(body.stream), {
17855
- authType: model.authType ?? "api",
17856
- extraHeaders: model.headers,
17857
- refreshToken,
17858
- onTokenRefreshed: (refreshed) => {
17859
- model.apiKey = refreshed;
17860
- },
17861
- onUpstreamError: options.inferenceLogPath ? (statusCode, errorContent) => writeInferenceResponseErrorLog(options.inferenceLogPath, {
17862
- modelId: body.model,
17863
- provider: inferenceProvider(model),
17864
- route: "passthrough",
17865
- statusCode,
17866
- errorContent
17867
- }) : void 0
17868
- });
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
+ }
17869
18112
  return;
17870
18113
  }
17871
18114
  const npm = model.npm || (model.modelFormat === "anthropic" ? "@ai-sdk/anthropic" : void 0);
@@ -17917,15 +18160,21 @@ async function handleOpenAIChatCompletions(req, res, options, modelCache, plog)
17917
18160
  }
17918
18161
  res.write(chunk);
17919
18162
  };
17920
- await streamOpenAiResponse(languageModel, params, responseModelId, writeStreamChunk);
18163
+ await streamOpenAiResponse(languageModel, params, responseModelId, writeStreamChunk, {
18164
+ abortSignal: clientAbort.signal
18165
+ });
17921
18166
  if (!res.headersSent) writeStreamChunk("");
17922
18167
  res.end();
17923
18168
  } else {
17924
- 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
+ });
17925
18173
  sendJson(res, 200, response);
17926
18174
  }
17927
18175
  break;
17928
18176
  } catch (err) {
18177
+ if (clientDisconnected(clientAbort.signal)) break;
17929
18178
  const message = formatUpstreamError(err);
17930
18179
  const details = sdkUpstreamErrorDetails(err);
17931
18180
  const candidateStatus = details?.statusCode ?? upstreamHttpStatus(err, message);
@@ -18450,7 +18699,7 @@ function forwardRawAnthropicRequest(req, res, rawBody, origin, rejectUnauthorize
18450
18699
  let settled = false;
18451
18700
  let responseEnded = false;
18452
18701
  let failed = false;
18453
- let clientDisconnected = false;
18702
+ let clientDisconnected2 = false;
18454
18703
  const writeLifecycle = (event, extra = {}) => {
18455
18704
  if (!lifecycle) return;
18456
18705
  writeInferenceResponseLifecycleLog(lifecycle.logPath, {
@@ -18492,7 +18741,7 @@ function forwardRawAnthropicRequest(req, res, rawBody, origin, rejectUnauthorize
18492
18741
  const errorType = (err) => err.code ?? err.name;
18493
18742
  let upstream;
18494
18743
  let attempt = 0;
18495
- const isRetryableUpstreamFailure = (err, request3) => attempt <= retryBudget && !headersReceived && !failed && !clientDisconnected && !isLocalShutdown() && request3.reusedSocket === true && err.code === RETRYABLE_PASSTHROUGH_CODE;
18744
+ const isRetryableUpstreamFailure = (err, request3) => attempt <= retryBudget && !headersReceived && !failed && !clientDisconnected2 && !isLocalShutdown() && request3.reusedSocket === true && err.code === RETRYABLE_PASSTHROUGH_CODE;
18496
18745
  const sendAttempt = () => {
18497
18746
  attempt += 1;
18498
18747
  const request3 = https.request({
@@ -18531,7 +18780,7 @@ function forwardRawAnthropicRequest(req, res, rawBody, origin, rejectUnauthorize
18531
18780
  done();
18532
18781
  });
18533
18782
  upstreamRes.once("error", (err) => {
18534
- if (clientDisconnected || failed) {
18783
+ if (clientDisconnected2 || failed) {
18535
18784
  done();
18536
18785
  return;
18537
18786
  }
@@ -18555,7 +18804,7 @@ function forwardRawAnthropicRequest(req, res, rawBody, origin, rejectUnauthorize
18555
18804
  });
18556
18805
  upstream = request3;
18557
18806
  request3.once("error", (err) => {
18558
- if (clientDisconnected) {
18807
+ if (clientDisconnected2) {
18559
18808
  done();
18560
18809
  return;
18561
18810
  }
@@ -18603,7 +18852,7 @@ function forwardRawAnthropicRequest(req, res, rawBody, origin, rejectUnauthorize
18603
18852
  };
18604
18853
  res.once("finish", () => {
18605
18854
  stopProgress();
18606
- if (failed || clientDisconnected) return;
18855
+ if (failed || clientDisconnected2) return;
18607
18856
  const now = Date.now();
18608
18857
  writeLifecycle("response_completed", {
18609
18858
  statusCode,
@@ -18617,7 +18866,7 @@ function forwardRawAnthropicRequest(req, res, rawBody, origin, rejectUnauthorize
18617
18866
  res.once("close", () => {
18618
18867
  stopProgress();
18619
18868
  if (res.writableFinished || failed) return;
18620
- clientDisconnected = true;
18869
+ clientDisconnected2 = true;
18621
18870
  const now = Date.now();
18622
18871
  writeLifecycle("response_client_disconnected", {
18623
18872
  statusCode,
@@ -18646,7 +18895,7 @@ function forwardToAdapter(req, res, rawBody, adapter, adapterRequest = http2.req
18646
18895
  let chunks = 0;
18647
18896
  let adapterEnded = false;
18648
18897
  let failed = false;
18649
- let clientDisconnected = false;
18898
+ let clientDisconnected2 = false;
18650
18899
  let adapterResponse;
18651
18900
  let upstream;
18652
18901
  const writeLifecycle = (event, extra = {}) => {
@@ -18684,7 +18933,7 @@ function forwardToAdapter(req, res, rawBody, adapter, adapterRequest = http2.req
18684
18933
  };
18685
18934
  res.once("finish", () => {
18686
18935
  stopProgress();
18687
- if (failed || clientDisconnected) return;
18936
+ if (failed || clientDisconnected2) return;
18688
18937
  const now = Date.now();
18689
18938
  writeLifecycle("response_completed", {
18690
18939
  statusCode,
@@ -18697,7 +18946,7 @@ function forwardToAdapter(req, res, rawBody, adapter, adapterRequest = http2.req
18697
18946
  res.once("close", () => {
18698
18947
  stopProgress();
18699
18948
  if (res.writableFinished || failed) return;
18700
- clientDisconnected = true;
18949
+ clientDisconnected2 = true;
18701
18950
  const now = Date.now();
18702
18951
  writeLifecycle("response_client_disconnected", {
18703
18952
  statusCode,
@@ -18714,7 +18963,7 @@ function forwardToAdapter(req, res, rawBody, adapter, adapterRequest = http2.req
18714
18963
  resolve3();
18715
18964
  });
18716
18965
  const failAdapterRequest = (err, failureSource) => {
18717
- if (clientDisconnected) {
18966
+ if (clientDisconnected2) {
18718
18967
  resolve3();
18719
18968
  return;
18720
18969
  }
@@ -18772,7 +19021,7 @@ function forwardToAdapter(req, res, rawBody, adapter, adapterRequest = http2.req
18772
19021
  });
18773
19022
  copyResponse(upstreamRes, res, void 0, lifecycle ? (usage) => writeLifecycle("response_usage", usage) : void 0);
18774
19023
  const failAdapterResponse = (err, failureSource) => {
18775
- if (clientDisconnected) {
19024
+ if (clientDisconnected2) {
18776
19025
  resolve3();
18777
19026
  return;
18778
19027
  }