mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
OC 1.17.8+ native-adapter gate (Ji() in bundled client) rejects any
model whose providerID is not exactly 'openai', 'anthropic', or does
not start with 'opencode'. The plugin's default providerId ('omniroute')
fails this gate, so every model dispatch returns 'Unable to determine
provider'. Auto-prefix in resolveOmniRoutePluginOptions so existing
opencode.json configs (providerId: 'omniroute') keep working without
user-facing changes — internally every catalog key, ModelV2 field,
and auth hook sees 'opencode-omniroute'.
The prefix also fixes the auth.json key mismatch: the resolved
(opencode-omniroute) key is what OC 1.17.8+ dispatcher reads when
looking up credentials.
712 lines
26 KiB
TypeScript
712 lines
26 KiB
TypeScript
/**
|
|
* T-05 combo-discovery contract tests.
|
|
*
|
|
* Covers:
|
|
* - `defaultOmniRouteCombosFetcher(baseURL, apiKey, timeoutMs?)`
|
|
* — envelope tolerance (`{combos: [...]}` and bare array), non-2xx errors.
|
|
* - `mapComboToModelV2(combo, members, providerId, baseURL)`
|
|
* — LCD policy across capabilities, limits, modalities; defensive
|
|
* posture on empty members; nice-name preference.
|
|
* - `createOmniRouteProviderHook(opts, deps)` extension
|
|
* — combos merged into the models map; collision resolution (combo
|
|
* wins, warn-once); soft-fail when the combos fetcher throws;
|
|
* combos cached + reused under the same TTL key as models.
|
|
*
|
|
* Mocking strategy mirrors `provider.test.ts`: both fetchers are
|
|
* dependency-injected at hook construction, no `fetch` monkey-patch.
|
|
*/
|
|
|
|
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import {
|
|
createOmniRouteProviderHook,
|
|
defaultOmniRouteCombosFetcher,
|
|
mapComboToModelV2,
|
|
type OmniRouteCombosFetcher,
|
|
type OmniRouteModelsFetcher,
|
|
type OmniRouteRawCombo,
|
|
type OmniRouteRawModelEntry,
|
|
} from "../src/index.js";
|
|
|
|
// ────────────────────────────────────────────────────────────────────────────
|
|
// Fixtures
|
|
// ────────────────────────────────────────────────────────────────────────────
|
|
|
|
const MODEL_PRIMARY: OmniRouteRawModelEntry = {
|
|
id: "claude-primary",
|
|
capabilities: {
|
|
tool_calling: true,
|
|
reasoning: true,
|
|
vision: true,
|
|
thinking: true,
|
|
temperature: true,
|
|
},
|
|
context_length: 200_000,
|
|
max_output_tokens: 64_000,
|
|
max_input_tokens: 180_000,
|
|
input_modalities: ["text", "image"],
|
|
output_modalities: ["text"],
|
|
};
|
|
|
|
const MODEL_SECONDARY: OmniRouteRawModelEntry = {
|
|
id: "claude-secondary",
|
|
capabilities: {
|
|
tool_calling: true,
|
|
reasoning: false,
|
|
vision: true,
|
|
thinking: false,
|
|
temperature: true,
|
|
},
|
|
context_length: 100_000,
|
|
max_output_tokens: 32_000,
|
|
max_input_tokens: 96_000,
|
|
input_modalities: ["text", "image"],
|
|
output_modalities: ["text"],
|
|
};
|
|
|
|
const MODEL_NO_TOOLS: OmniRouteRawModelEntry = {
|
|
id: "gemini-3-flash",
|
|
capabilities: { tool_calling: false, reasoning: false, vision: false, thinking: false },
|
|
context_length: 1_000_000,
|
|
max_output_tokens: 8_192,
|
|
input_modalities: ["text"],
|
|
output_modalities: ["text"],
|
|
};
|
|
|
|
const COMBO_CLAUDE_TIER: OmniRouteRawCombo = {
|
|
id: "combo-claude-tier",
|
|
name: "Claude Tier",
|
|
strategy: "priority",
|
|
models: [
|
|
{ id: "s1", kind: "model", model: "claude-primary", weight: 100 },
|
|
{ id: "s2", kind: "model", model: "claude-secondary", weight: 80 },
|
|
],
|
|
};
|
|
|
|
// ────────────────────────────────────────────────────────────────────────────
|
|
// Helpers
|
|
// ────────────────────────────────────────────────────────────────────────────
|
|
|
|
function stubModelsFetcher(
|
|
payload: OmniRouteRawModelEntry[]
|
|
): OmniRouteModelsFetcher & { callCount: () => number } {
|
|
let n = 0;
|
|
const f: OmniRouteModelsFetcher = async () => {
|
|
n++;
|
|
return payload;
|
|
};
|
|
return Object.assign(f, { callCount: () => n });
|
|
}
|
|
|
|
function stubCombosFetcher(
|
|
payload: OmniRouteRawCombo[]
|
|
): OmniRouteCombosFetcher & { callCount: () => number; callsBy: () => Array<[string, string]> } {
|
|
let n = 0;
|
|
const calls: Array<[string, string]> = [];
|
|
const f: OmniRouteCombosFetcher = async (baseURL, apiKey) => {
|
|
n++;
|
|
calls.push([baseURL, apiKey]);
|
|
return payload;
|
|
};
|
|
return Object.assign(f, {
|
|
callCount: () => n,
|
|
callsBy: () => calls,
|
|
});
|
|
}
|
|
|
|
function failingCombosFetcher(
|
|
err = new Error("boom")
|
|
): OmniRouteCombosFetcher & { callCount: () => number } {
|
|
let n = 0;
|
|
const f: OmniRouteCombosFetcher = async () => {
|
|
n++;
|
|
throw err;
|
|
};
|
|
return Object.assign(f, { callCount: () => n });
|
|
}
|
|
|
|
const apiAuth = (key: string): unknown => ({ type: "api", key });
|
|
|
|
// Capture console.warn invocations for the duration of a callback, then
|
|
// restore the original. Needed because the collision + soft-fail paths
|
|
// emit warnings we want to assert on.
|
|
async function withWarnCapture<T>(
|
|
fn: (warnings: Array<{ args: unknown[] }>) => Promise<T>
|
|
): Promise<{ result: T; warnings: Array<{ args: unknown[] }> }> {
|
|
const original = console.warn;
|
|
const warnings: Array<{ args: unknown[] }> = [];
|
|
console.warn = (...args: unknown[]) => {
|
|
warnings.push({ args });
|
|
};
|
|
try {
|
|
const result = await fn(warnings);
|
|
return { result, warnings };
|
|
} finally {
|
|
console.warn = original;
|
|
}
|
|
}
|
|
|
|
// ────────────────────────────────────────────────────────────────────────────
|
|
// defaultOmniRouteCombosFetcher — envelope tolerance + error surfacing
|
|
// ────────────────────────────────────────────────────────────────────────────
|
|
|
|
test("defaultOmniRouteCombosFetcher: parses {combos:[…]} envelope", async () => {
|
|
const originalFetch = globalThis.fetch;
|
|
globalThis.fetch = (async (input: unknown) => {
|
|
const url = typeof input === "string" ? input : (input as { url: string }).url;
|
|
assert.equal(url, "https://or.example.com/api/combos");
|
|
return new Response(
|
|
JSON.stringify({
|
|
combos: [
|
|
{ id: "c1", name: "Combo One", strategy: "priority", models: [] },
|
|
{ id: "c2", name: "Combo Two", strategy: "weighted", models: [] },
|
|
],
|
|
}),
|
|
{ status: 200, headers: { "Content-Type": "application/json" } }
|
|
);
|
|
}) as typeof fetch;
|
|
try {
|
|
const combos = await defaultOmniRouteCombosFetcher("https://or.example.com", "sk-test");
|
|
assert.equal(combos.length, 2);
|
|
assert.equal(combos[0].id, "c1");
|
|
assert.equal(combos[1].id, "c2");
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
}
|
|
});
|
|
|
|
test("defaultOmniRouteCombosFetcher: parses bare array envelope", async () => {
|
|
const originalFetch = globalThis.fetch;
|
|
globalThis.fetch = (async () => {
|
|
return new Response(JSON.stringify([{ id: "c1" }, { id: "c2" }, { not_an_id: 42 }]), {
|
|
status: 200,
|
|
});
|
|
}) as typeof fetch;
|
|
try {
|
|
const combos = await defaultOmniRouteCombosFetcher("https://or.example.com/v1", "sk-test");
|
|
// Strip /v1 before /api/combos, AND filter out entries with no string id.
|
|
assert.equal(combos.length, 2);
|
|
assert.equal(combos[0].id, "c1");
|
|
assert.equal(combos[1].id, "c2");
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
}
|
|
});
|
|
|
|
test("defaultOmniRouteCombosFetcher: strips trailing /v1 before /api/combos", async () => {
|
|
const originalFetch = globalThis.fetch;
|
|
let observedUrl = "";
|
|
globalThis.fetch = (async (input: unknown) => {
|
|
observedUrl = typeof input === "string" ? input : (input as { url: string }).url;
|
|
return new Response(JSON.stringify({ combos: [] }), { status: 200 });
|
|
}) as typeof fetch;
|
|
try {
|
|
await defaultOmniRouteCombosFetcher("https://or.example.com/v1/", "sk-test");
|
|
assert.equal(observedUrl, "https://or.example.com/api/combos");
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
}
|
|
});
|
|
|
|
test("defaultOmniRouteCombosFetcher: throws on non-2xx with status code in message", async () => {
|
|
const originalFetch = globalThis.fetch;
|
|
globalThis.fetch = (async () => {
|
|
return new Response(JSON.stringify({ error: "Invalid token" }), {
|
|
status: 403,
|
|
statusText: "Forbidden",
|
|
});
|
|
}) as typeof fetch;
|
|
try {
|
|
await assert.rejects(
|
|
async () => {
|
|
await defaultOmniRouteCombosFetcher("https://or.example.com", "sk-bad");
|
|
},
|
|
(err: unknown) => {
|
|
const msg = err instanceof Error ? err.message : String(err);
|
|
assert.match(msg, /403/, "status code must appear in message");
|
|
assert.match(msg, /\/api\/combos/, "url must appear in message");
|
|
return true;
|
|
}
|
|
);
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
}
|
|
});
|
|
|
|
test("defaultOmniRouteCombosFetcher: throws when apiKey missing", async () => {
|
|
await assert.rejects(
|
|
async () => defaultOmniRouteCombosFetcher("https://or.example.com", ""),
|
|
/apiKey required/
|
|
);
|
|
});
|
|
|
|
test("defaultOmniRouteCombosFetcher: throws when baseURL missing", async () => {
|
|
await assert.rejects(
|
|
async () => defaultOmniRouteCombosFetcher("", "sk-test"),
|
|
/baseURL required/
|
|
);
|
|
});
|
|
|
|
// ────────────────────────────────────────────────────────────────────────────
|
|
// mapComboToModelV2 — LCD semantics
|
|
// ────────────────────────────────────────────────────────────────────────────
|
|
|
|
test("mapComboToModelV2: empty members → capabilities all false (defensive)", () => {
|
|
const m = mapComboToModelV2(
|
|
{ id: "combo-empty", name: "Empty Combo" },
|
|
[],
|
|
"omniroute",
|
|
"https://or.example.com/v1"
|
|
);
|
|
assert.equal(m.id, "combo-empty");
|
|
assert.equal(m.name, "Empty Combo");
|
|
assert.equal(m.capabilities.temperature, false);
|
|
assert.equal(m.capabilities.reasoning, false);
|
|
assert.equal(m.capabilities.attachment, false);
|
|
assert.equal(m.capabilities.toolcall, false);
|
|
assert.equal(m.capabilities.input.text, false);
|
|
assert.equal(m.capabilities.output.text, false);
|
|
assert.equal(m.limit.context, 0);
|
|
assert.equal(m.limit.output, 0);
|
|
assert.equal(m.limit.input, undefined);
|
|
assert.deepEqual(m.cost, { input: 0, output: 0, cache: { read: 0, write: 0 } });
|
|
});
|
|
|
|
test("mapComboToModelV2: all members reasoning=true → combo reasoning=true", () => {
|
|
const m = mapComboToModelV2(
|
|
{ id: "c", models: [] },
|
|
[
|
|
MODEL_PRIMARY,
|
|
{
|
|
...MODEL_PRIMARY,
|
|
id: "p2",
|
|
capabilities: { ...MODEL_PRIMARY.capabilities, thinking: false, reasoning: true },
|
|
},
|
|
],
|
|
"omniroute",
|
|
"https://or.example.com/v1"
|
|
);
|
|
assert.equal(m.capabilities.reasoning, true);
|
|
});
|
|
|
|
test("mapComboToModelV2: any member reasoning=false → combo reasoning=false", () => {
|
|
const m = mapComboToModelV2(
|
|
{ id: "c", models: [] },
|
|
[MODEL_PRIMARY, MODEL_NO_TOOLS], // gemini-3-flash has reasoning:false, thinking:false
|
|
"omniroute",
|
|
"https://or.example.com/v1"
|
|
);
|
|
assert.equal(m.capabilities.reasoning, false);
|
|
});
|
|
|
|
test("mapComboToModelV2: limit.context is min of members'", () => {
|
|
const m = mapComboToModelV2(
|
|
{ id: "c", models: [] },
|
|
[MODEL_PRIMARY, MODEL_SECONDARY, MODEL_NO_TOOLS],
|
|
"omniroute",
|
|
"https://or.example.com/v1"
|
|
);
|
|
// min(200_000, 100_000, 1_000_000) = 100_000
|
|
assert.equal(m.limit.context, 100_000);
|
|
// min(64_000, 32_000, 8_192) = 8_192
|
|
assert.equal(m.limit.output, 8_192);
|
|
});
|
|
|
|
test("mapComboToModelV2: limit.input only emitted when EVERY member declares one", () => {
|
|
const m1 = mapComboToModelV2(
|
|
{ id: "c", models: [] },
|
|
[MODEL_PRIMARY, MODEL_SECONDARY],
|
|
"omniroute",
|
|
"https://or.example.com/v1"
|
|
);
|
|
// Both declare max_input_tokens → limit.input = min(180000, 96000)
|
|
assert.equal(m1.limit.input, 96_000);
|
|
|
|
const m2 = mapComboToModelV2(
|
|
{ id: "c", models: [] },
|
|
[MODEL_PRIMARY, MODEL_NO_TOOLS], // gemini-3-flash doesn't declare max_input_tokens
|
|
"omniroute",
|
|
"https://or.example.com/v1"
|
|
);
|
|
assert.equal(m2.limit.input, undefined);
|
|
});
|
|
|
|
test("mapComboToModelV2: nice name preferred from combo.name", () => {
|
|
const m1 = mapComboToModelV2(
|
|
{ id: "combo-x", name: "Pretty Name" },
|
|
[MODEL_PRIMARY],
|
|
"omniroute",
|
|
"https://or.example.com/v1"
|
|
);
|
|
assert.equal(m1.name, "Pretty Name");
|
|
|
|
// Falls back to id when name is absent or empty.
|
|
const m2 = mapComboToModelV2(
|
|
{ id: "combo-y" },
|
|
[MODEL_PRIMARY],
|
|
"omniroute",
|
|
"https://or.example.com/v1"
|
|
);
|
|
assert.equal(m2.name, "combo-y");
|
|
|
|
const m3 = mapComboToModelV2(
|
|
{ id: "combo-z", name: " " },
|
|
[MODEL_PRIMARY],
|
|
"omniroute",
|
|
"https://or.example.com/v1"
|
|
);
|
|
assert.equal(m3.name, "combo-z");
|
|
});
|
|
|
|
test("mapComboToModelV2: attachment AND vision flag both honored across members", () => {
|
|
// MODEL_PRIMARY: vision=true; MODEL_SECONDARY: vision=true → combo attachment=true
|
|
const yes = mapComboToModelV2(
|
|
{ id: "c1", models: [] },
|
|
[MODEL_PRIMARY, MODEL_SECONDARY],
|
|
"omniroute",
|
|
"https://or.example.com/v1"
|
|
);
|
|
assert.equal(yes.capabilities.attachment, true);
|
|
|
|
// Add a member with no vision/attachment → AND collapses to false
|
|
const no = mapComboToModelV2(
|
|
{ id: "c2", models: [] },
|
|
[MODEL_PRIMARY, MODEL_NO_TOOLS],
|
|
"omniroute",
|
|
"https://or.example.com/v1"
|
|
);
|
|
assert.equal(no.capabilities.attachment, false);
|
|
});
|
|
|
|
test("mapComboToModelV2: modalities AND'd across members", () => {
|
|
const m = mapComboToModelV2(
|
|
{ id: "c", models: [] },
|
|
[MODEL_PRIMARY, MODEL_SECONDARY], // both have text+image
|
|
"omniroute",
|
|
"https://or.example.com/v1"
|
|
);
|
|
assert.equal(m.capabilities.input.text, true);
|
|
assert.equal(m.capabilities.input.image, true);
|
|
assert.equal(m.capabilities.input.audio, false);
|
|
|
|
// Add a text-only member → image collapses to false.
|
|
const m2 = mapComboToModelV2(
|
|
{ id: "c", models: [] },
|
|
[MODEL_PRIMARY, MODEL_NO_TOOLS],
|
|
"omniroute",
|
|
"https://or.example.com/v1"
|
|
);
|
|
assert.equal(m2.capabilities.input.text, true);
|
|
assert.equal(m2.capabilities.input.image, false);
|
|
});
|
|
|
|
test("mapComboToModelV2: api block matches providerId + baseURL", () => {
|
|
const m = mapComboToModelV2(
|
|
{ id: "c" },
|
|
[MODEL_PRIMARY],
|
|
"omniroute-preprod",
|
|
"https://or-preprod.example.com/v1"
|
|
);
|
|
assert.equal(m.providerID, "omniroute-preprod");
|
|
assert.equal(m.api.id, "openai-compatible");
|
|
assert.equal(m.api.url, "https://or-preprod.example.com/v1");
|
|
assert.equal(m.api.npm, "@ai-sdk/openai-compatible");
|
|
assert.equal(m.status, "active");
|
|
});
|
|
|
|
test("mapComboToModelV2: explicit member temperature=false drops combo temperature=false", () => {
|
|
const tempFalse: OmniRouteRawModelEntry = {
|
|
id: "no-temp",
|
|
capabilities: { tool_calling: true, temperature: false },
|
|
context_length: 100_000,
|
|
max_output_tokens: 8_000,
|
|
input_modalities: ["text"],
|
|
output_modalities: ["text"],
|
|
};
|
|
const m = mapComboToModelV2(
|
|
{ id: "c" },
|
|
[MODEL_PRIMARY, tempFalse],
|
|
"omniroute",
|
|
"https://or.example.com/v1"
|
|
);
|
|
assert.equal(m.capabilities.temperature, false);
|
|
});
|
|
|
|
// ────────────────────────────────────────────────────────────────────────────
|
|
// createOmniRouteProviderHook — combos merge + collision + soft-fail + cache
|
|
// ────────────────────────────────────────────────────────────────────────────
|
|
|
|
test("models() returns combo entries merged into the map", async () => {
|
|
const modelsFetcher = stubModelsFetcher([MODEL_PRIMARY, MODEL_SECONDARY, MODEL_NO_TOOLS]);
|
|
const combosFetcher = stubCombosFetcher([COMBO_CLAUDE_TIER]);
|
|
const hook = createOmniRouteProviderHook(
|
|
{ baseURL: "https://or.example.com/v1" },
|
|
{ fetcher: modelsFetcher, combosFetcher }
|
|
);
|
|
const out = await hook.models!({} as never, { auth: apiAuth("sk-z") as never });
|
|
|
|
// 3 raw models + 1 combo = 4 entries
|
|
assert.equal(Object.keys(out).length, 4);
|
|
assert.ok(out["opencode-omniroute/claude-primary"]);
|
|
assert.ok(out["opencode-omniroute/claude-secondary"]);
|
|
assert.ok(out["opencode-omniroute/gemini-3-flash"]);
|
|
assert.ok(out["opencode-omniroute/claude-tier"]);
|
|
|
|
const combo = out["opencode-omniroute/claude-tier"];
|
|
assert.equal(combo.name, "Claude Tier");
|
|
assert.equal(combo.providerID, "opencode-omniroute");
|
|
// LCD over claude-primary (200k, reasoning) + claude-secondary (100k, no reasoning)
|
|
assert.equal(combo.limit.context, 100_000);
|
|
assert.equal(combo.capabilities.reasoning, false);
|
|
assert.equal(combo.capabilities.toolcall, true);
|
|
});
|
|
|
|
test("models(): combo with unknown member ids degrades to all-false LCD posture", async () => {
|
|
const modelsFetcher = stubModelsFetcher([MODEL_PRIMARY]); // catalog only has claude-primary
|
|
const combosFetcher = stubCombosFetcher([
|
|
{
|
|
id: "phantom",
|
|
name: "Phantom Combo",
|
|
models: [
|
|
{ id: "s1", kind: "model", model: "does-not-exist-1", weight: 50 },
|
|
{ id: "s2", kind: "model", model: "does-not-exist-2", weight: 50 },
|
|
],
|
|
},
|
|
]);
|
|
const hook = createOmniRouteProviderHook(
|
|
{ baseURL: "https://or.example.com/v1" },
|
|
{ fetcher: modelsFetcher, combosFetcher }
|
|
);
|
|
const out = await hook.models!({} as never, { auth: apiAuth("sk-z") as never });
|
|
assert.ok(out["opencode-omniroute/phantom-combo"]);
|
|
// With zero resolvable members, LCD = all-false (defensive posture).
|
|
assert.equal(out["opencode-omniroute/phantom-combo"].capabilities.toolcall, false);
|
|
assert.equal(out["opencode-omniroute/phantom-combo"].capabilities.reasoning, false);
|
|
assert.equal(out["opencode-omniroute/phantom-combo"].limit.context, 0);
|
|
});
|
|
|
|
test("models(): hidden combos are excluded from the map", async () => {
|
|
const modelsFetcher = stubModelsFetcher([MODEL_PRIMARY]);
|
|
const combosFetcher = stubCombosFetcher([
|
|
{
|
|
id: "visible",
|
|
name: "Visible",
|
|
models: [{ id: "s1", kind: "model", model: "claude-primary", weight: 100 }],
|
|
},
|
|
{
|
|
id: "hidden",
|
|
name: "Hidden",
|
|
isHidden: true,
|
|
models: [{ id: "s1", kind: "model", model: "claude-primary", weight: 100 }],
|
|
},
|
|
]);
|
|
const hook = createOmniRouteProviderHook(
|
|
{ baseURL: "https://or.example.com/v1" },
|
|
{ fetcher: modelsFetcher, combosFetcher }
|
|
);
|
|
const out = await hook.models!({} as never, { auth: apiAuth("sk-z") as never });
|
|
assert.ok(out["opencode-omniroute/visible"]);
|
|
assert.ok(!out["opencode-omniroute/hidden"], "hidden combo must be omitted");
|
|
});
|
|
|
|
test("models(): combo name exactly matches raw model id → raw deleted, raw deleted, no warn", async () => {
|
|
// Combo.name === raw model id triggers the dedup deletion. This mirrors
|
|
// the real OmniRoute payload where /v1/models pre-mirrors combos as
|
|
// no-slash raw entries whose ids match /api/combos friendly names.
|
|
const colliderCombo: OmniRouteRawCombo = {
|
|
id: "uuid-collider",
|
|
name: "claude-primary", // EXACT match to MODEL_PRIMARY.id
|
|
models: [{ id: "s1", kind: "model", model: "claude-secondary", weight: 100 }],
|
|
};
|
|
const modelsFetcher = stubModelsFetcher([MODEL_PRIMARY, MODEL_SECONDARY]);
|
|
const combosFetcher = stubCombosFetcher([colliderCombo]);
|
|
const hook = createOmniRouteProviderHook(
|
|
{ baseURL: "https://or.example.com/v1" },
|
|
{ fetcher: modelsFetcher, combosFetcher }
|
|
);
|
|
|
|
const { result: out, warnings } = await withWarnCapture(async (_w) => {
|
|
return hook.models!({} as never, { auth: apiAuth("sk-z") as never });
|
|
});
|
|
|
|
// Raw model replaced by combo of the same key; combo now lives at the bare slug.
|
|
assert.ok(out["opencode-omniroute/claude-primary"], "combo surfaces under prefixed key");
|
|
assert.equal(out["opencode-omniroute/claude-primary"].name, "claude-primary");
|
|
|
|
// No collision warning fires — dedup makes keys disjoint.
|
|
const collisionWarns = warnings.filter((w) => {
|
|
const msg = w.args[0];
|
|
return typeof msg === "string" && msg.includes("collides");
|
|
});
|
|
assert.equal(collisionWarns.length, 0, "no collision warn after dedup");
|
|
});
|
|
|
|
test("models(): two combos with same slug → second gets disambiguator suffix", async () => {
|
|
// Both combos slug to `claude` — second must get `claude-<id-prefix>`.
|
|
const combos: OmniRouteRawCombo[] = [
|
|
{
|
|
id: "uuid-a",
|
|
name: "Claude",
|
|
models: [{ id: "s", kind: "model", model: "claude-primary", weight: 1 }],
|
|
},
|
|
{
|
|
id: "uuid-b",
|
|
name: "Claude",
|
|
models: [{ id: "s", kind: "model", model: "claude-secondary", weight: 1 }],
|
|
},
|
|
];
|
|
const hook = createOmniRouteProviderHook(
|
|
{ baseURL: "https://or.example.com/v1" },
|
|
{
|
|
fetcher: stubModelsFetcher([MODEL_PRIMARY, MODEL_SECONDARY]),
|
|
combosFetcher: stubCombosFetcher(combos),
|
|
}
|
|
);
|
|
|
|
const out = await hook.models!({} as never, { auth: apiAuth("sk-z") as never });
|
|
// First combo gets the bare slug; second gets disambiguated.
|
|
assert.ok(out["opencode-omniroute/claude"], "first combo at prefixed slug");
|
|
assert.ok(out["opencode-omniroute/claude-uuid"], "second combo disambiguated by id prefix");
|
|
});
|
|
|
|
test("models(): combos fetch fails → falls back to models-only, warn emitted, no throw", async () => {
|
|
const modelsFetcher = stubModelsFetcher([MODEL_PRIMARY, MODEL_SECONDARY]);
|
|
const combosFetcher = failingCombosFetcher(new Error("ECONNRESET"));
|
|
const hook = createOmniRouteProviderHook(
|
|
{ baseURL: "https://or.example.com/v1" },
|
|
{ fetcher: modelsFetcher, combosFetcher }
|
|
);
|
|
|
|
const { result: out, warnings } = await withWarnCapture(async () => {
|
|
return hook.models!({} as never, { auth: apiAuth("sk-z") as never });
|
|
});
|
|
|
|
// Catalog includes the models but NOT any combo entries.
|
|
assert.equal(Object.keys(out).length, 2);
|
|
assert.ok(out["opencode-omniroute/claude-primary"]);
|
|
assert.ok(out["opencode-omniroute/claude-secondary"]);
|
|
|
|
// Soft-fail warning surfaced.
|
|
const softFail = warnings.find((w) => {
|
|
const msg = w.args[0];
|
|
return typeof msg === "string" && msg.includes("combos fetch failed");
|
|
});
|
|
assert.ok(softFail, "soft-fail warning must be emitted on combos fetch error");
|
|
assert.equal(combosFetcher.callCount(), 1);
|
|
});
|
|
|
|
test("models(): combos cached + reused within TTL (one combo fetch per TTL window)", async () => {
|
|
const modelsFetcher = stubModelsFetcher([MODEL_PRIMARY, MODEL_SECONDARY]);
|
|
const combosFetcher = stubCombosFetcher([COMBO_CLAUDE_TIER]);
|
|
let nowMs = 1_000_000;
|
|
const hook = createOmniRouteProviderHook(
|
|
{ baseURL: "https://or.example.com/v1", modelCacheTtl: 60_000 },
|
|
{ fetcher: modelsFetcher, combosFetcher, now: () => nowMs }
|
|
);
|
|
|
|
await hook.models!({} as never, { auth: apiAuth("sk-z") as never });
|
|
nowMs += 30_000; // half the TTL
|
|
const second = await hook.models!({} as never, { auth: apiAuth("sk-z") as never });
|
|
assert.equal(combosFetcher.callCount(), 1, "combos fetched only once within TTL");
|
|
assert.equal(modelsFetcher.callCount(), 1, "models fetched only once within TTL");
|
|
assert.ok(second["opencode-omniroute/claude-tier"]);
|
|
});
|
|
|
|
test("models(): combos refetched after TTL expiry (same key as models)", async () => {
|
|
const modelsFetcher = stubModelsFetcher([MODEL_PRIMARY]);
|
|
const combosFetcher = stubCombosFetcher([COMBO_CLAUDE_TIER]);
|
|
let nowMs = 1_000_000;
|
|
const hook = createOmniRouteProviderHook(
|
|
{ baseURL: "https://or.example.com/v1", modelCacheTtl: 60_000 },
|
|
{ fetcher: modelsFetcher, combosFetcher, now: () => nowMs }
|
|
);
|
|
|
|
await hook.models!({} as never, { auth: apiAuth("sk-z") as never });
|
|
nowMs += 60_001;
|
|
await hook.models!({} as never, { auth: apiAuth("sk-z") as never });
|
|
assert.equal(combosFetcher.callCount(), 2, "combos must refetch past TTL");
|
|
assert.equal(modelsFetcher.callCount(), 2, "models must refetch past TTL");
|
|
});
|
|
|
|
test("models(): combos fetcher receives the resolved baseURL + apiKey", async () => {
|
|
const modelsFetcher = stubModelsFetcher([MODEL_PRIMARY]);
|
|
const combosFetcher = stubCombosFetcher([COMBO_CLAUDE_TIER]);
|
|
const hook = createOmniRouteProviderHook(
|
|
{ baseURL: "https://or.example.com/v1" },
|
|
{ fetcher: modelsFetcher, combosFetcher }
|
|
);
|
|
await hook.models!({} as never, { auth: apiAuth("sk-spy") as never });
|
|
assert.deepEqual(combosFetcher.callsBy()[0], ["https://or.example.com/v1", "sk-spy"]);
|
|
});
|
|
|
|
test("models(): nested combo-ref context is the min of nested + raw members", async () => {
|
|
// Top-level combo MASTER-LIGHT has 1 raw model (claude-primary, 200k)
|
|
// and 2 combo-refs: OldLLM (8k member) and KIRO (32k member). The OLD
|
|
// plugin would advertise 200k (only the raw model); the fix should
|
|
// make it advertise 8k (the bottleneck across the member graph).
|
|
const modelsFetcher = stubModelsFetcher([
|
|
MODEL_PRIMARY,
|
|
{
|
|
id: "oldllm-member-1",
|
|
context_length: 8_000,
|
|
max_output_tokens: 4_000,
|
|
capabilities: {
|
|
tool_calling: false,
|
|
reasoning: false,
|
|
vision: false,
|
|
thinking: false,
|
|
temperature: true,
|
|
},
|
|
input_modalities: ["text"],
|
|
output_modalities: ["text"],
|
|
},
|
|
{
|
|
id: "kiro-member-1",
|
|
context_length: 32_000,
|
|
max_output_tokens: 8_000,
|
|
capabilities: {
|
|
tool_calling: true,
|
|
reasoning: false,
|
|
vision: false,
|
|
thinking: false,
|
|
temperature: true,
|
|
},
|
|
input_modalities: ["text"],
|
|
output_modalities: ["text"],
|
|
},
|
|
]);
|
|
const combosFetcher = stubCombosFetcher([
|
|
{
|
|
id: "oldllm",
|
|
name: "OldLLM",
|
|
models: [{ id: "s1", kind: "model", model: "oldllm-member-1", weight: 100 }],
|
|
},
|
|
{
|
|
id: "kiro",
|
|
name: "KIRO",
|
|
models: [{ id: "s1", kind: "model", model: "kiro-member-1", weight: 100 }],
|
|
},
|
|
{
|
|
id: "master-light",
|
|
name: "MASTER-LIGHT",
|
|
models: [
|
|
{ id: "r1", kind: "model", model: "claude-primary", weight: 50 },
|
|
{ id: "r2", kind: "combo-ref", comboName: "OldLLM", weight: 25 },
|
|
{ id: "r3", kind: "combo-ref", comboName: "KIRO", weight: 25 },
|
|
],
|
|
},
|
|
]);
|
|
const hook = createOmniRouteProviderHook(
|
|
{ baseURL: "https://or.example.com/v1" },
|
|
{ fetcher: modelsFetcher, combosFetcher }
|
|
);
|
|
const out = await hook.models!({} as never, { auth: apiAuth("sk-z") as never });
|
|
const masterLight = out["opencode-omniroute/master-light"];
|
|
assert.ok(masterLight, "MASTER-LIGHT entry must exist");
|
|
assert.equal(
|
|
masterLight.limit.context,
|
|
8_000,
|
|
`expected 8_000 (OldLLM bottleneck), got ${masterLight.limit.context}`
|
|
);
|
|
});
|