mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-04-08 03:00:28 -04:00
Refactor LLM registry to modular structure and improve admin UI
Moved LLM registry backend code into a dedicated llm_registry module with submodules for model types, notifications, schema utilities, and registry logic. Updated all backend imports to use the new structure. On the frontend, redesigned the admin LLM registry page with a dashboard layout, modularized data fetching, and improved forms for adding/editing providers and models. Updated UI components for better usability and maintainability.
This commit is contained in:
7
autogpt_platform/.claude/settings.local.json
Normal file
7
autogpt_platform/.claude/settings.local.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"mcp__github__get_commit"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,7 @@ from backend.data.block import (
|
||||
BlockSchemaInput,
|
||||
BlockSchemaOutput,
|
||||
)
|
||||
from backend.data.llm_model_types import ModelMetadata
|
||||
from backend.data.llm_registry import ModelMetadata
|
||||
from backend.data.model import (
|
||||
APIKeyCredentials,
|
||||
CredentialsField,
|
||||
|
||||
@@ -38,7 +38,7 @@ from backend.util.exceptions import (
|
||||
)
|
||||
from backend.util.settings import Config
|
||||
|
||||
from .llm_schema_utils import update_schema_with_llm_registry
|
||||
from backend.data.llm_registry import update_schema_with_llm_registry
|
||||
from .model import (
|
||||
ContributorDetails,
|
||||
Credentials,
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
"""
|
||||
LLM Registry module for managing LLM models, providers, and costs dynamically.
|
||||
|
||||
This module provides a database-driven registry system for LLM models,
|
||||
replacing hardcoded model configurations with a flexible admin-managed system.
|
||||
"""
|
||||
|
||||
from backend.data.llm_registry.model_types import ModelMetadata
|
||||
from backend.data.llm_registry import notifications
|
||||
from backend.data.llm_registry import schema_utils
|
||||
from backend.data.llm_registry.registry import (
|
||||
RegistryModel,
|
||||
RegistryModelCost,
|
||||
get_all_model_slugs_for_validation,
|
||||
get_default_model_slug,
|
||||
get_dynamic_model_slugs,
|
||||
get_fallback_model_for_disabled,
|
||||
get_llm_discriminator_mapping,
|
||||
get_llm_model_cost,
|
||||
get_llm_model_metadata,
|
||||
get_llm_model_schema_options,
|
||||
get_model_info,
|
||||
is_model_enabled,
|
||||
iter_dynamic_models,
|
||||
refresh_llm_registry,
|
||||
register_static_costs,
|
||||
register_static_metadata,
|
||||
)
|
||||
|
||||
# Re-export for backwards compatibility
|
||||
from backend.data.llm_registry.notifications import (
|
||||
REGISTRY_REFRESH_CHANNEL,
|
||||
publish_registry_refresh_notification,
|
||||
subscribe_to_registry_refresh,
|
||||
)
|
||||
from backend.data.llm_registry.schema_utils import (
|
||||
is_llm_model_field,
|
||||
refresh_llm_discriminator_mapping,
|
||||
refresh_llm_model_options,
|
||||
update_schema_with_llm_registry,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
# Types
|
||||
"ModelMetadata",
|
||||
"RegistryModel",
|
||||
"RegistryModelCost",
|
||||
# Registry functions
|
||||
"get_all_model_slugs_for_validation",
|
||||
"get_default_model_slug",
|
||||
"get_dynamic_model_slugs",
|
||||
"get_fallback_model_for_disabled",
|
||||
"get_llm_discriminator_mapping",
|
||||
"get_llm_model_cost",
|
||||
"get_llm_model_metadata",
|
||||
"get_llm_model_schema_options",
|
||||
"get_model_info",
|
||||
"is_model_enabled",
|
||||
"iter_dynamic_models",
|
||||
"refresh_llm_registry",
|
||||
"register_static_costs",
|
||||
"register_static_metadata",
|
||||
# Notifications
|
||||
"REGISTRY_REFRESH_CHANNEL",
|
||||
"publish_registry_refresh_notification",
|
||||
"subscribe_to_registry_refresh",
|
||||
# Schema utilities
|
||||
"is_llm_model_field",
|
||||
"refresh_llm_discriminator_mapping",
|
||||
"refresh_llm_model_options",
|
||||
"update_schema_with_llm_registry",
|
||||
]
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
"""Type definitions for LLM model metadata."""
|
||||
|
||||
from typing import NamedTuple
|
||||
|
||||
|
||||
class ModelMetadata(NamedTuple):
|
||||
"""Metadata for an LLM model."""
|
||||
|
||||
provider: str
|
||||
context_window: int
|
||||
max_output_tokens: int | None
|
||||
|
||||
@@ -89,3 +89,4 @@ async def subscribe_to_registry_refresh(
|
||||
exc_info=True,
|
||||
)
|
||||
raise
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
"""Core LLM registry implementation for managing models dynamically."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
@@ -7,13 +9,15 @@ from typing import Any, Iterable
|
||||
|
||||
import prisma.models
|
||||
|
||||
from backend.data.llm_model_types import ModelMetadata
|
||||
from backend.data.llm_registry.model_types import ModelMetadata
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class RegistryModelCost:
|
||||
"""Cost configuration for an LLM model."""
|
||||
|
||||
credit_cost: int
|
||||
credential_provider: str
|
||||
credential_id: str | None
|
||||
@@ -24,6 +28,8 @@ class RegistryModelCost:
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class RegistryModel:
|
||||
"""Represents a model in the LLM registry."""
|
||||
|
||||
slug: str
|
||||
display_name: str
|
||||
description: str | None
|
||||
@@ -44,11 +50,13 @@ _lock = asyncio.Lock()
|
||||
|
||||
|
||||
def register_static_metadata(metadata: dict[Any, ModelMetadata]) -> None:
|
||||
"""Register static metadata for legacy models (deprecated)."""
|
||||
_static_metadata.update({str(key): value for key, value in metadata.items()})
|
||||
_refresh_cached_schema()
|
||||
|
||||
|
||||
def register_static_costs(costs: dict[Any, int]) -> None:
|
||||
"""Register static costs for legacy models (deprecated)."""
|
||||
_static_costs.update({str(key): value for key, value in costs.items()})
|
||||
|
||||
|
||||
@@ -148,6 +156,7 @@ async def refresh_llm_registry() -> None:
|
||||
|
||||
|
||||
def _refresh_cached_schema() -> None:
|
||||
"""Refresh cached schema options and discriminator mapping."""
|
||||
new_options = _build_schema_options()
|
||||
_schema_options.clear()
|
||||
_schema_options.extend(new_options)
|
||||
@@ -166,11 +175,8 @@ def get_llm_model_metadata(slug: str) -> ModelMetadata | None:
|
||||
return _static_metadata.get(slug)
|
||||
|
||||
|
||||
# Removed get_llm_model_metadata_async - direct database queries don't work in executor context
|
||||
# The registry should be refreshed on startup via initialize_blocks() or rest_api lifespan
|
||||
|
||||
|
||||
def get_llm_model_cost(slug: str) -> tuple[RegistryModelCost, ...]:
|
||||
"""Get model cost configuration by slug."""
|
||||
if slug in _dynamic_models:
|
||||
return _dynamic_models[slug].costs
|
||||
cost_value = _static_costs.get(slug)
|
||||
@@ -209,6 +215,7 @@ def get_llm_discriminator_mapping() -> dict[str, str]:
|
||||
|
||||
|
||||
def get_dynamic_model_slugs() -> set[str]:
|
||||
"""Get all dynamic model slugs from the registry."""
|
||||
return set(_dynamic_models.keys())
|
||||
|
||||
|
||||
@@ -226,6 +233,7 @@ def get_all_model_slugs_for_validation() -> set[str]:
|
||||
|
||||
|
||||
def iter_dynamic_models() -> Iterable[RegistryModel]:
|
||||
"""Iterate over all dynamic models in the registry."""
|
||||
return tuple(_dynamic_models.values())
|
||||
|
||||
|
||||
@@ -286,7 +294,7 @@ def get_model_info(model_slug: str) -> RegistryModel | None:
|
||||
def get_default_model_slug() -> str:
|
||||
"""
|
||||
Get the default model slug to use for block defaults.
|
||||
|
||||
|
||||
Prefers "gpt-4o" if it exists and is enabled, otherwise returns
|
||||
the first enabled model from the registry, or "gpt-4o" as fallback.
|
||||
"""
|
||||
@@ -295,11 +303,12 @@ def get_default_model_slug() -> str:
|
||||
preferred_model = _dynamic_models.get(preferred_slug)
|
||||
if preferred_model and preferred_model.is_enabled:
|
||||
return preferred_slug
|
||||
|
||||
|
||||
# Find first enabled model
|
||||
for model in sorted(_dynamic_models.values(), key=lambda m: m.display_name.lower()):
|
||||
if model.is_enabled:
|
||||
return model.slug
|
||||
|
||||
|
||||
# Fallback to preferred slug even if not in registry (for backwards compatibility)
|
||||
return preferred_slug
|
||||
return preferred_slug
|
||||
|
||||
@@ -8,7 +8,11 @@ and model options from the LLM registry into block schemas.
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from backend.data import llm_registry
|
||||
from backend.data.llm_registry.registry import (
|
||||
get_all_model_slugs_for_validation,
|
||||
get_llm_discriminator_mapping,
|
||||
get_llm_model_schema_options,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -44,7 +48,7 @@ def refresh_llm_model_options(field_schema: dict[str, Any]) -> None:
|
||||
Existing graphs may have disabled models selected - they should pass validation
|
||||
and the fallback logic in llm_call() will handle using an alternative model.
|
||||
"""
|
||||
fresh_options = llm_registry.get_llm_model_schema_options()
|
||||
fresh_options = get_llm_model_schema_options()
|
||||
if not fresh_options:
|
||||
return
|
||||
|
||||
@@ -52,7 +56,7 @@ def refresh_llm_model_options(field_schema: dict[str, Any]) -> None:
|
||||
if "options" in field_schema:
|
||||
field_schema["options"] = fresh_options
|
||||
|
||||
all_known_slugs = llm_registry.get_all_model_slugs_for_validation()
|
||||
all_known_slugs = get_all_model_slugs_for_validation()
|
||||
if all_known_slugs and "enum" in field_schema:
|
||||
existing_enum = set(field_schema.get("enum", []))
|
||||
combined_enum = existing_enum | all_known_slugs
|
||||
@@ -70,7 +74,7 @@ def refresh_llm_discriminator_mapping(field_schema: dict[str, Any]) -> None:
|
||||
return
|
||||
|
||||
# Always refresh the mapping to get latest models
|
||||
fresh_mapping = llm_registry.get_llm_discriminator_mapping()
|
||||
fresh_mapping = get_llm_discriminator_mapping()
|
||||
if fresh_mapping:
|
||||
field_schema["discriminator_mapping"] = fresh_mapping
|
||||
|
||||
@@ -117,3 +121,4 @@ def update_schema_with_llm_registry(
|
||||
field_name,
|
||||
exc,
|
||||
)
|
||||
|
||||
@@ -44,7 +44,7 @@ from backend.integrations.providers import ProviderName
|
||||
from backend.util.json import loads as json_loads
|
||||
from backend.util.settings import Secrets
|
||||
|
||||
from .llm_schema_utils import update_schema_with_llm_registry
|
||||
from backend.data.llm_registry import update_schema_with_llm_registry
|
||||
|
||||
# Type alias for any provider name (including custom ones)
|
||||
AnyProviderName = str # Will be validated as ProviderName at runtime
|
||||
|
||||
@@ -10,7 +10,7 @@ import logging
|
||||
from backend.data import db, llm_registry
|
||||
from backend.data.block import BlockSchema, initialize_blocks
|
||||
from backend.data.block_cost_config import refresh_llm_costs
|
||||
from backend.data.llm_registry_notifications import subscribe_to_registry_refresh
|
||||
from backend.data.llm_registry import subscribe_to_registry_refresh
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ async def _refresh_runtime_state() -> None:
|
||||
logger.debug("Could not clear v2 builder cache: %s", e)
|
||||
|
||||
# Notify all executor services to refresh their registry cache
|
||||
from backend.data.llm_registry_notifications import (
|
||||
from backend.data.llm_registry import (
|
||||
publish_registry_refresh_notification,
|
||||
)
|
||||
|
||||
|
||||
@@ -79,7 +79,7 @@ async def event_broadcaster(manager: ConnectionManager):
|
||||
|
||||
async def registry_refresh_worker():
|
||||
"""Listen for LLM registry refresh notifications and broadcast to all clients."""
|
||||
from backend.data.llm_registry_notifications import REGISTRY_REFRESH_CHANNEL
|
||||
from backend.data.llm_registry import REGISTRY_REFRESH_CHANNEL
|
||||
from backend.data.redis_client import connect_async
|
||||
|
||||
redis = await connect_async()
|
||||
|
||||
@@ -31,14 +31,10 @@ export async function createLlmProviderAction(formData: FormData) {
|
||||
? String(formData.get("description"))
|
||||
: undefined,
|
||||
default_credential_provider: formData.get("default_credential_provider")
|
||||
? String(formData.get("default_credential_provider"))
|
||||
: undefined,
|
||||
default_credential_id: formData.get("default_credential_id")
|
||||
? String(formData.get("default_credential_id"))
|
||||
: undefined,
|
||||
default_credential_type: formData.get("default_credential_type")
|
||||
? String(formData.get("default_credential_type"))
|
||||
? String(formData.get("default_credential_provider")).trim()
|
||||
: undefined,
|
||||
default_credential_id: undefined, // Not needed - system uses credential_provider to lookup
|
||||
default_credential_type: "api_key", // Default to api_key
|
||||
supports_tools: formData.get("supports_tools") === "on",
|
||||
supports_json_output: formData.get("supports_json_output") !== "off",
|
||||
supports_reasoning: formData.get("supports_reasoning") === "on",
|
||||
@@ -52,13 +48,24 @@ export async function createLlmProviderAction(formData: FormData) {
|
||||
}
|
||||
|
||||
export async function createLlmModelAction(formData: FormData) {
|
||||
const providerId = String(formData.get("provider_id"));
|
||||
|
||||
// Fetch provider to get default credentials
|
||||
const api = new BackendApi();
|
||||
const providersResponse = await api.listAdminLlmProviders(false);
|
||||
const provider = providersResponse.providers.find((p) => p.id === providerId);
|
||||
|
||||
if (!provider) {
|
||||
throw new Error("Provider not found");
|
||||
}
|
||||
|
||||
const payload: CreateLlmModelRequest = {
|
||||
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: String(formData.get("provider_id")),
|
||||
provider_id: providerId,
|
||||
context_window: Number(formData.get("context_window") || 0),
|
||||
max_output_tokens: formData.get("max_output_tokens")
|
||||
? Number(formData.get("max_output_tokens"))
|
||||
@@ -69,21 +76,15 @@ export async function createLlmModelAction(formData: FormData) {
|
||||
costs: [
|
||||
{
|
||||
credit_cost: Number(formData.get("credit_cost") || 0),
|
||||
credential_provider: String(
|
||||
formData.get("credential_provider") || "",
|
||||
).trim(),
|
||||
credential_id: formData.get("credential_id")
|
||||
? String(formData.get("credential_id"))
|
||||
: undefined,
|
||||
credential_type: formData.get("credential_type")
|
||||
? String(formData.get("credential_type"))
|
||||
: undefined,
|
||||
credential_provider:
|
||||
provider.default_credential_provider || provider.name,
|
||||
credential_id: provider.default_credential_id || undefined,
|
||||
credential_type: provider.default_credential_type || "api_key",
|
||||
metadata: {},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const api = new BackendApi();
|
||||
await api.createAdminLlmModel(payload);
|
||||
revalidatePath(ADMIN_LLM_PATH);
|
||||
}
|
||||
|
||||
@@ -1,68 +1,102 @@
|
||||
import type { LlmProvider } from "@/lib/autogpt-server-api/types";
|
||||
import { createLlmModelAction } from "../actions";
|
||||
import { Button } from "@/components/atoms/Button/Button";
|
||||
|
||||
export function AddModelForm({ providers }: { providers: LlmProvider[] }) {
|
||||
interface Props {
|
||||
providers: LlmProvider[];
|
||||
}
|
||||
|
||||
export function AddModelForm({ providers }: Props) {
|
||||
return (
|
||||
<form
|
||||
action={createLlmModelAction}
|
||||
className="space-y-8 rounded-lg border border-border bg-card p-8 shadow-sm"
|
||||
>
|
||||
<div className="space-y-2">
|
||||
<h2 className="text-2xl font-semibold tracking-tight">Add Model</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
<form action={createLlmModelAction} className="space-y-6">
|
||||
<div className="space-y-1">
|
||||
<h3 className="text-lg font-semibold">Add Model</h3>
|
||||
<p className="text-sm text-gray-600">
|
||||
Register a new model slug, metadata, and pricing.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-8">
|
||||
<div className="space-y-5">
|
||||
<div className="space-y-6">
|
||||
{/* Basic Information */}
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-1">
|
||||
<h3 className="text-base font-semibold text-foreground">Basic Information</h3>
|
||||
<h3 className="text-sm font-semibold text-foreground">
|
||||
Basic Information
|
||||
</h3>
|
||||
<p className="text-xs text-muted-foreground">Core model details</p>
|
||||
</div>
|
||||
<div className="grid gap-5 md:grid-cols-2">
|
||||
<label className="space-y-2.5">
|
||||
<span className="text-sm font-medium text-foreground">Model Slug</span>
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<div className="space-y-2">
|
||||
<label
|
||||
htmlFor="slug"
|
||||
className="text-sm font-medium text-foreground"
|
||||
>
|
||||
Model Slug <span className="text-destructive">*</span>
|
||||
</label>
|
||||
<input
|
||||
id="slug"
|
||||
required
|
||||
name="slug"
|
||||
className="w-full rounded-md border border-input bg-background px-4 py-2.5 text-sm transition-colors focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/20"
|
||||
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm transition-colors placeholder:text-muted-foreground focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/20"
|
||||
placeholder="gpt-4.1-mini-2025-04-14"
|
||||
/>
|
||||
</label>
|
||||
<label className="space-y-2.5">
|
||||
<span className="text-sm font-medium text-foreground">Display Name</span>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label
|
||||
htmlFor="display_name"
|
||||
className="text-sm font-medium text-foreground"
|
||||
>
|
||||
Display Name <span className="text-destructive">*</span>
|
||||
</label>
|
||||
<input
|
||||
id="display_name"
|
||||
required
|
||||
name="display_name"
|
||||
className="w-full rounded-md border border-input bg-background px-4 py-2.5 text-sm transition-colors focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/20"
|
||||
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm transition-colors placeholder:text-muted-foreground focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/20"
|
||||
placeholder="GPT 4.1 Mini"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<label className="space-y-2.5">
|
||||
<span className="text-sm font-medium text-foreground">Description</span>
|
||||
<div className="space-y-2">
|
||||
<label
|
||||
htmlFor="description"
|
||||
className="text-sm font-medium text-foreground"
|
||||
>
|
||||
Description
|
||||
</label>
|
||||
<textarea
|
||||
id="description"
|
||||
name="description"
|
||||
rows={3}
|
||||
className="w-full rounded-md border border-input bg-background px-4 py-2.5 text-sm transition-colors focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/20"
|
||||
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm transition-colors placeholder:text-muted-foreground focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/20"
|
||||
placeholder="Optional description..."
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-5 border-t border-border pt-6">
|
||||
{/* Model Configuration */}
|
||||
<div className="space-y-4 border-t border-border pt-6">
|
||||
<div className="space-y-1">
|
||||
<h3 className="text-base font-semibold text-foreground">Model Configuration</h3>
|
||||
<p className="text-xs text-muted-foreground">Model capabilities and limits</p>
|
||||
<h3 className="text-sm font-semibold text-foreground">
|
||||
Model Configuration
|
||||
</h3>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Model capabilities and limits
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid gap-5 md:grid-cols-3">
|
||||
<label className="space-y-2.5">
|
||||
<span className="text-sm font-medium text-foreground">Provider</span>
|
||||
<div className="grid gap-4 sm:grid-cols-3">
|
||||
<div className="space-y-2">
|
||||
<label
|
||||
htmlFor="provider_id"
|
||||
className="text-sm font-medium text-foreground"
|
||||
>
|
||||
Provider <span className="text-destructive">*</span>
|
||||
</label>
|
||||
<select
|
||||
id="provider_id"
|
||||
required
|
||||
name="provider_id"
|
||||
className="w-full rounded-md border border-input bg-background px-4 py-2.5 text-sm transition-colors focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/20"
|
||||
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm transition-colors focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/20"
|
||||
defaultValue=""
|
||||
>
|
||||
<option value="" disabled>
|
||||
@@ -74,102 +108,100 @@ export function AddModelForm({ providers }: { providers: LlmProvider[] }) {
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<label className="space-y-2.5">
|
||||
<span className="text-sm font-medium text-foreground">Context Window</span>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label
|
||||
htmlFor="context_window"
|
||||
className="text-sm font-medium text-foreground"
|
||||
>
|
||||
Context Window <span className="text-destructive">*</span>
|
||||
</label>
|
||||
<input
|
||||
id="context_window"
|
||||
required
|
||||
type="number"
|
||||
name="context_window"
|
||||
className="w-full rounded-md border border-input bg-background px-4 py-2.5 text-sm transition-colors focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/20"
|
||||
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm transition-colors placeholder:text-muted-foreground focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/20"
|
||||
placeholder="128000"
|
||||
min={1}
|
||||
/>
|
||||
</label>
|
||||
<label className="space-y-2.5">
|
||||
<span className="text-sm font-medium text-foreground">Max Output Tokens</span>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label
|
||||
htmlFor="max_output_tokens"
|
||||
className="text-sm font-medium text-foreground"
|
||||
>
|
||||
Max Output Tokens
|
||||
</label>
|
||||
<input
|
||||
id="max_output_tokens"
|
||||
type="number"
|
||||
name="max_output_tokens"
|
||||
className="w-full rounded-md border border-input bg-background px-4 py-2.5 text-sm transition-colors focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/20"
|
||||
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm transition-colors placeholder:text-muted-foreground focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/20"
|
||||
placeholder="16384"
|
||||
min={1}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-5 border-t border-border pt-6">
|
||||
{/* Pricing */}
|
||||
<div className="space-y-4 border-t border-border pt-6">
|
||||
<div className="space-y-1">
|
||||
<h3 className="text-base font-semibold text-foreground">Pricing & Credentials</h3>
|
||||
<p className="text-xs text-muted-foreground">Cost and credential configuration</p>
|
||||
<h3 className="text-sm font-semibold text-foreground">Pricing</h3>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Credit cost per run (credentials are managed via the provider)
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid gap-5 md:grid-cols-2 lg:grid-cols-4">
|
||||
<label className="space-y-2.5">
|
||||
<span className="text-sm font-medium text-foreground">Credit Cost</span>
|
||||
<div className="grid gap-4 sm:grid-cols-1">
|
||||
<div className="space-y-2">
|
||||
<label
|
||||
htmlFor="credit_cost"
|
||||
className="text-sm font-medium text-foreground"
|
||||
>
|
||||
Credit Cost <span className="text-destructive">*</span>
|
||||
</label>
|
||||
<input
|
||||
id="credit_cost"
|
||||
required
|
||||
type="number"
|
||||
name="credit_cost"
|
||||
className="w-full rounded-md border border-input bg-background px-4 py-2.5 text-sm transition-colors focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/20"
|
||||
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm transition-colors placeholder:text-muted-foreground focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/20"
|
||||
placeholder="5"
|
||||
min={0}
|
||||
/>
|
||||
</label>
|
||||
<label className="space-y-2.5">
|
||||
<span className="text-sm font-medium text-foreground">Credential Provider</span>
|
||||
<input
|
||||
required
|
||||
name="credential_provider"
|
||||
className="w-full rounded-md border border-input bg-background px-4 py-2.5 text-sm transition-colors focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/20"
|
||||
placeholder="openai"
|
||||
/>
|
||||
</label>
|
||||
<label className="space-y-2.5">
|
||||
<span className="text-sm font-medium text-foreground">Credential ID</span>
|
||||
<input
|
||||
name="credential_id"
|
||||
className="w-full rounded-md border border-input bg-background px-4 py-2.5 text-sm transition-colors focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/20"
|
||||
placeholder="cred-id"
|
||||
/>
|
||||
</label>
|
||||
<label className="space-y-2.5">
|
||||
<span className="text-sm font-medium text-foreground">Credential Type</span>
|
||||
<input
|
||||
name="credential_type"
|
||||
defaultValue="api_key"
|
||||
className="w-full rounded-md border border-input bg-background px-4 py-2.5 text-sm transition-colors focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/20"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<p className="mt-3 text-xs text-muted-foreground">
|
||||
Credit cost is always in platform credits.
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Credit cost is always in platform credits. Credentials are inherited
|
||||
from the selected provider.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3 border-t border-border pt-6">
|
||||
<label className="flex items-center gap-3 text-sm font-medium">
|
||||
<input type="hidden" name="is_enabled" value="off" />
|
||||
<input
|
||||
type="checkbox"
|
||||
name="is_enabled"
|
||||
defaultChecked
|
||||
className="h-4 w-4 rounded border-input"
|
||||
/>
|
||||
{/* Enabled Toggle */}
|
||||
<div className="flex items-center gap-3 border-t border-border pt-6">
|
||||
<input type="hidden" name="is_enabled" value="off" />
|
||||
<input
|
||||
id="is_enabled"
|
||||
type="checkbox"
|
||||
name="is_enabled"
|
||||
defaultChecked
|
||||
className="h-4 w-4 rounded border-input"
|
||||
/>
|
||||
<label
|
||||
htmlFor="is_enabled"
|
||||
className="text-sm font-medium text-foreground"
|
||||
>
|
||||
Enabled by default
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end border-t border-border pt-6">
|
||||
<button
|
||||
type="submit"
|
||||
className="inline-flex items-center rounded-md bg-primary px-8 py-3 text-sm font-semibold text-primary-foreground shadow-sm transition-colors hover:bg-primary/90"
|
||||
>
|
||||
<div className="flex justify-end border-t border-gray-200 pt-4">
|
||||
<Button type="submit" variant="primary" size="small">
|
||||
Save Model
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,124 +1,204 @@
|
||||
import { createLlmProviderAction } from "../actions";
|
||||
import { Button } from "@/components/atoms/Button/Button";
|
||||
|
||||
export function AddProviderForm() {
|
||||
return (
|
||||
<form
|
||||
action={createLlmProviderAction}
|
||||
className="space-y-8 rounded-lg border border-border bg-card p-8 shadow-sm"
|
||||
>
|
||||
<div className="space-y-2">
|
||||
<h2 className="text-2xl font-semibold tracking-tight">Add Provider</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
<form action={createLlmProviderAction} className="space-y-6">
|
||||
<div className="space-y-1">
|
||||
<h3 className="text-lg font-semibold">Add Provider</h3>
|
||||
<p className="text-sm text-gray-600">
|
||||
Define a new upstream provider and default credential information.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-8">
|
||||
<div className="space-y-5">
|
||||
{/* Setup Instructions */}
|
||||
<div className="rounded-lg border border-blue-200 bg-blue-50 p-4">
|
||||
<div className="space-y-2">
|
||||
<h4 className="text-sm font-semibold text-blue-900">
|
||||
Before Adding a Provider
|
||||
</h4>
|
||||
<p className="text-xs text-blue-800">
|
||||
To use a new provider, you must first configure its credentials in the
|
||||
backend:
|
||||
</p>
|
||||
<ol className="list-inside list-decimal space-y-1 text-xs text-blue-800">
|
||||
<li>
|
||||
Add the credential to{" "}
|
||||
<code className="rounded bg-blue-100 px-1 py-0.5 font-mono">
|
||||
backend/integrations/credentials_store.py
|
||||
</code>{" "}
|
||||
with a UUID, provider name, and settings secret reference
|
||||
</li>
|
||||
<li>
|
||||
Add it to the{" "}
|
||||
<code className="rounded bg-blue-100 px-1 py-0.5 font-mono">
|
||||
PROVIDER_CREDENTIALS
|
||||
</code>{" "}
|
||||
dictionary in{" "}
|
||||
<code className="rounded bg-blue-100 px-1 py-0.5 font-mono">
|
||||
backend/data/block_cost_config.py
|
||||
</code>
|
||||
</li>
|
||||
<li>
|
||||
Use the <strong>same provider name</strong> in the "Credential Provider"
|
||||
field below that matches the key in{" "}
|
||||
<code className="rounded bg-blue-100 px-1 py-0.5 font-mono">
|
||||
PROVIDER_CREDENTIALS
|
||||
</code>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
{/* Basic Information */}
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-1">
|
||||
<h3 className="text-base font-semibold text-foreground">Basic Information</h3>
|
||||
<h3 className="text-sm font-semibold text-foreground">
|
||||
Basic Information
|
||||
</h3>
|
||||
<p className="text-xs text-muted-foreground">Core provider details</p>
|
||||
</div>
|
||||
<div className="grid gap-5 md:grid-cols-2">
|
||||
<label className="space-y-2.5">
|
||||
<span className="text-sm font-medium text-foreground">Provider Slug</span>
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<div className="space-y-2">
|
||||
<label
|
||||
htmlFor="name"
|
||||
className="text-sm font-medium text-foreground"
|
||||
>
|
||||
Provider Slug <span className="text-destructive">*</span>
|
||||
</label>
|
||||
<input
|
||||
id="name"
|
||||
required
|
||||
name="name"
|
||||
className="w-full rounded-md border border-input bg-background px-4 py-2.5 text-sm transition-colors focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/20"
|
||||
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm transition-colors placeholder:text-muted-foreground focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/20"
|
||||
placeholder="e.g. openai"
|
||||
/>
|
||||
</label>
|
||||
<label className="space-y-2.5">
|
||||
<span className="text-sm font-medium text-foreground">Display Name</span>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label
|
||||
htmlFor="display_name"
|
||||
className="text-sm font-medium text-foreground"
|
||||
>
|
||||
Display Name <span className="text-destructive">*</span>
|
||||
</label>
|
||||
<input
|
||||
id="display_name"
|
||||
required
|
||||
name="display_name"
|
||||
className="w-full rounded-md border border-input bg-background px-4 py-2.5 text-sm transition-colors focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/20"
|
||||
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm transition-colors placeholder:text-muted-foreground focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/20"
|
||||
placeholder="OpenAI"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<label className="space-y-2.5">
|
||||
<span className="text-sm font-medium text-foreground">Description</span>
|
||||
<div className="space-y-2">
|
||||
<label
|
||||
htmlFor="description"
|
||||
className="text-sm font-medium text-foreground"
|
||||
>
|
||||
Description
|
||||
</label>
|
||||
<textarea
|
||||
id="description"
|
||||
name="description"
|
||||
rows={3}
|
||||
className="w-full rounded-md border border-input bg-background px-4 py-2.5 text-sm transition-colors focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/20"
|
||||
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm transition-colors placeholder:text-muted-foreground focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/20"
|
||||
placeholder="Optional description..."
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="space-y-5 border-t border-border pt-6">
|
||||
<div className="space-y-1">
|
||||
<h3 className="text-base font-semibold text-foreground">Default Credentials</h3>
|
||||
<p className="text-xs text-muted-foreground">Default credential configuration</p>
|
||||
</div>
|
||||
<div className="grid gap-5 md:grid-cols-3">
|
||||
<label className="space-y-2.5">
|
||||
<span className="text-sm font-medium text-foreground">Credential Provider</span>
|
||||
<input
|
||||
name="default_credential_provider"
|
||||
className="w-full rounded-md border border-input bg-background px-4 py-2.5 text-sm transition-colors focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/20"
|
||||
placeholder="openai"
|
||||
/>
|
||||
</label>
|
||||
<label className="space-y-2.5">
|
||||
<span className="text-sm font-medium text-foreground">Credential ID</span>
|
||||
<input
|
||||
name="default_credential_id"
|
||||
className="w-full rounded-md border border-input bg-background px-4 py-2.5 text-sm transition-colors focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/20"
|
||||
placeholder="cred-id"
|
||||
/>
|
||||
</label>
|
||||
<label className="space-y-2.5">
|
||||
<span className="text-sm font-medium text-foreground">Credential Type</span>
|
||||
<input
|
||||
name="default_credential_type"
|
||||
defaultValue="api_key"
|
||||
className="w-full rounded-md border border-input bg-background px-4 py-2.5 text-sm transition-colors focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/20"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-5 border-t border-border pt-6">
|
||||
{/* Default Credentials */}
|
||||
<div className="space-y-4 border-t border-border pt-6">
|
||||
<div className="space-y-1">
|
||||
<h3 className="text-base font-semibold text-foreground">Capabilities</h3>
|
||||
<h3 className="text-sm font-semibold text-foreground">
|
||||
Default Credentials
|
||||
</h3>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Credential provider name that matches the key in{" "}
|
||||
<code className="rounded bg-muted px-1 py-0.5 font-mono text-xs">
|
||||
PROVIDER_CREDENTIALS
|
||||
</code>
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label
|
||||
htmlFor="default_credential_provider"
|
||||
className="text-sm font-medium text-foreground"
|
||||
>
|
||||
Credential Provider <span className="text-destructive">*</span>
|
||||
</label>
|
||||
<input
|
||||
id="default_credential_provider"
|
||||
name="default_credential_provider"
|
||||
required
|
||||
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm transition-colors placeholder:text-muted-foreground focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/20"
|
||||
placeholder="openai"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
<strong>Important:</strong> This must exactly match the key in the{" "}
|
||||
<code className="rounded bg-muted px-1 py-0.5 font-mono text-xs">
|
||||
PROVIDER_CREDENTIALS
|
||||
</code>{" "}
|
||||
dictionary in{" "}
|
||||
<code className="rounded bg-muted px-1 py-0.5 font-mono text-xs">
|
||||
block_cost_config.py
|
||||
</code>
|
||||
. Common values: "openai", "anthropic", "groq", "open_router", etc.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Capabilities */}
|
||||
<div className="space-y-4 border-t border-border pt-6">
|
||||
<div className="space-y-1">
|
||||
<h3 className="text-sm font-semibold text-foreground">
|
||||
Capabilities
|
||||
</h3>
|
||||
<p className="text-xs text-muted-foreground">Provider feature flags</p>
|
||||
</div>
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<div className="grid gap-3 sm:grid-cols-2">
|
||||
{[
|
||||
{ name: "supports_tools", label: "Supports tools" },
|
||||
{ name: "supports_json_output", label: "Supports JSON output" },
|
||||
{ name: "supports_reasoning", label: "Supports reasoning" },
|
||||
{ name: "supports_parallel_tool", label: "Supports parallel tool calls" },
|
||||
{
|
||||
name: "supports_parallel_tool",
|
||||
label: "Supports parallel tool calls",
|
||||
},
|
||||
].map(({ name, label }) => (
|
||||
<label key={name} className="flex items-center gap-3 rounded-md border border-border bg-muted/30 px-4 py-3 text-sm font-medium transition-colors hover:bg-muted/50">
|
||||
<div
|
||||
key={name}
|
||||
className="flex items-center gap-3 rounded-md border border-border bg-muted/30 px-4 py-3 transition-colors hover:bg-muted/50"
|
||||
>
|
||||
<input type="hidden" name={name} value="off" />
|
||||
<input
|
||||
id={name}
|
||||
type="checkbox"
|
||||
name={name}
|
||||
defaultChecked={name !== "supports_reasoning" && name !== "supports_parallel_tool"}
|
||||
defaultChecked={
|
||||
name !== "supports_reasoning" &&
|
||||
name !== "supports_parallel_tool"
|
||||
}
|
||||
className="h-4 w-4 rounded border-input"
|
||||
/>
|
||||
{label}
|
||||
</label>
|
||||
<label
|
||||
htmlFor={name}
|
||||
className="text-sm font-medium text-foreground"
|
||||
>
|
||||
{label}
|
||||
</label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end border-t border-border pt-6">
|
||||
<button
|
||||
type="submit"
|
||||
className="inline-flex items-center rounded-md bg-primary px-8 py-3 text-sm font-semibold text-primary-foreground shadow-sm transition-colors hover:bg-primary/90"
|
||||
>
|
||||
<div className="flex justify-end border-t border-gray-200 pt-4">
|
||||
<Button type="submit" variant="primary" size="small">
|
||||
Save Provider
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -24,12 +24,9 @@ export function EditModelModal({
|
||||
styling={{ maxWidth: "768px", maxHeight: "90vh", overflowY: "auto" }}
|
||||
>
|
||||
<Dialog.Trigger>
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex items-center rounded border border-input px-3 py-1 text-xs font-semibold hover:bg-muted"
|
||||
>
|
||||
<Button variant="outline" size="small" className="min-w-0">
|
||||
Edit
|
||||
</button>
|
||||
</Button>
|
||||
</Dialog.Trigger>
|
||||
<Dialog.Content>
|
||||
<div className="mb-4 text-sm text-muted-foreground">
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { CaretDown } from "@phosphor-icons/react";
|
||||
import type { LlmModel, LlmProvider } from "@/lib/autogpt-server-api/types";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/molecules/DropdownMenu/DropdownMenu";
|
||||
import { Button } from "@/components/atoms/Button/Button";
|
||||
import { AddProviderForm } from "./AddProviderForm";
|
||||
import { AddModelForm } from "./AddModelForm";
|
||||
import { ProviderList } from "./ProviderList";
|
||||
import { ModelsTable } from "./ModelsTable";
|
||||
|
||||
interface Props {
|
||||
providers: LlmProvider[];
|
||||
models: LlmModel[];
|
||||
}
|
||||
|
||||
type FormType = "model" | "provider" | null;
|
||||
|
||||
export function LlmRegistryDashboard({ providers, models }: Props) {
|
||||
const [activeForm, setActiveForm] = useState<FormType>(null);
|
||||
|
||||
function handleFormSelect(type: FormType) {
|
||||
setActiveForm(activeForm === type ? null : type);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx-auto p-6">
|
||||
<div className="flex flex-col gap-6">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold">LLM Registry</h1>
|
||||
<p className="text-gray-500">
|
||||
Manage supported providers, models, and credit pricing
|
||||
</p>
|
||||
</div>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="primary" size="small" className="gap-2">
|
||||
Add New
|
||||
<CaretDown className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem onClick={() => handleFormSelect("model")}>
|
||||
Model
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => handleFormSelect("provider")}>
|
||||
Provider
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
|
||||
{/* Add Forms Section */}
|
||||
{activeForm && (
|
||||
<div className="rounded-lg border bg-white p-6 shadow-sm">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<h2 className="text-xl font-semibold">
|
||||
Add {activeForm === "model" ? "Model" : "Provider"}
|
||||
</h2>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setActiveForm(null)}
|
||||
className="text-sm text-gray-600 hover:text-gray-900"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
{activeForm === "model" ? (
|
||||
<AddModelForm providers={providers} />
|
||||
) : (
|
||||
<AddProviderForm />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Providers Section */}
|
||||
<div className="rounded-lg border bg-white p-6 shadow-sm">
|
||||
<div className="mb-4">
|
||||
<h2 className="text-xl font-semibold">Providers</h2>
|
||||
<p className="mt-1 text-sm text-gray-600">
|
||||
Default credentials and feature flags for upstream vendors
|
||||
</p>
|
||||
</div>
|
||||
<ProviderList providers={providers} />
|
||||
</div>
|
||||
|
||||
{/* Models Section */}
|
||||
<div className="rounded-lg border bg-white p-6 shadow-sm">
|
||||
<div className="mb-4">
|
||||
<h2 className="text-xl font-semibold">Models</h2>
|
||||
<p className="mt-1 text-sm text-gray-600">
|
||||
Toggle availability, adjust context windows, and update credit
|
||||
pricing
|
||||
</p>
|
||||
</div>
|
||||
<ModelsTable models={models} providers={providers} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { LlmModel, LlmProvider } from "@/lib/autogpt-server-api/types";
|
||||
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
@@ -8,7 +7,7 @@ import {
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/atoms/Table/Table";
|
||||
|
||||
import { Button } from "@/components/atoms/Button/Button";
|
||||
import { toggleLlmModelAction } from "../actions";
|
||||
import { DeleteModelModal } from "./DeleteModelModal";
|
||||
import { EditModelModal } from "./EditModelModal";
|
||||
@@ -43,7 +42,7 @@ export function ModelsTable({
|
||||
<TableHead>Max Output</TableHead>
|
||||
<TableHead>Cost</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
<TableHead className="text-right">Actions</TableHead>
|
||||
<TableHead>Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
@@ -95,16 +94,16 @@ export function ModelsTable({
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span
|
||||
className={`inline-flex rounded-full px-2 py-0.5 text-xs font-semibold ${
|
||||
className={`inline-flex rounded-full px-2.5 py-1 text-xs font-semibold ${
|
||||
model.is_enabled
|
||||
? "bg-green-100 text-green-700"
|
||||
? "bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400"
|
||||
: "bg-muted text-muted-foreground"
|
||||
}`}
|
||||
>
|
||||
{model.is_enabled ? "Enabled" : "Disabled"}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell className="text-right text-sm">
|
||||
<TableCell>
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
<ToggleModelButton
|
||||
modelId={model.id}
|
||||
@@ -134,15 +133,17 @@ function ToggleModelButton({
|
||||
isEnabled: boolean;
|
||||
}) {
|
||||
return (
|
||||
<form action={toggleLlmModelAction}>
|
||||
<form action={toggleLlmModelAction} className="inline">
|
||||
<input type="hidden" name="model_id" value={modelId} />
|
||||
<input type="hidden" name="is_enabled" value={(!isEnabled).toString()} />
|
||||
<button
|
||||
<Button
|
||||
type="submit"
|
||||
className="inline-flex items-center rounded border border-input px-3 py-1 text-xs font-semibold hover:bg-muted"
|
||||
variant="outline"
|
||||
size="small"
|
||||
className="min-w-0"
|
||||
>
|
||||
{isEnabled ? "Disable" : "Enable"}
|
||||
</button>
|
||||
</Button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,63 +1,15 @@
|
||||
import { withRoleAccess } from "@/lib/withRoleAccess";
|
||||
import {
|
||||
fetchLlmModels,
|
||||
fetchLlmProviders,
|
||||
} from "./actions";
|
||||
import { AddProviderForm } from "./components/AddProviderForm";
|
||||
import { AddModelForm } from "./components/AddModelForm";
|
||||
import { ProviderList } from "./components/ProviderList";
|
||||
import { ModelsTable } from "./components/ModelsTable";
|
||||
import { useLlmRegistryPage } from "./useLlmRegistryPage";
|
||||
import { LlmRegistryDashboard } from "./components/LlmRegistryDashboard";
|
||||
|
||||
async function LlmRegistryDashboard() {
|
||||
const [providersResponse, modelsResponse] = await Promise.all([
|
||||
fetchLlmProviders(),
|
||||
fetchLlmModels(),
|
||||
]);
|
||||
|
||||
const providers = providersResponse.providers;
|
||||
const models = modelsResponse.models;
|
||||
|
||||
return (
|
||||
<div className="mx-auto flex w-full max-w-7xl flex-col gap-12 p-8">
|
||||
<div className="space-y-2">
|
||||
<h1 className="text-4xl font-bold tracking-tight">LLM Registry</h1>
|
||||
<p className="text-base text-muted-foreground">
|
||||
Manage supported providers, models, and credit pricing
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-8 lg:grid-cols-2">
|
||||
<AddProviderForm />
|
||||
<AddModelForm providers={providers} />
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-2">
|
||||
<h2 className="text-3xl font-semibold tracking-tight">Providers</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Default credentials and feature flags for upstream vendors.
|
||||
</p>
|
||||
</div>
|
||||
<ProviderList providers={providers} />
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-2">
|
||||
<h2 className="text-3xl font-semibold tracking-tight">Models</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Toggle availability, adjust context windows, and update credit pricing.
|
||||
</p>
|
||||
</div>
|
||||
<ModelsTable models={models} providers={providers} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
async function LlmRegistryPage() {
|
||||
const data = await useLlmRegistryPage();
|
||||
return <LlmRegistryDashboard {...data} />;
|
||||
}
|
||||
|
||||
export default async function AdminLlmRegistryPage() {
|
||||
"use server";
|
||||
const withAdminAccess = await withRoleAccess(["admin"]);
|
||||
const ProtectedDashboard = await withAdminAccess(LlmRegistryDashboard);
|
||||
return <ProtectedDashboard />;
|
||||
const ProtectedLlmRegistryPage = await withAdminAccess(LlmRegistryPage);
|
||||
return <ProtectedLlmRegistryPage />;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* Hook for LLM Registry page data fetching and state management.
|
||||
*/
|
||||
|
||||
import {
|
||||
fetchLlmModels,
|
||||
fetchLlmProviders,
|
||||
} from "./actions";
|
||||
|
||||
export async function useLlmRegistryPage() {
|
||||
const [providersResponse, modelsResponse] = await Promise.all([
|
||||
fetchLlmProviders(),
|
||||
fetchLlmModels(),
|
||||
]);
|
||||
|
||||
return {
|
||||
providers: providersResponse.providers,
|
||||
models: modelsResponse.models,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user