-
Notifications
You must be signed in to change notification settings - Fork 19
feat(docs): Serve the Anthropic Messages API (Claude Code) end to end #360
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
dennis-upbound
merged 2 commits into
modelplaneai:main
from
haarchri:feature/docs-antropic-messages-api
Jul 21, 2026
Merged
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,100 @@ | ||
| --- | ||
| title: Anthropic Messages API | ||
| weight: 70 | ||
| description: Serve a model on the Anthropic Messages API and drive it from Claude Code. | ||
| --- | ||
| <!-- vale write-good.Passive = NO --> | ||
| A vLLM server registers the Anthropic Messages API at `/v1/messages` alongside | ||
| its OpenAI routes, with no extra flag. Modelplane's route matches the | ||
| `/<namespace>/<service>/` prefix and preserves the path below it, so the same | ||
| service URL answers both `/v1/chat/completions` and `/v1/messages`. A client that | ||
| speaks the Messages API, including Claude Code via `ANTHROPIC_BASE_URL`, talks to | ||
| the deployment directly. See | ||
| [Alternate APIs]({{< ref "/models/model-service.md" >}}) for the routing detail. | ||
|
|
||
| This recipe serves Qwen3-8B on a single NVIDIA H100 on Nebius, with tool calling | ||
| on: `--enable-auto-tool-choice` and `--tool-call-parser=hermes` are what let | ||
| Claude Code's tool use work. An 8B model needs a fraction of an H100, so the GPU | ||
| has ample headroom. Apply the platform side first, then the ML side. | ||
|
|
||
| ## Platform | ||
|
|
||
| {{< manifests "examples/anthropic-messages-api/inference-class.yaml" >}} | ||
|
|
||
| {{< manifests "examples/anthropic-messages-api/inference-cluster.yaml" >}} | ||
|
|
||
| ## Deployment | ||
|
|
||
| {{< manifests "examples/anthropic-messages-api/model-deployment.yaml" >}} | ||
|
|
||
| {{< manifests "examples/anthropic-messages-api/model-service.yaml" >}} | ||
|
|
||
| ## Send a request | ||
|
|
||
| Read the endpoint's public address from the `ModelService` status: | ||
|
|
||
| ```bash | ||
| ADDRESS=$(kubectl get ms qwen3-8b -n ml-team -o jsonpath='{.status.address}') | ||
| ``` | ||
|
|
||
| Post to `/v1/messages` in the Messages API shape. The `model` field is the | ||
| engine's `--served-model-name` (`qwen`); `max_tokens` is required: | ||
|
|
||
| ```bash | ||
| curl "$ADDRESS/v1/messages" \ | ||
| -H "Content-Type: application/json" \ | ||
| -H "anthropic-version: 2023-06-01" \ | ||
| -d '{ | ||
| "model": "qwen", | ||
| "max_tokens": 1024, | ||
| "messages": [{"role": "user", "content": "Hello!"}] | ||
| }' | ||
| ``` | ||
|
|
||
| `status.address` is the in-cluster gateway address, so run the request from | ||
| inside the cluster if your shell can't reach it directly: | ||
|
|
||
| ```bash | ||
| kubectl run -i --rm curl-test \ | ||
| --image=curlimages/curl \ | ||
| --restart=Never \ | ||
| --env="ADDRESS=$ADDRESS" \ | ||
| -- sh -c 'curl -s "$ADDRESS/v1/messages" \ | ||
| -H "Content-Type: application/json" \ | ||
| -H "anthropic-version: 2023-06-01" \ | ||
| -d "{\"model\":\"qwen\",\"max_tokens\":1024,\"messages\":[{\"role\":\"user\",\"content\":\"Hello!\"}]}"' | ||
| ``` | ||
|
|
||
| ## Point Claude Code at it | ||
|
|
||
| Claude Code appends `/v1/messages` to `ANTHROPIC_BASE_URL`, so point it at the | ||
| service address and map its model tiers onto the served name. vLLM doesn't check | ||
| the auth token, so any non-empty value works. Claude Code reserves 32000 output | ||
| tokens by default, which alone leaves little context room on a small model; cap | ||
| it with `CLAUDE_CODE_MAX_OUTPUT_TOKENS` so the input and output fit under the | ||
| engine's `--max-model-len` (40960 here): | ||
|
|
||
| ```bash | ||
| export ANTHROPIC_BASE_URL="$ADDRESS" | ||
| export ANTHROPIC_AUTH_TOKEN=dummy | ||
| export ANTHROPIC_DEFAULT_OPUS_MODEL=qwen | ||
| export ANTHROPIC_DEFAULT_SONNET_MODEL=qwen | ||
| export ANTHROPIC_DEFAULT_HAIKU_MODEL=qwen | ||
| export CLAUDE_CODE_MAX_OUTPUT_TOKENS=8192 | ||
| claude | ||
| ``` | ||
|
|
||
| The gateway must be reachable from wherever `claude` runs. If `$ADDRESS` is | ||
| in-cluster only, forward the Traefik gateway service to a local port: | ||
|
|
||
| ```bash | ||
| kubectl -n traefik-system port-forward svc/traefik 8080:80 | ||
| ``` | ||
|
|
||
| Then point `ANTHROPIC_BASE_URL` at the local port, keeping the service's | ||
| `/<namespace>/<service>` path prefix so Claude Code's `/v1/messages` still routes: | ||
|
|
||
| ```bash | ||
| export ANTHROPIC_BASE_URL="http://localhost:8080/ml-team/qwen3-8b" | ||
| ``` | ||
| <!-- vale write-good.Passive = YES --> |
37 changes: 37 additions & 0 deletions
37
docs/manifests/examples/anthropic-messages-api/inference-class.yaml
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| # InferenceClass for a single-H100 Nebius shape, serving Qwen3-8B. | ||
| # | ||
| # Nebius sizes nodes by platform + preset rather than an instance type; the | ||
| # preset determines the GPU, vCPU, and memory shape of each node. The devices | ||
| # block describes what a node of this class has, DRA-style - used by the | ||
| # scheduler to match models to clusters, and to form DRA ResourceClaims for | ||
| # claim: DRA devices. An 8B model needs a fraction of an H100, so this shape has | ||
| # ample headroom; a single L40S (gpu-l40s-a) is the economical alternative. | ||
| apiVersion: modelplane.ai/v1alpha1 | ||
| kind: InferenceClass | ||
| metadata: | ||
| name: h100-1x | ||
| spec: | ||
| description: "Nebius gpu-h100-sxm, 1x NVIDIA H100 80GB" | ||
| provisioning: | ||
| provider: Nebius | ||
| nebius: | ||
| platform: gpu-h100-sxm | ||
| preset: 1gpu-16vcpu-200gb | ||
| diskSizeGb: 200 | ||
| driversPreset: cuda13.0 | ||
| accelerator: | ||
| type: nvidia-h100 | ||
| count: 1 | ||
| devices: | ||
| - name: gpu | ||
| claim: DRA | ||
| driver: gpu.nvidia.com | ||
| deviceClassName: gpu.nvidia.com | ||
| count: 1 | ||
| attributes: | ||
| architecture: { string: Hopper } | ||
| cudaComputeCapability: { version: "9.0.0" } | ||
| capacity: | ||
| # The H100 80GB's real usable VRAM, what the NVIDIA DRA driver reports, | ||
| # not its nominal 80GB. A nodeSelector asking for >= 80Gi would never bind. | ||
| memory: { value: "81559Mi" } |
25 changes: 25 additions & 0 deletions
25
docs/manifests/examples/anthropic-messages-api/inference-cluster.yaml
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| # An InferenceCluster backed by a Nebius mk8s cluster. Modelplane provisions the | ||
| # full mk8s cluster and installs the inference stack; only the GPU node pool - | ||
| # referencing the InferenceClass above - is declared here. It authenticates to | ||
| # the cluster with the credentials of the Nebius ClusterProviderConfig named | ||
| # default, the same identity that provisions it. | ||
| # | ||
| # Delete with foreground cascading deletion for a clean teardown, so the | ||
| # inference stack uninstalls before the cluster's API server goes away: | ||
| # kubectl delete inferencecluster nebius-eu-north --cascade=foreground | ||
| apiVersion: modelplane.ai/v1alpha1 | ||
| kind: InferenceCluster | ||
| metadata: | ||
| name: nebius-eu-north | ||
| labels: | ||
| modelplane.ai/region: eu-north | ||
| spec: | ||
| cluster: | ||
| source: Nebius | ||
| nebius: {} | ||
| nodePools: | ||
| - name: gpu-h100 | ||
| className: h100-1x | ||
| nodeCount: 1 | ||
| minNodeCount: 1 | ||
| maxNodeCount: 1 |
23 changes: 23 additions & 0 deletions
23
docs/manifests/examples/anthropic-messages-api/inference-gateway.yaml
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| # The InferenceGateway creates a unified, OpenAI-compatible endpoint on the | ||
| # control plane cluster. It installs Traefik Proxy and creates a Gateway that | ||
| # routes traffic to model replicas on remote inference clusters. | ||
| # | ||
| # Create one InferenceGateway per control plane. It must be named "default". | ||
| # | ||
| # For kind or bare-metal clusters, set loadBalancer to MetalLB and configure an | ||
| # address pool. For cloud clusters with native LoadBalancer support, omit the | ||
| # loadBalancer field entirely. | ||
| apiVersion: modelplane.ai/v1alpha1 | ||
| kind: InferenceGateway | ||
| metadata: | ||
| name: default | ||
| spec: | ||
| backend: Traefik | ||
| traefik: | ||
| version: "40.2.0" | ||
|
|
||
| # Remove the loadBalancer section if your cluster supports LoadBalancer | ||
| # services natively (e.g. GKE, EKS). | ||
| loadBalancer: MetalLB | ||
| metallb: | ||
| addressPool: "172.18.255.200-172.18.255.250" |
67 changes: 67 additions & 0 deletions
67
docs/manifests/examples/anthropic-messages-api/model-deployment.yaml
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,67 @@ | ||
| # Qwen3-8B served on a single NVIDIA H100 (Nebius) by vLLM, exposed on the | ||
| # Anthropic Messages API. vLLM's server registers /v1/messages (and | ||
| # /v1/messages/count_tokens) alongside its OpenAI routes with no extra flag, so | ||
| # this is an ordinary serve: the Anthropic surface comes for free, and | ||
| # Modelplane preserves the path below the service prefix to reach it. | ||
| # | ||
| # The tool-calling flags are what make it usable from Claude Code, not decoration: | ||
| # | ||
| # --enable-auto-tool-choice with | ||
| # --tool-call-parser=hermes parse the model's tool calls so Claude Code's | ||
| # tool use works (qwen3_xml is for Qwen3-Coder, | ||
| # not this dense model). Qwen3's tool-use | ||
| # template ships in the tokenizer, so no | ||
| # --chat-template is needed. | ||
| # --reasoning-parser=qwen3 with | ||
| # --default-chat-template-kwargs turns thinking off. Qwen3 thinks by default, | ||
| # burying a one-line answer under a <think> | ||
| # block and forbidding greedy decode. | ||
| # --max-model-len=40960 Qwen3-8B's native context (past it needs YaRN). | ||
| # Claude Code reserves 32000 output tokens, so a | ||
| # smaller window 500s ("max_completion_tokens | ||
| # cannot be greater than max_model_len", then | ||
| # input+output overflow). The client must also | ||
| # cap output with CLAUDE_CODE_MAX_OUTPUT_TOKENS | ||
| # so input+output fit here. H100 has KV room. | ||
| # --gpu-memory-utilization headroom, not correctness. | ||
| # | ||
| # The v0.23.0 image is >0.17.1, so vLLM handles Claude Code's attribution header | ||
| # without breaking prefix caching. No --port or --host: Modelplane's routing | ||
| # expects the engine on its default :8000 with a /health probe, and passes args | ||
| # through verbatim. | ||
| apiVersion: modelplane.ai/v1alpha1 | ||
| kind: ModelDeployment | ||
| metadata: | ||
| name: qwen3-8b | ||
| namespace: ml-team | ||
| spec: | ||
| replicas: 1 | ||
| template: | ||
| spec: | ||
| # No clusterSelector: the single Nebius cluster is matched on device | ||
| # capacity alone. | ||
| engines: | ||
| - name: qwen3-8b | ||
| members: | ||
| - role: Standalone | ||
| nodeSelector: | ||
| devices: | ||
| - name: gpu | ||
| count: 1 | ||
| selectors: | ||
| - cel: | | ||
| device.capacity["gpu.nvidia.com"].memory.compareTo(quantity("20Gi")) >= 0 | ||
| template: | ||
| spec: | ||
| containers: | ||
| - name: engine | ||
| image: vllm/vllm-openai:v0.23.0 | ||
| args: | ||
| - "--model=Qwen/Qwen3-8B" | ||
| - "--served-model-name=qwen" | ||
| - "--max-model-len=40960" | ||
| - "--gpu-memory-utilization=0.92" | ||
| - "--reasoning-parser=qwen3" | ||
| - "--default-chat-template-kwargs={\"enable_thinking\": false}" | ||
| - "--enable-auto-tool-choice" | ||
| - "--tool-call-parser=hermes" |
16 changes: 16 additions & 0 deletions
16
docs/manifests/examples/anthropic-messages-api/model-service.yaml
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| # Exposes the qwen3-8b deployment as a single URL. The route matches the | ||
| # /<namespace>/<service>/ prefix and preserves the path below it, so the engine's | ||
| # Anthropic Messages API at .../v1/messages rides the same address as its | ||
| # OpenAI-compatible .../v1/chat/completions. Read the public address from | ||
| # status.address: | ||
| # kubectl get ms qwen3-8b -n ml-team -o jsonpath='{.status.address}' | ||
| apiVersion: modelplane.ai/v1alpha1 | ||
| kind: ModelService | ||
| metadata: | ||
| name: qwen3-8b | ||
| namespace: ml-team | ||
| spec: | ||
| endpoints: | ||
| - selector: | ||
| matchLabels: | ||
| modelplane.ai/deployment: qwen3-8b |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
add opt-in
NIX_SH_PORTSso services in the in-container kind cluster can be reached from the host for local testing (dev-only, inert unless set)...helps with:
kubectl -n traefik-system port-forward --address 0.0.0.0 svc/traefik 8080:80from inside the develop nix shell to get access on the host ,,,