From a2781f0db3f96434f735aecc99bc174eefa9e3de Mon Sep 17 00:00:00 2001 From: Chesars Date: Tue, 10 Mar 2026 19:51:23 -0300 Subject: [PATCH] fix(perplexity): remove models from shared TypedDict and restore error handling - Remove Perplexity-specific `models` field from ResponsesAPIOptionalRequestParams TypedDict to avoid polluting the shared OpenAI type. The param is still passed through via get_supported_openai_params + map_openai_params at runtime. - Add transform_response_api_response override to catch Perplexity's HTTP 200 with status:"failed" and raise BaseLLMException instead of silently succeeding. --- .../perplexity/responses/transformation.py | 35 ++++++- litellm/types/llms/openai.py | 2 - ...est_perplexity_responses_transformation.py | 93 +++++++++++++++++++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/litellm/llms/perplexity/responses/transformation.py b/litellm/llms/perplexity/responses/transformation.py index ad85b88d7d..233b368495 100644 --- a/litellm/llms/perplexity/responses/transformation.py +++ b/litellm/llms/perplexity/responses/transformation.py @@ -4,15 +4,20 @@ Perplexity Responses API — OpenAI-compatible. The only provider quirks: - cost returned as dict → handled by ResponseAPIUsage.parse_cost validator - preset models (preset/pro-search) → handled by transform_responses_api_request +- HTTP 200 with status:"failed" → raised as exception in transform_response_api_response Ref: https://docs.perplexity.ai/api-reference/responses-post """ from typing import Dict, Optional, Union +import httpx + +from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj +from litellm.llms.base_llm.chat.transformation import BaseLLMException from litellm.llms.openai.responses.transformation import OpenAIResponsesAPIConfig from litellm.secret_managers.main import get_secret_str -from litellm.types.llms.openai import ResponseInputParam +from litellm.types.llms.openai import ResponseInputParam, ResponsesAPIResponse from litellm.types.router import GenericLiteLLMParams from litellm.types.utils import LlmProviders @@ -78,6 +83,34 @@ class PerplexityResponsesConfig(OpenAIResponsesAPIConfig): headers=headers, ) + def transform_response_api_response( + self, + model: str, + raw_response: httpx.Response, + logging_obj: LiteLLMLoggingObj, + ) -> ResponsesAPIResponse: + """Check for Perplexity's status:'failed' on HTTP 200 before delegating to base.""" + try: + raw_response_json = raw_response.json() + except Exception: + raw_response_json = None + + if ( + isinstance(raw_response_json, dict) + and raw_response_json.get("status") == "failed" + ): + error = raw_response_json.get("error", {}) + raise BaseLLMException( + status_code=raw_response.status_code, + message=error.get("message", "Unknown Perplexity error"), + ) + + return super().transform_response_api_response( + model=model, + raw_response=raw_response, + logging_obj=logging_obj, + ) + def supports_native_websocket(self) -> bool: """Perplexity does not support native WebSocket for Responses API""" return False diff --git a/litellm/types/llms/openai.py b/litellm/types/llms/openai.py index ac661a1e61..b09303b577 100644 --- a/litellm/types/llms/openai.py +++ b/litellm/types/llms/openai.py @@ -1155,8 +1155,6 @@ class ResponsesAPIOptionalRequestParams(TypedDict, total=False): ] # Number of partial images to generate (1-3) for streaming image generation context_management: Optional[List[ContextManagementEntry]] """Context management configuration. E.g. [{\"type\": \"compaction\", \"compact_threshold\": 200000}] for server-side compaction (minimum 1000).""" - models: Optional[List[str]] - """Model fallback chain (e.g. Perplexity). Models are tried in order until one succeeds.""" class ResponsesAPIRequestParams(ResponsesAPIOptionalRequestParams, total=False): 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 8d38c3fb57..7a3c0316d4 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 @@ -7,11 +7,17 @@ transformations for the Agent API (Responses API). Source: litellm/llms/perplexity/responses/transformation.py """ +import json import os import sys +import httpx +import pytest + sys.path.insert(0, os.path.abspath("../../../../..")) +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.types.llms.openai import ResponsesAPIOptionalRequestParams from litellm.types.utils import LlmProviders @@ -359,3 +365,90 @@ class TestPerplexityResponsesTransformation: assert config is not None assert isinstance(config, PerplexityResponsesConfig) assert config.custom_llm_provider == LlmProviders.PERPLEXITY + + def test_failed_status_raises_exception(self): + """Perplexity HTTP 200 with status:'failed' must raise BaseLLMException""" + config = PerplexityResponsesConfig() + + failed_body = { + "status": "failed", + "error": {"message": "Model quota exceeded"}, + } + + raw_response = httpx.Response( + status_code=200, + json=failed_body, + request=httpx.Request("POST", "https://api.perplexity.ai/v1/responses"), + ) + + logging_obj = LiteLLMLoggingObj( + model="perplexity/openai/gpt-5.2", + messages=[], + stream=False, + call_type="responses", + start_time=None, + litellm_call_id="test", + function_id="test", + ) + + with pytest.raises(BaseLLMException) as exc_info: + config.transform_response_api_response( + model="perplexity/openai/gpt-5.2", + raw_response=raw_response, + logging_obj=logging_obj, + ) + + assert "Model quota exceeded" in str(exc_info.value.message) + + def test_successful_response_passes_through(self): + """Normal completed response delegates to base OpenAI handler""" + config = PerplexityResponsesConfig() + + success_body = { + "id": "resp_123", + "object": "response", + "created_at": 1700000000, + "status": "completed", + "model": "openai/gpt-5.2", + "output": [ + { + "type": "message", + "id": "msg_123", + "role": "assistant", + "status": "completed", + "content": [ + {"type": "output_text", "text": "Hello!", "annotations": []} + ], + } + ], + "usage": { + "input_tokens": 10, + "output_tokens": 5, + "total_tokens": 15, + }, + } + + raw_response = httpx.Response( + status_code=200, + json=success_body, + request=httpx.Request("POST", "https://api.perplexity.ai/v1/responses"), + ) + + logging_obj = LiteLLMLoggingObj( + model="perplexity/openai/gpt-5.2", + messages=[], + stream=False, + call_type="responses", + start_time=None, + litellm_call_id="test", + function_id="test", + ) + + response = config.transform_response_api_response( + model="perplexity/openai/gpt-5.2", + raw_response=raw_response, + logging_obj=logging_obj, + ) + + assert response.id == "resp_123" + assert response.status == "completed"