litellm/litellm/rust_bridge/diagnostics.py
devin-ai-integration[bot] b0ac23d385
feat(logger): dispatch Python logging through the Rust diagnostics processor (#42616)
* feat(logger): add shared Rust diagnostics and Python logging bridge

* feat(logger): dispatch diagnostic processing through Rust

* chore: regenerate Cargo.lock after rebase

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* test: allowlist bounded logging tree walkers in recursive detector

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* perf(logger): skip decoding plain access arguments

* test(logger): skip embedded-python logger test when litellm deps are absent

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* style: cargo fmt

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* test: expect NativeDiagnosticProcessor in the native public surface

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix(stub): export NativeDiagnosticProcessor via __new__ in _native.pyi

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* refactor(tracing): rename logger crate and document host sink contract

* test(logger): cover exc, stack, and nested extras in the diagnostic filter

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix(logger): keep rendered redacted line when template scan flags a key pattern

The blanket REDACTED for a changed msg/color template discarded lines
whose rendered form was already redacted by the same pipeline, e.g.
'password=%s' became 'REDACTED' instead of 'password=REDACTED'. Only
fall back to REDACTED when the rendered form did not change either,
which is where interpolation can mangle the key pattern the scrub
would otherwise see.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* ci(rust): install python deps so the logger bridge test runs

The end-to-end bridge test skipped silently when litellm's Python deps
were absent. uv sync --no-install-project installs them without a
maturin build, and PYTHONPATH makes them visible to the embedded
interpreter

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

---------

Co-authored-by: Yujong Lee <yujong@berri.ai>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-22 18:44:15 -07:00

59 lines
2 KiB
Python

from __future__ import annotations
from collections.abc import Callable, Sequence
from functools import lru_cache
from typing import Final, Protocol, TypeVar, cast
from litellm.rust_bridge.bindings import NativeBinding
ResultT: Final = TypeVar("ResultT")
class NativeDiagnosticProcessor(Protocol):
def redact_text(self, text: str) -> str: ...
def redact_structured_text(self, key: str | None, text: str) -> str: ...
def redact_client_message(self, text: str) -> str: ...
def process_diagnostic(
self,
message: str,
exception: str | None,
stack: str | None,
leaves: tuple[tuple[str | None, str], ...],
policy: tuple[bool, int, int],
) -> tuple[str, str | None, str | None, Sequence[str], bool]: ...
def scrub_access_arguments(self, arguments: tuple[str, ...]) -> Sequence[str]: ...
class NativeDiagnosticFactory(Protocol):
def __call__(self, minimum_custom_key_length: int) -> NativeDiagnosticProcessor: ...
def _as_factory(value: object) -> NativeDiagnosticFactory | None:
if not isinstance(value, type):
return None
return cast(NativeDiagnosticFactory, value) # cast-ok: PyO3 factory must be a type
PROCESSOR: Final = NativeBinding("NativeDiagnosticProcessor", validate=_as_factory)
@lru_cache(maxsize=4)
def _construct(factory: NativeDiagnosticFactory, minimum_custom_key_length: int) -> NativeDiagnosticProcessor:
return factory(minimum_custom_key_length)
def run(native: Callable[[NativeDiagnosticProcessor], ResultT], python: Callable[[], ResultT]) -> ResultT:
from litellm.constants import MINIMUM_CUSTOM_KEY_LENGTH
from litellm.rust_bridge.catalog import LoggerContext, decision
from litellm.rust_bridge.configuration import Decision
selected: Final = decision(LoggerContext())
if selected is Decision.PYTHON:
return python()
factory: Final = PROCESSOR.load()
if factory is None:
return python()
try:
return native(_construct(factory, MINIMUM_CUSTOM_KEY_LENGTH))
except Exception:
return python()