@aliou/pi-neuralwatt 0.0.1 → 0.1.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 +150 -0
- package/package.json +67 -5
- package/schema.json +24 -0
- package/src/config.ts +154 -0
- package/src/extensions/command-quotas/command.ts +83 -0
- package/src/extensions/command-quotas/components/quota-tabs.ts +251 -0
- package/src/extensions/command-quotas/components/quotas-display.ts +192 -0
- package/src/extensions/command-quotas/index.ts +23 -0
- package/src/extensions/provider/index.ts +146 -0
- package/src/extensions/provider/models.test.ts +120 -0
- package/src/extensions/provider/models.ts +285 -0
- package/src/extensions/quota-warnings/index.ts +75 -0
- package/src/extensions/quota-warnings/notifier.ts +98 -0
- package/src/extensions/sub-bar-integration/index.ts +142 -0
- package/src/extensions/sub-bar-integration/snapshot.ts +47 -0
- package/src/lib/env.ts +18 -0
- package/src/types/quota-api.ts +55 -0
- package/src/types/quota-events.ts +72 -0
- package/src/utils/quota-bar.ts +61 -0
- package/src/utils/quota-format.ts +25 -0
- package/src/utils/quotas.ts +73 -0
- package/index.js +0 -1
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import type { Theme } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import { truncateToWidth } from "@mariozechner/pi-tui";
|
|
3
|
+
import type { NeuralwattQuotas } from "../../../types/quota-api";
|
|
4
|
+
import {
|
|
5
|
+
percentCreditsRemaining,
|
|
6
|
+
percentEnergyRemaining,
|
|
7
|
+
renderProgressBar,
|
|
8
|
+
severityFromPercent,
|
|
9
|
+
} from "../../../utils/quota-bar";
|
|
10
|
+
import {
|
|
11
|
+
formatKwh,
|
|
12
|
+
formatTokens,
|
|
13
|
+
formatUsd,
|
|
14
|
+
} from "../../../utils/quota-format";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Subscription tab — plan details, energy quota, billing period.
|
|
18
|
+
* Shown only when an active subscription exists.
|
|
19
|
+
*/
|
|
20
|
+
export function renderSubscriptionTab(
|
|
21
|
+
quotas: NeuralwattQuotas,
|
|
22
|
+
contentWidth: number,
|
|
23
|
+
maxWidth: number,
|
|
24
|
+
theme: Theme,
|
|
25
|
+
): string[] {
|
|
26
|
+
const sub = quotas.subscription;
|
|
27
|
+
if (!sub) return [];
|
|
28
|
+
const lines: string[] = [];
|
|
29
|
+
const barWidth = Math.min(40, Math.max(20, contentWidth - 30));
|
|
30
|
+
|
|
31
|
+
// --- Plan info ---
|
|
32
|
+
lines.push("");
|
|
33
|
+
lines.push(
|
|
34
|
+
truncateToWidth(` ${theme.fg("accent", theme.bold("Plan"))}`, maxWidth),
|
|
35
|
+
);
|
|
36
|
+
lines.push(labelValue("Name", sub.plan, maxWidth, theme));
|
|
37
|
+
lines.push(labelValue("Status", sub.status, maxWidth, theme));
|
|
38
|
+
lines.push(
|
|
39
|
+
labelValue("Billing", sub.billing_interval ?? "\u2014", maxWidth, theme),
|
|
40
|
+
);
|
|
41
|
+
lines.push(
|
|
42
|
+
labelValue("Auto-renew", sub.auto_renew ? "yes" : "no", maxWidth, theme),
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
// --- Energy quota ---
|
|
46
|
+
lines.push("");
|
|
47
|
+
lines.push(
|
|
48
|
+
truncateToWidth(
|
|
49
|
+
` ${theme.fg("accent", theme.bold("Energy Quota"))}`,
|
|
50
|
+
maxWidth,
|
|
51
|
+
),
|
|
52
|
+
);
|
|
53
|
+
const energyPct = percentEnergyRemaining(sub);
|
|
54
|
+
const energyColor = severityFromPercent(energyPct);
|
|
55
|
+
const energyBar = renderProgressBar(energyPct, barWidth, theme, energyColor);
|
|
56
|
+
lines.push(
|
|
57
|
+
truncateToWidth(
|
|
58
|
+
` ${energyBar} ${theme.fg(energyColor, `${energyPct}%`)}`,
|
|
59
|
+
maxWidth,
|
|
60
|
+
),
|
|
61
|
+
);
|
|
62
|
+
lines.push(
|
|
63
|
+
labelValue(
|
|
64
|
+
"Remaining",
|
|
65
|
+
`${formatKwh(sub.kwh_remaining)} of ${formatKwh(sub.kwh_included)}`,
|
|
66
|
+
maxWidth,
|
|
67
|
+
theme,
|
|
68
|
+
),
|
|
69
|
+
);
|
|
70
|
+
lines.push(labelValue("Used", formatKwh(sub.kwh_used), maxWidth, theme));
|
|
71
|
+
|
|
72
|
+
if (sub.in_overage) {
|
|
73
|
+
lines.push(
|
|
74
|
+
truncateToWidth(
|
|
75
|
+
` ${theme.fg("error", "In overage \u2014 pay-per-use rates apply")}`,
|
|
76
|
+
maxWidth,
|
|
77
|
+
),
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// --- Billing period ---
|
|
82
|
+
lines.push("");
|
|
83
|
+
lines.push(
|
|
84
|
+
truncateToWidth(
|
|
85
|
+
` ${theme.fg("accent", theme.bold("Billing Period"))}`,
|
|
86
|
+
maxWidth,
|
|
87
|
+
),
|
|
88
|
+
);
|
|
89
|
+
const start = sub.current_period_start?.slice(0, 10) ?? "\u2014";
|
|
90
|
+
const end = sub.current_period_end?.slice(0, 10) ?? "\u2014";
|
|
91
|
+
lines.push(labelValue("Start", start, maxWidth, theme));
|
|
92
|
+
lines.push(labelValue("End", end, maxWidth, theme));
|
|
93
|
+
|
|
94
|
+
return lines;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Credits tab — credit balance only.
|
|
99
|
+
*/
|
|
100
|
+
export function renderCreditsTab(
|
|
101
|
+
quotas: NeuralwattQuotas,
|
|
102
|
+
contentWidth: number,
|
|
103
|
+
maxWidth: number,
|
|
104
|
+
theme: Theme,
|
|
105
|
+
): string[] {
|
|
106
|
+
const lines: string[] = [];
|
|
107
|
+
const barWidth = Math.min(40, Math.max(20, contentWidth - 30));
|
|
108
|
+
|
|
109
|
+
// --- Credit balance ---
|
|
110
|
+
lines.push("");
|
|
111
|
+
lines.push(
|
|
112
|
+
truncateToWidth(` ${theme.fg("accent", theme.bold("Credits"))}`, maxWidth),
|
|
113
|
+
);
|
|
114
|
+
const creditsPct = percentCreditsRemaining(quotas);
|
|
115
|
+
const creditsColor = severityFromPercent(creditsPct);
|
|
116
|
+
const creditsBar = renderProgressBar(
|
|
117
|
+
creditsPct,
|
|
118
|
+
barWidth,
|
|
119
|
+
theme,
|
|
120
|
+
creditsColor,
|
|
121
|
+
);
|
|
122
|
+
lines.push(
|
|
123
|
+
truncateToWidth(
|
|
124
|
+
` ${creditsBar} ${theme.fg(creditsColor, `${creditsPct}%`)}`,
|
|
125
|
+
maxWidth,
|
|
126
|
+
),
|
|
127
|
+
);
|
|
128
|
+
lines.push(
|
|
129
|
+
labelValue(
|
|
130
|
+
"Remaining",
|
|
131
|
+
`${formatUsd(quotas.balance.credits_remaining_usd)} of ${formatUsd(quotas.balance.total_credits_usd)}`,
|
|
132
|
+
maxWidth,
|
|
133
|
+
theme,
|
|
134
|
+
),
|
|
135
|
+
);
|
|
136
|
+
lines.push(
|
|
137
|
+
labelValue(
|
|
138
|
+
"Used",
|
|
139
|
+
formatUsd(quotas.balance.credits_used_usd),
|
|
140
|
+
maxWidth,
|
|
141
|
+
theme,
|
|
142
|
+
),
|
|
143
|
+
);
|
|
144
|
+
lines.push(
|
|
145
|
+
labelValue("Accounting", quotas.balance.accounting_method, maxWidth, theme),
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
return lines;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Usage & Key tab — usage this month, API key info, key allowance, rate limits.
|
|
153
|
+
* Shared data that doesn't belong exclusively to subscription or credits.
|
|
154
|
+
*/
|
|
155
|
+
export function renderUsageKeyTab(
|
|
156
|
+
quotas: NeuralwattQuotas,
|
|
157
|
+
_contentWidth: number,
|
|
158
|
+
maxWidth: number,
|
|
159
|
+
theme: Theme,
|
|
160
|
+
): string[] {
|
|
161
|
+
const lines: string[] = [];
|
|
162
|
+
|
|
163
|
+
// --- Usage this month ---
|
|
164
|
+
lines.push("");
|
|
165
|
+
lines.push(
|
|
166
|
+
truncateToWidth(
|
|
167
|
+
` ${theme.fg("accent", theme.bold("Usage (this month)"))}`,
|
|
168
|
+
maxWidth,
|
|
169
|
+
),
|
|
170
|
+
);
|
|
171
|
+
const usage = quotas.usage.current_month;
|
|
172
|
+
lines.push(labelValue("Cost", formatUsd(usage.cost_usd), maxWidth, theme));
|
|
173
|
+
lines.push(
|
|
174
|
+
labelValue("Requests", usage.requests.toLocaleString(), maxWidth, theme),
|
|
175
|
+
);
|
|
176
|
+
lines.push(labelValue("Tokens", formatTokens(usage.tokens), maxWidth, theme));
|
|
177
|
+
lines.push(
|
|
178
|
+
labelValue("Energy", formatKwh(usage.energy_kwh), maxWidth, theme),
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
// --- API Key ---
|
|
182
|
+
lines.push("");
|
|
183
|
+
lines.push(
|
|
184
|
+
truncateToWidth(` ${theme.fg("accent", theme.bold("API Key"))}`, maxWidth),
|
|
185
|
+
);
|
|
186
|
+
lines.push(
|
|
187
|
+
labelValue("Name", quotas.key.name || "(unnamed)", maxWidth, theme),
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
// --- Key allowance ---
|
|
191
|
+
if (quotas.key.allowance) {
|
|
192
|
+
lines.push("");
|
|
193
|
+
lines.push(
|
|
194
|
+
truncateToWidth(
|
|
195
|
+
` ${theme.fg("accent", theme.bold("Key Allowance"))}`,
|
|
196
|
+
maxWidth,
|
|
197
|
+
),
|
|
198
|
+
);
|
|
199
|
+
const alw = quotas.key.allowance;
|
|
200
|
+
lines.push(labelValue("Limit", formatUsd(alw.limit_usd), maxWidth, theme));
|
|
201
|
+
lines.push(labelValue("Spent", formatUsd(alw.spent_usd), maxWidth, theme));
|
|
202
|
+
lines.push(
|
|
203
|
+
labelValue("Remaining", formatUsd(alw.remaining_usd), maxWidth, theme),
|
|
204
|
+
);
|
|
205
|
+
lines.push(labelValue("Period", alw.period, maxWidth, theme));
|
|
206
|
+
if (alw.blocked) {
|
|
207
|
+
lines.push(
|
|
208
|
+
truncateToWidth(
|
|
209
|
+
` ${theme.fg("error", "BLOCKED \u2014 spending limit reached")}`,
|
|
210
|
+
maxWidth,
|
|
211
|
+
),
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// --- Rate Limits ---
|
|
217
|
+
lines.push("");
|
|
218
|
+
lines.push(
|
|
219
|
+
truncateToWidth(
|
|
220
|
+
` ${theme.fg("accent", theme.bold("Rate Limits"))}`,
|
|
221
|
+
maxWidth,
|
|
222
|
+
),
|
|
223
|
+
);
|
|
224
|
+
lines.push(
|
|
225
|
+
labelValue("Tier", quotas.limits.rate_limit_tier, maxWidth, theme),
|
|
226
|
+
);
|
|
227
|
+
lines.push(
|
|
228
|
+
labelValue(
|
|
229
|
+
"Overage cap",
|
|
230
|
+
quotas.limits.overage_limit_usd !== null
|
|
231
|
+
? formatUsd(quotas.limits.overage_limit_usd)
|
|
232
|
+
: "none",
|
|
233
|
+
maxWidth,
|
|
234
|
+
theme,
|
|
235
|
+
),
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
return lines;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/** Format a label: value pair with aligned labels. */
|
|
242
|
+
function labelValue(
|
|
243
|
+
label: string,
|
|
244
|
+
value: string,
|
|
245
|
+
maxWidth: number,
|
|
246
|
+
theme: Theme,
|
|
247
|
+
): string {
|
|
248
|
+
const labelWidth = 14;
|
|
249
|
+
const padded = label.padEnd(labelWidth);
|
|
250
|
+
return truncateToWidth(` ${theme.fg("dim", padded)} ${value}`, maxWidth);
|
|
251
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import type { Theme } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import { DynamicBorder } from "@mariozechner/pi-coding-agent";
|
|
3
|
+
import type { Component, TUI } from "@mariozechner/pi-tui";
|
|
4
|
+
import { Loader, matchesKey, truncateToWidth } from "@mariozechner/pi-tui";
|
|
5
|
+
import type { NeuralwattQuotas } from "../../../types/quota-api";
|
|
6
|
+
import {
|
|
7
|
+
renderCreditsTab,
|
|
8
|
+
renderSubscriptionTab,
|
|
9
|
+
renderUsageKeyTab,
|
|
10
|
+
} from "./quota-tabs";
|
|
11
|
+
|
|
12
|
+
type QuotasState =
|
|
13
|
+
| { type: "loading" }
|
|
14
|
+
| { type: "error"; message: string }
|
|
15
|
+
| { type: "loaded"; quotas: NeuralwattQuotas };
|
|
16
|
+
|
|
17
|
+
export class QuotasComponent implements Component {
|
|
18
|
+
private state: QuotasState = { type: "loading" };
|
|
19
|
+
private theme: Theme;
|
|
20
|
+
private tui: TUI;
|
|
21
|
+
private onClose: () => void;
|
|
22
|
+
private onRefetch: () => void;
|
|
23
|
+
private loader: Loader | null = null;
|
|
24
|
+
private activeTab = 0;
|
|
25
|
+
|
|
26
|
+
constructor(
|
|
27
|
+
theme: Theme,
|
|
28
|
+
tui: TUI,
|
|
29
|
+
onClose: () => void,
|
|
30
|
+
onRefetch: () => void,
|
|
31
|
+
) {
|
|
32
|
+
this.theme = theme;
|
|
33
|
+
this.tui = tui;
|
|
34
|
+
this.onClose = onClose;
|
|
35
|
+
this.onRefetch = onRefetch;
|
|
36
|
+
this.startLoader();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
private startLoader(): void {
|
|
40
|
+
this.loader = new Loader(
|
|
41
|
+
this.tui,
|
|
42
|
+
(s: string) => this.theme.fg("accent", s),
|
|
43
|
+
(s: string) => this.theme.fg("muted", s),
|
|
44
|
+
"Fetching quota...",
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
destroy(): void {
|
|
49
|
+
this.loader?.stop();
|
|
50
|
+
this.loader = null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
setState(state: QuotasState): void {
|
|
54
|
+
if (state.type === "loading") {
|
|
55
|
+
this.loader?.stop();
|
|
56
|
+
this.startLoader();
|
|
57
|
+
this.activeTab = 0;
|
|
58
|
+
} else if (this.state.type === "loading") {
|
|
59
|
+
this.loader?.stop();
|
|
60
|
+
this.loader = null;
|
|
61
|
+
}
|
|
62
|
+
this.state = state;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Build the list of active tab labels. */
|
|
66
|
+
private tabs(): string[] {
|
|
67
|
+
const hasSub =
|
|
68
|
+
this.state.type === "loaded" && this.state.quotas.subscription !== null;
|
|
69
|
+
return hasSub
|
|
70
|
+
? ["Subscription", "Credits", "Usage & Key"]
|
|
71
|
+
: ["Credits", "Usage & Key"];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
handleInput(data: string): boolean {
|
|
75
|
+
if (matchesKey(data, "escape") || data === "q") {
|
|
76
|
+
this.onClose();
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
if (data === "r") {
|
|
80
|
+
this.onRefetch();
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
const tabCount = this.tabs().length;
|
|
84
|
+
if (tabCount > 1) {
|
|
85
|
+
if (data === "\t") {
|
|
86
|
+
this.activeTab = (this.activeTab + 1) % tabCount;
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
if (data === "\x1b[Z" || data === "\x1b[2Z") {
|
|
90
|
+
this.activeTab = (this.activeTab - 1 + tabCount) % tabCount;
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
render(width: number): string[] {
|
|
98
|
+
const lines: string[] = [];
|
|
99
|
+
const border = new DynamicBorder((s: string) => this.theme.fg("border", s));
|
|
100
|
+
const contentWidth = Math.max(1, width - 4);
|
|
101
|
+
|
|
102
|
+
lines.push(...border.render(width));
|
|
103
|
+
lines.push(
|
|
104
|
+
truncateToWidth(
|
|
105
|
+
` ${this.theme.fg("accent", this.theme.bold("Neuralwatt API Quota"))}`,
|
|
106
|
+
width,
|
|
107
|
+
),
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
switch (this.state.type) {
|
|
111
|
+
case "loading":
|
|
112
|
+
if (this.loader) {
|
|
113
|
+
lines.push(...this.loader.render(width));
|
|
114
|
+
} else {
|
|
115
|
+
lines.push(this.theme.fg("muted", " Fetching quota..."));
|
|
116
|
+
}
|
|
117
|
+
break;
|
|
118
|
+
case "error":
|
|
119
|
+
lines.push(this.theme.fg("error", ` ${this.state.message}`));
|
|
120
|
+
break;
|
|
121
|
+
case "loaded": {
|
|
122
|
+
const tabLines = this.renderLoaded(
|
|
123
|
+
this.state.quotas,
|
|
124
|
+
contentWidth,
|
|
125
|
+
width,
|
|
126
|
+
);
|
|
127
|
+
lines.push(...tabLines);
|
|
128
|
+
break;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
lines.push("");
|
|
133
|
+
const hints = ["r refresh", "q/Esc close"];
|
|
134
|
+
if (this.tabs().length > 1) hints.push("Tab/Shift+Tab switch tab");
|
|
135
|
+
lines.push(this.theme.fg("dim", ` ${hints.join(" ")}`));
|
|
136
|
+
lines.push(...border.render(width));
|
|
137
|
+
|
|
138
|
+
return lines;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
private renderLoaded(
|
|
142
|
+
quotas: NeuralwattQuotas,
|
|
143
|
+
contentWidth: number,
|
|
144
|
+
maxWidth: number,
|
|
145
|
+
): string[] {
|
|
146
|
+
const lines: string[] = [];
|
|
147
|
+
const tabs = this.tabs();
|
|
148
|
+
|
|
149
|
+
// Render tab bar with bg highlight on active tab
|
|
150
|
+
lines.push("");
|
|
151
|
+
const tabParts = tabs.map((label, idx) => {
|
|
152
|
+
const fullLabel = ` ${label} `;
|
|
153
|
+
if (idx === this.activeTab) {
|
|
154
|
+
return this.theme.bg(
|
|
155
|
+
"selectedBg",
|
|
156
|
+
this.theme.fg("accent", this.theme.bold(fullLabel)),
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
return this.theme.fg("dim", fullLabel);
|
|
160
|
+
});
|
|
161
|
+
lines.push(truncateToWidth(` ${tabParts.join(" ")}`, maxWidth));
|
|
162
|
+
|
|
163
|
+
// Render content for each tab, equalize heights, show active
|
|
164
|
+
const allTabContent = tabs.map((label) => {
|
|
165
|
+
if (label === "Subscription") {
|
|
166
|
+
return renderSubscriptionTab(
|
|
167
|
+
quotas,
|
|
168
|
+
contentWidth,
|
|
169
|
+
maxWidth,
|
|
170
|
+
this.theme,
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
if (label === "Credits") {
|
|
174
|
+
return renderCreditsTab(quotas, contentWidth, maxWidth, this.theme);
|
|
175
|
+
}
|
|
176
|
+
// "Usage & Key"
|
|
177
|
+
return renderUsageKeyTab(quotas, contentWidth, maxWidth, this.theme);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
// Equalize tab heights so switching doesn't cause layout jumps
|
|
181
|
+
const maxLen = Math.max(...allTabContent.map((t) => t.length));
|
|
182
|
+
for (const tabLines of allTabContent) {
|
|
183
|
+
while (tabLines.length < maxLen) tabLines.push("");
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
lines.push(...allTabContent[this.activeTab]);
|
|
187
|
+
|
|
188
|
+
return lines;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
invalidate(): void {}
|
|
192
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import {
|
|
3
|
+
configLoader,
|
|
4
|
+
NEURALWATT_EXTENSIONS_REGISTER_EVENT,
|
|
5
|
+
NEURALWATT_EXTENSIONS_REQUEST_EVENT,
|
|
6
|
+
} from "../../config";
|
|
7
|
+
import { registerQuotasCommand } from "./command";
|
|
8
|
+
|
|
9
|
+
export default async function (pi: ExtensionAPI) {
|
|
10
|
+
await configLoader.load();
|
|
11
|
+
|
|
12
|
+
const config = configLoader.getConfig();
|
|
13
|
+
|
|
14
|
+
if (config.quotaCommand) {
|
|
15
|
+
registerQuotasCommand(pi);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
pi.events.on(NEURALWATT_EXTENSIONS_REQUEST_EVENT, () => {
|
|
19
|
+
pi.events.emit(NEURALWATT_EXTENSIONS_REGISTER_EVENT, {
|
|
20
|
+
feature: "quotaCommand",
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import type { AuthStorage, ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import {
|
|
3
|
+
configLoader,
|
|
4
|
+
emitConfigUpdated,
|
|
5
|
+
NEURALWATT_EXTENSIONS_REGISTER_EVENT,
|
|
6
|
+
NEURALWATT_EXTENSIONS_REQUEST_EVENT,
|
|
7
|
+
type NeuralwattFeatureId,
|
|
8
|
+
registerNeuralwattSettings,
|
|
9
|
+
} from "../../config";
|
|
10
|
+
import { getNeuralwattApiKey } from "../../lib/env";
|
|
11
|
+
import type { NeuralwattQuotas } from "../../types/quota-api";
|
|
12
|
+
import {
|
|
13
|
+
NEURALWATT_QUOTAS_REQUEST_EVENT,
|
|
14
|
+
NEURALWATT_QUOTAS_UPDATED_EVENT,
|
|
15
|
+
type NeuralwattQuotasUpdatedPayload,
|
|
16
|
+
parseQuotaHeaders,
|
|
17
|
+
} from "../../types/quota-events";
|
|
18
|
+
import { fetchQuotas } from "../../utils/quotas";
|
|
19
|
+
import { NEURALWATT_MODELS } from "./models";
|
|
20
|
+
|
|
21
|
+
export function registerNeuralwattProvider(pi: ExtensionAPI): void {
|
|
22
|
+
pi.registerProvider("neuralwatt", {
|
|
23
|
+
baseUrl: "https://api.neuralwatt.com/v1",
|
|
24
|
+
apiKey: "NEURALWATT_API_KEY",
|
|
25
|
+
api: "openai-completions",
|
|
26
|
+
authHeader: true,
|
|
27
|
+
headers: {
|
|
28
|
+
Referer: "https://pi.dev",
|
|
29
|
+
"X-Title": "npm:@aliou/pi-neuralwatt",
|
|
30
|
+
},
|
|
31
|
+
models: NEURALWATT_MODELS.map(({ fast: _fast, ...model }) => ({
|
|
32
|
+
...model,
|
|
33
|
+
compat: {
|
|
34
|
+
supportsDeveloperRole: false,
|
|
35
|
+
maxTokensField: "max_tokens",
|
|
36
|
+
...model.compat,
|
|
37
|
+
},
|
|
38
|
+
})),
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export default async function (pi: ExtensionAPI) {
|
|
43
|
+
await configLoader.load();
|
|
44
|
+
registerNeuralwattProvider(pi);
|
|
45
|
+
|
|
46
|
+
// Track which feature extensions loaded
|
|
47
|
+
const loadedFeatures = new Set<NeuralwattFeatureId>();
|
|
48
|
+
|
|
49
|
+
// Register settings (in the provider, so it's always available)
|
|
50
|
+
registerNeuralwattSettings(pi, {
|
|
51
|
+
getLoadedFeatures: () => loadedFeatures,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// --- Quota store (event-based) ---
|
|
55
|
+
let lastHeaderEmitAt = 0;
|
|
56
|
+
const HEADER_EMIT_THROTTLE_MS = 5_000;
|
|
57
|
+
|
|
58
|
+
function emitQuotas(
|
|
59
|
+
quotas: NeuralwattQuotas,
|
|
60
|
+
source: NeuralwattQuotasUpdatedPayload["source"],
|
|
61
|
+
): void {
|
|
62
|
+
const now = Date.now();
|
|
63
|
+
if (source === "header" && now - lastHeaderEmitAt < HEADER_EMIT_THROTTLE_MS)
|
|
64
|
+
return;
|
|
65
|
+
if (source === "header") lastHeaderEmitAt = now;
|
|
66
|
+
pi.events.emit(NEURALWATT_QUOTAS_UPDATED_EVENT, { quotas, source });
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Ingest quotas from response headers
|
|
70
|
+
pi.on("after_provider_response", (event, ctx) => {
|
|
71
|
+
if (ctx.model?.provider !== "neuralwatt") return;
|
|
72
|
+
const headerQuotas = parseQuotaHeaders(event.headers);
|
|
73
|
+
if (!headerQuotas) return;
|
|
74
|
+
|
|
75
|
+
const quotas: NeuralwattQuotas = {
|
|
76
|
+
snapshot_at: new Date().toISOString(),
|
|
77
|
+
balance: {
|
|
78
|
+
credits_remaining_usd: headerQuotas.allowanceRemainingUsd,
|
|
79
|
+
total_credits_usd: 0,
|
|
80
|
+
credits_used_usd: 0,
|
|
81
|
+
accounting_method: "token",
|
|
82
|
+
},
|
|
83
|
+
usage: {
|
|
84
|
+
lifetime: { cost_usd: 0, requests: 0, tokens: 0, energy_kwh: 0 },
|
|
85
|
+
current_month: { cost_usd: 0, requests: 0, tokens: 0, energy_kwh: 0 },
|
|
86
|
+
},
|
|
87
|
+
limits: { overage_limit_usd: null, rate_limit_tier: "standard" },
|
|
88
|
+
subscription:
|
|
89
|
+
headerQuotas.subscriptionPlan !== "none" &&
|
|
90
|
+
headerQuotas.energyRemaining !== undefined
|
|
91
|
+
? {
|
|
92
|
+
plan: headerQuotas.subscriptionPlan,
|
|
93
|
+
status: "active",
|
|
94
|
+
billing_interval: "month",
|
|
95
|
+
current_period_start: "",
|
|
96
|
+
current_period_end: "",
|
|
97
|
+
auto_renew: false,
|
|
98
|
+
kwh_included: headerQuotas.energyIncluded ?? 0,
|
|
99
|
+
kwh_used: headerQuotas.energyUsed ?? 0,
|
|
100
|
+
kwh_remaining: headerQuotas.energyRemaining,
|
|
101
|
+
in_overage: false,
|
|
102
|
+
}
|
|
103
|
+
: null,
|
|
104
|
+
key: { name: "", allowance: null },
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
emitQuotas(quotas, "header");
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// Respond to quota requests from other extensions
|
|
111
|
+
let quotaRequestInFlight = false;
|
|
112
|
+
pi.events.on(NEURALWATT_QUOTAS_REQUEST_EVENT, async (data: unknown) => {
|
|
113
|
+
if (quotaRequestInFlight) return;
|
|
114
|
+
quotaRequestInFlight = true;
|
|
115
|
+
try {
|
|
116
|
+
if (!data || typeof data !== "object") return;
|
|
117
|
+
const { authStorage } = data as { authStorage?: AuthStorage };
|
|
118
|
+
if (!authStorage) return;
|
|
119
|
+
const apiKey = await getNeuralwattApiKey(authStorage);
|
|
120
|
+
if (!apiKey) return;
|
|
121
|
+
const result = await fetchQuotas(apiKey);
|
|
122
|
+
if (result.success) emitQuotas(result.data.quotas, "api");
|
|
123
|
+
} finally {
|
|
124
|
+
quotaRequestInFlight = false;
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// Collect which feature extensions are loaded
|
|
129
|
+
pi.events.on(NEURALWATT_EXTENSIONS_REGISTER_EVENT, (data: unknown) => {
|
|
130
|
+
const { feature } = data as { feature: NeuralwattFeatureId };
|
|
131
|
+
loadedFeatures.add(feature);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// On session start: request extensions to register, then emit config
|
|
135
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
136
|
+
loadedFeatures.clear();
|
|
137
|
+
pi.events.emit(NEURALWATT_EXTENSIONS_REQUEST_EVENT, undefined);
|
|
138
|
+
emitConfigUpdated(pi);
|
|
139
|
+
|
|
140
|
+
if (ctx.model?.provider !== "neuralwatt") return;
|
|
141
|
+
const apiKey = await getNeuralwattApiKey(ctx.modelRegistry.authStorage);
|
|
142
|
+
if (!apiKey) return;
|
|
143
|
+
const result = await fetchQuotas(apiKey);
|
|
144
|
+
if (result.success) emitQuotas(result.data.quotas, "api");
|
|
145
|
+
});
|
|
146
|
+
}
|