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,562 @@
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
+ });