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 +1 -1
- package/src/cli.js +37 -14
- package/src/harnesses/claude-code.js +20 -17
- package/src/launch.js +40 -7
- package/src/launch.test.js +28 -11
package/package.json
CHANGED
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
|
|
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
|
-
|
|
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
|
-
|
|
1733
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
//
|
|
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/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
|