mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-07 08:26:10 +00:00
format
This commit is contained in:
parent
003cdc43df
commit
35a83fe8f7
2 changed files with 111 additions and 75 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue