Skip to content

[Responses] Normalize OpenResponses input items to match openai-python SDK requirements - #35508

Merged
chaunceyjiang merged 3 commits into
vllm-project:mainfrom
anencore94:fix/responses-openresponses-input-normalization
Nov 2, 2026
Merged

chaunceyjiang merged 3 commits into
vllm-project:mainfrom
anencore94:fix/responses-openresponses-input-normalization

Conversation

@anencore94

@anencore94 anencore94 commented Feb 27, 2026 •

Copy link
Copy Markdown
Contributor

Overview

Accept incomplete assistant and reasoning history in Responses API requests without losing turns or crashing on empty assistant content. Related to #33089 and openai/codex#12669.

Claims

  • Fill missing assistant/reasoning fields through the existing input_item_parsing validator, preserving supplied metadata and non-assistant inputs.
  • Preserve the message discriminator so typeless assistant turns retain their content.
  • Handle empty tool-only assistant turns and keep dict-form reasoning/assistant merging consistent with typed items.
  • Preserve current tool-schema validation when integrating upstream changes.

Validation

At 35b420c, after the CI bot merged current main, these CPU tests passed on macOS/Python 3.12:

PYTHONPATH=. VLLM_TARGET_DEVICE=cpu VLLM_PLUGINS='' CUDA_VISIBLE_DEVICES='' \
  ../vllm-pr-review/.venv/bin/python -m pytest \
  tests/entrypoints/openai/responses/test_protocol.py \
  tests/entrypoints/openai/responses/test_responses_utils.py \
  tests/entrypoints/openai/responses/test_function_call_parsing.py \
  tests/entrypoints/openai/parser/test_harmony_utils.py \
  tests/entrypoints/openai/responses/test_sampling_params.py -q
# 182 passed, 1 package-version warning

Buildkite #97292 completed successfully on this head; GitHub reports 69 passing checks and three skipped. The existing history replay E2E tests in test_simple.py were not run locally; the aggregate CI result does not imply every test ran. No model-quality or latency improvement is claimed. The full CodeRabbit review on this head found no actionable correctness issues, with a remaining docstring-coverage warning (55.56% against its 80% threshold).

Details

The validator normalizes raw dictionaries before SDK object construction. The conversion changes handle typed or dict-form history later, including empty assistant content. This follows the review suggestion to reuse input_item_parsing; it does not add a parallel entry point. The empty-content and dict/model merge-policy reports in this discussion are addressed in the same history-conversion path.

The latest main integration retained upstream tool-schema validation. This continues the existing PR and its issue scope rather than opening a competing implementation. AI assistance was used for implementation, upstream integration, tests and this description.


Pull Request Checklist
  • I used vLLM's /pr-checklist skill.
  • AI assistance was used during the creation of this PR.
  • Design Fit: Reuses the existing validator and history conversion paths.
  • Testing and Validation: 182 CPU tests pass locally. Buildkite completed successfully; GitHub reports 69 passing checks and three skipped, with local E2E limitations recorded above.
  • Code Quality and Style: Changes remain scoped to history compatibility and regression coverage.
  • Pull Request Contents: Records current validation, earlier review responses and remaining limitations.

@gemini-code-assist gemini-code-assist Bot left a comment

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.

Code Review

This pull request introduces a normalization step for the Responses API input items, specifically addressing scenarios where assistant messages and reasoning items might omit id and status fields. The changes ensure that these items are accepted by populating sensible default values while preserving any existing id or status values provided by clients. New test cases have been added to validate this behavior, ensuring robustness in multi-turn agent scenarios.

@chaunceyjiang chaunceyjiang left a comment

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.

thanks~

@chaunceyjiang chaunceyjiang left a comment

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.

Can you write an e2e test? Mainly to align with the OpenAI spec.

@chaunceyjiang chaunceyjiang added the ready ONLY add when PR is ready to merge/full CI is needed label Feb 27, 2026
@anencore94 anencore94 changed the title [Responses] Accept assistant/reasoning items without id/status in input [Responses] Normalize OpenResponses input items to match openai-python SDK requirements Feb 27, 2026
@anencore94
anencore94 force-pushed the fix/responses-openresponses-input-normalization branch 3 times, most recently from d1bc512 to 285c05b Compare February 27, 2026 16:01
@mergify

mergify Bot commented Feb 27, 2026

Copy link
Copy Markdown
Contributor

Hi @anencore94, the pre-commit checks have failed. Please run:

uv pip install pre-commit
pre-commit install
pre-commit run --all-files

Then, commit the changes and push to your branch.

For future commits, pre-commit will run automatically on changed files before each commit.

Tip

Is mypy or markdownlint failing?
mypy and markdownlint are run differently in CI. If the failure is related to either of these checks, please use the following commands to run them locally:
# For mypy (substitute "3.10" with the failing version if needed)
pre-commit run --hook-stage manual mypy-3.10
# For markdownlint
pre-commit run --hook-stage manual markdownlint

1 similar comment
@mergify

mergify Bot commented Feb 27, 2026

Copy link
Copy Markdown
Contributor

Hi @anencore94, the pre-commit checks have failed. Please run:

uv pip install pre-commit
pre-commit install
pre-commit run --all-files

Then, commit the changes and push to your branch.

For future commits, pre-commit will run automatically on changed files before each commit.

Tip

Is mypy or markdownlint failing?
mypy and markdownlint are run differently in CI. If the failure is related to either of these checks, please use the following commands to run them locally:
# For mypy (substitute "3.10" with the failing version if needed)
pre-commit run --hook-stage manual mypy-3.10
# For markdownlint
pre-commit run --hook-stage manual markdownlint

@anencore94
anencore94 force-pushed the fix/responses-openresponses-input-normalization branch from 91d4c32 to c6c4529 Compare February 28, 2026 05:19
@anencore94

anencore94 commented Feb 28, 2026 •

Copy link
Copy Markdown
Contributor Author

Additional issues discovered during E2E testing

While adding E2E tests (test_input_history_openai_shape_e2e, etc.), the server returned 500 errors, which led to discovering and fixing two additional issues.

1. utils.py — dict-form reasoning items unhandled in non-harmony path (KeyError: 'role')

Non-harmony models (e.g. Qwen3) use _construct_single_message_from_response_item() in utils.py to convert input items. This function only performed Pydantic model isinstance checks (isinstance(item, ResponseReasoningItem), etc.), but when Pydantic parses the ResponseInputItemParam TypedDict union, input items may remain as plain dicts rather than model instances. All checks would fail and fall through to return item. Since reasoning dicts have no role key, this caused KeyError: 'role' downstream.

Fix: Added dict-form handling for reasoning (type == "reasoning") and assistant message (role == "assistant") items.

2. harmony.py — improved handling for items with missing type field

Changed response_msg["type"] direct access to .get("type") and added fallback heuristics: presence of "role" key → message, presence of "summary" key → reasoning. Also made content access on reasoning items safe via .get() with an empty assistant message fallback.

3. Pydantic ValidatorIterator issue

TypedDict Iterable fields (summary, content) may remain as ValidatorIterator (lazy iterator) after Pydantic parsing, causing TypeError: object of type 'ValidatorIterator' has no len(). Fixed by materialising with list() before indexing.

Finally tested all e2e test (test_simple.py) in my local enviroments. Please take a look @chaunceyjiang. Thanks for suggesting e2e tests.

@anencore94
anencore94 force-pushed the fix/responses-openresponses-input-normalization branch from c6c4529 to 7e34164 Compare March 20, 2026 17:02
@anencore94

Copy link
Copy Markdown
Contributor Author

Rebased on latest main, resolved merge conflicts, and fixed the failing entrypoints-integration-api-server-1 CI test (updated 3 tests in test_harmony_utils.py to match the new empty-content reasoning behavior). All 47 CI checks are now passing. Ready for re-review. @chaunceyjiang @NickLucche @DarkLight1337

@anencore94

Copy link
Copy Markdown
Contributor Author

Gentle ping — all CI checks are green (47/47 pass). Would appreciate a re-review when you get a chance. @qandrew @DarkLight1337 @chaunceyjiang

@chaunceyjiang chaunceyjiang left a comment

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.

Thanks~

@qandrew PTAL.

@anencore94

Copy link
Copy Markdown
Contributor Author

PTAL 🙇 @chaunceyjiang @qandrew

@glistering96

Copy link
Copy Markdown

@anencore94 Thank you for your contribution. I was also hitting the same error with Codex <-> vLLM settings and was looking for a way to fix it.

I tested this branch and confirmed that Codex multi-turn conversations now work well. While testing, I noticed a related improvement that might be worth considering, especially around basic skill/tool-call cases.

Codex client sent a /v1/responses request whose input list contained an assistant ResponseOutputMessage with empty content:

ResponseOutputMessage
role: assistant
status: completed
content_len: 0

I do not know why there was an empty content though.

That item appeared right before a tool call:

ResponseFunctionToolCall
name: Excel
call_id: call_b2464d05ecf79a8e

So this looks like a tool-calling assistant turn where the assistant produced no visible text, only a tool call. That is a valid shape to tolerate.

vLLM crashed because vllm/entrypoints/openai/responses/utils.py:214 assumed every ResponseOutputMessage had at least one text content part:

item.content[0].text

When content=[], that raised:

IndexError: list index out of range

So I added a fallback in utils.py around _construct_single_message_from_response_item:

diff --git a/vllm/entrypoints/openai/responses/utils.py b/vllm/entrypoints/openai/responses/utils.py
index 0988c5358..7bab1e683 100644
--- a/vllm/entrypoints/openai/responses/utils.py
+++ b/vllm/entrypoints/openai/responses/utils.py
@@ -211,6 +211,11 @@ def _construct_single_message_from_response_item(
             "reasoning": reasoning,
         }
     elif isinstance(item, ResponseOutputMessage):
+        if not item.content:
+            return {
+                "role": "assistant",
+                "content": "",
+            }
         return {
             "role": "assistant",
             "content": item.content[0].text,

This may be out of scope for the current PR, so I’m happy to open a separate issue by myself if that would be preferable.

++)
This was my conversation with codex that raised the error.

image

@jackjee0258

jackjee0258 commented Apr 30, 2026 •

Copy link
Copy Markdown

i have same problem.

when i use codex desktop with own qwen3.6, if i choose any skill example Skill Installer bababab, it return like this .

Reconnecting... 1/5
We're currently experiencing high demand, which may cause temporary errors.

and qwen log put:

(APIServer pid=1652600) ERROR:    Exception in ASGI application                                                                                           
(APIServer pid=1652600) Traceback (most recent call last):                                                                                                
(APIServer pid=1652600)   File "/mnt/data/home/xx/.conda/envs/qwen_opus/lib/python3.12/site-packages/uvicorn/protocols/http/httptools_impl.py", line
 420, in run_asgi                                                                                                                                         
(APIServer pid=1652600)     result = await app(  # type: ignore[func-returns-value]                                                                       
(APIServer pid=1652600)              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^                                                                       
(APIServer pid=1652600)   File "/mnt/data/home/xx/.conda/envs/qwen_opus/lib/python3.12/site-packages/uvicorn/middleware/proxy_headers.py", line 60, 
in __call__                                                                                                                                               
(APIServer pid=1652600)     return await self.app(scope, receive, send)                                                                                   (APIServer pid=1652600)            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^                                                                                   
(APIServer pid=1652600)   File "/mnt/data/home/xx/.conda/envs/qwen_opus/lib/python3.12/site-packages/fastapi/applications.py", line 1159, in __call_
_                                                                                                                                                         (APIServer pid=1652600)     await super().__call__(scope, receive, send)                                                                                  
(APIServer pid=1652600)   File "/mnt/data/home/xx/.conda/envs/qwen_opus/lib/python3.12/site-packages/starlette/applications.py", line 107, in __call
__                                                                                                                                                        
(APIServer pid=1652600)     await self.middleware_stack(scope, receive, send)                                                                             
(APIServer pid=1652600)   File "/mnt/data/home/xx/.conda/envs/qwen_opus/lib/python3.12/site-packages/starlette/middleware/errors.py", line 186, in _
_call__                                                                                                                                                   
(APIServer pid=1652600)     raise exc                                                                                                                     
(APIServer pid=1652600)   File "/mnt/data/home/xx/.conda/envs/qwen_opus/lib/python3.12/site-packages/starlette/middleware/errors.py", line 164, in _
_call__                                                                                                                                                   (APIServer pid=1652600)     await self.app(scope, receive, _send)                                                                                         
(APIServer pid=1652600)   File "/mnt/data/home/xx/.conda/envs/qwen_opus/lib/python3.12/site-packages/starlette/middleware/base.py", line 191, in __c
all__                                                                                                                                                     
(APIServer pid=1652600)     with recv_stream, send_stream, collapse_excgroups():                                                                          
(APIServer pid=1652600)                                    ^^^^^^^^^^^^^^^^^^^^                                                                           
(APIServer pid=1652600)   File "/mnt/data/home/xx/.conda/envs/qwen_opus/lib/python3.12/contextlib.py", line 158, in __exit__                        
(APIServer pid=1652600)     self.gen.throw(value)                                                                                                         (APIServer pid=1652600)   File "/mnt/data/home/xx/.conda/envs/qwen_opus/lib/python3.12/site-packages/starlette/_utils.py", line 87, in collapse_excg
roups                                                                                                                                                     
(APIServer pid=1652600)     raise exc                                                                                                                     
(APIServer pid=1652600)   File "/mnt/data/home/xx/.conda/envs/qwen_opus/lib/python3.12/site-packages/starlette/middleware/base.py", line 193, in __call__                                 
(APIServer pid=1652600)     response = await self.dispatch_func(request, call_next)                                                                       
(APIServer pid=1652600)                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^                                                                       
(APIServer pid=1652600)   File "/mnt/data/home/xx/workplace/models/start_Qwen3.6-27B-WA/auth_middleware.py", line 76, in dispatch                   (APIServer pid=1652600)     return await call_next(request)                                                                                               
(APIServer pid=1652600)            ^^^^^^^^^^^^^^^^^^^^^^^^                                                                                               
(APIServer pid=1652600)   File "/mnt/data/home/xx/.conda/envs/qwen_opus/lib/python3.12/site-packages/starlette/middleware/base.py", line 168, in cal
l_next                                                                                                                                                    (APIServer pid=1652600)     raise app_exc from app_exc.__cause__ or app_exc.__context__                                                                   
(APIServer pid=1652600)   File "/mnt/data/home/xx/.conda/envs/qwen_opus/lib/python3.12/site-packages/starlette/middleware/base.py", line 144, in cor
o                                                                                                                                                         
(APIServer pid=1652600)     await self.app(scope, receive_or_disconnect, send_no_error)                                                                   (APIServer pid=1652600)   File "/mnt/data/home/xx/.conda/envs/qwen_opus/lib/python3.12/site-packages/starlette/middleware/cors.py", line 87, in __ca
ll__                                                                                                                                                      
(APIServer pid=1652600)     await self.app(scope, receive, send)                                                                                          
(APIServer pid=1652600)   File "/mnt/data/home/xx/.conda/envs/qwen_opus/lib/python3.12/site-packages/prometheus_fastapi_instrumentator/middleware.py", line 177, in __call__                                                     
(APIServer pid=1652600)     raise exc                                                                                                                     
(APIServer pid=1652600)   File "/mnt/data/home/xx/.conda/envs/qwen_opus/lib/python3.12/site-packages/prometheus_fastapi_instrumentator/middleware.py
", line 175, in __call__                                                                                                                                  (APIServer pid=1652600)     await self.app(scope, receive, send_wrapper)                                                                                  
(APIServer pid=1652600)   File "/mnt/data/home/xx/.conda/envs/qwen_opus/lib/python3.12/site-packages/starlette/middleware/exceptions.py", line 63, i
n __call__                                                                                                                                                
(APIServer pid=1652600)     await wrap_app_handling_exceptions(self.app, conn)(scope, receive, send)                                                      
(APIServer pid=1652600)   File "/mnt/data/home/xx/.conda/envs/qwen_opus/lib/python3.12/site-packages/starlette/_exception_handler.py", line 53, in wrapped_app                                                                                                                                                
(APIServer pid=1652600)     raise exc                                        
(APIServer pid=1652600)   File "/mnt/data/home/xx/.conda/envs/qwen_opus/lib/python3.12/site-packages/starlette/_exception_handler.py", line 42, in w
rapped_app                            
(APIServer pid=1652600)     await app(scope, receive, sender)                                                                                             
(APIServer pid=1652600)   File "/mnt/data/home/xx/.conda/envs/qwen_opus/lib/python3.12/site-packages/fastapi/middleware/asyncexitstack.py", line 18,
in __call__                                                                                                                                              
(APIServer pid=1652600)     await self.app(scope, receive, send)                                                                                          
(APIServer pid=1652600)   File "/mnt/data/home/xx/.conda/envs/qwen_opus/lib/python3.12/site-packages/starlette/routing.py", line 716, in __call__   
(APIServer pid=1652600)     await self.middleware_stack(scope, receive, send)                                                                             
(APIServer pid=1652600)   File "/mnt/data/home/xx/.conda/envs/qwen_opus/lib/python3.12/site-packages/starlette/routing.py", line 736, in app        
(APIServer pid=1652600)     await route.handle(scope, receive, send)                                                                                      (APIServer pid=1652600)   File "/mnt/data/home/xx/.conda/envs/qwen_opus/lib/python3.12/site-packages/starlette/routing.py", line 290, in handle     
(APIServer pid=1652600)     await self.app(scope, receive, send)                                                                                          
(APIServer pid=1652600)   File "/mnt/data/home/xx/.conda/envs/qwen_opus/lib/python3.12/site-packages/fastapi/routing.py", line 134, in app          
(APIServer pid=1652600)     await wrap_app_handling_exceptions(app, request)(scope, receive, send)                                                        
(APIServer pid=1652600)   File "/mnt/data/home/xx/.conda/envs/qwen_opus/lib/python3.12/site-packages/starlette/_exception_handler.py", line 53, in w
rapped_app                                                                                                                                                
(APIServer pid=1652600)     raise exc                                                                                                                     
(APIServer pid=1652600)   File "/mnt/data/home/xx/.conda/envs/qwen_opus/lib/python3.12/site-packages/starlette/_exception_handler.py", line 42, in w
rapped_app                                                                                                                                                
(APIServer pid=1652600)     await app(scope, receive, sender)                                                                                             
(APIServer pid=1652600)   File "/mnt/data/home/xx/.conda/envs/qwen_opus/lib/python3.12/site-packages/fastapi/routing.py", line 120, in app          
(APIServer pid=1652600)     response = await f(request)                                                                                                   (APIServer pid=1652600)                ^^^^^^^^^^^^^^^^                                                                                                   
(APIServer pid=1652600)   File "/mnt/data/home/xx/.conda/envs/qwen_opus/lib/python3.12/site-packages/fastapi/routing.py", line 674, in app          
(APIServer pid=1652600)     raw_response = await run_endpoint_function(                                                                                   
(APIServer pid=1652600)                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^                                                                                   
(APIServer pid=1652600)   File "/mnt/data/home/xx/.conda/envs/qwen_opus/lib/python3.12/site-packages/fastapi/routing.py", line 328, in run_endpoint_
function                                                                                                                                                  
(APIServer pid=1652600)     return await dependant.call(**values)                                                                                         (APIServer pid=1652600)            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^                                                                                         
(APIServer pid=1652600)   File "/mnt/data/home/xx/.conda/envs/qwen_opus/lib/python3.12/site-packages/vllm/entrypoints/utils.py", line 95, in wrapper
(APIServer pid=1652600)     return handler_task.result()                                                                                                  
(APIServer pid=1652600)            ^^^^^^^^^^^^^^^^^^^^^                                                                                                  (APIServer pid=1652600)   File "/mnt/data/home/xx/.conda/envs/qwen_opus/lib/python3.12/site-packages/vllm/entrypoints/utils.py", line 116, in wrappe
r                                                                                                                                                         
(APIServer pid=1652600)     return await func(*args, **kwargs)                                                                                            
(APIServer pid=1652600)            ^^^^^^^^^^^^^^^^^^^^^^^^^^^                                                                                            (APIServer pid=1652600)   File "/mnt/data/home/xx/.conda/envs/qwen_opus/lib/python3.12/site-packages/vllm/entrypoints/openai/responses/api_router.py
", line 65, in create_responses                                                                                                                           
(APIServer pid=1652600)     generator = await handler.create_responses(request, raw_request)                                                              
(APIServer pid=1652600)                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^                                                              (APIServer pid=1652600)   File "/mnt/data/home/xx/.conda/envs/qwen_opus/lib/python3.12/site-packages/vllm/entrypoints/openai/responses/serving.py", 
line 384, in create_responses                                                                                                                             
(APIServer pid=1652600)     messages, engine_inputs = await self._make_request(request, prev_response)                                                    
(APIServer pid=1652600)                               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^                                                    (APIServer pid=1652600)   File "/mnt/data/home/xx/.conda/envs/qwen_opus/lib/python3.12/site-packages/vllm/entrypoints/openai/responses/serving.py", 
line 593, in _make_request                                                                                                                                
(APIServer pid=1652600)     messages = construct_input_messages(                                                                                          
(APIServer pid=1652600)                ^^^^^^^^^^^^^^^^^^^^^^^^^                                                                                          (APIServer pid=1652600)   File "/mnt/data/home/xx/.conda/envs/qwen_opus/lib/python3.12/site-packages/vllm/entrypoints/openai/responses/utils.py", li
ne 119, in construct_input_messages                                                                                                                       
(APIServer pid=1652600)     input_messages = construct_chat_messages_with_tool_call(request_input)                                                        
(APIServer pid=1652600)                      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^                                                        (APIServer pid=1652600)   File "/mnt/data/home/xx/.conda/envs/qwen_opus/lib/python3.12/site-packages/vllm/entrypoints/openai/responses/utils.py", li
ne 172, in construct_chat_messages_with_tool_call                                                                                                         
(APIServer pid=1652600)     messages.append(_construct_single_message_from_response_item(item))                                                           
(APIServer pid=1652600)                     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^                                                            
(APIServer pid=1652600)   File "/mnt/data/home/xx/.conda/envs/qwen_opus/lib/python3.12/site-packages/vllm/entrypoints/openai/responses/utils.py", line 216, in _construct_single_message_from_response_item                                                                                                   
(APIServer pid=1652600)     "content": item.content[0].text,                                                                                              
(APIServer pid=1652600)                ~~~~~~~~~~~~^^^                                                                                                    
(APIServer pid=1652600) IndexError: list index out of range          

after that, all ask will be failed, qwen always log indexError.

please help me to deal it. thanks.

@mergify

mergify Bot commented May 23, 2026

Copy link
Copy Markdown
Contributor

This pull request has merge conflicts that must be resolved before it can be
merged. Please rebase the PR, @anencore94.

https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/syncing-a-fork

@mergify mergify Bot added the needs-rebase label May 23, 2026
@anencore94

Copy link
Copy Markdown
Contributor Author

Resolved merge conflicts with the latest main (typed-object materialization for input items overlapped with this PR's _normalize_openresponses_input_item normalization helper) and also addressed @glistering96's review comment: _construct_single_message_from_response_item() no longer crashes with IndexError when an assistant turn produces only a tool call (ResponseOutputMessage with content=[]). Added a unit test for the empty-content case.

PR-targeted local tests all green (93 passed):

  • tests/entrypoints/openai/responses/test_responses_utils.py
  • tests/entrypoints/openai/responses/test_function_call_parsing.py
  • tests/entrypoints/openai/responses/test_harmony_utils.py
  • tests/entrypoints/openai/responses/test_sampling_params.py

Pre-commit hooks (ruff-check, ruff-format, mypy, etc.) all pass on changed files.

PTAL when you have a chance @chaunceyjiang @qandrew @DarkLight1337. Thanks!

@anencore94

Copy link
Copy Markdown
Contributor Author

@TheDuyIT good catch, and apologies for the very slow reply — you were right. The dict-form type == "reasoning" and role == "assistant" branches should follow the same previous-assistant-message merge policy as the model-form branches; otherwise a dict-form reasoning item followed by an assistant message renders as two consecutive assistant turns instead of one merged turn. That's now aligned in the latest push: both dict-form branches merge into the preceding assistant message under the same contract as their model-form counterparts.

Also rebased onto the latest main. The branch had fallen a long way behind, so it's squashed into a single commit to keep the review readable. Two upstream behaviours I deliberately preserved while resolving the conflicts, for transparency:

  • [Bugfix] Responses API assistant EasyInputMessageParam input #44361's guard that string-content assistant messages stay EasyInputMessageParam dicts rather than being coerced, so no id/status get injected into those.
  • Upstream's choice to append the original, un-normalized item on the ValidationError fallback path (this PR previously appended the normalized dict there) — the newer behaviour gives better Pydantic error reporting.

@chaunceyjiang this should also settle your earlier question: the PR still routes everything through input_item_parsing, it just extracts the per-item defaulting into a helper rather than adding a parallel path.

I'll re-push shortly with the sign-off trailer restored on the squashed commit (DCO is red purely because of that, not the content). PTAL when either of you has a chance — this was blocking Responses-API adoption for at least one other team in this thread.

…n SDK requirements

Requests that replay a prior OpenResponses turn as input fail validation
because items produced by the SDK omit fields that vLLM's models require:
assistant messages arrive without `id`/`status`, reasoning items without a
`summary` default, and `output_text` parts without `annotations`. The result
is a ValidationError on otherwise well-formed conversation history.

Extract the per-item defaulting into a `_normalize_openresponses_input_item`
helper and route `input_item_parsing` through it, so the entry point stays the
same. Also handle the dict-form `reasoning` and `assistant` items that the
TypedDict union parsing can produce (materializing Pydantic
`ValidatorIterator` fields before indexing), and stop
`_construct_message_from_response_item` raising `IndexError` when an assistant
turn carries no content because it only emitted a tool call.

The dict-form branches follow the same previous-assistant-message merge
contract as their model-form counterparts, so a reasoning item followed by an
assistant message merges into one turn rather than emitting two consecutive
assistant turns.

`type` is optional in the OpenResponses schema, so the helper stamps back the
type it keyed off before filling defaults. Without that, a typeless assistant
item gains `id`/`status` and then matches `ItemReference` in the input union,
collapsing to `{"id": ...}` and silently dropping the assistant turn — where
unpatched vLLM had rejected the same payload outright.

Signed-off-by: Jaeyeon Kim <anencore94@gmail.com>
@anencore94
anencore94 force-pushed the fix/responses-openresponses-input-normalization branch from 01ca842 to 17660e9 Compare September 7, 2026 16:10
@anencore94

Copy link
Copy Markdown
Contributor Author

Rebased onto current main (the branch had gone conflicting again) and, while re-verifying, found and fixed a real defect in this PR that I want to flag rather than bury in the diff.

type was not being stamped back onto the normalized item. The helper keys off item.get("type", "message"), so a typeless assistant item — {"role": "assistant", "content": [{"type": "output_text", "text": "hi"}]}, which is exactly the OpenResponses shape this PR exists to accept — got id and status filled in but no type. With those fields present and no discriminator, the dict then matches ItemReference in the ResponseInputItemParam | ResponseOutputItem union and collapses to {"id": "msg_..."}, dropping role, content and status entirely.

That is worse than the bug this PR fixes. Unpatched main rejects that payload with a ValidationError; the PR as it stood accepted it and silently deleted the assistant turn from the conversation, surfacing downstream only as a misleading Unknown input type: None. Reproduced directly against the union from protocol.py.

The fix is one line — stamp back the type the helper already decided — plus four regression tests in tests/entrypoints/openai/responses/test_protocol.py covering the typeless assistant item end to end, explicit reasoning items, and non-assistant items being left untouched (there was previously no coverage of input_item_parsing at all).

I also corrected the commit message, which named _construct_single_message_from_response_item; the actual function is _construct_message_from_response_item.

@chaunceyjiang @TheDuyIT — no change to the behaviour you each reviewed; this only stops the normalization path from silently discarding a turn. ruff check / ruff format --check clean on all 8 files.

@coderabbitai

coderabbitai Bot commented Sep 7, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

No actionable comments were generated in the recent review.

Recent review info
Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 02edd519-5217-4f1b-99b5-0c5d42592312
Commits

Reviewing files that changed from the base of the PR and between d0d681e and 35b420c.

Files selected for processing: 8
  • tests/entrypoints/openai/parser/test_harmony_utils.py
  • tests/entrypoints/openai/responses/test_function_call_parsing.py
  • tests/entrypoints/openai/responses/test_protocol.py
  • tests/entrypoints/openai/responses/test_responses_utils.py
  • tests/entrypoints/openai/responses/test_simple.py
  • vllm/entrypoints/openai/responses/harmony.py
  • vllm/entrypoints/openai/responses/protocol.py
  • vllm/entrypoints/openai/responses/utils.py

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 8 remain after this review.

Walkthrough

The change normalizes incomplete OpenResponses assistant and reasoning items, accepts legacy message-shaped inputs, preserves empty reasoning messages, and converts empty assistant content to an empty string. Tests cover protocol parsing, metadata defaults and preservation, utility conversion, and history replay.

Possibly related issues

  • vllm-project/vllm#63310 — The change adds a discriminator for assistant history items without type, matching the issue’s reported rejection of those items.

Change: Bug fix

Merge Risk: Minimal · up to 35b42

The two reported reasoning-history failures are not reachable through validated requests. No actionable merge-blocking risk remains after normal checks.

Pre-merge checks: Passed 4
Check name Status Explanation
Title check Passed The title names the main change: normalizing OpenResponses input items so incomplete assistant and reasoning history is accepted. The objectives describe the same goal, including the reuse of input_it…
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.
Description check Passed The pull request description directly explains the Responses API history normalization changes, empty assistant content handling, related validation behavior, tests, and known testing limits.
Full details: Title check

Explanation

The title names the main change: normalizing OpenResponses input items so incomplete assistant and reasoning history is accepted. The objectives describe the same goal, including the reuse of input_item_parsing and the dict and typed-item history handling in the diff. The title is long at 87 characters, but it is specific and on topic.

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

Assisted-by: OpenAI Codex
Signed-off-by: Jaeyeon Kim <anencore94@gmail.com>
@anencore94
anencore94 requested a review from sfeng33 as a code owner October 26, 2026 08:25
@anencore94

Copy link
Copy Markdown
Contributor Author

/ci run

@github-actions

Copy link
Copy Markdown

✅ Triggered Buildkite CI #97292 for commit 35b420c77c58.

ℹ️ This PR is 17 commits behind upstream main. Merged upstream main into this branch as 35b420c77c58 before starting CI. Pull the branch before pushing more changes.

@anencore94

Copy link
Copy Markdown
Contributor Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor
✅ Action performed

Full review finished.

@anencore94

Copy link
Copy Markdown
Contributor Author

@chaunceyjiang could you re-review commit 35b420c77c58? Buildkite #97292 has completed successfully, with 69 passing GitHub checks and one skipped. The CPU regression suite passed 182 tests.

The full CodeRabbit review on this head found no actionable correctness issues; it still reports a docstring-coverage warning (55.56% against its 80% threshold). The implementation reuses input_item_parsing as requested and addresses the empty assistant-content and dict/model history-merging cases. Please let me know if anything further is needed for approval.

@chaunceyjiang chaunceyjiang self-assigned this Oct 28, 2026

@chaunceyjiang chaunceyjiang left a comment

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.

Thanks! I'll take a look at this PR in a couple of days.

@anencore94

Copy link
Copy Markdown
Contributor Author

@chaunceyjiang following up on 35b420c77c58. The revision reuses input_item_parsing as you suggested and covers the empty assistant-content and dict/model history cases. Buildkite #97292 passed, the current rollup has 69 successful checks and three skipped, and the full CodeRabbit review found no actionable correctness issues. Its docstring-coverage warning remains documented

Could you take another look and, if this resolves your concerns, approve the revision for merge? The 182 local CPU tests pass. Local history-replay E2E coverage remains unrun, as recorded in the PR body

@chaunceyjiang chaunceyjiang left a comment

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.

make sense.

Thanks~

@chaunceyjiang
chaunceyjiang merged commit 945d0a2 into vllm-project:main Nov 2, 2026
73 checks passed
@anencore94
anencore94 deleted the fix/responses-openresponses-input-normalization branch November 2, 2026 12:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

frontend ready ONLY add when PR is ready to merge/full CI is needed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants