pennyrouter 0.3.38 → 0.3.40

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pennyrouter",
3
- "version": "0.3.38",
3
+ "version": "0.3.40",
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
@@ -2035,8 +2035,6 @@ async function uninstall(flags) {
2035
2035
 
2036
2036
  if (anthropicTokenNotice) {
2037
2037
  console.log("");
2038
- console.log("Note: Your stored Anthropic token remains on the server for other machines.");
2039
- console.log("To delete it from the server, run: pennyrouter auth anthropic --forget");
2040
2038
  }
2041
2039
 
2042
2040
  manifest.installations = kept;
package/src/launch.js CHANGED
@@ -320,6 +320,16 @@ export async function installPennyCompare() {
320
320
  await mkdir(dirname(target), { recursive: true });
321
321
  await cp(await findInstalledPackage(name), target, { recursive: true });
322
322
  }
323
+ if (process.platform === "darwin") {
324
+ // node-pty 1.1.0's macOS prebuild ships a non-executable spawn-helper, and the copy
325
+ // keeps that mode; without +x every PTY launch fails at posix_spawnp.
326
+ const helper = join(modules, "node-pty", "prebuilds", `darwin-${process.arch}`, "spawn-helper");
327
+ try {
328
+ await chmod(helper, (await stat(helper)).mode | 0o111);
329
+ } catch (error) {
330
+ if (error.code !== "ENOENT") throw error;
331
+ }
332
+ }
323
333
  if (process.platform === "win32") {
324
334
  const link = join(pennyBinDir(), "penny-compare.cmd");
325
335
  const marker = "@REM PennyRouter-managed compare launcher";
@@ -917,6 +927,9 @@ export async function writeClaudeLaunchSettings({
917
927
  // An empty value is treated as unset by Claude Code, and prevents a
918
928
  // stale API key in the shared settings from creating dual auth.
919
929
  ANTHROPIC_API_KEY: "",
930
+ // Claude Code turns tool search off behind a custom base URL unless told
931
+ // otherwise; Penny relays deferred tools and their tool_reference blocks.
932
+ ENABLE_TOOL_SEARCH: "true",
920
933
  ANTHROPIC_DEFAULT_HAIKU_MODEL: "",
921
934
  ANTHROPIC_DEFAULT_HAIKU_MODEL_NAME: "",
922
935
  ANTHROPIC_DEFAULT_HAIKU_MODEL_DESCRIPTION: "",
@@ -1413,6 +1426,7 @@ export function buildPennyInvocation(harness, args, config, models = null) {
1413
1426
  // Ask Claude Code to discover the gateway's model catalog at startup. The gateway
1414
1427
  // advertises Penny-facing ids in /v1/models; this is separate from inference routing.
1415
1428
  env.CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY = "1";
1429
+ env.ENABLE_TOOL_SEARCH = "true";
1416
1430
  delete env.ANTHROPIC_API_KEY;
1417
1431
  for (const slot of ["haiku", "sonnet", "opus", "fable"]) {
1418
1432
  const name = `ANTHROPIC_DEFAULT_${slot.toUpperCase()}_MODEL`;
@@ -139,6 +139,7 @@ async function main() {
139
139
  assert.equal(claudeLaunchSettings.env.ANTHROPIC_BASE_URL, "https://gateway.example/");
140
140
  assert.equal(claudeLaunchSettings.env.ANTHROPIC_AUTH_TOKEN, "pr-super-secret");
141
141
  assert.equal(claudeLaunchSettings.env.ANTHROPIC_API_KEY, "");
142
+ assert.equal(claudeLaunchSettings.env.ENABLE_TOOL_SEARCH, "true");
142
143
  assert.equal(claudeLaunchSettings.env.ANTHROPIC_DEFAULT_OPUS_MODEL, "");
143
144
  assert.equal(claudeLaunchSettings.env.ANTHROPIC_DEFAULT_OPUS_MODEL_NAME, "");
144
145
  await writeClaudeLaunchSettings({
@@ -264,6 +265,7 @@ async function main() {
264
265
  assert.equal(claude.env.ANTHROPIC_AUTH_TOKEN, "pr-super-secret");
265
266
  assert.equal(claude.env.ANTHROPIC_BASE_URL, "https://gateway.example/");
266
267
  assert.equal(claude.env.CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY, "1");
268
+ assert.equal(claude.env.ENABLE_TOOL_SEARCH, "true");
267
269
  // 1M-capable slots carry the `[1m]` suffix; Haiku has no 1M variant and stays bare.
268
270
  // Per-slot suffixes, not the global CLAUDE_CODE_MAX_CONTEXT_TOKENS override, which
269
271
  // would wrongly hand Haiku a 1M window too.
@@ -7,18 +7,27 @@ that uses 1-hour caching, which the gateway does by default.
7
7
 
8
8
  from __future__ import annotations
9
9
 
10
- # input, cache_read, write_5m, write_1h, output
11
- RATES = {
12
- "claude-opus-5": (5.00e-06, 5.00e-07, 6.25e-06, 1.00e-05, 2.50e-05),
13
- "claude-sonnet-5": (3.00e-06, 3.00e-07, 3.75e-06, 6.00e-06, 1.50e-05),
14
- "claude-haiku-4-5": (1.00e-06, 1.00e-07, 1.25e-06, 2.00e-06, 5.00e-06),
15
- # OpenAI does not offer a 1-hour cache tier; its write rate applies uniformly.
16
- "gpt-5-codex": (1.25e-06, 1.25e-07, 1.25e-06, 1.25e-06, 1.00e-05),
17
- "gpt-5.1-codex": (1.25e-06, 1.25e-07, 1.25e-06, 1.25e-06, 1.00e-05),
18
- # GPT-5.6 standard pricing. Keep Luna explicit so penny-session does not silently
19
- # price it as Claude Sonnet when no GovCloud profile was selected.
20
- "gpt-5.6-luna": (0.20e-06, 0.020e-06, 0.250e-06, 0.250e-06, 1.20e-06),
21
- }
10
+ import json
11
+ from pathlib import Path
12
+
13
+
14
+ def _load_standard_rates():
15
+ """Standard per-token rates from rates.json (scripts/generate_compare_rates.py), the same
16
+ file the web-demo meters read. Returns (flat rates, tiered rates) keyed by model id; a tier
17
+ entry is (inclusive threshold, <= threshold rates, > threshold rates)."""
18
+ document = json.loads((Path(__file__).with_name("rates.json")).read_text())
19
+ flat, tiered = {}, {}
20
+ for name, model in document["models"].items():
21
+ flat[name] = tuple(model["rates"])
22
+ if "above" in model:
23
+ above = model["above"]
24
+ tiered[name] = (above["threshold"], tuple(model["rates"]), tuple(above["rates"]))
25
+ return flat, tiered
26
+
27
+
28
+ # input, cache_read, write_5m, write_1h, output. OpenAI does not offer a 1-hour cache tier;
29
+ # its write rate applies uniformly.
30
+ RATES, STANDARD_TIERED_RATES = _load_standard_rates()
22
31
 
23
32
  # --native-proxy means the comparison is measuring the GovCloud route. Pricing remains the
24
33
  # same on both panes, but both use this schedule instead of the standard one.
@@ -29,9 +38,7 @@ GOVCLOUD_RATES = {
29
38
  "claude-haiku-4-5": (1.10e-06, 1.10e-07, 1.375e-06, 2.20e-06, 5.50e-06),
30
39
  }
31
40
 
32
- # GPT GovCloud pricing changes when an individual request's input context exceeds 272K.
33
- # Each entry is (inclusive threshold, <= threshold rates, > threshold rates), with rate tuples
34
- # in the same input/read/write-5m/write-1h/output order as RATES.
41
+ # The GovCloud schedule for the same tiered GPT models.
35
42
  GOVCLOUD_TIERED_RATES = {
36
43
  "gpt-5.6-luna": (
37
44
  272_000,
@@ -88,11 +95,11 @@ def _matching(name: str, choices) -> str | None:
88
95
 
89
96
  def rates_for(model: str, profile: str = "standard", context_tokens: int = 0):
90
97
  name = _normalize(model)
91
- if profile == "govcloud":
92
- tiered = _matching(name, GOVCLOUD_TIERED_RATES)
93
- if tiered:
94
- threshold, low, high = GOVCLOUD_TIERED_RATES[tiered]
95
- return high if context_tokens > threshold else low
98
+ tiers = GOVCLOUD_TIERED_RATES if profile == "govcloud" else STANDARD_TIERED_RATES
99
+ tiered = _matching(name, tiers)
100
+ if tiered:
101
+ threshold, low, high = tiers[tiered]
102
+ return high if context_tokens > threshold else low
96
103
  rates = GOVCLOUD_RATES if profile == "govcloud" else RATES
97
104
  match = _matching(name, rates)
98
105
  if match:
@@ -0,0 +1,1078 @@
1
+ {
2
+ "source": "bifrost_datasheet",
3
+ "generated_at": "2026-09-29T15:31:58+00:00",
4
+ "order": [
5
+ "input",
6
+ "cache_read",
7
+ "cache_write_5m",
8
+ "cache_write_1h",
9
+ "output"
10
+ ],
11
+ "unit": "usd_per_token",
12
+ "models": {
13
+ "claude-fable-5": {
14
+ "rates": [
15
+ 1e-05,
16
+ 1e-06,
17
+ 1.25e-05,
18
+ 2e-05,
19
+ 5e-05
20
+ ]
21
+ },
22
+ "claude-fable-5-1": {
23
+ "rates": [
24
+ 1e-05,
25
+ 2.5e-07,
26
+ 1.25e-05,
27
+ 2e-05,
28
+ 5e-05
29
+ ]
30
+ },
31
+ "claude-haiku-4-5": {
32
+ "rates": [
33
+ 1e-06,
34
+ 1e-07,
35
+ 1.25e-06,
36
+ 2e-06,
37
+ 5e-06
38
+ ]
39
+ },
40
+ "claude-haiku-4-5-20251001": {
41
+ "rates": [
42
+ 1e-06,
43
+ 1e-07,
44
+ 1.25e-06,
45
+ 2e-06,
46
+ 5e-06
47
+ ]
48
+ },
49
+ "claude-mythos-5": {
50
+ "rates": [
51
+ 1e-05,
52
+ 1e-06,
53
+ 1.25e-05,
54
+ 2e-05,
55
+ 5e-05
56
+ ]
57
+ },
58
+ "claude-mythos-5-1": {
59
+ "rates": [
60
+ 1e-05,
61
+ 2.5e-07,
62
+ 1.25e-05,
63
+ 2e-05,
64
+ 5e-05
65
+ ]
66
+ },
67
+ "claude-mythos-preview": {
68
+ "rates": [
69
+ 1e-05,
70
+ 1e-06,
71
+ 1.25e-05,
72
+ 2e-05,
73
+ 5e-05
74
+ ]
75
+ },
76
+ "claude-opus-4-5": {
77
+ "rates": [
78
+ 5e-06,
79
+ 5e-07,
80
+ 6.25e-06,
81
+ 1e-05,
82
+ 2.5e-05
83
+ ]
84
+ },
85
+ "claude-opus-4-5-20251101": {
86
+ "rates": [
87
+ 5e-06,
88
+ 5e-07,
89
+ 6.25e-06,
90
+ 1e-05,
91
+ 2.5e-05
92
+ ]
93
+ },
94
+ "claude-opus-4-6": {
95
+ "rates": [
96
+ 5e-06,
97
+ 5e-07,
98
+ 6.25e-06,
99
+ 1e-05,
100
+ 2.5e-05
101
+ ]
102
+ },
103
+ "claude-opus-4-6-20260205": {
104
+ "rates": [
105
+ 5e-06,
106
+ 5e-07,
107
+ 6.25e-06,
108
+ 1e-05,
109
+ 2.5e-05
110
+ ]
111
+ },
112
+ "claude-opus-4-7": {
113
+ "rates": [
114
+ 5e-06,
115
+ 5e-07,
116
+ 6.25e-06,
117
+ 1e-05,
118
+ 2.5e-05
119
+ ]
120
+ },
121
+ "claude-opus-4-7-20260416": {
122
+ "rates": [
123
+ 5e-06,
124
+ 5e-07,
125
+ 6.25e-06,
126
+ 1e-05,
127
+ 2.5e-05
128
+ ]
129
+ },
130
+ "claude-opus-4-8": {
131
+ "rates": [
132
+ 5e-06,
133
+ 5e-07,
134
+ 6.25e-06,
135
+ 1e-05,
136
+ 2.5e-05
137
+ ]
138
+ },
139
+ "claude-opus-5": {
140
+ "rates": [
141
+ 5e-06,
142
+ 5e-07,
143
+ 6.25e-06,
144
+ 1e-05,
145
+ 2.5e-05
146
+ ]
147
+ },
148
+ "claude-opus-5-5": {
149
+ "rates": [
150
+ 4e-06,
151
+ 2e-07,
152
+ 5e-06,
153
+ 8e-06,
154
+ 2e-05
155
+ ]
156
+ },
157
+ "claude-sonnet-4-5": {
158
+ "rates": [
159
+ 3e-06,
160
+ 3e-07,
161
+ 3.75e-06,
162
+ 6e-06,
163
+ 1.5e-05
164
+ ],
165
+ "above": {
166
+ "threshold": 200000,
167
+ "rates": [
168
+ 6e-06,
169
+ 6e-07,
170
+ 7.5e-06,
171
+ 1.2e-05,
172
+ 2.25e-05
173
+ ]
174
+ }
175
+ },
176
+ "claude-sonnet-4-5-20250929": {
177
+ "rates": [
178
+ 3e-06,
179
+ 3e-07,
180
+ 3.75e-06,
181
+ 6e-06,
182
+ 1.5e-05
183
+ ],
184
+ "above": {
185
+ "threshold": 200000,
186
+ "rates": [
187
+ 6e-06,
188
+ 6e-07,
189
+ 7.5e-06,
190
+ 1.2e-05,
191
+ 2.25e-05
192
+ ]
193
+ }
194
+ },
195
+ "claude-sonnet-4-6": {
196
+ "rates": [
197
+ 3e-06,
198
+ 3e-07,
199
+ 3.75e-06,
200
+ 6e-06,
201
+ 1.5e-05
202
+ ]
203
+ },
204
+ "claude-sonnet-5": {
205
+ "rates": [
206
+ 2e-06,
207
+ 2e-07,
208
+ 2.5e-06,
209
+ 4e-06,
210
+ 1e-05
211
+ ]
212
+ },
213
+ "claude-sonnet-5-5": {
214
+ "rates": [
215
+ 2e-06,
216
+ 2e-07,
217
+ 2.5e-06,
218
+ 4e-06,
219
+ 1e-05
220
+ ]
221
+ },
222
+ "gpt-3.5-turbo": {
223
+ "rates": [
224
+ 5e-07,
225
+ 5e-07,
226
+ 5e-07,
227
+ 5e-07,
228
+ 1.5e-06
229
+ ]
230
+ },
231
+ "gpt-3.5-turbo-0125": {
232
+ "rates": [
233
+ 5e-07,
234
+ 5e-07,
235
+ 5e-07,
236
+ 5e-07,
237
+ 1.5e-06
238
+ ]
239
+ },
240
+ "gpt-3.5-turbo-1106": {
241
+ "rates": [
242
+ 1e-06,
243
+ 1e-06,
244
+ 1e-06,
245
+ 1e-06,
246
+ 2e-06
247
+ ]
248
+ },
249
+ "gpt-3.5-turbo-16k": {
250
+ "rates": [
251
+ 3e-06,
252
+ 3e-06,
253
+ 3e-06,
254
+ 3e-06,
255
+ 4e-06
256
+ ]
257
+ },
258
+ "gpt-3.5-turbo-instruct": {
259
+ "rates": [
260
+ 1.5e-06,
261
+ 1.5e-06,
262
+ 1.5e-06,
263
+ 1.5e-06,
264
+ 2e-06
265
+ ]
266
+ },
267
+ "gpt-3.5-turbo-instruct-0914": {
268
+ "rates": [
269
+ 1.5e-06,
270
+ 1.5e-06,
271
+ 1.5e-06,
272
+ 1.5e-06,
273
+ 2e-06
274
+ ]
275
+ },
276
+ "gpt-4": {
277
+ "rates": [
278
+ 3e-05,
279
+ 3e-05,
280
+ 3e-05,
281
+ 3e-05,
282
+ 6e-05
283
+ ]
284
+ },
285
+ "gpt-4-0613": {
286
+ "rates": [
287
+ 3e-05,
288
+ 3e-05,
289
+ 3e-05,
290
+ 3e-05,
291
+ 6e-05
292
+ ]
293
+ },
294
+ "gpt-4-1106-preview": {
295
+ "rates": [
296
+ 1e-05,
297
+ 1e-05,
298
+ 1e-05,
299
+ 1e-05,
300
+ 3e-05
301
+ ]
302
+ },
303
+ "gpt-4-turbo": {
304
+ "rates": [
305
+ 1e-05,
306
+ 1e-05,
307
+ 1e-05,
308
+ 1e-05,
309
+ 3e-05
310
+ ]
311
+ },
312
+ "gpt-4-turbo-2024-04-09": {
313
+ "rates": [
314
+ 1e-05,
315
+ 1e-05,
316
+ 1e-05,
317
+ 1e-05,
318
+ 3e-05
319
+ ]
320
+ },
321
+ "gpt-4.1": {
322
+ "rates": [
323
+ 2e-06,
324
+ 5e-07,
325
+ 2e-06,
326
+ 2e-06,
327
+ 8e-06
328
+ ]
329
+ },
330
+ "gpt-4.1-2025-04-14": {
331
+ "rates": [
332
+ 2e-06,
333
+ 5e-07,
334
+ 2e-06,
335
+ 2e-06,
336
+ 8e-06
337
+ ]
338
+ },
339
+ "gpt-4.1-mini": {
340
+ "rates": [
341
+ 4e-07,
342
+ 1e-07,
343
+ 4e-07,
344
+ 4e-07,
345
+ 1.6e-06
346
+ ]
347
+ },
348
+ "gpt-4.1-mini-2025-04-14": {
349
+ "rates": [
350
+ 4e-07,
351
+ 1e-07,
352
+ 4e-07,
353
+ 4e-07,
354
+ 1.6e-06
355
+ ]
356
+ },
357
+ "gpt-4.1-nano": {
358
+ "rates": [
359
+ 1e-07,
360
+ 2.5e-08,
361
+ 1e-07,
362
+ 1e-07,
363
+ 4e-07
364
+ ]
365
+ },
366
+ "gpt-4.1-nano-2025-04-14": {
367
+ "rates": [
368
+ 1e-07,
369
+ 2.5e-08,
370
+ 1e-07,
371
+ 1e-07,
372
+ 4e-07
373
+ ]
374
+ },
375
+ "gpt-4o": {
376
+ "rates": [
377
+ 2.5e-06,
378
+ 1.25e-06,
379
+ 2.5e-06,
380
+ 2.5e-06,
381
+ 1e-05
382
+ ]
383
+ },
384
+ "gpt-4o-2024-05-13": {
385
+ "rates": [
386
+ 5e-06,
387
+ 5e-06,
388
+ 5e-06,
389
+ 5e-06,
390
+ 1.5e-05
391
+ ]
392
+ },
393
+ "gpt-4o-2024-08-06": {
394
+ "rates": [
395
+ 2.5e-06,
396
+ 1.25e-06,
397
+ 2.5e-06,
398
+ 2.5e-06,
399
+ 1e-05
400
+ ]
401
+ },
402
+ "gpt-4o-2024-11-20": {
403
+ "rates": [
404
+ 2.5e-06,
405
+ 1.25e-06,
406
+ 2.5e-06,
407
+ 2.5e-06,
408
+ 1e-05
409
+ ]
410
+ },
411
+ "gpt-4o-mini": {
412
+ "rates": [
413
+ 1.5e-07,
414
+ 7.5e-08,
415
+ 1.5e-07,
416
+ 1.5e-07,
417
+ 6e-07
418
+ ]
419
+ },
420
+ "gpt-4o-mini-2024-07-18": {
421
+ "rates": [
422
+ 1.5e-07,
423
+ 7.5e-08,
424
+ 1.5e-07,
425
+ 1.5e-07,
426
+ 6e-07
427
+ ]
428
+ },
429
+ "gpt-5": {
430
+ "rates": [
431
+ 1.25e-06,
432
+ 1.25e-07,
433
+ 1.25e-06,
434
+ 1.25e-06,
435
+ 1e-05
436
+ ]
437
+ },
438
+ "gpt-5-2025-08-07": {
439
+ "rates": [
440
+ 1.25e-06,
441
+ 1.25e-07,
442
+ 1.25e-06,
443
+ 1.25e-06,
444
+ 1e-05
445
+ ]
446
+ },
447
+ "gpt-5-chat": {
448
+ "rates": [
449
+ 1.25e-06,
450
+ 1.25e-07,
451
+ 1.25e-06,
452
+ 1.25e-06,
453
+ 1e-05
454
+ ]
455
+ },
456
+ "gpt-5-chat-latest": {
457
+ "rates": [
458
+ 1.25e-06,
459
+ 1.25e-07,
460
+ 1.25e-06,
461
+ 1.25e-06,
462
+ 1e-05
463
+ ]
464
+ },
465
+ "gpt-5-codex": {
466
+ "rates": [
467
+ 1.25e-06,
468
+ 1.25e-07,
469
+ 1.25e-06,
470
+ 1.25e-06,
471
+ 1e-05
472
+ ]
473
+ },
474
+ "gpt-5-mini": {
475
+ "rates": [
476
+ 2.5e-07,
477
+ 2.5e-08,
478
+ 2.5e-07,
479
+ 2.5e-07,
480
+ 2e-06
481
+ ]
482
+ },
483
+ "gpt-5-mini-2025-08-07": {
484
+ "rates": [
485
+ 2.5e-07,
486
+ 2.5e-08,
487
+ 2.5e-07,
488
+ 2.5e-07,
489
+ 2e-06
490
+ ]
491
+ },
492
+ "gpt-5-nano": {
493
+ "rates": [
494
+ 5e-08,
495
+ 5e-09,
496
+ 5e-08,
497
+ 5e-08,
498
+ 4e-07
499
+ ]
500
+ },
501
+ "gpt-5-nano-2025-08-07": {
502
+ "rates": [
503
+ 5e-08,
504
+ 5e-09,
505
+ 5e-08,
506
+ 5e-08,
507
+ 4e-07
508
+ ]
509
+ },
510
+ "gpt-5-pro": {
511
+ "rates": [
512
+ 1.5e-05,
513
+ 1.5e-05,
514
+ 1.5e-05,
515
+ 1.5e-05,
516
+ 0.00012
517
+ ]
518
+ },
519
+ "gpt-5-pro-2025-10-06": {
520
+ "rates": [
521
+ 1.5e-05,
522
+ 1.5e-05,
523
+ 1.5e-05,
524
+ 1.5e-05,
525
+ 0.00012
526
+ ]
527
+ },
528
+ "gpt-5-search-api": {
529
+ "rates": [
530
+ 1.25e-06,
531
+ 1.25e-07,
532
+ 1.25e-06,
533
+ 1.25e-06,
534
+ 1e-05
535
+ ]
536
+ },
537
+ "gpt-5-search-api-2025-10-14": {
538
+ "rates": [
539
+ 1.25e-06,
540
+ 1.25e-07,
541
+ 1.25e-06,
542
+ 1.25e-06,
543
+ 1e-05
544
+ ]
545
+ },
546
+ "gpt-5.1": {
547
+ "rates": [
548
+ 1.25e-06,
549
+ 1.25e-07,
550
+ 1.25e-06,
551
+ 1.25e-06,
552
+ 1e-05
553
+ ]
554
+ },
555
+ "gpt-5.1-2025-11-13": {
556
+ "rates": [
557
+ 1.25e-06,
558
+ 1.25e-07,
559
+ 1.25e-06,
560
+ 1.25e-06,
561
+ 1e-05
562
+ ]
563
+ },
564
+ "gpt-5.1-chat-latest": {
565
+ "rates": [
566
+ 1.25e-06,
567
+ 1.25e-07,
568
+ 1.25e-06,
569
+ 1.25e-06,
570
+ 1e-05
571
+ ]
572
+ },
573
+ "gpt-5.1-codex": {
574
+ "rates": [
575
+ 1.25e-06,
576
+ 1.25e-07,
577
+ 1.25e-06,
578
+ 1.25e-06,
579
+ 1e-05
580
+ ]
581
+ },
582
+ "gpt-5.1-codex-max": {
583
+ "rates": [
584
+ 1.25e-06,
585
+ 1.25e-07,
586
+ 1.25e-06,
587
+ 1.25e-06,
588
+ 1e-05
589
+ ]
590
+ },
591
+ "gpt-5.1-codex-mini": {
592
+ "rates": [
593
+ 2.5e-07,
594
+ 2.5e-08,
595
+ 2.5e-07,
596
+ 2.5e-07,
597
+ 2e-06
598
+ ]
599
+ },
600
+ "gpt-5.2": {
601
+ "rates": [
602
+ 1.75e-06,
603
+ 1.75e-07,
604
+ 1.75e-06,
605
+ 1.75e-06,
606
+ 1.4e-05
607
+ ]
608
+ },
609
+ "gpt-5.2-2025-12-11": {
610
+ "rates": [
611
+ 1.75e-06,
612
+ 1.75e-07,
613
+ 1.75e-06,
614
+ 1.75e-06,
615
+ 1.4e-05
616
+ ]
617
+ },
618
+ "gpt-5.2-chat-latest": {
619
+ "rates": [
620
+ 1.75e-06,
621
+ 1.75e-07,
622
+ 1.75e-06,
623
+ 1.75e-06,
624
+ 1.4e-05
625
+ ]
626
+ },
627
+ "gpt-5.2-codex": {
628
+ "rates": [
629
+ 1.75e-06,
630
+ 1.75e-07,
631
+ 1.75e-06,
632
+ 1.75e-06,
633
+ 1.4e-05
634
+ ]
635
+ },
636
+ "gpt-5.2-pro": {
637
+ "rates": [
638
+ 2.1e-05,
639
+ 2.1e-05,
640
+ 2.1e-05,
641
+ 2.1e-05,
642
+ 0.000168
643
+ ]
644
+ },
645
+ "gpt-5.2-pro-2025-12-11": {
646
+ "rates": [
647
+ 2.1e-05,
648
+ 2.1e-05,
649
+ 2.1e-05,
650
+ 2.1e-05,
651
+ 0.000168
652
+ ]
653
+ },
654
+ "gpt-5.3-chat-latest": {
655
+ "rates": [
656
+ 1.75e-06,
657
+ 1.75e-07,
658
+ 1.75e-06,
659
+ 1.75e-06,
660
+ 1.4e-05
661
+ ]
662
+ },
663
+ "gpt-5.3-codex": {
664
+ "rates": [
665
+ 1.75e-06,
666
+ 1.75e-07,
667
+ 1.75e-06,
668
+ 1.75e-06,
669
+ 1.4e-05
670
+ ]
671
+ },
672
+ "gpt-5.3-codex-spark": {
673
+ "rates": [
674
+ 1.75e-06,
675
+ 1.75e-07,
676
+ 1.75e-06,
677
+ 1.75e-06,
678
+ 1.4e-05
679
+ ]
680
+ },
681
+ "gpt-5.4": {
682
+ "rates": [
683
+ 2.5e-06,
684
+ 2.5e-07,
685
+ 2.5e-06,
686
+ 2.5e-06,
687
+ 1.5e-05
688
+ ],
689
+ "above": {
690
+ "threshold": 272000,
691
+ "rates": [
692
+ 5e-06,
693
+ 5e-07,
694
+ 5e-06,
695
+ 5e-06,
696
+ 2.25e-05
697
+ ]
698
+ }
699
+ },
700
+ "gpt-5.4-2026-03-05": {
701
+ "rates": [
702
+ 2.5e-06,
703
+ 2.5e-07,
704
+ 2.5e-06,
705
+ 2.5e-06,
706
+ 1.5e-05
707
+ ],
708
+ "above": {
709
+ "threshold": 272000,
710
+ "rates": [
711
+ 5e-06,
712
+ 5e-07,
713
+ 5e-06,
714
+ 5e-06,
715
+ 2.25e-05
716
+ ]
717
+ }
718
+ },
719
+ "gpt-5.4-mini": {
720
+ "rates": [
721
+ 7.5e-07,
722
+ 7.5e-08,
723
+ 7.5e-07,
724
+ 7.5e-07,
725
+ 4.5e-06
726
+ ]
727
+ },
728
+ "gpt-5.4-mini-2026-03-17": {
729
+ "rates": [
730
+ 7.5e-07,
731
+ 7.5e-08,
732
+ 7.5e-07,
733
+ 7.5e-07,
734
+ 4.5e-06
735
+ ]
736
+ },
737
+ "gpt-5.4-nano": {
738
+ "rates": [
739
+ 2e-07,
740
+ 2e-08,
741
+ 2e-07,
742
+ 2e-07,
743
+ 1.25e-06
744
+ ]
745
+ },
746
+ "gpt-5.4-nano-2026-03-17": {
747
+ "rates": [
748
+ 2e-07,
749
+ 2e-08,
750
+ 2e-07,
751
+ 2e-07,
752
+ 1.25e-06
753
+ ]
754
+ },
755
+ "gpt-5.4-pro": {
756
+ "rates": [
757
+ 3e-05,
758
+ 3e-06,
759
+ 3e-05,
760
+ 3e-05,
761
+ 0.00018
762
+ ],
763
+ "above": {
764
+ "threshold": 272000,
765
+ "rates": [
766
+ 6e-05,
767
+ 6e-06,
768
+ 6e-05,
769
+ 6e-05,
770
+ 0.00027
771
+ ]
772
+ }
773
+ },
774
+ "gpt-5.4-pro-2026-03-05": {
775
+ "rates": [
776
+ 3e-05,
777
+ 3e-06,
778
+ 3e-05,
779
+ 3e-05,
780
+ 0.00018
781
+ ],
782
+ "above": {
783
+ "threshold": 272000,
784
+ "rates": [
785
+ 6e-05,
786
+ 6e-06,
787
+ 6e-05,
788
+ 6e-05,
789
+ 0.00027
790
+ ]
791
+ }
792
+ },
793
+ "gpt-5.5": {
794
+ "rates": [
795
+ 5e-06,
796
+ 5e-07,
797
+ 5e-06,
798
+ 5e-06,
799
+ 3e-05
800
+ ],
801
+ "above": {
802
+ "threshold": 272000,
803
+ "rates": [
804
+ 1e-05,
805
+ 1e-06,
806
+ 1e-05,
807
+ 1e-05,
808
+ 4.5e-05
809
+ ]
810
+ }
811
+ },
812
+ "gpt-5.5-2026-04-23": {
813
+ "rates": [
814
+ 5e-06,
815
+ 5e-07,
816
+ 5e-06,
817
+ 5e-06,
818
+ 3e-05
819
+ ],
820
+ "above": {
821
+ "threshold": 272000,
822
+ "rates": [
823
+ 1e-05,
824
+ 1e-06,
825
+ 1e-05,
826
+ 1e-05,
827
+ 4.5e-05
828
+ ]
829
+ }
830
+ },
831
+ "gpt-5.5-cyber": {
832
+ "rates": [
833
+ 1.25e-05,
834
+ 1.25e-06,
835
+ 1.25e-05,
836
+ 1.25e-05,
837
+ 7.5e-05
838
+ ]
839
+ },
840
+ "gpt-5.5-pro": {
841
+ "rates": [
842
+ 3e-05,
843
+ 3e-06,
844
+ 3e-05,
845
+ 3e-05,
846
+ 0.00018
847
+ ],
848
+ "above": {
849
+ "threshold": 272000,
850
+ "rates": [
851
+ 6e-05,
852
+ 6e-06,
853
+ 6e-05,
854
+ 6e-05,
855
+ 0.00027
856
+ ]
857
+ }
858
+ },
859
+ "gpt-5.5-pro-2026-04-23": {
860
+ "rates": [
861
+ 3e-05,
862
+ 3e-06,
863
+ 3e-05,
864
+ 3e-05,
865
+ 0.00018
866
+ ],
867
+ "above": {
868
+ "threshold": 272000,
869
+ "rates": [
870
+ 6e-05,
871
+ 6e-06,
872
+ 6e-05,
873
+ 6e-05,
874
+ 0.00027
875
+ ]
876
+ }
877
+ },
878
+ "gpt-5.6": {
879
+ "rates": [
880
+ 4e-06,
881
+ 4e-07,
882
+ 5e-06,
883
+ 5e-06,
884
+ 2e-05
885
+ ],
886
+ "above": {
887
+ "threshold": 272000,
888
+ "rates": [
889
+ 8e-06,
890
+ 8e-07,
891
+ 1e-05,
892
+ 1e-05,
893
+ 3e-05
894
+ ]
895
+ }
896
+ },
897
+ "gpt-5.6-cyber": {
898
+ "rates": [
899
+ 1.25e-05,
900
+ 1.25e-06,
901
+ 1.5625e-05,
902
+ 1.5625e-05,
903
+ 7.5e-05
904
+ ],
905
+ "above": {
906
+ "threshold": 272000,
907
+ "rates": [
908
+ 2.5e-05,
909
+ 2.5e-06,
910
+ 3.125e-05,
911
+ 3.125e-05,
912
+ 0.0001125
913
+ ]
914
+ }
915
+ },
916
+ "gpt-5.6-luna": {
917
+ "rates": [
918
+ 2e-07,
919
+ 2e-08,
920
+ 2.5e-07,
921
+ 2.5e-07,
922
+ 1.2e-06
923
+ ],
924
+ "above": {
925
+ "threshold": 272000,
926
+ "rates": [
927
+ 4e-07,
928
+ 4e-08,
929
+ 5e-07,
930
+ 5e-07,
931
+ 1.8e-06
932
+ ]
933
+ }
934
+ },
935
+ "gpt-5.6-sol": {
936
+ "rates": [
937
+ 4e-06,
938
+ 4e-07,
939
+ 5e-06,
940
+ 5e-06,
941
+ 2e-05
942
+ ],
943
+ "above": {
944
+ "threshold": 272000,
945
+ "rates": [
946
+ 8e-06,
947
+ 8e-07,
948
+ 1e-05,
949
+ 1e-05,
950
+ 3e-05
951
+ ]
952
+ }
953
+ },
954
+ "gpt-5.6-terra": {
955
+ "rates": [
956
+ 2e-06,
957
+ 2e-07,
958
+ 2.5e-06,
959
+ 2.5e-06,
960
+ 1.2e-05
961
+ ],
962
+ "above": {
963
+ "threshold": 272000,
964
+ "rates": [
965
+ 4e-06,
966
+ 4e-07,
967
+ 5e-06,
968
+ 5e-06,
969
+ 1.8e-05
970
+ ]
971
+ }
972
+ },
973
+ "gpt-6-astra": {
974
+ "rates": [
975
+ 1e-05,
976
+ 1e-06,
977
+ 1.25e-05,
978
+ 1.25e-05,
979
+ 5e-05
980
+ ],
981
+ "above": {
982
+ "threshold": 272000,
983
+ "rates": [
984
+ 2e-05,
985
+ 2e-06,
986
+ 2.5e-05,
987
+ 2.5e-05,
988
+ 7.5e-05
989
+ ]
990
+ }
991
+ },
992
+ "gpt-6-luna": {
993
+ "rates": [
994
+ 1e-07,
995
+ 1e-08,
996
+ 1.25e-07,
997
+ 1.25e-07,
998
+ 5e-07
999
+ ],
1000
+ "above": {
1001
+ "threshold": 272000,
1002
+ "rates": [
1003
+ 2e-07,
1004
+ 2e-08,
1005
+ 2.5e-07,
1006
+ 2.5e-07,
1007
+ 7.5e-07
1008
+ ]
1009
+ }
1010
+ },
1011
+ "gpt-6-sol": {
1012
+ "rates": [
1013
+ 2e-06,
1014
+ 2e-07,
1015
+ 2.5e-06,
1016
+ 2.5e-06,
1017
+ 1e-05
1018
+ ],
1019
+ "above": {
1020
+ "threshold": 272000,
1021
+ "rates": [
1022
+ 4e-06,
1023
+ 4e-07,
1024
+ 5e-06,
1025
+ 5e-06,
1026
+ 1.5e-05
1027
+ ]
1028
+ }
1029
+ },
1030
+ "gpt-daybreak-blue-latest": {
1031
+ "rates": [
1032
+ 4e-06,
1033
+ 4e-07,
1034
+ 5e-06,
1035
+ 5e-06,
1036
+ 2e-05
1037
+ ],
1038
+ "above": {
1039
+ "threshold": 272000,
1040
+ "rates": [
1041
+ 8e-06,
1042
+ 8e-07,
1043
+ 1e-05,
1044
+ 1e-05,
1045
+ 3e-05
1046
+ ]
1047
+ }
1048
+ },
1049
+ "gpt-daybreak-red-latest": {
1050
+ "rates": [
1051
+ 1.25e-05,
1052
+ 1.25e-06,
1053
+ 1.5625e-05,
1054
+ 1.5625e-05,
1055
+ 7.5e-05
1056
+ ],
1057
+ "above": {
1058
+ "threshold": 272000,
1059
+ "rates": [
1060
+ 2.5e-05,
1061
+ 2.5e-06,
1062
+ 3.125e-05,
1063
+ 3.125e-05,
1064
+ 0.0001125
1065
+ ]
1066
+ }
1067
+ },
1068
+ "gpt-rosalind-research": {
1069
+ "rates": [
1070
+ 5e-06,
1071
+ 5e-07,
1072
+ 5e-06,
1073
+ 5e-06,
1074
+ 2.5e-05
1075
+ ]
1076
+ }
1077
+ }
1078
+ }
@@ -1,18 +1,44 @@
1
+ import { readFileSync } from 'node:fs';
1
2
  import { mkdir, open, readdir, stat, writeFile } from 'node:fs/promises';
2
3
  import { homedir } from 'node:os';
3
4
  import { join } from 'node:path';
4
5
 
5
- // Mirrors the standard Claude rates in ../pricing.py (USD per token).
6
- const rates = {
7
- 'claude-opus-5': [5e-6, .5e-6, 6.25e-6, 10e-6, 25e-6],
8
- 'claude-sonnet-5': [3e-6, .3e-6, 3.75e-6, 6e-6, 15e-6],
9
- 'claude-haiku-4-5': [1e-6, .1e-6, 1.25e-6, 2e-6, 5e-6],
10
- };
6
+ // Standard per-token rates from ../rates.json (scripts/generate_compare_rates.py), the same
7
+ // file pricing.py reads. Each rate list is [input, cache read, cache write 5m, cache write 1h,
8
+ // output]; a model may carry an `above` list that applies once one request's input context
9
+ // exceeds its threshold.
10
+ const { models } = JSON.parse(readFileSync(new URL('../rates.json', import.meta.url), 'utf8'));
11
11
 
12
- function rateFor(model) {
13
- const normalized = String(model || '').replace(/^.*\//, '').replace('[1m]', '');
14
- const key = Object.keys(rates).find((name) => normalized.startsWith(name));
15
- return rates[key] || rates['claude-sonnet-5'];
12
+ function longestPrefix(name, table) {
13
+ const keys = Object.keys(table).filter((key) => name.startsWith(key));
14
+ return keys.sort((a, b) => b.length - a.length)[0];
15
+ }
16
+
17
+ function rateFor(model, contextTokens = 0) {
18
+ const normalized = String(model || '').replace(/^.*\//, '').replace('[1m]', '').trim();
19
+ const entry = models[longestPrefix(normalized, models)] || models['claude-sonnet-5'];
20
+ return entry.above && contextTokens > entry.above.threshold ? entry.above.rates : entry.rates;
21
+ }
22
+
23
+ // Codex's session_meta line embeds the full base instructions (~20 KB), so a fixed small
24
+ // read truncates it mid-JSON. Read until the first newline, bounded.
25
+ async function readFirstLine(path, limit = 1024 * 1024) {
26
+ const handle = await open(path, 'r');
27
+ try {
28
+ const chunks = [];
29
+ const buffer = Buffer.alloc(16 * 1024);
30
+ let position = 0;
31
+ while (position < limit) {
32
+ const { bytesRead } = await handle.read(buffer, 0, buffer.length, position);
33
+ if (!bytesRead) break;
34
+ const chunk = buffer.subarray(0, bytesRead);
35
+ const newline = chunk.indexOf(10);
36
+ if (newline >= 0) { chunks.push(Buffer.from(chunk.subarray(0, newline))); break; }
37
+ chunks.push(Buffer.from(chunk));
38
+ position += bytesRead;
39
+ }
40
+ return Buffer.concat(chunks).toString('utf8');
41
+ } finally { await handle.close(); }
16
42
  }
17
43
 
18
44
  function emptyTotals() {
@@ -56,17 +82,12 @@ export class ClaudeMeter {
56
82
  for (const entry of entries) {
57
83
  if (!entry.endsWith('.jsonl')) continue;
58
84
  const path = join(this.root, project.name, entry);
59
- let handle;
60
85
  try {
61
- handle = await open(path, 'r');
62
- const buffer = Buffer.alloc(4096);
63
- const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0);
64
- const first = buffer.subarray(0, bytesRead).toString('utf8').split('\n', 1)[0];
65
- if (JSON.parse(first).sessionId === this.sessionId) {
86
+ if (JSON.parse(await readFirstLine(path)).sessionId === this.sessionId) {
66
87
  this.transcriptPath = path;
67
88
  return path;
68
89
  }
69
- } catch { /* next */ } finally { await handle?.close(); }
90
+ } catch { /* next */ }
70
91
  }
71
92
  }
72
93
  return null;
@@ -120,7 +141,7 @@ export class ClaudeMeter {
120
141
  const input = Number(usage.input_tokens) || 0;
121
142
  const output = Number(usage.output_tokens) || 0;
122
143
  const model = message.model || this.model;
123
- const prices = rateFor(model);
144
+ const prices = rateFor(model, read + write + input);
124
145
  const cost = input * prices[0] + read * prices[1] + write5 * prices[2] +
125
146
  write1 * prices[3] + output * prices[4];
126
147
  const call = { timestamp: obj.timestamp || null, recorded_at: Date.now() / 1000,
@@ -177,11 +198,7 @@ export class CodexMeter extends ClaudeMeter {
177
198
  if (entry.isDirectory()) found.push(...await visit(path));
178
199
  else if (entry.name.endsWith('.jsonl')) {
179
200
  try {
180
- const handle = await open(path, 'r');
181
- const buffer = Buffer.alloc(4096);
182
- const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0);
183
- await handle.close();
184
- const meta = JSON.parse(buffer.subarray(0, bytesRead).toString('utf8').split('\n', 1)[0]).payload;
201
+ const meta = JSON.parse(await readFirstLine(path)).payload;
185
202
  const info = await stat(path);
186
203
  const expectedProvider = this.role === 'NATIVE' ? 'openai' : 'pennyrouter';
187
204
  if (meta?.cwd && meta.cwd.toLowerCase() === this.cwd.toLowerCase() &&
@@ -228,15 +245,16 @@ export class CodexMeter extends ClaudeMeter {
228
245
  this.previous = fingerprint;
229
246
  const [input, cached, write, output, reasoning] = delta;
230
247
  const fresh = Math.max(0, input - cached - write);
231
- const prices = [1.25e-6, .125e-6, 1.25e-6, 1.25e-6, 10e-6];
232
- const cost = fresh * prices[0] + cached * prices[1] + write * prices[2] +
233
- (output - reasoning) * prices[4] + reasoning * prices[4];
248
+ const context = Number(payload.info.last_token_usage?.input_tokens) || input;
249
+ const prices = rateFor(this.model, context);
250
+ const cost = fresh * prices[0] + cached * prices[1] + write * prices[2] + output * prices[4];
234
251
  const call = { timestamp: obj.timestamp || null, recorded_at: Date.now() / 1000,
235
252
  model: this.model, cache_read: cached, cache_write: write, write_5m: write,
236
253
  write_1h: 0, fresh_input: fresh, output, reasoning_output: reasoning,
237
- total: input + output, cost, context_tokens: input, pricing_profile: 'standard',
238
- rates_per_million: { input: 1.25, cache_read: .125, cache_write_5m: 1.25,
239
- cache_write_1h: 1.25, output: 10 } };
254
+ total: input + output, cost, context_tokens: context, pricing_profile: 'standard',
255
+ rates_per_million: Object.fromEntries(
256
+ ['input', 'cache_read', 'cache_write_5m', 'cache_write_1h', 'output']
257
+ .map((name, index) => [name, prices[index] * 1_000_000])) };
240
258
  this.calls.push(call);
241
259
  this.totals.cache_read += cached; this.totals.cache_write += write;
242
260
  this.totals.write_5m += write; this.totals.fresh_input += fresh;