| import { NextResponse } from "next/server"; |
| import { getProviderNodeById } from "@/models"; |
| import { isOpenAICompatibleProvider, isAnthropicCompatibleProvider, isCustomEmbeddingProvider, AI_PROVIDERS } from "@/shared/constants/providers"; |
| import { getDefaultModel } from "open-sse/config/providerModels.js"; |
| import { resolveOllamaLocalHost, resolveXiaomiTokenplanBaseUrl, PROVIDERS } from "open-sse/config/providers.js"; |
| import { openaiToCommandCodeRequest } from "open-sse/translator/request/openai-to-commandcode.js"; |
| import { normalizeProviderId } from "@/lib/providerNormalization"; |
|
|
| |
| |
| async function probeWebProvider(provider, apiKey) { |
| const p = AI_PROVIDERS[provider]; |
| if (!p) return null; |
| |
| const kinds = p.serviceKinds || ["llm"]; |
| const isWebOnly = kinds.every((k) => k === "webSearch" || k === "webFetch"); |
| if (!isWebOnly) return null; |
| const cfg = p.searchConfig || p.fetchConfig; |
| if (!cfg) return null; |
| if (cfg.authType === "none") return true; |
|
|
| let url = cfg.baseUrl; |
| const headers = { "Content-Type": "application/json" }; |
| let body; |
|
|
| |
| switch (cfg.authHeader) { |
| case "bearer": headers["Authorization"] = `Bearer ${apiKey}`; break; |
| case "x-api-key": headers["x-api-key"] = apiKey; break; |
| case "x-subscription-token":headers["x-subscription-token"] = apiKey; break; |
| case "key": url += `?key=${encodeURIComponent(apiKey)}&q=ping&cx=test`; break; |
| case "api_key": url += `?api_key=${encodeURIComponent(apiKey)}&q=ping&engine=google`; break; |
| } |
|
|
| |
| if (cfg.method === "POST") { |
| body = JSON.stringify({ query: "ping", q: "ping", url: "https://example.com" }); |
| } |
|
|
| const res = await fetch(url, { method: cfg.method, headers, body, signal: AbortSignal.timeout(8000) }); |
| return res.status !== 401 && res.status !== 403; |
| } |
|
|
| |
| |
| async function probeMediaProvider(provider, apiKey) { |
| const p = AI_PROVIDERS[provider]; |
| if (!p) return null; |
| const MEDIA_KINDS = new Set(["tts", "embedding", "stt", "image", "video", "music", "imageToText"]); |
| const kinds = p.serviceKinds || ["llm"]; |
| const isMediaOnly = kinds.every((k) => MEDIA_KINDS.has(k)); |
| if (!isMediaOnly) return null; |
| const cfg = p.ttsConfig || p.sttConfig || p.embeddingConfig || p.imageConfig || p.videoConfig || p.musicConfig; |
| |
| if (!cfg) return true; |
| if (p.noAuth || cfg.authType === "none") return true; |
| |
| if (cfg.authHeader === "playht" || cfg.authHeader === "aws-sigv4") return true; |
|
|
| const headers = { "Content-Type": "application/json", ...(cfg.extraHeaders || {}) }; |
|
|
| switch (cfg.authHeader) { |
| case "bearer": headers["Authorization"] = `Bearer ${apiKey}`; break; |
| case "key": headers["Authorization"] = `Key ${apiKey}`; break; |
| case "x-api-key": headers["x-api-key"] = apiKey; break; |
| case "x-key": headers["x-key"] = apiKey; break; |
| case "xi-api-key": headers["xi-api-key"] = apiKey; break; |
| case "token": headers["Authorization"] = `Token ${apiKey}`; break; |
| case "basic": headers["Authorization"] = `Basic ${apiKey}`; break; |
| default: return null; |
| } |
|
|
| const method = cfg.method || "POST"; |
| const res = await fetch(cfg.baseUrl, { |
| method, |
| headers, |
| body: method === "GET" ? undefined : JSON.stringify({ input: "ping", text: "ping", prompt: "ping", model: getDefaultModel(provider) || "test" }), |
| signal: AbortSignal.timeout(8000), |
| }); |
| return res.status !== 401 && res.status !== 403; |
| } |
|
|
| |
| export async function POST(request) { |
| try { |
| const body = await request.json(); |
| const provider = normalizeProviderId(body.provider); |
| const { apiKey, providerSpecificData } = body; |
|
|
| const isNoAuth = AI_PROVIDERS[provider]?.noAuth === true; |
| if (!provider || (!apiKey && provider !== "ollama-local" && !isNoAuth)) { |
| return NextResponse.json({ error: "Provider and API key required" }, { status: 400 }); |
| } |
|
|
| let isValid = false; |
| let error = null; |
|
|
| |
| 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", |
| }); |
| } |
|
|
| |
| if (isCustomEmbeddingProvider(provider)) { |
| const node = await getProviderNodeById(provider); |
| if (!node) { |
| return NextResponse.json({ error: "Custom Embedding node not found" }, { status: 404 }); |
| } |
| const baseUrl = node.baseUrl?.replace(/\/$/, ""); |
| const modelsRes = await fetch(`${baseUrl}/models`, { |
| headers: { "Authorization": `Bearer ${apiKey}` }, |
| }); |
| if (modelsRes.ok) { |
| return NextResponse.json({ valid: true }); |
| } |
| |
| if (modelsRes.status === 401 || modelsRes.status === 403) { |
| return NextResponse.json({ valid: false, error: "Invalid API key" }); |
| } |
| |
| const embedRes = await fetch(`${baseUrl}/embeddings`, { |
| method: "POST", |
| headers: { "Authorization": `Bearer ${apiKey}`, "Content-Type": "application/json" }, |
| body: JSON.stringify({ model: "test", input: "ping" }), |
| }); |
| |
| isValid = embedRes.status !== 401 && embedRes.status !== 403; |
| return NextResponse.json({ |
| valid: isValid, |
| error: isValid ? null : "Invalid API key", |
| }); |
| } |
|
|
| if (isAnthropicCompatibleProvider(provider)) { |
| const node = await getProviderNodeById(provider); |
| if (!node) { |
| return NextResponse.json({ error: "Anthropic Compatible node not found" }, { status: 404 }); |
| } |
|
|
| let normalizedBase = node.baseUrl?.trim().replace(/\/$/, "") || ""; |
| if (normalizedBase.endsWith("/messages")) { |
| normalizedBase = normalizedBase.slice(0, -9); |
| } |
|
|
| const modelsUrl = `${normalizedBase}/models`; |
|
|
| const res = await fetch(modelsUrl, { |
| headers: { |
| "x-api-key": apiKey, |
| "anthropic-version": "2023-06-01", |
| "Authorization": `Bearer ${apiKey}` |
| }, |
| }); |
|
|
| isValid = res.ok; |
| return NextResponse.json({ |
| valid: isValid, |
| error: isValid ? null : "Invalid API key", |
| }); |
| } |
|
|
| if (provider === "cloudflare-ai") { |
| const { providerSpecificData } = body; |
| const accountId = providerSpecificData?.accountId; |
| if (!accountId) { |
| return NextResponse.json({ valid: false, error: "Missing Account ID" }); |
| } |
| const url = `https://api.cloudflare.com/client/v4/accounts/${accountId}/ai/v1/chat/completions`; |
| const cfRes = await fetch(url, { |
| method: "POST", |
| headers: { "Authorization": `Bearer ${apiKey}`, "Content-Type": "application/json" }, |
| body: JSON.stringify({ |
| model: getDefaultModel("cloudflare-ai"), |
| messages: [{ role: "user", content: "test" }], |
| max_tokens: 1, |
| }), |
| }); |
| isValid = cfRes.status !== 401 && cfRes.status !== 403 && cfRes.status !== 404; |
| return NextResponse.json({ |
| valid: isValid, |
| error: isValid ? null : "Invalid API token or Account ID", |
| }); |
| } |
|
|
| if (provider === "azure") { |
| const { providerSpecificData } = body; |
| const endpoint = (providerSpecificData?.azureEndpoint || "").replace(/\/$/, ""); |
| const deployment = providerSpecificData?.deployment || "gpt-4"; |
| const apiVersion = providerSpecificData?.apiVersion || "2024-10-01-preview"; |
| const organization = providerSpecificData?.organization; |
|
|
| const url = `${endpoint}/openai/deployments/${deployment}/chat/completions?api-version=${apiVersion}`; |
| const headers = { |
| "api-key": apiKey, |
| "Content-Type": "application/json", |
| }; |
| if (organization) headers["OpenAI-Organization"] = organization; |
|
|
| const azureRes = await fetch(url, { |
| method: "POST", |
| headers, |
| body: JSON.stringify({ |
| messages: [{ role: "user", content: "test" }], |
| max_tokens: 1, |
| }), |
| }); |
| isValid = azureRes.status !== 401 && azureRes.status !== 403; |
| return NextResponse.json({ |
| valid: isValid, |
| error: isValid ? null : "Invalid API key or Azure configuration", |
| }); |
| } |
|
|
| |
| const webResult = await probeWebProvider(provider, apiKey); |
| if (webResult !== null) { |
| return NextResponse.json({ |
| valid: webResult, |
| error: webResult ? null : "Invalid API key", |
| }); |
| } |
|
|
| |
| const mediaResult = await probeMediaProvider(provider, apiKey); |
| if (mediaResult !== null) { |
| return NextResponse.json({ |
| valid: mediaResult, |
| error: mediaResult ? null : "Invalid API key", |
| }); |
| } |
|
|
| switch (provider) { |
| case "openai": |
| const openaiRes = await fetch("https://api.openai.com/v1/models", { |
| headers: { "Authorization": `Bearer ${apiKey}` }, |
| }); |
| isValid = openaiRes.ok; |
| break; |
|
|
| case "vercel-ai-gateway": |
| const vercelAiGatewayRes = await fetch("https://ai-gateway.vercel.sh/v1/models", { |
| headers: { "Authorization": `Bearer ${apiKey}` }, |
| }); |
| isValid = vercelAiGatewayRes.ok; |
| break; |
|
|
| case "anthropic": |
| const anthropicRes = await fetch("https://api.anthropic.com/v1/messages", { |
| method: "POST", |
| headers: { |
| "x-api-key": apiKey, |
| "anthropic-version": "2023-06-01", |
| "content-type": "application/json", |
| }, |
| body: JSON.stringify({ |
| model: "claude-3-haiku-20240307", |
| max_tokens: 1, |
| messages: [{ role: "user", content: "test" }], |
| }), |
| }); |
| isValid = anthropicRes.status !== 401; |
| break; |
|
|
| case "gemini": |
| const geminiRes = await fetch(`https://generativelanguage.googleapis.com/v1/models?key=${apiKey}`); |
| isValid = geminiRes.ok; |
| break; |
|
|
| case "openrouter": |
| const openrouterRes = await fetch("https://openrouter.ai/api/v1/models", { |
| headers: { "Authorization": `Bearer ${apiKey}` }, |
| }); |
| isValid = openrouterRes.ok; |
| break; |
|
|
| case "glm": |
| case "glm-cn": |
| case "kimi": |
| case "minimax": |
| case "minimax-cn": |
| case "alicode-intl": |
| case "alicode": |
| case "agentrouter": { |
| |
| const cfg = PROVIDERS[provider]; |
| const isOpenAiFormat = provider === "glm-cn" || provider === "alicode" || provider === "alicode-intl"; |
|
|
| if (isOpenAiFormat) { |
| const testModel = getDefaultModel(provider); |
| const res = await fetch(cfg.baseUrl, { |
| method: "POST", |
| headers: { "Authorization": `Bearer ${apiKey}`, "content-type": "application/json" }, |
| body: JSON.stringify({ model: testModel, max_tokens: 1, messages: [{ role: "user", content: "test" }] }), |
| }); |
| isValid = res.status !== 401 && res.status !== 403; |
| } else { |
| const testModel = getDefaultModel(provider) || "claude-sonnet-4-20250514"; |
| const res = await fetch(cfg.baseUrl, { |
| method: "POST", |
| headers: { |
| "x-api-key": apiKey, |
| "anthropic-version": "2023-06-01", |
| "content-type": "application/json", |
| ...(cfg.headers || {}), |
| }, |
| body: JSON.stringify({ model: testModel, max_tokens: 1, messages: [{ role: "user", content: "test" }] }), |
| }); |
| |
| isValid = res.status !== 401 && res.status !== 403; |
| } |
| break; |
| } |
| case "volcengine-ark": |
| case "byteplus": { |
| const res = await fetch(PROVIDERS[provider]?.baseUrl, { |
| method: "POST", |
| headers: { |
| "Authorization": `Bearer ${apiKey}`, |
| "content-type": "application/json", |
| }, |
| body: JSON.stringify({ |
| model: getDefaultModel(provider), |
| max_tokens: 1, |
| messages: [{ role: "user", content: "test" }], |
| }), |
| }); |
| isValid = res.status !== 401 && res.status !== 403; |
| break; |
| } |
|
|
| case "deepseek": |
| case "groq": |
| case "xai": |
| case "mistral": |
| case "perplexity": |
| case "together": |
| case "fireworks": |
| case "cerebras": |
| case "cohere": |
| case "nebius": |
| case "siliconflow": |
| case "hyperbolic": |
| case "ollama": |
| case "ollama-local": |
| case "assemblyai": |
| case "nanobanana": |
| case "chutes": |
| case "xiaomi-mimo": |
| case "xiaomi-tokenplan": |
| case "nvidia": { |
| const endpoints = { |
| ...Object.fromEntries( |
| Object.entries(PROVIDERS).filter(([, t]) => t.validateUrl).map(([id, t]) => [id, t.validateUrl]) |
| ), |
| |
| "ollama-local": `${resolveOllamaLocalHost({ providerSpecificData })}/api/tags`, |
| "xiaomi-tokenplan": `${resolveXiaomiTokenplanBaseUrl({ providerSpecificData })}/models`, |
| }; |
| const headers = {}; |
| if (apiKey) headers["Authorization"] = `Bearer ${apiKey}`; |
| const res = await fetch(endpoints[provider], { headers }); |
| |
| if (provider === "xai") { |
| isValid = res.status === 200 || res.status === 403; |
| } else { |
| isValid = res.ok; |
| } |
| break; |
| } |
|
|
| case "opencode-go": { |
| const res = await fetch("https://opencode.ai/zen/go/v1/chat/completions", { |
| method: "POST", |
| headers: { "Content-Type": "application/json", "Authorization": `Bearer ${apiKey}` }, |
| body: JSON.stringify({ |
| model: getDefaultModel("opencode-go"), |
| messages: [{ role: "user", content: "ping" }], |
| max_tokens: 1, |
| stream: false, |
| }), |
| }); |
| isValid = res.status !== 401 && res.status !== 403; |
| break; |
| } |
|
|
| case "commandcode": { |
| const cfg = PROVIDERS.commandcode; |
| const model = getDefaultModel("commandcode"); |
| const payload = openaiToCommandCodeRequest(model, { |
| messages: [{ role: "user", content: "ping" }], |
| max_tokens: 1, |
| stream: false, |
| }, false); |
| const res = await fetch(cfg.baseUrl, { |
| method: "POST", |
| headers: { |
| "Content-Type": "application/json", |
| ...(cfg.headers || {}), |
| "x-session-id": crypto.randomUUID(), |
| "Authorization": `Bearer ${apiKey}`, |
| }, |
| body: JSON.stringify(payload), |
| }); |
| isValid = res.status !== 401 && res.status !== 403; |
| break; |
| } |
|
|
| case "deepgram": { |
| const res = await fetch("https://api.deepgram.com/v1/projects", { |
| headers: { "Authorization": `Token ${apiKey}` }, |
| }); |
| isValid = res.ok; |
| break; |
| } |
|
|
| case "blackbox": { |
| const res = await fetch("https://api.blackbox.ai/chat/completions", { |
| method: "POST", |
| headers: { |
| "Authorization": `Bearer ${apiKey}`, |
| "Content-Type": "application/json", |
| }, |
| body: JSON.stringify({ |
| model: "gpt-4o", |
| messages: [{ role: "user", content: "test" }], |
| max_tokens: 10, |
| }), |
| }); |
| |
| isValid = res.status === 200 || res.status === 400; |
| break; |
| } |
|
|
| case "vertex": { |
| |
| |
| const saJson = (() => { try { const p = JSON.parse(apiKey); return p.type === "service_account" ? p : null; } catch { return null; } })(); |
| if (saJson) { |
| |
| isValid = !!(saJson.client_email && saJson.private_key && saJson.project_id); |
| } else { |
| |
| const probeRes = await fetch( |
| `https://aiplatform.googleapis.com/v1/publishers/google/models/__probe__:generateContent?key=${apiKey}`, |
| { method: "POST", headers: { "Content-Type": "application/json" }, body: "{}" } |
| ); |
| isValid = probeRes.status !== 401 && probeRes.status !== 403; |
| } |
| break; |
| } |
|
|
| case "vertex-partner": { |
| const saJson = (() => { try { const p = JSON.parse(apiKey); return p.type === "service_account" ? p : null; } catch { return null; } })(); |
| if (saJson) { |
| isValid = !!(saJson.client_email && saJson.private_key && saJson.project_id); |
| } else { |
| const probeRes = await fetch( |
| `https://aiplatform.googleapis.com/v1/publishers/google/models/__probe__:generateContent?key=${apiKey}`, |
| { method: "POST", headers: { "Content-Type": "application/json" }, body: "{}" } |
| ); |
| isValid = probeRes.status !== 401 && probeRes.status !== 403; |
| } |
| break; |
| } |
|
|
| case "grok-web": { |
| const token = apiKey.startsWith("sso=") ? apiKey.slice(4) : apiKey; |
| |
| const randomHex = (n) => { |
| const a = new Uint8Array(n); |
| crypto.getRandomValues(a); |
| return Array.from(a, (b) => b.toString(16).padStart(2, "0")).join(""); |
| }; |
| const statsigId = Buffer.from("e:TypeError: Cannot read properties of null (reading 'children')").toString("base64"); |
| const traceId = randomHex(16); |
| const spanId = randomHex(8); |
| const res = await fetch("https://grok.com/rest/app-chat/conversations/new", { |
| method: "POST", |
| headers: { |
| Accept: "*/*", |
| "Accept-Encoding": "gzip, deflate, br, zstd", |
| "Accept-Language": "en-US,en;q=0.9", |
| "Cache-Control": "no-cache", |
| "Content-Type": "application/json", |
| Cookie: `sso=${token}`, |
| Origin: "https://grok.com", |
| Pragma: "no-cache", |
| Referer: "https://grok.com/", |
| "Sec-Ch-Ua": '"Google Chrome";v="136", "Chromium";v="136", "Not(A:Brand";v="24"', |
| "Sec-Ch-Ua-Mobile": "?0", |
| "Sec-Ch-Ua-Platform": '"macOS"', |
| "Sec-Fetch-Dest": "empty", |
| "Sec-Fetch-Mode": "cors", |
| "Sec-Fetch-Site": "same-origin", |
| "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36", |
| "x-statsig-id": statsigId, |
| "x-xai-request-id": crypto.randomUUID(), |
| traceparent: `00-${traceId}-${spanId}-00`, |
| }, |
| body: JSON.stringify({ |
| temporary: true, modelName: "grok-4", modelMode: "MODEL_MODE_GROK_4", message: "ping", |
| fileAttachments: [], imageAttachments: [], |
| disableSearch: false, enableImageGeneration: false, returnImageBytes: false, |
| returnRawGrokInXaiRequest: false, enableImageStreaming: false, imageGenerationCount: 0, |
| forceConcise: false, toolOverrides: {}, enableSideBySide: true, sendFinalMetadata: true, |
| isReasoning: false, disableTextFollowUps: true, disableMemory: true, |
| forceSideBySide: false, isAsyncChat: false, disableSelfHarmShortCircuit: false, |
| }), |
| }); |
| |
| if (res.status === 401 || res.status === 403) { |
| isValid = false; |
| error = "Invalid SSO cookie — re-paste from grok.com DevTools → Cookies → sso"; |
| } else { |
| isValid = true; |
| } |
| break; |
| } |
|
|
| case "perplexity-web": { |
| let sessionToken = apiKey; |
| if (sessionToken.startsWith("__Secure-next-auth.session-token=")) { |
| sessionToken = sessionToken.slice("__Secure-next-auth.session-token=".length); |
| } |
| const tz = typeof Intl !== "undefined" ? Intl.DateTimeFormat().resolvedOptions().timeZone : "UTC"; |
| const res = await fetch("https://www.perplexity.ai/rest/sse/perplexity_ask", { |
| method: "POST", |
| headers: { |
| "Content-Type": "application/json", |
| Accept: "text/event-stream", |
| Origin: "https://www.perplexity.ai", |
| Referer: "https://www.perplexity.ai/", |
| "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36", |
| "X-App-ApiClient": "default", |
| "X-App-ApiVersion": "2.18", |
| Cookie: `__Secure-next-auth.session-token=${sessionToken}`, |
| }, |
| body: JSON.stringify({ |
| query_str: "ping", |
| params: { |
| query_str: "ping", search_focus: "internet", mode: "concise", model_preference: "pplx_pro", |
| sources: ["web"], attachments: [], |
| frontend_uuid: crypto.randomUUID(), frontend_context_uuid: crypto.randomUUID(), |
| version: "2.18", language: "en-US", timezone: tz, |
| search_recency_filter: null, is_incognito: true, use_schematized_api: true, last_backend_uuid: null, |
| }, |
| }), |
| }); |
| if (res.status === 401 || res.status === 403) { |
| isValid = false; |
| error = "Invalid session cookie — re-paste __Secure-next-auth.session-token from perplexity.ai"; |
| } else { |
| isValid = true; |
| } |
| break; |
| } |
|
|
| default: { |
| |
| const cfg = PROVIDERS[provider]; |
| if (!cfg || cfg.format !== "openai" || !cfg.baseUrl) { |
| return NextResponse.json({ error: "Provider validation not supported" }, { status: 400 }); |
| } |
| if (cfg.noAuth) { |
| isValid = true; |
| break; |
| } |
| |
| const headers = { "Content-Type": "application/json", ...(cfg.headers || {}) }; |
| if (cfg.authHeader === "x-api-key") headers["X-API-Key"] = apiKey; |
| else headers["Authorization"] = `Bearer ${apiKey}`; |
| |
| const modelsUrl = cfg.baseUrl.replace(/\/chat\/completions$/, "/models").replace(/\/chatbot$/, "/models"); |
| let probeOk = null; |
| try { |
| const probeRes = await fetch(modelsUrl, { headers, signal: AbortSignal.timeout(8000) }); |
| if (probeRes.status === 401 || probeRes.status === 403) probeOk = false; |
| else if (probeRes.ok) probeOk = true; |
| } catch { } |
| if (probeOk !== null) { |
| isValid = probeOk; |
| break; |
| } |
| |
| const defaultModel = getDefaultModel(provider) || "test"; |
| const chatRes = await fetch(cfg.baseUrl, { |
| method: "POST", |
| headers, |
| body: JSON.stringify({ model: defaultModel, messages: [{ role: "user", content: "ping" }], max_tokens: 1 }), |
| signal: AbortSignal.timeout(10000), |
| }); |
| isValid = chatRes.status !== 401 && chatRes.status !== 403; |
| break; |
| } |
| } |
| } catch (err) { |
| error = err.message; |
| isValid = false; |
| } |
|
|
| return NextResponse.json({ |
| valid: isValid, |
| error: isValid ? null : (error || "Invalid API key"), |
| }); |
| } catch (error) { |
| console.log("Error validating API key:", error); |
| return NextResponse.json({ error: "Validation failed" }, { status: 500 }); |
| } |
| } |
|
|