@bman654/clodex 2.15.0 → 2.16.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
@@ -276,6 +276,16 @@ Two things worth knowing about the numbers:
276
276
  number first only costs usable context. A provider that declares a share of its own
277
277
  is still honoured; clodex just never invents one. Use `--context` if you want a
278
278
  smaller window than the provider offers.
279
+ - **A window nobody published is not a ceiling.** Some servers list their models
280
+ without saying how much context each one takes. Clodex reports the 200,000 Claude
281
+ Code assumes anyway, but that number is a guess, not a measurement — so
282
+ `clodex models --context <model>=1m --save` raises it instead of being clamped back
283
+ down to the guess. Clamping still applies in full wherever a window *is* published,
284
+ by the provider or by clodex's own catalog. If the model is already baked into a
285
+ patched binary, re-run `clodex patch` so it picks the new window up. Set this model up
286
+ before this version? Run `clodex providers refresh-models <provider>` once — an older
287
+ clodex stored the guess as if the server had published it, and until you refresh it is
288
+ indistinguishable from a real limit and still clamps.
279
289
  - **The account ceiling moves.** It is server-side and per-account, and it has
280
290
  changed by more than 2x within a single day in the past. `max` reads whatever the
281
291
  catalog reports now and clamps to it, so a stale ceiling shrinks the stop rather
@@ -292,7 +302,7 @@ Two things worth knowing about the numbers:
292
302
  | `remove <id>` | Remove a provider by id |
293
303
  | `refresh-models [id]` | Update cached model lists |
294
304
 
295
- 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 and Chat Completions models; Responses-only entries are intentionally excluded. See [OpenCode Go provider](docs/opencode-go.md).
305
+ 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; entries whose transport has not been verified against the live endpoint are left out. See [OpenCode Go provider](docs/opencode-go.md).
296
306
 
297
307
  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: plain `http://` is accepted only after you confirm it, and only for loopback or private-network addresses, and an `https://` URL whose host is, or resolves to, a loopback (`127.0.0.0/8`, `::1`), private, link-local, unique-local, carrier-grade-NAT or well-known cloud-metadata address is refused. 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.
298
308
 
package/dist/cli.js CHANGED
@@ -346,7 +346,7 @@ import { join } from "path";
346
346
  // package.json
347
347
  var package_default = {
348
348
  name: "@bman654/clodex",
349
- version: "2.15.0",
349
+ version: "2.16.0",
350
350
  publishConfig: {
351
351
  access: "public"
352
352
  },
@@ -694,20 +694,22 @@ function getCacheIndex() {
694
694
  }
695
695
  return cacheIndex;
696
696
  }
697
- function contextWindowFromHeuristics(modelId) {
698
- const cached = heuristicCache.get(modelId);
699
- if (cached !== void 0) return cached;
697
+ function matchHeuristicWindow(modelId) {
698
+ if (heuristicCache.has(modelId)) return heuristicCache.get(modelId);
700
699
  for (const [pattern, size] of HEURISTIC_RULES) {
701
700
  if (pattern.test(modelId)) {
702
701
  heuristicCache.set(modelId, size);
703
702
  return size;
704
703
  }
705
704
  }
706
- heuristicCache.set(modelId, DEFAULT_CONTEXT_WINDOW);
707
- return DEFAULT_CONTEXT_WINDOW;
705
+ heuristicCache.set(modelId, void 0);
706
+ return void 0;
707
+ }
708
+ function lookupKnownContextWindow(modelId) {
709
+ return getCacheIndex().get(modelId) ?? matchHeuristicWindow(modelId);
708
710
  }
709
711
  function lookupContextWindow(modelId) {
710
- return getCacheIndex().get(modelId) ?? contextWindowFromHeuristics(modelId);
712
+ return lookupKnownContextWindow(modelId) ?? DEFAULT_CONTEXT_WINDOW;
711
713
  }
712
714
  function resolveContextWindow(modelId, explicit) {
713
715
  if (typeof explicit === "number" && explicit > 0) return explicit;
@@ -3160,17 +3162,19 @@ function parseContextStop(value) {
3160
3162
  return parsed;
3161
3163
  }
3162
3164
  function resolveContextStop(limits, stop = "standard") {
3163
- const standardRaw = positiveInteger(limits.contextWindow) ?? 0;
3164
- const ceiling = positiveInteger(limits.maxContextWindow) ?? standardRaw;
3165
- const requested = stop === "standard" ? standardRaw : stop === "max" ? ceiling : positiveInteger(stop) ?? standardRaw;
3166
- const raw = Math.min(requested, ceiling);
3165
+ const declaredWindow = positiveInteger(limits.contextWindow);
3166
+ const declaredCeiling = positiveInteger(limits.maxContextWindow);
3167
+ const standardRaw = declaredWindow ?? DEFAULT_CONTEXT_WINDOW;
3168
+ const ceiling = declaredCeiling ?? declaredWindow;
3169
+ const requested = stop === "standard" ? standardRaw : stop === "max" ? declaredCeiling ?? standardRaw : positiveInteger(stop) ?? standardRaw;
3170
+ const raw = ceiling === void 0 ? requested : Math.min(requested, ceiling);
3167
3171
  const effective = effectiveContextWindow(raw, limits.effectiveContextPercent);
3168
3172
  const boundary = positiveInteger(limits.pricingBoundary);
3169
3173
  return {
3170
3174
  stop,
3171
3175
  raw,
3172
3176
  effective,
3173
- ...requested > ceiling ? { clampedFrom: requested } : {},
3177
+ ...ceiling !== void 0 && requested > ceiling ? { clampedFrom: requested } : {},
3174
3178
  crossesPricingBoundary: boundary !== void 0 && effective > boundary
3175
3179
  };
3176
3180
  }
@@ -5208,6 +5212,52 @@ var opencode_go_models_default = [
5208
5212
  upstreamModelId: "minimax-m3",
5209
5213
  family: "minimax"
5210
5214
  },
5215
+ {
5216
+ id: "muse-spark-1.2-contributor",
5217
+ name: "Muse Spark 1.2 Contributor",
5218
+ contextWindow: 1048576,
5219
+ cost: {
5220
+ input: 0.1,
5221
+ output: 0.2,
5222
+ cache_read: 2e-3
5223
+ },
5224
+ modelFormat: "openai",
5225
+ npm: "@ai-sdk/openai",
5226
+ apiUrl: "https://opencode.ai/zen/go/v1",
5227
+ reasoning: true,
5228
+ modalities: [
5229
+ "text",
5230
+ "image"
5231
+ ],
5232
+ compatibility: {
5233
+ supportsReasoningEffort: false
5234
+ },
5235
+ upstreamModelId: "muse-spark-1.2-contributor",
5236
+ family: "muse"
5237
+ },
5238
+ {
5239
+ id: "muse-spark-1.3-contributor",
5240
+ name: "Muse Spark 1.3 Contributor",
5241
+ contextWindow: 1048576,
5242
+ cost: {
5243
+ input: 0.1,
5244
+ output: 0.2,
5245
+ cache_read: 2e-3
5246
+ },
5247
+ modelFormat: "openai",
5248
+ npm: "@ai-sdk/openai",
5249
+ apiUrl: "https://opencode.ai/zen/go/v1",
5250
+ reasoning: true,
5251
+ modalities: [
5252
+ "text",
5253
+ "image"
5254
+ ],
5255
+ compatibility: {
5256
+ supportsReasoningEffort: false
5257
+ },
5258
+ upstreamModelId: "muse-spark-1.3-contributor",
5259
+ family: "muse"
5260
+ },
5211
5261
  {
5212
5262
  id: "qwen3.6-plus",
5213
5263
  name: "Qwen3.6 Plus",
@@ -5471,6 +5521,10 @@ function openCodeGoPinnedApiUrl(npm) {
5471
5521
  if (npm === "@ai-sdk/anthropic") return OPENCODE_GO_ANTHROPIC_BASE_URL;
5472
5522
  return null;
5473
5523
  }
5524
+ function openCodeGoPinnedModelApiUrl(npm) {
5525
+ if (npm === "@ai-sdk/openai") return OPENCODE_GO_COMPLETIONS_BASE_URL;
5526
+ return openCodeGoPinnedApiUrl(npm);
5527
+ }
5474
5528
  function isProviderConfiguredForTemplate(provider, templateId) {
5475
5529
  if (provider.id === templateId) return true;
5476
5530
  return templateId === OPENCODE_GO_PROVIDER_ID && isRetainedOpenCodeGoProvider(provider);
@@ -5957,10 +6011,10 @@ var OPENAI_OAUTH_MODEL_SEEDS = [
5957
6011
  { id: "gpt-5", name: "GPT-5", contextWindow: 272e3, reasoning: true },
5958
6012
  // o-series reasoning (Plus+)
5959
6013
  { id: "o4-mini", name: "o4 Mini", reasoning: true },
5960
- { id: "o3", name: "o3", reasoning: true },
6014
+ { id: "o3", name: "o3", contextWindow: 2e5, reasoning: true },
5961
6015
  { id: "o3-mini", name: "o3 Mini", reasoning: true },
5962
- { id: "o1", name: "o1", reasoning: true },
5963
- { id: "o1-mini", name: "o1 Mini", reasoning: true }
6016
+ { id: "o1", name: "o1", contextWindow: 2e5, reasoning: true },
6017
+ { id: "o1-mini", name: "o1 Mini", contextWindow: 128e3, reasoning: true }
5964
6018
  ];
5965
6019
  function hasPricingBoundary(id) {
5966
6020
  const version = /^gpt-(\d+)(?:\.(\d+))?(?:-|$)/i.exec(id);
@@ -6008,7 +6062,7 @@ function buildOpenAiOAuthModels() {
6008
6062
  upstreamModelId: seed.id,
6009
6063
  family: prefix,
6010
6064
  brand: deriveBrand(prefix),
6011
- contextWindow: resolveContextWindow(seed.id, seed.contextWindow),
6065
+ contextWindow: seed.contextWindow ?? lookupKnownContextWindow(seed.id),
6012
6066
  maxContextWindow: seed.maxContextWindow,
6013
6067
  effectiveContextPercent: seed.effectiveContextPercent,
6014
6068
  pricingBoundary: seed.pricingBoundary ?? pricing.pricingBoundary,
@@ -6524,7 +6578,9 @@ function parseModelList(body, npm, allowBareArray) {
6524
6578
  const freeStatus = classifyFreeStatus({
6525
6579
  model: { cost, isFree: row.isFree }
6526
6580
  });
6527
- const contextWindow = row.context_length ?? row.contextWindow ?? row.context_window ?? row.max_model_len ?? resolveContextWindow(id);
6581
+ const contextWindow = row.context_length ?? row.contextWindow ?? row.context_window ?? row.max_model_len ?? // Left undefined when nothing is known: persisting the invented 200,000
6582
+ // would make it this model's permanent ceiling.
6583
+ lookupKnownContextWindow(id);
6528
6584
  models.push({
6529
6585
  id,
6530
6586
  name: normalizeGoogleDisplayName(row.name, id),
@@ -6556,7 +6612,7 @@ function materializeTemplateModel(template, model, baseUrl) {
6556
6612
  upstreamModelId: model.upstreamModelId ?? normalizedUpstream,
6557
6613
  family,
6558
6614
  brand: model.brand ?? deriveBrand(family),
6559
- contextWindow: model.contextWindow ?? resolveContextWindow(id),
6615
+ contextWindow: model.contextWindow ?? lookupKnownContextWindow(id),
6560
6616
  isFree: model.isFree ?? isFreeStatus(freeStatus),
6561
6617
  freeStatus,
6562
6618
  modelFormat: model.modelFormat ?? modelFormatForNpm(npm),
@@ -6758,10 +6814,10 @@ function resolveMaterializedApiUrl(cached, provider, npm) {
6758
6814
  if (!isRetainedOpenCodeGoProvider(provider)) {
6759
6815
  return cached.apiUrl ?? provider.api.url ?? "";
6760
6816
  }
6761
- return openCodeGoPinnedApiUrl(npm);
6817
+ return openCodeGoPinnedModelApiUrl(npm);
6762
6818
  }
6763
6819
  function projectContextStop(cached, providerId, modelId) {
6764
- const limits = contextLimitsFrom(cached, resolveContextWindow(modelId));
6820
+ const limits = contextLimitsFrom(cached, lookupKnownContextWindow(modelId));
6765
6821
  const stop = selectContextStop(providerId, modelId);
6766
6822
  const resolved = resolveContextStop(limits, stop);
6767
6823
  return {
@@ -7893,7 +7949,7 @@ function outboundHttpProxyAgent(targetUrl, env = process.env) {
7893
7949
  }
7894
7950
  return new HttpsProxyAgent(parsedProxy, { keepAlive: true });
7895
7951
  } catch (err) {
7896
- console.error(
7952
+ emitParentNotice(
7897
7953
  `clodex: HTTP(S)_PROXY cannot be used for a CONNECT tunnel; using a direct connection (${err instanceof Error ? err.message : String(err)})`
7898
7954
  );
7899
7955
  return void 0;
@@ -10777,7 +10833,7 @@ async function createLanguageModel(spec) {
10777
10833
  apiKey: "",
10778
10834
  ...spec.headers ? { headers: spec.headers } : {},
10779
10835
  fetch: fetchWithoutCredentialHeaders
10780
- } : { apiKey, ...spec.headers ? { headers: spec.headers } : {} };
10836
+ } : { apiKey, ...baseURL ? { baseURL } : {}, ...spec.headers ? { headers: spec.headers } : {} };
10781
10837
  const openai = createOpenAI(oauthOptions);
10782
10838
  return useResponsesEndpoint ? openai.responses(modelId) : openai.chat(modelId);
10783
10839
  }
@@ -14109,7 +14165,7 @@ function buildDesiredPatchConfig(options = {}) {
14109
14165
  compatibility: model.compatibility,
14110
14166
  upstreamModelId: upstreamModelId2
14111
14167
  });
14112
- const limits = contextLimitsFrom(model, resolveContextWindow(model.id));
14168
+ const limits = contextLimitsFrom(model, lookupKnownContextWindow(model.id));
14113
14169
  const stop = resolveContextStop(
14114
14170
  limits,
14115
14171
  options.sessionStops ? selectContextStop(provider.id, model.id, prefs.modelContextModes) : readContextStop(prefs.modelContextModes, provider.id, model.id) ?? "standard"
@@ -14678,7 +14734,9 @@ async function runPatchCommand(opts = {}) {
14678
14734
  return 1;
14679
14735
  }
14680
14736
  for (const id of desired.unknownWindows) {
14681
- p2.log.warn(`No context window metadata for ${id} \u2014 Claude Code will assume the 200k default.`);
14737
+ p2.log.warn(
14738
+ `No context window metadata for ${id} \u2014 Claude Code will assume the 200k default. Refresh the provider's models, then set a window with \`clodex models --context <model=stop> --save\`, then re-run \`clodex patch\`.`
14739
+ );
14682
14740
  }
14683
14741
  reportRejectedModelAliases(desired.rejectedAliasRejections);
14684
14742
  const localPatches = inspectLocalPatchSource(
@@ -15781,7 +15839,7 @@ async function fetchAnthropicModels(baseUrl, apiKey, extraHeaders) {
15781
15839
  upstreamModelId: id,
15782
15840
  family: id.split("-")[0] ?? id,
15783
15841
  brand: deriveBrand(id),
15784
- contextWindow: resolveContextWindow(id),
15842
+ contextWindow: lookupKnownContextWindow(id),
15785
15843
  modelFormat: "anthropic",
15786
15844
  npm: "@ai-sdk/anthropic",
15787
15845
  apiUrl: root
@@ -16328,7 +16386,7 @@ function buildDynamicOAuthModel(entry, seedById, codexCatalog) {
16328
16386
  upstreamModelId: id,
16329
16387
  family: prefix,
16330
16388
  brand: deriveBrand(prefix),
16331
- contextWindow: entry.context_window ?? resolveContextWindow(id),
16389
+ contextWindow: entry.context_window ?? lookupKnownContextWindow(id),
16332
16390
  maxContextWindow: entry.max_context_window,
16333
16391
  // Absent means no reduction. clodex reports the window the provider actually
16334
16392
  // gives; deciding how much of it to leave free is the client's job, and Claude
@@ -20433,6 +20491,14 @@ async function startHttpProxy(options) {
20433
20491
  );
20434
20492
  });
20435
20493
  const sockets = /* @__PURE__ */ new Set();
20494
+ let warnedConnectSelfProxy = false;
20495
+ const connectTunnelAgents = /* @__PURE__ */ new Map();
20496
+ const connectTunnelAgent = (proxyUrl, targetUrl) => {
20497
+ if (!connectTunnelAgents.has(proxyUrl)) {
20498
+ connectTunnelAgents.set(proxyUrl, outboundHttpProxyAgent(targetUrl));
20499
+ }
20500
+ return connectTunnelAgents.get(proxyUrl);
20501
+ };
20436
20502
  mitmServer.on("upgrade", (req, socket, head) => {
20437
20503
  forwardAnthropicUpgrade(
20438
20504
  req,
@@ -20462,6 +20528,65 @@ async function startHttpProxy(options) {
20462
20528
  clientSocket.end("HTTP/1.1 400 Bad Request\r\n\r\n");
20463
20529
  return;
20464
20530
  }
20531
+ const targetUrl = `https://${req.url}`;
20532
+ const outboundProxyUrl = outboundProxyUrlForTarget(targetUrl);
20533
+ const bound = proxyServer.address();
20534
+ const selfTargeting = outboundProxyUrl !== void 0 && bound !== null && typeof bound !== "string" && proxyUrlTargetsListener(outboundProxyUrl, bound.address, bound.port);
20535
+ if (selfTargeting && !warnedConnectSelfProxy) {
20536
+ warnedConnectSelfProxy = true;
20537
+ emitParentNotice(
20538
+ "clodex: HTTP(S)_PROXY points at this proxy; tunneling CONNECT direct"
20539
+ );
20540
+ }
20541
+ const outboundAgent = outboundProxyUrl !== void 0 && !selfTargeting ? connectTunnelAgent(outboundProxyUrl, targetUrl) : void 0;
20542
+ if (outboundAgent) {
20543
+ let upstream2;
20544
+ let proxyConnectStatus;
20545
+ req.once("proxyConnect", (response) => {
20546
+ proxyConnectStatus = response.statusCode;
20547
+ });
20548
+ clientSocket.once("close", () => {
20549
+ if (upstream2 && !upstream2.destroyed) upstream2.destroy();
20550
+ });
20551
+ void outboundAgent.connect(req, {
20552
+ host: target.host,
20553
+ port: target.port,
20554
+ secureEndpoint: false
20555
+ }).then((connected) => {
20556
+ upstream2 = connected;
20557
+ if (proxyConnectStatus !== 200 || clientSocket.destroyed) {
20558
+ connected.destroy();
20559
+ if (!clientSocket.destroyed) {
20560
+ clientSocket.end("HTTP/1.1 502 Bad Gateway\r\n\r\n", () => clientSocket.destroy());
20561
+ }
20562
+ return;
20563
+ }
20564
+ let tunnelEstablished2 = false;
20565
+ sockets.add(connected);
20566
+ connected.once("close", () => {
20567
+ sockets.delete(connected);
20568
+ if (tunnelEstablished2 && !clientSocket.destroyed) clientSocket.destroy();
20569
+ });
20570
+ connected.once("error", () => {
20571
+ if (clientSocket.destroyed) return;
20572
+ if (tunnelEstablished2) {
20573
+ clientSocket.destroy();
20574
+ return;
20575
+ }
20576
+ clientSocket.end("HTTP/1.1 502 Bad Gateway\r\n\r\n", () => clientSocket.destroy());
20577
+ });
20578
+ tunnelEstablished2 = true;
20579
+ clientSocket.write("HTTP/1.1 200 Connection Established\r\n\r\n");
20580
+ if (head.length > 0) connected.write(head);
20581
+ clientSocket.pipe(connected);
20582
+ connected.pipe(clientSocket);
20583
+ }).catch(() => {
20584
+ if (!clientSocket.destroyed) {
20585
+ clientSocket.end("HTTP/1.1 502 Bad Gateway\r\n\r\n", () => clientSocket.destroy());
20586
+ }
20587
+ });
20588
+ return;
20589
+ }
20465
20590
  const upstream = net.connect(target.port, target.host);
20466
20591
  let tunnelEstablished = false;
20467
20592
  sockets.add(upstream);
@@ -20527,6 +20652,8 @@ async function startHttpProxy(options) {
20527
20652
  for (const socket of sockets) socket.destroy();
20528
20653
  await new Promise((resolve4) => proxyServer.close(() => resolve4()));
20529
20654
  mitmServer.close();
20655
+ for (const agent of connectTunnelAgents.values()) agent?.destroy();
20656
+ connectTunnelAgents.clear();
20530
20657
  anthropicAgent?.destroy();
20531
20658
  adapter?.close();
20532
20659
  }
@@ -22242,7 +22369,7 @@ function applyContextStopAssignments(assignments, save, report, saveHint = "Add
22242
22369
  );
22243
22370
  return 1;
22244
22371
  }
22245
- const limits = contextLimitsFrom(cached, resolveContextWindow(assignment.modelId));
22372
+ const limits = contextLimitsFrom(cached, lookupKnownContextWindow(assignment.modelId));
22246
22373
  const resolved = resolveContextStop(limits, assignment.stop ?? "standard");
22247
22374
  const target = modelAliasTarget(assignment);
22248
22375
  const label = assignment.label === target ? target : `${assignment.label} (${target})`;