Add separate model selectors for Grok Build main, general-purpose, explore, and plan agents. Each override gets an independent 9Router custom-model slot and context_window derived from 9Router model capabilities. Preserve and restore pre-existing config on reset.
248 lines
8.3 KiB
JavaScript
248 lines
8.3 KiB
JavaScript
export const GROK_MAIN_MODEL_SLOT = "9router";
|
|
export const GROK_BUILTIN_DEFAULT = "grok-build";
|
|
export const GROK_SUBAGENT_TYPES = ["general-purpose", "explore", "plan"];
|
|
|
|
const UNSET_SENTINEL = "__9router_unset__";
|
|
const MODELS_SECTION = "models";
|
|
const SUBAGENT_MODELS_SECTION = "subagents.models";
|
|
|
|
const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
const tomlString = (value) => JSON.stringify(String(value));
|
|
|
|
const sectionRegExp = (section) =>
|
|
new RegExp(
|
|
`^\\[${escapeRegExp(section)}\\][ \\t]*\\r?\\n((?:(?!\\[)[^\\r\\n]*\\r?\\n?)*)`,
|
|
"m",
|
|
);
|
|
|
|
const modelSlot = (type) => `${GROK_MAIN_MODEL_SLOT}-${type}`;
|
|
|
|
const previousDefaultRegExp = /^# 9router-prev-default = "([^"]*)"[ \t]*\r?\n?/m;
|
|
const previousSubagentRegExp = (type) =>
|
|
new RegExp(
|
|
`^# 9router-prev-subagent-${escapeRegExp(type)} = "([^"]*)"[ \\t]*\\r?\\n?`,
|
|
"m",
|
|
);
|
|
|
|
function getSectionField(toml, section, key) {
|
|
const match = toml.match(sectionRegExp(section));
|
|
if (!match) return null;
|
|
const field = match[1].match(
|
|
new RegExp(`^[ \\t]*${escapeRegExp(key)}[ \\t]*=[ \\t]*"([^"]*)"`, "m"),
|
|
);
|
|
return field ? field[1] : null;
|
|
}
|
|
|
|
function getSectionNumber(toml, section, key) {
|
|
const match = toml.match(sectionRegExp(section));
|
|
if (!match) return null;
|
|
const field = match[1].match(
|
|
new RegExp(`^[ \\t]*${escapeRegExp(key)}[ \\t]*=[ \\t]*([0-9]+(?:\\.[0-9]+)?)`, "m"),
|
|
);
|
|
if (!field) return null;
|
|
const value = Number(field[1]);
|
|
return Number.isFinite(value) ? value : null;
|
|
}
|
|
|
|
function setSectionField(toml, section, key, value) {
|
|
const match = toml.match(sectionRegExp(section));
|
|
const line = `${key} = ${tomlString(value)}`;
|
|
if (!match) {
|
|
const prefix = toml.length > 0 && !toml.endsWith("\n") ? `${toml}\n` : toml;
|
|
return `${prefix}\n[${section}]\n${line}\n`;
|
|
}
|
|
|
|
const body = match[1] || "";
|
|
const fieldRegExp = new RegExp(
|
|
`^[ \\t]*${escapeRegExp(key)}[ \\t]*=[ \\t]*"[^"]*"`,
|
|
"m",
|
|
);
|
|
const nextBody = fieldRegExp.test(body)
|
|
? body.replace(fieldRegExp, line)
|
|
: `${line}\n${body}`;
|
|
return toml.replace(match[0], `[${section}]\n${nextBody}`);
|
|
}
|
|
|
|
function deleteSectionField(toml, section, key) {
|
|
const match = toml.match(sectionRegExp(section));
|
|
if (!match) return toml;
|
|
const fieldRegExp = new RegExp(
|
|
`^[ \\t]*${escapeRegExp(key)}[ \\t]*=[^\\r\\n]*\\r?\\n?`,
|
|
"m",
|
|
);
|
|
const nextBody = (match[1] || "").replace(fieldRegExp, "");
|
|
if (!nextBody.trim()) return toml.replace(match[0], "").replace(/\n{3,}/g, "\n\n");
|
|
return toml.replace(match[0], `[${section}]\n${nextBody}`);
|
|
}
|
|
|
|
function parseModelSection(toml, slot) {
|
|
const match = toml.match(sectionRegExp(`model.${slot}`));
|
|
if (!match) return null;
|
|
const body = match[1] || "";
|
|
const contextWindow = getSectionNumber(toml, `model.${slot}`, "context_window");
|
|
return {
|
|
model: getSectionField(toml, `model.${slot}`, "model"),
|
|
base_url: getSectionField(toml, `model.${slot}`, "base_url"),
|
|
name: getSectionField(toml, `model.${slot}`, "name"),
|
|
api_key: getSectionField(toml, `model.${slot}`, "api_key"),
|
|
api_backend: getSectionField(toml, `model.${slot}`, "api_backend"),
|
|
context_window: Number.isFinite(contextWindow) && contextWindow > 0 ? contextWindow : null,
|
|
raw: body,
|
|
};
|
|
}
|
|
|
|
function buildModelSection({ slot, model, baseUrl, apiKey, contextWindow, name }) {
|
|
const lines = [
|
|
`[model.${slot}]`,
|
|
`model = ${tomlString(model)}`,
|
|
`base_url = ${tomlString(baseUrl)}`,
|
|
`name = ${tomlString(name)}`,
|
|
`description = ${tomlString("Routed via 9Router gateway")}`,
|
|
`api_backend = "chat_completions"`,
|
|
];
|
|
if (apiKey) lines.push(`api_key = ${tomlString(apiKey)}`);
|
|
if (Number.isFinite(contextWindow) && contextWindow > 0) {
|
|
lines.push(`context_window = ${Math.floor(contextWindow)}`);
|
|
}
|
|
return `${lines.join("\n")}\n`;
|
|
}
|
|
|
|
function upsertModelSection(toml, config) {
|
|
const regexp = sectionRegExp(`model.${config.slot}`);
|
|
const section = buildModelSection(config);
|
|
if (regexp.test(toml)) return toml.replace(regexp, section);
|
|
const prefix = toml.length > 0 && !toml.endsWith("\n") ? `${toml}\n` : toml;
|
|
return `${prefix}\n${section}`;
|
|
}
|
|
|
|
function removeModelSection(toml, slot) {
|
|
return toml.replace(sectionRegExp(`model.${slot}`), "").replace(/\n{3,}/g, "\n\n");
|
|
}
|
|
|
|
function insertMarker(toml, marker) {
|
|
const mainSection = sectionRegExp(`model.${GROK_MAIN_MODEL_SLOT}`);
|
|
if (mainSection.test(toml)) {
|
|
return toml.replace(mainSection, (section) => `${marker}${section}`);
|
|
}
|
|
const prefix = toml.length > 0 && !toml.endsWith("\n") ? `${toml}\n` : toml;
|
|
return `${prefix}${marker}`;
|
|
}
|
|
|
|
function rememberPreviousDefault(toml) {
|
|
if (previousDefaultRegExp.test(toml)) return toml;
|
|
const current = getSectionField(toml, MODELS_SECTION, "default");
|
|
if (!current || current === GROK_MAIN_MODEL_SLOT) return toml;
|
|
return insertMarker(toml, `# 9router-prev-default = ${tomlString(current)}\n`);
|
|
}
|
|
|
|
function restorePreviousDefault(toml) {
|
|
const previous = toml.match(previousDefaultRegExp)?.[1] || GROK_BUILTIN_DEFAULT;
|
|
let next = toml.replace(previousDefaultRegExp, "");
|
|
if (getSectionField(next, MODELS_SECTION, "default") === GROK_MAIN_MODEL_SLOT) {
|
|
next = setSectionField(next, MODELS_SECTION, "default", previous);
|
|
}
|
|
return next;
|
|
}
|
|
|
|
function rememberPreviousSubagent(toml, type) {
|
|
const regexp = previousSubagentRegExp(type);
|
|
if (regexp.test(toml)) return toml;
|
|
const current = getSectionField(toml, SUBAGENT_MODELS_SECTION, type);
|
|
const previous = current == null ? UNSET_SENTINEL : current;
|
|
return insertMarker(
|
|
toml,
|
|
`# 9router-prev-subagent-${type} = ${tomlString(previous)}\n`,
|
|
);
|
|
}
|
|
|
|
function restorePreviousSubagent(toml, type) {
|
|
const regexp = previousSubagentRegExp(type);
|
|
const previous = toml.match(regexp)?.[1] || UNSET_SENTINEL;
|
|
let next = toml.replace(regexp, "");
|
|
if (getSectionField(next, SUBAGENT_MODELS_SECTION, type) !== modelSlot(type)) {
|
|
return next;
|
|
}
|
|
if (previous === UNSET_SENTINEL) {
|
|
return deleteSectionField(next, SUBAGENT_MODELS_SECTION, type);
|
|
}
|
|
return setSectionField(next, SUBAGENT_MODELS_SECTION, type, previous);
|
|
}
|
|
|
|
export function parseGrokBuildConfig(toml) {
|
|
const subagentModels = {};
|
|
const subagentMappings = {};
|
|
for (const type of GROK_SUBAGENT_TYPES) {
|
|
const mapping = getSectionField(toml, SUBAGENT_MODELS_SECTION, type);
|
|
subagentMappings[type] = mapping;
|
|
subagentModels[type] = mapping === modelSlot(type)
|
|
? parseModelSection(toml, mapping)
|
|
: null;
|
|
}
|
|
|
|
return {
|
|
model: parseModelSection(toml, GROK_MAIN_MODEL_SLOT),
|
|
default: getSectionField(toml, MODELS_SECTION, "default"),
|
|
subagentModels,
|
|
subagentMappings,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Apply main model and optional per-type subagent overrides while preserving all unrelated TOML.
|
|
* `subagentModels === undefined` leaves existing subagent config untouched for API compatibility.
|
|
*/
|
|
export function applyGrokBuildConfig(
|
|
toml,
|
|
{ baseUrl, apiKey, model, contextWindow, subagentModels },
|
|
) {
|
|
let next = rememberPreviousDefault(toml);
|
|
next = upsertModelSection(next, {
|
|
slot: GROK_MAIN_MODEL_SLOT,
|
|
model,
|
|
baseUrl,
|
|
apiKey,
|
|
contextWindow,
|
|
name: "9Router",
|
|
});
|
|
next = setSectionField(next, MODELS_SECTION, "default", GROK_MAIN_MODEL_SLOT);
|
|
|
|
if (subagentModels && typeof subagentModels === "object") {
|
|
for (const type of GROK_SUBAGENT_TYPES) {
|
|
const selected = subagentModels[type];
|
|
const slot = modelSlot(type);
|
|
if (selected?.model) {
|
|
next = rememberPreviousSubagent(next, type);
|
|
next = upsertModelSection(next, {
|
|
slot,
|
|
model: selected.model,
|
|
baseUrl,
|
|
apiKey,
|
|
contextWindow: selected.contextWindow,
|
|
name: `9Router ${type}`,
|
|
});
|
|
next = setSectionField(next, SUBAGENT_MODELS_SECTION, type, slot);
|
|
} else {
|
|
next = restorePreviousSubagent(next, type);
|
|
next = removeModelSection(next, slot);
|
|
}
|
|
}
|
|
}
|
|
|
|
return next;
|
|
}
|
|
|
|
export function resetGrokBuildConfig(toml) {
|
|
let next = toml;
|
|
for (const type of GROK_SUBAGENT_TYPES) {
|
|
next = restorePreviousSubagent(next, type);
|
|
next = removeModelSection(next, modelSlot(type));
|
|
}
|
|
next = removeModelSection(next, GROK_MAIN_MODEL_SLOT);
|
|
next = restorePreviousDefault(next);
|
|
return next.replace(/\n{3,}/g, "\n\n");
|
|
}
|
|
|
|
export function getGrokSubagentSlot(type) {
|
|
return GROK_SUBAGENT_TYPES.includes(type) ? modelSlot(type) : null;
|
|
}
|