@nijaru/pi-peak-hours 0.0.3 → 0.0.4

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
@@ -100,10 +100,10 @@ A malformed field falls back to its default rather than disabling the extension;
100
100
 
101
101
  - **Only the selected model drives the indicator.** Schedules are provider- and model-scoped, so the footer never shows a rate for a service that does not bill by time of day. A rate above the recorded one reads `peak`, below it `off-peak`, and a discount is never labelled a peak. The quoted rate and the written cost come from the same multiplication, so they cannot disagree.
102
102
  - **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.
103
- - **Rate by request start.** DeepSeek bills by arrival time, so the multiplier is chosen from the `message_start` timestamp rather than `message_end`. A request spanning a window boundary would otherwise land on the wrong side.
103
+ - **Rate by request start.** DeepSeek bills by arrival time, so the multiplier is chosen from the logical request start captured in `before_provider_request`, not `message_end`. `message_start` only fires once the response begins, so it would misprice a request that spans a window boundary.
104
104
  - **Scaled once.** Retries and overflow recovery can re-deliver the same message object, so adjustments are guarded by identity; the replacement returned to pi is registered too, so a replayed replacement cannot be scaled twice.
105
105
  - **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.
106
- - **Coverage.** Assistant messages carry the bulk of a session's spend. Compaction and branch-summary usage, plus token usage reported by tools, also feed the footer but cannot be adjusted after the fact, so totals stay an estimate rather than an invoice.
106
+ - **Coverage.** Assistant messages carry the bulk of a session's spend. Compaction and branch-summary usage, prompt-cache-warming refreshes, and token usage reported by tools also consume budget but cannot be adjusted after the fact, so totals stay an estimate rather than an invoice.
107
107
 
108
108
  ## Stack
109
109
 
@@ -552,6 +552,7 @@ export default function peakHours(pi: ExtensionAPI, options: PeakHoursOptions =
552
552
  let active = parseActive(config.active);
553
553
  const adjusted = adjustedMessages();
554
554
  let pendingStart: number | undefined;
555
+ let requestStartMs: number | undefined;
555
556
  let timer: ReturnType<typeof setTimeout> | undefined;
556
557
 
557
558
  function refresh(): void {
@@ -635,6 +636,7 @@ export default function peakHours(pi: ExtensionAPI, options: PeakHoursOptions =
635
636
 
636
637
  pi.on("session_shutdown", async (_event, ctx) => {
637
638
  pendingStart = undefined;
639
+ requestStartMs = undefined;
638
640
  disarm();
639
641
  ctx.ui.setStatus(STATUS_KEY, undefined);
640
642
  });
@@ -649,8 +651,17 @@ export default function peakHours(pi: ExtensionAPI, options: PeakHoursOptions =
649
651
  updateStatus(ctx);
650
652
  });
651
653
 
654
+ // The provider bills when the request arrives, and `message_start` is emitted
655
+ // only once the response begins, so capture the logical request start here and
656
+ // fall back to the message clock when no request was observed.
657
+ pi.on("before_provider_request", async () => {
658
+ if (active) requestStartMs = Date.now();
659
+ });
660
+
652
661
  pi.on("message_start", async (event) => {
653
- if (event.message.role === "assistant") pendingStart = Date.now();
662
+ if (event.message.role !== "assistant") return;
663
+ pendingStart = requestStartMs ?? Date.now();
664
+ requestStartMs = undefined;
654
665
  });
655
666
 
656
667
  pi.on("message_end", async (event, ctx) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nijaru/pi-peak-hours",
3
- "version": "0.0.3",
3
+ "version": "0.0.4",
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": "0.85.1",
38
- "@earendil-works/pi-coding-agent": "0.85.1",
37
+ "@earendil-works/pi-ai": "0.87.0",
38
+ "@earendil-works/pi-coding-agent": "0.87.0",
39
39
  "@types/bun": "latest",
40
40
  "oxlint": "latest",
41
41
  "typescript": "^6.0.3"