Repository navigation
feat(combo): add sticky weighted selection limit with exhaustion-aware renormalization #4489
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
b20f916
3cca03d
85ab546
6ee6bdb
1857fe0
d95bd9a
36cddd0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -99,16 +99,21 @@ import type { | |||||||||||||||||
| HandleRoundRobinOptions, | ||||||||||||||||||
| ResolvedComboTarget, | ||||||||||||||||||
| AutoProviderCandidate, | ||||||||||||||||||
| ComboRuntimeStep, | ||||||||||||||||||
| HistoricalLatencyStatsEntry, | ||||||||||||||||||
| } from "./combo/types.ts"; | ||||||||||||||||||
|
|
||||||||||||||||||
| import { | ||||||||||||||||||
| MAX_RR_COUNTERS, | ||||||||||||||||||
| rrCounters, | ||||||||||||||||||
| rrStickyTargets, | ||||||||||||||||||
| weightedStickyTargets, | ||||||||||||||||||
| clampStickyRoundRobinTargetLimit, | ||||||||||||||||||
| clampStickyWeightedTargetLimit, | ||||||||||||||||||
| getStickyRoundRobinStartIndex, | ||||||||||||||||||
| recordStickyRoundRobinSuccess, | ||||||||||||||||||
| getStickyWeightedExecutionKey, | ||||||||||||||||||
| recordStickyWeightedSuccess, | ||||||||||||||||||
| } from "./combo/rrState.ts"; | ||||||||||||||||||
| import { validateResponseQuality, toRetryAfterDisplayValue } from "./combo/validateQuality.ts"; | ||||||||||||||||||
| import { | ||||||||||||||||||
|
|
@@ -143,6 +148,7 @@ import { | |||||||||||||||||
| getModelContextLimitForModelString, | ||||||||||||||||||
| resolveComboTargets, | ||||||||||||||||||
| resolveWeightedTargets, | ||||||||||||||||||
| resolveWeightedStepGroups, | ||||||||||||||||||
| sortTargetsByContextSize, | ||||||||||||||||||
| } from "./combo/comboStructure.ts"; | ||||||||||||||||||
| import { | ||||||||||||||||||
|
|
@@ -697,10 +703,31 @@ export async function handleComboChat({ | |||||||||||||||||
| const maxSetRetries = config.maxSetRetries ?? 0; | ||||||||||||||||||
| const setRetryDelayMs = resolveDelayMs(config.setRetryDelayMs, 2000); | ||||||||||||||||||
|
|
||||||||||||||||||
| const isTargetSelectableForWeighted = async (target: ResolvedComboTarget): Promise<boolean> => { | ||||||||||||||||||
| const rawModel = parseModel(target.modelStr).model || target.modelStr; | ||||||||||||||||||
| if (target.provider && getCircuitBreaker(target.provider).getStatus().state === "OPEN") return false; | ||||||||||||||||||
| if ( | ||||||||||||||||||
| resilienceSettings.providerCooldown.enabled && | ||||||||||||||||||
| Boolean(target.provider && target.provider !== "unknown") && | ||||||||||||||||||
| isProviderInCooldown(target.provider, target.connectionId ?? undefined, resilienceSettings) | ||||||||||||||||||
| ) { | ||||||||||||||||||
| return false; | ||||||||||||||||||
| } | ||||||||||||||||||
| if ( | ||||||||||||||||||
| target.provider && | ||||||||||||||||||
| rawModel && | ||||||||||||||||||
| isModelLocked(target.provider, target.connectionId || "", rawModel) | ||||||||||||||||||
| ) { | ||||||||||||||||||
| return false; | ||||||||||||||||||
| } | ||||||||||||||||||
| return isModelAvailable ? await isModelAvailable(target.modelStr, target) : true; | ||||||||||||||||||
| }; | ||||||||||||||||||
|
|
||||||||||||||||||
| // #2562: Expand provider-wildcard steps (e.g. `fta/*`, `openai/gpt-4*`) into | ||||||||||||||||||
| // concrete model entries sourced from the live synced-models catalog + registry. | ||||||||||||||||||
| // Must run before resolveComboTargets so that wildcard-originated steps are | ||||||||||||||||||
| // treated identically to hand-authored entries by all downstream logic. | ||||||||||||||||||
| // Must run before any step-group / target resolution so that wildcard-originated | ||||||||||||||||||
| // steps are treated identically to hand-authored entries by all downstream logic | ||||||||||||||||||
| // (including the sticky-weighted eligibility pass below). | ||||||||||||||||||
| const expandedCombo = await expandProviderWildcardsInCombo(combo); | ||||||||||||||||||
| const expandedAllCombos = allCombos | ||||||||||||||||||
| ? Array.isArray(allCombos) | ||||||||||||||||||
|
|
@@ -713,21 +740,66 @@ export async function handleComboChat({ | |||||||||||||||||
| } | ||||||||||||||||||
| : allCombos; | ||||||||||||||||||
|
|
||||||||||||||||||
| let orderedTargets = | ||||||||||||||||||
| const stickyWeightedLimit = clampStickyWeightedTargetLimit( | ||||||||||||||||||
| (config as Record<string, unknown>).stickyWeightedLimit | ||||||||||||||||||
| ); | ||||||||||||||||||
| if ( | ||||||||||||||||||
| strategy === "weighted" && | ||||||||||||||||||
| !weightedStickyTargets.has(combo.name) && | ||||||||||||||||||
| weightedStickyTargets.size >= MAX_RR_COUNTERS | ||||||||||||||||||
| ) { | ||||||||||||||||||
| const oldest = weightedStickyTargets.keys().next().value; | ||||||||||||||||||
| if (oldest !== undefined) weightedStickyTargets.delete(oldest); | ||||||||||||||||||
| } | ||||||||||||||||||
| let stepGroups: Array<{ step: ComboRuntimeStep; targets: ResolvedComboTarget[] }> | undefined; | ||||||||||||||||||
| const weightedEligibleKeys = new Set<string>(); | ||||||||||||||||||
| if (strategy === "weighted") { | ||||||||||||||||||
| stepGroups = resolveWeightedStepGroups(expandedCombo, expandedAllCombos); | ||||||||||||||||||
| for (const group of stepGroups) { | ||||||||||||||||||
| const availability = await Promise.all(group.targets.map(isTargetSelectableForWeighted)); | ||||||||||||||||||
| if (availability.some(Boolean)) weightedEligibleKeys.add(group.step.executionKey); | ||||||||||||||||||
| } | ||||||||||||||||||
| } | ||||||||||||||||||
| const rawStickyWeightedKey = | ||||||||||||||||||
| strategy === "weighted" ? getStickyWeightedExecutionKey(combo.name, stickyWeightedLimit) : null; | ||||||||||||||||||
| const stickyWeightedKey = | ||||||||||||||||||
| rawStickyWeightedKey && weightedEligibleKeys.has(rawStickyWeightedKey) | ||||||||||||||||||
| ? rawStickyWeightedKey | ||||||||||||||||||
| : null; | ||||||||||||||||||
| if (strategy !== "weighted" || stickyWeightedLimit <= 1) { | ||||||||||||||||||
| weightedStickyTargets.delete(combo.name); | ||||||||||||||||||
| } else if (rawStickyWeightedKey && !stickyWeightedKey) { | ||||||||||||||||||
| weightedStickyTargets.delete(combo.name); | ||||||||||||||||||
| } | ||||||||||||||||||
| const weightedResolution = | ||||||||||||||||||
| strategy === "weighted" | ||||||||||||||||||
| ? resolveWeightedTargets(expandedCombo, expandedAllCombos)?.orderedTargets || [] | ||||||||||||||||||
| : resolveComboTargets( | ||||||||||||||||||
| ? resolveWeightedTargets( | ||||||||||||||||||
| expandedCombo, | ||||||||||||||||||
| expandedAllCombos, | ||||||||||||||||||
| clampComboDepth(config.maxComboDepth) | ||||||||||||||||||
| ); | ||||||||||||||||||
| stickyWeightedKey, | ||||||||||||||||||
| weightedEligibleKeys, | ||||||||||||||||||
| stepGroups | ||||||||||||||||||
| ) | ||||||||||||||||||
| : null; | ||||||||||||||||||
|
Comment on lines
+774
to
+783
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Pass the pre-resolved
Suggested change
|
||||||||||||||||||
| const getWeightedStepKeyForTarget = (target: ResolvedComboTarget): string | null => { | ||||||||||||||||||
| if (!weightedResolution?.orderedSteps) return null; | ||||||||||||||||||
| const step = weightedResolution.orderedSteps.find((entry) => | ||||||||||||||||||
| target.executionKey === entry.executionKey || | ||||||||||||||||||
| target.executionKey.startsWith(entry.executionKey + ">") | ||||||||||||||||||
| ); | ||||||||||||||||||
| return step?.executionKey || null; | ||||||||||||||||||
| }; | ||||||||||||||||||
| let orderedTargets = | ||||||||||||||||||
| strategy === "weighted" | ||||||||||||||||||
| ? weightedResolution?.orderedTargets || [] | ||||||||||||||||||
| : resolveComboTargets(expandedCombo, expandedAllCombos, clampComboDepth(config.maxComboDepth)); | ||||||||||||||||||
|
|
||||||||||||||||||
| orderedTargets = await applyRequestTagRouting(orderedTargets, body, log); | ||||||||||||||||||
|
|
||||||||||||||||||
| if (strategy === "weighted") { | ||||||||||||||||||
| log.info( | ||||||||||||||||||
| "COMBO", | ||||||||||||||||||
| `Weighted selection${allCombos ? " with nested resolution" : ""}: ${orderedTargets.length} total targets` | ||||||||||||||||||
| `Weighted selection${stickyWeightedKey ? " (sticky)" : ""}${allCombos ? " with nested resolution" : ""}: ${orderedTargets.length} total targets` | ||||||||||||||||||
| ); | ||||||||||||||||||
| } else if (allCombos) { | ||||||||||||||||||
| log.info("COMBO", `${strategy} with nested resolution: ${orderedTargets.length} total targets`); | ||||||||||||||||||
|
|
@@ -1575,6 +1647,12 @@ export async function handleComboChat({ | |||||||||||||||||
| if (provider && provider !== "unknown") { | ||||||||||||||||||
| recordProviderSuccess(provider, target.connectionId ?? undefined); | ||||||||||||||||||
| } | ||||||||||||||||||
| if (strategy === "weighted" && stickyWeightedLimit > 1) { | ||||||||||||||||||
| const stickySuccessKey = getWeightedStepKeyForTarget(target); | ||||||||||||||||||
| if (stickySuccessKey) { | ||||||||||||||||||
| recordStickyWeightedSuccess(combo.name, stickySuccessKey, stickyWeightedLimit); | ||||||||||||||||||
| } | ||||||||||||||||||
| } | ||||||||||||||||||
| // Webhook fan-out: best-effort, never blocks the response stream. | ||||||||||||||||||
| notifyWebhookEvent("request.completed", { | ||||||||||||||||||
| combo: combo.name, | ||||||||||||||||||
|
|
@@ -2216,6 +2294,45 @@ async function handleRoundRobinCombo({ | |||||||||||||||||
| : (settings as Record<string, unknown> | null)?.stickyRoundRobinLimit | ||||||||||||||||||
| ); | ||||||||||||||||||
| const stickyRoundRobinEnabled = stickyLimit > 1; | ||||||||||||||||||
| // Exhaustion-aware sticky: if the currently sticky target is no longer | ||||||||||||||||||
| // available (circuit breaker OPEN, provider cooldown, model lockout, or | ||||||||||||||||||
| // isModelAvailable returns false), clear the sticky record so the rotation | ||||||||||||||||||
| // starts at the counter position instead of probing a dead target. | ||||||||||||||||||
| if (stickyRoundRobinEnabled) { | ||||||||||||||||||
| const sticky = rrStickyTargets.get(combo.name); | ||||||||||||||||||
| if (sticky) { | ||||||||||||||||||
| const stickyTarget = filteredTargets.find( | ||||||||||||||||||
| (target) => target.executionKey === sticky.executionKey | ||||||||||||||||||
| ); | ||||||||||||||||||
| if (stickyTarget) { | ||||||||||||||||||
| const rawModel = parseModel(stickyTarget.modelStr).model || stickyTarget.modelStr; | ||||||||||||||||||
| const stickyAvailable = | ||||||||||||||||||
| (!stickyTarget.provider || getCircuitBreaker(stickyTarget.provider).getStatus().state !== "OPEN") && | ||||||||||||||||||
| !( | ||||||||||||||||||
| resilienceSettings.providerCooldown.enabled && | ||||||||||||||||||
| Boolean(stickyTarget.provider && stickyTarget.provider !== "unknown") && | ||||||||||||||||||
| isProviderInCooldown( | ||||||||||||||||||
| stickyTarget.provider, | ||||||||||||||||||
| stickyTarget.connectionId ?? undefined, | ||||||||||||||||||
| resilienceSettings | ||||||||||||||||||
| ) | ||||||||||||||||||
| ) && | ||||||||||||||||||
| !( | ||||||||||||||||||
| stickyTarget.provider && | ||||||||||||||||||
| rawModel && | ||||||||||||||||||
| isModelLocked(stickyTarget.provider, stickyTarget.connectionId || "", rawModel) | ||||||||||||||||||
| ) && | ||||||||||||||||||
| (isModelAvailable ? await isModelAvailable(stickyTarget.modelStr, stickyTarget) : true); | ||||||||||||||||||
| if (!stickyAvailable) { | ||||||||||||||||||
| log.info( | ||||||||||||||||||
| "COMBO-RR", | ||||||||||||||||||
| `Clearing stale sticky target ${stickyTarget.modelStr} — unavailable` | ||||||||||||||||||
| ); | ||||||||||||||||||
| rrStickyTargets.delete(combo.name); | ||||||||||||||||||
| } | ||||||||||||||||||
| } | ||||||||||||||||||
| } | ||||||||||||||||||
| } | ||||||||||||||||||
| if ( | ||||||||||||||||||
| !rrCounters.has(combo.name) && | ||||||||||||||||||
| !rrStickyTargets.has(combo.name) && | ||||||||||||||||||
|
|
||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -607,38 +607,66 @@ export function resolveComboTargets( | |||||||||||||||||||||||||
| : getDirectComboTargets(combo); | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| export function resolveWeightedTargets( | ||||||||||||||||||||||||||
| export function resolveWeightedStepGroups( | ||||||||||||||||||||||||||
| combo: ComboLike, | ||||||||||||||||||||||||||
| allCombos: ComboCollectionLike | ||||||||||||||||||||||||||
| ): Array<{ step: ComboRuntimeStep; targets: ResolvedComboTarget[] }> { | ||||||||||||||||||||||||||
| return getOrderedTopLevelRuntimeSteps(combo, allCombos) | ||||||||||||||||||||||||||
| .map((step) => ({ | ||||||||||||||||||||||||||
| step, | ||||||||||||||||||||||||||
| targets: !allCombos | ||||||||||||||||||||||||||
| ? step.kind === "model" | ||||||||||||||||||||||||||
| ? [step] | ||||||||||||||||||||||||||
| : [] | ||||||||||||||||||||||||||
| : expandRuntimeStep(step, allCombos, new Set([combo.name])), | ||||||||||||||||||||||||||
| })) | ||||||||||||||||||||||||||
| .filter((group) => group.targets.length > 0); | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| export function resolveWeightedTargets( | ||||||||||||||||||||||||||
| combo: ComboLike, | ||||||||||||||||||||||||||
| allCombos: ComboCollectionLike, | ||||||||||||||||||||||||||
| preferredExecutionKey: string | null = null, | ||||||||||||||||||||||||||
| eligibleExecutionKeys: ReadonlySet<string> | null = null, | ||||||||||||||||||||||||||
| stepGroups?: Array<{ step: ComboRuntimeStep; targets: ResolvedComboTarget[] }> | ||||||||||||||||||||||||||
| ): { | ||||||||||||||||||||||||||
| orderedTargets: ResolvedComboTarget[]; | ||||||||||||||||||||||||||
| selectedStep: ComboRuntimeStep | null; | ||||||||||||||||||||||||||
| orderedSteps: ComboRuntimeStep[]; | ||||||||||||||||||||||||||
| } { | ||||||||||||||||||||||||||
|
Comment on lines
+626
to
636
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add an optional export function resolveWeightedTargets(
combo: ComboLike,
allCombos: ComboCollectionLike,
preferredExecutionKey: string | null = null,
eligibleExecutionKeys: ReadonlySet<string> | null = null,
stepGroups?: Array<{ step: ComboRuntimeStep; targets: ResolvedComboTarget[] }>
): {
orderedTargets: ResolvedComboTarget[];
selectedStep: ComboRuntimeStep | null;
orderedSteps: ComboRuntimeStep[];
} { |
||||||||||||||||||||||||||
| const topLevelSteps = getOrderedTopLevelRuntimeSteps(combo, allCombos); | ||||||||||||||||||||||||||
| const topLevelSteps = getOrderedTopLevelRuntimeSteps(combo, allCombos).filter((step) => | ||||||||||||||||||||||||||
| eligibleExecutionKeys ? eligibleExecutionKeys.has(step.executionKey) : true | ||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||
| if (topLevelSteps.length === 0) { | ||||||||||||||||||||||||||
| return { orderedTargets: [], selectedStep: null }; | ||||||||||||||||||||||||||
| return { orderedTargets: [], selectedStep: null, orderedSteps: [] }; | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| const selectedStep = selectWeightedTarget(topLevelSteps); | ||||||||||||||||||||||||||
| const preferredStep = preferredExecutionKey | ||||||||||||||||||||||||||
| ? topLevelSteps.find((step) => step.executionKey === preferredExecutionKey) || null | ||||||||||||||||||||||||||
| : null; | ||||||||||||||||||||||||||
| const selectedStep = preferredStep || selectWeightedTarget(topLevelSteps); | ||||||||||||||||||||||||||
| if (!selectedStep) { | ||||||||||||||||||||||||||
| return { orderedTargets: [], selectedStep: null }; | ||||||||||||||||||||||||||
| return { orderedTargets: [], selectedStep: null, orderedSteps: [] }; | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| const orderedSteps = orderTargetsForWeightedFallback( | ||||||||||||||||||||||||||
| topLevelSteps, | ||||||||||||||||||||||||||
| selectedStep.executionKey, | ||||||||||||||||||||||||||
| hasCompositeTierRuntimeOrder(combo) | ||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||
| const expandedTargets = orderedSteps.flatMap((step) => { | ||||||||||||||||||||||||||
| if (!step) return []; | ||||||||||||||||||||||||||
| if (!allCombos) { | ||||||||||||||||||||||||||
| return step.kind === "model" ? [step] : []; | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| return expandRuntimeStep(step, allCombos, new Set([combo.name])); | ||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||
| const targetsByStep = new Map( | ||||||||||||||||||||||||||
| (stepGroups || resolveWeightedStepGroups(combo, allCombos)).map((group) => [ | ||||||||||||||||||||||||||
| group.step.executionKey, | ||||||||||||||||||||||||||
| group.targets, | ||||||||||||||||||||||||||
| ]) | ||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||
|
Comment on lines
+657
to
+662
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use the optional
Suggested change
|
||||||||||||||||||||||||||
| const expandedTargets = orderedSteps.flatMap( | ||||||||||||||||||||||||||
| (step) => targetsByStep.get(step.executionKey) || [] | ||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||||
| orderedTargets: dedupeTargetsByExecutionKey(expandedTargets), | ||||||||||||||||||||||||||
| selectedStep, | ||||||||||||||||||||||||||
| orderedSteps, | ||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
resolveWeightedStepGroupsis called twice per request for weighted combos: once on line 587 to check target availability, and again insideresolveWeightedTargetson line 604. Since this function performs recursive DAG expansion and array allocations, calling it twice is redundant. We can optimize this by declaringstepGroupsoutside theifblock and passing it toresolveWeightedTargets.