Merge pull request #9694 from BerriAI/litellm_fix_azure_o_series

[Bug fix] Azure o-series tool calling
This commit is contained in:
Ishaan Jaff
2025-04-02 20:59:00 -07:00
committed by GitHub
10 changed files with 285 additions and 7 deletions
+73 -1
View File
@@ -107,4 +107,76 @@ response = litellm.completion(
</TabItem>
</Tabs>
**additional_drop_params**: List or null - Is a list of openai params you want to drop when making a call to the model.
**additional_drop_params**: List or null - Is a list of openai params you want to drop when making a call to the model.
## Specify allowed openai params in a request
Tell litellm to allow specific openai params in a request. Use this if you get a `litellm.UnsupportedParamsError` and want to allow a param. LiteLLM will pass the param as is to the model.
<Tabs>
<TabItem value="sdk" label="LiteLLM Python SDK">
In this example we pass `allowed_openai_params=["tools"]` to allow the `tools` param.
```python showLineNumbers title="Pass allowed_openai_params to LiteLLM Python SDK"
await litellm.acompletion(
model="azure/o_series/<my-deployment-name>",
api_key="xxxxx",
api_base=api_base,
messages=[{"role": "user", "content": "Hello! return a json object"}],
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"]}}}]
allowed_openai_params=["tools"],
)
```
</TabItem>
<TabItem value="proxy" label="LiteLLM Proxy">
When using litellm proxy you can pass `allowed_openai_params` in two ways:
1. Dynamically pass `allowed_openai_params` in a request
2. Set `allowed_openai_params` on the config.yaml file for a specific model
#### Dynamically pass allowed_openai_params in a request
In this example we pass `allowed_openai_params=["tools"]` to allow the `tools` param for a request sent to the model set on the proxy.
```python showLineNumbers title="Dynamically pass allowed_openai_params in a request"
import openai
from openai import AsyncAzureOpenAI
import openai
client = openai.OpenAI(
api_key="anything",
base_url="http://0.0.0.0:4000"
)
response = client.chat.completions.create(
model="gpt-3.5-turbo",
messages = [
{
"role": "user",
"content": "this is a test request, write a short poem"
}
],
extra_body={
"allowed_openai_params": ["tools"]
}
)
```
#### Set allowed_openai_params on config.yaml
You can also set `allowed_openai_params` on the config.yaml file for a specific model. This means that all requests to this deployment are allowed to pass in the `tools` param.
```yaml showLineNumbers title="Set allowed_openai_params on config.yaml"
model_list:
- model_name: azure-o1-preview
litellm_params:
model: azure/o_series/<my-deployment-name>
api_key: xxxxx
api_base: https://openai-prod-test.openai.azure.com/openai/deployments/o1/chat/completions?api-version=2025-01-01-preview
allowed_openai_params: ["tools"]
```
</TabItem>
</Tabs>
@@ -14,6 +14,7 @@ Translations handled by LiteLLM:
from typing import List, Optional
import litellm
from litellm import verbose_logger
from litellm.types.llms.openai import AllMessageValues
from litellm.utils import get_model_info
@@ -22,6 +23,27 @@ from ...openai.chat.o_series_transformation import OpenAIOSeriesConfig
class AzureOpenAIO1Config(OpenAIOSeriesConfig):
def get_supported_openai_params(self, model: str) -> list:
"""
Get the supported OpenAI params for the Azure O-Series models
"""
all_openai_params = litellm.OpenAIGPTConfig().get_supported_openai_params(
model=model
)
non_supported_params = [
"logprobs",
"top_p",
"presence_penalty",
"frequency_penalty",
"top_logprobs",
]
o_series_only_param = ["reasoning_effort"]
all_openai_params.extend(o_series_only_param)
return [
param for param in all_openai_params if param not in non_supported_params
]
def should_fake_stream(
self,
model: Optional[str],
+1
View File
@@ -1115,6 +1115,7 @@ def completion( # type: ignore # noqa: PLR0915
messages=messages,
reasoning_effort=reasoning_effort,
thinking=thinking,
allowed_openai_params=kwargs.get("allowed_openai_params"),
**non_default_params,
)
+1
View File
@@ -1950,6 +1950,7 @@ all_litellm_params = [
"use_in_pass_through",
"merge_reasoning_content_in_choices",
"litellm_credential_name",
"allowed_openai_params",
] + list(StandardCallbackDynamicParams.__annotations__.keys())
+38 -2
View File
@@ -2843,6 +2843,7 @@ def get_optional_params( # noqa: PLR0915
api_version=None,
parallel_tool_calls=None,
drop_params=None,
allowed_openai_params: Optional[List[str]] = None,
reasoning_effort=None,
additional_drop_params=None,
messages: Optional[List[AllMessageValues]] = None,
@@ -2928,6 +2929,7 @@ def get_optional_params( # noqa: PLR0915
"api_version": None,
"parallel_tool_calls": None,
"drop_params": None,
"allowed_openai_params": None,
"additional_drop_params": None,
"messages": None,
"reasoning_effort": None,
@@ -2944,6 +2946,7 @@ def get_optional_params( # noqa: PLR0915
and k != "custom_llm_provider"
and k != "api_version"
and k != "drop_params"
and k != "allowed_openai_params"
and k != "additional_drop_params"
and k != "messages"
and k in default_params
@@ -3053,6 +3056,12 @@ def get_optional_params( # noqa: PLR0915
tool_function["parameters"] = new_parameters
def _check_valid_arg(supported_params: List[str]):
"""
Check if the params passed to completion() are supported by the provider
Args:
supported_params: List[str] - supported params from the litellm config
"""
verbose_logger.info(
f"\nLiteLLM completion() model= {model}; provider = {custom_llm_provider}"
)
@@ -3086,7 +3095,7 @@ def get_optional_params( # noqa: PLR0915
else:
raise UnsupportedParamsError(
status_code=500,
message=f"{custom_llm_provider} does not support parameters: {unsupported_params}, for model={model}. To drop these, set `litellm.drop_params=True` or for proxy:\n\n`litellm_settings:\n drop_params: true`\n",
message=f"{custom_llm_provider} does not support parameters: {list(unsupported_params.keys())}, for model={model}. To drop these, set `litellm.drop_params=True` or for proxy:\n\n`litellm_settings:\n drop_params: true`\n. \n If you want to use these params dynamically send allowed_openai_params={list(unsupported_params.keys())} in your request.",
)
supported_params = get_supported_openai_params(
@@ -3096,7 +3105,14 @@ def get_optional_params( # noqa: PLR0915
supported_params = get_supported_openai_params(
model=model, custom_llm_provider="openai"
)
_check_valid_arg(supported_params=supported_params or [])
supported_params = supported_params or []
allowed_openai_params = allowed_openai_params or []
supported_params.extend(allowed_openai_params)
_check_valid_arg(
supported_params=supported_params or [],
)
## raise exception if provider doesn't support passed in param
if custom_llm_provider == "anthropic":
## check if unsupported param passed in
@@ -3735,6 +3751,26 @@ def get_optional_params( # noqa: PLR0915
if k not in default_params.keys():
optional_params[k] = passed_params[k]
print_verbose(f"Final returned optional params: {optional_params}")
optional_params = _apply_openai_param_overrides(
optional_params=optional_params,
non_default_params=non_default_params,
allowed_openai_params=allowed_openai_params,
)
return optional_params
def _apply_openai_param_overrides(
optional_params: dict, non_default_params: dict, allowed_openai_params: list
):
"""
If user passes in allowed_openai_params, apply them to optional_params
These params will get passed as is to the LLM API since the user opted in to passing them in the request
"""
if allowed_openai_params:
for param in allowed_openai_params:
if param not in optional_params:
optional_params[param] = non_default_params.pop(param, None)
return optional_params
@@ -137,6 +137,8 @@ async def test_supports_tool_choice():
or model_name in block_list
or "azure/eu" in model_name
or "azure/us" in model_name
or "o1" in model_name
or "o3" in model_name
):
continue
+55 -3
View File
@@ -21,9 +21,10 @@ from base_llm_unit_tests import BaseLLMChatTest, BaseOSeriesModelsTest
class TestAzureOpenAIO1(BaseOSeriesModelsTest, BaseLLMChatTest):
def get_base_completion_call_args(self):
return {
"model": "azure/o1-preview",
"model": "azure/o1",
"api_key": os.getenv("AZURE_OPENAI_O1_KEY"),
"api_base": "https://openai-gpt-4-test-v-1.openai.azure.com",
"api_base": "https://openai-prod-test.openai.azure.com",
"api_version": "2024-12-01-preview"
}
def get_client(self):
@@ -31,7 +32,7 @@ class TestAzureOpenAIO1(BaseOSeriesModelsTest, BaseLLMChatTest):
return AzureOpenAI(
api_key="my-fake-o1-key",
base_url="https://openai-gpt-4-test-v-1.openai.azure.com",
base_url="https://openai-prod-test.openai.azure.com",
api_version="2024-02-15-preview",
)
@@ -170,3 +171,54 @@ def test_openai_o_series_max_retries_0(mock_get_openai_client):
mock_get_openai_client.assert_called_once()
assert mock_get_openai_client.call_args.kwargs["max_retries"] == 0
@pytest.mark.asyncio
async def test_azure_o1_series_response_format_extra_params():
"""
Tool calling should work for all azure o_series models.
"""
litellm._turn_on_debug()
from openai import AsyncAzureOpenAI
litellm.set_verbose = True
client = AsyncAzureOpenAI(
api_key="fake-api-key",
base_url="https://openai-prod-test.openai.azure.com/openai/deployments/o1/chat/completions?api-version=2025-01-01-preview",
api_version="2025-01-01-preview"
)
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_object'}
tool_choice = "auto"
with patch.object(
client.chat.completions.with_raw_response, "create"
) as mock_client:
try:
await litellm.acompletion(
client=client,
model="azure/o_series/<my-deployment-name>",
api_key="xxxxx",
api_base="https://openai-prod-test.openai.azure.com/openai/deployments/o1/chat/completions?api-version=2025-01-01-preview",
api_version="2024-12-01-preview",
messages=[{"role": "user", "content": "Hello! return a json object"}],
tools=tools,
response_format=response_format,
tool_choice=tool_choice
)
except Exception as e:
print(f"Error: {e}")
mock_client.assert_called_once()
request_body = mock_client.call_args.kwargs
print("request_body: ", json.dumps(request_body, indent=4))
assert request_body["tools"] == tools
assert request_body["response_format"] == response_format
assert request_body["tool_choice"] == tool_choice
+57
View File
@@ -17,6 +17,9 @@ import pytest
import litellm
from litellm import RateLimitError, Timeout, completion, completion_cost, embedding
from unittest.mock import AsyncMock, patch
from litellm import RateLimitError, Timeout, completion, completion_cost, embedding
from litellm.llms.custom_httpx.http_handler import AsyncHTTPHandler
litellm.num_retries = 3
@@ -224,3 +227,57 @@ async def test_chat_completion_cohere_stream(sync_mode):
pass
except Exception as e:
pytest.fail(f"Error occurred: {e}")
@pytest.mark.asyncio
async def test_cohere_request_body_with_allowed_params():
"""
Test to validate that when allowed_openai_params is provided, the request body contains
the correct response_format and reasoning_effort values.
"""
# Define test parameters
test_response_format = {"type": "json"}
test_reasoning_effort = "low"
test_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"]
}
}
}]
client = AsyncHTTPHandler()
# Mock the post method
with patch.object(client, "post", new=AsyncMock()) as mock_post:
try:
await litellm.acompletion(
model="cohere/command",
messages=[{"content": "what llm are you", "role": "user"}],
allowed_openai_params=["tools", "response_format", "reasoning_effort"],
response_format=test_response_format,
reasoning_effort=test_reasoning_effort,
tools=test_tools,
client=client
)
except Exception:
pass # We only care about the request body validation
# Verify the API call was made
mock_post.assert_called_once()
# Get and parse the request body
request_data = json.loads(mock_post.call_args.kwargs["data"])
print(f"request_data: {request_data}")
# Validate request contains our specified parameters
assert "allowed_openai_params" not in request_data
assert request_data["response_format"] == test_response_format
assert request_data["reasoning_effort"] == test_reasoning_effort
@@ -67,6 +67,30 @@ def test_anthropic_optional_params(stop_sequence, expected_count):
assert len(optional_params) == expected_count
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_bedrock_optional_params_embeddings():
litellm.drop_params = True
optional_params = get_optional_params_embeddings(
@@ -1380,6 +1404,16 @@ def test_azure_modalities_param():
assert optional_params["modalities"] == ["text", "audio"]
assert optional_params["audio"] == {"type": "audio_input", "input": "test.wav"}
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",
[
@@ -1396,3 +1430,4 @@ def test_anthropic_unified_reasoning_content(model, provider):
reasoning_effort="high",
)
assert optional_params["thinking"] == {"type": "enabled", "budget_tokens": 4096}
+1 -1
View File
@@ -44,7 +44,7 @@ def test_completion_invalid_param_cohere():
except Exception as e:
assert isinstance(e, litellm.UnsupportedParamsError)
print("got an exception=", str(e))
if " cohere does not support parameters: {'seed': 12}" in str(e):
if "cohere does not support parameters: ['seed']" in str(e):
pass
else:
pytest.fail(f"An error occurred {e}")