mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-02 05:12:11 +03:00
## New Features - Combo Builder v2 wizard UI (multi-stage: Basics → Steps → Strategy → Review) - Combo Step Architecture Schema v2 (ComboModelStep, ComboRefStep, pinned accounts) - Composite Tiers system for tiered model routing with fallback chains - Model Capabilities Registry (unified resolver merging specs + registry + synced data) - Observability module (buildHealthPayload, buildTelemetryPayload, buildSessionsSummary) - Session & Quota Monitor panels on Health dashboard - Combo Health per-target analytics via resolveNestedComboTargets() - Combo Builder Options API (GET /api/combos/builder/options) ## Performance - Middleware lazy loading (apiAuth, db/settings, modelSyncScheduler) - E2E auth bypass mode (NEXT_PUBLIC_OMNIROUTE_E2E_MODE) ## Bug Fixes - P2C credential selection with quota headroom awareness - Fixed-account combo steps bypass model cooldowns/circuit breakers - Combo metrics per-target tracking (byTarget with executionKey) - Call logs schema expansion (7 new columns + composite index) - Quota monitor lifecycle enrichment (status, snapshots, summary) - Codex quota fetcher hardening ## Maintenance - DB migration 021 (combo_call_log_targets) - Combo CRUD normalization on read - Playwright config + build script improvements - OpenAPI spec version sync to 3.6.4 ## Tests - 16 new test suites + 12 existing test updates - 86 files changed, +8318 -1378 lines
107 lines
3.3 KiB
TypeScript
107 lines
3.3 KiB
TypeScript
/**
|
|
* Combo Resolver — FASE-09 Domain Extraction (T-46)
|
|
*
|
|
* Extracts combo resolution logic from handleChat into a dedicated
|
|
* domain service. Handles model selection based on combo strategy
|
|
* (priority, round-robin, random, least-used).
|
|
*
|
|
* @module domain/comboResolver
|
|
*/
|
|
|
|
/**
|
|
* @typedef {import('./types.js').Combo} Combo
|
|
*/
|
|
import { getComboStepTarget, getComboStepWeight } from "@/lib/combos/steps";
|
|
|
|
/** @type {Map<string, number>} Persistent round-robin counters per combo */
|
|
const roundRobinCounters = new Map();
|
|
|
|
/**
|
|
* Resolve which model to use from a combo based on its strategy.
|
|
*
|
|
* @param {Combo} combo - The combo configuration
|
|
* @param {{ modelUsageCounts?: Record<string, number> }} [context] - Optional context
|
|
* @returns {{ model: string, index: number }}
|
|
* @throws {Error} If combo has no models
|
|
*/
|
|
export function resolveComboModel(combo: any, context: any = {}) {
|
|
const models = combo.models || [];
|
|
if (models.length === 0) {
|
|
throw new Error(`Combo "${combo.name}" has no models configured`);
|
|
}
|
|
|
|
// Normalize models to { model, weight } format
|
|
const normalized = models
|
|
.map((entry) => ({
|
|
model: getComboStepTarget(entry) || "",
|
|
weight: getComboStepWeight(entry) || 1,
|
|
}))
|
|
.filter((entry) => entry.model);
|
|
|
|
const strategy = combo.strategy || "priority";
|
|
|
|
switch (strategy) {
|
|
case "priority":
|
|
return { model: normalized[0].model, index: 0 };
|
|
|
|
case "round-robin": {
|
|
// Persistent counter per combo for deterministic round-robin
|
|
const comboKey = combo.id || combo.name || "default";
|
|
if (!roundRobinCounters.has(comboKey)) {
|
|
roundRobinCounters.set(comboKey, 0);
|
|
}
|
|
const counter = roundRobinCounters.get(comboKey);
|
|
const index = counter % normalized.length;
|
|
roundRobinCounters.set(comboKey, counter + 1);
|
|
return { model: normalized[index].model, index };
|
|
}
|
|
|
|
case "random": {
|
|
// Weighted random selection
|
|
const totalWeight = normalized.reduce((sum, m) => sum + (m.weight || 1), 0);
|
|
let rand = Math.random() * totalWeight;
|
|
|
|
for (let i = 0; i < normalized.length; i++) {
|
|
rand -= normalized[i].weight || 1;
|
|
if (rand <= 0) {
|
|
return { model: normalized[i].model, index: i };
|
|
}
|
|
}
|
|
return { model: normalized[0].model, index: 0 };
|
|
}
|
|
|
|
case "least-used": {
|
|
const usageCounts = context.modelUsageCounts || {};
|
|
let minUsage = Infinity;
|
|
let minIndex = 0;
|
|
|
|
for (let i = 0; i < normalized.length; i++) {
|
|
const usage = usageCounts[normalized[i].model] || 0;
|
|
if (usage < minUsage) {
|
|
minUsage = usage;
|
|
minIndex = i;
|
|
}
|
|
}
|
|
|
|
return { model: normalized[minIndex].model, index: minIndex };
|
|
}
|
|
|
|
default:
|
|
return { model: normalized[0].model, index: 0 };
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get the fallback models for a combo (all models except the primary).
|
|
*
|
|
* @param {Combo} combo
|
|
* @param {number} primaryIndex - Index of the primary model
|
|
* @returns {string[]} Remaining models in order
|
|
*/
|
|
export function getComboFallbacks(combo, primaryIndex) {
|
|
const models = (combo.models || [])
|
|
.map((entry) => getComboStepTarget(entry))
|
|
.filter((entry): entry is string => !!entry);
|
|
return [...models.slice(primaryIndex + 1), ...models.slice(0, primaryIndex)];
|
|
}
|