@robhowley/pi-openrouter 0.10.0 → 0.11.0

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.
@@ -13,7 +13,7 @@ export type KeyStatus =
13
13
  export type BYOKStatus = 'incl' | 'excl' | '?';
14
14
 
15
15
  /** Reset cadence for key limits */
16
- export type ResetCadence = 'monthly' | 'daily' | 'never' | 'partial';
16
+ export type ResetCadence = 'monthly' | 'weekly' | 'daily' | 'never' | 'partial';
17
17
 
18
18
  /** Information about a single OpenRouter key */
19
19
  export interface KeyInfo {
@@ -23,14 +23,21 @@ export interface KeyInfo {
23
23
  used: number; // Current usage (currency)
24
24
  limit?: number; // Key cap (optional)
25
25
  remaining?: number; // limit - used
26
- resetCadence: ResetCadence; // monthly, daily, never, or partial
26
+ resetCadence: ResetCadence; // monthly, weekly, daily, never, or partial
27
27
  byok: BYOKStatus; // incl (true), excl (false), ? (unavailable)
28
- hash: string; // Key hash for identification
28
+ hash?: string; // Trusted key hash for identification/mutation when available
29
29
  disabled: boolean; // Whether key is disabled
30
30
  workspaceName: string; // Name of the workspace this key belongs to
31
31
  spend: number; // Spend associated with this key (in USD)
32
32
  }
33
33
 
34
+ /** How the currently authenticated key relates to listed inventory rows. */
35
+ export type CurrentKeyRelation =
36
+ | { kind: 'inventory-match'; hash: string; label: string }
37
+ | { kind: 'external-provisioning'; label: string }
38
+ | { kind: 'ambiguous-label'; label: string; matchingHashes: string[] }
39
+ | { kind: 'unresolved'; reason: string; label?: string };
40
+
34
41
  /** Rollup status for the entire account */
35
42
  export type RollupStatus =
36
43
  | { status: 'unavailable'; message?: never }
@@ -0,0 +1,287 @@
1
+ import { createApiKey, setApiKeyDisabled } from './account-client.js';
2
+
3
+ export interface HandlerResult {
4
+ success: boolean;
5
+ message: string;
6
+ /** Create-only one-time secret. The command router must render this outside notify/log paths. */
7
+ secret?: string;
8
+ }
9
+
10
+ export interface ParsedApiKeyCreateArgs {
11
+ name: string;
12
+ limit?: number | null;
13
+ limitReset?: 'daily' | 'weekly' | 'monthly' | null;
14
+ includeByokInLimit?: boolean;
15
+ workspaceId?: string;
16
+ expiresAt?: Date;
17
+ }
18
+
19
+ const API_KEY_CREATE_USAGE =
20
+ 'Usage: /openrouter api-key-create <name> [limit=<usd|none>] [reset=<daily|weekly|monthly|none>] [byok=<incl|excl>] [workspace=<id>] [expires=<UTC ISO>]';
21
+ const API_KEY_DISABLE_USAGE = 'Usage: /openrouter api-key-disable <hash>';
22
+ const API_KEY_ENABLE_USAGE = 'Usage: /openrouter api-key-enable <hash>';
23
+
24
+ const ALLOWED_RESET_VALUES = new Set(['daily', 'weekly', 'monthly', 'none']);
25
+ const ALLOWED_BYOK_VALUES = new Set(['incl', 'excl']);
26
+
27
+ export function isUtcIsoTimestamp(value: string): boolean {
28
+ if (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3})?Z$/.test(value)) {
29
+ return false;
30
+ }
31
+
32
+ const parsed = new Date(value);
33
+ if (Number.isNaN(parsed.getTime())) {
34
+ return false;
35
+ }
36
+
37
+ const canonical = parsed.toISOString();
38
+ return value === canonical || value === canonical.replace('.000Z', 'Z');
39
+ }
40
+
41
+ function tokenizeArgs(
42
+ args: string,
43
+ ): { ok: true; value: string[] } | { ok: false; message: string } {
44
+ const tokens: string[] = [];
45
+ let current = '';
46
+ let quote: '"' | "'" | null = null;
47
+
48
+ for (const char of args.trim()) {
49
+ if (quote) {
50
+ if (char === quote) {
51
+ quote = null;
52
+ } else {
53
+ current += char;
54
+ }
55
+ continue;
56
+ }
57
+
58
+ if (char === '"' || char === "'") {
59
+ quote = char;
60
+ continue;
61
+ }
62
+
63
+ if (/\s/.test(char)) {
64
+ if (current) {
65
+ tokens.push(current);
66
+ current = '';
67
+ }
68
+ continue;
69
+ }
70
+
71
+ current += char;
72
+ }
73
+
74
+ if (quote) {
75
+ return {
76
+ ok: false,
77
+ message: `Unterminated quoted string in arguments.\n${API_KEY_CREATE_USAGE}`,
78
+ };
79
+ }
80
+
81
+ if (current) {
82
+ tokens.push(current);
83
+ }
84
+
85
+ return { ok: true, value: tokens };
86
+ }
87
+
88
+ export function parseApiKeyCreateArgs(
89
+ args: string,
90
+ ): { ok: true; value: ParsedApiKeyCreateArgs } | { ok: false; message: string } {
91
+ const tokenized = tokenizeArgs(args);
92
+ if (!tokenized.ok) {
93
+ return { ok: false, message: tokenized.message };
94
+ }
95
+
96
+ const parts = tokenized.value;
97
+ const name = parts[0];
98
+
99
+ if (!name || name.includes('=')) {
100
+ return { ok: false, message: API_KEY_CREATE_USAGE };
101
+ }
102
+
103
+ const parsed: ParsedApiKeyCreateArgs = { name };
104
+
105
+ for (const token of parts.slice(1)) {
106
+ const separatorIndex = token.indexOf('=');
107
+ if (separatorIndex === -1) {
108
+ return {
109
+ ok: false,
110
+ message: `Invalid option: "${token}"\nExpected key=value options after the key name.\n${API_KEY_CREATE_USAGE}`,
111
+ };
112
+ }
113
+
114
+ const key = token.slice(0, separatorIndex).trim();
115
+ const rawValue = token.slice(separatorIndex + 1).trim();
116
+
117
+ switch (key) {
118
+ case 'limit': {
119
+ if (rawValue === 'none') {
120
+ parsed.limit = null;
121
+ break;
122
+ }
123
+
124
+ if (rawValue === '') {
125
+ return {
126
+ ok: false,
127
+ message: `Invalid limit value: "${rawValue}"\nExpected a non-negative USD amount or 'none'.`,
128
+ };
129
+ }
130
+
131
+ const limit = Number(rawValue);
132
+ if (!Number.isFinite(limit) || limit < 0) {
133
+ return {
134
+ ok: false,
135
+ message: `Invalid limit value: "${rawValue}"\nExpected a non-negative USD amount or 'none'.`,
136
+ };
137
+ }
138
+
139
+ parsed.limit = limit;
140
+ break;
141
+ }
142
+ case 'reset': {
143
+ if (!ALLOWED_RESET_VALUES.has(rawValue)) {
144
+ return {
145
+ ok: false,
146
+ message: `Invalid reset value: "${rawValue}"\nAllowed values: daily, weekly, monthly, none.`,
147
+ };
148
+ }
149
+
150
+ parsed.limitReset =
151
+ rawValue === 'none' ? null : (rawValue as 'daily' | 'weekly' | 'monthly');
152
+ break;
153
+ }
154
+ case 'byok': {
155
+ if (!ALLOWED_BYOK_VALUES.has(rawValue)) {
156
+ return {
157
+ ok: false,
158
+ message: `Invalid byok value: "${rawValue}"\nAllowed values: incl, excl.`,
159
+ };
160
+ }
161
+
162
+ parsed.includeByokInLimit = rawValue === 'incl';
163
+ break;
164
+ }
165
+ case 'workspace': {
166
+ if (!rawValue) {
167
+ return {
168
+ ok: false,
169
+ message: 'Invalid workspace value: workspace id cannot be empty.',
170
+ };
171
+ }
172
+
173
+ parsed.workspaceId = rawValue;
174
+ break;
175
+ }
176
+ case 'expires': {
177
+ if (!isUtcIsoTimestamp(rawValue)) {
178
+ return {
179
+ ok: false,
180
+ message: `Invalid expires value: "${rawValue}"\nExpected an ISO 8601 UTC timestamp ending in Z, for example 2026-06-01T00:00:00Z.`,
181
+ };
182
+ }
183
+
184
+ parsed.expiresAt = new Date(rawValue);
185
+ break;
186
+ }
187
+ default:
188
+ return {
189
+ ok: false,
190
+ message: `Unknown option: "${key}"\nAllowed options: limit, reset, byok, workspace, expires.\n${API_KEY_CREATE_USAGE}`,
191
+ };
192
+ }
193
+ }
194
+
195
+ return { ok: true, value: parsed };
196
+ }
197
+
198
+ export async function handleApiKeyCreate(args: string): Promise<HandlerResult> {
199
+ const parsed = parseApiKeyCreateArgs(args);
200
+ if (!parsed.ok) {
201
+ return {
202
+ success: false,
203
+ message: parsed.message,
204
+ };
205
+ }
206
+
207
+ try {
208
+ const created = await createApiKey(parsed.value);
209
+ const status = created.keyState.disabled ? 'disabled' : 'enabled';
210
+
211
+ return {
212
+ success: true,
213
+ message: [
214
+ 'OpenRouter API key created',
215
+ `Name: ${created.keyState.name}`,
216
+ `Status: ${status}`,
217
+ 'Use /openrouter account to inspect or toggle the key.',
218
+ 'Secret shown in secure overlay; store it now.',
219
+ 'Warning: This secret cannot be recovered and was not written or cached locally.',
220
+ ].join('\n'),
221
+ secret: created.key,
222
+ };
223
+ } catch (error) {
224
+ return {
225
+ success: false,
226
+ message: getErrorMessage(error),
227
+ };
228
+ }
229
+ }
230
+
231
+ export async function handleApiKeyDisable(args: string): Promise<HandlerResult> {
232
+ return handleApiKeyToggle(args, true);
233
+ }
234
+
235
+ export async function handleApiKeyEnable(args: string): Promise<HandlerResult> {
236
+ return handleApiKeyToggle(args, false);
237
+ }
238
+
239
+ async function handleApiKeyToggle(args: string, disabled: boolean): Promise<HandlerResult> {
240
+ const tokenized = tokenizeArgs(args);
241
+ if (!tokenized.ok) {
242
+ return {
243
+ success: false,
244
+ message: disabled ? API_KEY_DISABLE_USAGE : API_KEY_ENABLE_USAGE,
245
+ };
246
+ }
247
+
248
+ const parts = tokenized.value;
249
+ const hash = parts[0];
250
+
251
+ if (!hash || parts.length !== 1) {
252
+ return {
253
+ success: false,
254
+ message: disabled ? API_KEY_DISABLE_USAGE : API_KEY_ENABLE_USAGE,
255
+ };
256
+ }
257
+
258
+ try {
259
+ const keyInfo = await setApiKeyDisabled(hash, disabled);
260
+ return {
261
+ success: true,
262
+ message: [
263
+ `OpenRouter API key ${disabled ? 'disabled' : 'enabled'}`,
264
+ `Name: ${keyInfo.name}`,
265
+ `Status: ${keyInfo.disabled ? 'disabled' : 'enabled'}`,
266
+ 'Run /openrouter account to verify.',
267
+ ].join('\n'),
268
+ };
269
+ } catch (error) {
270
+ return {
271
+ success: false,
272
+ message: getToggleErrorMessage(error, disabled),
273
+ };
274
+ }
275
+ }
276
+
277
+ function getToggleErrorMessage(error: unknown, disabled: boolean): string {
278
+ const message = getErrorMessage(error);
279
+ if (/OPENROUTER_MANAGEMENT_KEY/i.test(message)) {
280
+ return message;
281
+ }
282
+ return `Failed to ${disabled ? 'disable' : 'enable'} OpenRouter API key. Check the key identifier and management-key permissions.`;
283
+ }
284
+
285
+ function getErrorMessage(error: unknown): string {
286
+ return error instanceof Error ? error.message : String(error);
287
+ }
@@ -12,8 +12,13 @@ import { UsageOverlayComponent } from './overlay.js';
12
12
  import { getCurrentSessionId } from './hooks.js';
13
13
  import { AccountOverlayComponent } from './account-overlay.js';
14
14
  import { computeRollupStatus, sortKeys } from './account-format.js';
15
- import { getAllKeys, getCurrentKey, getAccountCredits } from './account-client.js';
16
- import type { KeyInfo, RollupStatus } from './account-types.js';
15
+ import {
16
+ getAllKeys,
17
+ getCurrentKey,
18
+ resolveCurrentKeyRelation,
19
+ getAccountCredits,
20
+ } from './account-client.js';
21
+ import type { CurrentKeyRelation, KeyInfo, RollupStatus } from './account-types.js';
17
22
  import {
18
23
  syncModels,
19
24
  getSyncState,
@@ -30,6 +35,8 @@ import {
30
35
  handleModelOverrideClear,
31
36
  handleModelOverrideList,
32
37
  } from './models/override-commands.js';
38
+ import type { HandlerResult } from './api-key-commands.js';
39
+ import { handleApiKeyCreate, handleApiKeyDisable, handleApiKeyEnable } from './api-key-commands.js';
33
40
 
34
41
  export const OPENROUTER_SUBCOMMANDS = [
35
42
  'usage',
@@ -40,6 +47,7 @@ export const OPENROUTER_SUBCOMMANDS = [
40
47
  'model-override-set',
41
48
  'model-override-clear',
42
49
  'model-override-list',
50
+ 'api-key-create',
43
51
  ] as const;
44
52
 
45
53
  export function registerOpenRouterCommands(pi: Pick<ExtensionAPI, 'registerCommand'>): void {
@@ -70,7 +78,7 @@ export function registerOpenRouterCommands(pi: Pick<ExtensionAPI, 'registerComma
70
78
  });
71
79
 
72
80
  pi.registerCommand('openrouter', {
73
- description: 'OpenRouter commands: usage, account, session, models-sync, models-status',
81
+ description: `OpenRouter commands: ${OPENROUTER_SUBCOMMANDS.join(', ')}`,
74
82
  getArgumentCompletions: getOpenRouterSubcommandCompletions,
75
83
  handler: async (args, ctx) => {
76
84
  await handleOpenRouterCommand(args, ctx);
@@ -122,6 +130,19 @@ async function handleOpenRouterCommand(args: string, ctx: ExtensionContext): Pro
122
130
  await handleModelOverrideListCommand(subcommandArgs, ctx);
123
131
  break;
124
132
  }
133
+ case 'api-key-create': {
134
+ await handleApiKeyCreateCommand(subcommandArgs, ctx);
135
+ break;
136
+ }
137
+ // Back-compat only: hidden from public help/completions in favor of /openrouter account toggle UX.
138
+ case 'api-key-disable': {
139
+ await handleApiKeyDisableCommand(subcommandArgs, ctx);
140
+ break;
141
+ }
142
+ case 'api-key-enable': {
143
+ await handleApiKeyEnableCommand(subcommandArgs, ctx);
144
+ break;
145
+ }
125
146
  default: {
126
147
  notifyUnknownSubcommand(ctx);
127
148
  break;
@@ -294,6 +315,77 @@ async function handleModelOverrideListCommand(
294
315
  }
295
316
  }
296
317
 
318
+ async function handleApiKeyCreateCommand(
319
+ subcommandArgs: string,
320
+ ctx: ExtensionContext,
321
+ ): Promise<void> {
322
+ const result = await handleApiKeyCreate(subcommandArgs);
323
+ if (!result.success) {
324
+ ctx.ui.notify(result.message, 'error');
325
+ return;
326
+ }
327
+
328
+ if (result.secret) {
329
+ await showApiKeySecretOverlay(ctx, result);
330
+ }
331
+ ctx.ui.notify(result.message, 'info');
332
+ }
333
+
334
+ async function handleApiKeyDisableCommand(
335
+ subcommandArgs: string,
336
+ ctx: ExtensionContext,
337
+ ): Promise<void> {
338
+ const result = await handleApiKeyDisable(subcommandArgs);
339
+ ctx.ui.notify(result.message, result.success ? 'info' : 'error');
340
+ }
341
+
342
+ async function handleApiKeyEnableCommand(
343
+ subcommandArgs: string,
344
+ ctx: ExtensionContext,
345
+ ): Promise<void> {
346
+ const result = await handleApiKeyEnable(subcommandArgs);
347
+ ctx.ui.notify(result.message, result.success ? 'info' : 'error');
348
+ }
349
+
350
+ async function showApiKeySecretOverlay(
351
+ ctx: ExtensionContext,
352
+ result: HandlerResult,
353
+ ): Promise<void> {
354
+ if (!result.secret) return;
355
+
356
+ const lines = [
357
+ 'OpenRouter API key created',
358
+ '',
359
+ ...result.message
360
+ .split('\n')
361
+ .filter((line) => line !== 'OpenRouter API key created' && !line.startsWith('Secret shown')),
362
+ '',
363
+ 'Secret (store now; shown once):',
364
+ result.secret,
365
+ '',
366
+ 'Press any key to close.',
367
+ ];
368
+
369
+ await ctx.ui.custom<void>(
370
+ (_tui, theme, _keybindings, done) => ({
371
+ handleInput: () => {
372
+ done();
373
+ },
374
+ render: (_width: number) =>
375
+ lines.map((line, index) => (index === 0 ? theme.bold(line) : line)),
376
+ invalidate: () => {},
377
+ dispose: () => {},
378
+ wantsKeyRelease: false,
379
+ }),
380
+ {
381
+ overlay: true,
382
+ overlayOptions: {
383
+ width: 120,
384
+ },
385
+ },
386
+ );
387
+ }
388
+
297
389
  function notifyUnknownSubcommand(ctx: ExtensionContext): void {
298
390
  const message = `Available subcommands: ${OPENROUTER_SUBCOMMANDS.join(', ')}`;
299
391
  ctx.ui.notify(`OpenRouter subcommands\n${message}`, 'error');
@@ -336,13 +428,21 @@ async function showAccountOverlay(ctx: ExtensionContext) {
336
428
  let error: string | null = null;
337
429
  let keyInfo: KeyInfo[] | null = null;
338
430
  let credits: number | null = null;
431
+ let canManageKeys = false;
432
+ let currentKeyRelation: CurrentKeyRelation | undefined;
339
433
 
340
434
  try {
341
- const allKeys = await getAllKeys();
435
+ const keyInventory = await getAllKeys();
436
+ canManageKeys = keyInventory.canManageKeys;
342
437
 
343
- if (allKeys && allKeys.length > 0) {
344
- keyInfo = allKeys;
345
- } else {
438
+ if (keyInventory.keys.length > 0) {
439
+ keyInfo = keyInventory.keys;
440
+ try {
441
+ currentKeyRelation = await resolveCurrentKeyRelation(keyInfo);
442
+ } catch {
443
+ // Safe gating: disabling stays blocked until current-key identity is available.
444
+ }
445
+ } else if (keyInventory.degradedReason === 'management-unavailable') {
346
446
  error = 'Key list unavailable - set OPENROUTER_MANAGEMENT_KEY for full key inventory.';
347
447
 
348
448
  try {
@@ -360,12 +460,12 @@ async function showAccountOverlay(ctx: ExtensionContext) {
360
460
 
361
461
  credits = await getAccountCredits();
362
462
 
363
- if (!keyInfo && !credits) {
463
+ if (!keyInfo && !credits && keyInventory.degradedReason === 'missing-api-key') {
364
464
  error =
365
465
  'OpenRouter API key not found. Set OPENROUTER_MANAGEMENT_KEY (preferred) or OPENROUTER_API_KEY to use /openrouter-account.';
366
466
  }
367
467
 
368
- if (!keyInfo && credits !== null) {
468
+ if (!keyInfo && credits !== null && keyInventory.degradedReason === 'management-unavailable') {
369
469
  error =
370
470
  error ||
371
471
  'Key information unavailable. Set OPENROUTER_MANAGEMENT_KEY for full key inventory.';
@@ -379,7 +479,15 @@ async function showAccountOverlay(ctx: ExtensionContext) {
379
479
  keyInfo = sortKeys(keyInfo);
380
480
  }
381
481
 
382
- await showAccountOverlayComponent(ctx, keyInfo, credits, rollupStatus, error);
482
+ await showAccountOverlayComponent(
483
+ ctx,
484
+ keyInfo,
485
+ credits,
486
+ rollupStatus,
487
+ error,
488
+ canManageKeys,
489
+ currentKeyRelation,
490
+ );
383
491
  } catch (error_) {
384
492
  const err = error_ as Error;
385
493
  error =
@@ -400,7 +508,7 @@ async function showAccountOverlay(ctx: ExtensionContext) {
400
508
  ? computeRollupStatus(keyInfo)
401
509
  : { status: 'unavailable' as const };
402
510
 
403
- await showAccountOverlayComponent(ctx, keyInfo, credits, rollupStatus, error);
511
+ await showAccountOverlayComponent(ctx, keyInfo, credits, rollupStatus, error, false);
404
512
  }
405
513
  }
406
514
 
@@ -410,6 +518,8 @@ async function showAccountOverlayComponent(
410
518
  credits: number | null,
411
519
  rollupStatus: RollupStatus,
412
520
  error: string | null,
521
+ canManageKeys: boolean,
522
+ currentKeyRelation?: CurrentKeyRelation,
413
523
  ) {
414
524
  await ctx.ui.custom<void>(
415
525
  (_tui, theme, _keybindings, done) => {
@@ -422,6 +532,8 @@ async function showAccountOverlayComponent(
422
532
  done,
423
533
  () => _tui.requestRender(),
424
534
  ctx,
535
+ canManageKeys,
536
+ currentKeyRelation,
425
537
  );
426
538
 
427
539
  return {
@@ -1,5 +1,11 @@
1
1
  import type { Model as SDKModel } from '@openrouter/sdk/models/index.js';
2
- import type { GetCurrentKeyData, ListData } from '@openrouter/sdk/models/operations/index.js';
2
+ import type {
3
+ CreateKeysData,
4
+ GetCurrentKeyData,
5
+ GetKeyData,
6
+ ListData,
7
+ UpdateKeysData,
8
+ } from '@openrouter/sdk/models/operations/index.js';
3
9
  import type { BYOKStatus, ResetCadence } from './account-types.js';
4
10
  import type { OpenRouterModel } from './models/types.js';
5
11
 
@@ -9,7 +15,7 @@ export interface NormalizedKeyMetadata {
9
15
  used: number;
10
16
  resetCadence: ResetCadence;
11
17
  byok: BYOKStatus;
12
- hash: string;
18
+ hash?: string;
13
19
  disabled: boolean;
14
20
  limit?: number;
15
21
  remaining?: number;
@@ -83,7 +89,9 @@ export function normalizeOpenRouterModel(model: OpenRouterModel | SDKModel): Ope
83
89
  * Normalize SDK key metadata into the package's canonical internal shape.
84
90
  * Converts SDK null/variant fields once so account code can stay domain-focused.
85
91
  */
86
- export function normalizeSdkKeyMetadata(raw: GetCurrentKeyData | ListData): NormalizedKeyMetadata {
92
+ export function normalizeSdkKeyMetadata(
93
+ raw: GetCurrentKeyData | ListData | GetKeyData | CreateKeysData | UpdateKeysData,
94
+ ): NormalizedKeyMetadata {
87
95
  const used = raw.usage ?? raw.usageMonthly ?? 0;
88
96
  const limit = raw.limit ?? undefined;
89
97
  const remaining = raw.limitRemaining ?? undefined;
@@ -96,10 +104,14 @@ export function normalizeSdkKeyMetadata(raw: GetCurrentKeyData | ListData): Norm
96
104
  }
97
105
 
98
106
  let resetCadence: ResetCadence = 'partial';
99
- if (raw.limitReset) {
107
+ if (raw.limitReset === null) {
108
+ resetCadence = 'never';
109
+ } else if (raw.limitReset) {
100
110
  const reset = raw.limitReset.toLowerCase();
101
111
  if (reset === 'monthly') {
102
112
  resetCadence = 'monthly';
113
+ } else if (reset === 'weekly') {
114
+ resetCadence = 'weekly';
103
115
  } else if (reset === 'daily') {
104
116
  resetCadence = 'daily';
105
117
  } else if (reset === 'never') {
@@ -107,16 +119,21 @@ export function normalizeSdkKeyMetadata(raw: GetCurrentKeyData | ListData): Norm
107
119
  }
108
120
  }
109
121
 
122
+ const hash =
123
+ 'hash' in raw && typeof raw.hash === 'string' && raw.hash.trim() !== '' ? raw.hash : undefined;
124
+
110
125
  const normalized: NormalizedKeyMetadata = {
111
126
  name: 'name' in raw ? (raw as ListData).name : raw.label,
112
127
  label: raw.label,
113
128
  used,
114
129
  resetCadence,
115
130
  byok,
116
- hash: 'hash' in raw ? (raw as ListData).hash : 'unknown',
117
131
  disabled: 'disabled' in raw ? (raw as ListData).disabled : false,
118
132
  };
119
133
 
134
+ if (hash !== undefined) {
135
+ normalized.hash = hash;
136
+ }
120
137
  if (limit !== undefined) {
121
138
  normalized.limit = limit;
122
139
  }
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@robhowley/pi-openrouter",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "type": "module",
5
- "description": "Live OpenRouter spend/account TUI overlays, user-scoped model sync, and session tagging for Pi.",
5
+ "description": "Live OpenRouter spend/account TUI overlays, user-scoped model sync, api key management, and session tagging for Pi.",
6
6
  "license": "MIT",
7
7
  "files": [
8
8
  "extensions",