@bman654/clodex 2.18.10 → 2.20.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 +76 -5
- package/dist/{chunk-VFYRDLDD.js → chunk-Q2YFYQQT.js} +76 -5
- package/dist/chunk-Q2YFYQQT.js.map +1 -0
- package/dist/claude-wrapper.js +17 -7
- package/dist/claude-wrapper.js.map +1 -1
- package/dist/cli.js +402 -61
- package/dist/cli.js.map +1 -1
- package/docs/background-agents.md +4 -3
- package/docs/ollama-cloud.md +35 -0
- package/package.json +1 -1
- package/dist/chunk-VFYRDLDD.js.map +0 -1
package/dist/cli.js
CHANGED
|
@@ -7,12 +7,14 @@ import {
|
|
|
7
7
|
REGISTRY_SCHEMA_VERSION_WITH_MATERIALIZED_ACTIVE_ACCOUNT,
|
|
8
8
|
assertRegistryWriteOwnership,
|
|
9
9
|
backupDir,
|
|
10
|
+
cancelOnExit,
|
|
10
11
|
clearActiveOAuthAccount,
|
|
11
12
|
collectPristineFacts,
|
|
12
13
|
contentAddressedBackupPath,
|
|
13
14
|
customProviderId,
|
|
14
15
|
emitParentNotice,
|
|
15
16
|
ensureSecureAppHome,
|
|
17
|
+
exitAfterDrain,
|
|
16
18
|
findClaudeBinary,
|
|
17
19
|
getAppHome,
|
|
18
20
|
getClaudeVersionForBinary,
|
|
@@ -74,7 +76,7 @@ import {
|
|
|
74
76
|
withRegistryWriteLockSync,
|
|
75
77
|
writeParentNoticeLines,
|
|
76
78
|
writeParentNoticeLinesSync
|
|
77
|
-
} from "./chunk-
|
|
79
|
+
} from "./chunk-Q2YFYQQT.js";
|
|
78
80
|
|
|
79
81
|
// src/cli.ts
|
|
80
82
|
import pc13 from "picocolors";
|
|
@@ -323,19 +325,25 @@ function launchClaude(env, model, extraArgs) {
|
|
|
323
325
|
env,
|
|
324
326
|
shell: isWindows
|
|
325
327
|
});
|
|
326
|
-
const
|
|
327
|
-
child.kill(
|
|
328
|
+
const forwardSigint = () => {
|
|
329
|
+
child.kill("SIGINT");
|
|
328
330
|
};
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
331
|
+
const forwardSigterm = () => {
|
|
332
|
+
child.kill("SIGTERM");
|
|
333
|
+
};
|
|
334
|
+
process.once("SIGINT", forwardSigint);
|
|
335
|
+
process.once("SIGTERM", forwardSigterm);
|
|
336
|
+
const stopForwarding = () => {
|
|
337
|
+
process.off("SIGINT", forwardSigint);
|
|
338
|
+
process.off("SIGTERM", forwardSigterm);
|
|
339
|
+
};
|
|
340
|
+
const finish = (code) => {
|
|
341
|
+
stopForwarding();
|
|
336
342
|
restore();
|
|
337
|
-
resolve4(
|
|
338
|
-
}
|
|
343
|
+
resolve4(code);
|
|
344
|
+
};
|
|
345
|
+
child.on("exit", (code) => finish(code ?? 0));
|
|
346
|
+
child.on("error", () => finish(1));
|
|
339
347
|
});
|
|
340
348
|
}
|
|
341
349
|
|
|
@@ -346,7 +354,7 @@ import { join } from "path";
|
|
|
346
354
|
// package.json
|
|
347
355
|
var package_default = {
|
|
348
356
|
name: "@bman654/clodex",
|
|
349
|
-
version: "2.
|
|
357
|
+
version: "2.20.0",
|
|
350
358
|
publishConfig: {
|
|
351
359
|
access: "public"
|
|
352
360
|
},
|
|
@@ -5429,6 +5437,55 @@ function openCodeGoSessionHeaders(model, claudeSessionId) {
|
|
|
5429
5437
|
return { "x-opencode-session": claudeSessionId ?? OPENCODE_GO_FALLBACK_SESSION_ID };
|
|
5430
5438
|
}
|
|
5431
5439
|
|
|
5440
|
+
// src/data/ollama-cloud-models.ts
|
|
5441
|
+
var OLLAMA_CLOUD_PROVIDER_ID = "ollama-cloud";
|
|
5442
|
+
var OLLAMA_CLOUD_PROVIDER_NAME = "Ollama Cloud";
|
|
5443
|
+
var OLLAMA_CLOUD_COMPLETIONS_BASE_URL = "https://ollama.com/v1";
|
|
5444
|
+
var OPENAI_COMPATIBLE = "@ai-sdk/openai-compatible";
|
|
5445
|
+
var CATALOG = [
|
|
5446
|
+
{
|
|
5447
|
+
id: "kimi-k3",
|
|
5448
|
+
name: "Kimi K3",
|
|
5449
|
+
contextWindow: 1048576,
|
|
5450
|
+
family: "kimi",
|
|
5451
|
+
modalities: ["text", "image"]
|
|
5452
|
+
},
|
|
5453
|
+
{ id: "glm-5.3", name: "GLM-5.3", contextWindow: 1048576, family: "glm", modalities: ["text"] },
|
|
5454
|
+
{
|
|
5455
|
+
id: "glm-5.3-flash",
|
|
5456
|
+
name: "GLM-5.3 Flash",
|
|
5457
|
+
contextWindow: 1048576,
|
|
5458
|
+
family: "glm",
|
|
5459
|
+
modalities: ["text", "image"]
|
|
5460
|
+
},
|
|
5461
|
+
{
|
|
5462
|
+
id: "deepseek-v4.1-flash",
|
|
5463
|
+
name: "DeepSeek V4.1 Flash",
|
|
5464
|
+
contextWindow: 1048576,
|
|
5465
|
+
family: "deepseek",
|
|
5466
|
+
modalities: ["text", "image"]
|
|
5467
|
+
}
|
|
5468
|
+
].map((model) => ({
|
|
5469
|
+
...model,
|
|
5470
|
+
upstreamModelId: model.id,
|
|
5471
|
+
modelFormat: "openai",
|
|
5472
|
+
npm: OPENAI_COMPATIBLE,
|
|
5473
|
+
apiUrl: OLLAMA_CLOUD_COMPLETIONS_BASE_URL,
|
|
5474
|
+
// Ollama publishes pay-as-you-go per-token rates (https://ollama.com/pricing),
|
|
5475
|
+
// but clodex does not record them for this provider: no cost is set, and the
|
|
5476
|
+
// template's preserveModelPricing keeps the global pricing cache off it.
|
|
5477
|
+
compatibility: {
|
|
5478
|
+
supportsStore: false,
|
|
5479
|
+
supportsDeveloperRole: false,
|
|
5480
|
+
maxTokensField: "max_tokens",
|
|
5481
|
+
// Effort mapping is unverified against ollama.com; send none.
|
|
5482
|
+
supportsReasoningEffort: false
|
|
5483
|
+
}
|
|
5484
|
+
}));
|
|
5485
|
+
function buildOllamaCloudModels() {
|
|
5486
|
+
return structuredClone(CATALOG);
|
|
5487
|
+
}
|
|
5488
|
+
|
|
5432
5489
|
// src/provider-templates.ts
|
|
5433
5490
|
async function verifyOpenCodeGoCredential(apiKey) {
|
|
5434
5491
|
const model = buildOpenCodeGoModels().find((entry) => entry.modelFormat === "openai");
|
|
@@ -5474,6 +5531,40 @@ async function verifyOpenCodeGoCredential(apiKey) {
|
|
|
5474
5531
|
}
|
|
5475
5532
|
return null;
|
|
5476
5533
|
}
|
|
5534
|
+
async function verifyOllamaCloudCredential(apiKey) {
|
|
5535
|
+
const model = buildOllamaCloudModels()[0];
|
|
5536
|
+
if (!model) return null;
|
|
5537
|
+
try {
|
|
5538
|
+
const response = await fetch(`${OLLAMA_CLOUD_COMPLETIONS_BASE_URL}/chat/completions`, {
|
|
5539
|
+
method: "POST",
|
|
5540
|
+
headers: {
|
|
5541
|
+
"Content-Type": "application/json",
|
|
5542
|
+
Authorization: `Bearer ${apiKey}`
|
|
5543
|
+
},
|
|
5544
|
+
body: JSON.stringify({ model: model.upstreamModelId ?? model.id }),
|
|
5545
|
+
signal: AbortSignal.timeout(TEST_TIMEOUT_MS)
|
|
5546
|
+
});
|
|
5547
|
+
if (response.status === 401 || response.status === 403) {
|
|
5548
|
+
let entitlement = false;
|
|
5549
|
+
if (response.status === 403) {
|
|
5550
|
+
const text6 = await response.text().catch(() => "");
|
|
5551
|
+
entitlement = /\b(?:subscription|plan|upgrade|quota|model|access)\b/i.test(text6) && !/\bapi[ _-]?key\b/i.test(text6);
|
|
5552
|
+
} else {
|
|
5553
|
+
await response.body?.cancel?.().catch(() => void 0);
|
|
5554
|
+
}
|
|
5555
|
+
if (!entitlement) {
|
|
5556
|
+
return "Ollama Cloud rejected this API key (authentication failed). Check the key and try again.";
|
|
5557
|
+
}
|
|
5558
|
+
} else {
|
|
5559
|
+
try {
|
|
5560
|
+
await response.body?.cancel?.();
|
|
5561
|
+
} catch {
|
|
5562
|
+
}
|
|
5563
|
+
}
|
|
5564
|
+
} catch {
|
|
5565
|
+
}
|
|
5566
|
+
return null;
|
|
5567
|
+
}
|
|
5477
5568
|
var PROVIDER_TEMPLATES = [
|
|
5478
5569
|
{
|
|
5479
5570
|
id: "openai",
|
|
@@ -5485,6 +5576,24 @@ var PROVIDER_TEMPLATES = [
|
|
|
5485
5576
|
modelSource: "api-list",
|
|
5486
5577
|
supported: true
|
|
5487
5578
|
},
|
|
5579
|
+
{
|
|
5580
|
+
id: OLLAMA_CLOUD_PROVIDER_ID,
|
|
5581
|
+
name: OLLAMA_CLOUD_PROVIDER_NAME,
|
|
5582
|
+
authType: "api",
|
|
5583
|
+
npm: "@ai-sdk/openai-compatible",
|
|
5584
|
+
defaultBaseUrl: OLLAMA_CLOUD_COMPLETIONS_BASE_URL,
|
|
5585
|
+
modelsPath: "/models",
|
|
5586
|
+
signupUrl: "https://ollama.com",
|
|
5587
|
+
modelSource: "api-list",
|
|
5588
|
+
verifyCredential: verifyOllamaCloudCredential,
|
|
5589
|
+
// Getter for the same reason as opencode-go: no eager catalog clone at import.
|
|
5590
|
+
get staticModels() {
|
|
5591
|
+
return buildOllamaCloudModels();
|
|
5592
|
+
},
|
|
5593
|
+
staticModelPolicy: "allowlist",
|
|
5594
|
+
preserveModelPricing: true,
|
|
5595
|
+
supported: true
|
|
5596
|
+
},
|
|
5488
5597
|
{
|
|
5489
5598
|
id: OPENCODE_GO_PROVIDER_ID,
|
|
5490
5599
|
name: OPENCODE_GO_PROVIDER_NAME,
|
|
@@ -5682,9 +5791,12 @@ function getUserPricingCachePath() {
|
|
|
5682
5791
|
function loadPricingCache() {
|
|
5683
5792
|
return readPricingFile(getUserPricingCachePath()) ?? loadBundledPricingCache();
|
|
5684
5793
|
}
|
|
5685
|
-
async function fetchPricingCache() {
|
|
5794
|
+
async function fetchPricingCache(signal) {
|
|
5686
5795
|
const controller = new AbortController();
|
|
5687
5796
|
const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
5797
|
+
const onCallerAbort = () => controller.abort();
|
|
5798
|
+
if (signal?.aborted) controller.abort();
|
|
5799
|
+
else signal?.addEventListener("abort", onCallerAbort, { once: true });
|
|
5688
5800
|
try {
|
|
5689
5801
|
const response = await fetch(PRICING_API_URL, {
|
|
5690
5802
|
signal: controller.signal,
|
|
@@ -5699,6 +5811,7 @@ async function fetchPricingCache() {
|
|
|
5699
5811
|
return null;
|
|
5700
5812
|
} finally {
|
|
5701
5813
|
clearTimeout(timer);
|
|
5814
|
+
signal?.removeEventListener("abort", onCallerAbort);
|
|
5702
5815
|
}
|
|
5703
5816
|
}
|
|
5704
5817
|
function pickPricingRow(rows, platform) {
|
|
@@ -5802,17 +5915,24 @@ function applyPricingToRegistryProviders(registry, cache) {
|
|
|
5802
5915
|
return changed;
|
|
5803
5916
|
}
|
|
5804
5917
|
function enrichPricingAsync(onComplete) {
|
|
5918
|
+
const controller = new AbortController();
|
|
5919
|
+
const { signal } = controller;
|
|
5920
|
+
const unregister = cancelOnExit(() => controller.abort());
|
|
5805
5921
|
void (async () => {
|
|
5806
|
-
const fetched = await fetchPricingCache();
|
|
5922
|
+
const fetched = await fetchPricingCache(signal);
|
|
5923
|
+
if (signal.aborted) return;
|
|
5807
5924
|
const cache = fetched ?? loadPricingCache();
|
|
5808
5925
|
const changed = await withRegistryWriteLock(() => {
|
|
5926
|
+
if (signal.aborted) return void 0;
|
|
5809
5927
|
const registry = loadRegistryStrict();
|
|
5810
5928
|
const updated = applyPricingToRegistryProviders(registry, cache);
|
|
5811
5929
|
if (updated) saveRegistry(registry);
|
|
5812
5930
|
return updated;
|
|
5813
|
-
});
|
|
5814
|
-
onComplete?.(changed);
|
|
5815
|
-
})().catch(() =>
|
|
5931
|
+
}, { signal });
|
|
5932
|
+
if (changed !== void 0) onComplete?.(changed);
|
|
5933
|
+
})().catch(() => {
|
|
5934
|
+
if (!signal.aborted) onComplete?.(false);
|
|
5935
|
+
}).finally(unregister);
|
|
5816
5936
|
}
|
|
5817
5937
|
function pricingPlatformForProvider(templateId, providerId) {
|
|
5818
5938
|
return TEMPLATE_TO_PRICING_PLATFORM[templateId] ?? TEMPLATE_TO_PRICING_PLATFORM[providerId];
|
|
@@ -5821,6 +5941,7 @@ function pricingPlatformForProvider(templateId, providerId) {
|
|
|
5821
5941
|
// src/registry/models-dev.ts
|
|
5822
5942
|
var MODELS_DEV_API_URL = "https://models.dev/api.json";
|
|
5823
5943
|
var FETCH_TIMEOUT_MS2 = 15e3;
|
|
5944
|
+
var MODELS_DEV_REFRESH_DELAY_MS = 500;
|
|
5824
5945
|
var FILE_MODE2 = 384;
|
|
5825
5946
|
var META_KEY = "_relay_meta";
|
|
5826
5947
|
var memoryCache = null;
|
|
@@ -5921,9 +6042,12 @@ function loadModelsDevCache() {
|
|
|
5921
6042
|
if (memoryCache && memoryCachePath === "bundled") return memoryCache;
|
|
5922
6043
|
return rememberModelsDevCache("bundled", loadBundledModelsDevCache());
|
|
5923
6044
|
}
|
|
5924
|
-
async function fetchModelsDevCache() {
|
|
6045
|
+
async function fetchModelsDevCache(signal) {
|
|
5925
6046
|
const controller = new AbortController();
|
|
5926
6047
|
const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS2);
|
|
6048
|
+
const onCallerAbort = () => controller.abort();
|
|
6049
|
+
if (signal?.aborted) controller.abort();
|
|
6050
|
+
else signal?.addEventListener("abort", onCallerAbort, { once: true });
|
|
5927
6051
|
try {
|
|
5928
6052
|
const response = await fetch(MODELS_DEV_API_URL, {
|
|
5929
6053
|
signal: controller.signal,
|
|
@@ -5939,16 +6063,28 @@ async function fetchModelsDevCache() {
|
|
|
5939
6063
|
return null;
|
|
5940
6064
|
} finally {
|
|
5941
6065
|
clearTimeout(timer);
|
|
6066
|
+
signal?.removeEventListener("abort", onCallerAbort);
|
|
5942
6067
|
}
|
|
5943
6068
|
}
|
|
5944
6069
|
function resolveModelsDevSlug(providerId) {
|
|
5945
6070
|
return REGISTRY_TO_MODELS_DEV[providerId] ?? providerId;
|
|
5946
6071
|
}
|
|
5947
6072
|
function refreshModelsDevCacheAsync(onComplete) {
|
|
5948
|
-
|
|
5949
|
-
|
|
5950
|
-
|
|
5951
|
-
|
|
6073
|
+
const controller = new AbortController();
|
|
6074
|
+
const start = setTimeout(() => {
|
|
6075
|
+
void (async () => {
|
|
6076
|
+
try {
|
|
6077
|
+
const updated = await fetchModelsDevCache(controller.signal) !== null;
|
|
6078
|
+
onComplete?.(updated);
|
|
6079
|
+
} finally {
|
|
6080
|
+
unregister();
|
|
6081
|
+
}
|
|
6082
|
+
})();
|
|
6083
|
+
}, MODELS_DEV_REFRESH_DELAY_MS);
|
|
6084
|
+
const unregister = cancelOnExit(() => {
|
|
6085
|
+
clearTimeout(start);
|
|
6086
|
+
controller.abort();
|
|
6087
|
+
});
|
|
5952
6088
|
}
|
|
5953
6089
|
function findModelsDevModel(providerId, modelId, cache = loadModelsDevCache()) {
|
|
5954
6090
|
const slug = resolveModelsDevSlug(providerId);
|
|
@@ -6512,7 +6648,10 @@ function writeInferenceResponseLifecycleLog(path, entry) {
|
|
|
6512
6648
|
...entry.failureSource ? { failureSource: entry.failureSource } : {},
|
|
6513
6649
|
...entry.terminationSource ? { terminationSource: entry.terminationSource } : {},
|
|
6514
6650
|
...attempt !== void 0 ? { attempt } : {},
|
|
6515
|
-
...entry.reusedSocket !== void 0 ? { reusedSocket: entry.reusedSocket } : {}
|
|
6651
|
+
...entry.reusedSocket !== void 0 ? { reusedSocket: entry.reusedSocket } : {},
|
|
6652
|
+
...entry.outageHold !== void 0 ? { outageHold: entry.outageHold } : {},
|
|
6653
|
+
...entry.holdElapsedMs !== void 0 ? { holdElapsedMs: entry.holdElapsedMs } : {},
|
|
6654
|
+
...entry.retryDelayMs !== void 0 ? { retryDelayMs: entry.retryDelayMs } : {}
|
|
6516
6655
|
}));
|
|
6517
6656
|
}
|
|
6518
6657
|
function writeProxyLifecycleLog(path, entry) {
|
|
@@ -8104,7 +8243,10 @@ import { AsyncLocalStorage } from "async_hooks";
|
|
|
8104
8243
|
|
|
8105
8244
|
// src/outbound-proxy.ts
|
|
8106
8245
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
8246
|
+
import diagnosticsChannel from "diagnostics_channel";
|
|
8247
|
+
import { Socket } from "net";
|
|
8107
8248
|
import { networkInterfaces } from "os";
|
|
8249
|
+
import { TLSSocket } from "tls";
|
|
8108
8250
|
import { HttpsProxyAgent } from "https-proxy-agent";
|
|
8109
8251
|
var OUTBOUND_PROXY_HOP_HEADER = "x-clodex-proxy-hop";
|
|
8110
8252
|
var outboundProxyHopNonce = randomUUID3();
|
|
@@ -8177,12 +8319,34 @@ function proxyUrlTargetsListener(proxyUrl, listenerHost, listenerPort, localAddr
|
|
|
8177
8319
|
return isWildcard(boundHost) && (isWildcard(proxyHost) || localAddresses.has(proxyHost));
|
|
8178
8320
|
}
|
|
8179
8321
|
var dispatcherInstalled = false;
|
|
8322
|
+
function abandonPendingConnectionsOnExit() {
|
|
8323
|
+
const pending = /* @__PURE__ */ new Set();
|
|
8324
|
+
const track = (message) => {
|
|
8325
|
+
const socket = message.socket;
|
|
8326
|
+
if (!(socket instanceof Socket)) return;
|
|
8327
|
+
pending.add(socket);
|
|
8328
|
+
const settled = () => pending.delete(socket);
|
|
8329
|
+
socket.once(socket instanceof TLSSocket ? "secureConnect" : "connect", settled);
|
|
8330
|
+
socket.once("close", settled);
|
|
8331
|
+
};
|
|
8332
|
+
diagnosticsChannel.subscribe("net.client.socket", track);
|
|
8333
|
+
cancelOnExit(() => {
|
|
8334
|
+
diagnosticsChannel.unsubscribe("net.client.socket", track);
|
|
8335
|
+
for (const socket of pending) {
|
|
8336
|
+
socket.on("error", () => {
|
|
8337
|
+
});
|
|
8338
|
+
socket.destroy(Object.assign(new Error("clodex is exiting"), { code: "ECONNABORTED" }));
|
|
8339
|
+
}
|
|
8340
|
+
pending.clear();
|
|
8341
|
+
});
|
|
8342
|
+
}
|
|
8180
8343
|
async function installOutboundDispatcher() {
|
|
8181
8344
|
if (dispatcherInstalled) return true;
|
|
8182
8345
|
try {
|
|
8183
8346
|
const { Agent: Agent3, EnvHttpProxyAgent, setGlobalDispatcher } = await import("undici");
|
|
8184
8347
|
const dispatcher = hasOutboundProxyEnv() ? new EnvHttpProxyAgent({ allowH2: false }) : new Agent3({ allowH2: false });
|
|
8185
8348
|
setGlobalDispatcher(dispatcher);
|
|
8349
|
+
abandonPendingConnectionsOnExit();
|
|
8186
8350
|
dispatcherInstalled = true;
|
|
8187
8351
|
return true;
|
|
8188
8352
|
} catch (err) {
|
|
@@ -8699,6 +8863,34 @@ function passthroughUpstreamRetries(env = process.env, warn = defaultWarn) {
|
|
|
8699
8863
|
}
|
|
8700
8864
|
return DEFAULT_PASSTHROUGH_RETRIES;
|
|
8701
8865
|
}
|
|
8866
|
+
var PASSTHROUGH_OUTAGE_HOLD_ENV = "CLODEX_PASSTHROUGH_OUTAGE_HOLD_MS";
|
|
8867
|
+
var DEFAULT_PASSTHROUGH_OUTAGE_HOLD_MS = 12e4;
|
|
8868
|
+
var MAX_PASSTHROUGH_OUTAGE_HOLD_MS = 57e4;
|
|
8869
|
+
function passthroughOutageHoldMs(env = process.env, warn = defaultWarn) {
|
|
8870
|
+
const raw = env[PASSTHROUGH_OUTAGE_HOLD_ENV]?.trim();
|
|
8871
|
+
if (raw === void 0 || raw === "") return DEFAULT_PASSTHROUGH_OUTAGE_HOLD_MS;
|
|
8872
|
+
const value = Number(raw);
|
|
8873
|
+
if (!Number.isInteger(value) || value < 0) {
|
|
8874
|
+
reportOnce(
|
|
8875
|
+
`${PASSTHROUGH_OUTAGE_HOLD_ENV}=${raw}`,
|
|
8876
|
+
`ignoring ${PASSTHROUGH_OUTAGE_HOLD_ENV}=${raw} (expected a non-negative integer number of milliseconds)`,
|
|
8877
|
+
warn
|
|
8878
|
+
);
|
|
8879
|
+
return DEFAULT_PASSTHROUGH_OUTAGE_HOLD_MS;
|
|
8880
|
+
}
|
|
8881
|
+
if (value > MAX_PASSTHROUGH_OUTAGE_HOLD_MS) {
|
|
8882
|
+
reportOnce(
|
|
8883
|
+
`${PASSTHROUGH_OUTAGE_HOLD_ENV}=${raw}`,
|
|
8884
|
+
`clamping ${PASSTHROUGH_OUTAGE_HOLD_ENV}=${raw} to ${MAX_PASSTHROUGH_OUTAGE_HOLD_MS}ms (the ceiling keeps the hold 29 s inside the 599 s Claude Code waits for response headers after a request has gone unanswered, at its default API_TIMEOUT_MS)`,
|
|
8885
|
+
warn
|
|
8886
|
+
);
|
|
8887
|
+
return MAX_PASSTHROUGH_OUTAGE_HOLD_MS;
|
|
8888
|
+
}
|
|
8889
|
+
return value;
|
|
8890
|
+
}
|
|
8891
|
+
function outageRetryDelayMs(retry) {
|
|
8892
|
+
return Math.min(15e3, 1e3 * 2 ** Math.max(0, retry - 1));
|
|
8893
|
+
}
|
|
8702
8894
|
|
|
8703
8895
|
// src/oauth/ws-upgrade-pacer.ts
|
|
8704
8896
|
var WS_NEW_CONNECTIONS_PER_MIN_ENV = "CLODEX_WS_MAX_NEW_CONNECTIONS_PER_MIN";
|
|
@@ -9782,12 +9974,17 @@ function deleteEntry(entry, closeSocket = true) {
|
|
|
9782
9974
|
entry.inFlight = false;
|
|
9783
9975
|
entry.current = void 0;
|
|
9784
9976
|
unregisterEntry(entry);
|
|
9785
|
-
if (closeSocket)
|
|
9786
|
-
|
|
9787
|
-
|
|
9788
|
-
|
|
9789
|
-
|
|
9977
|
+
if (closeSocket) closeGracefully(entry.socket);
|
|
9978
|
+
}
|
|
9979
|
+
var WS_CLOSING = 2;
|
|
9980
|
+
function closeGracefully(socket) {
|
|
9981
|
+
try {
|
|
9982
|
+
socket.close();
|
|
9983
|
+
} catch {
|
|
9984
|
+
return;
|
|
9790
9985
|
}
|
|
9986
|
+
if (socket.readyState !== WS_CLOSING) return;
|
|
9987
|
+
socket.once("close", cancelOnExit(() => socket.terminate()));
|
|
9791
9988
|
}
|
|
9792
9989
|
function failContext(entry, ctx, message, diagnosticDetails, statusCode, retryAfter) {
|
|
9793
9990
|
if (ctx.closed || entry.current !== ctx) return;
|
|
@@ -11125,6 +11322,10 @@ async function createLanguageModel(spec) {
|
|
|
11125
11322
|
...spec.authType !== "none" && apiKey.trim() ? { apiKey } : {},
|
|
11126
11323
|
...spec.authType === "none" ? { fetch: fetchWithoutCredentialHeaders } : {},
|
|
11127
11324
|
...spec.headers ? { headers: spec.headers } : {},
|
|
11325
|
+
// ollama.com sends a streamed usage chunk only when the request carries
|
|
11326
|
+
// stream_options.include_usage (observed 2026-10-13); without it every
|
|
11327
|
+
// streamed turn reports output_tokens 0. Scoped to this provider only.
|
|
11328
|
+
...spec.providerId === OLLAMA_CLOUD_PROVIDER_ID ? { includeUsage: true } : {},
|
|
11128
11329
|
...spec.compatibility ? {
|
|
11129
11330
|
transformRequestBody: (body) => transformOpenAiCompatibleRequestBody(body, spec.compatibility)
|
|
11130
11331
|
} : {}
|
|
@@ -20069,6 +20270,19 @@ var MAX_BODY_BYTES = 50 * 1024 * 1024;
|
|
|
20069
20270
|
var MAX_ERROR_BODY_BYTES = 64 * 1024;
|
|
20070
20271
|
var MAX_USAGE_SSE_BLOCK_BYTES = 64 * 1024;
|
|
20071
20272
|
var RETRYABLE_PASSTHROUGH_CODE = "ECONNRESET";
|
|
20273
|
+
var OUTAGE_HOLD_CODES = /* @__PURE__ */ new Set([
|
|
20274
|
+
"ENOTFOUND",
|
|
20275
|
+
"EAI_AGAIN",
|
|
20276
|
+
"EAI_FAIL",
|
|
20277
|
+
"ECONNREFUSED",
|
|
20278
|
+
"ENETUNREACH",
|
|
20279
|
+
"EHOSTUNREACH",
|
|
20280
|
+
"ENETDOWN",
|
|
20281
|
+
"EHOSTDOWN",
|
|
20282
|
+
"EADDRNOTAVAIL",
|
|
20283
|
+
"ECONNRESET",
|
|
20284
|
+
"ETIMEDOUT"
|
|
20285
|
+
]);
|
|
20072
20286
|
function upstreamUnreachableDetail(err) {
|
|
20073
20287
|
return err.message || err.code || err.name || "connection failed";
|
|
20074
20288
|
}
|
|
@@ -20223,10 +20437,17 @@ function requestHeadersWithoutProxyHeaders(req) {
|
|
|
20223
20437
|
}
|
|
20224
20438
|
return headers;
|
|
20225
20439
|
}
|
|
20226
|
-
function forwardRawAnthropicRequest(req, res, rawBody, origin, rejectUnauthorized, agent, onErrorResponse, onResponseUsage, lifecycle, isLocalShutdown = () => false) {
|
|
20440
|
+
function forwardRawAnthropicRequest(req, res, rawBody, origin, rejectUnauthorized, agent, onErrorResponse, onResponseUsage, lifecycle, isLocalShutdown = () => false, passthrough = {}) {
|
|
20227
20441
|
return new Promise((resolve4) => {
|
|
20228
20442
|
const startedAt = Date.now();
|
|
20229
20443
|
const retryBudget = passthroughUpstreamRetries();
|
|
20444
|
+
const outageHoldMs = passthrough.holdOutages ? passthroughOutageHoldMs() : 0;
|
|
20445
|
+
const holdDeadline = startedAt + outageHoldMs;
|
|
20446
|
+
let lastHoldError;
|
|
20447
|
+
let outageRetries = 0;
|
|
20448
|
+
let reuseRetries = 0;
|
|
20449
|
+
let holdTimer;
|
|
20450
|
+
let attemptDeadlineTimer;
|
|
20230
20451
|
let lastActivityAt = startedAt;
|
|
20231
20452
|
let headersReceived = false;
|
|
20232
20453
|
let firstByteAt;
|
|
@@ -20278,8 +20499,49 @@ function forwardRawAnthropicRequest(req, res, rawBody, origin, rejectUnauthorize
|
|
|
20278
20499
|
const errorType = (err) => err.code ?? err.name;
|
|
20279
20500
|
let upstream;
|
|
20280
20501
|
let attempt = 0;
|
|
20281
|
-
const isRetryableUpstreamFailure = (err, request3) =>
|
|
20282
|
-
const
|
|
20502
|
+
const isRetryableUpstreamFailure = (err, request3) => reuseRetries < retryBudget && !headersReceived && !failed && !clientDisconnected2 && !isLocalShutdown() && request3.reusedSocket === true && err.code === RETRYABLE_PASSTHROUGH_CODE;
|
|
20503
|
+
const isOutageHoldFailure = (err, request3, tlsEstablished) => outageHoldMs > 0 && !headersReceived && !failed && !clientDisconnected2 && !isLocalShutdown() && request3.reusedSocket !== true && !tlsEstablished && OUTAGE_HOLD_CODES.has(err.code ?? "");
|
|
20504
|
+
const clearAttemptDeadline = () => {
|
|
20505
|
+
if (attemptDeadlineTimer) clearTimeout(attemptDeadlineTimer);
|
|
20506
|
+
attemptDeadlineTimer = void 0;
|
|
20507
|
+
};
|
|
20508
|
+
const answer502 = (detail, failure) => {
|
|
20509
|
+
failed = true;
|
|
20510
|
+
stopProgress();
|
|
20511
|
+
clearAttemptDeadline();
|
|
20512
|
+
const now = Date.now();
|
|
20513
|
+
writeLifecycle("response_failed", {
|
|
20514
|
+
statusCode: 502,
|
|
20515
|
+
phase: responsePhase(),
|
|
20516
|
+
durationMs: now - startedAt,
|
|
20517
|
+
idleMs: now - lastActivityAt,
|
|
20518
|
+
bytes,
|
|
20519
|
+
chunks,
|
|
20520
|
+
errorType: failure.errorType,
|
|
20521
|
+
terminationSource: isLocalShutdown() ? "local_shutdown" : "upstream_failure",
|
|
20522
|
+
attempt,
|
|
20523
|
+
reusedSocket: failure.reusedSocket,
|
|
20524
|
+
...failure.outageHold ? { outageHold: true } : {}
|
|
20525
|
+
});
|
|
20526
|
+
onErrorResponse?.(502, `Anthropic upstream unreachable: ${detail}`);
|
|
20527
|
+
if (!res.headersSent) res.writeHead(502, { "Content-Type": "text/plain" });
|
|
20528
|
+
res.end(`Anthropic upstream unreachable: ${detail}`);
|
|
20529
|
+
done();
|
|
20530
|
+
};
|
|
20531
|
+
const holdExpiredDetail = (pendingHandshake) => {
|
|
20532
|
+
const parts = [`outage hold of ${outageHoldMs} ms expired`];
|
|
20533
|
+
if (pendingHandshake) parts[0] += " before the TLS handshake completed";
|
|
20534
|
+
if (lastHoldError) parts.push(`last upstream error: ${upstreamUnreachableDetail(lastHoldError)}`);
|
|
20535
|
+
return parts.join("; ");
|
|
20536
|
+
};
|
|
20537
|
+
const lateReplayDetail = () => {
|
|
20538
|
+
const parts = [
|
|
20539
|
+
`reused connection was reset and the outage hold of ${outageHoldMs} ms had expired before the request could be resent, so it was not resent on a new connection`
|
|
20540
|
+
];
|
|
20541
|
+
if (lastHoldError) parts.push(`last upstream error: ${upstreamUnreachableDetail(lastHoldError)}`);
|
|
20542
|
+
return parts.join("; ");
|
|
20543
|
+
};
|
|
20544
|
+
const sendAttempt = (replayOfReset = false) => {
|
|
20283
20545
|
attempt += 1;
|
|
20284
20546
|
const request3 = https.request({
|
|
20285
20547
|
protocol: "https:",
|
|
@@ -20290,8 +20552,10 @@ function forwardRawAnthropicRequest(req, res, rawBody, origin, rejectUnauthorize
|
|
|
20290
20552
|
headers: requestHeadersWithoutProxyHeaders(req),
|
|
20291
20553
|
servername: net.isIP(origin.hostname) ? void 0 : origin.hostname,
|
|
20292
20554
|
rejectUnauthorized,
|
|
20293
|
-
agent
|
|
20555
|
+
agent,
|
|
20556
|
+
...passthrough.lookup ? { lookup: passthrough.lookup } : {}
|
|
20294
20557
|
}, (upstreamRes) => {
|
|
20558
|
+
clearAttemptDeadline();
|
|
20295
20559
|
headersReceived = true;
|
|
20296
20560
|
statusCode = upstreamRes.statusCode ?? 502;
|
|
20297
20561
|
lastActivityAt = Date.now();
|
|
@@ -20340,12 +20604,95 @@ function forwardRawAnthropicRequest(req, res, rawBody, origin, rejectUnauthorize
|
|
|
20340
20604
|
});
|
|
20341
20605
|
});
|
|
20342
20606
|
upstream = request3;
|
|
20607
|
+
let tlsEstablished = false;
|
|
20608
|
+
const cutPendingAttempt = (lateReplay) => {
|
|
20609
|
+
if (tlsEstablished || headersReceived || failed || clientDisconnected2) return;
|
|
20610
|
+
answer502(lateReplay ? lateReplayDetail() : holdExpiredDetail(true), {
|
|
20611
|
+
errorType: "outage_hold_expired",
|
|
20612
|
+
reusedSocket: false,
|
|
20613
|
+
outageHold: true
|
|
20614
|
+
});
|
|
20615
|
+
request3.destroy();
|
|
20616
|
+
};
|
|
20617
|
+
const attemptRemaining = holdDeadline - Date.now();
|
|
20618
|
+
const startedAfterDeadline = outageHoldMs > 0 && attemptRemaining <= 0;
|
|
20619
|
+
if (outageHoldMs > 0 && attemptRemaining > 0) {
|
|
20620
|
+
attemptDeadlineTimer = setTimeout(() => {
|
|
20621
|
+
attemptDeadlineTimer = void 0;
|
|
20622
|
+
cutPendingAttempt(false);
|
|
20623
|
+
}, attemptRemaining);
|
|
20624
|
+
}
|
|
20625
|
+
request3.once("socket", (socket) => {
|
|
20626
|
+
if (request3.reusedSocket) {
|
|
20627
|
+
clearAttemptDeadline();
|
|
20628
|
+
return;
|
|
20629
|
+
}
|
|
20630
|
+
if (startedAfterDeadline) {
|
|
20631
|
+
cutPendingAttempt(replayOfReset);
|
|
20632
|
+
return;
|
|
20633
|
+
}
|
|
20634
|
+
socket.once("secureConnect", () => {
|
|
20635
|
+
tlsEstablished = true;
|
|
20636
|
+
clearAttemptDeadline();
|
|
20637
|
+
});
|
|
20638
|
+
});
|
|
20343
20639
|
request3.once("error", (err) => {
|
|
20640
|
+
clearAttemptDeadline();
|
|
20344
20641
|
if (clientDisconnected2) {
|
|
20345
20642
|
done();
|
|
20346
20643
|
return;
|
|
20347
20644
|
}
|
|
20645
|
+
if (isOutageHoldFailure(err, request3, tlsEstablished)) {
|
|
20646
|
+
lastHoldError = err;
|
|
20647
|
+
const now = Date.now();
|
|
20648
|
+
const remaining = holdDeadline - now;
|
|
20649
|
+
if (remaining <= 0) {
|
|
20650
|
+
answer502(holdExpiredDetail(false), {
|
|
20651
|
+
errorType: errorType(err),
|
|
20652
|
+
reusedSocket: false,
|
|
20653
|
+
outageHold: true
|
|
20654
|
+
});
|
|
20655
|
+
return;
|
|
20656
|
+
}
|
|
20657
|
+
outageRetries += 1;
|
|
20658
|
+
const backoff = outageRetryDelayMs(outageRetries);
|
|
20659
|
+
const holdDelay = Math.min(backoff, remaining);
|
|
20660
|
+
const waitReachesDeadline = backoff >= remaining;
|
|
20661
|
+
writeLifecycle("response_retried", {
|
|
20662
|
+
phase: responsePhase(),
|
|
20663
|
+
durationMs: now - startedAt,
|
|
20664
|
+
idleMs: now - lastActivityAt,
|
|
20665
|
+
errorType: errorType(err),
|
|
20666
|
+
terminationSource: "upstream_failure",
|
|
20667
|
+
attempt,
|
|
20668
|
+
reusedSocket: false,
|
|
20669
|
+
outageHold: true,
|
|
20670
|
+
holdElapsedMs: now - startedAt,
|
|
20671
|
+
retryDelayMs: holdDelay
|
|
20672
|
+
});
|
|
20673
|
+
lastActivityAt = now;
|
|
20674
|
+
holdTimer = setTimeout(() => {
|
|
20675
|
+
holdTimer = void 0;
|
|
20676
|
+
if (clientDisconnected2 || failed || isLocalShutdown()) {
|
|
20677
|
+
if (isLocalShutdown()) res.destroy();
|
|
20678
|
+
done();
|
|
20679
|
+
return;
|
|
20680
|
+
}
|
|
20681
|
+
if (waitReachesDeadline || Date.now() >= holdDeadline) {
|
|
20682
|
+
answer502(holdExpiredDetail(false), {
|
|
20683
|
+
errorType: errorType(err),
|
|
20684
|
+
reusedSocket: false,
|
|
20685
|
+
outageHold: true
|
|
20686
|
+
});
|
|
20687
|
+
return;
|
|
20688
|
+
}
|
|
20689
|
+
lastActivityAt = Date.now();
|
|
20690
|
+
sendAttempt();
|
|
20691
|
+
}, holdDelay);
|
|
20692
|
+
return;
|
|
20693
|
+
}
|
|
20348
20694
|
if (isRetryableUpstreamFailure(err, request3)) {
|
|
20695
|
+
reuseRetries += 1;
|
|
20349
20696
|
const retriedAt = Date.now();
|
|
20350
20697
|
writeLifecycle("response_retried", {
|
|
20351
20698
|
phase: responsePhase(),
|
|
@@ -20357,33 +20704,17 @@ function forwardRawAnthropicRequest(req, res, rawBody, origin, rejectUnauthorize
|
|
|
20357
20704
|
reusedSocket: true
|
|
20358
20705
|
});
|
|
20359
20706
|
lastActivityAt = retriedAt;
|
|
20360
|
-
sendAttempt();
|
|
20707
|
+
sendAttempt(true);
|
|
20361
20708
|
return;
|
|
20362
20709
|
}
|
|
20363
20710
|
if (failed) {
|
|
20364
20711
|
done();
|
|
20365
20712
|
return;
|
|
20366
20713
|
}
|
|
20367
|
-
|
|
20368
|
-
stopProgress();
|
|
20369
|
-
const now = Date.now();
|
|
20370
|
-
writeLifecycle("response_failed", {
|
|
20371
|
-
statusCode: 502,
|
|
20372
|
-
phase: responsePhase(),
|
|
20373
|
-
durationMs: now - startedAt,
|
|
20374
|
-
idleMs: now - lastActivityAt,
|
|
20375
|
-
bytes,
|
|
20376
|
-
chunks,
|
|
20714
|
+
answer502(upstreamUnreachableDetail(err), {
|
|
20377
20715
|
errorType: errorType(err),
|
|
20378
|
-
terminationSource: isLocalShutdown() ? "local_shutdown" : "upstream_failure",
|
|
20379
|
-
attempt,
|
|
20380
20716
|
reusedSocket: request3.reusedSocket === true
|
|
20381
20717
|
});
|
|
20382
|
-
const detail = upstreamUnreachableDetail(err);
|
|
20383
|
-
onErrorResponse?.(502, `Anthropic upstream unreachable: ${detail}`);
|
|
20384
|
-
if (!res.headersSent) res.writeHead(502, { "Content-Type": "text/plain" });
|
|
20385
|
-
res.end(`Anthropic upstream unreachable: ${detail}`);
|
|
20386
|
-
done();
|
|
20387
20718
|
});
|
|
20388
20719
|
request3.end(rawBody);
|
|
20389
20720
|
};
|
|
@@ -20404,6 +20735,11 @@ function forwardRawAnthropicRequest(req, res, rawBody, origin, rejectUnauthorize
|
|
|
20404
20735
|
stopProgress();
|
|
20405
20736
|
if (res.writableFinished || failed) return;
|
|
20406
20737
|
clientDisconnected2 = true;
|
|
20738
|
+
if (holdTimer) {
|
|
20739
|
+
clearTimeout(holdTimer);
|
|
20740
|
+
holdTimer = void 0;
|
|
20741
|
+
}
|
|
20742
|
+
clearAttemptDeadline();
|
|
20407
20743
|
const now = Date.now();
|
|
20408
20744
|
writeLifecycle("response_client_disconnected", {
|
|
20409
20745
|
statusCode,
|
|
@@ -20926,7 +21262,14 @@ async function startHttpProxy(options) {
|
|
|
20926
21262
|
provider: "anthropic",
|
|
20927
21263
|
progressIntervalMs: options.responseProgressIntervalMs ?? INFERENCE_PROGRESS_INTERVAL_MS
|
|
20928
21264
|
} : void 0,
|
|
20929
|
-
() => shuttingDown
|
|
21265
|
+
() => shuttingDown,
|
|
21266
|
+
// Only messages generation is held. count_tokens, OAuth, bootstrap and
|
|
21267
|
+
// telemetry calls carry no lifecycle log and short client timeouts of
|
|
21268
|
+
// their own, so holding them would only slow startup without a trace.
|
|
21269
|
+
{
|
|
21270
|
+
holdOutages: messagesEndpoint === "messages",
|
|
21271
|
+
lookup: options.anthropicLookup
|
|
21272
|
+
}
|
|
20930
21273
|
);
|
|
20931
21274
|
return;
|
|
20932
21275
|
}
|
|
@@ -22476,7 +22819,7 @@ function parseArgs(args) {
|
|
|
22476
22819
|
}
|
|
22477
22820
|
function rootHelpText() {
|
|
22478
22821
|
return `${pc13.bold("clodex")} v${VERSION}
|
|
22479
|
-
Bridge Claude Code to OpenAI
|
|
22822
|
+
Bridge Claude Code to OpenAI, OpenCode Go, Ollama Cloud and custom OpenAI-compatible models.
|
|
22480
22823
|
|
|
22481
22824
|
${pc13.bold("Usage:")}
|
|
22482
22825
|
clodex claude [options] [claude-flags]
|
|
@@ -22558,6 +22901,7 @@ ${pc13.bold("Providers:")}
|
|
|
22558
22901
|
openai OpenAI API key (platform.openai.com)
|
|
22559
22902
|
openai-oauth ChatGPT/Codex plan OAuth \u2014 sign in with clodex providers auth openai
|
|
22560
22903
|
opencode-go OpenCode Go API key \u2014 add with clodex providers add
|
|
22904
|
+
ollama-cloud Ollama Cloud API key \u2014 add with clodex providers add
|
|
22561
22905
|
custom-<name> Any OpenAI-compatible server (OpenRouter, LM Studio, \u2026) \u2014 add with clodex providers add
|
|
22562
22906
|
|
|
22563
22907
|
${pc13.bold("Model switching:")}
|
|
@@ -23742,14 +24086,11 @@ function isCliEntryPoint() {
|
|
|
23742
24086
|
}
|
|
23743
24087
|
}
|
|
23744
24088
|
if (isCliEntryPoint()) {
|
|
23745
|
-
|
|
23746
|
-
|
|
23747
|
-
|
|
23748
|
-
if (
|
|
23749
|
-
|
|
23750
|
-
}
|
|
23751
|
-
console.error(pc13.red("\nUnexpected error:"), err);
|
|
23752
|
-
process.exit(1);
|
|
24089
|
+
const exitOptions = { trace: Boolean(parseArgs(process.argv.slice(2)).trace) };
|
|
24090
|
+
main().then((exitCode) => exitAfterDrain(exitCode, exitOptions), (err) => {
|
|
24091
|
+
const cancelled = err === /* @__PURE__ */ Symbol.for("clack:cancel");
|
|
24092
|
+
if (!cancelled) console.error(pc13.red("\nUnexpected error:"), err);
|
|
24093
|
+
exitAfterDrain(cancelled ? 0 : 1, exitOptions);
|
|
23753
24094
|
});
|
|
23754
24095
|
}
|
|
23755
24096
|
export {
|