Deployments
When specifying an operator preference for deployment planning, use English, such as
prefer prefix cache, more GPU for performance, or use fewer GPU resources.
The deterministic planner matches English preference keywords; Chinese keyword aliases
are no longer supported.
Python domain contracts
The models in llm_d_lens.deploy.contracts and the
llm_d_lens.deploy.service.DeploymentService protocol define the internal
Configuration-to-Deploy boundary. They are not additional HTTP endpoints.
Configuration owns validation/generation of immutable configuration artifacts;
Deploy owns provider execution and lifecycle evidence. Evaluation owns workload
validation and benchmark execution; Simulation does not own deployment lifecycle.
Commands and immutable artifacts
| Contract | Fields and constraints |
|---|---|
VersionedPayload | Nonempty schema_version; object value defaults to {}. The envelope does not prescribe producer-owned keys. |
ConfigurationArtifact | artifact_id defaults to a UUID; nonempty schema_version, kind, provider_ref, checksum; media_type defaults to application/yaml; at least one of content or content_ref is required. |
DeploymentCreateRequest | UUID request_id; input_schema_version="v1"; nonempty configuration_artifacts; deployment_policy defaults to a non-null VersionedPayload(schema_version="v1", value={}); optional cluster_snapshot_ref/cluster_snapshot; object provenance. |
DeploymentKillRequest | UUID request_id; input_schema_version="v1"; required nonempty execution_id; optional expected_namespace/reason; object provenance. |
DeploymentArtifact | UUID artifact_id; nonempty artifact_hash and configuration_artifact_ids; optional source_ref, manifest_ref, manifest_checksum, values_checksum; required rendered_payload; created_at. |
provider_ref selects the deployment provider; it does not transfer configuration
ownership to Deploy. The current GuideAdapterDeploymentService is narrower than
the generic DTO: each create call requires exactly one artifact containing an
inline JSON object with media_type="application/json". The run planner splits
the public run request into cases; do not pass a generic YAML/content-reference
artifact directly to this adapter and assume it supports it.
create(request, *, model_token=None, on_initial_execution=None) returns
DeploymentExecution. Runtime binding does not change configuration ownership.
kill(request, execution) verifies matching execution IDs, the expected namespace
when supplied, and provider state managed by that service before cleanup. It
does not accept arbitrary resource selectors.
Execution, observation and evidence
DeploymentExecution carries execution_id, request_id, status, artifact,
configuration_artifacts, optional endpoint/namespace/resource_snapshot,
provenance, evidence_refs, optional versioned diagnostics and timestamps.
Additional current fields are forwarded_endpoint, monitoring_setup (setup
audit, not live monitoring status), data_plane, and editable descriptive
metadata (display_name, description), separate from origin provenance.
The complete run/case/execution state enums are listed below. A ready execution
must contain DeploymentEndpoint: url, protocol="http" and optional
service_ref, model_ref, baseline_url. Endpoint metadata must not carry
credentials, kubeconfig or authorization values.
get(execution_id) returns DeploymentExecution, not another status DTO.
The concrete service looks up its in-memory execution state and raises KeyError
for unknown IDs; it is not itself a database query. The worker/DAO boundary owns
durable records and restores execution context when needed. The protocol also
exposes restore_execution, refresh_diagnostics, stop and clean; use the
authenticated REST operations below for external clients.
get_logs(DeploymentLogRequest(...)) returns a DeploymentLogPage, separate from
execution metadata:
- Request:
execution_id, optional nonnegative integer-stringcursor,limitfrom 1 to 1000 (default 100),follow=False, andsources=[]. - Current sources are
workloadandevents, not arbitrary Kubernetes selectors. Entries containtimestamp,source,messageand optionalseverity. - Response:
execution_id,entries,next_cursor,complete,evidence_refs.follow=Truesetscomplete=False; this method still returns one page, not a streaming iterator or a promise to wait for new logs. - Archived diagnostics use
ExecutionLogArchiveanddeployment-log://<execution_id>/archivereferences. Preserve evidence separately from database records; see storage settings. Retained snapshots are partial diagnostics, not a complete cluster log history.
Consumer projections
These are pure DTO helpers, not lifecycle calls or REST permission checks.
benchmark_input() requires ready plus an endpoint and otherwise raises
ValueError. It projects execution ID, artifact, endpoint, namespace and provenance.
simulation_input() accepts any execution state and additionally projects
deployment status and the optional resource snapshot/endpoint. It preserves
execution evidence rather than substituting current mutable cluster state.
The helpers exist, but current Evaluation/Simulation routes are not direct
callers; do not describe these projections as their active request API.
from llm_d_lens.deploy.contracts import (
BenchmarkDeploymentInput,
DeploymentExecution,
SimulationDeploymentInput,
)
def benchmark_projection(execution: DeploymentExecution) -> BenchmarkDeploymentInput:
return execution.benchmark_input()
def simulation_projection(execution: DeploymentExecution) -> SimulationDeploymentInput:
return execution.simulation_input()
Hardware rendering contract
Deployment capability metadata describes provider behavior; its variants array is
empty because variants belong to the selected source catalog. Rendering and
Evaluation use the resolved hardware profile for images, accelerator resources and
scheduling. Legacy configurations can resolve hardware from their rendered DRA or
extended-resource requests; conflicting hardware evidence is rejected.
Python callers can inspect the same source entry points before rendering:
from pathlib import Path
from llm_d_lens.deploy.providers.hardware_profile import guide_overlays
variants = guide_overlays(
Path("/path/to/llm-d"), "optimized-baseline", accelerator="nvidia",
)
print(sorted(variants))
Backend API
Prepare Guide source (Node)
POST /api/guide-planning/prepare accepts guide, accelerator, modelServer,
guideVariant and the selected clusterId. It prepares the official source
without deploying and returns its commit plus manifest-derived controls:
{"ready": true, "commit": "selected-source-commit", "capabilities": {"cacheCpuGiB": false, "rdmaNicCount": true}}
The booleans describe supported CPU-cache mutation and a matching profile NIC
claim. They do not depend on variant names. This endpoint is owned by
apps/server/src/routes/guidePlanning.ts; there is no Python callable for this Node operation.
Python deployment providers use guide_overlays (above) and
llm_d_lens.hardware.resolver.deployment_mode(profile, access_mode) for the
corresponding source and hardware contracts.
Agentic create and refinement streams use llm_d_lens.agentic.streaming.planning_stream_response. The routes retain principal resolution, expected-error classification, and their existing generic error messages. Streams emit progress followed by complete or error; disconnect cleanup cancels and awaits unfinished planning. Gateway operation/log streams remain separate.
Recalculating an Agentic plan does not require an AI provider. Without one,
AgenticDeploymentService.refine() refreshes planning facts and uses deterministic
candidate generation and scoring with the new preference. If an available AI
scorer fails, recalculation keeps the validated candidates and returns
deterministically scored alternatives instead.
Agentic candidate planning and approval revalidation use the deployment request's
max_model_len and gpu_memory_utilization for memory and KV-cache checks, even
when the supplied planning_facts contains different values. Deterministic and
AI-generated candidates use the same utilization as the deployed configuration.
PD deployments apply that utilization to both prefill and decode vLLM roles.
The estimate uses GPU capacity and model overhead, not vLLM's measured free KV
cache at startup.
Deployments live under the FastAPI router in llm_d_lens/deploy/router.py with the /api/v1/deployments prefix.
Use the installation instructions and
storage environment settings for the current monorepo
and Lens storage roots. The old .llm-d-bench/deploy and host-specific
/root/.llm_d_lens/run_store paths are not the current storage contract.
Execution discovery is read-only and uses execution_id as its cross-module
reference; listing does not start port-forwards. Use the execution endpoint
action to obtain a host-reachable URL rather than assuming a persisted
in-cluster endpoint or old tunnel is reachable.
| Record | Lifecycle states |
|---|---|
| Run | queued, running, succeeded, partially_succeeded, failed, cleaned, cancelling, cancelled |
| Case | queued, rendering, deploying, ready, failed, stopped, cancelling, cancelled, cleaned, cleaned_up |
| Execution | draft, validated, rendered, deploying, ready, failed, rolling_back, rolled_back, cleaned, cleaned_up |
A stopped case has a rolled_back execution; these are different state enums.
Benchmarking requires a ready execution with an endpoint and an active cluster
session. Python worker examples below run inside a configured Lens backend;
external clients should use authenticated HTTP so route permissions and resource
scope checks apply.
Optimized and precise routing capability metadata reuse shared_prefix_routing_workload() in llm_d_lens.deploy.capabilities. Each scenario and recommended workload receives independent nested values; provider-specific baselines and goals are unchanged. Precise-provider calibration and model discovery share lazy modelserver invocation traversal while retaining their respective last-value and first-model selection rules.
Agentic candidate generation requires its first tool batch to contain exactly
get_cluster_overview and search_candidates. An invalid first batch is not executed;
the generator gives the AI one corrective retry before using built-in candidates. If
the retry also violates the tool contract, the fallback is reported as an invalid
generator response rather than a provider connectivity failure. A tool-call limit
failure retains the round, observed call count, and limit in
generator_fallback_detail on the run and generator.fallback_detail in its
planning snapshot; generator_fallback_reason remains the stable category.
Agentic planning estimates missing exact AIC predictions separately for each validated baseline-vllm,
optimized-baseline, and pd-disaggregation candidate using its workload and GPU topology. The
candidate's estimate is supplied to AI scoring as aic_estimate; an unavailable estimate remains
unknown, not a performance or SLO guarantee. Agentic estimates do not pass the original search
TTFT/TPOT targets; they verify the fixed topology and workload without an SLO filter. Generator
fallback also estimates the top three built-in candidates, using the resolved workload when the
failed generator left no tool trace. Unsupported or unavailable topologies remain unknown. Relaxed AIC
search accepts complete configurations using no more than the available GPU budget. Its predictions
do not establish compliance with the original SLO. Agentic preserves the relaxed anchor as a
validated candidate even when the generator suggests only a smaller topology, then estimates its
exact topology and workload. Candidates with known estimates that miss the original latency targets
are marked not_satisfied; among comparable misses, smaller relative target violations rank ahead
of GPU savings. Unknown estimates are not treated as failures.
When an external planner scores multiple candidates, its selection rationale explains why each
lower-scored returned candidate ranks below the top choice using the supplied planning facts. Only
the returned top three are scored; no reason is inferred for candidates outside that set.
Planning-fact collection matches discovered accelerator models against registered hardware profiles to select one AIC system. Unknown, ambiguous, or mixed systems leave AIC evidence unavailable.
AIC support checks and estimates use the SDK release pinned in pyproject.toml
(currently 0.12.0), including its bundled support matrix and performance data.
An unsupported P/D result describes that installed matrix, not a hardware-level
prohibition. The 0.12.0 matrix passes Qwen/Qwen3-0.6B on B60/vLLM for P/D;
candidate availability still depends on the requested workload and GPU budget.
After changing the dependency pin, reinstall project dependencies and restart
the backend so support checks and searches use the same SDK release.
Estimate an AIC topology
- REST API
- Python API
POST /api/v1/aic/estimate
Send model_name, scenario (inference_scheduling or pd_disaggregation),
gpu_count, mean_input_tokens, mean_output_tokens, the AIC system/backend/database
and the topology fields (tp and replicas, or prefill/decode TP and replicas).
Optional ttft_target_ms and tpot_target_ms retain the constrained experiment path;
omit both to estimate the fixed topology without an SLO filter.
{
"model_name": "Qwen/Qwen3-0.6B",
"scenario": "inference_scheduling",
"gpu_count": 1,
"mean_input_tokens": 1024,
"mean_output_tokens": 256,
"aic_system_name": "b60",
"aic_backend_name": "vllm",
"aic_database_mode": "SILICON",
"tp": 1,
"replicas": 1
}
The response includes mode, topology fields, ttft_ms, tpot_ms and
throughput_tokens_per_sec when measurable. Estimates do not prove SLO compliance.
from llm_d_lens.aic.models import AICEstimateRequest
from llm_d_lens.aic.service import estimate
result = await estimate(AICEstimateRequest(
model_name="Qwen/Qwen3-0.6B", scenario="inference_scheduling",
gpu_count=1, mean_input_tokens=1024, mean_output_tokens=256,
aic_system_name="b60", aic_backend_name="vllm", tp=1, replicas=1,
))
List deployment node labels
List the distinct key/value labels on nodes of a cluster the caller can access. Advanced placement uses these values to populate its optional node-label selector.
- REST API
- Python API
GET /api/v1/deployments/node-labels?cluster_id=cluster-a
Query: cluster_id (required cluster ID). Response:
{"items": [{"key": "pool", "value": "accelerator"}]}
from llm_d_bench.deploy.router import list_deployment_node_labels
labels = await list_deployment_node_labels(cluster_id="cluster-a", request=authenticated_request)
Start a deployment run
Start a deployment run from one or more already-rendered deployable configurations.
For Router 0.11 endpoint-picker images, planning and installation adapt legacy P/D
plugin settings: disagg-headers-handler is handled by disagg-profile-handler,
and parameters.deciderPluginName becomes parameters.deciders.prefill. This also
applies when retrying saved bundles. Source assets and their checksums remain
unchanged; deployment evidence records the derived values actually installed.
Conflicting old/new deciders and custom legacy header parameters are rejected.
Other Router versions and custom image repositories retain their own settings.
The same pure Python transformation is shared by configuration validation and
llm_d_bench.deploy.providers.deployment_bundle.install_deployment_bundle:
import yaml
from llm_d_bench.configuration.router_compatibility import compatible_router_values
values = yaml.safe_load(saved_router_values_yaml)
effective_values = compatible_router_values(values, chart_version="v0.11.0")
- REST API
- Python API
POST /api/v1/deployments/runs
Request body: DeploymentRunCreateRequest
configurations(DeployableConfiguration[], required): rendered deployment payloads to execute.failure_policy("stop" | "continue", default"continue"): whether later cases should continue after a failure.provenance(object, optional): caller-owned origin metadata.runtime_binding(RuntimeBinding, optional): environment overrides, cluster session binding, andplacement({"replicas":"prefer-same-node"|"same-node"|"balanced"|"spread"|"none","nodeLabel"?:{"key":string,"value":string}}). Model-server Deployments in the same rendered manifest share a placement group, including PD Prefill and Decode; router Pods are excluded.nodeLabelis accepted only withsame-nodeand must exist on a node in the cluster identified byprovenance.cluster_server_id. LeaderWorkerSet cannot usebalancedorspreadbecause its leader/worker Pods are not separate replicas. For optimized-baseline, placement is applied to the generated Kustomize overlay before deployment. Shared creation preflight requires a ready model and revision in registered Model Cache storage. Ordinary provider rendering sets Hugging Face offline flags; exact published manifests bypass that rendering and are not covered by the same final-manifest enforcement. Missing snapshots must be downloaded through Model Cache before deployment.
{
"configurations": [
{
"schema_version": "deployable-configuration.v1",
"type": "baseline",
"format": "helm",
"provider_ref": "optimized-baseline",
"checksum": "sha256:0f4f7e4b",
"content": {
"model": {"name": "Qwen/Qwen3-8B"},
"decode": {
"replicaCount": 1,
"tensorParallelSize": 1,
"maxModelLen": 4096
},
"runtime": {
"image": "ghcr.io/llm-d/llm-d-xpu:v0.9.0",
"imageMode": "pull-existing"
}
},
"provenance": {
"deployment_name": "qwen3-8b-smoke",
"description": "Deploy one optimized baseline for evaluation"
}
}
],
"failure_policy": "continue",
"provenance": {
"cluster_session_id": "123e4567-e89b-12d3-a456-426614174000",
"deployment_source": {"kind": "manual"}
},
"runtime_binding": {
"schema_version": "runtime-binding.v1",
"environment": {
"HTTP_PROXY": "http://proxy.example:911"
},
"cluster_session_id": "123e4567-e89b-12d3-a456-426614174000",
"placement": {"replicas": "same-node", "nodeLabel": {"key": "pool", "value": "accelerator"}}
}
}
Response: DeploymentRun
{
"id": "8c52df8b-5d31-4a36-8c01-3952abf2bf89",
"status": "queued",
"failure_policy": "continue",
"provenance": {
"cluster_session_id": "123e4567-e89b-12d3-a456-426614174000",
"deployment_source": {"kind": "manual"}
},
"created_at": "2026-09-23T09:18:24.287000+00:00",
"started_at": null,
"finished_at": null,
"source_configurations": [
{
"schema_version": "deployable-configuration.v1",
"type": "baseline",
"format": "helm",
"provider_ref": "optimized-baseline",
"checksum": "sha256:0f4f7e4b",
"content": {
"model": {"name": "Qwen/Qwen3-8B"},
"decode": {"replicaCount": 1, "tensorParallelSize": 1, "maxModelLen": 4096},
"runtime": {"image": "ghcr.io/llm-d/llm-d-xpu:v0.9.0", "imageMode": "pull-existing"}
},
"provenance": {"deployment_name": "qwen3-8b-smoke", "description": "Deploy one optimized baseline for evaluation"}
}
],
"cases": [
{
"id": "1f31e4de-5452-4b6c-a38f-d404ed5eff10",
"run_id": "8c52df8b-5d31-4a36-8c01-3952abf2bf89",
"ordinal": 0,
"source_configuration_ordinal": 0,
"provider_ref": "optimized-baseline",
"component": "decode",
"depends_on": [],
"status": "queued",
"attempt": 1,
"execution_id": null,
"failure": null,
"create_request": {
"request_id": "58b85f84-c8ef-47e1-8cb9-61831eb9d310",
"input_schema_version": "v1",
"configuration_artifacts": [
{
"artifact_id": "524a4178-7e9f-49ea-bccd-8ddb163ae2fd",
"schema_version": "deployable-configuration.v1",
"kind": "configuration",
"provider_ref": "optimized-baseline",
"media_type": "application/yaml",
"checksum": "sha256:0f4f7e4b",
"content": "{\"model\":{\"name\":\"Qwen/Qwen3-8B\"}}"
}
],
"deployment_policy": {"schema_version": "v1", "value": {"deployment_name": "qwen3-8b-smoke"}},
"cluster_snapshot_ref": null,
"cluster_snapshot": null,
"provenance": {}
}
}
]
}
import asyncio
from llm_d_lens.deploy.application import deployment_run_manager
from llm_d_lens.deploy.contracts import DeploymentRunCreateRequest
from llm_d_lens.deploy.standard_kubernetes_service import build_standard_kubernetes_service_configuration
from llm_d_lens.deploy.standard_kubernetes_service import StandardKubernetesServiceRequest
async def main() -> None:
configuration = build_standard_kubernetes_service_configuration(
StandardKubernetesServiceRequest(
model="Qwen/Qwen3-8B",
deployment_name="qwen3-8b-smoke",
cluster_session_id="123e4567-e89b-12d3-a456-426614174000",
replicas=1,
tensor_parallel_size=1,
)
)
run = await deployment_run_manager.start_run(
DeploymentRunCreateRequest(
configurations=[configuration],
provenance={
"cluster_session_id": "123e4567-e89b-12d3-a456-426614174000",
"deployment_source": {"kind": "manual"},
},
)
)
print(run.id, run.status)
asyncio.run(main())
Start a standard vLLM deployment run
Start a Standard Kubernetes Service deployment run using Lens's allowlisted vLLM contract.
The generated namespace uses the backend's configured deployment prefix (for example, llmd-baseline-vllm-...).
Standard vLLM deployments use the baseline adapter for rollout readiness and cleanup; they do not require a published Guide manifest's readinessDeployments contract.
Only registered Model Cache storage is accepted. It requires a ready entry for the selected model and serves it offline using the entry's downloaded revision; a conflicting custom vLLM revision is rejected. Requests using local-cache or direct pvc storage fail validation. Shared creation preflight also revalidates model/revision readiness and local-disk node synchronization through llm_d_lens.deploy.providers.storage_mount.require_cached_model. Ordinary provider rendering mounts the registered cache read-only for serving; cacheOffline=false cannot disable its offline flags. Exact published manifests are not covered by those rendering guarantees. Missing or incomplete models must be downloaded or synchronized through Model Cache.
The generated run defaults to preferring replicas on one node without making it mandatory. placement_replicas also supports required co-location, balanced distribution, or required separation across model-server Pods in the same rendered manifest. For required co-location, placement_node_label can restrict the Pods to a selected cluster node by its kubernetes.io/hostname label; the label is checked against that cluster before deployment. Tensor-parallel accelerator claims are requested by each model-server Pod, keeping its TP devices in that Pod. Agentic planning inherits the same fields and applies the selected preset on approval.
- REST API
- Python API
POST /api/v1/deployments/standard-vllm-runs
Request body: StandardKubernetesServiceRequest
model(string, required)deployment_name(string, required)description(string, optional)cluster_session_id(string, required)replicas(integer, required)tensor_parallel_size(integer, required)placement_replicas("prefer-same-node" | "same-node" | "balanced" | "spread", default"prefer-same-node")placement_node_label({"key":"kubernetes.io/hostname","value":"node-name"}ornull, optional; requiresplacement_replicas: "same-node")storage_type("model-cache", default"model-cache")storage_volume_id(string, required, nonempty registered Model Cache volume ID)model_cache_path,pvc_name(legacy fields, no longer used for deployment storage)max_model_len,gpu_memory_utilization,max_num_seqs,max_num_batched_tokensenable_prefix_caching,enforce_eager,vllm_arguments,use_cluster_proxy,http_proxy,https_proxy,no_proxy
{
"model": "Qwen/Qwen3-8B",
"deployment_name": "qwen3-standard",
"description": "Standard baseline for smoke tests",
"cluster_session_id": "123e4567-e89b-12d3-a456-426614174000",
"replicas": 1,
"tensor_parallel_size": 1,
"storage_type": "model-cache",
"storage_volume_id": "storage-model-cache",
"max_model_len": 8192,
"gpu_memory_utilization": 0.9,
"max_num_seqs": 256,
"max_num_batched_tokens": 8192,
"enable_prefix_caching": true,
"enforce_eager": false,
"vllm_arguments": [
{"name": "max-model-len", "value": "8192"}
],
"use_cluster_proxy": true,
"http_proxy": "",
"https_proxy": "",
"no_proxy": ""
}
Response: DeploymentRun
{
"id": "73606e29-a348-4920-bf8c-3be5e038a8fe",
"status": "queued",
"failure_policy": "continue",
"provenance": {
"deployment_source": {"kind": "standard-kubernetes-service"},
"cluster_session_id": "123e4567-e89b-12d3-a456-426614174000",
"deployment_name": "qwen3-standard",
"description": "Standard baseline for smoke tests",
"model_market": {
"deployment_name": "qwen3-standard",
"description": "Standard baseline for smoke tests"
}
},
"created_at": "2026-09-23T09:18:24.287000+00:00",
"source_configurations": [
{
"schema_version": "deployable-configuration.v1",
"type": "baseline",
"format": "helm",
"provider_ref": "baseline-vllm",
"checksum": "sha256:9eb2d61d",
"content": {
"model": {"name": "Qwen/Qwen3-8B"},
"decode": {"replicaCount": 1, "tensorParallelSize": 1, "maxModelLen": 8192},
"runtime": {
"image": "ghcr.io/llm-d/llm-d-xpu:v0.9.0",
"imageMode": "pull-existing",
"modelSource": "auto-cache",
"storageVolumeId": "storage-model-cache",
"cacheOffline": true
},
"customParameters": [
{"target": "decode", "kind": "argument", "name": "gpu-memory-utilization", "value": "0.9"},
{"target": "decode", "kind": "argument", "name": "enable-prefix-caching", "value": "true"}
]
},
"provenance": {
"source": "standard-kubernetes-service.v1",
"deployment_name": "qwen3-standard",
"description": "Standard baseline for smoke tests",
"storage_type": "model-cache"
}
}
],
"cases": []
}
import asyncio
from llm_d_lens.deploy.application import deployment_run_manager
from llm_d_lens.deploy.contracts import DeploymentRunCreateRequest
from llm_d_lens.deploy.standard_kubernetes_service import (
StandardKubernetesServiceRequest,
build_standard_kubernetes_service_configuration,
)
async def main() -> None:
request = StandardKubernetesServiceRequest(
model="Qwen/Qwen3-8B",
deployment_name="qwen3-standard",
description="Standard baseline for smoke tests",
cluster_session_id="123e4567-e89b-12d3-a456-426614174000",
storage_volume_id="storage-model-cache",
replicas=1,
tensor_parallel_size=1,
)
configuration = build_standard_kubernetes_service_configuration(request)
run = await deployment_run_manager.start_run(
DeploymentRunCreateRequest(
configurations=[configuration],
provenance={
"deployment_source": {"kind": "standard-kubernetes-service"},
"cluster_session_id": request.cluster_session_id,
"cluster_server_id": "cluster-123",
"deployment_name": request.deployment_name,
"description": request.description.strip(),
},
)
)
print(run.id, run.status)
asyncio.run(main())
List deployment runs
List deployment runs Lens has recorded.
GET /api/v1/deployments is the collection-root alias with the same query
parameter and response. Results are returned newest first.
- REST API
- Python API
GET /api/v1/deployments/runs
Request: no body.
- Query parameter:
query(string, optional) searches by run id, provider ref, deployment name, or namespace.
Response: array of DeploymentRun records.
[
{
"id": "8c52df8b-5d31-4a36-8c01-3952abf2bf89",
"status": "running",
"failure_policy": "continue",
"provenance": {
"cluster_server_id": "cluster-1",
"cluster_session_id": "123e4567-e89b-12d3-a456-426614174000"
},
"created_at": "2026-09-23T09:18:24.287000+00:00",
"started_at": "2026-09-23T09:18:40.001000+00:00",
"finished_at": null,
"source_configurations": [],
"cases": []
}
]
from llm_d_lens.deploy.router import _store
runs = _store.list_runs()
for run in runs:
print(run.id, run.status)
Get a deployment run
Get the full detail of one deployment run, including all cases and their current status.
- REST API
- Python API
GET /api/v1/deployments/runs/{run_id}
Request: no body.
- Path parameter:
run_id(string, required)
Response: DeploymentRun
{
"id": "8c52df8b-5d31-4a36-8c01-3952abf2bf89",
"status": "running",
"failure_policy": "continue",
"provenance": {
"cluster_server_id": "cluster-1",
"cluster_session_id": "123e4567-e89b-12d3-a456-426614174000"
},
"created_at": "2026-09-23T09:18:24.287000+00:00",
"started_at": "2026-09-23T09:18:40.001000+00:00",
"finished_at": null,
"source_configurations": [
{
"schema_version": "deployable-configuration.v1",
"type": "baseline",
"format": "helm",
"provider_ref": "baseline-vllm",
"checksum": "sha256:9eb2d61d",
"content": {"model": {"name": "Qwen/Qwen3-8B"}},
"provenance": {"deployment_name": "qwen3-standard"}
}
],
"cases": [
{
"id": "1f31e4de-5452-4b6c-a38f-d404ed5eff10",
"run_id": "8c52df8b-5d31-4a36-8c01-3952abf2bf89",
"ordinal": 0,
"source_configuration_ordinal": 0,
"provider_ref": "baseline-vllm",
"component": "decode",
"depends_on": [],
"status": "deploying",
"attempt": 1,
"execution_id": "execution-123",
"failure": null,
"create_request": {
"request_id": "58b85f84-c8ef-47e1-8cb9-61831eb9d310",
"input_schema_version": "v1",
"configuration_artifacts": [],
"deployment_policy": {"schema_version": "v1", "value": {"deployment_name": "qwen3-standard"}},
"cluster_snapshot_ref": null,
"cluster_snapshot": null,
"provenance": {}
}
}
]
}
from llm_d_lens.deploy.router import _store
run = _store.get_run("8c52df8b-5d31-4a36-8c01-3952abf2bf89")
if run is not None:
print(run.id, run.status, len(run.cases))
Cancel a deployment run
Request cancellation of an active deployment run.
- REST API
- Python API
POST /api/v1/deployments/runs/{run_id}/cancel
Request: no body.
- Path parameter:
run_id(string, required)
Response:
{
"id": "8c52df8b-5d31-4a36-8c01-3952abf2bf89",
"status": "cancelling"
}
from llm_d_lens.deploy.application import deployment_run_manager
run_id = "8c52df8b-5d31-4a36-8c01-3952abf2bf89"
deployment_run_manager.worker_for_run(run_id).request_cancel(run_id)
Rebind a deployment run's cluster session
Rebind a deployment run to a different active cluster session.
- REST API
- Python API
POST /api/v1/deployments/runs/{run_id}/cluster-session
Request body: RebindClusterSessionRequest
- Path parameter:
run_id(string, required) cluster_session_id(string, required)
{
"cluster_session_id": "123e4567-e89b-12d3-a456-426614174111"
}
Response: HTTP 204 No Content. No JSON body is returned.
from llm_d_lens.deploy.application import deployment_run_manager
run_id = "8c52df8b-5d31-4a36-8c01-3952abf2bf89"
deployment_run_manager.rebind_cluster_session(
run_id,
"123e4567-e89b-12d3-a456-426614174111",
)
List deployment executions
List deployment executions without creating new port-forwards.
This route reads stored records; it does not query the cluster itself. A
background reconciler keeps those records current, re-checking every run that is
running, succeeded, partially succeeded or failed (a failed rollout whose
Pods later recover is promoted back to ready, and one whose namespace is gone
becomes cleaned). Only cleaned and cancelled runs are terminal. Call
POST /api/v1/deployments/runs/{run_id}/cases/{case_id}/refresh when you need an
immediate re-check instead of waiting for that cycle.
- REST API
- Python API
GET /api/v1/deployments/executions
Request: no body.
- Query parameters:
status(DeploymentStatus, optional),statuses(DeploymentStatus[], optional),cluster_id(string, optional),query(string, optional),limit(integer, default200).
Response:
{
"items": [
{
"execution_id": "execution-ready",
"status": "ready",
"cluster_id": "cluster-1",
"cluster_session_id": "session-1",
"namespace": "namespace",
"name": "qwen",
"display_name": "",
"description": "",
"model": "Qwen",
"backend": "vLLM",
"guide": "optimized-baseline",
"endpoint": "http://execution-ready.namespace.svc:8000",
"forwarded_endpoint": null,
"replicas": null,
"tensor_parallel_size": null,
"image": null,
"preserve_deployment": true,
"created_at": "2026-09-23T09:18:24.287000+00:00",
"updated_at": null,
"max_model_len": null,
"gpu_memory_utilization": null,
"enable_prefix_caching": null,
"max_num_seqs": null,
"max_num_batched_tokens": null,
"storage_type": null,
"storage_volume_id": null,
"mount_path": null,
"pvc_name": null,
"failure": null,
"custom_parameters": [],
"baseline_endpoint": null,
"service_ref": null
}
]
}
from llm_d_lens.deploy.contracts import DeploymentStatus
from llm_d_lens.deploy.executions import list_execution_contexts
contexts = list_execution_contexts(status=DeploymentStatus.READY, cluster_id="cluster-1", limit=20)
for context in contexts:
print(context.execution_id, context.status, context.endpoint)
Get a deployment execution
Get one deployment execution by execution id.
- REST API
- Python API
GET /api/v1/deployments/executions/{execution_id}
Request: no body.
- Path parameter:
execution_id(string, required)
Response:
{
"execution_id": "execution-ready",
"status": "ready",
"cluster_id": "cluster-1",
"cluster_session_id": "session-1",
"namespace": "namespace",
"name": "qwen",
"display_name": "",
"description": "",
"model": "Qwen",
"backend": "vLLM",
"guide": "optimized-baseline",
"endpoint": "http://execution-ready.namespace.svc:8000",
"forwarded_endpoint": null,
"preserve_deployment": true,
"created_at": "2026-09-23T09:18:24.287000+00:00",
"custom_parameters": []
}
from llm_d_lens.deploy.executions import find_execution_context
context = find_execution_context("execution-ready")
if context is not None:
print(context.execution_id, context.namespace, context.endpoint)
Get execution pod status
Get live pod status for all pods in a deployment execution's namespace.
- REST API
- Python API
GET /api/v1/deployments/executions/{execution_id}/pods
Request: no body.
- Path parameter:
execution_id(string, required)
Response:
{
"execution_id": "exec-123",
"namespace": "prism-exec-123",
"pods": [
{
"name": "pod-1",
"namespace": "prism-exec-123",
"phase": "Running",
"ready": true,
"pod_ip": "10.244.0.5",
"node_name": "worker-1",
"restarts": 0,
"status_reason": "Running",
"containers": [
{
"name": "vllm",
"image": "vllm:latest",
"ready": true,
"restart_count": 0,
"state": "running"
}
],
"creation_timestamp": "2026-09-06T00:00:00Z"
}
]
}
import asyncio
from llm_d_lens.deploy.executions import find_execution_context
from llm_d_lens.utils.kubernetes import run_kubectl
async def main() -> None:
context = find_execution_context("exec-123")
if context is None or context.namespace is None:
return
result = await run_kubectl(
["get", "pods", "--namespace", context.namespace, "-o", "json"],
cluster_id=context.cluster_id,
timeout=15,
)
print(result.stdout)
asyncio.run(main())
Get pod logs for an execution
Get logs for one pod inside a deployment execution's namespace.
- REST API
- Python API
GET /api/v1/deployments/executions/{execution_id}/pods/{pod_name}/logs
Request: no body.
- Path parameters:
execution_id(string, required),pod_name(string, required) - Query parameters:
container(string, optional),tail(integer, default200),previous(boolean, defaultfalse)
Response:
{
"execution_id": "exec-123",
"namespace": "prism-exec-123",
"pod_name": "pod-1",
"container": "vllm",
"logs": "[INFO] Server started\n[INFO] Listening on :8000",
"success": true
}
import asyncio
from llm_d_lens.deploy.executions import find_execution_context
from llm_d_lens.utils.kubernetes import run_kubectl
async def main() -> None:
context = find_execution_context("exec-123")
if context is None or context.namespace is None:
return
result = await run_kubectl(
["logs", "pod-1", "--namespace", context.namespace, "--tail=200", "--container=vllm"],
cluster_id=context.cluster_id,
timeout=20,
)
print(result.stdout)
asyncio.run(main())
Connect to a deployment execution endpoint
Create or reuse a reachable forwarded endpoint for a deployment execution and return the local URL.
- REST API
- Python API
POST /api/v1/deployments/executions/{execution_id}/endpoint
Request: no body.
- Path parameter:
execution_id(string, required)
Response:
{
"success": true,
"execution_id": "execution-ready",
"id": "forward-1",
"local_port": 38000,
"endpoint": "http://127.0.0.1:38000/v1",
"forwarded_endpoint": "http://127.0.0.1:38000/v1",
"namespace": "namespace",
"service": "qwen-router",
"remote_port": 8000
}
import asyncio
from llm_d_lens.deploy.executions import ensure_execution_endpoint
async def main() -> None:
resolved = await ensure_execution_endpoint("execution-ready")
print(resolved.execution_id, resolved.endpoint)
asyncio.run(main())
Get a deployment case
Get one deployment case within a deployment run, including its associated execution and deployment summary payloads.
- REST API
- Python API
GET /api/v1/deployments/runs/{run_id}/cases/{case_id}
Request: no body.
- Path parameters:
run_id(string, required),case_id(string, required)
Response:
{
"case": {
"id": "case-execution-ready",
"run_id": "run-1",
"ordinal": 0,
"source_configuration_ordinal": 0,
"provider_ref": "optimized-baseline",
"component": "decode",
"depends_on": [],
"status": "ready",
"attempt": 1,
"execution_id": "execution-ready",
"failure": null,
"create_request": {
"request_id": "58b85f84-c8ef-47e1-8cb9-61831eb9d310",
"input_schema_version": "v1",
"configuration_artifacts": [
{
"artifact_id": "524a4178-7e9f-49ea-bccd-8ddb163ae2fd",
"schema_version": "deployable-configuration.v1",
"kind": "configuration",
"provider_ref": "optimized-baseline",
"media_type": "application/yaml",
"checksum": "sha256:0f4f7e4b",
"content": "{\"model\":{\"name\":\"Qwen\"}}"
}
],
"deployment_policy": {"schema_version": "v1", "value": {"deployment_name": "qwen"}},
"cluster_snapshot_ref": null,
"cluster_snapshot": null,
"provenance": {}
}
},
"execution": {
"execution_id": "execution-ready",
"request_id": "58b85f84-c8ef-47e1-8cb9-61831eb9d310",
"status": "ready",
"artifact": {
"artifact_id": "art-1",
"artifact_hash": "sha256:ef35",
"configuration_artifact_ids": ["524a4178-7e9f-49ea-bccd-8ddb163ae2fd"],
"source_ref": null,
"manifest_ref": "vllm/manifest.yaml",
"manifest_checksum": null,
"values_checksum": null,
"rendered_payload": {"schema_version": "v1", "value": {}},
"created_at": "2026-09-23T09:18:24.287000+00:00"
},
"endpoint": {
"url": "http://execution-ready.namespace.svc:8000",
"protocol": "http",
"service_ref": null,
"model_ref": null,
"baseline_url": null
},
"forwarded_endpoint": null,
"monitoring_setup": null,
"namespace": "namespace",
"configuration_artifacts": [],
"resource_snapshot": null,
"provenance": {"cluster_server_id": "cluster-1", "cluster_session_id": "session-1"},
"metadata": {"display_name": "", "description": ""},
"evidence_refs": [],
"diagnostics": null,
"created_at": "2026-09-23T09:18:24.287000+00:00",
"updated_at": "2026-09-23T09:20:00.000000+00:00"
},
"deployment": {
"run_id": "run-1",
"case_id": "case-execution-ready",
"execution_id": "execution-ready",
"name": "qwen",
"display_name": "",
"description": "",
"status": "ready",
"namespace": "namespace",
"endpoint": "http://execution-ready.namespace.svc:8000",
"forwarded_endpoint": null,
"guide": "optimized-baseline",
"backend": "vLLM",
"model_name": "Qwen",
"replicas": null,
"tensor_parallel_size": null,
"image": null
}
}
from llm_d_lens.deploy.router import _store
case = _store.get_case("run-1", "case-execution-ready")
print(case.id if case else "missing")
Get deployment case logs
Get orchestration logs for one deployment case.
- REST API
- Python API
GET /api/v1/deployments/runs/{run_id}/cases/{case_id}/logs
Request: no body.
- Path parameters:
run_id(string, required),case_id(string, required) - Query parameter:
limit(integer, default200, max1000)
Response: DeploymentLogPage
{
"execution_id": "execution-ready",
"entries": [
{
"timestamp": "2026-09-23T09:19:01.000000+00:00",
"source": "provider",
"message": "Rendered deployment manifest",
"severity": "info"
},
{
"timestamp": "2026-09-23T09:19:12.000000+00:00",
"source": "provider",
"message": "Deployment reached Ready status",
"severity": "info"
}
],
"next_cursor": null,
"complete": true,
"evidence_refs": []
}
import asyncio
from llm_d_lens.deploy.application import deployment_run_manager
async def main() -> None:
logs = await deployment_run_manager.worker_for_run("run-1").get_case_logs(
"run-1",
"case-execution-ready",
limit=200,
)
print(logs.execution_id, len(logs.entries))
asyncio.run(main())
Clean a deployment case
Clean up one deployment case's runtime resources and marks it cleaned.
- REST API
- Python API
POST /api/v1/deployments/runs/{run_id}/cases/{case_id}/clean
Request body: CleanCaseRequest
- Path parameters:
run_id(string, required),case_id(string, required) preserve_rendered_overlay(boolean, defaultfalse)
{
"preserve_rendered_overlay": true
}
Response:
{
"case": {
"id": "case-execution-ready",
"run_id": "run-1",
"ordinal": 0,
"source_configuration_ordinal": 0,
"provider_ref": "optimized-baseline",
"component": "decode",
"depends_on": [],
"status": "cleaned",
"attempt": 1,
"execution_id": "execution-ready",
"failure": null,
"create_request": {
"request_id": "58b85f84-c8ef-47e1-8cb9-61831eb9d310",
"input_schema_version": "v1",
"configuration_artifacts": [],
"deployment_policy": {"schema_version": "v1", "value": {"deployment_name": "qwen"}},
"cluster_snapshot_ref": null,
"cluster_snapshot": null,
"provenance": {}
}
},
"execution": {
"execution_id": "execution-ready",
"request_id": "58b85f84-c8ef-47e1-8cb9-61831eb9d310",
"status": "cleaned",
"artifact": {
"artifact_id": "art-1",
"artifact_hash": "sha256:ef35",
"configuration_artifact_ids": ["524a4178-7e9f-49ea-bccd-8ddb163ae2fd"],
"source_ref": null,
"manifest_ref": "vllm/manifest.yaml",
"manifest_checksum": null,
"values_checksum": null,
"rendered_payload": {"schema_version": "v1", "value": {}},
"created_at": "2026-09-23T09:18:24.287000+00:00"
},
"endpoint": {
"url": "http://execution-ready.namespace.svc:8000",
"protocol": "http",
"service_ref": null,
"model_ref": null,
"baseline_url": null
},
"forwarded_endpoint": null,
"monitoring_setup": null,
"namespace": "namespace",
"configuration_artifacts": [],
"resource_snapshot": null,
"provenance": {"cluster_server_id": "cluster-1", "cluster_session_id": "session-1"},
"metadata": {"display_name": "", "description": ""},
"evidence_refs": [],
"diagnostics": null,
"created_at": "2026-09-23T09:18:24.287000+00:00",
"updated_at": "2026-09-23T09:22:00.000000+00:00"
},
"deployment": {
"run_id": "run-1",
"case_id": "case-execution-ready",
"execution_id": "execution-ready",
"name": "qwen",
"display_name": "",
"description": "",
"status": "cleaned",
"namespace": "namespace",
"endpoint": "http://execution-ready.namespace.svc:8000",
"forwarded_endpoint": null,
"guide": "optimized-baseline",
"backend": "vLLM",
"model_name": "Qwen",
"replicas": null,
"tensor_parallel_size": null,
"image": null
}
}
import asyncio
from llm_d_lens.deploy.application import deployment_run_manager
async def main() -> None:
case, execution = await deployment_run_manager.worker_for_run("run-1").clean_case(
"run-1",
"case-execution-ready",
preserve_rendered_overlay=True,
)
print(case.status, execution.status)
asyncio.run(main())
Access-binding routes also exist for deployment runs and executions at GET/POST/DELETE /runs/{run_id}/access..., GET/POST/DELETE /executions/{execution_id}/access..., and GET .../access/options; see llm_d_lens/deploy/router.py for those exact handlers.
Additional endpoints exist for the deploy domain — see the full OpenAPI reference.
See the full request/response schemas in the FastAPI OpenAPI schema (regenerate with npm run docs:api).
Delete a deployment run
- REST API
- Python API
DELETE /api/v1/deployments/runs/{run_id}
No request body. Requires deployment:execution:delete and access to the run.
Returns 204 with no body. Missing runs return 404; active
(queued, running, cancelling) runs or lifecycle conflicts return 409.
Cancel active orchestration and wait for a terminal state before deleting.
This is not a metadata-only delete: remaining cases with stored executions are
cleaned first, requiring their active cluster session, then stored run/execution
records and associated shares are removed. Already-cleaned cases are skipped.
By contrast, case clean removes runtime resources but retains the lifecycle
record; preserve_rendered_overlay=true retains rendered files.
Example request: DELETE /api/v1/deployments/runs/run-1 (no JSON body or response).
import asyncio
from llm_d_lens.deploy.application import deployment_run_manager
async def main() -> None:
await deployment_run_manager.worker_for_run("run-1").delete_run("run-1")
asyncio.run(main())
The worker raises ValueError for lifecycle conflicts; the route maps it to 409.
Refresh a deployment case
- REST API
- Python API
POST /api/v1/deployments/runs/{run_id}/cases/{case_id}/refresh
No body is required ({} is accepted). Requires deployment:case:execute.
Returns 200 with the same case, execution, and deployment envelope as
Get case, refreshed from provider diagnostics. Stopped or cleaned records are
preserved rather than made ready again. Missing case/execution or refresh
ValueError returns 404. Response excerpt:
{
"case": {"id": "case-1", "status": "ready", "execution_id": "execution-1"},
"execution": {"execution_id": "execution-1", "status": "ready"},
"deployment": {"run_id": "run-1", "case_id": "case-1", "status": "ready"}
}
import asyncio
from llm_d_lens.deploy.application import deployment_run_manager
async def main() -> None:
execution = await deployment_run_manager.worker_for_run("run-1").refresh_case_execution(
"run-1", "case-1"
)
print(execution.status)
asyncio.run(main())
The worker returns the execution; the HTTP route assembles the case envelope.
Stop a deployment case
- REST API
- Python API
POST /api/v1/deployments/runs/{run_id}/cases/{case_id}/stop
No body is required ({} is accepted). Requires deployment:case:execute and
an active cluster session. Invokes provider stop and retains the records;
it is not deletion of the run. Returns 200 with the Get case envelope,
or 409 for worker ValueError. Response excerpt:
{
"case": {"id": "case-1", "status": "stopped", "execution_id": "execution-1"},
"execution": {"execution_id": "execution-1", "status": "rolled_back"},
"deployment": {"run_id": "run-1", "case_id": "case-1", "status": "stopped"}
}
import asyncio
from llm_d_lens.deploy.application import deployment_run_manager
async def main() -> None:
case, execution = await deployment_run_manager.worker_for_run("run-1").stop_case(
"run-1", "case-1"
)
print(case.status, execution.status)
asyncio.run(main())
Restart a deployment case
:::warning Current restart limitation
The route exists, but the current worker cleans the old execution and marks its
case cleaned before calling DeploymentPlanService.retry_case(), which accepts
only the latest stopped or failed case. Consequently, successful cleanup is
followed by 409, not a queued replacement. Cleanup has already taken effect;
an error is not evidence that the old resources remain. Do not repeatedly retry
this route expecting it to restore the deployment.
:::
- REST API
- Python API
POST /api/v1/deployments/runs/{run_id}/cases/{case_id}/restart
Requires deployment:case:execute, an active cluster session, and a stopped or
failed case. Body: RestartCaseRequest; model_token is an optional string
override, with empty/whitespace treated as absent. Send {} to omit it.
The declared success shape is HTTP 200, { "case": ... }, but the current
cleanup/retry conflict prevents that path after successful cleanup.
Lifecycle ValueError becomes 409; invalid bodies return 422.
{}
Current conflict response excerpt (the application adds Problem Details fields):
{
"status": 409,
"detail": "only the latest stopped or failed deployment case may be retried"
}
import asyncio
from llm_d_lens.deploy.application import deployment_run_manager
async def main() -> None:
try:
case = await deployment_run_manager.worker_for_run("run-1").restart_case(
"run-1", "case-1", model_token=None
)
except ValueError as error:
print(f"Restart failed; inspect the case because cleanup may have occurred: {error}")
raise
print(case.id, case.status)
asyncio.run(main())
Remote SSH deployment
These legacy endpoints are implemented by the Node BFF in
apps/server/src/routes/remoteDeploy.ts, not the Python Deploy service. They
execute commands over SSH rather than create v1 deployment runs. Their current
single-node Intel Data Center GPU/XPU restriction is specific to this legacy
implementation, not the capability limit of the profile-driven v1 API.
SSH operations require PRISM_REMOTE_DEPLOY_ENABLED=true in production (enabled
by default outside production). Production targets must also match
PRISM_REMOTE_HOST_ALLOWLIST. Defaults remain readable when SSH operations are
disabled, subject to authentication/permission checks.
| Operation | Lens permission |
|---|---|
| Defaults | remote-deploy:target:read |
| Fingerprint, test | remote-deploy:target:connect |
| Start, status, logs | remote-deploy:deploy:execute |
| Teardown | remote-deploy:deploy:teardown |
Authenticated SSH operations require target.host, username,
hostFingerprint and a nonempty cards array of numeric ID strings.
port defaults to 22; authMethod defaults to agent and also accepts key
or password. Agent authentication requires the BFF's SSH_AUTH_SOCK.
Key authentication requires a server-side keyPath beneath PRISM_SSH_KEY_DIR
(default $HOME/.ssh). Password authentication supplies
credentials.password on each request; it is not stored or returned.
Fingerprint discovery does not authenticate and does not require cards.
Confirm its result through a trusted channel before using it; a returned
fingerprint is not itself proof of host identity.
Route errors are { "error": "..." }: 400 for invalid input/credentials,
403 for a production host outside the allowlist, 503 for disabled SSH
operations, and normally 502 for SSH/command failures. Gateway authentication
and permission errors use the gateway's Problem Details format.
Python HTTP client for remote operations
There is no Python service-layer equivalent for these Node endpoints. The Python tabs below use this HTTP helper instead. The environment variables in this example are client inputs, not new Lens server configuration. Set them from an authenticated Lens session; never commit their values.
import os
import httpx
def remote_request(method: str, path: str, body: dict | None = None) -> dict:
with httpx.Client(
base_url=os.environ["LENS_URL"],
cookies={
"prism_session": os.environ["LENS_SESSION"],
"prism_csrf": os.environ["LENS_CSRF"],
},
headers={"x-prism-csrf": os.environ["LENS_CSRF"]},
timeout=300,
) as client:
response = client.request(method, path, json=body)
response.raise_for_status()
return response.json()
target = {
"host": "192.0.2.20",
"port": 22,
"username": "operator",
"authMethod": "agent",
"hostFingerprint": os.environ["VERIFIED_SSH_FINGERPRINT"],
"cards": ["0"],
}
REST request examples below show operation fields; for authenticated operations,
add the complete target object above and optional per-request credentials.
The Python examples demonstrate the complete request assembly.
Get remote deployment defaults
- REST API
- Python API
GET /api/remote-deploy/defaults, no body. Returns 200:
{
"repository": "", "branch": "", "httpProxy": "", "httpsProxy": "",
"noProxy": "", "mountPath": "", "mountModelName": ""
}
Values come from the BFF environment. Repository uses LLM_D_ROOT, then
LLM_D_REPOSITORY, otherwise empty; there is no host-specific default checkout.
Branch uses LLM_D_BRANCH. Proxy fields prefer HTTP_PROXY, HTTPS_PROXY,
NO_PROXY, then their LLM_D_ equivalents. Mount fields use
LLM_D_MOUNT_PATH and LLM_D_MOUNT_MODEL_NAME.
defaults = remote_request("GET", "/api/remote-deploy/defaults")
print(defaults["repository"])
Discover an SSH host fingerprint
- REST API
- Python API
POST /api/remote-deploy/fingerprint. Request:
{"target": {"host": "192.0.2.20", "port": 22, "username": "operator"}}
Only host is required; port defaults to 22 and username to probe.
Returns 200 with fingerprint, host, port. Illustrative response:
{"fingerprint": "SHA256:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", "host": "192.0.2.20", "port": 22}
discovered = remote_request("POST", "/api/remote-deploy/fingerprint", {
"target": {"host": target["host"], "port": target["port"], "username": target["username"]}
})
print(discovered["fingerprint"])
Confirm independently; do not automatically overwrite the verified fingerprint.
Test a remote deployment target
- REST API
- Python API
POST /api/remote-deploy/test. Body: target and optional credentials;
no additional operation fields ({}).
Probes hostname/OS, llm-d-bench, kubectl/cluster connectivity and xpu-smi.
Returns 200 with ok and output (last 12,000 characters):
{"ok": false, "output": "kubectl could not reach the cluster"}
A completed probe with nonzero exit can return 200 and ok=false; HTTP success
does not prove readiness. Connection failures use the error statuses above.
probe = remote_request("POST", "/api/remote-deploy/test", {"target": target})
if not probe["ok"]:
raise RuntimeError(probe["output"])
print(probe["output"])
Start a remote deployment
- REST API
- Python API
POST /api/remote-deploy/start. Add target and optional credentials to:
{
"plan": {
"namespace": "llm-d-optimized", "name": "llm-d-service",
"accelerator": "Intel Data Center GPU", "nodes": 1,
"guide": "optimized-baseline", "model": "Qwen/Qwen3-0.6B",
"decodeReplicas": 1, "decodeTp": 1,
"deploymentTarget": {"repository": "https://github.com/llm-d/llm-d.git", "branch": "main"}
}
}
Namespace/name default to the values shown and must be Kubernetes-style names.
Repository must be an absolute remote path, ~/ path, or GitHub HTTPS URL;
branch defaults to main. URL checkouts use the remote host's
LENS_CACHE_DIR (otherwise $XDG_CACHE_HOME/lens or $HOME/.cache/lens),
under deploy-repos/<owner>/<repository>.
The plan may set deploymentTarget.httpProxy, httpsProxy, noProxy, and a
paired absolute mountPath / mountModelName.
The accelerator must match the legacy Intel label and nodes must be one.
Decode TP/replicas default to 1 (replicas is a fallback for decodeReplicas);
prefill TP/replicas default to zero. Dimensions are integers at most 128,
decode dimensions at least one. Required topology cards cannot exceed selected
card count. Card IDs are validated/count-checked; this endpoint does not promise
physical-device pinning. An explicit guide must be a valid identifier;
without one, source/topology chooses the guide, with epd unsupported.
Runs PRISM_REMOTE_DEPLOY_BIN (default llm-d-bench) with deploy custom --no-wait,
then patches model-container mount/proxy settings. Returns 200, not 202,
after commands complete; this is not a ready deployment. Response excerpt:
{
"id": "remote-1700000000000", "name": "llm-d-service",
"cluster": "operator@192.0.2.20:22", "remoteHost": "192.0.2.20",
"nodes": 1, "gpusPerNode": 1, "selectedCards": ["0"], "replicas": 1,
"namespace": "llm-d-optimized", "status": "deploying",
"createdAt": "2026-10-13T00:00:00.000Z", "endpoint": null, "pods": [], "logTail": []
}
The response also includes candidate labels and supplied machine/accelerator
metadata; replicas reflects plan.replicas or 1, not necessarily decodeReplicas.
Poll status to determine rollout state.
plan = {
"namespace": "llm-d-optimized", "name": "llm-d-service",
"accelerator": "Intel Data Center GPU", "nodes": 1,
"guide": "optimized-baseline", "model": "Qwen/Qwen3-0.6B",
"decodeReplicas": 1, "decodeTp": 1,
"deploymentTarget": {"repository": "https://github.com/llm-d/llm-d.git", "branch": "main"},
}
started = remote_request("POST", "/api/remote-deploy/start", {"target": target, "plan": plan})
print(started["namespace"], started["status"])
Get remote deployment status
- REST API
- Python API
POST /api/remote-deploy/status. Add target and optional credentials to:
{"namespace": "llm-d-optimized"}
Returns 200 with status, pods, podsReady, podsTotal, endpoint,
error, logTail. States include deploying, ready, ready_no_endpoint,
failed, not_found; inspect status, not just the HTTP code.
Pod entries contain name, status, ready, restarts, node, gpuCount,
reason, message. Endpoint discovery uses Gateway/LoadBalancer addresses or
Gateway Service DNS; it does not establish a local tunnel. Example:
{"status": "deploying", "pods": [], "podsReady": 0, "podsTotal": 0, "endpoint": null, "error": null, "logTail": []}
status = remote_request("POST", "/api/remote-deploy/status", {
"target": target, "namespace": "llm-d-optimized"
})
print(status["status"], status["endpoint"])
Get remote deployment logs
- REST API
- Python API
POST /api/remote-deploy/logs. Add target and optional credentials to:
{"namespace": "llm-d-optimized", "tail": 100}
Optional pod selects a pod. tail defaults to 100 and is bounded to 1..1000.
Returns 200 with logs and truncated, retaining at most the final 32,000
JavaScript string code units, not a guaranteed 32 KiB byte limit:
{"logs": "Model server started\n", "truncated": false}
logs = remote_request("POST", "/api/remote-deploy/logs", {
"target": target, "namespace": "llm-d-optimized", "tail": 100
})
print(logs["logs"], logs["truncated"])
Tear down a remote deployment
- REST API
- Python API
POST /api/remote-deploy/teardown. Add target and optional credentials to:
{"namespace": "llm-d-optimized"}
Runs remote deploy cleanup -n <namespace> -f. Returns 200 with success,
namespace, and the final 100 output lines in logTail:
{"success": true, "namespace": "llm-d-optimized", "logTail": []}
removed = remote_request("POST", "/api/remote-deploy/teardown", {
"target": target, "namespace": "llm-d-optimized"
})
print(removed["success"], removed["namespace"])
Additional deployment APIs
List orphan deployment namespaces
List namespaces on a cluster that match the deployment namespace prefix but are not referenced by any Lens execution record. These are leftovers from records deleted without namespace cleanup, or from a rebuilt database; nothing else in Lens points at them.
- REST API
- Python API
GET /api/v1/deployments/orphans
Query parameters:
cluster_id(string, required): the cluster to scan.
Response: the deployment prefix and the orphan namespaces.
{
"clusterId": "cluster-a",
"prefix": "llmd-",
"items": [
{ "name": "llmd-optimized-baseline-1-1-20260918032423139529", "createdAt": "2026-09-18T03:24:23Z", "phase": "Active" }
]
}
import asyncio
from llm_d_lens.deploy.orphans import scan_orphan_namespaces
orphans = asyncio.run(scan_orphan_namespaces("cluster-a"))
print([orphan.api_payload() for orphan in orphans])
Clean orphan deployment namespaces
Delete the given orphan namespaces (and the InferencePool/EPP/model server inside them). Names outside the deployment prefix, or still referenced by a Lens execution record, are refused rather than deleted.
- REST API
- Python API
POST /api/v1/deployments/orphans/clean
Request body (CleanOrphansRequest):
clusterId(string, required)namespaces(string[], required): the orphan namespaces to delete.
{
"clusterId": "cluster-a",
"namespaces": ["llmd-optimized-baseline-1-1-20260918032423139529"]
}
Response: deleted names and any per-namespace failures.
{
"clusterId": "cluster-a",
"cleaned": ["llmd-optimized-baseline-1-1-20260918032423139529"],
"failed": []
}
import asyncio
from llm_d_lens.deploy.orphans import clean_orphan_namespaces
cleaned, failed = asyncio.run(
clean_orphan_namespaces(
"cluster-a", ["llmd-optimized-baseline-1-1-20260918032423139529"]
)
)
print({"cleaned": cleaned, "failed": failed})
Configuration preparation APIs
These Configuration-domain routes prepare artifacts for Deployments and Evaluation. Request and response models live in llm_d_lens.configuration.models. The examples use illustrative cluster/session IDs and guide commits; replace them with an active cluster session and real rendered guide provenance. Saving publishes an artifact; rendering alone does not deploy it.
The workflow is source selection/editing, resolve, render, save, then deployment. Resolve normalizes each source configuration before capability checking and validation, returning per-configuration feedback. The candidate source identifies input origin, not deployment type. Render binds candidate intent to actual Guide resources; save publishes immutable artifact content, identity and checksums. Configuration owns normalization, validation and artifact publication; Deploy owns runtime validation, resource lifecycle, teardown and rollback. Checksums alone cannot validate topology or resource semantics. These boundaries apply to both the REST endpoints and their Python service functions below.
The HTTP boundary uses application/problem+json errors. Shared DomainError instances retain their original status and code (for example a conflict remains 409); they are not converted into an operation-specific 500. Python service calls raise their original exceptions; the route adapters in llm_d_lens.configuration.router apply the HTTP mappings below.
Resolve source configurations
- REST API
- Python API
POST /api/v1/configurations/resolve
Request: ResolveRequest. Success: HTTP 200, ResolveResponse. candidate_source and a nonempty configurations list are required; target.cluster_id is optional. Returns results, each with configuration_id, candidate_config, and validation (status, errors, warnings). Configuration errors escaping the resolver return 400; unexpected failures return 500 with Unable to resolve configurations.
{
"candidate_source": {
"name": "manual"
},
"configurations": [
{
"configuration_id": "example",
"input_mode": "form",
"payload": {
"type": "baseline",
"candidate_source": {
"name": "manual"
},
"target": {
"model": "New/Model"
},
"serving": {
"replicas": 2,
"tensor_parallel_size": 1
},
"runtime": {
"image": "model:v1"
},
"guide_settings": {
"routerValues": "router:\n epp:\n replicas: 2"
}
}
}
],
"target": {
"cluster_id": "cluster"
}
}
Save the request JSON above as resolve-request.json.
import asyncio
from pathlib import Path
from llm_d_lens.configuration.models import ResolveRequest
from llm_d_lens.configuration.service import resolve_configurations
request = ResolveRequest.model_validate_json(Path("resolve-request.json").read_text())
result = asyncio.run(resolve_configurations(request))
print(result.model_dump(mode="json"))
Render a deployable configuration
- REST API
- Python API
POST /api/v1/configurations/render
Request: RenderRequest. Success: HTTP 200, RenderResponse. Requires candidate_config and render: format, guide/template references, pinned guide source, rendered manifest, active cluster reference, and deployment readiness/endpoint contract. Optional bundle, reference manifest, model secret and deployment name remain supported. Returns deployable_configuration and metadata. Configuration or value errors return 400; unexpected failures return 500 with Unable to render configuration.
{
"candidate_config": {
"type": "baseline",
"candidate_source": {
"name": "manual"
},
"target": {
"model": "New/Model"
},
"serving": {
"replicas": 2,
"tensor_parallel_size": 1
},
"runtime": {
"image": "model:v1"
},
"guide_settings": {
"routerValues": "router:\n epp:\n replicas: 2"
}
},
"render": {
"format": "manifest",
"guide_ref": "optimized-baseline",
"template_ref": "guides/optimized-baseline/modelserver/xpu/vllm/kustomization.yaml",
"guide_source": {
"repository": "llm-d/llm-d",
"requestedRef": "main",
"commit": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"guide": "optimized-baseline",
"accelerator": "xpu",
"modelServer": "vllm",
"variant": ".",
"files": [
"guides/optimized-baseline/modelserver/xpu/vllm/kustomization.yaml"
]
},
"rendered_manifest": "apiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: decode\nspec:\n replicas: 2\n template:\n spec:\n containers:\n - args:\n - New/Model\n - --tensor-parallel-size=1\n command:\n - vllm\n - serve\n image: model:v1\n name: modelserver\n---\napiVersion: v1\nkind: Service\nmetadata:\n name: model\nspec:\n ports:\n - port: 8000\n",
"deployment_bundle": {
"schemaVersion": "guide-deployment-bundle.v1",
"guide": "optimized-baseline",
"sourceCommit": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"helm": {
"chart": "oci://ghcr.io/llm-d/charts/llm-d-router-standalone",
"version": "v0.9.0",
"releaseName": "optimized-baseline",
"values": [
{
"name": "router-base.yaml",
"content": "router: {}",
"checksum": "sha256:162352753bb2618923b371c7236fc0c69f6604abaceef33d769f7f1686f845f6"
},
{
"name": "router-guide.yaml",
"content": "router:\n epp:\n replicas: 1",
"checksum": "sha256:bc3156ef61b87ae553e01848266fbe4f3dc526e0072f0d93f6dd9ede8c8784fd"
},
{
"name": "router-effective.yaml",
"content": "router:\n epp:\n replicas: 2",
"checksum": "sha256:816e7b274c9e9c55442a5be90c74d252a517a62f49dfdcf6fba60ac8443d189a"
}
]
},
"resources": []
},
"cluster_ref": {
"id": "cluster",
"name": "cluster",
"session_id": "00000000-0000-0000-0000-000000000001",
"connection": "managed-kubeconfig"
},
"deployment": {
"readinessDeployments": [
"decode"
],
"endpoint": {
"protocol": "http",
"serviceName": "model",
"port": 8000
}
}
}
}
Save the request JSON above as render-request.json.
from pathlib import Path
from llm_d_lens.configuration.models import RenderRequest
from llm_d_lens.configuration.service import render_configuration
request = RenderRequest.model_validate_json(Path("render-request.json").read_text())
result = render_configuration(request)
print(result.model_dump(mode="json"))
Save a configuration artifact
- REST API
- Python API
POST /api/v1/configurations/save
Request: SaveRequest. Success: HTTP 200, SaveResponse. Requires deployable_configuration; file.name is optional. The configuration file URI is derived from the artifact ID and file name and is preserved when reading the saved artifact. Returns configuration_file (URI, type, file name, format, path, size, SHA-256) and artifact (ID, revision, status, deployable configuration, file metadata, creation time). Configuration, filesystem or value errors return 400; unexpected failures return 500 with Unable to save configuration.
{
"deployable_configuration": {
"schema_version": "deployable-configuration.v1",
"type": "baseline",
"format": "manifest",
"content": {
"deploymentType": "baseline",
"model": {
"name": "New/Model"
},
"serving": {
"replicaCount": 2,
"tensorParallelSize": 1
},
"runtime": {
"image": "model:v1"
},
"guideSettings": {
"routerValues": "router:\n epp:\n replicas: 2"
},
"officialGuide": {
"source": {
"repository": "llm-d/llm-d",
"requestedRef": "main",
"commit": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"guide": "optimized-baseline",
"accelerator": "xpu",
"modelServer": "vllm",
"variant": ".",
"files": [
"guides/optimized-baseline/modelserver/xpu/vllm/kustomization.yaml"
]
},
"renderedManifest": "apiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: decode\nspec:\n replicas: 2\n template:\n spec:\n containers:\n - args:\n - New/Model\n - --tensor-parallel-size=1\n command:\n - vllm\n - serve\n image: model:v1\n name: modelserver\n---\napiVersion: v1\nkind: Service\nmetadata:\n name: model\nspec:\n ports:\n - port: 8000\n",
"manifestChecksum": "sha256:f0677cfd3ecd361a1e549b6a17fbf64c884d406152095c777b359d9d95f2b408",
"cluster": {
"id": "cluster",
"name": "cluster",
"session_id": "00000000-0000-0000-0000-000000000001",
"connection": "managed-kubeconfig"
},
"deployment": {
"readinessDeployments": [
"decode"
],
"endpoint": {
"protocol": "http",
"serviceName": "model",
"port": 8000
}
},
"modelSecret": {
"mode": "none",
"sourceNamespace": null,
"sourceName": null
},
"deploymentBundle": {
"schemaVersion": "guide-deployment-bundle.v1",
"guide": "optimized-baseline",
"sourceCommit": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"helm": {
"chart": "oci://ghcr.io/llm-d/charts/llm-d-router-standalone",
"version": "v0.9.0",
"releaseName": "optimized-baseline",
"values": [
{
"name": "router-base.yaml",
"content": "router: {}",
"checksum": "sha256:162352753bb2618923b371c7236fc0c69f6604abaceef33d769f7f1686f845f6"
},
{
"name": "router-guide.yaml",
"content": "router:\n epp:\n replicas: 1",
"checksum": "sha256:bc3156ef61b87ae553e01848266fbe4f3dc526e0072f0d93f6dd9ede8c8784fd"
},
{
"name": "router-effective.yaml",
"content": "router:\n epp:\n replicas: 2",
"checksum": "sha256:816e7b274c9e9c55442a5be90c74d252a517a62f49dfdcf6fba60ac8443d189a"
}
]
},
"resources": []
}
}
},
"provider_ref": "optimized-baseline",
"checksum": "sha256:da087b6024d1c8c6ef531f06d55e78d2a249594feb96b17580af140ac68c029f",
"provenance": {
"candidate_source": {
"name": "manual",
"run_id": null,
"result_id": null
},
"guide_ref": "optimized-baseline",
"template_ref": "guides/optimized-baseline/modelserver/xpu/vllm/kustomization.yaml",
"guide_source": {
"repository": "llm-d/llm-d",
"requestedRef": "main",
"commit": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"guide": "optimized-baseline",
"accelerator": "xpu",
"modelServer": "vllm",
"variant": ".",
"files": [
"guides/optimized-baseline/modelserver/xpu/vllm/kustomization.yaml"
]
},
"cluster_ref": {
"id": "cluster",
"name": "cluster",
"session_id": "00000000-0000-0000-0000-000000000001",
"connection": "managed-kubeconfig"
},
"manifest_checksum": "sha256:f0677cfd3ecd361a1e549b6a17fbf64c884d406152095c777b359d9d95f2b408",
"renderer_version": "1.0"
}
},
"file": {
"name": "example.yaml"
}
}
Save the request JSON above as save-request.json.
from pathlib import Path
from llm_d_lens.configuration.models import SaveRequest
from llm_d_lens.configuration.service import save_configuration
request = SaveRequest.model_validate_json(Path("save-request.json").read_text())
result = save_configuration(request)
print(result.model_dump(mode="json"))
Deployment access APIs (removed)
Deployment sharing and deployment:run:share have been retired. Use
model-service inference grants
instead. Migration audits and removes existing deployment shares; it does not
convert them into broader model grants.
- REST API
- Python API
The former GET/POST/DELETE /api/v1/deployments/executions/{execution_id}/access
and /api/v1/deployments/runs/{run_id}/access routes, including
/access/options and /access/{binding_id}, are no longer registered.
They no longer grant, list or revoke access.
Replace deployment-share calls with the model service's scoped grant methods:
from llm_d_lens.model_service.service import default_service
def revoke_model_grant(model_id: str, binding_id: str, principal) -> None:
default_service().revoke_access(model_id, binding_id, principal=principal)
The method requires model-service:group:share in the model's cluster and rejects
bindings belonging to another model. Generic binding creation rejects retired
deployment_run and deployment_execution resource types.