Skip to content

feat(zmq): typed structured outputs for the direct backend, with e2e coverage - #2041

Open
slin1237 wants to merge 10 commits into
mainfrom
feat/zmq-e2e-tests
Open

feat(zmq): typed structured outputs for the direct backend, with e2e coverage#2041
slin1237 wants to merge 10 commits into
mainfrom
feat/zmq-e2e-tests

Conversation

@slin1237

@slin1237 slin1237 commented Aug 4, 2026

Copy link
Copy Markdown
Member

Summary

Adds end-to-end coverage for the ZMQ direct backend connector and folds in
the two pieces of engine-side support the e2e lanes exercise:

  1. E2E harness for ZMQ — a new ConnectionMode.ZMQ path through the
    worker/gateway fixtures, worker-pool eviction that accounts for the
    single-gateway handshake sockets, ZMQ command builders, connection-mode
    validation, and the CI lanes (e2e-1gpu-chat-zmq for vLLM and
    TokenSpeed).

  2. Typed structured outputs — ports the structured-outputs (guided
    decoding) protocol module into crates/engine_zmq_client and wires the
    proto constraint oneof onto typed StructuredOutputsParams in the
    gateway's ZMQ client, replacing the previous reject-guard. Constraints
    supported: JSON schema, regex, grammar, structural tag, choice, and
    json-object. The backend lowers to guidance engine-side, matching the
    reference behavior.

  3. Readiness timing — gRPC/HTTP workers are health-checked (model
    loaded) by the pool before the gateway starts, but ZMQ workers return
    immediately and load the model inside the gateway's readiness gate. The
    fixtures now widen the gateway readiness timeout to the model's
    startup_timeout (default 300s) for ZMQ so model load isn't clipped by
    the 60s router default.

Testing

  • engine-zmq-client unit tests (structured-outputs round-trip, constraint
    validation: exactly-one / missing / multiple / json-object=false).
  • Gateway zmq_client translation tests for each constraint kind.
  • ZMQ e2e chat lanes for vLLM and TokenSpeed on 1 GPU.

Run the existing local (non-PD/EPD) e2e cases over the ZMQ direct
backend, in addition to gRPC/HTTP, for both vLLM and TokenSpeed.

- Add ConnectionMode.ZMQ and an E2E_CONNECTION_MODE per-lane override
  (mirrors how E2E_RUNTIME picks the engine): a grpc/http case runs over
  ZMQ without a separate parametrize value.
- Worker: derive the ipc:// base_url from serve._zmq_ipc_url, skip the
  worker health wait (readiness is gateway-gated), and build the headless
  vLLM/TokenSpeed commands by delegating to the serve launchers so engine
  flags and the FNV-1a handshake port stay in lockstep with production.
- Gateway: pass --router-backend explicitly (the router cannot probe the
  wire from an ipc:// URL).
- Worker pool: do not cache ZMQ workers (an engine dials one gateway's
  handshake sockets and cannot be reused); stop them with the gateway.
- Exclude the gRPC-only families from ZMQ lanes (PD, EPD, multi-worker)
  and collapse grpc/http twins onto a single ZMQ run, via
  pytest_collection_modifyitems using only public API.
- CI: add an e2e-1gpu-chat-zmq lane (vllm, tokenspeed) and thread the
  connection_mode input through the gpu job. TokenSpeed keeps its pinned
  install commit.
- Add no-GPU unit tests for the ZMQ command builders and base_url.

Signed-off-by: Simo Lin <25425177+slin1237@users.noreply.github.com>
@github-actions github-actions Bot added ci CI/CD configuration changes tests Test changes labels Aug 4, 2026
@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Added optional ZMQ connection-mode support for GPU end-to-end test workflows.
    • Added ZMQ chat completion coverage for vLLM and TokenSpeed.
    • Added support for launching and managing ZMQ worker connections.
    • Added structured-output support for vLLM over ZMQ, including JSON schema, regex, grammar, choice, and structural-tag formats.
  • Bug Fixes

    • Improved test selection and validation for incompatible or duplicate ZMQ scenarios.
    • Ensured ZMQ workflow failures are reported by the overall test run.
  • Tests

    • Added coverage for ZMQ commands, IPC endpoints, configuration parsing, structured outputs, and filtering behavior.

Walkthrough

Adds ZMQ as an E2E connection mode and adds typed vLLM structured-output support for the ZMQ EngineCore client. It updates worker and gateway lifecycle handling, test selection, GPU CI coverage, protocol serialization, request translation, and validation tests.

Changes

ZMQ E2E support

Layer / File(s) Summary
Connection mode contract
e2e_test/infra/constants.py, e2e_test/infra/__init__.py, e2e_test/fixtures/setup_backend.py, e2e_test/infra/test_connection_mode.py, e2e_test/fixtures/test_connection_mode_validation.py
Adds ConnectionMode.ZMQ, environment override parsing, public exports, and runtime engine validation.
ZMQ worker launch support
e2e_test/infra/worker.py, e2e_test/infra/worker_pool.py, e2e_test/infra/gateway.py, e2e_test/infra/test_zmq_cmd_builders.py
Adds ZMQ URLs, launcher commands, gateway-managed readiness, backend gateway arguments, uncached worker startup, and command-builder tests.
Gateway and fixture lifecycle
e2e_test/fixtures/setup_backend.py
Applies connection overrides, passes runtime backends and readiness timeouts to gateways, and stops ZMQ workers during teardown.
Test selection and CI integration
e2e_test/fixtures/hooks.py, e2e_test/fixtures/test_hooks_zmq_filter.py, .github/workflows/e2e-gpu-job.yml, .github/workflows/pr-test-rust.yml
Filters incompatible tests, deduplicates local HTTP/gRPC cases, configures workflow overrides and artifact names, and adds the ZMQ GPU matrix job.

vLLM structured outputs

Layer / File(s) Summary
Structured-output protocol types
crates/engine_zmq_client/src/error.rs, crates/engine_zmq_client/src/protocol/vllm/*
Adds typed structured-output constraints, options, constructors, Python-compatible serialization, validation, and sampling integration.
Structured-output request translation
model_gateway/src/routers/grpc/zmq_client.rs
Translates vLLM JSON, regex, grammar, structural-tag, JSON-object, and choice constraints into typed EngineCore parameters and retains prompt-logprob validation.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant CI
  participant e2e-gpu-job
  participant setup_backend
  participant Worker
  participant Gateway
  CI->>e2e-gpu-job: Start with connection_mode=zmq
  e2e-gpu-job->>setup_backend: Set E2E_CONNECTION_MODE
  setup_backend->>Worker: Build and start ZMQ worker
  setup_backend->>Gateway: Start gateway with backend and readiness timeout
  Gateway->>Worker: Validate readiness
  setup_backend->>Worker: Stop ZMQ worker during teardown
Loading

Possibly related PRs

Suggested labels: protocols

Suggested reviewers: key4ng, catherinesue

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 68.97% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
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.
Title check ✅ Passed The title clearly identifies typed structured outputs for the ZMQ direct backend and the related end-to-end coverage.
Description check ✅ Passed The description accurately covers the ZMQ e2e harness, typed structured outputs, readiness timing, and testing.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/zmq-e2e-tests

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.

# holds the GPUs the caller is about to claim. ZMQ workers are
# likewise uncached: the engine dials one gateway's handshake
# sockets and cannot be reused by the next class's gateway.
if worker_type != WorkerType.REGULAR or mode == ConnectionMode.ZMQ:

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: The condition now also matches WorkerType.REGULAR + ConnectionMode.ZMQ, but the log message on line 99 still says "non-REGULAR". For a ZMQ regular worker, the log would read "evicting … for non-REGULAR regular/…" which is contradictory. Consider updating the message to something like "non-cacheable":

Suggested change
if worker_type != WorkerType.REGULAR or mode == ConnectionMode.ZMQ:
if worker_type != WorkerType.REGULAR or mode == ConnectionMode.ZMQ:
if self._key is not None:
logger.info(
"WorkerPool: evicting %s to free GPUs for non-cacheable %s/%s",

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

Actionable comments posted: 4

🧹 Nitpick comments (1)
e2e_test/fixtures/hooks.py (1)

158-183: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

🟡 Nit Add collection tests for the ZMQ item filter and deduplication key.

E2E_CONNECTION_MODE=zmq now depends on _filter_zmq_items, but the e2e tests only cover ZMQ command builders and base URLs. Add tests for grpc/http twin collapse, http-only retention, PD/EPD/empty-tuple exclusions, multi-worker exclusion, and cases without setup_backend.

🤖 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 `@e2e_test/fixtures/hooks.py` around lines 158 - 183, Add collection-level
tests covering `_filter_zmq_items` and `_zmq_dedup_key`: verify grpc/http twins
collapse to one run, http-only cases remain, PD/EPD cases including empty
topology tuples are deselected, multi-worker items are excluded, and items
without `setup_backend` remain kept. Use representative pytest items or existing
fixture helpers and assert both kept and deselected collections.
🤖 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.

Inline comments:
In @.github/workflows/pr-test-rust.yml:
- Around line 546-580: The e2e-gpu-job workflow generates identical worker-log
artifact names for the gRPC and ZMQ chat jobs. Update the artifact naming logic
in e2e-gpu-job.yml to include E2E_CONNECTION_MODE in the suffix, preserving the
existing engine, GPU tier, and test-directory components so ZMQ logs remain
uniquely restorable.

In `@e2e_test/fixtures/setup_backend.py`:
- Around line 149-153: Reject ConnectionMode.ZMQ after resolving engine unless
the engine is vllm or tokenspeed, using a shared validation helper. Apply this
validation at e2e_test/fixtures/setup_backend.py lines 149-153 and 484-489, and
add parameterized tests covering supported and unsupported runtime/mode
combinations.

In `@e2e_test/infra/constants.py`:
- Around line 140-141: Update the connection-mode parsing around
ENV_CONNECTION_MODE to distinguish an unset variable from an explicitly empty
value: return None only when the environment lookup returns None, and raise
ValueError for an empty string before calling ConnectionMode. Preserve
case-insensitive handling for valid values and rejection of invalid values, and
add tests covering unset, empty, valid mixed-case, and invalid inputs.

In `@e2e_test/infra/test_zmq_cmd_builders.py`:
- Around line 7-8: Move the module-level pytest.importorskip("smg.serve") from
the top of test_zmq_cmd_builders.py into the ZMQ-specific tests or fixture that
requires serve, so test_grpc_worker_still_uses_grpc_url always runs
independently of smg.serve availability.

---

Nitpick comments:
In `@e2e_test/fixtures/hooks.py`:
- Around line 158-183: Add collection-level tests covering `_filter_zmq_items`
and `_zmq_dedup_key`: verify grpc/http twins collapse to one run, http-only
cases remain, PD/EPD cases including empty topology tuples are deselected,
multi-worker items are excluded, and items without `setup_backend` remain kept.
Use representative pytest items or existing fixture helpers and assert both kept
and deselected collections.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: fcf2da9c-5d27-4e54-8b44-71c108d7f055

📥 Commits

Reviewing files that changed from the base of the PR and between 3f701a3 and 054ff01.

📒 Files selected for processing (10)
  • .github/workflows/e2e-gpu-job.yml
  • .github/workflows/pr-test-rust.yml
  • e2e_test/fixtures/hooks.py
  • e2e_test/fixtures/setup_backend.py
  • e2e_test/infra/__init__.py
  • e2e_test/infra/constants.py
  • e2e_test/infra/gateway.py
  • e2e_test/infra/test_zmq_cmd_builders.py
  • e2e_test/infra/worker.py
  • e2e_test/infra/worker_pool.py

Comment thread .github/workflows/pr-test-rust.yml
Comment thread e2e_test/fixtures/setup_backend.py
Comment thread e2e_test/infra/constants.py Outdated
Comment thread e2e_test/infra/test_zmq_cmd_builders.py Outdated

@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, well-structured test-infrastructure PR. The ZMQ lane integration is thorough: collection filtering correctly deselects PD/EPD/multi-worker families, the dedup logic for grpc/http twins is sound, worker lifecycle is properly handled (no caching for ZMQ, explicit teardown), and the CI wiring threads the connection_mode input through correctly. One minor nit on a log message.

The ZMQ chat lane failed CI because gateway.py passed --router-backend,
which launch_router.py does not accept (the flag is --backend); the router
crashed on every launch and the lane timed out. Switch to --backend.

Also address review feedback:
- disambiguate worker-log artifact names by connection mode so the gRPC
  and ZMQ vllm chat legs no longer collide
- reject ConnectionMode.ZMQ for engines that can't speak it via a shared
  validation helper, failing fast instead of timing out
- distinguish an unset E2E_CONNECTION_MODE from a set-but-empty/invalid
  value, which now raises
- scope the smg.serve importorskip to the ZMQ command-builder tests so the
  gRPC test runs without the wheel
- add unit tests for connection-mode parsing, engine validation, and the
  ZMQ collection filter

Signed-off-by: Simo Lin <25425177+slin1237@users.noreply.github.com>
@slin1237
slin1237 force-pushed the feat/zmq-e2e-tests branch from 137b453 to 4e49efa Compare August 4, 2026 02:38

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
e2e_test/fixtures/test_connection_mode_validation.py (1)

8-9: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Keep the validation test independent of cloud fixture dependencies.

pytest.importorskip("fixtures.setup_backend") makes e2e_test/fixtures/test_connection_mode_validation.py unreachable when fixtures.setup_backend cannot import, including when Anthropic/OpenAI/GenAI cloud modules are unavailable. Move _validate_connection_mode and ZMQ_CAPABLE_ENGINES out to an unused dependency-free E2E infra module, import it from both fixtures/setup_backend.py and this test file, and require CI to collect and run all three parametrized test cases.

🤖 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 `@e2e_test/fixtures/test_connection_mode_validation.py` around lines 8 - 9,
Move _validate_connection_mode and ZMQ_CAPABLE_ENGINES from the cloud-dependent
fixture path into a dependency-free E2E infrastructure module, then import and
reuse them in both fixtures/setup_backend.py and
test_connection_mode_validation.py. Remove the pytest.importorskip dependency
from the validation test so all three parametrized cases are collected and run
even when cloud SDKs are unavailable.

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.

Inline comments:
In @.github/workflows/e2e-gpu-job.yml:
- Line 177: Update the workflow step around MODE so it reuses the already
assigned E2E_CONNECTION_MODE instead of interpolating inputs.connection_mode
into shell source. Validate connection_mode against the supported wire-mode enum
before using it as an artifact suffix or writing it to GITHUB_OUTPUT, and reject
unsupported values.

---

Nitpick comments:
In `@e2e_test/fixtures/test_connection_mode_validation.py`:
- Around line 8-9: Move _validate_connection_mode and ZMQ_CAPABLE_ENGINES from
the cloud-dependent fixture path into a dependency-free E2E infrastructure
module, then import and reuse them in both fixtures/setup_backend.py and
test_connection_mode_validation.py. Remove the pytest.importorskip dependency
from the validation test so all three parametrized cases are collected and run
even when cloud SDKs are unavailable.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: dd8af8a5-1b21-4c2d-9151-e1a1fd2e22d3

📥 Commits

Reviewing files that changed from the base of the PR and between 054ff01 and 4e49efa.

📒 Files selected for processing (8)
  • .github/workflows/e2e-gpu-job.yml
  • e2e_test/fixtures/setup_backend.py
  • e2e_test/fixtures/test_connection_mode_validation.py
  • e2e_test/fixtures/test_hooks_zmq_filter.py
  • e2e_test/infra/constants.py
  • e2e_test/infra/gateway.py
  • e2e_test/infra/test_connection_mode.py
  • e2e_test/infra/test_zmq_cmd_builders.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • e2e_test/infra/gateway.py
  • e2e_test/infra/test_zmq_cmd_builders.py

Comment thread .github/workflows/e2e-gpu-job.yml
E2E_RUNTIME: ${{ inputs.engine }}
E2E_GPU_TIER: ${{ inputs.gpu_tier }}
E2E_VLLM_KV_BACKEND: ${{ inputs.vllm_kv_backend }}
E2E_CONNECTION_MODE: ${{ inputs.connection_mode }}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔴 Important: This env var is set unconditionally for every job that calls this reusable workflow. The connection_mode input defaults to "" (line 60), so every existing caller that doesn't pass connection_mode (e.g. e2e-1gpu-chat, e2e-1gpu-completions, …) will run with E2E_CONNECTION_MODE="".

get_connection_mode_override() in constants.py (line 146-147) intentionally raises ValueError on a set-but-empty value. Since pytest_collection_modifyitems calls this function at collection time, all non-ZMQ e2e jobs will crash before running any tests.

The workflow default and the Python validation are in conflict. One fix — treat empty the same as unset in the Python code:

if not value:
    return None

Or conditionally set the env var only when non-empty:

Suggested change
E2E_CONNECTION_MODE: ${{ inputs.connection_mode }}
E2E_CONNECTION_MODE: ${{ inputs.connection_mode || '' }}

(Though that still sets it to "" — the Python-side fix is cleaner.)

The workflow always exports E2E_CONNECTION_MODE and leaves it empty for the
non-override lanes, so raising on a set-but-blank value broke every regular
e2e lane at collection time. Map blank to None (the default) while still
rejecting genuinely-unrecognized values.

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

Port the engine-core structured-outputs params (guided decoding) into the
engine ZMQ client and wire the proto constraint oneof onto the EngineCore
wire, so ZMQ-backed vLLM workers honor json/regex/grammar/choice/
json_object/structural_tag constraints instead of returning unconstrained
text.

The client exposes an enum-backed StructuredOutputsParams and serializes
through a private sparse wire struct (omit-defaults, skip-if-false) that
matches Python engine-core; the backend is always lowered to guidance.
translate_sampling maps json_object=false to no constraint.

Signed-off-by: Simo Lin <25425177+slin1237@users.noreply.github.com>
ZMQ workers spawn and return immediately (readiness is gated by the
gateway, not the worker), so model load happens inside the gateway's
readiness wait. That wait used DEFAULT_ROUTER_TIMEOUT (60s), too short
for large models. Use the model's startup_timeout for ZMQ gateways (never
shrinking an explicitly larger gateway timeout); gRPC/HTTP unchanged.

Signed-off-by: Simo Lin <25425177+slin1237@users.noreply.github.com>
@github-actions github-actions Bot added grpc gRPC client and router changes model-gateway Model gateway crate changes labels Aug 4, 2026
@@ -0,0 +1,315 @@
// SPDX-License-Identifier: Apache-2.0

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: The merge base commit (4bec9d6, PR #2043) removed SPDX-License-Identifier headers from every other file in this crate. This new file re-introduces one. Drop it to stay consistent:

Suggested change
// SPDX-License-Identifier: Apache-2.0
// Ported from the Apache-2.0 reference `vllm-engine-core-client`

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
e2e_test/fixtures/setup_backend.py (1)

281-312: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🔴 Important: Guard ZMQ worker cleanup before gateway operations.

In _setup_local and backend_router, _start_workers_tracked(...) / get_pool().acquire() can return ZMQ workers before the protected region starts. If _gateway_readiness_timeout(...) or Gateway() raises, stop_workers(workers) is skipped. Move worker acquisition into the try and place stop_workers(workers) in the inner finally; initialize gateway = None first and only call gateway.shutdown() when it is not None.

🤖 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 `@e2e_test/fixtures/setup_backend.py` around lines 281 - 312, Update the setup
flow around `_setup_local` and `backend_router` so ZMQ worker acquisition occurs
inside the protected `try` block, with `workers` cleanup in an inner `finally`
that always calls `stop_workers(workers)`. Initialize `gateway` to `None` before
gateway creation and only invoke `gateway.shutdown()` when a gateway was
successfully created, including when `_gateway_readiness_timeout(...)` or
`Gateway()` raises.
🧹 Nitpick comments (2)
e2e_test/fixtures/setup_backend.py (1)

115-133: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

🟡 Nit: Add direct tests for _gateway_readiness_timeout.

Test these cases: non-ZMQ returns base_timeout, ZMQ preserves a larger base_timeout, and ZMQ uses a larger startup_timeout. Also test the explicit DEFAULT_STARTUP_TIMEOUT fallback.

The supplied test context covers _validate_connection_mode, but it does not show coverage for these timeout branches. Add or confirm parameterized coverage.

As per coding guidelines: “Run the pr-test-analyzer agent on changed files 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 `@e2e_test/fixtures/setup_backend.py` around lines 115 - 133, Add direct
parameterized tests for _gateway_readiness_timeout covering non-ZMQ returning
base_timeout, ZMQ retaining a larger base_timeout, ZMQ selecting a larger model
startup_timeout, and ZMQ using DEFAULT_STARTUP_TIMEOUT when startup_timeout is
absent. Mock get_model_spec as needed, and ensure the changed fixture file is
included in pr-test-analyzer verification.

Source: Coding guidelines

model_gateway/src/routers/grpc/zmq_client.rs (1)

1397-1454: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

🟡 Nit Test the structured-output path over ZMQ.

Line 1397 only checks in-memory translation. It does not verify that StructuredOutputsParams reaches EngineCore after ZMQ MessagePack serialization. Extend an existing VLLM ZMQ test with a constraint and assert the mock engine receives it. Add Grammar, StructuralTag, and invalid-JSON-schema fallback cases here so every translate_constraint branch has coverage.

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/zmq_client.rs` around lines 1397 - 1454,
Extend the existing VLLM ZMQ integration test around the structured-output
request path, rather than only testing translate_request in memory. Add a
structured-output constraint to the request and assert the mock EngineCore
receives the deserialized StructuredOutputsParams after ZMQ MessagePack
transport; cover Grammar, StructuralTag, and invalid JSON-schema fallback
branches in translate_constraint, 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.

Outside diff comments:
In `@e2e_test/fixtures/setup_backend.py`:
- Around line 281-312: Update the setup flow around `_setup_local` and
`backend_router` so ZMQ worker acquisition occurs inside the protected `try`
block, with `workers` cleanup in an inner `finally` that always calls
`stop_workers(workers)`. Initialize `gateway` to `None` before gateway creation
and only invoke `gateway.shutdown()` when a gateway was successfully created,
including when `_gateway_readiness_timeout(...)` or `Gateway()` raises.

---

Nitpick comments:
In `@e2e_test/fixtures/setup_backend.py`:
- Around line 115-133: Add direct parameterized tests for
_gateway_readiness_timeout covering non-ZMQ returning base_timeout, ZMQ
retaining a larger base_timeout, ZMQ selecting a larger model startup_timeout,
and ZMQ using DEFAULT_STARTUP_TIMEOUT when startup_timeout is absent. Mock
get_model_spec as needed, and ensure the changed fixture file is included in
pr-test-analyzer verification.

In `@model_gateway/src/routers/grpc/zmq_client.rs`:
- Around line 1397-1454: Extend the existing VLLM ZMQ integration test around
the structured-output request path, rather than only testing translate_request
in memory. Add a structured-output constraint to the request and assert the mock
EngineCore receives the deserialized StructuredOutputsParams after ZMQ
MessagePack transport; cover Grammar, StructuralTag, and invalid JSON-schema
fallback branches in translate_constraint, 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: 88f856d3-1aab-436a-95ce-37e5764de010

📥 Commits

Reviewing files that changed from the base of the PR and between 72a287b and baf9332.

📒 Files selected for processing (6)
  • crates/engine_zmq_client/src/error.rs
  • crates/engine_zmq_client/src/protocol/vllm/mod.rs
  • crates/engine_zmq_client/src/protocol/vllm/sampling.rs
  • crates/engine_zmq_client/src/protocol/vllm/structured_outputs.rs
  • e2e_test/fixtures/setup_backend.py
  • model_gateway/src/routers/grpc/zmq_client.rs

Use #[expect] instead of #[allow] for the trivially_copy_pass_by_ref
predicate (clippy::allow_attributes is deny-by-default), apply nightly
rustfmt to the ported module and the constraint translator, and let
ruff-format normalize the readiness-timeout helper calls.

Signed-off-by: Simo Lin <25425177+slin1237@users.noreply.github.com>
@slin1237 slin1237 changed the title test(e2e): cover the ZMQ direct backend for vLLM and TokenSpeed feat(zmq): typed structured outputs for the direct backend, with e2e coverage Aug 4, 2026
@claude

claude Bot commented Aug 4, 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.

EngineCore requires a concrete max_tokens (the wire field is
non-optional and serde-defaults to 16). The ZMQ path bypasses vLLM's
OpenAI frontend, which would otherwise default an unset max_tokens to
max_model_len - prompt_len. Without that, requests fell back to 16
tokens and truncated generation -- e.g. streaming reasoning never
reached content past the </think> transition.

Take max_model_len from the connected engine's ready handshake and
apply the same default; a connected engine is now required to translate
a request. Validated on Qwen3-30B-A3B over ZMQ: reasoning and content
both stream with finish delivered.

Signed-off-by: Simo Lin <25425177+slin1237@users.noreply.github.com>
The ZMQ direct path only forwarded per-token logprobs, dropping the
top_logprobs alternatives that clients request via `logprobs: N`. The
engine returns PositionLogprobs.entries as [sampled, top1..topk] (k+1
columns); take the first N entries per position to match what the gRPC
servicer produces, and accumulate them for the completion aggregate.

Adds a unit test asserting the requested-count shaping.

Signed-off-by: Simo Lin <25425177+slin1237@users.noreply.github.com>
Stop-string detection needs detokenized text, which vLLM does in its
frontend OutputProcessor. Over gRPC the Python servicer forwards the
stop strings to the engine, so vLLM trims the output and returns
finish_reason=stop itself. The ZMQ direct path talks to EngineCore,
which has no stop-string detection, so it ran to max_tokens and reported
"length" even though the gateway's own StopSequenceDecoder had already
trimmed the text correctly.

Synthesize finish_reason=stop from the local decoder signal across the
OpenAI chat (stream + non-stream), completions (non-stream), and
Anthropic Messages (stream + non-stream) paths, mirroring the existing
completions-streaming precedent. This is a no-op for gRPC: vLLM trims
the stop string before returning, so the local decoder never fires, and
matched_stop falls back to the engine value via or_else so token-stop
and length results are preserved.

Adds StopSequenceDecoder::matched_stop() so the matched string flows
into the OpenAI matched_stop field and the Anthropic stop_reason=
stop_sequence / stop_sequence field, matching gRPC parity. Unit tests
cover the string-match and token-stop cases.

Signed-off-by: Simo Lin <25425177+slin1237@users.noreply.github.com>
@github-actions github-actions Bot added the tokenizer Tokenizer related changes label Aug 4, 2026
Engines that can't match string `stop` sequences server-side previously
handled them in three divergent ways: SGLang gRPC (skip_tokenizer_init)
had a dedicated resolver that dropped the strings and converted
single-token stops to stop_token_ids; the ZMQ vLLM path silently dropped
strings with no early-stop conversion; and the ZMQ TokenSpeed path
rejected any request carrying `stop` with a 400.

Consolidate all of them onto one helper, resolve_string_stops, applied
in the chat/completion/generate/messages request-building stages. For
backends whose engine sees token ids only — SGLang on any transport, and
every direct-ZMQ backend — it drops the string `stop` list (the
router-side StopSequenceDecoder trims the text) and forwards single-token
stops as stop_token_ids for early stopping; multi-token stops fall to the
decoder. gRPC vLLM (servicer detokenizes), TRT-LLM (server-side), and MLX
keep their strings untouched, gated by the backend's is_zmq() flag.

This fixes the TokenSpeed 400 (stop strings now work over ZMQ) and gives
the ZMQ vLLM path the single-token early-stop optimization it lacked. The
finish_reason=stop synthesis added earlier is already backend-neutral, so
the two halves together give consistent stop-string behavior across
gRPC-SGLang, ZMQ-vLLM, and ZMQ-TokenSpeed.

Supersedes #1877 (SGLang-only input-side fix). Adds unit tests for the
shared resolver and for TokenSpeed accepting residual stop strings.

Signed-off-by: Simo Lin <25425177+slin1237@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci CI/CD configuration changes grpc gRPC client and router changes model-gateway Model gateway crate changes tests Test changes tokenizer Tokenizer related changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant