pi-spark 0.26.0 → 0.28.0

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,23 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.28.0](https://github.com/zlliang/pi-spark/compare/v0.27.0...v0.28.0) (2026-09-22)
4
+
5
+
6
+ ### ⚠ BREAKING CHANGES
7
+
8
+ * use Pi 0.87 context projections for recaps and titles
9
+
10
+ ### Bug Fixes
11
+
12
+ * use Pi 0.87 context projections for recaps and titles ([a822fa3](https://github.com/zlliang/pi-spark/commit/a822fa350f1146123d46a7dbf53d812947ff7b49))
13
+
14
+ ## [0.27.0](https://github.com/zlliang/pi-spark/compare/v0.26.0...v0.27.0) (2026-09-20)
15
+
16
+
17
+ ### Features
18
+
19
+ * **credits:** honor Chinese holidays in DeepSeek off-peak pricing ([fc99f7d](https://github.com/zlliang/pi-spark/commit/fc99f7d152091e7c751745b46f35840676270538))
20
+
3
21
  ## [0.26.0](https://github.com/zlliang/pi-spark/compare/v0.25.0...v0.26.0) (2026-09-20)
4
22
 
5
23
 
@@ -74,4 +74,6 @@ Cache admission remains provider-controlled and noisy. In repeated GPT-5.6 Luna
74
74
 
75
75
  Use the [`measuring-model-cache`](../.agents/skills/measuring-model-cache/SKILL.md) skill to repeat the comparison after changes to Pi, providers, session handling, title, recap, or other background model features.
76
76
 
77
+ ---
78
+
77
79
  **Update Sep 19, 2026:** Background calls now use Pi 0.86's `ctx.modelRegistry.streamSimple(...).result()` to support extension-registered providers and resolve authentication automatically. Independent Codex UUIDv7 sessions and cleanup in `finally` remain unchanged.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-spark",
3
- "version": "0.26.0",
3
+ "version": "0.28.0",
4
4
  "description": "Pi package that polishes your daily experience",
5
5
  "keywords": [
6
6
  "pi-coding-agent",
@@ -44,17 +44,17 @@
44
44
  "zod": "^4.4.3"
45
45
  },
46
46
  "peerDependencies": {
47
- "@earendil-works/pi-agent-core": ">=0.86.0",
48
- "@earendil-works/pi-ai": ">=0.86.0",
49
- "@earendil-works/pi-coding-agent": ">=0.86.0",
50
- "@earendil-works/pi-tui": ">=0.86.0",
47
+ "@earendil-works/pi-agent-core": ">=0.87.0",
48
+ "@earendil-works/pi-ai": ">=0.87.0",
49
+ "@earendil-works/pi-coding-agent": ">=0.87.0",
50
+ "@earendil-works/pi-tui": ">=0.87.0",
51
51
  "typebox": "*"
52
52
  },
53
53
  "devDependencies": {
54
- "@earendil-works/pi-agent-core": ">=0.86.0",
55
- "@earendil-works/pi-ai": ">=0.86.0",
56
- "@earendil-works/pi-coding-agent": ">=0.86.0",
57
- "@earendil-works/pi-tui": ">=0.86.0",
54
+ "@earendil-works/pi-agent-core": ">=0.87.0",
55
+ "@earendil-works/pi-ai": ">=0.87.0",
56
+ "@earendil-works/pi-coding-agent": ">=0.87.0",
57
+ "@earendil-works/pi-tui": ">=0.87.0",
58
58
  "@types/node": "*",
59
59
  "typebox": "*",
60
60
  "typescript": "^7.0.2"
@@ -0,0 +1,205 @@
1
+ {
2
+ "$schema": "https://raw.githubusercontent.com/NateScarlet/holiday-cn/master/schema.json",
3
+ "$id": "https://raw.githubusercontent.com/NateScarlet/holiday-cn/master/2026.json",
4
+ "year": 2026,
5
+ "papers": [
6
+ "https://www.gov.cn/zhengce/zhengceku/202511/content_7047091.htm"
7
+ ],
8
+ "days": [
9
+ {
10
+ "name": "元旦",
11
+ "date": "2026-01-01",
12
+ "isOffDay": true
13
+ },
14
+ {
15
+ "name": "元旦",
16
+ "date": "2026-01-02",
17
+ "isOffDay": true
18
+ },
19
+ {
20
+ "name": "元旦",
21
+ "date": "2026-01-03",
22
+ "isOffDay": true
23
+ },
24
+ {
25
+ "name": "元旦",
26
+ "date": "2026-01-04",
27
+ "isOffDay": false
28
+ },
29
+ {
30
+ "name": "春节",
31
+ "date": "2026-02-14",
32
+ "isOffDay": false
33
+ },
34
+ {
35
+ "name": "春节",
36
+ "date": "2026-02-15",
37
+ "isOffDay": true
38
+ },
39
+ {
40
+ "name": "春节",
41
+ "date": "2026-02-16",
42
+ "isOffDay": true
43
+ },
44
+ {
45
+ "name": "春节",
46
+ "date": "2026-02-17",
47
+ "isOffDay": true
48
+ },
49
+ {
50
+ "name": "春节",
51
+ "date": "2026-02-18",
52
+ "isOffDay": true
53
+ },
54
+ {
55
+ "name": "春节",
56
+ "date": "2026-02-19",
57
+ "isOffDay": true
58
+ },
59
+ {
60
+ "name": "春节",
61
+ "date": "2026-02-20",
62
+ "isOffDay": true
63
+ },
64
+ {
65
+ "name": "春节",
66
+ "date": "2026-02-21",
67
+ "isOffDay": true
68
+ },
69
+ {
70
+ "name": "春节",
71
+ "date": "2026-02-22",
72
+ "isOffDay": true
73
+ },
74
+ {
75
+ "name": "春节",
76
+ "date": "2026-02-23",
77
+ "isOffDay": true
78
+ },
79
+ {
80
+ "name": "春节",
81
+ "date": "2026-02-28",
82
+ "isOffDay": false
83
+ },
84
+ {
85
+ "name": "清明节",
86
+ "date": "2026-04-04",
87
+ "isOffDay": true
88
+ },
89
+ {
90
+ "name": "清明节",
91
+ "date": "2026-04-05",
92
+ "isOffDay": true
93
+ },
94
+ {
95
+ "name": "清明节",
96
+ "date": "2026-04-06",
97
+ "isOffDay": true
98
+ },
99
+ {
100
+ "name": "劳动节",
101
+ "date": "2026-05-01",
102
+ "isOffDay": true
103
+ },
104
+ {
105
+ "name": "劳动节",
106
+ "date": "2026-05-02",
107
+ "isOffDay": true
108
+ },
109
+ {
110
+ "name": "劳动节",
111
+ "date": "2026-05-03",
112
+ "isOffDay": true
113
+ },
114
+ {
115
+ "name": "劳动节",
116
+ "date": "2026-05-04",
117
+ "isOffDay": true
118
+ },
119
+ {
120
+ "name": "劳动节",
121
+ "date": "2026-05-05",
122
+ "isOffDay": true
123
+ },
124
+ {
125
+ "name": "劳动节",
126
+ "date": "2026-05-09",
127
+ "isOffDay": false
128
+ },
129
+ {
130
+ "name": "端午节",
131
+ "date": "2026-06-19",
132
+ "isOffDay": true
133
+ },
134
+ {
135
+ "name": "端午节",
136
+ "date": "2026-06-20",
137
+ "isOffDay": true
138
+ },
139
+ {
140
+ "name": "端午节",
141
+ "date": "2026-06-21",
142
+ "isOffDay": true
143
+ },
144
+ {
145
+ "name": "国庆节",
146
+ "date": "2026-09-20",
147
+ "isOffDay": false
148
+ },
149
+ {
150
+ "name": "中秋节",
151
+ "date": "2026-09-25",
152
+ "isOffDay": true
153
+ },
154
+ {
155
+ "name": "中秋节",
156
+ "date": "2026-09-26",
157
+ "isOffDay": true
158
+ },
159
+ {
160
+ "name": "中秋节",
161
+ "date": "2026-09-27",
162
+ "isOffDay": true
163
+ },
164
+ {
165
+ "name": "国庆节",
166
+ "date": "2026-10-01",
167
+ "isOffDay": true
168
+ },
169
+ {
170
+ "name": "国庆节",
171
+ "date": "2026-10-02",
172
+ "isOffDay": true
173
+ },
174
+ {
175
+ "name": "国庆节",
176
+ "date": "2026-10-03",
177
+ "isOffDay": true
178
+ },
179
+ {
180
+ "name": "国庆节",
181
+ "date": "2026-10-04",
182
+ "isOffDay": true
183
+ },
184
+ {
185
+ "name": "国庆节",
186
+ "date": "2026-10-05",
187
+ "isOffDay": true
188
+ },
189
+ {
190
+ "name": "国庆节",
191
+ "date": "2026-10-06",
192
+ "isOffDay": true
193
+ },
194
+ {
195
+ "name": "国庆节",
196
+ "date": "2026-10-07",
197
+ "isOffDay": true
198
+ },
199
+ {
200
+ "name": "国庆节",
201
+ "date": "2026-10-10",
202
+ "isOffDay": false
203
+ }
204
+ ]
205
+ }
@@ -23,7 +23,7 @@ export interface PricingRule {
23
23
 
24
24
  /** Updates registry and active model prices without changing the provider's rule data. */
25
25
  export function applyPricingRules(ctx: ExtensionContext, provider: CreditsProvider, timestamp: number): void {
26
- (provider.pricingRules ?? []).forEach((rule) => {
26
+ (provider.pricingRules?.(timestamp) ?? []).forEach((rule) => {
27
27
  const cost = resolveCost(rule, timestamp);
28
28
  const model = ctx.modelRegistry.find(provider.id, rule.model);
29
29
 
@@ -4,8 +4,12 @@ import { http, withAuth } from "../../../utils/http";
4
4
  import type { Cost } from "../pricing";
5
5
  import type { Credits, CreditsProvider } from "../types";
6
6
 
7
+ import holidays2026 from "../../../data/holiday-cn-2026.json" with { type: "json" };
8
+
7
9
  const PROVIDER = "deepseek";
8
10
  const URL = "https://api.deepseek.com/user/balance";
11
+ const BEIJING_OFFSET_MS = 8 * 60 * 60 * 1000;
12
+ const HOLIDAYS = new Set(holidays2026.days.filter((day) => day.isOffDay).map((day) => day.date));
9
13
 
10
14
  interface DeepSeekBalanceResponse {
11
15
  balance_infos?: DeepSeekBalanceInfo[] | null;
@@ -16,46 +20,44 @@ interface DeepSeekBalanceInfo {
16
20
  total_balance?: string | number;
17
21
  }
18
22
 
23
+ /** Checks for an all-day off-peak date in Beijing, including weekends with makeup work. */
24
+ function isDeepSeekOffPeakDay(timestamp: number): boolean {
25
+ // Beijing uses UTC+8 without daylight saving time.
26
+ const date = new Date(timestamp + BEIJING_OFFSET_MS);
27
+ const day = date.getUTCDay();
28
+
29
+ // Years without bundled holiday data fall back to weekends only.
30
+ return day === 0 || day === 6 || HOLIDAYS.has(date.toISOString().slice(0, 10));
31
+ }
32
+
19
33
  export const deepseekProvider: CreditsProvider = {
20
34
  id: PROVIDER,
21
35
  label: "DeepSeek",
22
36
  link: "https://platform.deepseek.com/usage",
23
37
 
24
38
  // 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
- ],
39
+ pricingRules(timestamp) {
40
+ const offPeakDay = isDeepSeekOffPeakDay(timestamp);
41
+
42
+ return [
43
+ {
44
+ model: "deepseek-flash",
45
+ defaultCost: { input: 0.15, output: 0.6, cacheRead: 0.003, cacheWrite: 0, label: "Off-peak" },
46
+ windows: offPeakDay ? [] : [
47
+ { start: "01:00", end: "04:00", cost: { input: 0.3, output: 1.2, cacheRead: 0.006, cacheWrite: 0, label: "Peak" } },
48
+ { start: "06:00", end: "10:00", cost: { input: 0.3, output: 1.2, cacheRead: 0.006, cacheWrite: 0, label: "Peak" } },
49
+ ],
50
+ },
51
+ {
52
+ model: "deepseek-v4-pro",
53
+ defaultCost: { input: 0.66, output: 1.98, cacheRead: 0.022, cacheWrite: 0, label: "Off-peak" },
54
+ windows: offPeakDay ? [] : [
55
+ { start: "01:00", end: "04:00", cost: { input: 1.32, output: 3.96, cacheRead: 0.044, cacheWrite: 0, label: "Peak" } },
56
+ { start: "06:00", end: "10:00", cost: { input: 1.32, output: 3.96, cacheRead: 0.044, cacheWrite: 0, label: "Peak" } },
57
+ ],
58
+ },
59
+ ];
60
+ },
59
61
 
60
62
  async fetch(apiKey, signal, cost?: Cost): Promise<Credits> {
61
63
  const payload = await withAuth(http, apiKey).get(URL, { signal }).json<DeepSeekBalanceResponse>();
@@ -27,7 +27,7 @@ export interface CreditsProvider {
27
27
  readonly id: CreditsProviderId;
28
28
  readonly label: string;
29
29
  readonly link?: string;
30
- readonly pricingRules?: PricingRule[];
30
+ readonly pricingRules?: (timestamp: number) => PricingRule[];
31
31
  fetch(apiKey: string, signal: AbortSignal, cost?: Cost): Promise<Credits>;
32
32
  register?(pi: ExtensionAPI, ctx: ExtensionContext, refresh: RefreshCredits): void;
33
33
  }
@@ -1,6 +1,6 @@
1
1
  import parseDuration from "parse-duration";
2
2
  import * as z from "zod";
3
- import { convertToLlm, sessionEntryToContextMessages } from "@earendil-works/pi-coding-agent";
3
+ import { convertToLlm } from "@earendil-works/pi-coding-agent";
4
4
 
5
5
  import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
6
6
 
@@ -69,7 +69,7 @@ export class IdleListener {
69
69
  }
70
70
 
71
71
  private check(ctx: ExtensionContext): void {
72
- const canEnter = ctx.isIdle() && ctx.sessionManager.buildContextEntries().some((entry) => convertToLlm(sessionEntryToContextMessages(entry)).length > 0);
72
+ const canEnter = ctx.isIdle() && convertToLlm(ctx.sessionManager.buildSessionProjection().messages).some((message) => message.role !== "system");
73
73
  if (!canEnter) {
74
74
  this.lastEditorText = undefined;
75
75
  if (this.entered) {
@@ -1,4 +1,4 @@
1
- import { convertToLlm, serializeConversation, sessionEntryToContextMessages } from "@earendil-works/pi-coding-agent";
1
+ import { convertToLlm, serializeConversation } from "@earendil-works/pi-coding-agent";
2
2
 
3
3
  import { clearRecapWidget, setRecapLoadingWidget, setRecapTextWidget } from "./widget";
4
4
  import { sanitizeText } from "../../utils/format";
@@ -109,7 +109,7 @@ export class RecapManager {
109
109
  }
110
110
 
111
111
  private buildPrompt(ctx: ExtensionContext): string {
112
- const messages = ctx.sessionManager.buildContextEntries().flatMap(sessionEntryToContextMessages);
112
+ const messages = ctx.sessionManager.buildSessionProjection().messages;
113
113
  const text = serializeConversation(convertToLlm(messages));
114
114
  const conversation = text.length > MAX_CONVERSATION_CHARS ? text.slice(-MAX_CONVERSATION_CHARS) : text;
115
115
 
@@ -91,7 +91,7 @@ export class TitleManager {
91
91
  }
92
92
 
93
93
  private buildPrompt(ctx: ExtensionContext): string {
94
- const messages = ctx.sessionManager.getBranch().filter((entry) => entry.type === "message").map((entry) => entry.message);
94
+ const messages = ctx.sessionManager.buildSessionProjection().messages;
95
95
  const text = serializeConversation(convertToLlm(messages));
96
96
  const conversation = text.length > MAX_CONVERSATION_CHARS ? text.slice(0, MAX_CONVERSATION_CHARS) : text;
97
97