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
- Sign in to https://www.perplexity.ai with your existing Perplexity account (Pro plan or higher grants API access).
- Go to Settings → API (https://www.perplexity.ai/settings/api).
- Click Generate to create a new API key.
- 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
Related
Context
Currently the only implementation of
IAiServiceisAiService, which calls OpenAI'sgpt-4.1-nanofor text summarisation andgpt-image-1.5for 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 alternativeIAiServiceimplementation backed by Perplexity.Perplexity's main advantage for this use case is real-time web search grounding:
sonarandsonar-proretrieve live information from the web before generating a response, which can produce more current and citation-backed Bitcoin news summaries.API Information
Endpoint
Available models for text
sonarsonar-prosonar-reasoning-proRequest 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
OpenAIResponsemodel class can be reused as-is.Image generation
Perplexity does not offer an image generation API.
PerplexityAiService.GenerateImageAsyncmust either:Array.Empty<byte[]>()and log a warning (FeedGenerator already handles null/empty image gracefully), orAiServiceif a fallback is desired (optional, as a future improvement).Implementation Plan
1. New file:
src/Services/PerplexityAiService.cs2. New environment variable
AI_PROVIDERopenai(default) orperplexity. Selects whichIAiServiceimplementation to register.PERPLEXITY_API_KEYAI_PROVIDER=perplexity3. Update
Program.csAdd conditional registration based on
AI_PROVIDER:4.
src/local.settings.json.exampleanddocs/configuration.mdAdd:
Document both variables in
docs/configuration.mdunder a new "AI Provider" subsection that explains the two options side by side.How to Get a Perplexity API Key
PERPLEXITY_API_KEY.Tests to Add
tests/Services/PerplexityAiServiceTests.cs:GetSummaryAsync_WhenApiReturnsValidResponse_ReturnsTrimmedContentGetSummaryAsync_WhenTextAlreadyShortEnough_ReturnsTextUnchangedGetSummaryAsync_WhenApiReturns429_ReturnsEmptyStringGetSummaryAsync_WhenApiReturnsNonSuccess_ReturnsEmptyStringGetImagePromptAsync_WhenApiReturnsValidResponse_ReturnsPromptGenerateImageAsync_AlwaysReturnsEmptyByteArrayAcceptance Criteria
PerplexityAiServiceimplementsIAiServiceand compilesAI_PROVIDER=perplexity,IAiServiceresolves toPerplexityAiServiceAI_PROVIDER=openaior unset, behaviour is unchanged (AiService)GetSummaryAsyncreturns a web-grounded summary from Perplexity SonarGenerateImageAsyncreturns empty byte array and logs a warningFeedGeneratorstill publishes posts without image when image is empty (existing graceful degradation)PERPLEXITY_API_KEYandAI_PROVIDERdocumentedRelated
OPENAI_API_KEYis correctly documented as the OpenAI-specific key)