Odex talks to any server that speaks the Chat Completions API (/v1/chat/completions), and it is tuned for vLLM. This guide covers:
- which
vllm serveflags each model needs, and why - how to run vLLM on one or more GPUs, locally or remotely
- what Odex's Doctor checks
- how to fix the common problems
Parser names and API behavior were checked against vLLM v0.30.0 (September 2026). The research notes, with sources, are in research/vllm-flags.md. The commands below are the ones shipped in presets/models.toml, so Odex's Doctor suggests the same flags.
Status: these commands come from the vLLM source, the vLLM recipes and the model cards. The Odex test endpoint was unreachable while this guide was written, so they have not yet been run end to end against Odex. Run the smoke suite on your own server to check your setup.
- Requirements
- Quick start
- Per-model commands
- What each flag does
- Context length (
--max-model-len) - Multiple GPUs and multiple models
- API keys and remote endpoints
- Doctor
- Smoke-testing a server with Odex
- Troubleshooting
- vLLM on Linux with an NVIDIA or AMD GPU. vLLM doesn't run natively on Windows. On a Windows machine, use one of these:
- WSL2 with the NVIDIA driver's CUDA-on-WSL support:
pip install vllminside Ubuntu on WSL. WSL forwardslocalhost:8000to Windows. - Docker Desktop with the WSL2 backend and GPU support:
docker run --gpus all -p 8000:8000 vllm/vllm-openai:latest <model> <flags>. - A Linux GPU server on your network (see remote endpoints).
- WSL2 with the NVIDIA driver's CUDA-on-WSL support:
- vLLM version. v0.10.2 or later works for most models. Some parsers need newer releases (listed per model below). The latest release is the safest choice: newer versions fix parser bugs, for example the
qwen3reasoning parser from v0.17. - VRAM for the model weights plus the KV cache for your
--max-model-len(see context length).
# 1. Serve a coding model (Linux or WSL2).
pip install -U vllm
vllm serve Qwen/Qwen3-Coder-30B-A3B-Instruct \
--enable-auto-tool-choice --tool-call-parser qwen3_coder \
--max-model-len 131072 --enable-prefix-caching
# 2. Check it from the machine running Odex.
curl http://localhost:8000/v1/models- Start Odex. Onboarding detects
localhost:8000, lists the served models with their context windows, lets you assign roles and runs Doctor. Without the app:
odex-engine --base-url http://localhost:8000/v1 doctorOr add the endpoint to ~/.odex/config.toml. See config.md for every key.
[model_providers.local]
base_url = "http://localhost:8000/v1"
[roles]
main = "local:Qwen/Qwen3-Coder-30B-A3B-Instruct"Odex picks a preset by matching the served model id against each preset's match globs, for example *qwen3-coder*. If you rename the model with --served-model-name, choose the preset explicitly:
[models.coder]
provider = "local"
model = "my-coder" # the --served-model-name
preset = "qwen3-coder"Each command is the preset's serve line. Sampling parameters (temperature, top_p, top_k, ...) are not server flags: Odex sends the preset's values with every request. Parallelism flags (-tp, -dp, expert parallel) depend on your hardware, so they are left out; see multiple GPUs.
| Preset | Models | Command | Min vLLM | Notes |
|---|---|---|---|---|
qwen3-coder |
Qwen3-Coder-30B-A3B / 480B-A35B Instruct (+FP8) | vllm serve Qwen/Qwen3-Coder-30B-A3B-Instruct --enable-auto-tool-choice --tool-call-parser qwen3_coder --max-model-len 131072 --enable-prefix-caching |
0.10.0 | Non-thinking: no reasoning parser. 256k native; drop to --max-model-len 32768 if memory is tight. qwen3_xml is an alias from v0.11. |
qwen3-coder-next |
Qwen3-Coder-Next | vllm serve Qwen/Qwen3-Coder-Next --enable-auto-tool-choice --tool-call-parser qwen3_coder --max-model-len 131072 --enable-prefix-caching |
0.15.0 | Non-thinking. |
qwen3_5 |
Qwen3.5 / 3.6 / 3.8 | vllm serve Qwen/Qwen3.6-35B-A3B --trust-remote-code --enable-auto-tool-choice --tool-call-parser qwen3_coder --reasoning-parser qwen3 --max-model-len 131072 --enable-prefix-caching |
0.17.0 | Thinks by default. /reasoning none sends enable_thinking=false. Add --language-model-only to skip the vision encoder. Keep at least 128k for long thinking. |
qwen3-thinking |
Qwen3-*-Thinking-2507, Qwen3-Next-Thinking | vllm serve Qwen/Qwen3-30B-A3B-Thinking-2507 --enable-auto-tool-choice --tool-call-parser hermes --reasoning-parser deepseek_r1 --max-model-len 131072 --enable-prefix-caching |
0.10.0 | The chat template pre-fills <think>. deepseek_r1 handles that on every version; qwen3 only from v0.17. |
qwen3-instruct |
Qwen3-*-Instruct-2507, Qwen3-Next-Instruct | vllm serve Qwen/Qwen3-30B-A3B-Instruct-2507 --enable-auto-tool-choice --tool-call-parser hermes --max-model-len 131072 --enable-prefix-caching |
0.10.0 | Non-thinking: no reasoning parser. The 235B model also needs --trust-remote-code. |
qwen3-hybrid |
Qwen3-8B / 14B / 32B / 30B-A3B / 235B-A22B (original) | vllm serve Qwen/Qwen3-32B --enable-auto-tool-choice --tool-call-parser hermes --reasoning-parser qwen3 --max-model-len 32768 --enable-prefix-caching |
0.8.5 | 32k native. For 128k add --hf-overrides '{"max_position_embeddings":131072,"rope_parameters":{"rope_type":"yarn","factor":4.0,"original_max_position_embeddings":32768}}' --max-model-len 131072 (--rope-scaling no longer exists). |
qwen3-vl |
Qwen3-VL / Qwen2.5-VL Instruct | vllm serve Qwen/Qwen3-VL-30B-A3B-Instruct --enable-auto-tool-choice --tool-call-parser hermes --max-model-len 128000 --limit-mm-per-prompt.video 0 --enable-prefix-caching |
0.11.0 | The vision role. Points in normalized 0–1000 coordinates. Thinking variants add --reasoning-parser qwen3 (v0.17+) or deepseek_r1. Parsers were derived from the chat template. |
| Preset | Models | Command | Min vLLM | Notes |
|---|---|---|---|---|
glm-4_5 |
GLM-4.5, 4.5-Air, 4.6 | vllm serve zai-org/GLM-4.5-Air --trust-remote-code --enable-auto-tool-choice --tool-call-parser glm45 --reasoning-parser glm45 --max-model-len 131072 --enable-prefix-caching |
0.10.1 | Hybrid thinking; /reasoning none turns it off. |
glm-4_7 |
GLM-4.7, 4.7-Flash, GLM-5 | vllm serve zai-org/GLM-4.7-FP8 --trust-remote-code --enable-auto-tool-choice --tool-call-parser glm47 --reasoning-parser glm45 --max-model-len 131072 --enable-prefix-caching |
0.14.0 | GLM-5 uses --reasoning-parser glm47 (v0.24+) and the recipe adds --chat-template-content-format=string. |
glm-4v |
GLM-4.5V, 4.1V, 4.6V | vllm serve zai-org/GLM-4.5V --trust-remote-code --enable-auto-tool-choice --tool-call-parser glm45 --reasoning-parser glm45 --max-model-len 65536 --mm-encoder-tp-mode data --enable-prefix-caching |
0.10.2 | Its generation_config.json is almost greedy (top_k: 1). That's harmless with Odex, which always sends explicit sampling. |
| Preset | Models | Command | Min vLLM | Notes |
|---|---|---|---|---|
gpt-oss |
gpt-oss-20b, gpt-oss-120b | vllm serve openai/gpt-oss-20b --enable-auto-tool-choice --tool-call-parser openai --reasoning-parser openai_gptoss --max-model-len 131072 --enable-prefix-caching |
0.10.2 | Harmony format; no chat template needed. Always set both parsers. Effort maps to the reasoning_effort request field. |
| Preset | Models | Command | Min vLLM | Notes |
|---|---|---|---|---|
deepseek-v4 |
DeepSeek-V4-Flash, V4-Pro | vllm serve deepseek-ai/DeepSeek-V4-Flash --trust-remote-code --tokenizer-mode deepseek_v4 --enable-auto-tool-choice --tool-call-parser deepseek_v4 --reasoning-parser deepseek_v4 --kv-cache-dtype fp8 --block-size 256 --max-model-len 131072 --enable-prefix-caching |
0.20.0 | Thinking via chat_template_kwargs.thinking. |
deepseek-v3_2 |
DeepSeek-V3.2 (+Exp) | vllm serve deepseek-ai/DeepSeek-V3.2 --trust-remote-code --tokenizer-mode deepseek_v32 --enable-auto-tool-choice --tool-call-parser deepseek_v32 --reasoning-parser deepseek_v3 --max-model-len 131072 --enable-prefix-caching |
0.13.0 | Thinking is off by default; tool calls work in both modes. |
deepseek-v3_1 |
DeepSeek-V3.1, V3.1-Terminus | vllm serve deepseek-ai/DeepSeek-V3.1-Terminus --trust-remote-code --enable-auto-tool-choice --tool-call-parser deepseek_v31 --reasoning-parser deepseek_v3 --chat-template examples/tool_chat_template_deepseekv31.jinja --max-model-len 131072 --enable-prefix-caching |
0.11.1 | Tool calls only work without thinking, so the default effort is none. See chat templates. |
| Preset | Models | Command | Min vLLM | Notes |
|---|---|---|---|---|
devstral |
Devstral, Devstral 2, Mistral Small/Medium/Large, Magistral, Codestral, Ministral | vllm serve mistralai/Devstral-Small-2507 --tokenizer-mode mistral --config-format mistral --load-format mistral --enable-auto-tool-choice --tool-call-parser mistral --max-model-len 131072 --enable-prefix-caching |
0.10.0 | Devstral 2 detects the Mistral format automatically (mistral_common >= 1.8.6). Reasoning variants (Magistral, Mistral-Small-4) add --reasoning-parser mistral. Mistral uses 9-character tool-call ids; Odex returns them unchanged. |
| Preset | Models | Command | Min vLLM | Notes |
|---|---|---|---|---|
kimi-k2 |
Kimi-K2-Instruct (-0905) | vllm serve moonshotai/Kimi-K2-Instruct-0905 --trust-remote-code --enable-auto-tool-choice --tool-call-parser kimi_k2 --max-model-len 131072 --enable-prefix-caching |
0.12.0 | Non-thinking: don't set --reasoning-parser kimi_k2, which assumes thinking. |
kimi-k2-thinking |
Kimi-K2-Thinking, K2.5, K2.6, K2.7 | vllm serve moonshotai/Kimi-K2-Thinking --trust-remote-code --enable-auto-tool-choice --tool-call-parser kimi_k2 --reasoning-parser kimi_k2 --max-model-len 131072 --enable-prefix-caching |
0.12.0 | Interleaved thinking: Odex sends reasoning back every turn (reasoning_history = "all"). |
minimax-m2 |
MiniMax-M2, M2.1, M2.5, M2.7 | vllm serve MiniMaxAI/MiniMax-M2 --trust-remote-code --enable-auto-tool-choice --tool-call-parser minimax_m2 --reasoning-parser minimax_m2 --max-model-len 131072 --enable-prefix-caching |
0.11.1 | Interleaved thinking. Use minimax_m2, not minimax_m2_append_think: Odex sends back the reasoning field. |
llama4 |
Llama 4 Scout / Maverick | vllm serve meta-llama/Llama-4-Scout-17B-16E-Instruct --enable-auto-tool-choice --tool-call-parser llama4_pythonic --chat-template examples/tool_chat_template_llama4_pythonic.jinja --max-model-len 131072 --enable-prefix-caching |
0.10.0 | Native windows are huge (10M for Scout), so set --max-model-len to what fits in memory. |
llama3 |
Llama 3.1 / 3.2 / 3.3 | vllm serve meta-llama/Llama-3.3-70B-Instruct --enable-auto-tool-choice --tool-call-parser llama3_json --chat-template examples/tool_chat_template_llama3.1_json.jinja --max-model-len 131072 --enable-prefix-caching |
0.10.0 | No parallel tool calls. Odex uses the minimal tool profile. |
seed-oss |
Seed-OSS-36B-Instruct | vllm serve ByteDance-Seed/Seed-OSS-36B-Instruct --trust-remote-code --enable-auto-tool-choice --tool-call-parser seed_oss --reasoning-parser seed_oss --max-model-len 65536 --enable-prefix-caching |
0.11.0 | Thinking budget via chat_template_kwargs.thinking_budget (Odex maps effort levels to 0 / 512 / 1024 / 4096 / unlimited). |
mellum2 |
Mellum2-12B-A2.5B-Instruct | vllm serve JetBrains/Mellum2-12B-A2.5B-Instruct --trust-remote-code --enable-auto-tool-choice --tool-call-parser hermes --max-model-len 131072 --enable-prefix-caching |
0.23.0 | The -Thinking variant adds --reasoning-parser qwen3. |
generic |
anything else | vllm serve <model> --enable-auto-tool-choice --tool-call-parser hermes --enable-prefix-caching |
— | Fallback. Run Doctor to find out what the model supports, and check the model card for the right parser. |
| Flag | Why Odex needs it |
|---|---|
--enable-auto-tool-choice |
Odex sends tool_choice: "auto" on every agent request. Without this flag, vLLM rejects those requests with HTTP 400: "auto" tool choice requires --enable-auto-tool-choice and --tool-call-parser to be set. |
--tool-call-parser <name> |
Turns the model's tool-call markup into structured tool_calls. It must match the model family. With the wrong parser (or none), tool calls arrive as plain text in content. Odex's fallback parser recovers them, but streaming and parallel calls suffer, and Doctor warns. vLLM refuses to start if --enable-auto-tool-choice is set without a parser. |
--reasoning-parser <name> |
Moves the model's thinking into the reasoning field so it doesn't pollute the answer or the history. Only for thinking models. On a non-thinking model (Qwen3-Coder, Qwen3-Instruct-2507, Kimi-K2-Instruct), recent parsers start in "reasoning" state, so the whole answer lands in reasoning and content comes back empty. |
--max-model-len <n> |
The context window. vLLM reserves KV cache for it, and Odex reads it from /v1/models to budget every request. See below. |
--enable-prefix-caching |
Reuses the KV cache for a shared prompt prefix. Odex keeps the prefix byte-stable (fixed system prompt, deterministic tool order, AGENTS.md near the top, volatile data late) and prunes in large batches, so each step of a long turn only prefills the new tokens. That's the biggest latency win for agents. Recent vLLM versions enable it by default; the presets pass it explicitly anyway. Doctor measures the speed-up. |
--trust-remote-code |
Needed by models that ship custom code or tokenizers (GLM, DeepSeek, Kimi, MiniMax, Qwen3.5+). |
--tokenizer-mode |
mistral for Mistral-format checkpoints, deepseek_v32 and deepseek_v4 for the new DeepSeek chat formats. Without it, the chat format and tool calls are wrong. |
--chat-template <file> |
Some models need vLLM's tool-aware template (Llama 3.x and 4, DeepSeek-V3.1). See chat templates. |
--served-model-name <id> |
Optional. Odex matches presets by served id, so either keep the HF id in the name or set preset = "..." in [models.<key>]. |
--api-key <key> |
Optional. See API keys. |
--kv-cache-dtype fp8 |
Halves the KV cache, so a longer --max-model-len fits in the same memory. Quality cost is small on recent GPUs. |
--gpu-memory-utilization 0.9 |
Share of VRAM vLLM may use (default 0.9). Lower it when other processes share the GPU. |
--max-num-seqs <n> |
Concurrent sequences. Several Odex threads and subagents run in parallel; set Odex's max_concurrent_requests to the same value or lower. |
Flags Odex does not need:
- Sampling flags (
--generation-config,--override-generation-config): Odex sends sampling with every request. vLLM never readspresence_penaltyfrom the model config anyway. --default-chat-template-kwargs: Odex sendschat_template_kwargsper request (for exampleenable_thinking, controlled by/reasoning). A server-wide default still works as a fallback.--enable-reasoning: removed in v0.10. Some old model cards still show it.
--chat-template examples/<file>.jinja paths are relative to the vLLM repository. The official Docker image ships them in /vllm-workspace/examples/. A pip install vllm may not include them, so download the file from the vLLM GitHub repository at the tag that matches your version:
VLLM_TAG=v$(python -c "import vllm; print(vllm.__version__)")
curl -LO https://raw.githubusercontent.com/vllm-project/vllm/$VLLM_TAG/examples/tool_chat_template_llama3.1_json.jinja
vllm serve meta-llama/Llama-3.3-70B-Instruct ... --chat-template ./tool_chat_template_llama3.1_json.jinja- Bigger is better, up to what fits. Odex works with any window: the context engine prunes and compacts automatically, and the soak test runs 200 steps on a 4,096-token window. But each compaction costs a summarization call and loses detail, so give agents room. Aim for 64k–128k for coding agents, and at least 32k.
- Thinking models need more. Reasoning tokens count toward the window while a turn runs. Keep at least 64k, and 128k for Qwen3.5/3.6 and the Thinking-2507 models.
- Memory. The KV cache grows linearly with the window and with concurrency. If vLLM says the KV cache can't fit
max_model_len, reduce the window, use--kv-cache-dtype fp8, quantize (FP8/AWQ), or add GPUs. - Odex reads it automatically from
/v1/models. To make Odex budget for less than the server allows (to leave room for other users, or to test compaction), setcontext_windowin[models.<key>]. Never set it higher than the server's limit. - Small windows (≤ 8,192 tokens) switch Odex to a compact system prompt and a 7-tool set, and cap AGENTS.md at about 12% of the window.
-
One model across GPUs in one machine:
--tensor-parallel-size N(-tp N), where N divides the attention head count (2, 4, 8). For large MoE models (Qwen3-Coder-480B, DeepSeek, Kimi-K2, GLM-4.x), add--enable-expert-parallel. The vLLM recipes list tested combinations per model. -
More throughput for many parallel agents:
--data-parallel-size Nruns N replicas behind one endpoint. Combine it with-tpas memory requires. -
Across machines:
--pipeline-parallel-sizewith a Ray cluster. See the vLLM distributed-serving docs. -
Vision models:
--mm-encoder-tp-mode dataruns the vision encoder data-parallel, which is faster for screenshot-heavy computer use. -
Several models on one box: run one
vllm serveper model on different ports and GPUs. Each one only claims--gpu-memory-utilizationof the GPUs it can see.CUDA_VISIBLE_DEVICES=0,1 vllm serve Qwen/Qwen3-Coder-30B-A3B-Instruct --port 8000 -tp 2 ... CUDA_VISIBLE_DEVICES=2 vllm serve Qwen/Qwen3-VL-30B-A3B-Instruct --port 8001 ...
Then add one provider per port and assign roles:
[model_providers.coder] base_url = "http://gpu-box:8000/v1" [model_providers.vision] base_url = "http://gpu-box:8001/v1" [roles] main = "coder:Qwen/Qwen3-Coder-30B-A3B-Instruct" vision = "vision:Qwen/Qwen3-VL-30B-A3B-Instruct"
-
Small helper models. The
compactorandutilityroles (summaries, titles, commit messages) can use a smaller, faster model. That frees the main model for the agent.
-
Server side:
vllm serve ... --api-key <key>, or setVLLM_API_KEY. vLLM then requiresAuthorization: Bearer <key>on/v1/*routes. A wrong key gets HTTP 401 with the body{"error":"Unauthorized"}./health,/version,/tokenizeand/metricsstay unauthenticated, so don't expose the port to untrusted networks. -
Odex side: enter the key in Settings → Models & Endpoints. It is encrypted with the OS keychain and only held in the engine's memory. For the CLI, use
api_key_env:[model_providers.gpu1] base_url = "http://10.0.0.5:8000/v1" api_key_env = "GPU1_VLLM_KEY"
-
Remote over SSH: the simplest secure option is a tunnel,
ssh -N -L 8000:localhost:8000 gpu-box, with Odex pointed athttp://localhost:8000/v1. -
Behind a reverse proxy (TLS, auth):
- Turn off response buffering for
/v1/chat/completions, or streaming arrives in bursts and the idle timeout fires. For nginx:proxy_buffering off; proxy_read_timeout 600s;. - Allow long requests. Odex's
stream_idle_timeout_msdefaults to 300 s. - Keep the
/v1prefix inbase_url, for examplehttps://llm.example.com/vllm/v1. Odex finds/healthand/tokenizeby stripping the trailing/v1. - Gateways that need extra headers or query parameters: use
headersandquery_paramson the provider.
- Turn off response buffering for
-
Concurrency: each provider has a
max_concurrent_requestsqueue (default 8). Raise it for a big server with--max-num-seqsto match. vLLM's continuous batching makes parallel threads and subagents cheap.
Doctor probes one model on one endpoint and shows a pass / warn / fail table, plus the vllm serve flags that would fix what failed (with a copy button in the app). Results are cached in ~/.odex/models_cache.json and feed the model's detected capabilities.
Run it from Settings → Models & Endpoints → Doctor, at the end of onboarding, or from the CLI:
odex-engine doctor # first model of every configured endpoint
odex-engine doctor --model coder # a [models] key, provider:model, or a served id
odex-engine doctor --quick # skip the prefix-cache timing
odex-engine doctor --json # machine-readable report
odex-engine --base-url http://gpu-box:8000/v1 doctor # no config needed| Check | What it does | If it fails or warns |
|---|---|---|
connect: Endpoint reachable |
GET /v1/models and /version; checks that the model is served and reads max_model_len. |
Fail: wrong base_url (did you include /v1?), server down, firewall, wrong API key (401), or the model id isn't served. The detail lists the served ids. Doctor stops here. |
health: /health |
GET /health at the server root. |
Fail: vLLM reports its engine as dead; restart it. Warn: no /health (not vLLM, or a proxy only forwards /v1). Harmless. |
streaming: Streaming + usage |
Streams a short reply with stream_options.include_usage. |
Fail: streaming is broken (often a buffering proxy); Doctor stops here. Warn: no incremental deltas (buffering proxy) or no usage chunk. Without usage, Odex can't calibrate token counts as well. |
toolCall: Native tool call |
Asks for a get_weather call with tools offered. |
Fail: no tool call at all, or a 400 like "auto" tool choice requires --enable-auto-tool-choice. Add --enable-auto-tool-choice --tool-call-parser <parser>. Warn: the call came back as text and the client fallback parser recovered it: the parser is missing or wrong for this model. |
streamedToolCall: Streamed tool call |
The same, streamed: expects tool_calls deltas. |
Fail: no calls. Warn: calls only appeared after fallback parsing. Same fix as above. |
parallelToolCalls: Parallel tool calls |
Asks for two independent calls in one response. | Warn only: the model calls one tool at a time. Odex still works, just with more round trips. Llama 3.x can't do parallel calls. |
reasoning: Reasoning parser |
Reasoning models only: asks for a short chain of thought. | Pass: thinking arrived in a dedicated field. Warn: thinking was mixed into content (</think> seen). Add --reasoning-parser <parser>, or upgrade vLLM (see reasoning leaks). Odex strips it on the client meanwhile. Warn: no reasoning returned; thinking may be disabled. Skip: not a reasoning model. |
vision: Vision (image input) |
Sends a 16×16 red PNG and asks for its color. | Skip: the server rejected the image (text-only model). Assign a vision role model for computer use. Warn: the image was accepted but the answer was wrong. |
tokenize: /tokenize |
Counts tokens for a chat via POST /tokenize. |
Warn only: Odex estimates token counts instead. Usually means a proxy doesn't forward the server root. |
prefixCache: Prefix caching |
Sends the same ~5k-token prompt twice and compares time to first token (or cached-token usage). | Warn: less than a 1.3× speed-up and no cached tokens: add --enable-prefix-caching. Skipped with --quick. |
structuredOutput: Structured output |
Requests a JSON object via response_format: {type: "json_schema"}. |
Fail: the server rejected response_format. Warn: the output didn't match the schema. Odex then repairs JSON on the client and falls back to an extractive summary if compaction fails. Make sure you aren't relying on guided_json (see structured outputs). |
The suggested command combines the model's preset serve line with the flags from the failing checks.
Headless. --base-url only applies when ~/.odex/config.toml defines no [model_providers]; pass --home <empty dir> to ignore your config and keep the test thread out of your history.
mkdir /tmp/odex-smoke && cd /tmp/odex-smoke
odex-engine --base-url http://gpu-box:8000/v1 exec \
"Create hello.txt containing 'hello from odex', then print it with a shell command." \
--auto-approve
cat hello.txtUseful exec flags: -m <model>, -C <dir>, --permission-mode read-only|auto|full-access, --effort <level>, --json (events as JSON lines), --plan, --resume <thread-id>, --output-last-message <file> and --timeout <secs>. Without --auto-approve, approval requests are denied.
The opt-in real-vLLM test suite (engine/core/tests/real_vllm.rs) automates this. It runs discovery plus Doctor, a file-writing task, fixing a failing Node test, an MCP round-trip, and compaction on a 4,096-token window:
cd engine
cargo build -p odex-mcp-client --bins # optional: enables the MCP round-trip
ODEX_E2E_BASE_URL=http://gpu-box:8000/v1 \
ODEX_E2E_MODEL=Qwen/Qwen3-Coder-30B-A3B-Instruct \
cargo test -p odex-core --test real_vllm -- --nocapture --test-threads=1ODEX_E2E_API_KEY is passed to the server if set. ODEX_E2E_TIMEOUT_SECS changes the per-turn timeout (default 600). Without ODEX_E2E_BASE_URL, every test prints "skipped" and passes.
vLLM answers HTTP 400 when prompt plus max_tokens exceeds --max-model-len. Depending on the version, the message is one of:
This model's maximum context length is N tokens. However, you requested M output tokens and your prompt contains T input tokens…(v0.18+)This model's maximum context length is N tokens. However, your request has T input tokens…or'max_tokens' or 'max_completion_tokens' is too large…(up to v0.17)max_tokens=M cannot be greater than max_model_len=…(v0.18+)
Odex parses all of them and recovers on its own: it prunes, compacts, trims and retries (Tier 3), so a thread shows a compaction notice instead of an error. If you still see overflow errors:
context_windowset higher than the server's--max-model-len. Remove the override, or lower it.- A proxy or gateway rewrites the error body. Odex needs the text above to detect an overflow. Pass the vLLM error through unchanged.
max_output_tokenslarger than the window (themax_tokens … cannot be greater than max_model_lenmessage). Lower it in[models.<key>].- The compactor role points at a model with a smaller window than the history it summarizes. Odex runs map-reduce in chunks, but a very small compactor makes compaction slow. Use a model with at least 16k.
The symptoms: <tool_call>{...}</tool_call>, [TOOL_CALLS], <|tool▁calls▁begin|> or similar markup in the answer, or Doctor's toolCall check warns "recovered by the client fallback parser".
- The server has no tool parser, or the wrong one. Restart vLLM with
--enable-auto-tool-choice --tool-call-parser <parser>from the table above. - Meanwhile, Odex's fallback parser recognizes the Hermes, Qwen3-Coder XML, GLM, Mistral, DeepSeek, Kimi, Llama-3 JSON and pythonic formats automatically. It pauses streaming at the marker so raw markup doesn't flash in the thread, and it ignores calls to tool names that weren't offered.
- If you get HTTP 400
"auto" tool choice requires --enable-auto-tool-choice and --tool-call-parser to be set, the flags are missing entirely. Odex can't work around that, because the request is rejected. - If the model ignores tools altogether, check that the chat template supports tools (Llama 3.x and DeepSeek-V3.1 need
--chat-template) and that the preset didn't setcapabilities.tools = false.
The symptoms: <think>…</think> in messages, or the model's deliberation appears as the answer.
- No reasoning parser. Add
--reasoning-parser <parser>for thinking models (Qwen3 hybrid and Thinking, Qwen3.5+, GLM, gpt-oss, DeepSeek with thinking, Kimi-K2-Thinking, MiniMax-M2, Seed-OSS). - The
qwen3parser on vLLM before v0.17 only splits reasoning when both<think>and</think>are generated. Thinking-2507 and Qwen3.5+ templates pre-fill<think>, so only</think>appears. Upgrade to v0.17+ or use--reasoning-parser deepseek_r1. - Odex strips
<think>blocks on the client for every model, and holds back early content on reasoning models to catch a pre-filled<think>. So the thread stays clean, but history and token counts are better with a server-side parser. - The opposite problem, an empty answer with everything in "reasoning", means a reasoning parser is set on a non-thinking model (Qwen3-Coder, Qwen3-Instruct-2507, Kimi-K2-Instruct). Remove
--reasoning-parser. - Field names changed across versions:
reasoning_contentup to v0.11.0, both names from v0.11.1 to v0.15, and onlyreasoningfrom v0.16. Odex reads both.
Odex uses structured output for compaction summaries, commit messages, /review findings and automatic-review verdicts.
- Odex sends
response_format: {"type": "json_schema", ...}, which works on every vLLM version checked (v0.10 to v0.30). - vLLM's old
guided_json(and the otherguided_*fields) was deprecated in v0.11 and removed in v0.12. Since then it is silently ignored: HTTP 200 with unconstrained output and only a server-side warning. Don't setstructured_output = "guided_json"unless the server is older than v0.11. Newer servers acceptstructured_outputs: {...}in the request body (v0.11+), but Odex doesn't need it. - If summaries keep falling back to the extractive mode (the compaction notice says so), run Doctor and look at
structuredOutput. Fixes: upgrade vLLM; choose a backend with--structured-outputs-config.backend xgrammarorguidanceif the default fails on a schema; or point thecompactorrole at a model that follows JSON schemas well. - With a reasoning parser set, the constraint applies to the content after reasoning. Qwen3-Coder doesn't need a reasoning parser at all.
- 401
{"error":"Unauthorized"}: the API key is missing or wrong. Check Settings → Models & Endpoints, or the variable named inapi_key_env. - "model … does not exist" (404): the model key doesn't match the served id.
curl <base_url>/modelsshows the exact ids. Mind--served-model-name. - The answer stops mid-sentence (
finish_reason: length):max_output_tokensis too low, or long thinking used up the budget. Raise it, or lower the effort with/reasoning. - Repetition loops: Odex breaks repeated identical tool calls and degenerate text with a nudge. Persistent loops usually mean the sampling is wrong. Keep the preset's values, which include
presence_penaltywhere the vendor recommends it. Avoidtemperature = 0for Qwen3 and GLM. - Very slow first token on every step: prefix caching is off or ineffective. Check Doctor's
prefixCache. Also make sure no proxy or middleware changes the prompt between requests. ValueError: … KV cache … max_model_lenat startup: the window doesn't fit in VRAM. Lower--max-model-len, use--kv-cache-dtype fp8, raise--gpu-memory-utilization, or add GPUs (-tp).- Chat template file not found: see chat templates.
- Mistral tool-call ids: with the HF tokenizer, Mistral templates need 9-character ids. Odex keeps the ids vLLM returns; don't put a proxy in between that rewrites them.