pi-spark 0.20.0 → 0.21.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/LICENSE.md +9 -0
- package/README.md +28 -4
- package/docs/reverse-engineering-fireworks-credits.md +1 -1
- package/index.ts +2 -0
- package/package.json +14 -9
- package/src/config/schema.ts +2 -0
- package/src/features/credits/index.ts +1 -1
- package/src/features/credits/manager.ts +4 -5
- package/src/features/credits/providers/deepseek.ts +2 -9
- package/src/features/credits/providers/kimi-code.ts +2 -9
- package/src/features/credits/providers/moonshot.ts +2 -9
- package/src/features/credits/providers/openai-codex.ts +27 -47
- package/src/features/credits/providers/openrouter.ts +2 -9
- package/src/features/credits/providers/vercel-ai-gateway.ts +2 -9
- package/src/features/editor/config.ts +3 -0
- package/src/features/editor/index.ts +21 -4
- package/src/features/footer/index.ts +2 -2
- package/src/features/fullscreen/filler.ts +51 -49
- package/src/features/fullscreen/index.ts +2 -2
- package/src/features/presets/manager.ts +7 -3
- package/src/features/recap/index.ts +1 -1
- package/src/features/title/index.ts +8 -3
- package/src/features/title/manager.ts +2 -3
- package/src/features/write/config.ts +3 -0
- package/src/features/write/index.ts +177 -0
- package/src/features/write/path.ts +33 -0
- package/src/utils/auth.ts +11 -0
- package/src/utils/format.ts +3 -4
- package/src/utils/http.ts +16 -0
- package/src/utils/usage.ts +2 -0
- package/themes/github-dark-default.json +91 -0
- package/themes/github-light-default.json +91 -0
- package/LICENSE +0 -21
package/LICENSE.md
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026-present Zilong Liang
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
6
|
+
|
|
7
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
CHANGED
|
@@ -69,13 +69,19 @@ pi-spark generates a short recap of the current session after it goes idle, or o
|
|
|
69
69
|
|
|
70
70
|
### Title
|
|
71
71
|
|
|
72
|
-
pi-spark names the session automatically after the first
|
|
72
|
+
pi-spark names the session automatically after the first completed turn, so it's easy to find later in the session selector.
|
|
73
73
|
|
|
74
|
-
-
|
|
74
|
+
- Once the agent completes its first response and any tool calls from that turn, pi-spark generates a short title from the conversation and sets it as the session name.
|
|
75
75
|
- Generation is silent: nothing is shown in the UI, and the usage is recorded in a session entry like the recap.
|
|
76
76
|
- Sessions that already have a name are left untouched.
|
|
77
77
|
- The title generation can use its own model, configured separately from your working model.
|
|
78
78
|
|
|
79
|
+
### Scrolling write previews
|
|
80
|
+
|
|
81
|
+
Long, streaming `write` tool calls keep their latest lines visible instead of pinning the preview to the beginning. The omitted-line count appears above the preview, while `ctrl+o` still expands the full content.
|
|
82
|
+
|
|
83
|
+
https://github.com/user-attachments/assets/b7bb77c1-c188-46a2-aba9-6487bdf58fa0
|
|
84
|
+
|
|
79
85
|
## Configuration
|
|
80
86
|
|
|
81
87
|
pi-spark reads config from `~/.pi/agent/spark.json` and from the current project's `.pi/spark.json`. Project config overrides matching global fields.
|
|
@@ -126,7 +132,8 @@ All fields are optional. Each top-level feature runs with the defaults below unl
|
|
|
126
132
|
| `fullscreen` | `{}` | Clears the screen and scrollback on start and exit, and pins the editor and footer to the bottom. |
|
|
127
133
|
| `presets` | `{ [name]: Preset }` | Defines named model presets, keyed by name. |
|
|
128
134
|
| `recap` | `RecapConfig` | Generates a session recap when idle or on demand. |
|
|
129
|
-
| `title` | `TitleConfig` | Names the session automatically after the first
|
|
135
|
+
| `title` | `TitleConfig` | Names the session automatically after the first completed turn. |
|
|
136
|
+
| `write` | `{}` | Keeps the latest lines of streaming `write` tool calls visible. |
|
|
130
137
|
|
|
131
138
|
#### `CreditsConfig`
|
|
132
139
|
|
|
@@ -149,7 +156,7 @@ All supported providers are enabled by default. Set a provider to `false` to dis
|
|
|
149
156
|
|
|
150
157
|
#### `EditorConfig`
|
|
151
158
|
|
|
152
|
-
The `spinner` field is optional and defaults to `tildes`.
|
|
159
|
+
The `spinner` field is optional and defaults to `tildes`. `thinkingLevelIndicator` is also optional and defaults to `border`.
|
|
153
160
|
|
|
154
161
|
| Field | Value | Description |
|
|
155
162
|
| --- | --- | --- |
|
|
@@ -157,6 +164,8 @@ The `spinner` field is optional and defaults to `tildes`.
|
|
|
157
164
|
| | `lights` | `○, ●` |
|
|
158
165
|
| | `tildes` (default) | `∼, ≈, ≋, ≈, ∼` |
|
|
159
166
|
| | `pulse` | `·, •, ●, •, ·` |
|
|
167
|
+
| `thinkingLevelIndicator` | `border` (default) | Uses the thinking-level color for the editor border. |
|
|
168
|
+
| | `model` | Uses the thinking-level color for the model name and the dim color for the editor border. |
|
|
160
169
|
|
|
161
170
|
#### `FooterConfig`
|
|
162
171
|
|
|
@@ -213,3 +222,18 @@ For example, to disable the customized footer:
|
|
|
213
222
|
"footer": false
|
|
214
223
|
}
|
|
215
224
|
```
|
|
225
|
+
|
|
226
|
+
## Themes
|
|
227
|
+
|
|
228
|
+
pi-spark ships with two custom themes inspired by [GitHub's VS Code themes](https://github.com/primer/github-vscode-theme):
|
|
229
|
+
|
|
230
|
+
- [`github-light-default`](themes/github-light-default.json)
|
|
231
|
+
- [`github-dark-default`](themes/github-dark-default.json)
|
|
232
|
+
|
|
233
|
+
Select either theme in `/settings`, or configure automatic switching between them:
|
|
234
|
+
|
|
235
|
+
```json
|
|
236
|
+
{
|
|
237
|
+
"theme": "github-light-default/github-dark-default"
|
|
238
|
+
}
|
|
239
|
+
```
|
|
@@ -89,7 +89,7 @@ The amount is `units + nanos / 1e9` (the standard `google.type.Money` encoding),
|
|
|
89
89
|
The provider lives in [`src/features/credits/providers/fireworks.ts`](../src/features/credits/providers/fireworks.ts) and follows the same `CreditsProvider` contract as the HTTP-based providers; only the transport differs.
|
|
90
90
|
|
|
91
91
|
- **Transport.** A single, reused `@grpc/grpc-js` client dials `gateway.fireworks.ai:443` over TLS. The service is declared in [`fireworks.proto`](../src/features/credits/providers/fireworks.proto) — the minimal slice needed, loaded at runtime with `@grpc/proto-loader`. The `.proto` is resolved relative to the module via `import.meta.url`, so it ships and loads from the package directory.
|
|
92
|
-
- **Auth.** Each call sends `x-api-key: <key>` in gRPC metadata. The
|
|
92
|
+
- **Auth.** Each call sends `x-api-key: <key>` in gRPC metadata. The manager resolves provider authentication through Pi's `modelRegistry.getProviderAuth("fireworks")`. For Fireworks, Pi returns the API key sourced from `FIREWORKS_API_KEY`.
|
|
93
93
|
- **Account resolution.** `ListAccounts` is called once and the resolved `accounts/<id>` is cached per API key, so steady-state refreshes are a single `GetBalance` round trip.
|
|
94
94
|
- **Cancellation.** The shared `AbortSignal` cancels the in-flight gRPC call, and a defensive deadline bounds each request.
|
|
95
95
|
- **Result.** `Money` is converted to a dollar amount and returned as `{ type: "balance", remaining }`, which the status line renders like any other balance provider.
|
package/index.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { registerFullscreen } from "./src/features/fullscreen";
|
|
|
6
6
|
import { registerPresets } from "./src/features/presets";
|
|
7
7
|
import { registerRecap } from "./src/features/recap";
|
|
8
8
|
import { registerTitle } from "./src/features/title";
|
|
9
|
+
import { registerWrite } from "./src/features/write";
|
|
9
10
|
|
|
10
11
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
11
12
|
|
|
@@ -26,4 +27,5 @@ export default function (pi: ExtensionAPI) {
|
|
|
26
27
|
registerPresets(pi);
|
|
27
28
|
registerRecap(pi);
|
|
28
29
|
registerTitle(pi);
|
|
30
|
+
registerWrite(pi);
|
|
29
31
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-spark",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.21.1",
|
|
4
4
|
"description": "Pi package that polishes your daily experience.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-coding-agent",
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
"files": [
|
|
16
16
|
"index.ts",
|
|
17
17
|
"src",
|
|
18
|
+
"themes",
|
|
18
19
|
"assets",
|
|
19
20
|
"docs",
|
|
20
21
|
"README.md",
|
|
@@ -24,6 +25,9 @@
|
|
|
24
25
|
"extensions": [
|
|
25
26
|
"./index.ts"
|
|
26
27
|
],
|
|
28
|
+
"themes": [
|
|
29
|
+
"./themes"
|
|
30
|
+
],
|
|
27
31
|
"image": "https://raw.githubusercontent.com/zlliang/pi-spark/main/assets/cover.png"
|
|
28
32
|
},
|
|
29
33
|
"scripts": {
|
|
@@ -33,22 +37,23 @@
|
|
|
33
37
|
"@grpc/grpc-js": "^1.14.4",
|
|
34
38
|
"@grpc/proto-loader": "^0.8.1",
|
|
35
39
|
"defu": "^6.1.7",
|
|
40
|
+
"ky": "^2.0.2",
|
|
36
41
|
"parse-duration": "^2.1.8",
|
|
37
42
|
"pretty-ms": "^9.3.0",
|
|
38
43
|
"zod": "^4.4.3"
|
|
39
44
|
},
|
|
40
45
|
"peerDependencies": {
|
|
41
|
-
"@earendil-works/pi-agent-core": ">=0.
|
|
42
|
-
"@earendil-works/pi-ai": ">=0.
|
|
43
|
-
"@earendil-works/pi-coding-agent": ">=0.
|
|
44
|
-
"@earendil-works/pi-tui": ">=0.
|
|
46
|
+
"@earendil-works/pi-agent-core": ">=0.81.0",
|
|
47
|
+
"@earendil-works/pi-ai": ">=0.81.0",
|
|
48
|
+
"@earendil-works/pi-coding-agent": ">=0.81.0",
|
|
49
|
+
"@earendil-works/pi-tui": ">=0.81.0",
|
|
45
50
|
"typebox": "*"
|
|
46
51
|
},
|
|
47
52
|
"devDependencies": {
|
|
48
|
-
"@earendil-works/pi-agent-core": ">=0.
|
|
49
|
-
"@earendil-works/pi-ai": ">=0.
|
|
50
|
-
"@earendil-works/pi-coding-agent": ">=0.
|
|
51
|
-
"@earendil-works/pi-tui": ">=0.
|
|
53
|
+
"@earendil-works/pi-agent-core": ">=0.81.0",
|
|
54
|
+
"@earendil-works/pi-ai": ">=0.81.0",
|
|
55
|
+
"@earendil-works/pi-coding-agent": ">=0.81.0",
|
|
56
|
+
"@earendil-works/pi-tui": ">=0.81.0",
|
|
52
57
|
"@types/node": "*",
|
|
53
58
|
"typebox": "*",
|
|
54
59
|
"typescript": "^7.0.2"
|
package/src/config/schema.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { fullscreenConfigSchema } from "../features/fullscreen/config";
|
|
|
7
7
|
import { presetsConfigSchema } from "../features/presets/config";
|
|
8
8
|
import { recapConfigSchema } from "../features/recap/config";
|
|
9
9
|
import { titleConfigSchema } from "../features/title/config";
|
|
10
|
+
import { writeConfigSchema } from "../features/write/config";
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* Raw option shape for each feature. The enable/disable/default policy lives in `loadConfig`:
|
|
@@ -21,6 +22,7 @@ export const featureSchemas = {
|
|
|
21
22
|
presets: presetsConfigSchema,
|
|
22
23
|
recap: recapConfigSchema,
|
|
23
24
|
title: titleConfigSchema,
|
|
25
|
+
write: writeConfigSchema,
|
|
24
26
|
} as const;
|
|
25
27
|
|
|
26
28
|
/** Resolved config for every feature; `false` means the feature is disabled. */
|
|
@@ -18,7 +18,7 @@ export function registerCredits(pi: ExtensionAPI): void {
|
|
|
18
18
|
|
|
19
19
|
pi.on("session_start", (_event, ctx) => {
|
|
20
20
|
const config = loadConfig(ctx).credits;
|
|
21
|
-
if (
|
|
21
|
+
if (ctx.mode !== "tui" || !config) return;
|
|
22
22
|
|
|
23
23
|
const providers = getEnabledProviders(config);
|
|
24
24
|
if (providers.length === 0) return;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { getAuthToken } from "../../utils/auth";
|
|
1
2
|
import { findProvider } from "./providers";
|
|
2
3
|
import { renderCredits, renderError } from "./status";
|
|
3
4
|
|
|
@@ -5,7 +6,6 @@ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
|
5
6
|
import type { CreditsProvider } from "./types";
|
|
6
7
|
|
|
7
8
|
const STATUS_KEY = "credits";
|
|
8
|
-
const REQUEST_TIMEOUT_MS = 30_000;
|
|
9
9
|
|
|
10
10
|
export class CreditsManager {
|
|
11
11
|
private inflight: AbortController | undefined = undefined;
|
|
@@ -48,15 +48,14 @@ export class CreditsManager {
|
|
|
48
48
|
|
|
49
49
|
private async fetch(ctx: ExtensionContext, provider: CreditsProvider, signal: AbortSignal): Promise<void> {
|
|
50
50
|
try {
|
|
51
|
-
const
|
|
51
|
+
const token = await getAuthToken(ctx.modelRegistry, provider.id);
|
|
52
52
|
if (signal.aborted) return;
|
|
53
|
-
if (!
|
|
53
|
+
if (!token) {
|
|
54
54
|
ctx.ui.setStatus(STATUS_KEY, undefined);
|
|
55
55
|
return;
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
const
|
|
59
|
-
const credits = await provider.fetch(apiKey, signals);
|
|
58
|
+
const credits = await provider.fetch(token, signal);
|
|
60
59
|
|
|
61
60
|
// The active model may have changed while the request was in flight.
|
|
62
61
|
if (ctx.model?.provider !== provider.id) return;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { convertToUSD, toNumber } from "../../../utils/format";
|
|
2
|
+
import { http, withAuth } from "../../../utils/http";
|
|
2
3
|
|
|
3
4
|
import type { Credits, CreditsProvider } from "../types";
|
|
4
5
|
|
|
@@ -19,15 +20,7 @@ export const deepseekProvider: CreditsProvider = {
|
|
|
19
20
|
label: "DeepSeek",
|
|
20
21
|
|
|
21
22
|
async fetch(apiKey, signal): Promise<Credits> {
|
|
22
|
-
const
|
|
23
|
-
Accept: "application/json",
|
|
24
|
-
Authorization: `Bearer ${apiKey}`,
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
const response = await fetch(URL, { headers, signal });
|
|
28
|
-
if (!response.ok) throw new Error("request failed");
|
|
29
|
-
|
|
30
|
-
const payload = (await response.json()) as DeepSeekBalanceResponse;
|
|
23
|
+
const payload = await withAuth(http, apiKey).get(URL, { signal }).json<DeepSeekBalanceResponse>();
|
|
31
24
|
const balance = payload.balance_infos?.find((entry) => entry.currency === "USD") ?? payload.balance_infos?.[0];
|
|
32
25
|
const remaining = await convertToUSD(toNumber(balance?.total_balance), balance?.currency, signal);
|
|
33
26
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { toNumber } from "../../../utils/format";
|
|
2
|
+
import { http, withAuth } from "../../../utils/http";
|
|
2
3
|
|
|
3
4
|
import type { Credits, CreditsLane, CreditsProvider } from "../types";
|
|
4
5
|
|
|
@@ -82,15 +83,7 @@ export const kimiCodeProvider: CreditsProvider = {
|
|
|
82
83
|
label: "Kimi Code",
|
|
83
84
|
|
|
84
85
|
async fetch(apiKey, signal): Promise<Credits> {
|
|
85
|
-
const
|
|
86
|
-
Accept: "application/json",
|
|
87
|
-
Authorization: `Bearer ${apiKey}`,
|
|
88
|
-
};
|
|
89
|
-
|
|
90
|
-
const response = await fetch(URL, { headers, signal });
|
|
91
|
-
if (!response.ok) throw new Error("request failed");
|
|
92
|
-
|
|
93
|
-
const payload = (await response.json()) as KimiCodeUsageResponse;
|
|
86
|
+
const payload = await withAuth(http, apiKey).get(URL, { signal }).json<KimiCodeUsageResponse>();
|
|
94
87
|
const lanes: CreditsLane[] = [];
|
|
95
88
|
|
|
96
89
|
payload.limits?.forEach((item) => {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { convertToUSD, toNumber } from "../../../utils/format";
|
|
2
|
+
import { http, withAuth } from "../../../utils/http";
|
|
2
3
|
|
|
3
4
|
import type { Credits, CreditsProvider } from "../types";
|
|
4
5
|
|
|
@@ -19,15 +20,7 @@ function createMoonshotProvider(id: "moonshotai" | "moonshotai-cn", host: string
|
|
|
19
20
|
label: "Moonshot",
|
|
20
21
|
|
|
21
22
|
async fetch(apiKey, signal): Promise<Credits> {
|
|
22
|
-
const
|
|
23
|
-
Accept: "application/json",
|
|
24
|
-
Authorization: `Bearer ${apiKey}`,
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
const response = await fetch(`https://${host}/v1/users/me/balance`, { headers, signal });
|
|
28
|
-
if (!response.ok) throw new Error("request failed");
|
|
29
|
-
|
|
30
|
-
const payload = (await response.json()) as MoonshotBalanceResponse;
|
|
23
|
+
const payload = await withAuth(http, apiKey).get(`https://${host}/v1/users/me/balance`, { signal }).json<MoonshotBalanceResponse>();
|
|
31
24
|
const remaining = await convertToUSD(toNumber(payload.data?.available_balance), currency, signal);
|
|
32
25
|
|
|
33
26
|
return { type: "balance", remaining };
|
|
@@ -3,9 +3,12 @@ import { readStoredCredential } from "@earendil-works/pi-coding-agent";
|
|
|
3
3
|
import prettyMilliseconds from "pretty-ms";
|
|
4
4
|
|
|
5
5
|
import { confirmCodexReset, formatAvailableResets, showCodexResetLoader, showCodexResetSelector } from "./openai-codex-panel";
|
|
6
|
+
import { getAuthToken } from "../../../utils/auth";
|
|
6
7
|
import { toNumber } from "../../../utils/format";
|
|
8
|
+
import { http, withAuth } from "../../../utils/http";
|
|
7
9
|
|
|
8
10
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
11
|
+
import type { KyInstance } from "ky";
|
|
9
12
|
import type { CodexResetPanelData } from "./openai-codex-panel";
|
|
10
13
|
import type { Credits, CreditsLane, CreditsProvider, RefreshCredits } from "../types";
|
|
11
14
|
|
|
@@ -14,7 +17,6 @@ const BASE_URL = "https://chatgpt.com/backend-api";
|
|
|
14
17
|
const USAGE_PATH = "/wham/usage";
|
|
15
18
|
const RESET_CREDITS_PATH = "/wham/rate-limit-reset-credits";
|
|
16
19
|
const CONSUME_RESET_PATH = "/wham/rate-limit-reset-credits/consume";
|
|
17
|
-
const REQUEST_TIMEOUT_MS = 30_000;
|
|
18
20
|
|
|
19
21
|
interface CodexUsageResponse {
|
|
20
22
|
rate_limit?: {
|
|
@@ -50,14 +52,13 @@ interface ConsumeResetResponse {
|
|
|
50
52
|
}
|
|
51
53
|
|
|
52
54
|
async function runCodexReset(ctx: ExtensionContext, refresh: RefreshCredits): Promise<void> {
|
|
53
|
-
const
|
|
54
|
-
if (!
|
|
55
|
+
const token = await getAuthToken(ctx.modelRegistry, PROVIDER);
|
|
56
|
+
if (!token) return;
|
|
55
57
|
|
|
56
|
-
const
|
|
58
|
+
const client = createClient(token);
|
|
57
59
|
|
|
58
60
|
const credit = await showCodexResetSelector(ctx, async (signal) => {
|
|
59
|
-
const
|
|
60
|
-
const [usage, details] = await Promise.all([fetchUsage(headers, requestSignal), fetchResetCredits(headers, requestSignal)]);
|
|
61
|
+
const [usage, details] = await Promise.all([fetchUsage(client, signal), fetchResetCredits(client, signal)]);
|
|
61
62
|
|
|
62
63
|
return {
|
|
63
64
|
...details,
|
|
@@ -66,63 +67,42 @@ async function runCodexReset(ctx: ExtensionContext, refresh: RefreshCredits): Pr
|
|
|
66
67
|
});
|
|
67
68
|
if (!credit || !(await confirmCodexReset(ctx, credit))) return;
|
|
68
69
|
|
|
69
|
-
await redeemReset(ctx,
|
|
70
|
+
await redeemReset(ctx, client, credit, refresh);
|
|
70
71
|
}
|
|
71
72
|
|
|
72
|
-
async function redeemReset(ctx: ExtensionContext,
|
|
73
|
+
async function redeemReset(ctx: ExtensionContext, client: KyInstance, credit: BankedRateLimitReset, refresh: RefreshCredits): Promise<void> {
|
|
73
74
|
await showCodexResetLoader(ctx, async () => {
|
|
74
|
-
const response = await consumeReset(
|
|
75
|
+
const response = await consumeReset(client, uuidv7(), credit.id);
|
|
75
76
|
if (response.code === "nothing_to_reset") throw new Error("No Codex rate-limit window currently needs a reset");
|
|
76
77
|
if (response.code === "no_credit") throw new Error("No Codex resets are available");
|
|
77
78
|
}).finally(() => refresh(ctx));
|
|
78
79
|
}
|
|
79
80
|
|
|
80
|
-
function
|
|
81
|
-
const
|
|
82
|
-
Accept: "application/json",
|
|
83
|
-
Authorization: `Bearer ${apiKey}`,
|
|
84
|
-
};
|
|
85
|
-
|
|
81
|
+
function createClient(apiKey: string): KyInstance {
|
|
82
|
+
const client = withAuth(http, apiKey);
|
|
86
83
|
const credential = readStoredCredential(PROVIDER);
|
|
87
84
|
const accountId = credential?.type === "oauth" ? credential.accountId : undefined;
|
|
88
|
-
if (typeof accountId === "string" && accountId.length > 0) headers["ChatGPT-Account-ID"] = accountId;
|
|
89
|
-
|
|
90
|
-
return headers;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
async function requestJson<T>(path: string, headers: Record<string, string>, signal: AbortSignal, init: RequestInit = {}): Promise<T> {
|
|
94
|
-
const requestHeaders = new Headers(headers);
|
|
95
|
-
new Headers(init.headers).forEach((value, key) => requestHeaders.set(key, value));
|
|
96
|
-
|
|
97
|
-
const response = await fetch(`${BASE_URL}${path}`, {
|
|
98
|
-
...init,
|
|
99
|
-
headers: requestHeaders,
|
|
100
|
-
signal,
|
|
101
|
-
});
|
|
102
|
-
if (!response.ok) throw new Error(`Codex backend returned ${response.status}`);
|
|
103
85
|
|
|
104
|
-
return
|
|
86
|
+
return typeof accountId === "string" && accountId.length > 0
|
|
87
|
+
? client.extend({ headers: { "ChatGPT-Account-ID": accountId } })
|
|
88
|
+
: client;
|
|
105
89
|
}
|
|
106
90
|
|
|
107
|
-
async function fetchUsage(
|
|
108
|
-
return
|
|
91
|
+
async function fetchUsage(client: KyInstance, signal: AbortSignal): Promise<CodexUsageResponse> {
|
|
92
|
+
return client.get(`${BASE_URL}${USAGE_PATH}`, { signal }).json<CodexUsageResponse>();
|
|
109
93
|
}
|
|
110
94
|
|
|
111
|
-
async function fetchResetCredits(
|
|
112
|
-
return
|
|
95
|
+
async function fetchResetCredits(client: KyInstance, signal: AbortSignal): Promise<RateLimitResetCreditsResponse> {
|
|
96
|
+
return client.get(`${BASE_URL}${RESET_CREDITS_PATH}`, { signal }).json<RateLimitResetCreditsResponse>();
|
|
113
97
|
}
|
|
114
98
|
|
|
115
|
-
async function consumeReset(
|
|
116
|
-
return
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
{
|
|
121
|
-
method: "POST",
|
|
122
|
-
headers: { "Content-Type": "application/json" },
|
|
123
|
-
body: JSON.stringify({ redeem_request_id: redeemRequestId, credit_id: creditId }),
|
|
99
|
+
async function consumeReset(client: KyInstance, redeemRequestId: string, creditId: string): Promise<ConsumeResetResponse> {
|
|
100
|
+
return client.post(`${BASE_URL}${CONSUME_RESET_PATH}`, {
|
|
101
|
+
json: {
|
|
102
|
+
redeem_request_id: redeemRequestId,
|
|
103
|
+
credit_id: creditId
|
|
124
104
|
},
|
|
125
|
-
);
|
|
105
|
+
}).json<ConsumeResetResponse>();
|
|
126
106
|
}
|
|
127
107
|
|
|
128
108
|
function toCredits(usage: CodexUsageResponse, suffix?: string): Credits {
|
|
@@ -153,8 +133,8 @@ export const openaiCodexProvider: CreditsProvider = {
|
|
|
153
133
|
label: "Codex",
|
|
154
134
|
|
|
155
135
|
async fetch(apiKey, signal): Promise<Credits> {
|
|
156
|
-
const
|
|
157
|
-
const usage = await fetchUsage(
|
|
136
|
+
const client = createClient(apiKey);
|
|
137
|
+
const usage = await fetchUsage(client, signal);
|
|
158
138
|
const availableCount = usage.rate_limit_reset_credits?.available_count ?? 0;
|
|
159
139
|
const suffix = availableCount > 0 ? `(${formatAvailableResets(availableCount)})` : undefined;
|
|
160
140
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { toNumber } from "../../../utils/format";
|
|
2
|
+
import { http, withAuth } from "../../../utils/http";
|
|
2
3
|
|
|
3
4
|
import type { Credits, CreditsProvider } from "../types";
|
|
4
5
|
|
|
@@ -17,15 +18,7 @@ export const openrouterProvider: CreditsProvider = {
|
|
|
17
18
|
label: "OpenRouter",
|
|
18
19
|
|
|
19
20
|
async fetch(apiKey, signal): Promise<Credits> {
|
|
20
|
-
const
|
|
21
|
-
Accept: "application/json",
|
|
22
|
-
Authorization: `Bearer ${apiKey}`,
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
const response = await fetch(URL, { headers, signal });
|
|
26
|
-
if (!response.ok) throw new Error("request failed");
|
|
27
|
-
|
|
28
|
-
const payload = (await response.json()) as OpenRouterCreditsResponse;
|
|
21
|
+
const payload = await withAuth(http, apiKey).get(URL, { signal }).json<OpenRouterCreditsResponse>();
|
|
29
22
|
const totalCredits = toNumber(payload.data?.total_credits);
|
|
30
23
|
const totalUsage = toNumber(payload.data?.total_usage);
|
|
31
24
|
const remaining = typeof totalCredits === "number" && typeof totalUsage === "number" ? totalCredits - totalUsage : undefined;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { toNumber } from "../../../utils/format";
|
|
2
|
+
import { http, withAuth } from "../../../utils/http";
|
|
2
3
|
|
|
3
4
|
import type { Credits, CreditsProvider } from "../types";
|
|
4
5
|
|
|
@@ -14,15 +15,7 @@ export const vercelAiGatewayProvider: CreditsProvider = {
|
|
|
14
15
|
label: "Vercel",
|
|
15
16
|
|
|
16
17
|
async fetch(apiKey, signal): Promise<Credits> {
|
|
17
|
-
const
|
|
18
|
-
Accept: "application/json",
|
|
19
|
-
Authorization: `Bearer ${apiKey}`,
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
const response = await fetch(URL, { headers, signal });
|
|
23
|
-
if (!response.ok) throw new Error("request failed");
|
|
24
|
-
|
|
25
|
-
const payload = (await response.json()) as VercelCreditsResponse;
|
|
18
|
+
const payload = await withAuth(http, apiKey).get(URL, { signal }).json<VercelCreditsResponse>();
|
|
26
19
|
const remaining = toNumber(payload.balance);
|
|
27
20
|
|
|
28
21
|
return { type: "balance", remaining };
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import * as z from "zod";
|
|
2
2
|
|
|
3
3
|
const spinnerPresetSchema = z.enum(["dots", "lights", "tildes", "pulse"]);
|
|
4
|
+
const thinkingLevelIndicatorSchema = z.enum(["border", "model"]);
|
|
4
5
|
|
|
5
6
|
export type SpinnerPreset = z.infer<typeof spinnerPresetSchema>;
|
|
7
|
+
export type ThinkingLevelIndicator = z.infer<typeof thinkingLevelIndicatorSchema>;
|
|
6
8
|
|
|
7
9
|
export const editorConfigSchema = z.object({
|
|
8
10
|
spinner: spinnerPresetSchema.optional(),
|
|
11
|
+
thinkingLevelIndicator: thinkingLevelIndicatorSchema.optional(),
|
|
9
12
|
});
|
|
@@ -9,16 +9,18 @@ import { formatModel } from "../../utils/format";
|
|
|
9
9
|
import type { ExtensionAPI, ExtensionContext, KeybindingsManager } from "@earendil-works/pi-coding-agent";
|
|
10
10
|
import type { TUI, EditorTheme } from "@earendil-works/pi-tui";
|
|
11
11
|
import type { EventCollector } from "../../events";
|
|
12
|
+
import type { ThinkingLevelIndicator } from "./config";
|
|
12
13
|
|
|
13
14
|
class Editor extends CustomEditor {
|
|
14
15
|
private pi: ExtensionAPI;
|
|
15
16
|
private ctx: ExtensionContext;
|
|
16
17
|
|
|
17
18
|
private spinner: Spinner;
|
|
19
|
+
private thinkingLevelIndicator: ThinkingLevelIndicator;
|
|
18
20
|
private workingMessage: string | undefined;
|
|
19
21
|
private slots: { modelBefore: string | undefined };
|
|
20
22
|
|
|
21
|
-
constructor(pi: ExtensionAPI, ctx: ExtensionContext, tui: TUI, theme: EditorTheme, keybindings: KeybindingsManager, spinner: Spinner = new Spinner()) {
|
|
23
|
+
constructor(pi: ExtensionAPI, ctx: ExtensionContext, tui: TUI, theme: EditorTheme, keybindings: KeybindingsManager, spinner: Spinner = new Spinner(), thinkingLevelIndicator: ThinkingLevelIndicator = "border") {
|
|
22
24
|
super(tui, theme, keybindings);
|
|
23
25
|
|
|
24
26
|
this.pi = pi;
|
|
@@ -26,6 +28,7 @@ class Editor extends CustomEditor {
|
|
|
26
28
|
|
|
27
29
|
this.spinner = spinner;
|
|
28
30
|
this.spinner.setTUI(tui);
|
|
31
|
+
this.thinkingLevelIndicator = thinkingLevelIndicator;
|
|
29
32
|
this.workingMessage = undefined;
|
|
30
33
|
this.slots = { modelBefore: undefined };
|
|
31
34
|
}
|
|
@@ -41,6 +44,12 @@ class Editor extends CustomEditor {
|
|
|
41
44
|
}
|
|
42
45
|
|
|
43
46
|
render(width: number): string[] {
|
|
47
|
+
// Pi reapplies the thinking-level border color when the level changes. Use the dim color here
|
|
48
|
+
// while preserving Bash mode's dedicated border color.
|
|
49
|
+
if (this.thinkingLevelIndicator === "model" && !this.getText().trimStart().startsWith("!")) {
|
|
50
|
+
this.borderColor = (text) => this.ctx.ui.theme.fg("dim", text);
|
|
51
|
+
}
|
|
52
|
+
|
|
44
53
|
const lines = super.render(width);
|
|
45
54
|
if (lines.length === 0) return lines;
|
|
46
55
|
|
|
@@ -77,8 +86,16 @@ class Editor extends CustomEditor {
|
|
|
77
86
|
|
|
78
87
|
const modelBeforeText = this.slots.modelBefore;
|
|
79
88
|
const modelText = formatModel(this.ctx.model?.provider, this.ctx.model?.id, this.pi.getThinkingLevel());
|
|
89
|
+
const coloredModelText = this.thinkingLevelIndicator === "model" ? this.withThinkingLevelColor(modelText) : theme.fg("dim", modelText);
|
|
90
|
+
|
|
91
|
+
return [modelBeforeText ? theme.fg("dim", modelBeforeText) : undefined, coloredModelText].filter(Boolean).join(theme.fg("dim", " · "));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
private withThinkingLevelColor(text: string): string {
|
|
95
|
+
const theme = this.ctx.ui.theme;
|
|
96
|
+
const thinkingLevel = this.pi.getThinkingLevel();
|
|
80
97
|
|
|
81
|
-
return theme.
|
|
98
|
+
return theme.getThinkingBorderColor(thinkingLevel)(text);
|
|
82
99
|
}
|
|
83
100
|
}
|
|
84
101
|
|
|
@@ -89,13 +106,13 @@ export function registerEditor(pi: ExtensionAPI, events: EventCollector): void {
|
|
|
89
106
|
|
|
90
107
|
pi.on("session_start", (_event, ctx) => {
|
|
91
108
|
const config = loadConfig(ctx).editor;
|
|
92
|
-
if (
|
|
109
|
+
if (ctx.mode !== "tui" || !config) return;
|
|
93
110
|
|
|
94
111
|
spinner = new Spinner(config.spinner);
|
|
95
112
|
|
|
96
113
|
ctx.ui.setWorkingVisible(false);
|
|
97
114
|
ctx.ui.setEditorComponent((tui, theme, keybindings) => {
|
|
98
|
-
editor = new Editor(pi, ctx, tui, theme, keybindings, spinner);
|
|
115
|
+
editor = new Editor(pi, ctx, tui, theme, keybindings, spinner, config.thinkingLevelIndicator);
|
|
99
116
|
|
|
100
117
|
events.on(PRESET_CHANGE, (data) => {
|
|
101
118
|
const payload = parsePresetChange(data);
|
|
@@ -75,7 +75,7 @@ class FooterComponent implements Component {
|
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
private getStyledCostText(): string {
|
|
78
|
-
const totalCost = this.ctx.sessionManager.
|
|
78
|
+
const totalCost = this.ctx.sessionManager.getEntries().reduce((acc, entry) => acc + (getEntryUsage(entry)?.cost.total ?? 0), 0);
|
|
79
79
|
|
|
80
80
|
// Hide cost below half a cent, since it would render as $0.00.
|
|
81
81
|
if (totalCost < 0.005) return "";
|
|
@@ -100,7 +100,7 @@ class FooterComponent implements Component {
|
|
|
100
100
|
export function registerFooter(pi: ExtensionAPI): void {
|
|
101
101
|
pi.on("session_start", (_event, ctx) => {
|
|
102
102
|
const config = loadConfig(ctx).footer;
|
|
103
|
-
if (
|
|
103
|
+
if (ctx.mode !== "tui" || !config) return;
|
|
104
104
|
|
|
105
105
|
ctx.ui.setFooter((_tui, theme, footerData) => new FooterComponent(ctx, theme, footerData, config.statusPosition));
|
|
106
106
|
});
|