mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-03 05:45:04 +03:00
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
100 lines
3.1 KiB
JavaScript
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)];
|
|
}
|