This commit is contained in:
Kesku 2026-02-19 03:29:03 +00:00 committed by Sameer Kankute
parent 003cdc43df
commit 35a83fe8f7
2 changed files with 111 additions and 75 deletions

View file

@ -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

View file

@ -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,
}
}