litellm/tests/unit/test_utils_get_optional_params.py
devin-ai-integration[bot] fa2c8984ba
test: move the unit half of 126 mixed legacy files into tests/unit (#45090)
* test: move the unit half of 126 mixed legacy files into tests/unit

* test: restore litellm globals that moved tests set

* test: finalize migration test cleanup

* test: restore original bodies of moved legacy tests

The move into tests/unit had rewritten 612 test bodies, and some of the rewrites dropped assertions. Each moved test now carries its original body from the legacy file, with only the imports, helpers, fake provider credentials and monkeypatched env it needs to run under tests/unit

test_timeout_streaming goes back to tests/local_testing because it needs the fake OpenAI endpoint server. The image payload fixture moves with its only user, and two tests that leaked global state (a registered model cost entry and queued logging tasks) are now isolated

* test: drop module imports shadowed by restored local imports

* test: assert on LiteLLM output in no-assertion moved tests and isolate leaks

Twenty no-assertion candidates get one assertion on the value LiteLLM returns, with the original lines unchanged. Four tests go back to their legacy files because they only check types or imports, write into the working directory, or cannot assert without a body change

Two moved tests leaked globals into later tests in the same worker, so monkeypatch fixtures now restore the retry-after header parser and the end user cost tracking flags

* test: drain queued logging tasks before the Phoenix span test

The moved Phoenix test counted spans from logging tasks that earlier tests had queued, so the drain fixture moves to tests/unit/conftest.py and both it and the Datadog batch test use it. test_factory_function goes back to its legacy file because its returned wrapper calls the real Assistants API and cannot be asserted on without a body change

---------

Co-authored-by: yuneng <yuneng@berri.ai>
2026-10-07 14:07:43 -07:00

1863 lines
67 KiB
Python

from unittest.mock import MagicMock, patch
import litellm
import pytest
from litellm.utils import (
get_optional_params,
get_optional_params_embeddings,
get_optional_params_image_gen,
get_requester_metadata,
validate_openai_optional_params,
)
def _check_additional_properties(schema):
if isinstance(schema, dict):
if "additionalProperties" in schema or "strict" in schema:
raise ValueError("additionalProperties and strict should not be in the schema")
for key, value in schema.items():
_check_additional_properties(value)
elif isinstance(schema, list):
for item in schema:
_check_additional_properties(item)
return schema
@pytest.mark.parametrize("stop_sequence, expected_count", [("\n", 0), (["\n"], 0), (["finish_reason"], 1)])
def test_anthropic_optional_params(stop_sequence, expected_count):
"""
Test if whitespace character optional param is dropped by anthropic
"""
litellm.drop_params = True
optional_params = get_optional_params(model="claude-3", custom_llm_provider="anthropic", stop=stop_sequence)
assert len(optional_params) == expected_count
def test_get_requester_metadata_returns_none_for_empty():
metadata = {"requester_metadata": {}}
assert get_requester_metadata(metadata) is None
@patch("litellm.main.openai_chat_completions.completion")
def test_requester_metadata_forwarded_to_openai(mock_completion):
mock_completion.return_value = MagicMock()
metadata = {
"requester_metadata": {
"custom_meta_key": "value",
"hidden_params": "secret",
"int_value": 123,
}
}
original_api_key = litellm.api_key
litellm.api_key = "sk-test"
original_preview_flag = litellm.enable_preview_features
litellm.enable_preview_features = True
try:
litellm.completion(
model="gpt-4o",
messages=[{"role": "user", "content": "hi"}],
metadata=metadata,
)
finally:
litellm.api_key = original_api_key
litellm.enable_preview_features = original_preview_flag
sent_metadata = mock_completion.call_args.kwargs["optional_params"]["metadata"]
assert sent_metadata == {"custom_meta_key": "value"}
def test_get_optional_params_with_allowed_openai_params():
"""
Test if use can dynamically pass in allowed_openai_params to override default behavior
"""
litellm.drop_params = True
tools = [
{
"type": "function",
"function": {
"name": "get_current_time",
"description": "Get the current time in a given location.",
"parameters": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "The city name, e.g. San Francisco",
}
},
"required": ["location"],
},
},
}
]
response_format = {"type": "json"}
reasoning_effort = "low"
optional_params = get_optional_params(
model="cf/llama-3.1-70b-instruct",
custom_llm_provider="cloudflare",
allowed_openai_params=["tools", "reasoning_effort", "response_format"],
tools=tools,
response_format=response_format,
reasoning_effort=reasoning_effort,
)
print(f"optional_params: {optional_params}")
assert optional_params["tools"] == tools
assert optional_params["response_format"] == response_format
assert optional_params["reasoning_effort"] == reasoning_effort
def test_allowed_openai_params_does_not_forward_unset_params():
"""
Regression test for https://github.com/BerriAI/litellm/issues/25697
When a user lists a param in ``allowed_openai_params`` but does not
actually send that param in the request, litellm must not forward it
to the provider SDK as ``None``. The openai SDK rejects unknown
top-level kwargs with
``AsyncCompletions.create() got an unexpected keyword argument 'enable_thinking'``.
Reproduces the reported config where the user listed both
``chat_template_kwargs`` and ``enable_thinking`` in
``allowed_openai_params`` and only sent ``chat_template_kwargs``
(with ``enable_thinking`` nested inside it). Previously the loop
added ``optional_params["enable_thinking"] = None`` which then
crashed the openai client.
"""
from litellm.utils import apply_openai_param_overrides
chat_template_kwargs = {"enable_thinking": False}
optional_params: dict = {}
non_default_params = {"chat_template_kwargs": chat_template_kwargs}
result = apply_openai_param_overrides(
optional_params=optional_params,
non_default_params=non_default_params,
allowed_openai_params=["chat_template_kwargs", "enable_thinking"],
)
assert result["chat_template_kwargs"] == chat_template_kwargs
assert "enable_thinking" not in result
assert "chat_template_kwargs" not in non_default_params
def test_bedrock_optional_params_embeddings():
litellm.drop_params = True
optional_params = get_optional_params_embeddings(
model="", user="John", encoding_format=None, custom_llm_provider="bedrock"
)
assert len(optional_params) == 0
@pytest.mark.parametrize(
"model",
[
"us.anthropic.claude-3-haiku-20240307-v1:0",
"us.meta.llama3-2-11b-instruct-v1:0",
"anthropic.claude-3-haiku-20240307-v1:0",
],
)
def test_bedrock_optional_params_completions(model):
tools = [
{
"type": "function",
"function": {
"name": "structure_output",
"description": "Send structured output back to the user",
"strict": True,
"parameters": {
"type": "object",
"properties": {
"reasoning": {"type": "string"},
"sentiment": {"type": "string"},
},
"required": ["reasoning", "sentiment"],
"additionalProperties": False,
},
"additionalProperties": False,
},
}
]
optional_params = get_optional_params(
model=model,
max_tokens=10,
temperature=0.1,
tools=tools,
custom_llm_provider="bedrock",
)
print(f"optional_params: {optional_params}")
assert len(optional_params) == 4
assert optional_params == {
"maxTokens": 10,
"stream": False,
"temperature": 0.1,
"tools": tools,
}
@pytest.mark.parametrize(
"model",
[
"bedrock/amazon.titan-large",
"bedrock/meta.llama3-2-11b-instruct-v1:0",
"bedrock/ai21.j2-ultra-v1",
"bedrock/cohere.command-nightly",
"bedrock/mistral.mistral-7b",
],
)
def test_bedrock_optional_params_simple(model):
litellm.drop_params = True
get_optional_params(
model=model,
max_tokens=10,
temperature=0.1,
custom_llm_provider="bedrock",
)
assert (
get_optional_params(model=model, max_tokens=10, temperature=0.1, custom_llm_provider="bedrock")["temperature"]
== 0.1
)
@pytest.mark.parametrize(
"model, expected_dimensions, dimensions_kwarg",
[
("bedrock/amazon.titan-embed-text-v1", False, None),
("bedrock/amazon.titan-embed-image-v1", True, "embeddingConfig"),
("bedrock/amazon.titan-embed-text-v2:0", True, "dimensions"),
("bedrock/cohere.embed-multilingual-v3", True, None),
],
)
def test_bedrock_optional_params_embeddings_dimension(model, expected_dimensions, dimensions_kwarg):
litellm.drop_params = True
optional_params = get_optional_params_embeddings(
model=model,
user="John",
encoding_format=None,
dimensions=20,
custom_llm_provider="bedrock",
)
if expected_dimensions:
assert len(optional_params) == 1
else:
assert len(optional_params) == 0
if dimensions_kwarg is not None:
assert dimensions_kwarg in optional_params
def test_google_ai_studio_optional_params_embeddings():
optional_params = get_optional_params_embeddings(
model="",
user="John",
encoding_format=None,
custom_llm_provider="gemini",
drop_params=True,
)
assert len(optional_params) == 0
def test_openai_optional_params_embeddings():
litellm.drop_params = True
optional_params = get_optional_params_embeddings(
model="", user="John", encoding_format=None, custom_llm_provider="openai"
)
assert len(optional_params) == 1
assert optional_params["user"] == "John"
def test_azure_optional_params_embeddings():
litellm.drop_params = True
optional_params = get_optional_params_embeddings(
model="chatgpt-v-3",
user="John",
encoding_format=None,
custom_llm_provider="azure",
)
assert len(optional_params) == 1
assert optional_params["user"] == "John"
def test_databricks_optional_params():
litellm.drop_params = True
optional_params = get_optional_params(
model="",
user="John",
custom_llm_provider="databricks",
max_tokens=10,
temperature=0.2,
stream=True,
)
print(f"optional_params: {optional_params}")
assert len(optional_params) == 3
assert "user" not in optional_params
def test_azure_ai_mistral_optional_params():
litellm.drop_params = True
optional_params = get_optional_params(
model="mistral-large-latest",
user="John",
custom_llm_provider="openai",
max_tokens=10,
temperature=0.2,
)
assert "user" not in optional_params
def test_vertex_ai_llama_3_optional_params():
litellm.vertex_llama3_models = ["meta/llama3-405b-instruct-maas"]
litellm.drop_params = True
optional_params = get_optional_params(
model="meta/llama3-405b-instruct-maas",
user="John",
custom_llm_provider="vertex_ai",
max_tokens=10,
temperature=0.2,
)
assert "user" not in optional_params
def test_vertex_ai_mistral_optional_params():
litellm.vertex_mistral_models = ["mistral-large@2407"]
litellm.drop_params = True
optional_params = get_optional_params(
model="mistral-large@2407",
user="John",
custom_llm_provider="vertex_ai",
max_tokens=10,
temperature=0.2,
)
assert "user" not in optional_params
assert "max_tokens" in optional_params
assert "temperature" in optional_params
def test_azure_gpt_optional_params_gpt_vision():
optional_params = litellm.utils.get_optional_params(
model="",
user="John",
custom_llm_provider="azure",
max_tokens=10,
temperature=0.2,
enhancements={"ocr": {"enabled": True}, "grounding": {"enabled": True}},
dataSources=[
{
"type": "AzureComputerVision",
"parameters": {
"endpoint": "<your_computer_vision_endpoint>",
"key": "<your_computer_vision_key>",
},
}
],
)
print(optional_params)
assert optional_params["max_tokens"] == 10
assert optional_params["temperature"] == 0.2
assert optional_params["extra_body"] == {
"enhancements": {"ocr": {"enabled": True}, "grounding": {"enabled": True}},
"dataSources": [
{
"type": "AzureComputerVision",
"parameters": {
"endpoint": "<your_computer_vision_endpoint>",
"key": "<your_computer_vision_key>",
},
}
],
}
def test_azure_gpt_optional_params_gpt_vision_with_extra_body():
optional_params = litellm.utils.get_optional_params(
model="",
user="John",
custom_llm_provider="azure",
max_tokens=10,
temperature=0.2,
extra_body={
"meta": "hi",
},
enhancements={"ocr": {"enabled": True}, "grounding": {"enabled": True}},
dataSources=[
{
"type": "AzureComputerVision",
"parameters": {
"endpoint": "<your_computer_vision_endpoint>",
"key": "<your_computer_vision_key>",
},
}
],
)
print(optional_params)
assert optional_params["max_tokens"] == 10
assert optional_params["temperature"] == 0.2
assert optional_params["extra_body"] == {
"enhancements": {"ocr": {"enabled": True}, "grounding": {"enabled": True}},
"dataSources": [
{
"type": "AzureComputerVision",
"parameters": {
"endpoint": "<your_computer_vision_endpoint>",
"key": "<your_computer_vision_key>",
},
}
],
"meta": "hi",
}
def test_openai_extra_headers():
optional_params = litellm.utils.get_optional_params(
model="",
user="John",
custom_llm_provider="openai",
max_tokens=10,
temperature=0.2,
extra_headers={"AI-Resource Group": "ishaan-resource"},
)
print(optional_params)
assert optional_params["max_tokens"] == 10
assert optional_params["temperature"] == 0.2
assert optional_params["extra_headers"] == {"AI-Resource Group": "ishaan-resource"}
@pytest.mark.parametrize(
"api_version",
[
"2024-02-01",
"2024-07-01",
"2023-07-01-preview",
"2024-03-01-preview",
],
)
def test_azure_tool_choice(api_version):
"""
Test azure tool choice on older + new version
"""
litellm.drop_params = True
optional_params = litellm.utils.get_optional_params(
model="chatgpt-v-3",
user="John",
custom_llm_provider="azure",
max_tokens=10,
temperature=0.2,
extra_headers={"AI-Resource Group": "ishaan-resource"},
tool_choice="required",
api_version=api_version,
)
print(f"{optional_params}")
if api_version == "2024-07-01":
assert optional_params["tool_choice"] == "required"
else:
assert "tool_choice" not in optional_params, (
"tool choice should not be present. Got - tool_choice={} for api version={}".format(
optional_params["tool_choice"], api_version
)
)
@pytest.mark.parametrize(
"model, provider, should_drop",
[("command-r", "cohere", True), ("gpt-3.5-turbo", "openai", False)],
)
def test_drop_params_parallel_tool_calls(model, provider, should_drop):
"""
https://github.com/BerriAI/litellm/issues/4584
"""
response = litellm.utils.get_optional_params(
model=model,
custom_llm_provider=provider,
response_format={"type": "json"},
parallel_tool_calls=True,
drop_params=True,
)
print(response)
if should_drop:
assert "response_format" not in response
assert "parallel_tool_calls" not in response
else:
assert "response_format" in response
assert "parallel_tool_calls" in response
def test_dynamic_drop_additional_params_stream_options():
"""
Make a call to vertex ai, dropping 'stream_options' specifically
"""
optional_params = litellm.utils.get_optional_params(
model="mistral-large-2411@001",
custom_llm_provider="vertex_ai",
stream_options={"include_usage": True},
additional_drop_params=["stream_options"],
)
assert "stream_options" not in optional_params
def test_get_optional_params_image_gen():
response = litellm.utils.get_optional_params_image_gen(aws_region_name="us-east-1", custom_llm_provider="openai")
print(response)
assert "aws_region_name" not in response
response = litellm.utils.get_optional_params_image_gen(aws_region_name="us-east-1", custom_llm_provider="bedrock")
print(response)
assert "aws_region_name" in response
def test_bedrock_optional_params_embeddings_provider_specific_params():
optional_params = get_optional_params_embeddings(
model="my-custom-model",
custom_llm_provider="huggingface",
wait_for_model=True,
)
assert len(optional_params) == 1
@pytest.mark.parametrize(
"provider",
[
"vertex_ai",
"vertex_ai_beta",
],
)
def test_vertex_safety_settings(provider):
litellm.vertex_ai_safety_settings = [
{
"category": "HARM_CATEGORY_HARASSMENT",
"threshold": "BLOCK_NONE",
},
{
"category": "HARM_CATEGORY_HATE_SPEECH",
"threshold": "BLOCK_NONE",
},
{
"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT",
"threshold": "BLOCK_NONE",
},
{
"category": "HARM_CATEGORY_DANGEROUS_CONTENT",
"threshold": "BLOCK_NONE",
},
]
optional_params = get_optional_params(
model="gemini-1.5-pro", custom_llm_provider=provider
)
assert len(optional_params) == 1
@pytest.mark.parametrize(
"model, provider, expectedAddProp",
[("gemini-1.5-pro", "vertex_ai_beta", False), ("gpt-3.5-turbo", "openai", True)],
)
def test_parse_additional_properties_json_schema(model, provider, expectedAddProp):
optional_params = get_optional_params(
model=model,
custom_llm_provider=provider,
response_format={
"type": "json_schema",
"json_schema": {
"name": "math_reasoning",
"schema": {
"type": "object",
"properties": {
"steps": {
"type": "array",
"items": {
"type": "object",
"properties": {
"explanation": {"type": "string"},
"output": {"type": "string"},
},
"required": ["explanation", "output"],
"additionalProperties": False,
},
},
"final_answer": {"type": "string"},
},
"required": ["steps", "final_answer"],
"additionalProperties": False,
},
"strict": True,
},
},
)
print(optional_params)
if provider == "vertex_ai_beta":
schema = optional_params["response_schema"]
elif provider == "openai":
schema = optional_params["response_format"]["json_schema"]["schema"]
assert ("additionalProperties" in schema) == expectedAddProp
def test_o1_model_params():
optional_params = get_optional_params(
model="o1-2024-12-17",
custom_llm_provider="openai",
seed=10,
user="John",
)
assert optional_params["seed"] == 10
assert optional_params["user"] == "John"
def test_azure_o1_model_params():
optional_params = get_optional_params(
model="o1",
custom_llm_provider="azure",
seed=10,
user="John",
)
assert optional_params["seed"] == 10
assert optional_params["user"] == "John"
@pytest.mark.parametrize(
"temperature, expected_error",
[(0.2, True), (1, False), (0, True)],
)
@pytest.mark.parametrize("provider", ["openai", "azure"])
def test_o1_model_temperature_params(provider, temperature, expected_error):
if expected_error:
with pytest.raises(litellm.UnsupportedParamsError):
get_optional_params(
model="o1",
custom_llm_provider=provider,
temperature=temperature,
)
else:
get_optional_params(
model="o1-2024-12-17",
custom_llm_provider="openai",
temperature=temperature,
)
def test_unmapped_gemini_model_params():
"""
Test if unmapped gemini model optional params are translated correctly
"""
optional_params = get_optional_params(
model="gemini-new-model",
custom_llm_provider="vertex_ai",
stop="stop_word",
)
assert optional_params["stop_sequences"] == ["stop_word"]
@pytest.mark.parametrize(
"provider, model",
[
("hosted_vllm", "my-vllm-model"),
("gemini", "gemini-1.5-pro"),
("vertex_ai", "gemini-1.5-pro"),
],
)
def test_drop_nested_params_add_prop_and_strict(provider, model):
"""
Relevant issue - https://github.com/BerriAI/litellm/issues/5288
Relevant issue - https://github.com/BerriAI/litellm/issues/6136
"""
tools = [
{
"type": "function",
"function": {
"name": "structure_output",
"description": "Send structured output back to the user",
"strict": True,
"parameters": {
"type": "object",
"properties": {
"reasoning": {"type": "string"},
"sentiment": {"type": "string"},
},
"required": ["reasoning", "sentiment"],
"additionalProperties": False,
},
"additionalProperties": False,
},
}
]
tool_choice = {"type": "function", "function": {"name": "structure_output"}}
optional_params = get_optional_params(
model=model,
custom_llm_provider=provider,
temperature=0.2,
tools=tools,
tool_choice=tool_choice,
additional_drop_params=[
["tools", "function", "strict"],
["tools", "function", "additionalProperties"],
],
)
_check_additional_properties(optional_params["tools"])
def test_hosted_vllm_tool_param():
"""
Relevant issue - https://github.com/BerriAI/litellm/issues/6228
"""
optional_params = get_optional_params(
model="my-vllm-model",
custom_llm_provider="hosted_vllm",
temperature=0.2,
tools=None,
tool_choice=None,
)
assert "tools" not in optional_params
assert "tool_choice" not in optional_params
def test_unmapped_vertex_anthropic_model():
optional_params = get_optional_params(
model="claude-3-5-sonnet-v250@20241022",
custom_llm_provider="vertex_ai",
max_retries=10,
)
assert "max_retries" not in optional_params
@pytest.mark.parametrize("provider", ["anthropic", "vertex_ai"])
def test_anthropic_parallel_tool_calls(provider):
optional_params = get_optional_params(
model="claude-3-5-sonnet-v250@20241022",
custom_llm_provider=provider,
parallel_tool_calls=True,
)
print(f"optional_params: {optional_params}")
assert optional_params["tool_choice"]["disable_parallel_tool_use"] is False
def test_anthropic_computer_tool_use():
tools = [
{
"type": "computer_20241022",
"function": {
"name": "computer",
"parameters": {
"display_height_px": 100,
"display_width_px": 100,
"display_number": 1,
},
},
}
]
optional_params = get_optional_params(
model="claude-3-5-sonnet-v250@20241022",
custom_llm_provider="anthropic",
tools=tools,
)
assert optional_params["tools"][0]["type"] == "computer_20241022"
assert optional_params["tools"][0]["display_height_px"] == 100
assert optional_params["tools"][0]["display_width_px"] == 100
assert optional_params["tools"][0]["display_number"] == 1
def test_vertex_schema_field():
tools = [
{
"type": "function",
"function": {
"name": "json",
"description": "Respond with a JSON object.",
"parameters": {
"type": "object",
"properties": {
"thinking": {
"type": "string",
"description": "Your internal thoughts on different problem details given the guidance.",
},
"problems": {
"type": "array",
"items": {
"type": "object",
"properties": {
"icon": {
"type": "string",
"enum": [
"BarChart2",
"Bell",
],
"description": "The name of a Lucide icon to display",
},
"color": {
"type": "string",
"description": "A Tailwind color class for the icon, e.g., 'text-red-500'",
},
"problem": {
"type": "string",
"description": "The title of the problem being addressed, approximately 3-5 words.",
},
"description": {
"type": "string",
"description": "A brief explanation of the problem, approximately 20 words.",
},
"impacts": {
"type": "array",
"items": {"type": "string"},
"description": "A list of potential impacts or consequences of the problem, approximately 3 words each.",
},
"automations": {
"type": "array",
"items": {"type": "string"},
"description": "A list of potential automations to address the problem, approximately 3-5 words each.",
},
},
"required": [
"icon",
"color",
"problem",
"description",
"impacts",
"automations",
],
"additionalProperties": False,
},
"description": "Please generate problem cards that match this guidance.",
},
},
"required": ["thinking", "problems"],
"additionalProperties": False,
"$schema": "http://json-schema.org/draft-07/schema#",
},
},
}
]
optional_params = get_optional_params(
model="gemini-1.5-flash",
custom_llm_provider="vertex_ai",
tools=tools,
)
print(optional_params)
print(optional_params["tools"][0]["function_declarations"][0])
assert "$schema" not in optional_params["tools"][0]["function_declarations"][0]["parameters"]
def test_watsonx_tool_choice():
optional_params = get_optional_params(model="gemini-1.5-pro", custom_llm_provider="watsonx", tool_choice="auto")
print(optional_params)
assert optional_params["tool_choice_option"] == "auto"
def test_watsonx_text_top_k():
optional_params = get_optional_params(model="gemini-1.5-pro", custom_llm_provider="watsonx_text", top_k=10)
print(optional_params)
assert optional_params["top_k"] == 10
def test_together_ai_model_params():
optional_params = get_optional_params(model="together_ai", custom_llm_provider="together_ai", logprobs=1)
print(optional_params)
assert optional_params["logprobs"] == 1
def test_forward_user_param():
from litellm.utils import get_supported_openai_params, get_optional_params
model = "claude-3-5-sonnet-20240620"
optional_params = get_optional_params(
model=model,
user="test_user",
custom_llm_provider="anthropic",
)
assert optional_params["metadata"]["user_id"] == "test_user"
def test_lm_studio_embedding_params():
optional_params = get_optional_params_embeddings(
model="lm_studio/gemma2-9b-it",
custom_llm_provider="lm_studio",
dimensions=1024,
drop_params=True,
)
assert len(optional_params) == 0
def test_ollama_pydantic_obj():
from pydantic import BaseModel
class ResponseFormat(BaseModel):
x: str
y: str
get_optional_params(
model="qwen2:0.5b",
custom_llm_provider="ollama",
response_format=ResponseFormat,
)
assert get_optional_params(model="qwen2:0.5b", custom_llm_provider="ollama", response_format=ResponseFormat)[
"format"
]["required"] == ["x", "y"]
def test_gemini_frequency_penalty_listed_in_vertex_ai_supported_params():
from litellm.utils import get_supported_openai_params
optional_params = get_supported_openai_params(
model="gemini-1.5-flash",
custom_llm_provider="vertex_ai",
request_type="chat_completion",
)
assert optional_params is not None
assert "frequency_penalty" in optional_params
def test_litellm_proxy_claude_3_5_sonnet():
tools = [
{
"type": "function",
"function": {
"name": "get_current_weather",
"description": "Get the current weather in a given location",
"parameters": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "The city and state, e.g. San Francisco, CA",
},
"unit": {"type": "string", "enum": ["celsius", "fahrenheit"]},
},
"required": ["location"],
},
},
}
]
tool_choice = "auto"
optional_params = get_optional_params(
model="claude-3-5-sonnet",
custom_llm_provider="litellm_proxy",
tools=tools,
tool_choice=tool_choice,
)
assert optional_params["tools"] == tools
assert optional_params["tool_choice"] == tool_choice
def test_is_vertex_anthropic_model():
assert (
litellm.VertexAIAnthropicConfig().is_supported_model(
model="claude-3-5-sonnet", custom_llm_provider="litellm_proxy"
)
is False
)
def test_groq_response_format_json_schema():
optional_params = get_optional_params(
model="llama-3.1-70b-versatile",
custom_llm_provider="groq",
response_format={"type": "json_object"},
)
assert optional_params is not None
assert "response_format" in optional_params
assert optional_params["response_format"]["type"] == "json_object"
def test_gemini_frequency_penalty():
optional_params = get_optional_params(model="gemini-1.5-flash", custom_llm_provider="gemini", frequency_penalty=0.5)
assert optional_params["frequency_penalty"] == 0.5
def test_azure_prediction_param():
optional_params = get_optional_params(
model="chatgpt-v2",
custom_llm_provider="azure",
prediction={
"type": "content",
"content": "LiteLLM is a very useful way to connect to a variety of LLMs.",
},
)
assert optional_params["prediction"] == {
"type": "content",
"content": "LiteLLM is a very useful way to connect to a variety of LLMs.",
}
def test_vertex_ai_ft_llama():
optional_params = get_optional_params(
model="1984786713414729728",
custom_llm_provider="vertex_ai",
frequency_penalty=0.5,
max_retries=10,
)
assert optional_params["frequency_penalty"] == 0.5
assert "max_retries" not in optional_params
@pytest.mark.parametrize(
"model, expected_thinking",
[
("claude-3-5-sonnet", False),
("claude-3-7-sonnet", True),
("gpt-3.5-turbo", False),
],
)
def test_anthropic_thinking_param(model, expected_thinking):
optional_params = get_optional_params(
model=model,
custom_llm_provider="anthropic",
thinking={"type": "enabled", "budget_tokens": 1024},
drop_params=True,
)
if expected_thinking:
assert "thinking" in optional_params
else:
assert "thinking" not in optional_params
def test_bedrock_invoke_anthropic_max_tokens():
passed_params = {
"model": "invoke/us.anthropic.claude-haiku-4-5-20251001-v1:0",
"functions": None,
"function_call": None,
"temperature": 0.8,
"top_p": None,
"n": 1,
"stream": False,
"stream_options": None,
"stop": None,
"max_tokens": None,
"max_completion_tokens": 1024,
"modalities": None,
"prediction": None,
"audio": None,
"presence_penalty": None,
"frequency_penalty": None,
"logit_bias": None,
"user": None,
"custom_llm_provider": "bedrock",
"response_format": {"type": "text"},
"seed": None,
"tools": [
{
"type": "function",
"function": {
"name": "generate_plan",
"description": "Generate a plan to execute the task using only the tools outlined in your context.",
"input_schema": {
"type": "object",
"properties": {
"steps": {
"type": "array",
"items": {
"type": "object",
"properties": {
"type": {
"type": "string",
"description": "The type of step to execute",
},
"tool_name": {
"type": "string",
"description": "The name of the tool to use for this step",
},
"tool_input": {
"type": "object",
"description": "The input to pass to the tool. Make sure this complies with the schema for the tool.",
},
"tool_output": {
"type": "object",
"description": "(Optional) The output from the tool if needed for future steps. Make sure this complies with the schema for the tool.",
},
},
"required": ["type"],
},
}
},
},
},
},
{
"type": "function",
"function": {
"name": "generate_wire_tool",
"description": "Create a wire transfer with complete wire instructions",
"input_schema": {
"type": "object",
"properties": {
"company_id": {
"type": "integer",
"description": "The ID of the company receiving the investment",
},
"investment_id": {
"type": "integer",
"description": "The ID of the investment memo",
},
"dollar_amount": {
"type": "number",
"description": "The amount to wire in USD",
},
"wiring_instructions": {
"type": "object",
"description": "Complete bank account and routing information for the wire",
"properties": {
"account_name": {
"type": "string",
"description": "Name on the bank account",
},
"address_1": {
"type": "string",
"description": "Primary address line",
},
"address_2": {
"type": "string",
"description": "Secondary address line (optional)",
},
"city": {"type": "string"},
"state": {"type": "string"},
"zip": {"type": "string"},
"country": {"type": "string", "default": "US"},
"bank_name": {"type": "string"},
"account_number": {"type": "string"},
"routing_number": {"type": "string"},
"account_type": {
"type": "string",
"enum": ["checking", "savings"],
"default": "checking",
},
"swift_code": {
"type": "string",
"description": "Required for international wires",
},
"iban": {
"type": "string",
"description": "Required for some international wires",
},
"bank_city": {"type": "string"},
"bank_state": {"type": "string"},
"bank_country": {"type": "string", "default": "US"},
"bank_to_bank_instructions": {
"type": "string",
"description": "Additional instructions for the bank (optional)",
},
"intermediary_bank_name": {
"type": "string",
"description": "Name of intermediary bank if required (optional)",
},
},
"required": [
"account_name",
"address_1",
"country",
"bank_name",
"account_number",
"routing_number",
"account_type",
"bank_country",
],
},
},
"required": [
"company_id",
"investment_id",
"dollar_amount",
"wiring_instructions",
],
},
},
},
{
"type": "function",
"function": {
"name": "search_companies",
"description": "Search for companies by name or other criteria to get their IDs",
"input_schema": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "Name or part of name to search for",
},
"batch": {
"type": "string",
"description": 'Optional batch filter (e.g., "W21", "S22")',
},
"status": {
"type": "string",
"enum": [
"live",
"dead",
"adrift",
"exited",
"went_public",
"all",
],
"description": "Filter by company status",
"default": "live",
},
"limit": {
"type": "integer",
"description": "Maximum number of results to return",
"default": 10,
},
},
"required": ["query"],
},
"output_schema": {
"type": "object",
"properties": {
"status": {
"type": "string",
"description": "Success or error status",
},
"results": {
"type": "array",
"description": "List of companies matching the search criteria",
"items": {
"type": "object",
"properties": {
"id": {
"type": "integer",
"description": "Company ID to use in other API calls",
},
"name": {"type": "string"},
"batch": {"type": "string"},
"status": {"type": "string"},
"valuation": {"type": "string"},
"url": {"type": "string"},
"description": {"type": "string"},
"founders": {"type": "string"},
},
},
},
"results_count": {
"type": "integer",
"description": "Number of companies returned",
},
"total_matches": {
"type": "integer",
"description": "Total number of matches found",
},
},
},
},
},
],
"tool_choice": None,
"max_retries": 0,
"logprobs": None,
"top_logprobs": None,
"extra_headers": None,
"api_version": None,
"parallel_tool_calls": None,
"drop_params": True,
"reasoning_effort": None,
"additional_drop_params": None,
"messages": [
{
"role": "system",
"content": "You are an AI assistant that helps prepare a wire for a pro rata investment.",
},
{"role": "user", "content": [{"type": "text", "text": "hi"}]},
],
"thinking": None,
"kwargs": {},
}
optional_params = get_optional_params(**passed_params)
print(f"optional_params: {optional_params}")
assert "max_tokens_to_sample" not in optional_params
assert optional_params["max_tokens"] == 1024
def test_bedrock_invoke_claude_4_anthropic_max_tokens():
passed_params = {
"model": "invoke/us.anthropic.claude-sonnet-4-5-20250929-v1:0",
"functions": None,
"function_call": None,
"temperature": 0.8,
"top_p": None,
"n": 1,
"stream": False,
"stream_options": None,
"stop": None,
"max_tokens": None,
"max_completion_tokens": 1024,
"modalities": None,
"prediction": None,
"audio": None,
"presence_penalty": None,
"frequency_penalty": None,
"logit_bias": None,
"user": None,
"custom_llm_provider": "bedrock",
"response_format": {"type": "text"},
"seed": None,
"tools": [
{
"type": "function",
"function": {
"name": "generate_plan",
"description": "Generate a plan to execute the task using only the tools outlined in your context.",
"input_schema": {
"type": "object",
"properties": {
"steps": {
"type": "array",
"items": {
"type": "object",
"properties": {
"type": {
"type": "string",
"description": "The type of step to execute",
},
"tool_name": {
"type": "string",
"description": "The name of the tool to use for this step",
},
"tool_input": {
"type": "object",
"description": "The input to pass to the tool. Make sure this complies with the schema for the tool.",
},
"tool_output": {
"type": "object",
"description": "(Optional) The output from the tool if needed for future steps. Make sure this complies with the schema for the tool.",
},
},
"required": ["type"],
},
}
},
},
},
},
{
"type": "function",
"function": {
"name": "generate_wire_tool",
"description": "Create a wire transfer with complete wire instructions",
"input_schema": {
"type": "object",
"properties": {
"company_id": {
"type": "integer",
"description": "The ID of the company receiving the investment",
},
"investment_id": {
"type": "integer",
"description": "The ID of the investment memo",
},
"dollar_amount": {
"type": "number",
"description": "The amount to wire in USD",
},
"wiring_instructions": {
"type": "object",
"description": "Complete bank account and routing information for the wire",
"properties": {
"account_name": {
"type": "string",
"description": "Name on the bank account",
},
"address_1": {
"type": "string",
"description": "Primary address line",
},
"address_2": {
"type": "string",
"description": "Secondary address line (optional)",
},
"city": {"type": "string"},
"state": {"type": "string"},
"zip": {"type": "string"},
"country": {"type": "string", "default": "US"},
"bank_name": {"type": "string"},
"account_number": {"type": "string"},
"routing_number": {"type": "string"},
"account_type": {
"type": "string",
"enum": ["checking", "savings"],
"default": "checking",
},
"swift_code": {
"type": "string",
"description": "Required for international wires",
},
"iban": {
"type": "string",
"description": "Required for some international wires",
},
"bank_city": {"type": "string"},
"bank_state": {"type": "string"},
"bank_country": {"type": "string", "default": "US"},
"bank_to_bank_instructions": {
"type": "string",
"description": "Additional instructions for the bank (optional)",
},
"intermediary_bank_name": {
"type": "string",
"description": "Name of intermediary bank if required (optional)",
},
},
"required": [
"account_name",
"address_1",
"country",
"bank_name",
"account_number",
"routing_number",
"account_type",
"bank_country",
],
},
},
"required": [
"company_id",
"investment_id",
"dollar_amount",
"wiring_instructions",
],
},
},
},
{
"type": "function",
"function": {
"name": "search_companies",
"description": "Search for companies by name or other criteria to get their IDs",
"input_schema": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "Name or part of name to search for",
},
"batch": {
"type": "string",
"description": 'Optional batch filter (e.g., "W21", "S22")',
},
"status": {
"type": "string",
"enum": [
"live",
"dead",
"adrift",
"exited",
"went_public",
"all",
],
"description": "Filter by company status",
"default": "live",
},
"limit": {
"type": "integer",
"description": "Maximum number of results to return",
"default": 10,
},
},
"required": ["query"],
},
"output_schema": {
"type": "object",
"properties": {
"status": {
"type": "string",
"description": "Success or error status",
},
"results": {
"type": "array",
"description": "List of companies matching the search criteria",
"items": {
"type": "object",
"properties": {
"id": {
"type": "integer",
"description": "Company ID to use in other API calls",
},
"name": {"type": "string"},
"batch": {"type": "string"},
"status": {"type": "string"},
"valuation": {"type": "string"},
"url": {"type": "string"},
"description": {"type": "string"},
"founders": {"type": "string"},
},
},
},
"results_count": {
"type": "integer",
"description": "Number of companies returned",
},
"total_matches": {
"type": "integer",
"description": "Total number of matches found",
},
},
},
},
},
],
"tool_choice": None,
"max_retries": 0,
"logprobs": None,
"top_logprobs": None,
"extra_headers": None,
"api_version": None,
"parallel_tool_calls": None,
"drop_params": True,
"reasoning_effort": None,
"additional_drop_params": None,
"messages": [
{
"role": "system",
"content": "You are an AI assistant that helps prepare a wire for a pro rata investment.",
},
{"role": "user", "content": [{"type": "text", "text": "hi"}]},
],
"thinking": None,
"kwargs": {},
}
optional_params = get_optional_params(**passed_params)
print(f"optional_params: {optional_params}")
assert "max_tokens_to_sample" not in optional_params
assert optional_params["max_tokens"] == 1024
def test_azure_modalities_param():
optional_params = get_optional_params(
model="chatgpt-v2",
custom_llm_provider="azure",
modalities=["text", "audio"],
audio={"type": "audio_input", "input": "test.wav"},
)
assert optional_params["modalities"] == ["text", "audio"]
assert optional_params["audio"] == {"type": "audio_input", "input": "test.wav"}
def test_litellm_proxy_thinking_param():
optional_params = get_optional_params(
model="gpt-4o",
custom_llm_provider="litellm_proxy",
thinking={"type": "enabled", "budget_tokens": 1024},
)
assert optional_params["extra_body"]["thinking"] == {
"type": "enabled",
"budget_tokens": 1024,
}
def test_gemini_modalities_param():
optional_params = get_optional_params(
model="gemini-1.5-pro",
custom_llm_provider="gemini",
modalities=["text", "image"],
)
assert optional_params["responseModalities"] == ["TEXT", "IMAGE"]
def test_azure_response_format_param():
optional_params = litellm.get_optional_params(
model="azure/o_series/test-o3-mini",
custom_llm_provider="azure/o_series",
tools=[
{
"type": "function",
"function": {
"name": "get_current_time",
"description": "Get the current time in a given location.",
"parameters": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "The city name, e.g. San Francisco",
}
},
"required": ["location"],
},
},
}
],
)
assert optional_params["tools"][0]["function"]["name"] == "get_current_time"
@pytest.mark.parametrize(
"model, provider",
[
("claude-3-7-sonnet-20240620-v1:0", "anthropic"),
("anthropic.claude-sonnet-4-5-20250929-v1:0", "bedrock"),
("invoke/anthropic.claude-3-7-sonnet-20240620-v1:0", "bedrock"),
("claude-3-7-sonnet@20250219", "vertex_ai"),
],
)
def test_anthropic_unified_reasoning_content(model, provider):
from litellm.constants import DEFAULT_REASONING_EFFORT_HIGH_THINKING_BUDGET
optional_params = get_optional_params(
model=model,
custom_llm_provider=provider,
reasoning_effort="high",
)
assert optional_params["thinking"] == {
"type": "enabled",
"budget_tokens": DEFAULT_REASONING_EFFORT_HIGH_THINKING_BUDGET,
}
def test_azure_response_format(monkeypatch):
monkeypatch.setenv("AZURE_API_VERSION", "2025-02-01")
optional_params = get_optional_params(
model="azure/gpt-4o-mini",
custom_llm_provider="azure",
response_format={"type": "json_object"},
)
assert optional_params["response_format"] == {"type": "json_object"}
def test_cohere_embed_dimensions_param():
optional_params = get_optional_params_embeddings(
model="embed-multilingual-v3.0",
custom_llm_provider="cohere",
encoding_format="float",
)
assert optional_params["embedding_types"] == ["float"]
def test_optional_params_with_additional_drop_params():
optional_params = get_optional_params(
model="gpt-4o",
custom_llm_provider="openai",
additional_drop_params=["red"],
drop_params=True,
red="blue",
)
print(f"optional_params: {optional_params}")
assert "red" not in optional_params
assert "red" not in optional_params["extra_body"]
def test_azure_ai_cohere_embed_input_type_param():
optional_params = get_optional_params_embeddings(
model="embed-v-4-0",
custom_llm_provider="azure_ai",
input_type="text",
dimensions=1536,
)
assert optional_params["dimensions"] == 1536
assert optional_params["extra_body"]["input_type"] == "text"
def test_optional_params_image_gen_with_aspect_ratio():
optional_params = get_optional_params_image_gen(
model="imagen-4.0-ultra-generate-001",
custom_llm_provider="vertex_ai",
aspect_ratio="16:9",
)
assert optional_params["aspect_ratio"] == "16:9"
def test_validate_openai_optional_params_stop_truncation():
"""
Test that validate_openai_optional_params truncates stop sequences to 4 elements
when more than 4 are provided, as OpenAI only supports up to 4 stop sequences.
"""
stop_sequences = ["stop1", "stop2", "stop3", "stop4", "stop5", "stop6"]
result = validate_openai_optional_params(stop=stop_sequences)
assert result == ["stop1", "stop2", "stop3", "stop4"]
assert len(result) == 4
stop_sequences_4 = ["stop1", "stop2", "stop3", "stop4"]
result = validate_openai_optional_params(stop=stop_sequences_4)
assert result == ["stop1", "stop2", "stop3", "stop4"]
assert len(result) == 4
stop_sequences_2 = ["stop1", "stop2"]
result = validate_openai_optional_params(stop=stop_sequences_2)
assert result == ["stop1", "stop2"]
assert len(result) == 2
stop_string = "stop1"
result = validate_openai_optional_params(stop=stop_string)
assert result == "stop1"
result = validate_openai_optional_params(stop=None)
assert result is None
result = validate_openai_optional_params(stop=[])
assert result == []
def test_validate_openai_optional_params_disable_stop_sequence_limit():
"""
Test that validate_openai_optional_params respects the disable_stop_sequence_limit flag.
When litellm.disable_stop_sequence_limit is True, stop sequences should not be truncated.
"""
original_value = litellm.disable_stop_sequence_limit
try:
litellm.disable_stop_sequence_limit = True
stop_sequences = ["stop1", "stop2", "stop3", "stop4", "stop5", "stop6"]
result = validate_openai_optional_params(stop=stop_sequences)
assert result == ["stop1", "stop2", "stop3", "stop4", "stop5", "stop6"]
assert len(result) == 6
litellm.disable_stop_sequence_limit = False
stop_sequences = ["stop1", "stop2", "stop3", "stop4", "stop5", "stop6"]
result = validate_openai_optional_params(stop=stop_sequences)
assert result == ["stop1", "stop2", "stop3", "stop4"]
assert len(result) == 4
finally:
litellm.disable_stop_sequence_limit = original_value
def test_validate_openai_optional_params_integration():
"""
Test that validate_openai_optional_params is properly integrated in the completion flow.
"""
try:
with patch("litellm.llms.openai.openai.OpenAI") as mock_client:
mock_response = MagicMock()
mock_response.choices = [MagicMock()]
mock_response.choices[0].message.content = "Test response"
mock_response.model = "gpt-3.5-turbo"
mock_response.id = "test-id"
mock_response.created = 1234567890
mock_response.usage = MagicMock()
mock_response.usage.prompt_tokens = 10
mock_response.usage.completion_tokens = 5
mock_response.usage.total_tokens = 15
mock_client.return_value.chat.completions.create.return_value = mock_response
response = litellm.completion(
model="gpt-3.5-turbo",
messages=[{"role": "user", "content": "Hello"}],
stop=["stop1", "stop2", "stop3", "stop4", "stop5", "stop6"],
mock_response="Test response",
)
assert response is not None
except Exception as e:
pytest.fail(f"validate_openai_optional_params integration failed: {e}")
def test_drop_store_param_for_anthropic():
"""
Test that the OpenAI-specific `store` parameter is correctly dropped
when calling Anthropic with drop_params=True.
`store` is an OpenAI Chat Completion parameter (for storing completions
for distillation/evals) that Anthropic does not support. Without proper
handling, it leaks through to the Anthropic API and causes a
"store: Extra inputs are not permitted" error.
Ref: https://github.com/BerriAI/litellm/issues/19700
"""
optional_params = get_optional_params(
model="claude-sonnet-4-5-20250929",
custom_llm_provider="anthropic",
drop_params=True,
store=True,
)
assert "store" not in optional_params
def test_additional_drop_params_store_for_anthropic():
"""
Test that `additional_drop_params=["store"]` correctly strips the `store`
parameter for non-OpenAI providers like Anthropic.
Ref: https://github.com/BerriAI/litellm/issues/19700
"""
optional_params = get_optional_params(
model="claude-sonnet-4-5-20250929",
custom_llm_provider="anthropic",
additional_drop_params=["store"],
store=True,
)
assert "store" not in optional_params
def test_store_in_openai_chat_completion_params():
"""
Test that `store` is recognized as a standard OpenAI Chat Completion
parameter. This ensures it is correctly handled by helper functions
like `get_standard_openai_params()` and provider configs that rely on
`OPENAI_CHAT_COMPLETION_PARAMS`.
Without `store` in this list, functions that filter by known OpenAI
params will silently drop it for OpenAI calls or incorrectly treat
it as a provider-specific param for non-OpenAI providers.
Ref: https://github.com/BerriAI/litellm/issues/19700
"""
from litellm.constants import OPENAI_CHAT_COMPLETION_PARAMS
assert "store" in OPENAI_CHAT_COMPLETION_PARAMS
from litellm.utils import get_standard_openai_params
result = get_standard_openai_params({"store": True, "temperature": 0.7})
assert "store" in result
assert result["store"] is True
def test_store_param_passed_through_openai_azure():
"""
Test that the `store` parameter is correctly passed through to OpenAI
and Azure OpenAI providers when using get_optional_params().
This verifies the fix for the regression where `store` was being filtered
out by get_non_default_completion_params() due to architectural issues
in parameter processing pipeline.
Ref: https://github.com/BerriAI/litellm/issues/19700
"""
optional_params_openai = get_optional_params(
model="gpt-4o",
custom_llm_provider="openai",
store=True,
)
assert "store" in optional_params_openai
assert optional_params_openai["store"] is True
optional_params_azure = get_optional_params(
model="gpt-4.1-2025-04-14",
custom_llm_provider="azure",
store=True,
)
assert "store" in optional_params_azure
assert optional_params_azure["store"] is True
optional_params_false = get_optional_params(
model="gpt-4o",
custom_llm_provider="openai",
store=False,
)
assert "store" in optional_params_false
assert optional_params_false["store"] is False