From dc95d240532f99c00f67c2840d8acdb791087d34 Mon Sep 17 00:00:00 2001 From: jopemachine Date: Thu, 5 Mar 2026 07:56:39 +0000 Subject: [PATCH 01/12] fix: Consider ExtraVFolderMountInput.mount_destination as required --- .../common/dto/manager/deployment/request.py | 2 +- .../api/gql/deployment/types/revision.py | 16 ++++--------- .../manager/api/rest/deployment/adapter.py | 23 +++++++++++-------- .../backend/manager/data/deployment/types.py | 4 ++-- src/ai/backend/manager/errors/deployment.py | 12 ++++++++++ 5 files changed, 33 insertions(+), 24 deletions(-) diff --git a/src/ai/backend/common/dto/manager/deployment/request.py b/src/ai/backend/common/dto/manager/deployment/request.py index 7ce8fa63560..d53b45844e9 100644 --- a/src/ai/backend/common/dto/manager/deployment/request.py +++ b/src/ai/backend/common/dto/manager/deployment/request.py @@ -257,7 +257,7 @@ class ExtraVFolderMountInput(BaseRequestModel): """Extra vfolder mount input.""" vfolder_id: UUID = Field(description="VFolder ID to mount") - mount_destination: str | None = Field(default=None, description="Mount destination path") + mount_destination: str = Field(description="Mount destination path") class RevisionInput(BaseRequestModel): diff --git a/src/ai/backend/manager/api/gql/deployment/types/revision.py b/src/ai/backend/manager/api/gql/deployment/types/revision.py index 00f566dd9d7..d495ec9b8e2 100644 --- a/src/ai/backend/manager/api/gql/deployment/types/revision.py +++ b/src/ai/backend/manager/api/gql/deployment/types/revision.py @@ -115,7 +115,7 @@ async def vfolder(self, info: Info[StrawberryGQLContext]) -> VFolder: @classmethod def from_dataclass(cls, data: ModelMountConfigData) -> ModelMountConfig | None: - if data.vfolder_id is None or data.mount_destination is None: + if data.vfolder_id is None: return None return cls( _vfolder_id=data.vfolder_id, @@ -487,7 +487,7 @@ class ModelMountConfigInput: @strawberry.input(description="Added in 25.19.0") class ExtraVFolderMountInput: vfolder_id: ID - mount_destination: str | None + mount_destination: str @strawberry.input( @@ -515,11 +515,7 @@ def to_model_revision_creator(self) -> ModelRevisionCreator: extra_mounts = [ MountInfo( vfolder_id=UUID(str(extra_mount.vfolder_id)), - kernel_path=PurePosixPath( - extra_mount.mount_destination - if extra_mount.mount_destination is not None - else "" - ), + kernel_path=PurePosixPath(extra_mount.mount_destination), ) for extra_mount in self.extra_mounts ] @@ -574,11 +570,7 @@ def to_model_revision_creator(self) -> ModelRevisionCreator: extra_mounts = [ MountInfo( vfolder_id=UUID(str(extra_mount.vfolder_id)), - kernel_path=PurePosixPath( - extra_mount.mount_destination - if extra_mount.mount_destination is not None - else "" - ), + kernel_path=PurePosixPath(extra_mount.mount_destination), ) for extra_mount in self.extra_mounts ] diff --git a/src/ai/backend/manager/api/rest/deployment/adapter.py b/src/ai/backend/manager/api/rest/deployment/adapter.py index 0515dc79076..dd23b9093ed 100644 --- a/src/ai/backend/manager/api/rest/deployment/adapter.py +++ b/src/ai/backend/manager/api/rest/deployment/adapter.py @@ -65,6 +65,7 @@ DeploymentPolicyData, ExecutionSpec, ModelDeploymentData, + ModelMountConfigData, ModelRevisionData, MountInfo, ReplicaSpec, @@ -78,6 +79,7 @@ RouteTrafficStatus as ManagerRouteTrafficStatus, ) from ai.backend.manager.errors.api import InvalidAPIParameters +from ai.backend.manager.errors.deployment import IncompleteRevisionData from ai.backend.manager.models.deployment_policy import BlueGreenSpec, RollingUpdateSpec from ai.backend.manager.repositories.base import ( BatchQuerier, @@ -224,17 +226,21 @@ def convert_to_dto(self, data: ModelRevisionData) -> RevisionDTO: model_runtime_config=ModelRuntimeConfigDTO( runtime_variant=data.model_runtime_config.runtime_variant, ), - model_mount_config=ModelMountConfigDTO( - # TODO: Generating a random UUID when vfolder_id is None creates a reference to a non-existent vfolder. Should raise an error instead. - vfolder_id=data.model_mount_config.vfolder_id or uuid4(), - # TODO: Empty string is not a valid path when mount_destination is None. Should make it a required field or assign a sensible default path. - mount_destination=data.model_mount_config.mount_destination or "", - definition_path=data.model_mount_config.definition_path, - ), + model_mount_config=self._convert_model_mount_config(data.model_mount_config), created_at=data.created_at, image_id=data.image_id, ) + @staticmethod + def _convert_model_mount_config(config: ModelMountConfigData) -> ModelMountConfigDTO: + if config.vfolder_id is None: + raise IncompleteRevisionData("model_mount_config.vfolder_id is required but was None") + return ModelMountConfigDTO( + vfolder_id=config.vfolder_id, + mount_destination=config.mount_destination, + definition_path=config.definition_path, + ) + def build_querier(self, request: SearchRevisionsRequest) -> BatchQuerier: """ Build a BatchQuerier for revisions from search request. @@ -390,8 +396,7 @@ def build_revision_creator(revision_input: RevisionInput) -> ModelRevisionCreato extra_mounts = [ MountInfo( vfolder_id=mount.vfolder_id, - # TODO: Empty string is not a valid path when mount_destination is None. Should make it a required field or assign a sensible default path. - kernel_path=PurePosixPath(mount.mount_destination or ""), + kernel_path=PurePosixPath(mount.mount_destination), ) for mount in revision_input.extra_mounts ] diff --git a/src/ai/backend/manager/data/deployment/types.py b/src/ai/backend/manager/data/deployment/types.py index 2b2d00caaf7..0f28c3e1bf5 100644 --- a/src/ai/backend/manager/data/deployment/types.py +++ b/src/ai/backend/manager/data/deployment/types.py @@ -470,7 +470,7 @@ class ModelRuntimeConfigData: @dataclass class ModelMountConfigData: vfolder_id: UUID | None - mount_destination: str | None + mount_destination: str definition_path: str @@ -521,7 +521,7 @@ class ModelDeploymentData: replica_state: ReplicaStateData default_deployment_strategy: DeploymentStrategy created_user_id: UUID - access_token_ids: UUID | None = None + access_token_ids: list[UUID] | None = None class DeploymentOrderField(enum.StrEnum): diff --git a/src/ai/backend/manager/errors/deployment.py b/src/ai/backend/manager/errors/deployment.py index 5aa44050029..8ffaa0b05ab 100644 --- a/src/ai/backend/manager/errors/deployment.py +++ b/src/ai/backend/manager/errors/deployment.py @@ -145,3 +145,15 @@ def error_code(self) -> ErrorCode: operation=ErrorOperation.READ, error_detail=ErrorDetail.INVALID_PARAMETERS, ) + + +class IncompleteRevisionData(BackendAIError, web.HTTPInternalServerError): + error_type = "https://api.backend.ai/probs/incomplete-revision-data" + error_title = "Revision data is missing required fields." + + def error_code(self) -> ErrorCode: + return ErrorCode( + domain=ErrorDomain.MODEL_SERVICE, + operation=ErrorOperation.READ, + error_detail=ErrorDetail.INVALID_PARAMETERS, + ) From e85b802fd0535c2d4c440b043f2cca079672c029 Mon Sep 17 00:00:00 2001 From: jopemachine Date: Thu, 5 Mar 2026 08:00:31 +0000 Subject: [PATCH 02/12] chore: update api schema dump Co-authored-by: octodog --- docs/manager/graphql-reference/supergraph.graphql | 2 +- docs/manager/graphql-reference/v2-schema.graphql | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/manager/graphql-reference/supergraph.graphql b/docs/manager/graphql-reference/supergraph.graphql index ff61daf3b17..593a9f1014f 100644 --- a/docs/manager/graphql-reference/supergraph.graphql +++ b/docs/manager/graphql-reference/supergraph.graphql @@ -4412,7 +4412,7 @@ input ExtraVFolderMountInput @join__type(graph: STRAWBERRY) { vfolderId: ID! - mountDestination: String + mountDestination: String! } """ diff --git a/docs/manager/graphql-reference/v2-schema.graphql b/docs/manager/graphql-reference/v2-schema.graphql index e98175038c1..af3d0f8ba55 100644 --- a/docs/manager/graphql-reference/v2-schema.graphql +++ b/docs/manager/graphql-reference/v2-schema.graphql @@ -2452,7 +2452,7 @@ type ExtraVFolderMountEdge { """Added in 25.19.0""" input ExtraVFolderMountInput { vfolderId: ID! - mountDestination: String + mountDestination: String! } """ From c95ffb387e091228e345819d8e1612965fd92ef5 Mon Sep 17 00:00:00 2001 From: jopemachine Date: Thu, 5 Mar 2026 08:11:30 +0000 Subject: [PATCH 03/12] WIP --- .../common/dto/manager/deployment/request.py | 2 +- .../api/gql/deployment/types/revision.py | 10 ++--- .../manager/api/rest/deployment/adapter.py | 20 +++------- .../backend/manager/data/deployment/types.py | 2 +- ...make_deployment_revision_model_not_null.py | 37 +++++++++++++++++++ .../manager/models/deployment_revision/row.py | 2 +- .../deployment/creators/deployment.py | 2 +- .../deployment/creators/endpoint.py | 2 +- .../deployment/creators/revision.py | 2 +- 9 files changed, 53 insertions(+), 26 deletions(-) create mode 100644 src/ai/backend/manager/models/alembic/versions/c4602c3d4f1d_make_deployment_revision_model_not_null.py diff --git a/src/ai/backend/common/dto/manager/deployment/request.py b/src/ai/backend/common/dto/manager/deployment/request.py index d53b45844e9..7ce8fa63560 100644 --- a/src/ai/backend/common/dto/manager/deployment/request.py +++ b/src/ai/backend/common/dto/manager/deployment/request.py @@ -257,7 +257,7 @@ class ExtraVFolderMountInput(BaseRequestModel): """Extra vfolder mount input.""" vfolder_id: UUID = Field(description="VFolder ID to mount") - mount_destination: str = Field(description="Mount destination path") + mount_destination: str | None = Field(default=None, description="Mount destination path") class RevisionInput(BaseRequestModel): diff --git a/src/ai/backend/manager/api/gql/deployment/types/revision.py b/src/ai/backend/manager/api/gql/deployment/types/revision.py index d495ec9b8e2..86f2dd68f13 100644 --- a/src/ai/backend/manager/api/gql/deployment/types/revision.py +++ b/src/ai/backend/manager/api/gql/deployment/types/revision.py @@ -114,9 +114,7 @@ async def vfolder(self, info: Info[StrawberryGQLContext]) -> VFolder: return VFolder(id=ID(vfolder_global_id)) @classmethod - def from_dataclass(cls, data: ModelMountConfigData) -> ModelMountConfig | None: - if data.vfolder_id is None: - return None + def from_dataclass(cls, data: ModelMountConfigData) -> ModelMountConfig: return cls( _vfolder_id=data.vfolder_id, mount_destination=data.mount_destination, @@ -487,7 +485,7 @@ class ModelMountConfigInput: @strawberry.input(description="Added in 25.19.0") class ExtraVFolderMountInput: vfolder_id: ID - mount_destination: str + mount_destination: str | None @strawberry.input( @@ -515,7 +513,7 @@ def to_model_revision_creator(self) -> ModelRevisionCreator: extra_mounts = [ MountInfo( vfolder_id=UUID(str(extra_mount.vfolder_id)), - kernel_path=PurePosixPath(extra_mount.mount_destination), + kernel_path=PurePosixPath(extra_mount.mount_destination or ""), ) for extra_mount in self.extra_mounts ] @@ -570,7 +568,7 @@ def to_model_revision_creator(self) -> ModelRevisionCreator: extra_mounts = [ MountInfo( vfolder_id=UUID(str(extra_mount.vfolder_id)), - kernel_path=PurePosixPath(extra_mount.mount_destination), + kernel_path=PurePosixPath(extra_mount.mount_destination or ""), ) for extra_mount in self.extra_mounts ] diff --git a/src/ai/backend/manager/api/rest/deployment/adapter.py b/src/ai/backend/manager/api/rest/deployment/adapter.py index dd23b9093ed..33155cd7045 100644 --- a/src/ai/backend/manager/api/rest/deployment/adapter.py +++ b/src/ai/backend/manager/api/rest/deployment/adapter.py @@ -65,7 +65,6 @@ DeploymentPolicyData, ExecutionSpec, ModelDeploymentData, - ModelMountConfigData, ModelRevisionData, MountInfo, ReplicaSpec, @@ -79,7 +78,6 @@ RouteTrafficStatus as ManagerRouteTrafficStatus, ) from ai.backend.manager.errors.api import InvalidAPIParameters -from ai.backend.manager.errors.deployment import IncompleteRevisionData from ai.backend.manager.models.deployment_policy import BlueGreenSpec, RollingUpdateSpec from ai.backend.manager.repositories.base import ( BatchQuerier, @@ -226,21 +224,15 @@ def convert_to_dto(self, data: ModelRevisionData) -> RevisionDTO: model_runtime_config=ModelRuntimeConfigDTO( runtime_variant=data.model_runtime_config.runtime_variant, ), - model_mount_config=self._convert_model_mount_config(data.model_mount_config), + model_mount_config=ModelMountConfigDTO( + vfolder_id=data.model_mount_config.vfolder_id, + mount_destination=data.model_mount_config.mount_destination, + definition_path=data.model_mount_config.definition_path, + ), created_at=data.created_at, image_id=data.image_id, ) - @staticmethod - def _convert_model_mount_config(config: ModelMountConfigData) -> ModelMountConfigDTO: - if config.vfolder_id is None: - raise IncompleteRevisionData("model_mount_config.vfolder_id is required but was None") - return ModelMountConfigDTO( - vfolder_id=config.vfolder_id, - mount_destination=config.mount_destination, - definition_path=config.definition_path, - ) - def build_querier(self, request: SearchRevisionsRequest) -> BatchQuerier: """ Build a BatchQuerier for revisions from search request. @@ -396,7 +388,7 @@ def build_revision_creator(revision_input: RevisionInput) -> ModelRevisionCreato extra_mounts = [ MountInfo( vfolder_id=mount.vfolder_id, - kernel_path=PurePosixPath(mount.mount_destination), + kernel_path=PurePosixPath(mount.mount_destination or ""), ) for mount in revision_input.extra_mounts ] diff --git a/src/ai/backend/manager/data/deployment/types.py b/src/ai/backend/manager/data/deployment/types.py index 0f28c3e1bf5..951fb728d6f 100644 --- a/src/ai/backend/manager/data/deployment/types.py +++ b/src/ai/backend/manager/data/deployment/types.py @@ -469,7 +469,7 @@ class ModelRuntimeConfigData: @dataclass class ModelMountConfigData: - vfolder_id: UUID | None + vfolder_id: UUID mount_destination: str definition_path: str diff --git a/src/ai/backend/manager/models/alembic/versions/c4602c3d4f1d_make_deployment_revision_model_not_null.py b/src/ai/backend/manager/models/alembic/versions/c4602c3d4f1d_make_deployment_revision_model_not_null.py new file mode 100644 index 00000000000..01e5ecaf1ef --- /dev/null +++ b/src/ai/backend/manager/models/alembic/versions/c4602c3d4f1d_make_deployment_revision_model_not_null.py @@ -0,0 +1,37 @@ +"""make_deployment_revision_model_not_null + +Revision ID: c4602c3d4f1d +Revises: 3f5c20f7bb07 +Create Date: 2026-03-05 08:08:11.357370 + +""" + +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision = "c4602c3d4f1d" +down_revision = "3f5c20f7bb07" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # Delete revisions with NULL model (vfolder_id) as they are invalid data. + # A deployment revision without a model vfolder reference cannot function. + op.execute("DELETE FROM deployment_revisions WHERE model IS NULL") + op.alter_column( + "deployment_revisions", + "model", + existing_type=sa.dialects.postgresql.UUID(), + nullable=False, + ) + + +def downgrade() -> None: + op.alter_column( + "deployment_revisions", + "model", + existing_type=sa.dialects.postgresql.UUID(), + nullable=True, + ) diff --git a/src/ai/backend/manager/models/deployment_revision/row.py b/src/ai/backend/manager/models/deployment_revision/row.py index d1c18f7d59c..8e6eb73dca3 100644 --- a/src/ai/backend/manager/models/deployment_revision/row.py +++ b/src/ai/backend/manager/models/deployment_revision/row.py @@ -91,7 +91,7 @@ class DeploymentRevisionRow(Base): # type: ignore[misc] image: Mapped[uuid.UUID] = mapped_column("image", GUID, nullable=False) # Model configuration - model: Mapped[uuid.UUID | None] = mapped_column("model", GUID, nullable=True) + model: Mapped[uuid.UUID] = mapped_column("model", GUID, nullable=False) model_mount_destination: Mapped[str] = mapped_column( "model_mount_destination", sa.String(length=1024), diff --git a/src/ai/backend/manager/repositories/deployment/creators/deployment.py b/src/ai/backend/manager/repositories/deployment/creators/deployment.py index b5bdcbc5707..d37d11384ea 100644 --- a/src/ai/backend/manager/repositories/deployment/creators/deployment.py +++ b/src/ai/backend/manager/repositories/deployment/creators/deployment.py @@ -83,7 +83,7 @@ class DeploymentMountFields: Corresponds to VFolderMountsCreator in data layer. """ - model_vfolder_id: uuid.UUID | None + model_vfolder_id: uuid.UUID model_mount_destination: str = "/models" model_definition_path: str | None = None extra_mounts: Sequence[VFolderMount] = () diff --git a/src/ai/backend/manager/repositories/deployment/creators/endpoint.py b/src/ai/backend/manager/repositories/deployment/creators/endpoint.py index d8d121ccfae..04f2cbe5cbd 100644 --- a/src/ai/backend/manager/repositories/deployment/creators/endpoint.py +++ b/src/ai/backend/manager/repositories/deployment/creators/endpoint.py @@ -59,7 +59,7 @@ class LegacyEndpointCreatorSpec(CreatorSpec[EndpointRow]): resource_opts: Mapping[str, Any] | None # Model revision fields - mounts (from MountMetadata) - model: UUID | None + model: UUID model_mount_destination: str model_definition_path: str | None extra_mounts: Sequence[VFolderMount] diff --git a/src/ai/backend/manager/repositories/deployment/creators/revision.py b/src/ai/backend/manager/repositories/deployment/creators/revision.py index 9405e531621..87e50a4f506 100644 --- a/src/ai/backend/manager/repositories/deployment/creators/revision.py +++ b/src/ai/backend/manager/repositories/deployment/creators/revision.py @@ -33,7 +33,7 @@ class DeploymentRevisionCreatorSpec(CreatorSpec[DeploymentRevisionRow]): resource_opts: Mapping[str, Any] cluster_mode: str cluster_size: int - model_id: uuid.UUID | None + model_id: uuid.UUID model_mount_destination: str model_definition_path: str | None model_definition: Mapping[str, Any] | None From 330c8390e06728de8838a73d8e49b332a97ff0b2 Mon Sep 17 00:00:00 2001 From: jopemachine Date: Thu, 5 Mar 2026 08:14:50 +0000 Subject: [PATCH 04/12] chore: update api schema dump Co-authored-by: octodog --- docs/manager/graphql-reference/supergraph.graphql | 2 +- docs/manager/graphql-reference/v2-schema.graphql | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/manager/graphql-reference/supergraph.graphql b/docs/manager/graphql-reference/supergraph.graphql index 593a9f1014f..ff61daf3b17 100644 --- a/docs/manager/graphql-reference/supergraph.graphql +++ b/docs/manager/graphql-reference/supergraph.graphql @@ -4412,7 +4412,7 @@ input ExtraVFolderMountInput @join__type(graph: STRAWBERRY) { vfolderId: ID! - mountDestination: String! + mountDestination: String } """ diff --git a/docs/manager/graphql-reference/v2-schema.graphql b/docs/manager/graphql-reference/v2-schema.graphql index af3d0f8ba55..e98175038c1 100644 --- a/docs/manager/graphql-reference/v2-schema.graphql +++ b/docs/manager/graphql-reference/v2-schema.graphql @@ -2452,7 +2452,7 @@ type ExtraVFolderMountEdge { """Added in 25.19.0""" input ExtraVFolderMountInput { vfolderId: ID! - mountDestination: String! + mountDestination: String } """ From c0007c55c0b2958c56602c531b1d3d6e20de27ca Mon Sep 17 00:00:00 2001 From: jopemachine Date: Thu, 5 Mar 2026 08:30:13 +0000 Subject: [PATCH 05/12] WIP --- .../api/gql/deployment/types/revision.py | 4 +- .../manager/api/rest/deployment/adapter.py | 18 ++++++--- .../backend/manager/data/deployment/types.py | 2 +- ...make_deployment_revision_model_not_null.py | 37 ------------------- .../manager/models/deployment_revision/row.py | 2 +- .../deployment/creators/deployment.py | 2 +- .../deployment/creators/endpoint.py | 2 +- .../deployment/creators/revision.py | 2 +- 8 files changed, 21 insertions(+), 48 deletions(-) delete mode 100644 src/ai/backend/manager/models/alembic/versions/c4602c3d4f1d_make_deployment_revision_model_not_null.py diff --git a/src/ai/backend/manager/api/gql/deployment/types/revision.py b/src/ai/backend/manager/api/gql/deployment/types/revision.py index 86f2dd68f13..bf9bf4702a0 100644 --- a/src/ai/backend/manager/api/gql/deployment/types/revision.py +++ b/src/ai/backend/manager/api/gql/deployment/types/revision.py @@ -114,7 +114,9 @@ async def vfolder(self, info: Info[StrawberryGQLContext]) -> VFolder: return VFolder(id=ID(vfolder_global_id)) @classmethod - def from_dataclass(cls, data: ModelMountConfigData) -> ModelMountConfig: + def from_dataclass(cls, data: ModelMountConfigData) -> ModelMountConfig | None: + if data.vfolder_id is None: + return None return cls( _vfolder_id=data.vfolder_id, mount_destination=data.mount_destination, diff --git a/src/ai/backend/manager/api/rest/deployment/adapter.py b/src/ai/backend/manager/api/rest/deployment/adapter.py index 33155cd7045..d03fc25bf46 100644 --- a/src/ai/backend/manager/api/rest/deployment/adapter.py +++ b/src/ai/backend/manager/api/rest/deployment/adapter.py @@ -65,6 +65,7 @@ DeploymentPolicyData, ExecutionSpec, ModelDeploymentData, + ModelMountConfigData, ModelRevisionData, MountInfo, ReplicaSpec, @@ -78,6 +79,7 @@ RouteTrafficStatus as ManagerRouteTrafficStatus, ) from ai.backend.manager.errors.api import InvalidAPIParameters +from ai.backend.manager.errors.deployment import IncompleteRevisionData from ai.backend.manager.models.deployment_policy import BlueGreenSpec, RollingUpdateSpec from ai.backend.manager.repositories.base import ( BatchQuerier, @@ -224,15 +226,21 @@ def convert_to_dto(self, data: ModelRevisionData) -> RevisionDTO: model_runtime_config=ModelRuntimeConfigDTO( runtime_variant=data.model_runtime_config.runtime_variant, ), - model_mount_config=ModelMountConfigDTO( - vfolder_id=data.model_mount_config.vfolder_id, - mount_destination=data.model_mount_config.mount_destination, - definition_path=data.model_mount_config.definition_path, - ), + model_mount_config=self._convert_model_mount_config(data.model_mount_config), created_at=data.created_at, image_id=data.image_id, ) + @staticmethod + def _convert_model_mount_config(config: ModelMountConfigData) -> ModelMountConfigDTO: + if config.vfolder_id is None: + raise IncompleteRevisionData("model_mount_config.vfolder_id is required but was None") + return ModelMountConfigDTO( + vfolder_id=config.vfolder_id, + mount_destination=config.mount_destination, + definition_path=config.definition_path, + ) + def build_querier(self, request: SearchRevisionsRequest) -> BatchQuerier: """ Build a BatchQuerier for revisions from search request. diff --git a/src/ai/backend/manager/data/deployment/types.py b/src/ai/backend/manager/data/deployment/types.py index 951fb728d6f..0f28c3e1bf5 100644 --- a/src/ai/backend/manager/data/deployment/types.py +++ b/src/ai/backend/manager/data/deployment/types.py @@ -469,7 +469,7 @@ class ModelRuntimeConfigData: @dataclass class ModelMountConfigData: - vfolder_id: UUID + vfolder_id: UUID | None mount_destination: str definition_path: str diff --git a/src/ai/backend/manager/models/alembic/versions/c4602c3d4f1d_make_deployment_revision_model_not_null.py b/src/ai/backend/manager/models/alembic/versions/c4602c3d4f1d_make_deployment_revision_model_not_null.py deleted file mode 100644 index 01e5ecaf1ef..00000000000 --- a/src/ai/backend/manager/models/alembic/versions/c4602c3d4f1d_make_deployment_revision_model_not_null.py +++ /dev/null @@ -1,37 +0,0 @@ -"""make_deployment_revision_model_not_null - -Revision ID: c4602c3d4f1d -Revises: 3f5c20f7bb07 -Create Date: 2026-03-05 08:08:11.357370 - -""" - -import sqlalchemy as sa -from alembic import op - -# revision identifiers, used by Alembic. -revision = "c4602c3d4f1d" -down_revision = "3f5c20f7bb07" -branch_labels = None -depends_on = None - - -def upgrade() -> None: - # Delete revisions with NULL model (vfolder_id) as they are invalid data. - # A deployment revision without a model vfolder reference cannot function. - op.execute("DELETE FROM deployment_revisions WHERE model IS NULL") - op.alter_column( - "deployment_revisions", - "model", - existing_type=sa.dialects.postgresql.UUID(), - nullable=False, - ) - - -def downgrade() -> None: - op.alter_column( - "deployment_revisions", - "model", - existing_type=sa.dialects.postgresql.UUID(), - nullable=True, - ) diff --git a/src/ai/backend/manager/models/deployment_revision/row.py b/src/ai/backend/manager/models/deployment_revision/row.py index 8e6eb73dca3..d1c18f7d59c 100644 --- a/src/ai/backend/manager/models/deployment_revision/row.py +++ b/src/ai/backend/manager/models/deployment_revision/row.py @@ -91,7 +91,7 @@ class DeploymentRevisionRow(Base): # type: ignore[misc] image: Mapped[uuid.UUID] = mapped_column("image", GUID, nullable=False) # Model configuration - model: Mapped[uuid.UUID] = mapped_column("model", GUID, nullable=False) + model: Mapped[uuid.UUID | None] = mapped_column("model", GUID, nullable=True) model_mount_destination: Mapped[str] = mapped_column( "model_mount_destination", sa.String(length=1024), diff --git a/src/ai/backend/manager/repositories/deployment/creators/deployment.py b/src/ai/backend/manager/repositories/deployment/creators/deployment.py index d37d11384ea..b5bdcbc5707 100644 --- a/src/ai/backend/manager/repositories/deployment/creators/deployment.py +++ b/src/ai/backend/manager/repositories/deployment/creators/deployment.py @@ -83,7 +83,7 @@ class DeploymentMountFields: Corresponds to VFolderMountsCreator in data layer. """ - model_vfolder_id: uuid.UUID + model_vfolder_id: uuid.UUID | None model_mount_destination: str = "/models" model_definition_path: str | None = None extra_mounts: Sequence[VFolderMount] = () diff --git a/src/ai/backend/manager/repositories/deployment/creators/endpoint.py b/src/ai/backend/manager/repositories/deployment/creators/endpoint.py index 04f2cbe5cbd..d8d121ccfae 100644 --- a/src/ai/backend/manager/repositories/deployment/creators/endpoint.py +++ b/src/ai/backend/manager/repositories/deployment/creators/endpoint.py @@ -59,7 +59,7 @@ class LegacyEndpointCreatorSpec(CreatorSpec[EndpointRow]): resource_opts: Mapping[str, Any] | None # Model revision fields - mounts (from MountMetadata) - model: UUID + model: UUID | None model_mount_destination: str model_definition_path: str | None extra_mounts: Sequence[VFolderMount] diff --git a/src/ai/backend/manager/repositories/deployment/creators/revision.py b/src/ai/backend/manager/repositories/deployment/creators/revision.py index 87e50a4f506..9405e531621 100644 --- a/src/ai/backend/manager/repositories/deployment/creators/revision.py +++ b/src/ai/backend/manager/repositories/deployment/creators/revision.py @@ -33,7 +33,7 @@ class DeploymentRevisionCreatorSpec(CreatorSpec[DeploymentRevisionRow]): resource_opts: Mapping[str, Any] cluster_mode: str cluster_size: int - model_id: uuid.UUID + model_id: uuid.UUID | None model_mount_destination: str model_definition_path: str | None model_definition: Mapping[str, Any] | None From cde6ee05dd76e4cf61d073210bde6dbdf03fdabd Mon Sep 17 00:00:00 2001 From: jopemachine Date: Fri, 6 Mar 2026 01:50:19 +0000 Subject: [PATCH 06/12] WIP --- .../api/gql/deployment/types/revision.py | 2 +- .../manager/api/rest/deployment/adapter.py | 20 ++++++++----------- .../backend/manager/data/deployment/types.py | 2 +- 3 files changed, 10 insertions(+), 14 deletions(-) diff --git a/src/ai/backend/manager/api/gql/deployment/types/revision.py b/src/ai/backend/manager/api/gql/deployment/types/revision.py index bf9bf4702a0..656eb1b50e6 100644 --- a/src/ai/backend/manager/api/gql/deployment/types/revision.py +++ b/src/ai/backend/manager/api/gql/deployment/types/revision.py @@ -115,7 +115,7 @@ async def vfolder(self, info: Info[StrawberryGQLContext]) -> VFolder: @classmethod def from_dataclass(cls, data: ModelMountConfigData) -> ModelMountConfig | None: - if data.vfolder_id is None: + if data.vfolder_id is None or data.mount_destination is None: return None return cls( _vfolder_id=data.vfolder_id, diff --git a/src/ai/backend/manager/api/rest/deployment/adapter.py b/src/ai/backend/manager/api/rest/deployment/adapter.py index d03fc25bf46..4c11a3e15ab 100644 --- a/src/ai/backend/manager/api/rest/deployment/adapter.py +++ b/src/ai/backend/manager/api/rest/deployment/adapter.py @@ -65,7 +65,6 @@ DeploymentPolicyData, ExecutionSpec, ModelDeploymentData, - ModelMountConfigData, ModelRevisionData, MountInfo, ReplicaSpec, @@ -212,6 +211,9 @@ class RevisionAdapter(BaseFilterAdapter): def convert_to_dto(self, data: ModelRevisionData) -> RevisionDTO: """Convert ModelRevisionData to DTO.""" + mount_config = data.model_mount_config + if mount_config.vfolder_id is None or mount_config.mount_destination is None: + raise IncompleteRevisionData(f"Revision {data.id} has incomplete model mount config") return RevisionDTO( id=data.id, name=data.name, @@ -226,21 +228,15 @@ def convert_to_dto(self, data: ModelRevisionData) -> RevisionDTO: model_runtime_config=ModelRuntimeConfigDTO( runtime_variant=data.model_runtime_config.runtime_variant, ), - model_mount_config=self._convert_model_mount_config(data.model_mount_config), + model_mount_config=ModelMountConfigDTO( + vfolder_id=mount_config.vfolder_id, + mount_destination=mount_config.mount_destination, + definition_path=mount_config.definition_path, + ), created_at=data.created_at, image_id=data.image_id, ) - @staticmethod - def _convert_model_mount_config(config: ModelMountConfigData) -> ModelMountConfigDTO: - if config.vfolder_id is None: - raise IncompleteRevisionData("model_mount_config.vfolder_id is required but was None") - return ModelMountConfigDTO( - vfolder_id=config.vfolder_id, - mount_destination=config.mount_destination, - definition_path=config.definition_path, - ) - def build_querier(self, request: SearchRevisionsRequest) -> BatchQuerier: """ Build a BatchQuerier for revisions from search request. diff --git a/src/ai/backend/manager/data/deployment/types.py b/src/ai/backend/manager/data/deployment/types.py index 0f28c3e1bf5..74bcbd68d38 100644 --- a/src/ai/backend/manager/data/deployment/types.py +++ b/src/ai/backend/manager/data/deployment/types.py @@ -470,7 +470,7 @@ class ModelRuntimeConfigData: @dataclass class ModelMountConfigData: vfolder_id: UUID | None - mount_destination: str + mount_destination: str | None definition_path: str From 73bfe573e6852c0d164afef5220a0285974298b1 Mon Sep 17 00:00:00 2001 From: jopemachine Date: Fri, 6 Mar 2026 02:01:29 +0000 Subject: [PATCH 07/12] docs: Add news fragment --- changes/9673.fix.md | 1 + src/ai/backend/manager/api/rest/deployment/adapter.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 changes/9673.fix.md diff --git a/changes/9673.fix.md b/changes/9673.fix.md new file mode 100644 index 00000000000..9df5518cdf5 --- /dev/null +++ b/changes/9673.fix.md @@ -0,0 +1 @@ +Incorrect types and fallback defaults in deployment conversion logic diff --git a/src/ai/backend/manager/api/rest/deployment/adapter.py b/src/ai/backend/manager/api/rest/deployment/adapter.py index 4c11a3e15ab..5333de9b016 100644 --- a/src/ai/backend/manager/api/rest/deployment/adapter.py +++ b/src/ai/backend/manager/api/rest/deployment/adapter.py @@ -212,7 +212,7 @@ class RevisionAdapter(BaseFilterAdapter): def convert_to_dto(self, data: ModelRevisionData) -> RevisionDTO: """Convert ModelRevisionData to DTO.""" mount_config = data.model_mount_config - if mount_config.vfolder_id is None or mount_config.mount_destination is None: + if mount_config.vfolder_id is None: raise IncompleteRevisionData(f"Revision {data.id} has incomplete model mount config") return RevisionDTO( id=data.id, @@ -230,7 +230,7 @@ def convert_to_dto(self, data: ModelRevisionData) -> RevisionDTO: ), model_mount_config=ModelMountConfigDTO( vfolder_id=mount_config.vfolder_id, - mount_destination=mount_config.mount_destination, + mount_destination=mount_config.mount_destination or "", definition_path=mount_config.definition_path, ), created_at=data.created_at, From 05dd450d182468f01a7a6303848f3b87dacaa936 Mon Sep 17 00:00:00 2001 From: jopemachine Date: Fri, 6 Mar 2026 03:57:19 +0000 Subject: [PATCH 08/12] WIP --- .../backend/manager/api/gql/deployment/types/revision.py | 8 ++++++-- src/ai/backend/manager/api/rest/deployment/adapter.py | 4 +++- src/ai/backend/manager/data/deployment/types.py | 2 +- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/ai/backend/manager/api/gql/deployment/types/revision.py b/src/ai/backend/manager/api/gql/deployment/types/revision.py index 656eb1b50e6..2cea0dd01c3 100644 --- a/src/ai/backend/manager/api/gql/deployment/types/revision.py +++ b/src/ai/backend/manager/api/gql/deployment/types/revision.py @@ -515,7 +515,9 @@ def to_model_revision_creator(self) -> ModelRevisionCreator: extra_mounts = [ MountInfo( vfolder_id=UUID(str(extra_mount.vfolder_id)), - kernel_path=PurePosixPath(extra_mount.mount_destination or ""), + kernel_path=PurePosixPath(extra_mount.mount_destination) + if extra_mount.mount_destination + else None, ) for extra_mount in self.extra_mounts ] @@ -570,7 +572,9 @@ def to_model_revision_creator(self) -> ModelRevisionCreator: extra_mounts = [ MountInfo( vfolder_id=UUID(str(extra_mount.vfolder_id)), - kernel_path=PurePosixPath(extra_mount.mount_destination or ""), + kernel_path=PurePosixPath(extra_mount.mount_destination) + if extra_mount.mount_destination + else None, ) for extra_mount in self.extra_mounts ] diff --git a/src/ai/backend/manager/api/rest/deployment/adapter.py b/src/ai/backend/manager/api/rest/deployment/adapter.py index 5333de9b016..8ab85c2f54b 100644 --- a/src/ai/backend/manager/api/rest/deployment/adapter.py +++ b/src/ai/backend/manager/api/rest/deployment/adapter.py @@ -392,7 +392,9 @@ def build_revision_creator(revision_input: RevisionInput) -> ModelRevisionCreato extra_mounts = [ MountInfo( vfolder_id=mount.vfolder_id, - kernel_path=PurePosixPath(mount.mount_destination or ""), + kernel_path=PurePosixPath(mount.mount_destination) + if mount.mount_destination + else None, ) for mount in revision_input.extra_mounts ] diff --git a/src/ai/backend/manager/data/deployment/types.py b/src/ai/backend/manager/data/deployment/types.py index 74bcbd68d38..c90949c83cb 100644 --- a/src/ai/backend/manager/data/deployment/types.py +++ b/src/ai/backend/manager/data/deployment/types.py @@ -244,7 +244,7 @@ class MountSpec: @dataclass class MountInfo: vfolder_id: UUID - kernel_path: PurePosixPath + kernel_path: PurePosixPath | None = None @dataclass From be99ddb662210b82b8ef0bda69c026c46349693b Mon Sep 17 00:00:00 2001 From: jopemachine Date: Fri, 6 Mar 2026 04:02:08 +0000 Subject: [PATCH 09/12] WIP --- src/ai/backend/common/dto/manager/deployment/response.py | 2 +- src/ai/backend/manager/api/rest/deployment/adapter.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ai/backend/common/dto/manager/deployment/response.py b/src/ai/backend/common/dto/manager/deployment/response.py index b25e8082fa3..4302718182e 100644 --- a/src/ai/backend/common/dto/manager/deployment/response.py +++ b/src/ai/backend/common/dto/manager/deployment/response.py @@ -94,7 +94,7 @@ class ModelMountConfigDTO(BaseModel): """Model mount configuration for revision.""" vfolder_id: UUID = Field(description="VFolder ID for model") - mount_destination: str = Field(description="Mount destination path") + mount_destination: str | None = Field(description="Mount destination path") definition_path: str = Field(description="Model definition path") diff --git a/src/ai/backend/manager/api/rest/deployment/adapter.py b/src/ai/backend/manager/api/rest/deployment/adapter.py index 8ab85c2f54b..b485a631bab 100644 --- a/src/ai/backend/manager/api/rest/deployment/adapter.py +++ b/src/ai/backend/manager/api/rest/deployment/adapter.py @@ -230,7 +230,7 @@ def convert_to_dto(self, data: ModelRevisionData) -> RevisionDTO: ), model_mount_config=ModelMountConfigDTO( vfolder_id=mount_config.vfolder_id, - mount_destination=mount_config.mount_destination or "", + mount_destination=mount_config.mount_destination, definition_path=mount_config.definition_path, ), created_at=data.created_at, From 43b26e378110b66b25b729382995060fa6e2012f Mon Sep 17 00:00:00 2001 From: jopemachine Date: Fri, 6 Mar 2026 04:07:41 +0000 Subject: [PATCH 10/12] fix: Specify type --- src/ai/backend/manager/api/rest/deployment/adapter.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ai/backend/manager/api/rest/deployment/adapter.py b/src/ai/backend/manager/api/rest/deployment/adapter.py index b485a631bab..7c2bc1e30cf 100644 --- a/src/ai/backend/manager/api/rest/deployment/adapter.py +++ b/src/ai/backend/manager/api/rest/deployment/adapter.py @@ -8,7 +8,6 @@ from datetime import UTC, datetime from pathlib import PurePosixPath -from typing import Any from uuid import UUID, uuid4 from ai.backend.common.data.model_deployment.types import DeploymentStrategy @@ -26,6 +25,7 @@ DeploymentFilter, DeploymentOrder, DeploymentPolicyDTO, + DeploymentStrategyInput, ModelMountConfigDTO, ModelRuntimeConfigDTO, NetworkConfigDTO, @@ -497,7 +497,7 @@ def build_creator( def _build_policy_config( self, - strategy_input: Any, # DeploymentStrategyInput + strategy_input: DeploymentStrategyInput, ) -> DeploymentPolicyConfig: """Build DeploymentPolicyConfig from strategy input.""" strategy = DeploymentStrategy(strategy_input.type) From 22eb8c4b71eb3effa3c8df3e72af046dea6ec5f2 Mon Sep 17 00:00:00 2001 From: jopemachine Date: Fri, 6 Mar 2026 04:15:19 +0000 Subject: [PATCH 11/12] fix: Migrate RoutingRow.created_at to non-nullble --- .../manager/api/rest/deployment/adapter.py | 3 +- .../backend/manager/data/deployment/types.py | 2 +- .../manager/data/model_serving/types.py | 2 +- ...5_make_routings_created_at_non_nullable.py | 38 +++++++++++++++++++ src/ai/backend/manager/models/routing/row.py | 4 +- .../deployment/db_source/db_source.py | 6 +-- .../manager/services/deployment/service.py | 2 +- 7 files changed, 47 insertions(+), 10 deletions(-) create mode 100644 src/ai/backend/manager/models/alembic/versions/b1009fe7f865_make_routings_created_at_non_nullable.py diff --git a/src/ai/backend/manager/api/rest/deployment/adapter.py b/src/ai/backend/manager/api/rest/deployment/adapter.py index 7c2bc1e30cf..24cdaf58330 100644 --- a/src/ai/backend/manager/api/rest/deployment/adapter.py +++ b/src/ai/backend/manager/api/rest/deployment/adapter.py @@ -6,7 +6,6 @@ from __future__ import annotations -from datetime import UTC, datetime from pathlib import PurePosixPath from uuid import UUID, uuid4 @@ -301,7 +300,7 @@ def convert_to_dto(self, data: RouteInfo) -> RouteDTO: session_id=str(data.session_id) if data.session_id else None, status=CommonRouteStatus(data.status.value), traffic_ratio=data.traffic_ratio, - created_at=data.created_at or datetime.now(tz=UTC), + created_at=data.created_at, revision_id=data.revision_id, traffic_status=CommonRouteTrafficStatus(data.traffic_status.value), error_data=data.error_data, diff --git a/src/ai/backend/manager/data/deployment/types.py b/src/ai/backend/manager/data/deployment/types.py index c90949c83cb..a121db9d2a2 100644 --- a/src/ai/backend/manager/data/deployment/types.py +++ b/src/ai/backend/manager/data/deployment/types.py @@ -388,7 +388,7 @@ class RouteInfo: session_id: SessionId | None status: RouteStatus traffic_ratio: float - created_at: datetime | None + created_at: datetime revision_id: UUID | None traffic_status: RouteTrafficStatus error_data: dict[str, Any] = field(default_factory=dict) diff --git a/src/ai/backend/manager/data/model_serving/types.py b/src/ai/backend/manager/data/model_serving/types.py index 10047e4f50b..016df4e1c87 100644 --- a/src/ai/backend/manager/data/model_serving/types.py +++ b/src/ai/backend/manager/data/model_serving/types.py @@ -93,7 +93,7 @@ class RoutingData: session: uuid.UUID | None status: RouteStatus traffic_ratio: float - created_at: datetime | None + created_at: datetime error_data: dict[str, Any] diff --git a/src/ai/backend/manager/models/alembic/versions/b1009fe7f865_make_routings_created_at_non_nullable.py b/src/ai/backend/manager/models/alembic/versions/b1009fe7f865_make_routings_created_at_non_nullable.py new file mode 100644 index 00000000000..a3f8dc5fccb --- /dev/null +++ b/src/ai/backend/manager/models/alembic/versions/b1009fe7f865_make_routings_created_at_non_nullable.py @@ -0,0 +1,38 @@ +"""Make routings.created_at non-nullable + +Revision ID: b1009fe7f865 +Revises: 3f5c20f7bb07 +Create Date: 2026-03-06 04:11:09.336691 + +""" + +from alembic import op +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = "b1009fe7f865" +down_revision = "3f5c20f7bb07" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # Fill any existing NULLs with now() before adding NOT NULL constraint + op.execute("UPDATE routings SET created_at = now() WHERE created_at IS NULL") + op.alter_column( + "routings", + "created_at", + existing_type=postgresql.TIMESTAMP(timezone=True), + nullable=False, + existing_server_default="now()", + ) + + +def downgrade() -> None: + op.alter_column( + "routings", + "created_at", + existing_type=postgresql.TIMESTAMP(timezone=True), + nullable=True, + existing_server_default="now()", + ) diff --git a/src/ai/backend/manager/models/routing/row.py b/src/ai/backend/manager/models/routing/row.py index 51a9d9c1f9f..d5db0d4a7bc 100644 --- a/src/ai/backend/manager/models/routing/row.py +++ b/src/ai/backend/manager/models/routing/row.py @@ -82,11 +82,11 @@ class RoutingRow(Base): # type: ignore[misc] ) weight: Mapped[int | None] = mapped_column("weight", sa.Integer(), nullable=True, default=None) traffic_ratio: Mapped[float] = mapped_column("traffic_ratio", sa.Float(), nullable=False) - created_at: Mapped[datetime | None] = mapped_column( + created_at: Mapped[datetime] = mapped_column( "created_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), - nullable=True, + nullable=False, ) error_data: Mapped[dict[str, Any] | None] = mapped_column( diff --git a/src/ai/backend/manager/repositories/deployment/db_source/db_source.py b/src/ai/backend/manager/repositories/deployment/db_source/db_source.py index b2499f1106a..df2be03caa4 100644 --- a/src/ai/backend/manager/repositories/deployment/db_source/db_source.py +++ b/src/ai/backend/manager/repositories/deployment/db_source/db_source.py @@ -5,7 +5,7 @@ from collections.abc import AsyncIterator, Mapping, Sequence from contextlib import asynccontextmanager as actxmgr from dataclasses import dataclass -from datetime import UTC, datetime +from datetime import datetime from typing import Any, cast import sqlalchemy as sa @@ -882,7 +882,7 @@ async def get_routes_by_endpoint( session_id=SessionId(row.session) if row.session else None, status=row.status, traffic_ratio=row.traffic_ratio, - created_at=row.created_at or datetime.now(tz=UTC), + created_at=row.created_at, error_data=row.error_data or {}, ) for row in rows @@ -1447,7 +1447,7 @@ async def get_routes_by_statuses( session_id=SessionId(row.session) if row.session else None, status=row.status, traffic_ratio=row.traffic_ratio, - created_at=row.created_at or datetime.now(tz=UTC), + created_at=row.created_at, error_data=row.error_data or {}, ) route_data_list.append(route_data) diff --git a/src/ai/backend/manager/services/deployment/service.py b/src/ai/backend/manager/services/deployment/service.py index d37c107fd5b..4304afcc7d0 100644 --- a/src/ai/backend/manager/services/deployment/service.py +++ b/src/ai/backend/manager/services/deployment/service.py @@ -265,7 +265,7 @@ def _convert_route_info_to_replica_data(route: RouteInfo) -> ModelReplicaData: else ActivenessStatus.INACTIVE, weight=int(route.traffic_ratio * 100), # Convert ratio to weight detail=route.error_data, - created_at=route.created_at or datetime.now(tz=UTC), + created_at=route.created_at, ) From 70993ed592896b39463641e14828435702f19f98 Mon Sep 17 00:00:00 2001 From: jopemachine Date: Fri, 6 Mar 2026 04:21:04 +0000 Subject: [PATCH 12/12] fix: Alembic script --- .../b1009fe7f865_make_routings_created_at_non_nullable.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ai/backend/manager/models/alembic/versions/b1009fe7f865_make_routings_created_at_non_nullable.py b/src/ai/backend/manager/models/alembic/versions/b1009fe7f865_make_routings_created_at_non_nullable.py index a3f8dc5fccb..e4380a99aae 100644 --- a/src/ai/backend/manager/models/alembic/versions/b1009fe7f865_make_routings_created_at_non_nullable.py +++ b/src/ai/backend/manager/models/alembic/versions/b1009fe7f865_make_routings_created_at_non_nullable.py @@ -1,7 +1,7 @@ """Make routings.created_at non-nullable Revision ID: b1009fe7f865 -Revises: 3f5c20f7bb07 +Revises: 0b1efbb2db84 Create Date: 2026-03-06 04:11:09.336691 """ @@ -11,7 +11,7 @@ # revision identifiers, used by Alembic. revision = "b1009fe7f865" -down_revision = "3f5c20f7bb07" +down_revision = "0b1efbb2db84" branch_labels = None depends_on = None