From 5fe8ddfd8c0a26e1bd3d2ea758e7143fd1e918fa Mon Sep 17 00:00:00 2001 From: Bentlybro Date: Wed, 11 Mar 2026 10:32:48 +0000 Subject: [PATCH] feat(platform): Add LLM registry public read API Implements public GET endpoints for querying LLM models and providers - Part 3 of 6 in the incremental registry rollout. **Endpoints:** - GET /api/llm/models - List all models (filterable by enabled_only) - GET /api/llm/providers - List providers with their models **Design:** - Uses in-memory registry from PR 2 (no DB queries) - Fast reads from cache populated at startup - Grouped by provider for easy UI rendering **Response models:** - LlmModel - model info with capabilities, costs, creator - LlmProvider - provider with nested models - LlmModelsResponse - list + total count - LlmProvidersResponse - grouped by provider **Authentication:** - Requires user auth (requires_user dependency) - Public within authenticated sessions **Integration:** - Registered in rest_api.py at /api prefix - Tagged with v2 + llm for OpenAPI grouping **What's NOT included (later PRs):** - Admin write API (PR 4) - Block integration (PR 5) - Redis cache (PR 6) Lines: ~180 total Files: 4 (3 new, 1 modified) Review time: < 10 minutes --- .../backend/backend/api/rest_api.py | 6 + .../backend/backend/server/v2/llm/__init__.py | 5 + .../backend/backend/server/v2/llm/model.py | 67 +++++++++ .../backend/backend/server/v2/llm/routes.py | 141 ++++++++++++++++++ 4 files changed, 219 insertions(+) create mode 100644 autogpt_platform/backend/backend/server/v2/llm/__init__.py create mode 100644 autogpt_platform/backend/backend/server/v2/llm/model.py create mode 100644 autogpt_platform/backend/backend/server/v2/llm/routes.py diff --git a/autogpt_platform/backend/backend/api/rest_api.py b/autogpt_platform/backend/backend/api/rest_api.py index 948857fc6d..45f0bbb8e5 100644 --- a/autogpt_platform/backend/backend/api/rest_api.py +++ b/autogpt_platform/backend/backend/api/rest_api.py @@ -41,6 +41,7 @@ import backend.data.graph import backend.data.llm_registry import backend.data.user import backend.integrations.webhooks.utils +import backend.server.v2.llm import backend.util.service import backend.util.settings from backend.api.features.library.exceptions import ( @@ -397,6 +398,11 @@ app.include_router( tags=["oauth"], prefix="/api/oauth", ) +app.include_router( + backend.server.v2.llm.router, + tags=["v2", "llm"], + prefix="/api", +) app.mount("/external-api", external_api) diff --git a/autogpt_platform/backend/backend/server/v2/llm/__init__.py b/autogpt_platform/backend/backend/server/v2/llm/__init__.py new file mode 100644 index 0000000000..c69da22392 --- /dev/null +++ b/autogpt_platform/backend/backend/server/v2/llm/__init__.py @@ -0,0 +1,5 @@ +"""LLM registry public API.""" + +from .routes import router + +__all__ = ["router"] diff --git a/autogpt_platform/backend/backend/server/v2/llm/model.py b/autogpt_platform/backend/backend/server/v2/llm/model.py new file mode 100644 index 0000000000..5dd6731020 --- /dev/null +++ b/autogpt_platform/backend/backend/server/v2/llm/model.py @@ -0,0 +1,67 @@ +"""Pydantic models for LLM registry public API.""" + +from __future__ import annotations + +from typing import Any + +import pydantic + + +class LlmModelCost(pydantic.BaseModel): + """Cost configuration for an LLM model.""" + + unit: str # "RUN" or "TOKENS" + credit_cost: int = pydantic.Field(ge=0) + credential_provider: str + credential_id: str | None = None + credential_type: str | None = None + currency: str | None = None + metadata: dict[str, Any] = pydantic.Field(default_factory=dict) + + +class LlmModelCreator(pydantic.BaseModel): + """Represents the organization that created/trained the model.""" + + id: str + name: str + display_name: str + description: str | None = None + website_url: str | None = None + logo_url: str | None = None + + +class LlmModel(pydantic.BaseModel): + """Public-facing LLM model information.""" + + slug: str + display_name: str + description: str | None = None + provider_name: str + creator: LlmModelCreator | None = None + context_window: int + max_output_tokens: int | None = None + price_tier: int # 1=cheapest, 2=medium, 3=expensive + is_recommended: bool = False + capabilities: dict[str, Any] = pydantic.Field(default_factory=dict) + costs: list[LlmModelCost] = pydantic.Field(default_factory=list) + + +class LlmProvider(pydantic.BaseModel): + """Provider with its enabled models.""" + + name: str + display_name: str + models: list[LlmModel] = pydantic.Field(default_factory=list) + + +class LlmModelsResponse(pydantic.BaseModel): + """Response for GET /llm/models.""" + + models: list[LlmModel] + total: int + + +class LlmProvidersResponse(pydantic.BaseModel): + """Response for GET /llm/providers.""" + + providers: list[LlmProvider] diff --git a/autogpt_platform/backend/backend/server/v2/llm/routes.py b/autogpt_platform/backend/backend/server/v2/llm/routes.py new file mode 100644 index 0000000000..5b39d7f34d --- /dev/null +++ b/autogpt_platform/backend/backend/server/v2/llm/routes.py @@ -0,0 +1,141 @@ +"""Public read-only API for LLM registry.""" + +import autogpt_libs.auth +import fastapi + +from backend.data.llm_registry import ( + RegistryModelCreator, + get_all_models, + get_enabled_models, +) +from backend.server.v2.llm import model as llm_model + +router = fastapi.APIRouter( + prefix="/llm", + tags=["llm"], + dependencies=[fastapi.Security(autogpt_libs.auth.requires_user)], +) + + +def _map_creator( + creator: RegistryModelCreator | None, +) -> llm_model.LlmModelCreator | None: + """Convert registry creator to API model.""" + if not creator: + return None + return llm_model.LlmModelCreator( + id=creator.id, + name=creator.name, + display_name=creator.display_name, + description=creator.description, + website_url=creator.website_url, + logo_url=creator.logo_url, + ) + + +@router.get("/models", response_model=llm_model.LlmModelsResponse) +async def list_models( + enabled_only: bool = fastapi.Query( + default=True, description="Only return enabled models" + ), +): + """ + List all LLM models available to users. + + Returns models from the in-memory registry cache. + Use enabled_only=true to filter to only enabled models (default). + """ + # Get models from in-memory registry + registry_models = get_enabled_models() if enabled_only else get_all_models() + + # Map to API response models + models = [ + llm_model.LlmModel( + slug=model.slug, + display_name=model.display_name, + description=model.description, + provider_name=model.provider_display_name, + creator=_map_creator(model.creator), + context_window=model.metadata.context_window, + max_output_tokens=model.metadata.max_output_tokens, + price_tier=model.metadata.price_tier, + is_recommended=model.is_recommended, + capabilities=model.capabilities, + costs=[ + llm_model.LlmModelCost( + unit=cost.unit, + credit_cost=cost.credit_cost, + credential_provider=cost.credential_provider, + credential_id=cost.credential_id, + credential_type=cost.credential_type, + currency=cost.currency, + metadata=cost.metadata, + ) + for cost in model.costs + ], + ) + for model in registry_models + ] + + return llm_model.LlmModelsResponse(models=models, total=len(models)) + + +@router.get("/providers", response_model=llm_model.LlmProvidersResponse) +async def list_providers(): + """ + List all LLM providers with their enabled models. + + Groups enabled models by provider from the in-memory registry. + """ + # Get all enabled models and group by provider + registry_models = get_enabled_models() + + # Group models by provider + provider_map: dict[str, list] = {} + for model in registry_models: + provider_key = model.metadata.provider + if provider_key not in provider_map: + provider_map[provider_key] = [] + provider_map[provider_key].append(model) + + # Build provider responses + providers = [] + for provider_key, models in sorted(provider_map.items()): + # Use the first model's provider display name + display_name = models[0].provider_display_name if models else provider_key + + providers.append( + llm_model.LlmProvider( + name=provider_key, + display_name=display_name, + models=[ + llm_model.LlmModel( + slug=model.slug, + display_name=model.display_name, + description=model.description, + provider_name=model.provider_display_name, + creator=_map_creator(model.creator), + context_window=model.metadata.context_window, + max_output_tokens=model.metadata.max_output_tokens, + price_tier=model.metadata.price_tier, + is_recommended=model.is_recommended, + capabilities=model.capabilities, + costs=[ + llm_model.LlmModelCost( + unit=cost.unit, + credit_cost=cost.credit_cost, + credential_provider=cost.credential_provider, + credential_id=cost.credential_id, + credential_type=cost.credential_type, + currency=cost.currency, + metadata=cost.metadata, + ) + for cost in model.costs + ], + ) + for model in sorted(models, key=lambda m: m.display_name) + ], + ) + ) + + return llm_model.LlmProvidersResponse(providers=providers)