Python: Add PDF file support to OpenAI content parser with filename mapping (#1121)

* Add mapping for application media type in OpenAI responses client

* Enhance multimodal input samples: Add PDF testing functionality and fix image sample

* Standardize filename handling and add multimodal samples

- Standardized filename extraction logic between chat and responses clients
- Both clients now omit filename when not provided (no default fallback)
- Added Azure Responses API multimodal sample with PDF support
- Cleaned up Azure Chat sample to focus on supported features only
- Fixed test comment placement for better code documentation
- Updated README with clear API capability differences

* Enhance multimodal input samples with image and PDF handling

- Refactor image and PDF handling in `azure_chat_multimodal.py` and `openai_chat_multimodal.py` to use new utility functions.
- Add `load_sample_pdf` and `create_sample_image` functions for better test asset management.
- Remove redundant code for creating sample images and PDFs.
- Introduce a sample PDF file in `sample_assets` for testing purposes.

* Fix formatting in OpenAI chat client

---------

Co-authored-by: Dmytro Struk <13853051+dmytrostruk@users.noreply.github.com>
This commit is contained in:
Christian Glessner
2025-10-07 19:04:33 +02:00
committed by GitHub
Unverified
parent 1d27b57672
commit 8bb1266020
8 changed files with 268 additions and 77 deletions
@@ -419,27 +419,24 @@ class OpenAIBaseChatClient(OpenAIBase, BaseChatClient):
"format": audio_format,
},
}
case DataContent() | UriContent() if content.media_type and content.media_type.startswith("application/"):
if content.media_type == "application/pdf":
if content.uri.startswith("data:"):
filename = (
getattr(content, "filename", None)
or content.additional_properties.get("filename", "document.pdf")
if hasattr(content, "additional_properties") and content.additional_properties
else "document.pdf"
)
return {
"type": "file",
"file": {
"file_data": content.uri, # Send full data URI
"filename": filename,
},
}
return content.to_dict(exclude_none=True)
return content.to_dict(exclude_none=True)
case DataContent() | UriContent() if content.has_top_level_media_type(
"application"
) and content.uri.startswith("data:"):
# All application/* media types should be treated as files for OpenAI
filename = getattr(content, "filename", None) or (
content.additional_properties.get("filename")
if hasattr(content, "additional_properties") and content.additional_properties
else None
)
file_obj = {"file_data": content.uri}
if filename:
file_obj["filename"] = filename
return {
"type": "file",
"file": file_obj,
}
case _:
# Default fallback for all other content types
return content.to_dict(exclude_none=True)
@override
@@ -454,6 +454,19 @@ class OpenAIBaseResponsesClient(OpenAIBase, BaseChatClient):
"format": format,
},
}
if content.has_top_level_media_type("application"):
filename = getattr(content, "filename", None) or (
content.additional_properties.get("filename")
if hasattr(content, "additional_properties") and content.additional_properties
else None
)
file_obj = {
"type": "input_file",
"file_data": content.uri,
}
if filename:
file_obj["filename"] = filename
return file_obj
return {}
case FunctionCallContent():
return {
@@ -756,7 +756,12 @@ def test_openai_content_parser_data_content_image(openai_unit_test_env: dict[str
assert result["input_audio"]["data"] == "//uQAAAAWGluZwAAAA8AAAACAAACcQ=="
assert result["input_audio"]["format"] == "mp3"
# Test DataContent with PDF file
def test_openai_content_parser_document_file_mapping(openai_unit_test_env: dict[str, str]) -> None:
"""Test _openai_content_parser converts document files (PDF, DOCX, etc.) to OpenAI file format."""
client = OpenAIChatClient()
# Test PDF without filename - should omit filename in OpenAI payload
pdf_data_content = DataContent(
uri="data:application/pdf;base64,JVBERi0xLjQKJcfsj6IKNSAwIG9iago8PC9UeXBlL0NhdGFsb2cvUGFnZXMgMiAwIFI+PgplbmRvYmoKMiAwIG9iago8PC9UeXBlL1BhZ2VzL0tpZHNbMyAwIFJdL0NvdW50IDE+PgplbmRvYmoKMyAwIG9iago8PC9UeXBlL1BhZ2UvTWVkaWFCb3ggWzAgMCA2MTIgNzkyXS9QYXJlbnQgMiAwIFIvUmVzb3VyY2VzPDwvRm9udDw8L0YxIDQgMCBSPj4+Pi9Db250ZW50cyA1IDAgUj4+CmVuZG9iago0IDAgb2JqCjw8L1R5cGUvRm9udC9TdWJ0eXBlL1R5cGUxL0Jhc2VGb250L0hlbHZldGljYT4+CmVuZG9iago1IDAgb2JqCjw8L0xlbmd0aCA0ND4+CnN0cmVhbQpCVApxCjcwIDUwIFRECi9GMSA4IFRmCihIZWxsbyBXb3JsZCEpIFRqCkVUCmVuZHN0cmVhbQplbmRvYmoKeHJlZgowIDYKMDAwMDAwMDAwMCA2NTUzNSBmIAowMDAwMDAwMDA5IDAwMDAwIG4gCjAwMDAwMDAwNTggMDAwMDAgbiAKMDAwMDAwMDExNSAwMDAwMCBuIAowMDAwMDAwMjQ1IDAwMDAwIG4gCjAwMDAwMDAzMDcgMDAwMDAgbiAKdHJhaWxlcgo8PC9TaXplIDYvUm9vdCAxIDAgUj4+CnN0YXJ0eHJlZgo0MDUKJSVFT0Y=",
media_type="application/pdf",
@@ -764,14 +769,15 @@ def test_openai_content_parser_data_content_image(openai_unit_test_env: dict[str
result = client._openai_content_parser(pdf_data_content) # type: ignore
# Should convert to OpenAI file format
# Should convert to OpenAI file format without filename
assert result["type"] == "file"
assert result["file"]["filename"] == "document.pdf"
assert "filename" not in result["file"] # No filename provided, so none should be set
assert "file_data" in result["file"]
# Base64 data should be the full data URI (OpenAI requirement)
assert result["file"]["file_data"].startswith("data:application/pdf;base64,")
assert result["file"]["file_data"] == pdf_data_content.uri
# Test DataContent with PDF and custom filename
# Test PDF with custom filename via additional_properties
pdf_with_filename = DataContent(
uri="data:application/pdf;base64,JVBERi0xLjQ=",
media_type="application/pdf",
@@ -783,17 +789,75 @@ def test_openai_content_parser_data_content_image(openai_unit_test_env: dict[str
# Should use custom filename
assert result["type"] == "file"
assert result["file"]["filename"] == "report.pdf"
assert result["file"]["file_data"] == "data:application/pdf;base64,JVBERi0xLjQ="
# Test different application/* media types - all should now be mapped to file format
test_cases = [
{
"media_type": "application/json",
"filename": "data.json",
"base64": "eyJrZXkiOiJ2YWx1ZSJ9",
},
{
"media_type": "application/xml",
"filename": "config.xml",
"base64": "PD94bWwgdmVyc2lvbj0iMS4wIj8+",
},
{
"media_type": "application/octet-stream",
"filename": "binary.bin",
"base64": "AQIDBAUGBwgJCg==",
},
]
def test_openai_chat_client_with_callable_api_key() -> None:
"""Test OpenAIChatClient initialization with callable API key."""
for case in test_cases:
# Test without filename
doc_content = DataContent(
uri=f"data:{case['media_type']};base64,{case['base64']}",
media_type=case["media_type"],
)
async def get_api_key() -> str:
return "test-api-key-123"
result = client._openai_content_parser(doc_content) # type: ignore
client = OpenAIChatClient(model_id="gpt-4o", api_key=get_api_key)
# All application/* types should now be mapped to file format
assert result["type"] == "file"
assert "filename" not in result["file"] # Should omit filename when not provided
assert result["file"]["file_data"] == doc_content.uri
# Verify client was created successfully
assert client.model_id == "gpt-4o"
# OpenAI SDK now manages callable API keys internally
assert client.client is not None
# Test with filename - should now use file format with filename
doc_with_filename = DataContent(
uri=f"data:{case['media_type']};base64,{case['base64']}",
media_type=case["media_type"],
additional_properties={"filename": case["filename"]},
)
result = client._openai_content_parser(doc_with_filename) # type: ignore
# Should now use file format with filename
assert result["type"] == "file"
assert result["file"]["filename"] == case["filename"]
assert result["file"]["file_data"] == doc_with_filename.uri
# Test edge case: empty additional_properties dict
pdf_empty_props = DataContent(
uri="data:application/pdf;base64,JVBERi0xLjQ=",
media_type="application/pdf",
additional_properties={},
)
result = client._openai_content_parser(pdf_empty_props) # type: ignore
assert result["type"] == "file"
assert "filename" not in result["file"]
# Test edge case: None filename in additional_properties
pdf_none_filename = DataContent(
uri="data:application/pdf;base64,JVBERi0xLjQ=",
media_type="application/pdf",
additional_properties={"filename": None},
)
result = client._openai_content_parser(pdf_none_filename) # type: ignore
assert result["type"] == "file"
assert "filename" not in result["file"] # None filename should be omitted