mirror of
https://github.com/BerriAI/litellm.git
synced 2026-08-28 05:25:59 +00:00
fix(containers): surface provider errors from container file content endpoint (#37737)
The generic container handler returned response.content for endpoints marked returns_binary before it ran any status or error check, so a non-2xx answer from the provider was handed back to the caller as raw bytes. Asking for the content of a container file that does not exist returned the provider's 404 error body as an opaque payload instead of raising. Move the check ahead of the binary short-circuit and apply it to every container file endpoint, falling back to the response text when the error body is not JSON.
This commit is contained in:
parent
cb89c7aa8f
commit
2a863f8bdd
2 changed files with 161 additions and 42 deletions
|
|
@ -39,6 +39,10 @@ RESPONSE_TYPES: Final[dict[str, type]] = {
|
|||
"DeleteContainerFileResponse": DeleteContainerFileResponse,
|
||||
}
|
||||
|
||||
ContainerEndpointResponse = (
|
||||
ContainerFileListResponse | ContainerFileObject | DeleteContainerFileResponse | bytes | dict[str, object]
|
||||
)
|
||||
|
||||
|
||||
def _load_endpoints_config() -> dict:
|
||||
"""Load the endpoints configuration from JSON file."""
|
||||
|
|
@ -101,6 +105,51 @@ def _build_query_params(
|
|||
return params
|
||||
|
||||
|
||||
def _error_message_from_response(response: httpx.Response) -> str:
|
||||
try:
|
||||
body: Final = response.json()
|
||||
except ValueError:
|
||||
return response.text
|
||||
|
||||
if isinstance(body, dict) and isinstance(body.get("error"), dict):
|
||||
message: Final = body["error"].get("message")
|
||||
if isinstance(message, str):
|
||||
return message
|
||||
|
||||
return response.text
|
||||
|
||||
|
||||
def _transform_response(
|
||||
response: httpx.Response,
|
||||
returns_binary: bool,
|
||||
response_type_name: str,
|
||||
) -> ContainerEndpointResponse:
|
||||
from litellm.llms.base_llm.chat.transformation import BaseLLMException
|
||||
|
||||
if httpx.codes.is_error(response.status_code):
|
||||
raise BaseLLMException(
|
||||
status_code=response.status_code,
|
||||
message=_error_message_from_response(response),
|
||||
headers=dict(response.headers),
|
||||
)
|
||||
|
||||
if returns_binary:
|
||||
return response.content
|
||||
|
||||
response_json: Final = response.json()
|
||||
if "error" in response_json:
|
||||
raise BaseLLMException(
|
||||
status_code=response.status_code,
|
||||
message=response_json.get("error", {}).get("message", str(response_json)),
|
||||
headers=dict(response.headers),
|
||||
)
|
||||
|
||||
response_type: Final = RESPONSE_TYPES.get(response_type_name)
|
||||
if response_type:
|
||||
return response_type(**response_json)
|
||||
return response_json
|
||||
|
||||
|
||||
def _prepare_multipart_file_upload(
|
||||
file: Any,
|
||||
headers: dict[str, Any],
|
||||
|
|
@ -270,27 +319,11 @@ class GenericContainerHandler:
|
|||
else:
|
||||
raise ValueError(f"Unsupported HTTP method: {method}")
|
||||
|
||||
# For binary responses, return raw content
|
||||
if returns_binary:
|
||||
return response.content
|
||||
|
||||
# Check for error response
|
||||
response_json: Final = response.json()
|
||||
if "error" in response_json:
|
||||
from litellm.llms.base_llm.chat.transformation import BaseLLMException
|
||||
|
||||
error_msg: Final = response_json.get("error", {}).get("message", str(response_json))
|
||||
raise BaseLLMException(
|
||||
status_code=response.status_code,
|
||||
message=error_msg,
|
||||
headers=dict(response.headers),
|
||||
)
|
||||
|
||||
# Parse response
|
||||
response_type: Final = RESPONSE_TYPES.get(endpoint_config["response_type"])
|
||||
if response_type:
|
||||
return response_type(**response_json)
|
||||
return response_json
|
||||
return _transform_response(
|
||||
response=response,
|
||||
returns_binary=returns_binary,
|
||||
response_type_name=endpoint_config["response_type"],
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
raise e
|
||||
|
|
@ -378,27 +411,11 @@ class GenericContainerHandler:
|
|||
else:
|
||||
raise ValueError(f"Unsupported HTTP method: {method}")
|
||||
|
||||
# For binary responses, return raw content
|
||||
if returns_binary:
|
||||
return response.content
|
||||
|
||||
# Check for error response
|
||||
response_json: Final = response.json()
|
||||
if "error" in response_json:
|
||||
from litellm.llms.base_llm.chat.transformation import BaseLLMException
|
||||
|
||||
error_msg: Final = response_json.get("error", {}).get("message", str(response_json))
|
||||
raise BaseLLMException(
|
||||
status_code=response.status_code,
|
||||
message=error_msg,
|
||||
headers=dict(response.headers),
|
||||
)
|
||||
|
||||
# Parse response
|
||||
response_type: Final = RESPONSE_TYPES.get(endpoint_config["response_type"])
|
||||
if response_type:
|
||||
return response_type(**response_json)
|
||||
return response_json
|
||||
return _transform_response(
|
||||
response=response,
|
||||
returns_binary=returns_binary,
|
||||
response_type_name=endpoint_config["response_type"],
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
raise e
|
||||
|
|
|
|||
102
tests/test_litellm/llms/custom_httpx/test_container_handler.py
Normal file
102
tests/test_litellm/llms/custom_httpx/test_container_handler.py
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
from unittest.mock import MagicMock
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
|
||||
import litellm
|
||||
from litellm.llms.base_llm.chat.transformation import BaseLLMException
|
||||
from litellm.llms.custom_httpx.container_handler import generic_container_handler
|
||||
from litellm.llms.custom_httpx.http_handler import AsyncHTTPHandler, HTTPHandler
|
||||
from litellm.types.router import GenericLiteLLMParams
|
||||
from litellm.utils import ProviderConfigManager
|
||||
|
||||
FILE_NOT_FOUND_BODY = {
|
||||
"error": {
|
||||
"message": "File not found.",
|
||||
"type": "invalid_request_error",
|
||||
"param": None,
|
||||
"code": None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def _sync_client(response: httpx.Response) -> HTTPHandler:
|
||||
handler = HTTPHandler()
|
||||
handler.client = httpx.Client(transport=httpx.MockTransport(lambda _request: response))
|
||||
return handler
|
||||
|
||||
|
||||
def _async_client(response: httpx.Response) -> AsyncHTTPHandler:
|
||||
handler = AsyncHTTPHandler()
|
||||
handler.client = httpx.AsyncClient(transport=httpx.MockTransport(lambda _request: response))
|
||||
return handler
|
||||
|
||||
|
||||
def _handle(endpoint_name: str, client, **overrides):
|
||||
return generic_container_handler.handle(
|
||||
endpoint_name=endpoint_name,
|
||||
container_provider_config=ProviderConfigManager.get_provider_container_config(
|
||||
provider=litellm.LlmProviders.OPENAI
|
||||
),
|
||||
litellm_params=GenericLiteLLMParams(api_key="sk-test"),
|
||||
logging_obj=MagicMock(),
|
||||
client=client,
|
||||
container_id="cntr_real",
|
||||
file_id="cfile_nonexistent",
|
||||
**overrides,
|
||||
)
|
||||
|
||||
|
||||
def test_binary_endpoint_raises_on_error_status():
|
||||
with pytest.raises(BaseLLMException) as exc_info:
|
||||
_handle(
|
||||
"retrieve_container_file_content",
|
||||
_sync_client(httpx.Response(404, json=FILE_NOT_FOUND_BODY)),
|
||||
)
|
||||
|
||||
assert exc_info.value.status_code == 404
|
||||
assert exc_info.value.message == "File not found."
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_async_binary_endpoint_raises_on_error_status():
|
||||
with pytest.raises(BaseLLMException) as exc_info:
|
||||
await _handle(
|
||||
"aretrieve_container_file_content",
|
||||
_async_client(httpx.Response(404, json=FILE_NOT_FOUND_BODY)),
|
||||
_is_async=True,
|
||||
)
|
||||
|
||||
assert exc_info.value.status_code == 404
|
||||
assert exc_info.value.message == "File not found."
|
||||
|
||||
|
||||
def test_binary_endpoint_returns_raw_content_on_success():
|
||||
content = _handle(
|
||||
"retrieve_container_file_content",
|
||||
_sync_client(httpx.Response(200, content=b"\x00binary-payload")),
|
||||
)
|
||||
|
||||
assert content == b"\x00binary-payload"
|
||||
|
||||
|
||||
def test_error_status_with_non_json_body_surfaces_response_text():
|
||||
with pytest.raises(BaseLLMException) as exc_info:
|
||||
_handle(
|
||||
"retrieve_container_file_content",
|
||||
_sync_client(httpx.Response(502, content=b"<html>bad gateway</html>")),
|
||||
)
|
||||
|
||||
assert exc_info.value.status_code == 502
|
||||
assert exc_info.value.message == "<html>bad gateway</html>"
|
||||
|
||||
|
||||
def test_json_endpoint_still_raises_provider_error_message():
|
||||
with pytest.raises(BaseLLMException) as exc_info:
|
||||
_handle(
|
||||
"retrieve_container_file",
|
||||
_sync_client(httpx.Response(404, json=FILE_NOT_FOUND_BODY)),
|
||||
)
|
||||
|
||||
assert exc_info.value.status_code == 404
|
||||
assert exc_info.value.message == "File not found."
|
||||
Loading…
Add table
Reference in a new issue