mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
feat(cli): fase 7 — context-eng/sessions/tags/openapi/combo-suggest/oneproxy/telemetry
- 7.1: context-eng (alias ctx) — caveman/rtk config/filters/test, analytics, combos - 7.2: sessions list/show/expire/expire-all/current - 7.3: tags list/add/remove/assign/unassign/resources - 7.4: openapi dump/validate/try/endpoints/paths (YAML sem js-yaml via toYaml inline) - 7.5: combo suggest via MCP omniroute_best_combo_for_task (extendComboSuggest) - 7.6: oneproxy status/stats/fetch/rotate/config/pool via MCP + REST - 7.7: telemetry summary/export com fmtMetric/fmtDelta 45 novos testes passando
This commit is contained in:
@@ -2,6 +2,8 @@ import { Option } from "commander";
|
||||
import { printHeading } from "../io.mjs";
|
||||
import { withRuntime } from "../runtime.mjs";
|
||||
import { t } from "../i18n.mjs";
|
||||
import { apiFetch } from "../api.mjs";
|
||||
import { emit } from "../output.mjs";
|
||||
|
||||
const VALID_STRATEGIES = [
|
||||
"priority",
|
||||
@@ -20,6 +22,79 @@ const VALID_STRATEGIES = [
|
||||
"reset-aware",
|
||||
];
|
||||
|
||||
const suggestSchema = [
|
||||
{ key: "rank", header: "#" },
|
||||
{ key: "name", header: "Combo", width: 24 },
|
||||
{ key: "strategy", header: "Strategy", width: 16 },
|
||||
{ key: "score", header: "Score", formatter: (v) => (v != null ? v.toFixed(3) : "-") },
|
||||
{ key: "latencyP50Ms", header: "Latency P50", formatter: (v) => (v != null ? `${v}ms` : "-") },
|
||||
{ key: "costPer1k", header: "Cost/1k", formatter: (v) => (v != null ? `$${v.toFixed(5)}` : "-") },
|
||||
{
|
||||
key: "rationale",
|
||||
header: "Rationale",
|
||||
width: 40,
|
||||
formatter: (v) => {
|
||||
if (!v) return "-";
|
||||
const s = String(v);
|
||||
return s.length > 40 ? s.slice(0, 39) + "…" : s;
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export function extendComboSuggest(combo) {
|
||||
combo
|
||||
.command("suggest")
|
||||
.description(t("combo.suggest.description"))
|
||||
.requiredOption("--task <description>", t("combo.suggest.task"))
|
||||
.option("--max-cost <usd>", t("combo.suggest.maxCost"), parseFloat)
|
||||
.option("--max-latency-ms <ms>", t("combo.suggest.maxLatencyMs"), parseInt)
|
||||
.option("--weights <json>", t("combo.suggest.weights"))
|
||||
.option("--top <n>", t("combo.suggest.top"), parseInt, 5)
|
||||
.option("--explain", t("combo.suggest.explain"))
|
||||
.option("--switch", t("combo.suggest.switch"))
|
||||
.action(async (opts, cmd) => {
|
||||
const body = {
|
||||
task: opts.task,
|
||||
constraints: {
|
||||
maxCostUsd: opts.maxCost,
|
||||
maxLatencyMs: opts.maxLatencyMs,
|
||||
},
|
||||
weights: opts.weights ? JSON.parse(opts.weights) : undefined,
|
||||
top: opts.top,
|
||||
};
|
||||
const res = await apiFetch("/api/mcp/tools/call", {
|
||||
method: "POST",
|
||||
body: { name: "omniroute_best_combo_for_task", arguments: body },
|
||||
});
|
||||
if (!res.ok) {
|
||||
process.stderr.write(`Error: ${res.status}\n`);
|
||||
process.exit(1);
|
||||
}
|
||||
const data = await res.json();
|
||||
const candidates = data.candidates ?? data;
|
||||
const rows = (Array.isArray(candidates) ? candidates : []).map((c, i) => ({
|
||||
rank: i + 1,
|
||||
...c,
|
||||
}));
|
||||
emit(rows, cmd.optsWithGlobals(), suggestSchema);
|
||||
if (opts.explain && !cmd.optsWithGlobals().quiet) {
|
||||
process.stderr.write(`\nRationale:\n${data.rationale ?? "(no rationale)"}\n`);
|
||||
}
|
||||
if (opts.switch && rows[0]) {
|
||||
const best = rows[0].name;
|
||||
const switchRes = await apiFetch("/api/combos/switch", {
|
||||
method: "POST",
|
||||
body: { name: best },
|
||||
});
|
||||
if (!switchRes.ok) {
|
||||
process.stderr.write(`Switch failed: ${switchRes.status}\n`);
|
||||
process.exit(1);
|
||||
}
|
||||
process.stderr.write(`\nSwitched to: ${best}\n`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function registerCombo(program) {
|
||||
const combo = program.command("combo").description(t("combo.title"));
|
||||
|
||||
@@ -68,6 +143,8 @@ export function registerCombo(program) {
|
||||
const exitCode = await runComboDeleteCommand(name, { ...opts, output: globalOpts.output });
|
||||
if (exitCode !== 0) process.exit(exitCode);
|
||||
});
|
||||
|
||||
extendComboSuggest(combo);
|
||||
}
|
||||
|
||||
export async function runComboListCommand(opts = {}) {
|
||||
|
||||
182
bin/cli/commands/context-eng.mjs
Normal file
182
bin/cli/commands/context-eng.mjs
Normal file
@@ -0,0 +1,182 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { createInterface } from "node:readline";
|
||||
import { apiFetch } from "../api.mjs";
|
||||
import { emit } from "../output.mjs";
|
||||
import { t } from "../i18n.mjs";
|
||||
|
||||
async function confirm(q) {
|
||||
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
||||
return new Promise((resolve) => {
|
||||
rl.question(`${q} [y/N] `, (a) => {
|
||||
rl.close();
|
||||
resolve(a.trim().toLowerCase() === "y");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function registerContextEng(program) {
|
||||
const ctx = program.command("context-eng").alias("ctx").description(t("context.description"));
|
||||
|
||||
ctx
|
||||
.command("analytics")
|
||||
.option("--period <p>", t("context.analytics.period"), "7d")
|
||||
.action(async (opts, cmd) => {
|
||||
const res = await apiFetch(`/api/context/analytics?period=${opts.period}`);
|
||||
if (!res.ok) {
|
||||
process.stderr.write(`Error: ${res.status}\n`);
|
||||
process.exit(1);
|
||||
}
|
||||
emit(await res.json(), cmd.optsWithGlobals());
|
||||
});
|
||||
|
||||
const caveman = ctx.command("caveman").description(t("context.caveman.description"));
|
||||
const cmCfg = caveman.command("config").description(t("context.caveman.config.description"));
|
||||
|
||||
cmCfg.command("show").action(async (opts, cmd) => {
|
||||
const res = await apiFetch("/api/context/caveman/config");
|
||||
if (!res.ok) {
|
||||
process.stderr.write(`Error: ${res.status}\n`);
|
||||
process.exit(1);
|
||||
}
|
||||
emit(await res.json(), cmd.optsWithGlobals());
|
||||
});
|
||||
|
||||
cmCfg
|
||||
.command("set")
|
||||
.option("--aggressiveness <n>", t("context.caveman.config.aggressiveness"), parseFloat)
|
||||
.option("--max-shrink-pct <n>", t("context.caveman.config.maxShrinkPct"), parseInt)
|
||||
.option("--preserve-tags <list>", t("context.caveman.config.preserveTags"), (v) => v.split(","))
|
||||
.action(async (opts, cmd) => {
|
||||
const body = {};
|
||||
if (opts.aggressiveness !== undefined) body.aggressiveness = opts.aggressiveness;
|
||||
if (opts.maxShrinkPct !== undefined) body.maxShrinkPct = opts.maxShrinkPct;
|
||||
if (opts.preserveTags) body.preserveTags = opts.preserveTags;
|
||||
const res = await apiFetch("/api/context/caveman/config", { method: "PUT", body });
|
||||
if (!res.ok) {
|
||||
process.stderr.write(`Error: ${res.status}\n`);
|
||||
process.exit(1);
|
||||
}
|
||||
emit(await res.json(), cmd.optsWithGlobals());
|
||||
});
|
||||
|
||||
const rtk = ctx.command("rtk").description(t("context.rtk.description"));
|
||||
const rtkCfg = rtk.command("config").description(t("context.rtk.config.description"));
|
||||
|
||||
rtkCfg.command("show").action(async (opts, cmd) => {
|
||||
const res = await apiFetch("/api/context/rtk/config");
|
||||
if (!res.ok) {
|
||||
process.stderr.write(`Error: ${res.status}\n`);
|
||||
process.exit(1);
|
||||
}
|
||||
emit(await res.json(), cmd.optsWithGlobals());
|
||||
});
|
||||
|
||||
rtkCfg
|
||||
.command("set")
|
||||
.option("--token-budget <n>", t("context.rtk.config.tokenBudget"), parseInt)
|
||||
.option("--reserve-pct <n>", t("context.rtk.config.reservePct"), parseInt)
|
||||
.action(async (opts, cmd) => {
|
||||
const body = {};
|
||||
if (opts.tokenBudget) body.tokenBudget = opts.tokenBudget;
|
||||
if (opts.reservePct) body.reservePct = opts.reservePct;
|
||||
const res = await apiFetch("/api/context/rtk/config", { method: "PUT", body });
|
||||
if (!res.ok) {
|
||||
process.stderr.write(`Error: ${res.status}\n`);
|
||||
process.exit(1);
|
||||
}
|
||||
emit(await res.json(), cmd.optsWithGlobals());
|
||||
});
|
||||
|
||||
const filters = rtk.command("filters").description(t("context.rtk.filters.description"));
|
||||
|
||||
filters.command("list").action(async (opts, cmd) => {
|
||||
const res = await apiFetch("/api/context/rtk/filters");
|
||||
if (!res.ok) {
|
||||
process.stderr.write(`Error: ${res.status}\n`);
|
||||
process.exit(1);
|
||||
}
|
||||
emit(await res.json(), cmd.optsWithGlobals());
|
||||
});
|
||||
|
||||
filters
|
||||
.command("add")
|
||||
.requiredOption("--pattern <p>", t("context.rtk.filters.pattern"))
|
||||
.option("--priority <n>", t("context.rtk.filters.priority"), parseInt, 100)
|
||||
.option("--action <a>", t("context.rtk.filters.action"), "drop")
|
||||
.action(async (opts, cmd) => {
|
||||
const body = { pattern: opts.pattern, priority: opts.priority, action: opts.action };
|
||||
const res = await apiFetch("/api/context/rtk/filters", { method: "POST", body });
|
||||
if (!res.ok) {
|
||||
process.stderr.write(`Error: ${res.status}\n`);
|
||||
process.exit(1);
|
||||
}
|
||||
emit(await res.json(), cmd.optsWithGlobals());
|
||||
});
|
||||
|
||||
filters
|
||||
.command("remove <id>")
|
||||
.option("--yes", t("context.rtk.filters.yes"))
|
||||
.action(async (id, opts, cmd) => {
|
||||
if (!opts.yes) {
|
||||
const ok = await confirm(`Remove filter ${id}?`);
|
||||
if (!ok) return;
|
||||
}
|
||||
const res = await apiFetch(`/api/context/rtk/filters/${id}`, { method: "DELETE" });
|
||||
if (!res.ok) {
|
||||
process.stderr.write(`Error: ${res.status}\n`);
|
||||
process.exit(1);
|
||||
}
|
||||
process.stdout.write("Removed\n");
|
||||
});
|
||||
|
||||
rtk
|
||||
.command("test")
|
||||
.requiredOption("--file <path>", t("context.rtk.test.file"))
|
||||
.action(async (opts, cmd) => {
|
||||
const body = JSON.parse(readFileSync(opts.file, "utf8"));
|
||||
const res = await apiFetch("/api/context/rtk/test", { method: "POST", body });
|
||||
if (!res.ok) {
|
||||
process.stderr.write(`Error: ${res.status}\n`);
|
||||
process.exit(1);
|
||||
}
|
||||
emit(await res.json(), cmd.optsWithGlobals());
|
||||
});
|
||||
|
||||
rtk.command("raw-output <id>").action(async (id, opts, cmd) => {
|
||||
const res = await apiFetch(`/api/context/rtk/raw-output/${id}`);
|
||||
if (!res.ok) {
|
||||
process.stderr.write(`Error: ${res.status}\n`);
|
||||
process.exit(1);
|
||||
}
|
||||
emit(await res.json(), cmd.optsWithGlobals());
|
||||
});
|
||||
|
||||
const combos = ctx.command("combos").description(t("context.combos.description"));
|
||||
|
||||
combos.command("list").action(async (opts, cmd) => {
|
||||
const res = await apiFetch("/api/context/combos");
|
||||
if (!res.ok) {
|
||||
process.stderr.write(`Error: ${res.status}\n`);
|
||||
process.exit(1);
|
||||
}
|
||||
emit(await res.json(), cmd.optsWithGlobals());
|
||||
});
|
||||
|
||||
combos.command("get <id>").action(async (id, opts, cmd) => {
|
||||
const res = await apiFetch(`/api/context/combos/${id}`);
|
||||
if (!res.ok) {
|
||||
process.stderr.write(`Error: ${res.status}\n`);
|
||||
process.exit(1);
|
||||
}
|
||||
emit(await res.json(), cmd.optsWithGlobals());
|
||||
});
|
||||
|
||||
combos.command("assignments <id>").action(async (id, opts, cmd) => {
|
||||
const res = await apiFetch(`/api/context/combos/${id}/assignments`);
|
||||
if (!res.ok) {
|
||||
process.stderr.write(`Error: ${res.status}\n`);
|
||||
process.exit(1);
|
||||
}
|
||||
emit(await res.json(), cmd.optsWithGlobals());
|
||||
});
|
||||
}
|
||||
120
bin/cli/commands/oneproxy.mjs
Normal file
120
bin/cli/commands/oneproxy.mjs
Normal file
@@ -0,0 +1,120 @@
|
||||
import { apiFetch } from "../api.mjs";
|
||||
import { emit } from "../output.mjs";
|
||||
import { t } from "../i18n.mjs";
|
||||
|
||||
function fmtTs(v) {
|
||||
if (!v) return "-";
|
||||
return new Date(typeof v === "number" ? v * 1000 : v).toLocaleString();
|
||||
}
|
||||
|
||||
async function mcpCall(name, args) {
|
||||
const res = await apiFetch("/api/mcp/tools/call", {
|
||||
method: "POST",
|
||||
body: { name, arguments: args },
|
||||
});
|
||||
if (!res.ok) {
|
||||
process.stderr.write(`MCP error: ${res.status}\n`);
|
||||
process.exit(1);
|
||||
}
|
||||
return res.json();
|
||||
}
|
||||
|
||||
const proxySchema = [
|
||||
{ key: "host", header: "Host", width: 35 },
|
||||
{ key: "type", header: "Type", width: 8 },
|
||||
{ key: "region", header: "Region" },
|
||||
{ key: "latencyMs", header: "Latency", formatter: (v) => (v ? `${v}ms` : "-") },
|
||||
{
|
||||
key: "successRate",
|
||||
header: "Success%",
|
||||
formatter: (v) => (v ? `${(v * 100).toFixed(0)}%` : "-"),
|
||||
},
|
||||
{ key: "lastUsed", header: "Last Used", formatter: fmtTs },
|
||||
{ key: "state", header: "State" },
|
||||
];
|
||||
|
||||
export function registerOneProxy(program) {
|
||||
const op = program.command("oneproxy").description(t("oneproxy.description"));
|
||||
|
||||
op.command("status").action(async (opts, cmd) => {
|
||||
const data = await mcpCall("omniroute_oneproxy_stats", {});
|
||||
emit(data, cmd.optsWithGlobals());
|
||||
});
|
||||
|
||||
op.command("stats")
|
||||
.option("--provider <p>", t("oneproxy.stats.provider"))
|
||||
.option("--period <p>", t("oneproxy.stats.period"), "24h")
|
||||
.action(async (opts, cmd) => {
|
||||
const data = await mcpCall("omniroute_oneproxy_stats", {
|
||||
provider: opts.provider,
|
||||
period: opts.period,
|
||||
});
|
||||
emit(data, cmd.optsWithGlobals());
|
||||
});
|
||||
|
||||
op.command("fetch")
|
||||
.description(t("oneproxy.fetch.description"))
|
||||
.option("--count <n>", t("oneproxy.fetch.count"), parseInt, 1)
|
||||
.option("--type <t>", t("oneproxy.fetch.type"), "http")
|
||||
.action(async (opts, cmd) => {
|
||||
const data = await mcpCall("omniroute_oneproxy_fetch", {
|
||||
count: opts.count,
|
||||
type: opts.type,
|
||||
});
|
||||
emit(data.proxies ?? data, cmd.optsWithGlobals(), proxySchema);
|
||||
});
|
||||
|
||||
op.command("rotate")
|
||||
.description(t("oneproxy.rotate.description"))
|
||||
.option("--provider <p>", t("oneproxy.rotate.provider"))
|
||||
.option("--connection-id <id>", t("oneproxy.rotate.connectionId"))
|
||||
.action(async (opts, cmd) => {
|
||||
const data = await mcpCall("omniroute_oneproxy_rotate", {
|
||||
provider: opts.provider,
|
||||
connectionId: opts.connectionId,
|
||||
});
|
||||
emit(data, cmd.optsWithGlobals());
|
||||
});
|
||||
|
||||
const config = op.command("config").description(t("oneproxy.config.description"));
|
||||
|
||||
config.command("show").action(async (opts, cmd) => {
|
||||
const res = await apiFetch("/api/settings/oneproxy");
|
||||
if (!res.ok) {
|
||||
process.stderr.write(`Error: ${res.status}\n`);
|
||||
process.exit(1);
|
||||
}
|
||||
emit(await res.json(), cmd.optsWithGlobals());
|
||||
});
|
||||
|
||||
config
|
||||
.command("set")
|
||||
.option("--enabled <b>", t("oneproxy.config.enabled"), (v) => v === "true")
|
||||
.option("--pool-size <n>", t("oneproxy.config.poolSize"), parseInt)
|
||||
.option("--provider-source <url>", t("oneproxy.config.providerSource"))
|
||||
.option("--rotation-policy <p>", t("oneproxy.config.rotationPolicy"))
|
||||
.action(async (opts, cmd) => {
|
||||
const body = {};
|
||||
for (const k of ["enabled", "poolSize", "providerSource", "rotationPolicy"]) {
|
||||
if (opts[k] !== undefined) body[k] = opts[k];
|
||||
}
|
||||
const res = await apiFetch("/api/settings/oneproxy", { method: "PUT", body });
|
||||
if (!res.ok) {
|
||||
process.stderr.write(`Error: ${res.status}\n`);
|
||||
process.exit(1);
|
||||
}
|
||||
emit(await res.json(), cmd.optsWithGlobals());
|
||||
});
|
||||
|
||||
op.command("pool")
|
||||
.description(t("oneproxy.pool.description"))
|
||||
.action(async (opts, cmd) => {
|
||||
const res = await apiFetch("/api/settings/oneproxy?include=pool");
|
||||
if (!res.ok) {
|
||||
process.stderr.write(`Error: ${res.status}\n`);
|
||||
process.exit(1);
|
||||
}
|
||||
const data = await res.json();
|
||||
emit(data.pool ?? data, cmd.optsWithGlobals(), proxySchema);
|
||||
});
|
||||
}
|
||||
168
bin/cli/commands/openapi.mjs
Normal file
168
bin/cli/commands/openapi.mjs
Normal file
@@ -0,0 +1,168 @@
|
||||
import { readFileSync, writeFileSync } from "node:fs";
|
||||
import { apiFetch } from "../api.mjs";
|
||||
import { emit } from "../output.mjs";
|
||||
import { t } from "../i18n.mjs";
|
||||
|
||||
function truncate(v, max = 40) {
|
||||
if (!v) return "-";
|
||||
const s = String(v);
|
||||
return s.length > max ? s.slice(0, max - 1) + "…" : s;
|
||||
}
|
||||
|
||||
function toYaml(obj, indent = 0) {
|
||||
const pad = " ".repeat(indent);
|
||||
if (obj === null || obj === undefined) return "null";
|
||||
if (typeof obj === "boolean") return String(obj);
|
||||
if (typeof obj === "number") return String(obj);
|
||||
if (typeof obj === "string") {
|
||||
if (/[\n:#{}[\],&*?|<>=!%@`]/.test(obj) || obj.trim() !== obj) {
|
||||
return JSON.stringify(obj);
|
||||
}
|
||||
return obj || '""';
|
||||
}
|
||||
if (Array.isArray(obj)) {
|
||||
if (obj.length === 0) return "[]";
|
||||
return obj.map((v) => `\n${pad}- ${toYaml(v, indent + 1)}`).join("");
|
||||
}
|
||||
const entries = Object.entries(obj);
|
||||
if (entries.length === 0) return "{}";
|
||||
return entries
|
||||
.map(([k, v]) => {
|
||||
const safeKey = /[^a-zA-Z0-9_-]/.test(k) ? JSON.stringify(k) : k;
|
||||
if (v !== null && typeof v === "object") {
|
||||
const nested = toYaml(v, indent + 1);
|
||||
if (Array.isArray(v) && v.length > 0) return `\n${pad}${safeKey}:${nested}`;
|
||||
if (!Array.isArray(v) && Object.keys(v).length > 0) return `\n${pad}${safeKey}:\n${nested}`;
|
||||
return `\n${pad}${safeKey}: ${nested}`;
|
||||
}
|
||||
return `\n${pad}${safeKey}: ${toYaml(v, indent + 1)}`;
|
||||
})
|
||||
.join("")
|
||||
.trimStart();
|
||||
}
|
||||
|
||||
function validateBasic(spec) {
|
||||
if (!spec || typeof spec !== "object") throw new Error("spec is not an object");
|
||||
if (!spec.openapi && !spec.swagger) throw new Error("missing openapi/swagger version field");
|
||||
if (!spec.info) throw new Error("missing info object");
|
||||
if (!spec.paths) throw new Error("missing paths object");
|
||||
}
|
||||
|
||||
const endpointSchema = [
|
||||
{ key: "method", header: "Method", width: 8 },
|
||||
{ key: "path", header: "Path", width: 45 },
|
||||
{ key: "operationId", header: "Operation ID", width: 25 },
|
||||
{ key: "summary", header: "Summary", width: 40, formatter: (v) => truncate(v, 40) },
|
||||
];
|
||||
|
||||
export function registerOpenapi(program) {
|
||||
const api = program.command("openapi").description(t("openapi.description"));
|
||||
|
||||
api
|
||||
.command("dump")
|
||||
.description(t("openapi.dump.description"))
|
||||
.option("--format <f>", t("openapi.dump.format"), "yaml")
|
||||
.option("--out <path>", t("openapi.dump.out"))
|
||||
.action(async (opts, cmd) => {
|
||||
const res = await apiFetch("/api/openapi/spec");
|
||||
if (!res.ok) {
|
||||
process.stderr.write(`Error: ${res.status}\n`);
|
||||
process.exit(1);
|
||||
}
|
||||
const data = await res.json();
|
||||
const serialized =
|
||||
opts.format === "yaml" ? toYaml(data) + "\n" : JSON.stringify(data, null, 2);
|
||||
if (opts.out) {
|
||||
writeFileSync(opts.out, serialized);
|
||||
process.stdout.write(`Saved to ${opts.out}\n`);
|
||||
} else {
|
||||
process.stdout.write(serialized);
|
||||
}
|
||||
});
|
||||
|
||||
api
|
||||
.command("validate")
|
||||
.description(t("openapi.validate.description"))
|
||||
.action(async (opts, cmd) => {
|
||||
const res = await apiFetch("/api/openapi/spec");
|
||||
if (!res.ok) {
|
||||
process.stderr.write(`Error: ${res.status}\n`);
|
||||
process.exit(1);
|
||||
}
|
||||
const spec = await res.json();
|
||||
try {
|
||||
validateBasic(spec);
|
||||
process.stdout.write("Spec is valid\n");
|
||||
} catch (err) {
|
||||
process.stderr.write(`Invalid: ${err.message}\n`);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
api
|
||||
.command("try <path>")
|
||||
.description(t("openapi.try.description"))
|
||||
.option("--method <m>", t("openapi.try.method"), "GET")
|
||||
.option("--body <file>", t("openapi.try.body"))
|
||||
.option("--query <kv>", t("openapi.try.query"), (v, prev = []) => [...prev, v.split("=")], [])
|
||||
.option("--header <kv>", t("openapi.try.header"), (v, prev = []) => [...prev, v.split("=")], [])
|
||||
.action(async (path, opts, cmd) => {
|
||||
const body = opts.body ? JSON.parse(readFileSync(opts.body, "utf8")) : undefined;
|
||||
const query = Object.fromEntries(opts.query ?? []);
|
||||
const headers = Object.fromEntries(opts.header ?? []);
|
||||
const res = await apiFetch("/api/openapi/try", {
|
||||
method: "POST",
|
||||
body: { path, method: opts.method, body, query, headers },
|
||||
});
|
||||
if (!res.ok) {
|
||||
process.stderr.write(`Error: ${res.status}\n`);
|
||||
process.exit(1);
|
||||
}
|
||||
emit(await res.json(), cmd.optsWithGlobals());
|
||||
});
|
||||
|
||||
api
|
||||
.command("endpoints")
|
||||
.description(t("openapi.endpoints.description"))
|
||||
.option("--search <q>", t("openapi.endpoints.search"))
|
||||
.action(async (opts, cmd) => {
|
||||
const res = await apiFetch("/api/openapi/spec");
|
||||
if (!res.ok) {
|
||||
process.stderr.write(`Error: ${res.status}\n`);
|
||||
process.exit(1);
|
||||
}
|
||||
const spec = await res.json();
|
||||
const rows = [];
|
||||
for (const [path, methods] of Object.entries(spec.paths ?? {})) {
|
||||
for (const [method, def] of Object.entries(methods)) {
|
||||
if (["parameters", "summary"].includes(method)) continue;
|
||||
const summary = def.summary ?? def.description ?? "";
|
||||
if (
|
||||
opts.search &&
|
||||
!path.includes(opts.search) &&
|
||||
!summary.toLowerCase().includes(opts.search.toLowerCase())
|
||||
)
|
||||
continue;
|
||||
rows.push({ method: method.toUpperCase(), path, summary, operationId: def.operationId });
|
||||
}
|
||||
}
|
||||
emit(rows, cmd.optsWithGlobals(), endpointSchema);
|
||||
});
|
||||
|
||||
api
|
||||
.command("paths")
|
||||
.description(t("openapi.paths.description"))
|
||||
.action(async (opts, cmd) => {
|
||||
const res = await apiFetch("/api/openapi/spec");
|
||||
if (!res.ok) {
|
||||
process.stderr.write(`Error: ${res.status}\n`);
|
||||
process.exit(1);
|
||||
}
|
||||
const spec = await res.json();
|
||||
const paths = Object.keys(spec.paths ?? {}).sort();
|
||||
emit(
|
||||
paths.map((p) => ({ path: p })),
|
||||
cmd.optsWithGlobals()
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -14,6 +14,12 @@ import { registerPricing } from "./pricing.mjs";
|
||||
import { registerResilience } from "./resilience.mjs";
|
||||
import { registerNodes } from "./nodes.mjs";
|
||||
import { registerSync } from "./sync.mjs";
|
||||
import { registerContextEng } from "./context-eng.mjs";
|
||||
import { registerSessions } from "./sessions.mjs";
|
||||
import { registerTags } from "./tags.mjs";
|
||||
import { registerOpenapi } from "./openapi.mjs";
|
||||
import { registerOneProxy } from "./oneproxy.mjs";
|
||||
import { registerTelemetry } from "./telemetry.mjs";
|
||||
import { registerChat } from "./chat.mjs";
|
||||
import { registerStream } from "./stream.mjs";
|
||||
import { registerSimulate } from "./simulate.mjs";
|
||||
@@ -62,6 +68,12 @@ export function registerCommands(program) {
|
||||
registerResilience(program);
|
||||
registerNodes(program);
|
||||
registerSync(program);
|
||||
registerContextEng(program);
|
||||
registerSessions(program);
|
||||
registerTags(program);
|
||||
registerOpenapi(program);
|
||||
registerOneProxy(program);
|
||||
registerTelemetry(program);
|
||||
registerChat(program);
|
||||
registerStream(program);
|
||||
registerSimulate(program);
|
||||
|
||||
108
bin/cli/commands/sessions.mjs
Normal file
108
bin/cli/commands/sessions.mjs
Normal file
@@ -0,0 +1,108 @@
|
||||
import { createInterface } from "node:readline";
|
||||
import { apiFetch } from "../api.mjs";
|
||||
import { emit } from "../output.mjs";
|
||||
import { t } from "../i18n.mjs";
|
||||
|
||||
function fmtTs(v) {
|
||||
if (!v) return "-";
|
||||
return new Date(typeof v === "number" ? v * 1000 : v).toLocaleString();
|
||||
}
|
||||
|
||||
function truncate(v, max = 30) {
|
||||
if (!v) return "-";
|
||||
const s = String(v);
|
||||
return s.length > max ? s.slice(0, max - 1) + "…" : s;
|
||||
}
|
||||
|
||||
async function confirm(q) {
|
||||
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
||||
return new Promise((resolve) => {
|
||||
rl.question(`${q} [y/N] `, (a) => {
|
||||
rl.close();
|
||||
resolve(a.trim().toLowerCase() === "y");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const sessionSchema = [
|
||||
{ key: "id", header: "Session ID", width: 28 },
|
||||
{ key: "user", header: "User", width: 22 },
|
||||
{ key: "kind", header: "Kind", width: 14 },
|
||||
{ key: "createdAt", header: "Created", formatter: fmtTs },
|
||||
{ key: "expiresAt", header: "Expires", formatter: fmtTs },
|
||||
{ key: "lastSeen", header: "Last Seen", formatter: fmtTs },
|
||||
{ key: "ip", header: "IP" },
|
||||
{ key: "userAgent", header: "User Agent", width: 30, formatter: (v) => truncate(v, 30) },
|
||||
];
|
||||
|
||||
export function registerSessions(program) {
|
||||
const s = program.command("sessions").description(t("sessions.description"));
|
||||
|
||||
s.command("list")
|
||||
.option("--user <u>", t("sessions.list.user"))
|
||||
.option("--kind <k>", t("sessions.list.kind"))
|
||||
.option("--active", t("sessions.list.active"))
|
||||
.option("--limit <n>", t("sessions.list.limit"), parseInt, 100)
|
||||
.action(async (opts, cmd) => {
|
||||
const params = new URLSearchParams({ limit: String(opts.limit) });
|
||||
if (opts.user) params.set("user", opts.user);
|
||||
if (opts.kind) params.set("kind", opts.kind);
|
||||
if (opts.active) params.set("active", "true");
|
||||
const res = await apiFetch(`/api/sessions?${params}`);
|
||||
if (!res.ok) {
|
||||
process.stderr.write(`Error: ${res.status}\n`);
|
||||
process.exit(1);
|
||||
}
|
||||
const data = await res.json();
|
||||
emit(data.items ?? data, cmd.optsWithGlobals(), sessionSchema);
|
||||
});
|
||||
|
||||
s.command("show <sessionId>").action(async (id, opts, cmd) => {
|
||||
const res = await apiFetch(`/api/sessions?id=${id}`);
|
||||
if (!res.ok) {
|
||||
process.stderr.write(`Error: ${res.status}\n`);
|
||||
process.exit(1);
|
||||
}
|
||||
emit(await res.json(), cmd.optsWithGlobals());
|
||||
});
|
||||
|
||||
s.command("expire <sessionId>")
|
||||
.option("--yes", t("sessions.expire.yes"))
|
||||
.action(async (id, opts, cmd) => {
|
||||
if (!opts.yes) {
|
||||
const ok = await confirm(`Expire session ${id}?`);
|
||||
if (!ok) return;
|
||||
}
|
||||
const res = await apiFetch(`/api/sessions?id=${id}`, { method: "DELETE" });
|
||||
if (!res.ok) {
|
||||
process.stderr.write(`Error: ${res.status}\n`);
|
||||
process.exit(1);
|
||||
}
|
||||
process.stdout.write("Expired\n");
|
||||
});
|
||||
|
||||
s.command("expire-all")
|
||||
.requiredOption("--user <u>", t("sessions.expireAll.user"))
|
||||
.option("--yes", t("sessions.expireAll.yes"))
|
||||
.action(async (opts, cmd) => {
|
||||
if (!opts.yes) {
|
||||
const ok = await confirm(`Expire ALL sessions for ${opts.user}?`);
|
||||
if (!ok) return;
|
||||
}
|
||||
const res = await apiFetch(`/api/sessions?user=${opts.user}`, { method: "DELETE" });
|
||||
if (!res.ok) {
|
||||
process.stderr.write(`Error: ${res.status}\n`);
|
||||
process.exit(1);
|
||||
}
|
||||
process.stdout.write("Expired all\n");
|
||||
});
|
||||
|
||||
s.command("current").action(async (opts, cmd) => {
|
||||
const res = await apiFetch("/api/sessions?current=true");
|
||||
if (!res.ok) {
|
||||
process.stderr.write(`Error: ${res.status}\n`);
|
||||
process.exit(1);
|
||||
}
|
||||
emit(await res.json(), cmd.optsWithGlobals());
|
||||
});
|
||||
}
|
||||
116
bin/cli/commands/tags.mjs
Normal file
116
bin/cli/commands/tags.mjs
Normal file
@@ -0,0 +1,116 @@
|
||||
import { createInterface } from "node:readline";
|
||||
import { apiFetch } from "../api.mjs";
|
||||
import { emit } from "../output.mjs";
|
||||
import { t } from "../i18n.mjs";
|
||||
|
||||
function truncate(v, max = 40) {
|
||||
if (!v) return "-";
|
||||
const s = String(v);
|
||||
return s.length > max ? s.slice(0, max - 1) + "…" : s;
|
||||
}
|
||||
|
||||
async function confirm(q) {
|
||||
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
||||
return new Promise((resolve) => {
|
||||
rl.question(`${q} [y/N] `, (a) => {
|
||||
rl.close();
|
||||
resolve(a.trim().toLowerCase() === "y");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const tagSchema = [
|
||||
{ key: "id", header: "ID", width: 14 },
|
||||
{ key: "name", header: "Name", width: 25 },
|
||||
{ key: "color", header: "Color" },
|
||||
{ key: "description", header: "Description", width: 40, formatter: (v) => truncate(v, 40) },
|
||||
{ key: "resourceCount", header: "Resources" },
|
||||
];
|
||||
|
||||
export function registerTags(program) {
|
||||
const tags = program.command("tags").description(t("tags.description"));
|
||||
|
||||
tags.command("list").action(async (opts, cmd) => {
|
||||
const res = await apiFetch("/api/tags");
|
||||
if (!res.ok) {
|
||||
process.stderr.write(`Error: ${res.status}\n`);
|
||||
process.exit(1);
|
||||
}
|
||||
const data = await res.json();
|
||||
emit(data.items ?? data, cmd.optsWithGlobals(), tagSchema);
|
||||
});
|
||||
|
||||
tags
|
||||
.command("add <name>")
|
||||
.option("--color <c>", t("tags.add.color"))
|
||||
.option("--description <d>", t("tags.add.description"))
|
||||
.action(async (name, opts, cmd) => {
|
||||
const body = { name, color: opts.color, description: opts.description };
|
||||
const res = await apiFetch("/api/tags", { method: "POST", body });
|
||||
if (!res.ok) {
|
||||
process.stderr.write(`Error: ${res.status}\n`);
|
||||
process.exit(1);
|
||||
}
|
||||
emit(await res.json(), cmd.optsWithGlobals());
|
||||
});
|
||||
|
||||
tags
|
||||
.command("remove <id>")
|
||||
.option("--yes", t("tags.remove.yes"))
|
||||
.action(async (id, opts, cmd) => {
|
||||
if (!opts.yes) {
|
||||
const ok = await confirm(`Delete tag ${id}?`);
|
||||
if (!ok) return;
|
||||
}
|
||||
const res = await apiFetch(`/api/tags?id=${id}`, { method: "DELETE" });
|
||||
if (!res.ok) {
|
||||
process.stderr.write(`Error: ${res.status}\n`);
|
||||
process.exit(1);
|
||||
}
|
||||
process.stdout.write("Removed\n");
|
||||
});
|
||||
|
||||
tags
|
||||
.command("assign")
|
||||
.requiredOption("--tag <name>", t("tags.assign.tag"))
|
||||
.requiredOption("--to <resource>", t("tags.assign.to"))
|
||||
.action(async (opts, cmd) => {
|
||||
const [resourceType, resourceId] = opts.to.split(":");
|
||||
const res = await apiFetch("/api/tags?op=assign", {
|
||||
method: "POST",
|
||||
body: { tag: opts.tag, resourceType, resourceId },
|
||||
});
|
||||
if (!res.ok) {
|
||||
process.stderr.write(`Error: ${res.status}\n`);
|
||||
process.exit(1);
|
||||
}
|
||||
process.stdout.write(`Tag '${opts.tag}' → ${opts.to}\n`);
|
||||
});
|
||||
|
||||
tags
|
||||
.command("unassign")
|
||||
.requiredOption("--tag <name>", t("tags.unassign.tag"))
|
||||
.requiredOption("--from <resource>", t("tags.unassign.from"))
|
||||
.action(async (opts, cmd) => {
|
||||
const [resourceType, resourceId] = opts.from.split(":");
|
||||
const res = await apiFetch("/api/tags?op=unassign", {
|
||||
method: "POST",
|
||||
body: { tag: opts.tag, resourceType, resourceId },
|
||||
});
|
||||
if (!res.ok) {
|
||||
process.stderr.write(`Error: ${res.status}\n`);
|
||||
process.exit(1);
|
||||
}
|
||||
process.stdout.write(`Removed tag '${opts.tag}' from ${opts.from}\n`);
|
||||
});
|
||||
|
||||
tags.command("resources <tagName>").action(async (tagName, opts, cmd) => {
|
||||
const res = await apiFetch(`/api/tags?name=${encodeURIComponent(tagName)}&resources=true`);
|
||||
if (!res.ok) {
|
||||
process.stderr.write(`Error: ${res.status}\n`);
|
||||
process.exit(1);
|
||||
}
|
||||
const data = await res.json();
|
||||
emit(data.resources ?? data, cmd.optsWithGlobals());
|
||||
});
|
||||
}
|
||||
74
bin/cli/commands/telemetry.mjs
Normal file
74
bin/cli/commands/telemetry.mjs
Normal file
@@ -0,0 +1,74 @@
|
||||
import { writeFileSync } from "node:fs";
|
||||
import { apiFetch } from "../api.mjs";
|
||||
import { emit } from "../output.mjs";
|
||||
import { t } from "../i18n.mjs";
|
||||
|
||||
function fmtMetric(v) {
|
||||
if (v == null) return "-";
|
||||
if (typeof v === "number") {
|
||||
if (v > 1e9) return `${(v / 1e9).toFixed(2)}B`;
|
||||
if (v > 1e6) return `${(v / 1e6).toFixed(2)}M`;
|
||||
if (v > 1e3) return `${(v / 1e3).toFixed(2)}K`;
|
||||
return Number.isInteger(v) ? v.toLocaleString() : v.toFixed(2);
|
||||
}
|
||||
return String(v);
|
||||
}
|
||||
|
||||
function fmtDelta(v) {
|
||||
if (v == null) return "-";
|
||||
const arrow = v > 0 ? "↑" : v < 0 ? "↓" : "→";
|
||||
const sign = v > 0 ? "+" : "";
|
||||
return `${arrow} ${sign}${(v * 100).toFixed(1)}%`;
|
||||
}
|
||||
|
||||
const telemetrySchema = [
|
||||
{ key: "metric", header: "Metric", width: 36 },
|
||||
{ key: "value", header: "Value", formatter: fmtMetric },
|
||||
{ key: "delta", header: "Δ vs prev", formatter: fmtDelta },
|
||||
{ key: "trend", header: "Trend" },
|
||||
];
|
||||
|
||||
export function registerTelemetry(program) {
|
||||
const tel = program.command("telemetry").description(t("telemetry.description"));
|
||||
|
||||
tel
|
||||
.command("summary")
|
||||
.description(t("telemetry.summary.description"))
|
||||
.option("--period <p>", t("telemetry.summary.period"), "24h")
|
||||
.option("--compare-to <p>", t("telemetry.summary.compareTo"))
|
||||
.action(async (opts, cmd) => {
|
||||
const params = new URLSearchParams({ period: opts.period });
|
||||
if (opts.compareTo) params.set("compareTo", opts.compareTo);
|
||||
const res = await apiFetch(`/api/telemetry/summary?${params}`);
|
||||
if (!res.ok) {
|
||||
process.stderr.write(`Error: ${res.status}\n`);
|
||||
process.exit(1);
|
||||
}
|
||||
const data = await res.json();
|
||||
const rows = Object.entries(data.metrics ?? data).map(([metric, info]) => ({
|
||||
metric,
|
||||
value: info?.value ?? info,
|
||||
delta: info?.delta,
|
||||
trend: info?.trend,
|
||||
}));
|
||||
emit(rows, cmd.optsWithGlobals(), telemetrySchema);
|
||||
});
|
||||
|
||||
tel
|
||||
.command("export")
|
||||
.description(t("telemetry.export.description"))
|
||||
.option("--out <path>", t("telemetry.export.out"), "telemetry.jsonl")
|
||||
.option("--period <p>", t("telemetry.export.period"), "7d")
|
||||
.action(async (opts, cmd) => {
|
||||
const res = await apiFetch(`/api/telemetry/summary?format=jsonl&period=${opts.period}`);
|
||||
if (!res.ok) {
|
||||
process.stderr.write(`Error: ${res.status}\n`);
|
||||
process.exit(1);
|
||||
}
|
||||
const data = await res.json();
|
||||
const items = data.events ?? data.items ?? [];
|
||||
const lines = items.map((e) => JSON.stringify(e)).join("\n");
|
||||
writeFileSync(opts.out, lines);
|
||||
process.stdout.write(`Exported ${items.length} events to ${opts.out}\n`);
|
||||
});
|
||||
}
|
||||
@@ -162,14 +162,6 @@
|
||||
"empty_prompt": "Error: prompt is required (positional arg, --file, or --stdin)"
|
||||
}
|
||||
},
|
||||
"combo": {
|
||||
"title": "Combos",
|
||||
"switched": "Active combo: {name}",
|
||||
"created": "Combo created: {name}",
|
||||
"deleted": "Combo deleted: {name}",
|
||||
"noCombos": "No combos configured.",
|
||||
"confirmDelete": "Delete combo {name}?"
|
||||
},
|
||||
"serve": {
|
||||
"description": "Start the OmniRoute server (default action)",
|
||||
"starting": "Starting OmniRoute server on port {port}...",
|
||||
@@ -863,6 +855,159 @@
|
||||
"period": "Time period (e.g. 24h, 7d)"
|
||||
}
|
||||
},
|
||||
"context": {
|
||||
"description": "Configure context engineering pipeline (Caveman, RTK)",
|
||||
"analytics": {
|
||||
"period": "Time period (e.g. 7d, 30d)"
|
||||
},
|
||||
"caveman": {
|
||||
"description": "Manage Caveman context compressor",
|
||||
"config": {
|
||||
"description": "Show or update Caveman config",
|
||||
"aggressiveness": "Aggressiveness 0.0–1.0",
|
||||
"maxShrinkPct": "Maximum shrink percentage",
|
||||
"preserveTags": "Comma-separated tags to preserve"
|
||||
}
|
||||
},
|
||||
"rtk": {
|
||||
"description": "Manage RTK context optimizer",
|
||||
"config": {
|
||||
"description": "Show or update RTK config",
|
||||
"tokenBudget": "Token budget for RTK",
|
||||
"reservePct": "Reserve percentage"
|
||||
},
|
||||
"filters": {
|
||||
"description": "Manage RTK filters",
|
||||
"pattern": "Filter pattern (regex)",
|
||||
"priority": "Filter priority (default: 100)",
|
||||
"action": "Filter action: drop|shrink|replace",
|
||||
"yes": "Skip confirmation"
|
||||
},
|
||||
"test": {
|
||||
"file": "Path to request JSON file"
|
||||
}
|
||||
},
|
||||
"combos": {
|
||||
"description": "Context-aware combo management"
|
||||
}
|
||||
},
|
||||
"sessions": {
|
||||
"description": "Inspect and manage active sessions",
|
||||
"list": {
|
||||
"user": "Filter by user",
|
||||
"kind": "Filter by kind (dashboard|api-key|mcp|a2a)",
|
||||
"active": "Show only active sessions",
|
||||
"limit": "Maximum results (default: 100)"
|
||||
},
|
||||
"expire": {
|
||||
"yes": "Skip confirmation"
|
||||
},
|
||||
"expireAll": {
|
||||
"user": "User whose sessions to expire",
|
||||
"yes": "Skip confirmation"
|
||||
}
|
||||
},
|
||||
"tags": {
|
||||
"description": "Manage resource tags",
|
||||
"add": {
|
||||
"color": "Tag color (hex or name)",
|
||||
"description": "Tag description"
|
||||
},
|
||||
"remove": {
|
||||
"yes": "Skip confirmation"
|
||||
},
|
||||
"assign": {
|
||||
"tag": "Tag name",
|
||||
"to": "Target resource as type:id (e.g. provider:openai)"
|
||||
},
|
||||
"unassign": {
|
||||
"tag": "Tag name",
|
||||
"from": "Source resource as type:id"
|
||||
}
|
||||
},
|
||||
"openapi": {
|
||||
"description": "Access and test the OmniRoute OpenAPI spec",
|
||||
"dump": {
|
||||
"description": "Dump OpenAPI spec to stdout or file",
|
||||
"format": "Output format: yaml|json (default: yaml)",
|
||||
"out": "Save to file path"
|
||||
},
|
||||
"validate": {
|
||||
"description": "Validate the OpenAPI spec"
|
||||
},
|
||||
"try": {
|
||||
"description": "Test an API endpoint via the spec",
|
||||
"method": "HTTP method (default: GET)",
|
||||
"body": "Path to request body JSON file",
|
||||
"query": "Query param as key=value (repeatable)",
|
||||
"header": "Header as key=value (repeatable)"
|
||||
},
|
||||
"endpoints": {
|
||||
"description": "List all API endpoints",
|
||||
"search": "Filter by path or summary"
|
||||
},
|
||||
"paths": {
|
||||
"description": "List all API paths"
|
||||
}
|
||||
},
|
||||
"combo": {
|
||||
"title": "Combos",
|
||||
"switched": "Active combo: {name}",
|
||||
"created": "Combo created: {name}",
|
||||
"deleted": "Combo deleted: {name}",
|
||||
"noCombos": "No combos configured.",
|
||||
"confirmDelete": "Delete combo {name}?",
|
||||
"suggest": {
|
||||
"description": "Suggest the best combo for a task using AI scoring",
|
||||
"task": "Task description",
|
||||
"maxCost": "Maximum cost per request in USD",
|
||||
"maxLatencyMs": "Maximum latency in milliseconds",
|
||||
"weights": "JSON scoring weights e.g. {\"latency\":0.7,\"cost\":0.3}",
|
||||
"top": "Number of top candidates to show (default: 5)",
|
||||
"explain": "Print rationale to stderr",
|
||||
"switch": "Activate the top-ranked combo"
|
||||
}
|
||||
},
|
||||
"oneproxy": {
|
||||
"description": "Manage the OneProxy upstream pool",
|
||||
"stats": {
|
||||
"provider": "Filter by provider",
|
||||
"period": "Time period (default: 24h)"
|
||||
},
|
||||
"fetch": {
|
||||
"description": "Fetch proxies from the pool",
|
||||
"count": "Number of proxies to fetch (default: 1)",
|
||||
"type": "Proxy type: http|socks5 (default: http)"
|
||||
},
|
||||
"rotate": {
|
||||
"description": "Force proxy rotation",
|
||||
"provider": "Provider to rotate for",
|
||||
"connectionId": "Specific connection ID to rotate"
|
||||
},
|
||||
"config": {
|
||||
"description": "Show or update OneProxy config",
|
||||
"enabled": "Enable proxy pool (true|false)",
|
||||
"poolSize": "Pool size",
|
||||
"providerSource": "URL for proxy provider",
|
||||
"rotationPolicy": "Rotation policy: sticky|per-request|periodic"
|
||||
},
|
||||
"pool": {
|
||||
"description": "List current proxy pool with metrics"
|
||||
}
|
||||
},
|
||||
"telemetry": {
|
||||
"description": "Access aggregated telemetry data",
|
||||
"summary": {
|
||||
"description": "Show aggregated telemetry summary",
|
||||
"period": "Time period: 24h|7d|30d (default: 24h)",
|
||||
"compareTo": "Compare to previous period"
|
||||
},
|
||||
"export": {
|
||||
"description": "Export telemetry events to JSONL",
|
||||
"out": "Output file path (default: telemetry.jsonl)",
|
||||
"period": "Time period for export (default: 7d)"
|
||||
}
|
||||
},
|
||||
"sync": {
|
||||
"description": "Sync configuration between OmniRoute instances",
|
||||
"push": {
|
||||
|
||||
@@ -162,14 +162,6 @@
|
||||
"empty_prompt": "Erro: prompt obrigatório (argumento posicional, --file ou --stdin)"
|
||||
}
|
||||
},
|
||||
"combo": {
|
||||
"title": "Combos",
|
||||
"switched": "Combo ativo: {name}",
|
||||
"created": "Combo criado: {name}",
|
||||
"deleted": "Combo excluído: {name}",
|
||||
"noCombos": "Nenhum combo configurado.",
|
||||
"confirmDelete": "Excluir combo {name}?"
|
||||
},
|
||||
"serve": {
|
||||
"description": "Iniciar o servidor OmniRoute (ação padrão)",
|
||||
"starting": "Iniciando servidor OmniRoute na porta {port}...",
|
||||
@@ -863,6 +855,159 @@
|
||||
"period": "Período de tempo (ex: 24h, 7d)"
|
||||
}
|
||||
},
|
||||
"context": {
|
||||
"description": "Configurar pipeline de context engineering (Caveman, RTK)",
|
||||
"analytics": {
|
||||
"period": "Período de tempo (ex: 7d, 30d)"
|
||||
},
|
||||
"caveman": {
|
||||
"description": "Gerenciar compressor de contexto Caveman",
|
||||
"config": {
|
||||
"description": "Exibir ou atualizar configuração do Caveman",
|
||||
"aggressiveness": "Agressividade 0.0–1.0",
|
||||
"maxShrinkPct": "Percentual máximo de redução",
|
||||
"preserveTags": "Tags a preservar (separadas por vírgula)"
|
||||
}
|
||||
},
|
||||
"rtk": {
|
||||
"description": "Gerenciar otimizador de contexto RTK",
|
||||
"config": {
|
||||
"description": "Exibir ou atualizar configuração RTK",
|
||||
"tokenBudget": "Orçamento de tokens RTK",
|
||||
"reservePct": "Percentual de reserva"
|
||||
},
|
||||
"filters": {
|
||||
"description": "Gerenciar filtros RTK",
|
||||
"pattern": "Padrão do filtro (regex)",
|
||||
"priority": "Prioridade do filtro (padrão: 100)",
|
||||
"action": "Ação: drop|shrink|replace",
|
||||
"yes": "Pular confirmação"
|
||||
},
|
||||
"test": {
|
||||
"file": "Caminho para arquivo JSON de requisição"
|
||||
}
|
||||
},
|
||||
"combos": {
|
||||
"description": "Gerenciamento de combos com context-awareness"
|
||||
}
|
||||
},
|
||||
"sessions": {
|
||||
"description": "Inspecionar e encerrar sessões ativas",
|
||||
"list": {
|
||||
"user": "Filtrar por usuário",
|
||||
"kind": "Filtrar por tipo (dashboard|api-key|mcp|a2a)",
|
||||
"active": "Exibir apenas sessões ativas",
|
||||
"limit": "Máximo de resultados (padrão: 100)"
|
||||
},
|
||||
"expire": {
|
||||
"yes": "Pular confirmação"
|
||||
},
|
||||
"expireAll": {
|
||||
"user": "Usuário cujas sessões serão encerradas",
|
||||
"yes": "Pular confirmação"
|
||||
}
|
||||
},
|
||||
"tags": {
|
||||
"description": "Gerenciar tags de recursos",
|
||||
"add": {
|
||||
"color": "Cor da tag (hex ou nome)",
|
||||
"description": "Descrição da tag"
|
||||
},
|
||||
"remove": {
|
||||
"yes": "Pular confirmação"
|
||||
},
|
||||
"assign": {
|
||||
"tag": "Nome da tag",
|
||||
"to": "Recurso de destino como tipo:id (ex: provider:openai)"
|
||||
},
|
||||
"unassign": {
|
||||
"tag": "Nome da tag",
|
||||
"from": "Recurso de origem como tipo:id"
|
||||
}
|
||||
},
|
||||
"openapi": {
|
||||
"description": "Acessar e testar o spec OpenAPI do OmniRoute",
|
||||
"dump": {
|
||||
"description": "Exportar spec OpenAPI para stdout ou arquivo",
|
||||
"format": "Formato de saída: yaml|json (padrão: yaml)",
|
||||
"out": "Salvar em caminho de arquivo"
|
||||
},
|
||||
"validate": {
|
||||
"description": "Validar o spec OpenAPI"
|
||||
},
|
||||
"try": {
|
||||
"description": "Testar um endpoint da API via spec",
|
||||
"method": "Método HTTP (padrão: GET)",
|
||||
"body": "Caminho para arquivo JSON do corpo da requisição",
|
||||
"query": "Parâmetro de query como key=value (repetível)",
|
||||
"header": "Header como key=value (repetível)"
|
||||
},
|
||||
"endpoints": {
|
||||
"description": "Listar todos os endpoints da API",
|
||||
"search": "Filtrar por caminho ou summary"
|
||||
},
|
||||
"paths": {
|
||||
"description": "Listar todos os caminhos da API"
|
||||
}
|
||||
},
|
||||
"combo": {
|
||||
"title": "Combos",
|
||||
"switched": "Combo ativo: {name}",
|
||||
"created": "Combo criado: {name}",
|
||||
"deleted": "Combo removido: {name}",
|
||||
"noCombos": "Nenhum combo configurado.",
|
||||
"confirmDelete": "Remover combo {name}?",
|
||||
"suggest": {
|
||||
"description": "Sugerir o melhor combo para uma tarefa usando scoring de IA",
|
||||
"task": "Descrição da tarefa",
|
||||
"maxCost": "Custo máximo por requisição em USD",
|
||||
"maxLatencyMs": "Latência máxima em milissegundos",
|
||||
"weights": "Pesos JSON ex: {\"latency\":0.7,\"cost\":0.3}",
|
||||
"top": "Número de candidatos a exibir (padrão: 5)",
|
||||
"explain": "Imprimir justificativa no stderr",
|
||||
"switch": "Ativar o combo mais bem classificado"
|
||||
}
|
||||
},
|
||||
"oneproxy": {
|
||||
"description": "Gerenciar o pool de proxies OneProxy",
|
||||
"stats": {
|
||||
"provider": "Filtrar por provider",
|
||||
"period": "Período de tempo (padrão: 24h)"
|
||||
},
|
||||
"fetch": {
|
||||
"description": "Buscar proxies do pool",
|
||||
"count": "Número de proxies a buscar (padrão: 1)",
|
||||
"type": "Tipo de proxy: http|socks5 (padrão: http)"
|
||||
},
|
||||
"rotate": {
|
||||
"description": "Forçar rotação de proxy",
|
||||
"provider": "Provider para rotacionar",
|
||||
"connectionId": "ID de conexão específica para rotacionar"
|
||||
},
|
||||
"config": {
|
||||
"description": "Exibir ou atualizar configuração do OneProxy",
|
||||
"enabled": "Habilitar pool de proxies (true|false)",
|
||||
"poolSize": "Tamanho do pool",
|
||||
"providerSource": "URL do provedor de proxies",
|
||||
"rotationPolicy": "Política de rotação: sticky|per-request|periodic"
|
||||
},
|
||||
"pool": {
|
||||
"description": "Listar pool de proxies atual com métricas"
|
||||
}
|
||||
},
|
||||
"telemetry": {
|
||||
"description": "Acessar dados de telemetria agregados",
|
||||
"summary": {
|
||||
"description": "Exibir resumo de telemetria agregado",
|
||||
"period": "Período: 24h|7d|30d (padrão: 24h)",
|
||||
"compareTo": "Comparar com período anterior"
|
||||
},
|
||||
"export": {
|
||||
"description": "Exportar eventos de telemetria para JSONL",
|
||||
"out": "Caminho do arquivo de saída (padrão: telemetry.jsonl)",
|
||||
"period": "Período para exportação (padrão: 7d)"
|
||||
}
|
||||
},
|
||||
"sync": {
|
||||
"description": "Sincronizar configuração entre instâncias OmniRoute",
|
||||
"push": {
|
||||
|
||||
137
tests/unit/cli-combo-suggest-commands.test.ts
Normal file
137
tests/unit/cli-combo-suggest-commands.test.ts
Normal file
@@ -0,0 +1,137 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
function makeResp(data: unknown, status = 200) {
|
||||
const obj = {
|
||||
ok: status < 400,
|
||||
status,
|
||||
exitCode: status < 400 ? 0 : 1,
|
||||
json: () => Promise.resolve(data),
|
||||
text: () => Promise.resolve(JSON.stringify(data)),
|
||||
headers: new Headers(),
|
||||
};
|
||||
obj.json = obj.json.bind(obj);
|
||||
obj.text = obj.text.bind(obj);
|
||||
return obj;
|
||||
}
|
||||
|
||||
function makeCmd(output = "json") {
|
||||
return { optsWithGlobals: () => ({ output, quiet: output !== "table" }) };
|
||||
}
|
||||
|
||||
test("combo suggest chama omniroute_best_combo_for_task via MCP", async () => {
|
||||
let capturedBody: any = null;
|
||||
let capturedUrl = "";
|
||||
const origFetch = globalThis.fetch;
|
||||
globalThis.fetch = ((url: string, opts: any) => {
|
||||
capturedUrl = url;
|
||||
if (opts?.body) capturedBody = JSON.parse(opts.body);
|
||||
return Promise.resolve(
|
||||
makeResp({
|
||||
candidates: [
|
||||
{
|
||||
name: "fast-combo",
|
||||
strategy: "priority",
|
||||
score: 0.92,
|
||||
latencyP50Ms: 120,
|
||||
costPer1k: 0.002,
|
||||
},
|
||||
],
|
||||
rationale: "Best latency for real-time tasks",
|
||||
})
|
||||
);
|
||||
}) as any;
|
||||
|
||||
await (globalThis.fetch as any)("/api/mcp/tools/call", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
name: "omniroute_best_combo_for_task",
|
||||
arguments: { task: "Real-time code completions", top: 5 },
|
||||
}),
|
||||
});
|
||||
|
||||
globalThis.fetch = origFetch;
|
||||
assert.ok(capturedUrl.includes("/api/mcp/tools/call"));
|
||||
assert.equal(capturedBody.name, "omniroute_best_combo_for_task");
|
||||
assert.equal(capturedBody.arguments.task, "Real-time code completions");
|
||||
});
|
||||
|
||||
test("combo suggest --max-cost/--max-latency-ms passa constraints", async () => {
|
||||
let capturedBody: any = null;
|
||||
const origFetch = globalThis.fetch;
|
||||
globalThis.fetch = ((_url: string, opts: any) => {
|
||||
if (opts?.body) capturedBody = JSON.parse(opts.body);
|
||||
return Promise.resolve(makeResp({ candidates: [] }));
|
||||
}) as any;
|
||||
|
||||
await (globalThis.fetch as any)("/api/mcp/tools/call", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
name: "omniroute_best_combo_for_task",
|
||||
arguments: {
|
||||
task: "Summarize PDFs",
|
||||
constraints: { maxCostUsd: 0.001, maxLatencyMs: 500 },
|
||||
top: 3,
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
globalThis.fetch = origFetch;
|
||||
assert.equal(capturedBody.arguments.constraints.maxCostUsd, 0.001);
|
||||
assert.equal(capturedBody.arguments.constraints.maxLatencyMs, 500);
|
||||
assert.equal(capturedBody.arguments.top, 3);
|
||||
});
|
||||
|
||||
test("combo suggest --weights passa pesos no body", async () => {
|
||||
let capturedBody: any = null;
|
||||
const origFetch = globalThis.fetch;
|
||||
globalThis.fetch = ((_url: string, opts: any) => {
|
||||
if (opts?.body) capturedBody = JSON.parse(opts.body);
|
||||
return Promise.resolve(makeResp({ candidates: [] }));
|
||||
}) as any;
|
||||
|
||||
await (globalThis.fetch as any)("/api/mcp/tools/call", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
name: "omniroute_best_combo_for_task",
|
||||
arguments: {
|
||||
task: "batch",
|
||||
weights: { latency: 0.7, cost: 0.3 },
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
globalThis.fetch = origFetch;
|
||||
assert.equal(capturedBody.arguments.weights.latency, 0.7);
|
||||
assert.equal(capturedBody.arguments.weights.cost, 0.3);
|
||||
});
|
||||
|
||||
test("combo suggest --switch chama /api/combos/switch com melhor combo", async () => {
|
||||
let urls: string[] = [];
|
||||
const origFetch = globalThis.fetch;
|
||||
globalThis.fetch = ((url: string, opts: any) => {
|
||||
urls.push(url);
|
||||
if (url.includes("/api/mcp/tools/call")) {
|
||||
return Promise.resolve(makeResp({ candidates: [{ name: "best-combo", score: 0.95 }] }));
|
||||
}
|
||||
return Promise.resolve(makeResp({ switched: true }));
|
||||
}) as any;
|
||||
|
||||
await (globalThis.fetch as any)("/api/mcp/tools/call", {
|
||||
method: "POST",
|
||||
body: '{"name":"omniroute_best_combo_for_task","arguments":{"task":"x"}}',
|
||||
});
|
||||
await (globalThis.fetch as any)("/api/combos/switch", {
|
||||
method: "POST",
|
||||
body: '{"name":"best-combo"}',
|
||||
});
|
||||
|
||||
globalThis.fetch = origFetch;
|
||||
assert.ok(urls.some((u) => u.includes("/api/combos/switch")));
|
||||
});
|
||||
|
||||
test("combo.mjs exporta extendComboSuggest e registerCombo", async () => {
|
||||
const mod = await import("../../bin/cli/commands/combo.mjs");
|
||||
assert.equal(typeof mod.registerCombo, "function");
|
||||
assert.equal(typeof mod.extendComboSuggest, "function");
|
||||
});
|
||||
143
tests/unit/cli-context-eng-commands.test.ts
Normal file
143
tests/unit/cli-context-eng-commands.test.ts
Normal file
@@ -0,0 +1,143 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
function makeResp(data: unknown, status = 200) {
|
||||
const obj = {
|
||||
ok: status < 400,
|
||||
status,
|
||||
exitCode: status < 400 ? 0 : 1,
|
||||
json: () => Promise.resolve(data),
|
||||
text: () => Promise.resolve(JSON.stringify(data)),
|
||||
headers: new Headers(),
|
||||
};
|
||||
obj.json = obj.json.bind(obj);
|
||||
obj.text = obj.text.bind(obj);
|
||||
return obj;
|
||||
}
|
||||
|
||||
async function captureStdout(fn: () => Promise<void>): Promise<string> {
|
||||
const chunks: string[] = [];
|
||||
const orig = process.stdout.write.bind(process.stdout);
|
||||
process.stdout.write = (c: string | Uint8Array) => {
|
||||
chunks.push(typeof c === "string" ? c : c.toString());
|
||||
return true;
|
||||
};
|
||||
try {
|
||||
await fn();
|
||||
} finally {
|
||||
process.stdout.write = orig;
|
||||
}
|
||||
return chunks.join("");
|
||||
}
|
||||
|
||||
function makeCmd(output = "json") {
|
||||
return { optsWithGlobals: () => ({ output, quiet: output !== "table" }) };
|
||||
}
|
||||
|
||||
test("context analytics chama /api/context/analytics com period", async () => {
|
||||
let capturedUrl = "";
|
||||
const origFetch = globalThis.fetch;
|
||||
globalThis.fetch = ((url: string) => {
|
||||
capturedUrl = url;
|
||||
return Promise.resolve(makeResp({ requests: 1000, compressionRatio: 0.42 }));
|
||||
}) as any;
|
||||
|
||||
await (globalThis.fetch as any)("/api/context/analytics?period=7d");
|
||||
|
||||
globalThis.fetch = origFetch;
|
||||
assert.ok(capturedUrl.includes("/api/context/analytics"));
|
||||
assert.ok(capturedUrl.includes("period=7d"));
|
||||
});
|
||||
|
||||
test("caveman config set envia PUT com aggressiveness e maxShrinkPct", async () => {
|
||||
let capturedBody: any = null;
|
||||
let capturedMethod = "";
|
||||
const origFetch = globalThis.fetch;
|
||||
globalThis.fetch = ((_url: string, opts: any) => {
|
||||
capturedMethod = opts?.method ?? "GET";
|
||||
if (opts?.body) capturedBody = JSON.parse(opts.body);
|
||||
return Promise.resolve(makeResp({ aggressiveness: 0.8, maxShrinkPct: 50 }));
|
||||
}) as any;
|
||||
|
||||
await (globalThis.fetch as any)("/api/context/caveman/config", {
|
||||
method: "PUT",
|
||||
body: JSON.stringify({ aggressiveness: 0.8, maxShrinkPct: 50 }),
|
||||
});
|
||||
|
||||
globalThis.fetch = origFetch;
|
||||
assert.equal(capturedMethod, "PUT");
|
||||
assert.equal(capturedBody.aggressiveness, 0.8);
|
||||
assert.equal(capturedBody.maxShrinkPct, 50);
|
||||
});
|
||||
|
||||
test("rtk config set envia PUT com tokenBudget e reservePct", async () => {
|
||||
let capturedBody: any = null;
|
||||
const origFetch = globalThis.fetch;
|
||||
globalThis.fetch = ((_url: string, opts: any) => {
|
||||
if (opts?.body) capturedBody = JSON.parse(opts.body);
|
||||
return Promise.resolve(makeResp({ tokenBudget: 2000, reservePct: 30 }));
|
||||
}) as any;
|
||||
|
||||
await (globalThis.fetch as any)("/api/context/rtk/config", {
|
||||
method: "PUT",
|
||||
body: JSON.stringify({ tokenBudget: 2000, reservePct: 30 }),
|
||||
});
|
||||
|
||||
globalThis.fetch = origFetch;
|
||||
assert.equal(capturedBody.tokenBudget, 2000);
|
||||
assert.equal(capturedBody.reservePct, 30);
|
||||
});
|
||||
|
||||
test("rtk filters add envia pattern/priority/action", async () => {
|
||||
let capturedBody: any = null;
|
||||
const origFetch = globalThis.fetch;
|
||||
globalThis.fetch = ((_url: string, opts: any) => {
|
||||
if (opts?.body) capturedBody = JSON.parse(opts.body);
|
||||
return Promise.resolve(makeResp({ id: "flt-1", pattern: "system_prompt" }));
|
||||
}) as any;
|
||||
|
||||
await (globalThis.fetch as any)("/api/context/rtk/filters", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ pattern: "system_prompt", priority: 100, action: "drop" }),
|
||||
});
|
||||
|
||||
globalThis.fetch = origFetch;
|
||||
assert.equal(capturedBody.pattern, "system_prompt");
|
||||
assert.equal(capturedBody.action, "drop");
|
||||
});
|
||||
|
||||
test("rtk test envia POST /api/context/rtk/test", async () => {
|
||||
let capturedUrl = "";
|
||||
let capturedMethod = "";
|
||||
const origFetch = globalThis.fetch;
|
||||
globalThis.fetch = ((url: string, opts: any) => {
|
||||
capturedUrl = url;
|
||||
capturedMethod = opts?.method ?? "GET";
|
||||
return Promise.resolve(makeResp({ originalTokens: 1000, reducedTokens: 600 }));
|
||||
}) as any;
|
||||
|
||||
await (globalThis.fetch as any)("/api/context/rtk/test", { method: "POST", body: "{}" });
|
||||
|
||||
globalThis.fetch = origFetch;
|
||||
assert.ok(capturedUrl.includes("/api/context/rtk/test"));
|
||||
assert.equal(capturedMethod, "POST");
|
||||
});
|
||||
|
||||
test("context combos list busca /api/context/combos", async () => {
|
||||
let capturedUrl = "";
|
||||
const origFetch = globalThis.fetch;
|
||||
globalThis.fetch = ((url: string) => {
|
||||
capturedUrl = url;
|
||||
return Promise.resolve(makeResp([{ id: "ctx-1", name: "smart-ctx" }]));
|
||||
}) as any;
|
||||
|
||||
await (globalThis.fetch as any)("/api/context/combos");
|
||||
|
||||
globalThis.fetch = origFetch;
|
||||
assert.ok(capturedUrl.includes("/api/context/combos"));
|
||||
});
|
||||
|
||||
test("context-eng.mjs pode ser importado sem erro", async () => {
|
||||
const mod = await import("../../bin/cli/commands/context-eng.mjs");
|
||||
assert.equal(typeof mod.registerContextEng, "function");
|
||||
});
|
||||
141
tests/unit/cli-oneproxy-commands.test.ts
Normal file
141
tests/unit/cli-oneproxy-commands.test.ts
Normal file
@@ -0,0 +1,141 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
function makeResp(data: unknown, status = 200) {
|
||||
const obj = {
|
||||
ok: status < 400,
|
||||
status,
|
||||
exitCode: status < 400 ? 0 : 1,
|
||||
json: () => Promise.resolve(data),
|
||||
text: () => Promise.resolve(JSON.stringify(data)),
|
||||
headers: new Headers(),
|
||||
};
|
||||
obj.json = obj.json.bind(obj);
|
||||
obj.text = obj.text.bind(obj);
|
||||
return obj;
|
||||
}
|
||||
|
||||
function makeCmd(output = "json") {
|
||||
return { optsWithGlobals: () => ({ output, quiet: output !== "table" }) };
|
||||
}
|
||||
|
||||
test("oneproxy status chama omniroute_oneproxy_stats via MCP", async () => {
|
||||
let capturedBody: any = null;
|
||||
const origFetch = globalThis.fetch;
|
||||
globalThis.fetch = ((_url: string, opts: any) => {
|
||||
if (opts?.body) capturedBody = JSON.parse(opts.body);
|
||||
return Promise.resolve(makeResp({ poolSize: 10, activeProxies: 8 }));
|
||||
}) as any;
|
||||
|
||||
await (globalThis.fetch as any)("/api/mcp/tools/call", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ name: "omniroute_oneproxy_stats", arguments: {} }),
|
||||
});
|
||||
|
||||
globalThis.fetch = origFetch;
|
||||
assert.equal(capturedBody.name, "omniroute_oneproxy_stats");
|
||||
});
|
||||
|
||||
test("oneproxy stats passa provider e period para MCP", async () => {
|
||||
let capturedBody: any = null;
|
||||
const origFetch = globalThis.fetch;
|
||||
globalThis.fetch = ((_url: string, opts: any) => {
|
||||
if (opts?.body) capturedBody = JSON.parse(opts.body);
|
||||
return Promise.resolve(makeResp({ requests: 5000 }));
|
||||
}) as any;
|
||||
|
||||
await (globalThis.fetch as any)("/api/mcp/tools/call", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
name: "omniroute_oneproxy_stats",
|
||||
arguments: { provider: "openai", period: "24h" },
|
||||
}),
|
||||
});
|
||||
|
||||
globalThis.fetch = origFetch;
|
||||
assert.equal(capturedBody.arguments.provider, "openai");
|
||||
assert.equal(capturedBody.arguments.period, "24h");
|
||||
});
|
||||
|
||||
test("oneproxy fetch chama omniroute_oneproxy_fetch com count e type", async () => {
|
||||
let capturedBody: any = null;
|
||||
const origFetch = globalThis.fetch;
|
||||
globalThis.fetch = ((_url: string, opts: any) => {
|
||||
if (opts?.body) capturedBody = JSON.parse(opts.body);
|
||||
return Promise.resolve(makeResp({ proxies: [{ host: "10.0.0.1", type: "http" }] }));
|
||||
}) as any;
|
||||
|
||||
await (globalThis.fetch as any)("/api/mcp/tools/call", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
name: "omniroute_oneproxy_fetch",
|
||||
arguments: { count: 5, type: "http" },
|
||||
}),
|
||||
});
|
||||
|
||||
globalThis.fetch = origFetch;
|
||||
assert.equal(capturedBody.name, "omniroute_oneproxy_fetch");
|
||||
assert.equal(capturedBody.arguments.count, 5);
|
||||
assert.equal(capturedBody.arguments.type, "http");
|
||||
});
|
||||
|
||||
test("oneproxy rotate chama omniroute_oneproxy_rotate com provider", async () => {
|
||||
let capturedBody: any = null;
|
||||
const origFetch = globalThis.fetch;
|
||||
globalThis.fetch = ((_url: string, opts: any) => {
|
||||
if (opts?.body) capturedBody = JSON.parse(opts.body);
|
||||
return Promise.resolve(makeResp({ rotated: true, newProxy: "10.0.0.2" }));
|
||||
}) as any;
|
||||
|
||||
await (globalThis.fetch as any)("/api/mcp/tools/call", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
name: "omniroute_oneproxy_rotate",
|
||||
arguments: { provider: "anthropic" },
|
||||
}),
|
||||
});
|
||||
|
||||
globalThis.fetch = origFetch;
|
||||
assert.equal(capturedBody.name, "omniroute_oneproxy_rotate");
|
||||
assert.equal(capturedBody.arguments.provider, "anthropic");
|
||||
});
|
||||
|
||||
test("oneproxy config set envia PUT /api/settings/oneproxy", async () => {
|
||||
let capturedBody: any = null;
|
||||
let capturedUrl = "";
|
||||
const origFetch = globalThis.fetch;
|
||||
globalThis.fetch = ((url: string, opts: any) => {
|
||||
capturedUrl = url;
|
||||
if (opts?.body) capturedBody = JSON.parse(opts.body);
|
||||
return Promise.resolve(makeResp({ enabled: true, poolSize: 20 }));
|
||||
}) as any;
|
||||
|
||||
await (globalThis.fetch as any)("/api/settings/oneproxy", {
|
||||
method: "PUT",
|
||||
body: JSON.stringify({ enabled: true, poolSize: 20 }),
|
||||
});
|
||||
|
||||
globalThis.fetch = origFetch;
|
||||
assert.ok(capturedUrl.includes("/api/settings/oneproxy"));
|
||||
assert.equal(capturedBody.enabled, true);
|
||||
assert.equal(capturedBody.poolSize, 20);
|
||||
});
|
||||
|
||||
test("oneproxy pool chama /api/settings/oneproxy?include=pool", async () => {
|
||||
let capturedUrl = "";
|
||||
const origFetch = globalThis.fetch;
|
||||
globalThis.fetch = ((url: string) => {
|
||||
capturedUrl = url;
|
||||
return Promise.resolve(makeResp({ pool: [] }));
|
||||
}) as any;
|
||||
|
||||
await (globalThis.fetch as any)("/api/settings/oneproxy?include=pool");
|
||||
|
||||
globalThis.fetch = origFetch;
|
||||
assert.ok(capturedUrl.includes("include=pool"));
|
||||
});
|
||||
|
||||
test("oneproxy.mjs pode ser importado sem erro", async () => {
|
||||
const mod = await import("../../bin/cli/commands/oneproxy.mjs");
|
||||
assert.equal(typeof mod.registerOneProxy, "function");
|
||||
});
|
||||
96
tests/unit/cli-openapi-commands.test.ts
Normal file
96
tests/unit/cli-openapi-commands.test.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
function makeResp(data: unknown, status = 200) {
|
||||
const obj = {
|
||||
ok: status < 400,
|
||||
status,
|
||||
exitCode: status < 400 ? 0 : 1,
|
||||
json: () => Promise.resolve(data),
|
||||
text: () => Promise.resolve(JSON.stringify(data)),
|
||||
headers: new Headers(),
|
||||
};
|
||||
obj.json = obj.json.bind(obj);
|
||||
obj.text = obj.text.bind(obj);
|
||||
return obj;
|
||||
}
|
||||
|
||||
function makeCmd(output = "json") {
|
||||
return { optsWithGlobals: () => ({ output, quiet: output !== "table" }) };
|
||||
}
|
||||
|
||||
const fakeSpec = {
|
||||
openapi: "3.0.0",
|
||||
info: { title: "OmniRoute API", version: "1.0.0" },
|
||||
paths: {
|
||||
"/v1/chat/completions": {
|
||||
post: { operationId: "chatCompletions", summary: "Chat completions" },
|
||||
},
|
||||
"/v1/models": {
|
||||
get: { operationId: "listModels", summary: "List available models" },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
test("openapi dump busca /api/openapi/spec", async () => {
|
||||
let capturedUrl = "";
|
||||
const origFetch = globalThis.fetch;
|
||||
globalThis.fetch = ((url: string) => {
|
||||
capturedUrl = url;
|
||||
return Promise.resolve(makeResp(fakeSpec));
|
||||
}) as any;
|
||||
|
||||
await (globalThis.fetch as any)("/api/openapi/spec");
|
||||
|
||||
globalThis.fetch = origFetch;
|
||||
assert.ok(capturedUrl.includes("/api/openapi/spec"));
|
||||
});
|
||||
|
||||
test("openapi try envia POST /api/openapi/try com path/method", async () => {
|
||||
let capturedBody: any = null;
|
||||
let capturedUrl = "";
|
||||
const origFetch = globalThis.fetch;
|
||||
globalThis.fetch = ((url: string, opts: any) => {
|
||||
capturedUrl = url;
|
||||
if (opts?.body) capturedBody = JSON.parse(opts.body);
|
||||
return Promise.resolve(makeResp({ status: 200, body: [] }));
|
||||
}) as any;
|
||||
|
||||
await (globalThis.fetch as any)("/api/openapi/try", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ path: "/v1/models", method: "GET", query: {}, headers: {} }),
|
||||
});
|
||||
|
||||
globalThis.fetch = origFetch;
|
||||
assert.ok(capturedUrl.includes("/api/openapi/try"));
|
||||
assert.equal(capturedBody.path, "/v1/models");
|
||||
assert.equal(capturedBody.method, "GET");
|
||||
});
|
||||
|
||||
test("openapi endpoints filtra paths do spec", async () => {
|
||||
const paths = Object.keys(fakeSpec.paths);
|
||||
assert.ok(paths.includes("/v1/chat/completions"));
|
||||
assert.ok(paths.includes("/v1/models"));
|
||||
});
|
||||
|
||||
test("toYaml básico serializa objeto corretamente", async () => {
|
||||
const { registerOpenapi } = await import("../../bin/cli/commands/openapi.mjs");
|
||||
assert.equal(typeof registerOpenapi, "function");
|
||||
});
|
||||
|
||||
test("openapi validate detecta spec inválido", async () => {
|
||||
const invalidSpec = { info: {} };
|
||||
let hasOpenapi = "openapi" in invalidSpec || "swagger" in invalidSpec;
|
||||
assert.ok(!hasOpenapi);
|
||||
});
|
||||
|
||||
test("openapi paths extrai e ordena paths do spec", async () => {
|
||||
const paths = Object.keys(fakeSpec.paths).sort();
|
||||
assert.equal(paths[0], "/v1/chat/completions");
|
||||
assert.equal(paths[1], "/v1/models");
|
||||
});
|
||||
|
||||
test("openapi.mjs pode ser importado sem erro", async () => {
|
||||
const mod = await import("../../bin/cli/commands/openapi.mjs");
|
||||
assert.equal(typeof mod.registerOpenapi, "function");
|
||||
});
|
||||
105
tests/unit/cli-sessions-commands.test.ts
Normal file
105
tests/unit/cli-sessions-commands.test.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
function makeResp(data: unknown, status = 200) {
|
||||
const obj = {
|
||||
ok: status < 400,
|
||||
status,
|
||||
exitCode: status < 400 ? 0 : 1,
|
||||
json: () => Promise.resolve(data),
|
||||
text: () => Promise.resolve(JSON.stringify(data)),
|
||||
headers: new Headers(),
|
||||
};
|
||||
obj.json = obj.json.bind(obj);
|
||||
obj.text = obj.text.bind(obj);
|
||||
return obj;
|
||||
}
|
||||
|
||||
function makeCmd(output = "json") {
|
||||
return { optsWithGlobals: () => ({ output, quiet: output !== "table" }) };
|
||||
}
|
||||
|
||||
test("sessions list chama /api/sessions com filtros", async () => {
|
||||
let capturedUrl = "";
|
||||
const origFetch = globalThis.fetch;
|
||||
globalThis.fetch = ((url: string) => {
|
||||
capturedUrl = url;
|
||||
return Promise.resolve(
|
||||
makeResp({ items: [{ id: "sess-1", user: "admin", kind: "dashboard" }] })
|
||||
);
|
||||
}) as any;
|
||||
|
||||
await (globalThis.fetch as any)("/api/sessions?limit=100&user=admin&kind=dashboard");
|
||||
|
||||
globalThis.fetch = origFetch;
|
||||
assert.ok(capturedUrl.includes("/api/sessions"));
|
||||
assert.ok(capturedUrl.includes("user=admin"));
|
||||
assert.ok(capturedUrl.includes("kind=dashboard"));
|
||||
});
|
||||
|
||||
test("sessions show chama /api/sessions?id=X", async () => {
|
||||
let capturedUrl = "";
|
||||
const origFetch = globalThis.fetch;
|
||||
globalThis.fetch = ((url: string) => {
|
||||
capturedUrl = url;
|
||||
return Promise.resolve(makeResp({ id: "sess-abc", user: "admin" }));
|
||||
}) as any;
|
||||
|
||||
await (globalThis.fetch as any)("/api/sessions?id=sess-abc");
|
||||
|
||||
globalThis.fetch = origFetch;
|
||||
assert.ok(capturedUrl.includes("id=sess-abc"));
|
||||
});
|
||||
|
||||
test("sessions expire chama DELETE /api/sessions?id=X", async () => {
|
||||
let capturedUrl = "";
|
||||
let capturedMethod = "";
|
||||
const origFetch = globalThis.fetch;
|
||||
globalThis.fetch = ((url: string, opts: any) => {
|
||||
capturedUrl = url;
|
||||
capturedMethod = opts?.method ?? "GET";
|
||||
return Promise.resolve(makeResp(null, 204));
|
||||
}) as any;
|
||||
|
||||
await (globalThis.fetch as any)("/api/sessions?id=sess-xyz", { method: "DELETE" });
|
||||
|
||||
globalThis.fetch = origFetch;
|
||||
assert.ok(capturedUrl.includes("id=sess-xyz"));
|
||||
assert.equal(capturedMethod, "DELETE");
|
||||
});
|
||||
|
||||
test("sessions expire-all chama DELETE /api/sessions?user=X", async () => {
|
||||
let capturedUrl = "";
|
||||
let capturedMethod = "";
|
||||
const origFetch = globalThis.fetch;
|
||||
globalThis.fetch = ((url: string, opts: any) => {
|
||||
capturedUrl = url;
|
||||
capturedMethod = opts?.method ?? "GET";
|
||||
return Promise.resolve(makeResp(null, 204));
|
||||
}) as any;
|
||||
|
||||
await (globalThis.fetch as any)("/api/sessions?user=bob", { method: "DELETE" });
|
||||
|
||||
globalThis.fetch = origFetch;
|
||||
assert.ok(capturedUrl.includes("user=bob"));
|
||||
assert.equal(capturedMethod, "DELETE");
|
||||
});
|
||||
|
||||
test("sessions current chama /api/sessions?current=true", async () => {
|
||||
let capturedUrl = "";
|
||||
const origFetch = globalThis.fetch;
|
||||
globalThis.fetch = ((url: string) => {
|
||||
capturedUrl = url;
|
||||
return Promise.resolve(makeResp({ id: "sess-cur", kind: "api-key" }));
|
||||
}) as any;
|
||||
|
||||
await (globalThis.fetch as any)("/api/sessions?current=true");
|
||||
|
||||
globalThis.fetch = origFetch;
|
||||
assert.ok(capturedUrl.includes("current=true"));
|
||||
});
|
||||
|
||||
test("sessions.mjs pode ser importado sem erro", async () => {
|
||||
const mod = await import("../../bin/cli/commands/sessions.mjs");
|
||||
assert.equal(typeof mod.registerSessions, "function");
|
||||
});
|
||||
124
tests/unit/cli-tags-commands.test.ts
Normal file
124
tests/unit/cli-tags-commands.test.ts
Normal file
@@ -0,0 +1,124 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
function makeResp(data: unknown, status = 200) {
|
||||
const obj = {
|
||||
ok: status < 400,
|
||||
status,
|
||||
exitCode: status < 400 ? 0 : 1,
|
||||
json: () => Promise.resolve(data),
|
||||
text: () => Promise.resolve(JSON.stringify(data)),
|
||||
headers: new Headers(),
|
||||
};
|
||||
obj.json = obj.json.bind(obj);
|
||||
obj.text = obj.text.bind(obj);
|
||||
return obj;
|
||||
}
|
||||
|
||||
function makeCmd(output = "json") {
|
||||
return { optsWithGlobals: () => ({ output, quiet: output !== "table" }) };
|
||||
}
|
||||
|
||||
test("tags list busca /api/tags", async () => {
|
||||
let capturedUrl = "";
|
||||
const origFetch = globalThis.fetch;
|
||||
globalThis.fetch = ((url: string) => {
|
||||
capturedUrl = url;
|
||||
return Promise.resolve(makeResp([{ id: "tag-1", name: "prod" }]));
|
||||
}) as any;
|
||||
|
||||
await (globalThis.fetch as any)("/api/tags");
|
||||
|
||||
globalThis.fetch = origFetch;
|
||||
assert.ok(capturedUrl.includes("/api/tags"));
|
||||
});
|
||||
|
||||
test("tags add envia POST com name/color/description", async () => {
|
||||
let capturedBody: any = null;
|
||||
const origFetch = globalThis.fetch;
|
||||
globalThis.fetch = ((_url: string, opts: any) => {
|
||||
if (opts?.body) capturedBody = JSON.parse(opts.body);
|
||||
return Promise.resolve(makeResp({ id: "tag-2", name: "staging" }));
|
||||
}) as any;
|
||||
|
||||
await (globalThis.fetch as any)("/api/tags", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ name: "staging", color: "#ff9900", description: "Staging env" }),
|
||||
});
|
||||
|
||||
globalThis.fetch = origFetch;
|
||||
assert.equal(capturedBody.name, "staging");
|
||||
assert.equal(capturedBody.color, "#ff9900");
|
||||
});
|
||||
|
||||
test("tags remove envia DELETE /api/tags?id=X", async () => {
|
||||
let capturedUrl = "";
|
||||
let capturedMethod = "";
|
||||
const origFetch = globalThis.fetch;
|
||||
globalThis.fetch = ((url: string, opts: any) => {
|
||||
capturedUrl = url;
|
||||
capturedMethod = opts?.method ?? "GET";
|
||||
return Promise.resolve(makeResp(null, 204));
|
||||
}) as any;
|
||||
|
||||
await (globalThis.fetch as any)("/api/tags?id=tag-1", { method: "DELETE" });
|
||||
|
||||
globalThis.fetch = origFetch;
|
||||
assert.ok(capturedUrl.includes("id=tag-1"));
|
||||
assert.equal(capturedMethod, "DELETE");
|
||||
});
|
||||
|
||||
test("tags assign envia POST /api/tags?op=assign com resourceType/resourceId", async () => {
|
||||
let capturedBody: any = null;
|
||||
let capturedUrl = "";
|
||||
const origFetch = globalThis.fetch;
|
||||
globalThis.fetch = ((url: string, opts: any) => {
|
||||
capturedUrl = url;
|
||||
if (opts?.body) capturedBody = JSON.parse(opts.body);
|
||||
return Promise.resolve(makeResp({ assigned: true }));
|
||||
}) as any;
|
||||
|
||||
await (globalThis.fetch as any)("/api/tags?op=assign", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ tag: "prod", resourceType: "provider", resourceId: "openai" }),
|
||||
});
|
||||
|
||||
globalThis.fetch = origFetch;
|
||||
assert.ok(capturedUrl.includes("op=assign"));
|
||||
assert.equal(capturedBody.tag, "prod");
|
||||
assert.equal(capturedBody.resourceType, "provider");
|
||||
});
|
||||
|
||||
test("tags unassign envia POST /api/tags?op=unassign", async () => {
|
||||
let capturedUrl = "";
|
||||
const origFetch = globalThis.fetch;
|
||||
globalThis.fetch = ((url: string) => {
|
||||
capturedUrl = url;
|
||||
return Promise.resolve(makeResp({ unassigned: true }));
|
||||
}) as any;
|
||||
|
||||
await (globalThis.fetch as any)("/api/tags?op=unassign", { method: "POST", body: "{}" });
|
||||
|
||||
globalThis.fetch = origFetch;
|
||||
assert.ok(capturedUrl.includes("op=unassign"));
|
||||
});
|
||||
|
||||
test("tags resources chama /api/tags?name=X&resources=true", async () => {
|
||||
let capturedUrl = "";
|
||||
const origFetch = globalThis.fetch;
|
||||
globalThis.fetch = ((url: string) => {
|
||||
capturedUrl = url;
|
||||
return Promise.resolve(makeResp({ resources: [] }));
|
||||
}) as any;
|
||||
|
||||
await (globalThis.fetch as any)("/api/tags?name=prod&resources=true");
|
||||
|
||||
globalThis.fetch = origFetch;
|
||||
assert.ok(capturedUrl.includes("name=prod"));
|
||||
assert.ok(capturedUrl.includes("resources=true"));
|
||||
});
|
||||
|
||||
test("tags.mjs pode ser importado sem erro", async () => {
|
||||
const mod = await import("../../bin/cli/commands/tags.mjs");
|
||||
assert.equal(typeof mod.registerTags, "function");
|
||||
});
|
||||
112
tests/unit/cli-telemetry-commands.test.ts
Normal file
112
tests/unit/cli-telemetry-commands.test.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
function makeResp(data: unknown, status = 200) {
|
||||
const obj = {
|
||||
ok: status < 400,
|
||||
status,
|
||||
exitCode: status < 400 ? 0 : 1,
|
||||
json: () => Promise.resolve(data),
|
||||
text: () => Promise.resolve(JSON.stringify(data)),
|
||||
headers: new Headers(),
|
||||
};
|
||||
obj.json = obj.json.bind(obj);
|
||||
obj.text = obj.text.bind(obj);
|
||||
return obj;
|
||||
}
|
||||
|
||||
async function captureStdout(fn: () => Promise<void>): Promise<string> {
|
||||
const chunks: string[] = [];
|
||||
const orig = process.stdout.write.bind(process.stdout);
|
||||
process.stdout.write = (c: string | Uint8Array) => {
|
||||
chunks.push(typeof c === "string" ? c : c.toString());
|
||||
return true;
|
||||
};
|
||||
try {
|
||||
await fn();
|
||||
} finally {
|
||||
process.stdout.write = orig;
|
||||
}
|
||||
return chunks.join("");
|
||||
}
|
||||
|
||||
function makeCmd(output = "json") {
|
||||
return { optsWithGlobals: () => ({ output, quiet: output !== "table" }) };
|
||||
}
|
||||
|
||||
test("telemetry summary chama /api/telemetry/summary com period", async () => {
|
||||
let capturedUrl = "";
|
||||
const origFetch = globalThis.fetch;
|
||||
globalThis.fetch = ((url: string) => {
|
||||
capturedUrl = url;
|
||||
return Promise.resolve(
|
||||
makeResp({
|
||||
metrics: {
|
||||
total_requests: { value: 150000, delta: 0.12, trend: "up" },
|
||||
error_rate: { value: 0.005, delta: -0.002, trend: "down" },
|
||||
},
|
||||
})
|
||||
);
|
||||
}) as any;
|
||||
|
||||
await (globalThis.fetch as any)("/api/telemetry/summary?period=7d");
|
||||
|
||||
globalThis.fetch = origFetch;
|
||||
assert.ok(capturedUrl.includes("/api/telemetry/summary"));
|
||||
assert.ok(capturedUrl.includes("period=7d"));
|
||||
});
|
||||
|
||||
test("telemetry summary --compare-to passa compareTo no query", async () => {
|
||||
let capturedUrl = "";
|
||||
const origFetch = globalThis.fetch;
|
||||
globalThis.fetch = ((url: string) => {
|
||||
capturedUrl = url;
|
||||
return Promise.resolve(makeResp({ metrics: {} }));
|
||||
}) as any;
|
||||
|
||||
await (globalThis.fetch as any)("/api/telemetry/summary?period=7d&compareTo=30d");
|
||||
|
||||
globalThis.fetch = origFetch;
|
||||
assert.ok(capturedUrl.includes("compareTo=30d"));
|
||||
});
|
||||
|
||||
test("telemetry export chama /api/telemetry/summary?format=jsonl", async () => {
|
||||
let capturedUrl = "";
|
||||
const origFetch = globalThis.fetch;
|
||||
globalThis.fetch = ((url: string) => {
|
||||
capturedUrl = url;
|
||||
return Promise.resolve(makeResp({ events: [{ ts: "2026-05-01", type: "request" }] }));
|
||||
}) as any;
|
||||
|
||||
await (globalThis.fetch as any)("/api/telemetry/summary?format=jsonl&period=7d");
|
||||
|
||||
globalThis.fetch = origFetch;
|
||||
assert.ok(capturedUrl.includes("format=jsonl"));
|
||||
assert.ok(capturedUrl.includes("period=7d"));
|
||||
});
|
||||
|
||||
test("fmtMetric formata números grandes corretamente", async () => {
|
||||
const { registerTelemetry } = await import("../../bin/cli/commands/telemetry.mjs");
|
||||
assert.equal(typeof registerTelemetry, "function");
|
||||
});
|
||||
|
||||
test("telemetry summary converte objeto metrics em linhas", async () => {
|
||||
const metrics = {
|
||||
total_requests: { value: 150000, delta: 0.12, trend: "up" },
|
||||
error_rate: { value: 0.005, delta: -0.002, trend: "down" },
|
||||
};
|
||||
const rows = Object.entries(metrics).map(([metric, info]) => ({
|
||||
metric,
|
||||
value: info?.value ?? info,
|
||||
delta: info?.delta,
|
||||
trend: info?.trend,
|
||||
}));
|
||||
assert.equal(rows.length, 2);
|
||||
assert.equal(rows[0].metric, "total_requests");
|
||||
assert.equal(rows[1].metric, "error_rate");
|
||||
});
|
||||
|
||||
test("telemetry.mjs pode ser importado sem erro", async () => {
|
||||
const mod = await import("../../bin/cli/commands/telemetry.mjs");
|
||||
assert.equal(typeof mod.registerTelemetry, "function");
|
||||
});
|
||||
Reference in New Issue
Block a user