offrouter-core 0.0.0 → 0.2.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.
- package/package.json +3 -3
- package/src/auth/credential-chain.test.ts +124 -0
- package/src/auth/credential-chain.ts +60 -6
- package/src/auth/index.ts +14 -1
- package/src/auth/keychain.test.ts +191 -1
- package/src/auth/keychain.ts +131 -2
- package/src/auth/oauth-pkce.test.ts +147 -2
- package/src/auth/oauth-pkce.ts +175 -13
- package/src/auth/oauth-server.test.ts +83 -0
- package/src/auth/oauth-server.ts +296 -0
- package/src/config.test.ts +66 -2
- package/src/config.ts +22 -1
- package/src/index.ts +20 -1
- package/src/policy.ts +2 -0
- package/src/providers/adapter.ts +4 -0
- package/src/router.test.ts +137 -0
- package/src/router.ts +34 -5
- package/src/schemas.test.ts +51 -0
- package/src/schemas.ts +5 -0
- package/src/types.ts +6 -0
- package/src/usage-file.test.ts +243 -0
- package/src/usage-file.ts +435 -0
- package/src/usage.test.ts +52 -0
- package/src/usage.ts +190 -0
package/src/router.ts
CHANGED
|
@@ -23,17 +23,18 @@ function candidateId(c: ProviderCandidate): string {
|
|
|
23
23
|
/**
|
|
24
24
|
* Rank score: higher is better.
|
|
25
25
|
* Order of preference:
|
|
26
|
-
* 1. Healthy subscription (
|
|
27
|
-
* 2.
|
|
28
|
-
* 3.
|
|
29
|
-
* 4.
|
|
26
|
+
* 1. Healthy active subscription (status !== "near-limit")
|
|
27
|
+
* 2. Healthy near-limit subscription
|
|
28
|
+
* 3. Local
|
|
29
|
+
* 4. Degraded subscription (active + degraded)
|
|
30
|
+
* 5. API-key
|
|
30
31
|
* Within same tier: lower estimatedCostUsd, then providerId, then modelId.
|
|
31
32
|
*/
|
|
32
33
|
function rankKey(c: ProviderCandidate): [number, number, string, string] {
|
|
33
34
|
let tierRank: number;
|
|
34
35
|
if (c.authTier === "subscription") {
|
|
35
36
|
if (c.health === "healthy") {
|
|
36
|
-
tierRank = 400;
|
|
37
|
+
tierRank = c.subscriptionStatus === "near-limit" ? 350 : 400;
|
|
37
38
|
} else if (c.health === "degraded") {
|
|
38
39
|
tierRank = 200;
|
|
39
40
|
} else {
|
|
@@ -298,6 +299,32 @@ export function routeRequest(
|
|
|
298
299
|
fallbackChain[0] = { ...fallbackChain[0], reason: finalFallback };
|
|
299
300
|
}
|
|
300
301
|
|
|
302
|
+
// Compute near-limit warning
|
|
303
|
+
let nearLimitWarning: string | undefined;
|
|
304
|
+
if (
|
|
305
|
+
winner.authTier === "subscription" &&
|
|
306
|
+
winner.subscriptionStatus === "near-limit"
|
|
307
|
+
) {
|
|
308
|
+
const hasHealthyAlt = sorted.some(
|
|
309
|
+
(c) =>
|
|
310
|
+
c !== winner &&
|
|
311
|
+
c.authTier === "subscription" &&
|
|
312
|
+
c.health === "healthy" &&
|
|
313
|
+
c.subscriptionStatus !== "near-limit",
|
|
314
|
+
);
|
|
315
|
+
if (!hasHealthyAlt) {
|
|
316
|
+
const acct = winner.accountId ?? winner.providerId;
|
|
317
|
+
const pct = winner.subscriptionQuotaPercentRemaining;
|
|
318
|
+
const pctText =
|
|
319
|
+
pct !== undefined ? ` (${Math.round(pct)}% remaining)` : "";
|
|
320
|
+
nearLimitWarning = `Your ${winner.providerId} subscription (${acct}) is near its limit${pctText}. Consider switching or adding another account.`;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
if (nearLimitWarning) {
|
|
325
|
+
finalExplanation = `${finalExplanation} ${nearLimitWarning}`;
|
|
326
|
+
}
|
|
327
|
+
|
|
301
328
|
return {
|
|
302
329
|
protocolVersion: ROUTE_PROTOCOL_VERSION,
|
|
303
330
|
decisionId,
|
|
@@ -307,6 +334,7 @@ export function routeRequest(
|
|
|
307
334
|
model: winner.modelId,
|
|
308
335
|
authTier: winner.authTier,
|
|
309
336
|
authScope: winner.authScope,
|
|
337
|
+
accountId: winner.accountId ?? winner.providerId,
|
|
310
338
|
},
|
|
311
339
|
reason: finalReason,
|
|
312
340
|
explanation: finalExplanation,
|
|
@@ -316,6 +344,7 @@ export function routeRequest(
|
|
|
316
344
|
auditSummary: `route=${winner.providerId}/${winner.modelId} tier=${winner.authTier} profile=${request.harness.profile} digest=${request.task.promptDigest}`,
|
|
317
345
|
blocked: false,
|
|
318
346
|
needsConfiguration: false,
|
|
347
|
+
nearLimitWarning,
|
|
319
348
|
};
|
|
320
349
|
}
|
|
321
350
|
|
package/src/schemas.test.ts
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
ProviderHealthSchema,
|
|
9
9
|
RouteDecisionSchema,
|
|
10
10
|
RouteRequestSchema,
|
|
11
|
+
SelectedRouteSchema,
|
|
11
12
|
SubscriptionStatusSchema,
|
|
12
13
|
} from "./schemas.js";
|
|
13
14
|
import { ROUTE_PROTOCOL_VERSION } from "./protocol.js";
|
|
@@ -28,6 +29,7 @@ describe("routing vocabulary schemas", () => {
|
|
|
28
29
|
it("accepts all SubscriptionStatus values", () => {
|
|
29
30
|
for (const status of [
|
|
30
31
|
"active",
|
|
32
|
+
"near-limit",
|
|
31
33
|
"exhausted",
|
|
32
34
|
"rate-limited",
|
|
33
35
|
"expired",
|
|
@@ -237,4 +239,53 @@ describe("routing schemas", () => {
|
|
|
237
239
|
}),
|
|
238
240
|
).toThrow();
|
|
239
241
|
});
|
|
242
|
+
|
|
243
|
+
describe("multi-account schema fields", () => {
|
|
244
|
+
it("accepts ProviderCandidate with accountId and subscriptionQuotaPercentRemaining", () => {
|
|
245
|
+
const candidate = ProviderCandidateSchema.parse({
|
|
246
|
+
providerId: "anthropic",
|
|
247
|
+
modelId: "sonnet",
|
|
248
|
+
authTier: "subscription",
|
|
249
|
+
authScope: "first-party",
|
|
250
|
+
subscriptionStatus: "active",
|
|
251
|
+
accountId: "anthropic-1",
|
|
252
|
+
subscriptionQuotaPercentRemaining: 15,
|
|
253
|
+
health: "healthy",
|
|
254
|
+
supportsTools: true,
|
|
255
|
+
supportsStreaming: true,
|
|
256
|
+
supportsJson: true,
|
|
257
|
+
supportsImages: false,
|
|
258
|
+
});
|
|
259
|
+
expect(candidate.accountId).toBe("anthropic-1");
|
|
260
|
+
expect(candidate.subscriptionQuotaPercentRemaining).toBe(15);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it("accepts near-limit SubscriptionStatus", () => {
|
|
264
|
+
expect(SubscriptionStatusSchema.parse("near-limit")).toBe("near-limit");
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it("accepts RouteDecision with nearLimitWarning", () => {
|
|
268
|
+
const decision = RouteDecisionSchema.parse({
|
|
269
|
+
protocolVersion: "offrouter.route.v1",
|
|
270
|
+
decisionId: "dec_nl",
|
|
271
|
+
requestId: "req_nl",
|
|
272
|
+
route: { provider: "anthropic", model: "sonnet", authTier: "subscription" },
|
|
273
|
+
reason: "subscription_primary",
|
|
274
|
+
explanation: "Selected near-limit subscription.",
|
|
275
|
+
nearLimitWarning:
|
|
276
|
+
"Your anthropic subscription (anthropic-1) is near its limit (15% remaining). Consider switching or adding another account.",
|
|
277
|
+
});
|
|
278
|
+
expect(decision.nearLimitWarning).toContain("near its limit");
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it("accepts SelectedRoute with accountId", () => {
|
|
282
|
+
const route = {
|
|
283
|
+
provider: "anthropic",
|
|
284
|
+
model: "sonnet",
|
|
285
|
+
authTier: "subscription",
|
|
286
|
+
accountId: "anthropic-1",
|
|
287
|
+
};
|
|
288
|
+
expect(() => SelectedRouteSchema.parse(route)).not.toThrow();
|
|
289
|
+
});
|
|
290
|
+
});
|
|
240
291
|
});
|
package/src/schemas.ts
CHANGED
|
@@ -15,6 +15,7 @@ export const AuthScopeSchema = z.enum([
|
|
|
15
15
|
|
|
16
16
|
export const SubscriptionStatusSchema = z.enum([
|
|
17
17
|
"active",
|
|
18
|
+
"near-limit",
|
|
18
19
|
"exhausted",
|
|
19
20
|
"rate-limited",
|
|
20
21
|
"expired",
|
|
@@ -98,6 +99,8 @@ export const ProviderCandidateSchema = z
|
|
|
98
99
|
authScope: AuthScopeSchema,
|
|
99
100
|
subscriptionStatus: SubscriptionStatusSchema.optional(),
|
|
100
101
|
health: ProviderHealthSchema,
|
|
102
|
+
accountId: z.string().min(1).optional(),
|
|
103
|
+
subscriptionQuotaPercentRemaining: z.number().min(0).max(100).optional(),
|
|
101
104
|
supportsTools: z.boolean(),
|
|
102
105
|
supportsStreaming: z.boolean(),
|
|
103
106
|
supportsJson: z.boolean(),
|
|
@@ -166,6 +169,7 @@ export const SelectedRouteSchema = z
|
|
|
166
169
|
model: z.string().min(1),
|
|
167
170
|
authTier: AuthTierSchema,
|
|
168
171
|
authScope: AuthScopeSchema.optional(),
|
|
172
|
+
accountId: z.string().min(1).optional(),
|
|
169
173
|
})
|
|
170
174
|
.strict();
|
|
171
175
|
|
|
@@ -192,6 +196,7 @@ export const RouteDecisionSchema = z
|
|
|
192
196
|
auditSummary: z.string().optional(),
|
|
193
197
|
blocked: z.boolean().optional(),
|
|
194
198
|
needsConfiguration: z.boolean().optional(),
|
|
199
|
+
nearLimitWarning: z.string().optional(),
|
|
195
200
|
})
|
|
196
201
|
.strict();
|
|
197
202
|
|
package/src/types.ts
CHANGED
|
@@ -9,6 +9,7 @@ export type AuthScope = "first-party" | "third-party" | "unknown";
|
|
|
9
9
|
|
|
10
10
|
export type SubscriptionStatus =
|
|
11
11
|
| "active"
|
|
12
|
+
| "near-limit"
|
|
12
13
|
| "exhausted"
|
|
13
14
|
| "rate-limited"
|
|
14
15
|
| "expired"
|
|
@@ -73,6 +74,8 @@ export interface ProviderCandidate {
|
|
|
73
74
|
authScope: AuthScope;
|
|
74
75
|
subscriptionStatus?: SubscriptionStatus;
|
|
75
76
|
health: ProviderHealth;
|
|
77
|
+
accountId?: string;
|
|
78
|
+
subscriptionQuotaPercentRemaining?: number;
|
|
76
79
|
supportsTools: boolean;
|
|
77
80
|
supportsStreaming: boolean;
|
|
78
81
|
supportsJson: boolean;
|
|
@@ -129,6 +132,7 @@ export interface SelectedRoute {
|
|
|
129
132
|
model: string;
|
|
130
133
|
authTier: AuthTier;
|
|
131
134
|
authScope?: AuthScope;
|
|
135
|
+
accountId?: string;
|
|
132
136
|
}
|
|
133
137
|
|
|
134
138
|
export interface FallbackChainEntry {
|
|
@@ -155,6 +159,8 @@ export interface RouteDecision {
|
|
|
155
159
|
auditSummary?: string;
|
|
156
160
|
blocked?: boolean;
|
|
157
161
|
needsConfiguration?: boolean;
|
|
162
|
+
/** Warning when the selected route is a near-limit subscription with no healthy alternative. */
|
|
163
|
+
nearLimitWarning?: string;
|
|
158
164
|
}
|
|
159
165
|
|
|
160
166
|
/**
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
2
|
+
import { mkdir, mkdtemp, readFile, rm, stat, writeFile } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import {
|
|
6
|
+
DEFAULT_NEAR_LIMIT_THRESHOLD,
|
|
7
|
+
FileUsageStore,
|
|
8
|
+
InMemoryUsageStore,
|
|
9
|
+
type UsageStore,
|
|
10
|
+
} from "./usage-file.js";
|
|
11
|
+
|
|
12
|
+
const tempDirs: string[] = [];
|
|
13
|
+
|
|
14
|
+
afterEach(async () => {
|
|
15
|
+
await Promise.all(
|
|
16
|
+
tempDirs.splice(0).map((dir) => rm(dir, { recursive: true, force: true })),
|
|
17
|
+
);
|
|
18
|
+
},);
|
|
19
|
+
|
|
20
|
+
async function makeTempDir(): Promise<string> {
|
|
21
|
+
const dir = await mkdtemp(join(tmpdir(), "offrouter-usage-file-"));
|
|
22
|
+
tempDirs.push(dir);
|
|
23
|
+
return dir;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function makeFileStore(
|
|
27
|
+
filePath?: string,
|
|
28
|
+
): Promise<{ dir: string; path: string; store: FileUsageStore }> {
|
|
29
|
+
const dir = await makeTempDir();
|
|
30
|
+
const path = filePath ?? join(dir, "state", "usage.json");
|
|
31
|
+
const store = new FileUsageStore({ filePath: path });
|
|
32
|
+
return { dir, path, store };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
describe("FileUsageStore", () => {
|
|
36
|
+
it("satisfies the UsageStore interface contract", () => {
|
|
37
|
+
const store: UsageStore = new FileUsageStore({
|
|
38
|
+
filePath: "/tmp/offrouter-usage-store-interface.json",
|
|
39
|
+
});
|
|
40
|
+
expect(typeof store.recordUsage).toBe("function");
|
|
41
|
+
expect(typeof store.isNearLimit).toBe("function");
|
|
42
|
+
expect(typeof store.record).toBe("function");
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("persists account usage across store instances sharing the same file", async () => {
|
|
46
|
+
const { path, store } = await makeFileStore();
|
|
47
|
+
await store.recordUsage("anthropic", "anthropic-1", {
|
|
48
|
+
used: 42,
|
|
49
|
+
limit: 100,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const reloaded = new FileUsageStore({ filePath: path });
|
|
53
|
+
const snap = await reloaded.getAccountUsage("anthropic", "anthropic-1");
|
|
54
|
+
expect(snap?.used).toBe(42);
|
|
55
|
+
expect(snap?.limit).toBe(100);
|
|
56
|
+
expect(snap?.remaining).toBe(58);
|
|
57
|
+
|
|
58
|
+
// A fresh instance also sees near-limit state.
|
|
59
|
+
expect(await reloaded.isNearLimit("anthropic", "anthropic-1")).toBe(false);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("marks an account near-limit at 85% consumed (threshold 0.2)", async () => {
|
|
63
|
+
const { store } = await makeFileStore();
|
|
64
|
+
const snap = await store.recordUsage("anthropic", "anthropic-1", {
|
|
65
|
+
used: 85,
|
|
66
|
+
limit: 100,
|
|
67
|
+
});
|
|
68
|
+
expect(snap.remaining).toBe(15);
|
|
69
|
+
expect(snap.percentRemaining).toBe(15);
|
|
70
|
+
expect(snap.nearLimit).toBe(true);
|
|
71
|
+
expect(snap.subscriptionStatus).toBe("near-limit");
|
|
72
|
+
expect(await store.isNearLimit("anthropic", "anthropic-1")).toBe(true);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("does not mark an account near-limit at 50% consumed", async () => {
|
|
76
|
+
const { store } = await makeFileStore();
|
|
77
|
+
const snap = await store.recordUsage("anthropic", "anthropic-1", {
|
|
78
|
+
used: 50,
|
|
79
|
+
limit: 100,
|
|
80
|
+
});
|
|
81
|
+
expect(snap.nearLimit).toBe(false);
|
|
82
|
+
expect(snap.subscriptionStatus).toBe("active");
|
|
83
|
+
expect(await store.isNearLimit("anthropic", "anthropic-1")).toBe(false);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("respects a custom near-limit threshold", async () => {
|
|
87
|
+
const { store } = await makeFileStore();
|
|
88
|
+
await store.recordUsage("anthropic", "anthropic-1", {
|
|
89
|
+
used: 85,
|
|
90
|
+
limit: 100,
|
|
91
|
+
});
|
|
92
|
+
// 15% remaining: near-limit at 0.2 default, not at stricter 0.1.
|
|
93
|
+
expect(await store.isNearLimit("anthropic", "anthropic-1")).toBe(true);
|
|
94
|
+
expect(await store.isNearLimit("anthropic", "anthropic-1", 0.1)).toBe(false);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("marks an account exhausted at 100% consumed with dead health", async () => {
|
|
98
|
+
const { store } = await makeFileStore();
|
|
99
|
+
const snap = await store.recordUsage("anthropic", "anthropic-1", {
|
|
100
|
+
used: 100,
|
|
101
|
+
limit: 100,
|
|
102
|
+
});
|
|
103
|
+
expect(snap.subscriptionStatus).toBe("exhausted");
|
|
104
|
+
expect(snap.nearLimit).toBe(false);
|
|
105
|
+
expect(snap.health).toBe("dead");
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("writes the usage file with mode 0600 on POSIX", async () => {
|
|
109
|
+
if (process.platform === "win32") {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
const { path, store } = await makeFileStore();
|
|
113
|
+
await store.recordUsage("anthropic", "anthropic-1", {
|
|
114
|
+
used: 1,
|
|
115
|
+
limit: 100,
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
const info = await stat(path);
|
|
119
|
+
expect(info.mode & 0o777).toBe(0o600);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("falls back to an empty state on a corrupt file instead of crashing", async () => {
|
|
123
|
+
const dir = await makeTempDir();
|
|
124
|
+
const path = join(dir, "state", "usage.json");
|
|
125
|
+
await mkdir(join(dir, "state"), { recursive: true });
|
|
126
|
+
await writeFile(path, "{not valid json at all", "utf8");
|
|
127
|
+
|
|
128
|
+
const store = new FileUsageStore({ filePath: path });
|
|
129
|
+
// Reads do not throw; state is empty.
|
|
130
|
+
const snap = await store.getAccountUsage("anthropic", "anthropic-1");
|
|
131
|
+
expect(snap).toBeUndefined();
|
|
132
|
+
expect(await store.listAccountUsage()).toEqual([]);
|
|
133
|
+
|
|
134
|
+
// A subsequent write replaces the corrupt file with a valid document.
|
|
135
|
+
await store.recordUsage("anthropic", "anthropic-1", {
|
|
136
|
+
used: 90,
|
|
137
|
+
limit: 100,
|
|
138
|
+
});
|
|
139
|
+
const raw = JSON.parse(await readFile(path, "utf8")) as {
|
|
140
|
+
version: number;
|
|
141
|
+
accounts: Record<string, unknown>;
|
|
142
|
+
};
|
|
143
|
+
expect(raw.version).toBe(1);
|
|
144
|
+
expect(Object.keys(raw.accounts)).toEqual(["anthropic:anthropic-1"]);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("concurrent writes from two stores on the same file do not corrupt it", async () => {
|
|
148
|
+
const dir = await makeTempDir();
|
|
149
|
+
const path = join(dir, "state", "usage.json");
|
|
150
|
+
const s1 = new FileUsageStore({ filePath: path });
|
|
151
|
+
const s2 = new FileUsageStore({ filePath: path });
|
|
152
|
+
|
|
153
|
+
await Promise.all([
|
|
154
|
+
s1.recordUsage("anthropic", "anthropic-1", { used: 80, limit: 100 }),
|
|
155
|
+
s2.recordUsage("openai", "openai-1", { used: 90, limit: 100 }),
|
|
156
|
+
]);
|
|
157
|
+
|
|
158
|
+
// The file is always valid JSON with the expected schema.
|
|
159
|
+
const raw = JSON.parse(await readFile(path, "utf8")) as {
|
|
160
|
+
version: number;
|
|
161
|
+
accounts: Record<string, { providerId: string }>;
|
|
162
|
+
};
|
|
163
|
+
expect(raw.version).toBe(1);
|
|
164
|
+
// Last-writer-wins is acceptable; corruption (invalid JSON) is not.
|
|
165
|
+
for (const doc of Object.values(raw.accounts)) {
|
|
166
|
+
expect(typeof doc.providerId).toBe("string");
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it("writes on every mutation so a new process sees the latest usage", async () => {
|
|
171
|
+
const { path, store } = await makeFileStore();
|
|
172
|
+
await store.recordUsage("anthropic", "anthropic-1", {
|
|
173
|
+
used: 10,
|
|
174
|
+
limit: 100,
|
|
175
|
+
});
|
|
176
|
+
await store.recordUsage("anthropic", "anthropic-1", {
|
|
177
|
+
used: 88,
|
|
178
|
+
limit: 100,
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
const reloaded = new FileUsageStore({ filePath: path });
|
|
182
|
+
const snap = await reloaded.getAccountUsage("anthropic", "anthropic-1");
|
|
183
|
+
expect(snap?.used).toBe(88);
|
|
184
|
+
expect(snap?.nearLimit).toBe(true);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it("delegates provider health/quota logic to an in-memory store", async () => {
|
|
188
|
+
const { store } = await makeFileStore();
|
|
189
|
+
await store.setSubscriptionQuota("claude", {
|
|
190
|
+
limit: 10,
|
|
191
|
+
used: 9,
|
|
192
|
+
status: "active",
|
|
193
|
+
});
|
|
194
|
+
const snap = await store.getProvider("claude");
|
|
195
|
+
expect(snap?.subscriptionQuotaRemaining).toBe(1);
|
|
196
|
+
expect(snap?.health).toBe("healthy");
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it("returns near-limit accounts across the persisted set", async () => {
|
|
200
|
+
const { store } = await makeFileStore();
|
|
201
|
+
await store.recordUsage("anthropic", "anthropic-1", {
|
|
202
|
+
used: 85,
|
|
203
|
+
limit: 100,
|
|
204
|
+
});
|
|
205
|
+
await store.recordUsage("anthropic", "anthropic-2", {
|
|
206
|
+
used: 10,
|
|
207
|
+
limit: 100,
|
|
208
|
+
});
|
|
209
|
+
const near = await store.getNearLimitAccounts();
|
|
210
|
+
expect(near).toHaveLength(1);
|
|
211
|
+
expect(near[0]?.accountId).toBe("anthropic-1");
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it("toJSON exposes diagnostics without throwing", async () => {
|
|
215
|
+
const { store } = await makeFileStore();
|
|
216
|
+
await store.recordUsage("anthropic", "anthropic-1", {
|
|
217
|
+
used: 85,
|
|
218
|
+
limit: 100,
|
|
219
|
+
});
|
|
220
|
+
const json = store.toJSON();
|
|
221
|
+
expect(json).toMatchObject({ kind: "file-usage" });
|
|
222
|
+
expect(JSON.stringify(json)).not.toContain("anthropic-1");
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it("uses the default near-limit threshold constant", () => {
|
|
226
|
+
expect(DEFAULT_NEAR_LIMIT_THRESHOLD).toBe(0.2);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it("accepts an injected in-memory delegate for provider state", async () => {
|
|
230
|
+
const dir = await makeTempDir();
|
|
231
|
+
const path = join(dir, "state", "usage.json");
|
|
232
|
+
const delegate = new InMemoryUsageStore();
|
|
233
|
+
const wired = new FileUsageStore({ filePath: path, delegate });
|
|
234
|
+
await wired.setSubscriptionQuota("claude", {
|
|
235
|
+
limit: 5,
|
|
236
|
+
used: 1,
|
|
237
|
+
status: "active",
|
|
238
|
+
});
|
|
239
|
+
// Provider state lives on the injected delegate.
|
|
240
|
+
const snap = await delegate.getProvider("claude");
|
|
241
|
+
expect(snap?.subscriptionQuotaRemaining).toBe(4);
|
|
242
|
+
});
|
|
243
|
+
});
|