test(bedrock): align backported integration harness with release APIs

This commit is contained in:
Joshua Valluru 2026-10-20 18:49:10 -07:00
parent 71b6509656
commit 0dbf29974c
7 changed files with 257 additions and 1652 deletions

View file

@ -267,3 +267,25 @@ def team_admin_permissions(gateway: Gateway, fields: Sequence[str]) -> Iterator[
yield
finally:
_set_team_admin_permissions(gateway, [str(field) for field in original] if isinstance(original, list) else ())
def held(read: Callable[[], T], satisfied: Callable[[T], bool], *, holding: float, seconds: float = 10) -> T:
"""`eventually`, and then `satisfied` must hold on every read for `holding` more seconds; a miss restarts the hold."""
deadline: Final = time.monotonic() + seconds
while True:
converged: Final = eventually(read, satisfied, seconds=max(deadline - time.monotonic(), 0.0))
misses: Final = _misses_within(read, satisfied, holding)
if not misses:
return converged
assert time.monotonic() < deadline, f"State did not hold: {misses[0]!r}"
time.sleep(0.1)
def _misses_within(read: Callable[[], T], satisfied: Callable[[T], bool], holding: float) -> tuple[T, ...]:
until: Final = time.monotonic() + holding
while time.monotonic() < until:
time.sleep(0.1)
observed: Final = read()
if not satisfied(observed):
return (observed,)
return ()

View file

@ -1,12 +1,11 @@
from integration._support.process import owned_proxy
import yaml
from pathlib import Path
import json
import uuid
from pathlib import Path
from typing import Final
import httpx
import pytest
import yaml
from integration._support.client import Gateway
from integration._support.mcp import (
ENTRY_POINTS,
@ -25,6 +24,7 @@ from integration._support.mcp import (
text_result,
tool_calls,
)
from integration._support.process import owned_proxy
ADD: Final = {"http": "add", "sse": "add", "stdio": "add", "openapi": "getpet"}
ARGUMENTS: Final = {"add": {"a": 3, "b": 4}, "getpet": {"petId": "7"}}

View file

@ -137,13 +137,15 @@ async def test_modern_input_request_uses_existing_elicitation_callback(
modern_caller: bool, sampling: bool, elicitation_mode: str
) -> None:
from queue import SimpleQueue
from mcp.types import (
ElicitResult,
ElicitRequestParams,
TextContent,
CreateMessageRequestParams,
CreateMessageResult,
ElicitRequestParams,
ElicitResult,
TextContent,
)
from litellm.proxy._experimental.mcp_server.interactions import BoundInputRequiredResult
observed: Final[SimpleQueue[str]] = SimpleQueue()
@ -1883,7 +1885,9 @@ async def test_http_response_handler_preserves_success_and_http_errors(status_co
@pytest.mark.asyncio
async def test_http_status_check_allows_auth_refresh_before_rejecting() -> None:
from litellm.proxy._experimental.mcp_server.outbound_credentials.client_credentials import ClientCredentialsBearerAuth
from litellm.proxy._experimental.mcp_server.outbound_credentials.client_credentials import (
ClientCredentialsBearerAuth,
)
seen = []
@ -2148,6 +2152,7 @@ def test_sse_read_failure_is_preserved() -> None:
@pytest.mark.parametrize("mode", ["ok", "closed", "silent"])
async def test_transport_completion_and_normal_messages(transport: MCPTransport, mode: str, protocol_version: str) -> None:
from mcp import ClientSession
from litellm.proxy._experimental.mcp_server.rest_endpoints import _connection_error_message
logging_callback: Final = AsyncMock()
@ -3637,7 +3642,13 @@ def test_modern_upstream_rejects_legacy_sse_transport() -> None:
("resources/templates/list", "resourceTemplates", {"name": "second", "uriTemplate": "status://{name}"}),
])
async def test_single_catalog_page_preserves_cursor_metadata_and_request_cursor(method, field, item):
from mcp.types import ListToolsRequest, ListPromptsRequest, ListResourcesRequest, ListResourceTemplatesRequest, PaginatedRequestParams
from mcp.types import (
ListPromptsRequest,
ListResourcesRequest,
ListResourceTemplatesRequest,
ListToolsRequest,
PaginatedRequestParams,
)
def respond(request: httpx2.Request) -> httpx2.Response:
if request.method != "POST":
@ -3691,7 +3702,10 @@ async def test_single_catalog_page_preserves_cursor_metadata_and_request_cursor(
@pytest.mark.parametrize("cursor", [None, "continuation"])
async def test_optional_catalog_distinguishes_absent_capability_from_failed_continuation(method, failure, cursor):
from mcp.types import (
ListPromptsRequest, ListResourcesRequest, ListResourceTemplatesRequest, PaginatedRequestParams,
ListPromptsRequest,
ListResourcesRequest,
ListResourceTemplatesRequest,
PaginatedRequestParams,
)
methods = []

View file

@ -1,21 +1,21 @@
import sys
import ast, asyncio, os
import ast
import asyncio
import importlib
import inspect
import json
import os
import sys
from unittest import mock
from unittest.mock import patch
import httpx
import pytest
import respx
from fastapi.testclient import TestClient
import importlib
import litellm
from litellm import constants, get_llm_provider, MorphChatConfig
from litellm import MorphChatConfig, constants, get_llm_provider
from tests._vcr_conftest_common import install_live_call_probe, record_vcr_outcome
from unittest.mock import patch
def _build_constant_env_var_map() -> dict[str, str]:

View file

@ -1,8 +1,10 @@
import datetime
import os
import time
from collections.abc import Mapping
from types import MappingProxyType, SimpleNamespace
from typing import Final, Literal, cast
from unittest.mock import MagicMock, patch
import httpx
import pytest
@ -11,7 +13,16 @@ from pydantic import BaseModel
import litellm
from litellm import TranscriptionResponse, model_cost
from litellm.cost_calculator import BaseTokenUsageProcessor, RealtimeAPITokenUsageProcessor, ResponsesWebSocketTokenUsageProcessor, batch_cost_calculator, completion_cost, cost_per_token, handle_realtime_stream_cost_calculation, response_cost_calculator
from litellm.cost_calculator import (
BaseTokenUsageProcessor,
RealtimeAPITokenUsageProcessor,
ResponsesWebSocketTokenUsageProcessor,
batch_cost_calculator,
completion_cost,
cost_per_token,
handle_realtime_stream_cost_calculation,
response_cost_calculator,
)
from litellm.litellm_core_utils.litellm_logging import Logging
from litellm.litellm_core_utils.llm_response_utils.convert_dict_to_response import (
convert_to_model_response_object,
@ -23,11 +34,25 @@ from litellm.llms.together_ai.cost_calculator import get_model_params_and_catego
from litellm.types.llms.base import CachedTokensDetails
from litellm.types.llms.openai import OpenAIRealtimeStreamList, ResponseAPIUsage, ResponsesAPIResponse
from litellm.types.rerank import RerankResponse
from litellm.types.utils import CallTypes, Choices, CompletionTokensDetailsWrapper, EmbeddingResponse, ImageObject, ImageResponse, ImageUsage, ImageUsageInputTokensDetails, LiteLLMRealtimeStreamLoggingObject, Message, ModelInfo, ModelResponse, PromptTokensDetails, PromptTokensDetailsWrapper, Usage
from litellm.types.utils import (
CallTypes,
Choices,
CompletionTokensDetailsWrapper,
EmbeddingResponse,
ImageObject,
ImageResponse,
ImageUsage,
ImageUsageInputTokensDetails,
LiteLLMRealtimeStreamLoggingObject,
Message,
ModelInfo,
ModelResponse,
PromptTokensDetails,
PromptTokensDetailsWrapper,
Usage,
)
from litellm.types.videos.main import VideoObject
from litellm.utils import supports_prompt_caching
import os
from unittest.mock import MagicMock, patch
@pytest.fixture
@ -7801,7 +7826,7 @@ def test_completion_cost_model_response_cost(response_model, custom_llm_provider
def test_select_model_name_for_cost_calc():
from litellm.cost_calculator import select_model_name_for_cost_calc
from litellm.types.utils import ModelResponse, Choices, Usage, Message
from litellm.types.utils import Choices, Message, ModelResponse, Usage
args = {
"model": "Mistral-large-nmefg",

View file

@ -3249,7 +3249,9 @@ class TestExtraBodyCannotOverrideModel:
}
]
untouched = litellm.get_optional_params(model="my-vllm-model", custom_llm_provider="hosted_vllm", tools=tools())
untouched = litellm.get_optional_params(
model="my-vllm-model", custom_llm_provider="hosted_vllm", tools=tools()
)
assert untouched["tools"][0]["function"]["custom_marker"] == "LEAK", untouched
result = litellm.get_optional_params(
@ -4240,9 +4242,7 @@ def test_is_prompt_caching_valid_prompt_stops_counting_once_the_minimum_is_reach
assert is_prompt_caching_valid_prompt(model="claude-opus-4-8", messages=long_prompt, min_token_count=1024) is True
assert sum(counted_messages) < len(long_prompt), sum(counted_messages)
full_count = litellm.token_counter(
model="claude-opus-4-8", messages=long_prompt, use_default_image_token_count=True
)
full_count = litellm.token_counter(model="claude-opus-4-8", messages=long_prompt, use_default_image_token_count=True)
assert (
is_prompt_caching_valid_prompt(model="claude-opus-4-8", messages=long_prompt, min_token_count=full_count)
is True
@ -4709,13 +4709,7 @@ _SUCCESS_RESPONSES_BY_CALL_TYPE: Final = (
pytest.param(EmbeddingResponse(model="text-embedding-3-small"), CallTypes.aembedding, id="embedding"),
pytest.param(
ResponsesAPIResponse(
id="resp_abc",
created_at=1,
output=[],
parallel_tool_calls=False,
tool_choice="auto",
tools=[],
model="gpt-5.6",
id="resp_abc", created_at=1, output=[], parallel_tool_calls=False, tool_choice="auto", tools=[], model="gpt-5.6"
),
CallTypes.aresponses,
id="responses",
@ -4743,9 +4737,7 @@ async def test_success_deployment_hook_raising_keeps_response_and_runs_later_hoo
assert result is response
assert second_hook.seen_responses == (response,)
failure_logs: Final = tuple(
r for r in caplog.records if "async_post_call_success_deployment_hook error" in r.message
)
failure_logs: Final = tuple(r for r in caplog.records if "async_post_call_success_deployment_hook error" in r.message)
assert len(failure_logs) == 1
assert "_ChatShapedSuccessDeploymentHook" in failure_logs[0].message
assert str(call_type) in failure_logs[0].message
@ -4882,13 +4874,17 @@ async def test_wrapper_async_logs_converted_responses_stream_with_standard_loggi
({"metadata": None}, True, False),
],
)
def test_is_litellm_router_call_is_async_aware(kwargs: Mapping[str, object], is_async: bool, expected: bool) -> None:
def test_is_litellm_router_call_is_async_aware(
kwargs: Mapping[str, object], is_async: bool, expected: bool
) -> None:
assert _is_litellm_router_call(kwargs, is_async=is_async) is expected
@pytest.mark.asyncio
@pytest.mark.parametrize("stream", [False, True], ids=["non_streaming", "streaming"])
async def test_router_aresponses_does_not_run_sdk_retries(monkeypatch: pytest.MonkeyPatch, stream: bool) -> None:
async def test_router_aresponses_does_not_run_sdk_retries(
monkeypatch: pytest.MonkeyPatch, stream: bool
) -> None:
monkeypatch.setattr(litellm, "disable_aiohttp_transport", True)
litellm.in_memory_llm_clients_cache.flush_cache()
model_list: Final = [
@ -4902,7 +4898,9 @@ async def test_router_aresponses_does_not_run_sdk_retries(monkeypatch: pytest.Mo
},
}
]
router: Final = litellm.Router(model_list=model_list, num_retries=0, retry_after=0, disable_cooldowns=True)
router: Final = litellm.Router(
model_list=model_list, num_retries=0, retry_after=0, disable_cooldowns=True
)
try:
with respx.mock(assert_all_called=True) as respx_mock:
@ -4938,7 +4936,9 @@ def test_router_responses_keeps_sdk_retries_for_sync_router_call(
},
}
]
router: Final = litellm.Router(model_list=model_list, num_retries=0, retry_after=0, disable_cooldowns=True)
router: Final = litellm.Router(
model_list=model_list, num_retries=0, retry_after=0, disable_cooldowns=True
)
try:
with respx.mock(assert_all_called=True) as respx_mock:
@ -6782,9 +6782,7 @@ def test_function_setup_logs_the_search_query_edit_prompt_and_ocr_document_summa
@pytest.mark.parametrize("original_function", ("atext_completion", "text_completion"))
def test_function_setup_without_a_prompt_leaves_the_missing_prompt_to_request_validation(
original_function: str,
) -> None:
def test_function_setup_without_a_prompt_leaves_the_missing_prompt_to_request_validation(original_function: str) -> None:
assert _logged_request_messages(original_function, model="gpt-4o") is None
@ -6826,7 +6824,6 @@ def _vcr_outcome_gate(request, vcr):
yield
record_vcr_outcome(request, vcr)
@pytest.fixture(scope="function")
def setup_and_teardown():
"""
@ -6840,7 +6837,6 @@ def setup_and_teardown():
loop.close()
asyncio.set_event_loop(None)
MODEL: Final = "anthropic/claude-haiku-4-5"
@ -6850,7 +6846,6 @@ def test_validate_tool_choice_none():
result = validate_chat_completion_tool_choice(None, model=MODEL)
assert result is None
@pytest.mark.usefixtures("_vcr_outcome_gate", "setup_and_teardown")
def test_validate_tool_choice_string():
"""Test that string values are returned as-is."""
@ -6858,7 +6853,6 @@ def test_validate_tool_choice_string():
assert validate_chat_completion_tool_choice("none", model=MODEL) == "none"
assert validate_chat_completion_tool_choice("required", model=MODEL) == "required"
@pytest.mark.usefixtures("_vcr_outcome_gate", "setup_and_teardown")
def test_validate_tool_choice_standard_dict():
"""Test standard OpenAI format with function."""
@ -6866,7 +6860,6 @@ def test_validate_tool_choice_standard_dict():
result = validate_chat_completion_tool_choice(tool_choice, model=MODEL)
assert result == tool_choice
@pytest.mark.usefixtures("_vcr_outcome_gate", "setup_and_teardown")
def test_validate_tool_choice_cursor_format():
"""Cursor IDE format {"type": "auto"} is unwrapped to the bare string."""
@ -6874,7 +6867,6 @@ def test_validate_tool_choice_cursor_format():
assert validate_chat_completion_tool_choice({"type": "none"}, model=MODEL) == "none"
assert validate_chat_completion_tool_choice({"type": "required"}, model=MODEL) == "required"
@pytest.mark.usefixtures("_vcr_outcome_gate", "setup_and_teardown")
@pytest.mark.parametrize(
"tool_choice",
@ -6896,7 +6888,6 @@ def test_validate_tool_choice_invalid_dict_is_a_400(tool_choice):
assert exc_info.value.status_code == 400
assert exc_info.value.model == MODEL
@pytest.mark.usefixtures("_vcr_outcome_gate", "setup_and_teardown")
@pytest.mark.parametrize("tool_choice", [123, []])
def test_validate_tool_choice_invalid_type_is_a_400(tool_choice):
@ -6907,7 +6898,6 @@ def test_validate_tool_choice_invalid_type_is_a_400(tool_choice):
validate_chat_completion_tool_choice(tool_choice, model=MODEL)
assert exc_info.value.status_code == 400
@pytest.mark.usefixtures("_vcr_outcome_gate", "setup_and_teardown")
def test_validate_tool_choice_without_model_is_still_a_400():
"""Callers that predate the model argument keep getting a 400, with an empty model on the error."""
@ -6916,14 +6906,12 @@ def test_validate_tool_choice_without_model_is_still_a_400():
assert exc_info.value.status_code == 400
assert exc_info.value.model == ""
@pytest.fixture()
def _vcr_outcome_gate_local_testing(request, vcr):
install_live_call_probe(request, vcr)
yield
record_vcr_outcome(request, vcr)
@pytest.fixture(scope="function")
def isolate_litellm_state():
"""
@ -6976,7 +6964,6 @@ def isolate_litellm_state():
setattr(litellm, attr, original_value)
_invalidate_model_cost_lowercase_map()
_SCALAR_DEFAULTS = {
"num_retries": getattr(litellm, "num_retries", None),
"num_retries_per_request": getattr(litellm, "num_retries_per_request", None),
@ -7020,7 +7007,6 @@ def setup_and_teardown_local_testing():
litellm.in_memory_llm_clients_cache.flush_cache()
yield
@pytest.mark.usefixtures(
"_vcr_outcome_gate_local_testing",
"isolate_litellm_state",
@ -7046,7 +7032,6 @@ def test_vertex_projects():
assert "vertex_ai_project" in optional_params
assert "vertex_ai_location" in optional_params
@pytest.mark.usefixtures(
"_vcr_outcome_gate_local_testing",
"isolate_litellm_state",
@ -7062,7 +7047,6 @@ def test_bedrock_embed_v2_regular():
print(f"received optional_params: {optional_params}")
assert optional_params == {"dimensions": 512}
@pytest.mark.usefixtures(
"_vcr_outcome_gate_local_testing",
"isolate_litellm_state",
@ -7081,7 +7065,6 @@ def test_bedrock_embed_v2_with_drop_params():
print(f"received optional_params: {optional_params}")
assert optional_params == {"dimensions": 512, "embeddingTypes": ["binary"]}
@pytest.mark.usefixtures(
"_vcr_outcome_gate_local_testing",
"isolate_litellm_state",
@ -7103,7 +7086,6 @@ def test_openai_non_text_embedding_3_with_allowed_openai_params():
print(f"received optional_params: {optional_params}")
assert optional_params.get("dimensions") == 1024
@pytest.mark.usefixtures(
"_vcr_outcome_gate_local_testing",
"isolate_litellm_state",
@ -7130,7 +7112,6 @@ def test_openai_non_text_embedding_3_without_allowed_openai_params_raises():
finally:
litellm.drop_params = prev_drop_params
@pytest.mark.usefixtures(
"_vcr_outcome_gate_local_testing",
"isolate_litellm_state",
@ -7159,7 +7140,6 @@ def test_openai_non_text_embedding_3_drop_params_per_call():
finally:
litellm.drop_params = prev_drop_params
@pytest.mark.usefixtures(
"_vcr_outcome_gate_local_testing",
"isolate_litellm_state",
@ -7187,14 +7167,12 @@ def test_openai_non_text_embedding_3_drop_params_global():
finally:
litellm.drop_params = prev_drop_params
@pytest.fixture()
def _vcr_outcome_gate_search_tests(request, vcr):
install_live_call_probe(request, vcr)
yield
record_vcr_outcome(request, vcr)
@pytest.mark.usefixtures("_vcr_outcome_gate_search_tests")
def test_search_tool_name_in_all_litellm_params():
"""
@ -7204,7 +7182,6 @@ def test_search_tool_name_in_all_litellm_params():
"""
assert "search_tool_name" in all_litellm_params
@pytest.mark.usefixtures("_vcr_outcome_gate_search_tests")
def test_filter_out_search_tool_name():
"""
@ -7231,7 +7208,6 @@ def test_filter_out_search_tool_name():
assert filtered["query"] == "latest ai developments"
assert filtered["max_results"] == 5
@pytest.mark.asyncio
async def test_nested_wrapper_exits_schedule_one_async_success_log(monkeypatch: pytest.MonkeyPatch) -> None:
"""Chat over the Responses bridge exits two @client wrappers with one logging object. Issue
@ -7384,7 +7360,9 @@ def test_trimming_with_system_message_within_max_tokens():
"content": "This is a medium normal message, let's say litellm is awesome.",
},
]
trimmed_messages = trim_messages(messages, max_tokens=30, model="gpt-4-0613")
trimmed_messages = trim_messages(
messages, max_tokens=30, model="gpt-4-0613"
)
assert len(trimmed_messages) == 2
assert trimmed_messages[0]["content"] == "This is a short system message"
@ -7567,7 +7545,9 @@ def test_aget_valid_models():
print(valid_models)
# list of openai supported llms on litellm
expected_models = litellm.open_ai_chat_completion_models | litellm.open_ai_text_completion_models
expected_models = (
litellm.open_ai_chat_completion_models | litellm.open_ai_text_completion_models
)
assert set(valid_models) == set(expected_models)
@ -7656,7 +7636,9 @@ def test_get_chat_completion_prompt():
prompt_variables=None,
)
assert litellm_logging_obj.messages == [{"role": "user", "content": updated_message}]
assert litellm_logging_obj.messages == [
{"role": "user", "content": updated_message}
]
def test_redact_msgs_from_logs():
@ -7728,7 +7710,9 @@ def test_redact_embedding_response():
litellm.turn_off_message_logging = True
# Create a test EmbeddingResponse with usage data
original_usage = litellm.Usage(prompt_tokens=10, completion_tokens=0, total_tokens=10)
original_usage = litellm.Usage(
prompt_tokens=10, completion_tokens=0, total_tokens=10
)
original_data = [
{"object": "embedding", "index": 0, "embedding": [0.1, 0.2, 0.3, 0.4, 0.5]},
{"object": "embedding", "index": 1, "embedding": [0.6, 0.7, 0.8, 0.9, 1.0]},
@ -7764,7 +7748,9 @@ def test_redact_embedding_response():
# Assert the redacted response preserves critical metadata
assert _redacted_response_obj.usage == original_usage # usage should be preserved
assert _redacted_response_obj.model == "text-embedding-3-small" # model should be preserved
assert (
_redacted_response_obj.model == "text-embedding-3-small"
) # model should be preserved
assert _redacted_response_obj.object == "list" # object should be preserved
# Assert sensitive data is cleared
@ -7818,8 +7804,12 @@ def test_redact_msgs_from_logs_with_dynamic_params():
)
# Test Case 1: standard_callback_dynamic_params = False (or not set)
standard_callback_dynamic_params = StandardCallbackDynamicParams(turn_off_message_logging=False)
litellm_logging_obj.model_call_details["standard_callback_dynamic_params"] = standard_callback_dynamic_params
standard_callback_dynamic_params = StandardCallbackDynamicParams(
turn_off_message_logging=False
)
litellm_logging_obj.model_call_details["standard_callback_dynamic_params"] = (
standard_callback_dynamic_params
)
_redacted_response_obj = redact_message_input_output_from_logging(
result=response_obj,
model_call_details=litellm_logging_obj.model_call_details,
@ -7828,8 +7818,12 @@ def test_redact_msgs_from_logs_with_dynamic_params():
assert _redacted_response_obj.choices[0].message.content == test_content
# Test Case 2: standard_callback_dynamic_params = True
standard_callback_dynamic_params = StandardCallbackDynamicParams(turn_off_message_logging=True)
litellm_logging_obj.model_call_details["standard_callback_dynamic_params"] = standard_callback_dynamic_params
standard_callback_dynamic_params = StandardCallbackDynamicParams(
turn_off_message_logging=True
)
litellm_logging_obj.model_call_details["standard_callback_dynamic_params"] = (
standard_callback_dynamic_params
)
_redacted_response_obj = redact_message_input_output_from_logging(
result=response_obj,
model_call_details=litellm_logging_obj.model_call_details,
@ -7840,7 +7834,9 @@ def test_redact_msgs_from_logs_with_dynamic_params():
# Test Case 3: standard_callback_dynamic_params does not set turn_off_message_logging
# since litellm.turn_off_message_logging is True redaction should occur
standard_callback_dynamic_params = StandardCallbackDynamicParams()
litellm_logging_obj.model_call_details["standard_callback_dynamic_params"] = standard_callback_dynamic_params
litellm_logging_obj.model_call_details["standard_callback_dynamic_params"] = (
standard_callback_dynamic_params
)
_redacted_response_obj = redact_message_input_output_from_logging(
result=response_obj,
model_call_details=litellm_logging_obj.model_call_details,
@ -8184,7 +8180,10 @@ def test_models_by_provider():
continue
elif k == "sample_spec":
continue
elif v["litellm_provider"] == "sagemaker" or v["litellm_provider"] == "bedrock_converse":
elif (
v["litellm_provider"] == "sagemaker"
or v["litellm_provider"] == "bedrock_converse"
):
continue
elif v.get("mode") in ("search", "decisions", "evaluation"):
continue
@ -8192,7 +8191,9 @@ def test_models_by_provider():
providers.add(v["litellm_provider"])
for provider in providers:
assert provider in models_by_provider.keys() or JSONProviderRegistry.exists(provider)
assert provider in models_by_provider.keys() or JSONProviderRegistry.exists(
provider
)
@pytest.fixture
@ -8214,11 +8215,16 @@ def restore_end_user_cost_tracking_flags(monkeypatch: pytest.MonkeyPatch) -> Non
({"user_api_key_end_user_id": "123"}, True, None),
],
)
def test_get_end_user_id_for_cost_tracking(litellm_params, disable_end_user_cost_tracking, expected_end_user_id):
def test_get_end_user_id_for_cost_tracking(
litellm_params, disable_end_user_cost_tracking, expected_end_user_id
):
from litellm.utils import get_end_user_id_for_cost_tracking
litellm.disable_end_user_cost_tracking = disable_end_user_cost_tracking
assert get_end_user_id_for_cost_tracking(litellm_params=litellm_params) == expected_end_user_id
assert (
get_end_user_id_for_cost_tracking(litellm_params=litellm_params)
== expected_end_user_id
)
@pytest.mark.usefixtures("restore_end_user_cost_tracking_flags")
@ -8235,9 +8241,13 @@ def test_get_end_user_id_for_cost_tracking_prometheus_only(
):
from litellm.utils import get_end_user_id_for_cost_tracking
litellm.enable_end_user_cost_tracking_prometheus_only = enable_end_user_cost_tracking_prometheus_only
litellm.enable_end_user_cost_tracking_prometheus_only = (
enable_end_user_cost_tracking_prometheus_only
)
assert (
get_end_user_id_for_cost_tracking(litellm_params=litellm_params, service_type="prometheus")
get_end_user_id_for_cost_tracking(
litellm_params=litellm_params, service_type="prometheus"
)
== expected_end_user_id
)
@ -8252,14 +8262,20 @@ def test_get_end_user_id_for_cost_tracking_prometheus_only(
),
# Test with only litellm_metadata field (new behavior)
(
{"litellm_metadata": {"user_api_key_end_user_id": "user_from_litellm_metadata"}},
{
"litellm_metadata": {
"user_api_key_end_user_id": "user_from_litellm_metadata"
}
},
"user_from_litellm_metadata",
),
# Test with both fields - metadata should take precedence for user_api_key fields
(
{
"metadata": {"user_api_key_end_user_id": "user_from_metadata"},
"litellm_metadata": {"user_api_key_end_user_id": "user_from_litellm_metadata"},
"litellm_metadata": {
"user_api_key_end_user_id": "user_from_litellm_metadata"
},
},
"user_from_metadata",
),
@ -8275,7 +8291,9 @@ def test_get_end_user_id_for_cost_tracking_prometheus_only(
(
{
"metadata": {},
"litellm_metadata": {"user_api_key_end_user_id": "user_from_litellm_metadata"},
"litellm_metadata": {
"user_api_key_end_user_id": "user_from_litellm_metadata"
},
},
"user_from_litellm_metadata",
),
@ -8283,7 +8301,9 @@ def test_get_end_user_id_for_cost_tracking_prometheus_only(
({}, None),
],
)
def test_get_end_user_id_for_cost_tracking_metadata_handling(litellm_params, expected_end_user_id):
def test_get_end_user_id_for_cost_tracking_metadata_handling(
litellm_params, expected_end_user_id
):
"""
Test that get_end_user_id_for_cost_tracking correctly handles both metadata and litellm_metadata
fields using the get_litellm_metadata_from_kwargs helper function.
@ -8542,7 +8562,9 @@ def test_add_custom_logger_callback_to_specific_event_e2e_failure(monkeypatch):
assert len(litellm.success_callback) == curr_len_success_callback
assert len(litellm.failure_callback) == curr_len_failure_callback
assert any(isinstance(callback, OpenMeterLogger) for callback in litellm.failure_callback)
assert any(
isinstance(callback, OpenMeterLogger) for callback in litellm.failure_callback
)
@pytest.mark.asyncio
@ -8639,14 +8661,20 @@ def test_validate_user_messages_invalid_content_type():
[
{
"name": "default_on_guardrail",
"callbacks": [CustomGuardrail(guardrail_name="test_guardrail", default_on=True)],
"callbacks": [
CustomGuardrail(guardrail_name="test_guardrail", default_on=True)
],
"kwargs": {"metadata": {"requester_metadata": {"guardrails": []}}},
"expected": ["test_guardrail"],
},
{
"name": "request_specific_guardrail",
"callbacks": [CustomGuardrail(guardrail_name="test_guardrail", default_on=False)],
"kwargs": {"metadata": {"requester_metadata": {"guardrails": ["test_guardrail"]}}},
"callbacks": [
CustomGuardrail(guardrail_name="test_guardrail", default_on=False)
],
"kwargs": {
"metadata": {"requester_metadata": {"guardrails": ["test_guardrail"]}}
},
"expected": ["test_guardrail"],
},
{
@ -8655,12 +8683,18 @@ def test_validate_user_messages_invalid_content_type():
CustomGuardrail(guardrail_name="default_guardrail", default_on=True),
CustomGuardrail(guardrail_name="request_guardrail", default_on=False),
],
"kwargs": {"metadata": {"requester_metadata": {"guardrails": ["request_guardrail"]}}},
"kwargs": {
"metadata": {
"requester_metadata": {"guardrails": ["request_guardrail"]}
}
},
"expected": ["default_guardrail", "request_guardrail"],
},
{
"name": "empty_metadata",
"callbacks": [CustomGuardrail(guardrail_name="test_guardrail", default_on=False)],
"callbacks": [
CustomGuardrail(guardrail_name="test_guardrail", default_on=False)
],
"kwargs": {},
"expected": [],
},