@bman654/clodex 2.18.5 → 2.18.7

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
@@ -74,7 +74,7 @@ import {
74
74
  withRegistryWriteLockSync,
75
75
  writeParentNoticeLines,
76
76
  writeParentNoticeLinesSync
77
- } from "./chunk-YGEPQOPA.js";
77
+ } from "./chunk-VFYRDLDD.js";
78
78
 
79
79
  // src/cli.ts
80
80
  import pc13 from "picocolors";
@@ -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.18.5",
349
+ version: "2.18.7",
350
350
  publishConfig: {
351
351
  access: "public"
352
352
  },
@@ -6062,16 +6062,33 @@ function compareCodexClientVersions(a, b) {
6062
6062
  }
6063
6063
  return 0;
6064
6064
  }
6065
- function requiresNewerCodexClient(model) {
6066
- if (!model.useResponsesLite) return false;
6065
+ function atLeast(version, floor) {
6066
+ const order = compareCodexClientVersions(version, floor);
6067
+ return order === 0 || order === 1;
6068
+ }
6069
+ function codexClientRequirement(model) {
6070
+ if (!model.useResponsesLite) return void 0;
6067
6071
  const minimum = readCodexClientVersion(model.minimalClientVersion);
6068
- return minimum !== void 0 && compareCodexClientVersions(minimum, CODEX_RESPONSES_LITE_VERSION) === 1;
6072
+ const withheldAt = readCodexClientVersion(model.withheldAtClientVersion);
6073
+ const blockingMinimum = minimum !== void 0 && compareCodexClientVersions(minimum, CODEX_RESPONSES_LITE_VERSION) === 1 ? minimum : void 0;
6074
+ const blockingWithheld = withheldAt !== void 0 && atLeast(withheldAt, CODEX_RESPONSES_LITE_VERSION) ? withheldAt : void 0;
6075
+ if (blockingMinimum !== void 0 && (blockingWithheld === void 0 || compareCodexClientVersions(blockingMinimum, blockingWithheld) === 1)) {
6076
+ return { kind: "minimum", version: blockingMinimum };
6077
+ }
6078
+ return blockingWithheld === void 0 ? void 0 : { kind: "withheld", version: blockingWithheld };
6079
+ }
6080
+ function requiresNewerCodexClient(model) {
6081
+ return codexClientRequirement(model) !== void 0;
6069
6082
  }
6070
6083
  function codexClientVersionWarning(models) {
6071
- const unavailable = models.filter(requiresNewerCodexClient);
6084
+ const unavailable = models.flatMap((model) => {
6085
+ const requirement = codexClientRequirement(model);
6086
+ return requirement ? [{ id: model.id, requirement }] : [];
6087
+ });
6072
6088
  if (unavailable.length === 0) return void 0;
6073
- const details = unavailable.map((model) => `${printableServerText(model.id)} (requires ${model.minimalClientVersion})`).join(", ");
6074
- return `Hidden ChatGPT-plan models: ${details}. clodex sends Codex client version ${CODEX_RESPONSES_LITE_VERSION} for Responses-Lite; their catalog minimum exceeds this version. Update clodex to a release supporting their required version to use them.`;
6089
+ const details = unavailable.map(({ id, requirement }) => `${printableServerText(id)} (requires ${requirement.kind === "minimum" ? requirement.version : `a version newer than ${requirement.version}`})`).join(", ");
6090
+ const reason = unavailable.every(({ requirement }) => requirement.kind === "minimum") ? "their catalog minimum exceeds this version" : "the ChatGPT catalog does not offer them to this version";
6091
+ return `Hidden ChatGPT-plan models: ${details}. clodex sends Codex client version ${CODEX_RESPONSES_LITE_VERSION} for Responses-Lite; ${reason}. Update clodex to a release supporting their required version to use them.`;
6075
6092
  }
6076
6093
 
6077
6094
  // src/data/openai-oauth-models.ts
@@ -15992,21 +16009,17 @@ var BLOCKED_HOSTNAMES = /* @__PURE__ */ new Set([
15992
16009
  "169.254.169.254",
15993
16010
  "metadata.google.internal",
15994
16011
  "169.254.170.2",
15995
- "fd00:ec2::254"
16012
+ "fd00:ec2::254",
16013
+ "100.100.100.200",
16014
+ // Alibaba Cloud ECS
16015
+ "fd20:ce::254"
16016
+ // Google Cloud IPv6
15996
16017
  ]);
15997
- function isBlockedIp(ipStr, allowInsecureLocal) {
16018
+ var NON_PUBLIC_RANGES = /* @__PURE__ */ new Set(["loopback", "private", "uniqueLocal", "carrierGradeNat"]);
16019
+ function isBlockedIp(ipStr) {
15998
16020
  try {
15999
16021
  const ip = ipaddr.process(ipStr);
16000
- const range = ip.range();
16001
- if (allowInsecureLocal && (range === "loopback" || range === "private")) {
16002
- return false;
16003
- }
16004
- if (range === "loopback") return true;
16005
- if (range === "private") return true;
16006
- if (range === "linkLocal") return true;
16007
- if (range === "uniqueLocal") return true;
16008
- if (range === "carrierGradeNat") return true;
16009
- return false;
16022
+ return ip.range() === "linkLocal" || BLOCKED_HOSTNAMES.has(ip.toString());
16010
16023
  } catch {
16011
16024
  return true;
16012
16025
  }
@@ -16040,10 +16053,10 @@ async function validateCustomEndpointUrl(rawUrl, opts = {}) {
16040
16053
  return {
16041
16054
  ok: false,
16042
16055
  error: "Only HTTPS URLs are allowed.",
16043
- hint: "For local or LAN servers (Ollama, LM Studio, vLLM), allow insecure HTTP when prompted."
16056
+ hint: "For a server on this machine, your LAN or Tailscale (Ollama, LM Studio, vLLM), allow insecure HTTP when prompted."
16044
16057
  };
16045
16058
  } else if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
16046
- return { ok: false, error: "URL must use https:// or user-approved http:// for local/LAN servers." };
16059
+ return { ok: false, error: "URL must use https:// or user-approved http:// for local, LAN or Tailscale servers." };
16047
16060
  }
16048
16061
  const rawHostname = parsed.hostname.toLowerCase();
16049
16062
  const hostname = rawHostname.replace(/^\[(.*)\]$/, "$1");
@@ -16051,7 +16064,7 @@ async function validateCustomEndpointUrl(rawUrl, opts = {}) {
16051
16064
  return {
16052
16065
  ok: false,
16053
16066
  error: "This URL points to a blocked internal/metadata host.",
16054
- hint: "Use a public API endpoint for your provider."
16067
+ hint: "Use your model server's own loopback, LAN, Tailscale or public address."
16055
16068
  };
16056
16069
  }
16057
16070
  const addresses = await resolveHostAddresses(hostname);
@@ -16068,19 +16081,18 @@ async function validateCustomEndpointUrl(rawUrl, opts = {}) {
16068
16081
  } catch {
16069
16082
  continue;
16070
16083
  }
16071
- if (isBlockedIp(addr, allowLocal)) {
16084
+ if (isBlockedIp(addr)) {
16072
16085
  return {
16073
16086
  ok: false,
16074
- error: "URL resolves to a private or restricted network address.",
16075
- hint: "Use a public HTTPS endpoint, or explicitly allow insecure HTTP for a trusted local/LAN server."
16087
+ error: "URL resolves to a restricted link-local or cloud metadata address.",
16088
+ hint: "Link-local addresses are not supported. Use the server's loopback, LAN, Tailscale or public address."
16076
16089
  };
16077
16090
  }
16078
16091
  }
16079
16092
  if (parsed.protocol === "http:") {
16080
16093
  const allResolvedAddressesAreLocal = addresses.every((addr) => {
16081
16094
  try {
16082
- const range = ipaddr.process(addr).range();
16083
- return range === "loopback" || range === "private" || range === "uniqueLocal";
16095
+ return NON_PUBLIC_RANGES.has(ipaddr.process(addr).range());
16084
16096
  } catch {
16085
16097
  return false;
16086
16098
  }
@@ -16088,8 +16100,8 @@ async function validateCustomEndpointUrl(rawUrl, opts = {}) {
16088
16100
  if (!allowLocal || !allResolvedAddressesAreLocal) {
16089
16101
  return {
16090
16102
  ok: false,
16091
- error: "HTTP is only allowed for local loopback or private-network addresses.",
16092
- hint: "Use https://, or allow insecure HTTP for a trusted local/LAN server."
16103
+ error: "HTTP is only allowed for addresses on this machine, your LAN or a private network such as Tailscale.",
16104
+ hint: "Use https:// for a server on the public internet."
16093
16105
  };
16094
16106
  }
16095
16107
  }
@@ -16337,9 +16349,9 @@ async function runCustomEndpointAddFlow(onResult) {
16337
16349
  }
16338
16350
  let allowInsecureLocal = false;
16339
16351
  if (/^http:\/\//i.test(String(baseUrl).trim())) {
16340
- p3.log.warn("HTTP is not encrypted. Only use it for a trusted local or LAN server.");
16352
+ p3.log.warn("HTTP is not encrypted. Only use it for a trusted server on this machine, your LAN or Tailscale.");
16341
16353
  const allow = await p3.confirm({
16342
- message: "Allow insecure HTTP for this local/LAN server?",
16354
+ message: "Allow insecure HTTP for this local, LAN or Tailscale server?",
16343
16355
  initialValue: true
16344
16356
  });
16345
16357
  if (p3.isCancel(allow)) {
@@ -16629,12 +16641,7 @@ async function resolveRefreshCredentialWithSource(provider, resolveKey, selected
16629
16641
  return resolved;
16630
16642
  }
16631
16643
 
16632
- // src/registry/refresh-models.ts
16633
- async function refreshOAuthProvider(provider, accessToken) {
16634
- const tpl = provider.templateId ?? provider.id;
16635
- if (tpl === "openai" || tpl === "openai-oauth") return refreshOpenAiOAuthModels(accessToken);
16636
- throw new Error(`refreshOAuthProvider: unsupported template "${tpl}"`);
16637
- }
16644
+ // src/registry/openai-oauth-catalog.ts
16638
16645
  function positiveInteger2(value) {
16639
16646
  return typeof value === "number" && Number.isSafeInteger(value) && value > 0 ? value : void 0;
16640
16647
  }
@@ -16754,7 +16761,41 @@ async function fetchJsonWithAuth(url, accessToken, timeoutMs) {
16754
16761
  }
16755
16762
  }
16756
16763
  }
16757
- async function refreshOpenAiOAuthModels(accessToken) {
16764
+ async function markModelsWithheldAtRequestVersion(models, accessToken, timeoutMs, previous) {
16765
+ const pinned = await fetchJsonWithAuth(
16766
+ `https://chatgpt.com/backend-api/codex/models?client_version=${CODEX_RESPONSES_LITE_VERSION}`,
16767
+ accessToken,
16768
+ timeoutMs
16769
+ );
16770
+ const offered = readOfferedIds(pinned.body);
16771
+ if (!offered) return carryWithheldMarkers(models, previous);
16772
+ return models.map((model) => offered.has(model.id) ? model : { ...model, withheldAtClientVersion: CODEX_RESPONSES_LITE_VERSION });
16773
+ }
16774
+ function readOfferedIds(body) {
16775
+ if (!body || typeof body !== "object") return void 0;
16776
+ const record = body;
16777
+ const shape = Array.isArray(record.models) ? { rows: record.models, key: "slug" } : Array.isArray(record.data) ? { rows: record.data, key: "id" } : void 0;
16778
+ if (!shape || shape.rows.length === 0) return void 0;
16779
+ const ids = /* @__PURE__ */ new Set();
16780
+ for (const row of shape.rows) {
16781
+ const id = row !== null && typeof row === "object" ? row[shape.key] : void 0;
16782
+ if (typeof id !== "string" || id === "" || id !== id.trim()) return void 0;
16783
+ ids.add(id);
16784
+ }
16785
+ return ids;
16786
+ }
16787
+ function carryWithheldMarkers(models, previous) {
16788
+ const withheld = new Map(previous.flatMap((model) => {
16789
+ const version = readCodexClientVersion(model.withheldAtClientVersion);
16790
+ return version ? [[model.id, version]] : [];
16791
+ }));
16792
+ if (withheld.size === 0) return models;
16793
+ return models.map((model) => {
16794
+ const version = withheld.get(model.id);
16795
+ return version ? { ...model, withheldAtClientVersion: version } : model;
16796
+ });
16797
+ }
16798
+ async function refreshOpenAiOAuthModels(accessToken, previous = []) {
16758
16799
  const TIMEOUT_MS = 1e4;
16759
16800
  const seedById = new Map(buildOpenAiOAuthModels().map((m) => [m.id, m]));
16760
16801
  const toModels = (entries, codexCatalog) => entries.map((entry) => buildDynamicOAuthModel(entry, seedById, codexCatalog));
@@ -16766,7 +16807,11 @@ async function refreshOpenAiOAuthModels(accessToken) {
16766
16807
  );
16767
16808
  const codexEntries = parseOpenAiModelEntries(codexResult.body);
16768
16809
  if (codexEntries.length > 0) {
16769
- return { models: toModels(codexEntries, true), source: "live" };
16810
+ const models = toModels(codexEntries, true);
16811
+ return {
16812
+ models: await markModelsWithheldAtRequestVersion(models, accessToken, TIMEOUT_MS, previous),
16813
+ source: "live"
16814
+ };
16770
16815
  }
16771
16816
  const chatGptResult = await fetchJsonWithAuth(
16772
16817
  "https://chatgpt.com/backend-api/models",
@@ -16775,7 +16820,10 @@ async function refreshOpenAiOAuthModels(accessToken) {
16775
16820
  );
16776
16821
  const chatGptEntries = parseOpenAiModelEntries(chatGptResult.body).filter(({ id }) => !CHATGPT_CODEX_UNSUPPORTED_MODELS.has(id));
16777
16822
  if (chatGptEntries.length > 0) {
16778
- return { models: toModels(chatGptEntries, false), source: "live" };
16823
+ return {
16824
+ models: carryWithheldMarkers(toModels(chatGptEntries, false), previous),
16825
+ source: "live"
16826
+ };
16779
16827
  }
16780
16828
  const failures = [codexResult.error, chatGptResult.error].filter((error) => error !== void 0);
16781
16829
  const credentialFailure = failures.find((error) => /(?:\brejected\b|\b401\b|\b403\b)/i.test(error));
@@ -16786,6 +16834,15 @@ async function refreshOpenAiOAuthModels(accessToken) {
16786
16834
  credentialRejected: credentialFailure !== void 0
16787
16835
  };
16788
16836
  }
16837
+
16838
+ // src/registry/refresh-models.ts
16839
+ async function refreshOAuthProvider(provider, accessToken) {
16840
+ const tpl = provider.templateId ?? provider.id;
16841
+ if (tpl === "openai" || tpl === "openai-oauth") {
16842
+ return refreshOpenAiOAuthModels(accessToken, provider.modelsCache?.models);
16843
+ }
16844
+ throw new Error(`refreshOAuthProvider: unsupported template "${tpl}"`);
16845
+ }
16789
16846
  function warningSuffix(models) {
16790
16847
  const warning = codexClientVersionWarning(applyOAuthSeedContextMetadata(models));
16791
16848
  return warning ? ` ${warning}` : "";
@@ -16817,8 +16874,9 @@ async function refreshApiListProvider(provider, apiKey) {
16817
16874
  let safeBaseUrl = baseUrl;
16818
16875
  const templateDefault = catalogTemplate?.defaultBaseUrl?.trim();
16819
16876
  if (configuredUrl && configuredUrl !== templateDefault) {
16877
+ const customEndpoint = provider.templateId === "custom-openai" || provider.templateId === "custom-anthropic";
16820
16878
  const urlCheck = await validateCustomEndpointUrl(baseUrl, {
16821
- allowInsecureLocal: catalogTemplate?.apiKeyOptional === true
16879
+ allowInsecureLocal: catalogTemplate?.apiKeyOptional === true || customEndpoint
16822
16880
  });
16823
16881
  if (!urlCheck.ok || !urlCheck.normalizedUrl) {
16824
16882
  return { models: [], error: `${urlCheck.error ?? "Invalid API base URL."} ${urlCheck.hint ?? ""}`.trim() };
@@ -17016,7 +17074,7 @@ async function refreshProviderModels(providerId, apiKey, registry, credentialSna
17016
17074
  }
17017
17075
  } else {
17018
17076
  const template = resolveProviderTemplate(provider);
17019
- const keyOptional = template?.apiKeyOptional === true;
17077
+ const keyOptional = template?.apiKeyOptional === true || isAnonymousProvider(provider);
17020
17078
  const effectiveKey = keyOptional && isLikelyPlaceholderKey(apiKey) ? "" : apiKey;
17021
17079
  if (!keyOptional && isLikelyPlaceholderKey(effectiveKey)) {
17022
17080
  if (cachedModelCount(cacheProvider) > 0) {