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 +14 -0
- package/README.md +7 -1
- package/assets/screenshot-peak-off-peak-pricing.png +0 -0
- package/package.json +1 -1
- package/src/features/credits/index.ts +25 -12
- package/src/features/credits/manager.ts +4 -4
- package/src/features/credits/pricing.ts +57 -0
- package/src/features/credits/providers/deepseek.ts +39 -2
- package/src/features/credits/providers/index.ts +1 -1
- package/src/features/credits/types.ts +3 -1
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
|

|
|
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
|
+

|
|
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
|
{
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -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 (!
|
|
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("
|
|
37
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|