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 +

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