@bman654/clodex 2.10.0 → 2.11.1

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
@@ -241,11 +241,13 @@ Manage favorite models (max 20) and short aliases. Favorites feed the endpoint-m
241
241
 
242
242
  #### Context stops and the pricing boundary
243
243
 
244
- A context window is a cost dial as much as a capacity number. OpenAI prices GPT-5.6
245
- prompts above **272,000 input tokens at 2x input and 1.5x output for the full
246
- request**, which is why the Codex catalog reports a 272,000 window rather than the
247
- model's ceiling. Clodex follows that: the default `standard` stop stays under the
248
- line, and a larger window is something you ask for.
244
+ A context window is a cost dial as much as a capacity number. OpenAI prices GPT-5.5
245
+ and later prompts above **272,000 input tokens at 2x input and 1.5x output for the
246
+ full request**, which is why the Codex catalog reports a 272,000 window rather than
247
+ the model's ceiling. Newer families inherit the same boundary, so a model released
248
+ after this was written is covered without a clodex update. Clodex follows that: the
249
+ default `standard` stop stays under the line, and a larger window is something you
250
+ ask for.
249
251
 
250
252
  ```sh
251
253
  clodex models --context sol=max --save # this model's default, with a cost warning
@@ -253,19 +255,21 @@ clodex claude --context sol=max # this launch only, nothing saved
253
255
  clodex models --context sol=default --save # back to the provider's tuned window
254
256
  ```
255
257
 
256
- Each stop is reported with the numbers behind it: the raw window, the headroom
257
- percentage the Codex catalog uses, the effective window a client should fill, and the
258
- account ceiling a larger stop can reach. A stop above the ceiling is clamped and says
259
- so. When a request's own reported token count crosses the boundary, clodex warns once
260
- per model for the life of the process, because the client's token count and the
261
- provider's differ after translation and only the provider's settles it.
258
+ Each stop is reported with the numbers behind it: the raw window, the effective
259
+ window a client should fill, and the account ceiling a larger stop can reach. A stop
260
+ above the ceiling is clamped and says so. When a request's own reported token count
261
+ crosses the boundary, clodex warns once per model for the life of the process,
262
+ because the client's token count and the provider's differ after translation and only
263
+ the provider's settles it.
262
264
 
263
265
  Two things worth knowing about the numbers:
264
266
 
265
- - **ChatGPT/Codex OAuth models carry a 95% headroom convention**, matching the Codex
266
- client. Their reported window is 5% below the raw catalog value: `gpt-5.6-sol`
267
- reports 258,400 rather than 272,000. This applies to that provider only; API-key
268
- and OpenCode Go models keep their full window.
267
+ - **Clodex reports the window the provider actually gives, and holds nothing back.**
268
+ Deciding how much of a window to leave free is the client's job Claude Code
269
+ already reserves a fixed amount below whatever window it is told, and shrinking the
270
+ number first only costs usable context. A provider that declares a share of its own
271
+ is still honoured; clodex just never invents one. Use `--context` if you want a
272
+ smaller window than the provider offers.
269
273
  - **The account ceiling moves.** It is server-side and per-account, and it has
270
274
  changed by more than 2x within a single day in the past. `max` reads whatever the
271
275
  catalog reports now and clamps to it, so a stale ceiling shrinks the stop rather
package/dist/cli.js CHANGED
@@ -382,7 +382,7 @@ import { join } from "path";
382
382
  // package.json
383
383
  var package_default = {
384
384
  name: "@bman654/clodex",
385
- version: "2.10.0",
385
+ version: "2.11.1",
386
386
  publishConfig: {
387
387
  access: "public"
388
388
  },
@@ -469,7 +469,7 @@ var package_default = {
469
469
 
470
470
  // src/constants.ts
471
471
  var CODEX_RESPONSES_LITE_WS_URL = "wss://chatgpt.com/backend-api/codex/responses";
472
- var CODEX_RESPONSES_LITE_VERSION = "0.144.1";
472
+ var CODEX_RESPONSES_LITE_VERSION = "0.153.3";
473
473
  var CODEX_RESPONSES_WEBSOCKETS_BETA = "responses_websockets=2026-02-06";
474
474
  var TEST_TIMEOUT_MS = 1e4;
475
475
  var CONFLICTING_ENV_VARS = [
@@ -3121,7 +3121,6 @@ async function deleteProviderCredential(authRef, diag) {
3121
3121
  }
3122
3122
 
3123
3123
  // src/context-modes.ts
3124
- var DEFAULT_EFFECTIVE_CONTEXT_PERCENT = 95;
3125
3124
  function positiveInteger(value) {
3126
3125
  return typeof value === "number" && Number.isSafeInteger(value) && value > 0 ? value : void 0;
3127
3126
  }
@@ -5873,6 +5872,18 @@ var CHATGPT_CODEX_UNSUPPORTED_MODELS = /* @__PURE__ */ new Set([
5873
5872
  // confirmed: rejected by chatgpt.com/backend-api/codex
5874
5873
  ]);
5875
5874
  var OPENAI_OAUTH_MODEL_SEEDS = [
5875
+ // GPT-6 family. The window and ceiling are what the live Codex catalog returned on
5876
+ // 2026-09-04 and are deliberately NOT the published API numbers: the model card
5877
+ // lists a 1,050,000 context window, but the Codex client is served a smaller one,
5878
+ // and this path is Codex-only. Output limit and the pricing band come from the
5879
+ // card (https://developers.openai.com/api/docs/models/gpt-6-astra), which states
5880
+ // "Prompts with more than 272K input tokens are priced at 2x input and cache rates
5881
+ // and 1.5x output for the full request" — the same boundary the GPT-5.6 family has.
5882
+ { id: "gpt-6-astra", name: "GPT-6 Astra", contextWindow: 272e3, maxContextWindow: 872e3, maxOutputTokens: 128e3, reasoning: true, useResponsesLite: true, preferWebSockets: true },
5883
+ // "An alias for our flagship general-purpose models, with safeguards calibrated
5884
+ // for defensive cybersecurity work" — access is gated on a separate opt-in
5885
+ // program, so most installs will never see this id in their catalog.
5886
+ { id: "gpt-daybreak-blue-latest", name: "GPT Daybreak Blue", contextWindow: 272e3, maxContextWindow: 872e3, maxOutputTokens: 128e3, reasoning: true, useResponsesLite: true, preferWebSockets: true },
5876
5887
  // GPT-5.6 family (Sol / Terra / Luna)
5877
5888
  { id: "gpt-5.6-sol", name: "GPT-5.6 Sol", contextWindow: 272e3, maxContextWindow: 872e3, maxOutputTokens: 128e3, reasoning: true },
5878
5889
  { id: "gpt-5.6-terra", name: "GPT-5.6 Terra", contextWindow: 272e3, maxContextWindow: 872e3, maxOutputTokens: 128e3, reasoning: true },
@@ -5891,14 +5902,26 @@ var OPENAI_OAUTH_MODEL_SEEDS = [
5891
5902
  { id: "o1", name: "o1", reasoning: true },
5892
5903
  { id: "o1-mini", name: "o1 Mini", reasoning: true }
5893
5904
  ];
5894
- var PRICING_BOUNDARY_FAMILIES = /^gpt-5\.[56]/;
5905
+ function hasPricingBoundary(id) {
5906
+ const version = /^gpt-(\d+)(?:\.(\d+))?(?:-|$)/i.exec(id);
5907
+ if (version) {
5908
+ const major = Number(version[1]);
5909
+ const minor = version[2] ? Number(version[2]) : 0;
5910
+ if (major > 5 || major === 5 && minor >= 5) return true;
5911
+ }
5912
+ return /^gpt-daybreak(?:-|$)/i.test(id);
5913
+ }
5895
5914
  function openAiPricingMetadata(id) {
5896
- if (!PRICING_BOUNDARY_FAMILIES.test(id)) return {};
5915
+ if (!hasPricingBoundary(id)) return {};
5897
5916
  return {
5898
5917
  pricingBoundary: GPT_5_6_PRICING_BOUNDARY,
5899
5918
  pricingBoundaryNote: GPT_5_6_PRICING_NOTE
5900
5919
  };
5901
5920
  }
5921
+ var LEGACY_IMPOSED_CONTEXT_PERCENT = 95;
5922
+ function migratedEffectiveContextPercent(cached) {
5923
+ return cached === LEGACY_IMPOSED_CONTEXT_PERCENT ? void 0 : cached;
5924
+ }
5902
5925
  function applyOAuthSeedContextMetadata(models) {
5903
5926
  const seedById = new Map(buildOpenAiOAuthModels().map((model) => [model.id, model]));
5904
5927
  return models.map((model) => {
@@ -5907,10 +5930,11 @@ function applyOAuthSeedContextMetadata(models) {
5907
5930
  return {
5908
5931
  ...model,
5909
5932
  maxContextWindow: model.maxContextWindow ?? seed?.maxContextWindow,
5910
- effectiveContextPercent: model.effectiveContextPercent ?? seed?.effectiveContextPercent ?? DEFAULT_EFFECTIVE_CONTEXT_PERCENT,
5933
+ effectiveContextPercent: migratedEffectiveContextPercent(model.effectiveContextPercent),
5911
5934
  pricingBoundary: model.pricingBoundary ?? seed?.pricingBoundary ?? pricing.pricingBoundary,
5912
5935
  pricingBoundaryNote: model.pricingBoundaryNote ?? seed?.pricingBoundaryNote ?? pricing.pricingBoundaryNote,
5913
- maxOutputTokens: model.maxOutputTokens ?? seed?.maxOutputTokens
5936
+ maxOutputTokens: model.maxOutputTokens ?? seed?.maxOutputTokens,
5937
+ reasoning: seed?.reasoning ?? model.reasoning
5914
5938
  };
5915
5939
  });
5916
5940
  }
@@ -5926,7 +5950,7 @@ function buildOpenAiOAuthModels() {
5926
5950
  brand: deriveBrand(prefix),
5927
5951
  contextWindow: resolveContextWindow(seed.id, seed.contextWindow),
5928
5952
  maxContextWindow: seed.maxContextWindow,
5929
- effectiveContextPercent: seed.effectiveContextPercent ?? DEFAULT_EFFECTIVE_CONTEXT_PERCENT,
5953
+ effectiveContextPercent: seed.effectiveContextPercent,
5930
5954
  pricingBoundary: seed.pricingBoundary ?? pricing.pricingBoundary,
5931
5955
  pricingBoundaryNote: seed.pricingBoundaryNote ?? pricing.pricingBoundaryNote,
5932
5956
  maxOutputTokens: seed.maxOutputTokens,
@@ -10253,7 +10277,7 @@ async function createLanguageModel(spec) {
10253
10277
  }
10254
10278
  var ANTHROPIC_EFFORT_LEVELS = ["low", "medium", "high"];
10255
10279
  var OPENAI_EFFORT_LEVELS = ["low", "medium", "high", "xhigh"];
10256
- var GPT_56_EFFORT_LEVELS = ["none", "low", "medium", "high", "xhigh", "max"];
10280
+ var CODEX_EXTENDED_EFFORT_LEVELS = ["none", "low", "medium", "high", "xhigh", "max"];
10257
10281
  var GEMINI_EFFORT_LEVELS = ["low", "medium", "high"];
10258
10282
  var MISTRAL_EFFORT_LEVELS = ["high", "off"];
10259
10283
  var XAI_EFFORT_LEVELS = ["none", "low", "medium", "high"];
@@ -10417,14 +10441,32 @@ function mapCodexEffortToAnthropic(effort) {
10417
10441
  return void 0;
10418
10442
  }
10419
10443
  }
10420
- function isGpt56Model(modelId) {
10421
- return /^gpt-5\.6(?:-|$)/i.test(modelId);
10444
+ function isCodexReasoningFamily(modelId) {
10445
+ return !isChatVariant(modelId) && supportsExtendedCodexEffort(modelId);
10446
+ }
10447
+ function isChatVariant(modelId) {
10448
+ return /-chat(?:-|$)/i.test(modelId);
10449
+ }
10450
+ function supportsExtendedCodexEffort(modelId) {
10451
+ const version = /^gpt-(\d+)(?:\.(\d+))?(?:-|$)/i.exec(modelId);
10452
+ if (version) {
10453
+ const major = Number(version[1]);
10454
+ const minor = version[2] ? Number(version[2]) : 0;
10455
+ if (major > 5 || major === 5 && minor >= 6) return true;
10456
+ }
10457
+ return /^gpt-daybreak(?:-|$)/i.test(modelId);
10458
+ }
10459
+ function supportsNoneEffort(modelId) {
10460
+ return /^gpt-5\.6(?:-|$)/i.test(modelId) || /^gpt-daybreak-blue(?:-|$)/i.test(modelId);
10422
10461
  }
10423
10462
  function isReasoningSummaryUnsupportedModel(modelId) {
10424
10463
  return /codex-spark(?:-|$)/i.test(modelId);
10425
10464
  }
10426
10465
  function mapCodexEffortToOpenAI(effort, modelId) {
10427
- if (modelId && isGpt56Model(modelId) && GPT_56_EFFORT_LEVELS.includes(effort)) {
10466
+ if (effort === "none") {
10467
+ return modelId && supportsNoneEffort(modelId) ? "none" : void 0;
10468
+ }
10469
+ if (modelId && supportsExtendedCodexEffort(modelId) && CODEX_EXTENDED_EFFORT_LEVELS.includes(effort)) {
10428
10470
  return effort;
10429
10471
  }
10430
10472
  if (effort === "xhigh") return "high";
@@ -10565,9 +10607,9 @@ function getReasoningCapabilities(npm, modelId, metadata) {
10565
10607
  }
10566
10608
  if (npm === "@ai-sdk/openai" || npm === "@ai-sdk/azure") {
10567
10609
  const prefersResponses = modelPrefersResponsesApi(modelId);
10568
- if (prefersResponses || metadata?.reasoning) {
10610
+ if (prefersResponses || isCodexReasoningFamily(modelId) || metadata?.reasoning) {
10569
10611
  return {
10570
- levels: isGpt56Model(modelId) ? [...GPT_56_EFFORT_LEVELS] : [...OPENAI_EFFORT_LEVELS],
10612
+ levels: supportsExtendedCodexEffort(modelId) ? CODEX_EXTENDED_EFFORT_LEVELS.filter((level) => level !== "none" || supportsNoneEffort(modelId)) : [...OPENAI_EFFORT_LEVELS],
10571
10613
  defaultLevel: "medium",
10572
10614
  supportsSummaries: true,
10573
10615
  mode: "controllable",
@@ -10731,10 +10773,14 @@ function effortProviderOptions(npm, effort, modelId, metadata) {
10731
10773
  return mapped ? { openrouter: { reasoning: { effort: mapped, exclude: false } } } : void 0;
10732
10774
  }
10733
10775
  if (npm === "@ai-sdk/openai" || npm === "@ai-sdk/azure") {
10734
- if (!modelId || !modelPrefersResponsesApi(modelId)) return void 0;
10776
+ if (!modelId || isChatVariant(modelId)) return void 0;
10777
+ if (!(modelPrefersResponsesApi(modelId) || isCodexReasoningFamily(modelId))) {
10778
+ return void 0;
10779
+ }
10735
10780
  const reasoningEffort = mapCodexEffortToOpenAI(effort, modelId);
10736
10781
  if (!reasoningEffort) return void 0;
10737
- return isReasoningSummaryUnsupportedModel(modelId) ? { openai: { reasoningEffort, reasoningSummary: null } } : { openai: { reasoningEffort } };
10782
+ const openaiOptions = { reasoningEffort, forceReasoning: true };
10783
+ return isReasoningSummaryUnsupportedModel(modelId) ? { openai: { ...openaiOptions, reasoningSummary: null } } : { openai: openaiOptions };
10738
10784
  }
10739
10785
  if (npm === "@ai-sdk/xai") {
10740
10786
  if (!modelId || !isXaiReasoningEffortModel(modelId)) return void 0;
@@ -15021,7 +15067,7 @@ function parseOpenAiModelEntries(body) {
15021
15067
  }
15022
15068
  return [];
15023
15069
  }
15024
- function buildDynamicOAuthModel(entry, seedById) {
15070
+ function buildDynamicOAuthModel(entry, seedById, codexCatalog) {
15025
15071
  const seed = seedById.get(entry.id);
15026
15072
  if (seed) {
15027
15073
  return {
@@ -15047,12 +15093,27 @@ function buildDynamicOAuthModel(entry, seedById) {
15047
15093
  brand: deriveBrand(prefix),
15048
15094
  contextWindow: entry.context_window ?? resolveContextWindow(id),
15049
15095
  maxContextWindow: entry.max_context_window,
15050
- effectiveContextPercent: entry.effective_context_window_percent ?? DEFAULT_EFFECTIVE_CONTEXT_PERCENT,
15096
+ // Absent means no reduction. clodex reports the window the provider actually
15097
+ // gives; deciding how much of it to leave free is the client's job, and Claude
15098
+ // Code already reserves a flat 33,000 tokens below whatever it is told.
15099
+ effectiveContextPercent: entry.effective_context_window_percent,
15051
15100
  maxOutputTokens: entry.max_output_tokens,
15052
15101
  ...openAiPricingMetadata(id),
15053
15102
  modelFormat: "openai",
15054
15103
  npm: "@ai-sdk/openai",
15055
- reasoning: modelPrefersResponsesApi(id),
15104
+ // Assume a model from the Codex listing reasons. That endpoint reports no
15105
+ // reasoning field of its own, so the old `modelPrefersResponsesApi(id)` was an
15106
+ // id-pattern GUESS that silently said "no" to every family it had not been
15107
+ // taught yet — gpt-6-astra and gpt-daybreak-blue-latest both landed as
15108
+ // non-reasoning that way, which dropped the user's chosen effort and removed
15109
+ // the effort selector from the patched binary (getPatchReasoningCapabilities
15110
+ // early-returns on a `false`). Verified against all 11 models in the live
15111
+ // catalog on 2026-09-04.
15112
+ //
15113
+ // This only decides what the effort UI offers. It is NOT on its own enough to
15114
+ // put reasoning.effort on the wire — effortProviderOptions admits by family —
15115
+ // so a wrong `true` here costs an unusable menu entry, not a 400.
15116
+ reasoning: codexCatalog ? true : modelPrefersResponsesApi(id),
15056
15117
  useResponsesLite: entry.useResponsesLite,
15057
15118
  preferWebSockets: entry.preferWebSockets
15058
15119
  };
@@ -15081,7 +15142,7 @@ async function fetchJsonWithAuth(url, accessToken, timeoutMs) {
15081
15142
  async function refreshOpenAiOAuthModels(accessToken) {
15082
15143
  const TIMEOUT_MS = 1e4;
15083
15144
  const seedById = new Map(buildOpenAiOAuthModels().map((m) => [m.id, m]));
15084
- const toModels = (entries) => entries.map((entry) => buildDynamicOAuthModel(entry, seedById));
15145
+ const toModels = (entries, codexCatalog) => entries.map((entry) => buildDynamicOAuthModel(entry, seedById, codexCatalog));
15085
15146
  const claudeVersion = getInstalledClaudeVersion();
15086
15147
  const codexResult = await fetchJsonWithAuth(
15087
15148
  `https://chatgpt.com/backend-api/codex/models?client_version=${claudeVersion}`,
@@ -15090,7 +15151,7 @@ async function refreshOpenAiOAuthModels(accessToken) {
15090
15151
  );
15091
15152
  const codexEntries = parseOpenAiModelEntries(codexResult.body);
15092
15153
  if (codexEntries.length > 0) {
15093
- return { models: toModels(codexEntries), source: "live" };
15154
+ return { models: toModels(codexEntries, true), source: "live" };
15094
15155
  }
15095
15156
  const chatGptResult = await fetchJsonWithAuth(
15096
15157
  "https://chatgpt.com/backend-api/models",
@@ -15099,7 +15160,7 @@ async function refreshOpenAiOAuthModels(accessToken) {
15099
15160
  );
15100
15161
  const chatGptEntries = parseOpenAiModelEntries(chatGptResult.body).filter(({ id }) => !CHATGPT_CODEX_UNSUPPORTED_MODELS.has(id));
15101
15162
  if (chatGptEntries.length > 0) {
15102
- return { models: toModels(chatGptEntries), source: "live" };
15163
+ return { models: toModels(chatGptEntries, false), source: "live" };
15103
15164
  }
15104
15165
  const failures = [codexResult.error, chatGptResult.error].filter((error) => error !== void 0);
15105
15166
  const credentialFailure = failures.find((error) => /(?:\brejected\b|\b401\b|\b403\b)/i.test(error));