diff --git a/litellm/llms/perplexity/responses/transformation.py b/litellm/llms/perplexity/responses/transformation.py index 91d40d8247f..cf1ed19cba2 100644 --- a/litellm/llms/perplexity/responses/transformation.py +++ b/litellm/llms/perplexity/responses/transformation.py @@ -22,6 +22,18 @@ from litellm.types.router import GenericLiteLLMParams from litellm.types.utils import LlmProviders +def perplexity_uses_agent_api(model: str) -> bool: + """Whether a Perplexity model id names the Agent API rather than the chat endpoint. + + Agent API ids keep their own vendor namespace once litellm's ``perplexity/`` prefix is + stripped (``perplexity/sonar``, ``openai/gpt-5.2``, ``preset/pro-search``), while chat, + embedding and search ids are a single segment (``sonar``, ``sonar-pro``). Reading the id + keeps routing independent of the cost map, which does not carry every model Perplexity + serves and whose flat ``perplexity/sonar`` chat entry otherwise shadows the Agent API one. + """ + return "/" in model + + class PerplexityResponsesConfig(OpenAIResponsesAPIConfig): def get_supported_openai_params(self, model: str) -> list: """Ref: https://docs.perplexity.ai/api-reference/responses-post""" diff --git a/litellm/main.py b/litellm/main.py index 17edafcdfca..f89e3a37b42 100644 --- a/litellm/main.py +++ b/litellm/main.py @@ -108,6 +108,7 @@ from litellm.llms.cohere.common_utils import CohereModelInfo from litellm.llms.custom_httpx.http_handler import AsyncHTTPHandler, HTTPHandler from litellm.llms.openai.chat.gpt_5_transformation import OpenAIGPT5Config from litellm.llms.openai_like.json_loader import JSONProviderRegistry +from litellm.llms.perplexity.responses.transformation import perplexity_uses_agent_api from litellm.llms.vertex_ai.common_utils import ( VertexAIModelRoute, get_vertex_ai_model_route, @@ -1084,6 +1085,9 @@ def responses_api_bridge_check( mode = "responses" model_info["mode"] = mode + if custom_llm_provider == "perplexity" and perplexity_uses_agent_api(model): + model_info["mode"] = "responses" + # OpenAI/Azure GPT-5 chat-completions that need Responses-only fields (e.g. # ``reasoningSummary`` in ``extra_body``) must be bridged; Chat Completions rejects # those keys. diff --git a/tests/test_litellm/llms/perplexity/responses/test_perplexity_responses_transformation.py b/tests/test_litellm/llms/perplexity/responses/test_perplexity_responses_transformation.py index 534176e381a..3de4cc9210e 100644 --- a/tests/test_litellm/llms/perplexity/responses/test_perplexity_responses_transformation.py +++ b/tests/test_litellm/llms/perplexity/responses/test_perplexity_responses_transformation.py @@ -15,7 +15,10 @@ import pytest from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj from litellm.llms.base_llm.chat.transformation import BaseLLMException -from litellm.llms.perplexity.responses.transformation import PerplexityResponsesConfig +from litellm.llms.perplexity.responses.transformation import ( + PerplexityResponsesConfig, + perplexity_uses_agent_api, +) from litellm.types.llms.openai import ResponsesAPIOptionalRequestParams from litellm.types.utils import LlmProviders from litellm.utils import ProviderConfigManager @@ -587,3 +590,55 @@ class TestPerplexityResponsesTransformation: assert result.type == "response.completed" assert result.response.usage.cost == 0.0003 assert isinstance(result.response.usage.cost, float) + + +class TestPerplexityAgentApiRouting: + """Routing an Agent API deployment to /v1/responses must not depend on the cost map. + + ``completion()`` picks the responses bridge off ``mode`` in the cost map, so an Agent API + model litellm ships no pricing for was unroutable on /v1/chat/completions, and + ``perplexity/perplexity/sonar`` resolved to the older flat ``perplexity/sonar`` chat entry + several steps before the doubled-prefix key. Both went out on Perplexity's chat endpoint + and came back 400 naming a model id nobody wrote. + """ + + @pytest.mark.parametrize( + "model", + [ + "perplexity/sonar", + "perplexity/glm-5.2", + "perplexity/nemotron-3-ultra-550b-a55b", + "openai/gpt-5.2", + "anthropic/claude-opus-4-7", + "preset/pro-search", + ], + ) + def test_agent_api_models_bridge_to_responses(self, model): + from litellm.main import responses_api_bridge_check + + model_info, _ = responses_api_bridge_check(model=model, custom_llm_provider="perplexity") + + assert model_info.get("mode") == "responses" + + @pytest.mark.parametrize("model", ["sonar", "sonar-pro", "sonar-reasoning", "llama-3.1-70b-instruct"]) + def test_chat_models_stay_on_chat_completions(self, model): + from litellm.main import responses_api_bridge_check + + model_info, _ = responses_api_bridge_check(model=model, custom_llm_provider="perplexity") + + assert model_info.get("mode") != "responses" + + def test_other_providers_keep_a_slash_in_the_model_name_on_chat(self): + from litellm.main import responses_api_bridge_check + + model_info, _ = responses_api_bridge_check( + model="meta-llama/Llama-3.3-70B-Instruct", custom_llm_provider="together_ai" + ) + + assert model_info.get("mode") != "responses" + + def test_agent_api_ids_are_told_apart_by_their_vendor_namespace(self): + assert perplexity_uses_agent_api("perplexity/sonar") is True + assert perplexity_uses_agent_api("preset/pro-search") is True + assert perplexity_uses_agent_api("sonar") is False + assert perplexity_uses_agent_api("sonar-pro") is False