Python: Support detail field in OpenAI Chat API image_url payload (#4756)

* Support detail field in OpenAI image_url payload (#4616)

Include the optional 'detail' field from Content.additional_properties
when building image_url payloads for the OpenAI Chat API, matching the
existing pattern used for 'filename' in document file payloads.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Apply pre-commit auto-fixes

* Remove reproduction report

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Simplify detail extraction from additional_properties (#4616)

- Remove unnecessary hasattr check; additional_properties is always
  initialized as a dict on Content instances.
- Use 'is not None' instead of truthy check to be more precise.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Apply pre-commit auto-fixes

* Remove detail allowlist in chat client to align with responses client

Replace the strict allowlist check ('low', 'high', 'auto') with an
isinstance(detail, str) check so that any valid string detail value is
passed through to OpenAI. This aligns the chat client behavior with the
responses client, which passes detail through unconditionally.

Also add test coverage for:
- Future/unknown string detail values being passed through
- Data URI images (covering the 'data' branch of the match)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Evan Mattson
2026-03-20 09:33:57 +09:00
committed by GitHub
Unverified
parent 4c287c2424
commit 47ead84753
2 changed files with 98 additions and 1 deletions
@@ -713,9 +713,13 @@ class RawOpenAIChatClient( # type: ignore[misc]
"content": content.result if content.result is not None else "",
}
case "data" | "uri" if content.has_top_level_media_type("image"):
image_url_obj: dict[str, Any] = {"url": content.uri}
detail = content.additional_properties.get("detail")
if isinstance(detail, str):
image_url_obj["detail"] = detail
return {
"type": "image_url",
"image_url": {"url": content.uri},
"image_url": image_url_obj,
}
case "data" | "uri" if content.has_top_level_media_type("audio"):
if content.media_type and "wav" in content.media_type:
@@ -462,6 +462,99 @@ def test_prepare_content_for_openai_data_content_image(
assert result["input_audio"]["format"] == "mp3"
def test_prepare_content_for_openai_image_url_detail(
openai_unit_test_env: dict[str, str],
) -> None:
"""Test _prepare_content_for_openai includes the detail field in image_url when specified."""
client = OpenAIChatClient()
# Test image with detail set to "high"
image_with_detail = Content.from_uri(
uri="https://example.com/image.png",
media_type="image/png",
additional_properties={"detail": "high"},
)
result = client._prepare_content_for_openai(image_with_detail) # type: ignore
assert result["type"] == "image_url"
assert result["image_url"]["url"] == "https://example.com/image.png"
assert result["image_url"]["detail"] == "high"
# Test image with detail set to "low"
image_low_detail = Content.from_uri(
uri="https://example.com/image.png",
media_type="image/png",
additional_properties={"detail": "low"},
)
result = client._prepare_content_for_openai(image_low_detail) # type: ignore
assert result["image_url"]["detail"] == "low"
# Test image with detail set to "auto"
image_auto_detail = Content.from_uri(
uri="https://example.com/image.png",
media_type="image/png",
additional_properties={"detail": "auto"},
)
result = client._prepare_content_for_openai(image_auto_detail) # type: ignore
assert result["image_url"]["detail"] == "auto"
# Test image without detail should not include it
image_no_detail = Content.from_uri(
uri="https://example.com/image.png",
media_type="image/png",
)
result = client._prepare_content_for_openai(image_no_detail) # type: ignore
assert result["type"] == "image_url"
assert result["image_url"]["url"] == "https://example.com/image.png"
assert "detail" not in result["image_url"]
# Test image with a future/unknown string detail value should pass it through
image_future_detail = Content.from_uri(
uri="https://example.com/image.png",
media_type="image/png",
additional_properties={"detail": "ultra"},
)
result = client._prepare_content_for_openai(image_future_detail) # type: ignore
assert result["type"] == "image_url"
assert result["image_url"]["url"] == "https://example.com/image.png"
assert result["image_url"]["detail"] == "ultra"
# Test image with data URI should include detail
image_data_uri = Content.from_uri(
uri="data:image/png;base64,iVBORw0KGgo",
media_type="image/png",
additional_properties={"detail": "high"},
)
result = client._prepare_content_for_openai(image_data_uri) # type: ignore
assert result["type"] == "image_url"
assert result["image_url"]["url"] == "data:image/png;base64,iVBORw0KGgo"
assert result["image_url"]["detail"] == "high"
# Test image with non-string detail value should not include it
image_non_string_detail = Content.from_uri(
uri="https://example.com/image.png",
media_type="image/png",
additional_properties={"detail": 123},
)
result = client._prepare_content_for_openai(image_non_string_detail) # type: ignore
assert result["type"] == "image_url"
assert result["image_url"]["url"] == "https://example.com/image.png"
assert "detail" not in result["image_url"]
def test_prepare_content_for_openai_document_file_mapping(
openai_unit_test_env: dict[str, str],
) -> None: