merge(release): reconcile frozen remote tip 6158c9ae (#8875)

This commit is contained in:
diegosouzapw
2026-08-23 19:04:37 -03:00
77 changed files with 2162 additions and 808 deletions

View File

@@ -3,6 +3,7 @@ import { printHeading } from "../io.mjs";
import { withRuntime } from "../runtime.mjs";
import { t } from "../i18n.mjs";
import { apiFetch } from "../api.mjs";
import { mcpCallTool } from "../mcpClient.mjs";
import { emit } from "../output.mjs";
import { resolveComboModels, collectModel } from "./comboModels.mjs";
@@ -63,15 +64,7 @@ export function extendComboSuggest(combo) {
weights: opts.weights ? JSON.parse(opts.weights) : undefined,
top: opts.top,
};
const res = await apiFetch("/api/mcp/tools/call", {
method: "POST",
body: { name: "omniroute_best_combo_for_task", arguments: body },
});
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
const data = await res.json();
const data = await mcpCallTool("omniroute_best_combo_for_task", body);
const candidates = data.candidates ?? data;
const rows = (Array.isArray(candidates) ? candidates : []).map((c, i) => ({
rank: i + 1,

View File

@@ -1,5 +1,6 @@
import { readFileSync } from "node:fs";
import { apiFetch } from "../api.mjs";
import { mcpCallTool } from "../mcpClient.mjs";
import { emit } from "../output.mjs";
import { t } from "../i18n.mjs";
@@ -78,18 +79,17 @@ async function restComboStats(period) {
}
async function mcpCall(name, args, restFallback) {
const res = await apiFetch("/api/mcp/tools/call", {
method: "POST",
body: { name, arguments: args },
});
if (res.ok) return res.json();
// 404 = MCP tool surface not mounted on this build; 501 = not implemented.
// Anything else is a genuine error and we surface it.
if ((res.status === 404 || res.status === 501) && typeof restFallback === "function") {
return restFallback();
try {
return await mcpCallTool(name, args);
} catch (err) {
// Keep the REST fallback behavior for builds where the MCP surface
// is unreachable / not mounted. Anything else rethrows as an error.
const status = err?.status || err?.cause?.status;
if ((status === 404 || status === 501) && typeof restFallback === "function") {
return restFallback();
}
throw err;
}
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
async function confirm(q) {

View File

@@ -61,27 +61,12 @@ export function registerMcp(program) {
? JSON.parse(argsPositional)
: {};
if (opts.stream) {
await runMcpStream(tool, args, globalOpts);
return;
}
const exitCode = await runMcpCallCommand(tool, args, {
...opts,
stream: opts.stream,
}, globalOpts);
const extraHeaders = opts.scope?.length ? { "X-MCP-Scopes": opts.scope.join(",") } : {};
const res = await apiFetch("/api/mcp/tools/call", {
method: "POST",
body: { name: tool, arguments: args },
headers: extraHeaders,
});
if (res.status === 403) {
process.stderr.write("Scope denied\n");
process.exit(4);
}
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
const data = await res.json();
emit(data, globalOpts);
if (exitCode !== 0) process.exit(exitCode);
});
mcp
@@ -99,112 +84,132 @@ export function registerMcp(program) {
const data = await res.json();
emit(data.scopes ?? data, cmd.optsWithGlobals());
});
// 5.2 — mcp tools + mcp audit
const tools = mcp.command("tools").description(t("mcp.tools.description"));
tools
.command("list")
.description(t("mcp.tools.list.description"))
.option("--scope <s>", t("mcp.tools.list.scope"))
.action(async (opts, cmd) => {
const params = new URLSearchParams();
if (opts.scope) params.set("scope", opts.scope);
const res = await apiFetch(`/api/mcp/tools?${params}`);
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
const data = await res.json();
emit(data.tools ?? data, cmd.optsWithGlobals(), mcpToolSchema);
});
tools
.command("info <name>")
.description(t("mcp.tools.info.description"))
.action(async (name, opts, cmd) => {
const res = await apiFetch(`/api/mcp/tools?name=${encodeURIComponent(name)}`);
if (!res.ok) {
process.stderr.write(`Not found: ${name}\n`);
process.exit(1);
}
emit(await res.json(), cmd.optsWithGlobals());
});
tools
.command("schema <name>")
.description(t("mcp.tools.schema.description"))
.option("--io <kind>", t("mcp.tools.schema.io"), "input")
.action(async (name, opts, cmd) => {
const res = await apiFetch(`/api/mcp/tools?name=${encodeURIComponent(name)}&io=${opts.io}`);
if (!res.ok) {
process.stderr.write(`Not found: ${name}\n`);
process.exit(1);
}
const data = await res.json();
const globalOpts = cmd.optsWithGlobals();
if (globalOpts.output === "json") {
process.stdout.write(JSON.stringify(data.schema ?? data, null, 2) + "\n");
} else {
emit(data.schema ?? data, globalOpts);
}
});
const audit = mcp.command("audit").description(t("mcp.audit.description"));
audit
.command("tail")
.option("--follow", t("audit.tail.follow"))
.option("--limit <n>", t("audit.tail.limit"), parseInt, 100)
.action(async (opts, cmd) => {
const { runAuditTail } = await import("./audit.mjs");
await runAuditTail({ ...opts, source: "mcp" }, cmd);
});
audit
.command("stats")
.option("--period <p>", t("audit.stats.period"), "7d")
.action(async (opts, cmd) => {
const res = await apiFetch(`/api/mcp/audit/stats?period=${opts.period}`);
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
emit(await res.json(), cmd.optsWithGlobals());
});
}
async function runMcpStream(tool, args, globalOpts) {
/**
* Shared JSON-RPC 2.0 MCP client used by both stream and non-stream `mcp call`.
*
* Protocol:
* 1. POST /api/mcp/stream with initialize → get Mcp-Session-Id header
* 2. POST /api/mcp/stream with tools/call + Mcp-Session-Id header
*
* When `stream` is true, writes SSE data chunks to stdout as they arrive.
* When `stream` is false, returns the parsed JSON-RPC result.
*
* Returns the exit code (0 = success, non-zero = failure).
*/
async function mcpJsonRpcCall(tool, args, { stream = false, globalOpts = {} } = {}) {
const baseUrl = globalOpts.baseUrl ?? "http://localhost:20128";
const apiKey = globalOpts.apiKey ?? "";
const res = await fetch(`${baseUrl}/api/mcp/stream`, {
const streamUrl = `${baseUrl}/api/mcp/stream`;
const hdrs = {
"Content-Type": "application/json",
Accept: stream ? "text/event-stream" : "application/json",
...(apiKey ? { Authorization: `Bearer ${apiKey}` } : {}),
};
// Step 1 — initialize
const initRes = await fetch(streamUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
...(apiKey ? { Authorization: `Bearer ${apiKey}` } : {}),
},
body: JSON.stringify({ name: tool, arguments: args }),
headers: hdrs,
body: JSON.stringify({
jsonrpc: "2.0",
id: 1,
method: "initialize",
params: {
protocolVersion: "2024-11-05",
capabilities: {},
clientInfo: { name: "omniroute-cli", version: "1.0" },
},
}),
});
if (!res.ok) {
process.stderr.write(`HTTP ${res.status}\n`);
process.exit(1);
if (!initRes.ok) {
const text = await initRes.text().catch(() => "");
process.stderr.write(`MCP initialize failed: HTTP ${initRes.status}${text ? `${text}` : ""}\n`);
return 1;
}
const reader = res.body.getReader();
const sessionId = initRes.headers.get("mcp-session-id");
if (!sessionId) {
process.stderr.write("MCP initialize failed: no Mcp-Session-Id in response\n");
return 1;
}
// Step 2 — tools/call
const callHeaders = {
...hdrs,
"mcp-session-id": sessionId,
};
const callRes = await fetch(streamUrl, {
method: "POST",
headers: callHeaders,
body: JSON.stringify({
jsonrpc: "2.0",
id: 2,
method: "tools/call",
params: { name: tool, arguments: args },
}),
});
if (!callRes.ok) {
const text = await callRes.text().catch(() => "");
process.stderr.write(`MCP call failed: HTTP ${callRes.status}${text ? `${text}` : ""}\n`);
return 1;
}
if (stream) {
return readMcpSseStream(callRes.body);
}
// Non-stream: parse JSON-RPC response
const data = await callRes.json();
if (data.error) {
process.stderr.write(`MCP error: ${data.error.message || JSON.stringify(data.error)}\n`);
return 1;
}
// Print the result content
const content = data.result?.content;
if (content) {
for (const item of content) {
if (item.type === "text") {
process.stdout.write(item.text + "\n");
} else if (item.type === "resource") {
process.stdout.write(JSON.stringify(item.resource) + "\n");
} else {
process.stdout.write(JSON.stringify(item) + "\n");
}
}
} else {
process.stdout.write(JSON.stringify(data.result, null, 2) + "\n");
}
return 0;
}
async function readMcpSseStream(body) {
if (!body) return 1;
const reader = body.getReader();
const dec = new TextDecoder();
let buf = "";
while (true) {
const { done, value } = await reader.read();
if (done) break;
buf += dec.decode(value, { stream: true });
const lines = buf.split("\n");
buf = lines.pop() ?? "";
for (const line of lines) {
if (line.startsWith("data: ")) {
const raw = line.slice(6).trim();
if (raw && raw !== "[DONE]") process.stdout.write(raw + "\n");
}
}
const lines = buf.split("\n");
for (const line of lines) {
if (line.startsWith("data: ")) {
const raw = line.slice(6).trim();
if (raw && raw !== "[DONE]") process.stdout.write(raw + "\n");
}
}
return 0;
}
export async function runMcpCallCommand(tool, args, opts = {}, globalOpts = {}) {
return mcpJsonRpcCall(tool, args, { stream: opts.stream, globalOpts });
}
export async function runMcpStatusCommand(opts = {}) {
@@ -233,7 +238,8 @@ export async function runMcpStatusCommand(opts = {}) {
}
const transport = status.transport || "stdio";
console.log(status.running ? t("mcp.running", { transport }) : t("mcp.stopped"));
const online = status.online ?? status.running;
console.log(online ? t("mcp.running", { transport }) : t("mcp.stopped"));
if (status.toolsCount !== undefined) console.log(` Tools: ${status.toolsCount}`);
if (status.scopes?.length) {
console.log(" Scopes:");

View File

@@ -1,4 +1,5 @@
import { apiFetch } from "../api.mjs";
import { mcpCallTool } from "../mcpClient.mjs";
import { emit } from "../output.mjs";
import { t } from "../i18n.mjs";
@@ -8,15 +9,7 @@ function fmtTs(v) {
}
async function mcpCall(name, args) {
const res = await apiFetch("/api/mcp/tools/call", {
method: "POST",
body: { name, arguments: args },
});
if (!res.ok) {
process.stderr.write(`MCP error: ${res.status}\n`);
process.exit(1);
}
return res.json();
return mcpCallTool(name, args);
}
const proxySchema = [

View File

@@ -1,6 +1,7 @@
import { createInterface } from "node:readline";
import { Argument } from "commander";
import { apiFetch } from "../api.mjs";
import { mcpCallTool } from "../mcpClient.mjs";
import { emit } from "../output.mjs";
import { t } from "../i18n.mjs";
@@ -166,14 +167,7 @@ export function registerResilience(program) {
])
)
.action(async (name, opts, cmd) => {
const res = await apiFetch("/api/mcp/tools/call", {
method: "POST",
body: { name: "omniroute_set_resilience_profile", arguments: { profile: name } },
});
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
await mcpCallTool("omniroute_set_resilience_profile", { profile: name });
process.stdout.write(`Profile: ${name}\n`);
});

View File

@@ -1,5 +1,6 @@
import { readFileSync } from "node:fs";
import { apiFetch } from "../api.mjs";
import { mcpCallTool } from "../mcpClient.mjs";
import { emit } from "../output.mjs";
import { t } from "../i18n.mjs";
@@ -106,14 +107,7 @@ export async function runSkillsInstall(opts, cmd) {
}
export async function runSkillsEnable(id, opts, cmd) {
const res = await apiFetch("/api/mcp/tools/call", {
method: "POST",
body: { name: "omniroute_skills_enable", arguments: { skillId: id, enabled: true } },
});
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
await mcpCallTool("omniroute_skills_enable", { skillId: id, enabled: true });
process.stdout.write(`Enabled: ${id}\n`);
}
@@ -122,14 +116,7 @@ export async function runSkillsDisable(id, opts, cmd) {
const ok = await confirm(`Disable ${id}?`);
if (!ok) return;
}
const res = await apiFetch("/api/mcp/tools/call", {
method: "POST",
body: { name: "omniroute_skills_enable", arguments: { skillId: id, enabled: false } },
});
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
await mcpCallTool("omniroute_skills_enable", { skillId: id, enabled: false });
process.stdout.write(`Disabled: ${id}\n`);
}
@@ -153,16 +140,11 @@ export async function runSkillsExecute(id, opts, cmd) {
: opts.inputFile
? JSON.parse(readFileSync(opts.inputFile, "utf8"))
: {};
const res = await apiFetch("/api/mcp/tools/call", {
method: "POST",
body: { name: "omniroute_skills_execute", arguments: { skillId: id, input } },
timeout: opts.timeout ?? 30000,
});
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
const data = await res.json();
const data = await mcpCallTool(
"omniroute_skills_execute",
{ skillId: id, input },
{ timeout: opts.timeout ?? 30000 },
);
emit(data, globalOpts);
}

127
bin/cli/mcpClient.mjs Normal file
View File

@@ -0,0 +1,127 @@
/**
* Shared MCP JSON-RPC client for CLI commands.
*
* The server exposes MCP through /api/mcp/stream (Streamable HTTP transport).
* Calling a tool requires:
* 1. POST initialize → get Mcp-Session-Id response header
* 2. POST tools/call with that session header
*
* Older CLI paths POSTed { name, arguments } to /api/mcp/tools/call, which is
* not a registered route, so every MCP-backed command was broken.
*
* These functions route through apiFetch so CLI auth, remote contexts and
* timeouts are handled the same way as every other management API call.
*/
import { apiFetch } from "./api.mjs";
function mcpError(message, status) {
const err = new Error(message);
if (status) err.status = status;
return err;
}
async function callMcpEndpoint(payload, { timeout, stream }) {
const res = await apiFetch("/api/mcp/stream", {
method: "POST",
body: payload,
timeout,
acceptNotOk: true,
headers: stream ? { Accept: "text/event-stream" } : {},
});
if (!res.ok) {
const text = await res.text().catch(() => "");
throw mcpError(
`${payload.method} ${payload.id}: HTTP ${res.status}${text ? `${text}` : ""}`,
res.status,
);
}
return res;
}
/**
* Call an MCP tool over /api/mcp/stream.
*
* Non-stream: returns the JSON-RPC result payload.
* Stream: writes SSE `data:` chunks to stdout and returns null on success.
*/
export async function mcpCallTool(name, args = {}, options = {}) {
const { timeout, scope } = options;
const scopeHeader = scope?.length ? { "X-MCP-Scopes": scope.join(",") } : {};
const initRes = await callMcpEndpoint(
{
jsonrpc: "2.0",
id: 1,
method: "initialize",
params: {
protocolVersion: "2024-11-05",
capabilities: {},
clientInfo: { name: "omniroute-cli", version: "1.0" },
},
},
{ timeout, stream: options.stream },
);
const sessionId = initRes.headers.get("mcp-session-id");
if (!sessionId) {
throw mcpError("MCP initialize failed: no Mcp-Session-Id in response", 500);
}
const callRes = await callMcpEndpoint(
{
jsonrpc: "2.0",
id: 2,
method: "tools/call",
params: { name, arguments: args },
},
{ timeout, stream: options.stream },
);
if (options.stream) {
return consumeSse(callRes.body, options.onChunk);
}
const data = await callRes.json();
if (data.error) {
const err = mcpError(`MCP error: ${data.error.message || JSON.stringify(data.error)}`);
err.code = data.error.code;
throw err;
}
if (data.result?.isError) {
const msg = data.result?.content?.[0]?.text || "unknown tool error";
throw mcpError(`MCP error: ${msg}`, 500);
}
return data.result;
}
async function consumeSse(body, onChunk) {
if (!body) throw mcpError("MCP stream returned no body", 500);
const reader = body.getReader();
const decoder = new TextDecoder();
let buf = "";
const flushLines = () => {
let idx;
while ((idx = buf.indexOf("\n")) >= 0) {
const line = buf.slice(0, idx);
buf = buf.slice(idx + 1);
if (line.startsWith("data: ")) {
const raw = line.slice(6).trim();
if (raw && raw !== "[DONE]") (onChunk ?? writeStdout)(raw);
}
}
};
while (true) {
const { done, value } = await reader.read();
if (done) break;
buf += decoder.decode(value, { stream: true });
flushLines();
}
buf += decoder.decode();
flushLines();
return null;
}
function writeStdout(raw) {
process.stdout.write(raw + "\n");
}

View File

@@ -0,0 +1 @@
- **feat(dashboard):** replace the hard Home → onboarding redirect with a dismissable first-run readiness card so returning users can stay on Home while new users still get a clear 4-step path ([#11282](https://github.com/diegosouzapw/OmniRoute/pull/11282))

View File

@@ -0,0 +1 @@
- **fix(ollama):** Ollama Local models are no longer flattened to `chat` at sync time — the synced store persists every advertised capability and chat filtering moves to read time, so `/v1/embeddings` and `/v1/images/generations` stop rejecting models the daemon reports as capable ([#11271](https://github.com/diegosouzapw/OmniRoute/pull/11271)) — thanks @yourspraveen

View File

@@ -6,7 +6,7 @@ lastUpdated: 2026-07-31
# OmniRoute Antigravity (Google One AI) Onboarding Guide
> **What you get**: Access to Gemini 3.1 Pro, Gemini 3.5 Flash, Claude Sonnet 4.6, and other models through your Google One AI Pro subscription — routed through OmniRoute as a unified gateway.
> **What you get**: Access to Gemini 3.1 Pro, Gemini 3.7 Flash, Claude Sonnet 4.6, and other models through your Google One AI Pro subscription — routed through OmniRoute as a unified gateway.
**Official references**:
@@ -45,7 +45,7 @@ Both providers share the **same Google backend** — identical OAuth client, tok
**Why the model catalog differs**: Google's CLI is "optimized for speed and low overhead" and "co-optimized with Gemini models" (per Google's official blog). The Web/IDE product is "optimized for comprehensiveness." The CLI uses `:fetchAvailableModels` to dynamically discover models, while the IDE uses a static curated list.
**In practice**: Use `agy/` prefix for Gemini models (e.g. `agy/gemini-3.5-flash-high`). Use `antigravity/` for the static curated list. Both hit the same Google backend, but expose different model naming. The quota is shared — using either provider counts against the same Google account's limits.
**In practice**: Use `agy/` prefix for Gemini models (e.g. `agy/gemini-3.7-flash-high`). Use `antigravity/` for the static curated list. Both hit the same Google backend, but expose different model naming. The quota is shared — using either provider counts against the same Google account's limits.
---

View File

@@ -113,6 +113,7 @@ const AGY_RETIRED_MODEL_IDS = new Set([
"gemini-3.6-flash-medium",
"gemini-3.6-flash-low",
"gemini-3-flash-agent",
"gemini-3.5-flash",
"gemini-3.5-flash-extra-low",
"gemini-3.5-flash-low",
"gemini-3.5-flash-high",

View File

@@ -179,6 +179,7 @@ const ANTIGRAVITY_RETIRED_MODEL_IDS = new Set([
"gemini-3.6-flash-medium",
"gemini-3.6-flash-low",
"gemini-3-flash-agent",
"gemini-3.5-flash",
"gemini-3.5-flash-extra-low",
"gemini-3.5-flash-low",
"gemini-3.5-flash-high",

View File

@@ -8,7 +8,6 @@
"gemma-4-26b-it": { "rpm": 16000, "rpd": 14400, "tpm": 16000 },
"gemma-4-31b-it": { "rpm": 16000, "rpd": 14400, "tpm": 16000 },
"gemini-embedding-exp-03-07": { "rpm": 100, "rpd": 1000, "tpm": 30000 },
"gemini-3.5-flash": { "rpm": 5, "rpd": 20, "tpm": 250000 },
"gemini-3.1-flash-lite": { "rpm": 15, "rpd": 500, "tpm": 250000 },
"gemini-3.1-pro": { "rpm": 0, "rpd": 0, "tpm": 0 },
"gemini-2.5-flash-lite": { "rpm": 10, "rpd": 20, "tpm": 250000 },

View File

@@ -186,6 +186,9 @@ export function getModelTargetFormat(aliasOrId: string, modelId: string): string
// executor's /codex/i routing, 9router#102). Scoped to the openai alias so other
// providers shipping *-pro ids keep their own endpoint semantics.
if (alias === "openai" && /-pro$/i.test(bareModelId)) return "openai-responses";
// ponytail: Claude models on Vertex use rawPredict with Anthropic Messages format,
// not the Gemini generateContent format. Mirrors executor isClaudeModel() check.
if ((alias === "vertex" || alias === "vp") && /^claude-/i.test(bareModelId)) return "claude";
// Model-level targetFormat is provider-scoped: a catalog entry declares how THIS
// provider's endpoint serves the model — do NOT import another provider's tag.
// #9994 scoped this for providers WITH a catalog; #10072 extends it to catalogless

View File

@@ -228,14 +228,14 @@ export const cursorProvider: RegistryEntry = {
{ id: "gpt-5.1-low", name: "GPT-5.1 Low" },
{ id: "gpt-5.1", name: "GPT-5.1" },
{ id: "gpt-5.1-high", name: "GPT-5.1 High" },
{ id: "gemini-3.5-flash", name: "Gemini 3.5 Flash" },
{ id: "claude-4-sonnet", name: "Sonnet 4" },
{ id: "claude-4-sonnet-thinking", name: "Sonnet 4 Thinking" },
{ id: "gpt-5-mini", name: "GPT-5 Mini" },
{ id: "kimi-k3-low", name: "Kimi K3 Low" },
{ id: "kimi-k3-max", name: "Kimi K3" },
{ id: "glm-5.2-high", name: "GLM 5.2" },
{ id: "glm-5.2-max", name: "GLM 5.2 Max" }, ],
{ id: "glm-5.2-max", name: "GLM 5.2 Max" },
],
};
/**

View File

@@ -219,5 +219,18 @@ export const opencode_goProvider: RegistryEntry = {
supportedThinkingEfforts: ["none", "low", "high", "max"],
targetFormat: "openai-responses",
},
// Console Go free GLM-tier model (live-verified 2026-08-23): the upstream
// rejects every reasoning_effort outside {low, high, max} whenever tools
// are present — "[1210] This model always engages in thinking and cannot
// be disabled; please use low, high, or max" — which broke clients that
// default to reasoning_effort:"medium" (Hermes). Declaring the exact
// vocabulary lets sanitizeReasoningEffortForProvider clamp off-vocabulary
// requests to the nearest accepted tier instead of burning a 400.
{
id: "ox-alpha-free",
name: "ox-alpha (free)",
supportsReasoning: true,
supportedThinkingEfforts: ["low", "high", "max"],
},
],
};

View File

@@ -27,8 +27,17 @@ export const vertexProvider: RegistryEntry = {
{ id: "DeepSeek-V4-Pro", name: "DeepSeek V4 Pro (Vertex Partner)" },
{ id: "Qwen3.6-35B-A3B", name: "Qwen3.6 35B A3B (Vertex Partner)" },
{ id: "GLM-5.1-FP8", name: "GLM-5.1 (Vertex Partner)" },
{ id: "claude-opus-4-7", name: "Claude Opus 4.7 (Vertex)" },
{ id: "claude-sonnet-4-6", name: "Claude Sonnet 4.6 (Vertex)" },
{ id: "claude-fable-5", name: "Claude Fable 5 (Vertex)", targetFormat: "claude" },
{ id: "claude-opus-5", name: "Claude Opus 5 (Vertex)", targetFormat: "claude" },
{ id: "claude-sonnet-5", name: "Claude Sonnet 5 (Vertex)", targetFormat: "claude" },
{ id: "claude-opus-4-8", name: "Claude Opus 4.8 (Vertex)", targetFormat: "claude" },
{ id: "claude-opus-4-7", name: "Claude Opus 4.7 (Vertex)", targetFormat: "claude" },
{ id: "claude-opus-4-6", name: "Claude Opus 4.6 (Vertex)", targetFormat: "claude" },
{ id: "claude-sonnet-4-6", name: "Claude Sonnet 4.6 (Vertex)", targetFormat: "claude" },
{ id: "claude-sonnet-4-5-v2", name: "Claude Sonnet 4.5 v2 (Vertex)", targetFormat: "claude" },
{ id: "claude-sonnet-4-5", name: "Claude Sonnet 4.5 (Vertex)", targetFormat: "claude" },
{ id: "claude-opus-4-5", name: "Claude Opus 4.5 (Vertex)", targetFormat: "claude" },
{ id: "claude-haiku-4-5", name: "Claude Haiku 4.5 (Vertex)", targetFormat: "claude" },
],
passthroughModels: true,
};

View File

@@ -13,10 +13,17 @@ export const vertex_partnerProvider: RegistryEntry = {
{ id: "DeepSeek-V4-Pro", name: "DeepSeek V4 Pro" },
{ id: "Qwen3.6-35B-A3B", name: "Qwen 3.6 35B A3B" },
{ id: "GLM-5.1-FP8", name: "GLM 5.1" },
// Sweep 2026-06-19: + Claude Opus on Vertex (Anthropic partner models).
{ id: "claude-opus-4-8", name: "Claude Opus 4.8" },
{ id: "claude-opus-4-7", name: "Claude Opus 4.7" },
{ id: "claude-opus-4-6", name: "Claude Opus 4.6" },
{ id: "claude-sonnet-4-6", name: "Claude Sonnet 4.6" },
{ id: "claude-fable-5", name: "Claude Fable 5", targetFormat: "claude" },
{ id: "claude-opus-5", name: "Claude Opus 5", targetFormat: "claude" },
{ id: "claude-sonnet-5", name: "Claude Sonnet 5", targetFormat: "claude" },
{ id: "claude-opus-4-8", name: "Claude Opus 4.8", targetFormat: "claude" },
{ id: "claude-opus-4-7", name: "Claude Opus 4.7", targetFormat: "claude" },
{ id: "claude-opus-4-6", name: "Claude Opus 4.6", targetFormat: "claude" },
{ id: "claude-sonnet-4-6", name: "Claude Sonnet 4.6", targetFormat: "claude" },
{ id: "claude-sonnet-4-5-v2", name: "Claude Sonnet 4.5 v2", targetFormat: "claude" },
{ id: "claude-sonnet-4-5", name: "Claude Sonnet 4.5", targetFormat: "claude" },
{ id: "claude-sonnet-4", name: "Claude Sonnet 4", targetFormat: "claude" },
{ id: "claude-opus-4-5", name: "Claude Opus 4.5", targetFormat: "claude" },
{ id: "claude-haiku-4-5", name: "Claude Haiku 4.5", targetFormat: "claude" },
],
};

View File

@@ -11,6 +11,7 @@ import {
import {
getLearnedReasoningEffort,
clampToLearned,
REASONING_EFFORT_ORDER,
} from "../../services/learnedReasoningEffortCaps.ts";
/**
@@ -357,6 +358,43 @@ export function sanitizeReasoningEffortForProvider(
}
}
// ── explicit per-model capability clamp ──────────────────────────────────
// When the registry declares supportedThinkingEfforts for this exact model
// and the requested effort falls outside that vocabulary, remap to the
// nearest declared tier: the smallest ranked value ≥ the request, else the
// highest declared (a request above the ceiling lands on the ceiling).
// Live case: opencode-go/ox-alpha-free (Console Go) only accepts
// {low, high, max} — a client's reasoning_effort:"medium" reached the
// upstream verbatim and 400'd every turn ("[1210] This model always engages
// in thinking and cannot be disabled; please use low, high, or max"). The
// learned-caps path can't help here (it only clamps down from xhigh/max,
// and this error text isn't a parseable enum), so the declaration is the
// only source of truth. Models without an explicit declaration keep
// #8057's trust-the-upstream pass-through.
const providerModelIdForClamp = modelStr.startsWith(`${provider}/`)
? modelStr.slice(provider.length + 1)
: modelStr;
const declaredEfforts = getProviderModels(provider).find(
(entry) => entry.id === providerModelIdForClamp || entry.aliases?.includes(providerModelIdForClamp)
)?.supportedThinkingEfforts;
const declaredRanked = (
Array.isArray(declaredEfforts) ? declaredEfforts : []
)
.map((tier) => ({ tier, rank: REASONING_EFFORT_ORDER.indexOf(tier) }))
.filter((x) => x.rank >= 0)
.sort((a, b) => a.rank - b.rank);
if (declaredRanked.length > 0 && !declaredEfforts!.includes(effortStr)) {
const requestedRank = REASONING_EFFORT_ORDER.indexOf(effortStr);
const nearest =
declaredRanked.find((x) => x.rank >= requestedRank) ??
declaredRanked[declaredRanked.length - 1];
log?.info?.(
"REASONING_SANITIZE",
`${provider}/${modelStr}: mapped reasoning_effort ${effortStr}${nearest.tier} (model accepts ${declaredEfforts!.join("/")})`
);
return writeEffortValue(b, nearest.tier, c);
}
const supportsXHigh = supportsXHighEffort(provider, modelStr);
const supportsMax = supportsMaxEffortForProvider(provider, modelStr);

View File

@@ -28,6 +28,7 @@ import type {
ResolvedComboTarget,
} from "./types.ts";
import { extractSessionAffinityKey } from "@/sse/services/auth";
import { filterChatSelectableModels } from "../modelEndpointPolicy.ts";
import { DEFAULT_INTENT_CONFIG, type IntentClassifierConfig } from "../intentClassifier.ts";
import { getTaskFitness } from "../autoCombo/taskFitness.ts";
import {
@@ -470,10 +471,13 @@ export async function expandAutoComboCandidatePool(
// catalog only when the user has none. This keeps catalog-only models
// (e.g. openrouter/auto) out of pure-auto pools when the operator only
// synced a subset (e.g. OpenRouter with importFreeModelsOnly).
const [syncedModels, customModels] = await Promise.all([
// #11088 (option 1): the synced store now persists non-chat models too —
// chat combo pools must keep filtering them out at read time.
const [syncedModelsRaw, customModels] = await Promise.all([
getSyncedAvailableModels(providerId),
getCustomModels(providerId),
]);
const syncedModels = filterChatSelectableModels(providerId, syncedModelsRaw);
const hiddenModels = hiddenModelsMap.get(providerId);
const userVisibleIds = new Set<string>();
for (const m of syncedModels) if (m.id && !hiddenModels?.has(m.id)) userVisibleIds.add(m.id);

View File

@@ -78,7 +78,7 @@ const FALLBACK_MODEL_SEEDS: FallbackModelSeed[] = [
/** Presets exposed by the web client's model picker (id → text/multimodal model). */
export const CONOL_FALLBACK_MODEL_PRESETS: ConolModelPreset[] = [
{ id: "flash", text: "deepseek/deepseek-v4-flash", multimodal: "google/gemini-3.5-flash" },
{ id: "flash", text: "deepseek/deepseek-v4-flash", multimodal: "google/gemini-3.7-flash" },
{ id: "moderate", text: "deepseek/deepseek-v4-pro", multimodal: "claude-sonnet-5" },
{ id: "pro", text: "z-ai/glm-5.2", multimodal: "moonshotai/kimi-k3" },
{ id: "ultra", text: "claude-fable-5", multimodal: "claude-fable-5" },

View File

@@ -6,7 +6,7 @@
*/
export interface PromptQlModel {
/** Client-facing id (model_reference slug, e.g. gemini-3.5-flash). */
/** Client-facing id (model_reference slug, e.g. gemini-3.7-flash). */
id: string;
/** Friendly picker label. */
name: string;

View File

@@ -0,0 +1,97 @@
"use client";
import { useEffect, useState } from "react";
import Link from "next/link";
import { useTranslations } from "next-intl";
const DISMISS_STORAGE_KEY = "omniroute-first-run-readiness-dismissed";
type FirstRunReadinessCardProps = {
setupComplete: boolean;
};
/**
* Soft entry path for first-run users. Replaces the hard redirect to
* /dashboard/onboarding so returning users can dismiss and stay on Home.
*/
export default function FirstRunReadinessCard({ setupComplete }: FirstRunReadinessCardProps) {
const t = useTranslations("home");
const [visible, setVisible] = useState(false);
useEffect(() => {
if (setupComplete) {
setVisible(false);
return;
}
try {
setVisible(!localStorage.getItem(DISMISS_STORAGE_KEY));
} catch {
setVisible(true);
}
}, [setupComplete]);
if (!visible || setupComplete) return null;
const dismiss = () => {
try {
localStorage.setItem(DISMISS_STORAGE_KEY, "true");
} catch {
// ignore storage failures; still hide for this session
}
setVisible(false);
};
const steps = [
t("readinessStep1"),
t("readinessStep2"),
t("readinessStep3"),
t("readinessStep4"),
];
return (
<div
role="region"
aria-label={t("readinessTitle")}
className="mb-4 rounded-xl border border-blue-200 dark:border-blue-500/30 bg-blue-50 dark:bg-blue-500/10 px-5 py-4"
>
<div className="flex items-start justify-between gap-4">
<div className="min-w-0 flex-1">
<p className="text-xs font-medium uppercase tracking-wide text-blue-700/80 dark:text-blue-300/80">
{t("readinessEyebrow")}
</p>
<h2 className="mt-1 text-lg font-semibold text-blue-950 dark:text-blue-100">
{t("readinessTitle")}
</h2>
<p className="mt-1 text-sm text-blue-900/80 dark:text-blue-200/80">
{t("readinessSubtitle")}
</p>
<ol className="mt-3 space-y-1.5 text-sm text-blue-900 dark:text-blue-100">
{steps.map((label, index) => (
<li key={label} className="flex items-center gap-2">
<span className="inline-flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-blue-200/80 dark:bg-blue-400/20 text-xs font-semibold text-blue-800 dark:text-blue-200">
{index + 1}
</span>
<span>{label}</span>
</li>
))}
</ol>
<div className="mt-4 flex flex-wrap items-center gap-3">
<Link
href="/dashboard/onboarding"
className="inline-flex items-center rounded-lg bg-blue-600 px-3.5 py-2 text-sm font-medium text-white hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-400"
>
{t("readinessContinue")}
</Link>
<button
type="button"
onClick={dismiss}
className="text-sm font-medium text-blue-800/80 hover:text-blue-950 dark:text-blue-200/80 dark:hover:text-blue-100"
>
{t("readinessDismiss")}
</button>
</div>
</div>
</div>
</div>
);
}

View File

@@ -2,7 +2,6 @@
import { useEffect, useRef, useState, useCallback } from "react";
import { Card, Button, ModelSelectModal } from "@/shared/components";
import Image from "next/image";
import { useTranslations } from "next-intl";
import { copyToClipboard } from "@/shared/utils/clipboard";
import { buildOpenCodeConfigDocument } from "@/shared/services/opencodeConfig";
@@ -643,38 +642,32 @@ export default function DefaultToolCard({
};
const renderIcon = () => {
// Tool SVGs are non-square (e.g. opencode is 234×42, cursor is 467×532).
// next/image's dev check warns whenever the rendered aspect-ratio size
// differs from the square width/height attributes, so these render as a
// plain <img> capped at 32px on both axes — true ratio, no dev noise.
const renderImg = (src: string) => (
// eslint-disable-next-line @next/next/no-img-element -- local static SVG asset
<img
src={src}
alt={tool.name}
width={32}
height={32}
className="size-8 object-contain rounded-lg"
style={{ width: "auto", height: "auto", maxWidth: 32, maxHeight: 32 }}
onError={(e) => {
(e.currentTarget as HTMLElement).style.display = "none";
}}
/>
);
if (tool.image) {
return (
<Image
src={tool.image}
alt={tool.name}
width={32}
height={32}
className="size-8 object-contain rounded-lg"
sizes="32px"
onError={(e) => {
(e.currentTarget as HTMLElement).style.display = "none";
}}
/>
);
return renderImg(tool.image);
}
if (tool.imageLight || tool.imageDark) {
const themedSrc = isDark
? tool.imageDark || tool.imageLight
: tool.imageLight || tool.imageDark;
return (
<Image
src={themedSrc}
alt={tool.name}
width={32}
height={32}
className="size-8 object-contain rounded-lg"
sizes="32px"
onError={(e) => {
(e.currentTarget as HTMLElement).style.display = "none";
}}
/>
);
return renderImg(themedSrc);
}
if (tool.icon) {
return (

View File

@@ -1,4 +1,3 @@
import { redirect } from "next/navigation";
import { getMachineId } from "@/shared/utils/machine";
import { getSettings } from "@/lib/localDb";
import HomePageClient from "../dashboard/HomePageClient";
@@ -7,19 +6,18 @@ import KimiSponsorBanner from "../dashboard/KimiSponsorBanner";
import CheaperInferenceSponsorBanner from "../dashboard/CheaperInferenceSponsorBanner";
import VscodeCopilotBanner from "../dashboard/VscodeCopilotBanner";
import NewsBanner from "../dashboard/NewsBanner";
import FirstRunReadinessCard from "../dashboard/FirstRunReadinessCard";
export const dynamic = "force-dynamic";
export default async function HomePage() {
const settings = await getSettings();
if (!settings.setupComplete) {
redirect("/dashboard/onboarding");
}
const machineId = await getMachineId();
const isBootstrapped = process.env.OMNIROUTE_BOOTSTRAPPED === "true";
return (
<>
{isBootstrapped && <BootstrapBanner />}
<FirstRunReadinessCard setupComplete={Boolean(settings.setupComplete)} />
<KimiSponsorBanner />
<CheaperInferenceSponsorBanner />
<VscodeCopilotBanner />

View File

@@ -1,5 +1,11 @@
import { isSelfHostedChatProvider } from "@/shared/constants/providers";
import { getStaticModelsForProvider, type LocalCatalogModel } from "@/lib/providers/staticModels";
import { SAFE_OUTBOUND_FETCH_PRESETS, safeOutboundFetch } from "@/shared/network/safeOutboundFetch";
import { getProviderValidationGuard } from "@/shared/network/outboundUrlGuardPolicy";
import {
buildOllamaShowUrl,
enrichOllamaModelsWithCapabilities,
} from "@/lib/providerModels/ollamaCapabilities";
export type JsonRecord = Record<string, unknown>;
@@ -102,3 +108,35 @@ export function buildNamedOpenAiStyleHeaders(
return headers;
}
// #11087 — Ollama's OpenAI-compatible /v1/models response carries no capability
// data, so every local model looked like a chat model and image/embedding
// requests were routed to text-only models. Probe /api/show per model (bounded
// concurrency, failures degrade to the unenriched entry) to recover the
// advertised capabilities. Lives here rather than inline in route.ts to keep the
// route file under its frozen file-size cap.
export async function enrichOllamaLocalModels(
models: unknown[],
baseUrl: string,
proxy: unknown,
token: string | null | undefined
): Promise<JsonRecord[]> {
const showUrl = buildOllamaShowUrl(baseUrl);
return enrichOllamaModelsWithCapabilities(models, async (modelId) => {
try {
const showResponse = await safeOutboundFetch(showUrl, {
...SAFE_OUTBOUND_FETCH_PRESETS.modelsProbe,
// Same guard tier as the discovery probe above: local-first, so LAN
// Ollama hosts are reachable while the outbound guard stays enforced.
guard: getProviderValidationGuard(),
proxyConfig: proxy,
method: "POST",
headers: buildOptionalBearerHeaders(token),
body: JSON.stringify({ model: modelId, verbose: false }),
});
return showResponse.ok ? await showResponse.json() : null;
} catch {
return null;
}
});
}

View File

@@ -85,10 +85,7 @@ import {
} from "@/lib/providerModels/modelDiscovery";
import { buildProviderModelsUrl, getDiscoveryClientVersionOptions } from "./discoveryClientVersion";
import { getAdobeModels } from "./adobeFireflyDiscovery";
import {
parseGeminiModelsList,
type GeminiDiscoveryModel,
} from "@/lib/providerModels/geminiModelsParser";
import { parseGeminiModelsList } from "@/lib/providerModels/geminiModelsParser";
import { getSyncedAvailableModels, getCustomModels } from "@/lib/db/models";
import { isConnectionUnavailableToAuxiliaryActivity } from "@/lib/exclusiveLeaseIsolation";
import { fetchCursorAgentModels } from "@/lib/providerModels/cursorAgent";
@@ -106,6 +103,7 @@ import {
mergeSpecialtyCatalogIntoLiveModels,
buildOptionalBearerHeaders,
buildNamedOpenAiStyleHeaders,
enrichOllamaLocalModels,
} from "./discovery/helpers";
import {
fetchAntigravityDiscoveryModelsCached,
@@ -792,6 +790,8 @@ export async function GET(
models = isNamedOpenAIStyleProvider(provider)
? normalizeOpenAiLikeModelsResponse(data, provider)
: data.data || data.models || [];
if (provider === "ollama-local")
models = await enrichOllamaLocalModels(models, baseUrl, proxy, token);
break; // Success!
}
@@ -1803,7 +1803,7 @@ export async function GET(
const headers: Record<string, string> = { "Content-Type": "application/json" };
if (bearerToken) headers["Authorization"] = `Bearer ${bearerToken}`;
const allModels: GeminiDiscoveryModel[] = [];
const allModels: any[] = [];
let pageUrl = queryKey ? `${baseUrl}&key=${encodeURIComponent(queryKey)}` : baseUrl;
let pageCount = 0;
const MAX_PAGES = 20;
@@ -1849,6 +1849,60 @@ export async function GET(
throw error;
}
// ponytail: Anthropic partner models via Model Garden publisher endpoint (Bearer only)
if (bearerToken) {
const psd = asRecord(connection.providerSpecificData);
const region =
(typeof psd.region === "string" && psd.region.trim()) || "us-central1";
// Extract project_id from SA JSON for project-scoped listing (mirrors executor URL pattern).
// Falls back to global publisher endpoint if no project available.
let anthropicModelsUrl: string;
let projectId: string | null = null;
if (credential) {
try {
const sa = JSON.parse(credential);
if (sa?.project_id) projectId = sa.project_id;
} catch { /* not SA JSON, skip */ }
}
if (projectId) {
anthropicModelsUrl = `https://aiplatform.googleapis.com/v1/projects/${projectId}/locations/${region}/publishers/anthropic/models`;
} else {
anthropicModelsUrl = `https://aiplatform.googleapis.com/v1/publishers/anthropic/models`;
}
try {
const anthropicResponse = await safeOutboundFetch(anthropicModelsUrl, {
...SAFE_OUTBOUND_FETCH_PRESETS.modelsDiscovery,
guard: getProviderOutboundGuard(),
proxyConfig: proxy,
method: "GET",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${bearerToken}`,
},
});
if (anthropicResponse.ok) {
const anthropicData = await anthropicResponse.json();
const { parseVertexAnthropicModels } = await import(
"@/lib/providerModels/vertexAnthropicModelsParser"
);
allModels.push(...parseVertexAnthropicModels(anthropicData));
} else {
console.log("[models] Vertex Anthropic partner discovery failed", {
provider,
region,
status: anthropicResponse.status,
});
}
} catch (err) {
console.log("[models] Vertex Anthropic partner discovery error", {
provider,
error: err instanceof Error ? err.message : String(err),
});
}
}
if (allModels.length > 0) {
return buildApiDiscoveryResponse(allModels);
}

View File

@@ -23,6 +23,10 @@ import { getComboByName } from "@/lib/db/combos";
import { getAllCustomModels } from "@/lib/db/models";
import { resolveProxyForConnection } from "@/lib/db/settings";
import { resolveImageRouteModel } from "@/lib/images/imageRouteModel";
import {
resolveLocalSyncedEndpointRoute,
type LocalSyncedEndpointRoute,
} from "@/lib/providerModels/syncedEndpointRouting";
import { runWithProxyContext } from "@omniroute/open-sse/utils/proxyFetch.ts";
import { attachOmniRouteMetaHeaders } from "@/domain/omnirouteResponseMeta";
import { calculateModalCost } from "@/lib/usage/costCalculator";
@@ -145,6 +149,16 @@ async function postHandler(request, context) {
// Parse model to get provider
let { provider, model: requestedModel } = parseImageModel(body.model);
let isCustomModel = false;
let syncedEndpointRoute: LocalSyncedEndpointRoute | null = null;
if (!provider) {
syncedEndpointRoute = await resolveLocalSyncedEndpointRoute(body.model, "images");
if (syncedEndpointRoute) {
provider = syncedEndpointRoute.provider;
body.model = `${syncedEndpointRoute.provider}/${syncedEndpointRoute.model}`;
isCustomModel = true;
}
}
// If not in built-in registry, check custom models tagged for images
if (!provider) {
@@ -231,9 +245,8 @@ async function postHandler(request, context) {
credentials = await getProviderCredentialsWithQuotaPreflight(
provider,
null,
null,
requestedModel
);
syncedEndpointRoute?.connectionIds ?? null,
requestedModel );
if (!credentials) {
return errorResponse(
HTTP_STATUS.BAD_REQUEST,

View File

@@ -1874,7 +1874,16 @@
"directDownloadHint": "Or download the respective installer format directly:",
"releaseNotes": "Release Notes",
"readMore": "Read More",
"noAuthLabel": "No Auth"
"noAuthLabel": "No Auth",
"readinessEyebrow": "Get ready to route",
"readinessTitle": "Send your first request",
"readinessSubtitle": "Four small steps. OmniRoute checks readiness as you go.",
"readinessStep1": "Connect a provider",
"readinessStep2": "Configure endpoint authentication",
"readinessStep3": "Copy your endpoint",
"readinessStep4": "Send a test request",
"readinessContinue": "Continue setup",
"readinessDismiss": "Dismiss for now"
},
"analytics": {
"title": "Analytics",

View File

@@ -31,6 +31,7 @@ import { isPrivateHost, isCloudMetadataHost } from "@/shared/network/outboundUrl
import { calculateCost } from "@/lib/usage/costCalculator";
import { attachOmniRouteMetaHeaders } from "@/domain/omnirouteResponseMeta";
import { generateRequestId } from "@/shared/utils/requestId";
import { resolveLocalSyncedEndpointRoute } from "@/lib/providerModels/syncedEndpointRouting";
type ValidatedEmbeddingBody = Record<string, unknown> & { model: string };
type ProviderCredentialsResult = Awaited<ReturnType<typeof getProviderCredentials>>;
@@ -164,7 +165,17 @@ export async function createEmbeddingResponse(
model: options.resolvedModel ?? body.model,
}
: parseEmbeddingModel(body.model, dynamicProviders);
const { provider, model: resolvedModel } = parsedModel;
let { provider, model: resolvedModel } = parsedModel;
// #11088: a bare local-model request routes through the connection that
// advertises the requested endpoint — only when no explicit resolvedProvider
// already won above (explicit resolution takes precedence).
const syncedEndpointRoute = options.resolvedProvider
? null
: await resolveLocalSyncedEndpointRoute(body.model, "embeddings");
if (syncedEndpointRoute) {
provider = syncedEndpointRoute.provider;
resolvedModel = syncedEndpointRoute.model;
}
if (!provider) {
return errorResponse(
HTTP_STATUS.BAD_REQUEST,
@@ -172,6 +183,7 @@ export async function createEmbeddingResponse(
);
}
let credentials: ProviderCredentialsResult | null = null;
let providerConfig: EmbeddingProvider | null =
options.resolvedProvider ||
dynamicProviders.find((dp) => dp.id === provider) ||
@@ -179,6 +191,48 @@ export async function createEmbeddingResponse(
null;
let credentialsProviderId = provider;
if (syncedEndpointRoute) {
credentials = await getProviderCredentials(
provider,
null,
syncedEndpointRoute.connectionIds,
syncedEndpointRoute.model
);
if (!credentials) {
return errorResponse(
HTTP_STATUS.BAD_REQUEST,
`No credentials for embedding provider: ${provider}`
);
}
if ("allRateLimited" in credentials && credentials.allRateLimited) {
return unavailableResponse(
HTTP_STATUS.RATE_LIMITED,
`[${provider}] All accounts rate limited`,
credentials.retryAfter,
credentials.retryAfterHuman
);
}
const providerSpecificData = (credentials as { providerSpecificData?: Record<string, unknown> })
.providerSpecificData;
const configuredBaseUrl = providerSpecificData?.baseUrl;
if (typeof configuredBaseUrl !== "string" || configuredBaseUrl.trim().length === 0) {
return errorResponse(
HTTP_STATUS.BAD_REQUEST,
`No base URL configured for embedding provider: ${provider}`
);
}
let baseUrl = configuredBaseUrl.trim();
while (baseUrl.endsWith("/")) baseUrl = baseUrl.slice(0, -1);
providerConfig = {
id: provider,
baseUrl: baseUrl.endsWith("/embeddings") ? baseUrl : `${baseUrl}/embeddings`,
authType: "apikey",
authHeader: "bearer",
models: [],
};
}
if (!providerConfig) {
try {
const allNodes = (await getCachedProviderNodes()) as unknown as EmbeddingProviderNodeRow[];
@@ -226,8 +280,7 @@ export async function createEmbeddingResponse(
);
}
let credentials: ProviderCredentialsResult | null = null;
if (providerConfig.authType !== "none") {
if (!credentials && providerConfig.authType !== "none") {
credentials = await getProviderCredentials(credentialsProviderId);
if (!credentials) {
return errorResponse(

View File

@@ -34,6 +34,8 @@ const IGNORED_METHODS = new Set([
"asyncBatchEmbedContent",
]);
const RETIRED_GEMINI_MODEL_IDS = new Set(["gemini-3.5-flash"]);
export interface GeminiDiscoveryModel {
id: string;
name: string;
@@ -46,36 +48,38 @@ export interface GeminiDiscoveryModel {
}
export function parseGeminiModelsList(data: any): GeminiDiscoveryModel[] {
return (data?.models || []).map((m: Record<string, unknown>) => {
const methods: string[] = Array.isArray(m.supportedGenerationMethods)
? (m.supportedGenerationMethods as string[])
: [];
return (data?.models || [])
.map((m: Record<string, unknown>) => {
const methods: string[] = Array.isArray(m.supportedGenerationMethods)
? (m.supportedGenerationMethods as string[])
: [];
const endpoints = new Set<string>(
methods
.filter((method) => !IGNORED_METHODS.has(method))
.map((method) => METHOD_TO_ENDPOINT[method] || "chat")
);
const endpoints = new Set<string>(
methods
.filter((method) => !IGNORED_METHODS.has(method))
.map((method) => METHOD_TO_ENDPOINT[method] || "chat")
);
const id = ((m.name as string) || (m.id as string) || "").replace(/^models\//, "");
const lowerId = id.toLowerCase();
const id = ((m.name as string) || (m.id as string) || "").replace(/^models\//, "");
const lowerId = id.toLowerCase();
// Keep Veo models in the video bucket even when the method list is incomplete.
if (lowerId.includes("veo")) {
endpoints.add("video");
}
// Keep Veo models in the video bucket even when the method list is incomplete.
if (lowerId.includes("veo")) {
endpoints.add("video");
}
if (endpoints.size === 0) endpoints.add("chat");
if (endpoints.size === 0) endpoints.add("chat");
return {
...m,
id,
name: (m.displayName as string) || id,
supportedEndpoints: [...endpoints],
...(typeof m.inputTokenLimit === "number" ? { inputTokenLimit: m.inputTokenLimit } : {}),
...(typeof m.outputTokenLimit === "number" ? { outputTokenLimit: m.outputTokenLimit } : {}),
...(typeof m.description === "string" ? { description: m.description } : {}),
...(m.thinking === true ? { supportsThinking: true } : {}),
} as GeminiDiscoveryModel;
});
return {
...m,
id,
name: (m.displayName as string) || id,
supportedEndpoints: [...endpoints],
...(typeof m.inputTokenLimit === "number" ? { inputTokenLimit: m.inputTokenLimit } : {}),
...(typeof m.outputTokenLimit === "number" ? { outputTokenLimit: m.outputTokenLimit } : {}),
...(typeof m.description === "string" ? { description: m.description } : {}),
...(m.thinking === true ? { supportsThinking: true } : {}),
} as GeminiDiscoveryModel;
})
.filter((model: GeminiDiscoveryModel) => !RETIRED_GEMINI_MODEL_IDS.has(model.id));
}

View File

@@ -20,9 +20,12 @@ import { normalizeDiscoveredModels } from "@/lib/providerModels/modelDiscovery";
import {
ANTIGRAVITY_MODEL_ALIASES,
ANTIGRAVITY_REVERSE_MODEL_ALIASES,
isDiscoverableAntigravityModelId,
} from "@omniroute/open-sse/config/antigravityModelAliases.ts";
import { isDiscoverableAgyModelId } from "@omniroute/open-sse/config/agyModels.ts";
import { filterChatSelectableModels } from "@omniroute/open-sse/services/modelEndpointPolicy.ts";
import { filterSelectableModels } from "@omniroute/open-sse/services/modelLifecycle.ts";
import { isSelfHostedChatProvider } from "@/shared/constants/providers";
type JsonRecord = Record<string, unknown>;
@@ -253,10 +256,25 @@ export async function importManagedModels({
const previousSyncedAvailableModels =
previousSyncedAvailableModelsInput ??
(await getSyncedAvailableModelsForConnection(providerId, connectionId));
const discoveredModels = filterChatSelectableModels(
providerId,
filterSelectableModels(providerId, normalizeDiscoveredModels(fetchedModels, providerId))
);
const normalizedDiscoveredModels = normalizeDiscoveredModels(fetchedModels, providerId);
// Gemini 3.5 Flash elimination (ddf1bb760, carried from #11259): antigravity/
// agy discovery is restricted to each family's discoverable ids BEFORE any
// chat-selection filtering.
const providerFilteredModels =
providerId === "antigravity"
? normalizedDiscoveredModels.filter((model) => isDiscoverableAntigravityModelId(model.id))
: providerId === "agy"
? normalizedDiscoveredModels.filter((model) => isDiscoverableAgyModelId(model.id))
: normalizedDiscoveredModels;
// #11088 (option 1): self-hosted providers keep their non-chat models — chat
// filtering happens at read time (resolveLocalSyncedEndpointRoute). Every other
// provider keeps the import-time chat filter: the read-time path is gated on
// isSelfHostedChatProvider, so dropping it globally leaked image/video models
// into OpenAI chat selections (#11271).
const selectableModels = filterSelectableModels(providerId, providerFilteredModels);
const discoveredModels = isSelfHostedChatProvider(providerId)
? selectableModels
: filterChatSelectableModels(providerId, selectableModels);
const candidateImportedModels = normalizeImportedModels(discoveredModels);
const importedIds = new Set(candidateImportedModels.map((model) => model.id));

View File

@@ -6,7 +6,6 @@ import {
} from "@/lib/db/models";
import { CANONICAL_EFFORT_VALUES } from "@/shared/reasoning/effortStandardization";
import { isObsoleteKiroModelAlias } from "@omniroute/open-sse/services/kiroModels.ts";
import { filterChatSelectableModels } from "@omniroute/open-sse/services/modelEndpointPolicy.ts";
import { filterSelectableModels } from "@omniroute/open-sse/services/modelLifecycle.ts";
type JsonRecord = Record<string, unknown>;
@@ -379,9 +378,13 @@ export async function persistDiscoveredModels(
connectionId: string,
models: unknown
): Promise<SyncedAvailableModel[]> {
const normalized = filterChatSelectableModels(
// #11088 (option 1): the synced store is endpoint-agnostic — images/embeddings
// models must persist so per-connection endpoint routing (#11088) and the
// /v1/models catalog can see them. Chat selectability is applied at read time
// (auto-pool expansion, chat projections), not at write time.
const normalized = filterSelectableModels(
providerId,
filterSelectableModels(providerId, normalizeDiscoveredModels(models, providerId))
normalizeDiscoveredModels(models, providerId)
);
await replaceSyncedAvailableModelsForConnection(providerId, connectionId, normalized);
return normalized;

View File

@@ -0,0 +1,98 @@
import { z } from "zod";
type JsonRecord = Record<string, unknown>;
const ollamaShowResponseSchema = z
.object({
capabilities: z.array(z.string().max(64)).max(32).optional(),
})
.passthrough();
const OLLAMA_CAPABILITY_TO_ENDPOINT: Readonly<Record<string, string>> = {
completion: "chat",
embedding: "embeddings",
image: "images",
};
const MAX_CONCURRENT_SHOW_REQUESTS = 4;
function asRecord(value: unknown): JsonRecord {
return value && typeof value === "object" && !Array.isArray(value) ? (value as JsonRecord) : {};
}
export function buildOllamaShowUrl(openAiBaseUrl: string): string {
let base = openAiBaseUrl.trim();
while (base.endsWith("/")) base = base.slice(0, -1);
base = base.replace(/\/(?:chat\/completions|completions|embeddings|images\/generations)$/i, "");
if (base.endsWith("/v1")) base = base.slice(0, -3);
return `${base}/api/show`;
}
export function applyOllamaShowCapabilities(model: unknown, showResponse: unknown): JsonRecord {
const record = asRecord(model);
const parsed = ollamaShowResponseSchema.safeParse(showResponse);
if (!parsed.success || !parsed.data.capabilities) return record;
const capabilities = Array.from(
new Set(parsed.data.capabilities.map((value) => value.trim().toLowerCase()).filter(Boolean))
);
const supportedEndpoints = Array.from(
new Set(
capabilities
.map((capability) => OLLAMA_CAPABILITY_TO_ENDPOINT[capability])
.filter((endpoint): endpoint is string => Boolean(endpoint))
)
);
if (supportedEndpoints.length === 0) return record;
const apiFormat = supportedEndpoints.includes("chat")
? "chat-completions"
: supportedEndpoints.includes("embeddings")
? "embeddings"
: "images-generations";
return {
...record,
apiFormat,
supportedEndpoints,
...(capabilities.includes("vision") ? { supportsVision: true } : {}),
...(capabilities.includes("tools") ? { supportsTools: true } : {}),
...(capabilities.includes("thinking") ? { supportsThinking: true } : {}),
};
}
export async function enrichOllamaModelsWithCapabilities(
models: unknown[],
fetchShow: (modelId: string) => Promise<unknown | null>
): Promise<JsonRecord[]> {
const output: JsonRecord[] = new Array(models.length);
let nextIndex = 0;
const worker = async () => {
while (nextIndex < models.length) {
const index = nextIndex++;
const model = asRecord(models[index]);
const modelId =
typeof model.id === "string"
? model.id
: typeof model.name === "string"
? model.name
: typeof model.model === "string"
? model.model
: null;
if (!modelId) {
output[index] = model;
continue;
}
try {
output[index] = applyOllamaShowCapabilities(model, await fetchShow(modelId));
} catch {
output[index] = model;
}
}
};
const workerCount = Math.min(MAX_CONCURRENT_SHOW_REQUESTS, Math.max(1, models.length));
await Promise.all(Array.from({ length: workerCount }, () => worker()));
return output;
}

View File

@@ -0,0 +1,31 @@
import { getSyncedAvailableModelsByConnection } from "@/lib/db/models";
import { isSelfHostedChatProvider, resolveProviderId } from "@/shared/constants/providers";
export type LocalSyncedEndpointRoute = {
provider: string;
model: string;
connectionIds: string[];
};
export async function resolveLocalSyncedEndpointRoute(
modelStr: string,
endpoint: "embeddings" | "images"
): Promise<LocalSyncedEndpointRoute | null> {
const slashIndex = modelStr.indexOf("/");
if (slashIndex <= 0 || slashIndex === modelStr.length - 1) return null;
const provider = resolveProviderId(modelStr.slice(0, slashIndex));
const model = modelStr.slice(slashIndex + 1);
if (!isSelfHostedChatProvider(provider)) return null;
const byConnection = await getSyncedAvailableModelsByConnection(provider);
const connectionIds = Object.entries(byConnection)
.filter(([, models]) =>
models.some(
(candidate) => candidate.id === model && candidate.supportedEndpoints?.includes(endpoint)
)
)
.map(([connectionId]) => connectionId);
return connectionIds.length > 0 ? { provider, model, connectionIds } : null;
}

View File

@@ -0,0 +1,44 @@
interface VertexPublisherModel {
name?: string;
displayName?: string;
description?: string;
supportedActions?: string[];
versionId?: string;
[key: string]: unknown;
}
export interface VertexAnthropicDiscoveryModel {
id: string;
name: string;
supportedEndpoints: string[];
targetFormat: string;
owned_by: string;
description?: string;
[key: string]: unknown;
}
export function parseVertexAnthropicModels(data: unknown): VertexAnthropicDiscoveryModel[] {
if (!data || typeof data !== "object") return [];
const envelope = data as { models?: unknown[] };
const models = Array.isArray(envelope.models) ? envelope.models : [];
return models
.map((m: unknown) => {
const model = m as VertexPublisherModel;
const rawName = typeof model.name === "string" ? model.name : "";
// "publishers/anthropic/models/claude-sonnet-4-6" or
// "projects/x/locations/y/publishers/anthropic/models/claude-sonnet-4-6"
const id = rawName.replace(/^(?:projects\/[^/]+\/locations\/[^/]+\/)?publishers\/anthropic\/models\//, "") || rawName;
if (!id) return null;
return {
id,
name: (typeof model.displayName === "string" && model.displayName) || id,
supportedEndpoints: ["chat"],
targetFormat: "claude",
...(typeof model.description === "string" ? { description: model.description } : {}),
owned_by: "anthropic",
} satisfies VertexAnthropicDiscoveryModel;
})
.filter((m): m is VertexAnthropicDiscoveryModel => m !== null);
}

View File

@@ -20,6 +20,11 @@ export const DEFAULT_ALLOWED_ORIGINS: readonly string[] = Object.freeze([
"http://127.0.0.1:20128",
"http://localhost:20128",
"http://[::1]:20128",
// 0.0.0.0 is the "unspecified" address but browsers treat it as loopback
// when the user pastes it into the address bar; the dashboard is reachable
// at http://0.0.0.0:20128 and its WS Origin is exactly that string. Same
// local-only posture as the entries above — it never refers to a LAN host.
"http://0.0.0.0:20128",
]);
/**

View File

@@ -401,34 +401,56 @@ const ProviderIcon = memo(function ProviderIcon({
className={className}
style={{ display: "inline-flex", alignItems: "center", ...style }}
>
<Image
{/* eslint-disable-next-line @next/next/no-img-element -- themed local SVG asset; see the Tier 2 comment for why these use a plain <img> */}
<img
src={themedSrc}
alt={providerId}
width={size}
height={size}
style={{ objectFit: "contain" }}
style={{
objectFit: "contain",
flex: "none",
width: "auto",
height: "auto",
maxWidth: size,
maxHeight: size,
}}
onError={() => setFailedAssets((current) => ({ ...current, [themedKey]: true }))}
unoptimized
/>
</span>
);
}
// Tier 2: Local SVG — fastest, cached separately from the JS bundle
// Tier 2: Local SVG — fastest, cached separately from the JS bundle.
// Rendered as a plain <img> (not next/image): provider SVGs carry their own
// intrinsic aspect ratio (e.g. opencode.svg is 234×42), and next/image's
// dev-mode check warns whenever the layout size differs from the square
// width/height attributes — a false positive for non-square logos rendered
// at fixed icon sizes. We keep `width/height` attributes for layout reserve
// but let the intrinsic ratio win on both axes (`width/height: "auto"`) so
// wide logos like opencode render at their true aspect ratio instead of
// being letterboxed into a 1:1 box.
if (hasSvg && !svgFailed) {
return (
<span
className={className}
style={{ display: "inline-flex", alignItems: "center", ...style }}
>
<Image
{/* eslint-disable-next-line @next/next/no-img-element -- local static SVG asset, see comment above */}
<img
src={`/providers/${localSvgId}.svg`}
alt={providerId}
width={size}
height={size}
style={{ objectFit: "contain" }}
style={{
objectFit: "contain",
flex: "none",
width: "auto",
height: "auto",
maxWidth: size,
maxHeight: size,
}}
onError={() => setFailedAssets((current) => ({ ...current, [svgKey]: true }))}
unoptimized
/>
</span>
);

View File

@@ -1,7 +1,6 @@
"use client";
import Link from "next/link";
import Image from "next/image";
import { useTranslations } from "next-intl";
import type { CliCatalogEntry } from "@/shared/schemas/cliCatalog";
import type { ToolBatchStatus } from "@/shared/types/cliBatchStatus";
@@ -38,12 +37,18 @@ export default function CliToolCard({
<div className="flex items-center gap-2.5">
{/* Icon / image */}
{imageSrc ? (
<Image
// Plain <img> (not next/image): tool SVGs are non-square (opencode
// 234×42, cursor 467×532) and next/image's dev check warns whenever the
// rendered aspect-ratio size differs from the square width/height
// attributes. object-contain + max caps keep the logo at its true ratio.
// eslint-disable-next-line @next/next/no-img-element -- local static SVG asset
<img
src={imageSrc}
alt={tool.name}
width={32}
height={32}
className="rounded-md object-contain flex-shrink-0"
style={{ width: "auto", height: "auto", maxWidth: 32, maxHeight: 32 }}
/>
) : (
<span

View File

@@ -99,7 +99,7 @@ const GPT_5_6_MODEL_SPEC = {
supportsVision: true,
} satisfies ModelSpec;
const GEMINI_35_FLASH_MODEL_SPEC = {
const GEMINI_36_FLASH_MODEL_SPEC = {
maxOutputTokens: 65536,
contextWindow: 1048576,
supportsThinking: false,
@@ -160,7 +160,7 @@ export const MODEL_SPECS: Record<string, ModelSpec> = {
aliases: ["openai/gpt-4o"],
},
// ── Gemini 2.5 and provider-neutral 3.5 Flash series ─────────────
// ── Gemini 2.5 Flash ─────────────────────────────────────────────
"gemini-2.5-flash": {
maxOutputTokens: 65536,
contextWindow: 1048576,
@@ -171,16 +171,6 @@ export const MODEL_SPECS: Record<string, ModelSpec> = {
supportsTools: true,
supportsVision: true,
},
"gemini-3.5-flash-extra-low": {
...GEMINI_35_FLASH_MODEL_SPEC,
thinkingBudgetCap: 0,
},
"gemini-3.5-flash-low": { ...GEMINI_35_FLASH_MODEL_SPEC },
"gemini-3-flash-agent": {
...GEMINI_35_FLASH_MODEL_SPEC,
thinkingBudgetCap: 0,
},
// ── Gemini 3.7 Flash (current Antigravity/AGY live tiers) ─────────
// The tier suffix configures the thinking budget passed to the upstream
// gemini-3.7-flash-tiered backend (high: 24.5k, medium: 8k, low: 1k).
@@ -234,9 +224,9 @@ export const MODEL_SPECS: Record<string, ModelSpec> = {
// Provider-neutral compatibility for providers that still serve Gemini 3.6.
// Antigravity/AGY availability is governed by their own provider catalogs and
// retirement filters; these shared specs must not be treated as an allowlist.
"gemini-3.6-flash-high": { ...GEMINI_35_FLASH_MODEL_SPEC },
"gemini-3.6-flash-medium": { ...GEMINI_35_FLASH_MODEL_SPEC },
"gemini-3.6-flash-low": { ...GEMINI_35_FLASH_MODEL_SPEC },
"gemini-3.6-flash-high": { ...GEMINI_36_FLASH_MODEL_SPEC },
"gemini-3.6-flash-medium": { ...GEMINI_36_FLASH_MODEL_SPEC },
"gemini-3.6-flash-low": { ...GEMINI_36_FLASH_MODEL_SPEC },
// ── Gemini 3 Flash series ───────────────────────────────────────
"gemini-3-flash": {
@@ -282,20 +272,6 @@ export const MODEL_SPECS: Record<string, ModelSpec> = {
aliases: ["gemini-3-pro-low"],
},
// ── Gemini 3.5 Flash ─────────────────────────────────────────────
// #10286: the base Google AI Studio model DOES support reasoning (it has
// an effort-tier alias gemini-3.5-flash-high) — override the shared spec's
// supportsThinking:false here only. Do NOT flip GEMINI_35_FLASH_MODEL_SPEC
// itself: it is also spread into the Antigravity flash-tier aliases
// (gemini-3.5-flash-low/-extra-low, gemini-3-flash-agent, gemini-3.6-flash-*)
// which reject client-supplied thinking params because the model id itself
// selects the reasoning tier upstream.
"gemini-3.5-flash": {
...GEMINI_35_FLASH_MODEL_SPEC,
supportsThinking: true,
aliases: ["gemini-3.5-flash-high"],
},
// ── Claude Opus 4.5 ─────────────────────────────────────────────
"claude-opus-4-5": {
maxOutputTokens: 32768,

View File

@@ -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);

View File

@@ -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
);

View File

@@ -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",

View File

@@ -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",

View File

@@ -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 () => {

View File

@@ -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");
});

View File

@@ -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);
});

View File

@@ -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 () => {

View File

@@ -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 () => {

View File

@@ -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;

View File

@@ -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(

View File

@@ -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`);
}
});

View 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"
);
});

View File

@@ -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) {

View File

@@ -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 {

View File

@@ -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");
});

View File

@@ -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 }> }>;
};

View File

@@ -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");

View File

@@ -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 }> }>;
};

View File

@@ -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"
);
});

View File

@@ -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,

View 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;
}

View 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}`);
}
});
});

View File

@@ -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.

View File

@@ -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);
}
});

View File

@@ -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);

View 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");
});

View File

@@ -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,

View 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");
});

View File

@@ -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);

View File

@@ -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", () => {

View File

@@ -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");

View File

@@ -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" } }],

View File

@@ -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"));

View File

@@ -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" },

View File

@@ -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

View 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");
});