Spaces:
Runtime error
Runtime error
| import os from "node:os"; | |
| import path from "node:path"; | |
| import { existsSync, mkdirSync, writeFileSync, copyFileSync } from "node:fs"; | |
| import { apiFetch } from "../api.mjs"; | |
| import { createPrompt, printSuccess, printError, printInfo, printHeading } from "../io.mjs"; | |
| import { t } from "../i18n.mjs"; | |
| /** | |
| * `omniroute configure <cli>` — interactive provider+model picker that writes a | |
| * local CLI config pointed at the ACTIVE OmniRoute context (local or remote). | |
| * | |
| * The model catalog comes from the active context's GET /v1/models, so when you | |
| * are in remote mode (`omniroute connect ...`) you pick from the remote server's | |
| * live models and the profile is written on THIS machine. | |
| * | |
| * v1 targets the Codex CLI (writes ~/.codex/<name>.config.toml). The credential | |
| * is referenced by env var (OMNIROUTE_API_KEY) — never written to disk. | |
| */ | |
| const SUPPORTED = ["codex"]; | |
| /** Derive a short, filesystem-safe profile name from a model id. */ | |
| export function profileNameFromModel(modelId) { | |
| const afterProvider = String(modelId).includes("/") | |
| ? String(modelId).split("/").slice(1).join("/") | |
| : String(modelId); | |
| return afterProvider.replace(/[^a-zA-Z0-9]+/g, "").toLowerCase() || "model"; | |
| } | |
| /** Provider id for a catalog entry: explicit owned_by, else the id prefix. */ | |
| function providerOf(entry) { | |
| if (entry && typeof entry.owned_by === "string" && entry.owned_by) return entry.owned_by; | |
| const id = typeof entry === "string" ? entry : entry?.id || ""; | |
| return id.includes("/") ? id.split("/")[0] : "(none)"; | |
| } | |
| function contextWindowOf(entry) { | |
| for (const c of [entry?.context_length, entry?.max_context_window_tokens]) { | |
| if (typeof c === "number" && Number.isFinite(c) && c > 0) return c; | |
| } | |
| return null; | |
| } | |
| async function fetchModels(globalOpts) { | |
| const res = await apiFetch("/v1/models", { ...globalOpts, acceptNotOk: true }); | |
| if (!res.ok) { | |
| let msg = `HTTP ${res.status}`; | |
| try { | |
| const b = await res.json(); | |
| msg = b?.error?.message || b?.error || msg; | |
| } catch { | |
| /* ignore */ | |
| } | |
| throw new Error(`Could not fetch models: ${msg}`); | |
| } | |
| const body = await res.json(); | |
| const list = Array.isArray(body) ? body : body.data || body.models || []; | |
| return list.filter((m) => (typeof m === "string" ? m : m?.id)); | |
| } | |
| function buildCodexProfile(modelId, ctx) { | |
| const lines = [ | |
| `# codex --profile ${profileNameFromModel(modelId)}`, | |
| `# ${modelId} — generated by 'omniroute configure codex'`, | |
| `model = "${modelId}"`, | |
| `model_provider = "omniroute"`, | |
| ]; | |
| if (ctx && ctx > 0) { | |
| const compact = Math.floor(ctx * 0.85); | |
| lines.push(`model_context_window = ${ctx}`); | |
| lines.push(`model_auto_compact_token_limit = ${compact}`); | |
| } | |
| return lines.join("\n") + "\n"; | |
| } | |
| async function configureCodex(modelId, ctxWindow, opts) { | |
| const codexHome = opts.codexHome || path.join(os.homedir(), ".codex"); | |
| if (!existsSync(codexHome)) mkdirSync(codexHome, { recursive: true }); | |
| const profile = opts.name || profileNameFromModel(modelId); | |
| const filePath = path.join(codexHome, `${profile}.config.toml`); | |
| if (existsSync(filePath)) { | |
| copyFileSync(filePath, `${filePath}.bak`); | |
| } | |
| writeFileSync(filePath, buildCodexProfile(modelId, ctxWindow), "utf8"); | |
| printSuccess(`Wrote ${filePath}`); | |
| printInfo(`Use it: codex --profile ${profile}`); | |
| printInfo("Prereq: ~/.codex/config.toml must define the [model_providers.omniroute] block"); | |
| printInfo(" (run the Codex setup once — see docs/guides/CODEX-CLI-CONFIGURATION.md)."); | |
| } | |
| export async function runConfigureCommand(cli, opts = {}, cmd) { | |
| const target = String(cli || "").toLowerCase(); | |
| if (!SUPPORTED.includes(target)) { | |
| printError(`Unsupported CLI '${cli}'. Supported: ${SUPPORTED.join(", ")}.`); | |
| return 2; | |
| } | |
| const globalOpts = cmd ? cmd.optsWithGlobals() : {}; | |
| let models; | |
| try { | |
| models = await fetchModels(globalOpts); | |
| } catch (e) { | |
| printError(e instanceof Error ? e.message : String(e)); | |
| return 1; | |
| } | |
| if (!models.length) { | |
| printError("The server returned no models."); | |
| return 1; | |
| } | |
| // Resolve model: explicit flags or interactive pick. | |
| let chosenId = opts.model; | |
| if (chosenId && opts.provider && !chosenId.includes("/")) { | |
| chosenId = `${opts.provider}/${chosenId}`; | |
| } | |
| if (!chosenId) { | |
| const ids = models.map((m) => (typeof m === "string" ? m : m.id)); | |
| const providers = [...new Set(models.map(providerOf))].sort(); | |
| const prompt = createPrompt(); | |
| try { | |
| printHeading("Configure Codex CLI"); | |
| let providerList = providers; | |
| if (opts.provider) { | |
| providerList = providers.filter((p) => p === opts.provider); | |
| } else { | |
| printInfo(`Providers: ${providers.join(", ")}`); | |
| const p = await prompt.ask("Provider"); | |
| if (p) providerList = providers.filter((x) => x === p); | |
| } | |
| const inProvider = ids.filter((id) => providerList.includes(providerOf(byId(models, id)))); | |
| const candidates = inProvider.length ? inProvider : ids; | |
| printInfo(`Models: ${candidates.slice(0, 40).join(", ")}${candidates.length > 40 ? " …" : ""}`); | |
| chosenId = await prompt.ask("Model id"); | |
| } finally { | |
| prompt.close(); | |
| } | |
| } | |
| if (!chosenId) { | |
| printError("No model selected."); | |
| return 2; | |
| } | |
| const entry = byId(models, chosenId); | |
| if (!entry) { | |
| printError(`Model '${chosenId}' is not in the catalog.`); | |
| return 2; | |
| } | |
| const ctxWindow = contextWindowOf(entry); | |
| if (target === "codex") { | |
| await configureCodex(chosenId, ctxWindow, opts); | |
| } | |
| return 0; | |
| } | |
| function byId(models, id) { | |
| for (const m of models) { | |
| const mid = typeof m === "string" ? m : m.id; | |
| if (mid === id) return m; | |
| } | |
| return null; | |
| } | |
| export function registerConfigure(program) { | |
| program | |
| .command("configure <cli>") | |
| .description( | |
| t("configure.description") || | |
| "Pick a provider+model from the active server and write a local CLI config (v1: codex)" | |
| ) | |
| .option("--provider <id>", "Provider id (skips the interactive provider prompt)") | |
| .option("--model <id>", "Model id (skips the interactive model prompt)") | |
| .option("--name <name>", "Profile name to write (default: derived from model)") | |
| .option("--codex-home <dir>", "Codex home dir (default: ~/.codex)") | |
| .action(async (cli, opts, cmd) => { | |
| const code = await runConfigureCommand(cli, opts, cmd); | |
| if (code !== 0) process.exit(code); | |
| }); | |
| } | |