fix(combo): exclude hidden leaves from catalog and dispatch (#8878)

Validated in local merge-train T5 (base49+contributors+pacocartones)
This commit is contained in:
Ahmet Çetinkaya
2026-08-06 06:08:52 +03:00
committed by GitHub
parent 9751821338
commit 749fc75beb
21 changed files with 1043 additions and 134 deletions

View File

@@ -54,6 +54,7 @@ const INTENT_TO_TASK: Record<IntentType, TaskType> = {
export interface PipelineComboParams {
body: Record<string, unknown>;
combo: Record<string, unknown>;
availableModels?: readonly string[];
handleChatCore: (body: Record<string, unknown>, modelStr?: string) => Promise<Response>;
log: {
info: (...args: unknown[]) => void;
@@ -85,7 +86,7 @@ export interface StageExecutorResult {
*/
function resolveModelForTier(
tier: FitnessTier,
availableModels: string[],
availableModels: readonly string[],
taskType: string
): string {
// Score each available model for this task type and tier
@@ -125,7 +126,7 @@ function createStageExecutor(
body: Record<string, unknown>,
handleChatCore: (body: Record<string, unknown>, modelStr?: string) => Promise<Response>,
log: { info: (...args: unknown[]) => void; warn: (...args: unknown[]) => void },
availableModels: string[],
availableModels: readonly string[],
taskType: string
): (args: StageExecutorArgs & { fitnessTier?: FitnessTier }) => Promise<StageExecutorResult> {
return async ({
@@ -222,6 +223,7 @@ function estimateTokens(messages: Array<{ role: string; content: unknown }>): nu
export async function handlePipelineCombo({
body,
combo,
availableModels: routedModels,
handleChatCore,
log,
settings,
@@ -291,7 +293,13 @@ export async function handlePipelineCombo({
})
.filter((model): model is string => typeof model === "string" && model.length > 0)
: [];
const availableModels = comboModels.length ? comboModels : ["deepseek-chat"];
const availableModels =
routedModels === undefined
? comboModels.length
? comboModels
: ["deepseek-chat"]
: routedModels;
if (availableModels.length === 0) throw new Error("PIPELINE_NO_MODELS");
// ── Create stage executor ─────────────────────────────────────────────────
const stageExecutor = createStageExecutor(body, handleChatCore, log, availableModels, taskType);

View File

@@ -570,6 +570,7 @@ export async function handleComboChat({
signal,
apiKeyAllowedConnections = null,
nesting = null,
hiddenModelsByProvider = getHiddenModelsByProvider(),
}: HandleComboChatOptions): Promise<Response> {
const comboCtx = createComboContext({ body, combo, settings, relayOptions, log });
const {
@@ -607,6 +608,7 @@ export async function handleComboChat({
clientRequestedStream,
handleSingleModelWithTimeout,
log,
hiddenModelsByProvider,
});
if (pinnedDispatch) return pinnedDispatch;
}
@@ -628,6 +630,7 @@ export async function handleComboChat({
relayOptions,
signal,
apiKeyAllowedConnections,
hiddenModelsByProvider,
runCombo: handleComboChat,
});
if (fusionDispatch) return fusionDispatch;
@@ -636,7 +639,12 @@ export async function handleComboChat({
// chaosEngine.ts (dispatchChaosFromCombo), returning null when not chaos-enabled.
const chaosDispatch = dispatchChaosFromCombo({
cfg,
comboModels: combo.models || [],
comboModels: resolveComboTargets(
combo,
allCombos,
clampComboDepth(config.maxComboDepth),
hiddenModelsByProvider
).map((target) => target.modelStr),
comboName: combo.name,
body,
handleSingleModel: handleSingleModelWithTimeout,
@@ -649,8 +657,10 @@ export async function handleComboChat({
combo,
config,
strategy,
allCombos,
handleSingleModelWithTimeout,
log,
hiddenModelsByProvider,
});
if (pipelineDispatch) return pipelineDispatch;
@@ -669,6 +679,7 @@ export async function handleComboChat({
relayOptions,
signal,
apiKeyAllowedConnections,
hiddenModelsByProvider,
runCombo: handleComboChat,
});
if (runtimeUnitDispatch) return runtimeUnitDispatch;
@@ -684,6 +695,7 @@ export async function handleComboChat({
settings,
allCombos,
signal,
hiddenModelsByProvider,
});
}
@@ -708,6 +720,7 @@ export async function handleComboChat({
isModelAvailable,
handleSingleModelWithTimeout,
buildAutoCandidates,
hiddenModelsByProvider,
});
if ("earlyResponse" in targetResolution) return targetResolution.earlyResponse;
const { stickyWeightedLimit, getWeightedStepKeyForTarget, preScreenMap } = targetResolution;
@@ -749,7 +762,7 @@ export async function handleComboChat({
combo,
config,
body,
resolveShadowTargets(combo, config, allCombos),
resolveShadowTargets(combo, config, allCombos, hiddenModelsByProvider),
handleSingleModel,
isModelAvailable,
strategy,
@@ -2184,6 +2197,7 @@ async function handleRoundRobinCombo({
settings,
allCombos,
signal,
hiddenModelsByProvider = getHiddenModelsByProvider(),
}: HandleRoundRobinOptions): Promise<Response> {
const config = settings
? resolveComboConfig(combo, settings)
@@ -2225,7 +2239,8 @@ async function handleRoundRobinCombo({
const orderedTargets = resolveComboTargets(
rrExpandedCombo,
rrExpandedAllCombos,
clampComboDepth(config.maxComboDepth)
clampComboDepth(config.maxComboDepth),
hiddenModelsByProvider
);
const tagFilteredTargets = await applyRequestTagRouting(orderedTargets, body, log);
const evalRankedTargets = orderTargetsByEvalScores(tagFilteredTargets, config.evalRouting, log);
@@ -2298,7 +2313,7 @@ async function handleRoundRobinCombo({
combo,
config,
body,
resolveShadowTargets(combo, config, allCombos),
resolveShadowTargets(combo, config, allCombos, hiddenModelsByProvider),
handleSingleModel,
isModelAvailable,
"round-robin",

View File

@@ -12,12 +12,14 @@
*/
import { getModelContextLimit } from "../../../src/lib/modelCapabilities";
import { getHiddenModelsByProvider } from "../../../src/lib/db/models";
import { getComboModelString, normalizeComboStep } from "../../../src/lib/combos/steps.ts";
import { getProviderByAlias, getProviderById } from "../../../src/shared/constants/providers.ts";
import { estimateTokens } from "../contextManager.ts";
import { getResolvedModelCapabilities } from "../modelCapabilities.ts";
import { parseModel, stripContextWindowSuffix } from "../model.ts";
import { dedupeTargetsByExecutionKey, isRecord } from "./comboData.ts";
import { isComboModelVisible } from "./comboVisibility.ts";
import { getTargetProvider, MAX_COMBO_DEPTH } from "./comboPredicates.ts";
import { evaluateContextLimit } from "./contextOverrideGate.ts";
import { hasEstimableContent } from "./knownContextOverflow.ts";
@@ -32,6 +34,7 @@ import type {
ComboLike,
ComboLogger,
ComboRuntimeStep,
HiddenModelsByProvider,
NestedComboMode,
ResolvedComboTarget,
ResolvedComboUnit,
@@ -132,15 +135,28 @@ function normalizeRuntimeStep(
: {}),
weight,
label,
prompt: step.prompt || null,
} satisfies ResolvedComboTarget;
}
function getDirectComboTargets(combo: ComboLike): ResolvedComboTarget[] {
return getOrderedTopLevelRuntimeSteps(combo, null).filter(
(entry): entry is ResolvedComboTarget => entry?.kind === "model"
function isComboTargetVisible(
target: ResolvedComboTarget,
hiddenModelsByProvider: HiddenModelsByProvider
): boolean {
return isComboModelVisible(
target.modelStr,
target.providerId || target.provider,
hiddenModelsByProvider
);
}
export function filterVisibleComboTargets(
targets: ResolvedComboTarget[],
hiddenModelsByProvider: HiddenModelsByProvider = getHiddenModelsByProvider()
): ResolvedComboTarget[] {
return targets.filter((target) => isComboTargetVisible(target, hiddenModelsByProvider));
}
function getTopLevelRuntimeSteps(
combo: ComboLike,
allCombos: ComboCollectionLike,
@@ -832,36 +848,50 @@ export function sortTargetsByContextSize(targets: ResolvedComboTarget[]) {
export function resolveComboTargets(
combo: ComboLike,
allCombos: ComboCollectionLike,
maxDepth: number = MAX_COMBO_DEPTH
maxDepth: number = MAX_COMBO_DEPTH,
hiddenModelsByProvider: HiddenModelsByProvider = getHiddenModelsByProvider()
): ResolvedComboTarget[] {
return allCombos
? resolveNestedComboTargets(combo, allCombos, new Set<string>(), 0, [], maxDepth)
: getDirectComboTargets(combo);
return filterVisibleComboTargets(
allCombos
? resolveNestedComboTargets(combo, allCombos, new Set<string>(), 0, [], maxDepth)
: getOrderedTopLevelRuntimeSteps(combo, null).filter(
(entry): entry is ResolvedComboTarget => entry?.kind === "model"
),
hiddenModelsByProvider
);
}
export function resolveComboRuntimeUnits(
combo: ComboLike,
allCombos: ComboCollectionLike,
mode: NestedComboMode,
maxDepth: number = MAX_COMBO_DEPTH
maxDepth: number = MAX_COMBO_DEPTH,
hiddenModelsByProvider: HiddenModelsByProvider = getHiddenModelsByProvider()
): ResolvedComboUnit[] {
if (mode === "flatten" || !allCombos) return resolveComboTargets(combo, allCombos, maxDepth);
if (mode === "flatten" || !allCombos)
return resolveComboTargets(combo, allCombos, maxDepth, hiddenModelsByProvider);
validateComboDAG(combo.name, allCombos, new Set<string>(), 0, maxDepth);
return getOrderedTopLevelRuntimeSteps(combo, allCombos);
return getOrderedTopLevelRuntimeSteps(combo, allCombos).filter(
(unit) => unit.kind === "combo-ref" || isComboTargetVisible(unit, hiddenModelsByProvider)
);
}
export function resolveWeightedStepGroups(
combo: ComboLike,
allCombos: ComboCollectionLike
allCombos: ComboCollectionLike,
hiddenModelsByProvider: HiddenModelsByProvider = getHiddenModelsByProvider()
): 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])),
targets: filterVisibleComboTargets(
!allCombos
? step.kind === "model"
? [step]
: []
: expandRuntimeStep(step, allCombos, new Set([combo.name])),
hiddenModelsByProvider
),
}))
.filter((group) => group.targets.length > 0);
}

View File

@@ -0,0 +1,23 @@
import { getHiddenModelsByProvider } from "../../../src/lib/db/models";
import { parseModel, resolveCanonicalProviderModel } from "../model.ts";
import type { HiddenModelsByProvider } from "./types.ts";
export function isComboModelVisible(
modelStr: string,
providerId: string | null = null,
hiddenModelsByProvider: HiddenModelsByProvider = getHiddenModelsByProvider()
): boolean {
const parsed = parseModel(modelStr);
const hasExplicitProvider =
providerId && providerId !== parsed.provider && providerId !== parsed.providerAlias;
const rawModel = hasExplicitProvider ? modelStr : parsed.model || modelStr;
const resolved = resolveCanonicalProviderModel(
providerId || parsed.provider || parsed.providerAlias,
rawModel
);
return (
!resolved.provider ||
!resolved.model ||
!hiddenModelsByProvider.get(resolved.provider)?.has(resolved.model)
);
}

View File

@@ -45,6 +45,7 @@ import type {
HandleComboChatOptions,
HandleSingleModel,
IsModelAvailable,
HiddenModelsByProvider,
NestedComboMode,
ResolvedComboUnit,
SingleModelTarget,
@@ -68,6 +69,7 @@ type PreludeBaseOptionArgs = {
relayOptions?: HandleComboChatOptions["relayOptions"];
signal?: AbortSignal | null;
apiKeyAllowedConnections?: string[] | null;
hiddenModelsByProvider?: HiddenModelsByProvider;
};
/** Rebuild handleComboChat's option bag verbatim for a recursive dispatch. */
@@ -83,6 +85,7 @@ function buildBaseOptions(a: PreludeBaseOptionArgs): HandleComboChatOptions {
relayOptions: a.relayOptions,
signal: a.signal,
apiKeyAllowedConnections: a.apiKeyAllowedConnections,
hiddenModelsByProvider: a.hiddenModelsByProvider,
};
}
@@ -232,6 +235,7 @@ export async function tryPinnedModelDispatch(args: {
clientRequestedStream: boolean;
handleSingleModelWithTimeout: HandleSingleModel;
log: ComboLogger;
hiddenModelsByProvider?: HiddenModelsByProvider;
}): Promise<Response | null> {
const {
body,
@@ -242,6 +246,7 @@ export async function tryPinnedModelDispatch(args: {
clientRequestedStream,
handleSingleModelWithTimeout,
log,
hiddenModelsByProvider,
} = args;
// The pin is read from session_model_history (a PRIOR turn) and may name a
// model that has since been removed from this combo, or a provider whose
@@ -254,11 +259,12 @@ export async function tryPinnedModelDispatch(args: {
// when allCombos is authoritative (non-empty) so we can resolve combo-refs;
// the auto-combo redirect path passes an empty list and keeps prior behavior.
const haveFullCombos = Array.isArray(allCombos) ? allCombos.length > 0 : !!allCombos;
const pinInCombo =
!haveFullCombos ||
resolveComboTargets(combo, allCombos, clampComboDepth(config.maxComboDepth)).some(
(t) => t.modelStr === pinnedModel
);
const pinInCombo = resolveComboTargets(
combo,
haveFullCombos ? allCombos : null,
clampComboDepth(config.maxComboDepth),
hiddenModelsByProvider
).some((target) => target.modelStr === pinnedModel);
// Honor the pin only if it is still a combo target AND its provider is not
// DURABLY down. Without the health gate a pin keeps routing a session to a
// dead/credits-exhausted/throttled account forever (strategy bypassed, no
@@ -330,6 +336,7 @@ export async function tryFusionDispatch(args: {
relayOptions?: HandleComboChatOptions["relayOptions"];
signal?: AbortSignal | null;
apiKeyAllowedConnections?: string[] | null;
hiddenModelsByProvider?: HiddenModelsByProvider;
runCombo: RunCombo;
}): Promise<Response | null> {
const { cfg, combo, config, strategy, log } = args;
@@ -347,9 +354,14 @@ export async function tryFusionDispatch(args: {
if (strategy !== "fusion") return null;
const { panel: fusionModels, comboRefUnits } = extractFusionPanelSpec(
combo.models || [],
resolveComboTargets(
combo,
args.allCombos,
clampComboDepth(config.maxComboDepth),
args.hiddenModelsByProvider
).map((target) => target.modelStr),
combo.name,
args.allCombos
null
);
// Untyped like the existing `nestingContext` further down — `nesting` is
// already `ComboNestingContext | null` per HandleComboChatOptions, no new
@@ -389,26 +401,28 @@ export async function tryPipelineDispatch(args: {
combo: ComboLike;
config: ComboSetupConfig;
strategy: string;
allCombos?: ComboCollectionLike;
handleSingleModelWithTimeout: HandleSingleModel;
log: ComboLogger;
hiddenModelsByProvider?: HiddenModelsByProvider;
}): Promise<Response | null> {
const { body, combo, config, strategy, handleSingleModelWithTimeout, log } = args;
const {
body,
combo,
config,
strategy,
allCombos,
handleSingleModelWithTimeout,
log,
hiddenModelsByProvider,
} = args;
if (strategy !== "pipeline") return null;
const pipelineSteps = (combo.models || [])
.map((m): PipelineStep | null => {
if (typeof m === "string") return { model: m };
if (m && typeof m === "object") {
const obj = m as Record<string, unknown>;
if (typeof obj.model === "string") {
return {
model: obj.model,
prompt: typeof obj.prompt === "string" ? obj.prompt : undefined,
};
}
}
return null;
})
.filter((s): s is PipelineStep => Boolean(s));
const pipelineSteps: PipelineStep[] = resolveComboTargets(
combo,
allCombos,
clampComboDepth(config.maxComboDepth),
hiddenModelsByProvider
).map((target) => ({ target, prompt: target.prompt }));
return handlePipelineChat({
body,
steps: pipelineSteps,
@@ -523,6 +537,7 @@ export async function tryRuntimeUnitDispatch(args: {
relayOptions?: HandleComboChatOptions["relayOptions"];
signal?: AbortSignal | null;
apiKeyAllowedConnections?: string[] | null;
hiddenModelsByProvider?: HiddenModelsByProvider;
runCombo: RunCombo;
}): Promise<Response | null> {
const { body, combo, config, strategy, allCombos, log, settings } = args;
@@ -531,7 +546,13 @@ export async function tryRuntimeUnitDispatch(args: {
const executeModeUnits =
nestedComboMode === "execute" && allCombos
? resolveComboRuntimeUnits(combo, allCombos, "execute", nestingContext.maxDepth)
? resolveComboRuntimeUnits(
combo,
allCombos,
"execute",
nestingContext.maxDepth,
args.hiddenModelsByProvider
)
: [];
const hasExecutableComboRef = executeModeUnits.some((unit) => unit.kind === "combo-ref");
const simpleExecuteStrategies = new Set([

View File

@@ -16,13 +16,14 @@
import { secureRandomFloat } from "../../../src/shared/utils/secureRandom";
import { recordComboShadowRequest } from "../comboMetrics.ts";
import { isRecord } from "./comboData.ts";
import { resolveNestedComboTargets } from "./comboStructure.ts";
import { filterVisibleComboTargets, resolveNestedComboTargets } from "./comboStructure.ts";
import { toRecordedTarget } from "./comboPredicates.ts";
import type {
ComboLike,
ComboCollectionLike,
ComboLogger,
HandleSingleModel,
HiddenModelsByProvider,
IsModelAvailable,
ResolvedComboTarget,
ShadowRoutingConfig,
@@ -47,7 +48,8 @@ function normalizeShadowRoutingConfig(config: Record<string, unknown>): ShadowRo
export function resolveShadowTargets(
combo: ComboLike,
config: Record<string, unknown>,
allCombos: ComboCollectionLike
allCombos: ComboCollectionLike,
hiddenModelsByProvider?: HiddenModelsByProvider
): ResolvedComboTarget[] {
const shadowConfig = normalizeShadowRoutingConfig(config);
if (!shadowConfig.enabled || shadowConfig.targets.length === 0) return [];
@@ -58,7 +60,10 @@ export function resolveShadowTargets(
name: `${combo.name}:shadow`,
models: shadowConfig.targets,
};
return resolveNestedComboTargets(shadowCombo, allCombos, new Set([combo.name]), 0, ["shadow"])
return filterVisibleComboTargets(
resolveNestedComboTargets(shadowCombo, allCombos, new Set([combo.name]), 0, ["shadow"]),
hiddenModelsByProvider
)
.slice(0, shadowConfig.maxTargets)
.map((target) => ({
...target,

View File

@@ -88,6 +88,7 @@ import type {
ComboRuntimeStep,
HandleSingleModel,
IsModelAvailable,
HiddenModelsByProvider,
ResolvedComboTarget,
} from "./types.ts";
@@ -111,6 +112,7 @@ export interface ResolveComboTargetPipelineDeps {
* this leaf), so importing it directly would create an import cycle.
*/
buildAutoCandidates: ResolveAutoStrategyDeps["buildAutoCandidates"];
hiddenModelsByProvider?: HiddenModelsByProvider;
}
export interface ResolvedComboTargetPipeline {
@@ -204,10 +206,15 @@ async function collectWeightedEligibility(
expandedCombo: ComboLike,
expandedAllCombos: ComboCollectionLike,
resilienceSettings: ResilienceSettings,
isModelAvailable?: IsModelAvailable
isModelAvailable?: IsModelAvailable,
hiddenModelsByProvider?: HiddenModelsByProvider
): Promise<{ stepGroups: WeightedStepGroups; weightedEligibleKeys: Set<string> }> {
const weightedEligibleKeys = new Set<string>();
const stepGroups = resolveWeightedStepGroups(expandedCombo, expandedAllCombos);
const stepGroups = resolveWeightedStepGroups(
expandedCombo,
expandedAllCombos,
hiddenModelsByProvider
);
for (const group of stepGroups) {
const availability = await Promise.all(
group.targets.map((target) =>
@@ -260,7 +267,8 @@ async function resolveWeightedSelection(
expandedCombo,
expandedAllCombos,
deps.resilienceSettings,
deps.isModelAvailable
deps.isModelAvailable,
deps.hiddenModelsByProvider
);
stepGroups = eligibility.stepGroups;
weightedEligibleKeys = eligibility.weightedEligibleKeys;
@@ -351,7 +359,8 @@ function logTargetPoolSize(
* auto routing (pipeline disabled, below token threshold, or dispatch failure).
*/
async function dispatchSmartPipeline(
deps: ResolveComboTargetPipelineDeps
deps: ResolveComboTargetPipelineDeps,
availableModels: readonly string[]
): Promise<Response | null> {
const { body, combo, strategy, config, settings, signal, log } = deps;
if (strategy !== "auto") return null;
@@ -362,6 +371,7 @@ async function dispatchSmartPipeline(
const pipelineRaw = await handlePipelineCombo({
body,
combo,
availableModels,
handleChatCore: deps.handleSingleModelWithTimeout,
log: {
info: log.info,
@@ -687,7 +697,8 @@ export async function resolveComboTargetPipeline(
: resolveComboTargets(
expandedCombo,
expandedAllCombos,
clampComboDepth(config.maxComboDepth)
clampComboDepth(config.maxComboDepth),
deps.hiddenModelsByProvider
);
orderedTargets = await applyRequestTagRouting(orderedTargets, body, log);
@@ -699,7 +710,10 @@ export async function resolveComboTargetPipeline(
logTargetPoolSize(strategy, allCombos, orderedTargets, stickyWeightedKey, log);
const pipelineResponse = await dispatchSmartPipeline(deps);
const pipelineResponse = await dispatchSmartPipeline(
deps,
orderedTargets.map((target) => target.modelStr)
);
if (pipelineResponse) return { earlyResponse: pipelineResponse };
const ordering = await orderByStrategy(deps, orderedTargets);

View File

@@ -95,6 +95,8 @@ export type ComboNestingContext = {
attemptBudget: { count: number; limit: number };
};
export type HiddenModelsByProvider = ReadonlyMap<string, ReadonlySet<string>>;
export type HandleComboChatOptions = {
body: Record<string, unknown>;
combo: ComboLike;
@@ -107,6 +109,7 @@ export type HandleComboChatOptions = {
signal?: AbortSignal | null;
apiKeyAllowedConnections?: string[] | null;
nesting?: ComboNestingContext | null;
hiddenModelsByProvider?: HiddenModelsByProvider;
};
export type HandleRoundRobinOptions = Omit<
@@ -167,6 +170,7 @@ export type ResolvedComboTarget = {
allowedConnectionIds?: string[] | null;
weight: number;
label: string | null;
prompt?: string | null;
failoverBeforeRetry?: unknown;
trafficType?: "production" | "shadow";
/**

View File

@@ -20,7 +20,7 @@
*/
import { errorResponse, sanitizeErrorMessage } from "../utils/error.ts";
import { extractTextContent } from "../translator/helpers/geminiHelper.ts";
import type { ComboLogger, HandleSingleModel } from "./combo/types.ts";
import type { ComboLogger, HandleSingleModel, ResolvedComboTarget } from "./combo/types.ts";
// Fusion tuning. Overridable per-combo via combo.config.fusionTuning.
export const FUSION_DEFAULTS = {
@@ -108,10 +108,7 @@ export function appendUserTurn(body: Body, text: string): Body {
} else if (Array.isArray(body.input)) {
next.input = [...(body.input as unknown[]), { role: "user", content: text }];
} else if (Array.isArray(body.contents)) {
next.contents = [
...(body.contents as unknown[]),
{ role: "user", parts: [{ text }] },
];
next.contents = [...(body.contents as unknown[]), { role: "user", parts: [{ text }] }];
} else {
next.messages = [{ role: "user", content: text }];
}
@@ -159,10 +156,7 @@ export function isToolBearingRequest(body: Body): boolean {
type Sentinel = { __timeout?: true; __error?: unknown };
// Resolve a Response (or sentinel) within ms; the loser keeps running but is ignored.
function withTimeout(
promise: Promise<Response>,
ms: number
): Promise<Response | Sentinel> {
function withTimeout(promise: Promise<Response>, ms: number): Promise<Response | Sentinel> {
return new Promise((resolve) => {
const t = setTimeout(() => resolve({ __timeout: true }), ms);
Promise.resolve(promise)
@@ -224,16 +218,33 @@ export function collectPanel(
});
}
export type FusionModel = ResolvedComboTarget | string;
export type HandleFusionChatOptions = {
body: Body;
models: string[];
models: FusionModel[];
handleSingleModel: HandleSingleModel;
log: ComboLogger;
comboName?: string;
judgeModel?: string | null;
judgeTarget?: ResolvedComboTarget | null;
tuning?: FusionTuning | null;
};
function getFusionModelString(model: FusionModel): string {
return typeof model === "string" ? model : model.modelStr;
}
function dispatchFusionModel(
handleSingleModel: HandleSingleModel,
body: Body,
model: FusionModel
): Promise<Response> {
return typeof model === "string"
? handleSingleModel(body, model)
: handleSingleModel(body, model.modelStr, model);
}
/**
* Handle a fusion combo: fan the prompt out to every panel model in parallel,
* then a judge model synthesizes one final answer from all panel responses.
@@ -260,6 +271,7 @@ export async function handleFusionChat({
log,
comboName,
judgeModel,
judgeTarget,
tuning,
}: HandleFusionChatOptions): Promise<Response> {
const panel = Array.isArray(models) ? models.filter(Boolean) : [];
@@ -269,7 +281,7 @@ export async function handleFusionChat({
// A single-model fusion has nothing to fuse — just answer directly.
if (panel.length === 1) {
return handleSingleModel(body, panel[0]);
return dispatchFusionModel(handleSingleModel, body, panel[0]);
}
// Reject an oversized panel BEFORE fan-out (issue #1905): fanning out N
@@ -296,10 +308,10 @@ export async function handleFusionChat({
// gracefully via the answers.length===1 branch below (issue #6454).
const minPanel = Math.min(Math.max(1, cfg.minPanel), panel.length);
const hasExplicitJudge = Boolean(judgeModel && judgeModel.trim());
const judge = hasExplicitJudge ? (judgeModel as string).trim() : panel[0];
const judge = hasExplicitJudge ? (judgeModel as string).trim() : getFusionModelString(panel[0]);
log.info(
"FUSION",
`Combo "${comboName ?? ""}" | panel=${panel.length} [${panel.join(", ")}] | judge=${judge} | quorum=${minPanel}`
`Combo "${comboName ?? ""}" | panel=${panel.length} [${panel.map(getFusionModelString).join(", ")}] | judge=${judge} | quorum=${minPanel}`
);
// Tool-bearing requests get no value from panel synthesis — panel members
@@ -322,8 +334,8 @@ export async function handleFusionChat({
void _tc;
const panelBody: Body = { ...rest, stream: false };
const t0 = Date.now();
const calls = panel.map((m) =>
withTimeout(handleSingleModel(panelBody, m), cfg.panelHardTimeoutMs)
const calls = panel.map((target) =>
withTimeout(dispatchFusionModel(handleSingleModel, panelBody, target), cfg.panelHardTimeoutMs)
);
const settled = await collectPanel(calls, { ...cfg, minPanel });
log.info("FUSION", `fan-out collected in ${Date.now() - t0}ms`);
@@ -333,7 +345,7 @@ export async function handleFusionChat({
const failures: Array<{ model: string; reason: string }> = [];
for (let i = 0; i < settled.length; i++) {
const res = settled[i];
const model = panel[i];
const model = getFusionModelString(panel[i]);
if (!res) {
log.warn("FUSION", `Panel ${model} dropped (straggler/timeout)`);
failures.push({ model, reason: "straggler_dropped" });
@@ -399,10 +411,7 @@ export async function handleFusionChat({
// synthesizing from a single source through itself would be redundant —
// answer directly with the lone survivor (issue #6454).
if (!hasExplicitJudge) {
log.info(
"FUSION",
`Only ${answers[0].model} succeeded — answering directly (no fusion)`
);
log.info("FUSION", `Only ${answers[0].model} succeeded — answering directly (no fusion)`);
return handleSingleModel(body, answers[0].model);
}
// An explicit judgeModel IS configured: honor it even with a single
@@ -421,8 +430,8 @@ export async function handleFusionChat({
// SURVIVOR: prefer panel[0] when it survived, otherwise the first survivor.
const effectiveJudge = hasExplicitJudge
? judge
: answers.some((a) => a.model === panel[0])
? panel[0]
: answers.some((a) => a.model === getFusionModelString(panel[0]))
? getFusionModelString(panel[0])
: answers[0].model;
if (answers.length === 1) {
@@ -435,5 +444,7 @@ export async function handleFusionChat({
// 4. Judge analyzes + writes one final answer (streams to client if requested).
const judgeBody = appendUserTurn(body, buildJudgePrompt(answers));
log.info("FUSION", `Judging ${answers.length} answers with ${effectiveJudge}`);
return handleSingleModel(judgeBody, effectiveJudge);
return judgeTarget
? handleSingleModel(judgeBody, judgeTarget.modelStr, judgeTarget)
: handleSingleModel(judgeBody, effectiveJudge);
}

View File

@@ -40,14 +40,30 @@
* a bad-request or auth error wastes quota and will never succeed.
*/
import { errorResponse } from "../utils/error.ts";
import type { ComboLogger, HandleSingleModel } from "./combo/types.ts";
import type { ComboLogger, HandleSingleModel, ResolvedComboTarget } from "./combo/types.ts";
// extractPanelText is a generic assistant-text extractor (OpenAI chat / Claude /
// Gemini / Responses) — reused here to read each step's output, not fusion-specific.
import { extractPanelText } from "./fusion.ts";
type Body = Record<string, unknown>;
export type PipelineStep = { model: string; prompt?: string | null };
export type PipelineStep =
| {
target: ResolvedComboTarget;
prompt?: string | null;
}
| {
model: string;
prompt?: string | null;
};
function getStepModel(step: PipelineStep): string {
return "target" in step ? step.target.modelStr : step.model;
}
function getStepTarget(step: PipelineStep): ResolvedComboTarget | undefined {
return "target" in step ? step.target : undefined;
}
/**
* Prepend a system instruction to the client's original conversation (format-aware),
@@ -146,23 +162,32 @@ export async function handlePipelineChat({
maxRetries = 0,
retryDelayMs = 1000,
}: HandlePipelineChatOptions): Promise<Response> {
const chain = (Array.isArray(steps) ? steps : []).filter((s) => s && s.model);
const chain = (Array.isArray(steps) ? steps : []).filter((step): step is PipelineStep =>
Boolean(step && getStepModel(step))
);
if (chain.length === 0) {
return errorResponse(400, "Pipeline combo has no models");
}
log.info(
"PIPELINE",
`Combo "${comboName ?? ""}" | steps=${chain.length} [${chain.map((s) => s.model).join(" -> ")}]`
`Combo "${comboName ?? ""}" | steps=${chain.length} [${chain.map(getStepModel).join(" -> ")}]`
);
// Single-step pipeline: nothing to chain — run it directly (streams to client).
if (chain.length === 1) {
return handleSingleModel(prependSystemInstruction(body, chain[0].prompt), chain[0].model);
const step = chain[0];
return handleSingleModel(
prependSystemInstruction(body, step.prompt),
getStepModel(step),
getStepTarget(step)
);
}
let prevOutput = "";
for (let i = 0; i < chain.length; i++) {
const step = chain[i];
const stepModel = getStepModel(step);
const stepTarget = getStepTarget(step);
const isFinal = i === chain.length - 1;
const isFirst = i === 0;
@@ -174,46 +199,55 @@ export async function handlePipelineChat({
if (!isFinal) stepBody = stripStreaming(stepBody);
const t0 = Date.now();
let res = await handleSingleModel(stepBody, step.model);
let res = await handleSingleModel(stepBody, stepModel, stepTarget);
if (isFinal) {
log.info("PIPELINE", `Final step ${step.model} responded (${Date.now() - t0}ms)`);
log.info("PIPELINE", `Final step ${stepModel} responded (${Date.now() - t0}ms)`);
return res;
}
// Transient retry: if the intermediate step failed with a retryable status
// (429/502/503/504), retry the same step up to maxRetries times before
// giving up. Non-transient errors (400/401/403/404) fail immediately.
for (let attempt = 0; attempt < maxRetries && !res.ok && TRANSIENT_STATUS.has(res.status); attempt++) {
for (
let attempt = 0;
attempt < maxRetries && !res.ok && TRANSIENT_STATUS.has(res.status);
attempt++
) {
log.warn(
"PIPELINE",
`Step ${i + 1} (${step.model}) transient ${res.status}, retrying ${attempt + 1}/${maxRetries} in ${retryDelayMs}ms`
`Step ${i + 1} (${stepModel}) transient ${res.status}, retrying ${attempt + 1}/${maxRetries} in ${retryDelayMs}ms`
);
await sleep(retryDelayMs);
res = await handleSingleModel(stepBody, step.model);
res = await handleSingleModel(stepBody, stepModel, stepTarget);
}
// An intermediate step must succeed with usable text — otherwise fail the whole
// pipeline (never silently swallow; the client gets a clear, sanitized error).
if (!res.ok) {
log.warn("PIPELINE", `Step ${i + 1} (${step.model}) failed`, { status: res.status });
log.warn("PIPELINE", `Step ${i + 1} (${stepModel}) failed`, {
status: res.status,
});
const status = res.status >= 400 && res.status <= 599 ? res.status : 502;
return errorResponse(status, `Pipeline step ${i + 1} (${step.model}) failed`);
return errorResponse(status, `Pipeline step ${i + 1} (${stepModel}) failed`);
}
try {
const json = await res.clone().json();
prevOutput = extractPanelText(json);
} catch {
log.warn("PIPELINE", `Step ${i + 1} (${step.model}) returned an unparseable body`);
return errorResponse(502, `Pipeline step ${i + 1} (${step.model}) returned an unparseable body`);
log.warn("PIPELINE", `Step ${i + 1} (${stepModel}) returned an unparseable body`);
return errorResponse(
502,
`Pipeline step ${i + 1} (${stepModel}) returned an unparseable body`
);
}
if (!prevOutput.trim()) {
log.warn("PIPELINE", `Step ${i + 1} (${step.model}) returned empty output`);
return errorResponse(502, `Pipeline step ${i + 1} (${step.model}) returned empty output`);
log.warn("PIPELINE", `Step ${i + 1} (${stepModel}) returned empty output`);
return errorResponse(502, `Pipeline step ${i + 1} (${stepModel}) returned empty output`);
}
log.info(
"PIPELINE",
`Step ${i + 1} ${step.model} ok (${prevOutput.length} chars, ${Date.now() - t0}ms)`
`Step ${i + 1} ${stepModel} ok (${prevOutput.length} chars, ${Date.now() - t0}ms)`
);
}

View File

@@ -489,14 +489,13 @@ async function buildUnifiedModelsResponseCore(
const buildComboCatalogMetadata = (
combo: Parameters<typeof resolveNestedComboTargets>[0],
allCombos: Parameters<typeof resolveNestedComboTargets>[1]
targets: ComboCatalogTarget[]
) => {
const explicitContextLength = isPositiveFiniteNumber(combo.context_length)
? combo.context_length
: undefined;
const baseMetadata = explicitContextLength ? { context_length: explicitContextLength } : {};
const targets = resolveNestedComboTargets(combo, allCombos) as ComboCatalogTarget[];
if (targets.length === 0) return baseMetadata;
const targetMetadata = targets.map((target) => getComboTargetCatalogMetadata(target));
@@ -641,16 +640,13 @@ async function buildUnifiedModelsResponseCore(
combo as Parameters<typeof resolveNestedComboTargets>[0],
combos as Parameters<typeof resolveNestedComboTargets>[1]
) as ComboCatalogTarget[];
if (
comboTargets.some((target) => {
const resolved = getComboTargetModelId(target);
return resolved ? getModelIsHidden(resolved.providerId, resolved.modelId) : false;
})
) {
continue;
}
const visibleTargets = comboTargets.filter((target) => {
const resolved = getComboTargetModelId(target);
return resolved ? !getModelIsHidden(resolved.providerId, resolved.modelId) : true;
});
if (visibleTargets.length === 0) continue;
const comboMetadata = buildComboCatalogMetadata(combo, combos);
const comboMetadata = buildComboCatalogMetadata(combo, visibleTargets);
listedIds.add(combo.name);
models.push({

View File

@@ -27,6 +27,7 @@ export interface CustomModelEntry {
export type ComboCatalogTarget = {
modelStr?: string;
provider?: string | null;
providerId?: string | null;
};
export type ComboTargetCatalogMetadata = {

View File

@@ -1,6 +1,6 @@
import { PROVIDER_MODELS, PROVIDER_ID_TO_ALIAS } from "@/shared/constants/models";
import { AI_PROVIDERS } from "@/shared/constants/providers";
import { parseModel } from "@omniroute/open-sse/services/model";
import { parseModel, resolveCanonicalProviderModel } from "@omniroute/open-sse/services/model";
// Alias <-> providerId resolution maps for the unified model catalog. Extracted
// verbatim from ./catalog.ts. `FALLBACK_ALIAS_TO_PROVIDER` is also consumed directly by
@@ -77,6 +77,7 @@ export type AliasMaps = ReturnType<typeof buildAliasMaps>;
export type ProviderPrefixedTarget = {
modelStr?: string;
provider?: string | null;
providerId?: string | null;
};
/**
@@ -142,20 +143,30 @@ export function getComboTargetModelId(
maps: AliasMaps,
target: ProviderPrefixedTarget
): { providerId: string; modelId: string } | null {
const rawProvider = typeof target.provider === "string" ? target.provider.trim() : "";
const rawProvider =
typeof target.providerId === "string"
? target.providerId.trim()
: typeof target.provider === "string"
? target.provider.trim()
: "";
const modelStr = typeof target.modelStr === "string" ? target.modelStr.trim() : "";
if (!rawProvider || rawProvider === "unknown" || !modelStr) return null;
const providerId = resolveCanonicalProviderId(maps.aliasToProviderId, rawProvider);
if (!providerId || providerId === "unknown") return null;
let modelId = modelStr;
for (const prefix of getProviderPrefixes(maps, providerId, rawProvider)) {
const prefixWithSlash = `${prefix}/`;
if (modelStr.startsWith(prefixWithSlash)) {
const modelId = modelStr.slice(prefixWithSlash.length).trim();
return modelId ? { providerId, modelId } : null;
modelId = modelStr.slice(prefixWithSlash.length).trim();
break;
}
}
return { providerId, modelId: modelStr };
if (!modelId) return null;
const canonical = resolveCanonicalProviderModel(providerId, modelId);
return canonical.provider && canonical.model
? { providerId: canonical.provider, modelId: canonical.model }
: null;
}

View File

@@ -16,6 +16,7 @@ export interface ComboModelStep {
allowedConnectionIds?: string[] | null;
weight: number;
label?: string;
prompt?: string | null;
tags?: string[];
}
@@ -237,6 +238,7 @@ export function normalizeComboStep(
const explicitId = toTrimmedString(value.id);
const weight = toWeight(value.weight);
const label = toTrimmedString(value.label);
const prompt = toTrimmedString(value.prompt);
if (value.kind === "combo-ref") {
const comboRefName = toTrimmedString(value.comboName);
@@ -291,6 +293,7 @@ export function normalizeComboStep(
...(connectionId !== undefined ? { connectionId } : {}),
weight,
...(label ? { label } : {}),
...(prompt ? { prompt } : {}),
...(tags && tags.length > 0 ? { tags } : {}),
...(allowedConnectionIds && allowedConnectionIds.length > 0 ? { allowedConnectionIds } : {}),
};

View File

@@ -857,35 +857,45 @@ export function getModelIsHidden(providerId: string, modelId: string): boolean {
*/
export function getHiddenModelsByProvider(): Map<string, Set<string>> {
const db = getDbInstance();
const result = new Map<string, Set<string>>();
// Query all rows from key_value for both namespaces
const visibilityByProvider = new Map<string, Map<string, boolean>>();
const rows = db
.prepare(
"SELECT key, value FROM key_value WHERE namespace IN ('modelCompatOverrides', 'customModels')"
"SELECT namespace, key, value FROM key_value WHERE namespace IN ('modelCompatOverrides', 'customModels')"
)
.all() as Array<{ key: string; value: string | null }>;
.all() as Array<{ namespace: string; key: string; value: string | null }>;
for (const row of rows) {
if (!row.value) continue;
try {
const parsed = JSON.parse(row.value);
if (!Array.isArray(parsed)) continue;
for (const entry of parsed) {
if (entry && typeof entry === "object" && entry.isHidden) {
const modelId = entry.id;
if (typeof modelId === "string" && modelId.length > 0) {
if (!result.has(row.key)) result.set(row.key, new Set());
result.get(row.key)!.add(modelId);
for (const namespace of ["modelCompatOverrides", "customModels"]) {
for (const row of rows) {
if (row.namespace !== namespace || !row.value) continue;
try {
const parsed = JSON.parse(row.value);
if (!Array.isArray(parsed)) continue;
for (const entry of parsed) {
if (!entry || typeof entry !== "object") continue;
const modelId = (entry as { id?: unknown }).id;
if (typeof modelId !== "string" || modelId.length === 0) continue;
if (!Object.prototype.hasOwnProperty.call(entry, "isHidden")) continue;
let visibility = visibilityByProvider.get(row.key);
if (!visibility) {
visibility = new Map<string, boolean>();
visibilityByProvider.set(row.key, visibility);
}
visibility.set(modelId, Boolean((entry as { isHidden?: unknown }).isHidden));
}
} catch {
// Skip malformed entries
}
} catch {
// Skip malformed entries
}
}
return result;
return new Map(
[...visibilityByProvider].flatMap(([providerId, visibility]) => {
const hiddenModels = [...visibility].flatMap(([modelId, isHidden]) =>
isHidden ? [modelId] : []
);
return hiddenModels.length > 0 ? [[providerId, new Set(hiddenModels)] as const] : [];
})
);
}
/**

View File

@@ -27,9 +27,13 @@ import fs from "node:fs";
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-test-hidden-4558-"));
process.env.DATA_DIR = tmpDir;
const { mergeModelCompatOverride, getHiddenModelsByProvider, getModelIsHidden } = await import(
"../../src/lib/localDb.ts"
);
const {
addCustomModel,
getHiddenModelsByProvider,
getModelIsHidden,
mergeModelCompatOverride,
updateCustomModel,
} = await import("../../src/lib/localDb.ts");
const { resetDbInstance } = await import("../../src/lib/db/core.ts");
before(() => {
@@ -79,3 +83,13 @@ test("un-hiding a model (isHidden: null) removes it from the map", () => {
"un-hidden model must drop out of the hidden set"
);
});
test("an explicit visible custom-model setting overrides a hidden compat fallback", async () => {
const modelId = "gpt-custom-visible-override";
mergeModelCompatOverride(PROVIDER, modelId, { isHidden: true });
await addCustomModel(PROVIDER, modelId);
await updateCustomModel(PROVIDER, modelId, { isHidden: false });
assert.equal(getModelIsHidden(PROVIDER, modelId), false);
assert.equal(getHiddenModelsByProvider().get(PROVIDER)?.has(modelId) ?? false, false);
});

View File

@@ -16,6 +16,7 @@ process.env.DATA_DIR = TEST_DATA_DIR;
process.env.API_KEY_SECRET = process.env.API_KEY_SECRET || "combo-fusion-test-secret";
const { handleComboChat } = await import("../../open-sse/services/combo.ts");
const modelsDb = await import("../../src/lib/db/models.ts");
const noop = () => {};
const log = { info: noop, warn: noop, debug: noop, error: noop };
@@ -224,6 +225,91 @@ test("fusion: honors an explicit judgeModel even with a single surviving panel a
assert.equal(res.status, 200);
});
test("fusion: preserves explicit providers when filtering structured panel targets", async () => {
modelsDb.mergeModelCompatOverride("nvidia", "openai/gpt-oss-120b", { isHidden: true });
const seen: string[] = [];
const res = await handleComboChat({
body: { messages: [{ role: "user", content: "Q" }] },
combo: {
name: "structured-fusion-combo",
strategy: "fusion",
models: [
{ model: "openai/gpt-oss-120b", providerId: "nvidia" },
{ model: "p/a" },
{ model: "p/b" },
],
config: {},
},
handleSingleModel: async (_body: Body, model: string) => {
seen.push(model);
return okResponse(`answer-${model}`);
},
log,
settings: {},
allCombos: [],
});
assert.equal(res.status, 200);
assert.deepEqual(seen, ["p/a", "p/b", "p/a"]);
});
test("fusion: dispatches visible structured panel targets with their provider identity", async () => {
const targets: Array<{ model: string; providerId?: string | null }> = [];
const res = await handleComboChat({
body: { messages: [{ role: "user", content: "Q" }] },
combo: {
name: "structured-fusion-dispatch",
strategy: "fusion",
models: [
{ model: "vendor/model-a", providerId: "nvidia" },
{ model: "vendor/model-b", providerId: "nvidia" },
],
config: {},
},
handleSingleModel: async (_body: Body, model: string, target) => {
targets.push({
model,
providerId: target && "providerId" in target ? target.providerId : undefined,
});
return okResponse(`answer-${model}`);
},
log,
settings: {},
allCombos: [],
});
assert.equal(res.status, 200);
assert.deepEqual(targets.slice(0, 2), [
{ model: "vendor/model-a", providerId: "nvidia" },
{ model: "vendor/model-b", providerId: "nvidia" },
]);
});
test("fusion: never dispatches hidden panel members or a hidden explicit judge", async () => {
modelsDb.mergeModelCompatOverride("p", "hidden-panel", { isHidden: true });
modelsDb.mergeModelCompatOverride("p", "hidden-judge", { isHidden: true });
const seen: string[] = [];
const res = await handleComboChat({
body: { messages: [{ role: "user", content: "Q" }] },
combo: fusionCombo(["p/hidden-panel", "p/a", "p/b"], {
judgeModel: "p/hidden-judge",
}),
handleSingleModel: async (_body: Body, model: string) => {
seen.push(model);
return okResponse(`answer-${model}`);
},
log,
settings: {},
allCombos: [],
});
assert.equal(res.status, 200);
assert.deepEqual(seen, ["p/a", "p/b", "p/a"]);
});
test("fusion: returns 503 when the whole panel fails", async () => {
const handleSingleModel = async () => errResponse(500);
const res = await handleComboChat({

View File

@@ -0,0 +1,328 @@
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import test from "node:test";
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-hidden-combo-routing-"));
process.env.DATA_DIR = TEST_DATA_DIR;
const { handleComboChat, resolveShadowTargets } = await import("../../open-sse/services/combo.ts");
const core = await import("../../src/lib/db/core.ts");
const contextHandoffsDb = await import("../../src/lib/db/contextHandoffs.ts");
const modelsDb = await import("../../src/lib/db/models.ts");
const noop = (..._args: unknown[]) => {};
const log = { info: noop, warn: noop, error: noop, debug: noop };
function okResponse(): Response {
return Response.json({ choices: [{ message: { content: "ok" } }] });
}
test.beforeEach(() => {
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
});
test.after(() => {
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
});
test("handleComboChat never routes hidden leaves in priority, weighted, or round-robin combos", async () => {
modelsDb.mergeModelCompatOverride("openai", "hidden-combo-leaf", { isHidden: true });
for (const strategy of ["priority", "weighted", "round-robin"] as const) {
const calls: string[] = [];
const result = await handleComboChat({
body: {},
combo: {
name: `hidden-${strategy}`,
strategy,
models: [
{ model: "openai/hidden-combo-leaf", weight: 100 },
{ model: "openai/visible-combo-leaf", weight: 1 },
],
config: { maxRetries: 0, concurrencyPerModel: 1, queueTimeoutMs: 1000 },
},
handleSingleModel: async (_body: unknown, modelStr: string) => {
calls.push(modelStr);
return okResponse();
},
isModelAvailable: async () => true,
log,
settings: null,
relayOptions: null,
allCombos: null,
});
assert.equal(result.ok, true);
assert.deepEqual(calls, ["openai/visible-combo-leaf"]);
}
});
test("handleComboChat preserves explicit providers for custom slashful model ids", async () => {
modelsDb.mergeModelCompatOverride("nvidia", "vendor/model-x", { isHidden: true });
const calls: string[] = [];
const result = await handleComboChat({
body: {},
combo: {
name: "structured-custom-slashful",
strategy: "priority",
models: [
{ model: "vendor/model-x", providerId: "nvidia" },
{ model: "openai/visible-sibling", providerId: "openai" },
],
config: { maxRetries: 0 },
},
handleSingleModel: async (_body: unknown, modelStr: string) => {
calls.push(modelStr);
return okResponse();
},
isModelAvailable: async () => true,
log,
settings: null,
relayOptions: null,
allCombos: null,
});
assert.equal(result.ok, true);
assert.deepEqual(calls, ["openai/visible-sibling"]);
});
test("handleComboChat filters hidden leaves through nested flatten and execute modes", async () => {
modelsDb.mergeModelCompatOverride("openai", "hidden-nested-leaf", { isHidden: true });
const child = {
name: "hidden-leaf-child",
strategy: "priority",
models: ["openai/hidden-nested-leaf", "openai/visible-nested-leaf"],
};
for (const nestedComboMode of ["flatten", "execute"] as const) {
const calls: string[] = [];
const parent = {
name: `hidden-leaf-parent-${nestedComboMode}`,
strategy: "priority",
models: [{ kind: "combo-ref", comboName: child.name }],
config: { nestedComboMode, maxRetries: 0 },
};
const result = await handleComboChat({
body: {},
combo: parent,
handleSingleModel: async (_body: unknown, modelStr: string) => {
calls.push(modelStr);
return okResponse();
},
isModelAvailable: async () => true,
log,
settings: null,
relayOptions: null,
allCombos: [parent, child],
});
assert.equal(result.ok, true);
assert.deepEqual(calls, ["openai/visible-nested-leaf"]);
}
});
test("handleComboChat auto pipeline never dispatches hidden combo leaves", async () => {
modelsDb.mergeModelCompatOverride("openai", "o3", { isHidden: true });
const calls: string[] = [];
const result = await handleComboChat({
body: {
messages: [
{
role: "user",
content:
"Analyze this complex distributed systems failure and produce a rigorous implementation plan with explicit tradeoffs, invariants, edge cases, and verification steps for every stage.",
},
],
},
combo: {
name: "auto/smart",
strategy: "auto",
models: ["openai/o3", "openai/gpt-4o-mini"],
config: {
pipeline_enabled: true,
skip_pipeline_for_tokens_under: 0,
max_reflection_loops: 0,
},
},
handleSingleModel: async (_body: unknown, modelStr: string) => {
calls.push(modelStr);
return okResponse();
},
isModelAvailable: async () => true,
log,
settings: null,
relayOptions: null,
allCombos: null,
});
assert.equal(result.ok, true);
assert.ok(calls.length > 0);
assert.deepEqual(new Set(calls), new Set(["openai/gpt-4o-mini"]));
});
test("handleComboChat drops a context-cache pin when the pinned model becomes hidden", async () => {
const sessionId = "hidden-pin-session";
const comboName = "hidden-pin-combo";
modelsDb.mergeModelCompatOverride("openai", "hidden-pinned-leaf", { isHidden: true });
contextHandoffsDb.recordSessionModelUsage(
sessionId,
comboName,
"openai/hidden-pinned-leaf",
"openai"
);
const calls: string[] = [];
const result = await handleComboChat({
body: { messages: [{ role: "user", content: "continue" }] },
combo: {
name: comboName,
strategy: "priority",
models: ["openai/hidden-pinned-leaf", "openai/visible-pinned-leaf"],
context_cache_protection: true,
},
handleSingleModel: async (_body: unknown, modelStr: string) => {
calls.push(modelStr);
return okResponse();
},
isModelAvailable: async () => true,
log,
settings: null,
relayOptions: { sessionId },
allCombos: null,
});
assert.equal(result.ok, true);
assert.deepEqual(calls, ["openai/visible-pinned-leaf"]);
});
test("resolveShadowTargets excludes hidden leaves before applying the target limit", () => {
modelsDb.mergeModelCompatOverride("openai", "hidden-shadow-leaf", { isHidden: true });
const targets = resolveShadowTargets(
{ name: "hidden-shadow-combo", models: [] },
{
shadowRouting: {
enabled: true,
targets: ["openai/hidden-shadow-leaf", "openai/visible-shadow-leaf"],
sampleRate: 1,
maxTargets: 1,
},
},
null
);
assert.deepEqual(
targets.map((target) => target.modelStr),
["openai/visible-shadow-leaf"]
);
});
test("handleComboChat canonicalizes provider and model aliases before filtering hidden leaves", async () => {
modelsDb.mergeModelCompatOverride("github", "claude-opus-4-5-20251101", { isHidden: true });
const calls: string[] = [];
const result = await handleComboChat({
body: {},
combo: {
name: "hidden-alias-leaf",
strategy: "priority",
models: ["gh/claude-4.5-opus", "openai/visible-alias-sibling"],
config: { maxRetries: 0 },
},
handleSingleModel: async (_body: unknown, modelStr: string) => {
calls.push(modelStr);
return okResponse();
},
isModelAvailable: async () => true,
log,
settings: null,
relayOptions: null,
allCombos: null,
});
assert.equal(result.ok, true);
assert.deepEqual(calls, ["openai/visible-alias-sibling"]);
});
test("handleComboChat preserves an explicit provider for slashful model ids", async () => {
modelsDb.mergeModelCompatOverride("nvidia", "openai/gpt-oss-120b", { isHidden: true });
const calls: string[] = [];
const result = await handleComboChat({
body: {},
combo: {
name: "hidden-structured-leaf",
strategy: "priority",
models: [
{ model: "openai/gpt-oss-120b", providerId: "nvidia" },
{ model: "openai/visible-structured-sibling", providerId: "openai" },
],
config: { maxRetries: 0 },
},
handleSingleModel: async (_body: unknown, modelStr: string) => {
calls.push(modelStr);
return okResponse();
},
isModelAvailable: async () => true,
log,
settings: null,
relayOptions: null,
allCombos: null,
});
assert.equal(result.ok, true);
assert.deepEqual(calls, ["openai/visible-structured-sibling"]);
});
test("handleComboChat reuses one hidden-model snapshot through nested execute mode", async () => {
modelsDb.mergeModelCompatOverride("openai", "hidden-snapshot-leaf", { isHidden: true });
const child = {
name: "hidden-snapshot-child",
strategy: "priority",
models: ["openai/hidden-snapshot-leaf", "openai/visible-snapshot-leaf"],
config: { nestedComboMode: "execute", maxRetries: 0 },
};
const parent = {
name: "hidden-snapshot-parent",
strategy: "priority",
models: [{ kind: "combo-ref", comboName: child.name }],
config: { nestedComboMode: "execute", maxRetries: 0 },
};
const db = core.getDbInstance();
const originalPrepare = db.prepare.bind(db);
let hiddenSnapshotReads = 0;
db.prepare = (sql: string) => {
if (sql.includes("namespace IN ('modelCompatOverrides', 'customModels')")) {
hiddenSnapshotReads += 1;
}
return originalPrepare(sql);
};
try {
const calls: string[] = [];
const result = await handleComboChat({
body: {},
combo: parent,
handleSingleModel: async (_body: unknown, modelStr: string) => {
calls.push(modelStr);
return okResponse();
},
isModelAvailable: async () => true,
log,
settings: null,
relayOptions: null,
allCombos: [parent, child],
});
assert.equal(result.ok, true);
assert.deepEqual(calls, ["openai/visible-snapshot-leaf"]);
assert.equal(hiddenSnapshotReads, 1);
} finally {
db.prepare = originalPrepare;
}
});

View File

@@ -16,6 +16,7 @@ process.env.DATA_DIR = TEST_DATA_DIR;
process.env.API_KEY_SECRET = process.env.API_KEY_SECRET || "combo-pipeline-test-secret";
const { handleComboChat } = await import("../../open-sse/services/combo.ts");
const modelsDb = await import("../../src/lib/db/models.ts");
const noop = () => {};
const log = { info: noop, warn: noop, debug: noop, error: noop };
@@ -103,6 +104,55 @@ test("pipeline: 2 steps — step 1 gets the original input, step 2 gets step 1's
assert.equal(json.choices[0].message.content, "FINAL_B");
});
test("pipeline: skips hidden steps without dispatching them", async () => {
modelsDb.mergeModelCompatOverride("p", "hidden-step", { isHidden: true });
const seen: string[] = [];
const res = await handleComboChat({
body: { messages: [{ role: "user", content: "hi" }] },
combo: pipelineCombo([{ model: "p/a" }, { model: "p/hidden-step" }, { model: "p/b" }]),
handleSingleModel: async (_body: Body, model: string) => {
seen.push(model);
return okResponse(`answer-${model}`);
},
log,
settings: {},
allCombos: [],
});
assert.equal(res.status, 200);
assert.deepEqual(seen, ["p/a", "p/b"]);
});
test("pipeline: preserves explicit providers when filtering structured steps", async () => {
modelsDb.mergeModelCompatOverride("nvidia", "openai/gpt-oss-120b", { isHidden: true });
const seen: string[] = [];
const res = await handleComboChat({
body: { messages: [{ role: "user", content: "hi" }] },
combo: {
name: "structured-pipeline-combo",
strategy: "pipeline",
models: [
{ model: "p/a" },
{ model: "openai/gpt-oss-120b", providerId: "nvidia" },
{ model: "p/b" },
],
config: {},
},
handleSingleModel: async (_body: Body, model: string) => {
seen.push(model);
return okResponse(`answer-${model}`);
},
log,
settings: {},
allCombos: [],
});
assert.equal(res.status, 200);
assert.deepEqual(seen, ["p/a", "p/b"]);
});
test("pipeline: 3-step chain threads output → input correctly", async () => {
const seen: string[] = [];
const seenBodies: Body[] = [];
@@ -203,3 +253,29 @@ test("pipeline: a single-step pipeline runs the one model directly and streams t
assert.equal(seenBodies[0].stream, true);
assert.equal(res.status, 200);
});
test("pipeline: dispatches visible structured steps with their provider identity", async () => {
const targets: Array<{ model: string; providerId?: string | null }> = [];
const res = await handleComboChat({
body: { messages: [{ role: "user", content: "hi" }] },
combo: {
name: "structured-pipeline-dispatch",
strategy: "pipeline",
models: [{ model: "vendor/model", providerId: "nvidia" }],
config: {},
},
handleSingleModel: async (_body: Body, model: string, target) => {
targets.push({
model,
providerId: target && "providerId" in target ? target.providerId : undefined,
});
return okResponse("done");
},
log,
settings: {},
allCombos: [],
});
assert.equal(res.status, 200);
assert.deepEqual(targets, [{ model: "vendor/model", providerId: "nvidia" }]);
});

View File

@@ -12,6 +12,7 @@ process.env.DATA_DIR = mkdtempSync(join(tmpdir(), "omniroute-embed-combo-reject-
const { createCombo } = await import("../../src/lib/db/combos.ts");
const { resetDbInstance } = await import("../../src/lib/db/core.ts");
const { createEmbeddingResponse } = await import("../../src/lib/embeddings/service.ts");
const { mergeModelCompatOverride } = await import("../../src/lib/db/models.ts");
test.after(() => {
// Release the SQLite handle so the native test runner can exit (CLAUDE.md #3).
@@ -76,3 +77,21 @@ test("createEmbeddingResponse allows a uniform-dimension embedding combo to proc
"uniform combo must not trip the dimension guard"
);
});
test("createEmbeddingResponse excludes hidden leaves before embedding family validation and dispatch", async () => {
mergeModelCompatOverride("nebius", "Qwen/Qwen3-Embedding-8B", { isHidden: true });
await createCombo({
name: "partial-hidden-embeds-combo",
strategy: "priority",
models: ["nebius/Qwen/Qwen3-Embedding-8B", "openai/text-embedding-3-small"],
});
const res = await createEmbeddingResponse({
model: "partial-hidden-embeds-combo",
input: "hello world",
});
const body = JSON.stringify(await res.json());
assert.doesNotMatch(body, /incompatible vector dimensions/);
assert.doesNotMatch(body, /Qwen3-Embedding-8B/);
});

View File

@@ -0,0 +1,200 @@
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import test from "node:test";
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-hidden-combo-catalog-"));
process.env.DATA_DIR = TEST_DATA_DIR;
process.env.API_KEY_SECRET ||= "hidden-combo-catalog-test-secret";
const apiKeysDb = await import("../../src/lib/db/apiKeys.ts");
const combosDb = await import("../../src/lib/db/combos.ts");
const core = await import("../../src/lib/db/core.ts");
const modelsDb = await import("../../src/lib/db/models.ts");
const modelsDevSync = await import("../../src/lib/modelsDevSync.ts");
const v1ModelsCatalog = await import("../../src/app/api/v1/models/catalog.ts");
type CatalogModel = {
id: string;
context_length?: number;
max_output_tokens?: number;
};
function capability(limitContext: number, limitOutput: number) {
return {
tool_call: null,
reasoning: null,
attachment: null,
structured_output: null,
temperature: null,
modalities_input: JSON.stringify([]),
modalities_output: JSON.stringify([]),
knowledge_cutoff: null,
release_date: null,
last_updated: null,
status: null,
family: null,
open_weights: null,
limit_context: limitContext,
limit_input: null,
limit_output: limitOutput,
interleaved_field: null,
};
}
async function getCatalogData(): Promise<CatalogModel[]> {
const response = await v1ModelsCatalog.getUnifiedModelsResponse(
new Request("http://localhost/api/v1/models")
);
const body: { data: CatalogModel[] } = await response.json();
assert.equal(response.status, 200);
return body.data;
}
test.beforeEach(() => {
core.resetDbInstance();
apiKeysDb.resetApiKeyState();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
v1ModelsCatalog.__resetCatalogBuilderRunsForTest();
});
test.after(() => {
modelsDevSync.saveModelsDevCapabilities({});
core.resetDbInstance();
apiKeysDb.resetApiKeyState();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
});
test("v1 models catalog keeps partially hidden combos and derives metadata from visible targets", async () => {
modelsDevSync.saveModelsDevCapabilities({
openai: {
"catalog-hidden-leaf": capability(1000, 100),
"catalog-visible-leaf": capability(8000, 800),
},
});
modelsDb.mergeModelCompatOverride("openai", "catalog-hidden-leaf", { isHidden: true });
await combosDb.createCombo({
name: "partially-hidden-router",
strategy: "priority",
models: ["openai/catalog-hidden-leaf", "openai/catalog-visible-leaf"],
});
const combo = (await getCatalogData()).find((item) => item.id === "partially-hidden-router");
assert.ok(combo);
assert.equal(combo.context_length, 8000);
assert.equal(combo.max_output_tokens, 800);
});
test("v1 models catalog omits combos when every resolved target is hidden", async () => {
modelsDb.mergeModelCompatOverride("openai", "catalog-hidden-alpha", { isHidden: true });
modelsDb.mergeModelCompatOverride("openai", "catalog-hidden-beta", { isHidden: true });
await combosDb.createCombo({
name: "fully-hidden-router",
strategy: "priority",
models: ["openai/catalog-hidden-alpha", "openai/catalog-hidden-beta"],
});
const data = await getCatalogData();
assert.equal(
data.some((item) => item.id === "fully-hidden-router"),
false
);
});
test("v1 models catalog applies hidden leaf filtering through nested combos", async () => {
modelsDevSync.saveModelsDevCapabilities({
openai: {
"nested-hidden-leaf": capability(2000, 200),
"nested-visible-leaf": capability(16000, 1600),
"nested-hidden-only": capability(4000, 400),
},
});
modelsDb.mergeModelCompatOverride("openai", "nested-hidden-leaf", { isHidden: true });
modelsDb.mergeModelCompatOverride("openai", "nested-hidden-only", { isHidden: true });
await combosDb.createCombo({
name: "partially-hidden-child",
strategy: "priority",
models: ["openai/nested-hidden-leaf", "openai/nested-visible-leaf"],
});
await combosDb.createCombo({
name: "partially-hidden-parent",
strategy: "priority",
models: ["partially-hidden-child"],
});
await combosDb.createCombo({
name: "fully-hidden-child",
strategy: "priority",
models: ["openai/nested-hidden-only"],
});
await combosDb.createCombo({
name: "fully-hidden-parent",
strategy: "priority",
models: ["fully-hidden-child"],
});
const data = await getCatalogData();
const parent = data.find((item) => item.id === "partially-hidden-parent");
assert.ok(parent);
assert.equal(parent.context_length, 16000);
assert.equal(parent.max_output_tokens, 1600);
assert.equal(
data.some((item) => item.id === "fully-hidden-child"),
false
);
assert.equal(
data.some((item) => item.id === "fully-hidden-parent"),
false
);
});
test("v1 models catalog canonicalizes aliases before deciding that a combo is fully hidden", async () => {
modelsDb.mergeModelCompatOverride("github", "claude-opus-4-5-20251101", { isHidden: true });
await combosDb.createCombo({
name: "hidden-alias-router",
strategy: "priority",
models: ["gh/claude-4.5-opus"],
});
const data = await getCatalogData();
assert.equal(
data.some((item) => item.id === "hidden-alias-router"),
false
);
});
test("v1 models catalog preserves explicit providers for slashful model ids", async () => {
modelsDb.mergeModelCompatOverride("nvidia", "openai/gpt-oss-120b", { isHidden: true });
await combosDb.createCombo({
name: "hidden-slashful-router",
strategy: "priority",
models: [{ model: "openai/gpt-oss-120b", providerId: "nvidia" }],
});
const data = await getCatalogData();
assert.equal(
data.some((item) => item.id === "hidden-slashful-router"),
false
);
});
test("v1 models catalog omits combos with no resolved targets", async () => {
await combosDb.createCombo({
name: "empty-router",
strategy: "priority",
models: [],
});
const data = await getCatalogData();
assert.equal(
data.some((item) => item.id === "empty-router"),
false
);
});