diff --git a/open-sse/config/providerModels.js b/open-sse/config/providerModels.js index 74b2576..64b4302 100644 --- a/open-sse/config/providerModels.js +++ b/open-sse/config/providerModels.js @@ -328,6 +328,13 @@ export const PROVIDER_MODELS = { { id: "deepseek/deepseek-chat", name: "DeepSeek Chat" }, { id: "deepseek/deepseek-reasoner", name: "DeepSeek Reasoner" }, ], + oc: [ // OpenCode + { id: "nemotron-3-super-free", name: "Nemotron 3 Super" }, + { id: "qwen3.6-plus-free", name: "Qwen 3.6 Plus" }, + // { id: "big-pickle", name: "Big Pickle", targetFormat: "claude" }, + { id: "minimax-m2.5-free", name: "MiniMax M2.5", targetFormat: "claude" }, + ], + cl: [ // Cline { id: "anthropic/claude-sonnet-4.6", name: "Claude Sonnet 4.6" }, { id: "anthropic/claude-opus-4.6", name: "Claude Opus 4.6" }, @@ -589,6 +596,7 @@ const OAUTH_ALIASES = { "kimi-coding": "kmc", kilocode: "kc", cline: "cl", + opencode: "oc", vertex: "vertex", "vertex-partner": "vertex-partner", }; diff --git a/open-sse/config/providers.js b/open-sse/config/providers.js index 4dd54ab..eb89fd2 100644 --- a/open-sse/config/providers.js +++ b/open-sse/config/providers.js @@ -332,4 +332,9 @@ export const PROVIDERS = { baseUrl: "https://copilot.tencent.com/v1/chat/completions", format: "openai", }, + opencode: { + baseUrl: "https://opencode.ai", + format: "openai", + headers: { "x-opencode-client": "desktop" } + }, }; diff --git a/open-sse/executors/index.js b/open-sse/executors/index.js index 0c314fb..f77212c 100644 --- a/open-sse/executors/index.js +++ b/open-sse/executors/index.js @@ -8,6 +8,7 @@ import { CodexExecutor } from "./codex.js"; import { CursorExecutor } from "./cursor.js"; import { VertexExecutor } from "./vertex.js"; import { QwenExecutor } from "./qwen.js"; +import { OpenCodeExecutor } from "./opencode.js"; import { DefaultExecutor } from "./default.js"; const executors = { @@ -23,6 +24,7 @@ const executors = { vertex: new VertexExecutor("vertex"), "vertex-partner": new VertexExecutor("vertex-partner"), qwen: new QwenExecutor(), + opencode: new OpenCodeExecutor(), }; const defaultCache = new Map(); @@ -49,3 +51,4 @@ export { CursorExecutor } from "./cursor.js"; export { VertexExecutor } from "./vertex.js"; export { DefaultExecutor } from "./default.js"; export { QwenExecutor } from "./qwen.js"; +export { OpenCodeExecutor } from "./opencode.js"; diff --git a/open-sse/executors/opencode.js b/open-sse/executors/opencode.js new file mode 100644 index 0000000..5e77e12 --- /dev/null +++ b/open-sse/executors/opencode.js @@ -0,0 +1,27 @@ +import { BaseExecutor } from "./base.js"; +import { PROVIDERS } from "../config/providers.js"; + +// Models that use /zen/v1/messages (claude format) +const MESSAGES_MODELS = new Set(["big-pickle", "minimax-m2.5-free"]); + +export class OpenCodeExecutor extends BaseExecutor { + constructor() { + super("opencode", PROVIDERS.opencode); + } + + buildUrl(model) { + const base = "https://opencode.ai"; + return MESSAGES_MODELS.has(model) + ? `${base}/zen/v1/messages` + : `${base}/zen/v1/chat/completions`; + } + + buildHeaders() { + return { + "Content-Type": "application/json", + "Authorization": "Bearer public", + "x-opencode-client": "desktop", + "Accept": "text/event-stream" + }; + } +} diff --git a/src/app/(dashboard)/dashboard/cli-tools/components/OpenClawToolCard.js b/src/app/(dashboard)/dashboard/cli-tools/components/OpenClawToolCard.js index ac08e74..4f3149a 100644 --- a/src/app/(dashboard)/dashboard/cli-tools/components/OpenClawToolCard.js +++ b/src/app/(dashboard)/dashboard/cli-tools/components/OpenClawToolCard.js @@ -22,6 +22,8 @@ export default function OpenClawToolCard({ const [message, setMessage] = useState(null); const [selectedApiKey, setSelectedApiKey] = useState(""); const [selectedModel, setSelectedModel] = useState(""); + const [agentModels, setAgentModels] = useState({}); // { [agentId]: modelId } + const [agentModalFor, setAgentModalFor] = useState(null); // agentId opening modal const [modalOpen, setModalOpen] = useState(false); const [modelAliases, setModelAliases] = useState({}); const [showManualConfigModal, setShowManualConfigModal] = useState(false); @@ -74,14 +76,18 @@ export default function OpenClawToolCard({ const provider = openclawStatus.settings?.models?.providers?.["9router"]; if (provider) { const primaryModel = openclawStatus.settings?.agents?.defaults?.model?.primary; - if (primaryModel) { - const modelId = primaryModel.replace("9router/", ""); - setSelectedModel(modelId); - } + if (primaryModel) setSelectedModel(primaryModel.replace("9router/", "")); if (provider.apiKey && apiKeys?.some(k => k.key === provider.apiKey)) { setSelectedApiKey(provider.apiKey); } } + // Init per-agent models from enriched agents list + const agentList = openclawStatus.agents || []; + const initAgentModels = {}; + agentList.forEach((agent) => { + if (agent.currentModel) initAgentModels[agent.id] = agent.currentModel; + }); + setAgentModels(initAgentModels); } }, [openclawStatus, apiKeys]); @@ -131,7 +137,8 @@ export default function OpenClawToolCard({ body: JSON.stringify({ baseUrl: getEffectiveBaseUrl(), apiKey: keyToUse, - model: selectedModel + model: selectedModel, + agentModels, }), }); const data = await res.json(); @@ -170,7 +177,12 @@ export default function OpenClawToolCard({ }; const handleModelSelect = (model) => { - setSelectedModel(model.value); + if (agentModalFor) { + setAgentModels(prev => ({ ...prev, [agentModalFor]: model.value })); + setAgentModalFor(null); + } else { + setSelectedModel(model.value); + } setModalOpen(false); }; @@ -298,14 +310,31 @@ export default function OpenClawToolCard({ )} - {/* Model */} + {/* Default Model */}
- Model + 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 && }
+ + {/* Per-agent model overrides */} + {(openclawStatus.agents || []).filter(a => a.agentDir).map((agent) => ( +
+ Agent {agent.name || agent.id} + arrow_forward + setAgentModels(prev => ({ ...prev, [agent.id]: e.target.value }))} + placeholder={`default (${selectedModel || "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" + /> + + {agentModels[agent.id] && } +
+ ))} {message && ( diff --git a/src/app/(dashboard)/dashboard/endpoint/EndpointPageClient.js b/src/app/(dashboard)/dashboard/endpoint/EndpointPageClient.js index 5b19256..543bb6f 100644 --- a/src/app/(dashboard)/dashboard/endpoint/EndpointPageClient.js +++ b/src/app/(dashboard)/dashboard/endpoint/EndpointPageClient.js @@ -472,7 +472,7 @@ export default function APIPageClient({ machineId }) { + + {/* Tunnel Status */} {tunnelStatus && (
handleSetAlias(model.id, alias, providerStorageAlias)} onDeleteAlias={() => handleDeleteAlias(existingAlias)} testStatus={modelTestResults[model.id]} - onTest={connections.length > 0 ? () => handleTestModel(model.id) : undefined} + onTest={connections.length > 0 || isFreeNoAuth ? () => handleTestModel(model.id) : undefined} isTesting={testingModelId === model.id} isFree={model.isFree} /> @@ -607,7 +608,7 @@ export default function ProviderDetailPage() { onSetAlias={() => {}} onDeleteAlias={() => handleDeleteAlias(model.alias)} testStatus={modelTestResults[model.id]} - onTest={connections.length > 0 ? () => handleTestModel(model.id) : undefined} + onTest={connections.length > 0 || isFreeNoAuth ? () => handleTestModel(model.id) : undefined} isTesting={testingModelId === model.id} isCustom isFree={false} @@ -808,80 +809,94 @@ export default function ProviderDetailPage() { )} {/* Connections */} - -
-

Connections

- {/* Round Robin toggle */} -
- Round Robin - - {providerStrategy === "round-robin" && ( -
- Sticky: - handleStickyLimitChange(e.target.value)} - placeholder="1" - className="w-14 px-2 py-1 text-xs border border-border rounded-md bg-background focus:outline-none focus:border-primary" - /> -
- )} -
-
- - {connections.length === 0 ? ( -
-
- {isOAuth ? "lock" : "key"} + {isFreeNoAuth ? ( + +
+
+ lock_open +
+
+

No authentication required

+

This provider is ready to use.

-

No connections yet

-

Add your first connection to get started

- {!isCompatible && ( -
- {providerId === "iflow" && ( - - )} - -
- )}
- ) : ( - <> - {connectionsList} - {!isCompatible && ( -
- {providerId === "iflow" && ( + + ) : ( + +
+

Connections

+ {/* Round Robin toggle */} +
+ Round Robin + + {providerStrategy === "round-robin" && ( +
+ Sticky: + handleStickyLimitChange(e.target.value)} + placeholder="1" + className="w-14 px-2 py-1 text-xs border border-border rounded-md bg-background focus:outline-none focus:border-primary" + /> +
+ )} +
+
+ + {connections.length === 0 ? ( +
+
+ {isOAuth ? "lock" : "key"} +
+

No connections yet

+

Add your first connection to get started

+ {!isCompatible && ( +
+ {providerId === "iflow" && ( + + )} + +
+ )} +
+ ) : ( + <> + {connectionsList} + {!isCompatible && ( +
+ {providerId === "iflow" && ( + + )} - )} - -
- )} - - )} -
+
+ )} + + )} +
+ )} {/* Models */} diff --git a/src/app/(dashboard)/dashboard/providers/page.js b/src/app/(dashboard)/dashboard/providers/page.js index ab8bd07..90a4d8d 100644 --- a/src/app/(dashboard)/dashboard/providers/page.js +++ b/src/app/(dashboard)/dashboard/providers/page.js @@ -502,6 +502,7 @@ export default function ProvidersPage() { function ProviderCard({ providerId, provider, stats, authType, onToggle }) { const { connected, error, errorCode, errorTime, allDisabled } = stats; + const isNoAuth = !!provider.noAuth; const dotColors = { free: "bg-green-500", @@ -553,6 +554,8 @@ function ProviderCard({ providerId, provider, stats, authType, onToggle }) { Disabled + ) : isNoAuth ? ( + Ready ) : ( <> {getStatusDisplay(connected, error, errorCode)} diff --git a/src/app/api/cli-tools/claude-settings/route.js b/src/app/api/cli-tools/claude-settings/route.js index 7c76f30..d3251c7 100644 --- a/src/app/api/cli-tools/claude-settings/route.js +++ b/src/app/api/cli-tools/claude-settings/route.js @@ -16,15 +16,23 @@ const getClaudeSettingsPath = () => { }; -// Check if claude CLI is installed +// Check if claude CLI is installed (via which/where or config file exists) const checkClaudeInstalled = async () => { try { const isWindows = os.platform() === "win32"; - const command = isWindows ? "where claude" : "command -v claude"; - await execAsync(command, { windowsHide: true }); + const command = isWindows ? "where claude" : "which claude"; + const env = isWindows + ? { ...process.env, PATH: `${process.env.APPDATA}\\npm;${process.env.PATH}` } + : process.env; + await execAsync(command, { windowsHide: true, env }); return true; } catch { - return false; + try { + await fs.access(getClaudeSettingsPath()); + return true; + } catch { + return false; + } } }; diff --git a/src/app/api/cli-tools/codex-settings/route.js b/src/app/api/cli-tools/codex-settings/route.js index b2ec6be..f8c64b9 100644 --- a/src/app/api/cli-tools/codex-settings/route.js +++ b/src/app/api/cli-tools/codex-settings/route.js @@ -41,15 +41,23 @@ const deleteNestedSection = (obj, dottedKey) => { delete cur[keys[keys.length - 1]]; }; -// Check if codex CLI is installed +// Check if codex CLI is installed (via which/where or config file exists) const checkCodexInstalled = async () => { try { const isWindows = os.platform() === "win32"; - const command = isWindows ? "where codex" : "command -v codex"; - await execAsync(command, { windowsHide: true }); + const command = isWindows ? "where codex" : "which codex"; + const env = isWindows + ? { ...process.env, PATH: `${process.env.APPDATA}\\npm;${process.env.PATH}` } + : process.env; + await execAsync(command, { windowsHide: true, env }); return true; } catch { - return false; + try { + await fs.access(getCodexConfigPath()); + return true; + } catch { + return false; + } } }; diff --git a/src/app/api/cli-tools/droid-settings/route.js b/src/app/api/cli-tools/droid-settings/route.js index 1a7a198..6e50422 100644 --- a/src/app/api/cli-tools/droid-settings/route.js +++ b/src/app/api/cli-tools/droid-settings/route.js @@ -12,15 +12,23 @@ const execAsync = promisify(exec); const getDroidDir = () => path.join(os.homedir(), ".factory"); const getDroidSettingsPath = () => path.join(getDroidDir(), "settings.json"); -// Check if droid CLI is installed +// Check if droid CLI is installed (via which/where or config file exists) const checkDroidInstalled = async () => { try { const isWindows = os.platform() === "win32"; - const command = isWindows ? "where droid" : "command -v droid"; - await execAsync(command, { windowsHide: true }); + const command = isWindows ? "where droid" : "which droid"; + const env = isWindows + ? { ...process.env, PATH: `${process.env.APPDATA}\\npm;${process.env.PATH}` } + : process.env; + await execAsync(command, { windowsHide: true, env }); return true; } catch { - return false; + try { + await fs.access(getDroidSettingsPath()); + return true; + } catch { + return false; + } } }; diff --git a/src/app/api/cli-tools/openclaw-settings/route.js b/src/app/api/cli-tools/openclaw-settings/route.js index 4eba92b..e92705a 100644 --- a/src/app/api/cli-tools/openclaw-settings/route.js +++ b/src/app/api/cli-tools/openclaw-settings/route.js @@ -12,15 +12,24 @@ const execAsync = promisify(exec); const getOpenClawDir = () => path.join(os.homedir(), ".openclaw"); const getOpenClawSettingsPath = () => path.join(getOpenClawDir(), "openclaw.json"); -// Check if openclaw CLI is installed +// Check if openclaw CLI is installed (via which/where or config file exists) const checkOpenClawInstalled = async () => { try { const isWindows = os.platform() === "win32"; const command = isWindows ? "where openclaw" : "which openclaw"; - await execAsync(command, { windowsHide: true }); + // On Windows, inject %APPDATA%\npm into PATH so npm global packages are found + const env = isWindows + ? { ...process.env, PATH: `${process.env.APPDATA}\\npm;${process.env.PATH}` } + : process.env; + await execAsync(command, { windowsHide: true, env }); return true; } catch { - return false; + try { + await fs.access(getOpenClawSettingsPath()); + return true; + } catch { + return false; + } } }; @@ -42,6 +51,19 @@ const has9RouterConfig = (settings) => { return !!settings.models.providers["9router"]; }; +// Read per-agent models.json and return current model id (without "9router/" prefix) +const readAgentModel = async (agentDir) => { + try { + const modelsPath = path.join(agentDir, "models.json"); + const content = await fs.readFile(modelsPath, "utf-8"); + const data = JSON.parse(content); + const models = data?.providers?.["9router"]?.models; + return models?.[0]?.id || null; + } catch { + return null; + } +}; + // GET - Check openclaw CLI and read current settings export async function GET() { try { @@ -57,9 +79,19 @@ export async function GET() { const settings = await readSettings(); + // Enrich agents list with current per-agent model from models.json + const agentList = settings?.agents?.list || []; + const enrichedAgents = await Promise.all( + agentList.map(async (agent) => { + const agentModel = agent.agentDir ? await readAgentModel(agent.agentDir) : null; + return { ...agent, currentModel: agentModel }; + }) + ); + return NextResponse.json({ installed: true, settings, + agents: enrichedAgents, has9Router: has9RouterConfig(settings), settingsPath: getOpenClawSettingsPath(), }); @@ -69,10 +101,31 @@ export async function GET() { } } +// Write per-agent models.json +const writeAgentModels = async (agentDir, model, baseUrl, apiKey) => { + await fs.mkdir(agentDir, { recursive: true }); + const modelsPath = path.join(agentDir, "models.json"); + let existing = {}; + try { + const content = await fs.readFile(modelsPath, "utf-8"); + existing = JSON.parse(content); + } catch { /* No existing */ } + + if (!existing.providers) existing.providers = {}; + existing.providers["9router"] = { + baseUrl, + apiKey: apiKey || "your_api_key", + api: "openai-completions", + models: [{ id: model, name: model.split("/").pop() || model }], + }; + await fs.writeFile(modelsPath, JSON.stringify(existing, null, 2)); +}; + // POST - Update 9Router settings (merge with existing settings) export async function POST(request) { try { - const { baseUrl, apiKey, model } = await request.json(); + // agentModels: { [agentId]: modelId } for per-agent override + const { baseUrl, apiKey, model, agentModels = {} } = await request.json(); if (!baseUrl || !model) { return NextResponse.json({ error: "baseUrl and model are required" }, { status: 400 }); @@ -81,17 +134,14 @@ export async function POST(request) { const openclawDir = getOpenClawDir(); const settingsPath = getOpenClawSettingsPath(); - // Ensure directory exists await fs.mkdir(openclawDir, { recursive: true }); - // Read existing settings or create new let settings = {}; try { const existingSettings = await fs.readFile(settingsPath, "utf-8"); settings = JSON.parse(existingSettings); } catch { /* No existing settings */ } - // Ensure structure exists if (!settings.agents) settings.agents = {}; if (!settings.agents.defaults) settings.agents.defaults = {}; if (!settings.agents.defaults.model) settings.agents.defaults.model = {}; @@ -99,32 +149,64 @@ export async function POST(request) { if (!settings.models) settings.models = {}; if (!settings.models.providers) settings.models.providers = {}; - // Normalize baseUrl to ensure /v1 suffix const normalizedBaseUrl = baseUrl.endsWith("/v1") ? baseUrl : `${baseUrl}/v1`; - - // Update agents.defaults.model.primary const fullModelId = `9router/${model}`; + + // Remove all old 9router/* entries from agents.defaults.models + Object.keys(settings.agents.defaults.models) + .filter((k) => k.startsWith("9router/")) + .forEach((k) => { delete settings.agents.defaults.models[k]; }); + + // Update default model settings.agents.defaults.model.primary = fullModelId; - // IMPORTANT: Add to allowlist in agents.defaults.models - if (!settings.agents.defaults.models[fullModelId]) { - settings.agents.defaults.models[fullModelId] = {}; + // Collect all unique models (default + per-agent) + const allModelIds = new Set([model]); + Object.values(agentModels).forEach((m) => { if (m) allModelIds.add(m); }); + + // Add fresh 9router models to allowlist + allModelIds.forEach((m) => { + settings.agents.defaults.models[`9router/${m}`] = {}; + }); + + // Remove old 9router model from each agent in agents.list + if (settings.agents.list) { + settings.agents.list = settings.agents.list.map((agent) => { + if (agent.model?.startsWith("9router/")) { + const { model: _, ...rest } = agent; + return rest; + } + return agent; + }); } - // Update models.providers.9router + // Update models.providers.9router with all models settings.models.providers["9router"] = { baseUrl: normalizedBaseUrl, apiKey: apiKey || "your_api_key", api: "openai-completions", - models: [ - { - id: model, - name: model.split("/").pop() || model, - }, - ], + models: [...allModelIds].map((m) => ({ id: m, name: m.split("/").pop() || m })), }; - // Write settings + // Set per-agent model in agents.list and write models.json + if (settings.agents.list) { + settings.agents.list = settings.agents.list.map((agent) => { + const agentModel = agentModels[agent.id]; + if (agentModel) return { ...agent, model: `9router/${agentModel}` }; + return agent; + }); + + // Write per-agent models.json for agents with agentDir + await Promise.all( + settings.agents.list.map(async (agent) => { + if (!agent.agentDir) return; + const agentModel = agentModels[agent.id]; + const modelToWrite = agentModel || model; // fallback to default + await writeAgentModels(agent.agentDir, modelToWrite, normalizedBaseUrl, apiKey); + }) + ); + } + await fs.writeFile(settingsPath, JSON.stringify(settings, null, 2)); return NextResponse.json({ diff --git a/src/app/api/cli-tools/opencode-settings/route.js b/src/app/api/cli-tools/opencode-settings/route.js index bf140df..7e6793a 100644 --- a/src/app/api/cli-tools/opencode-settings/route.js +++ b/src/app/api/cli-tools/opencode-settings/route.js @@ -12,14 +12,23 @@ const execAsync = promisify(exec); const getConfigDir = () => path.join(os.homedir(), ".config", "opencode"); const getConfigPath = () => path.join(getConfigDir(), "opencode.json"); +// Check if opencode CLI is installed (via which/where or config file exists) const checkOpenCodeInstalled = async () => { try { const isWindows = os.platform() === "win32"; - const command = isWindows ? "where opencode" : "command -v opencode"; - await execAsync(command, { windowsHide: true }); + const command = isWindows ? "where opencode" : "which opencode"; + const env = isWindows + ? { ...process.env, PATH: `${process.env.APPDATA}\\npm;${process.env.PATH}` } + : process.env; + await execAsync(command, { windowsHide: true, env }); return true; } catch { - return false; + try { + await fs.access(getConfigPath()); + return true; + } catch { + return false; + } } }; diff --git a/src/lib/tunnel/tunnelManager.js b/src/lib/tunnel/tunnelManager.js index 58ce87c..c19672d 100644 --- a/src/lib/tunnel/tunnelManager.js +++ b/src/lib/tunnel/tunnelManager.js @@ -122,7 +122,10 @@ async function scheduleReconnect(attempt) { isReconnecting = false; const nextAttempt = attempt + 1; if (nextAttempt < MAX_RECONNECT_ATTEMPTS) scheduleReconnect(nextAttempt); - else console.log("[Tunnel] All reconnect attempts exhausted"); + else { + console.log("[Tunnel] All reconnect attempts exhausted, disabling tunnel"); + await updateSettings({ tunnelEnabled: false }); + } } } diff --git a/src/shared/components/ModelSelectModal.js b/src/shared/components/ModelSelectModal.js index 62c5a37..83a74bb 100644 --- a/src/shared/components/ModelSelectModal.js +++ b/src/shared/components/ModelSelectModal.js @@ -4,15 +4,19 @@ import { useState, useMemo, useEffect } from "react"; import PropTypes from "prop-types"; import Modal from "./Modal"; import { getModelsByProviderId, PROVIDER_ID_TO_ALIAS } from "@/shared/constants/models"; -import { OAUTH_PROVIDERS, APIKEY_PROVIDERS, FREE_TIER_PROVIDERS, isOpenAICompatibleProvider, isAnthropicCompatibleProvider } from "@/shared/constants/providers"; +import { OAUTH_PROVIDERS, APIKEY_PROVIDERS, FREE_PROVIDERS, FREE_TIER_PROVIDERS, isOpenAICompatibleProvider, isAnthropicCompatibleProvider } from "@/shared/constants/providers"; // Provider order: OAuth first, then Free Tier, then API Key (matches dashboard/providers) const PROVIDER_ORDER = [ ...Object.keys(OAUTH_PROVIDERS), + ...Object.keys(FREE_PROVIDERS), ...Object.keys(FREE_TIER_PROVIDERS), ...Object.keys(APIKEY_PROVIDERS), ]; +// Providers that need no auth — always show in model selector +const NO_AUTH_PROVIDER_IDS = Object.keys(FREE_PROVIDERS).filter(id => FREE_PROVIDERS[id].noAuth); + export default function ModelSelectModal({ isOpen, onClose, @@ -58,7 +62,7 @@ export default function ModelSelectModal({ if (isOpen) fetchProviderNodes(); }, [isOpen]); - const allProviders = useMemo(() => ({ ...OAUTH_PROVIDERS, ...FREE_TIER_PROVIDERS, ...APIKEY_PROVIDERS }), []); + const allProviders = useMemo(() => ({ ...OAUTH_PROVIDERS, ...FREE_PROVIDERS, ...FREE_TIER_PROVIDERS, ...APIKEY_PROVIDERS }), []); // Group models by provider with priority order const groupedModels = useMemo(() => { @@ -70,6 +74,7 @@ export default function ModelSelectModal({ // Only show connected providers (including both standard and custom) const providerIdsToShow = new Set([ ...activeConnectionIds, // Only connected providers + ...NO_AUTH_PROVIDER_IDS, // No-auth providers always visible ]); // Sort by PROVIDER_ORDER diff --git a/src/shared/components/Sidebar.js b/src/shared/components/Sidebar.js index 61362d4..101f50a 100644 --- a/src/shared/components/Sidebar.js +++ b/src/shared/components/Sidebar.js @@ -23,11 +23,11 @@ const navItems = [ const debugItems = [ { href: "/dashboard/console-log", label: "Console Log", icon: "terminal" }, + { href: "/dashboard/translator", label: "Translator", icon: "translate" }, ]; const systemItems = [ { href: "/dashboard/proxy-pools", label: "Proxy Pools", icon: "lan" }, - { href: "/dashboard/profile", label: "Settings", icon: "settings" }, ]; export default function Sidebar({ onClose }) { @@ -171,52 +171,6 @@ export default function Sidebar({ onClose }) {
)} - {/* Debug section */} -
-

- Debug -

- {enableTranslator && ( - - - translate - - Translator - - )} - {debugItems.map((item) => ( - - - {item.icon} - - {item.label} - - ))} -
{/* System section */}
@@ -246,23 +200,61 @@ export default function Sidebar({ onClose }) { {item.label} ))} + + {/* Debug items (inside System section, before Settings) */} + {debugItems.map((item) => { + const show = item.href !== "/dashboard/translator" || enableTranslator; + return show ? ( + + + {item.icon} + + {item.label} + + ) : null; + })} + + {/* Settings */} + + + settings + + Settings +
{/* Footer section */}
- {/* Info message */} -
-
- info -
-
- - Service is running in terminal. You can close this web page. Shutdown will stop the service. - -
-
- {/* Shutdown button */}