---
last_mapped: 2026-03-16T00:00:00Z
scope: sub-region
---

# CLI / SDK Map

## Overview
The `anyscale` PyPI package: a Click CLI and Python SDK for the Anyscale platform. Two dispatch paths coexist -- a legacy controller layer (raw OpenAPI clients) and a newer SDK layer (`@sdk_command` + `AnyscaleClient`). Generated API clients target internal (`/api/v2`) and external (`/ext/v0`) backend REST APIs. Bazel and pip/setuptools build systems are maintained in parallel.

## Architecture
| Domain | Directory | Purpose | Depends On |
|--------|-----------|---------|------------|
| SDK core | `anyscale/_private/` | `AnyscaleClient` HTTP gateway, `FakeAnyscaleClient`, `BaseSDK`/`WorkloadSDK`, models, docgen | generated clients, `authenticate.py` |
| Domain modules | `anyscale/<domain>/` (16 domains) | `@sdk_command` public API + `_private/<domain>_sdk.py` impl + `models.py` | `_private/`, generated clients |
| Click handlers | `anyscale/commands/` | CLI input layer: 24 `*_commands.py` files registered in `scripts.py`; dispatch to SDK or controllers | domain modules, controllers, `_private/` |
| Legacy controllers | `anyscale/controllers/` | `BaseController` + raw dual-API-client impls; write-frozen except bug fixes | `authenticate.py`, generated clients, `utils/` |
| Generated clients | `anyscale/client/openapi_client/`, `anyscale/sdk/anyscale_client/` | Auto-generated REST clients (internal ~879 models, external ~168 models) | backend OpenAPI spec (codegen in `frontend/web/`) |
| Vendored libs | `anyscale/anyscale_pydantic/`, `anyscale/shared_anyscale_utils/` | Pydantic v1 isolation; host/env resolution (synced from repo root) | (leaf) |
| Utilities | `anyscale/utils/`, `anyscale/api_utils/` | Cloud provider setup (CF/GCP/ARM), log retrieval, entity resolution | generated clients, `boto3`, `google-cloud-*` |
| Tests | `tests/` | Two fixture hierarchies (controller vs SDK); Bazel + pytest | `anyscale/`, `FakeAnyscaleClient` |

## Domain Guide

### SDK Path (new features go here)
**Purpose:** 16 domain modules following a uniform 4-file pattern (`commands.py`, `models.py`, `_private/<domain>_sdk.py`, `__init__.py`).
**Start here:** `anyscale/_private/anyscale_client/common.py` (~70-method ABC), then `anyscale/_private/sdk/__init__.py` (`@sdk_command` decorator)
**Key packages:**
- `compute_config/models.py` -- highest fan-out model file; imported by job, service, workspace, job_queue, schedule
- `image/_private/image_sdk.py` -- dual role: public SDK for `anyscale.image.*` AND internal dep of `WorkloadSDK` (affects all 4 workload domains)

**SDK inheritance:**
| Base class | Domains |
|------------|---------|
| `WorkloadSDK` | job, service, workspace, job_queue |
| `BaseSDK` | cloud, image, compute_config, schedule, policy, resource_quota, user, user_group, organization_invitation, service_account, aggregated_instance_usage, project |

### Click Handlers (commands/)
**Purpose:** CLI input layer. Each `*_commands.py` defines a Click group dispatching to SDK or controller.
**Dispatch status:**
- SDK-only: aggregated_instance_usage, image, job_queue, organization_invitation, policy, project, resource_quota, service_account, user, user_group, scim, workspace_v2
- Mixed: job (submit legacy format, logs --follow), service (rollback, ID resolution), compute_config (create only), schedule (apply)
- Controller-only: cloud (all writes), cluster, logs, auth, login, config, migrate, machine_pool, experimental_integrations

### Legacy Controllers
**Purpose:** Write-frozen except bug fixes. Still the sole path for cloud lifecycle, cluster, logs, machine pools.
**Key detail:** `cloud_controller.py` is the sole cloud registration/setup/delete impl; no SDK replacement exists.

### Support Packages
- `webterminal/` -- Tornado terminal server deployed to cluster nodes (NOT a CLI command); new deps require updating `backend/server/services/application_templates_service.py`
- `models/` -- legacy Pydantic v1 models with live API calls in validators; distinct from SDK-path `<domain>/models.py`
- `terraform/` -- `main.tf` used by `utils/gcp_managed_setup_utils.py` for GCP managed cloud setup; not general IaC

### Tests
**Start here:** `tests/unit/conftest.py` (`sdk_with_fakes`) for SDK tests; `tests/conftest.py` for controller tests.
**Three independent conftests** -- never mix them:
1. `tests/conftest.py` -- controller path: `mock_auth_api_client`, GCP mock WSGI, autouse `time.sleep` patch
2. `tests/unit/conftest.py` -- SDK path: `sdk_with_fakes`
3. `tests/api_utils/conftest.py` -- mock factory helpers only
**Build:** `bazel test //frontend/cli/tests/unit/...` (SDK, small/fast) or `//frontend/cli/tests/...` (all)

## Cross-Cutting Concerns
- **Dual dispatch (in-flight migration):** Per-subcommand, not per-file. `cloud_commands.py`, `job_commands.py`, `service_commands.py` mix controller and SDK calls
- **`@sdk_command` singletons:** `_LAZY_SDK_SINGLETONS` module-level dict persists for process lifetime; tests must inject `_private_sdk=` or contaminate each other
- **Import-time side effects:** `anyscale.conf` resolves env vars at import; `telemetry.py` monkey-patches Click; `scripts.py` sets `os.environ["AWS_PROFILE"]` at module scope; all 24 command modules load eagerly
- **Pydantic v1 vendored:** All models use `from anyscale.anyscale_pydantic import ...`; bare `import pydantic` anywhere in CLI source is a bug
- **`shared_anyscale_utils/`** vendored copy synced by `copy-anyscale-utils.sh`; edits here are overwritten; `AUTOGENERATED` header on every `.py` file
- **Dual build systems:** `BUILD.bazel` (Bazel/CI) and `setup.py` + `requirements.in` (pip/PyPI) must stay in sync; GCP deps are core in Bazel but optional extras (`[gcp]`) in pip
- **Auth singleton:** `authenticate.get_auth_api_client()` caches both success AND failure for process lifetime

## Key Flows
1. **SDK command:** `anyscale.job.submit()` -> `@sdk_command` creates `PrivateJobSDK(WorkloadSDK)` singleton -> `WorkloadSDK.override_and_upload_local_dirs_*()` -> `AnyscaleClient.submit_job()` -> backend REST API
2. **Legacy command:** `anyscale job submit -f config.yaml` -> `commands/job_commands.py` -> `JobController().submit()` -> `BaseController.api_client` (internal DefaultApi) -> backend REST API
3. **CLI startup:** `scripts.py` imports `conf` + `telemetry` (side effects) -> eagerly loads all 24 command modules -> `AliasedGroup.get_command()` dispatches

## Navigation
| To... | Look at... |
|-------|-----------|
| Add a new SDK domain | Copy any `anyscale/<domain>/` 4-file pattern; add methods to `AnyscaleClientInterface` + `AnyscaleClient` + `FakeAnyscaleClient`; register in `scripts.py` |
| Add a CLI subcommand | `anyscale/commands/<domain>_commands.py`; use `@sdk_command` path, not controllers |
| Understand the test double | `anyscale/_private/anyscale_client/fake_anyscale_client.py` (in-memory state machine; inject via `_private_sdk=`) |
| Change workload upload logic | `anyscale/_private/workload/workload_sdk.py` (affects job/service/workspace/job_queue) |
| Debug cloud setup | `anyscale/controllers/cloud_controller.py` + `anyscale/utils/cloudformation_utils.py` / `gcp_utils.py` / `arm_utils.py`; GCP new path: `anyscale/terraform/main.tf` via `utils/gcp_managed_setup_utils.py` |
| Regenerate API clients | `cd frontend/web && npm run build-api` (no codegen tooling in this directory) |
| Generate SDK docs | `python -m anyscale._private.docgen <output_dir>` |
| Write SDK-path tests | `tests/unit/` with `sdk_with_fakes` fixture; NEVER mix with `tests/conftest.py` controller fixtures |
| Write controller-path tests | `tests/controllers/` with `mock_auth_api_client` from `tests/conftest.py` |
| Add a runtime dependency | Update `requirements.in` + `BUILD.bazel` deps + `setup.py` extras (if cloud-specific) |

## Gotchas
- **Naming trap:** Two `commands.py` per domain -- `commands/<domain>_commands.py` (Click CLI handler) vs `<domain>/commands.py` (SDK public API); different directories, completely different roles
- **Naming trap:** `anyscale/cloud_utils.py` and `anyscale/utils/cloud_utils.py` are different modules; the `utils/` version imports from the top-level one
- **Naming trap:** `anyscale/models/job_model.py::JobConfig` (Pydantic v1, live API calls in validators) vs `anyscale/job/models.py::JobConfig` (frozen dataclass, no side effects) -- same class name, opposite semantics
- **Hidden coupling:** `setup_k8s.py` lives in `commands/` but is infrastructure logic, not a Click command; internally calls `CloudController`
- **Hidden coupling:** `api_utils/job_logs_util.py` imports from `controllers/logs_controller` -- utility depends on controller (inverted layering)
- **Hidden coupling:** `workspace_commands_v2.py` reaches into `_LAZY_SDK_SINGLETONS` directly for SSH HTTPS connection setup
- **Hidden coupling:** `schedule/_private/schedule_sdk.py` instantiates `PrivateJobSDK` internally -- changes to job SDK's `job_config_to_internal_prod_job_conf` break schedule
- **Hidden coupling:** `webterminal/command_persister.py` imports `commands/session_commands_hidden.py` -- data-plane module depends on CLI commands layer
