feat: Add OpenAI-compatible provider nodes

- Support multiple OpenAI-compatible providers with custom prefix/baseUrl
- Add provider nodes CRUD (create/read/update/delete)
- URL building: baseUrl + /chat/completions or /responses
- Model import from /models endpoint
- API key validation via /models
- Usage type safety across all translators
- OAuth token auto-refresh for expired tokens
This commit is contained in:
decolua
2026-02-02 19:45:12 +07:00
parent 1b14c9d66b
commit 0a28f9f924
25 changed files with 1276 additions and 151 deletions
+25 -2
View File
@@ -1,6 +1,7 @@
import { NextResponse } from "next/server";
import { getProviderConnections, createProviderConnection, isCloudEnabled } from "@/models";
import { getProviderConnections, createProviderConnection, getProviderNodeById, isCloudEnabled } from "@/models";
import { APIKEY_PROVIDERS } from "@/shared/constants/config";
import { isOpenAICompatibleProvider } from "@/shared/constants/providers";
import { getConsistentMachineId } from "@/shared/utils/machineId";
import { syncToCloud } from "@/app/api/sync/cloud/route";
@@ -32,7 +33,7 @@ export async function POST(request) {
const { provider, apiKey, name, priority, globalPriority, defaultModel, testStatus } = body;
// Validation
if (!provider || !APIKEY_PROVIDERS[provider]) {
if (!provider || (!APIKEY_PROVIDERS[provider] && !isOpenAICompatibleProvider(provider))) {
return NextResponse.json({ error: "Invalid provider" }, { status: 400 });
}
if (!apiKey) {
@@ -42,6 +43,27 @@ export async function POST(request) {
return NextResponse.json({ error: "Name is required" }, { status: 400 });
}
let providerSpecificData = null;
if (isOpenAICompatibleProvider(provider)) {
const node = await getProviderNodeById(provider);
if (!node) {
return NextResponse.json({ error: "OpenAI Compatible node not found" }, { status: 404 });
}
const existingConnections = await getProviderConnections({ provider });
if (existingConnections.length > 0) {
return NextResponse.json({ error: "Only one connection is allowed for this OpenAI Compatible node" }, { status: 400 });
}
providerSpecificData = {
prefix: node.prefix,
apiType: node.apiType,
baseUrl: node.baseUrl,
nodeName: node.name,
};
}
const newConnection = await createProviderConnection({
provider,
authType: "apikey",
@@ -50,6 +72,7 @@ export async function POST(request) {
priority: priority || 1,
globalPriority: globalPriority || null,
defaultModel: defaultModel || null,
providerSpecificData,
isActive: true,
testStatus: testStatus || "unknown",
});