fix(models): store provider custom models by provider scope
This commit is contained in:
1 parent
8efacc1147
commit
707a91555d
6 files changed
+282
-118
No files matched your search
@@ -11,6 +11,7 @@ import { useCopyToClipboard } from "@/shared/hooks/useCopyToClipboard";
|
||||
import { useModelCaps } from "@/shared/hooks/useModelCaps";
|
||||
import { translate } from "@/i18n/runtime";
|
||||
import { fetchSuggestedModels } from "@/shared/utils/providerModelsFetcher";
|
||||
import { getProviderCustomModelRows } from "@/shared/utils/providerCustomModels";
|
||||
import ModelRow from "./ModelRow";
|
||||
import PassthroughModelsSection from "./PassthroughModelsSection";
|
||||
import CompatibleModelsSection from "./CompatibleModelsSection";
|
||||
@@ -45,6 +46,7 @@ export default function ProviderDetailPage() {
|
||||
const [showBulkProxyModal, setShowBulkProxyModal] = useState(false);
|
||||
const [selectedConnection, setSelectedConnection] = useState(null);
|
||||
const [modelAliases, setModelAliases] = useState({});
|
||||
const [customModels, setCustomModels] = useState([]);
|
||||
const [headerImgError, setHeaderImgError] = useState(false);
|
||||
const [modelTestResults, setModelTestResults] = useState({});
|
||||
const [modelsTestError, setModelsTestError] = useState("");
|
||||
@@ -224,6 +226,18 @@ export default function ProviderDetailPage() {
|
||||
}
|
||||
}, []);
|
||||
|
||||
const fetchCustomModels = useCallback(async () => {
|
||||
try {
|
||||
const res = await fetch("/api/models/custom", { cache: "no-store" });
|
||||
const data = await res.json();
|
||||
if (res.ok) {
|
||||
setCustomModels(data.models || []);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("Error fetching custom models:", error);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Fetch free models from Kilo API for kilocode provider
|
||||
useEffect(() => {
|
||||
if (providerId !== "kilocode") return;
|
||||
@@ -393,8 +407,9 @@ export default function ProviderDetailPage() {
|
||||
useEffect(() => {
|
||||
fetchConnections();
|
||||
fetchAliases();
|
||||
fetchCustomModels();
|
||||
fetchDisabledModels();
|
||||
}, [fetchConnections, fetchAliases, fetchDisabledModels]);
|
||||
}, [fetchConnections, fetchAliases, fetchCustomModels, fetchDisabledModels]);
|
||||
|
||||
// Fetch suggested models from provider's public API (if configured)
|
||||
useEffect(() => {
|
||||
@@ -435,6 +450,38 @@ export default function ProviderDetailPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddCustomModel = async (modelId, type = "llm", providerAliasOverride = providerStorageAlias) => {
|
||||
try {
|
||||
const res = await fetch("/api/models/custom", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ providerAlias: providerAliasOverride, id: modelId, type }),
|
||||
});
|
||||
if (res.ok) {
|
||||
await fetchCustomModels();
|
||||
if (typeof window !== "undefined") window.dispatchEvent(new CustomEvent("customModelChanged"));
|
||||
} else {
|
||||
const data = await res.json();
|
||||
alert(data.error || "Failed to add custom model");
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("Error adding custom model:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteCustomModel = async (modelId, type = "llm", providerAliasOverride = providerStorageAlias) => {
|
||||
try {
|
||||
const params = new URLSearchParams({ providerAlias: providerAliasOverride, id: modelId, type });
|
||||
const res = await fetch(`/api/models/custom?${params}`, { method: "DELETE" });
|
||||
if (res.ok) {
|
||||
await fetchCustomModels();
|
||||
if (typeof window !== "undefined") window.dispatchEvent(new CustomEvent("customModelChanged"));
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("Error deleting custom model:", error);
|
||||
}
|
||||
};
|
||||
|
||||
// Fetch Qoder model list and automatically add to available models
|
||||
const handleImportQoderModels = async () => {
|
||||
if (importingQoderModels) return;
|
||||
@@ -465,20 +512,14 @@ export default function ProviderDetailPage() {
|
||||
|
||||
// Qoder model ID format may be "qoder/auto" or "auto", need to remove prefix
|
||||
const cleanModelId = modelId.replace(/^qoder\//, "");
|
||||
const fullModel = `${providerStorageAlias}/${cleanModelId}`;
|
||||
|
||||
// Check if already exists
|
||||
if (Object.values(modelAliases).includes(fullModel)) {
|
||||
const alreadyExists = customModels.some(
|
||||
(entry) => entry.providerAlias === providerStorageAlias && entry.id === cleanModelId && (entry.kind || entry.type || "llm") === "llm"
|
||||
) || Object.values(modelAliases).includes(`${providerStorageAlias}/${cleanModelId}`);
|
||||
if (alreadyExists) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Use model ID as alias
|
||||
const alias = cleanModelId;
|
||||
if (modelAliases[alias]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
await handleSetAlias(cleanModelId, alias, providerStorageAlias);
|
||||
|
||||
await handleAddCustomModel(cleanModelId, "llm", providerStorageAlias);
|
||||
importedCount += 1;
|
||||
}
|
||||
|
||||
@@ -926,10 +967,13 @@ export default function ProviderDetailPage() {
|
||||
providerStorageAlias={providerStorageAlias}
|
||||
providerDisplayAlias={providerDisplayAlias}
|
||||
modelAliases={modelAliases}
|
||||
customModels={customModels}
|
||||
copied={copied}
|
||||
onCopy={copy}
|
||||
onSetAlias={handleSetAlias}
|
||||
onDeleteAlias={handleDeleteAlias}
|
||||
onAddCustomModel={(modelId) => handleAddCustomModel(modelId, "llm", providerStorageAlias)}
|
||||
onDeleteCustomModel={(modelId) => handleDeleteCustomModel(modelId, "llm", providerStorageAlias)}
|
||||
connections={connections}
|
||||
isAnthropic={isAnthropicCompatible}
|
||||
/>
|
||||
@@ -944,36 +988,33 @@ export default function ProviderDetailPage() {
|
||||
const disabledSet = new Set(disabledModelIds);
|
||||
const displayModels = allModels.filter((m) => !disabledSet.has(m.id));
|
||||
const disabledDisplayModels = allModels.filter((m) => disabledSet.has(m.id));
|
||||
// Custom models added by user (stored as aliases: modelId → providerAlias/modelId)
|
||||
const customModels = Object.entries(modelAliases)
|
||||
.filter(([alias, fullModel]) => {
|
||||
const prefix = `${providerStorageAlias}/`;
|
||||
if (!fullModel.startsWith(prefix)) return false;
|
||||
const modelId = fullModel.slice(prefix.length);
|
||||
// Only show if not already in hardcoded list
|
||||
// For passthroughModels, include all aliases (model IDs may contain slashes like "anthropic/claude-3")
|
||||
if (providerInfo.passthroughModels) return !models.some((m) => m.id === modelId);
|
||||
return !models.some((m) => m.id === modelId) && alias === modelId;
|
||||
})
|
||||
.map(([alias, fullModel]) => ({
|
||||
id: fullModel.slice(`${providerStorageAlias}/`.length),
|
||||
alias,
|
||||
fullModel,
|
||||
}));
|
||||
const customModelRows = getProviderCustomModelRows({
|
||||
customModels,
|
||||
modelAliases,
|
||||
providerAlias: providerStorageAlias,
|
||||
builtInModels: models,
|
||||
type: "llm",
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap gap-3">
|
||||
{/* Custom models first */}
|
||||
{customModels.map((model) => (
|
||||
{customModelRows.map((model) => (
|
||||
<ModelRow
|
||||
key={model.id}
|
||||
model={{ id: model.id }}
|
||||
key={`${model.source}-${model.fullModel}`}
|
||||
model={{ id: model.id, name: model.name }}
|
||||
fullModel={`${providerDisplayAlias}/${model.id}`}
|
||||
alias={model.alias}
|
||||
copied={copied}
|
||||
onCopy={copy}
|
||||
onSetAlias={() => {}}
|
||||
onDeleteAlias={() => handleDeleteAlias(model.alias)}
|
||||
onDeleteAlias={() => {
|
||||
if (model.source === "custom") {
|
||||
handleDeleteCustomModel(model.id, "llm", providerStorageAlias);
|
||||
} else {
|
||||
handleDeleteAlias(model.alias);
|
||||
}
|
||||
}}
|
||||
testStatus={modelTestResults[model.id]}
|
||||
onTest={connections.length > 0 || isFreeNoAuth ? () => handleTestModel(model.id) : undefined}
|
||||
isTesting={testingModelIds.has(model.id)}
|
||||
@@ -1034,7 +1075,10 @@ export default function ProviderDetailPage() {
|
||||
|
||||
{/* Suggested models from provider API — show only models not yet added */}
|
||||
{suggestedModels.length > 0 && (() => {
|
||||
const addedFullModels = new Set(Object.values(modelAliases));
|
||||
const addedFullModels = new Set([
|
||||
...Object.values(modelAliases),
|
||||
...customModelRows.map((model) => model.fullModel),
|
||||
]);
|
||||
const hardcodedIds = new Set(models.map((m) => m.id));
|
||||
const notAdded = suggestedModels.filter(
|
||||
(m) => !addedFullModels.has(`${providerStorageAlias}/${m.id}`) && !hardcodedIds.has(m.id)
|
||||
@@ -1048,8 +1092,7 @@ export default function ProviderDetailPage() {
|
||||
<button
|
||||
key={m.id}
|
||||
onClick={async () => {
|
||||
const alias = m.id.split("/").pop();
|
||||
await handleSetAlias(m.id, alias, providerStorageAlias);
|
||||
await handleAddCustomModel(m.id, "llm", providerStorageAlias);
|
||||
}}
|
||||
className="flex items-center gap-1 px-2.5 py-1.5 rounded-lg border border-black/10 dark:border-white/10 text-xs text-text-muted hover:text-primary hover:border-primary/40 hover:bg-primary/5 transition-colors"
|
||||
title={`${m.name} · ${(m.contextLength / 1000).toFixed(0)}k ctx`}
|
||||
@@ -1580,11 +1623,7 @@ export default function ProviderDetailPage() {
|
||||
providerAlias={providerStorageAlias}
|
||||
providerDisplayAlias={providerDisplayAlias}
|
||||
onSave={async (modelId) => {
|
||||
// For passthrough providers (OpenRouter), use last segment as alias to avoid slash conflicts
|
||||
const alias = providerInfo?.passthroughModels
|
||||
? modelId.split("/").pop()
|
||||
: modelId;
|
||||
await handleSetAlias(modelId, alias, providerStorageAlias);
|
||||
await handleAddCustomModel(modelId, "llm", providerStorageAlias);
|
||||
setShowAddCustomModel(false);
|
||||
}}
|
||||
onClose={() => setShowAddCustomModel(false)}
|
||||
|
||||
Reference in new issue
Block a user