mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-24 00:52:24 +00:00
* docs(rust): plan Python interop foundation * fix(rust): preserve Python settings coercion at the native boundary * chore(rust): drop interop planning note Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * feat(rust): resolve OCR provider secrets through an async SecretSource before transformation Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * feat(rust): project the Python secret manager into the bridge and resolve OCR secrets through it Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * refactor(rust): drop premium_user from the secret manager snapshot Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix(rust-bridge): read the private key management globals once in the settings snapshot Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix(rust): bound the bridge secret manager state cache to the active snapshot Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(rust): inline coercion unit tests * fix(rust): preserve Python secret manager bindings * refactor(rust-bridge): let settings projectors own their contract specs Each settings group now declares its SettingSpec rows next to the projector that reads them, and the manifest test derives python_settings.json from those tables instead of a hand-copied duplicate. Field carries (group, name) instead of a dotted path, and coercion gains the dict-item reader plus the Redis Boolean, certificate-requirement, non-empty string, and numeric adapters that the cache configuration projection adopts next. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * refactor(rust-bridge): capture the secret manager binding in one settings read The secret_manager accessor now carries the live client and settings objects, so the bridge classifies the binding from a single snapshot instead of re-reading litellm globals. The unreachable native arm and the service alias go away, the binding-to-state mapping moves next to the snapshot, and the Python callback precomputes its key_manager name. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * refactor(rust-bridge): execute typed settings field declarations * refactor(rust-bridge): compare cache backends by identity behind one exact trait cache-response gains an object-safe ExactResponseCache so every exact-match backend sits behind one pointer; WriteBuffer flushes through it. The bridge's NativeResponseCache shrinks from nine variants and fifteen per-backend accessors to an exact service plus the three semantic backends, and facade mismatch detection compares BackendIdentity values instead of matching on each backend type. Request projections move next to NativeRequest. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * refactor(rust-bridge): drive both Python-embedded semantic caches through one execution Redis-semantic and Valkey-semantic operations now share one SemanticExecution body: await the Python embedder, seed the task-local vector, run the native backend, repeat per batch entry. Valkey drops its with_embedder path in favor of the same seeded embedder, and each backend keeps its own embedding-failure policy. PythonEmbedder exposes one call shape. Redis-semantic thresholds are compared at the backend's f32 width, which un-breaks the redis-stack parity tests that a 0.8 facade threshold failed before this branch. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * wip * feat(rust-bridge): complete response cache runtime surface * fix(rust-bridge): preserve secret manager callback exceptions * refactor(rust-bridge): unify route cache and secret rollout catalog --------- Co-authored-by: Yujong Lee <yujong@berri.ai> Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
425 lines
15 KiB
Python
425 lines
15 KiB
Python
from collections.abc import Awaitable, Callable, Mapping
|
|
from typing import Final, cast # noqa: TID251 # narrows legacy callable signatures for inspect
|
|
|
|
import httpx
|
|
import pytest
|
|
|
|
import litellm
|
|
from litellm.llms.base_llm.ocr.transformation import OCRResponse
|
|
from litellm.ocr.dispatch import (
|
|
_ADISPATCH, # pyright: ignore[reportPrivateUsage] # tests configured dispatch
|
|
_DISPATCH, # pyright: ignore[reportPrivateUsage] # tests configured dispatch
|
|
)
|
|
from litellm.rust_bridge import catalog
|
|
from litellm.rust_bridge.bindings import NativeBinding
|
|
from litellm.rust_bridge.catalog import Route, RouteRule, Rules
|
|
from litellm.rust_bridge.configuration import Rollout
|
|
from litellm.rust_bridge.ocr.entrypoints import (
|
|
NATIVE_AOCR,
|
|
NATIVE_OCR,
|
|
LiteLLMOcrRequest,
|
|
NativeAocr,
|
|
NativeOcr,
|
|
)
|
|
|
|
PYTHON_RULES: Final[Rules] = (RouteRule(Route.OCR, Rollout.PYTHON_ONLY),)
|
|
RUST_RULES: Final[Rules] = (RouteRule(Route.OCR, Rollout.RUST_REQUIRED),)
|
|
|
|
|
|
def ocr_binding(native: NativeOcr | None) -> NativeBinding[NativeOcr]:
|
|
binding: Final[NativeBinding[NativeOcr]] = NativeBinding("ocr", validate=lambda _: None)
|
|
binding.override(native)
|
|
return binding
|
|
|
|
|
|
def aocr_binding(native: NativeAocr | None) -> NativeBinding[NativeAocr]:
|
|
binding: Final[NativeBinding[NativeAocr]] = NativeBinding("aocr", validate=lambda _: None)
|
|
binding.override(native)
|
|
return binding
|
|
|
|
|
|
def response(model: str = "mistral/mistral-ocr-latest") -> OCRResponse:
|
|
return OCRResponse(pages=[], model=model)
|
|
|
|
|
|
def test_python_route_forwards_original_call_shape() -> None:
|
|
document: Final[Mapping[str, object]] = {
|
|
"type": "document_url",
|
|
"document_url": "https://example.invalid/document.pdf",
|
|
}
|
|
pages: Final = [0]
|
|
args: Final[tuple[object, ...]] = ("mistral/mistral-ocr-latest", document)
|
|
kwargs: Final[Mapping[str, object]] = {"pages": pages}
|
|
captured: Final[list[tuple[tuple[object, ...], Mapping[str, object]]]] = []
|
|
expected: Final = response()
|
|
|
|
def python(*call_args: object, **call_kwargs: object) -> OCRResponse: # kwargs-ok: records public call shape
|
|
captured.append((call_args, call_kwargs))
|
|
return expected
|
|
|
|
def native(
|
|
request: LiteLLMOcrRequest,
|
|
args: tuple[object, ...],
|
|
kwargs: Mapping[str, object],
|
|
) -> OCRResponse:
|
|
pytest.fail("Python-only dispatch must not call native")
|
|
|
|
result: Final = _DISPATCH.run(
|
|
args,
|
|
kwargs,
|
|
python=python,
|
|
binding=ocr_binding(native),
|
|
native=lambda hook, request, call_args, call_kwargs: hook(request, call_args, call_kwargs),
|
|
rules=PYTHON_RULES,
|
|
)
|
|
|
|
assert result is expected
|
|
call_args, call_kwargs = captured[0]
|
|
assert call_args == args
|
|
assert call_args[1] is document
|
|
assert call_kwargs == kwargs
|
|
assert call_kwargs["pages"] is pages
|
|
assert kwargs == {"pages": pages}
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_async_python_route_forwards_original_call_shape() -> None:
|
|
document: Final[Mapping[str, object]] = {"type": "file", "file": b"pdf"}
|
|
pages: Final = [1]
|
|
args: Final[tuple[object, ...]] = ("mistral/mistral-ocr-latest", document)
|
|
kwargs: Final[Mapping[str, object]] = {"pages": pages}
|
|
captured: Final[list[tuple[tuple[object, ...], Mapping[str, object]]]] = []
|
|
expected: Final = response()
|
|
|
|
async def python(
|
|
*call_args: object,
|
|
**call_kwargs: object, # kwargs-ok: records public call shape
|
|
) -> OCRResponse:
|
|
captured.append((call_args, call_kwargs))
|
|
return expected
|
|
|
|
async def native(
|
|
request: LiteLLMOcrRequest,
|
|
args: tuple[object, ...],
|
|
kwargs: Mapping[str, object],
|
|
) -> OCRResponse:
|
|
pytest.fail("Python-only dispatch must not call native")
|
|
|
|
result: Final = await _ADISPATCH.arun(
|
|
args,
|
|
kwargs,
|
|
python=python,
|
|
binding=aocr_binding(native),
|
|
native=lambda hook, request, call_args, call_kwargs: hook(request, call_args, call_kwargs),
|
|
rules=PYTHON_RULES,
|
|
)
|
|
|
|
assert result is expected
|
|
call_args, call_kwargs = captured[0]
|
|
assert call_args == args
|
|
assert call_args[1] is document
|
|
assert call_kwargs == kwargs
|
|
assert call_kwargs["pages"] is pages
|
|
assert kwargs == {"pages": pages}
|
|
|
|
|
|
def test_native_receives_normalized_positional_request_and_original_call_shape() -> None:
|
|
document: Final[Mapping[str, object]] = {"type": "file", "file": b"pdf"}
|
|
timeout: Final = httpx.Timeout(30)
|
|
extra_headers: Final[dict[str, object]] = {"x-test": "1"}
|
|
pages: Final = [0, 2]
|
|
args: Final[tuple[object, ...]] = ("mistral/mistral-ocr-latest", document)
|
|
kwargs: Final[Mapping[str, object]] = {
|
|
"api_key": "test-key",
|
|
"api_base": "https://example.invalid",
|
|
"timeout": timeout,
|
|
"custom_llm_provider": "mistral",
|
|
"extra_headers": extra_headers,
|
|
"pages": pages,
|
|
}
|
|
captured: Final[list[tuple[LiteLLMOcrRequest, tuple[object, ...], Mapping[str, object]]]] = []
|
|
expected: Final = response()
|
|
|
|
def python(*call_args: object, **call_kwargs: object) -> OCRResponse: # kwargs-ok: rejected fallback
|
|
pytest.fail("Required Rust dispatch must not call Python")
|
|
|
|
def native(
|
|
request: LiteLLMOcrRequest,
|
|
args: tuple[object, ...],
|
|
kwargs: Mapping[str, object],
|
|
) -> OCRResponse:
|
|
captured.append((request, args, kwargs))
|
|
return expected
|
|
|
|
result: Final = _DISPATCH.run(
|
|
args,
|
|
kwargs,
|
|
python=python,
|
|
binding=ocr_binding(native),
|
|
native=lambda hook, request, call_args, call_kwargs: hook(request, call_args, call_kwargs),
|
|
rules=RUST_RULES,
|
|
)
|
|
|
|
request, call_args, call_kwargs = captured[0]
|
|
assert result is expected
|
|
assert request.model == "mistral/mistral-ocr-latest"
|
|
assert request.document is document
|
|
assert request.api_key == "test-key"
|
|
assert request.api_base == "https://example.invalid"
|
|
assert request.timeout is timeout
|
|
assert request.custom_llm_provider == "mistral"
|
|
assert request.extra_headers is extra_headers
|
|
assert request.kwargs == {"pages": pages}
|
|
assert request.kwargs["pages"] is pages
|
|
assert call_args is args
|
|
assert call_kwargs is kwargs
|
|
|
|
|
|
def test_native_preserves_keyword_model_and_document_in_original_call_shape() -> None:
|
|
document: Final[Mapping[str, object]] = {
|
|
"type": "document_url",
|
|
"document_url": "https://example.invalid/document.pdf",
|
|
}
|
|
pages: Final = [1]
|
|
args: Final[tuple[object, ...]] = ()
|
|
kwargs: Final[Mapping[str, object]] = {
|
|
"model": "mistral/mistral-ocr-latest",
|
|
"document": document,
|
|
"pages": pages,
|
|
}
|
|
captured: Final[list[tuple[LiteLLMOcrRequest, tuple[object, ...], Mapping[str, object]]]] = []
|
|
expected: Final = response()
|
|
|
|
def python(*call_args: object, **call_kwargs: object) -> OCRResponse: # kwargs-ok: rejected fallback
|
|
pytest.fail("Required Rust dispatch must not call Python")
|
|
|
|
def native(
|
|
request: LiteLLMOcrRequest,
|
|
args: tuple[object, ...],
|
|
kwargs: Mapping[str, object],
|
|
) -> OCRResponse:
|
|
captured.append((request, args, kwargs))
|
|
return expected
|
|
|
|
result: Final = _DISPATCH.run(
|
|
args,
|
|
kwargs,
|
|
python=python,
|
|
binding=ocr_binding(native),
|
|
native=lambda hook, request, call_args, call_kwargs: hook(request, call_args, call_kwargs),
|
|
rules=RUST_RULES,
|
|
)
|
|
|
|
request, call_args, call_kwargs = captured[0]
|
|
assert result is expected
|
|
assert request.model == "mistral/mistral-ocr-latest"
|
|
assert request.document is document
|
|
assert request.kwargs == {"pages": pages}
|
|
assert call_args is args
|
|
assert call_kwargs is kwargs
|
|
assert call_kwargs["model"] == "mistral/mistral-ocr-latest"
|
|
assert call_kwargs["document"] is document
|
|
|
|
|
|
def test_aocr_marker_bypasses_native() -> None:
|
|
document: Final[Mapping[str, object]] = {"type": "file", "file": b"pdf"}
|
|
args: Final[tuple[object, ...]] = ("mistral/mistral-ocr-latest", document)
|
|
kwargs: Final[Mapping[str, object]] = {"aocr": True}
|
|
captured: Final[list[tuple[tuple[object, ...], Mapping[str, object]]]] = []
|
|
expected: Final = response()
|
|
|
|
def python(*call_args: object, **call_kwargs: object) -> OCRResponse: # kwargs-ok: records public call shape
|
|
captured.append((call_args, call_kwargs))
|
|
return expected
|
|
|
|
def native(
|
|
request: LiteLLMOcrRequest,
|
|
args: tuple[object, ...],
|
|
kwargs: Mapping[str, object],
|
|
) -> OCRResponse:
|
|
pytest.fail("aocr's inner ocr call must stay on Python")
|
|
|
|
result: Final = _DISPATCH.run(
|
|
args,
|
|
kwargs,
|
|
python=python,
|
|
binding=ocr_binding(native),
|
|
native=lambda hook, request, call_args, call_kwargs: hook(request, call_args, call_kwargs),
|
|
rules=RUST_RULES,
|
|
)
|
|
|
|
assert result is expected
|
|
assert captured == [(args, kwargs)]
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("args", "kwargs", "message"),
|
|
(
|
|
(
|
|
("mistral/mistral-ocr-latest", {"type": "file", "file": b"pdf"}),
|
|
{"model": "duplicate"},
|
|
r"ocr\(\) got multiple values for argument 'model'",
|
|
),
|
|
(
|
|
("mistral/mistral-ocr-latest",),
|
|
{},
|
|
r"ocr\(\) missing 1 required positional argument: 'document'",
|
|
),
|
|
),
|
|
)
|
|
def test_ocr_parser_errors_before_python_or_native(
|
|
args: tuple[object, ...], kwargs: Mapping[str, object], message: str
|
|
) -> None:
|
|
def python(*call_args: object, **call_kwargs: object) -> OCRResponse: # kwargs-ok: rejects parser failures
|
|
pytest.fail("OCR parser failures must not call Python")
|
|
|
|
def native(
|
|
request: LiteLLMOcrRequest,
|
|
args: tuple[object, ...],
|
|
kwargs: Mapping[str, object],
|
|
) -> OCRResponse:
|
|
pytest.fail("OCR parser failures must not call native")
|
|
|
|
with pytest.raises(TypeError, match=message):
|
|
_DISPATCH.run(
|
|
args,
|
|
kwargs,
|
|
python=python,
|
|
binding=ocr_binding(native),
|
|
native=lambda hook, request, call_args, call_kwargs: hook(request, call_args, call_kwargs),
|
|
rules=RUST_RULES,
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.parametrize(
|
|
("args", "kwargs", "message"),
|
|
(
|
|
(
|
|
("mistral/mistral-ocr-latest", {"type": "file", "file": b"pdf"}),
|
|
{"model": "duplicate"},
|
|
r"aocr\(\) got multiple values for argument 'model'",
|
|
),
|
|
(
|
|
("mistral/mistral-ocr-latest",),
|
|
{},
|
|
r"aocr\(\) missing 1 required positional argument: 'document'",
|
|
),
|
|
),
|
|
)
|
|
async def test_aocr_parser_errors_before_python_or_native(
|
|
args: tuple[object, ...], kwargs: Mapping[str, object], message: str
|
|
) -> None:
|
|
async def python(
|
|
*call_args: object,
|
|
**call_kwargs: object, # kwargs-ok: rejects parser failures
|
|
) -> OCRResponse:
|
|
pytest.fail("OCR parser failures must not call Python")
|
|
|
|
async def native(
|
|
request: LiteLLMOcrRequest,
|
|
args: tuple[object, ...],
|
|
kwargs: Mapping[str, object],
|
|
) -> OCRResponse:
|
|
pytest.fail("OCR parser failures must not call native")
|
|
|
|
with pytest.raises(TypeError, match=message):
|
|
await _ADISPATCH.arun(
|
|
args,
|
|
kwargs,
|
|
python=python,
|
|
binding=aocr_binding(native),
|
|
native=lambda hook, request, call_args, call_kwargs: hook(request, call_args, call_kwargs),
|
|
rules=RUST_RULES,
|
|
)
|
|
|
|
|
|
def test_public_ocr_routes_through_dispatch(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
document: Final[Mapping[str, object]] = {
|
|
"type": "document_url",
|
|
"document_url": "https://example.invalid/document.pdf",
|
|
}
|
|
captured: Final[list[LiteLLMOcrRequest]] = []
|
|
expected: Final = response()
|
|
|
|
def native(
|
|
request: LiteLLMOcrRequest,
|
|
args: tuple[object, ...],
|
|
kwargs: Mapping[str, object],
|
|
) -> OCRResponse:
|
|
captured.append(request)
|
|
return expected
|
|
|
|
NATIVE_OCR.override(native)
|
|
monkeypatch.setattr(catalog, "RULES", RUST_RULES)
|
|
public_ocr: Final = cast(Callable[..., OCRResponse], litellm.ocr)
|
|
try:
|
|
result: Final = public_ocr(model="mistral/mistral-ocr-latest", document=document)
|
|
finally:
|
|
NATIVE_OCR.reset()
|
|
assert result is expected
|
|
assert [request.model for request in captured] == ["mistral/mistral-ocr-latest"]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_public_aocr_routes_through_dispatch(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
document: Final[Mapping[str, object]] = {
|
|
"type": "document_url",
|
|
"document_url": "https://example.invalid/document.pdf",
|
|
}
|
|
captured: Final[list[LiteLLMOcrRequest]] = []
|
|
expected: Final = response()
|
|
|
|
async def native(
|
|
request: LiteLLMOcrRequest,
|
|
args: tuple[object, ...],
|
|
kwargs: Mapping[str, object],
|
|
) -> OCRResponse:
|
|
captured.append(request)
|
|
return expected
|
|
|
|
NATIVE_AOCR.override(native)
|
|
monkeypatch.setattr(catalog, "RULES", RUST_RULES)
|
|
public_aocr: Final = cast(Callable[..., Awaitable[OCRResponse]], litellm.aocr)
|
|
try:
|
|
result: Final = await public_aocr(model="mistral/mistral-ocr-latest", document=document)
|
|
finally:
|
|
NATIVE_AOCR.reset()
|
|
assert result is expected
|
|
assert [request.model for request in captured] == ["mistral/mistral-ocr-latest"]
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("model", "custom_llm_provider", "expected"),
|
|
(
|
|
("aws_textract/detect-document-text", None, "native"),
|
|
("detect-document-text", "aws_textract", "native"),
|
|
("mistral/mistral-ocr-latest", None, "python"),
|
|
("mistral/mistral-ocr-latest", "aws_textract", "native"),
|
|
("aws_textract", None, "python"),
|
|
),
|
|
)
|
|
def test_provider_scoped_rule_sees_the_provider_named_by_the_model_prefix(
|
|
model: str, custom_llm_provider: str | None, expected: str
|
|
) -> None:
|
|
rules: Final[Rules] = (
|
|
RouteRule(Route.OCR, Rollout.RUST_REQUIRED, providers=frozenset({"aws_textract"})),
|
|
RouteRule(Route.OCR, Rollout.PYTHON_ONLY),
|
|
)
|
|
document: Final[Mapping[str, object]] = {"type": "image_url", "image_url": "data:image/png;base64,YQ=="}
|
|
kwargs: Final[Mapping[str, object]] = (
|
|
{} if custom_llm_provider is None else {"custom_llm_provider": custom_llm_provider}
|
|
)
|
|
python_response: Final = response("python")
|
|
native_response: Final = response("native")
|
|
|
|
result: Final = _DISPATCH.run(
|
|
(model, document),
|
|
kwargs,
|
|
python=lambda *_args, **_kwargs: python_response,
|
|
binding=ocr_binding(lambda *_args, **_kwargs: native_response),
|
|
native=lambda _hook, _request, _args, _kwargs: native_response,
|
|
rules=rules,
|
|
)
|
|
|
|
assert cast(OCRResponse, result).model == expected # noqa: TID251 # sync dispatch returns the response itself
|