mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-07 08:26:10 +00:00
fix(logging): handle dict result.response in streaming /v1/responses spend log
On the streaming /v1/responses spend-log path result.response can arrive as a dict rather than a ResponsesAPIResponse, so the unified-usage step raised AttributeError on result.response.usage. Read usage via .get on a dict (attr otherwise) and write the transformed usage back through the matching branch. Fixes #29913.
This commit is contained in:
parent
3c24f37502
commit
8253bd7617
2 changed files with 170 additions and 302 deletions
|
|
@ -3670,21 +3670,23 @@ class Logging(LiteLLMLoggingBaseClass):
|
|||
(ResponseCompletedEvent, ResponseIncompleteEvent, ResponseFailedEvent),
|
||||
):
|
||||
## return unified Usage object
|
||||
if isinstance(result.response.usage, ResponseAPIUsage):
|
||||
transformed_usage: Final = ResponseAPILoggingUtils._transform_response_api_usage_to_chat_usage(
|
||||
result.response.usage
|
||||
)
|
||||
# result.response may be a dict on the streaming /v1/responses spend-log
|
||||
# path (#29913); a bare `.usage` attribute access would raise there.
|
||||
resp: Final = result.response
|
||||
usage: Final = resp.get("usage") if isinstance(resp, dict) else getattr(resp, "usage", None)
|
||||
if isinstance(usage, ResponseAPIUsage):
|
||||
transformed_usage: Final = ResponseAPILoggingUtils._transform_response_api_usage_to_chat_usage(usage)
|
||||
# Set as dict instead of Usage object so model_dump() serializes it correctly
|
||||
setattr(
|
||||
result.response,
|
||||
"usage",
|
||||
(
|
||||
transformed_usage.model_dump()
|
||||
if hasattr(transformed_usage, "model_dump")
|
||||
else dict(transformed_usage)
|
||||
),
|
||||
new_usage: Final = (
|
||||
transformed_usage.model_dump()
|
||||
if hasattr(transformed_usage, "model_dump")
|
||||
else dict(transformed_usage)
|
||||
)
|
||||
return result.response
|
||||
if isinstance(resp, dict):
|
||||
resp["usage"] = new_usage
|
||||
else:
|
||||
setattr(resp, "usage", new_usage)
|
||||
return resp
|
||||
elif isinstance(result, InteractionsAPIStreamingResponse):
|
||||
return self._assemble_completed_interaction_response(result)
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -273,9 +273,7 @@ def test_response_cost_calculator_uses_router_model_id_from_litellm_metadata():
|
|||
|
||||
assert cost is not None, "Cost should not be None"
|
||||
expected_cost = (10 * custom_input_cost) + (5 * custom_output_cost)
|
||||
assert cost == pytest.approx(
|
||||
expected_cost
|
||||
), f"Expected {expected_cost}, got {cost}"
|
||||
assert cost == pytest.approx(expected_cost), f"Expected {expected_cost}, got {cost}"
|
||||
finally:
|
||||
litellm.model_cost.pop(custom_model_id, None)
|
||||
|
||||
|
|
@ -872,13 +870,8 @@ async def test_datadog_logger_not_shadowed_by_llm_obs(monkeypatch):
|
|||
|
||||
# Regression check: we expect a distinct DataDogLogger, not the LLM Obs logger
|
||||
assert type(datadog_logger) is DataDogLogger
|
||||
assert any(
|
||||
isinstance(cb, DataDogLLMObsLogger)
|
||||
for cb in logging_module._in_memory_loggers
|
||||
)
|
||||
assert any(
|
||||
type(cb) is DataDogLogger for cb in logging_module._in_memory_loggers
|
||||
)
|
||||
assert any(isinstance(cb, DataDogLLMObsLogger) for cb in logging_module._in_memory_loggers)
|
||||
assert any(type(cb) is DataDogLogger for cb in logging_module._in_memory_loggers)
|
||||
finally:
|
||||
logging_module._in_memory_loggers.clear()
|
||||
|
||||
|
|
@ -889,9 +882,7 @@ async def test_logfire_logger_accepts_env_vars_for_base_url(monkeypatch):
|
|||
|
||||
# Required env vars for Logfire integration
|
||||
monkeypatch.setenv("LOGFIRE_TOKEN", "test-token")
|
||||
monkeypatch.setenv(
|
||||
"LOGFIRE_BASE_URL", "https://logfire-api-custom.pydantic.dev"
|
||||
) # no trailing slash on purpose
|
||||
monkeypatch.setenv("LOGFIRE_BASE_URL", "https://logfire-api-custom.pydantic.dev") # no trailing slash on purpose
|
||||
|
||||
# Import after env vars are set (important if module-level caching exists)
|
||||
from litellm.integrations.opentelemetry import OpenTelemetry # logger class
|
||||
|
|
@ -910,9 +901,7 @@ async def test_logfire_logger_accepts_env_vars_for_base_url(monkeypatch):
|
|||
|
||||
# Sanity: we got the right logger type and it is cached
|
||||
assert type(logger) is OpenTelemetry
|
||||
assert any(
|
||||
type(cb) is OpenTelemetry for cb in logging_module._in_memory_loggers
|
||||
)
|
||||
assert any(type(cb) is OpenTelemetry for cb in logging_module._in_memory_loggers)
|
||||
|
||||
# Core regression check: base URL env var should influence the exporter endpoint.
|
||||
#
|
||||
|
|
@ -923,9 +912,7 @@ async def test_logfire_logger_accepts_env_vars_for_base_url(monkeypatch):
|
|||
or getattr(logger, "config", None)
|
||||
or getattr(logger, "_otel_config", None)
|
||||
)
|
||||
assert (
|
||||
cfg is not None
|
||||
), "Expected OpenTelemetry logger to keep an otel config on the instance"
|
||||
assert cfg is not None, "Expected OpenTelemetry logger to keep an otel config on the instance"
|
||||
|
||||
endpoint = getattr(cfg, "endpoint", None) or getattr(cfg, "otlp_endpoint", None)
|
||||
assert endpoint is not None, "Expected otel config to expose the OTLP endpoint"
|
||||
|
|
@ -1083,9 +1070,7 @@ async def test_logging_non_streaming_request():
|
|||
|
||||
# Use the filtered call for assertions
|
||||
call_args = calls_with_expected_input[0]
|
||||
standard_logging_object = call_args.kwargs["kwargs"][
|
||||
"standard_logging_object"
|
||||
]
|
||||
standard_logging_object = call_args.kwargs["kwargs"]["standard_logging_object"]
|
||||
assert standard_logging_object["stream"] is not True
|
||||
finally:
|
||||
# Restore original callbacks to ensure test isolation
|
||||
|
|
@ -1103,18 +1088,14 @@ async def test_logging_non_streaming_request():
|
|||
"agenerate_content_stream",
|
||||
],
|
||||
)
|
||||
def test_success_handler_skips_sync_callbacks_for_async_requests(
|
||||
logging_obj, async_flag
|
||||
):
|
||||
def test_success_handler_skips_sync_callbacks_for_async_requests(logging_obj, async_flag):
|
||||
"""Ensure sync success callbacks are skipped when async call type flags are set."""
|
||||
from litellm.integrations.custom_logger import CustomLogger
|
||||
|
||||
class DummyLogger(CustomLogger):
|
||||
pass
|
||||
|
||||
logging_obj.stream = (
|
||||
False # simulate non-streaming request where sync callbacks would normally run
|
||||
)
|
||||
logging_obj.stream = False # simulate non-streaming request where sync callbacks would normally run
|
||||
logging_obj.model_call_details["litellm_params"] = {async_flag: True}
|
||||
logging_obj.litellm_params = logging_obj.model_call_details["litellm_params"]
|
||||
|
||||
|
|
@ -1190,21 +1171,11 @@ def test_success_handler_runs_sync_callbacks_for_sync_requests(logging_obj, call
|
|||
def test_is_sync_litellm_request():
|
||||
assert LitellmLogging._is_sync_litellm_request({}) is True
|
||||
assert LitellmLogging._is_sync_litellm_request({"acompletion": True}) is False
|
||||
assert (
|
||||
LitellmLogging._is_sync_litellm_request({"allm_passthrough_route": True})
|
||||
is False
|
||||
)
|
||||
assert (
|
||||
LitellmLogging._is_sync_litellm_request({"aanthropic_messages": True}) is False
|
||||
)
|
||||
assert LitellmLogging._is_sync_litellm_request({"allm_passthrough_route": True}) is False
|
||||
assert LitellmLogging._is_sync_litellm_request({"aanthropic_messages": True}) is False
|
||||
assert LitellmLogging._is_sync_litellm_request({"agenerate_content": True}) is False
|
||||
assert (
|
||||
LitellmLogging._is_sync_litellm_request({"agenerate_content_stream": True})
|
||||
is False
|
||||
)
|
||||
assert (
|
||||
LitellmLogging._is_sync_litellm_request({"aanthropic_messages": False}) is True
|
||||
)
|
||||
assert LitellmLogging._is_sync_litellm_request({"agenerate_content_stream": True}) is False
|
||||
assert LitellmLogging._is_sync_litellm_request({"aanthropic_messages": False}) is True
|
||||
|
||||
|
||||
def test_get_litellm_params_propagates_allm_passthrough_route():
|
||||
|
|
@ -1251,9 +1222,7 @@ async def test_dispatch_success_handlers_invokes_callbacks_once_for_final_stream
|
|||
logging_obj.model_call_details["litellm_params"] = {"acompletion": True}
|
||||
|
||||
with (
|
||||
patch.object(
|
||||
mock_callback, "async_log_success_event", new_callable=AsyncMock
|
||||
) as mock_async_log,
|
||||
patch.object(mock_callback, "async_log_success_event", new_callable=AsyncMock) as mock_async_log,
|
||||
patch.object(mock_callback, "log_success_event") as mock_sync_log,
|
||||
patch.object(
|
||||
logging_obj,
|
||||
|
|
@ -1314,9 +1283,7 @@ async def test_dispatch_success_handlers_sync_path_invokes_callback_once_for_fin
|
|||
|
||||
with (
|
||||
patch.object(mock_callback, "log_success_event") as mock_sync_log,
|
||||
patch.object(
|
||||
mock_callback, "async_log_success_event", new_callable=AsyncMock
|
||||
) as mock_async_log,
|
||||
patch.object(mock_callback, "async_log_success_event", new_callable=AsyncMock) as mock_async_log,
|
||||
patch.object(
|
||||
logging_obj,
|
||||
"_success_handler_helper_fn",
|
||||
|
|
@ -1358,20 +1325,14 @@ async def test_dispatch_prefer_async_handlers_runs_legacy_callbacks(
|
|||
logging_obj.model_call_details["litellm_params"] = {}
|
||||
|
||||
with (
|
||||
patch.object(
|
||||
logging_obj, "async_success_handler", new_callable=AsyncMock
|
||||
) as mock_async,
|
||||
patch.object(
|
||||
logging_obj, "success_handler", new_callable=MagicMock
|
||||
) as mock_sync,
|
||||
patch.object(logging_obj, "async_success_handler", new_callable=AsyncMock) as mock_async,
|
||||
patch.object(logging_obj, "success_handler", new_callable=MagicMock) as mock_sync,
|
||||
patch.object(
|
||||
logging_obj,
|
||||
"_should_run_sync_callbacks_for_async_calls",
|
||||
return_value=True,
|
||||
),
|
||||
patch(
|
||||
"litellm.litellm_core_utils.litellm_logging.executor.submit"
|
||||
) as mock_submit,
|
||||
patch("litellm.litellm_core_utils.litellm_logging.executor.submit") as mock_submit,
|
||||
):
|
||||
await logging_obj.dispatch_success_handlers(
|
||||
result=result,
|
||||
|
|
@ -1405,9 +1366,7 @@ async def test_dispatch_success_handlers_invokes_async_callback_for_pass_through
|
|||
|
||||
try:
|
||||
with (
|
||||
patch.object(
|
||||
mock_callback, "async_log_success_event", new_callable=AsyncMock
|
||||
) as mock_async_log,
|
||||
patch.object(mock_callback, "async_log_success_event", new_callable=AsyncMock) as mock_async_log,
|
||||
patch.object(mock_callback, "log_success_event") as mock_sync_log,
|
||||
):
|
||||
await logging_obj.dispatch_success_handlers(result={"id": "pt-1"})
|
||||
|
|
@ -1434,20 +1393,14 @@ async def test_dispatch_failure_handlers_prefer_async_does_not_submit_sync_handl
|
|||
logging_obj.model_call_details["litellm_params"] = {}
|
||||
|
||||
with (
|
||||
patch.object(
|
||||
logging_obj, "async_failure_handler", new_callable=AsyncMock
|
||||
) as mock_async,
|
||||
patch.object(
|
||||
logging_obj, "failure_handler", new_callable=MagicMock
|
||||
) as mock_sync,
|
||||
patch.object(logging_obj, "async_failure_handler", new_callable=AsyncMock) as mock_async,
|
||||
patch.object(logging_obj, "failure_handler", new_callable=MagicMock) as mock_sync,
|
||||
patch.object(
|
||||
logging_obj,
|
||||
"_should_run_sync_failure_callbacks_for_async_calls",
|
||||
return_value=False,
|
||||
),
|
||||
patch(
|
||||
"litellm.litellm_core_utils.litellm_logging.executor.submit"
|
||||
) as mock_submit,
|
||||
patch("litellm.litellm_core_utils.litellm_logging.executor.submit") as mock_submit,
|
||||
):
|
||||
await logging_obj.dispatch_failure_handlers(
|
||||
exception,
|
||||
|
|
@ -1530,12 +1483,8 @@ async def test_dispatch_failure_handlers_submits_sync_handler_for_failure_only_c
|
|||
patch.object(litellm, "success_callback", []),
|
||||
patch.object(litellm, "failure_callback", [_sync_failure_callback]),
|
||||
patch.object(logging_obj, "async_failure_handler", new_callable=AsyncMock),
|
||||
patch.object(
|
||||
logging_obj, "failure_handler", new_callable=MagicMock
|
||||
) as mock_sync,
|
||||
patch(
|
||||
"litellm.litellm_core_utils.litellm_logging.executor.submit"
|
||||
) as mock_submit,
|
||||
patch.object(logging_obj, "failure_handler", new_callable=MagicMock) as mock_sync,
|
||||
patch("litellm.litellm_core_utils.litellm_logging.executor.submit") as mock_submit,
|
||||
):
|
||||
await logging_obj.dispatch_failure_handlers(
|
||||
exception,
|
||||
|
|
@ -1562,15 +1511,9 @@ async def test_dispatch_failure_handlers_sync_sdk_shortcut_runs_sync_handler_inl
|
|||
logging_obj.model_call_details["litellm_params"] = {}
|
||||
|
||||
with (
|
||||
patch.object(
|
||||
logging_obj, "async_failure_handler", new_callable=AsyncMock
|
||||
) as mock_async,
|
||||
patch.object(
|
||||
logging_obj, "failure_handler", new_callable=MagicMock
|
||||
) as mock_sync,
|
||||
patch(
|
||||
"litellm.litellm_core_utils.litellm_logging.executor.submit"
|
||||
) as mock_submit,
|
||||
patch.object(logging_obj, "async_failure_handler", new_callable=AsyncMock) as mock_async,
|
||||
patch.object(logging_obj, "failure_handler", new_callable=MagicMock) as mock_sync,
|
||||
patch("litellm.litellm_core_utils.litellm_logging.executor.submit") as mock_submit,
|
||||
):
|
||||
await logging_obj.dispatch_failure_handlers(
|
||||
exception,
|
||||
|
|
@ -1617,14 +1560,10 @@ def test_success_handler_skips_guardrail_logging_hook_when_disabled(logging_obj)
|
|||
event_hook=GuardrailEventHooks.logging_only,
|
||||
)
|
||||
guardrail.should_run_guardrail = MagicMock(return_value=False)
|
||||
guardrail.logging_hook = MagicMock(
|
||||
return_value=(logging_obj.model_call_details, model_response)
|
||||
)
|
||||
guardrail.logging_hook = MagicMock(return_value=(logging_obj.model_call_details, model_response))
|
||||
|
||||
dummy_logger = DummyLogger()
|
||||
dummy_logger.logging_hook = MagicMock(
|
||||
return_value=(logging_obj.model_call_details, model_response)
|
||||
)
|
||||
dummy_logger.logging_hook = MagicMock(return_value=(logging_obj.model_call_details, model_response))
|
||||
|
||||
with patch.object(
|
||||
logging_obj,
|
||||
|
|
@ -1758,11 +1697,7 @@ def test_get_request_tags_from_metadata_and_litellm_metadata():
|
|||
|
||||
# Test case 2: Tags in litellm_metadata only
|
||||
tags = StandardLoggingPayloadSetup._get_request_tags(
|
||||
litellm_params={
|
||||
"litellm_metadata": {
|
||||
"tags": ["litellm-metadata-tag-1", "litellm-metadata-tag-2"]
|
||||
}
|
||||
},
|
||||
litellm_params={"litellm_metadata": {"tags": ["litellm-metadata-tag-1", "litellm-metadata-tag-2"]}},
|
||||
proxy_server_request={},
|
||||
)
|
||||
assert "litellm-metadata-tag-1" in tags
|
||||
|
|
@ -1867,15 +1802,9 @@ def test_get_request_tags_does_not_mutate_original_tags():
|
|||
user_agent_count_2 = len([t for t in tags2 if t.startswith("User-Agent:")])
|
||||
user_agent_count_3 = len([t for t in tags3 if t.startswith("User-Agent:")])
|
||||
|
||||
assert (
|
||||
user_agent_count_1 == 2
|
||||
), f"Expected 2 User-Agent tags, got {user_agent_count_1}"
|
||||
assert (
|
||||
user_agent_count_2 == 2
|
||||
), f"Expected 2 User-Agent tags, got {user_agent_count_2}"
|
||||
assert (
|
||||
user_agent_count_3 == 2
|
||||
), f"Expected 2 User-Agent tags, got {user_agent_count_3}"
|
||||
assert user_agent_count_1 == 2, f"Expected 2 User-Agent tags, got {user_agent_count_1}"
|
||||
assert user_agent_count_2 == 2, f"Expected 2 User-Agent tags, got {user_agent_count_2}"
|
||||
assert user_agent_count_3 == 2, f"Expected 2 User-Agent tags, got {user_agent_count_3}"
|
||||
|
||||
# Verify all returned lists are independent (different objects)
|
||||
assert tags1 is not tags2
|
||||
|
|
@ -1908,9 +1837,7 @@ def test_get_extra_header_tags():
|
|||
|
||||
# Test case 3: Extra headers configured but request has no headers dict
|
||||
litellm.extra_spend_tag_headers = ["x-custom", "x-tenant"]
|
||||
result = StandardLoggingPayloadSetup._get_extra_header_tags(
|
||||
proxy_server_request={"headers": "not-a-dict"}
|
||||
)
|
||||
result = StandardLoggingPayloadSetup._get_extra_header_tags(proxy_server_request={"headers": "not-a-dict"})
|
||||
assert result is None
|
||||
|
||||
# Test case 4: Extra headers configured but none match request headers
|
||||
|
|
@ -2211,9 +2138,7 @@ def test_get_masked_values():
|
|||
"presidio_anonymizer_api_base": None,
|
||||
"vertex_credentials": "{sensitive_api_key}",
|
||||
}
|
||||
masked_values = _get_masked_values(
|
||||
sensitive_object, unmasked_length=4, number_of_asterisks=4
|
||||
)
|
||||
masked_values = _get_masked_values(sensitive_object, unmasked_length=4, number_of_asterisks=4)
|
||||
assert masked_values["presidio_anonymizer_api_base"] is None
|
||||
assert masked_values["vertex_credentials"] == "{s****y}"
|
||||
|
||||
|
|
@ -2238,9 +2163,7 @@ async def test_e2e_generate_cold_storage_object_key_successful():
|
|||
patch("litellm.integrations.s3.get_s3_object_key") as mock_get_s3_key,
|
||||
):
|
||||
# Mock the S3 object key generation to return a predictable result
|
||||
mock_get_s3_key.return_value = (
|
||||
"2025-01-15/time-10-30-45-123456_chatcmpl-test-12345.json"
|
||||
)
|
||||
mock_get_s3_key.return_value = "2025-01-15/time-10-30-45-123456_chatcmpl-test-12345.json"
|
||||
|
||||
# Call the function
|
||||
result = StandardLoggingPayloadSetup._generate_cold_storage_object_key(
|
||||
|
|
@ -2281,16 +2204,12 @@ async def test_e2e_generate_cold_storage_object_key_with_custom_logger_s3_path()
|
|||
|
||||
with (
|
||||
patch("litellm.cold_storage_custom_logger", "s3_v2"),
|
||||
patch(
|
||||
"litellm.logging_callback_manager.get_active_custom_logger_for_callback_name"
|
||||
) as mock_get_logger,
|
||||
patch("litellm.logging_callback_manager.get_active_custom_logger_for_callback_name") as mock_get_logger,
|
||||
patch("litellm.integrations.s3.get_s3_object_key") as mock_get_s3_key,
|
||||
):
|
||||
# Setup mocks
|
||||
mock_get_logger.return_value = mock_custom_logger
|
||||
mock_get_s3_key.return_value = (
|
||||
"storage/2025-01-15/time-10-30-45-123456_chatcmpl-test-12345.json"
|
||||
)
|
||||
mock_get_s3_key.return_value = "storage/2025-01-15/time-10-30-45-123456_chatcmpl-test-12345.json"
|
||||
|
||||
# Call the function
|
||||
result = StandardLoggingPayloadSetup._generate_cold_storage_object_key(
|
||||
|
|
@ -2309,9 +2228,7 @@ async def test_e2e_generate_cold_storage_object_key_with_custom_logger_s3_path()
|
|||
)
|
||||
|
||||
# Verify the result
|
||||
assert (
|
||||
result == "storage/2025-01-15/time-10-30-45-123456_chatcmpl-test-12345.json"
|
||||
)
|
||||
assert result == "storage/2025-01-15/time-10-30-45-123456_chatcmpl-test-12345.json"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
|
@ -2334,16 +2251,12 @@ async def test_e2e_generate_cold_storage_object_key_with_logger_no_s3_path():
|
|||
|
||||
with (
|
||||
patch("litellm.cold_storage_custom_logger", "s3_v2"),
|
||||
patch(
|
||||
"litellm.logging_callback_manager.get_active_custom_logger_for_callback_name"
|
||||
) as mock_get_logger,
|
||||
patch("litellm.logging_callback_manager.get_active_custom_logger_for_callback_name") as mock_get_logger,
|
||||
patch("litellm.integrations.s3.get_s3_object_key") as mock_get_s3_key,
|
||||
):
|
||||
# Setup mocks
|
||||
mock_get_logger.return_value = mock_custom_logger
|
||||
mock_get_s3_key.return_value = (
|
||||
"2025-01-15/time-10-30-45-123456_chatcmpl-test-12345.json"
|
||||
)
|
||||
mock_get_s3_key.return_value = "2025-01-15/time-10-30-45-123456_chatcmpl-test-12345.json"
|
||||
|
||||
# Call the function
|
||||
result = StandardLoggingPayloadSetup._generate_cold_storage_object_key(
|
||||
|
|
@ -2459,9 +2372,7 @@ def test_get_usage_as_dict():
|
|||
assert result == {"prompt_tokens": 20, "completion_tokens": 30}
|
||||
|
||||
# Test case 5: response_obj with no usage key returns empty
|
||||
result = StandardLoggingPayloadSetup.get_usage_as_dict(
|
||||
response_obj={"id": "resp-1", "choices": []}
|
||||
)
|
||||
result = StandardLoggingPayloadSetup.get_usage_as_dict(response_obj={"id": "resp-1", "choices": []})
|
||||
assert result == {"prompt_tokens": 0, "completion_tokens": 0, "total_tokens": 0}
|
||||
|
||||
|
||||
|
|
@ -2474,26 +2385,20 @@ def test_append_system_prompt_messages():
|
|||
# Test case 1: system in kwargs with existing messages
|
||||
kwargs = {"system": "You are a helpful assistant"}
|
||||
messages = [{"role": "user", "content": "Hello"}]
|
||||
result = StandardLoggingPayloadSetup.append_system_prompt_messages(
|
||||
kwargs=kwargs, messages=messages
|
||||
)
|
||||
result = StandardLoggingPayloadSetup.append_system_prompt_messages(kwargs=kwargs, messages=messages)
|
||||
assert len(result) == 2
|
||||
assert result[0] == {"role": "system", "content": "You are a helpful assistant"}
|
||||
assert result[1] == {"role": "user", "content": "Hello"}
|
||||
|
||||
# Test case 2: system in kwargs with None messages
|
||||
kwargs = {"system": "You are a helpful assistant"}
|
||||
result = StandardLoggingPayloadSetup.append_system_prompt_messages(
|
||||
kwargs=kwargs, messages=None
|
||||
)
|
||||
result = StandardLoggingPayloadSetup.append_system_prompt_messages(kwargs=kwargs, messages=None)
|
||||
assert len(result) == 1
|
||||
assert result[0] == {"role": "system", "content": "You are a helpful assistant"}
|
||||
|
||||
# Test case 3: system in kwargs with empty messages list
|
||||
kwargs = {"system": "You are a helpful assistant"}
|
||||
result = StandardLoggingPayloadSetup.append_system_prompt_messages(
|
||||
kwargs=kwargs, messages=[]
|
||||
)
|
||||
result = StandardLoggingPayloadSetup.append_system_prompt_messages(kwargs=kwargs, messages=[])
|
||||
assert len(result) == 1
|
||||
assert result[0] == {"role": "system", "content": "You are a helpful assistant"}
|
||||
|
||||
|
|
@ -2503,24 +2408,18 @@ def test_append_system_prompt_messages():
|
|||
{"role": "system", "content": "You are a helpful assistant"},
|
||||
{"role": "user", "content": "Hello"},
|
||||
]
|
||||
result = StandardLoggingPayloadSetup.append_system_prompt_messages(
|
||||
kwargs=kwargs, messages=messages
|
||||
)
|
||||
result = StandardLoggingPayloadSetup.append_system_prompt_messages(kwargs=kwargs, messages=messages)
|
||||
assert len(result) == 2
|
||||
assert result[0] == {"role": "system", "content": "You are a helpful assistant"}
|
||||
|
||||
# Test case 5: no system in kwargs returns messages unchanged
|
||||
kwargs = {}
|
||||
messages = [{"role": "user", "content": "Hello"}]
|
||||
result = StandardLoggingPayloadSetup.append_system_prompt_messages(
|
||||
kwargs=kwargs, messages=messages
|
||||
)
|
||||
result = StandardLoggingPayloadSetup.append_system_prompt_messages(kwargs=kwargs, messages=messages)
|
||||
assert result == messages
|
||||
|
||||
# Test case 6: None kwargs returns messages unchanged
|
||||
result = StandardLoggingPayloadSetup.append_system_prompt_messages(
|
||||
kwargs=None, messages=messages
|
||||
)
|
||||
result = StandardLoggingPayloadSetup.append_system_prompt_messages(kwargs=None, messages=messages)
|
||||
assert result == messages
|
||||
|
||||
|
||||
|
|
@ -2581,12 +2480,11 @@ async def test_async_success_handler_sets_standard_logging_object_for_pass_throu
|
|||
|
||||
# Verify that standard_logging_object was set
|
||||
assert "standard_logging_object" in logging_obj.model_call_details, (
|
||||
"standard_logging_object should be set for pass-through endpoints "
|
||||
"even when complete_streaming_response is None"
|
||||
"standard_logging_object should be set for pass-through endpoints even when complete_streaming_response is None"
|
||||
)
|
||||
assert logging_obj.model_call_details["standard_logging_object"] is not None, (
|
||||
"standard_logging_object should not be None for pass-through endpoints"
|
||||
)
|
||||
assert (
|
||||
logging_obj.model_call_details["standard_logging_object"] is not None
|
||||
), "standard_logging_object should not be None for pass-through endpoints"
|
||||
|
||||
# Verify that async_complete_streaming_response was set to prevent re-processing
|
||||
# This is consistent with the existing code pattern for regular streaming
|
||||
|
|
@ -2594,15 +2492,13 @@ async def test_async_success_handler_sets_standard_logging_object_for_pass_throu
|
|||
"async_complete_streaming_response should be set to prevent re-processing, "
|
||||
"consistent with the existing code pattern"
|
||||
)
|
||||
assert (
|
||||
logging_obj.model_call_details["async_complete_streaming_response"] is result
|
||||
), "async_complete_streaming_response should be set to the result"
|
||||
assert logging_obj.model_call_details["async_complete_streaming_response"] is result, (
|
||||
"async_complete_streaming_response should be set to the result"
|
||||
)
|
||||
|
||||
# Verify that response_cost is set to None (cost calculation not possible for pass-through)
|
||||
# This is consistent with the error handling in the non-pass-through code path
|
||||
assert (
|
||||
"response_cost" in logging_obj.model_call_details
|
||||
), "response_cost should be set for pass-through endpoints"
|
||||
assert "response_cost" in logging_obj.model_call_details, "response_cost should be set for pass-through endpoints"
|
||||
assert logging_obj.model_call_details["response_cost"] is None, (
|
||||
"response_cost should be None for pass-through endpoints since "
|
||||
"StandardPassThroughResponseObject doesn't have standard usage info"
|
||||
|
|
@ -2661,14 +2557,10 @@ async def test_async_success_handler_prevents_reprocessing_for_pass_through_endp
|
|||
# Verify first call set the values
|
||||
assert "standard_logging_object" in logging_obj.model_call_details
|
||||
assert "async_complete_streaming_response" in logging_obj.model_call_details
|
||||
first_standard_logging_object = logging_obj.model_call_details[
|
||||
"standard_logging_object"
|
||||
]
|
||||
first_standard_logging_object = logging_obj.model_call_details["standard_logging_object"]
|
||||
|
||||
# Second call - should return early due to async_complete_streaming_response guard
|
||||
with patch.object(
|
||||
logging_obj, "get_combined_callback_list", return_value=[]
|
||||
) as mock_callbacks:
|
||||
with patch.object(logging_obj, "get_combined_callback_list", return_value=[]) as mock_callbacks:
|
||||
await logging_obj.async_success_handler(
|
||||
result=result,
|
||||
start_time=start_time,
|
||||
|
|
@ -2679,10 +2571,9 @@ async def test_async_success_handler_prevents_reprocessing_for_pass_through_endp
|
|||
mock_callbacks.assert_not_called()
|
||||
|
||||
# Verify standard_logging_object wasn't modified by second call
|
||||
assert (
|
||||
logging_obj.model_call_details["standard_logging_object"]
|
||||
is first_standard_logging_object
|
||||
), "standard_logging_object should not be modified on re-processing"
|
||||
assert logging_obj.model_call_details["standard_logging_object"] is first_standard_logging_object, (
|
||||
"standard_logging_object should not be modified on re-processing"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
|
@ -2721,9 +2612,7 @@ async def test_async_success_handler_sets_standard_logging_object_for_streaming_
|
|||
}
|
||||
|
||||
# Create a pass-through response object (simulating unparseable streaming response)
|
||||
result = StandardPassThroughResponseObject(
|
||||
response='data: {"chunk": 1}\ndata: {"chunk": 2}\ndata: [DONE]'
|
||||
)
|
||||
result = StandardPassThroughResponseObject(response='data: {"chunk": 1}\ndata: {"chunk": 2}\ndata: [DONE]')
|
||||
|
||||
start_time = datetime.now()
|
||||
end_time = datetime.now()
|
||||
|
|
@ -2743,9 +2632,9 @@ async def test_async_success_handler_sets_standard_logging_object_for_streaming_
|
|||
"standard_logging_object should be set for streaming pass-through endpoints "
|
||||
"even when the response cannot be parsed into a ModelResponse"
|
||||
)
|
||||
assert (
|
||||
logging_obj.model_call_details["standard_logging_object"] is not None
|
||||
), "standard_logging_object should not be None for streaming pass-through endpoints"
|
||||
assert logging_obj.model_call_details["standard_logging_object"] is not None, (
|
||||
"standard_logging_object should not be None for streaming pass-through endpoints"
|
||||
)
|
||||
|
||||
|
||||
def test_get_error_information_error_code_priority():
|
||||
|
|
@ -2787,30 +2676,22 @@ def test_get_error_information_error_code_priority():
|
|||
self.message = message
|
||||
super().__init__(message)
|
||||
|
||||
both_exception = BothAttributesException(
|
||||
code="400", status_code=500, message="Bad Request"
|
||||
)
|
||||
both_exception = BothAttributesException(code="400", status_code=500, message="Bad Request")
|
||||
result = StandardLoggingPayloadSetup.get_error_information(both_exception)
|
||||
assert result["error_code"] == "400" # Should prefer 'code' over 'status_code'
|
||||
|
||||
# Test case 4: Exception with 'code' as empty string - should fall back to 'status_code'
|
||||
empty_code_exception = BothAttributesException(
|
||||
code="", status_code=404, message="Not Found"
|
||||
)
|
||||
empty_code_exception = BothAttributesException(code="", status_code=404, message="Not Found")
|
||||
result = StandardLoggingPayloadSetup.get_error_information(empty_code_exception)
|
||||
assert result["error_code"] == "404" # Should fall back to status_code
|
||||
|
||||
# Test case 5: Exception with 'code' as "None" string - should fall back to 'status_code'
|
||||
none_string_exception = BothAttributesException(
|
||||
code="None", status_code=503, message="Service Unavailable"
|
||||
)
|
||||
none_string_exception = BothAttributesException(code="None", status_code=503, message="Service Unavailable")
|
||||
result = StandardLoggingPayloadSetup.get_error_information(none_string_exception)
|
||||
assert result["error_code"] == "503" # Should fall back to status_code
|
||||
|
||||
# Test case 6: Exception with 'code' as None - should fall back to 'status_code'
|
||||
none_code_exception = BothAttributesException(
|
||||
code=None, status_code=401, message="Unauthorized"
|
||||
)
|
||||
none_code_exception = BothAttributesException(code=None, status_code=401, message="Unauthorized")
|
||||
result = StandardLoggingPayloadSetup.get_error_information(none_code_exception)
|
||||
assert result["error_code"] == "401" # Should fall back to status_code
|
||||
|
||||
|
|
@ -2859,9 +2740,7 @@ def test_get_error_information_prefers_message_attribute_over_str():
|
|||
)
|
||||
|
||||
result = StandardLoggingPayloadSetup.get_error_information(exc)
|
||||
assert (
|
||||
result["error_message"] == msg
|
||||
), f"expected message from .message attribute, got {result['error_message']!r}"
|
||||
assert result["error_message"] == msg, f"expected message from .message attribute, got {result['error_message']!r}"
|
||||
assert result["error_code"] == "401"
|
||||
assert result["error_class"] == "ProxyExceptionLike"
|
||||
|
||||
|
|
@ -2936,8 +2815,7 @@ def test_get_error_information_preserves_explicit_empty_message():
|
|||
exc = ProxyExceptionLike(message="", code=500)
|
||||
result = StandardLoggingPayloadSetup.get_error_information(exc)
|
||||
assert result["error_message"] == "", (
|
||||
"explicit empty .message must survive verbatim; got "
|
||||
f"{result['error_message']!r}"
|
||||
f"explicit empty .message must survive verbatim; got {result['error_message']!r}"
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -3200,9 +3078,7 @@ def test_process_hidden_params_recalculates_cost_after_failure_handler_zero():
|
|||
choices=[{"message": {"role": "assistant", "content": "ok"}}],
|
||||
usage=Usage(prompt_tokens=9698, completion_tokens=30, total_tokens=9728),
|
||||
)
|
||||
logging_obj._process_hidden_params_and_response_cost(
|
||||
result, datetime.now(), datetime.now()
|
||||
)
|
||||
logging_obj._process_hidden_params_and_response_cost(result, datetime.now(), datetime.now())
|
||||
|
||||
cost = logging_obj.model_call_details.get("response_cost")
|
||||
assert cost is not None and cost > 0
|
||||
|
|
@ -3226,9 +3102,7 @@ def test_process_hidden_params_preserves_zero_cost_in_hidden_params():
|
|||
litellm_call_id="test-hidden-zero-cost",
|
||||
function_id="test-hidden-zero-cost",
|
||||
)
|
||||
logging_obj.model_call_details["litellm_params"] = {
|
||||
"model": "gemini-2.5-flash-lite"
|
||||
}
|
||||
logging_obj.model_call_details["litellm_params"] = {"model": "gemini-2.5-flash-lite"}
|
||||
logging_obj.optional_params = {}
|
||||
|
||||
result = ModelResponse(
|
||||
|
|
@ -3238,9 +3112,7 @@ def test_process_hidden_params_preserves_zero_cost_in_hidden_params():
|
|||
)
|
||||
result._hidden_params = {"response_cost": 0.0}
|
||||
|
||||
logging_obj._process_hidden_params_and_response_cost(
|
||||
result, datetime.now(), datetime.now()
|
||||
)
|
||||
logging_obj._process_hidden_params_and_response_cost(result, datetime.now(), datetime.now())
|
||||
|
||||
assert logging_obj.model_call_details.get("response_cost") == 0.0
|
||||
slo = logging_obj.model_call_details.get("standard_logging_object") or {}
|
||||
|
|
@ -3289,9 +3161,7 @@ def test_process_hidden_params_uses_hidden_params_cost_after_failure_handler_zer
|
|||
)
|
||||
result._hidden_params = {"response_cost": passthrough_cost}
|
||||
|
||||
logging_obj._process_hidden_params_and_response_cost(
|
||||
result, datetime.now(), datetime.now()
|
||||
)
|
||||
logging_obj._process_hidden_params_and_response_cost(result, datetime.now(), datetime.now())
|
||||
|
||||
assert logging_obj.model_call_details.get("response_cost") == passthrough_cost
|
||||
slo = logging_obj.model_call_details.get("standard_logging_object") or {}
|
||||
|
|
@ -3348,9 +3218,7 @@ def test_function_setup_litellm_metadata_populates_metadata():
|
|||
assert litellm_metadata.get("user_api_key_hash") == test_api_key_hash
|
||||
|
||||
# metadata should be a COPY, not an alias — mutating one must not affect the other
|
||||
assert (
|
||||
metadata is not litellm_metadata
|
||||
), "litellm_params['metadata'] should be a copy, not the same object"
|
||||
assert metadata is not litellm_metadata, "litellm_params['metadata'] should be a copy, not the same object"
|
||||
|
||||
|
||||
def test_function_setup_litellm_metadata_guardrail_writes_visible_after_setup():
|
||||
|
|
@ -3395,9 +3263,9 @@ def test_function_setup_litellm_metadata_guardrail_writes_visible_after_setup():
|
|||
litellm_params = logging_obj.model_call_details.get("litellm_params", {})
|
||||
litellm_metadata = litellm_params.get("litellm_metadata")
|
||||
assert litellm_metadata is not None
|
||||
assert litellm_metadata.get("standard_logging_guardrail_information") == [
|
||||
guardrail_entry
|
||||
], "guardrail writes after function_setup must be visible to the logging object"
|
||||
assert litellm_metadata.get("standard_logging_guardrail_information") == [guardrail_entry], (
|
||||
"guardrail writes after function_setup must be visible to the logging object"
|
||||
)
|
||||
assert litellm_metadata.get("applied_guardrails") == ["pam-ethical-request"]
|
||||
|
||||
merged = StandardLoggingPayloadSetup.merge_litellm_metadata(litellm_params)
|
||||
|
|
@ -3566,9 +3434,7 @@ def test_failure_handler_skips_sync_callbacks_for_pass_through_requests(logging_
|
|||
|
||||
|
||||
@pytest.mark.parametrize("call_type", ["completion", "acompletion"])
|
||||
def test_failure_handler_runs_sync_callbacks_for_non_pass_through_requests(
|
||||
logging_obj, call_type
|
||||
):
|
||||
def test_failure_handler_runs_sync_callbacks_for_non_pass_through_requests(logging_obj, call_type):
|
||||
"""Ensure sync failure callbacks still fire for normal (non-pass-through) requests."""
|
||||
from litellm.integrations.custom_logger import CustomLogger
|
||||
|
||||
|
|
@ -3729,9 +3595,7 @@ def test_standard_logging_hidden_params_backfills_response_cost_without_mutating
|
|||
)
|
||||
response._hidden_params = {"response_cost": None, "model_id": "mid-test"}
|
||||
|
||||
payload = logging_obj._build_standard_logging_payload(
|
||||
response, datetime.now(), datetime.now()
|
||||
)
|
||||
payload = logging_obj._build_standard_logging_payload(response, datetime.now(), datetime.now())
|
||||
|
||||
assert payload is not None
|
||||
assert payload["hidden_params"]["response_cost"] == 0.002
|
||||
|
|
@ -3785,10 +3649,7 @@ def test_merge_hidden_params_from_response_into_metadata_no_op_when_empty():
|
|||
_hidden_params = {}
|
||||
|
||||
logging_obj._merge_hidden_params_from_response_into_metadata(_NoHp())
|
||||
assert (
|
||||
"hidden_params"
|
||||
not in logging_obj.model_call_details["litellm_params"]["metadata"]
|
||||
)
|
||||
assert "hidden_params" not in logging_obj.model_call_details["litellm_params"]["metadata"]
|
||||
|
||||
|
||||
# ── StandardLoggingPayloadSetup.get_additional_headers ───────────────────────
|
||||
|
|
@ -3877,9 +3738,7 @@ def _model_router_response(selected_model: str, stamp: bool):
|
|||
from litellm.types.utils import ModelResponse
|
||||
|
||||
response = ModelResponse(model=selected_model)
|
||||
response._hidden_params = (
|
||||
{AZURE_MODEL_ROUTER_SELECTED_MODEL_KEY: selected_model} if stamp else {}
|
||||
)
|
||||
response._hidden_params = {AZURE_MODEL_ROUTER_SELECTED_MODEL_KEY: selected_model} if stamp else {}
|
||||
return response
|
||||
|
||||
|
||||
|
|
@ -3903,9 +3762,7 @@ def test_standard_logging_payload_uses_stamped_model_router_model(logging_obj):
|
|||
"messages": [],
|
||||
"litellm_params": {"metadata": {}},
|
||||
},
|
||||
init_response_obj=_model_router_response(
|
||||
"azure_ai/grok-4-1-fast-reasoning", stamp=True
|
||||
),
|
||||
init_response_obj=_model_router_response("azure_ai/grok-4-1-fast-reasoning", stamp=True),
|
||||
start_time=now,
|
||||
end_time=now,
|
||||
logging_obj=logging_obj,
|
||||
|
|
@ -3937,9 +3794,7 @@ def test_standard_logging_payload_keeps_requested_model_without_router_stamp(
|
|||
"messages": [],
|
||||
"litellm_params": {"metadata": {}},
|
||||
},
|
||||
init_response_obj=_model_router_response(
|
||||
"azure_ai/grok-4-1-fast-reasoning", stamp=False
|
||||
),
|
||||
init_response_obj=_model_router_response("azure_ai/grok-4-1-fast-reasoning", stamp=False),
|
||||
start_time=now,
|
||||
end_time=now,
|
||||
logging_obj=logging_obj,
|
||||
|
|
@ -3985,9 +3840,7 @@ def test_success_handler_computes_cost_for_dict_response():
|
|||
"_build_standard_logging_payload",
|
||||
return_value={"response_cost": expected_cost},
|
||||
),
|
||||
patch(
|
||||
"litellm.litellm_core_utils.litellm_logging.emit_standard_logging_payload"
|
||||
),
|
||||
patch("litellm.litellm_core_utils.litellm_logging.emit_standard_logging_payload"),
|
||||
patch.object(
|
||||
logging_obj,
|
||||
"_is_recognized_call_type_for_logging",
|
||||
|
|
@ -4024,9 +3877,7 @@ def test_success_handler_preserves_precomputed_cost_for_dict_response():
|
|||
"_build_standard_logging_payload",
|
||||
return_value={"response_cost": precomputed_cost},
|
||||
),
|
||||
patch(
|
||||
"litellm.litellm_core_utils.litellm_logging.emit_standard_logging_payload"
|
||||
),
|
||||
patch("litellm.litellm_core_utils.litellm_logging.emit_standard_logging_payload"),
|
||||
patch.object(
|
||||
logging_obj,
|
||||
"_is_recognized_call_type_for_logging",
|
||||
|
|
@ -4065,9 +3916,7 @@ def test_success_handler_unified_helper_runs_for_typed_results():
|
|||
"_build_standard_logging_payload",
|
||||
return_value={"response_cost": expected_cost},
|
||||
),
|
||||
patch(
|
||||
"litellm.litellm_core_utils.litellm_logging.emit_standard_logging_payload"
|
||||
),
|
||||
patch("litellm.litellm_core_utils.litellm_logging.emit_standard_logging_payload"),
|
||||
patch.object(
|
||||
logging_obj,
|
||||
"_is_recognized_call_type_for_logging",
|
||||
|
|
@ -4122,9 +3971,7 @@ class TestFirstApiCallStartTimeSetOnce:
|
|||
assert first == obj.model_call_details["api_call_start_time"]
|
||||
# Set on the logging object only — user metadata untouched.
|
||||
assert user_meta == {}
|
||||
assert (
|
||||
"first_api_call_start_time" not in obj.model_call_details["litellm_params"]
|
||||
)
|
||||
assert "first_api_call_start_time" not in obj.model_call_details["litellm_params"]
|
||||
|
||||
time.sleep(0.002) # ensure a distinct retry timestamp
|
||||
obj.pre_call(input="hi", api_key="sk-test")
|
||||
|
|
@ -4141,18 +3988,16 @@ def test_get_error_information_for_logging_payload_ignores_spoofed_disconnect_wi
|
|||
baseline = StandardLoggingPayloadSetup.get_error_information(
|
||||
original_exception=ValueError("provider failure"),
|
||||
)
|
||||
error_information, error_str = (
|
||||
StandardLoggingPayloadSetup.get_error_information_for_logging_payload(
|
||||
metadata={
|
||||
"error_information": {
|
||||
"error_code": "499",
|
||||
"error_message": "Client disconnected the request",
|
||||
"error_class": "ClientDisconnected",
|
||||
}
|
||||
},
|
||||
original_exception=ValueError("provider failure"),
|
||||
error_str="provider failure",
|
||||
)
|
||||
error_information, error_str = StandardLoggingPayloadSetup.get_error_information_for_logging_payload(
|
||||
metadata={
|
||||
"error_information": {
|
||||
"error_code": "499",
|
||||
"error_message": "Client disconnected the request",
|
||||
"error_class": "ClientDisconnected",
|
||||
}
|
||||
},
|
||||
original_exception=ValueError("provider failure"),
|
||||
error_str="provider failure",
|
||||
)
|
||||
assert error_information == baseline
|
||||
assert error_str == "provider failure"
|
||||
|
|
@ -4166,22 +4011,18 @@ def test_get_error_information_for_logging_payload_client_disconnect():
|
|||
"error_message": "Client disconnected the request",
|
||||
"error_class": "ClientDisconnected",
|
||||
}
|
||||
error_information, error_str = (
|
||||
StandardLoggingPayloadSetup.get_error_information_for_logging_payload(
|
||||
metadata={"client_disconnected": True, "error_information": custom_error},
|
||||
original_exception=None,
|
||||
error_str=None,
|
||||
)
|
||||
error_information, error_str = StandardLoggingPayloadSetup.get_error_information_for_logging_payload(
|
||||
metadata={"client_disconnected": True, "error_information": custom_error},
|
||||
original_exception=None,
|
||||
error_str=None,
|
||||
)
|
||||
assert error_information == custom_error
|
||||
assert error_str == "Client disconnected the request"
|
||||
|
||||
error_information, error_str = (
|
||||
StandardLoggingPayloadSetup.get_error_information_for_logging_payload(
|
||||
metadata={"client_disconnected": True},
|
||||
original_exception=None,
|
||||
error_str="existing error",
|
||||
)
|
||||
error_information, error_str = StandardLoggingPayloadSetup.get_error_information_for_logging_payload(
|
||||
metadata={"client_disconnected": True},
|
||||
original_exception=None,
|
||||
error_str="existing error",
|
||||
)
|
||||
assert error_information["error_code"] == "499"
|
||||
assert error_str == "existing error"
|
||||
|
|
@ -4189,12 +4030,10 @@ def test_get_error_information_for_logging_payload_client_disconnect():
|
|||
baseline = StandardLoggingPayloadSetup.get_error_information(
|
||||
original_exception=None,
|
||||
)
|
||||
error_information, error_str = (
|
||||
StandardLoggingPayloadSetup.get_error_information_for_logging_payload(
|
||||
metadata={},
|
||||
original_exception=None,
|
||||
error_str=None,
|
||||
)
|
||||
error_information, error_str = StandardLoggingPayloadSetup.get_error_information_for_logging_payload(
|
||||
metadata={},
|
||||
original_exception=None,
|
||||
error_str=None,
|
||||
)
|
||||
assert error_information == baseline
|
||||
assert error_str is None
|
||||
|
|
@ -4229,9 +4068,7 @@ def test_get_error_information_prefers_message_attribute_over_empty_str():
|
|||
def __str__(self):
|
||||
return ""
|
||||
|
||||
info = StandardLoggingPayloadSetup.get_error_information(
|
||||
original_exception=_SilentExc()
|
||||
)
|
||||
info = StandardLoggingPayloadSetup.get_error_information(original_exception=_SilentExc())
|
||||
assert info["error_message"] == "real failure detail"
|
||||
assert info["error_code"] == "401"
|
||||
|
||||
|
|
@ -4262,9 +4099,7 @@ def _responses_api_response_with_text(text="hello world"):
|
|||
type="message",
|
||||
role="assistant",
|
||||
status="completed",
|
||||
content=[
|
||||
ResponseOutputText(annotations=[], text=text, type="output_text")
|
||||
],
|
||||
content=[ResponseOutputText(annotations=[], text=text, type="output_text")],
|
||||
)
|
||||
],
|
||||
usage=ResponseAPIUsage(input_tokens=11, output_tokens=7, total_tokens=18),
|
||||
|
|
@ -4279,9 +4114,7 @@ def _responses_api_response_with_text(text="hello world"):
|
|||
("ResponseFailedEvent", "response.failed"),
|
||||
],
|
||||
)
|
||||
def test_handle_anthropic_messages_response_logging_translates_terminal_responses_api_event(
|
||||
event_cls, event_type
|
||||
):
|
||||
def test_handle_anthropic_messages_response_logging_translates_terminal_responses_api_event(event_cls, event_type):
|
||||
"""Regression for #28595 / #28943. When anthropic_messages routes to the OpenAI
|
||||
Responses backend and stream=True, success_handler receives a terminal Responses
|
||||
API event. The handler must translate it to a ModelResponse whose choices carry
|
||||
|
|
@ -4320,10 +4153,7 @@ def test_handle_anthropic_messages_response_logging_passes_model_response_throug
|
|||
"""Anthropic-native path already yields a ModelResponse; it must be returned unchanged."""
|
||||
logging_obj = _anthropic_messages_logging_obj()
|
||||
model_response = ModelResponse()
|
||||
assert (
|
||||
logging_obj._handle_anthropic_messages_response_logging(result=model_response)
|
||||
is model_response
|
||||
)
|
||||
assert logging_obj._handle_anthropic_messages_response_logging(result=model_response) is model_response
|
||||
|
||||
|
||||
def test_handle_anthropic_messages_response_logging_degrades_on_unparseable_responses_payload():
|
||||
|
|
@ -4619,9 +4449,7 @@ def test_non_image_response_has_no_output_image_count(logging_obj):
|
|||
|
||||
def test_zero_token_video_usage_preserves_duration_seconds(logging_obj):
|
||||
"""Video usage bills by duration; the payload must keep duration_seconds even with zero tokens."""
|
||||
payload = _build_payload_for_media_response(
|
||||
logging_obj, {"id": "video-1", "usage": {"duration_seconds": 4.0}}
|
||||
)
|
||||
payload = _build_payload_for_media_response(logging_obj, {"id": "video-1", "usage": {"duration_seconds": 4.0}})
|
||||
|
||||
assert payload is not None
|
||||
assert payload["metadata"]["usage_object"]["duration_seconds"] == 4.0
|
||||
|
|
@ -5600,6 +5428,8 @@ def test_prompt_hooks_skip_prompt_managers_when_no_prompt_id(logging_obj, tmp_pa
|
|||
)
|
||||
for hook in [cb for cb in litellm.callbacks if isinstance(cb, VectorStorePreCallHook)]:
|
||||
litellm.logging_callback_manager.remove_callback_from_list_by_object(litellm.callbacks, hook)
|
||||
|
||||
|
||||
def test_newrelic_dispatch_prefers_otel_v2_when_flag_on(monkeypatch):
|
||||
"""With LITELLM_OTEL_V2 on, the "newrelic" callback builds the OTel v2
|
||||
logger (per-team credential routing); with the flag off (default) it keeps
|
||||
|
|
@ -5752,7 +5582,9 @@ def test_get_error_information_skips_traceback_for_budget_rejection_with_provide
|
|||
from litellm.litellm_core_utils.litellm_logging import StandardLoggingPayloadSetup
|
||||
|
||||
assert litellm.log_client_error_tracebacks is False
|
||||
over_budget = _raise_and_catch(litellm.BudgetExceededError(current_cost=0.01, max_budget=0.0, llm_provider="anthropic"))
|
||||
over_budget = _raise_and_catch(
|
||||
litellm.BudgetExceededError(current_cost=0.01, max_budget=0.0, llm_provider="anthropic")
|
||||
)
|
||||
result = StandardLoggingPayloadSetup.get_error_information(over_budget)
|
||||
assert result["error_code"] == "429"
|
||||
assert result["llm_provider"] == "anthropic"
|
||||
|
|
@ -5782,3 +5614,37 @@ def test_failure_handler_helper_fn_builds_payload_once_per_exception():
|
|||
other_exc = _raise_and_catch(_ClientError(status_code=429, message="rate limited"))
|
||||
obj._failure_handler_helper_fn(exception=other_exc, traceback_exception="")
|
||||
assert obj.model_call_details["standard_logging_object"] is not first_payload
|
||||
|
||||
|
||||
def test_get_assembled_streaming_response_handles_dict_response_usage(logging_obj):
|
||||
"""#29913: on the streaming /v1/responses spend-log path result.response can
|
||||
arrive as a dict rather than a ResponsesAPIResponse. The unified-usage step
|
||||
must not assume a `.usage` attribute (it raised AttributeError before)."""
|
||||
import datetime as _dt
|
||||
|
||||
from litellm.types.llms.openai import (
|
||||
ResponseAPIUsage,
|
||||
ResponseCompletedEvent,
|
||||
ResponsesAPIStreamEvents,
|
||||
)
|
||||
|
||||
usage = ResponseAPIUsage(input_tokens=11, output_tokens=7, total_tokens=18)
|
||||
event = ResponseCompletedEvent.model_construct(
|
||||
type=ResponsesAPIStreamEvents.RESPONSE_COMPLETED,
|
||||
response={"usage": usage},
|
||||
)
|
||||
|
||||
now = _dt.datetime.now()
|
||||
result = logging_obj._get_assembled_streaming_response(
|
||||
result=event,
|
||||
start_time=now,
|
||||
end_time=now,
|
||||
is_async=False,
|
||||
streaming_chunks=[],
|
||||
)
|
||||
|
||||
assert isinstance(result, dict)
|
||||
assert isinstance(result["usage"], dict)
|
||||
assert result["usage"]["total_tokens"] == 18
|
||||
assert result["usage"]["prompt_tokens"] == 11
|
||||
assert result["usage"]["completion_tokens"] == 7
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue