mirror of
https://github.com/Nezumi-2711/9router.git
synced 2026-09-22 13:38:31 +00:00
fix: update the permission for viewing models
This commit is contained in:
@@ -2,8 +2,6 @@
|
||||
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { Badge, Button } from "@/shared/components";
|
||||
import { getModelsByProviderId } from "@/shared/constants/models";
|
||||
import { isAnthropicCompatibleProvider, isOpenAICompatibleProvider } from "@/shared/constants/providers";
|
||||
|
||||
const STORAGE_KEYS = {
|
||||
sessions: "basic-chat.sessions",
|
||||
@@ -109,63 +107,6 @@ function cloneSession(session) {
|
||||
};
|
||||
}
|
||||
|
||||
function getProviderLabel(connection) {
|
||||
return connection?.name || humanize(connection?.provider || connection?.id || "provider");
|
||||
}
|
||||
|
||||
function normalizeStaticModel(model, connection) {
|
||||
if (!model?.id) return null;
|
||||
return {
|
||||
id: `${connection.provider}/${model.id}`,
|
||||
requestModel: `${connection.provider}/${model.id}`,
|
||||
name: model.name || model.id,
|
||||
providerId: connection.provider,
|
||||
providerName: getProviderLabel(connection),
|
||||
source: "static",
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeLiveModel(model, connection) {
|
||||
const rawId = typeof model === "string" ? model : model?.id || model?.name || model?.model || "";
|
||||
if (!rawId) return null;
|
||||
|
||||
const displayName = typeof model === "string"
|
||||
? model
|
||||
: model?.name || model?.displayName || rawId;
|
||||
|
||||
let requestModel = rawId;
|
||||
const isCompatible = isOpenAICompatibleProvider(connection.provider) || isAnthropicCompatibleProvider(connection.provider);
|
||||
if (isCompatible && !rawId.includes("/")) {
|
||||
requestModel = `${connection.provider}/${rawId}`;
|
||||
}
|
||||
|
||||
return {
|
||||
id: requestModel,
|
||||
requestModel,
|
||||
name: displayName,
|
||||
providerId: connection.provider,
|
||||
providerName: getProviderLabel(connection),
|
||||
source: "live",
|
||||
};
|
||||
}
|
||||
|
||||
function parseProviderModelsPayload(data) {
|
||||
if (Array.isArray(data?.models)) return data.models;
|
||||
if (Array.isArray(data?.data)) return data.data;
|
||||
if (Array.isArray(data?.results)) return data.results;
|
||||
if (Array.isArray(data)) return data;
|
||||
return [];
|
||||
}
|
||||
|
||||
function dedupeModels(models) {
|
||||
const map = new Map();
|
||||
for (const model of models) {
|
||||
if (!model?.id) continue;
|
||||
if (!map.has(model.id)) map.set(model.id, model);
|
||||
}
|
||||
return Array.from(map.values());
|
||||
}
|
||||
|
||||
export default function BasicChatPageClient() {
|
||||
const [providerGroups, setProviderGroups] = useState([]);
|
||||
const [loadingData, setLoadingData] = useState(true);
|
||||
@@ -218,79 +159,39 @@ export default function BasicChatPageClient() {
|
||||
setLoadError("");
|
||||
|
||||
try {
|
||||
const providersRes = await fetch("/api/providers", { cache: "no-store" });
|
||||
const providersData = await providersRes.json().catch(() => ({}));
|
||||
const connections = Array.isArray(providersData.connections)
|
||||
? providersData.connections.filter((connection) => connection?.isActive !== false)
|
||||
: [];
|
||||
|
||||
if (connections.length === 0) {
|
||||
if (!cancelled) {
|
||||
setProviderGroups([]);
|
||||
setLoadError("No providers connected yet.");
|
||||
}
|
||||
return;
|
||||
}
|
||||
const modelsRes = await fetch("/api/models/connected", { cache: "no-store" });
|
||||
const modelsData = await modelsRes.json().catch(() => ({}));
|
||||
if (!modelsRes.ok) throw new Error(modelsData.error || "Failed to load added models.");
|
||||
|
||||
const providerMap = new Map();
|
||||
|
||||
for (const connection of connections) {
|
||||
const providerId = connection.provider || connection.id;
|
||||
const providerName = getProviderLabel(connection);
|
||||
const providerType = isOpenAICompatibleProvider(providerId)
|
||||
? "openai-compatible"
|
||||
: isAnthropicCompatibleProvider(providerId)
|
||||
? "anthropic-compatible"
|
||||
: providerId;
|
||||
for (const model of modelsData.models || []) {
|
||||
if (model.disabled || !model.fullModel) continue;
|
||||
const providerId = model.provider?.id || model.providerAlias;
|
||||
const providerName = model.provider?.name || humanize(providerId);
|
||||
|
||||
if (!providerMap.has(providerId)) {
|
||||
providerMap.set(providerId, {
|
||||
providerId,
|
||||
providerName,
|
||||
providerType,
|
||||
connections: [],
|
||||
models: [],
|
||||
});
|
||||
}
|
||||
|
||||
const group = providerMap.get(providerId);
|
||||
group.providerName = group.providerName || providerName;
|
||||
group.providerType = group.providerType || providerType;
|
||||
group.connections.push(connection);
|
||||
|
||||
const staticModels = getModelsByProviderId(providerId)
|
||||
.map((model) => normalizeStaticModel(model, connection))
|
||||
.filter(Boolean);
|
||||
group.models.push(...staticModels);
|
||||
}
|
||||
|
||||
const liveResults = await Promise.all(
|
||||
connections.map(async (connection) => {
|
||||
try {
|
||||
const response = await fetch(`/api/providers/${connection.id}/models`, { cache: "no-store" });
|
||||
const data = await response.json().catch(() => ({}));
|
||||
if (!response.ok) return { connection, models: [] };
|
||||
const models = parseProviderModelsPayload(data)
|
||||
.map((model) => normalizeLiveModel(model, connection))
|
||||
.filter(Boolean);
|
||||
return { connection, models };
|
||||
} catch {
|
||||
return { connection, models: [] };
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
for (const result of liveResults) {
|
||||
const providerId = result.connection.provider || result.connection.id;
|
||||
const group = providerMap.get(providerId);
|
||||
if (!group) continue;
|
||||
group.models.push(...result.models);
|
||||
group.models.push({
|
||||
id: model.fullModel,
|
||||
requestModel: model.fullModel,
|
||||
name: model.name || model.alias || model.model,
|
||||
providerId,
|
||||
providerName,
|
||||
source: "added",
|
||||
});
|
||||
}
|
||||
|
||||
const normalized = Array.from(providerMap.values())
|
||||
.map((group) => ({
|
||||
...group,
|
||||
models: dedupeModels(group.models).sort((a, b) => a.name.localeCompare(b.name)),
|
||||
models: group.models.sort((a, b) => a.name.localeCompare(b.name)),
|
||||
}))
|
||||
.filter((group) => group.models.length > 0)
|
||||
.sort((a, b) => a.providerName.localeCompare(b.providerName));
|
||||
@@ -298,12 +199,12 @@ export default function BasicChatPageClient() {
|
||||
if (!cancelled) {
|
||||
setProviderGroups(normalized);
|
||||
if (normalized.length === 0) {
|
||||
setLoadError("Providers connected but no models available.");
|
||||
setLoadError("No added models are available. Add a model from a provider first.");
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
if (!cancelled) {
|
||||
setLoadError(textValue(error?.message) || "Failed to load providers/models.");
|
||||
setLoadError(textValue(error?.message) || "Failed to load added models.");
|
||||
setProviderGroups([]);
|
||||
}
|
||||
} finally {
|
||||
@@ -761,7 +662,7 @@ export default function BasicChatPageClient() {
|
||||
<div className="absolute left-0 top-[calc(100%+10px)] z-30 w-[min(520px,calc(100vw-2rem))] overflow-hidden rounded-[20px] border border-white/10 bg-[#262626] shadow-2xl shadow-black/50">
|
||||
<div className="border-b border-white/10 px-4 py-3">
|
||||
<p className="text-xs uppercase tracking-[0.22em] text-white/45">Models</p>
|
||||
<p className="text-sm text-white/75">Only from connected providers</p>
|
||||
<p className="text-sm text-white/75">Added models from connected providers</p>
|
||||
</div>
|
||||
<div className="max-h-[60vh] overflow-y-auto p-2 custom-scrollbar">
|
||||
{providerGroups.map((group) => (
|
||||
|
||||
@@ -18,16 +18,18 @@ export default function ToolDetailClient({ toolId, machineId }) {
|
||||
const [tailscaleEnabled, setTailscaleEnabled] = useState(false);
|
||||
const [tailscaleUrl, setTailscaleUrl] = useState("");
|
||||
const [apiKeys, setApiKeys] = useState([]);
|
||||
const [availableModels, setAvailableModels] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
let mounted = true;
|
||||
(async () => {
|
||||
try {
|
||||
const [provRes, settingsRes, tunnelRes, keysRes] = await Promise.all([
|
||||
const [provRes, settingsRes, tunnelRes, keysRes, modelsRes] = await Promise.all([
|
||||
fetch("/api/providers"),
|
||||
fetch("/api/settings"),
|
||||
fetch("/api/tunnel/status"),
|
||||
fetch("/api/keys"),
|
||||
fetch("/api/models/connected", { cache: "no-store" }),
|
||||
]);
|
||||
if (!mounted) return;
|
||||
if (provRes.ok) {
|
||||
@@ -49,6 +51,10 @@ export default function ToolDetailClient({ toolId, machineId }) {
|
||||
const data = await keysRes.json();
|
||||
setApiKeys(data.keys || []);
|
||||
}
|
||||
if (modelsRes.ok) {
|
||||
const data = await modelsRes.json();
|
||||
setAvailableModels((data.models || []).filter((model) => !model.disabled));
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("Error loading tool data:", error);
|
||||
} finally {
|
||||
@@ -78,6 +84,7 @@ export default function ToolDetailClient({ toolId, machineId }) {
|
||||
tailscaleEnabled,
|
||||
tailscaleUrl,
|
||||
activeProviders: getActiveProviders(),
|
||||
availableModels,
|
||||
cloudEnabled,
|
||||
};
|
||||
|
||||
|
||||
@@ -165,6 +165,7 @@ export default function ConfigGeneratorCard({
|
||||
baseUrl,
|
||||
apiKeys,
|
||||
activeProviders,
|
||||
availableModels = [],
|
||||
cloudEnabled,
|
||||
tunnelEnabled,
|
||||
tunnelPublicUrl,
|
||||
@@ -183,7 +184,7 @@ export default function ConfigGeneratorCard({
|
||||
const [coworkThinking, setCoworkThinking] = useState({});
|
||||
const [copilotTokens, setCopilotTokens] = useState({});
|
||||
const [copilotThinking, setCopilotThinking] = useState({});
|
||||
const [connectedModels, setConnectedModels] = useState(null);
|
||||
const connectedModels = availableModels;
|
||||
const [customBaseUrl, setCustomBaseUrl] = useState("");
|
||||
const [modelModalOpen, setModelModalOpen] = useState(false);
|
||||
const [configModalOpen, setConfigModalOpen] = useState(false);
|
||||
@@ -226,26 +227,6 @@ export default function ConfigGeneratorCard({
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (toolId !== "claude" && toolId !== "codex" && toolId !== "opencode" && toolId !== "cowork" && toolId !== "copilot") return;
|
||||
|
||||
let cancelled = false;
|
||||
const loadConnectedModels = async () => {
|
||||
try {
|
||||
const response = await fetch("/api/models/connected", { cache: "no-store" });
|
||||
if (!response.ok) throw new Error("Failed to load connected models");
|
||||
const data = await response.json();
|
||||
if (!cancelled) setConnectedModels(data.models || []);
|
||||
} catch (error) {
|
||||
console.log(`Error loading connected models for ${tool.name}:`, error);
|
||||
if (!cancelled) setConnectedModels([]);
|
||||
}
|
||||
};
|
||||
|
||||
loadConnectedModels();
|
||||
return () => { cancelled = true; };
|
||||
}, [toolId, tool.name]);
|
||||
|
||||
const addModel = (selected) => {
|
||||
if (!selected?.value || selectedModels.includes(selected.value)) return;
|
||||
setSelectedModels((current) => [...current, selected.value]);
|
||||
@@ -593,7 +574,7 @@ export default function ConfigGeneratorCard({
|
||||
title={toolId === "claude" && claudeModelSlot ? `Select ${claudeModelSlot} model` : toolId === "codex" ? "Select Codex model" : toolId === "opencode" ? "Add OpenCode model" : `Add model for ${tool.name}`}
|
||||
closeOnSelect={toolId === "claude" || toolId === "codex"}
|
||||
addedModelValues={toolId === "claude" ? Object.values(claudeModels).filter(Boolean) : toolId === "codex" ? [codexModel].filter(Boolean) : toolId === "opencode" ? opencodeModels : selectedModels}
|
||||
availableModels={toolId === "claude" || toolId === "codex" || toolId === "opencode" || toolId === "cowork" || toolId === "copilot" ? connectedModels : null}
|
||||
availableModels={availableModels}
|
||||
/>
|
||||
<ManualConfigModal isOpen={configModalOpen} onClose={() => setConfigModalOpen(false)} title={`${tool.name} configuration`} configs={configs} />
|
||||
</Card>
|
||||
|
||||
@@ -6,12 +6,11 @@ import { useCopyToClipboard } from "@/shared/hooks/useCopyToClipboard";
|
||||
import Image from "next/image";
|
||||
import ApiKeySelect from "./ApiKeySelect";
|
||||
|
||||
export default function DefaultToolCard({ toolId, tool, baseUrl, apiKeys, activeProviders = [], cloudEnabled = false, tunnelEnabled = false }) {
|
||||
export default function DefaultToolCard({ toolId, tool, baseUrl, apiKeys, activeProviders = [], availableModels = [], cloudEnabled = false, tunnelEnabled = false }) {
|
||||
const [copiedField, setCopiedField] = useState(null);
|
||||
const [showModelModal, setShowModelModal] = useState(false);
|
||||
const [modelValue, setModelValue] = useState("");
|
||||
const [selectedModels, setSelectedModels] = useState([]);
|
||||
const [connectedModels, setConnectedModels] = useState(null);
|
||||
const [isExpanded, setIsExpanded] = useState(true);
|
||||
|
||||
// Initialize state directly with computed value - no need for useEffect
|
||||
@@ -19,26 +18,6 @@ export default function DefaultToolCard({ toolId, tool, baseUrl, apiKeys, active
|
||||
apiKeys?.length > 0 ? apiKeys[0].key : ""
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (toolId !== "cursor") return;
|
||||
|
||||
let cancelled = false;
|
||||
const loadConnectedModels = async () => {
|
||||
try {
|
||||
const response = await fetch("/api/models/connected", { cache: "no-store" });
|
||||
if (!response.ok) throw new Error("Failed to load connected models");
|
||||
const data = await response.json();
|
||||
if (!cancelled) setConnectedModels(data.models || []);
|
||||
} catch (error) {
|
||||
console.log(`Error loading connected models for ${tool.name}:`, error);
|
||||
if (!cancelled) setConnectedModels([]);
|
||||
}
|
||||
};
|
||||
|
||||
loadConnectedModels();
|
||||
return () => { cancelled = true; };
|
||||
}, [toolId, tool.name]);
|
||||
|
||||
const replaceVars = (text) => {
|
||||
const keyToUse = (selectedApiKey && selectedApiKey.trim())
|
||||
? selectedApiKey
|
||||
@@ -340,7 +319,7 @@ export default function DefaultToolCard({ toolId, tool, baseUrl, apiKeys, active
|
||||
title={tool.modelSelection === "multiple" ? "Add Cursor custom model" : "Select Model"}
|
||||
closeOnSelect={tool.modelSelection !== "multiple"}
|
||||
addedModelValues={tool.modelSelection === "multiple" ? selectedModels : []}
|
||||
availableModels={toolId === "cursor" ? connectedModels : null}
|
||||
availableModels={availableModels}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
|
||||
@@ -22,6 +22,7 @@ export default function MitmToolCard({
|
||||
isWin,
|
||||
apiKeys,
|
||||
activeProviders,
|
||||
availableModels = [],
|
||||
hasActiveProviders,
|
||||
modelAliases = {},
|
||||
cloudEnabled,
|
||||
@@ -41,18 +42,20 @@ export default function MitmToolCard({
|
||||
const canRunWithoutPassword = isWin || hasCachedPassword || needsSudoPassword === false;
|
||||
|
||||
useEffect(() => {
|
||||
if (isExpanded) loadSavedMappings();
|
||||
}, [isExpanded]);
|
||||
if (!isExpanded) return;
|
||||
let cancelled = false;
|
||||
|
||||
const loadSavedMappings = async () => {
|
||||
try {
|
||||
const res = await fetch(`/api/cli-tools/antigravity-mitm/alias?tool=${tool.id}`);
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
if (Object.keys(data.aliases || {}).length > 0) setModelMappings(data.aliases);
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
};
|
||||
fetch(`/api/cli-tools/antigravity-mitm/alias?tool=${tool.id}`)
|
||||
.then((res) => res.ok ? res.json() : null)
|
||||
.then((data) => {
|
||||
if (!cancelled && Object.keys(data?.aliases || {}).length > 0) {
|
||||
setModelMappings(data.aliases);
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
|
||||
return () => { cancelled = true; };
|
||||
}, [isExpanded, tool.id]);
|
||||
|
||||
const saveMappings = useCallback(async (mappings) => {
|
||||
try {
|
||||
@@ -311,6 +314,7 @@ export default function MitmToolCard({
|
||||
selectedModel={currentEditingAlias ? modelMappings[currentEditingAlias] : null}
|
||||
activeProviders={activeProviders}
|
||||
modelAliases={modelAliases}
|
||||
availableModels={availableModels}
|
||||
title={`Select model for ${currentEditingAlias}`}
|
||||
/>
|
||||
</>
|
||||
|
||||
@@ -86,6 +86,10 @@ export default function CombosPage() {
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
if (res.ok) {
|
||||
const judgeModel = comboStrategies[id]?.judgeModel;
|
||||
if (judgeModel && !selectableModels.some((model) => model.fullModel === judgeModel)) {
|
||||
await handleSetComboStrategy(id, { judgeModel: "" });
|
||||
}
|
||||
await fetchData();
|
||||
setEditingCombo(null);
|
||||
} else {
|
||||
@@ -243,7 +247,8 @@ const STRATEGY_OPTIONS = [
|
||||
function ComboCard({ combo, modelCaps = {}, availableModels = [], copied, onCopy, onEdit, onDelete, strategy = {}, onSetStrategy }) {
|
||||
const [showJudgeSelect, setShowJudgeSelect] = useState(false);
|
||||
const current = strategy.fallbackStrategy || "fallback";
|
||||
const judge = strategy.judgeModel || "";
|
||||
const availableModelValues = new Set(availableModels.map((model) => model.fullModel));
|
||||
const judge = availableModelValues.has(strategy.judgeModel) ? strategy.judgeModel : "";
|
||||
const isFusion = current === "fusion";
|
||||
|
||||
return (
|
||||
@@ -303,7 +308,10 @@ function ComboCard({ combo, modelCaps = {}, availableModels = [], copied, onCopy
|
||||
<Select
|
||||
options={STRATEGY_OPTIONS}
|
||||
value={current}
|
||||
onChange={(e) => onSetStrategy({ fallbackStrategy: e.target.value })}
|
||||
onChange={(e) => onSetStrategy({
|
||||
fallbackStrategy: e.target.value,
|
||||
...(!judge && strategy.judgeModel ? { judgeModel: "" } : {}),
|
||||
})}
|
||||
selectClassName="py-1.5 text-xs"
|
||||
/>
|
||||
</div>
|
||||
@@ -452,9 +460,12 @@ function ModelItem({ id, index, model, isFirst, isLast, onEdit, onMoveUp, onMove
|
||||
}
|
||||
|
||||
function ComboFormModal({ isOpen, combo, onClose, onSave, availableModels = [], kindFilter = null }) {
|
||||
const availableModelValues = new Set(availableModels.map((model) => model.fullModel));
|
||||
// Initialize state with combo values - key prop on parent handles reset on remount
|
||||
const [name, setName] = useState(combo?.name || "");
|
||||
const [models, setModels] = useState(combo?.models || []);
|
||||
const [models, setModels] = useState(() => (
|
||||
(combo?.models || []).filter((model) => availableModelValues.has(model))
|
||||
));
|
||||
const [showModelSelect, setShowModelSelect] = useState(false);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [nameError, setNameError] = useState("");
|
||||
@@ -528,8 +539,9 @@ function ComboFormModal({ isOpen, combo, onClose, onSave, availableModels = [],
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!validateName(name)) return;
|
||||
const eligibleModels = models.filter((model) => availableModelValues.has(model));
|
||||
setSaving(true);
|
||||
await onSave({ name: name.trim(), models });
|
||||
await onSave({ name: name.trim(), models: eligibleModels });
|
||||
setSaving(false);
|
||||
};
|
||||
|
||||
|
||||
@@ -2,75 +2,49 @@
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { MITM_TOOLS } from "@/shared/constants/cliTools";
|
||||
import { getModelsByProviderId } from "@/shared/constants/models";
|
||||
import { isOpenAICompatibleProvider, isAnthropicCompatibleProvider } from "@/shared/constants/providers";
|
||||
import { MitmServerCard, MitmToolCard } from "@/app/(dashboard)/dashboard/cli-tools/components";
|
||||
|
||||
export default function MitmPageClient() {
|
||||
const [connections, setConnections] = useState([]);
|
||||
const [apiKeys, setApiKeys] = useState([]);
|
||||
const [modelAliases, setModelAliases] = useState({});
|
||||
const [availableModels, setAvailableModels] = useState([]);
|
||||
const [cloudEnabled, setCloudEnabled] = useState(false);
|
||||
const [expandedTool, setExpandedTool] = useState(null);
|
||||
const [mitmStatus, setMitmStatus] = useState({ running: false, certExists: false, dnsStatus: {}, hasCachedPassword: false });
|
||||
|
||||
useEffect(() => {
|
||||
fetchConnections();
|
||||
fetchApiKeys();
|
||||
fetchAliases();
|
||||
fetchCloudSettings();
|
||||
let cancelled = false;
|
||||
|
||||
Promise.all([
|
||||
fetch("/api/providers"),
|
||||
fetch("/api/keys"),
|
||||
fetch("/api/models/alias"),
|
||||
fetch("/api/models/connected", { cache: "no-store" }),
|
||||
fetch("/api/settings"),
|
||||
]).then(async ([connectionsRes, keysRes, aliasesRes, modelsRes, settingsRes]) => {
|
||||
const [connectionsData, keysData, aliasesData, modelsData, settingsData] = await Promise.all([
|
||||
connectionsRes.ok ? connectionsRes.json() : {},
|
||||
keysRes.ok ? keysRes.json() : {},
|
||||
aliasesRes.ok ? aliasesRes.json() : {},
|
||||
modelsRes.ok ? modelsRes.json() : {},
|
||||
settingsRes.ok ? settingsRes.json() : {},
|
||||
]);
|
||||
if (cancelled) return;
|
||||
|
||||
setConnections(connectionsData.connections || []);
|
||||
setApiKeys(keysData.keys || []);
|
||||
setModelAliases(aliasesData.aliases || {});
|
||||
setAvailableModels((modelsData.models || []).filter((model) => !model.disabled));
|
||||
setCloudEnabled(settingsData.cloudEnabled || false);
|
||||
}).catch(() => {});
|
||||
|
||||
return () => { cancelled = true; };
|
||||
}, []);
|
||||
|
||||
const fetchConnections = async () => {
|
||||
try {
|
||||
const res = await fetch("/api/providers");
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
setConnections(data.connections || []);
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
};
|
||||
|
||||
const fetchApiKeys = async () => {
|
||||
try {
|
||||
const res = await fetch("/api/keys");
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
setApiKeys(data.keys || []);
|
||||
}
|
||||
} 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");
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
setCloudEnabled(data.cloudEnabled || false);
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
};
|
||||
|
||||
const getActiveProviders = () => connections.filter(c => c.isActive !== false);
|
||||
|
||||
const hasActiveProviders = () => {
|
||||
const active = getActiveProviders();
|
||||
return active.some(conn =>
|
||||
getModelsByProviderId(conn.provider).length > 0 ||
|
||||
isOpenAICompatibleProvider(conn.provider) ||
|
||||
isAnthropicCompatibleProvider(conn.provider)
|
||||
);
|
||||
};
|
||||
const hasActiveProviders = () => availableModels.length > 0;
|
||||
|
||||
const mitmTools = Object.entries(MITM_TOOLS);
|
||||
|
||||
@@ -105,6 +79,7 @@ export default function MitmPageClient() {
|
||||
isWin={mitmStatus.isWin === true}
|
||||
apiKeys={apiKeys}
|
||||
activeProviders={getActiveProviders()}
|
||||
availableModels={availableModels}
|
||||
hasActiveProviders={hasActiveProviders()}
|
||||
modelAliases={modelAliases}
|
||||
cloudEnabled={cloudEnabled}
|
||||
|
||||
@@ -65,7 +65,7 @@ function ProviderModelsCard({ group, canManage, onSetModelDisabled, onSetModelsD
|
||||
<div className="min-w-0">
|
||||
<h2 className="truncate font-semibold text-text-main">{group.provider.name}</h2>
|
||||
<p className="mt-0.5 text-xs text-text-muted">
|
||||
{group.models.length} available model{group.models.length === 1 ? "" : "s"}
|
||||
{group.models.length} added model{group.models.length === 1 ? "" : "s"}
|
||||
</p>
|
||||
</div>
|
||||
<span className={`material-symbols-outlined ml-auto text-[18px] text-text-muted transition-transform ${expanded ? "rotate-180" : ""}`}>
|
||||
@@ -254,8 +254,8 @@ export default function ModelsPage() {
|
||||
</div>
|
||||
<p className="mt-1 text-sm text-text-muted">
|
||||
{canManage
|
||||
? "Select which connected-provider models are available through the API."
|
||||
: "Browse models currently available through your connected providers."}
|
||||
? "Manage models explicitly added from connected providers."
|
||||
: "Browse added models currently available through connected providers."}
|
||||
</p>
|
||||
</div>
|
||||
<label className="relative block w-full sm:w-80">
|
||||
@@ -276,7 +276,7 @@ export default function ModelsPage() {
|
||||
<p className="mt-1 text-lg font-semibold tabular-nums text-text-main">{providerCount}</p>
|
||||
</div>
|
||||
<div className="bg-surface px-4 py-3">
|
||||
<p className="text-xs font-medium text-text-muted">Available models</p>
|
||||
<p className="text-xs font-medium text-text-muted">Added models</p>
|
||||
<p className="mt-1 text-lg font-semibold tabular-nums text-text-main">{models.length}</p>
|
||||
</div>
|
||||
</section>
|
||||
@@ -296,7 +296,7 @@ export default function ModelsPage() {
|
||||
<span className="material-symbols-outlined text-[32px] text-text-muted">search_off</span>
|
||||
<p className="mt-2 text-sm text-text-muted">
|
||||
{models.length === 0
|
||||
? "No models are available. Add and activate a provider connection first."
|
||||
? "No models have been added. Open a connected provider and add a model first."
|
||||
: "No models match your search."}
|
||||
</p>
|
||||
</Card>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import PropTypes from "prop-types";
|
||||
import { CapacityBadges } from "@/shared/components";
|
||||
|
||||
export default function ModelRow({ model, fullModel, alias, copied, onCopy, testStatus, isCustom, isFree, onDeleteAlias, onTest, isTesting, onDisable, caps, thinkingSuffix }) {
|
||||
export default function ModelRow({ model, fullModel, alias, copied, onCopy, testStatus, isCustom, isAdded, isFree, onDeleteAlias, onAdd, onRemove, onTest, isTesting, onDisable, caps, thinkingSuffix }) {
|
||||
const displayModel = thinkingSuffix ? `${fullModel}(${thinkingSuffix})` : fullModel;
|
||||
const borderColor = testStatus === "ok"
|
||||
? "border-green-500/40"
|
||||
@@ -60,11 +60,20 @@ export default function ModelRow({ model, fullModel, alias, copied, onCopy, test
|
||||
{copied === `model-${model.id}` ? "Copied!" : "Copy"}
|
||||
</span>
|
||||
</div>
|
||||
{isCustom ? (
|
||||
{onAdd && !isAdded ? (
|
||||
<button
|
||||
onClick={onDeleteAlias}
|
||||
onClick={onAdd}
|
||||
className="ml-auto inline-flex items-center gap-1 rounded px-1.5 py-0.5 text-[10px] font-medium text-primary transition-colors hover:bg-primary/10"
|
||||
title="Add model to the dashboard catalog"
|
||||
>
|
||||
<span className="material-symbols-outlined text-sm">add</span>
|
||||
Add
|
||||
</button>
|
||||
) : onRemove || isCustom ? (
|
||||
<button
|
||||
onClick={onRemove || onDeleteAlias}
|
||||
className="ml-auto rounded p-0.5 text-text-muted opacity-100 transition-opacity hover:bg-red-500/10 hover:text-red-500 sm:opacity-0 sm:group-hover:opacity-100"
|
||||
title="Remove custom model"
|
||||
title="Remove model from the dashboard catalog"
|
||||
>
|
||||
<span className="material-symbols-outlined text-sm">close</span>
|
||||
</button>
|
||||
@@ -92,8 +101,11 @@ ModelRow.propTypes = {
|
||||
onCopy: PropTypes.func.isRequired,
|
||||
testStatus: PropTypes.oneOf(["ok", "error"]),
|
||||
isCustom: PropTypes.bool,
|
||||
isAdded: PropTypes.bool,
|
||||
isFree: PropTypes.bool,
|
||||
onDeleteAlias: PropTypes.func,
|
||||
onAdd: PropTypes.func,
|
||||
onRemove: PropTypes.func,
|
||||
onTest: PropTypes.func,
|
||||
isTesting: PropTypes.bool,
|
||||
onDisable: PropTypes.func,
|
||||
|
||||
@@ -1058,14 +1058,24 @@ export default function ProviderDetailPage() {
|
||||
...kiloFreeModels.filter((fm) => !models.some((m) => m.id === fm.id)),
|
||||
].filter((m) => { const k = getModelKind(m); return !k || k === "llm"; });
|
||||
const disabledSet = new Set(disabledModelIds);
|
||||
const addedModelIds = new Set(
|
||||
customModels
|
||||
.filter((entry) => (
|
||||
entry.providerAlias === providerStorageAlias
|
||||
&& (entry.kind || entry.type || "llm") === "llm"
|
||||
&& entry.id
|
||||
))
|
||||
.map((entry) => entry.id),
|
||||
);
|
||||
const displayModels = allModels.filter((m) => !disabledSet.has(m.id));
|
||||
const disabledDisplayModels = allModels.filter((m) => disabledSet.has(m.id));
|
||||
const customModelRows = getProviderCustomModelRows({
|
||||
customModels,
|
||||
modelAliases,
|
||||
providerAlias: providerStorageAlias,
|
||||
builtInModels: models,
|
||||
builtInModels: allModels,
|
||||
type: "llm",
|
||||
includeLegacyAliases: false,
|
||||
});
|
||||
|
||||
return (
|
||||
@@ -1091,6 +1101,7 @@ export default function ProviderDetailPage() {
|
||||
onTest={connections.length > 0 || isFreeNoAuth ? () => handleTestModel(model.id) : undefined}
|
||||
isTesting={testingModelIds.has(model.id)}
|
||||
isCustom
|
||||
isAdded
|
||||
isFree={false}
|
||||
caps={getCaps(`${providerId}/${model.id}`)}
|
||||
thinkingSuffix={resolveThinkingSuffix(model.id)}
|
||||
@@ -1117,7 +1128,11 @@ export default function ProviderDetailPage() {
|
||||
onTest={connections.length > 0 || isFreeNoAuth ? () => handleTestModel(model.id) : undefined}
|
||||
isTesting={testingModelIds.has(model.id)}
|
||||
isFree={model.isFree}
|
||||
onDisable={() => handleDisableModel(model.id)}
|
||||
isAdded={addedModelIds.has(model.id)}
|
||||
onAdd={() => handleAddCustomModel(model.id, "llm", providerStorageAlias)}
|
||||
onRemove={addedModelIds.has(model.id)
|
||||
? () => handleDeleteCustomModel(model.id, "llm", providerStorageAlias)
|
||||
: undefined}
|
||||
caps={getCaps(`${providerId}/${model.id}`)}
|
||||
thinkingSuffix={resolveThinkingSuffix(model.id)}
|
||||
/>
|
||||
@@ -1597,7 +1612,7 @@ export default function ProviderDetailPage() {
|
||||
<div className="mb-4 flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<h2 className="text-lg font-semibold">
|
||||
{"Available Models"}
|
||||
{"Provider Models"}
|
||||
</h2>
|
||||
{providerThinkingLevels && (
|
||||
<select
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
import { getUsers } from "@/lib/db";
|
||||
import { disableModels, enableModels, getDisabledModels } from "@/lib/disabledModelsDb";
|
||||
import { requireAdminUser, requireUsageDashboardUser } from "@/lib/auth/currentUser";
|
||||
import { AI_MODELS } from "@/shared/constants/models";
|
||||
import {
|
||||
AI_PROVIDERS,
|
||||
getProviderAlias,
|
||||
@@ -35,6 +34,18 @@ function getConnectionProviderAliases(connection) {
|
||||
return [...new Set([connection.provider, alias])];
|
||||
}
|
||||
|
||||
function getModelType(model) {
|
||||
return model?.kind || model?.type || "llm";
|
||||
}
|
||||
|
||||
function getAliasByFullModel(modelAliases) {
|
||||
return new Map(
|
||||
Object.entries(modelAliases)
|
||||
.filter(([, fullModel]) => typeof fullModel === "string")
|
||||
.map(([alias, fullModel]) => [fullModel, alias]),
|
||||
);
|
||||
}
|
||||
|
||||
function getProviderLabel(providerAlias) {
|
||||
const provider = getProviderByAlias(providerAlias) || AI_PROVIDERS[providerAlias];
|
||||
return {
|
||||
@@ -70,7 +81,9 @@ function getForbiddenResponse(error) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// GET /api/models/connected - List models from providers with a usable active connection.
|
||||
// GET /api/models/connected - List administrator-added LLMs from providers with
|
||||
// a usable active connection. Provider registries and live /models responses are
|
||||
// discovery sources only; a customModels record is the explicit availability source.
|
||||
export async function GET() {
|
||||
try {
|
||||
const user = await requireUsageDashboardUser();
|
||||
@@ -84,37 +97,28 @@ export async function GET() {
|
||||
getUsers(),
|
||||
]);
|
||||
|
||||
const connectionCountByAlias = new Map();
|
||||
const connectedProviderByAlias = new Map();
|
||||
for (const connection of connections) {
|
||||
if (!isViableConnection(connection)) continue;
|
||||
|
||||
if (
|
||||
isOpenAICompatibleProvider(connection.provider)
|
||||
|| isAnthropicCompatibleProvider(connection.provider)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const alias of getConnectionProviderAliases(connection)) {
|
||||
connectionCountByAlias.set(alias, (connectionCountByAlias.get(alias) || 0) + 1);
|
||||
if (!connectedProviderByAlias.has(alias)) {
|
||||
connectedProviderByAlias.set(alias, {
|
||||
providerId: connection.provider,
|
||||
providerAlias: getProviderAlias(connection.provider) || connection.provider,
|
||||
provider: getProviderLabel(connection.provider),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const staticModels = AI_MODELS
|
||||
.filter((model) => connectionCountByAlias.has(model.provider))
|
||||
.map((model) => {
|
||||
const providerAlias = getProviderAlias(model.provider) || model.provider;
|
||||
const disabled = disabledModels[providerAlias] || disabledModels[model.provider] || [];
|
||||
const caps = getCapabilitiesForModel(model.provider, model.model);
|
||||
|
||||
return {
|
||||
...model,
|
||||
provider: getProviderLabel(model.provider),
|
||||
providerAlias,
|
||||
fullModel: `${model.provider}/${model.model}`,
|
||||
alias: modelAliases[`${model.provider}/${model.model}`] || model.model,
|
||||
disabled: disabled.includes(model.model),
|
||||
caps: {
|
||||
vision: caps.vision,
|
||||
search: caps.search,
|
||||
reasoning: caps.reasoning,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
// Compatible providers are dynamic and therefore absent from AI_MODELS.
|
||||
// Their catalog is the explicit list maintained by an administrator on the
|
||||
// provider detail page. The provider-node ID is retained as the alias so
|
||||
@@ -140,39 +144,57 @@ export async function GET() {
|
||||
}
|
||||
}
|
||||
|
||||
const compatibleModels = [];
|
||||
const compatibleProviderByAlias = new Map();
|
||||
for (const [providerId, connection] of viableCompatibleConnections) {
|
||||
const provider = getCompatibleProviderLabel(providerId, nodeById.get(providerId), connection);
|
||||
const disabled = disabledModels[providerId] || [];
|
||||
compatibleProviderByAlias.set(providerId, {
|
||||
providerId,
|
||||
providerAlias: providerId,
|
||||
provider,
|
||||
});
|
||||
}
|
||||
|
||||
for (const customModel of customModels) {
|
||||
const kind = customModel.kind || customModel.type || "llm";
|
||||
if (customModel.providerAlias !== providerId || kind !== "llm" || !customModel.id) continue;
|
||||
const aliasByFullModel = getAliasByFullModel(modelAliases);
|
||||
const seenFullModels = new Set();
|
||||
const models = customModels
|
||||
.filter((customModel) => customModel?.id && getModelType(customModel) === "llm")
|
||||
.map((customModel) => {
|
||||
const providerEntry = connectedProviderByAlias.get(customModel.providerAlias)
|
||||
|| compatibleProviderByAlias.get(customModel.providerAlias);
|
||||
if (!providerEntry) return null;
|
||||
|
||||
const modelId = String(customModel.id).trim();
|
||||
if (!modelId) continue;
|
||||
if (!modelId) return null;
|
||||
|
||||
const fullModel = `${providerId}/${modelId}`;
|
||||
const caps = getCapabilitiesForModel(providerId, modelId);
|
||||
compatibleModels.push({
|
||||
provider,
|
||||
providerAlias: providerId,
|
||||
const storageAlias = customModel.providerAlias;
|
||||
const fullModel = `${storageAlias}/${modelId}`;
|
||||
if (seenFullModels.has(fullModel)) return null;
|
||||
seenFullModels.add(fullModel);
|
||||
|
||||
const disabled = new Set([
|
||||
...(disabledModels[storageAlias] || []),
|
||||
...(disabledModels[providerEntry.providerId] || []),
|
||||
...(disabledModels[providerEntry.providerAlias] || []),
|
||||
]);
|
||||
const caps = getCapabilitiesForModel(providerEntry.providerId, modelId);
|
||||
|
||||
return {
|
||||
provider: providerEntry.provider,
|
||||
providerAlias: storageAlias,
|
||||
model: modelId,
|
||||
name: customModel.name || modelId,
|
||||
fullModel,
|
||||
alias: modelAliases[fullModel] || modelId,
|
||||
disabled: disabled.includes(modelId),
|
||||
alias: aliasByFullModel.get(fullModel) || modelId,
|
||||
disabled: disabled.has(modelId),
|
||||
isCustom: true,
|
||||
caps: {
|
||||
vision: caps.vision,
|
||||
search: caps.search,
|
||||
reasoning: caps.reasoning,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const models = [...staticModels, ...compatibleModels]
|
||||
};
|
||||
})
|
||||
.filter(Boolean)
|
||||
.filter((model) => user.role === "admin" || !model.disabled)
|
||||
.sort((a, b) => (
|
||||
a.provider.name.localeCompare(b.provider.name)
|
||||
|
||||
@@ -50,13 +50,20 @@ function ModelItem({ index, model, isFirst, isLast, onEdit, onMoveUp, onMoveDown
|
||||
}
|
||||
|
||||
// Reusable Combo create/edit modal. forcePrefix auto-prepends to name.
|
||||
export default function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders, kindFilter = null, forcePrefix = "", title }) {
|
||||
export default function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders, availableModels = null, kindFilter = null, forcePrefix = "", title }) {
|
||||
// Strip prefix when editing existing combo so user only edits suffix
|
||||
const initialName = combo?.name
|
||||
? (forcePrefix && combo.name.startsWith(forcePrefix) ? combo.name.slice(forcePrefix.length) : combo.name)
|
||||
: "";
|
||||
const [name, setName] = useState(initialName);
|
||||
const [models, setModels] = useState(combo?.models || []);
|
||||
const availableModelValues = Array.isArray(availableModels)
|
||||
? new Set(availableModels.map((model) => model.fullModel))
|
||||
: null;
|
||||
const [models, setModels] = useState(() => (
|
||||
availableModelValues
|
||||
? (combo?.models || []).filter((model) => availableModelValues.has(model))
|
||||
: combo?.models || []
|
||||
));
|
||||
const [showModelSelect, setShowModelSelect] = useState(false);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [nameError, setNameError] = useState("");
|
||||
@@ -101,8 +108,11 @@ export default function ComboFormModal({ isOpen, combo, onClose, onSave, activeP
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!validateName(name)) return;
|
||||
const eligibleModels = availableModelValues
|
||||
? models.filter((model) => availableModelValues.has(model))
|
||||
: models;
|
||||
setSaving(true);
|
||||
await onSave({ name: forcePrefix + name.trim(), models });
|
||||
await onSave({ name: forcePrefix + name.trim(), models: eligibleModels });
|
||||
setSaving(false);
|
||||
};
|
||||
|
||||
@@ -169,6 +179,7 @@ export default function ComboFormModal({ isOpen, combo, onClose, onSave, activeP
|
||||
<ModelSelectModal isOpen={showModelSelect} onClose={() => setShowModelSelect(false)}
|
||||
onSelect={handleAddModel} onDeselect={handleDeselectModel}
|
||||
activeProviders={activeProviders} modelAliases={modelAliases}
|
||||
availableModels={availableModels}
|
||||
title="Add Model to Combo" kindFilter={kindFilter}
|
||||
addedModelValues={models} closeOnSelect={false} />
|
||||
</>
|
||||
|
||||
@@ -79,8 +79,11 @@ export default function ModelSelectModal({
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) fetchProviderNodes();
|
||||
}, [isOpen]);
|
||||
// An injected catalog already includes all provider display metadata. Avoid
|
||||
// the admin-only provider-nodes endpoint for member-facing selectors such
|
||||
// as the Combos page.
|
||||
if (isOpen && !Array.isArray(availableModels)) fetchProviderNodes();
|
||||
}, [isOpen, availableModels]);
|
||||
|
||||
const fetchCustomModels = async () => {
|
||||
try {
|
||||
|
||||
@@ -58,9 +58,14 @@ describe("GET /api/models/connected", () => {
|
||||
requireUsageDashboardUser.mockReset();
|
||||
getCapabilitiesForModel.mockReset();
|
||||
|
||||
getModelAliases.mockResolvedValue({ "alpha/enabled": "preferred-alpha" });
|
||||
getModelAliases.mockResolvedValue({ "preferred-alpha": "alpha-alias/enabled" });
|
||||
getDisabledModels.mockResolvedValue({ "alpha-alias": ["disabled"] });
|
||||
getCustomModels.mockResolvedValue([]);
|
||||
getCustomModels.mockResolvedValue([
|
||||
{ providerAlias: "alpha-alias", id: "enabled", name: "Enabled model", type: "llm" },
|
||||
{ providerAlias: "alpha-alias", id: "disabled", name: "Disabled model", type: "llm" },
|
||||
{ providerAlias: "alpha-alias", id: "embedding", name: "Embedding model", type: "embedding" },
|
||||
{ providerAlias: "beta-alias", id: "inactive", name: "Inactive provider model", type: "llm" },
|
||||
]);
|
||||
getProviderNodes.mockResolvedValue([]);
|
||||
getUsers.mockResolvedValue([{ id: "admin", role: "admin", isActive: true }]);
|
||||
getCapabilitiesForModel.mockReturnValue({ vision: false, search: true, reasoning: true });
|
||||
@@ -70,7 +75,7 @@ describe("GET /api/models/connected", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("returns every connected-provider model to an administrator, including disabled rows", async () => {
|
||||
it("returns added models for a connected provider to an administrator, including disabled rows", async () => {
|
||||
requireUsageDashboardUser.mockResolvedValue({ id: "admin", role: "admin" });
|
||||
|
||||
const response = await GET();
|
||||
@@ -79,22 +84,34 @@ describe("GET /api/models/connected", () => {
|
||||
expect(response.status).toBe(200);
|
||||
expect(body.models).toEqual([
|
||||
expect.objectContaining({
|
||||
fullModel: "alpha/disabled",
|
||||
fullModel: "alpha-alias/disabled",
|
||||
providerAlias: "alpha-alias",
|
||||
disabled: true,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
fullModel: "alpha/enabled",
|
||||
fullModel: "alpha-alias/enabled",
|
||||
alias: "preferred-alpha",
|
||||
disabled: false,
|
||||
caps: { vision: false, search: true, reasoning: true },
|
||||
}),
|
||||
]);
|
||||
expect(body.models).not.toEqual(expect.arrayContaining([
|
||||
expect.objectContaining({ fullModel: "beta/inactive" }),
|
||||
expect.objectContaining({ model: "embedding" }),
|
||||
expect.objectContaining({ fullModel: "beta-alias/inactive" }),
|
||||
]));
|
||||
});
|
||||
|
||||
it("does not include registry models without an explicit added-model record", async () => {
|
||||
requireUsageDashboardUser.mockResolvedValue({ id: "admin", role: "admin" });
|
||||
getCustomModels.mockResolvedValue([]);
|
||||
|
||||
const response = await GET();
|
||||
const body = await response.json();
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(body.models).toEqual([]);
|
||||
});
|
||||
|
||||
it("excludes disabled models for non-administrators", async () => {
|
||||
requireUsageDashboardUser.mockResolvedValue({ id: "member", role: "user" });
|
||||
|
||||
@@ -103,7 +120,7 @@ describe("GET /api/models/connected", () => {
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(body.models).toEqual([
|
||||
expect.objectContaining({ fullModel: "alpha/enabled", disabled: false }),
|
||||
expect.objectContaining({ fullModel: "alpha-alias/enabled", disabled: false }),
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -126,7 +143,7 @@ describe("GET /api/models/connected", () => {
|
||||
{ providerAlias: providerId, id: "gpt-company", name: "Company GPT", type: "llm" },
|
||||
{ providerAlias: providerId, id: "company-embed", name: "Company Embed", type: "embedding" },
|
||||
]);
|
||||
getModelAliases.mockResolvedValue({ [`${providerId}/gpt-company`]: "company-chat" });
|
||||
getModelAliases.mockResolvedValue({ "company-chat": `${providerId}/gpt-company` });
|
||||
|
||||
const response = await GET();
|
||||
const body = await response.json();
|
||||
|
||||
Reference in New Issue
Block a user