@inixiative/agent-session 0.3.1 → 0.5.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inixiative/agent-session",
3
- "version": "0.3.1",
3
+ "version": "0.5.0",
4
4
  "description": "Drive coding-agent CLIs as persistent, streaming, event-captured sessions over interchangeable transports, with primed decision sessions, subscription pools, routing, limit polling and continuity.",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -18,7 +18,7 @@
18
18
  "prepublishOnly": "bun run check",
19
19
  "measure:primed": "bun scripts/measure-primed.ts",
20
20
  "lint": "biome check .",
21
- "prepare": "lefthook install"
21
+ "prepare": "if git rev-parse --git-dir >/dev/null 2>&1; then lefthook install; fi"
22
22
  },
23
23
  "keywords": [
24
24
  "claude-code",
@@ -34,9 +34,12 @@
34
34
  "eval"
35
35
  ],
36
36
  "license": "MIT",
37
+ "dependencies": {
38
+ "@inixiative/json-rules": "^3.5.0"
39
+ },
37
40
  "devDependencies": {
38
41
  "@biomejs/biome": "2.5.2",
39
- "@inixiative/config": "^0.5.41",
42
+ "@inixiative/config": "^0.5.63",
40
43
  "@types/bun": "1.4.2",
41
44
  "lefthook": "2.1.9",
42
45
  "typescript": "6.0.3"
package/src/index.ts CHANGED
@@ -86,16 +86,27 @@ export {
86
86
  } from './primed';
87
87
  export {
88
88
  assessCandidate,
89
+ type BillingMode,
89
90
  type CandidateRequest,
91
+ DEFAULT_STRATEGY,
90
92
  type ExclusionReason,
93
+ type OrderBy,
94
+ type OrderingSpec,
95
+ type OrderingTerm,
91
96
  type RankedCandidate,
92
97
  type RepositoryRoute,
93
- type RoutingMode,
98
+ type ResolvedOrdering,
99
+ ROUTING_STRATEGIES,
100
+ type RoutingField,
94
101
  type RoutingRequest,
102
+ type RoutingStrategy,
95
103
  rankCandidates,
96
104
  rankSubscriptionAccounts,
97
105
  repositoryIdentity,
106
+ resolveOrdering,
107
+ type SortDir,
98
108
  type SubscriptionCandidate,
109
+ type UnknownUtilizationPolicy,
99
110
  } from './routing';
100
111
  // Transports: how a session reaches its model, with declared capabilities.
101
112
  export {
package/src/pool.ts CHANGED
@@ -20,8 +20,9 @@ import { type LimitSnapshot, mergeLimits } from './limits';
20
20
  import { probeClaudeLimits, probeCodexLimits } from './limits-probe';
21
21
  import {
22
22
  type ExclusionReason,
23
+ type OrderingSpec,
23
24
  type RankedCandidate,
24
- type RoutingMode,
25
+ type ResolvedOrdering,
25
26
  rankCandidates,
26
27
  type SubscriptionCandidate,
27
28
  } from './routing';
@@ -48,6 +49,8 @@ export interface InstanceConfig {
48
49
  readonly models?: Readonly<Record<string, readonly string[]>>;
49
50
  /** Concurrent leases. Default 1. */
50
51
  readonly concurrencyLimit?: number;
52
+ /** Lower serves first under a priority ordering. Default 0. */
53
+ readonly priority?: number;
51
54
  readonly enabled?: boolean;
52
55
  readonly authentication?: 'subscription' | 'api-key';
53
56
  /** Session config merged into every session opened on this instance (bin, spawn, SDK query…). */
@@ -60,7 +63,9 @@ export interface PoolRequest {
60
63
  readonly effort?: string;
61
64
  readonly organizationId?: string;
62
65
  readonly preferredInstanceId?: string;
63
- readonly mode?: RoutingMode;
66
+ readonly ordering?: OrderingSpec;
67
+ /** Only the preferred instance may serve. Requires preferredInstanceId. */
68
+ readonly pinned?: boolean;
64
69
  /** Restrict to these transports (e.g. only resumable ones). */
65
70
  readonly transports?: readonly TransportKind[];
66
71
  readonly excludeInstanceIds?: readonly string[];
@@ -90,8 +95,8 @@ export type PoolEvent =
90
95
  readonly leaseId: string;
91
96
  readonly instanceId: string;
92
97
  readonly transport: PooledTransport;
93
- readonly mode: RoutingMode;
94
- readonly utilizationPercent: number;
98
+ readonly ordering: ResolvedOrdering;
99
+ readonly utilizationPercent: number | null;
95
100
  readonly rank: number;
96
101
  }
97
102
  | { readonly type: 'released'; readonly leaseId: string; readonly instanceId: string }
@@ -288,6 +293,7 @@ export class SubscriptionPool {
288
293
  rank(request: PoolRequest): {
289
294
  candidates: RankedCandidate[];
290
295
  excluded: Record<string, ExclusionReason>;
296
+ ordering: ResolvedOrdering;
291
297
  } {
292
298
  const now = this._now();
293
299
  const effort = request.effort ?? 'default';
@@ -304,6 +310,7 @@ export class SubscriptionPool {
304
310
  models: state.config.models ?? { [request.model]: [effort] },
305
311
  activeRuns: state.leases.size,
306
312
  concurrencyLimit: state.config.concurrencyLimit ?? 1,
313
+ priority: state.config.priority ?? 0,
307
314
  windows: (limits?.windows ?? []).map((w) => ({
308
315
  usedPercent: w.usedPercent,
309
316
  reservedPercent: reserved,
@@ -323,7 +330,8 @@ export class SubscriptionPool {
323
330
  effort,
324
331
  now,
325
332
  maximumObservationAgeMs: this._maxAge,
326
- mode: request.mode,
333
+ ordering: request.ordering,
334
+ ...(request.pinned !== undefined ? { pinned: request.pinned } : {}),
327
335
  organizationId: request.organizationId,
328
336
  preferredId: request.preferredInstanceId,
329
337
  },
@@ -333,7 +341,7 @@ export class SubscriptionPool {
333
341
 
334
342
  /** Lease the best instance now, or throw PoolExhaustedError with every exclusion reason. */
335
343
  acquire(request: PoolRequest): Lease {
336
- const { candidates, excluded } = this.rank(request);
344
+ const { candidates, excluded, ordering } = this.rank(request);
337
345
  const best = candidates[0];
338
346
  if (!best) {
339
347
  this._emit({ type: 'exhausted', runtime: request.runtime, model: request.model, excluded });
@@ -346,12 +354,7 @@ export class SubscriptionPool {
346
354
  excluded,
347
355
  );
348
356
  }
349
- return this._lease(
350
- this._state(best.accountId),
351
- request.mode ?? 'quartile-balanced',
352
- best.utilizationPercent,
353
- 0,
354
- );
357
+ return this._lease(this._state(best.accountId), ordering, best.utilizationPercent, 0);
355
358
  }
356
359
 
357
360
  /** Refresh stale limits, lease an instance and open a session on it. The lease ends with the session. */
@@ -429,7 +432,7 @@ export class SubscriptionPool {
429
432
  );
430
433
  const allowed = new Set(shared.map((s) => s.config.id));
431
434
  await this._refreshStale(request, allowed);
432
- const { candidates, excluded } = this.rank({
435
+ const { candidates, excluded, ordering } = this.rank({
433
436
  ...request,
434
437
  excludeInstanceIds: [
435
438
  ...(request.excludeInstanceIds ?? []),
@@ -451,12 +454,7 @@ export class SubscriptionPool {
451
454
  );
452
455
  binding.session?.kill();
453
456
  binding.lease?.release();
454
- const lease = this._lease(
455
- this._state(best.accountId),
456
- request.mode ?? 'quartile-balanced',
457
- best.utilizationPercent,
458
- 0,
459
- );
457
+ const lease = this._lease(this._state(best.accountId), ordering, best.utilizationPercent, 0);
460
458
  try {
461
459
  const session = this._session(
462
460
  lease,
@@ -566,8 +564,8 @@ export class SubscriptionPool {
566
564
  }
567
565
  private _lease(
568
566
  state: InstanceState,
569
- mode: RoutingMode,
570
- utilizationPercent: number,
567
+ ordering: ResolvedOrdering,
568
+ utilizationPercent: number | null,
571
569
  rank: number,
572
570
  ): Lease {
573
571
  const leaseId = crypto.randomUUID();
@@ -593,7 +591,7 @@ export class SubscriptionPool {
593
591
  leaseId,
594
592
  instanceId: state.config.id,
595
593
  transport: state.config.transport,
596
- mode,
594
+ ordering,
597
595
  utilizationPercent,
598
596
  rank,
599
597
  });
package/src/routing.ts CHANGED
@@ -8,18 +8,84 @@
8
8
  // does not mint authority, read credentials or call a model, and a routing
9
9
  // preference never widens a grant (organization, runtime, model, billing).
10
10
  //
11
- // Strategies:
12
- // quartile-balanced (default) lowest usage quartile first; the preferred
13
- // (owning) instance wins within its quartile.
14
- // owner-first the preferred instance while eligible, then
15
- // spillover by utilization.
16
- // pinned the preferred instance only; otherwise none.
11
+ // Ordering is an ordered list of terms over the fields projected onto each
12
+ // eligible candidate, in `@inixiative/json-rules`' `OrderBy` vocabulary.
13
+ // Named strategies are presets over that list; a caller may pass its own terms
14
+ // instead. `id` is always the final term, so ranking is total and deterministic.
17
15
  //
18
- // Unknown, stale, future or reset-crossing limit observations exclude an
19
- // instance: routing never guesses capacity.
16
+ // Eligibility is separate from ordering: `pinned` restricts which instances may
17
+ // serve at all, and unknown, stale, future or reset-crossing limit observations
18
+ // mean routing has no capacity reading for an instance. By default that excludes
19
+ // it — routing never guesses capacity.
20
20
  // ---------------------------------------------------------------------------
21
21
 
22
- export type RoutingMode = 'quartile-balanced' | 'owner-first' | 'pinned';
22
+ import type { OrderBy, SortDir } from '@inixiative/json-rules';
23
+ import { orderRecords } from '@inixiative/json-rules';
24
+
25
+ export type { OrderBy, SortDir };
26
+
27
+ export type RoutingField = 'priority' | 'utilization' | 'quartile' | 'headroom' | 'preferred';
28
+
29
+ export interface OrderingTerm {
30
+ readonly field: RoutingField;
31
+ readonly dir?: SortDir;
32
+ }
33
+
34
+ export type RoutingStrategy =
35
+ | 'quartile-balanced'
36
+ | 'owner-first'
37
+ | 'priority-balanced'
38
+ | 'priority-strict'
39
+ | 'least-used';
40
+
41
+ export type OrderingSpec = RoutingStrategy | readonly OrderingTerm[];
42
+
43
+ /** Every term carries a direction. Assignable to json-rules' `OrderBy`. */
44
+ export type ResolvedOrdering = { field: RoutingField; dir: SortDir }[];
45
+
46
+ /** The direction that puts the better candidate first when a term omits one. */
47
+ const NATURAL_DIRECTION: Record<RoutingField, SortDir> = {
48
+ priority: 'asc',
49
+ utilization: 'asc',
50
+ quartile: 'asc',
51
+ headroom: 'desc',
52
+ preferred: 'desc',
53
+ };
54
+
55
+ export const ROUTING_STRATEGIES: Record<RoutingStrategy, readonly OrderingTerm[]> = {
56
+ 'quartile-balanced': [{ field: 'quartile' }, { field: 'preferred' }, { field: 'utilization' }],
57
+ 'owner-first': [{ field: 'preferred' }, { field: 'utilization' }],
58
+ // Load decides between tiers that differ by a quartile; priority decides within one.
59
+ 'priority-balanced': [
60
+ { field: 'quartile' },
61
+ { field: 'priority' },
62
+ { field: 'preferred' },
63
+ { field: 'utilization' },
64
+ ],
65
+ // Priority dominates: a lower tier serves only once every higher one is excluded.
66
+ 'priority-strict': [{ field: 'priority' }, { field: 'utilization' }],
67
+ 'least-used': [{ field: 'utilization' }],
68
+ };
69
+
70
+ export const DEFAULT_STRATEGY: RoutingStrategy = 'quartile-balanced';
71
+
72
+ const ROUTING_FIELDS = Object.keys(NATURAL_DIRECTION) as RoutingField[];
73
+
74
+ /** Resolve a strategy name or explicit term list into terms, each with a direction. */
75
+ export const resolveOrdering = (spec: OrderingSpec = DEFAULT_STRATEGY): ResolvedOrdering => {
76
+ if (typeof spec === 'string') {
77
+ const preset = ROUTING_STRATEGIES[spec];
78
+ if (!preset) throw Error(`Unknown routing strategy "${spec}"`);
79
+ return resolveOrdering(preset);
80
+ }
81
+ if (!spec.length) throw Error('Ordering requires at least one term');
82
+ return spec.map((term) => {
83
+ if (!ROUTING_FIELDS.includes(term.field)) throw Error(`Unknown ordering field "${term.field}"`);
84
+ if (term.dir !== undefined && term.dir !== 'asc' && term.dir !== 'desc')
85
+ throw Error(`Unknown sort direction "${term.dir}"`);
86
+ return { field: term.field, dir: term.dir ?? NATURAL_DIRECTION[term.field] };
87
+ });
88
+ };
23
89
 
24
90
  export interface RepositoryRoute {
25
91
  repository: string;
@@ -37,6 +103,8 @@ export interface SubscriptionCandidate {
37
103
  models: Readonly<Record<string, readonly string[]>>;
38
104
  activeRuns: number;
39
105
  concurrencyLimit: number;
106
+ /** Lower serves first. Absent is 0, so prioritized and unprioritized instances order together. */
107
+ priority?: number;
40
108
  /** All provider enforcement windows, plus locally retained reservations. `resetsAt: Infinity` means none reported. */
41
109
  windows: readonly {
42
110
  usedPercent: number | null;
@@ -50,6 +118,26 @@ export interface SubscriptionCandidate {
50
118
  unavailableUntil?: number;
51
119
  }
52
120
 
121
+ /**
122
+ * What to do with an instance routing has no usable capacity reading for.
123
+ *
124
+ * `exclude` (default) suits provider-enforced subscription windows, where an
125
+ * unread login may already be exhausted and sending work there fails the turn.
126
+ * `rank-last` suits capacity whose spend is bounded elsewhere (Kingdom gates on
127
+ * allocation policies), where a never-observed instance must stay usable.
128
+ */
129
+ export type UnknownUtilizationPolicy = 'exclude' | 'rank-last';
130
+
131
+ /**
132
+ * How an instance is paid for. Routing serves only `subscription` unless the
133
+ * caller widens it: spending real money is the caller's decision, never a
134
+ * routing default. Kingdom widens to `api-key`, where a run's spend is bounded
135
+ * by its allocation policies.
136
+ */
137
+ export type BillingMode = 'subscription' | 'api-key';
138
+
139
+ const DEFAULT_BILLING: readonly BillingMode[] = ['subscription'];
140
+
53
141
  export interface RoutingRequest {
54
142
  repository: string;
55
143
  organizationId?: string;
@@ -58,7 +146,10 @@ export interface RoutingRequest {
58
146
  effort: string;
59
147
  now: number;
60
148
  maximumObservationAgeMs: number;
61
- mode?: RoutingMode;
149
+ ordering?: OrderingSpec;
150
+ pinned?: boolean;
151
+ unknownUtilization?: UnknownUtilizationPolicy;
152
+ allowedBilling?: readonly BillingMode[];
62
153
  }
63
154
 
64
155
  /** Repository resolution already done by the caller (or not needed). */
@@ -70,19 +161,27 @@ export interface CandidateRequest {
70
161
  effort: string;
71
162
  now: number;
72
163
  maximumObservationAgeMs: number;
73
- mode?: RoutingMode;
164
+ ordering?: OrderingSpec;
165
+ /** Only the preferred instance may serve; others are excluded as `not-pinned`. */
166
+ pinned?: boolean;
167
+ unknownUtilization?: UnknownUtilizationPolicy;
168
+ /** Billing modes this request may use. Default: subscription only. */
169
+ allowedBilling?: readonly BillingMode[];
74
170
  }
75
171
 
76
172
  export interface RankedCandidate {
77
173
  accountId: string;
78
- utilizationPercent: number;
79
- quartile: number;
174
+ /** Null when no usable observation exists and the policy is `rank-last`. */
175
+ utilizationPercent: number | null;
176
+ quartile: number | null;
177
+ priority: number;
178
+ headroom: number;
80
179
  preferred: boolean;
81
180
  }
82
181
 
83
182
  export type ExclusionReason =
84
183
  | 'disabled'
85
- | 'not-subscription'
184
+ | 'billing'
86
185
  | 'organization'
87
186
  | 'runtime'
88
187
  | 'model'
@@ -133,17 +232,19 @@ export function repositoryIdentity(remote: string): string {
133
232
  export function assessCandidate(
134
233
  request: CandidateRequest,
135
234
  account: SubscriptionCandidate,
136
- ): { excluded: ExclusionReason } | { utilizationPercent: number } {
235
+ ): { excluded: ExclusionReason } | { utilizationPercent: number | null } {
137
236
  if (
138
237
  !account.id ||
139
238
  !Number.isSafeInteger(account.activeRuns) ||
140
239
  account.activeRuns < 0 ||
141
240
  !Number.isSafeInteger(account.concurrencyLimit) ||
142
- account.concurrencyLimit < 1
241
+ account.concurrencyLimit < 1 ||
242
+ (account.priority !== undefined && !Number.isFinite(account.priority))
143
243
  )
144
244
  return { excluded: 'invalid' };
145
245
  if (!account.enabled) return { excluded: 'disabled' };
146
- if (account.authentication !== 'subscription') return { excluded: 'not-subscription' };
246
+ if (!(request.allowedBilling ?? DEFAULT_BILLING).includes(account.authentication))
247
+ return { excluded: 'billing' };
147
248
  if (
148
249
  request.organizationId !== undefined &&
149
250
  !account.organizationIds.includes(request.organizationId)
@@ -151,13 +252,16 @@ export function assessCandidate(
151
252
  return { excluded: 'organization' };
152
253
  if (account.runtime !== request.runtime) return { excluded: 'runtime' };
153
254
  if (!account.models[request.model]?.includes(request.effort)) return { excluded: 'model' };
154
- if (request.mode === 'pinned' && account.id !== request.preferredId)
155
- return { excluded: 'not-pinned' };
255
+ if (request.pinned && account.id !== request.preferredId) return { excluded: 'not-pinned' };
156
256
  if (account.unavailableUntil !== undefined && account.unavailableUntil > request.now)
157
257
  return { excluded: 'unavailable' };
158
258
  if (account.activeRuns >= account.concurrencyLimit) return { excluded: 'occupied' };
159
259
  if (account.blocked) return { excluded: 'blocked' };
160
- if (account.windows.length === 0) return { excluded: 'no-observation' };
260
+ const unreadable = request.unknownUtilization === 'rank-last' ? null : undefined;
261
+ if (account.windows.length === 0) {
262
+ if (unreadable === undefined) return { excluded: 'no-observation' };
263
+ return { utilizationPercent: null };
264
+ }
161
265
  const fractions: number[] = [];
162
266
  for (const window of account.windows) {
163
267
  if (
@@ -168,7 +272,7 @@ export function assessCandidate(
168
272
  !Number.isFinite(window.reservedPercent) ||
169
273
  window.reservedPercent < 0
170
274
  )
171
- return { excluded: 'invalid' };
275
+ return unreadable === undefined ? { excluded: 'invalid' } : { utilizationPercent: null };
172
276
  // resetsAt === Infinity: the provider reported no reset for this window, so no reset can have crossed the observation.
173
277
  if (
174
278
  !Number.isFinite(window.observedAt) ||
@@ -177,28 +281,46 @@ export function assessCandidate(
177
281
  (window.resetsAt !== Number.POSITIVE_INFINITY &&
178
282
  (!Number.isFinite(window.resetsAt) || window.resetsAt <= request.now))
179
283
  )
180
- return { excluded: 'stale-observation' };
284
+ return unreadable === undefined
285
+ ? { excluded: 'stale-observation' }
286
+ : { utilizationPercent: null };
181
287
  fractions.push(window.usedPercent + window.reservedPercent);
182
288
  }
183
289
  if (fractions.some((p) => p >= 100)) return { excluded: 'exhausted' };
184
290
  return { utilizationPercent: Math.max(...fractions) };
185
291
  }
186
292
 
293
+ /** Term fields as own properties, so json-rules orders them by name. */
294
+ const sortKey = (candidate: RankedCandidate, index: number) => ({
295
+ index,
296
+ priority: candidate.priority,
297
+ utilization: candidate.utilizationPercent,
298
+ quartile: candidate.quartile,
299
+ headroom: candidate.headroom,
300
+ preferred: Number(candidate.preferred),
301
+ id: candidate.accountId,
302
+ });
303
+
187
304
  /** Rank eligible candidates deterministically (ties broken by id). */
188
305
  export function rankCandidates(
189
306
  request: CandidateRequest,
190
307
  accounts: readonly SubscriptionCandidate[],
191
- ): { candidates: RankedCandidate[]; excluded: Record<string, ExclusionReason> } {
308
+ ): {
309
+ candidates: RankedCandidate[];
310
+ excluded: Record<string, ExclusionReason>;
311
+ ordering: ResolvedOrdering;
312
+ } {
192
313
  if (
193
314
  !Number.isFinite(request.now) ||
194
315
  !Number.isFinite(request.maximumObservationAgeMs) ||
195
316
  request.maximumObservationAgeMs <= 0
196
317
  )
197
318
  throw Error('Invalid observation policy');
198
- if (request.mode === 'pinned' && !request.preferredId)
319
+ if (request.pinned && !request.preferredId)
199
320
  throw Error('Pinned routing requires a preferred instance');
200
321
  if (new Set(accounts.map((a) => a.id)).size !== accounts.length)
201
322
  throw Error('Duplicate account identity');
323
+ const ordering = resolveOrdering(request.ordering);
202
324
  const excluded: Record<string, ExclusionReason> = {};
203
325
  const ranked = accounts.flatMap((account) => {
204
326
  const assessed = assessCandidate(request, account);
@@ -211,22 +333,15 @@ export function rankCandidates(
211
333
  {
212
334
  accountId: account.id,
213
335
  utilizationPercent,
214
- quartile: Math.floor(utilizationPercent / 25),
336
+ quartile: utilizationPercent === null ? null : Math.floor(utilizationPercent / 25),
337
+ priority: account.priority ?? 0,
338
+ headroom: account.concurrencyLimit - account.activeRuns,
215
339
  preferred: account.id === request.preferredId,
216
340
  },
217
341
  ];
218
342
  });
219
- ranked.sort((a, b) =>
220
- request.mode === 'owner-first' || request.mode === 'pinned'
221
- ? Number(b.preferred) - Number(a.preferred) ||
222
- a.utilizationPercent - b.utilizationPercent ||
223
- a.accountId.localeCompare(b.accountId)
224
- : a.quartile - b.quartile ||
225
- Number(b.preferred) - Number(a.preferred) ||
226
- a.utilizationPercent - b.utilizationPercent ||
227
- a.accountId.localeCompare(b.accountId),
228
- );
229
- return { candidates: ranked, excluded };
343
+ const ordered = orderRecords(ranked.map(sortKey), [...ordering, { field: 'id', dir: 'asc' }]);
344
+ return { candidates: ordered.map((key) => ranked[key.index]!), excluded, ordering };
230
345
  }
231
346
 
232
347
  /** The draft's repository-scoped entry point: resolve ownership, then rank. */
@@ -249,7 +364,7 @@ export function rankSubscriptionAccounts(
249
364
  throw Error('Incomplete repository routing rule');
250
365
  if (request.organizationId && request.organizationId !== route.organizationId)
251
366
  throw Error('Explicit organization conflicts with repository ownership');
252
- const { candidates } = rankCandidates(
367
+ const { candidates, ordering } = rankCandidates(
253
368
  {
254
369
  organizationId: route.organizationId,
255
370
  preferredId: route.preferredAccountId,
@@ -258,7 +373,12 @@ export function rankSubscriptionAccounts(
258
373
  effort: request.effort,
259
374
  now: request.now,
260
375
  maximumObservationAgeMs: request.maximumObservationAgeMs,
261
- mode: request.mode,
376
+ ordering: request.ordering,
377
+ ...(request.pinned !== undefined ? { pinned: request.pinned } : {}),
378
+ ...(request.unknownUtilization !== undefined
379
+ ? { unknownUtilization: request.unknownUtilization }
380
+ : {}),
381
+ ...(request.allowedBilling !== undefined ? { allowedBilling: request.allowedBilling } : {}),
262
382
  },
263
383
  accounts,
264
384
  );
@@ -267,7 +387,7 @@ export function rankSubscriptionAccounts(
267
387
  organizationId: route.organizationId,
268
388
  model: request.model,
269
389
  effort: request.effort,
270
- mode: request.mode ?? 'quartile-balanced',
390
+ ordering,
271
391
  candidates,
272
392
  };
273
393
  }