From 6da2dee62fb5144f66c217289e0cf7bfbc7607ad Mon Sep 17 00:00:00 2001 From: Bentlybro Date: Thu, 22 Jan 2026 13:08:29 +0000 Subject: [PATCH] Add edit and delete functionality for LLM providers Introduces backend API and frontend UI for editing and deleting LLM providers. Providers can only be deleted if they have no associated models. Includes new modals for editing and deleting providers, updates provider list to show model count and actions, and adds corresponding actions and API integration. --- .../backend/api/features/admin/llm_routes.py | 25 ++ .../backend/backend/server/v2/llm/db.py | 38 +++ .../src/app/(platform)/admin/llms/actions.ts | 47 ++++ .../llms/components/DeleteProviderModal.tsx | 127 +++++++++ .../llms/components/EditProviderModal.tsx | 263 ++++++++++++++++++ .../admin/llms/components/ProviderList.tsx | 23 ++ .../frontend/src/app/api/openapi.json | 51 +++- 7 files changed, 572 insertions(+), 2 deletions(-) create mode 100644 autogpt_platform/frontend/src/app/(platform)/admin/llms/components/DeleteProviderModal.tsx create mode 100644 autogpt_platform/frontend/src/app/(platform)/admin/llms/components/EditProviderModal.tsx diff --git a/autogpt_platform/backend/backend/api/features/admin/llm_routes.py b/autogpt_platform/backend/backend/api/features/admin/llm_routes.py index 4eb461ffba..eec442e568 100644 --- a/autogpt_platform/backend/backend/api/features/admin/llm_routes.py +++ b/autogpt_platform/backend/backend/api/features/admin/llm_routes.py @@ -98,6 +98,31 @@ async def update_llm_provider( return provider +@router.delete( + "/providers/{provider_id}", + summary="Delete LLM provider", + response_model=dict, +) +async def delete_llm_provider(provider_id: str): + """ + Delete an LLM provider. + + A provider can only be deleted if it has no associated models. + Delete all models from the provider first before deleting the provider. + """ + try: + await llm_db.delete_provider(provider_id) + await _refresh_runtime_state() + logger.info("Deleted LLM provider '%s'", provider_id) + return {"success": True, "message": "Provider deleted successfully"} + except ValueError as e: + logger.warning("Failed to delete provider '%s': %s", provider_id, e) + raise fastapi.HTTPException(status_code=400, detail=str(e)) + except Exception as e: + logger.exception("Failed to delete provider '%s': %s", provider_id, e) + raise fastapi.HTTPException(status_code=500, detail=str(e)) + + @router.get( "/models", summary="List LLM models", diff --git a/autogpt_platform/backend/backend/server/v2/llm/db.py b/autogpt_platform/backend/backend/server/v2/llm/db.py index 78f64796d5..765aba3222 100644 --- a/autogpt_platform/backend/backend/server/v2/llm/db.py +++ b/autogpt_platform/backend/backend/server/v2/llm/db.py @@ -151,6 +151,44 @@ async def upsert_provider( return _map_provider(record) +async def delete_provider(provider_id: str) -> bool: + """ + Delete an LLM provider. + + A provider can only be deleted if it has no associated models. + Due to onDelete: Restrict on LlmModel.Provider, the database will + block deletion if models exist. + + Args: + provider_id: UUID of the provider to delete + + Returns: + True if deleted successfully + + Raises: + ValueError: If provider not found or has associated models + """ + # Check if provider exists + provider = await prisma.models.LlmProvider.prisma().find_unique( + where={"id": provider_id}, + include={"Models": True}, + ) + if not provider: + raise ValueError(f"Provider with id '{provider_id}' not found") + + # Check if provider has any models + model_count = len(provider.Models) if provider.Models else 0 + if model_count > 0: + raise ValueError( + f"Cannot delete provider '{provider.displayName}' because it has " + f"{model_count} model(s). Delete all models first." + ) + + # Safe to delete + await prisma.models.LlmProvider.prisma().delete(where={"id": provider_id}) + return True + + async def list_models( provider_id: str | None = None, enabled_only: bool = False, diff --git a/autogpt_platform/frontend/src/app/(platform)/admin/llms/actions.ts b/autogpt_platform/frontend/src/app/(platform)/admin/llms/actions.ts index b2924d9015..7c8f2d67ae 100644 --- a/autogpt_platform/frontend/src/app/(platform)/admin/llms/actions.ts +++ b/autogpt_platform/frontend/src/app/(platform)/admin/llms/actions.ts @@ -6,6 +6,8 @@ import { revalidatePath } from "next/cache"; import { getV2ListLlmProviders, postV2CreateLlmProvider, + patchV2UpdateLlmProvider, + deleteV2DeleteLlmProvider, getV2ListLlmModels, postV2CreateLlmModel, patchV2UpdateLlmModel, @@ -74,6 +76,51 @@ export async function createLlmProviderAction(formData: FormData) { revalidatePath(ADMIN_LLM_PATH); } +export async function deleteLlmProviderAction( + formData: FormData, +): Promise { + const providerId = String(formData.get("provider_id")); + + const response = await deleteV2DeleteLlmProvider(providerId); + if (response.status !== 200) { + const errorData = response.data as { detail?: string }; + throw new Error(errorData?.detail || "Failed to delete provider"); + } + revalidatePath(ADMIN_LLM_PATH); +} + +export async function updateLlmProviderAction(formData: FormData) { + const providerId = String(formData.get("provider_id")); + + const payload: UpsertLlmProviderRequest = { + name: String(formData.get("name") || "").trim(), + display_name: String(formData.get("display_name") || "").trim(), + description: formData.get("description") + ? String(formData.get("description")) + : undefined, + default_credential_provider: formData.get("default_credential_provider") + ? String(formData.get("default_credential_provider")).trim() + : undefined, + default_credential_id: formData.get("default_credential_id") + ? String(formData.get("default_credential_id")).trim() + : undefined, + default_credential_type: formData.get("default_credential_type") + ? String(formData.get("default_credential_type")).trim() + : "api_key", + supports_tools: formData.getAll("supports_tools").includes("on"), + supports_json_output: formData.getAll("supports_json_output").includes("on"), + supports_reasoning: formData.getAll("supports_reasoning").includes("on"), + supports_parallel_tool: formData.getAll("supports_parallel_tool").includes("on"), + metadata: {}, + }; + + const response = await patchV2UpdateLlmProvider(providerId, payload); + if (response.status !== 200) { + throw new Error("Failed to update LLM provider"); + } + revalidatePath(ADMIN_LLM_PATH); +} + // ============================================================================= // Model Actions // ============================================================================= diff --git a/autogpt_platform/frontend/src/app/(platform)/admin/llms/components/DeleteProviderModal.tsx b/autogpt_platform/frontend/src/app/(platform)/admin/llms/components/DeleteProviderModal.tsx new file mode 100644 index 0000000000..24244d327e --- /dev/null +++ b/autogpt_platform/frontend/src/app/(platform)/admin/llms/components/DeleteProviderModal.tsx @@ -0,0 +1,127 @@ +"use client"; + +import { useState } from "react"; +import { useRouter } from "next/navigation"; +import { Dialog } from "@/components/molecules/Dialog/Dialog"; +import { Button } from "@/components/atoms/Button/Button"; +import type { LlmProvider } from "@/app/api/__generated__/models/llmProvider"; +import { deleteLlmProviderAction } from "../actions"; + +export function DeleteProviderModal({ provider }: { provider: LlmProvider }) { + const [open, setOpen] = useState(false); + const [isDeleting, setIsDeleting] = useState(false); + const [error, setError] = useState(null); + const router = useRouter(); + + const modelCount = provider.models?.length ?? 0; + const hasModels = modelCount > 0; + + async function handleDelete(formData: FormData) { + setIsDeleting(true); + setError(null); + try { + await deleteLlmProviderAction(formData); + setOpen(false); + router.refresh(); + } catch (err) { + setError(err instanceof Error ? err.message : "Failed to delete provider"); + } finally { + setIsDeleting(false); + } + } + + return ( + + + + + +
+
+
+
+ {hasModels ? "🚫" : "⚠️"} +
+
+

You are about to delete:

+

+ {provider.display_name}{" "} + + ({provider.name}) + +

+ {hasModels ? ( +

+ This provider has {modelCount} model(s). You must delete all + models before you can delete this provider. +

+ ) : ( +

+ This provider has no models and can be safely deleted. +

+ )} +
+
+
+ +
+ + + {error && ( +
+ {error} +
+ )} + + + + + +
+
+
+
+ ); +} diff --git a/autogpt_platform/frontend/src/app/(platform)/admin/llms/components/EditProviderModal.tsx b/autogpt_platform/frontend/src/app/(platform)/admin/llms/components/EditProviderModal.tsx new file mode 100644 index 0000000000..135a530b3d --- /dev/null +++ b/autogpt_platform/frontend/src/app/(platform)/admin/llms/components/EditProviderModal.tsx @@ -0,0 +1,263 @@ +"use client"; + +import { useState } from "react"; +import { Dialog } from "@/components/molecules/Dialog/Dialog"; +import { Button } from "@/components/atoms/Button/Button"; +import { updateLlmProviderAction } from "../actions"; +import { useRouter } from "next/navigation"; +import type { LlmProvider } from "@/app/api/__generated__/models/llmProvider"; + +export function EditProviderModal({ provider }: { provider: LlmProvider }) { + const [open, setOpen] = useState(false); + const [isSubmitting, setIsSubmitting] = useState(false); + const [error, setError] = useState(null); + const router = useRouter(); + + async function handleSubmit(formData: FormData) { + setIsSubmitting(true); + setError(null); + try { + await updateLlmProviderAction(formData); + setOpen(false); + router.refresh(); + } catch (err) { + setError( + err instanceof Error ? err.message : "Failed to update provider", + ); + } finally { + setIsSubmitting(false); + } + } + + return ( + + + + + +
+ Update provider configuration and capabilities. +
+ +
+ + + {/* Basic Information */} +
+
+

+ Basic Information +

+

+ Core provider details +

+
+
+
+ + +
+
+ + +
+
+
+ +