pi-spark 0.22.3 → 0.23.1

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/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.23.1](https://github.com/zlliang/pi-spark/compare/v0.23.0...v0.23.1) (2026-09-12)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * **credits:** refresh pricing on user input ([8183402](https://github.com/zlliang/pi-spark/commit/8183402da978b41696685e244a698214fd519ce4))
9
+
10
+ ## [0.23.0](https://github.com/zlliang/pi-spark/compare/v0.22.3...v0.23.0) (2026-09-11)
11
+
12
+
13
+ ### Features
14
+
15
+ * **credits:** support provider peak/off-peak pricing ([f0dbf19](https://github.com/zlliang/pi-spark/commit/f0dbf19dca6f7a3eb290490052489eb0649680d1))
16
+
3
17
  ## [0.22.3](https://github.com/zlliang/pi-spark/compare/v0.22.2...v0.22.3) (2026-09-10)
4
18
 
5
19
 
package/README.md CHANGED
@@ -56,6 +56,12 @@ Banked rate-limit resets are saved benefits that can reset eligible Codex usage
56
56
 
57
57
  ![Codex resets](./assets/screenshot-codex-resets.png)
58
58
 
59
+ #### Peak/off-peak pricing
60
+
61
+ Pi does not currently support time-based pricing, so its cost estimates can be inaccurate for providers with peak/off-peak rates, such as [DeepSeek](https://api-docs.deepseek.com/quick_start/pricing). pi-spark updates model cost metadata so subsequent usage calculations reflect the current rate.
62
+
63
+ ![Peak/off-peak pricing](./assets/screenshot-peak-off-peak-pricing.png)
64
+
59
65
  ### Presets
60
66
 
61
67
  pi-spark lets you define named model presets in `spark.json` (see [Configuration](#configuration)), so you can switch between models and thinking levels without retyping provider details. The active preset is shown on the editor's top border.
@@ -145,7 +151,7 @@ All fields are optional. Each top-level feature runs with the defaults below unl
145
151
 
146
152
  #### `CreditsConfig`
147
153
 
148
- All supported providers are enabled by default. Set a provider to `false` to disable its credits status, or to `true` to override a global `false` in project config.
154
+ All supported providers are enabled by default. Set a provider to `false` to disable its credits status and pricing updates, or to `true` to override a global `false` in project config.
149
155
 
150
156
  ```json
151
157
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-spark",
3
- "version": "0.22.3",
3
+ "version": "0.23.1",
4
4
  "description": "Pi package that polishes your daily experience",
5
5
  "keywords": [
6
6
  "pi-coding-agent",
@@ -1,24 +1,15 @@
1
1
  import { CreditsManager } from "./manager";
2
2
  import { getEnabledProviders, registerProviderExtensions } from "./providers";
3
3
  import { loadConfig } from "../../config";
4
- import { isUsage } from "../../utils/usage";
5
4
 
6
- import type { AgentMessage } from "@earendil-works/pi-agent-core";
7
5
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
8
6
 
9
- function hasCost(message: AgentMessage): boolean {
10
- const usage = (message as { usage?: unknown }).usage;
11
- if (!isUsage(usage)) return false;
12
-
13
- return usage.cost.total > 0 || usage.input > 0 || usage.output > 0;
14
- }
15
-
16
7
  export function registerCredits(pi: ExtensionAPI): void {
17
8
  let creditsManager: CreditsManager | undefined = undefined;
18
9
 
19
10
  pi.on("session_start", (_event, ctx) => {
20
11
  const config = loadConfig(ctx).credits;
21
- if (!["tui", "rpc"].includes(ctx.mode) || !config) return;
12
+ if (!config) return;
22
13
 
23
14
  const providers = getEnabledProviders(config);
24
15
  if (providers.length === 0) return;
@@ -29,13 +20,29 @@ export function registerCredits(pi: ExtensionAPI): void {
29
20
  creditsManager.refresh(ctx);
30
21
  });
31
22
 
23
+ // Background calls run at `before_agent_start`, before the first `context` event, and the model
24
+ // registry may have been refreshed since the last apply.
25
+ pi.on("input", (_event, ctx) => {
26
+ creditsManager?.refresh(ctx);
27
+ });
28
+
32
29
  pi.on("model_select", (_event, ctx) => {
33
30
  creditsManager?.refresh(ctx);
34
31
  });
35
32
 
36
- pi.on("turn_end", (event, ctx) => {
37
- if (!hasCost(event.message)) return;
33
+ pi.on("context", (_event, ctx) => {
34
+ creditsManager?.refresh(ctx);
35
+ });
36
+
37
+ pi.on("turn_end", (_event, ctx) => {
38
+ creditsManager?.refresh(ctx);
39
+ });
40
+
41
+ pi.on("agent_settled", (_event, ctx) => {
42
+ creditsManager?.refresh(ctx);
43
+ });
38
44
 
45
+ pi.on("session_before_compact", (_event, ctx) => {
39
46
  creditsManager?.refresh(ctx);
40
47
  });
41
48
 
@@ -43,6 +50,12 @@ export function registerCredits(pi: ExtensionAPI): void {
43
50
  creditsManager?.refresh(ctx);
44
51
  });
45
52
 
53
+ pi.on("session_before_tree", (event, ctx) => {
54
+ if (!event.preparation.userWantsSummary) return;
55
+
56
+ creditsManager?.refresh(ctx);
57
+ });
58
+
46
59
  pi.on("session_tree", (event, ctx) => {
47
60
  if (!event.summaryEntry) return;
48
61
 
@@ -1,5 +1,6 @@
1
1
  import { getAuthToken } from "../../utils/auth";
2
2
  import { findProvider } from "./providers";
3
+ import { applyPricingRules } from "./pricing";
3
4
  import { renderCredits, renderError } from "./status";
4
5
 
5
6
  import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
@@ -17,6 +18,7 @@ export class CreditsManager {
17
18
  }
18
19
 
19
20
  async refresh(ctx: ExtensionContext): Promise<void> {
21
+ this.providers.forEach((provider) => applyPricingRules(ctx, provider, Date.now()));
20
22
  this.inflight?.abort();
21
23
 
22
24
  const provider = findProvider(this.providers, ctx.model?.provider);
@@ -55,10 +57,8 @@ export class CreditsManager {
55
57
  return;
56
58
  }
57
59
 
58
- const credits = await provider.fetch(token, signal);
59
-
60
- // The active model may have changed while the request was in flight.
61
- if (ctx.model?.provider !== provider.id) return;
60
+ const credits = await provider.fetch(token, signal, ctx.model?.cost);
61
+ if (signal.aborted || ctx.model?.provider !== provider.id) return;
62
62
 
63
63
  ctx.ui.setStatus(STATUS_KEY, renderCredits(ctx.ui.theme, provider.label, credits, provider.link));
64
64
  } catch (error) {
@@ -0,0 +1,57 @@
1
+ import type { Api, Model } from "@earendil-works/pi-ai";
2
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
3
+ import type { CreditsProvider } from "./types";
4
+
5
+ export type Cost = Model<Api>["cost"] & { label?: string };
6
+
7
+ /**
8
+ * A time-of-day price window. `start`/`end` are `HH:MM` in UTC. Bounds are [start, end); equal
9
+ * bounds mean all day. When `start > end` the window wraps past midnight (e.g., 16:30 → 00:30).
10
+ */
11
+ interface PricingWindow {
12
+ start: string;
13
+ end: string;
14
+ days?: number[];
15
+ cost: Cost;
16
+ }
17
+
18
+ export interface PricingRule {
19
+ model: string;
20
+ defaultCost: Cost;
21
+ windows: PricingWindow[];
22
+ }
23
+
24
+ /** Updates registry and active model prices without changing the provider's rule data. */
25
+ export function applyPricingRules(ctx: ExtensionContext, provider: CreditsProvider, timestamp: number): void {
26
+ (provider.pricingRules ?? []).forEach((rule) => {
27
+ const cost = resolveCost(rule, timestamp);
28
+ const model = ctx.modelRegistry.find(provider.id, rule.model);
29
+
30
+ if (model) model.cost = { ...cost };
31
+ if (ctx.model?.provider === provider.id && ctx.model.id === rule.model) ctx.model.cost = { ...cost };
32
+ });
33
+ }
34
+
35
+ function resolveCost(rule: PricingRule, timestamp: number): Cost {
36
+ const date = new Date(timestamp);
37
+ const minutes = date.getUTCHours() * 60 + date.getUTCMinutes();
38
+
39
+ const cost = rule.windows.reduce((result, window) => {
40
+ if (window.days && !window.days.includes(date.getUTCDay())) return result;
41
+ if (inDailyRange(minutes, parseHHMM(window.start), parseHHMM(window.end))) return window.cost;
42
+ return result;
43
+ }, rule.defaultCost);
44
+
45
+ return cost;
46
+ }
47
+
48
+ function parseHHMM(hhmm: string): number {
49
+ const [hours, minutes] = hhmm.split(":");
50
+ return Number(hours) * 60 + Number(minutes);
51
+ }
52
+
53
+ function inDailyRange(minutes: number, start: number, end: number): boolean {
54
+ if (start === end) return true;
55
+ if (start < end) return minutes >= start && minutes < end;
56
+ return minutes >= start || minutes < end;
57
+ }
@@ -1,6 +1,7 @@
1
1
  import { convertToUSD, toNumber } from "../../../utils/format";
2
2
  import { http, withAuth } from "../../../utils/http";
3
3
 
4
+ import type { Cost } from "../pricing";
4
5
  import type { Credits, CreditsProvider } from "../types";
5
6
 
6
7
  const PROVIDER = "deepseek";
@@ -20,11 +21,47 @@ export const deepseekProvider: CreditsProvider = {
20
21
  label: "DeepSeek",
21
22
  link: "https://platform.deepseek.com/usage",
22
23
 
23
- async fetch(apiKey, signal): Promise<Credits> {
24
+ // See https://api-docs.deepseek.com/quick_start/pricing/.
25
+ pricingRules: [
26
+ {
27
+ model: "deepseek-flash",
28
+ defaultCost: { input: 0.15, output: 0.6, cacheRead: 0.003, cacheWrite: 0, label: "off-peak" },
29
+ windows: [
30
+ { start: "01:00", end: "04:00", days: [1, 2, 3, 4, 5], cost: { input: 0.3, output: 1.2, cacheRead: 0.006, cacheWrite: 0, label: "peak" } },
31
+ { start: "06:00", end: "10:00", days: [1, 2, 3, 4, 5], cost: { input: 0.3, output: 1.2, cacheRead: 0.006, cacheWrite: 0, label: "peak" } },
32
+ ],
33
+ },
34
+ {
35
+ model: "deepseek-v4-flash",
36
+ defaultCost: { input: 0.15, output: 0.6, cacheRead: 0.003, cacheWrite: 0, label: "off-peak" },
37
+ windows: [
38
+ { start: "01:00", end: "04:00", days: [1, 2, 3, 4, 5], cost: { input: 0.3, output: 1.2, cacheRead: 0.006, cacheWrite: 0, label: "peak" } },
39
+ { start: "06:00", end: "10:00", days: [1, 2, 3, 4, 5], cost: { input: 0.3, output: 1.2, cacheRead: 0.006, cacheWrite: 0, label: "peak" } },
40
+ ],
41
+ },
42
+ {
43
+ model: "deepseek-v4-flash-vision-exp",
44
+ defaultCost: { input: 0.15, output: 0.6, cacheRead: 0.003, cacheWrite: 0, label: "off-peak" },
45
+ windows: [
46
+ { start: "01:00", end: "04:00", days: [1, 2, 3, 4, 5], cost: { input: 0.3, output: 1.2, cacheRead: 0.006, cacheWrite: 0, label: "peak" } },
47
+ { start: "06:00", end: "10:00", days: [1, 2, 3, 4, 5], cost: { input: 0.3, output: 1.2, cacheRead: 0.006, cacheWrite: 0, label: "peak" } },
48
+ ],
49
+ },
50
+ {
51
+ model: "deepseek-v4-pro",
52
+ defaultCost: { input: 0.66, output: 1.98, cacheRead: 0.022, cacheWrite: 0, label: "off-peak" },
53
+ windows: [
54
+ { start: "01:00", end: "04:00", days: [1, 2, 3, 4, 5], cost: { input: 1.32, output: 3.96, cacheRead: 0.044, cacheWrite: 0, label: "peak" } },
55
+ { start: "06:00", end: "10:00", days: [1, 2, 3, 4, 5], cost: { input: 1.32, output: 3.96, cacheRead: 0.044, cacheWrite: 0, label: "peak" } },
56
+ ],
57
+ },
58
+ ],
59
+
60
+ async fetch(apiKey, signal, cost?: Cost): Promise<Credits> {
24
61
  const payload = await withAuth(http, apiKey).get(URL, { signal }).json<DeepSeekBalanceResponse>();
25
62
  const balance = payload.balance_infos?.find((entry) => entry.currency === "USD") ?? payload.balance_infos?.[0];
26
63
  const remaining = await convertToUSD(toNumber(balance?.total_balance), balance?.currency, signal);
27
64
 
28
- return { type: "balance", remaining };
65
+ return { type: "balance", remaining, suffix: cost?.label ? `(${cost.label})` : undefined };
29
66
  },
30
67
  };
@@ -31,5 +31,5 @@ export function findProvider(providers: CreditsProvider[], provider?: string): C
31
31
  }
32
32
 
33
33
  export function registerProviderExtensions(pi: ExtensionAPI, ctx: ExtensionContext, providers: CreditsProvider[], refresh: RefreshCredits): void {
34
- for (const provider of providers) provider.register?.(pi, ctx, refresh);
34
+ providers.forEach((provider) => provider.register?.(pi, ctx, refresh));
35
35
  }
@@ -1,5 +1,6 @@
1
1
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
2
2
  import type { CreditsProviderId } from "./config";
3
+ import type { Cost, PricingRule } from "./pricing";
3
4
 
4
5
  /**
5
6
  * Normalized credits/usage for a provider.
@@ -26,6 +27,7 @@ export interface CreditsProvider {
26
27
  readonly id: CreditsProviderId;
27
28
  readonly label: string;
28
29
  readonly link?: string;
29
- fetch(apiKey: string, signal: AbortSignal): Promise<Credits>;
30
+ readonly pricingRules?: PricingRule[];
31
+ fetch(apiKey: string, signal: AbortSignal, cost?: Cost): Promise<Credits>;
30
32
  register?(pi: ExtensionAPI, ctx: ExtensionContext, refresh: RefreshCredits): void;
31
33
  }