pennyrouter 0.3.31 → 0.3.33

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.31",
3
+ "version": "0.3.33",
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
@@ -1595,9 +1595,7 @@ async function selectCodexFundingProviders(flags, selected) {
1595
1595
 
1596
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.");
1597
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
- );
1598
+ const options = CODEX_PROVIDER_OPTIONS;
1601
1599
  return promptFundingProviderCheckboxes(
1602
1600
  options,
1603
1601
  "Which funding source should PennyRouter use for Codex?",
@@ -1703,7 +1701,7 @@ async function configureUserFunding(flags, selected, claudeAnthropicAuth,
1703
1701
  || process.env.PENNYROUTER_ANTHROPIC_PROXY_API_KEY
1704
1702
  || process.env.PENNYROUTER_PROXY_API_KEY || "",
1705
1703
  ).trim();
1706
- const codexSubscriptionAuth = wantsCodex && codexProviders.has("subscription")
1704
+ let codexSubscriptionAuth = wantsCodex && codexProviders.has("subscription")
1707
1705
  ? await detectCodexChatgptAuth()
1708
1706
  : false;
1709
1707
 
@@ -1729,9 +1727,15 @@ async function configureUserFunding(flags, selected, claudeAnthropicAuth,
1729
1727
  }
1730
1728
  }
1731
1729
  if (wantsCodex && codexProviders.has("subscription") && !codexSubscriptionAuth) {
1732
- throw new Error(
1733
- "ChatGPT subscription was selected, but Codex is not logged in. Run `codex login`, or select an API-key provider.",
1734
- );
1730
+ console.log("Codex needs to sign in to your ChatGPT account. Starting `codex login`...");
1731
+ const login = spawnSync("codex", ["login"], {
1732
+ stdio: "inherit",
1733
+ shell: process.platform === "win32",
1734
+ });
1735
+ if (login.error || login.status !== 0 || !(await detectCodexChatgptAuth())) {
1736
+ throw new Error("Codex login did not complete. Run `codex login`, then rerun `npx pennyrouter install`.");
1737
+ }
1738
+ codexSubscriptionAuth = true;
1735
1739
  }
1736
1740
  if (wantsCodex && codexProviders.has("openai-api") && !openaiApiKey) {
1737
1741
  if (!interactive) throw new Error("OpenAI API was selected, but no key was provided.");
@@ -2129,14 +2133,17 @@ async function selectHarnesses(flags) {
2129
2133
  }
2130
2134
 
2131
2135
  const detected = [];
2136
+ const available = [];
2132
2137
  for (const id of DEFAULT_INSTALL_HARNESS_IDS) {
2133
2138
  const harness = HARNESS_BY_ID[id];
2134
- if (await safeDetect(harness)) detected.push(harness);
2139
+ const installed = await safeDetect(harness);
2140
+ if (installed) detected.push(harness);
2141
+ available.push({ ...harness, installed });
2135
2142
  }
2136
2143
 
2137
2144
  if (flags.yes) return detected;
2138
2145
 
2139
- return promptHarnessCheckboxes(detected);
2146
+ return promptHarnessCheckboxes(available, detected);
2140
2147
  }
2141
2148
 
2142
2149
  async function printAdditionalHarnessesHint(selected) {
@@ -2153,10 +2160,14 @@ async function printAdditionalHarnessesHint(selected) {
2153
2160
  console.log(`To use PennyRouter with ${names}, install ${available.length === 1 ? "it" : "them"} and re-run \`npx pennyrouter install\`.`);
2154
2161
  }
2155
2162
 
2156
- async function promptHarnessCheckboxes(detected) {
2163
+ async function promptHarnessCheckboxes(available, detected) {
2157
2164
  const detectedIds = new Set(detected.map((harness) => harness.id));
2158
2165
  const selectedIds = new Set(detectedIds);
2159
- const harnesses = orderedHarnessesForPrompt(detectedIds);
2166
+ const harnesses = orderedHarnessesForPrompt(new Set(available.map((harness) => harness.id)))
2167
+ .map((harness) => ({
2168
+ ...harness,
2169
+ installed: available.find((item) => item.id === harness.id)?.installed ?? false,
2170
+ }));
2160
2171
  if (!input.isTTY || !output.isTTY) return promptHarnessCheckboxesByText(harnesses, selectedIds);
2161
2172
 
2162
2173
  return promptHarnessCheckboxesByKeys(harnesses, selectedIds);
@@ -2178,7 +2189,8 @@ async function promptHarnessCheckboxesByKeys(harnesses, selectedIds) {
2178
2189
  harnesses.forEach((harness, index) => {
2179
2190
  const pointer = index === cursor ? color(">", "cyan") : " ";
2180
2191
  const checked = selectedIds.has(harness.id) ? color("x", "green") : " ";
2181
- const name = index === cursor ? color(harness.name, "cyan") : harness.name;
2192
+ const label = harness.installed ? harness.name : `${harness.name} (install ${harness.name} first)`;
2193
+ const name = harness.installed && index === cursor ? color(label, "cyan") : label;
2182
2194
  output.write(` ${pointer} [${checked}] ${name}\n`);
2183
2195
  });
2184
2196
  output.write("\nEnter to continue.\n");
@@ -2215,7 +2227,12 @@ async function promptHarnessCheckboxesByKeys(harnesses, selectedIds) {
2215
2227
  }
2216
2228
 
2217
2229
  if (key.name === "space") {
2218
- const id = harnesses[cursor].id;
2230
+ const harness = harnesses[cursor];
2231
+ if (!harness.installed) {
2232
+ render();
2233
+ return;
2234
+ }
2235
+ const id = harness.id;
2219
2236
  if (selectedIds.has(id)) selectedIds.delete(id);
2220
2237
  else selectedIds.add(id);
2221
2238
  render();
@@ -2242,7 +2259,8 @@ async function promptHarnessCheckboxesByText(harnesses, selectedIds) {
2242
2259
  console.log(color("Where should PennyRouter be installed?", "bold"));
2243
2260
  for (const harness of harnesses) {
2244
2261
  const checked = selectedIds.has(harness.id) ? color("x", "green") : " ";
2245
- console.log(` [${checked}] ${harness.name}`);
2262
+ const label = harness.installed ? harness.name : `${harness.name} (install ${harness.name} first)`;
2263
+ console.log(` [${checked}] ${label}`);
2246
2264
  }
2247
2265
  console.log("");
2248
2266
  const answer = await rl.question(
@@ -2261,6 +2279,11 @@ async function promptHarnessCheckboxesByText(harnesses, selectedIds) {
2261
2279
  console.log(`Unknown tool "${token}".`);
2262
2280
  continue;
2263
2281
  }
2282
+ if (!harnesses.find((harness) => harness.id === id)?.installed) {
2283
+ const unavailable = HARNESS_BY_ID[id];
2284
+ console.log(`Install ${unavailable.name} first to select it.`);
2285
+ continue;
2286
+ }
2264
2287
  if (selectedIds.has(id)) selectedIds.delete(id);
2265
2288
  else selectedIds.add(id);
2266
2289
  }
@@ -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;
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