-
Round Robin
-
- {providerStrategy === "round-robin" && (
-
-
Sticky:
-
handleStickyLimitChange(e.target.value)}
- placeholder="1"
- className="w-14 px-2 py-1 text-xs border border-border rounded-md bg-background focus:outline-none focus:border-primary"
- />
+
+ {/* Thinking config */}
+ {/* {thinkingConfig && (
+
+ Thinking
+
- )}
+ )} */}
+ {/* Round Robin toggle */}
+
+
Round Robin
+
+ {providerStrategy === "round-robin" && (
+
+ Sticky:
+ handleStickyLimitChange(e.target.value)}
+ placeholder="1"
+ className="w-14 px-2 py-1 text-xs border border-border rounded-md bg-background focus:outline-none focus:border-primary"
+ />
+
+ )}
+
diff --git a/src/app/api/9remote/install/route.js b/src/app/api/9remote/install/route.js
deleted file mode 100644
index 2332687e..00000000
--- a/src/app/api/9remote/install/route.js
+++ /dev/null
@@ -1,25 +0,0 @@
-import { NextResponse } from "next/server";
-import { exec } from "child_process";
-import { join, dirname } from "path";
-
-// Use npm from the same Node.js that runs Next.js — ensures 9remote
-// lands in the correct global bin (nvm or system, whichever is active)
-const npmBin = join(dirname(process.execPath), "npm");
-
-function installPackage() {
- return new Promise((resolve, reject) => {
- exec(`"${npmBin}" install -g 9remote`, { windowsHide: true }, (err, stdout, stderr) => {
- if (err) reject(new Error(stderr || err.message));
- else resolve(stdout);
- });
- });
-}
-
-export async function POST() {
- try {
- await installPackage();
- return NextResponse.json({ ok: true });
- } catch (error) {
- return NextResponse.json({ ok: false, error: error.message }, { status: 500 });
- }
-}
diff --git a/src/app/api/9remote/start/route.js b/src/app/api/9remote/start/route.js
deleted file mode 100644
index 67b60f9f..00000000
--- a/src/app/api/9remote/start/route.js
+++ /dev/null
@@ -1,43 +0,0 @@
-import { NextResponse } from "next/server";
-import { spawn } from "child_process";
-import { join, dirname } from "path";
-import os from "os";
-import { setRemoteProcess } from "@/lib/9remoteManager";
-
-const bin9remote = join(dirname(process.execPath), "9remote");
-
-export async function POST() {
- try {
- const nodeDir = dirname(process.execPath);
- const existingPath = process.env.PATH || "";
- const path = existingPath.includes(nodeDir)
- ? existingPath
- : `${nodeDir}:${existingPath}`;
-
- const env = {
- HOME: os.homedir(),
- PATH: path,
- USER: process.env.USER || process.env.LOGNAME,
- LANG: process.env.LANG || "en_US.UTF-8",
- TERM: process.env.TERM || "xterm-256color",
- TMPDIR: process.env.TMPDIR || os.tmpdir(),
- SHELL: process.env.SHELL,
- };
- const home = os.homedir();
-
- // Spawn without detached - process will be child of Next.js and receive SIGTERM
- const child = spawn(bin9remote, ["ui", "--start"], {
- cwd: home,
- stdio: "ignore",
- env,
- windowsHide: process.platform === "win32",
- });
-
- // Store child process for manual cleanup if needed
- setRemoteProcess(child);
-
- return NextResponse.json({ ok: true });
- } catch (error) {
- return NextResponse.json({ ok: false, error: error.message }, { status: 500 });
- }
-}
diff --git a/src/app/api/9remote/status/route.js b/src/app/api/9remote/status/route.js
deleted file mode 100644
index 735b22b5..00000000
--- a/src/app/api/9remote/status/route.js
+++ /dev/null
@@ -1,25 +0,0 @@
-import { NextResponse } from "next/server";
-import { existsSync } from "fs";
-import { join, dirname } from "path";
-
-const bin9remote = join(dirname(process.execPath), "9remote");
-const AGENT_URL = "http://localhost:2208";
-
-async function isRunning() {
- try {
- const res = await fetch(`${AGENT_URL}/api/health`, {
- signal: AbortSignal.timeout(1500),
- });
- return res.ok;
- } catch {
- return false;
- }
-}
-
-export async function GET() {
- const running = await isRunning();
- if (running) return NextResponse.json({ installed: true, running: true });
-
- const installed = existsSync(bin9remote);
- return NextResponse.json({ installed, running: false });
-}
diff --git a/src/lib/9remoteManager.js b/src/lib/9remoteManager.js
deleted file mode 100644
index 0b608ee0..00000000
--- a/src/lib/9remoteManager.js
+++ /dev/null
@@ -1,35 +0,0 @@
-// 9remote process lifecycle manager
-let remoteProcess = null;
-
-export function setRemoteProcess(child) {
- remoteProcess = child;
-}
-
-export function getRemoteProcess() {
- return remoteProcess;
-}
-
-export function killRemote() {
- if (!remoteProcess) return;
-
- try {
- remoteProcess.kill("SIGTERM");
- console.log(`[9remote] Killed process ${remoteProcess.pid}`);
- remoteProcess = null;
- } catch (err) {
- console.log(`[9remote] Failed to kill:`, err.message);
- remoteProcess = null;
- }
-}
-
-// Register cleanup handlers
-if (typeof process !== "undefined") {
- const cleanup = () => {
- killRemote();
- process.exit(0);
- };
-
- process.on("SIGTERM", cleanup);
- process.on("SIGINT", cleanup);
- process.on("beforeExit", killRemote);
-}
diff --git a/src/mitm/handlers/antigravity.js b/src/mitm/handlers/antigravity.js
index d88d7335..21695e37 100644
--- a/src/mitm/handlers/antigravity.js
+++ b/src/mitm/handlers/antigravity.js
@@ -8,7 +8,7 @@ async function intercept(req, res, bodyBuffer, mappedModel) {
try {
const body = JSON.parse(bodyBuffer.toString());
body.model = mappedModel;
- const routerRes = await fetchRouter(body);
+ const routerRes = await fetchRouter(body, "/v1/chat/completions", req.headers);
await pipeSSE(routerRes, res);
} catch (error) {
err(`[antigravity] ${error.message}`);
diff --git a/src/mitm/handlers/base.js b/src/mitm/handlers/base.js
index 1268d160..07f9d57d 100644
--- a/src/mitm/handlers/base.js
+++ b/src/mitm/handlers/base.js
@@ -6,13 +6,26 @@ const ROUTER_BASE = String(process.env.MITM_ROUTER_BASE || DEFAULT_LOCAL_ROUTER)
.replace(/\/+$/, "") || DEFAULT_LOCAL_ROUTER;
const API_KEY = process.env.ROUTER_API_KEY;
+// Headers that must not be forwarded to 9Router
+const STRIP_HEADERS = new Set([
+ "host", "content-length", "connection", "transfer-encoding",
+ "content-type", "authorization"
+]);
+
/**
- * Send body to 9Router at the given path and return the fetch Response object
+ * Send body to 9Router at the given path and return the fetch Response object.
+ * Optionally forwards client headers (stripped of hop-by-hop / overridden keys).
*/
-async function fetchRouter(openaiBody, path = "/v1/chat/completions") {
+async function fetchRouter(openaiBody, path = "/v1/chat/completions", clientHeaders = {}) {
+ const forwarded = {};
+ for (const [k, v] of Object.entries(clientHeaders)) {
+ if (!STRIP_HEADERS.has(k.toLowerCase())) forwarded[k] = v;
+ }
+
const response = await fetch(`${ROUTER_BASE}${path}`, {
method: "POST",
headers: {
+ ...forwarded,
"Content-Type": "application/json",
...(API_KEY && { "Authorization": `Bearer ${API_KEY}` })
},
diff --git a/src/mitm/handlers/copilot.js b/src/mitm/handlers/copilot.js
index 78a52bb5..cf6a920c 100644
--- a/src/mitm/handlers/copilot.js
+++ b/src/mitm/handlers/copilot.js
@@ -23,7 +23,7 @@ async function intercept(req, res, bodyBuffer, mappedModel) {
const body = JSON.parse(bodyBuffer.toString());
body.model = mappedModel;
const routerPath = resolveRouterPath(req.url);
- const routerRes = await fetchRouter(body, routerPath);
+ const routerRes = await fetchRouter(body, routerPath, req.headers);
await pipeSSE(routerRes, res);
} catch (error) {
err(`[copilot] ${error.message}`);
diff --git a/src/mitm/handlers/kiro.js b/src/mitm/handlers/kiro.js
index 5b4c0e76..5f73b508 100644
--- a/src/mitm/handlers/kiro.js
+++ b/src/mitm/handlers/kiro.js
@@ -8,7 +8,7 @@ async function intercept(req, res, bodyBuffer, mappedModel) {
try {
const body = JSON.parse(bodyBuffer.toString());
body.model = mappedModel;
- const routerRes = await fetchRouter(body);
+ const routerRes = await fetchRouter(body, "/v1/chat/completions", req.headers);
await pipeSSE(routerRes, res);
} catch (error) {
err(`[Kiro] ${error.message}`);
diff --git a/src/shared/components/NineRemoteModal.js b/src/shared/components/NineRemoteModal.js
deleted file mode 100644
index e79ca8ea..00000000
--- a/src/shared/components/NineRemoteModal.js
+++ /dev/null
@@ -1,253 +0,0 @@
-"use client";
-
-import { useEffect, useRef, useState, useCallback } from "react";
-import { createPortal } from "react-dom";
-
-const S = {
- CHECKING: "checking",
- NOT_INSTALLED: "not_installed",
- NOT_RUNNING: "not_running",
- INSTALLING: "installing",
- STARTING: "starting",
- RUNNING: "running",
- ERROR: "error",
-};
-
-const POLL_INTERVAL_MS = 2000;
-const POLL_TIMEOUT_MS = 60000;
-
-const FEATURES = [
- { icon: "terminal", label: "Terminal", desc: "Full shell access" },
- { icon: "cast", label: "Desktop", desc: "Screen sharing" },
- { icon: "folder_open", label: "Files", desc: "Browse & edit files" },
-];
-
-const BULLETS = [
- { icon: "qr_code_scanner", text: "Scan QR to connect instantly" },
- { icon: "wifi_off", text: "No port forwarding needed" },
- { icon: "devices", text: "Works on any device" },
-];
-
-export default function NineRemoteModal({ isOpen, onClose, onInstalled }) {
- const [state, setState] = useState(S.CHECKING);
- const [errorMsg, setErrorMsg] = useState("");
- const pollRef = useRef(null);
-
- const stopPolling = () => {
- if (pollRef.current) { clearTimeout(pollRef.current); pollRef.current = null; }
- };
-
- const pollUntilRunning = useCallback(() => {
- stopPolling();
- const startedAt = Date.now();
- const poll = async () => {
- if (Date.now() - startedAt > POLL_TIMEOUT_MS) {
- setState(S.ERROR);
- setErrorMsg("9Remote is taking too long to start. Try running `9remote ui` manually.");
- return;
- }
- try {
- const res = await fetch("/api/9remote/status");
- const data = await res.json();
- if (data.running) { setState(S.RUNNING); return; }
- } catch {}
- pollRef.current = setTimeout(poll, POLL_INTERVAL_MS);
- };
- poll();
- }, []);
-
- const checkAndInit = useCallback(async () => {
- setState(S.CHECKING);
- setErrorMsg("");
- try {
- const res = await fetch("/api/9remote/status");
- const data = await res.json();
- if (data.running) { setState(S.RUNNING); return; }
- if (!data.installed) { setState(S.NOT_INSTALLED); return; }
- setState(S.NOT_RUNNING);
- } catch (err) { setState(S.ERROR); setErrorMsg(err.message); }
- }, []);
-
- const handleStart = useCallback(async () => {
- setState(S.STARTING);
- try {
- const res = await fetch("/api/9remote/start", { method: "POST" });
- if (!res.ok) throw new Error("Failed to start 9Remote");
- pollUntilRunning();
- } catch (err) { setState(S.ERROR); setErrorMsg(err.message); }
- }, [pollUntilRunning]);
-
- const handleInstall = async () => {
- setState(S.INSTALLING);
- setErrorMsg("");
- try {
- const res = await fetch("/api/9remote/install", { method: "POST" });
- if (!res.ok) { const d = await res.json(); throw new Error(d.error || "Install failed"); }
- onInstalled?.();
- await handleStart();
- } catch (err) { setState(S.ERROR); setErrorMsg(err.message); }
- };
-
- useEffect(() => {
- if (isOpen) checkAndInit();
- else stopPolling();
- return stopPolling;
- }, [isOpen, checkAndInit]);
-
- useEffect(() => {
- if (!isOpen) return;
- document.body.style.overflow = "hidden";
- const onEsc = (e) => { if (e.key === "Escape") onClose(); };
- document.addEventListener("keydown", onEsc);
- return () => { document.body.style.overflow = ""; document.removeEventListener("keydown", onEsc); };
- }, [isOpen, onClose]);
-
- if (!isOpen) return null;
-
- const btnCfg = {
- [S.NOT_INSTALLED]: { label: "Install 9Remote", icon: "download", onClick: handleInstall, loading: false },
- [S.INSTALLING]: { label: "Installing...", icon: "hourglass_top", onClick: null, loading: true },
- [S.NOT_RUNNING]: { label: "Start 9Remote", icon: "play_arrow", onClick: handleStart, loading: false },
- [S.STARTING]: { label: "Starting...", icon: "hourglass_top", onClick: null, loading: true },
- [S.CHECKING]: { label: "Checking...", icon: "hourglass_top", onClick: null, loading: true },
- }[state];
-
- // Running — iframe only
- if (state === S.RUNNING) {
- return createPortal(
-
,
- document.body
- );
- }
-
- // Error state
- if (state === S.ERROR) {
- return createPortal(
-
-
-
-
- 9Remote
-
-
-
-
- error
-
-
-
Something went wrong
-
{errorMsg}
-
-
-
-
-
,
- document.body
- );
- }
-
- // Main card — layout cố định, chỉ đổi button
- return createPortal(
-
-
-
-
- {/* Header */}
-
-
-
- terminal
-
-
9Remote
-
-
-
-
- {/* Body */}
-
- {/* Hero */}
-
-
- terminal
-
-
9Remote
-
- Access your terminal, desktop & files from anywhere
-
-
-
- {/* Feature cards */}
-
- {FEATURES.map(({ icon, label, desc }) => (
-
-
{icon}
-
{label}
-
{desc}
-
- ))}
-
-
- {/* Bullets */}
-
- {BULLETS.map(({ icon, text }) => (
-
- {icon}
- {text}
-
- ))}
-
-
- {/* CTA button */}
- {btnCfg && (
-
- )}
-
-
-
,
- document.body
- );
-}
diff --git a/src/shared/components/index.js b/src/shared/components/index.js
index 9ed963ce..c057b8bd 100644
--- a/src/shared/components/index.js
+++ b/src/shared/components/index.js
@@ -19,7 +19,6 @@ export { default as ManualConfigModal } from "./ManualConfigModal";
export { default as UsageStats } from "./UsageStats";
export { default as LanguageSwitcher } from "./LanguageSwitcher";
export { default as NineRemoteButton } from "./NineRemoteButton";
-export { default as NineRemoteModal } from "./NineRemoteModal";
export { default as RequestLogger } from "./RequestLogger";
export { default as KiroAuthModal } from "./KiroAuthModal";
export { default as KiroOAuthWrapper } from "./KiroOAuthWrapper";
diff --git a/src/shared/constants/providers.js b/src/shared/constants/providers.js
index 50b65f7b..439a8051 100644
--- a/src/shared/constants/providers.js
+++ b/src/shared/constants/providers.js
@@ -4,7 +4,7 @@
export const FREE_PROVIDERS = {
kiro: { id: "kiro", alias: "kr", name: "Kiro AI", icon: "psychology_alt", color: "#FF6B35" },
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", deprecated: true, deprecationNotice: "Google has tightened Gemini CLI abuse detection and restricted Pro models to paid accounts (Mar 25, 2026). Using this provider may violate ToS and risk account bans." },
+ "gemini-cli": { id: "gemini-cli", alias: "gc", name: "Gemini CLI", icon: "terminal", color: "#4285F4", deprecated: true, deprecationNotice: "Gemini CLI is designed exclusively for Gemini CLI. Using it with other tools (OpenClaw, Claude, Codex...) may result in account restrictions or bans." },
// gitlab: { id: "gitlab", alias: "gl", name: "GitLab Duo", icon: "code", color: "#FC6D26" },
// codebuddy: { id: "codebuddy", alias: "cb", name: "CodeBuddy", icon: "smart_toy", color: "#006EFF" },
// qoder: { id: "qoder", alias: "qd", name: "Qoder AI", icon: "water_drop", color: "#EC4899" },
@@ -14,18 +14,35 @@ export const FREE_PROVIDERS = {
// Free Tier Providers (has free access but may require account/API key)
export const FREE_TIER_PROVIDERS = {
- openrouter: { id: "openrouter", alias: "openrouter", name: "OpenRouter", icon: "router", color: "#F97316", textIcon: "OR", website: "https://openrouter.ai", notice: { text: "Free tier: 27+ free models, no credit card needed, 200 req/day. After $10 credit: 1,000 req/day.", apiKeyUrl: "https://openrouter.ai/settings/keys" }, modelsFetcher: { url: "https://openrouter.ai/api/v1/models", type: "openrouter-free" }, passthroughModels: true },
+ openrouter: { id: "openrouter", alias: "openrouter", name: "OpenRouter", icon: "router", color: "#F97316", textIcon: "OR", website: "https://openrouter.ai", notice: { text: "Free tier: 27+ free models, no credit card needed, 200 req/day. After $10 credit: 1,000 req/day.", apiKeyUrl: "https://openrouter.ai/settings/keys" }, modelsFetcher: { url: "https://openrouter.ai/api/v1/models", type: "openrouter-free" }, passthroughModels: true, serviceKinds: ["llm", "embedding"] },
nvidia: { id: "nvidia", alias: "nvidia", name: "NVIDIA NIM", icon: "developer_board", color: "#76B900", textIcon: "NV", website: "https://developer.nvidia.com/nim", notice: { text: "Free access for NVIDIA Developer Program members (prototyping & testing).", apiKeyUrl: "https://build.nvidia.com/settings/api-keys" } },
ollama: { id: "ollama", alias: "ollama", name: "Ollama Cloud", icon: "cloud", color: "#ffffffff", textIcon: "OL", website: "https://ollama.com", notice: { text: "Free tier: light usage, 1 cloud model at a time (limits reset every 5h & 7d). Pro $20/mo · Max $100/mo.", apiKeyUrl: "https://ollama.com/settings/keys" } },
vertex: { id: "vertex", alias: "vx", name: "Vertex AI", icon: "cloud", color: "#4285F4", textIcon: "VX", website: "https://cloud.google.com/vertex-ai", notice: { text: "New Google Cloud accounts get $300 free credits. Requires GCP project + Service Account with Vertex AI API enabled.", apiKeyUrl: "https://console.cloud.google.com/iam-admin/serviceaccounts" } },
gemini: { id: "gemini", alias: "gemini", name: "Gemini", icon: "diamond", color: "#4285F4", textIcon: "GE", website: "https://ai.google.dev", serviceKinds: ["llm", "embedding"] },
};
+// Thinking config definitions
+// options: list of selectable modes ("auto" = no override from server)
+// defaultMode: fallback when user hasn't configured
+// extended: claude-style thinking (thinking.type + budget_tokens) — used by most providers
+// effort: openai-style reasoning_effort — only openai + codex
+export const THINKING_CONFIG = {
+ extended: {
+ options: ["auto", "on", "off"],
+ defaultMode: "auto",
+ defaultBudgetTokens: 10000
+ },
+ effort: {
+ options: ["auto", "none", "low", "medium", "high"],
+ defaultMode: "auto"
+ }
+};
+
// 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", deprecated: true, deprecationNotice: "Antigravity has tightened abuse detection and restricted model access. Using this provider may violate ToS and risk account bans." },
- codex: { id: "codex", alias: "cx", name: "OpenAI Codex", icon: "code", color: "#3B82F6" },
+ antigravity: { id: "antigravity", alias: "ag", name: "Antigravity", icon: "rocket_launch", color: "#F59E0B", deprecated: true, deprecationNotice: "AG is designed exclusively for Antigravity IDE. Using it with other tools (OpenClaw, Claude, Codex...) may result in account restrictions or bans." },
+ codex: { id: "codex", alias: "cx", name: "OpenAI Codex", icon: "code", color: "#3B82F6", thinkingConfig: THINKING_CONFIG.effort },
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" },
@@ -42,7 +59,7 @@ export const APIKEY_PROVIDERS = {
"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", serviceKinds: ["llm", "embedding", "tts"] },
+ openai: { id: "openai", alias: "openai", name: "OpenAI", icon: "auto_awesome", color: "#10A37F", textIcon: "OA", website: "https://platform.openai.com", serviceKinds: ["llm", "embedding", "tts"], thinkingConfig: THINKING_CONFIG.effort },
anthropic: { id: "anthropic", alias: "anthropic", name: "Anthropic", icon: "smart_toy", color: "#D97757", textIcon: "AN", website: "https://console.anthropic.com", serviceKinds: ["llm"] },
diff --git a/src/sse/handlers/chat.js b/src/sse/handlers/chat.js
index bd45748a..b2e64a1e 100644
--- a/src/sse/handlers/chat.js
+++ b/src/sse/handlers/chat.js
@@ -189,6 +189,7 @@ async function handleSingleModelChat(body, modelStr, clientRawRequest = null, re
// Use shared chatCore
const chatSettings = await getSettings();
+ const providerThinking = (chatSettings.providerThinking || {})[provider] || null;
const result = await handleChatCore({
body: { ...body, model: `${provider}/${model}` },
modelInfo: { provider, model },
@@ -199,6 +200,7 @@ async function handleSingleModelChat(body, modelStr, clientRawRequest = null, re
userAgent,
apiKey,
ccFilterNaming: !!chatSettings.ccFilterNaming,
+ providerThinking,
// Detect source format by endpoint + body
sourceFormatOverride: request?.url ? detectFormatByEndpoint(new URL(request.url).pathname, body) : null,
onCredentialsRefreshed: async (newCreds) => {