diff --git a/tests/tool_use/test_responses_request_validations.py b/tests/tool_use/test_responses_request_validations.py index b50482960c..caeb7042d6 100644 --- a/tests/tool_use/test_responses_request_validations.py +++ b/tests/tool_use/test_responses_request_validations.py @@ -187,6 +187,27 @@ def test_responses_request_empty_tools_named_tool_choice(): ) +@pytest.mark.parametrize("detail", [None, "low"], ids=["default", "explicit"]) +def test_input_image_detail_defaults_or_preserves(detail: str | None) -> None: + image = { + "type": "input_image", + "image_url": "https://example.com/image.png", + } + if detail is not None: + image["detail"] = detail + + item = { + "role": "user", + "content": [image], + } + + request = ResponsesRequest.model_validate({"input": [item]}) + + assert request.input[0]["content"][0]["detail"] == ( + "auto" if detail is None else detail + ) + + # Regression tests for parallel_tool_calls=null crash in Responses API # (from_request() passed None to ResponsesResponse.parallel_tool_calls, # a non-optional bool field, causing a Pydantic ValidationError during diff --git a/vllm/entrypoints/openai/responses/protocol.py b/vllm/entrypoints/openai/responses/protocol.py index 750d1db84c..7e286aa782 100644 --- a/vllm/entrypoints/openai/responses/protocol.py +++ b/vllm/entrypoints/openai/responses/protocol.py @@ -4,7 +4,7 @@ # Adapted from # https://github.com/lm-sys/FastChat/blob/168ccc29d3f7edc50823016105c024fe2282732a/fastchat/protocol/openai_api_protocol.py import time -from typing import Any, Literal, TypeAlias +from typing import Annotated, Any, Literal, TypeAlias from openai.types.responses import ( ResponseCodeInterpreterCallCodeDeltaEvent, @@ -49,6 +49,7 @@ from openai.types.responses.tool import Tool from openai.types.shared import Metadata, Reasoning from openai_harmony import Message as OpenAIHarmonyMessage from pydantic import ( + BeforeValidator, Field, ValidationError, field_serializer, @@ -135,6 +136,31 @@ ResponseInputOutputMessage: TypeAlias = ( ResponseInputOutputItem: TypeAlias = ResponseInputItemParam | ResponseOutputItem +def _default_input_image_details(value: Any) -> Any: + """Set the API default for input images before SDK type validation.""" + if not isinstance(value, dict): + return value + + content = value.get("content") + if not isinstance(content, list): + return value + + new_content = [] + changed = False + for part in content: + new_part = part + if ( + isinstance(part, dict) + and part.get("type") == "input_image" + and "detail" not in part + ): + new_part = {**part, "detail": "auto"} + changed = True + new_content.append(new_part) + + return {**value, "content": new_content} if changed else value + + class ResponsesRequest(OpenAIBaseModel): # Ordered by official OpenAI API documentation # https://platform.openai.com/docs/api-reference/responses/create @@ -152,7 +178,15 @@ class ResponsesRequest(OpenAIBaseModel): ] | None ) = None - input: str | list[ResponseInputOutputItem] + input: ( + str + | list[ + Annotated[ + ResponseInputOutputItem, + BeforeValidator(_default_input_image_details), + ] + ] + ) instructions: str | None = None max_output_tokens: int | None = None max_tool_calls: int | None = None