| Author | HyeokJin Kim (hyeokjin@lablup.com) |
|---|---|
| Status | Draft |
| Created | 2026-01-17 |
| Created-Version | 26.1.0 |
| Target-Version | |
| Implemented-Version |
- JIRA: BA-3928
- Related BEP: BEP-1022: Pydantic Field Metadata Annotation
- Related BEP: BEP-1010: New GQL
BEP-1022 defined BackendAIAPIMeta for API field metadata management and mentioned Strawberry GraphQL integration in Phase 4. However, the specific implementation method for integrating Strawberry's type system with metadata was not specified.
- Required metadata may be missing: Version information should be included in description, but since it's free-form text, there's no guarantee that required values (version, deprecation, etc.) are included
- Inconsistency with Config structure and REST API: Config uses
BackendAIConfigMeta, REST API usesBackendAIAPIMetawithAnnotated, but GraphQL uses strings directly - Manual deprecation handling: Cannot automatically set deprecation_reason from metadata
- Define specific patterns for integrating
BackendAIAPIMetawith Strawberry GraphQL - Provide utility functions for automatic description and deprecation handling
- Maintain consistency with BEP-1022's Pydantic-based approach
- Establish foundation for automatic API documentation generation
@strawberry.type(
description="Added in 26.1.0. User-level usage bucket..."
)
class UserUsageBucketGQL(Node):
user_uuid: UUID = strawberry.field(
description="UUID of the user this usage bucket belongs to."
)
# No version info, no structured metadata- Version information is free-form text within description
- Cannot programmatically extract version information
- Must manually add deprecation for each field
- Cannot auto-generate changelog or API documentation
Unlike Pydantic, Strawberry does not automatically extract metadata from Annotated types. Therefore, we provide wrappers that integrate BackendAIAPIMeta with various Strawberry components:
backend_ai_field():strawberry.field()wrapper - for field definitionsbackend_ai_type():@strawberry.typewrapper - Output type decoratorbackend_ai_input(): Pydantic model-based Input decorator - ensures same validation as REST API
# src/ai/backend/manager/api/gql/utils.py
from __future__ import annotations
from collections.abc import Callable, Sequence
from typing import Any
import strawberry
from pydantic import BaseModel
from strawberry.field import StrawberryField
from ai.backend.common.meta import BackendAIAPIMeta
def backend_ai_field(
meta: BackendAIAPIMeta,
*,
name: str | None = None,
default: Any = strawberry.UNSET,
default_factory: Callable[[], Any] | None = None,
init: bool = True,
repr_: bool = True,
hash_: bool | None = None,
compare: bool = True,
graphql_type: Any | None = None,
permission_classes: list[type] | None = None,
directives: Sequence[object] | None = None,
) -> StrawberryField:
"""Create a Strawberry field with BackendAI metadata.
Automatically generates description with version prefix and
sets deprecation_reason from metadata.
Args:
meta: BackendAI API metadata containing description, version, etc.
name: GraphQL field name (if different from Python attribute)
default: Field default value
default_factory: Default value factory function
init: Include in dataclass __init__
repr_: Include in dataclass __repr__
hash_: Include in dataclass __hash__
compare: Include in dataclass comparison methods
graphql_type: Explicit GraphQL type specification
permission_classes: List of field access permission classes
directives: List of GraphQL directives
Returns:
StrawberryField with integrated metadata
Example:
>>> user_uuid: UUID = backend_ai_field(
... BackendAIAPIMeta(
... description="UUID of the user",
... added_version="26.1.0",
... )
... )
"""
# Generate description with version prefix
description = f"Added in {meta.added_version}. {meta.description}"
# Add deprecated marker if applicable
if meta.deprecated_version:
description = f"[Deprecated in {meta.deprecated_version}] {description}"
# metadata is used internally by Strawberry for storing additional field info
# - Enables programmatic version info extraction during introspection
# - Used by API documentation generation tools
# - Not exposed in GraphQL schema (server-side only)
return strawberry.field(
name=name,
default=default,
default_factory=default_factory,
init=init,
repr=repr_,
hash=hash_,
compare=compare,
description=description,
deprecation_reason=meta.deprecation_hint,
graphql_type=graphql_type,
permission_classes=permission_classes or [],
directives=directives or (),
metadata={"backend_ai_meta": meta},
)
def backend_ai_type(
meta: BackendAIAPIMeta,
*,
name: str | None = None,
directives: Sequence[object] | None = None,
):
"""Strawberry type decorator with BackendAI metadata.
Example:
>>> @backend_ai_type(
... BackendAIAPIMeta(
... description="User-level usage bucket",
... added_version="26.1.0",
... )
... )
... class UserUsageBucketGQL(Node):
... pass
"""
description = f"Added in {meta.added_version}. {meta.description}"
if meta.deprecated_version:
description = f"[Deprecated in {meta.deprecated_version}] {description}"
def decorator(cls):
return strawberry.type(
cls,
name=name,
description=description,
directives=directives or (),
)
return decorator
def backend_ai_input(
model: type[BaseModel],
meta: BackendAIAPIMeta,
*,
name: str | None = None,
all_fields: bool = True,
directives: Sequence[object] | None = None,
):
"""Pydantic model-based Strawberry input decorator.
Retrieves validation rules from Pydantic model and
integrates version and description metadata from BackendAIAPIMeta.
Input types always operate on Pydantic basis to ensure same validation as REST API.
Args:
model: Pydantic model with validation rules defined
meta: BackendAI API metadata containing description, version, etc.
name: GraphQL input type name (default: class name)
all_fields: Whether to include all fields from Pydantic model
directives: List of GraphQL directives
Example:
>>> @backend_ai_input(
... model=CreateObjectStorageSpec,
... meta=BackendAIAPIMeta(
... description="Object Storage creation input",
... added_version="25.14.0",
... ),
... )
... class CreateObjectStorageInput:
... pass
"""
from strawberry.experimental import pydantic as strawberry_pydantic
description = f"Added in {meta.added_version}. {meta.description}"
def decorator(cls):
return strawberry_pydantic.input(
model=model,
all_fields=all_fields,
name=name,
description=description,
directives=directives or (),
)(cls)
return decoratorfrom ai.backend.common.meta import BackendAIAPIMeta
from ai.backend.manager.api.gql.utils import backend_ai_type, backend_ai_field
@backend_ai_type(
BackendAIAPIMeta(
description="Bucket aggregating resource usage per user",
added_version="26.1.0",
)
)
class UserUsageBucketGQL(Node):
id: NodeID[str]
user_uuid: UUID = backend_ai_field(
BackendAIAPIMeta(
description="UUID of the user this usage bucket belongs to",
added_version="26.1.0",
)
)
project_id: UUID = backend_ai_field(
BackendAIAPIMeta(
description="UUID of the project the user belongs to",
added_version="26.1.0",
)
)
# Deprecated field example
legacy_group_id: UUID | None = backend_ai_field(
BackendAIAPIMeta(
description="Legacy group identifier",
added_version="25.1.0",
deprecated_version="26.1.0",
deprecation_hint="Use project_id instead",
),
default=None,
)@backend_ai_input(
model=CreateObjectStorageSpec,
meta=BackendAIAPIMeta(
description="Object Storage creation input",
added_version="25.14.0",
),
)
class CreateObjectStorageInput:
pass@strawberry.type
class Mutation:
@backend_ai_field(
BackendAIAPIMeta(
description="Create new Object Storage configuration",
added_version="25.14.0",
)
)
async def create_object_storage(
self,
input: CreateObjectStorageInput,
info: Info[StrawberryGQLContext],
) -> ObjectStorage:
...# src/ai/backend/manager/api/gql/utils.py
def get_gql_field_meta(
gql_type: type,
field_name: str,
) -> BackendAIAPIMeta | None:
"""Extract BackendAIAPIMeta from Strawberry field.
Useful for documentation generation and introspection tools.
"""
strawberry_type = getattr(gql_type, '__strawberry_definition__', None)
if strawberry_type is None:
return None
for field in strawberry_type.fields:
if field.python_name == field_name:
return field.metadata.get("backend_ai_meta")
return None
def collect_all_field_versions(gql_type: type) -> dict[str, str]:
"""Collect version information for all fields in a type.
Returns:
Dict mapping field names to added_version
"""
result = {}
strawberry_type = getattr(gql_type, '__strawberry_definition__', None)
if strawberry_type is None:
return result
for field in strawberry_type.fields:
meta = field.metadata.get("backend_ai_meta")
if meta:
result[field.python_name] = meta.added_version
return resultThe wrapper functions generate standard GraphQL schema with version information:
"""
Added in 26.1.0. Bucket aggregating resource usage per user
"""
type UserUsageBucket implements Node {
id: ID!
"""
Added in 26.1.0. UUID of the user this usage bucket belongs to
"""
userUuid: UUID!
"""
Added in 26.1.0. UUID of the project the user belongs to
"""
projectId: UUID!
"""
[Deprecated in 26.1.0] Added in 25.1.0. Legacy group identifier
"""
legacyGroupId: UUID @deprecated(reason: "Use project_id instead")
}- Existing
strawberry.field(description=...)pattern continues to work - New pattern is opt-in and can be applied gradually
- No breaking changes in GraphQL schema output
- New types/fields use
backend_ai_field()andbackend_ai_type() - Existing types are gradually migrated when modified
- No need to migrate everything at once
@strawberry.type(description="Added in 25.14.0. Legacy type")
class LegacyType:
# Existing pattern - continues to work
old_field: str = strawberry.field(description="Added in 25.14.0. Existing field")
# New pattern
new_field: str = backend_ai_field(
BackendAIAPIMeta(
description="New field with structured metadata",
added_version="26.1.0",
)
)Goal: Implement wrapper functions
Tasks:
- Add
backend_ai_field()tosrc/ai/backend/manager/api/gql/utils.py - Add
backend_ai_type()decorator - Add
backend_ai_input()decorator - Add metadata extraction utilities
- Add unit tests
Goal: Use new pattern for all new GraphQL types
Tasks:
- Update coding guidelines to recommend new pattern
- Apply to new types under development
- Document in
api/gql/README.md
Goal: Migrate existing types when modified
Tasks:
- Create migration checklist
- Update types when modified for other reasons
- Track migration progress
Goal: Leverage metadata for documentation
Tasks:
- Generate API changelog from version metadata
- Generate deprecation reports
- Integrate with API documentation tools
- Should we create a schema extension that automatically validates all fields have metadata?
- Should we support metadata extraction from
Annotatedtypes in addition to wrapper functions? - How should we handle fields inherited from base classes (e.g.,
Node.id)?