@openlines/opl-pi-sht 0.2.11 → 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,8 @@
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
18
20
  > ### v0.2.11 - OpenRouter key-limit usage in opl-footer
19
21
  > - `opl-footer` added an OpenRouter API key's used amount, cap, and percentage.
20
22
  > ### v0.2.10 - Codex subscription usage in opl-footer
@@ -141,7 +143,7 @@ Cold prompt-cache write, measured /init session (opl-modes lazy tools + MCP adap
141
143
 
142
144
  ### See what the agent is doing
143
145
 
144
- - **`opl-footer`** surfaces model, cost, token and cache activity, git state, agent status, per-turn timing, an optional exact remaining ChatGPT Codex subscription-quota segment, and optional OpenRouter API-key cap usage. 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.
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.
145
147
  - **`opl-input`** is a configurable editor that reflects the active mode's identity, so you always know which mode you are typing into.
146
148
  ![input-footer](images/ss-input-footer.png)
147
149
 
@@ -100,7 +100,7 @@ See the tracked [`configs/opl-footer.json.sample`](../../configs/opl-footer.json
100
100
 
101
101
  ## Codex Subscription Usage
102
102
 
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 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.
104
104
 
105
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.
106
106
 
@@ -108,7 +108,7 @@ No OAuth token, account ID, or response body is written to disk, logged, added t
108
108
 
109
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
110
 
111
- It runs after session start, model selection, and settled agent turns, with a 30-second refresh floor and a 15-second request timeout. 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.
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
112
 
113
113
  ## Context Bar
114
114
 
@@ -124,14 +124,31 @@ export default function footer(pi: ExtensionAPI) {
124
124
  let codexUsageLastAttempt = 0;
125
125
  let codexUsageGeneration = 0;
126
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
+ };
127
135
  let openRouterUsage: OpenRouterUsageSnapshot | null = null;
128
136
  let openRouterUsageLastAttempt = 0;
129
137
  let openRouterUsageGeneration = 0;
130
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
+ };
131
147
 
132
148
  const refreshOpenRouterUsage = (ctx: ExtensionContext, force = false): Promise<void> => {
133
149
  const isOpenRouter = ctx.model?.provider === "openrouter";
134
150
  if (!isOpenRouter) {
151
+ cancelOpenRouterUsageRefresh();
135
152
  openRouterUsageGeneration++;
136
153
  openRouterUsageInFlight = null;
137
154
  openRouterUsage = null;
@@ -142,6 +159,7 @@ export default function footer(pi: ExtensionAPI) {
142
159
  const config = getEffectiveConfig();
143
160
  const enabled = LAYOUT_ROWS.some((row) => config[row]?.includes("openrouter_usage"));
144
161
  if (!enabled) {
162
+ cancelOpenRouterUsageRefresh();
145
163
  openRouterUsageGeneration++;
146
164
  openRouterUsageInFlight = null;
147
165
  openRouterUsage = null;
@@ -149,10 +167,23 @@ export default function footer(pi: ExtensionAPI) {
149
167
  tuiRef?.requestRender();
150
168
  return Promise.resolve();
151
169
  }
152
- if (openRouterUsageInFlight) return openRouterUsageInFlight;
170
+ if (openRouterUsageInFlight) {
171
+ openRouterUsagePendingCtx = ctx;
172
+ return openRouterUsageInFlight;
173
+ }
153
174
 
154
175
  const now = Date.now();
155
- if (!force && now - openRouterUsageLastAttempt < CODEX_USAGE_REFRESH_MS) return Promise.resolve();
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();
156
187
  openRouterUsageLastAttempt = now;
157
188
  const generation = ++openRouterUsageGeneration;
158
189
 
@@ -165,6 +196,10 @@ export default function footer(pi: ExtensionAPI) {
165
196
  })
166
197
  .finally(() => {
167
198
  if (openRouterUsageInFlight === request) openRouterUsageInFlight = null;
199
+ if (generation !== openRouterUsageGeneration) return;
200
+ const pendingCtx = openRouterUsagePendingCtx;
201
+ openRouterUsagePendingCtx = null;
202
+ if (pendingCtx) void refreshOpenRouterUsage(pendingCtx);
168
203
  });
169
204
  openRouterUsageInFlight = request;
170
205
  return request;
@@ -174,6 +209,7 @@ export default function footer(pi: ExtensionAPI) {
174
209
  const model = ctx.model;
175
210
  const isCodexSubscription = model?.provider === "openai-codex" && ctx.modelRegistry.isUsingOAuth(model);
176
211
  if (!isCodexSubscription) {
212
+ cancelCodexUsageRefresh();
177
213
  codexUsageGeneration++;
178
214
  codexUsageInFlight = null;
179
215
  codexUsage = null;
@@ -184,6 +220,7 @@ export default function footer(pi: ExtensionAPI) {
184
220
  const config = getEffectiveConfig();
185
221
  const enabled = LAYOUT_ROWS.some((row) => config[row]?.includes("codex_usage"));
186
222
  if (!enabled) {
223
+ cancelCodexUsageRefresh();
187
224
  codexUsageGeneration++;
188
225
  codexUsageInFlight = null;
189
226
  codexUsage = null;
@@ -191,10 +228,23 @@ export default function footer(pi: ExtensionAPI) {
191
228
  tuiRef?.requestRender();
192
229
  return Promise.resolve();
193
230
  }
194
- if (codexUsageInFlight) return codexUsageInFlight;
231
+ if (codexUsageInFlight) {
232
+ codexUsagePendingCtx = ctx;
233
+ return codexUsageInFlight;
234
+ }
195
235
 
196
236
  const now = Date.now();
197
- 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();
198
248
  codexUsageLastAttempt = now;
199
249
  const generation = ++codexUsageGeneration;
200
250
 
@@ -207,6 +257,10 @@ export default function footer(pi: ExtensionAPI) {
207
257
  })
208
258
  .finally(() => {
209
259
  if (codexUsageInFlight === request) codexUsageInFlight = null;
260
+ if (generation !== codexUsageGeneration) return;
261
+ const pendingCtx = codexUsagePendingCtx;
262
+ codexUsagePendingCtx = null;
263
+ if (pendingCtx) void refreshCodexUsage(pendingCtx);
210
264
  });
211
265
  codexUsageInFlight = request;
212
266
  return request;
@@ -241,10 +295,12 @@ export default function footer(pi: ExtensionAPI) {
241
295
  currentCtx = ctx;
242
296
  lastBranchLength = 0;
243
297
  cachedUsageStats = null;
298
+ cancelCodexUsageRefresh();
244
299
  codexUsage = null;
245
300
  codexUsageLastAttempt = 0;
246
301
  codexUsageGeneration++;
247
302
  codexUsageInFlight = null;
303
+ cancelOpenRouterUsageRefresh();
248
304
  openRouterUsage = null;
249
305
  openRouterUsageLastAttempt = 0;
250
306
  openRouterUsageGeneration++;
@@ -267,6 +323,22 @@ export default function footer(pi: ExtensionAPI) {
267
323
  }
268
324
  });
269
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);
339
+ }
340
+ });
341
+
270
342
  pi.on("model_select", async (_event: unknown, ctx: ExtensionContext) => {
271
343
  void refreshCodexUsage(ctx, true);
272
344
  void refreshOpenRouterUsage(ctx, true);
@@ -320,6 +392,8 @@ export default function footer(pi: ExtensionAPI) {
320
392
  }
321
393
  statusTracker.toolEnded(event.toolCallId);
322
394
  tuiRef?.requestRender();
395
+ void refreshCodexUsage(_ctx);
396
+ void refreshOpenRouterUsage(_ctx);
323
397
  });
324
398
 
325
399
  pi.on("message_update", async (_event: unknown, _ctx: ExtensionContext) => {
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.11",
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",