Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 34 additions & 8 deletions src/vllm_router/routers/routing_logic.py
Original file line number Diff line number Diff line change
Expand Up @@ -398,10 +398,23 @@ async def route_request(
# Remote /tokenize fallback (let errors bubble up to keep behavior simple)
remote_url = endpoints[0].url + "/tokenize"
headers = {"Content-Type": "application/json"}
data = {
"model": endpoints[0].model_names[0],
"prompt": request_json.get("prompt", ""),
}
# Chat-completions bodies carry `messages`, not `prompt` - send
# them as vLLM's TokenizeChatRequest so the engine applies its
# own chat template (`add_generation_prompt=True` is the
# chat-completions default, pinned explicitly). Sending the old
# prompt-form payload for a chat body tokenizes "" and the KV
# lookup keys on garbage.
if "messages" in request_json:
data = {
"model": endpoints[0].model_names[0],
"messages": request_json["messages"],
"add_generation_prompt": True,
}
else:
data = {
"model": endpoints[0].model_names[0],
"prompt": request_json.get("prompt", ""),
}
Comment on lines +407 to +417

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Correctness Issue: Local Tokenizer Bypasses Chat Template / Messages

If the local tokenizer successfully loads (e.g., if the model is cached locally or the model name matches), the try block at line 391 will succeed because request_json.get("prompt", "") will return "" (since prompt is not in a chat-completions request).

This means self.tokenizer.encode("") will execute successfully without raising an exception, and the router will completely bypass this except Exception: block. As a result, it will perform a KV lookup on the empty prompt "" instead of the actual chat messages.

To fix this, we should explicitly prevent the local tokenizer from encoding an empty prompt when messages is present in the request. Since local chat template tokenization is not yet supported in this interim patch, we can raise an exception in the try block if messages is in request_json to force the remote /tokenize fallback.

For example, update the try block above (around line 391) to:

try:
    if "messages" in request_json:
        raise NotImplementedError("Local chat template tokenization not supported yet")
    if self.tokenizer is None:
        self.tokenizer = AutoTokenizer.from_pretrained(
            endpoints[0].model_names[0]
        )
    token_ids = self.tokenizer.encode(request_json.get("prompt", ""))

body = requests.post(
remote_url, headers=headers, json=data, timeout=10
).json()
Comment on lines 418 to 420

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Performance Issue: Blocking HTTP Call on the Event Loop

Calling requests.post synchronously inside an async def function blocks the entire FastAPI event loop, which can severely degrade performance and increase latency for all concurrent requests under load.

We should run this blocking HTTP call in an executor, just like it is done in LoadAwareRouter.tokenize_prompt.

Suggested change
body = requests.post(
remote_url, headers=headers, json=data, timeout=10
).json()
loop = asyncio.get_running_loop()
response = await loop.run_in_executor(
None,
lambda: requests.post(
remote_url, headers=headers, json=data, timeout=10
)
)
body = response.json()

Expand Down Expand Up @@ -661,10 +674,23 @@ async def tokenize_prompt(
except Exception:
remote_url = endpoints[0].url + "/tokenize"
headers = {"Content-Type": "application/json"}
data = {
"model": endpoints[0].model_names[0],
"prompt": request_json.get("prompt", ""),
}
# Chat-completions bodies carry `messages`, not `prompt` - send
# them as vLLM's TokenizeChatRequest so the engine applies its
# own chat template (`add_generation_prompt=True` is the
# chat-completions default, pinned explicitly). Sending the old
# prompt-form payload for a chat body tokenizes "" and the KV
# lookup keys on garbage.
if "messages" in request_json:
data = {
"model": endpoints[0].model_names[0],
"messages": request_json["messages"],
"add_generation_prompt": True,
}
else:
data = {
"model": endpoints[0].model_names[0],
"prompt": request_json.get("prompt", ""),
}
Comment on lines +683 to +693

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Correctness Issue: Local Tokenizer Bypasses Chat Template / Messages

Similar to the issue in KvawareRouter.route_request, if the local tokenizer successfully loads, the try block at line 668 will succeed because request_json.get("prompt", "") will return "" (since prompt is not in a chat-completions request).

This means self.tokenizer.encode("") will execute successfully without raising an exception, and the router will completely bypass this except Exception: block. As a result, it will perform a KV lookup on the empty prompt "" instead of the actual chat messages.

To fix this, we should explicitly prevent the local tokenizer from encoding an empty prompt when messages is present in the request. Since local chat template tokenization is not yet supported in this interim patch, we can raise an exception in the try block if messages is in request_json to force the remote /tokenize fallback.

For example, update the try block above (around line 668) to:

try:
    if "messages" in request_json:
        raise NotImplementedError("Local chat template tokenization not supported yet")
    if self.tokenizer is None:
        self.tokenizer = AutoTokenizer.from_pretrained(
            endpoints[0].model_names[0]
        )
    return self.tokenizer.encode(request_json.get("prompt", ""))

loop = asyncio.get_running_loop()
response = await loop.run_in_executor(
None,
Expand Down