Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion examples/basic/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import (
"github.com/agentuity/llmproxy/providers/googleai"
"github.com/agentuity/llmproxy/providers/groq"
"github.com/agentuity/llmproxy/providers/openai"
"github.com/agentuity/llmproxy/providers/perplexity"
"github.com/agentuity/llmproxy/providers/xai"
"go.opentelemetry.io/otel/trace"
)
Expand Down Expand Up @@ -112,6 +113,15 @@ func main() {
logr.Info("Registered: x.AI")
}

if apiKey := os.Getenv("PERPLEXITY_API_KEY"); apiKey != "" {
provider, err := perplexity.New(apiKey)
if err != nil {
log.Fatalf("failed to create perplexity provider: %v", err)
}
registry.Register(provider)
logr.Info("Registered: Perplexity")
}

if apiKey := os.Getenv("GOOGLE_AI_API_KEY"); apiKey != "" {
provider, err := googleai.New(apiKey)
if err != nil {
Expand Down Expand Up @@ -166,7 +176,7 @@ func main() {
openaiProvider, _ = registry.Get("groq")
}
if openaiProvider == nil {
for _, name := range []string{"anthropic", "fireworks", "xai", "googleai"} {
for _, name := range []string{"anthropic", "fireworks", "xai", "perplexity", "googleai"} {
if p, ok := registry.Get(name); ok {
openaiProvider = p
break
Expand Down
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ module github.com/agentuity/llmproxy

go 1.26.2

require go.opentelemetry.io/otel/trace v1.43.0

require (
github.com/cespare/xxhash/v2 v2.3.0 // indirect
go.opentelemetry.io/otel v1.43.0 // indirect
go.opentelemetry.io/otel/trace v1.43.0 // indirect
)
10 changes: 10 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=
go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
26 changes: 26 additions & 0 deletions providers/perplexity/provider.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package perplexity

import (
"github.com/agentuity/llmproxy"
"github.com/agentuity/llmproxy/providers/openai_compatible"
)

type Provider struct {
*llmproxy.BaseProvider
}

func New(apiKey string) (*Provider, error) {
resolver, err := NewResolver("https://api.perplexity.ai")
if err != nil {
return nil, err
}

return &Provider{
BaseProvider: llmproxy.NewBaseProvider("perplexity",
llmproxy.WithBodyParser(&openai_compatible.Parser{}),
llmproxy.WithRequestEnricher(openai_compatible.NewEnricher(apiKey)),
llmproxy.WithResponseExtractor(openai_compatible.NewExtractor()),
llmproxy.WithURLResolver(resolver),
),
}, nil
}
103 changes: 103 additions & 0 deletions providers/perplexity/provider_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package perplexity

import (
"net/http/httptest"
"testing"

"github.com/agentuity/llmproxy"
)

func TestNew(t *testing.T) {
provider, err := New("test-api-key")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}

if provider.Name() != "perplexity" {
t.Errorf("Name = %q, want %q", provider.Name(), "perplexity")
}
if provider.BodyParser() == nil {
t.Error("BodyParser should not be nil")
}
if provider.RequestEnricher() == nil {
t.Error("RequestEnricher should not be nil")
}
if provider.ResponseExtractor() == nil {
t.Error("ResponseExtractor should not be nil")
}
if provider.URLResolver() == nil {
t.Error("URLResolver should not be nil")
}
}

func TestResolver_PerplexityURL(t *testing.T) {
provider, err := New("test-api-key")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}

resolver := provider.URLResolver()
meta := llmproxy.BodyMetadata{Model: "sonar"}
u, err := resolver.Resolve(meta)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}

expected := "https://api.perplexity.ai/v1/sonar"
if u.String() != expected {
t.Errorf("URL = %q, want %q", u.String(), expected)
}
}

func TestResolver_InvalidURL(t *testing.T) {
_, err := NewResolver("://invalid-url")
if err == nil {
t.Fatal("expected error for invalid URL")
}
}

func TestEnricher_SetsAuthorization(t *testing.T) {
provider, err := New("pplx-test-key")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}

enricher := provider.RequestEnricher()
req := httptest.NewRequest("POST", "https://api.perplexity.ai/v1/sonar", nil)

err = enricher.Enrich(req, llmproxy.BodyMetadata{}, nil)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}

if auth := req.Header.Get("Authorization"); auth != "Bearer pplx-test-key" {
t.Errorf("Authorization = %q, want %q", auth, "Bearer pplx-test-key")
}
if ct := req.Header.Get("Content-Type"); ct != "application/json" {
t.Errorf("Content-Type = %q, want %q", ct, "application/json")
}
}

func TestNew_EmptyKey(t *testing.T) {
provider, err := New("")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}

if provider.Name() != "perplexity" {
t.Errorf("Name = %q, want %q", provider.Name(), "perplexity")
}

enricher := provider.RequestEnricher()
req := httptest.NewRequest("POST", "https://api.perplexity.ai/v1/sonar", nil)
req.Header.Set("Authorization", "Bearer incoming-token")

err = enricher.Enrich(req, llmproxy.BodyMetadata{}, nil)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}

if auth := req.Header.Get("Authorization"); auth != "" {
t.Errorf("Authorization = %q, want empty (header should be deleted for empty key)", auth)
}
}
23 changes: 23 additions & 0 deletions providers/perplexity/resolver.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package perplexity

import (
"net/url"

"github.com/agentuity/llmproxy"
)

type Resolver struct {
BaseURL *url.URL
}

func (r *Resolver) Resolve(meta llmproxy.BodyMetadata) (*url.URL, error) {
return r.BaseURL.JoinPath("v1", "sonar"), nil
}

func NewResolver(baseURL string) (*Resolver, error) {
u, err := url.Parse(baseURL)
if err != nil {
return nil, err
}
return &Resolver{BaseURL: u}, nil
}
Loading