@opengeni/subscriptions 2.0.1 → 2.0.2-canary.38789554071001
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/adapter.d.ts +17 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +88 -2
- package/dist/index.js.map +1 -1
- package/dist/quota.d.ts +18 -1
- package/dist/reference-model.d.ts +1 -1
- package/dist/reference.js +12 -1
- package/dist/reference.js.map +1 -1
- package/dist/types.d.ts +23 -0
- package/dist/wait-presentation.d.ts +4 -0
- package/package.json +1 -1
- package/src/adapter.ts +17 -0
- package/src/index.ts +1 -0
- package/src/placement.ts +5 -0
- package/src/quota.ts +116 -3
- package/src/reference-model.ts +22 -1
- package/src/types.ts +24 -0
- package/src/wait-presentation.ts +27 -0
package/src/quota.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ModelId, SubscriptionConnection, SubscriptionQuota } from "./types";
|
|
1
|
+
import type { ModelId, QuotaBlockClaim, SubscriptionConnection, SubscriptionQuota } from "./types";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* What the shared quota model says about a connection's capacity now
|
|
@@ -73,6 +73,89 @@ export function modelCooldownUntil(
|
|
|
73
73
|
return until !== undefined && until > now ? until : null;
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
+
/** One stored block a usage reading may end: a model cooldown, or (model null) the exhaustion deadline. */
|
|
77
|
+
export type SubscriptionQuotaBlock = {
|
|
78
|
+
modelId: ModelId | null;
|
|
79
|
+
until: number;
|
|
80
|
+
/** Its claim, only while it still explains this exact deadline. */
|
|
81
|
+
claim: QuotaBlockClaim | null;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* The claim of a stored block (a model's cooldown, or with `modelId` null
|
|
86
|
+
* the exhaustion deadline), only while it is tied to the block's current
|
|
87
|
+
* deadline; null otherwise (fail closed).
|
|
88
|
+
*/
|
|
89
|
+
export function quotaBlockClaim(
|
|
90
|
+
quota: Pick<
|
|
91
|
+
SubscriptionQuota,
|
|
92
|
+
"modelCooldowns" | "modelCooldownClaims" | "exhaustedUntil" | "exhaustedClaim"
|
|
93
|
+
>,
|
|
94
|
+
modelId: ModelId | null,
|
|
95
|
+
): QuotaBlockClaim | null {
|
|
96
|
+
const until = modelId === null ? quota.exhaustedUntil : quota.modelCooldowns[modelId];
|
|
97
|
+
const claim = modelId === null ? quota.exhaustedClaim : quota.modelCooldownClaims?.[modelId];
|
|
98
|
+
return until !== null &&
|
|
99
|
+
until !== undefined &&
|
|
100
|
+
claim !== null &&
|
|
101
|
+
claim !== undefined &&
|
|
102
|
+
claim.until === until &&
|
|
103
|
+
claim.windowIds.length > 0
|
|
104
|
+
? claim
|
|
105
|
+
: null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** The quota document a store writes: claims only while they explain a block. */
|
|
109
|
+
export type StoredQuotaDocument = Pick<
|
|
110
|
+
SubscriptionQuota,
|
|
111
|
+
| "windows"
|
|
112
|
+
| "modelCooldowns"
|
|
113
|
+
| "modelCooldownClaims"
|
|
114
|
+
| "exhaustedUntil"
|
|
115
|
+
| "exhaustedKind"
|
|
116
|
+
| "exhaustedClaim"
|
|
117
|
+
| "source"
|
|
118
|
+
>;
|
|
119
|
+
|
|
120
|
+
/** The stored document of one quota (a claim that no longer explains its block is dropped). */
|
|
121
|
+
export function storedQuotaDocument(quota: SubscriptionQuota): StoredQuotaDocument {
|
|
122
|
+
const modelCooldownClaims: Record<string, QuotaBlockClaim> = {};
|
|
123
|
+
for (const modelId of Object.keys(quota.modelCooldowns)) {
|
|
124
|
+
const claim = quotaBlockClaim(quota, modelId);
|
|
125
|
+
if (claim) modelCooldownClaims[modelId] = claim;
|
|
126
|
+
}
|
|
127
|
+
const exhaustedClaim = quotaBlockClaim(quota, null);
|
|
128
|
+
return {
|
|
129
|
+
windows: quota.windows,
|
|
130
|
+
modelCooldowns: quota.modelCooldowns,
|
|
131
|
+
...(Object.keys(modelCooldownClaims).length > 0 ? { modelCooldownClaims } : {}),
|
|
132
|
+
exhaustedUntil: quota.exhaustedUntil,
|
|
133
|
+
exhaustedKind: quota.exhaustedKind,
|
|
134
|
+
...(exhaustedClaim ? { exhaustedClaim } : {}),
|
|
135
|
+
source: quota.source,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* The claim of a block merged from several observations at `until`: every
|
|
141
|
+
* source whose own block has that exact deadline must carry a claim tied to
|
|
142
|
+
* it (their window ids are joined); otherwise none (an unexplained limit
|
|
143
|
+
* with the same deadline keeps the block).
|
|
144
|
+
*/
|
|
145
|
+
function mergedClaim(
|
|
146
|
+
until: number,
|
|
147
|
+
sources: readonly { until: number | null | undefined; claim: QuotaBlockClaim | null }[],
|
|
148
|
+
): QuotaBlockClaim | null {
|
|
149
|
+
const deciding = sources.filter((source) => source.until === until);
|
|
150
|
+
if (deciding.length === 0 || deciding.some((source) => source.claim === null)) return null;
|
|
151
|
+
const claims = deciding.map((source) => source.claim!);
|
|
152
|
+
return {
|
|
153
|
+
until,
|
|
154
|
+
observedAt: Math.max(...claims.map((claim) => claim.observedAt)),
|
|
155
|
+
windowIds: [...new Set(claims.flatMap((claim) => claim.windowIds))],
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
76
159
|
/**
|
|
77
160
|
* A quota observation applies only to the credential generation it was made
|
|
78
161
|
* with, so a refusal seen before a refresh cannot quarantine the renewed
|
|
@@ -140,6 +223,31 @@ export function applyQuotaObservation(
|
|
|
140
223
|
current.exhaustedUntil !== null &&
|
|
141
224
|
current.exhaustedUntil > observedAt &&
|
|
142
225
|
(observation.exhaustedUntil === null || observation.exhaustedUntil < current.exhaustedUntil);
|
|
226
|
+
const exhaustedUntil = keepCurrentDeadline ? current.exhaustedUntil : observation.exhaustedUntil;
|
|
227
|
+
// A block keeps its evidence only where every observation that set its
|
|
228
|
+
// merged deadline explained it (`QuotaBlockClaim`).
|
|
229
|
+
const sources = [current, observation];
|
|
230
|
+
const modelCooldownClaims: Record<string, QuotaBlockClaim> = {};
|
|
231
|
+
for (const [modelId, until] of Object.entries(modelCooldowns)) {
|
|
232
|
+
const claim = mergedClaim(
|
|
233
|
+
until,
|
|
234
|
+
sources.map((source) => ({
|
|
235
|
+
until: source.modelCooldowns[modelId],
|
|
236
|
+
claim: quotaBlockClaim(source, modelId),
|
|
237
|
+
})),
|
|
238
|
+
);
|
|
239
|
+
if (claim) modelCooldownClaims[modelId] = claim;
|
|
240
|
+
}
|
|
241
|
+
const exhaustedClaim =
|
|
242
|
+
exhaustedUntil === null
|
|
243
|
+
? null
|
|
244
|
+
: mergedClaim(
|
|
245
|
+
exhaustedUntil,
|
|
246
|
+
sources.map((source) => ({
|
|
247
|
+
until: source.exhaustedUntil,
|
|
248
|
+
claim: quotaBlockClaim(source, null),
|
|
249
|
+
})),
|
|
250
|
+
);
|
|
143
251
|
const reported = new Set(observation.windows.map((window) => window.id));
|
|
144
252
|
const omittedExhausted =
|
|
145
253
|
observation.source === "usage_endpoint" && options.usageReadingsComplete === true
|
|
@@ -151,13 +259,18 @@ export function applyQuotaObservation(
|
|
|
151
259
|
window.resetsAt !== null &&
|
|
152
260
|
window.resetsAt > observedAt,
|
|
153
261
|
);
|
|
154
|
-
|
|
262
|
+
const merged: SubscriptionQuota = {
|
|
155
263
|
...observation,
|
|
156
264
|
windows: omittedExhausted.length
|
|
157
265
|
? [...observation.windows, ...omittedExhausted]
|
|
158
266
|
: observation.windows,
|
|
159
267
|
modelCooldowns,
|
|
160
|
-
exhaustedUntil
|
|
268
|
+
exhaustedUntil,
|
|
161
269
|
exhaustedKind: keepCurrentDeadline ? current.exhaustedKind : observation.exhaustedKind,
|
|
162
270
|
};
|
|
271
|
+
delete merged.modelCooldownClaims;
|
|
272
|
+
delete merged.exhaustedClaim;
|
|
273
|
+
if (Object.keys(modelCooldownClaims).length > 0) merged.modelCooldownClaims = modelCooldownClaims;
|
|
274
|
+
if (exhaustedClaim) merged.exhaustedClaim = exhaustedClaim;
|
|
275
|
+
return merged;
|
|
163
276
|
}
|
package/src/reference-model.ts
CHANGED
|
@@ -143,6 +143,7 @@ export type SwitchKind =
|
|
|
143
143
|
|
|
144
144
|
export type WaitReason =
|
|
145
145
|
| "pinned_account_unavailable"
|
|
146
|
+
| "pinned_account_disabled"
|
|
146
147
|
/** The explicit choice can never serve this work until a person changes something (D-24). */
|
|
147
148
|
| "pinned_account_ineligible"
|
|
148
149
|
| "no_eligible_capacity"
|
|
@@ -566,6 +567,21 @@ export function pinnedNeverServes(
|
|
|
566
567
|
return !isAuthorized(world, session, { ...pinned, healthy: true, allocatorEnabled: true });
|
|
567
568
|
}
|
|
568
569
|
|
|
570
|
+
function pinnedAllocationDisabled(world: World, session: Session, pinned: Connection): boolean {
|
|
571
|
+
if (!pinned.allocatorEnabled) return true;
|
|
572
|
+
if (pinned.assignmentPolicies === undefined) return false;
|
|
573
|
+
const source =
|
|
574
|
+
effectiveSettings(world.settings, session.workspaceId).values.inferenceSource?.[
|
|
575
|
+
pinned.provider
|
|
576
|
+
] ?? "automatic";
|
|
577
|
+
return !pinned.assignmentPolicies.some(
|
|
578
|
+
(policy) =>
|
|
579
|
+
policy.workspaceId === session.workspaceId &&
|
|
580
|
+
(source === "automatic" || policy.inferencePool === source) &&
|
|
581
|
+
policy.allocatorEnabled,
|
|
582
|
+
);
|
|
583
|
+
}
|
|
584
|
+
|
|
569
585
|
/** Future times at which some quota or model cooldown in the world resets, ascending. */
|
|
570
586
|
function futureResetTimes(world: World, now: number): number[] {
|
|
571
587
|
const times = new Set<number>();
|
|
@@ -652,6 +668,9 @@ export function decide(world: World, sessionId: string, now: number): Decision {
|
|
|
652
668
|
if (pinnedNeverServes(world, session, pinned)) {
|
|
653
669
|
return { kind: "wait", reason: "pinned_account_ineligible", earliestResetAt: null };
|
|
654
670
|
}
|
|
671
|
+
if (pinned && pinnedAllocationDisabled(world, session, pinned)) {
|
|
672
|
+
return { kind: "wait", reason: "pinned_account_disabled", earliestResetAt: null };
|
|
673
|
+
}
|
|
655
674
|
// Otherwise the earliest future time the chosen account can serve (D-22).
|
|
656
675
|
return {
|
|
657
676
|
kind: "wait",
|
|
@@ -906,7 +925,9 @@ export function checkDecision(
|
|
|
906
925
|
: session.pinnedConnectionId
|
|
907
926
|
? never
|
|
908
927
|
? "pinned_account_ineligible"
|
|
909
|
-
:
|
|
928
|
+
: pinned && pinnedAllocationDisabled(world, session, pinned)
|
|
929
|
+
? "pinned_account_disabled"
|
|
930
|
+
: "pinned_account_unavailable"
|
|
910
931
|
: "no_eligible_capacity";
|
|
911
932
|
if (decision.reason !== expectedReason) {
|
|
912
933
|
fail(
|
package/src/types.ts
CHANGED
|
@@ -101,12 +101,34 @@ export type QuotaWindow = {
|
|
|
101
101
|
|
|
102
102
|
export type QuotaSource = "usage_endpoint" | "response_headers" | "refusal";
|
|
103
103
|
|
|
104
|
+
/**
|
|
105
|
+
* Which reported limits one stored block (a model cooldown, or the quota
|
|
106
|
+
* exhaustion deadline) came from: the provider's own window ids, as the
|
|
107
|
+
* observation that set the block named them, tied to that block's exact
|
|
108
|
+
* deadline. A block whose deadline no longer equals `until` (a later write
|
|
109
|
+
* extended it without evidence) has no claim. A block without a claim (an
|
|
110
|
+
* unrecognized or missing limit, or a refusal without usage evidence) never
|
|
111
|
+
* ends before its own deadline.
|
|
112
|
+
*/
|
|
113
|
+
export type QuotaBlockClaim = {
|
|
114
|
+
/** The deadline of the block this claim explains. */
|
|
115
|
+
until: number;
|
|
116
|
+
/** When the observation that set the block was made. */
|
|
117
|
+
observedAt: number;
|
|
118
|
+
/** The provider window ids whose limit set the block (never empty). */
|
|
119
|
+
windowIds: readonly string[];
|
|
120
|
+
};
|
|
121
|
+
|
|
104
122
|
export type SubscriptionQuota = {
|
|
105
123
|
windows: readonly QuotaWindow[];
|
|
106
124
|
/** Per-model cooldowns (model-specific limits some providers report). */
|
|
107
125
|
modelCooldowns: Readonly<Record<ModelId, number>>;
|
|
126
|
+
/** Optional evidence per model cooldown (see `QuotaBlockClaim`); absent: none. */
|
|
127
|
+
modelCooldownClaims?: Readonly<Record<ModelId, QuotaBlockClaim>>;
|
|
108
128
|
exhaustedUntil: number | null;
|
|
109
129
|
exhaustedKind: "quota" | "rate_limit" | null;
|
|
130
|
+
/** Optional evidence for `exhaustedUntil` (see `QuotaBlockClaim`); absent: none. */
|
|
131
|
+
exhaustedClaim?: QuotaBlockClaim | null;
|
|
110
132
|
revision: number;
|
|
111
133
|
observedAt: number | null;
|
|
112
134
|
observedRefreshGeneration: number | null;
|
|
@@ -315,6 +337,8 @@ export type AccountSwitchReason =
|
|
|
315
337
|
|
|
316
338
|
export type WaitReason =
|
|
317
339
|
| "pinned_account_unavailable"
|
|
340
|
+
/** Allocation was explicitly switched off; a person must enable it or change the pin. */
|
|
341
|
+
| "pinned_account_disabled"
|
|
318
342
|
/** The explicit choice can never serve this work until a person changes something (D-24). */
|
|
319
343
|
| "pinned_account_ineligible"
|
|
320
344
|
| "no_eligible_capacity"
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { WaitReason } from "./types";
|
|
2
|
+
|
|
3
|
+
/** Current wait guidance shared by initial parking and later reclassification.
|
|
4
|
+
* These are new audit events only; never rewrite an earlier model request. */
|
|
5
|
+
export function subscriptionCapacityWaitReasonText(reason: string, name: string): string | null {
|
|
6
|
+
const messages: Record<WaitReason, string> = {
|
|
7
|
+
pinned_account_unavailable: `The ${name} account chosen for this session is unavailable right now. This turn continues automatically once it is available again.`,
|
|
8
|
+
pinned_account_disabled: `The ${name} account pinned to this chat is turned off. Turn it back on, choose another account, or switch the chat to automatic. Waiting alone will not fix this.`,
|
|
9
|
+
pinned_account_ineligible: `The ${name} account chosen for this session can no longer serve this work. This turn continues once another account is chosen or the session is switched back to automatic.`,
|
|
10
|
+
no_eligible_capacity: `No ${name} subscription has capacity for this turn right now. It continues automatically when an account is available.`,
|
|
11
|
+
model_not_allowed: `This model is not allowed for the ${name} subscriptions available here. This turn continues if that changes.`,
|
|
12
|
+
compaction_provider_locked:
|
|
13
|
+
"This session is tied to a provider that has no capacity right now. It continues automatically when an account is available.",
|
|
14
|
+
accepted_authority_unavailable:
|
|
15
|
+
"This work was accepted before the account move and its account access could not be carried. Send it again to continue.",
|
|
16
|
+
};
|
|
17
|
+
return Object.hasOwn(messages, reason) ? messages[reason as WaitReason] : null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function subscriptionCapacityWaitNeedsAction(reason: string): boolean {
|
|
21
|
+
return [
|
|
22
|
+
"pinned_account_disabled",
|
|
23
|
+
"pinned_account_ineligible",
|
|
24
|
+
"model_not_allowed",
|
|
25
|
+
"accepted_authority_unavailable",
|
|
26
|
+
].includes(reason);
|
|
27
|
+
}
|