1 line
132 KiB
JSON
1 line
132 KiB
JSON
{"repo": "arosyihuddin/9router-supabase", "files": {"src/app/api/providers/validate/route.js": {"f213a60a2984a7ad16ffa4bd05649df10649ae7e": {"t": 1785746023.1424627, "n": 27421, "h": "966b55bb5ded3a6f", "c": "import { NextResponse } from \"next/server\";\nimport { getProviderNodeById } from \"@/models\";\nimport { isOpenAICompatibleProvider, isAnthropicCompatibleProvider, isCustomEmbeddingProvider, AI_PROVIDERS } from \"@/shared/constants/providers\";\nimport { getDefaultModel } from \"open-sse/config/providerModels.js\";\nimport { resolveOllamaLocalHost, PROVIDERS } from \"open-sse/config/providers.js\";\nimport { openaiToCommandCode } from \"open-sse/translator/request/openai-to-commandcode.js\";\nimport { PROVIDER_ENDPOINTS } from \"@/shared/constants/config\";\nimport { normalizeProviderId } from \"@/lib/providerNormalization\";\nimport { normalizeCustomHeaders } from \"@/shared/utils/customHeaders\";\n\n// Probe a webSearch/webFetch provider using its searchConfig/fetchConfig.\n// Returns true if API key is accepted (status !== 401 && !== 403).\nasync function probeWebProvider(provider, apiKey) {\n const p = AI_PROVIDERS[provider];\n if (!p) return null;\n // Skip if provider has dual-purpose (LLM + search), let LLM validate handle it\n const kinds = p.serviceKinds || [\"llm\"];\n const isWebOnly = kinds.every((k) => k === \"webSearch\" || k === \"webFetch\");\n if (!isWebOnly) return null;\n const cfg = p.searchConfig || p.fetchConfig;\n if (!cfg) return null;\n if (cfg.authType === \"none\") return true; // no-auth (e.g. searxng)\n\n let url = cfg.baseUrl;\n const headers = { \"Content-Type\": \"application/json\" };\n let body;\n\n // Apply auth based on authHeader\n switch (cfg.authHeader) {\n case \"bearer\": headers[\"Authorization\"] = `Bearer ${apiKey}`; break;\n case \"x-api-key\": headers[\"x-api-key\"] = apiKey; break;\n case \"x-subscription-token\":headers[\"x-subscription-token\"] = apiKey; break;\n case \"key\": url += `?key=${encodeURIComponent(apiKey)}&q=ping&cx=test`; break; // google-pse\n case \"api_key\": url += `?api_key=${encodeURIComponent(apiKey)}&q=ping&engine=google`; break; // searchapi\n }\n\n // Minimal body for POST endpoints; GET sends nothing\n if (cfg.method === \"POST\") {\n body = JSON.stringify({ query: \"ping\", q: \"ping\", url: \"https://example.com\" });\n }\n\n const res = await fetch(url, { method: cfg.method, headers, body, signal: AbortSignal.timeout(8000) });\n return res.status !== 401 && res.status !== 403;\n}\n\n// Probe a media provider (tts/embedding/stt/image/video) using *Config.\n// Returns true if API key is accepted; null to skip (let default handler decide).\nasync function probeMediaProvider(provider, apiKey) {\n const p = AI_PROVIDERS[provider];\n if (!p) return null;\n const MEDIA_KINDS = new Set([\"tts\", \"embedding\", \"stt\", \"image\", \"video\", \"music\", \"imageToText\"]);\n const kinds = p.serviceKinds || [\"llm\"];\n const isMediaOnly = kinds.every((k) => MEDIA_KINDS.has(k));\n if (!isMediaOnly) return null;\n const cfg = p.ttsConfig || p.sttConfig || p.embeddingConfig || p.imageConfig || p.videoConfig || p.musicConfig;\n // No probe config → best-effort accept (validate at usage time)\n if (!cfg) return true;\n if (p.noAuth || cfg.authType === \"none\") return true;\n // Skip auth schemes that need provider-specific data\n if (cfg.authHeader === \"playht\" || cfg.authHeader === \"aws-sigv4\") return true;\n\n const headers = { \"Content-Type\": \"application/json\", ...(cfg.extraHeaders || {}) };\n\n switch (cfg.authHeader) {\n case \"bearer\": headers[\"Authorization\"] = `Bearer ${apiKey}`; break;\n case \"key\": headers[\"Authorization\"] = `Key ${apiKey}`; break;\n case \"x-api-key\": headers[\"x-api-key\"] = apiKey; break;\n case \"x-key\": headers[\"x-key\"] = apiKey; break;\n case \"xi-api-key\": headers[\"xi-api-key\"] = apiKey; break;\n case \"token\": headers[\"Authorization\"] = `Token ${apiKey}`; break;\n case \"basic\": headers[\"Authorization\"] = `Basic ${apiKey}`; break;\n default: return null;\n }\n\n const method = cfg.method || \"POST\";\n const res = await fetch(cfg.baseUrl, {\n method,\n headers,\n body: method === \"GET\" ? undefined : JSON.stringify({ input: \"ping\", text: \"ping\", prompt: \"ping\", model: cfg.models?.[0]?.id || \"test\" }),\n signal: AbortSignal.timeout(8000),\n });\n return res.status !== 401 && res.status !== 403;\n}\n\n// POST /api/providers/validate - Validate API key with provider\nexport async function POST(request) {\n try {\n const body = await request.json();\n const provider = normalizeProviderId(body.provider);\n const { apiKey, providerSpecificData } = body;\n\n const isNoAuth = AI_PROVIDERS[provider]?.noAuth === true;\n if (!provider || (!apiKey && provider !== \"ollama-local\" && !isNoAuth)) {\n return NextResponse.json({ error: \"Provider and API key required\" }, { status: 400 });\n }\n\n let isValid = false;\n let error = null;\n\n // Validate with each provider\n try {\n if (isOpenAICompatibleProvider(provider)) {\n const node = await getProviderNodeById(provider);\n if (!node) {\n return NextResponse.json({ error: \"OpenAI Compatible node not found\" }, { status: 404 });\n }\n const modelsUrl = `${node.baseUrl?.replace(/\\/$/, \"\")}/models`;\n const res = await fetch(modelsUrl, {\n headers: {\n \"Authorization\": `Bearer ${apiKey}`,\n ...normalizeCustomHeaders(node.customHeaders),\n },\n });\n isValid = res.ok;\n return NextResponse.json({\n valid: isValid,\n error: isValid ? null : \"Invalid API key\",\n });\n }\n\n // Custom Embedding nodes: probe /models (most embedding APIs are OpenAI-compatible)\n if (isCustomEmbeddingProvider(provider)) {\n const node = await getProviderNodeById(provider);\n if (!node) {\n return NextResponse.json({ error: \"Custom Embedding node not found\" }, { status: 404 });\n }\n const baseUrl = node.baseUrl?.replace(/\\/$/, \"\");\n const modelsRes = await fetch(`${baseUrl}/models`, {\n headers: { \"Authorization\": `Bearer ${apiKey}` },\n });\n if (modelsRes.ok) {\n return NextResponse.json({ valid: true });\n }\n // Auth errors are definitive\n if (modelsRes.status === 401 || modelsRes.status === 403) {\n return NextResponse.json({ valid: false, error: \"Invalid API key\" });\n }\n // Fallback: probe /embeddings with a common test model — many providers lack /models\n const embedRes = await fetch(`${baseUrl}/embeddings`, {\n method: \"POST\",\n headers: { \"Authorization\": `Bearer ${apiKey}`, \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ model: \"test\", input: \"ping\" }),\n });\n // 401/403 = bad key; anything else (including 400 \"model not found\") means key works\n isValid = embedRes.status !== 401 && embedRes.status !== 403;\n return NextResponse.json({\n valid: isValid,\n error: isValid ? null : \"Invalid API key\",\n });\n }\n\n if (isAnthropicCompatibleProvider(provider)) {\n const node = await getProviderNodeById(provider);\n if (!node) {\n return NextResponse.json({ error: \"Anthropic Compatible node not found\" }, { status: 404 });\n }\n\n let normalizedBase = node.baseUrl?.trim().replace(/\\/$/, \"\") || \"\";\n if (normalizedBase.endsWith(\"/messages\")) {\n normalizedBase = normalizedBase.slice(0, -9); // remove /messages\n }\n\n const modelsUrl = `${normalizedBase}/models`;\n\n const res = await fetch(modelsUrl, {\n headers: {\n \"x-api-key\": apiKey,\n \"anthropic-version\": \"2023-06-01\",\n \"Authorization\": `Bearer ${apiKey}`\n },\n });\n\n isValid = res.ok;\n return NextResponse.json({\n valid: isValid,\n error: isValid ? null : \"Invalid API key\",\n });\n }\n\n if (provider === \"cloudflare-ai\") {\n const { providerSpecificData } = body;\n const accountId = providerSpecificData?.accountId;\n if (!accountId) {\n return NextResponse.json({ valid: false, error: \"Missing Account ID\" });\n }\n const url = `https://api.cloudflare.com/client/v4/accounts/${accountId}/ai/v1/chat/completions`;\n const cfRes = await fetch(url, {\n method: \"POST\",\n headers: { \"Authorization\": `Bearer ${apiKey}`, \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n model: getDefaultModel(\"cloudflare-ai\"),\n messages: [{ role: \"user\", content: \"test\" }],\n max_tokens: 1,\n }),\n });\n isValid = cfRes.status !== 401 && cfRes.status !== 403 && cfRes.status !== 404;\n return NextResponse.json({\n valid: isValid,\n error: isValid ? null : \"Invalid API token or Account ID\",\n });\n }\n\n if (provider === \"azure\") {\n const { providerSpecificData } = body;\n const endpoint = (providerSpecificData?.azureEndpoint || \"\").replace(/\\/$/, \"\");\n const deployment = providerSpecificData?.deployment || \"gpt-4\";\n const apiVersion = providerSpecificData?.apiVersion || \"2024-10-01-preview\";\n const organization = providerSpecificData?.organization;\n\n const url = `${endpoint}/openai/deployments/${deployment}/chat/completions?api-version=${apiVersion}`;\n const headers = {\n \"api-key\": apiKey,\n \"Content-Type\": \"application/json\",\n };\n if (organization) headers[\"OpenAI-Organization\"] = organization;\n\n const azureRes = await fetch(url, {\n method: \"POST\",\n headers,\n body: JSON.stringify({\n messages: [{ role: \"user\", content: \"test\" }],\n max_tokens: 1,\n }),\n });\n isValid = azureRes.status !== 401 && azureRes.status !== 403;\n return NextResponse.json({\n valid: isValid,\n error: isValid ? null : \"Invalid API key or Azure configuration\",\n });\n }\n\n // Generic probe for webSearch/webFetch providers (config-driven)\n const webResult = await probeWebProvider(provider, apiKey);\n if (webResult !== null) {\n return NextResponse.json({\n valid: webResult,\n error: webResult ? null : \"Invalid API key\",\n });\n }\n\n // Generic probe for tts/embedding providers (config-driven)\n const mediaResult = await probeMediaProvider(provider, apiKey);\n if (mediaResult !== null) {\n return NextResponse.json({\n valid: mediaResult,\n error: mediaResult ? null : \"Invalid API key\",\n });\n }\n\n switch (provider) {\n case \"openai\":\n const openaiRes = await fetch(\"https://api.openai.com/v1/models\", {\n headers: { \"Authorization\": `Bearer ${apiKey}` },\n });\n isValid = openaiRes.ok;\n break;\n\n case \"vercel-ai-gateway\":\n const vercelAiGatewayRes = await fetch(\"https://ai-gateway.vercel.sh/v1/models\", {\n headers: { \"Authorization\": `Bearer ${apiKey}` },\n });\n isValid = vercelAiGatewayRes.ok;\n break;\n\n case \"anthropic\":\n const anthropicRes = await fetch(\"https://api.anthropic.com/v1/messages\", {\n method: \"POST\",\n headers: {\n \"x-api-key\": apiKey,\n \"anthropic-version\": \"2023-06-01\",\n \"content-type\": \"application/json\",\n },\n body: JSON.stringify({\n model: \"claude-3-haiku-20240307\",\n max_tokens: 1,\n messages: [{ role: \"user\", content: \"test\" }],\n }),\n });\n isValid = anthropicRes.status !== 401;\n break;\n\n case \"gemini\":\n const geminiRes = await fetch(`https://generativelanguage.googleapis.com/v1/models?key=${apiKey}`);\n isValid = geminiRes.ok;\n break;\n\n case \"openrouter\":\n const openrouterRes = await fetch(\"https://openrouter.ai/api/v1/models\", {\n headers: { \"Authorization\": `Bearer ${apiKey}` },\n });\n isValid = openrouterRes.ok;\n break;\n\n case \"glm\":\n case \"glm-cn\":\n case \"kimi\":\n case \"minimax\":\n case \"minimax-cn\":\n case \"alicode-intl\":\n case \"alicode\":\n case \"agentrouter\": {\n // Use baseUrl from PROVIDERS (DRY); separate openai-format vs claude-format flow\n const cfg = PROVIDERS[provider];\n const isOpenAiFormat = provider === \"glm-cn\" || provider === \"alicode\" || provider === \"alicode-intl\";\n\n if (isOpenAiFormat) {\n const testModel = getDefaultModel(provider);\n const res = await fetch(cfg.baseUrl, {\n method: \"POST\",\n headers: { \"Authorization\": `Bearer ${apiKey}`, \"content-type\": \"application/json\" },\n body: JSON.stringify({ model: testModel, max_tokens: 1, messages: [{ role: \"user\", content: \"test\" }] }),\n });\n isValid = res.status !== 401 && res.status !== 403;\n } else {\n const testModel = getDefaultModel(provider) || \"claude-sonnet-4-20250514\";\n const res = await fetch(cfg.baseUrl, {\n method: \"POST\",\n headers: {\n \"x-api-key\": apiKey,\n \"anthropic-version\": \"2023-06-01\",\n \"content-type\": \"application/json\",\n ...(cfg.headers || {}),\n },\n body: JSON.stringify({ model: testModel, max_tokens: 1, messages: [{ role: \"user\", content: \"test\" }] }),\n });\n // 400 = model resolution error but auth passed (e.g. agentrouter \"no available channel\")\n isValid = res.status !== 401 && res.status !== 403;\n }\n break;\n }\n case \"volcengine-ark\":\n case \"byteplus\": {\n const res = await fetch(PROVIDER_ENDPOINTS[provider], {\n method: \"POST\",\n headers: {\n \"Authorization\": `Bearer ${apiKey}`,\n \"content-type\": \"application/json\",\n },\n body: JSON.stringify({\n model: getDefaultModel(provider),\n max_tokens: 1,\n messages: [{ role: \"user\", content: \"test\" }],\n }),\n });\n isValid = res.status !== 401 && res.status !== 403;\n break;\n }\n\n case \"deepseek\":\n case \"groq\":\n case \"xai\":\n case \"mistral\":\n case \"perplexity\":\n case \"together\":\n case \"fireworks\":\n case \"cerebras\":\n case \"cohere\":\n case \"nebius\":\n case \"siliconflow\":\n case \"hyperbolic\":\n case \"ollama\":\n case \"ollama-local\":\n case \"assemblyai\":\n case \"nanobanana\":\n case \"chutes\":\n case \"xiaomi-mimo\":\n case \"xiaomi-tokenplan\":\n case \"nvidia\": {\n const endpoints = {\n deepseek: \"https://api.deepseek.com/models\",\n groq: \"https://api.groq.com/openai/v1/models\",\n xai: \"https://api.x.ai/v1/models\",\n mistral: \"https://api.mistral.ai/v1/models\",\n perplexity: \"https://api.perplexity.ai/models\",\n together: \"https://api.together.xyz/v1/models\",\n fireworks: \"https://api.fireworks.ai/inference/v1/models\",\n cerebras: \"https://api.cerebras.ai/v1/models\",\n cohere: \"https://api.cohere.ai/v1/models\",\n nebius: \"https://api.studio.nebius.ai/v1/models\",\n siliconflow: \"https://api.siliconflow.cn/v1/models\",\n hyperbolic: \"https://api.hyperbolic.xyz/v1/models\",\n ollama: \"https://ollama.com/api/tags\",\n \"ollama-local\": `${resolveOllamaLocalHost({ providerSpecificData })}/api/tags`,\n assemblyai: \"https://api.assemblyai.com/v1/account\",\n nanobanana: \"https://api.nanobananaapi.ai/v1/models\",\n chutes: \"https://llm.chutes.ai/v1/models\",\n nvidia: \"https://integrate.api.nvidia.com/v1/models\",\n \"xiaomi-mimo\": \"https://api.xiaomimimo.com/v1/models\",\n \"xiaomi-tokenplan\": \"https://token-plan-sgp.xiaomimimo.com/v1/models\"\n };\n const headers = {};\n if (apiKey) headers[\"Authorization\"] = `Bearer ${apiKey}`;\n const res = await fetch(endpoints[provider], { headers });\n // xai returns 400 for bad key, 403 for valid-but-no-credit. Other providers use 401.\n if (provider === \"xai\") {\n isValid = res.status === 200 || res.status === 403;\n } else {\n isValid = res.ok;\n }\n break;\n }\n\n case \"opencode-go\": {\n const res = await fetch(\"https://opencode.ai/zen/go/v1/chat/completions\", {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", \"Authorization\": `Bearer ${apiKey}` },\n body: JSON.stringify({\n model: getDefaultModel(\"opencode-go\"),\n messages: [{ role: \"user\", content: \"ping\" }],\n max_tokens: 1,\n stream: false,\n }),\n });\n isValid = res.status !== 401 && res.status !== 403;\n break;\n }\n\n case \"commandcode\": {\n const cfg = PROVIDERS.commandcode;\n const model = getDefaultModel(\"commandcode\");\n const payload = openaiToCommandCode(model, {\n messages: [{ role: \"user\", content: \"ping\" }],\n max_tokens: 1,\n stream: false,\n }, false);\n const res = await fetch(cfg.baseUrl, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n ...(cfg.headers || {}),\n \"x-session-id\": crypto.randomUUID(),\n \"Authorization\": `Bearer ${apiKey}`,\n },\n body: JSON.stringify(payload),\n });\n isValid = res.status !== 401 && res.status !== 403;\n break;\n }\n\n case \"deepgram\": {\n const res = await fetch(\"https://api.deepgram.com/v1/projects\", {\n headers: { \"Authorization\": `Token ${apiKey}` },\n });\n isValid = res.ok;\n break;\n }\n\n case \"blackbox\": {\n const res = await fetch(\"https://api.blackbox.ai/chat/completions\", {\n method: \"POST\",\n headers: {\n \"Authorization\": `Bearer ${apiKey}`,\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify({\n model: \"gpt-4o\",\n messages: [{ role: \"user\", content: \"test\" }],\n max_tokens: 10,\n }),\n });\n // Returns 401 for invalid key, 200 for valid, 400 for malformed\n isValid = res.status === 200 || res.status === 400;\n break;\n }\n\n case \"vertex\": {\n // Raw key: probe global endpoint (always 404 for unknown model, never 401)\n // SA JSON: attempt token mint via JWT assertion\n const saJson = (() => { try { const p = JSON.parse(apiKey); return p.type === \"service_account\" ? p : null; } catch { return null; } })();\n if (saJson) {\n // Validate SA JSON has required fields\n isValid = !!(saJson.client_email && saJson.private_key && saJson.project_id);\n } else {\n // Raw key: probe Vertex — 404 means key is valid (model just doesn't exist), 401 means invalid key\n const probeRes = await fetch(\n `https://aiplatform.googleapis.com/v1/publishers/google/models/__probe__:generateContent?key=${apiKey}`,\n { method: \"POST\", headers: { \"Content-Type\": \"application/json\" }, body: \"{}\" }\n );\n isValid = probeRes.status !== 401 && probeRes.status !== 403;\n }\n break;\n }\n\n case \"vertex-partner\": {\n const saJson = (() => { try { const p = JSON.parse(apiKey); return p.type === \"service_account\" ? p : null; } catch { return null; } })();\n if (saJson) {\n isValid = !!(saJson.client_email && saJson.private_key && saJson.project_id);\n } else {\n const probeRes = await fetch(\n `https://aiplatform.googleapis.com/v1/publishers/google/models/__probe__:generateContent?key=${apiKey}`,\n { method: \"POST\", headers: { \"Content-Type\": \"application/json\" }, body: \"{}\" }\n );\n isValid = probeRes.status !== 401 && probeRes.status !== 403;\n }\n break;\n }\n\n case \"grok-web\": {\n const token = apiKey.startsWith(\"sso=\") ? apiKey.slice(4) : apiKey;\n // Cloudflare-bypass: send POST with same browser fingerprint headers as GrokWebExecutor\n const randomHex = (n) => {\n const a = new Uint8Array(n);\n crypto.getRandomValues(a);\n return Array.from(a, (b) => b.toString(16).padStart(2, \"0\")).join(\"\");\n };\n const statsigId = Buffer.from(\"e:TypeError: Cannot read properties of null (reading 'children')\").toString(\"base64\");\n const traceId = randomHex(16);\n const spanId = randomHex(8);\n const res = await fetch(\"https://grok.com/rest/app-chat/conversations/new\", {\n method: \"POST\",\n headers: {\n Accept: \"*/*\",\n \"Accept-Encoding\": \"gzip, deflate, br, zstd\",\n \"Accept-Language\": \"en-US,en;q=0.9\",\n \"Cache-Control\": \"no-cache\",\n \"Content-Type\": \"application/json\",\n Cookie: `sso=${token}`,\n Origin: \"https://grok.com\",\n Pragma: \"no-cache\",\n Referer: \"https://grok.com/\",\n \"Sec-Ch-Ua\": '\"Google Chrome\";v=\"136\", \"Chromium\";v=\"136\", \"Not(A:Brand\";v=\"24\"',\n \"Sec-Ch-Ua-Mobile\": \"?0\",\n \"Sec-Ch-Ua-Platform\": '\"macOS\"',\n \"Sec-Fetch-Dest\": \"empty\",\n \"Sec-Fetch-Mode\": \"cors\",\n \"Sec-Fetch-Site\": \"same-origin\",\n \"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\",\n \"x-statsig-id\": statsigId,\n \"x-xai-request-id\": crypto.randomUUID(),\n traceparent: `00-${traceId}-${spanId}-00`,\n },\n body: JSON.stringify({\n temporary: true, modelName: \"grok-4\", modelMode: \"MODEL_MODE_GROK_4\", message: \"ping\",\n fileAttachments: [], imageAttachments: [],\n disableSearch: false, enableImageGeneration: false, returnImageBytes: false,\n returnRawGrokInXaiRequest: false, enableImageStreaming: false, imageGenerationCount: 0,\n forceConcise: false, toolOverrides: {}, enableSideBySide: true, sendFinalMetadata: true,\n isReasoning: false, disableTextFollowUps: true, disableMemory: true,\n forceSideBySide: false, isAsyncChat: false, disableSelfHarmShortCircuit: false,\n }),\n });\n // Cookie valid = any non-401/403 response (200, 400, 429 all mean cookie accepted)\n if (res.status === 401 || res.status === 403) {\n isValid = false;\n error = \"Invalid SSO cookie — re-paste from grok.com DevTools → Cookies → sso\";\n } else {\n isValid = true;\n }\n break;\n }\n\n case \"perplexity-web\": {\n let sessionToken = apiKey;\n if (sessionToken.startsWith(\"__Secure-next-auth.session-token=\")) {\n sessionToken = sessionToken.slice(\"__Secure-next-auth.session-token=\".length);\n }\n const tz = typeof Intl !== \"undefined\" ? Intl.DateTimeFormat().resolvedOptions().timeZone : \"UTC\";\n const res = await fetch(\"https://www.perplexity.ai/rest/sse/perplexity_ask\", {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Accept: \"text/event-stream\",\n Origin: \"https://www.perplexity.ai\",\n Referer: \"https://www.perplexity.ai/\",\n \"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\",\n \"X-App-ApiClient\": \"default\",\n \"X-App-ApiVersion\": \"2.18\",\n Cookie: `__Secure-next-auth.session-token=${sessionToken}`,\n },\n body: JSON.stringify({\n query_str: \"ping\",\n params: {\n query_str: \"ping\", search_focus: \"internet\", mode: \"concise\", model_preference: \"pplx_pro\",\n sources: [\"web\"], attachments: [],\n frontend_uuid: crypto.randomUUID(), frontend_context_uuid: crypto.randomUUID(),\n version: \"2.18\", language: \"en-US\", timezone: tz,\n search_recency_filter: null, is_incognito: true, use_schematized_api: true, last_backend_uuid: null,\n },\n }),\n });\n if (res.status === 401 || res.status === 403) {\n isValid = false;\n error = \"Invalid session cookie — re-paste __Secure-next-auth.session-token from perplexity.ai\";\n } else {\n isValid = true;\n }\n break;\n }\n\n default: {\n // Generic probe for OpenAI-compatible providers (config-driven from PROVIDERS)\n const cfg = PROVIDERS[provider];\n if (!cfg || cfg.format !== \"openai\" || !cfg.baseUrl) {\n return NextResponse.json({ error: \"Provider validation not supported\" }, { status: 400 });\n }\n if (cfg.noAuth) {\n isValid = true;\n break;\n }\n // Build auth headers based on cfg.authHeader (default: bearer)\n const headers = { \"Content-Type\": \"application/json\", ...(cfg.headers || {}) };\n if (cfg.authHeader === \"x-api-key\") headers[\"X-API-Key\"] = apiKey;\n else headers[\"Authorization\"] = `Bearer ${apiKey}`;\n // Try /models first (fast GET), fallback to chat probe on ambiguous response\n const modelsUrl = cfg.baseUrl.replace(/\\/chat\\/completions$/, \"/models\").replace(/\\/chatbot$/, \"/models\");\n let probeOk = null;\n try {\n const probeRes = await fetch(modelsUrl, { headers, signal: AbortSignal.timeout(8000) });\n if (probeRes.status === 401 || probeRes.status === 403) probeOk = false;\n else if (probeRes.ok) probeOk = true;\n } catch { /* fallback to chat */ }\n if (probeOk !== null) {\n isValid = probeOk;\n break;\n }\n // Fallback: minimal chat probe\n const defaultModel = getDefaultModel(provider) || \"test\";\n const chatRes = await fetch(cfg.baseUrl, {\n method: \"POST\",\n headers,\n body: JSON.stringify({ model: defaultModel, messages: [{ role: \"user\", content: \"ping\" }], max_tokens: 1 }),\n signal: AbortSignal.timeout(10000),\n });\n isValid = chatRes.status !== 401 && chatRes.status !== 403;\n break;\n }\n }\n } catch (err) {\n error = err.message;\n isValid = false;\n }\n\n return NextResponse.json({\n valid: isValid,\n error: isValid ? null : (error || \"Invalid API key\"),\n });\n } catch (error) {\n console.log(\"Error validating API key:\", error);\n return NextResponse.json({ error: \"Validation failed\" }, { status: 500 });\n }\n}\n"}, "417830d5482710b28f8f3d52ecdbe9536d2ee2a2": {"t": 1785758899.0692046, "n": 27421, "h": "966b55bb5ded3a6f", "c": "import { NextResponse } from \"next/server\";\nimport { getProviderNodeById } from \"@/models\";\nimport { isOpenAICompatibleProvider, isAnthropicCompatibleProvider, isCustomEmbeddingProvider, AI_PROVIDERS } from \"@/shared/constants/providers\";\nimport { getDefaultModel } from \"open-sse/config/providerModels.js\";\nimport { resolveOllamaLocalHost, PROVIDERS } from \"open-sse/config/providers.js\";\nimport { openaiToCommandCode } from \"open-sse/translator/request/openai-to-commandcode.js\";\nimport { PROVIDER_ENDPOINTS } from \"@/shared/constants/config\";\nimport { normalizeProviderId } from \"@/lib/providerNormalization\";\nimport { normalizeCustomHeaders } from \"@/shared/utils/customHeaders\";\n\n// Probe a webSearch/webFetch provider using its searchConfig/fetchConfig.\n// Returns true if API key is accepted (status !== 401 && !== 403).\nasync function probeWebProvider(provider, apiKey) {\n const p = AI_PROVIDERS[provider];\n if (!p) return null;\n // Skip if provider has dual-purpose (LLM + search), let LLM validate handle it\n const kinds = p.serviceKinds || [\"llm\"];\n const isWebOnly = kinds.every((k) => k === \"webSearch\" || k === \"webFetch\");\n if (!isWebOnly) return null;\n const cfg = p.searchConfig || p.fetchConfig;\n if (!cfg) return null;\n if (cfg.authType === \"none\") return true; // no-auth (e.g. searxng)\n\n let url = cfg.baseUrl;\n const headers = { \"Content-Type\": \"application/json\" };\n let body;\n\n // Apply auth based on authHeader\n switch (cfg.authHeader) {\n case \"bearer\": headers[\"Authorization\"] = `Bearer ${apiKey}`; break;\n case \"x-api-key\": headers[\"x-api-key\"] = apiKey; break;\n case \"x-subscription-token\":headers[\"x-subscription-token\"] = apiKey; break;\n case \"key\": url += `?key=${encodeURIComponent(apiKey)}&q=ping&cx=test`; break; // google-pse\n case \"api_key\": url += `?api_key=${encodeURIComponent(apiKey)}&q=ping&engine=google`; break; // searchapi\n }\n\n // Minimal body for POST endpoints; GET sends nothing\n if (cfg.method === \"POST\") {\n body = JSON.stringify({ query: \"ping\", q: \"ping\", url: \"https://example.com\" });\n }\n\n const res = await fetch(url, { method: cfg.method, headers, body, signal: AbortSignal.timeout(8000) });\n return res.status !== 401 && res.status !== 403;\n}\n\n// Probe a media provider (tts/embedding/stt/image/video) using *Config.\n// Returns true if API key is accepted; null to skip (let default handler decide).\nasync function probeMediaProvider(provider, apiKey) {\n const p = AI_PROVIDERS[provider];\n if (!p) return null;\n const MEDIA_KINDS = new Set([\"tts\", \"embedding\", \"stt\", \"image\", \"video\", \"music\", \"imageToText\"]);\n const kinds = p.serviceKinds || [\"llm\"];\n const isMediaOnly = kinds.every((k) => MEDIA_KINDS.has(k));\n if (!isMediaOnly) return null;\n const cfg = p.ttsConfig || p.sttConfig || p.embeddingConfig || p.imageConfig || p.videoConfig || p.musicConfig;\n // No probe config → best-effort accept (validate at usage time)\n if (!cfg) return true;\n if (p.noAuth || cfg.authType === \"none\") return true;\n // Skip auth schemes that need provider-specific data\n if (cfg.authHeader === \"playht\" || cfg.authHeader === \"aws-sigv4\") return true;\n\n const headers = { \"Content-Type\": \"application/json\", ...(cfg.extraHeaders || {}) };\n\n switch (cfg.authHeader) {\n case \"bearer\": headers[\"Authorization\"] = `Bearer ${apiKey}`; break;\n case \"key\": headers[\"Authorization\"] = `Key ${apiKey}`; break;\n case \"x-api-key\": headers[\"x-api-key\"] = apiKey; break;\n case \"x-key\": headers[\"x-key\"] = apiKey; break;\n case \"xi-api-key\": headers[\"xi-api-key\"] = apiKey; break;\n case \"token\": headers[\"Authorization\"] = `Token ${apiKey}`; break;\n case \"basic\": headers[\"Authorization\"] = `Basic ${apiKey}`; break;\n default: return null;\n }\n\n const method = cfg.method || \"POST\";\n const res = await fetch(cfg.baseUrl, {\n method,\n headers,\n body: method === \"GET\" ? undefined : JSON.stringify({ input: \"ping\", text: \"ping\", prompt: \"ping\", model: cfg.models?.[0]?.id || \"test\" }),\n signal: AbortSignal.timeout(8000),\n });\n return res.status !== 401 && res.status !== 403;\n}\n\n// POST /api/providers/validate - Validate API key with provider\nexport async function POST(request) {\n try {\n const body = await request.json();\n const provider = normalizeProviderId(body.provider);\n const { apiKey, providerSpecificData } = body;\n\n const isNoAuth = AI_PROVIDERS[provider]?.noAuth === true;\n if (!provider || (!apiKey && provider !== \"ollama-local\" && !isNoAuth)) {\n return NextResponse.json({ error: \"Provider and API key required\" }, { status: 400 });\n }\n\n let isValid = false;\n let error = null;\n\n // Validate with each provider\n try {\n if (isOpenAICompatibleProvider(provider)) {\n const node = await getProviderNodeById(provider);\n if (!node) {\n return NextResponse.json({ error: \"OpenAI Compatible node not found\" }, { status: 404 });\n }\n const modelsUrl = `${node.baseUrl?.replace(/\\/$/, \"\")}/models`;\n const res = await fetch(modelsUrl, {\n headers: {\n \"Authorization\": `Bearer ${apiKey}`,\n ...normalizeCustomHeaders(node.customHeaders),\n },\n });\n isValid = res.ok;\n return NextResponse.json({\n valid: isValid,\n error: isValid ? null : \"Invalid API key\",\n });\n }\n\n // Custom Embedding nodes: probe /models (most embedding APIs are OpenAI-compatible)\n if (isCustomEmbeddingProvider(provider)) {\n const node = await getProviderNodeById(provider);\n if (!node) {\n return NextResponse.json({ error: \"Custom Embedding node not found\" }, { status: 404 });\n }\n const baseUrl = node.baseUrl?.replace(/\\/$/, \"\");\n const modelsRes = await fetch(`${baseUrl}/models`, {\n headers: { \"Authorization\": `Bearer ${apiKey}` },\n });\n if (modelsRes.ok) {\n return NextResponse.json({ valid: true });\n }\n // Auth errors are definitive\n if (modelsRes.status === 401 || modelsRes.status === 403) {\n return NextResponse.json({ valid: false, error: \"Invalid API key\" });\n }\n // Fallback: probe /embeddings with a common test model — many providers lack /models\n const embedRes = await fetch(`${baseUrl}/embeddings`, {\n method: \"POST\",\n headers: { \"Authorization\": `Bearer ${apiKey}`, \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ model: \"test\", input: \"ping\" }),\n });\n // 401/403 = bad key; anything else (including 400 \"model not found\") means key works\n isValid = embedRes.status !== 401 && embedRes.status !== 403;\n return NextResponse.json({\n valid: isValid,\n error: isValid ? null : \"Invalid API key\",\n });\n }\n\n if (isAnthropicCompatibleProvider(provider)) {\n const node = await getProviderNodeById(provider);\n if (!node) {\n return NextResponse.json({ error: \"Anthropic Compatible node not found\" }, { status: 404 });\n }\n\n let normalizedBase = node.baseUrl?.trim().replace(/\\/$/, \"\") || \"\";\n if (normalizedBase.endsWith(\"/messages\")) {\n normalizedBase = normalizedBase.slice(0, -9); // remove /messages\n }\n\n const modelsUrl = `${normalizedBase}/models`;\n\n const res = await fetch(modelsUrl, {\n headers: {\n \"x-api-key\": apiKey,\n \"anthropic-version\": \"2023-06-01\",\n \"Authorization\": `Bearer ${apiKey}`\n },\n });\n\n isValid = res.ok;\n return NextResponse.json({\n valid: isValid,\n error: isValid ? null : \"Invalid API key\",\n });\n }\n\n if (provider === \"cloudflare-ai\") {\n const { providerSpecificData } = body;\n const accountId = providerSpecificData?.accountId;\n if (!accountId) {\n return NextResponse.json({ valid: false, error: \"Missing Account ID\" });\n }\n const url = `https://api.cloudflare.com/client/v4/accounts/${accountId}/ai/v1/chat/completions`;\n const cfRes = await fetch(url, {\n method: \"POST\",\n headers: { \"Authorization\": `Bearer ${apiKey}`, \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n model: getDefaultModel(\"cloudflare-ai\"),\n messages: [{ role: \"user\", content: \"test\" }],\n max_tokens: 1,\n }),\n });\n isValid = cfRes.status !== 401 && cfRes.status !== 403 && cfRes.status !== 404;\n return NextResponse.json({\n valid: isValid,\n error: isValid ? null : \"Invalid API token or Account ID\",\n });\n }\n\n if (provider === \"azure\") {\n const { providerSpecificData } = body;\n const endpoint = (providerSpecificData?.azureEndpoint || \"\").replace(/\\/$/, \"\");\n const deployment = providerSpecificData?.deployment || \"gpt-4\";\n const apiVersion = providerSpecificData?.apiVersion || \"2024-10-01-preview\";\n const organization = providerSpecificData?.organization;\n\n const url = `${endpoint}/openai/deployments/${deployment}/chat/completions?api-version=${apiVersion}`;\n const headers = {\n \"api-key\": apiKey,\n \"Content-Type\": \"application/json\",\n };\n if (organization) headers[\"OpenAI-Organization\"] = organization;\n\n const azureRes = await fetch(url, {\n method: \"POST\",\n headers,\n body: JSON.stringify({\n messages: [{ role: \"user\", content: \"test\" }],\n max_tokens: 1,\n }),\n });\n isValid = azureRes.status !== 401 && azureRes.status !== 403;\n return NextResponse.json({\n valid: isValid,\n error: isValid ? null : \"Invalid API key or Azure configuration\",\n });\n }\n\n // Generic probe for webSearch/webFetch providers (config-driven)\n const webResult = await probeWebProvider(provider, apiKey);\n if (webResult !== null) {\n return NextResponse.json({\n valid: webResult,\n error: webResult ? null : \"Invalid API key\",\n });\n }\n\n // Generic probe for tts/embedding providers (config-driven)\n const mediaResult = await probeMediaProvider(provider, apiKey);\n if (mediaResult !== null) {\n return NextResponse.json({\n valid: mediaResult,\n error: mediaResult ? null : \"Invalid API key\",\n });\n }\n\n switch (provider) {\n case \"openai\":\n const openaiRes = await fetch(\"https://api.openai.com/v1/models\", {\n headers: { \"Authorization\": `Bearer ${apiKey}` },\n });\n isValid = openaiRes.ok;\n break;\n\n case \"vercel-ai-gateway\":\n const vercelAiGatewayRes = await fetch(\"https://ai-gateway.vercel.sh/v1/models\", {\n headers: { \"Authorization\": `Bearer ${apiKey}` },\n });\n isValid = vercelAiGatewayRes.ok;\n break;\n\n case \"anthropic\":\n const anthropicRes = await fetch(\"https://api.anthropic.com/v1/messages\", {\n method: \"POST\",\n headers: {\n \"x-api-key\": apiKey,\n \"anthropic-version\": \"2023-06-01\",\n \"content-type\": \"application/json\",\n },\n body: JSON.stringify({\n model: \"claude-3-haiku-20240307\",\n max_tokens: 1,\n messages: [{ role: \"user\", content: \"test\" }],\n }),\n });\n isValid = anthropicRes.status !== 401;\n break;\n\n case \"gemini\":\n const geminiRes = await fetch(`https://generativelanguage.googleapis.com/v1/models?key=${apiKey}`);\n isValid = geminiRes.ok;\n break;\n\n case \"openrouter\":\n const openrouterRes = await fetch(\"https://openrouter.ai/api/v1/models\", {\n headers: { \"Authorization\": `Bearer ${apiKey}` },\n });\n isValid = openrouterRes.ok;\n break;\n\n case \"glm\":\n case \"glm-cn\":\n case \"kimi\":\n case \"minimax\":\n case \"minimax-cn\":\n case \"alicode-intl\":\n case \"alicode\":\n case \"agentrouter\": {\n // Use baseUrl from PROVIDERS (DRY); separate openai-format vs claude-format flow\n const cfg = PROVIDERS[provider];\n const isOpenAiFormat = provider === \"glm-cn\" || provider === \"alicode\" || provider === \"alicode-intl\";\n\n if (isOpenAiFormat) {\n const testModel = getDefaultModel(provider);\n const res = await fetch(cfg.baseUrl, {\n method: \"POST\",\n headers: { \"Authorization\": `Bearer ${apiKey}`, \"content-type\": \"application/json\" },\n body: JSON.stringify({ model: testModel, max_tokens: 1, messages: [{ role: \"user\", content: \"test\" }] }),\n });\n isValid = res.status !== 401 && res.status !== 403;\n } else {\n const testModel = getDefaultModel(provider) || \"claude-sonnet-4-20250514\";\n const res = await fetch(cfg.baseUrl, {\n method: \"POST\",\n headers: {\n \"x-api-key\": apiKey,\n \"anthropic-version\": \"2023-06-01\",\n \"content-type\": \"application/json\",\n ...(cfg.headers || {}),\n },\n body: JSON.stringify({ model: testModel, max_tokens: 1, messages: [{ role: \"user\", content: \"test\" }] }),\n });\n // 400 = model resolution error but auth passed (e.g. agentrouter \"no available channel\")\n isValid = res.status !== 401 && res.status !== 403;\n }\n break;\n }\n case \"volcengine-ark\":\n case \"byteplus\": {\n const res = await fetch(PROVIDER_ENDPOINTS[provider], {\n method: \"POST\",\n headers: {\n \"Authorization\": `Bearer ${apiKey}`,\n \"content-type\": \"application/json\",\n },\n body: JSON.stringify({\n model: getDefaultModel(provider),\n max_tokens: 1,\n messages: [{ role: \"user\", content: \"test\" }],\n }),\n });\n isValid = res.status !== 401 && res.status !== 403;\n break;\n }\n\n case \"deepseek\":\n case \"groq\":\n case \"xai\":\n case \"mistral\":\n case \"perplexity\":\n case \"together\":\n case \"fireworks\":\n case \"cerebras\":\n case \"cohere\":\n case \"nebius\":\n case \"siliconflow\":\n case \"hyperbolic\":\n case \"ollama\":\n case \"ollama-local\":\n case \"assemblyai\":\n case \"nanobanana\":\n case \"chutes\":\n case \"xiaomi-mimo\":\n case \"xiaomi-tokenplan\":\n case \"nvidia\": {\n const endpoints = {\n deepseek: \"https://api.deepseek.com/models\",\n groq: \"https://api.groq.com/openai/v1/models\",\n xai: \"https://api.x.ai/v1/models\",\n mistral: \"https://api.mistral.ai/v1/models\",\n perplexity: \"https://api.perplexity.ai/models\",\n together: \"https://api.together.xyz/v1/models\",\n fireworks: \"https://api.fireworks.ai/inference/v1/models\",\n cerebras: \"https://api.cerebras.ai/v1/models\",\n cohere: \"https://api.cohere.ai/v1/models\",\n nebius: \"https://api.studio.nebius.ai/v1/models\",\n siliconflow: \"https://api.siliconflow.cn/v1/models\",\n hyperbolic: \"https://api.hyperbolic.xyz/v1/models\",\n ollama: \"https://ollama.com/api/tags\",\n \"ollama-local\": `${resolveOllamaLocalHost({ providerSpecificData })}/api/tags`,\n assemblyai: \"https://api.assemblyai.com/v1/account\",\n nanobanana: \"https://api.nanobananaapi.ai/v1/models\",\n chutes: \"https://llm.chutes.ai/v1/models\",\n nvidia: \"https://integrate.api.nvidia.com/v1/models\",\n \"xiaomi-mimo\": \"https://api.xiaomimimo.com/v1/models\",\n \"xiaomi-tokenplan\": \"https://token-plan-sgp.xiaomimimo.com/v1/models\"\n };\n const headers = {};\n if (apiKey) headers[\"Authorization\"] = `Bearer ${apiKey}`;\n const res = await fetch(endpoints[provider], { headers });\n // xai returns 400 for bad key, 403 for valid-but-no-credit. Other providers use 401.\n if (provider === \"xai\") {\n isValid = res.status === 200 || res.status === 403;\n } else {\n isValid = res.ok;\n }\n break;\n }\n\n case \"opencode-go\": {\n const res = await fetch(\"https://opencode.ai/zen/go/v1/chat/completions\", {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", \"Authorization\": `Bearer ${apiKey}` },\n body: JSON.stringify({\n model: getDefaultModel(\"opencode-go\"),\n messages: [{ role: \"user\", content: \"ping\" }],\n max_tokens: 1,\n stream: false,\n }),\n });\n isValid = res.status !== 401 && res.status !== 403;\n break;\n }\n\n case \"commandcode\": {\n const cfg = PROVIDERS.commandcode;\n const model = getDefaultModel(\"commandcode\");\n const payload = openaiToCommandCode(model, {\n messages: [{ role: \"user\", content: \"ping\" }],\n max_tokens: 1,\n stream: false,\n }, false);\n const res = await fetch(cfg.baseUrl, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n ...(cfg.headers || {}),\n \"x-session-id\": crypto.randomUUID(),\n \"Authorization\": `Bearer ${apiKey}`,\n },\n body: JSON.stringify(payload),\n });\n isValid = res.status !== 401 && res.status !== 403;\n break;\n }\n\n case \"deepgram\": {\n const res = await fetch(\"https://api.deepgram.com/v1/projects\", {\n headers: { \"Authorization\": `Token ${apiKey}` },\n });\n isValid = res.ok;\n break;\n }\n\n case \"blackbox\": {\n const res = await fetch(\"https://api.blackbox.ai/chat/completions\", {\n method: \"POST\",\n headers: {\n \"Authorization\": `Bearer ${apiKey}`,\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify({\n model: \"gpt-4o\",\n messages: [{ role: \"user\", content: \"test\" }],\n max_tokens: 10,\n }),\n });\n // Returns 401 for invalid key, 200 for valid, 400 for malformed\n isValid = res.status === 200 || res.status === 400;\n break;\n }\n\n case \"vertex\": {\n // Raw key: probe global endpoint (always 404 for unknown model, never 401)\n // SA JSON: attempt token mint via JWT assertion\n const saJson = (() => { try { const p = JSON.parse(apiKey); return p.type === \"service_account\" ? p : null; } catch { return null; } })();\n if (saJson) {\n // Validate SA JSON has required fields\n isValid = !!(saJson.client_email && saJson.private_key && saJson.project_id);\n } else {\n // Raw key: probe Vertex — 404 means key is valid (model just doesn't exist), 401 means invalid key\n const probeRes = await fetch(\n `https://aiplatform.googleapis.com/v1/publishers/google/models/__probe__:generateContent?key=${apiKey}`,\n { method: \"POST\", headers: { \"Content-Type\": \"application/json\" }, body: \"{}\" }\n );\n isValid = probeRes.status !== 401 && probeRes.status !== 403;\n }\n break;\n }\n\n case \"vertex-partner\": {\n const saJson = (() => { try { const p = JSON.parse(apiKey); return p.type === \"service_account\" ? p : null; } catch { return null; } })();\n if (saJson) {\n isValid = !!(saJson.client_email && saJson.private_key && saJson.project_id);\n } else {\n const probeRes = await fetch(\n `https://aiplatform.googleapis.com/v1/publishers/google/models/__probe__:generateContent?key=${apiKey}`,\n { method: \"POST\", headers: { \"Content-Type\": \"application/json\" }, body: \"{}\" }\n );\n isValid = probeRes.status !== 401 && probeRes.status !== 403;\n }\n break;\n }\n\n case \"grok-web\": {\n const token = apiKey.startsWith(\"sso=\") ? apiKey.slice(4) : apiKey;\n // Cloudflare-bypass: send POST with same browser fingerprint headers as GrokWebExecutor\n const randomHex = (n) => {\n const a = new Uint8Array(n);\n crypto.getRandomValues(a);\n return Array.from(a, (b) => b.toString(16).padStart(2, \"0\")).join(\"\");\n };\n const statsigId = Buffer.from(\"e:TypeError: Cannot read properties of null (reading 'children')\").toString(\"base64\");\n const traceId = randomHex(16);\n const spanId = randomHex(8);\n const res = await fetch(\"https://grok.com/rest/app-chat/conversations/new\", {\n method: \"POST\",\n headers: {\n Accept: \"*/*\",\n \"Accept-Encoding\": \"gzip, deflate, br, zstd\",\n \"Accept-Language\": \"en-US,en;q=0.9\",\n \"Cache-Control\": \"no-cache\",\n \"Content-Type\": \"application/json\",\n Cookie: `sso=${token}`,\n Origin: \"https://grok.com\",\n Pragma: \"no-cache\",\n Referer: \"https://grok.com/\",\n \"Sec-Ch-Ua\": '\"Google Chrome\";v=\"136\", \"Chromium\";v=\"136\", \"Not(A:Brand\";v=\"24\"',\n \"Sec-Ch-Ua-Mobile\": \"?0\",\n \"Sec-Ch-Ua-Platform\": '\"macOS\"',\n \"Sec-Fetch-Dest\": \"empty\",\n \"Sec-Fetch-Mode\": \"cors\",\n \"Sec-Fetch-Site\": \"same-origin\",\n \"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\",\n \"x-statsig-id\": statsigId,\n \"x-xai-request-id\": crypto.randomUUID(),\n traceparent: `00-${traceId}-${spanId}-00`,\n },\n body: JSON.stringify({\n temporary: true, modelName: \"grok-4\", modelMode: \"MODEL_MODE_GROK_4\", message: \"ping\",\n fileAttachments: [], imageAttachments: [],\n disableSearch: false, enableImageGeneration: false, returnImageBytes: false,\n returnRawGrokInXaiRequest: false, enableImageStreaming: false, imageGenerationCount: 0,\n forceConcise: false, toolOverrides: {}, enableSideBySide: true, sendFinalMetadata: true,\n isReasoning: false, disableTextFollowUps: true, disableMemory: true,\n forceSideBySide: false, isAsyncChat: false, disableSelfHarmShortCircuit: false,\n }),\n });\n // Cookie valid = any non-401/403 response (200, 400, 429 all mean cookie accepted)\n if (res.status === 401 || res.status === 403) {\n isValid = false;\n error = \"Invalid SSO cookie — re-paste from grok.com DevTools → Cookies → sso\";\n } else {\n isValid = true;\n }\n break;\n }\n\n case \"perplexity-web\": {\n let sessionToken = apiKey;\n if (sessionToken.startsWith(\"__Secure-next-auth.session-token=\")) {\n sessionToken = sessionToken.slice(\"__Secure-next-auth.session-token=\".length);\n }\n const tz = typeof Intl !== \"undefined\" ? Intl.DateTimeFormat().resolvedOptions().timeZone : \"UTC\";\n const res = await fetch(\"https://www.perplexity.ai/rest/sse/perplexity_ask\", {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Accept: \"text/event-stream\",\n Origin: \"https://www.perplexity.ai\",\n Referer: \"https://www.perplexity.ai/\",\n \"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\",\n \"X-App-ApiClient\": \"default\",\n \"X-App-ApiVersion\": \"2.18\",\n Cookie: `__Secure-next-auth.session-token=${sessionToken}`,\n },\n body: JSON.stringify({\n query_str: \"ping\",\n params: {\n query_str: \"ping\", search_focus: \"internet\", mode: \"concise\", model_preference: \"pplx_pro\",\n sources: [\"web\"], attachments: [],\n frontend_uuid: crypto.randomUUID(), frontend_context_uuid: crypto.randomUUID(),\n version: \"2.18\", language: \"en-US\", timezone: tz,\n search_recency_filter: null, is_incognito: true, use_schematized_api: true, last_backend_uuid: null,\n },\n }),\n });\n if (res.status === 401 || res.status === 403) {\n isValid = false;\n error = \"Invalid session cookie — re-paste __Secure-next-auth.session-token from perplexity.ai\";\n } else {\n isValid = true;\n }\n break;\n }\n\n default: {\n // Generic probe for OpenAI-compatible providers (config-driven from PROVIDERS)\n const cfg = PROVIDERS[provider];\n if (!cfg || cfg.format !== \"openai\" || !cfg.baseUrl) {\n return NextResponse.json({ error: \"Provider validation not supported\" }, { status: 400 });\n }\n if (cfg.noAuth) {\n isValid = true;\n break;\n }\n // Build auth headers based on cfg.authHeader (default: bearer)\n const headers = { \"Content-Type\": \"application/json\", ...(cfg.headers || {}) };\n if (cfg.authHeader === \"x-api-key\") headers[\"X-API-Key\"] = apiKey;\n else headers[\"Authorization\"] = `Bearer ${apiKey}`;\n // Try /models first (fast GET), fallback to chat probe on ambiguous response\n const modelsUrl = cfg.baseUrl.replace(/\\/chat\\/completions$/, \"/models\").replace(/\\/chatbot$/, \"/models\");\n let probeOk = null;\n try {\n const probeRes = await fetch(modelsUrl, { headers, signal: AbortSignal.timeout(8000) });\n if (probeRes.status === 401 || probeRes.status === 403) probeOk = false;\n else if (probeRes.ok) probeOk = true;\n } catch { /* fallback to chat */ }\n if (probeOk !== null) {\n isValid = probeOk;\n break;\n }\n // Fallback: minimal chat probe\n const defaultModel = getDefaultModel(provider) || \"test\";\n const chatRes = await fetch(cfg.baseUrl, {\n method: \"POST\",\n headers,\n body: JSON.stringify({ model: defaultModel, messages: [{ role: \"user\", content: \"ping\" }], max_tokens: 1 }),\n signal: AbortSignal.timeout(10000),\n });\n isValid = chatRes.status !== 401 && chatRes.status !== 403;\n break;\n }\n }\n } catch (err) {\n error = err.message;\n isValid = false;\n }\n\n return NextResponse.json({\n valid: isValid,\n error: isValid ? null : (error || \"Invalid API key\"),\n });\n } catch (error) {\n console.log(\"Error validating API key:\", error);\n return NextResponse.json({ error: \"Validation failed\" }, { status: 500 });\n }\n}\n"}, "3fff67f25a6473fc7f51236c934493a990d2a3bf": {"t": 1785758902.578701, "n": 27268, "h": "9e91d85b46210f4b", "c": "import { NextResponse } from \"next/server\";\nimport { getProviderNodeById } from \"@/models\";\nimport { isOpenAICompatibleProvider, isAnthropicCompatibleProvider, isCustomEmbeddingProvider, AI_PROVIDERS } from \"@/shared/constants/providers\";\nimport { getDefaultModel } from \"open-sse/config/providerModels.js\";\nimport { resolveOllamaLocalHost, PROVIDERS } from \"open-sse/config/providers.js\";\nimport { openaiToCommandCode } from \"open-sse/translator/request/openai-to-commandcode.js\";\nimport { PROVIDER_ENDPOINTS } from \"@/shared/constants/config\";\nimport { normalizeProviderId } from \"@/lib/providerNormalization\";\n\n// Probe a webSearch/webFetch provider using its searchConfig/fetchConfig.\n// Returns true if API key is accepted (status !== 401 && !== 403).\nasync function probeWebProvider(provider, apiKey) {\n const p = AI_PROVIDERS[provider];\n if (!p) return null;\n // Skip if provider has dual-purpose (LLM + search), let LLM validate handle it\n const kinds = p.serviceKinds || [\"llm\"];\n const isWebOnly = kinds.every((k) => k === \"webSearch\" || k === \"webFetch\");\n if (!isWebOnly) return null;\n const cfg = p.searchConfig || p.fetchConfig;\n if (!cfg) return null;\n if (cfg.authType === \"none\") return true; // no-auth (e.g. searxng)\n\n let url = cfg.baseUrl;\n const headers = { \"Content-Type\": \"application/json\" };\n let body;\n\n // Apply auth based on authHeader\n switch (cfg.authHeader) {\n case \"bearer\": headers[\"Authorization\"] = `Bearer ${apiKey}`; break;\n case \"x-api-key\": headers[\"x-api-key\"] = apiKey; break;\n case \"x-subscription-token\":headers[\"x-subscription-token\"] = apiKey; break;\n case \"key\": url += `?key=${encodeURIComponent(apiKey)}&q=ping&cx=test`; break; // google-pse\n case \"api_key\": url += `?api_key=${encodeURIComponent(apiKey)}&q=ping&engine=google`; break; // searchapi\n }\n\n // Minimal body for POST endpoints; GET sends nothing\n if (cfg.method === \"POST\") {\n body = JSON.stringify({ query: \"ping\", q: \"ping\", url: \"https://example.com\" });\n }\n\n const res = await fetch(url, { method: cfg.method, headers, body, signal: AbortSignal.timeout(8000) });\n return res.status !== 401 && res.status !== 403;\n}\n\n// Probe a media provider (tts/embedding/stt/image/video) using *Config.\n// Returns true if API key is accepted; null to skip (let default handler decide).\nasync function probeMediaProvider(provider, apiKey) {\n const p = AI_PROVIDERS[provider];\n if (!p) return null;\n const MEDIA_KINDS = new Set([\"tts\", \"embedding\", \"stt\", \"image\", \"video\", \"music\", \"imageToText\"]);\n const kinds = p.serviceKinds || [\"llm\"];\n const isMediaOnly = kinds.every((k) => MEDIA_KINDS.has(k));\n if (!isMediaOnly) return null;\n const cfg = p.ttsConfig || p.sttConfig || p.embeddingConfig || p.imageConfig || p.videoConfig || p.musicConfig;\n // No probe config → best-effort accept (validate at usage time)\n if (!cfg) return true;\n if (p.noAuth || cfg.authType === \"none\") return true;\n // Skip auth schemes that need provider-specific data\n if (cfg.authHeader === \"playht\" || cfg.authHeader === \"aws-sigv4\") return true;\n\n const headers = { \"Content-Type\": \"application/json\", ...(cfg.extraHeaders || {}) };\n\n switch (cfg.authHeader) {\n case \"bearer\": headers[\"Authorization\"] = `Bearer ${apiKey}`; break;\n case \"key\": headers[\"Authorization\"] = `Key ${apiKey}`; break;\n case \"x-api-key\": headers[\"x-api-key\"] = apiKey; break;\n case \"x-key\": headers[\"x-key\"] = apiKey; break;\n case \"xi-api-key\": headers[\"xi-api-key\"] = apiKey; break;\n case \"token\": headers[\"Authorization\"] = `Token ${apiKey}`; break;\n case \"basic\": headers[\"Authorization\"] = `Basic ${apiKey}`; break;\n default: return null;\n }\n\n const method = cfg.method || \"POST\";\n const res = await fetch(cfg.baseUrl, {\n method,\n headers,\n body: method === \"GET\" ? undefined : JSON.stringify({ input: \"ping\", text: \"ping\", prompt: \"ping\", model: cfg.models?.[0]?.id || \"test\" }),\n signal: AbortSignal.timeout(8000),\n });\n return res.status !== 401 && res.status !== 403;\n}\n\n// POST /api/providers/validate - Validate API key with provider\nexport async function POST(request) {\n try {\n const body = await request.json();\n const provider = normalizeProviderId(body.provider);\n const { apiKey, providerSpecificData } = body;\n\n const isNoAuth = AI_PROVIDERS[provider]?.noAuth === true;\n if (!provider || (!apiKey && provider !== \"ollama-local\" && !isNoAuth)) {\n return NextResponse.json({ error: \"Provider and API key required\" }, { status: 400 });\n }\n\n let isValid = false;\n let error = null;\n\n // Validate with each provider\n try {\n if (isOpenAICompatibleProvider(provider)) {\n const node = await getProviderNodeById(provider);\n if (!node) {\n return NextResponse.json({ error: \"OpenAI Compatible node not found\" }, { status: 404 });\n }\n const modelsUrl = `${node.baseUrl?.replace(/\\/$/, \"\")}/models`;\n const res = await fetch(modelsUrl, {\n headers: { \"Authorization\": `Bearer ${apiKey}` },\n });\n isValid = res.ok;\n return NextResponse.json({\n valid: isValid,\n error: isValid ? null : \"Invalid API key\",\n });\n }\n\n // Custom Embedding nodes: probe /models (most embedding APIs are OpenAI-compatible)\n if (isCustomEmbeddingProvider(provider)) {\n const node = await getProviderNodeById(provider);\n if (!node) {\n return NextResponse.json({ error: \"Custom Embedding node not found\" }, { status: 404 });\n }\n const baseUrl = node.baseUrl?.replace(/\\/$/, \"\");\n const modelsRes = await fetch(`${baseUrl}/models`, {\n headers: { \"Authorization\": `Bearer ${apiKey}` },\n });\n if (modelsRes.ok) {\n return NextResponse.json({ valid: true });\n }\n // Auth errors are definitive\n if (modelsRes.status === 401 || modelsRes.status === 403) {\n return NextResponse.json({ valid: false, error: \"Invalid API key\" });\n }\n // Fallback: probe /embeddings with a common test model — many providers lack /models\n const embedRes = await fetch(`${baseUrl}/embeddings`, {\n method: \"POST\",\n headers: { \"Authorization\": `Bearer ${apiKey}`, \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ model: \"test\", input: \"ping\" }),\n });\n // 401/403 = bad key; anything else (including 400 \"model not found\") means key works\n isValid = embedRes.status !== 401 && embedRes.status !== 403;\n return NextResponse.json({\n valid: isValid,\n error: isValid ? null : \"Invalid API key\",\n });\n }\n\n if (isAnthropicCompatibleProvider(provider)) {\n const node = await getProviderNodeById(provider);\n if (!node) {\n return NextResponse.json({ error: \"Anthropic Compatible node not found\" }, { status: 404 });\n }\n\n let normalizedBase = node.baseUrl?.trim().replace(/\\/$/, \"\") || \"\";\n if (normalizedBase.endsWith(\"/messages\")) {\n normalizedBase = normalizedBase.slice(0, -9); // remove /messages\n }\n\n const modelsUrl = `${normalizedBase}/models`;\n\n const res = await fetch(modelsUrl, {\n headers: {\n \"x-api-key\": apiKey,\n \"anthropic-version\": \"2023-06-01\",\n \"Authorization\": `Bearer ${apiKey}`\n },\n });\n\n isValid = res.ok;\n return NextResponse.json({\n valid: isValid,\n error: isValid ? null : \"Invalid API key\",\n });\n }\n\n if (provider === \"cloudflare-ai\") {\n const { providerSpecificData } = body;\n const accountId = providerSpecificData?.accountId;\n if (!accountId) {\n return NextResponse.json({ valid: false, error: \"Missing Account ID\" });\n }\n const url = `https://api.cloudflare.com/client/v4/accounts/${accountId}/ai/v1/chat/completions`;\n const cfRes = await fetch(url, {\n method: \"POST\",\n headers: { \"Authorization\": `Bearer ${apiKey}`, \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n model: getDefaultModel(\"cloudflare-ai\"),\n messages: [{ role: \"user\", content: \"test\" }],\n max_tokens: 1,\n }),\n });\n isValid = cfRes.status !== 401 && cfRes.status !== 403 && cfRes.status !== 404;\n return NextResponse.json({\n valid: isValid,\n error: isValid ? null : \"Invalid API token or Account ID\",\n });\n }\n\n if (provider === \"azure\") {\n const { providerSpecificData } = body;\n const endpoint = (providerSpecificData?.azureEndpoint || \"\").replace(/\\/$/, \"\");\n const deployment = providerSpecificData?.deployment || \"gpt-4\";\n const apiVersion = providerSpecificData?.apiVersion || \"2024-10-01-preview\";\n const organization = providerSpecificData?.organization;\n\n const url = `${endpoint}/openai/deployments/${deployment}/chat/completions?api-version=${apiVersion}`;\n const headers = {\n \"api-key\": apiKey,\n \"Content-Type\": \"application/json\",\n };\n if (organization) headers[\"OpenAI-Organization\"] = organization;\n\n const azureRes = await fetch(url, {\n method: \"POST\",\n headers,\n body: JSON.stringify({\n messages: [{ role: \"user\", content: \"test\" }],\n max_tokens: 1,\n }),\n });\n isValid = azureRes.status !== 401 && azureRes.status !== 403;\n return NextResponse.json({\n valid: isValid,\n error: isValid ? null : \"Invalid API key or Azure configuration\",\n });\n }\n\n // Generic probe for webSearch/webFetch providers (config-driven)\n const webResult = await probeWebProvider(provider, apiKey);\n if (webResult !== null) {\n return NextResponse.json({\n valid: webResult,\n error: webResult ? null : \"Invalid API key\",\n });\n }\n\n // Generic probe for tts/embedding providers (config-driven)\n const mediaResult = await probeMediaProvider(provider, apiKey);\n if (mediaResult !== null) {\n return NextResponse.json({\n valid: mediaResult,\n error: mediaResult ? null : \"Invalid API key\",\n });\n }\n\n switch (provider) {\n case \"openai\":\n const openaiRes = await fetch(\"https://api.openai.com/v1/models\", {\n headers: { \"Authorization\": `Bearer ${apiKey}` },\n });\n isValid = openaiRes.ok;\n break;\n\n case \"vercel-ai-gateway\":\n const vercelAiGatewayRes = await fetch(\"https://ai-gateway.vercel.sh/v1/models\", {\n headers: { \"Authorization\": `Bearer ${apiKey}` },\n });\n isValid = vercelAiGatewayRes.ok;\n break;\n\n case \"anthropic\":\n const anthropicRes = await fetch(\"https://api.anthropic.com/v1/messages\", {\n method: \"POST\",\n headers: {\n \"x-api-key\": apiKey,\n \"anthropic-version\": \"2023-06-01\",\n \"content-type\": \"application/json\",\n },\n body: JSON.stringify({\n model: \"claude-3-haiku-20240307\",\n max_tokens: 1,\n messages: [{ role: \"user\", content: \"test\" }],\n }),\n });\n isValid = anthropicRes.status !== 401;\n break;\n\n case \"gemini\":\n const geminiRes = await fetch(`https://generativelanguage.googleapis.com/v1/models?key=${apiKey}`);\n isValid = geminiRes.ok;\n break;\n\n case \"openrouter\":\n const openrouterRes = await fetch(\"https://openrouter.ai/api/v1/models\", {\n headers: { \"Authorization\": `Bearer ${apiKey}` },\n });\n isValid = openrouterRes.ok;\n break;\n\n case \"glm\":\n case \"glm-cn\":\n case \"kimi\":\n case \"minimax\":\n case \"minimax-cn\":\n case \"alicode-intl\":\n case \"alicode\":\n case \"agentrouter\": {\n // Use baseUrl from PROVIDERS (DRY); separate openai-format vs claude-format flow\n const cfg = PROVIDERS[provider];\n const isOpenAiFormat = provider === \"glm-cn\" || provider === \"alicode\" || provider === \"alicode-intl\";\n\n if (isOpenAiFormat) {\n const testModel = getDefaultModel(provider);\n const res = await fetch(cfg.baseUrl, {\n method: \"POST\",\n headers: { \"Authorization\": `Bearer ${apiKey}`, \"content-type\": \"application/json\" },\n body: JSON.stringify({ model: testModel, max_tokens: 1, messages: [{ role: \"user\", content: \"test\" }] }),\n });\n isValid = res.status !== 401 && res.status !== 403;\n } else {\n const testModel = getDefaultModel(provider) || \"claude-sonnet-4-20250514\";\n const res = await fetch(cfg.baseUrl, {\n method: \"POST\",\n headers: {\n \"x-api-key\": apiKey,\n \"anthropic-version\": \"2023-06-01\",\n \"content-type\": \"application/json\",\n ...(cfg.headers || {}),\n },\n body: JSON.stringify({ model: testModel, max_tokens: 1, messages: [{ role: \"user\", content: \"test\" }] }),\n });\n // 400 = model resolution error but auth passed (e.g. agentrouter \"no available channel\")\n isValid = res.status !== 401 && res.status !== 403;\n }\n break;\n }\n case \"volcengine-ark\":\n case \"byteplus\": {\n const res = await fetch(PROVIDER_ENDPOINTS[provider], {\n method: \"POST\",\n headers: {\n \"Authorization\": `Bearer ${apiKey}`,\n \"content-type\": \"application/json\",\n },\n body: JSON.stringify({\n model: getDefaultModel(provider),\n max_tokens: 1,\n messages: [{ role: \"user\", content: \"test\" }],\n }),\n });\n isValid = res.status !== 401 && res.status !== 403;\n break;\n }\n\n case \"deepseek\":\n case \"groq\":\n case \"xai\":\n case \"mistral\":\n case \"perplexity\":\n case \"together\":\n case \"fireworks\":\n case \"cerebras\":\n case \"cohere\":\n case \"nebius\":\n case \"siliconflow\":\n case \"hyperbolic\":\n case \"ollama\":\n case \"ollama-local\":\n case \"assemblyai\":\n case \"nanobanana\":\n case \"chutes\":\n case \"xiaomi-mimo\":\n case \"xiaomi-tokenplan\":\n case \"nvidia\": {\n const endpoints = {\n deepseek: \"https://api.deepseek.com/models\",\n groq: \"https://api.groq.com/openai/v1/models\",\n xai: \"https://api.x.ai/v1/models\",\n mistral: \"https://api.mistral.ai/v1/models\",\n perplexity: \"https://api.perplexity.ai/models\",\n together: \"https://api.together.xyz/v1/models\",\n fireworks: \"https://api.fireworks.ai/inference/v1/models\",\n cerebras: \"https://api.cerebras.ai/v1/models\",\n cohere: \"https://api.cohere.ai/v1/models\",\n nebius: \"https://api.studio.nebius.ai/v1/models\",\n siliconflow: \"https://api.siliconflow.cn/v1/models\",\n hyperbolic: \"https://api.hyperbolic.xyz/v1/models\",\n ollama: \"https://ollama.com/api/tags\",\n \"ollama-local\": `${resolveOllamaLocalHost({ providerSpecificData })}/api/tags`,\n assemblyai: \"https://api.assemblyai.com/v1/account\",\n nanobanana: \"https://api.nanobananaapi.ai/v1/models\",\n chutes: \"https://llm.chutes.ai/v1/models\",\n nvidia: \"https://integrate.api.nvidia.com/v1/models\",\n \"xiaomi-mimo\": \"https://api.xiaomimimo.com/v1/models\",\n \"xiaomi-tokenplan\": \"https://token-plan-sgp.xiaomimimo.com/v1/models\"\n };\n const headers = {};\n if (apiKey) headers[\"Authorization\"] = `Bearer ${apiKey}`;\n const res = await fetch(endpoints[provider], { headers });\n // xai returns 400 for bad key, 403 for valid-but-no-credit. Other providers use 401.\n if (provider === \"xai\") {\n isValid = res.status === 200 || res.status === 403;\n } else {\n isValid = res.ok;\n }\n break;\n }\n\n case \"opencode-go\": {\n const res = await fetch(\"https://opencode.ai/zen/go/v1/chat/completions\", {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", \"Authorization\": `Bearer ${apiKey}` },\n body: JSON.stringify({\n model: getDefaultModel(\"opencode-go\"),\n messages: [{ role: \"user\", content: \"ping\" }],\n max_tokens: 1,\n stream: false,\n }),\n });\n isValid = res.status !== 401 && res.status !== 403;\n break;\n }\n\n case \"commandcode\": {\n const cfg = PROVIDERS.commandcode;\n const model = getDefaultModel(\"commandcode\");\n const payload = openaiToCommandCode(model, {\n messages: [{ role: \"user\", content: \"ping\" }],\n max_tokens: 1,\n stream: false,\n }, false);\n const res = await fetch(cfg.baseUrl, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n ...(cfg.headers || {}),\n \"x-session-id\": crypto.randomUUID(),\n \"Authorization\": `Bearer ${apiKey}`,\n },\n body: JSON.stringify(payload),\n });\n isValid = res.status !== 401 && res.status !== 403;\n break;\n }\n\n case \"deepgram\": {\n const res = await fetch(\"https://api.deepgram.com/v1/projects\", {\n headers: { \"Authorization\": `Token ${apiKey}` },\n });\n isValid = res.ok;\n break;\n }\n\n case \"blackbox\": {\n const res = await fetch(\"https://api.blackbox.ai/chat/completions\", {\n method: \"POST\",\n headers: {\n \"Authorization\": `Bearer ${apiKey}`,\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify({\n model: \"gpt-4o\",\n messages: [{ role: \"user\", content: \"test\" }],\n max_tokens: 10,\n }),\n });\n // Returns 401 for invalid key, 200 for valid, 400 for malformed\n isValid = res.status === 200 || res.status === 400;\n break;\n }\n\n case \"vertex\": {\n // Raw key: probe global endpoint (always 404 for unknown model, never 401)\n // SA JSON: attempt token mint via JWT assertion\n const saJson = (() => { try { const p = JSON.parse(apiKey); return p.type === \"service_account\" ? p : null; } catch { return null; } })();\n if (saJson) {\n // Validate SA JSON has required fields\n isValid = !!(saJson.client_email && saJson.private_key && saJson.project_id);\n } else {\n // Raw key: probe Vertex — 404 means key is valid (model just doesn't exist), 401 means invalid key\n const probeRes = await fetch(\n `https://aiplatform.googleapis.com/v1/publishers/google/models/__probe__:generateContent?key=${apiKey}`,\n { method: \"POST\", headers: { \"Content-Type\": \"application/json\" }, body: \"{}\" }\n );\n isValid = probeRes.status !== 401 && probeRes.status !== 403;\n }\n break;\n }\n\n case \"vertex-partner\": {\n const saJson = (() => { try { const p = JSON.parse(apiKey); return p.type === \"service_account\" ? p : null; } catch { return null; } })();\n if (saJson) {\n isValid = !!(saJson.client_email && saJson.private_key && saJson.project_id);\n } else {\n const probeRes = await fetch(\n `https://aiplatform.googleapis.com/v1/publishers/google/models/__probe__:generateContent?key=${apiKey}`,\n { method: \"POST\", headers: { \"Content-Type\": \"application/json\" }, body: \"{}\" }\n );\n isValid = probeRes.status !== 401 && probeRes.status !== 403;\n }\n break;\n }\n\n case \"grok-web\": {\n const token = apiKey.startsWith(\"sso=\") ? apiKey.slice(4) : apiKey;\n // Cloudflare-bypass: send POST with same browser fingerprint headers as GrokWebExecutor\n const randomHex = (n) => {\n const a = new Uint8Array(n);\n crypto.getRandomValues(a);\n return Array.from(a, (b) => b.toString(16).padStart(2, \"0\")).join(\"\");\n };\n const statsigId = Buffer.from(\"e:TypeError: Cannot read properties of null (reading 'children')\").toString(\"base64\");\n const traceId = randomHex(16);\n const spanId = randomHex(8);\n const res = await fetch(\"https://grok.com/rest/app-chat/conversations/new\", {\n method: \"POST\",\n headers: {\n Accept: \"*/*\",\n \"Accept-Encoding\": \"gzip, deflate, br, zstd\",\n \"Accept-Language\": \"en-US,en;q=0.9\",\n \"Cache-Control\": \"no-cache\",\n \"Content-Type\": \"application/json\",\n Cookie: `sso=${token}`,\n Origin: \"https://grok.com\",\n Pragma: \"no-cache\",\n Referer: \"https://grok.com/\",\n \"Sec-Ch-Ua\": '\"Google Chrome\";v=\"136\", \"Chromium\";v=\"136\", \"Not(A:Brand\";v=\"24\"',\n \"Sec-Ch-Ua-Mobile\": \"?0\",\n \"Sec-Ch-Ua-Platform\": '\"macOS\"',\n \"Sec-Fetch-Dest\": \"empty\",\n \"Sec-Fetch-Mode\": \"cors\",\n \"Sec-Fetch-Site\": \"same-origin\",\n \"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\",\n \"x-statsig-id\": statsigId,\n \"x-xai-request-id\": crypto.randomUUID(),\n traceparent: `00-${traceId}-${spanId}-00`,\n },\n body: JSON.stringify({\n temporary: true, modelName: \"grok-4\", modelMode: \"MODEL_MODE_GROK_4\", message: \"ping\",\n fileAttachments: [], imageAttachments: [],\n disableSearch: false, enableImageGeneration: false, returnImageBytes: false,\n returnRawGrokInXaiRequest: false, enableImageStreaming: false, imageGenerationCount: 0,\n forceConcise: false, toolOverrides: {}, enableSideBySide: true, sendFinalMetadata: true,\n isReasoning: false, disableTextFollowUps: true, disableMemory: true,\n forceSideBySide: false, isAsyncChat: false, disableSelfHarmShortCircuit: false,\n }),\n });\n // Cookie valid = any non-401/403 response (200, 400, 429 all mean cookie accepted)\n if (res.status === 401 || res.status === 403) {\n isValid = false;\n error = \"Invalid SSO cookie — re-paste from grok.com DevTools → Cookies → sso\";\n } else {\n isValid = true;\n }\n break;\n }\n\n case \"perplexity-web\": {\n let sessionToken = apiKey;\n if (sessionToken.startsWith(\"__Secure-next-auth.session-token=\")) {\n sessionToken = sessionToken.slice(\"__Secure-next-auth.session-token=\".length);\n }\n const tz = typeof Intl !== \"undefined\" ? Intl.DateTimeFormat().resolvedOptions().timeZone : \"UTC\";\n const res = await fetch(\"https://www.perplexity.ai/rest/sse/perplexity_ask\", {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Accept: \"text/event-stream\",\n Origin: \"https://www.perplexity.ai\",\n Referer: \"https://www.perplexity.ai/\",\n \"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\",\n \"X-App-ApiClient\": \"default\",\n \"X-App-ApiVersion\": \"2.18\",\n Cookie: `__Secure-next-auth.session-token=${sessionToken}`,\n },\n body: JSON.stringify({\n query_str: \"ping\",\n params: {\n query_str: \"ping\", search_focus: \"internet\", mode: \"concise\", model_preference: \"pplx_pro\",\n sources: [\"web\"], attachments: [],\n frontend_uuid: crypto.randomUUID(), frontend_context_uuid: crypto.randomUUID(),\n version: \"2.18\", language: \"en-US\", timezone: tz,\n search_recency_filter: null, is_incognito: true, use_schematized_api: true, last_backend_uuid: null,\n },\n }),\n });\n if (res.status === 401 || res.status === 403) {\n isValid = false;\n error = \"Invalid session cookie — re-paste __Secure-next-auth.session-token from perplexity.ai\";\n } else {\n isValid = true;\n }\n break;\n }\n\n default: {\n // Generic probe for OpenAI-compatible providers (config-driven from PROVIDERS)\n const cfg = PROVIDERS[provider];\n if (!cfg || cfg.format !== \"openai\" || !cfg.baseUrl) {\n return NextResponse.json({ error: \"Provider validation not supported\" }, { status: 400 });\n }\n if (cfg.noAuth) {\n isValid = true;\n break;\n }\n // Build auth headers based on cfg.authHeader (default: bearer)\n const headers = { \"Content-Type\": \"application/json\", ...(cfg.headers || {}) };\n if (cfg.authHeader === \"x-api-key\") headers[\"X-API-Key\"] = apiKey;\n else headers[\"Authorization\"] = `Bearer ${apiKey}`;\n // Try /models first (fast GET), fallback to chat probe on ambiguous response\n const modelsUrl = cfg.baseUrl.replace(/\\/chat\\/completions$/, \"/models\").replace(/\\/chatbot$/, \"/models\");\n let probeOk = null;\n try {\n const probeRes = await fetch(modelsUrl, { headers, signal: AbortSignal.timeout(8000) });\n if (probeRes.status === 401 || probeRes.status === 403) probeOk = false;\n else if (probeRes.ok) probeOk = true;\n } catch { /* fallback to chat */ }\n if (probeOk !== null) {\n isValid = probeOk;\n break;\n }\n // Fallback: minimal chat probe\n const defaultModel = getDefaultModel(provider) || \"test\";\n const chatRes = await fetch(cfg.baseUrl, {\n method: \"POST\",\n headers,\n body: JSON.stringify({ model: defaultModel, messages: [{ role: \"user\", content: \"ping\" }], max_tokens: 1 }),\n signal: AbortSignal.timeout(10000),\n });\n isValid = chatRes.status !== 401 && chatRes.status !== 403;\n break;\n }\n }\n } catch (err) {\n error = err.message;\n isValid = false;\n }\n\n return NextResponse.json({\n valid: isValid,\n error: isValid ? null : (error || \"Invalid API key\"),\n });\n } catch (error) {\n console.log(\"Error validating API key:\", error);\n return NextResponse.json({ error: \"Validation failed\" }, { status: 500 });\n }\n}\n"}, "e93298305656c9eac093d8280392d868c1704fee": {"t": 1785758904.1242907, "n": 26888, "h": "060eebc6d09ecb72", "c": "import { NextResponse } from \"next/server\";\nimport { getProviderNodeById } from \"@/models\";\nimport { isOpenAICompatibleProvider, isAnthropicCompatibleProvider, isCustomEmbeddingProvider, AI_PROVIDERS } from \"@/shared/constants/providers\";\nimport { getDefaultModel } from \"open-sse/config/providerModels.js\";\nimport { resolveOllamaLocalHost, PROVIDERS } from \"open-sse/config/providers.js\";\nimport { openaiToCommandCode } from \"open-sse/translator/request/openai-to-commandcode.js\";\nimport { PROVIDER_ENDPOINTS } from \"@/shared/constants/config\";\nimport { normalizeProviderId } from \"@/lib/providerNormalization\";\n\n// Probe a webSearch/webFetch provider using its searchConfig/fetchConfig.\n// Returns true if API key is accepted (status !== 401 && !== 403).\nasync function probeWebProvider(provider, apiKey) {\n const p = AI_PROVIDERS[provider];\n if (!p) return null;\n // Skip if provider has dual-purpose (LLM + search), let LLM validate handle it\n const kinds = p.serviceKinds || [\"llm\"];\n const isWebOnly = kinds.every((k) => k === \"webSearch\" || k === \"webFetch\");\n if (!isWebOnly) return null;\n const cfg = p.searchConfig || p.fetchConfig;\n if (!cfg) return null;\n if (cfg.authType === \"none\") return true; // no-auth (e.g. searxng)\n\n let url = cfg.baseUrl;\n const headers = { \"Content-Type\": \"application/json\" };\n let body;\n\n // Apply auth based on authHeader\n switch (cfg.authHeader) {\n case \"bearer\": headers[\"Authorization\"] = `Bearer ${apiKey}`; break;\n case \"x-api-key\": headers[\"x-api-key\"] = apiKey; break;\n case \"x-subscription-token\":headers[\"x-subscription-token\"] = apiKey; break;\n case \"key\": url += `?key=${encodeURIComponent(apiKey)}&q=ping&cx=test`; break; // google-pse\n case \"api_key\": url += `?api_key=${encodeURIComponent(apiKey)}&q=ping&engine=google`; break; // searchapi\n }\n\n // Minimal body for POST endpoints; GET sends nothing\n if (cfg.method === \"POST\") {\n body = JSON.stringify({ query: \"ping\", q: \"ping\", url: \"https://example.com\" });\n }\n\n const res = await fetch(url, { method: cfg.method, headers, body, signal: AbortSignal.timeout(8000) });\n return res.status !== 401 && res.status !== 403;\n}\n\n// Probe a media provider (tts/embedding/stt/image/video) using *Config.\n// Returns true if API key is accepted; null to skip (let default handler decide).\nasync function probeMediaProvider(provider, apiKey) {\n const p = AI_PROVIDERS[provider];\n if (!p) return null;\n const MEDIA_KINDS = new Set([\"tts\", \"embedding\", \"stt\", \"image\", \"video\", \"music\", \"imageToText\"]);\n const kinds = p.serviceKinds || [\"llm\"];\n const isMediaOnly = kinds.every((k) => MEDIA_KINDS.has(k));\n if (!isMediaOnly) return null;\n const cfg = p.ttsConfig || p.sttConfig || p.embeddingConfig || p.imageConfig || p.videoConfig || p.musicConfig;\n // No probe config → best-effort accept (validate at usage time)\n if (!cfg) return true;\n if (p.noAuth || cfg.authType === \"none\") return true;\n // Skip auth schemes that need provider-specific data\n if (cfg.authHeader === \"playht\" || cfg.authHeader === \"aws-sigv4\") return true;\n\n const headers = { \"Content-Type\": \"application/json\", ...(cfg.extraHeaders || {}) };\n\n switch (cfg.authHeader) {\n case \"bearer\": headers[\"Authorization\"] = `Bearer ${apiKey}`; break;\n case \"key\": headers[\"Authorization\"] = `Key ${apiKey}`; break;\n case \"x-api-key\": headers[\"x-api-key\"] = apiKey; break;\n case \"x-key\": headers[\"x-key\"] = apiKey; break;\n case \"xi-api-key\": headers[\"xi-api-key\"] = apiKey; break;\n case \"token\": headers[\"Authorization\"] = `Token ${apiKey}`; break;\n case \"basic\": headers[\"Authorization\"] = `Basic ${apiKey}`; break;\n default: return null;\n }\n\n const method = cfg.method || \"POST\";\n const res = await fetch(cfg.baseUrl, {\n method,\n headers,\n body: method === \"GET\" ? undefined : JSON.stringify({ input: \"ping\", text: \"ping\", prompt: \"ping\", model: cfg.models?.[0]?.id || \"test\" }),\n signal: AbortSignal.timeout(8000),\n });\n return res.status !== 401 && res.status !== 403;\n}\n\n// POST /api/providers/validate - Validate API key with provider\nexport async function POST(request) {\n try {\n const body = await request.json();\n const provider = normalizeProviderId(body.provider);\n const { apiKey, providerSpecificData } = body;\n\n const isNoAuth = AI_PROVIDERS[provider]?.noAuth === true;\n if (!provider || (!apiKey && provider !== \"ollama-local\" && !isNoAuth)) {\n return NextResponse.json({ error: \"Provider and API key required\" }, { status: 400 });\n }\n\n let isValid = false;\n let error = null;\n\n // Validate with each provider\n try {\n if (isOpenAICompatibleProvider(provider)) {\n const node = await getProviderNodeById(provider);\n if (!node) {\n return NextResponse.json({ error: \"OpenAI Compatible node not found\" }, { status: 404 });\n }\n const modelsUrl = `${node.baseUrl?.replace(/\\/$/, \"\")}/models`;\n const res = await fetch(modelsUrl, {\n headers: { \"Authorization\": `Bearer ${apiKey}` },\n });\n isValid = res.ok;\n return NextResponse.json({\n valid: isValid,\n error: isValid ? null : \"Invalid API key\",\n });\n }\n\n // Custom Embedding nodes: probe /models (most embedding APIs are OpenAI-compatible)\n if (isCustomEmbeddingProvider(provider)) {\n const node = await getProviderNodeById(provider);\n if (!node) {\n return NextResponse.json({ error: \"Custom Embedding node not found\" }, { status: 404 });\n }\n const baseUrl = node.baseUrl?.replace(/\\/$/, \"\");\n const modelsRes = await fetch(`${baseUrl}/models`, {\n headers: { \"Authorization\": `Bearer ${apiKey}` },\n });\n if (modelsRes.ok) {\n return NextResponse.json({ valid: true });\n }\n // Auth errors are definitive\n if (modelsRes.status === 401 || modelsRes.status === 403) {\n return NextResponse.json({ valid: false, error: \"Invalid API key\" });\n }\n // Fallback: probe /embeddings with a common test model — many providers lack /models\n const embedRes = await fetch(`${baseUrl}/embeddings`, {\n method: \"POST\",\n headers: { \"Authorization\": `Bearer ${apiKey}`, \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ model: \"test\", input: \"ping\" }),\n });\n // 401/403 = bad key; anything else (including 400 \"model not found\") means key works\n isValid = embedRes.status !== 401 && embedRes.status !== 403;\n return NextResponse.json({\n valid: isValid,\n error: isValid ? null : \"Invalid API key\",\n });\n }\n\n if (isAnthropicCompatibleProvider(provider)) {\n const node = await getProviderNodeById(provider);\n if (!node) {\n return NextResponse.json({ error: \"Anthropic Compatible node not found\" }, { status: 404 });\n }\n\n let normalizedBase = node.baseUrl?.trim().replace(/\\/$/, \"\") || \"\";\n if (normalizedBase.endsWith(\"/messages\")) {\n normalizedBase = normalizedBase.slice(0, -9); // remove /messages\n }\n\n const modelsUrl = `${normalizedBase}/models`;\n\n const res = await fetch(modelsUrl, {\n headers: {\n \"x-api-key\": apiKey,\n \"anthropic-version\": \"2023-06-01\",\n \"Authorization\": `Bearer ${apiKey}`\n },\n });\n\n isValid = res.ok;\n return NextResponse.json({\n valid: isValid,\n error: isValid ? null : \"Invalid API key\",\n });\n }\n\n if (provider === \"cloudflare-ai\") {\n const { providerSpecificData } = body;\n const accountId = providerSpecificData?.accountId;\n if (!accountId) {\n return NextResponse.json({ valid: false, error: \"Missing Account ID\" });\n }\n const url = `https://api.cloudflare.com/client/v4/accounts/${accountId}/ai/v1/chat/completions`;\n const cfRes = await fetch(url, {\n method: \"POST\",\n headers: { \"Authorization\": `Bearer ${apiKey}`, \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n model: getDefaultModel(\"cloudflare-ai\"),\n messages: [{ role: \"user\", content: \"test\" }],\n max_tokens: 1,\n }),\n });\n isValid = cfRes.status !== 401 && cfRes.status !== 403 && cfRes.status !== 404;\n return NextResponse.json({\n valid: isValid,\n error: isValid ? null : \"Invalid API token or Account ID\",\n });\n }\n\n if (provider === \"azure\") {\n const { providerSpecificData } = body;\n const endpoint = (providerSpecificData?.azureEndpoint || \"\").replace(/\\/$/, \"\");\n const deployment = providerSpecificData?.deployment || \"gpt-4\";\n const apiVersion = providerSpecificData?.apiVersion || \"2024-10-01-preview\";\n const organization = providerSpecificData?.organization;\n\n const url = `${endpoint}/openai/deployments/${deployment}/chat/completions?api-version=${apiVersion}`;\n const headers = {\n \"api-key\": apiKey,\n \"Content-Type\": \"application/json\",\n };\n if (organization) headers[\"OpenAI-Organization\"] = organization;\n\n const azureRes = await fetch(url, {\n method: \"POST\",\n headers,\n body: JSON.stringify({\n messages: [{ role: \"user\", content: \"test\" }],\n max_tokens: 1,\n }),\n });\n isValid = azureRes.status !== 401 && azureRes.status !== 403;\n return NextResponse.json({\n valid: isValid,\n error: isValid ? null : \"Invalid API key or Azure configuration\",\n });\n }\n\n // Generic probe for webSearch/webFetch providers (config-driven)\n const webResult = await probeWebProvider(provider, apiKey);\n if (webResult !== null) {\n return NextResponse.json({\n valid: webResult,\n error: webResult ? null : \"Invalid API key\",\n });\n }\n\n // Generic probe for tts/embedding providers (config-driven)\n const mediaResult = await probeMediaProvider(provider, apiKey);\n if (mediaResult !== null) {\n return NextResponse.json({\n valid: mediaResult,\n error: mediaResult ? null : \"Invalid API key\",\n });\n }\n\n switch (provider) {\n case \"openai\":\n const openaiRes = await fetch(\"https://api.openai.com/v1/models\", {\n headers: { \"Authorization\": `Bearer ${apiKey}` },\n });\n isValid = openaiRes.ok;\n break;\n\n case \"anthropic\":\n const anthropicRes = await fetch(\"https://api.anthropic.com/v1/messages\", {\n method: \"POST\",\n headers: {\n \"x-api-key\": apiKey,\n \"anthropic-version\": \"2023-06-01\",\n \"content-type\": \"application/json\",\n },\n body: JSON.stringify({\n model: \"claude-3-haiku-20240307\",\n max_tokens: 1,\n messages: [{ role: \"user\", content: \"test\" }],\n }),\n });\n isValid = anthropicRes.status !== 401;\n break;\n\n case \"gemini\":\n const geminiRes = await fetch(`https://generativelanguage.googleapis.com/v1/models?key=${apiKey}`);\n isValid = geminiRes.ok;\n break;\n\n case \"openrouter\":\n const openrouterRes = await fetch(\"https://openrouter.ai/api/v1/models\", {\n headers: { \"Authorization\": `Bearer ${apiKey}` },\n });\n isValid = openrouterRes.ok;\n break;\n\n case \"glm\":\n case \"glm-cn\":\n case \"kimi\":\n case \"minimax\":\n case \"minimax-cn\":\n case \"alicode-intl\":\n case \"alicode\":\n case \"agentrouter\": {\n // Use baseUrl from PROVIDERS (DRY); separate openai-format vs claude-format flow\n const cfg = PROVIDERS[provider];\n const isOpenAiFormat = provider === \"glm-cn\" || provider === \"alicode\" || provider === \"alicode-intl\";\n\n if (isOpenAiFormat) {\n const testModel = getDefaultModel(provider);\n const res = await fetch(cfg.baseUrl, {\n method: \"POST\",\n headers: { \"Authorization\": `Bearer ${apiKey}`, \"content-type\": \"application/json\" },\n body: JSON.stringify({ model: testModel, max_tokens: 1, messages: [{ role: \"user\", content: \"test\" }] }),\n });\n isValid = res.status !== 401 && res.status !== 403;\n } else {\n const testModel = getDefaultModel(provider) || \"claude-sonnet-4-20250514\";\n const res = await fetch(cfg.baseUrl, {\n method: \"POST\",\n headers: {\n \"x-api-key\": apiKey,\n \"anthropic-version\": \"2023-06-01\",\n \"content-type\": \"application/json\",\n ...(cfg.headers || {}),\n },\n body: JSON.stringify({ model: testModel, max_tokens: 1, messages: [{ role: \"user\", content: \"test\" }] }),\n });\n // 400 = model resolution error but auth passed (e.g. agentrouter \"no available channel\")\n isValid = res.status !== 401 && res.status !== 403;\n }\n break;\n }\n case \"volcengine-ark\":\n case \"byteplus\": {\n const res = await fetch(PROVIDER_ENDPOINTS[provider], {\n method: \"POST\",\n headers: {\n \"Authorization\": `Bearer ${apiKey}`,\n \"content-type\": \"application/json\",\n },\n body: JSON.stringify({\n model: getDefaultModel(provider),\n max_tokens: 1,\n messages: [{ role: \"user\", content: \"test\" }],\n }),\n });\n isValid = res.status !== 401 && res.status !== 403;\n break;\n }\n\n case \"deepseek\":\n case \"groq\":\n case \"xai\":\n case \"mistral\":\n case \"perplexity\":\n case \"together\":\n case \"fireworks\":\n case \"cerebras\":\n case \"cohere\":\n case \"nebius\":\n case \"siliconflow\":\n case \"hyperbolic\":\n case \"ollama\":\n case \"ollama-local\":\n case \"assemblyai\":\n case \"nanobanana\":\n case \"chutes\":\n case \"xiaomi-mimo\":\n case \"nvidia\": {\n const endpoints = {\n deepseek: \"https://api.deepseek.com/models\",\n groq: \"https://api.groq.com/openai/v1/models\",\n xai: \"https://api.x.ai/v1/models\",\n mistral: \"https://api.mistral.ai/v1/models\",\n perplexity: \"https://api.perplexity.ai/models\",\n together: \"https://api.together.xyz/v1/models\",\n fireworks: \"https://api.fireworks.ai/inference/v1/models\",\n cerebras: \"https://api.cerebras.ai/v1/models\",\n cohere: \"https://api.cohere.ai/v1/models\",\n nebius: \"https://api.studio.nebius.ai/v1/models\",\n siliconflow: \"https://api.siliconflow.cn/v1/models\",\n hyperbolic: \"https://api.hyperbolic.xyz/v1/models\",\n ollama: \"https://ollama.com/api/tags\",\n \"ollama-local\": `${resolveOllamaLocalHost({ providerSpecificData })}/api/tags`,\n assemblyai: \"https://api.assemblyai.com/v1/account\",\n nanobanana: \"https://api.nanobananaapi.ai/v1/models\",\n chutes: \"https://llm.chutes.ai/v1/models\",\n nvidia: \"https://integrate.api.nvidia.com/v1/models\",\n \"xiaomi-mimo\": \"https://api.xiaomimimo.com/v1/models\"\n };\n const headers = {};\n if (apiKey) headers[\"Authorization\"] = `Bearer ${apiKey}`;\n const res = await fetch(endpoints[provider], { headers });\n // xai returns 400 for bad key, 403 for valid-but-no-credit. Other providers use 401.\n if (provider === \"xai\") {\n isValid = res.status === 200 || res.status === 403;\n } else {\n isValid = res.ok;\n }\n break;\n }\n\n case \"opencode-go\": {\n const res = await fetch(\"https://opencode.ai/zen/go/v1/chat/completions\", {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", \"Authorization\": `Bearer ${apiKey}` },\n body: JSON.stringify({\n model: getDefaultModel(\"opencode-go\"),\n messages: [{ role: \"user\", content: \"ping\" }],\n max_tokens: 1,\n stream: false,\n }),\n });\n isValid = res.status !== 401 && res.status !== 403;\n break;\n }\n\n case \"commandcode\": {\n const cfg = PROVIDERS.commandcode;\n const model = getDefaultModel(\"commandcode\");\n const payload = openaiToCommandCode(model, {\n messages: [{ role: \"user\", content: \"ping\" }],\n max_tokens: 1,\n stream: false,\n }, false);\n const res = await fetch(cfg.baseUrl, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n ...(cfg.headers || {}),\n \"x-session-id\": crypto.randomUUID(),\n \"Authorization\": `Bearer ${apiKey}`,\n },\n body: JSON.stringify(payload),\n });\n isValid = res.status !== 401 && res.status !== 403;\n break;\n }\n\n case \"deepgram\": {\n const res = await fetch(\"https://api.deepgram.com/v1/projects\", {\n headers: { \"Authorization\": `Token ${apiKey}` },\n });\n isValid = res.ok;\n break;\n }\n\n case \"blackbox\": {\n const res = await fetch(\"https://api.blackbox.ai/chat/completions\", {\n method: \"POST\",\n headers: {\n \"Authorization\": `Bearer ${apiKey}`,\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify({\n model: \"gpt-4o\",\n messages: [{ role: \"user\", content: \"test\" }],\n max_tokens: 10,\n }),\n });\n // Returns 401 for invalid key, 200 for valid, 400 for malformed\n isValid = res.status === 200 || res.status === 400;\n break;\n }\n\n case \"vertex\": {\n // Raw key: probe global endpoint (always 404 for unknown model, never 401)\n // SA JSON: attempt token mint via JWT assertion\n const saJson = (() => { try { const p = JSON.parse(apiKey); return p.type === \"service_account\" ? p : null; } catch { return null; } })();\n if (saJson) {\n // Validate SA JSON has required fields\n isValid = !!(saJson.client_email && saJson.private_key && saJson.project_id);\n } else {\n // Raw key: probe Vertex — 404 means key is valid (model just doesn't exist), 401 means invalid key\n const probeRes = await fetch(\n `https://aiplatform.googleapis.com/v1/publishers/google/models/__probe__:generateContent?key=${apiKey}`,\n { method: \"POST\", headers: { \"Content-Type\": \"application/json\" }, body: \"{}\" }\n );\n isValid = probeRes.status !== 401 && probeRes.status !== 403;\n }\n break;\n }\n\n case \"vertex-partner\": {\n const saJson = (() => { try { const p = JSON.parse(apiKey); return p.type === \"service_account\" ? p : null; } catch { return null; } })();\n if (saJson) {\n isValid = !!(saJson.client_email && saJson.private_key && saJson.project_id);\n } else {\n const probeRes = await fetch(\n `https://aiplatform.googleapis.com/v1/publishers/google/models/__probe__:generateContent?key=${apiKey}`,\n { method: \"POST\", headers: { \"Content-Type\": \"application/json\" }, body: \"{}\" }\n );\n isValid = probeRes.status !== 401 && probeRes.status !== 403;\n }\n break;\n }\n\n case \"grok-web\": {\n const token = apiKey.startsWith(\"sso=\") ? apiKey.slice(4) : apiKey;\n // Cloudflare-bypass: send POST with same browser fingerprint headers as GrokWebExecutor\n const randomHex = (n) => {\n const a = new Uint8Array(n);\n crypto.getRandomValues(a);\n return Array.from(a, (b) => b.toString(16).padStart(2, \"0\")).join(\"\");\n };\n const statsigId = Buffer.from(\"e:TypeError: Cannot read properties of null (reading 'children')\").toString(\"base64\");\n const traceId = randomHex(16);\n const spanId = randomHex(8);\n const res = await fetch(\"https://grok.com/rest/app-chat/conversations/new\", {\n method: \"POST\",\n headers: {\n Accept: \"*/*\",\n \"Accept-Encoding\": \"gzip, deflate, br, zstd\",\n \"Accept-Language\": \"en-US,en;q=0.9\",\n \"Cache-Control\": \"no-cache\",\n \"Content-Type\": \"application/json\",\n Cookie: `sso=${token}`,\n Origin: \"https://grok.com\",\n Pragma: \"no-cache\",\n Referer: \"https://grok.com/\",\n \"Sec-Ch-Ua\": '\"Google Chrome\";v=\"136\", \"Chromium\";v=\"136\", \"Not(A:Brand\";v=\"24\"',\n \"Sec-Ch-Ua-Mobile\": \"?0\",\n \"Sec-Ch-Ua-Platform\": '\"macOS\"',\n \"Sec-Fetch-Dest\": \"empty\",\n \"Sec-Fetch-Mode\": \"cors\",\n \"Sec-Fetch-Site\": \"same-origin\",\n \"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\",\n \"x-statsig-id\": statsigId,\n \"x-xai-request-id\": crypto.randomUUID(),\n traceparent: `00-${traceId}-${spanId}-00`,\n },\n body: JSON.stringify({\n temporary: true, modelName: \"grok-4\", modelMode: \"MODEL_MODE_GROK_4\", message: \"ping\",\n fileAttachments: [], imageAttachments: [],\n disableSearch: false, enableImageGeneration: false, returnImageBytes: false,\n returnRawGrokInXaiRequest: false, enableImageStreaming: false, imageGenerationCount: 0,\n forceConcise: false, toolOverrides: {}, enableSideBySide: true, sendFinalMetadata: true,\n isReasoning: false, disableTextFollowUps: true, disableMemory: true,\n forceSideBySide: false, isAsyncChat: false, disableSelfHarmShortCircuit: false,\n }),\n });\n // Cookie valid = any non-401/403 response (200, 400, 429 all mean cookie accepted)\n if (res.status === 401 || res.status === 403) {\n isValid = false;\n error = \"Invalid SSO cookie — re-paste from grok.com DevTools → Cookies → sso\";\n } else {\n isValid = true;\n }\n break;\n }\n\n case \"perplexity-web\": {\n let sessionToken = apiKey;\n if (sessionToken.startsWith(\"__Secure-next-auth.session-token=\")) {\n sessionToken = sessionToken.slice(\"__Secure-next-auth.session-token=\".length);\n }\n const tz = typeof Intl !== \"undefined\" ? Intl.DateTimeFormat().resolvedOptions().timeZone : \"UTC\";\n const res = await fetch(\"https://www.perplexity.ai/rest/sse/perplexity_ask\", {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Accept: \"text/event-stream\",\n Origin: \"https://www.perplexity.ai\",\n Referer: \"https://www.perplexity.ai/\",\n \"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\",\n \"X-App-ApiClient\": \"default\",\n \"X-App-ApiVersion\": \"2.18\",\n Cookie: `__Secure-next-auth.session-token=${sessionToken}`,\n },\n body: JSON.stringify({\n query_str: \"ping\",\n params: {\n query_str: \"ping\", search_focus: \"internet\", mode: \"concise\", model_preference: \"pplx_pro\",\n sources: [\"web\"], attachments: [],\n frontend_uuid: crypto.randomUUID(), frontend_context_uuid: crypto.randomUUID(),\n version: \"2.18\", language: \"en-US\", timezone: tz,\n search_recency_filter: null, is_incognito: true, use_schematized_api: true, last_backend_uuid: null,\n },\n }),\n });\n if (res.status === 401 || res.status === 403) {\n isValid = false;\n error = \"Invalid session cookie — re-paste __Secure-next-auth.session-token from perplexity.ai\";\n } else {\n isValid = true;\n }\n break;\n }\n\n default: {\n // Generic probe for OpenAI-compatible providers (config-driven from PROVIDERS)\n const cfg = PROVIDERS[provider];\n if (!cfg || cfg.format !== \"openai\" || !cfg.baseUrl) {\n return NextResponse.json({ error: \"Provider validation not supported\" }, { status: 400 });\n }\n if (cfg.noAuth) {\n isValid = true;\n break;\n }\n // Build auth headers based on cfg.authHeader (default: bearer)\n const headers = { \"Content-Type\": \"application/json\", ...(cfg.headers || {}) };\n if (cfg.authHeader === \"x-api-key\") headers[\"X-API-Key\"] = apiKey;\n else headers[\"Authorization\"] = `Bearer ${apiKey}`;\n // Try /models first (fast GET), fallback to chat probe on ambiguous response\n const modelsUrl = cfg.baseUrl.replace(/\\/chat\\/completions$/, \"/models\").replace(/\\/chatbot$/, \"/models\");\n let probeOk = null;\n try {\n const probeRes = await fetch(modelsUrl, { headers, signal: AbortSignal.timeout(8000) });\n if (probeRes.status === 401 || probeRes.status === 403) probeOk = false;\n else if (probeRes.ok) probeOk = true;\n } catch { /* fallback to chat */ }\n if (probeOk !== null) {\n isValid = probeOk;\n break;\n }\n // Fallback: minimal chat probe\n const defaultModel = getDefaultModel(provider) || \"test\";\n const chatRes = await fetch(cfg.baseUrl, {\n method: \"POST\",\n headers,\n body: JSON.stringify({ model: defaultModel, messages: [{ role: \"user\", content: \"ping\" }], max_tokens: 1 }),\n signal: AbortSignal.timeout(10000),\n });\n isValid = chatRes.status !== 401 && chatRes.status !== 403;\n break;\n }\n }\n } catch (err) {\n error = err.message;\n isValid = false;\n }\n\n return NextResponse.json({\n valid: isValid,\n error: isValid ? null : (error || \"Invalid API key\"),\n });\n } catch (error) {\n console.log(\"Error validating API key:\", error);\n return NextResponse.json({ error: \"Validation failed\" }, { status: 500 });\n }\n}\n"}}, "gitbook/content/vi/integration/other-tools.md": {"f213a60a2984a7ad16ffa4bd05649df10649ae7e": {"t": 1785746498.6073048, "n": 8901, "h": "112ae36200ae684c", "c": "# Tích hợp các Công cụ khác\n\n9Router tương thích với mọi công cụ hỗ trợ format API OpenAI. Hướng dẫn này bao gồm pattern tích hợp tổng quát cho nhiều công cụ và ứng dụng tùy chỉnh.\n\n## Tổng quan\n\n9Router cung cấp API endpoint tương thích OpenAI hoạt động với:\n- Script và ứng dụng tùy chỉnh\n- API client và công cụ test\n- Công cụ CLI và utility\n- Tích hợp bên thứ ba\n- Framework phát triển\n\n## Pattern Setup Tổng quát\n\nMọi công cụ tương thích OpenAI có thể kết nối đến 9Router bằng các cài đặt sau:\n\n**9Router cục bộ:**\n```\nBase URL: http://localhost:20128/v1\nAPI Key: your-api-key-from-dashboard\nModel: any 9Router model (cc/*, cx/*, glm/*, etc.)\n```\n\n**9Router cloud:**\n```\nBase URL: https://9router.com/v1\nAPI Key: your-api-key-from-dashboard\nModel: any 9Router model (cc/*, cx/*, glm/*, etc.)\n```\n\n## Model có sẵn\n\n### Claude Models (Anthropic)\n- `cc/claude-opus-4-5-20251101`\n- `cc/claude-sonnet-4-20250514`\n- `cc/claude-haiku-4-20250514`\n\n### DeepSeek Models\n- `cx/deepseek-chat`\n- `cx/deepseek-reasoner`\n\n### GLM Models (Zhipu AI)\n- `glm/glm-4-plus`\n- `glm/glm-4-flash`\n\n## Ví dụ Tích hợp\n\n### Python với OpenAI SDK\n\n```python\nfrom openai import OpenAI\n\nclient = OpenAI(\n api_key=\"your-api-key-from-dashboard\",\n base_url=\"http://localhost:20128/v1\"\n)\n\nresponse = client.chat.completions.create(\n model=\"cc/claude-sonnet-4-20250514\",\n messages=[\n {\"role\": \"user\", \"content\": \"Hello, how are you?\"}\n ]\n)\n\nprint(response.choices[0].message.content)\n```\n\n### Node.js với OpenAI SDK\n\n```javascript\nimport OpenAI from \"openai\";\n\nconst client = new OpenAI({\n apiKey: \"your-api-key-from-dashboard\",\n baseURL: \"http://localhost:20128/v1\"\n});\n\nconst response = await client.chat.completions.create({\n model: \"cc/claude-sonnet-4-20250514\",\n messages: [\n { role: \"user\", content: \"Hello, how are you?\" }\n ]\n});\n\nconsole.log(response.choices[0].message.content);\n```\n\n### Lệnh cURL\n\n```bash\ncurl http://localhost:20128/v1/chat/completions \\\n -H \"Content-Type: application/json\" \\\n -H \"Authorization: Bearer your-api-key-from-dashboard\" \\\n -d '{\n \"model\": \"cc/claude-sonnet-4-20250514\",\n \"messages\": [\n {\"role\": \"user\", \"content\": \"Hello, how are you?\"}\n ]\n }'\n```\n\n### HTTP Client (Postman, Insomnia)\n\n**Request:**\n```\nPOST http://localhost:20128/v1/chat/completions\n```\n\n**Headers:**\n```\nContent-Type: application/json\nAuthorization: Bearer your-api-key-from-dashboard\n```\n\n**Body:**\n```json\n{\n \"model\": \"cc/claude-sonnet-4-20250514\",\n \"messages\": [\n {\"role\": \"user\", \"content\": \"Hello, how are you?\"}\n ],\n \"temperature\": 0.7,\n \"max_tokens\": 1000\n}\n```\n\n### Tích hợp LangChain\n\n```python\nfrom langchain.chat_models import ChatOpenAI\nfrom langchain.schema import HumanMessage\n\nllm = ChatOpenAI(\n model_name=\"cc/claude-sonnet-4-20250514\",\n openai_api_key=\"your-api-key-from-dashboard\",\n openai_api_base=\"http://localhost:20128/v1\",\n temperature=0.7\n)\n\nmessages = [HumanMessage(content=\"Explain quantum computing\")]\nresponse = llm(messages)\nprint(response.content)\n```\n\n### Tích hợp LlamaIndex\n\n```python\nfrom llama_index.llms import OpenAI\n\nllm = OpenAI(\n model=\"cc/claude-sonnet-4-20250514\",\n api_key=\"your-api-key-from-dashboard\",\n api_base=\"http://localhost:20128/v1\"\n)\n\nresponse = llm.complete(\"What is machine learning?\")\nprint(response.text)\n```\n\n## Ví dụ Script Tùy chỉnh\n\n### Script Xử lý Batch\n\n```python\nimport openai\nimport json\n\nopenai.api_key = \"your-api-key-from-dashboard\"\nopenai.api_base = \"http://localhost:20128/v1\"\n\ndef process_batch(prompts, model=\"cx/deepseek-chat\"):\n results = []\n for prompt in prompts:\n response = openai.ChatCompletion.create(\n model=model,\n messages=[{\"role\": \"user\", \"content\": prompt}]\n )\n results.append({\n \"prompt\": prompt,\n \"response\": response.choices[0].message.content\n })\n return results\n\nprompts = [\n \"Explain AI in one sentence\",\n \"What is machine learning?\",\n \"Define neural networks\"\n]\n\nresults = process_batch(prompts)\nprint(json.dumps(results, indent=2))\n```\n\n### Xử lý Streaming Response\n\n```javascript\nimport OpenAI from \"openai\";\n\nconst client = new OpenAI({\n apiKey: \"your-api-key-from-dashboard\",\n baseURL: \"http://localhost:20128/v1\"\n});\n\nasync function streamResponse(prompt) {\n const stream = await client.chat.completions.create({\n model: \"cc/claude-sonnet-4-20250514\",\n messages: [{ role: \"user\", content: prompt }],\n stream: true\n });\n\n for await (const chunk of stream) {\n const content = chunk.choices[0]?.delta?.content || \"\";\n process.stdout.write(content);\n }\n}\n\nstreamResponse(\"Write a short story about AI\");\n```\n\n### So sánh Nhiều Model\n\n```python\nfrom openai import OpenAI\n\nclient = OpenAI(\n api_key=\"your-api-key-from-dashboard\",\n base_url=\"http://localhost:20128/v1\"\n)\n\nmodels = [\n \"cc/claude-sonnet-4-20250514\",\n \"cx/deepseek-chat\",\n \"glm/glm-4-plus\"\n]\n\nprompt = \"Explain quantum computing in simple terms\"\n\nfor model in models:\n response = client.chat.completions.create(\n model=model,\n messages=[{\"role\": \"user\", \"content\": prompt}]\n )\n print(f\"\\n=== {model} ===\")\n print(response.choices[0].message.content)\n```\n\n## Pattern Tích hợp Phổ biến\n\n### Biến môi trường\n\nLưu credentials an toàn:\n\n```bash\n# .env file\nROUTER_API_KEY=your-api-key-from-dashboard\nROUTER_BASE_URL=http://localhost:20128/v1\nROUTER_MODEL=cc/claude-sonnet-4-20250514\n```\n\n```python\nimport os\nfrom openai import OpenAI\n\nclient = OpenAI(\n api_key=os.getenv(\"ROUTER_API_KEY\"),\n base_url=os.getenv(\"ROUTER_BASE_URL\")\n)\n```\n\n### Xử lý Lỗi\n\n```python\nfrom openai import OpenAI, OpenAIError\n\nclient = OpenAI(\n api_key=\"your-api-key\",\n base_url=\"http://localhost:20128/v1\"\n)\n\ntry:\n response = client.chat.completions.create(\n model=\"cc/claude-sonnet-4-20250514\",\n messages=[{\"role\": \"user\", \"content\": \"Hello\"}]\n )\n print(response.choices[0].message.content)\nexcept OpenAIError as e:\n print(f\"Error: {e}\")\n```\n\n### Logic Retry\n\n```python\nimport time\nfrom openai import OpenAI, RateLimitError\n\nclient = OpenAI(\n api_key=\"your-api-key\",\n base_url=\"http://localhost:20128/v1\"\n)\n\ndef chat_with_retry(prompt, max_retries=3):\n for attempt in range(max_retries):\n try:\n response = client.chat.completions.create(\n model=\"cc/claude-sonnet-4-20250514\",\n messages=[{\"role\": \"user\", \"content\": prompt}]\n )\n return response.choices[0].message.content\n except RateLimitError:\n if attempt < max_retries - 1:\n time.sleep(2 ** attempt) # Exponential backoff\n else:\n raise\n```\n\n## Troubleshooting\n\n### Vấn đề Connection\n\n**Vấn đề:** Không kết nối được đến 9Router\n```bash\n# Check if 9Router is running\ncurl http://localhost:20128/health\n\n# Expected response:\n{\"status\": \"ok\"}\n```\n\n**Giải pháp:**\n- Xác minh 9Router đang chạy\n- Kiểm tra port 20128 không bị chặn\n- Đảm bảo base URL đúng (bao gồm `/v1`)\n\n### Lỗi Xác thực\n\n**Vấn đề:** 401 Unauthorized\n```\nError: Invalid API key\n```\n\n**Giải pháp:**\n- Xác minh API key từ dashboard\n- Kiểm tra format header Authorization: `Bearer your-api-key`\n- Đảm bảo không có khoảng trắng hoặc xuống dòng thừa trong API key\n\n### Model Not Found\n\n**Vấn đề:** 404 Model not found\n```\nError: Model 'cc/claude-opus' not found\n```\n\n**Giải pháp:**\n- Dùng tên model chính xác (case-sensitive)\n- Kiểm tra model có sẵn: `curl http://localhost:20128/v1/models`\n- Xác minh model được bật trong plan của bạn\n\n### Vấn đề Timeout\n\n**Vấn đề:** Request timeout\n```\nError: Request timed out after 30s\n```\n\n**Giải pháp:**\n- Tăng timeout trong cấu hình client\n- Dùng model nhanh hơn cho task nhạy cảm về thời gian\n- Kiểm tra kết nối network đến 9Router\n\n### Rate Limiting\n\n**Vấn đề:** 429 Too Many Requests\n```\nError: Rate limit exceeded\n```\n\n**Giải pháp:**\n- Triển khai exponential backoff\n- Giảm tần suất request\n- Kiểm tra rate limit trong dashboard\n- Cân nhắc nâng cấp plan\n\n## Best Practices\n\n### Bảo mật\n- Lưu API key trong biến môi trường\n- Không bao giờ commit API key vào version control\n- Dùng HTTPS cho cloud deployment\n- Xoay API key định kỳ\n\n### Hiệu năng\n- Dùng model phù hợp với độ phức tạp task\n- Triển khai caching cho query lặp lại\n- Dùng streaming cho phản hồi dài\n- Batch request khi có thể\n\n### Xử lý Lỗi\n- Luôn triển khai try-catch block\n- Thêm logic retry với exponential backoff\n- Log lỗi để debug\n- Cung cấp cơ chế fallback\n\n### Tối ưu Chi phí\n- Chọn model tiết kiệm chi phí cho task đơn giản\n- Cache phản hồi khi phù hợp\n- Theo dõi usage trong dashboard\n- Đặt giới hạn request trong code\n\n## Bước tiếp theo\n\n- [Cấu hình Cursor](cursor.md) cho tích hợp IDE\n- [Setup Continue](continue.md) cho VSCode\n- [Khám phá CLI usage](../cli/basic-usage.md)\n- [Tìm hiểu về chọn model](../models/overview.md)\n- [API Reference](../api/reference.md)\n"}, "e93298305656c9eac093d8280392d868c1704fee": {"t": 1785758900.3698332, "n": 8901, "h": "112ae36200ae684c", "c": "# Tích hợp các Công cụ khác\n\n9Router tương thích với mọi công cụ hỗ trợ format API OpenAI. Hướng dẫn này bao gồm pattern tích hợp tổng quát cho nhiều công cụ và ứng dụng tùy chỉnh.\n\n## Tổng quan\n\n9Router cung cấp API endpoint tương thích OpenAI hoạt động với:\n- Script và ứng dụng tùy chỉnh\n- API client và công cụ test\n- Công cụ CLI và utility\n- Tích hợp bên thứ ba\n- Framework phát triển\n\n## Pattern Setup Tổng quát\n\nMọi công cụ tương thích OpenAI có thể kết nối đến 9Router bằng các cài đặt sau:\n\n**9Router cục bộ:**\n```\nBase URL: http://localhost:20128/v1\nAPI Key: your-api-key-from-dashboard\nModel: any 9Router model (cc/*, cx/*, glm/*, etc.)\n```\n\n**9Router cloud:**\n```\nBase URL: https://9router.com/v1\nAPI Key: your-api-key-from-dashboard\nModel: any 9Router model (cc/*, cx/*, glm/*, etc.)\n```\n\n## Model có sẵn\n\n### Claude Models (Anthropic)\n- `cc/claude-opus-4-5-20251101`\n- `cc/claude-sonnet-4-20250514`\n- `cc/claude-haiku-4-20250514`\n\n### DeepSeek Models\n- `cx/deepseek-chat`\n- `cx/deepseek-reasoner`\n\n### GLM Models (Zhipu AI)\n- `glm/glm-4-plus`\n- `glm/glm-4-flash`\n\n## Ví dụ Tích hợp\n\n### Python với OpenAI SDK\n\n```python\nfrom openai import OpenAI\n\nclient = OpenAI(\n api_key=\"your-api-key-from-dashboard\",\n base_url=\"http://localhost:20128/v1\"\n)\n\nresponse = client.chat.completions.create(\n model=\"cc/claude-sonnet-4-20250514\",\n messages=[\n {\"role\": \"user\", \"content\": \"Hello, how are you?\"}\n ]\n)\n\nprint(response.choices[0].message.content)\n```\n\n### Node.js với OpenAI SDK\n\n```javascript\nimport OpenAI from \"openai\";\n\nconst client = new OpenAI({\n apiKey: \"your-api-key-from-dashboard\",\n baseURL: \"http://localhost:20128/v1\"\n});\n\nconst response = await client.chat.completions.create({\n model: \"cc/claude-sonnet-4-20250514\",\n messages: [\n { role: \"user\", content: \"Hello, how are you?\" }\n ]\n});\n\nconsole.log(response.choices[0].message.content);\n```\n\n### Lệnh cURL\n\n```bash\ncurl http://localhost:20128/v1/chat/completions \\\n -H \"Content-Type: application/json\" \\\n -H \"Authorization: Bearer your-api-key-from-dashboard\" \\\n -d '{\n \"model\": \"cc/claude-sonnet-4-20250514\",\n \"messages\": [\n {\"role\": \"user\", \"content\": \"Hello, how are you?\"}\n ]\n }'\n```\n\n### HTTP Client (Postman, Insomnia)\n\n**Request:**\n```\nPOST http://localhost:20128/v1/chat/completions\n```\n\n**Headers:**\n```\nContent-Type: application/json\nAuthorization: Bearer your-api-key-from-dashboard\n```\n\n**Body:**\n```json\n{\n \"model\": \"cc/claude-sonnet-4-20250514\",\n \"messages\": [\n {\"role\": \"user\", \"content\": \"Hello, how are you?\"}\n ],\n \"temperature\": 0.7,\n \"max_tokens\": 1000\n}\n```\n\n### Tích hợp LangChain\n\n```python\nfrom langchain.chat_models import ChatOpenAI\nfrom langchain.schema import HumanMessage\n\nllm = ChatOpenAI(\n model_name=\"cc/claude-sonnet-4-20250514\",\n openai_api_key=\"your-api-key-from-dashboard\",\n openai_api_base=\"http://localhost:20128/v1\",\n temperature=0.7\n)\n\nmessages = [HumanMessage(content=\"Explain quantum computing\")]\nresponse = llm(messages)\nprint(response.content)\n```\n\n### Tích hợp LlamaIndex\n\n```python\nfrom llama_index.llms import OpenAI\n\nllm = OpenAI(\n model=\"cc/claude-sonnet-4-20250514\",\n api_key=\"your-api-key-from-dashboard\",\n api_base=\"http://localhost:20128/v1\"\n)\n\nresponse = llm.complete(\"What is machine learning?\")\nprint(response.text)\n```\n\n## Ví dụ Script Tùy chỉnh\n\n### Script Xử lý Batch\n\n```python\nimport openai\nimport json\n\nopenai.api_key = \"your-api-key-from-dashboard\"\nopenai.api_base = \"http://localhost:20128/v1\"\n\ndef process_batch(prompts, model=\"cx/deepseek-chat\"):\n results = []\n for prompt in prompts:\n response = openai.ChatCompletion.create(\n model=model,\n messages=[{\"role\": \"user\", \"content\": prompt}]\n )\n results.append({\n \"prompt\": prompt,\n \"response\": response.choices[0].message.content\n })\n return results\n\nprompts = [\n \"Explain AI in one sentence\",\n \"What is machine learning?\",\n \"Define neural networks\"\n]\n\nresults = process_batch(prompts)\nprint(json.dumps(results, indent=2))\n```\n\n### Xử lý Streaming Response\n\n```javascript\nimport OpenAI from \"openai\";\n\nconst client = new OpenAI({\n apiKey: \"your-api-key-from-dashboard\",\n baseURL: \"http://localhost:20128/v1\"\n});\n\nasync function streamResponse(prompt) {\n const stream = await client.chat.completions.create({\n model: \"cc/claude-sonnet-4-20250514\",\n messages: [{ role: \"user\", content: prompt }],\n stream: true\n });\n\n for await (const chunk of stream) {\n const content = chunk.choices[0]?.delta?.content || \"\";\n process.stdout.write(content);\n }\n}\n\nstreamResponse(\"Write a short story about AI\");\n```\n\n### So sánh Nhiều Model\n\n```python\nfrom openai import OpenAI\n\nclient = OpenAI(\n api_key=\"your-api-key-from-dashboard\",\n base_url=\"http://localhost:20128/v1\"\n)\n\nmodels = [\n \"cc/claude-sonnet-4-20250514\",\n \"cx/deepseek-chat\",\n \"glm/glm-4-plus\"\n]\n\nprompt = \"Explain quantum computing in simple terms\"\n\nfor model in models:\n response = client.chat.completions.create(\n model=model,\n messages=[{\"role\": \"user\", \"content\": prompt}]\n )\n print(f\"\\n=== {model} ===\")\n print(response.choices[0].message.content)\n```\n\n## Pattern Tích hợp Phổ biến\n\n### Biến môi trường\n\nLưu credentials an toàn:\n\n```bash\n# .env file\nROUTER_API_KEY=your-api-key-from-dashboard\nROUTER_BASE_URL=http://localhost:20128/v1\nROUTER_MODEL=cc/claude-sonnet-4-20250514\n```\n\n```python\nimport os\nfrom openai import OpenAI\n\nclient = OpenAI(\n api_key=os.getenv(\"ROUTER_API_KEY\"),\n base_url=os.getenv(\"ROUTER_BASE_URL\")\n)\n```\n\n### Xử lý Lỗi\n\n```python\nfrom openai import OpenAI, OpenAIError\n\nclient = OpenAI(\n api_key=\"your-api-key\",\n base_url=\"http://localhost:20128/v1\"\n)\n\ntry:\n response = client.chat.completions.create(\n model=\"cc/claude-sonnet-4-20250514\",\n messages=[{\"role\": \"user\", \"content\": \"Hello\"}]\n )\n print(response.choices[0].message.content)\nexcept OpenAIError as e:\n print(f\"Error: {e}\")\n```\n\n### Logic Retry\n\n```python\nimport time\nfrom openai import OpenAI, RateLimitError\n\nclient = OpenAI(\n api_key=\"your-api-key\",\n base_url=\"http://localhost:20128/v1\"\n)\n\ndef chat_with_retry(prompt, max_retries=3):\n for attempt in range(max_retries):\n try:\n response = client.chat.completions.create(\n model=\"cc/claude-sonnet-4-20250514\",\n messages=[{\"role\": \"user\", \"content\": prompt}]\n )\n return response.choices[0].message.content\n except RateLimitError:\n if attempt < max_retries - 1:\n time.sleep(2 ** attempt) # Exponential backoff\n else:\n raise\n```\n\n## Troubleshooting\n\n### Vấn đề Connection\n\n**Vấn đề:** Không kết nối được đến 9Router\n```bash\n# Check if 9Router is running\ncurl http://localhost:20128/health\n\n# Expected response:\n{\"status\": \"ok\"}\n```\n\n**Giải pháp:**\n- Xác minh 9Router đang chạy\n- Kiểm tra port 20128 không bị chặn\n- Đảm bảo base URL đúng (bao gồm `/v1`)\n\n### Lỗi Xác thực\n\n**Vấn đề:** 401 Unauthorized\n```\nError: Invalid API key\n```\n\n**Giải pháp:**\n- Xác minh API key từ dashboard\n- Kiểm tra format header Authorization: `Bearer your-api-key`\n- Đảm bảo không có khoảng trắng hoặc xuống dòng thừa trong API key\n\n### Model Not Found\n\n**Vấn đề:** 404 Model not found\n```\nError: Model 'cc/claude-opus' not found\n```\n\n**Giải pháp:**\n- Dùng tên model chính xác (case-sensitive)\n- Kiểm tra model có sẵn: `curl http://localhost:20128/v1/models`\n- Xác minh model được bật trong plan của bạn\n\n### Vấn đề Timeout\n\n**Vấn đề:** Request timeout\n```\nError: Request timed out after 30s\n```\n\n**Giải pháp:**\n- Tăng timeout trong cấu hình client\n- Dùng model nhanh hơn cho task nhạy cảm về thời gian\n- Kiểm tra kết nối network đến 9Router\n\n### Rate Limiting\n\n**Vấn đề:** 429 Too Many Requests\n```\nError: Rate limit exceeded\n```\n\n**Giải pháp:**\n- Triển khai exponential backoff\n- Giảm tần suất request\n- Kiểm tra rate limit trong dashboard\n- Cân nhắc nâng cấp plan\n\n## Best Practices\n\n### Bảo mật\n- Lưu API key trong biến môi trường\n- Không bao giờ commit API key vào version control\n- Dùng HTTPS cho cloud deployment\n- Xoay API key định kỳ\n\n### Hiệu năng\n- Dùng model phù hợp với độ phức tạp task\n- Triển khai caching cho query lặp lại\n- Dùng streaming cho phản hồi dài\n- Batch request khi có thể\n\n### Xử lý Lỗi\n- Luôn triển khai try-catch block\n- Thêm logic retry với exponential backoff\n- Log lỗi để debug\n- Cung cấp cơ chế fallback\n\n### Tối ưu Chi phí\n- Chọn model tiết kiệm chi phí cho task đơn giản\n- Cache phản hồi khi phù hợp\n- Theo dõi usage trong dashboard\n- Đặt giới hạn request trong code\n\n## Bước tiếp theo\n\n- [Cấu hình Cursor](cursor.md) cho tích hợp IDE\n- [Setup Continue](continue.md) cho VSCode\n- [Khám phá CLI usage](../cli/basic-usage.md)\n- [Tìm hiểu về chọn model](../models/overview.md)\n- [API Reference](../api/reference.md)\n"}}}} |