@aliou/pi-synthetic 0.15.0 → 0.16.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/package.json +1 -1
- package/src/extensions/provider/index.ts +67 -1
- package/src/extensions/quota-warnings/index.ts +79 -24
- package/src/extensions/sub-bar-integration/index.ts +33 -83
- package/src/extensions/usage-status/index.ts +68 -143
- package/src/services/quota-store.test.ts +211 -0
- package/src/services/quota-store.ts +112 -0
- package/src/services/quota-warnings.test.ts +393 -0
- package/src/services/quota-warnings.ts +149 -0
- package/src/types/quotas.ts +47 -0
- package/src/extensions/quota-warnings/notifier.test.ts +0 -280
- package/src/extensions/quota-warnings/notifier.ts +0 -200
package/package.json
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
1
|
+
import type { AuthStorage, ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
2
2
|
import {
|
|
3
3
|
configLoader,
|
|
4
4
|
emitSyntheticConfigUpdated,
|
|
@@ -12,6 +12,18 @@ import {
|
|
|
12
12
|
type SyntheticFeatureId,
|
|
13
13
|
seedSyntheticConfigIfMissing,
|
|
14
14
|
} from "../../config";
|
|
15
|
+
import { getSyntheticApiKey } from "../../lib/env";
|
|
16
|
+
import { QuotaStore } from "../../services/quota-store";
|
|
17
|
+
import {
|
|
18
|
+
parseQuotaHeader,
|
|
19
|
+
type QuotasResponse,
|
|
20
|
+
SYNTHETIC_QUOTAS_READ_EVENT,
|
|
21
|
+
SYNTHETIC_QUOTAS_REQUEST_EVENT,
|
|
22
|
+
SYNTHETIC_QUOTAS_UPDATED_EVENT,
|
|
23
|
+
type SyntheticQuotasReadPayload,
|
|
24
|
+
type SyntheticQuotasRequestPayload,
|
|
25
|
+
} from "../../types/quotas";
|
|
26
|
+
import { fetchQuotas } from "../../utils/quotas";
|
|
15
27
|
import { SYNTHETIC_MODELS } from "./models";
|
|
16
28
|
|
|
17
29
|
export function buildSyntheticProviderModels(includeProxiedModels: boolean) {
|
|
@@ -71,6 +83,54 @@ export default async function (pi: ExtensionAPI) {
|
|
|
71
83
|
getLoadedFeatures: () => loadedFeatures,
|
|
72
84
|
});
|
|
73
85
|
|
|
86
|
+
const quotaStore = new QuotaStore();
|
|
87
|
+
let currentAuthStorage: AuthStorage | undefined;
|
|
88
|
+
|
|
89
|
+
async function fetchQuotasFromAuth(): Promise<QuotasResponse | undefined> {
|
|
90
|
+
if (!currentAuthStorage) return undefined;
|
|
91
|
+
const apiKey = await getSyntheticApiKey(currentAuthStorage);
|
|
92
|
+
if (!apiKey) return undefined;
|
|
93
|
+
const result = await fetchQuotas(apiKey);
|
|
94
|
+
return result.success ? result.data.quotas : undefined;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
quotaStore.subscribe((snapshot) => {
|
|
98
|
+
pi.events.emit(SYNTHETIC_QUOTAS_UPDATED_EVENT, {
|
|
99
|
+
quotas: snapshot.quotas,
|
|
100
|
+
source: snapshot.source,
|
|
101
|
+
updatedAt: snapshot.updatedAt,
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
pi.on("after_provider_response", (event, ctx) => {
|
|
106
|
+
if (ctx.model?.provider !== "synthetic") return;
|
|
107
|
+
const quotas = parseQuotaHeader(event.headers);
|
|
108
|
+
if (quotas) quotaStore.ingest(quotas, "header");
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
pi.events.on(SYNTHETIC_QUOTAS_REQUEST_EVENT, async (data: unknown) => {
|
|
112
|
+
const payload = data as SyntheticQuotasRequestPayload | undefined;
|
|
113
|
+
const snapshot = await quotaStore.refreshFromApi(fetchQuotasFromAuth);
|
|
114
|
+
if (payload?.respond) {
|
|
115
|
+
payload.respond(snapshot);
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
pi.events.on(SYNTHETIC_QUOTAS_READ_EVENT, (data: unknown) => {
|
|
120
|
+
const { respond } = data as SyntheticQuotasReadPayload;
|
|
121
|
+
respond(quotaStore.getSnapshot());
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
pi.on("session_before_switch", () => {
|
|
125
|
+
quotaStore.clear();
|
|
126
|
+
currentAuthStorage = undefined;
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
pi.on("session_shutdown", () => {
|
|
130
|
+
quotaStore.clear();
|
|
131
|
+
currentAuthStorage = undefined;
|
|
132
|
+
});
|
|
133
|
+
|
|
74
134
|
pi.on("session_start", async (_event, ctx) => {
|
|
75
135
|
const messages = pendingMessages.splice(0).map((m) => `- ${m}`);
|
|
76
136
|
if (messages.length > 0) {
|
|
@@ -81,7 +141,13 @@ export default async function (pi: ExtensionAPI) {
|
|
|
81
141
|
}
|
|
82
142
|
|
|
83
143
|
loadedFeatures.clear();
|
|
144
|
+
quotaStore.clear();
|
|
145
|
+
currentAuthStorage = ctx.modelRegistry.authStorage;
|
|
84
146
|
pi.events.emit(SYNTHETIC_EXTENSIONS_REQUEST_EVENT, undefined);
|
|
85
147
|
emitSyntheticConfigUpdated(pi);
|
|
148
|
+
|
|
149
|
+
if (ctx.model?.provider === "synthetic") {
|
|
150
|
+
await quotaStore.refreshFromApi(fetchQuotasFromAuth);
|
|
151
|
+
}
|
|
86
152
|
});
|
|
87
153
|
}
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
ExtensionAPI,
|
|
3
|
+
ExtensionContext,
|
|
4
|
+
} from "@mariozechner/pi-coding-agent";
|
|
2
5
|
import {
|
|
3
6
|
configLoader,
|
|
4
7
|
SYNTHETIC_CONFIG_UPDATED_EVENT,
|
|
@@ -6,48 +9,100 @@ import {
|
|
|
6
9
|
SYNTHETIC_EXTENSIONS_REQUEST_EVENT,
|
|
7
10
|
type SyntheticConfigUpdatedPayload,
|
|
8
11
|
} from "../../config";
|
|
9
|
-
import {
|
|
12
|
+
import { QuotaWarningNotifier } from "../../services/quota-warnings";
|
|
13
|
+
import {
|
|
14
|
+
SYNTHETIC_QUOTAS_READ_EVENT,
|
|
15
|
+
SYNTHETIC_QUOTAS_REQUEST_EVENT,
|
|
16
|
+
type SyntheticQuotasReadPayload,
|
|
17
|
+
type SyntheticQuotasRequestPayload,
|
|
18
|
+
type SyntheticQuotasSnapshotPayload,
|
|
19
|
+
} from "../../types/quotas";
|
|
10
20
|
|
|
11
21
|
export default async function (pi: ExtensionAPI) {
|
|
12
22
|
await configLoader.load();
|
|
13
23
|
|
|
14
24
|
let enabled = configLoader.getConfig().quotaWarnings;
|
|
15
|
-
|
|
16
|
-
|
|
25
|
+
|
|
26
|
+
const notifier = new QuotaWarningNotifier();
|
|
27
|
+
|
|
28
|
+
function requestQuotas(
|
|
29
|
+
respond?: (snapshot: SyntheticQuotasSnapshotPayload | undefined) => void,
|
|
30
|
+
): void {
|
|
31
|
+
pi.events.emit(SYNTHETIC_QUOTAS_REQUEST_EVENT, {
|
|
32
|
+
respond,
|
|
33
|
+
} satisfies SyntheticQuotasRequestPayload);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function readQuotas(
|
|
37
|
+
respond: (snapshot: SyntheticQuotasSnapshotPayload | undefined) => void,
|
|
38
|
+
): void {
|
|
39
|
+
pi.events.emit(SYNTHETIC_QUOTAS_READ_EVENT, {
|
|
40
|
+
respond,
|
|
41
|
+
} satisfies SyntheticQuotasReadPayload);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function evaluateFromStoreOrRefresh(ctx: ExtensionContext): void {
|
|
45
|
+
if (!enabled || ctx.model?.provider !== "synthetic") return;
|
|
46
|
+
readQuotas((snapshot) => {
|
|
47
|
+
if (snapshot) {
|
|
48
|
+
notifier.evaluate(
|
|
49
|
+
snapshot.quotas,
|
|
50
|
+
snapshot.source === "header",
|
|
51
|
+
(message, level) => {
|
|
52
|
+
ctx.ui.notify(message, level);
|
|
53
|
+
},
|
|
54
|
+
);
|
|
55
|
+
} else {
|
|
56
|
+
requestQuotas((refreshed) => {
|
|
57
|
+
if (!refreshed) return;
|
|
58
|
+
notifier.evaluate(
|
|
59
|
+
refreshed.quotas,
|
|
60
|
+
refreshed.source === "header",
|
|
61
|
+
(message, level) => {
|
|
62
|
+
ctx.ui.notify(message, level);
|
|
63
|
+
},
|
|
64
|
+
);
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
}
|
|
17
69
|
|
|
18
70
|
pi.events.on(SYNTHETIC_CONFIG_UPDATED_EVENT, (data: unknown) => {
|
|
19
71
|
enabled = (data as SyntheticConfigUpdatedPayload).config.quotaWarnings;
|
|
20
72
|
|
|
21
73
|
if (!enabled) {
|
|
22
|
-
clearAlertState();
|
|
74
|
+
notifier.clearAlertState();
|
|
23
75
|
return;
|
|
24
76
|
}
|
|
25
77
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
triggerCheck(currentContext, currentModel, false);
|
|
29
|
-
}
|
|
78
|
+
notifier.clearAlertState();
|
|
79
|
+
// In config updates we don't have ctx, so we just clear. The next lifecycle event will refresh.
|
|
30
80
|
});
|
|
31
81
|
|
|
32
|
-
pi.on("session_start",
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
if (!enabled || ctx.model?.provider !== "synthetic") return;
|
|
36
|
-
clearAlertState();
|
|
37
|
-
triggerCheck(ctx, ctx.model, false);
|
|
82
|
+
pi.on("session_start", (_event, ctx) => {
|
|
83
|
+
notifier.clearAlertState();
|
|
84
|
+
evaluateFromStoreOrRefresh(ctx);
|
|
38
85
|
});
|
|
39
86
|
|
|
40
|
-
pi.on("
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
87
|
+
pi.on("model_select", (_event, ctx) => {
|
|
88
|
+
notifier.clearAlertState();
|
|
89
|
+
evaluateFromStoreOrRefresh(ctx);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
pi.on("agent_end", (_event, ctx) => {
|
|
93
|
+
evaluateFromStoreOrRefresh(ctx);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
pi.on("turn_end", (_event, ctx) => {
|
|
97
|
+
evaluateFromStoreOrRefresh(ctx);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
pi.on("session_before_switch", () => {
|
|
101
|
+
notifier.clearAlertState();
|
|
45
102
|
});
|
|
46
103
|
|
|
47
|
-
pi.on("session_shutdown",
|
|
48
|
-
|
|
49
|
-
currentModel = undefined;
|
|
50
|
-
clearAlertState();
|
|
104
|
+
pi.on("session_shutdown", () => {
|
|
105
|
+
notifier.clearAlertState();
|
|
51
106
|
});
|
|
52
107
|
|
|
53
108
|
pi.events.on(SYNTHETIC_EXTENSIONS_REQUEST_EVENT, () => {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
2
2
|
import {
|
|
3
3
|
configLoader,
|
|
4
4
|
SYNTHETIC_CONFIG_UPDATED_EVENT,
|
|
@@ -6,9 +6,13 @@ import {
|
|
|
6
6
|
SYNTHETIC_EXTENSIONS_REQUEST_EVENT,
|
|
7
7
|
type SyntheticConfigUpdatedPayload,
|
|
8
8
|
} from "../../config";
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
import {
|
|
10
|
+
type QuotasResponse,
|
|
11
|
+
SYNTHETIC_QUOTAS_REQUEST_EVENT,
|
|
12
|
+
SYNTHETIC_QUOTAS_UPDATED_EVENT,
|
|
13
|
+
type SyntheticQuotasUpdatedPayload,
|
|
14
|
+
} from "../../types/quotas";
|
|
15
|
+
import { formatResetTime } from "../../utils/quotas";
|
|
12
16
|
|
|
13
17
|
interface RateWindow {
|
|
14
18
|
label: string;
|
|
@@ -24,14 +28,6 @@ interface UsageSnapshot {
|
|
|
24
28
|
lastSuccessAt?: number;
|
|
25
29
|
}
|
|
26
30
|
|
|
27
|
-
interface SubCoreSettingsPayload {
|
|
28
|
-
settings?: {
|
|
29
|
-
behavior?: {
|
|
30
|
-
refreshInterval: number;
|
|
31
|
-
};
|
|
32
|
-
};
|
|
33
|
-
}
|
|
34
|
-
|
|
35
31
|
function toUsageSnapshot(quotas: QuotasResponse): UsageSnapshot {
|
|
36
32
|
const windows: RateWindow[] = [];
|
|
37
33
|
|
|
@@ -105,64 +101,42 @@ function toUsageSnapshot(quotas: QuotasResponse): UsageSnapshot {
|
|
|
105
101
|
};
|
|
106
102
|
}
|
|
107
103
|
|
|
108
|
-
async function emitCurrentUsage(
|
|
109
|
-
pi: ExtensionAPI,
|
|
110
|
-
authStorage: AuthStorage,
|
|
111
|
-
): Promise<void> {
|
|
112
|
-
const apiKey = await getSyntheticApiKey(authStorage);
|
|
113
|
-
if (!apiKey) return;
|
|
114
|
-
const result = await fetchQuotas(apiKey);
|
|
115
|
-
if (!result.success) return;
|
|
116
|
-
pi.events.emit("sub-core:update-current", {
|
|
117
|
-
state: {
|
|
118
|
-
provider: "synthetic",
|
|
119
|
-
usage: toUsageSnapshot(result.data.quotas),
|
|
120
|
-
},
|
|
121
|
-
});
|
|
122
|
-
}
|
|
123
|
-
|
|
124
104
|
export function registerSubBarIntegration(pi: ExtensionAPI): void {
|
|
125
|
-
let interval: NodeJS.Timeout | undefined;
|
|
126
|
-
let refreshMs = 60000;
|
|
127
105
|
let subCoreReady = false;
|
|
128
106
|
let currentProvider: string | undefined;
|
|
129
|
-
let currentAuthStorage: AuthStorage | undefined;
|
|
130
107
|
let enabled = configLoader.getConfig().subBarIntegration;
|
|
131
108
|
|
|
132
109
|
function isSynthetic(): boolean {
|
|
133
110
|
return enabled && currentProvider === "synthetic";
|
|
134
111
|
}
|
|
135
112
|
|
|
136
|
-
function
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
113
|
+
function emitUsage(quotas: QuotasResponse): void {
|
|
114
|
+
pi.events.emit("sub-core:update-current", {
|
|
115
|
+
state: {
|
|
116
|
+
provider: "synthetic",
|
|
117
|
+
usage: toUsageSnapshot(quotas),
|
|
118
|
+
},
|
|
119
|
+
});
|
|
141
120
|
}
|
|
142
121
|
|
|
143
|
-
function
|
|
144
|
-
|
|
145
|
-
currentAuthStorage = authStorage;
|
|
146
|
-
void emitCurrentUsage(pi, authStorage);
|
|
147
|
-
const ms = Math.max(10000, refreshMs);
|
|
148
|
-
interval = setInterval(() => {
|
|
149
|
-
if (isSynthetic() && currentAuthStorage) {
|
|
150
|
-
void emitCurrentUsage(pi, currentAuthStorage);
|
|
151
|
-
}
|
|
152
|
-
}, ms);
|
|
153
|
-
interval.unref?.();
|
|
122
|
+
function requestQuotas(): void {
|
|
123
|
+
pi.events.emit(SYNTHETIC_QUOTAS_REQUEST_EVENT, undefined);
|
|
154
124
|
}
|
|
155
125
|
|
|
126
|
+
// Receive quota updates from the provider extension
|
|
127
|
+
pi.events.on(SYNTHETIC_QUOTAS_UPDATED_EVENT, (data: unknown) => {
|
|
128
|
+
if (!isSynthetic() || !subCoreReady) return;
|
|
129
|
+
const { quotas } = data as SyntheticQuotasUpdatedPayload;
|
|
130
|
+
emitUsage(quotas);
|
|
131
|
+
});
|
|
132
|
+
|
|
156
133
|
pi.events.on(SYNTHETIC_CONFIG_UPDATED_EVENT, (data: unknown) => {
|
|
157
134
|
enabled = (data as SyntheticConfigUpdatedPayload).config.subBarIntegration;
|
|
158
135
|
|
|
159
|
-
if (!enabled)
|
|
160
|
-
stop();
|
|
161
|
-
return;
|
|
162
|
-
}
|
|
136
|
+
if (!enabled) return;
|
|
163
137
|
|
|
164
|
-
if (subCoreReady &&
|
|
165
|
-
|
|
138
|
+
if (subCoreReady && currentProvider === "synthetic") {
|
|
139
|
+
requestQuotas();
|
|
166
140
|
}
|
|
167
141
|
});
|
|
168
142
|
|
|
@@ -170,48 +144,24 @@ export function registerSubBarIntegration(pi: ExtensionAPI): void {
|
|
|
170
144
|
subCoreReady = true;
|
|
171
145
|
});
|
|
172
146
|
|
|
173
|
-
pi.
|
|
174
|
-
|
|
175
|
-
if (payload.settings?.behavior?.refreshInterval) {
|
|
176
|
-
refreshMs = payload.settings.behavior.refreshInterval * 1000;
|
|
177
|
-
if (interval && isSynthetic() && currentAuthStorage) {
|
|
178
|
-
startPolling(currentAuthStorage);
|
|
179
|
-
}
|
|
180
|
-
}
|
|
147
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
148
|
+
currentProvider = ctx.model?.provider;
|
|
181
149
|
});
|
|
182
150
|
|
|
183
|
-
pi.on("
|
|
151
|
+
pi.on("model_select", async (_event, ctx) => {
|
|
184
152
|
currentProvider = ctx.model?.provider;
|
|
185
|
-
currentAuthStorage = ctx.modelRegistry.authStorage;
|
|
186
153
|
|
|
187
154
|
if (subCoreReady && isSynthetic()) {
|
|
188
|
-
|
|
189
|
-
if (apiKey) {
|
|
190
|
-
startPolling(currentAuthStorage);
|
|
191
|
-
}
|
|
155
|
+
requestQuotas();
|
|
192
156
|
}
|
|
193
157
|
});
|
|
194
158
|
|
|
195
|
-
pi.on("
|
|
196
|
-
currentProvider =
|
|
197
|
-
currentAuthStorage = ctx.modelRegistry.authStorage;
|
|
198
|
-
|
|
199
|
-
if (subCoreReady && isSynthetic()) {
|
|
200
|
-
const apiKey = await getSyntheticApiKey(currentAuthStorage);
|
|
201
|
-
if (apiKey) {
|
|
202
|
-
startPolling(currentAuthStorage);
|
|
203
|
-
} else {
|
|
204
|
-
stop();
|
|
205
|
-
}
|
|
206
|
-
} else {
|
|
207
|
-
stop();
|
|
208
|
-
}
|
|
159
|
+
pi.on("session_before_switch", (_event, ctx) => {
|
|
160
|
+
currentProvider = ctx.model?.provider;
|
|
209
161
|
});
|
|
210
162
|
|
|
211
163
|
pi.on("session_shutdown", () => {
|
|
212
164
|
currentProvider = undefined;
|
|
213
|
-
currentAuthStorage = undefined;
|
|
214
|
-
stop();
|
|
215
165
|
});
|
|
216
166
|
}
|
|
217
167
|
|
|
@@ -9,9 +9,15 @@ import {
|
|
|
9
9
|
SYNTHETIC_EXTENSIONS_REQUEST_EVENT,
|
|
10
10
|
type SyntheticConfigUpdatedPayload,
|
|
11
11
|
} from "../../config";
|
|
12
|
-
import {
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
import {
|
|
13
|
+
type QuotasResponse,
|
|
14
|
+
SYNTHETIC_QUOTAS_READ_EVENT,
|
|
15
|
+
SYNTHETIC_QUOTAS_REQUEST_EVENT,
|
|
16
|
+
type SyntheticQuotasReadPayload,
|
|
17
|
+
type SyntheticQuotasRequestPayload,
|
|
18
|
+
type SyntheticQuotasSnapshotPayload,
|
|
19
|
+
} from "../../types/quotas";
|
|
20
|
+
import { formatResetTime } from "../../utils/quotas";
|
|
15
21
|
import {
|
|
16
22
|
assessWindow,
|
|
17
23
|
getSeverityColor,
|
|
@@ -20,7 +26,6 @@ import {
|
|
|
20
26
|
} from "../../utils/quotas-severity";
|
|
21
27
|
|
|
22
28
|
const EXTENSION_ID = "synthetic-usage";
|
|
23
|
-
const REFRESH_INTERVAL_MS = 60_000;
|
|
24
29
|
|
|
25
30
|
type WindowStatus = {
|
|
26
31
|
label: string;
|
|
@@ -73,175 +78,95 @@ function formatStatus(ctx: ExtensionContext, windows: WindowStatus[]): string {
|
|
|
73
78
|
return parts.join(" ");
|
|
74
79
|
}
|
|
75
80
|
|
|
76
|
-
function
|
|
77
|
-
|
|
78
|
-
let activeContext: ExtensionContext | undefined;
|
|
79
|
-
let isRefreshInFlight = false;
|
|
80
|
-
let queuedRefresh = false;
|
|
81
|
-
let lastSnapshot: WindowStatus[] | undefined;
|
|
81
|
+
export default async function (pi: ExtensionAPI) {
|
|
82
|
+
await configLoader.load();
|
|
82
83
|
|
|
83
|
-
|
|
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
|
-
}
|
|
84
|
+
let enabled = configLoader.getConfig().usageStatus;
|
|
125
85
|
|
|
126
|
-
function
|
|
127
|
-
|
|
128
|
-
|
|
86
|
+
function requestQuotas(
|
|
87
|
+
respond: (snapshot: SyntheticQuotasSnapshotPayload | undefined) => void,
|
|
88
|
+
): void {
|
|
89
|
+
pi.events.emit(SYNTHETIC_QUOTAS_REQUEST_EVENT, {
|
|
90
|
+
respond,
|
|
91
|
+
} satisfies SyntheticQuotasRequestPayload);
|
|
129
92
|
}
|
|
130
93
|
|
|
131
|
-
function
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
}
|
|
137
|
-
refreshTimer.unref?.();
|
|
94
|
+
function readQuotas(
|
|
95
|
+
respond: (snapshot: SyntheticQuotasSnapshotPayload | undefined) => void,
|
|
96
|
+
): void {
|
|
97
|
+
pi.events.emit(SYNTHETIC_QUOTAS_READ_EVENT, {
|
|
98
|
+
respond,
|
|
99
|
+
} satisfies SyntheticQuotasReadPayload);
|
|
138
100
|
}
|
|
139
101
|
|
|
140
|
-
function
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
102
|
+
function renderSnapshot(
|
|
103
|
+
ctx: ExtensionContext,
|
|
104
|
+
snapshot: SyntheticQuotasSnapshotPayload | undefined,
|
|
105
|
+
): void {
|
|
106
|
+
if (!ctx.hasUI) return;
|
|
107
|
+
if (!snapshot) {
|
|
108
|
+
ctx.ui.setStatus(
|
|
109
|
+
EXTENSION_ID,
|
|
110
|
+
ctx.ui.theme.fg("dim", "loading usage..."),
|
|
111
|
+
);
|
|
112
|
+
return;
|
|
144
113
|
}
|
|
145
|
-
ctx?.ui.setStatus(EXTENSION_ID, undefined);
|
|
146
|
-
}
|
|
147
114
|
|
|
148
|
-
|
|
149
|
-
if (
|
|
150
|
-
const apiKey = await getSyntheticApiKey(
|
|
151
|
-
ctx.modelRegistry.authStorage,
|
|
152
|
-
).catch(() => undefined);
|
|
153
|
-
if (!apiKey) {
|
|
115
|
+
const windows = parseSnapshot(snapshot.quotas);
|
|
116
|
+
if (windows.length === 0) {
|
|
154
117
|
ctx.ui.setStatus(EXTENSION_ID, undefined);
|
|
155
118
|
return;
|
|
156
119
|
}
|
|
157
|
-
ctx.ui.setStatus(EXTENSION_ID, ctx.ui.theme.fg("dim", "loading usage..."));
|
|
158
|
-
}
|
|
159
120
|
|
|
160
|
-
|
|
161
|
-
if (!ctx.hasUI || !lastSnapshot) return false;
|
|
162
|
-
ctx.ui.setStatus(EXTENSION_ID, formatStatus(ctx, lastSnapshot));
|
|
163
|
-
return true;
|
|
121
|
+
ctx.ui.setStatus(EXTENSION_ID, formatStatus(ctx, windows));
|
|
164
122
|
}
|
|
165
123
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
setLoadingStatus,
|
|
171
|
-
renderFromLastSnapshot,
|
|
172
|
-
};
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
export default async function (pi: ExtensionAPI) {
|
|
176
|
-
await configLoader.load();
|
|
124
|
+
function clearStatus(ctx: ExtensionContext): void {
|
|
125
|
+
if (!ctx.hasUI) return;
|
|
126
|
+
ctx.ui.setStatus(EXTENSION_ID, undefined);
|
|
127
|
+
}
|
|
177
128
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
129
|
+
function renderFromStoreOrRefresh(ctx: ExtensionContext): void {
|
|
130
|
+
if (!enabled || ctx.model?.provider !== "synthetic") {
|
|
131
|
+
clearStatus(ctx);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
readQuotas((snapshot) => {
|
|
135
|
+
if (snapshot) {
|
|
136
|
+
renderSnapshot(ctx, snapshot);
|
|
137
|
+
} else {
|
|
138
|
+
renderSnapshot(ctx, undefined); // show loading
|
|
139
|
+
requestQuotas((refreshed) => renderSnapshot(ctx, refreshed));
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
}
|
|
182
143
|
|
|
183
144
|
pi.events.on(SYNTHETIC_CONFIG_UPDATED_EVENT, (data: unknown) => {
|
|
184
145
|
enabled = (data as SyntheticConfigUpdatedPayload).config.usageStatus;
|
|
146
|
+
});
|
|
185
147
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
return;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
if (currentContext && currentProvider === "synthetic") {
|
|
192
|
-
refresher.startAutoRefresh();
|
|
193
|
-
void refresher.refreshFor(currentContext);
|
|
194
|
-
}
|
|
148
|
+
pi.on("session_start", (_event, ctx) => {
|
|
149
|
+
renderFromStoreOrRefresh(ctx);
|
|
195
150
|
});
|
|
196
151
|
|
|
197
|
-
pi.on("
|
|
198
|
-
|
|
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);
|
|
152
|
+
pi.on("model_select", (_event, ctx) => {
|
|
153
|
+
renderFromStoreOrRefresh(ctx);
|
|
204
154
|
});
|
|
205
155
|
|
|
206
|
-
pi.on("
|
|
207
|
-
|
|
208
|
-
currentProvider = ctx.model?.provider;
|
|
209
|
-
if (!enabled || ctx.model?.provider !== "synthetic") return;
|
|
210
|
-
void refresher.refreshFor(ctx);
|
|
156
|
+
pi.on("agent_end", (_event, ctx) => {
|
|
157
|
+
renderFromStoreOrRefresh(ctx);
|
|
211
158
|
});
|
|
212
159
|
|
|
213
|
-
pi.on("
|
|
214
|
-
|
|
215
|
-
if (
|
|
216
|
-
event.reason === "new" ||
|
|
217
|
-
event.reason === "resume" ||
|
|
218
|
-
event.reason === "fork"
|
|
219
|
-
) {
|
|
220
|
-
currentContext = ctx;
|
|
221
|
-
currentProvider = ctx.model?.provider;
|
|
222
|
-
if (enabled && ctx.model?.provider === "synthetic") {
|
|
223
|
-
void refresher.refreshFor(ctx);
|
|
224
|
-
} else {
|
|
225
|
-
refresher.stopAutoRefresh(ctx);
|
|
226
|
-
}
|
|
227
|
-
}
|
|
160
|
+
pi.on("turn_end", (_event, ctx) => {
|
|
161
|
+
renderFromStoreOrRefresh(ctx);
|
|
228
162
|
});
|
|
229
163
|
|
|
230
|
-
pi.on("
|
|
231
|
-
|
|
232
|
-
currentProvider = ctx.model?.provider;
|
|
233
|
-
if (enabled && ctx.model?.provider === "synthetic") {
|
|
234
|
-
refresher.startAutoRefresh();
|
|
235
|
-
void refresher.refreshFor(ctx);
|
|
236
|
-
} else {
|
|
237
|
-
refresher.stopAutoRefresh(ctx);
|
|
238
|
-
}
|
|
164
|
+
pi.on("session_before_switch", (_event, ctx) => {
|
|
165
|
+
clearStatus(ctx);
|
|
239
166
|
});
|
|
240
167
|
|
|
241
168
|
pi.on("session_shutdown", (_event, ctx) => {
|
|
242
|
-
|
|
243
|
-
currentProvider = undefined;
|
|
244
|
-
refresher.stopAutoRefresh(ctx);
|
|
169
|
+
clearStatus(ctx);
|
|
245
170
|
});
|
|
246
171
|
|
|
247
172
|
pi.events.on(SYNTHETIC_EXTENSIONS_REQUEST_EVENT, () => {
|