mirror of
https://github.com/BerriAI/litellm.git
synced 2026-08-28 05:25:59 +00:00
* test: drop the cwd-relative sys.path.insert calls from the test suite
TQ003 stands at 1,077 across 1,058 files, and 1,015 of them are the same shape:
sys.path.insert(0, os.path.abspath("../..")) and its deeper siblings. The
argument resolves against the working directory rather than the file, so from
the repo root, where every job runs pytest, it inserts the directory two levels
above the checkout. It has never pointed at litellm. The package is installed
into the environment anyway, which is what actually makes the import work, and
what the rule's message has said all along.
Removing them leaves 1,634 imports of sys and os with no remaining reference,
and those go too, except where another test module imports the name back out of
the file. The rest of TQ003 is 62 call sites that resolve against __file__ or a
variable, which are a different question and are left alone.
Collection is identical either way: 45,871 tests and the same 51 pre-existing
collection errors before and after, and ruff reports no new undefined name.
* test: drop the duplicate imports the sys.path sweep exposed to F811
* test(pre-call-utils): restore the os import the new bedrock tests need
2127 lines
76 KiB
Python
2127 lines
76 KiB
Python
#### What this tests ####
|
|
# This tests if get_optional_params works as expected
|
|
import asyncio
|
|
import inspect
|
|
import time
|
|
import traceback
|
|
|
|
import pytest
|
|
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import litellm
|
|
from litellm.litellm_core_utils.prompt_templates.factory import map_system_message_pt
|
|
from litellm.types.completion import (
|
|
ChatCompletionMessageParam,
|
|
ChatCompletionSystemMessageParam,
|
|
ChatCompletionUserMessageParam,
|
|
)
|
|
from litellm.utils import (
|
|
get_optional_params,
|
|
get_optional_params_embeddings,
|
|
get_optional_params_image_gen,
|
|
get_requester_metadata,
|
|
validate_openai_optional_params,
|
|
)
|
|
|
|
## get_optional_params_embeddings
|
|
### Models: OpenAI, Azure, Bedrock
|
|
### Scenarios: w/ optional params + litellm.drop_params = True
|
|
|
|
|
|
def test_supports_system_message():
|
|
"""
|
|
Check if litellm.completion(...,supports_system_message=False)
|
|
"""
|
|
messages = [
|
|
ChatCompletionSystemMessageParam(role="system", content="Listen here!"),
|
|
ChatCompletionUserMessageParam(role="user", content="Hello there!"),
|
|
]
|
|
|
|
new_messages = map_system_message_pt(messages=messages)
|
|
|
|
assert len(new_messages) == 1
|
|
assert new_messages[0]["role"] == "user"
|
|
|
|
## confirm you can make a openai call with this param
|
|
|
|
response = litellm.completion(
|
|
model="gpt-3.5-turbo", messages=new_messages, supports_system_message=False
|
|
)
|
|
|
|
assert isinstance(response, litellm.ModelResponse)
|
|
|
|
|
|
@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
|
|
# enable_thinking was NOT sent as a top-level param — it must not be
|
|
# forwarded to the provider SDK (openai AsyncCompletions.create would
|
|
# reject an unknown kwarg, even if its value is None).
|
|
assert "enable_thinking" not in result
|
|
# And the only entry actually moved out of non_default_params is
|
|
# the one the caller sent.
|
|
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",
|
|
)
|
|
|
|
|
|
@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():
|
|
# for OpenAI, Azure all extra params need to get passed as extra_body to OpenAI python. We assert we actually set extra_body here
|
|
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>",
|
|
},
|
|
}
|
|
],
|
|
}
|
|
|
|
|
|
# test_azure_gpt_optional_params_gpt_vision()
|
|
|
|
|
|
def test_azure_gpt_optional_params_gpt_vision_with_extra_body():
|
|
# if user passes extra_body, we should not over write it, we should pass it along to OpenAI python
|
|
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",
|
|
}
|
|
|
|
|
|
# test_azure_gpt_optional_params_gpt_vision_with_extra_body()
|
|
|
|
|
|
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", # potential future version with tool_choice="required" supported
|
|
"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("drop_params", [True, False, None])
|
|
def test_dynamic_drop_params(drop_params):
|
|
"""
|
|
Make a call to cohere w/ drop params = True vs. false.
|
|
"""
|
|
if drop_params is True:
|
|
optional_params = litellm.utils.get_optional_params(
|
|
model="command-r",
|
|
custom_llm_provider="cohere",
|
|
response_format={"type": "json"},
|
|
drop_params=drop_params,
|
|
)
|
|
else:
|
|
try:
|
|
optional_params = litellm.utils.get_optional_params(
|
|
model="command-r",
|
|
custom_llm_provider="cohere",
|
|
response_format={"type": "json"},
|
|
drop_params=drop_params,
|
|
)
|
|
pytest.fail("Expected to fail")
|
|
except Exception as e:
|
|
pass
|
|
|
|
|
|
def test_dynamic_drop_params_e2e():
|
|
with patch(
|
|
"litellm.llms.custom_httpx.http_handler.HTTPHandler.post", new=MagicMock()
|
|
) as mock_response:
|
|
try:
|
|
response = litellm.completion(
|
|
model="command-r",
|
|
messages=[{"role": "user", "content": "Hey, how's it going?"}],
|
|
response_format={"key": "value"},
|
|
drop_params=True,
|
|
)
|
|
except Exception as e:
|
|
pass
|
|
|
|
mock_response.assert_called_once()
|
|
print(mock_response.call_args.kwargs["data"])
|
|
assert "response_format" not in mock_response.call_args.kwargs["data"]
|
|
|
|
|
|
def test_dynamic_pass_additional_params():
|
|
with patch(
|
|
"litellm.llms.custom_httpx.http_handler.HTTPHandler.post", new=MagicMock()
|
|
) as mock_response:
|
|
try:
|
|
response = litellm.completion(
|
|
model="command-r",
|
|
messages=[{"role": "user", "content": "Hey, how's it going?"}],
|
|
custom_param="test",
|
|
api_key="my-custom-key",
|
|
)
|
|
except Exception as e:
|
|
print(f"Error occurred: {e}")
|
|
pass
|
|
|
|
mock_response.assert_called_once()
|
|
print(mock_response.call_args.kwargs["data"])
|
|
assert "custom_param" in mock_response.call_args.kwargs["data"]
|
|
assert "api_key" not in mock_response.call_args.kwargs["data"]
|
|
|
|
|
|
@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_params_parallel_tool_calls():
|
|
"""
|
|
https://github.com/BerriAI/litellm/issues/4584
|
|
"""
|
|
with patch(
|
|
"litellm.llms.custom_httpx.http_handler.HTTPHandler.post", new=MagicMock()
|
|
) as mock_response:
|
|
try:
|
|
response = litellm.completion(
|
|
model="command-r",
|
|
messages=[{"role": "user", "content": "Hey, how's it going?"}],
|
|
parallel_tool_calls=True,
|
|
drop_params=True,
|
|
)
|
|
except Exception as e:
|
|
pass
|
|
|
|
mock_response.assert_called_once()
|
|
print(mock_response.call_args.kwargs["data"])
|
|
assert "parallel_tool_calls" not in mock_response.call_args.kwargs["data"]
|
|
|
|
|
|
@pytest.mark.parametrize("drop_params", [True, False, None])
|
|
def test_dynamic_drop_additional_params(drop_params):
|
|
"""
|
|
Make a call to cohere, dropping 'response_format' specifically
|
|
"""
|
|
if drop_params is True:
|
|
optional_params = litellm.utils.get_optional_params(
|
|
model="command-r",
|
|
custom_llm_provider="cohere",
|
|
response_format={"type": "json"},
|
|
additional_drop_params=["response_format"],
|
|
)
|
|
else:
|
|
try:
|
|
optional_params = litellm.utils.get_optional_params(
|
|
model="command-r",
|
|
custom_llm_provider="cohere",
|
|
response_format={"type": "json"},
|
|
)
|
|
pytest.fail("Expected to fail")
|
|
except Exception as e:
|
|
pass
|
|
|
|
|
|
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_dynamic_drop_additional_params_e2e():
|
|
with patch(
|
|
"litellm.llms.custom_httpx.http_handler.HTTPHandler.post", new=MagicMock()
|
|
) as mock_response:
|
|
try:
|
|
response = litellm.completion(
|
|
model="command-r",
|
|
messages=[{"role": "user", "content": "Hey, how's it going?"}],
|
|
response_format={"key": "value"},
|
|
additional_drop_params=["response_format"],
|
|
)
|
|
except Exception as e:
|
|
print(f"Error occurred: {e}")
|
|
pass
|
|
|
|
mock_response.assert_called_once()
|
|
print(mock_response.call_args.kwargs["data"])
|
|
assert "response_format" not in mock_response.call_args.kwargs["data"]
|
|
assert "additional_drop_params" not in mock_response.call_args.kwargs["data"]
|
|
|
|
|
|
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
|
|
|
|
|
|
def test_get_optional_params_num_retries():
|
|
"""
|
|
Relevant issue - https://github.com/BerriAI/litellm/issues/5124
|
|
"""
|
|
with patch(
|
|
"litellm.main.get_optional_params",
|
|
new=MagicMock(return_value={"max_retries": 0}),
|
|
) as mock_client:
|
|
_ = litellm.completion(
|
|
model="gpt-3.5-turbo",
|
|
messages=[{"role": "user", "content": "Hello world"}],
|
|
num_retries=10,
|
|
)
|
|
|
|
mock_client.assert_called()
|
|
|
|
print(f"mock_client.call_args: {mock_client.call_args}")
|
|
assert mock_client.call_args.kwargs["max_retries"] == 10
|
|
|
|
|
|
@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"]
|
|
|
|
|
|
def _check_additional_properties(schema):
|
|
if isinstance(schema, dict):
|
|
# Remove the 'additionalProperties' key if it exists and is set to False
|
|
if "additionalProperties" in schema or "strict" in schema:
|
|
raise ValueError(
|
|
"additionalProperties and strict should not be in the schema"
|
|
)
|
|
|
|
# Recursively process all dictionary values
|
|
for key, value in schema.items():
|
|
_check_additional_properties(value)
|
|
|
|
elif isinstance(schema, list):
|
|
# Recursively process all items in the list
|
|
for item in schema:
|
|
_check_additional_properties(item)
|
|
|
|
return schema
|
|
|
|
|
|
@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,
|
|
)
|
|
|
|
|
|
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"],
|
|
},
|
|
},
|
|
}
|
|
],
|
|
)
|
|
|
|
|
|
@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_optional_params_responses_api_allowed_openai_params():
|
|
from litellm import responses
|
|
from unittest.mock import patch, MagicMock
|
|
from litellm.llms.custom_httpx.http_handler import HTTPHandler
|
|
|
|
client = HTTPHandler()
|
|
|
|
with patch.object(client, "post") as mock_post:
|
|
try:
|
|
response = litellm.responses(
|
|
model="openai/o1-pro",
|
|
input="Tell me a three sentence bedtime story about a unicorn.",
|
|
max_output_tokens=100,
|
|
top_logprobs=10,
|
|
allowed_openai_params=["top_logprobs"],
|
|
client=client,
|
|
)
|
|
except Exception as e:
|
|
import traceback
|
|
|
|
traceback.print_exc()
|
|
print("error: ", e)
|
|
|
|
mock_post.assert_called_once()
|
|
request_body = mock_post.call_args.kwargs
|
|
print("request_body: ", request_body)
|
|
assert "top_logprobs" in request_body["json"]
|
|
|
|
|
|
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.
|
|
"""
|
|
# Test with more than 4 stop sequences - should truncate to 4
|
|
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
|
|
|
|
# Test with exactly 4 stop sequences - should not truncate
|
|
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
|
|
|
|
# Test with less than 4 stop sequences - should not truncate
|
|
stop_sequences_2 = ["stop1", "stop2"]
|
|
result = validate_openai_optional_params(stop=stop_sequences_2)
|
|
assert result == ["stop1", "stop2"]
|
|
assert len(result) == 2
|
|
|
|
# Test with single stop sequence as string - should return as is
|
|
stop_string = "stop1"
|
|
result = validate_openai_optional_params(stop=stop_string)
|
|
assert result == "stop1"
|
|
|
|
# Test with None - should return None
|
|
result = validate_openai_optional_params(stop=None)
|
|
assert result is None
|
|
|
|
# Test with empty list - should return empty list
|
|
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.
|
|
"""
|
|
# Save original value
|
|
original_value = litellm.disable_stop_sequence_limit
|
|
|
|
try:
|
|
# Test with disable_stop_sequence_limit = True - should NOT truncate
|
|
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
|
|
|
|
# Test with disable_stop_sequence_limit = False - should truncate to 4
|
|
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:
|
|
# Restore original value
|
|
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.
|
|
"""
|
|
# Test that completion with more than 4 stop sequences works without error
|
|
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
|
|
)
|
|
|
|
# Call completion with more than 4 stop sequences
|
|
response = litellm.completion(
|
|
model="gpt-3.5-turbo",
|
|
messages=[{"role": "user", "content": "Hello"}],
|
|
stop=["stop1", "stop2", "stop3", "stop4", "stop5", "stop6"],
|
|
mock_response="Test response", # This will use mock
|
|
)
|
|
|
|
# Verify the call was made (stop sequences should be truncated internally)
|
|
assert response is not None
|
|
except Exception as e:
|
|
# Should not raise an exception
|
|
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
|
|
|
|
# Verify get_standard_openai_params recognizes store
|
|
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
|
|
"""
|
|
# Test OpenAI provider
|
|
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
|
|
|
|
# Test Azure OpenAI provider
|
|
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
|
|
|
|
# Test with store=False
|
|
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
|