first commit

This commit is contained in:
Beyhan Oğur
2026-04-26 21:52:23 +03:00
commit 880f412e2c
2662 changed files with 866266 additions and 0 deletions

View File

@@ -0,0 +1,337 @@
package perplexity
import (
schemas "github.com/maximhq/bifrost/core/schemas"
)
// ToPerplexityChatCompletionRequest converts a Bifrost request to Perplexity chat completion request
func ToPerplexityChatCompletionRequest(bifrostReq *schemas.BifrostChatRequest) *PerplexityChatRequest {
if bifrostReq == nil || bifrostReq.Input == nil {
return nil
}
messages := bifrostReq.Input
perplexityReq := &PerplexityChatRequest{
Model: bifrostReq.Model,
Messages: messages,
}
// Map parameters if they exist
if bifrostReq.Params != nil {
// Core parameters
perplexityReq.MaxTokens = bifrostReq.Params.MaxCompletionTokens
perplexityReq.Temperature = bifrostReq.Params.Temperature
perplexityReq.TopP = bifrostReq.Params.TopP
perplexityReq.PresencePenalty = bifrostReq.Params.PresencePenalty
perplexityReq.FrequencyPenalty = bifrostReq.Params.FrequencyPenalty
perplexityReq.ResponseFormat = bifrostReq.Params.ResponseFormat
// Tool calling parameters
perplexityReq.Tools = bifrostReq.Params.Tools
perplexityReq.ToolChoice = bifrostReq.Params.ToolChoice
perplexityReq.ParallelToolCalls = bifrostReq.Params.ParallelToolCalls
// Standard parameters
perplexityReq.Stop = bifrostReq.Params.Stop
perplexityReq.LogProbs = bifrostReq.Params.LogProbs
perplexityReq.TopLogProbs = bifrostReq.Params.TopLogProbs
// Handle reasoning effort mapping
if bifrostReq.Params.Reasoning != nil && bifrostReq.Params.Reasoning.Effort != nil {
effort := *bifrostReq.Params.Reasoning.Effort
switch effort {
case "minimal":
perplexityReq.ReasoningEffort = schemas.Ptr("low")
case "xhigh", "max":
perplexityReq.ReasoningEffort = schemas.Ptr("high")
default:
perplexityReq.ReasoningEffort = &effort
}
}
// Handle extra parameters for Perplexity-specific fields
if bifrostReq.Params.ExtraParams != nil {
perplexityReq.ExtraParams = bifrostReq.Params.ExtraParams
// Search-related parameters
if searchMode, ok := schemas.SafeExtractStringPointer(bifrostReq.Params.ExtraParams["search_mode"]); ok {
delete(perplexityReq.ExtraParams, "search_mode")
perplexityReq.SearchMode = searchMode
}
if languagePreference, ok := schemas.SafeExtractStringPointer(bifrostReq.Params.ExtraParams["language_preference"]); ok {
delete(perplexityReq.ExtraParams, "language_preference")
perplexityReq.LanguagePreference = languagePreference
}
if searchDomainFilter, ok := schemas.SafeExtractStringSlice(bifrostReq.Params.ExtraParams["search_domain_filter"]); ok {
delete(perplexityReq.ExtraParams, "search_domain_filter")
perplexityReq.SearchDomainFilter = searchDomainFilter
}
if returnImages, ok := schemas.SafeExtractBoolPointer(bifrostReq.Params.ExtraParams["return_images"]); ok {
delete(perplexityReq.ExtraParams, "return_images")
perplexityReq.ReturnImages = returnImages
}
if returnRelatedQuestions, ok := schemas.SafeExtractBoolPointer(bifrostReq.Params.ExtraParams["return_related_questions"]); ok {
delete(perplexityReq.ExtraParams, "return_related_questions")
perplexityReq.ReturnRelatedQuestions = returnRelatedQuestions
}
if searchRecencyFilter, ok := schemas.SafeExtractStringPointer(bifrostReq.Params.ExtraParams["search_recency_filter"]); ok {
delete(perplexityReq.ExtraParams, "search_recency_filter")
perplexityReq.SearchRecencyFilter = searchRecencyFilter
}
if searchAfterDateFilter, ok := schemas.SafeExtractStringPointer(bifrostReq.Params.ExtraParams["search_after_date_filter"]); ok {
delete(perplexityReq.ExtraParams, "search_after_date_filter")
perplexityReq.SearchAfterDateFilter = searchAfterDateFilter
}
if searchBeforeDateFilter, ok := schemas.SafeExtractStringPointer(bifrostReq.Params.ExtraParams["search_before_date_filter"]); ok {
delete(perplexityReq.ExtraParams, "search_before_date_filter")
perplexityReq.SearchBeforeDateFilter = searchBeforeDateFilter
}
if lastUpdatedAfterFilter, ok := schemas.SafeExtractStringPointer(bifrostReq.Params.ExtraParams["last_updated_after_filter"]); ok {
delete(perplexityReq.ExtraParams, "last_updated_after_filter")
perplexityReq.LastUpdatedAfterFilter = lastUpdatedAfterFilter
}
if lastUpdatedBeforeFilter, ok := schemas.SafeExtractStringPointer(bifrostReq.Params.ExtraParams["last_updated_before_filter"]); ok {
delete(perplexityReq.ExtraParams, "last_updated_before_filter")
perplexityReq.LastUpdatedBeforeFilter = lastUpdatedBeforeFilter
}
if topK, ok := schemas.SafeExtractIntPointer(bifrostReq.Params.ExtraParams["top_k"]); ok {
delete(perplexityReq.ExtraParams, "top_k")
perplexityReq.TopK = topK
}
if stream, ok := schemas.SafeExtractBoolPointer(bifrostReq.Params.ExtraParams["stream"]); ok {
delete(perplexityReq.ExtraParams, "stream")
perplexityReq.Stream = stream
}
if disableSearch, ok := schemas.SafeExtractBoolPointer(bifrostReq.Params.ExtraParams["disable_search"]); ok {
delete(perplexityReq.ExtraParams, "disable_search")
perplexityReq.DisableSearch = disableSearch
}
if enableSearchClassifier, ok := schemas.SafeExtractBoolPointer(bifrostReq.Params.ExtraParams["enable_search_classifier"]); ok {
delete(perplexityReq.ExtraParams, "enable_search_classifier")
perplexityReq.EnableSearchClassifier = enableSearchClassifier
}
// Perplexity-specific request fields
if numSearchResults, ok := schemas.SafeExtractIntPointer(bifrostReq.Params.ExtraParams["num_search_results"]); ok {
delete(perplexityReq.ExtraParams, "num_search_results")
perplexityReq.NumSearchResults = numSearchResults
}
if numImages, ok := schemas.SafeExtractIntPointer(bifrostReq.Params.ExtraParams["num_images"]); ok {
delete(perplexityReq.ExtraParams, "num_images")
perplexityReq.NumImages = numImages
}
if searchLanguageFilter, ok := schemas.SafeExtractStringSlice(bifrostReq.Params.ExtraParams["search_language_filter"]); ok {
delete(perplexityReq.ExtraParams, "search_language_filter")
perplexityReq.SearchLanguageFilter = searchLanguageFilter
}
if imageFormatFilter, ok := schemas.SafeExtractStringSlice(bifrostReq.Params.ExtraParams["image_format_filter"]); ok {
delete(perplexityReq.ExtraParams, "image_format_filter")
perplexityReq.ImageFormatFilter = imageFormatFilter
}
if imageDomainFilter, ok := schemas.SafeExtractStringSlice(bifrostReq.Params.ExtraParams["image_domain_filter"]); ok {
delete(perplexityReq.ExtraParams, "image_domain_filter")
perplexityReq.ImageDomainFilter = imageDomainFilter
}
if safeSearch, ok := schemas.SafeExtractBoolPointer(bifrostReq.Params.ExtraParams["safe_search"]); ok {
delete(perplexityReq.ExtraParams, "safe_search")
perplexityReq.SafeSearch = safeSearch
}
if streamMode, ok := schemas.SafeExtractStringPointer(bifrostReq.Params.ExtraParams["stream_mode"]); ok {
delete(perplexityReq.ExtraParams, "stream_mode")
perplexityReq.StreamMode = streamMode
}
// Handle web_search_options
if webSearchOptionsParam, ok := schemas.SafeExtractFromMap(bifrostReq.Params.ExtraParams, "web_search_options"); ok {
if webSearchOptionsSlice, ok := webSearchOptionsParam.([]interface{}); ok {
var webSearchOptions []WebSearchOption
updatedWebSearchOptionsSlice := make([]interface{}, 0, len(webSearchOptionsSlice))
for _, optionInterface := range webSearchOptionsSlice {
if optionMap, ok := optionInterface.(map[string]interface{}); ok {
option := WebSearchOption{}
if searchContextSize, ok := schemas.SafeExtractStringPointer(optionMap["search_context_size"]); ok {
delete(optionMap, "search_context_size")
option.SearchContextSize = searchContextSize
}
if imageResultsEnhancedRelevance, ok := schemas.SafeExtractBoolPointer(optionMap["image_results_enhanced_relevance"]); ok {
delete(optionMap, "image_results_enhanced_relevance")
option.ImageResultsEnhancedRelevance = imageResultsEnhancedRelevance
}
if searchType, ok := schemas.SafeExtractStringPointer(optionMap["search_type"]); ok {
delete(optionMap, "search_type")
option.SearchType = searchType
}
// Handle user_location
if userLocationParam, ok := schemas.SafeExtractFromMap(optionMap, "user_location"); ok {
if userLocationMap, ok := userLocationParam.(map[string]interface{}); ok {
userLocation := &WebSearchOptionUserLocation{}
if latitude, ok := schemas.SafeExtractFloat64Pointer(userLocationMap["latitude"]); ok {
delete(userLocationMap, "latitude")
userLocation.Latitude = latitude
}
if longitude, ok := schemas.SafeExtractFloat64Pointer(userLocationMap["longitude"]); ok {
delete(userLocationMap, "longitude")
userLocation.Longitude = longitude
}
if city, ok := schemas.SafeExtractStringPointer(userLocationMap["city"]); ok {
delete(userLocationMap, "city")
userLocation.City = city
}
if country, ok := schemas.SafeExtractStringPointer(userLocationMap["country"]); ok {
delete(userLocationMap, "country")
userLocation.Country = country
}
if region, ok := schemas.SafeExtractStringPointer(userLocationMap["region"]); ok {
delete(userLocationMap, "region")
userLocation.Region = region
}
if len(userLocationMap) == 0 {
delete(optionMap, "user_location")
} else {
optionMap["user_location"] = userLocationMap
}
option.UserLocation = userLocation
}
}
webSearchOptions = append(webSearchOptions, option)
// Persist remaining custom fields from optionMap back to ExtraParams
if len(optionMap) > 0 {
updatedWebSearchOptionsSlice = append(updatedWebSearchOptionsSlice, optionMap)
}
} else {
// Preserve non-map entries as-is
updatedWebSearchOptionsSlice = append(updatedWebSearchOptionsSlice, optionInterface)
}
}
perplexityReq.WebSearchOptions = webSearchOptions
// Put remaining custom fields back into ExtraParams
if len(updatedWebSearchOptionsSlice) > 0 {
perplexityReq.ExtraParams["web_search_options"] = updatedWebSearchOptionsSlice
} else {
delete(perplexityReq.ExtraParams, "web_search_options")
}
}
}
// Handle media_response
if mediaResponseParam, ok := schemas.SafeExtractFromMap(bifrostReq.Params.ExtraParams, "media_response"); ok {
if mediaResponseMap, ok := mediaResponseParam.(map[string]interface{}); ok {
mediaResponse := &MediaResponse{}
if overridesParam, ok := schemas.SafeExtractFromMap(mediaResponseMap, "overrides"); ok {
if overridesMap, ok := overridesParam.(map[string]interface{}); ok {
overrides := MediaResponseOverrides{}
if returnVideos, ok := schemas.SafeExtractBoolPointer(overridesMap["return_videos"]); ok {
delete(overridesMap, "return_videos")
overrides.ReturnVideos = returnVideos
}
if returnImages, ok := schemas.SafeExtractBoolPointer(overridesMap["return_images"]); ok {
delete(overridesMap, "return_images")
overrides.ReturnImages = returnImages
}
// Put remaining overridesMap fields back into mediaResponseMap at correct nested location
if len(overridesMap) > 0 {
mediaResponseMap["overrides"] = overridesMap
} else {
delete(mediaResponseMap, "overrides")
}
mediaResponse.Overrides = overrides
}
}
perplexityReq.ExtraParams["media_response"] = mediaResponseMap
perplexityReq.MediaResponse = mediaResponse
}
}
}
}
return perplexityReq
}
// ToBifrostChatResponse converts a Perplexity chat completion response to Bifrost format
func (response *PerplexityChatResponse) ToBifrostChatResponse(model string) *schemas.BifrostChatResponse {
if response == nil {
return nil
}
bifrostResponse := &schemas.BifrostChatResponse{
ID: response.ID,
Model: model,
Object: response.Object,
Created: response.Created,
ExtraFields: schemas.BifrostResponseExtraFields{
},
SearchResults: response.SearchResults,
Videos: response.Videos,
Citations: response.Citations,
}
// Map all response fields
if len(response.Choices) > 0 {
bifrostResponse.Choices = response.Choices
}
// Convert usage information with all available fields
if response.Usage != nil {
usage := &schemas.BifrostLLMUsage{
PromptTokens: response.Usage.PromptTokens,
CompletionTokens: response.Usage.CompletionTokens,
TotalTokens: response.Usage.TotalTokens,
}
// Map Perplexity-specific usage details to CompletionTokensDetails
completionDetails := &schemas.ChatCompletionTokensDetails{}
hasCompletionDetails := false
if response.Usage.CitationTokens != nil {
completionDetails.CitationTokens = response.Usage.CitationTokens
hasCompletionDetails = true
}
if response.Usage.NumSearchQueries != nil {
completionDetails.NumSearchQueries = response.Usage.NumSearchQueries
hasCompletionDetails = true
}
if response.Usage.ReasoningTokens != nil {
completionDetails.ReasoningTokens = *response.Usage.ReasoningTokens
hasCompletionDetails = true
}
if hasCompletionDetails {
usage.CompletionTokensDetails = completionDetails
}
if response.Usage.Cost != nil {
usage.Cost = response.Usage.Cost
}
bifrostResponse.Usage = usage
}
return bifrostResponse
}

View File

@@ -0,0 +1,445 @@
// Package providers implements various LLM providers and their utility functions.
// This file contains the Perplexity provider implementation.
package perplexity
import (
"context"
"fmt"
"net/http"
"strings"
"time"
"github.com/maximhq/bifrost/core/providers/openai"
providerUtils "github.com/maximhq/bifrost/core/providers/utils"
schemas "github.com/maximhq/bifrost/core/schemas"
"github.com/valyala/fasthttp"
)
// PerplexityProvider implements the Provider interface for Perplexity's API.
type PerplexityProvider struct {
logger schemas.Logger // Logger for provider operations
client *fasthttp.Client // HTTP client for unary API requests (ReadTimeout bounds overall response)
streamingClient *fasthttp.Client // HTTP client for streaming API requests (no ReadTimeout; idle governed by NewIdleTimeoutReader)
networkConfig schemas.NetworkConfig // Network configuration including extra headers
sendBackRawRequest bool // Whether to include raw request in BifrostResponse
sendBackRawResponse bool // Whether to include raw response in BifrostResponse
}
// NewPerplexityProvider creates a new Perplexity provider instance.
// It initializes the HTTP client with the provided configuration and sets up response pools.
// The client is configured with timeouts, concurrency limits, and optional proxy settings.
func NewPerplexityProvider(config *schemas.ProviderConfig, logger schemas.Logger) (*PerplexityProvider, error) {
config.CheckAndSetDefaults()
requestTimeout := time.Second * time.Duration(config.NetworkConfig.DefaultRequestTimeoutInSeconds)
client := &fasthttp.Client{
ReadTimeout: requestTimeout,
WriteTimeout: requestTimeout,
MaxConnsPerHost: config.NetworkConfig.MaxConnsPerHost,
MaxIdleConnDuration: 30 * time.Second,
MaxConnWaitTimeout: requestTimeout,
MaxConnDuration: time.Second * time.Duration(schemas.DefaultMaxConnDurationInSeconds),
ConnPoolStrategy: fasthttp.FIFO,
}
// Configure proxy and retry policy
client = providerUtils.ConfigureProxy(client, config.ProxyConfig, logger)
client = providerUtils.ConfigureDialer(client)
client = providerUtils.ConfigureTLS(client, config.NetworkConfig, logger)
streamingClient := providerUtils.BuildStreamingClient(client)
// Set default BaseURL if not provided
if config.NetworkConfig.BaseURL == "" {
config.NetworkConfig.BaseURL = "https://api.perplexity.ai"
}
config.NetworkConfig.BaseURL = strings.TrimRight(config.NetworkConfig.BaseURL, "/")
return &PerplexityProvider{
logger: logger,
client: client,
streamingClient: streamingClient,
networkConfig: config.NetworkConfig,
sendBackRawRequest: config.SendBackRawRequest,
sendBackRawResponse: config.SendBackRawResponse,
}, nil
}
// GetProviderKey returns the provider identifier for Perplexity.
func (provider *PerplexityProvider) GetProviderKey() schemas.ModelProvider {
return schemas.Perplexity
}
// completeRequest sends a request to Perplexity's API and handles the response.
// It constructs the API URL, sets up authentication, and processes the response.
// Returns the response body or an error if the request fails.
func (provider *PerplexityProvider) completeRequest(ctx *schemas.BifrostContext, jsonData []byte, url string, key string, model string) ([]byte, time.Duration, map[string]string, *schemas.BifrostError) {
// Create the request with the JSON body
req := fasthttp.AcquireRequest()
resp := fasthttp.AcquireResponse()
defer fasthttp.ReleaseRequest(req)
defer fasthttp.ReleaseResponse(resp)
// Set any extra headers from network config
providerUtils.SetExtraHeaders(ctx, req, provider.networkConfig.ExtraHeaders, nil)
req.SetRequestURI(url)
req.Header.SetMethod(http.MethodPost)
req.Header.SetContentType("application/json")
if key != "" {
req.Header.Set("Authorization", "Bearer "+key)
}
if !providerUtils.ApplyLargePayloadRequestBodyWithModelNormalization(ctx, req, schemas.Perplexity) {
req.SetBody(jsonData)
}
// Send the request
latency, bifrostErr, wait := providerUtils.MakeRequestWithContext(ctx, provider.client, req, resp)
defer wait()
if bifrostErr != nil {
return nil, latency, nil, bifrostErr
}
// Extract provider response headers before status check so error responses also forward them
providerResponseHeaders := providerUtils.ExtractProviderResponseHeaders(resp)
// Handle error response
if resp.StatusCode() != fasthttp.StatusOK {
provider.logger.Debug(fmt.Sprintf("error from %s provider: %s", provider.GetProviderKey(), string(resp.Body())))
return nil, latency, providerResponseHeaders, openai.ParseOpenAIError(resp)
}
body, err := providerUtils.CheckAndDecodeBody(resp)
if err != nil {
return nil, latency, providerResponseHeaders, providerUtils.NewBifrostOperationError(schemas.ErrProviderResponseDecode, err)
}
// Read the response body and copy it before releasing the response
// to avoid use-after-free since resp.Body() references fasthttp's internal buffer
bodyCopy := append([]byte(nil), body...)
return bodyCopy, latency, providerResponseHeaders, nil
}
// ListModels performs a list models request to Perplexity's API.
func (provider *PerplexityProvider) ListModels(ctx *schemas.BifrostContext, keys []schemas.Key, request *schemas.BifrostListModelsRequest) (*schemas.BifrostListModelsResponse, *schemas.BifrostError) {
return nil, providerUtils.NewUnsupportedOperationError(schemas.ListModelsRequest, provider.GetProviderKey())
}
// TextCompletion is not supported by the Perplexity provider.
func (provider *PerplexityProvider) TextCompletion(ctx *schemas.BifrostContext, key schemas.Key, request *schemas.BifrostTextCompletionRequest) (*schemas.BifrostTextCompletionResponse, *schemas.BifrostError) {
return nil, providerUtils.NewUnsupportedOperationError(schemas.TextCompletionRequest, provider.GetProviderKey())
}
// TextCompletionStream performs a streaming text completion request to Perplexity's API.
// It formats the request, sends it to Perplexity, and processes the response.
// Returns a channel of BifrostStreamChunk objects or an error if the request fails.
func (provider *PerplexityProvider) TextCompletionStream(ctx *schemas.BifrostContext, postHookRunner schemas.PostHookRunner, postHookSpanFinalizer func(context.Context), key schemas.Key, request *schemas.BifrostTextCompletionRequest) (chan *schemas.BifrostStreamChunk, *schemas.BifrostError) {
return nil, providerUtils.NewUnsupportedOperationError(schemas.TextCompletionStreamRequest, provider.GetProviderKey())
}
// ChatCompletion performs a chat completion request to the Perplexity API.
func (provider *PerplexityProvider) ChatCompletion(ctx *schemas.BifrostContext, key schemas.Key, request *schemas.BifrostChatRequest) (*schemas.BifrostChatResponse, *schemas.BifrostError) {
// Convert to Perplexity chat completion request
jsonBody, err := providerUtils.CheckContextAndGetRequestBody(
ctx,
request,
func() (providerUtils.RequestBodyWithExtraParams, error) {
return ToPerplexityChatCompletionRequest(request), nil
})
if err != nil {
return nil, err
}
responseBody, latency, providerResponseHeaders, err := provider.completeRequest(ctx, jsonBody, provider.networkConfig.BaseURL+providerUtils.GetPathFromContext(ctx, "/chat/completions"), key.Value.GetValue(), request.Model)
if err != nil {
return nil, providerUtils.EnrichError(ctx, err, jsonBody, nil, provider.sendBackRawRequest, provider.sendBackRawResponse)
}
var response PerplexityChatResponse
rawRequest, rawResponse, bifrostErr := providerUtils.HandleProviderResponse(responseBody, &response, jsonBody, providerUtils.ShouldSendBackRawRequest(ctx, provider.sendBackRawRequest), providerUtils.ShouldSendBackRawResponse(ctx, provider.sendBackRawResponse))
if bifrostErr != nil {
return nil, providerUtils.EnrichError(ctx, bifrostErr, jsonBody, responseBody, provider.sendBackRawRequest, provider.sendBackRawResponse)
}
bifrostResponse := response.ToBifrostChatResponse(request.Model)
// Set ExtraFields
bifrostResponse.ExtraFields.Latency = latency.Milliseconds()
bifrostResponse.ExtraFields.ProviderResponseHeaders = providerResponseHeaders
// Set raw request if enabled
if providerUtils.ShouldSendBackRawRequest(ctx, provider.sendBackRawRequest) {
bifrostResponse.ExtraFields.RawRequest = rawRequest
}
// Set raw response if enabled
if providerUtils.ShouldSendBackRawResponse(ctx, provider.sendBackRawResponse) {
bifrostResponse.ExtraFields.RawResponse = rawResponse
}
return bifrostResponse, nil
}
// ChatCompletionStream performs a streaming chat completion request to the Perplexity API.
// It supports real-time streaming of responses using Server-Sent Events (SSE).
// Uses Perplexity's OpenAI-compatible streaming format.
// Returns a channel containing BifrostStreamChunk objects representing the stream or an error if the request fails.
func (provider *PerplexityProvider) ChatCompletionStream(ctx *schemas.BifrostContext, postHookRunner schemas.PostHookRunner, postHookSpanFinalizer func(context.Context), key schemas.Key, request *schemas.BifrostChatRequest) (chan *schemas.BifrostStreamChunk, *schemas.BifrostError) {
var authHeader map[string]string
if key.Value.GetValue() != "" {
authHeader = map[string]string{"Authorization": "Bearer " + key.Value.GetValue()}
}
customRequestConverter := func(request *schemas.BifrostChatRequest) (providerUtils.RequestBodyWithExtraParams, error) {
reqBody := ToPerplexityChatCompletionRequest(request)
reqBody.Stream = schemas.Ptr(true)
return reqBody, nil
}
// Use shared OpenAI-compatible streaming logic
return openai.HandleOpenAIChatCompletionStreaming(
ctx,
provider.streamingClient,
provider.networkConfig.BaseURL+"/chat/completions",
request,
authHeader,
provider.networkConfig.ExtraHeaders,
providerUtils.ShouldSendBackRawRequest(ctx, provider.sendBackRawRequest),
providerUtils.ShouldSendBackRawResponse(ctx, provider.sendBackRawResponse),
schemas.Perplexity,
postHookRunner,
customRequestConverter,
nil,
nil,
nil,
nil,
provider.logger,
postHookSpanFinalizer,
)
}
// Responses performs a responses request to the Perplexity API.
func (provider *PerplexityProvider) Responses(ctx *schemas.BifrostContext, key schemas.Key, request *schemas.BifrostResponsesRequest) (*schemas.BifrostResponsesResponse, *schemas.BifrostError) {
chatResponse, err := provider.ChatCompletion(ctx, key, request.ToChatRequest())
if err != nil {
return nil, err
}
response := chatResponse.ToBifrostResponsesResponse()
return response, nil
}
// ResponsesStream performs a streaming responses request to the Perplexity API.
func (provider *PerplexityProvider) ResponsesStream(ctx *schemas.BifrostContext, postHookRunner schemas.PostHookRunner, postHookSpanFinalizer func(context.Context), key schemas.Key, request *schemas.BifrostResponsesRequest) (chan *schemas.BifrostStreamChunk, *schemas.BifrostError) {
ctx.SetValue(schemas.BifrostContextKeyIsResponsesToChatCompletionFallback, true)
return provider.ChatCompletionStream(
ctx,
postHookRunner,
postHookSpanFinalizer,
key,
request.ToChatRequest(),
)
}
// Embedding is not supported by the Perplexity provider.
func (provider *PerplexityProvider) Embedding(ctx *schemas.BifrostContext, key schemas.Key, request *schemas.BifrostEmbeddingRequest) (*schemas.BifrostEmbeddingResponse, *schemas.BifrostError) {
return nil, providerUtils.NewUnsupportedOperationError(schemas.EmbeddingRequest, provider.GetProviderKey())
}
// Speech is not supported by the Perplexity provider.
func (provider *PerplexityProvider) Speech(ctx *schemas.BifrostContext, key schemas.Key, request *schemas.BifrostSpeechRequest) (*schemas.BifrostSpeechResponse, *schemas.BifrostError) {
return nil, providerUtils.NewUnsupportedOperationError(schemas.SpeechRequest, provider.GetProviderKey())
}
// Rerank is not supported by the Perplexity provider.
func (provider *PerplexityProvider) Rerank(ctx *schemas.BifrostContext, key schemas.Key, request *schemas.BifrostRerankRequest) (*schemas.BifrostRerankResponse, *schemas.BifrostError) {
return nil, providerUtils.NewUnsupportedOperationError(schemas.RerankRequest, provider.GetProviderKey())
}
// OCR is not supported by the Perplexity provider.
func (provider *PerplexityProvider) OCR(ctx *schemas.BifrostContext, key schemas.Key, request *schemas.BifrostOCRRequest) (*schemas.BifrostOCRResponse, *schemas.BifrostError) {
return nil, providerUtils.NewUnsupportedOperationError(schemas.OCRRequest, provider.GetProviderKey())
}
// SpeechStream is not supported by the Perplexity provider.
func (provider *PerplexityProvider) SpeechStream(ctx *schemas.BifrostContext, postHookRunner schemas.PostHookRunner, postHookSpanFinalizer func(context.Context), key schemas.Key, request *schemas.BifrostSpeechRequest) (chan *schemas.BifrostStreamChunk, *schemas.BifrostError) {
return nil, providerUtils.NewUnsupportedOperationError(schemas.SpeechStreamRequest, provider.GetProviderKey())
}
// Transcription is not supported by the Perplexity provider.
func (provider *PerplexityProvider) Transcription(ctx *schemas.BifrostContext, key schemas.Key, request *schemas.BifrostTranscriptionRequest) (*schemas.BifrostTranscriptionResponse, *schemas.BifrostError) {
return nil, providerUtils.NewUnsupportedOperationError(schemas.TranscriptionRequest, provider.GetProviderKey())
}
// TranscriptionStream is not supported by the Perplexity provider.
func (provider *PerplexityProvider) TranscriptionStream(ctx *schemas.BifrostContext, postHookRunner schemas.PostHookRunner, postHookSpanFinalizer func(context.Context), key schemas.Key, request *schemas.BifrostTranscriptionRequest) (chan *schemas.BifrostStreamChunk, *schemas.BifrostError) {
return nil, providerUtils.NewUnsupportedOperationError(schemas.TranscriptionStreamRequest, provider.GetProviderKey())
}
// ImageGeneration is not supported by the Perplexity provider.
func (provider *PerplexityProvider) ImageGeneration(ctx *schemas.BifrostContext, key schemas.Key, request *schemas.BifrostImageGenerationRequest) (*schemas.BifrostImageGenerationResponse, *schemas.BifrostError) {
return nil, providerUtils.NewUnsupportedOperationError(schemas.ImageGenerationRequest, provider.GetProviderKey())
}
// ImageGenerationStream is not supported by the Perplexity provider.
func (provider *PerplexityProvider) ImageGenerationStream(ctx *schemas.BifrostContext, postHookRunner schemas.PostHookRunner, postHookSpanFinalizer func(context.Context), key schemas.Key, request *schemas.BifrostImageGenerationRequest) (chan *schemas.BifrostStreamChunk, *schemas.BifrostError) {
return nil, providerUtils.NewUnsupportedOperationError(schemas.ImageGenerationStreamRequest, provider.GetProviderKey())
}
// ImageEdit is not supported by the Perplexity provider.
func (provider *PerplexityProvider) ImageEdit(ctx *schemas.BifrostContext, key schemas.Key, request *schemas.BifrostImageEditRequest) (*schemas.BifrostImageGenerationResponse, *schemas.BifrostError) {
return nil, providerUtils.NewUnsupportedOperationError(schemas.ImageEditRequest, provider.GetProviderKey())
}
// ImageEditStream is not supported by the Perplexity provider.
func (provider *PerplexityProvider) ImageEditStream(ctx *schemas.BifrostContext, postHookRunner schemas.PostHookRunner, postHookSpanFinalizer func(context.Context), key schemas.Key, request *schemas.BifrostImageEditRequest) (chan *schemas.BifrostStreamChunk, *schemas.BifrostError) {
return nil, providerUtils.NewUnsupportedOperationError(schemas.ImageEditStreamRequest, provider.GetProviderKey())
}
// ImageVariation is not supported by the Perplexity provider.
func (provider *PerplexityProvider) ImageVariation(ctx *schemas.BifrostContext, key schemas.Key, request *schemas.BifrostImageVariationRequest) (*schemas.BifrostImageGenerationResponse, *schemas.BifrostError) {
return nil, providerUtils.NewUnsupportedOperationError(schemas.ImageVariationRequest, provider.GetProviderKey())
}
// VideoGeneration is not supported by the Perplexity provider.
func (provider *PerplexityProvider) VideoGeneration(_ *schemas.BifrostContext, _ schemas.Key, _ *schemas.BifrostVideoGenerationRequest) (*schemas.BifrostVideoGenerationResponse, *schemas.BifrostError) {
return nil, providerUtils.NewUnsupportedOperationError(schemas.VideoGenerationRequest, provider.GetProviderKey())
}
// VideoRetrieve is not supported by the Perplexity provider.
func (provider *PerplexityProvider) VideoRetrieve(_ *schemas.BifrostContext, _ schemas.Key, _ *schemas.BifrostVideoRetrieveRequest) (*schemas.BifrostVideoGenerationResponse, *schemas.BifrostError) {
return nil, providerUtils.NewUnsupportedOperationError(schemas.VideoRetrieveRequest, provider.GetProviderKey())
}
// VideoDownload is not supported by the Perplexity provider.
func (provider *PerplexityProvider) VideoDownload(_ *schemas.BifrostContext, _ schemas.Key, _ *schemas.BifrostVideoDownloadRequest) (*schemas.BifrostVideoDownloadResponse, *schemas.BifrostError) {
return nil, providerUtils.NewUnsupportedOperationError(schemas.VideoDownloadRequest, provider.GetProviderKey())
}
// VideoDelete is not supported by Perplexity provider.
func (provider *PerplexityProvider) VideoDelete(_ *schemas.BifrostContext, _ schemas.Key, _ *schemas.BifrostVideoDeleteRequest) (*schemas.BifrostVideoDeleteResponse, *schemas.BifrostError) {
return nil, providerUtils.NewUnsupportedOperationError(schemas.VideoDeleteRequest, provider.GetProviderKey())
}
// VideoList is not supported by Perplexity provider.
func (provider *PerplexityProvider) VideoList(_ *schemas.BifrostContext, _ schemas.Key, _ *schemas.BifrostVideoListRequest) (*schemas.BifrostVideoListResponse, *schemas.BifrostError) {
return nil, providerUtils.NewUnsupportedOperationError(schemas.VideoListRequest, provider.GetProviderKey())
}
// VideoRemix is not supported by Perplexity provider.
func (provider *PerplexityProvider) VideoRemix(_ *schemas.BifrostContext, _ schemas.Key, _ *schemas.BifrostVideoRemixRequest) (*schemas.BifrostVideoGenerationResponse, *schemas.BifrostError) {
return nil, providerUtils.NewUnsupportedOperationError(schemas.VideoRemixRequest, provider.GetProviderKey())
}
// BatchCreate is not supported by Perplexity provider.
func (provider *PerplexityProvider) BatchCreate(_ *schemas.BifrostContext, _ schemas.Key, _ *schemas.BifrostBatchCreateRequest) (*schemas.BifrostBatchCreateResponse, *schemas.BifrostError) {
return nil, providerUtils.NewUnsupportedOperationError(schemas.BatchCreateRequest, provider.GetProviderKey())
}
// BatchList is not supported by Perplexity provider.
func (provider *PerplexityProvider) BatchList(_ *schemas.BifrostContext, _ []schemas.Key, _ *schemas.BifrostBatchListRequest) (*schemas.BifrostBatchListResponse, *schemas.BifrostError) {
return nil, providerUtils.NewUnsupportedOperationError(schemas.BatchListRequest, provider.GetProviderKey())
}
// BatchRetrieve is not supported by Perplexity provider.
func (provider *PerplexityProvider) BatchRetrieve(_ *schemas.BifrostContext, _ []schemas.Key, _ *schemas.BifrostBatchRetrieveRequest) (*schemas.BifrostBatchRetrieveResponse, *schemas.BifrostError) {
return nil, providerUtils.NewUnsupportedOperationError(schemas.BatchRetrieveRequest, provider.GetProviderKey())
}
// BatchCancel is not supported by Perplexity provider.
func (provider *PerplexityProvider) BatchCancel(_ *schemas.BifrostContext, _ []schemas.Key, _ *schemas.BifrostBatchCancelRequest) (*schemas.BifrostBatchCancelResponse, *schemas.BifrostError) {
return nil, providerUtils.NewUnsupportedOperationError(schemas.BatchCancelRequest, provider.GetProviderKey())
}
// BatchDelete is not supported by Perplexity provider.
func (provider *PerplexityProvider) BatchDelete(_ *schemas.BifrostContext, _ []schemas.Key, _ *schemas.BifrostBatchDeleteRequest) (*schemas.BifrostBatchDeleteResponse, *schemas.BifrostError) {
return nil, providerUtils.NewUnsupportedOperationError(schemas.BatchDeleteRequest, provider.GetProviderKey())
}
// BatchResults is not supported by Perplexity provider.
func (provider *PerplexityProvider) BatchResults(_ *schemas.BifrostContext, _ []schemas.Key, _ *schemas.BifrostBatchResultsRequest) (*schemas.BifrostBatchResultsResponse, *schemas.BifrostError) {
return nil, providerUtils.NewUnsupportedOperationError(schemas.BatchResultsRequest, provider.GetProviderKey())
}
// FileUpload is not supported by Perplexity provider.
func (provider *PerplexityProvider) FileUpload(_ *schemas.BifrostContext, _ schemas.Key, _ *schemas.BifrostFileUploadRequest) (*schemas.BifrostFileUploadResponse, *schemas.BifrostError) {
return nil, providerUtils.NewUnsupportedOperationError(schemas.FileUploadRequest, provider.GetProviderKey())
}
// FileList is not supported by Perplexity provider.
func (provider *PerplexityProvider) FileList(_ *schemas.BifrostContext, _ []schemas.Key, _ *schemas.BifrostFileListRequest) (*schemas.BifrostFileListResponse, *schemas.BifrostError) {
return nil, providerUtils.NewUnsupportedOperationError(schemas.FileListRequest, provider.GetProviderKey())
}
// FileRetrieve is not supported by Perplexity provider.
func (provider *PerplexityProvider) FileRetrieve(_ *schemas.BifrostContext, _ []schemas.Key, _ *schemas.BifrostFileRetrieveRequest) (*schemas.BifrostFileRetrieveResponse, *schemas.BifrostError) {
return nil, providerUtils.NewUnsupportedOperationError(schemas.FileRetrieveRequest, provider.GetProviderKey())
}
// FileDelete is not supported by Perplexity provider.
func (provider *PerplexityProvider) FileDelete(_ *schemas.BifrostContext, _ []schemas.Key, _ *schemas.BifrostFileDeleteRequest) (*schemas.BifrostFileDeleteResponse, *schemas.BifrostError) {
return nil, providerUtils.NewUnsupportedOperationError(schemas.FileDeleteRequest, provider.GetProviderKey())
}
// FileContent is not supported by Perplexity provider.
func (provider *PerplexityProvider) FileContent(_ *schemas.BifrostContext, _ []schemas.Key, _ *schemas.BifrostFileContentRequest) (*schemas.BifrostFileContentResponse, *schemas.BifrostError) {
return nil, providerUtils.NewUnsupportedOperationError(schemas.FileContentRequest, provider.GetProviderKey())
}
// CountTokens is not supported by the Perplexity provider.
func (provider *PerplexityProvider) CountTokens(_ *schemas.BifrostContext, _ schemas.Key, _ *schemas.BifrostResponsesRequest) (*schemas.BifrostCountTokensResponse, *schemas.BifrostError) {
return nil, providerUtils.NewUnsupportedOperationError(schemas.CountTokensRequest, provider.GetProviderKey())
}
// ContainerCreate is not supported by the Perplexity provider.
func (provider *PerplexityProvider) ContainerCreate(_ *schemas.BifrostContext, _ schemas.Key, _ *schemas.BifrostContainerCreateRequest) (*schemas.BifrostContainerCreateResponse, *schemas.BifrostError) {
return nil, providerUtils.NewUnsupportedOperationError(schemas.ContainerCreateRequest, provider.GetProviderKey())
}
// ContainerList is not supported by the Perplexity provider.
func (provider *PerplexityProvider) ContainerList(_ *schemas.BifrostContext, _ []schemas.Key, _ *schemas.BifrostContainerListRequest) (*schemas.BifrostContainerListResponse, *schemas.BifrostError) {
return nil, providerUtils.NewUnsupportedOperationError(schemas.ContainerListRequest, provider.GetProviderKey())
}
// ContainerRetrieve is not supported by the Perplexity provider.
func (provider *PerplexityProvider) ContainerRetrieve(_ *schemas.BifrostContext, _ []schemas.Key, _ *schemas.BifrostContainerRetrieveRequest) (*schemas.BifrostContainerRetrieveResponse, *schemas.BifrostError) {
return nil, providerUtils.NewUnsupportedOperationError(schemas.ContainerRetrieveRequest, provider.GetProviderKey())
}
// ContainerDelete is not supported by the Perplexity provider.
func (provider *PerplexityProvider) ContainerDelete(_ *schemas.BifrostContext, _ []schemas.Key, _ *schemas.BifrostContainerDeleteRequest) (*schemas.BifrostContainerDeleteResponse, *schemas.BifrostError) {
return nil, providerUtils.NewUnsupportedOperationError(schemas.ContainerDeleteRequest, provider.GetProviderKey())
}
// ContainerFileCreate is not supported by the Perplexity provider.
func (provider *PerplexityProvider) ContainerFileCreate(_ *schemas.BifrostContext, _ schemas.Key, _ *schemas.BifrostContainerFileCreateRequest) (*schemas.BifrostContainerFileCreateResponse, *schemas.BifrostError) {
return nil, providerUtils.NewUnsupportedOperationError(schemas.ContainerFileCreateRequest, provider.GetProviderKey())
}
// ContainerFileList is not supported by the Perplexity provider.
func (provider *PerplexityProvider) ContainerFileList(_ *schemas.BifrostContext, _ []schemas.Key, _ *schemas.BifrostContainerFileListRequest) (*schemas.BifrostContainerFileListResponse, *schemas.BifrostError) {
return nil, providerUtils.NewUnsupportedOperationError(schemas.ContainerFileListRequest, provider.GetProviderKey())
}
// ContainerFileRetrieve is not supported by the Perplexity provider.
func (provider *PerplexityProvider) ContainerFileRetrieve(_ *schemas.BifrostContext, _ []schemas.Key, _ *schemas.BifrostContainerFileRetrieveRequest) (*schemas.BifrostContainerFileRetrieveResponse, *schemas.BifrostError) {
return nil, providerUtils.NewUnsupportedOperationError(schemas.ContainerFileRetrieveRequest, provider.GetProviderKey())
}
// ContainerFileContent is not supported by the Perplexity provider.
func (provider *PerplexityProvider) ContainerFileContent(_ *schemas.BifrostContext, _ []schemas.Key, _ *schemas.BifrostContainerFileContentRequest) (*schemas.BifrostContainerFileContentResponse, *schemas.BifrostError) {
return nil, providerUtils.NewUnsupportedOperationError(schemas.ContainerFileContentRequest, provider.GetProviderKey())
}
// ContainerFileDelete is not supported by the Perplexity provider.
func (provider *PerplexityProvider) ContainerFileDelete(_ *schemas.BifrostContext, _ []schemas.Key, _ *schemas.BifrostContainerFileDeleteRequest) (*schemas.BifrostContainerFileDeleteResponse, *schemas.BifrostError) {
return nil, providerUtils.NewUnsupportedOperationError(schemas.ContainerFileDeleteRequest, provider.GetProviderKey())
}
// Passthrough is not supported by the Perplexity provider.
func (provider *PerplexityProvider) Passthrough(_ *schemas.BifrostContext, _ schemas.Key, _ *schemas.BifrostPassthroughRequest) (*schemas.BifrostPassthroughResponse, *schemas.BifrostError) {
return nil, providerUtils.NewUnsupportedOperationError(schemas.PassthroughRequest, provider.GetProviderKey())
}
func (provider *PerplexityProvider) PassthroughStream(_ *schemas.BifrostContext, _ schemas.PostHookRunner, _ func(context.Context), _ schemas.Key, _ *schemas.BifrostPassthroughRequest) (chan *schemas.BifrostStreamChunk, *schemas.BifrostError) {
return nil, providerUtils.NewUnsupportedOperationError(schemas.PassthroughStreamRequest, provider.GetProviderKey())
}

View File

@@ -0,0 +1,603 @@
package perplexity_test
import (
"encoding/json"
"os"
"strings"
"testing"
"github.com/maximhq/bifrost/core/internal/llmtests"
"github.com/maximhq/bifrost/core/providers/perplexity"
"github.com/maximhq/bifrost/core/schemas"
)
func TestPerplexity(t *testing.T) {
t.Parallel()
if strings.TrimSpace(os.Getenv("PERPLEXITY_API_KEY")) == "" {
t.Skip("Skipping Perplexity tests because PERPLEXITY_API_KEY is not set")
}
client, ctx, cancel, err := llmtests.SetupTest()
if err != nil {
t.Fatalf("Error initializing test setup: %v", err)
}
defer cancel()
defer client.Shutdown()
testConfig := llmtests.ComprehensiveTestConfig{
Provider: schemas.Perplexity,
ChatModel: "sonar-pro",
TextModel: "", // Perplexity doesn't support text completion
EmbeddingModel: "", // Perplexity doesn't support embedding
Scenarios: llmtests.TestScenarios{
TextCompletion: false, // Not supported
SimpleChat: true,
CompletionStream: true,
MultiTurnConversation: true,
ToolCalls: false,
MultipleToolCalls: false,
End2EndToolCalling: false,
AutomaticFunctionCall: false,
ImageURL: false, // Not supported yet
ImageBase64: false, // Not supported yet
MultipleImages: false, // Not supported yet
CompleteEnd2End: false,
FileBase64: false,
FileURL: false,
Embedding: false, // Not supported yet
ListModels: false,
PassThroughExtraParams: true,
},
}
t.Run("PerplexityTests", func(t *testing.T) {
llmtests.RunAllComprehensiveTests(t, client, ctx, testConfig)
})
}
func TestToBifrostChatResponse_Citations(t *testing.T) {
t.Parallel()
t.Run("citations are mapped to bifrost response", func(t *testing.T) {
response := &perplexity.PerplexityChatResponse{
ID: "test-id",
Model: "sonar-pro",
Object: "chat.completion",
Created: 1234567890,
Citations: []string{
"https://example.com/article1",
"https://example.com/article2",
"https://example.com/article3",
},
SearchResults: []schemas.SearchResult{
{Title: "Article 1", URL: "https://example.com/article1"},
},
Videos: []schemas.VideoResult{
{URL: "https://example.com/video1"},
},
Choices: []schemas.BifrostResponseChoice{
{
Index: 0,
FinishReason: schemas.Ptr("stop"),
ChatNonStreamResponseChoice: &schemas.ChatNonStreamResponseChoice{
Message: &schemas.ChatMessage{
Role: "assistant",
Content: &schemas.ChatMessageContent{ContentStr: schemas.Ptr("Here is the answer with citations [1][2][3].")},
},
},
},
},
Usage: &perplexity.Usage{
PromptTokens: 10,
CompletionTokens: 20,
TotalTokens: 30,
CitationTokens: schemas.Ptr(5),
NumSearchQueries: schemas.Ptr(2),
},
}
bifrostResp := response.ToBifrostChatResponse("sonar-pro")
if bifrostResp == nil {
t.Fatal("expected non-nil bifrost response")
}
// Verify citations are preserved
if len(bifrostResp.Citations) != 3 {
t.Fatalf("expected 3 citations, got %d", len(bifrostResp.Citations))
}
if bifrostResp.Citations[0] != "https://example.com/article1" {
t.Errorf("expected first citation to be 'https://example.com/article1', got '%s'", bifrostResp.Citations[0])
}
if bifrostResp.Citations[1] != "https://example.com/article2" {
t.Errorf("expected second citation to be 'https://example.com/article2', got '%s'", bifrostResp.Citations[1])
}
if bifrostResp.Citations[2] != "https://example.com/article3" {
t.Errorf("expected third citation to be 'https://example.com/article3', got '%s'", bifrostResp.Citations[2])
}
// Verify search results are preserved
if len(bifrostResp.SearchResults) != 1 {
t.Fatalf("expected 1 search result, got %d", len(bifrostResp.SearchResults))
}
// Verify videos are preserved
if len(bifrostResp.Videos) != 1 {
t.Fatalf("expected 1 video, got %d", len(bifrostResp.Videos))
}
// Verify usage citation tokens are mapped
if bifrostResp.Usage == nil || bifrostResp.Usage.CompletionTokensDetails == nil {
t.Fatal("expected usage with completion token details")
}
if bifrostResp.Usage.CompletionTokensDetails.CitationTokens == nil || *bifrostResp.Usage.CompletionTokensDetails.CitationTokens != 5 {
t.Error("expected citation_tokens to be 5")
}
if bifrostResp.Usage.CompletionTokensDetails.NumSearchQueries == nil || *bifrostResp.Usage.CompletionTokensDetails.NumSearchQueries != 2 {
t.Error("expected num_search_queries to be 2")
}
})
t.Run("nil citations remain nil", func(t *testing.T) {
response := &perplexity.PerplexityChatResponse{
ID: "test-id-2",
Model: "sonar-pro",
Object: "chat.completion",
Created: 1234567890,
Choices: []schemas.BifrostResponseChoice{},
}
bifrostResp := response.ToBifrostChatResponse("sonar-pro")
if bifrostResp == nil {
t.Fatal("expected non-nil bifrost response")
}
if bifrostResp.Citations != nil {
t.Errorf("expected nil citations, got %v", bifrostResp.Citations)
}
if bifrostResp.SearchResults != nil {
t.Errorf("expected nil search results, got %v", bifrostResp.SearchResults)
}
})
t.Run("empty citations slice is preserved", func(t *testing.T) {
response := &perplexity.PerplexityChatResponse{
ID: "test-id-3",
Model: "sonar-pro",
Object: "chat.completion",
Created: 1234567890,
Citations: []string{},
Choices: []schemas.BifrostResponseChoice{},
}
bifrostResp := response.ToBifrostChatResponse("sonar-pro")
if bifrostResp == nil {
t.Fatal("expected non-nil bifrost response")
}
if bifrostResp.Citations == nil {
t.Error("expected non-nil (empty) citations slice")
}
if len(bifrostResp.Citations) != 0 {
t.Errorf("expected 0 citations, got %d", len(bifrostResp.Citations))
}
})
}
func TestWebSearchOption_JSONSerialization(t *testing.T) {
t.Parallel()
t.Run("corrected field name serializes as image_results_enhanced_relevance", func(t *testing.T) {
option := perplexity.WebSearchOption{
SearchContextSize: schemas.Ptr("high"),
ImageResultsEnhancedRelevance: schemas.Ptr(true),
SearchType: schemas.Ptr("news"),
}
data, err := json.Marshal(option)
if err != nil {
t.Fatalf("failed to marshal WebSearchOption: %v", err)
}
jsonStr := string(data)
// Verify corrected field name
if !strings.Contains(jsonStr, `"image_results_enhanced_relevance"`) {
t.Errorf("expected JSON to contain 'image_results_enhanced_relevance', got: %s", jsonStr)
}
// Verify old incorrect field name is NOT present
if strings.Contains(jsonStr, `"image_search_relevance_enhanced"`) {
t.Errorf("JSON should not contain old field name 'image_search_relevance_enhanced', got: %s", jsonStr)
}
// Verify search_type is present
if !strings.Contains(jsonStr, `"search_type"`) {
t.Errorf("expected JSON to contain 'search_type', got: %s", jsonStr)
}
})
t.Run("deserialization with corrected field name", func(t *testing.T) {
jsonStr := `{"search_context_size":"medium","image_results_enhanced_relevance":true,"search_type":"academic"}`
var option perplexity.WebSearchOption
if err := json.Unmarshal([]byte(jsonStr), &option); err != nil {
t.Fatalf("failed to unmarshal WebSearchOption: %v", err)
}
if option.SearchContextSize == nil || *option.SearchContextSize != "medium" {
t.Error("expected search_context_size to be 'medium'")
}
if option.ImageResultsEnhancedRelevance == nil || !*option.ImageResultsEnhancedRelevance {
t.Error("expected image_results_enhanced_relevance to be true")
}
if option.SearchType == nil || *option.SearchType != "academic" {
t.Error("expected search_type to be 'academic'")
}
})
t.Run("search_type extraction from ExtraParams", func(t *testing.T) {
bifrostReq := &schemas.BifrostChatRequest{
Model: "sonar-pro",
Input: []schemas.ChatMessage{
{Role: "user", Content: &schemas.ChatMessageContent{ContentStr: schemas.Ptr("test")}},
},
Params: &schemas.ChatParameters{
ExtraParams: map[string]interface{}{
"web_search_options": []interface{}{
map[string]interface{}{
"search_context_size": "high",
"image_results_enhanced_relevance": true,
"search_type": "news",
},
},
},
},
}
result := perplexity.ToPerplexityChatCompletionRequest(bifrostReq)
if result == nil {
t.Fatal("expected non-nil result")
}
if len(result.WebSearchOptions) != 1 {
t.Fatalf("expected 1 web search option, got %d", len(result.WebSearchOptions))
}
opt := result.WebSearchOptions[0]
if opt.SearchContextSize == nil || *opt.SearchContextSize != "high" {
t.Error("expected search_context_size to be 'high'")
}
if opt.ImageResultsEnhancedRelevance == nil || !*opt.ImageResultsEnhancedRelevance {
t.Error("expected image_results_enhanced_relevance to be true")
}
if opt.SearchType == nil || *opt.SearchType != "news" {
t.Error("expected search_type to be 'news'")
}
})
}
func TestBifrostCost_DeserializationWithPerplexityFields(t *testing.T) {
t.Parallel()
t.Run("deserializes new cost breakdown fields", func(t *testing.T) {
jsonStr := `{
"input_tokens_cost": 0.001,
"output_tokens_cost": 0.002,
"reasoning_tokens_cost": 0.003,
"citation_tokens_cost": 0.004,
"search_queries_cost": 0.005,
"request_cost": 0.006,
"total_cost": 0.021
}`
var cost schemas.BifrostCost
if err := cost.UnmarshalJSON([]byte(jsonStr)); err != nil {
t.Fatalf("failed to unmarshal BifrostCost: %v", err)
}
if cost.InputTokensCost != 0.001 {
t.Errorf("expected input_tokens_cost 0.001, got %f", cost.InputTokensCost)
}
if cost.OutputTokensCost != 0.002 {
t.Errorf("expected output_tokens_cost 0.002, got %f", cost.OutputTokensCost)
}
if cost.ReasoningTokensCost != 0.003 {
t.Errorf("expected reasoning_tokens_cost 0.003, got %f", cost.ReasoningTokensCost)
}
if cost.CitationTokensCost != 0.004 {
t.Errorf("expected citation_tokens_cost 0.004, got %f", cost.CitationTokensCost)
}
if cost.SearchQueriesCost != 0.005 {
t.Errorf("expected search_queries_cost 0.005, got %f", cost.SearchQueriesCost)
}
if cost.RequestCost != 0.006 {
t.Errorf("expected request_cost 0.006, got %f", cost.RequestCost)
}
if cost.TotalCost != 0.021 {
t.Errorf("expected total_cost 0.021, got %f", cost.TotalCost)
}
})
t.Run("still works with float-only cost", func(t *testing.T) {
var cost schemas.BifrostCost
if err := cost.UnmarshalJSON([]byte(`0.42`)); err != nil {
t.Fatalf("failed to unmarshal float cost: %v", err)
}
if cost.TotalCost != 0.42 {
t.Errorf("expected total_cost 0.42, got %f", cost.TotalCost)
}
})
t.Run("perplexity usage cost round-trip", func(t *testing.T) {
jsonStr := `{
"prompt_tokens": 100,
"completion_tokens": 50,
"total_tokens": 150,
"citation_tokens": 10,
"cost": {
"input_tokens_cost": 0.01,
"output_tokens_cost": 0.02,
"citation_tokens_cost": 0.005,
"search_queries_cost": 0.003,
"total_cost": 0.038
}
}`
var usage perplexity.Usage
if err := json.Unmarshal([]byte(jsonStr), &usage); err != nil {
t.Fatalf("failed to unmarshal Usage: %v", err)
}
if usage.Cost == nil {
t.Fatal("expected non-nil cost")
}
if usage.Cost.CitationTokensCost != 0.005 {
t.Errorf("expected citation_tokens_cost 0.005, got %f", usage.Cost.CitationTokensCost)
}
if usage.Cost.SearchQueriesCost != 0.003 {
t.Errorf("expected search_queries_cost 0.003, got %f", usage.Cost.SearchQueriesCost)
}
})
}
func TestToPerplexityChatCompletionRequest_ToolCalling(t *testing.T) {
t.Parallel()
t.Run("tools and tool_choice are mapped", func(t *testing.T) {
bifrostReq := &schemas.BifrostChatRequest{
Model: "sonar-pro",
Input: []schemas.ChatMessage{
{Role: "user", Content: &schemas.ChatMessageContent{ContentStr: schemas.Ptr("What's the weather?")}},
},
Params: &schemas.ChatParameters{
Tools: []schemas.ChatTool{
{
Type: schemas.ChatToolTypeFunction,
Function: &schemas.ChatToolFunction{
Name: "get_weather",
Description: schemas.Ptr("Get the weather for a location"),
},
},
},
ToolChoice: &schemas.ChatToolChoice{
ChatToolChoiceStr: schemas.Ptr("auto"),
},
ParallelToolCalls: schemas.Ptr(true),
},
}
result := perplexity.ToPerplexityChatCompletionRequest(bifrostReq)
if result == nil {
t.Fatal("expected non-nil result")
}
// Verify tools
if len(result.Tools) != 1 {
t.Fatalf("expected 1 tool, got %d", len(result.Tools))
}
if result.Tools[0].Type != schemas.ChatToolTypeFunction {
t.Errorf("expected tool type 'function', got '%s'", result.Tools[0].Type)
}
if result.Tools[0].Function == nil || result.Tools[0].Function.Name != "get_weather" {
t.Error("expected tool function name 'get_weather'")
}
// Verify tool_choice
if result.ToolChoice == nil {
t.Fatal("expected non-nil tool_choice")
}
if result.ToolChoice.ChatToolChoiceStr == nil || *result.ToolChoice.ChatToolChoiceStr != "auto" {
t.Error("expected tool_choice to be 'auto'")
}
// Verify parallel_tool_calls
if result.ParallelToolCalls == nil || !*result.ParallelToolCalls {
t.Error("expected parallel_tool_calls to be true")
}
})
t.Run("nil tools remain nil", func(t *testing.T) {
bifrostReq := &schemas.BifrostChatRequest{
Model: "sonar-pro",
Input: []schemas.ChatMessage{
{Role: "user", Content: &schemas.ChatMessageContent{ContentStr: schemas.Ptr("hello")}},
},
Params: &schemas.ChatParameters{},
}
result := perplexity.ToPerplexityChatCompletionRequest(bifrostReq)
if result == nil {
t.Fatal("expected non-nil result")
}
if result.Tools != nil {
t.Errorf("expected nil tools, got %v", result.Tools)
}
if result.ToolChoice != nil {
t.Error("expected nil tool_choice")
}
if result.ParallelToolCalls != nil {
t.Error("expected nil parallel_tool_calls")
}
})
}
func TestToPerplexityChatCompletionRequest_NewFields(t *testing.T) {
t.Parallel()
t.Run("stop, logprobs, top_logprobs are mapped", func(t *testing.T) {
bifrostReq := &schemas.BifrostChatRequest{
Model: "sonar-pro",
Input: []schemas.ChatMessage{
{Role: "user", Content: &schemas.ChatMessageContent{ContentStr: schemas.Ptr("test")}},
},
Params: &schemas.ChatParameters{
Stop: []string{"END", "STOP"},
LogProbs: schemas.Ptr(true),
TopLogProbs: schemas.Ptr(5),
},
}
result := perplexity.ToPerplexityChatCompletionRequest(bifrostReq)
if result == nil {
t.Fatal("expected non-nil result")
}
// Verify stop
if len(result.Stop) != 2 {
t.Fatalf("expected 2 stop sequences, got %d", len(result.Stop))
}
if result.Stop[0] != "END" || result.Stop[1] != "STOP" {
t.Errorf("expected stop sequences ['END', 'STOP'], got %v", result.Stop)
}
// Verify logprobs
if result.LogProbs == nil || !*result.LogProbs {
t.Error("expected logprobs to be true")
}
// Verify top_logprobs
if result.TopLogProbs == nil || *result.TopLogProbs != 5 {
t.Error("expected top_logprobs to be 5")
}
})
t.Run("perplexity-specific fields from ExtraParams", func(t *testing.T) {
bifrostReq := &schemas.BifrostChatRequest{
Model: "sonar-pro",
Input: []schemas.ChatMessage{
{Role: "user", Content: &schemas.ChatMessageContent{ContentStr: schemas.Ptr("test")}},
},
Params: &schemas.ChatParameters{
ExtraParams: map[string]interface{}{
"num_search_results": 5,
"num_images": 3,
"search_language_filter": []interface{}{"en", "fr"},
"image_format_filter": []interface{}{"png", "jpg"},
"image_domain_filter": []interface{}{"example.com"},
"safe_search": true,
"stream_mode": "partial",
},
},
}
result := perplexity.ToPerplexityChatCompletionRequest(bifrostReq)
if result == nil {
t.Fatal("expected non-nil result")
}
// Verify num_search_results
if result.NumSearchResults == nil || *result.NumSearchResults != 5 {
t.Errorf("expected num_search_results to be 5, got %v", result.NumSearchResults)
}
// Verify num_images
if result.NumImages == nil || *result.NumImages != 3 {
t.Errorf("expected num_images to be 3, got %v", result.NumImages)
}
// Verify search_language_filter
if len(result.SearchLanguageFilter) != 2 {
t.Fatalf("expected 2 search language filters, got %d", len(result.SearchLanguageFilter))
}
if result.SearchLanguageFilter[0] != "en" || result.SearchLanguageFilter[1] != "fr" {
t.Errorf("expected search_language_filter ['en', 'fr'], got %v", result.SearchLanguageFilter)
}
// Verify image_format_filter
if len(result.ImageFormatFilter) != 2 {
t.Fatalf("expected 2 image format filters, got %d", len(result.ImageFormatFilter))
}
if result.ImageFormatFilter[0] != "png" || result.ImageFormatFilter[1] != "jpg" {
t.Errorf("expected image_format_filter ['png', 'jpg'], got %v", result.ImageFormatFilter)
}
// Verify image_domain_filter
if len(result.ImageDomainFilter) != 1 {
t.Fatalf("expected 1 image domain filter, got %d", len(result.ImageDomainFilter))
}
if result.ImageDomainFilter[0] != "example.com" {
t.Errorf("expected image_domain_filter ['example.com'], got %v", result.ImageDomainFilter)
}
// Verify safe_search
if result.SafeSearch == nil || !*result.SafeSearch {
t.Error("expected safe_search to be true")
}
// Verify stream_mode
if result.StreamMode == nil || *result.StreamMode != "partial" {
t.Errorf("expected stream_mode to be 'partial', got %v", result.StreamMode)
}
// Verify fields are removed from ExtraParams
for _, key := range []string{"num_search_results", "num_images", "search_language_filter", "image_format_filter", "image_domain_filter", "safe_search", "stream_mode"} {
if _, exists := result.ExtraParams[key]; exists {
t.Errorf("expected '%s' to be removed from ExtraParams", key)
}
}
})
t.Run("new fields serialize correctly in JSON", func(t *testing.T) {
req := perplexity.PerplexityChatRequest{
Model: "sonar-pro",
Messages: []schemas.ChatMessage{
{Role: "user", Content: &schemas.ChatMessageContent{ContentStr: schemas.Ptr("test")}},
},
NumSearchResults: schemas.Ptr(10),
NumImages: schemas.Ptr(5),
SearchLanguageFilter: []string{"en"},
SafeSearch: schemas.Ptr(true),
StreamMode: schemas.Ptr("partial"),
Stop: []string{"END"},
LogProbs: schemas.Ptr(true),
TopLogProbs: schemas.Ptr(3),
}
data, err := json.Marshal(req)
if err != nil {
t.Fatalf("failed to marshal request: %v", err)
}
jsonStr := string(data)
expectedFields := []string{
`"num_search_results":10`,
`"num_images":5`,
`"search_language_filter":["en"]`,
`"safe_search":true`,
`"stream_mode":"partial"`,
`"stop":["END"]`,
`"logprobs":true`,
`"top_logprobs":3`,
}
for _, field := range expectedFields {
if !strings.Contains(jsonStr, field) {
t.Errorf("expected JSON to contain '%s', got: %s", field, jsonStr)
}
}
})
}

View File

@@ -0,0 +1,217 @@
package perplexity
import (
"github.com/maximhq/bifrost/core/schemas"
)
// ToPerplexityResponsesRequest converts a BifrostResponsesRequest to PerplexityChatRequest
func ToPerplexityResponsesRequest(bifrostReq *schemas.BifrostResponsesRequest) *PerplexityChatRequest {
if bifrostReq == nil {
return nil
}
perplexityReq := &PerplexityChatRequest{
Model: bifrostReq.Model,
}
// Map basic parameters
if bifrostReq.Params != nil {
// Core parameters
perplexityReq.MaxTokens = bifrostReq.Params.MaxOutputTokens
perplexityReq.Temperature = bifrostReq.Params.Temperature
perplexityReq.TopP = bifrostReq.Params.TopP
// Handle reasoning effort mapping
if bifrostReq.Params.Reasoning != nil && bifrostReq.Params.Reasoning.Effort != nil {
if *bifrostReq.Params.Reasoning.Effort == "minimal" {
perplexityReq.ReasoningEffort = schemas.Ptr("low")
} else {
perplexityReq.ReasoningEffort = schemas.Ptr(*bifrostReq.Params.Reasoning.Effort)
}
}
// Handle extra parameters for Perplexity-specific fields
if bifrostReq.Params.ExtraParams != nil {
// Search-related parameters
if searchMode, ok := schemas.SafeExtractStringPointer(bifrostReq.Params.ExtraParams["search_mode"]); ok {
perplexityReq.SearchMode = searchMode
}
if languagePreference, ok := schemas.SafeExtractStringPointer(bifrostReq.Params.ExtraParams["language_preference"]); ok {
perplexityReq.LanguagePreference = languagePreference
}
if searchDomainFilter, ok := schemas.SafeExtractStringSlice(bifrostReq.Params.ExtraParams["search_domain_filter"]); ok {
perplexityReq.SearchDomainFilter = searchDomainFilter
}
if returnImages, ok := schemas.SafeExtractBoolPointer(bifrostReq.Params.ExtraParams["return_images"]); ok {
perplexityReq.ReturnImages = returnImages
}
if returnRelatedQuestions, ok := schemas.SafeExtractBoolPointer(bifrostReq.Params.ExtraParams["return_related_questions"]); ok {
perplexityReq.ReturnRelatedQuestions = returnRelatedQuestions
}
if searchRecencyFilter, ok := schemas.SafeExtractStringPointer(bifrostReq.Params.ExtraParams["search_recency_filter"]); ok {
perplexityReq.SearchRecencyFilter = searchRecencyFilter
}
if searchAfterDateFilter, ok := schemas.SafeExtractStringPointer(bifrostReq.Params.ExtraParams["search_after_date_filter"]); ok {
perplexityReq.SearchAfterDateFilter = searchAfterDateFilter
}
if searchBeforeDateFilter, ok := schemas.SafeExtractStringPointer(bifrostReq.Params.ExtraParams["search_before_date_filter"]); ok {
perplexityReq.SearchBeforeDateFilter = searchBeforeDateFilter
}
if lastUpdatedAfterFilter, ok := schemas.SafeExtractStringPointer(bifrostReq.Params.ExtraParams["last_updated_after_filter"]); ok {
perplexityReq.LastUpdatedAfterFilter = lastUpdatedAfterFilter
}
if lastUpdatedBeforeFilter, ok := schemas.SafeExtractStringPointer(bifrostReq.Params.ExtraParams["last_updated_before_filter"]); ok {
perplexityReq.LastUpdatedBeforeFilter = lastUpdatedBeforeFilter
}
if topK, ok := schemas.SafeExtractIntPointer(bifrostReq.Params.ExtraParams["top_k"]); ok {
perplexityReq.TopK = topK
}
if stream, ok := schemas.SafeExtractBoolPointer(bifrostReq.Params.ExtraParams["stream"]); ok {
perplexityReq.Stream = stream
}
if disableSearch, ok := schemas.SafeExtractBoolPointer(bifrostReq.Params.ExtraParams["disable_search"]); ok {
perplexityReq.DisableSearch = disableSearch
}
if enableSearchClassifier, ok := schemas.SafeExtractBoolPointer(bifrostReq.Params.ExtraParams["enable_search_classifier"]); ok {
perplexityReq.EnableSearchClassifier = enableSearchClassifier
}
if presencePenalty, ok := schemas.SafeExtractFloat64Pointer(bifrostReq.Params.ExtraParams["presence_penalty"]); ok {
perplexityReq.PresencePenalty = presencePenalty
}
if frequencyPenalty, ok := schemas.SafeExtractFloat64Pointer(bifrostReq.Params.ExtraParams["frequency_penalty"]); ok {
perplexityReq.FrequencyPenalty = frequencyPenalty
}
if responseFormat, ok := schemas.SafeExtractFromMap(bifrostReq.Params.ExtraParams, "response_format"); ok {
perplexityReq.ResponseFormat = &responseFormat
}
// Perplexity-specific request fields
if numSearchResults, ok := schemas.SafeExtractIntPointer(bifrostReq.Params.ExtraParams["num_search_results"]); ok {
perplexityReq.NumSearchResults = numSearchResults
}
if numImages, ok := schemas.SafeExtractIntPointer(bifrostReq.Params.ExtraParams["num_images"]); ok {
perplexityReq.NumImages = numImages
}
if searchLanguageFilter, ok := schemas.SafeExtractStringSlice(bifrostReq.Params.ExtraParams["search_language_filter"]); ok {
perplexityReq.SearchLanguageFilter = searchLanguageFilter
}
if imageFormatFilter, ok := schemas.SafeExtractStringSlice(bifrostReq.Params.ExtraParams["image_format_filter"]); ok {
perplexityReq.ImageFormatFilter = imageFormatFilter
}
if imageDomainFilter, ok := schemas.SafeExtractStringSlice(bifrostReq.Params.ExtraParams["image_domain_filter"]); ok {
perplexityReq.ImageDomainFilter = imageDomainFilter
}
if safeSearch, ok := schemas.SafeExtractBoolPointer(bifrostReq.Params.ExtraParams["safe_search"]); ok {
perplexityReq.SafeSearch = safeSearch
}
if streamMode, ok := schemas.SafeExtractStringPointer(bifrostReq.Params.ExtraParams["stream_mode"]); ok {
perplexityReq.StreamMode = streamMode
}
// Handle web_search_options
if webSearchOptionsParam, ok := schemas.SafeExtractFromMap(bifrostReq.Params.ExtraParams, "web_search_options"); ok {
if webSearchOptionsSlice, ok := webSearchOptionsParam.([]interface{}); ok {
var webSearchOptions []WebSearchOption
for _, optionInterface := range webSearchOptionsSlice {
if optionMap, ok := optionInterface.(map[string]interface{}); ok {
option := WebSearchOption{}
if searchContextSize, ok := schemas.SafeExtractStringPointer(optionMap["search_context_size"]); ok {
option.SearchContextSize = searchContextSize
}
if imageResultsEnhancedRelevance, ok := schemas.SafeExtractBoolPointer(optionMap["image_results_enhanced_relevance"]); ok {
option.ImageResultsEnhancedRelevance = imageResultsEnhancedRelevance
}
if searchType, ok := schemas.SafeExtractStringPointer(optionMap["search_type"]); ok {
option.SearchType = searchType
}
// Handle user_location
if userLocationParam, ok := schemas.SafeExtractFromMap(optionMap, "user_location"); ok {
if userLocationMap, ok := userLocationParam.(map[string]interface{}); ok {
userLocation := &WebSearchOptionUserLocation{}
if latitude, ok := schemas.SafeExtractFloat64Pointer(userLocationMap["latitude"]); ok {
userLocation.Latitude = latitude
}
if longitude, ok := schemas.SafeExtractFloat64Pointer(userLocationMap["longitude"]); ok {
userLocation.Longitude = longitude
}
if city, ok := schemas.SafeExtractStringPointer(userLocationMap["city"]); ok {
userLocation.City = city
}
if country, ok := schemas.SafeExtractStringPointer(userLocationMap["country"]); ok {
userLocation.Country = country
}
if region, ok := schemas.SafeExtractStringPointer(userLocationMap["region"]); ok {
userLocation.Region = region
}
option.UserLocation = userLocation
}
}
webSearchOptions = append(webSearchOptions, option)
}
}
perplexityReq.WebSearchOptions = webSearchOptions
}
}
// Handle media_response
if mediaResponseParam, ok := schemas.SafeExtractFromMap(bifrostReq.Params.ExtraParams, "media_response"); ok {
if mediaResponseMap, ok := mediaResponseParam.(map[string]interface{}); ok {
mediaResponse := &MediaResponse{}
if overridesParam, ok := schemas.SafeExtractFromMap(mediaResponseMap, "overrides"); ok {
if overridesMap, ok := overridesParam.(map[string]interface{}); ok {
overrides := MediaResponseOverrides{}
if returnVideos, ok := schemas.SafeExtractBoolPointer(overridesMap["return_videos"]); ok {
overrides.ReturnVideos = returnVideos
}
if returnImages, ok := schemas.SafeExtractBoolPointer(overridesMap["return_images"]); ok {
overrides.ReturnImages = returnImages
}
mediaResponse.Overrides = overrides
}
}
perplexityReq.MediaResponse = mediaResponse
}
}
}
}
// Process ResponsesInput (which contains the Responses messages)
if bifrostReq.Input != nil {
perplexityReq.Messages = schemas.ToChatMessages(bifrostReq.Input)
}
return perplexityReq
}

View File

@@ -0,0 +1,98 @@
package perplexity
import "github.com/maximhq/bifrost/core/schemas"
// PerplexityChatRequest represents a Perplexity chat completion request
type PerplexityChatRequest struct {
Model string `json:"model"` // Required: Model to use for chat completion
Messages []schemas.ChatMessage `json:"messages"` // Required: Array of message objects
SearchMode *string `json:"search_mode"` // Required: Search mode
ReasoningEffort *string `json:"reasoning_effort"` // Required: Reasoning effort (low, medium, high)
MaxTokens *int `json:"max_tokens,omitempty"` // Optional: Maximum tokens to generate
Temperature *float64 `json:"temperature,omitempty"` // Optional: Sampling temperature
TopP *float64 `json:"top_p,omitempty"` // Optional: Top-p sampling
LanguagePreference *string `json:"language_preference,omitempty"` // Optional: Language preference
SearchDomainFilter []string `json:"search_domain_filter,omitempty"` // Optional: Search domain filter
ReturnImages *bool `json:"return_images,omitempty"` // Optional: Return images
ReturnRelatedQuestions *bool `json:"return_related_questions,omitempty"` // Optional: Return related questions
SearchRecencyFilter *string `json:"search_recency_filter,omitempty"` // Optional: Search recency filter
SearchAfterDateFilter *string `json:"search_after_date_filter,omitempty"` // Optional: Search after date filter
SearchBeforeDateFilter *string `json:"search_before_date_filter,omitempty"` // Optional: Search before date filter
LastUpdatedAfterFilter *string `json:"last_updated_after_filter,omitempty"` // Optional: Last updated after filter
LastUpdatedBeforeFilter *string `json:"last_updated_before_filter,omitempty"` // Optional: Last updated before filter
TopK *int `json:"top_k,omitempty"` // Optional: Top-k sampling
Stream *bool `json:"stream,omitempty"` // Optional: Enable streaming
PresencePenalty *float64 `json:"presence_penalty,omitempty"` // Optional: Presence penalty
FrequencyPenalty *float64 `json:"frequency_penalty,omitempty"` // Optional: Frequency penalty
ResponseFormat *interface{} `json:"response_format,omitempty"` // Format for the response
DisableSearch *bool `json:"disable_search,omitempty"` // Optional: Disable search
EnableSearchClassifier *bool `json:"enable_search_classifier,omitempty"` // Optional: Enable search classifier
WebSearchOptions []WebSearchOption `json:"web_search_options,omitempty"` // Optional: Web search options
MediaResponse *MediaResponse `json:"media_response,omitempty"` // Optional: Media response
Tools []schemas.ChatTool `json:"tools,omitempty"` // Optional: Tools available for the model
ToolChoice *schemas.ChatToolChoice `json:"tool_choice,omitempty"` // Optional: Whether to call a tool
ParallelToolCalls *bool `json:"parallel_tool_calls,omitempty"` // Optional: Enable parallel tool calls
Stop []string `json:"stop,omitempty"` // Optional: Stop sequences
LogProbs *bool `json:"logprobs,omitempty"` // Optional: Return log probabilities
TopLogProbs *int `json:"top_logprobs,omitempty"` // Optional: Number of top log probabilities
NumSearchResults *int `json:"num_search_results,omitempty"` // Optional: Number of search results
NumImages *int `json:"num_images,omitempty"` // Optional: Number of images
SearchLanguageFilter []string `json:"search_language_filter,omitempty"` // Optional: Search language filter
ImageFormatFilter []string `json:"image_format_filter,omitempty"` // Optional: Image format filter
ImageDomainFilter []string `json:"image_domain_filter,omitempty"` // Optional: Image domain filter
SafeSearch *bool `json:"safe_search,omitempty"` // Optional: Enable safe search
StreamMode *string `json:"stream_mode,omitempty"` // Optional: Stream mode
ExtraParams map[string]interface{} `json:"-"`
}
// GetExtraParams implements the RequestBodyWithExtraParams interface
func (r *PerplexityChatRequest) GetExtraParams() map[string]interface{} {
return r.ExtraParams
}
type WebSearchOption struct {
SearchContextSize *string `json:"search_context_size,omitempty"` // "low" | "medium" | "high"
UserLocation *WebSearchOptionUserLocation `json:"user_location,omitempty"` // The approximate location of the user
ImageResultsEnhancedRelevance *bool `json:"image_results_enhanced_relevance,omitempty"` // Optional: Image results enhanced relevance
SearchType *string `json:"search_type,omitempty"` // Optional: "general" | "news" | "academic" | "social" | "writing"
}
type WebSearchOptionUserLocation struct {
Latitude *float64 `json:"latitude,omitempty"`
Longitude *float64 `json:"longitude,omitempty"`
City *string `json:"city,omitempty"` // Free text input for the city
Country *string `json:"country,omitempty"` // Two-letter ISO country code
Region *string `json:"region,omitempty"` // Free text input for the region
}
type MediaResponse struct {
Overrides MediaResponseOverrides `json:"overrides,omitempty"` // Optional: Overrides for the media response
}
type MediaResponseOverrides struct {
ReturnVideos *bool `json:"return_videos,omitempty"` // Optional: Return videos
ReturnImages *bool `json:"return_images,omitempty"` // Optional: Return images
}
type PerplexityChatResponse struct {
ID string `json:"id"`
Choices []schemas.BifrostResponseChoice `json:"choices"`
Created int `json:"created"` // The Unix timestamp (in seconds).
Model string `json:"model"`
Object string `json:"object"` // "chat.completion" or "chat.completion.chunk"
Citations []string `json:"citations,omitempty"`
SearchResults []schemas.SearchResult `json:"search_results,omitempty"`
Videos []schemas.VideoResult `json:"videos,omitempty"`
Usage *Usage `json:"usage,omitempty"`
}
type Usage struct {
PromptTokens int `json:"prompt_tokens"`
CompletionTokens int `json:"completion_tokens"`
TotalTokens int `json:"total_tokens"`
SearchContextSize *string `json:"search_context_size,omitempty"`
CitationTokens *int `json:"citation_tokens,omitempty"`
NumSearchQueries *int `json:"num_search_queries,omitempty"`
ReasoningTokens *int `json:"reasoning_tokens,omitempty"`
Cost *schemas.BifrostCost `json:"cost,omitempty"`
}