@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 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 v0.99.x release`
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, and an optional exact remaining ChatGPT Codex 5-hour/weekly subscription-quota segment with reset countdowns and stale-snapshot fallback; `/configure-opl` edits and reorders its layout interactively.
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
  ![input-footer](images/ss-input-footer.png)
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 is hidden by default; add it to any row to show ChatGPT Codex subscription windows.",
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 runs after session start, model selection, and settled agent turns, with a 30-second refresh floor and a 15-second request timeout. It classifies the returned windows by duration rather than response order. This is ChatGPT subscription quota, not OpenAI Platform API-key usage or billing.
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) return codexUsageInFlight;
231
+ if (codexUsageInFlight) {
232
+ codexUsagePendingCtx = ctx;
233
+ return codexUsageInFlight;
234
+ }
149
235
 
150
236
  const now = Date.now();
151
- if (!force && now - codexUsageLastAttempt < CODEX_USAGE_REFRESH_MS) return Promise.resolve();
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.10",
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",