A deploy control plane for the sglang and vllm charts. Design and rationale:
docs/architecture.md. Building and testing: DEVELOPMENT.md.
One document, read by both binaries. Found via --config, $SWISS_CONFIG,
./swiss.yaml, ~/.config/swiss/swiss.yaml.
catalog: https://modelsphere.github.io/model-catalog/ # the default when left out
cluster:
name: prod
profile:
file: ./examples/site-prod.yaml # or configMap: swiss/site-profile
server:
addr: ":8080"
allowDeploy: falseThe CLI works without one (--catalog / --profile, the catalog defaulting to
https://modelsphere.github.io/model-catalog/); swissd requires one.
go build -o swiss ./cmd/swiss
swiss catalog list
swiss catalog show kimi-k2.5
swiss plan --model qwen3.6-35b-a3b --release fallback-qwen-01 \
--service-id fallback-qwen-01 --explain
swiss plan --model qwen3.6-35b-a3b --release fallback-qwen-01 -o plan.json
swiss render --plan plan.json # helm template; values.schema.json runs here
swiss diff --plan plan.json # exit 2 when something would change
swiss apply --plan plan.json # release must exist
swiss install --plan plan.json # release must not exist--explain shows which layer set each value. emit is not implemented.
cd web && npm install && npm run build && cd ..
go build -o swissd ./cmd/swissd
./swissd --config examples/swiss.yamlPOST /api/login /api/logout GET /api/session (open)
GET /api/cluster /api/sites /api/catalog /api/catalog/{model}
GET /api/releases /api/nodes /api/deployments /api/runs
PUT /api/profile
POST /api/plans /api/diff /api/apply /api/install (allowDeploy only)
applyWith on the site profile chooses the backend for a new install: helm (the default when the field is omitted) or llmsvc when the LLMService CRD is served. A release already in the cluster stays on the backend that owns it. llmsvc requires the plan's source.schema to be v2; empty means v1 and is rejected with 400 and nothing is written. Helm accepts v1 and v2. The CLI swiss install and swiss apply stay on helm. See docs/architecture.md.
CI publishes the image to ghcr.io/modelsphere/swissd and
4pdosc/swissd on Docker Hub, and the chart to
oci://ghcr.io/modelsphere/charts/swiss, at one version. The OCI chart pulls the
GHCR image; charts in the modelsphere helm repo
are the other channel and pull from Docker Hub.
helm install swiss oci://ghcr.io/modelsphere/charts/swiss -n swiss --create-namespace \
--set 'rbac.namespaces={models,kimi}'From a checkout:
docker build -t ghcr.io/modelsphere/swissd:$(./hack/bump.sh) .
helm install swiss ./helm/swiss -n swiss --create-namespace \
--set 'rbac.namespaces={models,kimi}'rbac.namespaces is rendered into both the Roles and cluster.namespaces, so
swissd lists only namespaces it was granted — a Role cannot authorise a
cluster-wide list. rbac.scope: cluster drops the list and reads everything.
rbac.applyWith is helm (today's grant), both (that grant plus
LLMService), or llmsvc (LLMService writes and the read grants, without Secret,
Role or workload writes). Anything else is refused. The new-install backend is
applyWith on the site profile, which an operator edits without a helm upgrade.
Deploy mode recommends a volume, and runs one replica. The database holds the audit log (who applied or upgraded what, with the output) and the stored plans (the release -> plan history used to roll back or revert to an earlier plan). Neither is cluster state: each applied plan is also written beside its release, so what is deployed stays visible and upgradable without the database. The chart defaults to a PVC. With persistence off and no existingClaim, the database is an emptyDir, and a pod restart or reschedule loses it. Use a volume in production. One replica either way: sqlite is a single writer.
One instance per cluster, inside the cluster it manages. Read the RBAC section of
the design doc before installing: rbac.scope decides whether swissd can read
every Secret in the cluster.
make test-e2e builds swissd, installs it on a kind cluster with llm-operator
and the CRDs the vllm chart renders, and deploys the catalog's CPU-only mock-llm
through both backends. Needs Go, docker, kind, kubectl and helm; no GPU.
make test-e2e # KEEP_CLUSTER=1 to leave the cluster up
make test-e2e E2E_ARGS='-ginkgo.label-filter=helm'What is real and what is mocked, and every setting: docs/e2e.md.