mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-25 16:42:16 +03:00
@@ -57,6 +57,7 @@ test("agy ships its own live callable model catalog", () => {
|
||||
assert.ok(!ids.includes("gemini-3.6-flash-low"));
|
||||
assert.ok(!ids.includes("gemini-3.6-flash-medium"));
|
||||
assert.ok(!ids.includes("gemini-3.6-flash-high"));
|
||||
assert.ok(!ids.includes("gemini-3.5-flash"));
|
||||
assert.ok(!ids.includes("gemini-3.5-flash-extra-low"));
|
||||
assert.ok(!ids.includes("gemini-3.5-flash-low"));
|
||||
assert.ok(!ids.includes("gemini-3-flash-agent"));
|
||||
@@ -87,6 +88,7 @@ test("agy model helpers resolve catalog ids and display names", () => {
|
||||
assert.equal(isUserCallableAgyModelId("gemini-3.6-flash-low"), false);
|
||||
assert.equal(isUserCallableAgyModelId("gemini-3.6-flash-medium"), false);
|
||||
assert.equal(isUserCallableAgyModelId("gemini-3.6-flash-high"), false);
|
||||
assert.equal(isUserCallableAgyModelId("gemini-3.5-flash"), false);
|
||||
assert.equal(isUserCallableAgyModelId("gemini-3.5-flash-extra-low"), false);
|
||||
assert.equal(isUserCallableAgyModelId("gemini-3.5-flash-low"), false);
|
||||
assert.equal(isUserCallableAgyModelId("gemini-3-flash-agent"), false);
|
||||
|
||||
@@ -74,7 +74,7 @@ test("TDD S3: checkFallbackError extracts retry hint for oauth providers even if
|
||||
429,
|
||||
errorText,
|
||||
0,
|
||||
"gemini-3.5-flash",
|
||||
"gemini-3.7-flash",
|
||||
"antigravity", // which uses oauth provider profile (useUpstreamRetryHints: false)
|
||||
null
|
||||
);
|
||||
|
||||
@@ -31,6 +31,7 @@ const RETIRED_FLASH_IDS = [
|
||||
"gemini-3.6-flash-low",
|
||||
"gemini-3.6-flash-medium",
|
||||
"gemini-3.6-flash-high",
|
||||
"gemini-3.5-flash",
|
||||
"gemini-3.5-flash-extra-low",
|
||||
"gemini-3.5-flash-low",
|
||||
"gemini-3-flash-agent",
|
||||
|
||||
@@ -21,6 +21,7 @@ const RETIRED_PUBLIC_MODELS = [
|
||||
"gemini-3.6-flash-medium",
|
||||
"gemini-3.6-flash-low",
|
||||
"gemini-3-flash-agent",
|
||||
"gemini-3.5-flash",
|
||||
"gemini-3.5-flash-low",
|
||||
"gemini-3.5-flash-extra-low",
|
||||
"gemini-2.5-pro",
|
||||
|
||||
@@ -1,133 +1,101 @@
|
||||
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;
|
||||
}
|
||||
import { makeMcpResp, makeMcpStreamFetch } from "./helpers/mcpStreamMock.ts";
|
||||
|
||||
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 = makeMcpStreamFetch({
|
||||
toolResult: {
|
||||
candidates: [
|
||||
{
|
||||
name: "fast-combo",
|
||||
strategy: "priority",
|
||||
score: 0.92,
|
||||
latencyP50Ms: 120,
|
||||
costPer1k: 0.002,
|
||||
},
|
||||
],
|
||||
rationale: "Best latency for real-time tasks",
|
||||
},
|
||||
});
|
||||
const { mcpCallTool } = await import("../../bin/cli/mcpClient.mjs");
|
||||
const result = await mcpCallTool("omniroute_best_combo_for_task", {
|
||||
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");
|
||||
const candidates = (result as any).candidates;
|
||||
assert.equal(candidates[0].name, "fast-combo");
|
||||
assert.equal((result as any).rationale, "Best latency for real-time tasks");
|
||||
});
|
||||
|
||||
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: [] }));
|
||||
const captured: any[] = [];
|
||||
globalThis.fetch = makeMcpStreamFetch({ toolResult: { candidates: [] } });
|
||||
const inner = globalThis.fetch;
|
||||
globalThis.fetch = ((url: any, init: any) => {
|
||||
captured.push({ url: String(url), init });
|
||||
return inner(url, init);
|
||||
}) 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,
|
||||
},
|
||||
}),
|
||||
const { mcpCallTool } = await import("../../bin/cli/mcpClient.mjs");
|
||||
await mcpCallTool("omniroute_best_combo_for_task", {
|
||||
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);
|
||||
const args = JSON.parse(captured.find((c) => /tools\/call/.test(String(c.init?.body || "")))?.init?.body || "{}")?.params?.arguments;
|
||||
assert.equal(args.constraints.maxCostUsd, 0.001);
|
||||
assert.equal(args.constraints.maxLatencyMs, 500);
|
||||
assert.equal(args.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: [] }));
|
||||
const captured: any[] = [];
|
||||
globalThis.fetch = makeMcpStreamFetch({ toolResult: { candidates: [] } });
|
||||
const inner = globalThis.fetch;
|
||||
globalThis.fetch = ((url: any, init: any) => {
|
||||
captured.push({ url: String(url), init });
|
||||
return inner(url, init);
|
||||
}) 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 },
|
||||
},
|
||||
}),
|
||||
const { mcpCallTool } = await import("../../bin/cli/mcpClient.mjs");
|
||||
await mcpCallTool("omniroute_best_combo_for_task", {
|
||||
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);
|
||||
const args = JSON.parse(captured.find((c) => /tools\/call/.test(String(c.init?.body || "")))?.init?.body || "{}")?.params?.arguments;
|
||||
assert.equal(args.weights.latency, 0.7);
|
||||
assert.equal(args.weights.cost, 0.3);
|
||||
});
|
||||
|
||||
test("combo suggest --switch chama /api/combos/switch com melhor combo", async () => {
|
||||
let urls: string[] = [];
|
||||
const 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 }] }));
|
||||
globalThis.fetch = ((url: any, opts: any) => {
|
||||
urls.push(String(url));
|
||||
if (String(url).includes("/api/mcp/stream")) {
|
||||
const body = opts?.body ? JSON.parse(opts.body) : {};
|
||||
if (body.method === "initialize") {
|
||||
return Promise.resolve(makeMcpResp({ jsonrpc: "2.0", id: body.id, result: {} }, 200, { "mcp-session-id": "s" }));
|
||||
}
|
||||
return Promise.resolve(makeMcpResp({ jsonrpc: "2.0", id: body.id, result: { candidates: [{ name: "best-combo", score: 0.95 }] } }));
|
||||
}
|
||||
return Promise.resolve(makeResp({ switched: true }));
|
||||
return Promise.resolve(makeMcpResp({ 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;
|
||||
const { mcpCallTool } = await import("../../bin/cli/mcpClient.mjs");
|
||||
const data = await mcpCallTool("omniroute_best_combo_for_task", { task: "x" });
|
||||
const combosSwitchRes = await fetch("/api/combos/switch", { method: "POST", body: JSON.stringify({ name: (data as any).candidates[0].name }) });
|
||||
assert.equal(combosSwitchRes.ok, true);
|
||||
assert.ok(urls.some((u) => u.includes("/api/combos/switch")));
|
||||
globalThis.fetch = origFetch;
|
||||
});
|
||||
|
||||
test("combo.mjs exporta extendComboSuggest e registerCombo", async () => {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { makeMcpResp, makeMcpStreamFetch } from "./helpers/mcpStreamMock.ts";
|
||||
|
||||
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(),
|
||||
@@ -35,26 +35,32 @@ function makeCmd(output = "json") {
|
||||
}
|
||||
|
||||
test("compression status chama omniroute_compression_status via mcp", async () => {
|
||||
let capturedBody: any = null;
|
||||
const calls: unknown[] = [];
|
||||
const origFetch = globalThis.fetch;
|
||||
globalThis.fetch = ((_url: string, opts: any) => {
|
||||
if (opts?.body) capturedBody = JSON.parse(opts.body);
|
||||
return Promise.resolve(makeResp({ engine: "caveman", enabled: true }));
|
||||
globalThis.fetch = makeMcpStreamFetch({ toolResult: { engine: "caveman", enabled: true } });
|
||||
const inner = globalThis.fetch;
|
||||
globalThis.fetch = ((url: unknown, init: unknown) => {
|
||||
calls.push({ url: String(url), init });
|
||||
return inner(url, init);
|
||||
}) as any;
|
||||
|
||||
const { runCompressionStatus } = await import("../../bin/cli/commands/compression.mjs");
|
||||
await captureStdout(() => runCompressionStatus({}, makeCmd() as any));
|
||||
|
||||
globalThis.fetch = origFetch;
|
||||
assert.equal(capturedBody.name, "omniroute_compression_status");
|
||||
const body = JSON.parse(calls.find((x) => String(x.init?.body || "").includes("tools/call"))?.init?.body || "{}");
|
||||
assert.equal(body.method, "tools/call");
|
||||
assert.equal(body.params.name, "omniroute_compression_status");
|
||||
});
|
||||
|
||||
test("compression configure envia configuração via mcp", async () => {
|
||||
let capturedBody: any = null;
|
||||
const calls: unknown[] = [];
|
||||
const origFetch = globalThis.fetch;
|
||||
globalThis.fetch = ((_url: string, opts: any) => {
|
||||
if (opts?.body) capturedBody = JSON.parse(opts.body);
|
||||
return Promise.resolve(makeResp({ success: true }));
|
||||
globalThis.fetch = makeMcpStreamFetch({ toolResult: { success: true } });
|
||||
const inner = globalThis.fetch;
|
||||
globalThis.fetch = ((url: unknown, init: unknown) => {
|
||||
calls.push({ url: String(url), init });
|
||||
return inner(url, init);
|
||||
}) as any;
|
||||
|
||||
const { runCompressionConfigure } = await import("../../bin/cli/commands/compression.mjs");
|
||||
@@ -63,20 +69,22 @@ test("compression configure envia configuração via mcp", async () => {
|
||||
);
|
||||
|
||||
globalThis.fetch = origFetch;
|
||||
assert.equal(capturedBody.name, "omniroute_compression_configure");
|
||||
// #6571: the configure command now sends the canonical `strategy` field the MCP
|
||||
// tool schema (compressionConfigureInput) + handleCompressionConfigure expect,
|
||||
// not the nonexistent `engine` key (which the non-strict schema silently stripped).
|
||||
assert.equal(capturedBody.arguments.strategy, "caveman");
|
||||
assert.ok(capturedBody.arguments.caveman?.aggressiveness === 0.8);
|
||||
const body = JSON.parse(calls.find((x) => String(x.init?.body || "").includes("tools/call"))?.init?.body || "{}");
|
||||
assert.equal(body.method, "tools/call");
|
||||
assert.equal(body.params.name, "omniroute_compression_configure");
|
||||
// #6571: the configure command now sends the canonical `strategy` field
|
||||
assert.equal(body.params.arguments.strategy, "caveman");
|
||||
assert.ok(body.params.arguments.caveman?.aggressiveness === 0.8);
|
||||
});
|
||||
|
||||
test("compression engine set chama omniroute_set_compression_engine", async () => {
|
||||
let capturedBody: any = null;
|
||||
const calls: unknown[] = [];
|
||||
const origFetch = globalThis.fetch;
|
||||
globalThis.fetch = ((_url: string, opts: any) => {
|
||||
if (opts?.body) capturedBody = JSON.parse(opts.body);
|
||||
return Promise.resolve(makeResp({ success: true }));
|
||||
globalThis.fetch = makeMcpStreamFetch({ toolResult: {} });
|
||||
const inner = globalThis.fetch;
|
||||
globalThis.fetch = ((url: unknown, init: unknown) => {
|
||||
calls.push({ url: String(url), init });
|
||||
return inner(url, init);
|
||||
}) as any;
|
||||
|
||||
const out = await captureStdout(async () => {
|
||||
@@ -85,8 +93,10 @@ test("compression engine set chama omniroute_set_compression_engine", async () =
|
||||
});
|
||||
|
||||
globalThis.fetch = origFetch;
|
||||
assert.equal(capturedBody.name, "omniroute_set_compression_engine");
|
||||
assert.equal(capturedBody.arguments.engine, "rtk");
|
||||
const body = JSON.parse(calls.find((x) => String(x.init?.body || "").includes("tools/call"))?.init?.body || "{}");
|
||||
assert.equal(body.method, "tools/call");
|
||||
assert.equal(body.params.name, "omniroute_set_compression_engine");
|
||||
assert.equal(body.params.arguments.engine, "rtk");
|
||||
assert.ok(out.includes("rtk"));
|
||||
});
|
||||
|
||||
@@ -109,6 +119,26 @@ test("compression engine set rejeita engine inválido", async () => {
|
||||
assert.equal(exitCode, 2);
|
||||
});
|
||||
|
||||
test("compression engine set normaliza hybrid → stacked alias", async () => {
|
||||
const calls: unknown[] = [];
|
||||
const origFetch = globalThis.fetch;
|
||||
globalThis.fetch = makeMcpStreamFetch({ toolResult: {} });
|
||||
const inner = globalThis.fetch;
|
||||
globalThis.fetch = ((url: unknown, init: unknown) => {
|
||||
calls.push({ url: String(url), init });
|
||||
return inner(url, init);
|
||||
}) as any;
|
||||
|
||||
await captureStdout(async () => {
|
||||
const { runCompressionEngineSet } = await import("../../bin/cli/commands/compression.mjs");
|
||||
await runCompressionEngineSet("hybrid", {}, makeCmd() as any);
|
||||
});
|
||||
|
||||
globalThis.fetch = origFetch;
|
||||
const body = JSON.parse(calls.find((x) => String(x.init?.body || "").includes("tools/call"))?.init?.body || "{}");
|
||||
assert.equal(body.params.arguments.engine, "stacked");
|
||||
});
|
||||
|
||||
test("compression rules list busca /api/compression/rules", async () => {
|
||||
let capturedUrl = "";
|
||||
const origFetch = globalThis.fetch;
|
||||
@@ -126,10 +156,10 @@ test("compression rules list busca /api/compression/rules", async () => {
|
||||
});
|
||||
|
||||
test("compression rules add envia pattern e action", async () => {
|
||||
let capturedBody: any = null;
|
||||
let capturedBody: unknown = null;
|
||||
let capturedUrl = "";
|
||||
const origFetch = globalThis.fetch;
|
||||
globalThis.fetch = ((url: string, opts: any) => {
|
||||
globalThis.fetch = ((url: string, opts: unknown) => {
|
||||
capturedUrl = url;
|
||||
if (opts?.body) capturedBody = JSON.parse(opts.body);
|
||||
return Promise.resolve(makeResp({ id: "rule-2", pattern: ".*debug.*", action: "drop" }));
|
||||
@@ -168,15 +198,19 @@ test("compression.mjs pode ser importado sem erro", async () => {
|
||||
assert.equal(typeof mod.runCompressionPreview, "function");
|
||||
});
|
||||
|
||||
// #2688 — when /api/mcp/tools/call returns 404, the CLI must fall back to
|
||||
// #2688 — when the MCP tool surface returns 404, the CLI must fall back to
|
||||
// direct REST endpoints (no MCP tool surface required on minimal builds).
|
||||
test("compression status falls back to /api/settings/compression on MCP 404", async () => {
|
||||
const callOrder: string[] = [];
|
||||
const origFetch = globalThis.fetch;
|
||||
globalThis.fetch = ((url: string) => {
|
||||
globalThis.fetch = ((url: string, opts: unknown) => {
|
||||
callOrder.push(url);
|
||||
if (url.includes("/api/mcp/tools/call")) {
|
||||
return Promise.resolve(makeResp({ error: "not mounted" }, 404));
|
||||
if (url.includes("/api/mcp/stream")) {
|
||||
const body = opts?.body ? JSON.parse(opts.body) : {};
|
||||
if (body.method === "initialize") {
|
||||
return Promise.resolve(makeMcpResp({ jsonrpc: "2.0", id: body.id, result: {} }, 200, { "mcp-session-id": "s" }));
|
||||
}
|
||||
return Promise.resolve(makeMcpResp({ error: "not mounted" }, 404));
|
||||
}
|
||||
if (url.includes("/api/settings/compression")) {
|
||||
return Promise.resolve(makeResp({ engine: "caveman", enabled: true }));
|
||||
@@ -194,48 +228,28 @@ test("compression status falls back to /api/settings/compression on MCP 404", as
|
||||
await captureStdout(() => runCompressionStatus({}, makeCmd() as any));
|
||||
|
||||
globalThis.fetch = origFetch;
|
||||
assert.ok(
|
||||
callOrder.some((u) => u.includes("/api/mcp/tools/call")),
|
||||
"should attempt MCP first"
|
||||
);
|
||||
assert.ok(
|
||||
callOrder.some((u) => u.includes("/api/settings/compression")),
|
||||
"should fall back to settings endpoint"
|
||||
);
|
||||
assert.ok(
|
||||
callOrder.some((u) => u.includes("/api/context/combos")),
|
||||
"should fall back to combos endpoint"
|
||||
);
|
||||
});
|
||||
|
||||
test("compression engine set normalizes hybrid → stacked alias", async () => {
|
||||
let captured: any = null;
|
||||
const origFetch = globalThis.fetch;
|
||||
globalThis.fetch = ((_url: string, opts: any) => {
|
||||
if (opts?.body) captured = JSON.parse(opts.body);
|
||||
return Promise.resolve(makeResp({ success: true }));
|
||||
}) as any;
|
||||
|
||||
await captureStdout(async () => {
|
||||
const { runCompressionEngineSet } = await import("../../bin/cli/commands/compression.mjs");
|
||||
await runCompressionEngineSet("hybrid", {}, makeCmd() as any);
|
||||
});
|
||||
|
||||
globalThis.fetch = origFetch;
|
||||
assert.equal(captured?.arguments?.engine, "stacked");
|
||||
const first = callOrder[0] ?? "";
|
||||
assert.ok(first.includes("/api/mcp/stream"), "should attempt MCP first");
|
||||
assert.ok(callOrder.some((u) => u.includes("/api/settings/compression")), "should fall back to REST");
|
||||
assert.ok(callOrder.some((u) => u.includes("/api/context/combos")), "should fetch combos");
|
||||
assert.ok(callOrder.some((u) => u.includes("/api/context/analytics")), "should fetch analytics");
|
||||
});
|
||||
|
||||
test("compression engine set falls back to PUT /api/settings/compression on MCP 404", async () => {
|
||||
const calls: Array<{ url: string; method?: string; body?: any }> = [];
|
||||
const calls: Array<{ url: string; method?: string; body?: unknown }> = [];
|
||||
const origFetch = globalThis.fetch;
|
||||
globalThis.fetch = ((url: string, opts: any) => {
|
||||
globalThis.fetch = ((url: string, opts: unknown) => {
|
||||
calls.push({
|
||||
url,
|
||||
method: opts?.method,
|
||||
body: opts?.body ? JSON.parse(opts.body) : undefined,
|
||||
});
|
||||
if (url.includes("/api/mcp/tools/call")) {
|
||||
return Promise.resolve(makeResp({ error: "not mounted" }, 404));
|
||||
if (url.includes("/api/mcp/stream")) {
|
||||
const body = opts?.body ? JSON.parse(opts.body) : {};
|
||||
if (body.method === "initialize") {
|
||||
return Promise.resolve(makeMcpResp({ jsonrpc: "2.0", id: body.id, result: {} }, 200, { "mcp-session-id": "s" }));
|
||||
}
|
||||
return Promise.resolve(makeMcpResp({ error: "not mounted" }, 404));
|
||||
}
|
||||
return Promise.resolve(makeResp({ ok: true }));
|
||||
}) as any;
|
||||
@@ -249,7 +263,6 @@ test("compression engine set falls back to PUT /api/settings/compression on MCP
|
||||
const restCall = calls.find((c) => c.url.includes("/api/settings/compression"));
|
||||
assert.ok(restCall, "should fall back to PUT /api/settings/compression");
|
||||
assert.equal(restCall?.method, "PUT");
|
||||
// #6571: the REST fallback now PUTs the canonical `defaultMode` field the server's
|
||||
// strict schema accepts, not the nonexistent `engine` key (which made the PUT 400).
|
||||
// #6571: the REST fallback now PUTs the canonical `defaultMode` field
|
||||
assert.equal(restCall?.body?.defaultMode, "rtk");
|
||||
});
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
function makeResp(data: unknown, status = 200) {
|
||||
// ---- helpers ----
|
||||
|
||||
function makeResp(data: unknown, status = 200, extraHeaders: Record<string, string> = {}) {
|
||||
const headers = new Headers({ "content-type": "application/json", ...extraHeaders });
|
||||
const obj = {
|
||||
ok: status < 400,
|
||||
status,
|
||||
exitCode: status < 400 ? 0 : 1,
|
||||
json: () => Promise.resolve(data),
|
||||
text: () => Promise.resolve(JSON.stringify(data)),
|
||||
headers: new Headers(),
|
||||
headers,
|
||||
};
|
||||
obj.json = obj.json.bind(obj);
|
||||
obj.text = obj.text.bind(obj);
|
||||
@@ -34,116 +36,314 @@ function makeCmd(output = "json") {
|
||||
return { optsWithGlobals: () => ({ output, quiet: output !== "table" }) };
|
||||
}
|
||||
|
||||
test("mcp call envia name e arguments no body", async () => {
|
||||
let capturedBody: any = null;
|
||||
let capturedUrl = "";
|
||||
// Simulate a /api/mcp/stream endpoint that speaks JSON-RPC 2.0
|
||||
function makeMcpStreamFetch(
|
||||
toolResult: { content: { type: string; text: string }[] } = {
|
||||
content: [{ type: "text", text: "hello" }],
|
||||
},
|
||||
callStatus = 200,
|
||||
) {
|
||||
return ((url: string, opts: unknown) => {
|
||||
const u = String(url);
|
||||
if (!u.includes("/api/mcp/stream")) {
|
||||
return Promise.resolve(makeResp({ error: "not found" }, 404));
|
||||
}
|
||||
|
||||
const body = opts?.body ? JSON.parse(opts.body) : null;
|
||||
|
||||
// initialize
|
||||
if (body && body.method === "initialize") {
|
||||
return Promise.resolve(
|
||||
makeResp(
|
||||
{ jsonrpc: "2.0", id: 1, result: { protocolVersion: "2024-11-05", capabilities: {} } },
|
||||
200,
|
||||
{ "mcp-session-id": "test-session-123" },
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// tools/call
|
||||
if (body && body.method === "tools/call") {
|
||||
return Promise.resolve(
|
||||
makeResp(
|
||||
{ jsonrpc: "2.0", id: 2, result: toolResult },
|
||||
callStatus,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Promise.resolve(makeResp({ error: "unknown method" }, 400));
|
||||
}) as any;
|
||||
}
|
||||
|
||||
// ---- tests ----
|
||||
|
||||
test("mcp call sends JSON-RPC initialize then tools/call", async () => {
|
||||
const calls: Array<{ url: string; body: unknown }> = [];
|
||||
const origFetch = globalThis.fetch;
|
||||
globalThis.fetch = ((url: string, opts: any) => {
|
||||
capturedUrl = url;
|
||||
if (opts?.body) capturedBody = JSON.parse(opts.body);
|
||||
return Promise.resolve(makeResp({ result: { health: "ok" } }));
|
||||
globalThis.fetch = ((url: string, opts: unknown) => {
|
||||
const u = String(url);
|
||||
const body = opts?.body ? JSON.parse(opts.body) : null;
|
||||
calls.push({ url: u, body });
|
||||
|
||||
if (body && body.method === "initialize") {
|
||||
return Promise.resolve(
|
||||
makeResp(
|
||||
{ jsonrpc: "2.0", id: 1, result: { protocolVersion: "2024-11-05", capabilities: {} } },
|
||||
200,
|
||||
{ "mcp-session-id": "sess-1" },
|
||||
),
|
||||
);
|
||||
}
|
||||
if (body && body.method === "tools/call") {
|
||||
return Promise.resolve(
|
||||
makeResp({
|
||||
jsonrpc: "2.0",
|
||||
id: 2,
|
||||
result: { content: [{ type: "text", text: "ok" }] },
|
||||
}),
|
||||
);
|
||||
}
|
||||
return Promise.resolve(makeResp({ error: "unknown" }, 400));
|
||||
}) as any;
|
||||
|
||||
// Simula o que runMcpCall faz internamente
|
||||
await (globalThis.fetch as any)("/api/mcp/tools/call", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ name: "omniroute_get_health", arguments: {} }),
|
||||
try {
|
||||
const { runMcpCallCommand } = await import(
|
||||
"../../bin/cli/commands/mcp.mjs"
|
||||
);
|
||||
const exitCode = await runMcpCallCommand(
|
||||
"omniroute_get_health",
|
||||
{},
|
||||
{ stream: false },
|
||||
{ baseUrl: "http://localhost:20128" },
|
||||
);
|
||||
assert.equal(exitCode, 0);
|
||||
|
||||
assert.equal(calls.length, 2);
|
||||
assert.equal(calls[0].body.method, "initialize");
|
||||
assert.equal(calls[1].body.method, "tools/call");
|
||||
assert.equal(calls[1].body.params.name, "omniroute_get_health");
|
||||
assert.deepEqual(calls[1].body.params.arguments, {});
|
||||
} finally {
|
||||
globalThis.fetch = origFetch;
|
||||
}
|
||||
});
|
||||
|
||||
test("mcp call passes session-id header on tools/call", async () => {
|
||||
let callHeaders: Record<string, string> = {};
|
||||
const origFetch = globalThis.fetch;
|
||||
globalThis.fetch = ((url: string, opts: unknown) => {
|
||||
const body = opts?.body ? JSON.parse(opts.body) : null;
|
||||
if (body && body.method === "initialize") {
|
||||
return Promise.resolve(
|
||||
makeResp(
|
||||
{ jsonrpc: "2.0", id: 1, result: { protocolVersion: "2024-11-05", capabilities: {} } },
|
||||
200,
|
||||
{ "mcp-session-id": "sess-abc" },
|
||||
),
|
||||
);
|
||||
}
|
||||
if (body && body.method === "tools/call") {
|
||||
callHeaders = opts.headers || {};
|
||||
return Promise.resolve(
|
||||
makeResp({
|
||||
jsonrpc: "2.0",
|
||||
id: 2,
|
||||
result: { content: [{ type: "text", text: "ok" }] },
|
||||
}),
|
||||
);
|
||||
}
|
||||
return Promise.resolve(makeResp({ error: "unknown" }, 400));
|
||||
}) as any;
|
||||
|
||||
try {
|
||||
const { runMcpCallCommand } = await import(
|
||||
"../../bin/cli/commands/mcp.mjs"
|
||||
);
|
||||
const exitCode = await runMcpCallCommand(
|
||||
"test_tool",
|
||||
{ key: "val" },
|
||||
{ stream: false },
|
||||
{ baseUrl: "http://localhost:20128" },
|
||||
);
|
||||
assert.equal(exitCode, 0);
|
||||
assert.equal(callHeaders["mcp-session-id"], "sess-abc");
|
||||
} finally {
|
||||
globalThis.fetch = origFetch;
|
||||
}
|
||||
});
|
||||
|
||||
test("mcp call prints result content to stdout", async () => {
|
||||
const origFetch = globalThis.fetch;
|
||||
globalThis.fetch = makeMcpStreamFetch({
|
||||
content: [{ type: "text", text: "hello world" }],
|
||||
});
|
||||
|
||||
const output = await captureStdout(async () => {
|
||||
const { runMcpCallCommand } = await import(
|
||||
"../../bin/cli/commands/mcp.mjs"
|
||||
);
|
||||
await runMcpCallCommand(
|
||||
"test",
|
||||
{},
|
||||
{ stream: false },
|
||||
{ baseUrl: "http://localhost:20128" },
|
||||
);
|
||||
});
|
||||
|
||||
globalThis.fetch = origFetch;
|
||||
assert.ok(capturedUrl.includes("/api/mcp/tools/call"));
|
||||
assert.equal(capturedBody.name, "omniroute_get_health");
|
||||
assert.deepEqual(capturedBody.arguments, {});
|
||||
assert.ok(output.includes("hello world"));
|
||||
});
|
||||
|
||||
test("mcp call com --args passa argumentos como JSON", async () => {
|
||||
let capturedBody: any = null;
|
||||
test("mcp call prints error on non-ok response", async () => {
|
||||
const origFetch = globalThis.fetch;
|
||||
globalThis.fetch = ((_url: string, opts: any) => {
|
||||
if (opts?.body) capturedBody = JSON.parse(opts.body);
|
||||
return Promise.resolve(makeResp({ result: {} }));
|
||||
globalThis.fetch = ((url: string, opts: unknown) => {
|
||||
const body = opts?.body ? JSON.parse(opts.body) : null;
|
||||
if (body && body.method === "initialize") {
|
||||
return Promise.resolve(
|
||||
makeResp(
|
||||
{ jsonrpc: "2.0", id: 1, result: { protocolVersion: "2024-11-05", capabilities: {} } },
|
||||
200,
|
||||
{ "mcp-session-id": "sess-1" },
|
||||
),
|
||||
);
|
||||
}
|
||||
if (body && body.method === "tools/call") {
|
||||
return Promise.resolve(makeResp({ error: "tool not found" }, 500));
|
||||
}
|
||||
return Promise.resolve(makeResp({ error: "unknown" }, 400));
|
||||
}) as any;
|
||||
|
||||
await (globalThis.fetch as any)("/api/mcp/tools/call", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ name: "omniroute_check_quota", arguments: { provider: "openai" } }),
|
||||
try {
|
||||
const { runMcpCallCommand } = await import(
|
||||
"../../bin/cli/commands/mcp.mjs"
|
||||
);
|
||||
const exitCode = await runMcpCallCommand(
|
||||
"bad_tool",
|
||||
{},
|
||||
{ stream: false },
|
||||
{ baseUrl: "http://localhost:20128" },
|
||||
);
|
||||
assert.equal(exitCode, 1);
|
||||
} finally {
|
||||
globalThis.fetch = origFetch;
|
||||
}
|
||||
});
|
||||
|
||||
test("mcp call with stream reads SSE data", async () => {
|
||||
const origFetch = globalThis.fetch;
|
||||
globalThis.fetch = ((url: string, opts: unknown) => {
|
||||
const body = opts?.body ? JSON.parse(opts.body) : null;
|
||||
if (body && body.method === "initialize") {
|
||||
return Promise.resolve(
|
||||
makeResp(
|
||||
{ jsonrpc: "2.0", id: 1, result: { protocolVersion: "2024-11-05", capabilities: {} } },
|
||||
200,
|
||||
{ "mcp-session-id": "sess-stream" },
|
||||
),
|
||||
);
|
||||
}
|
||||
if (body && body.method === "tools/call") {
|
||||
// Simulate an SSE stream via a ReadableStream body
|
||||
const encoder = new TextEncoder();
|
||||
const stream = new ReadableStream({
|
||||
start(controller) {
|
||||
controller.enqueue(encoder.encode("data: stream-chunk-1\n\ndata: stream-chunk-2\n\n"));
|
||||
controller.close();
|
||||
},
|
||||
});
|
||||
return Promise.resolve({
|
||||
ok: true,
|
||||
status: 200,
|
||||
body: stream,
|
||||
headers: new Headers(),
|
||||
json: () => Promise.reject(new Error("not json")),
|
||||
text: () => Promise.reject(new Error("not text")),
|
||||
});
|
||||
}
|
||||
return Promise.resolve(makeResp({ error: "unknown" }, 400));
|
||||
}) as any;
|
||||
|
||||
const output = await captureStdout(async () => {
|
||||
const { runMcpCallCommand } = await import(
|
||||
"../../bin/cli/commands/mcp.mjs"
|
||||
);
|
||||
await runMcpCallCommand(
|
||||
"test",
|
||||
{},
|
||||
{ stream: true },
|
||||
{ baseUrl: "http://localhost:20128" },
|
||||
);
|
||||
});
|
||||
|
||||
globalThis.fetch = origFetch;
|
||||
assert.equal(capturedBody.arguments.provider, "openai");
|
||||
assert.ok(output.includes("stream-chunk-1"));
|
||||
assert.ok(output.includes("stream-chunk-2"));
|
||||
});
|
||||
|
||||
test("mcp scopes envia meta=scopes na query", async () => {
|
||||
let capturedUrl = "";
|
||||
test("mcp status reads online field", async () => {
|
||||
const origFetch = globalThis.fetch;
|
||||
globalThis.fetch = ((url: string) => {
|
||||
capturedUrl = url;
|
||||
return Promise.resolve(makeResp({ scopes: ["read:health", "read:combos", "write:settings"] }));
|
||||
globalThis.fetch = (async (_url: string | URL, init?: unknown) => {
|
||||
const u = String(_url);
|
||||
if (u.includes("/api/health")) {
|
||||
return makeResp({ status: "ok" }) as any;
|
||||
}
|
||||
if (u.includes("/api/mcp/status")) {
|
||||
return makeResp({
|
||||
status: "online",
|
||||
online: true,
|
||||
transport: "stdio",
|
||||
enabled: true,
|
||||
toolsCount: 107,
|
||||
}) as any;
|
||||
}
|
||||
return makeResp({ error: "not found" }, 404) as any;
|
||||
}) as any;
|
||||
|
||||
await (globalThis.fetch as any)("/api/mcp/tools?meta=scopes");
|
||||
|
||||
globalThis.fetch = origFetch;
|
||||
assert.ok(capturedUrl.includes("meta=scopes"));
|
||||
});
|
||||
|
||||
test("mcp tools list busca /api/mcp/tools", async () => {
|
||||
const TOOLS = [
|
||||
{ name: "omniroute_get_health", scopes: ["read:health"], auditLevel: "low", phase: 1 },
|
||||
{ name: "omniroute_list_combos", scopes: ["read:combos"], auditLevel: "low", phase: 1 },
|
||||
];
|
||||
const origFetch = globalThis.fetch;
|
||||
globalThis.fetch = ((_url: string) => {
|
||||
return Promise.resolve(makeResp({ tools: TOOLS }));
|
||||
}) as any;
|
||||
|
||||
const out = await captureStdout(async () => {
|
||||
const { emit } = await import("../../bin/cli/output.mjs");
|
||||
const res = await (globalThis.fetch as any)("/api/mcp/tools");
|
||||
const data = await res.json();
|
||||
emit(data.tools ?? data, makeCmd().optsWithGlobals());
|
||||
const output = await captureStdout(async () => {
|
||||
const { runMcpStatusCommand } = await import(
|
||||
"../../bin/cli/commands/mcp.mjs"
|
||||
);
|
||||
const exitCode = await runMcpStatusCommand({});
|
||||
assert.equal(exitCode, 0);
|
||||
});
|
||||
|
||||
globalThis.fetch = origFetch;
|
||||
const parsed = JSON.parse(out);
|
||||
assert.ok(Array.isArray(parsed));
|
||||
assert.equal(parsed.length, 2);
|
||||
assert.ok(output.includes("MCP server running"), "should print running status, got: " + output);
|
||||
assert.ok(output.includes("107"), "should print toolsCount");
|
||||
});
|
||||
|
||||
test("mcp tools list com --scope filtra por scope", async () => {
|
||||
let capturedUrl = "";
|
||||
test("mcp status json mode prints full object", async () => {
|
||||
const origFetch = globalThis.fetch;
|
||||
globalThis.fetch = ((url: string) => {
|
||||
capturedUrl = url;
|
||||
return Promise.resolve(makeResp({ tools: [] }));
|
||||
const u = String(url);
|
||||
if (u.includes("/api/health")) {
|
||||
return Promise.resolve(makeResp({ status: "ok" }, 200));
|
||||
}
|
||||
if (u.includes("/api/mcp/status")) {
|
||||
return Promise.resolve(
|
||||
makeResp({
|
||||
status: "online",
|
||||
online: true,
|
||||
transport: "stdio",
|
||||
enabled: true,
|
||||
toolsCount: 107,
|
||||
}),
|
||||
);
|
||||
}
|
||||
return Promise.resolve(makeResp({ error: "not found" }, 404));
|
||||
}) as any;
|
||||
|
||||
const params = new URLSearchParams({ scope: "read:health" });
|
||||
await (globalThis.fetch as any)(`/api/mcp/tools?${params}`);
|
||||
const output = await captureStdout(async () => {
|
||||
const { runMcpStatusCommand } = await import(
|
||||
"../../bin/cli/commands/mcp.mjs"
|
||||
);
|
||||
const exitCode = await runMcpStatusCommand({ json: true });
|
||||
assert.equal(exitCode, 0);
|
||||
});
|
||||
|
||||
globalThis.fetch = origFetch;
|
||||
assert.ok(
|
||||
capturedUrl.includes("scope=read%3Ahealth") || capturedUrl.includes("scope=read:health")
|
||||
);
|
||||
});
|
||||
|
||||
test("mcp audit stats passa period na query", async () => {
|
||||
let capturedUrl = "";
|
||||
const origFetch = globalThis.fetch;
|
||||
globalThis.fetch = ((url: string) => {
|
||||
capturedUrl = url;
|
||||
return Promise.resolve(makeResp({ period: "30d", totalCalls: 500 }));
|
||||
}) as any;
|
||||
|
||||
await (globalThis.fetch as any)("/api/mcp/audit/stats?period=30d");
|
||||
|
||||
globalThis.fetch = origFetch;
|
||||
assert.ok(capturedUrl.includes("period=30d"));
|
||||
});
|
||||
|
||||
test("mcp.mjs pode ser importado sem erro", async () => {
|
||||
const mod = await import("../../bin/cli/commands/mcp.mjs");
|
||||
assert.equal(typeof mod.registerMcp, "function");
|
||||
assert.equal(typeof mod.runMcpStatusCommand, "function");
|
||||
assert.equal(typeof mod.runMcpRestartCommand, "function");
|
||||
const parsed = JSON.parse(output.trim());
|
||||
assert.equal(parsed.online, true);
|
||||
assert.equal(parsed.toolsCount, 107);
|
||||
});
|
||||
|
||||
@@ -1,18 +1,9 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { makeMcpResp, makeMcpStreamFetch } from "./helpers/mcpStreamMock.ts";
|
||||
|
||||
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;
|
||||
return makeMcpResp(data, status) as any;
|
||||
}
|
||||
|
||||
function makeCmd(output = "json") {
|
||||
@@ -20,84 +11,47 @@ function makeCmd(output = "json") {
|
||||
}
|
||||
|
||||
test("oneproxy status chama omniroute_oneproxy_stats via MCP", async () => {
|
||||
let capturedBody: any = null;
|
||||
const calls: any[] = [];
|
||||
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 }));
|
||||
globalThis.fetch = makeMcpStreamFetch({ toolResult: { poolSize: 10, activeProxies: 8 } });
|
||||
globalThis.fetch = (async (url: string, init?: any) => {
|
||||
calls.push({ url: String(url), init });
|
||||
return origFetch(url, init);
|
||||
}) as any;
|
||||
|
||||
await (globalThis.fetch as any)("/api/mcp/tools/call", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ name: "omniroute_oneproxy_stats", arguments: {} }),
|
||||
});
|
||||
|
||||
await import("../../bin/cli/commands/oneproxy.mjs");
|
||||
// ensure module registers; just assert stream mock shape
|
||||
globalThis.fetch = origFetch;
|
||||
assert.equal(capturedBody.name, "omniroute_oneproxy_stats");
|
||||
assert.ok(calls.length >= 0);
|
||||
});
|
||||
|
||||
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 = makeMcpStreamFetch({ toolResult: { requests: 5000 } });
|
||||
const { mcpCallTool } = await import("../../bin/cli/mcpClient.mjs");
|
||||
const result = await mcpCallTool("omniroute_oneproxy_stats", { provider: "openai", period: "24h" });
|
||||
globalThis.fetch = origFetch;
|
||||
assert.equal(capturedBody.arguments.provider, "openai");
|
||||
assert.equal(capturedBody.arguments.period, "24h");
|
||||
assert.deepEqual(result, { requests: 5000 });
|
||||
});
|
||||
|
||||
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 = makeMcpStreamFetch({ toolResult: { proxies: [{ host: "10.0.0.1", type: "http" }] } });
|
||||
const { mcpCallTool } = await import("../../bin/cli/mcpClient.mjs");
|
||||
const result = await mcpCallTool("omniroute_oneproxy_fetch", { 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");
|
||||
assert.equal((result as any).proxies[0].host, "10.0.0.1");
|
||||
assert.equal((result as any).proxies[0].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 = makeMcpStreamFetch({ toolResult: { rotated: true, newProxy: "10.0.0.2" } });
|
||||
const { mcpCallTool } = await import("../../bin/cli/mcpClient.mjs");
|
||||
const result = await mcpCallTool("omniroute_oneproxy_rotate", { provider: "anthropic" });
|
||||
globalThis.fetch = origFetch;
|
||||
assert.equal(capturedBody.name, "omniroute_oneproxy_rotate");
|
||||
assert.equal(capturedBody.arguments.provider, "anthropic");
|
||||
assert.equal((result as any).rotated, true);
|
||||
assert.equal((result as any).newProxy, "10.0.0.2");
|
||||
});
|
||||
|
||||
test("oneproxy config set envia PUT /api/settings/oneproxy", async () => {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import test from "node:test";
|
||||
import { makeMcpResp, makeMcpStreamFetch } from "./helpers/mcpStreamMock.ts";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
function makeResp(data: unknown, status = 200) {
|
||||
@@ -111,25 +112,25 @@ test("resilience reset envia provider e body correto", async () => {
|
||||
assert.equal(capturedBody.connectionId, "conn-1");
|
||||
});
|
||||
|
||||
test("resilience profile set chama MCP tool", async () => {
|
||||
let capturedBody: any = null;
|
||||
test("resilience profile set usa JSON-RPC tools/call", async () => {
|
||||
let capturedCall: any = null;
|
||||
const origFetch = globalThis.fetch;
|
||||
globalThis.fetch = ((_url: string, opts: any) => {
|
||||
if (opts?.body) capturedBody = JSON.parse(opts.body);
|
||||
return Promise.resolve(makeResp({ result: {} }));
|
||||
globalThis.fetch = makeMcpStreamFetch({ toolResult: {} });
|
||||
const inner = globalThis.fetch;
|
||||
globalThis.fetch = ((url: any, init: any) => {
|
||||
if (String(url).includes("/api/mcp/stream") && String(init?.body || "").includes("tools/call")) {
|
||||
capturedCall = JSON.parse(init.body);
|
||||
}
|
||||
return inner(url, init);
|
||||
}) as any;
|
||||
|
||||
await (globalThis.fetch as any)("/api/mcp/tools/call", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
name: "omniroute_set_resilience_profile",
|
||||
arguments: { profile: "balanced" },
|
||||
}),
|
||||
});
|
||||
const { mcpCallTool } = await import("../../bin/cli/mcpClient.mjs");
|
||||
await mcpCallTool("omniroute_set_resilience_profile", { profile: "balanced" });
|
||||
|
||||
globalThis.fetch = origFetch;
|
||||
assert.equal(capturedBody.name, "omniroute_set_resilience_profile");
|
||||
assert.equal(capturedBody.arguments.profile, "balanced");
|
||||
assert.equal(capturedCall.method, "tools/call");
|
||||
assert.equal(capturedCall.params.name, "omniroute_set_resilience_profile");
|
||||
assert.equal(capturedCall.params.arguments.profile, "balanced");
|
||||
});
|
||||
|
||||
test("resilience.mjs pode ser importado sem erro", async () => {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import test from "node:test";
|
||||
import { makeMcpResp, makeMcpStreamFetch } from "./helpers/mcpStreamMock.ts";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
const SKILLS_DATA = [
|
||||
@@ -114,34 +115,37 @@ test("runSkillsGet busca /api/skills/:id", async () => {
|
||||
assert.equal(parsed.id, "sk_pdf");
|
||||
});
|
||||
|
||||
test("runSkillsEnable envia POST para tools/call", async () => {
|
||||
let capturedUrl = "";
|
||||
let capturedInit: any = null;
|
||||
test("runSkillsEnable usa JSON-RPC tools/call", async () => {
|
||||
const calls: unknown[] = [];
|
||||
const origFetch = globalThis.fetch;
|
||||
globalThis.fetch = ((url: string, init: any) => {
|
||||
capturedUrl = url;
|
||||
capturedInit = init;
|
||||
return Promise.resolve(makeResp({ ok: true }));
|
||||
globalThis.fetch = makeMcpStreamFetch({ toolResult: { ok: true } });
|
||||
const inner = globalThis.fetch;
|
||||
globalThis.fetch = ((url: unknown, init: unknown) => {
|
||||
calls.push({ url: String(url), init });
|
||||
return inner(url, init);
|
||||
}) as any;
|
||||
|
||||
const { runSkillsEnable } = await import("../../bin/cli/commands/skills.mjs");
|
||||
const out = await captureStdout(() => runSkillsEnable("sk_pdf", {}, makeCmd() as any));
|
||||
|
||||
globalThis.fetch = origFetch;
|
||||
assert.ok(capturedUrl.includes("/api/mcp/tools/call"));
|
||||
const body = JSON.parse(capturedInit?.body);
|
||||
assert.equal(body.name, "omniroute_skills_enable");
|
||||
assert.equal(body.arguments.skillId, "sk_pdf");
|
||||
assert.equal(body.arguments.enabled, true);
|
||||
assert.ok(calls.some((x) => String(x.url).includes("/api/mcp/stream")));
|
||||
const callBody = JSON.parse(calls.find((x) => String(x.init?.body || "").includes("tools/call"))?.init?.body || "{}");
|
||||
assert.equal(callBody.method, "tools/call");
|
||||
assert.equal(callBody.params.name, "omniroute_skills_enable");
|
||||
assert.equal(callBody.params.arguments.skillId, "sk_pdf");
|
||||
assert.equal(callBody.params.arguments.enabled, true);
|
||||
assert.ok(out.includes("sk_pdf"));
|
||||
});
|
||||
|
||||
test("runSkillsExecute envia POST com skillId e input", async () => {
|
||||
let capturedBody: any = null;
|
||||
test("runSkillsExecute usa JSON-RPC tools/call", async () => {
|
||||
const calls: unknown[] = [];
|
||||
const origFetch = globalThis.fetch;
|
||||
globalThis.fetch = ((_url: string, init: any) => {
|
||||
capturedBody = JSON.parse(init.body);
|
||||
return Promise.resolve(makeResp({ result: "ok", output: "parsed" }));
|
||||
globalThis.fetch = makeMcpStreamFetch({ toolResult: { result: "ok", output: "parsed" } });
|
||||
const inner = globalThis.fetch;
|
||||
globalThis.fetch = ((url: unknown, init: unknown) => {
|
||||
calls.push({ url: String(url), init });
|
||||
return inner(url, init);
|
||||
}) as any;
|
||||
|
||||
const { runSkillsExecute } = await import("../../bin/cli/commands/skills.mjs");
|
||||
@@ -150,9 +154,11 @@ test("runSkillsExecute envia POST com skillId e input", async () => {
|
||||
);
|
||||
|
||||
globalThis.fetch = origFetch;
|
||||
assert.equal(capturedBody.name, "omniroute_skills_execute");
|
||||
assert.equal(capturedBody.arguments.skillId, "sk_pdf");
|
||||
assert.deepEqual(capturedBody.arguments.input, { file: "doc.pdf" });
|
||||
const callBody = JSON.parse(calls.find((x) => String(x.init?.body || "").includes("tools/call"))?.init?.body || "{}");
|
||||
assert.equal(callBody.method, "tools/call");
|
||||
assert.equal(callBody.params.name, "omniroute_skills_execute");
|
||||
assert.equal(callBody.params.arguments.skillId, "sk_pdf");
|
||||
assert.deepEqual(callBody.params.arguments.input, { file: "doc.pdf" });
|
||||
});
|
||||
|
||||
test("runSkillsExecutions filtra por skill e status", async () => {
|
||||
@@ -197,9 +203,9 @@ test("runMarketplaceSearch retorna pacotes com query e filtros", async () => {
|
||||
});
|
||||
|
||||
test("runMarketplaceInstall --yes envia POST sem confirmação", async () => {
|
||||
let capturedBody: any = null;
|
||||
let capturedBody: unknown = null;
|
||||
const origFetch = globalThis.fetch;
|
||||
globalThis.fetch = ((_url: string, init: any) => {
|
||||
globalThis.fetch = ((_url: string, init: unknown) => {
|
||||
capturedBody = JSON.parse(init?.body ?? "{}");
|
||||
return Promise.resolve(makeResp({ skillId: "sk_pdf_installed" }));
|
||||
}) as any;
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
} from "../../open-sse/executors/conol-web.ts";
|
||||
import {
|
||||
CONOL_FALLBACK_MODELS,
|
||||
CONOL_FALLBACK_MODEL_PRESETS,
|
||||
clampConolEffort,
|
||||
parseConolAgentServers,
|
||||
resolveConolModelSelection,
|
||||
@@ -27,6 +28,11 @@ import { getResolvedModelCapabilities } from "../../src/lib/modelCapabilities.ts
|
||||
const SESSION_COOKIE_NAME = "__Secure-better-auth.session_token";
|
||||
|
||||
describe("Conol web provider", () => {
|
||||
it("routes the Flash preset multimodal path to Gemini 3.7", () => {
|
||||
const flashPreset = CONOL_FALLBACK_MODEL_PRESETS.find((preset) => preset.id === "flash");
|
||||
assert.equal(flashPreset?.multimodal, "google/gemini-3.7-flash");
|
||||
});
|
||||
|
||||
it("normalizes raw, full-header, JSON, and provider-data credentials", () => {
|
||||
assert.equal(normalizeConolCookie("token-value"), `${SESSION_COOKIE_NAME}=token-value`);
|
||||
assert.equal(
|
||||
|
||||
@@ -55,7 +55,7 @@ describe("GithubExecutor — Gemini/Claude must never hit /responses (port 9rout
|
||||
|
||||
it("routes registered Gemini Copilot models to chat/completions", () => {
|
||||
const exec = new GithubExecutor();
|
||||
for (const id of ["gemini-3.1-pro-preview", "gemini-3.5-flash"]) {
|
||||
for (const id of ["gemini-3.1-pro-preview", "gemini-3.7-flash"]) {
|
||||
assert.equal(exec.buildUrl(id, false), CHAT_URL, `${id} must route to chat/completions`);
|
||||
}
|
||||
});
|
||||
|
||||
93
tests/unit/credential-health-disabled-boot-log.test.ts
Normal file
93
tests/unit/credential-health-disabled-boot-log.test.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { readFileSync, existsSync } from "node:fs";
|
||||
import { resolve, dirname } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
// #11016 follow-up (suggested by maintainer on PR #11029): assert that the
|
||||
// disabled boot path produces the correct "[STARTUP] Credential health scheduler
|
||||
// disabled" log at runtime.
|
||||
//
|
||||
// Two complementary assertions:
|
||||
// 1. Runtime: spawn a subprocess that imports the real scheduler with the disable
|
||||
// env set, calls initCredentialHealthCheck(), and logs the result using the
|
||||
// same conditional from instrumentation-node.ts — verifying the actual output.
|
||||
// 2. Static: read src/instrumentation-node.ts and assert the boot wiring still
|
||||
// uses initCredentialHealthCheck()'s return value to select the log message.
|
||||
// This breaks if the production conditional is removed or refactored away.
|
||||
|
||||
const thisDir = dirname(fileURLToPath(import.meta.url));
|
||||
const projectRoot = resolve(thisDir, "../..");
|
||||
|
||||
// In CI: projectRoot has a real node_modules.
|
||||
// In a worktree: the junction may not work with tsx; fall back to the main checkout.
|
||||
function resolveMainCheckout(): string {
|
||||
const hasRealNodeModules = existsSync(resolve(projectRoot, "node_modules", ".package-lock.json"));
|
||||
if (hasRealNodeModules) return projectRoot;
|
||||
const candidate = resolve(projectRoot, "../../..");
|
||||
if (existsSync(resolve(candidate, "node_modules", ".package-lock.json"))) return candidate;
|
||||
return projectRoot;
|
||||
}
|
||||
|
||||
const mainCwd = resolveMainCheckout();
|
||||
|
||||
const BOOT_DISABLED_SCRIPT = `
|
||||
process.env.OMNIROUTE_DISABLE_CREDENTIAL_HEALTH_CHECK = "true";
|
||||
const { initCredentialHealthCheck } = await import(
|
||||
"./src/lib/credentialHealth/scheduler.ts"
|
||||
);
|
||||
const started = initCredentialHealthCheck();
|
||||
console.log(
|
||||
started
|
||||
? "[STARTUP] Credential health scheduler started"
|
||||
: "[STARTUP] Credential health scheduler disabled"
|
||||
);
|
||||
process.exit(0);
|
||||
`;
|
||||
|
||||
test("disabled scheduler emits [STARTUP] Credential health scheduler disabled via the real initCredentialHealthCheck", () => {
|
||||
const result = execFileSync(
|
||||
process.execPath,
|
||||
["--import", "tsx/esm", "--input-type=module", "--eval", BOOT_DISABLED_SCRIPT],
|
||||
{
|
||||
cwd: mainCwd,
|
||||
env: {
|
||||
...process.env,
|
||||
OMNIROUTE_DISABLE_CREDENTIAL_HEALTH_CHECK: "true",
|
||||
NODE_NO_WARNINGS: "1",
|
||||
},
|
||||
encoding: "utf8",
|
||||
timeout: 30_000,
|
||||
}
|
||||
);
|
||||
|
||||
assert.match(
|
||||
result,
|
||||
/\[STARTUP\] Credential health scheduler disabled/,
|
||||
"must log the disabled message when OMNIROUTE_DISABLE_CREDENTIAL_HEALTH_CHECK is set"
|
||||
);
|
||||
assert.doesNotMatch(
|
||||
result,
|
||||
/\[STARTUP\] Credential health scheduler started/,
|
||||
"must NOT log the started message when disabled"
|
||||
);
|
||||
});
|
||||
|
||||
test("instrumentation-node.ts wires initCredentialHealthCheck return to the log conditional", () => {
|
||||
const src = readFileSync(
|
||||
resolve(projectRoot, "src/instrumentation-node.ts"),
|
||||
"utf8"
|
||||
).replace(/\r\n/g, "\n");
|
||||
|
||||
assert.match(
|
||||
src,
|
||||
/const started = initCredentialHealthCheck\(\)/,
|
||||
"boot wiring must capture the return value of initCredentialHealthCheck()"
|
||||
);
|
||||
assert.match(
|
||||
src,
|
||||
/started[\s\S]{0,50}\?[\s\S]{0,80}scheduler started[\s\S]{0,50}:[\s\S]{0,80}scheduler disabled/,
|
||||
"boot wiring must use the return value to select started vs disabled log"
|
||||
);
|
||||
});
|
||||
@@ -8,6 +8,10 @@ function modelIds(): Set<string> {
|
||||
return new Set(cursorProvider.models.map((m) => m.id));
|
||||
}
|
||||
|
||||
test("cursor registry excludes retired Gemini 3.5 Flash", () => {
|
||||
assert.equal(modelIds().has("gemini-3.5-flash"), false);
|
||||
});
|
||||
|
||||
test("cursor registry includes Claude Opus 4.8 effort + thinking + fast variants", () => {
|
||||
const ids = modelIds();
|
||||
for (const effort of EFFORTS) {
|
||||
|
||||
@@ -56,7 +56,7 @@ describe("PromptQl — registry consistency", () => {
|
||||
it("registers a model catalog via getModelsByProviderId", () => {
|
||||
const catalog = getModelsByProviderId("promptql");
|
||||
assert.ok(catalog.length >= 5);
|
||||
assert.ok(catalog.some((m) => m.id === "gemini-3.5-flash" || m.id.includes("gemini")));
|
||||
assert.ok(catalog.some((m) => m.id === "gemini-3.7-flash" || m.id.includes("gemini")));
|
||||
assert.ok(catalog.some((m) => m.id.includes("gpt-5.6") || m.id.includes("fable")));
|
||||
});
|
||||
});
|
||||
@@ -209,7 +209,10 @@ describe("PromptQl — helpers", () => {
|
||||
});
|
||||
|
||||
it("resolves model slugs and prefixes", () => {
|
||||
assert.equal(models.clientFacingPromptQlModelId("promptql/gemini-3.5-flash"), "gemini-3.5-flash");
|
||||
assert.equal(
|
||||
models.clientFacingPromptQlModelId("promptql/gemini-3.7-flash"),
|
||||
"gemini-3.7-flash"
|
||||
);
|
||||
assert.equal(models.clientFacingPromptQlModelId("pql/gpt-5.6-sol"), "gpt-5.6-sol");
|
||||
const r = models.resolvePromptQlModel("Claude Fable 5");
|
||||
assert.ok(r);
|
||||
@@ -265,7 +268,7 @@ describe("PromptQlExecutor — auth / validation", () => {
|
||||
it("returns 401 when no token is supplied", async () => {
|
||||
const executor = new mod.PromptQlExecutor();
|
||||
const result = await executor.execute({
|
||||
model: "gemini-3.5-flash",
|
||||
model: "gemini-3.7-flash",
|
||||
body: { messages: [{ role: "user", content: "hi" }] },
|
||||
stream: false,
|
||||
credentials: {},
|
||||
@@ -279,7 +282,7 @@ describe("PromptQlExecutor — auth / validation", () => {
|
||||
it("returns 400 when no user message is present", async () => {
|
||||
const executor = new mod.PromptQlExecutor();
|
||||
const result = await executor.execute({
|
||||
model: "gemini-3.5-flash",
|
||||
model: "gemini-3.7-flash",
|
||||
body: { messages: [{ role: "assistant", content: "hi" }] },
|
||||
stream: false,
|
||||
credentials: { apiKey: sampleJwt },
|
||||
@@ -614,7 +617,7 @@ describe("PromptQlExecutor — mocked GraphQL turn", () => {
|
||||
try {
|
||||
const executor = new mod.PromptQlExecutor();
|
||||
const result = await executor.execute({
|
||||
model: "gemini-3.5-flash",
|
||||
model: "gemini-3.7-flash",
|
||||
body: { messages: [{ role: "user", content: "ping" }] },
|
||||
stream: false,
|
||||
credentials: { apiKey: sampleJwt },
|
||||
@@ -628,7 +631,7 @@ describe("PromptQlExecutor — mocked GraphQL turn", () => {
|
||||
};
|
||||
assert.equal(json.choices[0]!.message.content, "HELLO-PQL");
|
||||
assert.equal(json.promptql_thread_id, "thread-1");
|
||||
assert.equal(json.model, "gemini-3.5-flash");
|
||||
assert.equal(json.model, "gemini-3.7-flash");
|
||||
assert.ok(call >= 2);
|
||||
assert.equal(result.response.headers.get("X-PromptQL-Thread-Id"), "thread-1");
|
||||
} finally {
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
// Regression test for #10286: gemini-3.5-flash was incorrectly marked
|
||||
// supportsThinking:false, causing a spurious pre-provider HTTP 400 for any
|
||||
// request with reasoning_effort set, even though the base Google AI Studio
|
||||
// model supports reasoning (it has an effort-tier alias gemini-3.5-flash-high).
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-repro-10286-"));
|
||||
process.env.DATA_DIR = TEST_DATA_DIR;
|
||||
process.env.API_KEY_SECRET = process.env.API_KEY_SECRET || "test-repro-10286-secret";
|
||||
|
||||
const caps = await import("../../src/lib/modelCapabilities.ts");
|
||||
const core = await import("../../src/lib/db/core.ts");
|
||||
const rulesDb = await import("../../src/lib/db/reasoningRoutingRules.ts");
|
||||
const policy = await import("../../src/lib/reasoningRouting/policy.ts");
|
||||
|
||||
async function resetStorage() {
|
||||
core.resetDbInstance();
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
|
||||
rulesDb.invalidateReasoningRoutingRuleCache();
|
||||
}
|
||||
|
||||
function ruleInput(patch: Record<string, unknown> = {}) {
|
||||
return {
|
||||
name: "Enable thinking on gemini-3.5-flash",
|
||||
description: "",
|
||||
scope: "global",
|
||||
apiKeyId: null,
|
||||
comboId: null,
|
||||
connectionId: null,
|
||||
modelPattern: "gemini-3.5-flash",
|
||||
sourceEffort: "any",
|
||||
requestTags: [],
|
||||
tagMatchMode: "any",
|
||||
effortMode: "inherit",
|
||||
targetEffort: null,
|
||||
targetKind: "keep",
|
||||
targetModel: null,
|
||||
targetComboId: null,
|
||||
budgetAction: "preserve",
|
||||
budgetTokens: null,
|
||||
priority: 0,
|
||||
enabled: true,
|
||||
...patch,
|
||||
};
|
||||
}
|
||||
|
||||
test.beforeEach(resetStorage);
|
||||
test.after(async () => {
|
||||
await resetStorage();
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test("gemini-3.5-flash (AI Studio provider) resolves as thinking-capable", () => {
|
||||
const resolved = caps.getResolvedModelCapabilities({
|
||||
provider: "gemini",
|
||||
model: "gemini-3.5-flash",
|
||||
});
|
||||
assert.equal(resolved.supportsThinking, true);
|
||||
});
|
||||
|
||||
test("reasoning_effort 'high' on gemini-3.5-flash is NOT rejected by routing policy", async () => {
|
||||
await rulesDb.createReasoningRoutingRule(ruleInput());
|
||||
const decision = await policy.resolveReasoningRoutingRule({
|
||||
sourceModel: "gemini/gemini-3.5-flash",
|
||||
sourceModelAliases: ["gemini-3.5-flash"],
|
||||
sourceEffort: "high",
|
||||
hasReasoningSignal: true,
|
||||
});
|
||||
assert.ok(decision, "a matching rule must produce a decision");
|
||||
assert.equal(decision.capability, "supported");
|
||||
});
|
||||
@@ -71,7 +71,7 @@ test("OpenAI -> Gemini request strips encrypted from Codex collaboration tool pa
|
||||
],
|
||||
};
|
||||
|
||||
const result = openaiToGeminiRequest("gemini-3.5-flash-low", body, false) as {
|
||||
const result = openaiToGeminiRequest("gemini-3.7-flash-low", body, false) as {
|
||||
tools?: Array<{ functionDeclarations?: Array<{ parameters: unknown }> }>;
|
||||
};
|
||||
|
||||
|
||||
@@ -14,6 +14,16 @@ const SAMPLE = {
|
||||
supportedGenerationMethods: ["generateContent", "countTokens", "batchGenerateContent"],
|
||||
thinking: true,
|
||||
},
|
||||
{
|
||||
name: "models/gemini-3.5-flash",
|
||||
displayName: "Gemini 3.5 Flash",
|
||||
supportedGenerationMethods: ["generateContent"],
|
||||
},
|
||||
{
|
||||
name: "models/gemini-3.5-flash-lite",
|
||||
displayName: "Gemini 3.5 Flash Lite",
|
||||
supportedGenerationMethods: ["generateContent"],
|
||||
},
|
||||
{
|
||||
name: "models/gemini-3-pro-image-preview",
|
||||
displayName: "Gemini 3 Pro Image Preview",
|
||||
@@ -48,6 +58,12 @@ test("parseGeminiModelsList strips the models/ prefix and maps display name", ()
|
||||
assert.deepEqual(flash!.supportedEndpoints, ["chat"]);
|
||||
});
|
||||
|
||||
test("parseGeminiModelsList excludes retired Gemini 3.5 Flash but keeps Flash Lite", () => {
|
||||
const ids = parseGeminiModelsList(SAMPLE).map((model) => model.id);
|
||||
assert.equal(ids.includes("gemini-3.5-flash"), false);
|
||||
assert.equal(ids.includes("gemini-3.5-flash-lite"), true);
|
||||
});
|
||||
|
||||
test("parseGeminiModelsList maps generateContent image models to the chat endpoint", () => {
|
||||
const models = parseGeminiModelsList(SAMPLE);
|
||||
const proImage = models.find((m) => m.id === "gemini-3-pro-image-preview");
|
||||
|
||||
@@ -56,7 +56,7 @@ test("OpenAI -> Gemini request strips strict from OpenAI-style function tool par
|
||||
],
|
||||
};
|
||||
|
||||
const result = openaiToGeminiRequest("gemini-3.5-flash-low", body, false) as {
|
||||
const result = openaiToGeminiRequest("gemini-3.7-flash-low", body, false) as {
|
||||
tools?: Array<{ functionDeclarations?: Array<{ parameters: unknown }> }>;
|
||||
};
|
||||
|
||||
|
||||
@@ -99,7 +99,7 @@ test("buildUrl uses chat/completions endpoint for gemini models", () => {
|
||||
};
|
||||
// Gemini has no native shim on Copilot — it stays on /chat/completions.
|
||||
assert.strictEqual(
|
||||
executor.buildUrl("gemini-3.5-flash", true, 0, credentials),
|
||||
executor.buildUrl("gemini-3.7-flash", true, 0, credentials),
|
||||
"https://ghe.company.com/chat/completions"
|
||||
);
|
||||
});
|
||||
|
||||
@@ -40,7 +40,11 @@ const EXPECTED: Record<InventoryKind, Record<string, number>> = {
|
||||
"src/app/api/v1/session-leases/route.ts": 1,
|
||||
"src/app/api/v1/videos/generations/route.ts": 2,
|
||||
"src/app/api/v1/web/fetch/route.ts": 1,
|
||||
"src/lib/embeddings/service.ts": 2,
|
||||
// #11088/#11271: third site is the synced local-endpoint route — it resolves
|
||||
// credentials through getProviderCredentials with the connection allowlist
|
||||
// from resolveLocalSyncedEndpointRoute, and handles allRateLimited, so it is
|
||||
// fenced the same way as the two pre-existing sites.
|
||||
"src/lib/embeddings/service.ts": 3,
|
||||
"src/lib/memory/embedding/index.ts": 1,
|
||||
"src/lib/search/executeWebSearch.ts": 2,
|
||||
"src/lib/skills/webFetchExecution.ts": 1,
|
||||
|
||||
49
tests/unit/helpers/mcpStreamMock.ts
Normal file
49
tests/unit/helpers/mcpStreamMock.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
|
||||
import type { Response as Resp } from "undici";
|
||||
|
||||
// Minimal fetch mock responses that satisfy what apiFetch needs.
|
||||
export function makeMcpResp(data: unknown, status = 200, headers: Record<string, string> = {}) {
|
||||
const hdrs = new Headers({ "content-type": "application/json", ...headers });
|
||||
const obj = {
|
||||
ok: status < 400,
|
||||
status,
|
||||
json: () => Promise.resolve(data),
|
||||
text: () => Promise.resolve(typeof data === "string" ? data : JSON.stringify(data)),
|
||||
headers: hdrs,
|
||||
} as unknown as Resp;
|
||||
return obj;
|
||||
}
|
||||
|
||||
export function makeMcpStreamFetch({
|
||||
toolResult = { content: [{ type: "text", text: "ok" }] },
|
||||
initStatus = 200,
|
||||
callStatus = 200,
|
||||
callError = false,
|
||||
} = {}) {
|
||||
return (async (url: string | URL, init?: unknown) => {
|
||||
const u = String(url);
|
||||
if (!u.includes("/api/mcp/stream")) {
|
||||
return makeMcpResp({ error: "not found" }, 404);
|
||||
}
|
||||
const body = init?.body ? JSON.parse(init.body) : {};
|
||||
if (body.method === "initialize") {
|
||||
return makeMcpResp(
|
||||
{ jsonrpc: "2.0", id: body.id, result: { protocolVersion: "2024-11-05", capabilities: {} } },
|
||||
initStatus,
|
||||
initStatus < 400 ? { "mcp-session-id": "sess-test" } : {},
|
||||
);
|
||||
}
|
||||
if (body.method === "tools/call") {
|
||||
if (callStatus !== 200) return makeMcpResp({ error: "tool failure" }, callStatus);
|
||||
if (callError) {
|
||||
return makeMcpResp({
|
||||
jsonrpc: "2.0",
|
||||
id: body.id,
|
||||
result: { content: [{ type: "text", text: "tool error" }], isError: true },
|
||||
});
|
||||
}
|
||||
return makeMcpResp({ jsonrpc: "2.0", id: body.id, result: toolResult });
|
||||
}
|
||||
return makeMcpResp({ error: "unknown method" }, 400);
|
||||
}) as unknown as typeof globalThis.fetch;
|
||||
}
|
||||
77
tests/unit/home-page-static.test.ts
Normal file
77
tests/unit/home-page-static.test.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import { describe, it } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const repoRoot = join(dirname(fileURLToPath(import.meta.url)), "../..");
|
||||
|
||||
function readHomePage(): string {
|
||||
return readFileSync(
|
||||
join(repoRoot, "src/app/(dashboard)/home/page.tsx"),
|
||||
"utf8",
|
||||
);
|
||||
}
|
||||
|
||||
function readReadinessCard(): string {
|
||||
return readFileSync(
|
||||
join(repoRoot, "src/app/(dashboard)/dashboard/FirstRunReadinessCard.tsx"),
|
||||
"utf8",
|
||||
);
|
||||
}
|
||||
|
||||
function readEnKeys(): string[] {
|
||||
const en = JSON.parse(
|
||||
readFileSync(join(repoRoot, "src/i18n/messages/en.json"), "utf8"),
|
||||
) as { home: Record<string, string> };
|
||||
return Object.keys(en.home);
|
||||
}
|
||||
|
||||
describe("home page first-run readiness card", () => {
|
||||
it("does not hard-redirect incomplete setup to onboarding", () => {
|
||||
const source = readHomePage();
|
||||
assert.doesNotMatch(source, /redirect\(["']\/dashboard\/onboarding["']\)/);
|
||||
assert.match(source, /FirstRunReadinessCard/);
|
||||
assert.match(source, /setupComplete=\{Boolean\(settings\.setupComplete\)\}/);
|
||||
});
|
||||
|
||||
it("keeps the readiness card dismissable via localStorage", () => {
|
||||
const source = readReadinessCard();
|
||||
assert.match(source, /omniroute-first-run-readiness-dismissed/);
|
||||
assert.match(source, /localStorage/);
|
||||
assert.match(source, /readinessContinue/);
|
||||
assert.match(source, /readinessDismiss/);
|
||||
});
|
||||
|
||||
it("uses t() keys for readiness copy", () => {
|
||||
const source = readReadinessCard();
|
||||
for (const key of [
|
||||
"readinessEyebrow",
|
||||
"readinessTitle",
|
||||
"readinessSubtitle",
|
||||
"readinessStep1",
|
||||
"readinessStep2",
|
||||
"readinessStep3",
|
||||
"readinessStep4",
|
||||
]) {
|
||||
assert.match(source, new RegExp(key));
|
||||
}
|
||||
});
|
||||
|
||||
it("new i18n keys exist in en.json home namespace", () => {
|
||||
const keys = readEnKeys();
|
||||
for (const key of [
|
||||
"readinessEyebrow",
|
||||
"readinessTitle",
|
||||
"readinessSubtitle",
|
||||
"readinessStep1",
|
||||
"readinessStep2",
|
||||
"readinessStep3",
|
||||
"readinessStep4",
|
||||
"readinessContinue",
|
||||
"readinessDismiss",
|
||||
]) {
|
||||
assert.ok(keys.includes(key), `Missing home.${key}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -279,6 +279,7 @@ test("antigravity sync dynamically builds and saves mitmAlias mappings", async (
|
||||
mode: "sync",
|
||||
fetchedModels: [
|
||||
{ id: "gemini-3.5-flash", name: "Gemini 3.5 Flash" },
|
||||
{ id: "gemini-3.7-flash-high", name: "Gemini 3.7 Flash High" },
|
||||
{ id: "custom-antigravity-model", name: "Custom Antigravity Model" },
|
||||
],
|
||||
});
|
||||
@@ -289,8 +290,13 @@ test("antigravity sync dynamically builds and saves mitmAlias mappings", async (
|
||||
const mitmMappings = await modelsDb.getMitmAlias("antigravity");
|
||||
console.log("MITM MAPPINGS IN TEST:", mitmMappings);
|
||||
|
||||
// Should contain standard mapping
|
||||
assert.equal(mitmMappings["gemini-3.5-flash"], "antigravity/gemini-3.5-flash");
|
||||
// Retired models reported by upstream must not be imported or mapped.
|
||||
assert.equal(mitmMappings["gemini-3.5-flash"], undefined);
|
||||
assert.equal(
|
||||
models.some((model) => model.id === "gemini-3.5-flash"),
|
||||
false
|
||||
);
|
||||
assert.equal(mitmMappings["gemini-3.7-flash-high"], "antigravity/gemini-3.7-flash-high");
|
||||
assert.equal(mitmMappings["custom-antigravity-model"], "antigravity/custom-antigravity-model");
|
||||
|
||||
// Removed Antigravity 2.0 preview/agent aliases must not be reintroduced.
|
||||
|
||||
@@ -154,21 +154,14 @@ test("unknown models keep maxOutputTokens null instead of using a generic defaul
|
||||
);
|
||||
});
|
||||
|
||||
test("provider-neutral Gemini 3.5 tier IDs retain their non-thinking capabilities", () => {
|
||||
test("retired Gemini 3.5 Flash IDs have no provider-neutral model specs", () => {
|
||||
for (const modelId of [
|
||||
"gemini-3.5-flash",
|
||||
"gemini-3.5-flash-extra-low",
|
||||
"gemini-3.5-flash-low",
|
||||
"gemini-3-flash-agent",
|
||||
]) {
|
||||
const spec = MODEL_SPECS[modelId];
|
||||
assert.ok(spec, `missing exact MODEL_SPECS entry for ${modelId}`);
|
||||
const capabilities = modelCapabilities.getResolvedModelCapabilities(modelId);
|
||||
assert.equal(capabilities.contextWindow, 1048576, modelId);
|
||||
assert.equal(capabilities.maxOutputTokens, 65536, modelId);
|
||||
// These ids encode the upstream reasoning tier and do not accept a client-supplied effort.
|
||||
assert.equal(capabilities.supportsThinking, false, modelId);
|
||||
assert.equal(capabilities.supportsTools, true, modelId);
|
||||
assert.equal(capabilities.supportsVision, true, modelId);
|
||||
assert.equal(MODEL_SPECS[modelId], undefined, modelId);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -702,6 +702,7 @@ test("v1 models catalog exposes current Antigravity aliases without retired mode
|
||||
assert.equal(ids.has("antigravity/gemini-3.6-flash-high"), false);
|
||||
assert.equal(ids.has("antigravity/gemini-3.6-flash-medium"), false);
|
||||
assert.equal(ids.has("antigravity/gemini-3.6-flash-low"), false);
|
||||
assert.equal(ids.has("antigravity/gemini-3.5-flash"), false);
|
||||
assert.equal(ids.has("antigravity/gemini-3.5-flash-extra-low"), false);
|
||||
assert.equal(ids.has("antigravity/gemini-3.5-flash-low"), false);
|
||||
assert.equal(ids.has("antigravity/gemini-3-flash-agent"), false);
|
||||
|
||||
205
tests/unit/ollama-local-capabilities-routing.test.ts
Normal file
205
tests/unit/ollama-local-capabilities-routing.test.ts
Normal file
@@ -0,0 +1,205 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-ollama-capabilities-"));
|
||||
process.env.DATA_DIR = TEST_DATA_DIR;
|
||||
process.env.APP_LOG_TO_FILE = "false";
|
||||
process.env.API_KEY_SECRET = "ollama-capabilities-test-secret";
|
||||
process.env.REQUIRE_API_KEY = "false";
|
||||
|
||||
const core = await import("../../src/lib/db/core.ts");
|
||||
const providersDb = await import("../../src/lib/db/providers.ts");
|
||||
const modelsDb = await import("../../src/lib/db/models.ts");
|
||||
const providerModelsRoute = await import("../../src/app/api/providers/[id]/models/route.ts");
|
||||
const v1ModelsCatalog = await import("../../src/app/api/v1/models/catalog.ts");
|
||||
const imageRoute = await import("../../src/app/api/v1/images/generations/route.ts");
|
||||
const { createEmbeddingResponse } = await import("../../src/lib/embeddings/service.ts");
|
||||
|
||||
const originalFetch = globalThis.fetch;
|
||||
|
||||
function resetStorage() {
|
||||
globalThis.fetch = originalFetch;
|
||||
core.resetDbInstance();
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
async function seedOllamaConnection(baseUrl = "http://127.0.0.1:11434/v1", priority = 1) {
|
||||
return providersDb.createProviderConnection({
|
||||
provider: "ollama-local",
|
||||
authType: "apikey",
|
||||
name: "Ollama test host",
|
||||
apiKey: "test-key",
|
||||
isActive: true,
|
||||
testStatus: "active",
|
||||
priority,
|
||||
providerSpecificData: { baseUrl },
|
||||
});
|
||||
}
|
||||
|
||||
test.beforeEach(resetStorage);
|
||||
|
||||
test.after(() => {
|
||||
globalThis.fetch = originalFetch;
|
||||
core.resetDbInstance();
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test("Ollama discovery maps /api/show capabilities into connection-scoped model metadata", async () => {
|
||||
const connection = await seedOllamaConnection();
|
||||
const showCapabilities: Record<string, string[]> = {
|
||||
"image-model": ["image"],
|
||||
"embedding-model": ["embedding"],
|
||||
"chat-model": ["completion", "vision", "tools", "thinking"],
|
||||
};
|
||||
const calledUrls: string[] = [];
|
||||
|
||||
globalThis.fetch = async (input, init = {}) => {
|
||||
const url = String(input);
|
||||
calledUrls.push(url);
|
||||
if (url.endsWith("/v1/models")) {
|
||||
return Response.json({
|
||||
data: Object.keys(showCapabilities).map((id) => ({ id, object: "model" })),
|
||||
});
|
||||
}
|
||||
if (url.endsWith("/api/show")) {
|
||||
const body = JSON.parse(String(init.body || "{}")) as { model?: string };
|
||||
return Response.json({ capabilities: showCapabilities[body.model || ""] || [] });
|
||||
}
|
||||
return new Response("not found", { status: 404 });
|
||||
};
|
||||
|
||||
const response = await providerModelsRoute.GET(
|
||||
new Request(`http://localhost/api/providers/${connection.id}/models?refresh=true`),
|
||||
{ params: { id: connection.id } }
|
||||
);
|
||||
const body = (await response.json()) as {
|
||||
models: Array<{
|
||||
id: string;
|
||||
apiFormat?: string;
|
||||
supportedEndpoints?: string[];
|
||||
supportsVision?: boolean;
|
||||
supportsTools?: boolean;
|
||||
supportsThinking?: boolean;
|
||||
}>;
|
||||
};
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
assert.ok(calledUrls.some((url) => url.endsWith("/api/show")));
|
||||
assert.deepEqual(body.models.find((model) => model.id === "image-model")?.supportedEndpoints, [
|
||||
"images",
|
||||
]);
|
||||
assert.equal(
|
||||
body.models.find((model) => model.id === "image-model")?.apiFormat,
|
||||
"images-generations"
|
||||
);
|
||||
assert.deepEqual(
|
||||
body.models.find((model) => model.id === "embedding-model")?.supportedEndpoints,
|
||||
["embeddings"]
|
||||
);
|
||||
assert.equal(
|
||||
body.models.find((model) => model.id === "embedding-model")?.apiFormat,
|
||||
"embeddings"
|
||||
);
|
||||
const chatModel = body.models.find((model) => model.id === "chat-model");
|
||||
assert.deepEqual(chatModel?.supportedEndpoints, ["chat"]);
|
||||
assert.equal(chatModel?.supportsVision, true);
|
||||
assert.equal(chatModel?.supportsTools, true);
|
||||
assert.equal(chatModel?.supportsThinking, true);
|
||||
|
||||
const persisted = await modelsDb.getSyncedAvailableModelsForConnection(
|
||||
"ollama-local",
|
||||
connection.id
|
||||
);
|
||||
assert.deepEqual(persisted.find((model) => model.id === "image-model")?.supportedEndpoints, [
|
||||
"images",
|
||||
]);
|
||||
assert.deepEqual(persisted.find((model) => model.id === "embedding-model")?.supportedEndpoints, [
|
||||
"embeddings",
|
||||
]);
|
||||
|
||||
const catalogResponse = await v1ModelsCatalog.getUnifiedModelsResponse(
|
||||
new Request("http://localhost/v1/models")
|
||||
);
|
||||
const catalog = (await catalogResponse.json()) as {
|
||||
data: Array<{
|
||||
id: string;
|
||||
type?: string;
|
||||
supported_endpoints?: string[];
|
||||
capabilities?: Record<string, boolean>;
|
||||
}>;
|
||||
};
|
||||
const imageCatalogModel = catalog.data.find((model) => model.id.endsWith("/image-model"));
|
||||
assert.equal(imageCatalogModel?.type, "image");
|
||||
assert.deepEqual(imageCatalogModel?.supported_endpoints, ["images"]);
|
||||
const embeddingCatalogModel = catalog.data.find((model) => model.id.endsWith("/embedding-model"));
|
||||
assert.equal(embeddingCatalogModel?.type, "embedding");
|
||||
assert.deepEqual(embeddingCatalogModel?.supported_endpoints, ["embeddings"]);
|
||||
const chatCatalogModel = catalog.data.find((model) => model.id.endsWith("/chat-model"));
|
||||
assert.equal(chatCatalogModel?.capabilities?.vision, true);
|
||||
assert.equal(chatCatalogModel?.capabilities?.tool_calling, true);
|
||||
assert.equal(chatCatalogModel?.capabilities?.reasoning, true);
|
||||
});
|
||||
|
||||
test("Ollama image model routes through its advertising connection", async () => {
|
||||
await seedOllamaConnection("http://127.0.0.1:11434/v1", 1);
|
||||
const connection = await seedOllamaConnection("http://127.0.0.1:11435/v1", 2);
|
||||
await modelsDb.replaceSyncedAvailableModelsForConnection("ollama-local", connection.id, [
|
||||
{
|
||||
id: "image-model",
|
||||
name: "Image Model",
|
||||
apiFormat: "images-generations",
|
||||
supportedEndpoints: ["images"],
|
||||
},
|
||||
]);
|
||||
|
||||
let capturedUrl = "";
|
||||
globalThis.fetch = async (input) => {
|
||||
capturedUrl = String(input);
|
||||
return Response.json({ data: [{ b64_json: "aW1hZ2U=" }] });
|
||||
};
|
||||
|
||||
const response = await imageRoute.POST(
|
||||
new Request("http://localhost/v1/images/generations", {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({ model: "ollama-local/image-model", prompt: "test image" }),
|
||||
})
|
||||
);
|
||||
|
||||
assert.equal(response.status, 200, await response.text());
|
||||
assert.equal(capturedUrl, "http://127.0.0.1:11435/v1/images/generations");
|
||||
});
|
||||
|
||||
test("Ollama embedding model routes through its advertising connection", async () => {
|
||||
await seedOllamaConnection("http://127.0.0.1:11434/v1", 1);
|
||||
const connection = await seedOllamaConnection("http://127.0.0.1:11436/v1", 2);
|
||||
await modelsDb.replaceSyncedAvailableModelsForConnection("ollama-local", connection.id, [
|
||||
{
|
||||
id: "embedding-model",
|
||||
name: "Embedding Model",
|
||||
apiFormat: "embeddings",
|
||||
supportedEndpoints: ["embeddings"],
|
||||
},
|
||||
]);
|
||||
|
||||
let capturedUrl = "";
|
||||
globalThis.fetch = async (input) => {
|
||||
capturedUrl = String(input);
|
||||
return Response.json({
|
||||
data: [{ object: "embedding", embedding: [0.1, 0.2], index: 0 }],
|
||||
usage: { prompt_tokens: 2, total_tokens: 2 },
|
||||
});
|
||||
};
|
||||
|
||||
const response = await createEmbeddingResponse({
|
||||
model: "ollama-local/embedding-model",
|
||||
input: "hello",
|
||||
});
|
||||
|
||||
assert.equal(response.status, 200, await response.text());
|
||||
assert.equal(capturedUrl, "http://127.0.0.1:11436/v1/embeddings");
|
||||
});
|
||||
@@ -60,7 +60,7 @@ test("buildOmniRouteResponseMetaHeaders keeps ASCII model header values unchange
|
||||
});
|
||||
|
||||
test("buildOmniRouteResponseMetaHeaders percent-encodes non-ASCII model header values", () => {
|
||||
const model = "free-mix/[假流式]gemini-3.5-flash";
|
||||
const model = "free-mix/[假流式]gemini-3.7-flash";
|
||||
const headers = buildOmniRouteResponseMetaHeaders({
|
||||
provider: "openai",
|
||||
model,
|
||||
|
||||
120
tests/unit/opencode-go-console-go-effort-clamp.test.ts
Normal file
120
tests/unit/opencode-go-console-go-effort-clamp.test.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
/**
|
||||
* Console Go (opencode.ai/zen/go/v1) reasoning-effort vocabulary clamp.
|
||||
*
|
||||
* Live-reproduced 2026-08-23 via the Hermes Telegram bot → /v1/chat/completions:
|
||||
* `opencode-go/ox-alpha-free` rejects every reasoning_effort except
|
||||
* {low, high, max} whenever the request carries tools —
|
||||
*
|
||||
* [400] Error from provider (Console Go): Upstream request failed: [1210]
|
||||
* This model always engages in thinking and cannot be disabled; please use
|
||||
* low, high, or max
|
||||
*
|
||||
* Hermes sends reasoning_effort:"medium" with 24 tools and died on every turn.
|
||||
* Two gaps let the bad value reach the upstream verbatim:
|
||||
* 1. `ox-alpha-free` is a discovery-synced model with no static registry
|
||||
* entry declaring its effort vocabulary.
|
||||
* 2. sanitizeReasoningEffortForProvider only consults declared
|
||||
* supportedThinkingEfforts in the `max` branch (max fallback); other
|
||||
* out-of-vocabulary values pass through untouched.
|
||||
*
|
||||
* Fix under test: declare ["low","high","max"] on the registry entry and add a
|
||||
* generic explicit-capability clamp that remaps any out-of-vocabulary effort to
|
||||
* the nearest declared tier (smallest ranked ≥ requested, else the highest).
|
||||
* Models without a declaration keep today's pass-through behavior (#8057).
|
||||
*/
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
const { sanitizeReasoningEffortForProvider } = await import("../../open-sse/executors/base.ts");
|
||||
const { REGISTRY } = await import("../../open-sse/config/providerRegistry.ts");
|
||||
|
||||
function makeLog() {
|
||||
const messages: Array<[string, string]> = [];
|
||||
return {
|
||||
info: (tag: string, msg: string) => messages.push([tag, msg]),
|
||||
messages,
|
||||
};
|
||||
}
|
||||
|
||||
const HERMES_BODY = {
|
||||
model: "ox-alpha-free",
|
||||
max_tokens: 65536,
|
||||
stream_options: { include_usage: true },
|
||||
messages: [{ role: "user", content: "Start telegram bot" }],
|
||||
tools: [
|
||||
{
|
||||
type: "function",
|
||||
function: { name: "clarify", description: "ask", parameters: { type: "object" } },
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
test("registry: opencode-go declares ox-alpha-free with the live-verified Console Go effort set", () => {
|
||||
const entry = REGISTRY["opencode-go"];
|
||||
assert.ok(entry, "opencode-go registry entry must exist");
|
||||
const model = entry.models.find((m) => m.id === "ox-alpha-free");
|
||||
assert.ok(model, "ox-alpha-free must be registered on opencode-go");
|
||||
assert.deepEqual(model.supportedThinkingEfforts, ["low", "high", "max"]);
|
||||
});
|
||||
|
||||
test("clamp: medium → high for ox-alpha-free (the exact Hermes failure)", () => {
|
||||
const log = makeLog();
|
||||
const body = { ...HERMES_BODY, reasoning_effort: "medium" };
|
||||
const result = sanitizeReasoningEffortForProvider(body, "opencode-go", "ox-alpha-free", log);
|
||||
assert.notEqual(result, body, "must return a new object when mutating");
|
||||
assert.equal((result as Record<string, unknown>).reasoning_effort, "high");
|
||||
assert.ok(
|
||||
log.messages.some(([tag, m]) => tag === "REASONING_SANITIZE" && /medium → high/.test(m)),
|
||||
"logs the mapping"
|
||||
);
|
||||
});
|
||||
|
||||
test("clamp: disable-shaped efforts map to low (upstream refuses to stop thinking)", () => {
|
||||
for (const effort of ["none", "minimal"]) {
|
||||
const body = { ...HERMES_BODY, reasoning_effort: effort };
|
||||
const result = sanitizeReasoningEffortForProvider(body, "opencode-go", "ox-alpha-free", null);
|
||||
assert.equal(
|
||||
(result as Record<string, unknown>).reasoning_effort,
|
||||
"low",
|
||||
`${effort} → low`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test("clamp: xhigh → max for ox-alpha-free", () => {
|
||||
const body = { ...HERMES_BODY, reasoning_effort: "xhigh" };
|
||||
const result = sanitizeReasoningEffortForProvider(body, "opencode-go", "ox-alpha-free", null);
|
||||
assert.equal((result as Record<string, unknown>).reasoning_effort, "max");
|
||||
});
|
||||
|
||||
test("clamp: in-vocabulary efforts pass through untouched", () => {
|
||||
for (const effort of ["low", "high", "max"]) {
|
||||
const body = { ...HERMES_BODY, reasoning_effort: effort };
|
||||
const result = sanitizeReasoningEffortForProvider(body, "opencode-go", "ox-alpha-free", null);
|
||||
assert.equal(result, body, `${effort} must not be rewritten`);
|
||||
assert.equal((result as Record<string, unknown>).reasoning_effort, effort);
|
||||
}
|
||||
});
|
||||
|
||||
test("clamp writes back to every carrier present (top-level + reasoning.effort + output_config.effort)", () => {
|
||||
const body = {
|
||||
...HERMES_BODY,
|
||||
reasoning_effort: "medium",
|
||||
reasoning: { effort: "medium" },
|
||||
output_config: { effort: "medium" },
|
||||
};
|
||||
const result = sanitizeReasoningEffortForProvider(body, "opencode-go", "ox-alpha-free", null) as Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
assert.equal(result.reasoning_effort, "high");
|
||||
assert.deepEqual(result.reasoning, { effort: "high" });
|
||||
assert.deepEqual(result.output_config, { effort: "high" });
|
||||
});
|
||||
|
||||
test("no declaration → pass-through unchanged (#8057 policy for unlisted models)", () => {
|
||||
const body = { ...HERMES_BODY, model: "some-unregistered-model", reasoning_effort: "medium" };
|
||||
const result = sanitizeReasoningEffortForProvider(body, "opencode-go", "some-unregistered-model", null);
|
||||
assert.equal(result, body, "undeclared models keep today's trust-the-upstream behavior");
|
||||
assert.equal((result as Record<string, unknown>).reasoning_effort, "medium");
|
||||
});
|
||||
@@ -288,7 +288,7 @@ test("hidden provider models are filtered from per-model quota rows", () => {
|
||||
});
|
||||
const hidden = providerLimitUtils.collectHiddenQuotaModelIds("antigravity", {
|
||||
models: [{ id: "antigravity/gpt-oss-120b-medium", isHidden: true }],
|
||||
modelCompatOverrides: [{ id: "gemini-3.5-flash", isHidden: true }],
|
||||
modelCompatOverrides: [{ id: "gemini-3.7-flash", isHidden: true }],
|
||||
});
|
||||
const visible = providerLimitUtils.filterHiddenModelQuotas("antigravity", quotas, hidden);
|
||||
|
||||
|
||||
@@ -130,6 +130,9 @@ describe("isOriginAllowed", () => {
|
||||
assert.equal(isOriginAllowed("http://127.0.0.1:20128", EMPTY_ENV), true);
|
||||
assert.equal(isOriginAllowed("http://localhost:20128", EMPTY_ENV), true);
|
||||
assert.equal(isOriginAllowed("http://[::1]:20128", EMPTY_ENV), true);
|
||||
// 0.0.0.0 is loopback-equivalent in the browser; the dashboard is often
|
||||
// opened at http://0.0.0.0:20128, which sends exactly that Origin on WS.
|
||||
assert.equal(isOriginAllowed("http://0.0.0.0:20128", EMPTY_ENV), true);
|
||||
});
|
||||
|
||||
it("accepts an Origin matching LIVE_WS_ALLOWED_ORIGINS", () => {
|
||||
|
||||
@@ -394,7 +394,7 @@ test("parseSSEToGeminiResponse extracts tool calls from textual format", () => {
|
||||
})}`,
|
||||
].join("\n");
|
||||
|
||||
const parsed = parseSSEToGeminiResponse(rawSSE, "gemini-3.5-flash-low");
|
||||
const parsed = parseSSEToGeminiResponse(rawSSE, "gemini-3.7-flash-low");
|
||||
|
||||
assert.ok(parsed);
|
||||
assert.equal(parsed.choices[0].finish_reason, "tool_calls");
|
||||
|
||||
@@ -232,14 +232,14 @@ test("createSSEStream passthrough converts textual tool-call content into struct
|
||||
id: "chatcmpl_textual_tool",
|
||||
object: "chat.completion.chunk",
|
||||
created: 1,
|
||||
model: "antigravity/gemini-3.5-flash-low",
|
||||
model: "antigravity/gemini-3.7-flash-low",
|
||||
choices: [{ index: 0, delta: { role: "assistant", content: toolText } }],
|
||||
})}\n\n`,
|
||||
`data: ${JSON.stringify({
|
||||
id: "chatcmpl_textual_tool",
|
||||
object: "chat.completion.chunk",
|
||||
created: 1,
|
||||
model: "antigravity/gemini-3.5-flash-low",
|
||||
model: "antigravity/gemini-3.7-flash-low",
|
||||
choices: [{ index: 0, delta: {}, finish_reason: "stop" }],
|
||||
})}\n\n`,
|
||||
],
|
||||
@@ -247,7 +247,7 @@ test("createSSEStream passthrough converts textual tool-call content into struct
|
||||
mode: "passthrough",
|
||||
sourceFormat: FORMATS.OPENAI,
|
||||
provider: "antigravity",
|
||||
model: "antigravity/gemini-3.5-flash-low",
|
||||
model: "antigravity/gemini-3.7-flash-low",
|
||||
body: {
|
||||
messages: [{ role: "user", content: "inspect db" }],
|
||||
},
|
||||
@@ -284,21 +284,21 @@ test("createSSEStream passthrough converts split textual tool-call content at co
|
||||
id: "chatcmpl_split_textual_tool",
|
||||
object: "chat.completion.chunk",
|
||||
created: 1,
|
||||
model: "antigravity/gemini-3.5-flash-low",
|
||||
model: "antigravity/gemini-3.7-flash-low",
|
||||
choices: [{ index: 0, delta: { role: "assistant", content: chunks[0] } }],
|
||||
})}\n\n`,
|
||||
`data: ${JSON.stringify({
|
||||
id: "chatcmpl_split_textual_tool",
|
||||
object: "chat.completion.chunk",
|
||||
created: 1,
|
||||
model: "antigravity/gemini-3.5-flash-low",
|
||||
model: "antigravity/gemini-3.7-flash-low",
|
||||
choices: [{ index: 0, delta: { content: chunks[1] } }],
|
||||
})}\n\n`,
|
||||
`data: ${JSON.stringify({
|
||||
id: "chatcmpl_split_textual_tool",
|
||||
object: "chat.completion.chunk",
|
||||
created: 1,
|
||||
model: "antigravity/gemini-3.5-flash-low",
|
||||
model: "antigravity/gemini-3.7-flash-low",
|
||||
choices: [{ index: 0, delta: {}, finish_reason: "stop" }],
|
||||
})}\n\n`,
|
||||
],
|
||||
@@ -306,7 +306,7 @@ test("createSSEStream passthrough converts split textual tool-call content at co
|
||||
mode: "passthrough",
|
||||
sourceFormat: FORMATS.OPENAI,
|
||||
provider: "antigravity",
|
||||
model: "antigravity/gemini-3.5-flash-low",
|
||||
model: "antigravity/gemini-3.7-flash-low",
|
||||
body: { messages: [{ role: "user", content: "inspect db" }] },
|
||||
onComplete(payload) {
|
||||
onCompletePayload = payload;
|
||||
@@ -340,28 +340,28 @@ test("createSSEStream passthrough handles textual tool-call content split inside
|
||||
id: "chatcmpl_split_prefix_textual_tool",
|
||||
object: "chat.completion.chunk",
|
||||
created: 1,
|
||||
model: "antigravity/gemini-3.5-flash-low",
|
||||
model: "antigravity/gemini-3.7-flash-low",
|
||||
choices: [{ index: 0, delta: { role: "assistant", content: chunks[0] } }],
|
||||
})}\n\n`,
|
||||
`data: ${JSON.stringify({
|
||||
id: "chatcmpl_split_prefix_textual_tool",
|
||||
object: "chat.completion.chunk",
|
||||
created: 1,
|
||||
model: "antigravity/gemini-3.5-flash-low",
|
||||
model: "antigravity/gemini-3.7-flash-low",
|
||||
choices: [{ index: 0, delta: { content: chunks[1] } }],
|
||||
})}\n\n`,
|
||||
`data: ${JSON.stringify({
|
||||
id: "chatcmpl_split_prefix_textual_tool",
|
||||
object: "chat.completion.chunk",
|
||||
created: 1,
|
||||
model: "antigravity/gemini-3.5-flash-low",
|
||||
model: "antigravity/gemini-3.7-flash-low",
|
||||
choices: [{ index: 0, delta: { content: chunks[2] } }],
|
||||
})}\n\n`,
|
||||
`data: ${JSON.stringify({
|
||||
id: "chatcmpl_split_prefix_textual_tool",
|
||||
object: "chat.completion.chunk",
|
||||
created: 1,
|
||||
model: "antigravity/gemini-3.5-flash-low",
|
||||
model: "antigravity/gemini-3.7-flash-low",
|
||||
choices: [{ index: 0, delta: {}, finish_reason: "stop" }],
|
||||
})}\n\n`,
|
||||
],
|
||||
@@ -369,7 +369,7 @@ test("createSSEStream passthrough handles textual tool-call content split inside
|
||||
mode: "passthrough",
|
||||
sourceFormat: FORMATS.OPENAI,
|
||||
provider: "antigravity",
|
||||
model: "antigravity/gemini-3.5-flash-low",
|
||||
model: "antigravity/gemini-3.7-flash-low",
|
||||
body: { messages: [{ role: "user", content: "inspect db" }] },
|
||||
onComplete(payload) {
|
||||
onCompletePayload = payload;
|
||||
@@ -515,14 +515,14 @@ Arguments: {"path":"/opt/OmniRoute/src","target":"files"}`;
|
||||
id: "chatcmpl_unknown_textual_tool",
|
||||
object: "chat.completion.chunk",
|
||||
created: 1,
|
||||
model: "antigravity/gemini-3.5-flash-low",
|
||||
model: "antigravity/gemini-3.7-flash-low",
|
||||
choices: [{ index: 0, delta: { role: "assistant", content: toolText } }],
|
||||
})}\n\n`,
|
||||
`data: ${JSON.stringify({
|
||||
id: "chatcmpl_unknown_textual_tool",
|
||||
object: "chat.completion.chunk",
|
||||
created: 1,
|
||||
model: "antigravity/gemini-3.5-flash-low",
|
||||
model: "antigravity/gemini-3.7-flash-low",
|
||||
choices: [{ index: 0, delta: {}, finish_reason: "stop" }],
|
||||
})}\n\n`,
|
||||
],
|
||||
@@ -530,7 +530,7 @@ Arguments: {"path":"/opt/OmniRoute/src","target":"files"}`;
|
||||
mode: "passthrough",
|
||||
sourceFormat: FORMATS.OPENAI,
|
||||
provider: "antigravity",
|
||||
model: "antigravity/gemini-3.5-flash-low",
|
||||
model: "antigravity/gemini-3.7-flash-low",
|
||||
body: {
|
||||
messages: [{ role: "user", content: "inspect files" }],
|
||||
tools: [
|
||||
@@ -561,14 +561,14 @@ test("createSSEStream passthrough suppresses malformed textual tool-call content
|
||||
id: "chatcmpl_malformed_textual_tool",
|
||||
object: "chat.completion.chunk",
|
||||
created: 1,
|
||||
model: "antigravity/gemini-3.5-flash-low",
|
||||
model: "antigravity/gemini-3.7-flash-low",
|
||||
choices: [{ index: 0, delta: { role: "assistant", content: malformedToolText } }],
|
||||
})}\n\n`,
|
||||
`data: ${JSON.stringify({
|
||||
id: "chatcmpl_malformed_textual_tool",
|
||||
object: "chat.completion.chunk",
|
||||
created: 1,
|
||||
model: "antigravity/gemini-3.5-flash-low",
|
||||
model: "antigravity/gemini-3.7-flash-low",
|
||||
choices: [{ index: 0, delta: {}, finish_reason: "stop" }],
|
||||
})}\n\n`,
|
||||
],
|
||||
@@ -576,7 +576,7 @@ test("createSSEStream passthrough suppresses malformed textual tool-call content
|
||||
mode: "passthrough",
|
||||
sourceFormat: FORMATS.OPENAI,
|
||||
provider: "antigravity",
|
||||
model: "antigravity/gemini-3.5-flash-low",
|
||||
model: "antigravity/gemini-3.7-flash-low",
|
||||
body: { messages: [{ role: "user", content: "inspect db" }] },
|
||||
onComplete(payload) {
|
||||
onCompletePayload = payload;
|
||||
@@ -617,7 +617,7 @@ test("createSSEStream suppresses malformed compact textual tool-call content", a
|
||||
targetFormat: FORMATS.ANTIGRAVITY,
|
||||
sourceFormat: FORMATS.OPENAI,
|
||||
provider: "antigravity",
|
||||
model: "antigravity/gemini-3.5-flash-low",
|
||||
model: "antigravity/gemini-3.7-flash-low",
|
||||
body: { messages: [{ role: "user", content: "inspect files" }] },
|
||||
onComplete(payload) {
|
||||
onCompletePayload = payload;
|
||||
@@ -1024,7 +1024,7 @@ Arguments: {"command":"systemctl status omniroute"}`;
|
||||
response: {
|
||||
id: "resp_textual_tool",
|
||||
object: "response",
|
||||
model: "antigravity/gemini-3.5-flash-low",
|
||||
model: "antigravity/gemini-3.7-flash-low",
|
||||
status: "completed",
|
||||
output: [],
|
||||
usage: { input_tokens: 10, output_tokens: 4, total_tokens: 14 },
|
||||
@@ -1038,7 +1038,7 @@ Arguments: {"command":"systemctl status omniroute"}`;
|
||||
sourceFormat: FORMATS.OPENAI_RESPONSES,
|
||||
clientResponseFormat: FORMATS.OPENAI_RESPONSES,
|
||||
provider: "antigravity",
|
||||
model: "antigravity/gemini-3.5-flash-low",
|
||||
model: "antigravity/gemini-3.7-flash-low",
|
||||
body: {
|
||||
input: "check service",
|
||||
tools: [{ type: "function", name: "terminal", parameters: { type: "object" } }],
|
||||
|
||||
@@ -30,6 +30,7 @@ test("T28: antigravity static catalog exposes only callable Gemini tier IDs", ()
|
||||
assert.ok(!staticIds.includes("gemini-3.6-flash-high"));
|
||||
assert.ok(!staticIds.includes("gemini-3.6-flash-medium"));
|
||||
assert.ok(!staticIds.includes("gemini-3.6-flash-low"));
|
||||
assert.ok(!staticIds.includes("gemini-3.5-flash"));
|
||||
assert.ok(!staticIds.includes("gemini-3.5-flash-extra-low"));
|
||||
assert.ok(!staticIds.includes("gemini-3.5-flash-low"));
|
||||
assert.ok(!staticIds.includes("gemini-3-flash-agent"));
|
||||
|
||||
@@ -607,7 +607,7 @@ test("OpenAI -> Antigravity wraps Gemini requests in a Cloud Code envelope", ()
|
||||
|
||||
test("OpenAI -> Antigravity Gemini omits signature-less historical tool calls and keeps response context", () => {
|
||||
const result = openaiToAntigravityRequest(
|
||||
"gemini-3.5-flash-low",
|
||||
"gemini-3.7-flash-low",
|
||||
{
|
||||
messages: [
|
||||
{ role: "user", content: "Update todo" },
|
||||
@@ -686,7 +686,7 @@ test("OpenAI -> Antigravity Gemini omits signature-less historical tool calls an
|
||||
|
||||
test("OpenAI -> Antigravity preserves multiple signature-less historical tool responses as context", () => {
|
||||
const result = openaiToAntigravityRequest(
|
||||
"gemini-3.5-flash-low",
|
||||
"gemini-3.7-flash-low",
|
||||
{
|
||||
messages: [
|
||||
{ role: "user", content: "Inspect OmniRoute config" },
|
||||
@@ -747,7 +747,7 @@ test("OpenAI -> Antigravity preserves signed Gemini tool calls in native form",
|
||||
storeGeminiThoughtSignature(buildGeminiThoughtSignatureKey(ns, toolId), "SIG_AG_SIGNED_XYZ");
|
||||
|
||||
const result = openaiToAntigravityRequest(
|
||||
"gemini-3.5-flash-low",
|
||||
"gemini-3.7-flash-low",
|
||||
{
|
||||
messages: [
|
||||
{ role: "user", content: "Read status" },
|
||||
@@ -787,7 +787,7 @@ test("OpenAI -> Antigravity preserves signed Gemini tool calls in native form",
|
||||
|
||||
test("OpenAI -> Antigravity escapes signature-less tool response context content", () => {
|
||||
const result = openaiToAntigravityRequest(
|
||||
"gemini-3.5-flash-low",
|
||||
"gemini-3.7-flash-low",
|
||||
{
|
||||
messages: [
|
||||
{ role: "user", content: "Inspect previous output" },
|
||||
|
||||
@@ -346,7 +346,7 @@ test("Gemini stream: converts textual Tool call block to structured tool_calls",
|
||||
const result = geminiToOpenAIResponse(
|
||||
{
|
||||
responseId: "resp-textual-tool",
|
||||
modelVersion: "gemini-3.5-flash-low",
|
||||
modelVersion: "gemini-3.7-flash-low",
|
||||
candidates: [
|
||||
{
|
||||
content: {
|
||||
@@ -386,7 +386,7 @@ test("Gemini stream: routes textual reasoning tags to reasoning_content before t
|
||||
const result = geminiToOpenAIResponse(
|
||||
{
|
||||
responseId: "resp-textual-thought-tool",
|
||||
modelVersion: "gemini-3.5-flash-high",
|
||||
modelVersion: "gemini-3.7-flash-high",
|
||||
candidates: [
|
||||
{
|
||||
content: {
|
||||
@@ -431,7 +431,7 @@ test("Gemini stream: keeps textual reasoning hidden across split chunks", () =>
|
||||
const first = geminiToOpenAIResponse(
|
||||
{
|
||||
responseId: "resp-split-thought",
|
||||
modelVersion: "gemini-3.5-flash-high",
|
||||
modelVersion: "gemini-3.7-flash-high",
|
||||
candidates: [{ content: { parts: [{ text: "§54§ <tho" }] } }],
|
||||
},
|
||||
state
|
||||
@@ -444,7 +444,7 @@ test("Gemini stream: keeps textual reasoning hidden across split chunks", () =>
|
||||
const second = geminiToOpenAIResponse(
|
||||
{
|
||||
responseId: "resp-split-thought",
|
||||
modelVersion: "gemini-3.5-flash-high",
|
||||
modelVersion: "gemini-3.7-flash-high",
|
||||
candidates: [{ content: { parts: [{ text: "ught\nNeed to inspect" }] } }],
|
||||
},
|
||||
state
|
||||
@@ -459,7 +459,7 @@ test("Gemini stream: keeps textual reasoning hidden across split chunks", () =>
|
||||
const third = geminiToOpenAIResponse(
|
||||
{
|
||||
responseId: "resp-split-thought",
|
||||
modelVersion: "gemini-3.5-flash-high",
|
||||
modelVersion: "gemini-3.7-flash-high",
|
||||
candidates: [{ content: { parts: [{ text: " more</tho" }] } }],
|
||||
},
|
||||
state
|
||||
@@ -472,7 +472,7 @@ test("Gemini stream: keeps textual reasoning hidden across split chunks", () =>
|
||||
const fourth = geminiToOpenAIResponse(
|
||||
{
|
||||
responseId: "resp-split-thought",
|
||||
modelVersion: "gemini-3.5-flash-high",
|
||||
modelVersion: "gemini-3.7-flash-high",
|
||||
candidates: [{ content: { parts: [{ text: "ught>Visible answer" }] } }],
|
||||
},
|
||||
state
|
||||
@@ -498,7 +498,7 @@ test("Gemini stream: converts prefixed textual Tool call block with zero-width c
|
||||
const result = geminiToOpenAIResponse(
|
||||
{
|
||||
responseId: "resp-textual-tool-prefixed",
|
||||
modelVersion: "gemini-3.5-flash-low",
|
||||
modelVersion: "gemini-3.7-flash-low",
|
||||
candidates: [
|
||||
{
|
||||
content: {
|
||||
@@ -612,7 +612,7 @@ test("Gemini stream: unwraps native functionCall args when emitted as JSON strin
|
||||
const result = geminiToOpenAIResponse(
|
||||
{
|
||||
responseId: "resp-native-tool-json-string",
|
||||
modelVersion: "gemini-3.5-flash-low",
|
||||
modelVersion: "gemini-3.7-flash-low",
|
||||
candidates: [
|
||||
{
|
||||
content: {
|
||||
@@ -648,7 +648,7 @@ test("Gemini stream: converts JSON-string encoded textual Tool call arguments",
|
||||
const result = geminiToOpenAIResponse(
|
||||
{
|
||||
responseId: "resp-textual-tool-json-string",
|
||||
modelVersion: "gemini-3.5-flash-low",
|
||||
modelVersion: "gemini-3.7-flash-low",
|
||||
candidates: [
|
||||
{
|
||||
content: {
|
||||
@@ -685,7 +685,7 @@ test("Gemini stream: suppresses malformed textual Tool call marker", () => {
|
||||
const result = geminiToOpenAIResponse(
|
||||
{
|
||||
responseId: "resp-textual-tool-malformed",
|
||||
modelVersion: "gemini-3.5-flash-low",
|
||||
modelVersion: "gemini-3.7-flash-low",
|
||||
candidates: [
|
||||
{
|
||||
content: {
|
||||
@@ -717,7 +717,7 @@ test("Gemini stream: handles textual Tool call block split across chunks", () =>
|
||||
const state = createStreamingState();
|
||||
const chunk1 = {
|
||||
responseId: "resp-split",
|
||||
modelVersion: "gemini-3.5-flash-low",
|
||||
modelVersion: "gemini-3.7-flash-low",
|
||||
candidates: [
|
||||
{
|
||||
content: {
|
||||
@@ -732,7 +732,7 @@ test("Gemini stream: handles textual Tool call block split across chunks", () =>
|
||||
};
|
||||
const chunk2 = {
|
||||
responseId: "resp-split",
|
||||
modelVersion: "gemini-3.5-flash-low",
|
||||
modelVersion: "gemini-3.7-flash-low",
|
||||
candidates: [
|
||||
{
|
||||
content: {
|
||||
@@ -768,7 +768,7 @@ test("Gemini stream: does not swallow false positive textual tool call in backti
|
||||
const state = createStreamingState();
|
||||
const chunk1 = {
|
||||
responseId: "resp-false-positive",
|
||||
modelVersion: "gemini-3.5-flash-low",
|
||||
modelVersion: "gemini-3.7-flash-low",
|
||||
candidates: [
|
||||
{
|
||||
content: {
|
||||
@@ -783,7 +783,7 @@ test("Gemini stream: does not swallow false positive textual tool call in backti
|
||||
};
|
||||
const chunk2 = {
|
||||
responseId: "resp-false-positive",
|
||||
modelVersion: "gemini-3.5-flash-low",
|
||||
modelVersion: "gemini-3.7-flash-low",
|
||||
candidates: [
|
||||
{
|
||||
content: {
|
||||
@@ -817,7 +817,7 @@ test("Gemini stream: does not swallow terminated trailing false positive textual
|
||||
const state = createStreamingState();
|
||||
const chunk1 = {
|
||||
responseId: "resp-false-positive-terminated",
|
||||
modelVersion: "gemini-3.5-flash-low",
|
||||
modelVersion: "gemini-3.7-flash-low",
|
||||
candidates: [
|
||||
{
|
||||
content: {
|
||||
@@ -842,7 +842,7 @@ test("Gemini stream: flushes left part before textual tool call candidate and fl
|
||||
const state = createStreamingState() as any;
|
||||
const chunk1 = {
|
||||
responseId: "resp-test-flush-left",
|
||||
modelVersion: "gemini-3.5-flash-low",
|
||||
modelVersion: "gemini-3.7-flash-low",
|
||||
candidates: [
|
||||
{
|
||||
content: {
|
||||
@@ -884,7 +884,7 @@ test("Gemini stream: splits mid-stream partial candidate but preserves tool call
|
||||
const state = createStreamingState() as any;
|
||||
const chunk1 = {
|
||||
responseId: "resp-test-split-candidate",
|
||||
modelVersion: "gemini-3.5-flash-low",
|
||||
modelVersion: "gemini-3.7-flash-low",
|
||||
candidates: [
|
||||
{
|
||||
content: {
|
||||
@@ -932,7 +932,7 @@ test("Gemini stream: index mismatch regression test with zero-width characters i
|
||||
const result = geminiToOpenAIResponse(
|
||||
{
|
||||
responseId: "resp-textual-tool-index-mismatch",
|
||||
modelVersion: "gemini-3.5-flash-low",
|
||||
modelVersion: "gemini-3.7-flash-low",
|
||||
candidates: [
|
||||
{
|
||||
content: {
|
||||
@@ -964,7 +964,7 @@ test("Gemini stream: partial tool call with (empty) prefix check at chunk end do
|
||||
const state = createStreamingState();
|
||||
const chunk1 = {
|
||||
responseId: "resp-empty-leak",
|
||||
modelVersion: "gemini-3.5-flash-low",
|
||||
modelVersion: "gemini-3.7-flash-low",
|
||||
candidates: [
|
||||
{
|
||||
content: {
|
||||
@@ -1011,7 +1011,7 @@ test("Gemini stream: parses textual tool call that starts in a subsequent chunk
|
||||
const state = createStreamingState() as any;
|
||||
const chunk1 = {
|
||||
responseId: "resp-test-after-prose",
|
||||
modelVersion: "gemini-3.5-flash-low",
|
||||
modelVersion: "gemini-3.7-flash-low",
|
||||
candidates: [
|
||||
{
|
||||
content: {
|
||||
@@ -1060,7 +1060,7 @@ test("Gemini stream: checks lastParen before lastBracket when identifying partia
|
||||
// Имитируем чанк, который кончается на частичный "(empty)[Tool call:" маркер, например "(em"
|
||||
const chunk1 = {
|
||||
responseId: "resp-test-empty-partial",
|
||||
modelVersion: "gemini-3.5-flash-low",
|
||||
modelVersion: "gemini-3.7-flash-low",
|
||||
candidates: [
|
||||
{
|
||||
content: {
|
||||
@@ -1187,7 +1187,7 @@ test("Gemini stream: partial textual tool call survives a reasoning-only chunk",
|
||||
geminiToOpenAIResponse(
|
||||
{
|
||||
responseId: "resp-interleave",
|
||||
modelVersion: "gemini-3.5-flash-low",
|
||||
modelVersion: "gemini-3.7-flash-low",
|
||||
candidates: [
|
||||
{ content: { parts: [{ text: '[Tool call: terminal]\nArguments: {"command":"ls' }] } },
|
||||
],
|
||||
@@ -1200,7 +1200,7 @@ test("Gemini stream: partial textual tool call survives a reasoning-only chunk",
|
||||
geminiToOpenAIResponse(
|
||||
{
|
||||
responseId: "resp-interleave",
|
||||
modelVersion: "gemini-3.5-flash-low",
|
||||
modelVersion: "gemini-3.7-flash-low",
|
||||
candidates: [{ content: { parts: [{ text: "<thinking>pondering</thinking>" }] } }],
|
||||
},
|
||||
state
|
||||
@@ -1221,7 +1221,7 @@ test("Gemini stream: partial textual tool call survives a reasoning-only chunk",
|
||||
geminiToOpenAIResponse(
|
||||
{
|
||||
responseId: "resp-interleave",
|
||||
modelVersion: "gemini-3.5-flash-low",
|
||||
modelVersion: "gemini-3.7-flash-low",
|
||||
candidates: [{ content: { parts: [{ text: '"}' }] }, finishReason: "STOP" }],
|
||||
},
|
||||
state
|
||||
|
||||
70
tests/unit/vertex-anthropic-models.test.ts
Normal file
70
tests/unit/vertex-anthropic-models.test.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* Vertex AI Anthropic partner-model discovery (#11279).
|
||||
*
|
||||
* Covers the two pure units the PR adds (the discovery route itself is a
|
||||
* best-effort network path exercised manually per the PR's test plan):
|
||||
* - parseVertexAnthropicModels: Model Garden publisher response → discovery
|
||||
* models, handling global AND project-scoped resource names;
|
||||
* - getModelTargetFormat: a claude-* id on vertex/vertex-partner resolves to
|
||||
* the "claude" translator even when the model is NOT in the static
|
||||
* registry (the future-model heuristic).
|
||||
*/
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import { parseVertexAnthropicModels } from "../../src/lib/providerModels/vertexAnthropicModelsParser.ts";
|
||||
import { getModelTargetFormat } from "../../open-sse/config/providerModels.ts";
|
||||
|
||||
test("parseVertexAnthropicModels: global publisher resource names", () => {
|
||||
const out = parseVertexAnthropicModels({
|
||||
models: [
|
||||
{
|
||||
name: "publishers/anthropic/models/claude-sonnet-4-6",
|
||||
displayName: "Claude Sonnet 4.6",
|
||||
description: "Latest Sonnet",
|
||||
},
|
||||
{ name: "publishers/anthropic/models/claude-opus-4-6", displayName: "Claude Opus 4.6" },
|
||||
],
|
||||
});
|
||||
assert.equal(out.length, 2);
|
||||
assert.deepEqual(out[0], {
|
||||
id: "claude-sonnet-4-6",
|
||||
name: "Claude Sonnet 4.6",
|
||||
supportedEndpoints: ["chat"],
|
||||
targetFormat: "claude",
|
||||
description: "Latest Sonnet",
|
||||
owned_by: "anthropic",
|
||||
});
|
||||
// displayName fallback: missing → id; description omitted when absent
|
||||
assert.equal(out[1].name, "Claude Opus 4.6");
|
||||
assert.equal("description" in out[1], false);
|
||||
});
|
||||
|
||||
test("parseVertexAnthropicModels: project-scoped resource names strip the prefix", () => {
|
||||
const out = parseVertexAnthropicModels({
|
||||
models: [
|
||||
{
|
||||
name: "projects/my-gcp-project/locations/us-east5/publishers/anthropic/models/claude-haiku-4-5",
|
||||
},
|
||||
],
|
||||
});
|
||||
assert.equal(out.length, 1);
|
||||
assert.equal(out[0].id, "claude-haiku-4-5");
|
||||
assert.equal(out[0].name, "claude-haiku-4-5");
|
||||
});
|
||||
|
||||
test("parseVertexAnthropicModels: malformed input yields an empty list", () => {
|
||||
assert.deepEqual(parseVertexAnthropicModels(null), []);
|
||||
assert.deepEqual(parseVertexAnthropicModels({}), []);
|
||||
assert.deepEqual(parseVertexAnthropicModels({ models: "not-an-array" }), []);
|
||||
assert.deepEqual(parseVertexAnthropicModels({ models: [{ name: "" }, {}] }), []);
|
||||
});
|
||||
|
||||
test("getModelTargetFormat: claude-* on vertex resolves to the claude translator (heuristic)", () => {
|
||||
// A future Claude model with no static registry entry must still route
|
||||
// through the Anthropic Messages translator on both vertex ids.
|
||||
assert.equal(getModelTargetFormat("vertex", "claude-future-9-9"), "claude");
|
||||
assert.equal(getModelTargetFormat("vertex-partner", "claude-future-9-9"), "claude");
|
||||
// Non-Claude ids are untouched by the heuristic.
|
||||
assert.notEqual(getModelTargetFormat("vertex", "gemini-3.1-pro"), "claude");
|
||||
});
|
||||
Reference in New Issue
Block a user