Enhance provider models and chat handling with new thinking configurations
This commit is contained in:
@@ -221,7 +221,16 @@ export const PROVIDER_MODELS = {
|
|||||||
{ id: "text-embedding-005", name: "Text Embedding 005", type: "embedding" },
|
{ id: "text-embedding-005", name: "Text Embedding 005", type: "embedding" },
|
||||||
{ id: "text-embedding-004", name: "Text Embedding 004 (Legacy)", 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: [
|
glm: [
|
||||||
{ id: "glm-5.1", name: "GLM 5.1" },
|
{ id: "glm-5.1", name: "GLM 5.1" },
|
||||||
{ id: "glm-5", name: "GLM 5" },
|
{ id: "glm-5", name: "GLM 5" },
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import { detectClientTool, isNativePassthrough } from "../utils/clientDetector.j
|
|||||||
* @param {object} options.credentials - Provider credentials
|
* @param {object} options.credentials - Provider credentials
|
||||||
* @param {string} options.sourceFormatOverride - Override detected source format (e.g. "openai-responses")
|
* @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 { provider, model } = modelInfo;
|
||||||
const requestStartTime = Date.now();
|
const requestStartTime = Date.now();
|
||||||
|
|
||||||
@@ -39,6 +39,20 @@ export async function handleChatCore({ body, modelInfo, credentials, log, onCred
|
|||||||
const targetFormat = modelTargetFormat || getTargetFormat(provider);
|
const targetFormat = modelTargetFormat || getTargetFormat(provider);
|
||||||
const stripList = getModelStrip(alias, model);
|
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 clientRequestedStreaming = body.stream === true || sourceFormat === FORMATS.ANTIGRAVITY || sourceFormat === FORMATS.GEMINI || sourceFormat === FORMATS.GEMINI_CLI;
|
||||||
const providerRequiresStreaming = provider === "openai" || provider === "codex";
|
const providerRequiresStreaming = provider === "openai" || provider === "codex";
|
||||||
let stream = providerRequiresStreaming ? true : (body.stream !== false);
|
let stream = providerRequiresStreaming ? true : (body.stream !== false);
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { useParams, useRouter } from "next/navigation";
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { Card, Button, Badge, Input, Modal, CardSkeleton, OAuthModal, KiroOAuthWrapper, CursorAuthModal, IFlowCookieModal, GitLabAuthModal, Toggle, Select, EditConnectionModal } from "@/shared/components";
|
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 { getModelsByProviderId } from "@/shared/constants/models";
|
||||||
import { useCopyToClipboard } from "@/shared/hooks/useCopyToClipboard";
|
import { useCopyToClipboard } from "@/shared/hooks/useCopyToClipboard";
|
||||||
import { fetchSuggestedModels } from "@/shared/utils/providerModelsFetcher";
|
import { fetchSuggestedModels } from "@/shared/utils/providerModelsFetcher";
|
||||||
@@ -37,6 +37,7 @@ export default function ProviderDetailPage() {
|
|||||||
const [bulkUpdatingProxy, setBulkUpdatingProxy] = useState(false);
|
const [bulkUpdatingProxy, setBulkUpdatingProxy] = useState(false);
|
||||||
const [providerStrategy, setProviderStrategy] = useState(null); // null = use global, "round-robin" = override
|
const [providerStrategy, setProviderStrategy] = useState(null); // null = use global, "round-robin" = override
|
||||||
const [providerStickyLimit, setProviderStickyLimit] = useState("");
|
const [providerStickyLimit, setProviderStickyLimit] = useState("");
|
||||||
|
const [thinkingMode, setThinkingMode] = useState("auto");
|
||||||
const [suggestedModels, setSuggestedModels] = useState([]);
|
const [suggestedModels, setSuggestedModels] = useState([]);
|
||||||
const [kiloFreeModels, setKiloFreeModels] = useState([]);
|
const [kiloFreeModels, setKiloFreeModels] = useState([]);
|
||||||
const { copied, copy } = useCopyToClipboard();
|
const { copied, copy } = useCopyToClipboard();
|
||||||
@@ -60,6 +61,7 @@ export default function ProviderDetailPage() {
|
|||||||
const isOpenAICompatible = isOpenAICompatibleProvider(providerId);
|
const isOpenAICompatible = isOpenAICompatibleProvider(providerId);
|
||||||
const isAnthropicCompatible = isAnthropicCompatibleProvider(providerId);
|
const isAnthropicCompatible = isAnthropicCompatibleProvider(providerId);
|
||||||
const isCompatible = isOpenAICompatible || isAnthropicCompatible;
|
const isCompatible = isOpenAICompatible || isAnthropicCompatible;
|
||||||
|
const thinkingConfig = AI_PROVIDERS[providerId]?.thinkingConfig || THINKING_CONFIG.extended;
|
||||||
|
|
||||||
const providerStorageAlias = isCompatible ? providerId : providerAlias;
|
const providerStorageAlias = isCompatible ? providerId : providerAlias;
|
||||||
const providerDisplayAlias = isCompatible
|
const providerDisplayAlias = isCompatible
|
||||||
@@ -111,6 +113,9 @@ export default function ProviderDetailPage() {
|
|||||||
const override = (settingsData.providerStrategies || {})[providerId] || {};
|
const override = (settingsData.providerStrategies || {})[providerId] || {};
|
||||||
setProviderStrategy(override.fallbackStrategy || null);
|
setProviderStrategy(override.fallbackStrategy || null);
|
||||||
setProviderStickyLimit(override.stickyRoundRobinLimit != null ? String(override.stickyRoundRobinLimit) : "1");
|
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) {
|
if (nodesRes.ok) {
|
||||||
let node = (nodesData.nodes || []).find((entry) => entry.id === providerId) || null;
|
let node = (nodesData.nodes || []).find((entry) => entry.id === providerId) || null;
|
||||||
|
|
||||||
@@ -197,6 +202,32 @@ export default function ProviderDetailPage() {
|
|||||||
saveProviderStrategy("round-robin", value);
|
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(() => {
|
useEffect(() => {
|
||||||
fetchConnections();
|
fetchConnections();
|
||||||
fetchAliases();
|
fetchAliases();
|
||||||
@@ -731,22 +762,22 @@ export default function ProviderDetailPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{providerInfo.deprecated && (
|
{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]">
|
<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-text-muted mt-0.5 shrink-0">info</span>
|
<span className="material-symbols-outlined text-[16px] text-yellow-500 mt-0.5 shrink-0">warning</span>
|
||||||
<p className="text-xs text-text-muted leading-relaxed">{providerInfo.deprecationNotice}</p>
|
<p className="text-xs text-red-600 dark:text-yellow-400 leading-relaxed">{providerInfo.deprecationNotice}</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{providerInfo.notice && !providerInfo.deprecated && (
|
{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]">
|
<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-text-muted shrink-0">info</span>
|
<span className="material-symbols-outlined text-[16px] text-blue-500 shrink-0">info</span>
|
||||||
<p className="text-xs text-text-muted leading-relaxed">{providerInfo.notice.text}</p>
|
<p className="text-xs text-blue-600 dark:text-blue-400 leading-relaxed">{providerInfo.notice.text}</p>
|
||||||
{providerInfo.notice.apiKeyUrl && (
|
{providerInfo.notice.apiKeyUrl && (
|
||||||
<a
|
<a
|
||||||
href={providerInfo.notice.apiKeyUrl}
|
href={providerInfo.notice.apiKeyUrl}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
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 →
|
Get API Key →
|
||||||
</a>
|
</a>
|
||||||
@@ -826,6 +857,22 @@ export default function ProviderDetailPage() {
|
|||||||
<Card>
|
<Card>
|
||||||
<div className="flex items-center justify-between mb-4">
|
<div className="flex items-center justify-between mb-4">
|
||||||
<h2 className="text-lg font-semibold">Connections</h2>
|
<h2 className="text-lg font-semibold">Connections</h2>
|
||||||
|
<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 */}
|
{/* Round Robin toggle */}
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="text-xs text-text-muted font-medium">Round Robin</span>
|
<span className="text-xs text-text-muted font-medium">Round Robin</span>
|
||||||
@@ -848,6 +895,7 @@ export default function ProviderDetailPage() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{connections.length === 0 ? (
|
{connections.length === 0 ? (
|
||||||
<div className="text-center py-12">
|
<div className="text-center py-12">
|
||||||
|
|||||||
@@ -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 });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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 });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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 });
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
@@ -8,7 +8,7 @@ async function intercept(req, res, bodyBuffer, mappedModel) {
|
|||||||
try {
|
try {
|
||||||
const body = JSON.parse(bodyBuffer.toString());
|
const body = JSON.parse(bodyBuffer.toString());
|
||||||
body.model = mappedModel;
|
body.model = mappedModel;
|
||||||
const routerRes = await fetchRouter(body);
|
const routerRes = await fetchRouter(body, "/v1/chat/completions", req.headers);
|
||||||
await pipeSSE(routerRes, res);
|
await pipeSSE(routerRes, res);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
err(`[antigravity] ${error.message}`);
|
err(`[antigravity] ${error.message}`);
|
||||||
|
|||||||
@@ -6,13 +6,26 @@ const ROUTER_BASE = String(process.env.MITM_ROUTER_BASE || DEFAULT_LOCAL_ROUTER)
|
|||||||
.replace(/\/+$/, "") || DEFAULT_LOCAL_ROUTER;
|
.replace(/\/+$/, "") || DEFAULT_LOCAL_ROUTER;
|
||||||
const API_KEY = process.env.ROUTER_API_KEY;
|
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}`, {
|
const response = await fetch(`${ROUTER_BASE}${path}`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
|
...forwarded,
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
...(API_KEY && { "Authorization": `Bearer ${API_KEY}` })
|
...(API_KEY && { "Authorization": `Bearer ${API_KEY}` })
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ async function intercept(req, res, bodyBuffer, mappedModel) {
|
|||||||
const body = JSON.parse(bodyBuffer.toString());
|
const body = JSON.parse(bodyBuffer.toString());
|
||||||
body.model = mappedModel;
|
body.model = mappedModel;
|
||||||
const routerPath = resolveRouterPath(req.url);
|
const routerPath = resolveRouterPath(req.url);
|
||||||
const routerRes = await fetchRouter(body, routerPath);
|
const routerRes = await fetchRouter(body, routerPath, req.headers);
|
||||||
await pipeSSE(routerRes, res);
|
await pipeSSE(routerRes, res);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
err(`[copilot] ${error.message}`);
|
err(`[copilot] ${error.message}`);
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ async function intercept(req, res, bodyBuffer, mappedModel) {
|
|||||||
try {
|
try {
|
||||||
const body = JSON.parse(bodyBuffer.toString());
|
const body = JSON.parse(bodyBuffer.toString());
|
||||||
body.model = mappedModel;
|
body.model = mappedModel;
|
||||||
const routerRes = await fetchRouter(body);
|
const routerRes = await fetchRouter(body, "/v1/chat/completions", req.headers);
|
||||||
await pipeSSE(routerRes, res);
|
await pipeSSE(routerRes, res);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
err(`[Kiro] ${error.message}`);
|
err(`[Kiro] ${error.message}`);
|
||||||
|
|||||||
@@ -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 & 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
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -19,7 +19,6 @@ export { default as ManualConfigModal } from "./ManualConfigModal";
|
|||||||
export { default as UsageStats } from "./UsageStats";
|
export { default as UsageStats } from "./UsageStats";
|
||||||
export { default as LanguageSwitcher } from "./LanguageSwitcher";
|
export { default as LanguageSwitcher } from "./LanguageSwitcher";
|
||||||
export { default as NineRemoteButton } from "./NineRemoteButton";
|
export { default as NineRemoteButton } from "./NineRemoteButton";
|
||||||
export { default as NineRemoteModal } from "./NineRemoteModal";
|
|
||||||
export { default as RequestLogger } from "./RequestLogger";
|
export { default as RequestLogger } from "./RequestLogger";
|
||||||
export { default as KiroAuthModal } from "./KiroAuthModal";
|
export { default as KiroAuthModal } from "./KiroAuthModal";
|
||||||
export { default as KiroOAuthWrapper } from "./KiroOAuthWrapper";
|
export { default as KiroOAuthWrapper } from "./KiroOAuthWrapper";
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
export const FREE_PROVIDERS = {
|
export const FREE_PROVIDERS = {
|
||||||
kiro: { id: "kiro", alias: "kr", name: "Kiro AI", icon: "psychology_alt", color: "#FF6B35" },
|
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" },
|
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" },
|
// gitlab: { id: "gitlab", alias: "gl", name: "GitLab Duo", icon: "code", color: "#FC6D26" },
|
||||||
// codebuddy: { id: "codebuddy", alias: "cb", name: "CodeBuddy", icon: "smart_toy", color: "#006EFF" },
|
// 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" },
|
// 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)
|
// Free Tier Providers (has free access but may require account/API key)
|
||||||
export const FREE_TIER_PROVIDERS = {
|
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" } },
|
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" } },
|
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" } },
|
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"] },
|
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
|
// OAuth Providers
|
||||||
export const OAUTH_PROVIDERS = {
|
export const OAUTH_PROVIDERS = {
|
||||||
claude: { id: "claude", alias: "cc", name: "Claude Code", icon: "smart_toy", color: "#D97757" },
|
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." },
|
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" },
|
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" },
|
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" },
|
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" },
|
// "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" },
|
"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: { 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" },
|
"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"] },
|
anthropic: { id: "anthropic", alias: "anthropic", name: "Anthropic", icon: "smart_toy", color: "#D97757", textIcon: "AN", website: "https://console.anthropic.com", serviceKinds: ["llm"] },
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -189,6 +189,7 @@ async function handleSingleModelChat(body, modelStr, clientRawRequest = null, re
|
|||||||
|
|
||||||
// Use shared chatCore
|
// Use shared chatCore
|
||||||
const chatSettings = await getSettings();
|
const chatSettings = await getSettings();
|
||||||
|
const providerThinking = (chatSettings.providerThinking || {})[provider] || null;
|
||||||
const result = await handleChatCore({
|
const result = await handleChatCore({
|
||||||
body: { ...body, model: `${provider}/${model}` },
|
body: { ...body, model: `${provider}/${model}` },
|
||||||
modelInfo: { provider, model },
|
modelInfo: { provider, model },
|
||||||
@@ -199,6 +200,7 @@ async function handleSingleModelChat(body, modelStr, clientRawRequest = null, re
|
|||||||
userAgent,
|
userAgent,
|
||||||
apiKey,
|
apiKey,
|
||||||
ccFilterNaming: !!chatSettings.ccFilterNaming,
|
ccFilterNaming: !!chatSettings.ccFilterNaming,
|
||||||
|
providerThinking,
|
||||||
// Detect source format by endpoint + body
|
// Detect source format by endpoint + body
|
||||||
sourceFormatOverride: request?.url ? detectFormatByEndpoint(new URL(request.url).pathname, body) : null,
|
sourceFormatOverride: request?.url ? detectFormatByEndpoint(new URL(request.url).pathname, body) : null,
|
||||||
onCredentialsRefreshed: async (newCreds) => {
|
onCredentialsRefreshed: async (newCreds) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user