@openlines/opl-pi-sht 0.2.10 → 0.2.12
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 +6 -2
- package/configs/opl-footer.json.sample +1 -1
- package/extensions/opl-footer/README.md +9 -1
- package/extensions/opl-footer/config.ts +1 -1
- package/extensions/opl-footer/index.ts +131 -2
- package/extensions/opl-footer/openrouter-usage.ts +77 -0
- package/extensions/opl-footer/segments/index.ts +2 -0
- package/extensions/opl-footer/segments/openrouter-usage.ts +21 -0
- package/extensions/opl-footer/types.ts +3 -0
- package/images/ss-init.png +0 -0
- package/images/ss-input-footer.png +0 -0
- package/images/ss-input-pet.png +0 -0
- package/images/ss-mode-custom.png +0 -0
- package/images/ss-questionnaire0.png +0 -0
- package/images/ss-questionnaire1.png +0 -0
- package/images/ss-simplebench.png +0 -0
- package/images/ss-todo.png +0 -0
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -15,6 +15,10 @@
|
|
|
15
15
|
A portable collection of various Pi coding agent extensions. Repository directories and config files use `opl-`; established Pi-facing commands and tool names stay compatible.
|
|
16
16
|
|
|
17
17
|
>
|
|
18
|
+
> ### v0.2.12 - refresh OpenRouter/Codex usage during agent runs
|
|
19
|
+
> - `opl-footer` fixed usage refresh from agent_settled to after every turn/toolcall
|
|
20
|
+
> ### v0.2.11 - OpenRouter key-limit usage in opl-footer
|
|
21
|
+
> - `opl-footer` added an OpenRouter API key's used amount, cap, and percentage.
|
|
18
22
|
> ### v0.2.10 - Codex subscription usage in opl-footer
|
|
19
23
|
> - `opl-footer` added ChatGPT Codex subs 5-hour/weekly quota segment with reset countdowns and stale-snapshot fallback.
|
|
20
24
|
> ### v0.2.9 - pi dependencies patch, updated test scripts
|
|
@@ -24,7 +28,7 @@ A portable collection of various Pi coding agent extensions. Repository director
|
|
|
24
28
|
>
|
|
25
29
|
> See [CHANGELOG](CHANGELOG.md) for more details.
|
|
26
30
|
>
|
|
27
|
-
> `Current project state also marked 'SAFE' for pi
|
|
31
|
+
> `Current project state also marked 'SAFE' for pi v1.0.0 release`
|
|
28
32
|
>
|
|
29
33
|
|
|
30
34
|
## Installation
|
|
@@ -139,7 +143,7 @@ Cold prompt-cache write, measured /init session (opl-modes lazy tools + MCP adap
|
|
|
139
143
|
|
|
140
144
|
### See what the agent is doing
|
|
141
145
|
|
|
142
|
-
- **`opl-footer`** surfaces model, cost, token and cache activity, git state, agent status, per-turn timing,
|
|
146
|
+
- **`opl-footer`** surfaces model, cost, token and cache activity, git state, agent status, per-turn timing, optional exact remaining ChatGPT Codex subscription quota and OpenRouter API-key cap usage. Both usage segments refresh during agent runs after assistant responses and tool completions (30-second floor, coalesced trailing refresh). The OpenRouter segment resolves Pi's active key locally and calls only OpenRouter's key endpoint; keys and response bodies are neither persisted nor logged. `/configure-opl` edits and reorders its layout interactively.
|
|
143
147
|
- **`opl-input`** is a configurable editor that reflects the active mode's identity, so you always know which mode you are typing into.
|
|
144
148
|

|
|
145
149
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"_comment": "Available segment IDs: pi, model, path, git, thinking, caveman, plan_mode, chat_mode, mode_switcher, separator, token_in, token_out, token_total, cache_read, cache_write, cost, context_pct, context_total, session_stats, perf_stats, status, codex_usage, text:<literal>. codex_usage
|
|
2
|
+
"_comment": "Available segment IDs: pi, model, path, git, thinking, caveman, plan_mode, chat_mode, mode_switcher, separator, token_in, token_out, token_total, cache_read, cache_write, cost, context_pct, context_total, session_stats, perf_stats, status, codex_usage, openrouter_usage, text:<literal>. codex_usage and openrouter_usage are hidden by default; add codex_usage to show ChatGPT Codex subscription windows, or openrouter_usage to show the active OpenRouter API-key limit usage.",
|
|
3
3
|
"row1LeftSegments": [
|
|
4
4
|
"pi",
|
|
5
5
|
"separator",
|
|
@@ -32,6 +32,7 @@ The third row is populated after the first completed turn. Its session and perfo
|
|
|
32
32
|
- **Git integration**: branch plus staged, unstaged, and untracked counts, with invalidation after relevant file and Git commands
|
|
33
33
|
- **Token and cost tracking**: total, cache, input/output, and accumulated cost segments
|
|
34
34
|
- **Codex subscription quota**: optional exact 5-hour and weekly ChatGPT Codex usage percentages with reset countdowns and stale-snapshot fallback
|
|
35
|
+
- **OpenRouter key-limit usage**: optional per-key paid-usage amount, limit, and percentage with stale-snapshot fallback
|
|
35
36
|
- **Session statistics**: prompt, API-call, and model tool-call counts
|
|
36
37
|
- **Performance statistics**: cumulative LLM/tool duration, most recent user-prompt-to-completion turnaround time, average time to first token, output rate, and cache-hit percentage
|
|
37
38
|
- **Thinking, mode, and status indicators**: thinking-level colors plus caveman, plan, chat, unified mode, and `Working`/`Waiting`/`Ready` status segments
|
|
@@ -93,15 +94,22 @@ See the tracked [`configs/opl-footer.json.sample`](../../configs/opl-footer.json
|
|
|
93
94
|
| `perf_stats` | LLM/tool timing, TTFT, output rate, and cache-hit percentage | Hidden until the first prompt |
|
|
94
95
|
| `status` | `Working`, `Waiting`, or `Ready` | `accent`, `warning`, and `success` theme colors respectively; optional and hidden by default |
|
|
95
96
|
| `codex_usage` | `5h 76% ↻2h18m · W 37% ↻3d7h` | Exact remaining ChatGPT Codex subscription quota; OAuth-only, optional and hidden by default; a retained snapshot is marked `(stale)` after refresh failure |
|
|
97
|
+
| `openrouter_usage` | `$3.4500 / $25.0000 (13.8%)` | OpenRouter API-key limit usage; only on the selected `openrouter` provider, optional and hidden by default; a retained snapshot is marked `(stale)` after refresh failure |
|
|
96
98
|
| `separator` | `\|` divider | Coloured via `separator` in `colors` |
|
|
97
99
|
| `text:...` | Literal text, e.g. `text:⚡` | — |
|
|
98
100
|
|
|
99
101
|
## Codex Subscription Usage
|
|
100
102
|
|
|
101
|
-
Add `codex_usage` to any row through `/configure-opl` or `opl-footer.json`. While the active model is the OAuth-authenticated `openai-codex` provider, the footer asks Pi's model registry to resolve the same short-lived OAuth access token that Pi uses for the selected model, then sends `GET https://chatgpt.com/backend-api/wham/usage` directly from the local Pi process. The request carries `Authorization: Bearer <Pi OAuth token>`, `Accept: application/json`, a fixed `User-Agent: opl-footer-codex-usage`, and the token's `chatgpt_account_id` JWT claim as `chatgpt-account-id` when present. It
|
|
103
|
+
Add `codex_usage` to any row through `/configure-opl` or `opl-footer.json`. While the active model is the OAuth-authenticated `openai-codex` provider, the footer asks Pi's model registry to resolve the same short-lived OAuth access token that Pi uses for the selected model, then sends `GET https://chatgpt.com/backend-api/wham/usage` directly from the local Pi process. The request carries `Authorization: Bearer <Pi OAuth token>`, `Accept: application/json`, a fixed `User-Agent: opl-footer-codex-usage`, and the token's `chatgpt_account_id` JWT claim as `chatgpt-account-id` when present. It refreshes after session start, model selection, each completed assistant response, each tool completion, and settled agent runs. Refreshes are non-blocking, with a 30-second floor and a 15-second request timeout; events within that floor coalesce into one trailing refresh, including events received during an in-flight request. Pending refreshes are cancelled on session reset, shutdown, or switching away from Codex, and recheck whether the segment is enabled before fetching. It classifies the returned windows by duration rather than response order. This is ChatGPT subscription quota, not OpenAI Platform API-key usage or billing.
|
|
102
104
|
|
|
103
105
|
No OAuth token, account ID, or response body is written to disk, logged, added to the Pi session, or sent anywhere other than `chatgpt.com` for that request. **The footer retains only the parsed 5-hour/weekly percentage and reset time in process memory for the active session.** The endpoint is an internal ChatGPT backend API and can change without notice. A failed refresh keeps the most recent exact snapshot and marks it `(stale)`; if no request has ever succeeded, the segment stays hidden. Disabling the segment prevents future requests and discards any result from a request already in flight.
|
|
104
106
|
|
|
107
|
+
## OpenRouter Usage
|
|
108
|
+
|
|
109
|
+
Add `openrouter_usage` to any row through `/configure-opl` or `opl-footer.json`. While the selected model provider is `openrouter`, the footer resolves the same API key Pi uses for inference and sends `GET https://openrouter.ai/api/v1/key` from the local Pi process. It renders the configured per-key limit as `$<limit - limit_remaining> / $<limit> (<percent>%)`; an absent, malformed, or unlimited key cap leaves the segment hidden. The numerator deliberately uses OpenRouter's authoritative `limit_remaining`, rather than `usage` or `byok_usage`, because those ledgers may not both count against the key limit.
|
|
110
|
+
|
|
111
|
+
It refreshes after session start, model selection, each completed assistant response, each tool completion, and settled agent runs. Refreshes are non-blocking, with a 30-second floor and a 15-second request timeout; events within that floor coalesce into one trailing refresh, including events received during an in-flight request. Pending refreshes are cancelled on session reset, shutdown, or switching away from OpenRouter, and recheck whether the segment is enabled before fetching. No API key or response body is written to disk, logged, added to the Pi session, or sent anywhere other than `openrouter.ai`. A failed refresh keeps the most recent exact snapshot and marks it `(stale)`; if no request has ever succeeded, the segment stays hidden. Disabling the segment prevents future requests and discards any result from a request already in flight.
|
|
112
|
+
|
|
105
113
|
## Context Bar
|
|
106
114
|
|
|
107
115
|
The `context_pct` segment's bar is fully configurable via `segmentOptions.contextBar`:
|
|
@@ -27,7 +27,7 @@ export const CONFIGURABLE_SEGMENTS: StatusLineSegmentId[] = [
|
|
|
27
27
|
"pi", "model", "path", "git", "thinking", "caveman", "plan_mode",
|
|
28
28
|
"chat_mode", "mode_switcher", "token_in", "token_out", "token_total",
|
|
29
29
|
"cache_read", "cache_write", "cost", "context_pct", "context_total",
|
|
30
|
-
"session_stats", "perf_stats", "status", "codex_usage",
|
|
30
|
+
"session_stats", "perf_stats", "status", "codex_usage", "openrouter_usage",
|
|
31
31
|
];
|
|
32
32
|
|
|
33
33
|
const DEFAULT_LAYOUTS: Record<FooterLayoutKey, StatusLineSegmentId[]> = {
|
|
@@ -13,6 +13,7 @@ import { getIcons } from "./icons.js";
|
|
|
13
13
|
import { getDefaultColors, fg } from "./theme.js";
|
|
14
14
|
import { showFooterConfigurator } from "./configure.js";
|
|
15
15
|
import { fetchCodexUsage, refreshCodexUsageSnapshot, type CodexUsageSnapshot } from "./codex-usage.js";
|
|
16
|
+
import { fetchOpenRouterUsage, refreshOpenRouterUsageSnapshot, type OpenRouterUsageSnapshot } from "./openrouter-usage.js";
|
|
16
17
|
|
|
17
18
|
const GIT_BRANCH_PATTERNS: RegExp[] = [
|
|
18
19
|
// init/clone included: creating a repo mid-session must clear the not-a-repo back-off.
|
|
@@ -123,11 +124,92 @@ export default function footer(pi: ExtensionAPI) {
|
|
|
123
124
|
let codexUsageLastAttempt = 0;
|
|
124
125
|
let codexUsageGeneration = 0;
|
|
125
126
|
let codexUsageInFlight: Promise<void> | null = null;
|
|
127
|
+
let codexUsageTimer: ReturnType<typeof setTimeout> | null = null;
|
|
128
|
+
let codexUsagePendingCtx: ExtensionContext | null = null;
|
|
129
|
+
|
|
130
|
+
const cancelCodexUsageRefresh = () => {
|
|
131
|
+
if (codexUsageTimer !== null) clearTimeout(codexUsageTimer);
|
|
132
|
+
codexUsageTimer = null;
|
|
133
|
+
codexUsagePendingCtx = null;
|
|
134
|
+
};
|
|
135
|
+
let openRouterUsage: OpenRouterUsageSnapshot | null = null;
|
|
136
|
+
let openRouterUsageLastAttempt = 0;
|
|
137
|
+
let openRouterUsageGeneration = 0;
|
|
138
|
+
let openRouterUsageInFlight: Promise<void> | null = null;
|
|
139
|
+
let openRouterUsageTimer: ReturnType<typeof setTimeout> | null = null;
|
|
140
|
+
let openRouterUsagePendingCtx: ExtensionContext | null = null;
|
|
141
|
+
|
|
142
|
+
const cancelOpenRouterUsageRefresh = () => {
|
|
143
|
+
if (openRouterUsageTimer !== null) clearTimeout(openRouterUsageTimer);
|
|
144
|
+
openRouterUsageTimer = null;
|
|
145
|
+
openRouterUsagePendingCtx = null;
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const refreshOpenRouterUsage = (ctx: ExtensionContext, force = false): Promise<void> => {
|
|
149
|
+
const isOpenRouter = ctx.model?.provider === "openrouter";
|
|
150
|
+
if (!isOpenRouter) {
|
|
151
|
+
cancelOpenRouterUsageRefresh();
|
|
152
|
+
openRouterUsageGeneration++;
|
|
153
|
+
openRouterUsageInFlight = null;
|
|
154
|
+
openRouterUsage = null;
|
|
155
|
+
tuiRef?.requestRender();
|
|
156
|
+
return Promise.resolve();
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const config = getEffectiveConfig();
|
|
160
|
+
const enabled = LAYOUT_ROWS.some((row) => config[row]?.includes("openrouter_usage"));
|
|
161
|
+
if (!enabled) {
|
|
162
|
+
cancelOpenRouterUsageRefresh();
|
|
163
|
+
openRouterUsageGeneration++;
|
|
164
|
+
openRouterUsageInFlight = null;
|
|
165
|
+
openRouterUsage = null;
|
|
166
|
+
openRouterUsageLastAttempt = 0;
|
|
167
|
+
tuiRef?.requestRender();
|
|
168
|
+
return Promise.resolve();
|
|
169
|
+
}
|
|
170
|
+
if (openRouterUsageInFlight) {
|
|
171
|
+
openRouterUsagePendingCtx = ctx;
|
|
172
|
+
return openRouterUsageInFlight;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const now = Date.now();
|
|
176
|
+
const remaining = CODEX_USAGE_REFRESH_MS - (now - openRouterUsageLastAttempt);
|
|
177
|
+
if (!force && remaining > 0) {
|
|
178
|
+
if (openRouterUsageTimer === null) {
|
|
179
|
+
openRouterUsageTimer = setTimeout(() => {
|
|
180
|
+
openRouterUsageTimer = null;
|
|
181
|
+
void refreshOpenRouterUsage(ctx);
|
|
182
|
+
}, remaining);
|
|
183
|
+
}
|
|
184
|
+
return Promise.resolve();
|
|
185
|
+
}
|
|
186
|
+
cancelOpenRouterUsageRefresh();
|
|
187
|
+
openRouterUsageLastAttempt = now;
|
|
188
|
+
const generation = ++openRouterUsageGeneration;
|
|
189
|
+
|
|
190
|
+
let request: Promise<void>;
|
|
191
|
+
request = refreshOpenRouterUsageSnapshot(openRouterUsage, () => fetchOpenRouterUsage(ctx))
|
|
192
|
+
.then((snapshot) => {
|
|
193
|
+
if (generation !== openRouterUsageGeneration) return;
|
|
194
|
+
openRouterUsage = snapshot;
|
|
195
|
+
tuiRef?.requestRender();
|
|
196
|
+
})
|
|
197
|
+
.finally(() => {
|
|
198
|
+
if (openRouterUsageInFlight === request) openRouterUsageInFlight = null;
|
|
199
|
+
if (generation !== openRouterUsageGeneration) return;
|
|
200
|
+
const pendingCtx = openRouterUsagePendingCtx;
|
|
201
|
+
openRouterUsagePendingCtx = null;
|
|
202
|
+
if (pendingCtx) void refreshOpenRouterUsage(pendingCtx);
|
|
203
|
+
});
|
|
204
|
+
openRouterUsageInFlight = request;
|
|
205
|
+
return request;
|
|
206
|
+
};
|
|
126
207
|
|
|
127
208
|
const refreshCodexUsage = (ctx: ExtensionContext, force = false): Promise<void> => {
|
|
128
209
|
const model = ctx.model;
|
|
129
210
|
const isCodexSubscription = model?.provider === "openai-codex" && ctx.modelRegistry.isUsingOAuth(model);
|
|
130
211
|
if (!isCodexSubscription) {
|
|
212
|
+
cancelCodexUsageRefresh();
|
|
131
213
|
codexUsageGeneration++;
|
|
132
214
|
codexUsageInFlight = null;
|
|
133
215
|
codexUsage = null;
|
|
@@ -138,6 +220,7 @@ export default function footer(pi: ExtensionAPI) {
|
|
|
138
220
|
const config = getEffectiveConfig();
|
|
139
221
|
const enabled = LAYOUT_ROWS.some((row) => config[row]?.includes("codex_usage"));
|
|
140
222
|
if (!enabled) {
|
|
223
|
+
cancelCodexUsageRefresh();
|
|
141
224
|
codexUsageGeneration++;
|
|
142
225
|
codexUsageInFlight = null;
|
|
143
226
|
codexUsage = null;
|
|
@@ -145,10 +228,23 @@ export default function footer(pi: ExtensionAPI) {
|
|
|
145
228
|
tuiRef?.requestRender();
|
|
146
229
|
return Promise.resolve();
|
|
147
230
|
}
|
|
148
|
-
if (codexUsageInFlight)
|
|
231
|
+
if (codexUsageInFlight) {
|
|
232
|
+
codexUsagePendingCtx = ctx;
|
|
233
|
+
return codexUsageInFlight;
|
|
234
|
+
}
|
|
149
235
|
|
|
150
236
|
const now = Date.now();
|
|
151
|
-
|
|
237
|
+
const remaining = CODEX_USAGE_REFRESH_MS - (now - codexUsageLastAttempt);
|
|
238
|
+
if (!force && remaining > 0) {
|
|
239
|
+
if (codexUsageTimer === null) {
|
|
240
|
+
codexUsageTimer = setTimeout(() => {
|
|
241
|
+
codexUsageTimer = null;
|
|
242
|
+
void refreshCodexUsage(ctx);
|
|
243
|
+
}, remaining);
|
|
244
|
+
}
|
|
245
|
+
return Promise.resolve();
|
|
246
|
+
}
|
|
247
|
+
cancelCodexUsageRefresh();
|
|
152
248
|
codexUsageLastAttempt = now;
|
|
153
249
|
const generation = ++codexUsageGeneration;
|
|
154
250
|
|
|
@@ -161,6 +257,10 @@ export default function footer(pi: ExtensionAPI) {
|
|
|
161
257
|
})
|
|
162
258
|
.finally(() => {
|
|
163
259
|
if (codexUsageInFlight === request) codexUsageInFlight = null;
|
|
260
|
+
if (generation !== codexUsageGeneration) return;
|
|
261
|
+
const pendingCtx = codexUsagePendingCtx;
|
|
262
|
+
codexUsagePendingCtx = null;
|
|
263
|
+
if (pendingCtx) void refreshCodexUsage(pendingCtx);
|
|
164
264
|
});
|
|
165
265
|
codexUsageInFlight = request;
|
|
166
266
|
return request;
|
|
@@ -184,6 +284,7 @@ export default function footer(pi: ExtensionAPI) {
|
|
|
184
284
|
await showFooterConfigurator(ctx, () => {
|
|
185
285
|
tuiRef?.requestRender();
|
|
186
286
|
void refreshCodexUsage(ctx, true);
|
|
287
|
+
void refreshOpenRouterUsage(ctx, true);
|
|
187
288
|
});
|
|
188
289
|
},
|
|
189
290
|
});
|
|
@@ -194,10 +295,16 @@ export default function footer(pi: ExtensionAPI) {
|
|
|
194
295
|
currentCtx = ctx;
|
|
195
296
|
lastBranchLength = 0;
|
|
196
297
|
cachedUsageStats = null;
|
|
298
|
+
cancelCodexUsageRefresh();
|
|
197
299
|
codexUsage = null;
|
|
198
300
|
codexUsageLastAttempt = 0;
|
|
199
301
|
codexUsageGeneration++;
|
|
200
302
|
codexUsageInFlight = null;
|
|
303
|
+
cancelOpenRouterUsageRefresh();
|
|
304
|
+
openRouterUsage = null;
|
|
305
|
+
openRouterUsageLastAttempt = 0;
|
|
306
|
+
openRouterUsageGeneration++;
|
|
307
|
+
openRouterUsageInFlight = null;
|
|
201
308
|
llmMs = 0;
|
|
202
309
|
toolMs = 0;
|
|
203
310
|
ttftSamples = [];
|
|
@@ -212,11 +319,29 @@ export default function footer(pi: ExtensionAPI) {
|
|
|
212
319
|
if (ctx.hasUI) {
|
|
213
320
|
setupFooter(ctx);
|
|
214
321
|
void refreshCodexUsage(ctx, true);
|
|
322
|
+
void refreshOpenRouterUsage(ctx, true);
|
|
323
|
+
}
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
pi.on("session_shutdown", async () => {
|
|
327
|
+
cancelCodexUsageRefresh();
|
|
328
|
+
codexUsageGeneration++;
|
|
329
|
+
codexUsageInFlight = null;
|
|
330
|
+
cancelOpenRouterUsageRefresh();
|
|
331
|
+
openRouterUsageGeneration++;
|
|
332
|
+
openRouterUsageInFlight = null;
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
pi.on("message_end", async (event, ctx) => {
|
|
336
|
+
if (event.message.role === "assistant") {
|
|
337
|
+
void refreshCodexUsage(ctx);
|
|
338
|
+
void refreshOpenRouterUsage(ctx);
|
|
215
339
|
}
|
|
216
340
|
});
|
|
217
341
|
|
|
218
342
|
pi.on("model_select", async (_event: unknown, ctx: ExtensionContext) => {
|
|
219
343
|
void refreshCodexUsage(ctx, true);
|
|
344
|
+
void refreshOpenRouterUsage(ctx, true);
|
|
220
345
|
});
|
|
221
346
|
|
|
222
347
|
// Track user-prompt-to-completion turnaround. agent_start may fire multiple
|
|
@@ -237,6 +362,7 @@ export default function footer(pi: ExtensionAPI) {
|
|
|
237
362
|
statusTracker.agentSettled();
|
|
238
363
|
tuiRef?.requestRender();
|
|
239
364
|
void refreshCodexUsage(_ctx);
|
|
365
|
+
void refreshOpenRouterUsage(_ctx);
|
|
240
366
|
});
|
|
241
367
|
|
|
242
368
|
pi.on("turn_start", async (_event: unknown, _ctx: ExtensionContext) => {
|
|
@@ -266,6 +392,8 @@ export default function footer(pi: ExtensionAPI) {
|
|
|
266
392
|
}
|
|
267
393
|
statusTracker.toolEnded(event.toolCallId);
|
|
268
394
|
tuiRef?.requestRender();
|
|
395
|
+
void refreshCodexUsage(_ctx);
|
|
396
|
+
void refreshOpenRouterUsage(_ctx);
|
|
269
397
|
});
|
|
270
398
|
|
|
271
399
|
pi.on("message_update", async (_event: unknown, _ctx: ExtensionContext) => {
|
|
@@ -402,6 +530,7 @@ export default function footer(pi: ExtensionAPI) {
|
|
|
402
530
|
sessionStats: { prompts: branchPrompts, apiCalls: branchApiCalls, toolCalls: branchToolCalls, llmMs, toolMs, ttftSamples, lastTurnaroundMs },
|
|
403
531
|
agentStatus: statusTracker.status(),
|
|
404
532
|
codexUsage,
|
|
533
|
+
openRouterUsage,
|
|
405
534
|
};
|
|
406
535
|
}
|
|
407
536
|
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
|
|
3
|
+
const USAGE_URL = "https://openrouter.ai/api/v1/key";
|
|
4
|
+
const REQUEST_TIMEOUT_MS = 15_000;
|
|
5
|
+
|
|
6
|
+
export interface OpenRouterUsageSnapshot {
|
|
7
|
+
used: number;
|
|
8
|
+
limit: number;
|
|
9
|
+
fetchedAt: number;
|
|
10
|
+
stale: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
type FetchLike = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
|
|
14
|
+
type UsageRecord = Record<string, unknown>;
|
|
15
|
+
|
|
16
|
+
function asRecord(value: unknown): UsageRecord | null {
|
|
17
|
+
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
18
|
+
? value as UsageRecord
|
|
19
|
+
: null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function parseOpenRouterUsage(data: unknown, fetchedAt = Date.now()): OpenRouterUsageSnapshot | null {
|
|
23
|
+
const root = asRecord(data);
|
|
24
|
+
const key = asRecord(root?.data);
|
|
25
|
+
const limit = key?.limit;
|
|
26
|
+
const remaining = key?.limit_remaining;
|
|
27
|
+
if (
|
|
28
|
+
typeof limit !== "number" || !Number.isFinite(limit) || limit <= 0 ||
|
|
29
|
+
typeof remaining !== "number" || !Number.isFinite(remaining)
|
|
30
|
+
) return null;
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
used: Math.max(0, Math.round((limit - remaining) * 1_000_000_000) / 1_000_000_000),
|
|
34
|
+
limit,
|
|
35
|
+
fetchedAt,
|
|
36
|
+
stale: false,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function refreshOpenRouterUsageSnapshot(
|
|
41
|
+
previous: OpenRouterUsageSnapshot | null,
|
|
42
|
+
load: () => Promise<OpenRouterUsageSnapshot>,
|
|
43
|
+
): Promise<OpenRouterUsageSnapshot | null> {
|
|
44
|
+
try {
|
|
45
|
+
return await load();
|
|
46
|
+
} catch {
|
|
47
|
+
return previous ? { ...previous, stale: true } : null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function fetchOpenRouterUsage(
|
|
52
|
+
ctx: ExtensionContext,
|
|
53
|
+
fetchFn: FetchLike = globalThis.fetch,
|
|
54
|
+
fetchedAt = Date.now(),
|
|
55
|
+
): Promise<OpenRouterUsageSnapshot> {
|
|
56
|
+
const model = ctx.model;
|
|
57
|
+
if (!model || model.provider !== "openrouter") {
|
|
58
|
+
throw new Error("OpenRouter usage requires an openrouter model");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
|
|
62
|
+
if (!auth.ok || !auth.apiKey) throw new Error("OpenRouter authentication is unavailable");
|
|
63
|
+
|
|
64
|
+
const response = await fetchFn(USAGE_URL, {
|
|
65
|
+
headers: {
|
|
66
|
+
Authorization: `Bearer ${auth.apiKey}`,
|
|
67
|
+
Accept: "application/json",
|
|
68
|
+
"User-Agent": "opl-footer-openrouter-usage",
|
|
69
|
+
},
|
|
70
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
71
|
+
});
|
|
72
|
+
if (!response.ok) throw new Error(`OpenRouter usage request failed (${response.status})`);
|
|
73
|
+
|
|
74
|
+
const snapshot = parseOpenRouterUsage(await response.json(), fetchedAt);
|
|
75
|
+
if (!snapshot) throw new Error("OpenRouter usage response contained no usable key limit");
|
|
76
|
+
return snapshot;
|
|
77
|
+
}
|
|
@@ -15,6 +15,7 @@ import { modeSwitcherSegment } from "./mode-switcher.js";
|
|
|
15
15
|
import { sessionStatsSegment, perfStatsSegment } from "./session-stats.js";
|
|
16
16
|
import { statusSegment } from "./status.js";
|
|
17
17
|
import { codexUsageSegment } from "./codex-usage.js";
|
|
18
|
+
import { openRouterUsageSegment } from "./openrouter-usage.js";
|
|
18
19
|
|
|
19
20
|
const SEGMENTS = {
|
|
20
21
|
pi: piSegment,
|
|
@@ -39,6 +40,7 @@ const SEGMENTS = {
|
|
|
39
40
|
perf_stats: perfStatsSegment,
|
|
40
41
|
status: statusSegment,
|
|
41
42
|
codex_usage: codexUsageSegment,
|
|
43
|
+
openrouter_usage: openRouterUsageSegment,
|
|
42
44
|
};
|
|
43
45
|
|
|
44
46
|
export function renderSegment(id: StatusLineSegmentId, ctx: SegmentContext): RenderedSegment {
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { RenderedSegment, SegmentContext } from "../types.js";
|
|
2
|
+
import { applyColor } from "../theme.js";
|
|
3
|
+
|
|
4
|
+
function formatUsd(value: number): string {
|
|
5
|
+
return `$${value.toFixed(4)}`;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export const openRouterUsageSegment = {
|
|
9
|
+
id: "openrouter_usage" as const,
|
|
10
|
+
render(ctx: SegmentContext): RenderedSegment {
|
|
11
|
+
const snapshot = ctx.openRouterUsage;
|
|
12
|
+
if (!snapshot) return { content: "", visible: false };
|
|
13
|
+
|
|
14
|
+
const percent = (snapshot.used / snapshot.limit) * 100;
|
|
15
|
+
const stale = snapshot.stale ? applyColor(ctx.theme, "dim", " (stale)") : "";
|
|
16
|
+
return {
|
|
17
|
+
content: `${formatUsd(snapshot.used)} / ${formatUsd(snapshot.limit)} (${percent.toFixed(1)}%)${stale}`,
|
|
18
|
+
visible: true,
|
|
19
|
+
};
|
|
20
|
+
},
|
|
21
|
+
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Theme, ThemeColor } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import type { IconSet } from "./icons.js";
|
|
3
3
|
import type { CodexUsageSnapshot } from "./codex-usage.js";
|
|
4
|
+
import type { OpenRouterUsageSnapshot } from "./openrouter-usage.js";
|
|
4
5
|
|
|
5
6
|
// Theme color - either a pi theme color name or a custom hex color
|
|
6
7
|
export type ColorValue = ThemeColor | `#${string}`;
|
|
@@ -56,6 +57,7 @@ export type StatusLineSegmentId =
|
|
|
56
57
|
| "perf_stats"
|
|
57
58
|
| "status"
|
|
58
59
|
| "codex_usage"
|
|
60
|
+
| "openrouter_usage"
|
|
59
61
|
| "separator"
|
|
60
62
|
| `text:${string}`;
|
|
61
63
|
|
|
@@ -134,6 +136,7 @@ export interface SegmentContext {
|
|
|
134
136
|
sessionStats: SessionStats;
|
|
135
137
|
agentStatus: AgentStatus;
|
|
136
138
|
codexUsage: CodexUsageSnapshot | null;
|
|
139
|
+
openRouterUsage: OpenRouterUsageSnapshot | null;
|
|
137
140
|
now?: number;
|
|
138
141
|
}
|
|
139
142
|
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openlines/opl-pi-sht",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.12",
|
|
4
4
|
"description": "opl-pi-sht (shouldHaveThat) Pi coding-agent extension kit: token-efficient context, tool and session safety, fingerprinted /init, model benchmarking, web access, and browser automation.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
10
|
"extensions/",
|
|
11
|
-
"configs/"
|
|
11
|
+
"configs/",
|
|
12
|
+
"images/"
|
|
12
13
|
],
|
|
13
14
|
"keywords": [
|
|
14
15
|
"pi",
|