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,283 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ InMemoryStateStore,
4
+ InMemoryUsageStore,
5
+ type UsageEvent,
6
+ } from "./usage.js";
7
+
8
+ function err(
9
+ providerId: string,
10
+ kind: UsageEvent["kind"] = "request_error",
11
+ at?: Date,
12
+ ): UsageEvent {
13
+ return {
14
+ providerId,
15
+ authTier: "subscription",
16
+ kind,
17
+ at,
18
+ };
19
+ }
20
+
21
+ describe("InMemoryUsageStore", () => {
22
+ it("applies concurrent usage updates without losing counters", async () => {
23
+ const store = new InMemoryUsageStore();
24
+ const providerId = "claude";
25
+
26
+ await Promise.all(
27
+ Array.from({ length: 50 }, () =>
28
+ store.record({
29
+ providerId,
30
+ authTier: "subscription",
31
+ kind: "subscription_usage",
32
+ amount: 1,
33
+ }),
34
+ ),
35
+ );
36
+
37
+ const snap = await store.getProvider(providerId);
38
+ expect(snap?.subscriptionQuotaUsed).toBe(50);
39
+ expect(snap?.successCount).toBe(50);
40
+ });
41
+
42
+ it("tracks quota remaining for subscription capacity", async () => {
43
+ const store = new InMemoryUsageStore();
44
+ await store.setSubscriptionQuota("claude", {
45
+ limit: 100,
46
+ used: 40,
47
+ status: "active",
48
+ });
49
+
50
+ let snap = await store.getProvider("claude");
51
+ expect(snap?.subscriptionQuotaLimit).toBe(100);
52
+ expect(snap?.subscriptionQuotaUsed).toBe(40);
53
+ expect(snap?.subscriptionQuotaRemaining).toBe(60);
54
+ expect(snap?.subscriptionStatus).toBe("active");
55
+ expect(snap?.health).toBe("healthy");
56
+
57
+ await store.record({
58
+ providerId: "claude",
59
+ authTier: "subscription",
60
+ kind: "subscription_usage",
61
+ amount: 25,
62
+ });
63
+
64
+ snap = await store.getProvider("claude");
65
+ expect(snap?.subscriptionQuotaUsed).toBe(65);
66
+ expect(snap?.subscriptionQuotaRemaining).toBe(35);
67
+ });
68
+
69
+ it("downgrades provider health via sliding-window error budget and cooldown", async () => {
70
+ const t0 = Date.UTC(2026, 0, 1, 12, 0, 0);
71
+ let nowMs = t0;
72
+ const store = new InMemoryUsageStore({
73
+ windowMs: 10_000,
74
+ healthyMaxErrors: 2,
75
+ deadMinErrors: 5,
76
+ cooldownMs: 30_000,
77
+ now: () => new Date(nowMs),
78
+ });
79
+
80
+ // 0-2 errors remain healthy
81
+ await store.record(err("openai", "request_error", new Date(nowMs)));
82
+ await store.record(
83
+ err("openai", "request_error", new Date((nowMs += 10))),
84
+ );
85
+ let snap = await store.getProvider("openai");
86
+ expect(snap?.health).toBe("healthy");
87
+ expect(snap?.recentErrorCount).toBe(2);
88
+
89
+ // 3-4 => degraded
90
+ await store.record(
91
+ err("openai", "request_error", new Date((nowMs += 10))),
92
+ );
93
+ snap = await store.getProvider("openai");
94
+ expect(snap?.health).toBe("degraded");
95
+
96
+ await store.record(
97
+ err("openai", "request_error", new Date((nowMs += 10))),
98
+ );
99
+ snap = await store.getProvider("openai");
100
+ expect(snap?.health).toBe("degraded");
101
+ expect(snap?.recentErrorCount).toBe(4);
102
+
103
+ // 5 => dead + cooldown
104
+ await store.record(
105
+ err("openai", "timeout", new Date((nowMs += 10))),
106
+ );
107
+ snap = await store.getProvider("openai");
108
+ expect(snap?.health).toBe("dead");
109
+ expect(snap?.cooldownUntil).toBeGreaterThan(nowMs);
110
+ expect(snap?.healthReason).toMatch(/Error budget exhausted|Cooldown/);
111
+
112
+ // Still dead during cooldown even if window would eventually age out
113
+ nowMs += 5_000;
114
+ snap = await store.getProvider("openai");
115
+ expect(snap?.health).toBe("dead");
116
+
117
+ // After cooldown, and with aged-out errors, recovers to healthy
118
+ nowMs += 40_000;
119
+ snap = await store.getProvider("openai");
120
+ expect(snap?.health).toBe("healthy");
121
+ expect(snap?.cooldownUntil).toBeUndefined();
122
+ });
123
+
124
+ it("marks hard auth failure as dead immediately with cooldown", async () => {
125
+ const t0 = Date.UTC(2026, 0, 2, 0, 0, 0);
126
+ let nowMs = t0;
127
+ const store = new InMemoryUsageStore({
128
+ cooldownMs: 15_000,
129
+ now: () => new Date(nowMs),
130
+ });
131
+
132
+ const result = await store.record({
133
+ providerId: "gemini",
134
+ authTier: "subscription",
135
+ kind: "auth_failure",
136
+ at: new Date(nowMs),
137
+ });
138
+ expect(result.snapshot.health).toBe("dead");
139
+ expect(result.healthChanged).toBe(true);
140
+ expect(result.previousHealth).toBe("healthy");
141
+
142
+ nowMs += 20_000;
143
+ const snap = await store.getProvider("gemini");
144
+ // No recent window errors beyond the single err once pruned? still within window.
145
+ // Cooldown expired; single error is within healthy budget (default 2).
146
+ expect(snap?.health).toBe("healthy");
147
+ });
148
+
149
+ it("records subscription quota exhaustion separately from API-key spend", async () => {
150
+ const store = new InMemoryUsageStore();
151
+
152
+ await store.setSubscriptionQuota("claude", {
153
+ limit: 10,
154
+ used: 9,
155
+ status: "active",
156
+ });
157
+
158
+ // One more subscription unit exhausts.
159
+ await store.record({
160
+ providerId: "claude",
161
+ authTier: "subscription",
162
+ kind: "subscription_usage",
163
+ amount: 1,
164
+ });
165
+
166
+ let snap = await store.getProvider("claude");
167
+ expect(snap?.subscriptionStatus).toBe("exhausted");
168
+ expect(snap?.subscriptionQuotaRemaining).toBe(0);
169
+ expect(snap?.apiKeySpendUsd).toBe(0);
170
+ // Exhaustion alone is degraded, not necessarily dead.
171
+ expect(snap?.health).toBe("degraded");
172
+
173
+ // Paid fallback spend is tracked independently.
174
+ await store.record({
175
+ providerId: "claude",
176
+ authTier: "api-key",
177
+ kind: "api_key_spend",
178
+ amount: 0.03,
179
+ });
180
+ await store.record({
181
+ providerId: "claude",
182
+ authTier: "api-key",
183
+ kind: "api_key_spend",
184
+ amount: 0.02,
185
+ });
186
+
187
+ snap = await store.getProvider("claude");
188
+ expect(snap?.apiKeySpendUsd).toBeCloseTo(0.05, 8);
189
+ // Subscription counters unchanged by api-key spend.
190
+ expect(snap?.subscriptionQuotaUsed).toBe(10);
191
+ expect(snap?.subscriptionStatus).toBe("exhausted");
192
+ });
193
+
194
+ it("explains why paid fallback was or was not used", async () => {
195
+ const store = new InMemoryUsageStore();
196
+
197
+ // Healthy subscription with remaining quota => prefer subscription.
198
+ await store.setSubscriptionQuota("claude", {
199
+ limit: 100,
200
+ used: 10,
201
+ status: "active",
202
+ });
203
+ let explain = await store.explainPaidFallback("claude", {
204
+ hasApiKeyCandidate: true,
205
+ });
206
+ expect(explain.recommendation).toBe("prefer_subscription");
207
+ expect(explain.subscriptionQuotaRemaining).toBe(90);
208
+ expect(explain.usedPaidFallback).toBe(false);
209
+ expect(explain.reason).toMatch(/should not be used yet|preferred/i);
210
+
211
+ // Exhaust subscription => allow API-key fallback.
212
+ await store.setSubscriptionQuota("claude", {
213
+ limit: 100,
214
+ used: 100,
215
+ status: "exhausted",
216
+ });
217
+ explain = await store.explainPaidFallback("claude", {
218
+ hasApiKeyCandidate: true,
219
+ });
220
+ expect(explain.recommendation).toBe("allow_api_key_fallback");
221
+ expect(explain.reason).toMatch(/exhausted/i);
222
+
223
+ // After paid spend, explanation notes fallback was used.
224
+ await store.record({
225
+ providerId: "claude",
226
+ authTier: "api-key",
227
+ kind: "api_key_spend",
228
+ amount: 1.25,
229
+ });
230
+ explain = await store.explainPaidFallback("claude", {
231
+ hasApiKeyCandidate: true,
232
+ });
233
+ expect(explain.apiKeySpendUsd).toBe(1.25);
234
+ expect(explain.usedPaidFallback).toBe(true);
235
+ expect(explain.subscriptionQuotaRemaining).toBe(0);
236
+
237
+ // Distinguishes unknown / no subscription data + api-key only.
238
+ explain = await store.explainPaidFallback("unknown-provider", {
239
+ hasApiKeyCandidate: true,
240
+ });
241
+ expect(explain.recommendation).toBe("api_key_only");
242
+ });
243
+
244
+ it("toJSON never includes secrets or raw prompts", async () => {
245
+ const store = new InMemoryUsageStore();
246
+ await store.record({
247
+ providerId: "openai",
248
+ authTier: "api-key",
249
+ kind: "api_key_spend",
250
+ amount: 0.5,
251
+ detail: "sk-should-not-appear-in-json-anyway-but-caller-redacts",
252
+ });
253
+ const json = JSON.stringify(store.toJSON());
254
+ expect(json).not.toMatch(/sk-/);
255
+ expect(store.toJSON()).toMatchObject({
256
+ kind: "in-memory-usage",
257
+ providers: {
258
+ openai: {
259
+ apiKeySpendUsd: 0.5,
260
+ },
261
+ },
262
+ });
263
+ });
264
+ });
265
+
266
+ describe("InMemoryStateStore", () => {
267
+ it("composes audit + usage under one StateStore-style handle", async () => {
268
+ const state = new InMemoryStateStore();
269
+ await state.usage.setSubscriptionQuota("claude", {
270
+ limit: 5,
271
+ used: 5,
272
+ status: "exhausted",
273
+ });
274
+ const explain = await state.usage.explainPaidFallback("claude", {
275
+ hasApiKeyCandidate: true,
276
+ });
277
+ expect(explain.recommendation).toBe("allow_api_key_fallback");
278
+
279
+ const json = state.toJSON();
280
+ expect(json).toMatchObject({ kind: "in-memory-state" });
281
+ expect(JSON.stringify(json)).not.toMatch(/sk-/);
282
+ });
283
+ });