Why Your vLLM Tool Calls Break at Random
Open models don't emit tool calls; they emit text that gets regexed into JSON. Constrained decoding makes bad arguments impossible.
Your agent runs flawlessly against a hosted API. Then you point it at a self-hosted model on vLLM to cut costs, and every tenth tool call comes back mangled: half a JSON object, a stray comma, or the model narrating around the call instead of making it.
Ubicloud, which runs managed vLLM, just published a post on fixing tool-call accuracy. The lesson generalizes to anyone serving open models: the model usually isn't the problem. The tool-call plumbing around it is doing something fundamentally fragile.
Why this matters
Self-hosting inference is now routine for cost and data-control reasons. But agents are chains, and a chain is only as reliable as its weakest link. One unparseable tool call can kill an entire run, trigger a retry storm, or silently drop a row from a batch job. This plumbing — not benchmark scores — decides whether an open model is deployable for agentic work.
How it works
Hosted APIs emit tool calls as structured output. The JSON arrives pre-parsed; you never see raw text. Open models have no such channel. They emit plain tokens, and vLLM attaches a tool-call parser — hermes, mistral, llama3_json — that scans the token stream and carves out a function name plus arguments.
That's parsing, and parsing is brittle. It only works if the raw output matches the expected template exactly. Single quotes instead of double, an unescaped newline, a friendly "Sure, calling that now" — and extraction fails.
Constrained decoding flips the approach. Compile each tool's JSON schema into a grammar using vLLM's structured outputs (xgrammar or outlines backend), and the sampler masks every token that would violate the schema at each step. The parser still wraps results into OpenAI-style tool_calls, but malformed arguments stop being unlikely — they become impossible.
Where this helps
- Agent loops on self-hosted models: tool arguments become deterministic instead of best-effort.
- Tools whose arguments become SQL or HTTP requests. A malformed argument there is a production incident, not a chat typo.
- Batch pipelines that log and skip failures. Schema enforcement drops silent data loss to zero.
- Multi-tool routing: pick the right parser per model once, then enforce a schema per tool forever.
Watch out
Grammar compilation costs real time per unique schema. Cache schemas and reuse them — don't regenerate per request. Token masking also adds slight latency per step, usually negligible next to generation.
Overly strict schemas can hurt quality: the model must commit to field values it might otherwise reason toward. Keep schemas permissive where correctness allows.
The parser must still match the model's chat template. A Hermes parser on a Mistral template produces garbage no grammar will fix. And if your client streams tool-call deltas, buffer partial JSON before validating.
Try it yourself
Serve a model with a matching parser, enable structured outputs, and force tool arguments through a schema:
# vllm serve Qwen/Qwen2.5-7B-Instruct \
# --enable-auto-tool-choice --tool-call-parser hermes \
# --enable-structured-output
from openai import OpenAI
from pydantic import BaseModel
from typing import Literal
class WeatherArgs(BaseModel):
city: str
unit: Literal["celsius", "fahrenheit"]
client = OpenAI(base_url="http://localhost:8000/v1", api_key="none")
resp = client.chat.completions.create(
model="Qwen/Qwen2.5-7B-Instruct",
messages=[{"role": "user", "content": "Weather in Tokyo?"}],
extra_body={"guided_json": WeatherArgs.model_json_schema()},
)
print(resp.choices[0].message.content) # schema-valid JSON, guaranteedTL;DR
- What happened: Ubicloud published its fixes for tool-call accuracy in vLLM — the fragile part is text parsing, not model intelligence.
- Why it matters: schema-constrained decoding makes tool arguments deterministic, closing the biggest reliability gap between self-hosted and hosted APIs.
- Try today: enable structured outputs and pass your tool's JSON schema so malformed calls can't happen.