@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/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-VFYRDLDD.js";
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 forward = (signal) => {
327
- child.kill(signal);
328
+ const forwardSigint = () => {
329
+ child.kill("SIGINT");
328
330
  };
329
- process.once("SIGINT", () => forward("SIGINT"));
330
- process.once("SIGTERM", () => forward("SIGTERM"));
331
- child.on("exit", (code) => {
332
- restore();
333
- resolve4(code ?? 0);
334
- });
335
- child.on("error", (err) => {
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(1);
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.18.10",
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(() => onComplete?.(false));
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
- void (async () => {
5949
- const updated = await fetchModelsDevCache() !== null;
5950
- onComplete?.(updated);
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
- try {
9787
- entry.socket.close();
9788
- } catch {
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) => attempt <= retryBudget && !headersReceived && !failed && !clientDisconnected2 && !isLocalShutdown() && request3.reusedSocket === true && err.code === RETRYABLE_PASSTHROUGH_CODE;
20282
- const sendAttempt = () => {
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
- failed = true;
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 and OpenCode Go models.
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
- main().then((exitCode) => {
23746
- process.exit(exitCode);
23747
- }).catch((err) => {
23748
- if (err === /* @__PURE__ */ Symbol.for("clack:cancel")) {
23749
- process.exit(0);
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 {