Merge branch 'main' into refactor-split-ports

This commit is contained in:
Steven
2026-02-26 15:17:56 +00:00
committed by GitHub
182 changed files with 13566 additions and 4012 deletions

View File

@@ -59,11 +59,23 @@ export async function GET() {
try {
const statuses = {};
// Run all runtime checks in parallel
// Run all runtime checks in parallel with individual timeouts
const RUNTIME_CHECK_TIMEOUT = 5000; // 5s per tool max
await Promise.all(
CLI_TOOL_IDS.map(async (toolId) => {
try {
const runtime = await getCliRuntimeStatus(toolId);
const runtime = (await Promise.race([
getCliRuntimeStatus(toolId),
new Promise((_, reject) =>
setTimeout(() => reject(new Error("Timeout")), RUNTIME_CHECK_TIMEOUT)
),
])) as {
installed: boolean;
runnable: boolean;
command?: string;
commandPath?: string;
reason?: string;
};
statuses[toolId] = {
installed: runtime.installed,
runnable: runtime.runnable,
@@ -75,7 +87,7 @@ export async function GET() {
statuses[toolId] = {
installed: false,
runnable: false,
reason: error.message,
reason: error.message || "Check failed",
};
}
})

View File

@@ -1,8 +1,71 @@
import { NextResponse } from "next/server";
import { deleteApiKey, isCloudEnabled } from "@/lib/localDb";
import {
deleteApiKey,
getApiKeyById,
updateApiKeyPermissions,
isCloudEnabled,
} from "@/lib/localDb";
import { getConsistentMachineId } from "@/shared/utils/machineId";
import { syncToCloud } from "@/lib/cloudSync";
// GET /api/keys/[id] - Get single API key
export async function GET(request, { params }) {
try {
const { id } = await params;
const key = await getApiKeyById(id);
if (!key) {
return NextResponse.json({ error: "Key not found" }, { status: 404 });
}
// Mask the key value
return NextResponse.json({
...key,
key: key.key ? key.key.slice(0, 8) + "****" + key.key.slice(-4) : null,
});
} catch (error) {
console.log("Error fetching key:", error);
return NextResponse.json({ error: "Failed to fetch key" }, { status: 500 });
}
}
// PATCH /api/keys/[id] - Update API key permissions
export async function PATCH(request, { params }) {
try {
const { id } = await params;
const body = await request.json();
const { allowedModels } = body;
// Validate allowedModels is an array
if (!Array.isArray(allowedModels)) {
return NextResponse.json({ error: "allowedModels must be an array" }, { status: 400 });
}
// Validate each model ID is a string
for (const model of allowedModels) {
if (typeof model !== "string") {
return NextResponse.json({ error: "Each model ID must be a string" }, { status: 400 });
}
}
const updated = await updateApiKeyPermissions(id, allowedModels);
if (!updated) {
return NextResponse.json({ error: "Key not found" }, { status: 404 });
}
// Auto sync to Cloud if enabled
await syncKeysToCloudIfEnabled();
return NextResponse.json({
message: "Permissions updated successfully",
allowedModels,
});
} catch (error) {
console.log("Error updating key permissions:", error);
return NextResponse.json({ error: "Failed to update permissions" }, { status: 500 });
}
}
// DELETE /api/keys/[id] - Delete API key
export async function DELETE(request, { params }) {
try {

View File

@@ -2,10 +2,16 @@ import { NextResponse } from "next/server";
import { getModelAliases, setModelAlias, deleteModelAlias, isCloudEnabled } from "@/models";
import { getConsistentMachineId } from "@/shared/utils/machineId";
import { syncToCloud } from "@/lib/cloudSync";
import { isAuthenticated } from "@/shared/utils/apiAuth";
// GET /api/models/alias - Get all aliases
export async function GET() {
export async function GET(request) {
try {
// Require authentication for security
if (!(await isAuthenticated(request))) {
return NextResponse.json({ error: "Authentication required" }, { status: 401 });
}
const aliases = await getModelAliases();
return NextResponse.json({ aliases });
} catch (error) {
@@ -17,6 +23,11 @@ export async function GET() {
// PUT /api/models/alias - Set model alias
export async function PUT(request) {
try {
// Require authentication for security
if (!(await isAuthenticated(request))) {
return NextResponse.json({ error: "Authentication required" }, { status: 401 });
}
const body = await request.json();
const { model, alias } = body;
@@ -37,6 +48,11 @@ export async function PUT(request) {
// DELETE /api/models/alias?alias=xxx - Delete alias
export async function DELETE(request) {
try {
// Require authentication for security
if (!(await isAuthenticated(request))) {
return NextResponse.json({ error: "Authentication required" }, { status: 401 });
}
const { searchParams } = new URL(request.url);
const alias = searchParams.get("alias");

View File

@@ -10,6 +10,8 @@ import { createProviderConnection, isCloudEnabled } from "@/models";
import { getConsistentMachineId } from "@/shared/utils/machineId";
import { syncToCloud } from "@/lib/cloudSync";
import { startLocalServer } from "@/lib/oauth/utils/server";
import { getProxyConfig } from "@/lib/localDb";
import { runWithProxyContext } from "@omniroute/open-sse/utils/proxyFetch.ts";
// Use globalThis to persist callback server state across Next.js HMR reloads
if (!globalThis.__codexCallbackState) {
@@ -23,7 +25,10 @@ if (!globalThis.__codexCallbackState) {
// GET /api/oauth/[provider]/authorize - Generate auth URL
// GET /api/oauth/[provider]/device-code - Request device code (for device_code flow)
export async function GET(request: Request, { params }: { params: Promise<{ provider: string; action: string }> }) {
export async function GET(
request: Request,
{ params }: { params: Promise<{ provider: string; action: string }> }
) {
try {
const { provider, action } = await params;
const { searchParams } = new URL(request.url);
@@ -141,7 +146,10 @@ async function handleStartCallbackServer(provider: string, searchParams: URLSear
// POST /api/oauth/[provider]/exchange - Exchange code for tokens and save
// POST /api/oauth/[provider]/poll - Poll for token (device_code flow)
export async function POST(request: Request, { params }: { params: Promise<{ provider: string; action: string }> }) {
export async function POST(
request: Request,
{ params }: { params: Promise<{ provider: string; action: string }> }
) {
try {
const { provider, action } = await params;
const body = await request.json();
@@ -153,8 +161,14 @@ export async function POST(request: Request, { params }: { params: Promise<{ pro
return NextResponse.json({ error: "Missing required fields" }, { status: 400 });
}
// Exchange code for tokens
const tokenData = await exchangeTokens(provider, code, redirectUri, codeVerifier, state);
// Resolve proxy for this provider (provider-level → global → direct)
const proxyConfig = await getProxyConfig();
const proxy = proxyConfig.providers?.[provider] || proxyConfig.global || null;
// Exchange code for tokens (through proxy if configured)
const tokenData = await runWithProxyContext(proxy, () =>
exchangeTokens(provider, code, redirectUri, codeVerifier, state)
);
// Save to database
const connection: any = await createProviderConnection({
@@ -289,13 +303,13 @@ export async function POST(request: Request, { params }: { params: Promise<{ pro
}
try {
// Exchange code for tokens
const tokenData = await exchangeTokens(
provider,
params.code,
redirectUri,
codeVerifier,
params.state
// Resolve proxy for this provider
const proxyConfig = await getProxyConfig();
const proxy = proxyConfig.providers?.[provider] || proxyConfig.global || null;
// Exchange code for tokens (through proxy if configured)
const tokenData = await runWithProxyContext(proxy, () =>
exchangeTokens(provider, params.code, redirectUri, codeVerifier, params.state)
);
// Save to database

View File

@@ -4,6 +4,7 @@ import {
addCustomModel,
removeCustomModel,
} from "@/lib/localDb";
import { isAuthenticated } from "@/shared/utils/apiAuth";
/**
* GET /api/provider-models?provider=<id>
@@ -11,6 +12,14 @@ import {
*/
export async function GET(request) {
try {
// Require authentication for security
if (!(await isAuthenticated(request))) {
return Response.json(
{ error: { message: "Authentication required", type: "invalid_api_key" } },
{ status: 401 }
);
}
const { searchParams } = new URL(request.url);
const provider = searchParams.get("provider");
@@ -31,6 +40,14 @@ export async function GET(request) {
*/
export async function POST(request) {
try {
// Require authentication for security
if (!(await isAuthenticated(request))) {
return Response.json(
{ error: { message: "Authentication required", type: "invalid_api_key" } },
{ status: 401 }
);
}
const body = await request.json();
const { provider, modelId, modelName, source } = body;
@@ -56,6 +73,14 @@ export async function POST(request) {
*/
export async function DELETE(request) {
try {
// Require authentication for security
if (!(await isAuthenticated(request))) {
return Response.json(
{ error: { message: "Authentication required", type: "invalid_api_key" } },
{ status: 401 }
);
}
const { searchParams } = new URL(request.url);
const provider = searchParams.get("provider");
const modelId = searchParams.get("model");

View File

@@ -198,6 +198,14 @@ const PROVIDER_MODELS_CONFIG = {
authPrefix: "Bearer ",
parseResponse: (data) => data.data || data.models || [],
},
kilocode: {
url: "https://api.kilo.ai/api/openrouter/models",
method: "GET",
headers: { "Content-Type": "application/json" },
authHeader: "Authorization",
authPrefix: "Bearer ",
parseResponse: (data) => data.data || data.models || [],
},
};
/**
@@ -220,8 +228,17 @@ export async function GET(request, { params }) {
{ status: 400 }
);
}
const url = `${baseUrl.replace(/\/$/, "")}/models`;
const response = await fetch(url, {
let modelsUrl = baseUrl.replace(/\/$/, "");
if (modelsUrl.endsWith("/chat/completions")) {
modelsUrl = modelsUrl.slice(0, -17) + "/models";
} else if (modelsUrl.endsWith("/completions")) {
modelsUrl = modelsUrl.slice(0, -12) + "/models";
} else {
modelsUrl = `${modelsUrl}/models`;
}
const response = await fetch(modelsUrl, {
method: "GET",
headers: {
"Content-Type": "application/json",

View File

@@ -1,5 +1,10 @@
import { NextResponse } from "next/server";
import { getProviderConnectionById, updateProviderConnection, isCloudEnabled } from "@/lib/localDb";
import {
getProviderConnectionById,
updateProviderConnection,
isCloudEnabled,
resolveProxyForConnection,
} from "@/lib/localDb";
import { getConsistentMachineId } from "@/shared/utils/machineId";
import { syncToCloud } from "@/lib/cloudSync";
import { validateProviderApiKey } from "@/lib/providers/validation";
@@ -8,6 +13,7 @@ import { getCliRuntimeStatus } from "@/shared/services/cliRuntime";
import { getAccessToken } from "@omniroute/open-sse/services/tokenRefresh.ts";
import { saveCallLog } from "@/lib/usageDb";
import { logProxyEvent } from "@/lib/proxyLogger";
import { runWithProxyContext } from "@omniroute/open-sse/utils/proxyFetch.ts";
// OAuth provider test endpoints
const OAUTH_TEST_CONFIG = {
@@ -91,7 +97,12 @@ function toSafeMessage(value: any, fallback = "Unknown error"): string {
return trimmed || fallback;
}
function makeDiagnosis(type: string, source: string, message: string | null, code: string | null = null) {
function makeDiagnosis(
type: string,
source: string,
message: string | null,
code: string | null = null
) {
return {
type,
source,
@@ -100,7 +111,17 @@ function makeDiagnosis(type: string, source: string, message: string | null, cod
};
}
function classifyFailure({ error, statusCode = null, refreshFailed = false, unsupported = false }: { error: string; statusCode?: number | null; refreshFailed?: boolean; unsupported?: boolean }) {
function classifyFailure({
error,
statusCode = null,
refreshFailed = false,
unsupported = false,
}: {
error: string;
statusCode?: number | null;
refreshFailed?: boolean;
unsupported?: boolean;
}) {
const message = toSafeMessage(error, "Connection test failed");
const normalized = message.toLowerCase();
const numericStatus = Number.isFinite(statusCode) ? Number(statusCode) : null;
@@ -510,6 +531,14 @@ export async function testSingleConnection(connectionId: string) {
return { valid: false, error: "Connection not found", diagnosis: null, latencyMs: 0 };
}
// Resolve proxy for this connection (key → combo → provider → global → direct)
let proxyInfo: any = null;
try {
proxyInfo = await resolveProxyForConnection(connectionId);
} catch (proxyErr: any) {
console.log(`[ConnectionTest] Failed to resolve proxy for ${connectionId}:`, proxyErr?.message);
}
let result;
const startTime = Date.now();
const runtime = await getProviderRuntimeStatus(connection.provider);
@@ -522,9 +551,13 @@ export async function testSingleConnection(connectionId: string) {
diagnosis: (runtime as any).diagnosis,
};
} else if (connection.authType === "apikey") {
result = await testApiKeyConnection(connection);
result = await runWithProxyContext(proxyInfo?.proxy || null, () =>
testApiKeyConnection(connection)
);
} else {
result = await testOAuthConnection(connection);
result = await runWithProxyContext(proxyInfo?.proxy || null, () =>
testOAuthConnection(connection)
);
}
const latencyMs = Date.now() - startTime;
@@ -591,9 +624,9 @@ export async function testSingleConnection(connectionId: string) {
try {
logProxyEvent({
status: result.valid ? "success" : "error",
proxy: null,
level: "provider-test",
levelId: null,
proxy: proxyInfo?.proxy || null,
level: proxyInfo?.level || "provider-test",
levelId: proxyInfo?.levelId || null,
provider: connection.provider,
targetUrl: `${connection.provider}/connection-test`,
latencyMs,

View File

@@ -1,5 +1,6 @@
import { NextResponse } from "next/server";
import { getSettings, updateSettings } from "@/lib/localDb";
import { clearHealthCheckLogCache } from "@/lib/tokenHealthCheck";
import bcrypt from "bcryptjs";
import { updateSettingsSchema, validateBody } from "@/shared/validation/schemas";
import { getRuntimePorts } from "@/lib/runtime/ports";
@@ -66,6 +67,12 @@ export async function PATCH(request) {
}
const settings = await updateSettings(body);
// Clear health check log cache if that setting was updated
if ("hideHealthCheckLogs" in body) {
clearHealthCheckLogCache();
}
const { password, ...safeSettings } = settings;
return NextResponse.json(safeSettings);
} catch (error) {

View File

@@ -4,6 +4,7 @@ import { getProviderCredentials, extractApiKey, isValidApiKey } from "@/sse/serv
import { parseSpeechModel } from "@omniroute/open-sse/config/audioRegistry.ts";
import { errorResponse } from "@omniroute/open-sse/utils/error.ts";
import { HTTP_STATUS } from "@omniroute/open-sse/config/constants.ts";
import { enforceApiKeyPolicy } from "@/shared/utils/apiKeyPolicy";
/**
* Handle CORS preflight
@@ -41,6 +42,10 @@ export async function POST(request) {
return errorResponse(HTTP_STATUS.BAD_REQUEST, "Missing model");
}
// Enforce API key policies (model restrictions + budget limits)
const policy = await enforceApiKeyPolicy(request, body.model);
if (policy.rejection) return policy.rejection;
const { provider } = parseSpeechModel(body.model);
if (!provider) {
return errorResponse(

View File

@@ -4,6 +4,7 @@ import { getProviderCredentials, extractApiKey, isValidApiKey } from "@/sse/serv
import { parseTranscriptionModel } from "@omniroute/open-sse/config/audioRegistry.ts";
import { errorResponse } from "@omniroute/open-sse/utils/error.ts";
import { HTTP_STATUS } from "@omniroute/open-sse/config/constants.ts";
import { enforceApiKeyPolicy } from "@/shared/utils/apiKeyPolicy";
/**
* Handle CORS preflight
@@ -43,6 +44,10 @@ export async function POST(request) {
return errorResponse(HTTP_STATUS.BAD_REQUEST, "Missing model");
}
// Enforce API key policies (model restrictions + budget limits)
const policy = await enforceApiKeyPolicy(request, model as string);
if (policy.rejection) return policy.rejection;
const { provider } = parseTranscriptionModel(model);
if (!provider) {
return errorResponse(

View File

@@ -9,6 +9,7 @@ import { errorResponse } from "@omniroute/open-sse/utils/error.ts";
import { HTTP_STATUS } from "@omniroute/open-sse/config/constants.ts";
import * as log from "@/sse/utils/logger";
import { toJsonErrorPayload } from "@/shared/utils/upstreamError";
import { enforceApiKeyPolicy } from "@/shared/utils/apiKeyPolicy";
/**
* Handle CORS preflight
@@ -78,6 +79,10 @@ export async function POST(request) {
return errorResponse(HTTP_STATUS.BAD_REQUEST, "Missing input");
}
// Enforce API key policies (model restrictions + budget limits)
const policy = await enforceApiKeyPolicy(request, body.model);
if (policy.rejection) return policy.rejection;
// Parse model to get provider
const { provider } = parseEmbeddingModel(body.model);
if (!provider) {

View File

@@ -6,6 +6,7 @@ import { errorResponse } from "@omniroute/open-sse/utils/error.ts";
import { HTTP_STATUS } from "@omniroute/open-sse/config/constants.ts";
import * as log from "@/sse/utils/logger";
import { toJsonErrorPayload } from "@/shared/utils/upstreamError";
import { enforceApiKeyPolicy } from "@/shared/utils/apiKeyPolicy";
/**
* Handle CORS preflight
@@ -75,6 +76,10 @@ export async function POST(request) {
return errorResponse(HTTP_STATUS.BAD_REQUEST, "Invalid prompt: expected a non-empty string");
}
// Enforce API key policies (model restrictions + budget limits)
const policy = await enforceApiKeyPolicy(request, body.model);
if (policy.rejection) return policy.rejection;
// Parse model to get provider
const { provider } = parseImageModel(body.model);
if (!provider) {

View File

@@ -1,8 +1,14 @@
import { CORS_ORIGIN } from "@/shared/utils/cors";
import { PROVIDER_MODELS, PROVIDER_ID_TO_ALIAS } from "@/shared/constants/models";
import { AI_PROVIDERS } from "@/shared/constants/providers";
import { getProviderConnections, getCombos, getAllCustomModels, getSettings } from "@/lib/localDb";
import { extractApiKey, isValidApiKey } from "@/sse/services/auth";
import {
getProviderConnections,
getCombos,
getAllCustomModels,
getSettings,
getProviderNodes,
} from "@/lib/localDb";
import { isAuthenticated } from "@/shared/utils/apiAuth";
import { getAllEmbeddingModels } from "@omniroute/open-sse/config/embeddingRegistry.ts";
import { getAllImageModels } from "@omniroute/open-sse/config/imageRegistry.ts";
import { getAllRerankModels } from "@omniroute/open-sse/config/rerankRegistry.ts";
@@ -97,16 +103,25 @@ export async function OPTIONS() {
*/
export async function GET(request: Request) {
try {
// Issue #100: Optionally require API key for /models (security hardening)
// When enabled, unauthenticated requests get 404 to hide endpoint existence
// Issue #100: Optionally require authentication for /models (security hardening)
// When enabled, unauthenticated requests get 401 with proper error response.
// Supports API key (Bearer token) for external clients and JWT cookie for dashboard.
let settings: Record<string, any> = {};
try {
settings = await getSettings();
} catch {}
if (settings.requireAuthForModels === true) {
const apiKey = extractApiKey(request);
if (!apiKey || !(await isValidApiKey(apiKey))) {
return new Response("Not Found", { status: 404 });
if (!(await isAuthenticated(request))) {
return Response.json(
{
error: {
message: "Authentication required",
type: "invalid_request_error",
code: "invalid_api_key",
},
},
{ status: 401 }
);
}
}
@@ -130,6 +145,26 @@ export async function GET(request: Request) {
console.log("Could not fetch providers, showing only combos/custom models");
}
// Get provider nodes (for compatible providers with custom prefixes)
let providerNodes = [];
try {
providerNodes = await getProviderNodes();
} catch (e) {
console.log("Could not fetch provider nodes");
}
// Build map of provider node ID to prefix and type for compatible providers
const providerIdToPrefix: Record<string, string> = {};
const nodeIdToProviderType: Record<string, string> = {};
for (const node of providerNodes) {
if (node.prefix) {
providerIdToPrefix[node.id] = node.prefix;
}
if (node.type) {
nodeIdToProviderType[node.id] = node.type;
}
}
// Get combos
let combos = [];
try {
@@ -279,14 +314,19 @@ export async function GET(request: Request) {
try {
const customModelsMap: Record<string, any[]> = await getAllCustomModels();
for (const [providerId, providerCustomModels] of Object.entries(customModelsMap)) {
const alias = providerIdToAlias[providerId] || providerId;
// For compatible providers, use the prefix from provider nodes
const prefix = providerIdToPrefix[providerId];
const alias = prefix || providerIdToAlias[providerId] || providerId;
const canonicalProviderId = FALLBACK_ALIAS_TO_PROVIDER[alias] || providerId;
// Only include if provider is active — check alias, canonical ID, or raw providerId
// (raw check needed for OpenAI-compatible providers whose ID isn't in the alias map)
// Only include if provider is active — check alias, canonical ID, raw providerId,
// or the parent provider type (for compatible providers whose node ID is a UUID)
const parentProviderType = nodeIdToProviderType[providerId];
if (
!activeAliases.has(alias) &&
!activeAliases.has(canonicalProviderId) &&
!activeAliases.has(providerId)
!activeAliases.has(providerId) &&
!(parentProviderType && activeAliases.has(parentProviderType))
)
continue;
@@ -306,7 +346,8 @@ export async function GET(request: Request) {
custom: true,
});
if (canonicalProviderId !== alias) {
// Only add provider-prefixed version if different from alias
if (canonicalProviderId !== alias && !prefix) {
const providerPrefixedId = `${canonicalProviderId}/${model.id}`;
if (models.some((m) => m.id === providerPrefixedId)) continue;
models.push({

View File

@@ -4,6 +4,7 @@ import { getProviderCredentials, extractApiKey, isValidApiKey } from "@/sse/serv
import { parseModerationModel } from "@omniroute/open-sse/config/moderationRegistry.ts";
import { errorResponse } from "@omniroute/open-sse/utils/error.ts";
import { HTTP_STATUS } from "@omniroute/open-sse/config/constants.ts";
import { enforceApiKeyPolicy } from "@/shared/utils/apiKeyPolicy";
/**
* Handle CORS preflight
@@ -38,6 +39,11 @@ export async function POST(request) {
}
const model = body.model || "omni-moderation-latest";
// Enforce API key policies (model restrictions + budget limits)
const policy = await enforceApiKeyPolicy(request, model);
if (policy.rejection) return policy.rejection;
const { provider } = parseModerationModel(model);
// Default to openai if no provider prefix

View File

@@ -5,6 +5,7 @@ import { getRegistryEntry } from "@omniroute/open-sse/config/providerRegistry.ts
import { getProviderCredentials, extractApiKey, isValidApiKey } from "@/sse/services/auth";
import { handleEmbedding } from "@omniroute/open-sse/handlers/embeddings.ts";
import * as log from "@/sse/utils/logger";
import { enforceApiKeyPolicy } from "@/shared/utils/apiKeyPolicy";
/**
* Handle CORS preflight
@@ -53,6 +54,10 @@ export async function POST(request, { params }) {
body.model = `${providerAlias}/${body.model}`;
}
// Enforce API key policies (model restrictions + budget limits)
const policy = await enforceApiKeyPolicy(request, body.model);
if (policy.rejection) return policy.rejection;
// Validate provider match
if (body.model) {
const prefix = body.model.split("/")[0];

View File

@@ -6,6 +6,7 @@ import { getProviderCredentials, extractApiKey, isValidApiKey } from "@/sse/serv
import { getImageProvider } from "@omniroute/open-sse/config/imageRegistry.ts";
import * as log from "@/sse/utils/logger";
import { toJsonErrorPayload } from "@/shared/utils/upstreamError";
import { enforceApiKeyPolicy } from "@/shared/utils/apiKeyPolicy";
/**
* Handle CORS preflight
@@ -60,6 +61,10 @@ export async function POST(request, { params }) {
body.model = `${rawProvider}/${body.model}`;
}
// Enforce API key policies (model restrictions + budget limits)
const policy = await enforceApiKeyPolicy(request, body.model);
if (policy.rejection) return policy.rejection;
// Validate provider match
const modelProvider = body.model.split("/")[0];
if (modelProvider !== rawProvider) {

View File

@@ -4,6 +4,7 @@ import { getProviderCredentials, extractApiKey, isValidApiKey } from "@/sse/serv
import { parseRerankModel } from "@omniroute/open-sse/config/rerankRegistry.ts";
import { errorResponse } from "@omniroute/open-sse/utils/error.ts";
import { HTTP_STATUS } from "@omniroute/open-sse/config/constants.ts";
import { enforceApiKeyPolicy } from "@/shared/utils/apiKeyPolicy";
/**
* Handle CORS preflight
@@ -44,6 +45,10 @@ export async function POST(request) {
return errorResponse(HTTP_STATUS.BAD_REQUEST, "Missing model");
}
// Enforce API key policies (model restrictions + budget limits)
const policy = await enforceApiKeyPolicy(request, body.model);
if (policy.rejection) return policy.rejection;
const { provider } = parseRerankModel(body.model);
if (!provider) {
return errorResponse(