From 003cdc43df1f43063e912a3a366662998f576848 Mon Sep 17 00:00:00 2001 From: Kesku Date: Thu, 19 Feb 2026 03:14:03 +0000 Subject: [PATCH] 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. --- docs/my-website/docs/providers/perplexity.md | 121 ++++-- litellm/llms/perplexity/responses/__init__.py | 2 +- .../perplexity/responses/transformation.py | 88 ++++- model_prices_and_context_window.json | 95 ++++- ...est_perplexity_responses_transformation.py | 370 ++++++++++++++++++ 5 files changed, 620 insertions(+), 56 deletions(-) create mode 100644 tests/test_litellm/llms/perplexity/responses/test_perplexity_responses_transformation.py diff --git a/docs/my-website/docs/providers/perplexity.md b/docs/my-website/docs/providers/perplexity.md index 68adf9939c6..e3991c63bff 100644 --- a/docs/my-website/docs/providers/perplexity.md +++ b/docs/my-website/docs/providers/perplexity.md @@ -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=[ diff --git a/litellm/llms/perplexity/responses/__init__.py b/litellm/llms/perplexity/responses/__init__.py index 9bdf810e839..3285a472113 100644 --- a/litellm/llms/perplexity/responses/__init__.py +++ b/litellm/llms/perplexity/responses/__init__.py @@ -1,5 +1,5 @@ """ -Perplexity Agentic Research API (Responses API) module +Perplexity Agent API (Responses API) module """ from .transformation import PerplexityResponsesConfig diff --git a/litellm/llms/perplexity/responses/transformation.py b/litellm/llms/perplexity/responses/transformation.py index 178e76ea970..9e801c92f3b 100644 --- a/litellm/llms/perplexity/responses/transformation.py +++ b/litellm/llms/perplexity/responses/transformation.py @@ -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 diff --git a/model_prices_and_context_window.json b/model_prices_and_context_window.json index 3909ce4c8b0..624714feb87 100644 --- a/model_prices_and_context_window.json +++ b/model_prices_and_context_window.json @@ -26555,65 +26555,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, 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 new file mode 100644 index 00000000000..d2526cb7c65 --- /dev/null +++ b/tests/test_litellm/llms/perplexity/responses/test_perplexity_responses_transformation.py @@ -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