fix(harness): trace OCR Python and native dispatch paths

This commit is contained in:
Yujong Lee 2026-09-14 12:03:54 -07:00
parent d4869ba710
commit 209fc7afb0
5 changed files with 132 additions and 58 deletions

View file

@ -64,6 +64,7 @@ class TraceScenario:
fixture: Callable[[Engine, str], RouteFixture]
mappings: tuple[TraceMapping, ...]
asynchronous: bool
python_rust_enabled: bool = False
@dataclass(frozen=True, slots=True)

View file

@ -1,10 +1,13 @@
from __future__ import annotations
import asyncio
import os
from collections.abc import AsyncIterable, Awaitable, Iterable
from contextlib import nullcontext
from dataclasses import dataclass
from pathlib import Path
from typing import Final, Protocol, cast
from unittest.mock import patch
from ....shared.parity.replay import replay_server
from ....shared.reporting.models import Surface
@ -105,7 +108,7 @@ def _collect(
def collect_trace(
spec: RouteSpec, engine: Engine, *, asynchronous: bool
spec: RouteSpec, engine: Engine, *, asynchronous: bool, python_rust_enabled: bool = False
) -> tuple[FunctionTraceEvent, ...] | TraceExecutionFailure:
function: Final = _entrypoint(spec, engine, asynchronous=asynchronous)
if isinstance(function, TraceExecutionFailure):
@ -126,7 +129,13 @@ def collect_trace(
expected_failure=base_fixture.expected_failure,
consume_stream=base_fixture.consume_stream,
)
collected: Final = _collect(function, fixture, engine, asynchronous=asynchronous)
environment: Final = (
patch.dict(os.environ, {"LITELLM_RUST": "1" if python_rust_enabled else "0"})
if engine == "python"
else nullcontext()
)
with environment:
collected: Final = _collect(function, fixture, engine, asynchronous=asynchronous)
provider.take_requests(len(fixture.provider_responses))
except Exception as error:
return TraceExecutionFailure(engine, f"{type(error).__name__}: {error}")
@ -159,7 +168,14 @@ def execute_trace(
fixture=scenario.fixture,
)
python_trace: Final = (
collect_trace(scenario_route, "python", asynchronous=scenario.asynchronous) if engine != "rust" else ()
collect_trace(
scenario_route,
"python",
asynchronous=scenario.asynchronous,
python_rust_enabled=scenario.python_rust_enabled,
)
if engine != "rust"
else ()
)
rust_trace: Final = (
collect_trace(scenario_route, "rust", asynchronous=scenario.asynchronous) if engine != "python" else ()

View file

@ -1,7 +1,7 @@
from __future__ import annotations
import json
from typing import Final, cast
from typing import Final
from .....shared.parity.recorded_http import HttpHeader, RecordedHttpResponse
from .....shared.tracing.steps import Engine, mapping
@ -53,6 +53,23 @@ ASYNC_MAPPINGS: Final = (
),
)
PUBLIC_RUST_DISPATCH_MAPPINGS: Final = (
mapping(span="public_sdk_entrypoint", python_frame=r"ocr/main\.py:\d+ a?ocr$"),
mapping(span="public_request", python_frame=r"ocr/main\.py:\d+ _public_request$"),
mapping(span="bind_request", python_frame=r"ocr/main\.py:\d+ _bind_request$"),
mapping(span="rust_ocr_enabled", python_frame=r"rust_bridge/configuration\.py:\d+ rust_ocr_enabled$"),
mapping(span="select_native_ocr", python_frame=r"rust_bridge/ocr_lifecycle\.py:\d+ select$"),
mapping(span="load_native_bridge", python_frame=r"rust_bridge/bindings\.py:\d+ NativeBinding\.load$"),
mapping(span="native_call_setup", python_frame=r"rust_bridge/lifecycle\.py:\d+ setup$"),
mapping(span="native_response", python_frame=r"rust_bridge/ocr\.py:\d+ _response$"),
mapping(span="native_call_finalize", python_frame=r"rust_bridge/lifecycle\.py:\d+ finalize$"),
mapping(
span="native_success_bookkeeping",
python_frame=r"rust_bridge/lifecycle\.py:\d+ success_bookkeeping$",
),
*(mapping(rust_span=item.rust) for item in SYNC_MAPPINGS if item.rust is not None),
)
CALLBACK_SUCCESS_SYNC_MAPPINGS: Final = (*SYNC_MAPPINGS, SUCCESS_CALLBACK_SYNC_MAPPING)
CALLBACK_SUCCESS_ASYNC_MAPPINGS: Final = (*ASYNC_MAPPINGS, SUCCESS_CALLBACK_ASYNC_MAPPING)
CALLBACK_FAILURE_SYNC_MAPPINGS: Final = (
@ -164,23 +181,6 @@ def _azure_fixture(engine: Engine, _base_url: str) -> RouteFixture:
)
def _vertex_fixture(engine: Engine, _base_url: str) -> RouteFixture:
fixture: Final = _fixture(
engine,
"vertex_ai/mistral-ocr-maas",
{"type": "image_url", "image_url": "data:image/png;base64,aGVsbG8="},
)
vertex: Final = {"vertex_project": "trace-project", "vertex_location": "us-central1"}
optional_params: Final = cast(dict[str, object], fixture.kwargs.get("optional_params", {}))
return RouteFixture(
kwargs={
**fixture.kwargs,
**({"optional_params": {**optional_params, **vertex}} if engine == "rust" else vertex),
},
provider_responses=fixture.provider_responses,
)
def _vertex_deepseek_fixture(engine: Engine, _base_url: str) -> RouteFixture:
vertex: Final = {"vertex_project": "trace-project", "vertex_location": "us-central1"}
return RouteFixture(
@ -204,6 +204,28 @@ def _vertex_deepseek_fixture(engine: Engine, _base_url: str) -> RouteFixture:
)
def _cohere_fixture(engine: Engine, _base_url: str) -> RouteFixture:
return RouteFixture(
kwargs={
"model": "cohere/parse-v5.0",
"document": {"type": "image_url", "image_url": "data:image/png;base64,aGVsbG8="},
**({"optional_params": {"output_format": "blocks"}} if engine == "rust" else {"output_format": "blocks"}),
},
provider_responses=(
RecordedHttpResponse.from_bytes(
200,
(HttpHeader(name="content-type", value="application/json"),),
json.dumps(
{
"pages": [{"index": 0, "blocks": [{"type": "text", "text": {"content": "hello"}}]}],
"meta": {"billed_units": {"pages": 1}},
}
).encode(),
),
),
)
def _azure_document_intelligence_fixture(engine: Engine, base_url: str) -> RouteFixture:
completed: Final = json.dumps(
{
@ -249,30 +271,6 @@ def _azure_document_intelligence_fixture(engine: Engine, base_url: str) -> Route
)
VERTEX_COMMON_MAPPINGS: Final = (
*COMMON_MAPPINGS[:7],
mapping(
rust_span="transform_ocr_request",
python_frame=(
r"VertexAIOCRConfig\.(?:async_)?transform_ocr_request$"
r"|MistralOCRConfig\.transform_ocr_request$"
),
),
COMMON_MAPPINGS[-1],
)
VERTEX_SYNC_MAPPINGS: Final = (
*VERTEX_COMMON_MAPPINGS,
mapping(rust_span="execute_ocr_provider_call", python_frame=r"BaseLLMHTTPHandler\.ocr$"),
mapping(span="python_transform_ocr_response_wrapper", python_frame=r"BaseLLMHTTPHandler\._transform_ocr_response$"),
mapping(rust_span="transform_ocr_response", python_frame=r"MistralOCRConfig\.transform_ocr_response$"),
)
VERTEX_ASYNC_MAPPINGS: Final = (
*VERTEX_COMMON_MAPPINGS,
mapping(span="python_ocr_wrapper", python_frame=r"BaseLLMHTTPHandler\.ocr$"),
mapping(rust_span="execute_ocr_provider_call", python_frame=r"BaseLLMHTTPHandler\.async_ocr$"),
mapping(rust_span="transform_ocr_response", python_frame=r"MistralOCRConfig\.transform_ocr_response$"),
)
DEEPSEEK_COMMON_MAPPINGS: Final = (
mapping(rust_span="ocr", python_frame=r"ocr/main\.py:\d+ a?ocr$"),
mapping(rust_span="prepare_ocr_call", python_frame=r"ocr/main\.py:\d+ _prepare_ocr_request$"),
@ -352,6 +350,20 @@ DOCUMENT_INTELLIGENCE_ASYNC_MAPPINGS: Final = (
mapping(span="python_poll_http_request", python_frame=r"AsyncHTTPHandler\.get$"),
)
COHERE_COMMON_MAPPINGS: Final = (
*COMMON_MAPPINGS[:7],
mapping(
rust_span="transform_ocr_request",
python_frame=r"CohereParseConfig\.(?:async_)?transform_ocr_request$",
),
COMMON_MAPPINGS[-1],
mapping(rust_span="transform_ocr_response", python_frame=r"CohereParseConfig\.transform_ocr_response$"),
)
COHERE_ASYNC_MAPPINGS: Final = (
*COHERE_COMMON_MAPPINGS,
mapping(span="python_ocr_wrapper", python_frame=r"BaseLLMHTTPHandler\.ocr$"),
mapping(rust_span="execute_ocr_provider_call", python_frame=r"BaseLLMHTTPHandler\.async_ocr$"),
)
SPEC: Final = RouteSpec("ocr", ("ocr", "aocr"), ("ocr", "aocr"), _mistral_fixture)
TRACE_SUITE: Final = TraceSuite(
@ -417,18 +429,6 @@ TRACE_SUITE: Final = TraceSuite(
mappings=(*DOCUMENT_INTELLIGENCE_ASYNC_MAPPINGS, IGNORED_SUCCESS_CALLBACK_MAPPING),
asynchronous=True,
),
TraceScenario(
name="sync-vertex-ai",
fixture=_vertex_fixture,
mappings=(*VERTEX_SYNC_MAPPINGS, IGNORED_SUCCESS_CALLBACK_MAPPING),
asynchronous=False,
),
TraceScenario(
name="async-vertex-ai",
fixture=_vertex_fixture,
mappings=(*VERTEX_ASYNC_MAPPINGS, IGNORED_SUCCESS_CALLBACK_MAPPING),
asynchronous=True,
),
TraceScenario(
name="sync-vertex-deepseek",
fixture=_vertex_deepseek_fixture,
@ -441,5 +441,25 @@ TRACE_SUITE: Final = TraceSuite(
mappings=(*DEEPSEEK_ASYNC_MAPPINGS, IGNORED_SUCCESS_CALLBACK_MAPPING),
asynchronous=True,
),
TraceScenario(
name="async-cohere",
fixture=_cohere_fixture,
mappings=(*COHERE_ASYNC_MAPPINGS, IGNORED_SUCCESS_CALLBACK_MAPPING),
asynchronous=True,
),
TraceScenario(
name="sync-public-rust-dispatch",
fixture=_mistral_fixture,
mappings=PUBLIC_RUST_DISPATCH_MAPPINGS,
asynchronous=False,
python_rust_enabled=True,
),
TraceScenario(
name="async-public-rust-dispatch",
fixture=_mistral_fixture,
mappings=PUBLIC_RUST_DISPATCH_MAPPINGS,
asynchronous=True,
python_rust_enabled=True,
),
),
)

View file

@ -16,6 +16,7 @@ def _suite(module: str) -> TraceSuite:
def test_core_sdk_scenario_matrix_keeps_distinct_migration_paths() -> None:
chat: Final = _suite("tests.rust-python-harness.strategies.trace_parity.sdk.chat_completions.case")
messages: Final = _suite("tests.rust-python-harness.strategies.trace_parity.sdk.messages.case")
ocr: Final = _suite("tests.rust-python-harness.strategies.trace_parity.sdk.ocr.case")
responses: Final = _suite("tests.rust-python-harness.strategies.trace_parity.sdk.responses.case")
assert {(scenario.name, scenario.asynchronous) for scenario in chat.scenarios} >= {
@ -38,6 +39,11 @@ def test_core_sdk_scenario_matrix_keeps_distinct_migration_paths() -> None:
("async-bedrock-invalid-thinking-retry", True),
("sync-unsupported", False),
}
assert {(scenario.name, scenario.asynchronous) for scenario in ocr.scenarios} >= {
("async-cohere", True),
("sync-public-rust-dispatch", False),
("async-public-rust-dispatch", True),
}
assert {(scenario.name, scenario.asynchronous) for scenario in responses.scenarios} >= {
("sync-openai", False),
("async-openai", True),

View file

@ -1,7 +1,9 @@
from __future__ import annotations
import importlib
import os
from pathlib import Path
from types import SimpleNamespace
from typing import Final, cast
import pytest
@ -10,11 +12,12 @@ import litellm
from ...shared.reporting.models import Coverage, HarnessCase, HarnessRun, RunStatus, SdkFunction, Surface
from ...shared.reporting.strategy import ModuleCaseSpec
from ...shared.tracing.profiler import FunctionTraceEvent
from ...shared.tracing.steps import Engine, PipelineStep
from .models import GatewayRouteSpec, RouteFixture, RouteSpec, TraceScenario, TraceSuite
from .reporting import TraceArtifact
from .runner import run_trace_cases, run_trace_scenario, runner_selection, scenario_nodeids, validate_trace_suite
from .sdk.execution import execute_trace
from .sdk.execution import SdkCall, collect_trace, execute_trace
def _fixture(_engine: Engine, _base_url: str) -> RouteFixture:
@ -77,6 +80,34 @@ def test_python_engine_skips_native_bridge(monkeypatch: pytest.MonkeyPatch, tmp_
assert selected == [(frozenset({"mistral"}), "python")]
def test_python_trace_controls_native_ocr_dispatch(monkeypatch: pytest.MonkeyPatch) -> None:
execution: Final = importlib.import_module("tests.rust-python-harness.strategies.trace_parity.sdk.execution")
route: Final = RouteSpec("ocr", ("ocr", "aocr"), ("ocr", "aocr"), _fixture)
observed: list[str | None] = []
def collect(
_function: SdkCall,
_fixture: RouteFixture,
_engine: Engine,
*,
asynchronous: bool,
) -> SimpleNamespace:
observed.append(os.environ.get("LITELLM_RUST"))
return SimpleNamespace(
events=(FunctionTraceEvent(0, None, "aocr" if asynchronous else "ocr"),),
error=None,
)
monkeypatch.setenv("LITELLM_RUST", "1")
monkeypatch.setattr(execution, "_collect", collect)
collect_trace(route, "python", asynchronous=False)
collect_trace(route, "python", asynchronous=True, python_rust_enabled=True)
assert observed == ["0", "1"]
assert os.environ["LITELLM_RUST"] == "1"
def test_expected_provider_failure_omits_feedback_banner(
capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch
) -> None: