@cortexkit/anthropic-auth-core 2.0.2 → 2.0.3

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 (52) hide show
  1. package/dist/accounts.d.ts +7 -1
  2. package/dist/cache1h.d.ts +2 -0
  3. package/dist/cachekeep-registry.d.ts +3 -0
  4. package/dist/dump.d.ts +2 -0
  5. package/dist/fast.d.ts +2 -0
  6. package/dist/index.d.ts +9 -4
  7. package/dist/index.js +27333 -25202
  8. package/dist/internal-types/auth-menu/terminal.d.ts +43 -0
  9. package/dist/internal-types/claustrum/consumer.d.ts +132 -0
  10. package/dist/internal-types/claustrum/custody.d.ts +178 -0
  11. package/dist/internal-types/claustrum/enrollment.d.ts +164 -0
  12. package/dist/internal-types/claustrum/errors.d.ts +16 -0
  13. package/dist/internal-types/claustrum/host-slot.d.ts +39 -0
  14. package/dist/internal-types/claustrum/index.d.ts +19 -0
  15. package/dist/internal-types/claustrum/insula.d.ts +69 -0
  16. package/dist/internal-types/claustrum/interlock.d.ts +27 -0
  17. package/dist/internal-types/claustrum/roster.d.ts +221 -0
  18. package/dist/internal-types/fs/refresh-file-lock.d.ts +8 -2
  19. package/dist/internal-types/quota/format.d.ts +26 -0
  20. package/dist/internal-types/quota/index.d.ts +2 -2
  21. package/dist/internal-types/routing/admission.d.ts +93 -0
  22. package/dist/internal-types/routing/index.d.ts +8 -0
  23. package/dist/internal-types/routing/ordered.d.ts +47 -0
  24. package/dist/internal-types/routing/pins.d.ts +22 -0
  25. package/dist/internal-types/routing/sticky.d.ts +158 -0
  26. package/dist/internal-types/setup/command-error.d.ts +17 -0
  27. package/dist/internal-types/setup/command.d.ts +3 -0
  28. package/dist/internal-types/setup/detect.d.ts +6 -0
  29. package/dist/internal-types/setup/index.d.ts +6 -0
  30. package/dist/internal-types/setup/process-fence.d.ts +43 -0
  31. package/dist/internal-types/setup/register.d.ts +11 -0
  32. package/dist/internal-types/setup/types.d.ts +138 -0
  33. package/dist/internal-types/store/pool.d.ts +1 -1
  34. package/dist/internal-types/store/refresh-lock.d.ts +0 -1
  35. package/dist/logging.d.ts +2 -0
  36. package/dist/mid-conversation-output-config.d.ts +35 -0
  37. package/dist/native-account-runtime.d.ts +6 -2
  38. package/dist/native-account-view.d.ts +19 -3
  39. package/dist/native-custody-activation.d.ts +1 -1
  40. package/dist/native-menu-executor.d.ts +1 -1
  41. package/dist/native-migration.d.ts +25 -4
  42. package/dist/native-quota-observations.d.ts +26 -0
  43. package/dist/native-transient-account.d.ts +3 -4
  44. package/dist/native-ui.d.ts +2 -0
  45. package/dist/native-usage-publication.d.ts +5 -0
  46. package/dist/native-vault-runtime.d.ts +4 -0
  47. package/dist/network-errors.d.ts +1 -0
  48. package/dist/pool-authority.d.ts +25 -7
  49. package/dist/pool-paths.d.ts +2 -0
  50. package/dist/quotas.d.ts +15 -2
  51. package/dist/relay.d.ts +20 -0
  52. package/package.json +2 -2
@@ -0,0 +1,27 @@
1
+ /**
2
+ * The declined-account interlock: the user's "do not use this vault account",
3
+ * kept on the consumer side because the vault has no notion of it.
4
+ *
5
+ * A decline follows the provider account, not the vault record: an entry made
6
+ * while the account was known matches any credential id or alias that logs
7
+ * into that same account, so relabelling, adding an alias or removing and
8
+ * re-adding the account never lifts it. A credential id that now logs into a
9
+ * different known account does not match, because that account has its own
10
+ * policy. While either side's account is unknown the entry falls back to the
11
+ * credential id, so an adapter that makes no identity claim cannot slip past
12
+ * it. Never keyed on the record version, so a token refresh cannot lift it.
13
+ */
14
+ export interface DeclinedAccount {
15
+ readonly credentialId: string;
16
+ readonly accountIdentity?: string;
17
+ }
18
+ export declare function isDeclined(entries: readonly DeclinedAccount[], credentialId: string, accountIdentity?: string): boolean;
19
+ export declare function declineAccount(entries: readonly DeclinedAccount[], credentialId: string, accountIdentity?: string): DeclinedAccount[];
20
+ /**
21
+ * Lift every entry that declines a row: the row's credential ids (the
22
+ * representative and its aliases) under the row's account. An entry for a
23
+ * different known account is left alone even when it names one of these
24
+ * credential ids, so accepting a replacement never re-enables the account the
25
+ * user declined.
26
+ */
27
+ export declare function acceptAccount(entries: readonly DeclinedAccount[], credentialIds: readonly string[], accountIdentity?: string): DeclinedAccount[];
@@ -0,0 +1,221 @@
1
+ import { type QuotaMap, type QuotaObservation } from '../quota/index.js';
2
+ import type { RoutingRow } from '../routing/index.js';
3
+ import type { ClaustrumScopedAttempt, ClaustrumScopedCustody, SkippedVaultRecord, VaultCredential, VaultCredentialType, VaultInventory } from './custody.js';
4
+ import { type DeclinedAccount } from './interlock.js';
5
+ /**
6
+ * One vault account as the pool sees it. It carries no bearer material: the
7
+ * access token is fetched from the vault for every send. Several vault
8
+ * records logged into the same provider account collapse into one row, so
9
+ * duplicate logins never count as extra quota.
10
+ */
11
+ export interface VaultRosterRow {
12
+ routeId: string;
13
+ credentialId: string;
14
+ credentialType: VaultCredentialType;
15
+ accountIdentity?: string;
16
+ /** Other credential ids logged into the same provider account. */
17
+ aliases?: string[];
18
+ state: string;
19
+ label: string;
20
+ email?: string;
21
+ orgName?: string;
22
+ enabled: boolean;
23
+ addedAt: number;
24
+ quota?: QuotaMap;
25
+ /**
26
+ * The vault listed this record in a form this consumer could not use, so the
27
+ * row is the last good projection kept as it was rather than dropped.
28
+ */
29
+ stale?: true;
30
+ /**
31
+ * The vault's latest list named no account for `credentialId`, so
32
+ * `accountIdentity` is the last account that credential was known to log
33
+ * into, kept until the vault names one again. It proves neither that the
34
+ * credential still logs into that account nor that it changed accounts.
35
+ */
36
+ unclaimed?: true;
37
+ }
38
+ export interface VaultRosterFile {
39
+ version: 1;
40
+ view?: string;
41
+ /**
42
+ * False when the vault's reply carried records this consumer could not use.
43
+ * An incomplete reply never removes an account it may still hold: every
44
+ * previous member it does not account for is kept.
45
+ */
46
+ complete: boolean;
47
+ /** The unusable records of an incomplete reply: a safe id and a fixed reason. */
48
+ rejected?: SkippedVaultRecord[];
49
+ rows: VaultRosterRow[];
50
+ declined: DeclinedAccount[];
51
+ }
52
+ /** How a vault credential is presented in the pool. */
53
+ export type AccountMapper = (credential: VaultCredential) => {
54
+ label?: string;
55
+ };
56
+ export interface ProjectionOptions {
57
+ /** Ids already used by local rows; a vault route never takes one. */
58
+ reservedRouteIds?: ReadonlySet<string>;
59
+ /** Prefix for vault route ids, so they read differently from local ids. */
60
+ routePrefix?: string;
61
+ mapAccount?: AccountMapper;
62
+ now?: number;
63
+ }
64
+ export declare const DEFAULT_ROUTE_PREFIX = "vault:";
65
+ /**
66
+ * Project the vault's list onto the previous roster. Pure: callers serialize
67
+ * discovery and commit. Route ids, quota and the time an account was added
68
+ * survive for the same account; a credential that now logs into a different
69
+ * known account gets a new route id and no inherited quota.
70
+ *
71
+ * A record listed without an identity keeps the last account it was known to
72
+ * log into (the row is marked `unclaimed`). Dropping that binding would let a
73
+ * later different account look like an identity learned for the first time
74
+ * and inherit the first account's route and quota.
75
+ *
76
+ * A reply with unusable records is incomplete: a previous member it does not
77
+ * account for (its record was rejected, or a rejected record has no usable id
78
+ * and could be any of them) is kept, as an alias of its live account or as a
79
+ * `stale` row. Only a complete reply removes an account.
80
+ */
81
+ export declare function projectVaultRoster(previous: VaultRosterFile | undefined, inventory: VaultInventory, options?: ProjectionOptions): VaultRosterFile;
82
+ /**
83
+ * The rows `/routing` selects among. Only enabled, active accounts route;
84
+ * a declined or cold vault account stays listed for the menu but never
85
+ * reaches admission.
86
+ */
87
+ export declare function vaultRoutingRows(roster: VaultRosterFile | undefined): RoutingRow[];
88
+ export declare function readVaultRoster(path: string): Promise<VaultRosterFile | undefined>;
89
+ /** The ownership assertion available to a roster mutation callback. */
90
+ export interface VaultRosterLockContext {
91
+ assertOwned: () => Promise<void>;
92
+ }
93
+ export interface MutateVaultRosterOptions {
94
+ /**
95
+ * Runs when the new roster is fully staged on disk, immediately before the
96
+ * rename that publishes it, so a caller can confirm a lease of its own at
97
+ * the moment of publication rather than when `change` returned. If it
98
+ * throws, nothing is published and the staged file is removed. The
99
+ * roster's own write lock is reasserted right after it.
100
+ */
101
+ beforePublish?: () => Promise<void>;
102
+ }
103
+ /**
104
+ * Read, change and write the roster under its write lock. `change` returns
105
+ * undefined to leave the file as it is. Call `assertOwned()` immediately
106
+ * before any rename performed inside `change`; if it throws, do not rename.
107
+ */
108
+ export declare function mutateVaultRoster<T>(path: string, change: (current: VaultRosterFile | undefined, context: VaultRosterLockContext) => Promise<{
109
+ next?: VaultRosterFile;
110
+ result: T;
111
+ }> | {
112
+ next?: VaultRosterFile;
113
+ result: T;
114
+ }, options?: MutateVaultRosterOptions): Promise<T>;
115
+ /** Mark a vault account declined; it stays listed but never routes. */
116
+ export declare function declineVaultRoute(path: string, routeId: string): Promise<undefined>;
117
+ /** Lift the user's decline for a vault account and every alias of it. */
118
+ export declare function acceptVaultRoute(path: string, routeId: string): Promise<undefined>;
119
+ /**
120
+ * The receipt fields a quota or profile observation must carry to say which
121
+ * send (credential and account) it came from. Pass the
122
+ * `ClaustrumScopedAttempt` that send was authorized with.
123
+ */
124
+ export type QuotaReceipt = Pick<ClaustrumScopedAttempt, 'credentialId' | 'accountIdentity' | 'accountIdentitySource' | 'expectedAccountIdentity'>;
125
+ /**
126
+ * Merge a quota observation into a vault row through `/quota`'s merge. The
127
+ * write is fenced on the receipt the reading was taken with: it lands only
128
+ * while the row still holds that receipt's credential (as representative or
129
+ * alias) and account, so a slow read for a replaced account can never land on
130
+ * its successor. Returns whether the observation was kept.
131
+ */
132
+ export declare function recordVaultQuota(path: string, input: QuotaReceipt & {
133
+ routeId: string;
134
+ observation: QuotaObservation;
135
+ }): Promise<boolean>;
136
+ /**
137
+ * Merge a quota observation that belongs to an account rather than to a
138
+ * served credential, such as a background reading from insula. The write
139
+ * lands only while the row is still bound to that exact known account, so a
140
+ * reading taken before a replacement never reaches the successor. Unlike
141
+ * `recordVaultQuota` it never binds an account: a row without a known account
142
+ * is refused. Returns whether the observation was kept.
143
+ */
144
+ export declare function recordVaultAccountQuota(path: string, input: {
145
+ routeId: string;
146
+ accountIdentity: string;
147
+ observation: QuotaObservation;
148
+ }): Promise<boolean>;
149
+ /**
150
+ * Discover and commit the roster. The lease covers the list call and the
151
+ * commit, not just the write: an opaque view cannot tell a delayed old reply
152
+ * from a newer one, so a run that lost its lease is refused at commit. A
153
+ * failed list, or custody switching off mid-run, never writes anything; a
154
+ * peer holding the lease means its last committed roster is served instead.
155
+ */
156
+ export declare function refreshVaultRoster(options: {
157
+ path: string;
158
+ custody: Pick<ClaustrumScopedCustody, 'discover'>;
159
+ isActive?: () => boolean | Promise<boolean>;
160
+ signal?: AbortSignal;
161
+ projection?: ProjectionOptions | (() => ProjectionOptions);
162
+ }): Promise<VaultRosterFile | undefined>;
163
+ /**
164
+ * A host-owned main account as last verified from the vault: the plugin's
165
+ * logical route, the credential that serves it and the account it logs into.
166
+ */
167
+ export interface VaultPrimaryBinding {
168
+ routeId: string;
169
+ credentialId: string;
170
+ accountIdentity: string;
171
+ view: string;
172
+ }
173
+ export type VaultPrimaryUnavailableReason = 'malformed' | 'unclaimed' | 'incomplete' | 'identity-changed';
174
+ export type VaultPrimary =
175
+ /**
176
+ * Main is bound. `replaced` is the previous binding when it named a
177
+ * different account: everything owned by that account (quota, profile,
178
+ * backoff, cache affinity) must be dropped, and outstanding observations
179
+ * for it fenced, even though the logical route stays the same.
180
+ */
181
+ {
182
+ status: 'ready';
183
+ binding: VaultPrimaryBinding;
184
+ replaced?: VaultPrimaryBinding;
185
+ }
186
+ /** A complete reply lists no primary record: there is no main. */
187
+ | {
188
+ status: 'absent';
189
+ }
190
+ /**
191
+ * The reply cannot say who main is. Not the same as absent: the last
192
+ * verified binding is handed back untouched, and serving it still needs a
193
+ * fresh receipt that matches it.
194
+ */
195
+ | {
196
+ status: 'unavailable';
197
+ reason: VaultPrimaryUnavailableReason;
198
+ lastVerified?: VaultPrimaryBinding;
199
+ };
200
+ /**
201
+ * The seam for a plugin that keeps a main account: one fixed account the host
202
+ * owns under a logical route, outside the pool's rotating rows. The library
203
+ * has no main convention of its own: the plugin names the
204
+ * vault record that holds the role (`primaryCredentialId`) and its logical
205
+ * route. Main follows the account that record claims; another credential of
206
+ * that same account may serve it when the record itself is not active, but a
207
+ * credential of any other account is never promoted into main.
208
+ *
209
+ * Inputs are the plugin's filtered inventory, whether that inventory is
210
+ * complete (defaults to the reply having no unusable records; pass the
211
+ * roster's `complete` or false to override), the previous verified binding
212
+ * and, on the request path, the account the chosen request expects.
213
+ */
214
+ export declare function resolveVaultPrimary(input: {
215
+ inventory: VaultInventory;
216
+ complete?: boolean;
217
+ previous?: VaultPrimaryBinding;
218
+ expectedAccountIdentity?: string;
219
+ primaryCredentialId: string;
220
+ routeId: string;
221
+ }): VaultPrimary;
@@ -13,7 +13,7 @@ export interface RefreshFileLock {
13
13
  whenLost(): Promise<LockLoss>;
14
14
  hasLost(): boolean;
15
15
  }
16
- export declare function acquireRefreshFileLock(options: {
16
+ type RefreshFileLockOptions = {
17
17
  name: string;
18
18
  ttlMs: number;
19
19
  /**
@@ -22,7 +22,6 @@ export declare function acquireRefreshFileLock(options: {
22
22
  * be one host's path.
23
23
  */
24
24
  path: string;
25
- now?: () => number;
26
25
  renew?: boolean;
27
26
  renewIntervalMs?: number;
28
27
  /** Notifies a live-owner refusal without awaiting; observer failures are ignored. */
@@ -30,4 +29,11 @@ export declare function acquireRefreshFileLock(options: {
30
29
  /** Synchronous, never awaited; throws and returned promise/thenable rejections are isolated. */
31
30
  onRenewalTimer?: (event: 'scheduled' | 'cancelled') => void;
32
31
  onStep?: (step: 'stale-marker-stat' | 'stale-marker-claimed' | 'stale-lock-observed' | 'lock-read-ready' | 'lock-missing' | 'stale-lock-confirmed' | 'stale-lock-removed' | 'stale-lock-recreated' | 'eviction-marker-acquired' | 'renewal-owner-confirmed' | 'renewal-marker-unavailable' | 'renewal-write-fenced' | 'renewal-write-ready' | 'renewal-write-finished' | 'relinquish-read' | 'renewal-finished' | 'release-owner-confirmed') => void | Promise<void>;
32
+ };
33
+ /** Filesystem leases share wall time across all callers and processes. */
34
+ export declare function acquireRefreshFileLock(options: RefreshFileLockOptions): Promise<RefreshFileLock | null>;
35
+ /** @internal Clock seam for lease tests; not exported by the package's fs entry point. */
36
+ export declare function acquireRefreshFileLockWithClock(options: RefreshFileLockOptions & {
37
+ now?: () => number;
33
38
  }): Promise<RefreshFileLock | null>;
39
+ export {};
@@ -21,6 +21,26 @@ export interface QuotaTextOptions {
21
21
  /** Defaults to `QUOTA_STALE_AFTER_MS`. */
22
22
  staleAfterMs?: number;
23
23
  }
24
+ export interface QuotaWindowPart {
25
+ kind: 'window' | 'budget';
26
+ /** The display name of a quota window, or `credits` for the budget. */
27
+ name: string;
28
+ /** The shared figure wording, including `credits` for budget figures. */
29
+ figure: string;
30
+ /** The unrounded remaining percentage, clamped to 0–100, when known. */
31
+ remainingPercent?: number;
32
+ /** A bare relative reset span such as `4d`, only when the reset is future. */
33
+ resetIn?: string;
34
+ checkedAt: number;
35
+ /** Whether the reset followed this reading and has arrived by `now`. */
36
+ resetSinceReading: boolean;
37
+ }
38
+ export interface QuotaParts {
39
+ /** Reported windows in display order, followed by the budget when present. */
40
+ windows: QuotaWindowPart[];
41
+ /** The shared checked-age wording, present only when the reading is stale. */
42
+ age?: string;
43
+ }
24
44
  /**
25
45
  * The name a window is shown under. A general window (scope `all`) is named
26
46
  * by its stored length; a model-scoped window keeps the label the plugin
@@ -29,6 +49,12 @@ export interface QuotaTextOptions {
29
49
  * were recorded) falls back to its label.
30
50
  */
31
51
  export declare function quotaWindowName(limit: Pick<ProjectedLimit, 'scope' | 'label' | 'windowMinutes'>): string;
52
+ /**
53
+ * Structured quota wording for layouts that need one row per window. The
54
+ * windows use the same filtering and order as `quotaTextParts`; future resets
55
+ * and the optional age keep the shared formatter's wording.
56
+ */
57
+ export declare function quotaWindowParts(quota: ProjectedQuota | undefined, options: QuotaTextOptions): QuotaParts;
32
58
  /**
33
59
  * The pieces of a quota text, in order: one per reported window
34
60
  * (`5h 58% left, resets 2h`), then the credit budget when there is one, then
@@ -1,7 +1,7 @@
1
1
  export type { QuotaCodec } from './codec.js';
2
2
  export { quotaCodec } from './codec.js';
3
- export type { QuotaTextForm, QuotaTextOptions } from './format.js';
4
- export { formatQuota, NO_LIMITS_REPORTED, NO_QUOTA_READING, QUOTA_STALE_AFTER_MS, quotaTextParts, quotaWindowName, } from './format.js';
3
+ export type { QuotaParts, QuotaTextForm, QuotaTextOptions, QuotaWindowPart, } from './format.js';
4
+ export { formatQuota, NO_LIMITS_REPORTED, NO_QUOTA_READING, QUOTA_STALE_AFTER_MS, quotaTextParts, quotaWindowName, quotaWindowParts, } from './format.js';
5
5
  export type { CreditBudgetCleared, CreditBudgetEntry, CreditBudgetReading, QuotaAbsentEntry, QuotaCounter, QuotaEntry, QuotaMap, QuotaReadingEntry, QuotaRetiredEntry, } from './map.js';
6
6
  export { ALL_SCOPE, DEFAULT_REQUIRED_LABELS, decodeQuotaMap, emptyQuotaMap, isQuotaMap, } from './map.js';
7
7
  export type { ObservedBudget, ObservedPair, ObservedReading, QuotaObservation, } from './merge.js';
@@ -0,0 +1,93 @@
1
+ import { type QuotaMap } from '../quota/map.js';
2
+ import { type ProjectedQuota } from '../quota/projection.js';
3
+ export type RowKind = 'oauth' | 'api-key';
4
+ export interface RoutingRow {
5
+ id: string;
6
+ kind: RowKind;
7
+ /** The row's stored quota map; absent is the same as an empty map. */
8
+ quota?: QuotaMap;
9
+ }
10
+ export interface WindowRef {
11
+ scope: string;
12
+ label: string;
13
+ }
14
+ export interface ExclusionInputs {
15
+ /** Row id to the time (ms) a rate-limit mark expires. */
16
+ rateLimitMarks?: ReadonlyMap<string, number>;
17
+ /** Row id to the time (ms) a failed refresh may be retried. */
18
+ refreshBackoff?: ReadonlyMap<string, number>;
19
+ }
20
+ export interface AdmissionInput extends ExclusionInputs {
21
+ rows: readonly RoutingRow[];
22
+ /** `all` (the default) or a model family. */
23
+ scope?: string;
24
+ /** Labels the scope requires an entry for; defaults to `['primary']`. */
25
+ requiredLabels?: readonly string[];
26
+ now: number;
27
+ /**
28
+ * Called synchronously, once per refusal at gates 2 to 4, with the row id.
29
+ * Its return value is ignored and never awaited, so a pull the caller
30
+ * starts from here cannot delay the refusal.
31
+ */
32
+ requestPull?: (rowId: string) => void;
33
+ }
34
+ export type AdmissionRefusal = {
35
+ id: string;
36
+ stage: 1;
37
+ gate: 2;
38
+ reason: 'needs-first-reading';
39
+ pullRequested: true;
40
+ } | {
41
+ id: string;
42
+ stage: 1;
43
+ gate: 3;
44
+ reason: 'unknown-window';
45
+ window: WindowRef;
46
+ pullRequested: true;
47
+ } | {
48
+ id: string;
49
+ stage: 1;
50
+ gate: 4;
51
+ reason: 'unknown-reset';
52
+ window: WindowRef;
53
+ pullRequested: true;
54
+ } | {
55
+ id: string;
56
+ stage: 1;
57
+ gate: 5;
58
+ reason: 'exhausted';
59
+ window: WindowRef;
60
+ resetsAt: string;
61
+ resetAtMs: number;
62
+ } | {
63
+ id: string;
64
+ stage: 2;
65
+ reason: 'budget-spent';
66
+ resetsAt: string;
67
+ resetAtMs: number;
68
+ };
69
+ export interface AdmissionExclusion {
70
+ id: string;
71
+ reason: 'rate-limited' | 'refresh-backoff';
72
+ until: number;
73
+ }
74
+ export interface AdmittedRow {
75
+ id: string;
76
+ kind: RowKind;
77
+ /** The projection admission judged; absent for API-key rows. */
78
+ projection?: ProjectedQuota;
79
+ /** Set when a spent budget was kept because no other path survived. */
80
+ lastPath?: true;
81
+ }
82
+ export interface AdmissionResult {
83
+ /** In input order. */
84
+ admitted: AdmittedRow[];
85
+ refused: AdmissionRefusal[];
86
+ excluded: AdmissionExclusion[];
87
+ /** Ids a pull was requested for, in input order. */
88
+ pulls: string[];
89
+ }
90
+ /** The exclusion that applies to `id` at `now`, if any. */
91
+ export declare function exclusionFor(id: string, inputs: ExclusionInputs, now: number): AdmissionExclusion | undefined;
92
+ /** Runs both admission stages over `input.rows`. */
93
+ export declare function admit(input: AdmissionInput): AdmissionResult;
@@ -0,0 +1,8 @@
1
+ export type { AdmissionExclusion, AdmissionInput, AdmissionRefusal, AdmissionResult, AdmittedRow, ExclusionInputs, RoutingRow, RowKind, WindowRef, } from './admission.js';
2
+ export { admit, exclusionFor } from './admission.js';
3
+ export type { OrderedAttempt, OrderedPlacement, OrderedRoute, OrderedRouteInput, ResolvedRoutingMode, RoutingMode, } from './ordered.js';
4
+ export { DEFAULT_FORMER_MAIN_ID, nextOrderedAttempt, orderForPlacement, resolveRoutingMode, routeOrdered, } from './ordered.js';
5
+ export type { StickyPin } from './pins.js';
6
+ export { isPinValid, pendingBytesForPins } from './pins.js';
7
+ export type { PinAction, StickyBreakDecision, StickyRoute, StickyRouteInput, StickySelection, StickySelectionCandidate, StickySelectionInput, StickyStatusClass, StickyStatusClassifier, } from './sticky.js';
8
+ export { decideStickyBreak, defaultStickyStatusClass, MIN_RESET_HOURS, MIN_WEIGHT, QUOTA_STALENESS_MS, routeSticky, selectStickyCandidate, snapshotCheckedAt, sustainableWindowWeight, } from './sticky.js';
@@ -0,0 +1,47 @@
1
+ import { type AdmissionInput, type AdmissionResult } from './admission.js';
2
+ export type RoutingMode = 'ordered' | 'sticky-balanced';
3
+ /** Where `ordered` routing places the former main row. */
4
+ export type OrderedPlacement = 'roster' | 'main-first' | 'fallback-first';
5
+ export interface ResolvedRoutingMode {
6
+ mode: RoutingMode;
7
+ placement: OrderedPlacement;
8
+ }
9
+ /** The former main row's id when the caller supplies none. */
10
+ export declare const DEFAULT_FORMER_MAIN_ID = "main";
11
+ /**
12
+ * Resolves a persisted `routing.mode` value. `main-first` and
13
+ * `fallback-first` are aliases of `ordered` that move the former main row;
14
+ * an absent or unrecognised value is `ordered` in roster order. The
15
+ * persisted value is only read here, never rewritten.
16
+ */
17
+ export declare function resolveRoutingMode(value: unknown): ResolvedRoutingMode;
18
+ /**
19
+ * Orders `ids` (in roster order) for a placement: `main-first` moves the row
20
+ * named `formerMainId` to the front, `fallback-first` to the back, and
21
+ * `roster` (or a missing former main row) keeps roster order.
22
+ */
23
+ export declare function orderForPlacement(ids: readonly string[], placement: OrderedPlacement, formerMainId?: string): string[];
24
+ export interface OrderedRouteInput extends AdmissionInput {
25
+ placement?: OrderedPlacement;
26
+ formerMainId?: string;
27
+ /** Killswitch verdict per row; `false` drops the row, a missing row passes. */
28
+ killswitch?: ReadonlyMap<string, boolean>;
29
+ }
30
+ export interface OrderedRoute {
31
+ /** Admitted, non-killed row ids in the order they are to be tried. */
32
+ order: string[];
33
+ admission: AdmissionResult;
34
+ }
35
+ export declare function routeOrdered(input: OrderedRouteInput): OrderedRoute;
36
+ export interface OrderedAttempt {
37
+ id: string;
38
+ /** The response status; undefined when no response arrived. */
39
+ status?: number;
40
+ }
41
+ /**
42
+ * The next row to try, given the attempts so far: the first row when none
43
+ * has been tried, the next untried row when the last attempt's status is one
44
+ * of `retryStatuses`, and undefined otherwise (the last response stands) or
45
+ * when every row has been tried.
46
+ */
47
+ export declare function nextOrderedAttempt(order: readonly string[], attempts: readonly OrderedAttempt[], retryStatuses: readonly number[]): string | undefined;
@@ -0,0 +1,22 @@
1
+ export interface StickyPin {
2
+ accountId: string;
3
+ /** The row's recorded wire identity when the pin was made, if known. */
4
+ wireIdentity?: string;
5
+ /** The request size recorded with the pin; weighs pending bytes. */
6
+ inputBytes?: number;
7
+ /** The projection time the placement was judged on. */
8
+ quotaCheckedAt?: number;
9
+ }
10
+ /**
11
+ * A pin survives while its row id is in `validIds` and the row still holds
12
+ * the account it was placed on. Only two known, differing identities prove
13
+ * the account changed: an unknown identity on either side keeps the pin,
14
+ * because re-placing a session on missing evidence throws its cache away.
15
+ */
16
+ export declare function isPinValid(pin: StickyPin, validIds: ReadonlySet<string>, currentIdentity: string | undefined): boolean;
17
+ /**
18
+ * Sums the request bytes of every other session's pin per row, as openai-auth
19
+ * does. A pin counts only while it was judged on the row's current
20
+ * projection time, so a fresh reading resets the row's pending load.
21
+ */
22
+ export declare function pendingBytesForPins(pins: Iterable<readonly [sessionKey: string, pin: StickyPin]>, quotaCheckedAtById: ReadonlyMap<string, number | undefined>, excludedSessionKey?: string): Map<string, number>;
@@ -0,0 +1,158 @@
1
+ import { type ProjectedQuota } from '../quota/projection.js';
2
+ import { type AdmissionInput, type AdmissionRefusal, type AdmissionResult, type RoutingRow, type WindowRef } from './admission.js';
3
+ import { type StickyPin } from './pins.js';
4
+ export declare const QUOTA_STALENESS_MS: number;
5
+ export declare const MIN_RESET_HOURS: number;
6
+ export declare const MIN_WEIGHT = 0.000001;
7
+ /** The projection's time, else the caller's cache-entry time. */
8
+ export declare function snapshotCheckedAt(quota: ProjectedQuota | null | undefined, entryCheckedAt?: number): number | undefined;
9
+ export type StickyBreakDecision = {
10
+ action: 'retain';
11
+ reason: 'unknown' | 'stale' | 'healthy' | 'transient';
12
+ } | {
13
+ action: 'migrate';
14
+ reason: 'exhausted' | 'permanent' | 'killswitch';
15
+ /** The exhausted limit, named by its (scope, label), not its position. */
16
+ window?: WindowRef;
17
+ resetsAt?: string;
18
+ };
19
+ /**
20
+ * How the adapter reads a failed response's HTTP status for the break
21
+ * decision. `permanent` moves the session off its row before any quota is
22
+ * consulted; `transient` and `healthy` keep it unless the quota says the row
23
+ * is spent. A missing status means the request failed without a response.
24
+ */
25
+ export type StickyStatusClass = 'permanent' | 'transient' | 'healthy';
26
+ export type StickyStatusClassifier = (status: number | undefined) => StickyStatusClass;
27
+ /**
28
+ * The status rule used when the adapter supplies none, carried from
29
+ * openai-auth: 401 and 403 are permanent; no response, 0, a non-finite
30
+ * status, 429 and 5xx are transient; anything else leaves the row healthy.
31
+ * A provider whose 403 can mean an organisation or model policy rather than
32
+ * a dead account supplies its own classifier and can fall back to this one
33
+ * for the statuses it does not single out.
34
+ */
35
+ export declare function defaultStickyStatusClass(status: number | undefined): StickyStatusClass;
36
+ /** Classifies whether a pinned session should leave its row after a failure. */
37
+ export declare function decideStickyBreak(input: {
38
+ quota: ProjectedQuota | null | undefined;
39
+ quotaCheckedAt?: number;
40
+ status?: number;
41
+ now: number;
42
+ killswitchPasses?: boolean;
43
+ /** Defaults to `defaultStickyStatusClass`. */
44
+ classifyStatus?: StickyStatusClassifier;
45
+ }): StickyBreakDecision;
46
+ export declare function sustainableWindowWeight(window: {
47
+ remainingPercent: number;
48
+ resetsAt?: string;
49
+ }, reservePercent: number, now: number): number;
50
+ export interface StickySelectionCandidate {
51
+ accountId: string;
52
+ quota: ProjectedQuota | null | undefined;
53
+ quotaCheckedAt?: number;
54
+ /** Reserve percent per window label; a missing label reserves nothing. */
55
+ reservePercent: Readonly<Record<string, number>>;
56
+ configuredOrder: number;
57
+ resetCreditsApplicable?: number;
58
+ /** `false` excludes the candidate from weighted and fallback placement. */
59
+ killswitchPasses?: boolean;
60
+ }
61
+ export interface StickySelectionInput {
62
+ candidates: readonly StickySelectionCandidate[];
63
+ pendingBytes: ReadonlyMap<string, number>;
64
+ requestBytes: number;
65
+ now: number;
66
+ onEmptyWeightedSet?: () => void;
67
+ }
68
+ export interface StickySelection {
69
+ accountId: string;
70
+ quotaCheckedAt?: number;
71
+ source: 'weighted' | 'mode-fallback';
72
+ }
73
+ /**
74
+ * Places a session: the lowest projected pressure among candidates with a
75
+ * fresh positive weight, else (`mode-fallback`) the first candidate in
76
+ * configured order, preferring one with an applicable reset credit.
77
+ */
78
+ export declare function selectStickyCandidate(input: StickySelectionInput): StickySelection | undefined;
79
+ /** Reserve percent per window label; a missing label reserves nothing. */
80
+ export type ReservePercent = Readonly<Record<string, number>>;
81
+ /**
82
+ * Reserve percentages per row: a map keyed by row id, or a function of the
83
+ * row. A row the map lacks, or for which the function returns undefined,
84
+ * takes the shared `reservePercent`.
85
+ */
86
+ export type RowReservePercent = ReadonlyMap<string, ReservePercent> | ((row: RoutingRow) => ReservePercent | undefined);
87
+ /**
88
+ * What a valid pin does when its row is not dispatched.
89
+ *
90
+ * `keep`: the pin is retained whatever kept its row from this request.
91
+ *
92
+ * `move-on-confirmed-exhaustion`: the pin moves to the row this request is
93
+ * dispatched to when its own row was refused as confirmed exhausted (a spent
94
+ * window with a future reset, or a spent credit budget) or killed by the
95
+ * killswitch. A refusal for unknown quota (no reading yet, a missing window,
96
+ * an exhausted reading without a usable reset) and an exclusion (rate-limit
97
+ * mark, refresh backoff) keep the pin while this request is served elsewhere.
98
+ * With no admissible row the pin is retained either way.
99
+ */
100
+ export type RefusedPinPolicy = 'keep' | 'move-on-confirmed-exhaustion';
101
+ export interface StickyRouteInput extends AdmissionInput {
102
+ requestBytes: number;
103
+ /** Bytes already committed per row, for example from other sessions' pins. */
104
+ pendingBytes?: ReadonlyMap<string, number>;
105
+ /** Killswitch verdict per row; a missing row passes. */
106
+ killswitch?: ReadonlyMap<string, boolean>;
107
+ /** Reserve percent per window label, for every row without its own. */
108
+ reservePercent?: ReservePercent;
109
+ /** Per-row reserves, which replace `reservePercent` for the rows they cover. */
110
+ rowReservePercent?: RowReservePercent;
111
+ /** Defaults to `keep`. */
112
+ refusedPinPolicy?: RefusedPinPolicy;
113
+ resetCreditsApplicable?: ReadonlyMap<string, number>;
114
+ /** The session's current pin, if it has one. */
115
+ pin?: StickyPin;
116
+ /** Each row's recorded wire identity; missing means unknown. */
117
+ identities?: ReadonlyMap<string, string | undefined>;
118
+ onEmptyWeightedSet?: () => void;
119
+ }
120
+ /**
121
+ * What the caller does with the session's pin: keep it, replace it with
122
+ * `pin`, or drop it. Under the default `keep` policy a valid pin is always
123
+ * kept, even when this request was routed elsewhere because its row was
124
+ * refused, excluded or killed; `refusedPinPolicy` can move it instead.
125
+ */
126
+ export type PinAction = {
127
+ action: 'retain';
128
+ } | {
129
+ action: 'assign';
130
+ pin: StickyPin;
131
+ } | {
132
+ action: 'clear';
133
+ } | {
134
+ action: 'none';
135
+ };
136
+ export type StickyRoute = {
137
+ outcome: 'dispatch';
138
+ accountId: string;
139
+ source: 'pin' | 'weighted' | 'mode-fallback';
140
+ quotaCheckedAt?: number;
141
+ pin: PinAction;
142
+ /** Rows that selection chose and admission then refused, in selection order. */
143
+ refusedSelections: AdmissionRefusal[];
144
+ admission: AdmissionResult;
145
+ } | {
146
+ outcome: 'no-admissible-account';
147
+ pin: PinAction;
148
+ refusedSelections: AdmissionRefusal[];
149
+ admission: AdmissionResult;
150
+ };
151
+ /**
152
+ * Routes one request in `sticky-balanced` mode. A valid pin whose row is
153
+ * admitted, not excluded and not killed is dispatched as is. Otherwise
154
+ * selection runs over the non-excluded rows; each selected row admission
155
+ * refused is removed and selection re-runs, so the loop ends either on an
156
+ * admitted row or with no admissible account.
157
+ */
158
+ export declare function routeSticky(input: StickyRouteInput): StickyRoute;