@aliou/pi-synthetic 0.12.0 → 0.13.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 +22 -0
- package/package.json +5 -2
- package/src/config.ts +277 -0
- package/src/extensions/command-quotas/command.ts +9 -0
- package/src/extensions/command-quotas/components/quotas-display.ts +2 -1
- package/src/extensions/command-quotas/index.ts +18 -3
- package/src/extensions/provider/index.ts +115 -8
- package/src/extensions/provider/models.test.ts +14 -0
- package/src/extensions/provider/models.ts +24 -23
- package/src/extensions/quota-warnings/index.ts +41 -5
- package/src/extensions/{command-quotas/sub-integration.ts → sub-bar-integration/index.ts} +34 -9
- package/src/extensions/usage-status/index.ts +245 -0
- package/src/extensions/web-search/index.ts +45 -1
- package/src/extensions/web-search/tool.ts +7 -0
package/README.md
CHANGED
|
@@ -73,6 +73,28 @@ Check your API usage:
|
|
|
73
73
|
/synthetic:quotas
|
|
74
74
|
```
|
|
75
75
|
|
|
76
|
+
### Usage Status
|
|
77
|
+
|
|
78
|
+
When a Synthetic model is active, the footer status bar shows live quota usage (e.g. `week:82% (↺in 3d) 5h:95%`). Colors follow the same severity assessment as quota warnings: green by default, yellow/red only when projected usage is at risk. The status auto-refreshes every 60 seconds and after each turn.
|
|
79
|
+
|
|
80
|
+
### Quota Warnings
|
|
81
|
+
|
|
82
|
+
The extension automatically notifies you when you approach or exceed your Synthetic API quotas. Notifications fire on severity transitions only (no repeated alerts for the same level) and use correct terminology (regen/tick/resets) with precise time formatting.
|
|
83
|
+
|
|
84
|
+
- Escalation always notifies
|
|
85
|
+
- `high` and `critical` levels have no cooldown
|
|
86
|
+
- `warning` level has a 60-minute cooldown
|
|
87
|
+
|
|
88
|
+
## Disabling Features
|
|
89
|
+
|
|
90
|
+
Each feature (provider, web search, quotas command, usage status, quota warnings) is a separate Pi extension. You can disable individual features using `pi config`:
|
|
91
|
+
|
|
92
|
+
```
|
|
93
|
+
pi config extensions.disabled add @aliou/pi-synthetic/quota-warnings
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
This prevents the quota-warnings extension from loading while keeping the rest of pi-synthetic active. Replace `quota-warnings` with `web-search`, `command-quotas`, or `provider` to disable other features.
|
|
97
|
+
|
|
76
98
|
## Adding or Updating Models
|
|
77
99
|
|
|
78
100
|
Models are hardcoded in `src/providers/models.ts`. To add or update models:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aliou/pi-synthetic",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.1",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"private": false,
|
|
@@ -18,7 +18,9 @@
|
|
|
18
18
|
"./src/extensions/provider/index.ts",
|
|
19
19
|
"./src/extensions/web-search/index.ts",
|
|
20
20
|
"./src/extensions/command-quotas/index.ts",
|
|
21
|
-
"./src/extensions/
|
|
21
|
+
"./src/extensions/sub-bar-integration/index.ts",
|
|
22
|
+
"./src/extensions/quota-warnings/index.ts",
|
|
23
|
+
"./src/extensions/usage-status/index.ts"
|
|
22
24
|
],
|
|
23
25
|
"video": "https://assets.aliou.me/pi-extensions/demos/pi-synthetic.mp4"
|
|
24
26
|
},
|
|
@@ -34,6 +36,7 @@
|
|
|
34
36
|
"@mariozechner/pi-tui": "0.61.0"
|
|
35
37
|
},
|
|
36
38
|
"dependencies": {
|
|
39
|
+
"@aliou/pi-utils-settings": "^0.13.0",
|
|
37
40
|
"@aliou/pi-utils-ui": "^0.1.2"
|
|
38
41
|
},
|
|
39
42
|
"devDependencies": {
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ConfigLoader,
|
|
3
|
+
type Migration,
|
|
4
|
+
registerSettingsCommand,
|
|
5
|
+
type SettingsSection,
|
|
6
|
+
} from "@aliou/pi-utils-settings";
|
|
7
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
8
|
+
import type { SettingItem } from "@mariozechner/pi-tui";
|
|
9
|
+
import pkg from "../package.json" with { type: "json" };
|
|
10
|
+
|
|
11
|
+
export type SyntheticFeatureId =
|
|
12
|
+
| "webSearch"
|
|
13
|
+
| "quotasCommand"
|
|
14
|
+
| "subBarIntegration"
|
|
15
|
+
| "usageStatus"
|
|
16
|
+
| "quotaWarnings";
|
|
17
|
+
|
|
18
|
+
export const SYNTHETIC_EXTENSIONS_REQUEST_EVENT =
|
|
19
|
+
"synthetic:extensions:request" as const;
|
|
20
|
+
|
|
21
|
+
export const SYNTHETIC_EXTENSIONS_REGISTER_EVENT =
|
|
22
|
+
"synthetic:extensions:register" as const;
|
|
23
|
+
|
|
24
|
+
export interface SyntheticExtensionsRegisterPayload {
|
|
25
|
+
feature: SyntheticFeatureId;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Config schema version. Stamped on disk when the initial migration runs or
|
|
30
|
+
* when the config is seeded. Uses the package version; bumping the package
|
|
31
|
+
* does not retrigger migrations (we only run them when `configVersion` is
|
|
32
|
+
* missing), but it records which release first created the file.
|
|
33
|
+
*/
|
|
34
|
+
export const SYNTHETIC_CONFIG_VERSION: string = pkg.version;
|
|
35
|
+
|
|
36
|
+
export interface SyntheticConfig {
|
|
37
|
+
configVersion?: string;
|
|
38
|
+
webSearch?: boolean;
|
|
39
|
+
quotasCommand?: boolean;
|
|
40
|
+
usageStatus?: boolean;
|
|
41
|
+
quotaWarnings?: boolean;
|
|
42
|
+
subBarIntegration?: boolean;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface ResolvedSyntheticConfig {
|
|
46
|
+
configVersion: string;
|
|
47
|
+
webSearch: boolean;
|
|
48
|
+
quotasCommand: boolean;
|
|
49
|
+
usageStatus: boolean;
|
|
50
|
+
quotaWarnings: boolean;
|
|
51
|
+
subBarIntegration: boolean;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const DEFAULT_CONFIG: ResolvedSyntheticConfig = {
|
|
55
|
+
configVersion: SYNTHETIC_CONFIG_VERSION,
|
|
56
|
+
webSearch: true,
|
|
57
|
+
quotasCommand: true,
|
|
58
|
+
usageStatus: false,
|
|
59
|
+
quotaWarnings: false,
|
|
60
|
+
subBarIntegration: true,
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// Module-level flag set when the v1 migration runs or when the global config
|
|
64
|
+
// is seeded for the first time. Consumed once by the provider extension to
|
|
65
|
+
// display a one-time notice about the new settings UI.
|
|
66
|
+
let pendingMigrationNotice = false;
|
|
67
|
+
|
|
68
|
+
export function hasPendingMigrationNotice(): boolean {
|
|
69
|
+
return pendingMigrationNotice;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function clearPendingMigrationNotice(): void {
|
|
73
|
+
pendingMigrationNotice = false;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function markMigrationNoticePending(): void {
|
|
77
|
+
pendingMigrationNotice = true;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const migrations: Migration<SyntheticConfig>[] = [
|
|
81
|
+
{
|
|
82
|
+
name: "seed-defaults",
|
|
83
|
+
shouldRun: (config) => config.configVersion === undefined,
|
|
84
|
+
run: (config) => {
|
|
85
|
+
markMigrationNoticePending();
|
|
86
|
+
return {
|
|
87
|
+
configVersion: SYNTHETIC_CONFIG_VERSION,
|
|
88
|
+
webSearch: config.webSearch ?? DEFAULT_CONFIG.webSearch,
|
|
89
|
+
quotasCommand: config.quotasCommand ?? DEFAULT_CONFIG.quotasCommand,
|
|
90
|
+
usageStatus: config.usageStatus ?? DEFAULT_CONFIG.usageStatus,
|
|
91
|
+
quotaWarnings: config.quotaWarnings ?? DEFAULT_CONFIG.quotaWarnings,
|
|
92
|
+
subBarIntegration:
|
|
93
|
+
config.subBarIntegration ?? DEFAULT_CONFIG.subBarIntegration,
|
|
94
|
+
};
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
];
|
|
98
|
+
|
|
99
|
+
const QUOTA_WARNING_THRESHOLDS_DESCRIPTION =
|
|
100
|
+
"Toggle warnings when your quotas reach thresholds. Thresholds: warning at 80% projected usage, high at 90%, critical at 100% for fixed windows; dynamic windows use adaptive projected thresholds based on window progress.";
|
|
101
|
+
|
|
102
|
+
export const configLoader = new ConfigLoader<
|
|
103
|
+
SyntheticConfig,
|
|
104
|
+
ResolvedSyntheticConfig
|
|
105
|
+
>("synthetic", DEFAULT_CONFIG, { migrations });
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Seed the global config file on first use. When no config file exists in
|
|
109
|
+
* any scope, this writes the current defaults (with configVersion) to the
|
|
110
|
+
* global scope and flags the migration notice as pending.
|
|
111
|
+
*
|
|
112
|
+
* Must be called after `configLoader.load()`.
|
|
113
|
+
*/
|
|
114
|
+
export async function seedSyntheticConfigIfMissing(): Promise<void> {
|
|
115
|
+
if (configLoader.hasConfig("global") || configLoader.hasConfig("local")) {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
markMigrationNoticePending();
|
|
119
|
+
try {
|
|
120
|
+
await configLoader.save("global", {
|
|
121
|
+
configVersion: SYNTHETIC_CONFIG_VERSION,
|
|
122
|
+
webSearch: DEFAULT_CONFIG.webSearch,
|
|
123
|
+
quotasCommand: DEFAULT_CONFIG.quotasCommand,
|
|
124
|
+
usageStatus: DEFAULT_CONFIG.usageStatus,
|
|
125
|
+
quotaWarnings: DEFAULT_CONFIG.quotaWarnings,
|
|
126
|
+
subBarIntegration: DEFAULT_CONFIG.subBarIntegration,
|
|
127
|
+
});
|
|
128
|
+
} catch {
|
|
129
|
+
// If the write fails, keep the notice pending so the user still sees it.
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export const SYNTHETIC_CONFIG_UPDATED_EVENT =
|
|
134
|
+
"synthetic:config:updated" as const;
|
|
135
|
+
|
|
136
|
+
export interface SyntheticConfigUpdatedPayload {
|
|
137
|
+
config: ResolvedSyntheticConfig;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function emitSyntheticConfigUpdated(pi: ExtensionAPI): void {
|
|
141
|
+
pi.events.emit(SYNTHETIC_CONFIG_UPDATED_EVENT, {
|
|
142
|
+
config: configLoader.getConfig(),
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export interface RegisterSyntheticSettingsOptions {
|
|
147
|
+
getLoadedFeatures: () => Set<SyntheticFeatureId>;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function featureRow(
|
|
151
|
+
id: SyntheticFeatureId,
|
|
152
|
+
label: string,
|
|
153
|
+
description: string,
|
|
154
|
+
configValue: boolean,
|
|
155
|
+
isLoaded: boolean,
|
|
156
|
+
): SettingItem {
|
|
157
|
+
if (isLoaded) {
|
|
158
|
+
return {
|
|
159
|
+
id,
|
|
160
|
+
label,
|
|
161
|
+
description,
|
|
162
|
+
currentValue: configValue ? "enabled" : "disabled",
|
|
163
|
+
values: ["enabled", "disabled"],
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
return {
|
|
167
|
+
id,
|
|
168
|
+
label,
|
|
169
|
+
description: `${description} (Not loaded by Pi)`,
|
|
170
|
+
currentValue: "unavailable",
|
|
171
|
+
values: [],
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function registerSyntheticSettings(
|
|
176
|
+
pi: ExtensionAPI,
|
|
177
|
+
options: RegisterSyntheticSettingsOptions,
|
|
178
|
+
): void {
|
|
179
|
+
const { getLoadedFeatures } = options;
|
|
180
|
+
|
|
181
|
+
registerSettingsCommand<SyntheticConfig, ResolvedSyntheticConfig>(pi, {
|
|
182
|
+
commandName: "synthetic:settings",
|
|
183
|
+
commandDescription: "Configure Synthetic extension settings",
|
|
184
|
+
title: "Synthetic Settings",
|
|
185
|
+
configStore: configLoader,
|
|
186
|
+
buildSections: (tabConfig, resolved): SettingsSection[] => {
|
|
187
|
+
const loaded = getLoadedFeatures();
|
|
188
|
+
const webSearch = tabConfig?.webSearch ?? resolved.webSearch;
|
|
189
|
+
const quotasCommand = tabConfig?.quotasCommand ?? resolved.quotasCommand;
|
|
190
|
+
const usageStatus = tabConfig?.usageStatus ?? resolved.usageStatus;
|
|
191
|
+
const quotaWarnings = tabConfig?.quotaWarnings ?? resolved.quotaWarnings;
|
|
192
|
+
const subBarIntegration =
|
|
193
|
+
tabConfig?.subBarIntegration ?? resolved.subBarIntegration;
|
|
194
|
+
|
|
195
|
+
const sections: SettingsSection[] = [];
|
|
196
|
+
|
|
197
|
+
sections.push(
|
|
198
|
+
{
|
|
199
|
+
label: "Tools",
|
|
200
|
+
items: [
|
|
201
|
+
featureRow(
|
|
202
|
+
"webSearch",
|
|
203
|
+
"Web Search",
|
|
204
|
+
"Toggle `synthetic_web_search`, a tool for searching online with zero data retention",
|
|
205
|
+
webSearch,
|
|
206
|
+
loaded.has("webSearch"),
|
|
207
|
+
),
|
|
208
|
+
],
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
label: "Quotas",
|
|
212
|
+
items: [
|
|
213
|
+
featureRow(
|
|
214
|
+
"quotasCommand",
|
|
215
|
+
"Quotas Command",
|
|
216
|
+
"Toggle the `/synthetic:quotas` command, showing your quotas at a glance",
|
|
217
|
+
quotasCommand,
|
|
218
|
+
loaded.has("quotasCommand"),
|
|
219
|
+
),
|
|
220
|
+
featureRow(
|
|
221
|
+
"usageStatus",
|
|
222
|
+
"Usage widget",
|
|
223
|
+
"Toggle the usage widget, showing your usage at a glance",
|
|
224
|
+
usageStatus,
|
|
225
|
+
loaded.has("usageStatus"),
|
|
226
|
+
),
|
|
227
|
+
featureRow(
|
|
228
|
+
"quotaWarnings",
|
|
229
|
+
"Quota Warnings",
|
|
230
|
+
QUOTA_WARNING_THRESHOLDS_DESCRIPTION,
|
|
231
|
+
quotaWarnings,
|
|
232
|
+
loaded.has("quotaWarnings"),
|
|
233
|
+
),
|
|
234
|
+
],
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
label: "Integration",
|
|
238
|
+
items: [
|
|
239
|
+
featureRow(
|
|
240
|
+
"subBarIntegration",
|
|
241
|
+
"pi-sub-bar integration",
|
|
242
|
+
"Integration with `@marckrenn/pi-sub-bar`",
|
|
243
|
+
subBarIntegration,
|
|
244
|
+
loaded.has("subBarIntegration"),
|
|
245
|
+
),
|
|
246
|
+
],
|
|
247
|
+
},
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
return sections;
|
|
251
|
+
},
|
|
252
|
+
onSettingChange: (id, newValue, config) => {
|
|
253
|
+
if (!getLoadedFeatures().has(id as SyntheticFeatureId)) {
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const enabled = newValue === "enabled";
|
|
258
|
+
switch (id) {
|
|
259
|
+
case "webSearch":
|
|
260
|
+
return { ...config, webSearch: enabled };
|
|
261
|
+
case "quotasCommand":
|
|
262
|
+
return { ...config, quotasCommand: enabled };
|
|
263
|
+
case "usageStatus":
|
|
264
|
+
return { ...config, usageStatus: enabled };
|
|
265
|
+
case "quotaWarnings":
|
|
266
|
+
return { ...config, quotaWarnings: enabled };
|
|
267
|
+
case "subBarIntegration":
|
|
268
|
+
return { ...config, subBarIntegration: enabled };
|
|
269
|
+
default:
|
|
270
|
+
return null;
|
|
271
|
+
}
|
|
272
|
+
},
|
|
273
|
+
onSave: async () => {
|
|
274
|
+
emitSyntheticConfigUpdated(pi);
|
|
275
|
+
},
|
|
276
|
+
});
|
|
277
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import { configLoader } from "../../config";
|
|
2
3
|
import { getSyntheticApiKey } from "../../lib/env";
|
|
3
4
|
import { fetchQuotas } from "../../utils/quotas";
|
|
4
5
|
import { QuotasComponent } from "./components/quotas-display";
|
|
@@ -10,6 +11,14 @@ export function registerQuotasCommand(pi: ExtensionAPI): void {
|
|
|
10
11
|
pi.registerCommand("synthetic:quotas", {
|
|
11
12
|
description: "Display Synthetic API usage quotas",
|
|
12
13
|
handler: async (_args, ctx) => {
|
|
14
|
+
if (!configLoader.getConfig().quotasCommand) {
|
|
15
|
+
ctx.ui.notify(
|
|
16
|
+
"Synthetic quotas command is disabled. Restart Pi to unload the command after re-enabling or disabling it.",
|
|
17
|
+
"warning",
|
|
18
|
+
);
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
13
22
|
const apiKey = await getSyntheticApiKey(ctx.modelRegistry.authStorage);
|
|
14
23
|
if (!apiKey) {
|
|
15
24
|
ctx.ui.notify(MISSING_AUTH_MESSAGE, "warning");
|
|
@@ -150,7 +150,6 @@ export class QuotasComponent implements Component {
|
|
|
150
150
|
width,
|
|
151
151
|
),
|
|
152
152
|
);
|
|
153
|
-
lines.push("");
|
|
154
153
|
|
|
155
154
|
switch (this.state.type) {
|
|
156
155
|
case "loading":
|
|
@@ -186,6 +185,8 @@ export class QuotasComponent implements Component {
|
|
|
186
185
|
const windows = toWindows(quotas);
|
|
187
186
|
const barWidth = Math.min(50, Math.max(20, contentWidth - 20));
|
|
188
187
|
|
|
188
|
+
lines.push("");
|
|
189
|
+
|
|
189
190
|
for (const window of windows) {
|
|
190
191
|
lines.push(...this.renderWindow(window, barWidth, maxWidth));
|
|
191
192
|
lines.push("");
|
|
@@ -1,8 +1,23 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import {
|
|
3
|
+
configLoader,
|
|
4
|
+
SYNTHETIC_EXTENSIONS_REGISTER_EVENT,
|
|
5
|
+
SYNTHETIC_EXTENSIONS_REQUEST_EVENT,
|
|
6
|
+
} from "../../config";
|
|
2
7
|
import { registerQuotasCommand } from "./command";
|
|
3
|
-
import { registerSubIntegration } from "./sub-integration";
|
|
4
8
|
|
|
5
9
|
export default async function (pi: ExtensionAPI) {
|
|
6
|
-
|
|
7
|
-
|
|
10
|
+
await configLoader.load();
|
|
11
|
+
|
|
12
|
+
const config = configLoader.getConfig();
|
|
13
|
+
|
|
14
|
+
if (config.quotasCommand) {
|
|
15
|
+
registerQuotasCommand(pi);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
pi.events.on(SYNTHETIC_EXTENSIONS_REQUEST_EVENT, () => {
|
|
19
|
+
pi.events.emit(SYNTHETIC_EXTENSIONS_REGISTER_EVENT, {
|
|
20
|
+
feature: "quotasCommand",
|
|
21
|
+
});
|
|
22
|
+
});
|
|
8
23
|
}
|
|
@@ -1,6 +1,59 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import { visibleWidth, wrapTextWithAnsi } from "@mariozechner/pi-tui";
|
|
3
|
+
import {
|
|
4
|
+
clearPendingMigrationNotice,
|
|
5
|
+
configLoader,
|
|
6
|
+
emitSyntheticConfigUpdated,
|
|
7
|
+
hasPendingMigrationNotice,
|
|
8
|
+
registerSyntheticSettings,
|
|
9
|
+
SYNTHETIC_EXTENSIONS_REGISTER_EVENT,
|
|
10
|
+
SYNTHETIC_EXTENSIONS_REQUEST_EVENT,
|
|
11
|
+
type SyntheticExtensionsRegisterPayload,
|
|
12
|
+
type SyntheticFeatureId,
|
|
13
|
+
seedSyntheticConfigIfMissing,
|
|
14
|
+
} from "../../config";
|
|
2
15
|
import { SYNTHETIC_MODELS } from "./models";
|
|
3
16
|
|
|
17
|
+
const MIGRATION_NOTICE_MESSAGE_TYPE = "synthetic:migration-notice";
|
|
18
|
+
const MIGRATION_NOTICE_TITLE = "pi-synthetic";
|
|
19
|
+
const MIGRATION_NOTICE_CONTENT = [
|
|
20
|
+
"New optional features added to `pi-synthetic`:",
|
|
21
|
+
"- Usage widget",
|
|
22
|
+
"- Quotas warnings",
|
|
23
|
+
"",
|
|
24
|
+
"Enable them either with `pi config` or inside of `pi` with `/synthetic:settings`.",
|
|
25
|
+
].join("\n");
|
|
26
|
+
|
|
27
|
+
/** Wrap lines in a rounded Unicode frame with 1-char inner padding. */
|
|
28
|
+
function wrapInRoundedBorder(
|
|
29
|
+
lines: string[],
|
|
30
|
+
width: number,
|
|
31
|
+
colorFn: (text: string) => string,
|
|
32
|
+
): string[] {
|
|
33
|
+
const innerWidth = Math.max(1, width - 2);
|
|
34
|
+
const hBar = "\u2500".repeat(innerWidth);
|
|
35
|
+
const top = colorFn(`\u256D${hBar}\u256E`);
|
|
36
|
+
const bottom = colorFn(`\u2570${hBar}\u256F`);
|
|
37
|
+
const left = colorFn("\u2502");
|
|
38
|
+
const right = colorFn("\u2502");
|
|
39
|
+
|
|
40
|
+
const wrapped = lines.map((line) => {
|
|
41
|
+
const contentWidth = visibleWidth(line);
|
|
42
|
+
const fill = Math.max(0, innerWidth - contentWidth);
|
|
43
|
+
return `${left}${line}${" ".repeat(fill)}${right}`;
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
return [top, ...wrapped, bottom];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Highlight `backtick-wrapped` spans using the accent color. */
|
|
50
|
+
function highlightInlineCode(
|
|
51
|
+
text: string,
|
|
52
|
+
colorFn: (text: string) => string,
|
|
53
|
+
): string {
|
|
54
|
+
return text.replace(/`([^`]+)`/g, (_, code) => colorFn(code));
|
|
55
|
+
}
|
|
56
|
+
|
|
4
57
|
export function registerSyntheticProvider(pi: ExtensionAPI): void {
|
|
5
58
|
pi.registerProvider("synthetic", {
|
|
6
59
|
baseUrl: "https://api.synthetic.new/openai/v1",
|
|
@@ -10,14 +63,8 @@ export function registerSyntheticProvider(pi: ExtensionAPI): void {
|
|
|
10
63
|
Referer: "https://pi.dev",
|
|
11
64
|
"X-Title": "npm:@aliou/pi-synthetic",
|
|
12
65
|
},
|
|
13
|
-
models: SYNTHETIC_MODELS.map((model) => ({
|
|
14
|
-
|
|
15
|
-
name: model.name,
|
|
16
|
-
reasoning: model.reasoning,
|
|
17
|
-
input: model.input,
|
|
18
|
-
cost: model.cost,
|
|
19
|
-
contextWindow: model.contextWindow,
|
|
20
|
-
maxTokens: model.maxTokens,
|
|
66
|
+
models: SYNTHETIC_MODELS.map(({ provider: _provider, ...model }) => ({
|
|
67
|
+
...model,
|
|
21
68
|
compat: {
|
|
22
69
|
supportsDeveloperRole: false,
|
|
23
70
|
maxTokensField: "max_tokens",
|
|
@@ -28,5 +75,65 @@ export function registerSyntheticProvider(pi: ExtensionAPI): void {
|
|
|
28
75
|
}
|
|
29
76
|
|
|
30
77
|
export default async function (pi: ExtensionAPI) {
|
|
78
|
+
await configLoader.load();
|
|
79
|
+
await seedSyntheticConfigIfMissing();
|
|
31
80
|
registerSyntheticProvider(pi);
|
|
81
|
+
|
|
82
|
+
pi.registerMessageRenderer(
|
|
83
|
+
MIGRATION_NOTICE_MESSAGE_TYPE,
|
|
84
|
+
(message, _options, theme) => {
|
|
85
|
+
const rawContent =
|
|
86
|
+
typeof message.content === "string"
|
|
87
|
+
? message.content
|
|
88
|
+
: MIGRATION_NOTICE_CONTENT;
|
|
89
|
+
const accent = (t: string) => theme.fg("accent", t);
|
|
90
|
+
const borderColor = accent;
|
|
91
|
+
const title = theme.bold(accent(MIGRATION_NOTICE_TITLE));
|
|
92
|
+
const body = highlightInlineCode(rawContent, accent);
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
render(width: number) {
|
|
96
|
+
// border (2) + inner padding (2)
|
|
97
|
+
const contentWidth = Math.max(1, width - 4);
|
|
98
|
+
const bodyLines = wrapTextWithAnsi(body, contentWidth);
|
|
99
|
+
const lines = [title, "", ...bodyLines];
|
|
100
|
+
const padded = lines.map((line) => ` ${line} `);
|
|
101
|
+
return wrapInRoundedBorder(padded, width, borderColor);
|
|
102
|
+
},
|
|
103
|
+
handleInput() {
|
|
104
|
+
return false;
|
|
105
|
+
},
|
|
106
|
+
invalidate() {},
|
|
107
|
+
};
|
|
108
|
+
},
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
const loadedFeatures = new Set<SyntheticFeatureId>();
|
|
112
|
+
|
|
113
|
+
pi.events.on(SYNTHETIC_EXTENSIONS_REGISTER_EVENT, (data: unknown) => {
|
|
114
|
+
const { feature } = data as SyntheticExtensionsRegisterPayload;
|
|
115
|
+
loadedFeatures.add(feature);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
registerSyntheticSettings(pi, {
|
|
119
|
+
getLoadedFeatures: () => loadedFeatures,
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
pi.on("session_start", async () => {
|
|
123
|
+
loadedFeatures.clear();
|
|
124
|
+
pi.events.emit(SYNTHETIC_EXTENSIONS_REQUEST_EVENT, undefined);
|
|
125
|
+
emitSyntheticConfigUpdated(pi);
|
|
126
|
+
|
|
127
|
+
if (hasPendingMigrationNotice()) {
|
|
128
|
+
clearPendingMigrationNotice();
|
|
129
|
+
pi.sendMessage(
|
|
130
|
+
{
|
|
131
|
+
customType: MIGRATION_NOTICE_MESSAGE_TYPE,
|
|
132
|
+
content: MIGRATION_NOTICE_CONTENT,
|
|
133
|
+
display: true,
|
|
134
|
+
},
|
|
135
|
+
{ triggerTurn: false },
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
});
|
|
32
139
|
}
|
|
@@ -4,6 +4,7 @@ import { SYNTHETIC_MODELS } from "./models";
|
|
|
4
4
|
interface ApiModel {
|
|
5
5
|
id: string;
|
|
6
6
|
name: string;
|
|
7
|
+
provider: string | null;
|
|
7
8
|
input_modalities: string[];
|
|
8
9
|
output_modalities: string[];
|
|
9
10
|
context_length: number;
|
|
@@ -156,6 +157,19 @@ function compareModels(
|
|
|
156
157
|
});
|
|
157
158
|
}
|
|
158
159
|
}
|
|
160
|
+
|
|
161
|
+
// Check provider
|
|
162
|
+
if (
|
|
163
|
+
apiModel.provider !== null &&
|
|
164
|
+
apiModel.provider !== hardcoded.provider
|
|
165
|
+
) {
|
|
166
|
+
discrepancies.push({
|
|
167
|
+
model: hardcoded.id,
|
|
168
|
+
field: "provider",
|
|
169
|
+
hardcoded: hardcoded.provider,
|
|
170
|
+
api: apiModel.provider,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
159
173
|
}
|
|
160
174
|
|
|
161
175
|
// Check for API models not in hardcoded list
|
|
@@ -2,29 +2,11 @@
|
|
|
2
2
|
// Source: https://api.synthetic.new/openai/v1/models
|
|
3
3
|
// maxTokens sourced from https://models.dev/api.json (synthetic provider)
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
cost: {
|
|
11
|
-
input: number;
|
|
12
|
-
output: number;
|
|
13
|
-
cacheRead: number;
|
|
14
|
-
cacheWrite: number;
|
|
15
|
-
};
|
|
16
|
-
contextWindow: number;
|
|
17
|
-
maxTokens: number;
|
|
18
|
-
compat?: {
|
|
19
|
-
supportsDeveloperRole?: boolean;
|
|
20
|
-
supportsReasoningEffort?: boolean;
|
|
21
|
-
reasoningEffortMap?: Partial<
|
|
22
|
-
Record<"minimal" | "low" | "medium" | "high" | "xhigh", string>
|
|
23
|
-
>;
|
|
24
|
-
maxTokensField?: "max_completion_tokens" | "max_tokens";
|
|
25
|
-
requiresToolResultName?: boolean;
|
|
26
|
-
requiresMistralToolIds?: boolean;
|
|
27
|
-
};
|
|
5
|
+
import type { ProviderModelConfig } from "@mariozechner/pi-coding-agent";
|
|
6
|
+
|
|
7
|
+
export interface SyntheticModelConfig extends ProviderModelConfig {
|
|
8
|
+
/** Upstream backend Synthetic proxies this model through (e.g. "fireworks", "together", "synthetic"). */
|
|
9
|
+
provider: string;
|
|
28
10
|
}
|
|
29
11
|
|
|
30
12
|
const SYNTHETIC_REASONING_EFFORT_MAP = {
|
|
@@ -40,6 +22,7 @@ export const SYNTHETIC_MODELS: SyntheticModelConfig[] = [
|
|
|
40
22
|
{
|
|
41
23
|
id: "hf:zai-org/GLM-4.7",
|
|
42
24
|
name: "zai-org/GLM-4.7",
|
|
25
|
+
provider: "synthetic",
|
|
43
26
|
reasoning: true,
|
|
44
27
|
compat: {
|
|
45
28
|
supportsReasoningEffort: true,
|
|
@@ -59,6 +42,7 @@ export const SYNTHETIC_MODELS: SyntheticModelConfig[] = [
|
|
|
59
42
|
{
|
|
60
43
|
id: "hf:zai-org/GLM-5",
|
|
61
44
|
name: "zai-org/GLM-5",
|
|
45
|
+
provider: "synthetic",
|
|
62
46
|
reasoning: true,
|
|
63
47
|
compat: {
|
|
64
48
|
supportsReasoningEffort: true,
|
|
@@ -78,6 +62,7 @@ export const SYNTHETIC_MODELS: SyntheticModelConfig[] = [
|
|
|
78
62
|
{
|
|
79
63
|
id: "hf:zai-org/GLM-5.1",
|
|
80
64
|
name: "zai-org/GLM-5.1",
|
|
65
|
+
provider: "synthetic",
|
|
81
66
|
reasoning: true,
|
|
82
67
|
compat: {
|
|
83
68
|
supportsReasoningEffort: true,
|
|
@@ -98,6 +83,7 @@ export const SYNTHETIC_MODELS: SyntheticModelConfig[] = [
|
|
|
98
83
|
{
|
|
99
84
|
id: "hf:zai-org/GLM-4.7-Flash",
|
|
100
85
|
name: "zai-org/GLM-4.7-Flash",
|
|
86
|
+
provider: "synthetic",
|
|
101
87
|
reasoning: true,
|
|
102
88
|
compat: {
|
|
103
89
|
supportsReasoningEffort: true,
|
|
@@ -117,6 +103,7 @@ export const SYNTHETIC_MODELS: SyntheticModelConfig[] = [
|
|
|
117
103
|
{
|
|
118
104
|
id: "hf:MiniMaxAI/MiniMax-M2.1",
|
|
119
105
|
name: "MiniMaxAI/MiniMax-M2.1",
|
|
106
|
+
provider: "fireworks",
|
|
120
107
|
reasoning: true,
|
|
121
108
|
compat: {
|
|
122
109
|
supportsReasoningEffort: true,
|
|
@@ -136,6 +123,7 @@ export const SYNTHETIC_MODELS: SyntheticModelConfig[] = [
|
|
|
136
123
|
{
|
|
137
124
|
id: "hf:meta-llama/Llama-3.3-70B-Instruct",
|
|
138
125
|
name: "meta-llama/Llama-3.3-70B-Instruct",
|
|
126
|
+
provider: "together",
|
|
139
127
|
reasoning: false,
|
|
140
128
|
input: ["text"],
|
|
141
129
|
cost: {
|
|
@@ -151,6 +139,7 @@ export const SYNTHETIC_MODELS: SyntheticModelConfig[] = [
|
|
|
151
139
|
{
|
|
152
140
|
id: "hf:deepseek-ai/DeepSeek-R1-0528",
|
|
153
141
|
name: "deepseek-ai/DeepSeek-R1-0528",
|
|
142
|
+
provider: "together",
|
|
154
143
|
reasoning: true,
|
|
155
144
|
compat: {
|
|
156
145
|
supportsReasoningEffort: true,
|
|
@@ -170,6 +159,7 @@ export const SYNTHETIC_MODELS: SyntheticModelConfig[] = [
|
|
|
170
159
|
{
|
|
171
160
|
id: "hf:deepseek-ai/DeepSeek-V3.2",
|
|
172
161
|
name: "deepseek-ai/DeepSeek-V3.2",
|
|
162
|
+
provider: "fireworks",
|
|
173
163
|
reasoning: false,
|
|
174
164
|
input: ["text"],
|
|
175
165
|
cost: {
|
|
@@ -185,6 +175,7 @@ export const SYNTHETIC_MODELS: SyntheticModelConfig[] = [
|
|
|
185
175
|
{
|
|
186
176
|
id: "hf:moonshotai/Kimi-K2-Instruct-0905",
|
|
187
177
|
name: "moonshotai/Kimi-K2-Instruct-0905",
|
|
178
|
+
provider: "fireworks",
|
|
188
179
|
reasoning: false,
|
|
189
180
|
input: ["text"],
|
|
190
181
|
cost: {
|
|
@@ -200,6 +191,7 @@ export const SYNTHETIC_MODELS: SyntheticModelConfig[] = [
|
|
|
200
191
|
{
|
|
201
192
|
id: "hf:moonshotai/Kimi-K2-Thinking",
|
|
202
193
|
name: "moonshotai/Kimi-K2-Thinking",
|
|
194
|
+
provider: "fireworks",
|
|
203
195
|
reasoning: true,
|
|
204
196
|
compat: {
|
|
205
197
|
supportsReasoningEffort: true,
|
|
@@ -219,6 +211,7 @@ export const SYNTHETIC_MODELS: SyntheticModelConfig[] = [
|
|
|
219
211
|
{
|
|
220
212
|
id: "hf:openai/gpt-oss-120b",
|
|
221
213
|
name: "openai/gpt-oss-120b",
|
|
214
|
+
provider: "fireworks",
|
|
222
215
|
reasoning: false,
|
|
223
216
|
input: ["text"],
|
|
224
217
|
cost: {
|
|
@@ -234,6 +227,7 @@ export const SYNTHETIC_MODELS: SyntheticModelConfig[] = [
|
|
|
234
227
|
{
|
|
235
228
|
id: "hf:Qwen/Qwen3-Coder-480B-A35B-Instruct",
|
|
236
229
|
name: "Qwen/Qwen3-Coder-480B-A35B-Instruct",
|
|
230
|
+
provider: "together",
|
|
237
231
|
reasoning: true,
|
|
238
232
|
compat: {
|
|
239
233
|
supportsReasoningEffort: true,
|
|
@@ -253,6 +247,7 @@ export const SYNTHETIC_MODELS: SyntheticModelConfig[] = [
|
|
|
253
247
|
{
|
|
254
248
|
id: "hf:moonshotai/Kimi-K2.5",
|
|
255
249
|
name: "moonshotai/Kimi-K2.5",
|
|
250
|
+
provider: "synthetic",
|
|
256
251
|
reasoning: true,
|
|
257
252
|
compat: {
|
|
258
253
|
supportsReasoningEffort: true,
|
|
@@ -272,6 +267,7 @@ export const SYNTHETIC_MODELS: SyntheticModelConfig[] = [
|
|
|
272
267
|
{
|
|
273
268
|
id: "hf:nvidia/Kimi-K2.5-NVFP4",
|
|
274
269
|
name: "nvidia/Kimi-K2.5-NVFP4",
|
|
270
|
+
provider: "synthetic",
|
|
275
271
|
reasoning: true,
|
|
276
272
|
compat: {
|
|
277
273
|
supportsReasoningEffort: true,
|
|
@@ -291,6 +287,7 @@ export const SYNTHETIC_MODELS: SyntheticModelConfig[] = [
|
|
|
291
287
|
{
|
|
292
288
|
id: "hf:deepseek-ai/DeepSeek-V3",
|
|
293
289
|
name: "deepseek-ai/DeepSeek-V3",
|
|
290
|
+
provider: "together",
|
|
294
291
|
reasoning: false,
|
|
295
292
|
input: ["text"],
|
|
296
293
|
cost: {
|
|
@@ -306,6 +303,7 @@ export const SYNTHETIC_MODELS: SyntheticModelConfig[] = [
|
|
|
306
303
|
{
|
|
307
304
|
id: "hf:Qwen/Qwen3-235B-A22B-Thinking-2507",
|
|
308
305
|
name: "Qwen/Qwen3-235B-A22B-Thinking-2507",
|
|
306
|
+
provider: "together",
|
|
309
307
|
reasoning: true,
|
|
310
308
|
compat: {
|
|
311
309
|
supportsReasoningEffort: true,
|
|
@@ -325,6 +323,7 @@ export const SYNTHETIC_MODELS: SyntheticModelConfig[] = [
|
|
|
325
323
|
{
|
|
326
324
|
id: "hf:Qwen/Qwen3.5-397B-A17B",
|
|
327
325
|
name: "Qwen/Qwen3.5-397B-A17B",
|
|
326
|
+
provider: "together",
|
|
328
327
|
reasoning: true,
|
|
329
328
|
compat: {
|
|
330
329
|
supportsReasoningEffort: true,
|
|
@@ -344,6 +343,7 @@ export const SYNTHETIC_MODELS: SyntheticModelConfig[] = [
|
|
|
344
343
|
{
|
|
345
344
|
id: "hf:MiniMaxAI/MiniMax-M2.5",
|
|
346
345
|
name: "MiniMaxAI/MiniMax-M2.5",
|
|
346
|
+
provider: "synthetic",
|
|
347
347
|
reasoning: true,
|
|
348
348
|
input: ["text"],
|
|
349
349
|
cost: {
|
|
@@ -364,6 +364,7 @@ export const SYNTHETIC_MODELS: SyntheticModelConfig[] = [
|
|
|
364
364
|
{
|
|
365
365
|
id: "hf:nvidia/NVIDIA-Nemotron-3-Super-120B-A12B-NVFP4",
|
|
366
366
|
name: "nvidia/NVIDIA-Nemotron-3-Super-120B-A12B-NVFP4",
|
|
367
|
+
provider: "synthetic",
|
|
367
368
|
reasoning: true,
|
|
368
369
|
compat: {
|
|
369
370
|
supportsReasoningEffort: true,
|
|
@@ -1,22 +1,58 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import {
|
|
3
|
+
configLoader,
|
|
4
|
+
SYNTHETIC_CONFIG_UPDATED_EVENT,
|
|
5
|
+
SYNTHETIC_EXTENSIONS_REGISTER_EVENT,
|
|
6
|
+
SYNTHETIC_EXTENSIONS_REQUEST_EVENT,
|
|
7
|
+
type SyntheticConfigUpdatedPayload,
|
|
8
|
+
} from "../../config";
|
|
2
9
|
import { clearAlertState, triggerCheck } from "./notifier";
|
|
3
10
|
|
|
4
11
|
export default async function (pi: ExtensionAPI) {
|
|
5
|
-
|
|
12
|
+
await configLoader.load();
|
|
13
|
+
|
|
14
|
+
let enabled = configLoader.getConfig().quotaWarnings;
|
|
15
|
+
let currentModel: { provider: string; id: string } | undefined;
|
|
16
|
+
let currentContext: Parameters<typeof triggerCheck>[0] | undefined;
|
|
17
|
+
|
|
18
|
+
pi.events.on(SYNTHETIC_CONFIG_UPDATED_EVENT, (data: unknown) => {
|
|
19
|
+
enabled = (data as SyntheticConfigUpdatedPayload).config.quotaWarnings;
|
|
20
|
+
|
|
21
|
+
if (!enabled) {
|
|
22
|
+
clearAlertState();
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (currentContext && currentModel?.provider === "synthetic") {
|
|
27
|
+
clearAlertState();
|
|
28
|
+
triggerCheck(currentContext, currentModel, false);
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
|
|
6
32
|
pi.on("session_start", async (_event, ctx) => {
|
|
7
|
-
|
|
33
|
+
currentContext = ctx;
|
|
34
|
+
currentModel = ctx.model;
|
|
35
|
+
if (!enabled || ctx.model?.provider !== "synthetic") return;
|
|
8
36
|
clearAlertState();
|
|
9
37
|
triggerCheck(ctx, ctx.model, false);
|
|
10
38
|
});
|
|
11
39
|
|
|
12
|
-
// Check after agent turn - only warn for newly crossed thresholds
|
|
13
40
|
pi.on("agent_end", async (_event, ctx) => {
|
|
14
|
-
|
|
41
|
+
currentContext = ctx;
|
|
42
|
+
currentModel = ctx.model;
|
|
43
|
+
if (!enabled || ctx.model?.provider !== "synthetic") return;
|
|
15
44
|
triggerCheck(ctx, ctx.model, true);
|
|
16
45
|
});
|
|
17
46
|
|
|
18
|
-
// Clear state on shutdown
|
|
19
47
|
pi.on("session_shutdown", async () => {
|
|
48
|
+
currentContext = undefined;
|
|
49
|
+
currentModel = undefined;
|
|
20
50
|
clearAlertState();
|
|
21
51
|
});
|
|
52
|
+
|
|
53
|
+
pi.events.on(SYNTHETIC_EXTENSIONS_REQUEST_EVENT, () => {
|
|
54
|
+
pi.events.emit(SYNTHETIC_EXTENSIONS_REGISTER_EVENT, {
|
|
55
|
+
feature: "quotaWarnings",
|
|
56
|
+
});
|
|
57
|
+
});
|
|
22
58
|
}
|
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
import type { AuthStorage, ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import {
|
|
3
|
+
configLoader,
|
|
4
|
+
SYNTHETIC_CONFIG_UPDATED_EVENT,
|
|
5
|
+
SYNTHETIC_EXTENSIONS_REGISTER_EVENT,
|
|
6
|
+
SYNTHETIC_EXTENSIONS_REQUEST_EVENT,
|
|
7
|
+
type SyntheticConfigUpdatedPayload,
|
|
8
|
+
} from "../../config";
|
|
2
9
|
import { getSyntheticApiKey } from "../../lib/env";
|
|
3
10
|
import type { QuotasResponse } from "../../types/quotas";
|
|
4
11
|
import { fetchQuotas, formatResetTime } from "../../utils/quotas";
|
|
@@ -28,7 +35,6 @@ interface SubCoreSettingsPayload {
|
|
|
28
35
|
function toUsageSnapshot(quotas: QuotasResponse): UsageSnapshot {
|
|
29
36
|
const windows: RateWindow[] = [];
|
|
30
37
|
|
|
31
|
-
// Weekly token limit (credits-based)
|
|
32
38
|
if (quotas.weeklyTokenLimit) {
|
|
33
39
|
const { weeklyTokenLimit } = quotas;
|
|
34
40
|
windows.push({
|
|
@@ -41,7 +47,6 @@ function toUsageSnapshot(quotas: QuotasResponse): UsageSnapshot {
|
|
|
41
47
|
});
|
|
42
48
|
}
|
|
43
49
|
|
|
44
|
-
// Rolling 5-hour limit (request-based)
|
|
45
50
|
if (quotas.rollingFiveHourLimit && quotas.rollingFiveHourLimit.max > 0) {
|
|
46
51
|
const { rollingFiveHourLimit } = quotas;
|
|
47
52
|
const used = rollingFiveHourLimit.max - rollingFiveHourLimit.remaining;
|
|
@@ -55,7 +60,6 @@ function toUsageSnapshot(quotas: QuotasResponse): UsageSnapshot {
|
|
|
55
60
|
});
|
|
56
61
|
}
|
|
57
62
|
|
|
58
|
-
// Legacy subscription (fallback if rollingFiveHourLimit not available)
|
|
59
63
|
if (
|
|
60
64
|
!quotas.rollingFiveHourLimit &&
|
|
61
65
|
quotas.subscription?.limit &&
|
|
@@ -117,15 +121,16 @@ async function emitCurrentUsage(
|
|
|
117
121
|
});
|
|
118
122
|
}
|
|
119
123
|
|
|
120
|
-
export function
|
|
124
|
+
export function registerSubBarIntegration(pi: ExtensionAPI): void {
|
|
121
125
|
let interval: NodeJS.Timeout | undefined;
|
|
122
126
|
let refreshMs = 60000;
|
|
123
127
|
let subCoreReady = false;
|
|
124
128
|
let currentProvider: string | undefined;
|
|
125
129
|
let currentAuthStorage: AuthStorage | undefined;
|
|
130
|
+
let enabled = configLoader.getConfig().subBarIntegration;
|
|
126
131
|
|
|
127
132
|
function isSynthetic(): boolean {
|
|
128
|
-
return currentProvider === "synthetic";
|
|
133
|
+
return enabled && currentProvider === "synthetic";
|
|
129
134
|
}
|
|
130
135
|
|
|
131
136
|
function stop(): void {
|
|
@@ -148,24 +153,33 @@ export function registerSubIntegration(pi: ExtensionAPI): void {
|
|
|
148
153
|
interval.unref?.();
|
|
149
154
|
}
|
|
150
155
|
|
|
151
|
-
|
|
156
|
+
pi.events.on(SYNTHETIC_CONFIG_UPDATED_EVENT, (data: unknown) => {
|
|
157
|
+
enabled = (data as SyntheticConfigUpdatedPayload).config.subBarIntegration;
|
|
158
|
+
|
|
159
|
+
if (!enabled) {
|
|
160
|
+
stop();
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (subCoreReady && currentAuthStorage && currentProvider === "synthetic") {
|
|
165
|
+
startPolling(currentAuthStorage);
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
|
|
152
169
|
pi.events.on("sub-core:ready", () => {
|
|
153
170
|
subCoreReady = true;
|
|
154
|
-
// Polling starts in session_start/model_select when provider is synthetic
|
|
155
171
|
});
|
|
156
172
|
|
|
157
173
|
pi.events.on("sub-core:settings:updated", (data: unknown) => {
|
|
158
174
|
const payload = data as SubCoreSettingsPayload;
|
|
159
175
|
if (payload.settings?.behavior?.refreshInterval) {
|
|
160
176
|
refreshMs = payload.settings.behavior.refreshInterval * 1000;
|
|
161
|
-
// Restart with new interval if currently running
|
|
162
177
|
if (interval && isSynthetic() && currentAuthStorage) {
|
|
163
178
|
startPolling(currentAuthStorage);
|
|
164
179
|
}
|
|
165
180
|
}
|
|
166
181
|
});
|
|
167
182
|
|
|
168
|
-
// Lifecycle events
|
|
169
183
|
pi.on("session_start", async (_event, ctx) => {
|
|
170
184
|
currentProvider = ctx.model?.provider;
|
|
171
185
|
currentAuthStorage = ctx.modelRegistry.authStorage;
|
|
@@ -200,3 +214,14 @@ export function registerSubIntegration(pi: ExtensionAPI): void {
|
|
|
200
214
|
stop();
|
|
201
215
|
});
|
|
202
216
|
}
|
|
217
|
+
|
|
218
|
+
export default async function (pi: ExtensionAPI) {
|
|
219
|
+
await configLoader.load();
|
|
220
|
+
registerSubBarIntegration(pi);
|
|
221
|
+
|
|
222
|
+
pi.events.on(SYNTHETIC_EXTENSIONS_REQUEST_EVENT, () => {
|
|
223
|
+
pi.events.emit(SYNTHETIC_EXTENSIONS_REGISTER_EVENT, {
|
|
224
|
+
feature: "subBarIntegration",
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
}
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ExtensionAPI,
|
|
3
|
+
ExtensionContext,
|
|
4
|
+
} from "@mariozechner/pi-coding-agent";
|
|
5
|
+
import {
|
|
6
|
+
configLoader,
|
|
7
|
+
SYNTHETIC_CONFIG_UPDATED_EVENT,
|
|
8
|
+
SYNTHETIC_EXTENSIONS_REGISTER_EVENT,
|
|
9
|
+
SYNTHETIC_EXTENSIONS_REQUEST_EVENT,
|
|
10
|
+
type SyntheticConfigUpdatedPayload,
|
|
11
|
+
} from "../../config";
|
|
12
|
+
import { getSyntheticApiKey } from "../../lib/env";
|
|
13
|
+
import type { QuotasResponse } from "../../types/quotas";
|
|
14
|
+
import { fetchQuotas, formatResetTime } from "../../utils/quotas";
|
|
15
|
+
import {
|
|
16
|
+
assessWindow,
|
|
17
|
+
getSeverityColor,
|
|
18
|
+
type RiskSeverity,
|
|
19
|
+
toWindows,
|
|
20
|
+
} from "../../utils/quotas-severity";
|
|
21
|
+
|
|
22
|
+
const EXTENSION_ID = "synthetic-usage";
|
|
23
|
+
const REFRESH_INTERVAL_MS = 60_000;
|
|
24
|
+
|
|
25
|
+
type WindowStatus = {
|
|
26
|
+
label: string;
|
|
27
|
+
usedPercent: number;
|
|
28
|
+
severity: RiskSeverity;
|
|
29
|
+
resetsAt: string | null;
|
|
30
|
+
limited: boolean;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
function parseSnapshot(quotas: QuotasResponse): WindowStatus[] {
|
|
34
|
+
const windows = toWindows(quotas);
|
|
35
|
+
return windows.map((w) => {
|
|
36
|
+
const assessment = assessWindow(w);
|
|
37
|
+
return {
|
|
38
|
+
label: w.label,
|
|
39
|
+
usedPercent: w.usedPercent,
|
|
40
|
+
severity: assessment.severity,
|
|
41
|
+
resetsAt: w.resetsAt.toISOString(),
|
|
42
|
+
limited: w.limited ?? false,
|
|
43
|
+
};
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const SHORT_LABELS: Record<string, string> = {
|
|
48
|
+
"Credits / week": "week",
|
|
49
|
+
"Requests / 5h": "5h",
|
|
50
|
+
"Search / hour": "search",
|
|
51
|
+
"Free Tool Calls / day": "tools",
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
function formatStatus(ctx: ExtensionContext, windows: WindowStatus[]): string {
|
|
55
|
+
const theme = ctx.ui.theme;
|
|
56
|
+
const parts: string[] = [];
|
|
57
|
+
|
|
58
|
+
for (const w of windows) {
|
|
59
|
+
const short = SHORT_LABELS[w.label] ?? w.label;
|
|
60
|
+
const remaining = Math.max(
|
|
61
|
+
0,
|
|
62
|
+
Math.min(100, Math.round(100 - w.usedPercent)),
|
|
63
|
+
);
|
|
64
|
+
const color = getSeverityColor(w.severity);
|
|
65
|
+
const pctText = theme.fg(color, `${remaining}%`);
|
|
66
|
+
const reset = w.resetsAt
|
|
67
|
+
? theme.fg("dim", ` (\u21ba${formatResetTime(w.resetsAt)})`)
|
|
68
|
+
: "";
|
|
69
|
+
const limitTag = w.limited ? theme.fg("error", " [limited]") : "";
|
|
70
|
+
parts.push(`${theme.fg("dim", `${short}:`)}${pctText}${reset}${limitTag}`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return parts.join(" ");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function createStatusRefresher() {
|
|
77
|
+
let refreshTimer: ReturnType<typeof setInterval> | undefined;
|
|
78
|
+
let activeContext: ExtensionContext | undefined;
|
|
79
|
+
let isRefreshInFlight = false;
|
|
80
|
+
let queuedRefresh = false;
|
|
81
|
+
let lastSnapshot: WindowStatus[] | undefined;
|
|
82
|
+
|
|
83
|
+
async function updateFooterStatus(ctx: ExtensionContext): Promise<void> {
|
|
84
|
+
if (!ctx.hasUI) return;
|
|
85
|
+
if (isRefreshInFlight) {
|
|
86
|
+
queuedRefresh = true;
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
isRefreshInFlight = true;
|
|
90
|
+
try {
|
|
91
|
+
const apiKey = await getSyntheticApiKey(ctx.modelRegistry.authStorage);
|
|
92
|
+
if (!apiKey) {
|
|
93
|
+
lastSnapshot = undefined;
|
|
94
|
+
ctx.ui.setStatus(EXTENSION_ID, undefined);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
const result = await fetchQuotas(apiKey);
|
|
98
|
+
if (!result.success) {
|
|
99
|
+
ctx.ui.setStatus(
|
|
100
|
+
EXTENSION_ID,
|
|
101
|
+
ctx.ui.theme.fg("warning", "usage unavailable"),
|
|
102
|
+
);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
const windows = parseSnapshot(result.data.quotas);
|
|
106
|
+
lastSnapshot = windows;
|
|
107
|
+
if (windows.length === 0) {
|
|
108
|
+
ctx.ui.setStatus(EXTENSION_ID, undefined);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
ctx.ui.setStatus(EXTENSION_ID, formatStatus(ctx, windows));
|
|
112
|
+
} catch {
|
|
113
|
+
ctx.ui.setStatus(
|
|
114
|
+
EXTENSION_ID,
|
|
115
|
+
ctx.ui.theme.fg("warning", "usage unavailable"),
|
|
116
|
+
);
|
|
117
|
+
} finally {
|
|
118
|
+
isRefreshInFlight = false;
|
|
119
|
+
if (queuedRefresh) {
|
|
120
|
+
queuedRefresh = false;
|
|
121
|
+
void updateFooterStatus(ctx);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function refreshFor(ctx: ExtensionContext): Promise<void> {
|
|
127
|
+
activeContext = ctx;
|
|
128
|
+
return updateFooterStatus(ctx);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function startAutoRefresh(): void {
|
|
132
|
+
if (refreshTimer) clearInterval(refreshTimer);
|
|
133
|
+
refreshTimer = setInterval(() => {
|
|
134
|
+
if (!activeContext) return;
|
|
135
|
+
void updateFooterStatus(activeContext);
|
|
136
|
+
}, REFRESH_INTERVAL_MS);
|
|
137
|
+
refreshTimer.unref?.();
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function stopAutoRefresh(ctx?: ExtensionContext): void {
|
|
141
|
+
if (refreshTimer) {
|
|
142
|
+
clearInterval(refreshTimer);
|
|
143
|
+
refreshTimer = undefined;
|
|
144
|
+
}
|
|
145
|
+
ctx?.ui.setStatus(EXTENSION_ID, undefined);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async function setLoadingStatus(ctx: ExtensionContext): Promise<void> {
|
|
149
|
+
if (!ctx.hasUI) return;
|
|
150
|
+
const apiKey = await getSyntheticApiKey(
|
|
151
|
+
ctx.modelRegistry.authStorage,
|
|
152
|
+
).catch(() => undefined);
|
|
153
|
+
if (!apiKey) {
|
|
154
|
+
ctx.ui.setStatus(EXTENSION_ID, undefined);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
ctx.ui.setStatus(EXTENSION_ID, ctx.ui.theme.fg("dim", "loading usage..."));
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function renderFromLastSnapshot(ctx: ExtensionContext): boolean {
|
|
161
|
+
if (!ctx.hasUI || !lastSnapshot) return false;
|
|
162
|
+
ctx.ui.setStatus(EXTENSION_ID, formatStatus(ctx, lastSnapshot));
|
|
163
|
+
return true;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return {
|
|
167
|
+
refreshFor,
|
|
168
|
+
startAutoRefresh,
|
|
169
|
+
stopAutoRefresh,
|
|
170
|
+
setLoadingStatus,
|
|
171
|
+
renderFromLastSnapshot,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export default async function (pi: ExtensionAPI) {
|
|
176
|
+
await configLoader.load();
|
|
177
|
+
|
|
178
|
+
const refresher = createStatusRefresher();
|
|
179
|
+
let enabled = configLoader.getConfig().usageStatus;
|
|
180
|
+
let currentContext: ExtensionContext | undefined;
|
|
181
|
+
let currentProvider: string | undefined;
|
|
182
|
+
|
|
183
|
+
pi.events.on(SYNTHETIC_CONFIG_UPDATED_EVENT, (data: unknown) => {
|
|
184
|
+
enabled = (data as SyntheticConfigUpdatedPayload).config.usageStatus;
|
|
185
|
+
|
|
186
|
+
if (!enabled) {
|
|
187
|
+
refresher.stopAutoRefresh(currentContext);
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (currentContext && currentProvider === "synthetic") {
|
|
192
|
+
refresher.startAutoRefresh();
|
|
193
|
+
void refresher.refreshFor(currentContext);
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
198
|
+
currentContext = ctx;
|
|
199
|
+
currentProvider = ctx.model?.provider;
|
|
200
|
+
if (!enabled || ctx.model?.provider !== "synthetic") return;
|
|
201
|
+
refresher.startAutoRefresh();
|
|
202
|
+
await refresher.setLoadingStatus(ctx);
|
|
203
|
+
await refresher.refreshFor(ctx);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
pi.on("turn_end", (_event, ctx) => {
|
|
207
|
+
currentContext = ctx;
|
|
208
|
+
currentProvider = ctx.model?.provider;
|
|
209
|
+
if (!enabled || ctx.model?.provider !== "synthetic") return;
|
|
210
|
+
void refresher.refreshFor(ctx);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
pi.on("session_switch", (_event, ctx) => {
|
|
214
|
+
currentContext = ctx;
|
|
215
|
+
currentProvider = ctx.model?.provider;
|
|
216
|
+
if (enabled && ctx.model?.provider === "synthetic") {
|
|
217
|
+
void refresher.refreshFor(ctx);
|
|
218
|
+
} else {
|
|
219
|
+
refresher.stopAutoRefresh(ctx);
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
pi.on("model_select", (_event, ctx) => {
|
|
224
|
+
currentContext = ctx;
|
|
225
|
+
currentProvider = ctx.model?.provider;
|
|
226
|
+
if (enabled && ctx.model?.provider === "synthetic") {
|
|
227
|
+
refresher.startAutoRefresh();
|
|
228
|
+
void refresher.refreshFor(ctx);
|
|
229
|
+
} else {
|
|
230
|
+
refresher.stopAutoRefresh(ctx);
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
pi.on("session_shutdown", (_event, ctx) => {
|
|
235
|
+
currentContext = undefined;
|
|
236
|
+
currentProvider = undefined;
|
|
237
|
+
refresher.stopAutoRefresh(ctx);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
pi.events.on(SYNTHETIC_EXTENSIONS_REQUEST_EVENT, () => {
|
|
241
|
+
pi.events.emit(SYNTHETIC_EXTENSIONS_REGISTER_EVENT, {
|
|
242
|
+
feature: "usageStatus",
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
}
|
|
@@ -1,6 +1,50 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
configLoader,
|
|
4
|
+
SYNTHETIC_CONFIG_UPDATED_EVENT,
|
|
5
|
+
SYNTHETIC_EXTENSIONS_REGISTER_EVENT,
|
|
6
|
+
SYNTHETIC_EXTENSIONS_REQUEST_EVENT,
|
|
7
|
+
type SyntheticConfigUpdatedPayload,
|
|
8
|
+
} from "../../config";
|
|
9
|
+
import {
|
|
10
|
+
registerSyntheticWebSearchTool,
|
|
11
|
+
SYNTHETIC_WEB_SEARCH_TOOL,
|
|
12
|
+
} from "./tool";
|
|
13
|
+
|
|
14
|
+
function syncToolActivation(pi: ExtensionAPI, enabled: boolean): void {
|
|
15
|
+
const allToolNames = new Set(pi.getAllTools().map((tool) => tool.name));
|
|
16
|
+
const activeTools = new Set(pi.getActiveTools());
|
|
17
|
+
|
|
18
|
+
if (!allToolNames.has(SYNTHETIC_WEB_SEARCH_TOOL)) return;
|
|
19
|
+
|
|
20
|
+
if (enabled) {
|
|
21
|
+
activeTools.add(SYNTHETIC_WEB_SEARCH_TOOL);
|
|
22
|
+
} else {
|
|
23
|
+
activeTools.delete(SYNTHETIC_WEB_SEARCH_TOOL);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
pi.setActiveTools([...activeTools].filter((name) => allToolNames.has(name)));
|
|
27
|
+
}
|
|
3
28
|
|
|
4
29
|
export default async function (pi: ExtensionAPI) {
|
|
30
|
+
await configLoader.load();
|
|
31
|
+
|
|
32
|
+
let webSearchEnabled = configLoader.getConfig().webSearch;
|
|
33
|
+
|
|
5
34
|
registerSyntheticWebSearchTool(pi);
|
|
35
|
+
|
|
36
|
+
pi.on("session_start", async () => {
|
|
37
|
+
syncToolActivation(pi, webSearchEnabled);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
pi.events.on(SYNTHETIC_CONFIG_UPDATED_EVENT, (data: unknown) => {
|
|
41
|
+
webSearchEnabled = (data as SyntheticConfigUpdatedPayload).config.webSearch;
|
|
42
|
+
syncToolActivation(pi, webSearchEnabled);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
pi.events.on(SYNTHETIC_EXTENSIONS_REQUEST_EVENT, () => {
|
|
46
|
+
pi.events.emit(SYNTHETIC_EXTENSIONS_REGISTER_EVENT, {
|
|
47
|
+
feature: "webSearch",
|
|
48
|
+
});
|
|
49
|
+
});
|
|
6
50
|
}
|
|
@@ -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 { configLoader } from "../../config";
|
|
12
13
|
import { getSyntheticApiKey } from "../../lib/env";
|
|
13
14
|
|
|
14
15
|
export const SYNTHETIC_WEB_SEARCH_TOOL = "synthetic_web_search" as const;
|
|
@@ -59,6 +60,12 @@ export function registerSyntheticWebSearchTool(pi: ExtensionAPI): void {
|
|
|
59
60
|
details: { query: params.query },
|
|
60
61
|
});
|
|
61
62
|
|
|
63
|
+
if (!configLoader.getConfig().webSearch) {
|
|
64
|
+
throw new Error(
|
|
65
|
+
"Synthetic web search is disabled. Re-enable it with synthetic:settings or pi config.",
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
62
69
|
const apiKey = await getSyntheticApiKey(ctx.modelRegistry.authStorage);
|
|
63
70
|
if (!apiKey) {
|
|
64
71
|
throw new Error(
|