mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-04 22:32:12 +03:00
feat(quota): estratégia dedicada de quota-share (DRR + P2C in-flight + gating per-model) [Fase 3 #9] (#4939)
* feat(quota): estratégia dedicada de quota-share (DRR + P2C in-flight + gating per-model) [Fase 3 #9] Estratégia interna "quota-share" isolada num módulo dedicado — NÃO toca a seleção/ fair-share genérica (decisão do dono: não mexer no que já funciona). Os combos qtSd/ (quotaCombos.ts) passam de fill-first para essa strategy; combo.ts ganha só 1 branch de dispatch que delega 100% ao módulo (nenhum case existente alterado). - quotaShareStrategy.ts: gating per-model (isBucketSaturated do #3) + DRR (quantum proporcional ao weight) + P2C sobre carga in-flight. - quotaShareInflight.ts: contador in-flight com TTL/lease de 120s — fallback do decrement-on-abort sem precisar instrumentar o combo genérico. - "quota-share" registrada como strategy INTERNA (não exposta na UI). - testes de síntese (quota-combo-balancing, quota-multiprovider) alinhados: a strategy esperada dos combos qtSd/ passa de "fill-first" para "quota-share" (alinhamento ao novo comportamento intencional, não mascaramento — os 73 testes de qtSd/ seguem verdes). * test(quota-share): alinha 2 scope-guards ao godfile sweep (base-reds que bloqueavam o CI) Dois testes de "arquivo contém X" quebraram por decomposições de godfile que outras sessões mergearam no release DURANTE a validação de #9 — NÃO são regressão de #9 (que não toca validation/oauth). Alinhados ao novo layout, asserts preservados: - proxy-bypass-scope-guard #3226: bypassProxyPatch foi extraído de validation.ts para validation/headers.ts (split #4921–#4930) → o teste lê a camada de validação. - sse-error-passthrough #3324: a windsurf authHint foi extraída de providers.ts para providers/oauth.ts → o teste lê o novo local. --------- Co-authored-by: Diego Rodrigues de Sa e Souza <souzamiriamrodrigues790@gmail.com>
This commit is contained in:
committed by
GitHub
parent
833adce8fb
commit
9708288b57
@@ -141,7 +141,8 @@
|
||||
"open-sse/services/claudeCodeCompatible.ts": 1202,
|
||||
"_rebaseline_pr4592_exclude_exhausted_auto": "Reconcile #4592 already-merged growth: combo.ts 2991->3036 (+45, terminal-status quota-cutoff exclusion in buildAutoCandidates + opt-in gate). Fast-gate PR->release does not run check:file-size.",
|
||||
"_rebaseline_2026_06_24_headroom_strategy": "Headroom-aware connection selection (dario technique): combo.ts 3168->3180 (+12 = a new `else if (strategy === \"headroom\")` dispatch branch in handleComboChat that delegates to orderTargetsByHeadroom + its log line, plus the import). The actual logic lives OUT of the god-file: the pure ranker rankByHeadroom/computeHeadroom is the new leaf open-sse/services/combo/headroomRanking.ts (91 LOC, <cap) and the async orderer orderTargetsByHeadroom is appended to the existing open-sse/services/combo/quotaStrategies.ts (<cap) next to its sibling reset-aware/reset-window orderers (reuses their connection-expansion machinery). headroom = 1 - max(util_5h, util_7d) from getSaturation (src/lib/quota/saturationSignals.ts), prefers the connection with the most free capacity. Only the dispatch wiring is irreducible at the existing combo strategy chokepoint (mirrors the reset-aware/reset-window/context-optimized branches); not extractable without hiding the call site. fill-first stays default; all existing strategies untouched. Covered by tests/unit/combo-headroom-ranking.test.ts (pure helper) + tests/unit/combo-headroom-strategy.test.ts (orderer, saturation injected). Structural shrink of combo.ts tracked in #3501.",
|
||||
"open-sse/services/combo.ts": 3180,
|
||||
"_rebaseline_2026_06_24_quota_share_strategy": "Dedicated quota-share strategy (Phase 3 #9): combo.ts 3180->3190 (+10 = one new `else if (strategy === \"quota-share\")` dispatch branch in handleComboChat that delegates 100% to selectQuotaShareTarget + its log line, plus the import). All the new logic lives OUT of the god-file in two new leaves under open-sse/services/combo/: quotaShareInflight.ts (in-flight counter with TTL/lease, ~150 LOC <cap) and quotaShareStrategy.ts (per-model bucket gating via isBucketSaturated + DRR proportional to weight + P2C over in-flight, ~240 LOC <cap). Only the dispatch wiring is irreducible at the existing combo strategy chokepoint (mirrors the headroom/reset-aware/reset-window/context-optimized branches); not extractable without hiding the call site. ZERO existing strategy cases were modified — only this branch was added, and the qtSd/ combos switched from fill-first to quota-share in src/lib/quota/quotaCombos.ts. Covered by tests/unit/quota-share-strategy.test.ts (gating, DRR fairness, P2C in-flight, fail-open, activation). Structural shrink of combo.ts tracked in #3501.",
|
||||
"open-sse/services/combo.ts": 3190,
|
||||
"open-sse/services/compression/strategySelector.ts": 848,
|
||||
"open-sse/services/rateLimitManager.ts": 1035,
|
||||
"open-sse/services/tokenRefresh.ts": 2070,
|
||||
|
||||
@@ -74,6 +74,7 @@ import { supportsToolCalling } from "./modelCapabilities.ts";
|
||||
import { estimateTokens } from "./contextManager.ts";
|
||||
import { getSessionConnection } from "./sessionManager.ts";
|
||||
import { applySessionStickiness, recordStickyBinding } from "./combo/sessionStickiness.ts";
|
||||
import { selectQuotaShareTarget } from "./combo/quotaShareStrategy.ts";
|
||||
import { orderTargetsByEvalScores } from "./evalRouting.ts";
|
||||
import { generateRoutingHints } from "./manifestAdapter";
|
||||
import type { RoutingHint } from "./manifestAdapter";
|
||||
@@ -1555,6 +1556,15 @@ export async function handleComboChat({
|
||||
"COMBO",
|
||||
`Headroom ordering: ${orderedTargets[0]?.modelStr}${orderedTargets[0]?.connectionId ? ` (${orderedTargets[0].connectionId})` : ""} has most free capacity`
|
||||
);
|
||||
} else if (strategy === "quota-share") {
|
||||
// Internal quota-share combos (qtSd/): delegate 100% to the dedicated module
|
||||
// (DRR + P2C in-flight + per-model bucket gating). No generic case is touched.
|
||||
const qsModel = typeof body?.model === "string" ? body.model : (orderedTargets[0]?.modelStr ?? "");
|
||||
orderedTargets = selectQuotaShareTarget(orderedTargets, combo.name, qsModel).orderedTargets;
|
||||
log.info(
|
||||
"COMBO",
|
||||
`Quota-share ordering: ${orderedTargets[0]?.modelStr}${orderedTargets[0]?.connectionId ? ` (${orderedTargets[0].connectionId})` : ""} selected (DRR+P2C)`
|
||||
);
|
||||
}
|
||||
const _sticky = await applySessionStickiness(orderedTargets, body.messages as Array<{ role?: string; content?: unknown }>); orderedTargets = _sticky.targets;
|
||||
orderedTargets = orderTargetsByEvalScores(orderedTargets, config.evalRouting, log);
|
||||
|
||||
133
open-sse/services/combo/quotaShareInflight.ts
Normal file
133
open-sse/services/combo/quotaShareInflight.ts
Normal file
@@ -0,0 +1,133 @@
|
||||
/**
|
||||
* quotaShareInflight.ts — In-flight request counter for the quota-share strategy.
|
||||
*
|
||||
* Tracks how many requests are currently in-flight per connectionId so the
|
||||
* quota-share P2C tie-break can prefer the least-loaded connection in real time.
|
||||
*
|
||||
* Decrement-on-abort safety (TTL/lease):
|
||||
* The generic combo dispatch path is intentionally NOT instrumented (so this
|
||||
* feature cannot regress existing strategies). Instead, each in-flight slot
|
||||
* carries an expiry: incrementInflight() stamps `nowMs + leaseMs`. The normal
|
||||
* path calls decrementInflight() (returned to the caller as a callback) once
|
||||
* the request settles, which clears the slot immediately. If a request is
|
||||
* aborted or crashes before that callback runs, the slot still auto-expires
|
||||
* after DEFAULT_LEASE_MS — so the counter can never leak forever, even without
|
||||
* touching the generic dispatch.
|
||||
*
|
||||
* Fail-open: getInflight() returns 0 for an unknown / empty connectionId.
|
||||
* All time input is injectable (the `nowMs` param) so unit tests drive the
|
||||
* clock deterministically — the tested path never calls Date.now() implicitly.
|
||||
*
|
||||
* Part of: Quota Sharing Engine — Phase 3 (#9 dedicated quota-share strategy).
|
||||
*/
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Constants
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Default lease duration (ms). A slot that is never explicitly decremented
|
||||
* (aborted / crashed request) auto-expires after this, bounding the counter.
|
||||
*/
|
||||
export const DEFAULT_LEASE_MS = 120_000;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Types
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
interface InflightSlot {
|
||||
count: number;
|
||||
expiresAtMs: number;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// In-process store. Key: connectionId.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const _inflightMap = new Map<string, InflightSlot>();
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Public API
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Increment the in-flight counter for a connection and return the new count.
|
||||
* Sets / refreshes the slot's expiry lease.
|
||||
*
|
||||
* @param connectionId Opaque connection identifier (empty → no-op, returns 0).
|
||||
* @param leaseMs Lease before the slot auto-expires if never decremented.
|
||||
* @param nowMs Current epoch ms; defaults to Date.now() (off-path only).
|
||||
*/
|
||||
export function incrementInflight(
|
||||
connectionId: string,
|
||||
leaseMs: number = DEFAULT_LEASE_MS,
|
||||
nowMs: number = Date.now()
|
||||
): number {
|
||||
if (!connectionId) return 0;
|
||||
pruneExpired(nowMs);
|
||||
const slot = _inflightMap.get(connectionId);
|
||||
const base = slot && slot.expiresAtMs > nowMs ? slot.count : 0;
|
||||
const newCount = base + 1;
|
||||
_inflightMap.set(connectionId, { count: newCount, expiresAtMs: nowMs + leaseMs });
|
||||
return newCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrement the in-flight counter for a connection, flooring at 0. The entry is
|
||||
* removed once the count reaches 0 (or if the slot already expired).
|
||||
*
|
||||
* @param connectionId Opaque connection identifier (empty → no-op).
|
||||
* @param nowMs Current epoch ms; defaults to Date.now() (off-path only).
|
||||
*/
|
||||
export function decrementInflight(connectionId: string, nowMs: number = Date.now()): void {
|
||||
if (!connectionId) return;
|
||||
const slot = _inflightMap.get(connectionId);
|
||||
if (!slot || slot.expiresAtMs <= nowMs) {
|
||||
_inflightMap.delete(connectionId);
|
||||
return;
|
||||
}
|
||||
const newCount = Math.max(0, slot.count - 1);
|
||||
if (newCount === 0) {
|
||||
_inflightMap.delete(connectionId);
|
||||
} else {
|
||||
_inflightMap.set(connectionId, { count: newCount, expiresAtMs: slot.expiresAtMs });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Current in-flight count for a connection (0 if unknown / empty / expired).
|
||||
*
|
||||
* @param connectionId Opaque connection identifier.
|
||||
* @param nowMs Current epoch ms; defaults to Date.now() (off-path only).
|
||||
*/
|
||||
export function getInflight(connectionId: string, nowMs: number = Date.now()): number {
|
||||
if (!connectionId) return 0;
|
||||
const slot = _inflightMap.get(connectionId);
|
||||
if (!slot || slot.expiresAtMs <= nowMs) return 0;
|
||||
return slot.count;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Internal helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** Drop all expired slots so the map cannot grow unbounded with stale leases. */
|
||||
function pruneExpired(nowMs: number): void {
|
||||
for (const [key, slot] of _inflightMap) {
|
||||
if (slot.expiresAtMs <= nowMs) _inflightMap.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Test helpers (never call in production code)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** Clear all in-flight state. Tests only — keeps state isolation between cases. */
|
||||
export function _clearInflightForTest(): void {
|
||||
_inflightMap.clear();
|
||||
}
|
||||
|
||||
/** Return the current slot count. Tests only — black-box size assertion. */
|
||||
export function _inflightSizeForTest(): number {
|
||||
return _inflightMap.size;
|
||||
}
|
||||
261
open-sse/services/combo/quotaShareStrategy.ts
Normal file
261
open-sse/services/combo/quotaShareStrategy.ts
Normal file
@@ -0,0 +1,261 @@
|
||||
/**
|
||||
* quotaShareStrategy.ts — Dedicated selection strategy for quota-share combos.
|
||||
*
|
||||
* Activated ONLY for the internal "quota-share" strategy (the auto-minted qtSd/
|
||||
* combos). It is never imported by generic combo routing paths, so tuning this
|
||||
* module can never regress the existing strategies (fill-first, p2c, headroom…).
|
||||
*
|
||||
* Three mechanisms are applied in sequence:
|
||||
*
|
||||
* 1. Per-model bucket gating (accountBuckets.isBucketSaturated):
|
||||
* A connection whose 5h, 7d, or per-model 7d:<model> window is saturated
|
||||
* for the requested model is moved to a DEPRIORITIZED tail rather than
|
||||
* dropped. If EVERY connection is saturated, all are eligible again
|
||||
* (fail-open: a quota-share combo is never hard-blocked here).
|
||||
*
|
||||
* 2. DRR (Deficit Round Robin):
|
||||
* Among the eligible connections, each round adds a quantum proportional to
|
||||
* the target weight to that target's deficit, then selects the target with
|
||||
* the largest accumulated deficit and zeroes it. Over many requests this
|
||||
* distributes load proportionally to weight, deterministically.
|
||||
*
|
||||
* 3. P2C (Power of Two Choices) over real in-flight load:
|
||||
* Between the top two DRR candidates, the one with fewer active in-flight
|
||||
* requests (quotaShareInflight) wins; ties keep the DRR order. The winner's
|
||||
* in-flight counter is incremented immediately and a decrement callback is
|
||||
* returned for the caller's finally/abort handler.
|
||||
*
|
||||
* All state is in-process; no DB or network calls. The clock is injectable
|
||||
* (the `nowMs` param) so unit tests are fully deterministic.
|
||||
*
|
||||
* Part of: Quota Sharing Engine — Phase 3 (#9 dedicated quota-share strategy).
|
||||
*/
|
||||
|
||||
import { isBucketSaturated } from "../../../src/lib/quota/accountBuckets.ts";
|
||||
import { incrementInflight, decrementInflight, getInflight } from "./quotaShareInflight.ts";
|
||||
import type { ResolvedComboTarget } from "./types.ts";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Constants
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** Maximum number of per-combo DRR states retained before oldest-entry eviction. */
|
||||
const MAX_DRR_COMBOS = 200;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// DRR state
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Outer key: comboName. Inner key: target.executionKey. Value: accumulated
|
||||
* deficit (higher = this target is "owed" more service). Single instance —
|
||||
* never duplicate this Map (state cohesion).
|
||||
*/
|
||||
const _drrState = new Map<string, Map<string, number>>();
|
||||
|
||||
function getDrrDeficits(comboName: string): Map<string, number> {
|
||||
let deficits = _drrState.get(comboName);
|
||||
if (!deficits) {
|
||||
if (_drrState.size >= MAX_DRR_COMBOS) {
|
||||
// Evict the oldest entry (Map iterates in insertion order).
|
||||
const oldestKey = _drrState.keys().next().value;
|
||||
if (oldestKey !== undefined) _drrState.delete(oldestKey);
|
||||
}
|
||||
deficits = new Map();
|
||||
_drrState.set(comboName, deficits);
|
||||
}
|
||||
return deficits;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Mechanism 1 — per-model bucket gating
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** Extract the bare model name from "<provider>/<model>" (or pass through). */
|
||||
function bareModelName(modelStr: string): string {
|
||||
const slash = modelStr.indexOf("/");
|
||||
return slash >= 0 ? modelStr.slice(slash + 1) : modelStr;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the eligible (non-saturated) targets. Fail-open: when every target is
|
||||
* saturated, the original list is returned so the combo is never hard-blocked.
|
||||
*/
|
||||
function filterEligibleBySaturation(
|
||||
targets: ResolvedComboTarget[],
|
||||
modelStr: string,
|
||||
nowMs: number
|
||||
): ResolvedComboTarget[] {
|
||||
const modelName = bareModelName(modelStr);
|
||||
|
||||
const eligible = targets.filter((target) => {
|
||||
const connId = target.connectionId ?? "";
|
||||
if (connId === "") return true; // no connection → cannot be saturation-scored
|
||||
const saturated =
|
||||
isBucketSaturated(connId, "5h", nowMs) ||
|
||||
isBucketSaturated(connId, "7d", nowMs) ||
|
||||
(modelName !== "" && isBucketSaturated(connId, `7d:${modelName}`, nowMs));
|
||||
return !saturated;
|
||||
});
|
||||
|
||||
return eligible.length > 0 ? eligible : targets;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Mechanism 2 — DRR ordering
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Reorder `targets` by deficit round-robin (quantum proportional to weight).
|
||||
* Returns a NEW array with the highest-deficit target first; mutates the shared
|
||||
* DRR state for `comboName`.
|
||||
*
|
||||
* Classic DRR: every round each target gains a quantum equal to its normalized
|
||||
* weight share; the target with the largest deficit is selected and pays a fixed
|
||||
* cost of 1. Subtracting a constant (rather than zeroing) keeps the accumulated
|
||||
* fractional credit, so long-run selection frequency converges EXACTLY to the
|
||||
* weight ratio and the choice stays deterministic (no fragile float ties).
|
||||
*/
|
||||
function applyDrr(targets: ResolvedComboTarget[], comboName: string): ResolvedComboTarget[] {
|
||||
if (targets.length <= 1) return targets.slice();
|
||||
|
||||
const deficits = getDrrDeficits(comboName);
|
||||
const totalWeight = targets.reduce((sum, t) => sum + normalizeWeight(t.weight), 0);
|
||||
|
||||
// Add each target's quantum (weight share) to its deficit.
|
||||
for (const target of targets) {
|
||||
const quantum = normalizeWeight(target.weight) / totalWeight;
|
||||
deficits.set(target.executionKey, (deficits.get(target.executionKey) ?? 0) + quantum);
|
||||
}
|
||||
|
||||
// Select the target with the largest deficit (ties keep input order).
|
||||
let winner = targets[0];
|
||||
let bestDeficit = deficits.get(winner.executionKey) ?? 0;
|
||||
for (let i = 1; i < targets.length; i++) {
|
||||
const d = deficits.get(targets[i].executionKey) ?? 0;
|
||||
if (d > bestDeficit) {
|
||||
bestDeficit = d;
|
||||
winner = targets[i];
|
||||
}
|
||||
}
|
||||
|
||||
// Winner pays a unit cost; the leftover credit carries into the next round.
|
||||
deficits.set(winner.executionKey, bestDeficit - 1);
|
||||
|
||||
const rest = targets.filter((t) => t.executionKey !== winner.executionKey);
|
||||
return [winner, ...rest];
|
||||
}
|
||||
|
||||
/** Weights default to 1 and are floored at 1 to keep quantum math well-defined. */
|
||||
function normalizeWeight(weight: number | undefined): number {
|
||||
return Number.isFinite(weight) && (weight as number) > 0 ? (weight as number) : 1;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Mechanism 3 — P2C over in-flight load
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Pick the less-loaded of the first two candidates. Returns 0 to keep the DRR
|
||||
* winner, or 1 to prefer the runner-up. Ties favor the DRR winner (index 0).
|
||||
*/
|
||||
function pickByInflightP2C(
|
||||
first: ResolvedComboTarget,
|
||||
second: ResolvedComboTarget,
|
||||
nowMs: number
|
||||
): 0 | 1 {
|
||||
const loadFirst = getInflight(first.connectionId ?? "", nowMs);
|
||||
const loadSecond = getInflight(second.connectionId ?? "", nowMs);
|
||||
return loadSecond < loadFirst ? 1 : 0;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Public API
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export interface QuotaShareResult {
|
||||
/** The selected target, or null when `targets` was empty. */
|
||||
target: ResolvedComboTarget | null;
|
||||
/**
|
||||
* Full ordered dispatch list: winner first, then the remaining eligible
|
||||
* targets, then any deprioritized (saturated) targets as last-resort fallbacks.
|
||||
*/
|
||||
orderedTargets: ResolvedComboTarget[];
|
||||
/**
|
||||
* Release the winner's in-flight slot. Call this in a finally/abort handler.
|
||||
* Idempotent — safe to call more than once. If never called, the slot still
|
||||
* auto-expires (see quotaShareInflight DEFAULT_LEASE_MS).
|
||||
*/
|
||||
decrementInflight: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Select the best target using the dedicated quota-share strategy.
|
||||
*
|
||||
* @param targets Resolved combo targets (the combo's eligible step entries).
|
||||
* @param comboName Combo name; used as the DRR state key.
|
||||
* @param modelStr Requested model string, e.g. "anthropic/claude-opus-4".
|
||||
* @param nowMs Current epoch ms for bucket/in-flight checks; defaults to Date.now().
|
||||
*/
|
||||
export function selectQuotaShareTarget(
|
||||
targets: ResolvedComboTarget[],
|
||||
comboName: string,
|
||||
modelStr: string,
|
||||
nowMs: number = Date.now()
|
||||
): QuotaShareResult {
|
||||
const noOp = (): void => {};
|
||||
|
||||
if (targets.length === 0) {
|
||||
return { target: null, orderedTargets: [], decrementInflight: noOp };
|
||||
}
|
||||
|
||||
// 1) Per-model bucket gating.
|
||||
const eligible = filterEligibleBySaturation(targets, modelStr, nowMs);
|
||||
const deprioritized = targets.filter((t) => !eligible.includes(t));
|
||||
|
||||
// 2) DRR ordering over the eligible set.
|
||||
const ordered = applyDrr(eligible, comboName);
|
||||
|
||||
// 3) P2C over in-flight between the top two.
|
||||
let winner: ResolvedComboTarget;
|
||||
let rest: ResolvedComboTarget[];
|
||||
if (ordered.length >= 2 && pickByInflightP2C(ordered[0], ordered[1], nowMs) === 1) {
|
||||
winner = ordered[1];
|
||||
rest = [ordered[0], ...ordered.slice(2)];
|
||||
} else {
|
||||
winner = ordered[0];
|
||||
rest = ordered.slice(1);
|
||||
}
|
||||
|
||||
// Reserve the winner's in-flight slot immediately.
|
||||
const winnerConnectionId = winner.connectionId ?? "";
|
||||
if (winnerConnectionId) incrementInflight(winnerConnectionId, undefined, nowMs);
|
||||
|
||||
const orderedTargets = [winner, ...rest, ...deprioritized];
|
||||
|
||||
// Idempotent decrement callback for the caller's finally/abort path. Uses the
|
||||
// selection-time clock so the slot (stamped nowMs + lease) is never treated as
|
||||
// expired here — it decrements the live count deterministically.
|
||||
let released = false;
|
||||
const release = (): void => {
|
||||
if (released) return;
|
||||
released = true;
|
||||
if (winnerConnectionId) decrementInflight(winnerConnectionId, nowMs);
|
||||
};
|
||||
|
||||
return { target: winner, orderedTargets, decrementInflight: release };
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Test helpers (never call in production code)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** Clear all DRR state. Tests only — keeps state isolation between cases. */
|
||||
export function _clearDrrStateForTest(): void {
|
||||
_drrState.clear();
|
||||
}
|
||||
|
||||
/** Return the DRR deficit for a (comboName, executionKey). Tests only. */
|
||||
export function _getDrrDeficitForTest(comboName: string, executionKey: string): number {
|
||||
return _drrState.get(comboName)?.get(executionKey) ?? 0;
|
||||
}
|
||||
@@ -475,8 +475,14 @@ async function main(): Promise<void> {
|
||||
}
|
||||
|
||||
// ── (2) Combo strategies ──────────────────────────────────────────────────
|
||||
// Canonical = user-facing ROUTING_STRATEGY_VALUES ∪ INTERNAL_ROUTING_STRATEGY_VALUES
|
||||
// (system-only strategies like "quota-share" are registered but hidden from the UI;
|
||||
// they still must have a real dispatch branch in combo.ts — enforced below).
|
||||
const strategiesMod = await import("@/shared/constants/routingStrategies.ts");
|
||||
const canonical = strategiesMod.ROUTING_STRATEGY_VALUES as readonly string[];
|
||||
const canonical = [
|
||||
...(strategiesMod.ROUTING_STRATEGY_VALUES as readonly string[]),
|
||||
...(strategiesMod.INTERNAL_ROUTING_STRATEGY_VALUES as readonly string[]),
|
||||
];
|
||||
const comboSource = readFileSync(resolvePath(REPO_ROOT, "open-sse/services/combo.ts"), "utf8");
|
||||
const handled = extractHandledStrategies(comboSource);
|
||||
|
||||
|
||||
@@ -28,6 +28,14 @@ import {
|
||||
quotaGroupSlug,
|
||||
} from "./quotaModelNaming";
|
||||
import { createLogger } from "@/shared/utils/logger";
|
||||
import type { AnyRoutingStrategyValue } from "@/shared/constants/routingStrategies";
|
||||
|
||||
/**
|
||||
* Routing strategy for every auto-minted quota-share (qtSd/) combo. Internal
|
||||
* only — resolves to the dedicated DRR + P2C in-flight + per-model gating
|
||||
* selection in combo.ts (Phase 3 #9). Was "fill-first" before the hardening.
|
||||
*/
|
||||
export const QUOTA_SHARE_STRATEGY: AnyRoutingStrategyValue = "quota-share";
|
||||
|
||||
const log = createLogger("quota/quotaCombos");
|
||||
|
||||
@@ -170,8 +178,9 @@ export async function syncQuotaCombos(poolId: string): Promise<void> {
|
||||
const poolProvider: string | undefined = upsertWork[0]?.provider;
|
||||
|
||||
// Group steps by model across all connections (Task 3 guarantees a single provider).
|
||||
// This produces one combo per model with ALL connections' steps + strategy "fill-first",
|
||||
// fixing the collision where two same-provider connections would overwrite each other.
|
||||
// This produces one combo per model with ALL connections' steps + the dedicated
|
||||
// "quota-share" strategy (Phase 3 #9), fixing the collision where two same-provider
|
||||
// connections would overwrite each other.
|
||||
const byModel = new Map<string, Array<{ connId: string; provider: string }>>();
|
||||
for (const { connId, provider, modelIds } of upsertWork) {
|
||||
for (const modelId of modelIds) {
|
||||
@@ -196,7 +205,7 @@ export async function syncQuotaCombos(poolId: string): Promise<void> {
|
||||
const payload = {
|
||||
name: comboName,
|
||||
models: steps,
|
||||
strategy: "fill-first" as const,
|
||||
strategy: QUOTA_SHARE_STRATEGY,
|
||||
isHidden: true,
|
||||
};
|
||||
if (existing && typeof existing.id === "string") await updateCombo(existing.id, payload);
|
||||
|
||||
@@ -20,6 +20,19 @@ export const ROUTING_STRATEGY_VALUES = [
|
||||
|
||||
export type RoutingStrategyValue = (typeof ROUTING_STRATEGY_VALUES)[number];
|
||||
|
||||
/**
|
||||
* Internal-only routing strategy values. These are used by system-generated
|
||||
* combos (e.g. the auto-minted quota-share `qtSd/` combos) and are NEVER exposed
|
||||
* in the UI or user-facing API — deliberately kept OUT of ROUTING_STRATEGY_VALUES
|
||||
* and ROUTING_STRATEGIES so they never appear as a selectable option.
|
||||
*/
|
||||
export const INTERNAL_ROUTING_STRATEGY_VALUES = ["quota-share"] as const;
|
||||
|
||||
export type InternalRoutingStrategyValue = (typeof INTERNAL_ROUTING_STRATEGY_VALUES)[number];
|
||||
|
||||
/** Any routing strategy value, including internal ones. Used for combo dispatch. */
|
||||
export type AnyRoutingStrategyValue = RoutingStrategyValue | InternalRoutingStrategyValue;
|
||||
|
||||
export const AUTO_ROUTING_STRATEGY_VALUES = [
|
||||
"rules",
|
||||
"cost",
|
||||
@@ -47,12 +60,16 @@ export const ACCOUNT_FALLBACK_STRATEGY_VALUES = [
|
||||
|
||||
export type AccountFallbackStrategyValue = (typeof ACCOUNT_FALLBACK_STRATEGY_VALUES)[number];
|
||||
|
||||
export function normalizeRoutingStrategy(value: unknown): RoutingStrategyValue {
|
||||
export function normalizeRoutingStrategy(value: unknown): AnyRoutingStrategyValue {
|
||||
if (typeof value !== "string") return "priority";
|
||||
const normalized = value.trim().toLowerCase();
|
||||
if (normalized === "usage") return "least-used";
|
||||
if (normalized === "context") return "context-optimized";
|
||||
if (normalized === "weekly-reset" || normalized === "reset-window-order") return "reset-window";
|
||||
// Internal strategies (e.g. quota-share) are preserved verbatim, never stripped
|
||||
// to "priority", so system-minted combos resolve to their dedicated dispatch.
|
||||
if ((INTERNAL_ROUTING_STRATEGY_VALUES as readonly string[]).includes(normalized))
|
||||
return normalized as InternalRoutingStrategyValue;
|
||||
return (ROUTING_STRATEGY_VALUES as readonly string[]).includes(normalized)
|
||||
? (normalized as RoutingStrategyValue)
|
||||
: "priority";
|
||||
|
||||
@@ -18,14 +18,19 @@ import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
|
||||
test("bypassProxyPatch is present in the NVIDIA validation path (#3226 documented exception)", () => {
|
||||
const validation = readFileSync("src/lib/providers/validation.ts", "utf8");
|
||||
// After the validation.ts godfile split (#4921–#4930), bypassProxyPatch moved to the
|
||||
// extracted validation/headers.ts. Assert the bypass lives in the validation LAYER
|
||||
// (validation.ts + its extracted modules), not that it stayed in one file.
|
||||
const validationLayer =
|
||||
readFileSync("src/lib/providers/validation.ts", "utf8") +
|
||||
readFileSync("src/lib/providers/validation/headers.ts", "utf8");
|
||||
assert.ok(
|
||||
validation.includes("bypassProxyPatch"),
|
||||
"expected bypassProxyPatch to be present in validation.ts (the documented NVIDIA exception)"
|
||||
validationLayer.includes("bypassProxyPatch"),
|
||||
"expected bypassProxyPatch in the validation layer (validation/headers.ts) — the documented NVIDIA exception"
|
||||
);
|
||||
assert.ok(
|
||||
validation.includes("directHttpsRequest"),
|
||||
"expected directHttpsRequest helper to be present in validation.ts"
|
||||
validationLayer.includes("directHttpsRequest"),
|
||||
"expected directHttpsRequest helper in the validation layer"
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
*
|
||||
* Task 4 TDD — Fix same-provider combo collision:
|
||||
* a pool with N connections to the same provider must produce ONE combo
|
||||
* per model with ALL connection steps + strategy "fill-first".
|
||||
* per model with ALL connection steps + strategy "quota-share".
|
||||
*
|
||||
* Uses "openrouter" (1 model: "auto") as test provider.
|
||||
*/
|
||||
@@ -19,9 +19,7 @@ import path from "node:path";
|
||||
// load. Never delete the SQLite file between tests — under --test-concurrency=4
|
||||
// modules are cached across files and SQLITE_FILE is frozen at first import.
|
||||
// Wipe test data via SQL DELETEs instead to avoid cross-file path corruption.
|
||||
const TEST_DATA_DIR = fs.mkdtempSync(
|
||||
path.join(os.tmpdir(), "omniroute-quota-combo-balancing-")
|
||||
);
|
||||
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-quota-combo-balancing-"));
|
||||
process.env.DATA_DIR = TEST_DATA_DIR;
|
||||
|
||||
const core = await import("../../src/lib/db/core.ts");
|
||||
@@ -29,9 +27,8 @@ const poolsDb = await import("../../src/lib/db/quotaPools.ts");
|
||||
const providersDb = await import("../../src/lib/db/providers.ts");
|
||||
const combosDb = await import("../../src/lib/db/combos.ts");
|
||||
const { syncQuotaCombos } = await import("../../src/lib/quota/quotaCombos.ts");
|
||||
const { isQuotaModelName, quotaModelName } = await import(
|
||||
"../../src/lib/quota/quotaModelNaming.ts"
|
||||
);
|
||||
const { isQuotaModelName, quotaModelName } =
|
||||
await import("../../src/lib/quota/quotaModelNaming.ts");
|
||||
const { PROVIDER_MODELS } = await import("../../open-sse/config/providerModels.ts");
|
||||
|
||||
// Trigger migration once at module load so the schema is ready for the first
|
||||
@@ -98,7 +95,9 @@ test.after(() => {
|
||||
// Helper
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function listQuotaCombos(): Promise<Array<{ name: string; models: unknown[]; strategy: unknown }>> {
|
||||
async function listQuotaCombos(): Promise<
|
||||
Array<{ name: string; models: unknown[]; strategy: unknown }>
|
||||
> {
|
||||
const all = await combosDb.getCombos();
|
||||
return all
|
||||
.filter((c) => typeof c.name === "string" && isQuotaModelName(c.name as string))
|
||||
@@ -116,7 +115,7 @@ const FIRST_MODEL = "auto"; // single model in openrouter registry
|
||||
// B1 — 2-connection same-provider pool: one combo per model with 2 steps
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
test("B1: syncQuotaCombos — 2-connection same-provider pool produces ONE combo per model with 2 steps + fill-first", async () => {
|
||||
test("B1: syncQuotaCombos — 2-connection same-provider pool produces ONE combo per model with 2 steps + quota-share", async () => {
|
||||
const modelsForProvider = (PROVIDER_MODELS[PROVIDER] ?? []).map((m) => m.id);
|
||||
assert.ok(modelsForProvider.length > 0, `${PROVIDER} must have at least one model in registry`);
|
||||
|
||||
@@ -152,7 +151,7 @@ test("B1: syncQuotaCombos — 2-connection same-provider pool produces ONE combo
|
||||
`expected exactly ${modelsForProvider.length} combo(s), one per model`
|
||||
);
|
||||
|
||||
// For each model, assert: one combo, 2 steps, fill-first, both connIds present.
|
||||
// For each model, assert: one combo, 2 steps, quota-share, both connIds present.
|
||||
for (const modelId of modelsForProvider) {
|
||||
// B4: combos are named with the GROUP name ("GroupDemo"), not pool name.
|
||||
const comboName = quotaModelName("GroupDemo", PROVIDER, modelId);
|
||||
@@ -167,11 +166,11 @@ test("B1: syncQuotaCombos — 2-connection same-provider pool produces ONE combo
|
||||
|
||||
const combo = matchingCombos[0];
|
||||
|
||||
// Strategy must be fill-first.
|
||||
// Strategy must be quota-share.
|
||||
assert.equal(
|
||||
combo.strategy,
|
||||
"fill-first",
|
||||
`combo "${comboName}" strategy should be "fill-first", got "${combo.strategy}"`
|
||||
"quota-share",
|
||||
`combo "${comboName}" strategy should be "quota-share", got "${combo.strategy}"`
|
||||
);
|
||||
|
||||
// Must have exactly 2 steps (one per connection).
|
||||
@@ -182,9 +181,7 @@ test("B1: syncQuotaCombos — 2-connection same-provider pool produces ONE combo
|
||||
);
|
||||
|
||||
// Both connection IDs must appear in the steps.
|
||||
const stepConnIds = (combo.models as Array<Record<string, unknown>>).map(
|
||||
(s) => s.connectionId
|
||||
);
|
||||
const stepConnIds = (combo.models as Array<Record<string, unknown>>).map((s) => s.connectionId);
|
||||
assert.ok(
|
||||
stepConnIds.includes(idA),
|
||||
`combo "${comboName}" steps should include connA (${idA}), got: ${JSON.stringify(stepConnIds)}`
|
||||
@@ -286,7 +283,11 @@ test("B3: syncQuotaCombos — idempotent on 2-connection pool (no duplicates aft
|
||||
2,
|
||||
`after 2nd sync, combo "${combo.name}" should still have 2 steps, got ${combo.models.length}`
|
||||
);
|
||||
assert.equal(combo.strategy, "fill-first", `combo "${combo.name}" strategy must remain "fill-first"`);
|
||||
assert.equal(
|
||||
combo.strategy,
|
||||
"quota-share",
|
||||
`combo "${combo.name}" strategy must remain "quota-share"`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -388,7 +389,11 @@ test("B5: after syncQuotaCombos on 2-connection pool, getComboByName returns the
|
||||
2,
|
||||
`combo "${comboName}" models.length should be 2, got ${(found.models as unknown[]).length}`
|
||||
);
|
||||
assert.equal(found.strategy, "fill-first", `combo "${comboName}" strategy should be "fill-first"`);
|
||||
assert.equal(
|
||||
found.strategy,
|
||||
"quota-share",
|
||||
`combo "${comboName}" strategy should be "quota-share"`
|
||||
);
|
||||
|
||||
// Verify no second combo by scanning all combos for the same name.
|
||||
const all = await combosDb.getCombos();
|
||||
|
||||
@@ -15,8 +15,8 @@
|
||||
* (one step pinned to connA). That assertion encoded the OLD COLLISION BUG — a
|
||||
* second same-provider connection would overwrite the combo, leaving only the last
|
||||
* connId's step. Task 4 fixes this by grouping all connections into one N-step
|
||||
* fill-first combo per model. D2.5 and D2.6 now assert the CORRECT behavior:
|
||||
* models.length === 2 (both connections) and strategy === "fill-first". This is
|
||||
* quota-share combo per model. D2.5 and D2.6 now assert the CORRECT behavior:
|
||||
* models.length === 2 (both connections) and strategy === "quota-share". This is
|
||||
* alignment to the corrected implementation, NOT masking — the prior assertions
|
||||
* encoded the bug, not the desired behavior.
|
||||
*
|
||||
@@ -30,7 +30,7 @@
|
||||
* D2.4 — enforce: pool with connectionIds [connA, connB]; enforce with connA
|
||||
* (the primary) still finds the pool — no regression on primary.
|
||||
* D2.5 — combos: syncQuotaCombos for a 2-connection same-provider pool creates
|
||||
* one combo per model with 2 steps (both connIds) + strategy fill-first.
|
||||
* one combo per model with 2 steps (both connIds) + strategy quota-share.
|
||||
* D2.6 — combos: prune — after removing connB from the pool (→ only connA),
|
||||
* re-sync collapses each combo to 1 step (connA only).
|
||||
*/
|
||||
@@ -55,9 +55,8 @@ const providersDb = await import("../../src/lib/db/providers.ts");
|
||||
const combosDb = await import("../../src/lib/db/combos.ts");
|
||||
const { resolveQuotaKeyScope } = await import("../../src/lib/quota/quotaKey.ts");
|
||||
const { syncQuotaCombos } = await import("../../src/lib/quota/quotaCombos.ts");
|
||||
const { isQuotaModelName, parseQuotaModelName, quotaModelName } = await import(
|
||||
"../../src/lib/quota/quotaModelNaming.ts"
|
||||
);
|
||||
const { isQuotaModelName, parseQuotaModelName, quotaModelName } =
|
||||
await import("../../src/lib/quota/quotaModelNaming.ts");
|
||||
const { PROVIDER_MODELS } = await import("../../open-sse/config/providerModels.ts");
|
||||
|
||||
// Trigger migration once at module load so the schema is ready for the first
|
||||
@@ -124,7 +123,9 @@ test.after(() => {
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function listQuotaCombos(): Promise<Array<{ name: string; models: unknown[]; strategy: unknown }>> {
|
||||
async function listQuotaCombos(): Promise<
|
||||
Array<{ name: string; models: unknown[]; strategy: unknown }>
|
||||
> {
|
||||
const all = await combosDb.getCombos();
|
||||
return all
|
||||
.filter((c) => typeof c.name === "string" && isQuotaModelName(c.name))
|
||||
@@ -263,9 +264,7 @@ test("D2.3: enforceQuotaShare — input connectionId matching a non-primary memb
|
||||
|
||||
// Assign an API key to the pool.
|
||||
const API_KEY_ID = "test-key-d23";
|
||||
poolsDb.upsertAllocations(pool.id, [
|
||||
{ apiKeyId: API_KEY_ID, weight: 50, policy: "hard" },
|
||||
]);
|
||||
poolsDb.upsertAllocations(pool.id, [{ apiKeyId: API_KEY_ID, weight: 50, policy: "hard" }]);
|
||||
|
||||
// Confirm allocation exists.
|
||||
const allocations = listAllocationsForApiKey(API_KEY_ID);
|
||||
@@ -320,9 +319,7 @@ test("D2.4: enforceQuotaShare — input connectionId matching the PRIMARY member
|
||||
});
|
||||
|
||||
const API_KEY_ID = "test-key-d24";
|
||||
poolsDb.upsertAllocations(pool.id, [
|
||||
{ apiKeyId: API_KEY_ID, weight: 50, policy: "hard" },
|
||||
]);
|
||||
poolsDb.upsertAllocations(pool.id, [{ apiKeyId: API_KEY_ID, weight: 50, policy: "hard" }]);
|
||||
|
||||
// Enforce with connA (the primary).
|
||||
const resultA = await enforceQuotaShare({
|
||||
@@ -342,9 +339,9 @@ test("D2.4: enforceQuotaShare — input connectionId matching the PRIMARY member
|
||||
// D2.5 — combos: syncQuotaCombos for 2-connection same-provider pool
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
test("D2.5: syncQuotaCombos — 2-connection same-provider pool creates one combo per model with 2 steps + fill-first (Task 4)", async () => {
|
||||
test("D2.5: syncQuotaCombos — 2-connection same-provider pool creates one combo per model with 2 steps + quota-share (Task 4)", async () => {
|
||||
// Task 4: N same-provider connections must produce ONE combo per model with
|
||||
// ALL connections' steps + strategy "fill-first". The old behavior (single step
|
||||
// ALL connections' steps + strategy "quota-share". The old behavior (single step
|
||||
// pinned to connA, strategy "priority") was the collision bug — last upsert won.
|
||||
const connA = await providersDb.createProviderConnection({
|
||||
provider: PROVIDER_A,
|
||||
@@ -384,7 +381,7 @@ test("D2.5: syncQuotaCombos — 2-connection same-provider pool creates one comb
|
||||
const quotaCombos = await listQuotaCombos();
|
||||
const comboMap = new Map(quotaCombos.map((c) => [c.name, c]));
|
||||
|
||||
// ── Verify PROVIDER_A combos exist with N-step fill-first ─────────────────
|
||||
// ── Verify PROVIDER_A combos exist with N-step quota-share ─────────────────
|
||||
for (const modelId of modelsA) {
|
||||
// Combos are named with the GROUP name ("GroupDemo", from group-demo), not pool name.
|
||||
const expectedName = quotaModelName("GroupDemo", PROVIDER_A, modelId);
|
||||
@@ -398,11 +395,11 @@ test("D2.5: syncQuotaCombos — 2-connection same-provider pool creates one comb
|
||||
`combo ${expectedName} should have 2 steps (both connections), got ${combo.models.length}`
|
||||
);
|
||||
|
||||
// Task 4: strategy must be fill-first.
|
||||
// Task 4: strategy must be quota-share.
|
||||
assert.equal(
|
||||
combo.strategy,
|
||||
"fill-first",
|
||||
`combo ${expectedName} strategy should be "fill-first", got "${combo.strategy}"`
|
||||
"quota-share",
|
||||
`combo ${expectedName} strategy should be "quota-share", got "${combo.strategy}"`
|
||||
);
|
||||
|
||||
// Both connIds must appear across steps.
|
||||
@@ -429,7 +426,7 @@ test("D2.5: syncQuotaCombos — 2-connection same-provider pool creates one comb
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
test("D2.6: syncQuotaCombos — after removing connB from same-provider pool, re-sync collapses each combo to 1 step (connA only)", async () => {
|
||||
// Task 4: initially a 2-connection pool produces 2-step fill-first combos.
|
||||
// Task 4: initially a 2-connection pool produces 2-step quota-share combos.
|
||||
// After removing connB (pool → only connA), re-sync rebuilds each combo with
|
||||
// a single step pinned to connA. The combo names are unchanged (same provider/
|
||||
// model), so no prune happens — the combos are updated in-place.
|
||||
@@ -498,7 +495,10 @@ test("D2.6: syncQuotaCombos — after removing connB from same-provider pool, re
|
||||
const afterProviders = new Set(
|
||||
after.map((c) => parseQuotaModelName(c.name)?.provider).filter(Boolean)
|
||||
);
|
||||
assert.ok(afterProviders.has(PROVIDER_A), `${PROVIDER_A} combos should survive after connB removal`);
|
||||
assert.ok(
|
||||
afterProviders.has(PROVIDER_A),
|
||||
`${PROVIDER_A} combos should survive after connB removal`
|
||||
);
|
||||
for (const modelId of modelsA) {
|
||||
const expectedName = quotaModelName("GroupDemo", PROVIDER_A, modelId);
|
||||
const found = after.find((c) => c.name === expectedName);
|
||||
|
||||
312
tests/unit/quota-share-strategy.test.ts
Normal file
312
tests/unit/quota-share-strategy.test.ts
Normal file
@@ -0,0 +1,312 @@
|
||||
/**
|
||||
* Unit tests for the dedicated quota-share strategy (Phase 3 #9).
|
||||
*
|
||||
* Covers the three mechanisms of the isolated strategy module plus its
|
||||
* activation wiring:
|
||||
* 1. per-model bucket gating (accountBuckets.isBucketSaturated)
|
||||
* 2. DRR (deficit round-robin, quantum proportional to weight)
|
||||
* 3. P2C over real in-flight counters (quotaShareInflight)
|
||||
* 4. fail-open behavior (no data / all saturated / empty targets)
|
||||
* 5. qtSd/ combos are minted with strategy "quota-share"
|
||||
*
|
||||
* Runner: node:test + assert/strict (NO vitest, NO jest). Clock is injected via
|
||||
* the nowMs param on every call — the tested path never reads Date.now().
|
||||
*/
|
||||
|
||||
import { test, describe, beforeEach } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import {
|
||||
selectQuotaShareTarget,
|
||||
_clearDrrStateForTest,
|
||||
_getDrrDeficitForTest,
|
||||
} from "../../open-sse/services/combo/quotaShareStrategy.ts";
|
||||
import {
|
||||
incrementInflight,
|
||||
decrementInflight,
|
||||
getInflight,
|
||||
_clearInflightForTest,
|
||||
DEFAULT_LEASE_MS,
|
||||
} from "../../open-sse/services/combo/quotaShareInflight.ts";
|
||||
import {
|
||||
recordUsage,
|
||||
_clearBucketsForTest,
|
||||
} from "../../src/lib/quota/accountBuckets.ts";
|
||||
import { QUOTA_SHARE_STRATEGY } from "../../src/lib/quota/quotaCombos.ts";
|
||||
import { INTERNAL_ROUTING_STRATEGY_VALUES } from "../../src/shared/constants/routingStrategies.ts";
|
||||
import type { ResolvedComboTarget } from "../../open-sse/services/combo/types.ts";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const NOW = Date.UTC(2026, 5, 24, 12, 0, 0); // 2026-06-24T12:00:00.000Z
|
||||
const RESET_AT = new Date(NOW + 3_600_000).toISOString(); // +1h (window still open)
|
||||
|
||||
function makeTarget(
|
||||
executionKey: string,
|
||||
connectionId: string,
|
||||
weight = 100,
|
||||
modelStr = "anthropic/claude-sonnet-4-5"
|
||||
): ResolvedComboTarget {
|
||||
return {
|
||||
kind: "model",
|
||||
stepId: `step-${executionKey}`,
|
||||
executionKey,
|
||||
modelStr,
|
||||
provider: modelStr.split("/")[0],
|
||||
providerId: null,
|
||||
connectionId,
|
||||
weight,
|
||||
label: null,
|
||||
};
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Setup — reset all module state before each case for isolation.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
beforeEach(() => {
|
||||
_clearBucketsForTest();
|
||||
_clearDrrStateForTest();
|
||||
_clearInflightForTest();
|
||||
});
|
||||
|
||||
// ─── quotaShareInflight ─────────────────────────────────────────────────────
|
||||
|
||||
describe("quotaShareInflight", () => {
|
||||
test("increment → getInflight returns 1", () => {
|
||||
incrementInflight("conn-a", DEFAULT_LEASE_MS, NOW);
|
||||
assert.equal(getInflight("conn-a", NOW), 1);
|
||||
});
|
||||
|
||||
test("increment twice → getInflight returns 2", () => {
|
||||
incrementInflight("conn-a", DEFAULT_LEASE_MS, NOW);
|
||||
incrementInflight("conn-a", DEFAULT_LEASE_MS, NOW);
|
||||
assert.equal(getInflight("conn-a", NOW), 2);
|
||||
});
|
||||
|
||||
test("decrement after increment → returns 0", () => {
|
||||
incrementInflight("conn-a", DEFAULT_LEASE_MS, NOW);
|
||||
decrementInflight("conn-a", NOW);
|
||||
assert.equal(getInflight("conn-a", NOW), 0);
|
||||
});
|
||||
|
||||
test("decrement below 0 is safe (floors at 0)", () => {
|
||||
decrementInflight("conn-a", NOW); // no prior increment
|
||||
assert.equal(getInflight("conn-a", NOW), 0);
|
||||
});
|
||||
|
||||
test("TTL expiry: expired slot returns 0 without an explicit decrement", () => {
|
||||
const leaseMs = 1_000;
|
||||
incrementInflight("conn-b", leaseMs, NOW);
|
||||
assert.equal(getInflight("conn-b", NOW), 1);
|
||||
assert.equal(getInflight("conn-b", NOW + leaseMs + 1), 0);
|
||||
});
|
||||
|
||||
test("empty connectionId returns 0 (fail-open)", () => {
|
||||
assert.equal(getInflight("", NOW), 0);
|
||||
incrementInflight("", DEFAULT_LEASE_MS, NOW); // must not throw / not store
|
||||
assert.equal(getInflight("", NOW), 0);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── per-model bucket gating ────────────────────────────────────────────────
|
||||
|
||||
describe("gating: per-model bucket saturation", () => {
|
||||
test("saturated 5h window → that connection is deprioritized, the clean one wins", () => {
|
||||
recordUsage("conn-sat", "5h", 100, RESET_AT, NOW);
|
||||
|
||||
const targets = [makeTarget("ek-sat", "conn-sat"), makeTarget("ek-ok", "conn-ok")];
|
||||
const result = selectQuotaShareTarget(targets, "combo-5h", "anthropic/claude-sonnet-4-5", NOW);
|
||||
|
||||
assert.ok(result.target !== null, "must return a target");
|
||||
assert.equal(result.target.executionKey, "ek-ok", "non-saturated target must be selected");
|
||||
// Saturated target stays available as a fallback (deprioritized, not dropped).
|
||||
assert.ok(
|
||||
result.orderedTargets.some((t) => t.executionKey === "ek-sat"),
|
||||
"saturated target must still appear in the fallback list"
|
||||
);
|
||||
});
|
||||
|
||||
test("saturated 7d window → that connection is deprioritized", () => {
|
||||
recordUsage("conn-7d", "7d", 100, RESET_AT, NOW);
|
||||
const targets = [makeTarget("ek-7d", "conn-7d"), makeTarget("ek-clean", "conn-clean")];
|
||||
|
||||
const result = selectQuotaShareTarget(targets, "combo-7d", "anthropic/claude-sonnet-4-5", NOW);
|
||||
assert.equal(result.target?.executionKey, "ek-clean");
|
||||
});
|
||||
|
||||
test("saturated per-model 7d window for the requested model → deprioritized", () => {
|
||||
recordUsage("conn-pm", "7d:claude-sonnet-4-5", 100, RESET_AT, NOW);
|
||||
const targets = [
|
||||
makeTarget("ek-pm", "conn-pm", 100, "anthropic/claude-sonnet-4-5"),
|
||||
makeTarget("ek-other", "conn-other", 100, "anthropic/claude-sonnet-4-5"),
|
||||
];
|
||||
|
||||
const result = selectQuotaShareTarget(targets, "combo-pm", "anthropic/claude-sonnet-4-5", NOW);
|
||||
assert.equal(result.target?.executionKey, "ek-other");
|
||||
});
|
||||
|
||||
test("per-model saturation for a DIFFERENT model does NOT deprioritize", () => {
|
||||
// conn-x is saturated only on 7d:claude-opus-4, but the request is for sonnet.
|
||||
recordUsage("conn-x", "7d:claude-opus-4", 100, RESET_AT, NOW);
|
||||
const targets = [makeTarget("ek-x", "conn-x", 100, "anthropic/claude-sonnet-4-5")];
|
||||
|
||||
const result = selectQuotaShareTarget(targets, "combo-other-model", "anthropic/claude-sonnet-4-5", NOW);
|
||||
assert.equal(result.target?.executionKey, "ek-x", "must not be gated by an unrelated model window");
|
||||
});
|
||||
|
||||
test("fail-open: all targets saturated → all eligible again, still returns a target", () => {
|
||||
recordUsage("conn-1", "5h", 100, RESET_AT, NOW);
|
||||
recordUsage("conn-2", "5h", 100, RESET_AT, NOW);
|
||||
const targets = [makeTarget("ek-1", "conn-1"), makeTarget("ek-2", "conn-2")];
|
||||
|
||||
const result = selectQuotaShareTarget(targets, "combo-fo", "anthropic/claude-sonnet-4-5", NOW);
|
||||
assert.ok(result.target !== null, "must not return null when everything is saturated");
|
||||
assert.equal(result.orderedTargets.length, 2, "both targets remain dispatchable");
|
||||
});
|
||||
|
||||
test("empty targets → returns null (fail-open, no crash)", () => {
|
||||
const result = selectQuotaShareTarget([], "combo-empty", "anthropic/claude-sonnet-4-5", NOW);
|
||||
assert.equal(result.target, null);
|
||||
assert.deepEqual(result.orderedTargets, []);
|
||||
});
|
||||
|
||||
test("no buckets recorded at all → fail-open, first/healthy target selected", () => {
|
||||
const targets = [makeTarget("ek-a", "conn-a"), makeTarget("ek-b", "conn-b")];
|
||||
const result = selectQuotaShareTarget(targets, "combo-nodata", "anthropic/claude-sonnet-4-5", NOW);
|
||||
assert.ok(result.target !== null);
|
||||
assert.equal(result.orderedTargets.length, 2);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── DRR ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
describe("DRR: deficit round robin", () => {
|
||||
test("equal weight: two connections alternate across consecutive calls", () => {
|
||||
const t1 = makeTarget("ek-1", "conn-1", 100);
|
||||
const t2 = makeTarget("ek-2", "conn-2", 100);
|
||||
|
||||
const r1 = selectQuotaShareTarget([t1, t2], "combo-drr", "anthropic/claude-sonnet-4-5", NOW);
|
||||
const r2 = selectQuotaShareTarget([t1, t2], "combo-drr", "anthropic/claude-sonnet-4-5", NOW);
|
||||
|
||||
const selected = [r1.target?.executionKey, r2.target?.executionKey];
|
||||
assert.ok(selected.includes("ek-1"), "ek-1 must be selected at some point");
|
||||
assert.ok(selected.includes("ek-2"), "ek-2 must be selected at some point");
|
||||
});
|
||||
|
||||
test("higher weight connection receives proportionally more selections (2:1)", () => {
|
||||
const t1 = makeTarget("ek-heavy", "conn-heavy", 200); // 2x weight
|
||||
const t2 = makeTarget("ek-light", "conn-light", 100); // 1x weight
|
||||
|
||||
const counts: Record<string, number> = { "ek-heavy": 0, "ek-light": 0 };
|
||||
for (let i = 0; i < 30; i++) {
|
||||
const r = selectQuotaShareTarget(
|
||||
[t1, t2],
|
||||
"combo-weighted",
|
||||
"anthropic/claude-sonnet-4-5",
|
||||
NOW
|
||||
);
|
||||
if (r.target) counts[r.target.executionKey]++;
|
||||
// Simulate the request settling (as a real caller's finally handler would),
|
||||
// so the P2C in-flight tie-break sees equal load and the DRR weighting shows.
|
||||
r.decrementInflight();
|
||||
}
|
||||
assert.ok(
|
||||
counts["ek-heavy"] > counts["ek-light"],
|
||||
`heavy (${counts["ek-heavy"]}) should be selected more than light (${counts["ek-light"]})`
|
||||
);
|
||||
// Roughly 2:1 — heavy should be at least ~1.5x light over 30 rounds.
|
||||
assert.ok(
|
||||
counts["ek-heavy"] >= counts["ek-light"] * 1.5,
|
||||
`heavy/light ratio should be ~2:1, got ${counts["ek-heavy"]}:${counts["ek-light"]}`
|
||||
);
|
||||
});
|
||||
|
||||
test("DRR state is isolated per comboName", () => {
|
||||
const t = makeTarget("ek-shared", "conn-shared", 100);
|
||||
selectQuotaShareTarget([t], "combo-A", "anthropic/claude-sonnet-4-5", NOW);
|
||||
|
||||
const deficitA = _getDrrDeficitForTest("combo-A", "ek-shared");
|
||||
const deficitB = _getDrrDeficitForTest("combo-B", "ek-shared");
|
||||
assert.equal(deficitB, 0, "combo-B deficit must remain 0 (isolated)");
|
||||
assert.equal(deficitA, 0, "the selected target's deficit must be reset to 0");
|
||||
});
|
||||
|
||||
test("single eligible target is returned unchanged", () => {
|
||||
const t = makeTarget("ek-solo", "conn-solo", 100);
|
||||
const result = selectQuotaShareTarget([t], "combo-solo", "anthropic/claude-sonnet-4-5", NOW);
|
||||
assert.equal(result.target?.executionKey, "ek-solo");
|
||||
assert.equal(result.orderedTargets.length, 1);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── P2C in-flight ────────────────────────────────────────────────────────────
|
||||
|
||||
describe("P2C in-flight", () => {
|
||||
test("between two candidates, the one with fewer in-flight wins over the DRR pick", () => {
|
||||
const t1 = makeTarget("ek-busy", "conn-busy", 100);
|
||||
const t2 = makeTarget("ek-free", "conn-free", 100);
|
||||
|
||||
// Pre-load conn-busy with active in-flight requests.
|
||||
incrementInflight("conn-busy", DEFAULT_LEASE_MS, NOW);
|
||||
incrementInflight("conn-busy", DEFAULT_LEASE_MS, NOW);
|
||||
|
||||
const result = selectQuotaShareTarget([t1, t2], "combo-p2c", "anthropic/claude-sonnet-4-5", NOW);
|
||||
assert.equal(
|
||||
result.target?.executionKey,
|
||||
"ek-free",
|
||||
"the target with the lower in-flight load must win the P2C tie-break"
|
||||
);
|
||||
});
|
||||
|
||||
test("decrementInflight() releases the in-flight load", () => {
|
||||
const t = makeTarget("ek-dec", "conn-dec", 100);
|
||||
const result = selectQuotaShareTarget([t], "combo-dec", "anthropic/claude-sonnet-4-5", NOW);
|
||||
assert.equal(getInflight("conn-dec", NOW), 1, "in-flight should be 1 after a dispatch");
|
||||
result.decrementInflight();
|
||||
assert.equal(getInflight("conn-dec", NOW), 0, "in-flight should be 0 after the decrement");
|
||||
});
|
||||
|
||||
test("decrementInflight() is idempotent (safe to call more than once)", () => {
|
||||
const t = makeTarget("ek-idem", "conn-idem", 100);
|
||||
const result = selectQuotaShareTarget([t], "combo-idem", "anthropic/claude-sonnet-4-5", NOW);
|
||||
result.decrementInflight();
|
||||
result.decrementInflight(); // must not go negative or throw
|
||||
assert.equal(getInflight("conn-idem", NOW), 0);
|
||||
});
|
||||
|
||||
test("TTL releases in-flight automatically when decrement is never called (abort fallback)", () => {
|
||||
const t = makeTarget("ek-ttl", "conn-ttl", 100);
|
||||
const result = selectQuotaShareTarget([t], "combo-ttl", "anthropic/claude-sonnet-4-5", NOW);
|
||||
// Simulate an aborted request: do NOT call result.decrementInflight().
|
||||
assert.equal(getInflight("conn-ttl", NOW), 1);
|
||||
assert.equal(
|
||||
getInflight("conn-ttl", NOW + DEFAULT_LEASE_MS + 1),
|
||||
0,
|
||||
"the slot must auto-expire after the lease, preventing a permanent leak"
|
||||
);
|
||||
// The returned callback must remain referenced to satisfy the contract.
|
||||
assert.equal(typeof result.decrementInflight, "function");
|
||||
});
|
||||
});
|
||||
|
||||
// ─── activation wiring ──────────────────────────────────────────────────────
|
||||
|
||||
describe("activation: qtSd/ combos use strategy 'quota-share'", () => {
|
||||
test("QUOTA_SHARE_STRATEGY constant equals 'quota-share'", () => {
|
||||
assert.equal(
|
||||
QUOTA_SHARE_STRATEGY,
|
||||
"quota-share",
|
||||
"quotaCombos must mint qtSd/ combos with the 'quota-share' strategy"
|
||||
);
|
||||
});
|
||||
|
||||
test("'quota-share' is registered as an INTERNAL routing strategy", () => {
|
||||
assert.ok(
|
||||
(INTERNAL_ROUTING_STRATEGY_VALUES as readonly string[]).includes("quota-share"),
|
||||
"'quota-share' must be in the internal (non-UI) routing strategy list"
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -134,8 +134,10 @@ test("parseSSEToOpenAIResponse still returns null for an error-only SSE (boundar
|
||||
|
||||
test("PART 1: windsurf authHint references the `Windsurf: Provide Auth Token` command", () => {
|
||||
const here = path.dirname(fileURLToPath(import.meta.url));
|
||||
// After the providers.ts oauth-constants split, the windsurf authHint moved to
|
||||
// src/shared/constants/providers/oauth.ts.
|
||||
const providers = readFileSync(
|
||||
path.join(here, "../../src/shared/constants/providers.ts"),
|
||||
path.join(here, "../../src/shared/constants/providers/oauth.ts"),
|
||||
"utf8"
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user