mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-19 00:01:29 +00:00
fix(ocr): narrow public error attribute writes and cover callback failure mapping
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
parent
1f0c10147d
commit
cd4d78a26a
4 changed files with 82 additions and 6 deletions
|
|
@ -44,7 +44,7 @@ from litellm.llms.base_llm.base_model_iterator import (
|
|||
MockResponseIterator,
|
||||
)
|
||||
from litellm.llms.base_llm.batches.transformation import BaseBatchesConfig
|
||||
from litellm.llms.base_llm.chat.transformation import BaseConfig
|
||||
from litellm.llms.base_llm.chat.transformation import BaseConfig, BaseLLMException
|
||||
from litellm.llms.base_llm.containers.transformation import BaseContainerConfig
|
||||
from litellm.llms.base_llm.embedding.transformation import BaseEmbeddingConfig
|
||||
from litellm.llms.base_llm.evals.transformation import BaseEvalsAPIConfig
|
||||
|
|
@ -6060,8 +6060,6 @@ class BaseLLMHTTPHandler:
|
|||
error_headers = {}
|
||||
|
||||
if provider_config is None:
|
||||
from litellm.llms.base_llm.chat.transformation import BaseLLMException
|
||||
|
||||
raise BaseLLMException(
|
||||
status_code=status_code,
|
||||
message=error_text,
|
||||
|
|
@ -6074,7 +6072,11 @@ class BaseLLMHTTPHandler:
|
|||
status_code=status_code,
|
||||
headers=error_headers,
|
||||
)
|
||||
if isinstance(provider_config, BaseOCRConfig) and isinstance(error_response, httpx.Response):
|
||||
if (
|
||||
isinstance(provider_config, BaseOCRConfig)
|
||||
and isinstance(provider_error, BaseLLMException)
|
||||
and isinstance(error_response, httpx.Response)
|
||||
):
|
||||
provider_error.response = error_response
|
||||
if not isinstance(received_status_code, int):
|
||||
provider_error.status_code_is_synthesized = True
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ from types import MappingProxyType
|
|||
from typing import Final
|
||||
|
||||
import httpx
|
||||
import openai
|
||||
from pydantic import TypeAdapter, ValidationError
|
||||
|
||||
import litellm
|
||||
|
|
@ -59,7 +60,8 @@ def map_failure(error: Exception, request: LiteLLMOcrRequest, request_provider:
|
|||
original: Final = _upstream_failure(error)
|
||||
public_error: Final = failures.map_failure(original, request.model, request_provider, arguments(request))
|
||||
if isinstance(original, UpstreamFailure) and public_error.__context__ is original:
|
||||
public_error.response = original.response
|
||||
public_error.status_code = original.status_code
|
||||
public_error.__context__ = error
|
||||
if isinstance(public_error, openai.APIStatusError):
|
||||
public_error.response = original.response
|
||||
public_error.status_code = original.status_code
|
||||
return public_error
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ from litellm.llms.custom_httpx.http_handler import AsyncHTTPHandler, HTTPHandler
|
|||
from litellm.ocr.main import _prepare_ocr_request
|
||||
from litellm.rust_bridge import bindings, configuration, runtime
|
||||
from litellm.rust_bridge.ocr.entrypoints import NATIVE_AOCR, NATIVE_OCR
|
||||
from litellm.utils import ProviderConfigManager
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
|
@ -277,6 +278,7 @@ def _prepare(model: str, document: object, **kwargs: object) -> object:
|
|||
(
|
||||
("https://example.com/file.pdf", "document must be a dict"),
|
||||
({"type": "video_url", "video_url": "https://example.com/clip.mp4"}, "Invalid document type: video_url"),
|
||||
({"type": "document_url", "document_url": ""}, "Document URL is required"),
|
||||
),
|
||||
)
|
||||
def test_prepare_ocr_request_rejects_malformed_documents(document: object, match: str) -> None:
|
||||
|
|
@ -284,6 +286,20 @@ def test_prepare_ocr_request_rejects_malformed_documents(document: object, match
|
|||
_prepare("mistral/mistral-ocr-latest", document)
|
||||
|
||||
|
||||
def test_prepare_ocr_request_maps_param_mapping_errors_to_bad_request(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
config: Final = Mock()
|
||||
config.resolve_connection_params.return_value = ("test-key", None)
|
||||
config.get_supported_ocr_params.return_value = ["pages"]
|
||||
config.map_ocr_params.side_effect = ValueError("pages must be a list")
|
||||
monkeypatch.setattr(ProviderConfigManager, "get_provider_ocr_config", Mock(return_value=config))
|
||||
|
||||
with pytest.raises(litellm.BadRequestError, match="pages must be a list") as error:
|
||||
_prepare("mistral/mistral-ocr-latest", dict(PRICING_DOCUMENT), pages="1")
|
||||
|
||||
assert error.value.llm_provider == "mistral"
|
||||
assert isinstance(error.value.__cause__, ValueError)
|
||||
|
||||
|
||||
def test_prepare_ocr_request_rejects_provider_without_ocr_support() -> None:
|
||||
with pytest.raises(ValueError, match="OCR is not supported for provider: openai"):
|
||||
_prepare("openai/gpt-4o", dict(PRICING_DOCUMENT))
|
||||
|
|
|
|||
|
|
@ -1,4 +1,32 @@
|
|||
from typing import Final
|
||||
|
||||
import pytest
|
||||
|
||||
import litellm
|
||||
from litellm.rust_bridge.ocr.callbacks import UpstreamFailure, map_failure
|
||||
from litellm.rust_bridge.ocr.callbacks import response as build_ocr_response
|
||||
from litellm.rust_bridge.ocr.entrypoints import LiteLLMOcrRequest
|
||||
|
||||
REQUEST: Final = LiteLLMOcrRequest(
|
||||
model="mistral/mistral-ocr-latest",
|
||||
document={"type": "document_url", "document_url": "https://example.com/file.pdf"},
|
||||
api_key="test-key",
|
||||
api_base=None,
|
||||
timeout=None,
|
||||
custom_llm_provider=None,
|
||||
extra_headers=None,
|
||||
kwargs={"req_format": "markdown"},
|
||||
)
|
||||
|
||||
|
||||
class RustUpstreamError(Exception):
|
||||
def __init__(self, status: int, body: str, headers: tuple[tuple[str, str], ...]) -> None:
|
||||
super().__init__(status, body)
|
||||
self.headers: Final = list(headers)
|
||||
|
||||
|
||||
class RustFormatError(Exception):
|
||||
ocr_request_format_error: Final = True
|
||||
|
||||
|
||||
def test_rust_ocr_response_retains_provider_native_response():
|
||||
|
|
@ -16,3 +44,31 @@ def test_rust_ocr_response_retains_provider_native_response():
|
|||
|
||||
assert response.get_provider_native_response() == provider_response
|
||||
assert response.model_dump().get("provider_native_response") is None
|
||||
|
||||
|
||||
def test_map_failure_builds_public_error_from_upstream_status_and_headers() -> None:
|
||||
error: Final = RustUpstreamError(429, '{"message": "slow down"}', (("retry-after", "7"),))
|
||||
|
||||
public_error: Final = map_failure(error, REQUEST, "mistral")
|
||||
|
||||
assert isinstance(public_error, litellm.RateLimitError)
|
||||
assert public_error.status_code == 429
|
||||
assert public_error.response.headers["retry-after"] == "7"
|
||||
assert public_error.response.text == '{"message": "slow down"}'
|
||||
assert public_error.__context__ is error
|
||||
assert public_error.llm_provider == "mistral"
|
||||
|
||||
|
||||
def test_map_failure_leaves_non_upstream_errors_unwrapped() -> None:
|
||||
error: Final = RuntimeError("bridge exploded")
|
||||
|
||||
public_error: Final = map_failure(error, REQUEST, "mistral")
|
||||
|
||||
assert not isinstance(public_error, UpstreamFailure)
|
||||
assert isinstance(public_error, litellm.APIConnectionError)
|
||||
assert "bridge exploded" in str(public_error)
|
||||
|
||||
|
||||
def test_map_failure_reports_invalid_request_format_as_unsupported_params() -> None:
|
||||
with pytest.raises(litellm.UnsupportedParamsError, match="Invalid `req_format`: 'markdown'"):
|
||||
raise map_failure(RustFormatError(), REQUEST, "mistral")
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue