Skip to content

feat: add PerplexityAiService implementing IAiService using Sonar API #73

@artcava

Description

@artcava

Context

Currently the only implementation of IAiService is AiService, which calls OpenAI's gpt-4.1-nano for text summarisation and gpt-image-1.5 for image generation.

The Perplexity Sonar API offers a Chat Completions endpoint fully compatible with the OpenAI request/response format (POST https://api.perplexity.ai/chat/completions), authenticated with a Bearer token (PERPLEXITY_API_KEY). This makes it straightforward to add an alternative IAiService implementation backed by Perplexity.

Perplexity's main advantage for this use case is real-time web search grounding: sonar and sonar-pro retrieve live information from the web before generating a response, which can produce more current and citation-backed Bitcoin news summaries.


API Information

Endpoint

POST https://api.perplexity.ai/chat/completions
Authorization: Bearer {PERPLEXITY_API_KEY}
Content-Type: application/json

Available models for text

Model Input ($/1M) Output ($/1M) Notes
sonar $1 $1 Basic real-time search, most cost-effective
sonar-pro $3 $15 Deeper search, better reasoning
sonar-reasoning-pro $2 $8 Chain-of-thought + search

sonar is the recommended starting model: low cost and returns web-grounded summaries directly.

Request format (OpenAI-compatible)

{
  "model": "sonar",
  "messages": [
    { "role": "system", "content": "You are a concise Bitcoin news summariser." },
    { "role": "user", "content": "Summarise this text in under 200 characters: {text}" }
  ],
  "max_tokens": 120,
  "temperature": 0.5
}

Response format (identical to OpenAI)

{
  "choices": [{ "message": { "role": "assistant", "content": "..." } }],
  "usage": { "prompt_tokens": 12, "completion_tokens": 80, "total_tokens": 92 }
}

The existing OpenAIResponse model class can be reused as-is.

Image generation

Perplexity does not offer an image generation API. PerplexityAiService.GenerateImageAsync must either:

  • Return Array.Empty<byte[]>() and log a warning (FeedGenerator already handles null/empty image gracefully), or
  • Delegate image generation to the existing AiService if a fallback is desired (optional, as a future improvement).

Implementation Plan

1. New file: src/Services/PerplexityAiService.cs

using System.Net.Http.Json;
using XPoster.Abstraction;

namespace XPoster.Services;

/// <summary>
/// Implements <see cref="IAiService"/> using the Perplexity Sonar API.
/// Uses <c>sonar</c> for text summarisation and image-prompt generation.
/// Image generation is not supported by Perplexity; returns an empty byte array.
/// Credentials are read from the <c>PERPLEXITY_API_KEY</c> environment variable.
/// </summary>
public class PerplexityAiService : IAiService
{
    private readonly HttpClient _client;
    private readonly ILogger<PerplexityAiService> _logger;
    private const string BaseUrl = "https://api.perplexity.ai";
    private const string ChatModel = "sonar";

    public PerplexityAiService(IHttpClientFactory httpClientFactory, ILogger<PerplexityAiService> logger)
    {
        _logger = logger;
        _client = httpClientFactory.CreateClient();
        _client.BaseAddress = new Uri(BaseUrl);
        _client.DefaultRequestHeaders.Add(
            "Authorization",
            $"Bearer {Environment.GetEnvironmentVariable("PERPLEXITY_API_KEY")}"
        );
    }

    public async Task<string> GetSummaryAsync(string text, int messageMaxLength)
    {
        // ... same retry loop as AiService, pointing to Perplexity endpoint
    }

    public async Task<string> GetImagePromptAsync(string text)
    {
        // ... same pattern as GetSummaryAsync
    }

    public Task<byte[]> GenerateImageAsync(string prompt)
    {
        _logger.LogWarning("Perplexity does not support image generation. Returning empty image.");
        return Task.FromResult(Array.Empty<byte>());
    }
}

2. New environment variable

Variable Required Description
AI_PROVIDER Optional openai (default) or perplexity. Selects which IAiService implementation to register.
PERPLEXITY_API_KEY Required when AI_PROVIDER=perplexity API key from https://www.perplexity.ai/settings/api

3. Update Program.cs

Add conditional registration based on AI_PROVIDER:

var aiProvider = Environment.GetEnvironmentVariable("AI_PROVIDER") ?? "openai";
if (aiProvider.Equals("perplexity", StringComparison.OrdinalIgnoreCase))
    builder.Services.AddTransient<IAiService, PerplexityAiService>();
else
    builder.Services.AddTransient<IAiService, AiService>();

4. src/local.settings.json.example and docs/configuration.md

Add:

"AI_PROVIDER": "openai",
"PERPLEXITY_API_KEY": ""

Document both variables in docs/configuration.md under a new "AI Provider" subsection that explains the two options side by side.


How to Get a Perplexity API Key

  1. Sign in to https://www.perplexity.ai with your existing Perplexity account (Pro plan or higher grants API access).
  2. Go to Settings → API (https://www.perplexity.ai/settings/api).
  3. Click Generate to create a new API key.
  4. Copy the key and store it in PERPLEXITY_API_KEY.

API keys are charged per token/request. See the Perplexity pricing page for current rates. For the XPoster workload (2 summary calls/day × ~1K tokens each), estimated cost is < $0.01/day with the sonar model.


Tests to Add

tests/Services/PerplexityAiServiceTests.cs:

  • GetSummaryAsync_WhenApiReturnsValidResponse_ReturnsTrimmedContent
  • GetSummaryAsync_WhenTextAlreadyShortEnough_ReturnsTextUnchanged
  • GetSummaryAsync_WhenApiReturns429_ReturnsEmptyString
  • GetSummaryAsync_WhenApiReturnsNonSuccess_ReturnsEmptyString
  • GetImagePromptAsync_WhenApiReturnsValidResponse_ReturnsPrompt
  • GenerateImageAsync_AlwaysReturnsEmptyByteArray

Acceptance Criteria

  • PerplexityAiService implements IAiService and compiles
  • When AI_PROVIDER=perplexity, IAiService resolves to PerplexityAiService
  • When AI_PROVIDER=openai or unset, behaviour is unchanged (AiService)
  • GetSummaryAsync returns a web-grounded summary from Perplexity Sonar
  • GenerateImageAsync returns empty byte array and logs a warning
  • FeedGenerator still publishes posts without image when image is empty (existing graceful degradation)
  • PERPLEXITY_API_KEY and AI_PROVIDER documented
  • All unit tests pass
  • CI remains green

Related

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions