diff --git a/scripts/openapi_generator/app.py b/scripts/openapi_generator/app.py index 48afc157d..e98aafb35 100644 --- a/scripts/openapi_generator/app.py +++ b/scripts/openapi_generator/app.py @@ -76,14 +76,8 @@ def create_llama_stack_app() -> FastAPI: ], ) - # Import batches router to trigger router registration - try: - 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 + # Include routers for APIs that have them (automatic discovery) + from llama_stack.core.server.router_registry import build_router, has_router def dummy_impl_getter(api: Api) -> Any: """Dummy implementation getter for OpenAPI generation.""" @@ -95,7 +89,7 @@ def create_llama_stack_app() -> FastAPI: protocols = api_protocol_map() for api in protocols.keys(): if has_router(api): - router = create_router(api, dummy_impl_getter) + router = build_router(api, dummy_impl_getter) if router: app.include_router(router) diff --git a/src/llama_stack/core/inspect.py b/src/llama_stack/core/inspect.py index be5a26c14..ac7958968 100644 --- a/src/llama_stack/core/inspect.py +++ b/src/llama_stack/core/inspect.py @@ -10,7 +10,7 @@ from pydantic import BaseModel from llama_stack.core.datatypes import StackRunConfig 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_api import ( Api, @@ -120,7 +120,7 @@ class DistributionInspectImpl(Inspect): if not has_router(api): continue - router = create_router(api, dummy_impl_getter) + router = build_router(api, dummy_impl_getter) if not router: continue diff --git a/src/llama_stack/core/server/router_registry.py b/src/llama_stack/core/server/router_registry.py index e149d1346..4849241e2 100644 --- a/src/llama_stack/core/server/router_registry.py +++ b/src/llama_stack/core/server/router_registry.py @@ -4,12 +4,13 @@ # This source code is licensed under the terms described in the LICENSE file in # 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 -migrated to use explicit FastAPI routers instead of Protocol-based route discovery. +This module provides utilities to discover and create FastAPI routers from API packages. +Routers are automatically discovered by checking for routes modules in each API package. """ +import importlib from collections.abc import Callable from typing import TYPE_CHECKING, Any @@ -18,47 +19,42 @@ from fastapi import APIRouter if TYPE_CHECKING: 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: - """Check if an API has a registered router. + """Check if an API has a router factory in its routes module. Args: api: The API enum value 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: - """Create a router for an API if one is registered. +def build_router(api: "Api", impl_getter: Callable[["Api"], Any]) -> APIRouter | None: + """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: api: The API enum value impl_getter: Function that returns the implementation for a given API 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] - if api_value not in _router_factories: - return None + try: + routes_module = importlib.import_module(f"llama_stack_api.{api.value}.routes") + 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 diff --git a/src/llama_stack/core/server/routes.py b/src/llama_stack/core/server/routes.py index 25027267f..cd24e9af2 100644 --- a/src/llama_stack/core/server/routes.py +++ b/src/llama_stack/core/server/routes.py @@ -30,7 +30,7 @@ def get_all_api_routes( This function only returns routes from APIs that use the legacy @webmethod 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: external_apis: Optional dictionary of external API specifications diff --git a/src/llama_stack/core/server/server.py b/src/llama_stack/core/server/server.py index 76f283f3a..f34cef4a9 100644 --- a/src/llama_stack/core/server/server.py +++ b/src/llama_stack/core/server/server.py @@ -44,7 +44,7 @@ from llama_stack.core.request_headers import ( request_provider_data_context, 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.stack import ( Stack, @@ -449,14 +449,6 @@ def create_app() -> StackApp: external_apis = load_external_apis(config) 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: apis_to_serve = set(config.apis) else: @@ -483,15 +475,11 @@ def create_app() -> StackApp: for api_str in apis_to_serve: api = Api(api_str) - if has_router(api): - router = create_router(api, impl_getter) - if router: - app.include_router(router) - 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." - ) + # Try to discover and use a router factory from the API package + router = build_router(api, impl_getter) + if router: + app.include_router(router) + logger.debug(f"Registered router for {api} API") else: # Fall back to old webmethod-based route discovery until the migration is complete routes = all_routes[api] diff --git a/src/llama_stack_api/batches/__init__.py b/src/llama_stack_api/batches/__init__.py index 2dff546b4..6f778de8e 100644 --- a/src/llama_stack_api/batches/__init__.py +++ b/src/llama_stack_api/batches/__init__.py @@ -8,7 +8,7 @@ This module contains the Batches protocol definition. 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 diff --git a/src/llama_stack/core/server/routers/batches.py b/src/llama_stack_api/batches/routes.py similarity index 90% rename from src/llama_stack/core/server/routers/batches.py rename to src/llama_stack_api/batches/routes.py index de1605eaf..e8b6aaf41 100644 --- a/src/llama_stack/core/server/routers/batches.py +++ b/src/llama_stack_api/batches/routes.py @@ -7,7 +7,8 @@ """FastAPI router for the Batches API. 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 @@ -15,15 +16,14 @@ from typing import Annotated 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.models import CreateBatchRequest 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 -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. 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 router - - -# Register the router factory -register_router(Api.batches, create_batches_router) diff --git a/src/llama_stack_api/pyproject.toml b/src/llama_stack_api/pyproject.toml index 0ceb2bb4e..0fec354db 100644 --- a/src/llama_stack_api/pyproject.toml +++ b/src/llama_stack_api/pyproject.toml @@ -24,6 +24,7 @@ classifiers = [ "Topic :: Scientific/Engineering :: Information Analysis", ] dependencies = [ + "fastapi>=0.115.0,<1.0", "pydantic>=2.11.9", "jsonschema", "opentelemetry-sdk>=1.30.0", diff --git a/src/llama_stack/core/server/router_utils.py b/src/llama_stack_api/router_utils.py similarity index 73% rename from src/llama_stack/core/server/router_utils.py rename to src/llama_stack_api/router_utils.py index 1c508af76..1ad19b05f 100644 --- a/src/llama_stack/core/server/router_utils.py +++ b/src/llama_stack_api/router_utils.py @@ -4,7 +4,12 @@ # This source code is licensed under the terms described in the LICENSE file in # 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 = { 400: {"$ref": "#/components/responses/BadRequest400"}, diff --git a/uv.lock b/uv.lock index a343eb5d8..a5eded2fd 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 2 +revision = 3 requires-python = ">=3.12" 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')", @@ -2294,6 +2294,7 @@ name = "llama-stack-api" version = "0.4.0.dev0" source = { editable = "src/llama_stack_api" } dependencies = [ + { name = "fastapi" }, { name = "jsonschema" }, { name = "opentelemetry-exporter-otlp-proto-http" }, { name = "opentelemetry-sdk" }, @@ -2302,6 +2303,7 @@ dependencies = [ [package.metadata] requires-dist = [ + { name = "fastapi", specifier = ">=0.115.0,<1.0" }, { name = "jsonschema" }, { name = "opentelemetry-exporter-otlp-proto-http", 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/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/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]]