diff --git a/tests/tool_use/test_responses_request_validations.py b/tests/tool_use/test_responses_request_validations.py index 4952ea964eb8..ef2f56df4a34 100644 --- a/tests/tool_use/test_responses_request_validations.py +++ b/tests/tool_use/test_responses_request_validations.py @@ -1,6 +1,8 @@ # SPDX-License-Identifier: Apache-2.0 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project +from typing import Any, cast + import pytest from vllm.entrypoints.openai.responses.protocol import ( @@ -246,3 +248,54 @@ def test_responses_request_parallel_tool_calls_null_accepted(): {"input": "Hello", "model": "test-model", "parallel_tool_calls": None} ) assert req.parallel_tool_calls is None + + +@pytest.mark.parametrize("detail", [None, "low", "auto", "high"]) +def test_function_output_image_detail_defaults_or_preserves(detail): + image = {"type": "input_image", "image_url": "https://example.com/image.png"} + if detail is not None: + image["detail"] = detail + text = {"type": "input_text", "text": "Tool image"} + item = { + "type": "function_call_output", + "call_id": "call_1", + "output": [text, image], + } + request = ResponsesRequest.model_validate({"input": [item]}) + parsed = cast(list[dict[str, Any]], request.input) + output = parsed[0]["output"] + assert output[0] == text + actual = output[1] + assert actual["detail"] == ("auto" if detail is None else detail) + assert ("detail" in image) == (detail is not None) + + +def test_function_output_string_preserved(): + item = {"type": "function_call_output", "call_id": "call_1", "output": "result"} + request = ResponsesRequest.model_validate({"input": [item]}) + parsed = cast(list[dict[str, Any]], request.input) + assert parsed[0]["output"] == "result" + + +def test_message_and_multiple_function_output_images_default_together(): + image = {"type": "input_image", "image_url": "https://example.com/image.png"} + items = [ + {"role": "user", "content": [image]}, + {"type": "function_call_output", "call_id": "call_1", "output": [image, image]}, + ] + request = ResponsesRequest.model_validate({"input": items}) + parsed = cast(list[dict[str, Any]], request.input) + assert parsed[0]["content"][0]["detail"] == "auto" + assert [part["detail"] for part in parsed[1]["output"]] == ["auto", "auto"] + assert "detail" not in image + + +def test_function_output_invalid_explicit_image_detail_rejected(): + image = { + "type": "input_image", + "image_url": "https://example.com/image.png", + "detail": "invalid", + } + item = {"type": "function_call_output", "call_id": "call_1", "output": [image]} + with pytest.raises(ValueError, match="detail"): + ResponsesRequest.model_validate({"input": [item]}) diff --git a/vllm/entrypoints/openai/responses/protocol.py b/vllm/entrypoints/openai/responses/protocol.py index 200054a4ef59..6f996dbca6f7 100644 --- a/vllm/entrypoints/openai/responses/protocol.py +++ b/vllm/entrypoints/openai/responses/protocol.py @@ -141,7 +141,8 @@ def _default_input_image_details(value: Any) -> Any: if not isinstance(value, dict): return value - content = value.get("content") + field = "output" if value.get("type") == "function_call_output" else "content" + content = value.get(field) if not isinstance(content, list): return value @@ -158,7 +159,7 @@ def _default_input_image_details(value: Any) -> Any: changed = True new_content.append(new_part) - return {**value, "content": new_content} if changed else value + return {**value, field: new_content} if changed else value def _tool_field(tool: Any, field: str) -> Any: