offrouter-core 0.2.0 → 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 +9 -4
  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,699 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import type { PolicyConfig } from "./policy.js";
3
- import { routeRequest } from "./router.js";
4
- import type { ProviderCandidate, RouteRequest } from "./types.js";
5
-
6
- function baseRequest(
7
- overrides: Partial<RouteRequest> & {
8
- harness?: Partial<RouteRequest["harness"]>;
9
- task?: Partial<RouteRequest["task"]>;
10
- workspace?: Partial<RouteRequest["workspace"]>;
11
- constraints?: Partial<RouteRequest["constraints"]>;
12
- } = {},
13
- ): RouteRequest {
14
- return {
15
- protocolVersion: "offrouter.route.v1",
16
- requestId: overrides.requestId ?? "req_1",
17
- harness: {
18
- kind: "claude",
19
- profile: "claude-personal",
20
- ...overrides.harness,
21
- },
22
- task: {
23
- promptPreview: "explain this repo",
24
- promptDigest: "sha256:example",
25
- kind: "explain",
26
- risk: "low",
27
- ...overrides.task,
28
- },
29
- workspace: {
30
- cwd: "/tmp/project",
31
- trusted: true,
32
- ...overrides.workspace,
33
- },
34
- constraints: {
35
- maxCostUsd: 1,
36
- localOnly: false,
37
- subscriptionFirst: true,
38
- allowApiKeyFallback: true,
39
- ...overrides.constraints,
40
- },
41
- };
42
- }
43
-
44
- function candidate(
45
- partial: Partial<ProviderCandidate> &
46
- Pick<ProviderCandidate, "providerId" | "modelId" | "authTier">,
47
- ): ProviderCandidate {
48
- return {
49
- authScope: "first-party",
50
- health: "healthy",
51
- supportsTools: true,
52
- supportsStreaming: true,
53
- supportsJson: true,
54
- supportsImages: false,
55
- estimatedCostUsd: 0.01,
56
- ...partial,
57
- };
58
- }
59
-
60
- const personalConfig: PolicyConfig = {
61
- allowlistedProfiles: ["claude-personal", "codex-personal", "gemini-personal"],
62
- deniedProfilePatterns: ["*-work"],
63
- };
64
-
65
- describe("routeRequest", () => {
66
- it("picks a cheap fast allowed candidate for a simple low-risk explain task", () => {
67
- const decision = routeRequest(
68
- baseRequest({
69
- task: { kind: "explain", risk: "low" },
70
- }),
71
- [
72
- candidate({
73
- providerId: "slow",
74
- modelId: "heavy",
75
- authTier: "api-key",
76
- estimatedCostUsd: 0.2,
77
- }),
78
- candidate({
79
- providerId: "cheap",
80
- modelId: "fast",
81
- authTier: "api-key",
82
- estimatedCostUsd: 0.001,
83
- }),
84
- ],
85
- personalConfig,
86
- );
87
-
88
- expect(decision.blocked).toBeFalsy();
89
- expect(decision.route).toEqual(
90
- expect.objectContaining({
91
- provider: "cheap",
92
- model: "fast",
93
- authTier: "api-key",
94
- }),
95
- );
96
- expect(decision.reason).toBeTruthy();
97
- expect(decision.explanation).toBeTruthy();
98
- expect(decision.fallbackChain?.[0]).toEqual(
99
- expect.objectContaining({
100
- provider: "cheap",
101
- model: "fast",
102
- reason: "primary",
103
- }),
104
- );
105
- });
106
-
107
- it("prefers a healthy subscription-backed candidate over a cheaper API-key candidate", () => {
108
- const decision = routeRequest(
109
- baseRequest({
110
- constraints: { subscriptionFirst: true, allowApiKeyFallback: true },
111
- }),
112
- [
113
- candidate({
114
- providerId: "openrouter",
115
- modelId: "fast",
116
- authTier: "api-key",
117
- authScope: "third-party",
118
- estimatedCostUsd: 0.0001,
119
- }),
120
- candidate({
121
- providerId: "claude",
122
- modelId: "sonnet",
123
- authTier: "subscription",
124
- subscriptionStatus: "active",
125
- health: "healthy",
126
- estimatedCostUsd: 0,
127
- }),
128
- ],
129
- personalConfig,
130
- );
131
-
132
- expect(decision.route).toEqual(
133
- expect.objectContaining({
134
- provider: "claude",
135
- model: "sonnet",
136
- authTier: "subscription",
137
- }),
138
- );
139
- expect(decision.policyDenials).toContainEqual(
140
- expect.objectContaining({
141
- code: "api_key_blocked_by_subscription_first_policy",
142
- }),
143
- );
144
- });
145
-
146
- it("falls through exhausted subscription to API-key only when allowApiKeyFallback is true", () => {
147
- const candidates = [
148
- candidate({
149
- providerId: "claude",
150
- modelId: "sonnet",
151
- authTier: "subscription",
152
- subscriptionStatus: "exhausted",
153
- health: "healthy",
154
- }),
155
- candidate({
156
- providerId: "openrouter",
157
- modelId: "fast",
158
- authTier: "api-key",
159
- authScope: "third-party",
160
- estimatedCostUsd: 0.01,
161
- }),
162
- ];
163
-
164
- const allowed = routeRequest(
165
- baseRequest({
166
- constraints: { subscriptionFirst: true, allowApiKeyFallback: true },
167
- }),
168
- candidates,
169
- personalConfig,
170
- );
171
- expect(allowed.route).toEqual(
172
- expect.objectContaining({
173
- provider: "openrouter",
174
- model: "fast",
175
- authTier: "api-key",
176
- }),
177
- );
178
- expect(allowed.fallbackChain?.[0]?.reason).toBe("api_key_fallback");
179
- expect(allowed.explanation.toLowerCase()).toMatch(/api-?key|fallback/);
180
-
181
- const blocked = routeRequest(
182
- baseRequest({
183
- constraints: { subscriptionFirst: true, allowApiKeyFallback: false },
184
- }),
185
- candidates,
186
- personalConfig,
187
- );
188
- expect(blocked.blocked).toBe(true);
189
- expect(blocked.route == null).toBe(true);
190
- expect(blocked.policyDenials).toContainEqual(
191
- expect.objectContaining({
192
- code: "api_key_blocked_by_subscription_first_policy",
193
- }),
194
- );
195
- });
196
-
197
- it("prefers the next healthy subscription over API-key when one subscription is degraded", () => {
198
- const decision = routeRequest(
199
- baseRequest({
200
- constraints: { subscriptionFirst: true, allowApiKeyFallback: true },
201
- }),
202
- [
203
- candidate({
204
- providerId: "claude",
205
- modelId: "opus",
206
- authTier: "subscription",
207
- subscriptionStatus: "active",
208
- health: "degraded",
209
- estimatedCostUsd: 0,
210
- }),
211
- candidate({
212
- providerId: "openrouter",
213
- modelId: "fast",
214
- authTier: "api-key",
215
- authScope: "third-party",
216
- estimatedCostUsd: 0.001,
217
- }),
218
- candidate({
219
- providerId: "codex",
220
- modelId: "default",
221
- authTier: "subscription",
222
- subscriptionStatus: "active",
223
- health: "healthy",
224
- estimatedCostUsd: 0,
225
- }),
226
- ],
227
- personalConfig,
228
- );
229
-
230
- expect(decision.route).toEqual(
231
- expect.objectContaining({
232
- provider: "codex",
233
- model: "default",
234
- authTier: "subscription",
235
- }),
236
- );
237
- expect(decision.policyDenials).toContainEqual(
238
- expect.objectContaining({
239
- code: "api_key_blocked_by_subscription_first_policy",
240
- }),
241
- );
242
- });
243
-
244
- it("prefers local runtime over degraded subscription before API-key fallback", () => {
245
- const decision = routeRequest(
246
- baseRequest({
247
- constraints: { subscriptionFirst: true, allowApiKeyFallback: true },
248
- }),
249
- [
250
- candidate({
251
- providerId: "claude",
252
- modelId: "sonnet",
253
- authTier: "subscription",
254
- subscriptionStatus: "active",
255
- health: "degraded",
256
- estimatedCostUsd: 0,
257
- }),
258
- candidate({
259
- providerId: "ollama",
260
- modelId: "local-fast",
261
- authTier: "local",
262
- estimatedCostUsd: 0,
263
- }),
264
- candidate({
265
- providerId: "openrouter",
266
- modelId: "fast",
267
- authTier: "api-key",
268
- authScope: "third-party",
269
- estimatedCostUsd: 0.001,
270
- }),
271
- ],
272
- personalConfig,
273
- );
274
-
275
- expect(decision.route).toEqual(
276
- expect.objectContaining({
277
- provider: "ollama",
278
- model: "local-fast",
279
- authTier: "local",
280
- }),
281
- );
282
- expect(decision.policyDenials).not.toContainEqual(
283
- expect.objectContaining({
284
- code: "api_key_blocked_by_subscription_first_policy",
285
- }),
286
- );
287
- });
288
-
289
- it("denies API-key with api_key_blocked_by_subscription_first_policy when subscription-first applies", () => {
290
- const decision = routeRequest(
291
- baseRequest({
292
- constraints: { subscriptionFirst: true, allowApiKeyFallback: true },
293
- }),
294
- [
295
- candidate({
296
- providerId: "claude",
297
- modelId: "sonnet",
298
- authTier: "subscription",
299
- subscriptionStatus: "active",
300
- health: "healthy",
301
- }),
302
- candidate({
303
- providerId: "openrouter",
304
- modelId: "fast",
305
- authTier: "api-key",
306
- authScope: "third-party",
307
- }),
308
- ],
309
- personalConfig,
310
- );
311
-
312
- expect(decision.policyDenials).toContainEqual(
313
- expect.objectContaining({
314
- code: "api_key_blocked_by_subscription_first_policy",
315
- id: "openrouter:fast",
316
- }),
317
- );
318
- });
319
-
320
- it("excludes candidates without tool support for tool tasks", () => {
321
- const decision = routeRequest(
322
- baseRequest({
323
- task: { kind: "tools", risk: "medium", requiresTools: true },
324
- }),
325
- [
326
- candidate({
327
- providerId: "text-only",
328
- modelId: "chat",
329
- authTier: "api-key",
330
- supportsTools: false,
331
- estimatedCostUsd: 0.001,
332
- }),
333
- candidate({
334
- providerId: "toolful",
335
- modelId: "agent",
336
- authTier: "api-key",
337
- supportsTools: true,
338
- estimatedCostUsd: 0.02,
339
- }),
340
- ],
341
- personalConfig,
342
- );
343
-
344
- expect(decision.route).toEqual(
345
- expect.objectContaining({
346
- provider: "toolful",
347
- model: "agent",
348
- }),
349
- );
350
- expect(decision.policyDenials).toContainEqual(
351
- expect.objectContaining({
352
- code: "capability_missing",
353
- id: "text-only:chat",
354
- }),
355
- );
356
- });
357
-
358
- it("excludes text-only models for image tasks", () => {
359
- const decision = routeRequest(
360
- baseRequest({
361
- task: { kind: "image", risk: "low", requiresImages: true },
362
- }),
363
- [
364
- candidate({
365
- providerId: "text",
366
- modelId: "no-vision",
367
- authTier: "api-key",
368
- supportsImages: false,
369
- estimatedCostUsd: 0.001,
370
- }),
371
- candidate({
372
- providerId: "vision",
373
- modelId: "see",
374
- authTier: "api-key",
375
- supportsImages: true,
376
- estimatedCostUsd: 0.05,
377
- }),
378
- ],
379
- personalConfig,
380
- );
381
-
382
- expect(decision.route).toEqual(
383
- expect.objectContaining({
384
- provider: "vision",
385
- model: "see",
386
- }),
387
- );
388
- expect(decision.policyDenials).toContainEqual(
389
- expect.objectContaining({
390
- code: "capability_missing",
391
- id: "text:no-vision",
392
- }),
393
- );
394
- });
395
-
396
- it("returns a blocked decision for a denied work profile", () => {
397
- const decision = routeRequest(
398
- baseRequest({ harness: { profile: "claude-work" } }),
399
- [
400
- candidate({
401
- providerId: "claude",
402
- modelId: "sonnet",
403
- authTier: "subscription",
404
- subscriptionStatus: "active",
405
- }),
406
- ],
407
- personalConfig,
408
- );
409
-
410
- expect(decision.blocked).toBe(true);
411
- expect(decision.route == null).toBe(true);
412
- expect(decision.needsConfiguration).toBeFalsy();
413
- expect(decision.policyDenials).toContainEqual(
414
- expect.objectContaining({
415
- code: "work_profile_denied",
416
- id: "claude-work",
417
- }),
418
- );
419
- expect(decision.explanation.toLowerCase()).toMatch(/work|denied|blocked/);
420
- });
421
-
422
- it("returns a blocked decision for an untrusted workspace", () => {
423
- const decision = routeRequest(
424
- baseRequest({ workspace: { trusted: false } }),
425
- [
426
- candidate({
427
- providerId: "claude",
428
- modelId: "sonnet",
429
- authTier: "subscription",
430
- subscriptionStatus: "active",
431
- }),
432
- ],
433
- personalConfig,
434
- );
435
-
436
- expect(decision.blocked).toBe(true);
437
- expect(decision.route == null).toBe(true);
438
- expect(decision.needsConfiguration).toBe(false);
439
- expect(decision.policyDenials).toContainEqual(
440
- expect.objectContaining({
441
- code: "project_untrusted",
442
- }),
443
- );
444
- });
445
-
446
- it("returns needsConfiguration when no candidate is available", () => {
447
- const decision = routeRequest(baseRequest(), [], personalConfig);
448
-
449
- expect(decision.blocked).toBe(true);
450
- expect(decision.needsConfiguration).toBe(true);
451
- expect(decision.route == null).toBe(true);
452
- expect(decision.reason).toMatch(/configuration|no_candidate|none/i);
453
- expect(decision.explanation.toLowerCase()).toMatch(
454
- /configur|no candidate|provider/,
455
- );
456
- });
457
-
458
- it("returns needsConfiguration for an unallowlisted personal profile when no candidate is available", () => {
459
- const decision = routeRequest(
460
- baseRequest({ harness: { profile: "cursor-personal" } }),
461
- [],
462
- personalConfig,
463
- );
464
-
465
- expect(decision.blocked).toBe(true);
466
- expect(decision.needsConfiguration).toBe(true);
467
- expect(decision.route == null).toBe(true);
468
- expect(decision.reason).toMatch(/configuration|no_candidate|none/i);
469
- expect(decision.policyDenials).toContainEqual(
470
- expect.objectContaining({
471
- code: "profile_not_allowlisted",
472
- id: "cursor-personal",
473
- }),
474
- );
475
- });
476
-
477
- it("hard-blocks a work profile even when no candidate is available", () => {
478
- const decision = routeRequest(
479
- baseRequest({ harness: { profile: "claude-work" } }),
480
- [],
481
- personalConfig,
482
- );
483
-
484
- expect(decision.blocked).toBe(true);
485
- expect(decision.needsConfiguration).toBe(false);
486
- expect(decision.route == null).toBe(true);
487
- expect(decision.policyDenials).toContainEqual(
488
- expect.objectContaining({
489
- code: "work_profile_denied",
490
- id: "claude-work",
491
- }),
492
- );
493
- });
494
-
495
- it("is deterministic for the same inputs", () => {
496
- const request = baseRequest();
497
- const candidates = [
498
- candidate({
499
- providerId: "b",
500
- modelId: "two",
501
- authTier: "api-key",
502
- estimatedCostUsd: 0.02,
503
- }),
504
- candidate({
505
- providerId: "a",
506
- modelId: "one",
507
- authTier: "api-key",
508
- estimatedCostUsd: 0.01,
509
- }),
510
- ];
511
-
512
- const a = routeRequest(request, candidates, personalConfig);
513
- const b = routeRequest(request, candidates, personalConfig);
514
-
515
- expect(a.route).toEqual(b.route);
516
- expect(a.reason).toEqual(b.reason);
517
- expect(a.fallbackChain).toEqual(b.fallbackChain);
518
- });
519
-
520
- it("keeps audit summaries digest-only and excludes prompt previews", () => {
521
- const request = baseRequest({
522
- task: {
523
- promptPreview: "raw user text that should not appear in audit",
524
- promptDigest: "sha256:redacted",
525
- },
526
- });
527
-
528
- const success = routeRequest(
529
- request,
530
- [
531
- candidate({
532
- providerId: "cheap",
533
- modelId: "fast",
534
- authTier: "api-key",
535
- estimatedCostUsd: 0.001,
536
- }),
537
- ],
538
- personalConfig,
539
- );
540
- const blocked = routeRequest(
541
- baseRequest({
542
- harness: { profile: "claude-work" },
543
- task: request.task,
544
- }),
545
- [
546
- candidate({
547
- providerId: "claude",
548
- modelId: "sonnet",
549
- authTier: "subscription",
550
- subscriptionStatus: "active",
551
- }),
552
- ],
553
- personalConfig,
554
- );
555
- const needsConfiguration = routeRequest(request, [], personalConfig);
556
-
557
- for (const decision of [success, blocked, needsConfiguration]) {
558
- expect(decision.auditSummary).toContain("sha256:redacted");
559
- expect(decision.auditSummary).not.toContain("raw user text");
560
- }
561
- });
562
-
563
- describe("multi-account routing", () => {
564
- it("selects first healthy subscription when two accounts have same rank", () => {
565
- const decision = routeRequest(
566
- baseRequest({
567
- constraints: { subscriptionFirst: true, allowApiKeyFallback: false },
568
- }),
569
- [
570
- candidate({
571
- providerId: "anthropic",
572
- modelId: "sonnet",
573
- authTier: "subscription",
574
- subscriptionStatus: "active",
575
- health: "healthy",
576
- accountId: "anthropic-1",
577
- estimatedCostUsd: 0,
578
- }),
579
- candidate({
580
- providerId: "anthropic",
581
- modelId: "sonnet",
582
- authTier: "subscription",
583
- subscriptionStatus: "active",
584
- health: "healthy",
585
- accountId: "anthropic-2",
586
- estimatedCostUsd: 0,
587
- }),
588
- ],
589
- personalConfig,
590
- );
591
-
592
- expect(decision.blocked).toBeFalsy();
593
- // Same rankKey, stable sort by input order: first candidate wins
594
- expect(decision.route?.accountId).toBe("anthropic-1");
595
- });
596
-
597
- it("skips exhausted account and picks the next healthy one", () => {
598
- const decision = routeRequest(
599
- baseRequest({
600
- constraints: { subscriptionFirst: true, allowApiKeyFallback: true },
601
- }),
602
- [
603
- candidate({
604
- providerId: "anthropic",
605
- modelId: "sonnet",
606
- authTier: "subscription",
607
- subscriptionStatus: "exhausted",
608
- health: "healthy",
609
- accountId: "anthropic-1",
610
- estimatedCostUsd: 0,
611
- }),
612
- candidate({
613
- providerId: "anthropic",
614
- modelId: "sonnet",
615
- authTier: "subscription",
616
- subscriptionStatus: "active",
617
- health: "healthy",
618
- accountId: "anthropic-2",
619
- estimatedCostUsd: 0,
620
- }),
621
- ],
622
- personalConfig,
623
- );
624
-
625
- expect(decision.blocked).toBeFalsy();
626
- expect(decision.route?.accountId).toBe("anthropic-2");
627
- });
628
-
629
- it("prefers healthy active account over near-limit account", () => {
630
- const decision = routeRequest(
631
- baseRequest({
632
- constraints: { subscriptionFirst: true, allowApiKeyFallback: false },
633
- }),
634
- [
635
- candidate({
636
- providerId: "anthropic",
637
- modelId: "sonnet",
638
- authTier: "subscription",
639
- subscriptionStatus: "near-limit",
640
- subscriptionQuotaPercentRemaining: 15,
641
- health: "healthy",
642
- accountId: "anthropic-1",
643
- estimatedCostUsd: 0,
644
- }),
645
- candidate({
646
- providerId: "anthropic",
647
- modelId: "sonnet",
648
- authTier: "subscription",
649
- subscriptionStatus: "active",
650
- health: "healthy",
651
- accountId: "anthropic-2",
652
- estimatedCostUsd: 0,
653
- }),
654
- ],
655
- personalConfig,
656
- );
657
-
658
- expect(decision.blocked).toBeFalsy();
659
- expect(decision.route?.accountId).toBe("anthropic-2");
660
- expect(decision.nearLimitWarning).toBeUndefined();
661
- });
662
-
663
- it("selects near-limit subscription when no healthy alternative exists and warns", () => {
664
- const decision = routeRequest(
665
- baseRequest({
666
- constraints: { subscriptionFirst: true, allowApiKeyFallback: true },
667
- }),
668
- [
669
- candidate({
670
- providerId: "anthropic",
671
- modelId: "sonnet",
672
- authTier: "subscription",
673
- subscriptionStatus: "near-limit",
674
- subscriptionQuotaPercentRemaining: 15,
675
- health: "healthy",
676
- accountId: "anthropic-1",
677
- estimatedCostUsd: 0,
678
- }),
679
- candidate({
680
- providerId: "openrouter",
681
- modelId: "fast",
682
- authTier: "api-key",
683
- authScope: "third-party",
684
- estimatedCostUsd: 0.01,
685
- }),
686
- ],
687
- personalConfig,
688
- );
689
-
690
- expect(decision.blocked).toBeFalsy();
691
- expect(decision.route?.accountId).toBe("anthropic-1");
692
- expect(decision.nearLimitWarning).toBeTruthy();
693
- expect(decision.nearLimitWarning).toContain("near its limit");
694
- expect(decision.explanation).toContain("near its limit");
695
- expect(decision.route?.provider).toBe("anthropic");
696
- expect(decision.route?.model).toBe("sonnet");
697
- });
698
- });
699
- });