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 +1 -1
- package/src/cli.js +45 -7
- package/src/harnesses/claude-code.js +20 -17
- package/src/harnesses/codex.js +2 -0
- package/src/launch.js +40 -7
- package/src/launch.test.js +28 -11
package/package.json
CHANGED
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
|
-
|
|
1559
|
-
"Which
|
|
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([
|
|
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
|
-
|
|
1585
|
-
"Which
|
|
1586
|
-
new Set([
|
|
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
|
-
|
|
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
|
-
//
|
|
160
|
-
//
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
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/harnesses/codex.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
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);
|
package/src/launch.test.js
CHANGED
|
@@ -136,11 +136,11 @@ async function main() {
|
|
|
136
136
|
const claudeLaunchSettings = JSON.parse(
|
|
137
137
|
fs.readFileSync(claudeLaunchSettingsPath(), "utf8"),
|
|
138
138
|
);
|
|
139
|
-
assert.
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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,
|
|
264
|
-
assert.equal(claude.env.ANTHROPIC_DEFAULT_HAIKU_MODEL_DESCRIPTION,
|
|
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
|
-
//
|
|
293
|
-
//
|
|
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
|
|
301
|
-
assert.equal(persistedClaude.env
|
|
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
|