docs: api separation (#3630)

# What does this PR do?

First step towards cleaning up the API reference section of the docs.

- Separates API reference into 3 sections: stable (`v1`), experimental (`v1alpha` and `v1beta`), and deprecated (`deprecated=True`)
- Each section is accessible via the dropdown menu and `docs/api-overview`

<img width="1237" height="321" alt="Screenshot 2025-09-30 at 5 47 30 PM" src="https://github.com/user-attachments/assets/fe0e498c-b066-46ed-a48e-4739d3b6724c" />

<img width="860" height="510" alt="Screenshot 2025-09-30 at 5 47 49 PM" src="https://github.com/user-attachments/assets/a92a8d8c-94bf-42d5-9f5b-b47bb2b14f9c" />

- Deprecated APIs: Added styling to the sidebar, and a notice on the endpoint pages

<img width="867" height="428" alt="Screenshot 2025-09-30 at 5 47 43 PM" src="https://github.com/user-attachments/assets/9e6e050d-c782-461b-8084-5ff6496d7bd9" />

Closes #3628

TODO in follow-up PRs:

- Add the ability to annotate API groups with supplementary content  (so we can have longer descriptions of complex APIs like Responses)
- Clean up docstrings to show API endpoints (or short semantic titles) in the sidebar

## Test Plan

- Local testing
- Made sure API conformance test still passes
This commit is contained in:
Alexey Rybak 2025-10-01 10:13:31 -07:00 committed by GitHub
parent 7f1a33f51c
commit b6a5bccadf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 35504 additions and 25684 deletions

View file

@ -34,40 +34,52 @@ def str_presenter(dumper, data):
return dumper.represent_scalar("tag:yaml.org,2002:str", data, style=style)
def main(output_dir: str):
output_dir = Path(output_dir)
if not output_dir.exists():
raise ValueError(f"Directory {output_dir} does not exist")
def generate_spec(output_dir: Path, stability_filter: str = None, main_spec: bool = False):
"""Generate OpenAPI spec with optional stability filtering."""
# Validate API protocols before generating spec
return_type_errors = validate_api()
if return_type_errors:
print("\nAPI Method Return Type Validation Errors:\n")
for error in return_type_errors:
print(error, file=sys.stderr)
sys.exit(1)
now = str(datetime.now())
print(
"Converting the spec to YAML (openapi.yaml) and HTML (openapi.html) at " + now
)
print("")
if stability_filter:
title_suffix = {
"stable": " - Stable APIs" if not main_spec else "",
"experimental": " - Experimental APIs",
"deprecated": " - Deprecated APIs"
}.get(stability_filter, f" - {stability_filter.title()} APIs")
# Use main spec filename for stable when main_spec=True
if main_spec and stability_filter == "stable":
filename_prefix = ""
else:
filename_prefix = f"{stability_filter}-"
description_suffix = {
"stable": "\n\n**✅ STABLE**: Production-ready APIs with backward compatibility guarantees.",
"experimental": "\n\n**🧪 EXPERIMENTAL**: Pre-release APIs (v1alpha, v1beta) that may change before becoming stable.",
"deprecated": "\n\n**⚠️ DEPRECATED**: Legacy APIs that may be removed in future versions. Use for migration reference only."
}.get(stability_filter, "")
else:
title_suffix = ""
filename_prefix = ""
description_suffix = ""
spec = Specification(
LlamaStack,
Options(
server=Server(url="http://any-hosted-llama-stack.com"),
info=Info(
title="Llama Stack Specification",
title=f"Llama Stack Specification{title_suffix}",
version=LLAMA_STACK_API_V1,
description="""This is the specification of the Llama Stack that provides
description=f"""This is the specification of the Llama Stack that provides
a set of endpoints and their corresponding interfaces that are tailored to
best leverage Llama Models.""",
best leverage Llama Models.{description_suffix}""",
),
include_standard_error_responses=True,
stability_filter=stability_filter, # Pass the filter to the generator
),
)
with open(output_dir / "llama-stack-spec.yaml", "w", encoding="utf-8") as fp:
yaml_filename = f"{filename_prefix}llama-stack-spec.yaml"
html_filename = f"{filename_prefix}llama-stack-spec.html"
with open(output_dir / yaml_filename, "w", encoding="utf-8") as fp:
y = yaml.YAML()
y.default_flow_style = False
y.block_seq_indent = 2
@ -83,9 +95,36 @@ def main(output_dir: str):
fp,
)
with open(output_dir / "llama-stack-spec.html", "w") as fp:
with open(output_dir / html_filename, "w") as fp:
spec.write_html(fp, pretty_print=True)
print(f"Generated {yaml_filename} and {html_filename}")
def main(output_dir: str):
output_dir = Path(output_dir)
if not output_dir.exists():
raise ValueError(f"Directory {output_dir} does not exist")
# Validate API protocols before generating spec
return_type_errors = validate_api()
if return_type_errors:
print("\nAPI Method Return Type Validation Errors:\n")
for error in return_type_errors:
print(error, file=sys.stderr)
sys.exit(1)
now = str(datetime.now())
print(f"Converting the spec to YAML (openapi.yaml) and HTML (openapi.html) at {now}")
print("")
# Generate main spec as stable APIs (llama-stack-spec.yaml)
print("Generating main specification (stable APIs)...")
generate_spec(output_dir, "stable", main_spec=True)
print("Generating other stability-filtered specifications...")
generate_spec(output_dir, "experimental")
generate_spec(output_dir, "deprecated")
if __name__ == "__main__":
fire.Fire(main)

View file

@ -7,13 +7,14 @@
import hashlib
import inspect
import ipaddress
import os
import types
import typing
from dataclasses import make_dataclass
from pathlib import Path
from typing import Annotated, Any, Dict, get_args, get_origin, Set, Union
from fastapi import UploadFile
from pydantic import BaseModel
from llama_stack.apis.datatypes import Error
from llama_stack.strong_typing.core import JsonType
@ -35,6 +36,7 @@ from llama_stack.strong_typing.schema import (
SchemaOptions,
)
from llama_stack.strong_typing.serialization import json_dump_string, object_to_json
from pydantic import BaseModel
from .operations import (
EndpointOperation,
@ -811,16 +813,121 @@ class Generator:
requestBody=requestBody,
responses=responses,
callbacks=callbacks,
deprecated=True if "DEPRECATED" in op.func_name else None,
deprecated=getattr(op.webmethod, "deprecated", False)
or "DEPRECATED" in op.func_name,
security=[] if op.public else None,
)
def _get_api_stability_priority(self, api_level: str) -> int:
"""
Return sorting priority for API stability levels.
Lower numbers = higher priority (appear first)
:param api_level: The API level (e.g., "v1", "v1beta", "v1alpha")
:return: Priority number for sorting
"""
stability_order = {
"v1": 0, # Stable - highest priority
"v1beta": 1, # Beta - medium priority
"v1alpha": 2, # Alpha - lowest priority
}
return stability_order.get(api_level, 999) # Unknown levels go last
def generate(self) -> Document:
paths: Dict[str, PathItem] = {}
endpoint_classes: Set[type] = set()
for op in get_endpoint_operations(
self.endpoint, use_examples=self.options.use_examples
):
# Collect all operations and filter by stability if specified
operations = list(
get_endpoint_operations(
self.endpoint, use_examples=self.options.use_examples
)
)
# Filter operations by stability level if requested
if self.options.stability_filter:
filtered_operations = []
for op in operations:
deprecated = (
getattr(op.webmethod, "deprecated", False)
or "DEPRECATED" in op.func_name
)
stability_level = op.webmethod.level
if self.options.stability_filter == "stable":
# Include v1 non-deprecated endpoints
if stability_level == "v1" and not deprecated:
filtered_operations.append(op)
elif self.options.stability_filter == "experimental":
# Include v1alpha and v1beta endpoints (deprecated or not)
if stability_level in ["v1alpha", "v1beta"]:
filtered_operations.append(op)
elif self.options.stability_filter == "deprecated":
# Include only deprecated endpoints
if deprecated:
filtered_operations.append(op)
operations = filtered_operations
print(
f"Filtered to {len(operations)} operations for stability level: {self.options.stability_filter}"
)
# Sort operations by multiple criteria for consistent ordering:
# 1. Stability level with deprecation handling (global priority):
# - Active stable (v1) comes first
# - Beta (v1beta) comes next
# - Alpha (v1alpha) comes next
# - Deprecated stable (v1 deprecated) comes last
# 2. Route path (group related endpoints within same stability level)
# 3. HTTP method (GET, POST, PUT, DELETE, PATCH)
# 4. Operation name (alphabetical)
def sort_key(op):
http_method_order = {
HTTPMethod.GET: 0,
HTTPMethod.POST: 1,
HTTPMethod.PUT: 2,
HTTPMethod.DELETE: 3,
HTTPMethod.PATCH: 4,
}
# Enhanced stability priority for migration pattern support
deprecated = getattr(op.webmethod, "deprecated", False)
stability_priority = self._get_api_stability_priority(op.webmethod.level)
# Deprecated versions should appear after everything else
# This ensures deprecated stable endpoints come last globally
if deprecated:
stability_priority += 10 # Push deprecated endpoints to the end
return (
stability_priority, # Global stability handling comes first
op.get_route(
op.webmethod
), # Group by route path within stability level
http_method_order.get(op.http_method, 999),
op.func_name,
)
operations.sort(key=sort_key)
# Debug output for migration pattern tracking
migration_routes = {}
for op in operations:
route_key = (op.get_route(op.webmethod), op.http_method)
if route_key not in migration_routes:
migration_routes[route_key] = []
migration_routes[route_key].append(
(op.webmethod.level, getattr(op.webmethod, "deprecated", False))
)
for route_key, versions in migration_routes.items():
if len(versions) > 1:
print(f"Migration pattern detected for {route_key[1]} {route_key[0]}:")
for level, deprecated in versions:
status = "DEPRECATED" if deprecated else "ACTIVE"
print(f" - {level} ({status})")
for op in operations:
endpoint_classes.add(op.defining_class)
operation = self._build_operation(op)
@ -851,6 +958,7 @@ class Generator:
doc_string = parse_type(cls)
if hasattr(cls, "API_NAMESPACE") and cls.API_NAMESPACE != cls.__name__:
continue
operation_tags.append(
Tag(
name=cls.__name__,

View file

@ -54,6 +54,7 @@ class Options:
property_description_fun: Optional[Callable[[type, str, str], str]] = None
captions: Optional[Dict[str, str]] = None
include_standard_error_responses: bool = True
stability_filter: Optional[str] = None
default_captions: ClassVar[Dict[str, str]] = {
"Operations": "Operations",