@robhowley/pi-openrouter 0.10.0 → 0.11.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 +18 -3
- 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__/normalizers.test.ts +86 -3
- 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/normalizers.ts +22 -5
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# pi-openrouter
|
|
2
2
|
|
|
3
|
-
A [Pi](https://pi.dev/) extension for live OpenRouter visibility and environment sync: usage/account TUI overlays, automatic `session_id` tagging, user-scoped model catalog sync, and local model field overrides.
|
|
3
|
+
A [Pi](https://pi.dev/) extension for live OpenRouter visibility and environment sync: usage/account TUI overlays, automatic `session_id` tagging, user-scoped model catalog sync, api key management, and local model field overrides.
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
@@ -28,8 +28,9 @@ export OPENROUTER_MANAGEMENT_KEY=sk-or-...
|
|
|
28
28
|
|
|
29
29
|
```bash
|
|
30
30
|
/openrouter usage # usage/spend overlay
|
|
31
|
-
/openrouter account # credits, key limits, account health
|
|
31
|
+
/openrouter account # credits, key limits, account health, key toggle UI
|
|
32
32
|
/openrouter session # current OpenRouter session_id
|
|
33
|
+
/openrouter api-key-create # create an API key (management key required)
|
|
33
34
|
/openrouter models-sync # sync user-scoped OpenRouter models into Pi
|
|
34
35
|
/openrouter models-status # show model sync/cache status
|
|
35
36
|
/openrouter models-status --skipped # show skipped model reasons
|
|
@@ -103,10 +104,24 @@ The overlay shows:
|
|
|
103
104
|
- **BYOK limit behavior**
|
|
104
105
|
- **All visible keys**, when a management key is configured
|
|
105
106
|
|
|
106
|
-
|
|
107
|
+
Use `/openrouter account`, select a key with the arrow keys, press `t` to toggle enabled/disabled, and press Enter to confirm. You can still inspect the selected key's limit, usage, reset cadence, and BYOK behavior from the same overlay.
|
|
107
108
|
|
|
108
109
|
<img src="https://raw.githubusercontent.com/robhowley/pi-userland/main/packages/pi-openrouter/img/openrouter-account-tui.png" alt="OpenRouter Account Overlay" width="600">
|
|
109
110
|
|
|
111
|
+
## API key management
|
|
112
|
+
|
|
113
|
+
API key management requires `OPENROUTER_MANAGEMENT_KEY`. `OPENROUTER_API_KEY` alone is not enough for key creation or account-level toggles.
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
/openrouter api-key-create <name> [limit=<usd|none>] [reset=<daily|weekly|monthly|none>] [byok=<incl|excl>] [workspace=<id>] [expires=<UTC ISO>]
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Wrap `<name>` in quotes when it contains spaces, for example `/openrouter api-key-create "Team Key" limit=25`.
|
|
120
|
+
|
|
121
|
+
To enable or disable a key, use `/openrouter account`, select the key with the arrow keys, press `t` to toggle it, and press Enter to confirm.
|
|
122
|
+
|
|
123
|
+
When you create a key, Pi shows the returned secret once in an ephemeral overlay. Copy/store it immediately, do not expect Pi or OpenRouter to reveal it again, and avoid logging it or writing it to local files/debug output.
|
|
124
|
+
|
|
110
125
|
## Session tracking
|
|
111
126
|
|
|
112
127
|
`pi-openrouter` automatically tags OpenRouter requests with a `session_id` derived from the Pi session ID.
|
|
@@ -0,0 +1,488 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
vi.mock('@openrouter/sdk/sdk/sdk.js', () => ({
|
|
4
|
+
OpenRouter: vi.fn(),
|
|
5
|
+
}));
|
|
6
|
+
|
|
7
|
+
vi.mock('../client.js', () => ({
|
|
8
|
+
AuthError: class AuthError extends Error {
|
|
9
|
+
constructor(message: string) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.name = 'AuthError';
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
ApiError: class ApiError extends Error {
|
|
15
|
+
statusCode?: number;
|
|
16
|
+
|
|
17
|
+
constructor(message: string, statusCode?: number) {
|
|
18
|
+
super(message);
|
|
19
|
+
this.name = 'ApiError';
|
|
20
|
+
if (statusCode !== undefined) {
|
|
21
|
+
this.statusCode = statusCode;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
}));
|
|
26
|
+
|
|
27
|
+
function setKeys(apiKey?: string, mgmtKey?: string) {
|
|
28
|
+
if (apiKey === undefined) {
|
|
29
|
+
delete process.env['OPENROUTER_API_KEY'];
|
|
30
|
+
} else {
|
|
31
|
+
process.env['OPENROUTER_API_KEY'] = apiKey;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (mgmtKey === undefined) {
|
|
35
|
+
delete process.env['OPENROUTER_MANAGEMENT_KEY'];
|
|
36
|
+
} else {
|
|
37
|
+
process.env['OPENROUTER_MANAGEMENT_KEY'] = mgmtKey;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function createMockSDKClient(
|
|
42
|
+
overrides: {
|
|
43
|
+
create?: ReturnType<typeof vi.fn>;
|
|
44
|
+
update?: ReturnType<typeof vi.fn>;
|
|
45
|
+
} = {},
|
|
46
|
+
) {
|
|
47
|
+
return {
|
|
48
|
+
credits: { getCredits: vi.fn() },
|
|
49
|
+
workspaces: { list: vi.fn() },
|
|
50
|
+
apiKeys: {
|
|
51
|
+
create: overrides.create ?? vi.fn(),
|
|
52
|
+
update: overrides.update ?? vi.fn(),
|
|
53
|
+
list: vi.fn(),
|
|
54
|
+
getCurrentKeyMetadata: vi.fn(),
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function createInventoryKey(overrides: Record<string, unknown> = {}) {
|
|
60
|
+
return {
|
|
61
|
+
name: 'Primary',
|
|
62
|
+
label: 'sk-or-v1-123',
|
|
63
|
+
status: 'healthy',
|
|
64
|
+
used: 10,
|
|
65
|
+
remaining: 90,
|
|
66
|
+
limit: 100,
|
|
67
|
+
resetCadence: 'monthly',
|
|
68
|
+
byok: 'incl',
|
|
69
|
+
hash: 'hash-1',
|
|
70
|
+
disabled: false,
|
|
71
|
+
workspaceName: 'Main Workspace',
|
|
72
|
+
spend: 10,
|
|
73
|
+
...overrides,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function createCurrentKeyMetadata(overrides: Record<string, unknown> = {}) {
|
|
78
|
+
return {
|
|
79
|
+
byokUsage: 0,
|
|
80
|
+
byokUsageDaily: 0,
|
|
81
|
+
byokUsageMonthly: 0,
|
|
82
|
+
byokUsageWeekly: 0,
|
|
83
|
+
creatorUserId: null,
|
|
84
|
+
includeByokInLimit: true,
|
|
85
|
+
isFreeTier: false,
|
|
86
|
+
isManagementKey: true,
|
|
87
|
+
isProvisioningKey: false,
|
|
88
|
+
label: 'sk-or-v1-123',
|
|
89
|
+
limit: 100,
|
|
90
|
+
limitRemaining: 90,
|
|
91
|
+
limitReset: 'monthly',
|
|
92
|
+
usage: 10,
|
|
93
|
+
usageDaily: 0,
|
|
94
|
+
usageMonthly: 0,
|
|
95
|
+
usageWeekly: 0,
|
|
96
|
+
...overrides,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
describe('account-client api key management', () => {
|
|
101
|
+
beforeEach(() => {
|
|
102
|
+
vi.resetAllMocks();
|
|
103
|
+
vi.resetModules();
|
|
104
|
+
setKeys(undefined, undefined);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('returns a missing-api-key inventory result when no OpenRouter key is configured', async () => {
|
|
108
|
+
const { getAllKeys } = await import('../account-client.js');
|
|
109
|
+
|
|
110
|
+
await expect(getAllKeys()).resolves.toEqual({
|
|
111
|
+
keys: [],
|
|
112
|
+
canManageKeys: false,
|
|
113
|
+
degradedReason: 'missing-api-key',
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('returns an empty but manageable inventory when key listing succeeds without keys', async () => {
|
|
118
|
+
setKeys(undefined, 'mgmt-key');
|
|
119
|
+
|
|
120
|
+
const mockClient = createMockSDKClient();
|
|
121
|
+
mockClient.workspaces.list.mockResolvedValue({
|
|
122
|
+
async *[Symbol.asyncIterator]() {
|
|
123
|
+
yield { result: { data: [] } };
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
const { OpenRouter } = await import('@openrouter/sdk/sdk/sdk.js');
|
|
128
|
+
vi.mocked(OpenRouter).mockImplementation(() => mockClient as any);
|
|
129
|
+
|
|
130
|
+
const { getAllKeys } = await import('../account-client.js');
|
|
131
|
+
|
|
132
|
+
await expect(getAllKeys()).resolves.toEqual({
|
|
133
|
+
keys: [],
|
|
134
|
+
canManageKeys: true,
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('returns an explicit degraded inventory when key management is unavailable', async () => {
|
|
139
|
+
setKeys('plain-api-key', undefined);
|
|
140
|
+
|
|
141
|
+
const mockClient = createMockSDKClient();
|
|
142
|
+
mockClient.workspaces.list.mockRejectedValue({ status: 403, message: 'Forbidden' });
|
|
143
|
+
|
|
144
|
+
const { OpenRouter } = await import('@openrouter/sdk/sdk/sdk.js');
|
|
145
|
+
vi.mocked(OpenRouter).mockImplementation(() => mockClient as any);
|
|
146
|
+
|
|
147
|
+
const { getAllKeys } = await import('../account-client.js');
|
|
148
|
+
|
|
149
|
+
await expect(getAllKeys()).resolves.toEqual({
|
|
150
|
+
keys: [],
|
|
151
|
+
canManageKeys: false,
|
|
152
|
+
degradedReason: 'management-unavailable',
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('resolves an inventory-match relation from a unique inventory label match', async () => {
|
|
157
|
+
setKeys(undefined, 'mgmt-key');
|
|
158
|
+
|
|
159
|
+
const mockClient = createMockSDKClient();
|
|
160
|
+
mockClient.apiKeys.getCurrentKeyMetadata.mockResolvedValue({
|
|
161
|
+
data: createCurrentKeyMetadata({ label: 'sk-or-v1-active' }),
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
const { OpenRouter } = await import('@openrouter/sdk/sdk/sdk.js');
|
|
165
|
+
vi.mocked(OpenRouter).mockImplementation(() => mockClient as any);
|
|
166
|
+
|
|
167
|
+
const { resolveCurrentKeyRelation } = await import('../account-client.js');
|
|
168
|
+
|
|
169
|
+
await expect(
|
|
170
|
+
resolveCurrentKeyRelation([
|
|
171
|
+
createInventoryKey({
|
|
172
|
+
name: 'default-space-key',
|
|
173
|
+
label: 'sk-or-v1-active',
|
|
174
|
+
hash: 'hash-active',
|
|
175
|
+
workspaceName: 'Default',
|
|
176
|
+
}),
|
|
177
|
+
createInventoryKey({
|
|
178
|
+
name: 'other-key',
|
|
179
|
+
label: 'sk-or-v1-other',
|
|
180
|
+
hash: 'hash-other',
|
|
181
|
+
workspaceName: 'Dev',
|
|
182
|
+
}),
|
|
183
|
+
] as any),
|
|
184
|
+
).resolves.toEqual({
|
|
185
|
+
kind: 'inventory-match',
|
|
186
|
+
hash: 'hash-active',
|
|
187
|
+
label: 'sk-or-v1-active',
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('resolves an external-provisioning relation when the current provisioning key is outside inventory', async () => {
|
|
192
|
+
setKeys(undefined, 'mgmt-key');
|
|
193
|
+
|
|
194
|
+
const mockClient = createMockSDKClient();
|
|
195
|
+
mockClient.apiKeys.getCurrentKeyMetadata.mockResolvedValue({
|
|
196
|
+
data: createCurrentKeyMetadata({
|
|
197
|
+
label: 'sk-or-v1-provisioning',
|
|
198
|
+
isProvisioningKey: true,
|
|
199
|
+
}),
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
const { OpenRouter } = await import('@openrouter/sdk/sdk/sdk.js');
|
|
203
|
+
vi.mocked(OpenRouter).mockImplementation(() => mockClient as any);
|
|
204
|
+
|
|
205
|
+
const { resolveCurrentKeyRelation } = await import('../account-client.js');
|
|
206
|
+
|
|
207
|
+
await expect(
|
|
208
|
+
resolveCurrentKeyRelation([
|
|
209
|
+
createInventoryKey({
|
|
210
|
+
name: 'default-space-key',
|
|
211
|
+
label: 'sk-or-v1-active',
|
|
212
|
+
hash: 'hash-active',
|
|
213
|
+
workspaceName: 'Default',
|
|
214
|
+
}),
|
|
215
|
+
] as any),
|
|
216
|
+
).resolves.toEqual({
|
|
217
|
+
kind: 'external-provisioning',
|
|
218
|
+
label: 'sk-or-v1-provisioning',
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it('returns an ambiguous-label relation when multiple inventory rows share the current label', async () => {
|
|
223
|
+
setKeys(undefined, 'mgmt-key');
|
|
224
|
+
|
|
225
|
+
const mockClient = createMockSDKClient();
|
|
226
|
+
mockClient.apiKeys.getCurrentKeyMetadata.mockResolvedValue({
|
|
227
|
+
data: createCurrentKeyMetadata({ label: 'sk-or-v1-active' }),
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
const { OpenRouter } = await import('@openrouter/sdk/sdk/sdk.js');
|
|
231
|
+
vi.mocked(OpenRouter).mockImplementation(() => mockClient as any);
|
|
232
|
+
|
|
233
|
+
const { resolveCurrentKeyRelation } = await import('../account-client.js');
|
|
234
|
+
|
|
235
|
+
await expect(
|
|
236
|
+
resolveCurrentKeyRelation([
|
|
237
|
+
createInventoryKey({
|
|
238
|
+
name: 'default-space-key',
|
|
239
|
+
label: 'sk-or-v1-active',
|
|
240
|
+
hash: 'hash-active-1',
|
|
241
|
+
workspaceName: 'Default',
|
|
242
|
+
}),
|
|
243
|
+
createInventoryKey({
|
|
244
|
+
name: 'duplicate-label-key',
|
|
245
|
+
label: 'sk-or-v1-active',
|
|
246
|
+
hash: 'hash-active-2',
|
|
247
|
+
workspaceName: 'Dev',
|
|
248
|
+
}),
|
|
249
|
+
] as any),
|
|
250
|
+
).resolves.toEqual({
|
|
251
|
+
kind: 'ambiguous-label',
|
|
252
|
+
label: 'sk-or-v1-active',
|
|
253
|
+
matchingHashes: ['hash-active-1', 'hash-active-2'],
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it('createApiKey sends the expected request body and returns the secret with normalized mutation state', async () => {
|
|
258
|
+
setKeys(undefined, 'mgmt-key');
|
|
259
|
+
|
|
260
|
+
const expiresAt = new Date('2026-06-01T00:00:00Z');
|
|
261
|
+
const mockClient = createMockSDKClient({
|
|
262
|
+
create: vi.fn().mockResolvedValue({
|
|
263
|
+
key: 'sk-or-v1-created-secret',
|
|
264
|
+
data: {
|
|
265
|
+
byokUsage: 0,
|
|
266
|
+
byokUsageDaily: 0,
|
|
267
|
+
byokUsageMonthly: 0,
|
|
268
|
+
byokUsageWeekly: 0,
|
|
269
|
+
createdAt: '2026-05-30T00:00:00.000Z',
|
|
270
|
+
creatorUserId: null,
|
|
271
|
+
disabled: false,
|
|
272
|
+
hash: 'hash-create',
|
|
273
|
+
includeByokInLimit: false,
|
|
274
|
+
label: 'sk-or-v1-created',
|
|
275
|
+
limit: null,
|
|
276
|
+
limitRemaining: null,
|
|
277
|
+
limitReset: null,
|
|
278
|
+
name: 'Team Key',
|
|
279
|
+
updatedAt: null,
|
|
280
|
+
usage: 0,
|
|
281
|
+
usageDaily: 0,
|
|
282
|
+
usageMonthly: 0,
|
|
283
|
+
usageWeekly: 0,
|
|
284
|
+
workspaceId: 'ws-1',
|
|
285
|
+
expiresAt,
|
|
286
|
+
},
|
|
287
|
+
}),
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
const { OpenRouter } = await import('@openrouter/sdk/sdk/sdk.js');
|
|
291
|
+
vi.mocked(OpenRouter).mockImplementation(() => mockClient as any);
|
|
292
|
+
|
|
293
|
+
const { createApiKey } = await import('../account-client.js');
|
|
294
|
+
const result = await createApiKey({
|
|
295
|
+
name: 'Team Key',
|
|
296
|
+
limit: null,
|
|
297
|
+
limitReset: null,
|
|
298
|
+
includeByokInLimit: false,
|
|
299
|
+
workspaceId: 'ws-1',
|
|
300
|
+
expiresAt,
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
expect(mockClient.apiKeys.create).toHaveBeenCalledWith({
|
|
304
|
+
requestBody: {
|
|
305
|
+
name: 'Team Key',
|
|
306
|
+
limit: null,
|
|
307
|
+
limitReset: null,
|
|
308
|
+
includeByokInLimit: false,
|
|
309
|
+
workspaceId: 'ws-1',
|
|
310
|
+
expiresAt,
|
|
311
|
+
},
|
|
312
|
+
});
|
|
313
|
+
expect(result).toMatchObject({
|
|
314
|
+
key: 'sk-or-v1-created-secret',
|
|
315
|
+
keyState: {
|
|
316
|
+
name: 'Team Key',
|
|
317
|
+
hash: 'hash-create',
|
|
318
|
+
byok: 'excl',
|
|
319
|
+
resetCadence: 'never',
|
|
320
|
+
status: 'unbounded',
|
|
321
|
+
},
|
|
322
|
+
});
|
|
323
|
+
expect(result.keyState).not.toHaveProperty('workspaceName');
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
it('setApiKeyDisabled patches disabled status and returns normalized mutation state', async () => {
|
|
327
|
+
setKeys(undefined, 'mgmt-key');
|
|
328
|
+
|
|
329
|
+
const mockClient = createMockSDKClient({
|
|
330
|
+
update: vi.fn().mockResolvedValue({
|
|
331
|
+
data: {
|
|
332
|
+
byokUsage: 0,
|
|
333
|
+
byokUsageDaily: 0,
|
|
334
|
+
byokUsageMonthly: 0,
|
|
335
|
+
byokUsageWeekly: 0,
|
|
336
|
+
createdAt: '2026-05-30T00:00:00.000Z',
|
|
337
|
+
creatorUserId: null,
|
|
338
|
+
disabled: true,
|
|
339
|
+
hash: 'hash-disable',
|
|
340
|
+
includeByokInLimit: true,
|
|
341
|
+
label: 'sk-or-v1-disable',
|
|
342
|
+
limit: 100,
|
|
343
|
+
limitRemaining: 100,
|
|
344
|
+
limitReset: 'weekly',
|
|
345
|
+
name: 'Mutable Key',
|
|
346
|
+
updatedAt: null,
|
|
347
|
+
usage: 0,
|
|
348
|
+
usageDaily: 0,
|
|
349
|
+
usageMonthly: 0,
|
|
350
|
+
usageWeekly: 0,
|
|
351
|
+
workspaceId: 'ws-2',
|
|
352
|
+
},
|
|
353
|
+
}),
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
const { OpenRouter } = await import('@openrouter/sdk/sdk/sdk.js');
|
|
357
|
+
vi.mocked(OpenRouter).mockImplementation(() => mockClient as any);
|
|
358
|
+
|
|
359
|
+
const { setApiKeyDisabled } = await import('../account-client.js');
|
|
360
|
+
const result = await setApiKeyDisabled('hash-disable', true);
|
|
361
|
+
|
|
362
|
+
expect(mockClient.apiKeys.update).toHaveBeenCalledWith({
|
|
363
|
+
hash: 'hash-disable',
|
|
364
|
+
requestBody: { disabled: true },
|
|
365
|
+
});
|
|
366
|
+
expect(result).toMatchObject({
|
|
367
|
+
name: 'Mutable Key',
|
|
368
|
+
hash: 'hash-disable',
|
|
369
|
+
disabled: true,
|
|
370
|
+
byok: 'incl',
|
|
371
|
+
resetCadence: 'weekly',
|
|
372
|
+
status: 'disabled',
|
|
373
|
+
});
|
|
374
|
+
expect(result).not.toHaveProperty('workspaceName');
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
it('requires OPENROUTER_MANAGEMENT_KEY for create operations', async () => {
|
|
378
|
+
setKeys('plain-api-key', undefined);
|
|
379
|
+
|
|
380
|
+
const { createApiKey } = await import('../account-client.js');
|
|
381
|
+
|
|
382
|
+
await expect(createApiKey({ name: 'Team Key' })).rejects.toMatchObject({
|
|
383
|
+
message: 'OPENROUTER_MANAGEMENT_KEY is required for API key management.',
|
|
384
|
+
});
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
it('maps unauthorized create errors to a management-key-required message', async () => {
|
|
388
|
+
setKeys(undefined, 'bad-mgmt-key');
|
|
389
|
+
|
|
390
|
+
const mockClient = createMockSDKClient({
|
|
391
|
+
create: vi.fn().mockRejectedValue({ status: 401, message: 'Unauthorized' }),
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
const { OpenRouter } = await import('@openrouter/sdk/sdk/sdk.js');
|
|
395
|
+
vi.mocked(OpenRouter).mockImplementation(() => mockClient as any);
|
|
396
|
+
|
|
397
|
+
const { createApiKey } = await import('../account-client.js');
|
|
398
|
+
|
|
399
|
+
await expect(createApiKey({ name: 'Team Key' })).rejects.toMatchObject({
|
|
400
|
+
message:
|
|
401
|
+
'OPENROUTER_MANAGEMENT_KEY is required to create API keys. Set it to a valid management key.',
|
|
402
|
+
});
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
it('maps forbidden create errors to a management-key permissions message', async () => {
|
|
406
|
+
setKeys(undefined, 'mgmt-key');
|
|
407
|
+
|
|
408
|
+
const mockClient = createMockSDKClient({
|
|
409
|
+
create: vi.fn().mockRejectedValue({ status: 403, message: 'Forbidden' }),
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
const { OpenRouter } = await import('@openrouter/sdk/sdk/sdk.js');
|
|
413
|
+
vi.mocked(OpenRouter).mockImplementation(() => mockClient as any);
|
|
414
|
+
|
|
415
|
+
const { createApiKey } = await import('../account-client.js');
|
|
416
|
+
|
|
417
|
+
await expect(createApiKey({ name: 'Team Key' })).rejects.toMatchObject({
|
|
418
|
+
message:
|
|
419
|
+
'OPENROUTER_MANAGEMENT_KEY does not have permission to create API keys. Set it to a valid management key.',
|
|
420
|
+
statusCode: 403,
|
|
421
|
+
});
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
it('requires OPENROUTER_MANAGEMENT_KEY for toggle operations', async () => {
|
|
425
|
+
setKeys('plain-api-key', undefined);
|
|
426
|
+
|
|
427
|
+
const { setApiKeyDisabled } = await import('../account-client.js');
|
|
428
|
+
|
|
429
|
+
await expect(setApiKeyDisabled('hash-disable', true)).rejects.toMatchObject({
|
|
430
|
+
message: 'OPENROUTER_MANAGEMENT_KEY is required for API key management.',
|
|
431
|
+
});
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
it('maps unauthorized disable errors to a management-key-required message', async () => {
|
|
435
|
+
setKeys(undefined, 'bad-mgmt-key');
|
|
436
|
+
|
|
437
|
+
const mockClient = createMockSDKClient({
|
|
438
|
+
update: vi.fn().mockRejectedValue({ status: 401, message: 'Unauthorized' }),
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
const { OpenRouter } = await import('@openrouter/sdk/sdk/sdk.js');
|
|
442
|
+
vi.mocked(OpenRouter).mockImplementation(() => mockClient as any);
|
|
443
|
+
|
|
444
|
+
const { setApiKeyDisabled } = await import('../account-client.js');
|
|
445
|
+
|
|
446
|
+
await expect(setApiKeyDisabled('hash-disable', true)).rejects.toMatchObject({
|
|
447
|
+
message:
|
|
448
|
+
'OPENROUTER_MANAGEMENT_KEY is required to disable API keys. Set it to a valid management key.',
|
|
449
|
+
});
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
it('maps forbidden enable errors to a management-key permissions message', async () => {
|
|
453
|
+
setKeys(undefined, 'mgmt-key');
|
|
454
|
+
|
|
455
|
+
const mockClient = createMockSDKClient({
|
|
456
|
+
update: vi.fn().mockRejectedValue({ status: 403, message: 'Forbidden' }),
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
const { OpenRouter } = await import('@openrouter/sdk/sdk/sdk.js');
|
|
460
|
+
vi.mocked(OpenRouter).mockImplementation(() => mockClient as any);
|
|
461
|
+
|
|
462
|
+
const { setApiKeyDisabled } = await import('../account-client.js');
|
|
463
|
+
|
|
464
|
+
await expect(setApiKeyDisabled('hash-enable', false)).rejects.toMatchObject({
|
|
465
|
+
message:
|
|
466
|
+
'OPENROUTER_MANAGEMENT_KEY does not have permission to enable API keys. Set it to a valid management key.',
|
|
467
|
+
statusCode: 403,
|
|
468
|
+
});
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
it('preserves not-found update messages and status codes', async () => {
|
|
472
|
+
setKeys(undefined, 'mgmt-key');
|
|
473
|
+
|
|
474
|
+
const mockClient = createMockSDKClient({
|
|
475
|
+
update: vi.fn().mockRejectedValue({ status: 404, message: 'Key not found' }),
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
const { OpenRouter } = await import('@openrouter/sdk/sdk/sdk.js');
|
|
479
|
+
vi.mocked(OpenRouter).mockImplementation(() => mockClient as any);
|
|
480
|
+
|
|
481
|
+
const { setApiKeyDisabled } = await import('../account-client.js');
|
|
482
|
+
|
|
483
|
+
await expect(setApiKeyDisabled('missing-hash', true)).rejects.toMatchObject({
|
|
484
|
+
message: 'Key not found',
|
|
485
|
+
statusCode: 404,
|
|
486
|
+
});
|
|
487
|
+
});
|
|
488
|
+
});
|