diff --git a/tests/engine/test_omni_request_output.py b/tests/engine/test_omni_request_output.py index fa25bba9e..eb1bdfd41 100644 --- a/tests/engine/test_omni_request_output.py +++ b/tests/engine/test_omni_request_output.py @@ -270,3 +270,56 @@ class TestMsgpackRoundTrip: # Content merged from legacy nested key assert result.prompt == "legacy prompt" assert result.prompt_token_ids == [1, 2] + + +class TestRequestOutputFieldParity: + """``OmniRequestOutput`` must expose everything ``RequestOutput`` sets. + + ``RequestOutput`` is a plain class, so the dataclass-generated ``__init__`` + replaces its ``__init__`` and the inherited attributes only exist because + they are redeclared as fields. Any vLLM field that is not redeclared simply + does not exist on an omni output, and vLLM's own serving code reads those + attributes unconditionally — ``/v1/completions`` returned HTTP 500 with + ``AttributeError: 'OmniRequestOutput' object has no attribute + 'ec_transfer_params'`` for exactly that reason. This test fails on the next + vLLM bump that adds a field, instead of leaving it for an endpoint to hit. + """ + + def test_carries_every_attribute_request_output_sets(self) -> None: + reference = _make_text_request_output() + omni = OmniRequestOutput(request_id="req-parity") + + missing = sorted(name for name in vars(reference) if not name.startswith("_") and not hasattr(omni, name)) + assert not missing, ( + f"OmniRequestOutput is missing RequestOutput attributes {missing}; " + "redeclare them as dataclass fields in vllm_omni/outputs/__init__.py" + ) + + def test_transfer_param_fields_default_to_none(self) -> None: + omni = OmniRequestOutput(request_id="req-defaults") + assert omni.kv_transfer_params is None + assert omni.ec_transfer_params is None + assert omni.num_cached_tokens is None + assert omni.num_cache_creation_tokens is None + + def test_transfer_param_fields_are_copied_from_the_stage_output(self) -> None: + """Declaring the fields is not enough — they must also be copied. + + ``from_stage_output`` only carries the names listed in + ``_REQUEST_OUTPUT_CONTENT_ATTRS``. With the fields declared but absent + from that list the endpoint stops raising ``AttributeError`` while + silently dropping external-connector metadata and cache-creation + accounting, which is harder to notice than the crash it replaced. + """ + source = _make_text_request_output() + source.num_cache_creation_tokens = 7 + source.ec_transfer_params = {"connector": "shm", "handle": "abc"} + source.num_cached_tokens = 3 + source.kv_transfer_params = {"role": "producer"} + + omni = OmniRequestOutput.from_stage_output(source, stage_id=1) + + assert omni.num_cache_creation_tokens == 7 + assert omni.ec_transfer_params == {"connector": "shm", "handle": "abc"} + assert omni.num_cached_tokens == 3 + assert omni.kv_transfer_params == {"role": "producer"} diff --git a/vllm_omni/outputs/__init__.py b/vllm_omni/outputs/__init__.py index 105d0222d..90197f6e6 100644 --- a/vllm_omni/outputs/__init__.py +++ b/vllm_omni/outputs/__init__.py @@ -85,7 +85,9 @@ _REQUEST_OUTPUT_CONTENT_ATTRS = ( "encoder_prompt", "encoder_prompt_token_ids", "num_cached_tokens", + "num_cache_creation_tokens", "kv_transfer_params", + "ec_transfer_params", ) # Omni-specific content copied when the wrapped stage output is itself an @@ -147,7 +149,9 @@ class OmniRequestOutput(RequestOutput): encoder_prompt: str | None = None encoder_prompt_token_ids: list[int] | None = None num_cached_tokens: int | None = None + num_cache_creation_tokens: int | None = None kv_transfer_params: dict[str, Any] | None = None + ec_transfer_params: dict[str, Any] | None = None # --- Pipeline stage fields --- stage_id: int | None = None