fix(proxy): forward every method on the typesafe pass-through route

Backport of #41723 to stable/1.102.x.
Cherry-picked from 34718f0da6 (main).
This commit is contained in:
yuneng-jiang 2026-09-22 22:24:06 +00:00 • committed by mateo
parent b5ac61374e
commit 79b72b8594
5 changed files with 512 additions and 574 deletions

View file

@ -19996,6 +19996,50 @@
}
},
"/typesafe/{endpoint}": {
"delete": {
"description": "[Docs](https://docs.litellm.ai/docs/pass_through/typesafe)",
"operationId": "typesafe_proxy_route_typesafe__endpoint__delete",
"parameters": [
{
"in": "path",
"name": "endpoint",
"required": true,
"schema": {
"title": "Endpoint",
"type": "string"
}
}
],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {}
}
},
"description": "Successful Response"
},
"422": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
"description": "Validation Error"
}
},
"security": [
{
"APIKeyHeader": []
}
],
"summary": "Typesafe Proxy Route",
"tags": [
"llm_passthrough"
]
},
"get": {
"description": "[Docs](https://docs.litellm.ai/docs/pass_through/typesafe)",
"operationId": "typesafe_proxy_route_typesafe__endpoint__get",
@ -20040,6 +20084,50 @@
"llm_passthrough"
]
},
"patch": {
"description": "[Docs](https://docs.litellm.ai/docs/pass_through/typesafe)",
"operationId": "typesafe_proxy_route_typesafe__endpoint__patch",
"parameters": [
{
"in": "path",
"name": "endpoint",
"required": true,
"schema": {
"title": "Endpoint",
"type": "string"
}
}
],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {}
}
},
"description": "Successful Response"
},
"422": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
"description": "Validation Error"
}
},
"security": [
{
"APIKeyHeader": []
}
],
"summary": "Typesafe Proxy Route",
"tags": [
"llm_passthrough"
]
},
"post": {
"description": "[Docs](https://docs.litellm.ai/docs/pass_through/typesafe)",
"operationId": "typesafe_proxy_route_typesafe__endpoint__post",
@ -20083,6 +20171,50 @@
"tags": [
"llm_passthrough"
]
},
"put": {
"description": "[Docs](https://docs.litellm.ai/docs/pass_through/typesafe)",
"operationId": "typesafe_proxy_route_typesafe__endpoint__put",
"parameters": [
{
"in": "path",
"name": "endpoint",
"required": true,
"schema": {
"title": "Endpoint",
"type": "string"
}
}
],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {}
}
},
"description": "Successful Response"
},
"422": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
"description": "Validation Error"
}
},
"security": [
{
"APIKeyHeader": []
}
],
"summary": "Typesafe Proxy Route",
"tags": [
"llm_passthrough"
]
}
},
"/vertex_ai/discovery/{endpoint}": {

View file

@ -524,7 +524,7 @@ async def mistral_proxy_route(
@router.api_route(
"/typesafe/{endpoint:path}",
methods=["GET", "POST"], # mutable-ok: FastAPI route metadata requires a list
methods=["GET", "POST", "PUT", "DELETE", "PATCH"], # mutable-ok: FastAPI route metadata requires a list
tags=["TypeSafe AI Pass-through", "pass-through"], # mutable-ok: FastAPI route metadata requires a list
)
async def typesafe_proxy_route(

View file

@ -75,9 +75,7 @@ def test_basic_trimming_no_max_tokens_specified():
print("trimmed messages for gpt-4")
print(trimmed_messages)
# print(get_token_count(messages=trimmed_messages, model="claude-2"))
assert (
get_token_count(messages=trimmed_messages, model="gpt-4")
) <= litellm.model_cost["gpt-4"]["max_tokens"]
assert (get_token_count(messages=trimmed_messages, model="gpt-4")) <= litellm.model_cost["gpt-4"]["max_tokens"]
# test_basic_trimming_no_max_tokens_specified()
@ -94,9 +92,7 @@ def test_multiple_messages_trimming():
"content": "This is another long message that will also exceed the limit.",
},
]
trimmed_messages = trim_messages(
messages=messages, model="gpt-3.5-turbo", max_tokens=20
)
trimmed_messages = trim_messages(messages=messages, model="gpt-3.5-turbo", max_tokens=20)
# print(get_token_count(messages=trimmed_messages, model="gpt-3.5-turbo"))
assert (get_token_count(messages=trimmed_messages, model="gpt-3.5-turbo")) <= 20
@ -115,9 +111,7 @@ def test_multiple_messages_no_trimming():
"content": "This is another long message that will also exceed the limit.",
},
]
trimmed_messages = trim_messages(
messages=messages, model="gpt-3.5-turbo", max_tokens=100
)
trimmed_messages = trim_messages(messages=messages, model="gpt-3.5-turbo", max_tokens=100)
print("Trimmed messages")
print(trimmed_messages)
assert messages == trimmed_messages
@ -144,9 +138,7 @@ def test_large_trimming_multiple_messages():
def test_large_trimming_single_message():
messages = [
{"role": "user", "content": "This is a singlelongwordthatexceedsthelimit."}
]
messages = [{"role": "user", "content": "This is a singlelongwordthatexceedsthelimit."}]
trimmed_messages = trim_messages(messages, max_tokens=5, model="gpt-4-0613")
assert (get_token_count(messages=trimmed_messages, model="gpt-4-0613")) <= 5
assert (get_token_count(messages=trimmed_messages, model="gpt-4-0613")) > 0
@ -277,10 +269,7 @@ def test_trimming_with_model_cost_max_input_tokens(model):
},
]
trimmed_messages = trim_messages(messages, model=model)
assert (
get_token_count(trimmed_messages, model=model)
< litellm.model_cost[model]["max_input_tokens"]
)
assert get_token_count(trimmed_messages, model=model) < litellm.model_cost[model]["max_input_tokens"]
def test_trimming_with_untokenizable_field(caplog: pytest.LogCaptureFixture) -> None:
@ -333,9 +322,7 @@ 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)
@ -357,9 +344,7 @@ def test_get_valid_models_with_custom_llm_provider(custom_llm_provider):
provider=LlmProviders(custom_llm_provider),
)
assert provider_config is not None
valid_models = get_valid_models(
check_provider_endpoint=True, custom_llm_provider=custom_llm_provider
)
valid_models = get_valid_models(check_provider_endpoint=True, custom_llm_provider=custom_llm_provider)
print(valid_models)
assert len(valid_models) > 0
assert set(provider_config.get_models()) == set(valid_models)
@ -392,9 +377,7 @@ def test_validate_environment_empty_model():
def test_validate_environment_api_key():
response_obj = validate_environment(model="gpt-5-mini", api_key="sk-my-test-key")
assert (
response_obj["keys_in_environment"] is True
), f"Missing keys={response_obj['missing_keys']}"
assert response_obj["keys_in_environment"] is True, f"Missing keys={response_obj['missing_keys']}"
def test_validate_environment_api_version():
@ -404,9 +387,7 @@ def test_validate_environment_api_version():
api_base="https://fake.openai.azure.com/",
api_version="2024-02-15",
)
assert (
response_obj["keys_in_environment"] is True
), f"Missing keys={response_obj['missing_keys']}"
assert response_obj["keys_in_environment"] is True, f"Missing keys={response_obj['missing_keys']}"
def test_validate_environment_api_base_dynamic():
@ -481,18 +462,14 @@ def test_function_to_dict():
assert function_json["description"] == expected_output["description"]
assert function_json["parameters"]["type"] == expected_output["parameters"]["type"]
assert (
function_json["parameters"]["properties"]["location"]
== expected_output["parameters"]["properties"]["location"]
function_json["parameters"]["properties"]["location"] == expected_output["parameters"]["properties"]["location"]
)
# the enum can change it can be - which is why we don't assert on unit
# {'type': 'string', 'description': 'Temperature unit', 'enum': "['fahrenheit', 'celsius']"}
# {'type': 'string', 'description': 'Temperature unit', 'enum': "['celsius', 'fahrenheit']"}
assert (
function_json["parameters"]["required"]
== expected_output["parameters"]["required"]
)
assert function_json["parameters"]["required"] == expected_output["parameters"]["required"]
print("passed")
@ -561,9 +538,7 @@ def test_get_max_token_unit_test():
"""
model = "bedrock/anthropic.claude-3-haiku-20240307-v1:0"
max_tokens = get_max_tokens(
model
) # Returns a number instead of throwing an Exception
max_tokens = get_max_tokens(model) # Returns a number instead of throwing an Exception
assert isinstance(max_tokens, int)
@ -602,9 +577,7 @@ 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():
@ -676,9 +649,7 @@ 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]},
@ -714,9 +685,7 @@ 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
@ -770,12 +739,8 @@ 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,
@ -784,12 +749,8 @@ 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,
@ -800,9 +761,7 @@ 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,
@ -907,9 +866,7 @@ def test_get_llm_provider_ft_models():
@pytest.mark.parametrize("langfuse_trace_id", [None, "my-unique-trace-id"])
@pytest.mark.parametrize(
"langfuse_existing_trace_id", [None, "my-unique-existing-trace-id"]
)
@pytest.mark.parametrize("langfuse_existing_trace_id", [None, "my-unique-existing-trace-id"])
def test_logging_trace_id(langfuse_trace_id, langfuse_existing_trace_id):
"""
- Unit test for `_get_trace_id` function in Logging obj
@ -948,22 +905,13 @@ def test_logging_trace_id(langfuse_trace_id, langfuse_existing_trace_id):
## if existing_trace_id exists
if langfuse_existing_trace_id is not None:
assert (
litellm_logging_obj._get_trace_id(service_name="langfuse")
== langfuse_existing_trace_id
)
assert litellm_logging_obj._get_trace_id(service_name="langfuse") == langfuse_existing_trace_id
## if trace_id exists
elif langfuse_trace_id is not None:
assert (
litellm_logging_obj._get_trace_id(service_name="langfuse")
== langfuse_trace_id
)
assert litellm_logging_obj._get_trace_id(service_name="langfuse") == langfuse_trace_id
## if no trace_id or existing_trace_id is provided, use litellm_trace_id
else:
assert (
litellm_logging_obj._get_trace_id(service_name="langfuse")
== litellm_logging_obj.litellm_trace_id
)
assert litellm_logging_obj._get_trace_id(service_name="langfuse") == litellm_logging_obj.litellm_trace_id
def test_convert_model_response_object():
@ -1154,9 +1102,7 @@ def test_async_http_handler(mock_async_client):
concurrent_limit = 2
# Mock the transport creation to return a specific transport
with mock.patch.object(
AsyncHTTPHandler, "_create_async_transport"
) as mock_create_transport:
with mock.patch.object(AsyncHTTPHandler, "_create_async_transport") as mock_create_transport:
mock_transport = mock.MagicMock()
mock_create_transport.return_value = mock_transport
@ -1221,9 +1167,7 @@ def test_async_http_handler_force_ipv4(mock_async_client):
litellm.force_ipv4 = False
@pytest.mark.parametrize(
"model, expected_bool", [("gpt-3.5-turbo", False), ("gpt-4o-audio-preview", True)]
)
@pytest.mark.parametrize("model, expected_bool", [("gpt-3.5-turbo", False), ("gpt-4o-audio-preview", True)])
def test_supports_audio_input(model, expected_bool):
os.environ["LITELLM_LOCAL_MODEL_COST_MAP"] = "True"
litellm.model_cost = litellm.get_model_cost_map(url="")
@ -1277,9 +1221,7 @@ def test_is_base64_encoded_2():
[
{
"role": "user",
"content": [
{"type": "image_url", "url": "https://example.com/image.png"}
],
"content": [{"type": "image_url", "url": "https://example.com/image.png"}],
}
],
True,
@ -1355,21 +1297,15 @@ 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") == "search":
# Skip search providers as they don't have traditional models
elif v.get("mode") in ("search", "evaluation"):
continue
else:
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.mark.parametrize(
@ -1380,16 +1316,11 @@ def test_models_by_provider():
({"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.parametrize(
@ -1405,13 +1336,9 @@ 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
)
@ -1426,20 +1353,14 @@ 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",
),
@ -1455,9 +1376,7 @@ 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",
),
@ -1465,9 +1384,7 @@ 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.
@ -1631,9 +1548,7 @@ def test_get_valid_models_openai_proxy(monkeypatch):
mock_response.status_code = 200
mock_response.json.return_value = mock_response_data
with patch.object(
litellm.module_level_client, "get", return_value=mock_response
) as mock_post:
with patch.object(litellm.module_level_client, "get", return_value=mock_response) as mock_post:
valid_models = get_valid_models(check_provider_endpoint=True)
assert "litellm_proxy/gpt-5.5" in valid_models
@ -1710,16 +1625,11 @@ def test_get_valid_models_fireworks_ai(monkeypatch):
mock_response.status_code = 200
mock_response.json.return_value = mock_response_data
with patch.object(
litellm.module_level_client, "get", return_value=mock_response
) as mock_post:
with patch.object(litellm.module_level_client, "get", return_value=mock_response) as mock_post:
valid_models = get_valid_models(check_provider_endpoint=True)
print("valid_models", valid_models)
mock_post.assert_called_once()
assert (
"fireworks_ai/accounts/fireworks/models/llama-3.1-8b-instruct"
in valid_models
)
assert "fireworks_ai/accounts/fireworks/models/llama-3.1-8b-instruct" in valid_models
def test_get_valid_models_default(monkeypatch):
@ -1758,9 +1668,7 @@ def test_pick_cheapest_chat_model_from_llm_provider():
def test_get_num_retries(num_retries):
from litellm.utils import _get_wrapper_num_retries
assert _get_wrapper_num_retries(
kwargs={"num_retries": num_retries}, exception=Exception("test")
) == (
assert _get_wrapper_num_retries(kwargs={"num_retries": num_retries}, exception=Exception("test")) == (
num_retries,
{
"num_retries": num_retries,
@ -2033,9 +1941,7 @@ 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
@ -2062,20 +1968,13 @@ async def test_wrapper_kwargs_passthrough():
mock_original.assert_called_once()
# get litellm logging object
litellm_logging_obj: LiteLLMLoggingObject = mock_original.call_args.kwargs.get(
"litellm_logging_obj"
)
litellm_logging_obj: LiteLLMLoggingObject = mock_original.call_args.kwargs.get("litellm_logging_obj")
assert litellm_logging_obj is not None
print(
f"litellm_logging_obj.model_call_details: {litellm_logging_obj.model_call_details}"
)
print(f"litellm_logging_obj.model_call_details: {litellm_logging_obj.model_call_details}")
# get base model
assert (
litellm_logging_obj.model_call_details["litellm_params"]["base_model"]
== "gpt-5-mini"
)
assert litellm_logging_obj.model_call_details["litellm_params"]["base_model"] == "gpt-5-mini"
def test_dict_to_response_format_helper():
@ -2129,7 +2028,7 @@ def test_validate_user_messages_invalid_content_type():
messages = [{"content": [{"type": "invalid_type", "text": "Hello"}]}]
with pytest.raises(Exception, match='Please ensure all messages are valid OpenAI chat completion') as e:
with pytest.raises(Exception, match="Please ensure all messages are valid OpenAI chat completion") as e:
validate_chat_completion_user_messages(messages)
assert "Invalid message" in str(e)
@ -2146,20 +2045,14 @@ from unittest.mock import Mock
[
{
"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"],
},
{
@ -2168,18 +2061,12 @@ from unittest.mock import Mock
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": [],
},
@ -2286,9 +2173,7 @@ def test_get_provider_audio_transcription_config():
from litellm.types.utils import LlmProviders
for provider in LlmProviders:
config = ProviderConfigManager.get_provider_audio_transcription_config(
model="whisper-1", provider=provider
)
config = ProviderConfigManager.get_provider_audio_transcription_config(model="whisper-1", provider=provider)
@pytest.mark.parametrize(
@ -2331,9 +2216,7 @@ def test_get_valid_models_from_provider_cache_invalidation(monkeypatch):
monkeypatch.setenv("OPENAI_API_KEY", "123")
_model_cache.set_cached_model_info(
"openai", litellm_params=None, available_models=["gpt-5-mini"]
)
_model_cache.set_cached_model_info("openai", litellm_params=None, available_models=["gpt-5-mini"])
monkeypatch.delenv("OPENAI_API_KEY")
assert _model_cache.get_cached_model_info("openai") is None
@ -2422,12 +2305,8 @@ def test_delta_tool_calls_sequential_indices():
# Verify tool calls have sequential indices
assert delta.tool_calls is not None, "Tool calls should not be None"
assert len(delta.tool_calls) == 2
assert (
delta.tool_calls[0].index == 0
), f"First tool call should have index 0, got {delta.tool_calls[0].index}"
assert (
delta.tool_calls[1].index == 1
), f"Second tool call should have index 1, got {delta.tool_calls[1].index}"
assert delta.tool_calls[0].index == 0, f"First tool call should have index 0, got {delta.tool_calls[0].index}"
assert delta.tool_calls[1].index == 1, f"Second tool call should have index 1, got {delta.tool_calls[1].index}"
# Verify tool call details are preserved
assert delta.tool_calls[0].function.name == "get_weather_for_dallas"
@ -2440,9 +2319,7 @@ def test_completion_with_no_model():
"""
# test on empty
with pytest.raises(TypeError):
response = litellm.completion(
messages=[{"role": "user", "content": "Hello, how are you?"}]
)
response = litellm.completion(messages=[{"role": "user", "content": "Hello, how are you?"}])
def test_get_base_model_from_metadata():
@ -2455,43 +2332,31 @@ def test_get_base_model_from_metadata():
from litellm.utils import _get_base_model_from_metadata
# Test 1: base_model in metadata (Chat Completions API pattern)
model_call_details_with_metadata = {
"litellm_params": {"metadata": {"model_info": {"base_model": "azure/gpt-5.5"}}}
}
model_call_details_with_metadata = {"litellm_params": {"metadata": {"model_info": {"base_model": "azure/gpt-5.5"}}}}
result = _get_base_model_from_metadata(model_call_details_with_metadata)
assert result == "azure/gpt-5.5", f"Expected 'azure/gpt-5.5', got {result}"
# Test 2: base_model in litellm_metadata (Responses API and generic API calls pattern)
model_call_details_with_litellm_metadata = {
"litellm_params": {
"litellm_metadata": {"model_info": {"base_model": "azure/gpt-5-mini"}}
}
"litellm_params": {"litellm_metadata": {"model_info": {"base_model": "azure/gpt-5-mini"}}}
}
result = _get_base_model_from_metadata(model_call_details_with_litellm_metadata)
assert result == "azure/gpt-5-mini", f"Expected 'azure/gpt-5-mini', got {result}"
# Test 3: base_model in litellm_params (direct base_model)
model_call_details_with_direct_base_model = {
"litellm_params": {"base_model": "azure/gpt-5-mini"}
}
model_call_details_with_direct_base_model = {"litellm_params": {"base_model": "azure/gpt-5-mini"}}
result = _get_base_model_from_metadata(model_call_details_with_direct_base_model)
assert (
result == "azure/gpt-5-mini"
), f"Expected 'azure/gpt-5-mini', got {result}"
assert result == "azure/gpt-5-mini", f"Expected 'azure/gpt-5-mini', got {result}"
# Test 4: metadata takes precedence over litellm_metadata
model_call_details_with_both = {
"litellm_params": {
"metadata": {"model_info": {"base_model": "azure/gpt-4-from-metadata"}},
"litellm_metadata": {
"model_info": {"base_model": "azure/gpt-4-from-litellm-metadata"}
},
"litellm_metadata": {"model_info": {"base_model": "azure/gpt-4-from-litellm-metadata"}},
}
}
result = _get_base_model_from_metadata(model_call_details_with_both)
assert (
result == "azure/gpt-4-from-metadata"
), f"Expected metadata to take precedence, got {result}"
assert result == "azure/gpt-4-from-metadata", f"Expected metadata to take precedence, got {result}"
# Test 5: No base_model present
model_call_details_without_base_model = {"litellm_params": {"metadata": {}}}

View file

@ -16234,16 +16234,28 @@ export interface paths {
* @description [Docs](https://docs.litellm.ai/docs/pass_through/typesafe)
*/
get: operations["typesafe_proxy_route_typesafe__endpoint__get"];
put?: never;
/**
* Typesafe Proxy Route
* @description [Docs](https://docs.litellm.ai/docs/pass_through/typesafe)
*/
put: operations["typesafe_proxy_route_typesafe__endpoint__put"];
/**
* Typesafe Proxy Route
* @description [Docs](https://docs.litellm.ai/docs/pass_through/typesafe)
*/
post: operations["typesafe_proxy_route_typesafe__endpoint__post"];
delete?: never;
/**
* Typesafe Proxy Route
* @description [Docs](https://docs.litellm.ai/docs/pass_through/typesafe)
*/
delete: operations["typesafe_proxy_route_typesafe__endpoint__delete"];
options?: never;
head?: never;
patch?: never;
/**
* Typesafe Proxy Route
* @description [Docs](https://docs.litellm.ai/docs/pass_through/typesafe)
*/
patch: operations["typesafe_proxy_route_typesafe__endpoint__patch"];
trace?: never;
};
"/update/default_team_settings": {
@ -60354,6 +60366,37 @@ export interface operations {
};
};
};
typesafe_proxy_route_typesafe__endpoint__put: {
parameters: {
query?: never;
header?: never;
path: {
endpoint: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Successful Response */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": unknown;
};
};
/** @description Validation Error */
422: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["HTTPValidationError"];
};
};
};
};
typesafe_proxy_route_typesafe__endpoint__post: {
parameters: {
query?: never;
@ -60385,6 +60428,68 @@ export interface operations {
};
};
};
typesafe_proxy_route_typesafe__endpoint__delete: {
parameters: {
query?: never;
header?: never;
path: {
endpoint: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Successful Response */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": unknown;
};
};
/** @description Validation Error */
422: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["HTTPValidationError"];
};
};
};
};
typesafe_proxy_route_typesafe__endpoint__patch: {
parameters: {
query?: never;
header?: never;
path: {
endpoint: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Successful Response */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": unknown;
};
};
/** @description Validation Error */
422: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["HTTPValidationError"];
};
};
};
};
update_default_team_settings_update_default_team_settings_patch: {
parameters: {
query?: never;