Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changes/11522.enhance.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Project ``EndpointRow`` directly to ``ModelDeploymentData`` for the API path so deployment responses no longer pass through a fragile ``DeploymentInfo`` conversion.
16 changes: 16 additions & 0 deletions docs/manager/graphql-reference/supergraph.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -5234,6 +5234,17 @@ enum DeploymentHistoryOrderField
UPDATED_AT @join__enumValue(graph: STRAWBERRY)
}

"""
Added in UNRELEASED. Sub-step within a deployment's current lifecycle phase. ``None`` when the deployment is not in a sub-step-bearing phase.
"""
enum DeploymentLifecycleSubStep
@join__type(graph: STRAWBERRY)
{
DEPLOYING_PROVISIONING @join__enumValue(graph: STRAWBERRY)
DEPLOYING_ROLLING_BACK @join__enumValue(graph: STRAWBERRY)
DEPLOYING_COMPLETED @join__enumValue(graph: STRAWBERRY)
}

Comment on lines +5237 to +5247

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

have we decided to show substep?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't want to show this value yet.

"""Added in UNRELEASED. Deployment options payload response."""
type DeploymentOptionsInfoGQL
@join__type(graph: STRAWBERRY)
Expand Down Expand Up @@ -9333,6 +9344,11 @@ type ModelDeployment implements Node
"""
scalingState: ScalingState!

"""
Added in UNRELEASED. Sub-step within the current lifecycle phase. ``None`` when the deployment is not in a sub-step-bearing phase.
"""
subStep: DeploymentLifecycleSubStep

"""
Added in 26.4.3. The current active revision of this deployment, resolved via DataLoader.
"""
Expand Down
14 changes: 14 additions & 0 deletions docs/manager/graphql-reference/v2-schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -3498,6 +3498,15 @@ enum DeploymentHistoryOrderField {
UPDATED_AT
}

"""
Added in UNRELEASED. Sub-step within a deployment's current lifecycle phase. ``None`` when the deployment is not in a sub-step-bearing phase.
"""
enum DeploymentLifecycleSubStep {
DEPLOYING_PROVISIONING
DEPLOYING_ROLLING_BACK
DEPLOYING_COMPLETED
}

"""Added in UNRELEASED. Deployment options payload response."""
type DeploymentOptionsInfoGQL {
"""Handler-keyed scheduler policy (timeout + retry)."""
Expand Down Expand Up @@ -6132,6 +6141,11 @@ type ModelDeployment implements Node {
"""
scalingState: ScalingState!

"""
Added in UNRELEASED. Sub-step within the current lifecycle phase. ``None`` when the deployment is not in a sub-step-bearing phase.
"""
subStep: DeploymentLifecycleSubStep

"""
Added in 26.4.3. The current active revision of this deployment, resolved via DataLoader.
"""
Expand Down
21 changes: 9 additions & 12 deletions src/ai/backend/client/cli/deployment.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,31 +93,28 @@ def create_deployment_cmd(

@deployment.command("list")
@pass_ctx_obj
@click.option("--project-id", type=str, default=None, help="Filter by project ID")
@click.option("--project-id", type=str, required=True, help="Project ID to search within")
@click.option("--limit", type=int, default=50, help="Maximum items to return")
@click.option("--offset", type=int, default=0, help="Number of items to skip")
def list_deployments_cmd(
ctx: CLIContext,
project_id: str | None,
project_id: str,
limit: int,
offset: int,
) -> None:
"""List all deployments."""
"""List deployments within a project."""
from uuid import UUID

from ai.backend.client.session import Session
from ai.backend.common.dto.manager.deployment import (
DeploymentFilter,
SearchDeploymentsRequest,
)
from ai.backend.common.dto.manager.deployment import SearchLegacyDeploymentsRequest

with Session() as session:
try:
filter_cond = None
if project_id:
filter_cond = DeploymentFilter(project_id=UUID(project_id))

request = SearchDeploymentsRequest(filter=filter_cond, limit=limit, offset=offset)
request = SearchLegacyDeploymentsRequest(
project_id=UUID(project_id),
limit=limit,
offset=offset,
)
Comment thread
jopemachine marked this conversation as resolved.
result = session.Deployment.search(request)

deployments = result.deployments
Expand Down
4 changes: 2 additions & 2 deletions src/ai/backend/client/cli/v2/admin/deployment.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,10 @@ def search(
"""Search all deployments (admin)."""
from ai.backend.common.dto.manager.query import StringFilter
from ai.backend.common.dto.manager.v2.deployment.request import (
AdminSearchDeploymentsInput,
DeploymentFilter,
DeploymentOrder,
DeploymentStatusFilter,
SearchDeploymentsInput,
)
from ai.backend.common.dto.manager.v2.deployment.types import DeploymentOrderField

Expand Down Expand Up @@ -99,7 +99,7 @@ async def _run() -> None:
registry = await create_v2_registry(load_v2_config())
try:
result = await registry.deployment.admin_search(
AdminSearchDeploymentsInput(
SearchDeploymentsInput(
filter=filter_dto,
order=orders,
limit=limit,
Expand Down
4 changes: 2 additions & 2 deletions src/ai/backend/client/cli/v2/deployment/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,10 @@ def project_search(

from ai.backend.common.dto.manager.query import StringFilter
from ai.backend.common.dto.manager.v2.deployment.request import (
AdminSearchDeploymentsInput,
DeploymentFilter,
DeploymentOrder,
DeploymentStatusFilter,
SearchDeploymentsInput,
)
from ai.backend.common.dto.manager.v2.deployment.types import DeploymentOrderField

Expand All @@ -79,7 +79,7 @@ def project_search(
)
orders = parsed

body = AdminSearchDeploymentsInput(
body = SearchDeploymentsInput(
filter=filter_dto,
order=orders,
limit=limit,
Expand Down
4 changes: 2 additions & 2 deletions src/ai/backend/client/cli/v2/my/deployment.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,9 @@ def search(

from ai.backend.common.dto.manager.query import StringFilter
from ai.backend.common.dto.manager.v2.deployment.request import (
AdminSearchDeploymentsInput,
DeploymentFilter,
DeploymentStatusFilter,
SearchDeploymentsInput,
)

filter_dto: DeploymentFilter | None = None
Expand All @@ -56,7 +56,7 @@ def search(
async def _run() -> None:
registry = await create_v2_registry(load_v2_config())
try:
request = AdminSearchDeploymentsInput(
request = SearchDeploymentsInput(
filter=filter_dto,
first=first,
after=after,
Expand Down
6 changes: 3 additions & 3 deletions src/ai/backend/client/func/deployment.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
ListDeploymentsResponse,
ListRevisionsResponse,
ListRoutesResponse,
SearchDeploymentsRequest,
SearchLegacyDeploymentsRequest,
SearchRevisionsRequest,
SearchRoutesRequest,
UpdateDeploymentRequest,
Expand Down Expand Up @@ -58,9 +58,9 @@ async def create(
@classmethod
async def search(
cls,
request: SearchDeploymentsRequest,
request: SearchLegacyDeploymentsRequest,
) -> ListDeploymentsResponse:
"""Search deployments with filters."""
"""Search deployments within a project."""
rqst = Request("POST", "/deployments/search")
rqst.set_json(request.model_dump(mode="json"))
async with rqst.fetch() as resp:
Expand Down
4 changes: 2 additions & 2 deletions src/ai/backend/client/v2/domains/deployment.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
ListRevisionsResponse,
ListRoutesResponse,
SearchDeploymentPoliciesRequest,
SearchDeploymentsRequest,
SearchLegacyDeploymentsRequest,
SearchRevisionsRequest,
SearchRoutesRequest,
UpdateDeploymentRequest,
Expand Down Expand Up @@ -51,7 +51,7 @@ async def create_deployment(

async def search_deployments(
self,
request: SearchDeploymentsRequest,
request: SearchLegacyDeploymentsRequest,
) -> ListDeploymentsResponse:
return await self._client.typed_request(
"POST",
Expand Down
22 changes: 11 additions & 11 deletions src/ai/backend/client/v2/domains_v2/deployment.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
from ai.backend.common.dto.manager.v2.deployment.request import (
ActivateRevisionInput,
AddRevisionInput,
AdminSearchDeploymentsInput,
AdminSearchRevisionsInput,
BulkDeleteAccessTokensInput,
CreateAccessTokenInput,
Expand All @@ -25,6 +24,7 @@
SearchAccessTokensInput,
SearchAutoScalingRulesInput,
SearchDeploymentPoliciesInput,
SearchDeploymentsInput,
SearchReplicasInput,
SearchRoutesInput,
SyncReplicaInput,
Expand All @@ -36,7 +36,6 @@
ActivateRevisionPayload,
AddRevisionPayload,
AdminRefreshDeploymentRevisionsPayload,
AdminSearchDeploymentsPayload,
AdminSearchRevisionsPayload,
BulkDeleteAccessTokensPayload,
BulkDeleteAutoScalingRulesPayload,
Expand All @@ -56,6 +55,7 @@
SearchAccessTokensPayload,
SearchAutoScalingRulesPayload,
SearchDeploymentPoliciesPayload,
SearchDeploymentsPayload,
SearchReplicasPayload,
SearchRoutesPayload,
SyncReplicaPayload,
Expand Down Expand Up @@ -91,39 +91,39 @@ async def create(

async def admin_search(
self,
body: AdminSearchDeploymentsInput,
) -> AdminSearchDeploymentsPayload:
body: SearchDeploymentsInput,
) -> SearchDeploymentsPayload:
"""Search deployments with admin scope (superadmin only)."""
return await self._client.typed_request(
"POST",
_PATH + "/search",
request=body,
response_model=AdminSearchDeploymentsPayload,
response_model=SearchDeploymentsPayload,
)

async def my_search(
self,
body: AdminSearchDeploymentsInput,
) -> AdminSearchDeploymentsPayload:
body: SearchDeploymentsInput,
) -> SearchDeploymentsPayload:
"""Search deployments owned by the current user."""
return await self._client.typed_request(
"POST",
f"{_PATH}/my/search",
request=body,
response_model=AdminSearchDeploymentsPayload,
response_model=SearchDeploymentsPayload,
)

async def project_search(
self,
project_id: UUID,
body: AdminSearchDeploymentsInput,
) -> AdminSearchDeploymentsPayload:
body: SearchDeploymentsInput,
) -> SearchDeploymentsPayload:
"""Search deployments within a specific project."""
return await self._client.typed_request(
"POST",
f"{_PATH}/projects/{project_id}/search",
request=body,
response_model=AdminSearchDeploymentsPayload,
response_model=SearchDeploymentsPayload,
)

async def get(
Expand Down
39 changes: 39 additions & 0 deletions src/ai/backend/common/data/model_deployment/types.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
from __future__ import annotations

import enum
from typing import Any, Self

from ai.backend.common.data.endpoint.types import EndpointLifecycle
from ai.backend.common.types import CIStrEnum


Expand Down Expand Up @@ -48,12 +52,47 @@ def _missing_(cls, value: Any) -> Self | None:
return cls(alias)
return super()._missing_(value)

@classmethod
def from_lifecycle(cls, lifecycle: EndpointLifecycle) -> ModelDeploymentStatus:
match lifecycle:
case EndpointLifecycle.PENDING | EndpointLifecycle.CREATED:
return cls.PENDING
case EndpointLifecycle.READY | EndpointLifecycle.SCALING:
return cls.READY
case EndpointLifecycle.DEPLOYING:
return cls.DEPLOYING
case EndpointLifecycle.DESTROYING:
return cls.STOPPING
case EndpointLifecycle.DESTROYED:
return cls.STOPPED


class DeploymentStrategy(CIStrEnum):
ROLLING = "ROLLING"
BLUE_GREEN = "BLUE_GREEN"


class DeploymentLifecycleSubStep(enum.StrEnum):
"""Sub-steps within deployment lifecycle phases.

Member names are prefixed with the lifecycle phase they belong to
(e.g. ``DEPLOYING_``). String values are stored in the database as-is.
"""

# -- DEPLOYING phase --
DEPLOYING_PROVISIONING = "deploying_provisioning"
"""New revision routes are being provisioned and old routes are being drained."""
DEPLOYING_ROLLING_BACK = "deploying_rolling_back"
"""Clearing deploying_revision and transitioning to READY."""
DEPLOYING_COMPLETED = "deploying_completed"
"""All strategy conditions satisfied; triggers revision swap."""

@classmethod
def deploying_handler_sub_steps(cls) -> tuple[DeploymentLifecycleSubStep, ...]:
"""Sub-steps that have their own deploying handler (excludes COMPLETED, which is an evaluator outcome)."""
return (cls.DEPLOYING_PROVISIONING, cls.DEPLOYING_ROLLING_BACK)


class RouteStatus(CIStrEnum):
"""Lifecycle status of a route in the deployment."""

Expand Down
5 changes: 3 additions & 2 deletions src/ai/backend/common/dto/manager/deployment/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
RouteFilter,
RoutePathParam,
SearchDeploymentPoliciesRequest,
SearchDeploymentsRequest,
SearchLegacyDeploymentsRequest,
SearchRevisionsRequest,
SearchRoutesRequest,
UpdateDeploymentRequest,
Expand Down Expand Up @@ -90,11 +90,12 @@
# Request DTOs - Path params
"DeploymentPathParam",
"DeploymentPolicyPathParam",
"DeploymentProjectSearchPathParam",
"RevisionPathParam",
"RoutePathParam",
# Request DTOs - Search/List
"SearchDeploymentPoliciesRequest",
"SearchDeploymentsRequest",
"SearchLegacyDeploymentsRequest",
"SearchRevisionsRequest",
"SearchRoutesRequest",
# Request DTOs - Create inputs
Expand Down
16 changes: 13 additions & 3 deletions src/ai/backend/common/dto/manager/deployment/request.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
"RevisionFilter",
"RouteFilter",
# Search/List requests
"SearchDeploymentsRequest",
"SearchLegacyDeploymentsRequest",
"SearchRevisionsRequest",
"SearchRoutesRequest",
# Create requests
Expand Down Expand Up @@ -83,9 +83,19 @@ class RevisionFilter(BaseRequestModel):
deployment_id: UUID | None = Field(default=None, description="Filter by deployment ID")


class SearchDeploymentsRequest(BaseRequestModel):
"""Request body for searching deployments with filters, orders, and pagination."""
class SearchLegacyDeploymentsRequest(BaseRequestModel):
"""Legacy v1 search request body carrying its project scope inline.

Standalone request type so the v1 ``POST /deployments/search`` contract
stays independent of any shared base — every legacy-only field is
visible on this class. ``project_id`` is taken from the body rather
than the URL so existing clients keep their request shape; the handler
resolves it to the project scope on the v2-shared
``SearchProjectDeploymentsAction`` path. New callers should use the v2
scoped endpoints instead.
"""

project_id: UUID = Field(description="Project ID to scope the search to.")
filter: DeploymentFilter | None = Field(default=None, description="Filter conditions")
order: DeploymentOrder | None = Field(default=None, description="Order specification")
limit: int = Field(default=50, ge=1, le=1000, description="Maximum items to return")
Expand Down
Loading
Loading