Merge remote-tracking branch 'origin/release/v3.8.50' into fix/release-v3.8.50-basered-stream-dup-import

This commit is contained in:
Xiangzhe
2026-08-14 01:03:10 -03:00
145 changed files with 3582 additions and 1426 deletions

View File

@@ -595,11 +595,10 @@ test("chat pipeline persists Codex responses cache and reasoning tokens to call
assert.equal(callLog.tokens.reasoning, 13);
});
test("chat pipeline applies global Codex priority service tier inside combos", async () => {
await seedConnection("codex", { apiKey: "sk-codex-combo-priority" });
await settingsDb.updateSettings({
codexServiceTier: { enabled: true, tier: "priority" },
});
test("chat pipeline applies Codex OAuth fingerprint and priority tier inside combos", async () => {
setCliCompatProviders(["codex"]);
await seedConnection("codex", { authType: "oauth", accessToken: "codex-combo-oauth-token" });
await settingsDb.updateSettings({ codexServiceTier: { enabled: true, tier: "priority" } });
await combosDb.createCombo({
name: "codex-priority-combo",
strategy: "priority",
@@ -607,10 +606,8 @@ test("chat pipeline applies global Codex priority service tier inside combos", a
models: ["codex/gpt-5.5"],
});
const fetchCalls = [];
globalThis.fetch = async (url, init: RequestInit = {}) => {
globalThis.fetch = async (_url, init: RequestInit = {}) => {
fetchCalls.push({
url: String(url),
headers: toPlainHeaders(init.headers),
body: init.body ? JSON.parse(String(init.body)) : null,
});
@@ -619,21 +616,24 @@ test("chat pipeline applies global Codex priority service tier inside combos", a
const response = await handleChat(
buildRequest({
url: "http://localhost/v1/responses",
headers: { "session-id": "combo-client-session" },
body: {
model: "codex-priority-combo",
stream: false,
messages: [{ role: "user", content: "Use Codex combo priority" }],
input: "Use Codex combo priority",
},
})
);
const json = (await response.json()) as any;
assert.equal(response.status, 200);
assert.equal(json.object, "response");
assert.equal(fetchCalls.length, 1);
assert.match(fetchCalls[0].url, /\/responses$/);
assert.equal(fetchCalls[0].headers.Authorization, "Bearer sk-codex-combo-priority");
assert.equal(fetchCalls[0].body.service_tier, "priority");
assert.equal(json.choices[0].message.content, "combo priority ok");
const [call] = fetchCalls;
assert.equal(call.headers.Authorization, "Bearer codex-combo-oauth-token");
assert.notEqual(call.headers["session-id"], "combo-client-session");
assert.equal(call.headers["session-id"], call.body.client_metadata.session_id);
assert.equal(call.body.service_tier, "priority");
});
test("chat pipeline applies Codex CLI fingerprint to OAuth responses requests", async () => {
@@ -696,7 +696,11 @@ test("chat pipeline applies Codex CLI fingerprint to OAuth responses requests",
call.headers["User-Agent"],
`codex-cli/${getCodexClientVersion()} (Windows 10.0.26200; x64)`
);
assert.equal(call.headers["x-codex-window-id"], "conv_codex_fingerprint:0");
// Session convergence derives a fresh session/thread id instead of passing the
// client's raw conversation_id straight through, so the window id must be derived
// from the (converged) request id header, not the original client-supplied literal.
assert.notEqual(call.headers["session_id"], "conv_codex_fingerprint");
assert.equal(call.headers["x-codex-window-id"], `${call.headers["x-client-request-id"]}:0`);
assert.ok(call.headers["x-client-request-id"], "expected Codex request id header");
assert.ok(call.headers["x-codex-turn-metadata"], "expected Codex turn metadata header");

View File

@@ -62,6 +62,18 @@ test("invalid JSON → invalid_json with short message + detailed error", async
assert.equal(res.looksLikeSSE, false);
});
test("valid JSON with a non-object root → invalid_json", async () => {
for (const body of ["null", '"text"', "[]"]) {
const res = await parseNonStreamingResponseBody({
...baseOpts,
providerResponse: makeResponse(body, "application/json"),
});
assert.equal(res.kind, "invalid_json");
if (res.kind !== "invalid_json") continue;
assert.match(res.detailedError, /expected an object payload/);
}
});
test("valid SSE payload (by content-type) → ok with SSE-derived format", async () => {
const sse =
'data: {"id":"c1","object":"chat.completion.chunk","choices":[{"delta":{"content":"hello"},"index":0,"finish_reason":null}]}\n\n' +

View File

@@ -528,6 +528,43 @@ test("chatCore honors providerSpecificData.apiType for legacy openai-compatible
assert.equal("messages" in call.body, false);
assert.equal(payload.choices[0].message.content, "ok");
});
test("chatCore translates a streaming Responses upstream for a Chat client", async () => {
const { call, result } = await invokeChatCore({
provider: "openai-compatible-sp-openai",
model: "gpt-5.4",
endpoint: "/v1/chat/completions",
accept: "text/event-stream",
credentials: {
apiKey: "sk-test",
providerSpecificData: {
apiType: "responses",
baseUrl: "https://proxy.example.com/v1",
prefix: "sp-openai",
},
},
body: {
model: "gpt-5.4",
stream: true,
messages: [{ role: "user", content: "Reply with OK only." }],
},
responseFactory: () =>
new Response(
[
'data: {"type":"response.output_text.delta","delta":"ok"}',
"",
'data: {"type":"response.completed","response":{"id":"resp_stream","status":"completed","output":[],"usage":{"input_tokens":1,"output_tokens":1,"total_tokens":2}}}',
"",
].join("\n"),
{ status: 200, headers: { "Content-Type": "text/event-stream" } }
),
});
assert.equal(result.success, true);
assert.match(call.url, /\/responses$/);
const streamed = await result.response.text();
assert.match(streamed, /"content":"ok"/);
assert.match(streamed, /data: \[DONE\]/);
});
test("chatCore applies Responses input policy to openai-compatible targets", async () => {
const reasoningItems = [
{ id: "rs_valid", type: "reasoning", encrypted_content: "encrypted-blob" },

View File

@@ -49,3 +49,11 @@ test("normalizeExecutorResult wraps bare Response and passes through rich result
assert.equal(rich.url, "u");
assert.equal(rich.headers.a, "b");
});
test("normalizeExecutorResult rejects malformed executor output", () => {
assert.throws(() => normalizeExecutorResult({}), /must contain a Response/);
assert.throws(
() => normalizeExecutorResult({ response: "not-a-response" }),
/must contain a Response/
);
});

View File

@@ -2,6 +2,7 @@ import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import path from "node:path";
import { parseEnvValue } from "../../bin/cli/utils/parseEnvValue.mjs";
// #10100 — the .env loader kept inline comments inside values, so the shipped
// `QUOTA_STORE_DRIVER=sqlite # sqlite | redis` line produced the literal value
@@ -9,36 +10,6 @@ import path from "node:path";
// user annotating `QUOTA_STORE_DRIVER=redis # ...` silently got SQLite with no
// warning (the existing warning lives inside the `redis` branch).
const LOADER = path.resolve("bin/omniroute.mjs");
/**
* The loader is a CLI entrypoint with side effects on import, so exercise the
* pure helper by extracting it from source rather than importing the module.
*/
function loadParseEnvValue(): (raw: string) => string {
const source = fs.readFileSync(LOADER, "utf8");
const start = source.indexOf("function parseEnvValue(");
assert.ok(start > -1, "parseEnvValue should exist in bin/omniroute.mjs");
// Walk to the end of the function body.
let depth = 0;
let end = start;
for (let i = source.indexOf("{", start); i < source.length; i++) {
if (source[i] === "{") depth++;
else if (source[i] === "}") {
depth--;
if (depth === 0) {
end = i + 1;
break;
}
}
}
return new Function(`${source.slice(start, end)}; return parseEnvValue;`)() as (
raw: string
) => string;
}
const parseEnvValue = loadParseEnvValue();
test("an unquoted inline comment is stripped", () => {
assert.equal(parseEnvValue("sqlite # sqlite | redis"), "sqlite");
assert.equal(parseEnvValue("redis # sqlite | redis"), "redis");

View File

@@ -2,6 +2,7 @@ import { describe, it, mock } from "node:test";
import assert from "node:assert";
import fs, { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { parse } from "jsonc-parser";
import * as generator from "../../../src/lib/cli-helper/config-generator/index.ts";
// The UI's HERMES_ROLES catalog (HermesAgentToolCard.tsx) is a "use client" component
@@ -261,7 +262,7 @@ describe("config-generator", () => {
{ role: "delegation", model: "claude-3-5-sonnet" },
{ role: "vision", model: "gpt-4o" },
],
});
} as any);
assert.ok(!result.error);
assert.ok(typeof result.yaml === "string");
@@ -291,7 +292,7 @@ describe("config-generator", () => {
const result = await hermesAgent.generateHermesAgentConfig({
baseUrl: "",
selections: [{ role: "default", model: "x" }],
} as any);
});
assert.ok(result.error);
assert.ok(result.error.includes("baseUrl"));
@@ -513,9 +514,8 @@ describe("config-generator", () => {
])
);
try {
const { generateOpencodeConfig } = await import(
"../../../src/lib/cli-helper/config-generator/opencode.ts"
);
const { generateOpencodeConfig } =
await import("../../../src/lib/cli-helper/config-generator/opencode.ts");
const out = await generateOpencodeConfig({
baseUrl: "http://localhost:20128",
apiKey: "sk-test",
@@ -653,5 +653,70 @@ describe("config-generator", () => {
mock.restoreAll();
}
});
it("loads comments and trailing commas from opencode.jsonc and returns its real path (#10227)", async () => {
const existingJsonc = `{
// preserve this native OpenCode file instead of ignoring it
"$schema": "https://opencode.ai/config.json",
"provider": {
"custom": {
// keep comments inside unrelated providers too
"name": "Custom Provider"
},
"omniroute": {
"models": {
"manual-model": { "name": "Manual", "limit": { "context": 77777, }, },
},
},
},
}\n`;
let readPath = "";
mock.method(fs, "existsSync", (candidate) => String(candidate).endsWith("opencode.jsonc"));
mock.method(fs, "readFileSync", (candidate) => {
readPath = String(candidate);
return existingJsonc;
});
const stub = stubFetchOnce(
makeCatalogResponse([{ id: "manual-model", context_length: 131072 }])
);
try {
const result = await generator.generateConfig("opencode", {
baseUrl: "http://localhost:20128",
apiKey: "sk-test",
});
assert.strictEqual(result.success, true);
assert.match(result.configPath, /opencode\.jsonc$/);
assert.strictEqual(readPath, result.configPath);
assert.match(result.content || "", /preserve this native OpenCode file/);
assert.match(result.content || "", /keep comments inside unrelated providers too/);
const config = parse(result.content || "");
assert.deepStrictEqual(config.provider.custom, { name: "Custom Provider" });
assert.strictEqual(config.provider.omniroute.models["manual-model"].limit.context, 77777);
} finally {
stub.restore();
mock.restoreAll();
}
});
it("refuses to replace an invalid existing opencode.jsonc (#10227)", async () => {
mock.method(fs, "existsSync", (candidate) => String(candidate).endsWith("opencode.jsonc"));
mock.method(fs, "readFileSync", () => "{ invalid jsonc");
const stub = stubFetchOnce(makeCatalogResponse([{ id: "catalog-model", context_length: 8 }]));
try {
const result = await generator.generateConfig("opencode", {
baseUrl: "http://localhost:20128",
apiKey: "sk-test",
});
assert.strictEqual(result.success, false);
assert.match(result.error || "", /invalid.*JSONC|refus/i);
} finally {
stub.restore();
mock.restoreAll();
}
});
});
});

View File

@@ -0,0 +1,41 @@
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";
import * as toolDetector from "../../../src/lib/cli-helper/tool-detector.ts";
test("detectTool reports an existing opencode.jsonc as the real config path (#10227)", async () => {
const xdgRoot = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-detector-jsonc-"));
const configDir = path.join(xdgRoot, "opencode");
const configPath = path.join(configDir, "opencode.jsonc");
const previousXdg = process.env.XDG_CONFIG_HOME;
toolDetector.__setExecFileImpl(async () => ({ stdout: "v1.0.0\n", stderr: "" }));
try {
fs.mkdirSync(configDir, { recursive: true });
fs.writeFileSync(
configPath,
`{
// OpenCode accepts JSONC
"provider": {
"omniroute": { "options": { "baseURL": "http://localhost:20128/v1" } },
},
}\n`
);
process.env.XDG_CONFIG_HOME = xdgRoot;
const result = await toolDetector.detectTool("opencode");
assert.ok(result !== null);
assert.equal(result.configPath, configPath);
assert.equal(result.configured, true);
assert.match(result.configContents || "", /OpenCode accepts JSONC/);
} finally {
if (previousXdg === undefined) delete process.env.XDG_CONFIG_HOME;
else process.env.XDG_CONFIG_HOME = previousXdg;
fs.rmSync(xdgRoot, { recursive: true, force: true });
}
});

View File

@@ -322,4 +322,38 @@ describe("resolveOpencodeConfigPath — cross-platform", () => {
);
assert.equal(result, path.join("D:\\xdg", "opencode", "opencode.json"));
});
it("selects an existing opencode.jsonc instead of inventing opencode.json (#10227)", () => {
const xdgRoot = createTempDir();
const configDir = path.join(xdgRoot, "opencode");
const jsoncPath = path.join(configDir, "opencode.jsonc");
fs.mkdirSync(configDir, { recursive: true });
fs.writeFileSync(jsoncPath, "{\n // native OpenCode config\n}\n");
const result = resolveOpencodeConfigPathFn(
process.platform,
{ XDG_CONFIG_HOME: xdgRoot },
os.homedir()
);
assert.equal(result, jsoncPath);
});
it("prefers opencode.jsonc when both native filenames exist (#10227)", () => {
const xdgRoot = createTempDir();
const configDir = path.join(xdgRoot, "opencode");
const jsonPath = path.join(configDir, "opencode.json");
const jsoncPath = path.join(configDir, "opencode.jsonc");
fs.mkdirSync(configDir, { recursive: true });
fs.writeFileSync(jsonPath, "{}\n");
fs.writeFileSync(jsoncPath, "{}\n");
const result = resolveOpencodeConfigPathFn(
process.platform,
{ XDG_CONFIG_HOME: xdgRoot },
os.homedir()
);
assert.equal(result, jsoncPath);
});
});

View File

@@ -0,0 +1,139 @@
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { SignJWT } from "jose";
import { parse } from "jsonc-parser";
const originalDataDir = process.env.DATA_DIR;
const databaseRoot = await fs.mkdtemp(path.join(os.tmpdir(), "omniroute-apply-jsonc-db-"));
process.env.DATA_DIR = databaseRoot;
const applyRoute = await import("../../src/app/api/cli-tools/apply/route.ts");
const originalFetch = globalThis.fetch;
const originalJwtSecret = process.env.JWT_SECRET;
const originalApiKeySecret = process.env.API_KEY_SECRET;
const originalXdg = process.env.XDG_CONFIG_HOME;
const testRoots = new Set<string>();
async function createAuthCookie(): Promise<string> {
process.env.JWT_SECRET = "test-cli-tools-apply-secret";
const secret = new TextEncoder().encode(process.env.JWT_SECRET);
const token = await new SignJWT({ sub: "test-user" })
.setProtectedHeader({ alg: "HS256" })
.setIssuedAt()
.setExpirationTime("1h")
.sign(secret);
return `auth_token=${token}`;
}
async function postApply(): Promise<Response> {
const cookie = await createAuthCookie();
return applyRoute.POST(
new Request("http://localhost/api/cli-tools/apply", {
method: "POST",
headers: { "Content-Type": "application/json", cookie },
body: JSON.stringify({
toolId: "opencode",
baseUrl: "http://localhost:20128",
apiKey: "sk-test",
model: "catalog-model",
}),
})
);
}
test.beforeEach(async () => {
const root = await fs.mkdtemp(path.join(os.tmpdir(), "omniroute-apply-jsonc-"));
testRoots.add(root);
process.env.XDG_CONFIG_HOME = root;
process.env.API_KEY_SECRET = "test-secret";
globalThis.fetch = async () =>
new Response(
JSON.stringify({
data: [
{
id: "catalog-model",
context_length: 131072,
max_output_tokens: 8192,
},
],
}),
{ status: 200, headers: { "content-type": "application/json" } }
);
});
test.afterEach(async () => {
globalThis.fetch = originalFetch;
if (originalJwtSecret === undefined) delete process.env.JWT_SECRET;
else process.env.JWT_SECRET = originalJwtSecret;
if (originalApiKeySecret === undefined) delete process.env.API_KEY_SECRET;
else process.env.API_KEY_SECRET = originalApiKeySecret;
if (originalXdg === undefined) delete process.env.XDG_CONFIG_HOME;
else process.env.XDG_CONFIG_HOME = originalXdg;
for (const root of testRoots) await fs.rm(root, { recursive: true, force: true });
testRoots.clear();
});
test.after(async () => {
if (originalDataDir === undefined) delete process.env.DATA_DIR;
else process.env.DATA_DIR = originalDataDir;
await fs.rm(databaseRoot, { recursive: true, force: true });
});
test("apply writes back to the selected opencode.jsonc and does not create opencode.json (#10227)", async () => {
const configDir = path.join(process.env.XDG_CONFIG_HOME!, "opencode");
const jsoncPath = path.join(configDir, "opencode.jsonc");
const jsonPath = path.join(configDir, "opencode.json");
const original = `{
// this comment must survive the merge
"$schema": "https://opencode.ai/config.json",
"provider": {
"custom": {
// keep comments inside unrelated providers too
"name": "Custom Provider"
},
},
}\n`;
await fs.mkdir(configDir, { recursive: true });
await fs.writeFile(jsoncPath, original, "utf-8");
const response = await postApply();
const body = (await response.json()) as {
success?: boolean;
configPath?: string;
backupPath?: string;
};
assert.equal(response.status, 200);
assert.equal(body.success, true);
assert.equal(body.configPath, jsoncPath);
assert.equal(body.backupPath, path.join(configDir, ".omniroute.bak", "opencode.jsonc.bak"));
await assert.rejects(fs.access(jsonPath));
const updatedText = await fs.readFile(jsoncPath, "utf-8");
assert.match(updatedText, /this comment must survive the merge/);
assert.match(updatedText, /keep comments inside unrelated providers too/);
const updated = parse(updatedText);
assert.deepEqual(updated.provider.custom, { name: "Custom Provider" });
assert.equal(updated.provider.omniroute.models["catalog-model"].limit.context, 131072);
assert.equal(await fs.readFile(body.backupPath!, "utf-8"), original);
});
test("apply leaves an invalid opencode.jsonc untouched instead of overwriting it (#10227)", async () => {
const configDir = path.join(process.env.XDG_CONFIG_HOME!, "opencode");
const jsoncPath = path.join(configDir, "opencode.jsonc");
const jsonPath = path.join(configDir, "opencode.json");
const invalid = "{ invalid jsonc\n";
await fs.mkdir(configDir, { recursive: true });
await fs.writeFile(jsoncPath, invalid, "utf-8");
const response = await postApply();
const body = (await response.json()) as { error?: string };
assert.equal(response.status, 400);
assert.match(body.error || "", /invalid.*JSONC|refus/i);
assert.equal(await fs.readFile(jsoncPath, "utf-8"), invalid);
await assert.rejects(fs.access(jsonPath));
});

View File

@@ -1,5 +1,6 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import { parse } from "jsonc-parser";
import {
postProcessOpencodeConfig,
resolveOpencodeTarget,
@@ -50,6 +51,31 @@ test("postProcessOpencodeConfig preserves $schema, provider name and npm", () =>
assert.equal(cfg.provider.omniroute.npm, "@ai-sdk/openai-compatible");
});
test("postProcessOpencodeConfig preserves JSONC comments outside managed fields", () => {
const rawJsonc = `{
// top-level user comment
"provider": {
"custom": {
// nested provider comment
"npm": "@ai-sdk/custom",
},
"omniroute": {
"options": { "apiKey": "sk-secret-literal" },
"models": { "openai/gpt-4o": { "name": "GPT-4o" } },
},
},
}`;
const { json, modelCount } = postProcessOpencodeConfig(rawJsonc);
assert.match(json, /\/\/ top-level user comment/);
assert.match(json, /\/\/ nested provider comment/);
const config = parse(json);
assert.equal(config.provider.custom.npm, "@ai-sdk/custom");
assert.equal(config.provider.omniroute.options.apiKey, "{env:OMNIROUTE_API_KEY}");
assert.equal(modelCount, 1);
});
test("resolveOpencodeTarget: --remote wins and trailing slashes are trimmed", () => {
const { baseUrl } = resolveOpencodeTarget({ remote: "http://vps:20128/" });
assert.equal(baseUrl, "http://vps:20128");

View File

@@ -40,9 +40,8 @@ process.env.JWT_SECRET = "test-jwt-secret-codex-edit-6562";
process.env.INITIAL_PASSWORD = "admin-secret";
const core = await import("../../src/lib/db/core.ts");
const { createProviderConnection, getProviderConnectionById } = await import(
"../../src/lib/db/providers.ts"
);
const { createProviderConnection, getProviderConnectionById } =
await import("../../src/lib/db/providers.ts");
const providerByIdRoute = await import("../../src/app/api/providers/[id]/route.ts");
function resetDb() {
@@ -60,7 +59,11 @@ test.after(() => {
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
});
async function createCodexConnection(priority: number) {
async function createCodexConnection(
priority: number,
authType = "oauth",
providerSpecificData: Record<string, unknown> = {}
) {
// Mirrors createConnectionFromAuthFile()'s real Codex-import shape
// (src/lib/oauth/utils/codexAuthImport.ts) — an OAuth connection whose
// providerSpecificData already carries a normalized `requestDefaults`
@@ -70,7 +73,7 @@ async function createCodexConnection(priority: number) {
// Nth bulk-imported Codex account would already carry.
return createProviderConnection({
provider: "codex",
authType: "oauth",
authType,
name: "Codex (imported)",
email: "user@example.com",
priority,
@@ -85,6 +88,7 @@ async function createCodexConnection(priority: number) {
chatgptUserId: "user-123",
importedAt: new Date().toISOString(),
requestDefaults: { reasoningEffort: "medium", serviceTier: "fast" },
...providerSpecificData,
},
});
}
@@ -155,6 +159,37 @@ test("PUT /api/providers/[id] persists a Codex OAuth edit when priority already
assert.deepEqual(persistedPsd.requestDefaults, { reasoningEffort: "high" });
});
test("PUT /api/providers/[id] removes fingerprint mode from Codex API-key connections", async () => {
const connection = (await createCodexConnection(5, "apikey", {
codexFingerprintMode: "full",
codex_fingerprint_mode: "device",
})) as Record<string, unknown>;
const existingPsd = connection.providerSpecificData as Record<string, unknown>;
assert.equal(existingPsd.codexFingerprintMode, "full");
assert.equal(existingPsd.codex_fingerprint_mode, "device");
const payload = buildCodexEditPayload(connection);
payload.providerSpecificData.codexFingerprintMode = null;
payload.providerSpecificData.codex_fingerprint_mode = null;
const request = await makeManagementSessionRequest(
`http://localhost/api/providers/${connection.id}`,
{ method: "PUT", body: payload }
);
const response = await providerByIdRoute.PUT(request, {
params: Promise.resolve({ id: connection.id as string }),
});
assert.equal(response.status, 200);
const persisted = (await getProviderConnectionById(connection.id as string)) as Record<
string,
unknown
>;
const persistedPsd = persisted.providerSpecificData as Record<string, unknown>;
assert.equal(persistedPsd.codexFingerprintMode, undefined);
assert.equal(persistedPsd.codex_fingerprint_mode, undefined);
});
test("PUT /api/providers/[id] still rejects a genuinely invalid priority (control)", async () => {
const connection = (await createCodexConnection(5)) as Record<string, unknown>;

View File

@@ -0,0 +1,373 @@
import test from "node:test";
import assert from "node:assert/strict";
import {
applyCodexClientIdentityHeaders,
applyCodexClientMetadata,
applyCodexOriginalIdentityHeaders,
createCodexClientIdentity,
getCodexClientSessionId,
getCodexConvergedSessionId,
getCodexConvergedThreadId,
getCodexFingerprintMode,
getCodexInstallationId,
resolveCodexFingerprintIdentity,
resolveCodexOriginalIdentityHeaders,
withCodexFingerprintCredentials,
} from "../../open-sse/config/codexIdentity.ts";
const oauthCredentials = {
accessToken: "oauth-token",
connectionId: "connection-42",
providerSpecificData: { workspaceId: "workspace-42" },
};
test("Codex fingerprint mode defaults to session and only explicit off disables it", () => {
assert.equal(getCodexFingerprintMode(undefined), "session");
assert.equal(getCodexFingerprintMode({ codexFingerprintMode: "invalid" }), "session");
assert.equal(getCodexFingerprintMode({ codexFingerprintMode: "off" }), "off");
assert.equal(
resolveCodexFingerprintIdentity({
credentials: { ...oauthCredentials, providerSpecificData: { codexFingerprintMode: "off" } },
clientHeaders: { "session-id": "client-session" },
body: {},
}),
null
);
});
test("Codex off mode preserves original OAuth identity headers", () => {
const clientHeaders = {
"session-id": "client-session",
"thread-id": "client-thread",
"x-client-request-id": "client-request",
"x-codex-window-id": "client-thread:0",
"x-codex-turn-metadata": '{"turn_id":"client-turn"}',
};
const credentials = {
...oauthCredentials,
providerSpecificData: { codexFingerprintMode: "off" },
};
const original = resolveCodexOriginalIdentityHeaders({
credentials,
clientHeaders,
});
const wrapped = withCodexFingerprintCredentials(credentials, clientHeaders, {});
assert.deepEqual(wrapped.providerSpecificData.codexOriginalIdentityHeaders, original);
assert.equal(wrapped.providerSpecificData.codexClientIdentity, undefined);
const resolvedAgain = resolveCodexOriginalIdentityHeaders({
credentials,
clientHeaders,
});
assert.ok(resolvedAgain);
const headers: Record<string, string> = { session_id: "generated-session" };
applyCodexOriginalIdentityHeaders(headers, resolvedAgain);
assert.equal(headers["session-id"], "client-session");
assert.equal(headers["thread-id"], "client-thread");
assert.equal(headers["x-client-request-id"], "client-request");
assert.equal(headers["x-codex-window-id"], "client-thread:0");
assert.equal(headers["x-codex-turn-metadata"], '{"turn_id":"client-turn"}');
assert.equal(
resolveCodexOriginalIdentityHeaders({
credentials: {
...oauthCredentials,
requestEndpointPath: "/responses/compact",
providerSpecificData: { codexFingerprintMode: "off" },
},
clientHeaders: { "session-id": "compact-session" },
}),
null
);
});
test("Codex device/session/full modes preserve their Sub2API convergence boundaries", () => {
const providerSpecificData = { workspaceId: "workspace-42" };
const device = createCodexClientIdentity("client-session-a", providerSpecificData, {
mode: "device",
});
const sessionA = createCodexClientIdentity("client-session-a", providerSpecificData, {
mode: "session",
accountKey: "connection-42",
});
const sessionB = createCodexClientIdentity("client-session-b", providerSpecificData, {
mode: "session",
accountKey: "connection-42",
});
const fullA = createCodexClientIdentity("client-session-a", providerSpecificData, {
mode: "full",
accountKey: "connection-42",
});
const fullB = createCodexClientIdentity("client-session-b", providerSpecificData, {
mode: "full",
accountKey: "connection-42",
});
assert.ok(device && sessionA && sessionB && fullA && fullB);
assert.equal(device.installationId, getCodexInstallationId(providerSpecificData, undefined));
assert.notEqual(
getCodexInstallationId({}, "connection-a"),
getCodexInstallationId({}, "connection-b")
);
assert.equal(device.sessionId, "");
assert.equal(sessionA.sessionId, sessionB.sessionId);
assert.notEqual(sessionA.threadId, sessionB.threadId);
assert.equal(sessionA.windowId, `${sessionA.threadId}:0`);
assert.equal(fullA.sessionId, fullB.sessionId);
assert.equal(fullA.threadId, fullA.sessionId);
assert.equal(fullA.threadId, fullB.threadId);
assert.notEqual(sessionA.turnId, sessionB.turnId);
assert.equal(
getCodexConvergedSessionId(providerSpecificData, "connection-42"),
sessionA.sessionId
);
assert.equal(
getCodexConvergedThreadId("client-session-a", providerSpecificData, "connection-42"),
sessionA.threadId
);
});
test("Codex client session extraction prefers hyphenated session-id and rejects unsafe values", () => {
assert.equal(
getCodexClientSessionId({ "session-id": "hyphen", session_id: "underscore" }),
"hyphen"
);
assert.equal(getCodexClientSessionId({ session_id: "underscore" }), "underscore");
assert.equal(getCodexClientSessionId({ "session-id": "bad\\r\\nheader" }), null);
});
test("One Codex identity is shared by headers, body metadata, and nested turn metadata", () => {
const identity = resolveCodexFingerprintIdentity({
credentials: oauthCredentials,
clientHeaders: { "session-id": "client-session" },
body: {},
});
assert.ok(identity);
const headers: Record<string, string> = {};
applyCodexClientIdentityHeaders(headers, identity);
const body: Record<string, unknown> = {
client_metadata: { "x-codex-turn-metadata": '{"sandbox":"none"}' },
};
applyCodexClientMetadata(body, identity);
const headerMetadata = JSON.parse(headers["x-codex-turn-metadata"]);
const clientMetadata = body.client_metadata as Record<string, unknown>;
const bodyMetadata = JSON.parse(clientMetadata["x-codex-turn-metadata"] as string);
assert.equal(headers["session-id"], clientMetadata.session_id);
assert.equal(headers["thread-id"], clientMetadata.thread_id);
assert.equal(headers["x-codex-window-id"], clientMetadata["x-codex-window-id"]);
assert.equal(identity.installationId, headers["x-codex-installation-id"]);
assert.equal(identity.turnId, clientMetadata.turn_id);
assert.equal(identity.turnId, headerMetadata.turn_id);
assert.equal(identity.turnId, bodyMetadata.turn_id);
assert.equal(bodyMetadata.sandbox, "none");
});
test("Codex compact requests do not resolve a fingerprint identity", () => {
assert.equal(
resolveCodexFingerprintIdentity({
credentials: {
...oauthCredentials,
requestEndpointPath: "/responses/compact",
},
clientHeaders: { "session-id": "client-session" },
body: {},
}),
null
);
});
test("Codex HTTP off mode preserves original identity headers and body metadata", async () => {
const { CodexExecutor } = await import("../../open-sse/executors/codex.ts");
const executor = new CodexExecutor();
const originalFetch = globalThis.fetch;
let upstreamHeaders = new Headers();
let upstreamBody: Record<string, unknown> = {};
globalThis.fetch = async (_url, init) => {
upstreamHeaders = new Headers(init?.headers);
upstreamBody = JSON.parse(String(init?.body || "{}"));
return new Response(JSON.stringify({ id: "resp-off", object: "response" }), {
status: 200,
headers: { "Content-Type": "application/json" },
});
};
try {
await executor.execute({
model: "gpt-5.5",
body: {
model: "gpt-5.5",
input: [{ role: "user", content: "hello" }],
client_metadata: {
session_id: "client-session",
thread_id: "client-thread",
turn_id: "client-turn",
"x-codex-window-id": "client-thread:0",
"x-codex-turn-metadata": '{"turn_id":"client-turn"}',
},
_nativeCodexPassthrough: true,
},
stream: true,
clientHeaders: {
"session-id": "client-session",
"thread-id": "client-thread",
"x-client-request-id": "client-request",
"x-codex-window-id": "client-thread:0",
"x-codex-turn-metadata": '{"turn_id":"client-turn"}',
},
credentials: {
accessToken: "codex-token",
connectionId: "conn-http-off",
providerSpecificData: { workspaceId: "http-off", codexFingerprintMode: "off" },
},
});
} finally {
globalThis.fetch = originalFetch;
}
const metadata = upstreamBody.client_metadata as Record<string, unknown>;
assert.equal(upstreamHeaders.get("session-id"), "client-session");
assert.equal(upstreamHeaders.get("thread-id"), "client-thread");
assert.equal(upstreamHeaders.get("x-client-request-id"), "client-request");
assert.equal(upstreamHeaders.get("x-codex-window-id"), "client-thread:0");
assert.equal(upstreamHeaders.get("x-codex-turn-metadata"), '{"turn_id":"client-turn"}');
assert.equal(metadata.session_id, "client-session");
assert.equal(metadata.thread_id, "client-thread");
assert.equal(metadata.turn_id, "client-turn");
assert.equal(metadata["x-codex-window-id"], "client-thread:0");
assert.equal(metadata["x-codex-turn-metadata"], '{"turn_id":"client-turn"}');
});
test("Codex websocket off mode preserves original identity headers and body metadata", async () => {
const { CodexExecutor, __setCodexWebSocketTransportForTesting } =
await import("../../open-sse/executors/codex.ts");
const executor = new CodexExecutor();
let sent: string | null = null;
let wsHeaders: Record<string, string> = {};
__setCodexWebSocketTransportForTesting(async (_url, opts) => {
wsHeaders = (opts?.headers as Record<string, string>) || {};
return {
send(data: string) {
sent = data;
queueMicrotask(() => {
this.onmessage?.({
data: JSON.stringify({
type: "response.completed",
response: { status: "completed" },
}),
});
});
},
close() {},
onmessage: null,
onerror: null,
onclose: null,
};
});
try {
const result = await executor.execute({
model: "gpt-5.5",
body: {
model: "gpt-5.5",
input: [{ role: "user", content: "hello" }],
client_metadata: {
session_id: "client-session",
thread_id: "client-thread",
turn_id: "client-turn",
},
},
stream: true,
clientHeaders: {
"session-id": "client-session",
"thread-id": "client-thread",
"x-client-request-id": "client-request",
"x-codex-window-id": "client-thread:0",
"x-codex-turn-metadata": '{"turn_id":"client-turn"}',
},
credentials: {
accessToken: "codex-token",
connectionId: "conn-ws-off",
providerSpecificData: {
workspaceId: "ws-off",
codexTransport: "websocket",
codexFingerprintMode: "off",
},
},
});
await result.response.text();
} finally {
__setCodexWebSocketTransportForTesting(undefined);
}
assert.ok(sent);
const payload = JSON.parse(sent as string) as Record<string, unknown>;
const metadata = payload.client_metadata as Record<string, unknown>;
assert.equal(wsHeaders["session-id"], "client-session");
assert.equal(wsHeaders["thread-id"], "client-thread");
assert.equal(wsHeaders["x-client-request-id"], "client-request");
assert.equal(wsHeaders["x-codex-window-id"], "client-thread:0");
assert.equal(wsHeaders["x-codex-turn-metadata"], '{"turn_id":"client-turn"}');
assert.equal(metadata.session_id, "client-session");
assert.equal(metadata.thread_id, "client-thread");
assert.equal(metadata.turn_id, "client-turn");
});
test("Codex websocket headers and payload share one fingerprint identity", async () => {
const { CodexExecutor, __setCodexWebSocketTransportForTesting } =
await import("../../open-sse/executors/codex.ts");
const executor = new CodexExecutor();
let sent: string | null = null;
let wsHeaders: Record<string, string> = {};
__setCodexWebSocketTransportForTesting(async (_url, opts) => {
wsHeaders = (opts?.headers as Record<string, string>) || {};
return {
send(data: string) {
sent = data;
queueMicrotask(() => {
this.onmessage?.({
data: JSON.stringify({
type: "response.completed",
response: { status: "completed" },
}),
});
});
},
close() {},
onmessage: null,
onerror: null,
onclose: null,
};
});
try {
const result = await executor.execute({
model: "gpt-5.5",
body: {
model: "gpt-5.5",
session_id: "client-ws",
input: [{ role: "user", content: "hello" }],
},
stream: true,
credentials: {
accessToken: "codex-token",
connectionId: "conn-ws",
providerSpecificData: { workspaceId: "ws-ws", codexTransport: "websocket" },
},
});
await result.response.text();
} finally {
__setCodexWebSocketTransportForTesting(undefined);
}
assert.ok(sent);
const payload = JSON.parse(sent as string) as Record<string, unknown>;
const metadata = (payload.client_metadata as Record<string, unknown>) || {};
assert.equal(wsHeaders.session_id, metadata.session_id);
assert.equal(wsHeaders["x-client-request-id"], metadata.thread_id);
assert.equal(wsHeaders["x-codex-window-id"], metadata["x-codex-window-id"]);
assert.equal(payload.type, "response.create");
});

View File

@@ -0,0 +1,59 @@
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-codex-ws-fingerprint-"));
process.env.DATA_DIR = TEST_DATA_DIR;
process.env.APP_LOG_TO_FILE = "false";
process.env.OMNIROUTE_WS_BRIDGE_SECRET = "bridge-secret";
const core = await import("../../src/lib/db/core.ts");
const { createProviderConnection } = await import("../../src/lib/db/providers.ts");
const { POST } = await import("../../src/app/api/internal/codex-responses-ws/route.ts");
function resetDb() {
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
}
test.beforeEach(resetDb);
test.after(() => {
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
});
test("Codex internal websocket bridge prepare preserves original OAuth identity in off mode", async () => {
await createProviderConnection({
provider: "codex",
authType: "oauth",
name: "Codex WS off",
accessToken: "oauth-token",
isActive: true,
testStatus: "active",
providerSpecificData: { codexFingerprintMode: "off" },
});
const response = await POST(
new Request("http://omniroute.local/api/internal/codex-responses-ws", {
method: "POST",
headers: {
"content-type": "application/json",
"x-omniroute-ws-bridge-secret": "bridge-secret",
},
body: JSON.stringify({
action: "prepare",
requestUrl: "http://omniroute.local/v1/responses",
headers: { "session-id": "client-session", "thread-id": "client-thread" },
response: { model: "codex/gpt-5.5", input: "hello" },
}),
})
);
const body = await response.json();
assert.equal(response.status, 200, JSON.stringify(body));
assert.equal(body.headers["session-id"], "client-session");
assert.equal(body.headers["thread-id"], "client-thread");
});

View File

@@ -103,6 +103,7 @@ test("combo builder options route aggregates providers, connections, models and
});
await modelsDb.addCustomModel("openai", "custom-ops", "Custom Ops");
await modelsDb.addCustomModel("openai", "gpt-4.1", "Operator GPT");
// #6975: embeddings-only models (supportedEndpoints without "chat") are no longer
// dropped from the combo builder — they must appear like any other model.
await modelsDb.addCustomModel(
@@ -142,9 +143,12 @@ test("combo builder options route aggregates providers, connections, models and
assert.equal(openai.displayName, "OpenAI");
assert.equal(openai.connectionCount, 2);
assert.equal(openai.activeConnectionCount, 1);
assert.ok(openai.models.some((model) => model.id === "gpt-4.1"));
assert.equal(openai.models.find((model) => model.id === "gpt-4.1").outputTokenLimit, 32768);
assert.equal(openai.models.find((model) => model.id === "gpt-4.1").supportsThinking, false);
const sharedModel = openai.models.find((model) => model.id === "gpt-4.1");
assert.ok(sharedModel);
assert.equal(sharedModel.name, "Operator GPT");
assert.equal(sharedModel.contextLength, 1047576);
assert.equal(sharedModel.outputTokenLimit, 32768);
assert.equal(sharedModel.supportsThinking, false);
assert.equal(
openai.models.some((model) => model.id === "gpt-4o-mini"),
false

View File

@@ -76,4 +76,31 @@ describe("mergeProviderModelListing (cursor exclusive)", () => {
const ids = models.map((m) => m.id);
assert.deepEqual(ids, ["gpt-4o", "gpt-4o-mini"]);
});
it("overlays same-id custom metadata without erasing discovered fields", () => {
const models = mergeProviderModelListing({
providerId: "openai-compatible-chat-test",
registryModels: [],
syncedModels: [
{
id: "shared",
name: "Discovered name",
supportsVision: true,
contextLength: 128000,
},
],
customModels: [{ id: "shared", name: "Operator name", supportsVision: false }],
usesCuratedModelsOnly: false,
});
assert.deepEqual(models, [
{
id: "shared",
name: "Operator name",
source: "custom",
supportsVision: false,
contextLength: 128000,
},
]);
});
});

View File

@@ -108,7 +108,6 @@ test("host db/models.ts preserves its full public API after the split", async ()
"getModelPreserveOpenAIDeveloperRole",
"getModelIsHidden",
"getHiddenModelsByProvider",
"getModelIsDeleted",
"setModelIsHidden",
"getModelUpstreamExtraHeaders",
]) {

View File

@@ -89,7 +89,7 @@ test("replace invalidates only for canonical persisted changes", async () => {
assert.equal(catalogVersion(), beforeIdenticalVersion + 2, "content changes are persisted");
});
test("replace handles empty and deleted-model normalization without phantom writes", async () => {
test("replace handles empty and normalized models without phantom writes", async () => {
const startVersion = catalogVersion();
const startChanges = totalChanges();
@@ -97,18 +97,15 @@ test("replace handles empty and deleted-model normalization without phantom writ
assert.equal(totalChanges(), startChanges, "absent-to-empty replacement must be a no-op");
assert.equal(catalogVersion(), startVersion);
models.mergeModelCompatOverride("openai", "trashed", {
isDeleted: true,
isHidden: true,
});
const beforeDeletedAbsent = totalChanges();
const beforeDeletedVersion = catalogVersion();
await models.replaceSyncedAvailableModelsForConnection("openai", "absent", [
{ id: "trashed", name: "Trashed" },
{ id: "restored", name: "Restored" },
]);
assert.equal(totalChanges(), beforeDeletedAbsent, "filtered deleted models must stay absent");
assert.equal(catalogVersion(), beforeDeletedVersion);
assert.deepEqual(
(await models.getSyncedAvailableModelsForConnection("openai", "absent")).map(
(model) => model.id
),
["restored"]
);
seedSynced("openai", "present", [{ id: "existing", name: "Existing" }]);
const beforeDeleteVersion = catalogVersion();

View File

@@ -871,34 +871,6 @@ test("CodexExecutor.transformRequest passes GPT 5.6 Luna xhigh reasoning through
assert.equal(sanitized.reasoning_effort, undefined);
});
test("CodexExecutor.transformRequest merges Codex installation metadata", () => {
const executor = new CodexExecutor();
const result = executor.transformRequest(
"gpt-5.5",
{
model: "gpt-5.5",
input: [],
client_metadata: { existing: "keep" },
},
true,
{
providerSpecificData: {
codexClientIdentity: {
sessionId: "session-1",
turnId: "turn-1",
windowId: "session-1:0",
installationId: "11111111-1111-4111-a111-111111111111",
},
},
}
);
assert.deepEqual(result.client_metadata, {
existing: "keep",
"x-codex-installation-id": "11111111-1111-4111-a111-111111111111",
});
});
test("CodexExecutor.transformRequest omits client metadata for compact requests", () => {
const executor = new CodexExecutor();
const result = executor.transformRequest(
@@ -1043,20 +1015,25 @@ test("CodexExecutor.execute adds CLI-like session identity headers without chang
},
});
assert.equal(result.response.status, 200);
assert.equal(capturedHeaders?.get("session_id"), "conversation-1");
assert.equal(capturedHeaders?.get("x-client-request-id"), "conversation-1");
assert.equal(capturedHeaders?.get("x-codex-window-id"), "conversation-1:0");
const meta = (capturedBody?.client_metadata as Record<string, unknown>) || {};
const turnMetadata = JSON.parse(capturedHeaders?.get("x-codex-turn-metadata") || "{}");
assert.equal(turnMetadata.session_id, "conversation-1");
assert.equal(result.response.status, 200);
assert.notEqual(capturedHeaders?.get("session_id"), "conversation-1");
assert.equal(capturedHeaders?.get("session-id"), capturedHeaders?.get("session_id"));
assert.equal(capturedHeaders?.get("thread-id"), capturedHeaders?.get("x-client-request-id"));
assert.equal(
capturedHeaders?.get("x-codex-window-id"),
`${capturedHeaders?.get("x-client-request-id")}:0`
);
assert.equal(turnMetadata.session_id, capturedHeaders?.get("session_id"));
assert.equal(turnMetadata.thread_id, capturedHeaders?.get("thread-id"));
assert.equal(turnMetadata.turn_id, meta.turn_id);
assert.equal(turnMetadata.window_id, capturedHeaders?.get("x-codex-window-id"));
assert.equal(turnMetadata.thread_source, "user");
assert.equal(turnMetadata.sandbox, "none");
assert.equal(typeof turnMetadata.turn_id, "string");
assert.equal(capturedBody?.prompt_cache_key, "conversation-1");
assert.equal(
(capturedBody?.client_metadata as Record<string, unknown>)?.["x-codex-installation-id"],
"7f06a8ee-2981-4c81-a4ca-e443b5400a63"
);
assert.equal(meta["x-codex-installation-id"], "7f06a8ee-2981-4c81-a4ca-e443b5400a63");
} finally {
globalThis.fetch = originalFetch;
}
@@ -1087,9 +1064,11 @@ test("CodexExecutor.execute skips identity headers for unsafe session ids", asyn
credentials: { accessToken: "codex-token" },
});
assert.equal(capturedHeaders?.get("x-client-request-id"), null);
assert.equal(capturedHeaders?.get("x-codex-window-id"), null);
assert.equal(capturedHeaders?.get("x-codex-turn-metadata"), null);
assert.notEqual(capturedHeaders?.get("session_id"), "bad\r\nheader");
assert.ok(capturedHeaders?.get("session_id"));
assert.ok(capturedHeaders?.get("x-client-request-id"));
assert.ok(capturedHeaders?.get("x-codex-window-id"));
assert.ok(capturedHeaders?.get("x-codex-turn-metadata"));
} finally {
globalThis.fetch = originalFetch;
}

View File

@@ -48,8 +48,22 @@ for (const { entry, id, alias, chatUrl, modelsUrl } of providers) {
assert.equal(entry.modelsUrl, modelsUrl);
assert.equal(entry.passthroughModels, true);
});
}
// zylo-api and unorouter rely on the live catalog with no static seed (#9085).
for (const { entry, id } of providers.filter((p) => p.id !== "poolside")) {
test(`${id} relies on its live catalog without invented static model ids`, () => {
assert.deepEqual(entry.models, []);
});
}
// Poolside ships the two authenticated-probe models (#10216) as static seeds —
// they are the exact IDs the live catalog returns (authenticated probe 2026-08-07,
// #9085), not invented. Assert them explicitly so a future catalog change is a
// deliberate update, not a silent drift.
test("poolside ships the probed Laguna Preview models, not invented ids", () => {
const models = poolsideProvider.models;
assert.ok(Array.isArray(models) && models.length > 0, "poolside should seed its probed catalog");
const ids = models.map((m) => m.id);
assert.deepEqual(ids, ["poolside/laguna-xs-2.1", "poolside/laguna-s-2.1"]);
});

View File

@@ -11,6 +11,7 @@ const guideSettingsRoute =
const DUMMY_HOME = path.join(os.tmpdir(), "omniroute-guide-settings-test-" + Date.now());
const OPENCODE_CONFIG_PATH = path.join(DUMMY_HOME, ".config", "opencode", "opencode.json");
const OPENCODE_JSONC_CONFIG_PATH = path.join(DUMMY_HOME, ".config", "opencode", "opencode.jsonc");
// cliRuntime.ts hermes entry maps to .config/hermes/config.json (not .hermes/config.yaml)
const HERMES_CONFIG_PATH = path.join(DUMMY_HOME, ".config", "hermes", "config.json");
const originalXDG = process.env.XDG_CONFIG_HOME;
@@ -201,3 +202,24 @@ test("guide-settings POST preserves existing OpenCode config fields while only u
"opencode-go/kimi-k2.6": { name: "Kimi K2.6" },
});
});
test("guide-settings POST refuses to overwrite an invalid opencode.jsonc (#10227)", async () => {
const invalidJsonc = "{ invalid jsonc\n";
await fs.mkdir(path.dirname(OPENCODE_JSONC_CONFIG_PATH), { recursive: true });
await fs.writeFile(OPENCODE_JSONC_CONFIG_PATH, invalidJsonc, "utf-8");
const req = await buildRequest("opencode", {
baseUrl: "http://my-omni/v1",
apiKey: "sk-123",
models: ["cx/gpt-5.6-sol"],
});
const response = (await guideSettingsRoute.POST(req, {
params: { toolId: "opencode" },
})) as Response;
const data = (await response.json()) as { error?: string };
assert.equal(response.status, 500);
assert.match(data.error || "", /invalid JSONC.*refusing to overwrite/i);
assert.equal(await fs.readFile(OPENCODE_JSONC_CONFIG_PATH, "utf-8"), invalidJsonc);
await assert.rejects(fs.access(OPENCODE_CONFIG_PATH));
});

View File

@@ -13,6 +13,7 @@ const capabilityOverrides = await import("../../src/lib/db/modelCapabilityOverri
const models = await import("../../src/lib/db/models.ts");
const providers = await import("../../src/lib/db/providers.ts");
const catalog = await import("../../src/app/api/v1/models/catalog.ts");
const overrideRoute = await import("../../src/app/api/model-capability-overrides/route.ts");
const TARGET = "openai/gpt-5.6";
const LIMITS = { context: 372000, input: 353400, output: 128000 };
@@ -162,3 +163,109 @@ test("v1 model catalog projects an exact raw-alias context override", async () =
"the catalog entry keeps its raw alias and must project that exact override"
);
});
test("v1 model catalog projects a compatible provider prefix override stored under its node id", async () => {
const nodeId = "openai-compatible-chat-context-override";
const prefix = "wawapi-openai";
const modelId = "grok-4.6";
const contextWindow = 500000;
await providers.createProviderNode({
id: nodeId,
type: "openai-compatible",
prefix,
name: "WawAPI (OpenAI)",
apiType: "chat",
baseUrl: "https://example.com/v1",
});
const connection = await providers.createProviderConnection({
provider: nodeId,
authType: "api_key",
name: "compatible-provider-token-limit-catalog",
apiKey: "sk-test",
});
assert.equal(typeof connection.id, "string");
await models.replaceSyncedAvailableModelsForConnection(nodeId, connection.id as string, [
{ id: modelId, name: modelId, source: "imported" },
]);
const patch = await overrideRoute.PATCH(
new Request("http://localhost/api/model-capability-overrides", {
method: "PATCH",
headers: { "content-type": "application/json" },
body: JSON.stringify({
target: `${prefix}/${modelId}`,
key: "context_length",
value: contextWindow,
}),
})
);
assert.equal(patch.status, 200);
assert.equal(
contextOverrides.getModelContextOverrideRecord(nodeId, modelId)?.realContext,
contextWindow,
"the public prefix target must persist under the internal provider node id"
);
assert.equal(
(await getModel(`${prefix}/${modelId}`))?.context_length,
contextWindow,
"the public catalog row must read the override stored under the internal provider node id"
);
});
test("v1 model catalog overlays same-id custom metadata before final overrides", async () => {
const providerId = "openai-compatible-chat-custom-precedence";
const prefix = "custom-precedence";
const modelId = "shared-model";
await providers.createProviderNode({
id: providerId,
type: "openai-compatible",
prefix,
name: "Custom Precedence",
apiType: "chat",
baseUrl: "https://example.com/v1",
});
const connection = await providers.createProviderConnection({
provider: providerId,
authType: "api_key",
name: "custom-precedence-catalog",
apiKey: "sk-test",
});
await models.replaceSyncedAvailableModelsForConnection(providerId, connection.id as string, [
{
id: modelId,
name: "Discovered name",
source: "imported",
inputTokenLimit: 128000,
supportsVision: true,
},
]);
await models.addCustomModel(
providerId,
modelId,
"Operator name",
"manual",
"chat-completions",
["chat"],
undefined,
{ outputTokenLimit: 32000 },
false
);
assert.equal(
capabilityOverrides.setModelCapabilityOverride(
`${prefix}/${modelId}`,
"max_output_tokens",
64000
),
true
);
assert.equal(contextOverrides.setModelContextOverride(providerId, modelId, 500000), true);
const projected = await getModel(`${prefix}/${modelId}`);
assert.ok(projected);
assert.equal(projected.name, "Operator name");
assert.equal(projected.context_length, 500000);
assert.equal(projected.max_output_tokens, 64000);
assert.equal((projected.capabilities as Record<string, unknown>)?.vision, false);
assert.deepEqual(projected.input_modalities, ["text"]);
});

View File

@@ -201,6 +201,7 @@ test("findMissingArtifactPaths flags missing root runtime files in the tarball",
"bin/cli/data-dir.mjs",
"bin/cli/program.mjs",
"bin/cli/utils/ensureAndroidCacheDir.mjs",
"bin/cli/utils/parseEnvValue.mjs",
"bin/cli/utils/storageKeyProvision.mjs",
"bin/cli/utils/versionFastPath.mjs",
"bin/mcp-server.mjs",

View File

@@ -6,7 +6,11 @@
import test from "node:test";
import assert from "node:assert/strict";
import { isValidProviderIconUrl } from "../../src/shared/validation/iconUrl.ts";
import {
isValidProviderIconUrl,
MAX_ICON_DATA_URL_LENGTH,
MAX_ICON_URL_LENGTH,
} from "../../src/shared/validation/iconUrl.ts";
import {
createProviderNodeSchema,
updateProviderNodeSchema,
@@ -19,7 +23,7 @@ const VALID_SVG_DATA_URL =
const VALID_XICON_DATA_URL = "data:image/x-icon;base64,QUJDRA==";
const VALID_JPEG_DATA_URL = "data:image/jpeg;base64,/9j/4AAQSkZJRg==";
const VALID_HTTP = "https://example.com/logo.png";
const VALID_HTTP_2000 = "https://example.com/" + "a".repeat(1970) + ".png";
const VALID_HTTP_2000 = "https://example.com/" + "a".repeat(1976) + ".png";
// ---- shared validator ----
test("isValidProviderIconUrl accepts empty and http(s)", () => {
@@ -220,3 +224,84 @@ test("updateProviderNodeSchema accepts a valid data:image/*;base64 iconUrl", ()
});
assert.equal(result.success, true);
});
function dataIconUrlNearLength(maxLength: number): string {
const prefix = "data:image/png;base64,";
const payloadLength = maxLength - prefix.length;
const aligned = payloadLength - (payloadLength % 4);
return prefix + "A".repeat(aligned);
}
function dataIconUrlOverLimit(maxLength: number): string {
const atLimit = dataIconUrlNearLength(maxLength);
return atLimit + "AAAA";
}
test("create/update schemas accept a data:image iconUrl up to the shared 256 KiB cap", () => {
const iconUrl = dataIconUrlNearLength(MAX_ICON_DATA_URL_LENGTH);
assert.ok(iconUrl.length > MAX_ICON_URL_LENGTH);
assert.ok(iconUrl.length <= MAX_ICON_DATA_URL_LENGTH);
assert.equal(isValidProviderIconUrl(iconUrl), true);
const created = createProviderNodeSchema.safeParse({
name: "Test",
prefix: "test",
apiType: "chat",
iconUrl,
});
assert.equal(created.success, true, created.success ? "" : JSON.stringify(created.error.issues));
const updated = updateProviderNodeSchema.safeParse({
name: "Test",
prefix: "test",
baseUrl: "https://test.com",
iconUrl,
});
assert.equal(updated.success, true, updated.success ? "" : JSON.stringify(updated.error.issues));
});
test("create/update schemas reject an over-limit data:image iconUrl", () => {
const iconUrl = dataIconUrlOverLimit(MAX_ICON_DATA_URL_LENGTH);
assert.ok(iconUrl.length > MAX_ICON_DATA_URL_LENGTH);
assert.ok(iconUrl.length <= MAX_ICON_DATA_URL_LENGTH + 4);
assert.equal(isValidProviderIconUrl(iconUrl), false);
const created = createProviderNodeSchema.safeParse({
name: "Test",
prefix: "test",
apiType: "chat",
iconUrl,
});
assert.equal(created.success, false);
const updated = updateProviderNodeSchema.safeParse({
name: "Test",
prefix: "test",
baseUrl: "https://test.com",
iconUrl,
});
assert.equal(updated.success, false);
});
test("create/update schemas keep the 2000-char cap for http(s) iconUrl", () => {
const tooLongHttp = VALID_HTTP + "a".repeat(MAX_ICON_URL_LENGTH);
assert.ok(tooLongHttp.length > MAX_ICON_URL_LENGTH);
assert.equal(
createProviderNodeSchema.safeParse({
name: "Test",
prefix: "test",
apiType: "chat",
iconUrl: VALID_HTTP_2000,
}).success,
true
);
assert.equal(
createProviderNodeSchema.safeParse({
name: "Test",
prefix: "test",
apiType: "chat",
iconUrl: tooLongHttp,
}).success,
false
);
});

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", isDeleted: true }],
modelCompatOverrides: [{ id: "gemini-3.5-flash", isHidden: true }],
});
const visible = providerLimitUtils.filterHiddenModelQuotas("antigravity", quotas, hidden);

View File

@@ -99,6 +99,35 @@ test("per-connection models route includes user-added custom models on the local
assert.equal(custom.owned_by, "aimlapi", "custom model must be stamped owned_by = provider");
});
test("per-connection models route overlays same-id custom metadata", async () => {
const connection = await seedConnection("aimlapi", { apiKey: "aiml-key" });
await modelsDb.replaceSyncedAvailableModelsForConnection("aimlapi", connection.id, [
{ id: "shared-model", name: "Discovered name", supportsVision: true },
]);
await modelsDb.addCustomModel(
"aimlapi",
"shared-model",
"Operator name",
"manual",
"chat-completions",
["chat"],
undefined,
{},
false
);
globalThis.fetch = (async () => new Response("upstream down", { status: 500 })) as typeof fetch;
const response = await callRoute(connection.id);
const body = (await response.json()) as {
models?: Array<{ id: string; name?: string; supportsVision?: boolean }>;
};
const model = body.models?.find((entry) => entry.id === "shared-model");
assert.equal(response.status, 200);
assert.equal(model?.name, "Operator name");
assert.equal(model?.supportsVision, false);
});
test("per-connection models route can exclude response-only custom models for sync", async () => {
const connection = await seedConnection("aimlapi", { apiKey: "aiml-key" });

View File

@@ -129,6 +129,58 @@ test("provider nodes route creates an OpenAI-compatible node with iconUrl", asyn
assert.equal(body.node.iconUrl, "https://cdn.example.com/icons/custom.png");
});
const LONG_DATA_ICON_URL = "data:image/png;base64," + "A".repeat(2500);
type ProviderNodeResponse = {
node?: { id?: string; iconUrl?: string | null };
error?: { message?: string; details?: unknown };
};
test("provider nodes route creates a node with a data:image iconUrl longer than 2000 chars", async () => {
const response = await providerNodesRoute.POST(
makeRequest({
name: "Data Icon Node",
prefix: "data-icon",
apiType: "chat",
baseUrl: "https://dataicon.example.com/v1",
iconUrl: LONG_DATA_ICON_URL,
})
);
const body = (await response.json()) as ProviderNodeResponse;
assert.equal(response.status, 201, JSON.stringify(body));
assert.equal(body.node?.iconUrl, LONG_DATA_ICON_URL);
});
test("provider nodes route update accepts a data:image iconUrl longer than 2000 chars", async () => {
const createResponse = await providerNodesRoute.POST(
makeRequest({
name: "Data Icon Update Node",
prefix: "data-icon-update",
apiType: "chat",
baseUrl: "https://dataicon-update.example.com/v1",
})
);
const created = (await createResponse.json()) as ProviderNodeResponse;
const nodeId = created.node?.id;
assert.ok(nodeId);
const updateResponse = await providerNodesIdRoute.PUT(
makeUpdateRequest(nodeId, {
name: "Data Icon Update Node",
prefix: "data-icon-update",
apiType: "chat",
baseUrl: "https://dataicon-update.example.com/v1",
iconUrl: LONG_DATA_ICON_URL,
}),
{ params: Promise.resolve({ id: nodeId }) }
);
const updated = (await updateResponse.json()) as ProviderNodeResponse;
assert.equal(updateResponse.status, 200, JSON.stringify(updated));
assert.equal(updated.node?.iconUrl, LONG_DATA_ICON_URL);
});
test("provider nodes route creates nodes without iconUrl (null)", async () => {
const response = await providerNodesRoute.POST(
makeRequest({

View File

@@ -163,6 +163,27 @@ test("provider schemas accept max but reject ultra as a server-side Codex defaul
assert.equal(ultra.success, false);
});
test("provider schemas accept Codex fingerprint modes and reject unknown values", () => {
for (const mode of ["off", "device", "session", "full"]) {
const created = createProviderSchema.safeParse({
provider: "codex",
apiKey: "token",
name: "Codex",
providerSpecificData: { codexFingerprintMode: mode },
});
const updated = updateProviderConnectionSchema.safeParse({
providerSpecificData: { codexFingerprintMode: mode },
});
assert.equal(created.success, true, mode);
assert.equal(updated.success, true, mode);
}
const rejected = updateProviderConnectionSchema.safeParse({
providerSpecificData: { codexFingerprintMode: "aggressive" },
});
assert.equal(rejected.success, false);
});
test("provider schemas reject unknown Codex service tiers", () => {
const created = createProviderSchema.safeParse({
provider: "codex",

View File

@@ -109,4 +109,6 @@ test("wasRefreshTokenRotated: true only when the DB token changed to a non-empty
assert.equal(wasRefreshTokenRotated("rt-old", ""), false, "empty DB token = deactivate");
assert.equal(wasRefreshTokenRotated(null, "rt-new"), false, "unknown attempted = deactivate");
assert.equal(wasRefreshTokenRotated(undefined, undefined), false);
assert.equal(wasRefreshTokenRotated("rt-old", { token: "rt-new" }), false);
assert.equal(wasRefreshTokenRotated(42, "rt-new"), false);
});

View File

@@ -117,6 +117,13 @@ test("extractApiKey parses bearer headers and isValidApiKey validates persisted
assert.equal(await auth.isValidApiKey(""), false);
});
test("getProviderCredentials identifies synthetic no-auth credentials", async () => {
const credentials = await auth.getProviderCredentials("opencode");
assert.equal(credentials?.connectionId, "noauth");
assert.equal(credentials?.authType, "none");
});
test("getProviderCredentials reports rate limiting when only inactive suppressed records remain", async () => {
const retryAfter = futureIso();
await seedConnection("openai", {

View File

@@ -1,28 +1,9 @@
/**
* Deleting a CUSTOM model must not tombstone a same-id SYNCED model.
* Model deletion is physical, while persistent exclusion uses isHidden.
*
* Reported flow (provider `deepseek`, models `deepseek-v4-flash` / `deepseek-v4-pro`):
* 1. Eye-hide both synced models -> `isHidden:true` (no `isDeleted`)
* 2. Manually ADD custom models with the SAME ids
* 3. DELETE the custom models just created
* 4. The ORIGINAL synced models are gone too, yet the UI still lists them
*
* Step 3 is the bug. `DELETE /api/provider-models` resolves `provider` + `model`
* only it has no notion of WHICH of the two same-id rows the operator clicked.
* It unconditionally runs both removals and then, because the synced removal
* reports `true`, writes the `isDeleted:true` tombstone. From then on
* `replaceSyncedAvailableModelsForConnection` filters the id out of every
* re-import (`getModelIsDeleted`), so the provider can never resync: the sync
* endpoint keeps reporting `added: N` while the catalog stays empty, and
* `/v1/models` never lists the models again.
*
* The eye-hide in step 1 is what makes this reachable in practice a hidden
* model stays in the synced store (#3782), so the id exists in BOTH stores at
* the same time and one DELETE hits both.
*
* Guards: A = deleting a custom model leaves a same-id synced sibling intact and
* re-importable; B = deleting a synced-only model still tombstones (#3199 must
* not regress).
* A custom and synced row can share one id. Deleting that id prefers the custom
* row and leaves its synced sibling intact. Deleting a synced-only row removes
* the current discovery result, but a later upstream sync may restore it.
*/
import test from "node:test";
import assert from "node:assert/strict";
@@ -30,11 +11,8 @@ import fs from "node:fs";
import os from "node:os";
import path from "node:path";
// Hermetic DB: this test writes into the `customModels`, `syncedAvailableModels`
// and `modelCompatOverrides` namespaces. Without an isolated DATA_DIR it would
// leak that state into the shared dev/CI database, so a SECOND run would see
// stale tombstones and the preconditions would fail. Point DATA_DIR at a
// throwaway dir before any import that opens the SQLite handle.
// Hermetic DB: this test writes into the customModels and
// syncedAvailableModels namespaces.
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-delete-sibling-"));
process.env.DATA_DIR = TEST_DATA_DIR;
// The DELETE route is auth-gated; with no INITIAL_PASSWORD and no stored
@@ -70,17 +48,12 @@ async function deleteProviderModel(provider: string, modelId: string) {
test("A: deleting a custom model leaves a same-id synced sibling re-importable", async () => {
core.resetDbInstance();
// Step 1 — provider sync brings both models in; operator eye-hides one.
// Provider sync brings both models in; operator eye-hides one.
await modelsDb.replaceSyncedAvailableModelsForConnection(PROVIDER, CONNECTION, [
{ id: FLASH, name: FLASH },
{ id: PRO, name: PRO },
]);
modelsDb.mergeModelCompatOverride(PROVIDER, FLASH, { isHidden: true });
assert.equal(
modelsDb.getModelIsDeleted(PROVIDER, FLASH),
false,
"precondition: eye-hide must not mark the model deleted"
);
// Step 2 — operator manually adds a custom model with the SAME id.
await modelsDb.addCustomModel(PROVIDER, FLASH, FLASH);
@@ -88,11 +61,12 @@ test("A: deleting a custom model leaves a same-id synced sibling re-importable",
// Step 3 — operator deletes the custom model they just created.
await deleteProviderModel(PROVIDER, FLASH);
// Step 4 — the synced sibling must NOT have been tombstoned.
assert.equal(
modelsDb.getModelIsDeleted(PROVIDER, FLASH),
false,
"deleting the custom model must not write an isDeleted tombstone for the synced sibling"
// The synced sibling remains present; deleting the custom definition does not
// reinterpret the operation as a synced-model deletion.
assert.ok(
(await modelsDb.getSyncedAvailableModels(PROVIDER)).some(
(model: { id: string }) => model.id === FLASH
)
);
// The decisive assertion: a later re-sync must bring the model back.
@@ -107,29 +81,28 @@ test("A: deleting a custom model leaves a same-id synced sibling re-importable",
);
});
test("B: deleting a synced-only model still tombstones it (#3199 must not regress)", async () => {
test("B: deleting a synced-only model is temporary and re-sync restores it", async () => {
core.resetDbInstance();
await modelsDb.replaceSyncedAvailableModelsForConnection(PROVIDER, CONNECTION, [
{ id: PRO, name: PRO },
]);
assert.equal(modelsDb.getModelIsDeleted(PROVIDER, PRO), false, "precondition: not yet deleted");
// No custom row exists for this id — this is a real trash/delete.
await deleteProviderModel(PROVIDER, PRO);
assert.equal(
modelsDb.getModelIsDeleted(PROVIDER, PRO),
true,
"a synced-only delete must still write the isDeleted tombstone"
const result = await deleteProviderModel(PROVIDER, PRO);
assert.equal(result.removed, true);
assert.ok(
!(await modelsDb.getSyncedAvailableModels(PROVIDER)).some(
(model: { id: string }) => model.id === PRO
)
);
await modelsDb.replaceSyncedAvailableModelsForConnection(PROVIDER, CONNECTION, [
{ id: PRO, name: PRO },
]);
const ids = (await modelsDb.getSyncedAvailableModels(PROVIDER)).map((m: { id: string }) => m.id);
assert.ok(
!ids.includes(PRO),
`a deleted model must stay dropped across re-import; got [${ids.join(", ")}]`
(await modelsDb.getSyncedAvailableModels(PROVIDER)).some(
(model: { id: string }) => model.id === PRO
),
"upstream re-sync restores a physically deleted synced model"
);
});

View File

@@ -1,77 +0,0 @@
/**
* #3199 follow-up to #3204 — a deleted synced (fetched) model must STAY deleted
* across an auto-fetch re-import.
*
* #3204 added `removeSyncedAvailableModel`, but the DELETE route did not mark the
* model deleted and `replaceSyncedAvailableModelsForConnection` did not skip
* deleted ids — so the next `/models` sync re-imported the model and it
* reappeared. This test guards that a deleted id is filtered out on re-import.
*
* #3782 update: the delete marker is now the DISTINCT `isDeleted` flag (the route
* sets `isDeleted` + `isHidden`), separate from the EYE/visibility toggle which
* sets `isHidden` only and must be preserved across re-syncs. The sync filter
* keys on `isDeleted`, so this test now simulates the route via `isDeleted`.
*/
import test, { before, after } from "node:test";
import assert from "node:assert/strict";
import os from "node:os";
import path from "node:path";
import fs from "node:fs";
// Hermetic DB: this test marks a model hidden (an override persisted in the
// `modelCompatOverrides` key_value namespace). Without an isolated DATA_DIR it
// would write that override into the shared dev/CI database and never clean it
// up, so the SECOND run would see `model-del` already hidden and the first-sync
// precondition would fail. Point DATA_DIR at a throwaway dir before any import
// that opens the SQLite connection.
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-test-synced-del-"));
process.env.DATA_DIR = tmpDir;
const {
replaceSyncedAvailableModelsForConnection,
getSyncedAvailableModels,
mergeModelCompatOverride,
} = await import("../../src/lib/localDb.ts");
const { resetDbInstance } = await import("../../src/lib/db/core.ts");
before(() => {
resetDbInstance();
});
after(() => {
resetDbInstance();
fs.rmSync(tmpDir, { recursive: true, force: true });
});
test("a hidden (deleted) synced model is not re-added on re-import", async () => {
const provider = "llama-cpp";
const connectionId = "conn-3199";
// Initial sync brings in two models.
await replaceSyncedAvailableModelsForConnection(provider, connectionId, [
{ id: "model-keep", name: "Keep" },
{ id: "model-del", name: "Delete me" },
]);
let synced = (await getSyncedAvailableModels(provider)).map((m) => m.id);
assert.ok(synced.includes("model-del"), "both models present after first sync");
// Operator deletes model-del → the DELETE route marks it deleted.
// #3782: the route now writes the DISTINCT `isDeleted` marker (plus `isHidden`
// for back-compat) instead of bare `isHidden`, so an eye/visibility-hidden
// model — which sets `isHidden` only — is preserved across a re-sync while a
// genuinely-deleted one stays dropped. The sync filter keys on `isDeleted`.
mergeModelCompatOverride(provider, "model-del", { isDeleted: true, isHidden: true });
// Auto-fetch re-imports the SAME upstream list (still advertising model-del).
await replaceSyncedAvailableModelsForConnection(provider, connectionId, [
{ id: "model-keep", name: "Keep" },
{ id: "model-del", name: "Delete me" },
]);
synced = (await getSyncedAvailableModels(provider)).map((m) => m.id);
assert.ok(synced.includes("model-keep"), "non-deleted model stays");
assert.ok(
!synced.includes("model-del"),
"deleted (hidden) model must NOT be re-added by the re-import"
);
});

View File

@@ -0,0 +1,55 @@
/**
* A synced model delete removes the current discovery row only. If the upstream
* provider advertises that model again, the next sync restores it. Persistent
* exclusion is represented by isHidden, not a separate deletion tombstone.
*/
import test, { before, after } from "node:test";
import assert from "node:assert/strict";
import os from "node:os";
import path from "node:path";
import fs from "node:fs";
// Hermetic DB: this test mutates the syncedAvailableModels key_value namespace.
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-test-synced-del-"));
process.env.DATA_DIR = tmpDir;
const { replaceSyncedAvailableModelsForConnection, getSyncedAvailableModels } =
await import("../../src/lib/localDb.ts");
const { resetDbInstance } = await import("../../src/lib/db/core.ts");
before(() => {
resetDbInstance();
});
after(() => {
resetDbInstance();
fs.rmSync(tmpDir, { recursive: true, force: true });
});
test("a deleted synced model is restored when upstream advertises it again", async () => {
const provider = "llama-cpp";
const connectionId = "conn-3199";
// Initial sync brings in two models.
await replaceSyncedAvailableModelsForConnection(provider, connectionId, [
{ id: "model-keep", name: "Keep" },
{ id: "model-del", name: "Delete me" },
]);
let synced = (await getSyncedAvailableModels(provider)).map((m) => m.id);
assert.ok(synced.includes("model-del"), "both models present after first sync");
const { removeSyncedAvailableModel } = await import("../../src/lib/localDb.ts");
assert.equal(await removeSyncedAvailableModel(provider, "model-del"), true);
synced = (await getSyncedAvailableModels(provider)).map((m) => m.id);
assert.ok(!synced.includes("model-del"), "delete removes the current synced row");
// Auto-fetch re-imports the SAME upstream list (still advertising model-del).
await replaceSyncedAvailableModelsForConnection(provider, connectionId, [
{ id: "model-keep", name: "Keep" },
{ id: "model-del", name: "Delete me" },
]);
synced = (await getSyncedAvailableModels(provider)).map((m) => m.id);
assert.ok(synced.includes("model-keep"), "unrelated model stays");
assert.ok(synced.includes("model-del"), "upstream re-sync restores the deleted model");
});

View File

@@ -1,22 +1,9 @@
/**
* #3782 — "Auto Sync Enabling all Models".
*
* The user hides models with the EYE/visibility toggle (writes `isHidden:true`
* via `mergeModelCompatOverride`) to keep only their combo's models. Before this
* fix, `replaceSyncedAvailableModelsForConnection` dropped EVERY hidden id on a
* re-sync (it could not tell an eye-hidden model from a DELETED one), so the
* model fell out of the synced store and then churned back through the managed
* alias path — the reported "all models turn back on".
*
* The fix separates the two signals:
* - DELETE (trash) marks `isDeleted:true` (+ keeps `isHidden:true` for back-compat).
* - The EYE toggle sets only `isHidden:true`.
* - The sync filter drops a model only when it is DELETED, so eye-hidden models
* stay listed-but-hidden across re-syncs.
*
* This test guards that an eye-hidden model survives re-import (Test A), that a
* genuinely-new model defaults to visible (Test B), and that the DELETE path
* still drops on re-import (Test C — mirrors the #3199 delete flow).
* The eye toggle persists isHidden independently of synced discovery data.
* Re-sync therefore keeps a hidden model in the synced list without making it
* visible again, while genuinely new models default to visible.
*/
import test, { before, after } from "node:test";
import assert from "node:assert/strict";
@@ -104,29 +91,3 @@ test("B: a genuinely-new model on re-sync defaults to VISIBLE", async () => {
assert.ok(synced.includes("B"), "eye-hidden B still present");
assert.equal(getModelIsHidden(PROVIDER, "B"), true, "eye-hidden B still hidden");
});
test("C: a DELETED synced model still stays out on re-import (delete signal)", async () => {
const provider = "llama-cpp-del";
const connection = "conn-3782-del";
await replaceSyncedAvailableModelsForConnection(provider, connection, [
{ id: "keep", name: "Keep" },
{ id: "del", name: "Delete me" },
]);
let synced = (await getSyncedAvailableModels(provider)).map((m) => m.id);
assert.ok(synced.includes("del"), "both present after first sync");
// Operator DELETES (trash) `del` → the route marks it deleted. Mirror the real
// DELETE route: it sets BOTH the distinct delete marker and (back-compat) hidden.
mergeModelCompatOverride(provider, "del", { isDeleted: true, isHidden: true });
// Auto-fetch re-imports the SAME upstream list (still advertising `del`).
await replaceSyncedAvailableModelsForConnection(provider, connection, [
{ id: "keep", name: "Keep" },
{ id: "del", name: "Delete me" },
]);
synced = (await getSyncedAvailableModels(provider)).map((m) => m.id);
assert.ok(synced.includes("keep"), "non-deleted model stays");
assert.ok(!synced.includes("del"), "DELETED model must NOT be re-added by the re-import");
});

View File

@@ -20,7 +20,9 @@ test("T40: OpenCode card documents config paths and --variant usage", () => {
.join(" ")
.toLowerCase();
assert.match(notesText, /\.config\/opencode\/opencode\.json/);
assert.match(notesText, /or opencode\.json\b/);
assert.match(notesText, /\.config\/opencode\/opencode\.jsonc/);
assert.match(notesText, /preferred when present/);
// #3330: OpenCode uses ~/.config on all platforms (incl. Windows) — the note
// must no longer point Windows users at %APPDATA%.
assert.doesNotMatch(notesText, /%appdata%/);

View File

@@ -40,3 +40,34 @@ test("countTextTokens is additive-ish and monotonic for longer text", () => {
assert.ok(long > short);
assert.ok(short > 0);
});
test("countTextTokens fast-paths strings over 50k chars without tokenizing (worker wedge regression)", () => {
const big = "user: please review the attached patch\ntext: ".repeat(40_000);
const start = performance.now();
const tokens = countTextTokens(big);
const elapsed = performance.now() - start;
assert.equal(tokens, Math.ceil(big.length / 4));
assert.ok(elapsed < 1000, `fast path took ${elapsed.toFixed(0)}ms`);
});
test("countTextTokens strips base64 data URIs before tokenizing (images not counted as text)", () => {
const png =
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==";
const b64 = png.repeat(60);
const withImage = countTextTokens(
`{"image_url":{"url":"data:image/png;base64,${b64}"}}`,
{ provider: "codex" }
);
const stripped = countTextTokens('{"image_url":{"url":""}}', { provider: "codex" });
assert.equal(withImage, stripped);
});
test("countTextTokens does not tokenize huge base64 image payloads (wedge repro)", () => {
const b64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==";
const body = `{"image_url":{"url":"data:image/png;base64,${b64.repeat(14_000)}"}}`;
const start = performance.now();
const tokens = countTextTokens(body);
const elapsed = performance.now() - start;
assert.ok(tokens < 1000, `base64 payload inflates token count to ${tokens}`);
assert.ok(elapsed < 1000, `took ${elapsed.toFixed(0)}ms`);
});

View File

@@ -17,23 +17,28 @@ const { default: AddCompatibleProviderModal } =
const containers: Array<{ root: ReturnType<typeof createRoot>; el: HTMLDivElement }> = [];
function render(props: Record<string, unknown>) {
type ModalProps = React.ComponentProps<typeof AddCompatibleProviderModal>;
function render(props: Partial<ModalProps>) {
const el = document.createElement("div");
document.body.appendChild(el);
const root = createRoot(el);
act(() => {
root.render(
<AddCompatibleProviderModal
isOpen
mode="openai"
onClose={() => {}}
onCreated={() => {}}
{...(props as any)}
/>
);
});
const renderProps = (nextProps: Partial<ModalProps>) => {
act(() => {
root.render(
<AddCompatibleProviderModal
isOpen
mode="openai"
onClose={() => {}}
onCreated={() => {}}
{...nextProps}
/>
);
});
};
renderProps(props);
containers.push({ root, el });
return el;
return { el, rerender: renderProps };
}
function inputByLabel(el: Element, label: string): HTMLInputElement {
@@ -84,7 +89,7 @@ afterEach(() => {
describe("AddCompatibleProviderModal — iconUrl field-level validation", () => {
it("shows an inline error for an unsafe scheme and does NOT submit", async () => {
const el = render({});
const { el } = render({});
const modal = el.querySelector('[role="dialog"]')!;
setInputValue(inputByLabel(modal, "nameLabel"), "My Node");
@@ -102,7 +107,7 @@ describe("AddCompatibleProviderModal — iconUrl field-level validation", () =>
});
it("shows an inline error for a non-image data URL and does NOT submit", async () => {
const el = render({});
const { el } = render({});
const modal = el.querySelector('[role="dialog"]')!;
setInputValue(inputByLabel(modal, "nameLabel"), "My Node");
@@ -119,7 +124,7 @@ describe("AddCompatibleProviderModal — iconUrl field-level validation", () =>
});
it("accepts a valid data:image/*;base64 iconUrl and submits", async () => {
const el = render({});
const { el } = render({});
const modal = el.querySelector('[role="dialog"]')!;
setInputValue(inputByLabel(modal, "nameLabel"), "My Node");
@@ -137,4 +142,98 @@ describe("AddCompatibleProviderModal — iconUrl field-level validation", () =>
const body = JSON.parse(String(call[1].body));
expect(body.iconUrl).toBe("data:image/png;base64,iVBORw0KGgo=");
});
it("shows an inline error for an over-limit data URL and does NOT submit", async () => {
const { el } = render({});
const modal = el.querySelector('[role="dialog"]')!;
const tooLong = "data:image/png;base64," + "A".repeat(256 * 1024);
setInputValue(inputByLabel(modal, "nameLabel"), "My Node");
setInputValue(inputByLabel(modal, "prefixLabel"), "mynode");
setInputValue(inputByLabel(modal, "iconUrlLabel"), tooLong);
const buttons = Array.from(modal.querySelectorAll<HTMLButtonElement>("button"));
const addBtn = buttons.find((b) => b.textContent === "add");
act(() => addBtn!.click());
await waitFor(() => modal.textContent?.includes("iconUrlInvalid") ?? false);
expect(modal.textContent).toContain("iconUrlInvalid");
expect(fetch).not.toHaveBeenCalled();
});
it("surfaces a server non-2xx validation error instead of staying silent", async () => {
const onCreated = vi.fn();
vi.stubGlobal(
"fetch",
vi.fn(() =>
Promise.resolve({
ok: false,
status: 400,
json: () => Promise.resolve({ error: { message: "Icon URL too large" } }),
} as Response)
)
);
const { el } = render({ onCreated });
const modal = el.querySelector('[role="dialog"]')!;
setInputValue(inputByLabel(modal, "nameLabel"), "My Node");
setInputValue(inputByLabel(modal, "prefixLabel"), "mynode");
setInputValue(inputByLabel(modal, "iconUrlLabel"), "data:image/png;base64,iVBORw0KGgo=");
const buttons = Array.from(modal.querySelectorAll<HTMLButtonElement>("button"));
const addBtn = buttons.find((b) => b.textContent === "add");
act(() => addBtn!.click());
await waitFor(() => modal.textContent?.includes("Icon URL too large") ?? false);
expect(modal.textContent).toContain("Icon URL too large");
expect(onCreated).not.toHaveBeenCalled();
});
it("surfaces a network error when create fetch fails", async () => {
const onCreated = vi.fn();
vi.stubGlobal(
"fetch",
vi.fn(() => Promise.reject(new Error("offline")))
);
const { el } = render({ onCreated });
const modal = el.querySelector('[role="dialog"]')!;
setInputValue(inputByLabel(modal, "nameLabel"), "My Node");
setInputValue(inputByLabel(modal, "prefixLabel"), "mynode");
setInputValue(inputByLabel(modal, "iconUrlLabel"), "data:image/png;base64,iVBORw0KGgo=");
const buttons = Array.from(modal.querySelectorAll<HTMLButtonElement>("button"));
const addBtn = buttons.find((b) => b.textContent === "add");
act(() => addBtn!.click());
await waitFor(() => modal.textContent?.includes("Network error") ?? false);
const alert = modal.querySelector('[role="alert"]');
expect(alert?.textContent).toContain("Network error");
expect(alert?.getAttribute("aria-live")).toBe("assertive");
expect(onCreated).not.toHaveBeenCalled();
});
it("clears a previous save error when the modal reopens", async () => {
vi.stubGlobal(
"fetch",
vi.fn(() => Promise.reject(new Error("offline")))
);
const { el, rerender } = render({});
let modal = el.querySelector('[role="dialog"]')!;
setInputValue(inputByLabel(modal, "nameLabel"), "My Node");
setInputValue(inputByLabel(modal, "prefixLabel"), "mynode");
act(() =>
Array.from(modal.querySelectorAll("button"))
.find((button) => button.textContent === "add")!
.click()
);
await waitFor(() => modal.textContent?.includes("Network error") ?? false);
rerender({ isOpen: false });
rerender({ isOpen: true });
modal = el.querySelector('[role="dialog"]')!;
expect(modal.textContent).not.toContain("Network error");
expect(modal.querySelector('[role="alert"]')).toBeNull();
});
});

View File

@@ -17,22 +17,30 @@ const { default: EditCompatibleNodeModal } =
const containers: Array<{ root: ReturnType<typeof createRoot>; el: HTMLDivElement }> = [];
function render(node: Record<string, unknown>, onSave?: () => Promise<void>) {
type ModalProps = React.ComponentProps<typeof EditCompatibleNodeModal>;
type ModalNode = NonNullable<ModalProps["node"]>;
function render(node: ModalNode, onSave?: ModalProps["onSave"]) {
const el = document.createElement("div");
document.body.appendChild(el);
const root = createRoot(el);
act(() => {
root.render(
<EditCompatibleNodeModal
isOpen
node={node as any}
onSave={onSave || (async () => {})}
onClose={() => {}}
/>
);
});
const renderProps = (props: Partial<ModalProps>) => {
act(() => {
root.render(
<EditCompatibleNodeModal
isOpen
node={node}
onSave={onSave || (async () => {})}
onClose={() => {}}
{...props}
/>
);
});
};
renderProps({});
containers.push({ root, el });
return el;
return { el, rerender: renderProps };
}
function inputByLabel(el: Element, label: string): HTMLInputElement {
@@ -85,7 +93,7 @@ const NODE = {
describe("EditCompatibleNodeModal — iconUrl field-level validation", () => {
it("shows an inline error for an unsafe scheme and does NOT call onSave", async () => {
const onSave = vi.fn(async () => {});
const el = render({ ...NODE, iconUrl: "javascript:alert(1)" });
const { el } = render({ ...NODE, iconUrl: "javascript:alert(1)" });
const modal = el.querySelector('[role="dialog"]')!;
setInputValue(inputByLabel(modal, "iconUrlLabel"), "javascript:alert(1)");
@@ -100,7 +108,7 @@ describe("EditCompatibleNodeModal — iconUrl field-level validation", () => {
it("shows an inline error for a non-image data URL and does NOT call onSave", async () => {
const onSave = vi.fn(async () => {});
const el = render({ ...NODE, iconUrl: "data:text/html;base64,QUJD" });
const { el } = render({ ...NODE, iconUrl: "data:text/html;base64,QUJD" });
const modal = el.querySelector('[role="dialog"]')!;
setInputValue(inputByLabel(modal, "iconUrlLabel"), "data:text/html;base64,QUJD");
@@ -115,7 +123,7 @@ describe("EditCompatibleNodeModal — iconUrl field-level validation", () => {
it("accepts a valid data:image/*;base64 iconUrl and calls onSave with it", async () => {
const onSave = vi.fn(async () => {});
const el = render({ ...NODE, iconUrl: "" }, onSave);
const { el } = render({ ...NODE, iconUrl: "" }, onSave);
const modal = el.querySelector('[role="dialog"]')!;
setInputValue(inputByLabel(modal, "iconUrlLabel"), "data:image/png;base64,iVBORw0KGgo=");
@@ -128,4 +136,60 @@ describe("EditCompatibleNodeModal — iconUrl field-level validation", () => {
const payload = onSave.mock.calls[0][0];
expect(payload.iconUrl).toBe("data:image/png;base64,iVBORw0KGgo=");
});
it("shows an inline error for an over-limit data URL and does NOT call onSave", async () => {
const onSave = vi.fn(async () => {});
const { el } = render({ ...NODE, iconUrl: "" }, onSave);
const modal = el.querySelector('[role="dialog"]')!;
const tooLong = "data:image/png;base64," + "A".repeat(256 * 1024);
setInputValue(inputByLabel(modal, "iconUrlLabel"), tooLong);
const buttons = Array.from(modal.querySelectorAll<HTMLButtonElement>("button"));
const saveBtn = buttons.find((b) => b.textContent === "save");
act(() => saveBtn!.click());
await waitFor(() => modal.textContent?.includes("iconUrlInvalid") ?? false);
expect(modal.textContent).toContain("iconUrlInvalid");
expect(onSave).not.toHaveBeenCalled();
});
it("surfaces an error thrown by onSave instead of staying silent", async () => {
const onSave = vi.fn(async () => {
throw new Error("server said no");
});
const { el } = render({ ...NODE, iconUrl: "" }, onSave);
const modal = el.querySelector('[role="dialog"]')!;
setInputValue(inputByLabel(modal, "iconUrlLabel"), "data:image/png;base64,iVBORw0KGgo=");
const buttons = Array.from(modal.querySelectorAll<HTMLButtonElement>("button"));
const saveBtn = buttons.find((b) => b.textContent === "save");
act(() => saveBtn!.click());
await waitFor(() => modal.textContent?.includes("server said no") ?? false);
const alert = modal.querySelector('[role="alert"]');
expect(alert?.textContent).toContain("server said no");
expect(alert?.getAttribute("aria-live")).toBe("assertive");
expect(onSave).toHaveBeenCalledOnce();
});
it("clears a previous save error when the same node modal reopens", async () => {
const onSave = vi.fn(async () => {
throw new Error("server said no");
});
const { el, rerender } = render({ ...NODE, iconUrl: "" }, onSave);
let modal = el.querySelector('[role="dialog"]')!;
act(() =>
Array.from(modal.querySelectorAll("button"))
.find((button) => button.textContent === "save")!
.click()
);
await waitFor(() => modal.textContent?.includes("server said no") ?? false);
rerender({ isOpen: false });
rerender({ isOpen: true });
modal = el.querySelector('[role="dialog"]')!;
expect(modal.textContent).not.toContain("server said no");
expect(modal.querySelector('[role="alert"]')).toBeNull();
});
});

View File

@@ -0,0 +1,98 @@
// @vitest-environment jsdom
import React, { act } from "react";
import { createRoot, type Root } from "react-dom/client";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { useProviderNodeActions } from "../../../src/app/(dashboard)/dashboard/providers/[id]/hooks/useProviderNodeActions";
type Hook = ReturnType<typeof useProviderNodeActions>;
const t = ((key: string) => key) as Parameters<typeof useProviderNodeActions>[0]["t"];
function response(ok: boolean, body: unknown): Response {
return {
ok,
json: async () => body,
} as Response;
}
describe("useProviderNodeActions.handleUpdateNode", () => {
let root: Root | null = null;
let container: HTMLDivElement | null = null;
let captured: Hook | null = null;
beforeEach(() => {
vi.clearAllMocks();
container = document.createElement("div");
document.body.appendChild(container);
});
afterEach(() => {
if (root) act(() => root?.unmount());
root = null;
container?.remove();
container = null;
captured = null;
vi.unstubAllGlobals();
});
async function renderHook(overrides: Partial<Parameters<typeof useProviderNodeActions>[0]> = {}) {
const params: Parameters<typeof useProviderNodeActions>[0] = {
providerId: "node-1",
fetchConnections: vi.fn(async () => {}),
selectedConnection: null,
setProviderNode: vi.fn(),
setShowEditNodeModal: vi.fn(),
setShowEditModal: vi.fn(),
t,
...overrides,
};
function Probe() {
const hook = useProviderNodeActions(params);
React.useEffect(() => {
captured = hook;
}, [hook]);
return null;
}
await act(async () => {
root = createRoot(container!);
root.render(<Probe />);
});
return params;
}
it("throws the server message and keeps the modal open when PUT fails", async () => {
vi.stubGlobal(
"fetch",
vi.fn<typeof fetch>(async () => response(false, { error: { message: "Icon URL too large" } }))
);
const params = await renderHook();
await expect(captured!.handleUpdateNode({ iconUrl: "bad" })).rejects.toThrow(
"Icon URL too large"
);
expect(params.setProviderNode).not.toHaveBeenCalled();
expect(params.fetchConnections).not.toHaveBeenCalled();
expect(params.setShowEditNodeModal).not.toHaveBeenCalled();
});
it("does not report a successful PUT as a save failure when refresh rejects", async () => {
vi.stubGlobal(
"fetch",
vi.fn<typeof fetch>(async () => response(true, { node: { id: "node-1" } }))
);
const fetchConnections = vi.fn(async () => {
throw new Error("refresh failed");
});
const params = await renderHook({ fetchConnections });
await expect(
captured!.handleUpdateNode({ iconUrl: "data:image/png;base64,QUJD" })
).resolves.toBe(undefined);
expect(params.setProviderNode).toHaveBeenCalledWith({ id: "node-1" });
expect(params.setShowEditNodeModal).toHaveBeenCalledWith(false);
expect(fetchConnections).toHaveBeenCalledOnce();
});
});