mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-31 04:12:10 +03:00
refactor(combo): extract round-robin sticky state to combo/rrState.ts (QG v2 Fase 9 T5 D7a) (#4196)
Integrated into release/v3.8.29 — D7a round-robin sticky state extracted byte-identically to combo/rrState.ts; reconciled with #4194 (baseline combo.ts -> 3398, resolving the over-cap left by #4194).
This commit is contained in:
committed by
GitHub
parent
7e534d6a86
commit
f06a90d37f
@@ -11,6 +11,7 @@
|
||||
"_rebaseline_2026_06_18_qg9_combo_split_d5": "QG v2 Fase 9 T5 D5: combo.ts 4589->4430 — target sorters extracted byte-identically to the new open-sse/services/combo/targetSorters.ts (<cap, normalizeModelEntry/selectWeightedTarget/orderTargetsForWeightedFallback/sortModelsByCost/sortTargetsByCost/sortModelsByUsage/sortTargetsByUsage/getP2CTargetScore/orderTargetsByPowerOfTwoChoices). None were ever public, so combo.ts imports back only the six still called by code that stays (no re-export). getComboStepTarget/getComboStepWeight imports dropped from combo.ts (only normalizeModelEntry used them). Pure move, no logic change.",
|
||||
"_rebaseline_2026_06_18_qg9_combo_split_d6": "QG v2 Fase 9 T5 D6: combo.ts 4430->3819 — combo structure resolution extracted byte-identically to the new open-sse/services/combo/comboStructure.ts (638, <cap): runtime-step normalization, nested-combo/DAG expansion (resolveNestedComboTargets/expandRuntimeStep/getComboFromData/getComboModelsFromData/validateComboDAG/resolveNestedComboModels), weighted/direct resolution (resolveComboTargets/resolveWeightedTargets/getDirectComboTargets + composite-tier ordering), request-compatibility filtering (filterTargetsByRequestCompatibility + deriveRequestCompatibilityRequirements/getTargetCompatibilityFailures and their private helpers) and context-size sorting. Shared dedupeTargetsByExecutionKey + the toTrimmedString/toComboLike/getCombosArray normalizers also moved (dedupe to comboData.ts since both combo.ts and comboStructure use it). The 7 previously-public symbols are re-exported from combo.ts; the ~20 external consumers (chatCore.ts, /api/combos routes, embeddings, usage, catalog) are unchanged. shadowRouting.ts's resolveNestedComboTargets import redirected from the barrel to ./comboStructure.ts (removing the D4 temporary edge). Orphaned combo.ts imports dropped (getComboModelString/normalizeComboStep, getModelContextLimit, getResolvedModelCapabilities, getTargetProvider, MAX_COMBO_DEPTH, and 6 now-unused type imports). Pure move, no logic change.",
|
||||
"_rebaseline_2026_06_18_qg9_combo_split_d8": "QG v2 Fase 9 T5 D8 (reduced): combo.ts 3819->3432 — auto-strategy scoring/intent/tag-routing extracted byte-identically to the new open-sse/services/combo/autoStrategy.ts (434, <cap): the quota-soft execution-candidate registry (the single _activeExecutionCandidates Map + setCandidateQuotaSoftPenalty/_registerExecutionCandidates/_unregisterExecutionCandidates, kept together for state cohesion), QUOTA_SOFT_DEPRIORITIZE_FACTOR, scoreAutoTargets, expandAutoComboCandidatePool, intent extraction (toTextContent/extractPromptForIntent/mapIntentToTaskType/toStringArray/getIntentConfig), applyRequestTagRouting and deriveComboSessionKey. buildAutoCandidates + its two private-only helpers calculateTargetContextAffinity/getBootstrapLatencyMs (and the DEFAULT_MODEL_P95_MS/MIN_HISTORY_SAMPLES/OUTPUT_TOKEN_RATIO consts they own) were DELIBERATELY KEPT in combo.ts: buildAutoCandidates is the sole user of the internal reset-window helpers (resolveResetWindowConfig/fetchResetAwareQuotaWithCache/calculateResetWindowAffinity + the ResetWindowConfig type), so keeping it there leaves those helpers private (no export) and avoids a combo <-> autoStrategy import cycle (a later task can move the reset-window block out of combo.ts without breaking autoStrategy). autoStrategy.ts never imports from the combo barrel. dedupeTargetsByExecutionKey was already in comboData.ts (D6) and is NOT re-moved. QUOTA_SOFT_DEPRIORITIZE_FACTOR + setCandidateQuotaSoftPenalty stay re-exported from combo.ts for chatCore.ts's dynamic import(\"../services/combo\"); scoreAutoTargets/expandAutoComboCandidatePool keep their public re-export too. Orphaned combo.ts imports dropped where moved-out symbols stopped being referenced; ProviderCandidate/AutoProviderCandidate/HistoricalLatencyStatsEntry types are imported into combo.ts for buildAutoCandidates. Pure move, no logic change.",
|
||||
"_rebaseline_2026_06_18_qg9_combo_split_d7a": "QG v2 Fase 9 T5 D7a: combo.ts 3440->3398 (wc -l 3397 + 1, stacked on #4194 which added the passthrough-quota guards) — round-robin sticky state extracted byte-identically to the new open-sse/services/combo/rrState.ts (71, <cap). Moved exactly the 6 RR-state symbols: MAX_RR_COUNTERS, the two mutable module Maps rrCounters (new Map<string,number>) + rrStickyTargets, and the three helpers clampStickyRoundRobinTargetLimit/getStickyRoundRobinStartIndex/recordStickyRoundRobinSuccess. STATE COHESION: rrCounters and rrStickyTargets remain SINGLE instances defined once in rrState.ts; combo.ts imports the same references back and keeps mutating them directly in orderTargetsByResetAwareQuota/orderTargetsByResetWindow/handleRoundRobinCombo (no Map duplicated — sticky-3 round-robin behavior preserved, guarded by combo-routing-e2e.test.ts). The quota state left behind (MAX_RESET_AWARE_CACHE/resetAwareConnectionCache/resetAwareQuotaCache) stays in combo.ts for a later task (D7b). None of the 6 were ever public, so combo.ts imports all six back (no re-export). rrState.ts never imports from the combo barrel; it imports only ResolvedComboTarget from ./types.ts. Pure move, no logic change.",
|
||||
"_rebaseline_2026_06_18_8_2_sliding_window": "Fase 8.2 own growth: rateLimitManager.ts 1017->1022 (+5 = one import + one `await awaitProviderDefaultSlot(...)` call + a 2-line comment at the existing withRateLimit chokepoint). All sliding-window logic was extracted to the new open-sse/services/providerDefaultRateLimit.ts + open-sse/services/slidingWindowLimiter.ts (both <cap), NOT inlined. Thin wiring only; not further shrinkable.",
|
||||
"_rebaseline_2026_06_18_8_1_no_thinking_alias": "Fase 8.1 own growth: catalog.ts 1435->1440 (+5 = appendNoThinkingVariants(finalModels) call + comment at the existing finalModels chokepoint) and chat.ts 1458->1471 (+13 = applyNoThinkingAlias(body) call + comment right after body.model is read, before model resolution). All real logic lives in the new open-sse/utils/noThinkingAlias.ts (<cap); both edits are thin wiring of tested helpers at the single correct integration point in each file. Not extractable.",
|
||||
"_rebaseline_2026_06_18_4_4_midstream": "Fase 4.4 own growth: chatCore.ts 5980->6009 (+29 at the existing streaming-return chokepoint = capture streamRecovery.continueMidStream alongside .enabled; refactor the early-retry reopen thunk into a shared runUpstreamStream(body) helper — net DRY — and add the gated continueStream(assistantSoFar) thunk that re-runs the upstream with makeContinuationBody(bodyToSend, …), plus the onContinue log). All continuation logic (scanOpenAiSseText, makeContinuationBody, trimContinuationOverlap, the createRecoverableStream continuation path) lives in open-sse/services/streamRecovery.ts (<cap). Closes over the same per-attempt executor locals as the existing reopen; not extractable without hiding the dispatch boundary. Opt-in (default OFF).",
|
||||
@@ -76,7 +77,7 @@
|
||||
"open-sse/services/batchProcessor.ts": 828,
|
||||
"open-sse/services/browserBackedChat.ts": 850,
|
||||
"open-sse/services/claudeCodeCompatible.ts": 1202,
|
||||
"open-sse/services/combo.ts": 3432,
|
||||
"open-sse/services/combo.ts": 3398,
|
||||
"open-sse/services/rateLimitManager.ts": 1035,
|
||||
"open-sse/services/tokenRefresh.ts": 1997,
|
||||
"open-sse/services/usage.ts": 3408,
|
||||
|
||||
@@ -99,6 +99,14 @@ import type {
|
||||
HistoricalLatencyStatsEntry,
|
||||
} from "./combo/types.ts";
|
||||
|
||||
import {
|
||||
MAX_RR_COUNTERS,
|
||||
rrCounters,
|
||||
rrStickyTargets,
|
||||
clampStickyRoundRobinTargetLimit,
|
||||
getStickyRoundRobinStartIndex,
|
||||
recordStickyRoundRobinSuccess,
|
||||
} from "./combo/rrState.ts";
|
||||
import { validateResponseQuality, toRetryAfterDisplayValue } from "./combo/validateQuality.ts";
|
||||
import {
|
||||
TRANSIENT_FOR_SEMAPHORE,
|
||||
@@ -186,15 +194,8 @@ type QuotaFetchCacheConfig = {
|
||||
};
|
||||
type ResetWindowConfig = ReturnType<typeof resolveResetWindowConfig>;
|
||||
|
||||
// In-memory atomic counter per combo for round-robin distribution
|
||||
// Resets on server restart (by design — no stale state)
|
||||
// Eviction limits to prevent unbounded memory growth
|
||||
const MAX_RR_COUNTERS = 500;
|
||||
const MAX_RESET_AWARE_CACHE = 200;
|
||||
|
||||
const rrCounters = new Map<string, number>();
|
||||
const rrStickyTargets = new Map<string, { executionKey: string; successCount: number }>();
|
||||
|
||||
const resetAwareConnectionCache = new Map<
|
||||
string,
|
||||
{ fetchedAt: number; connections: Array<Record<string, unknown>> }
|
||||
@@ -204,50 +205,6 @@ const resetAwareQuotaCache = new Map<
|
||||
{ fetchedAt: number; quota: unknown; refreshPromise: Promise<unknown> | null }
|
||||
>();
|
||||
|
||||
function clampStickyRoundRobinTargetLimit(value: unknown): number {
|
||||
const numericValue = Number(value);
|
||||
if (!Number.isFinite(numericValue)) return 1;
|
||||
return Math.min(Math.max(Math.floor(numericValue), 1), 1000);
|
||||
}
|
||||
|
||||
function getStickyRoundRobinStartIndex(
|
||||
comboName: string,
|
||||
targets: ResolvedComboTarget[],
|
||||
stickyLimit: number
|
||||
): { startIndex: number; counter: number } {
|
||||
const sticky = rrStickyTargets.get(comboName);
|
||||
const stickyIndex = sticky
|
||||
? targets.findIndex((target) => target.executionKey === sticky.executionKey)
|
||||
: -1;
|
||||
if (stickyLimit > 1 && sticky && stickyIndex >= 0 && sticky.successCount < stickyLimit) {
|
||||
return { startIndex: stickyIndex, counter: rrCounters.get(comboName) || 0 };
|
||||
}
|
||||
|
||||
const counter = rrCounters.get(comboName) || 0;
|
||||
return { startIndex: counter % targets.length, counter };
|
||||
}
|
||||
|
||||
function recordStickyRoundRobinSuccess(
|
||||
comboName: string,
|
||||
target: ResolvedComboTarget,
|
||||
stickyLimit: number,
|
||||
targets: ResolvedComboTarget[]
|
||||
): void {
|
||||
const sticky = rrStickyTargets.get(comboName);
|
||||
const successCount = sticky?.executionKey === target.executionKey ? sticky.successCount + 1 : 1;
|
||||
if (successCount >= stickyLimit) {
|
||||
const servedIndex = targets.findIndex((entry) => entry.executionKey === target.executionKey);
|
||||
rrCounters.set(
|
||||
comboName,
|
||||
servedIndex >= 0 ? servedIndex + 1 : (rrCounters.get(comboName) || 0) + 1
|
||||
);
|
||||
rrStickyTargets.delete(comboName);
|
||||
return;
|
||||
}
|
||||
|
||||
rrStickyTargets.set(comboName, { executionKey: target.executionKey, successCount });
|
||||
}
|
||||
|
||||
function finiteNumberOrNull(value: unknown): number | null {
|
||||
const numericValue = Number(value);
|
||||
return Number.isFinite(numericValue) ? numericValue : null;
|
||||
|
||||
71
open-sse/services/combo/rrState.ts
Normal file
71
open-sse/services/combo/rrState.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* Round-robin sticky state for combo routing.
|
||||
*
|
||||
* Holds the two mutable module-level Maps that back round-robin distribution
|
||||
* (`rrCounters`) and sticky round-robin target affinity (`rrStickyTargets`),
|
||||
* plus the helpers that read/write them. Extracted byte-identically from
|
||||
* combo.ts (QG v2 Fase 9 T5 D7a).
|
||||
*
|
||||
* State cohesion: these two Maps MUST remain single instances. combo.ts imports
|
||||
* the same references back and mutates them directly (orderTargetsByResetAwareQuota,
|
||||
* orderTargetsByResetWindow, handleRoundRobinCombo) — never duplicate a Map.
|
||||
*
|
||||
* Pure leaf: this module never imports from the combo barrel.
|
||||
*/
|
||||
|
||||
import type { ResolvedComboTarget } from "./types.ts";
|
||||
|
||||
// In-memory atomic counter per combo for round-robin distribution
|
||||
// Resets on server restart (by design — no stale state)
|
||||
// Eviction limits to prevent unbounded memory growth
|
||||
export const MAX_RR_COUNTERS = 500;
|
||||
|
||||
export const rrCounters = new Map<string, number>();
|
||||
export const rrStickyTargets = new Map<
|
||||
string,
|
||||
{ executionKey: string; successCount: number }
|
||||
>();
|
||||
|
||||
export function clampStickyRoundRobinTargetLimit(value: unknown): number {
|
||||
const numericValue = Number(value);
|
||||
if (!Number.isFinite(numericValue)) return 1;
|
||||
return Math.min(Math.max(Math.floor(numericValue), 1), 1000);
|
||||
}
|
||||
|
||||
export function getStickyRoundRobinStartIndex(
|
||||
comboName: string,
|
||||
targets: ResolvedComboTarget[],
|
||||
stickyLimit: number
|
||||
): { startIndex: number; counter: number } {
|
||||
const sticky = rrStickyTargets.get(comboName);
|
||||
const stickyIndex = sticky
|
||||
? targets.findIndex((target) => target.executionKey === sticky.executionKey)
|
||||
: -1;
|
||||
if (stickyLimit > 1 && sticky && stickyIndex >= 0 && sticky.successCount < stickyLimit) {
|
||||
return { startIndex: stickyIndex, counter: rrCounters.get(comboName) || 0 };
|
||||
}
|
||||
|
||||
const counter = rrCounters.get(comboName) || 0;
|
||||
return { startIndex: counter % targets.length, counter };
|
||||
}
|
||||
|
||||
export function recordStickyRoundRobinSuccess(
|
||||
comboName: string,
|
||||
target: ResolvedComboTarget,
|
||||
stickyLimit: number,
|
||||
targets: ResolvedComboTarget[]
|
||||
): void {
|
||||
const sticky = rrStickyTargets.get(comboName);
|
||||
const successCount = sticky?.executionKey === target.executionKey ? sticky.successCount + 1 : 1;
|
||||
if (successCount >= stickyLimit) {
|
||||
const servedIndex = targets.findIndex((entry) => entry.executionKey === target.executionKey);
|
||||
rrCounters.set(
|
||||
comboName,
|
||||
servedIndex >= 0 ? servedIndex + 1 : (rrCounters.get(comboName) || 0) + 1
|
||||
);
|
||||
rrStickyTargets.delete(comboName);
|
||||
return;
|
||||
}
|
||||
|
||||
rrStickyTargets.set(comboName, { executionKey: target.executionKey, successCount });
|
||||
}
|
||||
Reference in New Issue
Block a user