mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-01 12:52:11 +03:00
feat(combo): add nestedComboMode execute — black-box combo-ref execution for selection strategies (#4537)
Integrated into release/v3.8.33
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"_comment": "Catraca de complexidade (check-complexity.mjs, ESLint core rules complexity>=15 e max-lines-per-function>80 sobre src+open-sse+electron+bin via eslint.complexity.config.mjs). Conta total de violacoes; so pode cair. --update ratcheta.",
|
||||
"count": 1913,
|
||||
"count": 1915,
|
||||
"_rebaseline_2026_06_21_v3833_4537_nested_combo": "Reconciliacao 1913->1915 (+2) do PR #4537 (nestedComboMode execute — black-box combo-ref execution). O +2 vem do branch novo de dispatch nested em handleComboChat (combo.ts: normalizeNestedComboMode + executeModeUnits/hasExecutableComboRef + o ramo simpleExecuteStrategies que roda resolveComboRuntimeUnits com recursion caps depth/cycle/budget) — ramos condicionais no chokepoint de dispatch do combo. Medido com `node scripts/check/check-complexity.mjs` no estado merged. Crescimento de feature legitimo recem-TDD'd, nao regressao; reducao estrutural fica como debt (#3501).",
|
||||
"_rebaseline_2026_06_21_v3833_reviewprs_r4_owner": "Reconciliacao release-volatil 1911->1913 (+2) apos o lote de PRs do owner desta rodada (#4560 RTK cache_control, #4552 Cursor auto-import macOS, #4551 Codex /responses probe, #4554 Cursor Composer </think> decode + o stack #3501 #4538/#4544/#4548). O fast-path do release nao roda check:complexity (so release->main). As 3 extracoes chatCore #3501 (#4538 recupera 4 leaves, #4544 failureUsage, #4548 claudeSystemRole+upstreamExecuteHeaders) sao PURAS/complexity-neutras (movem codigo p/ leaves sob o teto, chatCore ENCOLHE); o +2 vem dos condicionais NOVOS das features .32-portadas (#4554 decode de bloco </think> visivel do Composer + #4551 probe do endpoint real Codex /responses). Medido com `node scripts/check/check-complexity.mjs` no tip 4b34a75fe. Crescimento de feature legitimo recem-TDD'd, nao regressao; reducao estrutural fica como debt (#3501).",
|
||||
"_rebaseline_2026_06_21_v3833_reviewprs_r4": "Reconciliacao release-volatil 1906->1911 (+5) apos o lote /review-prs r4 (5 PRs de contribuidores mergeados em release/v3.8.33: #4557 health-polls, #4556 mobile-table, #4545 provider-wildcard, #4558 isHidden, #4489 sticky-weighted) + 1 merge concorrente de sessao paralela (#4565 quota perf). O fast-path do release nao roda check:complexity (so release->main), entao o ramo acumula sem rebaselinar. Condicionais NOVOS legitimos: #4545 (expandProviderWildcardsInCombo/Collection + wildcardMatch glob/registry branching em providerWildcard.ts/wildcardRouter.ts), #4489 (eligibility pass sticky-weighted: resolveWeightedStepGroups + isTargetSelectableForWeighted + renormalizacao em combo.ts), #4558 (filtro isHidden em buildAutoCandidates/virtualFactory) e #4565 (guardas de skip de quota_snapshots idle). Medido com `node scripts/check/check-complexity.mjs` no tip 39b2bbfea. Mesma familia dos rebaselines anteriores — crescimento de feature legitimo recem-TDD'd, nao regressao; reducao estrutural fica como debt (#3501).",
|
||||
"_rebaseline_2026_06_21_v3833_cycle_open_stranded": "1905 -> 1906 (+1). Abertura do ciclo v3.8.33: o +1 vem dos 4 commits stranded na release/v3.8.32 (pós-merge da v3.8.32) trazidos via cherry-pick para release/v3.8.33 — isolado em #4483 (autoStrategy.ts evaluateQuotaCutoff/quotaPreflight.ts, guardas de quota-cutoff inerentemente ramificadas, opt-in default-OFF). main mede 1905; o .33 branch (main + cherry-picks) mede 1906. Crescimento de feature legitimo recém-mergeado, não regressão; redução fica como debt (#3501).",
|
||||
|
||||
@@ -130,7 +130,7 @@
|
||||
"open-sse/services/batchProcessor.ts": 828,
|
||||
"open-sse/services/browserBackedChat.ts": 850,
|
||||
"open-sse/services/claudeCodeCompatible.ts": 1202,
|
||||
"open-sse/services/combo.ts": 2818,
|
||||
"open-sse/services/combo.ts": 2955,
|
||||
"open-sse/services/rateLimitManager.ts": 1035,
|
||||
"open-sse/services/tokenRefresh.ts": 1997,
|
||||
"open-sse/services/usage.ts": 3450,
|
||||
@@ -145,7 +145,7 @@
|
||||
"src/app/(dashboard)/dashboard/cache/page.tsx": 841,
|
||||
"src/app/(dashboard)/dashboard/cli-code/components/CodexToolCard.tsx": 900,
|
||||
"src/app/(dashboard)/dashboard/cloud-agents/page.tsx": 922,
|
||||
"src/app/(dashboard)/dashboard/combos/page.tsx": 4406,
|
||||
"src/app/(dashboard)/dashboard/combos/page.tsx": 4434,
|
||||
"src/app/(dashboard)/dashboard/costs/CostOverviewTab.tsx": 1495,
|
||||
"src/app/(dashboard)/dashboard/costs/quota-share/components/PoolWizard.tsx": 1007,
|
||||
"src/app/(dashboard)/dashboard/endpoint/EndpointPageClient.tsx": 2570,
|
||||
@@ -205,7 +205,7 @@
|
||||
"src/shared/constants/sidebarVisibility.ts": 1100,
|
||||
"src/shared/services/cliRuntime.ts": 1090,
|
||||
"src/shared/validation/schemas.ts": 2523,
|
||||
"src/sse/handlers/chat.ts": 1513,
|
||||
"src/sse/handlers/chat.ts": 1515,
|
||||
"src/sse/services/auth.ts": 2279
|
||||
},
|
||||
"testCap": 800,
|
||||
@@ -221,6 +221,7 @@
|
||||
"tests/unit/chatcore-translation-paths.test.ts": 2810,
|
||||
"tests/unit/chatgpt-web.test.ts": 2809,
|
||||
"tests/unit/combo-routing-engine.test.ts": 3213,
|
||||
"tests/unit/combo-strategy-fallbacks.test.ts": 880,
|
||||
"tests/unit/db-core-init.test.ts": 864,
|
||||
"tests/unit/db-migration-runner.test.ts": 1491,
|
||||
"tests/unit/db-settings-crud.test.ts": 941,
|
||||
|
||||
@@ -97,7 +97,9 @@ import type {
|
||||
SingleModelTarget,
|
||||
HandleComboChatOptions,
|
||||
HandleRoundRobinOptions,
|
||||
NestedComboMode,
|
||||
ResolvedComboTarget,
|
||||
ResolvedComboUnit,
|
||||
AutoProviderCandidate,
|
||||
ComboRuntimeStep,
|
||||
HistoricalLatencyStatsEntry,
|
||||
@@ -132,6 +134,7 @@ import {
|
||||
getExhaustedTargetSkipReason,
|
||||
} from "./combo/comboPredicates.ts";
|
||||
import { applyComboTargetExhaustion } from "./combo/targetExhaustion.ts";
|
||||
import { executeRuntimeUnitCombo } from "./combo/runtimeUnits.ts";
|
||||
import { dedupeTargetsByExecutionKey, isRecord } from "./combo/comboData.ts";
|
||||
import {
|
||||
expandProviderWildcardsInCombo,
|
||||
@@ -146,6 +149,7 @@ import {
|
||||
import {
|
||||
filterTargetsByRequestCompatibility,
|
||||
getModelContextLimitForModelString,
|
||||
resolveComboRuntimeUnits,
|
||||
resolveComboTargets,
|
||||
resolveWeightedTargets,
|
||||
resolveWeightedStepGroups,
|
||||
@@ -191,7 +195,7 @@ export { resolveShadowTargets, scheduleShadowRouting };
|
||||
// preScreenTargets was public from combo.ts before the reset-aware quota
|
||||
// extraction (combo split D7b). Keep the external surface stable.
|
||||
export { preScreenTargets };
|
||||
export { resolveComboTargets, filterTargetsByRequestCompatibility };
|
||||
export { resolveComboRuntimeUnits, resolveComboTargets, filterTargetsByRequestCompatibility };
|
||||
export {
|
||||
getComboFromData,
|
||||
getComboModelsFromData,
|
||||
@@ -225,6 +229,10 @@ const MIN_HISTORY_SAMPLES = 10;
|
||||
// Matches the example in GitHub issue #1812 (e.g. o3-like model: $3 input/$15 output).
|
||||
const OUTPUT_TOKEN_RATIO = 0.4;
|
||||
|
||||
function normalizeNestedComboMode(value: unknown): NestedComboMode {
|
||||
return value === "execute" ? "execute" : "flatten";
|
||||
}
|
||||
|
||||
function calculateTargetContextAffinity(
|
||||
target: ResolvedComboTarget,
|
||||
sessionId: string | null | undefined
|
||||
@@ -571,6 +579,7 @@ export async function handleComboChat({
|
||||
relayOptions,
|
||||
signal,
|
||||
apiKeyAllowedConnections = null,
|
||||
nesting = null,
|
||||
}: HandleComboChatOptions): Promise<Response> {
|
||||
// Combo setup phase (god-file decomposition fase 1): strategy / relay / resilience /
|
||||
// universal-handoff / context-cache pinning / agent middleware / config cascade / timeout.
|
||||
@@ -683,6 +692,132 @@ export async function handleComboChat({
|
||||
return handleSingleModelWithTimeout(body, pinnedModel);
|
||||
}
|
||||
|
||||
const nestingContext = nesting || {
|
||||
depth: 0,
|
||||
maxDepth: clampComboDepth(config.maxComboDepth),
|
||||
visitedComboNames: [combo.name],
|
||||
rootComboName: combo.name,
|
||||
attemptBudget: { count: 0, limit: MAX_GLOBAL_ATTEMPTS },
|
||||
};
|
||||
const nestedComboMode = normalizeNestedComboMode(config.nestedComboMode);
|
||||
|
||||
const executeModeUnits =
|
||||
nestedComboMode === "execute" && allCombos
|
||||
? resolveComboRuntimeUnits(combo, allCombos, "execute", nestingContext.maxDepth)
|
||||
: [];
|
||||
const hasExecutableComboRef = executeModeUnits.some((unit) => unit.kind === "combo-ref");
|
||||
const simpleExecuteStrategies = new Set([
|
||||
"priority",
|
||||
"round-robin",
|
||||
"random",
|
||||
"strict-random",
|
||||
"weighted",
|
||||
"fill-first",
|
||||
]);
|
||||
|
||||
if (hasExecutableComboRef && simpleExecuteStrategies.has(strategy)) {
|
||||
let runtimeUnits = executeModeUnits;
|
||||
let unitExecutionStrategy = strategy;
|
||||
if (strategy === "weighted") {
|
||||
const stickyLimit = clampStickyWeightedTargetLimit(
|
||||
(config as Record<string, unknown>).stickyWeightedLimit
|
||||
);
|
||||
const stickyKey = getStickyWeightedExecutionKey(combo.name, stickyLimit);
|
||||
const stickyUnit = stickyKey
|
||||
? runtimeUnits.find((unit) => unit.executionKey === stickyKey)
|
||||
: null;
|
||||
if (stickyUnit) {
|
||||
runtimeUnits = [
|
||||
stickyUnit,
|
||||
...runtimeUnits.filter((unit) => unit.executionKey !== stickyUnit.executionKey),
|
||||
];
|
||||
unitExecutionStrategy = "priority";
|
||||
}
|
||||
}
|
||||
if (strategy === "random") runtimeUnits = fisherYatesShuffle([...runtimeUnits]);
|
||||
if (strategy === "strict-random") {
|
||||
const key = await getNextFromDeck(
|
||||
`combo:${combo.name}`,
|
||||
runtimeUnits.map((unit) => unit.executionKey)
|
||||
);
|
||||
const selected = runtimeUnits.find((unit) => unit.executionKey === key) || runtimeUnits[0];
|
||||
runtimeUnits = [
|
||||
selected,
|
||||
...runtimeUnits.filter((unit) => unit.executionKey !== selected.executionKey),
|
||||
];
|
||||
}
|
||||
let runtimeStickyLimit: number | null = null;
|
||||
let runtimeStickyTargets: ResolvedComboUnit[] = runtimeUnits;
|
||||
if (strategy === "round-robin") {
|
||||
const perComboStickyLimit = (config as Record<string, unknown>).stickyRoundRobinLimit;
|
||||
runtimeStickyLimit = clampStickyRoundRobinTargetLimit(
|
||||
perComboStickyLimit !== undefined && perComboStickyLimit !== null
|
||||
? perComboStickyLimit
|
||||
: (settings as Record<string, unknown> | null)?.stickyRoundRobinLimit
|
||||
);
|
||||
const { startIndex, counter } = getStickyRoundRobinStartIndex(
|
||||
combo.name,
|
||||
runtimeUnits,
|
||||
runtimeStickyLimit
|
||||
);
|
||||
if (runtimeStickyLimit <= 1) rrCounters.set(combo.name, counter + 1);
|
||||
runtimeUnits = runtimeUnits.map(
|
||||
(_, offset) => runtimeUnits[(startIndex + offset) % runtimeUnits.length]
|
||||
);
|
||||
runtimeStickyTargets = executeModeUnits;
|
||||
}
|
||||
const execution = await executeRuntimeUnitCombo({
|
||||
body,
|
||||
combo,
|
||||
strategy: unitExecutionStrategy,
|
||||
effectiveComboStrategy: strategy,
|
||||
units: runtimeUnits,
|
||||
handleSingleModel: handleSingleModelWithTimeout,
|
||||
isModelAvailable,
|
||||
log,
|
||||
config,
|
||||
settings,
|
||||
allCombos,
|
||||
signal,
|
||||
nesting: nestingContext,
|
||||
baseOptions: {
|
||||
body,
|
||||
combo,
|
||||
handleSingleModel,
|
||||
isModelAvailable,
|
||||
log,
|
||||
settings,
|
||||
allCombos,
|
||||
relayOptions,
|
||||
signal,
|
||||
apiKeyAllowedConnections,
|
||||
},
|
||||
runCombo: handleComboChat,
|
||||
});
|
||||
if (strategy === "weighted" && execution.response.ok && execution.unit) {
|
||||
const stickyLimit = clampStickyWeightedTargetLimit(
|
||||
(config as Record<string, unknown>).stickyWeightedLimit
|
||||
);
|
||||
if (stickyLimit > 1)
|
||||
recordStickyWeightedSuccess(combo.name, execution.unit.executionKey, stickyLimit);
|
||||
}
|
||||
if (
|
||||
strategy === "round-robin" &&
|
||||
execution.response.ok &&
|
||||
execution.unit &&
|
||||
runtimeStickyLimit &&
|
||||
runtimeStickyLimit > 1
|
||||
) {
|
||||
recordStickyRoundRobinSuccess(
|
||||
combo.name,
|
||||
execution.unit,
|
||||
runtimeStickyLimit,
|
||||
runtimeStickyTargets
|
||||
);
|
||||
}
|
||||
return execution.response;
|
||||
}
|
||||
|
||||
// Route to round-robin handler if strategy matches
|
||||
if (strategy === "round-robin") {
|
||||
return handleRoundRobinCombo({
|
||||
@@ -1549,6 +1684,7 @@ export async function handleComboChat({
|
||||
}
|
||||
const result = await handleSingleModelWithTimeout(attemptBody, modelStr, {
|
||||
...targetForAttempt,
|
||||
effectiveComboStrategy: strategy,
|
||||
failoverBeforeRetry: config.failoverBeforeRetry,
|
||||
});
|
||||
|
||||
@@ -2499,6 +2635,7 @@ async function handleRoundRobinCombo({
|
||||
|
||||
const result = await handleSingleModel(attemptBody, modelStr, {
|
||||
...targetForAttempt,
|
||||
effectiveComboStrategy: "round-robin",
|
||||
failoverBeforeRetry: config.failoverBeforeRetry,
|
||||
});
|
||||
|
||||
|
||||
@@ -29,7 +29,9 @@ import type {
|
||||
ComboLike,
|
||||
ComboLogger,
|
||||
ComboRuntimeStep,
|
||||
NestedComboMode,
|
||||
ResolvedComboTarget,
|
||||
ResolvedComboUnit,
|
||||
} from "./types.ts";
|
||||
|
||||
function toTrimmedString(value: unknown): string | null {
|
||||
@@ -607,6 +609,17 @@ export function resolveComboTargets(
|
||||
: getDirectComboTargets(combo);
|
||||
}
|
||||
|
||||
export function resolveComboRuntimeUnits(
|
||||
combo: ComboLike,
|
||||
allCombos: ComboCollectionLike,
|
||||
mode: NestedComboMode,
|
||||
maxDepth: number = MAX_COMBO_DEPTH
|
||||
): ResolvedComboUnit[] {
|
||||
if (mode === "flatten" || !allCombos) return resolveComboTargets(combo, allCombos, maxDepth);
|
||||
validateComboDAG(combo.name, allCombos, new Set<string>(), 0, maxDepth);
|
||||
return getOrderedTopLevelRuntimeSteps(combo, allCombos);
|
||||
}
|
||||
|
||||
export function resolveWeightedStepGroups(
|
||||
combo: ComboLike,
|
||||
allCombos: ComboCollectionLike
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
* Pure leaf: this module never imports from the combo barrel.
|
||||
*/
|
||||
|
||||
import type { ResolvedComboTarget } from "./types.ts";
|
||||
import type { ResolvedComboUnit } from "./types.ts";
|
||||
|
||||
// In-memory atomic counter per combo for round-robin distribution
|
||||
// Resets on server restart (by design — no stale state)
|
||||
@@ -37,7 +37,7 @@ export const clampStickyWeightedTargetLimit = clampStickyRoundRobinTargetLimit;
|
||||
|
||||
export function getStickyRoundRobinStartIndex(
|
||||
comboName: string,
|
||||
targets: ResolvedComboTarget[],
|
||||
targets: ResolvedComboUnit[],
|
||||
stickyLimit: number
|
||||
): { startIndex: number; counter: number } {
|
||||
const sticky = rrStickyTargets.get(comboName);
|
||||
@@ -54,9 +54,9 @@ export function getStickyRoundRobinStartIndex(
|
||||
|
||||
export function recordStickyRoundRobinSuccess(
|
||||
comboName: string,
|
||||
target: ResolvedComboTarget,
|
||||
target: ResolvedComboUnit,
|
||||
stickyLimit: number,
|
||||
targets: ResolvedComboTarget[]
|
||||
targets: ResolvedComboUnit[]
|
||||
): void {
|
||||
const sticky = rrStickyTargets.get(comboName);
|
||||
const successCount = sticky?.executionKey === target.executionKey ? sticky.successCount + 1 : 1;
|
||||
|
||||
256
open-sse/services/combo/runtimeUnits.ts
Normal file
256
open-sse/services/combo/runtimeUnits.ts
Normal file
@@ -0,0 +1,256 @@
|
||||
// Nested combo runtime unit execution — see combo.ts for integration.
|
||||
import { errorResponse } from "../../utils/error.ts";
|
||||
import { recordComboRequest } from "../comboMetrics.ts";
|
||||
import { resolveDelayMs } from "./comboPredicates.ts";
|
||||
import { validateResponseQuality } from "./validateQuality.ts";
|
||||
import type {
|
||||
ComboCollectionLike,
|
||||
ComboLike,
|
||||
ComboLogger,
|
||||
ComboNestingContext,
|
||||
HandleComboChatOptions,
|
||||
HandleSingleModel,
|
||||
IsModelAvailable,
|
||||
ResolvedComboRefTarget,
|
||||
ResolvedComboUnit,
|
||||
} from "./types.ts";
|
||||
|
||||
export type RuntimeUnitExecutionResult = {
|
||||
response: Response;
|
||||
unit: ResolvedComboUnit | null;
|
||||
};
|
||||
|
||||
type RuntimeUnitRunner = (options: HandleComboChatOptions) => Promise<Response>;
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return !!value && typeof value === "object" && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function getCombosList(allCombos: ComboCollectionLike): ComboLike[] {
|
||||
const combos = Array.isArray(allCombos) ? allCombos : allCombos?.combos || [];
|
||||
return combos.filter(
|
||||
(combo): combo is ComboLike => isRecord(combo) && typeof combo.name === "string"
|
||||
);
|
||||
}
|
||||
|
||||
function findComboByName(allCombos: ComboCollectionLike, name: string): ComboLike | null {
|
||||
return getCombosList(allCombos).find((combo) => combo.name === name) || null;
|
||||
}
|
||||
|
||||
function unitDisplayName(unit: ResolvedComboUnit): string {
|
||||
return unit.kind === "combo-ref" ? `combo:${unit.comboName}` : unit.modelStr;
|
||||
}
|
||||
|
||||
function shuffleUnits(units: ResolvedComboUnit[]): ResolvedComboUnit[] {
|
||||
const result = [...units];
|
||||
for (let index = result.length - 1; index > 0; index -= 1) {
|
||||
const swapIndex = Math.floor(Math.random() * (index + 1));
|
||||
[result[index], result[swapIndex]] = [result[swapIndex], result[index]];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function selectWeightedUnit(units: ResolvedComboUnit[]): ResolvedComboUnit | null {
|
||||
const total = units.reduce((sum, unit) => sum + Math.max(0, Number(unit.weight) || 0), 0);
|
||||
if (total <= 0) return units[0] || null;
|
||||
let draw = Math.random() * total;
|
||||
for (const unit of units) {
|
||||
draw -= Math.max(0, Number(unit.weight) || 0);
|
||||
if (draw <= 0) return unit;
|
||||
}
|
||||
return units[units.length - 1] || null;
|
||||
}
|
||||
|
||||
async function executeModelUnit(args: {
|
||||
body: Record<string, unknown>;
|
||||
unit: Extract<ResolvedComboUnit, { kind: "model" }>;
|
||||
handleSingleModel: HandleSingleModel;
|
||||
isModelAvailable?: IsModelAvailable;
|
||||
failoverBeforeRetry: unknown;
|
||||
effectiveComboStrategy: string;
|
||||
}): Promise<Response> {
|
||||
if (args.isModelAvailable) {
|
||||
const available = await args.isModelAvailable(args.unit.modelStr, args.unit);
|
||||
if (!available) return errorResponse(503, `Model ${args.unit.modelStr} is unavailable`);
|
||||
}
|
||||
return args.handleSingleModel(args.body, args.unit.modelStr, {
|
||||
...args.unit,
|
||||
effectiveComboStrategy: args.effectiveComboStrategy,
|
||||
failoverBeforeRetry: args.failoverBeforeRetry,
|
||||
});
|
||||
}
|
||||
|
||||
function buildChildNestingContext(args: {
|
||||
context: ComboNestingContext;
|
||||
childComboName: string;
|
||||
}): ComboNestingContext | Response {
|
||||
if (args.context.depth >= args.context.maxDepth) {
|
||||
return errorResponse(503, `Max combo nesting depth (${args.context.maxDepth}) exceeded`);
|
||||
}
|
||||
if (args.context.visitedComboNames.includes(args.childComboName)) {
|
||||
return errorResponse(503, `Circular combo reference detected: ${args.childComboName}`);
|
||||
}
|
||||
return {
|
||||
...args.context,
|
||||
depth: args.context.depth + 1,
|
||||
visitedComboNames: [...args.context.visitedComboNames, args.childComboName],
|
||||
};
|
||||
}
|
||||
|
||||
async function executeComboRefUnit(args: {
|
||||
body: Record<string, unknown>;
|
||||
unit: ResolvedComboRefTarget;
|
||||
allCombos: ComboCollectionLike;
|
||||
runCombo: RuntimeUnitRunner;
|
||||
baseOptions: HandleComboChatOptions;
|
||||
nesting: ComboNestingContext;
|
||||
}): Promise<Response> {
|
||||
const childCombo = findComboByName(args.allCombos, args.unit.comboName);
|
||||
if (!childCombo) return errorResponse(503, `Nested combo "${args.unit.comboName}" not found`);
|
||||
const childNesting = buildChildNestingContext({
|
||||
context: args.nesting,
|
||||
childComboName: childCombo.name,
|
||||
});
|
||||
if (childNesting instanceof Response) return childNesting;
|
||||
return args.runCombo({
|
||||
...args.baseOptions,
|
||||
body: args.body,
|
||||
combo: childCombo,
|
||||
nesting: childNesting,
|
||||
});
|
||||
}
|
||||
|
||||
async function executeRuntimeUnit(args: {
|
||||
body: Record<string, unknown>;
|
||||
unit: ResolvedComboUnit;
|
||||
allCombos: ComboCollectionLike;
|
||||
handleSingleModel: HandleSingleModel;
|
||||
isModelAvailable?: IsModelAvailable;
|
||||
runCombo: RuntimeUnitRunner;
|
||||
baseOptions: HandleComboChatOptions;
|
||||
nesting: ComboNestingContext;
|
||||
failoverBeforeRetry: unknown;
|
||||
effectiveComboStrategy: string;
|
||||
}): Promise<Response> {
|
||||
if (args.unit.kind === "model") {
|
||||
return executeModelUnit({
|
||||
body: args.body,
|
||||
unit: args.unit,
|
||||
handleSingleModel: args.handleSingleModel,
|
||||
isModelAvailable: args.isModelAvailable,
|
||||
failoverBeforeRetry: args.failoverBeforeRetry,
|
||||
effectiveComboStrategy: args.effectiveComboStrategy,
|
||||
});
|
||||
}
|
||||
return executeComboRefUnit({
|
||||
body: args.body,
|
||||
unit: args.unit,
|
||||
allCombos: args.allCombos,
|
||||
runCombo: args.runCombo,
|
||||
baseOptions: args.baseOptions,
|
||||
nesting: args.nesting,
|
||||
});
|
||||
}
|
||||
|
||||
function orderUnitsForStrategy(strategy: string, units: ResolvedComboUnit[]): ResolvedComboUnit[] {
|
||||
if (strategy === "random") return shuffleUnits(units);
|
||||
if (strategy === "weighted") {
|
||||
const selected = selectWeightedUnit(units);
|
||||
if (!selected) return units;
|
||||
return [selected, ...units.filter((unit) => unit.executionKey !== selected.executionKey)];
|
||||
}
|
||||
return units;
|
||||
}
|
||||
|
||||
export async function executeRuntimeUnitCombo(args: {
|
||||
body: Record<string, unknown>;
|
||||
combo: ComboLike;
|
||||
strategy: string;
|
||||
effectiveComboStrategy?: string;
|
||||
units: ResolvedComboUnit[];
|
||||
handleSingleModel: HandleSingleModel;
|
||||
isModelAvailable?: IsModelAvailable;
|
||||
log: ComboLogger;
|
||||
config: Record<string, unknown>;
|
||||
settings?: Record<string, unknown> | null;
|
||||
allCombos: ComboCollectionLike;
|
||||
signal?: AbortSignal | null;
|
||||
nesting: ComboNestingContext;
|
||||
baseOptions: HandleComboChatOptions;
|
||||
runCombo: RuntimeUnitRunner;
|
||||
}): Promise<RuntimeUnitExecutionResult> {
|
||||
const maxRetries = Number(args.config.maxRetries ?? 1);
|
||||
const retryDelayMs = resolveDelayMs(args.config.retryDelayMs, 2000);
|
||||
const orderedUnits = orderUnitsForStrategy(args.strategy, args.units);
|
||||
const clientRequestedStream = args.body?.stream === true;
|
||||
const startTime = Date.now();
|
||||
const effectiveStrategy = args.effectiveComboStrategy ?? args.strategy;
|
||||
let lastResponse: Response | null = null;
|
||||
let fallbackCount = 0;
|
||||
|
||||
for (const unit of orderedUnits) {
|
||||
for (let retry = 0; retry <= maxRetries; retry += 1) {
|
||||
if (args.signal?.aborted)
|
||||
return { response: errorResponse(499, "Client disconnected"), unit };
|
||||
args.nesting.attemptBudget.count += 1;
|
||||
if (args.nesting.attemptBudget.count > args.nesting.attemptBudget.limit) {
|
||||
return { response: errorResponse(503, "Maximum combo retry limit reached"), unit };
|
||||
}
|
||||
if (retry > 0) {
|
||||
await new Promise((resolve) => setTimeout(resolve, retryDelayMs));
|
||||
}
|
||||
args.log.info(
|
||||
"COMBO",
|
||||
`Trying ${unit.kind} ${unitDisplayName(unit)}${retry > 0 ? ` (retry ${retry})` : ""}`
|
||||
);
|
||||
const response = await executeRuntimeUnit({
|
||||
body: args.body,
|
||||
unit,
|
||||
allCombos: args.allCombos,
|
||||
handleSingleModel: args.handleSingleModel,
|
||||
isModelAvailable: args.isModelAvailable,
|
||||
runCombo: args.runCombo,
|
||||
baseOptions: args.baseOptions,
|
||||
nesting: args.nesting,
|
||||
failoverBeforeRetry: args.config.failoverBeforeRetry,
|
||||
effectiveComboStrategy: effectiveStrategy,
|
||||
});
|
||||
lastResponse = response;
|
||||
if (response.ok) {
|
||||
if (unit.kind === "combo-ref") {
|
||||
recordComboRequest(args.combo.name, null, {
|
||||
success: true,
|
||||
latencyMs: Date.now() - startTime,
|
||||
fallbackCount,
|
||||
strategy: effectiveStrategy,
|
||||
target: { executionKey: unit.executionKey, stepId: unit.stepId, label: unit.label },
|
||||
});
|
||||
return { response, unit };
|
||||
}
|
||||
const quality = await validateResponseQuality(response, clientRequestedStream, args.log);
|
||||
if (quality.valid) {
|
||||
recordComboRequest(args.combo.name, unit.modelStr, {
|
||||
success: true,
|
||||
latencyMs: Date.now() - startTime,
|
||||
fallbackCount,
|
||||
strategy: effectiveStrategy,
|
||||
target: { executionKey: unit.executionKey, stepId: unit.stepId, label: unit.label },
|
||||
});
|
||||
return { response: quality.clonedResponse ?? response, unit };
|
||||
}
|
||||
}
|
||||
if (![408, 429, 500, 502, 503, 504].includes(response.status)) break;
|
||||
}
|
||||
fallbackCount += 1;
|
||||
}
|
||||
recordComboRequest(args.combo.name, null, {
|
||||
success: false,
|
||||
latencyMs: Date.now() - startTime,
|
||||
fallbackCount,
|
||||
strategy: effectiveStrategy,
|
||||
});
|
||||
return {
|
||||
response: lastResponse || errorResponse(503, "All nested combo units unavailable"),
|
||||
unit: null,
|
||||
};
|
||||
}
|
||||
@@ -44,6 +44,7 @@ export type ComboLogger = {
|
||||
export type SingleModelTarget =
|
||||
| (ResolvedComboTarget & {
|
||||
allowRateLimitedConnection?: boolean;
|
||||
effectiveComboStrategy?: string | null;
|
||||
modelAbortSignal?: AbortSignal | null;
|
||||
})
|
||||
| { modelAbortSignal: AbortSignal };
|
||||
@@ -65,6 +66,16 @@ export type ComboRelayOptions = {
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
export type NestedComboMode = "flatten" | "execute";
|
||||
|
||||
export type ComboNestingContext = {
|
||||
depth: number;
|
||||
maxDepth: number;
|
||||
visitedComboNames: string[];
|
||||
rootComboName: string;
|
||||
attemptBudget: { count: number; limit: number };
|
||||
};
|
||||
|
||||
export type HandleComboChatOptions = {
|
||||
body: Record<string, unknown>;
|
||||
combo: ComboLike;
|
||||
@@ -76,6 +87,7 @@ export type HandleComboChatOptions = {
|
||||
relayOptions?: ComboRelayOptions | null;
|
||||
signal?: AbortSignal | null;
|
||||
apiKeyAllowedConnections?: string[] | null;
|
||||
nesting?: ComboNestingContext | null;
|
||||
};
|
||||
|
||||
export type HandleRoundRobinOptions = Omit<
|
||||
@@ -130,13 +142,15 @@ export type ShadowRoutingConfig = {
|
||||
timeoutMs: number;
|
||||
};
|
||||
|
||||
export type ComboRuntimeStep =
|
||||
| ResolvedComboTarget
|
||||
| {
|
||||
kind: "combo-ref";
|
||||
stepId: string;
|
||||
executionKey: string;
|
||||
comboName: string;
|
||||
weight: number;
|
||||
label: string | null;
|
||||
};
|
||||
export type ResolvedComboRefTarget = {
|
||||
kind: "combo-ref";
|
||||
stepId: string;
|
||||
executionKey: string;
|
||||
comboName: string;
|
||||
weight: number;
|
||||
label: string | null;
|
||||
};
|
||||
|
||||
export type ResolvedComboUnit = ResolvedComboTarget | ResolvedComboRefTarget;
|
||||
|
||||
export type ComboRuntimeStep = ResolvedComboUnit;
|
||||
|
||||
@@ -50,6 +50,7 @@ const DEFAULT_COMBO_CONFIG = {
|
||||
handoffProviders: ["codex"],
|
||||
maxMessagesForSummary: 30,
|
||||
maxComboDepth: 3,
|
||||
nestedComboMode: "flatten",
|
||||
trackMetrics: true,
|
||||
reasoningTokenBufferEnabled: true,
|
||||
manifestRouting: false,
|
||||
|
||||
@@ -169,6 +169,8 @@ const ADVANCED_FIELD_HELP_FALLBACK = {
|
||||
"Number of times to retry the full target set when every target fails. 0 = no set-level retry.",
|
||||
setRetryDelayMs:
|
||||
"Delay between set-level retry attempts, giving transient issues time to resolve.",
|
||||
nestedComboMode:
|
||||
"How references to other combos are handled. Flatten expands a combo ref into this combo's target list (legacy). Execute treats a combo ref as a black-box target: the parent strategy selects the child combo, then the child runs its own strategy and retries.",
|
||||
};
|
||||
|
||||
const LEGACY_COMBO_RESILIENCE_KEYS = new Set([
|
||||
@@ -3880,6 +3882,32 @@ function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders, combo
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="grid grid-cols-1 gap-2 pt-2 border-t border-black/5 dark:border-white/5">
|
||||
<div>
|
||||
<FieldLabelWithHelp
|
||||
label={getI18nOrFallback(t, "nestedComboMode", "Nested Combo Behavior")}
|
||||
help={getI18nOrFallback(
|
||||
t,
|
||||
"advancedHelp.nestedComboMode",
|
||||
ADVANCED_FIELD_HELP_FALLBACK.nestedComboMode
|
||||
)}
|
||||
showHelp={!isExpertMode}
|
||||
/>
|
||||
<select
|
||||
value={config.nestedComboMode ?? "flatten"}
|
||||
onChange={(e) =>
|
||||
setConfig({
|
||||
...config,
|
||||
nestedComboMode: e.target.value === "execute" ? "execute" : "flatten",
|
||||
})
|
||||
}
|
||||
className="w-full text-xs py-1.5 px-2 rounded border border-black/10 dark:border-white/10 bg-surface-1 focus:border-primary focus:outline-none"
|
||||
>
|
||||
<option value="flatten">Flatten nested combos</option>
|
||||
<option value="execute">Execute nested combos as targets</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
{strategy === "context-relay" && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-2 pt-2 border-t border-black/5 dark:border-white/5">
|
||||
<div>
|
||||
|
||||
@@ -146,6 +146,7 @@ export const comboRuntimeConfigSchema = z
|
||||
handoffProviders: z.array(z.string().trim().min(1).max(100)).max(10).optional(),
|
||||
maxMessagesForSummary: z.coerce.number().int().min(5).max(100).optional(),
|
||||
maxComboDepth: z.coerce.number().int().min(1).max(10).optional(),
|
||||
nestedComboMode: z.enum(["flatten", "execute"]).optional(),
|
||||
trackMetrics: z.boolean().optional(),
|
||||
reasoningTokenBufferEnabled: z.boolean().optional(),
|
||||
compressionMode: compressionModeSchema.optional(),
|
||||
|
||||
@@ -636,6 +636,7 @@ export async function handleChat(
|
||||
allowedConnectionIds?: string[] | null;
|
||||
failoverBeforeRetry?: boolean;
|
||||
providerId?: string | null;
|
||||
effectiveComboStrategy?: string | null;
|
||||
}
|
||||
) =>
|
||||
handleSingleModelChat(
|
||||
@@ -662,7 +663,7 @@ export async function handleChat(
|
||||
cachedSettings: settings,
|
||||
providerId: target?.providerId ?? null,
|
||||
},
|
||||
combo.strategy,
|
||||
target?.effectiveComboStrategy ?? combo.strategy,
|
||||
true
|
||||
),
|
||||
isModelAvailable: checkModelAvailable,
|
||||
@@ -825,6 +826,7 @@ async function handleSingleModelChat(
|
||||
failoverBeforeRetry?: boolean;
|
||||
allowRateLimitedConnection?: boolean;
|
||||
providerId?: string | null;
|
||||
effectiveComboStrategy?: string | null;
|
||||
}
|
||||
) =>
|
||||
handleSingleModelChat(
|
||||
@@ -846,7 +848,7 @@ async function handleSingleModelChat(
|
||||
allowRateLimitedConnection: target?.allowRateLimitedConnection === true,
|
||||
providerId: target?.providerId ?? null,
|
||||
},
|
||||
redirectCombo.strategy ?? "priority",
|
||||
target?.effectiveComboStrategy ?? redirectCombo.strategy ?? "priority",
|
||||
false
|
||||
),
|
||||
isModelAvailable: async () => true,
|
||||
|
||||
@@ -26,6 +26,7 @@ test("getDefaultComboConfig returns a fresh copy of the defaults", () => {
|
||||
assert.equal(first.handoffThreshold, 0.85);
|
||||
assert.equal(first.maxMessagesForSummary, 30);
|
||||
assert.deepEqual(first.handoffProviders, ["codex"]);
|
||||
assert.equal(first.nestedComboMode, "flatten");
|
||||
assert.equal(first.failoverBeforeRetry, true);
|
||||
assert.equal(first.maxSetRetries, 0);
|
||||
assert.equal(first.setRetryDelayMs, 2000);
|
||||
@@ -531,6 +532,30 @@ test("createComboSchema accepts failoverBeforeRetry, maxSetRetries and setRetryD
|
||||
assert.equal(parsed.config.setRetryDelayMs, 1500);
|
||||
});
|
||||
|
||||
test("createComboSchema accepts nestedComboMode and rejects invalid values", () => {
|
||||
const parsed = createComboSchema.parse({
|
||||
name: "nested-execute",
|
||||
models: [{ kind: "combo-ref", comboName: "child" }],
|
||||
strategy: "priority",
|
||||
config: { nestedComboMode: "execute" },
|
||||
});
|
||||
assert.equal(parsed.config.nestedComboMode, "execute");
|
||||
|
||||
const flatten = createComboSchema.parse({
|
||||
name: "nested-flatten",
|
||||
models: ["openai/gpt-4o-mini"],
|
||||
config: { nestedComboMode: "flatten" },
|
||||
});
|
||||
assert.equal(flatten.config.nestedComboMode, "flatten");
|
||||
|
||||
const invalid = createComboSchema.safeParse({
|
||||
name: "nested-invalid",
|
||||
models: ["openai/gpt-4o-mini"],
|
||||
config: { nestedComboMode: "redirect" },
|
||||
});
|
||||
assert.equal(invalid.success, false);
|
||||
});
|
||||
|
||||
test("createComboSchema accepts per-combo stickyRoundRobinLimit and rejects out-of-range", () => {
|
||||
const parsed = createComboSchema.parse({
|
||||
name: "sticky-override",
|
||||
@@ -618,6 +643,17 @@ test("createComboSchema rejects setRetryDelayMs out of range", () => {
|
||||
assert.equal(negative.success, false);
|
||||
});
|
||||
|
||||
test("resolveComboConfig cascades nestedComboMode", () => {
|
||||
const result = resolveComboConfig(
|
||||
{ config: { nestedComboMode: "execute" } },
|
||||
{ comboDefaults: { nestedComboMode: "flatten" } }
|
||||
);
|
||||
assert.equal(result.nestedComboMode, "execute");
|
||||
|
||||
const defaulted = resolveComboConfig({ config: {} }, { comboDefaults: {} });
|
||||
assert.equal(defaulted.nestedComboMode, "flatten");
|
||||
});
|
||||
|
||||
test("resolveComboConfig cascades failoverBeforeRetry, maxSetRetries and setRetryDelayMs", () => {
|
||||
const result = resolveComboConfig(
|
||||
{
|
||||
|
||||
@@ -600,6 +600,164 @@ test("strict-random survives a stale deck entry after a target is removed", asyn
|
||||
assert.deepEqual(calls, ["claude/sonnet"]);
|
||||
});
|
||||
|
||||
test("nested execute mode treats combo refs as black-box priority targets", async () => {
|
||||
const calls: string[] = [];
|
||||
const outer = {
|
||||
name: "outer-execute-priority",
|
||||
strategy: "priority",
|
||||
models: [{ kind: "combo-ref", comboName: "child-priority" }, "gemini/fallback"],
|
||||
config: { nestedComboMode: "execute", maxRetries: 0, retryDelayMs: 0 },
|
||||
};
|
||||
const child = {
|
||||
name: "child-priority",
|
||||
strategy: "priority",
|
||||
models: ["openai/a", "claude/b"],
|
||||
config: { maxRetries: 0, retryDelayMs: 0 },
|
||||
};
|
||||
const result = await handleComboChat({
|
||||
body: {},
|
||||
combo: outer,
|
||||
handleSingleModel: async (_body: any, modelStr: string) => {
|
||||
calls.push(modelStr);
|
||||
return okResponse();
|
||||
},
|
||||
isModelAvailable: async () => true,
|
||||
log: createLog(),
|
||||
settings: null,
|
||||
allCombos: [outer, child],
|
||||
});
|
||||
|
||||
assert.equal(result.ok, true);
|
||||
assert.deepEqual(calls, ["openai/a"]);
|
||||
});
|
||||
|
||||
test("nested execute mode falls back to the next parent target after a child combo fails", async () => {
|
||||
const calls: string[] = [];
|
||||
const outer = {
|
||||
name: "outer-execute-fallback",
|
||||
strategy: "priority",
|
||||
models: [{ kind: "combo-ref", comboName: "child-down" }, "gemini/fallback"],
|
||||
config: { nestedComboMode: "execute", maxRetries: 0, retryDelayMs: 0 },
|
||||
};
|
||||
const child = {
|
||||
name: "child-down",
|
||||
strategy: "priority",
|
||||
models: ["openai/a", "claude/b"],
|
||||
config: { maxRetries: 0, retryDelayMs: 0 },
|
||||
};
|
||||
const result = await handleComboChat({
|
||||
body: {},
|
||||
combo: outer,
|
||||
handleSingleModel: async (_body: any, modelStr: string) => {
|
||||
calls.push(modelStr);
|
||||
return modelStr === "gemini/fallback" ? okResponse() : errorResponse(503);
|
||||
},
|
||||
isModelAvailable: async () => true,
|
||||
log: createLog(),
|
||||
settings: null,
|
||||
allCombos: [outer, child],
|
||||
});
|
||||
|
||||
assert.equal(result.ok, true);
|
||||
assert.deepEqual(calls, ["openai/a", "claude/b", "gemini/fallback"]);
|
||||
});
|
||||
|
||||
test("nested execute mode supports 3-level nesting", async () => {
|
||||
const calls: string[] = [];
|
||||
const a = {
|
||||
name: "level-a",
|
||||
strategy: "priority",
|
||||
models: [{ kind: "combo-ref", comboName: "level-b" }],
|
||||
config: { nestedComboMode: "execute", maxRetries: 0 },
|
||||
};
|
||||
const b = {
|
||||
name: "level-b",
|
||||
strategy: "priority",
|
||||
models: [{ kind: "combo-ref", comboName: "level-c" }],
|
||||
config: { nestedComboMode: "execute", maxRetries: 0 },
|
||||
};
|
||||
const c = {
|
||||
name: "level-c",
|
||||
strategy: "priority",
|
||||
models: ["openai/leaf"],
|
||||
config: { maxRetries: 0 },
|
||||
};
|
||||
const result = await handleComboChat({
|
||||
body: {},
|
||||
combo: a,
|
||||
handleSingleModel: async (_body: any, modelStr: string) => {
|
||||
calls.push(modelStr);
|
||||
return okResponse();
|
||||
},
|
||||
isModelAvailable: async () => true,
|
||||
log: createLog(),
|
||||
settings: null,
|
||||
allCombos: [a, b, c],
|
||||
});
|
||||
|
||||
assert.equal(result.ok, true);
|
||||
assert.deepEqual(calls, ["openai/leaf"]);
|
||||
});
|
||||
|
||||
test("nested execute mode fails cleanly on a circular combo reference", async () => {
|
||||
const a = {
|
||||
name: "cycle-a",
|
||||
strategy: "priority",
|
||||
models: [{ kind: "combo-ref", comboName: "cycle-b" }],
|
||||
config: { nestedComboMode: "execute", maxRetries: 0 },
|
||||
};
|
||||
const b = {
|
||||
name: "cycle-b",
|
||||
strategy: "priority",
|
||||
models: [{ kind: "combo-ref", comboName: "cycle-a" }],
|
||||
config: { nestedComboMode: "execute", maxRetries: 0 },
|
||||
};
|
||||
await assert.rejects(
|
||||
async () =>
|
||||
handleComboChat({
|
||||
body: {},
|
||||
combo: a,
|
||||
handleSingleModel: async () => okResponse(),
|
||||
isModelAvailable: async () => true,
|
||||
log: createLog(),
|
||||
settings: null,
|
||||
allCombos: [a, b],
|
||||
}),
|
||||
/[Cc]ircular/
|
||||
);
|
||||
});
|
||||
|
||||
test("flatten mode (default) expands nested combo refs into leaf targets", async () => {
|
||||
const calls: string[] = [];
|
||||
const outer = {
|
||||
name: "outer-flatten-default",
|
||||
strategy: "priority",
|
||||
models: [{ kind: "combo-ref", comboName: "child-flat" }],
|
||||
config: { maxRetries: 0 },
|
||||
};
|
||||
const child = {
|
||||
name: "child-flat",
|
||||
strategy: "priority",
|
||||
models: ["openai/a", "claude/b"],
|
||||
config: { maxRetries: 0 },
|
||||
};
|
||||
const result = await handleComboChat({
|
||||
body: {},
|
||||
combo: outer,
|
||||
handleSingleModel: async (_body: any, modelStr: string) => {
|
||||
calls.push(modelStr);
|
||||
return modelStr === "openai/a" ? errorResponse(503) : okResponse();
|
||||
},
|
||||
isModelAvailable: async () => true,
|
||||
log: createLog(),
|
||||
settings: null,
|
||||
allCombos: [outer, child],
|
||||
});
|
||||
|
||||
assert.equal(result.ok, true);
|
||||
assert.deepEqual(calls, ["openai/a", "claude/b"]);
|
||||
});
|
||||
|
||||
test("unknown strategy value normalizes to priority order", async () => {
|
||||
const calls: string[] = [];
|
||||
const result = await handleComboChat({
|
||||
|
||||
Reference in New Issue
Block a user