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/usage.ts
CHANGED
|
@@ -104,6 +104,42 @@ export interface RecordUsageResult {
|
|
|
104
104
|
previousHealth: ProviderHealth;
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
+
export const DEFAULT_NEAR_LIMIT_THRESHOLD = 0.2;
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Snapshot of usage for a specific account within a provider.
|
|
111
|
+
*/
|
|
112
|
+
export interface AccountUsageSnapshot {
|
|
113
|
+
providerId: string;
|
|
114
|
+
accountId: string;
|
|
115
|
+
used: number;
|
|
116
|
+
limit?: number;
|
|
117
|
+
remaining?: number;
|
|
118
|
+
percentRemaining?: number;
|
|
119
|
+
nearLimit: boolean;
|
|
120
|
+
subscriptionStatus: SubscriptionStatus;
|
|
121
|
+
/**
|
|
122
|
+
* Derived account health from quota usage: "dead" when exhausted,
|
|
123
|
+
* "degraded" when near-limit, otherwise "healthy". Useful for
|
|
124
|
+
* persisted stores that need a stable, restorable health signal.
|
|
125
|
+
*/
|
|
126
|
+
health?: ProviderHealth;
|
|
127
|
+
updatedAt: string;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Minimal persisted document for a single account's quota usage.
|
|
132
|
+
* Shared between the in-memory and file-backed usage stores so snapshot
|
|
133
|
+
* math stays in one place.
|
|
134
|
+
*/
|
|
135
|
+
export interface AccountUsageDoc {
|
|
136
|
+
providerId: string;
|
|
137
|
+
accountId: string;
|
|
138
|
+
used: number;
|
|
139
|
+
limit?: number;
|
|
140
|
+
updatedAt: string;
|
|
141
|
+
}
|
|
142
|
+
|
|
107
143
|
/**
|
|
108
144
|
* Why paid API-key capacity was or was not selected given current usage state.
|
|
109
145
|
* Consumed by doctor / status / route explain surfaces.
|
|
@@ -154,6 +190,35 @@ export interface UsageStore {
|
|
|
154
190
|
options?: { hasApiKeyCandidate?: boolean },
|
|
155
191
|
): Promise<PaidFallbackExplanation>;
|
|
156
192
|
|
|
193
|
+
/**
|
|
194
|
+
* Record usage for a specific account within a provider.
|
|
195
|
+
*/
|
|
196
|
+
recordUsage(
|
|
197
|
+
providerId: string,
|
|
198
|
+
accountId: string,
|
|
199
|
+
input: { used?: number; limit?: number; at?: Date },
|
|
200
|
+
): Promise<AccountUsageSnapshot>;
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Check whether a specific account is near its quota limit.
|
|
204
|
+
*/
|
|
205
|
+
isNearLimit(providerId: string, accountId: string, threshold?: number): Promise<boolean>;
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Get all accounts whose usage is near their quota limit.
|
|
209
|
+
*/
|
|
210
|
+
getNearLimitAccounts(threshold?: number): Promise<AccountUsageSnapshot[]>;
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Get the usage snapshot for a specific account.
|
|
214
|
+
*/
|
|
215
|
+
getAccountUsage(providerId: string, accountId: string): Promise<AccountUsageSnapshot | undefined>;
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* List all account usage snapshots.
|
|
219
|
+
*/
|
|
220
|
+
listAccountUsage(): Promise<AccountUsageSnapshot[]>;
|
|
221
|
+
|
|
157
222
|
/**
|
|
158
223
|
* JSON-safe diagnostic view. No secrets.
|
|
159
224
|
*/
|
|
@@ -184,6 +249,14 @@ interface InternalProviderState {
|
|
|
184
249
|
updatedAt: string;
|
|
185
250
|
}
|
|
186
251
|
|
|
252
|
+
interface AccountQuotaState {
|
|
253
|
+
providerId: string;
|
|
254
|
+
accountId: string;
|
|
255
|
+
used: number;
|
|
256
|
+
limit?: number;
|
|
257
|
+
updatedAt: string;
|
|
258
|
+
}
|
|
259
|
+
|
|
187
260
|
const DEFAULT_WINDOW_MS = 60_000;
|
|
188
261
|
const DEFAULT_HEALTHY_MAX_ERRORS = 2;
|
|
189
262
|
const DEFAULT_DEAD_MIN_ERRORS = 5;
|
|
@@ -200,6 +273,7 @@ const HARD_ERROR_KINDS: ReadonlySet<UsageEventKind> = new Set([
|
|
|
200
273
|
*/
|
|
201
274
|
export class InMemoryUsageStore implements UsageStore {
|
|
202
275
|
readonly #providers = new Map<string, InternalProviderState>();
|
|
276
|
+
readonly #accounts = new Map<string, AccountQuotaState>();
|
|
203
277
|
readonly #windowMs: number;
|
|
204
278
|
readonly #healthyMaxErrors: number;
|
|
205
279
|
readonly #deadMinErrors: number;
|
|
@@ -505,6 +579,78 @@ export class InMemoryUsageStore implements UsageStore {
|
|
|
505
579
|
};
|
|
506
580
|
}
|
|
507
581
|
|
|
582
|
+
async recordUsage(
|
|
583
|
+
providerId: string,
|
|
584
|
+
accountId: string,
|
|
585
|
+
input: { used?: number; limit?: number; at?: Date },
|
|
586
|
+
): Promise<AccountUsageSnapshot> {
|
|
587
|
+
return this.#exclusive(() => {
|
|
588
|
+
const at = input.at ?? this.#now();
|
|
589
|
+
const key = `${providerId}:${accountId}`;
|
|
590
|
+
let state = this.#accounts.get(key);
|
|
591
|
+
if (!state) {
|
|
592
|
+
state = { providerId, accountId, used: 0, updatedAt: at.toISOString() };
|
|
593
|
+
this.#accounts.set(key, state);
|
|
594
|
+
}
|
|
595
|
+
if (input.used !== undefined) state.used = input.used;
|
|
596
|
+
if (input.limit !== undefined) state.limit = input.limit;
|
|
597
|
+
state.updatedAt = at.toISOString();
|
|
598
|
+
return computeAccountSnapshot(state);
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
async isNearLimit(
|
|
603
|
+
providerId: string,
|
|
604
|
+
accountId: string,
|
|
605
|
+
threshold?: number,
|
|
606
|
+
): Promise<boolean> {
|
|
607
|
+
return this.#exclusive(() => {
|
|
608
|
+
const key = `${providerId}:${accountId}`;
|
|
609
|
+
const state = this.#accounts.get(key);
|
|
610
|
+
if (!state) return false;
|
|
611
|
+
return computeAccountSnapshot(state, threshold).nearLimit;
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
async getNearLimitAccounts(threshold?: number): Promise<AccountUsageSnapshot[]> {
|
|
616
|
+
return this.#exclusive(() => {
|
|
617
|
+
const out: AccountUsageSnapshot[] = [];
|
|
618
|
+
for (const state of this.#accounts.values()) {
|
|
619
|
+
const snap = computeAccountSnapshot(state, threshold);
|
|
620
|
+
if (snap.nearLimit) out.push(snap);
|
|
621
|
+
}
|
|
622
|
+
return out.sort((a, b) => {
|
|
623
|
+
if (a.providerId !== b.providerId) return a.providerId.localeCompare(b.providerId);
|
|
624
|
+
return a.accountId.localeCompare(b.accountId);
|
|
625
|
+
});
|
|
626
|
+
});
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
async getAccountUsage(
|
|
630
|
+
providerId: string,
|
|
631
|
+
accountId: string,
|
|
632
|
+
): Promise<AccountUsageSnapshot | undefined> {
|
|
633
|
+
return this.#exclusive(() => {
|
|
634
|
+
const key = `${providerId}:${accountId}`;
|
|
635
|
+
const state = this.#accounts.get(key);
|
|
636
|
+
if (!state) return undefined;
|
|
637
|
+
return computeAccountSnapshot(state);
|
|
638
|
+
});
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
async listAccountUsage(): Promise<AccountUsageSnapshot[]> {
|
|
642
|
+
return this.#exclusive(() => {
|
|
643
|
+
const out: AccountUsageSnapshot[] = [];
|
|
644
|
+
for (const state of this.#accounts.values()) {
|
|
645
|
+
out.push(computeAccountSnapshot(state));
|
|
646
|
+
}
|
|
647
|
+
return out.sort((a, b) => {
|
|
648
|
+
if (a.providerId !== b.providerId) return a.providerId.localeCompare(b.providerId);
|
|
649
|
+
return a.accountId.localeCompare(b.accountId);
|
|
650
|
+
});
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
|
|
508
654
|
toJSON(): Record<string, unknown> {
|
|
509
655
|
const providers: Record<string, unknown> = {};
|
|
510
656
|
for (const [id, state] of this.#providers) {
|
|
@@ -523,6 +669,7 @@ export class InMemoryUsageStore implements UsageStore {
|
|
|
523
669
|
return {
|
|
524
670
|
kind: "in-memory-usage",
|
|
525
671
|
providers,
|
|
672
|
+
accounts: Object.fromEntries(this.#accounts),
|
|
526
673
|
};
|
|
527
674
|
}
|
|
528
675
|
|
|
@@ -635,6 +782,49 @@ function toSnapshot(state: InternalProviderState): ProviderUsageSnapshot {
|
|
|
635
782
|
};
|
|
636
783
|
}
|
|
637
784
|
|
|
785
|
+
export function computeAccountSnapshot(
|
|
786
|
+
doc: AccountUsageDoc,
|
|
787
|
+
threshold: number = DEFAULT_NEAR_LIMIT_THRESHOLD,
|
|
788
|
+
): AccountUsageSnapshot {
|
|
789
|
+
const limit = doc.limit;
|
|
790
|
+
const used = doc.used;
|
|
791
|
+
const remaining =
|
|
792
|
+
limit === undefined ? undefined : Math.max(0, limit - used);
|
|
793
|
+
let status: SubscriptionStatus;
|
|
794
|
+
let nearLimit = false;
|
|
795
|
+
let health: ProviderHealth;
|
|
796
|
+
if (limit === undefined) {
|
|
797
|
+
status = "unknown";
|
|
798
|
+
health = "healthy";
|
|
799
|
+
} else if (used >= limit) {
|
|
800
|
+
status = "exhausted";
|
|
801
|
+
health = "dead";
|
|
802
|
+
} else if ((limit - used) / limit < threshold) {
|
|
803
|
+
status = "near-limit";
|
|
804
|
+
nearLimit = true;
|
|
805
|
+
health = "degraded";
|
|
806
|
+
} else {
|
|
807
|
+
status = "active";
|
|
808
|
+
health = "healthy";
|
|
809
|
+
}
|
|
810
|
+
const percentRemaining =
|
|
811
|
+
limit === undefined
|
|
812
|
+
? undefined
|
|
813
|
+
: Math.round(((limit - used) / limit) * 100);
|
|
814
|
+
return {
|
|
815
|
+
providerId: doc.providerId,
|
|
816
|
+
accountId: doc.accountId,
|
|
817
|
+
used,
|
|
818
|
+
limit,
|
|
819
|
+
remaining,
|
|
820
|
+
percentRemaining,
|
|
821
|
+
nearLimit,
|
|
822
|
+
subscriptionStatus: status,
|
|
823
|
+
health,
|
|
824
|
+
updatedAt: doc.updatedAt,
|
|
825
|
+
};
|
|
826
|
+
}
|
|
827
|
+
|
|
638
828
|
/**
|
|
639
829
|
* Composite facade for callers that want one injectable "state" handle.
|
|
640
830
|
* Writes stay on the specialized stores; this is a thin convenience.
|