mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-14 09:03:16 +00:00
fix(messages): log the caller's messages params on native calls like python
Python logs the requested optional params, taken before the provider transform, as model_parameters. The native handler logged the transformed body, so a reasoning_effort request was logged as thinking and output_config. The request context now carries the requested params. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013FDwtkaNTifWAP4W7JZavE
This commit is contained in:
parent
1d46ade6da
commit
a67684d814
3 changed files with 74 additions and 3 deletions
|
|
@ -131,6 +131,7 @@ async fn provider_call(
|
|||
let transform_context =
|
||||
MessagesTransformContext::new(shaping.capabilities, shaping.settings.drop_params);
|
||||
let filtered = requested_optional_params(sanitized, provider, &transform_context)?;
|
||||
let requested_params = serde_json::to_value(&filtered.params).map_err(serialize_failure)?;
|
||||
let trimmed =
|
||||
without_additional_drop_params(filtered, &shaping.settings.additional_drop_params)?;
|
||||
|
||||
|
|
@ -173,7 +174,7 @@ async fn provider_call(
|
|||
let request_context = RequestContext {
|
||||
model: body.model.clone(),
|
||||
custom_llm_provider: provider.as_str().to_string(),
|
||||
optional_params: serde_json::to_value(&body.params).map_err(serialize_failure)?,
|
||||
optional_params: requested_params,
|
||||
secret_fields: Vec::new(),
|
||||
api_key: api_key.map(SecretValue::new),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -8,7 +8,9 @@ use litellm_host::{
|
|||
};
|
||||
use litellm_inference_messages::{MessagesCallResponse, route::Messages};
|
||||
use litellm_inference_testing::{RecordingSecrets, no_secrets};
|
||||
use litellm_llms::base_llm::messages::context::MessagesModelCapabilities as AnthropicModelCapabilities;
|
||||
use litellm_llms::base_llm::messages::context::{
|
||||
MessagesModelCapabilities as AnthropicModelCapabilities, SupportedEffortTiers,
|
||||
};
|
||||
use rstest::rstest;
|
||||
|
||||
use super::*;
|
||||
|
|
@ -419,7 +421,7 @@ async fn no_raw_response_is_emitted_for_a_stream_or_a_failure(
|
|||
assert!(host.raw_responses().is_empty());
|
||||
}
|
||||
|
||||
/// Python logs `optional_params` as what it is about to send, so a dropped param must
|
||||
/// Python logs `optional_params` after its requested-param gate, so a dropped param must
|
||||
/// not resurface in callbacks.
|
||||
#[rstest]
|
||||
#[tokio::test]
|
||||
|
|
@ -450,3 +452,54 @@ async fn the_request_context_carries_the_shaped_params_without_model_or_messages
|
|||
.unwrap_or_else(|seen| panic!("before_provider_request runs once, saw {}", seen.len()));
|
||||
assert_eq!(optional_params, json!({"max_tokens": 16}));
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case::reasoning_effort_on_an_adaptive_model(
|
||||
AnthropicModelCapabilities {
|
||||
supports_reasoning: true,
|
||||
supports_adaptive_thinking: true,
|
||||
supports_output_config: true,
|
||||
effort_tiers: SupportedEffortTiers { low: true, ..SupportedEffortTiers::default() },
|
||||
..AnthropicModelCapabilities::default()
|
||||
},
|
||||
json!({"reasoning_effort": "low"})
|
||||
)]
|
||||
#[case::adaptive_payload_on_a_legacy_model(
|
||||
AnthropicModelCapabilities {
|
||||
supports_reasoning: true,
|
||||
..AnthropicModelCapabilities::default()
|
||||
},
|
||||
json!({"max_tokens": 3000, "thinking": {"type": "adaptive"}, "output_config": {"effort": "high"}})
|
||||
)]
|
||||
#[tokio::test]
|
||||
async fn the_request_context_carries_the_requested_reasoning_not_its_translation(
|
||||
call: MessagesCall,
|
||||
#[case] capabilities: AnthropicModelCapabilities,
|
||||
#[case] fields: Value,
|
||||
) {
|
||||
let upstream = upstream([message_response()]).await;
|
||||
let host = RecordingHost::passthrough(authenticated(
|
||||
MessagesCall {
|
||||
shaping: MessagesShaping {
|
||||
capabilities,
|
||||
..MessagesShaping::default()
|
||||
},
|
||||
..with_fields(call, fields.clone())
|
||||
},
|
||||
upstream.uri(),
|
||||
));
|
||||
|
||||
run_through(&host).await.expect("messages call succeeds");
|
||||
|
||||
let [optional_params] = <[Value; 1]>::try_from(host.optional_params.into_inner().unwrap())
|
||||
.unwrap_or_else(|seen| panic!("before_provider_request runs once, saw {}", seen.len()));
|
||||
assert_eq!(
|
||||
optional_params,
|
||||
Value::Object(
|
||||
object(json!({"max_tokens": 16}))
|
||||
.into_iter()
|
||||
.chain(object(fields))
|
||||
.collect()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -140,6 +140,23 @@ async def test_pre_api_call_sees_the_shaped_optional_params(server: RecordingSer
|
|||
assert optional_params["max_tokens"] == sent["max_tokens"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_success_event_logs_the_requested_reasoning_effort_not_its_translation(server: RecordingServer) -> None:
|
||||
recorder: Final = RecordingLogger()
|
||||
|
||||
with rebound(litellm, "callbacks", [recorder]):
|
||||
response: Final = await call(server, reasoning_effort="low")
|
||||
success: Final = await recorder.wait_for_async("async_log_success_event")
|
||||
|
||||
assert_served_natively(server, response)
|
||||
sent: Final = server.requests[0].body
|
||||
assert isinstance(sent, dict)
|
||||
assert sent["output_config"] == {"effort": "low"}
|
||||
assert isinstance(success[0].kwargs, dict)
|
||||
logged: Final = _OBJECT.validate_python(success[0].kwargs["standard_logging_object"])
|
||||
assert logged["model_parameters"] == {"max_tokens": 64, "stream": False, "reasoning_effort": "low"}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_logging_hook_and_redaction_edits_reach_the_async_success_event(server: RecordingServer) -> None:
|
||||
class Rewrite(HookRecorder):
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue