mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-07 08:26:10 +00:00
feat(perplexity): update Responses API integration to match Agent API
- Rename "Agentic Research API" to "Agent API" Expand - supported Responses API parameters - Fix function tool handling to pass custom function tools through unchanged instead of heuristically mapping them. -Update model registry with current Perplexity models and presets - Add Function Calling and Structured Outputs documentation sections. - Unit tests for transformation logic.
This commit is contained in:
parent
7ae157198b
commit
5899e909fd
5 changed files with 620 additions and 56 deletions
|
|
@ -120,7 +120,7 @@ All models listed here https://docs.perplexity.ai/docs/model-cards are supported
|
|||
|
||||
|
||||
|
||||
## Agentic Research API (Responses API)
|
||||
## Agent API (Responses API)
|
||||
|
||||
Requires v1.72.6+
|
||||
|
||||
|
|
@ -196,7 +196,7 @@ import os
|
|||
os.environ['PERPLEXITY_API_KEY'] = ""
|
||||
|
||||
response = responses(
|
||||
model="perplexity/openai/gpt-4o",
|
||||
model="perplexity/openai/gpt-5.2",
|
||||
input="Explain quantum computing in simple terms",
|
||||
custom_llm_provider="perplexity",
|
||||
max_output_tokens=500,
|
||||
|
|
@ -215,7 +215,7 @@ import os
|
|||
os.environ['PERPLEXITY_API_KEY'] = ""
|
||||
|
||||
response = responses(
|
||||
model="perplexity/anthropic/claude-3-5-sonnet-20241022",
|
||||
model="perplexity/anthropic/claude-sonnet-4-5",
|
||||
input="Write a short story about a robot learning to paint",
|
||||
custom_llm_provider="perplexity",
|
||||
max_output_tokens=500,
|
||||
|
|
@ -234,7 +234,7 @@ import os
|
|||
os.environ['PERPLEXITY_API_KEY'] = ""
|
||||
|
||||
response = responses(
|
||||
model="perplexity/google/gemini-2.0-flash-exp",
|
||||
model="perplexity/google/gemini-2.5-flash",
|
||||
input="Explain the concept of neural networks",
|
||||
custom_llm_provider="perplexity",
|
||||
max_output_tokens=500,
|
||||
|
|
@ -253,7 +253,7 @@ import os
|
|||
os.environ['PERPLEXITY_API_KEY'] = ""
|
||||
|
||||
response = responses(
|
||||
model="perplexity/xai/grok-2-1212",
|
||||
model="perplexity/xai/grok-4-1-fast-non-reasoning",
|
||||
input="What makes a good AI assistant?",
|
||||
custom_llm_provider="perplexity",
|
||||
max_output_tokens=500,
|
||||
|
|
@ -276,7 +276,7 @@ import os
|
|||
os.environ['PERPLEXITY_API_KEY'] = ""
|
||||
|
||||
response = responses(
|
||||
model="perplexity/openai/gpt-4o",
|
||||
model="perplexity/openai/gpt-5.2",
|
||||
input="What's the weather in San Francisco today?",
|
||||
custom_llm_provider="perplexity",
|
||||
tools=[{"type": "web_search"}],
|
||||
|
|
@ -286,6 +286,78 @@ response = responses(
|
|||
print(response.output)
|
||||
```
|
||||
|
||||
### Function Calling
|
||||
|
||||
The Agent API supports custom function tools. Pass function tools through unchanged:
|
||||
|
||||
```python
|
||||
from litellm import responses
|
||||
import os
|
||||
|
||||
os.environ['PERPLEXITY_API_KEY'] = ""
|
||||
|
||||
response = responses(
|
||||
model="perplexity/openai/gpt-5.2",
|
||||
input="What's the weather in San Francisco?",
|
||||
custom_llm_provider="perplexity",
|
||||
tools=[
|
||||
{"type": "web_search"},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "get_weather",
|
||||
"description": "Get the current weather for a location",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"location": {"type": "string"},
|
||||
"unit": {"type": "string", "enum": ["celsius", "fahrenheit"]},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
instructions="Use tools when appropriate.",
|
||||
)
|
||||
|
||||
print(response.output)
|
||||
```
|
||||
|
||||
### Structured Outputs
|
||||
|
||||
Request JSON schema structured outputs via the `text` parameter:
|
||||
|
||||
```python
|
||||
from litellm import responses
|
||||
import os
|
||||
|
||||
os.environ['PERPLEXITY_API_KEY'] = ""
|
||||
|
||||
response = responses(
|
||||
model="perplexity/preset/pro-search",
|
||||
input="Extract key facts about the Eiffel Tower",
|
||||
custom_llm_provider="perplexity",
|
||||
text={
|
||||
"format": {
|
||||
"type": "json_schema",
|
||||
"name": "facts",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"type": "string"},
|
||||
"height_meters": {"type": "number"},
|
||||
"year_built": {"type": "integer"},
|
||||
},
|
||||
"required": ["name", "height_meters", "year_built"],
|
||||
},
|
||||
"strict": True,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
print(response.output)
|
||||
```
|
||||
|
||||
|
||||
### Reasoning Effort (Responses API)
|
||||
|
||||
|
|
@ -319,7 +391,7 @@ import os
|
|||
os.environ['PERPLEXITY_API_KEY'] = ""
|
||||
|
||||
response = responses(
|
||||
model="perplexity/anthropic/claude-3-5-sonnet-20241022",
|
||||
model="perplexity/anthropic/claude-sonnet-4-5",
|
||||
input=[
|
||||
{"type": "message", "role": "system", "content": "You are a helpful assistant."},
|
||||
{"type": "message", "role": "user", "content": "What are the latest AI developments?"},
|
||||
|
|
@ -343,7 +415,7 @@ import os
|
|||
os.environ['PERPLEXITY_API_KEY'] = ""
|
||||
|
||||
response = responses(
|
||||
model="perplexity/openai/gpt-4o",
|
||||
model="perplexity/openai/gpt-5.2",
|
||||
input="Tell me a story about space exploration",
|
||||
custom_llm_provider="perplexity",
|
||||
stream=True,
|
||||
|
|
@ -360,23 +432,28 @@ for chunk in response:
|
|||
|
||||
| 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", ...)` |
|
||||
| OpenAI | gpt-5.1 | `responses(model="perplexity/openai/gpt-5.1", ...)` |
|
||||
| OpenAI | gpt-5-mini | `responses(model="perplexity/openai/gpt-5-mini", ...)` |
|
||||
| Anthropic | claude-opus-4-6 | `responses(model="perplexity/anthropic/claude-opus-4-6", ...)` |
|
||||
| Anthropic | claude-opus-4-5 | `responses(model="perplexity/anthropic/claude-opus-4-5", ...)` |
|
||||
| Anthropic | claude-sonnet-4-5 | `responses(model="perplexity/anthropic/claude-sonnet-4-5", ...)` |
|
||||
| Anthropic | claude-haiku-4-5 | `responses(model="perplexity/anthropic/claude-haiku-4-5", ...)` |
|
||||
| Google | gemini-3-pro-preview | `responses(model="perplexity/google/gemini-3-pro-preview", ...)` |
|
||||
| Google | gemini-3-flash-preview | `responses(model="perplexity/google/gemini-3-flash-preview", ...)` |
|
||||
| Google | gemini-2.5-pro | `responses(model="perplexity/google/gemini-2.5-pro", ...)` |
|
||||
| Google | gemini-2.5-flash | `responses(model="perplexity/google/gemini-2.5-flash", ...)` |
|
||||
| xAI | grok-4-1-fast-non-reasoning | `responses(model="perplexity/xai/grok-4-1-fast-non-reasoning", ...)` |
|
||||
| Perplexity | sonar | `responses(model="perplexity/perplexity/sonar", ...)` |
|
||||
|
||||
### 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", ...)`|
|
||||
| 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", ...)` |
|
||||
| advanced-deep-research | `responses(model="perplexity/preset/advanced-deep-research", ...)` |
|
||||
|
||||
### Complete Example
|
||||
|
||||
|
|
@ -388,7 +465,7 @@ os.environ['PERPLEXITY_API_KEY'] = ""
|
|||
|
||||
# Comprehensive example with multiple features
|
||||
response = responses(
|
||||
model="perplexity/openai/gpt-4o",
|
||||
model="perplexity/openai/gpt-5.2",
|
||||
input="Research the latest developments in quantum computing and provide sources",
|
||||
custom_llm_provider="perplexity",
|
||||
tools=[
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
"""
|
||||
Perplexity Agentic Research API (Responses API) module
|
||||
Perplexity Agent API (Responses API) module
|
||||
"""
|
||||
|
||||
from .transformation import PerplexityResponsesConfig
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
"""
|
||||
Transformation logic for Perplexity Agentic Research API (Responses API)
|
||||
Transformation logic for Perplexity Agent API (Responses API)
|
||||
|
||||
This module handles the translation between OpenAI's Responses API format
|
||||
and Perplexity's Responses API format, which supports:
|
||||
|
|
@ -32,10 +32,10 @@ from litellm.types.utils import LlmProviders
|
|||
|
||||
class PerplexityResponsesConfig(OpenAIResponsesAPIConfig):
|
||||
"""
|
||||
Configuration for Perplexity Agentic Research API (Responses API)
|
||||
Configuration for Perplexity Agent API (Responses API)
|
||||
|
||||
|
||||
Reference: https://docs.perplexity.ai/agentic-research/quickstart
|
||||
Reference: https://docs.perplexity.ai/docs/agent-api/overview
|
||||
"""
|
||||
|
||||
@property
|
||||
|
|
@ -47,6 +47,7 @@ class PerplexityResponsesConfig(OpenAIResponsesAPIConfig):
|
|||
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.
|
||||
"""
|
||||
return [
|
||||
"max_output_tokens",
|
||||
|
|
@ -58,6 +59,23 @@ class PerplexityResponsesConfig(OpenAIResponsesAPIConfig):
|
|||
"preset",
|
||||
"instructions",
|
||||
"models", # Model fallback support
|
||||
"tool_choice",
|
||||
"parallel_tool_calls",
|
||||
"max_tool_calls",
|
||||
"text",
|
||||
"previous_response_id",
|
||||
"store",
|
||||
"background",
|
||||
"truncation",
|
||||
"metadata",
|
||||
"safety_identifier",
|
||||
"user",
|
||||
"stream_options",
|
||||
"top_logprobs",
|
||||
"prompt_cache_key",
|
||||
"frequency_penalty",
|
||||
"presence_penalty",
|
||||
"service_tier",
|
||||
]
|
||||
|
||||
def validate_environment(
|
||||
|
|
@ -142,35 +160,75 @@ class PerplexityResponsesConfig(OpenAIResponsesAPIConfig):
|
|||
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"]
|
||||
if response_api_optional_params.get("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"]
|
||||
|
||||
# Storage and lifecycle
|
||||
if response_api_optional_params.get("store") is not None:
|
||||
mapped_params["store"] = response_api_optional_params["store"]
|
||||
if response_api_optional_params.get("background") is not None:
|
||||
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"]
|
||||
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"]
|
||||
if response_api_optional_params.get("frequency_penalty") is not None:
|
||||
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"]
|
||||
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
|
||||
Transform tools to Perplexity format.
|
||||
|
||||
Perplexity supports:
|
||||
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")
|
||||
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
|
||||
# Function tools: Perplexity supports them natively
|
||||
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"})
|
||||
perplexity_tools.append(tool)
|
||||
|
||||
return perplexity_tools
|
||||
|
||||
|
|
|
|||
|
|
@ -26642,65 +26642,124 @@
|
|||
"supports_function_calling": true,
|
||||
"supports_tool_choice": true
|
||||
},
|
||||
"perplexity/preset/fast-search": {
|
||||
"litellm_provider": "perplexity",
|
||||
"mode": "responses",
|
||||
"supports_web_search": true,
|
||||
"supports_preset": true,
|
||||
"supports_function_calling": true
|
||||
},
|
||||
"perplexity/preset/pro-search": {
|
||||
"litellm_provider": "perplexity",
|
||||
"mode": "responses",
|
||||
"supports_web_search": true,
|
||||
"supports_preset": true
|
||||
"supports_preset": true,
|
||||
"supports_function_calling": true
|
||||
},
|
||||
"perplexity/openai/gpt-4o": {
|
||||
"perplexity/preset/deep-research": {
|
||||
"litellm_provider": "perplexity",
|
||||
"mode": "responses",
|
||||
"supports_web_search": true,
|
||||
"supports_reasoning": false
|
||||
"supports_preset": true,
|
||||
"supports_function_calling": true
|
||||
},
|
||||
"perplexity/openai/gpt-4o-mini": {
|
||||
"perplexity/preset/advanced-deep-research": {
|
||||
"litellm_provider": "perplexity",
|
||||
"mode": "responses",
|
||||
"supports_web_search": true,
|
||||
"supports_reasoning": false
|
||||
"supports_preset": true,
|
||||
"supports_function_calling": true
|
||||
},
|
||||
"perplexity/openai/gpt-5.2": {
|
||||
"litellm_provider": "perplexity",
|
||||
"mode": "responses",
|
||||
"supports_web_search": true,
|
||||
"supports_reasoning": true
|
||||
"supports_reasoning": true,
|
||||
"supports_function_calling": true
|
||||
},
|
||||
"perplexity/anthropic/claude-3-5-sonnet-20241022": {
|
||||
"perplexity/openai/gpt-5.1": {
|
||||
"litellm_provider": "perplexity",
|
||||
"mode": "responses",
|
||||
"supports_web_search": true,
|
||||
"supports_reasoning": false
|
||||
"supports_reasoning": false,
|
||||
"supports_function_calling": true
|
||||
},
|
||||
"perplexity/anthropic/claude-3-5-haiku-20241022": {
|
||||
"perplexity/openai/gpt-5-mini": {
|
||||
"litellm_provider": "perplexity",
|
||||
"mode": "responses",
|
||||
"supports_web_search": true,
|
||||
"supports_reasoning": false
|
||||
"supports_reasoning": false,
|
||||
"supports_function_calling": true
|
||||
},
|
||||
"perplexity/google/gemini-2.0-flash-exp": {
|
||||
"perplexity/anthropic/claude-opus-4-6": {
|
||||
"litellm_provider": "perplexity",
|
||||
"mode": "responses",
|
||||
"supports_web_search": true,
|
||||
"supports_reasoning": false
|
||||
"supports_reasoning": false,
|
||||
"supports_function_calling": true
|
||||
},
|
||||
"perplexity/google/gemini-2.0-flash-thinking-exp": {
|
||||
"perplexity/anthropic/claude-opus-4-5": {
|
||||
"litellm_provider": "perplexity",
|
||||
"mode": "responses",
|
||||
"supports_web_search": true,
|
||||
"supports_reasoning": true
|
||||
"supports_reasoning": false,
|
||||
"supports_function_calling": true
|
||||
},
|
||||
"perplexity/xai/grok-2-1212": {
|
||||
"perplexity/anthropic/claude-sonnet-4-5": {
|
||||
"litellm_provider": "perplexity",
|
||||
"mode": "responses",
|
||||
"supports_web_search": true,
|
||||
"supports_reasoning": false
|
||||
"supports_reasoning": false,
|
||||
"supports_function_calling": true
|
||||
},
|
||||
"perplexity/xai/grok-2-vision-1212": {
|
||||
"perplexity/anthropic/claude-haiku-4-5": {
|
||||
"litellm_provider": "perplexity",
|
||||
"mode": "responses",
|
||||
"supports_web_search": true,
|
||||
"supports_reasoning": false
|
||||
"supports_reasoning": false,
|
||||
"supports_function_calling": true
|
||||
},
|
||||
"perplexity/google/gemini-3-pro-preview": {
|
||||
"litellm_provider": "perplexity",
|
||||
"mode": "responses",
|
||||
"supports_web_search": true,
|
||||
"supports_reasoning": false,
|
||||
"supports_function_calling": true
|
||||
},
|
||||
"perplexity/google/gemini-3-flash-preview": {
|
||||
"litellm_provider": "perplexity",
|
||||
"mode": "responses",
|
||||
"supports_web_search": true,
|
||||
"supports_reasoning": false,
|
||||
"supports_function_calling": true
|
||||
},
|
||||
"perplexity/google/gemini-2.5-pro": {
|
||||
"litellm_provider": "perplexity",
|
||||
"mode": "responses",
|
||||
"supports_web_search": true,
|
||||
"supports_reasoning": false,
|
||||
"supports_function_calling": true
|
||||
},
|
||||
"perplexity/google/gemini-2.5-flash": {
|
||||
"litellm_provider": "perplexity",
|
||||
"mode": "responses",
|
||||
"supports_web_search": true,
|
||||
"supports_reasoning": false,
|
||||
"supports_function_calling": true
|
||||
},
|
||||
"perplexity/xai/grok-4-1-fast-non-reasoning": {
|
||||
"litellm_provider": "perplexity",
|
||||
"mode": "responses",
|
||||
"supports_web_search": true,
|
||||
"supports_reasoning": false,
|
||||
"supports_function_calling": true
|
||||
},
|
||||
"perplexity/perplexity/sonar": {
|
||||
"litellm_provider": "perplexity",
|
||||
"mode": "responses",
|
||||
"supports_web_search": true,
|
||||
"supports_reasoning": false,
|
||||
"supports_function_calling": true
|
||||
},
|
||||
"publicai/aisingapore/Qwen-SEA-LION-v4-32B-IT": {
|
||||
"input_cost_per_token": 0.0,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,370 @@
|
|||
"""
|
||||
Tests for Perplexity Responses API transformation
|
||||
|
||||
Tests the PerplexityResponsesConfig class that handles Perplexity-specific
|
||||
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
|
||||
from litellm.utils import ProviderConfigManager
|
||||
|
||||
|
||||
class TestPerplexityResponsesTransformation:
|
||||
"""Test Perplexity Responses API configuration and transformations"""
|
||||
|
||||
def test_function_tool_passthrough(self):
|
||||
"""Function tools with name/description/parameters are preserved"""
|
||||
config = PerplexityResponsesConfig()
|
||||
|
||||
params = ResponsesAPIOptionalRequestParams(
|
||||
tools=[
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "get_weather",
|
||||
"description": "Get the current weather",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"location": {"type": "string"},
|
||||
"unit": {"type": "string", "enum": ["celsius", "fahrenheit"]},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
result = config.map_openai_params(
|
||||
response_api_optional_params=params,
|
||||
model="perplexity/openai/gpt-5.2",
|
||||
drop_params=False,
|
||||
)
|
||||
|
||||
assert "tools" in result
|
||||
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 "parameters" in result["tools"][0]["function"]
|
||||
|
||||
def test_web_search_tool_passthrough(self):
|
||||
"""web_search tools are passed through unchanged"""
|
||||
config = PerplexityResponsesConfig()
|
||||
|
||||
params = ResponsesAPIOptionalRequestParams(tools=[{"type": "web_search"}])
|
||||
|
||||
result = config.map_openai_params(
|
||||
response_api_optional_params=params,
|
||||
model="perplexity/openai/gpt-5.2",
|
||||
drop_params=False,
|
||||
)
|
||||
|
||||
assert "tools" in result
|
||||
assert len(result["tools"]) == 1
|
||||
assert result["tools"][0]["type"] == "web_search"
|
||||
|
||||
def test_fetch_url_tool_passthrough(self):
|
||||
"""fetch_url tools are passed through"""
|
||||
config = PerplexityResponsesConfig()
|
||||
|
||||
params = ResponsesAPIOptionalRequestParams(tools=[{"type": "fetch_url"}])
|
||||
|
||||
result = config.map_openai_params(
|
||||
response_api_optional_params=params,
|
||||
model="perplexity/openai/gpt-5.2",
|
||||
drop_params=False,
|
||||
)
|
||||
|
||||
assert "tools" in result
|
||||
assert len(result["tools"]) == 1
|
||||
assert result["tools"][0]["type"] == "fetch_url"
|
||||
|
||||
def test_mixed_tools_function_and_web_search(self):
|
||||
"""Mixed function and web_search tools are transformed correctly"""
|
||||
config = PerplexityResponsesConfig()
|
||||
|
||||
params = ResponsesAPIOptionalRequestParams(
|
||||
tools=[
|
||||
{"type": "web_search"},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "custom_tool",
|
||||
"description": "A custom tool",
|
||||
"parameters": {"type": "object"},
|
||||
},
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
result = config.map_openai_params(
|
||||
response_api_optional_params=params,
|
||||
model="perplexity/openai/gpt-5.2",
|
||||
drop_params=False,
|
||||
)
|
||||
|
||||
assert len(result["tools"]) == 2
|
||||
assert result["tools"][0]["type"] == "web_search"
|
||||
assert result["tools"][1]["type"] == "function"
|
||||
assert result["tools"][1]["function"]["name"] == "custom_tool"
|
||||
|
||||
def test_tool_choice_mapping(self):
|
||||
"""tool_choice passes through"""
|
||||
config = PerplexityResponsesConfig()
|
||||
|
||||
params = ResponsesAPIOptionalRequestParams(tool_choice="required", temperature=0.7)
|
||||
|
||||
result = config.map_openai_params(
|
||||
response_api_optional_params=params,
|
||||
model="perplexity/openai/gpt-5.2",
|
||||
drop_params=False,
|
||||
)
|
||||
|
||||
assert result.get("tool_choice") == "required"
|
||||
|
||||
def test_parallel_tool_calls(self):
|
||||
"""parallel_tool_calls passes through"""
|
||||
config = PerplexityResponsesConfig()
|
||||
|
||||
params = ResponsesAPIOptionalRequestParams(parallel_tool_calls=True, temperature=0.7)
|
||||
|
||||
result = config.map_openai_params(
|
||||
response_api_optional_params=params,
|
||||
model="perplexity/openai/gpt-5.2",
|
||||
drop_params=False,
|
||||
)
|
||||
|
||||
assert result.get("parallel_tool_calls") is True
|
||||
|
||||
def test_max_tool_calls_mapping(self):
|
||||
"""max_tool_calls passes through"""
|
||||
config = PerplexityResponsesConfig()
|
||||
|
||||
params = ResponsesAPIOptionalRequestParams(max_tool_calls=5, temperature=0.7)
|
||||
|
||||
result = config.map_openai_params(
|
||||
response_api_optional_params=params,
|
||||
model="perplexity/openai/gpt-5.2",
|
||||
drop_params=False,
|
||||
)
|
||||
|
||||
assert result.get("max_tool_calls") == 5
|
||||
|
||||
def test_text_passthrough(self):
|
||||
"""text param passes through as-is (Perplexity accepts Open Responses format directly)"""
|
||||
config = PerplexityResponsesConfig()
|
||||
|
||||
text_value = {
|
||||
"format": {
|
||||
"type": "json_schema",
|
||||
"name": "weather_response",
|
||||
"schema": {"type": "object", "properties": {"temp": {"type": "number"}}},
|
||||
"strict": True,
|
||||
}
|
||||
}
|
||||
|
||||
params = ResponsesAPIOptionalRequestParams(
|
||||
text=text_value,
|
||||
temperature=0.7,
|
||||
)
|
||||
|
||||
result = config.map_openai_params(
|
||||
response_api_optional_params=params,
|
||||
model="perplexity/openai/gpt-5.2",
|
||||
drop_params=False,
|
||||
)
|
||||
|
||||
assert "text" in result
|
||||
assert result["text"] == text_value
|
||||
assert "response_format" not in result
|
||||
|
||||
def test_previous_response_id(self):
|
||||
"""previous_response_id passes through"""
|
||||
config = PerplexityResponsesConfig()
|
||||
|
||||
params = ResponsesAPIOptionalRequestParams(
|
||||
previous_response_id="resp_abc123",
|
||||
temperature=0.7,
|
||||
)
|
||||
|
||||
result = config.map_openai_params(
|
||||
response_api_optional_params=params,
|
||||
model="perplexity/openai/gpt-5.2",
|
||||
drop_params=False,
|
||||
)
|
||||
|
||||
assert result.get("previous_response_id") == "resp_abc123"
|
||||
|
||||
def test_store_background_truncation(self):
|
||||
"""Lifecycle params pass through"""
|
||||
config = PerplexityResponsesConfig()
|
||||
|
||||
params = ResponsesAPIOptionalRequestParams(
|
||||
store=True,
|
||||
background=False,
|
||||
truncation="auto",
|
||||
temperature=0.7,
|
||||
)
|
||||
|
||||
result = config.map_openai_params(
|
||||
response_api_optional_params=params,
|
||||
model="perplexity/openai/gpt-5.2",
|
||||
drop_params=False,
|
||||
)
|
||||
|
||||
assert result.get("store") is True
|
||||
assert result.get("background") is False
|
||||
assert result.get("truncation") == "auto"
|
||||
|
||||
def test_metadata_safety_identifier_user(self):
|
||||
"""Metadata params pass through"""
|
||||
config = PerplexityResponsesConfig()
|
||||
|
||||
params = ResponsesAPIOptionalRequestParams(
|
||||
metadata={"request_id": "req_123"},
|
||||
safety_identifier="safety_123",
|
||||
user="user_456",
|
||||
temperature=0.7,
|
||||
)
|
||||
|
||||
result = config.map_openai_params(
|
||||
response_api_optional_params=params,
|
||||
model="perplexity/openai/gpt-5.2",
|
||||
drop_params=False,
|
||||
)
|
||||
|
||||
assert result.get("metadata") == {"request_id": "req_123"}
|
||||
assert result.get("safety_identifier") == "safety_123"
|
||||
assert result.get("user") == "user_456"
|
||||
|
||||
def test_all_supported_params_declared(self):
|
||||
"""get_supported_openai_params returns complete list"""
|
||||
config = PerplexityResponsesConfig()
|
||||
supported = config.get_supported_openai_params("perplexity/openai/gpt-5.2")
|
||||
|
||||
expected = [
|
||||
"max_output_tokens",
|
||||
"stream",
|
||||
"temperature",
|
||||
"top_p",
|
||||
"tools",
|
||||
"reasoning",
|
||||
"preset",
|
||||
"instructions",
|
||||
"models",
|
||||
"tool_choice",
|
||||
"parallel_tool_calls",
|
||||
"max_tool_calls",
|
||||
"text",
|
||||
"previous_response_id",
|
||||
"store",
|
||||
"background",
|
||||
"truncation",
|
||||
"metadata",
|
||||
"safety_identifier",
|
||||
"user",
|
||||
"stream_options",
|
||||
"top_logprobs",
|
||||
"prompt_cache_key",
|
||||
"frequency_penalty",
|
||||
"presence_penalty",
|
||||
"service_tier",
|
||||
]
|
||||
|
||||
for param in expected:
|
||||
assert param in supported, f"Missing supported param: {param}"
|
||||
|
||||
def test_cost_transformation(self):
|
||||
"""Perplexity cost dict to OpenAI float"""
|
||||
config = PerplexityResponsesConfig()
|
||||
|
||||
usage_data = {
|
||||
"input_tokens": 100,
|
||||
"output_tokens": 200,
|
||||
"total_tokens": 300,
|
||||
"cost": {
|
||||
"currency": "USD",
|
||||
"input_cost": 0.0001,
|
||||
"output_cost": 0.0002,
|
||||
"total_cost": 0.0003,
|
||||
},
|
||||
}
|
||||
|
||||
result = config._transform_usage(usage_data)
|
||||
|
||||
assert result["input_tokens"] == 100
|
||||
assert result["output_tokens"] == 200
|
||||
assert result["total_tokens"] == 300
|
||||
assert result["cost"] == 0.0003
|
||||
|
||||
def test_cost_transformation_float_passthrough(self):
|
||||
"""Cost already float passes through"""
|
||||
config = PerplexityResponsesConfig()
|
||||
|
||||
usage_data = {
|
||||
"input_tokens": 100,
|
||||
"output_tokens": 200,
|
||||
"total_tokens": 300,
|
||||
"cost": 0.0005,
|
||||
}
|
||||
|
||||
result = config._transform_usage(usage_data)
|
||||
|
||||
assert result["cost"] == 0.0005
|
||||
|
||||
def test_preset_handling(self):
|
||||
"""Preset model names work"""
|
||||
config = PerplexityResponsesConfig()
|
||||
|
||||
data = config.transform_responses_api_request(
|
||||
model="preset/pro-search",
|
||||
input="What is AI?",
|
||||
response_api_optional_request_params={"temperature": 0.7},
|
||||
litellm_params={},
|
||||
headers={},
|
||||
)
|
||||
|
||||
assert data["preset"] == "pro-search"
|
||||
assert data["input"] == "What is AI?"
|
||||
assert "temperature" in data
|
||||
|
||||
def test_get_complete_url(self):
|
||||
"""Correct endpoint URL"""
|
||||
config = PerplexityResponsesConfig()
|
||||
|
||||
url = config.get_complete_url(api_base=None, litellm_params={})
|
||||
assert url == "https://api.perplexity.ai/v1/responses"
|
||||
|
||||
custom_url = config.get_complete_url(
|
||||
api_base="https://custom.perplexity.ai",
|
||||
litellm_params={},
|
||||
)
|
||||
assert custom_url == "https://custom.perplexity.ai/v1/responses"
|
||||
|
||||
url_with_slash = config.get_complete_url(
|
||||
api_base="https://api.perplexity.ai/",
|
||||
litellm_params={},
|
||||
)
|
||||
assert url_with_slash == "https://api.perplexity.ai/v1/responses"
|
||||
|
||||
def test_perplexity_provider_config_registration(self):
|
||||
"""Test that Perplexity provider returns PerplexityResponsesConfig"""
|
||||
config = ProviderConfigManager.get_provider_responses_api_config(
|
||||
model="perplexity/openai/gpt-5.2",
|
||||
provider=LlmProviders.PERPLEXITY,
|
||||
)
|
||||
|
||||
assert config is not None
|
||||
assert isinstance(config, PerplexityResponsesConfig)
|
||||
assert config.custom_llm_provider == LlmProviders.PERPLEXITY
|
||||
Loading…
Add table
Reference in a new issue