Repository: friendliai/periflow-cli (language: python)

Issue:
Sync to latest PFS server
- Sync to latest PFS server
- Add PFS template cmd
- Add `settings.py` file to separate pft/pfs and server urls


Discussion / hints:

Fix patch:
diff --git a/pfcli/billing.py b/pfcli/billing.py
index cb0190b..af9131d 100644
--- a/pfcli/billing.py
+++ b/pfcli/billing.py
@@ -7,9 +7,9 @@
 import tabulate
 import typer
 
-from pfcli.service import ServiceType
+from pfcli.service import PeriFlowService, ServiceType
 from pfcli.service.client import build_client
-from pfcli.service.client.billing import BillingClientService, TimeGranularity
+from pfcli.service.client.billing import PFTBillingClientService, TimeGranularity
 from pfcli.service.formatter import PanelFormatter, TableFormatter
 from pfcli.utils.format import (
     datetime_to_simple_string,
@@ -37,23 +37,15 @@
 )
 
 
-@app.command(help="summarize billing information")
-def summary(
-    year: int = typer.Argument(...),
-    month: int = typer.Argument(...),
-    day: Optional[int] = typer.Argument(None),
-    view_project: bool = typer.Option(
-        False, "--project", "-p", help="View project-level cost summary."
-    ),
-    view_organization: bool = typer.Option(
-        False, "--organization", "-o", help="View organization-level cost summary."
-    ),
-    time_granularity: Optional[TimeGranularity] = typer.Option(
-        None, "--time-granularity", "-t", help="View within the given time granularity."
-    ),
+def billing_summary_for_train(
+    year: int,
+    month: int,
+    day: Optional[int],
+    view_project: bool,
+    view_organization: bool,
+    time_granularity: Optional[TimeGranularity],
 ):
-    """Summarize the billing information for the given time range"""
-    client: BillingClientService = build_client(ServiceType.BILLING_SUMMARY)
+    client: PFTBillingClientService = build_client(ServiceType.PFT_BILLING_SUMMARY)
 
     agg_by = "user_id"
 
@@ -103,3 +95,53 @@ def summary(
 
     table_formatter.render(prices)
     panel_formatter.render([{"price": round(total_price, 2)}])
+
+
+def billing_summary_for_serve(
+    year: int,
+    month: int,
+    day: Optional[int],
+    view_project: bool,
+    view_organization: bool,
+    time_granularity: Optional[TimeGranularity],
+):
+    # TODO: FILL ME
+    secho_error_and_exit(
+        "VM list for the deployment is not supported yet. Please contact the support team."
+    )
+
+
+@app.command(help="summarize billing information")
+def summary(
+    service: PeriFlowService = typer.Option(
+        ...,
+        "--service",
+        "-s",
+        help="PeriFlow service type to see service usage and costs.",
+    ),
+    year: int = typer.Argument(...),
+    month: int = typer.Argument(...),
+    day: Optional[int] = typer.Argument(None),
+    view_project: bool = typer.Option(
+        False, "--project", "-p", help="View project-level cost summary."
+    ),
+    view_organization: bool = typer.Option(
+        False, "--organization", "-o", help="View organization-level cost summary."
+    ),
+    time_granularity: Optional[TimeGranularity] = typer.Option(
+        None, "--time-granularity", "-t", help="View within the given time granularity."
+    ),
+):
+    """Summarize the billing information for the given time range"""
+    handler_map = {
+        PeriFlowService.TRAIN: billing_summary_for_train,
+        PeriFlowService.SERVE: billing_summary_for_serve,
+    }
+    handler_map[service](
+        year=year,
+        month=month,
+        day=day,
+        view_project=view_project,
+        view_organization=view_organization,
+        time_granularity=time_granularity,
+    )
diff --git a/pfcli/deployment.py b/pfcli/deployment.py
new file mode 100644
index 0000000..42d5b0c
--- /dev/null
+++ b/pfcli/deployment.py
@@ -0,0 +1,232 @@
+# Copyright (C) 2021 FriendliAI
+
+"""CLI for Deployment"""
+
+from dateutil.parser import parse
+from typing import Optional
+
+import ruamel.yaml
+import typer
+import yaml
+from click import Choice
+
+from pfcli.service import (
+    ServiceType,
+    GpuType,
+    CloudType,
+    EngineType,
+)
+from pfcli.service.client import (
+    DeploymentClientService,
+    build_client,
+)
+from pfcli.service.config import build_deployment_configurator
+from pfcli.service.formatter import PanelFormatter, TableFormatter
+from pfcli.utils.format import datetime_to_pretty_str, secho_error_and_exit
+from pfcli.utils.prompt import get_default_editor, open_editor
+
+
+app = typer.Typer(
+    no_args_is_help=True,
+    context_settings={"help_option_names": ["-h", "--help"]},
+    add_completion=False,
+)
+template_app = typer.Typer(
+    no_args_is_help=True,
+    context_settings={"help_option_names": ["-h", "--help"]},
+    add_completion=False,
+)
+
+app.add_typer(template_app, name="template", help="Manage deployment templates.")
+
+deployment_panel = PanelFormatter(
+    name="Overview",
+    fields=[
+        "id",
+        "config.name",
+        "status",
+        "vms",
+        "config.gpu_type",
+        "config.total_gpus",
+        "start",
+        "endpoint",
+    ],
+    headers=["ID", "Name", "Status", "VM", "Device", "Device Cnt", "Start", "Endpoint"],
+    extra_fields=["error"],
+    extra_headers=["error"],
+)
+
+deployment_table = TableFormatter(
+    name="Deployments",
+    fields=[
+        "id",
+        "config.name",
+        "status",
+        "vms",
+        "config.gpu_type",
+        "config.total_gpus",
+        "start",
+    ],
+    headers=["ID", "Name", "Status", "VM", "Device", "Device Cnt", "Start"],
+    extra_fields=["error"],
+    extra_headers=["error"],
+)
+
+deployment_panel.add_substitution_rule("waiting", "[bold]waiting")
+deployment_panel.add_substitution_rule("enqueued", "[bold cyan]enqueued")
+deployment_panel.add_substitution_rule("running", "[bold blue]running")
+deployment_panel.add_substitution_rule("success", "[bold green]success")
+deployment_panel.add_substitution_rule("failed", "[bold red]failed")
+deployment_panel.add_substitution_rule("terminated", "[bold yellow]terminated")
+deployment_panel.add_substitution_rule("terminating", "[bold magenta]terminating")
+deployment_panel.add_substitution_rule("cancelling", "[bold magenta]cancelling")
+
+deployment_table.add_substitution_rule("waiting", "[bold]waiting")
+deployment_table.add_substitution_rule("enqueued", "[bold cyan]enqueued")
+deployment_table.add_substitution_rule("running", "[bold blue]running")
+deployment_table.add_substitution_rule("success", "[bold green]success")
+deployment_table.add_substitution_rule("failed", "[bold red]failed")
+deployment_table.add_substitution_rule("terminated", "[bold yellow]terminated")
+deployment_table.add_substitution_rule("terminating", "[bold magenta]terminating")
+deployment_table.add_substitution_rule("cancelling", "[bold magenta]cancelling")
+
+
+@app.command()
+def list(
+    tail: Optional[int] = typer.Option(
+        None, "--tail", help="The number of deployment list to view at the tail"
+    ),
+    head: Optional[int] = typer.Option(
+        None, "--head", help="The number of deployment list to view at the head"
+    ),
+):
+    """List all deployments."""
+    client: DeploymentClientService = build_client(ServiceType.DEPLOYMENT)
+    deployments = client.list_deployments()["deployments"]
+
+    for deployment in deployments:
+        started_at = deployment.get("start")
+        if started_at is not None:
+            start = datetime_to_pretty_str(parse(started_at))
+        else:
+            start = None
+        deployment["start"] = start
+        deployment["vms"] = deployment["vms"][0]["name"]
+
+    if tail is not None or head is not None:
+        target_deployment_list = []
+        if tail:
+            target_deployment_list.extend(deployments[:tail])
+        if head:
+            target_deployment_list.extend(deployments[-head:])
+    else:
+        target_deployment_list = deployments
+
+    deployment_table.render(target_deployment_list)
+
+
+@app.command()
+def delete(deployment_id: str = typer.Argument(..., help="ID of deployment to delete")):
+    """Delete deployment."""
+    client: DeploymentClientService = build_client(ServiceType.DEPLOYMENT)
+    client.delete_deployment(deployment_id)
+
+
+@app.command()
+def view(
+    deployment_id: str = typer.Argument(..., help="deployment id to inspect detail.")
+):
+    """Show details of a deployment."""
+    client: DeploymentClientService = build_client(ServiceType.DEPLOYMENT)
+    deployment = client.get_deployment(deployment_id)
+
+    started_at = deployment.get("start")
+    if started_at is not None:
+        start = datetime_to_pretty_str(parse(started_at))
+    else:
+        start = None
+    deployment["start"] = start
+    deployment["vms"] = deployment["vms"][0]["name"]
+    deployment_panel.render([deployment])
+
+
+@app.command()
+def create(
+    project_id: str = typer.Option(
+        ..., "--project-id", "-pid", help="Project to deploy."
+    ),
+    checkpoint_id: str = typer.Option(
+        ..., "--checkpoint-id", "-id", help="Checkpoint id to deploy."
+    ),
+    deployment_name: str = typer.Option(
+        ..., "--name", "-n", help="The name of deployment. "
+    ),
+    gpu_type: GpuType = typer.Option(
+        ..., "--gpu-type", "-g", help="The GPU type where the deployment is deployed."
+    ),
+    cloud: CloudType = typer.Option(
+        ..., "--cloud", "-c", help="Type of cloud(aws, azure, gcp)."
+    ),
+    region: str = typer.Option(..., "--region", "-r", help="Region of cloud."),
+    engine: Optional[EngineType] = typer.Option(
+        EngineType.ORCA, "--engine", "-e", help="Type of engine(orca or triton)."
+    ),
+    config_file: typer.FileText = typer.Option(
+        ..., "--config-file", "-f", help="Path to configuration file."
+    ),
+):
+    """Create a deployment object by using model checkpoint."""
+    if engine is not EngineType.ORCA:
+        secho_error_and_exit("Only ORCA is supported!")
+
+    try:
+        config: dict = yaml.safe_load(config_file)
+    except yaml.YAMLError as e:
+        secho_error_and_exit(f"Error occurred while parsing engine config file... {e}")
+
+    request_data = {
+        "project_id": project_id,
+        "model_id": checkpoint_id,
+        "name": deployment_name,
+        "gpu_type": gpu_type,
+        "cloud": cloud,
+        "region": region,
+        **config,
+    }
+    typer.secho(f"request_data: {request_data}")
+    client: DeploymentClientService = build_client(ServiceType.DEPLOYMENT)
+    typer.secho(f"deployment url:{client.url_kwargs}")
+    deployment = client.create_deployment(request_data)
+
+    typer.secho(
+        f"Deployment ({deployment['id']}) started successfully. Use 'pf deployment view <id>' to see the deployment details.\n"
+        f"Run 'curl {deployment['endpoint']}' for inference request",
+        fg=typer.colors.BLUE,
+    )
+
+
+@template_app.command("create")
+def template_crate(
+    save_path: typer.FileTextWrite = typer.Option(
+        ..., "--save-path", "-s", help="Path to save job YAML configruation file."
+    )
+):
+    """Create a deployment engine configuration YAML file."""
+    engine_type = typer.prompt(
+        "What kind of engine type do you want?\n",
+        type=Choice([e.value for e in EngineType]),
+        prompt_suffix="\n>> ",
+    )
+    configurator = build_deployment_configurator(engine_type)
+    yaml_str = configurator.render()
+
+    yaml = ruamel.yaml.YAML()
+    code = yaml.load(yaml_str)
+    yaml.dump(code, save_path)
+
+    continue_edit = typer.confirm(
+        f"Do you want to open an editor to configure the job YAML file? (default editor: {get_default_editor()})",
+        prompt_suffix="\n>> ",
+    )
+    if continue_edit:
+        open_editor(save_path.name)
diff --git a/pfcli/job.py b/pfcli/job.py
index fe171ec..75bbda0 100644
--- a/pfcli/job.py
+++ b/pfcli/job.py
@@ -27,7 +27,7 @@
     storage_type_map_inv,
 )
 from pfcli.service.client import (
-    GroupVMConfigClientService,
+    PFTGroupVMConfigClientService,
     JobTemplateClientService,
     JobWebSocketClientService,
     ProjectDataClientService,
@@ -198,7 +198,9 @@ def refine_config(
         config["job_setting"]["workspace"] = {"mount_path": "/workspace"}
 
     data_client: ProjectDataClientService = build_client(ServiceType.PROJECT_DATA)
-    vm_client: GroupVMConfigClientService = build_client(ServiceType.GROUP_VM_CONFIG)
+    vm_client: PFTGroupVMConfigClientService = build_client(
+        ServiceType.PFT_GROUP_VM_CONFIG
+    )
     job_template_client: JobTemplateClientService = build_client(
         ServiceType.JOB_TEMPLATE
     )
diff --git a/pfcli/main.py b/pfcli/main.py
index 3f51caf..f0ac74a 100644
--- a/pfcli/main.py
+++ b/pfcli/main.py
@@ -16,7 +16,7 @@
     group,
     job,
     project,
-    serve,
+    deployment,
     vm,
 )
 from pfcli.context import (
@@ -44,16 +44,17 @@
     context_settings={"help_option_names": ["-h", "--help"]},
     add_completion=False,
 )
+
 app.add_typer(credential.app, name="credential", help="Manage credentials")
-app.add_typer(job.app, name="job", help="Manage jobs")
 app.add_typer(checkpoint.app, name="checkpoint", help="Manage checkpoints")
-app.add_typer(dataset.app, name="dataset", help="Manage datasets")
 app.add_typer(vm.app, name="vm", help="Manage VMs")
-app.add_typer(serve.app, name="serve", help="Manage serves")
+app.add_typer(deployment.app, name="deployment", help="Manage deployments")
 app.add_typer(project.app, name="project", help="Manage projects")
 app.add_typer(group.app, name="org", help="Manage organizations")
-app.add_typer(artifact.app, name="artifact", help="Manager artifacts")
 app.add_typer(billing.app, name="billing", help="Manage billing")
+app.add_typer(artifact.app, name="artifact", help="Manager artifacts")
+app.add_typer(job.app, name="job", help="Manage jobs")
+app.add_typer(dataset.app, name="dataset", help="Manage datasets")
 
 
 user_panel_formatter = PanelFormatter(
diff --git a/pfcli/serve.py b/pfcli/serve.py
deleted file mode 100644
index b999fd8..0000000
--- a/pfcli/serve.py
+++ /dev/null
@@ -1,155 +0,0 @@
-# Copyright (C) 2021 FriendliAI
-
-"""CLI for Serve"""
-
-from dateutil.parser import parse
-from typing import Optional
-from uuid import UUID
-
-import typer
-
-from pfcli.service import (
-    ServiceType,
-    GpuType,
-)
-from pfcli.service.client import (
-    ServeClientService,
-    build_client,
-)
-from pfcli.service.formatter import PanelFormatter, TableFormatter
-from pfcli.utils.format import datetime_to_pretty_str
-
-
-app = typer.Typer(
-    no_args_is_help=True,
-    context_settings={"help_option_names": ["-h", "--help"]},
-    add_completion=False,
-)
-
-serve_panel = PanelFormatter(
-    name="Overview",
-    fields=["id", "name", "status", "vm", "gpu_type", "num_gpus", "start", "endpoint"],
-    headers=["ID", "Name", "Status", "VM", "Device", "Device Cnt", "Start", "Endpoint"],
-    extra_fields=["error"],
-    extra_headers=["error"],
-)
-
-serve_table = TableFormatter(
-    name="Serves",
-    fields=[
-        "id",
-        "name",
-        "status",
-        "vm",
-        "gpu_type",
-        "num_gpus",
-        "start",
-    ],
-    headers=["ID", "Name", "Status", "VM", "Device", "Device Cnt", "Start"],
-    extra_fields=["error"],
-    extra_headers=["error"],
-)
-
-serve_panel.add_substitution_rule("waiting", "[bold]waiting")
-serve_panel.add_substitution_rule("enqueued", "[bold cyan]enqueued")
-serve_panel.add_substitution_rule("running", "[bold blue]running")
-serve_panel.add_substitution_rule("success", "[bold green]success")
-serve_panel.add_substitution_rule("failed", "[bold red]failed")
-serve_panel.add_substitution_rule("terminated", "[bold yellow]terminated")
-serve_panel.add_substitution_rule("terminating", "[bold magenta]terminating")
-serve_panel.add_substitution_rule("cancelling", "[bold magenta]cancelling")
-
-serve_table.add_substitution_rule("waiting", "[bold]waiting")
-serve_table.add_substitution_rule("enqueued", "[bold cyan]enqueued")
-serve_table.add_substitution_rule("running", "[bold blue]running")
-serve_table.add_substitution_rule("success", "[bold green]success")
-serve_table.add_substitution_rule("failed", "[bold red]failed")
-serve_table.add_substitution_rule("terminated", "[bold yellow]terminated")
-serve_table.add_substitution_rule("terminating", "[bold magenta]terminating")
-serve_table.add_substitution_rule("cancelling", "[bold magenta]cancelling")
-
-
-@app.command()
-def list(
-    tail: Optional[int] = typer.Option(
-        None, "--tail", help="The number of serve list to view at the tail"
-    ),
-    head: Optional[int] = typer.Option(
-        None, "--head", help="The number of serve list to view at the head"
-    ),
-):
-    """List all serves,"""
-    client: ServeClientService = build_client(ServiceType.SERVE)
-    serves = client.list_serves()
-
-    for serve in serves:
-        started_at = serve.get("start")
-        if started_at is not None:
-            start = datetime_to_pretty_str(parse(started_at))
-        else:
-            start = None
-        serve["start"] = start
-
-    if tail is not None or head is not None:
-        target_serve_list = []
-        if tail:
-            target_serve_list.extend(serves[:tail])
-        if head:
-            target_serve_list.extend(serves[-head:])
-    else:
-        target_serve_list = serves
-
-    serve_table.render(target_serve_list)
-
-
-@app.command()
-def stop(serve_id: str = typer.Argument(..., help="ID of serve to stop")):
-    """Delete serve."""
-    client: ServeClientService = build_client(ServiceType.SERVE)
-    client.delete_serve(serve_id)
-
-
-@app.command()
-def view(serve_id: str = typer.Argument(..., help="serve id to inspect detail.")):
-    """Show details of a serve."""
-    client: ServeClientService = build_client(ServiceType.SERVE)
-    serve = client.get_serve(serve_id)
-
-    started_at = serve.get("start")
-    if started_at is not None:
-        start = datetime_to_pretty_str(parse(started_at))
-    else:
-        start = None
-    serve["start"] = start
-
-    serve_panel.render([serve])
-
-
-@app.command()
-def create(
-    checkpoint_id: UUID = typer.Option(
-        ..., "--checkpoint-id", "-c", help="Checkpoint id to deploy."
-    ),
-    name: str = typer.Option(..., "--name", "-n", help="The name of serve deployment."),
-    gpu_type: GpuType = typer.Option(
-        ..., "--gpu-type", "-g", help="The GPU type where the serve is deployed."
-    ),
-    num_sessions: int = typer.Option(
-        ..., "--num-sessions", "-s", help="The number of sessions of serve deployment."
-    ),
-):
-    """Create a serve object by using model checkpoint."""
-    request_data = {
-        "name": name,
-        "model_id": checkpoint_id,
-        "gpu_type": gpu_type,
-        "num_sessions": num_sessions,
-    }
-    client: ServeClientService = build_client(ServiceType.SERVE)
-    serve = client.create_serve(request_data)
-
-    typer.secho(
-        f"Serve ({serve['id']}) started successfully. Use 'pf serivce view <id>' to see the serve details.\n"
-        f"Run 'curl {serve['endpoint']}' for inference request",
-        fg=typer.colors.BLUE,
-    )
diff --git a/pfcli/service/__init__.py b/pfcli/service/__init__.py
index 4ce8cc5..1d818cc 100644
--- a/pfcli/service/__init__.py
+++ b/pfcli/service/__init__.py
@@ -18,9 +18,9 @@ class ServiceType(str, Enum):
     PROJECT_JOB = "PROJECT_JOB"
     JOB_TEMPLATE = "JOB_TEMPLATE"
     GROUP_VM_QUOTA = "GROUP_VM_QUOTA"
-    PROJECT_VM_QUOTA = "PROJECT_VM_QUOTA"
-    PROJECT_VM_CONFIG = "PROJECT_VM_CONFIG"
-    GROUP_VM_CONFIG = "GROUP_VM_CONFIG"
+    PFT_PROJECT_VM_QUOTA = "PROJECT_VM_QUOTA"
+    PFT_PROJECT_VM_CONFIG = "PROJECT_VM_CONFIG"
+    PFT_GROUP_VM_CONFIG = "GROUP_VM_CONFIG"
     CREDENTIAL = "CREDENTIAL"
     PROJECT_CREDENTIAL = "PROJECT_CREDENTIAL"
     CREDENTIAL_TYPE = "CREDENTIAL_TYPE"
@@ -29,8 +29,8 @@ class ServiceType(str, Enum):
     CHECKPOINT = "CHECKPOINT"
     GROUP_PROJECT_CHECKPOINT = "GROUP_PROJECT_CHECKPOINT"
     JOB_WS = "JOB_WS"
-    SERVE = "SERVE"
-    BILLING_SUMMARY = "BILLING_SUMMARY"
+    DEPLOYMENT = "DEPLOYMENT"
+    PFT_BILLING_SUMMARY = "BILLING_SUMMARY"
     METRICS = "METRICS"
 
 
@@ -112,6 +112,11 @@ class SimpleJobStatus(str, Enum):
 }
 
 
+class PeriFlowService(str, Enum):
+    TRAIN = "train"
+    SERVE = "serve"
+
+
 class JobType(str, Enum):
     PREDEFINED = "predefined"
     CUSTOM = "custom"
@@ -154,6 +159,11 @@ class GpuType(str, Enum):
     V100 = "v100"
 
 
+class EngineType(str, Enum):
+    ORCA = "orca"
+    TRITON = "triton"
+
+
 storage_type_map: Dict[StorageType, str] = {
     StorageType.S3: "aws",
     StorageType.BLOB: "azure.blob",
diff --git a/pfcli/service/client/__init__.py b/pfcli/service/client/__init__.py
index 5833eef..8ab08ce 100644
--- a/pfcli/service/client/__init__.py
+++ b/pfcli/service/client/__init__.py
@@ -5,7 +5,7 @@
 
 from pfcli.service import ServiceType
 from pfcli.service.client.base import ClientService
-from pfcli.service.client.billing import BillingClientService
+from pfcli.service.client.billing import PFTBillingClientService
 from pfcli.service.client.checkpoint import CheckpointClientService
 from pfcli.service.client.credential import (
     CredentialClientService,
@@ -17,7 +17,7 @@
     GroupProjectCheckpointClientService,
     GroupProjectClientService,
     GroupProjectVMQuotaClientService,
-    GroupVMConfigClientService,
+    PFTGroupVMConfigClientService,
 )
 from pfcli.service.client.job import (
     JobTemplateClientService,
@@ -31,10 +31,10 @@
     ProjectClientService,
     ProjectCredentialClientService,
     ProjectDataClientService,
-    ProjectVMConfigClientService,
-    ProjectVMQuotaClientService,
+    PFTProjectVMConfigClientService,
+    PFTProjectVMQuotaClientService,
 )
-from pfcli.service.client.serve import ServeClientService
+from pfcli.service.client.deployment import DeploymentClientService
 from pfcli.service.client.user import (
     UserClientService,
     UserGroupClientService,
@@ -113,8 +113,8 @@
         GroupProjectVMQuotaClientService,
         Template(get_uri("group/$group_id/vm_quota/")),
     ),
-    ServiceType.PROJECT_VM_QUOTA: (
-        ProjectVMQuotaClientService,
+    ServiceType.PFT_PROJECT_VM_QUOTA: (
+        PFTProjectVMQuotaClientService,
         Template(get_uri("project/$project_id/vm_quota/")),
     ),
     ServiceType.CHECKPOINT: (CheckpointClientService, Template(get_mr_uri("models/"))),
@@ -122,18 +122,21 @@
         GroupProjectCheckpointClientService,
         Template(get_mr_uri("orgs/$group_id/prjs/$project_id/models/")),
     ),  # pylint: disable=line-too-long
-    ServiceType.PROJECT_VM_CONFIG: (
-        ProjectVMConfigClientService,
+    ServiceType.PFT_PROJECT_VM_CONFIG: (
+        PFTProjectVMConfigClientService,
         Template(get_uri("project/$project_id/vm_config/")),
     ),
-    ServiceType.GROUP_VM_CONFIG: (
-        GroupVMConfigClientService,
+    ServiceType.PFT_GROUP_VM_CONFIG: (
+        PFTGroupVMConfigClientService,
         Template(get_uri("group/$group_id/vm_config/")),
     ),
     ServiceType.JOB_WS: (JobWebSocketClientService, Template(get_wss_uri("job/"))),
-    ServiceType.SERVE: (ServeClientService, Template(get_pfs_uri("deployment/"))),
-    ServiceType.BILLING_SUMMARY: (
-        BillingClientService,
+    ServiceType.DEPLOYMENT: (
+        DeploymentClientService,
+        Template(get_pfs_uri("deployment/")),
+    ),
+    ServiceType.PFT_BILLING_SUMMARY: (
+        PFTBillingClientService,
         Template(get_meter_uri("training/instances/price/")),
     ),
     ServiceType.METRICS: (
diff --git a/pfcli/service/client/base.py b/pfcli/service/client/base.py
index b6197ec..146c186 100644
--- a/pfcli/service/client/base.py
+++ b/pfcli/service/client/base.py
@@ -3,6 +3,7 @@
 """PeriFlow Client Service"""
 
 import copy
+import typer
 import requests
 from dataclasses import dataclass
 from functools import wraps
@@ -34,6 +35,7 @@ def wrapper(*args, **kwargs) -> Response:
         try:
             return func(*args, **kwargs)
         except requests.HTTPError as exc:
+            # typer.secho(exc.response.content)
             secho_error_and_exit(err_prefix + decode_http_err(exc))
 
     return wrapper
diff --git a/pfcli/service/client/billing.py b/pfcli/service/client/billing.py
index 19c0568..99a15a6 100644
--- a/pfcli/service/client/billing.py
+++ b/pfcli/service/client/billing.py
@@ -19,7 +19,7 @@ class TimeGranularity(str, Enum):
     week = "week"
 
 
-class BillingClientService(
+class PFTBillingClientService(
     ClientService, GroupRequestMixin, ProjectRequestMixin, UserRequestMixin
 ):
     def __init__(self, template: Template, **kwargs):
diff --git a/pfcli/service/client/deployment.py b/pfcli/service/client/deployment.py
new file mode 100644
index 0000000..1e876b8
--- /dev/null
+++ b/pfcli/service/client/deployment.py
@@ -0,0 +1,31 @@
+# Copyright (C) 2022 FriendliAI
+
+"""PeriFlow DeploymentClient Service"""
+
+from typing import List
+
+from pfcli.service.client.base import ClientService, safe_request
+
+
+class DeploymentClientService(ClientService[str]):
+    def get_deployment(self, deployment_id: str) -> dict:
+        response = safe_request(
+            self.retrieve,
+            err_prefix=f"Deployment ({deployment_id}) is not found. You may enter wrongID.",
+        )(pk=deployment_id)
+        return response.json()
+
+    def create_deployment(self, config: dict) -> dict:
+        response = safe_request(self.post, err_prefix="Failed to post new deployment.")(
+            json=config
+        )
+        return response.json()
+
+    def list_deployments(self) -> dict:
+        response = safe_request(self.list, err_prefix="Failed to list deployments.")()
+        return response.json()
+
+    def delete_deployment(self, deployment_id: str) -> None:
+        safe_request(self.delete, err_prefix="Failed to delete deployment.")(
+            pk=deployment_id
+        )
diff --git a/pfcli/service/client/group.py b/pfcli/service/client/group.py
index 6a0aeec..4d9957a 100644
--- a/pfcli/service/client/group.py
+++ b/pfcli/service/client/group.py
@@ -77,7 +77,7 @@ def list_projects(self) -> List[dict]:
         return paginated_get(get_response_dict)
 
 
-class GroupVMConfigClientService(ClientService, GroupRequestMixin):
+class PFTGroupVMConfigClientService(ClientService, GroupRequestMixin):
     def __init__(self, template: Template, **kwargs):
         self.initialize_group()
         super().__init__(template, group_id=self.group_id, **kwargs)
diff --git a/pfcli/service/client/project.py b/pfcli/service/client/project.py
index 1f15656..804a0ef 100644
--- a/pfcli/service/client/project.py
+++ b/pfcli/service/client/project.py
@@ -107,7 +107,7 @@ def create_dataset(
         return response.json()
 
 
-class ProjectVMQuotaClientService(ClientService, ProjectRequestMixin):
+class PFTProjectVMQuotaClientService(ClientService, ProjectRequestMixin):
     def __init__(self, template: Template, **kwargs):
         self.initialize_project()
         super().__init__(template, project_id=self.project_id, **kwargs)
@@ -164,7 +164,7 @@ def create_credential(
         return response.json()
 
 
-class ProjectVMConfigClientService(ClientService[int], ProjectRequestMixin):
+class PFTProjectVMConfigClientService(ClientService[int], ProjectRequestMixin):
     def __init__(self, template: Template, **kwargs):
         self.initialize_project()
         super().__init__(template, project_id=self.project_id, **kwargs)
diff --git a/pfcli/service/client/serve.py b/pfcli/service/client/serve.py
deleted file mode 100644
index f4826e9..0000000
--- a/pfcli/service/client/serve.py
+++ /dev/null
@@ -1,29 +0,0 @@
-# Copyright (C) 2022 FriendliAI
-
-"""PeriFlow ServeClient Service"""
-
-from typing import List
-
-from pfcli.service.client.base import ClientService, safe_request
-
-
-class ServeClientService(ClientService):
-    def get_serve(self, serve_id: str) -> dict:
-        response = safe_request(
-            self.retrieve,
-            err_prefix=f"Serve ({serve_id}) is not found. You may enter wrongID.",
-        )(pk=serve_id)
-        return response.json()
-
-    def create_serve(self, config=dict) -> dict:
-        response = safe_request(self.post, err_prefix="Failed to post new serve.")(
-            json=config
-        )
-        return response.json()
-
-    def list_serves(self) -> List[dict]:
-        response = safe_request(self.list, err_prefix="Failed to list serves.")()
-        return response.json()
-
-    def delete_serve(self, serve_id: str) -> None:
-        safe_request(self.delete, err_prefix="Failed to delete serve.")(pk=serve_id)
diff --git a/pfcli/service/config.py b/pfcli/service/config.py
index 3834b4d..87c6a12 100644
--- a/pfcli/service/config.py
+++ b/pfcli/service/config.py
@@ -25,6 +25,7 @@
     JobType,
     ServiceType,
     CredType,
+    EngineType,
     cred_type_map_inv,
     storage_region_map,
 )
@@ -153,6 +154,38 @@
 """
 
 
+INFERENCE_SERVER_CONFIG = """
+# Inference server type config
+inference_server_type:
+  name:
+  repo:
+  tag:
+"""
+
+ORCA_CONFIG = """
+# Orca engine config
+orca_config:
+  max_batch_size:
+  max_token_count:
+  kv_cache_size:
+"""
+
+CKPT_CONFIG = """
+# Orca ckeckpoint config
+ckpt_config:
+  version:
+  catagory:
+  data_type:
+"""
+
+SCALER_CONFIG = """
+# Keda scaler config
+scaler_config:
+  min_deployment_count:
+  max_deployment_count:
+"""
+
+
 J = TypeVar("J", bound="JobConfigService")
 D = TypeVar("D", bound="DataConfigService")
 T = TypeVar("T", bound=Union[str, Tuple[Any, ...]])
@@ -548,6 +581,56 @@ def start_interaction(self) -> None:
         self.ready = True
 
 
+@dataclass
+class DeploymentConfigService(InteractiveConfigMixin):
+    """Deployment template configuration service."""
+
+    ready: bool = False
+    use_specific_image: bool = False
+    use_ckpt_config: bool = False
+
+    def _render(self) -> str:
+        yaml_str = ""
+        if self.use_specific_image:
+            yaml_str += INFERENCE_SERVER_CONFIG
+        if self.use_ckpt_config:
+            yaml_str += CKPT_CONFIG
+
+        return yaml_str
+
+
+@dataclass
+class OrcaDeploymentConfigService(DeploymentConfigService):
+    """Orca deployment template configuration service"""
+
+    use_scaler: bool = False
+
+    def start_interaction(self):
+        self.use_specific_image = typer.confirm(
+            "Will you use specific orca docker image?",
+            prompt_suffix="\n>> ",
+        )
+        self.use_ckpt_config = typer.confirm(
+            "Will you use checkpoint config?",
+            prompt_suffix="\n>> ",
+        )
+        self.use_scaler = typer.confirm(
+            "Will you use keda autoscaler?",
+            prompt_suffix="\n>> ",
+        )
+        self.ready = True
+
+    def render(self) -> str:
+        assert self.ready
+
+        yaml_str = ORCA_CONFIG
+        yaml_str += self._render()
+        if self.use_scaler:
+            yaml_str += SCALER_CONFIG
+
+        return yaml_str
+
+
 def build_job_configurator(job_type: str) -> J:
     if job_type == "custom":
         configurator = CustomJobConfigService()
@@ -570,3 +653,12 @@ def build_data_configurator(job_type: JobType) -> D:
 
     configurator.start_interaction()
     return configurator
+
+
+def build_deployment_configurator(engine_type: EngineType) -> DeploymentConfigService:
+    if engine_type == EngineType.ORCA:
+        configurator = OrcaDeploymentConfigService()
+    else:
+        secho_error_and_exit("Only orca engine type is supported!")
+    configurator.start_interaction()
+    return configurator
diff --git a/pfcli/vm.py b/pfcli/vm.py
index d6eacd2..66ddda6 100644
--- a/pfcli/vm.py
+++ b/pfcli/vm.py
@@ -6,16 +6,16 @@
 
 import typer
 
-from pfcli.service import CloudType, ServiceType
+from pfcli.service import CloudType, PeriFlowService, ServiceType
 from pfcli.service.client import (
     GroupProjectClientService,
     GroupProjectVMQuotaClientService,
-    GroupVMConfigClientService,
-    ProjectVMConfigClientService,
-    ProjectVMQuotaClientService,
+    PFTGroupVMConfigClientService,
+    PFTProjectVMConfigClientService,
+    PFTProjectVMQuotaClientService,
     build_client,
 )
-from pfcli.service.client.project import find_project_id
+from pfcli.service.client.project import PFTProjectVMConfigClientService, find_project_id
 from pfcli.service.formatter import PanelFormatter, TableFormatter
 from pfcli.utils.format import secho_error_and_exit
 
@@ -75,27 +75,21 @@
 )
 
 
-@app.command("list")
-def list(
-    cloud: Optional[CloudType] = typer.Option(
-        None, "--cloud", "-c", help="Filter list by cloud vendor."
-    ),
-    device_type: Optional[str] = typer.Option(
-        None, "--device-type", "-d", help="Filter list by device type."
-    ),
-):
-    """List all VM quota information."""
-    vm_quota_client: ProjectVMQuotaClientService = build_client(
-        ServiceType.PROJECT_VM_QUOTA
+def vm_list_for_train(cloud: Optional[CloudType], device_type: Optional[str]) -> None:
+    """List all VM information for training."""
+    vm_quota_client: PFTProjectVMQuotaClientService = build_client(
+        ServiceType.PFT_PROJECT_VM_QUOTA
     )
-    vm_config_client: ProjectVMConfigClientService = build_client(
-        ServiceType.PROJECT_VM_CONFIG
+    vm_config_client: PFTProjectVMConfigClientService = build_client(
+        ServiceType.PFT_PROJECT_VM_CONFIG
     )
-    group_vm_config_client: GroupVMConfigClientService = build_client(
-        ServiceType.GROUP_VM_CONFIG
+    group_vm_config_client: PFTGroupVMConfigClientService = build_client(
+        ServiceType.PFT_GROUP_VM_CONFIG
     )
 
-    vm_dict_list = vm_quota_client.list_vm_quotas(vendor=cloud, device_type=device_type)
+    vm_dict_list = vm_quota_client.list_vm_quotas(
+        vendor=cloud, device_type=device_type
+    )
     vm_id_map = group_vm_config_client.get_vm_config_id_map()
     available_vm_dict_list = []
     for vm_dict in vm_dict_list:
@@ -114,22 +108,50 @@ def list(
     available_vm_dict_list = sorted(
         available_vm_dict_list, key=lambda d: d["vm_config_type"]["code"]
     )
-
     formatter.render(available_vm_dict_list)
 
 
+def vm_list_for_serve(cloud: Optional[CloudType], device_type: Optional[str]) -> None:
+    # TODO: FILL ME
+    secho_error_and_exit(
+        "VM list for the deployment is not supported yet. Please contact the support team."
+    )
+
+
+@app.command("list", help="list up available VMs")
+def list(
+    service: PeriFlowService = typer.Option(
+        ...,
+        "--service",
+        "-s",
+        help="PeriFlow service type. Training and serving services are provided with different VM types.",
+    ),
+    cloud: Optional[CloudType] = typer.Option(
+        None, "--cloud", "-c", help="Filter list by cloud vendor."
+    ),
+    device_type: Optional[str] = typer.Option(
+        None, "--device-type", "-d", help="Filter list by device type."
+    ),
+):
+    handler_map = {
+        PeriFlowService.TRAIN: vm_list_for_train,
+        PeriFlowService.SERVE: vm_list_for_serve,
+    }
+    handler_map[service](cloud, device_type)
+
+
 @quota_app.command("view", help="view quota detail of a VM")
 def view(
     vm_instance_name: str = typer.Argument(..., help="vm type"),
 ):
-    vm_quota_client: ProjectVMQuotaClientService = build_client(
-        ServiceType.PROJECT_VM_QUOTA
+    vm_quota_client: PFTProjectVMQuotaClientService = build_client(
+        ServiceType.PFT_PROJECT_VM_QUOTA
     )
-    vm_config_client: ProjectVMConfigClientService = build_client(
-        ServiceType.PROJECT_VM_CONFIG
+    vm_config_client: PFTProjectVMConfigClientService = build_client(
+        ServiceType.PFT_PROJECT_VM_CONFIG
     )
-    group_vm_config_client: GroupVMConfigClientService = build_client(
-        ServiceType.GROUP_VM_CONFIG
+    group_vm_config_client: PFTGroupVMConfigClientService = build_client(
+        ServiceType.PFT_GROUP_VM_CONFIG
     )
 
     vm_dict_list = vm_quota_client.list_vm_quotas()


Test patch:
diff --git a/test/service/client/test_billing.py b/test/service/client/test_billing.py
index 4b44c64..c037889 100644
--- a/test/service/client/test_billing.py
+++ b/test/service/client/test_billing.py
@@ -9,19 +9,19 @@
 
 from pfcli.service import ServiceType
 from pfcli.service.client import build_client
-from pfcli.service.client.billing import BillingClientService
+from pfcli.service.client.billing import PFTBillingClientService
 
 
 @pytest.fixture
-def billing_summary_client(user_project_group_context) -> BillingClientService:
-    return build_client(ServiceType.BILLING_SUMMARY)
+def billing_summary_client(user_project_group_context) -> PFTBillingClientService:
+    return build_client(ServiceType.PFT_BILLING_SUMMARY)
 
 
 @pytest.mark.usefixtures("patch_auto_token_refresh")
 def test_billing_summary_client_get_summary(
-    requests_mock: requests_mock.Mocker, billing_summary_client: BillingClientService
+    requests_mock: requests_mock.Mocker, billing_summary_client: PFTBillingClientService
 ):
-    assert isinstance(billing_summary_client, BillingClientService)
+    assert isinstance(billing_summary_client, PFTBillingClientService)
 
     url_template = deepcopy(billing_summary_client.url_template)
 
diff --git a/test/service/client/test_group.py b/test/service/client/test_group.py
index 25848e5..5adb061 100644
--- a/test/service/client/test_group.py
+++ b/test/service/client/test_group.py
@@ -21,7 +21,7 @@
     GroupClientService,
     GroupProjectCheckpointClientService,
     GroupProjectVMQuotaClientService,
-    GroupVMConfigClientService,
+    PFTGroupVMConfigClientService,
 )
 
 
@@ -31,8 +31,8 @@ def group_client(user_project_group_context) -> GroupClientService:
 
 
 @pytest.fixture
-def group_vm_config_client(user_project_group_context) -> GroupVMConfigClientService:
-    return build_client(ServiceType.GROUP_VM_CONFIG)
+def group_vm_config_client(user_project_group_context) -> PFTGroupVMConfigClientService:
+    return build_client(ServiceType.PFT_GROUP_VM_CONFIG)
 
 
 @pytest.fixture
@@ -142,7 +142,7 @@ def test_group_client_accept_invite(
 @pytest.mark.usefixtures("patch_auto_token_refresh")
 def test_group_vm_config_client_get_id_by_name(
     requests_mock: requests_mock.Mocker,
-    group_vm_config_client: GroupVMConfigClientService,
+    group_vm_config_client: PFTGroupVMConfigClientService,
 ):
     example_data = [
         {
@@ -200,9 +200,9 @@ def test_group_vm_config_client_get_id_by_name(
 @pytest.mark.usefixtures("patch_auto_token_refresh")
 def test_group_vm_config_client_get_vm_config_id_map(
     requests_mock: requests_mock.Mocker,
-    group_vm_config_client: GroupVMConfigClientService,
+    group_vm_config_client: PFTGroupVMConfigClientService,
 ):
-    assert isinstance(group_vm_config_client, GroupVMConfigClientService)
+    assert isinstance(group_vm_config_client, PFTGroupVMConfigClientService)
 
     # Success
     requests_mock.get(
diff --git a/test/service/client/test_project.py b/test/service/client/test_project.py
index 4fea1ee..344533c 100644
--- a/test/service/client/test_project.py
+++ b/test/service/client/test_project.py
@@ -13,8 +13,8 @@
 from pfcli.service.client.project import (
     ProjectCredentialClientService,
     ProjectDataClientService,
-    ProjectVMConfigClientService,
-    ProjectVMQuotaClientService,
+    PFTProjectVMConfigClientService,
+    PFTProjectVMQuotaClientService,
 )
 
 
@@ -31,15 +31,15 @@ def project_data_client(user_project_group_context) -> ProjectDataClientService:
 
 
 @pytest.fixture
-def project_vm_quota_client(user_project_group_context) -> ProjectVMQuotaClientService:
-    return build_client(ServiceType.PROJECT_VM_QUOTA)
+def project_vm_quota_client(user_project_group_context) -> PFTProjectVMQuotaClientService:
+    return build_client(ServiceType.PFT_PROJECT_VM_QUOTA)
 
 
 @pytest.fixture
 def project_vm_config_client(
     user_project_group_context,
-) -> ProjectVMConfigClientService:
-    return build_client(ServiceType.PROJECT_VM_CONFIG)
+) -> PFTProjectVMConfigClientService:
+    return build_client(ServiceType.PFT_PROJECT_VM_CONFIG)
 
 
 @pytest.mark.usefixtures("patch_auto_token_refresh")
@@ -141,7 +141,7 @@ def test_project_data_client_create_dataset(
 @pytest.mark.usefixtures("patch_auto_token_refresh")
 def test_project_vm_quota_client_list_vm_quotas(
     requests_mock: requests_mock.Mocker,
-    project_vm_quota_client: ProjectVMQuotaClientService,
+    project_vm_quota_client: PFTProjectVMQuotaClientService,
 ):
     example_data = [
         {
@@ -304,7 +304,7 @@ def test_project_credential_client_service(
 @pytest.mark.usefixtures("patch_auto_token_refresh")
 def test_project_vm_config_client_get_active_vm_count(
     requests_mock: requests_mock.Mocker,
-    project_vm_config_client: ProjectVMConfigClientService,
+    project_vm_config_client: PFTProjectVMConfigClientService,
 ):
     # Success
     url_template = deepcopy(project_vm_config_client.url_template)
diff --git a/test/service/client/test_serve.py b/test/service/client/test_serve.py
deleted file mode 100644
index dbb7add..0000000
--- a/test/service/client/test_serve.py
+++ /dev/null
@@ -1,157 +0,0 @@
-# Copyright (C) 2022 FriendliAI
-
-"""Test ServeClient Service"""
-
-
-from copy import deepcopy
-
-import pytest
-import requests_mock
-import typer
-
-from pfcli.service import ServiceType
-from pfcli.service.client import build_client
-from pfcli.service.client.serve import ServeClientService
-
-
-@pytest.fixture
-def serve_client() -> ServeClientService:
-    return build_client(ServiceType.SERVE)
-
-
-@pytest.mark.usefixtures("patch_auto_token_refresh")
-def test_serve_client_get_serve(
-    requests_mock: requests_mock.Mocker, serve_client: ServeClientService
-):
-    assert isinstance(serve_client, ServeClientService)
-
-    url_template = deepcopy(serve_client.url_template)
-    url_template.attach_pattern("$serve_id/")
-
-    # Success
-    requests_mock.get(
-        url_template.render(serve_id=0),
-        json={
-            "id": 0,
-            "name": "name",
-            "status": "running",
-            "vm": "aws-g4dn.12xlarge",
-            "gpu_type": "t4",
-            "num_gpus": 1,
-            "start": "2022-04-18T05:55:14.365021Z",
-            "endpoint": "http:0.0.0.0:8000/0/v1/completions",
-        },
-    )
-    assert serve_client.get_serve(0) == {
-        "id": 0,
-        "name": "name",
-        "status": "running",
-        "vm": "aws-g4dn.12xlarge",
-        "gpu_type": "t4",
-        "num_gpus": 1,
-        "start": "2022-04-18T05:55:14.365021Z",
-        "endpoint": "http:0.0.0.0:8000/0/v1/completions",
-    }
-
-    requests_mock.get(url_template.render(serve_id=0), status_code=404)
-    with pytest.raises(typer.Exit):
-        serve_client.get_serve(0)
-
-
-@pytest.mark.usefixtures("patch_auto_token_refresh")
-def test_serve_client_create_serve(
-    requests_mock: requests_mock.Mocker, serve_client: ServeClientService
-):
-    assert isinstance(serve_client, ServeClientService)
-
-    # Success
-    requests_mock.post(
-        serve_client.url_template.render(),
-        json={
-            "id": 0,
-            "name": "name",
-            "status": "running",
-            "vm": "aws-g4dn.12xlarge",
-            "gpu_type": "t4",
-            "num_gpus": 1,
-            "start": "2022-04-18T05:55:14.365021Z",
-            "endpoint": "http:0.0.0.0:8000/0/v1/completions",
-        },
-    )
-
-    assert serve_client.create_serve({}) == {
-        "id": 0,
-        "name": "name",
-        "status": "running",
-        "vm": "aws-g4dn.12xlarge",
-        "gpu_type": "t4",
-        "num_gpus": 1,
-        "start": "2022-04-18T05:55:14.365021Z",
-        "endpoint": "http:0.0.0.0:8000/0/v1/completions",
-    }
-
-    requests_mock.post(serve_client.url_template.render(), status_code=404)
-    with pytest.raises(typer.Exit):
-        serve_client.create_serve({})
-
-
-@pytest.mark.usefixtures("patch_auto_token_refresh")
-def test_serve_client_list_serve(
-    requests_mock: requests_mock.Mocker, serve_client: ServeClientService
-):
-    assert isinstance(serve_client, ServeClientService)
-
-    # Success
-    requests_mock.get(
-        serve_client.url_template.render(),
-        json=[
-            {
-                "id": 0,
-                "name": "name",
-                "status": "running",
-                "vm": "aws-g4dn.12xlarge",
-                "gpu_type": "t4",
-                "num_gpus": 1,
-                "start": "2022-04-18T05:55:14.365021Z",
-                "endpoint": "http:0.0.0.0:8000/0/v1/completions",
-            }
-        ],
-    )
-
-    assert serve_client.list_serves() == [
-        {
-            "id": 0,
-            "name": "name",
-            "status": "running",
-            "vm": "aws-g4dn.12xlarge",
-            "gpu_type": "t4",
-            "num_gpus": 1,
-            "start": "2022-04-18T05:55:14.365021Z",
-            "endpoint": "http:0.0.0.0:8000/0/v1/completions",
-        }
-    ]
-
-    requests_mock.get(serve_client.url_template.render(), status_code=404)
-    with pytest.raises(typer.Exit):
-        serve_client.list_serves()
-
-
-@pytest.mark.usefixtures("patch_auto_token_refresh")
-def test_serve_client_delete_serve(
-    requests_mock: requests_mock.Mocker, serve_client: ServeClientService
-):
-    assert isinstance(serve_client, ServeClientService)
-
-    url_template = deepcopy(serve_client.url_template)
-    url_template.attach_pattern("$serve_id/")
-
-    # Success
-    requests_mock.delete(url_template.render(serve_id=0), status_code=204)
-    try:
-        serve_client.delete_serve(0)
-    except typer.Exit:
-        raise pytest.fail("serve client test failed.")
-
-    requests_mock.delete(url_template.render(serve_id=0), status_code=404)
-    with pytest.raises(typer.Exit):
-        serve_client.delete_serve(0)