@cortexkit/common-auth 0.1.2 → 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/dist/quota/codec.d.ts +11 -0
- package/dist/quota/codec.js +12 -0
- package/dist/quota/index.d.ts +8 -0
- package/dist/quota/index.js +4 -0
- package/dist/quota/map.d.ts +63 -0
- package/dist/quota/map.js +105 -0
- package/dist/quota/merge.d.ts +48 -0
- package/dist/quota/merge.js +162 -0
- package/dist/quota/projection.d.ts +57 -0
- package/dist/quota/projection.js +127 -0
- package/dist/routing/admission.d.ts +93 -0
- package/dist/routing/admission.js +140 -0
- package/dist/routing/index.d.ts +8 -0
- package/dist/routing/index.js +4 -0
- package/dist/routing/ordered.d.ts +47 -0
- package/dist/routing/ordered.js +58 -0
- package/dist/routing/pins.d.ts +22 -0
- package/dist/routing/pins.js +34 -0
- package/dist/routing/sticky.d.ts +118 -0
- package/dist/routing/sticky.js +310 -0
- package/dist/sidebar-file/sidebar-file.d.ts +4 -3
- package/dist/sidebar-file/sidebar-file.js +1 -4
- package/dist/store/attribution.d.ts +17 -0
- package/dist/store/attribution.js +46 -0
- package/dist/store/errors.d.ts +52 -0
- package/dist/store/errors.js +38 -0
- package/dist/store/hooks.d.ts +12 -0
- package/dist/store/hooks.js +34 -0
- package/dist/store/identity.d.ts +23 -0
- package/dist/store/identity.js +53 -0
- package/dist/store/index.d.ts +16 -0
- package/dist/store/index.js +5 -0
- package/dist/store/mutate.d.ts +111 -0
- package/dist/store/mutate.js +294 -0
- package/dist/store/pool.d.ts +86 -0
- package/dist/store/pool.js +98 -0
- package/dist/store/pull.d.ts +36 -0
- package/dist/store/pull.js +129 -0
- package/dist/store/refresh-lock.d.ts +63 -0
- package/dist/store/refresh-lock.js +125 -0
- package/dist/store/refresh.d.ts +48 -0
- package/dist/store/refresh.js +169 -0
- package/dist/store/rows.d.ts +53 -0
- package/dist/store/rows.js +250 -0
- package/dist/store/runtime.d.ts +28 -0
- package/dist/store/runtime.js +45 -0
- package/dist/store/schema.d.ts +133 -0
- package/dist/store/schema.js +323 -0
- package/package.json +13 -1
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface QuotaCodec {
|
|
2
|
+
readonly validate: (value: unknown) => boolean;
|
|
3
|
+
readonly merge: (stored: unknown, observation: unknown) => unknown;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* The quota codec the account-pool store (the `/store` subpath) takes when it
|
|
7
|
+
* is opened: `validate` checks a stored per-row quota map and `merge` applies
|
|
8
|
+
* an observation to one, so the store persists the map without interpreting
|
|
9
|
+
* it.
|
|
10
|
+
*/
|
|
11
|
+
export declare const quotaCodec: QuotaCodec;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { isQuotaMap } from './map.js';
|
|
2
|
+
import { mergeQuotaObservation } from './merge.js';
|
|
3
|
+
/**
|
|
4
|
+
* The quota codec the account-pool store (the `/store` subpath) takes when it
|
|
5
|
+
* is opened: `validate` checks a stored per-row quota map and `merge` applies
|
|
6
|
+
* an observation to one, so the store persists the map without interpreting
|
|
7
|
+
* it.
|
|
8
|
+
*/
|
|
9
|
+
export const quotaCodec = Object.freeze({
|
|
10
|
+
validate: isQuotaMap,
|
|
11
|
+
merge: mergeQuotaObservation,
|
|
12
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export type { QuotaCodec } from './codec.js';
|
|
2
|
+
export { quotaCodec } from './codec.js';
|
|
3
|
+
export type { CreditBudgetCleared, CreditBudgetEntry, CreditBudgetReading, QuotaAbsentEntry, QuotaEntry, QuotaMap, QuotaReadingEntry, QuotaRetiredEntry, } from './map.js';
|
|
4
|
+
export { ALL_SCOPE, DEFAULT_REQUIRED_LABELS, emptyQuotaMap, isQuotaMap, } from './map.js';
|
|
5
|
+
export type { ObservedBudget, ObservedPair, ObservedReading, QuotaObservation, } from './merge.js';
|
|
6
|
+
export { isQuotaObservation, mergeQuotaObservation, QuotaCodecError, } from './merge.js';
|
|
7
|
+
export type { ExhaustionReset, ProjectedBudget, ProjectedLimit, ProjectedQuota, } from './projection.js';
|
|
8
|
+
export { budgetExhaustedResetAt, futureResetAt, projectQuota, readsExhausted, } from './projection.js';
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { quotaCodec } from './codec.js';
|
|
2
|
+
export { ALL_SCOPE, DEFAULT_REQUIRED_LABELS, emptyQuotaMap, isQuotaMap, } from './map.js';
|
|
3
|
+
export { isQuotaObservation, mergeQuotaObservation, QuotaCodecError, } from './merge.js';
|
|
4
|
+
export { budgetExhaustedResetAt, futureResetAt, projectQuota, readsExhausted, } from './projection.js';
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/** The scope of a limit that binds every request regardless of model. */
|
|
2
|
+
export declare const ALL_SCOPE = "all";
|
|
3
|
+
/** The window label required when an admission call supplies none. */
|
|
4
|
+
export declare const DEFAULT_REQUIRED_LABELS: readonly string[];
|
|
5
|
+
export interface QuotaReadingEntry {
|
|
6
|
+
scope: string;
|
|
7
|
+
label: string;
|
|
8
|
+
kind: 'reading';
|
|
9
|
+
checkedAt: number;
|
|
10
|
+
usedPercent: number;
|
|
11
|
+
/** ISO timestamp at which the window resets, as the provider reported it. */
|
|
12
|
+
resetsAt?: string;
|
|
13
|
+
/** Window length in minutes, when the provider reported one. */
|
|
14
|
+
windowMinutes?: number;
|
|
15
|
+
}
|
|
16
|
+
export interface QuotaRetiredEntry {
|
|
17
|
+
scope: string;
|
|
18
|
+
label: string;
|
|
19
|
+
kind: 'retired';
|
|
20
|
+
retiredAt: number;
|
|
21
|
+
}
|
|
22
|
+
export interface QuotaAbsentEntry {
|
|
23
|
+
scope: string;
|
|
24
|
+
label: string;
|
|
25
|
+
kind: 'absent';
|
|
26
|
+
checkedAt: number;
|
|
27
|
+
}
|
|
28
|
+
export type QuotaEntry = QuotaReadingEntry | QuotaRetiredEntry | QuotaAbsentEntry;
|
|
29
|
+
export interface CreditBudgetReading {
|
|
30
|
+
kind: 'reading';
|
|
31
|
+
checkedAt: number;
|
|
32
|
+
/** The provider's own verdict that the budget is spent. */
|
|
33
|
+
reached: boolean;
|
|
34
|
+
remainingPercent?: number;
|
|
35
|
+
usedPercent?: number;
|
|
36
|
+
resetsAt?: string;
|
|
37
|
+
limit?: number;
|
|
38
|
+
used?: number;
|
|
39
|
+
remaining?: number;
|
|
40
|
+
unit?: string;
|
|
41
|
+
}
|
|
42
|
+
export interface CreditBudgetCleared {
|
|
43
|
+
kind: 'cleared';
|
|
44
|
+
checkedAt: number;
|
|
45
|
+
}
|
|
46
|
+
export type CreditBudgetEntry = CreditBudgetReading | CreditBudgetCleared;
|
|
47
|
+
export interface QuotaMap {
|
|
48
|
+
limits: QuotaEntry[];
|
|
49
|
+
/** Absent means no observation has reported on the budget yet. */
|
|
50
|
+
budget?: CreditBudgetEntry;
|
|
51
|
+
}
|
|
52
|
+
export declare function emptyQuotaMap(): QuotaMap;
|
|
53
|
+
/** The time an entry speaks for: its reading, retirement or absence time. */
|
|
54
|
+
export declare function entryTime(entry: QuotaEntry): number;
|
|
55
|
+
export declare function limitKey(scope: string, label: string): string;
|
|
56
|
+
export declare function isCreditBudgetEntry(value: unknown): value is CreditBudgetEntry;
|
|
57
|
+
/**
|
|
58
|
+
* True when `value` is a well-formed quota map. Unrecognised top-level keys
|
|
59
|
+
* are tolerated (and preserved by merge) so a newer writer's additions
|
|
60
|
+
* survive; a malformed entry, a duplicated (scope, label) key or a malformed
|
|
61
|
+
* budget makes the whole map invalid.
|
|
62
|
+
*/
|
|
63
|
+
export declare function isQuotaMap(value: unknown): value is QuotaMap;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// The per-row quota map: what the pool knows about one row's limits.
|
|
2
|
+
//
|
|
3
|
+
// A row's quota is a set of keyed limits. Each key is a (scope, window label)
|
|
4
|
+
// pair: the scope is `all` for a limit that binds every request, or a model
|
|
5
|
+
// family name for a limit that binds only that family; the label names the
|
|
6
|
+
// window the provider reports (for example `primary` or `secondary`). Each key
|
|
7
|
+
// holds one entry, which is either a reading, a retirement tombstone (the key
|
|
8
|
+
// held a reading until an authoritative observation stopped reporting it), or
|
|
9
|
+
// an absence record (an authoritative observation reported the key as having
|
|
10
|
+
// no limit before any reading was seen). Tombstones and absence records are
|
|
11
|
+
// evidence that the key is known to be unlimited; a missing key is unknown.
|
|
12
|
+
//
|
|
13
|
+
// Beside the limits sits the credit budget, a third pressure axis with its own
|
|
14
|
+
// reset clock. It is a tri-state: absent (never reported), a reading, or
|
|
15
|
+
// cleared (the provider reported that no budget exists).
|
|
16
|
+
//
|
|
17
|
+
// The map is plain JSON so the store can persist it without interpreting it.
|
|
18
|
+
/** The scope of a limit that binds every request regardless of model. */
|
|
19
|
+
export const ALL_SCOPE = 'all';
|
|
20
|
+
/** The window label required when an admission call supplies none. */
|
|
21
|
+
export const DEFAULT_REQUIRED_LABELS = Object.freeze([
|
|
22
|
+
'primary',
|
|
23
|
+
]);
|
|
24
|
+
export function emptyQuotaMap() {
|
|
25
|
+
return { limits: [] };
|
|
26
|
+
}
|
|
27
|
+
/** The time an entry speaks for: its reading, retirement or absence time. */
|
|
28
|
+
export function entryTime(entry) {
|
|
29
|
+
return entry.kind === 'retired' ? entry.retiredAt : entry.checkedAt;
|
|
30
|
+
}
|
|
31
|
+
export function limitKey(scope, label) {
|
|
32
|
+
// JSON-encoding the pair keeps keys unambiguous for any scope or label text.
|
|
33
|
+
return JSON.stringify([scope, label]);
|
|
34
|
+
}
|
|
35
|
+
function isRecord(value) {
|
|
36
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
37
|
+
}
|
|
38
|
+
function isFiniteNumber(value) {
|
|
39
|
+
return typeof value === 'number' && Number.isFinite(value);
|
|
40
|
+
}
|
|
41
|
+
function isName(value) {
|
|
42
|
+
return typeof value === 'string' && value.length > 0;
|
|
43
|
+
}
|
|
44
|
+
function optional(record, key, check) {
|
|
45
|
+
return record[key] === undefined || check(record[key]);
|
|
46
|
+
}
|
|
47
|
+
function isPositiveFinite(value) {
|
|
48
|
+
return isFiniteNumber(value) && value > 0;
|
|
49
|
+
}
|
|
50
|
+
function isString(value) {
|
|
51
|
+
return typeof value === 'string';
|
|
52
|
+
}
|
|
53
|
+
function isQuotaEntry(value) {
|
|
54
|
+
if (!isRecord(value) || !isName(value.scope) || !isName(value.label)) {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
switch (value.kind) {
|
|
58
|
+
case 'reading':
|
|
59
|
+
return (isFiniteNumber(value.checkedAt) &&
|
|
60
|
+
isFiniteNumber(value.usedPercent) &&
|
|
61
|
+
optional(value, 'resetsAt', isString) &&
|
|
62
|
+
optional(value, 'windowMinutes', isPositiveFinite));
|
|
63
|
+
case 'retired':
|
|
64
|
+
return isFiniteNumber(value.retiredAt);
|
|
65
|
+
case 'absent':
|
|
66
|
+
return isFiniteNumber(value.checkedAt);
|
|
67
|
+
default:
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
export function isCreditBudgetEntry(value) {
|
|
72
|
+
if (!isRecord(value) || !isFiniteNumber(value.checkedAt))
|
|
73
|
+
return false;
|
|
74
|
+
if (value.kind === 'cleared')
|
|
75
|
+
return true;
|
|
76
|
+
return (value.kind === 'reading' &&
|
|
77
|
+
typeof value.reached === 'boolean' &&
|
|
78
|
+
optional(value, 'remainingPercent', isFiniteNumber) &&
|
|
79
|
+
optional(value, 'usedPercent', isFiniteNumber) &&
|
|
80
|
+
optional(value, 'resetsAt', isString) &&
|
|
81
|
+
optional(value, 'limit', isFiniteNumber) &&
|
|
82
|
+
optional(value, 'used', isFiniteNumber) &&
|
|
83
|
+
optional(value, 'remaining', isFiniteNumber) &&
|
|
84
|
+
optional(value, 'unit', isString));
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* True when `value` is a well-formed quota map. Unrecognised top-level keys
|
|
88
|
+
* are tolerated (and preserved by merge) so a newer writer's additions
|
|
89
|
+
* survive; a malformed entry, a duplicated (scope, label) key or a malformed
|
|
90
|
+
* budget makes the whole map invalid.
|
|
91
|
+
*/
|
|
92
|
+
export function isQuotaMap(value) {
|
|
93
|
+
if (!isRecord(value) || !Array.isArray(value.limits))
|
|
94
|
+
return false;
|
|
95
|
+
const seen = new Set();
|
|
96
|
+
for (const entry of value.limits) {
|
|
97
|
+
if (!isQuotaEntry(entry))
|
|
98
|
+
return false;
|
|
99
|
+
const key = limitKey(entry.scope, entry.label);
|
|
100
|
+
if (seen.has(key))
|
|
101
|
+
return false;
|
|
102
|
+
seen.add(key);
|
|
103
|
+
}
|
|
104
|
+
return optional(value, 'budget', isCreditBudgetEntry);
|
|
105
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { type QuotaMap } from './map.js';
|
|
2
|
+
export interface ObservedReading {
|
|
3
|
+
/** Defaults to `all`. */
|
|
4
|
+
scope?: string;
|
|
5
|
+
label: string;
|
|
6
|
+
usedPercent: number;
|
|
7
|
+
resetsAt?: string;
|
|
8
|
+
windowMinutes?: number;
|
|
9
|
+
}
|
|
10
|
+
export interface ObservedPair {
|
|
11
|
+
/** Defaults to `all`. */
|
|
12
|
+
scope?: string;
|
|
13
|
+
label: string;
|
|
14
|
+
}
|
|
15
|
+
export type ObservedBudget = {
|
|
16
|
+
kind: 'reading';
|
|
17
|
+
reached: boolean;
|
|
18
|
+
remainingPercent?: number;
|
|
19
|
+
usedPercent?: number;
|
|
20
|
+
resetsAt?: string;
|
|
21
|
+
limit?: number;
|
|
22
|
+
used?: number;
|
|
23
|
+
remaining?: number;
|
|
24
|
+
unit?: string;
|
|
25
|
+
} | {
|
|
26
|
+
kind: 'cleared';
|
|
27
|
+
};
|
|
28
|
+
export interface QuotaObservation {
|
|
29
|
+
checkedAt: number;
|
|
30
|
+
readings?: ObservedReading[];
|
|
31
|
+
/** Pairs reported on with authority besides the ones carrying a reading. */
|
|
32
|
+
coverage?: ObservedPair[];
|
|
33
|
+
/** Absent means the observation says nothing about the budget. */
|
|
34
|
+
budget?: ObservedBudget;
|
|
35
|
+
}
|
|
36
|
+
/** Thrown by `mergeQuotaObservation` when either argument is malformed. */
|
|
37
|
+
export declare class QuotaCodecError extends TypeError {
|
|
38
|
+
constructor(message: string);
|
|
39
|
+
}
|
|
40
|
+
/** True when `value` is a well-formed observation; a pair read twice is not. */
|
|
41
|
+
export declare function isQuotaObservation(value: unknown): value is QuotaObservation;
|
|
42
|
+
/**
|
|
43
|
+
* Applies `observation` to `stored` (undefined for a row with no map yet) and
|
|
44
|
+
* returns the new map. Pure: neither argument is modified. Throws
|
|
45
|
+
* `QuotaCodecError` when either argument is malformed, so a store refuses the
|
|
46
|
+
* write rather than persisting a map it could not read back.
|
|
47
|
+
*/
|
|
48
|
+
export declare function mergeQuotaObservation(stored: unknown, observation: unknown): QuotaMap;
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
// Merging one observation into a stored quota map.
|
|
2
|
+
//
|
|
3
|
+
// One freshness rule applies everywhere: an observation applies to a key when
|
|
4
|
+
// it is not older than what the key holds, so an equal time applies and the
|
|
5
|
+
// observation applied last wins, as openai-auth's `freshestWindow` does.
|
|
6
|
+
//
|
|
7
|
+
// An observation carries readings plus a coverage list: the (scope, label)
|
|
8
|
+
// pairs it speaks for with authority. Every reading is implicitly covered. A
|
|
9
|
+
// covered pair the observation carries no reading for is reported as having
|
|
10
|
+
// no limit: an existing reading becomes a retirement tombstone, and a key with
|
|
11
|
+
// no entry gets an absence record. A pair outside the coverage is never
|
|
12
|
+
// touched, so a header-shaped partial observation (which covers only what it
|
|
13
|
+
// carries) cannot erase a family limit or the budget.
|
|
14
|
+
import { ALL_SCOPE, entryTime, isQuotaMap, limitKey, } from './map.js';
|
|
15
|
+
/** Thrown by `mergeQuotaObservation` when either argument is malformed. */
|
|
16
|
+
export class QuotaCodecError extends TypeError {
|
|
17
|
+
constructor(message) {
|
|
18
|
+
super(message);
|
|
19
|
+
this.name = 'QuotaCodecError';
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function isRecord(value) {
|
|
23
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
24
|
+
}
|
|
25
|
+
function isFiniteNumber(value) {
|
|
26
|
+
return typeof value === 'number' && Number.isFinite(value);
|
|
27
|
+
}
|
|
28
|
+
function isScope(value) {
|
|
29
|
+
return value === undefined || (typeof value === 'string' && value !== '');
|
|
30
|
+
}
|
|
31
|
+
function isLabel(value) {
|
|
32
|
+
return typeof value === 'string' && value !== '';
|
|
33
|
+
}
|
|
34
|
+
function optionalFinite(record, key) {
|
|
35
|
+
return record[key] === undefined || isFiniteNumber(record[key]);
|
|
36
|
+
}
|
|
37
|
+
function optionalString(record, key) {
|
|
38
|
+
return record[key] === undefined || typeof record[key] === 'string';
|
|
39
|
+
}
|
|
40
|
+
function isObservedReading(value) {
|
|
41
|
+
return (isRecord(value) &&
|
|
42
|
+
isScope(value.scope) &&
|
|
43
|
+
isLabel(value.label) &&
|
|
44
|
+
isFiniteNumber(value.usedPercent) &&
|
|
45
|
+
optionalString(value, 'resetsAt') &&
|
|
46
|
+
(value.windowMinutes === undefined ||
|
|
47
|
+
(isFiniteNumber(value.windowMinutes) && value.windowMinutes > 0)));
|
|
48
|
+
}
|
|
49
|
+
function isObservedPair(value) {
|
|
50
|
+
return isRecord(value) && isScope(value.scope) && isLabel(value.label);
|
|
51
|
+
}
|
|
52
|
+
function isObservedBudget(value) {
|
|
53
|
+
if (!isRecord(value))
|
|
54
|
+
return false;
|
|
55
|
+
if (value.kind === 'cleared')
|
|
56
|
+
return true;
|
|
57
|
+
return (value.kind === 'reading' &&
|
|
58
|
+
typeof value.reached === 'boolean' &&
|
|
59
|
+
optionalFinite(value, 'remainingPercent') &&
|
|
60
|
+
optionalFinite(value, 'usedPercent') &&
|
|
61
|
+
optionalString(value, 'resetsAt') &&
|
|
62
|
+
optionalFinite(value, 'limit') &&
|
|
63
|
+
optionalFinite(value, 'used') &&
|
|
64
|
+
optionalFinite(value, 'remaining') &&
|
|
65
|
+
optionalString(value, 'unit'));
|
|
66
|
+
}
|
|
67
|
+
/** True when `value` is a well-formed observation; a pair read twice is not. */
|
|
68
|
+
export function isQuotaObservation(value) {
|
|
69
|
+
if (!isRecord(value) || !isFiniteNumber(value.checkedAt))
|
|
70
|
+
return false;
|
|
71
|
+
const readings = value.readings ?? [];
|
|
72
|
+
const coverage = value.coverage ?? [];
|
|
73
|
+
if (!Array.isArray(readings) || !Array.isArray(coverage))
|
|
74
|
+
return false;
|
|
75
|
+
const read = new Set();
|
|
76
|
+
for (const reading of readings) {
|
|
77
|
+
if (!isObservedReading(reading))
|
|
78
|
+
return false;
|
|
79
|
+
const key = limitKey(reading.scope ?? ALL_SCOPE, reading.label);
|
|
80
|
+
if (read.has(key))
|
|
81
|
+
return false;
|
|
82
|
+
read.add(key);
|
|
83
|
+
}
|
|
84
|
+
if (!coverage.every(isObservedPair))
|
|
85
|
+
return false;
|
|
86
|
+
return value.budget === undefined || isObservedBudget(value.budget);
|
|
87
|
+
}
|
|
88
|
+
function compareEntries(left, right) {
|
|
89
|
+
if (left.scope !== right.scope)
|
|
90
|
+
return left.scope < right.scope ? -1 : 1;
|
|
91
|
+
if (left.label !== right.label)
|
|
92
|
+
return left.label < right.label ? -1 : 1;
|
|
93
|
+
return 0;
|
|
94
|
+
}
|
|
95
|
+
function mergeBudget(stored, observed, checkedAt) {
|
|
96
|
+
if (observed === undefined)
|
|
97
|
+
return stored;
|
|
98
|
+
if (stored !== undefined && checkedAt < stored.checkedAt)
|
|
99
|
+
return stored;
|
|
100
|
+
return { ...observed, checkedAt };
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Applies `observation` to `stored` (undefined for a row with no map yet) and
|
|
104
|
+
* returns the new map. Pure: neither argument is modified. Throws
|
|
105
|
+
* `QuotaCodecError` when either argument is malformed, so a store refuses the
|
|
106
|
+
* write rather than persisting a map it could not read back.
|
|
107
|
+
*/
|
|
108
|
+
export function mergeQuotaObservation(stored, observation) {
|
|
109
|
+
if (stored !== undefined && !isQuotaMap(stored)) {
|
|
110
|
+
throw new QuotaCodecError('stored quota map is malformed');
|
|
111
|
+
}
|
|
112
|
+
if (!isQuotaObservation(observation)) {
|
|
113
|
+
throw new QuotaCodecError('quota observation is malformed');
|
|
114
|
+
}
|
|
115
|
+
const base = stored ?? { limits: [] };
|
|
116
|
+
const at = observation.checkedAt;
|
|
117
|
+
const entries = new Map();
|
|
118
|
+
for (const entry of base.limits) {
|
|
119
|
+
entries.set(limitKey(entry.scope, entry.label), entry);
|
|
120
|
+
}
|
|
121
|
+
const applies = (key) => {
|
|
122
|
+
const existing = entries.get(key);
|
|
123
|
+
return existing === undefined || at >= entryTime(existing);
|
|
124
|
+
};
|
|
125
|
+
const carried = new Set();
|
|
126
|
+
for (const reading of observation.readings ?? []) {
|
|
127
|
+
const scope = reading.scope ?? ALL_SCOPE;
|
|
128
|
+
const key = limitKey(scope, reading.label);
|
|
129
|
+
carried.add(key);
|
|
130
|
+
if (!applies(key))
|
|
131
|
+
continue;
|
|
132
|
+
entries.set(key, {
|
|
133
|
+
scope,
|
|
134
|
+
label: reading.label,
|
|
135
|
+
kind: 'reading',
|
|
136
|
+
checkedAt: at,
|
|
137
|
+
usedPercent: reading.usedPercent,
|
|
138
|
+
...(reading.resetsAt === undefined ? {} : { resetsAt: reading.resetsAt }),
|
|
139
|
+
...(reading.windowMinutes === undefined
|
|
140
|
+
? {}
|
|
141
|
+
: { windowMinutes: reading.windowMinutes }),
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
for (const pair of observation.coverage ?? []) {
|
|
145
|
+
const scope = pair.scope ?? ALL_SCOPE;
|
|
146
|
+
const key = limitKey(scope, pair.label);
|
|
147
|
+
if (carried.has(key) || !applies(key))
|
|
148
|
+
continue;
|
|
149
|
+
const existing = entries.get(key);
|
|
150
|
+
entries.set(key, existing === undefined || existing.kind === 'absent'
|
|
151
|
+
? { scope, label: pair.label, kind: 'absent', checkedAt: at }
|
|
152
|
+
: { scope, label: pair.label, kind: 'retired', retiredAt: at });
|
|
153
|
+
}
|
|
154
|
+
// Keys this module does not recognise are carried over untouched.
|
|
155
|
+
const { limits: _limits, budget: _budget, ...unknownKeys } = base;
|
|
156
|
+
const budget = mergeBudget(base.budget, observation.budget, at);
|
|
157
|
+
return {
|
|
158
|
+
...unknownKeys,
|
|
159
|
+
limits: [...entries.values()].sort(compareEntries),
|
|
160
|
+
...(budget === undefined ? {} : { budget }),
|
|
161
|
+
};
|
|
162
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { type QuotaMap } from './map.js';
|
|
2
|
+
export interface ProjectedLimit {
|
|
3
|
+
scope: string;
|
|
4
|
+
label: string;
|
|
5
|
+
kind: 'reading' | 'retired' | 'absent';
|
|
6
|
+
/** The reading, retirement or absence time. */
|
|
7
|
+
checkedAt: number;
|
|
8
|
+
/** The stored window length; undefined when the provider reported none. */
|
|
9
|
+
windowMinutes?: number;
|
|
10
|
+
/** Present on readings only. */
|
|
11
|
+
usedPercent?: number;
|
|
12
|
+
/** Present on readings only: `100 - usedPercent`. */
|
|
13
|
+
remainingPercent?: number;
|
|
14
|
+
resetsAt?: string;
|
|
15
|
+
}
|
|
16
|
+
export interface ProjectedBudget {
|
|
17
|
+
checkedAt: number;
|
|
18
|
+
reached: boolean;
|
|
19
|
+
remainingPercent?: number;
|
|
20
|
+
resetsAt?: string;
|
|
21
|
+
}
|
|
22
|
+
export interface ProjectedQuota {
|
|
23
|
+
scope: string;
|
|
24
|
+
/** One entry per label: longest known window first, unknown lengths last. */
|
|
25
|
+
limits: readonly ProjectedLimit[];
|
|
26
|
+
/**
|
|
27
|
+
* The oldest reading time among the limits (or, with no reading, the oldest
|
|
28
|
+
* evidence time), so a projection is only as fresh as its stalest limit.
|
|
29
|
+
*/
|
|
30
|
+
checkedAt?: number;
|
|
31
|
+
/** Present only when the stored budget is a reading, not cleared. */
|
|
32
|
+
budget?: ProjectedBudget;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Resolves `map` for a request in `scope` (`all` or a model family). A family
|
|
36
|
+
* request sees only its own family's keys and the `all` keys. Per label, a
|
|
37
|
+
* family reading shadows the `all` entry; a family tombstone or absence
|
|
38
|
+
* record says only that no family-specific limit exists, so it does not hide
|
|
39
|
+
* an `all` entry for the same label and is used only when there is none.
|
|
40
|
+
*/
|
|
41
|
+
export declare function projectQuota(map: QuotaMap | undefined, scope?: string): ProjectedQuota;
|
|
42
|
+
export interface ExhaustionReset {
|
|
43
|
+
resetsAt: string;
|
|
44
|
+
resetAtMs: number;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* The credit budget's own exhaustion signal. Admission and the sticky
|
|
48
|
+
* routing decision to move a session off its pinned row both use it, so the
|
|
49
|
+
* two agree on what "spent" means. `reached` is the provider's
|
|
50
|
+
* verdict (the percentage is only a display value), and the check fails open
|
|
51
|
+
* on a missing, unparsable or already-passed reset.
|
|
52
|
+
*/
|
|
53
|
+
export declare function budgetExhaustedResetAt(quota: Pick<ProjectedQuota, 'budget'> | null | undefined, now: number): ExhaustionReset | undefined;
|
|
54
|
+
/** True for a reading at or beyond its whole window. */
|
|
55
|
+
export declare function readsExhausted(limit: ProjectedLimit): boolean;
|
|
56
|
+
/** The reset time of a limit when it parses and lies after `now`. */
|
|
57
|
+
export declare function futureResetAt(limit: ProjectedLimit, now: number): number | undefined;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// Projection of a keyed quota map onto one request scope.
|
|
2
|
+
//
|
|
3
|
+
// The selection primitives carried from openai-auth judge an account by a
|
|
4
|
+
// short list of windows; the keyed map can hold many (scope, label) keys. The
|
|
5
|
+
// projection resolves exactly one entry per window label for the request's
|
|
6
|
+
// scope, orders the result the way the primitives expect, and carries each
|
|
7
|
+
// entry's (scope, label) so a decision names the limit it judged rather than
|
|
8
|
+
// the position it occupied.
|
|
9
|
+
import { ALL_SCOPE, } from './map.js';
|
|
10
|
+
function project(entry) {
|
|
11
|
+
if (entry.kind === 'retired') {
|
|
12
|
+
return {
|
|
13
|
+
scope: entry.scope,
|
|
14
|
+
label: entry.label,
|
|
15
|
+
kind: 'retired',
|
|
16
|
+
checkedAt: entry.retiredAt,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
if (entry.kind === 'absent') {
|
|
20
|
+
return {
|
|
21
|
+
scope: entry.scope,
|
|
22
|
+
label: entry.label,
|
|
23
|
+
kind: 'absent',
|
|
24
|
+
checkedAt: entry.checkedAt,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
scope: entry.scope,
|
|
29
|
+
label: entry.label,
|
|
30
|
+
kind: 'reading',
|
|
31
|
+
checkedAt: entry.checkedAt,
|
|
32
|
+
usedPercent: entry.usedPercent,
|
|
33
|
+
remainingPercent: 100 - entry.usedPercent,
|
|
34
|
+
...(entry.windowMinutes === undefined
|
|
35
|
+
? {}
|
|
36
|
+
: { windowMinutes: entry.windowMinutes }),
|
|
37
|
+
...(entry.resetsAt === undefined ? {} : { resetsAt: entry.resetsAt }),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
function compareLimits(left, right) {
|
|
41
|
+
const leftKnown = left.windowMinutes !== undefined;
|
|
42
|
+
const rightKnown = right.windowMinutes !== undefined;
|
|
43
|
+
if (leftKnown !== rightKnown)
|
|
44
|
+
return leftKnown ? -1 : 1;
|
|
45
|
+
if (leftKnown && rightKnown && left.windowMinutes !== right.windowMinutes) {
|
|
46
|
+
return (right.windowMinutes ?? 0) - (left.windowMinutes ?? 0);
|
|
47
|
+
}
|
|
48
|
+
if (left.label !== right.label)
|
|
49
|
+
return left.label < right.label ? -1 : 1;
|
|
50
|
+
return 0;
|
|
51
|
+
}
|
|
52
|
+
function projectBudget(budget) {
|
|
53
|
+
return {
|
|
54
|
+
checkedAt: budget.checkedAt,
|
|
55
|
+
reached: budget.reached,
|
|
56
|
+
...(budget.remainingPercent === undefined
|
|
57
|
+
? {}
|
|
58
|
+
: { remainingPercent: budget.remainingPercent }),
|
|
59
|
+
...(budget.resetsAt === undefined ? {} : { resetsAt: budget.resetsAt }),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Resolves `map` for a request in `scope` (`all` or a model family). A family
|
|
64
|
+
* request sees only its own family's keys and the `all` keys. Per label, a
|
|
65
|
+
* family reading shadows the `all` entry; a family tombstone or absence
|
|
66
|
+
* record says only that no family-specific limit exists, so it does not hide
|
|
67
|
+
* an `all` entry for the same label and is used only when there is none.
|
|
68
|
+
*/
|
|
69
|
+
export function projectQuota(map, scope = ALL_SCOPE) {
|
|
70
|
+
const byLabel = new Map();
|
|
71
|
+
for (const entry of map?.limits ?? []) {
|
|
72
|
+
if (entry.scope !== scope && entry.scope !== ALL_SCOPE)
|
|
73
|
+
continue;
|
|
74
|
+
const current = byLabel.get(entry.label);
|
|
75
|
+
if (current === undefined) {
|
|
76
|
+
byLabel.set(entry.label, entry);
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
const family = entry.scope === ALL_SCOPE ? current : entry;
|
|
80
|
+
const general = entry.scope === ALL_SCOPE ? entry : current;
|
|
81
|
+
byLabel.set(entry.label, family.kind === 'reading' ? family : general);
|
|
82
|
+
}
|
|
83
|
+
const limits = [...byLabel.values()].map(project).sort(compareLimits);
|
|
84
|
+
const readings = limits.filter((limit) => limit.kind === 'reading');
|
|
85
|
+
const stamped = readings.length > 0 ? readings : limits;
|
|
86
|
+
const checkedAt = stamped.length > 0
|
|
87
|
+
? Math.min(...stamped.map((limit) => limit.checkedAt))
|
|
88
|
+
: undefined;
|
|
89
|
+
const budget = map?.budget?.kind === 'reading' ? projectBudget(map.budget) : undefined;
|
|
90
|
+
return {
|
|
91
|
+
scope,
|
|
92
|
+
limits,
|
|
93
|
+
...(checkedAt === undefined ? {} : { checkedAt }),
|
|
94
|
+
...(budget === undefined ? {} : { budget }),
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* The credit budget's own exhaustion signal. Admission and the sticky
|
|
99
|
+
* routing decision to move a session off its pinned row both use it, so the
|
|
100
|
+
* two agree on what "spent" means. `reached` is the provider's
|
|
101
|
+
* verdict (the percentage is only a display value), and the check fails open
|
|
102
|
+
* on a missing, unparsable or already-passed reset.
|
|
103
|
+
*/
|
|
104
|
+
export function budgetExhaustedResetAt(quota, now) {
|
|
105
|
+
const budget = quota?.budget;
|
|
106
|
+
if (budget?.reached !== true || typeof budget.resetsAt !== 'string') {
|
|
107
|
+
return undefined;
|
|
108
|
+
}
|
|
109
|
+
const resetAtMs = Date.parse(budget.resetsAt);
|
|
110
|
+
if (!Number.isFinite(resetAtMs) || resetAtMs <= now)
|
|
111
|
+
return undefined;
|
|
112
|
+
return { resetsAt: budget.resetsAt, resetAtMs };
|
|
113
|
+
}
|
|
114
|
+
/** True for a reading at or beyond its whole window. */
|
|
115
|
+
export function readsExhausted(limit) {
|
|
116
|
+
return (limit.kind === 'reading' &&
|
|
117
|
+
typeof limit.usedPercent === 'number' &&
|
|
118
|
+
Number.isFinite(limit.usedPercent) &&
|
|
119
|
+
limit.usedPercent >= 100);
|
|
120
|
+
}
|
|
121
|
+
/** The reset time of a limit when it parses and lies after `now`. */
|
|
122
|
+
export function futureResetAt(limit, now) {
|
|
123
|
+
if (typeof limit.resetsAt !== 'string')
|
|
124
|
+
return undefined;
|
|
125
|
+
const resetAtMs = Date.parse(limit.resetsAt);
|
|
126
|
+
return Number.isFinite(resetAtMs) && resetAtMs > now ? resetAtMs : undefined;
|
|
127
|
+
}
|