fix: adopt FastAPI directly in llama-stack-api

This commit migrates the Batches API to use FastAPI routers directly in the
API package, removing the need for custom decorator systems and manual router
registration. The API package now defines FastAPI routers using standard
FastAPI route decorators, making it self-sufficient and eliminating dependencies
on the server package.

The router implementation has been moved from llama_stack/core/server/routers/batches.py
to llama_stack_api/batches/routes.py, where it belongs alongside the protocol
and models.

Standard error responses (standard_responses) have been moved from the server
package to llama_stack_api/router_utils.py, ensuring the API package can
define complete routers without server dependencies. FastAPI has been added
as an explicit dependency to the llama-stack-api package, making it an
intentional dependency rather than an implicit one.

Router discovery is now fully automatic. The server discovers routers by
checking for routes modules in each API package and looking for a create_router
function. This eliminates the need for manual registration and makes the system
scalable - new APIs with router modules are automatically discovered and used.

The router registry has been simplified to use automatic discovery instead of
maintaining a manual registry. The build_router function (renamed from
create_router to better reflect its purpose) discovers and combines router
factories with implementations to create the final router instances.

Exposing Routers from the API is nice for the Bring Your Own API use
case too.

Signed-off-by: Sébastien Han <seb@redhat.com>
This commit is contained in:
Sébastien Han 2025-11-20 15:00:11 +01:00
parent 2fe24a6df8
commit 00e7ea6c3b
No known key found for this signature in database
10 changed files with 54 additions and 70 deletions

View file

@ -76,14 +76,8 @@ def create_llama_stack_app() -> FastAPI:
], ],
) )
# Import batches router to trigger router registration # Include routers for APIs that have them (automatic discovery)
try: from llama_stack.core.server.router_registry import build_router, has_router
from llama_stack.core.server.routers import batches # noqa: F401
except ImportError:
pass
# Include routers for APIs that have them registered
from llama_stack.core.server.router_registry import create_router, has_router
def dummy_impl_getter(api: Api) -> Any: def dummy_impl_getter(api: Api) -> Any:
"""Dummy implementation getter for OpenAPI generation.""" """Dummy implementation getter for OpenAPI generation."""
@ -95,7 +89,7 @@ def create_llama_stack_app() -> FastAPI:
protocols = api_protocol_map() protocols = api_protocol_map()
for api in protocols.keys(): for api in protocols.keys():
if has_router(api): if has_router(api):
router = create_router(api, dummy_impl_getter) router = build_router(api, dummy_impl_getter)
if router: if router:
app.include_router(router) app.include_router(router)

View file

@ -10,7 +10,7 @@ from pydantic import BaseModel
from llama_stack.core.datatypes import StackRunConfig from llama_stack.core.datatypes import StackRunConfig
from llama_stack.core.external import load_external_apis from llama_stack.core.external import load_external_apis
from llama_stack.core.server.router_registry import create_router, has_router from llama_stack.core.server.router_registry import build_router, has_router
from llama_stack.core.server.routes import get_all_api_routes from llama_stack.core.server.routes import get_all_api_routes
from llama_stack_api import ( from llama_stack_api import (
Api, Api,
@ -120,7 +120,7 @@ class DistributionInspectImpl(Inspect):
if not has_router(api): if not has_router(api):
continue continue
router = create_router(api, dummy_impl_getter) router = build_router(api, dummy_impl_getter)
if not router: if not router:
continue continue

View file

@ -4,12 +4,13 @@
# This source code is licensed under the terms described in the LICENSE file in # This source code is licensed under the terms described in the LICENSE file in
# the root directory of this source tree. # the root directory of this source tree.
"""Router registry for FastAPI routers. """Router utilities for FastAPI routers.
This module provides a way to register FastAPI routers for APIs that have been This module provides utilities to discover and create FastAPI routers from API packages.
migrated to use explicit FastAPI routers instead of Protocol-based route discovery. Routers are automatically discovered by checking for routes modules in each API package.
""" """
import importlib
from collections.abc import Callable from collections.abc import Callable
from typing import TYPE_CHECKING, Any from typing import TYPE_CHECKING, Any
@ -18,47 +19,42 @@ from fastapi import APIRouter
if TYPE_CHECKING: if TYPE_CHECKING:
from llama_stack_api.datatypes import Api from llama_stack_api.datatypes import Api
# Registry of router factory functions
# Each factory function takes a callable that returns the implementation for a given API
# and returns an APIRouter
# Use string keys to avoid circular imports
_router_factories: dict[str, Callable[[Callable[["Api"], Any]], APIRouter]] = {}
def register_router(api: "Api", router_factory: Callable[[Callable[["Api"], Any]], APIRouter]) -> None:
"""Register a router factory for an API.
Args:
api: The API enum value
router_factory: A function that takes an impl_getter function and returns an APIRouter
"""
_router_factories[api.value] = router_factory # type: ignore[attr-defined]
def has_router(api: "Api") -> bool: def has_router(api: "Api") -> bool:
"""Check if an API has a registered router. """Check if an API has a router factory in its routes module.
Args: Args:
api: The API enum value api: The API enum value
Returns: Returns:
True if a router factory is registered for this API True if the API has a routes module with a create_router function
""" """
return api.value in _router_factories # type: ignore[attr-defined] try:
routes_module = importlib.import_module(f"llama_stack_api.{api.value}.routes")
return hasattr(routes_module, "create_router")
except (ImportError, AttributeError):
return False
def create_router(api: "Api", impl_getter: Callable[["Api"], Any]) -> APIRouter | None: def build_router(api: "Api", impl_getter: Callable[["Api"], Any]) -> APIRouter | None:
"""Create a router for an API if one is registered. """Build a router for an API by combining its router factory with the implementation.
This function discovers the router factory from the API package's routes module
and calls it with the impl_getter to create the final router instance.
Args: Args:
api: The API enum value api: The API enum value
impl_getter: Function that returns the implementation for a given API impl_getter: Function that returns the implementation for a given API
Returns: Returns:
APIRouter if registered, None otherwise APIRouter if the API has a routes module with create_router, None otherwise
""" """
api_value = api.value # type: ignore[attr-defined] try:
if api_value not in _router_factories: routes_module = importlib.import_module(f"llama_stack_api.{api.value}.routes")
return None if hasattr(routes_module, "create_router"):
router_factory = routes_module.create_router
return router_factory(impl_getter)
except (ImportError, AttributeError):
pass
return _router_factories[api_value](impl_getter) return None

View file

@ -30,7 +30,7 @@ def get_all_api_routes(
This function only returns routes from APIs that use the legacy @webmethod This function only returns routes from APIs that use the legacy @webmethod
decorator system. For APIs that have been migrated to FastAPI routers, decorator system. For APIs that have been migrated to FastAPI routers,
use the router registry (router_registry.has_router() and router_registry.create_router()). use the router registry (router_registry.has_router() and router_registry.build_router()).
Args: Args:
external_apis: Optional dictionary of external API specifications external_apis: Optional dictionary of external API specifications

View file

@ -44,7 +44,7 @@ from llama_stack.core.request_headers import (
request_provider_data_context, request_provider_data_context,
user_from_scope, user_from_scope,
) )
from llama_stack.core.server.router_registry import create_router, has_router from llama_stack.core.server.router_registry import build_router
from llama_stack.core.server.routes import get_all_api_routes from llama_stack.core.server.routes import get_all_api_routes
from llama_stack.core.stack import ( from llama_stack.core.stack import (
Stack, Stack,
@ -449,14 +449,6 @@ def create_app() -> StackApp:
external_apis = load_external_apis(config) external_apis = load_external_apis(config)
all_routes = get_all_api_routes(external_apis) all_routes = get_all_api_routes(external_apis)
# Import batches router to trigger router registration
# This ensures the router is registered before we try to use it
# We will make this code better once the migration is complete
try:
from llama_stack.core.server.routers import batches # noqa: F401
except ImportError:
pass
if config.apis: if config.apis:
apis_to_serve = set(config.apis) apis_to_serve = set(config.apis)
else: else:
@ -483,15 +475,11 @@ def create_app() -> StackApp:
for api_str in apis_to_serve: for api_str in apis_to_serve:
api = Api(api_str) api = Api(api_str)
if has_router(api): # Try to discover and use a router factory from the API package
router = create_router(api, impl_getter) router = build_router(api, impl_getter)
if router: if router:
app.include_router(router) app.include_router(router)
logger.debug(f"Registered router for {api} API") logger.debug(f"Registered router for {api} API")
else:
logger.warning(
f"API '{api.value}' has a registered router factory but it returned None. Skipping this API."
)
else: else:
# Fall back to old webmethod-based route discovery until the migration is complete # Fall back to old webmethod-based route discovery until the migration is complete
routes = all_routes[api] routes = all_routes[api]

View file

@ -8,7 +8,7 @@
This module contains the Batches protocol definition. This module contains the Batches protocol definition.
Pydantic models are defined in llama_stack_api.batches.models. Pydantic models are defined in llama_stack_api.batches.models.
The router implementation is in llama_stack.core.server.routers.batches. The FastAPI router is defined in llama_stack_api.batches.routes.
""" """
from typing import Literal, Protocol, runtime_checkable from typing import Literal, Protocol, runtime_checkable

View file

@ -7,7 +7,8 @@
"""FastAPI router for the Batches API. """FastAPI router for the Batches API.
This module defines the FastAPI router for the Batches API using standard This module defines the FastAPI router for the Batches API using standard
FastAPI route decorators instead of Protocol-based route discovery. FastAPI route decorators. The router is defined in the API package to keep
all API-related code together.
""" """
from collections.abc import Callable from collections.abc import Callable
@ -15,15 +16,14 @@ from typing import Annotated
from fastapi import APIRouter, Body, Depends from fastapi import APIRouter, Body, Depends
from llama_stack.core.server.router_registry import register_router
from llama_stack.core.server.router_utils import standard_responses
from llama_stack_api.batches import Batches, BatchObject, ListBatchesResponse from llama_stack_api.batches import Batches, BatchObject, ListBatchesResponse
from llama_stack_api.batches.models import CreateBatchRequest from llama_stack_api.batches.models import CreateBatchRequest
from llama_stack_api.datatypes import Api from llama_stack_api.datatypes import Api
from llama_stack_api.router_utils import standard_responses
from llama_stack_api.version import LLAMA_STACK_API_V1 from llama_stack_api.version import LLAMA_STACK_API_V1
def create_batches_router(impl_getter: Callable[[Api], Batches]) -> APIRouter: def create_router(impl_getter: Callable[[Api], Batches]) -> APIRouter:
"""Create a FastAPI router for the Batches API. """Create a FastAPI router for the Batches API.
Args: Args:
@ -111,7 +111,3 @@ def create_batches_router(impl_getter: Callable[[Api], Batches]) -> APIRouter:
return await svc.list_batches(after=after, limit=limit) return await svc.list_batches(after=after, limit=limit)
return router return router
# Register the router factory
register_router(Api.batches, create_batches_router)

View file

@ -24,6 +24,7 @@ classifiers = [
"Topic :: Scientific/Engineering :: Information Analysis", "Topic :: Scientific/Engineering :: Information Analysis",
] ]
dependencies = [ dependencies = [
"fastapi>=0.115.0,<1.0",
"pydantic>=2.11.9", "pydantic>=2.11.9",
"jsonschema", "jsonschema",
"opentelemetry-sdk>=1.30.0", "opentelemetry-sdk>=1.30.0",

View file

@ -4,7 +4,12 @@
# This source code is licensed under the terms described in the LICENSE file in # This source code is licensed under the terms described in the LICENSE file in
# the root directory of this source tree. # the root directory of this source tree.
"""Utilities for creating FastAPI routers with standard error responses.""" """Utilities for creating FastAPI routers with standard error responses.
This module provides standard error response definitions for FastAPI routers.
These responses use OpenAPI $ref references to component responses defined
in the OpenAPI specification.
"""
standard_responses = { standard_responses = {
400: {"$ref": "#/components/responses/BadRequest400"}, 400: {"$ref": "#/components/responses/BadRequest400"},

6
uv.lock generated
View file

@ -1,5 +1,5 @@
version = 1 version = 1
revision = 2 revision = 3
requires-python = ">=3.12" requires-python = ">=3.12"
resolution-markers = [ resolution-markers = [
"(python_full_version >= '3.13' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux')", "(python_full_version >= '3.13' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux')",
@ -2294,6 +2294,7 @@ name = "llama-stack-api"
version = "0.4.0.dev0" version = "0.4.0.dev0"
source = { editable = "src/llama_stack_api" } source = { editable = "src/llama_stack_api" }
dependencies = [ dependencies = [
{ name = "fastapi" },
{ name = "jsonschema" }, { name = "jsonschema" },
{ name = "opentelemetry-exporter-otlp-proto-http" }, { name = "opentelemetry-exporter-otlp-proto-http" },
{ name = "opentelemetry-sdk" }, { name = "opentelemetry-sdk" },
@ -2302,6 +2303,7 @@ dependencies = [
[package.metadata] [package.metadata]
requires-dist = [ requires-dist = [
{ name = "fastapi", specifier = ">=0.115.0,<1.0" },
{ name = "jsonschema" }, { name = "jsonschema" },
{ name = "opentelemetry-exporter-otlp-proto-http", specifier = ">=1.30.0" }, { name = "opentelemetry-exporter-otlp-proto-http", specifier = ">=1.30.0" },
{ name = "opentelemetry-sdk", specifier = ">=1.30.0" }, { name = "opentelemetry-sdk", specifier = ">=1.30.0" },
@ -4656,6 +4658,8 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/6b/fa/3234f913fe9a6525a7b97c6dad1f51e72b917e6872e051a5e2ffd8b16fbb/ruamel.yaml.clib-0.2.14-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:70eda7703b8126f5e52fcf276e6c0f40b0d314674f896fc58c47b0aef2b9ae83", size = 137970, upload-time = "2025-09-22T19:51:09.472Z" }, { url = "https://files.pythonhosted.org/packages/6b/fa/3234f913fe9a6525a7b97c6dad1f51e72b917e6872e051a5e2ffd8b16fbb/ruamel.yaml.clib-0.2.14-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:70eda7703b8126f5e52fcf276e6c0f40b0d314674f896fc58c47b0aef2b9ae83", size = 137970, upload-time = "2025-09-22T19:51:09.472Z" },
{ url = "https://files.pythonhosted.org/packages/ef/ec/4edbf17ac2c87fa0845dd366ef8d5852b96eb58fcd65fc1ecf5fe27b4641/ruamel.yaml.clib-0.2.14-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a0cb71ccc6ef9ce36eecb6272c81afdc2f565950cdcec33ae8e6cd8f7fc86f27", size = 739639, upload-time = "2025-09-22T19:51:10.566Z" }, { url = "https://files.pythonhosted.org/packages/ef/ec/4edbf17ac2c87fa0845dd366ef8d5852b96eb58fcd65fc1ecf5fe27b4641/ruamel.yaml.clib-0.2.14-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a0cb71ccc6ef9ce36eecb6272c81afdc2f565950cdcec33ae8e6cd8f7fc86f27", size = 739639, upload-time = "2025-09-22T19:51:10.566Z" },
{ url = "https://files.pythonhosted.org/packages/15/18/b0e1fafe59051de9e79cdd431863b03593ecfa8341c110affad7c8121efc/ruamel.yaml.clib-0.2.14-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e7cb9ad1d525d40f7d87b6df7c0ff916a66bc52cb61b66ac1b2a16d0c1b07640", size = 764456, upload-time = "2025-09-22T19:51:11.736Z" }, { url = "https://files.pythonhosted.org/packages/15/18/b0e1fafe59051de9e79cdd431863b03593ecfa8341c110affad7c8121efc/ruamel.yaml.clib-0.2.14-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e7cb9ad1d525d40f7d87b6df7c0ff916a66bc52cb61b66ac1b2a16d0c1b07640", size = 764456, upload-time = "2025-09-22T19:51:11.736Z" },
{ url = "https://files.pythonhosted.org/packages/e7/cd/150fdb96b8fab27fe08d8a59fe67554568727981806e6bc2677a16081ec7/ruamel_yaml_clib-0.2.14-cp314-cp314-win32.whl", hash = "sha256:9b4104bf43ca0cd4e6f738cb86326a3b2f6eef00f417bd1e7efb7bdffe74c539", size = 102394, upload-time = "2025-11-14T21:57:36.703Z" },
{ url = "https://files.pythonhosted.org/packages/bd/e6/a3fa40084558c7e1dc9546385f22a93949c890a8b2e445b2ba43935f51da/ruamel_yaml_clib-0.2.14-cp314-cp314-win_amd64.whl", hash = "sha256:13997d7d354a9890ea1ec5937a219817464e5cc344805b37671562a401ca3008", size = 122673, upload-time = "2025-11-14T21:57:38.177Z" },
] ]
[[package]] [[package]]