feat: Add a default /v1/messages (Anthropic Messages) route to the base Gym…#1627
Conversation
… model server. Signed-off-by: Felipe Vieira Frujeri <ffrujeri@nvidia.com>
Adopt the official anthropic SDK types for the /v1/messages <-> Responses mapping instead of hand-rolled dicts: validate the egress Anthropic response via Message.model_validate and hint requests with MessageCreateParams. Types only — the SDK client is never used (it uses httpx, whose O(n^2) connection pooling hangs at high concurrency); all transport stays on aiohttp. Pin anthropic<=0.109.2, mirroring the openai pin. Add converter test coverage for empty output, empty-text system lists, and system-role messages. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Lin Jia <linj@nvidia.com>
… note Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Lin Jia <linj@nvidia.com>
Manual test — Tier 1 → Tier 2 (live)Validated the default Setup: Tier 1 — proxy
Tier 2 — real
Gotcha (now documented in the agent README): use Smoke-test commands added in c16946c. |
…ssages-proxy Signed-off-by: Lin Jia <linj@nvidia.com>
…ssages-proxy Signed-off-by: Lin Jia <linj@nvidia.com>
Add nemo_gym/anthropic_utils.py with the two boundary types for the Anthropic Messages API, mirroring openai_utils.py's wrapping strategy but deliberately minimal (no *ForTraining hierarchy, no async client): - NeMoGymAnthropicMessageCreateParamsNonStreaming: request validator, a BaseModel copy of the SDK's MessageCreateParams TypedDict with extra="forbid" and Iterable fields overridden to List for strict server-side validation at the ingress proxy. - NeMoGymAnthropicMessage: response validator, a thin subclass of the SDK's Message used to validate what we emit. Wire NeMoGymAnthropicMessage into AnthropicConverter's response validation path and add unit tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Lin Jia <linj@nvidia.com>
✅ End-to-end smoke test with the real Claude Code CLIVerified the default Setup
Results
The tool-use case round-trips the full bidirectional converter, including tool-call mapping in both directions. Reproduce RAY_TMPDIR=/tmp ng_run "+config_paths=[responses_api_models/vllm_model/configs/vllm_model.yaml]" \
++policy_base_url=https://integrate.api.nvidia.com/v1 \
++policy_model_name=meta/llama-3.3-70b-instruct ++policy_api_key="$NVAPI_KEY" \
++policy_model.responses_api_models.vllm_model.uses_reasoning_parser=false \
++policy_model.responses_api_models.vllm_model.uses_interleaved_reasoning=false &
# discover port via `ng_status`, then:
ANTHROPIC_BASE_URL=http://127.0.0.1:<port> ANTHROPIC_AUTH_TOKEN=dummy \
claude -p "Use the Bash tool to run: echo hello-from-tool — then tell me what it printed." \
--dangerously-skip-permissionsNote: a direct local-vLLM backend was not usable in this environment (vLLM's Triton JIT-compiles 🤖 Generated with Claude Code |
Resolve add/add conflicts in anthropic_converter and its tests by keeping main (#1627) as the source of truth.
…se Gym… (#1627) # What does this PR do? [Design note](#1620 (comment)) Makes **every NeMo Gym model server speak the Anthropic Messages API** by adding a `POST /v1/messages` route to the base `SimpleResponsesAPIModel`, with a default implementation that maps Anthropic Messages ↔ Responses around the server's own `responses()`. External harnesses that require an Anthropic endpoint — notably `claude_code_agent` (the Claude Code CLI only talks `/v1/messages`) — can now run against **any** Gym backend (vLLM, OpenAI, an inference provider), not just Anthropic's API. The Messages ↔ Responses mapping lives in one shared, transport-free, SDK-free converter, `nemo_gym/anthropic_converter.py`, used in both directions and shared with the egress `anthropic_model` provider (#1146/#1546). See `DESIGN-NOTE.md` for rationale. # Why Gym standardizes on the Responses API, but real blackbox agents speak other protocols. Rather than a bespoke per-agent bridge or a separate proxy server, the Messages spoke becomes a base-class capability: implement `responses()` once and get `/v1/messages` for free. This is the model-proxy layer the CustomAgent EPIC (#1042) assumes — its sidecar "path-preserving forwards" `/v1/messages` to these routes. # Key design points - **Route on the base class, not a separate proxy.** Every model server inherits `/v1/messages`; no extra process or network hop. (An earlier `anthropic_messages_proxy` + `SimpleMessagesAPIModel` iteration was removed in favor of this.) - **Delegates to the server's own `responses()`** — reuses whatever backend the server has; a small `inspect`-based dispatch handles both existing `responses()` signatures. - **Synthesized SSE.** Backends are non-streaming via Responses; the converter fabricates the Anthropic SSE event stream from the complete response (the Claude CLI requires streaming). - **Shared bidirectional converter**, transport-free and **no `anthropic` SDK** (local shapes over Gym's aiohttp). Fail-loud (`NotImplementedError`) on unsupported content. - **Eval-first.** Token-ids live only at the Responses hop and cannot ride the Anthropic SSE response; an RL path would side-channel them, not thread them through Messages. # Issues Closes #1620 (*"Add a default `/v1/messages` (Anthropic Messages) route to the base Gym model server"*). Closes #1566 Relationship to adjacent work: - **#1620** — this PR is its implementation: the `/v1/messages` route + default Messages ↔ Responses mapping on `SimpleResponsesAPIModel`, the shared converter, and the `claude_code_agent` end-to-end run satisfy its acceptance criteria. - **#1146 / #1546** (egress `anthropic_model`) — inverse direction; shares the same converter. - **#1286** (`responses_converter`) — sibling Chat-Completions ↔ Responses converter; same pattern. - **EPIC #1042** (CustomAgent) — its sidecar model proxy "path-preserving forwards" `/v1/messages` to the routes this PR adds. # Usage Every model server now answers Anthropic Messages directly — this is config-independent and is the core of the change. Against any running Gym model server: ```bash curl -s http://127.0.0.1:<model_server_port>/v1/messages -H 'content-type: application/json' -d '{ "model": "x", "max_tokens": 64, "messages": [{"role": "user", "content": "hi"}] }' ``` `claude_code_agent` reaches any Gym model server by setting its `model_server` ref — the harness resolves `ANTHROPIC_BASE_URL` from it and the CLI appends `/v1/messages` (see `responses_api_agents/claude_code_agent/app.py:_resolve_base_url`). The end-to-end run in **Testing** below used this path against a local vLLM. The verified `reasoning_gym` Claude Code config keeps its direct `anthropic_base_url` default; targeting a Gym model server is an opt-in `model_server` ref. # Additional information - **Changed:** `nemo_gym/base_responses_api_model.py` (new `/v1/messages` route + default `messages()`); **added** `nemo_gym/anthropic_converter.py` (shared bidirectional converter); `responses_api_agents/claude_code_agent/README.md` (documents the model-server path). No change to the verified `reasoning_gym_claude_code_agent.yaml` — targeting a Gym model server is an opt-in `model_server` ref on the agent. - **Converter surface:** `responses_to_anthropic` / `anthropic_to_responses` (egress, shared with #1546); `anthropic_request_to_responses` / `responses_to_anthropic_response` / `anthropic_response_to_sse` (ingress). Supports text, base64 images, function tools, tool_use/tool_result, thinking, stop-reason. Anthropic-API egress *policy* stays in the `anthropic_model` server, not the converter. # Testing - `tests/unit_tests/test_anthropic_converter.py` (ingress + round-trip) and `…_egress.py` (egress) — **100%** converter coverage. - `tests/unit_tests/test_responses_api_model_messages.py` — base-class `/v1/messages` for both `responses()` signatures + SSE framing. - Full unit suite green (284); `openai_model` tests pass unchanged. - **Live e2e:** `claude_code_agent → policy_model (vLLM) /v1/messages` on reasoning_gym — `finished_naturally=1`, 1 turn, ~5.5k tokens, 0 failures (reward reflects the small model's answer, not the plumbing). - Verified: `ruff check`, `ruff format --check`, `pre-commit`. # Open questions - Versioning/regression suite for the converters (OpenAI/Anthropic bump frequently). - Audit/align the existing Chat-Completions ↔ Responses tests. - Confirm fail-loud vs drop policy for items absent in a target schema (e.g. computer-use). --------- Signed-off-by: Felipe Vieira Frujeri <ffrujeri@nvidia.com> Signed-off-by: Lin Jia <linj@nvidia.com> Co-authored-by: Lin Jia <linj@nvidia.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Rita Fernandes Neves <rfernandesne@nvidia.com>
What does this PR do?
Design note
Makes every NeMo Gym model server speak the Anthropic Messages API by adding a
POST /v1/messagesroute to the baseSimpleResponsesAPIModel, with a default implementationthat maps Anthropic Messages ↔ Responses around the server's own
responses(). Externalharnesses that require an Anthropic endpoint — notably
claude_code_agent(the Claude Code CLIonly talks
/v1/messages) — can now run against any Gym backend (vLLM, OpenAI, an inferenceprovider), not just Anthropic's API.
The Messages ↔ Responses mapping lives in one shared, transport-free, SDK-free converter,
nemo_gym/anthropic_converter.py, used in both directions and shared with the egressanthropic_modelprovider (#1146/#1546).Why
Gym standardizes on the Responses API, but real blackbox agents speak other protocols. Rather
than a bespoke per-agent bridge or a separate proxy server, the Messages spoke becomes a
base-class capability: implement
responses()once and get/v1/messagesfor free. This isthe model-proxy layer the CustomAgent EPIC (#1042) assumes — its sidecar "path-preserving
forwards"
/v1/messagesto these routes.Key design points
/v1/messages;no extra process or network hop. (An earlier
anthropic_messages_proxy+SimpleMessagesAPIModeliteration was removed in favor of this.)
responses()— reuses whatever backend the server has; asmall
inspect-based dispatch handles both existingresponses()signatures.Anthropic SSE event stream from the complete response (the Claude CLI requires streaming).
anthropicSDK (local shapesover Gym's aiohttp). Fail-loud (
NotImplementedError) on unsupported content.response; an RL path would side-channel them, not thread them through Messages.
Issues
Closes #1620 ("Add a default
/v1/messages(Anthropic Messages) route to thebase Gym model server").
Closes #1566
Relationship to adjacent work:
/v1/messages(Anthropic Messages) route to the base Gym model server #1620 — this PR is its implementation: the/v1/messagesroute + default Messages ↔Responses mapping on
SimpleResponsesAPIModel, the shared converter, and theclaude_code_agentend-to-end run satisfy its acceptance criteria.anthropic_model) — inverse direction; shares the same converter.responses_converter) — sibling Chat-Completions ↔ Responses converter; same pattern./v1/messagesto the routes this PR adds.
Usage
Every model server now answers Anthropic Messages directly — this is config-independent and is
the core of the change. Against any running Gym model server:
claude_code_agentreaches any Gym model server by setting itsmodel_serverref — the harnessresolves
ANTHROPIC_BASE_URLfrom it and the CLI appends/v1/messages(seeresponses_api_agents/claude_code_agent/app.py:_resolve_base_url). The end-to-end run inTesting below used this path against a local vLLM. The verified
reasoning_gymClaude Codeconfig keeps its direct
anthropic_base_urldefault; targeting a Gym model server is an opt-inmodel_serverref.Additional information
nemo_gym/base_responses_api_model.py(new/v1/messagesroute + defaultmessages()); addednemo_gym/anthropic_converter.py(shared bidirectional converter);responses_api_agents/claude_code_agent/README.md(documents the model-server path). Nochange to the verified
reasoning_gym_claude_code_agent.yaml— targeting a Gym model server isan opt-in
model_serverref on the agent.responses_to_anthropic/anthropic_to_responses(egress, sharedwith feat: Add anthropic messages api support as a responses_api_model. #1546);
anthropic_request_to_responses/responses_to_anthropic_response/anthropic_response_to_sse(ingress). Supports text, base64 images, function tools,tool_use/tool_result, thinking, stop-reason. Anthropic-API egress policy stays in the
anthropic_modelserver, not the converter.Testing
tests/unit_tests/test_anthropic_converter.py(ingress + round-trip) and…_egress.py(egress) — 100% converter coverage.
tests/unit_tests/test_responses_api_model_messages.py— base-class/v1/messagesfor bothresponses()signatures + SSE framing.openai_modeltests pass unchanged.claude_code_agent → policy_model (vLLM) /v1/messageson reasoning_gym —finished_naturally=1, 1 turn, ~5.5k tokens, 0 failures (reward reflects the small model'sanswer, not the plumbing).
ruff check,ruff format --check,pre-commit.Open questions