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:
77
src/app/api/provider-nodes/[id]/route.js
Normal file
77
src/app/api/provider-nodes/[id]/route.js
Normal file
@@ -0,0 +1,77 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { deleteProviderConnectionsByProvider, deleteProviderNode, getProviderConnections, getProviderNodeById, updateProviderConnection, updateProviderNode } from "@/models";
|
||||
|
||||
// PUT /api/provider-nodes/[id] - Update provider node
|
||||
export async function PUT(request, { params }) {
|
||||
try {
|
||||
const { id } = await params;
|
||||
const body = await request.json();
|
||||
const { name, prefix, apiType, baseUrl } = body;
|
||||
const node = await getProviderNodeById(id);
|
||||
|
||||
if (!node) {
|
||||
return NextResponse.json({ error: "Provider node not found" }, { status: 404 });
|
||||
}
|
||||
|
||||
if (!name?.trim()) {
|
||||
return NextResponse.json({ error: "Name is required" }, { status: 400 });
|
||||
}
|
||||
|
||||
if (!prefix?.trim()) {
|
||||
return NextResponse.json({ error: "Prefix is required" }, { status: 400 });
|
||||
}
|
||||
|
||||
if (!apiType || !["chat", "responses"].includes(apiType)) {
|
||||
return NextResponse.json({ error: "Invalid OpenAI compatible API type" }, { status: 400 });
|
||||
}
|
||||
|
||||
if (!baseUrl?.trim()) {
|
||||
return NextResponse.json({ error: "Base URL is required" }, { status: 400 });
|
||||
}
|
||||
|
||||
const updated = await updateProviderNode(id, {
|
||||
name: name.trim(),
|
||||
prefix: prefix.trim(),
|
||||
apiType,
|
||||
baseUrl: baseUrl.trim(),
|
||||
});
|
||||
|
||||
const connections = await getProviderConnections({ provider: id });
|
||||
await Promise.all(connections.map((connection) => (
|
||||
updateProviderConnection(connection.id, {
|
||||
providerSpecificData: {
|
||||
...(connection.providerSpecificData || {}),
|
||||
prefix: prefix.trim(),
|
||||
apiType,
|
||||
baseUrl: baseUrl.trim(),
|
||||
nodeName: updated.name,
|
||||
}
|
||||
})
|
||||
)));
|
||||
|
||||
return NextResponse.json({ node: updated });
|
||||
} catch (error) {
|
||||
console.log("Error updating provider node:", error);
|
||||
return NextResponse.json({ error: "Failed to update provider node" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
// DELETE /api/provider-nodes/[id] - Delete provider node and its connections
|
||||
export async function DELETE(request, { params }) {
|
||||
try {
|
||||
const { id } = await params;
|
||||
const node = await getProviderNodeById(id);
|
||||
|
||||
if (!node) {
|
||||
return NextResponse.json({ error: "Provider node not found" }, { status: 404 });
|
||||
}
|
||||
|
||||
await deleteProviderConnectionsByProvider(id);
|
||||
await deleteProviderNode(id);
|
||||
|
||||
return NextResponse.json({ success: true });
|
||||
} catch (error) {
|
||||
console.log("Error deleting provider node:", error);
|
||||
return NextResponse.json({ error: "Failed to delete provider node" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
52
src/app/api/provider-nodes/route.js
Normal file
52
src/app/api/provider-nodes/route.js
Normal file
@@ -0,0 +1,52 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { createProviderNode, getProviderNodes } from "@/models";
|
||||
import { OPENAI_COMPATIBLE_PREFIX } from "@/shared/constants/providers";
|
||||
|
||||
const OPENAI_COMPATIBLE_DEFAULTS = {
|
||||
baseUrl: "https://api.openai.com/v1",
|
||||
};
|
||||
|
||||
// GET /api/provider-nodes - List all provider nodes
|
||||
export async function GET() {
|
||||
try {
|
||||
const nodes = await getProviderNodes();
|
||||
return NextResponse.json({ nodes });
|
||||
} catch (error) {
|
||||
console.log("Error fetching provider nodes:", error);
|
||||
return NextResponse.json({ error: "Failed to fetch provider nodes" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
// POST /api/provider-nodes - Create provider node
|
||||
export async function POST(request) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { name, prefix, apiType, baseUrl } = body;
|
||||
|
||||
if (!name?.trim()) {
|
||||
return NextResponse.json({ error: "Name is required" }, { status: 400 });
|
||||
}
|
||||
|
||||
if (!prefix?.trim()) {
|
||||
return NextResponse.json({ error: "Prefix is required" }, { status: 400 });
|
||||
}
|
||||
|
||||
if (!apiType || !["chat", "responses"].includes(apiType)) {
|
||||
return NextResponse.json({ error: "Invalid OpenAI compatible API type" }, { status: 400 });
|
||||
}
|
||||
|
||||
const node = await createProviderNode({
|
||||
id: `${OPENAI_COMPATIBLE_PREFIX}${apiType}-${crypto.randomUUID()}`,
|
||||
type: "openai-compatible",
|
||||
prefix: prefix.trim(),
|
||||
apiType,
|
||||
baseUrl: (baseUrl || OPENAI_COMPATIBLE_DEFAULTS.baseUrl).trim(),
|
||||
name: name.trim(),
|
||||
});
|
||||
|
||||
return NextResponse.json({ node }, { status: 201 });
|
||||
} catch (error) {
|
||||
console.log("Error creating provider node:", error);
|
||||
return NextResponse.json({ error: "Failed to create provider node" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
23
src/app/api/provider-nodes/validate/route.js
Normal file
23
src/app/api/provider-nodes/validate/route.js
Normal file
@@ -0,0 +1,23 @@
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
// POST /api/provider-nodes/validate - Validate API key against base URL /models
|
||||
export async function POST(request) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { baseUrl, apiKey } = body;
|
||||
|
||||
if (!baseUrl || !apiKey) {
|
||||
return NextResponse.json({ error: "Base URL and API key required" }, { status: 400 });
|
||||
}
|
||||
|
||||
const modelsUrl = `${baseUrl.replace(/\/$/, "")}/models`;
|
||||
const res = await fetch(modelsUrl, {
|
||||
headers: { "Authorization": `Bearer ${apiKey}` },
|
||||
});
|
||||
|
||||
return NextResponse.json({ valid: res.ok, error: res.ok ? null : "Invalid API key" });
|
||||
} catch (error) {
|
||||
console.log("Error validating OpenAI compatible base URL:", error);
|
||||
return NextResponse.json({ error: "Validation failed" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { getProviderConnectionById } from "@/models";
|
||||
import { isOpenAICompatibleProvider } from "@/shared/constants/providers";
|
||||
|
||||
// Provider models endpoints configuration
|
||||
const PROVIDER_MODELS_CONFIG = {
|
||||
@@ -85,6 +86,39 @@ export async function GET(request, { params }) {
|
||||
return NextResponse.json({ error: "Connection not found" }, { status: 404 });
|
||||
}
|
||||
|
||||
if (isOpenAICompatibleProvider(connection.provider)) {
|
||||
const baseUrl = connection.providerSpecificData?.baseUrl;
|
||||
if (!baseUrl) {
|
||||
return NextResponse.json({ error: "No base URL configured for OpenAI compatible provider" }, { status: 400 });
|
||||
}
|
||||
const url = `${baseUrl.replace(/\/$/, "")}/models`;
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": `Bearer ${connection.apiKey}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
console.log(`Error fetching models from ${connection.provider}:`, errorText);
|
||||
return NextResponse.json(
|
||||
{ error: `Failed to fetch models: ${response.status}` },
|
||||
{ status: response.status }
|
||||
);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const models = data.data || data.models || [];
|
||||
|
||||
return NextResponse.json({
|
||||
provider: connection.provider,
|
||||
connectionId: connection.id,
|
||||
models
|
||||
});
|
||||
}
|
||||
|
||||
const config = PROVIDER_MODELS_CONFIG[connection.provider];
|
||||
if (!config) {
|
||||
return NextResponse.json(
|
||||
@@ -145,4 +179,3 @@ export async function GET(request, { params }) {
|
||||
return NextResponse.json({ error: "Failed to fetch models" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ export async function PUT(request, { params }) {
|
||||
try {
|
||||
const { id } = await params;
|
||||
const body = await request.json();
|
||||
const { name, priority, globalPriority, defaultModel, isActive, apiKey } = body;
|
||||
const { name, priority, globalPriority, defaultModel, isActive, apiKey, testStatus, lastError, lastErrorAt } = body;
|
||||
|
||||
const existing = await getProviderConnectionById(id);
|
||||
if (!existing) {
|
||||
@@ -46,6 +46,9 @@ export async function PUT(request, { params }) {
|
||||
if (defaultModel !== undefined) updateData.defaultModel = defaultModel;
|
||||
if (isActive !== undefined) updateData.isActive = isActive;
|
||||
if (apiKey && existing.authType === "apikey") updateData.apiKey = apiKey;
|
||||
if (testStatus !== undefined) updateData.testStatus = testStatus;
|
||||
if (lastError !== undefined) updateData.lastError = lastError;
|
||||
if (lastErrorAt !== undefined) updateData.lastErrorAt = lastErrorAt;
|
||||
|
||||
const updated = await updateProviderConnection(id, updateData);
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import { NextResponse } from "next/server";
|
||||
import { getProviderConnectionById, updateProviderConnection, isCloudEnabled } from "@/lib/localDb";
|
||||
import { getConsistentMachineId } from "@/shared/utils/machineId";
|
||||
import { syncToCloud } from "@/app/api/sync/cloud/route";
|
||||
import { isOpenAICompatibleProvider } from "@/shared/constants/providers";
|
||||
import {
|
||||
GEMINI_CONFIG,
|
||||
ANTIGRAVITY_CONFIG,
|
||||
@@ -68,9 +69,9 @@ const OAUTH_TEST_CONFIG = {
|
||||
async function refreshOAuthToken(connection) {
|
||||
const provider = connection.provider;
|
||||
const refreshToken = connection.refreshToken;
|
||||
|
||||
|
||||
if (!refreshToken) return null;
|
||||
|
||||
|
||||
try {
|
||||
// Google-based providers (gemini-cli, antigravity)
|
||||
if (provider === "gemini-cli" || provider === "antigravity") {
|
||||
@@ -85,9 +86,9 @@ async function refreshOAuthToken(connection) {
|
||||
refresh_token: refreshToken,
|
||||
}),
|
||||
});
|
||||
|
||||
|
||||
if (!response.ok) return null;
|
||||
|
||||
|
||||
const data = await response.json();
|
||||
return {
|
||||
accessToken: data.access_token,
|
||||
@@ -95,7 +96,7 @@ async function refreshOAuthToken(connection) {
|
||||
refreshToken: data.refresh_token || refreshToken,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
// OpenAI/Codex
|
||||
if (provider === "codex") {
|
||||
const response = await fetch(CODEX_CONFIG.tokenUrl, {
|
||||
@@ -107,9 +108,9 @@ async function refreshOAuthToken(connection) {
|
||||
refresh_token: refreshToken,
|
||||
}),
|
||||
});
|
||||
|
||||
|
||||
if (!response.ok) return null;
|
||||
|
||||
|
||||
const data = await response.json();
|
||||
return {
|
||||
accessToken: data.access_token,
|
||||
@@ -117,11 +118,11 @@ async function refreshOAuthToken(connection) {
|
||||
refreshToken: data.refresh_token || refreshToken,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
// Kiro (AWS SSO or Social auth)
|
||||
if (provider === "kiro") {
|
||||
const { clientId, clientSecret, region } = connection;
|
||||
|
||||
|
||||
// AWS SSO OIDC refresh (Builder ID or IDC)
|
||||
if (clientId && clientSecret) {
|
||||
const endpoint = `https://oidc.${region || "us-east-1"}.amazonaws.com/token`;
|
||||
@@ -135,13 +136,13 @@ async function refreshOAuthToken(connection) {
|
||||
grantType: "refresh_token",
|
||||
}),
|
||||
});
|
||||
|
||||
|
||||
if (!response.ok) {
|
||||
const errText = await response.text();
|
||||
console.log(`Kiro AWS SSO refresh failed: ${response.status} - ${errText}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
const data = await response.json();
|
||||
return {
|
||||
accessToken: data.accessToken,
|
||||
@@ -149,20 +150,20 @@ async function refreshOAuthToken(connection) {
|
||||
refreshToken: data.refreshToken || refreshToken,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
// Social auth refresh (Google/GitHub)
|
||||
const response = await fetch(KIRO_CONFIG.socialRefreshUrl, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ refreshToken }),
|
||||
});
|
||||
|
||||
|
||||
if (!response.ok) {
|
||||
const errText = await response.text();
|
||||
console.log(`Kiro social refresh failed: ${response.status} - ${errText}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
const data = await response.json();
|
||||
return {
|
||||
accessToken: data.accessToken,
|
||||
@@ -170,7 +171,7 @@ async function refreshOAuthToken(connection) {
|
||||
refreshToken: data.refreshToken || refreshToken,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
return null;
|
||||
} catch (err) {
|
||||
console.log(`Error refreshing ${provider} token:`, err.message);
|
||||
@@ -195,7 +196,7 @@ async function syncToCloudIfEnabled() {
|
||||
try {
|
||||
const cloudEnabled = await isCloudEnabled();
|
||||
if (!cloudEnabled) return;
|
||||
|
||||
|
||||
const machineId = await getConsistentMachineId();
|
||||
await syncToCloud(machineId);
|
||||
} catch (error) {
|
||||
@@ -210,20 +211,20 @@ async function syncToCloudIfEnabled() {
|
||||
*/
|
||||
async function testOAuthConnection(connection) {
|
||||
const config = OAUTH_TEST_CONFIG[connection.provider];
|
||||
|
||||
|
||||
if (!config) {
|
||||
return { valid: false, error: "Provider test not supported", refreshed: false };
|
||||
}
|
||||
|
||||
|
||||
// Check if token exists
|
||||
if (!connection.accessToken) {
|
||||
return { valid: false, error: "No access token", refreshed: false };
|
||||
}
|
||||
|
||||
|
||||
let accessToken = connection.accessToken;
|
||||
let refreshed = false;
|
||||
let newTokens = null;
|
||||
|
||||
|
||||
// Auto-refresh if token is expired and provider supports refresh
|
||||
const tokenExpired = isTokenExpired(connection);
|
||||
if (config.refreshable && tokenExpired && connection.refreshToken) {
|
||||
@@ -237,7 +238,7 @@ async function testOAuthConnection(connection) {
|
||||
return { valid: false, error: "Token expired and refresh failed", refreshed: false };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// For providers that only check expiry (no test endpoint available)
|
||||
if (config.checkExpiry) {
|
||||
// If we already refreshed successfully, token is valid
|
||||
@@ -250,23 +251,23 @@ async function testOAuthConnection(connection) {
|
||||
}
|
||||
return { valid: true, error: null, refreshed: false, newTokens: null };
|
||||
}
|
||||
|
||||
|
||||
// Call test endpoint
|
||||
try {
|
||||
const headers = {
|
||||
[config.authHeader]: `${config.authPrefix}${accessToken}`,
|
||||
...config.extraHeaders,
|
||||
};
|
||||
|
||||
|
||||
const res = await fetch(config.url, {
|
||||
method: config.method,
|
||||
headers,
|
||||
});
|
||||
|
||||
|
||||
if (res.ok) {
|
||||
return { valid: true, error: null, refreshed, newTokens };
|
||||
}
|
||||
|
||||
|
||||
// If 401 and we haven't tried refresh yet, try refresh now
|
||||
if (res.status === 401 && config.refreshable && !refreshed && connection.refreshToken) {
|
||||
const tokens = await refreshOAuthToken(connection);
|
||||
@@ -279,21 +280,21 @@ async function testOAuthConnection(connection) {
|
||||
...config.extraHeaders,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
if (retryRes.ok) {
|
||||
return { valid: true, error: null, refreshed: true, newTokens: tokens };
|
||||
}
|
||||
}
|
||||
return { valid: false, error: "Token invalid or revoked", refreshed: false };
|
||||
}
|
||||
|
||||
|
||||
if (res.status === 401) {
|
||||
return { valid: false, error: "Token invalid or revoked", refreshed };
|
||||
}
|
||||
if (res.status === 403) {
|
||||
return { valid: false, error: "Access denied", refreshed };
|
||||
}
|
||||
|
||||
|
||||
return { valid: false, error: `API returned ${res.status}`, refreshed };
|
||||
} catch (err) {
|
||||
return { valid: false, error: err.message, refreshed };
|
||||
@@ -304,6 +305,23 @@ async function testOAuthConnection(connection) {
|
||||
* Test API key connection
|
||||
*/
|
||||
async function testApiKeyConnection(connection) {
|
||||
// OpenAI Compatible providers - test via /models endpoint
|
||||
if (isOpenAICompatibleProvider(connection.provider)) {
|
||||
const modelsBase = connection.providerSpecificData?.baseUrl;
|
||||
if (!modelsBase) {
|
||||
return { valid: false, error: "Missing base URL" };
|
||||
}
|
||||
try {
|
||||
const modelsUrl = `${modelsBase.replace(/\/$/, "")}/models`;
|
||||
const res = await fetch(modelsUrl, {
|
||||
headers: { "Authorization": `Bearer ${connection.apiKey}` },
|
||||
});
|
||||
return { valid: res.ok, error: res.ok ? null : "Invalid API key or base URL" };
|
||||
} catch (err) {
|
||||
return { valid: false, error: err.message };
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
switch (connection.provider) {
|
||||
case "openai": {
|
||||
@@ -312,7 +330,7 @@ async function testApiKeyConnection(connection) {
|
||||
});
|
||||
return { valid: res.ok, error: res.ok ? null : "Invalid API key" };
|
||||
}
|
||||
|
||||
|
||||
case "anthropic": {
|
||||
const res = await fetch("https://api.anthropic.com/v1/messages", {
|
||||
method: "POST",
|
||||
@@ -330,19 +348,19 @@ async function testApiKeyConnection(connection) {
|
||||
const valid = res.status !== 401;
|
||||
return { valid, error: valid ? null : "Invalid API key" };
|
||||
}
|
||||
|
||||
|
||||
case "gemini": {
|
||||
const res = await fetch(`https://generativelanguage.googleapis.com/v1/models?key=${connection.apiKey}`);
|
||||
return { valid: res.ok, error: res.ok ? null : "Invalid API key" };
|
||||
}
|
||||
|
||||
|
||||
case "openrouter": {
|
||||
const res = await fetch("https://openrouter.ai/api/v1/auth/key", {
|
||||
headers: { Authorization: `Bearer ${connection.apiKey}` },
|
||||
});
|
||||
return { valid: res.ok, error: res.ok ? null : "Invalid API key" };
|
||||
}
|
||||
|
||||
|
||||
case "glm": {
|
||||
// GLM uses Claude-compatible API at api.z.ai
|
||||
const res = await fetch("https://api.z.ai/api/anthropic/v1/messages", {
|
||||
@@ -361,7 +379,7 @@ async function testApiKeyConnection(connection) {
|
||||
const valid = res.status !== 401 && res.status !== 403;
|
||||
return { valid, error: valid ? null : "Invalid API key" };
|
||||
}
|
||||
|
||||
|
||||
case "minimax": {
|
||||
// MiniMax uses Claude-compatible API
|
||||
const res = await fetch("https://api.minimax.io/anthropic/v1/messages", {
|
||||
@@ -380,7 +398,7 @@ async function testApiKeyConnection(connection) {
|
||||
const valid = res.status !== 401 && res.status !== 403;
|
||||
return { valid, error: valid ? null : "Invalid API key" };
|
||||
}
|
||||
|
||||
|
||||
case "kimi": {
|
||||
// Kimi uses Claude-compatible API
|
||||
const res = await fetch("https://api.kimi.com/coding/v1/messages", {
|
||||
@@ -399,35 +417,35 @@ async function testApiKeyConnection(connection) {
|
||||
const valid = res.status !== 401 && res.status !== 403;
|
||||
return { valid, error: valid ? null : "Invalid API key" };
|
||||
}
|
||||
|
||||
|
||||
case "deepseek": {
|
||||
const res = await fetch("https://api.deepseek.com/models", {
|
||||
headers: { Authorization: `Bearer ${connection.apiKey}` },
|
||||
});
|
||||
return { valid: res.ok, error: res.ok ? null : "Invalid API key" };
|
||||
}
|
||||
|
||||
|
||||
case "groq": {
|
||||
const res = await fetch("https://api.groq.com/openai/v1/models", {
|
||||
headers: { Authorization: `Bearer ${connection.apiKey}` },
|
||||
});
|
||||
return { valid: res.ok, error: res.ok ? null : "Invalid API key" };
|
||||
}
|
||||
|
||||
|
||||
case "mistral": {
|
||||
const res = await fetch("https://api.mistral.ai/v1/models", {
|
||||
headers: { Authorization: `Bearer ${connection.apiKey}` },
|
||||
});
|
||||
return { valid: res.ok, error: res.ok ? null : "Invalid API key" };
|
||||
}
|
||||
|
||||
|
||||
case "xai": {
|
||||
const res = await fetch("https://api.x.ai/v1/models", {
|
||||
headers: { Authorization: `Bearer ${connection.apiKey}` },
|
||||
});
|
||||
return { valid: res.ok, error: res.ok ? null : "Invalid API key" };
|
||||
}
|
||||
|
||||
|
||||
default:
|
||||
return { valid: false, error: "Provider test not supported" };
|
||||
}
|
||||
@@ -447,7 +465,7 @@ export async function POST(request, { params }) {
|
||||
}
|
||||
|
||||
let result;
|
||||
|
||||
|
||||
if (connection.authType === "apikey") {
|
||||
result = await testApiKeyConnection(connection);
|
||||
} else {
|
||||
@@ -460,7 +478,7 @@ export async function POST(request, { params }) {
|
||||
lastError: result.valid ? null : result.error,
|
||||
lastErrorAt: result.valid ? null : new Date().toISOString(),
|
||||
};
|
||||
|
||||
|
||||
// If token was refreshed, update tokens in DB
|
||||
if (result.refreshed && result.newTokens) {
|
||||
updateData.accessToken = result.newTokens.accessToken;
|
||||
@@ -474,7 +492,7 @@ export async function POST(request, { params }) {
|
||||
|
||||
// Update status in db
|
||||
await updateProviderConnection(id, updateData);
|
||||
|
||||
|
||||
// Sync to cloud if token was refreshed
|
||||
if (result.refreshed) {
|
||||
await syncToCloudIfEnabled();
|
||||
@@ -490,4 +508,3 @@ export async function POST(request, { params }) {
|
||||
return NextResponse.json({ error: "Test failed" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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",
|
||||
});
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { getProviderNodeById } from "@/models";
|
||||
import { isOpenAICompatibleProvider } from "@/shared/constants/providers";
|
||||
|
||||
// POST /api/providers/validate - Validate API key with provider
|
||||
export async function POST(request) {
|
||||
@@ -15,6 +17,22 @@ export async function POST(request) {
|
||||
|
||||
// Validate with each provider
|
||||
try {
|
||||
if (isOpenAICompatibleProvider(provider)) {
|
||||
const node = await getProviderNodeById(provider);
|
||||
if (!node) {
|
||||
return NextResponse.json({ error: "OpenAI Compatible node not found" }, { status: 404 });
|
||||
}
|
||||
const modelsUrl = `${node.baseUrl?.replace(/\/$/, "")}/models`;
|
||||
const res = await fetch(modelsUrl, {
|
||||
headers: { "Authorization": `Bearer ${apiKey}` },
|
||||
});
|
||||
isValid = res.ok;
|
||||
return NextResponse.json({
|
||||
valid: isValid,
|
||||
error: isValid ? null : "Invalid API key",
|
||||
});
|
||||
}
|
||||
|
||||
switch (provider) {
|
||||
case "openai":
|
||||
const openaiRes = await fetch("https://api.openai.com/v1/models", {
|
||||
@@ -77,8 +95,8 @@ export async function POST(request) {
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
return NextResponse.json({ error: "Provider validation not supported" }, { status: 400 });
|
||||
default:
|
||||
return NextResponse.json({ error: "Provider validation not supported" }, { status: 400 });
|
||||
}
|
||||
} catch (err) {
|
||||
error = err.message;
|
||||
|
||||
@@ -31,7 +31,10 @@ export async function POST(request) {
|
||||
};
|
||||
|
||||
// Build URL and headers using provider service
|
||||
const url = buildProviderUrl(provider, body.model || "test-model", true, { baseUrlIndex: 0 });
|
||||
const url = buildProviderUrl(provider, body.model || "test-model", true, {
|
||||
baseUrlIndex: 0,
|
||||
baseUrl: connection.providerSpecificData?.baseUrl
|
||||
});
|
||||
console.log("🚀 ~ POST ~ url:", url)
|
||||
const headers = buildProviderHeaders(provider, credentials, true, body);
|
||||
console.log("🚀 ~ POST ~ headers:", headers)
|
||||
|
||||
@@ -91,7 +91,10 @@ export async function POST(request) {
|
||||
};
|
||||
|
||||
// Build URL and headers
|
||||
const url = buildProviderUrl(provider, model, true, { baseUrlIndex: 0 });
|
||||
const url = buildProviderUrl(provider, model, true, {
|
||||
baseUrlIndex: 0,
|
||||
baseUrl: connection.providerSpecificData?.baseUrl
|
||||
});
|
||||
const headers = buildProviderHeaders(provider, credentials, true, actualBody);
|
||||
|
||||
result = {
|
||||
|
||||
Reference in New Issue
Block a user