@aliou/pi-synthetic 0.11.0 → 0.13.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 +22 -0
- package/package.json +6 -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 +14 -168
- package/src/extensions/command-quotas/index.ts +18 -3
- package/src/extensions/provider/index.ts +113 -0
- package/src/extensions/provider/models.ts +1 -1
- package/src/extensions/quota-warnings/index.ts +58 -0
- package/src/extensions/quota-warnings/notifier.test.ts +280 -0
- package/src/extensions/quota-warnings/notifier.ts +200 -0
- 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/src/utils/quotas-severity.test.ts +278 -0
- package/src/utils/quotas-severity.ts +272 -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.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"private": false,
|
|
@@ -17,7 +17,10 @@
|
|
|
17
17
|
"extensions": [
|
|
18
18
|
"./src/extensions/provider/index.ts",
|
|
19
19
|
"./src/extensions/web-search/index.ts",
|
|
20
|
-
"./src/extensions/command-quotas/index.ts"
|
|
20
|
+
"./src/extensions/command-quotas/index.ts",
|
|
21
|
+
"./src/extensions/sub-bar-integration/index.ts",
|
|
22
|
+
"./src/extensions/quota-warnings/index.ts",
|
|
23
|
+
"./src/extensions/usage-status/index.ts"
|
|
21
24
|
],
|
|
22
25
|
"video": "https://assets.aliou.me/pi-extensions/demos/pi-synthetic.mp4"
|
|
23
26
|
},
|
|
@@ -33,6 +36,7 @@
|
|
|
33
36
|
"@mariozechner/pi-tui": "0.61.0"
|
|
34
37
|
},
|
|
35
38
|
"dependencies": {
|
|
39
|
+
"@aliou/pi-utils-settings": "^0.13.0",
|
|
36
40
|
"@aliou/pi-utils-ui": "^0.1.2"
|
|
37
41
|
},
|
|
38
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");
|
|
@@ -3,167 +3,19 @@ import { DynamicBorder } from "@mariozechner/pi-coding-agent";
|
|
|
3
3
|
import type { Component, TUI } from "@mariozechner/pi-tui";
|
|
4
4
|
import { Loader, matchesKey, truncateToWidth } from "@mariozechner/pi-tui";
|
|
5
5
|
import type { QuotasResponse } from "../../../types/quotas";
|
|
6
|
+
import {
|
|
7
|
+
assessWindow,
|
|
8
|
+
formatTimeRemaining,
|
|
9
|
+
getSeverityColor,
|
|
10
|
+
type QuotaWindow,
|
|
11
|
+
toWindows,
|
|
12
|
+
} from "../../../utils/quotas-severity";
|
|
6
13
|
|
|
7
14
|
type QuotasState =
|
|
8
15
|
| { type: "loading" }
|
|
9
16
|
| { type: "error"; message: string }
|
|
10
17
|
| { type: "loaded"; quotas: QuotasResponse };
|
|
11
18
|
|
|
12
|
-
interface QuotaWindow {
|
|
13
|
-
label: string;
|
|
14
|
-
usedPercent: number;
|
|
15
|
-
resetsAt: Date;
|
|
16
|
-
windowSeconds: number;
|
|
17
|
-
usedValue: number;
|
|
18
|
-
limitValue: number;
|
|
19
|
-
isCurrency?: boolean;
|
|
20
|
-
showPace?: boolean;
|
|
21
|
-
paceScale?: number;
|
|
22
|
-
limited?: boolean;
|
|
23
|
-
nextAmount?: string;
|
|
24
|
-
nextLabel?: string;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/** Safely compute percentage, guarding against division by zero */
|
|
28
|
-
function safePercent(used: number, limit: number): number {
|
|
29
|
-
if (!Number.isFinite(used) || !Number.isFinite(limit) || limit <= 0) return 0;
|
|
30
|
-
return Math.max(0, Math.min(100, (used / limit) * 100));
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/** Parse currency string like "$1,234.56" to number */
|
|
34
|
-
function parseCurrency(value: string): number {
|
|
35
|
-
const n = Number(value.replace(/[^0-9.-]/g, ""));
|
|
36
|
-
return Number.isFinite(n) ? n : 0;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function toWindows(quotas: QuotasResponse): QuotaWindow[] {
|
|
40
|
-
const windows: QuotaWindow[] = [];
|
|
41
|
-
|
|
42
|
-
if (quotas.weeklyTokenLimit) {
|
|
43
|
-
const { weeklyTokenLimit } = quotas;
|
|
44
|
-
const limitValue = parseCurrency(weeklyTokenLimit.maxCredits);
|
|
45
|
-
const remainingValue = parseCurrency(weeklyTokenLimit.remainingCredits);
|
|
46
|
-
windows.push({
|
|
47
|
-
label: "Credits / week",
|
|
48
|
-
usedPercent: Math.max(
|
|
49
|
-
0,
|
|
50
|
-
Math.min(100, 100 - weeklyTokenLimit.percentRemaining),
|
|
51
|
-
),
|
|
52
|
-
resetsAt: new Date(weeklyTokenLimit.nextRegenAt),
|
|
53
|
-
windowSeconds: 24 * 60 * 60,
|
|
54
|
-
usedValue: limitValue - remainingValue,
|
|
55
|
-
limitValue,
|
|
56
|
-
isCurrency: true,
|
|
57
|
-
showPace: true,
|
|
58
|
-
paceScale: 1 / 7,
|
|
59
|
-
nextAmount: `+${weeklyTokenLimit.nextRegenCredits}`,
|
|
60
|
-
nextLabel: "Next regen",
|
|
61
|
-
});
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
if (quotas.rollingFiveHourLimit && quotas.rollingFiveHourLimit.max > 0) {
|
|
65
|
-
const { rollingFiveHourLimit } = quotas;
|
|
66
|
-
const used = rollingFiveHourLimit.max - rollingFiveHourLimit.remaining;
|
|
67
|
-
const tickAmount =
|
|
68
|
-
rollingFiveHourLimit.tickPercent * rollingFiveHourLimit.max;
|
|
69
|
-
windows.push({
|
|
70
|
-
label: "Requests / 5h",
|
|
71
|
-
usedPercent: safePercent(used, rollingFiveHourLimit.max),
|
|
72
|
-
resetsAt: new Date(rollingFiveHourLimit.nextTickAt),
|
|
73
|
-
windowSeconds: 5 * 60 * 60,
|
|
74
|
-
usedValue: Math.round(used),
|
|
75
|
-
limitValue: rollingFiveHourLimit.max,
|
|
76
|
-
showPace: false,
|
|
77
|
-
limited: rollingFiveHourLimit.limited,
|
|
78
|
-
nextAmount: `+${tickAmount.toFixed(1)}`,
|
|
79
|
-
nextLabel: "Next tick",
|
|
80
|
-
});
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
if (quotas.search?.hourly?.limit && quotas.search.hourly.limit > 0) {
|
|
84
|
-
const { hourly } = quotas.search;
|
|
85
|
-
windows.push({
|
|
86
|
-
label: "Search / hour",
|
|
87
|
-
usedPercent: safePercent(hourly.requests, hourly.limit),
|
|
88
|
-
resetsAt: new Date(hourly.renewsAt),
|
|
89
|
-
windowSeconds: 60 * 60,
|
|
90
|
-
usedValue: hourly.requests,
|
|
91
|
-
limitValue: hourly.limit,
|
|
92
|
-
showPace: true,
|
|
93
|
-
paceScale: 1,
|
|
94
|
-
nextLabel: "Resets",
|
|
95
|
-
});
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
if (quotas.freeToolCalls?.limit && quotas.freeToolCalls.limit > 0) {
|
|
99
|
-
windows.push({
|
|
100
|
-
label: "Free Tool Calls / day",
|
|
101
|
-
usedPercent: safePercent(
|
|
102
|
-
quotas.freeToolCalls.requests,
|
|
103
|
-
quotas.freeToolCalls.limit,
|
|
104
|
-
),
|
|
105
|
-
resetsAt: new Date(quotas.freeToolCalls.renewsAt),
|
|
106
|
-
windowSeconds: 24 * 60 * 60,
|
|
107
|
-
usedValue: quotas.freeToolCalls.requests,
|
|
108
|
-
limitValue: quotas.freeToolCalls.limit,
|
|
109
|
-
showPace: true,
|
|
110
|
-
paceScale: 1,
|
|
111
|
-
nextLabel: "Resets",
|
|
112
|
-
});
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
return windows;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
function getPacePercent(window: QuotaWindow): number | null {
|
|
119
|
-
const totalMs = window.windowSeconds * 1000;
|
|
120
|
-
if (totalMs <= 0) return null;
|
|
121
|
-
const remainingMs = window.resetsAt.getTime() - Date.now();
|
|
122
|
-
const elapsedMs = totalMs - remainingMs;
|
|
123
|
-
return Math.max(0, Math.min(100, (elapsedMs / totalMs) * 100));
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
function getProjectedPercent(
|
|
127
|
-
usedPercent: number,
|
|
128
|
-
pacePercent: number | null,
|
|
129
|
-
): number {
|
|
130
|
-
if (pacePercent === null) return usedPercent;
|
|
131
|
-
const effectivePace = Math.max(5, pacePercent);
|
|
132
|
-
return Math.max(0, (usedPercent / effectivePace) * 100);
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
function getSeverity(
|
|
136
|
-
projectedPercent: number,
|
|
137
|
-
pacePercent: number | null,
|
|
138
|
-
): "success" | "warning" | "error" {
|
|
139
|
-
if (pacePercent === null) {
|
|
140
|
-
if (projectedPercent >= 100) return "error";
|
|
141
|
-
if (projectedPercent >= 90) return "warning";
|
|
142
|
-
return "success";
|
|
143
|
-
}
|
|
144
|
-
// Dynamic thresholds based on window progress
|
|
145
|
-
const progress = pacePercent / 100;
|
|
146
|
-
const warnThreshold = 260 - (260 - 120) * progress;
|
|
147
|
-
const highThreshold = 320 - (320 - 145) * progress;
|
|
148
|
-
const criticalThreshold = 400 - (400 - 170) * progress;
|
|
149
|
-
|
|
150
|
-
if (projectedPercent >= criticalThreshold) return "error";
|
|
151
|
-
if (projectedPercent >= highThreshold) return "error";
|
|
152
|
-
if (projectedPercent >= warnThreshold) return "warning";
|
|
153
|
-
return "success";
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
function formatTimeRemaining(date: Date): string {
|
|
157
|
-
const ms = date.getTime() - Date.now();
|
|
158
|
-
if (ms <= 0) return "now";
|
|
159
|
-
const totalMins = Math.ceil(ms / (1000 * 60));
|
|
160
|
-
const hours = Math.floor(totalMins / 60);
|
|
161
|
-
const mins = totalMins % 60;
|
|
162
|
-
if (hours >= 1) return mins > 0 ? `${hours}h${mins}m` : `${hours}h`;
|
|
163
|
-
const totalSecs = Math.ceil(ms / 1000);
|
|
164
|
-
return totalMins >= 1 ? `${totalMins}m` : `${totalSecs}s`;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
19
|
/**
|
|
168
20
|
* Convert a foreground ANSI escape to its background equivalent.
|
|
169
21
|
* Handles truecolor (38;2), 256-color (38;5), and basic (3X) escapes.
|
|
@@ -298,7 +150,6 @@ export class QuotasComponent implements Component {
|
|
|
298
150
|
width,
|
|
299
151
|
),
|
|
300
152
|
);
|
|
301
|
-
lines.push("");
|
|
302
153
|
|
|
303
154
|
switch (this.state.type) {
|
|
304
155
|
case "loading":
|
|
@@ -334,6 +185,8 @@ export class QuotasComponent implements Component {
|
|
|
334
185
|
const windows = toWindows(quotas);
|
|
335
186
|
const barWidth = Math.min(50, Math.max(20, contentWidth - 20));
|
|
336
187
|
|
|
188
|
+
lines.push("");
|
|
189
|
+
|
|
337
190
|
for (const window of windows) {
|
|
338
191
|
lines.push(...this.renderWindow(window, barWidth, maxWidth));
|
|
339
192
|
lines.push("");
|
|
@@ -355,15 +208,8 @@ export class QuotasComponent implements Component {
|
|
|
355
208
|
const lines: string[] = [];
|
|
356
209
|
const theme = this.theme;
|
|
357
210
|
|
|
358
|
-
const
|
|
359
|
-
const
|
|
360
|
-
rawPace !== null ? rawPace * (window.paceScale ?? 1) : null;
|
|
361
|
-
const projectedPercent = getProjectedPercent(
|
|
362
|
-
window.usedPercent,
|
|
363
|
-
pacePercent,
|
|
364
|
-
);
|
|
365
|
-
let severity = getSeverity(projectedPercent, pacePercent);
|
|
366
|
-
if (window.limited) severity = "error";
|
|
211
|
+
const assessment = assessWindow(window);
|
|
212
|
+
const color = getSeverityColor(assessment.severity);
|
|
367
213
|
|
|
368
214
|
// Label
|
|
369
215
|
lines.push(
|
|
@@ -375,8 +221,8 @@ export class QuotasComponent implements Component {
|
|
|
375
221
|
window.usedPercent,
|
|
376
222
|
barWidth,
|
|
377
223
|
theme,
|
|
378
|
-
|
|
379
|
-
pacePercent,
|
|
224
|
+
color,
|
|
225
|
+
assessment.pacePercent,
|
|
380
226
|
);
|
|
381
227
|
const usedStr = window.isCurrency
|
|
382
228
|
? `${Math.round(window.usedPercent)}%/$${window.limitValue.toFixed(2)}`
|
|
@@ -384,7 +230,7 @@ export class QuotasComponent implements Component {
|
|
|
384
230
|
const limitedBadge = window.limited ? theme.fg("error", " LIMITED") : "";
|
|
385
231
|
lines.push(
|
|
386
232
|
truncateToWidth(
|
|
387
|
-
` ${bar} ${theme.fg(
|
|
233
|
+
` ${bar} ${theme.fg(color, usedStr)}${limitedBadge}`,
|
|
388
234
|
maxWidth,
|
|
389
235
|
),
|
|
390
236
|
);
|
|
@@ -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
|
}
|