Skip to content

Commit 60d9baf

Browse files
jopemachineclaude
andauthored
refactor(BA-6003): bulk-migrate all pydantic BaseModel subclasses to BackendAISchema (#11554)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 9d5dbf2 commit 60d9baf

150 files changed

Lines changed: 745 additions & 608 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

changes/11554.enhance.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Migrate every remaining pydantic `BaseModel` subclass across `src/ai/backend/` to `BackendAISchema`, so any `model_validate()` failure auto-converts to a `BackendAISchemaValidationFailed` (HTTP 400) instead of leaking as raw `pydantic.ValidationError`.

src/ai/backend/account_manager/api/utils.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
from pydantic import BaseModel, ConfigDict, ValidationError
1414

1515
from ai.backend.account_manager.exceptions import AuthorizationFailed, InvalidAPIParameters
16+
from ai.backend.common.exception import BackendAISchemaValidationFailed
17+
from ai.backend.common.types import BackendAISchema
1618
from ai.backend.logging import BraceStyleAdapter
1719

1820

@@ -69,7 +71,7 @@ def mask_sensitive_keys(data: Mapping[str, Any]) -> Mapping[str, Any]:
6971
TBaseModel = TypeVar("TBaseModel", bound=BaseModel)
7072

7173

72-
class RequestData(BaseModel):
74+
class RequestData(BackendAISchema):
7375
model_config = ConfigDict(
7476
extra="allow",
7577
)
@@ -173,8 +175,13 @@ async def wrapped(
173175
kwargs["query"] = query_params
174176
except (json.decoder.JSONDecodeError, yaml.YAMLError, yaml.MarkedYAMLError) as e:
175177
raise InvalidAPIParameters("Malformed body") from e
176-
except ValidationError as e:
177-
raise InvalidAPIParameters("Input validation error", extra_data=e.errors()) from e
178+
except (BackendAISchemaValidationFailed, ValidationError) as e:
179+
# ``ValidationError`` covers plain ``BaseModel`` subclasses that
180+
# skip the ``BackendAISchema`` auto-conversion override.
181+
raise InvalidAPIParameters(
182+
"Input validation error",
183+
extra_data={"errors": e.errors()},
184+
) from e
178185
result = await handler(request, checked_params, *args, **kwargs)
179186
return ensure_stream_response_type(result)
180187

src/ai/backend/account_manager/config.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212

1313
import click
1414
from pydantic import (
15-
BaseModel,
1615
ConfigDict,
1716
Field,
1817
GetCoreSchemaHandler,
@@ -23,6 +22,7 @@
2322
from pydantic_core import PydanticUndefined, core_schema
2423

2524
from ai.backend.common import config
25+
from ai.backend.common.types import BackendAISchema
2626
from ai.backend.logging import LogLevel
2727

2828
from .types import EventLoopType
@@ -46,7 +46,7 @@ class TransactionIsolationLevel(enum.StrEnum):
4646
SERIALIZABLE = "SERIALIZABLE"
4747

4848

49-
class BaseSchema(BaseModel):
49+
class BaseSchema(BackendAISchema):
5050
model_config = ConfigDict(
5151
validate_by_name=True,
5252
from_attributes=True,

src/ai/backend/agent/kernel_registry/types.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,19 @@
33
from collections.abc import Mapping
44
from typing import TYPE_CHECKING, Self
55

6-
from pydantic import BaseModel, Field
6+
from pydantic import Field
77

88
from ai.backend.agent.kernel import KernelOwnershipData
99
from ai.backend.agent.proxy import DomainSocketPathPair
1010
from ai.backend.agent.resources import KernelResourceSpec
1111
from ai.backend.common.docker import ImageRef
12-
from ai.backend.common.types import AgentId, KernelId, ServicePort, SessionTypes
12+
from ai.backend.common.types import AgentId, BackendAISchema, KernelId, ServicePort, SessionTypes
1313

1414
if TYPE_CHECKING:
1515
from ai.backend.agent.docker.kernel import DockerKernel
1616

1717

18-
class KernelRecoveryData(BaseModel):
18+
class KernelRecoveryData(BackendAISchema):
1919
"""
2020
Data required for recovering a Kernel.
2121
Agent should load and write Kernel data using this structure

src/ai/backend/agent/proxy.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@
44
from typing import Any
55

66
import attrs
7-
from pydantic import BaseModel
87

98
from ai.backend.common.asyncio import current_loop
9+
from ai.backend.common.types import BackendAISchema
1010

1111

1212
@attrs.define(auto_attribs=True, slots=True)
@@ -16,7 +16,7 @@ class DomainSocketProxy:
1616
proxy_server: asyncio.AbstractServer
1717

1818

19-
class DomainSocketPathPair(BaseModel):
19+
class DomainSocketPathPair(BackendAISchema):
2020
host_sock_path: Path
2121
host_proxy_path: Path
2222

src/ai/backend/agent/scratch/types.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,15 @@
11
from collections.abc import Mapping
22
from typing import Self
33

4-
from pydantic import BaseModel
5-
64
from ai.backend.agent.kernel import KernelOwnershipData
75
from ai.backend.agent.kernel_registry.types import KernelRecoveryData
86
from ai.backend.agent.proxy import DomainSocketPathPair
97
from ai.backend.agent.resources import KernelResourceSpec
108
from ai.backend.common.docker import ImageRef
11-
from ai.backend.common.types import AgentId, KernelId, ServicePort, SessionTypes
9+
from ai.backend.common.types import AgentId, BackendAISchema, KernelId, ServicePort, SessionTypes
1210

1311

14-
class KernelRecoveryScratchData(BaseModel):
12+
class KernelRecoveryScratchData(BackendAISchema):
1513
"""
1614
Serializable subset of KernelRecoveryData for scratch storage.
1715
Excludes `resource_spec` and `environ` which are loaded separately.

src/ai/backend/agent/server.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,6 @@
4444
from callosum.ordering import ExitOrderedAsyncScheduler
4545
from callosum.rpc import Peer, RPCMessage
4646
from etcd_client import WatchEventType
47-
from pydantic import ValidationError
4847
from setproctitle import setproctitle
4948
from zmq.auth.certs import load_certificate
5049

@@ -87,7 +86,7 @@
8786
KernelTerminatedBroadcastEvent,
8887
)
8988
from ai.backend.common.events.event_types.kernel.types import KernelLifecycleEventReason
90-
from ai.backend.common.exception import ConfigurationError
89+
from ai.backend.common.exception import BackendAISchemaValidationFailed, ConfigurationError
9190
from ai.backend.common.health_checker.checkers.etcd import EtcdHealthChecker
9291
from ai.backend.common.health_checker.checkers.valkey import ValkeyHealthChecker
9392
from ai.backend.common.health_checker.probe import HealthProbe, HealthProbeOptions
@@ -1740,7 +1739,7 @@ def main(
17401739
if server_config.debug.enabled:
17411740
print("== Agent configuration ==")
17421741
pprint(server_config.model_dump(by_alias=True))
1743-
except ValidationError as e:
1742+
except BackendAISchemaValidationFailed as e:
17441743
print(
17451744
"ConfigurationError: Agent local config failed validation checks:",
17461745
file=sys.stderr,

src/ai/backend/appproxy/common/config.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from pydantic_core import PydanticUndefined, core_schema
1818

1919
from ai.backend.common.meta import BackendAIConfigMeta, CompositeType, ConfigExample
20+
from ai.backend.common.types import BackendAISchema
2021

2122
from .errors import (
2223
GroupNotFoundError,
@@ -30,7 +31,7 @@
3031
# FIXME: merge majority of common definitions to ai.backend.common when ready
3132

3233

33-
class BaseSchema(BaseModel):
34+
class BaseSchema(BackendAISchema):
3435
model_config = ConfigDict(
3536
populate_by_name=True,
3637
from_attributes=True,
@@ -723,7 +724,7 @@ class UnsupportedTypeError(RuntimeError):
723724

724725

725726
def generate_example_json(
726-
schema: type[BaseModel] | types.GenericAlias | types.UnionType,
727+
schema: type[BackendAISchema] | types.GenericAlias | types.UnionType,
727728
parent: list[str] | None = None,
728729
) -> dict[str, Any] | list[Any]:
729730
if parent is None:

src/ai/backend/appproxy/common/openapi.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from pydantic import BaseModel
99

1010
from ai.backend.appproxy.common.types import PydanticResponse
11+
from ai.backend.common.types import BackendAISchema
1112

1213
from . import __version__
1314

@@ -131,7 +132,7 @@ def generate_openapi(
131132
route_def["description"] = "\n".join(description)
132133
type_hints = get_type_hints(route.handler)
133134

134-
def _parse_schema(model_cls: type[BaseModel]) -> dict[str, Any]:
135+
def _parse_schema(model_cls: type[BackendAISchema]) -> dict[str, Any]:
135136
if not issubclass(model_cls, BaseModel):
136137
raise RuntimeError(f"{model_cls} not considered as a valid response type")
137138

src/ai/backend/appproxy/common/types.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
from aiohttp import web
1717
from pydantic import AliasChoices, AnyUrl, BaseModel, ConfigDict, Field
1818

19-
from ai.backend.common.types import ModelServiceStatus, RuntimeVariant
19+
from ai.backend.common.types import BackendAISchema, ModelServiceStatus, RuntimeVariant
2020

2121
# FIXME: merge majority of common definitions to ai.backend.common when ready
2222

@@ -84,7 +84,7 @@ class DigestModType(enum.StrEnum):
8484
]
8585

8686

87-
class RouteInfo(BaseModel):
87+
class RouteInfo(BackendAISchema):
8888
"""Information about a route within a circuit.
8989
9090
Routes describe a kernel endpoint (``kernel_host`` + ``kernel_port``)
@@ -157,7 +157,7 @@ def current_kernel_host(self) -> str:
157157
return self.kernel_host or "localhost"
158158

159159

160-
class SerializableCircuit(BaseModel):
160+
class SerializableCircuit(BackendAISchema):
161161
"""
162162
Serializable representation of `ai.backend.appproxy.coordinator.models.Circuit`
163163
"""
@@ -241,7 +241,7 @@ def traefik_router_name(self) -> str:
241241
return f"bai_router_{self.id}@etcd"
242242

243243

244-
class SerializableToken(BaseModel):
244+
class SerializableToken(BackendAISchema):
245245
login_session_token: str | None
246246
kernel_host: str
247247
kernel_port: int
@@ -252,7 +252,7 @@ class SerializableToken(BaseModel):
252252
domain_name: str
253253

254254

255-
class SessionConfig(BaseModel):
255+
class SessionConfig(BackendAISchema):
256256
model_config = ConfigDict(populate_by_name=True)
257257

258258
id: Annotated[UUID | None, Field(default=None)]
@@ -265,13 +265,13 @@ class SessionConfig(BaseModel):
265265
domain_name: str
266266

267267

268-
class EndpointConfig(BaseModel):
268+
class EndpointConfig(BackendAISchema):
269269
id: UUID
270270
runtime_variant: Annotated[RuntimeVariant | None, Field(default=None)]
271271
existing_url: AnyUrl | None
272272

273273

274-
class HealthCheckState(BaseModel):
274+
class HealthCheckState(BackendAISchema):
275275
"""
276276
Runtime health check state
277277
"""

0 commit comments

Comments
 (0)