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
package/src/usage.test.ts DELETED
@@ -1,335 +0,0 @@
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
- });
284
-
285
- describe("near-limit / per-account usage", () => {
286
- it("recordUsage returns near-limit snapshot when quota is nearly consumed", async () => {
287
- const store = new InMemoryUsageStore();
288
- const snap = await store.recordUsage("anthropic", "anthropic-1", { used: 85, limit: 100 });
289
- expect(snap.remaining).toBe(15);
290
- expect(snap.percentRemaining).toBe(15);
291
- expect(snap.nearLimit).toBe(true);
292
- expect(snap.subscriptionStatus).toBe("near-limit");
293
- });
294
-
295
- it("isNearLimit respects custom threshold", async () => {
296
- const store = new InMemoryUsageStore();
297
- await store.recordUsage("anthropic", "anthropic-1", { used: 85, limit: 100 });
298
- expect(await store.isNearLimit("anthropic", "anthropic-1")).toBe(true);
299
- expect(await store.isNearLimit("anthropic", "anthropic-1", 0.1)).toBe(false);
300
- });
301
-
302
- it("recordUsage detects exhaustion", async () => {
303
- const store = new InMemoryUsageStore();
304
- const snap = await store.recordUsage("anthropic", "anthropic-2", { used: 100, limit: 100 });
305
- expect(snap.nearLimit).toBe(false);
306
- expect(snap.subscriptionStatus).toBe("exhausted");
307
- });
308
-
309
- it("getNearLimitAccounts returns only accounts near their limit", async () => {
310
- const store = new InMemoryUsageStore();
311
- await store.recordUsage("anthropic", "anthropic-1", { used: 85, limit: 100 });
312
- await store.recordUsage("anthropic", "anthropic-2", { used: 100, limit: 100 });
313
- const nearLimit = await store.getNearLimitAccounts();
314
- expect(nearLimit).toHaveLength(1);
315
- expect(nearLimit[0]!.accountId).toBe("anthropic-1");
316
- });
317
-
318
- it("getAccountUsage returns correct snapshots for different accounts", async () => {
319
- const store = new InMemoryUsageStore();
320
- await store.recordUsage("anthropic", "anthropic-1", { used: 85, limit: 100 });
321
- await store.recordUsage("anthropic", "anthropic-2", { used: 10, limit: 100 });
322
- const snap1 = await store.getAccountUsage("anthropic", "anthropic-1");
323
- const snap2 = await store.getAccountUsage("anthropic", "anthropic-2");
324
- expect(snap1?.used).toBe(85);
325
- expect(snap2?.used).toBe(10);
326
- });
327
-
328
- it("existing setSubscriptionQuota and record still behave as before", async () => {
329
- const store = new InMemoryUsageStore();
330
- await store.setSubscriptionQuota("test", { limit: 50, used: 10, status: "active" });
331
- const snap = await store.getProvider("test");
332
- expect(snap?.subscriptionQuotaLimit).toBe(50);
333
- expect(snap?.subscriptionQuotaUsed).toBe(10);
334
- });
335
- });