diff --git a/src/app/(dashboard)/dashboard/cli-tools/CLIToolsPageClient.js b/src/app/(dashboard)/dashboard/cli-tools/CLIToolsPageClient.js
index 726ca3d..2705c8f 100644
--- a/src/app/(dashboard)/dashboard/cli-tools/CLIToolsPageClient.js
+++ b/src/app/(dashboard)/dashboard/cli-tools/CLIToolsPageClient.js
@@ -88,22 +88,6 @@ export default function CLIToolsPageClient({ machineId }) {
});
});
- if (models.length === 0) {
- Object.entries(PROVIDER_MODELS).forEach(([alias, providerModels]) => {
- providerModels.forEach(m => {
- const modelValue = `${alias}/${m.id}`;
- models.push({
- value: modelValue,
- label: `${alias}/${m.id}`,
- provider: alias,
- alias: alias,
- connectionName: alias,
- modelId: m.id,
- });
- });
- });
- }
-
return models;
};
diff --git a/src/app/(dashboard)/dashboard/combos/page.js b/src/app/(dashboard)/dashboard/combos/page.js
index 1e2b950..1e43659 100644
--- a/src/app/(dashboard)/dashboard/combos/page.js
+++ b/src/app/(dashboard)/dashboard/combos/page.js
@@ -1,8 +1,9 @@
"use client";
-import { useState, useEffect } from "react";
+import { useState, useEffect, useCallback } from "react";
import { Card, Button, Modal, Input, CardSkeleton, ModelSelectModal } from "@/shared/components";
import { useCopyToClipboard } from "@/shared/hooks/useCopyToClipboard";
+import { isOpenAICompatibleProvider, isAnthropicCompatibleProvider } from "@/shared/constants/providers";
// Validate combo name: only a-z, A-Z, 0-9, -, _
const VALID_NAME_REGEX = /^[a-zA-Z0-9_-]+$/;
@@ -17,7 +18,7 @@ export default function CombosPage() {
useEffect(() => {
fetchData();
- }, []);
+ }, []); // eslint-disable-line react-hooks/exhaustive-deps
const fetchData = async () => {
try {
@@ -235,21 +236,32 @@ function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders }) {
const [saving, setSaving] = useState(false);
const [nameError, setNameError] = useState("");
const [modelAliases, setModelAliases] = useState({});
+ const [providerNodes, setProviderNodes] = useState([]);
- // Fetch model aliases when modal opens
- useEffect(() => {
- if (isOpen) {
- const fetchModelAliases = async () => {
- try {
- const res = await fetch("/api/models/alias");
- const data = await res.json();
- if (res.ok) setModelAliases(data.aliases || {});
- } catch (error) {
- console.log("Error fetching model aliases:", error);
- }
- };
- fetchModelAliases();
+ const fetchModalData = async () => {
+ try {
+ const [aliasesRes, nodesRes] = await Promise.all([
+ fetch("/api/models/alias"),
+ fetch("/api/provider-nodes"),
+ ]);
+
+ if (!aliasesRes.ok || !nodesRes.ok) {
+ throw new Error(`Failed to fetch data: aliases=${aliasesRes.status}, nodes=${nodesRes.status}`);
+ }
+
+ const [aliasesData, nodesData] = await Promise.all([
+ aliasesRes.json(),
+ nodesRes.json(),
+ ]);
+ setModelAliases(aliasesData.aliases || {});
+ setProviderNodes(nodesData.nodes || []);
+ } catch (error) {
+ console.error("Error fetching modal data:", error);
}
+ };
+
+ useEffect(() => {
+ if (isOpen) fetchModalData();
}, [isOpen]);
const validateName = (value) => {
@@ -282,11 +294,20 @@ function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders }) {
setModels(models.filter((_, i) => i !== index));
};
- const handleModelChange = (index, value) => {
- const newModels = [...models];
- newModels[index] = value;
- setModels(newModels);
- };
+ // Format model display name with readable provider name
+ const formatModelDisplay = useCallback((modelValue) => {
+ const parts = modelValue.split('/');
+ if (parts.length !== 2) return modelValue;
+
+ const [providerId, modelId] = parts;
+ const matchedNode = providerNodes.find(node => node.id === providerId);
+
+ if (matchedNode) {
+ return `${matchedNode.name}/${modelId}`;
+ }
+
+ return modelValue;
+ }, [providerNodes]);
const handleMoveUp = (index) => {
if (index === 0) return;
@@ -352,14 +373,10 @@ function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders }) {
{/* Index badge */}
{index + 1}
- {/* Model Input */}
- handleModelChange(index, e.target.value)}
- placeholder="provider/model"
- className="flex-1 min-w-0 px-1.5 py-0.5 text-xs font-mono bg-transparent border-0 focus:outline-none text-text-main placeholder:text-text-muted/50"
- />
+ {/* Model display - show readable name only */}
+
+ {formatModelDisplay(model)}
+
{/* Priority arrows - horizontal, always visible */}
diff --git a/src/shared/components/ModelSelectModal.js b/src/shared/components/ModelSelectModal.js
index 8df74c0..9e727db 100644
--- a/src/shared/components/ModelSelectModal.js
+++ b/src/shared/components/ModelSelectModal.js
@@ -4,7 +4,7 @@ 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 } from "@/shared/constants/providers";
+import { OAUTH_PROVIDERS, APIKEY_PROVIDERS, isOpenAICompatibleProvider, isAnthropicCompatibleProvider } from "@/shared/constants/providers";
// Provider order: OAuth first, then API Key (matches dashboard/providers)
const PROVIDER_ORDER = [
@@ -23,15 +23,38 @@ export default function ModelSelectModal({
}) {
const [searchQuery, setSearchQuery] = useState("");
const [combos, setCombos] = useState([]);
+ const [providerNodes, setProviderNodes] = useState([]);
- // Fetch combos when modal opens
- useEffect(() => {
- if (isOpen) {
- fetch("/api/combos")
- .then(res => res.json())
- .then(data => setCombos(data.combos || []))
- .catch(() => setCombos([]));
+ const fetchCombos = async () => {
+ try {
+ const res = await fetch("/api/combos");
+ if (!res.ok) throw new Error(`Failed to fetch combos: ${res.status}`);
+ const data = await res.json();
+ setCombos(data.combos || []);
+ } catch (error) {
+ console.error("Error fetching combos:", error);
+ setCombos([]);
}
+ };
+
+ useEffect(() => {
+ if (isOpen) fetchCombos();
+ }, [isOpen]);
+
+ const fetchProviderNodes = async () => {
+ try {
+ const res = await fetch("/api/provider-nodes");
+ if (!res.ok) throw new Error(`Failed to fetch provider nodes: ${res.status}`);
+ const data = await res.json();
+ setProviderNodes(data.nodes || []);
+ } catch (error) {
+ console.error("Error fetching provider nodes:", error);
+ setProviderNodes([]);
+ }
+ };
+
+ useEffect(() => {
+ if (isOpen) fetchProviderNodes();
}, [isOpen]);
const allProviders = useMemo(() => ({ ...OAUTH_PROVIDERS, ...APIKEY_PROVIDERS }), []);
@@ -40,13 +63,16 @@ export default function ModelSelectModal({
const groupedModels = useMemo(() => {
const groups = {};
- // Get active provider IDs
- const activeProviderIds = activeProviders.length > 0
- ? activeProviders.map(p => p.provider)
- : PROVIDER_ORDER;
+ // 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
+ ]);
// Sort by PROVIDER_ORDER
- const sortedProviderIds = [...activeProviderIds].sort((a, b) => {
+ const sortedProviderIds = [...providerIdsToShow].sort((a, b) => {
const indexA = PROVIDER_ORDER.indexOf(a);
const indexB = PROVIDER_ORDER.indexOf(b);
return (indexA === -1 ? 999 : indexA) - (indexB === -1 ? 999 : indexB);
@@ -55,8 +81,8 @@ export default function ModelSelectModal({
sortedProviderIds.forEach((providerId) => {
const alias = PROVIDER_ID_TO_ALIAS[providerId] || providerId;
const providerInfo = allProviders[providerId] || { name: providerId, color: "#666" };
+ const isCustomProvider = isOpenAICompatibleProvider(providerId) || isAnthropicCompatibleProvider(providerId);
- // For passthrough providers, get models from aliases
if (providerInfo.passthroughModels) {
const aliasModels = Object.entries(modelAliases)
.filter(([, fullModel]) => fullModel.startsWith(`${alias}/`))
@@ -67,13 +93,43 @@ export default function ModelSelectModal({
}));
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: providerInfo.name,
+ name: displayName,
alias: alias,
color: providerInfo.color,
models: aliasModels,
};
}
+ } else if (isCustomProvider) {
+ // Match provider node to get custom name
+ 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 nodeModels = Object.entries(modelAliases)
+ .filter(([, fullModel]) => fullModel.startsWith(`${providerId}/`))
+ .map(([aliasName, fullModel]) => ({
+ id: fullModel.replace(`${providerId}/`, ""),
+ name: aliasName,
+ value: fullModel,
+ }));
+
+ // 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,
+ };
+ }
} else {
const models = getModelsByProviderId(providerId);
if (models.length > 0) {
@@ -92,7 +148,7 @@ export default function ModelSelectModal({
});
return groups;
- }, [activeProviders, modelAliases, allProviders]);
+ }, [activeProviders, modelAliases, allProviders, providerNodes]);
// Filter combos by search query
const filteredCombos = useMemo(() => {
@@ -112,11 +168,12 @@ export default function ModelSelectModal({
const matchedModels = group.models.filter(
(m) =>
m.name.toLowerCase().includes(query) ||
- m.id.toLowerCase().includes(query) ||
- group.name.toLowerCase().includes(query)
+ m.id.toLowerCase().includes(query)
);
- if (matchedModels.length > 0) {
+ const providerNameMatches = group.name.toLowerCase().includes(query);
+
+ if (matchedModels.length > 0 || providerNameMatches) {
filtered[providerId] = {
...group,
models: matchedModels,
@@ -210,7 +267,6 @@ export default function ModelSelectModal({
- {/* Models as wrap chips - compact */}
{group.models.map((model) => {
const isSelected = selectedModel === model.value;