pennyrouter 0.1.18 → 0.1.20
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 +1 -0
- package/package.json +1 -1
- package/src/harnesses/claude-code.js +9 -6
- package/src/statusline.js +15 -9
package/README.md
CHANGED
|
@@ -61,6 +61,7 @@ model you chose. "Penny Custom" is the dynamic option: PennyRouter cost-routes e
|
|
|
61
61
|
- Penny Opus: pins `anthropic/claude-opus-4-8`
|
|
62
62
|
- Penny Sonnet: pins `anthropic/claude-sonnet-4-6`
|
|
63
63
|
- Penny Haiku: pins `anthropic/claude-haiku-4-5-20251001`
|
|
64
|
+
- Penny Fable: pins `anthropic/claude-fable-5`
|
|
64
65
|
- Penny Custom: `pennyrouter/auto` — dynamic routing on the bundle/profile saved in the user's account
|
|
65
66
|
|
|
66
67
|
## Claude Pro/Max Subscription Auth
|
package/package.json
CHANGED
|
@@ -7,15 +7,17 @@ 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") that the gateway maps to that exact model — the user's pick
|
|
10
|
+
// ("Penny Opus/Sonnet/Haiku/Fable") that the gateway maps to that exact model — the user's pick
|
|
11
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 Fable slot is
|
|
13
|
-
// DYNAMIC "Penny Custom" option
|
|
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"
|
|
14
15
|
// installs (dynamic bundles) are still recognized for uninstall (see isPennyRouterModelValue).
|
|
15
16
|
const CLAUDE_CODE_MODELS = {
|
|
16
17
|
haiku: "Penny Haiku", // -> pinned anthropic/claude-haiku-4-5-20251001
|
|
17
18
|
sonnet: "Penny Sonnet", // -> pinned anthropic/claude-sonnet-4-6
|
|
18
19
|
opus: "Penny Opus", // -> pinned anthropic/claude-opus-4-8
|
|
20
|
+
fable: "Penny Fable", // -> pinned anthropic/claude-fable-5
|
|
19
21
|
custom: "Penny Custom", // -> dynamic (PennyRouter cost-routing)
|
|
20
22
|
};
|
|
21
23
|
// Retired dynamic-bundle labels (pre-pinned-trunk). Not written by a fresh install; matched
|
|
@@ -57,7 +59,7 @@ export const claudeCodeHarness = {
|
|
|
57
59
|
current.env.ANTHROPIC_DEFAULT_OPUS_MODEL = CLAUDE_CODE_MODELS.opus;
|
|
58
60
|
current.env.ANTHROPIC_DEFAULT_SONNET_MODEL = CLAUDE_CODE_MODELS.sonnet;
|
|
59
61
|
current.env.ANTHROPIC_DEFAULT_HAIKU_MODEL = CLAUDE_CODE_MODELS.haiku;
|
|
60
|
-
current.env.ANTHROPIC_DEFAULT_FABLE_MODEL = CLAUDE_CODE_MODELS.
|
|
62
|
+
current.env.ANTHROPIC_DEFAULT_FABLE_MODEL = CLAUDE_CODE_MODELS.fable;
|
|
61
63
|
|
|
62
64
|
const statusline = await configureClaudeCodeStatusline(current);
|
|
63
65
|
|
|
@@ -102,7 +104,7 @@ export const claudeCodeHarness = {
|
|
|
102
104
|
current.env.ANTHROPIC_DEFAULT_OPUS_MODEL = CLAUDE_CODE_MODELS.opus;
|
|
103
105
|
current.env.ANTHROPIC_DEFAULT_SONNET_MODEL = CLAUDE_CODE_MODELS.sonnet;
|
|
104
106
|
current.env.ANTHROPIC_DEFAULT_HAIKU_MODEL = CLAUDE_CODE_MODELS.haiku;
|
|
105
|
-
current.env.ANTHROPIC_DEFAULT_FABLE_MODEL = CLAUDE_CODE_MODELS.
|
|
107
|
+
current.env.ANTHROPIC_DEFAULT_FABLE_MODEL = CLAUDE_CODE_MODELS.fable;
|
|
106
108
|
|
|
107
109
|
const statusline = await configureClaudeCodeStatusline(current);
|
|
108
110
|
|
|
@@ -143,7 +145,7 @@ export const claudeCodeHarness = {
|
|
|
143
145
|
current.env.ANTHROPIC_DEFAULT_OPUS_MODEL = CLAUDE_CODE_MODELS.opus;
|
|
144
146
|
current.env.ANTHROPIC_DEFAULT_SONNET_MODEL = CLAUDE_CODE_MODELS.sonnet;
|
|
145
147
|
current.env.ANTHROPIC_DEFAULT_HAIKU_MODEL = CLAUDE_CODE_MODELS.haiku;
|
|
146
|
-
current.env.ANTHROPIC_DEFAULT_FABLE_MODEL = CLAUDE_CODE_MODELS.
|
|
148
|
+
current.env.ANTHROPIC_DEFAULT_FABLE_MODEL = CLAUDE_CODE_MODELS.fable;
|
|
147
149
|
|
|
148
150
|
const statusline = await configureClaudeCodeStatusline(current);
|
|
149
151
|
|
|
@@ -266,6 +268,7 @@ function isPennyRouterModelValue(value) {
|
|
|
266
268
|
value === CLAUDE_CODE_MODELS.haiku ||
|
|
267
269
|
value === CLAUDE_CODE_MODELS.sonnet ||
|
|
268
270
|
value === CLAUDE_CODE_MODELS.opus ||
|
|
271
|
+
value === CLAUDE_CODE_MODELS.fable ||
|
|
269
272
|
value === CLAUDE_CODE_MODELS.custom ||
|
|
270
273
|
LEGACY_PENNY_LABELS.includes(value)
|
|
271
274
|
);
|
package/src/statusline.js
CHANGED
|
@@ -140,10 +140,7 @@ let cacheAge = Infinity;
|
|
|
140
140
|
try { cacheAge = Date.now() - statSync(cachePath).mtimeMs; } catch { /* no cache yet */ }
|
|
141
141
|
|
|
142
142
|
const { base, key } = claudeEnv();
|
|
143
|
-
|
|
144
|
-
// writes both together). Do NOT sniff the base URL for "pennyrouter": that substring check
|
|
145
|
-
// false-negatived every dev/localhost gateway (e.g. localhost:8400), rendering a hard
|
|
146
|
-
// "penny off" while the gateway was serving the session fine (observed 2026-07-18).
|
|
143
|
+
|
|
147
144
|
const pennyInstalled = Boolean(base) && Boolean(key);
|
|
148
145
|
if (sessionId && pennyInstalled && cacheAge > refreshMs) {
|
|
149
146
|
spawn(process.execPath, [process.argv[1], "--refresh", sessionId], {
|
|
@@ -156,22 +153,31 @@ function authSuffix(mode) {
|
|
|
156
153
|
return mode === "sub" || mode === "byok" ? " (" + mode + ")" : "";
|
|
157
154
|
}
|
|
158
155
|
|
|
156
|
+
// Teammate badges (@penny ask). Rendered from the STRUCTURED fields, not the gateway's
|
|
157
|
+
// prebuilt \`label\` — this line is composed here (and may be appended to a wrap_command's
|
|
158
|
+
// output), so the client owns the layout. 📨 = questions waiting on you; 📬 = an answer came
|
|
159
|
+
// back to you. Both absent on an older gateway, which renders nothing.
|
|
160
|
+
function askBadges(c) {
|
|
161
|
+
if (!c) return "";
|
|
162
|
+
let out = "";
|
|
163
|
+
if (c.asks_open > 0) out += " · 📨 " + c.asks_open;
|
|
164
|
+
if (c.answers_fresh > 0) out += " · 📬 " + c.answers_fresh;
|
|
165
|
+
return out;
|
|
166
|
+
}
|
|
167
|
+
|
|
159
168
|
let penny;
|
|
160
169
|
if (!pennyInstalled) {
|
|
161
170
|
penny = "🪙 penny off";
|
|
162
171
|
} else if (cache && cache.known && cache.bypass) {
|
|
163
172
|
penny = "🪙 penny off";
|
|
164
173
|
} else {
|
|
165
|
-
penny = "🪙 penny" + authSuffix(cache && cache.auth_mode);
|
|
174
|
+
penny = "🪙 penny" + authSuffix(cache && cache.auth_mode) + askBadges(cache);
|
|
166
175
|
}
|
|
167
176
|
|
|
168
|
-
// Wrap the underlying statusline (e.g. ccstatusline) and append the penny segment to
|
|
169
|
-
// its FIRST line only, preserving every subsequent line verbatim (the Context bar and
|
|
170
|
-
// any other rows). Taking only line [0] here is what dropped multi-line statuslines.
|
|
171
177
|
let lines;
|
|
172
178
|
if (config.wrap_command) {
|
|
173
179
|
const run = spawnSync(config.wrap_command, {
|
|
174
|
-
shell: true, input: raw, encoding: "utf8", timeout:
|
|
180
|
+
shell: true, input: raw, encoding: "utf8", timeout: 5000,
|
|
175
181
|
});
|
|
176
182
|
lines = (run.stdout || "").replace(/\\n$/, "").split("\\n");
|
|
177
183
|
if (lines.length === 1 && lines[0] === "") lines = [];
|