diff --git a/src/app/(dashboard)/dashboard/cli-tools/CLIToolsPageClient.js b/src/app/(dashboard)/dashboard/cli-tools/CLIToolsPageClient.js index 8d930bf..726ca3d 100644 --- a/src/app/(dashboard)/dashboard/cli-tools/CLIToolsPageClient.js +++ b/src/app/(dashboard)/dashboard/cli-tools/CLIToolsPageClient.js @@ -1,6 +1,6 @@ "use client"; -import { useState, useEffect } from "react"; +import { useState, useEffect, useCallback } from "react"; import { Card, CardSkeleton } from "@/shared/components"; import { CLI_TOOLS } from "@/shared/constants/cliTools"; import { PROVIDER_MODELS, getModelsByProviderId, PROVIDER_ID_TO_ALIAS } from "@/shared/constants/models"; @@ -107,15 +107,21 @@ export default function CLIToolsPageClient({ machineId }) { return models; }; - const handleModelMappingChange = (toolId, modelAlias, targetModel) => { - setModelMappings(prev => ({ - ...prev, - [toolId]: { - ...prev[toolId], - [modelAlias]: targetModel, - }, - })); - }; + const handleModelMappingChange = useCallback((toolId, modelAlias, targetModel) => { + setModelMappings(prev => { + // Prevent unnecessary updates if value hasn't changed + if (prev[toolId]?.[modelAlias] === targetModel) { + return prev; + } + return { + ...prev, + [toolId]: { + ...prev[toolId], + [modelAlias]: targetModel, + }, + }; + }); + }, []); const getBaseUrl = () => { if (cloudEnabled && CLOUD_URL) { diff --git a/src/app/(dashboard)/dashboard/cli-tools/components/ClaudeToolCard.js b/src/app/(dashboard)/dashboard/cli-tools/components/ClaudeToolCard.js index b145c70..85eec0f 100644 --- a/src/app/(dashboard)/dashboard/cli-tools/components/ClaudeToolCard.js +++ b/src/app/(dashboard)/dashboard/cli-tools/components/ClaudeToolCard.js @@ -1,6 +1,6 @@ "use client"; -import { useState, useEffect } from "react"; +import { useState, useEffect, useRef } from "react"; import { Card, Button, ModelSelectModal, ManualConfigModal } from "@/shared/components"; import Image from "next/image"; @@ -29,6 +29,8 @@ export default function ClaudeToolCard({ const [selectedApiKey, setSelectedApiKey] = useState(""); const [modelAliases, setModelAliases] = useState({}); const [showManualConfigModal, setShowManualConfigModal] = useState(false); + const [customBaseUrl, setCustomBaseUrl] = useState(""); + const hasInitializedModels = useRef(false); const getConfigStatus = () => { if (!claudeStatus?.installed) return null; @@ -66,12 +68,17 @@ export default function ClaudeToolCard({ }; useEffect(() => { - if (claudeStatus?.installed) { + if (claudeStatus?.installed && !hasInitializedModels.current) { + hasInitializedModels.current = true; const env = claudeStatus.settings?.env || {}; + tool.defaultModels.forEach((model) => { if (model.envKey) { const value = env[model.envKey] || model.defaultValue || ""; - if (value) onModelMappingChange(model.alias, value); + // Only sync initial values from file once + if (value) { + onModelMappingChange(model.alias, value); + } } }); // Only set selectedApiKey if it exists in apiKeys list @@ -95,11 +102,13 @@ export default function ClaudeToolCard({ } }; + const getEffectiveBaseUrl = () => customBaseUrl || baseUrl; + const handleApplySettings = async () => { setApplying(true); setMessage(null); try { - const env = { ANTHROPIC_BASE_URL: baseUrl }; + const env = { ANTHROPIC_BASE_URL: getEffectiveBaseUrl() }; // Get key from dropdown, fallback to first key or sk_9router for localhost const keyToUse = selectedApiKey?.trim() @@ -167,7 +176,7 @@ export default function ClaudeToolCard({ const keyToUse = (selectedApiKey && selectedApiKey.trim()) ? selectedApiKey : (!cloudEnabled ? "sk_9router" : ""); - const env = { ANTHROPIC_BASE_URL: baseUrl, ANTHROPIC_AUTH_TOKEN: keyToUse }; + const env = { ANTHROPIC_BASE_URL: getEffectiveBaseUrl(), ANTHROPIC_AUTH_TOKEN: keyToUse }; tool.defaultModels.forEach((model) => { const targetModel = modelMappings[model.alias]; if (targetModel && model.envKey) env[model.envKey] = targetModel; @@ -240,26 +249,41 @@ export default function ClaudeToolCard({ {!checkingClaude && claudeStatus?.installed && ( <> -
- check_circle - URL: - {baseUrl} -
- -
- Key: - {apiKeys.length > 0 ? ( - - ) : ( - - {cloudEnabled ? "No API keys - Create one in Keys page" : "sk_9router"} - - )} -
-
+ {/* Base URL */} +
+ Base URL + arrow_forward + setCustomBaseUrl(e.target.value)} + placeholder="https://..." + className="flex-1 px-2 py-1.5 bg-surface rounded border border-border text-xs focus:outline-none focus:ring-1 focus:ring-primary/50" + /> + {customBaseUrl && customBaseUrl !== baseUrl && ( + + )} +
+ + {/* API Key */} +
+ API Key + arrow_forward + {apiKeys.length > 0 ? ( + + ) : ( + + {cloudEnabled ? "No API keys - Create one in Keys page" : "sk_9router (default)"} + + )} +
+ + {/* Model Mappings */} {tool.defaultModels.map((model) => (
{model.name} diff --git a/src/app/(dashboard)/dashboard/cli-tools/components/CodexToolCard.js b/src/app/(dashboard)/dashboard/cli-tools/components/CodexToolCard.js index 5e7aa7a..ecb1f05 100644 --- a/src/app/(dashboard)/dashboard/cli-tools/components/CodexToolCard.js +++ b/src/app/(dashboard)/dashboard/cli-tools/components/CodexToolCard.js @@ -16,6 +16,7 @@ export default function CodexToolCard({ tool, isExpanded, onToggle, baseUrl, api const [modalOpen, setModalOpen] = useState(false); const [modelAliases, setModelAliases] = useState({}); const [showManualConfigModal, setShowManualConfigModal] = useState(false); + const [customBaseUrl, setCustomBaseUrl] = useState(""); useEffect(() => { if (apiKeys?.length > 0 && !selectedApiKey) { @@ -40,7 +41,7 @@ export default function CodexToolCard({ tool, isExpanded, onToggle, baseUrl, api } }; - // Parse model from config content + // Parse model from config content (don't sync URL - always use baseUrl from props) useEffect(() => { if (codexStatus?.config) { const modelMatch = codexStatus.config.match(/^model\s*=\s*"([^"]+)"/m); @@ -57,6 +58,14 @@ export default function CodexToolCard({ tool, isExpanded, onToggle, baseUrl, api const configStatus = getConfigStatus(); + const getEffectiveBaseUrl = () => { + const url = customBaseUrl || `${baseUrl}/v1`; + // Ensure URL ends with /v1 + return url.endsWith("/v1") ? url : `${url}/v1`; + }; + + const getDisplayUrl = () => customBaseUrl || `${baseUrl}/v1`; + const checkCodexStatus = async () => { setCheckingCodex(true); try { @@ -82,7 +91,7 @@ export default function CodexToolCard({ tool, isExpanded, onToggle, baseUrl, api const res = await fetch("/api/cli-tools/codex-settings", { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ baseUrl, apiKey: keyToUse, model: selectedModel }), + body: JSON.stringify({ baseUrl: getEffectiveBaseUrl(), apiKey: keyToUse, model: selectedModel }), }); const data = await res.json(); if (res.ok) { @@ -134,7 +143,7 @@ model_provider = "9router" [model_providers.9router] name = "9Router" -base_url = "${baseUrl}/v1" +base_url = "${getEffectiveBaseUrl()}" wire_api = "responses" `; @@ -219,31 +228,48 @@ wire_api = "responses" {!checkingCodex && codexStatus?.installed && ( <> -
- check_circle - URL: - {baseUrl}/v1 -
+
+ {/* Base URL */} +
+ Base URL + arrow_forward + setCustomBaseUrl(e.target.value)} + placeholder="https://.../v1" + className="flex-1 px-2 py-1.5 bg-surface rounded border border-border text-xs focus:outline-none focus:ring-1 focus:ring-primary/50" + /> + {customBaseUrl && customBaseUrl !== `${baseUrl}/v1` && ( + + )} +
-
- Key: - {apiKeys.length > 0 ? ( - - ) : ( - - {cloudEnabled ? "No API keys - Create one in Keys page" : "sk_9router"} - - )} -
+ {/* API Key */} +
+ API Key + arrow_forward + {apiKeys.length > 0 ? ( + + ) : ( + + {cloudEnabled ? "No API keys - Create one in Keys page" : "sk_9router (default)"} + + )} +
-
- Model - arrow_forward - setSelectedModel(e.target.value)} placeholder="provider/model-id" className="flex-1 px-2 py-1.5 bg-surface rounded border border-border text-xs focus:outline-none focus:ring-1 focus:ring-primary/50" /> - - {selectedModel && } + {/* Model */} +
+ Model + arrow_forward + setSelectedModel(e.target.value)} placeholder="provider/model-id" className="flex-1 px-2 py-1.5 bg-surface rounded border border-border text-xs focus:outline-none focus:ring-1 focus:ring-primary/50" /> + + {selectedModel && } +
{message && ( diff --git a/src/app/(dashboard)/dashboard/cli-tools/components/DefaultToolCard.js b/src/app/(dashboard)/dashboard/cli-tools/components/DefaultToolCard.js index 172c251..ad58149 100644 --- a/src/app/(dashboard)/dashboard/cli-tools/components/DefaultToolCard.js +++ b/src/app/(dashboard)/dashboard/cli-tools/components/DefaultToolCard.js @@ -19,8 +19,14 @@ export default function DefaultToolCard({ toolId, tool, isExpanded, onToggle, ba ? selectedApiKey : (!cloudEnabled ? "sk_9router" : "your-api-key"); + // Add /v1 suffix only if not already present (DRY - avoid duplicate) + const normalizedBaseUrl = baseUrl || "http://localhost:3000"; + const baseUrlWithV1 = normalizedBaseUrl.endsWith("/v1") + ? normalizedBaseUrl + : `${normalizedBaseUrl}/v1`; + return text - .replace(/\{\{baseUrl\}\}/g, baseUrl || "http://localhost:3000") + .replace(/\{\{baseUrl\}\}/g, baseUrlWithV1) .replace(/\{\{apiKey\}\}/g, keyToUse) .replace(/\{\{model\}\}/g, modelValue || "provider/model-id"); }; diff --git a/src/shared/constants/cliTools.js b/src/shared/constants/cliTools.js index 72cb8da..18e5755 100644 --- a/src/shared/constants/cliTools.js +++ b/src/shared/constants/cliTools.js @@ -45,7 +45,7 @@ export const CLI_TOOLS = { guideSteps: [ { step: 1, title: "Open Settings", desc: "Go to Settings → Models" }, { step: 2, title: "Enable OpenAI API", desc: "Enable \"OpenAI API key\" option" }, - { step: 3, title: "Base URL", value: "{{baseUrl}}/v1", copyable: true }, + { step: 3, title: "Base URL", value: "{{baseUrl}}", copyable: true }, { step: 4, title: "API Key", type: "apiKeySelector" }, { step: 5, title: "Add Custom Model", desc: "Click \"View All Model\" → \"Add Custom Model\"" }, { step: 6, title: "Select Model", type: "modelSelector" },