@robhowley/pi-openrouter 0.9.0 → 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.
- package/README.md +39 -4
- package/extensions/openrouter/__tests__/cache.test.ts +769 -0
- package/extensions/openrouter/__tests__/client.test.ts +333 -15
- package/extensions/openrouter/__tests__/commands.test.ts +816 -0
- package/extensions/openrouter/__tests__/fixtures.ts +140 -1
- package/extensions/openrouter/__tests__/format.test.ts +19 -0
- package/extensions/openrouter/__tests__/hooks.test.ts +276 -0
- package/extensions/openrouter/__tests__/index.test.ts +112 -363
- package/extensions/openrouter/__tests__/local-usage.test.ts +777 -0
- package/extensions/openrouter/__tests__/normalizers.test.ts +288 -0
- package/extensions/openrouter/__tests__/overlay.test.ts +225 -0
- package/extensions/openrouter/__tests__/session-state.test.ts +233 -0
- package/extensions/openrouter/__tests__/session.test.ts +44 -43
- package/extensions/openrouter/account-client.ts +11 -61
- package/extensions/openrouter/cache.ts +203 -91
- package/extensions/openrouter/client.ts +49 -3
- package/extensions/openrouter/commands.ts +555 -0
- package/extensions/openrouter/format.ts +7 -4
- package/extensions/openrouter/hooks.ts +229 -0
- package/extensions/openrouter/index.ts +13 -990
- package/extensions/openrouter/local-usage.ts +145 -22
- package/extensions/openrouter/models/__tests__/cache.test.ts +63 -2
- package/extensions/openrouter/models/__tests__/mapper.test.ts +29 -0
- package/extensions/openrouter/models/__tests__/override-commands.test.ts +668 -0
- package/extensions/openrouter/models/__tests__/sync.test.ts +156 -4
- package/extensions/openrouter/models/cache.ts +27 -2
- package/extensions/openrouter/models/mapper.ts +35 -69
- package/extensions/openrouter/models/override-commands.ts +434 -0
- package/extensions/openrouter/models/skip-hints.ts +19 -0
- package/extensions/openrouter/models/sync.ts +22 -10
- package/extensions/openrouter/models/types.ts +2 -1
- package/extensions/openrouter/normalizers.ts +128 -0
- package/extensions/openrouter/overlay.ts +19 -8
- package/extensions/openrouter/session-state.ts +110 -0
- package/extensions/openrouter/session.ts +16 -0
- package/extensions/openrouter/types.ts +28 -9
- package/package.json +1 -1
|
@@ -1,997 +1,20 @@
|
|
|
1
|
-
import type { ExtensionAPI
|
|
2
|
-
import
|
|
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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
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
|
-
import {
|
|
32
|
-
loadModelOverrides,
|
|
33
|
-
saveModelOverrides,
|
|
34
|
-
setModelOverride,
|
|
35
|
-
removeModelOverride,
|
|
36
|
-
getModelOverride,
|
|
37
|
-
getOverrideModelIds,
|
|
38
|
-
hasOverrides,
|
|
39
|
-
} from './models/overrides.js';
|
|
40
|
-
import type { UserModelOverride, ThinkingLevelMap, ModelOverridesFile } from './models/types.js';
|
|
41
|
-
|
|
42
|
-
// Store the current session state for use in command handlers
|
|
43
|
-
let currentSessionState: OpenRouterSessionState | null = null;
|
|
44
|
-
let sessionTrackingInstalled = false;
|
|
45
|
-
|
|
46
|
-
// Store startup cache state for notifications
|
|
47
|
-
let startupCacheInfo: { count: number; age: string } | undefined;
|
|
48
|
-
|
|
49
|
-
// =============================================================================
|
|
50
|
-
// Utility Functions
|
|
51
|
-
// =============================================================================
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Format skipped models details for --skipped flag output.
|
|
55
|
-
*/
|
|
56
|
-
function getErrorMessage(error: unknown): string {
|
|
57
|
-
return error instanceof Error ? error.message : String(error);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function formatSkippedDetails(
|
|
61
|
-
skipCount: number,
|
|
62
|
-
groupedReasons: Record<string, number>,
|
|
63
|
-
skipReasons: Array<{ id: string; reason: string }>,
|
|
64
|
-
): string {
|
|
65
|
-
if (skipCount === 0) {
|
|
66
|
-
return '\n\nNo skipped models';
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
let details = `\n\nOpenRouter skipped models: ${skipCount}\n`;
|
|
70
|
-
for (const [reason, count] of Object.entries(groupedReasons)) {
|
|
71
|
-
details += `\n${count} ${reason}\n`;
|
|
72
|
-
const modelsWithReason = skipReasons.filter((r) => r.reason === reason).map((r) => r.id);
|
|
73
|
-
for (const id of modelsWithReason) {
|
|
74
|
-
details += `- ${id}\n`;
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
return details;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// =============================================================================
|
|
81
|
-
// Session State Management
|
|
82
|
-
// =============================================================================
|
|
83
|
-
|
|
84
|
-
function getCurrentSessionId(ctx: { sessionManager: { getSessionId(): string } }): string {
|
|
85
|
-
if (currentSessionState) {
|
|
86
|
-
return currentSessionState.sessionId;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
try {
|
|
90
|
-
const sessionId = ctx.sessionManager.getSessionId();
|
|
91
|
-
let formattedSessionId: string;
|
|
92
|
-
if (sessionId && sessionId !== '') {
|
|
93
|
-
formattedSessionId = formatSessionId(sessionId);
|
|
94
|
-
} else {
|
|
95
|
-
formattedSessionId = formatSessionId(crypto.randomUUID());
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
currentSessionState = { sessionId: formattedSessionId };
|
|
99
|
-
return formattedSessionId;
|
|
100
|
-
} catch {
|
|
101
|
-
// Generate fallback on any error
|
|
102
|
-
const fallbackId = formatSessionId(crypto.randomUUID());
|
|
103
|
-
currentSessionState = { sessionId: fallbackId };
|
|
104
|
-
return fallbackId;
|
|
105
|
-
}
|
|
106
|
-
}
|
|
11
|
+
export { addSessionIdToOpenRouterRequest, getCurrentSessionId };
|
|
107
12
|
|
|
108
13
|
export default async function (pi: ExtensionAPI) {
|
|
109
|
-
|
|
110
|
-
startupCacheInfo = undefined;
|
|
111
|
-
let startupCacheWarning: string | undefined;
|
|
112
|
-
if (isSyncEnabled()) {
|
|
113
|
-
const cache = await loadCache().catch(() => null);
|
|
114
|
-
|
|
115
|
-
if (cache?.models.length) {
|
|
116
|
-
try {
|
|
117
|
-
const { configs } = await mapOpenRouterModels(cache.models);
|
|
118
|
-
|
|
119
|
-
// Register models directly with Pi's OpenRouter provider
|
|
120
|
-
pi.registerProvider('openrouter', {
|
|
121
|
-
baseUrl: 'https://openrouter.ai/api/v1',
|
|
122
|
-
apiKey: 'OPENROUTER_API_KEY',
|
|
123
|
-
api: 'openai-completions',
|
|
124
|
-
models: configs,
|
|
125
|
-
authHeader: true,
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
// Store for session_start notification
|
|
129
|
-
const age = formatDuration(getCacheAgeMs(cache));
|
|
130
|
-
startupCacheInfo = { count: configs.length, age };
|
|
131
|
-
} catch (error) {
|
|
132
|
-
startupCacheInfo = undefined;
|
|
133
|
-
startupCacheWarning = `OpenRouter: cached models found but failed to register: ${error instanceof Error ? error.message : String(error)}`;
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// Install before_provider_request hook once
|
|
139
|
-
if (!sessionTrackingInstalled) {
|
|
140
|
-
sessionTrackingInstalled = true;
|
|
141
|
-
|
|
142
|
-
pi.on('before_provider_request', (event, ctx) => {
|
|
143
|
-
try {
|
|
144
|
-
// Validate the payload exists
|
|
145
|
-
const ev = event as unknown as Record<string, unknown>;
|
|
146
|
-
const payload = ev['payload'] as Record<string, unknown> | undefined;
|
|
147
|
-
if (!payload) {
|
|
148
|
-
return;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
// Check if this is an OpenRouter request
|
|
152
|
-
const isOpenRouter = isOpenRouterRequest(event, ctx);
|
|
153
|
-
if (!isOpenRouter) {
|
|
154
|
-
return;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
// Do not overwrite existing session_id
|
|
158
|
-
if ('session_id' in payload && payload['session_id'] !== undefined) {
|
|
159
|
-
return;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
// Add session_id to the payload (OpenRouter-specific field)
|
|
163
|
-
return {
|
|
164
|
-
...payload,
|
|
165
|
-
session_id: getCurrentSessionId(ctx),
|
|
166
|
-
};
|
|
167
|
-
} catch {
|
|
168
|
-
// Fail open - silently ignore errors
|
|
169
|
-
return;
|
|
170
|
-
}
|
|
171
|
-
});
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
// Hook turn_end to capture completed OpenRouter turns for local logging
|
|
175
|
-
pi.on('turn_end', async (event, ctx) => {
|
|
176
|
-
try {
|
|
177
|
-
const turnEvent = event as unknown as Record<string, unknown>;
|
|
178
|
-
|
|
179
|
-
const message = turnEvent['message'] as Record<string, unknown> | undefined;
|
|
180
|
-
if (!message) return;
|
|
181
|
-
|
|
182
|
-
// Check if this is an OpenRouter request based on the message content/model
|
|
183
|
-
// Include url/endpoint from turnEvent so isOpenRouterRequest can check them
|
|
184
|
-
const isOpenRouter = isOpenRouterRequest(
|
|
185
|
-
{
|
|
186
|
-
type: 'before_provider_request',
|
|
187
|
-
payload: message,
|
|
188
|
-
url: turnEvent['url'],
|
|
189
|
-
endpoint: turnEvent['endpoint'],
|
|
190
|
-
} as unknown as Parameters<typeof isOpenRouterRequest>[0],
|
|
191
|
-
ctx,
|
|
192
|
-
);
|
|
193
|
-
if (!isOpenRouter) return;
|
|
194
|
-
|
|
195
|
-
// Check if the message has usage data
|
|
196
|
-
const usage = (message as { usage?: unknown })['usage'] as
|
|
197
|
-
| {
|
|
198
|
-
input?: number;
|
|
199
|
-
output?: number;
|
|
200
|
-
cacheRead?: number;
|
|
201
|
-
cacheWrite?: number;
|
|
202
|
-
totalTokens?: number;
|
|
203
|
-
cost?: {
|
|
204
|
-
input?: number;
|
|
205
|
-
output?: number;
|
|
206
|
-
cacheRead?: number;
|
|
207
|
-
cacheWrite?: number;
|
|
208
|
-
total?: number;
|
|
209
|
-
};
|
|
210
|
-
}
|
|
211
|
-
| undefined;
|
|
212
|
-
if (!usage) return;
|
|
213
|
-
|
|
214
|
-
// Extract model from the message
|
|
215
|
-
const model = message['model'] as string | undefined;
|
|
216
|
-
const responseModel = message['responseModel'] as string | undefined;
|
|
217
|
-
const modelToLog = model || responseModel;
|
|
218
|
-
|
|
219
|
-
// Calculate total cost from usage.cost.total
|
|
220
|
-
const totalCost = usage.cost?.total;
|
|
221
|
-
|
|
222
|
-
const localEvent: LocalUsageEvent = {
|
|
223
|
-
id: crypto.randomUUID(),
|
|
224
|
-
generationId: String(message['responseId'] ?? ''),
|
|
225
|
-
sessionId: getCurrentSessionId(ctx),
|
|
226
|
-
completedAt: new Date().toISOString(),
|
|
227
|
-
model: modelToLog ?? 'unknown',
|
|
228
|
-
requests: 1,
|
|
229
|
-
promptTokens: usage.input ?? 0,
|
|
230
|
-
completionTokens: usage.output ?? 0,
|
|
231
|
-
reasoningTokens: 0,
|
|
232
|
-
cacheReadTokens: usage.cacheRead ?? 0,
|
|
233
|
-
cacheWriteTokens: usage.cacheWrite ?? 0,
|
|
234
|
-
cost: totalCost ?? 0,
|
|
235
|
-
};
|
|
236
|
-
|
|
237
|
-
// Write to local JSONL - fail open (don't throw)
|
|
238
|
-
writeLocalUsage(localEvent).catch(() => {});
|
|
239
|
-
} catch {
|
|
240
|
-
// Fail open - silently ignore errors
|
|
241
|
-
}
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
pi.on('session_shutdown', () => {
|
|
245
|
-
stopBackgroundRefresh();
|
|
246
|
-
});
|
|
247
|
-
|
|
248
|
-
// Notify on first session start after extension load
|
|
249
|
-
pi.on('session_start', (event, ctx) => {
|
|
250
|
-
if (!ctx.hasUI) return;
|
|
251
|
-
|
|
252
|
-
// Show a persistent status indicator
|
|
253
|
-
if (startupCacheInfo) {
|
|
254
|
-
const statusText = `OpenRouter ${startupCacheInfo.count} models`;
|
|
255
|
-
ctx.ui.setStatus('openrouter', ctx.ui.theme.fg('dim', statusText));
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
// Show a one-time notification on startup
|
|
259
|
-
if (event.reason === 'startup' && startupCacheInfo) {
|
|
260
|
-
const notice = `OpenRouter: ${startupCacheInfo.count} models loaded from cache (${startupCacheInfo.age} old). Run /openrouter models-sync to refresh.`;
|
|
261
|
-
ctx.ui.notify(notice, 'info');
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
if (event.reason === 'startup' && startupCacheWarning) {
|
|
265
|
-
ctx.ui.notify(startupCacheWarning, 'warning');
|
|
266
|
-
}
|
|
267
|
-
});
|
|
268
|
-
|
|
269
|
-
pi.registerCommand('openrouter-usage', {
|
|
270
|
-
description: 'Show OpenRouter usage: caps, spend, burn rate, and model breakdowns',
|
|
271
|
-
getArgumentCompletions: () => null,
|
|
272
|
-
handler: async (args, ctx) => {
|
|
273
|
-
startBackgroundRefresh();
|
|
274
|
-
const subcommand = args.trim() || undefined;
|
|
275
|
-
await showUsageOverlay(ctx, subcommand);
|
|
276
|
-
},
|
|
277
|
-
});
|
|
278
|
-
|
|
279
|
-
pi.registerCommand('openrouter-session', {
|
|
280
|
-
description: 'Show the current OpenRouter session ID for request grouping',
|
|
281
|
-
getArgumentCompletions: () => null,
|
|
282
|
-
handler: async (_args, ctx) => {
|
|
283
|
-
const idToShow = getCurrentSessionId(ctx);
|
|
284
|
-
ctx.ui.notify(`OpenRouter session_id\n${idToShow}`, 'info');
|
|
285
|
-
},
|
|
286
|
-
});
|
|
287
|
-
|
|
288
|
-
pi.registerCommand('openrouter-account', {
|
|
289
|
-
description: 'Show OpenRouter account and key health',
|
|
290
|
-
getArgumentCompletions: () => null,
|
|
291
|
-
handler: async (_args, ctx) => {
|
|
292
|
-
await showAccountOverlay(ctx);
|
|
293
|
-
},
|
|
294
|
-
});
|
|
295
|
-
|
|
296
|
-
// ============== MODELS COMMANDS (subcommands of /openrouter) ==============
|
|
297
|
-
|
|
298
|
-
// Single entry point with subcommands: /openrouter [usage|account|session|models-sync|models-status]
|
|
299
|
-
pi.registerCommand('openrouter', {
|
|
300
|
-
description: 'OpenRouter commands: usage, account, session, models-sync, models-status',
|
|
301
|
-
getArgumentCompletions: (prefix: string) => {
|
|
302
|
-
const subcommands = [
|
|
303
|
-
'usage',
|
|
304
|
-
'account',
|
|
305
|
-
'session',
|
|
306
|
-
'models-sync',
|
|
307
|
-
'models-status',
|
|
308
|
-
'model-override-set',
|
|
309
|
-
'model-override-clear',
|
|
310
|
-
'model-override-list',
|
|
311
|
-
];
|
|
312
|
-
const items = subcommands
|
|
313
|
-
.filter((s) => s.startsWith(prefix))
|
|
314
|
-
.map((s) => ({ value: s, label: s }));
|
|
315
|
-
return items.length > 0 ? items : null;
|
|
316
|
-
},
|
|
317
|
-
handler: async (args, ctx) => {
|
|
318
|
-
// Parse subcommand and args
|
|
319
|
-
const parts = args.trim().split(/\s+/);
|
|
320
|
-
const subcommand = parts[0] || '';
|
|
321
|
-
const subcommandArgs = parts.slice(1).join(' ').trim();
|
|
322
|
-
const flags = parts.slice(1).reduce(
|
|
323
|
-
(acc, flag) => {
|
|
324
|
-
acc[flag] = true;
|
|
325
|
-
return acc;
|
|
326
|
-
},
|
|
327
|
-
{} as Record<string, boolean>,
|
|
328
|
-
);
|
|
329
|
-
|
|
330
|
-
switch (subcommand) {
|
|
331
|
-
case 'usage': {
|
|
332
|
-
startBackgroundRefresh();
|
|
333
|
-
await showUsageOverlay(ctx, undefined);
|
|
334
|
-
break;
|
|
335
|
-
}
|
|
336
|
-
case 'account': {
|
|
337
|
-
await showAccountOverlay(ctx);
|
|
338
|
-
break;
|
|
339
|
-
}
|
|
340
|
-
case 'session': {
|
|
341
|
-
ctx.ui.notify(`OpenRouter session_id\n${getCurrentSessionId(ctx)}`, 'info');
|
|
342
|
-
break;
|
|
343
|
-
}
|
|
344
|
-
case 'models-sync': {
|
|
345
|
-
if (!isSyncEnabled()) {
|
|
346
|
-
ctx.ui.notify(
|
|
347
|
-
'OpenRouter model sync is disabled. Set openrouterModelSync: true in ~/.pi/agent/settings.json to enable.',
|
|
348
|
-
'error',
|
|
349
|
-
);
|
|
350
|
-
return;
|
|
351
|
-
}
|
|
352
|
-
const result = await syncModels(ctx);
|
|
353
|
-
|
|
354
|
-
// Display brief result using same color scheme as overlays
|
|
355
|
-
if (!result.success) {
|
|
356
|
-
let message = '';
|
|
357
|
-
if (result.source === 'cache') {
|
|
358
|
-
message = `OpenRouter models sync failed\n${result.registeredCount} registered from cache\nCache age: ${formatDuration(result.cacheAgeMs)}\nError: ${result.error}`;
|
|
359
|
-
} else {
|
|
360
|
-
message = `OpenRouter models unavailable\n0 registered\nError: ${result.error}`;
|
|
361
|
-
}
|
|
362
|
-
ctx.ui.notify(message, result.source === 'cache' ? 'warning' : 'error');
|
|
363
|
-
} else {
|
|
364
|
-
const message = `OpenRouter models synced\n${result.registeredCount} registered${result.skippedCount > 0 ? ` · ${result.skippedCount} skipped` : ''} · cache updated`;
|
|
365
|
-
ctx.ui.notify(message, 'info');
|
|
366
|
-
}
|
|
367
|
-
break;
|
|
368
|
-
}
|
|
369
|
-
case 'models-status': {
|
|
370
|
-
const state = getSyncState();
|
|
371
|
-
const skipReasons = await getSkipReasonsAsync();
|
|
372
|
-
const groupedReasons = groupSkipReasons(skipReasons);
|
|
373
|
-
|
|
374
|
-
// Get real-time cache age from disk
|
|
375
|
-
const cache = await loadCache();
|
|
376
|
-
const cacheAgeMs = cache ? getCacheAgeMs(cache) : null;
|
|
377
|
-
|
|
378
|
-
if (!state && !cache) {
|
|
379
|
-
ctx.ui.notify('OpenRouter models: not synced', 'error');
|
|
380
|
-
} else if (!state && cache) {
|
|
381
|
-
// Cache exists but no in-memory state (new Pi session)
|
|
382
|
-
const cachedCount = cache.models.length;
|
|
383
|
-
const message = `OpenRouter models cached\n${cachedCount} models in cache · age: ${formatDuration(cacheAgeMs)}\nRun '/openrouter models-sync' to register models`;
|
|
384
|
-
ctx.ui.notify(message, 'info');
|
|
385
|
-
} else if (state?.success) {
|
|
386
|
-
const skipCount = skipReasons.length;
|
|
387
|
-
let message = `OpenRouter models healthy\n${state.registeredCount} registered${skipCount > 0 ? ` · ${skipCount} skipped` : ''} · cache age: ${formatDuration(cacheAgeMs)}`;
|
|
388
|
-
|
|
389
|
-
if (flags['--skipped']) {
|
|
390
|
-
message += formatSkippedDetails(skipCount, groupedReasons, skipReasons);
|
|
391
|
-
}
|
|
392
|
-
ctx.ui.notify(message, 'info');
|
|
393
|
-
} else if (state?.source === 'cache') {
|
|
394
|
-
const skipCount = skipReasons.length;
|
|
395
|
-
let message = `OpenRouter models cached\n${state.registeredCount} registered${skipCount > 0 ? ` · ${skipCount} skipped` : ''}\nCache age: ${formatDuration(cacheAgeMs)}\nError: ${state.error}`;
|
|
396
|
-
|
|
397
|
-
if (flags['--skipped']) {
|
|
398
|
-
message += formatSkippedDetails(skipCount, groupedReasons, skipReasons);
|
|
399
|
-
}
|
|
400
|
-
ctx.ui.notify(message, 'warning');
|
|
401
|
-
} else {
|
|
402
|
-
ctx.ui.notify(
|
|
403
|
-
`OpenRouter models broken\n0 registered\nError: ${state?.error}`,
|
|
404
|
-
'error',
|
|
405
|
-
);
|
|
406
|
-
}
|
|
407
|
-
break;
|
|
408
|
-
}
|
|
409
|
-
case 'model-override-set': {
|
|
410
|
-
let userOverrides: ModelOverridesFile;
|
|
411
|
-
try {
|
|
412
|
-
userOverrides = await loadModelOverrides();
|
|
413
|
-
} catch (error) {
|
|
414
|
-
ctx.ui.notify(`Failed to load model overrides: ${getErrorMessage(error)}`, 'error');
|
|
415
|
-
break;
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
const result = await handleModelOverrideSet(subcommandArgs, userOverrides);
|
|
419
|
-
if (result.success) {
|
|
420
|
-
ctx.ui.notify(result.message, 'info');
|
|
421
|
-
// Notify if we just changed the currently active model
|
|
422
|
-
if (result.modelId && ctx.model && result.modelId === ctx.model.id) {
|
|
423
|
-
ctx.ui.notify(
|
|
424
|
-
'Model configuration updated. Run /openrouter models-sync to apply changes to the current conversation.',
|
|
425
|
-
'info',
|
|
426
|
-
);
|
|
427
|
-
}
|
|
428
|
-
} else {
|
|
429
|
-
ctx.ui.notify(result.message, 'error');
|
|
430
|
-
}
|
|
431
|
-
break;
|
|
432
|
-
}
|
|
433
|
-
case 'model-override-clear': {
|
|
434
|
-
let userOverrides: ModelOverridesFile;
|
|
435
|
-
try {
|
|
436
|
-
userOverrides = await loadModelOverrides();
|
|
437
|
-
} catch (error) {
|
|
438
|
-
ctx.ui.notify(`Failed to load model overrides: ${getErrorMessage(error)}`, 'error');
|
|
439
|
-
break;
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
const result = await handleModelOverrideClear(subcommandArgs, userOverrides);
|
|
443
|
-
if (result.success) {
|
|
444
|
-
ctx.ui.notify(result.message, 'info');
|
|
445
|
-
// Notify if we just cleared the currently active model
|
|
446
|
-
if (result.modelId && ctx.model && result.modelId === ctx.model.id) {
|
|
447
|
-
ctx.ui.notify(
|
|
448
|
-
'Model configuration updated. Run /openrouter models-sync to apply changes to the current conversation.',
|
|
449
|
-
'info',
|
|
450
|
-
);
|
|
451
|
-
}
|
|
452
|
-
} else {
|
|
453
|
-
ctx.ui.notify(result.message, 'error');
|
|
454
|
-
}
|
|
455
|
-
break;
|
|
456
|
-
}
|
|
457
|
-
case 'model-override-list': {
|
|
458
|
-
try {
|
|
459
|
-
const result = await handleModelOverrideList(subcommandArgs);
|
|
460
|
-
ctx.ui.notify(result, 'info');
|
|
461
|
-
} catch (error) {
|
|
462
|
-
ctx.ui.notify(`Failed to load model overrides: ${getErrorMessage(error)}`, 'error');
|
|
463
|
-
}
|
|
464
|
-
break;
|
|
465
|
-
}
|
|
466
|
-
default: {
|
|
467
|
-
const available = [
|
|
468
|
-
'usage',
|
|
469
|
-
'account',
|
|
470
|
-
'session',
|
|
471
|
-
'models-sync',
|
|
472
|
-
'models-status',
|
|
473
|
-
'model-override-set',
|
|
474
|
-
'model-override-clear',
|
|
475
|
-
'model-override-list',
|
|
476
|
-
];
|
|
477
|
-
const message =
|
|
478
|
-
available.length > 0
|
|
479
|
-
? `Available subcommands: ${available.join(', ')}${available.length > 1 ? '' : ''}`
|
|
480
|
-
: 'No subcommands available';
|
|
481
|
-
ctx.ui.notify(`OpenRouter subcommands\n${message}`, 'error');
|
|
482
|
-
break;
|
|
483
|
-
}
|
|
484
|
-
}
|
|
485
|
-
},
|
|
486
|
-
});
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
// =============================================================================
|
|
490
|
-
// Generic Model Override Handlers (Scoped Syntax)
|
|
491
|
-
// =============================================================================
|
|
492
|
-
|
|
493
|
-
interface HandlerResult {
|
|
494
|
-
success: boolean;
|
|
495
|
-
message: string;
|
|
496
|
-
modelId?: string;
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
interface ScopedField {
|
|
500
|
-
targetField: string;
|
|
501
|
-
targetType: 'string' | 'number' | 'boolean';
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
/**
|
|
505
|
-
* Scoped field name mapping: converts user-facing 'thinking.X' to internal 'thinkingLevelMap.X'
|
|
506
|
-
* Also supports exact PiModelConfig field names for future extensibility.
|
|
507
|
-
*/
|
|
508
|
-
const SCOPED_FIELD_MAP: Record<string, ScopedField> = {
|
|
509
|
-
// thinking.* shorthand - maps to thinkingLevelMap
|
|
510
|
-
'thinking.off': { targetField: 'thinkingLevelMap.off', targetType: 'string' },
|
|
511
|
-
'thinking.minimal': { targetField: 'thinkingLevelMap.minimal', targetType: 'string' },
|
|
512
|
-
'thinking.low': { targetField: 'thinkingLevelMap.low', targetType: 'string' },
|
|
513
|
-
'thinking.medium': { targetField: 'thinkingLevelMap.medium', targetType: 'string' },
|
|
514
|
-
'thinking.high': { targetField: 'thinkingLevelMap.high', targetType: 'string' },
|
|
515
|
-
'thinking.xhigh': { targetField: 'thinkingLevelMap.xhigh', targetType: 'string' },
|
|
516
|
-
|
|
517
|
-
// exact field names (passthrough)
|
|
518
|
-
'thinkingLevelMap.off': { targetField: 'thinkingLevelMap.off', targetType: 'string' },
|
|
519
|
-
'thinkingLevelMap.minimal': { targetField: 'thinkingLevelMap.minimal', targetType: 'string' },
|
|
520
|
-
'thinkingLevelMap.low': { targetField: 'thinkingLevelMap.low', targetType: 'string' },
|
|
521
|
-
'thinkingLevelMap.medium': { targetField: 'thinkingLevelMap.medium', targetType: 'string' },
|
|
522
|
-
'thinkingLevelMap.high': { targetField: 'thinkingLevelMap.high', targetType: 'string' },
|
|
523
|
-
'thinkingLevelMap.xhigh': { targetField: 'thinkingLevelMap.xhigh', targetType: 'string' },
|
|
524
|
-
|
|
525
|
-
// top-level fields (future extensibility)
|
|
526
|
-
contextWindow: { targetField: 'contextWindow', targetType: 'number' },
|
|
527
|
-
maxTokens: { targetField: 'maxTokens', targetType: 'number' },
|
|
528
|
-
reasoning: { targetField: 'reasoning', targetType: 'boolean' },
|
|
529
|
-
};
|
|
530
|
-
|
|
531
|
-
/**
|
|
532
|
-
* Parse a scoped assignment like "thinking.high=high" or "contextWindow=128000".
|
|
533
|
-
*/
|
|
534
|
-
export function parseScopedAssignment(
|
|
535
|
-
assignment: string,
|
|
536
|
-
): { fullPath: string; value: unknown } | null {
|
|
537
|
-
const eqIdx = assignment.indexOf('=');
|
|
538
|
-
if (eqIdx === -1) return null;
|
|
539
|
-
|
|
540
|
-
const scopedName = assignment.slice(0, eqIdx).trim();
|
|
541
|
-
const rawValue = assignment.slice(eqIdx + 1).trim();
|
|
542
|
-
|
|
543
|
-
const mapped = SCOPED_FIELD_MAP[scopedName];
|
|
544
|
-
if (!mapped) return null;
|
|
545
|
-
|
|
546
|
-
// Parse value by type
|
|
547
|
-
let parsedValue: unknown;
|
|
548
|
-
switch (mapped.targetType) {
|
|
549
|
-
case 'string':
|
|
550
|
-
// "null" -> null, otherwise string
|
|
551
|
-
parsedValue = rawValue === 'null' ? null : rawValue;
|
|
552
|
-
break;
|
|
553
|
-
case 'number': {
|
|
554
|
-
const num = parseInt(rawValue, 10);
|
|
555
|
-
if (isNaN(num)) return null;
|
|
556
|
-
parsedValue = num;
|
|
557
|
-
break;
|
|
558
|
-
}
|
|
559
|
-
case 'boolean':
|
|
560
|
-
if (rawValue !== 'true' && rawValue !== 'false') return null;
|
|
561
|
-
parsedValue = rawValue === 'true';
|
|
562
|
-
break;
|
|
563
|
-
default:
|
|
564
|
-
return null;
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
return { fullPath: mapped.targetField, value: parsedValue };
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
/**
|
|
571
|
-
* Apply a nested value to an object using dot notation path.
|
|
572
|
-
*/
|
|
573
|
-
export function applyNestedValue(obj: Record<string, unknown>, path: string, value: unknown): void {
|
|
574
|
-
const parts = path.split('.');
|
|
575
|
-
let current: unknown = obj;
|
|
576
|
-
|
|
577
|
-
for (let i = 0; i < parts.length - 1; i++) {
|
|
578
|
-
const key = parts[i]!;
|
|
579
|
-
const currentRecord = current as Record<string, unknown>;
|
|
580
|
-
if (
|
|
581
|
-
!(key in currentRecord) ||
|
|
582
|
-
typeof currentRecord[key] !== 'object' ||
|
|
583
|
-
currentRecord[key] === null
|
|
584
|
-
) {
|
|
585
|
-
currentRecord[key] = {};
|
|
586
|
-
}
|
|
587
|
-
current = currentRecord[key];
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
const finalKey = parts[parts.length - 1]!;
|
|
591
|
-
(current as Record<string, unknown>)[finalKey] = value;
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
/**
|
|
595
|
-
* Handle /openrouter model-override-set command.
|
|
596
|
-
* Format: model-override-set <model-id> <field=value>...
|
|
597
|
-
* Examples:
|
|
598
|
-
* /openrouter model-override-set deepseek/deepseek-v4-pro thinking.high=high thinking.xhigh=max
|
|
599
|
-
* /openrouter model-override-set deepseek/deepseek-v4-pro contextWindow=128000
|
|
600
|
-
*/
|
|
601
|
-
export async function handleModelOverrideSet(
|
|
602
|
-
args: string,
|
|
603
|
-
userOverrides: ModelOverridesFile,
|
|
604
|
-
): Promise<HandlerResult> {
|
|
605
|
-
const parts = args.trim().split(/\s+/).filter(Boolean);
|
|
606
|
-
|
|
607
|
-
if (parts.length < 1) {
|
|
608
|
-
return {
|
|
609
|
-
success: false,
|
|
610
|
-
message:
|
|
611
|
-
'Usage: /openrouter model-override-set <model-id> <field=value>...\nExample: /openrouter model-override-set deepseek/deepseek-v4-pro thinking.high=high thinking.xhigh=max',
|
|
612
|
-
};
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
const modelId = parts[0];
|
|
616
|
-
|
|
617
|
-
if (!modelId) {
|
|
618
|
-
return {
|
|
619
|
-
success: false,
|
|
620
|
-
message:
|
|
621
|
-
'Usage: /openrouter model-override-set <model-id> <field=value>...\nExample: /openrouter model-override-set deepseek/deepseek-v4-pro thinking.high=high thinking.xhigh=max',
|
|
622
|
-
};
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
// Validate model ID format (should be provider/model)
|
|
626
|
-
if (!modelId.includes('/')) {
|
|
627
|
-
return {
|
|
628
|
-
success: false,
|
|
629
|
-
message: `Invalid model ID format: "${modelId}"\nExpected format: provider/model (e.g., "deepseek/deepseek-v4-pro")`,
|
|
630
|
-
};
|
|
631
|
-
}
|
|
632
|
-
|
|
633
|
-
// Build override incrementally from assignments
|
|
634
|
-
const override: UserModelOverride = {};
|
|
635
|
-
const assignments = parts.slice(1).filter((p) => !p.startsWith('--'));
|
|
636
|
-
|
|
637
|
-
for (const assignment of assignments) {
|
|
638
|
-
const parsed = parseScopedAssignment(assignment);
|
|
639
|
-
if (!parsed) {
|
|
640
|
-
return {
|
|
641
|
-
success: false,
|
|
642
|
-
message: `Invalid assignment: "${assignment}"\nExpected format: field=value (e.g., thinking.high=high or contextWindow=128000)\nSee available fields with /openrouter model-override-list --fields`,
|
|
643
|
-
};
|
|
644
|
-
}
|
|
645
|
-
applyNestedValue(override as Record<string, unknown>, parsed.fullPath, parsed.value);
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
// If no assignments provided, error out
|
|
649
|
-
if (Object.keys(override).length === 0) {
|
|
650
|
-
return {
|
|
651
|
-
success: false,
|
|
652
|
-
message:
|
|
653
|
-
'No field assignments provided.\nUsage: /openrouter model-override-set <model-id> field=value [field=value]...\nExample: /openrouter model-override-set deepseek/deepseek-v4-pro thinking.high=high thinking.xhigh=max',
|
|
654
|
-
};
|
|
655
|
-
}
|
|
656
|
-
|
|
657
|
-
// Update overrides file
|
|
658
|
-
const updatedOverrides = setModelOverride(userOverrides, modelId, override);
|
|
659
|
-
try {
|
|
660
|
-
await saveModelOverrides(updatedOverrides);
|
|
661
|
-
} catch (error) {
|
|
662
|
-
return {
|
|
663
|
-
success: false,
|
|
664
|
-
message: `Failed to save overrides for ${modelId}: ${getErrorMessage(error)}`,
|
|
665
|
-
};
|
|
666
|
-
}
|
|
667
|
-
|
|
668
|
-
const savedOverride = updatedOverrides.overrides[modelId] as UserModelOverride;
|
|
669
|
-
|
|
670
|
-
// Format success message
|
|
671
|
-
const lines: string[] = [`Saved overrides for ${modelId}:`];
|
|
672
|
-
for (const [key, val] of Object.entries(savedOverride)) {
|
|
673
|
-
if (key === 'thinkingLevelMap' && val) {
|
|
674
|
-
lines.push(' thinkingLevelMap:');
|
|
675
|
-
for (const [level, mapped] of Object.entries(val as ThinkingLevelMap)) {
|
|
676
|
-
lines.push(` ${level}: ${mapped === null ? 'null' : mapped}`);
|
|
677
|
-
}
|
|
678
|
-
} else {
|
|
679
|
-
lines.push(` ${key}: ${val}`);
|
|
680
|
-
}
|
|
681
|
-
}
|
|
682
|
-
|
|
683
|
-
return {
|
|
684
|
-
success: true,
|
|
685
|
-
message: lines.join('\n'),
|
|
686
|
-
modelId,
|
|
687
|
-
};
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
/**
|
|
691
|
-
* Handle /openrouter model-override-clear command.
|
|
692
|
-
*/
|
|
693
|
-
export async function handleModelOverrideClear(
|
|
694
|
-
args: string,
|
|
695
|
-
userOverrides: ModelOverridesFile,
|
|
696
|
-
): Promise<HandlerResult> {
|
|
697
|
-
const parts = args.trim().split(/\s+/).filter(Boolean);
|
|
698
|
-
const modelId = parts[0];
|
|
699
|
-
|
|
700
|
-
if (!modelId) {
|
|
701
|
-
return {
|
|
702
|
-
success: false,
|
|
703
|
-
message: 'Usage: /openrouter model-override-clear <model-id>',
|
|
704
|
-
};
|
|
705
|
-
}
|
|
706
|
-
|
|
707
|
-
if (!modelId.includes('/')) {
|
|
708
|
-
return {
|
|
709
|
-
success: false,
|
|
710
|
-
message: `Invalid model ID format: "${modelId}"\nExpected format: provider/model`,
|
|
711
|
-
};
|
|
712
|
-
}
|
|
713
|
-
|
|
714
|
-
const existing = getModelOverride(userOverrides, modelId);
|
|
715
|
-
if (!existing) {
|
|
716
|
-
return {
|
|
717
|
-
success: false,
|
|
718
|
-
message: `No overrides found for ${modelId}`,
|
|
719
|
-
};
|
|
720
|
-
}
|
|
721
|
-
|
|
722
|
-
const updatedOverrides = removeModelOverride(userOverrides, modelId);
|
|
723
|
-
try {
|
|
724
|
-
await saveModelOverrides(updatedOverrides);
|
|
725
|
-
} catch (error) {
|
|
726
|
-
return {
|
|
727
|
-
success: false,
|
|
728
|
-
message: `Failed to clear overrides for ${modelId}: ${getErrorMessage(error)}`,
|
|
729
|
-
};
|
|
730
|
-
}
|
|
731
|
-
|
|
732
|
-
return {
|
|
733
|
-
success: true,
|
|
734
|
-
message: `Cleared all overrides for ${modelId}`,
|
|
735
|
-
modelId,
|
|
736
|
-
};
|
|
737
|
-
}
|
|
738
|
-
|
|
739
|
-
/**
|
|
740
|
-
* Handle /openrouter model-override-list command.
|
|
741
|
-
*/
|
|
742
|
-
export async function handleModelOverrideList(args: string): Promise<string> {
|
|
743
|
-
const userOverrides = await loadModelOverrides();
|
|
744
|
-
const modelId = args.trim();
|
|
745
|
-
|
|
746
|
-
// List available fields if --fields flag
|
|
747
|
-
if (modelId === '--fields') {
|
|
748
|
-
const fields = Object.keys(SCOPED_FIELD_MAP)
|
|
749
|
-
.map(
|
|
750
|
-
(k) => ` ${k}: ${SCOPED_FIELD_MAP[k]!.targetField} (${SCOPED_FIELD_MAP[k]!.targetType})`,
|
|
751
|
-
)
|
|
752
|
-
.join('\n');
|
|
753
|
-
return `Available override fields:\n${fields}`;
|
|
754
|
-
}
|
|
755
|
-
|
|
756
|
-
if (!hasOverrides(userOverrides)) {
|
|
757
|
-
return 'No model overrides configured.\nUse /openrouter model-override-set to add overrides.';
|
|
758
|
-
}
|
|
759
|
-
|
|
760
|
-
if (modelId) {
|
|
761
|
-
// Show specific model
|
|
762
|
-
const override = getModelOverride(userOverrides, modelId);
|
|
763
|
-
if (!override) {
|
|
764
|
-
return `No overrides configured for ${modelId}`;
|
|
765
|
-
}
|
|
766
|
-
|
|
767
|
-
const lines: string[] = [`Overrides for ${modelId}:`];
|
|
768
|
-
for (const [key, val] of Object.entries(override)) {
|
|
769
|
-
if (key === 'thinkingLevelMap' && val) {
|
|
770
|
-
lines.push(' thinkingLevelMap:');
|
|
771
|
-
for (const [level, mapped] of Object.entries(val as ThinkingLevelMap)) {
|
|
772
|
-
lines.push(` ${level}: ${mapped === null ? 'null' : mapped}`);
|
|
773
|
-
}
|
|
774
|
-
} else {
|
|
775
|
-
lines.push(` ${key}: ${val}`);
|
|
776
|
-
}
|
|
777
|
-
}
|
|
778
|
-
return lines.join('\n');
|
|
779
|
-
}
|
|
780
|
-
|
|
781
|
-
// List all overrides
|
|
782
|
-
const modelIds = getOverrideModelIds(userOverrides);
|
|
783
|
-
const lines: string[] = [`${modelIds.length} model(s) with overrides:`];
|
|
784
|
-
for (const id of modelIds) {
|
|
785
|
-
const override = getModelOverride(userOverrides, id);
|
|
786
|
-
if (override?.thinkingLevelMap && Object.keys(override.thinkingLevelMap).length > 0) {
|
|
787
|
-
const tlm = Object.entries(override.thinkingLevelMap)
|
|
788
|
-
.filter(([, v]) => v !== null)
|
|
789
|
-
.map(([k, v]) => `${k}=${v}`)
|
|
790
|
-
.join(',');
|
|
791
|
-
lines.push(` ${id}${tlm ? ` [${tlm}]` : ''}`);
|
|
792
|
-
} else {
|
|
793
|
-
lines.push(` ${id}`);
|
|
794
|
-
}
|
|
795
|
-
}
|
|
796
|
-
lines.push('\nUse /openrouter model-override-list <model-id> for details');
|
|
797
|
-
return lines.join('\n');
|
|
798
|
-
}
|
|
799
|
-
|
|
800
|
-
async function showAccountOverlay(ctx: ExtensionContext) {
|
|
801
|
-
let error: string | null = null;
|
|
802
|
-
let keyInfo: KeyInfo[] | null = null;
|
|
803
|
-
let credits: number | null = null;
|
|
804
|
-
|
|
805
|
-
try {
|
|
806
|
-
// Try to get all keys with management key
|
|
807
|
-
const allKeys = await getAllKeys();
|
|
808
|
-
|
|
809
|
-
if (allKeys && allKeys.length > 0) {
|
|
810
|
-
keyInfo = allKeys;
|
|
811
|
-
} else {
|
|
812
|
-
// getAllKeys() returns null or empty array when management key isn't available
|
|
813
|
-
// or when the API call fails with 403
|
|
814
|
-
error = 'Key list unavailable - set OPENROUTER_MANAGEMENT_KEY for full key inventory.';
|
|
815
|
-
|
|
816
|
-
// Fall back to current key only
|
|
817
|
-
try {
|
|
818
|
-
const currentKey = await getCurrentKey();
|
|
819
|
-
if (currentKey) {
|
|
820
|
-
keyInfo = [currentKey];
|
|
821
|
-
// Clear the error since we successfully got current key
|
|
822
|
-
error = null;
|
|
823
|
-
} else {
|
|
824
|
-
error = 'Failed to retrieve current key metadata. Check your API key permissions.';
|
|
825
|
-
}
|
|
826
|
-
} catch (err) {
|
|
827
|
-
error = `Failed to retrieve current key: ${(err as Error).message}`;
|
|
828
|
-
}
|
|
829
|
-
}
|
|
830
|
-
|
|
831
|
-
// Try to get account credits
|
|
832
|
-
credits = await getAccountCredits();
|
|
833
|
-
|
|
834
|
-
// Set error if we have no keys and no credits
|
|
835
|
-
if (!keyInfo && !credits) {
|
|
836
|
-
error =
|
|
837
|
-
'OpenRouter API key not found. Set OPENROUTER_MANAGEMENT_KEY (preferred) or OPENROUTER_API_KEY to use /openrouter-account.';
|
|
838
|
-
}
|
|
839
|
-
|
|
840
|
-
// Set error if we have credits but no keys
|
|
841
|
-
if (!keyInfo && credits !== null) {
|
|
842
|
-
error =
|
|
843
|
-
error ||
|
|
844
|
-
'Key information unavailable. Set OPENROUTER_MANAGEMENT_KEY for full key inventory.';
|
|
845
|
-
}
|
|
846
|
-
|
|
847
|
-
// Compute rollup status
|
|
848
|
-
const rollupStatus = keyInfo
|
|
849
|
-
? computeRollupStatus(keyInfo)
|
|
850
|
-
: { status: 'unavailable' as const };
|
|
851
|
-
|
|
852
|
-
// Sort keys
|
|
853
|
-
if (keyInfo) {
|
|
854
|
-
const sortedKeys = sortKeys(keyInfo);
|
|
855
|
-
keyInfo = sortedKeys;
|
|
856
|
-
}
|
|
857
|
-
|
|
858
|
-
await showAccountOverlayComponent(ctx, keyInfo, credits, rollupStatus, error);
|
|
859
|
-
} catch (error_) {
|
|
860
|
-
const err = error_ as Error;
|
|
861
|
-
error =
|
|
862
|
-
err instanceof AuthError
|
|
863
|
-
? 'OpenRouter API key not found. Set OPENROUTER_MANAGEMENT_KEY (preferred) or OPENROUTER_API_KEY to use /openrouter-account.'
|
|
864
|
-
: `API Error: ${err.message}`;
|
|
865
|
-
|
|
866
|
-
// Try to get current key for overlay even on error
|
|
867
|
-
try {
|
|
868
|
-
const currentKey = await getCurrentKey();
|
|
869
|
-
if (currentKey) {
|
|
870
|
-
keyInfo = [currentKey];
|
|
871
|
-
}
|
|
872
|
-
} catch {
|
|
873
|
-
// Ignore secondary errors
|
|
874
|
-
}
|
|
875
|
-
|
|
876
|
-
const rollupStatus = keyInfo
|
|
877
|
-
? computeRollupStatus(keyInfo)
|
|
878
|
-
: { status: 'unavailable' as const };
|
|
879
|
-
|
|
880
|
-
await showAccountOverlayComponent(ctx, keyInfo, credits, rollupStatus, error);
|
|
881
|
-
}
|
|
882
|
-
}
|
|
883
|
-
|
|
884
|
-
async function showAccountOverlayComponent(
|
|
885
|
-
ctx: ExtensionContext,
|
|
886
|
-
keyInfo: KeyInfo[] | null,
|
|
887
|
-
credits: number | null,
|
|
888
|
-
rollupStatus: RollupStatus,
|
|
889
|
-
error: string | null,
|
|
890
|
-
) {
|
|
891
|
-
await ctx.ui.custom<void>(
|
|
892
|
-
(_tui, theme, _keybindings, done) => {
|
|
893
|
-
const overlayComponent = new AccountOverlayComponent(
|
|
894
|
-
keyInfo,
|
|
895
|
-
credits,
|
|
896
|
-
rollupStatus,
|
|
897
|
-
error,
|
|
898
|
-
theme,
|
|
899
|
-
done,
|
|
900
|
-
() => _tui.requestRender(),
|
|
901
|
-
ctx,
|
|
902
|
-
);
|
|
903
|
-
|
|
904
|
-
return {
|
|
905
|
-
handleInput: (data: string) => {
|
|
906
|
-
overlayComponent.handleInput(data);
|
|
907
|
-
_tui.requestRender();
|
|
908
|
-
},
|
|
909
|
-
render: (width: number) => overlayComponent.render(width),
|
|
910
|
-
invalidate: () => overlayComponent.invalidate(),
|
|
911
|
-
dispose: () => {
|
|
912
|
-
overlayComponent.dispose();
|
|
913
|
-
},
|
|
914
|
-
wantsKeyRelease: false,
|
|
915
|
-
};
|
|
916
|
-
},
|
|
917
|
-
{
|
|
918
|
-
overlay: true,
|
|
919
|
-
overlayOptions: {
|
|
920
|
-
width: 100,
|
|
921
|
-
},
|
|
922
|
-
},
|
|
923
|
-
);
|
|
924
|
-
}
|
|
925
|
-
|
|
926
|
-
async function showUsageOverlay(ctx: ExtensionContext, _subcommand?: string) {
|
|
927
|
-
const cachedSummary = usageCache.get('usage');
|
|
928
|
-
const lastFetchTimestamp = usageCache.getTimestamp('usage');
|
|
929
|
-
const cachedMinutesAgo = lastFetchTimestamp
|
|
930
|
-
? Math.round((Date.now() - lastFetchTimestamp) / MS_PER_MINUTE)
|
|
931
|
-
: null;
|
|
932
|
-
|
|
933
|
-
if (cachedSummary) {
|
|
934
|
-
await showOverlay(ctx, cachedSummary, null, cachedMinutesAgo);
|
|
935
|
-
return;
|
|
936
|
-
}
|
|
937
|
-
|
|
938
|
-
let error: string | null = null;
|
|
939
|
-
let summary: UsageSummary | null = null;
|
|
940
|
-
|
|
941
|
-
try {
|
|
942
|
-
summary = await fetchAndAggregate();
|
|
943
|
-
if (!summary) {
|
|
944
|
-
error =
|
|
945
|
-
'OpenRouter API key not found. Set OPENROUTER_MANAGEMENT_KEY (preferred) or OPENROUTER_API_KEY to use /usage.';
|
|
946
|
-
} else {
|
|
947
|
-
usageCache.set('usage', summary);
|
|
948
|
-
}
|
|
949
|
-
|
|
950
|
-
await showOverlay(ctx, summary, error, 0);
|
|
951
|
-
} catch (error_) {
|
|
952
|
-
const err = error_ as Error;
|
|
953
|
-
error =
|
|
954
|
-
err instanceof AuthError
|
|
955
|
-
? 'OpenRouter API key not found. Set OPENROUTER_MANAGEMENT_KEY (preferred) or OPENROUTER_API_KEY to use /usage.'
|
|
956
|
-
: `API Error: ${err.message}`;
|
|
957
|
-
await showOverlay(ctx, null, error, cachedMinutesAgo || 0);
|
|
958
|
-
}
|
|
959
|
-
}
|
|
14
|
+
initializeSessionState();
|
|
960
15
|
|
|
961
|
-
|
|
962
|
-
ctx: ExtensionContext,
|
|
963
|
-
summary: UsageSummary | null,
|
|
964
|
-
error: string | null,
|
|
965
|
-
cachedMinutesAgo: number | null,
|
|
966
|
-
) {
|
|
967
|
-
await ctx.ui.custom<void>(
|
|
968
|
-
(_tui, theme, _keybindings, done) => {
|
|
969
|
-
const overlayComponent = new UsageOverlayComponent(
|
|
970
|
-
summary,
|
|
971
|
-
error,
|
|
972
|
-
cachedMinutesAgo,
|
|
973
|
-
theme,
|
|
974
|
-
done,
|
|
975
|
-
() => _tui.requestRender(),
|
|
976
|
-
);
|
|
16
|
+
const startupState = await loadStartupCacheState(pi);
|
|
977
17
|
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
overlayComponent.handleInput(data);
|
|
981
|
-
_tui.requestRender();
|
|
982
|
-
},
|
|
983
|
-
render: (width: number) => overlayComponent.render(width),
|
|
984
|
-
invalidate: () => overlayComponent.invalidate(),
|
|
985
|
-
dispose: () => {
|
|
986
|
-
overlayComponent.dispose();
|
|
987
|
-
},
|
|
988
|
-
};
|
|
989
|
-
},
|
|
990
|
-
{
|
|
991
|
-
overlay: true,
|
|
992
|
-
overlayOptions: {
|
|
993
|
-
width: 100,
|
|
994
|
-
},
|
|
995
|
-
},
|
|
996
|
-
);
|
|
18
|
+
installOpenRouterHooks(pi, startupState);
|
|
19
|
+
registerOpenRouterCommands(pi);
|
|
997
20
|
}
|