Skip to content

Commit bbb8e02

Browse files
jopemachineclaudelablup-octodog
authored
fix(BA-5983): accept partial ModelDefinition input in deployment API (#11531)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: octodog <mu001@lablup.com>
1 parent 3243034 commit bbb8e02

9 files changed

Lines changed: 161 additions & 94 deletions

File tree

changes/11531.fix.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Make ModelConfig / ModelDefinition / ModelServiceConfig / ModelHealthCheck GraphQL input fields optional so addModelRevision can inherit values from the runtime variant, model-definition.yaml, or revision preset.

docs/manager/graphql-reference/supergraph.graphql

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9235,10 +9235,10 @@ input ModelConfigInput
92359235
@join__type(graph: STRAWBERRY)
92369236
{
92379237
"""Name of the model."""
9238-
name: String!
9238+
name: String = null
92399239

92409240
"""Path to the model file."""
9241-
modelPath: String!
9241+
modelPath: String = null
92429242

92439243
"""Configuration for the model service."""
92449244
service: ModelServiceConfigInput = null
@@ -9264,7 +9264,7 @@ input ModelDefinitionInput
92649264
@join__type(graph: STRAWBERRY)
92659265
{
92669266
"""List of models in the model definition."""
9267-
models: [ModelConfigInput!]!
9267+
models: [ModelConfigInput!] = null
92689268
}
92699269

92709270
"""
@@ -9431,22 +9431,22 @@ input ModelHealthCheckInput
94319431
@join__type(graph: STRAWBERRY)
94329432
{
94339433
"""Interval in seconds between health checks."""
9434-
interval: Float! = 10
9434+
interval: Float = null
94359435

94369436
"""Path to check for health status."""
9437-
path: String!
9437+
path: String = null
94389438

94399439
"""Maximum number of retries for health check."""
9440-
maxRetries: Int! = 10
9440+
maxRetries: Int = null
94419441

94429442
"""Maximum time in seconds to wait for a health check response."""
9443-
maxWaitTime: Float! = 15
9443+
maxWaitTime: Float = null
94449444

94459445
"""Expected HTTP status code for a healthy response."""
9446-
expectedStatusCode: Int! = 200
9446+
expectedStatusCode: Int = null
94479447

94489448
"""Initial delay in seconds before the first health check."""
9449-
initialDelay: Float! = 60
9449+
initialDelay: Float = null
94509450
}
94519451

94529452
"""Added in 26.4.2. Metadata describing a model entry."""
@@ -9808,16 +9808,16 @@ input ModelServiceConfigInput
98089808
"""
98099809
List of pre-start actions to execute before starting the model service.
98109810
"""
9811-
preStartActions: [PreStartActionInput!]!
9811+
preStartActions: [PreStartActionInput!] = null
98129812

98139813
"""Command to start the model service."""
98149814
startCommand: [String!] = null
98159815

98169816
"""Shell configured for the model service."""
9817-
shell: String! = "/bin/bash"
9817+
shell: String = null
98189818

9819-
"""Port number for the model service. Must be greater than 1."""
9820-
port: Int!
9819+
"""Port number for the model service."""
9820+
port: Int = null
98219821

98229822
"""Health check configuration for the model service."""
98239823
healthCheck: ModelHealthCheckInput = null

docs/manager/graphql-reference/v2-schema.graphql

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6045,10 +6045,10 @@ Added in 26.4.0. Configuration for a single model within a model definition.
60456045
"""
60466046
input ModelConfigInput {
60476047
"""Name of the model."""
6048-
name: String!
6048+
name: String = null
60496049

60506050
"""Path to the model file."""
6051-
modelPath: String!
6051+
modelPath: String = null
60526052

60536053
"""Configuration for the model service."""
60546054
service: ModelServiceConfigInput = null
@@ -6070,7 +6070,7 @@ Added in 26.4.0. Model definition containing a list of model configurations.
60706070
"""
60716071
input ModelDefinitionInput {
60726072
"""List of models in the model definition."""
6073-
models: [ModelConfigInput!]!
6073+
models: [ModelConfigInput!] = null
60746074
}
60756075

60766076
"""
@@ -6218,22 +6218,22 @@ type ModelHealthCheck {
62186218
"""Added in 26.4.0. Health check configuration for a model service."""
62196219
input ModelHealthCheckInput {
62206220
"""Interval in seconds between health checks."""
6221-
interval: Float! = 10
6221+
interval: Float = null
62226222

62236223
"""Path to check for health status."""
6224-
path: String!
6224+
path: String = null
62256225

62266226
"""Maximum number of retries for health check."""
6227-
maxRetries: Int! = 10
6227+
maxRetries: Int = null
62286228

62296229
"""Maximum time in seconds to wait for a health check response."""
6230-
maxWaitTime: Float! = 15
6230+
maxWaitTime: Float = null
62316231

62326232
"""Expected HTTP status code for a healthy response."""
6233-
expectedStatusCode: Int! = 200
6233+
expectedStatusCode: Int = null
62346234

62356235
"""Initial delay in seconds before the first health check."""
6236-
initialDelay: Float! = 60
6236+
initialDelay: Float = null
62376237
}
62386238

62396239
"""Added in 26.4.2. Metadata describing a model entry."""
@@ -6559,16 +6559,16 @@ input ModelServiceConfigInput {
65596559
"""
65606560
List of pre-start actions to execute before starting the model service.
65616561
"""
6562-
preStartActions: [PreStartActionInput!]!
6562+
preStartActions: [PreStartActionInput!] = null
65636563

65646564
"""Command to start the model service."""
65656565
startCommand: [String!] = null
65666566

65676567
"""Shell configured for the model service."""
6568-
shell: String! = "/bin/bash"
6568+
shell: String = null
65696569

6570-
"""Port number for the model service. Must be greater than 1."""
6571-
port: Int!
6570+
"""Port number for the model service."""
6571+
port: Int = null
65726572

65736573
"""Health check configuration for the model service."""
65746574
healthCheck: ModelHealthCheckInput = null

src/ai/backend/common/config.py

Lines changed: 9 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -534,16 +534,9 @@ class ModelHealthCheckDraft(BaseConfigModel):
534534
def to_resolved(self) -> ModelHealthCheck:
535535
if self.path is None:
536536
raise ValueError("ModelHealthCheck.path is required")
537-
return ModelHealthCheck(
538-
interval=self.interval if self.interval is not None else 10.0,
539-
path=self.path,
540-
max_retries=self.max_retries if self.max_retries is not None else 10,
541-
max_wait_time=self.max_wait_time if self.max_wait_time is not None else 15.0,
542-
expected_status_code=(
543-
self.expected_status_code if self.expected_status_code is not None else 200
544-
),
545-
initial_delay=self.initial_delay if self.initial_delay is not None else 60.0,
546-
)
537+
# Drop unset (None) fields so the strict type's ``Field(default=...)``
538+
# declarations remain the single source of truth for default values.
539+
return ModelHealthCheck.model_validate(self.model_dump(exclude_none=True))
547540

548541

549542
class ModelServiceConfigDraft(BaseConfigModel):
@@ -561,12 +554,13 @@ def _coerce_start_command(cls, value: Any) -> Any:
561554
def to_resolved(self) -> ModelServiceConfig:
562555
if self.port is None:
563556
raise ValueError("ModelServiceConfig.port is required")
557+
# Drop unset (None) scalars so the strict type's ``Field(default=...)``
558+
# declarations remain the single source of truth for default values;
559+
# resolve the nested ``health_check`` draft explicitly so its own
560+
# required-field check (``path``) fires with a clear error message.
564561
return ModelServiceConfig(
565-
pre_start_actions=self.pre_start_actions or [],
566-
start_command=self.start_command,
567-
shell=self.shell if self.shell is not None else "/bin/bash",
568-
port=self.port,
569-
health_check=(self.health_check.to_resolved() if self.health_check else None),
562+
**self.model_dump(exclude_none=True, exclude={"health_check"}),
563+
health_check=self.health_check.to_resolved() if self.health_check else None,
570564
)
571565

572566

src/ai/backend/common/dto/manager/v2/deployment/request.py

Lines changed: 73 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,10 @@
1313
from pydantic import Field, field_validator
1414

1515
from ai.backend.common.api_handlers import SENTINEL, BaseRequestModel, Sentinel
16-
from ai.backend.common.config import ModelDefinitionDraft
16+
from ai.backend.common.config import (
17+
ModelDefinitionDraft,
18+
PreStartAction,
19+
)
1720
from ai.backend.common.data.model_deployment.types import (
1821
DeploymentStrategy,
1922
RouteHealthStatus,
@@ -80,10 +83,15 @@
8083
"EnvironmentVariablesInput",
8184
"ExtraVFolderMountInput",
8285
"ImageInput",
86+
"ModelConfigInput",
87+
"ModelDefinitionInput",
8388
"ModelDeploymentMetadataInput",
8489
"ModelDeploymentNetworkAccessInput",
90+
"ModelHealthCheckInput",
91+
"ModelMetadataInput",
8592
"ModelMountConfigInput",
8693
"ModelRuntimeConfigInput",
94+
"ModelServiceConfigInput",
8795
"ReplicaFilter",
8896
"ReplicaOrder",
8997
"ReplicaStatusFilter",
@@ -116,6 +124,67 @@
116124
)
117125

118126

127+
class ModelHealthCheckInput(BaseRequestModel):
128+
interval: float | None = None
129+
path: str | None = None
130+
max_retries: int | None = None
131+
max_wait_time: float | None = None
132+
expected_status_code: int | None = None
133+
initial_delay: float | None = None
134+
135+
136+
class ModelMetadataInput(BaseRequestModel):
137+
author: str | None = None
138+
title: str | None = None
139+
version: str | None = None
140+
created: str | None = None
141+
last_modified: str | None = None
142+
description: str | None = None
143+
task: str | None = None
144+
category: str | None = None
145+
architecture: str | None = None
146+
framework: list[str] | None = None
147+
label: list[str] | None = None
148+
license: str | None = None
149+
min_resource: dict[str, Any] | None = None
150+
151+
152+
class ModelServiceConfigInput(BaseRequestModel):
153+
pre_start_actions: list[PreStartAction] | None = None
154+
start_command: list[str] | None = None
155+
shell: str | None = None
156+
port: int | None = None
157+
health_check: ModelHealthCheckInput | None = None
158+
159+
160+
class ModelConfigInput(BaseRequestModel):
161+
name: str | None = None
162+
model_path: str | None = None
163+
service: ModelServiceConfigInput | None = None
164+
metadata: ModelMetadataInput | None = None
165+
166+
167+
class ModelDefinitionInput(BaseRequestModel):
168+
"""All-optional v2 input mirror of :class:`ModelDefinitionDraft`.
169+
170+
Fields a request omits are filled by lower-priority sources in the
171+
revision merge chain (runtime variant baseline, revision preset,
172+
vfolder ``model-definition.yaml``, ``model_mount_destination``
173+
default). Required-field enforcement happens later in
174+
``ModelDefinitionDraft.to_resolved`` after the merge.
175+
"""
176+
177+
models: list[ModelConfigInput] | None = None
178+
179+
def to_draft(self) -> ModelDefinitionDraft:
180+
# ``exclude_unset=True`` keeps the resulting draft's
181+
# ``model_fields_set`` aligned with what the caller actually
182+
# provided. Without it, every field would appear "explicitly
183+
# set" (to ``None``) and clobber lower-priority sources during
184+
# the revision merge.
185+
return ModelDefinitionDraft.model_validate(self.model_dump(exclude_unset=True))
186+
187+
119188
class ClusterConfigInput(BaseRequestModel):
120189
"""Cluster configuration input for a revision."""
121190

@@ -240,7 +309,7 @@ class CreateRevisionInputDTO(BaseRequestModel):
240309
image: ImageInput = Field(description="Container image")
241310
model_runtime_config: ModelRuntimeConfigInput = Field(description="Runtime configuration")
242311
model_mount_config: ModelMountConfigInput = Field(description="Model mount configuration")
243-
model_definition: ModelDefinitionDraft | None = Field(
312+
model_definition: ModelDefinitionInput | None = Field(
244313
default=None,
245314
description="Model definition to override the default values generated by the server",
246315
)
@@ -276,7 +345,7 @@ class AddRevisionGQLInputDTO(BaseRequestModel):
276345
image: ImageInput = Field(description="Container image")
277346
model_runtime_config: ModelRuntimeConfigInput = Field(description="Runtime configuration")
278347
model_mount_config: ModelMountConfigInput = Field(description="Model mount configuration")
279-
model_definition: ModelDefinitionDraft | None = Field(
348+
model_definition: ModelDefinitionInput | None = Field(
280349
default=None,
281350
description="Model definition to override the default values generated by the server",
282351
)
@@ -403,7 +472,7 @@ class RevisionInput(BaseRequestModel):
403472
default="/models", description="Mount destination for model vfolder"
404473
)
405474
model_definition_path: str = Field(description="Path to model definition file")
406-
model_definition: ModelDefinitionDraft | None = Field(
475+
model_definition: ModelDefinitionInput | None = Field(
407476
default=None,
408477
description="Model definition to override the default values generated by the server",
409478
)

src/ai/backend/manager/api/adapters/deployment/adapter.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -506,7 +506,9 @@ async def create(
506506
else None,
507507
),
508508
mounts=mounts_creator,
509-
model_definition=initial_revision.model_definition,
509+
model_definition=initial_revision.model_definition.to_draft()
510+
if initial_revision.model_definition is not None
511+
else None,
510512
revision_preset_id=initial_revision.revision_preset_id,
511513
execution=ExecutionSpec(
512514
runtime_variant_id=initial_revision.model_runtime_config.runtime_variant_id,
@@ -1111,7 +1113,9 @@ async def add_revision(
11111113
else None,
11121114
inference_runtime_config=input.model_runtime_config.inference_runtime_config,
11131115
),
1114-
model_definition=input.model_definition,
1116+
model_definition=input.model_definition.to_draft()
1117+
if input.model_definition is not None
1118+
else None,
11151119
revision_preset_id=input.revision_preset_id,
11161120
)
11171121
action_result = await self._processors.deployment.add_model_revision.wait_for_complete(

0 commit comments

Comments
 (0)