From 849d6b7cdbcd51afa4e9fecd3c748c8090c49d6e Mon Sep 17 00:00:00 2001 From: Sameer Kankute Date: Tue, 10 Feb 2026 17:00:49 +0530 Subject: [PATCH 1/6] Add perplexity response api class --- litellm/llms/perplexity/responses/__init__.py | 7 + .../perplexity/responses/transformation.py | 509 ++++++++++++++++++ 2 files changed, 516 insertions(+) create mode 100644 litellm/llms/perplexity/responses/__init__.py create mode 100644 litellm/llms/perplexity/responses/transformation.py diff --git a/litellm/llms/perplexity/responses/__init__.py b/litellm/llms/perplexity/responses/__init__.py new file mode 100644 index 00000000000..9bdf810e839 --- /dev/null +++ b/litellm/llms/perplexity/responses/__init__.py @@ -0,0 +1,7 @@ +""" +Perplexity Agentic Research API (Responses API) module +""" + +from .transformation import PerplexityResponsesConfig + +__all__ = ["PerplexityResponsesConfig"] diff --git a/litellm/llms/perplexity/responses/transformation.py b/litellm/llms/perplexity/responses/transformation.py new file mode 100644 index 00000000000..1dfee52d46e --- /dev/null +++ b/litellm/llms/perplexity/responses/transformation.py @@ -0,0 +1,509 @@ +""" +Transformation logic for Perplexity Agentic Research API (Responses API) + +This module handles the translation between OpenAI's Responses API format +and Perplexity's Responses API format, which supports: +- Third-party model access (OpenAI, Anthropic, Google, xAI, etc.) +- Presets for optimized configurations +- Web search and URL fetching tools +- Reasoning effort control +- Instructions parameter for system-level guidance +""" + +from typing import Any, Dict, List, Literal, Optional, Tuple, Union + +import httpx + +import litellm +from litellm._logging import verbose_logger +from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj +from litellm.llms.base_llm.chat.transformation import BaseLLMException +from litellm.llms.base_llm.responses.transformation import BaseResponsesAPIConfig +from litellm.secret_managers.main import get_secret_str +from litellm.types.llms.openai import ( + ResponseInputParam, + ResponsesAPIOptionalRequestParams, + ResponsesAPIResponse, + ResponsesAPIStreamingResponse, +) +from litellm.types.responses.main import DeleteResponseResult +from litellm.types.router import GenericLiteLLMParams +from litellm.types.utils import LlmProviders + + +class PerplexityResponsesConfig(BaseResponsesAPIConfig): + """ + Configuration for Perplexity Agentic Research API (Responses API) + + Reference: https://docs.perplexity.ai/agentic-research/quickstart + """ + + @property + def custom_llm_provider(self) -> LlmProviders: + return "perplexity" + + def get_supported_openai_params(self, model: str) -> list: + """ + Perplexity Responses API supports a different set of parameters + + Ref: https://docs.perplexity.ai/api-reference/responses-post + """ + return [ + "max_output_tokens", + "stream", + "temperature", + "top_p", + "tools", + "reasoning", + "preset", + "instructions", + ] + + def validate_environment( + self, headers: dict, model: str, litellm_params: Optional[GenericLiteLLMParams] + ) -> dict: + """Validate environment and set up headers""" + # Get API key from environment + api_key = ( + get_secret_str("PERPLEXITYAI_API_KEY") + or get_secret_str("PERPLEXITY_API_KEY") + ) + + if api_key: + headers["Authorization"] = f"Bearer {api_key}" + + headers["Content-Type"] = "application/json" + + return headers + + def get_complete_url( + self, + api_base: Optional[str], + litellm_params: dict, + ) -> str: + """Get the complete URL for the Perplexity Responses API""" + if api_base is None: + api_base = get_secret_str("PERPLEXITY_API_BASE") or "https://api.perplexity.ai" + + # Ensure api_base doesn't end with a slash + api_base = api_base.rstrip("/") + + # Add the responses endpoint + return f"{api_base}/v1/responses" + + def map_openai_params( + self, + response_api_optional_params: ResponsesAPIOptionalRequestParams, + model: str, + drop_params: bool, + ) -> Dict: + """ + Map OpenAI Responses API parameters to Perplexity format + + Key differences: + - Supports 'preset' parameter for predefined configurations + - Supports 'instructions' parameter for system-level guidance + - Tools are specified differently (web_search, fetch_url) + """ + mapped_params = {} + + # Map standard parameters + if response_api_optional_params.get("max_output_tokens"): + mapped_params["max_output_tokens"] = response_api_optional_params["max_output_tokens"] + + if response_api_optional_params.get("temperature"): + mapped_params["temperature"] = response_api_optional_params["temperature"] + + if response_api_optional_params.get("top_p"): + mapped_params["top_p"] = response_api_optional_params["top_p"] + + if response_api_optional_params.get("stream"): + mapped_params["stream"] = response_api_optional_params["stream"] + + if response_api_optional_params.get("stream_options"): + mapped_params["stream_options"] = response_api_optional_params["stream_options"] + + # Map Perplexity-specific parameters + if response_api_optional_params.get("preset"): + mapped_params["preset"] = response_api_optional_params["preset"] + + if response_api_optional_params.get("instructions"): + mapped_params["instructions"] = response_api_optional_params["instructions"] + + if response_api_optional_params.get("reasoning"): + mapped_params["reasoning"] = response_api_optional_params["reasoning"] + + if response_api_optional_params.get("tools"): + mapped_params["tools"] = self._transform_tools(response_api_optional_params["tools"]) + + return mapped_params + + def _transform_tools(self, tools: List[Dict[str, Any]]) -> List[Dict[str, Any]]: + """ + Transform tools to Perplexity format + + Perplexity supports: + - web_search: Performs web searches + - fetch_url: Fetches content from URLs + """ + perplexity_tools = [] + + for tool in tools: + if isinstance(tool, dict): + tool_type = tool.get("type") + + # Direct Perplexity tool format + if tool_type in ["web_search", "fetch_url"]: + perplexity_tools.append(tool) + + # OpenAI function format - try to map to Perplexity tools + elif tool_type == "function": + function = tool.get("function", {}) + function_name = function.get("name", "") + + if function_name == "web_search" or "search" in function_name.lower(): + perplexity_tools.append({"type": "web_search"}) + elif function_name == "fetch_url" or "fetch" in function_name.lower(): + perplexity_tools.append({"type": "fetch_url"}) + + return perplexity_tools + + def transform_responses_api_request( + self, + model: str, + input: Union[str, ResponseInputParam], + response_api_optional_request_params: Dict, + litellm_params: GenericLiteLLMParams, + headers: dict, + ) -> Dict: + """ + Transform request to Perplexity Responses API format + """ + # Check if the model is a preset (format: preset/preset-name) + if model.startswith("preset/"): + preset_name = model.replace("preset/", "") + data = { + "preset": preset_name, + "input": self._format_input(input), + } + # Check if preset is explicitly provided in params + elif response_api_optional_request_params.get("preset"): + data = { + "preset": response_api_optional_request_params.pop("preset"), + "input": self._format_input(input), + } + else: + # Full request format for third-party models + data = { + "model": model, + "input": self._format_input(input), + } + + # Add all optional parameters + for key, value in response_api_optional_request_params.items(): + data[key] = value + + return data + + def _format_input(self, input: Union[str, ResponseInputParam]) -> Union[str, List[Dict[str, Any]]]: + """ + Format input for Perplexity Responses API + + The API accepts either: + - A simple string for single-turn queries + - An array of message objects for multi-turn conversations + """ + if isinstance(input, str): + return input + + # Handle ResponseInputParam format + if isinstance(input, list): + formatted_messages = [] + for item in input: + if isinstance(item, dict): + formatted_message = { + "type": "message", + "role": item.get("role"), + "content": item.get("content", ""), + } + formatted_messages.append(formatted_message) + return formatted_messages + + return str(input) + + def transform_response_api_response( + self, + model: str, + raw_response: httpx.Response, + logging_obj: LiteLLMLoggingObj, + ) -> ResponsesAPIResponse: + """ + Transform Perplexity Responses API response to OpenAI Responses API format + """ + try: + raw_response_json = raw_response.json() + except Exception as e: + raise BaseLLMException( + status_code=raw_response.status_code, + message=f"Failed to parse response: {str(e)}", + ) + + # Check for error status + status = raw_response_json.get("status") + if status == "failed": + error = raw_response_json.get("error", {}) + error_message = error.get("message", "Unknown error") + raise BaseLLMException( + status_code=raw_response.status_code, + message=error_message, + ) + + # Transform usage to handle Perplexity's cost structure + usage_data = raw_response_json.get("usage", {}) + transformed_usage = self._transform_usage(usage_data) + + # Map Perplexity response to OpenAI Responses API format + response = ResponsesAPIResponse( + id=raw_response_json.get("id", ""), + object="response", + created_at=raw_response_json.get("created_at", 0), + status=raw_response_json.get("status", "completed"), + model=raw_response_json.get("model", model), + output=raw_response_json.get("output", []), + usage=transformed_usage, + ) + + return response + + def _transform_usage(self, usage_data: Dict[str, Any]) -> Dict[str, Any]: + """ + Transform Perplexity usage data to OpenAI format + + Perplexity returns: + { + "input_tokens": 100, + "output_tokens": 200, + "total_tokens": 300, + "cost": { + "currency": "USD", + "input_cost": 0.0001, + "output_cost": 0.0002, + "total_cost": 0.0003 + } + } + + OpenAI expects: + { + "input_tokens": 100, + "output_tokens": 200, + "total_tokens": 300 + } + """ + transformed = { + "input_tokens": usage_data.get("input_tokens", 0), + "output_tokens": usage_data.get("output_tokens", 0), + "total_tokens": usage_data.get("total_tokens", 0), + } + + # Add input_tokens_details if present + if "input_tokens_details" in usage_data: + transformed["input_tokens_details"] = usage_data["input_tokens_details"] + + # Add output_tokens_details if present + if "output_tokens_details" in usage_data: + transformed["output_tokens_details"] = usage_data["output_tokens_details"] + + return transformed + + def transform_streaming_response( + self, + model: str, + parsed_chunk: dict, + logging_obj: LiteLLMLoggingObj, + ) -> ResponsesAPIStreamingResponse: + """ + Transform a parsed streaming response chunk into a ResponsesAPIStreamingResponse + """ + # Map Perplexity streaming chunk to OpenAI format + return ResponsesAPIStreamingResponse(**parsed_chunk) + + def transform_delete_response_api_request( + self, + response_id: str, + api_base: str, + litellm_params: GenericLiteLLMParams, + headers: dict, + ) -> Tuple[str, Dict]: + """Transform delete response API request""" + # Perplexity may not support deleting responses + # Return appropriate URL and params + url = f"{api_base}/v1/responses/{response_id}" + return url, {} + + def transform_delete_response_api_response( + self, + raw_response: httpx.Response, + logging_obj: LiteLLMLoggingObj, + ) -> DeleteResponseResult: + """Transform delete response API response""" + try: + response_json = raw_response.json() + return DeleteResponseResult( + id=response_json.get("id", ""), + object="response.deleted", + deleted=response_json.get("deleted", True), + ) + except Exception as e: + raise BaseLLMException( + status_code=raw_response.status_code, + message=f"Failed to parse delete response: {str(e)}", + ) + + def transform_get_response_api_request( + self, + response_id: str, + api_base: str, + litellm_params: GenericLiteLLMParams, + headers: dict, + ) -> Tuple[str, Dict]: + """Transform get response API request""" + url = f"{api_base}/v1/responses/{response_id}" + return url, {} + + def transform_get_response_api_response( + self, + raw_response: httpx.Response, + logging_obj: LiteLLMLoggingObj, + ) -> ResponsesAPIResponse: + """Transform get response API response""" + return self.transform_response_api_response( + model="", # Model will be in the response + raw_response=raw_response, + logging_obj=logging_obj, + ) + + def transform_list_input_items_request( + self, + response_id: str, + api_base: str, + litellm_params: GenericLiteLLMParams, + headers: dict, + after: Optional[str] = None, + before: Optional[str] = None, + include: Optional[List[str]] = None, + limit: int = 20, + order: Literal["asc", "desc"] = "desc", + ) -> Tuple[str, Dict]: + """Transform list input items request""" + url = f"{api_base}/v1/responses/{response_id}/input_items" + params = { + "limit": limit, + "order": order, + } + + if after: + params["after"] = after + if before: + params["before"] = before + if include: + params["include"] = include + + return url, params + + def transform_list_input_items_response( + self, + raw_response: httpx.Response, + logging_obj: LiteLLMLoggingObj, + ) -> Dict: + """Transform list input items response""" + try: + return raw_response.json() + except Exception as e: + raise BaseLLMException( + status_code=raw_response.status_code, + message=f"Failed to parse list input items response: {str(e)}", + ) + + def get_error_class( + self, error_message: str, status_code: int, headers: Union[Dict, httpx.Headers] + ) -> BaseLLMException: + """Return appropriate error class based on status code""" + return BaseLLMException( + status_code=status_code, + message=error_message, + headers=headers, + ) + + def should_fake_stream( + self, + model: Optional[str], + stream: Optional[bool], + custom_llm_provider: Optional[str] = None, + ) -> bool: + """Returns True if litellm should fake a stream for the given model and stream value""" + return False + + ######################################################### + ########## CANCEL RESPONSE API TRANSFORMATION ########## + ######################################################### + def transform_cancel_response_api_request( + self, + response_id: str, + api_base: str, + litellm_params: GenericLiteLLMParams, + headers: dict, + ) -> Tuple[str, Dict]: + """Transform cancel response API request""" + # Perplexity may not support canceling responses + # Return appropriate URL and params + url = f"{api_base}/v1/responses/{response_id}/cancel" + return url, {} + + def transform_cancel_response_api_response( + self, + raw_response: httpx.Response, + logging_obj: LiteLLMLoggingObj, + ) -> ResponsesAPIResponse: + """Transform cancel response API response""" + return self.transform_response_api_response( + model="", # Model will be in the response + raw_response=raw_response, + logging_obj=logging_obj, + ) + + ######################################################### + ########## COMPACT RESPONSE API TRANSFORMATION ########## + ######################################################### + def transform_compact_response_api_request( + self, + model: str, + input: Union[str, ResponseInputParam], + response_api_optional_request_params: Dict, + api_base: str, + litellm_params: GenericLiteLLMParams, + headers: dict, + ) -> Tuple[str, Dict]: + """Transform compact response API request""" + # Perplexity may not support compact responses + # Return standard URL and transformed request + url = f"{api_base}/v1/responses" + request_data = self.transform_responses_api_request( + model=model, + input=input, + response_api_optional_request_params=response_api_optional_request_params, + litellm_params=litellm_params, + headers=headers, + ) + return url, request_data + + def transform_compact_response_api_response( + self, + raw_response: httpx.Response, + logging_obj: LiteLLMLoggingObj, + ) -> ResponsesAPIResponse: + """Transform compact response API response""" + return self.transform_response_api_response( + model="", # Model will be in the response + raw_response=raw_response, + logging_obj=logging_obj, + ) From be0ebb153e646a774df06bb3f8a2c2e7a922eabd Mon Sep 17 00:00:00 2001 From: Sameer Kankute Date: Tue, 10 Feb 2026 17:01:08 +0530 Subject: [PATCH 2/6] Add perplexity response api routing --- litellm/_lazy_imports_registry.py | 5 ++ litellm/utils.py | 2 + model_prices_and_context_window.json | 70 ++++++++++++++++++++++++++++ 3 files changed, 77 insertions(+) diff --git a/litellm/_lazy_imports_registry.py b/litellm/_lazy_imports_registry.py index a01fe9c11db..791a129880a 100644 --- a/litellm/_lazy_imports_registry.py +++ b/litellm/_lazy_imports_registry.py @@ -274,6 +274,7 @@ LLM_CONFIG_NAMES = ( "LmStudioEmbeddingConfig", "NscaleConfig", "PerplexityChatConfig", + "PerplexityResponsesConfig", "AzureOpenAIO1Config", "IBMWatsonXAIConfig", "IBMWatsonXChatConfig", @@ -1033,6 +1034,10 @@ _LLM_CONFIGS_IMPORT_MAP = { ".llms.perplexity.chat.transformation", "PerplexityChatConfig", ), + "PerplexityResponsesConfig": ( + ".llms.perplexity.responses.transformation", + "PerplexityResponsesConfig", + ), "AzureOpenAIO1Config": ( ".llms.azure.chat.o_series_transformation", "AzureOpenAIO1Config", diff --git a/litellm/utils.py b/litellm/utils.py index 6fdd2d88bca..ed0d6ee930d 100644 --- a/litellm/utils.py +++ b/litellm/utils.py @@ -8243,6 +8243,8 @@ class ProviderConfigManager: return litellm.VolcEngineResponsesAPIConfig() elif litellm.LlmProviders.MANUS == provider: return litellm.ManusResponsesAPIConfig() + elif litellm.LlmProviders.PERPLEXITY == provider: + return litellm.PerplexityResponsesConfig() return None @staticmethod diff --git a/model_prices_and_context_window.json b/model_prices_and_context_window.json index 815d29c7964..6076d290739 100644 --- a/model_prices_and_context_window.json +++ b/model_prices_and_context_window.json @@ -25614,6 +25614,76 @@ "supports_function_calling": true, "supports_tool_choice": true }, + "perplexity/preset/pro-search": { + "litellm_provider": "perplexity", + "mode": "responses", + "supports_web_search": true, + "supports_preset": true, + "preset_name": "pro-search" + }, + "perplexity/openai/gpt-4o": { + "litellm_provider": "perplexity", + "mode": "responses", + "supports_web_search": true, + "supports_reasoning": false, + "third_party_provider": "openai" + }, + "perplexity/openai/gpt-4o-mini": { + "litellm_provider": "perplexity", + "mode": "responses", + "supports_web_search": true, + "supports_reasoning": false, + "third_party_provider": "openai" + }, + "perplexity/openai/gpt-5.2": { + "litellm_provider": "perplexity", + "mode": "responses", + "supports_web_search": true, + "supports_reasoning": true, + "third_party_provider": "openai" + }, + "perplexity/anthropic/claude-3-5-sonnet-20241022": { + "litellm_provider": "perplexity", + "mode": "responses", + "supports_web_search": true, + "supports_reasoning": false, + "third_party_provider": "anthropic" + }, + "perplexity/anthropic/claude-3-5-haiku-20241022": { + "litellm_provider": "perplexity", + "mode": "responses", + "supports_web_search": true, + "supports_reasoning": false, + "third_party_provider": "anthropic" + }, + "perplexity/google/gemini-2.0-flash-exp": { + "litellm_provider": "perplexity", + "mode": "responses", + "supports_web_search": true, + "supports_reasoning": false, + "third_party_provider": "google" + }, + "perplexity/google/gemini-2.0-flash-thinking-exp": { + "litellm_provider": "perplexity", + "mode": "responses", + "supports_web_search": true, + "supports_reasoning": true, + "third_party_provider": "google" + }, + "perplexity/xai/grok-2-1212": { + "litellm_provider": "perplexity", + "mode": "responses", + "supports_web_search": true, + "supports_reasoning": false, + "third_party_provider": "xai" + }, + "perplexity/xai/grok-2-vision-1212": { + "litellm_provider": "perplexity", + "mode": "responses", + "supports_web_search": true, + "supports_reasoning": false, + "third_party_provider": "xai" + }, "publicai/aisingapore/Qwen-SEA-LION-v4-32B-IT": { "input_cost_per_token": 0.0, "litellm_provider": "publicai", From ac65524d9f5cfc353585f9be0152691a548e5447 Mon Sep 17 00:00:00 2001 From: Sameer Kankute Date: Tue, 10 Feb 2026 17:37:08 +0530 Subject: [PATCH 3/6] Use openai base config --- .../perplexity/responses/transformation.py | 259 +++++------------- 1 file changed, 75 insertions(+), 184 deletions(-) diff --git a/litellm/llms/perplexity/responses/transformation.py b/litellm/llms/perplexity/responses/transformation.py index 1dfee52d46e..27b78bac998 100644 --- a/litellm/llms/perplexity/responses/transformation.py +++ b/litellm/llms/perplexity/responses/transformation.py @@ -10,15 +10,14 @@ and Perplexity's Responses API format, which supports: - Instructions parameter for system-level guidance """ -from typing import Any, Dict, List, Literal, Optional, Tuple, Union +from typing import Any, Dict, List, Optional, Union import httpx -import litellm from litellm._logging import verbose_logger from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj from litellm.llms.base_llm.chat.transformation import BaseLLMException -from litellm.llms.base_llm.responses.transformation import BaseResponsesAPIConfig +from litellm.llms.openai.responses.transformation import OpenAIResponsesAPIConfig from litellm.secret_managers.main import get_secret_str from litellm.types.llms.openai import ( ResponseInputParam, @@ -26,14 +25,14 @@ from litellm.types.llms.openai import ( ResponsesAPIResponse, ResponsesAPIStreamingResponse, ) -from litellm.types.responses.main import DeleteResponseResult from litellm.types.router import GenericLiteLLMParams from litellm.types.utils import LlmProviders -class PerplexityResponsesConfig(BaseResponsesAPIConfig): +class PerplexityResponsesConfig(OpenAIResponsesAPIConfig): """ Configuration for Perplexity Agentic Research API (Responses API) + Reference: https://docs.perplexity.ai/agentic-research/quickstart """ @@ -57,6 +56,7 @@ class PerplexityResponsesConfig(BaseResponsesAPIConfig): "reasoning", "preset", "instructions", + "models", # Model fallback support ] def validate_environment( @@ -296,7 +296,8 @@ class PerplexityResponsesConfig(BaseResponsesAPIConfig): { "input_tokens": 100, "output_tokens": 200, - "total_tokens": 300 + "total_tokens": 300, + "cost": 0.0003 } """ transformed = { @@ -305,6 +306,19 @@ class PerplexityResponsesConfig(BaseResponsesAPIConfig): "total_tokens": usage_data.get("total_tokens", 0), } + # Transform cost from Perplexity format (dict) to OpenAI format (float) + cost_obj = usage_data.get("cost") + if isinstance(cost_obj, dict) and "total_cost" in cost_obj: + transformed["cost"] = cost_obj["total_cost"] + verbose_logger.debug( + "Transformed Perplexity cost object to float: %s -> %s", + cost_obj, + cost_obj["total_cost"] + ) + elif cost_obj is not None: + # If cost is already a float/number, use it as-is + transformed["cost"] = cost_obj + # Add input_tokens_details if present if "input_tokens_details" in usage_data: transformed["input_tokens_details"] = usage_data["input_tokens_details"] @@ -324,186 +338,63 @@ class PerplexityResponsesConfig(BaseResponsesAPIConfig): """ Transform a parsed streaming response chunk into a ResponsesAPIStreamingResponse """ - # Map Perplexity streaming chunk to OpenAI format - return ResponsesAPIStreamingResponse(**parsed_chunk) - - def transform_delete_response_api_request( - self, - response_id: str, - api_base: str, - litellm_params: GenericLiteLLMParams, - headers: dict, - ) -> Tuple[str, Dict]: - """Transform delete response API request""" - # Perplexity may not support deleting responses - # Return appropriate URL and params - url = f"{api_base}/v1/responses/{response_id}" - return url, {} - - def transform_delete_response_api_response( - self, - raw_response: httpx.Response, - logging_obj: LiteLLMLoggingObj, - ) -> DeleteResponseResult: - """Transform delete response API response""" - try: - response_json = raw_response.json() - return DeleteResponseResult( - id=response_json.get("id", ""), - object="response.deleted", - deleted=response_json.get("deleted", True), - ) - except Exception as e: - raise BaseLLMException( - status_code=raw_response.status_code, - message=f"Failed to parse delete response: {str(e)}", - ) - - def transform_get_response_api_request( - self, - response_id: str, - api_base: str, - litellm_params: GenericLiteLLMParams, - headers: dict, - ) -> Tuple[str, Dict]: - """Transform get response API request""" - url = f"{api_base}/v1/responses/{response_id}" - return url, {} - - def transform_get_response_api_response( - self, - raw_response: httpx.Response, - logging_obj: LiteLLMLoggingObj, - ) -> ResponsesAPIResponse: - """Transform get response API response""" - return self.transform_response_api_response( - model="", # Model will be in the response - raw_response=raw_response, - logging_obj=logging_obj, + # Get the event type from the chunk + verbose_logger.debug("Raw Perplexity Chunk=%s", parsed_chunk) + event_type = str(parsed_chunk.get("type")) + event_pydantic_model = PerplexityResponsesConfig.get_event_model_class( + event_type=event_type ) - - def transform_list_input_items_request( - self, - response_id: str, - api_base: str, - litellm_params: GenericLiteLLMParams, - headers: dict, - after: Optional[str] = None, - before: Optional[str] = None, - include: Optional[List[str]] = None, - limit: int = 20, - order: Literal["asc", "desc"] = "desc", - ) -> Tuple[str, Dict]: - """Transform list input items request""" - url = f"{api_base}/v1/responses/{response_id}/input_items" - params = { - "limit": limit, - "order": order, - } - if after: - params["after"] = after - if before: - params["before"] = before - if include: - params["include"] = include + # Transform Perplexity-specific fields to OpenAI format + parsed_chunk = self._transform_perplexity_chunk(parsed_chunk) - return url, params - - def transform_list_input_items_response( - self, - raw_response: httpx.Response, - logging_obj: LiteLLMLoggingObj, - ) -> Dict: - """Transform list input items response""" + # Defensive: Handle error.code being null (similar to OpenAI implementation) try: - return raw_response.json() + error_obj = parsed_chunk.get("error") + if isinstance(error_obj, dict) and error_obj.get("code") is None: + # Preserve other fields, but ensure `code` is a non-null string + parsed_chunk = dict(parsed_chunk) + parsed_chunk["error"] = dict(error_obj) + parsed_chunk["error"]["code"] = "unknown_error" + except Exception: + # If anything unexpected happens here, fall back to attempting + # instantiation and let higher-level handlers manage errors. + verbose_logger.debug("Failed to coalesce error.code in parsed_chunk") + + return event_pydantic_model(**parsed_chunk) + + def _transform_perplexity_chunk(self, chunk: dict) -> dict: + """ + Transform Perplexity-specific fields in a streaming chunk to OpenAI format. + + This handles: + - Converting Perplexity's cost object to a simple float + """ + # Make a copy to avoid modifying the original + chunk = dict(chunk) + + # Transform usage.cost from Perplexity format to OpenAI format + # Perplexity: {"currency": "USD", "input_cost": 0.0001, "output_cost": 0.0002, "total_cost": 0.0003} + # OpenAI: 0.0003 (just the total_cost as a float) + try: + response_obj = chunk.get("response") + if isinstance(response_obj, dict): + usage_obj = response_obj.get("usage") + if isinstance(usage_obj, dict): + cost_obj = usage_obj.get("cost") + if isinstance(cost_obj, dict) and "total_cost" in cost_obj: + # Replace the cost object with just the total_cost value + chunk = dict(chunk) + chunk["response"] = dict(response_obj) + chunk["response"]["usage"] = dict(usage_obj) + chunk["response"]["usage"]["cost"] = cost_obj["total_cost"] + verbose_logger.debug( + "Transformed Perplexity cost object to float: %s -> %s", + cost_obj, + cost_obj["total_cost"] + ) except Exception as e: - raise BaseLLMException( - status_code=raw_response.status_code, - message=f"Failed to parse list input items response: {str(e)}", - ) - - def get_error_class( - self, error_message: str, status_code: int, headers: Union[Dict, httpx.Headers] - ) -> BaseLLMException: - """Return appropriate error class based on status code""" - return BaseLLMException( - status_code=status_code, - message=error_message, - headers=headers, - ) - - def should_fake_stream( - self, - model: Optional[str], - stream: Optional[bool], - custom_llm_provider: Optional[str] = None, - ) -> bool: - """Returns True if litellm should fake a stream for the given model and stream value""" - return False - - ######################################################### - ########## CANCEL RESPONSE API TRANSFORMATION ########## - ######################################################### - def transform_cancel_response_api_request( - self, - response_id: str, - api_base: str, - litellm_params: GenericLiteLLMParams, - headers: dict, - ) -> Tuple[str, Dict]: - """Transform cancel response API request""" - # Perplexity may not support canceling responses - # Return appropriate URL and params - url = f"{api_base}/v1/responses/{response_id}/cancel" - return url, {} - - def transform_cancel_response_api_response( - self, - raw_response: httpx.Response, - logging_obj: LiteLLMLoggingObj, - ) -> ResponsesAPIResponse: - """Transform cancel response API response""" - return self.transform_response_api_response( - model="", # Model will be in the response - raw_response=raw_response, - logging_obj=logging_obj, - ) - - ######################################################### - ########## COMPACT RESPONSE API TRANSFORMATION ########## - ######################################################### - def transform_compact_response_api_request( - self, - model: str, - input: Union[str, ResponseInputParam], - response_api_optional_request_params: Dict, - api_base: str, - litellm_params: GenericLiteLLMParams, - headers: dict, - ) -> Tuple[str, Dict]: - """Transform compact response API request""" - # Perplexity may not support compact responses - # Return standard URL and transformed request - url = f"{api_base}/v1/responses" - request_data = self.transform_responses_api_request( - model=model, - input=input, - response_api_optional_request_params=response_api_optional_request_params, - litellm_params=litellm_params, - headers=headers, - ) - return url, request_data - - def transform_compact_response_api_response( - self, - raw_response: httpx.Response, - logging_obj: LiteLLMLoggingObj, - ) -> ResponsesAPIResponse: - """Transform compact response API response""" - return self.transform_response_api_response( - model="", # Model will be in the response - raw_response=raw_response, - logging_obj=logging_obj, - ) + # If transformation fails, log and continue with original chunk + verbose_logger.debug("Failed to transform Perplexity cost object: %s", e) + + return chunk From 9c1bf847298f8d836165cb6cfbf042a5dfbde167 Mon Sep 17 00:00:00 2001 From: Sameer Kankute Date: Tue, 10 Feb 2026 17:41:03 +0530 Subject: [PATCH 4/6] Fix mypy issues --- .../perplexity/responses/transformation.py | 31 ++++++++++++------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/litellm/llms/perplexity/responses/transformation.py b/litellm/llms/perplexity/responses/transformation.py index 27b78bac998..178e76ea970 100644 --- a/litellm/llms/perplexity/responses/transformation.py +++ b/litellm/llms/perplexity/responses/transformation.py @@ -20,6 +20,7 @@ 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 ( + ResponseAPIUsage, ResponseInputParam, ResponsesAPIOptionalRequestParams, ResponsesAPIResponse, @@ -39,7 +40,7 @@ class PerplexityResponsesConfig(OpenAIResponsesAPIConfig): @property def custom_llm_provider(self) -> LlmProviders: - return "perplexity" + return LlmProviders.PERPLEXITY def get_supported_openai_params(self, model: str) -> list: """ @@ -105,7 +106,7 @@ class PerplexityResponsesConfig(OpenAIResponsesAPIConfig): - Supports 'instructions' parameter for system-level guidance - Tools are specified differently (web_search, fetch_url) """ - mapped_params = {} + mapped_params: Dict[str, Any] = {} # Map standard parameters if response_api_optional_params.get("max_output_tokens"): @@ -123,18 +124,23 @@ class PerplexityResponsesConfig(OpenAIResponsesAPIConfig): if response_api_optional_params.get("stream_options"): mapped_params["stream_options"] = response_api_optional_params["stream_options"] - # Map Perplexity-specific parameters - if response_api_optional_params.get("preset"): - mapped_params["preset"] = response_api_optional_params["preset"] + # Map Perplexity-specific parameters (using .get() with Any dict access) + preset = response_api_optional_params.get("preset") # type: ignore + if preset: + mapped_params["preset"] = preset - if response_api_optional_params.get("instructions"): - mapped_params["instructions"] = response_api_optional_params["instructions"] + instructions = response_api_optional_params.get("instructions") # type: ignore + if instructions: + mapped_params["instructions"] = instructions if response_api_optional_params.get("reasoning"): mapped_params["reasoning"] = response_api_optional_params["reasoning"] - if response_api_optional_params.get("tools"): - mapped_params["tools"] = self._transform_tools(response_api_optional_params["tools"]) + tools = response_api_optional_params.get("tools") + if tools: + # Convert tools to list of dicts for transformation + tools_list = [dict(tool) if hasattr(tool, '__dict__') else tool for tool in tools] # type: ignore + mapped_params["tools"] = self._transform_tools(tools_list) # type: ignore return mapped_params @@ -260,7 +266,10 @@ class PerplexityResponsesConfig(OpenAIResponsesAPIConfig): # Transform usage to handle Perplexity's cost structure usage_data = raw_response_json.get("usage", {}) - transformed_usage = self._transform_usage(usage_data) + transformed_usage_dict = self._transform_usage(usage_data) + + # Convert usage dict to ResponseAPIUsage object + usage_obj = ResponseAPIUsage(**transformed_usage_dict) if transformed_usage_dict else None # Map Perplexity response to OpenAI Responses API format response = ResponsesAPIResponse( @@ -270,7 +279,7 @@ class PerplexityResponsesConfig(OpenAIResponsesAPIConfig): status=raw_response_json.get("status", "completed"), model=raw_response_json.get("model", model), output=raw_response_json.get("output", []), - usage=transformed_usage, + usage=usage_obj, ) return response From 2eb52db3e916e979f9466fad0fb3a55c199102df Mon Sep 17 00:00:00 2001 From: Sameer Kankute Date: Tue, 10 Feb 2026 17:44:00 +0530 Subject: [PATCH 5/6] Add documentation for perplexity --- docs/my-website/docs/providers/perplexity.md | 287 ++++++++++++++++++ ...odel_prices_and_context_window_backup.json | 60 ++++ model_prices_and_context_window.json | 30 +- 3 files changed, 357 insertions(+), 20 deletions(-) diff --git a/docs/my-website/docs/providers/perplexity.md b/docs/my-website/docs/providers/perplexity.md index 2fcb49c60fa..68adf9939c6 100644 --- a/docs/my-website/docs/providers/perplexity.md +++ b/docs/my-website/docs/providers/perplexity.md @@ -120,6 +120,293 @@ All models listed here https://docs.perplexity.ai/docs/model-cards are supported +## Agentic Research API (Responses API) + +Requires v1.72.6+ + + +### Using Presets + +Presets provide optimized defaults for specific use cases. Start with a preset for quick setup: + + + + +```python +from litellm import responses +import os + +os.environ['PERPLEXITY_API_KEY'] = "" + +# Using the pro-search preset +response = responses( + model="perplexity/preset/pro-search", + input="What are the latest developments in AI?", + custom_llm_provider="perplexity", +) + +print(response.output) +``` + + + + +1. Setup config.yaml + +```yaml +model_list: + - model_name: perplexity-pro-search + litellm_params: + model: perplexity/preset/pro-search + api_key: os.environ/PERPLEXITY_API_KEY +``` + +2. Start proxy + +```bash +litellm --config /path/to/config.yaml +``` + +3. Test it! + +```bash +curl http://0.0.0.0:4000/v1/responses \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer anything" \ + -d '{ + "model": "perplexity-pro-search", + "input": "What are the latest developments in AI?" + }' +``` + + + + +### Using Third-Party Models + +Access models from OpenAI, Anthropic, Google, xAI, and other providers through Perplexity's unified API: + + + + +```python +from litellm import responses +import os + +os.environ['PERPLEXITY_API_KEY'] = "" + +response = responses( + model="perplexity/openai/gpt-4o", + input="Explain quantum computing in simple terms", + custom_llm_provider="perplexity", + max_output_tokens=500, +) + +print(response.output) +``` + + + + +```python +from litellm import responses +import os + +os.environ['PERPLEXITY_API_KEY'] = "" + +response = responses( + model="perplexity/anthropic/claude-3-5-sonnet-20241022", + input="Write a short story about a robot learning to paint", + custom_llm_provider="perplexity", + max_output_tokens=500, +) + +print(response.output) +``` + + + + +```python +from litellm import responses +import os + +os.environ['PERPLEXITY_API_KEY'] = "" + +response = responses( + model="perplexity/google/gemini-2.0-flash-exp", + input="Explain the concept of neural networks", + custom_llm_provider="perplexity", + max_output_tokens=500, +) + +print(response.output) +``` + + + + +```python +from litellm import responses +import os + +os.environ['PERPLEXITY_API_KEY'] = "" + +response = responses( + model="perplexity/xai/grok-2-1212", + input="What makes a good AI assistant?", + custom_llm_provider="perplexity", + max_output_tokens=500, +) + +print(response.output) +``` + + + + +### Web Search Tool + +Enable web search capabilities to access real-time information: + +```python +from litellm import responses +import os + +os.environ['PERPLEXITY_API_KEY'] = "" + +response = responses( + model="perplexity/openai/gpt-4o", + input="What's the weather in San Francisco today?", + custom_llm_provider="perplexity", + tools=[{"type": "web_search"}], + instructions="You have access to a web_search tool. Use it for questions about current events.", +) + +print(response.output) +``` + + +### Reasoning Effort (Responses API) + +Control the reasoning effort level for reasoning-capable models: + +```python +from litellm import responses +import os + +os.environ['PERPLEXITY_API_KEY'] = "" + +response = responses( + model="perplexity/openai/gpt-5.2", + input="Solve this complex problem step by step", + custom_llm_provider="perplexity", + reasoning={"effort": "high"}, # Options: low, medium, high + max_output_tokens=1000, +) + +print(response.output) +``` + +### Multi-Turn Conversations + +Use message arrays for multi-turn conversations with context: + +```python +from litellm import responses +import os + +os.environ['PERPLEXITY_API_KEY'] = "" + +response = responses( + model="perplexity/anthropic/claude-3-5-sonnet-20241022", + input=[ + {"type": "message", "role": "system", "content": "You are a helpful assistant."}, + {"type": "message", "role": "user", "content": "What are the latest AI developments?"}, + ], + custom_llm_provider="perplexity", + instructions="Provide detailed, well-researched answers.", + max_output_tokens=800, +) + +print(response.output) +``` + +### Streaming Responses + +Stream responses for real-time output: + +```python +from litellm import responses +import os + +os.environ['PERPLEXITY_API_KEY'] = "" + +response = responses( + model="perplexity/openai/gpt-4o", + input="Tell me a story about space exploration", + custom_llm_provider="perplexity", + stream=True, + max_output_tokens=500, +) + +for chunk in response: + if hasattr(chunk, 'type'): + if chunk.type == "response.output_text.delta": + print(chunk.delta, end="", flush=True) +``` + +### Supported Third-Party Models + +| Provider | Model Name | Function Call | +|----------|------------|---------------| +| OpenAI | gpt-4o | `responses(model="perplexity/openai/gpt-4o", ...)` | +| OpenAI | gpt-4o-mini | `responses(model="perplexity/openai/gpt-4o-mini", ...)` | +| OpenAI | gpt-5.2 | `responses(model="perplexity/openai/gpt-5.2", ...)` | +| Anthropic | claude-3-5-sonnet-20241022 | `responses(model="perplexity/anthropic/claude-3-5-sonnet-20241022", ...)` | +| Anthropic | claude-3-5-haiku-20241022 | `responses(model="perplexity/anthropic/claude-3-5-haiku-20241022", ...)` | +| Google | gemini-2.0-flash-exp | `responses(model="perplexity/google/gemini-2.0-flash-exp", ...)` | +| Google | gemini-2.0-flash-thinking-exp | `responses(model="perplexity/google/gemini-2.0-flash-thinking-exp", ...)` | +| xAI | grok-2-1212 | `responses(model="perplexity/xai/grok-2-1212", ...)` | +| xAI | grok-2-vision-1212 | `responses(model="perplexity/xai/grok-2-vision-1212", ...)` | + +### Available Presets + +| Preset Name | Function Call | +|----------------|--------------------------------------------------------| +| fast-search | `responses(model="perplexity/preset/fast-search", ...)`| +| pro-search | `responses(model="perplexity/preset/pro-search", ...)` | +| deep-research | `responses(model="perplexity/preset/deep-research", ...)`| + +### Complete Example + +```python +from litellm import responses +import os + +os.environ['PERPLEXITY_API_KEY'] = "" + +# Comprehensive example with multiple features +response = responses( + model="perplexity/openai/gpt-4o", + input="Research the latest developments in quantum computing and provide sources", + custom_llm_provider="perplexity", + tools=[ + {"type": "web_search"}, + {"type": "fetch_url"} + ], + instructions="Use web_search to find relevant information and fetch_url to retrieve detailed content from sources. Provide citations for all claims.", + max_output_tokens=1000, + temperature=0.7, +) + +print(f"Response ID: {response.id}") +print(f"Model: {response.model}") +print(f"Status: {response.status}") +print(f"Output: {response.output}") +print(f"Usage: {response.usage}") +``` + :::info For more information about passing provider-specific parameters, [go here](../completion/provider_specific_params.md) diff --git a/litellm/model_prices_and_context_window_backup.json b/litellm/model_prices_and_context_window_backup.json index 815d29c7964..9b45dfd893f 100644 --- a/litellm/model_prices_and_context_window_backup.json +++ b/litellm/model_prices_and_context_window_backup.json @@ -25614,6 +25614,66 @@ "supports_function_calling": true, "supports_tool_choice": true }, + "perplexity/preset/pro-search": { + "litellm_provider": "perplexity", + "mode": "responses", + "supports_web_search": true, + "supports_preset": true + }, + "perplexity/openai/gpt-4o": { + "litellm_provider": "perplexity", + "mode": "responses", + "supports_web_search": true, + "supports_reasoning": false + }, + "perplexity/openai/gpt-4o-mini": { + "litellm_provider": "perplexity", + "mode": "responses", + "supports_web_search": true, + "supports_reasoning": false + }, + "perplexity/openai/gpt-5.2": { + "litellm_provider": "perplexity", + "mode": "responses", + "supports_web_search": true, + "supports_reasoning": true + }, + "perplexity/anthropic/claude-3-5-sonnet-20241022": { + "litellm_provider": "perplexity", + "mode": "responses", + "supports_web_search": true, + "supports_reasoning": false + }, + "perplexity/anthropic/claude-3-5-haiku-20241022": { + "litellm_provider": "perplexity", + "mode": "responses", + "supports_web_search": true, + "supports_reasoning": false + }, + "perplexity/google/gemini-2.0-flash-exp": { + "litellm_provider": "perplexity", + "mode": "responses", + "supports_web_search": true, + "supports_reasoning": false + }, + "perplexity/google/gemini-2.0-flash-thinking-exp": { + "litellm_provider": "perplexity", + "mode": "responses", + "supports_web_search": true, + "supports_reasoning": true + }, + "perplexity/xai/grok-2-1212": { + "litellm_provider": "perplexity", + "mode": "responses", + "supports_web_search": true, + "supports_reasoning": false + }, + "perplexity/xai/grok-2-vision-1212": { + "litellm_provider": "perplexity", + "mode": "responses", + "supports_web_search": true, + "supports_reasoning": false + }, "publicai/aisingapore/Qwen-SEA-LION-v4-32B-IT": { "input_cost_per_token": 0.0, "litellm_provider": "publicai", diff --git a/model_prices_and_context_window.json b/model_prices_and_context_window.json index 6076d290739..9b45dfd893f 100644 --- a/model_prices_and_context_window.json +++ b/model_prices_and_context_window.json @@ -25618,71 +25618,61 @@ "litellm_provider": "perplexity", "mode": "responses", "supports_web_search": true, - "supports_preset": true, - "preset_name": "pro-search" + "supports_preset": true }, "perplexity/openai/gpt-4o": { "litellm_provider": "perplexity", "mode": "responses", "supports_web_search": true, - "supports_reasoning": false, - "third_party_provider": "openai" + "supports_reasoning": false }, "perplexity/openai/gpt-4o-mini": { "litellm_provider": "perplexity", "mode": "responses", "supports_web_search": true, - "supports_reasoning": false, - "third_party_provider": "openai" + "supports_reasoning": false }, "perplexity/openai/gpt-5.2": { "litellm_provider": "perplexity", "mode": "responses", "supports_web_search": true, - "supports_reasoning": true, - "third_party_provider": "openai" + "supports_reasoning": true }, "perplexity/anthropic/claude-3-5-sonnet-20241022": { "litellm_provider": "perplexity", "mode": "responses", "supports_web_search": true, - "supports_reasoning": false, - "third_party_provider": "anthropic" + "supports_reasoning": false }, "perplexity/anthropic/claude-3-5-haiku-20241022": { "litellm_provider": "perplexity", "mode": "responses", "supports_web_search": true, - "supports_reasoning": false, - "third_party_provider": "anthropic" + "supports_reasoning": false }, "perplexity/google/gemini-2.0-flash-exp": { "litellm_provider": "perplexity", "mode": "responses", "supports_web_search": true, - "supports_reasoning": false, - "third_party_provider": "google" + "supports_reasoning": false }, "perplexity/google/gemini-2.0-flash-thinking-exp": { "litellm_provider": "perplexity", "mode": "responses", "supports_web_search": true, - "supports_reasoning": true, - "third_party_provider": "google" + "supports_reasoning": true }, "perplexity/xai/grok-2-1212": { "litellm_provider": "perplexity", "mode": "responses", "supports_web_search": true, - "supports_reasoning": false, - "third_party_provider": "xai" + "supports_reasoning": false }, "perplexity/xai/grok-2-vision-1212": { "litellm_provider": "perplexity", "mode": "responses", "supports_web_search": true, - "supports_reasoning": false, - "third_party_provider": "xai" + "supports_reasoning": false }, "publicai/aisingapore/Qwen-SEA-LION-v4-32B-IT": { "input_cost_per_token": 0.0, From 7d5141c28c3eed3f35cbf56449d636e864d5ea83 Mon Sep 17 00:00:00 2001 From: Sameer Kankute Date: Tue, 10 Feb 2026 17:58:15 +0530 Subject: [PATCH 6/6] Fix mypy issues --- litellm/__init__.py | 1 + litellm/_lazy_imports_registry.py | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/litellm/__init__.py b/litellm/__init__.py index 8174b9d2655..dacb928e8a7 100644 --- a/litellm/__init__.py +++ b/litellm/__init__.py @@ -1393,6 +1393,7 @@ if TYPE_CHECKING: from .llms.litellm_proxy.responses.transformation import LiteLLMProxyResponsesAPIConfig as LiteLLMProxyResponsesAPIConfig from .llms.volcengine.responses.transformation import VolcEngineResponsesAPIConfig as VolcEngineResponsesAPIConfig from .llms.manus.responses.transformation import ManusResponsesAPIConfig as ManusResponsesAPIConfig + from .llms.perplexity.responses.transformation import PerplexityResponsesConfig as PerplexityResponsesConfig from .llms.gemini.interactions.transformation import GoogleAIStudioInteractionsConfig as GoogleAIStudioInteractionsConfig from .llms.openai.chat.o_series_transformation import OpenAIOSeriesConfig as OpenAIOSeriesConfig, OpenAIOSeriesConfig as OpenAIO1Config from .llms.anthropic.skills.transformation import AnthropicSkillsConfig as AnthropicSkillsConfig diff --git a/litellm/_lazy_imports_registry.py b/litellm/_lazy_imports_registry.py index 791a129880a..051b957ed1d 100644 --- a/litellm/_lazy_imports_registry.py +++ b/litellm/_lazy_imports_registry.py @@ -226,6 +226,7 @@ LLM_CONFIG_NAMES = ( "XAIResponsesAPIConfig", "LiteLLMProxyResponsesAPIConfig", "VolcEngineResponsesAPIConfig", + "PerplexityResponsesConfig", "GoogleAIStudioInteractionsConfig", "OpenAIOSeriesConfig", "AnthropicSkillsConfig", @@ -902,6 +903,10 @@ _LLM_CONFIGS_IMPORT_MAP = { ".llms.manus.responses.transformation", "ManusResponsesAPIConfig", ), + "PerplexityResponsesConfig": ( + ".llms.perplexity.responses.transformation", + "PerplexityResponsesConfig", + ), "GoogleAIStudioInteractionsConfig": ( ".llms.gemini.interactions.transformation", "GoogleAIStudioInteractionsConfig",