mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-04 22:32:12 +03:00
* feat(cli-tools): add all Hermes Agent auxiliary model roles Extend HERMES_AGENT_ROLES from 7 to 18 slots to match the full auxiliary.* set in Hermes Agent config.yaml: - add: mcp, title_generation, memory_query_rewrite, tts_audio_tags, triage_specifier, kanban_decomposer, profile_describer, goal_judge, curator, monitor, background_review - reorder: web_extract before compression (match upstream docs) Backend generator/reader are generic on auxiliary.<role> — only the role catalog, UI card, and i18n (en + pl) needed updating. * fix(i18n): translate the 12 new Hermes auxiliary roles into vi and pt-BR The 22 new en.json keys landed only in pl.json, breaking the vi and pt-BR key-parity guards. Adds the same keys with real translations (no placeholder strings), keeping both parity assertions exact. Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com> * test(cli-helper): guard HERMES role catalog parity between backend and UI Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com> Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
522 lines
21 KiB
TypeScript
522 lines
21 KiB
TypeScript
import { describe, it } from "node:test";
|
|
import assert from "node:assert";
|
|
import { readFileSync } from "node:fs";
|
|
import { fileURLToPath } from "node:url";
|
|
import * as generator from "../../../src/lib/cli-helper/config-generator/index.ts";
|
|
|
|
// The UI's HERMES_ROLES catalog (HermesAgentToolCard.tsx) is a "use client" component
|
|
// module — importing it in the Node test runner would pull in React/JSX. Instead we
|
|
// extract the id list straight from source text, which is enough to diff catalogs
|
|
// without executing the component.
|
|
function readUiHermesRoleIds(): string[] {
|
|
const uiFilePath = fileURLToPath(
|
|
new URL(
|
|
"../../../src/app/(dashboard)/dashboard/cli-code/components/HermesAgentToolCard.tsx",
|
|
import.meta.url
|
|
)
|
|
);
|
|
const source = readFileSync(uiFilePath, "utf-8");
|
|
const arrayMatch = source.match(/const HERMES_ROLES: Role\[\] = \[([\s\S]*?)\n\];/);
|
|
assert.ok(arrayMatch, "could not locate HERMES_ROLES array in HermesAgentToolCard.tsx");
|
|
const body = arrayMatch[1];
|
|
return Array.from(body.matchAll(/id:\s*"([a-z0-9_]+)"/g)).map((m) => m[1]);
|
|
}
|
|
|
|
function readEnMessages(): { cliTools?: Record<string, string> } {
|
|
const enJsonPath = fileURLToPath(
|
|
new URL("../../../src/i18n/messages/en.json", import.meta.url)
|
|
);
|
|
return JSON.parse(readFileSync(enJsonPath, "utf-8"));
|
|
}
|
|
|
|
describe("config-generator", () => {
|
|
describe("validateBaseUrl", () => {
|
|
it("accepts http URLs", async () => {
|
|
const mod = await import("../../../src/lib/cli-helper/config-generator/index.ts");
|
|
assert.strictEqual(mod.validateBaseUrl("http://localhost:20128"), true);
|
|
});
|
|
|
|
it("accepts https URLs", async () => {
|
|
const mod = await import("../../../src/lib/cli-helper/config-generator/index.ts");
|
|
assert.strictEqual(mod.validateBaseUrl("https://example.com"), true);
|
|
});
|
|
|
|
it("rejects non-URL strings", async () => {
|
|
const mod = await import("../../../src/lib/cli-helper/config-generator/index.ts");
|
|
assert.strictEqual(mod.validateBaseUrl("not-a-url"), false);
|
|
});
|
|
});
|
|
|
|
describe("assertSafeCatalogUrl (SSRF guard, CodeQL #326)", () => {
|
|
it("allows the loopback OmniRoute target (the legitimate default) and returns a URL", async () => {
|
|
const { assertSafeCatalogUrl } = await import(
|
|
"../../../src/lib/cli-helper/config-generator/opencode.ts"
|
|
);
|
|
// The catalog source IS the user's own OmniRoute — localhost must stay allowed.
|
|
assert.doesNotThrow(() => assertSafeCatalogUrl("http://localhost:20128/v1/models"));
|
|
assert.doesNotThrow(() => assertSafeCatalogUrl("http://127.0.0.1:20128/v1/models"));
|
|
// Returns the validated, re-parsed URL (taint-severed value the caller fetches).
|
|
const safe = assertSafeCatalogUrl("http://localhost:20128/v1/models");
|
|
assert.ok(safe instanceof URL);
|
|
assert.equal(safe.href, "http://localhost:20128/v1/models");
|
|
});
|
|
|
|
it("allows a public OmniRoute Cloud target", async () => {
|
|
const { assertSafeCatalogUrl } = await import(
|
|
"../../../src/lib/cli-helper/config-generator/opencode.ts"
|
|
);
|
|
assert.doesNotThrow(() => assertSafeCatalogUrl("https://api.omniroute.online/v1/models"));
|
|
});
|
|
|
|
it("blocks the cloud-metadata SSRF→IAM pivot (169.254.169.254)", async () => {
|
|
const { assertSafeCatalogUrl } = await import(
|
|
"../../../src/lib/cli-helper/config-generator/opencode.ts"
|
|
);
|
|
assert.throws(() => assertSafeCatalogUrl("http://169.254.169.254/v1/models"));
|
|
assert.throws(() =>
|
|
assertSafeCatalogUrl("http://metadata.google.internal/v1/models")
|
|
);
|
|
});
|
|
|
|
it("blocks non-http(s) protocols and embedded credentials", async () => {
|
|
const { assertSafeCatalogUrl } = await import(
|
|
"../../../src/lib/cli-helper/config-generator/opencode.ts"
|
|
);
|
|
assert.throws(() => assertSafeCatalogUrl("file:///etc/passwd"));
|
|
assert.throws(() => assertSafeCatalogUrl("http://user:pass@example.com/v1/models"));
|
|
});
|
|
});
|
|
|
|
describe("generateConfig", () => {
|
|
it("returns error for invalid baseUrl", async () => {
|
|
const result = await generator.generateConfig("claude", {
|
|
baseUrl: "invalid",
|
|
apiKey: "sk-xxx",
|
|
});
|
|
assert.strictEqual(result.success, false);
|
|
assert.ok(result.error?.includes("Invalid baseUrl"));
|
|
});
|
|
|
|
it("returns error for empty apiKey", async () => {
|
|
const result = await generator.generateConfig("claude", {
|
|
baseUrl: "http://localhost:20128",
|
|
apiKey: "",
|
|
});
|
|
assert.strictEqual(result.success, false);
|
|
assert.ok(result.error?.includes("API key"));
|
|
});
|
|
|
|
it("returns success for valid claude config", async () => {
|
|
// This may fail if the claude generator has issues - just ensure error handling works
|
|
const result = await generator.generateConfig("claude", {
|
|
baseUrl: "http://localhost:20128",
|
|
apiKey: "sk-test",
|
|
});
|
|
// Either success or error (if generator missing), but check structure is correct
|
|
assert.ok("success" in result);
|
|
assert.ok("configPath" in result);
|
|
});
|
|
|
|
it("returns success for valid hermes config", async () => {
|
|
const result = await generator.generateConfig("hermes", {
|
|
baseUrl: "http://localhost:20128",
|
|
apiKey: "sk-test",
|
|
model: "gpt-5.4-mini",
|
|
});
|
|
assert.strictEqual(result.success, true);
|
|
assert.ok(result.configPath.endsWith(".hermes/config.yaml"));
|
|
assert.ok(String(result.content || "").includes("providers:"));
|
|
assert.ok(String(result.content || "").includes("omniroute"));
|
|
});
|
|
|
|
it("returns error for unknown tool", async () => {
|
|
const result = await generator.generateConfig("unknown-tool-xyz", {
|
|
baseUrl: "http://localhost:20128",
|
|
apiKey: "sk-xxx",
|
|
});
|
|
assert.strictEqual(result.success, false);
|
|
assert.ok(result.error?.includes("Unknown tool"));
|
|
});
|
|
});
|
|
|
|
describe("generateAllConfigs", () => {
|
|
it("returns array of GenerateResult for all tools", async () => {
|
|
const results = await generator.generateAllConfigs({
|
|
baseUrl: "http://localhost:20128",
|
|
apiKey: "sk-xxx",
|
|
});
|
|
assert.ok(Array.isArray(results));
|
|
assert.strictEqual(results.length, 7); // claude, codex, opencode, cline, kilocode, continue, hermes
|
|
});
|
|
});
|
|
|
|
describe("hermes-agent (rich multi-role)", () => {
|
|
it("exports HERMES_AGENT_ROLES with expected roles", async () => {
|
|
const hermesAgent =
|
|
await import("../../../src/lib/cli-helper/config-generator/hermes-agent.ts");
|
|
assert.ok(Array.isArray(hermesAgent.HERMES_AGENT_ROLES));
|
|
const ids = hermesAgent.HERMES_AGENT_ROLES.map((r: any) => r.id);
|
|
assert.ok(ids.includes("default"));
|
|
assert.ok(ids.includes("delegation"));
|
|
assert.ok(ids.includes("vision"));
|
|
assert.ok(ids.includes("approval"));
|
|
// Full catalog, including the 11 auxiliary roles added alongside HERMES_AGENT_ROLES
|
|
// (mcp, title_generation, memory_query_rewrite, tts_audio_tags, triage_specifier,
|
|
// kanban_decomposer, profile_describer, goal_judge, curator, monitor,
|
|
// background_review). Listed explicitly (not just parity-diffed against the UI
|
|
// below) so a role dropped from BOTH catalogs at once still fails this test.
|
|
const expectedIds = [
|
|
"default",
|
|
"delegation",
|
|
"vision",
|
|
"web_extract",
|
|
"compression",
|
|
"skills_hub",
|
|
"approval",
|
|
"mcp",
|
|
"title_generation",
|
|
"memory_query_rewrite",
|
|
"tts_audio_tags",
|
|
"triage_specifier",
|
|
"kanban_decomposer",
|
|
"profile_describer",
|
|
"goal_judge",
|
|
"curator",
|
|
"monitor",
|
|
"background_review",
|
|
];
|
|
assert.deepStrictEqual([...ids].sort(), [...expectedIds].sort());
|
|
});
|
|
|
|
it("keeps the backend HERMES_AGENT_ROLES catalog in sync with the UI's HERMES_ROLES catalog", async () => {
|
|
// The UI (HermesAgentToolCard.tsx) maintains its own parallel role catalog for
|
|
// rendering the role dropdowns. Nothing at the type level keeps the two catalogs
|
|
// in sync, so a role added to one and not the other would ship silently (the
|
|
// backend would accept a role the UI never offers, or the UI would offer a role
|
|
// the backend config generator doesn't know how to place in the YAML). Diffing
|
|
// the id lists turns that drift into a CI failure instead.
|
|
const hermesAgent =
|
|
await import("../../../src/lib/cli-helper/config-generator/hermes-agent.ts");
|
|
const backendIds: string[] = hermesAgent.HERMES_AGENT_ROLES.map((r) => r.id);
|
|
const uiIds = readUiHermesRoleIds();
|
|
|
|
const missingFromUi = backendIds.filter((id) => !uiIds.includes(id));
|
|
const missingFromBackend = uiIds.filter((id) => !backendIds.includes(id));
|
|
|
|
assert.deepStrictEqual(
|
|
missingFromUi,
|
|
[],
|
|
`role ids present in backend HERMES_AGENT_ROLES but missing from UI HERMES_ROLES: ${missingFromUi.join(", ")}`
|
|
);
|
|
assert.deepStrictEqual(
|
|
missingFromBackend,
|
|
[],
|
|
`role ids present in UI HERMES_ROLES but missing from backend HERMES_AGENT_ROLES: ${missingFromBackend.join(", ")}`
|
|
);
|
|
});
|
|
|
|
it("resolves labelKey/descriptionKey for every HERMES_AGENT_ROLES id in en.json's cliTools namespace", async () => {
|
|
// Generalizes the exact bug class the contributor had to hand-fix in their 2nd
|
|
// commit (missing vi/pt-BR translations for the new roles): every role's
|
|
// labelKey/descriptionKey must exist as a real key under cliTools in en.json,
|
|
// the source-of-truth locale, or the UI silently renders the raw key string.
|
|
const uiFilePath = fileURLToPath(
|
|
new URL(
|
|
"../../../src/app/(dashboard)/dashboard/cli-code/components/HermesAgentToolCard.tsx",
|
|
import.meta.url
|
|
)
|
|
);
|
|
const source = readFileSync(uiFilePath, "utf-8");
|
|
const arrayMatch = source.match(/const HERMES_ROLES: Role\[\] = \[([\s\S]*?)\n\];/);
|
|
assert.ok(arrayMatch, "could not locate HERMES_ROLES array in HermesAgentToolCard.tsx");
|
|
const body = arrayMatch[1];
|
|
const roleEntries = Array.from(
|
|
body.matchAll(/id:\s*"([a-z0-9_]+)"[\s\S]*?labelKey:\s*"([A-Za-z0-9]+)"[\s\S]*?descriptionKey:\s*"([A-Za-z0-9]+)"/g)
|
|
).map((m) => ({ id: m[1], labelKey: m[2], descriptionKey: m[3] }));
|
|
assert.ok(roleEntries.length > 0, "expected at least one role entry to be parsed");
|
|
|
|
const en = readEnMessages();
|
|
const cliTools = en.cliTools || {};
|
|
const missing: string[] = [];
|
|
for (const { id, labelKey, descriptionKey } of roleEntries) {
|
|
if (typeof cliTools[labelKey] !== "string") {
|
|
missing.push(`${id}: labelKey "${labelKey}"`);
|
|
}
|
|
if (typeof cliTools[descriptionKey] !== "string") {
|
|
missing.push(`${id}: descriptionKey "${descriptionKey}"`);
|
|
}
|
|
}
|
|
assert.deepStrictEqual(missing, [], `missing en.json cliTools keys: ${missing.join("; ")}`);
|
|
});
|
|
|
|
it("getCurrentHermesAgentRoles returns an object", async () => {
|
|
const hermesAgent =
|
|
await import("../../../src/lib/cli-helper/config-generator/hermes-agent.ts");
|
|
const roles = await hermesAgent.getCurrentHermesAgentRoles();
|
|
assert.ok(typeof roles === "object" && roles !== null);
|
|
});
|
|
|
|
it("generateHermesAgentConfig returns yaml string for valid payload", async () => {
|
|
const hermesAgent =
|
|
await import("../../../src/lib/cli-helper/config-generator/hermes-agent.ts");
|
|
const result = await hermesAgent.generateHermesAgentConfig({
|
|
baseUrl: "http://localhost:20128",
|
|
apiKey: "sk-test-omniroute",
|
|
selections: [
|
|
{ role: "default", model: "gpt-4o" },
|
|
{ role: "delegation", model: "claude-3-5-sonnet" },
|
|
{ role: "vision", model: "gpt-4o" },
|
|
],
|
|
});
|
|
|
|
assert.ok(!result.error);
|
|
assert.ok(typeof result.yaml === "string");
|
|
assert.ok(result.yaml.length > 50);
|
|
assert.ok(result.yaml.includes("provider: omniroute"));
|
|
});
|
|
|
|
it("generateHermesAgentConfig includes auxiliary section for non-default roles", async () => {
|
|
const hermesAgent =
|
|
await import("../../../src/lib/cli-helper/config-generator/hermes-agent.ts");
|
|
const result = await hermesAgent.generateHermesAgentConfig({
|
|
baseUrl: "http://localhost:20128",
|
|
apiKey: "sk-test",
|
|
selections: [
|
|
{ role: "compression", model: "test-model" },
|
|
{ role: "skills_hub", model: "test-model-2" },
|
|
],
|
|
});
|
|
|
|
assert.ok(result.yaml.includes("auxiliary:"));
|
|
assert.ok(result.yaml.includes("compression:"));
|
|
});
|
|
|
|
it("generateHermesAgentConfig returns error when baseUrl is missing", async () => {
|
|
const hermesAgent =
|
|
await import("../../../src/lib/cli-helper/config-generator/hermes-agent.ts");
|
|
const result = await hermesAgent.generateHermesAgentConfig({
|
|
baseUrl: "",
|
|
selections: [{ role: "default", model: "x" }],
|
|
} as any);
|
|
|
|
assert.ok(result.error);
|
|
assert.ok(result.error.includes("baseUrl"));
|
|
});
|
|
|
|
it("generateHermesAgentConfig correctly structures delegation and auxiliary roles", async () => {
|
|
const hermesAgent =
|
|
await import("../../../src/lib/cli-helper/config-generator/hermes-agent.ts");
|
|
const result = await hermesAgent.generateHermesAgentConfig({
|
|
baseUrl: "http://localhost:20128",
|
|
apiKey: "sk-test",
|
|
selections: [
|
|
{ role: "default", model: "model-default" },
|
|
{ role: "delegation", model: "model-delegation" },
|
|
{ role: "approval", model: "model-approval" },
|
|
],
|
|
});
|
|
|
|
const yaml = result.yaml;
|
|
assert.ok(yaml.includes("model:"));
|
|
assert.ok(yaml.includes("default: model-default"));
|
|
assert.ok(yaml.includes("delegation:"));
|
|
assert.ok(yaml.includes("auxiliary:"));
|
|
assert.ok(yaml.includes("approval:"));
|
|
});
|
|
|
|
it("generateHermesAgentConfig performs non-destructive merge (preserves other keys)", async () => {
|
|
// This test mainly verifies the function doesn't blow away unrelated config
|
|
const hermesAgent =
|
|
await import("../../../src/lib/cli-helper/config-generator/hermes-agent.ts");
|
|
const result = await hermesAgent.generateHermesAgentConfig({
|
|
baseUrl: "http://localhost:20128",
|
|
apiKey: "sk-test",
|
|
selections: [{ role: "default", model: "new-model" }],
|
|
});
|
|
|
|
// Should still contain providers block and the new model
|
|
assert.ok(result.yaml.includes("providers:"));
|
|
assert.ok(result.yaml.includes("new-model"));
|
|
});
|
|
});
|
|
|
|
describe("opencode (context-aware)", () => {
|
|
/**
|
|
* The catalog is the single source of truth for context windows —
|
|
* we never fabricate a default. Tests below pin this contract.
|
|
*/
|
|
function makeCatalogResponse(models: unknown[]): unknown {
|
|
return { object: "list", data: models };
|
|
}
|
|
|
|
const SAMPLE_CATALOG: unknown[] = [
|
|
{ id: "ds/deepseek-v4-flash", owned_by: "deepseek", context_length: 1_000_000, max_input_tokens: 1_000_000 },
|
|
{ id: "llama3", owned_by: "llama", max_context_window_tokens: 8192 },
|
|
{ id: "MASTER", owned_by: "combo", context_length: 131072, max_input_tokens: 131072 },
|
|
{ id: "Opencode FREE Omni", owned_by: "combo", context_length: 200000, max_input_tokens: 160000 },
|
|
// Combo whose targets have no known context — generator must NOT
|
|
// fabricate a default. The model is emitted without limit.context.
|
|
{ id: "NO_CTX_COMBO", owned_by: "combo" },
|
|
];
|
|
|
|
function stubFetchOnce(body: unknown, status = 200) {
|
|
const original = globalThis.fetch;
|
|
let calls = 0;
|
|
// @ts-ignore — globalThis.fetch signature is compatible for our purposes
|
|
globalThis.fetch = (async (url: string | URL, init?: RequestInit) => {
|
|
calls += 1;
|
|
return new Response(JSON.stringify(body), {
|
|
status,
|
|
headers: { "content-type": "application/json" },
|
|
});
|
|
}) as typeof fetch;
|
|
return {
|
|
calls: () => calls,
|
|
restore: () => {
|
|
globalThis.fetch = original;
|
|
},
|
|
};
|
|
}
|
|
|
|
it("emits limit.context from the catalog (no hardcoded fallback)", async () => {
|
|
const stub = stubFetchOnce(makeCatalogResponse(SAMPLE_CATALOG));
|
|
try {
|
|
const { generateOpencodeConfig } = await import(
|
|
"../../../src/lib/cli-helper/config-generator/opencode.ts"
|
|
);
|
|
const out = await generateOpencodeConfig({
|
|
baseUrl: "http://localhost:20128",
|
|
apiKey: "sk-test",
|
|
});
|
|
const cfg = JSON.parse(out);
|
|
const models = cfg.provider.omniroute.models;
|
|
assert.strictEqual(models["ds/deepseek-v4-flash"].limit.context, 1_000_000);
|
|
assert.strictEqual(models["MASTER"].limit.context, 131072);
|
|
// Combo with min-of-targets 200K: must reflect the catalog's value,
|
|
// not a hardcoded 128K.
|
|
assert.strictEqual(models["Opencode FREE Omni"].limit.context, 200000);
|
|
} finally {
|
|
stub.restore();
|
|
}
|
|
});
|
|
|
|
it("does NOT fabricate a default context when the catalog has no entry", async () => {
|
|
const stub = stubFetchOnce(makeCatalogResponse(SAMPLE_CATALOG));
|
|
try {
|
|
const { generateOpencodeConfig } = await import(
|
|
"../../../src/lib/cli-helper/config-generator/opencode.ts"
|
|
);
|
|
const out = await generateOpencodeConfig({
|
|
baseUrl: "http://localhost:20128",
|
|
apiKey: "sk-test",
|
|
});
|
|
const cfg = JSON.parse(out);
|
|
// NO_CTX_COMBO has no context_length in the catalog — generator
|
|
// must NOT default to 128K (or any other value). The entry is
|
|
// emitted without limit.context so OpenCode's own heuristic
|
|
// applies and the user can fix the upstream.
|
|
const noCtx = cfg.provider.omniroute.models["NO_CTX_COMBO"];
|
|
assert.strictEqual(
|
|
noCtx.limit?.context,
|
|
undefined,
|
|
`NO_CTX_COMBO should not have a fabricated limit.context (got ${noCtx.limit?.context})`
|
|
);
|
|
} finally {
|
|
stub.restore();
|
|
}
|
|
});
|
|
|
|
it("prefers max_context_window_tokens when context_length is absent", async () => {
|
|
const stub = stubFetchOnce(makeCatalogResponse(SAMPLE_CATALOG));
|
|
try {
|
|
const { generateOpencodeConfig } = await import(
|
|
"../../../src/lib/cli-helper/config-generator/opencode.ts"
|
|
);
|
|
const out = await generateOpencodeConfig({
|
|
baseUrl: "http://localhost:20128",
|
|
apiKey: "sk-test",
|
|
});
|
|
const cfg = JSON.parse(out);
|
|
assert.strictEqual(cfg.provider.omniroute.models.llama3.limit.context, 8192);
|
|
} finally {
|
|
stub.restore();
|
|
}
|
|
});
|
|
|
|
it("THROWS when the catalog fetch fails (no silent stale config)", async () => {
|
|
// When the catalog fetch fails, the generator MUST throw rather than
|
|
// emit a config with fabricated values. The CLI catches the error
|
|
// and surfaces it to the user; the user's existing opencode.json is
|
|
// left untouched.
|
|
const original = globalThis.fetch;
|
|
// @ts-ignore
|
|
globalThis.fetch = (async () => {
|
|
throw new Error("ECONNREFUSED");
|
|
}) as typeof fetch;
|
|
try {
|
|
const { generateOpencodeConfig } = await import(
|
|
"../../../src/lib/cli-helper/config-generator/opencode.ts"
|
|
);
|
|
let threw = false;
|
|
try {
|
|
await generateOpencodeConfig({
|
|
baseUrl: "http://localhost:20128",
|
|
apiKey: "sk-test",
|
|
});
|
|
} catch (e) {
|
|
threw = true;
|
|
assert.ok(
|
|
/catalog|fetch|ECONNREFUSED/i.test(String(e?.message ?? e)),
|
|
`Expected fetch error, got: ${String(e?.message ?? e)}`
|
|
);
|
|
}
|
|
assert.ok(threw, "generator must throw when catalog fetch fails");
|
|
} finally {
|
|
globalThis.fetch = original;
|
|
}
|
|
});
|
|
|
|
it("writes a top-level model prefixed with provider id when options.model is supplied", async () => {
|
|
const stub = stubFetchOnce(makeCatalogResponse(SAMPLE_CATALOG));
|
|
try {
|
|
const { generateOpencodeConfig } = await import(
|
|
"../../../src/lib/cli-helper/config-generator/opencode.ts"
|
|
);
|
|
const out = await generateOpencodeConfig({
|
|
baseUrl: "http://localhost:20128",
|
|
apiKey: "sk-test",
|
|
model: "MASTER",
|
|
});
|
|
const cfg = JSON.parse(out);
|
|
assert.strictEqual(cfg.model, "omniroute/MASTER");
|
|
} finally {
|
|
stub.restore();
|
|
}
|
|
});
|
|
|
|
it("auto-pulls the Opencode FREE Omni combo context (the user-reported case)", async () => {
|
|
// Regression guard: the catalog's min-of-targets for combos must be
|
|
// reflected verbatim. No hardcoded 128K, no fallback that overrides
|
|
// the catalog's actual value.
|
|
const stub = stubFetchOnce(makeCatalogResponse(SAMPLE_CATALOG));
|
|
try {
|
|
const { generateOpencodeConfig } = await import(
|
|
"../../../src/lib/cli-helper/config-generator/opencode.ts"
|
|
);
|
|
const out = await generateOpencodeConfig({
|
|
baseUrl: "http://localhost:20128",
|
|
apiKey: "sk-test",
|
|
});
|
|
const cfg = JSON.parse(out);
|
|
assert.strictEqual(
|
|
cfg.provider.omniroute.models["Opencode FREE Omni"].limit.context,
|
|
200000,
|
|
"Opencode FREE Omni must have context=200000 from the catalog, not 128000"
|
|
);
|
|
} finally {
|
|
stub.restore();
|
|
}
|
|
});
|
|
});
|
|
});
|