@nijaru/pi-peak-hours 0.0.7 → 0.0.9

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
@@ -8,7 +8,7 @@ The default covers DeepSeek direct only: pi records it at the published **peak**
8
8
 
9
9
  With a compatible [pi-provider-compaction](https://github.com/nijaru/pi-provider-compaction) companion loaded, this extension scales existing `usage.cost` before request completion reaches stream consumers or cache observers. It covers observed foreground requests, compaction, branch summaries, and cache warming for scheduled providers. Without the companion, it still corrects finalized foreground assistant messages through `message_end`; auxiliary usage stays unchanged. Both paths preserve token counts and existing cost corrections such as `/fast`.
10
10
 
11
- The status line quotes the selected model's scheduled catalog estimate — `· est. $0.30/$1.20/M` during a peak window, `· est. ▼ off-peak $0.15/$0.60/M` off it. `▲ peak` or `▼ off-peak` marks a rate above or below pi's recorded rate. With stock peak-base prices, peak has no indicator and off-peak is marked as a discount. `/peak-hours status` names the schedule.
11
+ The status line quotes the selected model's scheduled catalog estimate — `· $0.30/$1.20/M` during a peak window, `· ▼ off-peak $0.15/$0.60/M` off it. `▲ peak` or `▼ off-peak` marks a rate above or below pi's recorded rate. With stock peak-base prices, peak has no indicator and off-peak is marked as a discount. `/peak-hours status` names the schedule.
12
12
 
13
13
  The footer describes the current hour, not a particular request: a request invoked before a boundary uses its invocation timestamp even after the footer moves to the new hour. Request-specific corrections can also differ from the quoted catalog rate. Stale base prices remain stale; this extension applies a multiplier, not a price lookup. All costs are estimates, not invoices: server arrival times and Chinese public holidays are not modeled.
14
14
 
@@ -102,16 +102,16 @@ Note that a coding plan is billed in credits against a subscription, so pi's cos
102
102
 
103
103
  OpenRouter publishes the same windows machine-readably for models it hosts (`pricing.overrides` with `utc_days` / `utc_start` / `utc_end` on `https://openrouter.ai/api/v1/models`), but this extension reads its schedule from config rather than the network: the correction is a rate difference, not a price lookup, and a fetch would add a failure mode to a handler that must never throw.
104
104
 
105
- A malformed field falls back to its default rather than disabling the extension; a malformed file warns and resolves to defaults.
105
+ A malformed field falls back to its default rather than disabling the extension; a malformed file warns and resolves to defaults. `/peak-hours on|off` refuses to rewrite a malformed, unreadable, or non-object config and reports that the save failed, leaving the existing file untouched.
106
106
 
107
107
  ## Design notes
108
108
 
109
- - **Only the selected model drives the indicator.** Schedules are provider- and model-scoped, so the footer shows no rate for uncovered models. A rate above the recorded one reads `peak`, below it `off-peak`. The `est.` label applies even with request-wide coverage.
109
+ - **Only the selected model drives the indicator.** Schedules are provider- and model-scoped, so the footer shows no rate for uncovered models. A rate above the recorded one reads `peak`, below it `off-peak`.
110
110
  - **The footer follows the window it quotes.** pi has no render tick and fires no event while a session is idle, and a rate that changes at 04:00 would otherwise stay on screen until the next turn. The extension arms a single timer for the next instant the quoted rate changes and re-arms it after each fire, from the same clock reading as the status it is keeping fresh. It arms nothing when there is no status on screen, and the timer is unreferenced and cleared on session shutdown, so it neither holds pi open nor outlives its session.
111
111
  - **Rate by provider invocation.** Pi providers timestamp each assistant message before preparing and sending its HTTP request. That timestamp selects the multiplier, not completion time or the shared payload hook. With the companion, active state and schedules are captured at request dispatch; without it, foreground fallback uses the configuration at `message_end`.
112
112
  - **Scaled once.** Global weak identity guards protect usage and foreground message objects across loaded copies, retries, and returned replacements. Usage observed at dispatch remains handled even when its decision is unchanged (×1 or inactive), so fallback cannot overwrite it after a config change.
113
113
  - **Shared ownership.** The client requests usage-only leases for covered available models and the current model, deduplicated by provider. The companion scopes callbacks to the owning session. Retired leases are reacquired on reconciliation; shutdown and SDK disposal remove only this client's leases. Neither catalogs nor unrelated provider slots are replaced.
114
- - **Only cost moves.** The four cost fields are scaled and `total` is recomputed from them, matching pi's own invariant. Token counts are untouched, so rate-from-cost derivations stay self-consistent — including pi's cache-miss accounting, which derives its paid and read rates from the same message and scales with it.
114
+ - **Only cost moves.** The four catalog cost fields are scaled and estimated totals are recomputed from them. Provider-reported billed totals are preserved, including zero. Token counts are untouched, so rate-from-cost derivations stay self-consistent — including pi's cache-miss accounting, which derives its paid and read rates from the same message and scales with it.
115
115
  - **Coverage.** Only requests observed by the companion's safe dispatcher receive request-wide correction. Unavailable models, unsupported provider registrations, and usage reported independently by tools can remain uncorrected. Without the companion, coverage is foreground assistant messages only. The default weekday schedule does not encode Chinese public holidays, which DeepSeek treats as off-peak; holiday costs can therefore be overstated.
116
116
 
117
117
  ## Stack
@@ -4,7 +4,7 @@
4
4
  * DeepSeek is the clearest example: a 2x multiplier applies during peak windows
5
5
  * (01:00-04:00 and 06:00-10:00 UTC, Monday-Friday, as of September 2026) and the
6
6
  * off-peak rate is half of it. pi has no notion of time-of-day pricing —
7
- * `usage.cost` is computed once from the model's flat rates when usage is
7
+ * `usage.cost` component estimates use the model's flat rates when usage is
8
8
  * finalized — so a session is over- or understated by up to 2x, in whichever
9
9
  * direction the recorded rate sits from the billed one.
10
10
  *
@@ -15,8 +15,8 @@
15
15
  *
16
16
  * The footer indicator is scoped to the selected model and quotes the rate that
17
17
  * applies to it right now, in dollars per million tokens: with the default
18
- * schedule, `· est. $0.30/$1.20/M` at peak and
19
- * `· est. ▼ off-peak $0.15/$0.60/M` off it. This is a scheduled catalog estimate;
18
+ * schedule, `· $0.30/$1.20/M` at peak and
19
+ * `· ▼ off-peak $0.15/$0.60/M` off it. This is a scheduled catalog estimate;
20
20
  * request-specific corrections such as /fast can change the actual usage cost.
21
21
  * Uncovered models show no rate; switching models re-evaluates it.
22
22
  *
@@ -46,15 +46,15 @@
46
46
  * instead of correcting it.
47
47
  * - A message is never scaled twice. Retries and overflow recovery can deliver
48
48
  * the same message object again, so adjustments are guarded by identity.
49
- * - Only the four cost fields are touched, and `total` is recomputed from
50
- * them, preserving pi's own invariant. Token counts are real and never
49
+ * - Scale catalog cost fields and recompute estimated totals; preserve
50
+ * provider-reported billed totals, including zero. Token counts are never
51
51
  * modified, so any rate derived from cost/tokens stays self-consistent
52
52
  * (cache-miss accounting derives paid and read rates from the same message
53
53
  * and scales with it).
54
54
  * - Unknown or malformed configuration falls back to the defaults with a
55
55
  * warning; no path throws from a message handler.
56
56
  *
57
- * All costs remain estimates, not invoices: server arrival, Chinese public
57
+ * Scheduled rates remain estimates, not invoices: server arrival, Chinese public
58
58
  * holidays, and usage outside observed provider requests are not modeled.
59
59
  */
60
60
 
@@ -415,11 +415,11 @@ export function statusIndicator(
415
415
 
416
416
  switch (rateStateAt(now, schedule)) {
417
417
  case "peak":
418
- return `· est. ${STATUS_PEAK} ${rate}`;
418
+ return `· ${STATUS_PEAK} ${rate}`;
419
419
  case "off-peak":
420
- return `· est. ${STATUS_OFF_PEAK} ${rate}`;
420
+ return `· ${STATUS_OFF_PEAK} ${rate}`;
421
421
  default:
422
- return `· est. ${rate}`;
422
+ return `· ${rate}`;
423
423
  }
424
424
  }
425
425
 
@@ -444,14 +444,16 @@ export function readConfig(path: string): ConfigFile {
444
444
  }
445
445
  }
446
446
 
447
- /** Persist `active` while preserving keys this version does not model. */
448
- export function writeActive(path: string, active: boolean): void {
447
+ /** Refuse to replace unreadable/malformed config; preserve unknown valid keys. */
448
+ export function writeActive(path: string, active: boolean): boolean {
449
449
  try {
450
+ const config: unknown = existsSync(path) ? JSON.parse(readFileSync(path, "utf8")) : {};
451
+ if (!isRecord(config)) return false;
450
452
  mkdirSync(dirname(path), { recursive: true });
451
- writeFileSync(path, `${JSON.stringify({ ...readConfig(path), active }, null, 2)}\n`, "utf8");
452
- } catch (error) {
453
- const message = error instanceof Error ? error.message : String(error);
454
- console.warn(`[${STATUS_KEY}] Failed to write ${path}: ${message}`);
453
+ writeFileSync(path, `${JSON.stringify({ ...config, active }, null, 2)}\n`, "utf8");
454
+ return true;
455
+ } catch {
456
+ return false;
455
457
  }
456
458
  }
457
459
 
@@ -686,7 +688,10 @@ export default function peakHours(pi: ExtensionAPI, options: PeakHoursOptions =
686
688
  ctx.ui.notify("Usage: /peak-hours [on|off|status]", "error");
687
689
  return;
688
690
  }
689
- writeActive(configPath, arg === "on");
691
+ if (!writeActive(configPath, arg === "on")) {
692
+ ctx.ui.notify(`Peak hours: failed to save configuration at ${configPath}; keeping the previous setting.`, "error");
693
+ return;
694
+ }
690
695
  refresh();
691
696
  await pricing.reconcile(ctx);
692
697
  if (!pricing.isCurrent(ctx)) return;
@@ -36,7 +36,7 @@ export function applyMultiplier(cost: Usage["cost"], multiplier: number): Usage[
36
36
  cacheRead: cost.cacheRead * multiplier,
37
37
  cacheWrite: cost.cacheWrite * multiplier,
38
38
  };
39
- scaled.total = scaled.input + scaled.output + scaled.cacheRead + scaled.cacheWrite;
39
+ if (cost.totalFromProvider !== true) scaled.total = scaled.input + scaled.output + scaled.cacheRead + scaled.cacheWrite;
40
40
  return scaled;
41
41
  }
42
42
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nijaru/pi-peak-hours",
3
- "version": "0.0.7",
3
+ "version": "0.0.9",
4
4
  "type": "module",
5
5
  "description": "Correct pi's recorded cost for providers that bill by time of day, with a peak multiplier in the footer status.",
6
6
  "keywords": [
@@ -34,8 +34,8 @@
34
34
  "check": "bun test && oxlint extensions/ tests/ && tsc --noEmit"
35
35
  },
36
36
  "devDependencies": {
37
- "@earendil-works/pi-ai": "1.4.0",
38
- "@earendil-works/pi-coding-agent": "1.4.0",
37
+ "@earendil-works/pi-ai": "1.5.0",
38
+ "@earendil-works/pi-coding-agent": "1.5.0",
39
39
  "@types/bun": "latest",
40
40
  "oxlint": "latest",
41
41
  "typescript": "^6.0.3"