Files
9router/src/lib/comboPresets.js
T
Amir Seify 253199f16f feat(combos): Cursor/Claude Default presets + bulk select/delete/strategy
- Add Cursor Default / Claude Default on Dashboard -> Combos to generate
  unprefixed combo names that match Cursor/Claude client model IDs,
  seeded with cu/... or cc/... so those clients can route through 9Router.
- Add multi-select bulk Delete and bulk Set strategy (Fallback / Round Robin / Fusion).
- Docs and unit tests for preset builder.
2026-09-21 19:51:26 +07:00

122 lines
3.7 KiB
JavaScript

/**
* Build Cursor / Claude default combo presets.
* Combo names match client-native model IDs (no provider prefix);
* each is seeded with the matching prefixed 9router model so routing works.
*/
import { getProviderModels } from "open-sse/config/providerModels.js";
import { CLI_TOOLS } from "@/shared/constants/cliTools";
export const VALID_COMBO_NAME_REGEX = /^[a-zA-Z0-9_.\-]+$/;
export const PRESET_SOURCES = new Set(["cursor", "claude"]);
const CURSOR_ALIAS = "cu";
const CLAUDE_ALIAS = "cc";
/** Extra Claude Code aliases not listed in defaultModels. */
const CLAUDE_EXTRA_ALIAS_TARGETS = {
default: "cc/claude-sonnet-5",
opusplan: "cc/claude-opus-5",
};
/**
* @param {string} name
* @returns {boolean}
*/
export function isValidComboPresetName(name) {
return typeof name === "string" && name.length > 0 && VALID_COMBO_NAME_REGEX.test(name);
}
/**
* @param {string} name
* @param {string[]} models
* @param {Set<string>} [seen]
* @returns {{ name: string, models: string[] }|null}
*/
function pushItem(name, models, seen) {
if (!isValidComboPresetName(name)) return null;
if (seen?.has(name)) return null;
if (!Array.isArray(models) || models.length === 0) return null;
seen?.add(name);
return { name, models };
}
/**
* @param {Array<{id?: string}|string>} modelList
* @param {string} providerAlias
* @param {Set<string>} seen
* @returns {{ name: string, models: string[] }[]}
*/
function itemsFromProviderModels(modelList, providerAlias, seen) {
const out = [];
for (const entry of modelList || []) {
const id = typeof entry === "string" ? entry : entry?.id;
if (!id) continue;
const item = pushItem(id, [`${providerAlias}/${id}`], seen);
if (item) out.push(item);
}
return out;
}
/**
* Build Cursor default presets.
* Prefer live catalog when provided; otherwise static cu registry.
* @param {{ liveModels?: Array<{id: string}|string> }} [opts]
* @returns {{ name: string, models: string[] }[]}
*/
export function buildCursorPresetItems(opts = {}) {
const seen = new Set();
const live = Array.isArray(opts.liveModels) ? opts.liveModels : null;
if (live?.length) {
return itemsFromProviderModels(live, CURSOR_ALIAS, seen);
}
return itemsFromProviderModels(getProviderModels(CURSOR_ALIAS), CURSOR_ALIAS, seen);
}
/**
* Build Claude default presets from cc registry + Claude Code aliases.
* @returns {{ name: string, models: string[] }[]}
*/
export function buildClaudePresetItems() {
const seen = new Set();
const out = itemsFromProviderModels(getProviderModels(CLAUDE_ALIAS), CLAUDE_ALIAS, seen);
const claudeTool = CLI_TOOLS.claude || {};
for (const entry of claudeTool.defaultModels || []) {
const name = entry.alias || entry.id;
const target = entry.defaultValue;
if (!name || !target) continue;
const item = pushItem(name, [target], seen);
if (item) out.push(item);
}
for (const alias of claudeTool.modelAliases || []) {
if (seen.has(alias)) continue;
const target = CLAUDE_EXTRA_ALIAS_TARGETS[alias];
if (!target) continue;
const item = pushItem(alias, [target], seen);
if (item) out.push(item);
}
return out;
}
/**
* @param {"cursor"|"claude"} source
* @param {{ liveModels?: Array<{id: string}|string>, existingNames?: Iterable<string> }} [opts]
* @returns {{ name: string, models: string[], exists: boolean }[]}
*/
export function buildPresetItems(source, opts = {}) {
if (!PRESET_SOURCES.has(source)) return [];
const items = source === "cursor"
? buildCursorPresetItems({ liveModels: opts.liveModels })
: buildClaudePresetItems();
const existing = new Set(opts.existingNames || []);
return items.map((item) => ({
...item,
exists: existing.has(item.name),
}));
}