Enhance provider models and chat handling with new thinking configurations

This commit is contained in:
decolua
2026-04-13 12:04:57 +07:00
parent ee1271b6fd
commit 4c28a1671d
15 changed files with 142 additions and 421 deletions
+10 -1
View File
@@ -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" },
+15 -1
View File
@@ -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);
@@ -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() {
</div>
{providerInfo.deprecated && (
<div className="flex items-start gap-2 px-3 py-2 rounded-lg bg-black/[0.02] dark:bg-white/[0.02] border border-black/[0.05] dark:border-white/[0.05]">
<span className="material-symbols-outlined text-[16px] text-text-muted mt-0.5 shrink-0">info</span>
<p className="text-xs text-text-muted leading-relaxed">{providerInfo.deprecationNotice}</p>
<div className="flex items-center gap-2 px-3 py-2 rounded-lg bg-yellow-500/10 border border-yellow-500/30">
<span className="material-symbols-outlined text-[16px] text-yellow-500 mt-0.5 shrink-0">warning</span>
<p className="text-xs text-red-600 dark:text-yellow-400 leading-relaxed">{providerInfo.deprecationNotice}</p>
</div>
)}
{providerInfo.notice && !providerInfo.deprecated && (
<div className="flex items-center gap-2 px-3 py-2 rounded-lg bg-black/[0.02] dark:bg-white/[0.02] border border-black/[0.05] dark:border-white/[0.05]">
<span className="material-symbols-outlined text-[16px] text-text-muted shrink-0">info</span>
<p className="text-xs text-text-muted leading-relaxed">{providerInfo.notice.text}</p>
<div className="flex items-center gap-2 px-3 py-2 rounded-lg bg-blue-500/10 border border-blue-500/30">
<span className="material-symbols-outlined text-[16px] text-blue-500 shrink-0">info</span>
<p className="text-xs text-blue-600 dark:text-blue-400 leading-relaxed">{providerInfo.notice.text}</p>
{providerInfo.notice.apiKeyUrl && (
<a
href={providerInfo.notice.apiKeyUrl}
target="_blank"
rel="noopener noreferrer"
className="text-xs text-primary hover:underline shrink-0"
className="text-xs font-medium text-white bg-blue-500 hover:bg-blue-600 px-2 py-0.5 rounded shrink-0 transition-colors"
>
Get API Key
</a>
@@ -826,26 +857,43 @@ export default function ProviderDetailPage() {
<Card>
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-semibold">Connections</h2>
{/* Round Robin toggle */}
<div className="flex items-center gap-2">
<span className="text-xs text-text-muted font-medium">Round Robin</span>
<Toggle
checked={providerStrategy === "round-robin"}
onChange={handleRoundRobinToggle}
/>
{providerStrategy === "round-robin" && (
<div className="flex items-center gap-1.5">
<span className="text-xs text-text-muted">Sticky:</span>
<input
type="number"
min={1}
value={providerStickyLimit}
onChange={(e) => 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"
/>
<div className="flex items-center gap-4">
{/* Thinking config */}
{/* {thinkingConfig && (
<div className="flex items-center gap-2">
<span className="text-xs text-text-muted font-medium">Thinking</span>
<select
value={thinkingMode}
onChange={(e) => handleThinkingModeChange(e.target.value)}
className="text-xs px-2 py-1 border border-border rounded-md bg-background focus:outline-none focus:border-primary"
>
{thinkingConfig.options.map((opt) => (
<option key={opt} value={opt}>{opt.charAt(0).toUpperCase() + opt.slice(1)}</option>
))}
</select>
</div>
)}
)} */}
{/* Round Robin toggle */}
<div className="flex items-center gap-2">
<span className="text-xs text-text-muted font-medium">Round Robin</span>
<Toggle
checked={providerStrategy === "round-robin"}
onChange={handleRoundRobinToggle}
/>
{providerStrategy === "round-robin" && (
<div className="flex items-center gap-1.5">
<span className="text-xs text-text-muted">Sticky:</span>
<input
type="number"
min={1}
value={providerStickyLimit}
onChange={(e) => 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"
/>
</div>
)}
</div>
</div>
</div>
-25
View File
@@ -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 });
}
}
-43
View File
@@ -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 });
}
}
-25
View File
@@ -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 });
}
-35
View File
@@ -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);
}
+1 -1
View File
@@ -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}`);
+15 -2
View File
@@ -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}` })
},
+1 -1
View File
@@ -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}`);
+1 -1
View File
@@ -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}`);
-253
View File
@@ -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(
<div className="fixed inset-0 z-[9999] flex items-center justify-center p-4">
<div className="absolute inset-0 bg-black/60 backdrop-blur-sm" onClick={onClose} />
<div className="relative rounded-xl overflow-hidden shadow-2xl animate-in fade-in zoom-in-95 duration-200" style={{ width: 480, height: "90vh" }}>
<iframe
src="http://localhost:2208"
className="border-0 block w-full h-full"
title="9Remote UI"
/>
</div>
</div>,
document.body
);
}
// Error state
if (state === S.ERROR) {
return createPortal(
<div className="fixed inset-0 z-[9999] flex items-center justify-center p-4">
<div className="absolute inset-0 bg-black/60 backdrop-blur-sm" onClick={onClose} />
<div className="relative w-full max-w-sm rounded-xl overflow-hidden shadow-2xl bg-surface border border-black/10 dark:border-white/10 animate-in fade-in zoom-in-95 duration-200">
<div className="flex items-center justify-between px-5 py-4 border-b border-black/5 dark:border-white/5">
<span className="text-sm font-semibold text-text-main">9Remote</span>
<button onClick={onClose} className="w-7 h-7 flex items-center justify-center rounded-lg bg-black/5 dark:bg-white/5 border border-black/5 dark:border-white/10 text-text-muted hover:text-text-main transition-colors">
<span className="material-symbols-outlined text-base">close</span>
</button>
</div>
<div className="flex flex-col items-center gap-5 py-12 px-6 text-center">
<div className="w-14 h-14 rounded-full bg-red-500/10 flex items-center justify-center">
<span className="material-symbols-outlined text-red-400 text-[28px]">error</span>
</div>
<div className="space-y-1">
<p className="font-semibold text-text-main">Something went wrong</p>
<p className="text-xs text-text-muted font-mono break-all">{errorMsg}</p>
</div>
<button onClick={checkAndInit} className="flex items-center gap-2 px-4 py-2 rounded-lg bg-black/5 dark:bg-white/5 border border-black/5 dark:border-white/10 text-sm text-text-main hover:bg-black/10 dark:hover:bg-white/10 transition-colors">
<span className="material-symbols-outlined text-base">refresh</span>
Retry
</button>
</div>
</div>
</div>,
document.body
);
}
// Main card — layout cố định, chỉ đổi button
return createPortal(
<div className="fixed inset-0 z-[9999] flex items-center justify-center p-4">
<div className="absolute inset-0 bg-black/60 backdrop-blur-sm" onClick={onClose} />
<div className="relative w-full max-w-sm rounded-xl overflow-hidden shadow-2xl animate-in fade-in zoom-in-95 duration-200 flex flex-col bg-surface border border-black/10 dark:border-white/10"
style={{ minHeight: "90vh" }}
>
{/* Header */}
<div className="flex items-center justify-between px-5 py-4 border-b border-black/5 dark:border-white/5">
<div className="flex items-center gap-3">
<div className="w-7 h-7 rounded-lg flex items-center justify-center" style={{ background: "#FF570A" }}>
<span className="material-symbols-outlined text-white text-base">terminal</span>
</div>
<span className="text-xs font-bold uppercase tracking-wider" style={{ fontFamily: "monospace", color: "#FF570A" }}>9Remote</span>
</div>
<button onClick={onClose} className="w-7 h-7 flex items-center justify-center rounded-lg bg-black/5 dark:bg-white/5 border border-black/5 dark:border-white/10 text-text-muted hover:text-text-main transition-colors">
<span className="material-symbols-outlined text-base">close</span>
</button>
</div>
{/* Body */}
<div className="px-7 py-7 pb-9 flex flex-col flex-1 justify-between">
{/* Hero */}
<div className="flex flex-col items-center gap-2 text-center mt-2">
<div
className="w-14 h-14 rounded-2xl flex items-center justify-center mb-1"
style={{ background: "#FF570A", boxShadow: "rgba(255,87,10,0.35) 0px 8px 32px" }}
>
<span className="material-symbols-outlined text-white" style={{ fontSize: 30 }}>terminal</span>
</div>
<h1 className="text-lg font-bold text-text-main tracking-tight">9Remote</h1>
<p className="text-xs text-text-muted leading-5 max-w-[220px]">
Access your terminal, desktop &amp; files from anywhere
</p>
</div>
{/* Feature cards */}
<div className="flex gap-2 w-full mt-6">
{FEATURES.map(({ icon, label, desc }) => (
<div key={label} className="flex-1 flex flex-col items-center gap-1.5 py-4 px-1 rounded-xl border border-black/10 dark:border-white/10 bg-bg-alt">
<span className="material-symbols-outlined" style={{ fontSize: 22, color: "#ff6e33" }}>{icon}</span>
<p className="text-xs font-semibold text-text-main">{label}</p>
<p className="text-[10px] text-text-muted text-center leading-4">{desc}</p>
</div>
))}
</div>
{/* Bullets */}
<div className="flex flex-col gap-3 w-full mt-5">
{BULLETS.map(({ icon, text }) => (
<div key={icon} className="flex items-center gap-2.5">
<span className="material-symbols-outlined flex-shrink-0" style={{ fontSize: 16, color: "#ff6e33" }}>{icon}</span>
<span className="text-xs text-text-muted">{text}</span>
</div>
))}
</div>
{/* CTA button */}
{btnCfg && (
<button
onClick={btnCfg.onClick ?? undefined}
disabled={btnCfg.loading}
className={`mt-7 w-full py-3.5 flex items-center justify-center gap-2 text-sm font-semibold text-white rounded-xl transition-all ${
btnCfg.loading
? "opacity-60 cursor-not-allowed"
: "hover:opacity-90 active:scale-[0.98]"
}`}
style={{
background: "#FF570A",
boxShadow: btnCfg.loading ? "none" : "0 4px 16px rgba(255,87,10,0.35)",
}}
>
{btnCfg.loading ? (
<span className="flex items-center gap-1">
<span className="w-1.5 h-1.5 rounded-full bg-white animate-bounce" style={{ animationDelay: "0ms" }} />
<span className="w-1.5 h-1.5 rounded-full bg-white animate-bounce" style={{ animationDelay: "150ms" }} />
<span className="w-1.5 h-1.5 rounded-full bg-white animate-bounce" style={{ animationDelay: "300ms" }} />
</span>
) : (
<span className="material-symbols-outlined text-base">{btnCfg.icon}</span>
)}
{btnCfg.label}
</button>
)}
</div>
</div>
</div>,
document.body
);
}
-1
View File
@@ -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";
+22 -5
View File
@@ -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"] },
+2
View File
@@ -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) => {