@robhowley/pi-openrouter 0.8.3 → 0.9.1

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.
Files changed (40) hide show
  1. package/README.md +87 -4
  2. package/extensions/openrouter/__tests__/cache.test.ts +769 -0
  3. package/extensions/openrouter/__tests__/client.test.ts +333 -15
  4. package/extensions/openrouter/__tests__/commands.test.ts +816 -0
  5. package/extensions/openrouter/__tests__/fixtures.ts +140 -1
  6. package/extensions/openrouter/__tests__/format.test.ts +19 -0
  7. package/extensions/openrouter/__tests__/hooks.test.ts +276 -0
  8. package/extensions/openrouter/__tests__/index.test.ts +163 -0
  9. package/extensions/openrouter/__tests__/local-usage.test.ts +777 -0
  10. package/extensions/openrouter/__tests__/normalizers.test.ts +288 -0
  11. package/extensions/openrouter/__tests__/overlay.test.ts +225 -0
  12. package/extensions/openrouter/__tests__/session-state.test.ts +233 -0
  13. package/extensions/openrouter/__tests__/session.test.ts +44 -43
  14. package/extensions/openrouter/account-client.ts +11 -61
  15. package/extensions/openrouter/cache.ts +203 -91
  16. package/extensions/openrouter/client.ts +49 -3
  17. package/extensions/openrouter/commands.ts +555 -0
  18. package/extensions/openrouter/format.ts +7 -4
  19. package/extensions/openrouter/hooks.ts +229 -0
  20. package/extensions/openrouter/index.ts +13 -589
  21. package/extensions/openrouter/local-usage.ts +145 -22
  22. package/extensions/openrouter/models/__tests__/cache.test.ts +63 -2
  23. package/extensions/openrouter/models/__tests__/mapper-overrides.test.ts +102 -0
  24. package/extensions/openrouter/models/__tests__/mapper.test.ts +29 -0
  25. package/extensions/openrouter/models/__tests__/override-commands.test.ts +668 -0
  26. package/extensions/openrouter/models/__tests__/overrides.test.ts +237 -0
  27. package/extensions/openrouter/models/__tests__/sync.test.ts +156 -4
  28. package/extensions/openrouter/models/cache.ts +27 -2
  29. package/extensions/openrouter/models/mapper.ts +60 -77
  30. package/extensions/openrouter/models/override-commands.ts +434 -0
  31. package/extensions/openrouter/models/overrides.ts +174 -0
  32. package/extensions/openrouter/models/skip-hints.ts +19 -0
  33. package/extensions/openrouter/models/sync.ts +22 -10
  34. package/extensions/openrouter/models/types.ts +31 -1
  35. package/extensions/openrouter/normalizers.ts +128 -0
  36. package/extensions/openrouter/overlay.ts +19 -8
  37. package/extensions/openrouter/session-state.ts +110 -0
  38. package/extensions/openrouter/session.ts +16 -0
  39. package/extensions/openrouter/types.ts +28 -9
  40. package/package.json +1 -1
@@ -1,596 +1,20 @@
1
- import type { ExtensionAPI, ExtensionContext } from '@mariozechner/pi-coding-agent';
2
- import type { UsageSummary } from './types.js';
3
- import { MS_PER_MINUTE } from './models/types.js';
1
+ import type { ExtensionAPI } from '@mariozechner/pi-coding-agent';
2
+ import { registerOpenRouterCommands } from './commands.js';
4
3
  import {
5
- usageCache,
6
- startBackgroundRefresh,
7
- stopBackgroundRefresh,
8
- fetchAndAggregate,
9
- } from './cache.js';
10
- import { AuthError } from './client.js';
11
- import { UsageOverlayComponent } from './overlay.js';
12
- import { formatSessionId, isOpenRouterRequest, type OpenRouterSessionState } from './session.js';
13
- import { writeLocalUsage, type LocalUsageEvent } from './local-usage.js';
14
- import { AccountOverlayComponent } from './account-overlay.js';
15
- import { computeRollupStatus, sortKeys } from './account-format.js';
16
- import { getAllKeys, getCurrentKey, getAccountCredits } from './account-client.js';
17
- import type { KeyInfo } from './account-types.js';
18
- import type { RollupStatus } from './account-types.js';
19
- import crypto from 'node:crypto';
4
+ addSessionIdToOpenRouterRequest,
5
+ getCurrentSessionId,
6
+ initializeSessionState,
7
+ installOpenRouterHooks,
8
+ loadStartupCacheState,
9
+ } from './hooks.js';
20
10
 
21
- // Import models sync
22
- import {
23
- syncModels,
24
- getSyncState,
25
- isSyncEnabled,
26
- getSkipReasonsAsync,
27
- groupSkipReasons,
28
- } from './models/sync.js';
29
- import { loadCache, getCacheAgeMs, formatDuration } from './models/cache.js';
30
- import { mapOpenRouterModels } from './models/mapper.js';
31
-
32
- // Store the current session state for use in command handlers
33
- let currentSessionState: OpenRouterSessionState | null = null;
34
- let sessionTrackingInstalled = false;
35
-
36
- // Store startup cache state for notifications
37
- let startupCacheInfo: { count: number; age: string } | undefined;
38
-
39
- // =============================================================================
40
- // Utility Functions
41
- // =============================================================================
42
-
43
- /**
44
- * Format skipped models details for --skipped flag output.
45
- */
46
- function formatSkippedDetails(
47
- skipCount: number,
48
- groupedReasons: Record<string, number>,
49
- skipReasons: Array<{ id: string; reason: string }>,
50
- ): string {
51
- if (skipCount === 0) {
52
- return '\n\nNo skipped models';
53
- }
54
-
55
- let details = `\n\nOpenRouter skipped models: ${skipCount}\n`;
56
- for (const [reason, count] of Object.entries(groupedReasons)) {
57
- details += `\n${count} ${reason}\n`;
58
- const modelsWithReason = skipReasons.filter((r) => r.reason === reason).map((r) => r.id);
59
- for (const id of modelsWithReason) {
60
- details += `- ${id}\n`;
61
- }
62
- }
63
- return details;
64
- }
65
-
66
- // =============================================================================
67
- // Session State Management
68
- // =============================================================================
69
-
70
- function getCurrentSessionId(ctx: { sessionManager: { getSessionId(): string } }): string {
71
- if (currentSessionState) {
72
- return currentSessionState.sessionId;
73
- }
74
-
75
- try {
76
- const sessionId = ctx.sessionManager.getSessionId();
77
- let formattedSessionId: string;
78
- if (sessionId && sessionId !== '') {
79
- formattedSessionId = formatSessionId(sessionId);
80
- } else {
81
- formattedSessionId = formatSessionId(crypto.randomUUID());
82
- }
83
-
84
- currentSessionState = { sessionId: formattedSessionId };
85
- return formattedSessionId;
86
- } catch {
87
- // Generate fallback on any error
88
- const fallbackId = formatSessionId(crypto.randomUUID());
89
- currentSessionState = { sessionId: fallbackId };
90
- return fallbackId;
91
- }
92
- }
11
+ export { addSessionIdToOpenRouterRequest, getCurrentSessionId };
93
12
 
94
13
  export default async function (pi: ExtensionAPI) {
95
- // Eager cache load on extension startup (before any sessions)
96
- startupCacheInfo = undefined;
97
- let startupCacheWarning: string | undefined;
98
- if (isSyncEnabled()) {
99
- const cache = await loadCache().catch(() => null);
100
-
101
- if (cache?.models.length) {
102
- try {
103
- const { configs } = await mapOpenRouterModels(cache.models);
104
-
105
- // Register models directly with Pi's OpenRouter provider
106
- pi.registerProvider('openrouter', {
107
- baseUrl: 'https://openrouter.ai/api/v1',
108
- apiKey: 'OPENROUTER_API_KEY',
109
- api: 'openai-completions',
110
- models: configs,
111
- authHeader: true,
112
- });
113
-
114
- // Store for session_start notification
115
- const age = formatDuration(getCacheAgeMs(cache));
116
- startupCacheInfo = { count: configs.length, age };
117
- } catch (error) {
118
- startupCacheInfo = undefined;
119
- startupCacheWarning = `OpenRouter: cached models found but failed to register: ${error instanceof Error ? error.message : String(error)}`;
120
- }
121
- }
122
- }
123
-
124
- // Install before_provider_request hook once
125
- if (!sessionTrackingInstalled) {
126
- sessionTrackingInstalled = true;
127
-
128
- pi.on('before_provider_request', (event, ctx) => {
129
- try {
130
- // Validate the payload exists
131
- const ev = event as unknown as Record<string, unknown>;
132
- const payload = ev['payload'] as Record<string, unknown> | undefined;
133
- if (!payload) {
134
- return;
135
- }
136
-
137
- // Check if this is an OpenRouter request
138
- const isOpenRouter = isOpenRouterRequest(event, ctx);
139
- if (!isOpenRouter) {
140
- return;
141
- }
142
-
143
- // Do not overwrite existing session_id
144
- if ('session_id' in payload && payload['session_id'] !== undefined) {
145
- return;
146
- }
147
-
148
- // Add session_id to the payload (OpenRouter-specific field)
149
- return {
150
- ...payload,
151
- session_id: getCurrentSessionId(ctx),
152
- };
153
- } catch {
154
- // Fail open - silently ignore errors
155
- return;
156
- }
157
- });
158
- }
159
-
160
- // Hook turn_end to capture completed OpenRouter turns for local logging
161
- pi.on('turn_end', async (event, ctx) => {
162
- try {
163
- const turnEvent = event as unknown as Record<string, unknown>;
164
-
165
- const message = turnEvent['message'] as Record<string, unknown> | undefined;
166
- if (!message) return;
167
-
168
- // Check if this is an OpenRouter request based on the message content/model
169
- // Include url/endpoint from turnEvent so isOpenRouterRequest can check them
170
- const isOpenRouter = isOpenRouterRequest(
171
- {
172
- type: 'before_provider_request',
173
- payload: message,
174
- url: turnEvent['url'],
175
- endpoint: turnEvent['endpoint'],
176
- } as unknown as Parameters<typeof isOpenRouterRequest>[0],
177
- ctx,
178
- );
179
- if (!isOpenRouter) return;
180
-
181
- // Check if the message has usage data
182
- const usage = (message as { usage?: unknown })['usage'] as
183
- | {
184
- input?: number;
185
- output?: number;
186
- cacheRead?: number;
187
- cacheWrite?: number;
188
- totalTokens?: number;
189
- cost?: {
190
- input?: number;
191
- output?: number;
192
- cacheRead?: number;
193
- cacheWrite?: number;
194
- total?: number;
195
- };
196
- }
197
- | undefined;
198
- if (!usage) return;
199
-
200
- // Extract model from the message
201
- const model = message['model'] as string | undefined;
202
- const responseModel = message['responseModel'] as string | undefined;
203
- const modelToLog = model || responseModel;
204
-
205
- // Calculate total cost from usage.cost.total
206
- const totalCost = usage.cost?.total;
207
-
208
- const localEvent: LocalUsageEvent = {
209
- id: crypto.randomUUID(),
210
- generationId: String(message['responseId'] ?? ''),
211
- sessionId: getCurrentSessionId(ctx),
212
- completedAt: new Date().toISOString(),
213
- model: modelToLog ?? 'unknown',
214
- requests: 1,
215
- promptTokens: usage.input ?? 0,
216
- completionTokens: usage.output ?? 0,
217
- reasoningTokens: 0,
218
- cacheReadTokens: usage.cacheRead ?? 0,
219
- cacheWriteTokens: usage.cacheWrite ?? 0,
220
- cost: totalCost ?? 0,
221
- };
222
-
223
- // Write to local JSONL - fail open (don't throw)
224
- writeLocalUsage(localEvent).catch(() => {});
225
- } catch {
226
- // Fail open - silently ignore errors
227
- }
228
- });
229
-
230
- pi.on('session_shutdown', () => {
231
- stopBackgroundRefresh();
232
- });
233
-
234
- // Notify on first session start after extension load
235
- pi.on('session_start', (event, ctx) => {
236
- if (!ctx.hasUI) return;
237
-
238
- // Show a persistent status indicator
239
- if (startupCacheInfo) {
240
- const statusText = `OpenRouter ${startupCacheInfo.count} models`;
241
- ctx.ui.setStatus('openrouter', ctx.ui.theme.fg('dim', statusText));
242
- }
243
-
244
- // Show a one-time notification on startup
245
- if (event.reason === 'startup' && startupCacheInfo) {
246
- const notice = `OpenRouter: ${startupCacheInfo.count} models loaded from cache (${startupCacheInfo.age} old). Run /openrouter models-sync to refresh.`;
247
- ctx.ui.notify(notice, 'info');
248
- }
249
-
250
- if (event.reason === 'startup' && startupCacheWarning) {
251
- ctx.ui.notify(startupCacheWarning, 'warning');
252
- }
253
- });
254
-
255
- pi.registerCommand('openrouter-usage', {
256
- description: 'Show OpenRouter usage: caps, spend, burn rate, and model breakdowns',
257
- getArgumentCompletions: () => null,
258
- handler: async (args, ctx) => {
259
- startBackgroundRefresh();
260
- const subcommand = args.trim() || undefined;
261
- await showUsageOverlay(ctx, subcommand);
262
- },
263
- });
264
-
265
- pi.registerCommand('openrouter-session', {
266
- description: 'Show the current OpenRouter session ID for request grouping',
267
- getArgumentCompletions: () => null,
268
- handler: async (_args, ctx) => {
269
- const idToShow = getCurrentSessionId(ctx);
270
- ctx.ui.notify(`OpenRouter session_id\n${idToShow}`, 'info');
271
- },
272
- });
273
-
274
- pi.registerCommand('openrouter-account', {
275
- description: 'Show OpenRouter account and key health',
276
- getArgumentCompletions: () => null,
277
- handler: async (_args, ctx) => {
278
- await showAccountOverlay(ctx);
279
- },
280
- });
281
-
282
- // ============== MODELS COMMANDS (subcommands of /openrouter) ==============
283
-
284
- // Single entry point with subcommands: /openrouter [usage|account|session|models-sync|models-status]
285
- pi.registerCommand('openrouter', {
286
- description: 'OpenRouter commands: usage, account, session, models-sync, models-status',
287
- getArgumentCompletions: (prefix: string) => {
288
- const subcommands = ['usage', 'account', 'session', 'models-sync', 'models-status'];
289
- const items = subcommands
290
- .filter((s) => s.startsWith(prefix))
291
- .map((s) => ({ value: s, label: s }));
292
- return items.length > 0 ? items : null;
293
- },
294
- handler: async (args, ctx) => {
295
- // Parse subcommand and flags
296
- const parts = args.trim().split(/\s+/);
297
- const subcommand = parts[0] || '';
298
- const flags = parts.slice(1).reduce(
299
- (acc, flag) => {
300
- acc[flag] = true;
301
- return acc;
302
- },
303
- {} as Record<string, boolean>,
304
- );
305
-
306
- switch (subcommand) {
307
- case 'usage': {
308
- startBackgroundRefresh();
309
- await showUsageOverlay(ctx, undefined);
310
- break;
311
- }
312
- case 'account': {
313
- await showAccountOverlay(ctx);
314
- break;
315
- }
316
- case 'session': {
317
- ctx.ui.notify(`OpenRouter session_id\n${getCurrentSessionId(ctx)}`, 'info');
318
- break;
319
- }
320
- case 'models-sync': {
321
- if (!isSyncEnabled()) {
322
- ctx.ui.notify(
323
- 'OpenRouter model sync is disabled. Set openrouterModelSync: true in ~/.pi/agent/settings.json to enable.',
324
- 'error',
325
- );
326
- return;
327
- }
328
- const result = await syncModels(ctx);
329
-
330
- // Display brief result using same color scheme as overlays
331
- if (!result.success) {
332
- let message = '';
333
- if (result.source === 'cache') {
334
- message = `OpenRouter models sync failed\n${result.registeredCount} registered from cache\nCache age: ${formatDuration(result.cacheAgeMs)}\nError: ${result.error}`;
335
- } else {
336
- message = `OpenRouter models unavailable\n0 registered\nError: ${result.error}`;
337
- }
338
- ctx.ui.notify(message, result.source === 'cache' ? 'warning' : 'error');
339
- } else {
340
- const message = `OpenRouter models synced\n${result.registeredCount} registered${result.skippedCount > 0 ? ` · ${result.skippedCount} skipped` : ''} · cache updated`;
341
- ctx.ui.notify(message, 'info');
342
- }
343
- break;
344
- }
345
- case 'models-status': {
346
- const state = getSyncState();
347
- const skipReasons = await getSkipReasonsAsync();
348
- const groupedReasons = groupSkipReasons(skipReasons);
349
-
350
- // Get real-time cache age from disk
351
- const cache = await loadCache();
352
- const cacheAgeMs = cache ? getCacheAgeMs(cache) : null;
353
-
354
- if (!state && !cache) {
355
- ctx.ui.notify('OpenRouter models: not synced', 'error');
356
- } else if (!state && cache) {
357
- // Cache exists but no in-memory state (new Pi session)
358
- const cachedCount = cache.models.length;
359
- const message = `OpenRouter models cached\n${cachedCount} models in cache · age: ${formatDuration(cacheAgeMs)}\nRun '/openrouter models-sync' to register models`;
360
- ctx.ui.notify(message, 'info');
361
- } else if (state?.success) {
362
- const skipCount = skipReasons.length;
363
- let message = `OpenRouter models healthy\n${state.registeredCount} registered${skipCount > 0 ? ` · ${skipCount} skipped` : ''} · cache age: ${formatDuration(cacheAgeMs)}`;
364
-
365
- if (flags['--skipped']) {
366
- message += formatSkippedDetails(skipCount, groupedReasons, skipReasons);
367
- }
368
- ctx.ui.notify(message, 'info');
369
- } else if (state?.source === 'cache') {
370
- const skipCount = skipReasons.length;
371
- let message = `OpenRouter models cached\n${state.registeredCount} registered${skipCount > 0 ? ` · ${skipCount} skipped` : ''}\nCache age: ${formatDuration(cacheAgeMs)}\nError: ${state.error}`;
372
-
373
- if (flags['--skipped']) {
374
- message += formatSkippedDetails(skipCount, groupedReasons, skipReasons);
375
- }
376
- ctx.ui.notify(message, 'warning');
377
- } else {
378
- ctx.ui.notify(
379
- `OpenRouter models broken\n0 registered\nError: ${state?.error}`,
380
- 'error',
381
- );
382
- }
383
- break;
384
- }
385
- default: {
386
- const available = ['usage', 'account', 'session', 'models-sync', 'models-status'];
387
- const message =
388
- available.length > 0
389
- ? `Available subcommands: ${available.join(', ')}${available.length > 1 ? '' : ''}`
390
- : 'No subcommands available';
391
- ctx.ui.notify(`OpenRouter subcommands\n${message}`, 'error');
392
- break;
393
- }
394
- }
395
- },
396
- });
397
- }
398
-
399
- async function showAccountOverlay(ctx: ExtensionContext) {
400
- let error: string | null = null;
401
- let keyInfo: KeyInfo[] | null = null;
402
- let credits: number | null = null;
403
-
404
- try {
405
- // Try to get all keys with management key
406
- const allKeys = await getAllKeys();
407
-
408
- if (allKeys && allKeys.length > 0) {
409
- keyInfo = allKeys;
410
- } else {
411
- // getAllKeys() returns null or empty array when management key isn't available
412
- // or when the API call fails with 403
413
- error = 'Key list unavailable - set OPENROUTER_MANAGEMENT_KEY for full key inventory.';
414
-
415
- // Fall back to current key only
416
- try {
417
- const currentKey = await getCurrentKey();
418
- if (currentKey) {
419
- keyInfo = [currentKey];
420
- // Clear the error since we successfully got current key
421
- error = null;
422
- } else {
423
- error = 'Failed to retrieve current key metadata. Check your API key permissions.';
424
- }
425
- } catch (err) {
426
- error = `Failed to retrieve current key: ${(err as Error).message}`;
427
- }
428
- }
429
-
430
- // Try to get account credits
431
- credits = await getAccountCredits();
432
-
433
- // Set error if we have no keys and no credits
434
- if (!keyInfo && !credits) {
435
- error =
436
- 'OpenRouter API key not found. Set OPENROUTER_MANAGEMENT_KEY (preferred) or OPENROUTER_API_KEY to use /openrouter-account.';
437
- }
438
-
439
- // Set error if we have credits but no keys
440
- if (!keyInfo && credits !== null) {
441
- error =
442
- error ||
443
- 'Key information unavailable. Set OPENROUTER_MANAGEMENT_KEY for full key inventory.';
444
- }
445
-
446
- // Compute rollup status
447
- const rollupStatus = keyInfo
448
- ? computeRollupStatus(keyInfo)
449
- : { status: 'unavailable' as const };
450
-
451
- // Sort keys
452
- if (keyInfo) {
453
- const sortedKeys = sortKeys(keyInfo);
454
- keyInfo = sortedKeys;
455
- }
456
-
457
- await showAccountOverlayComponent(ctx, keyInfo, credits, rollupStatus, error);
458
- } catch (error_) {
459
- const err = error_ as Error;
460
- error =
461
- err instanceof AuthError
462
- ? 'OpenRouter API key not found. Set OPENROUTER_MANAGEMENT_KEY (preferred) or OPENROUTER_API_KEY to use /openrouter-account.'
463
- : `API Error: ${err.message}`;
464
-
465
- // Try to get current key for overlay even on error
466
- try {
467
- const currentKey = await getCurrentKey();
468
- if (currentKey) {
469
- keyInfo = [currentKey];
470
- }
471
- } catch {
472
- // Ignore secondary errors
473
- }
474
-
475
- const rollupStatus = keyInfo
476
- ? computeRollupStatus(keyInfo)
477
- : { status: 'unavailable' as const };
478
-
479
- await showAccountOverlayComponent(ctx, keyInfo, credits, rollupStatus, error);
480
- }
481
- }
482
-
483
- async function showAccountOverlayComponent(
484
- ctx: ExtensionContext,
485
- keyInfo: KeyInfo[] | null,
486
- credits: number | null,
487
- rollupStatus: RollupStatus,
488
- error: string | null,
489
- ) {
490
- await ctx.ui.custom<void>(
491
- (_tui, theme, _keybindings, done) => {
492
- const overlayComponent = new AccountOverlayComponent(
493
- keyInfo,
494
- credits,
495
- rollupStatus,
496
- error,
497
- theme,
498
- done,
499
- () => _tui.requestRender(),
500
- ctx,
501
- );
502
-
503
- return {
504
- handleInput: (data: string) => {
505
- overlayComponent.handleInput(data);
506
- _tui.requestRender();
507
- },
508
- render: (width: number) => overlayComponent.render(width),
509
- invalidate: () => overlayComponent.invalidate(),
510
- dispose: () => {
511
- overlayComponent.dispose();
512
- },
513
- wantsKeyRelease: false,
514
- };
515
- },
516
- {
517
- overlay: true,
518
- overlayOptions: {
519
- width: 100,
520
- },
521
- },
522
- );
523
- }
524
-
525
- async function showUsageOverlay(ctx: ExtensionContext, _subcommand?: string) {
526
- const cachedSummary = usageCache.get('usage');
527
- const lastFetchTimestamp = usageCache.getTimestamp('usage');
528
- const cachedMinutesAgo = lastFetchTimestamp
529
- ? Math.round((Date.now() - lastFetchTimestamp) / MS_PER_MINUTE)
530
- : null;
531
-
532
- if (cachedSummary) {
533
- await showOverlay(ctx, cachedSummary, null, cachedMinutesAgo);
534
- return;
535
- }
536
-
537
- let error: string | null = null;
538
- let summary: UsageSummary | null = null;
539
-
540
- try {
541
- summary = await fetchAndAggregate();
542
- if (!summary) {
543
- error =
544
- 'OpenRouter API key not found. Set OPENROUTER_MANAGEMENT_KEY (preferred) or OPENROUTER_API_KEY to use /usage.';
545
- } else {
546
- usageCache.set('usage', summary);
547
- }
548
-
549
- await showOverlay(ctx, summary, error, 0);
550
- } catch (error_) {
551
- const err = error_ as Error;
552
- error =
553
- err instanceof AuthError
554
- ? 'OpenRouter API key not found. Set OPENROUTER_MANAGEMENT_KEY (preferred) or OPENROUTER_API_KEY to use /usage.'
555
- : `API Error: ${err.message}`;
556
- await showOverlay(ctx, null, error, cachedMinutesAgo || 0);
557
- }
558
- }
14
+ initializeSessionState();
559
15
 
560
- async function showOverlay(
561
- ctx: ExtensionContext,
562
- summary: UsageSummary | null,
563
- error: string | null,
564
- cachedMinutesAgo: number | null,
565
- ) {
566
- await ctx.ui.custom<void>(
567
- (_tui, theme, _keybindings, done) => {
568
- const overlayComponent = new UsageOverlayComponent(
569
- summary,
570
- error,
571
- cachedMinutesAgo,
572
- theme,
573
- done,
574
- () => _tui.requestRender(),
575
- );
16
+ const startupState = await loadStartupCacheState(pi);
576
17
 
577
- return {
578
- handleInput: (data: string) => {
579
- overlayComponent.handleInput(data);
580
- _tui.requestRender();
581
- },
582
- render: (width: number) => overlayComponent.render(width),
583
- invalidate: () => overlayComponent.invalidate(),
584
- dispose: () => {
585
- overlayComponent.dispose();
586
- },
587
- };
588
- },
589
- {
590
- overlay: true,
591
- overlayOptions: {
592
- width: 100,
593
- },
594
- },
595
- );
18
+ installOpenRouterHooks(pi, startupState);
19
+ registerOpenRouterCommands(pi);
596
20
  }