- {apiKeys.length > 0 ? (
+ {apiKeys.length > 0 || selectedApiKey ? (
) : (
@@ -393,4 +403,3 @@ export default function ClaudeToolCard({
);
}
-
diff --git a/src/app/(dashboard)/dashboard/cli-tools/components/CodexToolCard.js b/src/app/(dashboard)/dashboard/cli-tools/components/CodexToolCard.js
index c75ed18a..44b3cd76 100644
--- a/src/app/(dashboard)/dashboard/cli-tools/components/CodexToolCard.js
+++ b/src/app/(dashboard)/dashboard/cli-tools/components/CodexToolCard.js
@@ -3,6 +3,7 @@
import { useState, useEffect } from "react";
import { Card, Button, ModelSelectModal, ManualConfigModal } from "@/shared/components";
import Image from "next/image";
+import EndpointPresetControl from "./EndpointPresetControl";
export default function CodexToolCard({ tool, isExpanded, onToggle, baseUrl, apiKeys, activeProviders, cloudEnabled, initialStatus }) {
const [codexStatus, setCodexStatus] = useState(initialStatus || null);
@@ -76,6 +77,7 @@ export default function CodexToolCard({ tool, isExpanded, onToggle, baseUrl, api
};
const getDisplayUrl = () => customBaseUrl || `${baseUrl}/v1`;
+ const hasCustomSelectedApiKey = selectedApiKey && !apiKeys.some((key) => key.key === selectedApiKey);
const checkCodexStatus = async () => {
setCheckingCodex(true);
@@ -279,6 +281,13 @@ model = "${effectiveSubagentModel}"
) : null;
})()}
+
Base URL
@@ -301,8 +310,9 @@ model = "${effectiveSubagentModel}"
API Key
arrow_forward
- {apiKeys.length > 0 ? (
+ {apiKeys.length > 0 || selectedApiKey ? (
) : (
diff --git a/src/app/(dashboard)/dashboard/cli-tools/components/CopilotToolCard.js b/src/app/(dashboard)/dashboard/cli-tools/components/CopilotToolCard.js
index b223fde1..192723cb 100644
--- a/src/app/(dashboard)/dashboard/cli-tools/components/CopilotToolCard.js
+++ b/src/app/(dashboard)/dashboard/cli-tools/components/CopilotToolCard.js
@@ -3,6 +3,7 @@
import { useState, useEffect } from "react";
import { Card, Button, ModelSelectModal, ManualConfigModal } from "@/shared/components";
import Image from "next/image";
+import EndpointPresetControl from "./EndpointPresetControl";
export default function CopilotToolCard({ tool, isExpanded, onToggle, baseUrl, apiKeys, activeProviders, cloudEnabled, initialStatus }) {
const [status, setStatus] = useState(initialStatus || null);
@@ -11,6 +12,7 @@ export default function CopilotToolCard({ tool, isExpanded, onToggle, baseUrl, a
const [restoring, setRestoring] = useState(false);
const [message, setMessage] = useState(null);
const [selectedApiKey, setSelectedApiKey] = useState("");
+ const [customBaseUrl, setCustomBaseUrl] = useState("");
const [modelAliases, setModelAliases] = useState({});
const [showManualConfigModal, setShowManualConfigModal] = useState(false);
@@ -66,7 +68,11 @@ export default function CopilotToolCard({ tool, isExpanded, onToggle, baseUrl, a
};
const configStatus = getConfigStatus();
- const getEffectiveBaseUrl = () => baseUrl.endsWith("/v1") ? baseUrl : `${baseUrl}/v1`;
+ const getEffectiveBaseUrl = () => {
+ const url = customBaseUrl || baseUrl;
+ return url.endsWith("/v1") ? url : `${url}/v1`;
+ };
+ const hasCustomSelectedApiKey = selectedApiKey && !apiKeys.some((key) => key.key === selectedApiKey);
const addModel = () => {
const val = modelInput.trim();
@@ -200,11 +206,30 @@ export default function CopilotToolCard({ tool, isExpanded, onToggle, baseUrl, a
+
+
+
+
+ setCustomBaseUrl(e.target.value)}
+ placeholder="https://.../v1"
+ className="px-3 py-2 bg-bg-secondary rounded-lg text-sm border border-border focus:outline-none focus:ring-1 focus:ring-primary/50"
+ />
+
+
{/* API Key */}
- {apiKeys.length > 0 ? (
+ {apiKeys.length > 0 || selectedApiKey ? (
) : (
diff --git a/src/app/(dashboard)/dashboard/cli-tools/components/DroidToolCard.js b/src/app/(dashboard)/dashboard/cli-tools/components/DroidToolCard.js
index 6698f11a..528de780 100644
--- a/src/app/(dashboard)/dashboard/cli-tools/components/DroidToolCard.js
+++ b/src/app/(dashboard)/dashboard/cli-tools/components/DroidToolCard.js
@@ -3,6 +3,7 @@
import { useState, useEffect, useRef } from "react";
import { Card, Button, ModelSelectModal, ManualConfigModal } from "@/shared/components";
import Image from "next/image";
+import EndpointPresetControl from "./EndpointPresetControl";
const CLOUD_URL = process.env.NEXT_PUBLIC_CLOUD_URL;
@@ -116,6 +117,7 @@ export default function DroidToolCard({
const url = customBaseUrl || baseUrl;
return url.endsWith("/v1") ? url : `${url}/v1`;
};
+ const hasCustomSelectedApiKey = selectedApiKey && !apiKeys.some((key) => key.key === selectedApiKey);
const addModel = () => {
const val = modelInput.trim();
@@ -296,6 +298,13 @@ export default function DroidToolCard({
)}
+
+
{/* Base URL */}
Base URL
@@ -318,8 +327,9 @@ export default function DroidToolCard({
API Key
arrow_forward
- {apiKeys.length > 0 ? (
+ {apiKeys.length > 0 || selectedApiKey ? (
) : (
@@ -415,4 +425,4 @@ export default function DroidToolCard({
/>
);
-}
\ No newline at end of file
+}
diff --git a/src/app/(dashboard)/dashboard/cli-tools/components/EndpointPresetControl.js b/src/app/(dashboard)/dashboard/cli-tools/components/EndpointPresetControl.js
new file mode 100644
index 00000000..d374d19a
--- /dev/null
+++ b/src/app/(dashboard)/dashboard/cli-tools/components/EndpointPresetControl.js
@@ -0,0 +1,128 @@
+"use client";
+
+import { useEffect, useMemo, useState } from "react";
+
+const STORAGE_KEY = "9router.cliToolEndpointPresets";
+
+function maskApiKey(apiKey) {
+ if (!apiKey) return "No API key";
+ if (apiKey.length <= 12) return `${apiKey.slice(0, 4)}...`;
+ return `${apiKey.slice(0, 8)}...${apiKey.slice(-4)}`;
+}
+
+function normalizePresets(value) {
+ if (!Array.isArray(value)) return [];
+ return value.filter((preset) => preset?.name && preset?.baseUrl && preset?.apiKey);
+}
+
+function readPresets() {
+ if (typeof window === "undefined") return [];
+ try {
+ return normalizePresets(JSON.parse(window.localStorage.getItem(STORAGE_KEY) || "[]"));
+ } catch {
+ return [];
+ }
+}
+
+function writePresets(presets) {
+ if (typeof window === "undefined") return;
+ window.localStorage.setItem(STORAGE_KEY, JSON.stringify(normalizePresets(presets)));
+}
+
+export default function EndpointPresetControl({
+ baseUrl,
+ apiKey,
+ onBaseUrlChange,
+ onApiKeyChange,
+}) {
+ const [presets, setPresets] = useState([]);
+ const [selectedName, setSelectedName] = useState("");
+
+ useEffect(() => {
+ setPresets(readPresets());
+ }, []);
+
+ const selectedPreset = useMemo(
+ () => presets.find((preset) => preset.name === selectedName) || null,
+ [presets, selectedName]
+ );
+
+ const handleSelect = (name) => {
+ setSelectedName(name);
+ const preset = presets.find((item) => item.name === name);
+ if (!preset) return;
+ onBaseUrlChange(preset.baseUrl);
+ onApiKeyChange(preset.apiKey);
+ };
+
+ const handleSave = () => {
+ const trimmedBaseUrl = (baseUrl || "").trim();
+ const trimmedApiKey = (apiKey || "").trim();
+ if (!trimmedBaseUrl || !trimmedApiKey) return;
+
+ let defaultName = selectedPreset?.name || trimmedBaseUrl;
+ try {
+ defaultName = selectedPreset?.name || new URL(trimmedBaseUrl).host;
+ } catch {
+ defaultName = selectedPreset?.name || trimmedBaseUrl;
+ }
+ const name = window.prompt("Preset name", defaultName);
+ if (!name?.trim()) return;
+
+ const nextPreset = { name: name.trim(), baseUrl: trimmedBaseUrl, apiKey: trimmedApiKey };
+ const nextPresets = [
+ ...presets.filter((preset) => preset.name !== nextPreset.name),
+ nextPreset,
+ ].sort((a, b) => a.name.localeCompare(b.name));
+
+ setPresets(nextPresets);
+ setSelectedName(nextPreset.name);
+ writePresets(nextPresets);
+ };
+
+ const handleDelete = () => {
+ if (!selectedPreset) return;
+ const nextPresets = presets.filter((preset) => preset.name !== selectedPreset.name);
+ setPresets(nextPresets);
+ setSelectedName("");
+ writePresets(nextPresets);
+ };
+
+ return (
+
+ Preset
+ arrow_forward
+
+
+ {selectedPreset && (
+
+ )}
+
+ );
+}
diff --git a/src/app/(dashboard)/dashboard/cli-tools/components/HermesToolCard.js b/src/app/(dashboard)/dashboard/cli-tools/components/HermesToolCard.js
index 3ed3ac79..3371aff3 100644
--- a/src/app/(dashboard)/dashboard/cli-tools/components/HermesToolCard.js
+++ b/src/app/(dashboard)/dashboard/cli-tools/components/HermesToolCard.js
@@ -3,6 +3,7 @@
import { useState, useEffect, useRef } from "react";
import { Card, Button, ModelSelectModal, ManualConfigModal } from "@/shared/components";
import Image from "next/image";
+import EndpointPresetControl from "./EndpointPresetControl";
const ENDPOINT = "/api/cli-tools/hermes-settings";
@@ -104,6 +105,7 @@ export default function HermesToolCard({
const url = customBaseUrl || getLocalBaseUrl();
return url.endsWith("/v1") ? url : `${url}/v1`;
};
+ const hasCustomSelectedApiKey = selectedApiKey && !apiKeys.some((key) => key.key === selectedApiKey);
const handleApply = async () => {
setApplying(true);
@@ -237,6 +239,13 @@ export default function HermesToolCard({
)}
+
+
Base URL
arrow_forward
@@ -257,8 +266,9 @@ export default function HermesToolCard({
API Key
arrow_forward
- {apiKeys.length > 0 ? (
+ {apiKeys.length > 0 || selectedApiKey ? (
) : (
diff --git a/src/app/(dashboard)/dashboard/cli-tools/components/OpenClawToolCard.js b/src/app/(dashboard)/dashboard/cli-tools/components/OpenClawToolCard.js
index bc786992..61ef8490 100644
--- a/src/app/(dashboard)/dashboard/cli-tools/components/OpenClawToolCard.js
+++ b/src/app/(dashboard)/dashboard/cli-tools/components/OpenClawToolCard.js
@@ -3,6 +3,7 @@
import { useState, useEffect, useRef } from "react";
import { Card, Button, ModelSelectModal, ManualConfigModal } from "@/shared/components";
import Image from "next/image";
+import EndpointPresetControl from "./EndpointPresetControl";
export default function OpenClawToolCard({
tool,
@@ -122,6 +123,7 @@ export default function OpenClawToolCard({
const url = customBaseUrl || getLocalBaseUrl();
return url.endsWith("/v1") ? url : `${url}/v1`;
};
+ const hasCustomSelectedApiKey = selectedApiKey && !apiKeys.some((key) => key.key === selectedApiKey);
const handleApplySettings = async () => {
setApplying(true);
@@ -287,6 +289,13 @@ export default function OpenClawToolCard({
)}
+
+
{/* Base URL */}
Base URL
@@ -309,8 +318,9 @@ export default function OpenClawToolCard({
API Key
arrow_forward
- {apiKeys.length > 0 ? (
+ {apiKeys.length > 0 || selectedApiKey ? (
) : (
diff --git a/src/app/(dashboard)/dashboard/cli-tools/components/OpenCodeToolCard.js b/src/app/(dashboard)/dashboard/cli-tools/components/OpenCodeToolCard.js
index d77d2b72..d006599c 100644
--- a/src/app/(dashboard)/dashboard/cli-tools/components/OpenCodeToolCard.js
+++ b/src/app/(dashboard)/dashboard/cli-tools/components/OpenCodeToolCard.js
@@ -3,6 +3,7 @@
import { useState, useEffect } from "react";
import { Card, Button, ModelSelectModal, ManualConfigModal } from "@/shared/components";
import Image from "next/image";
+import EndpointPresetControl from "./EndpointPresetControl";
export default function OpenCodeToolCard({ tool, isExpanded, onToggle, baseUrl, apiKeys, activeProviders, cloudEnabled, initialStatus }) {
const [status, setStatus] = useState(initialStatus || null);
@@ -81,6 +82,7 @@ export default function OpenCodeToolCard({ tool, isExpanded, onToggle, baseUrl,
};
const getDisplayUrl = () => customBaseUrl || `${baseUrl}/v1`;
+ const hasCustomSelectedApiKey = selectedApiKey && !apiKeys.some((key) => key.key === selectedApiKey);
const checkStatus = async () => {
setChecking(true);
@@ -266,6 +268,13 @@ export default function OpenCodeToolCard({ tool, isExpanded, onToggle, baseUrl,
)}
+
+
{/* Base URL */}
Base URL
@@ -288,8 +297,9 @@ export default function OpenCodeToolCard({ tool, isExpanded, onToggle, baseUrl,
API Key
arrow_forward
- {apiKeys.length > 0 ? (
+ {apiKeys.length > 0 || selectedApiKey ? (
) : (
diff --git a/src/app/(dashboard)/dashboard/cli-tools/components/index.js b/src/app/(dashboard)/dashboard/cli-tools/components/index.js
index 782fcae8..4500800d 100644
--- a/src/app/(dashboard)/dashboard/cli-tools/components/index.js
+++ b/src/app/(dashboard)/dashboard/cli-tools/components/index.js
@@ -10,4 +10,4 @@ export { default as CopilotToolCard } from "./CopilotToolCard";
export { default as MitmServerCard } from "./MitmServerCard";
export { default as MitmToolCard } from "./MitmToolCard";
export { default as MitmLinkCard } from "./MitmLinkCard";
-
+export { default as EndpointPresetControl } from "./EndpointPresetControl";