Failed to load usage statistics.
;
+
+ const spinner = (
+
+
+ {notifications.map((n) => {
+ const style = getToastStyle(n.type);
+ return (
+
+
+
{style.icon}
+
+ {n.title ?
{n.title}
: null}
+
{n.message}
+
+ {n.dismissible ? (
+
+ ) : null}
+
+
+ );
+ })}
+
+ {/* Mobile sidebar overlay */}
+ {sidebarOpen && (
+
setSidebarOpen(false)}
+ />
+ )}
+
+ {/* Sidebar - Desktop */}
+
+
+
+
+ {/* Sidebar - Mobile */}
+
+ setSidebarOpen(false)} />
+
+
+ {/* Main content */}
+
+ setSidebarOpen(true)} />
+
+
+
+ );
+}
diff --git a/9router/src/shared/components/layouts/index.js b/9router/src/shared/components/layouts/index.js
new file mode 100644
index 00000000..4b6ac5fc
--- /dev/null
+++ b/9router/src/shared/components/layouts/index.js
@@ -0,0 +1,4 @@
+// Layout Components - Export all
+export { default as DashboardLayout } from "./DashboardLayout";
+export { default as AuthLayout } from "./AuthLayout";
+
diff --git a/9router/src/shared/constants/cliTools.js b/9router/src/shared/constants/cliTools.js
new file mode 100644
index 00000000..69f9deeb
--- /dev/null
+++ b/9router/src/shared/constants/cliTools.js
@@ -0,0 +1,249 @@
+// MITM Tools — IDE tools intercepted via MITM proxy
+export const MITM_TOOLS = {
+ antigravity: {
+ id: "antigravity",
+ name: "Antigravity",
+ image: "/providers/antigravity.png",
+ color: "#4285F4",
+ description: "Google Antigravity IDE with MITM",
+ configType: "mitm",
+ mitmDomain: "daily-cloudcode-pa.googleapis.com",
+ modelAliases: ["claude-opus-4-6-thinking", "claude-sonnet-4-6", "gemini-3-flash", "gpt-oss-120b-medium", "gemini-3-pro-high", "gemini-3-pro-low"],
+ defaultModels: [
+ { id: "gemini-3.1-pro-high", name: "Gemini 3.1 Pro High", alias: "gemini-3.1-pro-high" },
+ { id: "gemini-3.1-pro-low", name: "Gemini 3.1 Pro Low", alias: "gemini-3.1-pro-low" },
+ { id: "gemini-3-flash", name: "Gemini 3 Flash", alias: "gemini-3-flash" },
+ { id: "claude-sonnet-4-6", name: "Claude Sonnet 4.6", alias: "claude-sonnet-4-6" },
+ { id: "claude-opus-4-6-thinking", name: "Claude Opus 4.6 Thinking", alias: "claude-opus-4-6-thinking" },
+ { id: "gpt-oss-120b-medium", name: "GPT OSS 120B Medium", alias: "gpt-oss-120b-medium" },
+ ],
+ },
+ copilot: {
+ id: "copilot",
+ name: "GitHub Copilot",
+ image: "/providers/copilot.png",
+ color: "#1F6FEB",
+ description: "GitHub Copilot IDE with MITM",
+ configType: "mitm",
+ mitmDomain: "api.individual.githubcopilot.com",
+ modelAliases: ["gpt-4o-mini", "claude-haiku-4.5", "gpt-4o", "gpt-5-mini"],
+ defaultModels: [
+ { id: "gpt-4o", name: "GPT-4o", alias: "gpt-4o" },
+ { id: "gpt-4.1", name: "GPT-4.1", alias: "gpt-4.1" },
+ { id: "claude-haiku-4.5", name: "Claude Haiku 4.5", alias: "claude-haiku-4.5" },
+ ],
+ },
+ kiro: {
+ id: "kiro",
+ name: "Kiro",
+ image: "/providers/kiro.png",
+ color: "#FF6B00",
+ description: "Kiro IDE with MITM",
+ configType: "mitm",
+ mitmDomain: "q.us-east-1.amazonaws.com",
+ defaultModels: [
+ { id: "claude-sonnet-4.5", name: "Claude Sonnet 4.5", alias: "claude-sonnet-4.5" },
+ { id: "claude-sonnet-4", name: "Claude Sonnet 4", alias: "claude-sonnet-4" },
+ { id: "claude-haiku-4.5", name: "Claude Haiku 4.5", alias: "claude-haiku-4.5" },
+ { id: "deepseek-3.2", name: "DeepSeek 3.2", alias: "deepseek-3.2" },
+ { id: "minimax-m2.1", name: "MiniMax M2.1", alias: "minimax-m2.1" },
+ { id: "simple-task", name: "Qwen3 Coder Next", alias: "simple-task" },
+ ],
+ },
+ // cursor: {
+ // id: "cursor",
+ // name: "Cursor",
+ // image: "/providers/cursor.png",
+ // color: "#000000",
+ // description: "Cursor IDE with MITM",
+ // configType: "mitm",
+ // mitmDomain: "api2.cursor.sh",
+ // defaultModels: [
+ // { id: "claude-sonnet-4-5", name: "Claude Sonnet 4.5", alias: "claude-sonnet-4-5" },
+ // { id: "claude-opus-4", name: "Claude Opus 4", alias: "claude-opus-4" },
+ // { id: "gpt-4o", name: "GPT-4o", alias: "gpt-4o" },
+ // ],
+ // },
+};
+
+// CLI Tools configuration
+export const CLI_TOOLS = {
+ claude: {
+ id: "claude",
+ name: "Claude Code",
+ icon: "terminal",
+ color: "#D97757",
+ description: "Anthropic Claude Code CLI",
+ configType: "env",
+ envVars: {
+ baseUrl: "ANTHROPIC_BASE_URL",
+ model: "ANTHROPIC_MODEL",
+ opusModel: "ANTHROPIC_DEFAULT_OPUS_MODEL",
+ sonnetModel: "ANTHROPIC_DEFAULT_SONNET_MODEL",
+ haikuModel: "ANTHROPIC_DEFAULT_HAIKU_MODEL",
+ },
+ modelAliases: ["default", "sonnet", "opus", "haiku", "opusplan"],
+ settingsFile: "~/.claude/settings.json",
+ defaultModels: [
+ { id: "opus", name: "Claude Opus", alias: "opus", envKey: "ANTHROPIC_DEFAULT_OPUS_MODEL", defaultValue: "cc/claude-opus-4-6" },
+ { id: "sonnet", name: "Claude Sonnet", alias: "sonnet", envKey: "ANTHROPIC_DEFAULT_SONNET_MODEL", defaultValue: "cc/claude-sonnet-4-6" },
+ { id: "haiku", name: "Claude Haiku", alias: "haiku", envKey: "ANTHROPIC_DEFAULT_HAIKU_MODEL", defaultValue: "cc/claude-haiku-4-5-20251001" },
+ ],
+ },
+ openclaw: {
+ id: "openclaw",
+ name: "Open Claw",
+ image: "/providers/openclaw.png",
+ color: "#FF6B35",
+ description: "Open Claw AI Assistant",
+ configType: "custom",
+ },
+ codex: {
+ id: "codex",
+ name: "OpenAI Codex CLI",
+ image: "/providers/codex.png",
+ color: "#10A37F",
+ description: "OpenAI Codex CLI",
+ configType: "custom",
+ },
+ opencode: {
+ id: "opencode",
+ name: "OpenCode",
+ image: "/providers/opencode.png",
+ color: "#E87040",
+ description: "OpenCode AI Terminal Assistant",
+ configType: "custom",
+ },
+ droid: {
+ id: "droid",
+ name: "Factory Droid",
+ image: "/providers/droid.png",
+ color: "#00D4FF",
+ description: "Factory Droid AI Assistant",
+ configType: "custom",
+ },
+ cursor: {
+ id: "cursor",
+ name: "Cursor",
+ image: "/providers/cursor.png",
+ color: "#000000",
+ description: "Cursor AI Code Editor",
+ configType: "guide",
+ requiresExternalUrl: true,
+ notes: [
+ { type: "warning", text: "Requires Cursor Pro account to use this feature." },
+ { type: "cloudCheck", text: "Cursor routes requests through its own server, so local endpoint is not supported. Please enable Tunnel or Cloud Endpoint in Settings." },
+ ],
+ guideSteps: [
+ { step: 1, title: "Open Settings", desc: "Go to Settings → Models" },
+ { step: 2, title: "Enable OpenAI API", desc: "Enable \"OpenAI API key\" option" },
+ { step: 3, title: "Base URL", value: "{{baseUrl}}", copyable: true },
+ { step: 4, title: "API Key", type: "apiKeySelector" },
+ { step: 5, title: "Add Custom Model", desc: "Click \"View All Model\" → \"Add Custom Model\"" },
+ { step: 6, title: "Select Model", type: "modelSelector" },
+ ],
+ },
+ cline: {
+ id: "cline",
+ name: "Cline",
+ image: "/providers/cline.png",
+ color: "#00D1B2",
+ description: "Cline AI Coding Assistant",
+ configType: "guide",
+ guideSteps: [
+ { step: 1, title: "Open Settings", desc: "Go to Cline Settings panel" },
+ { step: 2, title: "Select Provider", desc: "Choose API Provider → OpenAI Compatible" },
+ { step: 3, title: "Base URL", value: "{{baseUrl}}/v1", copyable: true },
+ { step: 4, title: "API Key", type: "apiKeySelector" },
+ { step: 5, title: "Select Model", type: "modelSelector" },
+ ],
+ },
+ kilo: {
+ id: "kilo",
+ name: "Kilo Code",
+ image: "/providers/kilocode.png",
+ color: "#FF6B6B",
+ description: "Kilo Code AI Assistant",
+ configType: "guide",
+ guideSteps: [
+ { step: 1, title: "Open Settings", desc: "Go to Kilo Code Settings panel" },
+ { step: 2, title: "Select Provider", desc: "Choose API Provider → OpenAI Compatible" },
+ { step: 3, title: "Base URL", value: "{{baseUrl}}/v1", copyable: true },
+ { step: 4, title: "API Key", type: "apiKeySelector" },
+ { step: 5, title: "Select Model", type: "modelSelector" },
+ ],
+ },
+ roo: {
+ id: "roo",
+ name: "Roo",
+ image: "/providers/roo.png",
+ color: "#FF6B6B",
+ description: "Roo AI Assistant",
+ configType: "guide",
+ guideSteps: [
+ { step: 1, title: "Open Settings", desc: "Go to Roo Settings panel" },
+ { step: 2, title: "Select Provider", desc: "Choose API Provider → Ollama" },
+ { step: 3, title: "Base URL", value: "{{baseUrl}}", copyable: true },
+ { step: 4, title: "API Key", type: "apiKeySelector" },
+ { step: 5, title: "Select Model", type: "modelSelector" },
+ ],
+ },
+ continue: {
+ id: "continue",
+ name: "Continue",
+ image: "/providers/continue.png",
+ color: "#7C3AED",
+ description: "Continue AI Assistant",
+ configType: "guide",
+ guideSteps: [
+ { step: 1, title: "Open Config", desc: "Open Continue configuration file" },
+ { step: 2, title: "API Key", type: "apiKeySelector" },
+ { step: 3, title: "Select Model", type: "modelSelector" },
+ { step: 4, title: "Add Model Config", desc: "Add the following configuration to your models array:" },
+ ],
+ codeBlock: {
+ language: "json",
+ code: `{
+ "apiBase": "{{baseUrl}}",
+ "title": "{{model}}",
+ "model": "{{model}}",
+ "provider": "openai",
+ "apiKey": "{{apiKey}}"
+}`,
+ },
+ },
+ // HIDDEN: gemini-cli
+ // "gemini-cli": {
+ // id: "gemini-cli",
+ // name: "Gemini CLI",
+ // icon: "terminal",
+ // color: "#4285F4",
+ // description: "Google Gemini CLI",
+ // configType: "env",
+ // envVars: {
+ // baseUrl: "GEMINI_API_BASE_URL",
+ // model: "GEMINI_MODEL",
+ // },
+ // defaultModels: [
+ // { id: "gemini-2.5-pro", name: "Gemini 2.5 Pro", alias: "pro" },
+ // { id: "gemini-2.5-flash", name: "Gemini 2.5 Flash", alias: "flash" },
+ // ],
+ // },
+};
+
+// Get all provider models for mapping dropdown
+export const getProviderModelsForMapping = (providers) => {
+ const result = [];
+ providers.forEach(conn => {
+ if (conn.isActive && (conn.testStatus === "active" || conn.testStatus === "success")) {
+ result.push({
+ connectionId: conn.id,
+ provider: conn.provider,
+ name: conn.name,
+ models: conn.models || [],
+ });
+ }
+ });
+ return result;
+};
+
diff --git a/9router/src/shared/constants/colors.js b/9router/src/shared/constants/colors.js
new file mode 100644
index 00000000..873f3e51
--- /dev/null
+++ b/9router/src/shared/constants/colors.js
@@ -0,0 +1,77 @@
+// Claude-inspired color palette for Endpoint Proxy
+// Light theme: Warm beige/cream tones
+// Dark theme: Deep charcoal/brown tones
+
+export const COLORS = {
+ // Primary - Warm Coral/Terracotta (Claude-like)
+ primary: {
+ DEFAULT: "#D97757",
+ hover: "#C56243",
+ light: "#E8A58C",
+ dark: "#B0664D",
+ },
+
+ // Light theme backgrounds
+ light: {
+ bg: "#FBF9F6",
+ bgAlt: "#F5F1ED",
+ surface: "#FFFFFF",
+ sidebar: "rgba(246, 246, 246, 0.8)",
+ border: "rgba(0, 0, 0, 0.1)",
+ textMain: "#383733",
+ textMuted: "#75736E",
+ },
+
+ // Dark theme backgrounds
+ dark: {
+ bg: "#191918",
+ bgAlt: "#1F1F1E",
+ surface: "#242423",
+ sidebar: "rgba(30, 30, 30, 0.8)",
+ border: "rgba(255, 255, 255, 0.1)",
+ textMain: "#ECEBE8",
+ textMuted: "#9E9D99",
+ },
+
+ // Status colors
+ status: {
+ success: "#22C55E",
+ successLight: "#DCFCE7",
+ successDark: "#166534",
+ warning: "#F59E0B",
+ warningLight: "#FEF3C7",
+ warningDark: "#92400E",
+ error: "#EF4444",
+ errorLight: "#FEE2E2",
+ errorDark: "#991B1B",
+ info: "#3B82F6",
+ infoLight: "#DBEAFE",
+ infoDark: "#1E40AF",
+ },
+};
+
+// CSS Variables mapping for Tailwind
+export const CSS_VARIABLES = {
+ light: {
+ "--color-primary": COLORS.primary.DEFAULT,
+ "--color-primary-hover": COLORS.primary.hover,
+ "--color-bg": COLORS.light.bg,
+ "--color-bg-alt": COLORS.light.bgAlt,
+ "--color-surface": COLORS.light.surface,
+ "--color-sidebar": COLORS.light.sidebar,
+ "--color-border": COLORS.light.border,
+ "--color-text-main": COLORS.light.textMain,
+ "--color-text-muted": COLORS.light.textMuted,
+ },
+ dark: {
+ "--color-primary": COLORS.primary.DEFAULT,
+ "--color-primary-hover": COLORS.primary.hover,
+ "--color-bg": COLORS.dark.bg,
+ "--color-bg-alt": COLORS.dark.bgAlt,
+ "--color-surface": COLORS.dark.surface,
+ "--color-sidebar": COLORS.dark.sidebar,
+ "--color-border": COLORS.dark.border,
+ "--color-text-main": COLORS.dark.textMain,
+ "--color-text-muted": COLORS.dark.textMuted,
+ },
+};
diff --git a/9router/src/shared/constants/config.js b/9router/src/shared/constants/config.js
new file mode 100644
index 00000000..53a3e53e
--- /dev/null
+++ b/9router/src/shared/constants/config.js
@@ -0,0 +1,67 @@
+import pkg from "../../../package.json" with { type: "json" };
+
+// App configuration
+export const APP_CONFIG = {
+ name: "Endpoint Proxy",
+ description: "AI Infrastructure Management",
+ version: pkg.version,
+};
+
+// Theme configuration
+export const THEME_CONFIG = {
+ storageKey: "theme",
+ defaultTheme: "system", // "light" | "dark" | "system"
+};
+
+// Subscription
+export const SUBSCRIPTION_CONFIG = {
+ price: 1.0,
+ currency: "USD",
+ interval: "month",
+ planName: "Pro Plan",
+};
+
+// API endpoints
+export const API_ENDPOINTS = {
+ users: "/api/users",
+ providers: "/api/providers",
+ payments: "/api/payments",
+ auth: "/api/auth",
+};
+
+export const CONSOLE_LOG_CONFIG = {
+ maxLines: 200,
+ pollIntervalMs: 1000,
+};
+
+// Provider API endpoints (for display only)
+export const PROVIDER_ENDPOINTS = {
+ openrouter: "https://openrouter.ai/api/v1/chat/completions",
+ glm: "https://api.z.ai/api/anthropic/v1/messages",
+ "glm-cn": "https://open.bigmodel.cn/api/coding/paas/v4/chat/completions",
+ kimi: "https://api.kimi.com/coding/v1/messages",
+ minimax: "https://api.minimax.io/anthropic/v1/messages",
+ "minimax-cn": "https://api.minimaxi.com/anthropic/v1/messages",
+ alicode: "https://coding.dashscope.aliyuncs.com/v1/chat/completions",
+ "alicode-intl": "https://coding-intl.dashscope.aliyuncs.com/v1/chat/completions",
+ openai: "https://api.openai.com/v1/chat/completions",
+ anthropic: "https://api.anthropic.com/v1/messages",
+ gemini: "https://generativelanguage.googleapis.com/v1beta/models",
+ ollama: "https://ollama.com/api/chat",
+ "ollama-local": "http://localhost:11434/api/chat",
+};
+
+// Re-export from providers.js for backward compatibility
+export {
+ FREE_PROVIDERS,
+ OAUTH_PROVIDERS,
+ APIKEY_PROVIDERS,
+ AI_PROVIDERS,
+ AUTH_METHODS,
+} from "./providers.js";
+
+// Re-export from models.js for backward compatibility
+export {
+ PROVIDER_MODELS,
+ AI_MODELS,
+} from "./models.js";
diff --git a/9router/src/shared/constants/index.js b/9router/src/shared/constants/index.js
new file mode 100644
index 00000000..820029c5
--- /dev/null
+++ b/9router/src/shared/constants/index.js
@@ -0,0 +1,4 @@
+// Shared Constants - Export all
+export * from "./colors";
+export * from "./config";
+
diff --git a/9router/src/shared/constants/models.js b/9router/src/shared/constants/models.js
new file mode 100644
index 00000000..4ccbf062
--- /dev/null
+++ b/9router/src/shared/constants/models.js
@@ -0,0 +1,35 @@
+// Import directly from file to avoid pulling in server-side dependencies via index.js
+export {
+ PROVIDER_MODELS,
+ getProviderModels,
+ getDefaultModel,
+ isValidModel as isValidModelCore,
+ findModelName,
+ getModelTargetFormat,
+ PROVIDER_ID_TO_ALIAS,
+ getModelsByProviderId
+} from "open-sse/config/providerModels.js";
+
+import { AI_PROVIDERS, isOpenAICompatibleProvider } from "./providers.js";
+import { PROVIDER_MODELS as MODELS } from "open-sse/config/providerModels.js";
+
+// Providers that accept any model (passthrough)
+const PASSTHROUGH_PROVIDERS = new Set(
+ Object.entries(AI_PROVIDERS)
+ .filter(([, p]) => p.passthroughModels)
+ .map(([key]) => key)
+);
+
+// Wrap isValidModel with passthrough providers
+export function isValidModel(aliasOrId, modelId) {
+ if (isOpenAICompatibleProvider(aliasOrId)) return true;
+ if (PASSTHROUGH_PROVIDERS.has(aliasOrId)) return true;
+ const models = MODELS[aliasOrId];
+ if (!models) return false;
+ return models.some(m => m.id === modelId);
+}
+
+// Legacy AI_MODELS for backward compatibility
+export const AI_MODELS = Object.entries(MODELS).flatMap(([alias, models]) =>
+ models.map(m => ({ provider: alias, model: m.id, name: m.name }))
+);
diff --git a/9router/src/shared/constants/pricing.js b/9router/src/shared/constants/pricing.js
new file mode 100644
index 00000000..f96038e0
--- /dev/null
+++ b/9router/src/shared/constants/pricing.js
@@ -0,0 +1,876 @@
+// Default pricing rates for AI models
+// All rates are in dollars per million tokens ($/1M tokens)
+// Based on user-provided pricing for Antigravity models and industry standards for others
+
+export const DEFAULT_PRICING = {
+ // OAuth Providers (using aliases)
+
+ // Claude Code (cc)
+ cc: {
+ "claude-opus-4-6": {
+ input: 5.00,
+ output: 25.00,
+ cached: 0.50,
+ reasoning: 25.00,
+ cache_creation: 6.25
+ },
+ "claude-opus-4-5-20251101": {
+ input: 5.00,
+ output: 25.00,
+ cached: 0.50,
+ reasoning: 25.00,
+ cache_creation: 6.25
+ },
+ "claude-sonnet-4-6": {
+ input: 3.00,
+ output: 15.00,
+ cached: 0.30,
+ reasoning: 15.00,
+ cache_creation: 3.75
+ },
+ "claude-sonnet-4-5-20250929": {
+ input: 3.00,
+ output: 15.00,
+ cached: 0.30,
+ reasoning: 15.00,
+ cache_creation: 3.75
+ },
+ "claude-haiku-4-5-20251001": {
+ input: 1.00,
+ output: 5.00,
+ cached: 0.10,
+ reasoning: 5.00,
+ cache_creation: 1.25
+ }
+ },
+
+ // OpenAI Codex (cx)
+ cx: {
+ "gpt-5.3-codex": {
+ input: 6.00,
+ output: 24.00,
+ cached: 3.00,
+ reasoning: 36.00,
+ cache_creation: 6.00
+ },
+ "gpt-5.3-codex-xhigh": {
+ input: 10.00,
+ output: 40.00,
+ cached: 5.00,
+ reasoning: 60.00,
+ cache_creation: 10.00
+ },
+ "gpt-5.3-codex-high": {
+ input: 8.00,
+ output: 32.00,
+ cached: 4.00,
+ reasoning: 48.00,
+ cache_creation: 8.00
+ },
+ "gpt-5.3-codex-low": {
+ input: 4.00,
+ output: 16.00,
+ cached: 2.00,
+ reasoning: 24.00,
+ cache_creation: 4.00
+ },
+ "gpt-5.3-codex-none": {
+ input: 3.00,
+ output: 12.00,
+ cached: 1.50,
+ reasoning: 18.00,
+ cache_creation: 3.00
+ },
+ "gpt-5.3-codex-spark": {
+ input: 3.00,
+ output: 12.00,
+ cached: 0.30,
+ reasoning: 12.00,
+ cache_creation: 3.00
+ },
+ "gpt-5.2-codex": {
+ input: 5.00,
+ output: 20.00,
+ cached: 2.50,
+ reasoning: 30.00,
+ cache_creation: 5.00
+ },
+ "gpt-5.2": {
+ input: 5.00,
+ output: 20.00,
+ cached: 2.50,
+ reasoning: 30.00,
+ cache_creation: 5.00
+ },
+ "gpt-5.1-codex-max": {
+ input: 8.00,
+ output: 32.00,
+ cached: 4.00,
+ reasoning: 48.00,
+ cache_creation: 8.00
+ },
+ "gpt-5.1-codex": {
+ input: 4.00,
+ output: 16.00,
+ cached: 2.00,
+ reasoning: 24.00,
+ cache_creation: 4.00
+ },
+ "gpt-5.1-codex-mini": {
+ input: 1.50,
+ output: 6.00,
+ cached: 0.75,
+ reasoning: 9.00,
+ cache_creation: 1.50
+ },
+ "gpt-5.1-codex-mini-high": {
+ input: 2.00,
+ output: 8.00,
+ cached: 1.00,
+ reasoning: 12.00,
+ cache_creation: 2.00
+ },
+ "gpt-5.1": {
+ input: 4.00,
+ output: 16.00,
+ cached: 2.00,
+ reasoning: 24.00,
+ cache_creation: 4.00
+ },
+ "gpt-5-codex": {
+ input: 3.00,
+ output: 12.00,
+ cached: 1.50,
+ reasoning: 18.00,
+ cache_creation: 3.00
+ },
+ "gpt-5-codex-mini": {
+ input: 1.00,
+ output: 4.00,
+ cached: 0.50,
+ reasoning: 6.00,
+ cache_creation: 1.00
+ }
+ },
+
+ // Gemini CLI (gc)
+ gc: {
+ "gemini-3-flash-preview": {
+ input: 0.50,
+ output: 3.00,
+ cached: 0.03,
+ reasoning: 4.50,
+ cache_creation: 0.50
+ },
+ "gemini-3-pro-preview": {
+ input: 2.00,
+ output: 12.00,
+ cached: 0.25,
+ reasoning: 18.00,
+ cache_creation: 2.00
+ },
+ "gemini-2.5-pro": {
+ input: 2.00,
+ output: 12.00,
+ cached: 0.25,
+ reasoning: 18.00,
+ cache_creation: 2.00
+ },
+ "gemini-2.5-flash": {
+ input: 0.30,
+ output: 2.50,
+ cached: 0.03,
+ reasoning: 3.75,
+ cache_creation: 0.30
+ },
+ "gemini-2.5-flash-lite": {
+ input: 0.15,
+ output: 1.25,
+ cached: 0.015,
+ reasoning: 1.875,
+ cache_creation: 0.15
+ }
+ },
+
+ // Qwen Code (qw)
+ qw: {
+ "qwen3-coder-plus": {
+ input: 1.00,
+ output: 4.00,
+ cached: 0.50,
+ reasoning: 6.00,
+ cache_creation: 1.00
+ },
+ "qwen3-coder-flash": {
+ input: 0.50,
+ output: 2.00,
+ cached: 0.25,
+ reasoning: 3.00,
+ cache_creation: 0.50
+ },
+ "vision-model": {
+ input: 1.50,
+ output: 6.00,
+ cached: 0.75,
+ reasoning: 9.00,
+ cache_creation: 1.50
+ },
+ "coder-model": {
+ input: 1.50,
+ output: 6.00,
+ cached: 0.75,
+ reasoning: 9.00,
+ cache_creation: 1.50
+ }
+ },
+
+ // iFlow AI (if)
+ if: {
+ "qwen3-coder-plus": {
+ input: 1.00,
+ output: 4.00,
+ cached: 0.50,
+ reasoning: 6.00,
+ cache_creation: 1.00
+ },
+ "kimi-k2": {
+ input: 1.00,
+ output: 4.00,
+ cached: 0.50,
+ reasoning: 6.00,
+ cache_creation: 1.00
+ },
+ "kimi-k2-thinking": {
+ input: 1.50,
+ output: 6.00,
+ cached: 0.75,
+ reasoning: 9.00,
+ cache_creation: 1.50
+ },
+ "kimi-k2.5": {
+ input: 1.20,
+ output: 4.80,
+ cached: 0.60,
+ reasoning: 7.20,
+ cache_creation: 1.20
+ },
+ "deepseek-r1": {
+ input: 0.75,
+ output: 3.00,
+ cached: 0.375,
+ reasoning: 4.50,
+ cache_creation: 0.75
+ },
+ "deepseek-v3.2-chat": {
+ input: 0.50,
+ output: 2.00,
+ cached: 0.25,
+ reasoning: 3.00,
+ cache_creation: 0.50
+ },
+ "deepseek-v3.2-reasoner": {
+ input: 0.75,
+ output: 3.00,
+ cached: 0.375,
+ reasoning: 4.50,
+ cache_creation: 0.75
+ },
+ "minimax-m2.1": {
+ input: 0.50,
+ output: 2.00,
+ cached: 0.25,
+ reasoning: 3.00,
+ cache_creation: 0.50
+ },
+ "minimax-m2.5": {
+ input: 0.60,
+ output: 2.40,
+ cached: 0.30,
+ reasoning: 3.60,
+ cache_creation: 0.60
+ },
+ "glm-4.6": {
+ input: 0.50,
+ output: 2.00,
+ cached: 0.25,
+ reasoning: 3.00,
+ cache_creation: 0.50
+ },
+ "glm-4.7": {
+ input: 0.75,
+ output: 3.00,
+ cached: 0.375,
+ reasoning: 4.50,
+ cache_creation: 0.75
+ },
+ "glm-5": {
+ input: 1.00,
+ output: 4.00,
+ cached: 0.50,
+ reasoning: 6.00,
+ cache_creation: 1.00
+ }
+ },
+
+ // Antigravity (ag) - User-provided pricing
+ ag: {
+ "gemini-3.1-pro-low": {
+ input: 2.00,
+ output: 12.00,
+ cached: 0.25,
+ reasoning: 18.00,
+ cache_creation: 2.00
+ },
+ "gemini-3.1-pro-high": {
+ input: 4.00,
+ output: 18.00,
+ cached: 0.50,
+ reasoning: 27.00,
+ cache_creation: 4.00
+ },
+ "gemini-3-flash": {
+ input: 0.50,
+ output: 3.00,
+ cached: 0.03,
+ reasoning: 4.50,
+ cache_creation: 0.50
+ },
+ "gemini-2.5-flash": {
+ input: 0.30,
+ output: 2.50,
+ cached: 0.03,
+ reasoning: 3.75,
+ cache_creation: 0.30
+ },
+ "claude-sonnet-4-6": {
+ input: 3.00,
+ output: 15.00,
+ cached: 0.30,
+ reasoning: 22.50,
+ cache_creation: 3.00
+ },
+ "claude-opus-4-5-thinking": {
+ input: 5.00,
+ output: 25.00,
+ cached: 0.50,
+ reasoning: 37.50,
+ cache_creation: 5.00
+ },
+ "claude-opus-4-6-thinking": {
+ input: 5.00,
+ output: 25.00,
+ cached: 0.50,
+ reasoning: 37.50,
+ cache_creation: 5.00
+ },
+ "gpt-oss-120b-medium": {
+ input: 0.50,
+ output: 2.00,
+ cached: 0.25,
+ reasoning: 3.00,
+ cache_creation: 0.50
+ }
+ },
+
+ // GitHub Copilot (gh)
+ gh: {
+ "gpt-3.5-turbo": {
+ input: 0.50,
+ output: 1.50,
+ cached: 0.25,
+ reasoning: 2.25,
+ cache_creation: 0.50
+ },
+ "gpt-4": {
+ input: 2.50,
+ output: 10.00,
+ cached: 1.25,
+ reasoning: 15.00,
+ cache_creation: 2.50
+ },
+ "gpt-4o": {
+ input: 2.50,
+ output: 10.00,
+ cached: 1.25,
+ reasoning: 15.00,
+ cache_creation: 2.50
+ },
+ "gpt-4o-mini": {
+ input: 0.15,
+ output: 0.60,
+ cached: 0.075,
+ reasoning: 0.90,
+ cache_creation: 0.15
+ },
+ "gpt-4.1": {
+ input: 2.50,
+ output: 10.00,
+ cached: 1.25,
+ reasoning: 15.00,
+ cache_creation: 2.50
+ },
+ "gpt-5": {
+ input: 3.00,
+ output: 12.00,
+ cached: 1.50,
+ reasoning: 18.00,
+ cache_creation: 3.00
+ },
+ "gpt-5-mini": {
+ input: 0.75,
+ output: 3.00,
+ cached: 0.375,
+ reasoning: 4.50,
+ cache_creation: 0.75
+ },
+ "gpt-5-codex": {
+ input: 3.00,
+ output: 12.00,
+ cached: 1.50,
+ reasoning: 18.00,
+ cache_creation: 3.00
+ },
+ "gpt-5.1": {
+ input: 4.00,
+ output: 16.00,
+ cached: 2.00,
+ reasoning: 24.00,
+ cache_creation: 4.00
+ },
+ "gpt-5.1-codex": {
+ input: 4.00,
+ output: 16.00,
+ cached: 2.00,
+ reasoning: 24.00,
+ cache_creation: 4.00
+ },
+ "gpt-5.1-codex-mini": {
+ input: 1.50,
+ output: 6.00,
+ cached: 0.75,
+ reasoning: 9.00,
+ cache_creation: 1.50
+ },
+ "gpt-5.1-codex-max": {
+ input: 8.00,
+ output: 32.00,
+ cached: 4.00,
+ reasoning: 48.00,
+ cache_creation: 8.00
+ },
+ "gpt-5.2": {
+ input: 5.00,
+ output: 20.00,
+ cached: 2.50,
+ reasoning: 30.00,
+ cache_creation: 5.00
+ },
+ "gpt-5.2-codex": {
+ input: 5.00,
+ output: 20.00,
+ cached: 2.50,
+ reasoning: 30.00,
+ cache_creation: 5.00
+ },
+ "gpt-5.3-codex": {
+ input: 1.75,
+ output: 14.00,
+ cached: 0.175,
+ reasoning: 14.00,
+ cache_creation: 1.75
+ },
+ "claude-haiku-4.5": {
+ input: 0.50,
+ output: 2.50,
+ cached: 0.05,
+ reasoning: 3.75,
+ cache_creation: 0.50
+ },
+ "claude-opus-4.1": {
+ input: 5.00,
+ output: 25.00,
+ cached: 0.50,
+ reasoning: 37.50,
+ cache_creation: 5.00
+ },
+ "claude-opus-4.5": {
+ input: 5.00,
+ output: 25.00,
+ cached: 0.50,
+ reasoning: 37.50,
+ cache_creation: 5.00
+ },
+ "claude-sonnet-4": {
+ input: 3.00,
+ output: 15.00,
+ cached: 0.30,
+ reasoning: 22.50,
+ cache_creation: 3.00
+ },
+ "claude-sonnet-4.5": {
+ input: 3.00,
+ output: 15.00,
+ cached: 0.30,
+ reasoning: 22.50,
+ cache_creation: 3.00
+ },
+ "claude-sonnet-4.6": {
+ input: 3.00,
+ output: 15.00,
+ cached: 0.30,
+ reasoning: 22.50,
+ cache_creation: 3.00
+ },
+ "claude-opus-4.6": {
+ input: 5.00,
+ output: 25.00,
+ cached: 0.50,
+ reasoning: 37.50,
+ cache_creation: 5.00
+ },
+ "gemini-2.5-pro": {
+ input: 2.00,
+ output: 12.00,
+ cached: 0.25,
+ reasoning: 18.00,
+ cache_creation: 2.00
+ },
+ "gemini-3-flash-preview": {
+ input: 0.50,
+ output: 3.00,
+ cached: 0.03,
+ reasoning: 4.50,
+ cache_creation: 0.50
+ },
+ "gemini-3-pro-preview": {
+ input: 2.00,
+ output: 12.00,
+ cached: 0.25,
+ reasoning: 18.00,
+ cache_creation: 2.00
+ },
+ "grok-code-fast-1": {
+ input: 0.50,
+ output: 2.00,
+ cached: 0.25,
+ reasoning: 3.00,
+ cache_creation: 0.50
+ },
+ "oswe-vscode-prime": {
+ input: 1.00,
+ output: 4.00,
+ cached: 0.50,
+ reasoning: 6.00,
+ cache_creation: 1.00
+ }
+ },
+
+ // Kiro AI (kr) - AWS CodeWhisperer
+ kr: {
+ "claude-sonnet-4.5": {
+ input: 3.00,
+ output: 15.00,
+ cached: 0.30,
+ reasoning: 22.50,
+ cache_creation: 3.00
+ },
+ "claude-haiku-4.5": {
+ input: 0.50,
+ output: 2.50,
+ cached: 0.05,
+ reasoning: 3.75,
+ cache_creation: 0.50
+ }
+ },
+
+ // API Key Providers (alias = id)
+
+ // OpenAI
+ openai: {
+ "gpt-4o": {
+ input: 2.50,
+ output: 10.00,
+ cached: 1.25,
+ reasoning: 15.00,
+ cache_creation: 2.50
+ },
+ "gpt-4o-mini": {
+ input: 0.15,
+ output: 0.60,
+ cached: 0.075,
+ reasoning: 0.90,
+ cache_creation: 0.15
+ },
+ "gpt-4-turbo": {
+ input: 10.00,
+ output: 30.00,
+ cached: 5.00,
+ reasoning: 45.00,
+ cache_creation: 10.00
+ },
+ "o1": {
+ input: 15.00,
+ output: 60.00,
+ cached: 7.50,
+ reasoning: 90.00,
+ cache_creation: 15.00
+ },
+ "o1-mini": {
+ input: 3.00,
+ output: 12.00,
+ cached: 1.50,
+ reasoning: 18.00,
+ cache_creation: 3.00
+ }
+ },
+
+ // Anthropic
+ anthropic: {
+ "claude-sonnet-4-20250514": {
+ input: 3.00,
+ output: 15.00,
+ cached: 1.50,
+ reasoning: 15.00,
+ cache_creation: 3.00
+ },
+ "claude-opus-4-20250514": {
+ input: 15.00,
+ output: 25.00,
+ cached: 7.50,
+ reasoning: 112.50,
+ cache_creation: 15.00
+ },
+ "claude-3-5-sonnet-20241022": {
+ input: 3.00,
+ output: 15.00,
+ cached: 1.50,
+ reasoning: 15.00,
+ cache_creation: 3.00
+ }
+ },
+
+ // Gemini
+ gemini: {
+ "gemini-3-pro-preview": {
+ input: 2.00,
+ output: 12.00,
+ cached: 0.25,
+ reasoning: 18.00,
+ cache_creation: 2.00
+ },
+ "gemini-2.5-pro": {
+ input: 2.00,
+ output: 12.00,
+ cached: 0.25,
+ reasoning: 18.00,
+ cache_creation: 2.00
+ },
+ "gemini-2.5-flash": {
+ input: 0.30,
+ output: 2.50,
+ cached: 0.03,
+ reasoning: 3.75,
+ cache_creation: 0.30
+ },
+ "gemini-2.5-flash-lite": {
+ input: 0.15,
+ output: 1.25,
+ cached: 0.015,
+ reasoning: 1.875,
+ cache_creation: 0.15
+ }
+ },
+
+ // OpenRouter
+ openrouter: {
+ "auto": {
+ input: 2.00,
+ output: 8.00,
+ cached: 1.00,
+ reasoning: 12.00,
+ cache_creation: 2.00
+ }
+ },
+
+ // GLM
+ glm: {
+ "glm-4.7": {
+ input: 0.75,
+ output: 3.00,
+ cached: 0.375,
+ reasoning: 4.50,
+ cache_creation: 0.75
+ },
+ "glm-4.6": {
+ input: 0.50,
+ output: 2.00,
+ cached: 0.25,
+ reasoning: 3.00,
+ cache_creation: 0.50
+ },
+ "glm-4.6v": {
+ input: 0.75,
+ output: 3.00,
+ cached: 0.375,
+ reasoning: 4.50,
+ cache_creation: 0.75
+ }
+ },
+
+ // Kimi
+ kimi: {
+ "kimi-k2.5": {
+ input: 1.20,
+ output: 4.80,
+ cached: 0.60,
+ reasoning: 7.20,
+ cache_creation: 1.20
+ },
+ "kimi-k2.5-thinking": {
+ input: 1.80,
+ output: 7.20,
+ cached: 0.90,
+ reasoning: 10.80,
+ cache_creation: 1.80
+ },
+ "kimi-latest": {
+ input: 1.00,
+ output: 4.00,
+ cached: 0.50,
+ reasoning: 6.00,
+ cache_creation: 1.00
+ }
+ },
+
+ // MiniMax
+ minimax: {
+ "MiniMax-M2.7": {
+ input: 0.50,
+ output: 2.00,
+ cached: 0.25,
+ reasoning: 3.00,
+ cache_creation: 0.50
+ },
+ "MiniMax-M2.5": {
+ input: 0.50,
+ output: 2.00,
+ cached: 0.25,
+ reasoning: 3.00,
+ cache_creation: 0.50
+ },
+ "MiniMax-M2.1": {
+ input: 0.50,
+ output: 2.00,
+ cached: 0.25,
+ reasoning: 3.00,
+ cache_creation: 0.50
+ }
+ },
+
+ // DeepSeek (official API pricing: https://api-docs.deepseek.com/quick_start/pricing)
+ deepseek: {
+ "deepseek-chat": {
+ input: 0.28,
+ output: 0.42,
+ cached: 0.028,
+ reasoning: 0.42,
+ cache_creation: 0.28
+ },
+ "deepseek-reasoner": {
+ input: 0.28,
+ output: 0.42,
+ cached: 0.028,
+ reasoning: 0.42,
+ cache_creation: 0.28
+ }
+ }
+};
+
+/**
+ * Provider-agnostic fallback pricing, keyed by model base name.
+ * Used when provider-specific lookup fails (e.g. openrouter, cu, fireworks use prefixed model IDs).
+ * Strip prefix before lookup: "deepseek/deepseek-chat" → "deepseek-chat"
+ */
+export const MODEL_PRICING = {
+ // DeepSeek
+ "deepseek-chat": { input: 0.28, output: 0.42, cached: 0.028, reasoning: 0.42, cache_creation: 0.28 },
+ "deepseek-reasoner": { input: 0.28, output: 0.42, cached: 0.028, reasoning: 0.42, cache_creation: 0.28 },
+};
+
+/**
+ * Get pricing for a specific provider and model
+ * @param {string} provider - Provider ID (e.g., "openai", "cc", "gc")
+ * @param {string} model - Model ID
+ * @returns {object|null} Pricing object or null if not found
+ */
+export function getPricingForModel(provider, model) {
+ if (!provider || !model) return null;
+
+ const providerPricing = DEFAULT_PRICING[provider];
+ if (!providerPricing) return null;
+
+ return providerPricing[model] || null;
+}
+
+/**
+ * Get all pricing data
+ * @returns {object} All default pricing
+ */
+export function getDefaultPricing() {
+ return DEFAULT_PRICING;
+}
+
+/**
+ * Format cost for display
+ * @param {number} cost - Cost in dollars
+ * @returns {string} Formatted cost string
+ */
+export function formatCost(cost) {
+ if (cost === null || cost === undefined || isNaN(cost)) return "$0.00";
+ return `$${cost.toFixed(2)}`;
+}
+
+/**
+ * Calculate cost from tokens and pricing
+ * @param {object} tokens - Token counts
+ * @param {object} pricing - Pricing object
+ * @returns {number} Cost in dollars
+ */
+export function calculateCostFromTokens(tokens, pricing) {
+ if (!tokens || !pricing) return 0;
+
+ let cost = 0;
+
+ // Input tokens (non-cached)
+ const inputTokens = tokens.prompt_tokens || tokens.input_tokens || 0;
+ const cachedTokens = tokens.cached_tokens || tokens.cache_read_input_tokens || 0;
+ const nonCachedInput = Math.max(0, inputTokens - cachedTokens);
+
+ cost += (nonCachedInput * (pricing.input / 1000000));
+
+ // Cached tokens
+ if (cachedTokens > 0) {
+ const cachedRate = pricing.cached || pricing.input; // Fallback to input rate
+ cost += (cachedTokens * (cachedRate / 1000000));
+ }
+
+ // Output tokens
+ const outputTokens = tokens.completion_tokens || tokens.output_tokens || 0;
+ cost += (outputTokens * (pricing.output / 1000000));
+
+ // Reasoning tokens
+ const reasoningTokens = tokens.reasoning_tokens || 0;
+ if (reasoningTokens > 0) {
+ const reasoningRate = pricing.reasoning || pricing.output; // Fallback to output rate
+ cost += (reasoningTokens * (reasoningRate / 1000000));
+ }
+
+ // Cache creation tokens
+ const cacheCreationTokens = tokens.cache_creation_input_tokens || 0;
+ if (cacheCreationTokens > 0) {
+ const cacheCreationRate = pricing.cache_creation || pricing.input; // Fallback to input rate
+ cost += (cacheCreationTokens * (cacheCreationRate / 1000000));
+ }
+
+ return cost;
+}
diff --git a/9router/src/shared/constants/providers.js b/9router/src/shared/constants/providers.js
new file mode 100644
index 00000000..f1dc0eef
--- /dev/null
+++ b/9router/src/shared/constants/providers.js
@@ -0,0 +1,120 @@
+// Provider definitions
+
+// Free Providers
+export const FREE_PROVIDERS = {
+ iflow: { id: "iflow", alias: "if", name: "iFlow AI", icon: "water_drop", color: "#6366F1" },
+ qwen: { id: "qwen", alias: "qw", name: "Qwen Code", icon: "psychology", color: "#10B981" },
+ "gemini-cli": { id: "gemini-cli", alias: "gc", name: "Gemini CLI", icon: "terminal", color: "#4285F4" },
+ kiro: { id: "kiro", alias: "kr", name: "Kiro AI", icon: "psychology_alt", color: "#FF6B35" },
+};
+
+// OAuth Providers
+export const OAUTH_PROVIDERS = {
+ claude: { id: "claude", alias: "cc", name: "Claude Code", icon: "smart_toy", color: "#D97757" },
+ antigravity: { id: "antigravity", alias: "ag", name: "Antigravity", icon: "rocket_launch", color: "#F59E0B" },
+ codex: { id: "codex", alias: "cx", name: "OpenAI Codex", icon: "code", color: "#3B82F6" },
+ github: { id: "github", alias: "gh", name: "GitHub Copilot", icon: "code", color: "#333333" },
+ cursor: { id: "cursor", alias: "cu", name: "Cursor IDE", icon: "edit_note", color: "#00D4AA" },
+ // "kimi-coding": { id: "kimi-coding", alias: "kmc", name: "Kimi Coding", icon: "psychology", color: "#1E40AF", textIcon: "KC" },
+ kilocode: { id: "kilocode", alias: "kc", name: "Kilo Code", icon: "code", color: "#FF6B35", textIcon: "KC" },
+ cline: { id: "cline", alias: "cl", name: "Cline", icon: "smart_toy", color: "#5B9BD5", textIcon: "CL" },
+};
+
+export const APIKEY_PROVIDERS = {
+ openrouter: { id: "openrouter", alias: "openrouter", name: "OpenRouter", icon: "router", color: "#F97316", textIcon: "OR", passthroughModels: true, website: "https://openrouter.ai" },
+ glm: { id: "glm", alias: "glm", name: "GLM Coding", icon: "code", color: "#2563EB", textIcon: "GL", website: "https://open.bigmodel.cn" },
+ "glm-cn": { id: "glm-cn", alias: "glm-cn", name: "GLM (China)", icon: "code", color: "#DC2626", textIcon: "GC", website: "https://open.bigmodel.cn" },
+ kimi: { id: "kimi", alias: "kimi", name: "Kimi", icon: "psychology", color: "#1E3A8A", textIcon: "KM", website: "https://kimi.moonshot.cn" },
+ minimax: { id: "minimax", alias: "minimax", name: "Minimax Coding", icon: "memory", color: "#7C3AED", textIcon: "MM", website: "https://www.minimaxi.com" },
+ "minimax-cn": { id: "minimax-cn", alias: "minimax-cn", name: "Minimax (China)", icon: "memory", color: "#DC2626", textIcon: "MC", website: "https://www.minimaxi.com" },
+ alicode: { id: "alicode", alias: "alicode", name: "Alibaba", icon: "cloud", color: "#FF6A00", textIcon: "ALi" },
+ "alicode-intl": { id: "alicode-intl", alias: "alicode-intl", name: "Alibaba Intl", icon: "cloud", color: "#FF6A00", textIcon: "ALi" },
+ openai: { id: "openai", alias: "openai", name: "OpenAI", icon: "auto_awesome", color: "#10A37F", textIcon: "OA", website: "https://platform.openai.com" },
+ anthropic: { id: "anthropic", alias: "anthropic", name: "Anthropic", icon: "smart_toy", color: "#D97757", textIcon: "AN", website: "https://console.anthropic.com" },
+ gemini: { id: "gemini", alias: "gemini", name: "Gemini", icon: "diamond", color: "#4285F4", textIcon: "GE", website: "https://ai.google.dev" },
+ deepseek: { id: "deepseek", alias: "ds", name: "DeepSeek", icon: "bolt", color: "#4D6BFE", textIcon: "DS", website: "https://deepseek.com" },
+ groq: { id: "groq", alias: "groq", name: "Groq", icon: "speed", color: "#F55036", textIcon: "GQ", website: "https://groq.com" },
+ xai: { id: "xai", alias: "xai", name: "xAI (Grok)", icon: "auto_awesome", color: "#1DA1F2", textIcon: "XA", website: "https://x.ai" },
+ mistral: { id: "mistral", alias: "mistral", name: "Mistral", icon: "air", color: "#FF7000", textIcon: "MI", website: "https://mistral.ai" },
+ perplexity: { id: "perplexity", alias: "pplx", name: "Perplexity", icon: "search", color: "#20808D", textIcon: "PP", website: "https://www.perplexity.ai" },
+ together: { id: "together", alias: "together", name: "Together AI", icon: "group_work", color: "#0F6FFF", textIcon: "TG", website: "https://www.together.ai" },
+ fireworks: { id: "fireworks", alias: "fireworks", name: "Fireworks AI", icon: "local_fire_department", color: "#7B2EF2", textIcon: "FW", website: "https://fireworks.ai" },
+ cerebras: { id: "cerebras", alias: "cerebras", name: "Cerebras", icon: "memory", color: "#FF4F00", textIcon: "CB", website: "https://www.cerebras.ai" },
+ cohere: { id: "cohere", alias: "cohere", name: "Cohere", icon: "hub", color: "#39594D", textIcon: "CO", website: "https://cohere.com" },
+ nvidia: { id: "nvidia", alias: "nvidia", name: "NVIDIA NIM", icon: "developer_board", color: "#76B900", textIcon: "NV", website: "https://developer.nvidia.com/nim" },
+ nebius: { id: "nebius", alias: "nebius", name: "Nebius AI", icon: "cloud", color: "#6C5CE7", textIcon: "NB", website: "https://nebius.com" },
+ siliconflow: { id: "siliconflow", alias: "siliconflow", name: "SiliconFlow", icon: "cloud_queue", color: "#5B6EF5", textIcon: "SF", website: "https://cloud.siliconflow.com" },
+ hyperbolic: { id: "hyperbolic", alias: "hyp", name: "Hyperbolic", icon: "bolt", color: "#00D4FF", textIcon: "HY", website: "https://hyperbolic.xyz" },
+ deepgram: { id: "deepgram", alias: "dg", name: "Deepgram", icon: "mic", color: "#13EF93", textIcon: "DG", website: "https://deepgram.com" },
+ assemblyai: { id: "assemblyai", alias: "aai", name: "AssemblyAI", icon: "record_voice_over", color: "#0062FF", textIcon: "AA", website: "https://assemblyai.com" },
+ nanobanana: { id: "nanobanana", alias: "nb", name: "NanoBanana", icon: "image", color: "#FFD700", textIcon: "NB", website: "https://nanobananaapi.ai" },
+ chutes: { id: "chutes", alias: "ch", name: "Chutes AI", icon: "water_drop", color: "#ffffffff", textIcon: "CH", website: "https://chutes.ai" },
+ ollama: { id: "ollama", alias: "ollama", name: "Ollama Cloud", icon: "cloud", color: "#ffffffff", textIcon: "OL", website: "https://ollama.com" },
+ "ollama-local": { id: "ollama-local", alias: "ollama-local", name: "Ollama Local", icon: "cloud", color: "#ffffffff", textIcon: "OL", website: "https://ollama.com" },
+ vertex: { id: "vertex", alias: "vx", name: "Vertex AI", icon: "cloud", color: "#4285F4", textIcon: "VX", website: "https://cloud.google.com/vertex-ai" },
+ "vertex-partner": { id: "vertex-partner", alias: "vxp", name: "Vertex Partner", icon: "cloud", color: "#34A853", textIcon: "VP", website: "https://cloud.google.com/vertex-ai/generative-ai/docs/partner-models/use-partner-models" },
+};
+
+export const OPENAI_COMPATIBLE_PREFIX = "openai-compatible-";
+export const ANTHROPIC_COMPATIBLE_PREFIX = "anthropic-compatible-";
+
+export function isOpenAICompatibleProvider(providerId) {
+ return typeof providerId === "string" && providerId.startsWith(OPENAI_COMPATIBLE_PREFIX);
+}
+
+export function isAnthropicCompatibleProvider(providerId) {
+ return typeof providerId === "string" && providerId.startsWith(ANTHROPIC_COMPATIBLE_PREFIX);
+}
+
+// All providers (combined)
+export const AI_PROVIDERS = { ...FREE_PROVIDERS, ...OAUTH_PROVIDERS, ...APIKEY_PROVIDERS };
+
+// Auth methods
+export const AUTH_METHODS = {
+ oauth: { id: "oauth", name: "OAuth", icon: "lock" },
+ apikey: { id: "apikey", name: "API Key", icon: "key" },
+};
+
+// Helper: Get provider by alias
+export function getProviderByAlias(alias) {
+ for (const provider of Object.values(AI_PROVIDERS)) {
+ if (provider.alias === alias || provider.id === alias) {
+ return provider;
+ }
+ }
+ return null;
+}
+
+// Helper: Get provider ID from alias
+export function resolveProviderId(aliasOrId) {
+ const provider = getProviderByAlias(aliasOrId);
+ return provider?.id || aliasOrId;
+}
+
+// Helper: Get alias from provider ID
+export function getProviderAlias(providerId) {
+ const provider = AI_PROVIDERS[providerId];
+ return provider?.alias || providerId;
+}
+
+// Alias to ID mapping (for quick lookup)
+export const ALIAS_TO_ID = Object.values(AI_PROVIDERS).reduce((acc, p) => {
+ acc[p.alias] = p.id;
+ return acc;
+}, {});
+
+// ID to Alias mapping
+export const ID_TO_ALIAS = Object.values(AI_PROVIDERS).reduce((acc, p) => {
+ acc[p.id] = p.alias;
+ return acc;
+}, {});
+
+// Providers that support usage/quota API
+export const USAGE_SUPPORTED_PROVIDERS = [
+ "claude",
+ "antigravity",
+ "kiro",
+ "github",
+ "codex",
+ "kimi-coding",
+];
diff --git a/9router/src/shared/hooks/index.js b/9router/src/shared/hooks/index.js
new file mode 100644
index 00000000..68c81c2d
--- /dev/null
+++ b/9router/src/shared/hooks/index.js
@@ -0,0 +1,2 @@
+// Shared Hooks - Export all
+export { useTheme } from "./useTheme";
diff --git a/9router/src/shared/hooks/useCopyToClipboard.js b/9router/src/shared/hooks/useCopyToClipboard.js
new file mode 100644
index 00000000..558aad89
--- /dev/null
+++ b/9router/src/shared/hooks/useCopyToClipboard.js
@@ -0,0 +1,29 @@
+"use client";
+
+import { useState, useCallback, useRef } from "react";
+
+/**
+ * Hook for copy to clipboard with feedback
+ * @param {number} resetDelay - Time in ms before resetting copied state (default: 2000)
+ * @returns {{ copied: string|null, copy: (text: string, id?: string) => void }}
+ */
+export function useCopyToClipboard(resetDelay = 2000) {
+ const [copied, setCopied] = useState(null);
+ const timeoutRef = useRef(null);
+
+ const copy = useCallback((text, id = "default") => {
+ navigator.clipboard.writeText(text);
+ setCopied(id);
+
+ if (timeoutRef.current) {
+ clearTimeout(timeoutRef.current);
+ }
+
+ timeoutRef.current = setTimeout(() => {
+ setCopied(null);
+ }, resetDelay);
+ }, [resetDelay]);
+
+ return { copied, copy };
+}
+
diff --git a/9router/src/shared/hooks/useTheme.js b/9router/src/shared/hooks/useTheme.js
new file mode 100644
index 00000000..45dae992
--- /dev/null
+++ b/9router/src/shared/hooks/useTheme.js
@@ -0,0 +1,60 @@
+"use client";
+
+import { useEffect, useState, useSyncExternalStore } from "react";
+import useThemeStore from "@/store/themeStore";
+
+// Subscribe to system theme changes
+function subscribeToSystemTheme(callback) {
+ if (typeof window === "undefined") return () => {};
+ const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
+ mediaQuery.addEventListener("change", callback);
+ return () => mediaQuery.removeEventListener("change", callback);
+}
+
+// Get current system theme preference
+function getSystemThemeSnapshot() {
+ if (typeof window === "undefined") return false;
+ return window.matchMedia("(prefers-color-scheme: dark)").matches;
+}
+
+// Server snapshot always returns false
+function getServerSnapshot() {
+ return false;
+}
+
+export function useTheme() {
+ const { theme, setTheme, toggleTheme, initTheme } = useThemeStore();
+
+ // Use useSyncExternalStore to safely subscribe to system theme
+ const systemPrefersDark = useSyncExternalStore(
+ subscribeToSystemTheme,
+ getSystemThemeSnapshot,
+ getServerSnapshot
+ );
+
+ useEffect(() => {
+ initTheme();
+ }, [initTheme]);
+
+ // Listen for system theme changes when theme is "system"
+ useEffect(() => {
+ if (theme !== "system") return;
+
+ const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
+ const handleChange = () => initTheme();
+
+ mediaQuery.addEventListener("change", handleChange);
+ return () => mediaQuery.removeEventListener("change", handleChange);
+ }, [theme, initTheme]);
+
+ // Compute isDark from current state (no effect needed)
+ const isDark = theme === "dark" || (theme === "system" && systemPrefersDark);
+
+ return {
+ theme,
+ setTheme,
+ toggleTheme,
+ isDark,
+ };
+}
+
diff --git a/9router/src/shared/services/cloudSyncScheduler.js b/9router/src/shared/services/cloudSyncScheduler.js
new file mode 100644
index 00000000..44de2489
--- /dev/null
+++ b/9router/src/shared/services/cloudSyncScheduler.js
@@ -0,0 +1,122 @@
+import { getConsistentMachineId } from "@/shared/utils/machineId";
+import { isCloudEnabled } from "@/lib/localDb";
+
+const INTERNAL_BASE_URL =
+ process.env.BASE_URL ||
+ process.env.NEXT_PUBLIC_BASE_URL ||
+ "http://localhost:20128";
+
+/**
+ * Cloud sync scheduler
+ */
+export class CloudSyncScheduler {
+ constructor(machineId = null, intervalMinutes = 15) {
+ this.machineId = machineId;
+ this.intervalMinutes = intervalMinutes;
+ this.intervalId = null;
+ }
+
+ /**
+ * Initialize machine ID if not provided
+ */
+ async initializeMachineId() {
+ if (!this.machineId) {
+ this.machineId = await getConsistentMachineId();
+ }
+ }
+
+ /**
+ * Start periodic sync (delays first sync to allow server to be ready)
+ */
+ async start() {
+ if (this.intervalId) {
+ return;
+ }
+
+ await this.initializeMachineId();
+
+ // Delay first sync by 30 seconds to ensure server is ready
+ setTimeout(() => {
+ this.syncWithRetry().catch(() => {});
+ }, 30000);
+
+ // Then sync periodically
+ this.intervalId = setInterval(() => {
+ this.syncWithRetry().catch(() => {});
+ }, this.intervalMinutes * 60 * 1000);
+ }
+
+ /**
+ * Stop periodic sync
+ */
+ stop() {
+ if (this.intervalId) {
+ clearInterval(this.intervalId);
+ this.intervalId = null;
+ }
+ }
+
+ /**
+ * Sync with retry logic (exponential backoff)
+ */
+ async syncWithRetry(maxRetries = 1) {
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
+ try {
+ const result = await this.sync();
+ return result;
+ } catch (error) {
+ if (attempt === maxRetries) {
+ return null;
+ }
+
+ const delay = Math.min(1000 * Math.pow(2, attempt), 10000); // Max 10s
+ await new Promise(resolve => setTimeout(resolve, delay));
+ }
+ }
+ }
+
+ /**
+ * Perform sync via internal API route (handles token update to db.json)
+ */
+ async sync() {
+ // Check if cloud is enabled
+ const enabled = await isCloudEnabled();
+ if (!enabled) {
+ return null;
+ }
+
+ await this.initializeMachineId();
+
+ // Call internal API route which handles both sync and token update
+ const response = await fetch(`${INTERNAL_BASE_URL}/api/sync/cloud`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ machineId: this.machineId, action: "sync" })
+ });
+
+ if (!response.ok) {
+ const errorData = await response.json().catch(() => ({}));
+ throw new Error(errorData.error || "Sync failed");
+ }
+
+ const result = await response.json();
+ return result;
+ }
+
+ /**
+ * Check if scheduler is running
+ */
+ isRunning() {
+ return this.intervalId !== null;
+ }
+}
+
+// Export a singleton instance if needed
+let cloudSyncScheduler = null;
+
+export async function getCloudSyncScheduler(machineId = null, intervalMinutes = 15) {
+ if (!cloudSyncScheduler) {
+ cloudSyncScheduler = new CloudSyncScheduler(machineId, intervalMinutes);
+ }
+ return cloudSyncScheduler;
+}
diff --git a/9router/src/shared/services/initializeApp.js b/9router/src/shared/services/initializeApp.js
new file mode 100644
index 00000000..42c665fa
--- /dev/null
+++ b/9router/src/shared/services/initializeApp.js
@@ -0,0 +1,228 @@
+import { cleanupProviderConnections, getSettings, updateSettings, getApiKeys } from "@/lib/localDb";
+import { enableTunnel, isTunnelManuallyDisabled, isTunnelReconnecting } from "@/lib/tunnel/tunnelManager";
+import { killCloudflared, isCloudflaredRunning, ensureCloudflared } from "@/lib/tunnel/cloudflared";
+import { getMitmStatus, startMitm, loadEncryptedPassword, initDbHooks } from "@/mitm/manager";
+import { fileURLToPath } from "url";
+import { dirname, join } from "path";
+import { existsSync } from "fs";
+
+import os from "os";
+
+// Inject correct paths and DB hooks into manager.js (CJS) from ESM context.
+// Must run before any MITM function is called.
+(function bootstrapMitm() {
+ // 1. Resolve server.js path from real ESM __filename (not bundled path)
+ if (!process.env.MITM_SERVER_PATH) {
+ try {
+ const thisFile = fileURLToPath(import.meta.url);
+ const appSrc = dirname(dirname(thisFile)); // src/
+ const candidate = join(appSrc, "mitm", "server.js");
+ if (existsSync(candidate)) {
+ process.env.MITM_SERVER_PATH = candidate;
+ }
+ } catch { /* ignore */ }
+ }
+
+ // 2. Inject DB functions so manager.js (CJS) can save/load settings
+ // without dynamic import issues inside webpack bundles
+ try {
+ initDbHooks(getSettings, updateSettings);
+ } catch { /* ignore */ }
+})();
+
+// Multiple modules register SIGINT/SIGTERM handlers legitimately
+process.setMaxListeners(20);
+
+// Use global to survive Next.js hot reload — prevents duplicate intervals
+const g = global.__appSingleton ??= {
+ signalHandlersRegistered: false,
+ watchdogInterval: null,
+ networkMonitorInterval: null,
+ lastNetworkFingerprint: null,
+ lastWatchdogTick: Date.now(),
+ lastTunnelRestartAt: 0,
+ tunnelRestartInProgress: false,
+ mitmStartInProgress: false,
+};
+
+const WATCHDOG_INTERVAL_MS = 60000;
+const NETWORK_CHECK_INTERVAL_MS = 5000;
+const NETWORK_RESTART_COOLDOWN_MS = 30000;
+
+/**
+ * Initialize app on startup
+ * - Cleanup stale data
+ * - Auto-reconnect tunnel if previously enabled
+ * - Register shutdown handler to kill cloudflared
+ * - Start watchdog to recover tunnel after sleep/wake
+ */
+export async function initializeApp() {
+ try {
+ await cleanupProviderConnections();
+
+ // Auto-reconnect tunnel if it was enabled before restart
+ const settings = await getSettings();
+ if (settings.tunnelEnabled && !isCloudflaredRunning()) {
+ console.log("[InitApp] Tunnel was enabled, auto-reconnecting...");
+ try {
+ await enableTunnel();
+ console.log("[InitApp] Tunnel reconnected");
+ } catch (error) {
+ console.log("[InitApp] Tunnel reconnect failed:", error.message);
+ }
+ }
+
+ // Kill cloudflared on process exit (register once only)
+ if (!g.signalHandlersRegistered) {
+ const cleanup = () => {
+ killCloudflared();
+ process.exit();
+ };
+ process.on("SIGINT", cleanup);
+ process.on("SIGTERM", cleanup);
+ g.signalHandlersRegistered = true;
+ }
+
+ // Pre-download cloudflared binary in background
+ ensureCloudflared().catch(() => {});
+
+ // Watchdog: recover tunnel after process crash
+ startWatchdog();
+
+ // Network monitor: detect sleep/wake + network changes → restart tunnel
+ startNetworkMonitor();
+
+ // Auto-start MITM if it was enabled before restart
+ autoStartMitm();
+ } catch (error) {
+ console.error("[InitApp] Error:", error);
+ }
+}
+
+/** Auto-start MITM if it was enabled before restart */
+async function autoStartMitm() {
+ if (g.mitmStartInProgress) return;
+ g.mitmStartInProgress = true;
+ try {
+ const settings = await getSettings();
+ if (!settings.mitmEnabled) return;
+
+ const mitmStatus = await getMitmStatus();
+ if (mitmStatus.running) return;
+
+ const password = await loadEncryptedPassword();
+ if (!password && process.platform !== "win32") {
+ console.log("[InitApp] MITM was enabled but no saved password found, skipping auto-start");
+ return;
+ }
+
+ // Need an active API key
+ const keys = await getApiKeys();
+ const activeKey = keys.find(k => k.isActive !== false);
+
+ console.log("[InitApp] MITM was enabled, auto-starting...");
+ await startMitm(activeKey?.key || "sk_9router", password);
+ console.log("[InitApp] MITM auto-started");
+ } catch (err) {
+ console.log("[InitApp] MITM auto-start failed:", err.message);
+ } finally {
+ g.mitmStartInProgress = false;
+ }
+}
+
+/** Periodically check tunnel process health and reconnect if crashed */
+function startWatchdog() {
+ if (g.watchdogInterval) return;
+ g.watchdogInterval = setInterval(async () => {
+ try {
+ if (isTunnelManuallyDisabled()) return;
+ if (isTunnelReconnecting()) return;
+ if (g.tunnelRestartInProgress) return;
+ const settings = await getSettings();
+ if (!settings.tunnelEnabled) return;
+ if (isCloudflaredRunning()) return;
+ console.log("[Watchdog] Tunnel process is down, attempting recovery...");
+ g.tunnelRestartInProgress = true;
+ try {
+ await enableTunnel();
+ console.log("[Watchdog] Tunnel recovered");
+ } finally {
+ g.tunnelRestartInProgress = false;
+ }
+ } catch (err) {
+ console.log("[Watchdog] Recovery failed:", err.message);
+ }
+ }, WATCHDOG_INTERVAL_MS);
+
+ if (g.watchdogInterval.unref) g.watchdogInterval.unref();
+}
+
+/** Get network fingerprint from active interfaces (IPv4 only) */
+function getNetworkFingerprint() {
+ const interfaces = os.networkInterfaces();
+ const active = [];
+ for (const [name, addrs] of Object.entries(interfaces)) {
+ if (!addrs) continue;
+ for (const addr of addrs) {
+ if (!addr.internal && addr.family === "IPv4") {
+ active.push(`${name}:${addr.address}`);
+ }
+ }
+ }
+ return active.sort().join("|");
+}
+
+/** Monitor network changes + sleep/wake → kill and reconnect tunnel */
+function startNetworkMonitor() {
+ if (g.networkMonitorInterval) return;
+
+ g.lastNetworkFingerprint = getNetworkFingerprint();
+ g.lastWatchdogTick = Date.now();
+
+ g.networkMonitorInterval = setInterval(async () => {
+ try {
+ if (isTunnelManuallyDisabled()) return;
+ const settings = await getSettings();
+ if (!settings.tunnelEnabled) return;
+
+ const now = Date.now();
+ const elapsed = now - g.lastWatchdogTick;
+ g.lastWatchdogTick = now;
+
+ const currentFingerprint = getNetworkFingerprint();
+ const networkChanged = currentFingerprint !== g.lastNetworkFingerprint;
+ const wasSleep = elapsed > NETWORK_CHECK_INTERVAL_MS * 3;
+
+ if (networkChanged) g.lastNetworkFingerprint = currentFingerprint;
+
+ if (!networkChanged && !wasSleep) return;
+
+ // Skip if restart already in progress or restarted recently
+ if (g.tunnelRestartInProgress) return;
+ if (isTunnelReconnecting()) return;
+ if (now - g.lastTunnelRestartAt < NETWORK_RESTART_COOLDOWN_MS) return;
+
+ const reason = wasSleep && networkChanged ? "sleep/wake + network change"
+ : wasSleep ? "sleep/wake" : "network change";
+ console.log(`[NetworkMonitor] ${reason} detected, restarting tunnel...`);
+
+ g.tunnelRestartInProgress = true;
+ g.lastTunnelRestartAt = now;
+ try {
+ killCloudflared();
+ await new Promise(r => setTimeout(r, 2000));
+ await enableTunnel();
+ console.log("[NetworkMonitor] Tunnel restarted");
+ g.lastNetworkFingerprint = getNetworkFingerprint();
+ } finally {
+ g.tunnelRestartInProgress = false;
+ }
+ } catch (err) {
+ console.log("[NetworkMonitor] Tunnel restart failed:", err.message);
+ }
+ }, NETWORK_CHECK_INTERVAL_MS);
+
+ if (g.networkMonitorInterval.unref) g.networkMonitorInterval.unref();
+}
+
+export default initializeApp;
diff --git a/9router/src/shared/services/initializeCloudSync.js b/9router/src/shared/services/initializeCloudSync.js
new file mode 100644
index 00000000..b26a7d55
--- /dev/null
+++ b/9router/src/shared/services/initializeCloudSync.js
@@ -0,0 +1,37 @@
+/* ========== CLOUD SYNC — COMMENTED OUT (replaced by Tunnel) ==========
+import { getCloudSyncScheduler } from "@/shared/services/cloudSyncScheduler";
+========== END CLOUD SYNC ========== */
+import { cleanupProviderConnections } from "@/lib/localDb";
+
+/**
+ * Initialize cloud sync scheduler
+ * This should be called when the application starts
+ */
+export async function initializeCloudSync() {
+ try {
+ // Cleanup null fields from existing data
+ await cleanupProviderConnections();
+
+ /* ========== CLOUD SYNC — COMMENTED OUT (replaced by Tunnel) ==========
+ // Create scheduler instance with default 15-minute interval
+ const scheduler = await getCloudSyncScheduler(null, 15);
+
+ // Start the scheduler
+ await scheduler.start();
+
+ return scheduler;
+ ========== END CLOUD SYNC ========== */
+ return null;
+ } catch (error) {
+ console.error("[CloudSync] Error initializing scheduler:", error);
+ throw error;
+ }
+}
+
+// For development/testing purposes
+if (typeof require !== "undefined" && require.main === module) {
+ initializeCloudSync().catch(console.log);
+}
+
+export default initializeCloudSync;
+
diff --git a/9router/src/shared/utils/api.js b/9router/src/shared/utils/api.js
new file mode 100644
index 00000000..5712ef9e
--- /dev/null
+++ b/9router/src/shared/utils/api.js
@@ -0,0 +1,93 @@
+/**
+ * API utility functions for making HTTP requests
+ */
+
+const DEFAULT_HEADERS = {
+ "Content-Type": "application/json",
+};
+
+/**
+ * Make a GET request
+ * @param {string} url - API endpoint
+ * @param {object} options - Fetch options
+ * @returns {Promise