Files
2026-07-13 13:39:12 +08:00

270 lines
9.3 KiB
TypeScript

// 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, releaseQualityClone } from "./validateQuality.ts";
import type { ResponseValidationConfig } from "./responseValidation.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 };
}
let unitClone: Response;
try {
unitClone = response.clone();
} catch {
unitClone = response;
}
const quality = await validateResponseQuality(
unitClone,
clientRequestedStream,
args.log,
args.config.responseValidation as ResponseValidationConfig | undefined
);
releaseQualityClone(unitClone, response, quality);
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, 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,
};
}