@aliou/pi-synthetic 0.8.4 → 0.9.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/README.md +16 -7
- package/package.json +5 -3
- package/src/extensions/command-quotas/command.ts +65 -0
- package/src/extensions/command-quotas/components/quotas-display.ts +314 -0
- package/src/extensions/command-quotas/index.ts +8 -0
- package/src/{hooks → extensions/command-quotas}/sub-integration.ts +45 -23
- package/src/{providers → extensions/provider}/index.ts +4 -0
- package/src/{providers → extensions/provider}/models.test.ts +1 -1
- package/src/{providers → extensions/provider}/models.ts +1 -1
- package/src/extensions/web-search/index.ts +6 -0
- package/src/{tools/search.ts → extensions/web-search/tool.ts} +6 -3
- package/src/lib/env.ts +18 -0
- package/src/utils/quotas.ts +7 -6
- package/src/commands/quotas.ts +0 -68
- package/src/components/quotas-display.ts +0 -240
- package/src/components/quotas-error.ts +0 -32
- package/src/components/quotas-loading.ts +0 -30
- package/src/components/tabbed-panel.ts +0 -161
- package/src/hooks/search-tool-availability.ts +0 -104
- package/src/index.ts +0 -17
package/README.md
CHANGED
|
@@ -8,18 +8,27 @@ A Pi extension that adds [Synthetic](https://synthetic.new) as a model provider,
|
|
|
8
8
|
|
|
9
9
|
Sign up at [synthetic.new](https://synthetic.new/?referral=NDWw1u3UDWiFyDR) to get an API key (referral link).
|
|
10
10
|
|
|
11
|
-
###
|
|
11
|
+
### Configure Credentials
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
The extension uses Pi's credential storage. Add your API key to `~/.pi/agent/auth.json` (recommended):
|
|
14
|
+
|
|
15
|
+
```json
|
|
16
|
+
{
|
|
17
|
+
"synthetic": { "type": "api_key", "key": "your-api-key-here" }
|
|
18
|
+
}
|
|
15
19
|
```
|
|
16
20
|
|
|
17
|
-
|
|
21
|
+
Or set environment variable:
|
|
18
22
|
|
|
19
23
|
```bash
|
|
20
|
-
|
|
24
|
+
export SYNTHETIC_API_KEY="your-api-key-here"
|
|
21
25
|
```
|
|
22
26
|
|
|
27
|
+
Credentials are resolved in this order:
|
|
28
|
+
1. CLI `--api-key` flag
|
|
29
|
+
2. `auth.json` entry for `synthetic`
|
|
30
|
+
3. Environment variable `SYNTHETIC_API_KEY`
|
|
31
|
+
|
|
23
32
|
### Install Extension
|
|
24
33
|
|
|
25
34
|
```bash
|
|
@@ -43,7 +52,7 @@ Once installed, select `synthetic` as your provider and choose from available mo
|
|
|
43
52
|
|
|
44
53
|
### Web Search Tool
|
|
45
54
|
|
|
46
|
-
The extension registers `synthetic_web_search` — a zero-data-retention web search tool.
|
|
55
|
+
The extension registers `synthetic_web_search` — a zero-data-retention web search tool. The tool is always visible; it fails with a clear message if credentials are missing or the account lacks a subscription.
|
|
47
56
|
|
|
48
57
|
### Reasoning Levels
|
|
49
58
|
|
|
@@ -124,7 +133,7 @@ This repository uses [Changesets](https://github.com/changesets/changesets) for
|
|
|
124
133
|
## Requirements
|
|
125
134
|
|
|
126
135
|
- Pi coding agent v0.50.0+
|
|
127
|
-
-
|
|
136
|
+
- Synthetic API key (configured in `~/.pi/agent/auth.json` or via `SYNTHETIC_API_KEY`)
|
|
128
137
|
|
|
129
138
|
## Links
|
|
130
139
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aliou/pi-synthetic",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"private": false,
|
|
@@ -15,7 +15,9 @@
|
|
|
15
15
|
},
|
|
16
16
|
"pi": {
|
|
17
17
|
"extensions": [
|
|
18
|
-
"./src/index.ts"
|
|
18
|
+
"./src/extensions/provider/index.ts",
|
|
19
|
+
"./src/extensions/web-search/index.ts",
|
|
20
|
+
"./src/extensions/command-quotas/index.ts"
|
|
19
21
|
],
|
|
20
22
|
"video": "https://assets.aliou.me/pi-extensions/demos/pi-synthetic.mp4"
|
|
21
23
|
},
|
|
@@ -34,7 +36,7 @@
|
|
|
34
36
|
"@aliou/pi-utils-ui": "^0.1.2"
|
|
35
37
|
},
|
|
36
38
|
"devDependencies": {
|
|
37
|
-
"@aliou/biome-plugins": "^0.
|
|
39
|
+
"@aliou/biome-plugins": "^0.7.0",
|
|
38
40
|
"@biomejs/biome": "^2.4.2",
|
|
39
41
|
"@changesets/cli": "^2.27.11",
|
|
40
42
|
"@mariozechner/pi-coding-agent": "0.61.0",
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import { getSyntheticApiKey } from "../../lib/env";
|
|
3
|
+
import { fetchQuotas } from "../../utils/quotas";
|
|
4
|
+
import { QuotasComponent } from "./components/quotas-display";
|
|
5
|
+
|
|
6
|
+
const MISSING_AUTH_MESSAGE =
|
|
7
|
+
"Synthetic quotas requires a Synthetic subscription. Add credentials to ~/.pi/agent/auth.json or set SYNTHETIC_API_KEY environment variable.";
|
|
8
|
+
|
|
9
|
+
export function registerQuotasCommand(pi: ExtensionAPI): void {
|
|
10
|
+
pi.registerCommand("synthetic:quotas", {
|
|
11
|
+
description: "Display Synthetic API usage quotas",
|
|
12
|
+
handler: async (_args, ctx) => {
|
|
13
|
+
const apiKey = await getSyntheticApiKey(ctx.modelRegistry.authStorage);
|
|
14
|
+
if (!apiKey) {
|
|
15
|
+
ctx.ui.notify(MISSING_AUTH_MESSAGE, "warning");
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const result = await ctx.ui.custom<null>((tui, theme, _kb, done) => {
|
|
20
|
+
const component = new QuotasComponent(theme, () => done(null));
|
|
21
|
+
|
|
22
|
+
fetchQuotas(apiKey)
|
|
23
|
+
.then((quotas) => {
|
|
24
|
+
if (!quotas) {
|
|
25
|
+
component.setState({
|
|
26
|
+
type: "error",
|
|
27
|
+
message:
|
|
28
|
+
"Failed to fetch quotas. Check your Synthetic subscription status.",
|
|
29
|
+
});
|
|
30
|
+
} else {
|
|
31
|
+
component.setState({ type: "loaded", quotas });
|
|
32
|
+
}
|
|
33
|
+
tui.requestRender();
|
|
34
|
+
})
|
|
35
|
+
.catch(() => {
|
|
36
|
+
component.setState({
|
|
37
|
+
type: "error",
|
|
38
|
+
message:
|
|
39
|
+
"Failed to fetch quotas. Check your Synthetic subscription status.",
|
|
40
|
+
});
|
|
41
|
+
tui.requestRender();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
render: (width: number) => component.render(width),
|
|
46
|
+
invalidate: () => component.invalidate(),
|
|
47
|
+
handleInput: (data: string) => component.handleInput(data),
|
|
48
|
+
};
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// RPC fallback: return JSON
|
|
52
|
+
if (result === undefined) {
|
|
53
|
+
const quotas = await fetchQuotas(apiKey);
|
|
54
|
+
if (!quotas) {
|
|
55
|
+
ctx.ui.notify(
|
|
56
|
+
JSON.stringify({ error: "Failed to fetch quotas" }),
|
|
57
|
+
"error",
|
|
58
|
+
);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
ctx.ui.notify(JSON.stringify(quotas, null, 2), "info");
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
}
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
import type { Theme } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import { DynamicBorder } from "@mariozechner/pi-coding-agent";
|
|
3
|
+
import type { Component } from "@mariozechner/pi-tui";
|
|
4
|
+
import {
|
|
5
|
+
matchesKey,
|
|
6
|
+
truncateToWidth,
|
|
7
|
+
visibleWidth,
|
|
8
|
+
} from "@mariozechner/pi-tui";
|
|
9
|
+
import type { QuotasResponse } from "../../../types/quotas";
|
|
10
|
+
|
|
11
|
+
type QuotasState =
|
|
12
|
+
| { type: "loading" }
|
|
13
|
+
| { type: "error"; message: string }
|
|
14
|
+
| { type: "loaded"; quotas: QuotasResponse };
|
|
15
|
+
|
|
16
|
+
interface QuotaWindow {
|
|
17
|
+
label: string;
|
|
18
|
+
usedPercent: number;
|
|
19
|
+
resetsAt: Date;
|
|
20
|
+
windowSeconds: number;
|
|
21
|
+
usedValue: number;
|
|
22
|
+
limitValue: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function toWindows(quotas: QuotasResponse): QuotaWindow[] {
|
|
26
|
+
const windows: QuotaWindow[] = [];
|
|
27
|
+
|
|
28
|
+
if (quotas.subscription.limit > 0) {
|
|
29
|
+
windows.push({
|
|
30
|
+
label: "Completions",
|
|
31
|
+
usedPercent:
|
|
32
|
+
(quotas.subscription.requests / quotas.subscription.limit) * 100,
|
|
33
|
+
resetsAt: new Date(quotas.subscription.renewsAt),
|
|
34
|
+
windowSeconds: 5 * 60 * 60,
|
|
35
|
+
usedValue: quotas.subscription.requests,
|
|
36
|
+
limitValue: quotas.subscription.limit,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (quotas.search.hourly.limit > 0) {
|
|
41
|
+
windows.push({
|
|
42
|
+
label: "Search",
|
|
43
|
+
usedPercent:
|
|
44
|
+
(quotas.search.hourly.requests / quotas.search.hourly.limit) * 100,
|
|
45
|
+
resetsAt: new Date(quotas.search.hourly.renewsAt),
|
|
46
|
+
windowSeconds: 60 * 60,
|
|
47
|
+
usedValue: quotas.search.hourly.requests,
|
|
48
|
+
limitValue: quotas.search.hourly.limit,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (quotas.freeToolCalls.limit > 0) {
|
|
53
|
+
windows.push({
|
|
54
|
+
label: "Free Tool Calls",
|
|
55
|
+
usedPercent:
|
|
56
|
+
(quotas.freeToolCalls.requests / quotas.freeToolCalls.limit) * 100,
|
|
57
|
+
resetsAt: new Date(quotas.freeToolCalls.renewsAt),
|
|
58
|
+
windowSeconds: 24 * 60 * 60,
|
|
59
|
+
usedValue: quotas.freeToolCalls.requests,
|
|
60
|
+
limitValue: quotas.freeToolCalls.limit,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return windows;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function getPacePercent(window: QuotaWindow): number | null {
|
|
68
|
+
const totalMs = window.windowSeconds * 1000;
|
|
69
|
+
if (totalMs <= 0) return null;
|
|
70
|
+
const remainingMs = window.resetsAt.getTime() - Date.now();
|
|
71
|
+
const elapsedMs = totalMs - remainingMs;
|
|
72
|
+
return Math.max(0, Math.min(100, (elapsedMs / totalMs) * 100));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function getProjectedPercent(
|
|
76
|
+
usedPercent: number,
|
|
77
|
+
pacePercent: number | null,
|
|
78
|
+
): number {
|
|
79
|
+
if (pacePercent === null) return usedPercent;
|
|
80
|
+
const effectivePace = Math.max(5, pacePercent);
|
|
81
|
+
return Math.max(0, (usedPercent / effectivePace) * 100);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function getSeverity(
|
|
85
|
+
projectedPercent: number,
|
|
86
|
+
pacePercent: number | null,
|
|
87
|
+
): "success" | "warning" | "error" {
|
|
88
|
+
if (pacePercent === null) {
|
|
89
|
+
if (projectedPercent >= 100) return "error";
|
|
90
|
+
if (projectedPercent >= 90) return "warning";
|
|
91
|
+
return "success";
|
|
92
|
+
}
|
|
93
|
+
// Dynamic thresholds based on window progress
|
|
94
|
+
const progress = pacePercent / 100;
|
|
95
|
+
const warnThreshold = 260 - (260 - 120) * progress;
|
|
96
|
+
const highThreshold = 320 - (320 - 145) * progress;
|
|
97
|
+
const criticalThreshold = 400 - (400 - 170) * progress;
|
|
98
|
+
|
|
99
|
+
if (projectedPercent >= criticalThreshold) return "error";
|
|
100
|
+
if (projectedPercent >= highThreshold) return "error";
|
|
101
|
+
if (projectedPercent >= warnThreshold) return "warning";
|
|
102
|
+
return "success";
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function formatResetDateTime(date: Date): string {
|
|
106
|
+
const now = new Date();
|
|
107
|
+
const isToday =
|
|
108
|
+
date.getDate() === now.getDate() &&
|
|
109
|
+
date.getMonth() === now.getMonth() &&
|
|
110
|
+
date.getFullYear() === now.getFullYear();
|
|
111
|
+
|
|
112
|
+
const timeStr = date.toLocaleTimeString("en-US", {
|
|
113
|
+
hour: "numeric",
|
|
114
|
+
minute: "2-digit",
|
|
115
|
+
hour12: true,
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
if (isToday) {
|
|
119
|
+
return `today ${timeStr}`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const dateStr = date.toLocaleDateString("en-US", {
|
|
123
|
+
month: "short",
|
|
124
|
+
day: "numeric",
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
return `${dateStr} ${timeStr}`;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function renderProgressBar(
|
|
131
|
+
percent: number,
|
|
132
|
+
width: number,
|
|
133
|
+
theme: Theme,
|
|
134
|
+
fillColor: "success" | "warning" | "error",
|
|
135
|
+
pacePercent?: number | null,
|
|
136
|
+
): string {
|
|
137
|
+
const clamped = Math.max(0, Math.min(100, Math.round(percent)));
|
|
138
|
+
const filled = Math.round((clamped / 100) * width);
|
|
139
|
+
const paceIndex =
|
|
140
|
+
pacePercent === null || pacePercent === undefined || pacePercent <= percent
|
|
141
|
+
? null
|
|
142
|
+
: Math.round((Math.max(0, Math.min(100, pacePercent)) / 100) * width);
|
|
143
|
+
|
|
144
|
+
const parts: string[] = [];
|
|
145
|
+
for (let idx = 0; idx < width; idx++) {
|
|
146
|
+
if (idx < filled) {
|
|
147
|
+
parts.push(theme.fg(fillColor, "█"));
|
|
148
|
+
} else if (paceIndex !== null && idx < paceIndex) {
|
|
149
|
+
parts.push(theme.fg(fillColor, "▓"));
|
|
150
|
+
} else {
|
|
151
|
+
parts.push(theme.fg("dim", "░"));
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return parts.join("");
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export class QuotasComponent implements Component {
|
|
159
|
+
private state: QuotasState = { type: "loading" };
|
|
160
|
+
private theme: Theme;
|
|
161
|
+
private onClose: () => void;
|
|
162
|
+
|
|
163
|
+
constructor(theme: Theme, onClose: () => void) {
|
|
164
|
+
this.theme = theme;
|
|
165
|
+
this.onClose = onClose;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
setState(state: QuotasState): void {
|
|
169
|
+
this.state = state;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
handleInput(data: string): boolean {
|
|
173
|
+
if (matchesKey(data, "escape") || data === "q") {
|
|
174
|
+
this.onClose();
|
|
175
|
+
return true;
|
|
176
|
+
}
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
render(width: number): string[] {
|
|
181
|
+
const lines: string[] = [];
|
|
182
|
+
const border = new DynamicBorder((s: string) => this.theme.fg("border", s));
|
|
183
|
+
const contentWidth = Math.max(1, width - 4);
|
|
184
|
+
|
|
185
|
+
lines.push(...border.render(width));
|
|
186
|
+
lines.push(
|
|
187
|
+
truncateToWidth(
|
|
188
|
+
` ${this.theme.fg("accent", this.theme.bold("Synthetic API Quotas"))}`,
|
|
189
|
+
width,
|
|
190
|
+
),
|
|
191
|
+
);
|
|
192
|
+
lines.push("");
|
|
193
|
+
|
|
194
|
+
switch (this.state.type) {
|
|
195
|
+
case "loading":
|
|
196
|
+
lines.push(this.theme.fg("muted", " Loading..."));
|
|
197
|
+
break;
|
|
198
|
+
case "error":
|
|
199
|
+
lines.push(this.theme.fg("error", ` ${this.state.message}`));
|
|
200
|
+
break;
|
|
201
|
+
case "loaded":
|
|
202
|
+
lines.push(
|
|
203
|
+
...this.renderLoaded(this.state.quotas, contentWidth, width),
|
|
204
|
+
);
|
|
205
|
+
break;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
lines.push("");
|
|
209
|
+
lines.push(this.theme.fg("dim", " q/Esc to close"));
|
|
210
|
+
lines.push(...border.render(width));
|
|
211
|
+
|
|
212
|
+
return lines;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
private renderLoaded(
|
|
216
|
+
quotas: QuotasResponse,
|
|
217
|
+
contentWidth: number,
|
|
218
|
+
maxWidth: number,
|
|
219
|
+
): string[] {
|
|
220
|
+
const lines: string[] = [];
|
|
221
|
+
const windows = toWindows(quotas);
|
|
222
|
+
const barWidth = Math.min(50, Math.max(20, contentWidth - 20));
|
|
223
|
+
|
|
224
|
+
for (const window of windows) {
|
|
225
|
+
lines.push(...this.renderWindow(window, barWidth, maxWidth));
|
|
226
|
+
lines.push("");
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Remove trailing empty line
|
|
230
|
+
if (lines.length > 0 && lines[lines.length - 1] === "") {
|
|
231
|
+
lines.pop();
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return lines;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
private renderWindow(
|
|
238
|
+
window: QuotaWindow,
|
|
239
|
+
barWidth: number,
|
|
240
|
+
maxWidth: number,
|
|
241
|
+
): string[] {
|
|
242
|
+
const lines: string[] = [];
|
|
243
|
+
const theme = this.theme;
|
|
244
|
+
|
|
245
|
+
const pacePercent = getPacePercent(window);
|
|
246
|
+
const projectedPercent = getProjectedPercent(
|
|
247
|
+
window.usedPercent,
|
|
248
|
+
pacePercent,
|
|
249
|
+
);
|
|
250
|
+
const severity = getSeverity(projectedPercent, pacePercent);
|
|
251
|
+
|
|
252
|
+
// Label
|
|
253
|
+
lines.push(
|
|
254
|
+
truncateToWidth(` ${theme.fg("accent", window.label)}`, maxWidth),
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
// Progress bar + usage
|
|
258
|
+
const bar = renderProgressBar(
|
|
259
|
+
window.usedPercent,
|
|
260
|
+
barWidth,
|
|
261
|
+
theme,
|
|
262
|
+
severity,
|
|
263
|
+
pacePercent,
|
|
264
|
+
);
|
|
265
|
+
const usedStr = `${window.usedValue.toLocaleString()}/${window.limitValue.toLocaleString()} (${Math.round(window.usedPercent)}%)`;
|
|
266
|
+
lines.push(
|
|
267
|
+
truncateToWidth(` ${bar} ${theme.fg(severity, usedStr)}`, maxWidth),
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
// Metadata: estimated + pace left, reset time right
|
|
271
|
+
const leftParts: string[] = [];
|
|
272
|
+
if (projectedPercent > 0) {
|
|
273
|
+
const estStr = `est ${Math.round(projectedPercent)}%`;
|
|
274
|
+
leftParts.push(
|
|
275
|
+
severity !== "success"
|
|
276
|
+
? theme.fg(severity, estStr)
|
|
277
|
+
: theme.fg("dim", estStr),
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (pacePercent !== null) {
|
|
282
|
+
const paceDiff = window.usedPercent - pacePercent;
|
|
283
|
+
if (Math.abs(paceDiff) > 5) {
|
|
284
|
+
if (paceDiff > 0) {
|
|
285
|
+
leftParts.push(
|
|
286
|
+
theme.fg("warning", `${Math.round(Math.abs(paceDiff))}% ahead`),
|
|
287
|
+
);
|
|
288
|
+
} else {
|
|
289
|
+
leftParts.push(
|
|
290
|
+
theme.fg("success", `${Math.round(Math.abs(paceDiff))}% behind`),
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const leftStr = leftParts.join(" ");
|
|
297
|
+
const resetStr = formatResetDateTime(window.resetsAt);
|
|
298
|
+
const rightStr = theme.fg("dim", resetStr);
|
|
299
|
+
|
|
300
|
+
const leftW = visibleWidth(leftStr);
|
|
301
|
+
const rightW = visibleWidth(rightStr);
|
|
302
|
+
const gap = Math.max(2, barWidth - leftW - rightW);
|
|
303
|
+
|
|
304
|
+
lines.push(
|
|
305
|
+
truncateToWidth(` ${leftStr}${" ".repeat(gap)}${rightStr}`, maxWidth),
|
|
306
|
+
);
|
|
307
|
+
|
|
308
|
+
return lines;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
invalidate(): void {
|
|
312
|
+
// No internal cached state to invalidate
|
|
313
|
+
}
|
|
314
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import { registerQuotasCommand } from "./command";
|
|
3
|
+
import { registerSubIntegration } from "./sub-integration";
|
|
4
|
+
|
|
5
|
+
export default async function (pi: ExtensionAPI) {
|
|
6
|
+
registerQuotasCommand(pi);
|
|
7
|
+
registerSubIntegration(pi);
|
|
8
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
1
|
+
import type { AuthStorage, ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import { getSyntheticApiKey } from "../../lib/env";
|
|
3
|
+
import type { QuotasResponse } from "../../types/quotas";
|
|
4
|
+
import { fetchQuotas, formatResetTime } from "../../utils/quotas";
|
|
4
5
|
|
|
5
6
|
interface RateWindow {
|
|
6
7
|
label: string;
|
|
@@ -53,7 +54,7 @@ function toUsageSnapshot(quotas: QuotasResponse): UsageSnapshot {
|
|
|
53
54
|
const pct =
|
|
54
55
|
(quotas.freeToolCalls.requests / quotas.freeToolCalls.limit) * 100;
|
|
55
56
|
windows.push({
|
|
56
|
-
label: "
|
|
57
|
+
label: "Tools",
|
|
57
58
|
usedPercent: Math.round(pct),
|
|
58
59
|
resetDescription: formatResetTime(quotas.freeToolCalls.renewsAt),
|
|
59
60
|
resetAt: quotas.freeToolCalls.renewsAt,
|
|
@@ -68,8 +69,13 @@ function toUsageSnapshot(quotas: QuotasResponse): UsageSnapshot {
|
|
|
68
69
|
};
|
|
69
70
|
}
|
|
70
71
|
|
|
71
|
-
async function emitCurrentUsage(
|
|
72
|
-
|
|
72
|
+
async function emitCurrentUsage(
|
|
73
|
+
pi: ExtensionAPI,
|
|
74
|
+
authStorage: AuthStorage,
|
|
75
|
+
): Promise<void> {
|
|
76
|
+
const apiKey = await getSyntheticApiKey(authStorage);
|
|
77
|
+
if (!apiKey) return;
|
|
78
|
+
const quotas = await fetchQuotas(apiKey);
|
|
73
79
|
if (!quotas) return;
|
|
74
80
|
pi.events.emit("sub-core:update-current", {
|
|
75
81
|
state: { provider: "synthetic", usage: toUsageSnapshot(quotas) },
|
|
@@ -77,12 +83,11 @@ async function emitCurrentUsage(pi: ExtensionAPI): Promise<void> {
|
|
|
77
83
|
}
|
|
78
84
|
|
|
79
85
|
export function registerSubIntegration(pi: ExtensionAPI): void {
|
|
80
|
-
if (!process.env.SYNTHETIC_API_KEY) return;
|
|
81
|
-
|
|
82
86
|
let interval: NodeJS.Timeout | undefined;
|
|
83
87
|
let refreshMs = 60000;
|
|
84
88
|
let subCoreReady = false;
|
|
85
89
|
let currentProvider: string | undefined;
|
|
90
|
+
let currentAuthStorage: AuthStorage | undefined;
|
|
86
91
|
|
|
87
92
|
function isSynthetic(): boolean {
|
|
88
93
|
return currentProvider === "synthetic";
|
|
@@ -95,15 +100,15 @@ export function registerSubIntegration(pi: ExtensionAPI): void {
|
|
|
95
100
|
}
|
|
96
101
|
}
|
|
97
102
|
|
|
98
|
-
function
|
|
103
|
+
function startPolling(authStorage: AuthStorage): void {
|
|
99
104
|
stop();
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
}
|
|
103
|
-
emitCurrentUsage(pi);
|
|
105
|
+
currentAuthStorage = authStorage;
|
|
106
|
+
void emitCurrentUsage(pi, authStorage);
|
|
104
107
|
const ms = Math.max(10000, refreshMs);
|
|
105
108
|
interval = setInterval(() => {
|
|
106
|
-
if (isSynthetic())
|
|
109
|
+
if (isSynthetic() && currentAuthStorage) {
|
|
110
|
+
void emitCurrentUsage(pi, currentAuthStorage);
|
|
111
|
+
}
|
|
107
112
|
}, ms);
|
|
108
113
|
interval.unref?.();
|
|
109
114
|
}
|
|
@@ -111,28 +116,44 @@ export function registerSubIntegration(pi: ExtensionAPI): void {
|
|
|
111
116
|
// Custom events (inter-extension bus)
|
|
112
117
|
pi.events.on("sub-core:ready", () => {
|
|
113
118
|
subCoreReady = true;
|
|
114
|
-
|
|
119
|
+
// Polling starts in session_start/model_select when provider is synthetic
|
|
115
120
|
});
|
|
116
121
|
|
|
117
122
|
pi.events.on("sub-core:settings:updated", (data: unknown) => {
|
|
118
123
|
const payload = data as SubCoreSettingsPayload;
|
|
119
124
|
if (payload.settings?.behavior?.refreshInterval) {
|
|
120
125
|
refreshMs = payload.settings.behavior.refreshInterval * 1000;
|
|
121
|
-
|
|
126
|
+
// Restart with new interval if currently running
|
|
127
|
+
if (interval && isSynthetic() && currentAuthStorage) {
|
|
128
|
+
startPolling(currentAuthStorage);
|
|
129
|
+
}
|
|
122
130
|
}
|
|
123
131
|
});
|
|
124
132
|
|
|
125
|
-
// Lifecycle events
|
|
126
|
-
pi.on("session_start", (_event, ctx) => {
|
|
133
|
+
// Lifecycle events
|
|
134
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
127
135
|
currentProvider = ctx.model?.provider;
|
|
128
|
-
|
|
136
|
+
currentAuthStorage = ctx.modelRegistry.authStorage;
|
|
137
|
+
|
|
138
|
+
if (subCoreReady && isSynthetic()) {
|
|
139
|
+
const apiKey = await getSyntheticApiKey(currentAuthStorage);
|
|
140
|
+
if (apiKey) {
|
|
141
|
+
startPolling(currentAuthStorage);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
129
144
|
});
|
|
130
145
|
|
|
131
|
-
pi.on("model_select", (event,
|
|
146
|
+
pi.on("model_select", async (event, ctx) => {
|
|
132
147
|
currentProvider = event.model?.provider;
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
148
|
+
currentAuthStorage = ctx.modelRegistry.authStorage;
|
|
149
|
+
|
|
150
|
+
if (subCoreReady && isSynthetic()) {
|
|
151
|
+
const apiKey = await getSyntheticApiKey(currentAuthStorage);
|
|
152
|
+
if (apiKey) {
|
|
153
|
+
startPolling(currentAuthStorage);
|
|
154
|
+
} else {
|
|
155
|
+
stop();
|
|
156
|
+
}
|
|
136
157
|
} else {
|
|
137
158
|
stop();
|
|
138
159
|
}
|
|
@@ -140,6 +161,7 @@ export function registerSubIntegration(pi: ExtensionAPI): void {
|
|
|
140
161
|
|
|
141
162
|
pi.on("session_shutdown", () => {
|
|
142
163
|
currentProvider = undefined;
|
|
164
|
+
currentAuthStorage = undefined;
|
|
143
165
|
stop();
|
|
144
166
|
});
|
|
145
167
|
}
|
|
@@ -9,6 +9,7 @@ import type {
|
|
|
9
9
|
import { getMarkdownTheme, keyHint } from "@mariozechner/pi-coding-agent";
|
|
10
10
|
import { Container, Markdown, Text } from "@mariozechner/pi-tui";
|
|
11
11
|
import { type Static, Type } from "@sinclair/typebox";
|
|
12
|
+
import { getSyntheticApiKey } from "../../lib/env";
|
|
12
13
|
|
|
13
14
|
export const SYNTHETIC_WEB_SEARCH_TOOL = "synthetic_web_search" as const;
|
|
14
15
|
|
|
@@ -51,16 +52,18 @@ export function registerSyntheticWebSearchTool(pi: ExtensionAPI): void {
|
|
|
51
52
|
onUpdate:
|
|
52
53
|
| ((result: AgentToolResult<WebSearchDetails>) => void)
|
|
53
54
|
| undefined,
|
|
54
|
-
|
|
55
|
+
ctx: ExtensionContext,
|
|
55
56
|
): Promise<AgentToolResult<WebSearchDetails>> {
|
|
56
57
|
onUpdate?.({
|
|
57
58
|
content: [{ type: "text", text: "Searching..." }],
|
|
58
59
|
details: { query: params.query },
|
|
59
60
|
});
|
|
60
61
|
|
|
61
|
-
const apiKey =
|
|
62
|
+
const apiKey = await getSyntheticApiKey(ctx.modelRegistry.authStorage);
|
|
62
63
|
if (!apiKey) {
|
|
63
|
-
throw new Error(
|
|
64
|
+
throw new Error(
|
|
65
|
+
"Synthetic web search requires a Synthetic subscription. Add credentials to ~/.pi/agent/auth.json or set SYNTHETIC_API_KEY environment variable.",
|
|
66
|
+
);
|
|
64
67
|
}
|
|
65
68
|
|
|
66
69
|
const response = await fetch("https://api.synthetic.new/v2/search", {
|
package/src/lib/env.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { AuthStorage } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
|
|
3
|
+
const PROVIDER_ID = "synthetic";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Get the Synthetic API key through Pi's auth handling.
|
|
7
|
+
*
|
|
8
|
+
* Resolution order:
|
|
9
|
+
* 1. Runtime override (CLI --api-key)
|
|
10
|
+
* 2. auth.json entry for "synthetic"
|
|
11
|
+
* 3. Environment variable SYNTHETIC_API_KEY
|
|
12
|
+
*/
|
|
13
|
+
export async function getSyntheticApiKey(
|
|
14
|
+
authStorage: AuthStorage,
|
|
15
|
+
): Promise<string | undefined> {
|
|
16
|
+
const key = await authStorage.getApiKey(PROVIDER_ID);
|
|
17
|
+
return key ?? process.env.SYNTHETIC_API_KEY;
|
|
18
|
+
}
|