Skip to content

refactor(zmq): clients finalize their own requests (no more stage-level transport probes) - #2068

Merged
slin1237 merged 5 commits into
mainfrom
zmq/phase1-client-finalization
Aug 7, 2026
Merged

refactor(zmq): clients finalize their own requests (no more stage-level transport probes)#2068
slin1237 merged 5 commits into
mainfrom
zmq/phase1-client-finalization

Conversation

@slin1237

@slin1237 slin1237 commented Aug 6, 2026

Copy link
Copy Markdown
Member

Motivation

Stacked on #2065. Phase 1 of the ZMQ architecture cleanup: the pipeline had one structural fact to model — vLLM EngineCore has no tokenizer — and it was being expressed as is_zmq() probes and catch-all runtime arms scattered across stages. This PR moves those decisions into the layer that already is the abstraction: BackendClient. No new types, tables, or layers.

Modifications (one commit each, reviewable in order)

  1. Client-owned request finalization. BackendClient::finalize_generate_request resolves string stops its engine can't match (token-only wires, SGLang skip_tokenizer_init) and returns the router's residual obligation — the stop strings the engine will never see. All four regular request-building stages drop their is_zmq() probes.
  2. EOS has one owning file. The request-time tokenizer backstop moves from the shared stage helper into zmq_client::fold_tokenizer_eos_backstop, next to the connect-time EosTokenIds. resolve_string_stops is now purely about stop strings.
  3. Harmony converges onto the obligations contract. Harmony request building finalizes through the client like the regular stages; response processing reads the stored obligation instead of inspecting the client (is_zmq on the single/decode leg) — the last response-side transport probe is gone. Regular-pipeline response side intentionally unchanged: MLX's proto carries no string-stop field, so its trimming still rides the ungated decoder (noted in the commit message as future work).
  4. No silent vLLM defaulting. The six _ => vLLM catch-alls (4× backend_client build matches, harmony's Zmq(_) arm, assemble's Vllm | Unspecified) become explicit RuntimeType::Vllm arms with hard errors for anything else — connect() already admits only vLLM/TokenSpeed, so this makes that contract visible at every dispatch point.

Checklist

  • cargo fmt clean
  • cargo test -p smg --lib: 1435 passed (the single failure, middleware::metrics::…interner, fails identically on clean main under the full parallel suite — pre-existing flake, passes in isolation)

@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

  • Bug Fixes
    • Improved stop-sequence handling across chat, completion, generation, and message requests.
    • Ensured stop strings are consistently trimmed from responses when required.
    • Added reliable end-of-sequence handling for supported vLLM and TokenSpeed connections, including duplicate prevention and respect for EOS settings.
    • Unsupported runtimes on ZMQ connections now return clear errors instead of being processed incorrectly.

Walkthrough

The change centralizes backend-specific stop finalization, adds router-owned stop obligations to response state, applies a ZMQ EOS backstop, and restricts ZMQ dispatch to supported runtimes. Harmony and regular request paths now use the centralized finalization flow.

Changes

Stop finalization and runtime routing

Layer / File(s) Summary
Backend stop finalization
model_gateway/src/routers/grpc/backend_client.rs, model_gateway/src/routers/grpc/common/stages/helpers.rs, model_gateway/src/routers/grpc/context.rs, model_gateway/src/routers/grpc/zmq_client.rs
BackendClient::finalize_generate_request now resolves stops, applies ZMQ EOS handling, and returns router stop obligations. Response state stores these obligations. EOS backstop tests cover insertion, deduplication, and ignore_eos.
Explicit ZMQ runtime dispatch
model_gateway/src/routers/grpc/backend_client.rs, model_gateway/src/routers/grpc/harmony/stages/request_building.rs, model_gateway/src/routers/grpc/multimodal/assemble.rs
ZMQ request and multimodal paths select vLLM only for RuntimeType::Vllm. Unsupported runtime variants now return errors.
Request and response pipeline integration
model_gateway/src/routers/grpc/regular/stages/*/request_building.rs, model_gateway/src/routers/grpc/harmony/stages/request_building.rs, model_gateway/src/routers/grpc/harmony/stages/response_processing.rs
Chat, completion, generate, message, and Harmony request builders use centralized finalization. Response processing reads stored router stop obligations.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Possibly related PRs

Suggested labels: tokenizer

Suggested reviewers: catherinesue, key4ng

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main refactor: BackendClient instances now finalize ZMQ client requests without stage-level transport probes.
Description check ✅ Passed The description directly explains client-owned request finalization, EOS handling, explicit runtime dispatch, and the related test results.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch zmq/phase1-client-finalization

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added grpc gRPC client and router changes model-gateway Model gateway crate changes labels Aug 6, 2026
@claude

claude Bot commented Aug 6, 2026

Copy link
Copy Markdown

👋 The PR description doesn't fully follow PULL_REQUEST_TEMPLATE.md:

  • Missing header: ## Description
  • Missing header: ### Problem
  • Missing header: ### Solution
  • Missing header: ## Changes
  • Missing header: ## Test Plan

Please update the PR description so reviewers have the context they need.


// gRPC vLLM matches stops server-side: nothing left for the router.
let mut req = vllm_request(vec!["."], vec![]);
assert!(resolve_string_stops(&mut req, Some(&mock_tokenizer()), false).is_empty());

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Nit: This test covers SGLang (always returns obligations) and gRPC vLLM (token_only_wire=false → empty), but doesn't assert the return value for the two ZMQ arms that also return obligations: vLLM with token_only_wire=true and TokenSpeed with token_only_wire=true.

The existing vllm_zmq_resolves_string_stops_and_injects_eos test above verifies side-effects (stop cleared, token ids set) but discards the return value. Worth adding a couple of assertions here to close the gap, e.g.:

// ZMQ vLLM: engine can't match strings, so all come back as obligations.
let mut req = vllm_request(vec![".", "Hello world"], vec![]);
let obligations = resolve_string_stops(&mut req, Some(&mock_tokenizer()), true);
assert_eq!(obligations, vec![".".to_string(), "Hello world".to_string()]);

@claude claude 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.

Clean refactor — the single-owner finalization model is a clear improvement over the scattered is_zmq() probes. The behavioral change for Harmony + SGLang gRPC (now correctly returning stop obligations) is an improvement, not a regression. One minor test coverage nit posted inline.

0 🔴 Important · 1 🟡 Nit · 0 🟣 Pre-existing

@slin1237
slin1237 force-pushed the zmq/phase0-arch-fixes branch from eb76224 to e08e75e Compare August 7, 2026 05:46
@slin1237
slin1237 force-pushed the zmq/phase1-client-finalization branch from e82264a to 6f6ef14 Compare August 7, 2026 05:56
Base automatically changed from zmq/phase0-arch-fixes to main August 7, 2026 07:21
…stop duty

The four request-building stages read builder_client.is_zmq() and threaded
the flag into helpers::resolve_string_stops — a client property enforced
two layers above the client. Move the decision to the client:

- BackendClient::finalize_generate_request resolves string stops its
  engine cannot match (token-only wires, SGLang skip_tokenizer_init) and
  returns the router's residual obligation: the stop strings the engine
  will never see, which response processing must trim from output text.
- resolve_string_stops returns those stripped strings instead of
  discarding them; its transport flag is now supplied by the client, and
  no stage carries is_zmq knowledge anymore.
- Obligation wiring into response processing lands with the follow-up
  convergence commit; stages currently discard the value, preserving
  today's behavior exactly.

Signed-off-by: Simo Lin <25425177+slin1237@users.noreply.github.com>
The EOS story was split across two layers: connect-time model-dir
resolution (EosTokenIds, zmq_client) and a request-time tokenizer fold
buried inside the shared stop-resolution helper — vLLM-ZMQ policy living
in a stage helper every backend shares.

Move the fold into zmq_client::fold_tokenizer_eos_backstop, invoked by
BackendClient::finalize_generate_request for vLLM-over-ZMQ only (the
tokenizer-less EngineCore stops at EOS only when the frontend supplies
ids; TokenSpeed's scheduler stops itself). resolve_string_stops is now
purely about stop strings, and every EOS mechanism — connect-time ids,
translate-time merge, request-time backstop — lives in one file.

Signed-off-by: Simo Lin <25425177+slin1237@users.noreply.github.com>
…_zmq

Harmony's response-processing stage inspected the selected client
(is_zmq on the single/decode leg) to decide whether the router must
scan channel text for string stops — the last response-side transport
probe. Converge it onto the request-building contract instead:

- HarmonyRequestBuildingStage finalizes its built request through
  BackendClient::finalize_generate_request like the regular stages, and
  stores the returned obligation on ResponseState.
- router_stop_strings() now just reads that stored obligation; empty
  for engines that match stops server-side. No transport inspection.
- The regular request-building stages store the same obligation instead
  of discarding it (parity for Chat: builders map request.stop into the
  proto, so the obligation equals what the old probe read; Responses
  builders don't carry stops yet, matching the probe's empty result).

The regular pipeline's response side intentionally still builds its
decoder from request params: MLX's proto has no string-stop field, so
the built request cannot report those strings as obligations yet, and
the ungated decoder remains the mechanism that trims them. Folding that
path onto obligations is future work once MLX stops ride the proto.

Signed-off-by: Simo Lin <25425177+slin1237@users.noreply.github.com>
Six dispatch sites treated "any ZMQ runtime that isn't TokenSpeed" as
vLLM via catch-all arms — an Unspecified (or future) runtime would
silently build vLLM-shaped requests instead of failing. connect()
already admits only vLLM/TokenSpeed over ZMQ, so make that contract
visible at every dispatch point:

- backend_client.rs: the four build_*_request ZMQ matches name
  RuntimeType::Vllm explicitly and return a hard error for anything
  else.
- harmony request building: the Zmq catch-all becomes a Vllm-guarded
  arm plus an explicit unsupported-runtime error.
- multimodal assemble: the ZMQ arm drops its Unspecified fallback; the
  existing not-supported error now catches it.

Signed-off-by: Simo Lin <25425177+slin1237@users.noreply.github.com>
The repo lints deny paths of 4+ segments (absolute-paths-max-segments =
3); bring helpers and fold_tokenizer_eos_backstop into scope in
backend_client, and MockTokenizer in the zmq_client tests.

Signed-off-by: Simo Lin <25425177+slin1237@users.noreply.github.com>
@slin1237
slin1237 force-pushed the zmq/phase1-client-finalization branch from 6f6ef14 to 45453dd Compare August 7, 2026 07:32

@coderabbitai coderabbitai 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.

🧹 Nitpick comments (1)
model_gateway/src/routers/grpc/backend_client.rs (1)

68-83: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy lift

🟡 Nit Add combined finalization tests.

The supplied tests cover resolve_string_stops and fold_tokenizer_eos_backstop separately. Add tests for finalize_generate_request that verify vLLM ZMQ receives stop IDs plus EOS IDs, TokenSpeed ZMQ receives no EOS IDs, and gRPC vLLM keeps server-side string stops.

As per coding guidelines, “Run the pr-test-analyzer agent to verify that tests adequately cover new or changed functionality.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@model_gateway/src/routers/grpc/backend_client.rs` around lines 68 - 83, Add
combined tests for finalize_generate_request covering vLLM ZMQ requests
retaining both resolved stop IDs and folded EOS IDs, TokenSpeed ZMQ requests
receiving no EOS IDs, and vLLM gRPC requests preserving server-side string
stops. Reuse the existing test helpers and fixtures for resolve_string_stops,
fold_tokenizer_eos_backstop, runtime selection, and request construction, then
run the pr-test-analyzer agent to verify coverage.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@model_gateway/src/routers/grpc/backend_client.rs`:
- Around line 68-83: Add combined tests for finalize_generate_request covering
vLLM ZMQ requests retaining both resolved stop IDs and folded EOS IDs,
TokenSpeed ZMQ requests receiving no EOS IDs, and vLLM gRPC requests preserving
server-side string stops. Reuse the existing test helpers and fixtures for
resolve_string_stops, fold_tokenizer_eos_backstop, runtime selection, and
request construction, then run the pr-test-analyzer agent to verify coverage.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 444ff063-e6bc-413d-9d73-f60a1118eaf8

📥 Commits

Reviewing files that changed from the base of the PR and between a976d82 and 45453dd.

📒 Files selected for processing (11)
  • model_gateway/src/routers/grpc/backend_client.rs
  • model_gateway/src/routers/grpc/common/stages/helpers.rs
  • model_gateway/src/routers/grpc/context.rs
  • model_gateway/src/routers/grpc/harmony/stages/request_building.rs
  • model_gateway/src/routers/grpc/harmony/stages/response_processing.rs
  • model_gateway/src/routers/grpc/multimodal/assemble.rs
  • model_gateway/src/routers/grpc/regular/stages/chat/request_building.rs
  • model_gateway/src/routers/grpc/regular/stages/completion/request_building.rs
  • model_gateway/src/routers/grpc/regular/stages/generate/request_building.rs
  • model_gateway/src/routers/grpc/regular/stages/messages/request_building.rs
  • model_gateway/src/routers/grpc/zmq_client.rs

@slin1237
slin1237 merged commit 459a796 into main Aug 7, 2026
41 of 44 checks passed
@slin1237
slin1237 deleted the zmq/phase1-client-finalization branch August 7, 2026 08:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

grpc gRPC client and router changes model-gateway Model gateway crate changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant