diff --git a/package.json b/package.json index 0a18c46..f0fc941 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "lowdb": "^7.0.1", "monaco-editor": "^0.55.1", "next": "^16.1.6", - "node-forge": "^1.3.1", + "node-forge": "^1.3.3", "node-machine-id": "^1.1.12", "open": "^11.0.0", "ora": "^9.1.0", diff --git a/src/app/(dashboard)/dashboard/cli-tools/components/MitmToolCard.js b/src/app/(dashboard)/dashboard/cli-tools/components/MitmToolCard.js index 88a5dfe..bc8be22 100644 --- a/src/app/(dashboard)/dashboard/cli-tools/components/MitmToolCard.js +++ b/src/app/(dashboard)/dashboard/cli-tools/components/MitmToolCard.js @@ -22,6 +22,7 @@ export default function MitmToolCard({ apiKeys, activeProviders, hasActiveProviders, + modelAliases = {}, cloudEnabled, onDnsChange, }) { @@ -74,7 +75,7 @@ export default function MitmToolCard({ }; const handleModelSelect = (model) => { - if (!currentEditingAlias) return; + if (!currentEditingAlias || model.isPlaceholder) return; const updated = { ...modelMappings, [currentEditingAlias]: model.value }; setModelMappings(updated); saveMappings(updated); @@ -104,7 +105,7 @@ export default function MitmToolCard({ }); const data = await res.json(); if (!res.ok) throw new Error(data.error || "Failed to toggle DNS"); - + if (action === "enable") { setMessage({ type: "success", @@ -116,7 +117,7 @@ export default function MitmToolCard({ text: "DNS disabled — traffic restored", }); } - + setShowPasswordModal(false); setSudoPassword(""); onDnsChange?.(data); @@ -303,6 +304,7 @@ export default function MitmToolCard({ onSelect={handleModelSelect} selectedModel={currentEditingAlias ? modelMappings[currentEditingAlias] : null} activeProviders={activeProviders} + modelAliases={modelAliases} title={`Select model for ${currentEditingAlias}`} /> diff --git a/src/app/(dashboard)/dashboard/mitm/MitmPageClient.js b/src/app/(dashboard)/dashboard/mitm/MitmPageClient.js index 6d4bff5..25b6bbc 100644 --- a/src/app/(dashboard)/dashboard/mitm/MitmPageClient.js +++ b/src/app/(dashboard)/dashboard/mitm/MitmPageClient.js @@ -2,7 +2,8 @@ import { useState, useEffect } from "react"; import { CLI_TOOLS } from "@/shared/constants/cliTools"; -import { getModelsByProviderId, PROVIDER_ID_TO_ALIAS } from "@/shared/constants/models"; +import { getModelsByProviderId } from "@/shared/constants/models"; +import { isOpenAICompatibleProvider, isAnthropicCompatibleProvider } from "@/shared/constants/providers"; import { MitmServerCard, MitmToolCard } from "@/app/(dashboard)/dashboard/cli-tools/components"; const MITM_TOOL_IDS = ["antigravity", "copilot"]; @@ -10,6 +11,7 @@ const MITM_TOOL_IDS = ["antigravity", "copilot"]; export default function MitmPageClient() { const [connections, setConnections] = useState([]); const [apiKeys, setApiKeys] = useState([]); + const [modelAliases, setModelAliases] = useState({}); const [cloudEnabled, setCloudEnabled] = useState(false); const [expandedTool, setExpandedTool] = useState(null); const [mitmStatus, setMitmStatus] = useState({ running: false, certExists: false, dnsStatus: {}, hasCachedPassword: false }); @@ -17,6 +19,7 @@ export default function MitmPageClient() { useEffect(() => { fetchConnections(); fetchApiKeys(); + fetchAliases(); fetchCloudSettings(); }, []); @@ -40,6 +43,16 @@ export default function MitmPageClient() { } catch { /* ignore */ } }; + const fetchAliases = async () => { + try { + const res = await fetch("/api/models/alias"); + if (res.ok) { + const data = await res.json(); + setModelAliases(data.aliases || {}); + } + } catch { /* ignore */ } + }; + const fetchCloudSettings = async () => { try { const res = await fetch("/api/settings"); @@ -54,7 +67,11 @@ export default function MitmPageClient() { const hasActiveProviders = () => { const active = getActiveProviders(); - return active.some(conn => getModelsByProviderId(conn.provider).length > 0); + return active.some(conn => + getModelsByProviderId(conn.provider).length > 0 || + isOpenAICompatibleProvider(conn.provider) || + isAnthropicCompatibleProvider(conn.provider) + ); }; const mitmTools = Object.entries(CLI_TOOLS).filter(([id]) => MITM_TOOL_IDS.includes(id)); @@ -82,6 +99,7 @@ export default function MitmPageClient() { apiKeys={apiKeys} activeProviders={getActiveProviders()} hasActiveProviders={hasActiveProviders()} + modelAliases={modelAliases} cloudEnabled={cloudEnabled} onDnsChange={(data) => setMitmStatus(prev => ({ ...prev, dnsStatus: data.dnsStatus ?? prev.dnsStatus }))} /> diff --git a/src/app/api/combos/route.js b/src/app/api/combos/route.js index 965610f..43e103a 100644 --- a/src/app/api/combos/route.js +++ b/src/app/api/combos/route.js @@ -1,6 +1,8 @@ import { NextResponse } from "next/server"; import { getCombos, createCombo, getComboByName } from "@/lib/localDb"; +export const dynamic = "force-dynamic"; + // Validate combo name: only a-z, A-Z, 0-9, -, _ const VALID_NAME_REGEX = /^[a-zA-Z0-9_-]+$/; diff --git a/src/app/api/keys/route.js b/src/app/api/keys/route.js index 98d2540..ab0470a 100644 --- a/src/app/api/keys/route.js +++ b/src/app/api/keys/route.js @@ -2,6 +2,8 @@ import { NextResponse } from "next/server"; import { getApiKeys, createApiKey } from "@/lib/localDb"; import { getConsistentMachineId } from "@/shared/utils/machineId"; +export const dynamic = "force-dynamic"; + // GET /api/keys - List API keys export async function GET() { try { diff --git a/src/app/api/models/alias/route.js b/src/app/api/models/alias/route.js index 93cf3a6..980ea6d 100644 --- a/src/app/api/models/alias/route.js +++ b/src/app/api/models/alias/route.js @@ -1,6 +1,8 @@ import { NextResponse } from "next/server"; import { getModelAliases, setModelAlias, deleteModelAlias } from "@/models"; +export const dynamic = "force-dynamic"; + // GET /api/models/alias - Get all aliases export async function GET() { try { diff --git a/src/app/api/provider-nodes/route.js b/src/app/api/provider-nodes/route.js index 61df88e..6a32cd9 100644 --- a/src/app/api/provider-nodes/route.js +++ b/src/app/api/provider-nodes/route.js @@ -3,6 +3,8 @@ import { createProviderNode, getProviderNodes } from "@/models"; import { OPENAI_COMPATIBLE_PREFIX, ANTHROPIC_COMPATIBLE_PREFIX } from "@/shared/constants/providers"; import { generateId } from "@/shared/utils"; +export const dynamic = "force-dynamic"; + const OPENAI_COMPATIBLE_DEFAULTS = { baseUrl: "https://api.openai.com/v1", }; diff --git a/src/app/api/providers/route.js b/src/app/api/providers/route.js index cedec64..01eba1d 100644 --- a/src/app/api/providers/route.js +++ b/src/app/api/providers/route.js @@ -3,6 +3,8 @@ import { getProviderConnections, createProviderConnection, getProviderNodeById, import { APIKEY_PROVIDERS } from "@/shared/constants/config"; import { isOpenAICompatibleProvider, isAnthropicCompatibleProvider } from "@/shared/constants/providers"; +export const dynamic = "force-dynamic"; + // GET /api/providers - List all connections export async function GET() { try { @@ -15,8 +17,8 @@ export async function GET() { for (const node of nodes) { if (node.id && node.name) nodeNameMap[node.id] = node.name; } - } catch {} - + } catch { } + // Hide sensitive fields, enrich name for compatible providers const safeConnections = connections.map(c => { const isCompatible = isOpenAICompatibleProvider(c.provider) || isAnthropicCompatibleProvider(c.provider); @@ -47,9 +49,9 @@ export async function POST(request) { const { provider, apiKey, name, priority, globalPriority, defaultModel, testStatus } = body; // Validation - const isValidProvider = APIKEY_PROVIDERS[provider] || - isOpenAICompatibleProvider(provider) || - isAnthropicCompatibleProvider(provider); + const isValidProvider = APIKEY_PROVIDERS[provider] || + isOpenAICompatibleProvider(provider) || + isAnthropicCompatibleProvider(provider); if (!provider || !isValidProvider) { return NextResponse.json({ error: "Invalid provider" }, { status: 400 }); diff --git a/src/shared/components/ModelSelectModal.js b/src/shared/components/ModelSelectModal.js index 9e727db..3640109 100644 --- a/src/shared/components/ModelSelectModal.js +++ b/src/shared/components/ModelSelectModal.js @@ -62,10 +62,10 @@ export default function ModelSelectModal({ // Group models by provider with priority order const groupedModels = useMemo(() => { const groups = {}; - + // Get all active provider IDs from connections const activeConnectionIds = activeProviders.map(p => p.provider); - + // Only show connected providers (including both standard and custom) const providerIdsToShow = new Set([ ...activeConnectionIds, // Only connected providers @@ -82,7 +82,7 @@ export default function ModelSelectModal({ const alias = PROVIDER_ID_TO_ALIAS[providerId] || providerId; const providerInfo = allProviders[providerId] || { name: providerId, color: "#666" }; const isCustomProvider = isOpenAICompatibleProvider(providerId) || isAnthropicCompatibleProvider(providerId); - + if (providerInfo.passthroughModels) { const aliasModels = Object.entries(modelAliases) .filter(([, fullModel]) => fullModel.startsWith(`${alias}/`)) @@ -91,12 +91,12 @@ export default function ModelSelectModal({ name: aliasName, value: fullModel, })); - + if (aliasModels.length > 0) { // Check for custom name from providerNodes (for compatible providers) const matchedNode = providerNodes.find(node => node.id === providerId); const displayName = matchedNode?.name || providerInfo.name; - + groups[providerId] = { name: displayName, alias: alias, @@ -105,31 +105,39 @@ export default function ModelSelectModal({ }; } } else if (isCustomProvider) { - // Match provider node to get custom name + // Find connection object to get prefix synchronously without waiting for providerNodes fetch + const connection = activeProviders.find(p => p.provider === providerId); const matchedNode = providerNodes.find(node => node.id === providerId); - const displayName = matchedNode?.name || providerInfo.name; - - // Get models from modelAliases using providerId (not prefix) - // modelAliases format: { alias: "providerId/modelId" } + const displayName = connection?.name || matchedNode?.name || providerInfo.name; + const nodePrefix = connection?.providerSpecificData?.prefix || matchedNode?.prefix || providerId; + + // Aliases are stored using the raw providerId as key (e.g. "openai-compatible-chat-/glm-4.7"), + // so we must filter by providerId, not by the display prefix. const nodeModels = Object.entries(modelAliases) .filter(([, fullModel]) => fullModel.startsWith(`${providerId}/`)) .map(([aliasName, fullModel]) => ({ id: fullModel.replace(`${providerId}/`, ""), name: aliasName, - value: fullModel, + value: `${nodePrefix}/${fullModel.replace(`${providerId}/`, "")}`, })); - - // Only add to groups if there are models (consistent with other provider types) - if (nodeModels.length > 0) { - groups[providerId] = { - name: displayName, - alias: matchedNode?.prefix || providerId, - color: providerInfo.color, - models: nodeModels, - isCustom: true, - hasModels: true, - }; - } + + // Always show compatible providers that are connected, even with no aliases. + // When no aliases exist, show a placeholder so users know it's available. + const modelsToShow = nodeModels.length > 0 ? nodeModels : [{ + id: `__placeholder__${providerId}`, + name: `${nodePrefix}/model-id`, + value: `${nodePrefix}/model-id`, + isPlaceholder: true, + }]; + + groups[providerId] = { + name: displayName, + alias: nodePrefix, + color: providerInfo.color, + models: modelsToShow, + isCustom: true, + hasModels: nodeModels.length > 0, + }; } else { const models = getModelsByProviderId(providerId); if (models.length > 0) { @@ -172,7 +180,7 @@ export default function ModelSelectModal({ ); const providerNameMatches = group.name.toLowerCase().includes(query); - + if (matchedModels.length > 0 || providerNameMatches) { filtered[providerId] = { ...group, @@ -236,8 +244,8 @@ export default function ModelSelectModal({ onClick={() => handleSelect({ id: combo.name, name: combo.name, value: combo.name })} className={` px-2 py-1 rounded-xl text-xs font-medium transition-all border hover:cursor-pointer - ${isSelected - ? "bg-primary text-white border-primary" + ${isSelected + ? "bg-primary text-white border-primary" : "bg-surface border-border text-text-main hover:border-primary/50 hover:bg-primary/5" } `} @@ -270,19 +278,28 @@ export default function ModelSelectModal({
{group.models.map((model) => { const isSelected = selectedModel === model.value; + const isPlaceholder = model.isPlaceholder; return ( ); })}