Files
OmniRoute/src/lib/usage/comboScoringInspector.ts
3g0r1ch d87b97a786 feat(routing): adaptive feedback loop v2 — operational/semantic quality, confidence, TTFT/ITL, end-to-end test (#10881)
Obrigado — feature substancial e bem estruturada: separa qualidade operacional (comportamento de wire: 4xx/5xx, 429, respostas malformadas, stream interrompido) de qualidade semântica (só setada por avaliadores externos, nunca inferida do sucesso HTTP), com confidence/sample-awareness para não deixar poucos sucessos de sorte dominarem o ranking. Instrumentação de streaming (TTFT/ITL) threaded até RoutingEvent, endpoint de explicabilidade, e teste E2E determinístico cobrindo degradação→recuperação→blip.

Validação (worktree própria a partir de origin/release/v3.8.50, merge limpo, 0 conflitos):
- typecheck:core limpo, complexity/cognitive-complexity dentro do baseline
- 59/59 testes passando (mlx-provider, routing-adaptive-e2e, routing-events(-concurrency), routing-otel, routing-quality, routing-scoring-quality, stream-timing, auto-combo-scoring-clamp)
2026-08-20 17:28:30 -03:00

527 lines
18 KiB
TypeScript

import { buildComboHealthAutopilotReport } from "@/lib/monitoring/comboHealthAutopilot";
import { getCombos } from "@/lib/db/combos";
import { getProviderConnections } from "@/lib/db/providers";
import { buildComboForecastResponse } from "@/lib/usage/comboForecast";
import { buildComboHealthResponse } from "@/lib/usage/comboHealth";
import {
inspectTargetResilience,
type ProviderConnectionView,
} from "@/lib/usage/resilienceExplain";
import { getCircuitBreaker } from "@/shared/utils/circuitBreaker";
import {
calculateFactors,
calculateScore,
DEFAULT_WEIGHTS,
type ProviderCandidate,
type ScoringFactors,
type ScoringWeights,
validateWeights,
} from "@omniroute/open-sse/services/autoCombo/scoring.ts";
import { getModePack } from "@omniroute/open-sse/services/autoCombo/modePacks.ts";
import { getTaskFitness } from "@omniroute/open-sse/services/autoCombo/taskFitness.ts";
import type {
ComboAutopilotCombo,
ComboAutopilotReport,
ComboForecastResponse,
ComboForecastHorizon,
ComboForecastRiskLevel,
ComboForecastTarget,
ComboHealthMetrics,
ComboHealthResponse,
ComboRecord,
ComboScoringInspectorCombo,
ComboScoringInspectorFactor,
ComboScoringInspectorFactorKey,
ComboScoringInspectorResponse,
ComboScoringInspectorSource,
ComboScoringInspectorTarget,
ComboScoringInspectorWeightSource,
UtilizationTimeRange,
} from "@/shared/types/utilization";
export interface ComboScoringInspectorOptions {
range: UtilizationTimeRange;
horizon: ComboForecastHorizon;
comboId?: string;
taskType?: string;
now?: number;
combos?: ComboRecord[];
healthResponse?: ComboHealthResponse;
forecastResponse?: ComboForecastResponse;
autopilotReport?: ComboAutopilotReport;
skipAutopilot?: boolean;
}
type TargetHealth = NonNullable<ComboHealthMetrics["targetHealth"]>[number];
type CandidateContext = {
target: TargetHealth;
forecastTarget?: ComboForecastTarget;
autopilotIssueCount: number;
sources: Partial<Record<ComboScoringInspectorFactorKey, ComboScoringInspectorSource>>;
notes: Partial<Record<ComboScoringInspectorFactorKey, string>>;
};
type InspectorWeights = {
weights: ScoringWeights;
source: ComboScoringInspectorWeightSource;
modePack: string | null;
warning?: string;
};
const FACTOR_KEYS: ComboScoringInspectorFactorKey[] = [
"quota",
"health",
"costInv",
"latencyInv",
"taskFit",
"stability",
"tierPriority",
"tierAffinity",
"specificityMatch",
"contextAffinity",
"cacheAffinity",
"sessionAvailability",
"resetWindowAffinity",
"connectionDensity",
"quality",
];
function roundNumber(value: number, digits = 4): number {
if (!Number.isFinite(value)) return 0;
return Number(value.toFixed(digits));
}
function normalizeTaskType(taskType: string | undefined): string {
return typeof taskType === "string" && taskType.trim().length > 0 ? taskType.trim() : "default";
}
function isRecord(value: unknown): value is Record<string, unknown> {
return value !== null && typeof value === "object" && !Array.isArray(value);
}
/**
* Resolves the auto-strategy config object a combo's weights should be read from,
* honoring the same precedence the runtime auto-combo strategy uses: a dedicated
* `autoConfig`, then a nested `config.auto`, then the raw `config`, then an empty
* fallback so callers can probe optional fields without further null-checks.
*/
function resolveInspectorAutoConfig(combo: ComboRecord | undefined): Record<string, unknown> {
if (isRecord(combo?.autoConfig)) return combo.autoConfig;
if (isRecord(combo?.config)) {
if (isRecord(combo.config.auto)) return combo.config.auto;
return combo.config;
}
return {};
}
/** Extracts the mode-pack name referenced by the config, if any. */
function resolveModePackName(config: Record<string, unknown>): string | null {
return typeof config.modePack === "string" ? config.modePack : null;
}
/** Resolves an explicit, validated `weights` object from the config, if present. */
function resolveExplicitWeights(config: Record<string, unknown>): ScoringWeights | undefined {
const explicitWeights = isRecord(config.weights) ? (config.weights as ScoringWeights) : undefined;
return explicitWeights && validateWeights(explicitWeights) ? explicitWeights : undefined;
}
function resolveInspectorWeights(combo: ComboRecord | undefined): InspectorWeights {
const config = resolveInspectorAutoConfig(combo);
const modePack = resolveModePackName(config);
const resolvedModePack = modePack ? getModePack(modePack) : undefined;
if (resolvedModePack) {
return { weights: resolvedModePack, source: "mode_pack", modePack };
}
const explicitWeights = resolveExplicitWeights(config);
if (explicitWeights) {
return { weights: explicitWeights, source: "explicit", modePack: null };
}
return {
weights: DEFAULT_WEIGHTS,
source: "default",
modePack: null,
warning:
modePack || isRecord(config.weights)
? "Configured auto weights are invalid or unavailable; default weights were used."
: undefined,
};
}
async function resolveConfiguredCombos(
options: ComboScoringInspectorOptions
): Promise<ComboRecord[]> {
// #7087: `options.combos`, when supplied, must always win — regardless of the
// health/forecast/autopilot short-circuit below. The dashboard integration
// (comboHealthDashboard.ts::buildComboHealthDashboardResponse) always calls this
// with `combos` AND `healthResponse`/`forecastResponse`/`autopilotReport` set
// together (it already fetched everything once upstream to avoid redundant work),
// which used to hit the short-circuit FIRST and silently discard the caller's
// `combos` — so `combosById`/`combosByName` (used by resolveInspectorWeights() to
// report the combo's real configured modePack/weights) came back empty and every
// combo's weightSource fell back to "default" through that call path.
if (options.combos) return options.combos;
const alreadyHaveHealthSignals =
options.healthResponse &&
options.forecastResponse &&
(options.skipAutopilot || options.autopilotReport);
if (alreadyHaveHealthSignals) return [];
return (await getCombos()) as ComboRecord[];
}
function buildEmptyAutopilotReport(options: ComboScoringInspectorOptions): ComboAutopilotReport {
return {
status: "healthy",
checkedAt: new Date(options.now ?? Date.now()).toISOString(),
timeRange: options.range,
horizon: options.horizon,
summary: {
comboCount: 0,
healthyCount: 0,
degradedCount: 0,
downCount: 0,
issueCount: 0,
actionableCount: 0,
},
combos: [],
};
}
function forecastRiskAffinity(risk: ComboForecastRiskLevel | null | undefined): number {
switch (risk) {
case "low":
return 0.8;
case "medium":
return 0.5;
case "high":
return 0.2;
case "critical":
return 0;
case "unknown":
default:
return 0.5;
}
}
function projectedCostPer1MTokens(forecastTarget: ComboForecastTarget | undefined): number | null {
if (!forecastTarget || forecastTarget.forecast.projectedTokens <= 0) return null;
return (
(forecastTarget.forecast.projectedCostUsd / forecastTarget.forecast.projectedTokens) * 1_000_000
);
}
function quotaRemaining(target: TargetHealth, forecastTarget: ComboForecastTarget | undefined) {
if (typeof target.quotaRemainingPct === "number") {
return {
value: target.quotaRemainingPct,
source: "combo_health" as const,
note: "Current target quota from combo health snapshots.",
};
}
if (typeof forecastTarget?.quota.projectedRemainingPct === "number") {
return {
value: forecastTarget.quota.projectedRemainingPct,
source: "combo_forecast" as const,
note: "Projected quota fallback from combo forecast.",
};
}
return {
value: 100,
source: "default" as const,
note: "No target quota signal; optimistic default used.",
};
}
function circuitState(provider: string): ProviderCandidate["circuitBreakerState"] {
const state = String(getCircuitBreaker(provider).getStatus().state);
if (state === "OPEN" || state === "HALF_OPEN") return state;
return "CLOSED";
}
function buildCandidate(
target: TargetHealth,
forecastTarget: ComboForecastTarget | undefined
): { candidate: ProviderCandidate; context: CandidateContext } {
const sources: CandidateContext["sources"] = {};
const notes: CandidateContext["notes"] = {};
const quota = quotaRemaining(target, forecastTarget);
sources.quota = quota.source;
notes.quota = quota.note;
const cost = projectedCostPer1MTokens(forecastTarget);
sources.costInv = cost === null ? "default" : "combo_forecast";
notes.costInv =
cost === null
? "No forecast token/cost signal; neutral cost default used."
: "Cost per 1M tokens derived from combo forecast.";
const latency = target.avgLatencyMs > 0 ? target.avgLatencyMs : 1_500;
sources.latencyInv = target.avgLatencyMs > 0 ? "combo_health" : "default";
notes.latencyInv =
target.avgLatencyMs > 0
? "Latency from target health history/runtime metrics."
: "No latency signal; 1500ms default used.";
const successRate = target.requests > 0 ? Math.max(0, Math.min(100, target.successRate)) : 90;
sources.stability = target.requests > 0 ? "combo_health" : "default";
notes.stability =
target.requests > 0
? "Stability proxy derived from success rate and latency."
: "No target request history; neutral stability default used.";
sources.health = "runtime";
notes.health = "Provider circuit breaker state at inspection time.";
sources.taskFit = "default";
notes.taskFit = "Static model/task fitness heuristic.";
sources.tierPriority = "default";
notes.tierPriority = "Account tier not available in combo health; standard tier assumed.";
sources.tierAffinity = "default";
sources.specificityMatch = "default";
sources.contextAffinity = "default";
notes.contextAffinity = "Read-only inspector is not session-bound; neutral affinity used.";
sources.resetWindowAffinity = forecastTarget ? "combo_forecast" : "default";
notes.resetWindowAffinity = forecastTarget
? "Reset-window affinity proxied from forecast quota risk."
: "No forecast risk for this target; neutral reset affinity used.";
return {
candidate: {
provider: target.provider,
model: target.model,
quotaRemaining: quota.value,
quotaTotal: 100,
circuitBreakerState: circuitState(target.provider),
costPer1MTokens: cost ?? 1,
p95LatencyMs: Math.max(1, latency),
latencyStdDev: Math.max(10, latency * (1 - successRate / 100) + 10),
errorRate: 1 - successRate / 100,
accountTier: "standard",
contextAffinity: 0.5,
resetWindowAffinity: forecastRiskAffinity(forecastTarget?.quota.risk),
},
context: {
target,
forecastTarget,
autopilotIssueCount: 0,
sources,
notes,
},
};
}
function factorBreakdown(
factors: ScoringFactors,
weights: ScoringWeights,
context: CandidateContext
): ComboScoringInspectorFactor[] {
return FACTOR_KEYS.map((key) => {
// Optional factors (cacheAffinity/sessionAvailability/quality) default to
// their scoring neutral (1 for a factor, 0 for a weight) so the contribution
// sum stays consistent with calculateScore.
const value = factors[key] ?? 1;
const weight = weights[key] ?? 0;
return {
key,
value: roundNumber(value),
weight: roundNumber(weight),
contribution: roundNumber(value * weight),
source: context.sources[key] ?? "default",
note: context.notes[key],
};
}).sort((left, right) => Math.abs(right.contribution) - Math.abs(left.contribution));
}
function targetForecastMap(targets: ComboForecastTarget[]): Map<string, ComboForecastTarget> {
const map = new Map<string, ComboForecastTarget>();
for (const target of targets) {
map.set(target.executionKey, target);
if (target.stepId) map.set(target.stepId, target);
}
return map;
}
function autopilotIssueCounts(combo: ComboAutopilotCombo | undefined): Map<string, number> {
const counts = new Map<string, number>();
if (!combo) return counts;
for (const issue of combo.issues) {
const key = issue.target.executionKey;
if (!key) continue;
counts.set(key, (counts.get(key) ?? 0) + 1);
}
return counts;
}
async function buildInspectorCombo(
combo: ComboHealthMetrics,
forecastTargets: Map<string, ComboForecastTarget>,
autopilotCombo: ComboAutopilotCombo | undefined,
taskType: string,
inspectorWeights: InspectorWeights
): Promise<ComboScoringInspectorCombo> {
const warnings: string[] = [];
const { weights } = inspectorWeights;
if (inspectorWeights.warning) warnings.push(inspectorWeights.warning);
const targets = combo.targetHealth ?? [];
if (combo.strategy !== "auto") {
warnings.push(
"Combo strategy is not auto; this is an explanatory recompute, not the runtime strategy."
);
}
if (targets.length === 0) {
warnings.push("Combo has no inspectable execution targets.");
}
const issueCounts = autopilotIssueCounts(autopilotCombo);
const contexts = targets.map((target) =>
buildCandidate(
target,
forecastTargets.get(target.executionKey) || forecastTargets.get(target.stepId)
)
);
const pool = contexts.map((entry) => entry.candidate);
const scored = contexts
.map((entry) => {
const factors = calculateFactors(entry.candidate, pool, taskType, getTaskFitness);
const score = calculateScore(factors, weights);
const issueCount = issueCounts.get(entry.context.target.executionKey) ?? 0;
entry.context.autopilotIssueCount = issueCount;
return { entry, factors, score };
})
.sort((left, right) => right.score - left.score);
const connectionsByProvider = new Map<string, ProviderConnectionView[]>();
await Promise.allSettled(
[...new Set(scored.map((item) => item.entry.context.target.provider).filter(Boolean))].map(
async (provider) => {
try {
connectionsByProvider.set(
provider,
(await getProviderConnections({ provider, isActive: true })) as ProviderConnectionView[]
);
} catch {
warnings.push(
`Provider connection prefetch failed for ${provider}; resilience details will use fallback inspection.`
);
}
}
)
);
const inspectorTargets: ComboScoringInspectorTarget[] = await Promise.all(
scored.map(async (item, index) => {
const target = item.entry.context.target;
const resilience = await inspectTargetResilience({
provider: target.provider,
model: target.model,
connectionId: target.connectionId,
providerConnections: connectionsByProvider.get(target.provider),
});
return {
executionKey: target.executionKey,
stepId: target.stepId,
provider: target.provider,
model: target.model,
connectionId: target.connectionId,
label: target.label,
rank: index + 1,
score: roundNumber(item.score),
factors: factorBreakdown(item.factors, weights, item.entry.context),
signals: {
quotaRemainingPct: target.quotaRemainingPct,
projectedQuotaRemainingPct:
item.entry.context.forecastTarget?.quota.projectedRemainingPct ?? null,
successRate: target.requests > 0 ? target.successRate : null,
avgLatencyMs: target.avgLatencyMs > 0 ? target.avgLatencyMs : null,
forecastRisk: item.entry.context.forecastTarget?.quota.risk ?? null,
autopilotIssueCount: item.entry.context.autopilotIssueCount,
resilience,
},
};
})
);
return {
comboId: combo.comboId,
comboName: combo.comboName,
strategy: combo.strategy,
taskType,
weights: weights as Record<ComboScoringInspectorFactorKey, number>,
weightSource: inspectorWeights.source,
modePack: inspectorWeights.modePack,
selectedExecutionKey: inspectorTargets[0]?.executionKey ?? null,
targets: inspectorTargets,
warnings,
};
}
export async function buildComboScoringInspectorResponse(
options: ComboScoringInspectorOptions
): Promise<ComboScoringInspectorResponse> {
const taskType = normalizeTaskType(options.taskType);
const configuredCombos = await resolveConfiguredCombos(options);
const [health, forecast] = await Promise.all([
options.healthResponse ??
buildComboHealthResponse({
range: options.range,
comboId: options.comboId,
now: options.now,
combos: configuredCombos,
}),
options.forecastResponse ??
buildComboForecastResponse({
range: options.range,
horizon: options.horizon,
comboId: options.comboId,
now: options.now,
combos: configuredCombos,
}),
]);
const autopilot =
options.autopilotReport ??
(options.skipAutopilot
? buildEmptyAutopilotReport(options)
: await buildComboHealthAutopilotReport({
range: options.range,
horizon: options.horizon,
comboId: options.comboId,
includeHealthy: true,
includeActions: false,
now: options.now,
combos: configuredCombos,
healthResponse: health,
forecastResponse: forecast,
}));
const forecastByComboId = new Map(forecast.combos.map((combo) => [combo.comboId, combo]));
const autopilotByComboId = new Map(autopilot.combos.map((combo) => [combo.comboId, combo]));
const combosById = new Map(
configuredCombos.flatMap((combo) => (combo.id ? [[combo.id, combo]] : []))
);
const combosByName = new Map(
configuredCombos.flatMap((combo) => (combo.name ? [[combo.name, combo]] : []))
);
return {
asOf: new Date(options.now ?? Date.now()).toISOString(),
timeRange: options.range,
horizon: options.horizon,
method: "read_only_recompute",
combos: await Promise.all(
health.combos.map((combo) =>
buildInspectorCombo(
combo,
targetForecastMap(forecastByComboId.get(combo.comboId)?.targets ?? []),
autopilotByComboId.get(combo.comboId),
taskType,
resolveInspectorWeights(
combosById.get(combo.comboId) ?? combosByName.get(combo.comboName)
)
)
)
),
};
}