diff --git a/core/providers/perplexity/perplexity.go b/core/providers/perplexity/perplexity.go index 2a1e55827b..1c8432ec9a 100644 --- a/core/providers/perplexity/perplexity.go +++ b/core/providers/perplexity/perplexity.go @@ -121,8 +121,19 @@ func (provider *PerplexityProvider) completeRequest(ctx *schemas.BifrostContext, } // ListModels performs a list models request to Perplexity's API. +// Perplexity's /v1/models endpoint is OpenAI-compatible, so the OpenAI handler is reused. 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()) + return openai.HandleOpenAIListModelsRequest( + ctx, + provider.client, + request, + provider.networkConfig.BaseURL+providerUtils.GetPathFromContext(ctx, "/v1/models"), + keys, + provider.networkConfig.ExtraHeaders, + provider.GetProviderKey(), + providerUtils.ShouldSendBackRawRequest(ctx, provider.sendBackRawRequest), + providerUtils.ShouldSendBackRawResponse(ctx, provider.sendBackRawResponse), + ) } // TextCompletion is not supported by the Perplexity provider. @@ -214,26 +225,68 @@ func (provider *PerplexityProvider) ChatCompletionStream(ctx *schemas.BifrostCon } // Responses performs a responses request to the Perplexity API. +// Models available on Perplexity's /v1/responses endpoint are routed there via the +// OpenAI-compatible handler; sonar-* models not supported on responses fall back to /chat/completions. 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 + if !isPerplexityResponsesSupported(schemas.ResolveCanonicalModel(ctx, request.Model)) { + chatResponse, err := provider.ChatCompletion(ctx, key, request.ToChatRequest()) + if err != nil { + return nil, err + } + return chatResponse.ToBifrostResponsesResponse(), nil } - response := chatResponse.ToBifrostResponsesResponse() - - return response, nil + return openai.HandleOpenAIResponsesRequest( + ctx, + provider.client, + provider.networkConfig.BaseURL+providerUtils.GetPathFromContext(ctx, "/v1/responses"), + request, + openai.BearerAuthHeader(key), + provider.networkConfig.ExtraHeaders, + providerUtils.ShouldSendBackRawRequest(ctx, provider.sendBackRawRequest), + providerUtils.ShouldSendBackRawResponse(ctx, provider.sendBackRawResponse), + schemas.Perplexity, + nil, + nil, + nil, + provider.logger, + ) } // ResponsesStream performs a streaming responses request to the Perplexity API. +// Models available on Perplexity's /v1/responses endpoint are streamed from there via the +// OpenAI-compatible handler; sonar-* models not supported on responses fall back to /chat/completions. 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( + if !isPerplexityResponsesSupported(schemas.ResolveCanonicalModel(ctx, request.Model)) { + ctx.SetValue(schemas.BifrostContextKeyIsResponsesToChatCompletionFallback, true) + return provider.ChatCompletionStream( + ctx, + postHookRunner, + postHookSpanFinalizer, + key, + request.ToChatRequest(), + ) + } + + return openai.HandleOpenAIResponsesStreaming( ctx, + provider.streamingClient, + provider.networkConfig.BaseURL+providerUtils.GetPathFromContext(ctx, "/v1/responses"), + request, + openai.BearerAuthHeader(key), + provider.networkConfig.ExtraHeaders, + provider.networkConfig.StreamIdleTimeoutInSeconds, + providerUtils.ShouldSendBackRawRequest(ctx, provider.sendBackRawRequest), + providerUtils.ShouldSendBackRawResponse(ctx, provider.sendBackRawResponse), + schemas.Perplexity, postHookRunner, + nil, + nil, + nil, + nil, + nil, + provider.logger, postHookSpanFinalizer, - key, - request.ToChatRequest(), ) } diff --git a/core/providers/perplexity/responses.go b/core/providers/perplexity/responses.go index fe229e99f5..5f21841eaa 100644 --- a/core/providers/perplexity/responses.go +++ b/core/providers/perplexity/responses.go @@ -1,9 +1,19 @@ package perplexity import ( + "strings" + "github.com/maximhq/bifrost/core/schemas" ) +// isPerplexityResponsesSupported reports whether the model should use /v1/responses vs /chat/completions. +// Denylist by design: /chat/completions serves only the Sonar family, so sonar-* variants go to chat and +// every other model (base sonar + non-Sonar) goes to responses. This keeps newly-shipped non-Sonar models +// working without a code change; they'd fail on chat, which doesn't serve them. +func isPerplexityResponsesSupported(model string) bool { + return !strings.HasPrefix(strings.TrimPrefix(model, "perplexity/"), "sonar-") +} + // ToPerplexityResponsesRequest converts a BifrostResponsesRequest to PerplexityChatRequest func ToPerplexityResponsesRequest(bifrostReq *schemas.BifrostResponsesRequest) *PerplexityChatRequest { if bifrostReq == nil { diff --git a/framework/modelcatalog/models.go b/framework/modelcatalog/models.go index 5fbb58f184..83a6244421 100644 --- a/framework/modelcatalog/models.go +++ b/framework/modelcatalog/models.go @@ -290,6 +290,8 @@ func (mc *ModelCatalog) RefineModelForProvider(provider schemas.ModelProvider, m return mc.refineNestedProviderModel(provider, model) case schemas.Replicate: return mc.refineNestedProviderModel(provider, model) + case schemas.Perplexity: + return mc.refineNestedProviderModel(provider, model) } return model, nil }