@robhowley/pi-openrouter 0.9.1 → 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.
- package/README.md +26 -5
- package/extensions/openrouter/__tests__/account-client.test.ts +488 -0
- package/extensions/openrouter/__tests__/account-overlay.test.ts +683 -0
- package/extensions/openrouter/__tests__/api-key-commands.test.ts +231 -0
- package/extensions/openrouter/__tests__/commands.test.ts +225 -3
- package/extensions/openrouter/__tests__/hooks.test.ts +262 -9
- package/extensions/openrouter/__tests__/normalizers.test.ts +86 -3
- package/extensions/openrouter/__tests__/status-bar.test.ts +262 -0
- package/extensions/openrouter/account-client.ts +275 -28
- package/extensions/openrouter/account-overlay.ts +417 -60
- package/extensions/openrouter/account-types.ts +10 -3
- package/extensions/openrouter/api-key-commands.ts +287 -0
- package/extensions/openrouter/commands.ts +123 -11
- package/extensions/openrouter/hooks.ts +70 -11
- package/extensions/openrouter/local-usage.ts +15 -10
- package/extensions/openrouter/normalizers.ts +22 -5
- package/extensions/openrouter/status-bar.ts +101 -0
- package/package.json +2 -2
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
2
|
|
|
3
3
|
const mocks = vi.hoisted(() => ({
|
|
4
4
|
stopBackgroundRefresh: vi.fn(),
|
|
@@ -10,6 +10,7 @@ const mocks = vi.hoisted(() => ({
|
|
|
10
10
|
mapOpenRouterModels: vi.fn(),
|
|
11
11
|
includeBuiltinRouterModels: vi.fn(),
|
|
12
12
|
isSyncEnabled: vi.fn(),
|
|
13
|
+
loadOpenRouterStatusBar: vi.fn(),
|
|
13
14
|
}));
|
|
14
15
|
|
|
15
16
|
vi.mock('../cache.js', () => ({
|
|
@@ -40,6 +41,10 @@ vi.mock('../models/sync.js', () => ({
|
|
|
40
41
|
isSyncEnabled: mocks.isSyncEnabled,
|
|
41
42
|
}));
|
|
42
43
|
|
|
44
|
+
vi.mock('../status-bar.js', () => ({
|
|
45
|
+
loadOpenRouterStatusBar: mocks.loadOpenRouterStatusBar,
|
|
46
|
+
}));
|
|
47
|
+
|
|
43
48
|
function createMockPi() {
|
|
44
49
|
const handlers = new Map<string, any>();
|
|
45
50
|
return {
|
|
@@ -69,6 +74,14 @@ function createMockContext(overrides: { hasUI?: boolean } = {}) {
|
|
|
69
74
|
} as any;
|
|
70
75
|
}
|
|
71
76
|
|
|
77
|
+
function createDeferredPromise<T>() {
|
|
78
|
+
let resolve!: (value: T | PromiseLike<T>) => void;
|
|
79
|
+
const promise = new Promise<T>((res) => {
|
|
80
|
+
resolve = res;
|
|
81
|
+
});
|
|
82
|
+
return { promise, resolve };
|
|
83
|
+
}
|
|
84
|
+
|
|
72
85
|
async function loadHooksModule() {
|
|
73
86
|
return import('../hooks.js');
|
|
74
87
|
}
|
|
@@ -77,6 +90,7 @@ describe('openrouter hooks', () => {
|
|
|
77
90
|
beforeEach(() => {
|
|
78
91
|
vi.resetModules();
|
|
79
92
|
vi.resetAllMocks();
|
|
93
|
+
vi.useRealTimers();
|
|
80
94
|
|
|
81
95
|
mocks.isOpenRouterRequest.mockReturnValue(true);
|
|
82
96
|
mocks.writeLocalUsage.mockResolvedValue(undefined);
|
|
@@ -86,9 +100,14 @@ describe('openrouter hooks', () => {
|
|
|
86
100
|
mocks.mapOpenRouterModels.mockResolvedValue({ configs: [{ id: 'model-a' }] });
|
|
87
101
|
mocks.includeBuiltinRouterModels.mockReturnValue([{ id: 'model-a' }, { id: 'router' }]);
|
|
88
102
|
mocks.isSyncEnabled.mockReturnValue(true);
|
|
103
|
+
mocks.loadOpenRouterStatusBar.mockResolvedValue({ kind: 'empty' });
|
|
89
104
|
});
|
|
90
105
|
|
|
91
|
-
|
|
106
|
+
afterEach(() => {
|
|
107
|
+
vi.useRealTimers();
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('loads cached startup models and preserves startup cache info', async () => {
|
|
92
111
|
const { pi } = createMockPi();
|
|
93
112
|
const { loadStartupCacheState } = await loadHooksModule();
|
|
94
113
|
|
|
@@ -148,41 +167,89 @@ describe('openrouter hooks', () => {
|
|
|
148
167
|
expect(pi.on.mock.calls.filter(([event]) => event === 'session_shutdown')).toHaveLength(2);
|
|
149
168
|
});
|
|
150
169
|
|
|
151
|
-
it('
|
|
170
|
+
it('sets startup usage status from the local burn-rate helper and keeps cache notifications', async () => {
|
|
152
171
|
const { handlers, pi } = createMockPi();
|
|
153
172
|
const ctx = createMockContext();
|
|
154
173
|
const { initializeSessionState, installOpenRouterHooks } = await loadHooksModule();
|
|
155
174
|
|
|
175
|
+
mocks.loadOpenRouterStatusBar.mockResolvedValue({
|
|
176
|
+
kind: 'ready',
|
|
177
|
+
text: 'OR $2.14 today · 1.3x 30d avg',
|
|
178
|
+
});
|
|
179
|
+
|
|
156
180
|
initializeSessionState();
|
|
157
181
|
installOpenRouterHooks(pi as any, {
|
|
158
182
|
info: { count: 5, age: '3 minutes' },
|
|
159
183
|
});
|
|
160
184
|
|
|
161
|
-
handlers.get('session_start')({ reason: 'startup' }, ctx);
|
|
185
|
+
await handlers.get('session_start')({ reason: 'startup' }, ctx);
|
|
162
186
|
|
|
163
|
-
expect(
|
|
187
|
+
expect(mocks.loadOpenRouterStatusBar).toHaveBeenCalledTimes(1);
|
|
188
|
+
await vi.waitFor(() => {
|
|
189
|
+
expect(ctx.ui.setStatus).toHaveBeenCalledWith(
|
|
190
|
+
'openrouter',
|
|
191
|
+
'dim:OR $2.14 today · 1.3x 30d avg',
|
|
192
|
+
);
|
|
193
|
+
});
|
|
194
|
+
expect(String(ctx.ui.setStatus.mock.calls[0]?.[1] ?? '')).not.toContain('models');
|
|
164
195
|
expect(ctx.ui.notify).toHaveBeenCalledWith(
|
|
165
196
|
'OpenRouter: 5 models loaded from cache (3 minutes old). Run /openrouter models-sync to refresh.',
|
|
166
197
|
'info',
|
|
167
198
|
);
|
|
168
199
|
});
|
|
169
200
|
|
|
170
|
-
it('
|
|
201
|
+
it('clears stale startup status asynchronously when cached models exist but local spend is empty', async () => {
|
|
171
202
|
const { handlers, pi } = createMockPi();
|
|
172
203
|
const ctx = createMockContext();
|
|
173
204
|
const { initializeSessionState, installOpenRouterHooks } = await loadHooksModule();
|
|
205
|
+
const statusLoad = createDeferredPromise<{ kind: 'ready'; text: string } | { kind: 'empty' }>();
|
|
206
|
+
|
|
207
|
+
mocks.loadOpenRouterStatusBar.mockReturnValue(statusLoad.promise);
|
|
208
|
+
|
|
209
|
+
initializeSessionState();
|
|
210
|
+
installOpenRouterHooks(pi as any, {
|
|
211
|
+
info: { count: 5, age: '3 minutes' },
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
await handlers.get('session_start')({ reason: 'startup' }, ctx);
|
|
215
|
+
|
|
216
|
+
expect(mocks.loadOpenRouterStatusBar).toHaveBeenCalledTimes(1);
|
|
217
|
+
expect(ctx.ui.notify).toHaveBeenCalledWith(
|
|
218
|
+
'OpenRouter: 5 models loaded from cache (3 minutes old). Run /openrouter models-sync to refresh.',
|
|
219
|
+
'info',
|
|
220
|
+
);
|
|
221
|
+
expect(ctx.ui.setStatus).not.toHaveBeenCalled();
|
|
222
|
+
|
|
223
|
+
statusLoad.resolve({ kind: 'empty' });
|
|
224
|
+
|
|
225
|
+
await vi.waitFor(() => {
|
|
226
|
+
expect(ctx.ui.setStatus).toHaveBeenCalledWith('openrouter', undefined);
|
|
227
|
+
});
|
|
228
|
+
expect(ctx.ui.theme.fg).not.toHaveBeenCalled();
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it('preserves the existing startup status when the usage helper fails open', async () => {
|
|
232
|
+
const { handlers, pi } = createMockPi();
|
|
233
|
+
const ctx = createMockContext();
|
|
234
|
+
const { initializeSessionState, installOpenRouterHooks } = await loadHooksModule();
|
|
235
|
+
|
|
236
|
+
mocks.loadOpenRouterStatusBar.mockResolvedValue({ kind: 'failed' });
|
|
174
237
|
|
|
175
238
|
initializeSessionState();
|
|
176
239
|
installOpenRouterHooks(pi as any, {
|
|
177
240
|
warning: 'OpenRouter: cached models found but failed to register: mapper failed',
|
|
178
241
|
});
|
|
179
242
|
|
|
180
|
-
handlers.get('session_start')({ reason: 'startup' }, ctx);
|
|
243
|
+
await handlers.get('session_start')({ reason: 'startup' }, ctx);
|
|
181
244
|
|
|
245
|
+
expect(mocks.loadOpenRouterStatusBar).toHaveBeenCalledTimes(1);
|
|
182
246
|
expect(ctx.ui.notify).toHaveBeenCalledWith(
|
|
183
247
|
'OpenRouter: cached models found but failed to register: mapper failed',
|
|
184
248
|
'warning',
|
|
185
249
|
);
|
|
250
|
+
await Promise.resolve();
|
|
251
|
+
expect(ctx.ui.setStatus).not.toHaveBeenCalled();
|
|
252
|
+
expect(ctx.ui.theme.fg).not.toHaveBeenCalled();
|
|
186
253
|
});
|
|
187
254
|
|
|
188
255
|
it('keeps before_provider_request session tagging behavior unchanged through the installed hook', async () => {
|
|
@@ -214,13 +281,20 @@ describe('openrouter hooks', () => {
|
|
|
214
281
|
});
|
|
215
282
|
});
|
|
216
283
|
|
|
217
|
-
it('
|
|
284
|
+
it('refreshes the usage status after a successful OpenRouter turn_end local write without blocking the hook', async () => {
|
|
218
285
|
const { handlers, pi } = createMockPi();
|
|
219
286
|
const ctx = createMockContext();
|
|
220
287
|
const { initializeSessionState, installOpenRouterHooks } = await loadHooksModule();
|
|
221
288
|
const randomUuidSpy = vi
|
|
222
289
|
.spyOn(globalThis.crypto, 'randomUUID')
|
|
223
290
|
.mockReturnValue('11111111-1111-4111-8111-111111111111');
|
|
291
|
+
const writeLocalUsageDeferred = createDeferredPromise<void>();
|
|
292
|
+
|
|
293
|
+
mocks.writeLocalUsage.mockReturnValue(writeLocalUsageDeferred.promise);
|
|
294
|
+
mocks.loadOpenRouterStatusBar.mockResolvedValue({
|
|
295
|
+
kind: 'ready',
|
|
296
|
+
text: 'OR $1.25 today · 30.0x 30d avg',
|
|
297
|
+
});
|
|
224
298
|
|
|
225
299
|
initializeSessionState();
|
|
226
300
|
installOpenRouterHooks(pi as any, {});
|
|
@@ -258,19 +332,198 @@ describe('openrouter hooks', () => {
|
|
|
258
332
|
cost: 1.25,
|
|
259
333
|
}),
|
|
260
334
|
);
|
|
335
|
+
expect(mocks.loadOpenRouterStatusBar).not.toHaveBeenCalled();
|
|
336
|
+
expect(ctx.ui.setStatus).not.toHaveBeenCalled();
|
|
337
|
+
|
|
338
|
+
writeLocalUsageDeferred.resolve();
|
|
339
|
+
|
|
340
|
+
await vi.waitFor(() => {
|
|
341
|
+
expect(mocks.loadOpenRouterStatusBar).toHaveBeenCalledTimes(1);
|
|
342
|
+
});
|
|
343
|
+
const writeCallOrder = mocks.writeLocalUsage.mock.invocationCallOrder[0] ?? 0;
|
|
344
|
+
const refreshCallOrder = mocks.loadOpenRouterStatusBar.mock.invocationCallOrder[0] ?? 0;
|
|
345
|
+
expect(writeCallOrder).toBeLessThan(refreshCallOrder);
|
|
346
|
+
await vi.waitFor(() => {
|
|
347
|
+
expect(ctx.ui.setStatus).toHaveBeenCalledWith(
|
|
348
|
+
'openrouter',
|
|
349
|
+
'dim:OR $1.25 today · 30.0x 30d avg',
|
|
350
|
+
);
|
|
351
|
+
});
|
|
352
|
+
expect(String(ctx.ui.setStatus.mock.calls[0]?.[1] ?? '')).not.toContain('models');
|
|
261
353
|
|
|
262
354
|
randomUuidSpy.mockRestore();
|
|
263
355
|
});
|
|
264
356
|
|
|
265
|
-
it('
|
|
357
|
+
it('does not write local usage or update status for non-OpenRouter turns', async () => {
|
|
266
358
|
const { handlers, pi } = createMockPi();
|
|
359
|
+
const ctx = createMockContext();
|
|
267
360
|
const { initializeSessionState, installOpenRouterHooks } = await loadHooksModule();
|
|
268
361
|
|
|
362
|
+
mocks.isOpenRouterRequest.mockReturnValue(false);
|
|
363
|
+
mocks.loadOpenRouterStatusBar.mockResolvedValue({
|
|
364
|
+
kind: 'ready',
|
|
365
|
+
text: 'OR $9.99 today · 9.9x 30d avg',
|
|
366
|
+
});
|
|
367
|
+
|
|
269
368
|
initializeSessionState();
|
|
270
369
|
installOpenRouterHooks(pi as any, {});
|
|
271
370
|
|
|
371
|
+
await handlers.get('turn_end')(
|
|
372
|
+
{
|
|
373
|
+
url: 'https://api.anthropic.com/v1/messages',
|
|
374
|
+
message: {
|
|
375
|
+
model: 'claude-sonnet-4',
|
|
376
|
+
responseId: 'resp-2',
|
|
377
|
+
usage: {
|
|
378
|
+
input: 4,
|
|
379
|
+
output: 2,
|
|
380
|
+
cost: { total: 0.42 },
|
|
381
|
+
},
|
|
382
|
+
},
|
|
383
|
+
},
|
|
384
|
+
ctx,
|
|
385
|
+
);
|
|
386
|
+
|
|
387
|
+
expect(mocks.writeLocalUsage).not.toHaveBeenCalled();
|
|
388
|
+
expect(mocks.loadOpenRouterStatusBar).not.toHaveBeenCalled();
|
|
389
|
+
expect(ctx.ui.setStatus).not.toHaveBeenCalled();
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
it('clears stale status after a turn when the usage helper reports empty local spend', async () => {
|
|
393
|
+
const { handlers, pi } = createMockPi();
|
|
394
|
+
const ctx = createMockContext();
|
|
395
|
+
const { initializeSessionState, installOpenRouterHooks } = await loadHooksModule();
|
|
396
|
+
|
|
397
|
+
mocks.loadOpenRouterStatusBar.mockResolvedValue({ kind: 'empty' });
|
|
398
|
+
|
|
399
|
+
initializeSessionState();
|
|
400
|
+
installOpenRouterHooks(pi as any, {});
|
|
401
|
+
|
|
402
|
+
await handlers.get('turn_end')(
|
|
403
|
+
{
|
|
404
|
+
url: 'https://openrouter.ai/api/v1/chat/completions',
|
|
405
|
+
message: {
|
|
406
|
+
model: 'openrouter/anthropic/claude-sonnet-4',
|
|
407
|
+
responseId: 'resp-3',
|
|
408
|
+
usage: {
|
|
409
|
+
input: 1,
|
|
410
|
+
output: 1,
|
|
411
|
+
cost: { total: 0.01 },
|
|
412
|
+
},
|
|
413
|
+
},
|
|
414
|
+
},
|
|
415
|
+
ctx,
|
|
416
|
+
);
|
|
417
|
+
|
|
418
|
+
await vi.waitFor(() => {
|
|
419
|
+
expect(ctx.ui.setStatus).toHaveBeenCalledWith('openrouter', undefined);
|
|
420
|
+
});
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
it('preserves the existing status after a turn when the usage helper fails open', async () => {
|
|
424
|
+
const { handlers, pi } = createMockPi();
|
|
425
|
+
const ctx = createMockContext();
|
|
426
|
+
const { initializeSessionState, installOpenRouterHooks } = await loadHooksModule();
|
|
427
|
+
|
|
428
|
+
mocks.loadOpenRouterStatusBar.mockResolvedValue({ kind: 'failed' });
|
|
429
|
+
|
|
430
|
+
initializeSessionState();
|
|
431
|
+
installOpenRouterHooks(pi as any, {});
|
|
432
|
+
|
|
433
|
+
await handlers.get('turn_end')(
|
|
434
|
+
{
|
|
435
|
+
url: 'https://openrouter.ai/api/v1/chat/completions',
|
|
436
|
+
message: {
|
|
437
|
+
model: 'openrouter/anthropic/claude-sonnet-4',
|
|
438
|
+
responseId: 'resp-4',
|
|
439
|
+
usage: {
|
|
440
|
+
input: 1,
|
|
441
|
+
output: 1,
|
|
442
|
+
cost: { total: 0.01 },
|
|
443
|
+
},
|
|
444
|
+
},
|
|
445
|
+
},
|
|
446
|
+
ctx,
|
|
447
|
+
);
|
|
448
|
+
|
|
449
|
+
await Promise.resolve();
|
|
450
|
+
expect(ctx.ui.setStatus).not.toHaveBeenCalled();
|
|
451
|
+
expect(ctx.ui.theme.fg).not.toHaveBeenCalled();
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
it('refreshes the status at UTC midnight and reschedules while the session stays active', async () => {
|
|
455
|
+
vi.useFakeTimers();
|
|
456
|
+
vi.setSystemTime(new Date('2026-05-22T23:59:50.000Z'));
|
|
457
|
+
|
|
458
|
+
const { handlers, pi } = createMockPi();
|
|
459
|
+
const ctx = createMockContext();
|
|
460
|
+
const { initializeSessionState, installOpenRouterHooks } = await loadHooksModule();
|
|
461
|
+
|
|
462
|
+
mocks.loadOpenRouterStatusBar
|
|
463
|
+
.mockResolvedValueOnce({ kind: 'ready', text: 'OR $4.50 today · 2.0x 30d avg' })
|
|
464
|
+
.mockResolvedValueOnce({ kind: 'ready', text: 'OR $0.25 today · 0.2x 30d avg' })
|
|
465
|
+
.mockResolvedValueOnce({ kind: 'ready', text: 'OR $0.30 today · 0.2x 30d avg' });
|
|
466
|
+
|
|
467
|
+
initializeSessionState();
|
|
468
|
+
installOpenRouterHooks(pi as any, {});
|
|
469
|
+
|
|
470
|
+
await handlers.get('session_start')({ reason: 'startup' }, ctx);
|
|
471
|
+
|
|
472
|
+
await vi.waitFor(() => {
|
|
473
|
+
expect(ctx.ui.setStatus).toHaveBeenCalledWith(
|
|
474
|
+
'openrouter',
|
|
475
|
+
'dim:OR $4.50 today · 2.0x 30d avg',
|
|
476
|
+
);
|
|
477
|
+
});
|
|
478
|
+
expect(mocks.loadOpenRouterStatusBar).toHaveBeenCalledTimes(1);
|
|
479
|
+
|
|
480
|
+
await vi.advanceTimersByTimeAsync(10_000);
|
|
481
|
+
|
|
482
|
+
await vi.waitFor(() => {
|
|
483
|
+
expect(mocks.loadOpenRouterStatusBar).toHaveBeenCalledTimes(2);
|
|
484
|
+
});
|
|
485
|
+
expect(ctx.ui.setStatus).toHaveBeenLastCalledWith(
|
|
486
|
+
'openrouter',
|
|
487
|
+
'dim:OR $0.25 today · 0.2x 30d avg',
|
|
488
|
+
);
|
|
489
|
+
|
|
490
|
+
await vi.advanceTimersByTimeAsync(24 * 60 * 60 * 1000);
|
|
491
|
+
|
|
492
|
+
await vi.waitFor(() => {
|
|
493
|
+
expect(mocks.loadOpenRouterStatusBar).toHaveBeenCalledTimes(3);
|
|
494
|
+
});
|
|
495
|
+
expect(ctx.ui.setStatus).toHaveBeenLastCalledWith(
|
|
496
|
+
'openrouter',
|
|
497
|
+
'dim:OR $0.30 today · 0.2x 30d avg',
|
|
498
|
+
);
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
it('clears the UTC-midnight rollover timer on session shutdown', async () => {
|
|
502
|
+
vi.useFakeTimers();
|
|
503
|
+
vi.setSystemTime(new Date('2026-05-22T23:59:50.000Z'));
|
|
504
|
+
|
|
505
|
+
const { handlers, pi } = createMockPi();
|
|
506
|
+
const ctx = createMockContext();
|
|
507
|
+
const { initializeSessionState, installOpenRouterHooks } = await loadHooksModule();
|
|
508
|
+
|
|
509
|
+
mocks.loadOpenRouterStatusBar.mockResolvedValue({
|
|
510
|
+
kind: 'ready',
|
|
511
|
+
text: 'OR $1.00 today · 1.0x 30d avg',
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
initializeSessionState();
|
|
515
|
+
installOpenRouterHooks(pi as any, {});
|
|
516
|
+
|
|
517
|
+
await handlers.get('session_start')({ reason: 'startup' }, ctx);
|
|
518
|
+
await vi.waitFor(() => {
|
|
519
|
+
expect(mocks.loadOpenRouterStatusBar).toHaveBeenCalledTimes(1);
|
|
520
|
+
});
|
|
521
|
+
|
|
272
522
|
handlers.get('session_shutdown')();
|
|
273
523
|
|
|
524
|
+
await vi.advanceTimersByTimeAsync(24 * 60 * 60 * 1000);
|
|
525
|
+
|
|
526
|
+
expect(mocks.loadOpenRouterStatusBar).toHaveBeenCalledTimes(1);
|
|
274
527
|
expect(mocks.stopBackgroundRefresh).toHaveBeenCalledTimes(1);
|
|
275
528
|
});
|
|
276
529
|
});
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
2
|
import type { Model as SDKModel } from '@openrouter/sdk/models/index.js';
|
|
3
|
-
import type {
|
|
3
|
+
import type {
|
|
4
|
+
CreateKeysData,
|
|
5
|
+
GetCurrentKeyData,
|
|
6
|
+
ListData,
|
|
7
|
+
UpdateKeysData,
|
|
8
|
+
} from '@openrouter/sdk/models/operations/index.js';
|
|
4
9
|
import {
|
|
5
10
|
normalizeOpenRouterModel,
|
|
6
11
|
normalizeSdkKeyMetadata,
|
|
@@ -89,6 +94,58 @@ function createListKeyData(overrides: Partial<ListData> = {}): ListData {
|
|
|
89
94
|
};
|
|
90
95
|
}
|
|
91
96
|
|
|
97
|
+
function createCreateKeysData(overrides: Partial<CreateKeysData> = {}): CreateKeysData {
|
|
98
|
+
return {
|
|
99
|
+
byokUsage: 0,
|
|
100
|
+
byokUsageDaily: 0,
|
|
101
|
+
byokUsageMonthly: 0,
|
|
102
|
+
byokUsageWeekly: 0,
|
|
103
|
+
createdAt: '2026-05-22T00:00:00.000Z',
|
|
104
|
+
creatorUserId: null,
|
|
105
|
+
disabled: false,
|
|
106
|
+
hash: 'hash-create',
|
|
107
|
+
includeByokInLimit: true,
|
|
108
|
+
label: 'sk-or-v1-create',
|
|
109
|
+
limit: 100,
|
|
110
|
+
limitRemaining: 40,
|
|
111
|
+
limitReset: 'weekly',
|
|
112
|
+
name: 'Created Key',
|
|
113
|
+
updatedAt: null,
|
|
114
|
+
usage: 60,
|
|
115
|
+
usageDaily: 0,
|
|
116
|
+
usageMonthly: 0,
|
|
117
|
+
usageWeekly: 0,
|
|
118
|
+
workspaceId: 'ws-create',
|
|
119
|
+
...overrides,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function createUpdateKeysData(overrides: Partial<UpdateKeysData> = {}): UpdateKeysData {
|
|
124
|
+
return {
|
|
125
|
+
byokUsage: 0,
|
|
126
|
+
byokUsageDaily: 0,
|
|
127
|
+
byokUsageMonthly: 0,
|
|
128
|
+
byokUsageWeekly: 0,
|
|
129
|
+
createdAt: '2026-05-22T00:00:00.000Z',
|
|
130
|
+
creatorUserId: null,
|
|
131
|
+
disabled: false,
|
|
132
|
+
hash: 'hash-update',
|
|
133
|
+
includeByokInLimit: false,
|
|
134
|
+
label: 'sk-or-v1-update',
|
|
135
|
+
limit: 100,
|
|
136
|
+
limitRemaining: 40,
|
|
137
|
+
limitReset: null,
|
|
138
|
+
name: 'Updated Key',
|
|
139
|
+
updatedAt: null,
|
|
140
|
+
usage: 60,
|
|
141
|
+
usageDaily: 0,
|
|
142
|
+
usageMonthly: 0,
|
|
143
|
+
usageWeekly: 0,
|
|
144
|
+
workspaceId: 'ws-update',
|
|
145
|
+
...overrides,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
92
149
|
describe('sdkModelToOpenRouterModel', () => {
|
|
93
150
|
it('normalizes SDK camelCase fields into canonical snake_case model shape', () => {
|
|
94
151
|
const normalized = sdkModelToOpenRouterModel(
|
|
@@ -237,9 +294,9 @@ describe('normalizeSdkKeyMetadata', () => {
|
|
|
237
294
|
remaining: 0,
|
|
238
295
|
byok: 'excl',
|
|
239
296
|
resetCadence: 'daily',
|
|
240
|
-
hash: 'unknown',
|
|
241
297
|
disabled: false,
|
|
242
298
|
});
|
|
299
|
+
expect(normalized).not.toHaveProperty('hash');
|
|
243
300
|
expect('limit' in normalized).toBe(true);
|
|
244
301
|
expect('remaining' in normalized).toBe(true);
|
|
245
302
|
});
|
|
@@ -258,9 +315,9 @@ describe('normalizeSdkKeyMetadata', () => {
|
|
|
258
315
|
name: 'sk-or-v1-current',
|
|
259
316
|
byok: '?',
|
|
260
317
|
resetCadence: 'partial',
|
|
261
|
-
hash: 'unknown',
|
|
262
318
|
disabled: false,
|
|
263
319
|
});
|
|
320
|
+
expect(normalized).not.toHaveProperty('hash');
|
|
264
321
|
expect(normalized).not.toHaveProperty('limit');
|
|
265
322
|
expect(normalized).not.toHaveProperty('remaining');
|
|
266
323
|
});
|
|
@@ -285,4 +342,30 @@ describe('normalizeSdkKeyMetadata', () => {
|
|
|
285
342
|
remaining: 40,
|
|
286
343
|
});
|
|
287
344
|
});
|
|
345
|
+
|
|
346
|
+
it('normalizes create responses with weekly resets', () => {
|
|
347
|
+
const normalized = normalizeSdkKeyMetadata(createCreateKeysData());
|
|
348
|
+
|
|
349
|
+
expect(normalized).toMatchObject({
|
|
350
|
+
name: 'Created Key',
|
|
351
|
+
label: 'sk-or-v1-create',
|
|
352
|
+
byok: 'incl',
|
|
353
|
+
resetCadence: 'weekly',
|
|
354
|
+
hash: 'hash-create',
|
|
355
|
+
disabled: false,
|
|
356
|
+
});
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
it('treats null limitReset in update responses as never', () => {
|
|
360
|
+
const normalized = normalizeSdkKeyMetadata(createUpdateKeysData());
|
|
361
|
+
|
|
362
|
+
expect(normalized).toMatchObject({
|
|
363
|
+
name: 'Updated Key',
|
|
364
|
+
label: 'sk-or-v1-update',
|
|
365
|
+
byok: 'excl',
|
|
366
|
+
resetCadence: 'never',
|
|
367
|
+
hash: 'hash-update',
|
|
368
|
+
disabled: false,
|
|
369
|
+
});
|
|
370
|
+
});
|
|
288
371
|
});
|