@bman654/clodex 2.8.5 → 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/README.md +93 -16
- package/dist/cli.js +714 -157
- package/dist/cli.js.map +1 -1
- package/docs/background-agents.md +4 -0
- package/package.json +1 -1
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.
|
|
385
|
+
version: "2.10.0",
|
|
386
386
|
publishConfig: {
|
|
387
387
|
access: "public"
|
|
388
388
|
},
|
|
@@ -3423,7 +3423,7 @@ function savedStopsAfter(current, assignments) {
|
|
|
3423
3423
|
}
|
|
3424
3424
|
|
|
3425
3425
|
// src/patch-transforms.ts
|
|
3426
|
-
var PATCH_TRANSFORMS_VERSION =
|
|
3426
|
+
var PATCH_TRANSFORMS_VERSION = 12;
|
|
3427
3427
|
var NATIVE_EFFORT_LEVELS = ["low", "medium", "high", "xhigh", "max"];
|
|
3428
3428
|
var BASE_EFFORT_LEVELS = ["low", "medium", "high"];
|
|
3429
3429
|
function projectNativeEffort(effort) {
|
|
@@ -3812,7 +3812,7 @@ function applyClodexPatches(source, config) {
|
|
|
3812
3812
|
}
|
|
3813
3813
|
applyOnce(
|
|
3814
3814
|
patchName,
|
|
3815
|
-
/(function [\w$]+\(\)\{)(let (?:[^;{}]|\{[^;{}]*\})*?[\w$]+\(process\.env\.CLAUDE_CODE_REMOTE\)\?(?:(?!\}\s*function )[\s\S])*?\)return process\.env;let ([\w$]+)=\{(?:(?!\}\s*function )[\s\S])*?return \3)(\})/,
|
|
3815
|
+
/(function [\w$]+\(\)\{)(let[ {[](?:[^;{}]|\{[^;{}]*\})*?(?:\{[^;{}]*)?(?:getAgentProxyEnv|[\w$]+\(process\.env\.CLAUDE_CODE_REMOTE\)\?)(?:(?!\}\s*function )[\s\S])*?\)return process\.env;let ([\w$]+)=\{(?:(?!\}\s*function )[\s\S])*?return \3)(\})/,
|
|
3816
3816
|
(match, head, body, _copyVar, tail) => {
|
|
3817
3817
|
const at = js.indexOf(match);
|
|
3818
3818
|
const closingBrace = at < 0 ? -1 : blockEndIndex(js, at + head.length - 1);
|
|
@@ -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
|
-
|
|
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,53 +11365,55 @@ function resolveUpstreamTools(tools, messages) {
|
|
|
10926
11365
|
return upstream;
|
|
10927
11366
|
}
|
|
10928
11367
|
|
|
10929
|
-
// src/upstream-
|
|
10930
|
-
|
|
10931
|
-
|
|
10932
|
-
|
|
10933
|
-
|
|
10934
|
-
|
|
10935
|
-
|
|
10936
|
-
|
|
10937
|
-
|
|
10938
|
-
|
|
10939
|
-
|
|
10940
|
-
|
|
10941
|
-
|
|
10942
|
-
|
|
10943
|
-
|
|
10944
|
-
|
|
10945
|
-
|
|
10946
|
-
|
|
10947
|
-
|
|
10948
|
-
|
|
10949
|
-
|
|
10950
|
-
|
|
10951
|
-
|
|
10952
|
-
|
|
10953
|
-
|
|
10954
|
-
|
|
10955
|
-
|
|
10956
|
-
|
|
10957
|
-
|
|
10958
|
-
|
|
10959
|
-
|
|
10960
|
-
|
|
10961
|
-
|
|
10962
|
-
|
|
10963
|
-
|
|
10964
|
-
|
|
10965
|
-
|
|
10966
|
-
|
|
10967
|
-
|
|
10968
|
-
|
|
10969
|
-
|
|
10970
|
-
const clientRetries = Number(raw);
|
|
10971
|
-
if (Number.isFinite(clientRetries) && clientRetries === 0) return 0;
|
|
10972
|
-
}
|
|
10973
|
-
return DEFAULT_PASSTHROUGH_RETRIES;
|
|
11368
|
+
// src/upstream-attempts.ts
|
|
11369
|
+
import { RetryError as RetryError2, wrapLanguageModel as wrapLanguageModel2 } from "ai";
|
|
11370
|
+
function trackUpstreamAttempts(model) {
|
|
11371
|
+
if (typeof model === "string") {
|
|
11372
|
+
return { model, deadlineError: (timeoutError) => timeoutError };
|
|
11373
|
+
}
|
|
11374
|
+
const failedAttempts = [];
|
|
11375
|
+
let waitingToRetry = false;
|
|
11376
|
+
const track = async (call) => {
|
|
11377
|
+
waitingToRetry = false;
|
|
11378
|
+
try {
|
|
11379
|
+
const result = await call();
|
|
11380
|
+
failedAttempts.length = 0;
|
|
11381
|
+
return result;
|
|
11382
|
+
} catch (error) {
|
|
11383
|
+
failedAttempts.push(error);
|
|
11384
|
+
waitingToRetry = true;
|
|
11385
|
+
throw error;
|
|
11386
|
+
}
|
|
11387
|
+
};
|
|
11388
|
+
const middleware = {
|
|
11389
|
+
specificationVersion: "v4",
|
|
11390
|
+
wrapGenerate: ({ doGenerate }) => track(doGenerate),
|
|
11391
|
+
wrapStream: ({ doStream }) => track(doStream)
|
|
11392
|
+
};
|
|
11393
|
+
return {
|
|
11394
|
+
model: wrapLanguageModel2({ model, middleware }),
|
|
11395
|
+
deadlineError: (timeoutError) => {
|
|
11396
|
+
if (!waitingToRetry || failedAttempts.length === 0) return timeoutError;
|
|
11397
|
+
const count = failedAttempts.length;
|
|
11398
|
+
return new RetryError2({
|
|
11399
|
+
message: [
|
|
11400
|
+
"Provider retry interrupted by a request deadline after",
|
|
11401
|
+
count,
|
|
11402
|
+
`failed ${count === 1 ? "attempt" : "attempts"}`
|
|
11403
|
+
].join(" "),
|
|
11404
|
+
reason: "abort",
|
|
11405
|
+
errors: [...failedAttempts]
|
|
11406
|
+
});
|
|
11407
|
+
}
|
|
11408
|
+
};
|
|
10974
11409
|
}
|
|
10975
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
|
+
|
|
10976
11417
|
// src/sdk-adapter.ts
|
|
10977
11418
|
function sdkTranslationErrorSignature(error) {
|
|
10978
11419
|
const message = error instanceof Error ? error.message : typeof error === "string" ? error : void 0;
|
|
@@ -11241,22 +11682,66 @@ function translateToolChoice(tc) {
|
|
|
11241
11682
|
if (tc.type === "tool" && tc.name) return { type: "tool", toolName: tc.name };
|
|
11242
11683
|
return void 0;
|
|
11243
11684
|
}
|
|
11244
|
-
var
|
|
11245
|
-
|
|
11246
|
-
|
|
11685
|
+
var {
|
|
11686
|
+
start: COMPACT_TEXT_ONLY_START,
|
|
11687
|
+
end: COMPACT_TEXT_ONLY_END
|
|
11688
|
+
} = CLAUDE_CODE_COMPACT_PROMPT_MARKERS;
|
|
11689
|
+
function isClaudeCodeCompactRequest(body) {
|
|
11247
11690
|
if (body.diagnostics !== void 0) return false;
|
|
11248
|
-
if (!body.tools?.some((candidate) => candidate.name === "StructuredOutput")) return false;
|
|
11249
11691
|
const finalMessage = body.messages.at(-1);
|
|
11250
11692
|
if (!finalMessage || finalMessage.role !== "user") return false;
|
|
11251
|
-
const
|
|
11252
|
-
return text5.
|
|
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) {
|
|
11702
|
+
if (body.diagnostics !== void 0) return false;
|
|
11703
|
+
const finalMessage = body.messages.at(-1);
|
|
11704
|
+
if (!finalMessage || finalMessage.role !== "user") return false;
|
|
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
|
+
}
|
|
11253
11737
|
}
|
|
11254
11738
|
function translateRequest(body, npm, options) {
|
|
11255
11739
|
const messages = body.messages ?? [];
|
|
11256
11740
|
annotateToolNames(messages);
|
|
11257
11741
|
const baseSystem = systemToString(body.system, true);
|
|
11258
11742
|
const systemText = baseSystem?.trim() || (options?.openAiOAuth ? "You are a coding assistant." : void 0);
|
|
11259
|
-
const compactRequest =
|
|
11743
|
+
const compactRequest = isClaudeCodeCompactRequest(body);
|
|
11744
|
+
if (!compactRequest) reportClaudeCodeCompactPromptDrift(body, options?.log);
|
|
11260
11745
|
let upstreamTools = resolveUpstreamTools(
|
|
11261
11746
|
body.tools,
|
|
11262
11747
|
messages
|
|
@@ -11347,8 +11832,6 @@ function toAnthropicUsage(u) {
|
|
|
11347
11832
|
cache_read_input_tokens: cacheRead
|
|
11348
11833
|
};
|
|
11349
11834
|
}
|
|
11350
|
-
var SDK_STREAM_IDLE_TIMEOUT_MS = 12e4;
|
|
11351
|
-
var SDK_TOTAL_TIMEOUT_MS = 10 * 6e4;
|
|
11352
11835
|
function streamAbortError(signal) {
|
|
11353
11836
|
if (signal?.reason instanceof Error) return signal.reason;
|
|
11354
11837
|
const error = new Error(
|
|
@@ -11569,42 +12052,44 @@ async function writeAnthropicStream(stream, modelId, write, log12, observer, too
|
|
|
11569
12052
|
emit("message_stop", { type: "message_stop" });
|
|
11570
12053
|
}
|
|
11571
12054
|
async function streamAnthropicResponse(model, params, modelId, write, log12, observer) {
|
|
11572
|
-
const idleTimeoutMs
|
|
12055
|
+
const { idleTimeoutMs, totalTimeoutMs, maxRetries } = upstreamRequestBudget({
|
|
12056
|
+
idleTimeoutMs: observer?.idleTimeoutMs
|
|
12057
|
+
});
|
|
12058
|
+
const attempts = trackUpstreamAttempts(model);
|
|
11573
12059
|
const idleAbort = new AbortController();
|
|
11574
12060
|
const stopForwardingAbort = forwardAbortSignal(observer?.abortSignal, idleAbort);
|
|
11575
12061
|
const abortSignal = idleAbort.signal;
|
|
11576
|
-
|
|
11577
|
-
|
|
11578
|
-
idleTimeoutMs
|
|
12062
|
+
const idleError = () => attempts.deadlineError(
|
|
12063
|
+
new Error(`no data received from provider for ${Math.round(idleTimeoutMs / 1e3)}s`)
|
|
11579
12064
|
);
|
|
12065
|
+
let idleTimer = setTimeout(() => idleAbort.abort(idleError()), idleTimeoutMs);
|
|
11580
12066
|
const totalTimer = setTimeout(
|
|
11581
|
-
() => idleAbort.abort(
|
|
11582
|
-
|
|
12067
|
+
() => idleAbort.abort(attempts.deadlineError(
|
|
12068
|
+
new Error(`provider stream exceeded ${Math.round(totalTimeoutMs / 1e3)}s`)
|
|
12069
|
+
)),
|
|
12070
|
+
totalTimeoutMs
|
|
11583
12071
|
);
|
|
11584
|
-
|
|
11585
|
-
|
|
11586
|
-
|
|
11587
|
-
|
|
11588
|
-
|
|
11589
|
-
|
|
11590
|
-
|
|
11591
|
-
|
|
11592
|
-
|
|
11593
|
-
|
|
11594
|
-
|
|
11595
|
-
|
|
12072
|
+
try {
|
|
12073
|
+
const result = streamText({
|
|
12074
|
+
model: attempts.model,
|
|
12075
|
+
...params,
|
|
12076
|
+
maxRetries,
|
|
12077
|
+
abortSignal,
|
|
12078
|
+
onError: () => {
|
|
12079
|
+
},
|
|
12080
|
+
onStepFinish: (step) => reportUnsupportedServiceTier(params, step.warnings)
|
|
12081
|
+
});
|
|
12082
|
+
const watchedStream = (async function* () {
|
|
12083
|
+
try {
|
|
12084
|
+
for await (const part of result.stream) {
|
|
12085
|
+
clearTimeout(idleTimer);
|
|
12086
|
+
idleTimer = setTimeout(() => idleAbort.abort(idleError()), idleTimeoutMs);
|
|
12087
|
+
yield part;
|
|
12088
|
+
}
|
|
12089
|
+
} finally {
|
|
11596
12090
|
clearTimeout(idleTimer);
|
|
11597
|
-
idleTimer = setTimeout(
|
|
11598
|
-
() => idleAbort.abort(new Error(`no data received from provider for ${Math.round(idleTimeoutMs / 1e3)}s`)),
|
|
11599
|
-
idleTimeoutMs
|
|
11600
|
-
);
|
|
11601
|
-
yield part;
|
|
11602
12091
|
}
|
|
11603
|
-
}
|
|
11604
|
-
clearTimeout(idleTimer);
|
|
11605
|
-
}
|
|
11606
|
-
})();
|
|
11607
|
-
try {
|
|
12092
|
+
})();
|
|
11608
12093
|
await writeAnthropicStream(watchedStream, modelId, write, log12, { ...observer, abortSignal }, params.tools);
|
|
11609
12094
|
} finally {
|
|
11610
12095
|
stopForwardingAbort();
|
|
@@ -11619,39 +12104,41 @@ async function generateAnthropicResponse(model, params, modelId, options) {
|
|
|
11619
12104
|
let finishReason;
|
|
11620
12105
|
let usage;
|
|
11621
12106
|
let warnings;
|
|
12107
|
+
const { idleTimeoutMs, totalTimeoutMs, maxRetries } = upstreamRequestBudget({
|
|
12108
|
+
idleTimeoutMs: options?.forceStream ? options.idleTimeoutMs : void 0
|
|
12109
|
+
});
|
|
12110
|
+
const attempts = trackUpstreamAttempts(model);
|
|
11622
12111
|
if (options?.forceStream) {
|
|
11623
12112
|
const forceAbort = new AbortController();
|
|
11624
12113
|
const stopForwardingAbort = forwardAbortSignal(options.abortSignal, forceAbort);
|
|
11625
12114
|
const abortSignal = forceAbort.signal;
|
|
11626
|
-
const
|
|
11627
|
-
|
|
11628
|
-
() => forceAbort.abort(new Error(`no data received from provider for ${Math.round(idleTimeoutMs / 1e3)}s`)),
|
|
11629
|
-
idleTimeoutMs
|
|
12115
|
+
const idleError = () => attempts.deadlineError(
|
|
12116
|
+
new Error(`no data received from provider for ${Math.round(idleTimeoutMs / 1e3)}s`)
|
|
11630
12117
|
);
|
|
12118
|
+
let idleTimer = setTimeout(() => forceAbort.abort(idleError()), idleTimeoutMs);
|
|
11631
12119
|
const totalTimer = setTimeout(
|
|
11632
|
-
() => forceAbort.abort(
|
|
11633
|
-
|
|
12120
|
+
() => forceAbort.abort(attempts.deadlineError(
|
|
12121
|
+
new Error(`provider stream exceeded ${Math.round(totalTimeoutMs / 1e3)}s`)
|
|
12122
|
+
)),
|
|
12123
|
+
totalTimeoutMs
|
|
11634
12124
|
);
|
|
11635
|
-
const r = streamText({
|
|
11636
|
-
model,
|
|
11637
|
-
...params,
|
|
11638
|
-
maxRetries: upstreamMaxRetries(),
|
|
11639
|
-
abortSignal,
|
|
11640
|
-
onError: () => {
|
|
11641
|
-
},
|
|
11642
|
-
onStepFinish: (step) => reportUnsupportedServiceTier(params, step.warnings)
|
|
11643
|
-
});
|
|
11644
12125
|
const streamedText = [];
|
|
11645
12126
|
const streamedToolCalls = [];
|
|
11646
12127
|
let streamedFinishReason = "stop";
|
|
11647
12128
|
let streamedUsage;
|
|
11648
12129
|
try {
|
|
12130
|
+
const r = streamText({
|
|
12131
|
+
model: attempts.model,
|
|
12132
|
+
...params,
|
|
12133
|
+
maxRetries,
|
|
12134
|
+
abortSignal,
|
|
12135
|
+
onError: () => {
|
|
12136
|
+
},
|
|
12137
|
+
onStepFinish: (step) => reportUnsupportedServiceTier(params, step.warnings)
|
|
12138
|
+
});
|
|
11649
12139
|
for await (const part of r.stream) {
|
|
11650
12140
|
clearTimeout(idleTimer);
|
|
11651
|
-
idleTimer = setTimeout(
|
|
11652
|
-
() => forceAbort.abort(new Error(`no data received from provider for ${Math.round(idleTimeoutMs / 1e3)}s`)),
|
|
11653
|
-
idleTimeoutMs
|
|
11654
|
-
);
|
|
12141
|
+
idleTimer = setTimeout(() => forceAbort.abort(idleError()), idleTimeoutMs);
|
|
11655
12142
|
options.onPart?.(part.type);
|
|
11656
12143
|
if (abortSignal.aborted || part.type === "abort") {
|
|
11657
12144
|
throw streamAbortError(abortSignal);
|
|
@@ -11686,17 +12173,22 @@ async function generateAnthropicResponse(model, params, modelId, options) {
|
|
|
11686
12173
|
const generateAbort = new AbortController();
|
|
11687
12174
|
const stopForwardingAbort = forwardAbortSignal(options?.abortSignal, generateAbort);
|
|
11688
12175
|
const totalTimer = setTimeout(
|
|
11689
|
-
() => generateAbort.abort(
|
|
11690
|
-
|
|
12176
|
+
() => generateAbort.abort(attempts.deadlineError(
|
|
12177
|
+
new Error(`provider request exceeded ${Math.round(totalTimeoutMs / 1e3)}s`)
|
|
12178
|
+
)),
|
|
12179
|
+
totalTimeoutMs
|
|
11691
12180
|
);
|
|
11692
12181
|
try {
|
|
11693
12182
|
const r = await generateText({
|
|
11694
|
-
model,
|
|
12183
|
+
model: attempts.model,
|
|
11695
12184
|
...params,
|
|
11696
|
-
maxRetries
|
|
12185
|
+
maxRetries,
|
|
11697
12186
|
abortSignal: generateAbort.signal
|
|
11698
12187
|
});
|
|
11699
12188
|
({ text: text5, toolCalls, finishReason, usage, warnings } = r);
|
|
12189
|
+
} catch (error) {
|
|
12190
|
+
if (generateAbort.signal.aborted) throw streamAbortError(generateAbort.signal);
|
|
12191
|
+
throw error;
|
|
11700
12192
|
} finally {
|
|
11701
12193
|
stopForwardingAbort();
|
|
11702
12194
|
clearTimeout(totalTimer);
|
|
@@ -12159,6 +12651,7 @@ async function startProxyCatalog(routes, defaultAliasId, debug = false, inferenc
|
|
|
12159
12651
|
openAiOAuth,
|
|
12160
12652
|
claudeSessionId,
|
|
12161
12653
|
maxTools: maxToolsForNpm(route.npm),
|
|
12654
|
+
log: plog,
|
|
12162
12655
|
reasoningMetadata: {
|
|
12163
12656
|
providerId: route.providerId,
|
|
12164
12657
|
apiBaseUrl: route.baseURL,
|
|
@@ -16670,24 +17163,81 @@ async function collectOpenAiStream(stream) {
|
|
|
16670
17163
|
}
|
|
16671
17164
|
return collected;
|
|
16672
17165
|
}
|
|
17166
|
+
function startUpstreamBudget(model, streaming) {
|
|
17167
|
+
const { idleTimeoutMs, totalTimeoutMs, maxRetries } = upstreamRequestBudget();
|
|
17168
|
+
const attempts = trackUpstreamAttempts(model);
|
|
17169
|
+
const abort = new AbortController();
|
|
17170
|
+
const idleError = () => attempts.deadlineError(new Error(
|
|
17171
|
+
`no data received from provider for ${Math.round(idleTimeoutMs / 1e3)}s`
|
|
17172
|
+
));
|
|
17173
|
+
let idleTimer = streaming ? setTimeout(() => abort.abort(idleError()), idleTimeoutMs) : void 0;
|
|
17174
|
+
const totalTimer = setTimeout(
|
|
17175
|
+
() => abort.abort(attempts.deadlineError(new Error(
|
|
17176
|
+
`provider ${streaming ? "stream" : "request"} exceeded ${Math.round(totalTimeoutMs / 1e3)}s`
|
|
17177
|
+
))),
|
|
17178
|
+
totalTimeoutMs
|
|
17179
|
+
);
|
|
17180
|
+
return {
|
|
17181
|
+
model: attempts.model,
|
|
17182
|
+
maxRetries,
|
|
17183
|
+
abortSignal: abort.signal,
|
|
17184
|
+
onStreamPart: () => {
|
|
17185
|
+
if (idleTimer === void 0) return;
|
|
17186
|
+
clearTimeout(idleTimer);
|
|
17187
|
+
idleTimer = setTimeout(() => abort.abort(idleError()), idleTimeoutMs);
|
|
17188
|
+
},
|
|
17189
|
+
close: () => {
|
|
17190
|
+
if (idleTimer !== void 0) clearTimeout(idleTimer);
|
|
17191
|
+
clearTimeout(totalTimer);
|
|
17192
|
+
if (!abort.signal.aborted) abort.abort();
|
|
17193
|
+
}
|
|
17194
|
+
};
|
|
17195
|
+
}
|
|
17196
|
+
async function* watchOpenAiStream(stream, budget) {
|
|
17197
|
+
for await (const part of stream) {
|
|
17198
|
+
if (budget.abortSignal.aborted || part.type === "abort") {
|
|
17199
|
+
throw budget.abortSignal.reason instanceof Error ? budget.abortSignal.reason : new Error("SDK stream aborted");
|
|
17200
|
+
}
|
|
17201
|
+
budget.onStreamPart();
|
|
17202
|
+
yield part;
|
|
17203
|
+
}
|
|
17204
|
+
if (budget.abortSignal.aborted) {
|
|
17205
|
+
throw budget.abortSignal.reason instanceof Error ? budget.abortSignal.reason : new Error("SDK stream aborted");
|
|
17206
|
+
}
|
|
17207
|
+
}
|
|
16673
17208
|
async function generateOpenAiResponse(model, params, responseModelId, options) {
|
|
16674
17209
|
let result;
|
|
16675
|
-
|
|
16676
|
-
|
|
16677
|
-
|
|
16678
|
-
|
|
16679
|
-
|
|
16680
|
-
|
|
16681
|
-
|
|
16682
|
-
|
|
16683
|
-
|
|
16684
|
-
|
|
16685
|
-
|
|
16686
|
-
|
|
16687
|
-
|
|
16688
|
-
|
|
16689
|
-
|
|
16690
|
-
|
|
17210
|
+
const streaming = options?.forceStream === true;
|
|
17211
|
+
const budget = startUpstreamBudget(model, streaming);
|
|
17212
|
+
try {
|
|
17213
|
+
if (streaming) {
|
|
17214
|
+
const { stream } = streamText2({
|
|
17215
|
+
model: budget.model,
|
|
17216
|
+
...params,
|
|
17217
|
+
maxRetries: budget.maxRetries,
|
|
17218
|
+
abortSignal: budget.abortSignal,
|
|
17219
|
+
onError: () => {
|
|
17220
|
+
},
|
|
17221
|
+
onStepFinish: (step) => reportUnsupportedServiceTier(params, step.warnings)
|
|
17222
|
+
});
|
|
17223
|
+
result = await collectOpenAiStream(watchOpenAiStream(stream, budget));
|
|
17224
|
+
} else {
|
|
17225
|
+
try {
|
|
17226
|
+
result = await generateText2({
|
|
17227
|
+
model: budget.model,
|
|
17228
|
+
...params,
|
|
17229
|
+
maxRetries: budget.maxRetries,
|
|
17230
|
+
abortSignal: budget.abortSignal
|
|
17231
|
+
});
|
|
17232
|
+
} catch (error) {
|
|
17233
|
+
if (budget.abortSignal.aborted && budget.abortSignal.reason instanceof Error) {
|
|
17234
|
+
throw budget.abortSignal.reason;
|
|
17235
|
+
}
|
|
17236
|
+
throw error;
|
|
17237
|
+
}
|
|
17238
|
+
}
|
|
17239
|
+
} finally {
|
|
17240
|
+
budget.close();
|
|
16691
17241
|
}
|
|
16692
17242
|
reportUnsupportedServiceTier(params, result.warnings);
|
|
16693
17243
|
const message = { role: "assistant", content: result.text || null };
|
|
@@ -16712,41 +17262,47 @@ async function generateOpenAiResponse(model, params, responseModelId, options) {
|
|
|
16712
17262
|
};
|
|
16713
17263
|
}
|
|
16714
17264
|
async function streamOpenAiResponse(model, params, responseModelId, onChunk) {
|
|
16715
|
-
const
|
|
16716
|
-
|
|
16717
|
-
|
|
16718
|
-
|
|
16719
|
-
|
|
16720
|
-
|
|
16721
|
-
|
|
16722
|
-
|
|
16723
|
-
|
|
16724
|
-
|
|
16725
|
-
|
|
16726
|
-
|
|
16727
|
-
|
|
17265
|
+
const budget = startUpstreamBudget(model, true);
|
|
17266
|
+
try {
|
|
17267
|
+
const { stream } = streamText2({
|
|
17268
|
+
model: budget.model,
|
|
17269
|
+
...params,
|
|
17270
|
+
maxRetries: budget.maxRetries,
|
|
17271
|
+
abortSignal: budget.abortSignal,
|
|
17272
|
+
onStepFinish: (step) => reportUnsupportedServiceTier(params, step.warnings)
|
|
17273
|
+
});
|
|
17274
|
+
const baseData = {
|
|
17275
|
+
id: `chatcmpl-${Date.now()}`,
|
|
17276
|
+
object: "chat.completion.chunk",
|
|
17277
|
+
created: Math.floor(Date.now() / 1e3),
|
|
17278
|
+
model: responseModelId
|
|
17279
|
+
};
|
|
17280
|
+
const send = (delta, finish_reason = null) => onChunk(`data: ${JSON.stringify({ ...baseData, choices: [{ index: 0, delta, finish_reason }] })}
|
|
16728
17281
|
|
|
16729
17282
|
`);
|
|
16730
|
-
|
|
16731
|
-
|
|
16732
|
-
|
|
16733
|
-
|
|
16734
|
-
|
|
16735
|
-
|
|
16736
|
-
|
|
16737
|
-
|
|
16738
|
-
|
|
16739
|
-
|
|
16740
|
-
|
|
16741
|
-
|
|
16742
|
-
|
|
16743
|
-
|
|
16744
|
-
|
|
16745
|
-
|
|
16746
|
-
|
|
17283
|
+
for await (const part of watchOpenAiStream(stream, budget)) {
|
|
17284
|
+
const p13 = part;
|
|
17285
|
+
switch (p13.type) {
|
|
17286
|
+
case "text-delta":
|
|
17287
|
+
send({ role: "assistant", content: p13.textDelta ?? p13.text ?? "" });
|
|
17288
|
+
break;
|
|
17289
|
+
case "tool-input-start":
|
|
17290
|
+
send({ role: "assistant", tool_calls: [{ index: 0, id: p13.id ?? p13.toolCallId, type: "function", function: { name: p13.toolName, arguments: "" } }] });
|
|
17291
|
+
break;
|
|
17292
|
+
case "tool-input-delta":
|
|
17293
|
+
send({ tool_calls: [{ index: 0, function: { arguments: p13.delta ?? p13.text ?? p13.argsTextDelta ?? "" } }] });
|
|
17294
|
+
break;
|
|
17295
|
+
case "finish":
|
|
17296
|
+
send({}, p13.finishReason || "stop");
|
|
17297
|
+
break;
|
|
17298
|
+
case "error":
|
|
17299
|
+
throw p13.error instanceof Error || p13.error && typeof p13.error === "object" ? p13.error : new Error(typeof p13.error === "string" ? p13.error : "Upstream stream failed");
|
|
17300
|
+
}
|
|
16747
17301
|
}
|
|
17302
|
+
onChunk("data: [DONE]\n\n");
|
|
17303
|
+
} finally {
|
|
17304
|
+
budget.close();
|
|
16748
17305
|
}
|
|
16749
|
-
onChunk("data: [DONE]\n\n");
|
|
16750
17306
|
}
|
|
16751
17307
|
|
|
16752
17308
|
// src/server/router.ts
|
|
@@ -17024,7 +17580,8 @@ async function handleAnthropicMessages(req, res, options, modelCache, plog) {
|
|
|
17024
17580
|
compatibility: model.compatibility,
|
|
17025
17581
|
upstreamModelId: upstreamModelId(model)
|
|
17026
17582
|
},
|
|
17027
|
-
maxTools: npmMaxTools
|
|
17583
|
+
maxTools: npmMaxTools,
|
|
17584
|
+
log: plog
|
|
17028
17585
|
});
|
|
17029
17586
|
const clientWantsStream = Boolean(body.stream);
|
|
17030
17587
|
const responseModelId = getResponseModelId(body.model, model, options);
|