mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-26 01:12:21 +00:00
fix: raise BadRequestError for file content blocks missing 'file' sub… (#24503)
Squash-merged by litellm-agent from krisxia0506's PR.
This commit is contained in:
parent
b3de50d277
commit
1ba02c0163
6 changed files with 499 additions and 17 deletions
|
|
@ -20,6 +20,7 @@ from typing import (
|
|||
cast,
|
||||
)
|
||||
|
||||
import litellm
|
||||
from litellm import verbose_logger
|
||||
from litellm.router_utils.batch_utils import InMemoryFile
|
||||
from litellm.types.llms.openai import (
|
||||
|
|
@ -1170,9 +1171,16 @@ def migrate_file_to_image_url(
|
|||
ChatCompletionImageUrlObject,
|
||||
)
|
||||
|
||||
file_id = message["file"].get("file_id")
|
||||
file_data = message["file"].get("file_data")
|
||||
format = message["file"].get("format")
|
||||
file_sub = message.get("file")
|
||||
if file_sub is None:
|
||||
raise litellm.BadRequestError(
|
||||
message="Content block has type='file' but is missing the required 'file' field",
|
||||
model=None,
|
||||
llm_provider=None,
|
||||
)
|
||||
file_id = file_sub.get("file_id")
|
||||
file_data = file_sub.get("file_data")
|
||||
format = file_sub.get("format")
|
||||
if not file_id and not file_data:
|
||||
raise ValueError("file_id and file_data are both None")
|
||||
image_url_object = ChatCompletionImageObject(
|
||||
|
|
|
|||
|
|
@ -2057,9 +2057,16 @@ def anthropic_process_openai_file_message(
|
|||
AnthropicMessagesContainerUploadParam,
|
||||
]:
|
||||
file_message = cast(ChatCompletionFileObject, message)
|
||||
file_data = file_message["file"].get("file_data")
|
||||
file_id = file_message["file"].get("file_id")
|
||||
format = file_message["file"].get("format")
|
||||
file_sub = file_message.get("file")
|
||||
if file_sub is None:
|
||||
raise litellm.BadRequestError(
|
||||
message="Content block has type='file' but is missing the required 'file' field",
|
||||
model=None,
|
||||
llm_provider="anthropic",
|
||||
)
|
||||
file_data = file_sub.get("file_data")
|
||||
file_id = file_sub.get("file_id")
|
||||
format = file_sub.get("format")
|
||||
if file_data:
|
||||
image_chunk = convert_to_anthropic_image_obj(
|
||||
openai_image_url=file_data,
|
||||
|
|
@ -4879,7 +4886,13 @@ class BedrockConverseMessagesProcessor:
|
|||
|
||||
@staticmethod
|
||||
def _process_file_message(message: ChatCompletionFileObject) -> BedrockContentBlock:
|
||||
file_message = message["file"]
|
||||
file_message = message.get("file")
|
||||
if file_message is None:
|
||||
raise litellm.BadRequestError(
|
||||
message="Content block has type='file' but is missing the required 'file' field",
|
||||
model=None,
|
||||
llm_provider="bedrock",
|
||||
)
|
||||
file_data = file_message.get("file_data")
|
||||
file_id = file_message.get("file_id")
|
||||
|
||||
|
|
@ -4900,7 +4913,13 @@ class BedrockConverseMessagesProcessor:
|
|||
async def _async_process_file_message(
|
||||
message: ChatCompletionFileObject,
|
||||
) -> BedrockContentBlock:
|
||||
file_message = message["file"]
|
||||
file_message = message.get("file")
|
||||
if file_message is None:
|
||||
raise litellm.BadRequestError(
|
||||
message="Content block has type='file' but is missing the required 'file' field",
|
||||
model=None,
|
||||
llm_provider="bedrock",
|
||||
)
|
||||
file_data = file_message.get("file_data")
|
||||
file_id = file_message.get("file_id")
|
||||
format = file_message.get("format")
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
from typing import List, Optional, cast
|
||||
|
||||
import litellm
|
||||
|
||||
from litellm.litellm_core_utils.prompt_templates.factory import (
|
||||
convert_generic_image_chunk_to_openai_image_obj,
|
||||
convert_to_anthropic_image_obj,
|
||||
|
|
@ -141,13 +143,20 @@ class GoogleAIStudioGeminiConfig(VertexGeminiConfig):
|
|||
img_element["image_url"] = converted_image_url # type: ignore
|
||||
elif element.get("type") == "file":
|
||||
file_element = cast(ChatCompletionFileObject, element)
|
||||
file_id = file_element["file"].get("file_id")
|
||||
_file_field = file_element.get("file")
|
||||
if _file_field is None:
|
||||
raise litellm.BadRequestError(
|
||||
message="Content block has type='file' but is missing the required 'file' field",
|
||||
model=model,
|
||||
llm_provider="gemini",
|
||||
)
|
||||
file_id = _file_field.get("file_id")
|
||||
if file_id and ("http://" in file_id or "https://" in file_id):
|
||||
# Convert HTTP/HTTPS file URL to base64 data
|
||||
try:
|
||||
base64_data = convert_url_to_base64(file_id)
|
||||
file_element["file"]["file_data"] = base64_data # type: ignore
|
||||
file_element["file"].pop("file_id", None) # type: ignore
|
||||
_file_field["file_data"] = base64_data # type: ignore
|
||||
_file_field.pop("file_id", None) # type: ignore
|
||||
except Exception:
|
||||
# If conversion fails, leave as is and let the API handle it
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -287,7 +287,13 @@ class OpenAIGPTConfig(BaseLLMModelInfo, BaseConfig):
|
|||
content_item["image_url"] = new_image_url_obj
|
||||
elif content_item.get("type") == "file":
|
||||
content_item = cast(ChatCompletionFileObject, content_item)
|
||||
file_obj = content_item["file"]
|
||||
file_obj = content_item.get("file")
|
||||
if file_obj is None:
|
||||
raise litellm.BadRequestError(
|
||||
message="Content block has type='file' but is missing the required 'file' field",
|
||||
model=None,
|
||||
llm_provider="openai",
|
||||
)
|
||||
new_file_obj = ChatCompletionFileObjectFile(
|
||||
**{ # type: ignore
|
||||
k: v
|
||||
|
|
|
|||
|
|
@ -407,11 +407,18 @@ def _gemini_convert_messages_with_history( # noqa: PLR0915
|
|||
_parts.append(_part)
|
||||
elif element["type"] == "file":
|
||||
file_element = cast(ChatCompletionFileObject, element)
|
||||
file_id = file_element["file"].get("file_id")
|
||||
format = file_element["file"].get("format")
|
||||
file_data = file_element["file"].get("file_data")
|
||||
detail = file_element["file"].get("detail")
|
||||
video_metadata = file_element["file"].get("video_metadata")
|
||||
_file_field = file_element.get("file")
|
||||
if _file_field is None:
|
||||
raise litellm.BadRequestError(
|
||||
message="Content block has type='file' but is missing the required 'file' field",
|
||||
model=model,
|
||||
llm_provider="vertex_ai",
|
||||
)
|
||||
file_id = _file_field.get("file_id")
|
||||
format = _file_field.get("format")
|
||||
file_data = _file_field.get("file_data")
|
||||
detail = _file_field.get("detail")
|
||||
video_metadata = _file_field.get("video_metadata")
|
||||
passed_file = file_id or file_data
|
||||
if passed_file is None:
|
||||
raise Exception(
|
||||
|
|
|
|||
433
tests/test_litellm/llms/test_file_content_block.py
Normal file
433
tests/test_litellm/llms/test_file_content_block.py
Normal file
|
|
@ -0,0 +1,433 @@
|
|||
"""
|
||||
Tests for handling malformed or invalid 'file' content blocks (missing or null
|
||||
`file` sub-field, HTTP file_id URLs for Google AI Studio).
|
||||
|
||||
Regression tests for:
|
||||
- litellm/llms/vertex_ai/gemini/transformation.py
|
||||
- litellm/llms/gemini/chat/transformation.py
|
||||
- litellm/litellm_core_utils/prompt_templates/common_utils.py
|
||||
(migrate_file_to_image_url raises on missing `file`; file-id helpers skip non-OpenAI shapes)
|
||||
- litellm/litellm_core_utils/prompt_templates/factory.py (Bedrock + Anthropic)
|
||||
- litellm/llms/openai/chat/gpt_transformation.py
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import copy
|
||||
from typing import List, cast
|
||||
|
||||
import pytest
|
||||
|
||||
import litellm
|
||||
from litellm.litellm_core_utils.prompt_templates.common_utils import (
|
||||
get_file_ids_from_messages,
|
||||
migrate_file_to_image_url,
|
||||
update_messages_with_model_file_ids,
|
||||
)
|
||||
from litellm.litellm_core_utils.prompt_templates.factory import (
|
||||
BedrockConverseMessagesProcessor,
|
||||
anthropic_process_openai_file_message,
|
||||
)
|
||||
from litellm.llms.gemini.chat.transformation import GoogleAIStudioGeminiConfig
|
||||
from litellm.llms.openai.chat.gpt_transformation import OpenAIGPTConfig
|
||||
from litellm.llms.vertex_ai.gemini.transformation import (
|
||||
_gemini_convert_messages_with_history,
|
||||
)
|
||||
from litellm.types.llms.openai import (
|
||||
AllMessageValues,
|
||||
ChatCompletionFileObject,
|
||||
OpenAIMessageContentListBlock,
|
||||
)
|
||||
|
||||
_MALFORMED_MESSAGES_RAW = [
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{"type": "text", "text": "hello"},
|
||||
{"type": "file"}, # Missing required "file" sub-field
|
||||
],
|
||||
}
|
||||
]
|
||||
|
||||
_WELL_FORMED_MESSAGES_RAW = [
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{"type": "text", "text": "hello"},
|
||||
{
|
||||
"type": "file",
|
||||
"file": {"file_id": "file-abc123", "format": "pdf"},
|
||||
},
|
||||
],
|
||||
}
|
||||
]
|
||||
|
||||
MALFORMED_FILE_OBJECT: ChatCompletionFileObject = cast(
|
||||
ChatCompletionFileObject, {"type": "file"}
|
||||
)
|
||||
|
||||
EXPLICIT_NULL_FILE_OBJECT: ChatCompletionFileObject = cast(
|
||||
ChatCompletionFileObject,
|
||||
{"type": "file", "file": None},
|
||||
)
|
||||
|
||||
|
||||
def _malformed() -> List[AllMessageValues]:
|
||||
return copy.deepcopy(cast(List[AllMessageValues], _MALFORMED_MESSAGES_RAW))
|
||||
|
||||
|
||||
def _well_formed() -> List[AllMessageValues]:
|
||||
return copy.deepcopy(cast(List[AllMessageValues], _WELL_FORMED_MESSAGES_RAW))
|
||||
|
||||
|
||||
def _explicit_null_file_in_content() -> List[AllMessageValues]:
|
||||
return copy.deepcopy(
|
||||
cast(
|
||||
List[AllMessageValues],
|
||||
[
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{"type": "text", "text": "hello"},
|
||||
{"type": "file", "file": None},
|
||||
],
|
||||
}
|
||||
],
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# vertex_ai/gemini/transformation.py
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_gemini_convert_messages_malformed_file_raises_bad_request():
|
||||
"""_gemini_convert_messages_with_history should raise BadRequestError (not KeyError)
|
||||
when a content block has type='file' but no 'file' sub-field."""
|
||||
with pytest.raises(litellm.BadRequestError, match="missing the required 'file' field"):
|
||||
_gemini_convert_messages_with_history(
|
||||
messages=_malformed(),
|
||||
model="gemini-2.0-flash",
|
||||
)
|
||||
|
||||
|
||||
def test_gemini_convert_messages_explicit_null_file_field_raises_bad_request():
|
||||
"""Explicit JSON null for `file` must be rejected like a missing `file` key."""
|
||||
with pytest.raises(litellm.BadRequestError, match="missing the required 'file' field"):
|
||||
_gemini_convert_messages_with_history(
|
||||
messages=_explicit_null_file_in_content(),
|
||||
model="gemini-2.0-flash",
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# gemini/chat/transformation.py - GoogleAIStudioGeminiConfig
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_google_ai_studio_transform_messages_malformed_file_raises_bad_request():
|
||||
"""GoogleAIStudioGeminiConfig._transform_messages should raise BadRequestError
|
||||
when a content block has type='file' but no 'file' sub-field."""
|
||||
config = GoogleAIStudioGeminiConfig()
|
||||
with pytest.raises(litellm.BadRequestError, match="missing the required 'file' field"):
|
||||
config._transform_messages(messages=_malformed(), model="gemini-2.0-flash")
|
||||
|
||||
|
||||
def test_google_ai_studio_transform_messages_explicit_null_file_field_raises_bad_request():
|
||||
"""Explicit JSON null for `file` must be rejected like a missing `file` key."""
|
||||
config = GoogleAIStudioGeminiConfig()
|
||||
with pytest.raises(litellm.BadRequestError, match="missing the required 'file' field"):
|
||||
config._transform_messages(
|
||||
messages=_explicit_null_file_in_content(), model="gemini-2.0-flash"
|
||||
)
|
||||
|
||||
|
||||
def test_google_ai_studio_transform_messages_http_file_id_converts_to_base64(monkeypatch):
|
||||
"""Google AI Studio rejects raw HTTP(S) file URLs; _transform_messages should
|
||||
fetch and replace them with base64 `file_data` before conversion."""
|
||||
# Data URL shape so downstream Gemini media parsing accepts the inlined bytes
|
||||
# (mirrors real `convert_url_to_base64` output from `_process_image_response`).
|
||||
fake_file_data = "data:application/pdf;base64,aGVsbG8="
|
||||
|
||||
def _fake_convert_url_to_base64(url: str) -> str:
|
||||
assert url == "https://example.com/doc.pdf"
|
||||
return fake_file_data
|
||||
|
||||
monkeypatch.setattr(
|
||||
"litellm.llms.gemini.chat.transformation.convert_url_to_base64",
|
||||
_fake_convert_url_to_base64,
|
||||
)
|
||||
messages = cast(
|
||||
List[AllMessageValues],
|
||||
[
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{"type": "text", "text": "hello"},
|
||||
{
|
||||
"type": "file",
|
||||
"file": {
|
||||
"file_id": "https://example.com/doc.pdf",
|
||||
"format": "pdf",
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
],
|
||||
)
|
||||
config = GoogleAIStudioGeminiConfig()
|
||||
config._transform_messages(messages=messages, model="gemini-2.0-flash")
|
||||
content = messages[0].get("content")
|
||||
assert isinstance(content, list)
|
||||
file_block = next(c for c in content if isinstance(c, dict) and c.get("type") == "file")
|
||||
file_field = file_block.get("file")
|
||||
assert isinstance(file_field, dict)
|
||||
assert file_field.get("file_data") == fake_file_data
|
||||
assert "file_id" not in file_field
|
||||
|
||||
|
||||
def test_google_ai_studio_transform_messages_http_file_id_convert_failure_leaves_file_unchanged(
|
||||
monkeypatch,
|
||||
):
|
||||
"""If convert_url_to_base64 fails, the Studio prep step must not mutate the block
|
||||
(see try/except in GoogleAIStudioGeminiConfig._transform_messages)."""
|
||||
https_id = "https://example.com/missing.pdf"
|
||||
|
||||
def _raise(_url: str) -> str:
|
||||
raise litellm.ImageFetchError("simulated fetch failure")
|
||||
|
||||
monkeypatch.setattr(
|
||||
"litellm.llms.gemini.chat.transformation.convert_url_to_base64",
|
||||
_raise,
|
||||
)
|
||||
messages = cast(
|
||||
List[AllMessageValues],
|
||||
[
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{"type": "text", "text": "hello"},
|
||||
{
|
||||
"type": "file",
|
||||
"file": {
|
||||
"file_id": https_id,
|
||||
"format": "application/pdf",
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
],
|
||||
)
|
||||
config = GoogleAIStudioGeminiConfig()
|
||||
config._transform_messages(messages=messages, model="gemini-2.0-flash")
|
||||
content = messages[0].get("content")
|
||||
assert isinstance(content, list)
|
||||
file_block = next(c for c in content if isinstance(c, dict) and c.get("type") == "file")
|
||||
file_field = file_block.get("file")
|
||||
assert isinstance(file_field, dict)
|
||||
assert file_field.get("file_id") == https_id
|
||||
assert file_field.get("format") == "application/pdf"
|
||||
assert "file_data" not in file_field
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# common_utils.py - update_messages_with_model_file_ids
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_update_messages_with_model_file_ids_malformed_skips_non_openai_file_block():
|
||||
"""Non-OpenAI file blocks (e.g. missing nested `file` dict) are skipped so callers
|
||||
relying on LangChain v1 / provider-native shapes are not rejected here."""
|
||||
messages = _malformed()
|
||||
result = update_messages_with_model_file_ids(
|
||||
messages=messages,
|
||||
model_id="some-model",
|
||||
model_file_id_mapping={},
|
||||
)
|
||||
assert result == messages
|
||||
content = result[0].get("content")
|
||||
assert isinstance(content, list)
|
||||
file_block = next(c for c in content if isinstance(c, dict) and c.get("type") == "file")
|
||||
assert "file" not in file_block
|
||||
|
||||
|
||||
def test_update_messages_with_model_file_ids_well_formed_updates():
|
||||
"""update_messages_with_model_file_ids should update file_id for well-formed blocks."""
|
||||
mapping = {"file-abc123": {"some-model": "provider-file-xyz"}}
|
||||
result = update_messages_with_model_file_ids(
|
||||
messages=_well_formed(),
|
||||
model_id="some-model",
|
||||
model_file_id_mapping=mapping,
|
||||
)
|
||||
content = result[0].get("content")
|
||||
assert isinstance(content, list)
|
||||
file_block = next(c for c in content if c.get("type") == "file")
|
||||
assert file_block.get("file", {}).get("file_id") == "provider-file-xyz"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# common_utils.py - get_file_ids_from_messages
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_get_file_ids_from_messages_malformed_skips_non_openai_file_block():
|
||||
"""Blocks with type='file' but no OpenAI `file` sub-dict yield no extracted ids."""
|
||||
assert get_file_ids_from_messages(messages=_malformed()) == []
|
||||
|
||||
|
||||
def test_get_file_ids_from_messages_well_formed_returns_ids():
|
||||
"""get_file_ids_from_messages should extract file_id from well-formed blocks."""
|
||||
messages: List[AllMessageValues] = cast(
|
||||
List[AllMessageValues],
|
||||
[
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{"type": "text", "text": "hello"},
|
||||
{"type": "file", "file": {"file_id": "file-abc123", "format": "pdf"}},
|
||||
],
|
||||
}
|
||||
],
|
||||
)
|
||||
result = get_file_ids_from_messages(messages=messages)
|
||||
assert result == ["file-abc123"]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# factory.py - BedrockConverseMessagesProcessor (sync + async)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_bedrock_process_file_message_malformed_raises_bad_request():
|
||||
"""_process_file_message should raise BadRequestError (not KeyError)
|
||||
when the file object is missing the 'file' sub-field."""
|
||||
with pytest.raises(litellm.BadRequestError, match="missing the required 'file' field"):
|
||||
BedrockConverseMessagesProcessor._process_file_message(MALFORMED_FILE_OBJECT)
|
||||
|
||||
|
||||
def test_bedrock_process_file_message_explicit_null_file_field_raises_bad_request():
|
||||
with pytest.raises(litellm.BadRequestError, match="missing the required 'file' field"):
|
||||
BedrockConverseMessagesProcessor._process_file_message(EXPLICIT_NULL_FILE_OBJECT)
|
||||
|
||||
|
||||
def test_bedrock_async_process_file_message_malformed_raises_bad_request():
|
||||
"""_async_process_file_message should raise BadRequestError (not KeyError)
|
||||
when the file object is missing the 'file' sub-field."""
|
||||
|
||||
async def _run() -> None:
|
||||
with pytest.raises(
|
||||
litellm.BadRequestError, match="missing the required 'file' field"
|
||||
):
|
||||
await BedrockConverseMessagesProcessor._async_process_file_message(
|
||||
MALFORMED_FILE_OBJECT
|
||||
)
|
||||
|
||||
asyncio.run(_run())
|
||||
|
||||
|
||||
def test_bedrock_async_process_file_message_explicit_null_file_field_raises_bad_request():
|
||||
async def _run() -> None:
|
||||
with pytest.raises(
|
||||
litellm.BadRequestError, match="missing the required 'file' field"
|
||||
):
|
||||
await BedrockConverseMessagesProcessor._async_process_file_message(
|
||||
EXPLICIT_NULL_FILE_OBJECT
|
||||
)
|
||||
|
||||
asyncio.run(_run())
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# openai/chat/gpt_transformation.py
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_openai_apply_common_transform_malformed_file_raises_bad_request():
|
||||
"""_apply_common_transform_content_item should raise BadRequestError (not KeyError)
|
||||
when a content block has type='file' but no 'file' sub-field."""
|
||||
config = OpenAIGPTConfig()
|
||||
malformed_block: OpenAIMessageContentListBlock = cast(
|
||||
OpenAIMessageContentListBlock, {"type": "file"}
|
||||
)
|
||||
with pytest.raises(litellm.BadRequestError, match="missing the required 'file' field"):
|
||||
config._apply_common_transform_content_item(malformed_block)
|
||||
|
||||
|
||||
def test_openai_apply_common_transform_explicit_null_file_field_raises_bad_request():
|
||||
config = OpenAIGPTConfig()
|
||||
explicit_null_block: OpenAIMessageContentListBlock = cast(
|
||||
OpenAIMessageContentListBlock,
|
||||
{"type": "file", "file": None},
|
||||
)
|
||||
with pytest.raises(litellm.BadRequestError, match="missing the required 'file' field"):
|
||||
config._apply_common_transform_content_item(explicit_null_block)
|
||||
|
||||
|
||||
def test_openai_apply_common_transform_well_formed_file_does_not_raise():
|
||||
"""_apply_common_transform_content_item should not raise for well-formed file blocks."""
|
||||
config = OpenAIGPTConfig()
|
||||
well_formed_block: OpenAIMessageContentListBlock = cast(
|
||||
OpenAIMessageContentListBlock,
|
||||
{"type": "file", "file": {"file_id": "file-abc123"}},
|
||||
)
|
||||
result = config._apply_common_transform_content_item(well_formed_block)
|
||||
assert result.get("type") == "file"
|
||||
file_field = cast(ChatCompletionFileObject, result).get("file", {})
|
||||
assert file_field.get("file_id") == "file-abc123"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# factory.py - anthropic_process_openai_file_message
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_anthropic_process_openai_file_message_malformed_raises_bad_request():
|
||||
"""anthropic_process_openai_file_message should raise BadRequestError (not KeyError)
|
||||
when the file object is missing the 'file' sub-field."""
|
||||
with pytest.raises(litellm.BadRequestError, match="missing the required 'file' field"):
|
||||
anthropic_process_openai_file_message(MALFORMED_FILE_OBJECT)
|
||||
|
||||
|
||||
def test_anthropic_process_openai_file_message_explicit_null_file_field_raises_bad_request():
|
||||
with pytest.raises(litellm.BadRequestError, match="missing the required 'file' field"):
|
||||
anthropic_process_openai_file_message(EXPLICIT_NULL_FILE_OBJECT)
|
||||
|
||||
|
||||
def test_anthropic_process_openai_file_message_well_formed_file_id_does_not_raise():
|
||||
"""anthropic_process_openai_file_message should not raise for a well-formed file_id block."""
|
||||
well_formed: ChatCompletionFileObject = cast(
|
||||
ChatCompletionFileObject,
|
||||
{"type": "file", "file": {"file_id": "file-abc123"}},
|
||||
)
|
||||
result = anthropic_process_openai_file_message(well_formed)
|
||||
assert result.get("type") in ("document", "image", "container_upload")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# common_utils.py - migrate_file_to_image_url
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_migrate_file_to_image_url_malformed_raises_bad_request():
|
||||
"""migrate_file_to_image_url should raise BadRequestError (not KeyError)
|
||||
when the file object is missing the 'file' sub-field."""
|
||||
with pytest.raises(litellm.BadRequestError, match="missing the required 'file' field"):
|
||||
migrate_file_to_image_url(MALFORMED_FILE_OBJECT)
|
||||
|
||||
|
||||
def test_migrate_file_to_image_url_explicit_null_file_field_raises_bad_request():
|
||||
with pytest.raises(litellm.BadRequestError, match="missing the required 'file' field"):
|
||||
migrate_file_to_image_url(EXPLICIT_NULL_FILE_OBJECT)
|
||||
|
||||
|
||||
def test_migrate_file_to_image_url_well_formed_returns_image_url():
|
||||
"""migrate_file_to_image_url should return an image_url block for a well-formed file."""
|
||||
well_formed: ChatCompletionFileObject = cast(
|
||||
ChatCompletionFileObject,
|
||||
{"type": "file", "file": {"file_id": "file-abc123", "format": "png"}},
|
||||
)
|
||||
result = migrate_file_to_image_url(well_formed)
|
||||
assert result.get("type") == "image_url"
|
||||
image_url = result.get("image_url", {})
|
||||
assert isinstance(image_url, dict)
|
||||
assert image_url.get("url") == "file-abc123"
|
||||
Loading…
Add table
Reference in a new issue