Spaces:
Runtime error
Runtime error
| /** | |
| * 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)]; | |
| } | |