From 9f702e230d5ee335d1083323fc7b016f099764f5 Mon Sep 17 00:00:00 2001 From: m4 Date: Tue, 21 Jul 2026 19:02:17 +0800 Subject: [PATCH] feat: adapter-spec-driven Registry Editor replaces legacy provider editors - New RegistryEditor renders providers, models, runtime parameters and capabilities from the backend's AdapterParameterSpec contract, with ModelAvailability badges, write-once credential staging, provider tests showing effective_request_options, revision CAS on save, and 422 detail paths surfaced verbatim (design doc sections 9, 11.8) - ConfigDialog now hosts the Registry Editor (admin-only; non-admins see a notice from the 403 path) - AccountDialog shows the session role and embeds an admin user-management section (list/create/promote/demote/password reset/delete, last-admin guard enforced server-side) - Deleted legacy ProviderProfiles/BuiltinProviders/RegistryBuiltinProviders editors, providerProfiles/legacyLlmConfig libs, the admin-token proxy and the /api/provider-profiles, /api/provider-actions, /api/default-model and /api/config BFF routes (design doc section 10 legacy removals) Co-Authored-By: Claude Opus 4.7 --- src/app/api/config/route.ts | 34 - src/app/api/default-model/route.ts | 12 - src/app/api/provider-actions/route.ts | 12 - src/app/api/provider-profiles/route.ts | 20 - src/app/components/AccountDialog.tsx | 32 +- src/app/components/BuiltinProvidersEditor.tsx | 1263 ------------- src/app/components/ConfigDialog.tsx | 21 +- src/app/components/ModelProvidersEditor.tsx | 5 - src/app/components/ProviderProfilesEditor.tsx | 1576 ----------------- .../RegistryBuiltinProvidersEditor.tsx | 1222 ------------- src/app/components/RegistryEditor.tsx | 1142 ++++++++++++ src/app/components/UserManagementSection.tsx | 239 +++ src/lib/legacyLlmConfig.ts | 227 --- src/lib/providerProfiles.ts | 371 ---- src/lib/registryDraft.ts | 156 ++ src/lib/server/evoscientistAdminProxy.ts | 175 -- 16 files changed, 1566 insertions(+), 4941 deletions(-) delete mode 100644 src/app/api/config/route.ts delete mode 100644 src/app/api/default-model/route.ts delete mode 100644 src/app/api/provider-actions/route.ts delete mode 100644 src/app/api/provider-profiles/route.ts delete mode 100644 src/app/components/BuiltinProvidersEditor.tsx delete mode 100644 src/app/components/ModelProvidersEditor.tsx delete mode 100644 src/app/components/ProviderProfilesEditor.tsx delete mode 100644 src/app/components/RegistryBuiltinProvidersEditor.tsx create mode 100644 src/app/components/RegistryEditor.tsx create mode 100644 src/app/components/UserManagementSection.tsx delete mode 100644 src/lib/legacyLlmConfig.ts delete mode 100644 src/lib/providerProfiles.ts create mode 100644 src/lib/registryDraft.ts delete mode 100644 src/lib/server/evoscientistAdminProxy.ts diff --git a/src/app/api/config/route.ts b/src/app/api/config/route.ts deleted file mode 100644 index 80e61b2..0000000 --- a/src/app/api/config/route.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { NextRequest, NextResponse } from "next/server"; -import { isAuthenticationEnabled } from "@/lib/auth"; -import { proxyEvoScientistAdminRequest } from "@/lib/server/evoscientistAdminProxy"; - -export const runtime = "nodejs"; - -async function proxy(request: NextRequest) { - if (!isAuthenticationEnabled()) { - return NextResponse.json( - { - error: - "Authentication required. Enable WebUI authentication to manage config.yaml.", - }, - { status: 403 } - ); - } - return proxyEvoScientistAdminRequest(request, { - upstreamPath: "/api/config", - requestLabel: "Built-in model configuration", - maxBodyBytes: 512_000, - }); -} - -export async function GET(request: NextRequest) { - return proxy(request); -} - -export async function PATCH(request: NextRequest) { - return proxy(request); -} - -export async function POST(request: NextRequest) { - return proxy(request); -} diff --git a/src/app/api/default-model/route.ts b/src/app/api/default-model/route.ts deleted file mode 100644 index e5b4356..0000000 --- a/src/app/api/default-model/route.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { NextRequest } from "next/server"; -import { proxyEvoScientistAdminRequest } from "@/lib/server/evoscientistAdminProxy"; - -export const runtime = "nodejs"; - -export async function PUT(request: NextRequest) { - return proxyEvoScientistAdminRequest(request, { - upstreamPath: "/api/default-model", - requestLabel: "Default model configuration", - maxBodyBytes: 8_192, - }); -} diff --git a/src/app/api/provider-actions/route.ts b/src/app/api/provider-actions/route.ts deleted file mode 100644 index 2ff5221..0000000 --- a/src/app/api/provider-actions/route.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { NextRequest } from "next/server"; -import { proxyEvoScientistAdminRequest } from "@/lib/server/evoscientistAdminProxy"; - -export const runtime = "nodejs"; - -export async function POST(request: NextRequest) { - return proxyEvoScientistAdminRequest(request, { - upstreamPath: "/api/provider-actions", - requestLabel: "Provider action", - maxBodyBytes: 32_768, - }); -} diff --git a/src/app/api/provider-profiles/route.ts b/src/app/api/provider-profiles/route.ts deleted file mode 100644 index 87e1619..0000000 --- a/src/app/api/provider-profiles/route.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { NextRequest } from "next/server"; -import { proxyEvoScientistAdminRequest } from "@/lib/server/evoscientistAdminProxy"; - -export const runtime = "nodejs"; - -async function proxy(request: NextRequest) { - return proxyEvoScientistAdminRequest(request, { - upstreamPath: "/api/provider-profiles", - requestLabel: "Provider configuration", - maxBodyBytes: 512_000, - }); -} - -export async function GET(request: NextRequest) { - return proxy(request); -} - -export async function PUT(request: NextRequest) { - return proxy(request); -} diff --git a/src/app/components/AccountDialog.tsx b/src/app/components/AccountDialog.tsx index 7013fa3..f4ec090 100644 --- a/src/app/components/AccountDialog.tsx +++ b/src/app/components/AccountDialog.tsx @@ -10,6 +10,7 @@ import { DialogTitle, } from "@/components/ui/dialog"; import { Button } from "@/components/ui/button"; +import { UserManagementSection } from "@/app/components/UserManagementSection"; interface AccountDialogProps { open: boolean; @@ -17,13 +18,16 @@ interface AccountDialogProps { } export function AccountDialog({ open, onOpenChange }: AccountDialogProps) { - const [username, setUsername] = useState(null); + const [account, setAccount] = useState<{ + username: string; + role: "admin" | "user"; + } | null>(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); useEffect(() => { if (!open) { - setUsername(null); + setAccount(null); setLoading(false); setError(null); return; @@ -36,6 +40,7 @@ export function AccountDialog({ open, onOpenChange }: AccountDialogProps) { .then(async (response) => { const body = (await response.json().catch(() => null)) as { username?: unknown; + role?: unknown; error?: unknown; } | null; if (!response.ok || typeof body?.username !== "string") { @@ -45,10 +50,13 @@ export function AccountDialog({ open, onOpenChange }: AccountDialogProps) { : "Account information is unavailable." ); } - return body.username; + return { + username: body.username, + role: body.role === "user" ? ("user" as const) : ("admin" as const), + }; }) .then((value) => { - if (!cancelled) setUsername(value); + if (!cancelled) setAccount(value); }) .catch((reason: unknown) => { if (!cancelled) { @@ -73,7 +81,7 @@ export function AccountDialog({ open, onOpenChange }: AccountDialogProps) { open={open} onOpenChange={onOpenChange} > - + User information @@ -85,7 +93,7 @@ export function AccountDialog({ open, onOpenChange }: AccountDialogProps) { className="rounded-md border border-border px-4 py-3" >

Username

- {loading || (!error && username === null) ? ( + {loading || (!error && account === null) ? (

Loading account...

@@ -96,12 +104,18 @@ export function AccountDialog({ open, onOpenChange }: AccountDialogProps) { > {error}

- ) : ( + ) : account ? (

- {username} + {account.username} + + {account.role} +

- )} + ) : null} + {account?.role === "admin" && ( + + )} - - ); - } - - const apiKeyField = selectedProvider.apiKeyField; - const apiKeyStatus = apiKeyField ? config.secrets[apiKeyField] : null; - const selectedEnvOverrides = providerFields(selectedProvider) - .map( - (field) => - config.env_overrides[field as keyof typeof config.env_overrides] - ) - .filter((name): name is string => Boolean(name)); - const providerConfigured = Boolean( - (apiKeyField && apiKeyStatus?.configured) || - (selectedProvider.baseUrlField && - values[selectedProvider.baseUrlField]) || - (selectedProvider.authModeField && - values[selectedProvider.authModeField] === "oauth") - ); - - return ( -
- {error && ( -
- {error} -
- )} - -
- - -
-
-
-
-

Default routing

-
-
-
- - -
-
- - - updateValue("model", event.target.value) - } - /> -
-
- - - updateValue("auxiliary_provider", event.target.value) - } - /> -
-
- - - updateValue("auxiliary_model", event.target.value) - } - /> -
-
- -