56 comments

[ 5.3 ms ] story [ 90.0 ms ] thread
I'm one of the authors of this blog post. Figuring out how to integrate the LLM, the inference and serving engine, and the constrained decoding implementation such that you get the intended output and overall quality definitely requires careful engineering and testing. Please feel free to AMA. :)
Do you know where <||> syntax is coming from? I find it very odd that existing markup wasn't used and there is very little info online about it.
The first place I think I encountered it, was GPT-2's tokenizer, back when OpenAI did actual open source stuff, maybe around 2020?

I'm guessing as with most stuff in the LLM ecosystem, someone did it, no one re-evaluated why and just copied it wholesale, and here we are. The most important point, that this sequence of text never occurs in "real text", is the main point of the specific character chosen, and seems to still work good for that purpose.

The original usage is only "<|endoftext|>" I think, which was the EOS token for GPT-2, then more and more special tokens were added as various delimiters to new model releases.

The syntax was specifically chosen to avoid confusion with other tag formats. XML could show up in any user or LLM-generated text and would mess up the message format. Some variations even used unicode symbols like "|" or "〈" to decrease the chance of confusion.

Also, back then, some people believed that it could prevent prompt injection, but history has shown that it was a futile attempt.

So if I understand it correctly, the tool calls are not actually done by the model?

In the sense that the model manifests them, but they're not actually a result of the model doing anything but the inference engine filtering down the anything only to valid moves?

Could something like that also be used in other contexts?

I heard about how people dynamically steer models by massaging the weights a bit, but massaging the sampler(?) also sounds like that might give a lot of control over outputs with the weights staying technically the same?

Exactly. The LLM is your next-token predictor, which output text and tool-calls. These calls get picked up by your client/harness (e.g., Claude Desktop) which executes them and sends output back to the LLM.
Yeah I know what a tool call is.
Correct, here's an implementation example using the Claude SDK [0] but it's the same approach on other platforms: you tell the model what tools it has available, it sends back a response saying it wants to use a specific tool with given parameters, your code executes that tool then sends the output back to the model on the next turn.

[0] https://platform.claude.com/docs/en/agents-and-tools/tool-us...

I think I might've phrased something in ways that were confusing to people, because now two people have explained tool calls to me for some reason?
Just trying to be helpful. I couldn’t see any other way to interpret the question below except as someone who doesn’t know how tool calls get done and who might therefore value a little insight. Didn’t mean to be patronising, apologies if I misinterpreted your question.

> So if I understand it correctly, the tool calls are not actually done by the model?

> but massaging the sampler(?) also sounds like that might give a lot of control over outputs with the weights staying technically the same?

Yes, this is what constrained decoding means. You constrain the model to only output from a set of "allowed" tokens (a.k.a. grammars).

So, if you use tool use with constrained decoding, you can be certain that the model will always choose from a list of approved tools (i.e. if tools=['read','write'], it cannot output 'execute', even if in other sessions it could, if execute was in the approved tools).

The tricky problem is that certain models work / have been trained in certain ways. And constraining them can lead to "invisible" problems. Constrained decoding only guarantees perfect syntax, not perfect semantics (i.e. it could write nonsense in the correct file).

> Could something like that also be used in other contexts?

Sure. You can build pretty cool grammars and constrain the model to do cool stuff, akin to "tool use" but more free hand. You could, for example, make it always emit a 10 point plan. For any prompt, it will give you a fixed syntax 10 point plan, always. And the plan will be constant with a plan_title and 10{plan_item, item_text}. But, as above, there's no guarantee that the individual items in the plan would make any sense... (i.e. step7 in a pasta making recipe would be "add glue").

Would this technique work with inference optimization techniques like DFlash and MTP? In other words does it slow down things to the point where its just better to retry?
Yes, constrained decoding and speculative decoding work together, although they need to be carefully integrated and incorrect integrations give result in bugs (see point 3 under Section "Differences in Constrained-Decoding Behavior" in the blog post). Performance overhead does not generally seem to be a large problem. Of the speculative tokens, those that are not allowed by the grammar get discarded.
Can the same mechanism be used to make sure it outputs syntactically correct code?
Is this even a real issue? Are you consistently getting code out of frontier models + harnesses(Codex, Claude Code, etc) that have syntax errors?
If the model is predicting syntactically incorrect tokens in code you probably have bigger issues
Yes. All you need is a formal grammar that defines the syntax of the text being generated. The llama.cpp repo has a decent writeup on the way that grammar-constrained decoding works in general [0], and it includes a bit of a simplistic example for generating syntactically-correct C code [1]. No reason this couldn't be extended to other things -- I personally have run experiments with generating syntactically-correct SQL statements when doing text-to-sql [2], and I think it's pretty cool.

[0] - https://github.com/ggml-org/llama.cpp/tree/master/grammars [1] - https://github.com/ggml-org/llama.cpp/blob/master/grammars/c... [2] - https://gist.github.com/HanClinto/b853571136fd42f8e2bde4c99c...

Amusingly constrained decoding is the same approach we use to turn an AR model into a /v1/systemone endpoint (I worked on the vLLM feature).

Tool calling is fraught with peril, especially because there's nothing that necessarily says that a models tools are fixed in place for the life of its run. In some cases, tools are added mid-run to avoid a giant prefix miss and (at least last time I checked!), the industry has not settled on OpenAI-style chat message formats for tool call updates.

I would absolutely prefer to run in a fully-constrained tool call environment, and would love if we hadn't YOLO'd the whole thing! The PTC-style tools sort of fix this by punting tool calling to JavaScript, but that's even harder to enforce, and models not trained on the style might take a turn of failed tool calls to get it right.

What would a "fully-constrained tool call environment" be?
>some cases, tools are added mid-run to avoid a giant prefix miss

Wait, can you actually do that? The model should not exactly care where in the context the tools are, but I haven't realised it's a thing that works

Context is just one long string. You can do whatever you want with it. Training helps to conform to specific syntactic idiosyncrasies, but LLMs are large enough these days that they can follow pretty much any instructions most of the time.

For example, if you want to use an LLM API that does not natively support tool calls, you can tell the LLM to emit the tool calls for your preferred tools in a markdown code block and parse them yourself.

> avoid a giant prefix miss

I was wondering how feasible it is to modify the context, instead of just appending - e.g. to remove malformed tool calls and their error messages that would pollute the context otherwise.

Sure, you'd have to rebuilt the KV cache and incur a performance loss, but that might be acceptable if it happens rarely enough.

This is the approach I took. Before leaving scope, results are filtered to remove tool call errors and bloat and instead append those errors to a different system. A much smaller tool call error result is returned to the agent while keeping the reasoning for failure intact.
Does this work with Qwen 3.8?
I believe there is constrained decoding available for Qwen 3.8 in vLLM. But mileage may vary: although you guarantee syntactically-correct tool calls, you'll want to observe the overall quality of the model with and without constrained decoding. In the case of Kimi, there is a clear recommendation from the creators to turn constrained decoding on. I don't think this is the case for Qwen 3.8.
I wish they'd published something about the actual impact on model quality. Schema-constrained decoding changes the distribution the LLM will output. I'd be curious how a back-up-and-retry method for tool calls (which also doesn't pollute the context and also has 100% tool-call accuracy) would compare. In their example stats (23/25 success rate), it'd be +9% tokens used for the tool call attempt itself, but that's +9% on just a small fraction of the total tokens, so if a retry solution gave any better accuracy I think it might win out.
(comment deleted)
We are also interested in this and may focus on it in a future post. They are two different perspectives: (1) What is the overall quality of LLM+serving stack+harness, and (2) How close does the behavior of our setup match the first-party provider's? In this post, we focused on the latter. Moonshot AI, the creators of Kimi, recommend constrained decoding to be on by default, and also serve Kimi that way. This implies that it helps overall performance, though it does not provide numbers for it. We want to serve Kimi as recommended, and ran into the challenges described in the post.

There is work that suggests that constrained decoding can negatively affect model quality, but this depends on many factors, among which the model in question [1, 2, 3]. Overall, it seems to me that there is a trend towards using constrained decoding. OpenAI's newer Responses API has constrained decoding turned on by default as well, similar to Moonshot AI's Kimi.

1: https://arxiv.org/abs/2408.02442

2: https://blog.dottxt.ai/say-what-you-mean.html

3: https://dylancastillo.co/posts/say-what-you-mean-sometimes.h...

I really appreciate this. I hope LLM inference providers and Chinese LLM labs start using this latest vLLM version ASAP.

I do things that require ~300 tool calls per prompt and I have been stuck with OpenAI and Anthropic since 2024.

If people still wonder what moat OpenAI and Anthropic have: They got 100% tool-calling accuracy (using constrained decoding) and prompt caching right all the way back in 2024.

> I do things that require ~300 tool calls per prompt

I have multiples of that with some regularity, what is the hangup?

llama.cpp has done this since forever. I've used other engines including vLLM here and there, and I was quite surprised that many of them don't have this feature. Glad vLLM has it now.
Constrained decoding has been in vLLM for a long time, and constrained tool call decoding has been in vLLM for a while, too.

The blog post is rather confusing because until you understand the entire situation it appears to imply that they fixed vLLM (what the title implies) but really they just needed to configure it correctly.

To be fair, the course of constrained tool calling configuration in vLLM changed over time in ways that changed behavior, which is the real problem. The Ubicloud team deserves a lot of credit for having benchmarks in place to track performance and catch regressions for investigations. Some of the providers you find on OpenRouter have inexplicably bad performance relative to others and clearly do not care this much.

However, if this article left you confused because constrained decoding and constrained tool call schema have been features in vLLM for a long time, jump to the end where they show what configuration changes they made.

Overall, a fair assessment. I do want to place a footnote here that we did fix vLLM in the sense that its correct behavior for Kimi required code changes: it was impossible to fix it in the configuration. Later, the vLLM team fixed this problem upstream. Now, with the latest version of vLLM, the problem we describe can be addressed by setting the `--tool-strict-level` flag to `parameter`.
If you work for ubicloud, I have a question: do you sell api access to kimi/etc, and if so, where is that listed??
We currently focus on serving DeepSeek models on dedicated clusters for large customers. We do not offer this as a publicly available service at the moment, but we may consider doing so in the future.
> The blog post is rather confusing because until you understand the entire situation it appears to imply that they fixed vLLM (what the title implies) but really they just needed to configure it correctly.

Just as a point of feedback for the authors, I enjoyed the article, and did not suffer the confusion pointed out above.

Should have just wrote "Fixing Tool-Call Accuracy with vLLM" instead of "Fixing Tool-Call Accuracy in vLLM"
> For example, when we served Kimi K2.6 with vLLM version 0.28, we got the expected tool call schema only 92% of the time. In other words, 2 out of every 25 attempted tool calls failed.

100% success is critical for tool calling.

The probability that the overall agent iteration is successful would scale very poorly even if we achieved 95%. With p95, if you need 50 tool calls that must execute sequentially to achieve a result, the chances that you won't have an error are only ~8%. There are ways to recover from bad tool calls (error feedback helps a lot), but if the agent is persistently using the wrong tools with the wrong args the overall system will not work very well.

That is bending the barrel to align the scope. What’s interesting is that the authors mention the cause of the failed calls, “…a model may generate a tool or field name that it has seen during the training, even if it does not exist in the current production environment. For example, it may generate search_web when the available tool is web_search, or use city_name when the active schema expects location”, but then failed to see that it’s also the solution. If you call it “web_search” but it calls it as “search_web” then just rename your function to “search_web” and let the problem resolve itself.

Things get more interesting if you don’t give it a call syntax and just see what it does based on the name alone.

Okay, this explains the feeling that I had that vLLMs tool parsing is much more precise than for example llama cpp or Ollama, I still use ollama due to mlx which of my understanding is not offered on vLLM unfortunately.