Skip to navigation

vLLM is a popular LLM inference engine. The NeMo Gym VLLMModel server wraps vLLM’s Chat Completions endpoint and converts requests and responses to NeMo Gym’s native format, the OpenAI Responses API schema.

Most open-source models use Chat Completions format, while NeMo Gym uses the Responses API natively. VLLMModel bridges this gap by converting between the two formats automatically. For background on why NeMo Gym chose the Responses API and how the two schemas differ, see Responses API.

VLLMModel provides a Responses API to Chat Completions mapping middleware layer via responses_api_models/vllm_model. It assumes you are pointing to a vLLM instance since it relies on vLLM-specific endpoints like /tokenize and vLLM-specific arguments like return_tokens_as_token_ids.

Two upstream backends

VLLMModel can drive either of vLLM’s OpenAI-compatible endpoints, selected by the use_completions_api config flag. Both backends keep the same external Gym surface — /v1/responses and /v1/chat/completions continue to work identically; only the call to vLLM swaps.

use_completions_apivLLM endpoint hitPrimary use case
false (default)POST /v1/chat/completionsInstruct models. vLLM applies the chat template, runs reasoning- and tool-call parsers server-side. The default rollout config (vllm_model.yaml) and training config (vllm_model_for_training.yaml) both target this backend.
truePOST /v1/completionsBase (non-instruct) models, or cases where the caller has already rendered the full prompt string upstream and wants the bytes forwarded verbatim. See The /v1/completions backend below.

To use VLLMModel, just change the responses_api_models/openai_model/configs/openai_model.yaml in your config paths to responses_api_models/vllm_model/configs/vllm_model.yaml!

gym env start \
--resources-server example_multi_step \
--model-type vllm_model

VLLMModel connects NeMo Gym to a vLLM server that you start and manage yourself. If you would prefer NeMo Gym to launch and manage vLLM itself, use LocalVLLMModel instead. See LocalVLLMModel to learn more.

Use VLLMModel

Below is an e2e example of how to spin up a NeMo Gym compatible vLLM Chat Completions OpenAI server and run rollout collection with it. This section walks through starting a vLLM server manually and connecting NeMo Gym to it through responses_api_models/vllm_model. If you want NeMo Gym to manage the vLLM server lifecycle for you instead, see LocalVLLMModel.

Install vLLM

Please run the steps below in a separate terminal than your NeMo Gym terminal! The installation will take a few minutes.

uv venv --python 3.13.14 --seed .venv
source .venv/bin/activate
# hf_transfer for faster model download
uv pip install hf_transfer vllm --torch-backend=auto

Recommended on workstations without a system CUDA toolkit (i.e. no /usr/local/cuda). Install FlashInfer’s pre-built kernel packages so vLLM’s sampler and allreduce paths do not try to JIT-compile via nvcc at startup. Both companion packages enforce strict version equality with the flashinfer-python that vLLM pulled in, so pin them to its exact version:

# Discover the flashinfer-python version that vLLM installed
FI_VER=$(python -c "import flashinfer; print(flashinfer.__version__)")
echo "flashinfer-python: $FI_VER"
# Discover your torch CUDA tag (e.g. cu128, cu129, cu130)
TORCH_CU=$(python -c "import torch; print('cu' + torch.version.cuda.replace('.', ''))")
echo "torch CUDA tag: $TORCH_CU"
# Pre-compiled CUBINs (CUTLASS / TRTLLM-gen kernels)
uv pip install "flashinfer-cubin==${FI_VER}"
# Pre-built JIT object cache for your CUDA build
uv pip install "flashinfer-jit-cache==${FI_VER}" --index-url "https://flashinfer.ai/whl/${TORCH_CU}"

Without these, the first sampling step inside vllm serve may fail with:

RuntimeError: Could not find nvcc and default cuda_home='/usr/local/cuda' doesn't exist

because PyPI’s PyTorch wheels ship the CUDA runtime libraries but not the nvcc compiler that FlashInfer’s JIT path falls back to.

If you install the companions without pinning their versions (e.g. uv pip install flashinfer-jit-cache --index-url https://flashinfer.ai/whl/cu130), the resolver picks the newest wheel on the index, and you will hit a different error at first sampling:

RuntimeError: flashinfer-jit-cache version (X.Y.Z+cuNNN) does not match flashinfer version (A.B.C)

That’s the version-equality check in flashinfer/jit/env.py — fix by pinning both companions to flashinfer.__version__ as shown above. See the FlashInfer installation docs for details.

Download the model

This download will take a few minutes.

# Qwen/Qwen3-4B-Thinking-2507, usable in Nemo RL!
HF_HOME=.cache/ \
HF_HUB_ENABLE_HF_TRANSFER=1 \
hf download Qwen/Qwen3-4B-Thinking-2507

If you get errors relating to HuggingFace rate limits, please provide your HF token to command above.

HF_TOKEN=... \
HF_HOME=.cache/ \
HF_HUB_ENABLE_HF_TRANSFER=1 \
hf download Qwen/Qwen3-4B-Thinking-2507

If you do not have a HuggingFace token, please follow the instructions here to create one!

Spin up a vLLM server

vLLM server configuration

  • If you want to use tools, find the appropriate vLLM arguments regarding the tool call parser to use. In this example, we use Qwen/Qwen3-4B-Thinking-2507, which is suggested to use the hermes tool call parser.
  • If you are using a reasoning model, find the appropriate vLLM arguments regarding reasoning parser to use. In this example, we use Qwen/Qwen3-4B-Thinking-2507, which is suggested to use the deepseek_r1 reasoning parser.
  • The example below uses --tensor-parallel-size 1 which requires 1 GPU.

The spinup step will take a few minutes.

HF_HOME=.cache/ \
HOME=. \
vllm serve \
Qwen/Qwen3-4B-Thinking-2507 \
--tensor-parallel-size 4 \
--enable-auto-tool-choice --tool-call-parser hermes \
--reasoning-parser deepseek_r1 \
--host 0.0.0.0 \
--port 10240

Configure NeMo Gym to use the local vLLM server

In a second terminal on the same GPU node that was used to spin up the vLLM server, enter the NeMo Gym Python environment, and start the NeMo Gym servers.

gym env start \
--resources-server example_multi_step \
--model-type vllm_model \
--model-url http://0.0.0.0:10240/v1 \
--model Qwen/Qwen3-4B-Thinking-2507 \
--model-api-key dummy_key

If you want to run NeMo Gym on a separate machine from the one used to spin up the vLLM server, please get the hostname of the machine used to run the vLLM server.

hostname -i

Then change --model-url http://0.0.0.0:10240/v1 to --model-url http://{hostname}:10240/v1.

Run rollout collection

In a third terminal on the same GPU node that was used to spin up the vLLM server, enter the NeMo Gym Python environment, and run rollout collection.

gym eval run --no-serve \
--agent example_multi_step_simple_agent \
--input resources_servers/example_multi_step/data/example.jsonl \
--output results/example_multi_step_rollouts.jsonl

The /v1/completions backend

Set use_completions_api: true to drive vLLM’s text-completions endpoint instead of chat completions. A ready-to-use config ships at responses_api_models/vllm_model/configs/vllm_model_completions.yaml:

policy_model:
responses_api_models:
vllm_model:
entrypoint: app.py
base_url: ${policy_base_url}
api_key: ${policy_api_key}
model: ${policy_model_name}
return_token_id_information: false
uses_reasoning_parser: false # /v1/completions does not run vLLM's reasoning parser
uses_interleaved_reasoning: false
use_completions_api: true # drives vLLM /v1/completions

Render modes

A second flag, render_chat_template, picks how the caller’s messages list becomes the prompt string:

  • render_chat_template: false (default) — raw render. Forwards bytes verbatim. Required input: a single user message, optionally preceded by a single system message; their content is joined with \n\n and sent as prompt. tools, multi-turn turns, and non-text content blocks are rejected. This is the cheapest path and the most common base-model setup.
  • render_chat_template: true — chat-template render. Renders the messages list to a prompt string client-side via HF AutoTokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True, tools=..., **chat_template_kwargs). Multi-turn assistant / tool turns and tools are allowed.

For Gym’s /v1/responses endpoint, a string input is forwarded as the prompt directly under raw mode; under chat-template mode the converter still wraps it as a single user message before rendering.

tokenizer config (chat-template mode)

When render_chat_template: true, the HF tokenizer is loaded once at server startup. By default it’s loaded from the same identifier as model. Override with tokenizer: for base-model setups where the model checkpoint has no chat template in its tokenizer config — point tokenizer: at a different model whose template you want to inherit:

model: Qwen/Qwen2.5-7B # base model, no chat template
use_completions_api: true
render_chat_template: true
tokenizer: Qwen/Qwen2.5-7B-Instruct # borrow the instruct chat template

If the loaded tokenizer has no chat_template, the server fails at startup — silent fallback to raw mode would mask a config bug. The transformers package is required at runtime for chat-template mode (it’s a Gym dep, but if it’s missing the startup error message points at it explicitly).

Constraint matrix

Constraintrender_chat_template: falserender_chat_template: true
Single user / [system, user] onlyrequiredlifted — full multi-turn ok
Assistant / tool turnsrejectedallowed
Toolsrejectedallowed — see note below
Audio / image contentrejectedrejected (text-only endpoint)
chat_template_kwargsunusedforwarded to apply_chat_template

Tools in chat-template mode. The chat template renders the tool definitions into the prompt (Hermes / Qwen / etc. all do this), but /v1/completions doesn’t run vLLM’s tool-call parser regardless of how the prompt is built — so any tool-call output text the model emits is not parsed by Gym. The caller is responsible for parsing tool calls out of the assistant text. If you need first-class tool-call routing, use use_completions_api: false.

<think>...</think> blocks emitted inline by the model are extracted downstream by ChatCompletionsConverter._extract_reasoning_from_content when results are converted back to a Response, exactly as on the chat path.

Token-ID extraction (RL training)

When return_token_id_information: true, the completions backend automatically adds logprobs: 0, return_token_ids: true, and return_tokens_as_token_ids: true to outbound requests. It reads prompt and generation token IDs directly from each choice. For older vLLM versions that omit inline IDs, it falls back to /tokenize for the prompt and the "token_id:<int>" entries in logprobs.tokens for the generation.

Use it

The end-to-end flow is identical to the Use VLLMModel walkthrough above; just swap the model-server config:

gym env start \
--config resources_servers/example_multi_step/configs/example_multi_step.yaml \
--config responses_api_models/vllm_model/configs/vllm_model_completions.yaml \
--model-url http://0.0.0.0:10240/v1 \
--model <your-model> \
--model-api-key dummy_key
gym eval run --no-serve \
--agent <your_agent> \
--input <your_data.jsonl> \
--output results/rollouts_completions.jsonl

Make sure your input JSONL respects the selected mode in the Constraint matrix above.

VLLMModel configuration reference

ParameterTypeDefaultDescription
base_urlstr or list[str]—Required. vLLM server endpoint(s). Supports list for load balancing.
api_keystr—Required. API key matching vLLM’s --api-key flag.
modelstr—Required. Model name as registered in vLLM.
return_token_id_informationbool—Required. Set true for training (token IDs + log probs), false for inference only.
uses_reasoning_parserbool—Required. Set true for reasoning models (extracts <think> tags), false otherwise.
reasoning_fieldstrbothWhich key(s) carry the <think> reasoning that vllm_model extracts from replayed assistant messages (uses_reasoning_parser: true, uses_interleaved_reasoning: true, preserve_reasoning_in_assistant_content: false): both, reasoning, or reasoning_content. Use a single key for servers that reject a message carrying both. When unset, falls back to the NEMO_GYM_REASONING_FIELD environment variable, then both.
refit_output_budget_to_windowbooltrueWhen the engine refuses a call for length because the requested output budget (max_tokens or max_completion_tokens) alone exceeds the context window, send the call once more with the budget lowered to the window minus the prompt minus a 16-token margin. The prompt length comes from the refusal, else from one /tokenize call. The model call’s capture records requested_max_tokens, applied_max_tokens and an output_budget_refit event. A budget within the window is not changed by this setting (see refit_output_budget_to_prompt). Set false to keep the refusal.
refit_output_budget_to_promptboolfalseOpt in to the same single resend when the requested output budget fits the context window by itself but the prompt and the budget together overflow it (for example a 131072-token budget in a 262144-token window, once the prompt passes 131072 tokens). The call is sent once more with the budget lowered to the window minus the prompt minus the 16-token margin, so the model answers with what the window leaves instead of the call ending on an empty length completion. The prompt length and the capture are the same as for refit_output_budget_to_window, and the refit is recorded in the same output_budget_refit event; it does not depend on that setting. A prompt that alone fills the window leaves no output token and keeps the context_length_exceeded refusal. Off by default, because a harness that compacts its history relies on that refusal as its cue; turn it on for a benchmark whose agent does not compact and whose output budget is large against the window.
replace_developer_role_with_systemboolfalseConvert “developer” role to “system” for models that don’t support developer role.
chat_template_kwargsdictnullOverride chat template parameters. Forwarded to upstream chat completions when use_completions_api: false, or to apply_chat_template when use_completions_api: true and render_chat_template: true.
forward_request_chat_template_kwargsboolfalseMerge a request’s top-level chat_template_kwargs over the configured value. When false, the request field is accepted and dropped, with a warning logged once. Chat path only: ignored when use_completions_api: true.
extra_bodydictnullPass additional vLLM-specific parameters (e.g., guided_json).
session_key_headerslist[str][]Names of headers that carry the current session key on every request to the engine made for a rollout, so a router can keep a session on one replica. Requires rollout correlation: set ++rollout_correlation=true (see Keep a Session on One Replica). Requests outside a rollout get no generated header. Static headers from default_headers still apply.
use_completions_apiboolfalseWhen true, drives vLLM’s /v1/completions instead of /v1/chat/completions. See Two upstream backends.
render_chat_templateboolfalseOnly consulted when use_completions_api: true. When true, renders messages to the prompt string client-side via AutoTokenizer.apply_chat_template. Lifts the multi-turn restriction. See Render modes.
tokenizerstrnullOnly consulted when use_completions_api: true and render_chat_template: true. HF identifier or local path passed to AutoTokenizer.from_pretrained. When null, falls back to model. Use this to borrow a chat template from a different model.
drop_hosted_toolsboolfalseStreaming (stream: true) requests only; available on every model server. When true, every tool type other than function and custom is dropped from the request before it reaches the backend: the provider-hosted tools (web search, file search, code interpreter, image generation, computer use, remote MCP) and client-executed built-in types such as local_shell. A backend that serves none of them cannot answer a call to one, so the model sees only the tools the client executes.
drop_custom_toolsboolfalseStreaming requests only; available on every model server. When true, custom (free-form) tools are dropped instead of being converted to Chat Completions custom tools, which a backend that expresses function tools only rejects with 422. Leave false for a backend with free-form tool support, or set custom_tool_strategy: function_bridge to keep the capability on a function-only backend instead of dropping it.
custom_tool_strategystr"passthrough"How a Responses custom (free-form) tool reaches a backend whose chat schema admits function tools only. passthrough sends the converted request unchanged and the backend decides: vLLM’s chat route refuses a request carrying a Chat custom tool, and drop_custom_tools remains the kill switch. function_bridge lowers each custom tool to a function tool with one required plain-string input parameter, replays custom_tool_call/custom_tool_call_output history in the lowered form, lifts a returned call whose name the request declared as custom back into a custom_tool_call item, and hoists custom entries from a streaming harness’s additional_tools carrier instead of dropping them. A grammar format on the tool is dropped at the lowering (warned once per tool name): no vLLM tool parser enforces a grammar through function calling. Honored by the servers that run the Responses-to-Chat conversion — vllm_model and its subclasses (the compaction and GenRM servers included), inference_provider, and azure_openai_model; rejected together with is_responses_native: true, whose native route renders function tools only. Parser caveat: with a JSON-format tool parser (hermes, llama3_json, mistral) the model must JSON-escape the free-form payload inside the bridged arguments object, and an escaping mistake surfaces as plain text with no tool call; XML-style parsers (qwen3_coder, nemotron_v3) carry the payload as raw text.
stream_keepalive_sfloatnullStreaming /v1/responses requests only; available on model servers that build their own responses (the Chat Completions conversion here, azure_openai_model, inference_provider), and refused at startup with is_responses_native: true. Responses streaming is buffered, so the client sees no event until the whole generation returns, and a client that bounds the silence between events (the Codex CLI’s stream_idle_timeout_ms, 300 s by default) abandons long turns. SSE comments do not reset such a timer. When set, a call still running after this many seconds opens the stream with response.created, then sends response.in_progress every this many seconds; the item and terminal events that follow are unchanged, and every event names the same response id, which capture records too. A failure after the stream opened ends it with response.failed, carrying the error code the client would otherwise have received (for example context_length_exceeded), instead of an HTTP error status.
drop_unrepresentable_request_fieldsboolfalseHow the Responses-to-Chat-Completions conversion treats a request field with no Chat Completions representation. When false, the conversion refuses the request with NotImplementedError, so the caller can route it to a server that serves Responses natively. When true, the reporting-only fields are dropped and the request proceeds: a non-empty include (every value; an empty include is already treated as absent), reasoning.summary and reasoning.generate_summary, and a message phase. A reasoning.context of auto or all_turns (the Codex CLI sends all_turns when its model catalog enables reasoning context) is dropped too: the conversion passes every reasoning item the request carries to the engine and leaves what is rendered to its chat template, which is what those values ask for. An image part in a tool’s output is replaced with the text placeholder [image omitted from tool output], so the model does not see that image, unless inline_tool_output_images is on. Every other unrepresentable field, a current_turn reasoning context included, is still refused. The setting also applies to the compaction model server (vllm_model_with_compaction) and has no effect when is_responses_native is true, because that path skips the conversion.
forward_unvalidated_reasoning_effortboolfalseRequires is_responses_native: false and use_completions_api: false. Forward a Responses reasoning.effort that the pinned OpenAI SDK does not list verbatim as the chat reasoning_effort, instead of refusing the request with 422. The engine validates the value. Efforts the SDK lists keep their existing path. The compaction model server (vllm_model_with_compaction) rejects it.
inline_tool_output_imagesboolfalseRequires is_responses_native: false. Send each image part of a Responses tool call output, such as the Codex CLI’s view_image returns, to the engine as an image_url part of the chat tool message. OpenAI’s chat tool messages carry text only, so by default such an output is refused, or replaced with a text placeholder under drop_unrepresentable_request_fields. vLLM accepts image parts in a tool message and leaves their rendering to the chat template, so a vision model sees the image inside the tool result. Detail original, which chat lacks, is sent as high. Other part types a tool message cannot carry are still refused.
endpoint_connection_retry_timeout_sfloatnullWall-clock seconds one model call may spend retrying connection errors before the last one is raised. See Bound retries to a dead model server.

Advanced: chat_template_kwargs

Override chat template behavior for specific models:

chat_template_kwargs:
enable_thinking: false # Model-specific

Per-request values can come from two places, merged over the configured value in this order (later wins):

  1. A request’s top-level chat_template_kwargs (for example, OpenAI SDK clients that pass it in extra_body). Applied only when forward_request_chat_template_kwargs: true; otherwise it is dropped and a warning is logged once per server. Forwarding changes generation for agents that send it on every call, such as Stirrup’s enable_thinking, so re-run baselines when you enable it.
  2. metadata.chat_template_kwargs, a JSON-encoded object on the request.

With use_completions_api: true, only the configured value and metadata.chat_template_kwargs apply. Streaming (stream: true) requests never forward the top-level field.

Advanced: extra_body

Pass vLLM-specific parameters not in the standard OpenAI API:

extra_body:
guided_json: '{"type": "object", "properties": {...}}'
min_tokens: 10
repetition_penalty: 1.1

The shipped vllm_model configs leave chat_template_kwargs, sampling_overrides, and extra_body unset, so a value a benchmark or environment config sets for policy_model survives --model-type vllm_model, whose config merges after the benchmark’s. A null there would replace it.

Bound retries to a dead model server

By default a model call retries a refused or dropped connection without a time limit, so a training run can ride out an engine restart. For evaluation, where a dead engine should fail the run, set a cap:

endpoint_connection_retry_timeout_s: 300

The value must be positive. The window starts at the call’s first connection error and closes when the server answers, so it bounds only an unbroken run of connection failures. A call that has connected and is generating is not cut short, and HTTP-status retries (for example a 503 with Retry-After) keep their own limits. Each model call gets its own window, so a caller that retries the failed call (for example the OpenAI SDK’s max_retries in an agent’s policy client) waits up to one window per retry. Set the cap above the model server’s startup time if rollouts can start before the engine accepts connections. null adds no time cap, but does not lift the attempt cap that endpoint_connection_retries sets when endpoint_file or route_around_failing_endpoints is used.

Use VLLMModel with multiple replicas of a model endpoint

The vLLM model server supports multiple endpoints for horizontal scaling:

base_url:
- http://gpu-node-1:8000/v1
- http://gpu-node-2:8000/v1
- http://gpu-node-3:8000/v1

How it works:

  1. Initial assignment: New sessions are assigned to endpoints using round-robin (session 1 → endpoint 1, session 2 → endpoint 2, etc.)
  2. Session affinity: Once assigned, a session always uses the same endpoint (tracked via HTTP session cookies)
  3. Why affinity? Multi-turn conversations and agentic workflows that call the model multiple times in one trajectory need to hit the same model endpoint in order to hit the prefix cache, which significantly speeds up the prefill phase of model inference.

Context Length Exceeded Handling

When a conversation exceeds the vLLM model’s maximum context length (max_seq_length), VLLMModel handles the error gracefully instead of crashing the entire rollout collection.

How it works

  1. vLLM rejects the request: vLLM returns an HTTP 400 error with a message like "This model's maximum context length is 32768 tokens. However, you requested 32818 tokens...".
  2. VLLMModel catches the error: Instead of propagating the exception, VLLMModel returns an empty response with finish_reason: "length".
  3. Responses API mapping: The finish_reason: "length" is converted to incomplete_details: { reason: "max_output_tokens" } in the Responses API response returned to the agent.

This is particularly important for multi-turn agentic rollouts where conversation length can grow unpredictably across tool-call turns.

How to detect truncated responses

Downstream consumers (agents, RL training frameworks) can check the incomplete_details field on the response:

response = await model_server.responses(request, body)
if response.incomplete_details and response.incomplete_details["reason"] == "max_output_tokens":
# The conversation exceeded the model's context window.
# The response output will be empty — handle accordingly.
pass

When incomplete_details.reason == "max_output_tokens", the response output is empty because vLLM rejected the request before generation began. This differs from a normal max_output_tokens truncation where the model generates up to the token limit — in this case, the input itself was too long.

Implications for training

When using NeMo Gym with NeMo RL or another training framework, responses with incomplete_details.reason == "max_output_tokens" indicate that the full conversation (prompt + prior generations) exceeded max_seq_length. Training frameworks should filter or handle these responses appropriately since they contain no generated tokens.

Training vs Offline Inference

By default, VLLMModel will not track any token IDs explicitly. However, token IDs are necessary when using NeMo Gym in conjunction with a training framework in order to train a model. For training workflows, use the training-dedicated config which enables token ID tracking:

# Use vllm_model_for_training.yaml
return_token_id_information: true

This enables:

  • prompt_token_ids: Token IDs for the input prompt
  • generation_token_ids: Token IDs for generated text
  • generation_log_probs: Log probabilities for each generated token