diff --git a/cli/cli.js b/cli/cli.js index 06cd979..6e4bfe6 100755 --- a/cli/cli.js +++ b/cli/cli.js @@ -197,8 +197,8 @@ function killCloudflaredByAppPort(appPort) { function killAllAppProcesses(appPort) { return new Promise((resolve) => { try { - // Kill MITM first (admin/sudo process, needs special handling) - killMitmByPidFile(); + // Kill MIT first (privileged process, needs special handling) + killProxyByPidFile(); // Kill cloudflared/tailscale by PID file (precise, only this app's tunnel) killTunnelByPidFile(); @@ -305,13 +305,13 @@ function waitForExit(pid, timeoutMs) { return false; } -// Kill MITM server by PID file (MITM runs as admin/sudo, needs special handling) -// Sends SIGTERM first so MITM can clean up /etc/hosts entries before dying. -function killMitmByPidFile() { +// Kill MIT server by PID file (runs privileged, needs special handling) +// Sends SIGTERM first so MIT can clean up host entries before dying. +function killProxyByPidFile() { try { - const mitmPidFile = path.join(getAppDataDir(), "mitm", ".mitm.pid"); - if (!fs.existsSync(mitmPidFile)) return; - const pid = parseInt(fs.readFileSync(mitmPidFile, "utf8").trim(), 10); + const pidFile = path.join(getAppDataDir(), "mitm", ".mitm.pid"); + if (!fs.existsSync(pidFile)) return; + const pid = parseInt(fs.readFileSync(pidFile, "utf8").trim(), 10); if (!pid) return; if (process.platform === "win32") { @@ -333,7 +333,7 @@ function killMitmByPidFile() { catch { try { process.kill(pid, "SIGKILL"); } catch { } } } } - try { fs.unlinkSync(mitmPidFile); } catch { } + try { fs.unlinkSync(pidFile); } catch { } } catch { } } @@ -584,8 +584,8 @@ function startServer(latestVersion) { const { killTray } = require("./src/cli/tray/tray"); killTray(); } catch (e) { } - // Kill MITM server (admin/sudo process) via PID file - killMitmByPidFile(); + // Kill MIT server (privileged process) via PID file + killProxyByPidFile(); // Kill cloudflared/tailscale via PID file (only this app's tunnel) killTunnelByPidFile(); // Kill server process directly @@ -772,7 +772,7 @@ function startServer(latestVersion) { if (aliveMs >= RESTART_RESET_MS) restartCount = 0; if (restartCount >= MAX_RESTARTS) { - console.error(`\n⚠️ Server crashed ${MAX_RESTARTS} times. Disabling MITM and restarting...`); + console.error(`\n⚠️ Server crashed ${MAX_RESTARTS} times. Disabling MIT and restarting...`); try { const dbPath = path.join(os.homedir(), process.platform === "win32" ? path.join("AppData", "Roaming", "9router", "db.json") : path.join(".9router", "db.json")); if (fs.existsSync(dbPath)) { diff --git a/cli/package.json b/cli/package.json index 6c907c1..097f409 100644 --- a/cli/package.json +++ b/cli/package.json @@ -1,6 +1,6 @@ { "name": "9router", - "version": "0.4.52", + "version": "0.4.55", "description": "9Router CLI - Start and manage 9Router server", "bin": { "9router": "./cli.js" diff --git a/open-sse/config/providers.js b/open-sse/config/providers.js index a8b0dd8..5f9d6a6 100644 --- a/open-sse/config/providers.js +++ b/open-sse/config/providers.js @@ -395,6 +395,8 @@ export const PROVIDERS = { baseUrl: "https://token-plan-sgp.xiaomimimo.com/v1/chat/completions", format: "openai" }, + // Region map for Xiaomi MiMo Token Plan (keys are cluster-specific) + // Used by resolveXiaomiTokenplanBaseUrl below // === Free-tier providers (synced from OmniRoute) === // Claude-format with Claude CLI header spoofing (auth: x-api-key) agentrouter: { baseUrl: "https://agentrouter.org/v1/messages", format: "claude", headers: { ...CLAUDE_CLI_SPOOF_HEADERS } }, @@ -437,3 +439,15 @@ export function resolveOllamaLocalHost(credentials) { const raw = credentials?.providerSpecificData?.baseUrl?.trim(); return (raw || OLLAMA_LOCAL_DEFAULT_HOST).replace(/\/$/, ""); } + +export const XIAOMI_TOKENPLAN_REGIONS = { + sgp: "https://token-plan-sgp.xiaomimimo.com/v1", + cn: "https://token-plan-cn.xiaomimimo.com/v1", + ams: "https://token-plan-ams.xiaomimimo.com/v1" +}; +export const XIAOMI_TOKENPLAN_DEFAULT_REGION = "sgp"; + +export function resolveXiaomiTokenplanBaseUrl(credentials) { + const region = credentials?.providerSpecificData?.region; + return XIAOMI_TOKENPLAN_REGIONS[region] || XIAOMI_TOKENPLAN_REGIONS[XIAOMI_TOKENPLAN_DEFAULT_REGION]; +} diff --git a/open-sse/executors/default.js b/open-sse/executors/default.js index 47bc9b8..bca9221 100644 --- a/open-sse/executors/default.js +++ b/open-sse/executors/default.js @@ -1,5 +1,5 @@ import { BaseExecutor } from "./base.js"; -import { PROVIDERS } from "../config/providers.js"; +import { PROVIDERS, resolveXiaomiTokenplanBaseUrl } from "../config/providers.js"; import { OAUTH_ENDPOINTS, buildKimiHeaders } from "../config/appConstants.js"; import { buildClineHeaders } from "../../src/shared/utils/clineAuth.js"; import { getCachedClaudeHeaders } from "../utils/claudeHeaderCache.js"; @@ -39,6 +39,9 @@ export class DefaultExecutor extends BaseExecutor { case "gemini": return `${this.config.baseUrl}/${model}:${stream ? "streamGenerateContent?alt=sse" : "generateContent"}`; default: { + if (this.provider === "xiaomi-tokenplan") { + return `${resolveXiaomiTokenplanBaseUrl(credentials)}/chat/completions`; + } const url = this.config.baseUrl; if (url?.includes("{accountId}")) { const accountId = credentials?.providerSpecificData?.accountId; diff --git a/package.json b/package.json index ecf204f..f063e70 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "9router-app", - "version": "0.4.52", + "version": "0.4.55", "description": "9Router web dashboard", "private": true, "scripts": { diff --git a/src/app/(dashboard)/dashboard/providers/[id]/AddApiKeyModal.js b/src/app/(dashboard)/dashboard/providers/[id]/AddApiKeyModal.js index 3145c8e..1a4c8b7 100644 --- a/src/app/(dashboard)/dashboard/providers/[id]/AddApiKeyModal.js +++ b/src/app/(dashboard)/dashboard/providers/[id]/AddApiKeyModal.js @@ -3,6 +3,7 @@ import { useState } from "react"; import PropTypes from "prop-types"; import { Button, Badge, Input, Modal, Select } from "@/shared/components"; +import { AI_PROVIDERS } from "@/shared/constants/providers"; const BULK_PLACEHOLDER = `name1|sk-key1\nname2|sk-key2\nsk-key-only-auto-named`; @@ -17,6 +18,8 @@ export default function AddApiKeyModal({ isOpen, provider, providerName, isCompa const isAzure = provider === "azure"; const isCloudflareAi = provider === "cloudflare-ai"; + const providerRegions = AI_PROVIDERS?.[provider]?.regions || null; + const defaultRegion = AI_PROVIDERS?.[provider]?.defaultRegion || providerRegions?.[0]?.id || ""; const [formData, setFormData] = useState({ name: "", @@ -33,6 +36,7 @@ export default function AddApiKeyModal({ isOpen, provider, providerName, isCompa organization: "", }); const [cloudflareData, setCloudflareData] = useState({ accountId: "" }); + const [region, setRegion] = useState(defaultRegion); const [validating, setValidating] = useState(false); const [validationResult, setValidationResult] = useState(null); const [saving, setSaving] = useState(false); @@ -55,6 +59,9 @@ export default function AddApiKeyModal({ isOpen, provider, providerName, isCompa if (isCloudflareAi) { return { accountId: cloudflareData.accountId }; } + if (providerRegions && region) { + return { region }; + } return undefined; }; @@ -234,6 +241,14 @@ export default function AddApiKeyModal({ isOpen, provider, providerName, isCompa )}

)} + {providerRegions && ( + { + if (providerId === "antigravity" && typeof window !== "undefined") { + const confirmed = window.localStorage.getItem(AG_RISK_STORAGE_KEY) === "true"; + if (!confirmed) { + setShowAgRiskModal(true); + return; + } + } + if (isOAuth) { + setShowOAuthModal(true); + return; + } + setAddConnectionError(""); + setShowAddApiKeyModal(true); + }; + + const handleAgRiskConfirm = () => { + if (typeof window !== "undefined") { + window.localStorage.setItem(AG_RISK_STORAGE_KEY, "true"); + } + setShowAgRiskModal(false); + if (isOAuth) { + setShowOAuthModal(true); + return; + } + setAddConnectionError(""); + setShowAddApiKeyModal(true); + }; + const providerInfo = providerNode ? { id: providerNode.id, @@ -1066,14 +1098,7 @@ export default function ProviderDetailPage() { @@ -1099,14 +1124,7 @@ export default function ProviderDetailPage() {