Skip to content

Commit f1a770c

Browse files
jopemachineclaude
andauthored
refactor(BA-6040): unify per-mount option DTOs and expose subpath in CreationConfigV7 (#11608)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 957c68b commit f1a770c

10 files changed

Lines changed: 54 additions & 70 deletions

File tree

changes/11608.enhance.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Unify per-vfolder mount-option DTOs into a single `MountOption` type and formally declare `subpath` / `mount_destination` on the session-creation wire schema (`CreationConfigV*.mount_options`). The previously separate SDK `ExtraMountOption` and `ExtraMountModel` types are removed; both session creation and inference service creation now share the same `MountOption`.

src/ai/backend/client/cli/service.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,12 @@
1414
)
1515
from ai.backend.client.compat import asyncio_run
1616
from ai.backend.client.exceptions import BackendError
17-
from ai.backend.client.func.service import ExtraMountOption
1817
from ai.backend.client.output.fields import routing_fields, service_fields
1918
from ai.backend.client.output.types import FieldSpec
2019
from ai.backend.client.session import AsyncSession, Session
2120
from ai.backend.common.arch import DEFAULT_IMAGE_ARCH
2221
from ai.backend.common.bgtask.types import BgtaskStatus
22+
from ai.backend.common.dto.manager.session.types import MountOption
2323
from ai.backend.common.types import ClusterMode, RuntimeVariant
2424

2525
from .extensions import pass_ctx_obj
@@ -326,7 +326,7 @@ def create(
326326
parsed_resources = prepare_resource_arg(resources)
327327
parsed_resource_opts = prepare_resource_arg(resource_opts)
328328
extra_mount_options = {
329-
key: ExtraMountOption.model_validate(opts) for key, opts in mount_options.items()
329+
key: MountOption.model_validate(opts) for key, opts in mount_options.items()
330330
}
331331
body = {
332332
"service_name": name,

src/ai/backend/client/cli/session/execute.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
if TYPE_CHECKING:
4242
from ai.backend.client.func.session import ComputeSession
4343
from ai.backend.common.arch import DEFAULT_IMAGE_ARCH
44+
from ai.backend.common.dto.manager.session.types import MountOption
4445
from ai.backend.common.types import ClusterMode, MountExpression
4546

4647
from .args import click_start_option
@@ -474,7 +475,10 @@ def run(
474475
envs = prepare_env_arg(env)
475476
resources = prepare_resource_arg(resources)
476477
resource_opts = prepare_resource_arg(resource_opts)
477-
mount, mount_map, mount_options = prepare_mount_arg(mount, escape=True)
478+
mount, mount_map, raw_mount_options = prepare_mount_arg(mount, escape=True)
479+
mount_options = {
480+
key: MountOption.model_validate(opts) for key, opts in raw_mount_options.items()
481+
}
478482

479483
env_ranges: dict[str, Any] = {v: r for v, r in env_range} # type: ignore[has-type]
480484
build_ranges: dict[str, Any] = {v: r for v, r in build_range} # type: ignore[has-type]

src/ai/backend/client/cli/session/lifecycle.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
from ai.backend.client.session import AsyncSession, Session
4444
from ai.backend.common.arch import DEFAULT_IMAGE_ARCH
4545
from ai.backend.common.bgtask.types import BgtaskStatus
46+
from ai.backend.common.dto.manager.session.types import MountOption
4647
from ai.backend.common.types import ClusterMode
4748

4849
from .args import click_start_option
@@ -196,7 +197,10 @@ def create(
196197
envs = prepare_env_arg(env)
197198
parsed_resources = prepare_resource_arg(resources)
198199
parsed_resource_opts = prepare_resource_arg(resource_opts)
199-
mount, mount_map, mount_options = prepare_mount_arg(mount, escape=True)
200+
mount, mount_map, raw_mount_options = prepare_mount_arg(mount, escape=True)
201+
mount_options = {
202+
key: MountOption.model_validate(opts) for key, opts in raw_mount_options.items()
203+
}
200204

201205
preopen_ports = preopen
202206
assigned_agent_list = assign_agent

src/ai/backend/client/func/service.py

Lines changed: 5 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -13,31 +13,13 @@
1313
from ai.backend.client.session import api_session
1414
from ai.backend.client.utils import dedent as _d
1515
from ai.backend.common.arch import DEFAULT_IMAGE_ARCH
16+
from ai.backend.common.dto.manager.session.types import MountOption
1617
from ai.backend.common.typed_validators import SESSION_NAME_MAX_LENGTH
17-
from ai.backend.common.types import BackendAISchema, RuntimeVariant
18+
from ai.backend.common.types import RuntimeVariant
1819

1920
from .base import BaseFunction, api_function
2021

21-
__all__ = (
22-
"ExtraMountOption",
23-
"Service",
24-
)
25-
26-
27-
class ExtraMountOption(BackendAISchema):
28-
"""Per-vfolder option overrides for ``Service.create`` extra mounts.
29-
30-
Keys in the parent mapping must match the entries passed to
31-
``extra_mounts`` (either a vfolder UUID string or a vfolder name).
32-
33-
Note: ``mount_destination`` lives on the separate ``extra_mount_map``
34-
parameter, not here — keeping the destination override outside this
35-
model leaves a single source of truth on the SDK surface.
36-
"""
37-
38-
type: str | None = None
39-
permission: str | None = None
40-
subpath: str | None = None
22+
__all__ = ("Service",)
4123

4224

4325
_default_fields: Sequence[FieldSpec] = (
@@ -122,7 +104,7 @@ async def create(
122104
scaling_group: str,
123105
extra_mounts: Sequence[str] | None = None,
124106
extra_mount_map: Mapping[str, str] | None = None,
125-
extra_mount_options: Mapping[str, ExtraMountOption] | None = None,
107+
extra_mount_options: Mapping[str, MountOption] | None = None,
126108
service_name: str | None = None,
127109
model_version: str | None = None,
128110
_dependencies: Sequence[str] | None = None,
@@ -201,7 +183,7 @@ async def create(
201183
if mount not in vfolder_name_to_id:
202184
raise BackendClientError(f"VFolder (name: {mount}) not found") from e
203185
vfolder_id = vfolder_name_to_id[mount]
204-
options = extra_mount_options.get(mount) or ExtraMountOption()
186+
options = extra_mount_options.get(mount) or MountOption()
205187
body = options.model_dump(exclude_none=True)
206188
if (dest := extra_mount_map.get(mount)) is not None:
207189
body["mount_destination"] = dest

src/ai/backend/client/func/session.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
from ai.backend.client.utils import dedent as _d
4545
from ai.backend.client.versioning import get_id_or_name, get_naming
4646
from ai.backend.common.arch import DEFAULT_IMAGE_ARCH
47+
from ai.backend.common.dto.manager.session.types import MountOption
4748
from ai.backend.common.types import ClusterMode, SessionTypes
4849

4950
from .base import BaseFunction, api_function
@@ -210,7 +211,7 @@ async def get_or_create(
210211
mount_map: Mapping[str, str] | None = None,
211212
mount_ids: list[UUID] | None = None,
212213
mount_id_map: Mapping[UUID, str] | None = None,
213-
mount_options: Mapping[str, Mapping[str, str]] | None = None,
214+
mount_options: Mapping[str, MountOption] | None = None,
214215
envs: Mapping[str, str] | None = None,
215216
startup_command: str | None = None,
216217
batch_timeout: str | int | None = None,
@@ -365,7 +366,9 @@ async def get_or_create(
365366
params["config"].update({
366367
"mount_map": mount_map,
367368
"mount_id_map": mount_id_map,
368-
"mount_options": mount_options,
369+
"mount_options": {
370+
k: v.model_dump(exclude_none=True) for k, v in mount_options.items()
371+
},
369372
"preopen_ports": preopen_ports,
370373
})
371374
if assign_agent is not None:
@@ -1421,7 +1424,7 @@ async def get_or_create(
14211424
callback_url: str | None = None,
14221425
mounts: list[str] | None = None,
14231426
mount_map: Mapping[str, str] | None = None,
1424-
mount_options: Mapping[str, Mapping[str, str]] | None = None,
1427+
mount_options: Mapping[str, MountOption] | None = None,
14251428
mount_ids: list[UUID] | None = None,
14261429
mount_id_map: Mapping[UUID, str] | None = None,
14271430
envs: Mapping[str, str] | None = None,

src/ai/backend/common/dto/manager/model_serving/request.py

Lines changed: 3 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@
2121
from ai.backend.common import typed_validators as tv
2222
from ai.backend.common.api_handlers import BaseRequestModel
2323
from ai.backend.common.dto.manager.query import StringFilter
24-
from ai.backend.common.types import MountPermission, MountTypes, RuntimeVariant
24+
from ai.backend.common.dto.manager.session.types import MountOption
25+
from ai.backend.common.types import RuntimeVariant
2526

2627
__all__ = (
2728
# Path param models
@@ -31,7 +32,6 @@
3132
"ListServeRequestModel",
3233
"ServiceFilterModel",
3334
"SearchServicesRequestModel",
34-
"ExtraMountModel",
3535
"ServiceConfigModel",
3636
"NewServiceRequestModel",
3737
"ScaleRequestModel",
@@ -63,33 +63,6 @@ class SearchServicesRequestModel(BaseRequestModel):
6363
limit: int = Field(default=20, ge=1, le=100)
6464

6565

66-
class ExtraMountModel(BaseRequestModel):
67-
"""Per-vfolder extra mount options for model service session creation."""
68-
69-
mount_destination: str | None = Field(
70-
default=None,
71-
description="Mount destination inside the container. Defaults to ``/home/work/{folder_name}``.",
72-
)
73-
type: MountTypes = Field(
74-
default=MountTypes.BIND,
75-
description="Mount type. Defaults to ``bind``.",
76-
)
77-
permission: MountPermission | None = Field(
78-
default=None,
79-
description=(
80-
"Permission override. ``null`` (default) inherits the vfolder's stored permission."
81-
),
82-
)
83-
subpath: str | None = Field(
84-
default=None,
85-
min_length=1,
86-
description=(
87-
"Subpath within the vfolder to mount. ``null`` (default) mounts the vfolder root."
88-
" Empty string is rejected; omit the field to mount the root."
89-
),
90-
)
91-
92-
9366
class ServiceConfigModel(BaseRequestModel):
9467
model_config = ConfigDict(protected_namespaces=())
9568

@@ -122,7 +95,7 @@ class ServiceConfigModel(BaseRequestModel):
12295
alias="vfolderSubpath",
12396
)
12497

125-
extra_mounts: dict[uuid.UUID, ExtraMountModel] = Field(
98+
extra_mounts: dict[uuid.UUID, MountOption] = Field(
12699
description=(
127100
"Specifications about extra VFolders mounted to model service session. "
128101
"MODEL type VFolders are not allowed to be attached to model service session"

src/ai/backend/common/dto/manager/session/types.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,13 +145,30 @@ class ResourceOpts(BaseFieldModel):
145145

146146

147147
class MountOption(BaseFieldModel):
148-
"""Per-mount option used inside ``mount_options`` mapping."""
148+
"""Per-mount option used inside ``mount_options`` mapping.
149149
150+
Single source of truth for per-vfolder mount overrides — used by both
151+
session creation (``CreationConfigV*.mount_options``) and inference
152+
service creation (``ServiceConfigModel.extra_mounts``).
153+
"""
154+
155+
mount_destination: str | None = Field(
156+
default=None,
157+
description="Mount destination inside the container. Defaults to ``/home/work/{folder_name}``.",
158+
)
150159
type: MountTypes = MountTypes.BIND
151160
permission: MountPermission | None = Field(
152161
default=None,
153162
validation_alias=AliasChoices("permission", "perm"),
154163
)
164+
subpath: str | None = Field(
165+
default=None,
166+
min_length=1,
167+
description=(
168+
"Subpath within the vfolder to mount. ``null`` (default) mounts the vfolder root."
169+
" Empty string is rejected; omit the field to mount the root."
170+
),
171+
)
155172
model_config = ConfigDict(extra="allow")
156173

157174

src/ai/backend/manager/api/rest/service/handler.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -675,7 +675,7 @@ async def _run_validation(
675675
model_mount_destination=params.config.model_mount_destination,
676676
vfolder_subpath=params.config.vfolder_subpath,
677677
extra_mounts={
678-
k: MountOption.from_model(v) for k, v in params.config.extra_mounts.items()
678+
k: MountOption.from_dto(v) for k, v in params.config.extra_mounts.items()
679679
},
680680
environ=params.config.environ,
681681
scaling_group=params.config.scaling_group,
@@ -777,7 +777,7 @@ def _to_start_action(
777777
model_mount_destination=params.config.model_mount_destination,
778778
vfolder_subpath=params.config.vfolder_subpath,
779779
extra_mounts={
780-
k: MountOption.from_model(v) for k, v in params.config.extra_mounts.items()
780+
k: MountOption.from_dto(v) for k, v in params.config.extra_mounts.items()
781781
},
782782
environ=params.config.environ,
783783
scaling_group=params.config.scaling_group,

src/ai/backend/manager/data/model_serving/types.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
from ai.backend.common.config import ModelDefinition
1414
from ai.backend.common.data.endpoint.types import EndpointLifecycle, ScalingState
1515
from ai.backend.common.data.user.types import UserRole
16-
from ai.backend.common.dto.manager.model_serving.request import ExtraMountModel
16+
from ai.backend.common.dto.manager.session.types import MountOption as MountOptionDTO
1717
from ai.backend.common.identifier.deployment import DeploymentID
1818
from ai.backend.common.identifier.runtime_variant import RuntimeVariantID
1919
from ai.backend.common.identifier.vfolder import VFolderUUID
@@ -197,13 +197,13 @@ class MountOption:
197197
subpath: str | None = None
198198

199199
@classmethod
200-
def from_model(cls, model: ExtraMountModel) -> MountOption:
201-
"""Convert a wire-level ``ExtraMountModel`` (DTO) into a ``MountOption``."""
200+
def from_dto(cls, dto: MountOptionDTO) -> MountOption:
201+
"""Convert the wire-level :class:`MountOption` DTO into a data-layer dataclass."""
202202
return cls(
203-
mount_destination=model.mount_destination,
204-
type=model.type,
205-
permission=model.permission,
206-
subpath=model.subpath,
203+
mount_destination=dto.mount_destination,
204+
type=dto.type,
205+
permission=dto.permission,
206+
subpath=dto.subpath,
207207
)
208208

209209
def to_dict(self) -> dict[str, Any]:

0 commit comments

Comments
 (0)