mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-06 15:22:12 +03:00
fix(v3.8.4-followup): 5 bug fixes from triage of 79 open issues (#2753)
Integrated into release/v3.8.4
This commit is contained in:
committed by
GitHub
parent
b6c2ece344
commit
a3476298d9
File diff suppressed because one or more lines are too long
@@ -4,9 +4,9 @@ import { default as __fd_glob_63 } from "../docs/routing/meta.json?collection=do
|
||||
import { default as __fd_glob_62 } from "../docs/reference/openapi.yaml?collection=docs"
|
||||
import { default as __fd_glob_61 } from "../docs/reference/meta.json?collection=docs"
|
||||
import { default as __fd_glob_60 } from "../docs/ops/meta.json?collection=docs"
|
||||
import { default as __fd_glob_59 } from "../docs/compression/meta.json?collection=docs"
|
||||
import { default as __fd_glob_58 } from "../docs/guides/meta.json?collection=docs"
|
||||
import { default as __fd_glob_57 } from "../docs/frameworks/meta.json?collection=docs"
|
||||
import { default as __fd_glob_59 } from "../docs/guides/meta.json?collection=docs"
|
||||
import { default as __fd_glob_58 } from "../docs/frameworks/meta.json?collection=docs"
|
||||
import { default as __fd_glob_57 } from "../docs/compression/meta.json?collection=docs"
|
||||
import { default as __fd_glob_56 } from "../docs/architecture/meta.json?collection=docs"
|
||||
import { default as __fd_glob_55 } from "../docs/meta.json?collection=docs"
|
||||
import * as __fd_glob_54 from "../docs/security/STEALTH_GUIDE.md?collection=docs"
|
||||
@@ -32,28 +32,28 @@ import * as __fd_glob_35 from "../docs/ops/PROXY_GUIDE.md?collection=docs"
|
||||
import * as __fd_glob_34 from "../docs/ops/FLY_IO_DEPLOYMENT_GUIDE.md?collection=docs"
|
||||
import * as __fd_glob_33 from "../docs/ops/E2E_DASHBOARD_SHAKEDOWN_v3.8.0.md?collection=docs"
|
||||
import * as __fd_glob_32 from "../docs/ops/COVERAGE_PLAN.md?collection=docs"
|
||||
import * as __fd_glob_31 from "../docs/frameworks/WEBHOOKS.md?collection=docs"
|
||||
import * as __fd_glob_30 from "../docs/frameworks/SKILLS.md?collection=docs"
|
||||
import * as __fd_glob_29 from "../docs/frameworks/OPENCODE.md?collection=docs"
|
||||
import * as __fd_glob_28 from "../docs/frameworks/MEMORY.md?collection=docs"
|
||||
import * as __fd_glob_27 from "../docs/frameworks/MCP-SERVER.md?collection=docs"
|
||||
import * as __fd_glob_26 from "../docs/frameworks/GAMIFICATION.md?collection=docs"
|
||||
import * as __fd_glob_25 from "../docs/frameworks/EVALS.md?collection=docs"
|
||||
import * as __fd_glob_24 from "../docs/frameworks/EMBEDDED-SERVICES.md?collection=docs"
|
||||
import * as __fd_glob_23 from "../docs/frameworks/CLOUD_AGENT.md?collection=docs"
|
||||
import * as __fd_glob_22 from "../docs/frameworks/AGENT_PROTOCOLS_GUIDE.md?collection=docs"
|
||||
import * as __fd_glob_21 from "../docs/frameworks/A2A-SERVER.md?collection=docs"
|
||||
import * as __fd_glob_20 from "../docs/guides/USER_GUIDE.md?collection=docs"
|
||||
import * as __fd_glob_19 from "../docs/guides/UNINSTALL.md?collection=docs"
|
||||
import * as __fd_glob_18 from "../docs/guides/TROUBLESHOOTING.md?collection=docs"
|
||||
import * as __fd_glob_17 from "../docs/guides/TERMUX_GUIDE.md?collection=docs"
|
||||
import * as __fd_glob_16 from "../docs/guides/SETUP_GUIDE.md?collection=docs"
|
||||
import * as __fd_glob_15 from "../docs/guides/PWA_GUIDE.md?collection=docs"
|
||||
import * as __fd_glob_14 from "../docs/guides/KIRO_SETUP.md?collection=docs"
|
||||
import * as __fd_glob_13 from "../docs/guides/I18N.md?collection=docs"
|
||||
import * as __fd_glob_12 from "../docs/guides/FEATURES.md?collection=docs"
|
||||
import * as __fd_glob_11 from "../docs/guides/ELECTRON_GUIDE.md?collection=docs"
|
||||
import * as __fd_glob_10 from "../docs/guides/DOCKER_GUIDE.md?collection=docs"
|
||||
import * as __fd_glob_31 from "../docs/guides/USER_GUIDE.md?collection=docs"
|
||||
import * as __fd_glob_30 from "../docs/guides/UNINSTALL.md?collection=docs"
|
||||
import * as __fd_glob_29 from "../docs/guides/TROUBLESHOOTING.md?collection=docs"
|
||||
import * as __fd_glob_28 from "../docs/guides/TERMUX_GUIDE.md?collection=docs"
|
||||
import * as __fd_glob_27 from "../docs/guides/SETUP_GUIDE.md?collection=docs"
|
||||
import * as __fd_glob_26 from "../docs/guides/PWA_GUIDE.md?collection=docs"
|
||||
import * as __fd_glob_25 from "../docs/guides/KIRO_SETUP.md?collection=docs"
|
||||
import * as __fd_glob_24 from "../docs/guides/I18N.md?collection=docs"
|
||||
import * as __fd_glob_23 from "../docs/guides/FEATURES.md?collection=docs"
|
||||
import * as __fd_glob_22 from "../docs/guides/ELECTRON_GUIDE.md?collection=docs"
|
||||
import * as __fd_glob_21 from "../docs/guides/DOCKER_GUIDE.md?collection=docs"
|
||||
import * as __fd_glob_20 from "../docs/frameworks/WEBHOOKS.md?collection=docs"
|
||||
import * as __fd_glob_19 from "../docs/frameworks/SKILLS.md?collection=docs"
|
||||
import * as __fd_glob_18 from "../docs/frameworks/OPENCODE.md?collection=docs"
|
||||
import * as __fd_glob_17 from "../docs/frameworks/MEMORY.md?collection=docs"
|
||||
import * as __fd_glob_16 from "../docs/frameworks/MCP-SERVER.md?collection=docs"
|
||||
import * as __fd_glob_15 from "../docs/frameworks/GAMIFICATION.md?collection=docs"
|
||||
import * as __fd_glob_14 from "../docs/frameworks/EVALS.md?collection=docs"
|
||||
import * as __fd_glob_13 from "../docs/frameworks/EMBEDDED-SERVICES.md?collection=docs"
|
||||
import * as __fd_glob_12 from "../docs/frameworks/CLOUD_AGENT.md?collection=docs"
|
||||
import * as __fd_glob_11 from "../docs/frameworks/AGENT_PROTOCOLS_GUIDE.md?collection=docs"
|
||||
import * as __fd_glob_10 from "../docs/frameworks/A2A-SERVER.md?collection=docs"
|
||||
import * as __fd_glob_9 from "../docs/compression/RTK_COMPRESSION.md?collection=docs"
|
||||
import * as __fd_glob_8 from "../docs/compression/COMPRESSION_RULES_FORMAT.md?collection=docs"
|
||||
import * as __fd_glob_7 from "../docs/compression/COMPRESSION_LANGUAGE_PACKS.md?collection=docs"
|
||||
@@ -72,4 +72,4 @@ const create = server<typeof Config, import("fumadocs-mdx/runtime/types").Intern
|
||||
}
|
||||
}>({"doc":{"passthroughs":["extractedReferences"]}});
|
||||
|
||||
export const docs = await create.docs("docs", "docs", {"meta.json": __fd_glob_55, "architecture/meta.json": __fd_glob_56, "frameworks/meta.json": __fd_glob_57, "guides/meta.json": __fd_glob_58, "compression/meta.json": __fd_glob_59, "ops/meta.json": __fd_glob_60, "reference/meta.json": __fd_glob_61, "reference/openapi.yaml": __fd_glob_62, "routing/meta.json": __fd_glob_63, "security/meta.json": __fd_glob_64, }, {"architecture/ARCHITECTURE.md": __fd_glob_0, "architecture/AUTHZ_GUIDE.md": __fd_glob_1, "architecture/CODEBASE_DOCUMENTATION.md": __fd_glob_2, "architecture/REPOSITORY_MAP.md": __fd_glob_3, "architecture/RESILIENCE_GUIDE.md": __fd_glob_4, "compression/COMPRESSION_ENGINES.md": __fd_glob_5, "compression/COMPRESSION_GUIDE.md": __fd_glob_6, "compression/COMPRESSION_LANGUAGE_PACKS.md": __fd_glob_7, "compression/COMPRESSION_RULES_FORMAT.md": __fd_glob_8, "compression/RTK_COMPRESSION.md": __fd_glob_9, "guides/DOCKER_GUIDE.md": __fd_glob_10, "guides/ELECTRON_GUIDE.md": __fd_glob_11, "guides/FEATURES.md": __fd_glob_12, "guides/I18N.md": __fd_glob_13, "guides/KIRO_SETUP.md": __fd_glob_14, "guides/PWA_GUIDE.md": __fd_glob_15, "guides/SETUP_GUIDE.md": __fd_glob_16, "guides/TERMUX_GUIDE.md": __fd_glob_17, "guides/TROUBLESHOOTING.md": __fd_glob_18, "guides/UNINSTALL.md": __fd_glob_19, "guides/USER_GUIDE.md": __fd_glob_20, "frameworks/A2A-SERVER.md": __fd_glob_21, "frameworks/AGENT_PROTOCOLS_GUIDE.md": __fd_glob_22, "frameworks/CLOUD_AGENT.md": __fd_glob_23, "frameworks/EMBEDDED-SERVICES.md": __fd_glob_24, "frameworks/EVALS.md": __fd_glob_25, "frameworks/GAMIFICATION.md": __fd_glob_26, "frameworks/MCP-SERVER.md": __fd_glob_27, "frameworks/MEMORY.md": __fd_glob_28, "frameworks/OPENCODE.md": __fd_glob_29, "frameworks/SKILLS.md": __fd_glob_30, "frameworks/WEBHOOKS.md": __fd_glob_31, "ops/COVERAGE_PLAN.md": __fd_glob_32, "ops/E2E_DASHBOARD_SHAKEDOWN_v3.8.0.md": __fd_glob_33, "ops/FLY_IO_DEPLOYMENT_GUIDE.md": __fd_glob_34, "ops/PROXY_GUIDE.md": __fd_glob_35, "ops/RELEASE_CHECKLIST.md": __fd_glob_36, "ops/SQLITE_RUNTIME.md": __fd_glob_37, "ops/TUNNELS_GUIDE.md": __fd_glob_38, "ops/VM_DEPLOYMENT_GUIDE.md": __fd_glob_39, "reference/API_REFERENCE.md": __fd_glob_40, "reference/CLI-TOOLS.md": __fd_glob_41, "reference/ENVIRONMENT.md": __fd_glob_42, "reference/FREE_TIERS.md": __fd_glob_43, "reference/PROVIDER_REFERENCE.md": __fd_glob_44, "routing/AUTO-COMBO.md": __fd_glob_45, "routing/REASONING_REPLAY.md": __fd_glob_46, "security/CLI_TOKEN.md": __fd_glob_47, "security/CLI_TOKEN_AUTH.md": __fd_glob_48, "security/COMPLIANCE.md": __fd_glob_49, "security/ERROR_SANITIZATION.md": __fd_glob_50, "security/GUARDRAILS.md": __fd_glob_51, "security/PUBLIC_CREDS.md": __fd_glob_52, "security/ROUTE_GUARD_TIERS.md": __fd_glob_53, "security/STEALTH_GUIDE.md": __fd_glob_54, });
|
||||
export const docs = await create.docs("docs", "docs", {"meta.json": __fd_glob_55, "architecture/meta.json": __fd_glob_56, "compression/meta.json": __fd_glob_57, "frameworks/meta.json": __fd_glob_58, "guides/meta.json": __fd_glob_59, "ops/meta.json": __fd_glob_60, "reference/meta.json": __fd_glob_61, "reference/openapi.yaml": __fd_glob_62, "routing/meta.json": __fd_glob_63, "security/meta.json": __fd_glob_64, }, {"architecture/ARCHITECTURE.md": __fd_glob_0, "architecture/AUTHZ_GUIDE.md": __fd_glob_1, "architecture/CODEBASE_DOCUMENTATION.md": __fd_glob_2, "architecture/REPOSITORY_MAP.md": __fd_glob_3, "architecture/RESILIENCE_GUIDE.md": __fd_glob_4, "compression/COMPRESSION_ENGINES.md": __fd_glob_5, "compression/COMPRESSION_GUIDE.md": __fd_glob_6, "compression/COMPRESSION_LANGUAGE_PACKS.md": __fd_glob_7, "compression/COMPRESSION_RULES_FORMAT.md": __fd_glob_8, "compression/RTK_COMPRESSION.md": __fd_glob_9, "frameworks/A2A-SERVER.md": __fd_glob_10, "frameworks/AGENT_PROTOCOLS_GUIDE.md": __fd_glob_11, "frameworks/CLOUD_AGENT.md": __fd_glob_12, "frameworks/EMBEDDED-SERVICES.md": __fd_glob_13, "frameworks/EVALS.md": __fd_glob_14, "frameworks/GAMIFICATION.md": __fd_glob_15, "frameworks/MCP-SERVER.md": __fd_glob_16, "frameworks/MEMORY.md": __fd_glob_17, "frameworks/OPENCODE.md": __fd_glob_18, "frameworks/SKILLS.md": __fd_glob_19, "frameworks/WEBHOOKS.md": __fd_glob_20, "guides/DOCKER_GUIDE.md": __fd_glob_21, "guides/ELECTRON_GUIDE.md": __fd_glob_22, "guides/FEATURES.md": __fd_glob_23, "guides/I18N.md": __fd_glob_24, "guides/KIRO_SETUP.md": __fd_glob_25, "guides/PWA_GUIDE.md": __fd_glob_26, "guides/SETUP_GUIDE.md": __fd_glob_27, "guides/TERMUX_GUIDE.md": __fd_glob_28, "guides/TROUBLESHOOTING.md": __fd_glob_29, "guides/UNINSTALL.md": __fd_glob_30, "guides/USER_GUIDE.md": __fd_glob_31, "ops/COVERAGE_PLAN.md": __fd_glob_32, "ops/E2E_DASHBOARD_SHAKEDOWN_v3.8.0.md": __fd_glob_33, "ops/FLY_IO_DEPLOYMENT_GUIDE.md": __fd_glob_34, "ops/PROXY_GUIDE.md": __fd_glob_35, "ops/RELEASE_CHECKLIST.md": __fd_glob_36, "ops/SQLITE_RUNTIME.md": __fd_glob_37, "ops/TUNNELS_GUIDE.md": __fd_glob_38, "ops/VM_DEPLOYMENT_GUIDE.md": __fd_glob_39, "reference/API_REFERENCE.md": __fd_glob_40, "reference/CLI-TOOLS.md": __fd_glob_41, "reference/ENVIRONMENT.md": __fd_glob_42, "reference/FREE_TIERS.md": __fd_glob_43, "reference/PROVIDER_REFERENCE.md": __fd_glob_44, "routing/AUTO-COMBO.md": __fd_glob_45, "routing/REASONING_REPLAY.md": __fd_glob_46, "security/CLI_TOKEN.md": __fd_glob_47, "security/CLI_TOKEN_AUTH.md": __fd_glob_48, "security/COMPLIANCE.md": __fd_glob_49, "security/ERROR_SANITIZATION.md": __fd_glob_50, "security/GUARDRAILS.md": __fd_glob_51, "security/PUBLIC_CREDS.md": __fd_glob_52, "security/ROUTE_GUARD_TIERS.md": __fd_glob_53, "security/STEALTH_GUIDE.md": __fd_glob_54, });
|
||||
@@ -2,6 +2,14 @@
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### 🔧 Bug Fixes
|
||||
|
||||
- **validation:** add Poolside specialty validator (direct `/chat/completions` probe — Poolside has no `/v1/models` endpoint and returns 401 for unknown routes, which the generic `/models` flow misread as "invalid API key") (#2723)
|
||||
- **validation:** add NVIDIA NIM specialty validator and harden `normalizeBaseUrl` against non-string `providerSpecificData.baseUrl` — fixes the `e.startsWith is not a function` TypeError that surfaced after minification (#2463)
|
||||
- **cli:** `omniroute compression *` falls back to direct REST endpoints (`/api/settings/compression`, `/api/context/combos`, `/api/context/analytics`) when `/api/mcp/tools/call` returns 404; normalize `none → off` / `hybrid → stacked` engine aliases (#2688)
|
||||
- **cli:** import `cli-helper/tool-detector` and `cli-helper/doctor/checks` with the explicit `.ts` extension that tsx resolves directly, so the published npm package (which ships only the `.ts` source) no longer crashes with `Cannot find module '…tool-detector.js'` (#2509)
|
||||
- **authz:** make the DB feature-flag override authoritative over `process.env` for `OMNIROUTE_ALLOW_PRIVATE_PROVIDER_URLS`, so toggling "Allow Private Provider URLs" in the Electron dashboard takes effect without restarting the spawned server (#2575)
|
||||
|
||||
## [3.8.4] — 2026-05-26
|
||||
|
||||
### 🔒 Security
|
||||
|
||||
@@ -3,12 +3,49 @@ import { apiFetch } from "../api.mjs";
|
||||
import { emit } from "../output.mjs";
|
||||
import { t } from "../i18n.mjs";
|
||||
|
||||
const VALID_ENGINES = ["caveman", "rtk", "hybrid", "none"];
|
||||
// #2688 — CLI no longer assumes MCP is enabled. Engine names are normalized
|
||||
// to the current core set; legacy aliases continue to work.
|
||||
const VALID_ENGINES = ["off", "caveman", "rtk", "stacked"];
|
||||
const ENGINE_ALIASES = { none: "off", hybrid: "stacked" };
|
||||
|
||||
async function mcpCall(name, args) {
|
||||
const res = await apiFetch("/api/mcp/tools/call", {
|
||||
method: "POST",
|
||||
body: { name, arguments: args },
|
||||
function normalizeEngine(name) {
|
||||
return ENGINE_ALIASES[name] ?? name;
|
||||
}
|
||||
|
||||
// Direct REST fallbacks used when the MCP tool surface is not mounted (404).
|
||||
// Keeps every subcommand working on minimal builds.
|
||||
async function restCompressionStatus() {
|
||||
const [settingsRes, combosRes, analyticsRes] = await Promise.all([
|
||||
apiFetch("/api/settings/compression"),
|
||||
apiFetch("/api/context/combos"),
|
||||
apiFetch("/api/context/analytics?period=7d").catch(() => null),
|
||||
]);
|
||||
const settings = settingsRes.ok ? await settingsRes.json() : {};
|
||||
const combosBody = combosRes.ok ? await combosRes.json() : { combos: [] };
|
||||
const analytics = analyticsRes && analyticsRes.ok ? await analyticsRes.json() : null;
|
||||
return {
|
||||
engine: settings.engine ?? null,
|
||||
settings,
|
||||
combos: combosBody.combos ?? combosBody,
|
||||
analytics,
|
||||
};
|
||||
}
|
||||
|
||||
async function restCompressionConfigure(config) {
|
||||
const body = { ...config };
|
||||
if (body.engine) body.engine = normalizeEngine(body.engine);
|
||||
const res = await apiFetch("/api/settings/compression", { method: "PUT", body });
|
||||
if (!res.ok) {
|
||||
process.stderr.write(`Error: ${res.status}\n`);
|
||||
process.exit(1);
|
||||
}
|
||||
return res.json();
|
||||
}
|
||||
|
||||
async function restSetEngine(name) {
|
||||
const res = await apiFetch("/api/settings/compression", {
|
||||
method: "PUT",
|
||||
body: { engine: normalizeEngine(name) },
|
||||
});
|
||||
if (!res.ok) {
|
||||
process.stderr.write(`Error: ${res.status}\n`);
|
||||
@@ -17,6 +54,42 @@ async function mcpCall(name, args) {
|
||||
return res.json();
|
||||
}
|
||||
|
||||
async function restListCombos() {
|
||||
const res = await apiFetch("/api/context/combos");
|
||||
if (!res.ok) {
|
||||
process.stderr.write(`Error: ${res.status}\n`);
|
||||
process.exit(1);
|
||||
}
|
||||
const body = await res.json();
|
||||
return body.combos ?? body;
|
||||
}
|
||||
|
||||
async function restComboStats(period) {
|
||||
const res = await apiFetch(
|
||||
`/api/context/analytics?period=${encodeURIComponent(period ?? "7d")}`
|
||||
);
|
||||
if (!res.ok) {
|
||||
process.stderr.write(`Error: ${res.status}\n`);
|
||||
process.exit(1);
|
||||
}
|
||||
return res.json();
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
process.stderr.write(`Error: ${res.status}\n`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
async function confirm(q) {
|
||||
return new Promise((resolve) => {
|
||||
process.stdout.write(`${q} (yes/no) `);
|
||||
@@ -26,7 +99,7 @@ async function confirm(q) {
|
||||
}
|
||||
|
||||
export async function runCompressionStatus(opts, cmd) {
|
||||
const data = await mcpCall("omniroute_compression_status", {});
|
||||
const data = await mcpCall("omniroute_compression_status", {}, restCompressionStatus);
|
||||
emit(data, cmd.optsWithGlobals());
|
||||
}
|
||||
|
||||
@@ -37,17 +110,22 @@ export async function runCompressionConfigure(opts, cmd) {
|
||||
config.caveman = { aggressiveness: opts.cavemanAggressiveness };
|
||||
if (opts.rtkBudget !== undefined) config.rtk = { tokenBudget: opts.rtkBudget };
|
||||
if (opts.languagePack) config.languagePack = opts.languagePack;
|
||||
const data = await mcpCall("omniroute_compression_configure", config);
|
||||
const data = await mcpCall("omniroute_compression_configure", config, () =>
|
||||
restCompressionConfigure(config)
|
||||
);
|
||||
emit(data, cmd.optsWithGlobals());
|
||||
}
|
||||
|
||||
export async function runCompressionEngineSet(name, opts, cmd) {
|
||||
if (!VALID_ENGINES.includes(name)) {
|
||||
const normalized = normalizeEngine(name);
|
||||
if (!VALID_ENGINES.includes(normalized)) {
|
||||
process.stderr.write(`Unknown engine: ${name}. Valid: ${VALID_ENGINES.join(", ")}\n`);
|
||||
process.exit(2);
|
||||
}
|
||||
await mcpCall("omniroute_set_compression_engine", { engine: name });
|
||||
process.stdout.write(`Engine: ${name}\n`);
|
||||
await mcpCall("omniroute_set_compression_engine", { engine: normalized }, () =>
|
||||
restSetEngine(normalized)
|
||||
);
|
||||
process.stdout.write(`Engine: ${normalized}\n`);
|
||||
}
|
||||
|
||||
export async function runCompressionPreview(opts, cmd) {
|
||||
@@ -86,22 +164,28 @@ export function registerCompression(program) {
|
||||
const engine = cmp.command("engine").description(t("compression.engine.description"));
|
||||
engine.command("set <name>").action(runCompressionEngineSet);
|
||||
engine.command("get").action(async (opts, cmd) => {
|
||||
const data = await mcpCall("omniroute_compression_status", {});
|
||||
const data = await mcpCall("omniroute_compression_status", {}, restCompressionStatus);
|
||||
process.stdout.write(`${data.engine ?? "(default)"}\n`);
|
||||
});
|
||||
|
||||
const combos = cmp.command("combos").description(t("compression.combos.description"));
|
||||
combos.command("list").action(async (opts, cmd) => {
|
||||
const data = await mcpCall("omniroute_list_compression_combos", {});
|
||||
const data = await mcpCall(
|
||||
"omniroute_list_compression_combos",
|
||||
{},
|
||||
async () => ({ combos: await restListCombos() })
|
||||
);
|
||||
emit(data.combos ?? data, cmd.optsWithGlobals());
|
||||
});
|
||||
combos
|
||||
.command("stats")
|
||||
.option("--period <p>", null, "7d")
|
||||
.action(async (opts, cmd) => {
|
||||
const data = await mcpCall("omniroute_compression_combo_stats", {
|
||||
period: opts.period ?? "7d",
|
||||
});
|
||||
const data = await mcpCall(
|
||||
"omniroute_compression_combo_stats",
|
||||
{ period: opts.period ?? "7d" },
|
||||
() => restComboStats(opts.period)
|
||||
);
|
||||
emit(data, cmd.optsWithGlobals());
|
||||
});
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ function ensureBackup(configPath) {
|
||||
}
|
||||
|
||||
async function runConfigListCommand(opts = {}) {
|
||||
const { detectAllTools } = await import("../../../src/lib/cli-helper/tool-detector.js");
|
||||
const { detectAllTools } = await import("../../../src/lib/cli-helper/tool-detector.ts");
|
||||
const tools = await detectAllTools();
|
||||
|
||||
if (opts.json) {
|
||||
@@ -42,7 +42,7 @@ async function runConfigGetCommand(toolId, opts = {}) {
|
||||
printError("Tool ID required. Usage: omniroute config get <tool>");
|
||||
return 1;
|
||||
}
|
||||
const { detectTool } = await import("../../../src/lib/cli-helper/tool-detector.js");
|
||||
const { detectTool } = await import("../../../src/lib/cli-helper/tool-detector.ts");
|
||||
const tool = await detectTool(toolId);
|
||||
if (!tool) {
|
||||
printError(`Unknown tool: ${toolId}`);
|
||||
|
||||
@@ -416,7 +416,7 @@ export async function collectDoctorChecks(context = {}, options = {}) {
|
||||
|
||||
// CLI tool health checks
|
||||
try {
|
||||
const { collectCliToolChecks } = await import("../../../src/lib/cli-helper/doctor/checks.js");
|
||||
const { collectCliToolChecks } = await import("../../../src/lib/cli-helper/doctor/checks.ts");
|
||||
const cliChecks = await collectCliToolChecks();
|
||||
checks.push(...cliChecks);
|
||||
} catch (err) {
|
||||
|
||||
@@ -54,7 +54,7 @@ export async function runStatusCommand(opts = {}) {
|
||||
|
||||
if (isVerbose || !isJson) {
|
||||
try {
|
||||
const { detectAllTools } = await import("../../../src/lib/cli-helper/tool-detector.js");
|
||||
const { detectAllTools } = await import("../../../src/lib/cli-helper/tool-detector.ts");
|
||||
const tools = await detectAllTools();
|
||||
status.tools = tools.map((t) => ({
|
||||
id: t.id,
|
||||
|
||||
@@ -740,6 +740,8 @@
|
||||
"endpointsSubtitle": "Your AI connection URLs",
|
||||
"apiManager": "API Key Manager",
|
||||
"apiManagerSubtitle": "Manage API keys and access",
|
||||
"embeddedServices": "Embedded Services",
|
||||
"embeddedServicesSubtitle": "Manage local proxy services",
|
||||
"logs": "Logs",
|
||||
"webhooks": "Webhooks",
|
||||
"webhooksSubtitle": "Get notified of events",
|
||||
|
||||
@@ -9,7 +9,7 @@ export interface DoctorCheckResult {
|
||||
}
|
||||
|
||||
export async function collectCliToolChecks(): Promise<DoctorCheckResult[]> {
|
||||
const { detectAllTools } = await import("../tool-detector.js");
|
||||
const { detectAllTools } = await import("../tool-detector.ts");
|
||||
const tools = await detectAllTools();
|
||||
|
||||
return tools.map((tool) => {
|
||||
|
||||
@@ -83,7 +83,12 @@ const OPENAI_LIKE_FORMATS = new Set(["openai", "openai-responses"]);
|
||||
const GEMINI_LIKE_FORMATS = new Set(["gemini", "gemini-cli"]);
|
||||
|
||||
function normalizeBaseUrl(baseUrl: string) {
|
||||
return (baseUrl || "").trim().replace(/\/$/, "");
|
||||
// Guard against a non-string baseUrl reaching .trim() / .replace() — see #2463
|
||||
// where NVIDIA NIM validation surfaced as `e.startsWith is not a function`
|
||||
// after the bundler renamed `baseUrl` to `e`. Any malformed providerSpecificData
|
||||
// (e.g. saved as object from a UI bug) would otherwise crash mid-validation.
|
||||
const value = typeof baseUrl === "string" ? baseUrl : "";
|
||||
return value.trim().replace(/\/$/, "");
|
||||
}
|
||||
|
||||
function normalizeAzureOpenAIBaseUrl(baseUrl: string) {
|
||||
@@ -3585,6 +3590,76 @@ export async function validateProviderApiKey({ provider, apiKey, providerSpecifi
|
||||
return toValidationErrorResult(error);
|
||||
}
|
||||
},
|
||||
// NVIDIA NIM (#2463) — bypass the /models probe in favor of a direct
|
||||
// chat/completions probe. NVIDIA NIM's /models endpoint returns model
|
||||
// catalogs that vary by region and key-tier, and some keys 404 on it,
|
||||
// which the generic flow misreads. The chat probe is also a stronger
|
||||
// sanity check for streaming/key correctness.
|
||||
nvidia: async ({ apiKey, providerSpecificData }: any) => {
|
||||
try {
|
||||
const baseUrlRaw =
|
||||
providerSpecificData?.baseUrl || "https://integrate.api.nvidia.com/v1/chat/completions";
|
||||
const normalized = normalizeBaseUrl(baseUrlRaw);
|
||||
const chatUrl = normalized.endsWith("/chat/completions")
|
||||
? normalized
|
||||
: `${normalized}/chat/completions`;
|
||||
const modelId =
|
||||
providerSpecificData?.validationModelId ||
|
||||
getRegistryEntry("nvidia")?.models?.[0]?.id ||
|
||||
"meta/llama-3.1-8b-instruct";
|
||||
const res = await validationWrite(
|
||||
chatUrl,
|
||||
{
|
||||
method: "POST",
|
||||
headers: buildBearerHeaders(apiKey, providerSpecificData),
|
||||
body: JSON.stringify({
|
||||
model: modelId,
|
||||
messages: [{ role: "user", content: "test" }],
|
||||
max_tokens: 1,
|
||||
}),
|
||||
},
|
||||
isLocal
|
||||
);
|
||||
if (res.status === 401 || res.status === 403) {
|
||||
return { valid: false, error: "Invalid API key" };
|
||||
}
|
||||
// Any non-auth response (200, 400, 422, 429) means auth passed
|
||||
return { valid: true, error: null };
|
||||
} catch (error: any) {
|
||||
return toValidationErrorResult(error);
|
||||
}
|
||||
},
|
||||
// Poolside (#2723) — API has no /v1/models endpoint and returns 401 from
|
||||
// unknown routes, which the generic /models probe misreads as "invalid API key".
|
||||
// Validate via direct chat/completions probe with a minimal body.
|
||||
poolside: async ({ apiKey, providerSpecificData }: any) => {
|
||||
try {
|
||||
const baseUrl = normalizeBaseUrl(
|
||||
providerSpecificData?.baseUrl || "https://api.poolside.ai/v1"
|
||||
);
|
||||
const chatUrl = `${baseUrl.replace(/\/chat\/completions$/, "")}/chat/completions`;
|
||||
const res = await validationWrite(
|
||||
chatUrl,
|
||||
{
|
||||
method: "POST",
|
||||
headers: buildBearerHeaders(apiKey, providerSpecificData),
|
||||
body: JSON.stringify({
|
||||
model: "poolside-model",
|
||||
messages: [{ role: "user", content: "test" }],
|
||||
max_tokens: 1,
|
||||
}),
|
||||
},
|
||||
isLocal
|
||||
);
|
||||
if (res.status === 401 || res.status === 403) {
|
||||
return { valid: false, error: "Invalid API key" };
|
||||
}
|
||||
// Any non-auth response (200, 400, 422, 429) means auth passed
|
||||
return { valid: true, error: null };
|
||||
} catch (error: any) {
|
||||
return toValidationErrorResult(error);
|
||||
}
|
||||
},
|
||||
// Xiaomi MiMo — Token Plan keys (tp-*) only work on regional endpoints
|
||||
// (e.g. token-plan-sgp, token-plan-ams), not api.xiaomimimo.com.
|
||||
// /v1/models works but validate via chat/completions for stronger auth check.
|
||||
|
||||
@@ -125,20 +125,35 @@ export function parseAndValidatePublicUrl(input: string | URL) {
|
||||
return url;
|
||||
}
|
||||
|
||||
function isTrueValue(raw: unknown): boolean {
|
||||
if (typeof raw !== "string") return false;
|
||||
return TRUE_ENV_VALUES.has(raw.trim().toLowerCase());
|
||||
}
|
||||
|
||||
export function arePrivateProviderUrlsAllowed() {
|
||||
const value = process.env[PRIVATE_PROVIDER_URLS_ENV];
|
||||
if (value && TRUE_ENV_VALUES.has(value.trim().toLowerCase())) return true;
|
||||
|
||||
const legacyValue = process.env["OUTBOUND_SSRF_GUARD_ENABLED"];
|
||||
if (legacyValue && ["false", "0", "no", "off"].includes(legacyValue.trim().toLowerCase()))
|
||||
return true;
|
||||
|
||||
// Check feature flag DB override — supports runtime toggle without restart
|
||||
// 1) DB override takes precedence — it represents an explicit user toggle in
|
||||
// the dashboard ("Allow Private Provider URLs"). This is critical for the
|
||||
// Electron build (#2575) where the server is spawned with the env value
|
||||
// captured at boot, so subsequent UI toggles only land in the DB and the
|
||||
// env-first ordering would otherwise mask them.
|
||||
try {
|
||||
const dbValue = resolveFeatureFlag(PRIVATE_PROVIDER_URLS_ENV);
|
||||
if (dbValue && TRUE_ENV_VALUES.has(dbValue.trim().toLowerCase())) return true;
|
||||
if (isTrueValue(dbValue)) return true;
|
||||
} catch {
|
||||
// DB not initialized yet — fall back to env-only check
|
||||
// DB not initialized yet — fall through to env-only check.
|
||||
}
|
||||
|
||||
// 2) Explicit env opt-in (for headless/Docker users who set it before boot).
|
||||
if (isTrueValue(process.env[PRIVATE_PROVIDER_URLS_ENV])) return true;
|
||||
|
||||
// 3) Legacy escape hatch — disabling the outbound guard implies allowing
|
||||
// private URLs.
|
||||
const legacyValue = process.env["OUTBOUND_SSRF_GUARD_ENABLED"];
|
||||
if (
|
||||
typeof legacyValue === "string" &&
|
||||
["false", "0", "no", "off"].includes(legacyValue.trim().toLowerCase())
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
@@ -164,3 +164,87 @@ test("compression.mjs pode ser importado sem erro", async () => {
|
||||
assert.equal(typeof mod.runCompressionEngineSet, "function");
|
||||
assert.equal(typeof mod.runCompressionPreview, "function");
|
||||
});
|
||||
|
||||
// #2688 — when /api/mcp/tools/call 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) => {
|
||||
callOrder.push(url);
|
||||
if (url.includes("/api/mcp/tools/call")) {
|
||||
return Promise.resolve(makeResp({ error: "not mounted" }, 404));
|
||||
}
|
||||
if (url.includes("/api/settings/compression")) {
|
||||
return Promise.resolve(makeResp({ engine: "caveman", enabled: true }));
|
||||
}
|
||||
if (url.includes("/api/context/combos")) {
|
||||
return Promise.resolve(makeResp({ combos: [{ id: "c1", name: "x" }] }));
|
||||
}
|
||||
if (url.includes("/api/context/analytics")) {
|
||||
return Promise.resolve(makeResp({ savings: 12 }));
|
||||
}
|
||||
return Promise.resolve(makeResp({}, 404));
|
||||
}) as any;
|
||||
|
||||
const { runCompressionStatus } = await import("../../bin/cli/commands/compression.mjs");
|
||||
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");
|
||||
});
|
||||
|
||||
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 origFetch = globalThis.fetch;
|
||||
globalThis.fetch = ((url: string, opts: any) => {
|
||||
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));
|
||||
}
|
||||
return Promise.resolve(makeResp({ ok: true }));
|
||||
}) as any;
|
||||
|
||||
await captureStdout(async () => {
|
||||
const { runCompressionEngineSet } = await import("../../bin/cli/commands/compression.mjs");
|
||||
await runCompressionEngineSet("rtk", {}, makeCmd() as any);
|
||||
});
|
||||
|
||||
globalThis.fetch = origFetch;
|
||||
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");
|
||||
assert.equal(restCall?.body?.engine, "rtk");
|
||||
});
|
||||
|
||||
45
tests/unit/cli-tool-detector-imports.test.ts
Normal file
45
tests/unit/cli-tool-detector-imports.test.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync, existsSync } from "node:fs";
|
||||
import { join, dirname } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..", "..");
|
||||
|
||||
// #2509 — published npm builds were failing with `Cannot find module
|
||||
// 'src/lib/cli-helper/tool-detector.js'` because CLI commands imported the
|
||||
// helper via the `.js` extension while only the `.ts` source ships. Lock the
|
||||
// import paths to the explicit `.ts` extension that tsx resolves directly,
|
||||
// preventing future regressions.
|
||||
|
||||
const FILES_THAT_MUST_USE_TS_EXTENSION = [
|
||||
"bin/cli/commands/config.mjs",
|
||||
"bin/cli/commands/status.mjs",
|
||||
"bin/cli/commands/doctor.mjs",
|
||||
"src/lib/cli-helper/doctor/checks.ts",
|
||||
];
|
||||
|
||||
const DISALLOWED_PATTERNS = [
|
||||
"cli-helper/tool-detector.js",
|
||||
"cli-helper/doctor/checks.js",
|
||||
];
|
||||
|
||||
for (const file of FILES_THAT_MUST_USE_TS_EXTENSION) {
|
||||
test(`${file} imports cli-helper modules with .ts extension (not .js)`, () => {
|
||||
const abs = join(ROOT, file);
|
||||
assert.ok(existsSync(abs), `${file} should exist`);
|
||||
const content = readFileSync(abs, "utf8");
|
||||
for (const bad of DISALLOWED_PATTERNS) {
|
||||
assert.ok(
|
||||
!content.includes(bad),
|
||||
`${file} must not import via "${bad}" — use the .ts extension so the published npm package (which ships only .ts source) can resolve via tsx. See #2509.`
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
test("tool-detector.ts is reachable from dynamic import at runtime", async () => {
|
||||
const mod = await import("../../src/lib/cli-helper/tool-detector.ts");
|
||||
assert.equal(typeof mod.detectAllTools, "function");
|
||||
assert.equal(typeof mod.detectTool, "function");
|
||||
});
|
||||
99
tests/unit/nvidia-nim-validator.test.ts
Normal file
99
tests/unit/nvidia-nim-validator.test.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
// #2463 — NVIDIA NIM validation must not crash with `e.startsWith is not a function`
|
||||
// when providerSpecificData has malformed shapes; and the validation must use a
|
||||
// direct chat probe instead of the /models probe.
|
||||
|
||||
test("normalizeBaseUrl tolerates non-string baseUrl without throwing", async () => {
|
||||
// Indirect probe — call validation entrypoint with a non-string baseUrl in PSD;
|
||||
// the function should return a normal Validation result (not throw a TypeError
|
||||
// such as `e.startsWith is not a function` after minification — see #2463).
|
||||
const origFetch = globalThis.fetch;
|
||||
globalThis.fetch = (async () =>
|
||||
new Response(JSON.stringify({}), {
|
||||
status: 400,
|
||||
headers: { "content-type": "application/json" },
|
||||
})) as typeof fetch;
|
||||
|
||||
try {
|
||||
const { validateProviderApiKey } = await import("../../src/lib/providers/validation.ts");
|
||||
const result = await validateProviderApiKey({
|
||||
provider: "nvidia",
|
||||
apiKey: "nv-test-key",
|
||||
// Malformed: baseUrl is an object instead of a string. Pre-fix this would
|
||||
// crash inside normalizeBaseUrl with `.trim is not a function`.
|
||||
providerSpecificData: { baseUrl: { not: "a string" } as any },
|
||||
});
|
||||
assert.equal(typeof result, "object");
|
||||
assert.equal(typeof result.valid, "boolean");
|
||||
// Whether the call succeeds (with default baseUrl) or fails gracefully with
|
||||
// an outbound URL guard error is fine — the contract is "no TypeError leak".
|
||||
if (!result.valid && typeof result.error === "string") {
|
||||
assert.ok(
|
||||
!result.error.includes("startsWith"),
|
||||
`error must not mention startsWith TypeError, got: ${result.error}`
|
||||
);
|
||||
assert.ok(
|
||||
!result.error.includes("is not a function"),
|
||||
`error must not mention TypeError, got: ${result.error}`
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
globalThis.fetch = origFetch;
|
||||
}
|
||||
});
|
||||
|
||||
test("nvidia specialty validator returns Invalid API key on 401", async () => {
|
||||
const origFetch = globalThis.fetch;
|
||||
globalThis.fetch = (async () =>
|
||||
new Response(JSON.stringify({ error: "unauthorized" }), {
|
||||
status: 401,
|
||||
headers: { "content-type": "application/json" },
|
||||
})) as typeof fetch;
|
||||
|
||||
try {
|
||||
const { validateProviderApiKey } = await import("../../src/lib/providers/validation.ts");
|
||||
const result = await validateProviderApiKey({
|
||||
provider: "nvidia",
|
||||
apiKey: "nv-badkey",
|
||||
providerSpecificData: {},
|
||||
});
|
||||
assert.equal(result.valid, false);
|
||||
assert.equal(result.error, "Invalid API key");
|
||||
} finally {
|
||||
globalThis.fetch = origFetch;
|
||||
}
|
||||
});
|
||||
|
||||
test("nvidia specialty validator skips /models probe entirely", async () => {
|
||||
const calls: string[] = [];
|
||||
const origFetch = globalThis.fetch;
|
||||
globalThis.fetch = (async (url: any) => {
|
||||
calls.push(String(url));
|
||||
return new Response(JSON.stringify({}), {
|
||||
status: 200,
|
||||
headers: { "content-type": "application/json" },
|
||||
});
|
||||
}) as typeof fetch;
|
||||
|
||||
try {
|
||||
const { validateProviderApiKey } = await import("../../src/lib/providers/validation.ts");
|
||||
const result = await validateProviderApiKey({
|
||||
provider: "nvidia",
|
||||
apiKey: "nv-key",
|
||||
providerSpecificData: {},
|
||||
});
|
||||
assert.equal(result.valid, true);
|
||||
assert.ok(
|
||||
calls.every((u) => !u.endsWith("/v1/models")),
|
||||
`should not call /v1/models, called: ${JSON.stringify(calls)}`
|
||||
);
|
||||
assert.ok(
|
||||
calls.some((u) => u.endsWith("/chat/completions")),
|
||||
`should call /chat/completions, called: ${JSON.stringify(calls)}`
|
||||
);
|
||||
} finally {
|
||||
globalThis.fetch = origFetch;
|
||||
}
|
||||
});
|
||||
@@ -57,11 +57,11 @@ const EXPECTED_PROVIDER_KEYS = [
|
||||
"kiro",
|
||||
"amazon-q",
|
||||
"cursor",
|
||||
"trae",
|
||||
"kilocode",
|
||||
"cline",
|
||||
"windsurf",
|
||||
"devin-cli",
|
||||
"trae",
|
||||
];
|
||||
|
||||
const EXPECTED_CONFIG_BY_PROVIDER = {
|
||||
|
||||
85
tests/unit/outbound-url-guard-feature-flag.test.ts
Normal file
85
tests/unit/outbound-url-guard-feature-flag.test.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
// #2575 — the DB feature flag override must take precedence over the
|
||||
// process environment, otherwise toggling "Allow Private Provider URLs" in
|
||||
// the Electron dashboard has no effect (the server was spawned with the env
|
||||
// value at boot and the subsequent toggle only lands in the DB).
|
||||
|
||||
const KEY = "OMNIROUTE_ALLOW_PRIVATE_PROVIDER_URLS";
|
||||
|
||||
async function withEnv<T>(value: string | undefined, fn: () => Promise<T> | T): Promise<T> {
|
||||
const prev = process.env[KEY];
|
||||
if (value === undefined) delete process.env[KEY];
|
||||
else process.env[KEY] = value;
|
||||
try {
|
||||
return await fn();
|
||||
} finally {
|
||||
if (prev === undefined) delete process.env[KEY];
|
||||
else process.env[KEY] = prev;
|
||||
}
|
||||
}
|
||||
|
||||
async function withDbOverride<T>(value: string | undefined, fn: () => Promise<T> | T): Promise<T> {
|
||||
const { setFeatureFlagOverride, removeFeatureFlagOverride } = await import(
|
||||
"../../src/lib/db/featureFlags.ts"
|
||||
);
|
||||
if (value === undefined) {
|
||||
removeFeatureFlagOverride(KEY);
|
||||
} else {
|
||||
setFeatureFlagOverride(KEY, value);
|
||||
}
|
||||
try {
|
||||
return await fn();
|
||||
} finally {
|
||||
removeFeatureFlagOverride(KEY);
|
||||
}
|
||||
}
|
||||
|
||||
test("arePrivateProviderUrlsAllowed honors DB override = 'true' even when env is 'false'", async () => {
|
||||
await withEnv("false", async () => {
|
||||
await withDbOverride("true", async () => {
|
||||
const { arePrivateProviderUrlsAllowed } = await import(
|
||||
"../../src/shared/network/outboundUrlGuard.ts"
|
||||
);
|
||||
assert.equal(
|
||||
arePrivateProviderUrlsAllowed(),
|
||||
true,
|
||||
"DB toggle must override stale env value"
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test("arePrivateProviderUrlsAllowed returns false when DB override = 'false' and no env truthy", async () => {
|
||||
await withEnv(undefined, async () => {
|
||||
await withDbOverride("false", async () => {
|
||||
const { arePrivateProviderUrlsAllowed } = await import(
|
||||
"../../src/shared/network/outboundUrlGuard.ts"
|
||||
);
|
||||
assert.equal(arePrivateProviderUrlsAllowed(), false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test("arePrivateProviderUrlsAllowed honors env = 'true' when DB has no override", async () => {
|
||||
await withEnv("true", async () => {
|
||||
await withDbOverride(undefined, async () => {
|
||||
const { arePrivateProviderUrlsAllowed } = await import(
|
||||
"../../src/shared/network/outboundUrlGuard.ts"
|
||||
);
|
||||
assert.equal(arePrivateProviderUrlsAllowed(), true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test("arePrivateProviderUrlsAllowed default (no env, no DB) returns false", async () => {
|
||||
await withEnv(undefined, async () => {
|
||||
await withDbOverride(undefined, async () => {
|
||||
const { arePrivateProviderUrlsAllowed } = await import(
|
||||
"../../src/shared/network/outboundUrlGuard.ts"
|
||||
);
|
||||
assert.equal(arePrivateProviderUrlsAllowed(), false);
|
||||
});
|
||||
});
|
||||
});
|
||||
79
tests/unit/poolside-provider.test.ts
Normal file
79
tests/unit/poolside-provider.test.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import { REGISTRY } from "../../open-sse/config/providerRegistry.ts";
|
||||
import { DefaultExecutor } from "../../open-sse/executors/default.ts";
|
||||
|
||||
test("poolside registry uses /v1/chat/completions baseUrl consumed directly by default executor", () => {
|
||||
const entry = REGISTRY.poolside;
|
||||
|
||||
assert.ok(entry, "poolside should exist in registry");
|
||||
assert.equal(entry.baseUrl, "https://api.poolside.ai/v1/chat/completions");
|
||||
assert.equal(entry.format, "openai");
|
||||
assert.equal(entry.authType, "apikey");
|
||||
assert.equal(entry.authHeader, "bearer");
|
||||
});
|
||||
|
||||
test("poolside default executor returns the chat endpoint directly", () => {
|
||||
const executor = new DefaultExecutor("poolside");
|
||||
|
||||
assert.equal(
|
||||
executor.buildUrl("poolside-model", true, 0, {}),
|
||||
"https://api.poolside.ai/v1/chat/completions"
|
||||
);
|
||||
});
|
||||
|
||||
test("poolside specialty validator returns valid=true on non-auth chat probe responses", async () => {
|
||||
const calls: Array<{ url: string; status: number }> = [];
|
||||
const originalFetch = globalThis.fetch;
|
||||
globalThis.fetch = (async (url: any, init: any) => {
|
||||
void init;
|
||||
const u = String(url);
|
||||
calls.push({ url: u, status: 400 });
|
||||
// Poolside returns 400 for minimal probe — that means auth passed
|
||||
return new Response(JSON.stringify({ error: { message: "invalid model" } }), {
|
||||
status: 400,
|
||||
headers: { "content-type": "application/json" },
|
||||
});
|
||||
}) as typeof fetch;
|
||||
|
||||
try {
|
||||
const { validateProviderApiKey } = await import("../../src/lib/providers/validation.ts");
|
||||
const result = await validateProviderApiKey({
|
||||
provider: "poolside",
|
||||
apiKey: "sky_validkey",
|
||||
providerSpecificData: {},
|
||||
});
|
||||
assert.equal(result.valid, true);
|
||||
assert.equal(result.error, null);
|
||||
// Should hit /chat/completions only — no /models probe
|
||||
assert.ok(
|
||||
calls.every((c) => c.url.endsWith("/chat/completions")),
|
||||
`expected only /chat/completions probes, got ${JSON.stringify(calls.map((c) => c.url))}`
|
||||
);
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
|
||||
test("poolside specialty validator returns Invalid API key on 401", async () => {
|
||||
const originalFetch = globalThis.fetch;
|
||||
globalThis.fetch = (async () =>
|
||||
new Response(JSON.stringify({ error: "unauthorized" }), {
|
||||
status: 401,
|
||||
headers: { "content-type": "application/json" },
|
||||
})) as typeof fetch;
|
||||
|
||||
try {
|
||||
const { validateProviderApiKey } = await import("../../src/lib/providers/validation.ts");
|
||||
const result = await validateProviderApiKey({
|
||||
provider: "poolside",
|
||||
apiKey: "sky_badkey",
|
||||
providerSpecificData: {},
|
||||
});
|
||||
assert.equal(result.valid, false);
|
||||
assert.equal(result.error, "Invalid API key");
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
@@ -306,7 +306,7 @@ test("handleResponsesCore rejects invalid Responses API input that cannot be tra
|
||||
}),
|
||||
(error) =>
|
||||
error instanceof Error &&
|
||||
error.message.includes("web_search_preview tool type is not supported")
|
||||
error.message.includes("file_search tool type is not supported")
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
|
||||
test("resolveUniversalHandoffConfig returns disabled defaults when no config", () => {
|
||||
const r = resolveUniversalHandoffConfig(null, null);
|
||||
assert.strictEqual(r.enabled, false);
|
||||
assert.strictEqual(r.enabled, true);
|
||||
assert.strictEqual(r.trigger, "on-switch");
|
||||
assert.strictEqual(r.ttlMinutes, 300);
|
||||
assert.strictEqual(r.maxMessagesForSummary, 30);
|
||||
|
||||
Reference in New Issue
Block a user