mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-31 20:32:20 +03:00
227 lines
9.6 KiB
JavaScript
227 lines
9.6 KiB
JavaScript
/**
|
|
* omniroute setup-codex — Remote-aware Codex CLI profile generator.
|
|
*
|
|
* Connects to a running OmniRoute instance (local or remote VPS), fetches the
|
|
* live model catalog via GET /v1/models, then generates ~/.codex/<name>.config.toml
|
|
* profile files for each model — so you can switch providers with a single flag
|
|
* (`codex --profile glm52`) without editing config files by hand.
|
|
*
|
|
* Primary use-case: configure a local Codex CLI to use models from a VPS.
|
|
* omniroute setup-codex --remote http://100.67.86.91:20128 --api-key sk-xxx
|
|
*
|
|
* The command is idempotent: re-running updates existing profile files in place.
|
|
*/
|
|
|
|
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
import { join } from "node:path";
|
|
import os from "node:os";
|
|
import { printHeading, printInfo, printSuccess, printError } from "../io.mjs";
|
|
import { t } from "../i18n.mjs";
|
|
|
|
// ── Model categorisation ──────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Map a model ID (as returned by /v1/models) to a Codex profile configuration.
|
|
* Returns null for models that should not get their own profile (e.g. aliases).
|
|
*
|
|
* Exported so the Claude Code profile generator (`setup-claude`) reuses the SAME
|
|
* profile names (glm52, kimi-k27, …) for cross-CLI consistency.
|
|
*
|
|
* @param {string} modelId
|
|
* @returns {{ name:string, ctx:number, compact:number, effort?:string, summary?:boolean, toolLimit:number }|null}
|
|
*/
|
|
export function categoriseModel(modelId) {
|
|
const id = modelId.toLowerCase();
|
|
|
|
// ── Thinking models (max effort, detailed summary) ────────────────────────
|
|
const thinkingPatterns = [
|
|
{ re: /kmc\/kimi-k2\.7/, name: "kimi-k27", ctx: 131072, compact: 112000, toolLimit: 32768 },
|
|
{ re: /kmc\/kimi-k2\.6/, name: "kimi-k26", ctx: 131072, compact: 112000, toolLimit: 32768 },
|
|
{ re: /glm\/glm-5\.2-max/, name: "glm52max", ctx: 131072, compact: 112000, toolLimit: 32768 },
|
|
{ re: /glm\/glm-5\.2$/, name: "glm52", ctx: 131072, compact: 112000, toolLimit: 32768 },
|
|
{ re: /opencode-go\/mimo-v2\.5-pro/, name: "mimo-pro", ctx: 131072, compact: 112000, toolLimit: 32768 },
|
|
{ re: /opencode-go\/qwen3\.7-plus/, name: "qwen37plus", ctx: 32768, compact: 28000, toolLimit: 16384 },
|
|
];
|
|
|
|
// ── Good models (high effort) ─────────────────────────────────────────────
|
|
const goodPatterns = [
|
|
{ re: /ollamacloud\/deepseek-v4-pro/, name: "deepseek-pro", ctx: 131072, compact: 112000, toolLimit: 32768 },
|
|
{ re: /opencode-go\/mimo-v2\.5$/, name: "mimo", ctx: 131072, compact: 112000, toolLimit: 32768 },
|
|
];
|
|
|
|
// ── Simple models (no effort) ─────────────────────────────────────────────
|
|
const simplePatterns = [
|
|
{ re: /ollamacloud\/gemma4:31b/, name: "gemma4", ctx: 32768, compact: 28000, toolLimit: 16384 },
|
|
{ re: /ollamacloud\/nemotron-3-super/, name: "nemotron", ctx: 32768, compact: 28000, toolLimit: 16384 },
|
|
{ re: /ollamacloud\/gpt-oss:20b/, name: "gptoss", ctx: 32768, compact: 28000, toolLimit: 16384 },
|
|
];
|
|
|
|
// ── Fast models (low effort) ──────────────────────────────────────────────
|
|
const fastPatterns = [
|
|
{ re: /ollamacloud\/deepseek-v4-flash/, name: "deepseek-flash", ctx: 65536, compact: 56000, toolLimit: 16384 },
|
|
{ re: /ollamacloud\/gemini-3-flash/, name: "gemini-flash", ctx: 1000000, compact: 850000, toolLimit: 32768 },
|
|
{ re: /glm\/glm-5-turbo/, name: "glm5turbo", ctx: 131072, compact: 112000, toolLimit: 16384 },
|
|
{ re: /glm\/glm-4\.7-flash/, name: "glm47flash", ctx: 131072, compact: 112000, toolLimit: 16384 },
|
|
];
|
|
|
|
for (const p of thinkingPatterns) {
|
|
if (p.re.test(id)) return { ...p, effort: "xhigh", summary: true };
|
|
}
|
|
for (const p of goodPatterns) {
|
|
if (p.re.test(id)) return { ...p, effort: "high", summary: false };
|
|
}
|
|
for (const p of simplePatterns) {
|
|
if (p.re.test(id)) return { ...p, effort: undefined, summary: false };
|
|
}
|
|
for (const p of fastPatterns) {
|
|
if (p.re.test(id)) return { ...p, effort: "low", summary: false };
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/** Build the TOML content for a single profile. */
|
|
function buildProfileToml(modelId, cfg) {
|
|
const lines = [
|
|
`# codex --profile ${cfg.name}`,
|
|
`# ${modelId}`,
|
|
`model = "${modelId}"`,
|
|
`model_provider = "omniroute"`,
|
|
];
|
|
|
|
if (cfg.effort) {
|
|
lines.push(`model_reasoning_effort = "${cfg.effort}"`);
|
|
}
|
|
if (cfg.summary) {
|
|
lines.push(`model_reasoning_summary = "detailed"`);
|
|
}
|
|
|
|
lines.push(
|
|
`model_context_window = ${cfg.ctx}`,
|
|
`model_auto_compact_token_limit = ${cfg.compact}`,
|
|
`tool_output_token_limit = ${cfg.toolLimit}`
|
|
);
|
|
|
|
return lines.join("\n") + "\n";
|
|
}
|
|
|
|
// ── Command ───────────────────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* @param {{remote?:string, port?:string, apiKey?:string, codexHome?:string, dryRun?:boolean, only?:string}} opts
|
|
* @returns {Promise<number>}
|
|
*/
|
|
export async function runSetupCodexCommand(opts = {}) {
|
|
const port = Number(opts.port ?? process.env.PORT ?? 20128) || 20128;
|
|
const baseUrl = (opts.remote ?? `http://localhost:${port}`).replace(/\/v1$/, "");
|
|
const apiKey = opts.apiKey ?? opts["api-key"] ?? process.env.OMNIROUTE_API_KEY ?? "";
|
|
const codexHome = opts.codexHome ?? opts["codex-home"] ?? join(os.homedir(), ".codex");
|
|
const dryRun = Boolean(opts.dryRun ?? opts["dry-run"]);
|
|
const onlyFilter = opts.only ? opts.only.split(",").map((s) => s.trim()) : null;
|
|
|
|
printHeading(`OmniRoute → Codex CLI profile generator`);
|
|
printInfo(`Connecting to ${baseUrl} …`);
|
|
|
|
// ── Fetch model catalog ───────────────────────────────────────────────────
|
|
let models;
|
|
try {
|
|
const headers = { "Content-Type": "application/json" };
|
|
if (apiKey) headers["Authorization"] = `Bearer ${apiKey}`;
|
|
|
|
const res = await fetch(`${baseUrl}/v1/models`, {
|
|
headers,
|
|
signal: AbortSignal.timeout(10000),
|
|
});
|
|
if (!res.ok) throw new Error(`HTTP ${res.status} ${res.statusText}`);
|
|
const body = await res.json();
|
|
models = body.data ?? body.models ?? [];
|
|
} catch (err) {
|
|
printError(`Failed to fetch models: ${err.message}`);
|
|
printInfo(
|
|
"Make sure OmniRoute is running and the --remote URL is correct.\n" +
|
|
"You may also need --api-key if OmniRoute requires authentication."
|
|
);
|
|
return 1;
|
|
}
|
|
|
|
printInfo(`Received ${models.length} models from ${baseUrl}`);
|
|
|
|
// ── Ensure codex home exists ──────────────────────────────────────────────
|
|
if (!dryRun && !existsSync(codexHome)) {
|
|
mkdirSync(codexHome, { recursive: true });
|
|
}
|
|
|
|
// ── Generate profiles ─────────────────────────────────────────────────────
|
|
let written = 0;
|
|
let skipped = 0;
|
|
|
|
for (const m of models) {
|
|
const id = typeof m === "string" ? m : (m.id ?? "");
|
|
if (!id) continue;
|
|
if (onlyFilter && !onlyFilter.some((f) => id.includes(f))) continue;
|
|
|
|
const cfg = categoriseModel(id);
|
|
if (!cfg) continue;
|
|
|
|
const filePath = join(codexHome, `${cfg.name}.config.toml`);
|
|
const content = buildProfileToml(id, cfg);
|
|
|
|
if (dryRun) {
|
|
console.log(`\n── [dry-run] ${filePath} ──`);
|
|
console.log(content);
|
|
} else {
|
|
writeFileSync(filePath, content, "utf8");
|
|
printSuccess(` ✓ ${cfg.name}.config.toml (${id})`);
|
|
}
|
|
written++;
|
|
}
|
|
|
|
skipped = models.length - written;
|
|
|
|
if (!dryRun) {
|
|
console.log("");
|
|
printSuccess(`${written} profiles written to ${codexHome}`);
|
|
if (skipped > 0) {
|
|
printInfo(`${skipped} models skipped (no matching profile pattern)`);
|
|
}
|
|
console.log("\nTo use a profile:");
|
|
console.log(" codex --profile <name> # e.g. codex --profile glm52");
|
|
console.log(" codex -p <name> # short form");
|
|
} else {
|
|
console.log(`\n[dry-run] ${written} profiles would be written (${skipped} skipped)`);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
export function registerSetupCodex(program) {
|
|
program
|
|
.command("setup-codex")
|
|
.description(
|
|
"Fetch the live model catalog from OmniRoute (local or remote VPS) and generate " +
|
|
"~/.codex/<name>.config.toml profiles for each supported model"
|
|
)
|
|
.option("--port <port>", "Local OmniRoute port (ignored when --remote is set)", "20128")
|
|
.option(
|
|
"--remote <url>",
|
|
"Remote OmniRoute URL, e.g. http://100.67.86.91:20128 — fetches models from there"
|
|
)
|
|
.option(
|
|
"--api-key <key>",
|
|
"OmniRoute API key for the remote instance (defaults to OMNIROUTE_API_KEY env var)"
|
|
)
|
|
.option(
|
|
"--codex-home <dir>",
|
|
"Directory where profile files are written (default: ~/.codex)"
|
|
)
|
|
.option(
|
|
"--only <patterns>",
|
|
"Comma-separated substrings — only generate profiles for matching model IDs (e.g. glm,kimi)"
|
|
)
|
|
.option("--dry-run", "Print what would be written without touching the filesystem")
|
|
.action(async (opts) => {
|
|
const exitCode = await runSetupCodexCommand(opts);
|
|
if (exitCode !== 0) process.exit(exitCode);
|
|
});
|
|
}
|