litellm/tests/test_litellm_rust/ocr/test_settings.py
yujonglee 0fc11653e2
test(rust-bridge): pin Messages and Responses callback contracts on the native path (#46443)
* test(rust-bridge): pin Messages and Responses callback contracts on the native path

Each test runs the native route and asserts what the Python path does today, so a
known gap is a strict xfail that names its reason and the exception it fails with
Messages and Responses share one Surface and hook recorder in support/callbacks.py,
and each surface folder lists its own expected counts and gaps.

Body-level hooks from the function body (pre-request hook, websearch short-circuit,
Messages interceptors, planned and legacy agentic loops, streaming deployment hook)
are xfail until native runs them

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(rust-bridge): give every route folder the same one-concern-per-file layout

Route data, the route Surface and the server fixture move into each route folder, and support/ keeps only
route-neutral helpers. test_inference.py splits into chat_completions/ and responses/, ocr/test_requests.py
splits into requests, failures and settings, and Messages and Responses get streaming and body hook files.
Messages request tests that duplicate inference-messages request.rs are removed, and drop_params read
from litellm.drop_params is now covered

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(rust-bridge): expect the required-route decline error for unsupported Responses requests

Co-Authored-By: Yujong Lee <yujong@berri.ai>

* test(rust-bridge): import route data from the route folders in the secrets unit tests

Co-Authored-By: Yujong Lee <yujong@berri.ai>

* test(rust-bridge): import NoReturn in the OCR dispatch tests

Co-Authored-By: Yujong Lee <yujong@berri.ai>

* test(rust-bridge): move route-neutral helpers out of support/callbacks.py

Co-Authored-By: Yujong Lee <yujong@berri.ai>

* test(rust-bridge): cover deployment rejection and concurrent callbacks

* test(rust-bridge): assert native serving via the x-litellm-rust marker in the route folder tests

Co-Authored-By: Yujong Lee <yujong@berri.ai>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-14 17:07:20 -07:00

198 lines
7 KiB
Python

from pathlib import Path
from typing import Final
import pytest
import litellm
from tests.test_litellm_rust.ocr.data import (
OCR_DOCUMENT,
call,
)
from tests.test_litellm_rust.support.callback_recorder import RecordingLogger
from tests.test_litellm_rust.support.recording_server import RecordingServer
from tests.test_litellm_rust.support.routing import assert_served_natively
pytestmark = pytest.mark.requires_rust_extension
@pytest.mark.asyncio
@pytest.mark.parametrize("asynchronous", [False, True])
@pytest.mark.parametrize(
"override, expected_key",
[
({}, "credential-key"),
({"api_key": "explicit-key"}, "explicit-key"),
({"api_key": None}, "environment-key"),
],
ids=["inherit", "explicit", "explicit-none"],
)
async def test_native_ocr_inherits_named_credentials_without_overwriting_arguments(
server: RecordingServer,
monkeypatch: pytest.MonkeyPatch,
asynchronous: bool,
override: dict[str, object],
expected_key: str,
) -> None:
from litellm.models.credentials import CredentialItem
pages: Final = [0]
opaque: Final = object()
monkeypatch.setenv("MISTRAL_API_KEY", "environment-key")
monkeypatch.setattr(
litellm,
"credential_list",
[
CredentialItem(credential_name="other", credential_info={}, credential_values={"api_key": "wrong-key"}),
CredentialItem(
credential_name="ocr-test",
credential_info={},
credential_values={
"api_key": "credential-key",
"api_base": server.base_url,
"pages": pages,
"opaque": opaque,
},
),
CredentialItem(credential_name="ocr-test", credential_info={}, credential_values={"api_key": "later-key"}),
],
)
class Observer(RecordingLogger):
def log_pre_api_call(self, model, messages, kwargs):
super().log_pre_api_call(model, messages, kwargs)
pages.append(2)
arguments: Final = {
"model": "mistral/mistral-ocr-latest",
"document": OCR_DOCUMENT,
"litellm_credential_name": "ocr-test",
"callbacks": [Observer()],
**override,
}
response: Final = await litellm.aocr(**arguments) if asynchronous else litellm.ocr(**arguments)
assert response.pages[0].markdown == "native OCR response"
assert server.requests[0].headers["authorization"] == f"Bearer {expected_key}"
assert server.requests[0].body["pages"] == [0, 2]
@pytest.mark.asyncio
@pytest.mark.parametrize("asynchronous", [False, True], ids=["sync", "async"])
@pytest.mark.parametrize(
"name,value",
[
("ssl_verify", object()),
("ssl_certificate", 1),
("ssl_certificate", ""),
("user_url_allowed_hosts", ["example.test", 1]),
],
)
async def test_native_settings_fail_before_provider_io(
server: RecordingServer,
monkeypatch: pytest.MonkeyPatch,
asynchronous: bool,
name: str,
value: object,
) -> None:
server.expected_requests = 0
monkeypatch.setattr(litellm, name, value)
with pytest.raises(ValueError, match=r"http_settings|url_policy"):
await call(server, asynchronous, num_retries=0)
assert server.requests == []
@pytest.mark.asyncio
@pytest.mark.parametrize("asynchronous", [False, True], ids=["sync", "async"])
@pytest.mark.parametrize(
"name,value",
[
("vertex_project", 1),
("vertex_location", ["region"]),
],
)
async def test_native_vertex_globals_fail_before_provider_io_only_for_vertex_calls(
server: RecordingServer,
monkeypatch: pytest.MonkeyPatch,
asynchronous: bool,
name: str,
value: object,
) -> None:
server.expected_requests = 1
monkeypatch.setattr(litellm, name, value)
await call(server, asynchronous, num_retries=0)
with pytest.raises(ValueError, match=r"provider_defaults"):
await call(server, asynchronous, model="vertex_ai/mistral-ocr-latest", num_retries=0)
assert len(server.requests) == 1
@pytest.mark.asyncio
@pytest.mark.parametrize("asynchronous", [False, True], ids=["sync", "async"])
async def test_native_ssl_context_is_terminal_configuration(server: RecordingServer, asynchronous: bool) -> None:
import ssl
server.expected_requests = 0
context: Final = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
with pytest.raises(ValueError, match=r"request\.ssl_verify.*SSLContext"):
await call(server, asynchronous, ssl_verify=context, num_retries=0)
assert server.requests == []
@pytest.mark.asyncio
@pytest.mark.parametrize("asynchronous", [False, True], ids=["sync", "async"])
async def test_native_settings_preserve_protocol_failures(
server: RecordingServer, monkeypatch: pytest.MonkeyPatch, asynchronous: bool
) -> None:
server.expected_requests = 0
failure: Final = LookupError("settings truth test failed")
cause: Final = RuntimeError("settings cause")
class RaisesBool:
def __bool__(self) -> bool:
raise failure from cause
monkeypatch.setattr(litellm, "force_ipv4", RaisesBool())
with pytest.raises(LookupError) as caught:
await call(server, asynchronous, num_retries=0)
assert caught.value is failure
assert caught.value.__cause__ is cause
assert caught.value.__traceback__ is not None
assert server.requests == []
@pytest.mark.asyncio
@pytest.mark.parametrize("asynchronous", [False, True], ids=["sync", "async"])
async def test_native_settings_observe_mutation_between_calls(
server: RecordingServer, monkeypatch: pytest.MonkeyPatch, asynchronous: bool
) -> None:
monkeypatch.setattr(litellm, "force_ipv4", "yes")
monkeypatch.setattr(litellm, "http2", 1)
monkeypatch.setattr(litellm, "vertex_project", [])
monkeypatch.setattr(litellm, "vertex_location", 0)
monkeypatch.setattr(litellm, "user_url_allowed_hosts", "EXAMPLE.TEST.")
response: Final = await call(server, asynchronous, num_retries=0)
assert response.pages[0].markdown == "native OCR response"
assert_served_natively(server)
monkeypatch.setattr(litellm, "ssl_certificate", 1)
with pytest.raises(ValueError, match=r"http_settings\.ssl_certificate"):
await call(server, asynchronous, num_retries=0)
assert len(server.requests) == 1
@pytest.mark.asyncio
@pytest.mark.parametrize("asynchronous", [False, True], ids=["sync", "async"])
@pytest.mark.parametrize("present", [False, True], ids=["missing", "invalid-pem"])
async def test_native_client_certificate_is_validated_before_io(
server: RecordingServer,
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
asynchronous: bool,
present: bool,
) -> None:
server.expected_requests = 0
certificate: Final = tmp_path / "client.pem"
if present:
certificate.write_text("invalid certificate")
monkeypatch.setattr(litellm, "ssl_certificate", str(certificate))
with pytest.raises(ValueError, match=r"http_settings\.ssl_certificate.*PEM") as caught:
await call(server, asynchronous, num_retries=0)
assert str(certificate) not in str(caught.value)
assert server.requests == []