From 844e0507684dd873997beeb62d16a11df99bc1f9 Mon Sep 17 00:00:00 2001 From: Elias TOURNEUX Date: Fri, 17 Apr 2026 09:05:37 -0400 Subject: [PATCH] fix(ovhcloud): fix tool calling --- litellm/llms/ovhcloud/chat/transformation.py | 31 +---- .../test_ovhcloud_chat_transformation.py | 112 ++++++++++++------ 2 files changed, 76 insertions(+), 67 deletions(-) diff --git a/litellm/llms/ovhcloud/chat/transformation.py b/litellm/llms/ovhcloud/chat/transformation.py index 1416b782f17..61bfcb0aca2 100644 --- a/litellm/llms/ovhcloud/chat/transformation.py +++ b/litellm/llms/ovhcloud/chat/transformation.py @@ -7,9 +7,8 @@ More information on our website: https://endpoints.ai.cloud.ovh.net from typing import Optional, Union, List import httpx -from litellm.utils import ModelResponseStream, _get_model_info_helper +from litellm.utils import ModelResponseStream from litellm.llms.openai.chat.gpt_transformation import OpenAIGPTConfig -from litellm._logging import verbose_logger from litellm.llms.ovhcloud.utils import OVHCloudException from litellm.llms.base_llm.base_model_iterator import BaseModelResponseIterator from litellm.llms.base_llm.chat.transformation import BaseLLMException @@ -21,34 +20,6 @@ class OVHCloudChatConfig(OpenAIGPTConfig): def custom_llm_provider(self) -> Optional[str]: return "ovhcloud" - def get_supported_openai_params(self, model: str) -> list: - """ - Details about function calling support can be found here: - https://help.ovhcloud.com/csm/en-gb-public-cloud-ai-endpoints-function-calling?id=kb_article_view&sysparm_article=KB0071907 - """ - supports_function_calling: Optional[bool] = None - try: - model_info = _get_model_info_helper(model, custom_llm_provider="ovhcloud") - supports_function_calling = model_info.get( - "supports_function_calling", None - ) - if supports_function_calling is None: - supports_function_calling = False - except Exception as e: - verbose_logger.debug(f"Error getting supported OpenAI params: {e}") - supports_function_calling = False - - optional_params = super().get_supported_openai_params(model) - if supports_function_calling is not True: - verbose_logger.debug( - "You can see our models supporting function_calling in our catalog: https://endpoints.ai.cloud.ovh.net/catalog " - ) - optional_params.remove("tools") - optional_params.remove("tool_choice") - optional_params.remove("function_call") - optional_params.remove("response_format") - return optional_params - def get_complete_url( self, api_base: Optional[str], diff --git a/tests/test_litellm/llms/ovhcloud/test_ovhcloud_chat_transformation.py b/tests/test_litellm/llms/ovhcloud/test_ovhcloud_chat_transformation.py index d391c91cb86..31e89313c38 100644 --- a/tests/test_litellm/llms/ovhcloud/test_ovhcloud_chat_transformation.py +++ b/tests/test_litellm/llms/ovhcloud/test_ovhcloud_chat_transformation.py @@ -21,6 +21,7 @@ from litellm.llms.ovhcloud.chat.transformation import ( config = OVHCloudChatConfig() model = "ovhcloud/Mistral-7B-Instruct-v0.3" + class TestOvhCloudChatCompletionStreamingHandler: def test_chunk_parser_successful(self): handler = OVHCloudChatCompletionStreamingHandler( @@ -58,7 +59,7 @@ class TestOvhCloudChatCompletionStreamingHandler: "error": { "message": "test error", "code": 400, - } + } } with pytest.raises(OVHCloudException) as exc_info: @@ -83,12 +84,10 @@ class TestOvhCloudChatCompletionStreamingHandler: class TestOVHCloudConfig: def test_transform_request_basic(self): - """Test basic request transformation""" + """Test basic request transformation""" transformed_request = config.transform_request( model, - messages=[ - {"role": "user", "content": "Hello, world!"} - ], + messages=[{"role": "user", "content": "Hello, world!"}], optional_params={}, litellm_params={}, headers={}, @@ -100,7 +99,7 @@ class TestOVHCloudConfig: ] def test_transform_request_with_extra_body(self): - """Test request transformation with extra_body parameters""" + """Test request transformation with extra_body parameters""" transformed_request = config.transform_request( model, messages=[{"role": "user", "content": "Hello, world!"}], @@ -115,60 +114,94 @@ class TestOVHCloudConfig: ] def test_map_openai_params(self): - """Test OpenAI parameter mapping""" + """Test OpenAI parameter mapping""" non_default_params = { "temperature": 0.7, "max_tokens": 100, "top_p": 0.9, } - + mapped_params = config.map_openai_params( non_default_params=non_default_params, optional_params={}, model=model, drop_params=False, ) - + assert mapped_params["temperature"] == 0.7 assert mapped_params["max_tokens"] == 100 assert mapped_params["top_p"] == 0.9 def test_get_error_class(self): - """Test error class creation""" + """Test error class creation""" error = config.get_error_class( error_message="Test error", status_code=400, - headers={"Content-Type": "application/json"} + headers={"Content-Type": "application/json"}, ) - + assert isinstance(error, OVHCloudException) assert error.message == "Test error" assert error.status_code == 400 + @pytest.mark.parametrize( + "model", + [ + "Meta-Llama-3_3-70B-Instruct", + "Meta-Llama-3_1-70B-Instruct", + "Mixtral-8x7B-Instruct-v0.1", + "gpt-oss-120b", + "some-model-not-in-the-cost-map", + ], + ) + def test_tools_not_filtered_by_static_model_map(self, model): + """ + OVHCloud AI Endpoints are OpenAI-compatible; tools/tool_choice must pass + through for any model. The server is responsible for rejecting unsupported + tool calls — LiteLLM must not strip them based on a stale static catalog. + """ + from litellm.utils import get_optional_params + + params = get_optional_params( + model=model, + custom_llm_provider="ovhcloud", + tools=[ + { + "type": "function", + "function": {"name": "x", "parameters": {}}, + } + ], + tool_choice="auto", + ) + + assert "tools" in params + assert "tool_choice" in params + def test_ovhcloud_integration(): import os from litellm import completion - - api_key = os.getenv("OVHCLOUD_API_KEY") - + + api_key = os.getenv("OVHCLOUD_API_KEY") + if not api_key: pytest.skip("OVHCLOUD_API_KEY not set, skipping test") - + response = completion( model, messages=[{"role": "user", "content": "Say hello in one word"}], api_key=api_key, max_tokens=10, - temperature=0.7 + temperature=0.7, ) - + assert response.choices[0].message.content assert len(response.choices[0].message.content.strip()) > 0 assert response.model assert response.usage assert response.usage.total_tokens > 0 + def test_OVHCloud_streaming_integration(): """ Integration test for streaming - requires real API key @@ -176,22 +209,24 @@ def test_OVHCloud_streaming_integration(): """ import os from litellm import completion - - api_key = os.getenv("OVHCLOUD_API_KEY") - + + api_key = os.getenv("OVHCLOUD_API_KEY") + if not api_key: pytest.skip("OVHCLOUD_API_KEY not set, skipping test") - + try: - print(f"🔍 Testing streaming with API key: {api_key[:6]}...{api_key[-4:]} (length: {len(api_key)})") + print( + f"🔍 Testing streaming with API key: {api_key[:6]}...{api_key[-4:]} (length: {len(api_key)})" + ) print(f"🔍 API base URL: {os.getenv('OVHCLOUD_API_BASE')}") - + response = completion( model, messages=[{"role": "user", "content": "Count from 1 to 5"}], api_key=api_key, max_tokens=50, - stream=True + stream=True, ) chunks = [] @@ -215,42 +250,45 @@ def test_OVHCloud_streaming_integration(): print(f"❌ Streaming integration test error details:") print(f" Error type: {type(e).__name__}") print(f" Error message: {str(e)}") - if hasattr(e, 'status_code'): + if hasattr(e, "status_code"): print(f" Status code: {e.status_code}") - if hasattr(e, 'response'): + if hasattr(e, "response"): print(f" Response: {e.response}") - + pytest.fail(f"Streaming integration test failed: {type(e).__name__}: {str(e)}") + def test_ovhcloud_with_custom_base_url(): """ Test OVHCloud with custom base URL """ import os from litellm import completion - - api_key = os.getenv("OVHCLOUD_API_KEY") - + + api_key = os.getenv("OVHCLOUD_API_KEY") + if not api_key: pytest.skip("OVHCLOUD_API_KEY not set, skipping test") - custom_base_url = os.getenv("OVHCLOUD_API_BASE", "https://oai.endpoints.kepler.ai.cloud.ovh.net/v1") - + custom_base_url = os.getenv( + "OVHCLOUD_API_BASE", "https://oai.endpoints.kepler.ai.cloud.ovh.net/v1" + ) + try: response = completion( model, messages=[{"role": "user", "content": "Hello"}], api_key=api_key, api_base=custom_base_url, - max_tokens=5 + max_tokens=5, ) - + assert response.choices[0].message.content print(f"✅ Custom base URL test passed: {response.choices[0].message.content}") - + except Exception as e: pytest.fail(f"Custom base URL test failed: {str(e)}") if __name__ == "__main__": - pytest.main([__file__, "-v"]) \ No newline at end of file + pytest.main([__file__, "-v"])