From 35a83fe8f7a39fe3ea23124f799374ea93244b81 Mon Sep 17 00:00:00 2001 From: Kesku Date: Thu, 19 Feb 2026 03:29:03 +0000 Subject: [PATCH] format --- .../perplexity/responses/transformation.py | 161 ++++++++++-------- ...est_perplexity_responses_transformation.py | 25 ++- 2 files changed, 111 insertions(+), 75 deletions(-) diff --git a/litellm/llms/perplexity/responses/transformation.py b/litellm/llms/perplexity/responses/transformation.py index 9e801c92f3b..4a6fb4d69e2 100644 --- a/litellm/llms/perplexity/responses/transformation.py +++ b/litellm/llms/perplexity/responses/transformation.py @@ -34,7 +34,7 @@ class PerplexityResponsesConfig(OpenAIResponsesAPIConfig): """ Configuration for Perplexity Agent API (Responses API) - + Reference: https://docs.perplexity.ai/docs/agent-api/overview """ @@ -45,7 +45,7 @@ class PerplexityResponsesConfig(OpenAIResponsesAPIConfig): 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 Params aligned with response-echo fields and Open Responses spec. """ @@ -83,16 +83,15 @@ class PerplexityResponsesConfig(OpenAIResponsesAPIConfig): ) -> 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") + 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( @@ -102,15 +101,17 @@ class PerplexityResponsesConfig(OpenAIResponsesAPIConfig): ) -> 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" - + 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( + def map_openai_params( # noqa: PLR0915 self, response_api_optional_params: ResponsesAPIOptionalRequestParams, model: str, @@ -118,65 +119,75 @@ class PerplexityResponsesConfig(OpenAIResponsesAPIConfig): ) -> 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: Dict[str, Any] = {} - + # Map standard parameters if response_api_optional_params.get("max_output_tokens"): - mapped_params["max_output_tokens"] = response_api_optional_params["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"] - + mapped_params["stream_options"] = response_api_optional_params[ + "stream_options" + ] + # 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 - + 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"] - + 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 + 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 - + # Tool control if response_api_optional_params.get("tool_choice"): mapped_params["tool_choice"] = response_api_optional_params["tool_choice"] if response_api_optional_params.get("parallel_tool_calls") is not None: - mapped_params["parallel_tool_calls"] = response_api_optional_params["parallel_tool_calls"] + mapped_params["parallel_tool_calls"] = response_api_optional_params[ + "parallel_tool_calls" + ] if response_api_optional_params.get("max_tool_calls"): - mapped_params["max_tool_calls"] = response_api_optional_params["max_tool_calls"] - + mapped_params["max_tool_calls"] = response_api_optional_params[ + "max_tool_calls" + ] + # Structured outputs text_param = response_api_optional_params.get("text") if text_param: mapped_params["text"] = text_param - + # Conversation continuity if response_api_optional_params.get("previous_response_id"): - mapped_params["previous_response_id"] = response_api_optional_params["previous_response_id"] - + mapped_params["previous_response_id"] = response_api_optional_params[ + "previous_response_id" + ] + # Storage and lifecycle if response_api_optional_params.get("store") is not None: mapped_params["store"] = response_api_optional_params["store"] @@ -184,52 +195,60 @@ class PerplexityResponsesConfig(OpenAIResponsesAPIConfig): mapped_params["background"] = response_api_optional_params["background"] if response_api_optional_params.get("truncation"): mapped_params["truncation"] = response_api_optional_params["truncation"] - + # Metadata if response_api_optional_params.get("metadata"): mapped_params["metadata"] = response_api_optional_params["metadata"] if response_api_optional_params.get("safety_identifier"): - mapped_params["safety_identifier"] = response_api_optional_params["safety_identifier"] + mapped_params["safety_identifier"] = response_api_optional_params[ + "safety_identifier" + ] if response_api_optional_params.get("user"): mapped_params["user"] = response_api_optional_params["user"] - + # Additional if response_api_optional_params.get("top_logprobs") is not None: mapped_params["top_logprobs"] = response_api_optional_params["top_logprobs"] if response_api_optional_params.get("prompt_cache_key"): - mapped_params["prompt_cache_key"] = response_api_optional_params["prompt_cache_key"] + mapped_params["prompt_cache_key"] = response_api_optional_params[ + "prompt_cache_key" + ] if response_api_optional_params.get("frequency_penalty") is not None: - mapped_params["frequency_penalty"] = response_api_optional_params["frequency_penalty"] + mapped_params["frequency_penalty"] = response_api_optional_params[ + "frequency_penalty" + ] if response_api_optional_params.get("presence_penalty") is not None: - mapped_params["presence_penalty"] = response_api_optional_params["presence_penalty"] + mapped_params["presence_penalty"] = response_api_optional_params[ + "presence_penalty" + ] if response_api_optional_params.get("service_tier"): mapped_params["service_tier"] = response_api_optional_params["service_tier"] - + return mapped_params - + def _transform_tools(self, tools: List[Dict[str, Any]]) -> List[Dict[str, Any]]: """ Transform tools to Perplexity format. - + Perplexity supports (per public OpenAPI spec): - web_search: Performs web searches - fetch_url: Fetches content from URLs - function: Function Calling """ 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) - + # Function tools: Perplexity supports them natively elif tool_type == "function": perplexity_tools.append(tool) - + return perplexity_tools def transform_responses_api_request( @@ -262,24 +281,26 @@ class PerplexityResponsesConfig(OpenAIResponsesAPIConfig): "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]]]: + 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 = [] @@ -292,7 +313,7 @@ class PerplexityResponsesConfig(OpenAIResponsesAPIConfig): } formatted_messages.append(formatted_message) return formatted_messages - + return str(input) def transform_response_api_response( @@ -325,10 +346,14 @@ class PerplexityResponsesConfig(OpenAIResponsesAPIConfig): # Transform usage to handle Perplexity's cost structure usage_data = raw_response_json.get("usage", {}) 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 - + usage_obj = ( + ResponseAPIUsage(**transformed_usage_dict) + if transformed_usage_dict + else None + ) + # Map Perplexity response to OpenAI Responses API format response = ResponsesAPIResponse( id=raw_response_json.get("id", ""), @@ -341,11 +366,11 @@ class PerplexityResponsesConfig(OpenAIResponsesAPIConfig): ) 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, @@ -358,7 +383,7 @@ class PerplexityResponsesConfig(OpenAIResponsesAPIConfig): "total_cost": 0.0003 } } - + OpenAI expects: { "input_tokens": 100, @@ -372,7 +397,7 @@ class PerplexityResponsesConfig(OpenAIResponsesAPIConfig): "output_tokens": usage_data.get("output_tokens", 0), "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: @@ -380,20 +405,20 @@ class PerplexityResponsesConfig(OpenAIResponsesAPIConfig): verbose_logger.debug( "Transformed Perplexity cost object to float: %s -> %s", cost_obj, - cost_obj["total_cost"] + 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"] - + # 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( @@ -411,10 +436,10 @@ class PerplexityResponsesConfig(OpenAIResponsesAPIConfig): event_pydantic_model = PerplexityResponsesConfig.get_event_model_class( event_type=event_type ) - + # Transform Perplexity-specific fields to OpenAI format parsed_chunk = self._transform_perplexity_chunk(parsed_chunk) - + # Defensive: Handle error.code being null (similar to OpenAI implementation) try: error_obj = parsed_chunk.get("error") @@ -433,13 +458,13 @@ class PerplexityResponsesConfig(OpenAIResponsesAPIConfig): 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) @@ -458,10 +483,10 @@ class PerplexityResponsesConfig(OpenAIResponsesAPIConfig): verbose_logger.debug( "Transformed Perplexity cost object to float: %s -> %s", cost_obj, - cost_obj["total_cost"] + cost_obj["total_cost"], ) except Exception as e: # If transformation fails, log and continue with original chunk verbose_logger.debug("Failed to transform Perplexity cost object: %s", e) - + return chunk 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 d2526cb7c65..cdd4ef913f9 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 @@ -6,13 +6,12 @@ transformations for the Agent API (Responses API). Source: litellm/llms/perplexity/responses/transformation.py """ + import os import sys sys.path.insert(0, os.path.abspath("../../../../..")) -import pytest - from litellm.llms.perplexity.responses.transformation import PerplexityResponsesConfig from litellm.types.llms.openai import ResponsesAPIOptionalRequestParams from litellm.types.utils import LlmProviders @@ -37,7 +36,10 @@ class TestPerplexityResponsesTransformation: "type": "object", "properties": { "location": {"type": "string"}, - "unit": {"type": "string", "enum": ["celsius", "fahrenheit"]}, + "unit": { + "type": "string", + "enum": ["celsius", "fahrenheit"], + }, }, }, }, @@ -55,7 +57,9 @@ class TestPerplexityResponsesTransformation: assert len(result["tools"]) == 1 assert result["tools"][0]["type"] == "function" assert result["tools"][0]["function"]["name"] == "get_weather" - assert result["tools"][0]["function"]["description"] == "Get the current weather" + assert ( + result["tools"][0]["function"]["description"] == "Get the current weather" + ) assert "parameters" in result["tools"][0]["function"] def test_web_search_tool_passthrough(self): @@ -123,7 +127,9 @@ class TestPerplexityResponsesTransformation: """tool_choice passes through""" config = PerplexityResponsesConfig() - params = ResponsesAPIOptionalRequestParams(tool_choice="required", temperature=0.7) + params = ResponsesAPIOptionalRequestParams( + tool_choice="required", temperature=0.7 + ) result = config.map_openai_params( response_api_optional_params=params, @@ -137,7 +143,9 @@ class TestPerplexityResponsesTransformation: """parallel_tool_calls passes through""" config = PerplexityResponsesConfig() - params = ResponsesAPIOptionalRequestParams(parallel_tool_calls=True, temperature=0.7) + params = ResponsesAPIOptionalRequestParams( + parallel_tool_calls=True, temperature=0.7 + ) result = config.map_openai_params( response_api_optional_params=params, @@ -169,7 +177,10 @@ class TestPerplexityResponsesTransformation: "format": { "type": "json_schema", "name": "weather_response", - "schema": {"type": "object", "properties": {"temp": {"type": "number"}}}, + "schema": { + "type": "object", + "properties": {"temp": {"type": "number"}}, + }, "strict": True, } }