Files
OmniRoute/src/domain/comboResolver.js
diegosouzapw 33c28f73db refactor(phase5-6): domain persistence, policy engine, OAuth extraction, proxy decoupling
Phase 5 — Foundation & Security:
- SQLite domain state persistence (5 tables, 4 modules: fallback, budget, lockout, circuit breaker)
- Write-through cache pattern for state survival across restarts
- Race condition fix in route.js (Promise-based singleton)
- Default password hardening (.env.example)
- Server init error handling improvement

Phase 6 — Architecture Refactoring:
- OAuth providers extracted into 12 individual modules (providers.js 1051→144 lines)
- Policy Engine (lockout→budget→fallback) with evaluateRequest/evaluateFirstAllowed
- Deterministic round-robin via persistent counter Map
- Telemetry window fix with proper recordedAt timestamps
- Proxy decoupled from API settings (direct import vs HTTP self-fetch)

Tests: 295 pass (22 new: domain-persistence 16, policy-engine 6)
Docs: CHANGELOG, README, ARCHITECTURE.md updated
2026-02-15 08:12:12 -03:00

100 lines
3.1 KiB
JavaScript

// @ts-check
/**
* 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
*/
/** @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, context = {}) {
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((m) => (typeof m === "string" ? { model: m, weight: 1 } : m));
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((m) => (typeof m === "string" ? m : m.model));
return [...models.slice(primaryIndex + 1), ...models.slice(0, primaryIndex)];
}