diff --git a/open-sse/config/providerModels.js b/open-sse/config/providerModels.js index faca3638..c95ee243 100644 --- a/open-sse/config/providerModels.js +++ b/open-sse/config/providerModels.js @@ -221,7 +221,16 @@ export const PROVIDER_MODELS = { { id: "text-embedding-005", name: "Text Embedding 005", type: "embedding" }, { id: "text-embedding-004", name: "Text Embedding 004 (Legacy)", type: "embedding" }, ], - openrouter: [], + openrouter: [ + // Embedding models + { id: "openai/text-embedding-3-large", name: "OpenAI Text Embedding 3 Large", type: "embedding" }, + { id: "openai/text-embedding-3-small", name: "OpenAI Text Embedding 3 Small", type: "embedding" }, + { id: "openai/text-embedding-ada-002", name: "OpenAI Text Embedding Ada 002", type: "embedding" }, + { id: "qwen/qwen3-embedding-8b", name: "Qwen3 Embedding 8B", type: "embedding" }, + { id: "perplexity/pplx-embed-v1-4b", name: "Perplexity Embed V1 4B", type: "embedding" }, + { id: "perplexity/pplx-embed-v1-0.6b", name: "Perplexity Embed V1 0.6B", type: "embedding" }, + { id: "nvidia/llama-nemotron-embed-vl-1b-v2:free", name: "NVIDIA Nemotron Embed VL 1B V2 (Free)", type: "embedding" }, + ], glm: [ { id: "glm-5.1", name: "GLM 5.1" }, { id: "glm-5", name: "GLM 5" }, diff --git a/open-sse/handlers/chatCore.js b/open-sse/handlers/chatCore.js index 0b3bcc17..780a4fee 100644 --- a/open-sse/handlers/chatCore.js +++ b/open-sse/handlers/chatCore.js @@ -24,7 +24,7 @@ import { detectClientTool, isNativePassthrough } from "../utils/clientDetector.j * @param {object} options.credentials - Provider credentials * @param {string} options.sourceFormatOverride - Override detected source format (e.g. "openai-responses") */ -export async function handleChatCore({ body, modelInfo, credentials, log, onCredentialsRefreshed, onRequestSuccess, onDisconnect, clientRawRequest, connectionId, userAgent, apiKey, ccFilterNaming, sourceFormatOverride }) { +export async function handleChatCore({ body, modelInfo, credentials, log, onCredentialsRefreshed, onRequestSuccess, onDisconnect, clientRawRequest, connectionId, userAgent, apiKey, ccFilterNaming, sourceFormatOverride, providerThinking }) { const { provider, model } = modelInfo; const requestStartTime = Date.now(); @@ -39,6 +39,20 @@ export async function handleChatCore({ body, modelInfo, credentials, log, onCred const targetFormat = modelTargetFormat || getTargetFormat(provider); const stripList = getModelStrip(alias, model); + // Inject provider-level thinking config override (only if client hasn't set) + // on/off → extended type (body.thinking), none/low/medium/high → effort type (body.reasoning_effort) + if (providerThinking?.mode && providerThinking.mode !== "auto") { + const mode = providerThinking.mode; + if (mode === "on" && !body.thinking) { + console.log("Injecting provider-level thinking config override: on"); + body = { ...body, thinking: { type: "enabled", budget_tokens: 10000 } }; + } else if (mode === "off" && !body.thinking) { + body = { ...body, thinking: { type: "disabled" } }; + } else if (!body.reasoning_effort) { + body = { ...body, reasoning_effort: mode }; + } + } + const clientRequestedStreaming = body.stream === true || sourceFormat === FORMATS.ANTIGRAVITY || sourceFormat === FORMATS.GEMINI || sourceFormat === FORMATS.GEMINI_CLI; const providerRequiresStreaming = provider === "openai" || provider === "codex"; let stream = providerRequiresStreaming ? true : (body.stream !== false); diff --git a/src/app/(dashboard)/dashboard/providers/[id]/page.js b/src/app/(dashboard)/dashboard/providers/[id]/page.js index 3adb0a6f..b9077e48 100644 --- a/src/app/(dashboard)/dashboard/providers/[id]/page.js +++ b/src/app/(dashboard)/dashboard/providers/[id]/page.js @@ -6,7 +6,7 @@ import { useParams, useRouter } from "next/navigation"; import Link from "next/link"; import Image from "next/image"; import { Card, Button, Badge, Input, Modal, CardSkeleton, OAuthModal, KiroOAuthWrapper, CursorAuthModal, IFlowCookieModal, GitLabAuthModal, Toggle, Select, EditConnectionModal } from "@/shared/components"; -import { OAUTH_PROVIDERS, APIKEY_PROVIDERS, FREE_PROVIDERS, FREE_TIER_PROVIDERS, getProviderAlias, isOpenAICompatibleProvider, isAnthropicCompatibleProvider } from "@/shared/constants/providers"; +import { OAUTH_PROVIDERS, APIKEY_PROVIDERS, FREE_PROVIDERS, FREE_TIER_PROVIDERS, getProviderAlias, isOpenAICompatibleProvider, isAnthropicCompatibleProvider, AI_PROVIDERS, THINKING_CONFIG } from "@/shared/constants/providers"; import { getModelsByProviderId } from "@/shared/constants/models"; import { useCopyToClipboard } from "@/shared/hooks/useCopyToClipboard"; import { fetchSuggestedModels } from "@/shared/utils/providerModelsFetcher"; @@ -37,6 +37,7 @@ export default function ProviderDetailPage() { const [bulkUpdatingProxy, setBulkUpdatingProxy] = useState(false); const [providerStrategy, setProviderStrategy] = useState(null); // null = use global, "round-robin" = override const [providerStickyLimit, setProviderStickyLimit] = useState(""); + const [thinkingMode, setThinkingMode] = useState("auto"); const [suggestedModels, setSuggestedModels] = useState([]); const [kiloFreeModels, setKiloFreeModels] = useState([]); const { copied, copy } = useCopyToClipboard(); @@ -60,6 +61,7 @@ export default function ProviderDetailPage() { const isOpenAICompatible = isOpenAICompatibleProvider(providerId); const isAnthropicCompatible = isAnthropicCompatibleProvider(providerId); const isCompatible = isOpenAICompatible || isAnthropicCompatible; + const thinkingConfig = AI_PROVIDERS[providerId]?.thinkingConfig || THINKING_CONFIG.extended; const providerStorageAlias = isCompatible ? providerId : providerAlias; const providerDisplayAlias = isCompatible @@ -111,6 +113,9 @@ export default function ProviderDetailPage() { const override = (settingsData.providerStrategies || {})[providerId] || {}; setProviderStrategy(override.fallbackStrategy || null); setProviderStickyLimit(override.stickyRoundRobinLimit != null ? String(override.stickyRoundRobinLimit) : "1"); + // Load per-provider thinking config + const thinkingCfg = (settingsData.providerThinking || {})[providerId] || {}; + setThinkingMode(thinkingCfg.mode || "auto"); if (nodesRes.ok) { let node = (nodesData.nodes || []).find((entry) => entry.id === providerId) || null; @@ -197,6 +202,32 @@ export default function ProviderDetailPage() { saveProviderStrategy("round-robin", value); }; + const saveThinkingConfig = async (mode) => { + try { + const settingsRes = await fetch("/api/settings", { cache: "no-store" }); + const settingsData = settingsRes.ok ? await settingsRes.json() : {}; + const current = settingsData.providerThinking || {}; + const updated = { ...current }; + if (!mode || mode === "auto") { + delete updated[providerId]; + } else { + updated[providerId] = { mode }; + } + await fetch("/api/settings", { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ providerThinking: updated }), + }); + } catch (error) { + console.log("Error saving thinking config:", error); + } + }; + + const handleThinkingModeChange = (mode) => { + setThinkingMode(mode); + saveThinkingConfig(mode); + }; + useEffect(() => { fetchConnections(); fetchAliases(); @@ -731,22 +762,22 @@ export default function ProviderDetailPage() { {providerInfo.deprecated && ( -
- info -

{providerInfo.deprecationNotice}

+
+ warning +

{providerInfo.deprecationNotice}

)} {providerInfo.notice && !providerInfo.deprecated && ( -
- info -

{providerInfo.notice.text}

+
+ info +

{providerInfo.notice.text}

{providerInfo.notice.apiKeyUrl && ( Get API Key → @@ -826,26 +857,43 @@ export default function ProviderDetailPage() {

Connections

- {/* 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" - /> +
+ {/* 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( -
-
-
-