@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 CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/%40bman654%2Fclodex.svg)](https://www.npmjs.com/package/@bman654/clodex)
4
4
 
5
- **clodex** lets you use your ChatGPT/Codex plan, OpenAI API models, or OpenCode Go models with Claude Code as first-class model choices.
5
+ **clodex** lets you use your ChatGPT/Codex plan, OpenAI API models, OpenCode Go or Ollama Cloud models, or models from a custom OpenAI-compatible server with Claude Code as first-class model choices.
6
6
  You can use them anywhere you use Anthropic models like Opus and Sonnet — as the main session model, and in subagents, workflows, and agent teams. Clodex integrates them directly into Claude Code, using Claude Code's system prompt.
7
7
  It works with your existing Claude Code plan as well as your Codex plans WITHOUT violating Anthropic's ToS.
8
8
  No messing with CMUX or child codex processes or any of that stuff.
@@ -43,6 +43,7 @@ clodex claude # 5. launch Claude Code on an OpenAI model
43
43
  | OpenAI (ChatGPT / Codex plan) | OAuth | Fully supported by the clodex maintainer |
44
44
  | OpenCode Go | API key | Community-supported — maintained by its contributor |
45
45
  | Custom OpenAI-compatible server (OpenRouter, vLLM, …) | API key, or none | Community-supported — maintained by its contributor |
46
+ | Ollama Cloud | API key | Community-supported — maintained by its contributor |
46
47
 
47
48
  **Community-supported** means the maintainer holds no account for that service,
48
49
  so it cannot be exercised against the live API here or debugged when the vendor
@@ -124,7 +125,7 @@ Launch Claude Code bridged to configured model providers. Unrecognized flags (an
124
125
  | `--dry-run` | Run the wizard but print a launch preview instead of launching (never persists anything) |
125
126
  | `--trace` | Write debug logs to `~/.clodex/logs/` and show errors on exit |
126
127
  | `--fast` | Request Codex fast mode (`service_tier=priority`) for ChatGPT/Codex OAuth routes; equivalent to `CLODEX_SERVICE_TIER=fast` |
127
- | `--provider <id>` | Endpoint mode only: boot provider id (`openai`, `openai-oauth`, or `opencode-go`); with `--model`, skips the wizard |
128
+ | `--provider <id>` | Endpoint mode only: boot provider id of a configured provider (e.g. `openai`, `openai-oauth`, `opencode-go`, `ollama-cloud`, or `custom-<name>`); with `--model`, skips the wizard |
128
129
  | `--model <id>` | Endpoint mode only: boot model id; with `--provider`, skips the wizard |
129
130
  | `--context <model=stop>` | Use a different share of a model's context window for this launch only; never saved. Reaches Claude Code through the exported catalog, so a binary patched by `clodex patch` keeps its baked window until the stop is saved and the patch re-run |
130
131
  | `--help`, `--version` | Help / version |
@@ -304,7 +305,7 @@ Two things worth knowing about the numbers:
304
305
  | Subcommand | Effect |
305
306
  | --- | --- |
306
307
  | *(none)* | Provider hub wizard |
307
- | `add` | Add OpenAI or OpenCode Go with an API key, add a custom OpenAI-compatible server by base URL, or sign in with ChatGPT |
308
+ | `add` | Add OpenAI, OpenCode Go or Ollama Cloud with an API key, add a custom OpenAI-compatible server by base URL, or sign in with ChatGPT |
308
309
  | `auth openai` | Sign in with ChatGPT/Codex-plan OAuth (device code; `--browser` for workspaces that disable device codes) |
309
310
  | `list` | Show configured providers |
310
311
  | `remove <id>` | Remove a provider by id |
@@ -324,7 +325,9 @@ and their seeded flags when discovery omits them. Models not using Responses-Lit
324
325
  this version header and are not hidden by this check. This check does not establish your
325
326
  account's model entitlement.
326
327
 
327
- Providers supported: `openai` (API key, platform.openai.com), `openai-oauth` (ChatGPT/Codex plan), and `opencode-go` (OpenCode Go API key). OpenCode Go exposes its Anthropic Messages, Chat Completions, and Responses models; only entries whose transport has been verified, live or against OpenCode's per-model endpoint table, are included. See [OpenCode Go provider](docs/opencode-go.md).
328
+ Providers supported: `openai` (API key, platform.openai.com), `openai-oauth` (ChatGPT/Codex plan), `opencode-go` (OpenCode Go API key), and `ollama-cloud` (Ollama Cloud API key). OpenCode Go exposes its Anthropic Messages, Chat Completions, and Responses models; only entries whose transport has been verified, live or against OpenCode's per-model endpoint table, are included. See [OpenCode Go provider](docs/opencode-go.md).
329
+
330
+ Ollama Cloud exposes a curated set of four Chat Completions models from `https://ollama.com/v1`; other models Ollama Cloud lists are hidden. See [Ollama Cloud provider](docs/ollama-cloud.md).
328
331
 
329
332
  A **custom OpenAI-compatible server** is any OpenAI-style API, given by its base URL (the part before `/chat/completions`), such as `https://openrouter.ai/api/v1`. `providers add` asks for a name, the base URL and an API key (leave it empty for a local server without auth), lists the server's models to check the connection, and saves the provider as `custom-<name>`. Its models then appear in `clodex models`; once saved as favorites they are addressed as `clodex:custom-<name>:<model>` and can be given a short alias with `clodex models --alias`. The base URL is checked before anything is saved and again when its models are refreshed: plain `http://` is accepted only after you confirm it, and only if every address the host resolves to is on this machine, your LAN or a private network such as Tailscale (loopback, private, unique-local IPv6 or carrier-grade-NAT `100.64.0.0/10` addresses); `https://` is accepted on any of those networks as well as the public internet; and a link-local (`169.254.0.0/16`, `fe80::/10`) or well-known cloud-metadata address is refused whether or not HTTP was approved. A server on another machine in your tailnet can be added by its `100.x` address, its MagicDNS name, or the HTTPS name `tailscale serve` gives it. A model's id and name are stored with surrounding whitespace trimmed, and a model whose trimmed id or name still contains a control character is skipped, whether the list is fetched by `providers add` or by `providers refresh-models`, and error text from the server is shown with control characters replaced by spaces. Only the OpenAI-compatible kind is offered in the menu.
330
333
 
@@ -457,8 +460,76 @@ clodex --version # version
457
460
  `CLODEX_UPSTREAM_MAX_RETRIES` value takes precedence on the HTTP MITM path;
458
461
  otherwise, Claude Code's own `CLAUDE_CODE_MAX_RETRIES=0` disables that replay
459
462
  so telling the client never to resend a request is not quietly undone one
460
- layer down.
463
+ layer down. On that path the retry setting controls only this pooled-connection
464
+ replay. A separate outage hold, with its own setting, also runs there; see
465
+ the next entry.
461
466
  Recovered requests appear in the inference log as `response_retried`.
467
+ - **Outage hold (proxy mode):** when a Claude Code request cannot reach
468
+ `api.anthropic.com` at all, clodex keeps retrying it for a while instead of
469
+ answering 502 at once, so a laptop waking from sleep, a Wi-Fi switch or a VPN
470
+ reconnect does not end the turn. A failure qualifies when the name does not
471
+ resolve, the connection is refused or has no route, or the connection is
472
+ reset or times out before its TLS handshake completes. clodex sends the
473
+ request only after the handshake, so none of these attempts sent anything
474
+ and retrying cannot submit the request twice. A failure after the handshake,
475
+ a certificate error, and a reset on a reused connection are not held. Retries
476
+ wait 1, 2, 4 and 8 seconds, then 15 seconds between attempts. The hold is a
477
+ deadline counted from when the request arrived: at the deadline clodex drops
478
+ any attempt that has not finished its handshake and answers 502, with a
479
+ message saying the outage hold expired and, if there was one, naming the last
480
+ upstream error.
481
+ If a reused connection is reset after the deadline and clodex replays the
482
+ request, the replay gets a 502 at once unless it can go out on another
483
+ reused connection.
484
+ `CLODEX_PASSTHROUGH_OUTAGE_HOLD_MS` sets the deadline in milliseconds. The
485
+ default is `120000` (2 minutes) and `0` turns the hold off. Values above
486
+ `570000` clamp to it with a one-time warning; malformed values are reported
487
+ once and ignored. Only messages generation (`/v1/messages`, including
488
+ `/v1/messages?beta=true`) is held. `count_tokens`, OAuth, startup and
489
+ telemetry calls still get their 502 at once. Each held retry appears in the
490
+ inference log as `response_retried` with `outageHold: true`.
491
+ Claude Code resends a 502 itself, so at its default of 10 retries a turn can
492
+ last through roughly 25 minutes of outage at the default hold. That figure is a
493
+ projection from Claude Code's retry schedule, not a measurement. Claude Code
494
+ can also give up sooner than the hold. It waits 180 seconds plus 1 second per
495
+ 32 KiB of request body for a query's first response headers. After one
496
+ request gets no response, later requests in the query get `API_TIMEOUT_MS`
497
+ minus 1 second (599 seconds at its default), and a second request with no
498
+ response ends the turn even with retries left. At Claude Code's defaults the
499
+ default hold answers inside the first window, so a request still waiting for
500
+ its connection gets its 502 before either limit. A hold above about
501
+ 180 seconds means the query's first held request is cut off by Claude Code
502
+ instead, and the hold must then stay below the longer window; the `570000`
503
+ ceiling does that at Claude Code's default `API_TIMEOUT_MS`. If you lower
504
+ `API_TIMEOUT_MS` in Claude Code's environment so that `API_TIMEOUT_MS` minus
505
+ 1 second is shorter than the hold, every turn whose outage outlasts both
506
+ windows ends on that second no-response after about min(180 s plus the body
507
+ allowance, `API_TIMEOUT_MS` − 1 s) + (`API_TIMEOUT_MS` − 1 s); keep the hold
508
+ below `API_TIMEOUT_MS` − 1 s, or set it to `0`. For
509
+ `0 < API_TIMEOUT_MS < 11000`, Claude Code skips both of those waits for
510
+ response headers, but its own request timeout still applies: if that is
511
+ shorter than the hold, it aborts each held request before clodex can return
512
+ its 502.
513
+ The cost is a longer wait when the outage does not end: with the hold off,
514
+ Claude Code gives up on a persistent fast-failing outage after about 3
515
+ minutes, and at the default hold after about 25 (both projections).
516
+ The hold has its own switch. `CLODEX_UPSTREAM_MAX_RETRIES=0` and
517
+ `CLAUDE_CODE_MAX_RETRIES=0` do not turn it off, because a held request was
518
+ never sent. If you want outages to fail fast, for example in a headless
519
+ `claude -p` or CI run that would otherwise wait through each hold unless its
520
+ caller sets a limit, also set `CLODEX_PASSTHROUGH_OUTAGE_HOLD_MS=0`. The
521
+ server process reads the variable: with a standalone `clodex server`, set it
522
+ when starting the server, because setting it on `clodex-claude` changes
523
+ nothing. When clodex itself uses `HTTPS_PROXY`, the hold covers an
524
+ unreachable proxy and a tunnel that fails before its TLS handshake completes.
525
+ When the proxy is up but Anthropic is not, the proxy usually answers clodex's
526
+ `CONNECT` with an error status, and that error response comes back at once,
527
+ as it does with the hold off. If the proxy closes the connection before
528
+ replying, clodex answers 502 at once; if it resets the connection instead,
529
+ the request is held as for an unreachable proxy. A replay after the deadline
530
+ that needs a new tunnel waits for the proxy's `CONNECT` reply and is not cut
531
+ while it waits, so a proxy that never replies leaves that request with no
532
+ response, and an error reply to it gets clodex's 502 instead of the proxy's.
462
533
  - **Connection pacing (ChatGPT/Codex plans):** when many agents run at once,
463
534
  clodex spaces out the new connections it opens to OpenAI, which should make a
464
535
  burst of parallel work less likely to trip OpenAI's own rate limit. (In the
@@ -282,8 +282,19 @@ function tryAcquireRegistryLock(lockPath = getRegistryLockPath(), options = {})
282
282
  }
283
283
  return null;
284
284
  }
285
- function sleep(ms) {
286
- return new Promise((resolve) => setTimeout(resolve, ms));
285
+ function sleep(ms, signal) {
286
+ if (!signal) return new Promise((resolve) => setTimeout(resolve, ms));
287
+ return new Promise((resolve, reject) => {
288
+ const onAbort = () => {
289
+ clearTimeout(timer);
290
+ reject(signal.reason);
291
+ };
292
+ const timer = setTimeout(() => {
293
+ signal.removeEventListener("abort", onAbort);
294
+ resolve();
295
+ }, ms);
296
+ signal.addEventListener("abort", onAbort, { once: true });
297
+ });
287
298
  }
288
299
  function sleepSync(ms) {
289
300
  Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
@@ -314,6 +325,7 @@ async function withRegistryWriteLock(operation, options = {}) {
314
325
  const deadline = now() + waitMs;
315
326
  let lease = null;
316
327
  while (!lease) {
328
+ options.signal?.throwIfAborted();
317
329
  lease = tryAcquireRegistryLock(lockPath, {
318
330
  now,
319
331
  isAlive: options.isAlive
@@ -321,7 +333,7 @@ async function withRegistryWriteLock(operation, options = {}) {
321
333
  if (lease) break;
322
334
  if (now() >= deadline)
323
335
  throw lockTimeoutError(lockPath, waitMs, options.isAlive ?? isPidAlive);
324
- await sleep(retryMs);
336
+ await sleep(retryMs, options.signal);
325
337
  }
326
338
  const leases = new Map(inheritedLeases);
327
339
  leases.set(lockPath, lease);
@@ -1749,6 +1761,63 @@ function computeWrapperEnv(baseEnv, state) {
1749
1761
  return env;
1750
1762
  }
1751
1763
 
1764
+ // src/process-exit.ts
1765
+ import { writeSync as writeSync3 } from "fs";
1766
+ var EXIT_DRAIN_GRACE_MS = 1e3;
1767
+ var exitCancellers = /* @__PURE__ */ new Set();
1768
+ var exitRequested = false;
1769
+ function cancelOnExit(cancel) {
1770
+ if (exitRequested) {
1771
+ runCanceller(cancel);
1772
+ return () => {
1773
+ };
1774
+ }
1775
+ exitCancellers.add(cancel);
1776
+ return () => {
1777
+ exitCancellers.delete(cancel);
1778
+ };
1779
+ }
1780
+ function runCanceller(cancel) {
1781
+ try {
1782
+ cancel();
1783
+ } catch {
1784
+ }
1785
+ }
1786
+ function describeActiveResources(resources = process.getActiveResourcesInfo()) {
1787
+ if (resources.length === 0) return "nothing reported";
1788
+ const counts = /* @__PURE__ */ new Map();
1789
+ for (const resource of resources) counts.set(resource, (counts.get(resource) ?? 0) + 1);
1790
+ return [...counts].map(([name, count]) => count > 1 ? `${name} x${count}` : name).join(", ");
1791
+ }
1792
+ function exitAfterDrain(code, options = {}) {
1793
+ if (exitRequested) return;
1794
+ exitRequested = true;
1795
+ process.exitCode = code;
1796
+ const ignoreStdioError = () => {
1797
+ };
1798
+ process.stdout.on("error", ignoreStdioError);
1799
+ process.stderr.on("error", ignoreStdioError);
1800
+ const cancellers = [...exitCancellers];
1801
+ exitCancellers.clear();
1802
+ for (const cancel of cancellers) runCanceller(cancel);
1803
+ const graceMs = options.graceMs ?? EXIT_DRAIN_GRACE_MS;
1804
+ const fallback = setTimeout(() => {
1805
+ if (options.trace || process.env.CLODEX_TRACE === "1") {
1806
+ const label = options.label ?? "clodex";
1807
+ try {
1808
+ writeSync3(
1809
+ 2,
1810
+ `${label}: event loop still busy ${graceMs}ms after exit was requested; exiting with process.exit(${code}). Still active: ${describeActiveResources()}
1811
+ `
1812
+ );
1813
+ } catch {
1814
+ }
1815
+ }
1816
+ process.exit(code);
1817
+ }, graceMs);
1818
+ fallback.unref();
1819
+ }
1820
+
1752
1821
  // src/patch-manifest.ts
1753
1822
  import { readFileSync as readFileSync5 } from "fs";
1754
1823
  import { join as join5 } from "path";
@@ -2204,6 +2273,8 @@ export {
2204
2273
  planPristineSource,
2205
2274
  planInspectedPristineSource,
2206
2275
  planRestoreOnly,
2207
- collectPristineFacts
2276
+ collectPristineFacts,
2277
+ cancelOnExit,
2278
+ exitAfterDrain
2208
2279
  };
2209
- //# sourceMappingURL=chunk-VFYRDLDD.js.map
2280
+ //# sourceMappingURL=chunk-Q2YFYQQT.js.map