Skip to content

[Bugfix] Route chat completions on the conversation, not the session id - #213

Open
aoshen02 wants to merge 1 commit into
vllm-project:mainfrom
aoshen02:fix/cache-aware-chat-routing-text
Open

aoshen02 wants to merge 1 commit into
vllm-project:mainfrom
aoshen02:fix/cache-aware-chat-routing-text

Conversation

@aoshen02

Copy link
Copy Markdown
Contributor

Problem

ChatCompletionRequest::extract_text_for_routing returns session_params.session_id, or an empty string when the request carries none:

https://github.com/vllm-project/router/blob/main/src/protocols/spec.rs#L544-L558

cache_aware feeds that value into its radix tree. With an empty string every chat request looks identical to every other one, so the tree never matches a prefix, match_rate stays at 0, and selection falls through to the minimum-load branch. Chat completions is the most used endpoint here, so cache_aware is effectively plain load balancing on it.

The sibling implementations of the same trait method already return real content — CompletionRequest returns the prompt, ResponsesRequest walks the input items. Chat is the odd one out.

Fix

Return the conversation text: system, user (including text parts of multimodal content), and assistant messages joined in order.

Session affinity is unaffected. Hash policies resolve their key through hash_key::extract_hash_key, which checks x-session-id and the other session headers first.

Behaviour change

A caller that sends session_params.session_id in the body and no session header previously got a stable routing key, because the session id happened to be the routing text and fell through to the request:{text} fallback. That key now becomes a hash of the conversation, which grows every turn, so such a caller loses session affinity until it sends a session header. Callers using headers — the documented path — are unaffected.

Testing

cargo test --lib — 485 passed, 0 failed. Two tests added alongside the existing GenerationRequest trait tests: one asserts a continued conversation extends the previous turn's routing text, one asserts session_params no longer displaces the conversation.

End to end on 3 nodes, replaying recorded Codex agent traces (128 sessions, 5280 requests, contexts to ~100K tokens) against 4 vLLM replicas with --policy cache_aware. The router's own debug counters for consecutive turns of one session:

turn before after
1 matched_chars=0 input_chars=0 matched_chars=0 input_chars=5100
2 matched_chars=0 input_chars=0 matched_chars=5100 input_chars=6702
3 matched_chars=0 input_chars=0 matched_chars=6702 input_chars=8304

Each turn now matches the entirety of the turn before it, which is the prefix reuse cache_aware exists to exploit. A 20-session consistent_hash run over the same workload kept every session pinned to one worker, unchanged by this patch.

Duplicate check

gh pr list --search cache_aware and an issue search for the routing-text behaviour found nothing covering this. The open cache-aware PRs are #211 (load double-decrement), #176 (PD stream load tracking) and #130 (KV-event routing), all unrelated.

AI assistance was used for this change; the diff and the test results above were reviewed and reproduced by the submitter.

🤖 Generated with Claude Code

ChatCompletionRequest::extract_text_for_routing returned session_params.
session_id, or an empty string when the request had none. cache_aware feeds
that value into its radix tree, so every chat request looked identical to
every other one: the tree could never match a prefix, the match rate stayed
at zero, and the policy fell through to its minimum-load branch. Chat
completions is the most used endpoint on this router, so in practice
cache_aware was plain load balancing there.

Return the conversation text instead, matching the CompletionRequest and
ResponsesRequest implementations of the same trait method. Session affinity
is unaffected: hash policies resolve their key through
hash_key::extract_hash_key, which reads request headers first.

Measured on a 3-node replay of recorded Codex agent traces (128 sessions,
5280 requests, ~100K-token contexts) with --policy cache_aware. The router's
own debug counters, per turn of one session:

  before   matched_chars=0     input_chars=0
  after    matched_chars=0     input_chars=5100
  after    matched_chars=5100  input_chars=6702
  after    matched_chars=6702  input_chars=8304

Each turn now matches the whole of the previous turn, which is the prefix
reuse cache_aware exists to exploit. A 20-session consistent_hash run over
the same workload kept every session on one worker, unchanged.

AI assistance was used for this change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: aab30fab32

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/protocols/spec.rs
Comment on lines +549 to +551
// policy silently degrades to min-load. Session affinity is a separate
// concern, resolved from headers and the body by `hash_key::extract_hash_key`.
let mut buffer = String::new();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve body session IDs for hash-based routing

When a chat client uses the documented session_params.session_id body field without a session header, route_typed_request now passes only this joined conversation to ConsistentHashPolicy and RendezvousHashPolicy. Consequently, hash_key::extract_hash_key cannot see the session ID, and a growing conversation produces a different fallback hash on each turn, potentially sending continual-prompting requests to different backends. The previous implementation supplied a stable session ID, so the conversation text and body routing metadata need to be exposed separately rather than discarding the latter.

Useful? React with 👍 / 👎.

@hsliuustc0106 hsliuustc0106 added the bug Something isn't working label Sep 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants