diff --git a/README.md b/README.md
index 0e2e8d9f..b7c13a48 100644
--- a/README.md
+++ b/README.md
@@ -145,7 +145,7 @@ Default URLs:

- Claude Code
+ Claude-Code
|

@@ -193,7 +193,36 @@ Default URLs:
## 🌐 Supported Providers
-### 🆓 Free Providers (Unlimited)
+### 🔐 OAuth Providers
+
+
+
+### 🆓 Free Providers
@@ -222,35 +251,6 @@ Default URLs:
-### 🔐 OAuth Providers
-
-
-
### 🔑 API Key Providers (40+)
diff --git a/package.json b/package.json
index d2547dc2..26b6e015 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "9router-app",
- "version": "0.3.32",
+ "version": "0.3.33",
"description": "9Router web dashboard",
"private": true,
"scripts": {
diff --git a/public/providers/chutes.png b/public/providers/chutes.png
index 07e4d1db..f39a6c21 100644
Binary files a/public/providers/chutes.png and b/public/providers/chutes.png differ
diff --git a/public/providers/kilocode.png b/public/providers/kilocode.png
index 147dac0c..b1272cfe 100644
Binary files a/public/providers/kilocode.png and b/public/providers/kilocode.png differ
diff --git a/public/providers/siliconflow.png b/public/providers/siliconflow.png
index a7358145..d599df2e 100644
Binary files a/public/providers/siliconflow.png and b/public/providers/siliconflow.png differ
diff --git a/src/app/api/provider-nodes/validate/route.js b/src/app/api/provider-nodes/validate/route.js
index 6d70a373..f85d8964 100644
--- a/src/app/api/provider-nodes/validate/route.js
+++ b/src/app/api/provider-nodes/validate/route.js
@@ -1,5 +1,37 @@
import { NextResponse } from "next/server";
+// Fetch with timeout wrapper
+const fetchWithTimeout = (url, options, timeout = 10000) => {
+ return Promise.race([
+ fetch(url, options),
+ new Promise((_, reject) =>
+ setTimeout(() => reject(new Error("Request timeout")), timeout)
+ )
+ ]);
+};
+
+// Validate URL format
+const isValidUrl = (url) => {
+ try {
+ new URL(url);
+ return true;
+ } catch {
+ return false;
+ }
+};
+
+// Parse error details for user-friendly messages
+const getErrorMessage = (error) => {
+ if (error.cause?.code === "ECONNREFUSED") return "Connection refused - provider node offline or unreachable";
+ if (error.cause?.code === "ENOTFOUND") return "DNS lookup failed - invalid domain or network issue";
+ if (error.cause?.code === "ETIMEDOUT") return "Connection timeout - provider node too slow";
+ if (error.message.includes("timeout")) return "Request timeout (>10s) - provider node not responding";
+ if (error.cause?.code === "CERT_HAS_EXPIRED") return "SSL certificate expired";
+ if (error.cause?.code === "UNABLE_TO_VERIFY_LEAF_SIGNATURE") return "SSL certificate verification failed";
+ if (error.cause?.code) return `Network error: ${error.cause.code}`;
+ return "Network connection failed - check URL and network connectivity";
+};
+
// POST /api/provider-nodes/validate - Validate API key against base URL
export async function POST(request) {
try {
@@ -10,6 +42,11 @@ export async function POST(request) {
return NextResponse.json({ error: "Base URL and API key required" }, { status: 400 });
}
+ // Validate URL format
+ if (!isValidUrl(baseUrl)) {
+ return NextResponse.json({ error: "Invalid URL format" }, { status: 400 });
+ }
+
// Anthropic Compatible Validation
if (type === "anthropic-compatible") {
// Robustly construct URL: remove trailing slash, and remove trailing /messages if user added it
@@ -21,7 +58,7 @@ export async function POST(request) {
// Use /models endpoint for validation as many compatible providers support it (like OpenAI)
const modelsUrl = `${normalizedBase}/models`;
- const res = await fetch(modelsUrl, {
+ const res = await fetchWithTimeout(modelsUrl, {
method: "GET",
headers: {
"x-api-key": apiKey,
@@ -30,18 +67,27 @@ export async function POST(request) {
}
});
- return NextResponse.json({ valid: res.ok, error: res.ok ? null : "Invalid API key" });
+ return NextResponse.json({ valid: res.ok, error: res.ok ? null : "Invalid API key or unauthorized" });
}
// OpenAI Compatible Validation (Default)
const modelsUrl = `${baseUrl.replace(/\/$/, "")}/models`;
- const res = await fetch(modelsUrl, {
+ const res = await fetchWithTimeout(modelsUrl, {
headers: { "Authorization": `Bearer ${apiKey}` },
});
- return NextResponse.json({ valid: res.ok, error: res.ok ? null : "Invalid API key" });
+ return NextResponse.json({ valid: res.ok, error: res.ok ? null : "Invalid API key or unauthorized" });
} catch (error) {
- console.log("Error validating provider node:", error);
- return NextResponse.json({ error: "Validation failed" }, { status: 500 });
+ const errorMessage = getErrorMessage(error);
+ console.error("Error validating provider node:", {
+ message: error.message,
+ cause: error.cause,
+ code: error.cause?.code,
+ userMessage: errorMessage
+ });
+ return NextResponse.json({
+ valid: false,
+ error: errorMessage
+ }, { status: 500 });
}
}
diff --git a/src/mitm/manager.js b/src/mitm/manager.js
index 168d51f6..5a68d5d5 100644
--- a/src/mitm/manager.js
+++ b/src/mitm/manager.js
@@ -374,7 +374,7 @@ async function startServer(apiKey, sudoPassword) {
].join("\r\n");
const tmpVbs = path.join(os.tmpdir(), `mitm_uac_${Date.now()}.vbs`);
fs.writeFileSync(tmpVbs, vbs, "utf8");
- spawn("wscript.exe", [tmpVbs], { stdio: "ignore", windowsHide: false, detached: true }).unref();
+ spawn("wscript.exe", [tmpVbs], { stdio: "ignore", windowsHide: true, detached: true }).unref();
await new Promise((resolve, reject) => {
const deadline = Date.now() + 90000;
@@ -511,7 +511,7 @@ async function stopServer(sudoPassword) {
].join("\r\n");
const tmpVbs = path.join(os.tmpdir(), "mitm_stop_uac.vbs");
fs.writeFileSync(tmpVbs, vbs, "utf8");
- spawn("wscript.exe", [tmpVbs], { stdio: "ignore", windowsHide: false, detached: true }).unref();
+ spawn("wscript.exe", [tmpVbs], { stdio: "ignore", windowsHide: true, detached: true }).unref();
await new Promise((resolve) => {
const deadline = Date.now() + 30000;
|