- 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.
122 lines
3.7 KiB
JavaScript
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),
|
|
}));
|
|
}
|