mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-05 23:02:10 +03:00
Resolve conflicts
This commit is contained in:
@@ -167,7 +167,7 @@ async function mockHealthPageApis(page: Page) {
|
||||
});
|
||||
});
|
||||
|
||||
await page.route("**/api/v1/db/health", async (route) => {
|
||||
await page.route("**/api/db/health", async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
|
||||
@@ -46,6 +46,10 @@ test("cloakAntigravityToolPayload cloaks custom tools, preserves native tools an
|
||||
assert.ok(names.includes(`workspace_read${AG_TOOL_SUFFIX}`));
|
||||
assert.ok(names.includes("run_command"));
|
||||
assert.ok(names.includes("browser_subagent"));
|
||||
assert.ok(names.includes("mcp_sequential_thinking_sequentialthinking"));
|
||||
for (const name of names) {
|
||||
assert.match(name, /^[a-zA-Z0-9_]+$/);
|
||||
}
|
||||
assert.equal(
|
||||
result.body.request.contents[0].parts[0].functionCall.name,
|
||||
`workspace_read${AG_TOOL_SUFFIX}`
|
||||
|
||||
@@ -153,6 +153,7 @@ const cases: Case[] = [
|
||||
expectedClass: "MANAGEMENT",
|
||||
},
|
||||
{ name: "/api/keys MANAGEMENT", path: "/api/keys", expectedClass: "MANAGEMENT" },
|
||||
{ name: "/api/db/health MANAGEMENT", path: "/api/db/health", expectedClass: "MANAGEMENT" },
|
||||
{ name: "/api/settings MANAGEMENT", path: "/api/settings", expectedClass: "MANAGEMENT" },
|
||||
{ name: "/api/audit MANAGEMENT", path: "/api/audit", expectedClass: "MANAGEMENT" },
|
||||
|
||||
|
||||
@@ -174,6 +174,20 @@ test("runAuthzPipeline allows dashboard sessions to read model catalog aliases",
|
||||
assert.equal(response.headers.get("x-omniroute-route-class"), "CLIENT_API");
|
||||
});
|
||||
|
||||
test("runAuthzPipeline allows dashboard sessions to reach DB health management API", async () => {
|
||||
await forceAuthRequired();
|
||||
|
||||
const response = await pipeline.runAuthzPipeline(
|
||||
request("http://localhost/api/db/health", {
|
||||
headers: { cookie: await dashboardCookie() },
|
||||
}),
|
||||
{ enforce: true }
|
||||
);
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
assert.equal(response.headers.get("x-omniroute-route-class"), "MANAGEMENT");
|
||||
});
|
||||
|
||||
test("runAuthzPipeline refreshes dashboard JWTs near expiry", async () => {
|
||||
await forceAuthRequired();
|
||||
const secret = new TextEncoder().encode(process.env.JWT_SECRET);
|
||||
|
||||
@@ -226,7 +226,7 @@ test("getCliRuntimeStatus ignores suspicious known-path binaries and symlink esc
|
||||
const suspiciousStatus = await cliRuntime.getCliRuntimeStatus("qoder");
|
||||
|
||||
assert.equal(suspiciousStatus.installed, false);
|
||||
assert.equal(suspiciousStatus.reason, "not_found");
|
||||
assert.equal(suspiciousStatus.reason, "suspicious_size");
|
||||
|
||||
if (process.platform !== "win32") {
|
||||
const escapePrefix = createTempDir("omniroute-cli-escape-");
|
||||
@@ -246,7 +246,7 @@ test("getCliRuntimeStatus ignores suspicious known-path binaries and symlink esc
|
||||
const escapedStatus = await escapedRuntime.getCliRuntimeStatus("qoder");
|
||||
|
||||
assert.equal(escapedStatus.installed, false);
|
||||
assert.equal(escapedStatus.reason, "not_found");
|
||||
assert.equal(escapedStatus.reason, "symlink_escape");
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -3,29 +3,44 @@ import assert from "node:assert/strict";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { SignJWT } from "jose";
|
||||
|
||||
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-db-health-route-"));
|
||||
process.env.DATA_DIR = TEST_DATA_DIR;
|
||||
process.env.API_KEY_SECRET = process.env.API_KEY_SECRET || "task-303-api-key-secret";
|
||||
|
||||
const core = await import("../../src/lib/db/core.ts");
|
||||
const apiKeysDb = await import("../../src/lib/db/apiKeys.ts");
|
||||
const routeModule = await import("../../src/app/api/v1/db/health/route.ts");
|
||||
const routeModule = await import("../../src/app/api/db/health/route.ts");
|
||||
|
||||
const ORIGINAL_JWT_SECRET = process.env.JWT_SECRET;
|
||||
const ORIGINAL_INITIAL_PASSWORD = process.env.INITIAL_PASSWORD;
|
||||
const TEST_JWT_SECRET = "db-health-route-jwt-secret";
|
||||
const TEST_INITIAL_PASSWORD = "db-health-route-password";
|
||||
|
||||
async function resetStorage() {
|
||||
core.resetDbInstance();
|
||||
apiKeysDb.resetApiKeyState();
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
|
||||
process.env.JWT_SECRET = TEST_JWT_SECRET;
|
||||
process.env.INITIAL_PASSWORD = TEST_INITIAL_PASSWORD;
|
||||
}
|
||||
|
||||
function makeRequest(method, token) {
|
||||
return new Request("http://localhost/api/v1/db/health", {
|
||||
function makeRequest(method, cookie) {
|
||||
return new Request("http://localhost/api/db/health", {
|
||||
method,
|
||||
headers: token ? { Authorization: `Bearer ${token}` } : {},
|
||||
headers: cookie ? { cookie } : {},
|
||||
});
|
||||
}
|
||||
|
||||
async function dashboardCookie() {
|
||||
const secret = new TextEncoder().encode(TEST_JWT_SECRET);
|
||||
const token = await new SignJWT({ authenticated: true })
|
||||
.setProtectedHeader({ alg: "HS256" })
|
||||
.setExpirationTime("1h")
|
||||
.sign(secret);
|
||||
return `auth_token=${token}`;
|
||||
}
|
||||
|
||||
function insertBrokenRows(db) {
|
||||
db.prepare(
|
||||
`INSERT INTO quota_snapshots
|
||||
@@ -43,35 +58,27 @@ test.beforeEach(async () => {
|
||||
|
||||
test.after(async () => {
|
||||
core.resetDbInstance();
|
||||
apiKeysDb.resetApiKeyState();
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
if (ORIGINAL_JWT_SECRET === undefined) delete process.env.JWT_SECRET;
|
||||
else process.env.JWT_SECRET = ORIGINAL_JWT_SECRET;
|
||||
if (ORIGINAL_INITIAL_PASSWORD === undefined) delete process.env.INITIAL_PASSWORD;
|
||||
else process.env.INITIAL_PASSWORD = ORIGINAL_INITIAL_PASSWORD;
|
||||
});
|
||||
|
||||
test("GET /api/v1/db/health requires authentication", async () => {
|
||||
const previousInitialPassword = process.env.INITIAL_PASSWORD;
|
||||
process.env.INITIAL_PASSWORD = "route-health-auth";
|
||||
test("GET /api/db/health requires authentication", async () => {
|
||||
const response = await routeModule.GET(makeRequest("GET"));
|
||||
const body = (await response.json()) as any;
|
||||
|
||||
try {
|
||||
const response = await routeModule.GET(makeRequest("GET"));
|
||||
const body = (await response.json()) as any;
|
||||
|
||||
assert.equal(response.status, 401);
|
||||
assert.equal(body.error.message, "Authentication required");
|
||||
} finally {
|
||||
if (previousInitialPassword === undefined) {
|
||||
delete process.env.INITIAL_PASSWORD;
|
||||
} else {
|
||||
process.env.INITIAL_PASSWORD = previousInitialPassword;
|
||||
}
|
||||
}
|
||||
assert.equal(response.status, 401);
|
||||
assert.equal(body.error.message, "Authentication required");
|
||||
});
|
||||
|
||||
test("GET /api/v1/db/health diagnoses without mutating database rows", async () => {
|
||||
const authKey = await apiKeysDb.createApiKey("Health Route", "machine-route-health");
|
||||
test("GET /api/db/health diagnoses without mutating database rows", async () => {
|
||||
const cookie = await dashboardCookie();
|
||||
const db = core.getDbInstance();
|
||||
insertBrokenRows(db);
|
||||
|
||||
const response = await routeModule.GET(makeRequest("GET", authKey.key));
|
||||
const response = await routeModule.GET(makeRequest("GET", cookie));
|
||||
const body = (await response.json()) as any;
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
@@ -81,12 +88,12 @@ test("GET /api/v1/db/health diagnoses without mutating database rows", async ()
|
||||
assert.equal((db.prepare("SELECT COUNT(*) AS count FROM domain_budgets").get() as any).count, 1);
|
||||
});
|
||||
|
||||
test("POST /api/v1/db/health repairs broken rows for authenticated callers", async () => {
|
||||
const authKey = await apiKeysDb.createApiKey("Health Route", "machine-route-health");
|
||||
test("POST /api/db/health repairs broken rows for authenticated callers", async () => {
|
||||
const cookie = await dashboardCookie();
|
||||
const db = core.getDbInstance();
|
||||
insertBrokenRows(db);
|
||||
|
||||
const response = await routeModule.POST(makeRequest("POST", authKey.key));
|
||||
const response = await routeModule.POST(makeRequest("POST", cookie));
|
||||
const body = (await response.json()) as any;
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
|
||||
@@ -115,7 +115,7 @@ test("replaceCustomModels preserves compat fields and respects the empty-list gu
|
||||
{
|
||||
id: "gpt-4.1",
|
||||
name: "GPT-4.1 Refreshed",
|
||||
source: "api-sync",
|
||||
source: "imported",
|
||||
supportsThinking: true,
|
||||
},
|
||||
]);
|
||||
@@ -147,12 +147,12 @@ test("removing a custom model also removes its compat override", async () => {
|
||||
|
||||
test("synced available models are unioned across connections and cleaned per connection", async () => {
|
||||
await modelsDb.replaceSyncedAvailableModelsForConnection("openai", "conn-a", [
|
||||
{ id: "gpt-4.1", name: "GPT-4.1", source: "api-sync" },
|
||||
{ id: "gpt-4.1-mini", name: "GPT-4.1 Mini", source: "api-sync" },
|
||||
{ id: "gpt-4.1", name: "GPT-4.1", source: "imported" },
|
||||
{ id: "gpt-4.1-mini", name: "GPT-4.1 Mini", source: "imported" },
|
||||
]);
|
||||
const union = await modelsDb.replaceSyncedAvailableModelsForConnection("openai", "conn-b", [
|
||||
{ id: "gpt-4.1-mini", name: "GPT-4.1 Mini", source: "api-sync" },
|
||||
{ id: "o3-mini", name: "o3-mini", source: "api-sync" },
|
||||
{ id: "gpt-4.1-mini", name: "GPT-4.1 Mini", source: "imported" },
|
||||
{ id: "o3-mini", name: "o3-mini", source: "imported" },
|
||||
]);
|
||||
const remaining = await modelsDb.deleteSyncedAvailableModelsForConnection("openai", "conn-a");
|
||||
const allProviders = await modelsDb.getAllSyncedAvailableModels();
|
||||
|
||||
@@ -12,6 +12,10 @@ import {
|
||||
isCodexResponsesWebSocketRequired,
|
||||
parseCodexQuotaHeaders,
|
||||
} from "../../open-sse/executors/codex.ts";
|
||||
import {
|
||||
clearRememberedResponseFunctionCallsForTesting,
|
||||
rememberResponseFunctionCalls,
|
||||
} from "../../open-sse/services/responsesToolCallState.ts";
|
||||
import {
|
||||
DEFAULT_THINKING_CONFIG,
|
||||
setThinkingBudgetConfig,
|
||||
@@ -22,6 +26,7 @@ import { CODEX_CHAT_DEFAULT_INSTRUCTIONS } from "../../open-sse/config/codexInst
|
||||
test.afterEach(() => {
|
||||
setThinkingBudgetConfig(DEFAULT_THINKING_CONFIG);
|
||||
__setCodexWebSocketTransportForTesting(undefined);
|
||||
clearRememberedResponseFunctionCallsForTesting();
|
||||
});
|
||||
|
||||
async function withEnv(entries: Record<string, string | undefined>, fn: () => any) {
|
||||
@@ -296,6 +301,48 @@ test("CodexExecutor.transformRequest preserves store-enabled responses state whe
|
||||
assert.equal(result.previous_response_id, "resp_prev_123");
|
||||
});
|
||||
|
||||
test("CodexExecutor.transformRequest rehydrates missing function_call items for stateful tool outputs", () => {
|
||||
const executor = new CodexExecutor();
|
||||
rememberResponseFunctionCalls("resp_prev_tool_123", [
|
||||
{
|
||||
type: "function_call",
|
||||
call_id: "call_tool_123",
|
||||
name: "workspace_read_file",
|
||||
arguments: '{"path":"README.md"}',
|
||||
},
|
||||
]);
|
||||
const body = {
|
||||
_nativeCodexPassthrough: true,
|
||||
previous_response_id: "resp_prev_tool_123",
|
||||
input: [
|
||||
{
|
||||
type: "function_call_output",
|
||||
call_id: "call_tool_123",
|
||||
output: '{"ok":true}',
|
||||
},
|
||||
],
|
||||
stream: false,
|
||||
};
|
||||
|
||||
const result = executor.transformRequest("gpt-5.5-low", body, false, {
|
||||
requestEndpointPath: "/responses",
|
||||
});
|
||||
|
||||
assert.equal(result.previous_response_id, undefined);
|
||||
assert.equal(result.store, false);
|
||||
assert.deepEqual(result.input[0], {
|
||||
type: "function_call",
|
||||
call_id: "call_tool_123",
|
||||
name: "workspace_read_file",
|
||||
arguments: '{"path":"README.md"}',
|
||||
});
|
||||
assert.deepEqual(result.input[1], {
|
||||
type: "function_call_output",
|
||||
call_id: "call_tool_123",
|
||||
output: '{"ok":true}',
|
||||
});
|
||||
});
|
||||
|
||||
test("CodexExecutor.transformRequest applies per-connection reasoning and service tier defaults", () => {
|
||||
const executor = new CodexExecutor();
|
||||
const result = executor.transformRequest(
|
||||
|
||||
@@ -67,10 +67,10 @@ test("GET /api/memory filters by q and returns matching stats", async () => {
|
||||
assert.equal(response.status, 200);
|
||||
const body = (await response.json()) as any;
|
||||
|
||||
assert.deepEqual(
|
||||
body.data.map((memory) => memory.key),
|
||||
["typescript:tooling", "typescript:guide"]
|
||||
);
|
||||
assert.deepEqual(body.data.map((memory) => memory.key).sort(), [
|
||||
"typescript:guide",
|
||||
"typescript:tooling",
|
||||
]);
|
||||
assert.equal(body.total, 2);
|
||||
assert.equal(body.stats.total, 2);
|
||||
assert.deepEqual(body.stats.byType, { factual: 1, semantic: 1 });
|
||||
|
||||
@@ -177,18 +177,21 @@ test("listMemories filters by api key, type and session while preserving newest-
|
||||
test("listMemories supports limit and offset pagination even when only offset is provided", async () => {
|
||||
insertMemoryRow({
|
||||
id: "page-1",
|
||||
key: "pagination:1",
|
||||
content: "oldest",
|
||||
createdAt: "2026-04-01T00:00:00.000Z",
|
||||
updatedAt: "2026-04-01T00:00:00.000Z",
|
||||
});
|
||||
insertMemoryRow({
|
||||
id: "page-2",
|
||||
key: "pagination:2",
|
||||
content: "middle",
|
||||
createdAt: "2026-04-02T00:00:00.000Z",
|
||||
updatedAt: "2026-04-02T00:00:00.000Z",
|
||||
});
|
||||
insertMemoryRow({
|
||||
id: "page-3",
|
||||
key: "pagination:3",
|
||||
content: "newest",
|
||||
createdAt: "2026-04-03T00:00:00.000Z",
|
||||
updatedAt: "2026-04-03T00:00:00.000Z",
|
||||
@@ -251,18 +254,21 @@ test("listMemories applies query filtering before pagination and type stats", as
|
||||
test("listMemories supports page-based pagination (page 1)", async () => {
|
||||
insertMemoryRow({
|
||||
id: "pg-1",
|
||||
key: "page:test:1",
|
||||
content: "first",
|
||||
createdAt: "2026-04-01T00:00:00.000Z",
|
||||
updatedAt: "2026-04-01T00:00:00.000Z",
|
||||
});
|
||||
insertMemoryRow({
|
||||
id: "pg-2",
|
||||
key: "page:test:2",
|
||||
content: "second",
|
||||
createdAt: "2026-04-02T00:00:00.000Z",
|
||||
updatedAt: "2026-04-02T00:00:00.000Z",
|
||||
});
|
||||
insertMemoryRow({
|
||||
id: "pg-3",
|
||||
key: "page:test:3",
|
||||
content: "third",
|
||||
createdAt: "2026-04-03T00:00:00.000Z",
|
||||
updatedAt: "2026-04-03T00:00:00.000Z",
|
||||
@@ -279,18 +285,21 @@ test("listMemories supports page-based pagination (page 1)", async () => {
|
||||
test("listMemories supports page-based pagination (page 2 returns remainder)", async () => {
|
||||
insertMemoryRow({
|
||||
id: "pg-1",
|
||||
key: "page:test:1",
|
||||
content: "first",
|
||||
createdAt: "2026-04-01T00:00:00.000Z",
|
||||
updatedAt: "2026-04-01T00:00:00.000Z",
|
||||
});
|
||||
insertMemoryRow({
|
||||
id: "pg-2",
|
||||
key: "page:test:2",
|
||||
content: "second",
|
||||
createdAt: "2026-04-02T00:00:00.000Z",
|
||||
updatedAt: "2026-04-02T00:00:00.000Z",
|
||||
});
|
||||
insertMemoryRow({
|
||||
id: "pg-3",
|
||||
key: "page:test:3",
|
||||
content: "third",
|
||||
createdAt: "2026-04-03T00:00:00.000Z",
|
||||
updatedAt: "2026-04-03T00:00:00.000Z",
|
||||
@@ -307,6 +316,7 @@ test("listMemories supports page-based pagination (page 2 returns remainder)", a
|
||||
test("listMemories returns empty data for a page beyond the result set", async () => {
|
||||
insertMemoryRow({
|
||||
id: "pg-1",
|
||||
key: "page:test:1",
|
||||
content: "only entry",
|
||||
createdAt: "2026-04-01T00:00:00.000Z",
|
||||
updatedAt: "2026-04-01T00:00:00.000Z",
|
||||
@@ -320,12 +330,14 @@ test("listMemories returns empty data for a page beyond the result set", async (
|
||||
test("listMemories page parameter defaults to page 1 when omitted with limit", async () => {
|
||||
insertMemoryRow({
|
||||
id: "pg-1",
|
||||
key: "page:test:1",
|
||||
content: "first",
|
||||
createdAt: "2026-04-01T00:00:00.000Z",
|
||||
updatedAt: "2026-04-01T00:00:00.000Z",
|
||||
});
|
||||
insertMemoryRow({
|
||||
id: "pg-2",
|
||||
key: "page:test:2",
|
||||
content: "second",
|
||||
createdAt: "2026-04-02T00:00:00.000Z",
|
||||
updatedAt: "2026-04-02T00:00:00.000Z",
|
||||
|
||||
@@ -9,8 +9,8 @@ import {
|
||||
|
||||
test("model catalog source normalization groups manual and synced rows separately", () => {
|
||||
assert.equal(normalizeModelCatalogSource("manual"), "custom");
|
||||
assert.equal(normalizeModelCatalogSource("imported"), "api-sync");
|
||||
assert.equal(normalizeModelCatalogSource("api-sync"), "api-sync");
|
||||
assert.equal(normalizeModelCatalogSource("imported"), "imported");
|
||||
assert.equal(normalizeModelCatalogSource("api-sync"), "imported");
|
||||
assert.equal(normalizeModelCatalogSource("fallback"), "fallback");
|
||||
assert.equal(normalizeModelCatalogSource("alias"), "alias");
|
||||
assert.equal(normalizeModelCatalogSource(undefined), "system");
|
||||
@@ -19,7 +19,7 @@ test("model catalog source normalization groups manual and synced rows separatel
|
||||
test("model catalog source labels stay user-facing", () => {
|
||||
assert.equal(getModelCatalogSourceLabel("system"), "Built-in");
|
||||
assert.equal(getModelCatalogSourceLabel("custom"), "Custom");
|
||||
assert.equal(getModelCatalogSourceLabel("api-sync"), "Synced");
|
||||
assert.equal(getModelCatalogSourceLabel("imported"), "Imported");
|
||||
assert.equal(getModelCatalogSourceLabel("fallback"), "Fallback");
|
||||
assert.equal(getModelCatalogSourceLabel("alias"), "Alias");
|
||||
});
|
||||
@@ -29,7 +29,7 @@ test("model catalog query matches id, display name, alias and source label", ()
|
||||
modelId: "qwen/qwen3-coder-480b-a35b-instruct",
|
||||
modelName: "Qwen3 Coder 480B",
|
||||
alias: "best-qwen",
|
||||
source: "api-sync",
|
||||
source: "imported",
|
||||
};
|
||||
|
||||
assert.equal(matchesModelCatalogQuery("", target), true);
|
||||
|
||||
@@ -5,9 +5,9 @@ const { getModelCatalogSourceLabel, normalizeModelCatalogSource } =
|
||||
await import("../../src/shared/utils/modelCatalogSearch.ts");
|
||||
|
||||
test("model catalog source normalizes synced import variants consistently", () => {
|
||||
assert.equal(normalizeModelCatalogSource("api-sync"), "api-sync");
|
||||
assert.equal(normalizeModelCatalogSource("auto-sync"), "api-sync");
|
||||
assert.equal(normalizeModelCatalogSource("imported"), "api-sync");
|
||||
assert.equal(getModelCatalogSourceLabel("auto-sync"), "Synced");
|
||||
assert.equal(getModelCatalogSourceLabel("imported"), "Synced");
|
||||
assert.equal(normalizeModelCatalogSource("api-sync"), "imported");
|
||||
assert.equal(normalizeModelCatalogSource("imported"), "imported");
|
||||
assert.equal(normalizeModelCatalogSource("auto-sync"), "imported");
|
||||
assert.equal(getModelCatalogSourceLabel("auto-sync"), "Imported");
|
||||
assert.equal(getModelCatalogSourceLabel("imported"), "Imported");
|
||||
});
|
||||
|
||||
@@ -53,11 +53,11 @@ test("model sync route skips success log when fetched models do not change store
|
||||
apiKey: "test-key",
|
||||
});
|
||||
|
||||
await modelsDb.replaceCustomModels("openrouter", [
|
||||
await modelsDb.replaceSyncedAvailableModelsForConnection("openrouter", connection.id, [
|
||||
{
|
||||
id: "custom-model-1",
|
||||
name: "Custom Model 1",
|
||||
source: "auto-sync",
|
||||
source: "imported",
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -85,6 +85,7 @@ test("model sync route skips success log when fetched models do not change store
|
||||
const body = (await response.json()) as any;
|
||||
assert.equal(body.logged, false);
|
||||
assert.deepEqual(body.modelChanges, { added: 0, removed: 0, updated: 0, total: 0 });
|
||||
assert.deepEqual(body.models, []);
|
||||
|
||||
const logs = await callLogs.getCallLogs({ model: "model-sync", limit: 10 });
|
||||
assert.equal(logs.length, 0);
|
||||
@@ -250,6 +251,45 @@ test("model sync route falls back to the upstream HTTP status when the models pa
|
||||
assert.equal(logs[0].error, "HTTP 429");
|
||||
});
|
||||
|
||||
test("model sync route reports invalid JSON /models responses without losing upstream status", async () => {
|
||||
await resetStorage();
|
||||
|
||||
const connection = await providersDb.createProviderConnection({
|
||||
provider: "openrouter",
|
||||
authType: "apikey",
|
||||
name: "Invalid JSON Sync",
|
||||
apiKey: "test-key",
|
||||
});
|
||||
|
||||
globalThis.fetch = async (url) => {
|
||||
assert.equal(
|
||||
String(url),
|
||||
`http://localhost/api/providers/${connection.id}/models?refresh=true`
|
||||
);
|
||||
return new Response("<html>bad gateway</html>", {
|
||||
status: 200,
|
||||
headers: { "content-type": "text/html" },
|
||||
});
|
||||
};
|
||||
|
||||
const response = await modelSyncRoute.POST(
|
||||
new Request(`http://localhost/api/providers/${connection.id}/sync-models`, {
|
||||
method: "POST",
|
||||
headers: scheduler.buildModelSyncInternalHeaders(),
|
||||
}),
|
||||
{ params: { id: connection.id } }
|
||||
);
|
||||
const body = (await response.json()) as any;
|
||||
const logs = await callLogs.getCallLogs({ model: "model-sync", limit: 10 });
|
||||
|
||||
assert.equal(response.status, 502);
|
||||
assert.equal(body.error, "Invalid JSON response from /models");
|
||||
assert.equal(body.upstreamStatus, 200);
|
||||
assert.equal(logs.length, 1);
|
||||
assert.equal(logs[0].status, 200);
|
||||
assert.equal(logs[0].error, "Invalid JSON response from /models");
|
||||
});
|
||||
|
||||
test("model sync route preserves previously synced models when the upstream omits the models list", async () => {
|
||||
await resetStorage();
|
||||
|
||||
@@ -260,11 +300,11 @@ test("model sync route preserves previously synced models when the upstream omit
|
||||
apiKey: "test-key",
|
||||
});
|
||||
|
||||
await modelsDb.replaceCustomModels("openrouter", [
|
||||
await modelsDb.replaceSyncedAvailableModelsForConnection("openrouter", connection.id, [
|
||||
{
|
||||
id: "persisted-model",
|
||||
name: "Persisted Model",
|
||||
source: "auto-sync",
|
||||
source: "imported",
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -290,13 +330,12 @@ test("model sync route preserves previously synced models when the upstream omit
|
||||
assert.equal(body.syncedModels, 1);
|
||||
assert.equal(body.logged, false);
|
||||
assert.deepEqual(body.modelChanges, { added: 0, removed: 0, updated: 0, total: 0 });
|
||||
assert.deepEqual(body.models, [
|
||||
assert.deepEqual(body.models, []);
|
||||
assert.deepEqual(await modelsDb.getSyncedAvailableModels("openrouter"), [
|
||||
{
|
||||
id: "persisted-model",
|
||||
name: "Persisted Model",
|
||||
source: "auto-sync",
|
||||
apiFormat: "chat-completions",
|
||||
supportedEndpoints: ["chat"],
|
||||
source: "imported",
|
||||
},
|
||||
]);
|
||||
assert.equal(logs.length, 0);
|
||||
@@ -348,24 +387,12 @@ test("model sync route writes synced available models for Gemini connections", a
|
||||
assert.equal(body.syncedModels, 1);
|
||||
assert.equal(body.logged, true);
|
||||
assert.deepEqual(body.modelChanges, { added: 1, removed: 0, updated: 0, total: 1 });
|
||||
assert.deepEqual(body.models, [
|
||||
{
|
||||
id: "gemini-custom-preview",
|
||||
name: "Gemini Custom Preview",
|
||||
source: "api-sync",
|
||||
apiFormat: "chat-completions",
|
||||
supportedEndpoints: ["chat", "embeddings"],
|
||||
inputTokenLimit: 32768,
|
||||
outputTokenLimit: 8192,
|
||||
description: "Custom Gemini preview model",
|
||||
supportsThinking: true,
|
||||
},
|
||||
]);
|
||||
assert.deepEqual(body.models, []);
|
||||
assert.deepEqual(synced, [
|
||||
{
|
||||
id: "gemini-custom-preview",
|
||||
name: "Gemini Custom Preview",
|
||||
source: "api-sync",
|
||||
source: "imported",
|
||||
supportedEndpoints: ["chat", "embeddings"],
|
||||
inputTokenLimit: 32768,
|
||||
outputTokenLimit: 8192,
|
||||
@@ -421,7 +448,7 @@ test("model sync route writes synced available models for non-Gemini providers t
|
||||
{
|
||||
id: "glm-5.1",
|
||||
name: "GLM 5.1",
|
||||
source: "api-sync",
|
||||
source: "imported",
|
||||
supportedEndpoints: ["chat"],
|
||||
inputTokenLimit: 262144,
|
||||
},
|
||||
@@ -439,6 +466,7 @@ test("model sync route import mode merges discovered models without deleting man
|
||||
});
|
||||
|
||||
await modelsDb.addCustomModel("openrouter", "manual-only", "Manual Only", "manual");
|
||||
await modelsDb.addCustomModel("openrouter", "router-v4", "Manual Router V4", "manual");
|
||||
await localDb.setModelAlias("manual-only", "openrouter/manual-only");
|
||||
|
||||
globalThis.fetch = async (url) => {
|
||||
@@ -467,16 +495,21 @@ test("model sync route import mode merges discovered models without deleting man
|
||||
assert.equal(body.updatedCount, 0);
|
||||
assert.equal(body.syncedAliases, 1);
|
||||
assert.deepEqual(body.modelChanges, { added: 1, removed: 0, updated: 0, total: 1 });
|
||||
assert.deepEqual(body.customModelChanges, { added: 0, removed: 1, updated: 0, total: 1 });
|
||||
assert.deepEqual(
|
||||
body.models.map((model) => ({ id: model.id, source: model.source })),
|
||||
[
|
||||
{ id: "manual-only", source: "manual" },
|
||||
{ id: "router-v4", source: "api-sync" },
|
||||
]
|
||||
[{ id: "manual-only", source: "manual" }]
|
||||
);
|
||||
assert.deepEqual(
|
||||
body.importedModels.map((model) => ({ id: model.id, source: model.source })),
|
||||
[{ id: "router-v4", source: "api-sync" }]
|
||||
[{ id: "router-v4", source: "imported" }]
|
||||
);
|
||||
assert.deepEqual(
|
||||
(await modelsDb.getSyncedAvailableModels("openrouter")).map((model) => ({
|
||||
id: model.id,
|
||||
source: model.source,
|
||||
})),
|
||||
[{ id: "router-v4", source: "imported" }]
|
||||
);
|
||||
assert.equal(aliases["manual-only"], "openrouter/manual-only");
|
||||
assert.equal(aliases["router-v4"], "openrouter/router-v4");
|
||||
@@ -492,12 +525,11 @@ test("model sync route import mode ignores supported endpoint ordering changes",
|
||||
apiKey: "test-key",
|
||||
});
|
||||
|
||||
await modelsDb.replaceCustomModels("openrouter", [
|
||||
await modelsDb.replaceSyncedAvailableModelsForConnection("openrouter", connection.id, [
|
||||
{
|
||||
id: "router-v4",
|
||||
name: "Router V4",
|
||||
source: "api-sync",
|
||||
apiFormat: "chat-completions",
|
||||
source: "imported",
|
||||
supportedEndpoints: ["chat", "embeddings"],
|
||||
},
|
||||
]);
|
||||
@@ -536,12 +568,13 @@ test("model sync route import mode ignores supported endpoint ordering changes",
|
||||
assert.equal(body.logged, false);
|
||||
assert.deepEqual(body.importedModels, []);
|
||||
assert.deepEqual(
|
||||
body.models.map((model) => ({
|
||||
(await modelsDb.getSyncedAvailableModels("openrouter")).map((model) => ({
|
||||
id: model.id,
|
||||
supportedEndpoints: model.supportedEndpoints,
|
||||
})),
|
||||
[{ id: "router-v4", supportedEndpoints: ["chat", "embeddings"] }]
|
||||
);
|
||||
assert.deepEqual(body.models, []);
|
||||
assert.equal(logs.length, 0);
|
||||
});
|
||||
|
||||
@@ -555,12 +588,11 @@ test("model sync route import mode reports updates without counting them as new
|
||||
apiKey: "test-key",
|
||||
});
|
||||
|
||||
await modelsDb.replaceCustomModels("openrouter", [
|
||||
await modelsDb.replaceSyncedAvailableModelsForConnection("openrouter", connection.id, [
|
||||
{
|
||||
id: "router-v4",
|
||||
name: "Router V4",
|
||||
source: "api-sync",
|
||||
apiFormat: "chat-completions",
|
||||
source: "imported",
|
||||
supportedEndpoints: ["chat"],
|
||||
},
|
||||
]);
|
||||
@@ -597,7 +629,7 @@ test("model sync route import mode reports updates without counting them as new
|
||||
assert.deepEqual(body.importedChanges, { added: 0, updated: 1, unchanged: 0, total: 1 });
|
||||
assert.deepEqual(body.importedModels, []);
|
||||
assert.deepEqual(
|
||||
body.models.map((model) => ({
|
||||
(await modelsDb.getSyncedAvailableModels("openrouter")).map((model) => ({
|
||||
id: model.id,
|
||||
name: model.name,
|
||||
supportedEndpoints: model.supportedEndpoints,
|
||||
@@ -610,6 +642,7 @@ test("model sync route import mode reports updates without counting them as new
|
||||
},
|
||||
]
|
||||
);
|
||||
assert.deepEqual(body.models, []);
|
||||
assert.equal(body.logged, true);
|
||||
assert.equal(logs.length, 1);
|
||||
});
|
||||
@@ -624,16 +657,16 @@ test("model sync route records added, removed, and updated model diffs with fall
|
||||
accessToken: "sync-token",
|
||||
});
|
||||
|
||||
await modelsDb.replaceCustomModels("openrouter", [
|
||||
await modelsDb.replaceSyncedAvailableModelsForConnection("openrouter", connection.id, [
|
||||
{
|
||||
id: "persisted-model",
|
||||
name: "Persisted Model",
|
||||
source: "auto-sync",
|
||||
source: "imported",
|
||||
},
|
||||
{
|
||||
id: "removed-model",
|
||||
name: "Removed Model",
|
||||
source: "auto-sync",
|
||||
source: "imported",
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -673,7 +706,7 @@ test("model sync route records added, removed, and updated model diffs with fall
|
||||
assert.equal(body.logged, true);
|
||||
assert.deepEqual(body.modelChanges, { added: 1, removed: 1, updated: 1, total: 3 });
|
||||
assert.deepEqual(
|
||||
body.models.map((model) => ({
|
||||
(await modelsDb.getSyncedAvailableModels("openrouter")).map((model) => ({
|
||||
id: model.id,
|
||||
name: model.name,
|
||||
supportedEndpoints: model.supportedEndpoints,
|
||||
@@ -689,7 +722,7 @@ test("model sync route records added, removed, and updated model diffs with fall
|
||||
{
|
||||
id: "fallback-model",
|
||||
name: "Fallback Model",
|
||||
supportedEndpoints: ["chat"],
|
||||
supportedEndpoints: undefined,
|
||||
description: "Fallback from model field",
|
||||
},
|
||||
]
|
||||
@@ -753,17 +786,12 @@ test("model sync route forwards cookies, filters built-ins, and syncs aliases fo
|
||||
assert.equal(body.provider, "openrouter");
|
||||
assert.equal(body.syncedModels, 3);
|
||||
assert.equal(body.availableModelsCount, 3);
|
||||
assert.equal(body.syncedAliases, 2);
|
||||
assert.equal(body.syncedAliases, 3);
|
||||
assert.equal(body.logged, true);
|
||||
assert.deepEqual(body.modelChanges, { added: 2, removed: 0, updated: 0, total: 2 });
|
||||
assert.deepEqual(
|
||||
body.models.map((model) => ({ id: model.id, name: model.name })),
|
||||
[
|
||||
{ id: "router-v2", name: "Router V2" },
|
||||
{ id: "router-v3", name: "Router V3" },
|
||||
]
|
||||
);
|
||||
assert.deepEqual(body.modelChanges, { added: 3, removed: 0, updated: 0, total: 3 });
|
||||
assert.deepEqual(body.models, []);
|
||||
assert.equal(aliases["stale-model"], undefined);
|
||||
assert.equal(aliases["auto"], "openrouter/auto");
|
||||
assert.equal(aliases["openrouter-router-v2"], "openrouter/router-v2");
|
||||
assert.equal(aliases["router-v3"], "openrouter/router-v3");
|
||||
assert.equal(logs.length, 1);
|
||||
@@ -782,6 +810,7 @@ test("model sync route reports synced managed models separately from preserved m
|
||||
});
|
||||
|
||||
await modelsDb.addCustomModel("openrouter", "manual-only", "Manual Only", "manual");
|
||||
await modelsDb.addCustomModel("openrouter", "router-v4", "Manual Router V4", "manual");
|
||||
|
||||
globalThis.fetch = async (url) => {
|
||||
assert.equal(
|
||||
@@ -807,12 +836,17 @@ test("model sync route reports synced managed models separately from preserved m
|
||||
assert.equal(body.availableModelsCount, 2);
|
||||
assert.equal(body.importedCount, 1);
|
||||
assert.equal(body.updatedCount, 0);
|
||||
assert.deepEqual(body.customModelChanges, { added: 0, removed: 1, updated: 0, total: 1 });
|
||||
assert.deepEqual(
|
||||
body.models.map((model) => ({ id: model.id, source: model.source })),
|
||||
[
|
||||
{ id: "manual-only", source: "manual" },
|
||||
{ id: "router-v4", source: "api-sync" },
|
||||
]
|
||||
[{ id: "manual-only", source: "manual" }]
|
||||
);
|
||||
assert.deepEqual(
|
||||
(await modelsDb.getSyncedAvailableModels("openrouter")).map((model) => ({
|
||||
id: model.id,
|
||||
source: model.source,
|
||||
})),
|
||||
[{ id: "router-v4", source: "imported" }]
|
||||
);
|
||||
});
|
||||
|
||||
@@ -868,33 +902,71 @@ test("model sync route uses provider-node prefixes when syncing compatible-provi
|
||||
assert.equal(aliases["cm-sonnet-4-6"], "anthropic-compatible-demo/sonnet-4-6");
|
||||
});
|
||||
|
||||
test("model sync route returns 500 and records a failure when the internal models fetch throws", async () => {
|
||||
test("model sync route falls back to in-process discovery when internal self-fetch throws", async () => {
|
||||
await resetStorage();
|
||||
|
||||
const connection = await providersDb.createProviderConnection({
|
||||
provider: "openrouter",
|
||||
provider: "openai-compatible-aio",
|
||||
authType: "apikey",
|
||||
name: "Exploding Sync",
|
||||
name: "AIO Import",
|
||||
apiKey: "test-key",
|
||||
providerSpecificData: {
|
||||
prefix: "aio",
|
||||
apiType: "chat",
|
||||
baseUrl: "https://api.bltcy.ai/v1",
|
||||
nodeName: "aio",
|
||||
autoSync: true,
|
||||
},
|
||||
});
|
||||
|
||||
globalThis.fetch = async () => {
|
||||
throw new Error("network exploded");
|
||||
const fetchCalls: string[] = [];
|
||||
globalThis.fetch = async (url) => {
|
||||
const urlString = String(url);
|
||||
fetchCalls.push(urlString);
|
||||
|
||||
if (urlString === `http://localhost/api/providers/${connection.id}/models?refresh=true`) {
|
||||
throw new Error("fetch failed");
|
||||
}
|
||||
|
||||
assert.equal(urlString, "https://api.bltcy.ai/v1/models");
|
||||
return Response.json({
|
||||
data: [{ id: "aio-model", name: "AIO Model" }],
|
||||
});
|
||||
};
|
||||
|
||||
const response = await modelSyncRoute.POST(
|
||||
new Request(`http://localhost/api/providers/${connection.id}/sync-models`, {
|
||||
new Request(`http://localhost/api/providers/${connection.id}/sync-models?mode=import`, {
|
||||
method: "POST",
|
||||
headers: scheduler.buildModelSyncInternalHeaders(),
|
||||
}),
|
||||
{ params: { id: connection.id } }
|
||||
);
|
||||
const body = (await response.json()) as any;
|
||||
const logs = await callLogs.getCallLogs({ model: "model-sync", limit: 10 });
|
||||
const body = (await response.json()) as {
|
||||
importedCount: number;
|
||||
importedModels: Array<{ id: string; source: string }>;
|
||||
};
|
||||
const customModels = (await modelsDb.getCustomModels("openai-compatible-aio")) as Array<{
|
||||
id: string;
|
||||
source: string;
|
||||
}>;
|
||||
const availableModels = await modelsDb.getSyncedAvailableModels("openai-compatible-aio");
|
||||
|
||||
assert.equal(response.status, 500);
|
||||
assert.equal(body.error, "network exploded");
|
||||
assert.equal(logs.length, 1);
|
||||
assert.equal(logs[0].status, 500);
|
||||
assert.equal(logs[0].provider, "openrouter");
|
||||
assert.equal(response.status, 200);
|
||||
assert.equal(body.importedCount, 1);
|
||||
assert.deepEqual(
|
||||
body.importedModels.map((model) => ({ id: model.id, source: model.source })),
|
||||
[{ id: "aio-model", source: "imported" }]
|
||||
);
|
||||
assert.deepEqual(
|
||||
customModels.map((model) => ({ id: model.id, source: model.source })),
|
||||
[]
|
||||
);
|
||||
assert.deepEqual(
|
||||
availableModels.map((model) => ({ id: model.id, source: model.source })),
|
||||
[{ id: "aio-model", source: "imported" }]
|
||||
);
|
||||
assert.deepEqual(fetchCalls, [
|
||||
`http://localhost/api/providers/${connection.id}/models?refresh=true`,
|
||||
"https://api.bltcy.ai/v1/models",
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -354,21 +354,21 @@ test("v1 models catalog includes synced Gemini models and duplicates audio model
|
||||
{
|
||||
id: "gemini-audio-live",
|
||||
name: "Gemini Audio Live",
|
||||
source: "api-sync",
|
||||
source: "imported",
|
||||
supportedEndpoints: ["audio"],
|
||||
inputTokenLimit: 4096,
|
||||
},
|
||||
{
|
||||
id: "text-embedding-004",
|
||||
name: "Text Embedding 004",
|
||||
source: "api-sync",
|
||||
source: "imported",
|
||||
supportedEndpoints: ["embeddings"],
|
||||
inputTokenLimit: 2048,
|
||||
},
|
||||
{
|
||||
id: "gemini-hidden",
|
||||
name: "Gemini Hidden",
|
||||
source: "api-sync",
|
||||
source: "imported",
|
||||
supportedEndpoints: ["chat"],
|
||||
},
|
||||
]
|
||||
@@ -402,7 +402,7 @@ test("v1 models catalog keeps Gemini chat models untyped when synced endpoints a
|
||||
{
|
||||
id: "gemini-2.5-pro-live",
|
||||
name: "Gemini 2.5 Pro Live",
|
||||
source: "api-sync",
|
||||
source: "imported",
|
||||
inputTokenLimit: 8192,
|
||||
},
|
||||
]);
|
||||
@@ -430,7 +430,7 @@ test("v1 models catalog includes synced non-Gemini provider models from discover
|
||||
{
|
||||
id: "glm-5.1",
|
||||
name: "GLM 5.1",
|
||||
source: "api-sync",
|
||||
source: "imported",
|
||||
supportedEndpoints: ["chat"],
|
||||
inputTokenLimit: 262144,
|
||||
},
|
||||
|
||||
123
tests/unit/oauth-kiro-idc.test.ts
Normal file
123
tests/unit/oauth-kiro-idc.test.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import { kiro } from "@/lib/oauth/providers/kiro";
|
||||
|
||||
test("kiro.requestDeviceCode returns resolved region for IDC token endpoint", async () => {
|
||||
const originalFetch = global.fetch;
|
||||
|
||||
const fetchCalls: Array<{ url: string; body?: string }> = [];
|
||||
|
||||
global.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => {
|
||||
const url = String(input);
|
||||
fetchCalls.push({ url, body: typeof init?.body === "string" ? init.body : undefined });
|
||||
|
||||
if (url.endsWith("/client/register")) {
|
||||
return new Response(JSON.stringify({ clientId: "client-ap", clientSecret: "secret-ap" }), {
|
||||
status: 200,
|
||||
});
|
||||
}
|
||||
|
||||
if (url.endsWith("/device_authorization")) {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
deviceCode: "dev-code",
|
||||
userCode: "user-code",
|
||||
verificationUri: "https://d-tenant.awsapps.com/start/#/device",
|
||||
verificationUriComplete: "https://d-tenant.awsapps.com/start/#/device?user_code=ABCD",
|
||||
expiresIn: 600,
|
||||
interval: 1,
|
||||
}),
|
||||
{ status: 200 }
|
||||
);
|
||||
}
|
||||
|
||||
return new Response("not-found", { status: 404 });
|
||||
}) as typeof fetch;
|
||||
|
||||
try {
|
||||
const result = await kiro.requestDeviceCode({
|
||||
registerClientUrl: "https://oidc.ap-southeast-1.amazonaws.com/client/register",
|
||||
deviceAuthUrl: "https://oidc.ap-southeast-1.amazonaws.com/device_authorization",
|
||||
tokenUrl: "https://oidc.ap-southeast-1.amazonaws.com/token",
|
||||
startUrl: "https://d-tenant.awsapps.com/start",
|
||||
clientName: "kiro-oauth-client",
|
||||
clientType: "public",
|
||||
scopes: ["codewhisperer:completions"],
|
||||
grantTypes: ["urn:ietf:params:oauth:grant-type:device_code", "refresh_token"],
|
||||
skipIssuerUrlForRegistration: true,
|
||||
});
|
||||
|
||||
assert.equal(result._region, "ap-southeast-1");
|
||||
assert.equal(result._clientId, "client-ap");
|
||||
assert.equal(result._clientSecret, "secret-ap");
|
||||
|
||||
const registerBody = JSON.parse(fetchCalls[0]?.body || "{}");
|
||||
assert.equal(registerBody.issuerUrl, undefined);
|
||||
} finally {
|
||||
global.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
|
||||
test("kiro.pollToken uses region provided by extraData", async () => {
|
||||
const originalFetch = global.fetch;
|
||||
let requestedUrl = "";
|
||||
|
||||
global.fetch = (async (input: RequestInfo | URL) => {
|
||||
requestedUrl = String(input);
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
accessToken: "access",
|
||||
refreshToken: "refresh",
|
||||
expiresIn: 3600,
|
||||
}),
|
||||
{ status: 200 }
|
||||
);
|
||||
}) as typeof fetch;
|
||||
|
||||
try {
|
||||
const result = await kiro.pollToken(
|
||||
{ tokenUrl: "https://oidc.us-east-1.amazonaws.com/token" },
|
||||
"device-code",
|
||||
null,
|
||||
{ _clientId: "cid", _clientSecret: "csecret", _region: "ap-southeast-1" }
|
||||
);
|
||||
|
||||
assert.equal(requestedUrl, "https://oidc.ap-southeast-1.amazonaws.com/token");
|
||||
assert.equal(result.ok, true);
|
||||
assert.equal(result.data.access_token, "access");
|
||||
assert.equal(result.data._region, "ap-southeast-1");
|
||||
} finally {
|
||||
global.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
|
||||
test("kiro.mapTokens persists region into providerSpecificData", () => {
|
||||
const mapped = kiro.mapTokens({
|
||||
access_token: "at",
|
||||
refresh_token: "rt",
|
||||
expires_in: 3600,
|
||||
_clientId: "cid",
|
||||
_clientSecret: "csec",
|
||||
_region: "ap-southeast-1",
|
||||
});
|
||||
|
||||
assert.equal(mapped.accessToken, "at");
|
||||
assert.equal(mapped.refreshToken, "rt");
|
||||
assert.equal(mapped.expiresIn, 3600);
|
||||
assert.equal(mapped.providerSpecificData.clientId, "cid");
|
||||
assert.equal(mapped.providerSpecificData.clientSecret, "csec");
|
||||
assert.equal(mapped.providerSpecificData.region, "ap-southeast-1");
|
||||
});
|
||||
|
||||
test("kiro.mapTokens defaults region to undefined when not provided", () => {
|
||||
const mapped = kiro.mapTokens({
|
||||
access_token: "at",
|
||||
refresh_token: "rt",
|
||||
expires_in: 3600,
|
||||
_clientId: "cid",
|
||||
_clientSecret: "csec",
|
||||
});
|
||||
|
||||
assert.equal(mapped.providerSpecificData.region, undefined);
|
||||
});
|
||||
@@ -461,7 +461,7 @@ test("provider models route caches discovered opencode-go models per connection"
|
||||
assert.equal(firstResponse.status, 200);
|
||||
assert.equal(firstBody.source, "api");
|
||||
assert.deepEqual(firstBody.models, [{ id: "glm-5.1", name: "GLM 5.1" }]);
|
||||
assert.deepEqual(cachedModels, [{ id: "glm-5.1", name: "GLM 5.1", source: "api-sync" }]);
|
||||
assert.deepEqual(cachedModels, [{ id: "glm-5.1", name: "GLM 5.1", source: "imported" }]);
|
||||
|
||||
globalThis.fetch = async () => {
|
||||
throw new Error("cached route should not hit upstream");
|
||||
@@ -472,7 +472,7 @@ test("provider models route caches discovered opencode-go models per connection"
|
||||
|
||||
assert.equal(cachedResponse.status, 200);
|
||||
assert.equal(cachedBody.source, "cache");
|
||||
assert.deepEqual(cachedBody.models, [{ id: "glm-5.1", name: "GLM 5.1", source: "api-sync" }]);
|
||||
assert.deepEqual(cachedBody.models, [{ id: "glm-5.1", name: "GLM 5.1", source: "imported" }]);
|
||||
assert.equal(fetchCalls, 1);
|
||||
});
|
||||
|
||||
@@ -481,7 +481,7 @@ test("provider models route falls back to cached models when a refresh fails", a
|
||||
apiKey: "opencode-go-key",
|
||||
});
|
||||
await modelsDb.replaceSyncedAvailableModelsForConnection("opencode-go", connection.id, [
|
||||
{ id: "cached-go", name: "Cached Go", source: "api-sync" },
|
||||
{ id: "cached-go", name: "Cached Go", source: "imported" },
|
||||
]);
|
||||
let fetchCalls = 0;
|
||||
|
||||
@@ -496,7 +496,7 @@ test("provider models route falls back to cached models when a refresh fails", a
|
||||
assert.equal(response.status, 200);
|
||||
assert.equal(body.source, "cache");
|
||||
assert.match(body.warning, /cached catalog/i);
|
||||
assert.deepEqual(body.models, [{ id: "cached-go", name: "Cached Go", source: "api-sync" }]);
|
||||
assert.deepEqual(body.models, [{ id: "cached-go", name: "Cached Go", source: "imported" }]);
|
||||
assert.equal(fetchCalls, 1);
|
||||
});
|
||||
|
||||
@@ -505,7 +505,7 @@ test("provider models route clears cached discovery when a refresh returns no re
|
||||
apiKey: "opencode-go-key",
|
||||
});
|
||||
await modelsDb.replaceSyncedAvailableModelsForConnection("opencode-go", connection.id, [
|
||||
{ id: "cached-go", name: "Cached Go", source: "api-sync" },
|
||||
{ id: "cached-go", name: "Cached Go", source: "imported" },
|
||||
]);
|
||||
|
||||
globalThis.fetch = async () => {
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
createStreamController,
|
||||
createDisconnectAwareStream,
|
||||
} from "../../open-sse/utils/streamHandler.ts";
|
||||
import { createPassthroughStreamWithLogger } from "../../open-sse/utils/stream.ts";
|
||||
|
||||
import { wantsProgress, createProgressTransform } from "../../open-sse/utils/progressTracker.ts";
|
||||
|
||||
@@ -50,6 +51,88 @@ test("createProgressTransform maps SSE text output to valid byte stream with pro
|
||||
assert.match(result, /done":true/);
|
||||
});
|
||||
|
||||
test("createPassthroughStreamWithLogger omits [DONE] for Responses clients", async () => {
|
||||
const transform = createPassthroughStreamWithLogger(
|
||||
"codex",
|
||||
null,
|
||||
null,
|
||||
"gpt-5.5-low",
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
"openai-responses"
|
||||
);
|
||||
|
||||
const writer = transform.writable.getWriter();
|
||||
await writer.write(
|
||||
new TextEncoder().encode(
|
||||
[
|
||||
"event: response.completed",
|
||||
'data: {"type":"response.completed","response":{"id":"resp_1","model":"gpt-5.5-low","status":"completed","output":[],"usage":{"input_tokens":1,"output_tokens":1,"total_tokens":2}}}',
|
||||
"",
|
||||
].join("\n")
|
||||
)
|
||||
);
|
||||
await writer.close();
|
||||
|
||||
const reader = transform.readable.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let result = "";
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
result += decoder.decode(value);
|
||||
}
|
||||
|
||||
assert.match(result, /event: response\.completed/);
|
||||
assert.doesNotMatch(result, /data: \[DONE\]/);
|
||||
});
|
||||
|
||||
test("createPassthroughStreamWithLogger synthesizes reasoning summary events from reasoning output items", async () => {
|
||||
const transform = createPassthroughStreamWithLogger(
|
||||
"codex",
|
||||
null,
|
||||
null,
|
||||
"gpt-5.5-low",
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
"openai-responses"
|
||||
);
|
||||
|
||||
const writer = transform.writable.getWriter();
|
||||
await writer.write(
|
||||
new TextEncoder().encode(
|
||||
[
|
||||
"event: response.output_item.done",
|
||||
'data: {"type":"response.output_item.done","response_id":"resp_reasoning_1","output_index":0,"item":{"id":"rs_resp_reasoning_1_0","type":"reasoning","summary":[{"type":"summary_text","text":"Reasoning summary text"}]}}',
|
||||
"",
|
||||
].join("\n")
|
||||
)
|
||||
);
|
||||
await writer.close();
|
||||
|
||||
const reader = transform.readable.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let result = "";
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
result += decoder.decode(value);
|
||||
}
|
||||
|
||||
assert.match(result, /event: response\.reasoning_summary_text\.delta/);
|
||||
assert.match(result, /"delta":"Reasoning summary text"/);
|
||||
assert.match(result, /event: response\.reasoning_summary_part\.done/);
|
||||
assert.match(result, /event: response\.output_item\.done/);
|
||||
});
|
||||
|
||||
test("createStreamController returns valid controller", () => {
|
||||
let completeLogged = false;
|
||||
let disconnectLogged = false;
|
||||
@@ -61,8 +144,8 @@ test("createStreamController returns valid controller", () => {
|
||||
};
|
||||
|
||||
const sc = createStreamController({
|
||||
connectionId: "conn_1",
|
||||
onStreamComplete: () => {},
|
||||
provider: "test",
|
||||
model: "conn_1",
|
||||
});
|
||||
|
||||
assert.equal(typeof sc.signal, "object");
|
||||
|
||||
@@ -464,6 +464,47 @@ test("refreshKiroToken uses the AWS OIDC flow when client credentials are presen
|
||||
});
|
||||
});
|
||||
|
||||
test("refreshKiroToken uses stored region for AWS OIDC refresh without authMethod", async () => {
|
||||
const log = createLog();
|
||||
const calls: any[] = [];
|
||||
|
||||
await withMockedFetch(
|
||||
async (url, options = {}) => {
|
||||
calls.push({ url, options });
|
||||
return jsonResponse({
|
||||
accessToken: "kiro-aws-access",
|
||||
refreshToken: "kiro-aws-refresh-next",
|
||||
expiresIn: 900,
|
||||
});
|
||||
},
|
||||
async () => {
|
||||
const result = await refreshKiroToken(
|
||||
"kiro-refresh",
|
||||
{
|
||||
clientId: "aws-client",
|
||||
clientSecret: "aws-secret",
|
||||
region: "ap-southeast-1",
|
||||
},
|
||||
log
|
||||
);
|
||||
|
||||
assert.deepEqual(result, {
|
||||
accessToken: "kiro-aws-access",
|
||||
refreshToken: "kiro-aws-refresh-next",
|
||||
expiresIn: 900,
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
assert.equal(calls[0].url, "https://oidc.ap-southeast-1.amazonaws.com/token");
|
||||
assert.deepEqual(JSON.parse(calls[0].options.body), {
|
||||
clientId: "aws-client",
|
||||
clientSecret: "aws-secret",
|
||||
refreshToken: "kiro-refresh",
|
||||
grantType: "refresh_token",
|
||||
});
|
||||
});
|
||||
|
||||
test("refreshKiroToken falls back to the social-auth refresh endpoint", async () => {
|
||||
const log = createLog();
|
||||
const calls: any[] = [];
|
||||
|
||||
@@ -76,7 +76,7 @@ test("Claude -> Gemini maps system, thinking, tool use, tool result and tools",
|
||||
);
|
||||
|
||||
assert.deepEqual(result.systemInstruction, {
|
||||
role: "user",
|
||||
role: "system",
|
||||
parts: [{ text: "Rules" }],
|
||||
});
|
||||
assert.equal(result.contents[0].role, "model");
|
||||
@@ -92,6 +92,7 @@ test("Claude -> Gemini maps system, thinking, tool use, tool result and tools",
|
||||
},
|
||||
});
|
||||
assert.equal(result.generationConfig.maxOutputTokens, 256);
|
||||
assert.match((result as any).tools[0].functionDeclarations[0].name, /^[a-zA-Z0-9_]+$/);
|
||||
assert.equal(result.generationConfig.temperature, 0.4);
|
||||
assert.equal(result.generationConfig.topP, 0.8);
|
||||
assert.deepEqual(result.generationConfig.thinkingConfig, {
|
||||
@@ -105,6 +106,19 @@ test("Claude -> Gemini maps system, thinking, tool use, tool result and tools",
|
||||
});
|
||||
});
|
||||
|
||||
test("Claude -> Gemini clamps maxOutputTokens to the model cap", () => {
|
||||
const result = claudeToGeminiRequest(
|
||||
"gemini-2.5-flash",
|
||||
{
|
||||
messages: [{ role: "user", content: [{ type: "text", text: "Hello" }] }],
|
||||
max_tokens: 999999,
|
||||
},
|
||||
false
|
||||
);
|
||||
|
||||
assert.equal(result.generationConfig.maxOutputTokens, 8192);
|
||||
});
|
||||
|
||||
test("Claude -> Gemini converts text and base64 images to Gemini parts", () => {
|
||||
const result = claudeToGeminiRequest(
|
||||
"gemini-2.5-flash",
|
||||
|
||||
@@ -224,7 +224,7 @@ test("OpenAI -> Gemini request maps messages, merged system instructions, tools
|
||||
false
|
||||
);
|
||||
|
||||
assert.equal((result as any).systemInstruction.role, "user");
|
||||
assert.equal((result as any).systemInstruction.role, "system");
|
||||
assert.deepEqual((result as any).systemInstruction.parts, [
|
||||
{ text: "Rule A" },
|
||||
{ text: "Rule B" },
|
||||
@@ -558,11 +558,15 @@ test("OpenAI -> Antigravity uses the Claude bridge for Claude-family models", ()
|
||||
|
||||
assert.equal(result.project, "proj-claude");
|
||||
assert.equal(result.userAgent, "antigravity");
|
||||
assert.equal((result as any).request?.systemInstruction.role, "system");
|
||||
assert.equal(
|
||||
(result as any).request?.systemInstruction.parts[0].text,
|
||||
ANTIGRAVITY_DEFAULT_SYSTEM
|
||||
);
|
||||
assert.equal((result as any).request?.systemInstruction.parts[1].text, "Project rules");
|
||||
assert.equal((result as any).request?.generationConfig.maxOutputTokens, 8192);
|
||||
assert.equal((result as any).request?.generationConfig.temperature, 1);
|
||||
assert.equal((result as any).request?.generationConfig.thinkingConfig, undefined);
|
||||
|
||||
const modelTurn = result.request.contents.find(
|
||||
(content) => content.role === "model" && content.parts.some((part) => part.functionCall)
|
||||
@@ -624,6 +628,7 @@ test("OpenAI -> Antigravity Claude bridge sanitizes long names and preserves res
|
||||
|
||||
const sanitizedToolName = (result as any).request?.tools[0].functionDeclarations[0].name;
|
||||
assert.equal(sanitizedToolName.length, 64);
|
||||
assert.match(sanitizedToolName, /^[a-zA-Z0-9_]+$/);
|
||||
assert.equal((result as any)._toolNameMap.get(sanitizedToolName), longToolName);
|
||||
|
||||
const modelTurn = result.request.contents.find(
|
||||
@@ -638,3 +643,22 @@ test("OpenAI -> Antigravity Claude bridge sanitizes long names and preserves res
|
||||
assert.ok(toolTurn, "expected a tool response turn");
|
||||
assert.equal(getFunctionResponse(toolTurn.parts[0]).name, sanitizedToolName);
|
||||
});
|
||||
|
||||
test("OpenAI -> Antigravity Claude bridge clamps output tokens and keeps thinking budget separate", () => {
|
||||
const result = openaiToAntigravityRequest(
|
||||
"claude-3-7-sonnet",
|
||||
{
|
||||
messages: [{ role: "user", content: "Summarize this" }],
|
||||
max_completion_tokens: 32000,
|
||||
reasoning_effort: "high",
|
||||
},
|
||||
false,
|
||||
{ projectId: "proj-claude-thinking" } as any
|
||||
);
|
||||
|
||||
assert.equal((result as any).request?.generationConfig.maxOutputTokens, 8192);
|
||||
assert.deepEqual((result as any).request?.generationConfig.thinkingConfig, {
|
||||
thinkingBudget: 131072,
|
||||
includeThoughts: true,
|
||||
});
|
||||
});
|
||||
|
||||
55
tests/unit/v1beta-models-route.test.ts
Normal file
55
tests/unit/v1beta-models-route.test.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
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-v1beta-models-"));
|
||||
process.env.DATA_DIR = TEST_DATA_DIR;
|
||||
process.env.API_KEY_SECRET = process.env.API_KEY_SECRET || "v1beta-models-test-secret";
|
||||
|
||||
const core = await import("../../src/lib/db/core.ts");
|
||||
const modelsDb = await import("../../src/lib/db/models.ts");
|
||||
const v1betaModelsRoute = await import("../../src/app/api/v1beta/models/route.ts");
|
||||
|
||||
async function resetStorage() {
|
||||
core.resetDbInstance();
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
test.beforeEach(async () => {
|
||||
await resetStorage();
|
||||
});
|
||||
|
||||
test.after(async () => {
|
||||
core.resetDbInstance();
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test("v1beta models route deduplicates custom models against built-in and synced entries", async () => {
|
||||
await modelsDb.replaceSyncedAvailableModelsForConnection("openai", "conn-main", [
|
||||
{
|
||||
id: "gpt-4o",
|
||||
name: "GPT-4o From Sync",
|
||||
source: "imported",
|
||||
},
|
||||
{
|
||||
id: "review-sync-only",
|
||||
name: "Review Sync Only",
|
||||
source: "imported",
|
||||
},
|
||||
]);
|
||||
await modelsDb.addCustomModel("openai", "gpt-4o", "GPT-4o Manual Duplicate");
|
||||
await modelsDb.addCustomModel("openai", "review-sync-only", "Review Manual Duplicate");
|
||||
await modelsDb.addCustomModel("openai", "review-manual-only", "Review Manual Only");
|
||||
|
||||
const response = await v1betaModelsRoute.GET();
|
||||
const body = (await response.json()) as { models: Array<{ name: string }> };
|
||||
const names = body.models.map((model) => model.name);
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
assert.equal(names.filter((name) => name === "models/openai/gpt-4o").length, 1);
|
||||
assert.equal(names.filter((name) => name === "models/openai/review-sync-only").length, 1);
|
||||
assert.equal(names.filter((name) => name === "models/openai/review-manual-only").length, 1);
|
||||
});
|
||||
@@ -16,7 +16,7 @@ test("xiaomi-mimo registry uses the current default base URL and MiMo V2 models"
|
||||
assert.equal(entry.baseUrl, "https://api.xiaomimimo.com/v1");
|
||||
assert.deepEqual(
|
||||
entry.models.map((model) => model.id),
|
||||
["mimo-v2-pro", "mimo-v2-omni", "mimo-v2-tts"]
|
||||
["mimo-v2.5-pro", "mimo-v2.5", "mimo-v2-omni", "mimo-v2-flash"]
|
||||
);
|
||||
});
|
||||
|
||||
@@ -24,7 +24,7 @@ test("xiaomi-mimo executor appends /chat/completions for regional base URLs", ()
|
||||
const executor = new DefaultExecutor("xiaomi-mimo");
|
||||
|
||||
assert.equal(
|
||||
executor.buildUrl("mimo-v2-pro", true, 0, {
|
||||
executor.buildUrl("mimo-v2.5", true, 0, {
|
||||
providerSpecificData: {
|
||||
baseUrl: "https://token-plan-ams.xiaomimimo.com/v1",
|
||||
},
|
||||
@@ -33,7 +33,7 @@ test("xiaomi-mimo executor appends /chat/completions for regional base URLs", ()
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
executor.buildUrl("mimo-v2-pro", true, 0, {
|
||||
executor.buildUrl("mimo-v2.5", true, 0, {
|
||||
providerSpecificData: {
|
||||
baseUrl: "https://token-plan-cn.xiaomimimo.com/v1/chat/completions",
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user