pennyrouter 0.2.4 → 0.2.7
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 +41 -4
- package/package.json +1 -1
- package/src/cli.js +149 -0
- package/src/harnesses/claude-code.js +37 -14
- package/src/launch.js +4 -0
- package/src/launch.test.js +2 -2
- package/src/session.js +1 -1
package/README.md
CHANGED
|
@@ -11,6 +11,7 @@ status checks, and uninstall metadata.
|
|
|
11
11
|
```bash
|
|
12
12
|
npx pennyrouter install
|
|
13
13
|
npx pennyrouter install --make-default
|
|
14
|
+
npx pennyrouter update
|
|
14
15
|
npx pennyrouter auth anthropic
|
|
15
16
|
npx pennyrouter install --harness claude-code,cline,opencode,codex,windsurf,zed,codegpt,aider
|
|
16
17
|
npx pennyrouter disable
|
|
@@ -79,6 +80,39 @@ the gateway must be redeployed or fixed before retrying. This check is about Pen
|
|
|
79
80
|
compatibility, not a demand that the user upgrade a Claude Code version that already emits a
|
|
80
81
|
supported request shape.
|
|
81
82
|
|
|
83
|
+
## Updating
|
|
84
|
+
|
|
85
|
+
To pick up a new CLI version without repeating the install interview:
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
npx pennyrouter update
|
|
89
|
+
npx pennyrouter update --dry-run
|
|
90
|
+
npx pennyrouter update --harness claude-code
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
`update` refreshes only the files generated from the CLI's own constants: the managed `penny`
|
|
94
|
+
runtime and launcher, the `/penny` Claude skill, the Claude settings overlay and statusline, the
|
|
95
|
+
Codex provider profile, and the model labels and context-window settings in managed tool configs.
|
|
96
|
+
Older installs are migrated forward, so retired model labels are replaced with the current ones.
|
|
97
|
+
|
|
98
|
+
It never touches credentials and never re-runs browser authorization. Upstream provider tokens
|
|
99
|
+
(Claude subscription, Anthropic, OpenAI, OpenRouter) are held encrypted at the gateway, and the
|
|
100
|
+
existing `pr-` account key and gateway URL are read back from the local config, so the command is
|
|
101
|
+
non-interactive and prompts for nothing.
|
|
102
|
+
|
|
103
|
+
Per-tool choices are preserved rather than re-asked. Bare-command routing is read from the
|
|
104
|
+
uninstall manifest, so a Penny-only install keeps bare `claude` and `codex` fully native and an
|
|
105
|
+
install that opted into global routing keeps it. Subscription auth mode is likewise carried
|
|
106
|
+
forward. Use `install` instead when the intent is to change those choices.
|
|
107
|
+
|
|
108
|
+
Because the managed tool config is rewritten only for tools whose bare command already routes
|
|
109
|
+
through PennyRouter, a Penny-only install leaves files such as `~/.claude/settings.json`
|
|
110
|
+
untouched and needs no session shutdown. Updating a tool that owns global routing requires
|
|
111
|
+
closing its running sessions, or passing `--allow-running-harnesses`. Unrelated keys and user
|
|
112
|
+
settings inside managed config files are preserved, and re-running `update` is idempotent.
|
|
113
|
+
|
|
114
|
+
Restart Claude Code or Codex afterwards to pick up the refreshed configuration.
|
|
115
|
+
|
|
82
116
|
## Supported Tools
|
|
83
117
|
|
|
84
118
|
- Claude Code
|
|
@@ -97,10 +131,10 @@ named-model choices PIN the trunk to that exact model — you pick the model, Pe
|
|
|
97
131
|
the bill via its side machinery (cache/compaction/swallowed background calls) without switching the
|
|
98
132
|
model you chose. "Penny Custom" is the dynamic option: PennyRouter cost-routes each turn.
|
|
99
133
|
|
|
100
|
-
-
|
|
101
|
-
-
|
|
102
|
-
-
|
|
103
|
-
-
|
|
134
|
+
- Opus 5 (PennyRouter): pins `anthropic/claude-opus-5`
|
|
135
|
+
- Sonnet 5 (PennyRouter): pins `anthropic/claude-sonnet-5`
|
|
136
|
+
- Haiku 4.5 (PennyRouter): pins `anthropic/claude-haiku-4-5-20251001`
|
|
137
|
+
- Fable 5 (PennyRouter): pins `anthropic/claude-fable-5`
|
|
104
138
|
- Penny Custom: `pennyrouter/auto` — dynamic routing on the bundle/profile saved in the user's account
|
|
105
139
|
|
|
106
140
|
## Claude Subscription Auth
|
|
@@ -178,6 +212,9 @@ directory, and records an uninstall manifest.
|
|
|
178
212
|
`disable` temporarily removes PennyRouter from selected tools while retaining a local
|
|
179
213
|
PennyRouter-managed snapshot for `enable`.
|
|
180
214
|
|
|
215
|
+
`update` rewrites those same managed files for the installed version without re-authorizing,
|
|
216
|
+
and without changing bare-command routing or stored credentials.
|
|
217
|
+
|
|
181
218
|
It does not delete PennyRouter accounts, credits, or remote API keys.
|
|
182
219
|
|
|
183
220
|
## License
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -85,6 +85,11 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
85
85
|
return;
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
+
if (command === "update") {
|
|
89
|
+
await update(flags);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
88
93
|
if (command === "auth") {
|
|
89
94
|
await authProvider(flags);
|
|
90
95
|
return;
|
|
@@ -571,6 +576,143 @@ async function install(flags) {
|
|
|
571
576
|
reportFailures(failures);
|
|
572
577
|
}
|
|
573
578
|
|
|
579
|
+
// Refresh the managed local files to this package version WITHOUT re-running any of the
|
|
580
|
+
// account/credential flow. Everything an update needs is already on disk: the pr- key and
|
|
581
|
+
// gateway URL live in the penny config (and, for older installs, in the harness config the
|
|
582
|
+
// installer wrote), and the upstream provider tokens live server-side, encrypted against the
|
|
583
|
+
// account. So no browser claim, no provider checkboxes, no token prompts — this command is
|
|
584
|
+
// non-interactive by construction and never touches a credential.
|
|
585
|
+
//
|
|
586
|
+
// What it DOES rewrite is everything regenerated from code constants, which is exactly what
|
|
587
|
+
// goes stale on a version bump: the copied runtime + `penny` shim, the /penny skill, the
|
|
588
|
+
// Claude launch settings overlay, the Codex provider profile, and the model labels +
|
|
589
|
+
// context-window env in the harness configs.
|
|
590
|
+
//
|
|
591
|
+
// Per-user choices are read back from the manifest, never re-asked: `default_routing` decides
|
|
592
|
+
// whether the bare `claude`/`codex` command keeps pointing at the gateway, and
|
|
593
|
+
// `anthropic_token_stored`/`openai_subscription_auth` decide which auth shape gets rewritten.
|
|
594
|
+
// Re-deriving either from prompt defaults would silently flip a --penny-only user into
|
|
595
|
+
// default routing, or drop subscription auth from a working install.
|
|
596
|
+
async function update(flags) {
|
|
597
|
+
const manifest = await loadManifest();
|
|
598
|
+
if (pruneUnsupportedManifestRecords(manifest).length > 0) await saveManifest(manifest);
|
|
599
|
+
const records = manifest.installations || [];
|
|
600
|
+
|
|
601
|
+
const requested = parseHarnessList(flags.harness);
|
|
602
|
+
const targets = records.filter((record) => {
|
|
603
|
+
if (record.disabled) return false;
|
|
604
|
+
if (flags.all || requested.length === 0) return true;
|
|
605
|
+
return requested.includes(record.harness);
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
if (records.length === 0) {
|
|
609
|
+
throw new Error("No PennyRouter installation found. Run `npx pennyrouter install` first.");
|
|
610
|
+
}
|
|
611
|
+
if (targets.length === 0) {
|
|
612
|
+
console.log("No enabled PennyRouter installations matched. Nothing to update.");
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
const launchConfig = await loadPennyConfig();
|
|
617
|
+
const globalAuth = await readClaudeCodeGatewayAuth();
|
|
618
|
+
const apiKey = flags.pennyKey || launchConfig.api_key || globalAuth.apiKey;
|
|
619
|
+
const gatewayBaseUrl = flags.gatewayBaseUrl
|
|
620
|
+
|| launchConfig.gateway_base_url
|
|
621
|
+
|| globalAuth.gatewayBaseUrl
|
|
622
|
+
|| process.env.PENNYROUTER_GATEWAY_BASE_URL
|
|
623
|
+
|| "https://api.pennyrouter.com";
|
|
624
|
+
|
|
625
|
+
if (!apiKey) {
|
|
626
|
+
throw new Error(
|
|
627
|
+
"Could not find your PennyRouter key in the local config. Pass `--penny-key pr-...`, "
|
|
628
|
+
+ "or run `npx pennyrouter install` to re-authorize.",
|
|
629
|
+
);
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
console.log("PennyRouter update");
|
|
633
|
+
console.log("");
|
|
634
|
+
console.log(`Key ${maskKey(apiKey)} · gateway ${gatewayBaseUrl}`);
|
|
635
|
+
console.log("Subscription and API keys are left untouched.");
|
|
636
|
+
console.log("");
|
|
637
|
+
|
|
638
|
+
if (flags.dryRun) {
|
|
639
|
+
for (const record of targets) {
|
|
640
|
+
const routing = record.default_routing === false ? "penny command only" : "default routing";
|
|
641
|
+
console.log(`Will refresh ${record.harness} (${routing}).`);
|
|
642
|
+
}
|
|
643
|
+
console.log("Will refresh the penny runtime and shim.");
|
|
644
|
+
console.log("Dry run only. No files changed.");
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
const failures = [];
|
|
649
|
+
let runtimeResult = null;
|
|
650
|
+
if (targets.some((record) => ["claude-code", "codex"].includes(record.harness))
|
|
651
|
+
&& process.platform !== "win32") {
|
|
652
|
+
runtimeResult = await installPennyRuntime({ updatePath: !flags.noPathUpdate });
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
for (const record of targets) {
|
|
656
|
+
const harness = HARNESS_BY_ID[record.harness];
|
|
657
|
+
if (!harness) continue;
|
|
658
|
+
|
|
659
|
+
await runHarnessJob(failures, record.harness, "update", async () => {
|
|
660
|
+
const supportsPennyCommand = ["claude-code", "codex"].includes(record.harness)
|
|
661
|
+
&& process.platform !== "win32";
|
|
662
|
+
// An update rewrites the harness's own config only when that config is ours to
|
|
663
|
+
// rewrite — i.e. the bare command still routes through the gateway. In penny-only
|
|
664
|
+
// mode the harness config belongs to the user and we touch just the launch profile.
|
|
665
|
+
const ownsGlobalRouting = !record.launch_available || record.default_routing !== false;
|
|
666
|
+
if (ownsGlobalRouting) assertNoRunningHarness(record.harness, flags);
|
|
667
|
+
|
|
668
|
+
const claudeSubscriptionAuth = record.harness === "claude-code"
|
|
669
|
+
&& Boolean(record.anthropic_token_stored);
|
|
670
|
+
const codexSubscriptionAuth = record.harness === "codex"
|
|
671
|
+
? await detectCodexChatgptAuth()
|
|
672
|
+
: false;
|
|
673
|
+
|
|
674
|
+
if (supportsPennyCommand) {
|
|
675
|
+
await configurePennyHarness(record.harness, {
|
|
676
|
+
apiKey,
|
|
677
|
+
gatewayBaseUrl,
|
|
678
|
+
subscriptionAuth: record.harness === "claude-code"
|
|
679
|
+
? claudeSubscriptionAuth
|
|
680
|
+
: codexSubscriptionAuth,
|
|
681
|
+
models: record.harness === "claude-code" ? CLAUDE_CODE_MODELS : null,
|
|
682
|
+
});
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
let next = record;
|
|
686
|
+
if (ownsGlobalRouting) {
|
|
687
|
+
const plan = await harness.planInstall();
|
|
688
|
+
// Pass the existing record through so install-time state the harness stashed
|
|
689
|
+
// (previous_model_provider, claude_ai_connectors_before) survives the rewrite
|
|
690
|
+
// and a later uninstall still restores the user's original values.
|
|
691
|
+
const installed = record.harness === "claude-code" && claudeSubscriptionAuth
|
|
692
|
+
? await harness.installStoredToken({ apiKey, gatewayBaseUrl, plan, existingRecord: record })
|
|
693
|
+
: await harness.install({ apiKey, gatewayBaseUrl, plan, existingRecord: record });
|
|
694
|
+
next = { ...record, ...installed, installed_at: record.installed_at };
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
next.updated_at = new Date().toISOString();
|
|
698
|
+
if (record.harness === "codex") next.openai_subscription_auth = codexSubscriptionAuth;
|
|
699
|
+
upsertManifestRecord(manifest, next);
|
|
700
|
+
await saveManifest(manifest);
|
|
701
|
+
console.log(`Updated ${record.harness}.`);
|
|
702
|
+
});
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
await saveManifest(manifest);
|
|
706
|
+
console.log("");
|
|
707
|
+
if (runtimeResult && !runtimeResult.available) {
|
|
708
|
+
console.log("Add ~/.local/bin to PATH before using the `penny` command.");
|
|
709
|
+
} else if (runtimeResult?.changed) {
|
|
710
|
+
console.log("Open a new terminal (or reload your shell) before using the `penny` command.");
|
|
711
|
+
}
|
|
712
|
+
console.log("Restart any running Claude Code or Codex sessions to pick up the update.");
|
|
713
|
+
reportFailures(failures);
|
|
714
|
+
}
|
|
715
|
+
|
|
574
716
|
async function chooseDefaultRouting(flags, selected, manifest) {
|
|
575
717
|
const out = new Map();
|
|
576
718
|
const launchable = selected.filter((harness) => ["claude-code", "codex"].includes(harness.id));
|
|
@@ -1560,13 +1702,20 @@ function printHelp() {
|
|
|
1560
1702
|
|
|
1561
1703
|
Usage:
|
|
1562
1704
|
pennyrouter install [--harness ${SUPPORTED_HARNESS_IDS}] [--all] [--yes] [--dry-run] [--make-default|--penny-only] [--no-path-update] [--anthropic-auth|--no-anthropic-auth] [--anthropic-api-key KEY] [--openai-api-key KEY] [--openrouter-api-key KEY] [--local[=URL]] [--allow-running-harnesses]
|
|
1705
|
+
pennyrouter update [--harness ${SUPPORTED_HARNESS_IDS}] [--all] [--dry-run] [--penny-key pr-...] [--gateway-base-url URL] [--allow-running-harnesses]
|
|
1563
1706
|
pennyrouter auth anthropic [--penny-key pr-...] [--token oauth-token] [--token-command CMD] [--gateway-base-url URL] [--forget]
|
|
1564
1707
|
pennyrouter disable [--harness ${SUPPORTED_HARNESS_IDS}] [--all] [--yes]
|
|
1565
1708
|
pennyrouter enable [--harness ${SUPPORTED_HARNESS_IDS}] [--all] [--yes]
|
|
1566
1709
|
pennyrouter uninstall [--harness ${SUPPORTED_HARNESS_IDS}] [--all] [--yes] [--forget-token]
|
|
1567
1710
|
pennyrouter status
|
|
1568
1711
|
|
|
1712
|
+
Update refreshes the managed local config and the penny runtime to this package version.
|
|
1713
|
+
It reuses your existing key and never re-asks for subscription or API credentials.
|
|
1714
|
+
|
|
1569
1715
|
Examples:
|
|
1716
|
+
npx pennyrouter update
|
|
1717
|
+
npx pennyrouter update --dry-run
|
|
1718
|
+
npx pennyrouter update --harness claude-code
|
|
1570
1719
|
npx pennyrouter install
|
|
1571
1720
|
npx pennyrouter install --make-default
|
|
1572
1721
|
npx pennyrouter install --penny-only
|
|
@@ -7,22 +7,29 @@ const CONNECTOR_SETTING = "disableClaudeAiConnectors";
|
|
|
7
7
|
// Values written into Claude Code's four model slots. CC displays a slot's VALUE verbatim in
|
|
8
8
|
// its /model menu AND sends it as the request `model`, so each value is both the menu label
|
|
9
9
|
// and the gateway's routing key. The three concrete Claude slots carry friendly PINNED labels
|
|
10
|
-
// ("
|
|
11
|
-
// runs on the model they chose, no dynamic tier switching on the
|
|
12
|
-
// calls still route cheap, which is where the savings come from).
|
|
13
|
-
//
|
|
14
|
-
// "Penny
|
|
15
|
-
//
|
|
10
|
+
// ("Opus 5/Sonnet 5/Haiku 4.5/Fable 5 (PennyRouter)") that the gateway maps to that exact
|
|
11
|
+
// model — the user's pick runs on the model they chose, no dynamic tier switching on the
|
|
12
|
+
// trunk (background/utility calls still route cheap, which is where the savings come from).
|
|
13
|
+
// Named "<Model> (PennyRouter)" rather than "Penny <Model>" so CC's own generated "Custom ...
|
|
14
|
+
// model" subtext still reads as a real Claude model routed through Penny, not a distinct
|
|
15
|
+
// custom model. The separate Fable slot is pinned to Fable; the DYNAMIC "Penny Custom" option
|
|
16
|
+
// lets PennyRouter cost-route. Legacy "Penny Speed/Core/Power/Opus/Sonnet/Haiku/Fable"
|
|
17
|
+
// installs (dynamic bundles, or the pre-rename pinned labels) are still recognized for
|
|
18
|
+
// uninstall (see isPennyRouterModelValue).
|
|
16
19
|
export const CLAUDE_CODE_MODELS = {
|
|
17
|
-
haiku: "
|
|
18
|
-
sonnet: "
|
|
19
|
-
opus: "
|
|
20
|
-
fable: "
|
|
21
|
-
custom: "Penny Custom",
|
|
20
|
+
haiku: "Haiku 4.5 (PennyRouter)", // -> pinned anthropic/claude-haiku-4-5-20251001
|
|
21
|
+
sonnet: "Sonnet 5 (PennyRouter)", // -> pinned anthropic/claude-sonnet-5
|
|
22
|
+
opus: "Opus 5 (PennyRouter)", // -> pinned anthropic/claude-opus-5
|
|
23
|
+
fable: "Fable 5 (PennyRouter)", // -> pinned anthropic/claude-fable-5
|
|
24
|
+
custom: "Penny Custom", // -> dynamic (PennyRouter cost-routing)
|
|
22
25
|
};
|
|
23
|
-
// Retired dynamic-bundle labels (pre-pinned-trunk)
|
|
24
|
-
// only so uninstall/detect still recognize and clean up
|
|
25
|
-
|
|
26
|
+
// Retired dynamic-bundle labels (pre-pinned-trunk) and the pre-rename pinned labels. Not
|
|
27
|
+
// written by a fresh install; matched only so uninstall/detect still recognize and clean up
|
|
28
|
+
// an older install.
|
|
29
|
+
const LEGACY_PENNY_LABELS = [
|
|
30
|
+
"Penny Speed", "Penny Core", "Penny Power",
|
|
31
|
+
"Penny Opus", "Penny Sonnet", "Penny Haiku", "Penny Fable",
|
|
32
|
+
];
|
|
26
33
|
|
|
27
34
|
export const claudeCodeHarness = {
|
|
28
35
|
id: "claude-code",
|
|
@@ -60,6 +67,10 @@ export const claudeCodeHarness = {
|
|
|
60
67
|
current.env.ANTHROPIC_DEFAULT_SONNET_MODEL = CLAUDE_CODE_MODELS.sonnet;
|
|
61
68
|
current.env.ANTHROPIC_DEFAULT_HAIKU_MODEL = CLAUDE_CODE_MODELS.haiku;
|
|
62
69
|
current.env.ANTHROPIC_DEFAULT_FABLE_MODEL = CLAUDE_CODE_MODELS.fable;
|
|
70
|
+
// The pinned labels above aren't real Anthropic model ids, so CC can't infer their
|
|
71
|
+
// context window from the name and falls back to a conservative 200K default. Tell it
|
|
72
|
+
// directly: every pinned slot is a Claude model with the 1M-context beta enabled upstream.
|
|
73
|
+
current.env.CLAUDE_CODE_MAX_CONTEXT_TOKENS = "1000000";
|
|
63
74
|
|
|
64
75
|
const statusline = await configureClaudeCodeStatusline(current);
|
|
65
76
|
|
|
@@ -82,6 +93,7 @@ export const claudeCodeHarness = {
|
|
|
82
93
|
"ANTHROPIC_DEFAULT_SONNET_MODEL",
|
|
83
94
|
"ANTHROPIC_DEFAULT_HAIKU_MODEL",
|
|
84
95
|
"ANTHROPIC_DEFAULT_FABLE_MODEL",
|
|
96
|
+
"CLAUDE_CODE_MAX_CONTEXT_TOKENS",
|
|
85
97
|
],
|
|
86
98
|
},
|
|
87
99
|
};
|
|
@@ -105,6 +117,10 @@ export const claudeCodeHarness = {
|
|
|
105
117
|
current.env.ANTHROPIC_DEFAULT_SONNET_MODEL = CLAUDE_CODE_MODELS.sonnet;
|
|
106
118
|
current.env.ANTHROPIC_DEFAULT_HAIKU_MODEL = CLAUDE_CODE_MODELS.haiku;
|
|
107
119
|
current.env.ANTHROPIC_DEFAULT_FABLE_MODEL = CLAUDE_CODE_MODELS.fable;
|
|
120
|
+
// The pinned labels above aren't real Anthropic model ids, so CC can't infer their
|
|
121
|
+
// context window from the name and falls back to a conservative 200K default. Tell it
|
|
122
|
+
// directly: every pinned slot is a Claude model with the 1M-context beta enabled upstream.
|
|
123
|
+
current.env.CLAUDE_CODE_MAX_CONTEXT_TOKENS = "1000000";
|
|
108
124
|
|
|
109
125
|
const statusline = await configureClaudeCodeStatusline(current);
|
|
110
126
|
|
|
@@ -127,6 +143,7 @@ export const claudeCodeHarness = {
|
|
|
127
143
|
"ANTHROPIC_DEFAULT_SONNET_MODEL",
|
|
128
144
|
"ANTHROPIC_DEFAULT_HAIKU_MODEL",
|
|
129
145
|
"ANTHROPIC_DEFAULT_FABLE_MODEL",
|
|
146
|
+
"CLAUDE_CODE_MAX_CONTEXT_TOKENS",
|
|
130
147
|
],
|
|
131
148
|
},
|
|
132
149
|
};
|
|
@@ -146,6 +163,10 @@ export const claudeCodeHarness = {
|
|
|
146
163
|
current.env.ANTHROPIC_DEFAULT_SONNET_MODEL = CLAUDE_CODE_MODELS.sonnet;
|
|
147
164
|
current.env.ANTHROPIC_DEFAULT_HAIKU_MODEL = CLAUDE_CODE_MODELS.haiku;
|
|
148
165
|
current.env.ANTHROPIC_DEFAULT_FABLE_MODEL = CLAUDE_CODE_MODELS.fable;
|
|
166
|
+
// The pinned labels above aren't real Anthropic model ids, so CC can't infer their
|
|
167
|
+
// context window from the name and falls back to a conservative 200K default. Tell it
|
|
168
|
+
// directly: every pinned slot is a Claude model with the 1M-context beta enabled upstream.
|
|
169
|
+
current.env.CLAUDE_CODE_MAX_CONTEXT_TOKENS = "1000000";
|
|
149
170
|
|
|
150
171
|
const statusline = await configureClaudeCodeStatusline(current);
|
|
151
172
|
|
|
@@ -168,6 +189,7 @@ export const claudeCodeHarness = {
|
|
|
168
189
|
"ANTHROPIC_DEFAULT_SONNET_MODEL",
|
|
169
190
|
"ANTHROPIC_DEFAULT_HAIKU_MODEL",
|
|
170
191
|
"ANTHROPIC_DEFAULT_FABLE_MODEL",
|
|
192
|
+
"CLAUDE_CODE_MAX_CONTEXT_TOKENS",
|
|
171
193
|
],
|
|
172
194
|
},
|
|
173
195
|
};
|
|
@@ -202,6 +224,7 @@ export const claudeCodeHarness = {
|
|
|
202
224
|
]) {
|
|
203
225
|
if (current.env[k] === LEGACY_MODEL_LABEL || isPennyRouterModelValue(current.env[k])) delete current.env[k];
|
|
204
226
|
}
|
|
227
|
+
delete current.env.CLAUDE_CODE_MAX_CONTEXT_TOKENS;
|
|
205
228
|
}
|
|
206
229
|
restoreClaudeAiConnectors(current, record);
|
|
207
230
|
if (current["anthropic.baseURL"] && isPennyRouterValue(current["anthropic.baseURL"])) {
|
package/src/launch.js
CHANGED
|
@@ -344,6 +344,10 @@ export function buildPennyInvocation(harness, args, config) {
|
|
|
344
344
|
if (models.sonnet) env.ANTHROPIC_DEFAULT_SONNET_MODEL = models.sonnet;
|
|
345
345
|
if (models.opus) env.ANTHROPIC_DEFAULT_OPUS_MODEL = models.opus;
|
|
346
346
|
if (models.fable) env.ANTHROPIC_DEFAULT_FABLE_MODEL = models.fable;
|
|
347
|
+
// The pinned labels above aren't real Anthropic model ids, so CC can't infer their
|
|
348
|
+
// context window from the name and falls back to a conservative 200K default. Tell it
|
|
349
|
+
// directly: every pinned slot is a Claude model with the 1M-context beta enabled upstream.
|
|
350
|
+
env.CLAUDE_CODE_MAX_CONTEXT_TOKENS = "1000000";
|
|
347
351
|
return {
|
|
348
352
|
command: "claude",
|
|
349
353
|
args: ["--settings", claudeLaunchSettingsPath(), ...args],
|
package/src/launch.test.js
CHANGED
|
@@ -73,7 +73,7 @@ async function main() {
|
|
|
73
73
|
apiKey: "pr-super-secret",
|
|
74
74
|
gatewayBaseUrl: "https://gateway.example/",
|
|
75
75
|
subscriptionAuth: true,
|
|
76
|
-
models: { haiku: "
|
|
76
|
+
models: { haiku: "Haiku 4.5 (PennyRouter)", sonnet: "Sonnet 5 (PennyRouter)", opus: "Opus 5 (PennyRouter)" },
|
|
77
77
|
});
|
|
78
78
|
await configurePennyHarness("codex", {
|
|
79
79
|
apiKey: "pr-super-secret",
|
|
@@ -100,7 +100,7 @@ async function main() {
|
|
|
100
100
|
assert.ok(!claude.args.includes("--plugin-dir"));
|
|
101
101
|
assert.equal(claude.env.ANTHROPIC_AUTH_TOKEN, "pr-super-secret");
|
|
102
102
|
assert.equal(claude.env.ANTHROPIC_BASE_URL, "https://gateway.example/");
|
|
103
|
-
assert.equal(claude.env.ANTHROPIC_DEFAULT_OPUS_MODEL, "
|
|
103
|
+
assert.equal(claude.env.ANTHROPIC_DEFAULT_OPUS_MODEL, "Opus 5 (PennyRouter)");
|
|
104
104
|
|
|
105
105
|
const codex = buildPennyInvocation("codex", ["resume", "--last"], config);
|
|
106
106
|
assert.deepEqual(codex.args, ["--profile", "pennyrouter", "resume", "--last"]);
|
package/src/session.js
CHANGED
|
@@ -97,7 +97,7 @@ export async function probeClaudeCodeGateway({ gatewayBaseUrl, apiKey }) {
|
|
|
97
97
|
"anthropic-beta": "compact-2026-01-12",
|
|
98
98
|
},
|
|
99
99
|
body: JSON.stringify({
|
|
100
|
-
model: "
|
|
100
|
+
model: "Sonnet 5 (PennyRouter)",
|
|
101
101
|
max_tokens: 1,
|
|
102
102
|
stream: false,
|
|
103
103
|
messages: [{ role: "user", content: "Reply OK." }],
|