offrouter-core 0.2.1 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. package/dist/audit.d.ts +137 -0
  2. package/dist/audit.d.ts.map +1 -0
  3. package/dist/audit.js +320 -0
  4. package/dist/audit.js.map +1 -0
  5. package/dist/auth/credential-chain.d.ts +84 -0
  6. package/dist/auth/credential-chain.d.ts.map +1 -0
  7. package/dist/auth/credential-chain.js +150 -0
  8. package/dist/auth/credential-chain.js.map +1 -0
  9. package/dist/auth/index.d.ts +10 -0
  10. package/dist/auth/index.d.ts.map +1 -0
  11. package/dist/auth/index.js +7 -0
  12. package/dist/auth/index.js.map +1 -0
  13. package/dist/auth/keychain.d.ts +60 -0
  14. package/dist/auth/keychain.d.ts.map +1 -0
  15. package/dist/auth/keychain.js +104 -0
  16. package/dist/auth/keychain.js.map +1 -0
  17. package/dist/auth/oauth-pkce.d.ts +138 -0
  18. package/dist/auth/oauth-pkce.d.ts.map +1 -0
  19. package/dist/auth/oauth-pkce.js +375 -0
  20. package/dist/auth/oauth-pkce.js.map +1 -0
  21. package/dist/auth/oauth-server.d.ts +36 -0
  22. package/dist/auth/oauth-server.d.ts.map +1 -0
  23. package/dist/auth/oauth-server.js +210 -0
  24. package/dist/auth/oauth-server.js.map +1 -0
  25. package/dist/config.d.ts +76 -0
  26. package/dist/config.d.ts.map +1 -0
  27. package/dist/config.js +355 -0
  28. package/dist/config.js.map +1 -0
  29. package/dist/delegation.d.ts +123 -0
  30. package/dist/delegation.d.ts.map +1 -0
  31. package/dist/delegation.js +455 -0
  32. package/dist/delegation.js.map +1 -0
  33. package/dist/index.d.ts +41 -0
  34. package/dist/index.d.ts.map +1 -0
  35. package/dist/index.js +26 -0
  36. package/dist/index.js.map +1 -0
  37. package/dist/policy.d.ts +45 -0
  38. package/dist/policy.d.ts.map +1 -0
  39. package/dist/policy.js +318 -0
  40. package/dist/policy.js.map +1 -0
  41. package/{src/protocol.ts → dist/protocol.d.ts} +2 -2
  42. package/dist/protocol.d.ts.map +1 -0
  43. package/dist/protocol.js +6 -0
  44. package/dist/protocol.js.map +1 -0
  45. package/dist/providers/adapter.d.ts +137 -0
  46. package/dist/providers/adapter.d.ts.map +1 -0
  47. package/dist/providers/adapter.js +163 -0
  48. package/dist/providers/adapter.js.map +1 -0
  49. package/dist/providers/catalog-data.d.ts +128 -0
  50. package/dist/providers/catalog-data.d.ts.map +1 -0
  51. package/dist/providers/catalog-data.js +397 -0
  52. package/dist/providers/catalog-data.js.map +1 -0
  53. package/dist/providers/catalog.d.ts +63 -0
  54. package/dist/providers/catalog.d.ts.map +1 -0
  55. package/dist/providers/catalog.js +394 -0
  56. package/dist/providers/catalog.js.map +1 -0
  57. package/dist/providers/fake.d.ts +46 -0
  58. package/dist/providers/fake.d.ts.map +1 -0
  59. package/dist/providers/fake.js +234 -0
  60. package/dist/providers/fake.js.map +1 -0
  61. package/dist/providers/openai-compatible.d.ts +65 -0
  62. package/dist/providers/openai-compatible.d.ts.map +1 -0
  63. package/dist/providers/openai-compatible.js +434 -0
  64. package/dist/providers/openai-compatible.js.map +1 -0
  65. package/dist/proxy/responses-mapper.d.ts +366 -0
  66. package/dist/proxy/responses-mapper.d.ts.map +1 -0
  67. package/dist/proxy/responses-mapper.js +517 -0
  68. package/dist/proxy/responses-mapper.js.map +1 -0
  69. package/dist/proxy/responses-server.d.ts +29 -0
  70. package/dist/proxy/responses-server.d.ts.map +1 -0
  71. package/dist/proxy/responses-server.js +360 -0
  72. package/dist/proxy/responses-server.js.map +1 -0
  73. package/dist/router.d.ts +18 -0
  74. package/dist/router.d.ts.map +1 -0
  75. package/dist/router.js +296 -0
  76. package/dist/router.js.map +1 -0
  77. package/dist/schemas.d.ts +560 -0
  78. package/dist/schemas.d.ts.map +1 -0
  79. package/{src/schemas.ts → dist/schemas.js} +83 -103
  80. package/dist/schemas.js.map +1 -0
  81. package/dist/secrets.d.ts +112 -0
  82. package/dist/secrets.d.ts.map +1 -0
  83. package/dist/secrets.js +326 -0
  84. package/dist/secrets.js.map +1 -0
  85. package/dist/types.d.ts +117 -0
  86. package/dist/types.d.ts.map +1 -0
  87. package/dist/types.js +6 -0
  88. package/dist/types.js.map +1 -0
  89. package/dist/usage-file.d.ts +59 -0
  90. package/dist/usage-file.d.ts.map +1 -0
  91. package/dist/usage-file.js +316 -0
  92. package/dist/usage-file.js.map +1 -0
  93. package/dist/usage.d.ts +235 -0
  94. package/dist/usage.d.ts.map +1 -0
  95. package/dist/usage.js +517 -0
  96. package/dist/usage.js.map +1 -0
  97. package/package.json +7 -2
  98. package/src/audit.test.ts +0 -302
  99. package/src/audit.ts +0 -553
  100. package/src/auth/credential-chain.test.ts +0 -326
  101. package/src/auth/credential-chain.ts +0 -235
  102. package/src/auth/index.ts +0 -45
  103. package/src/auth/keychain.test.ts +0 -265
  104. package/src/auth/keychain.ts +0 -168
  105. package/src/auth/oauth-pkce.test.ts +0 -329
  106. package/src/auth/oauth-pkce.ts +0 -571
  107. package/src/auth/oauth-server.test.ts +0 -83
  108. package/src/auth/oauth-server.ts +0 -296
  109. package/src/config.test.ts +0 -479
  110. package/src/config.ts +0 -505
  111. package/src/delegation.test.ts +0 -368
  112. package/src/delegation.ts +0 -605
  113. package/src/index.ts +0 -280
  114. package/src/policy.test.ts +0 -634
  115. package/src/policy.ts +0 -420
  116. package/src/providers/adapter.test.ts +0 -293
  117. package/src/providers/adapter.ts +0 -328
  118. package/src/providers/catalog-data.test.ts +0 -258
  119. package/src/providers/catalog-data.ts +0 -498
  120. package/src/providers/catalog.test.ts +0 -84
  121. package/src/providers/catalog.ts +0 -483
  122. package/src/providers/fake.ts +0 -312
  123. package/src/providers/openai-compatible.test.ts +0 -366
  124. package/src/providers/openai-compatible.ts +0 -554
  125. package/src/proxy/responses-mapper.test.ts +0 -290
  126. package/src/proxy/responses-mapper.ts +0 -736
  127. package/src/proxy/responses-server.test.ts +0 -322
  128. package/src/proxy/responses-server.ts +0 -469
  129. package/src/router.test.ts +0 -699
  130. package/src/router.ts +0 -352
  131. package/src/schemas.test.ts +0 -291
  132. package/src/secrets.test.ts +0 -271
  133. package/src/secrets.ts +0 -461
  134. package/src/types.ts +0 -173
  135. package/src/usage-file.test.ts +0 -243
  136. package/src/usage-file.ts +0 -435
  137. package/src/usage.test.ts +0 -335
  138. package/src/usage.ts +0 -859
  139. package/tsconfig.json +0 -9
@@ -1,634 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import { evaluatePolicy, type PolicyConfig } from "./policy.js";
3
- import type { ProviderCandidate, RouteRequest } from "./types.js";
4
-
5
- function baseRequest(
6
- overrides: Partial<RouteRequest> & {
7
- harness?: Partial<RouteRequest["harness"]>;
8
- task?: Partial<RouteRequest["task"]>;
9
- workspace?: Partial<RouteRequest["workspace"]>;
10
- constraints?: Partial<RouteRequest["constraints"]>;
11
- } = {},
12
- ): RouteRequest {
13
- return {
14
- protocolVersion: "offrouter.route.v1",
15
- requestId: overrides.requestId ?? "req_1",
16
- harness: {
17
- kind: "claude",
18
- profile: "claude-personal",
19
- ...overrides.harness,
20
- },
21
- task: {
22
- promptPreview: "explain this repo",
23
- promptDigest: "sha256:example",
24
- kind: "explain",
25
- risk: "low",
26
- ...overrides.task,
27
- },
28
- workspace: {
29
- cwd: "/tmp/project",
30
- trusted: true,
31
- ...overrides.workspace,
32
- },
33
- constraints: {
34
- maxCostUsd: 1,
35
- localOnly: false,
36
- subscriptionFirst: true,
37
- allowApiKeyFallback: true,
38
- ...overrides.constraints,
39
- },
40
- };
41
- }
42
-
43
- function candidate(
44
- partial: Partial<ProviderCandidate> &
45
- Pick<ProviderCandidate, "providerId" | "modelId" | "authTier">,
46
- ): ProviderCandidate {
47
- return {
48
- authScope: "first-party",
49
- health: "healthy",
50
- supportsTools: true,
51
- supportsStreaming: true,
52
- supportsJson: true,
53
- supportsImages: false,
54
- estimatedCostUsd: 0.01,
55
- ...partial,
56
- };
57
- }
58
-
59
- const personalConfig: PolicyConfig = {
60
- allowlistedProfiles: ["claude-personal", "codex-personal", "gemini-personal"],
61
- deniedProfilePatterns: ["*-work"],
62
- };
63
-
64
- describe("evaluatePolicy", () => {
65
- it("allows claude-personal", () => {
66
- const result = evaluatePolicy(
67
- baseRequest({ harness: { profile: "claude-personal" } }),
68
- [
69
- candidate({
70
- providerId: "claude",
71
- modelId: "sonnet",
72
- authTier: "subscription",
73
- subscriptionStatus: "active",
74
- }),
75
- ],
76
- personalConfig,
77
- );
78
-
79
- expect(result.allowed).toHaveLength(1);
80
- expect(result.denials).toEqual([]);
81
- });
82
-
83
- it("denies claude-work", () => {
84
- const result = evaluatePolicy(
85
- baseRequest({ harness: { profile: "claude-work" } }),
86
- [
87
- candidate({
88
- providerId: "claude",
89
- modelId: "sonnet",
90
- authTier: "subscription",
91
- subscriptionStatus: "active",
92
- }),
93
- ],
94
- personalConfig,
95
- );
96
-
97
- expect(result.allowed).toHaveLength(0);
98
- expect(result.denials).toContainEqual(
99
- expect.objectContaining({
100
- code: "work_profile_denied",
101
- target: "profile",
102
- id: "claude-work",
103
- }),
104
- );
105
- });
106
-
107
- it("denies wildcard *-work profiles", () => {
108
- for (const profile of ["codex-work", "gemini-work", "pi-work"]) {
109
- const result = evaluatePolicy(
110
- baseRequest({ harness: { profile } }),
111
- [
112
- candidate({
113
- providerId: "fake",
114
- modelId: "fast",
115
- authTier: "api-key",
116
- }),
117
- ],
118
- personalConfig,
119
- );
120
-
121
- expect(result.allowed).toHaveLength(0);
122
- expect(result.denials).toContainEqual(
123
- expect.objectContaining({
124
- code: "work_profile_denied",
125
- id: profile,
126
- }),
127
- );
128
- }
129
- });
130
-
131
- it("always denies *-work profiles even with custom deny patterns", () => {
132
- const result = evaluatePolicy(
133
- baseRequest({ harness: { profile: "claude-work" } }),
134
- [
135
- candidate({
136
- providerId: "fake",
137
- modelId: "fast",
138
- authTier: "api-key",
139
- }),
140
- ],
141
- {
142
- allowlistedProfiles: ["claude-work"],
143
- deniedProfilePatterns: ["corp-*"],
144
- },
145
- );
146
-
147
- expect(result.allowed).toHaveLength(0);
148
- expect(result.denials).toContainEqual(
149
- expect.objectContaining({
150
- code: "work_profile_denied",
151
- id: "claude-work",
152
- }),
153
- );
154
- });
155
-
156
- it("denies work-like staging profiles (e.g. claude-work-staging) even when allowlisted", () => {
157
- for (const profile of ["claude-work-staging", "codex-work-staging"]) {
158
- const result = evaluatePolicy(
159
- baseRequest({ harness: { profile } }),
160
- [
161
- candidate({
162
- providerId: "fake",
163
- modelId: "fast",
164
- authTier: "api-key",
165
- }),
166
- ],
167
- {
168
- // Work-like ids are denied by default even if explicitly allowlisted.
169
- allowlistedProfiles: [profile],
170
- },
171
- );
172
-
173
- expect(result.allowed).toHaveLength(0);
174
- expect(result.denials).toContainEqual(
175
- expect.objectContaining({
176
- code: "work_profile_denied",
177
- target: "profile",
178
- id: profile,
179
- }),
180
- );
181
- }
182
- });
183
-
184
- it("denies unallowlisted personal profiles", () => {
185
- const result = evaluatePolicy(
186
- baseRequest({ harness: { profile: "cursor-personal" } }),
187
- [
188
- candidate({
189
- providerId: "cursor",
190
- modelId: "default",
191
- authTier: "subscription",
192
- subscriptionStatus: "active",
193
- }),
194
- ],
195
- personalConfig,
196
- );
197
-
198
- expect(result.allowed).toHaveLength(0);
199
- expect(result.denials).toContainEqual(
200
- expect.objectContaining({
201
- code: "profile_not_allowlisted",
202
- target: "profile",
203
- id: "cursor-personal",
204
- }),
205
- );
206
- });
207
-
208
- it("excludes remote providers when localOnly is set", () => {
209
- const result = evaluatePolicy(
210
- baseRequest({ constraints: { localOnly: true } }),
211
- [
212
- candidate({
213
- providerId: "ollama",
214
- modelId: "llama",
215
- authTier: "local",
216
- estimatedCostUsd: 0,
217
- }),
218
- candidate({
219
- providerId: "openrouter",
220
- modelId: "fast",
221
- authTier: "api-key",
222
- authScope: "third-party",
223
- estimatedCostUsd: 0.001,
224
- }),
225
- candidate({
226
- providerId: "claude",
227
- modelId: "sonnet",
228
- authTier: "subscription",
229
- subscriptionStatus: "active",
230
- }),
231
- ],
232
- personalConfig,
233
- );
234
-
235
- expect(result.allowed.map((c) => c.providerId)).toEqual(["ollama"]);
236
- expect(result.denials).toContainEqual(
237
- expect.objectContaining({
238
- code: "local_only_required",
239
- id: "openrouter:fast",
240
- }),
241
- );
242
- expect(result.denials).toContainEqual(
243
- expect.objectContaining({
244
- code: "local_only_required",
245
- id: "claude:sonnet",
246
- }),
247
- );
248
- });
249
-
250
- it("excludes expensive providers under maxCostUsd", () => {
251
- const result = evaluatePolicy(
252
- baseRequest({ constraints: { maxCostUsd: 0.05 } }),
253
- [
254
- candidate({
255
- providerId: "cheap",
256
- modelId: "fast",
257
- authTier: "api-key",
258
- estimatedCostUsd: 0.01,
259
- }),
260
- candidate({
261
- providerId: "expensive",
262
- modelId: "opus",
263
- authTier: "api-key",
264
- estimatedCostUsd: 0.5,
265
- }),
266
- ],
267
- personalConfig,
268
- );
269
-
270
- expect(result.allowed.map((c) => c.providerId)).toEqual(["cheap"]);
271
- expect(result.denials).toContainEqual(
272
- expect.objectContaining({
273
- code: "cost_cap_exceeded",
274
- id: "expensive:opus",
275
- }),
276
- );
277
- });
278
-
279
- it("excludes unknown-cost API-key providers under maxCostUsd", () => {
280
- const result = evaluatePolicy(
281
- baseRequest({ constraints: { maxCostUsd: 0.05 } }),
282
- [
283
- candidate({
284
- providerId: "unknown-cost",
285
- modelId: "fast",
286
- authTier: "api-key",
287
- estimatedCostUsd: undefined,
288
- }),
289
- ],
290
- personalConfig,
291
- );
292
-
293
- expect(result.allowed).toHaveLength(0);
294
- expect(result.denials).toContainEqual(
295
- expect.objectContaining({
296
- code: "cost_cap_exceeded",
297
- id: "unknown-cost:fast",
298
- }),
299
- );
300
- });
301
-
302
- it("blocks untrusted workspaces", () => {
303
- const result = evaluatePolicy(
304
- baseRequest({ workspace: { trusted: false } }),
305
- [
306
- candidate({
307
- providerId: "claude",
308
- modelId: "sonnet",
309
- authTier: "subscription",
310
- subscriptionStatus: "active",
311
- }),
312
- ],
313
- personalConfig,
314
- );
315
-
316
- expect(result.allowed).toHaveLength(0);
317
- expect(result.denials).toContainEqual(
318
- expect.objectContaining({
319
- code: "project_untrusted",
320
- target: "workspace",
321
- }),
322
- );
323
- });
324
-
325
- it("excludes API-key candidates while a healthy subscription can satisfy the task", () => {
326
- const result = evaluatePolicy(
327
- baseRequest({
328
- constraints: { subscriptionFirst: true, allowApiKeyFallback: true },
329
- }),
330
- [
331
- candidate({
332
- providerId: "claude",
333
- modelId: "sonnet",
334
- authTier: "subscription",
335
- subscriptionStatus: "active",
336
- health: "healthy",
337
- estimatedCostUsd: 0,
338
- }),
339
- candidate({
340
- providerId: "openrouter",
341
- modelId: "fast",
342
- authTier: "api-key",
343
- authScope: "third-party",
344
- estimatedCostUsd: 0.001,
345
- }),
346
- ],
347
- personalConfig,
348
- );
349
-
350
- expect(result.allowed.map((c) => c.providerId)).toEqual(["claude"]);
351
- expect(result.denials).toContainEqual(
352
- expect.objectContaining({
353
- code: "api_key_blocked_by_subscription_first_policy",
354
- target: "candidate",
355
- id: "openrouter:fast",
356
- }),
357
- );
358
- });
359
-
360
- it("allows API-key fallback when only a degraded subscription can satisfy the task", () => {
361
- const result = evaluatePolicy(
362
- baseRequest({
363
- constraints: { subscriptionFirst: true, allowApiKeyFallback: true },
364
- }),
365
- [
366
- candidate({
367
- providerId: "claude",
368
- modelId: "sonnet",
369
- authTier: "subscription",
370
- subscriptionStatus: "active",
371
- health: "degraded",
372
- estimatedCostUsd: 0,
373
- }),
374
- candidate({
375
- providerId: "openrouter",
376
- modelId: "fast",
377
- authTier: "api-key",
378
- authScope: "third-party",
379
- estimatedCostUsd: 0.001,
380
- }),
381
- ],
382
- personalConfig,
383
- );
384
-
385
- expect(result.allowed.map((c) => c.providerId)).toEqual([
386
- "claude",
387
- "openrouter",
388
- ]);
389
- expect(result.denials).not.toContainEqual(
390
- expect.objectContaining({
391
- code: "api_key_blocked_by_subscription_first_policy",
392
- }),
393
- );
394
- });
395
-
396
- it("allows API-key candidates through policy when subscriptionFirst is disabled", () => {
397
- const result = evaluatePolicy(
398
- baseRequest({
399
- constraints: { subscriptionFirst: false, allowApiKeyFallback: false },
400
- }),
401
- [
402
- candidate({
403
- providerId: "claude",
404
- modelId: "sonnet",
405
- authTier: "subscription",
406
- subscriptionStatus: "active",
407
- health: "healthy",
408
- estimatedCostUsd: 0,
409
- }),
410
- candidate({
411
- providerId: "openrouter",
412
- modelId: "fast",
413
- authTier: "api-key",
414
- authScope: "third-party",
415
- estimatedCostUsd: 0.001,
416
- }),
417
- ],
418
- personalConfig,
419
- );
420
-
421
- expect(result.allowed.map((c) => c.providerId)).toEqual([
422
- "claude",
423
- "openrouter",
424
- ]);
425
- });
426
-
427
- it("allows API-key fallback only when subscriptions are exhausted, rate-limited, unavailable, or policy-ineligible", () => {
428
- const cases: Array<{
429
- label: string;
430
- candidates: ProviderCandidate[];
431
- expectApiKeyAllowed: boolean;
432
- }> = [
433
- {
434
- label: "exhausted",
435
- candidates: [
436
- candidate({
437
- providerId: "claude",
438
- modelId: "sonnet",
439
- authTier: "subscription",
440
- subscriptionStatus: "exhausted",
441
- health: "healthy",
442
- }),
443
- candidate({
444
- providerId: "openrouter",
445
- modelId: "fast",
446
- authTier: "api-key",
447
- authScope: "third-party",
448
- }),
449
- ],
450
- expectApiKeyAllowed: true,
451
- },
452
- {
453
- label: "rate-limited",
454
- candidates: [
455
- candidate({
456
- providerId: "claude",
457
- modelId: "sonnet",
458
- authTier: "subscription",
459
- subscriptionStatus: "rate-limited",
460
- health: "healthy",
461
- }),
462
- candidate({
463
- providerId: "openrouter",
464
- modelId: "fast",
465
- authTier: "api-key",
466
- authScope: "third-party",
467
- }),
468
- ],
469
- expectApiKeyAllowed: true,
470
- },
471
- {
472
- label: "unconfigured subscription",
473
- candidates: [
474
- candidate({
475
- providerId: "claude",
476
- modelId: "sonnet",
477
- authTier: "subscription",
478
- subscriptionStatus: "unconfigured",
479
- health: "dead",
480
- }),
481
- candidate({
482
- providerId: "openrouter",
483
- modelId: "fast",
484
- authTier: "api-key",
485
- authScope: "third-party",
486
- }),
487
- ],
488
- expectApiKeyAllowed: true,
489
- },
490
- {
491
- label: "no subscription candidates",
492
- candidates: [
493
- candidate({
494
- providerId: "openrouter",
495
- modelId: "fast",
496
- authTier: "api-key",
497
- authScope: "third-party",
498
- }),
499
- ],
500
- expectApiKeyAllowed: true,
501
- },
502
- {
503
- label: "subscription tool-ineligible for tool task",
504
- candidates: [
505
- candidate({
506
- providerId: "claude",
507
- modelId: "text",
508
- authTier: "subscription",
509
- subscriptionStatus: "active",
510
- health: "healthy",
511
- supportsTools: false,
512
- }),
513
- candidate({
514
- providerId: "openrouter",
515
- modelId: "tools",
516
- authTier: "api-key",
517
- authScope: "third-party",
518
- supportsTools: true,
519
- }),
520
- ],
521
- expectApiKeyAllowed: true,
522
- },
523
- ];
524
-
525
- for (const { label, candidates, expectApiKeyAllowed } of cases) {
526
- const result = evaluatePolicy(
527
- baseRequest({
528
- task: {
529
- kind: label.includes("tool") ? "tools" : "explain",
530
- requiresTools: label.includes("tool"),
531
- },
532
- constraints: { subscriptionFirst: true, allowApiKeyFallback: true },
533
- }),
534
- candidates,
535
- personalConfig,
536
- );
537
-
538
- const apiKeyAllowed = result.allowed.some((c) => c.authTier === "api-key");
539
- expect(apiKeyAllowed, label).toBe(expectApiKeyAllowed);
540
- }
541
- });
542
-
543
- it("denies API-key fallback when allowApiKeyFallback is false even if subscription is exhausted", () => {
544
- const result = evaluatePolicy(
545
- baseRequest({
546
- constraints: { subscriptionFirst: true, allowApiKeyFallback: false },
547
- }),
548
- [
549
- candidate({
550
- providerId: "claude",
551
- modelId: "sonnet",
552
- authTier: "subscription",
553
- subscriptionStatus: "exhausted",
554
- }),
555
- candidate({
556
- providerId: "openrouter",
557
- modelId: "fast",
558
- authTier: "api-key",
559
- authScope: "third-party",
560
- }),
561
- ],
562
- personalConfig,
563
- );
564
-
565
- expect(result.allowed).toHaveLength(0);
566
- expect(result.denials).toContainEqual(
567
- expect.objectContaining({
568
- code: "api_key_blocked_by_subscription_first_policy",
569
- id: "openrouter:fast",
570
- }),
571
- );
572
- });
573
-
574
- it("denies first-party subscription surface used as third-party unless policy allows", () => {
575
- const thirdPartySub = candidate({
576
- providerId: "openrouter",
577
- modelId: "claude-sonnet",
578
- authTier: "subscription",
579
- authScope: "third-party",
580
- subscriptionStatus: "active",
581
- });
582
-
583
- const denied = evaluatePolicy(
584
- baseRequest(),
585
- [thirdPartySub],
586
- personalConfig,
587
- );
588
- expect(denied.allowed).toHaveLength(0);
589
- expect(denied.denials).toContainEqual(
590
- expect.objectContaining({
591
- code: "subscription_scope_denied",
592
- target: "candidate",
593
- id: "openrouter:claude-sonnet",
594
- }),
595
- );
596
-
597
- const allowed = evaluatePolicy(baseRequest(), [thirdPartySub], {
598
- ...personalConfig,
599
- allowThirdPartySubscriptionAdapters: true,
600
- });
601
- expect(allowed.allowed).toHaveLength(1);
602
- });
603
-
604
- it("denies unknown subscription auth scope until the adapter proves support", () => {
605
- const result = evaluatePolicy(
606
- baseRequest(),
607
- [
608
- candidate({
609
- providerId: "unknown-sub",
610
- modelId: "sonnet",
611
- authTier: "subscription",
612
- authScope: "unknown",
613
- subscriptionStatus: "active",
614
- }),
615
- candidate({
616
- providerId: "openrouter",
617
- modelId: "fast",
618
- authTier: "api-key",
619
- authScope: "third-party",
620
- estimatedCostUsd: 0.001,
621
- }),
622
- ],
623
- personalConfig,
624
- );
625
-
626
- expect(result.allowed.map((c) => c.providerId)).toEqual(["openrouter"]);
627
- expect(result.denials).toContainEqual(
628
- expect.objectContaining({
629
- code: "subscription_scope_denied",
630
- id: "unknown-sub:sonnet",
631
- }),
632
- );
633
- });
634
- });