Skip to content

Let engines serve on another port and report health on another path - #573

Draft
dennis-upbound wants to merge 4 commits into
modelplaneai:mainfrom
dennis-upbound:dennis/engine-port-probe
Draft

dennis-upbound wants to merge 4 commits into
modelplaneai:mainfrom
dennis-upbound:dennis/engine-port-probe

Conversation

@dennis-upbound

@dennis-upbound dennis-upbound commented Oct 26, 2026 •

Copy link
Copy Markdown
Collaborator

Description of your changes

Fixes #462.

A ModelDeployment could only run an engine that listens on port 8000 and answers GET /health. The engine's default http port, its readiness probe, and the InferencePool's targetPorts were all fixed at 8000, the probe always asked /health, and the XRD rejected a readinessProbe on the engine container. An engine like the one in the issue, on 8080 with readiness at /v1/models, never went Ready.

This adds serving.port, the port every serving pod receives requests on, and lets the engine container set its own readinessProbe. Both mirror what's already there: ports and readinessProbe already exist on member containers for sidecars. The engine's probe uses the same corev1-shaped field, and a probe set there replaces the default whole. Before, the engine had to be on 8000 and answer /health. After:

serving:
  port: 8080
engines:
- name: main
  members:
  - role: Standalone
    template:
      spec:
        containers:
        - name: engine
          image: registry.example.com/my-engine:1.0
          readinessProbe:
            httpGet:
              path: /v1/models
              port: http

The port is on serving rather than on the engine because a replica's serving pods share one InferencePool, and its targetPorts applies to every pod. With PrefillDecode, the prefill engine and the decode sidecar both serve on it. The XRD port rules that hardcoded 8000 now compare against serving.port, so they move from each member up to the spec, where serving is visible. A rejection now points at the spec rather than at the member. The engine's http port defaults to the serving port, so the collector's scrape (by port name) and the default probe follow it. Neither ModelEndpoint URLs nor the gateway include the engine port, so neither changes.

serving.port has no schema default, so a deployment that doesn't set it stores and propagates exactly what it did before, and its replicas compose the same resources. New XRD validation rejects a probe path that doesn't start with / and a probe port outside 1 to 65535. These apply to sidecar probes too.

Decisions:

  • Modelplane doesn't probe a Worker, so a probe set on a Worker's engine passes through as written, the same as its ports. A Worker doesn't serve requests, and Modelplane doesn't give it an http port. The XRD rejects a Worker probe that refers to a port its engine container doesn't declare, because the worker pod would never go Ready.
  • The port rules read the port as self.?serving.?port.orValue(8000). CRD validation rules accept CEL optional types from Kubernetes 1.29: k8s.io/apiserver adds cel.OptionalTypes() to the base environment at version 1.28, and new rules compile against the previous minor version. The install guide requires a Kubernetes release that the project still supports, and the oldest of those is newer than 1.29.
  • serving.port needs nothing else. mode defaults to Unified before validation, so serving: {port: 8080} is enough.
  • The e2e mock engine runs on 8080 with serving.port set. Its probe asks /v1/models, and it no longer answers /health. It's the only end-to-end coverage of a port other than 8000, and the run fails if the engine's probe or the pool's port isn't applied. The unit tests and the live regression scenario cover the default port and probe.

Alternatives I considered:

  • Deriving the pool's port from the engine's http port when no sidecar declares 8000, with no new field. That breaks with PrefillDecode, where the decode pod's front port is the sidecar's, and it infers intent from which ports happen to be declared.
  • port and health (a path) on the engine. That covers fewer probes than a readinessProbe, and it would duplicate the ports and readinessProbe fields containers already have.

The second limit in the issue is out of scope: the endpoint picker rejects request paths it has no parser for, so a non-chat engine now goes Ready but still isn't reachable through the gateway. A follow-up issue will track it.

I haven't run e2e on this branch. I checked the CEL rules against a kube-apiserver 1.34 (envtest) with kubectl apply --dry-run=server, using valid and invalid ModelDeployments and ModelReplicas and every ModelDeployment under docs/manifests and e2e/manifests.

I have:

  • Read and followed Modelplane's contribution process.
  • Run nix flake check (or ./nix.sh flake check) and made sure it passes.
  • Added or updated tests covering any composition function changes.
  • Signed off every commit with git commit -s.

🤖 Generated with Claude Code

@github-actions

Copy link
Copy Markdown

Docs preview: https://modelplane-docs-pr-573.vercel.app (ready once the site's Content workflow finishes)

@dennis-upbound
dennis-upbound force-pushed the dennis/engine-port-probe branch 2 times, most recently from 0267e84 to 175742e Compare October 26, 2026 20:40
dennis-upbound and others added 4 commits October 26, 2026 20:59
A ModelDeployment could only run an engine that listens on port 8000
and answers GET /health. The XRDs pinned requests to 8000 in their
port rules and rejected a readinessProbe on the engine container, so
an engine on another port or with another health path never went
Ready.

This adds serving.port, the port every serving pod receives requests
on, and lets the engine container set its own readinessProbe. Both are
optional. Unset, the port is 8000 and Modelplane probes /health as
before. serving.port has no schema default, so a deployment that
doesn't set it stores and propagates exactly what it did.

The serving port is one value for the whole replica because a
replica's serving pods share one InferencePool, so it lives on serving
rather than on each engine. The port rules that checked 8000 move up
to the spec, where they can see serving.port. Probe paths must start
with "/", and probe ports must be a name or a number from 1 to 65535.
A Worker has no http port. Its engine's probe must refer to a port
that the engine container declares, or the worker pod never goes
Ready.

Towards modelplaneai#462.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Dennis Ramdass <dennis@upbound.io>
compose-model-replica fixed the engine's default http port, its
readiness probe and the InferencePool's targetPorts at 8000, and
always probed GET /health.

It now reads serving.port, defaulting to 8000, for all three, in the
native, llm-d and Grove backends alike. An engine that sets its own
readinessProbe gets it as written in place of the default probe, the
same way a sidecar's probe passes through. Modelplane doesn't probe a
Worker, so a probe set on a Worker's engine passes through as written,
like its ports. Replicas that set neither compose the same resources
as before.

Towards modelplaneai#462.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Dennis Ramdass <dennis@upbound.io>
The e2e deployment's mock engine listened on 8000 and answered
/health, so the end-to-end run only covered the default port and
probe.

It now listens on 8080, sets serving.port to match, and reports
readiness at /v1/models through its own readinessProbe. It no longer
answers /health. The pod goes Ready only if the engine's probe replaces
the default one, and a request through the gateway succeeds only if
the InferencePool targets 8080.

Towards modelplaneai#462.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Dennis Ramdass <dennis@upbound.io>
The docs said a serving pod receives requests on port 8000, and some
recipe comments said the engine must listen there.

Deploy a Model now describes serving.port and the engine's
readinessProbe, with an example of an engine on 8080 that reports
readiness at /v1/models. The routing guide's port rules refer to the
serving port, and the recipe comments call 8000 the default.

Fixes modelplaneai#462.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Dennis Ramdass <dennis@upbound.io>
@dennis-upbound
dennis-upbound force-pushed the dennis/engine-port-probe branch from 175742e to d483b10 Compare October 27, 2026 04:01

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Engines not on :8000 with /health never go Ready: port and probe are hardcoded

1 participant