pi-spark 0.16.0 → 0.17.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/README.md CHANGED
@@ -36,7 +36,7 @@ pi-spark ships with custom editor, footer, and fullscreen rendering, replacing t
36
36
 
37
37
  pi-spark shows the active provider's credit balance or rate-limit usage in the status line, so you can keep an eye on what's left without leaving the terminal.
38
38
 
39
- - Supported providers: DeepSeek, Fireworks, Moonshot, OpenAI Codex, OpenRouter, and Vercel AI Gateway.
39
+ - Supported providers: DeepSeek, Fireworks, Kimi Code, Moonshot, OpenAI Codex, OpenRouter, and Vercel AI Gateway.
40
40
  - Most provider fetching follows [CodexBar](https://github.com/steipete/codexbar). Fireworks is the exception: its balance sits behind an internal gRPC API, reverse-engineered from the `firectl` binary (see [docs/fireworks.md](./docs/fireworks.md)).
41
41
 
42
42
  ![Credits](./assets/screenshot-credits.png)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-spark",
3
- "version": "0.16.0",
3
+ "version": "0.17.1",
4
4
  "description": "Pi package that polishes your daily experience.",
5
5
  "keywords": [
6
6
  "pi-coding-agent",
@@ -1,5 +1,6 @@
1
1
  import { deepseekProvider } from "./deepseek";
2
2
  import { fireworksProvider } from "./fireworks";
3
+ import { kimiCodeProvider } from "./kimi-code";
3
4
  import { moonshotProvider, moonshotCnProvider } from "./moonshot";
4
5
  import { openaiCodexProvider } from "./openai-codex";
5
6
  import { openrouterProvider } from "./openrouter";
@@ -10,6 +11,7 @@ import type { CreditsProvider } from "../types";
10
11
  const PROVIDERS: CreditsProvider[] = [
11
12
  deepseekProvider,
12
13
  fireworksProvider,
14
+ kimiCodeProvider,
13
15
  moonshotProvider,
14
16
  moonshotCnProvider,
15
17
  openaiCodexProvider,
@@ -0,0 +1,69 @@
1
+ import { toNumber } from "../../../utils/format";
2
+
3
+ import type { Credits, CreditsLane, CreditsProvider } from "../types";
4
+
5
+ const PROVIDER = "kimi-coding";
6
+ const URL = "https://api.kimi.com/coding/v1/usages";
7
+
8
+ interface KimiCodeUsageResponse {
9
+ usage?: { used?: string | number } | null;
10
+ limits?: {
11
+ window?: { duration?: number; timeUnit?: string } | null;
12
+ detail?: { used?: string | number } | null;
13
+ }[] | null;
14
+ }
15
+
16
+ function toPercent(used: number | undefined): number | undefined {
17
+ if (used === undefined) return undefined;
18
+ return Math.min(100, Math.max(0, used));
19
+ }
20
+
21
+ function formatWindowLabel(window?: { duration?: number; timeUnit?: string } | null): string {
22
+ if (!window?.duration) return "Limit";
23
+
24
+ const duration = window.duration;
25
+ const unit = window.timeUnit?.toUpperCase() ?? "";
26
+
27
+ if (unit === "TIME_UNIT_MINUTE") {
28
+ if (duration >= 60 && duration % 60 === 0) return `${duration / 60}h`;
29
+ return `${duration}m`;
30
+ }
31
+ if (unit === "TIME_UNIT_HOUR") return `${duration}h`;
32
+ if (unit === "TIME_UNIT_DAY") return `${duration}d`;
33
+ if (unit === "TIME_UNIT_MONTH") return `${duration}mo`;
34
+ return `${duration}${unit.replace("TIME_UNIT_", "").toLowerCase()}`;
35
+ }
36
+
37
+ function buildLane(label: string, detail?: KimiCodeUsageResponse["usage"]): CreditsLane {
38
+ return { label, percent: toPercent(toNumber(detail?.used)) };
39
+ }
40
+
41
+ export const kimiCodeProvider: CreditsProvider = {
42
+ id: PROVIDER,
43
+ label: "Kimi Code",
44
+
45
+ async fetch(_ctx, apiKey, signal): Promise<Credits> {
46
+ const headers: Record<string, string> = {
47
+ Accept: "application/json",
48
+ Authorization: `Bearer ${apiKey}`,
49
+ };
50
+
51
+ const response = await fetch(URL, { headers, signal });
52
+ if (!response.ok) throw new Error("request failed");
53
+
54
+ const payload = (await response.json()) as KimiCodeUsageResponse;
55
+ const lanes: CreditsLane[] = [];
56
+
57
+ payload.limits?.forEach((item) => {
58
+ if (!item) return;
59
+
60
+ const label = formatWindowLabel(item.window);
61
+ lanes.push(buildLane(label, item.detail));
62
+ });
63
+
64
+ const weeklyLane = buildLane("7d", payload.usage);
65
+ lanes.push(weeklyLane);
66
+
67
+ return { type: "windows", lanes };
68
+ },
69
+ };
@@ -11,16 +11,14 @@ interface CodexUsageResponse {
11
11
  primary_window?: CodexRateWindow | null;
12
12
  secondary_window?: CodexRateWindow | null;
13
13
  } | null;
14
- credits?: {
15
- unlimited?: boolean;
16
- } | null;
14
+ credits?: { unlimited?: boolean } | null;
17
15
  }
18
16
 
19
17
  interface CodexRateWindow {
20
18
  used_percent?: number | string;
21
19
  }
22
20
 
23
- export function getAccountId(ctx: ExtensionContext): string | undefined {
21
+ function getAccountId(ctx: ExtensionContext): string | undefined {
24
22
  const credential = ctx.modelRegistry.authStorage.get(PROVIDER) as { accountId?: string } | undefined;
25
23
  const accountId = credential?.accountId;
26
24
 
@@ -85,7 +85,13 @@ export class RecapManager {
85
85
  const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
86
86
  if (!auth.ok) throw new Error(auth.error);
87
87
 
88
- const options: SimpleStreamOptions = { maxTokens: MAX_TOKENS, signal };
88
+ const options: SimpleStreamOptions = {
89
+ maxTokens: MAX_TOKENS,
90
+ // Codex uses the session ID for request routing; without it, background calls may be
91
+ // routed to an unavailable model.
92
+ sessionId: ctx.sessionManager.getSessionId(),
93
+ signal,
94
+ };
89
95
  if (auth.apiKey) options.apiKey = auth.apiKey;
90
96
  if (auth.headers) options.headers = auth.headers;
91
97
  if (thinkingLevel !== "off") options.reasoning = thinkingLevel;
@@ -99,6 +105,10 @@ export class RecapManager {
99
105
  }],
100
106
  }, options);
101
107
 
108
+ if (response.stopReason === "error") {
109
+ throw new Error(response.errorMessage ?? "Recap generation failed");
110
+ }
111
+
102
112
  const content = response.content
103
113
  .filter((block) => block.type === "text")
104
114
  .map((block) => block.text)
@@ -68,7 +68,13 @@ export class TitleManager {
68
68
  const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
69
69
  if (!auth.ok) throw new Error(auth.error);
70
70
 
71
- const options: SimpleStreamOptions = { maxTokens: MAX_TOKENS, signal };
71
+ const options: SimpleStreamOptions = {
72
+ maxTokens: MAX_TOKENS,
73
+ // Codex uses the session ID for request routing; without it, background calls may be
74
+ // routed to an unavailable model.
75
+ sessionId: ctx.sessionManager.getSessionId(),
76
+ signal,
77
+ };
72
78
  if (auth.apiKey) options.apiKey = auth.apiKey;
73
79
  if (auth.headers) options.headers = auth.headers;
74
80
  if (thinkingLevel !== "off") options.reasoning = thinkingLevel;
@@ -82,6 +88,10 @@ export class TitleManager {
82
88
  }],
83
89
  }, options);
84
90
 
91
+ if (response.stopReason === "error") {
92
+ throw new Error(response.errorMessage ?? "Title generation failed");
93
+ }
94
+
85
95
  const content = response.content
86
96
  .filter((block) => block.type === "text")
87
97
  .map((block) => block.text)