diff --git a/docs/my-website/docs/reasoning_content.md b/docs/my-website/docs/reasoning_content.md index 12a0f17ba0..7576e34ee3 100644 --- a/docs/my-website/docs/reasoning_content.md +++ b/docs/my-website/docs/reasoning_content.md @@ -18,6 +18,7 @@ Supported Providers: - XAI (`xai/`) - Google AI Studio (`google/`) - Vertex AI (`vertex_ai/`) +- Perplexity (`perplexity/`) LiteLLM will standardize the `reasoning_content` in the response and `thinking_blocks` in the assistant message. diff --git a/litellm/llms/perplexity/chat/transformation.py b/litellm/llms/perplexity/chat/transformation.py index dab64283ec..4ce2df51b6 100644 --- a/litellm/llms/perplexity/chat/transformation.py +++ b/litellm/llms/perplexity/chat/transformation.py @@ -4,12 +4,18 @@ Translate from OpenAI's `/v1/chat/completions` to Perplexity's `/v1/chat/complet from typing import Optional, Tuple +import litellm +from litellm._logging import verbose_logger from litellm.secret_managers.main import get_secret_str from ...openai.chat.gpt_transformation import OpenAIGPTConfig class PerplexityChatConfig(OpenAIGPTConfig): + @property + def custom_llm_provider(self) -> Optional[str]: + return "perplexity" + def _get_openai_compatible_provider_info( self, api_base: Optional[str], api_key: Optional[str] ) -> Tuple[Optional[str], Optional[str]]: @@ -29,7 +35,7 @@ class PerplexityChatConfig(OpenAIGPTConfig): Eg. Perplexity does not support tools, tool_choice, function_call, functions, etc. """ - return [ + base_openai_params = [ "frequency_penalty", "max_tokens", "max_completion_tokens", @@ -41,3 +47,12 @@ class PerplexityChatConfig(OpenAIGPTConfig): "max_retries", "extra_headers", ] + + try: + if litellm.supports_reasoning( + model=model, custom_llm_provider=self.custom_llm_provider + ): + base_openai_params.append("reasoning_effort") + except Exception as e: + verbose_logger.debug(f"Error checking if model supports reasoning: {e}") + return base_openai_params diff --git a/litellm/model_prices_and_context_window_backup.json b/litellm/model_prices_and_context_window_backup.json index c976c316c8..888097582d 100644 --- a/litellm/model_prices_and_context_window_backup.json +++ b/litellm/model_prices_and_context_window_backup.json @@ -439,6 +439,23 @@ "supports_system_messages": true, "supports_tool_choice": true }, + "gpt-4o-audio-preview-2025-06-03": { + "max_tokens": 16384, + "max_input_tokens": 128000, + "max_output_tokens": 16384, + "input_cost_per_token": 2.5e-06, + "input_cost_per_audio_token": 4.0e-5, + "output_cost_per_token": 1e-05, + "output_cost_per_audio_token": 8.0e-5, + "litellm_provider": "openai", + "mode": "chat", + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_audio_input": true, + "supports_audio_output": true, + "supports_system_messages": true, + "supports_tool_choice": true + }, "gpt-4o-mini-audio-preview": { "max_tokens": 16384, "max_input_tokens": 128000, diff --git a/tests/llm_translation/test_perplexity_reasoning.py b/tests/llm_translation/test_perplexity_reasoning.py new file mode 100644 index 0000000000..70b665ea33 --- /dev/null +++ b/tests/llm_translation/test_perplexity_reasoning.py @@ -0,0 +1,207 @@ +import json +import os +import sys +from unittest.mock import patch, MagicMock + +import pytest + +sys.path.insert( + 0, os.path.abspath("../..") +) # Adds the parent directory to the system path + +import litellm +from litellm import completion +from litellm.utils import get_optional_params + + +class TestPerplexityReasoning: + """ + Test suite for Perplexity Sonar reasoning models with reasoning_effort parameter + """ + + @pytest.mark.parametrize( + "model,reasoning_effort", + [ + ("perplexity/sonar-reasoning", "low"), + ("perplexity/sonar-reasoning", "medium"), + ("perplexity/sonar-reasoning", "high"), + ("perplexity/sonar-reasoning-pro", "low"), + ("perplexity/sonar-reasoning-pro", "medium"), + ("perplexity/sonar-reasoning-pro", "high"), + ] + ) + def test_perplexity_reasoning_effort_parameter_mapping(self, model, reasoning_effort): + """ + Test that reasoning_effort parameter is correctly mapped for Perplexity Sonar reasoning models + """ + # Set up local model cost map + os.environ["LITELLM_LOCAL_MODEL_COST_MAP"] = "True" + litellm.model_cost = litellm.get_model_cost_map(url="") + + # Get provider and optional params + _, provider, _, _ = litellm.get_llm_provider(model=model) + + optional_params = get_optional_params( + model=model, + custom_llm_provider=provider, + reasoning_effort=reasoning_effort, + ) + + # Verify that reasoning_effort is preserved in optional_params for Perplexity + assert "reasoning_effort" in optional_params + assert optional_params["reasoning_effort"] == reasoning_effort + + @pytest.mark.parametrize( + "model", + [ + "perplexity/sonar-reasoning", + "perplexity/sonar-reasoning-pro", + ] + ) + def test_perplexity_reasoning_effort_mock_completion(self, model): + """ + Test that reasoning_effort is correctly passed in actual completion call (mocked) + """ + from openai import OpenAI + from openai.types.chat.chat_completion import ChatCompletion + + litellm.set_verbose = True + + # Mock successful response with reasoning content + response_object = { + "id": "cmpl-test", + "object": "chat.completion", + "created": 1677652288, + "model": model.split("/")[1], + "choices": [ + { + "index": 0, + "message": { + "role": "assistant", + "content": "This is a test response from the reasoning model.", + "reasoning_content": "Let me think about this step by step...", + }, + "finish_reason": "stop", + } + ], + "usage": { + "prompt_tokens": 9, + "completion_tokens": 20, + "total_tokens": 29, + "completion_tokens_details": { + "reasoning_tokens": 15 + } + }, + } + + pydantic_obj = ChatCompletion(**response_object) + + def _return_pydantic_obj(*args, **kwargs): + new_response = MagicMock() + new_response.headers = {"content-type": "application/json"} + new_response.parse.return_value = pydantic_obj + return new_response + + openai_client = OpenAI(api_key="fake-api-key") + + with patch.object( + openai_client.chat.completions.with_raw_response, "create", side_effect=_return_pydantic_obj + ) as mock_client: + + response = completion( + model=model, + messages=[{"role": "user", "content": "Hello, please think about this carefully."}], + reasoning_effort="high", + client=openai_client, + ) + + # Verify the call was made + assert mock_client.called + + # Get the request data from the mock call + call_args = mock_client.call_args + request_data = call_args.kwargs + + # Verify reasoning_effort was included in the request + assert "reasoning_effort" in request_data + assert request_data["reasoning_effort"] == "high" + + # Verify response structure + assert response.choices[0].message.content is not None + assert response.choices[0].message.content == "This is a test response from the reasoning model." + + def test_perplexity_reasoning_models_support_reasoning(self): + """ + Test that Perplexity Sonar reasoning models are correctly identified as supporting reasoning + """ + from litellm.utils import supports_reasoning + + # Set up local model cost map + os.environ["LITELLM_LOCAL_MODEL_COST_MAP"] = "True" + litellm.model_cost = litellm.get_model_cost_map(url="") + + reasoning_models = [ + "perplexity/sonar-reasoning", + "perplexity/sonar-reasoning-pro", + ] + + for model in reasoning_models: + assert supports_reasoning(model, None), f"{model} should support reasoning" + + def test_perplexity_non_reasoning_models_dont_support_reasoning(self): + """ + Test that non-reasoning Perplexity models don't support reasoning + """ + from litellm.utils import supports_reasoning + + # Set up local model cost map + os.environ["LITELLM_LOCAL_MODEL_COST_MAP"] = "True" + litellm.model_cost = litellm.get_model_cost_map(url="") + + non_reasoning_models = [ + "perplexity/sonar", + "perplexity/sonar-pro", + "perplexity/llama-3.1-sonar-large-128k-chat", + "perplexity/mistral-7b-instruct", + ] + + for model in non_reasoning_models: + # These models should not support reasoning (should return False or raise exception) + try: + result = supports_reasoning(model, None) + # If it doesn't raise an exception, it should return False + assert result is False, f"{model} should not support reasoning" + except Exception: + # If it raises an exception, that's also acceptable behavior + pass + + @pytest.mark.parametrize( + "model,expected_api_base", + [ + ("perplexity/sonar-reasoning", "https://api.perplexity.ai"), + ("perplexity/sonar-reasoning-pro", "https://api.perplexity.ai"), + ] + ) + def test_perplexity_reasoning_api_base_configuration(self, model, expected_api_base): + """ + Test that Perplexity reasoning models use the correct API base + """ + from litellm.llms.perplexity.chat.transformation import PerplexityChatConfig + + config = PerplexityChatConfig() + api_base, _ = config._get_openai_compatible_provider_info( + api_base=None, api_key="test-key" + ) + + assert api_base == expected_api_base + + def test_perplexity_reasoning_effort_in_supported_params(self): + """ + Test that reasoning_effort is in the list of supported parameters for Perplexity + """ + from litellm.llms.perplexity.chat.transformation import PerplexityChatConfig + + config = PerplexityChatConfig() + supported_params = config.get_supported_openai_params(model="perplexity/sonar-reasoning") + + assert "reasoning_effort" in supported_params \ No newline at end of file