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 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
- - Penny Opus: pins `anthropic/claude-opus-4-8`
101
- - Penny Sonnet: pins `anthropic/claude-sonnet-4-6`
102
- - Penny Haiku: pins `anthropic/claude-haiku-4-5-20251001`
103
- - Penny Fable: pins `anthropic/claude-fable-5`
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pennyrouter",
3
- "version": "0.2.4",
3
+ "version": "0.2.7",
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
@@ -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
- // ("Penny Opus/Sonnet/Haiku/Fable") that the gateway maps to that exact model — the user's pick
11
- // runs on the model they chose, no dynamic tier switching on the trunk (background/utility
12
- // calls still route cheap, which is where the savings come from). The separate Fable slot is
13
- // pinned to Fable; the DYNAMIC "Penny Custom" option lets PennyRouter cost-route. Legacy
14
- // "Penny Speed/Core/Power"
15
- // installs (dynamic bundles) are still recognized for uninstall (see isPennyRouterModelValue).
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: "Penny Haiku", // -> pinned anthropic/claude-haiku-4-5-20251001
18
- sonnet: "Penny Sonnet", // -> pinned anthropic/claude-sonnet-4-6
19
- opus: "Penny Opus", // -> pinned anthropic/claude-opus-4-8
20
- fable: "Penny Fable", // -> pinned anthropic/claude-fable-5
21
- custom: "Penny Custom", // -> dynamic (PennyRouter cost-routing)
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). Not written by a fresh install; matched
24
- // only so uninstall/detect still recognize and clean up an older install.
25
- const LEGACY_PENNY_LABELS = ["Penny Speed", "Penny Core", "Penny Power"];
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],
@@ -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: "Penny Haiku", sonnet: "Penny Sonnet", opus: "Penny Opus" },
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, "Penny Opus");
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: "Penny Sonnet",
100
+ model: "Sonnet 5 (PennyRouter)",
101
101
  max_tokens: 1,
102
102
  stream: false,
103
103
  messages: [{ role: "user", content: "Reply OK." }],