pennyrouter 0.3.30 → 0.3.32

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pennyrouter",
3
- "version": "0.3.30",
3
+ "version": "0.3.32",
4
4
  "description": "Install and manage PennyRouter local coding-agent integrations.",
5
5
  "homepage": "https://pennyrouter.com",
6
6
  "bugs": {
package/src/cli.js CHANGED
@@ -685,10 +685,13 @@ export async function install(flags = {}) {
685
685
  const selected = await selectHarnesses(flags);
686
686
  if (selected.length === 0) {
687
687
  console.log("No supported harnesses selected.");
688
+ await printAdditionalHarnessesHint([]);
688
689
  return;
689
690
  }
690
691
 
691
692
  console.log("PennyRouter install");
693
+ console.log("PennyRouter can be installed into multiple coding harnesses on this machine.");
694
+ await printAdditionalHarnessesHint(selected);
692
695
  console.log("");
693
696
 
694
697
  const manifest = await loadManifest();
@@ -1554,12 +1557,24 @@ async function selectClaudeFundingProviders(flags, selected) {
1554
1557
 
1555
1558
  console.log("PennyRouter can use your Claude subscription, an Anthropic API key, an OpenRouter API key, or your own Anthropic-compatible proxy to run Claude Code.");
1556
1559
  console.log("Choose one or more providers. PennyRouter will try them in order when available.");
1560
+ const subscriptionAvailable = Boolean(
1561
+ flags.token || flags.tokenCommand || process.env.ANTHROPIC_AUTH_TOKEN || commandExists("claude"),
1562
+ );
1563
+ const options = CLAUDE_PROVIDER_OPTIONS.filter(
1564
+ ({ id }) => id !== "subscription" || subscriptionAvailable,
1565
+ );
1557
1566
  return promptFundingProviderCheckboxes(
1558
- CLAUDE_PROVIDER_OPTIONS,
1559
- "Which providers should PennyRouter use for Claude Code?",
1567
+ options,
1568
+ "Which funding source should PennyRouter use for Claude Code?",
1560
1569
  // An enterprise gateway usually fronts the model provider itself, so the proxy is the
1561
1570
  // expected answer there and leads; elsewhere the subscription does.
1562
- new Set([flags.privateGatewayFlow ? "anthropic-proxy" : "subscription"]),
1571
+ new Set([
1572
+ flags.privateGatewayFlow && options.some(({ id }) => id === "anthropic-proxy")
1573
+ ? "anthropic-proxy"
1574
+ : options.some(({ id }) => id === "subscription")
1575
+ ? "subscription"
1576
+ : options[0]?.id,
1577
+ ].filter(Boolean)),
1563
1578
  );
1564
1579
  }
1565
1580
 
@@ -1580,10 +1595,17 @@ async function selectCodexFundingProviders(flags, selected) {
1580
1595
 
1581
1596
  console.log("PennyRouter can use your ChatGPT subscription, an OpenAI API key, an OpenAI-compatible proxy, or an OpenRouter API key to run Codex.");
1582
1597
  console.log("Choose one or more providers. PennyRouter will try them in order when available.");
1598
+ const options = CODEX_PROVIDER_OPTIONS.filter(
1599
+ ({ id }) => id !== "subscription" || hasChatgptLogin,
1600
+ );
1583
1601
  return promptFundingProviderCheckboxes(
1584
- CODEX_PROVIDER_OPTIONS,
1585
- "Which providers should PennyRouter use for Codex?",
1586
- new Set([flags.privateGatewayFlow ? "openai-proxy" : "subscription"]),
1602
+ options,
1603
+ "Which funding source should PennyRouter use for Codex?",
1604
+ new Set([
1605
+ flags.privateGatewayFlow && options.some(({ id }) => id === "openai-proxy")
1606
+ ? "openai-proxy"
1607
+ : options[0]?.id,
1608
+ ].filter(Boolean)),
1587
1609
  );
1588
1610
  }
1589
1611
 
@@ -1632,7 +1654,9 @@ async function maybeConfigureClaudeAnthropicAuth(flags, selected, enabled = true
1632
1654
  } else if (flags.fromConfig) {
1633
1655
  throw new Error("Claude Code was not found on PATH, so subscription sign-in could not run.");
1634
1656
  } else {
1635
- console.log("Claude Code was not found on PATH. Paste an Anthropic bearer token, or leave blank to skip.");
1657
+ // The provider picker only offers subscription auth when Claude Code can perform
1658
+ // setup-token (or the user supplied a token). Never send users into a dead manual flow.
1659
+ return null;
1636
1660
  }
1637
1661
  const token = normalizeAnthropicToken(await promptSecretish("Anthropic token (blank to skip): "));
1638
1662
  if (!token) {
@@ -2115,6 +2139,20 @@ async function selectHarnesses(flags) {
2115
2139
  return promptHarnessCheckboxes(detected);
2116
2140
  }
2117
2141
 
2142
+ async function printAdditionalHarnessesHint(selected) {
2143
+ const selectedIds = new Set(selected.map(({ id }) => id));
2144
+ const available = [];
2145
+ for (const harness of Object.values(HARNESS_BY_ID)) {
2146
+ if (selectedIds.has(harness.id)) continue;
2147
+ if (!(await safeDetect(harness))) available.push(harness.name);
2148
+ }
2149
+ if (available.length === 0) return;
2150
+ const names = available.length === 1
2151
+ ? available[0]
2152
+ : `${available.slice(0, -1).join(", ")}, or ${available.at(-1)}`;
2153
+ console.log(`To use PennyRouter with ${names}, install ${available.length === 1 ? "it" : "them"} and re-run \`npx pennyrouter install\`.`);
2154
+ }
2155
+
2118
2156
  async function promptHarnessCheckboxes(detected) {
2119
2157
  const detectedIds = new Set(detected.map((harness) => harness.id));
2120
2158
  const selectedIds = new Set(detectedIds);
@@ -99,6 +99,8 @@ export async function fetchClaudeCodeModels(gatewayBaseUrl, apiKey) {
99
99
  model: id,
100
100
  ...(displayName && displayName !== id ? { label: displayName } : {}),
101
101
  ...(description ? { description } : {}),
102
+ ...(Number.isFinite(entry.max_input_tokens)
103
+ ? { max_input_tokens: entry.max_input_tokens } : {}),
102
104
  });
103
105
  }
104
106
  if (models.length === 0) {
@@ -107,6 +109,7 @@ export async function fetchClaudeCodeModels(gatewayBaseUrl, apiKey) {
107
109
  return {
108
110
  models,
109
111
  privateGateway: response.headers?.get?.("x-pennyrouter-private-gateway") === "1",
112
+ ...(payload?.anthropic_catalog_live === true ? { nativeCatalogLive: true } : {}),
110
113
  };
111
114
  }
112
115
 
@@ -156,13 +159,13 @@ export const claudeCodeHarness = {
156
159
  delete current.env[`ANTHROPIC_DEFAULT_${slot}_MODEL_DESCRIPTION`];
157
160
  }
158
161
  } else {
159
- // Claude Code displays these env values directly in its model picker; the
160
- // gateway decodes the friendly labels into bundle/profile overrides.
161
- current.env.ANTHROPIC_DEFAULT_OPUS_MODEL = CLAUDE_CODE_MODELS.opus;
162
- current.env.ANTHROPIC_DEFAULT_SONNET_MODEL = CLAUDE_CODE_MODELS.sonnet;
163
- current.env.ANTHROPIC_DEFAULT_HAIKU_MODEL = CLAUDE_CODE_MODELS.haiku;
164
- current.env.ANTHROPIC_DEFAULT_FABLE_MODEL = CLAUDE_CODE_MODELS.fable;
165
- writeClaudeCodeModelPresentation(current.env);
162
+ // Hosted discovery follows Anthropic's live catalog. Fixed slot overrides in
163
+ // shared settings would keep plain `claude` installs pinned to old releases.
164
+ for (const slot of ["OPUS", "SONNET", "HAIKU", "FABLE"]) {
165
+ delete current.env[`ANTHROPIC_DEFAULT_${slot}_MODEL`];
166
+ delete current.env[`ANTHROPIC_DEFAULT_${slot}_MODEL_NAME`];
167
+ delete current.env[`ANTHROPIC_DEFAULT_${slot}_MODEL_DESCRIPTION`];
168
+ }
166
169
  }
167
170
  delete current.env.ANTHROPIC_CUSTOM_MODEL_OPTION;
168
171
  delete current.env.ANTHROPIC_CUSTOM_MODEL_OPTION_NAME;
@@ -220,11 +223,11 @@ export const claudeCodeHarness = {
220
223
  delete current.env[`ANTHROPIC_DEFAULT_${slot}_MODEL_DESCRIPTION`];
221
224
  }
222
225
  } else {
223
- current.env.ANTHROPIC_DEFAULT_OPUS_MODEL = CLAUDE_CODE_MODELS.opus;
224
- current.env.ANTHROPIC_DEFAULT_SONNET_MODEL = CLAUDE_CODE_MODELS.sonnet;
225
- current.env.ANTHROPIC_DEFAULT_HAIKU_MODEL = CLAUDE_CODE_MODELS.haiku;
226
- current.env.ANTHROPIC_DEFAULT_FABLE_MODEL = CLAUDE_CODE_MODELS.fable;
227
- writeClaudeCodeModelPresentation(current.env);
226
+ for (const slot of ["OPUS", "SONNET", "HAIKU", "FABLE"]) {
227
+ delete current.env[`ANTHROPIC_DEFAULT_${slot}_MODEL`];
228
+ delete current.env[`ANTHROPIC_DEFAULT_${slot}_MODEL_NAME`];
229
+ delete current.env[`ANTHROPIC_DEFAULT_${slot}_MODEL_DESCRIPTION`];
230
+ }
228
231
  }
229
232
  delete current.env.ANTHROPIC_CUSTOM_MODEL_OPTION;
230
233
  delete current.env.ANTHROPIC_CUSTOM_MODEL_OPTION_NAME;
@@ -271,11 +274,11 @@ export const claudeCodeHarness = {
271
274
  current.env.ANTHROPIC_API_KEY = pennyKey;
272
275
  current.env.ANTHROPIC_AUTH_TOKEN = anthropicAuthToken;
273
276
  current.env.CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY = "1";
274
- current.env.ANTHROPIC_DEFAULT_OPUS_MODEL = CLAUDE_CODE_MODELS.opus;
275
- current.env.ANTHROPIC_DEFAULT_SONNET_MODEL = CLAUDE_CODE_MODELS.sonnet;
276
- current.env.ANTHROPIC_DEFAULT_HAIKU_MODEL = CLAUDE_CODE_MODELS.haiku;
277
- current.env.ANTHROPIC_DEFAULT_FABLE_MODEL = CLAUDE_CODE_MODELS.fable;
278
- writeClaudeCodeModelPresentation(current.env);
277
+ for (const slot of ["OPUS", "SONNET", "HAIKU", "FABLE"]) {
278
+ delete current.env[`ANTHROPIC_DEFAULT_${slot}_MODEL`];
279
+ delete current.env[`ANTHROPIC_DEFAULT_${slot}_MODEL_NAME`];
280
+ delete current.env[`ANTHROPIC_DEFAULT_${slot}_MODEL_DESCRIPTION`];
281
+ }
279
282
  delete current.env.ANTHROPIC_CUSTOM_MODEL_OPTION;
280
283
  delete current.env.ANTHROPIC_CUSTOM_MODEL_OPTION_NAME;
281
284
  delete current.env.ANTHROPIC_CUSTOM_MODEL_OPTION_DESCRIPTION;
@@ -86,6 +86,8 @@ export async function detectCodexChatgptAuth() {
86
86
  const status = spawnSync("codex", ["login", "status"], {
87
87
  encoding: "utf8",
88
88
  stdio: ["ignore", "pipe", "pipe"],
89
+ // Windows npm installs Codex as a .cmd shim, which requires shell resolution.
90
+ shell: process.platform === "win32",
89
91
  });
90
92
  return status.status === 0 && /logged in using chatgpt/i.test(`${status.stdout}\n${status.stderr}`);
91
93
  }
package/src/launch.js CHANGED
@@ -23,7 +23,6 @@ import { configureClaudeCodeStatusline } from "./statusline.js";
23
23
  import {
24
24
  fetchClaudeCodeModels,
25
25
  upgradeClaudeCodeModelValue,
26
- writeClaudeCodeModelPresentation,
27
26
  } from "./harnesses/claude-code.js";
28
27
  import {
29
28
  fetchCodexModels,
@@ -856,6 +855,7 @@ export async function writeClaudeLaunchSettings({
856
855
  apiKey,
857
856
  models = [],
858
857
  privateGateway = false,
858
+ slotModels = {},
859
859
  } = {}) {
860
860
  const native = (await readJsonFile(join(homeDir(), ".claude", "settings.json"), null)) || {};
861
861
  // Claude Code gives values from --settings precedence over the parent process
@@ -883,6 +883,22 @@ export async function writeClaudeLaunchSettings({
883
883
  // An empty value is treated as unset by Claude Code, and prevents a
884
884
  // stale API key in the shared settings from creating dual auth.
885
885
  ANTHROPIC_API_KEY: "",
886
+ ANTHROPIC_DEFAULT_HAIKU_MODEL: "",
887
+ ANTHROPIC_DEFAULT_HAIKU_MODEL_NAME: "",
888
+ ANTHROPIC_DEFAULT_HAIKU_MODEL_DESCRIPTION: "",
889
+ ANTHROPIC_DEFAULT_SONNET_MODEL: "",
890
+ ANTHROPIC_DEFAULT_SONNET_MODEL_NAME: "",
891
+ ANTHROPIC_DEFAULT_SONNET_MODEL_DESCRIPTION: "",
892
+ ANTHROPIC_DEFAULT_OPUS_MODEL: "",
893
+ ANTHROPIC_DEFAULT_OPUS_MODEL_NAME: "",
894
+ ANTHROPIC_DEFAULT_OPUS_MODEL_DESCRIPTION: "",
895
+ ANTHROPIC_DEFAULT_FABLE_MODEL: "",
896
+ ANTHROPIC_DEFAULT_FABLE_MODEL_NAME: "",
897
+ ANTHROPIC_DEFAULT_FABLE_MODEL_DESCRIPTION: "",
898
+ ...Object.fromEntries(
899
+ Object.entries(slotModels).filter(([slot]) => ["haiku", "sonnet", "opus", "fable"].includes(slot))
900
+ .map(([slot, value]) => [`ANTHROPIC_DEFAULT_${slot.toUpperCase()}_MODEL`, value]),
901
+ ),
886
902
  },
887
903
  }
888
904
  : {}),
@@ -892,6 +908,17 @@ export async function writeClaudeLaunchSettings({
892
908
  await privateWrite(claudeLaunchSettingsPath(), `${JSON.stringify(overlay, null, 2)}\n`);
893
909
  }
894
910
 
911
+ function hostedClaudeSlots(models) {
912
+ const slots = {};
913
+ for (const entry of models) {
914
+ const match = /^claude-(haiku|sonnet|opus|fable)-/.exec(entry.model || "");
915
+ if (!match || slots[match[1]]) continue;
916
+ const oneMillion = entry.max_input_tokens >= 1000000;
917
+ slots[match[1]] = `${entry.model}${oneMillion ? "[1m]" : ""}`;
918
+ }
919
+ return slots;
920
+ }
921
+
895
922
  function claudeCodeModelCachePath(profile) {
896
923
  return join(pennyDataDir(), `claude-models-${profile || DEFAULT_PROFILE}.json`);
897
924
  }
@@ -915,6 +942,7 @@ export async function resolveClaudeCodeLaunchModels(config) {
915
942
  // Old cache files predate the explicit server marker. Treating them as hosted
916
943
  // is the safe rollout behavior: never hide built-ins based on a guess.
917
944
  privateGateway: cached.privateGateway === true,
945
+ nativeCatalogLive: cached.nativeCatalogLive === true,
918
946
  };
919
947
  }
920
948
  console.error(
@@ -1326,21 +1354,23 @@ export function buildPennyInvocation(harness, args, config, models = null) {
1326
1354
  const env = { ...process.env, PENNYROUTER_API_KEY: config.api_key };
1327
1355
  if (harness === "claude") {
1328
1356
  env.PENNYROUTER_TITLE_SESSION_FILE = join(pennyDataDir(), `title-session-${randomUUID()}`);
1329
- const models = item.models || {};
1357
+ const claudeModels = models ?? item.models ?? {};
1330
1358
  env.ANTHROPIC_BASE_URL = config.gateway_base_url;
1331
1359
  env.ANTHROPIC_AUTH_TOKEN = config.api_key;
1332
1360
  // Ask Claude Code to discover the gateway's model catalog at startup. The gateway
1333
1361
  // advertises Penny-facing ids in /v1/models; this is separate from inference routing.
1334
1362
  env.CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY = "1";
1335
1363
  delete env.ANTHROPIC_API_KEY;
1336
- if (models.haiku) env.ANTHROPIC_DEFAULT_HAIKU_MODEL = upgradeClaudeCodeModelValue(models.haiku);
1337
- if (models.sonnet) env.ANTHROPIC_DEFAULT_SONNET_MODEL = upgradeClaudeCodeModelValue(models.sonnet);
1338
- if (models.opus) env.ANTHROPIC_DEFAULT_OPUS_MODEL = upgradeClaudeCodeModelValue(models.opus);
1339
- if (models.fable) env.ANTHROPIC_DEFAULT_FABLE_MODEL = upgradeClaudeCodeModelValue(models.fable);
1364
+ for (const slot of ["haiku", "sonnet", "opus", "fable"]) {
1365
+ const name = `ANTHROPIC_DEFAULT_${slot.toUpperCase()}_MODEL`;
1366
+ if (claudeModels[slot]) env[name] = upgradeClaudeCodeModelValue(claudeModels[slot]);
1367
+ else delete env[name];
1368
+ delete env[`${name}_NAME`];
1369
+ delete env[`${name}_DESCRIPTION`];
1370
+ }
1340
1371
  delete env.ANTHROPIC_CUSTOM_MODEL_OPTION;
1341
1372
  delete env.ANTHROPIC_CUSTOM_MODEL_OPTION_NAME;
1342
1373
  delete env.ANTHROPIC_CUSTOM_MODEL_OPTION_DESCRIPTION;
1343
- writeClaudeCodeModelPresentation(env);
1344
1374
  // Per-model limits must come from Claude Code's recognized native model ids. This
1345
1375
  // global override would incorrectly give Haiku the 1M window.
1346
1376
  delete env.CLAUDE_CODE_MAX_CONTEXT_TOKENS;
@@ -1472,11 +1502,14 @@ export async function runPenny(argv = process.argv.slice(2)) {
1472
1502
  // Refresh on every launch so profile selection is honored immediately,
1473
1503
  // including --private, --cloud, and named profiles.
1474
1504
  const catalog = await resolveClaudeCodeLaunchModels(config);
1505
+ models = catalog.privateGateway || !catalog.nativeCatalogLive
1506
+ ? {} : hostedClaudeSlots(catalog.models);
1475
1507
  await writeClaudeLaunchSettings({
1476
1508
  gatewayBaseUrl: config.gateway_base_url,
1477
1509
  apiKey: config.api_key,
1478
1510
  models: catalog.models,
1479
1511
  privateGateway: catalog.privateGateway,
1512
+ slotModels: models,
1480
1513
  });
1481
1514
  }
1482
1515
  if (harness === "opencode") models = await resolveOpenCodeLaunchModels(config);
@@ -136,11 +136,11 @@ async function main() {
136
136
  const claudeLaunchSettings = JSON.parse(
137
137
  fs.readFileSync(claudeLaunchSettingsPath(), "utf8"),
138
138
  );
139
- assert.deepEqual(claudeLaunchSettings.env, {
140
- ANTHROPIC_BASE_URL: "https://gateway.example/",
141
- ANTHROPIC_AUTH_TOKEN: "pr-super-secret",
142
- ANTHROPIC_API_KEY: "",
143
- });
139
+ assert.equal(claudeLaunchSettings.env.ANTHROPIC_BASE_URL, "https://gateway.example/");
140
+ assert.equal(claudeLaunchSettings.env.ANTHROPIC_AUTH_TOKEN, "pr-super-secret");
141
+ assert.equal(claudeLaunchSettings.env.ANTHROPIC_API_KEY, "");
142
+ assert.equal(claudeLaunchSettings.env.ANTHROPIC_DEFAULT_OPUS_MODEL, "");
143
+ assert.equal(claudeLaunchSettings.env.ANTHROPIC_DEFAULT_OPUS_MODEL_NAME, "");
144
144
  await writeClaudeLaunchSettings({
145
145
  gatewayBaseUrl: "https://gateway.example/",
146
146
  apiKey: "pr-super-secret",
@@ -153,6 +153,15 @@ async function main() {
153
153
  fs.readFileSync(claudeLaunchSettingsPath(), "utf8"),
154
154
  );
155
155
  assert.equal(hostedClaudeSettings.modelPicker, undefined);
156
+ await writeClaudeLaunchSettings({
157
+ gatewayBaseUrl: "https://gateway.example/",
158
+ apiKey: "pr-super-secret",
159
+ slotModels: { opus: "claude-opus-5-5[1m]" },
160
+ });
161
+ const refreshedClaudeSettings = JSON.parse(
162
+ fs.readFileSync(claudeLaunchSettingsPath(), "utf8"),
163
+ );
164
+ assert.equal(refreshedClaudeSettings.env.ANTHROPIC_DEFAULT_OPUS_MODEL, "claude-opus-5-5[1m]");
156
165
  await writeClaudeLaunchSettings({
157
166
  gatewayBaseUrl: "https://gateway.example/",
158
167
  apiKey: "pr-super-secret",
@@ -260,9 +269,13 @@ async function main() {
260
269
  // would wrongly hand Haiku a 1M window too.
261
270
  assert.equal(claude.env.ANTHROPIC_DEFAULT_OPUS_MODEL, "claude-opus-5[1m]");
262
271
  assert.equal(claude.env.ANTHROPIC_DEFAULT_HAIKU_MODEL, "claude-haiku-4-5-20251001");
263
- assert.equal(claude.env.ANTHROPIC_DEFAULT_HAIKU_MODEL_NAME, "Haiku 4.5");
264
- assert.equal(claude.env.ANTHROPIC_DEFAULT_HAIKU_MODEL_DESCRIPTION, "Fast, economical Claude model · 200K context");
272
+ assert.equal(claude.env.ANTHROPIC_DEFAULT_HAIKU_MODEL_NAME, undefined);
273
+ assert.equal(claude.env.ANTHROPIC_DEFAULT_HAIKU_MODEL_DESCRIPTION, undefined);
265
274
  assert.equal(claude.env.CLAUDE_CODE_MAX_CONTEXT_TOKENS, undefined);
275
+ const refreshedClaude = buildPennyInvocation("claude", [], config, {
276
+ haiku: "claude-haiku-4-5-20251001", opus: "claude-opus-5-5[1m]",
277
+ });
278
+ assert.equal(refreshedClaude.env.ANTHROPIC_DEFAULT_OPUS_MODEL, "claude-opus-5-5[1m]");
266
279
 
267
280
  const proxyStartup = buildPennyInvocation("claude", ["proxy"], config);
268
281
  assert.deepEqual(proxyStartup.args.slice(-1), ["@penny auth proxy"]);
@@ -289,16 +302,20 @@ async function main() {
289
302
  assert.ok(command.includes("@penny $ARGUMENTS"));
290
303
  assert.ok(command.includes("name: penny"));
291
304
 
292
- // Persistent direct-Claude installs use the same native ids and must not
293
- // leave the former global 1M override behind.
305
+ // A hosted reinstall clears old fixed slots so Claude Code can use its live
306
+ // gateway catalog and its own current built-in models.
307
+ fs.writeFileSync(path.join(home, ".claude", "settings.json"), JSON.stringify({
308
+ env: { ANTHROPIC_DEFAULT_OPUS_MODEL: "claude-opus-5[1m]",
309
+ ANTHROPIC_DEFAULT_OPUS_MODEL_NAME: "Opus 5" },
310
+ }));
294
311
  await claudeCodeHarness.install({
295
312
  apiKey: "pr-super-secret",
296
313
  gatewayBaseUrl: "https://gateway.example/",
297
314
  plan: { configPath: path.join(home, ".claude", "settings.json") },
298
315
  });
299
316
  const persistedClaude = JSON.parse(fs.readFileSync(path.join(home, ".claude", "settings.json"), "utf8"));
300
- assert.equal(persistedClaude.env.ANTHROPIC_DEFAULT_HAIKU_MODEL, "claude-haiku-4-5-20251001");
301
- assert.equal(persistedClaude.env.ANTHROPIC_DEFAULT_SONNET_MODEL, "claude-sonnet-5[1m]");
317
+ assert.equal("ANTHROPIC_DEFAULT_OPUS_MODEL" in persistedClaude.env, false);
318
+ assert.equal("ANTHROPIC_DEFAULT_OPUS_MODEL_NAME" in persistedClaude.env, false);
302
319
  assert.equal("CLAUDE_CODE_MAX_CONTEXT_TOKENS" in persistedClaude.env, false);
303
320
 
304
321
  // Private gateways own the model catalog. Their picker must not merge the