@bman654/clodex 2.9.0 → 2.10.0

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.9.0",
385
+ version: "2.10.0",
386
386
  publishConfig: {
387
387
  access: "public"
388
388
  },
@@ -8009,6 +8009,369 @@ function sanitizeToolInput(input, requiredProps) {
8009
8009
  return out;
8010
8010
  }
8011
8011
 
8012
+ // src/upstream-retry.ts
8013
+ var UPSTREAM_MAX_RETRIES_ENV = "CLODEX_UPSTREAM_MAX_RETRIES";
8014
+ var UPSTREAM_IDLE_TIMEOUT_ENV = "CLODEX_UPSTREAM_IDLE_TIMEOUT_MS";
8015
+ var UPSTREAM_TOTAL_TIMEOUT_ENV = "CLODEX_UPSTREAM_TOTAL_TIMEOUT_MS";
8016
+ var DEFAULT_UPSTREAM_IDLE_TIMEOUT_MS = 12e4;
8017
+ var DEFAULT_UPSTREAM_TOTAL_TIMEOUT_MS = 10 * 6e4;
8018
+ var MIN_UPSTREAM_IDLE_TIMEOUT_MS = 1e4;
8019
+ var MAX_UPSTREAM_IDLE_TIMEOUT_MS = 60 * 6e4;
8020
+ var MIN_UPSTREAM_TOTAL_TIMEOUT_MS = 6e4;
8021
+ var MAX_UPSTREAM_TOTAL_TIMEOUT_MS = 6 * 60 * 6e4;
8022
+ var SDK_INITIAL_RETRY_DELAY_MS = 2e3;
8023
+ var reportedValues = /* @__PURE__ */ new Set();
8024
+ var defaultWarn = (message) => emitParentNotice(`clodex: ${message}`);
8025
+ function reportOnce(key, message, warn) {
8026
+ if (reportedValues.has(key)) return;
8027
+ reportedValues.add(key);
8028
+ try {
8029
+ warn(message);
8030
+ } catch {
8031
+ }
8032
+ }
8033
+ function timeoutSetting(env, envName, fallback, min, max, warn) {
8034
+ const raw = env[envName]?.trim();
8035
+ if (raw === void 0 || raw === "") return { value: fallback, explicit: false };
8036
+ const value = Number(raw);
8037
+ if (!Number.isInteger(value) || value <= 0) {
8038
+ reportOnce(
8039
+ `${envName}=${raw}`,
8040
+ `ignoring ${envName}=${raw} (expected a positive integer number of milliseconds)`,
8041
+ warn
8042
+ );
8043
+ return { value: fallback, explicit: false };
8044
+ }
8045
+ if (value < min || value > max) {
8046
+ const clamped = Math.min(max, Math.max(min, value));
8047
+ reportOnce(
8048
+ `${envName}=${raw}`,
8049
+ `clamping ${envName}=${raw} to ${clamped}ms (supported range is ${min}-${max}ms)`,
8050
+ warn
8051
+ );
8052
+ return { value: clamped, explicit: true };
8053
+ }
8054
+ return { value, explicit: true };
8055
+ }
8056
+ function maxRetriesForIdleTimeout(idleTimeoutMs) {
8057
+ let retries = 0;
8058
+ let elapsedMs = 0;
8059
+ let delayMs = SDK_INITIAL_RETRY_DELAY_MS;
8060
+ while (elapsedMs + delayMs < idleTimeoutMs) {
8061
+ elapsedMs += delayMs;
8062
+ delayMs *= 2;
8063
+ retries += 1;
8064
+ }
8065
+ return retries;
8066
+ }
8067
+ var MAX_UPSTREAM_MAX_RETRIES = maxRetriesForIdleTimeout(
8068
+ DEFAULT_UPSTREAM_IDLE_TIMEOUT_MS
8069
+ );
8070
+ var DEFAULT_UPSTREAM_MAX_RETRIES = MAX_UPSTREAM_MAX_RETRIES;
8071
+ function configuredUpstreamMaxRetries(env, warn) {
8072
+ const raw = env[UPSTREAM_MAX_RETRIES_ENV]?.trim();
8073
+ if (raw === void 0 || raw === "") return void 0;
8074
+ const value = Number(raw);
8075
+ if (!Number.isInteger(value) || value < 0) {
8076
+ reportOnce(
8077
+ `${UPSTREAM_MAX_RETRIES_ENV}=${raw}`,
8078
+ `ignoring ${UPSTREAM_MAX_RETRIES_ENV}=${raw} (expected a non-negative integer)`,
8079
+ warn
8080
+ );
8081
+ return void 0;
8082
+ }
8083
+ return value;
8084
+ }
8085
+ function resolveUpstreamMaxRetries(env, warn, idleTimeoutMs) {
8086
+ const ceiling = maxRetriesForIdleTimeout(idleTimeoutMs);
8087
+ const defaultRetries = Math.min(DEFAULT_UPSTREAM_MAX_RETRIES, ceiling);
8088
+ const value = configuredUpstreamMaxRetries(env, warn);
8089
+ if (value === void 0) return defaultRetries;
8090
+ if (value > ceiling) {
8091
+ reportOnce(
8092
+ `${UPSTREAM_MAX_RETRIES_ENV}=${value}:idle=${idleTimeoutMs}`,
8093
+ `clamping ${UPSTREAM_MAX_RETRIES_ENV}=${value} to ${ceiling} (estimated from the SDK fallback backoff and resolved ${idleTimeoutMs}ms idle timeout; provider delays may allow fewer retries)`,
8094
+ warn
8095
+ );
8096
+ return ceiling;
8097
+ }
8098
+ return value;
8099
+ }
8100
+ function upstreamRequestBudget(options = {}) {
8101
+ const env = options.env ?? process.env;
8102
+ const warn = options.warn ?? defaultWarn;
8103
+ const hasIdleOverride = options.idleTimeoutMs !== void 0;
8104
+ const configuredIdle = options.idleTimeoutMs !== void 0 ? { value: options.idleTimeoutMs, explicit: false } : timeoutSetting(
8105
+ env,
8106
+ UPSTREAM_IDLE_TIMEOUT_ENV,
8107
+ DEFAULT_UPSTREAM_IDLE_TIMEOUT_MS,
8108
+ MIN_UPSTREAM_IDLE_TIMEOUT_MS,
8109
+ MAX_UPSTREAM_IDLE_TIMEOUT_MS,
8110
+ warn
8111
+ );
8112
+ const configuredTotal = timeoutSetting(
8113
+ env,
8114
+ UPSTREAM_TOTAL_TIMEOUT_ENV,
8115
+ DEFAULT_UPSTREAM_TOTAL_TIMEOUT_MS,
8116
+ MIN_UPSTREAM_TOTAL_TIMEOUT_MS,
8117
+ MAX_UPSTREAM_TOTAL_TIMEOUT_MS,
8118
+ warn
8119
+ );
8120
+ let idleTimeoutMs = configuredIdle.value;
8121
+ let totalTimeoutMs = configuredTotal.value;
8122
+ if (totalTimeoutMs < idleTimeoutMs) {
8123
+ if (hasIdleOverride) {
8124
+ idleTimeoutMs = totalTimeoutMs;
8125
+ } else if (configuredIdle.explicit && !configuredTotal.explicit) {
8126
+ reportOnce(
8127
+ `timeout-pair:raise-total:${idleTimeoutMs}:${totalTimeoutMs}`,
8128
+ `raising the resolved total timeout from ${totalTimeoutMs}ms to ${idleTimeoutMs}ms so it is not shorter than ${UPSTREAM_IDLE_TIMEOUT_ENV}`,
8129
+ warn
8130
+ );
8131
+ totalTimeoutMs = idleTimeoutMs;
8132
+ } else {
8133
+ reportOnce(
8134
+ `timeout-pair:lower-idle:${idleTimeoutMs}:${totalTimeoutMs}`,
8135
+ `lowering the resolved idle timeout from ${idleTimeoutMs}ms to ${totalTimeoutMs}ms because it cannot exceed ${UPSTREAM_TOTAL_TIMEOUT_ENV}`,
8136
+ warn
8137
+ );
8138
+ idleTimeoutMs = totalTimeoutMs;
8139
+ }
8140
+ }
8141
+ return {
8142
+ idleTimeoutMs,
8143
+ totalTimeoutMs,
8144
+ maxRetries: resolveUpstreamMaxRetries(env, warn, idleTimeoutMs)
8145
+ };
8146
+ }
8147
+ var CLIENT_MAX_RETRIES_ENV = "CLAUDE_CODE_MAX_RETRIES";
8148
+ var DEFAULT_PASSTHROUGH_RETRIES = 1;
8149
+ function passthroughUpstreamRetries(env = process.env, warn = defaultWarn) {
8150
+ const explicit = configuredUpstreamMaxRetries(env, warn);
8151
+ if (explicit !== void 0) {
8152
+ if (explicit <= MAX_UPSTREAM_MAX_RETRIES) return explicit;
8153
+ reportOnce(
8154
+ `${UPSTREAM_MAX_RETRIES_ENV}=${explicit}:passthrough`,
8155
+ `clamping ${UPSTREAM_MAX_RETRIES_ENV}=${explicit} to ${MAX_UPSTREAM_MAX_RETRIES} (the raw HTTP MITM path supports at most this many replays)`,
8156
+ warn
8157
+ );
8158
+ return MAX_UPSTREAM_MAX_RETRIES;
8159
+ }
8160
+ const raw = env[CLIENT_MAX_RETRIES_ENV]?.trim();
8161
+ if (raw !== void 0 && raw !== "") {
8162
+ const clientRetries = Number(raw);
8163
+ if (Number.isFinite(clientRetries) && clientRetries === 0) return 0;
8164
+ }
8165
+ return DEFAULT_PASSTHROUGH_RETRIES;
8166
+ }
8167
+
8168
+ // src/oauth/ws-upgrade-pacer.ts
8169
+ var WS_NEW_CONNECTIONS_PER_MIN_ENV = "CLODEX_WS_MAX_NEW_CONNECTIONS_PER_MIN";
8170
+ var DEFAULT_WS_NEW_CONNECTIONS_PER_MIN = 60;
8171
+ var MAX_WS_NEW_CONNECTIONS_PER_MIN = 600;
8172
+ var WS_NEW_CONNECTION_BURST = 10;
8173
+ var WS_NEW_CONNECTION_MAX_WAIT_CEILING_MS = 15e3;
8174
+ function resolvedPacingBudget() {
8175
+ const { idleTimeoutMs, maxRetries } = upstreamRequestBudget();
8176
+ return { idleTimeoutMs, maxRetries };
8177
+ }
8178
+ var SDK_INITIAL_BACKOFF_MS = 2e3;
8179
+ function wsNewConnectionMaxWaitMs(idleTimeoutMs, maxRetries) {
8180
+ if (!Number.isFinite(idleTimeoutMs) || !Number.isFinite(maxRetries) || idleTimeoutMs <= 0 || maxRetries < 0) {
8181
+ return 0;
8182
+ }
8183
+ const attempts = maxRetries + 1;
8184
+ const totalBackoffMs = SDK_INITIAL_BACKOFF_MS * (2 ** maxRetries - 1);
8185
+ const shareableMs = Math.max(0, idleTimeoutMs - totalBackoffMs) / 2;
8186
+ const bound = Math.floor(shareableMs / attempts);
8187
+ return Number.isFinite(bound) ? Math.min(WS_NEW_CONNECTION_MAX_WAIT_CEILING_MS, Math.max(0, bound)) : 0;
8188
+ }
8189
+ function pacedRetryAfterSeconds(requiredWaitMs, maxWaitMs) {
8190
+ const capSeconds = Number.isFinite(maxWaitMs) ? Math.max(1, Math.floor(maxWaitMs / 1e3)) : 1;
8191
+ const wantSeconds = Number.isFinite(requiredWaitMs) ? Math.max(1, Math.ceil(requiredWaitMs / 1e3)) : 1;
8192
+ return Math.min(wantSeconds, capSeconds);
8193
+ }
8194
+ function refusalScheduleMs(maxWaitMs, maxRetries) {
8195
+ if (!Number.isFinite(maxRetries) || maxRetries <= 0) return 0;
8196
+ return maxRetries * pacedRetryAfterSeconds(Number.MAX_SAFE_INTEGER, maxWaitMs) * 1e3;
8197
+ }
8198
+ function canRefuseAtRate(maxWaitMs, maxRetries, ratePerMinute) {
8199
+ if (!Number.isFinite(ratePerMinute) || ratePerMinute <= 0) return false;
8200
+ const refillIntervalMs = 6e4 / ratePerMinute;
8201
+ return refusalScheduleMs(maxWaitMs, maxRetries) >= refillIntervalMs;
8202
+ }
8203
+ function defaultSchedule(ms, fire) {
8204
+ const timer = setTimeout(fire, ms);
8205
+ return () => clearTimeout(timer);
8206
+ }
8207
+ function abortError(signal) {
8208
+ if (signal?.reason instanceof Error) return signal.reason;
8209
+ const error = new Error(
8210
+ typeof signal?.reason === "string" ? signal.reason : "WebSocket connection pacing aborted"
8211
+ );
8212
+ error.name = "AbortError";
8213
+ return error;
8214
+ }
8215
+ var reportedNotices = /* @__PURE__ */ new Set();
8216
+ function reportOnce2(key, message, warn) {
8217
+ if (reportedNotices.has(key)) return;
8218
+ reportedNotices.add(key);
8219
+ try {
8220
+ warn(message);
8221
+ } catch {
8222
+ }
8223
+ }
8224
+ var WsUpgradePacer = class {
8225
+ /** Longest any one request will be queued. Derived; exposed for diagnostics. */
8226
+ maxWaitMs;
8227
+ enabled;
8228
+ refillPerMs;
8229
+ capacity;
8230
+ canRefuse;
8231
+ maxDebt;
8232
+ now;
8233
+ schedule;
8234
+ tokens;
8235
+ lastRefillAt;
8236
+ constructor(options = {}) {
8237
+ const ratePerMinute = options.ratePerMinute ?? DEFAULT_WS_NEW_CONNECTIONS_PER_MIN;
8238
+ const burst = options.burst ?? WS_NEW_CONNECTION_BURST;
8239
+ const budget = options.idleTimeoutMs === void 0 || options.maxRetries === void 0 ? resolvedPacingBudget() : { idleTimeoutMs: options.idleTimeoutMs, maxRetries: options.maxRetries };
8240
+ const idleTimeoutMs = options.idleTimeoutMs ?? budget.idleTimeoutMs;
8241
+ const maxRetries = options.maxRetries ?? budget.maxRetries;
8242
+ this.maxWaitMs = wsNewConnectionMaxWaitMs(idleTimeoutMs, maxRetries);
8243
+ const rateRequested = Number.isFinite(ratePerMinute) && ratePerMinute > 0;
8244
+ this.enabled = rateRequested && this.maxWaitMs > 0;
8245
+ if (rateRequested && !this.enabled) {
8246
+ reportOnce2(
8247
+ `disabled:${maxRetries}:${idleTimeoutMs}`,
8248
+ `not pacing new OpenAI connections: a ${maxRetries}-retry budget leaves no room to queue inside the resolved ${idleTimeoutMs}ms request deadline`,
8249
+ (message) => emitParentNotice(`clodex: ${message}`)
8250
+ );
8251
+ }
8252
+ this.canRefuse = maxRetries > 0 && canRefuseAtRate(this.maxWaitMs, maxRetries, ratePerMinute);
8253
+ if (this.enabled && maxRetries > 0 && !this.canRefuse) {
8254
+ reportOnce2(
8255
+ `norefuse:${ratePerMinute}:${maxRetries}:${this.maxWaitMs}`,
8256
+ `pacing new OpenAI connections at ${ratePerMinute}/minute without refusing overflow: a ${maxRetries}-retry schedule spans only ${refusalScheduleMs(this.maxWaitMs, maxRetries)}ms, which cannot outlast the ${Math.round(6e4 / ratePerMinute)}ms wait for a free connection slot, so clodex shapes the opening burst and then admits remaining overflow instead of failing it`,
8257
+ (message) => emitParentNotice(`clodex: ${message}`)
8258
+ );
8259
+ }
8260
+ this.refillPerMs = this.enabled ? ratePerMinute / 6e4 : 0;
8261
+ this.capacity = Number.isFinite(burst) ? Math.max(1, burst) : WS_NEW_CONNECTION_BURST;
8262
+ this.maxDebt = this.maxWaitMs * this.refillPerMs;
8263
+ this.now = options.now ?? Date.now;
8264
+ this.schedule = options.schedule ?? defaultSchedule;
8265
+ this.tokens = this.capacity;
8266
+ this.lastRefillAt = this.now();
8267
+ }
8268
+ /**
8269
+ * Resolves when this request may open a new connection, or resolves to a
8270
+ * refusal the caller must report as a retryable rate limit. Callers that
8271
+ * reuse an existing connection must not call this at all.
8272
+ */
8273
+ async admit(signal) {
8274
+ if (!this.enabled) return { kind: "admitted", waitedMs: 0 };
8275
+ if (signal?.aborted) throw abortError(signal);
8276
+ const reservation = this.reserve(this.now());
8277
+ if (!reservation.admitted) {
8278
+ return {
8279
+ kind: "refused",
8280
+ requiredWaitMs: reservation.requiredWaitMs,
8281
+ // Capped at the bound: the SDK substitutes this hint for its own
8282
+ // backoff rung, so an uncapped one would overrun the deadline the
8283
+ // bound was derived to fit. See `pacedRetryAfterSeconds`.
8284
+ retryAfterSeconds: clampRetryAfterSeconds(
8285
+ pacedRetryAfterSeconds(reservation.requiredWaitMs, this.maxWaitMs)
8286
+ )
8287
+ };
8288
+ }
8289
+ if (reservation.waitMs <= 0) return { kind: "admitted", waitedMs: 0 };
8290
+ const startedAt = this.now();
8291
+ try {
8292
+ await this.sleep(reservation.waitMs, signal);
8293
+ } catch (error) {
8294
+ this.refund(reservation.consumed);
8295
+ throw error;
8296
+ }
8297
+ return { kind: "admitted", waitedMs: Math.max(0, this.now() - startedAt) };
8298
+ }
8299
+ reserve(now) {
8300
+ const elapsed = Math.max(0, now - this.lastRefillAt);
8301
+ this.tokens = Math.min(this.capacity, this.tokens + elapsed * this.refillPerMs);
8302
+ this.lastRefillAt = now;
8303
+ const waitMs = this.tokens >= 1 ? 0 : Math.ceil((1 - this.tokens) / this.refillPerMs);
8304
+ if (waitMs > this.maxWaitMs) {
8305
+ if (this.canRefuse) return { admitted: false, requiredWaitMs: waitMs };
8306
+ const before = this.tokens;
8307
+ this.tokens = Math.max(-this.maxDebt, this.tokens - 1);
8308
+ const consumed = before - this.tokens;
8309
+ return { admitted: true, waitMs: consumed > 0 ? this.maxWaitMs : 0, consumed };
8310
+ }
8311
+ this.tokens -= 1;
8312
+ return { admitted: true, waitMs, consumed: 1 };
8313
+ }
8314
+ refund(consumed) {
8315
+ this.tokens = Math.min(this.capacity, this.tokens + consumed);
8316
+ }
8317
+ /**
8318
+ * INVARIANT REQUIRED OF FUTURE EDITS: `admit` must reject an aborted signal
8319
+ * before reaching here, and nothing may be awaited between that check and
8320
+ * this call. An abort arriving in such a window would leave the request
8321
+ * parked on a listener an already-aborted signal never fires, and it would
8322
+ * wait out the full duration. There is deliberately no second check here to
8323
+ * catch that, because an untested guard is not a guarantee.
8324
+ */
8325
+ sleep(ms, signal) {
8326
+ return new Promise((resolve3, reject) => {
8327
+ let settled = false;
8328
+ let cancelTimer;
8329
+ const onAbort = () => {
8330
+ if (settled) return;
8331
+ settled = true;
8332
+ cancelTimer?.();
8333
+ reject(abortError(signal));
8334
+ };
8335
+ const fire = () => {
8336
+ if (settled) return;
8337
+ settled = true;
8338
+ signal?.removeEventListener("abort", onAbort);
8339
+ resolve3();
8340
+ };
8341
+ signal?.addEventListener("abort", onAbort, { once: true });
8342
+ cancelTimer = this.schedule(ms, fire);
8343
+ if (settled) cancelTimer();
8344
+ });
8345
+ }
8346
+ };
8347
+ function wsNewConnectionsPerMinute(env = process.env, warn = (message) => emitParentNotice(`clodex: ${message}`)) {
8348
+ const raw = env[WS_NEW_CONNECTIONS_PER_MIN_ENV]?.trim();
8349
+ if (raw === void 0 || raw === "") return DEFAULT_WS_NEW_CONNECTIONS_PER_MIN;
8350
+ const value = Number(raw);
8351
+ if (!Number.isInteger(value) || value < 0) {
8352
+ reportOnce2(
8353
+ `rate:${raw}`,
8354
+ `ignoring ${WS_NEW_CONNECTIONS_PER_MIN_ENV}=${raw} (expected a non-negative integer; using ${DEFAULT_WS_NEW_CONNECTIONS_PER_MIN})`,
8355
+ warn
8356
+ );
8357
+ return DEFAULT_WS_NEW_CONNECTIONS_PER_MIN;
8358
+ }
8359
+ if (value > MAX_WS_NEW_CONNECTIONS_PER_MIN) {
8360
+ reportOnce2(
8361
+ `rate:${raw}`,
8362
+ `clamping ${WS_NEW_CONNECTIONS_PER_MIN_ENV}=${raw} to ${MAX_WS_NEW_CONNECTIONS_PER_MIN} (a higher rate shapes nothing OpenAI throttles on)`,
8363
+ warn
8364
+ );
8365
+ return MAX_WS_NEW_CONNECTIONS_PER_MIN;
8366
+ }
8367
+ return value;
8368
+ }
8369
+ var sharedPacer;
8370
+ function sharedWsUpgradePacer() {
8371
+ sharedPacer ??= new WsUpgradePacer({ ratePerMinute: wsNewConnectionsPerMinute() });
8372
+ return sharedPacer;
8373
+ }
8374
+
8012
8375
  // src/oauth/responses-websocket.ts
8013
8376
  var RESPONSES_LITE_HEADER = "x-openai-internal-codex-responses-lite";
8014
8377
  var TERMINAL_EVENT_TYPES = /* @__PURE__ */ new Set(["response.completed", "response.failed", "response.incomplete"]);
@@ -9129,6 +9492,34 @@ function handleSocketMessage(entry, data) {
9129
9492
  closeContext(ctx);
9130
9493
  }
9131
9494
  }
9495
+ function pacedRefusalResponse(retryAfterSeconds) {
9496
+ const frame = {
9497
+ type: "error",
9498
+ sequence_number: 0,
9499
+ error: {
9500
+ type: anthropicErrorType(429),
9501
+ code: "429",
9502
+ message: `clodex is limiting how fast it opens new OpenAI connections to reduce the chance of an upstream rate limit; retry after ${retryAfterSeconds}s`,
9503
+ param: null,
9504
+ retry_after_seconds: retryAfterSeconds
9505
+ }
9506
+ };
9507
+ return new Response(`data: ${JSON.stringify(frame)}
9508
+
9509
+ `, {
9510
+ status: 200,
9511
+ headers: {
9512
+ "content-type": "text/event-stream; charset=utf-8",
9513
+ // A real header, not just the prose: `getRetryDelayInMs` reads headers and
9514
+ // ignores the body, so without one the SDK falls back to its fixed 2s/4s
9515
+ // ladder. This does NOT de-correlate the group — refusals debit nothing,
9516
+ // so everyone refused at the same instant sees the same deficit and gets
9517
+ // the same hint — it defers the whole group by long enough for the bucket
9518
+ // to refill, which is what turns a retry storm into a successful retry.
9519
+ "retry-after": String(retryAfterSeconds)
9520
+ }
9521
+ });
9522
+ }
9132
9523
  function numericRetryAfterHeader(value) {
9133
9524
  const single = Array.isArray(value) ? value[0] : value;
9134
9525
  return typeof single === "string" && /^\d+$/.test(single.trim()) ? Number(single.trim()) : void 0;
@@ -9270,7 +9661,7 @@ function createResponsesWebSocketFetch(wsUrl, log12, options = {}) {
9270
9661
  const promptFieldHashes = responsesWebSocketPromptFieldHashes(payload);
9271
9662
  const instructionsSnapshot = instructionsFromPayload(payload);
9272
9663
  const diagnosticCorrelation = diagnosticContext.getStore();
9273
- const now = resolvedOptions.now();
9664
+ let now = resolvedOptions.now();
9274
9665
  const evictions = cleanupExpiredConnections(now);
9275
9666
  const candidates = partitionKey ? connectionEntries(partitionKey) : [];
9276
9667
  const idleCandidates = candidates.filter((entry) => !entry.inFlight);
@@ -9341,6 +9732,53 @@ function createResponsesWebSocketFetch(wsUrl, log12, options = {}) {
9341
9732
  } else {
9342
9733
  decision = "unpartitioned_socket";
9343
9734
  }
9735
+ let pacingWaitedMs;
9736
+ if (!selected) {
9737
+ const pacer = options.pacer ?? sharedWsUpgradePacer();
9738
+ const pacingStartedAt = resolvedOptions.now();
9739
+ let admission;
9740
+ try {
9741
+ admission = await pacer.admit(init?.signal ?? void 0);
9742
+ } catch (error) {
9743
+ emitDiagnostic(options, {
9744
+ event: "ws_new_connection_paced",
9745
+ outcome: "aborted",
9746
+ decision,
9747
+ waitedMs: Math.max(0, resolvedOptions.now() - pacingStartedAt)
9748
+ }, diagnosticCorrelation);
9749
+ throw error;
9750
+ }
9751
+ if (admission.kind === "refused") {
9752
+ debug(
9753
+ `refused a new connection to hold the pacing rate; retry after ${admission.retryAfterSeconds}s`
9754
+ );
9755
+ emitDiagnostic(options, {
9756
+ event: "ws_new_connection_paced",
9757
+ outcome: "refused",
9758
+ decision,
9759
+ requiredWaitMs: admission.requiredWaitMs,
9760
+ retryAfterSeconds: admission.retryAfterSeconds
9761
+ }, diagnosticCorrelation);
9762
+ return pacedRefusalResponse(admission.retryAfterSeconds);
9763
+ }
9764
+ if (admission.waitedMs > 0) {
9765
+ pacingWaitedMs = admission.waitedMs;
9766
+ debug(`paced new connection by ${admission.waitedMs}ms`);
9767
+ emitDiagnostic(options, {
9768
+ event: "ws_new_connection_paced",
9769
+ outcome: "admitted",
9770
+ decision,
9771
+ waitedMs: admission.waitedMs
9772
+ }, diagnosticCorrelation);
9773
+ }
9774
+ now = resolvedOptions.now();
9775
+ evictions.push(...cleanupExpiredConnections(now));
9776
+ if (persistent && partitionKey && connectionEntries(partitionKey).some((entry) => entry.inFlight)) {
9777
+ persistent = false;
9778
+ decision = "parallel_isolated";
9779
+ debug("parallel request using an isolated socket after pacing");
9780
+ }
9781
+ }
9344
9782
  if (!selected && persistent) {
9345
9783
  evictions.push(...evictOldestIdleGeneration(
9346
9784
  "nursery",
@@ -9382,6 +9820,7 @@ function createResponsesWebSocketFetch(wsUrl, log12, options = {}) {
9382
9820
  continuationMatchMode: selectedMatch?.mode,
9383
9821
  promotedConnectionId,
9384
9822
  createdConnectionId: selected ? void 0 : nextConnectionDebugId,
9823
+ ...pacingWaitedMs !== void 0 ? { pacingWaitedMs } : {},
9385
9824
  createdGeneration: selected ? void 0 : persistent ? "nursery" : "isolated",
9386
9825
  incrementalInputItems: selectedDelta?.length,
9387
9826
  heads: candidates.map((entry) => ({
@@ -10926,162 +11365,6 @@ function resolveUpstreamTools(tools, messages) {
10926
11365
  return upstream;
10927
11366
  }
10928
11367
 
10929
- // src/upstream-retry.ts
10930
- var UPSTREAM_MAX_RETRIES_ENV = "CLODEX_UPSTREAM_MAX_RETRIES";
10931
- var UPSTREAM_IDLE_TIMEOUT_ENV = "CLODEX_UPSTREAM_IDLE_TIMEOUT_MS";
10932
- var UPSTREAM_TOTAL_TIMEOUT_ENV = "CLODEX_UPSTREAM_TOTAL_TIMEOUT_MS";
10933
- var DEFAULT_UPSTREAM_IDLE_TIMEOUT_MS = 12e4;
10934
- var DEFAULT_UPSTREAM_TOTAL_TIMEOUT_MS = 10 * 6e4;
10935
- var MIN_UPSTREAM_IDLE_TIMEOUT_MS = 1e4;
10936
- var MAX_UPSTREAM_IDLE_TIMEOUT_MS = 60 * 6e4;
10937
- var MIN_UPSTREAM_TOTAL_TIMEOUT_MS = 6e4;
10938
- var MAX_UPSTREAM_TOTAL_TIMEOUT_MS = 6 * 60 * 6e4;
10939
- var SDK_INITIAL_RETRY_DELAY_MS = 2e3;
10940
- var reportedValues = /* @__PURE__ */ new Set();
10941
- var defaultWarn = (message) => emitParentNotice(`clodex: ${message}`);
10942
- function reportOnce(key, message, warn) {
10943
- if (reportedValues.has(key)) return;
10944
- reportedValues.add(key);
10945
- try {
10946
- warn(message);
10947
- } catch {
10948
- }
10949
- }
10950
- function timeoutSetting(env, envName, fallback, min, max, warn) {
10951
- const raw = env[envName]?.trim();
10952
- if (raw === void 0 || raw === "") return { value: fallback, explicit: false };
10953
- const value = Number(raw);
10954
- if (!Number.isInteger(value) || value <= 0) {
10955
- reportOnce(
10956
- `${envName}=${raw}`,
10957
- `ignoring ${envName}=${raw} (expected a positive integer number of milliseconds)`,
10958
- warn
10959
- );
10960
- return { value: fallback, explicit: false };
10961
- }
10962
- if (value < min || value > max) {
10963
- const clamped = Math.min(max, Math.max(min, value));
10964
- reportOnce(
10965
- `${envName}=${raw}`,
10966
- `clamping ${envName}=${raw} to ${clamped}ms (supported range is ${min}-${max}ms)`,
10967
- warn
10968
- );
10969
- return { value: clamped, explicit: true };
10970
- }
10971
- return { value, explicit: true };
10972
- }
10973
- function maxRetriesForIdleTimeout(idleTimeoutMs) {
10974
- let retries = 0;
10975
- let elapsedMs = 0;
10976
- let delayMs = SDK_INITIAL_RETRY_DELAY_MS;
10977
- while (elapsedMs + delayMs < idleTimeoutMs) {
10978
- elapsedMs += delayMs;
10979
- delayMs *= 2;
10980
- retries += 1;
10981
- }
10982
- return retries;
10983
- }
10984
- var MAX_UPSTREAM_MAX_RETRIES = maxRetriesForIdleTimeout(
10985
- DEFAULT_UPSTREAM_IDLE_TIMEOUT_MS
10986
- );
10987
- var DEFAULT_UPSTREAM_MAX_RETRIES = MAX_UPSTREAM_MAX_RETRIES;
10988
- function configuredUpstreamMaxRetries(env, warn) {
10989
- const raw = env[UPSTREAM_MAX_RETRIES_ENV]?.trim();
10990
- if (raw === void 0 || raw === "") return void 0;
10991
- const value = Number(raw);
10992
- if (!Number.isInteger(value) || value < 0) {
10993
- reportOnce(
10994
- `${UPSTREAM_MAX_RETRIES_ENV}=${raw}`,
10995
- `ignoring ${UPSTREAM_MAX_RETRIES_ENV}=${raw} (expected a non-negative integer)`,
10996
- warn
10997
- );
10998
- return void 0;
10999
- }
11000
- return value;
11001
- }
11002
- function resolveUpstreamMaxRetries(env, warn, idleTimeoutMs) {
11003
- const ceiling = maxRetriesForIdleTimeout(idleTimeoutMs);
11004
- const defaultRetries = Math.min(DEFAULT_UPSTREAM_MAX_RETRIES, ceiling);
11005
- const value = configuredUpstreamMaxRetries(env, warn);
11006
- if (value === void 0) return defaultRetries;
11007
- if (value > ceiling) {
11008
- reportOnce(
11009
- `${UPSTREAM_MAX_RETRIES_ENV}=${value}:idle=${idleTimeoutMs}`,
11010
- `clamping ${UPSTREAM_MAX_RETRIES_ENV}=${value} to ${ceiling} (estimated from the SDK fallback backoff and resolved ${idleTimeoutMs}ms idle timeout; provider delays may allow fewer retries)`,
11011
- warn
11012
- );
11013
- return ceiling;
11014
- }
11015
- return value;
11016
- }
11017
- function upstreamRequestBudget(options = {}) {
11018
- const env = options.env ?? process.env;
11019
- const warn = options.warn ?? defaultWarn;
11020
- const hasIdleOverride = options.idleTimeoutMs !== void 0;
11021
- const configuredIdle = options.idleTimeoutMs !== void 0 ? { value: options.idleTimeoutMs, explicit: false } : timeoutSetting(
11022
- env,
11023
- UPSTREAM_IDLE_TIMEOUT_ENV,
11024
- DEFAULT_UPSTREAM_IDLE_TIMEOUT_MS,
11025
- MIN_UPSTREAM_IDLE_TIMEOUT_MS,
11026
- MAX_UPSTREAM_IDLE_TIMEOUT_MS,
11027
- warn
11028
- );
11029
- const configuredTotal = timeoutSetting(
11030
- env,
11031
- UPSTREAM_TOTAL_TIMEOUT_ENV,
11032
- DEFAULT_UPSTREAM_TOTAL_TIMEOUT_MS,
11033
- MIN_UPSTREAM_TOTAL_TIMEOUT_MS,
11034
- MAX_UPSTREAM_TOTAL_TIMEOUT_MS,
11035
- warn
11036
- );
11037
- let idleTimeoutMs = configuredIdle.value;
11038
- let totalTimeoutMs = configuredTotal.value;
11039
- if (totalTimeoutMs < idleTimeoutMs) {
11040
- if (hasIdleOverride) {
11041
- idleTimeoutMs = totalTimeoutMs;
11042
- } else if (configuredIdle.explicit && !configuredTotal.explicit) {
11043
- reportOnce(
11044
- `timeout-pair:raise-total:${idleTimeoutMs}:${totalTimeoutMs}`,
11045
- `raising the resolved total timeout from ${totalTimeoutMs}ms to ${idleTimeoutMs}ms so it is not shorter than ${UPSTREAM_IDLE_TIMEOUT_ENV}`,
11046
- warn
11047
- );
11048
- totalTimeoutMs = idleTimeoutMs;
11049
- } else {
11050
- reportOnce(
11051
- `timeout-pair:lower-idle:${idleTimeoutMs}:${totalTimeoutMs}`,
11052
- `lowering the resolved idle timeout from ${idleTimeoutMs}ms to ${totalTimeoutMs}ms because it cannot exceed ${UPSTREAM_TOTAL_TIMEOUT_ENV}`,
11053
- warn
11054
- );
11055
- idleTimeoutMs = totalTimeoutMs;
11056
- }
11057
- }
11058
- return {
11059
- idleTimeoutMs,
11060
- totalTimeoutMs,
11061
- maxRetries: resolveUpstreamMaxRetries(env, warn, idleTimeoutMs)
11062
- };
11063
- }
11064
- var CLIENT_MAX_RETRIES_ENV = "CLAUDE_CODE_MAX_RETRIES";
11065
- var DEFAULT_PASSTHROUGH_RETRIES = 1;
11066
- function passthroughUpstreamRetries(env = process.env, warn = defaultWarn) {
11067
- const explicit = configuredUpstreamMaxRetries(env, warn);
11068
- if (explicit !== void 0) {
11069
- if (explicit <= MAX_UPSTREAM_MAX_RETRIES) return explicit;
11070
- reportOnce(
11071
- `${UPSTREAM_MAX_RETRIES_ENV}=${explicit}:passthrough`,
11072
- `clamping ${UPSTREAM_MAX_RETRIES_ENV}=${explicit} to ${MAX_UPSTREAM_MAX_RETRIES} (the raw HTTP MITM path supports at most this many replays)`,
11073
- warn
11074
- );
11075
- return MAX_UPSTREAM_MAX_RETRIES;
11076
- }
11077
- const raw = env[CLIENT_MAX_RETRIES_ENV]?.trim();
11078
- if (raw !== void 0 && raw !== "") {
11079
- const clientRetries = Number(raw);
11080
- if (Number.isFinite(clientRetries) && clientRetries === 0) return 0;
11081
- }
11082
- return DEFAULT_PASSTHROUGH_RETRIES;
11083
- }
11084
-
11085
11368
  // src/upstream-attempts.ts
11086
11369
  import { RetryError as RetryError2, wrapLanguageModel as wrapLanguageModel2 } from "ai";
11087
11370
  function trackUpstreamAttempts(model) {
@@ -11125,6 +11408,12 @@ function trackUpstreamAttempts(model) {
11125
11408
  };
11126
11409
  }
11127
11410
 
11411
+ // src/claude-code-compact-prompt.ts
11412
+ var CLAUDE_CODE_COMPACT_PROMPT_MARKERS = Object.freeze({
11413
+ start: "CRITICAL: Respond with TEXT ONLY. Do NOT call any tools.",
11414
+ end: "REMINDER: Do NOT call any tools. Respond with plain text only"
11415
+ });
11416
+
11128
11417
  // src/sdk-adapter.ts
11129
11418
  function sdkTranslationErrorSignature(error) {
11130
11419
  const message = error instanceof Error ? error.message : typeof error === "string" ? error : void 0;
@@ -11393,22 +11682,66 @@ function translateToolChoice(tc) {
11393
11682
  if (tc.type === "tool" && tc.name) return { type: "tool", toolName: tc.name };
11394
11683
  return void 0;
11395
11684
  }
11396
- var COMPACT_TEXT_ONLY_START = "CRITICAL: Respond with TEXT ONLY. Do NOT call any tools.";
11397
- var COMPACT_TEXT_ONLY_END = "REMINDER: Do NOT call any tools. Respond with plain text only";
11398
- function isClaudeCodeStructuredOutputCompactRequest(body) {
11685
+ var {
11686
+ start: COMPACT_TEXT_ONLY_START,
11687
+ end: COMPACT_TEXT_ONLY_END
11688
+ } = CLAUDE_CODE_COMPACT_PROMPT_MARKERS;
11689
+ function isClaudeCodeCompactRequest(body) {
11690
+ if (body.diagnostics !== void 0) return false;
11691
+ const finalMessage = body.messages.at(-1);
11692
+ if (!finalMessage || finalMessage.role !== "user") return false;
11693
+ const texts = typeof finalMessage.content === "string" ? [finalMessage.content] : finalMessage.content.filter((block) => block.type === "text").map((block) => block.text ?? "");
11694
+ return texts.some((text5) => text5.startsWith(COMPACT_TEXT_ONLY_START) && text5.includes(COMPACT_TEXT_ONLY_END));
11695
+ }
11696
+ var COMPACT_DRIFT_ANCHOR = "Tool calls will be REJECTED and will waste your only turn";
11697
+ var COMPACT_DRIFT_ANCHOR_LINE = new RegExp(
11698
+ `(?:^|\\n)[-*\u2022\\s]{0,4}${COMPACT_DRIFT_ANCHOR}`
11699
+ );
11700
+ var COMPACT_DRIFT_OPENING = /^(?:(?:critical|important|warning|caution|urgent|notice):\s*)?(?:respond|return|answer|output|write|provide)\b(?=[^\n]{1,160}(?:\n|$))(?=[^\n]*\b(?:text\s+only|plain\s+text\s+only|only\s+(?:plain\s+)?text)\b)(?=[^\n]*\b(?:do not|don't|never|without)\b[^\n]{0,48}\btools?\b)/i;
11701
+ function looksLikeDriftedClaudeCodeCompactRequest(body) {
11399
11702
  if (body.diagnostics !== void 0) return false;
11400
- if (!body.tools?.some((candidate) => candidate.name === "StructuredOutput")) return false;
11401
11703
  const finalMessage = body.messages.at(-1);
11402
11704
  if (!finalMessage || finalMessage.role !== "user") return false;
11403
- const text5 = typeof finalMessage.content === "string" ? finalMessage.content : finalMessage.content.filter((block) => block.type === "text").map((block) => block.text ?? "").join("\n");
11404
- return text5.includes(COMPACT_TEXT_ONLY_START) && text5.includes(COMPACT_TEXT_ONLY_END);
11705
+ const texts = typeof finalMessage.content === "string" ? [finalMessage.content] : finalMessage.content.filter((block) => block.type === "text").map((block) => block.text ?? "");
11706
+ return texts.some((text5) => !text5.startsWith(COMPACT_TEXT_ONLY_START) && COMPACT_DRIFT_OPENING.test(text5) && COMPACT_DRIFT_ANCHOR_LINE.test(text5));
11707
+ }
11708
+ function claudeCodeVersionFromRequest(body) {
11709
+ const texts = typeof body.system === "string" ? [body.system] : (body.system ?? []).map((block) => typeof block === "string" ? block : block.text ?? "");
11710
+ for (const text5 of texts) {
11711
+ if (!text5.startsWith(CLAUDE_CODE_BILLING_HEADER_PREFIX)) continue;
11712
+ const match = text5.match(/\bcc_version=([0-9A-Za-z][0-9A-Za-z._+-]{0,63})(?:;|\s|$)/);
11713
+ if (match) return match[1];
11714
+ }
11715
+ return void 0;
11716
+ }
11717
+ var warnedCompactPromptDrifts = /* @__PURE__ */ new Set();
11718
+ var MAX_COMPACT_PROMPT_DRIFT_WARNINGS = 3;
11719
+ function reportClaudeCodeCompactPromptDrift(body, log12) {
11720
+ if (!looksLikeDriftedClaudeCodeCompactRequest(body)) return;
11721
+ const version = claudeCodeVersionFromRequest(body);
11722
+ const signature = version ?? "unknown-version";
11723
+ try {
11724
+ log12?.(`possible Claude Code compact prompt drift: ${signature}`);
11725
+ } catch {
11726
+ }
11727
+ if (warnedCompactPromptDrifts.has(signature)) return;
11728
+ if (warnedCompactPromptDrifts.size >= MAX_COMPACT_PROMPT_DRIFT_WARNINGS) return;
11729
+ warnedCompactPromptDrifts.add(signature);
11730
+ const versionText = version ? ` from Claude Code ${version}` : "";
11731
+ emitParentNotice(
11732
+ `clodex: warning: a request${versionText} looks like a compaction turn, but its prompt no longer matches clodex's text-only guard. Tools were left enabled and compaction may fail. Please report this at https://github.com/bman654/clodex/issues`
11733
+ );
11734
+ if (warnedCompactPromptDrifts.size === MAX_COMPACT_PROMPT_DRIFT_WARNINGS) {
11735
+ emitParentNotice("clodex: warning: further compact-prompt drift warnings suppressed.");
11736
+ }
11405
11737
  }
11406
11738
  function translateRequest(body, npm, options) {
11407
11739
  const messages = body.messages ?? [];
11408
11740
  annotateToolNames(messages);
11409
11741
  const baseSystem = systemToString(body.system, true);
11410
11742
  const systemText = baseSystem?.trim() || (options?.openAiOAuth ? "You are a coding assistant." : void 0);
11411
- const compactRequest = isClaudeCodeStructuredOutputCompactRequest(body);
11743
+ const compactRequest = isClaudeCodeCompactRequest(body);
11744
+ if (!compactRequest) reportClaudeCodeCompactPromptDrift(body, options?.log);
11412
11745
  let upstreamTools = resolveUpstreamTools(
11413
11746
  body.tools,
11414
11747
  messages
@@ -12318,6 +12651,7 @@ async function startProxyCatalog(routes, defaultAliasId, debug = false, inferenc
12318
12651
  openAiOAuth,
12319
12652
  claudeSessionId,
12320
12653
  maxTools: maxToolsForNpm(route.npm),
12654
+ log: plog,
12321
12655
  reasoningMetadata: {
12322
12656
  providerId: route.providerId,
12323
12657
  apiBaseUrl: route.baseURL,
@@ -17246,7 +17580,8 @@ async function handleAnthropicMessages(req, res, options, modelCache, plog) {
17246
17580
  compatibility: model.compatibility,
17247
17581
  upstreamModelId: upstreamModelId(model)
17248
17582
  },
17249
- maxTools: npmMaxTools
17583
+ maxTools: npmMaxTools,
17584
+ log: plog
17250
17585
  });
17251
17586
  const clientWantsStream = Boolean(body.stream);
17252
17587
  const responseModelId = getResponseModelId(body.model, model, options);