offrouter-core 0.0.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.
Files changed (41) hide show
  1. package/package.json +21 -0
  2. package/src/audit.test.ts +302 -0
  3. package/src/audit.ts +553 -0
  4. package/src/auth/credential-chain.test.ts +202 -0
  5. package/src/auth/credential-chain.ts +181 -0
  6. package/src/auth/index.ts +32 -0
  7. package/src/auth/keychain.test.ts +75 -0
  8. package/src/auth/keychain.ts +39 -0
  9. package/src/auth/oauth-pkce.test.ts +184 -0
  10. package/src/auth/oauth-pkce.ts +409 -0
  11. package/src/config.test.ts +415 -0
  12. package/src/config.ts +484 -0
  13. package/src/delegation.test.ts +368 -0
  14. package/src/delegation.ts +605 -0
  15. package/src/index.ts +261 -0
  16. package/src/policy.test.ts +634 -0
  17. package/src/policy.ts +418 -0
  18. package/src/protocol.ts +7 -0
  19. package/src/providers/adapter.test.ts +293 -0
  20. package/src/providers/adapter.ts +324 -0
  21. package/src/providers/catalog-data.test.ts +258 -0
  22. package/src/providers/catalog-data.ts +498 -0
  23. package/src/providers/catalog.test.ts +84 -0
  24. package/src/providers/catalog.ts +483 -0
  25. package/src/providers/fake.ts +312 -0
  26. package/src/providers/openai-compatible.test.ts +366 -0
  27. package/src/providers/openai-compatible.ts +554 -0
  28. package/src/proxy/responses-mapper.test.ts +290 -0
  29. package/src/proxy/responses-mapper.ts +736 -0
  30. package/src/proxy/responses-server.test.ts +322 -0
  31. package/src/proxy/responses-server.ts +469 -0
  32. package/src/router.test.ts +562 -0
  33. package/src/router.ts +323 -0
  34. package/src/schemas.test.ts +240 -0
  35. package/src/schemas.ts +203 -0
  36. package/src/secrets.test.ts +271 -0
  37. package/src/secrets.ts +461 -0
  38. package/src/types.ts +167 -0
  39. package/src/usage.test.ts +283 -0
  40. package/src/usage.ts +669 -0
  41. package/tsconfig.json +9 -0
@@ -0,0 +1,634 @@
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
+ });