From 0b52efe0bfdbb28168e06d4b47573bc9eb9f6cb8 Mon Sep 17 00:00:00 2001 From: narmstrong-berri Date: Fri, 6 Nov 2026 07:25:27 +0000 Subject: [PATCH] fix(messages): keep the provider out of a shared logger's litellm_params on native messages calls Python's messages handler only records custom_llm_provider in model_call_details. The native adapter also wrote it into litellm_params after every provider result, and the proxy reuses one logger across router hops, so a fallback hop's pre_call log showed the provider where Python shows null. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013FDwtkaNTifWAP4W7JZavE --- .../callbacks-legacy-python/src/adapter.rs | 52 ++++++++++++++++++- .../messages/test_callbacks.py | 32 +++++++++++- 2 files changed, 81 insertions(+), 3 deletions(-) diff --git a/litellm-rust/crates/callbacks-legacy-python/src/adapter.rs b/litellm-rust/crates/callbacks-legacy-python/src/adapter.rs index 1bb84184e4c..3c729930a1e 100644 --- a/litellm-rust/crates/callbacks-legacy-python/src/adapter.rs +++ b/litellm-rust/crates/callbacks-legacy-python/src/adapter.rs @@ -525,7 +525,11 @@ impl LegacyLogging { .getattr("litellm_params")? .cast_into::()? .copy()?; - params.set_item("custom_llm_provider", &facts.provider.provider)?; + if self.operation != LoggingOperation::Messages { + // Python's Messages handler hands the provider to the logger beside its params, never + // in them, and the proxy's logger carries over to the next fallback hop. + params.set_item("custom_llm_provider", &facts.provider.provider)?; + } crate::python::Logging::Update.call( py, ( @@ -1142,6 +1146,52 @@ assert logger.update['custom_llm_provider'] == 'edenai', logger.update }); } + #[rstest] + #[case::messages(crate::LoggingOperation::Messages, false)] + #[case::ocr(crate::LoggingOperation::Ocr, true)] + fn result_ready_puts_the_provider_in_the_logged_params_except_for_messages( + #[case] operation: crate::LoggingOperation, + #[case] in_params: bool, + ) { + Python::initialize(); + Python::attach(|py| { + let locals = namespace(py, PAYLOAD_LOGGER); + run( + py, + &locals, + c" +logger.litellm_params = {} +logger.optional_params = {} +logger.model_call_details = {} +logger._provider_reported_cost = None +", + ); + let mut logging = LegacyLogging { + logger: Some(PythonLogger::new(local(&locals, "logger").unbind())), + operation, + ..legacy_call(py, &locals, false) + }; + let facts = litellm_host::interceptors::ExecutionFacts { + provider: litellm_host::interceptors::ProviderIdentity { + model: "native-test".into(), + provider: "anthropic".into(), + }, + cost: litellm_host::interceptors::Cost::Deferred, + source: litellm_host::interceptors::ResultSource::Provider, + }; + logging.result_ready(py, &facts).unwrap(); + locals.set_item("in_params", in_params).unwrap(); + run( + py, + &locals, + c" +assert logger.update['custom_llm_provider'] == 'anthropic', logger.update +assert ('custom_llm_provider' in logger.update['litellm_params']) is in_params, logger.update +", + ); + }); + } + const DOCUMENT: &str = "data:application/pdf;base64,YWJj"; const EDITED: &str = "data:application/pdf;base64,ZWRpdGVk"; diff --git a/tests/test_litellm_rust/messages/test_callbacks.py b/tests/test_litellm_rust/messages/test_callbacks.py index a3c03898a12..58a262caf0d 100644 --- a/tests/test_litellm_rust/messages/test_callbacks.py +++ b/tests/test_litellm_rust/messages/test_callbacks.py @@ -10,14 +10,15 @@ from pydantic import TypeAdapter import litellm from litellm.integrations.argilla import ArgillaLogger from litellm.integrations.custom_logger import CustomLogger +from litellm.litellm_core_utils.litellm_logging import Logging from litellm.types.integrations.argilla import ArgillaItem from litellm.types.utils import ModelResponse, StandardLoggingPayload -from tests.test_litellm_rust.messages.data import BAD_REQUEST, MESSAGES_RESPONSE, RATE_LIMITED, call +from tests.test_litellm_rust.messages.data import BAD_REQUEST, MESSAGES_RESPONSE, RATE_LIMITED, REQUEST, call from tests.test_litellm_rust.support.callback_recorder import HookRecorder, RecordingLogger, request_body from tests.test_litellm_rust.support.isolation import rebound from tests.test_litellm_rust.support.recording_server import RecordingServer, ResponseSpec, relaxed from tests.test_litellm_rust.support.results import response_id, with_id -from tests.test_litellm_rust.support.routing import assert_served_natively +from tests.test_litellm_rust.support.routing import CALL_ID, assert_served_natively pytestmark = pytest.mark.requires_rust_extension @@ -110,6 +111,33 @@ async def test_pre_api_call_sees_the_wire_body(server: RecordingServer) -> None: assert request_body(pre_call) == server.requests[0].body +@pytest.mark.asyncio +async def test_pre_api_call_of_a_retried_call_keeps_the_provider_out_of_the_litellm_params( + server: RecordingServer, +) -> None: + recorder: Final = RecordingLogger() + logging_obj: Final = Logging( + model=REQUEST["model"], + messages=[], + stream=False, + start_time=datetime.now(), + call_type="anthropic_messages", + litellm_call_id=CALL_ID, + function_id=CALL_ID, + dynamic_input_callbacks=[recorder], + ) + + await call(server, litellm_logging_obj=logging_obj) + await call(server, litellm_logging_obj=logging_obj) + + pre_calls: Final = recorder.wait_for("log_pre_api_call", count=2) + + assert_served_natively(server, count=2) + for pre_call in pre_calls: + assert isinstance(pre_call.kwargs, dict) + assert "custom_llm_provider" not in _OBJECT.validate_python(pre_call.kwargs["litellm_params"]) + + @pytest.mark.asyncio async def test_pre_api_call_edits_reach_the_provider(server: RecordingServer) -> None: class Edit(CustomLogger):