diff --git a/autogpt_platform/frontend/src/app/(platform)/admin/layout.tsx b/autogpt_platform/frontend/src/app/(platform)/admin/layout.tsx index fec14244c7..eba736cc9e 100644 --- a/autogpt_platform/frontend/src/app/(platform)/admin/layout.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/admin/layout.tsx @@ -1,5 +1,6 @@ import { Sidebar } from "@/components/__legacy__/Sidebar"; import { Users, DollarSign, UserSearch, FileText } from "lucide-react"; +import { Gauge, ChatsCircle } from "@phosphor-icons/react/dist/ssr"; import { IconSliders } from "@/components/__legacy__/ui/icons"; @@ -21,11 +22,21 @@ const sidebarLinkGroups = [ href: "/admin/impersonation", icon: , }, + { + text: "Rate Limits", + href: "/admin/rate-limits", + icon: , + }, { text: "Execution Analytics", href: "/admin/execution-analytics", icon: , }, + { + text: "LLM Registry", + href: "/admin/llms", + icon: , + }, { text: "Admin User Management", href: "/admin/settings", diff --git a/autogpt_platform/frontend/src/app/(platform)/admin/llms/actions.ts b/autogpt_platform/frontend/src/app/(platform)/admin/llms/actions.ts new file mode 100644 index 0000000000..779bfba81e --- /dev/null +++ b/autogpt_platform/frontend/src/app/(platform)/admin/llms/actions.ts @@ -0,0 +1,374 @@ +"use server"; + +import { revalidatePath } from "next/cache"; +import { + createRequestHeaders, + getServerAuthToken, +} from "@/lib/autogpt-server-api/helpers"; +import { environment } from "@/services/environment"; + +const ADMIN_LLM_PATH = "/admin/llms"; + +// ============================================================================= +// Authenticated Fetch Helper +// ============================================================================= + +async function adminFetch( + endpoint: string, + options: RequestInit = {}, +): Promise<{ status: number; data: any }> { + const baseUrl = environment.getAGPTServerBaseUrl(); + const token = await getServerAuthToken(); + const headers = createRequestHeaders( + token, + !!options.body, + "application/json", + ); + + const response = await fetch(`${baseUrl}${endpoint}`, { + ...options, + headers: { + ...headers, + ...((options.headers as Record) || {}), + }, + }); + + let data: any = null; + if (response.status !== 204) { + const contentType = response.headers.get("content-type"); + const text = await response.text(); + if (text && contentType?.includes("application/json")) { + try { + data = JSON.parse(text); + } catch { + data = text; + } + } else { + data = text; + } + } + + if (!response.ok) { + const errorMessage = + data?.detail || data?.message || `HTTP ${response.status}`; + throw new Error(errorMessage); + } + + return { status: response.status, data }; +} + +// ============================================================================= +// Utilities +// ============================================================================= + +function getRequiredFormField( + formData: FormData, + fieldName: string, + displayName?: string, +): string { + const raw = formData.get(fieldName); + const value = raw ? String(raw).trim() : ""; + if (!value) { + throw new Error(`${displayName || fieldName} is required`); + } + return value; +} + +function getRequiredPositiveNumber( + formData: FormData, + fieldName: string, + displayName?: string, +): number { + const raw = formData.get(fieldName); + const value = Number(raw); + if (raw === null || raw === "" || !Number.isFinite(value) || value <= 0) { + throw new Error(`${displayName || fieldName} must be a positive number`); + } + return value; +} + +function getRequiredNumber( + formData: FormData, + fieldName: string, + displayName?: string, +): number { + const raw = formData.get(fieldName); + const value = Number(raw); + if (raw === null || raw === "" || !Number.isFinite(value)) { + throw new Error(`${displayName || fieldName} is required`); + } + return value; +} + +// ============================================================================= +// Provider Actions +// ============================================================================= + +export async function fetchLlmProviders() { + const { data } = await adminFetch("/api/llm/admin/providers"); + return data; +} + +export async function createLlmProviderAction(formData: FormData) { + const payload = { + 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", + metadata: {}, + }; + + await adminFetch("/api/llm/providers", { + method: "POST", + body: JSON.stringify(payload), + }); + revalidatePath(ADMIN_LLM_PATH); +} + +export async function deleteLlmProviderAction( + formData: FormData, +): Promise { + const providerName = getRequiredFormField( + formData, + "provider_id", + "Provider", + ); + await adminFetch(`/api/llm/providers/${providerName}`, { method: "DELETE" }); + revalidatePath(ADMIN_LLM_PATH); +} + +export async function updateLlmProviderAction(formData: FormData) { + const providerName = getRequiredFormField( + formData, + "provider_id", + "Provider", + ); + + const payload = { + 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", + metadata: {}, + }; + + await adminFetch(`/api/llm/providers/${providerName}`, { + method: "PATCH", + body: JSON.stringify(payload), + }); + revalidatePath(ADMIN_LLM_PATH); +} + +// ============================================================================= +// Model Actions +// ============================================================================= + +export async function fetchLlmModels(page?: number, pageSize?: number) { + const params = new URLSearchParams(); + if (page) params.set("page", String(page)); + if (pageSize) params.set("page_size", String(pageSize)); + params.set("enabled_only", "false"); + const query = params.toString() ? `?${params.toString()}` : ""; + const { data } = await adminFetch(`/api/llm/admin/models${query}`); + return data; +} + +export async function createLlmModelAction(formData: FormData) { + const creditCost = getRequiredNumber(formData, "credit_cost", "Credit cost"); + + const payload = { + slug: String(formData.get("slug") || "").trim(), + display_name: String(formData.get("display_name") || "").trim(), + description: formData.get("description") + ? String(formData.get("description")) + : undefined, + provider_id: getRequiredFormField(formData, "provider_id", "Provider"), + creator_id: formData.get("creator_id") + ? String(formData.get("creator_id")) + : undefined, + context_window: getRequiredPositiveNumber( + formData, + "context_window", + "Context window", + ), + max_output_tokens: formData.get("max_output_tokens") + ? Number(formData.get("max_output_tokens")) + : undefined, + price_tier: Number(formData.get("price_tier") || 1), + is_enabled: formData.getAll("is_enabled").includes("on"), + capabilities: {}, + metadata: {}, + costs: [ + { + unit: String(formData.get("unit") || "RUN"), + credit_cost: creditCost, + metadata: {}, + }, + ], + }; + + await adminFetch("/api/llm/models", { + method: "POST", + body: JSON.stringify(payload), + }); + revalidatePath(ADMIN_LLM_PATH); +} + +export async function updateLlmModelAction(formData: FormData) { + const modelSlug = getRequiredFormField(formData, "model_id", "Model"); + + const payload: Record = {}; + + if (formData.get("display_name")) + payload.display_name = String(formData.get("display_name")); + if (formData.get("description")) + payload.description = String(formData.get("description")); + if (formData.get("provider_id")) + payload.provider_id = String(formData.get("provider_id")); + if (formData.get("creator_id")) + payload.creator_id = String(formData.get("creator_id")); + if (formData.get("context_window")) + payload.context_window = Number(formData.get("context_window")); + if (formData.get("max_output_tokens")) + payload.max_output_tokens = Number(formData.get("max_output_tokens")); + if (formData.has("is_enabled")) + payload.is_enabled = formData.getAll("is_enabled").includes("on"); + + await adminFetch(`/api/llm/models/${modelSlug}`, { + method: "PATCH", + body: JSON.stringify(payload), + }); + revalidatePath(ADMIN_LLM_PATH); +} + +export async function toggleLlmModelAction(formData: FormData): Promise { + const modelSlug = getRequiredFormField(formData, "model_id", "Model"); + const shouldEnable = formData.get("is_enabled") === "true"; + + // Toggle is just a PATCH on is_enabled + await adminFetch(`/api/llm/models/${modelSlug}`, { + method: "PATCH", + body: JSON.stringify({ is_enabled: shouldEnable }), + }); + revalidatePath(ADMIN_LLM_PATH); +} + +export async function deleteLlmModelAction(formData: FormData): Promise { + const modelSlug = getRequiredFormField(formData, "model_id", "Model"); + await adminFetch(`/api/llm/models/${modelSlug}`, { method: "DELETE" }); + revalidatePath(ADMIN_LLM_PATH); +} + +export async function fetchLlmModelUsage(_modelId: string) { + // Not yet implemented in backend - return 0 + return { usage_count: 0 }; +} + +// ============================================================================= +// Migration Actions (not yet implemented in backend) +// ============================================================================= + +export async function fetchLlmMigrations(_includeReverted: boolean = false) { + return { migrations: [] }; +} + +export async function revertLlmMigrationAction( + _formData: FormData, +): Promise { + throw new Error("Migrations not yet implemented"); +} + +// ============================================================================= +// Creator Actions +// ============================================================================= + +export async function fetchLlmCreators() { + const { data } = await adminFetch(`/api/llm/creators`); + return data; +} + +export async function createLlmCreatorAction( + formData: FormData, +): Promise { + const payload = { + name: String(formData.get("name") || "").trim(), + display_name: String(formData.get("display_name") || "").trim(), + description: formData.get("description") + ? String(formData.get("description")) + : undefined, + website_url: formData.get("website_url") + ? String(formData.get("website_url")) + : undefined, + metadata: {}, + }; + + await adminFetch("/api/llm/creators", { + method: "POST", + body: JSON.stringify(payload), + }); + revalidatePath(ADMIN_LLM_PATH); +} + +export async function updateLlmCreatorAction( + formData: FormData, +): Promise { + const creatorName = getRequiredFormField(formData, "creator_id", "Creator"); + + const payload: Record = {}; + if (formData.get("display_name")) + payload.display_name = String(formData.get("display_name")); + if (formData.get("description")) + payload.description = String(formData.get("description")); + if (formData.get("website_url")) + payload.website_url = String(formData.get("website_url")); + + await adminFetch(`/api/llm/creators/${creatorName}`, { + method: "PATCH", + body: JSON.stringify(payload), + }); + revalidatePath(ADMIN_LLM_PATH); +} + +export async function deleteLlmCreatorAction( + formData: FormData, +): Promise { + const creatorName = getRequiredFormField(formData, "creator_id", "Creator"); + await adminFetch(`/api/llm/creators/${creatorName}`, { method: "DELETE" }); + revalidatePath(ADMIN_LLM_PATH); +} + +// ============================================================================= +// Recommended Model Actions +// ============================================================================= + +export async function setRecommendedModelAction( + formData: FormData, +): Promise { + const modelSlug = getRequiredFormField(formData, "model_id", "Model"); + + // Set recommended by updating the model + await adminFetch(`/api/llm/models/${modelSlug}`, { + method: "PATCH", + body: JSON.stringify({ is_recommended: true }), + }); + revalidatePath(ADMIN_LLM_PATH); +} diff --git a/autogpt_platform/frontend/src/app/(platform)/admin/llms/components/AddCreatorModal.tsx b/autogpt_platform/frontend/src/app/(platform)/admin/llms/components/AddCreatorModal.tsx new file mode 100644 index 0000000000..ab84c07092 --- /dev/null +++ b/autogpt_platform/frontend/src/app/(platform)/admin/llms/components/AddCreatorModal.tsx @@ -0,0 +1,147 @@ +"use client"; + +import { useState } from "react"; +import { Dialog } from "@/components/molecules/Dialog/Dialog"; +import { Button } from "@/components/atoms/Button/Button"; +import { createLlmCreatorAction } from "../actions"; +import { useRouter } from "next/navigation"; + +export function AddCreatorModal() { + 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 createLlmCreatorAction(formData); + setOpen(false); + router.refresh(); + } catch (err) { + setError(err instanceof Error ? err.message : "Failed to create creator"); + } finally { + setIsSubmitting(false); + } + } + + return ( + + + + + +
+ Add a new model creator (the organization that made/trained the + model). +
+ +
+
+
+ + +

+ Lowercase identifier (e.g., openai, meta, anthropic) +

+
+
+ + +
+
+ +
+ +