1- """
2- Translate from OpenAI's `/v1/chat/completions` to Perplexity's `/v1/chat/completions`
3- """
1+ """Translate from OpenAI's `/v1/chat/completions` to Perplexity's `/v1/chat/completions`."""
42
5- from typing import Any , List , Optional , Tuple
3+ from __future__ import annotations
4+
5+ from typing import TYPE_CHECKING , Any , List , Optional , Tuple
66
7- import httpx
87import litellm
98from litellm ._logging import verbose_logger
10- from litellm .secret_managers .main import get_secret_str
11- from litellm .types .llms .openai import AllMessageValues
12- from litellm .types .utils import Usage , PromptTokensDetailsWrapper
13- from litellm .litellm_core_utils .litellm_logging import Logging as LiteLLMLoggingObj
149from litellm .llms .openai .chat .gpt_transformation import OpenAIGPTConfig
15- from litellm .types .utils import ModelResponse
16- from litellm .types .llms .openai import ChatCompletionAnnotation
17- from litellm .types .llms .openai import ChatCompletionAnnotationURLCitation
10+ from litellm .secret_managers .main import get_secret_str
11+ from litellm .types .utils import ModelResponse , PromptTokensDetailsWrapper , Usage
12+
13+ if TYPE_CHECKING :
14+ import httpx
15+
16+ from litellm .litellm_core_utils .litellm_logging import Logging as LiteLLMLoggingObj
17+ from litellm .types .llms .openai import (
18+ AllMessageValues ,
19+ ChatCompletionAnnotation ,
20+ ChatCompletionAnnotationURLCitation ,
21+ )
1822
1923
2024class PerplexityChatConfig (OpenAIGPTConfig ):
25+ """Configuration for Perplexity chat completions."""
26+
2127 @property
22- def custom_llm_provider (self ) -> Optional [str ]:
28+ def custom_llm_provider (self ) -> str | None :
29+ """Return the custom LLM provider name."""
2330 return "perplexity"
2431
2532 def _get_openai_compatible_provider_info (
@@ -33,6 +40,38 @@ def _get_openai_compatible_provider_info(
3340 )
3441 return api_base , dynamic_api_key
3542
43+ def validate_environment (
44+ self ,
45+ headers : dict ,
46+ model : str ,
47+ messages : list ,
48+ optional_params : dict ,
49+ litellm_params : dict ,
50+ api_key : Optional [str ] = None ,
51+ api_base : Optional [str ] = None ,
52+ ) -> dict :
53+ """Validate Perplexity environment and set headers."""
54+ # Get API key from environment if not provided
55+ if api_key is None :
56+ _ , api_key = self ._get_openai_compatible_provider_info (
57+ api_base = api_base , api_key = api_key
58+ )
59+
60+ # Validate API key is present
61+ if api_key is None :
62+ raise ValueError (
63+ "The api_key client option must be set either by passing api_key to the client or by setting the PERPLEXITY_API_KEY environment variable"
64+ )
65+
66+ # Set authorization header
67+ headers ["Authorization" ] = f"Bearer { api_key } "
68+
69+ # Ensure Content-Type is set to application/json
70+ if "content-type" not in headers and "Content-Type" not in headers :
71+ headers ["Content-Type" ] = "application/json"
72+
73+ return headers
74+
3675 def get_supported_openai_params (self , model : str ) -> list :
3776 """
3877 Perplexity supports a subset of OpenAI params
@@ -72,7 +111,8 @@ def get_supported_openai_params(self, model: str) -> list:
72111
73112 return base_openai_params
74113
75- def transform_response (
114+
115+ def transform_response ( # noqa: PLR0913
76116 self ,
77117 model : str ,
78118 raw_response : httpx .Response ,
@@ -82,10 +122,11 @@ def transform_response(
82122 messages : List [AllMessageValues ],
83123 optional_params : dict ,
84124 litellm_params : dict ,
85- encoding : Any ,
125+ encoding : Any ,
86126 api_key : Optional [str ] = None ,
87- json_mode : Optional [bool ] = None ,
127+ json_mode : Optional [bool ] = None ,
88128 ) -> ModelResponse :
129+ """Transform Perplexity response to standard format."""
89130 # Call the parent transform_response first to handle the standard transformation
90131 model_response = super ().transform_response (
91132 model = model ,
@@ -104,28 +145,29 @@ def transform_response(
104145 # Extract and enhance usage with Perplexity-specific fields
105146 try :
106147 raw_response_json = raw_response .json ()
148+ self .add_cost_to_usage (model_response , raw_response_json )
107149 self ._enhance_usage_with_perplexity_fields (
108- model_response , raw_response_json
150+ model_response , raw_response_json ,
109151 )
110152 self ._add_citations_as_annotations (model_response , raw_response_json )
111- except Exception as e :
153+ except ( ValueError , TypeError , KeyError ) as e :
112154 verbose_logger .debug (f"Error extracting Perplexity-specific usage fields: { e } " )
113155
114156 return model_response
115157
116- def _enhance_usage_with_perplexity_fields (
117- self , model_response : ModelResponse , raw_response_json : dict
158+ def _enhance_usage_with_perplexity_fields (
159+ self , model_response : ModelResponse , raw_response_json : dict ,
118160 ) -> None :
119- """
120- Extract citation tokens and search queries from Perplexity API response
121- and add them to the usage object using standard LiteLLM fields.
161+ """Extract citation tokens and search queries from Perplexity API response.
162+
163+ Add them to the usage object using standard LiteLLM fields.
122164 """
123165 if not hasattr (model_response , "usage" ) or model_response .usage is None :
124166 # Create a usage object if it doesn't exist (when usage was None)
125167 model_response .usage = Usage ( # type: ignore[attr-defined]
126168 prompt_tokens = 0 ,
127169 completion_tokens = 0 ,
128- total_tokens = 0
170+ total_tokens = 0 ,
129171 )
130172
131173 usage = model_response .usage # type: ignore[attr-defined]
@@ -146,7 +188,7 @@ def _enhance_usage_with_perplexity_fields(
146188 # Extract search queries count from usage or response metadata
147189 # Perplexity might include this in the usage object or as separate metadata
148190 perplexity_usage = raw_response_json .get ("usage" , {})
149-
191+
150192 # Try to extract search queries from usage field first, then root level
151193 num_search_queries = perplexity_usage .get ("num_search_queries" )
152194 if num_search_queries is None :
@@ -155,18 +197,18 @@ def _enhance_usage_with_perplexity_fields(
155197 num_search_queries = perplexity_usage .get ("search_queries" )
156198 if num_search_queries is None :
157199 num_search_queries = raw_response_json .get ("search_queries" )
158-
200+
159201 # Create or update prompt_tokens_details to include web search requests and citation tokens
160202 if citation_tokens > 0 or (
161203 num_search_queries is not None and num_search_queries > 0
162204 ):
163205 if usage .prompt_tokens_details is None :
164206 usage .prompt_tokens_details = PromptTokensDetailsWrapper ()
165-
207+
166208 # Store citation tokens count for cost calculation
167209 if citation_tokens > 0 :
168- setattr ( usage , " citation_tokens" , citation_tokens )
169-
210+ usage . citation_tokens = citation_tokens
211+
170212 # Store search queries count in the standard web_search_requests field
171213 if num_search_queries is not None and num_search_queries > 0 :
172214 usage .prompt_tokens_details .web_search_requests = num_search_queries
@@ -248,4 +290,35 @@ def _add_citations_as_annotations(
248290 if citations :
249291 setattr (model_response , "citations" , citations )
250292 if search_results :
251- setattr (model_response , "search_results" , search_results )
293+ setattr (model_response , "search_results" , search_results )
294+
295+ def add_cost_to_usage (self , model_response : ModelResponse , raw_response_json : dict ) -> None :
296+ """Add the cost to the usage object."""
297+ try :
298+ usage_data = raw_response_json .get ("usage" )
299+ if usage_data :
300+ # Try different possible cost field locations
301+ response_cost = None
302+
303+ # Check if cost is directly in usage (flat structure)
304+ if "total_cost" in usage_data :
305+ response_cost = usage_data ["total_cost" ]
306+ # Check if cost is nested (cost.total_cost structure)
307+ elif "cost" in usage_data and isinstance (usage_data ["cost" ], dict ):
308+ response_cost = usage_data ["cost" ].get ("total_cost" )
309+ # Check if cost is a simple value
310+ elif "cost" in usage_data :
311+ response_cost = usage_data ["cost" ]
312+
313+ if response_cost is not None :
314+ # Store cost in hidden params for the cost calculator to use
315+ if not hasattr (model_response , "_hidden_params" ):
316+ model_response ._hidden_params = {}
317+ if "additional_headers" not in model_response ._hidden_params :
318+ model_response ._hidden_params ["additional_headers" ] = {}
319+ model_response ._hidden_params ["additional_headers" ][
320+ "llm_provider-x-litellm-response-cost"
321+ ] = float (response_cost )
322+ except (ValueError , TypeError , KeyError ) as e :
323+ verbose_logger .debug (f"Error adding cost to usage: { e } " )
324+ # If we can't extract cost, continue without it - don't fail the response
0 commit comments