diff --git a/src/app/(dashboard)/dashboard/providers/[id]/page.tsx b/src/app/(dashboard)/dashboard/providers/[id]/page.tsx index 734626e850..a30f91139d 100644 --- a/src/app/(dashboard)/dashboard/providers/[id]/page.tsx +++ b/src/app/(dashboard)/dashboard/providers/[id]/page.tsx @@ -1341,6 +1341,7 @@ PassthroughModelRow.propTypes = { function CustomModelsSection({ providerId, providerAlias, copied, onCopy }) { const t = useTranslations("providers"); + const notify = useNotificationStore(); const [customModels, setCustomModels] = useState([]); const [newModelId, setNewModelId] = useState(""); const [newModelName, setNewModelName] = useState(""); @@ -1348,6 +1349,10 @@ function CustomModelsSection({ providerId, providerAlias, copied, onCopy }) { const [newEndpoints, setNewEndpoints] = useState(["chat"]); const [adding, setAdding] = useState(false); const [loading, setLoading] = useState(true); + const [editingModelId, setEditingModelId] = useState(null); + const [editingApiFormat, setEditingApiFormat] = useState("chat-completions"); + const [editingEndpoints, setEditingEndpoints] = useState(["chat"]); + const [savingModelId, setSavingModelId] = useState(null); const fetchCustomModels = useCallback(async () => { try { @@ -1410,6 +1415,61 @@ function CustomModelsSection({ providerId, providerAlias, copied, onCopy }) { } }; + const beginEdit = (model) => { + setEditingModelId(model.id); + setEditingApiFormat(model.apiFormat || "chat-completions"); + setEditingEndpoints( + Array.isArray(model.supportedEndpoints) && model.supportedEndpoints.length + ? model.supportedEndpoints + : ["chat"] + ); + }; + + const cancelEdit = () => { + setEditingModelId(null); + setEditingApiFormat("chat-completions"); + setEditingEndpoints(["chat"]); + setSavingModelId(null); + }; + + const saveEdit = async (modelId) => { + if (!editingModelId || editingModelId !== modelId) return; + if (!editingEndpoints.length) { + notify.error("Select at least one supported endpoint"); + return; + } + + setSavingModelId(modelId); + try { + const model = customModels.find((m) => m.id === modelId); + const res = await fetch("/api/provider-models", { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + provider: providerId, + modelId, + modelName: model?.name || modelId, + source: model?.source || "manual", + apiFormat: editingApiFormat, + supportedEndpoints: editingEndpoints, + }), + }); + + if (!res.ok) { + throw new Error("Failed to save model endpoint settings"); + } + + await fetchCustomModels(); + notify.success("Saved model endpoint settings"); + cancelEdit(); + } catch (e) { + console.error("Failed to save custom model:", e); + notify.error("Failed to save model endpoint settings"); + } finally { + setSavingModelId(null); + } + }; + return (

@@ -1554,14 +1614,82 @@ function CustomModelsSection({ providerId, providerAlias, copied, onCopy }) { )}

+ + {editingModelId === model.id && ( +
+
+
+ + +
+ +
+ Supported Endpoints +
+ {["chat", "embeddings", "images", "audio"].map((ep) => ( + + ))} +
+
+
+
+ + +
+
+ )} + +
+ +
- ); })} diff --git a/src/app/api/provider-models/route.ts b/src/app/api/provider-models/route.ts index 1ce7f41cb4..2c871a3a5a 100644 --- a/src/app/api/provider-models/route.ts +++ b/src/app/api/provider-models/route.ts @@ -3,6 +3,7 @@ import { getAllCustomModels, addCustomModel, removeCustomModel, + updateCustomModel, } from "@/lib/localDb"; import { isAuthenticated } from "@/shared/utils/apiAuth"; import { providerModelMutationSchema } from "@/shared/validation/schemas"; @@ -84,6 +85,59 @@ export async function POST(request) { } } +/** + * PUT /api/provider-models + * Body: { provider, modelId, modelName?, apiFormat?, supportedEndpoints? } + */ +export async function PUT(request) { + let rawBody; + try { + rawBody = await request.json(); + } catch { + return Response.json( + { error: { message: "Invalid JSON body", type: "validation_error" } }, + { status: 400 } + ); + } + + try { + if (!(await isAuthenticated(request))) { + return Response.json( + { error: { message: "Authentication required", type: "invalid_api_key" } }, + { status: 401 } + ); + } + + const validation = validateBody(providerModelMutationSchema, rawBody); + if (isValidationFailure(validation)) { + return Response.json({ error: validation.error }, { status: 400 }); + } + + const { provider, modelId, modelName, apiFormat, supportedEndpoints } = validation.data; + + const model = await updateCustomModel(provider, modelId, { + modelName, + apiFormat, + supportedEndpoints, + }); + + if (!model) { + return Response.json( + { error: { message: "Model not found", type: "not_found" } }, + { status: 404 } + ); + } + + return Response.json({ model }); + } catch (error) { + console.error("Error updating provider model:", error); + return Response.json( + { error: { message: "Failed to update provider model", type: "server_error" } }, + { status: 500 } + ); + } +} + /** * DELETE /api/provider-models?provider=&model= */ diff --git a/src/lib/db/models.ts b/src/lib/db/models.ts index 71f90a402d..74a93f3cc9 100644 --- a/src/lib/db/models.ts +++ b/src/lib/db/models.ts @@ -177,3 +177,38 @@ export async function removeCustomModel(providerId, modelId) { backupDbFile("pre-write"); return true; } + +export async function updateCustomModel(providerId, modelId, updates = {}) { + const db = getDbInstance(); + const row = db + .prepare("SELECT value FROM key_value WHERE namespace = 'customModels' AND key = ?") + .get(providerId); + if (!row) return null; + + const value = getKeyValue(row).value; + if (!value) return null; + + const models = JSON.parse(value); + const index = models.findIndex((m) => m.id === modelId); + if (index === -1) return null; + + const current = models[index]; + const next = { + ...current, + ...(updates.modelName !== undefined ? { name: updates.modelName || current.name } : {}), + ...(updates.apiFormat !== undefined ? { apiFormat: updates.apiFormat } : {}), + ...(updates.supportedEndpoints !== undefined + ? { supportedEndpoints: updates.supportedEndpoints } + : {}), + }; + + models[index] = next; + + db.prepare("UPDATE key_value SET value = ? WHERE namespace = 'customModels' AND key = ?").run( + JSON.stringify(models), + providerId + ); + + backupDbFile("pre-write"); + return next; +} diff --git a/src/lib/localDb.ts b/src/lib/localDb.ts index d90fa68939..83aac1d6fc 100644 --- a/src/lib/localDb.ts +++ b/src/lib/localDb.ts @@ -40,6 +40,7 @@ export { getAllCustomModels, addCustomModel, removeCustomModel, + updateCustomModel, } from "./db/models"; export {