Spaces:
Runtime error
Runtime error
File size: 3,351 Bytes
cd8bd0a | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 | /**
* 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)];
}
|