feat: migrate Inspect API to FastAPI router (#4403)

# What does this PR do?

Migrate the Inspect API to the FastAPI router pattern.

Changes:
- Add inspect API to FastAPI router registry
- Add PUBLIC_ROUTE_KEY support for routes that don't require auth
- Update WebMethod creation to respect route's openapi_extra for
authentication requirements

Fixes: https://github.com/llamastack/llama-stack/issues/4346

<!-- Provide a short summary of what this PR does and why. Link to
relevant issues if applicable. -->

<!-- If resolving an issue, uncomment and update the line below -->
<!-- Closes #[issue-number] -->

## Test Plan

CI and various curls on /v1/inspect/routes, /v1/health, /v1/version

Signed-off-by: Sébastien Han <seb@redhat.com>
This commit is contained in:
Sébastien Han 2025-12-17 17:33:42 +01:00 committed by GitHub
parent cd5095a247
commit a7d509aaf9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 281 additions and 182 deletions

View file

@ -944,12 +944,10 @@ paths:
$ref: '#/components/responses/DefaultError'
tags:
- Inspect
summary: Health
description: |-
Get health status.
Get the current health status of the service.
summary: Get health status.
description: Get the current health status of the service.
operationId: health_v1_health_get
x-public: true
/v1/inspect/routes:
get:
responses:
@ -973,11 +971,8 @@ paths:
description: Default Response
tags:
- Inspect
summary: List Routes
description: |-
List routes.
List all available API routes with their methods and implementing providers.
summary: List routes.
description: List all available API routes with their methods and implementing providers.
operationId: list_routes_v1_inspect_routes_get
parameters:
- name: api_filter
@ -992,7 +987,9 @@ paths:
- deprecated
type: string
- type: 'null'
description: Optional filter to control which routes are returned. Can be an API level ('v1', 'v1alpha', 'v1beta') to show non-deprecated routes at that level, or 'deprecated' to show deprecated routes across all levels. If not specified, returns all non-deprecated routes.
title: Api Filter
description: Optional filter to control which routes are returned. Can be an API level ('v1', 'v1alpha', 'v1beta') to show non-deprecated routes at that level, or 'deprecated' to show deprecated routes across all levels. If not specified, returns all non-deprecated routes.
/v1/models:
get:
responses:
@ -3161,12 +3158,10 @@ paths:
$ref: '#/components/responses/DefaultError'
tags:
- Inspect
summary: Version
description: |-
Get version.
Get the version of the service.
summary: Get version.
description: Get the version of the service.
operationId: version_v1_version_get
x-public: true
/v1beta/datasetio/append-rows/{dataset_id}:
post:
responses:
@ -6366,6 +6361,7 @@ components:
properties:
status:
$ref: '#/components/schemas/HealthStatus'
description: The health status of the service
type: object
required:
- status
@ -6376,14 +6372,17 @@ components:
route:
type: string
title: Route
description: The API route path
method:
type: string
title: Method
description: The HTTP method for the route
provider_types:
items:
type: string
type: array
title: Provider Types
description: List of provider types implementing this route
type: object
required:
- route
@ -6398,6 +6397,7 @@ components:
$ref: '#/components/schemas/RouteInfo'
type: array
title: Data
description: List of available API routes
type: object
required:
- data
@ -10519,6 +10519,7 @@ components:
version:
type: string
title: Version
description: The version string of the service
type: object
required:
- version

View file

@ -3196,6 +3196,7 @@ components:
properties:
status:
$ref: '#/components/schemas/HealthStatus'
description: The health status of the service
type: object
required:
- status
@ -3206,14 +3207,17 @@ components:
route:
type: string
title: Route
description: The API route path
method:
type: string
title: Method
description: The HTTP method for the route
provider_types:
items:
type: string
type: array
title: Provider Types
description: List of provider types implementing this route
type: object
required:
- route
@ -3228,6 +3232,7 @@ components:
$ref: '#/components/schemas/RouteInfo'
type: array
title: Data
description: List of available API routes
type: object
required:
- data
@ -7349,6 +7354,7 @@ components:
version:
type: string
title: Version
description: The version string of the service
type: object
required:
- version

View file

@ -2980,6 +2980,7 @@ components:
properties:
status:
$ref: '#/components/schemas/HealthStatus'
description: The health status of the service
type: object
required:
- status
@ -2990,14 +2991,17 @@ components:
route:
type: string
title: Route
description: The API route path
method:
type: string
title: Method
description: The HTTP method for the route
provider_types:
items:
type: string
type: array
title: Provider Types
description: List of provider types implementing this route
type: object
required:
- route
@ -3012,6 +3016,7 @@ components:
$ref: '#/components/schemas/RouteInfo'
type: array
title: Data
description: List of available API routes
type: object
required:
- data
@ -6610,6 +6615,7 @@ components:
version:
type: string
title: Version
description: The version string of the service
type: object
required:
- version

View file

@ -942,12 +942,10 @@ paths:
$ref: '#/components/responses/DefaultError'
tags:
- Inspect
summary: Health
description: |-
Get health status.
Get the current health status of the service.
summary: Get health status.
description: Get the current health status of the service.
operationId: health_v1_health_get
x-public: true
/v1/inspect/routes:
get:
responses:
@ -971,11 +969,8 @@ paths:
description: Default Response
tags:
- Inspect
summary: List Routes
description: |-
List routes.
List all available API routes with their methods and implementing providers.
summary: List routes.
description: List all available API routes with their methods and implementing providers.
operationId: list_routes_v1_inspect_routes_get
parameters:
- name: api_filter
@ -990,7 +985,9 @@ paths:
- deprecated
type: string
- type: 'null'
description: Optional filter to control which routes are returned. Can be an API level ('v1', 'v1alpha', 'v1beta') to show non-deprecated routes at that level, or 'deprecated' to show deprecated routes across all levels. If not specified, returns all non-deprecated routes.
title: Api Filter
description: Optional filter to control which routes are returned. Can be an API level ('v1', 'v1alpha', 'v1beta') to show non-deprecated routes at that level, or 'deprecated' to show deprecated routes across all levels. If not specified, returns all non-deprecated routes.
/v1/models:
get:
responses:
@ -2701,12 +2698,10 @@ paths:
$ref: '#/components/responses/DefaultError'
tags:
- Inspect
summary: Version
description: |-
Get version.
Get the version of the service.
summary: Get version.
description: Get the version of the service.
operationId: version_v1_version_get
x-public: true
components:
schemas:
Error:
@ -5008,6 +5003,7 @@ components:
properties:
status:
$ref: '#/components/schemas/HealthStatus'
description: The health status of the service
type: object
required:
- status
@ -5018,14 +5014,17 @@ components:
route:
type: string
title: Route
description: The API route path
method:
type: string
title: Method
description: The HTTP method for the route
provider_types:
items:
type: string
type: array
title: Provider Types
description: List of provider types implementing this route
type: object
required:
- route
@ -5040,6 +5039,7 @@ components:
$ref: '#/components/schemas/RouteInfo'
type: array
title: Data
description: List of available API routes
type: object
required:
- data
@ -9143,6 +9143,7 @@ components:
version:
type: string
title: Version
description: The version string of the service
type: object
required:
- version

View file

@ -944,12 +944,10 @@ paths:
$ref: '#/components/responses/DefaultError'
tags:
- Inspect
summary: Health
description: |-
Get health status.
Get the current health status of the service.
summary: Get health status.
description: Get the current health status of the service.
operationId: health_v1_health_get
x-public: true
/v1/inspect/routes:
get:
responses:
@ -973,11 +971,8 @@ paths:
description: Default Response
tags:
- Inspect
summary: List Routes
description: |-
List routes.
List all available API routes with their methods and implementing providers.
summary: List routes.
description: List all available API routes with their methods and implementing providers.
operationId: list_routes_v1_inspect_routes_get
parameters:
- name: api_filter
@ -992,7 +987,9 @@ paths:
- deprecated
type: string
- type: 'null'
description: Optional filter to control which routes are returned. Can be an API level ('v1', 'v1alpha', 'v1beta') to show non-deprecated routes at that level, or 'deprecated' to show deprecated routes across all levels. If not specified, returns all non-deprecated routes.
title: Api Filter
description: Optional filter to control which routes are returned. Can be an API level ('v1', 'v1alpha', 'v1beta') to show non-deprecated routes at that level, or 'deprecated' to show deprecated routes across all levels. If not specified, returns all non-deprecated routes.
/v1/models:
get:
responses:
@ -3161,12 +3158,10 @@ paths:
$ref: '#/components/responses/DefaultError'
tags:
- Inspect
summary: Version
description: |-
Get version.
Get the version of the service.
summary: Get version.
description: Get the version of the service.
operationId: version_v1_version_get
x-public: true
/v1beta/datasetio/append-rows/{dataset_id}:
post:
responses:
@ -6366,6 +6361,7 @@ components:
properties:
status:
$ref: '#/components/schemas/HealthStatus'
description: The health status of the service
type: object
required:
- status
@ -6376,14 +6372,17 @@ components:
route:
type: string
title: Route
description: The API route path
method:
type: string
title: Method
description: The HTTP method for the route
provider_types:
items:
type: string
type: array
title: Provider Types
description: List of provider types implementing this route
type: object
required:
- route
@ -6398,6 +6397,7 @@ components:
$ref: '#/components/schemas/RouteInfo'
type: array
title: Data
description: List of available API routes
type: object
required:
- data
@ -10519,6 +10519,7 @@ components:
version:
type: string
title: Version
description: The version string of the service
type: object
required:
- version

View file

@ -28,9 +28,11 @@ class AuthenticationMiddleware:
4. Makes these attributes available to the route handlers for access control
Unauthenticated Access:
Endpoints can opt out of authentication by setting require_authentication=False
in their @webmethod decorator. This is typically used for operational endpoints
like /health and /version to support monitoring, load balancers, and observability tools.
Endpoints can opt out of authentication by:
- For legacy @webmethod routes: setting require_authentication=False in the decorator
- For FastAPI router routes: setting openapi_extra={PUBLIC_ROUTE_KEY: True}
This is typically used for operational endpoints like /health and /version to support
monitoring, load balancers, and observability tools.
The middleware supports multiple authentication providers through the AuthProvider interface:
- Kubernetes: Validates tokens against the Kubernetes API server

View file

@ -15,9 +15,8 @@ from typing import Any, cast
from fastapi import APIRouter
from fastapi.routing import APIRoute
from starlette.routing import Route
from llama_stack_api import batches, benchmarks, datasets, providers
from llama_stack_api import batches, benchmarks, datasets, inspect_api, providers
# Router factories for APIs that have FastAPI routers
# Add new APIs here as they are migrated to the router system
@ -28,6 +27,7 @@ _ROUTER_FACTORIES: dict[str, Callable[[Any], APIRouter]] = {
"benchmarks": benchmarks.fastapi_routes.create_router,
"datasets": datasets.fastapi_routes.create_router,
"providers": providers.fastapi_routes.create_router,
"inspect": inspect_api.fastapi_routes.create_router,
}
@ -63,38 +63,20 @@ def build_fastapi_router(api: "Api", impl: Any) -> APIRouter | None:
return cast(APIRouter, router_factory(impl))
def get_router_routes(router: APIRouter) -> list[Route]:
"""Extract routes from a FastAPI router.
def get_router_routes(router: APIRouter) -> list[APIRoute]:
"""Extract APIRoute objects from a FastAPI router.
Args:
router: The FastAPI router to extract routes from
Returns:
List of Route objects from the router
List of APIRoute objects from the router (preserves tags and other metadata)
"""
routes = []
for route in router.routes:
# FastAPI routers use APIRoute objects, which have path and methods attributes
# FastAPI routers use APIRoute objects, which have path, methods, tags, etc.
if isinstance(route, APIRoute):
# Combine router prefix with route path
routes.append(
Route(
path=route.path,
methods=route.methods,
name=route.name,
endpoint=route.endpoint,
)
)
elif isinstance(route, Route):
# Fallback for regular Starlette Route objects
routes.append(
Route(
path=route.path,
methods=route.methods,
name=route.name,
endpoint=route.endpoint,
)
)
routes.append(route)
return routes

View file

@ -19,6 +19,7 @@ from llama_stack.core.server.fastapi_router_registry import (
get_router_routes,
)
from llama_stack_api import Api, ExternalApiSpec, WebMethod
from llama_stack_api.router_utils import PUBLIC_ROUTE_KEY
EndpointFunc = Callable[..., Any]
PathParams = dict[str, str]
@ -124,7 +125,13 @@ def initialize_route_impls(impls, external_apis: dict[Api, ExternalApiSpec] | No
# - Pass summary directly in RouteMatch instead of WebMethod
# - Remove this WebMethod() instantiation entirely
# - Update library_client.py to use the extracted summary instead of webmethod.descriptive_name
webmethod = WebMethod(descriptive_name=None)
# Routes with openapi_extra[PUBLIC_ROUTE_KEY]=True don't require authentication
is_public = (route.openapi_extra or {}).get(PUBLIC_ROUTE_KEY, False)
webmethod = WebMethod(
descriptive_name=None,
require_authentication=not is_public,
)
route_impls[method][_convert_path_to_regex(route.path)] = (
func,
route.path,
@ -139,19 +146,19 @@ def initialize_route_impls(impls, external_apis: dict[Api, ExternalApiSpec] | No
if api not in impls:
continue
for route, webmethod in api_routes:
for legacy_route, webmethod in api_routes:
impl = impls[api]
func = getattr(impl, route.name)
func = getattr(impl, legacy_route.name)
# Get the first (and typically only) method from the set, filtering out HEAD
available_methods = [m for m in (route.methods or []) if m != "HEAD"]
available_methods = [m for m in (legacy_route.methods or []) if m != "HEAD"]
if not available_methods:
continue # Skip if only HEAD method is available
method = available_methods[0].lower()
if method not in route_impls:
route_impls[method] = {}
route_impls[method][_convert_path_to_regex(route.path)] = (
route_impls[method][_convert_path_to_regex(legacy_route.path)] = (
func,
route.path,
legacy_route.path,
webmethod,
)

View file

@ -1,103 +0,0 @@
# Copyright (c) Meta Platforms, Inc. and affiliates.
# All rights reserved.
#
# This source code is licensed under the terms described in the LICENSE file in
# the root directory of this source tree.
from typing import Literal, Protocol, runtime_checkable
from pydantic import BaseModel
from llama_stack_api.datatypes import HealthStatus
from llama_stack_api.schema_utils import json_schema_type, webmethod
from llama_stack_api.version import (
LLAMA_STACK_API_V1,
)
# Valid values for the route filter parameter.
# Actual API levels: v1, v1alpha, v1beta (filters by level, excludes deprecated)
# Special filter value: "deprecated" (shows deprecated routes regardless of level)
ApiFilter = Literal["v1", "v1alpha", "v1beta", "deprecated"]
@json_schema_type
class RouteInfo(BaseModel):
"""Information about an API route including its path, method, and implementing providers.
:param route: The API endpoint path
:param method: HTTP method for the route
:param provider_types: List of provider types that implement this route
"""
route: str
method: str
provider_types: list[str]
@json_schema_type
class HealthInfo(BaseModel):
"""Health status information for the service.
:param status: Current health status of the service
"""
status: HealthStatus
@json_schema_type
class VersionInfo(BaseModel):
"""Version information for the service.
:param version: Version number of the service
"""
version: str
@json_schema_type
class ListRoutesResponse(BaseModel):
"""Response containing a list of all available API routes.
:param data: List of available route information objects
"""
data: list[RouteInfo]
@runtime_checkable
class Inspect(Protocol):
"""Inspect
APIs for inspecting the Llama Stack service, including health status, available API routes with methods and implementing providers.
"""
@webmethod(route="/inspect/routes", method="GET", level=LLAMA_STACK_API_V1)
async def list_routes(self, api_filter: ApiFilter | None = None) -> ListRoutesResponse:
"""List routes.
List all available API routes with their methods and implementing providers.
:param api_filter: Optional filter to control which routes are returned. Can be an API level ('v1', 'v1alpha', 'v1beta') to show non-deprecated routes at that level, or 'deprecated' to show deprecated routes across all levels. If not specified, returns all non-deprecated routes.
:returns: Response containing information about all available routes.
"""
...
@webmethod(route="/health", method="GET", level=LLAMA_STACK_API_V1, require_authentication=False)
async def health(self) -> HealthInfo:
"""Get health status.
Get the current health status of the service.
:returns: Health information indicating if the service is operational.
"""
...
@webmethod(route="/version", method="GET", level=LLAMA_STACK_API_V1, require_authentication=False)
async def version(self) -> VersionInfo:
"""Get version.
Get the version of the service.
:returns: Version information containing the service version number.
"""
...

View file

@ -0,0 +1,37 @@
# Copyright (c) Meta Platforms, Inc. and affiliates.
# All rights reserved.
#
# This source code is licensed under the terms described in the LICENSE file in
# the root directory of this source tree.
"""Inspect API protocol and models.
This module contains the Inspect protocol definition.
Pydantic models are defined in llama_stack_api.inspect.models.
The FastAPI router is defined in llama_stack_api.inspect.fastapi_routes.
"""
# Import fastapi_routes for router factory access
from . import fastapi_routes
# Import protocol for re-export
from .api import Inspect
# Import models for re-export
from .models import (
ApiFilter,
HealthInfo,
ListRoutesResponse,
RouteInfo,
VersionInfo,
)
__all__ = [
"Inspect",
"ApiFilter",
"HealthInfo",
"ListRoutesResponse",
"RouteInfo",
"VersionInfo",
"fastapi_routes",
]

View file

@ -0,0 +1,25 @@
# Copyright (c) Meta Platforms, Inc. and affiliates.
# All rights reserved.
#
# This source code is licensed under the terms described in the LICENSE file in
# the root directory of this source tree.
from typing import Protocol, runtime_checkable
from .models import (
ApiFilter,
HealthInfo,
ListRoutesResponse,
VersionInfo,
)
@runtime_checkable
class Inspect(Protocol):
"""APIs for inspecting the Llama Stack service, including health status, available API routes with methods and implementing providers."""
async def list_routes(self, api_filter: ApiFilter | None = None) -> ListRoutesResponse: ...
async def health(self) -> HealthInfo: ...
async def version(self) -> VersionInfo: ...

View file

@ -0,0 +1,76 @@
# Copyright (c) Meta Platforms, Inc. and affiliates.
# All rights reserved.
#
# This source code is licensed under the terms described in the LICENSE file in
# the root directory of this source tree.
"""FastAPI router for the Inspect API.
This module defines the FastAPI router for the Inspect API using standard
FastAPI route decorators.
"""
from typing import Annotated
from fastapi import APIRouter, Query
from llama_stack_api.router_utils import PUBLIC_ROUTE_KEY, standard_responses
from llama_stack_api.version import LLAMA_STACK_API_V1
from .api import Inspect
from .models import (
ApiFilter,
HealthInfo,
ListRoutesResponse,
VersionInfo,
)
def create_router(impl: Inspect) -> APIRouter:
"""Create a FastAPI router for the Inspect API."""
router = APIRouter(
prefix=f"/{LLAMA_STACK_API_V1}",
tags=["Inspect"],
responses=standard_responses,
)
@router.get(
"/inspect/routes",
response_model=ListRoutesResponse,
summary="List routes.",
description="List all available API routes with their methods and implementing providers.",
responses={200: {"description": "Response containing information about all available routes."}},
)
async def list_routes(
api_filter: Annotated[
ApiFilter | None,
Query(
description="Optional filter to control which routes are returned. Can be an API level ('v1', 'v1alpha', 'v1beta') to show non-deprecated routes at that level, or 'deprecated' to show deprecated routes across all levels. If not specified, returns all non-deprecated routes."
),
] = None,
) -> ListRoutesResponse:
return await impl.list_routes(api_filter)
@router.get(
"/health",
response_model=HealthInfo,
summary="Get health status.",
description="Get the current health status of the service.",
responses={200: {"description": "Health information indicating if the service is operational."}},
openapi_extra={PUBLIC_ROUTE_KEY: True},
)
async def health() -> HealthInfo:
return await impl.health()
@router.get(
"/version",
response_model=VersionInfo,
summary="Get version.",
description="Get the version of the service.",
responses={200: {"description": "Version information containing the service version number."}},
openapi_extra={PUBLIC_ROUTE_KEY: True},
)
async def version() -> VersionInfo:
return await impl.version()
return router

View file

@ -0,0 +1,53 @@
# Copyright (c) Meta Platforms, Inc. and affiliates.
# All rights reserved.
#
# This source code is licensed under the terms described in the LICENSE file in
# the root directory of this source tree.
"""Pydantic models for Inspect API requests and responses.
This module defines the request and response models for the Inspect API
using Pydantic with Field descriptions for OpenAPI schema generation.
"""
from typing import Literal
from pydantic import BaseModel, Field
from llama_stack_api.datatypes import HealthStatus
from llama_stack_api.schema_utils import json_schema_type
# Valid values for the route filter parameter.
# Actual API levels: v1, v1alpha, v1beta (filters by level, excludes deprecated)
# Special filter value: "deprecated" (shows deprecated routes regardless of level)
ApiFilter = Literal["v1", "v1alpha", "v1beta", "deprecated"]
@json_schema_type
class RouteInfo(BaseModel):
"""Information about an API route including its path, method, and implementing providers."""
route: str = Field(description="The API route path")
method: str = Field(description="The HTTP method for the route")
provider_types: list[str] = Field(description="List of provider types implementing this route")
@json_schema_type
class HealthInfo(BaseModel):
"""Health status information for the service."""
status: HealthStatus = Field(description="The health status of the service")
@json_schema_type
class VersionInfo(BaseModel):
"""Version information for the service."""
version: str = Field(description="The version string of the service")
@json_schema_type
class ListRoutesResponse(BaseModel):
"""Response containing a list of all available API routes."""
data: list[RouteInfo] = Field(description="List of available API routes")

View file

@ -18,6 +18,11 @@ from typing import Annotated, Any, TypeVar
from fastapi import Path, Query
from pydantic import BaseModel
# OpenAPI extension key to mark routes that don't require authentication.
# Use this in FastAPI route decorators: @router.get("/health", openapi_extra={PUBLIC_ROUTE_KEY: True})
PUBLIC_ROUTE_KEY = "x-public"
standard_responses: dict[int | str, dict[str, Any]] = {
400: {"$ref": "#/components/responses/BadRequest400"},
429: {"$ref": "#/components/responses/TooManyRequests429"},