Added support for Perplexity (#545)
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg updater) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis updater) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage deb updater) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Read metadata (push) Waiting to run
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg updater) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis updater) (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage deb updater) (push) Blocked by required conditions

Co-authored-by: Thorsten Sommer <mail@tsommer.org>
This commit is contained in:
Peer Schüttandtsommer authored and GitHub committed 2025-08-31 14:27:35 +02:00
1 parent adf61a1384
commit 2a01d3bb6e
39 files changed
+398 -39

No files matched your search

@@ -21,7 +21,7 @@ public sealed class ProviderAlibabaCloud(ILogger logger) : BaseProvider("https:/
public override string InstanceName { get; set; } = "AlibabaCloud";
/// <inheritdoc />
public override async IAsyncEnumerable<string> StreamChatCompletion(Model chatModel, ChatThread chatThread, SettingsManager settingsManager, [EnumeratorCancellation] CancellationToken token = default)
public override async IAsyncEnumerable<ContentStreamChunk> StreamChatCompletion(Model chatModel, ChatThread chatThread, SettingsManager settingsManager, [EnumeratorCancellation] CancellationToken token = default)
{
// Get the API key:
var requestedSecret = await RUST_SERVICE.GetAPIKey(this);
@@ -18,7 +18,7 @@ public sealed class ProviderAnthropic(ILogger logger) : BaseProvider("https://ap
public override string InstanceName { get; set; } = "Anthropic";
/// <inheritdoc />
public override async IAsyncEnumerable<string> StreamChatCompletion(Model chatModel, ChatThread chatThread, SettingsManager settingsManager, [EnumeratorCancellation] CancellationToken token = default)
public override async IAsyncEnumerable<ContentStreamChunk> StreamChatCompletion(Model chatModel, ChatThread chatThread, SettingsManager settingsManager, [EnumeratorCancellation] CancellationToken token = default)
{
// Get the API key:
var requestedSecret = await RUST_SERVICE.GetAPIKey(this);
@@ -13,7 +13,7 @@ public readonly record struct ResponseStreamLine(string Type, int Index, Delta D
public bool ContainsContent() => this != default && !string.IsNullOrWhiteSpace(this.Delta.Text);
/// <inheritdoc />
public string GetContent() => this.Delta.Text;
public ContentStreamChunk GetContent() => new(this.Delta.Text, []);
}
/// <summary>
@@ -63,7 +63,7 @@ public abstract class BaseProvider : IProvider, ISecretId
public abstract string InstanceName { get; set; }
/// <inheritdoc />
public abstract IAsyncEnumerable<string> StreamChatCompletion(Model chatModel, ChatThread chatThread, SettingsManager settingsManager, CancellationToken token = default);
public abstract IAsyncEnumerable<ContentStreamChunk> StreamChatCompletion(Model chatModel, ChatThread chatThread, SettingsManager settingsManager, CancellationToken token = default);
/// <inheritdoc />
public abstract IAsyncEnumerable<ImageURL> StreamImageCompletion(Model imageModel, string promptPositive, string promptNegative = FilterOperator.String.Empty, ImageURL referenceImageURL = default, CancellationToken token = default);
@@ -96,7 +96,7 @@ public abstract class BaseProvider : IProvider, ISecretId
/// <param name="requestBuilder">A function that builds the request.</param>
/// <param name="token">The cancellation token.</param>
/// <returns>The status object of the request.</returns>
protected async Task<HttpRateLimitedStreamResult> SendRequest(Func<Task<HttpRequestMessage>> requestBuilder, CancellationToken token = default)
private async Task<HttpRateLimitedStreamResult> SendRequest(Func<Task<HttpRequestMessage>> requestBuilder, CancellationToken token = default)
{
const int MAX_RETRIES = 6;
const double RETRY_DELAY_SECONDS = 4;
@@ -189,7 +189,7 @@ public abstract class BaseProvider : IProvider, ISecretId
return new HttpRateLimitedStreamResult(true, false, string.Empty, response);
}
protected async IAsyncEnumerable<string> StreamChatCompletionInternal<T>(string providerName, Func<Task<HttpRequestMessage>> requestBuilder, [EnumeratorCancellation] CancellationToken token = default) where T : struct, IResponseStreamLine
protected async IAsyncEnumerable<ContentStreamChunk> StreamChatCompletionInternal<T>(string providerName, Func<Task<HttpRequestMessage>> requestBuilder, [EnumeratorCancellation] CancellationToken token = default) where T : struct, IResponseStreamLine
{
StreamReader? streamReader = null;
try
@@ -0,0 +1,16 @@
namespace AIStudio.Provider;
/// <summary>
/// A chunk of content from a content stream, along with its associated sources.
/// </summary>
/// <param name="Content">The text content of the chunk.</param>
/// <param name="Sources">The list of sources associated with the chunk.</param>
public sealed record ContentStreamChunk(string Content, IList<ISource> Sources)
{
/// <summary>
/// Implicit conversion to string.
/// </summary>
/// <param name="chunk">The content stream chunk.</param>
/// <returns>The text content of the chunk.</returns>
public static implicit operator string(ContentStreamChunk chunk) => chunk.Content;
}
@@ -20,7 +20,7 @@ public sealed class ProviderDeepSeek(ILogger logger) : BaseProvider("https://api
public override string InstanceName { get; set; } = "DeepSeek";
/// <inheritdoc />
public override async IAsyncEnumerable<string> StreamChatCompletion(Model chatModel, ChatThread chatThread, SettingsManager settingsManager, [EnumeratorCancellation] CancellationToken token = default)
public override async IAsyncEnumerable<ContentStreamChunk> StreamChatCompletion(Model chatModel, ChatThread chatThread, SettingsManager settingsManager, [EnumeratorCancellation] CancellationToken token = default)
{
// Get the API key:
var requestedSecret = await RUST_SERVICE.GetAPIKey(this);
@@ -19,7 +19,7 @@ public class ProviderFireworks(ILogger logger) : BaseProvider("https://api.firew
public override string InstanceName { get; set; } = "Fireworks.ai";
/// <inheritdoc />
public override async IAsyncEnumerable<string> StreamChatCompletion(Model chatModel, ChatThread chatThread, SettingsManager settingsManager, [EnumeratorCancellation] CancellationToken token = default)
public override async IAsyncEnumerable<ContentStreamChunk> StreamChatCompletion(Model chatModel, ChatThread chatThread, SettingsManager settingsManager, [EnumeratorCancellation] CancellationToken token = default)
{
// Get the API key:
var requestedSecret = await RUST_SERVICE.GetAPIKey(this);
@@ -14,7 +14,7 @@ public readonly record struct ResponseStreamLine(string Id, string Object, uint
public bool ContainsContent() => this != default && this.Choices.Count > 0;
/// <inheritdoc />
public string GetContent() => this.Choices[0].Delta.Content;
public ContentStreamChunk GetContent() => new(this.Choices[0].Delta.Content, []);
}
/// <summary>
@@ -20,7 +20,7 @@ public sealed class ProviderGWDG(ILogger logger) : BaseProvider("https://chat-ai
public override string InstanceName { get; set; } = "GWDG SAIA";
/// <inheritdoc />
public override async IAsyncEnumerable<string> StreamChatCompletion(Model chatModel, ChatThread chatThread, SettingsManager settingsManager, [EnumeratorCancellation] CancellationToken token = default)
public override async IAsyncEnumerable<ContentStreamChunk> StreamChatCompletion(Model chatModel, ChatThread chatThread, SettingsManager settingsManager, [EnumeratorCancellation] CancellationToken token = default)
{
// Get the API key:
var requestedSecret = await RUST_SERVICE.GetAPIKey(this);
@@ -20,7 +20,7 @@ public class ProviderGoogle(ILogger logger) : BaseProvider("https://generativela
public override string InstanceName { get; set; } = "Google Gemini";
/// <inheritdoc />
public override async IAsyncEnumerable<string> StreamChatCompletion(Provider.Model chatModel, ChatThread chatThread, SettingsManager settingsManager, [EnumeratorCancellation] CancellationToken token = default)
public override async IAsyncEnumerable<ContentStreamChunk> StreamChatCompletion(Provider.Model chatModel, ChatThread chatThread, SettingsManager settingsManager, [EnumeratorCancellation] CancellationToken token = default)
{
// Get the API key:
var requestedSecret = await RUST_SERVICE.GetAPIKey(this);
@@ -20,7 +20,7 @@ public class ProviderGroq(ILogger logger) : BaseProvider("https://api.groq.com/o
public override string InstanceName { get; set; } = "Groq";
/// <inheritdoc />
public override async IAsyncEnumerable<string> StreamChatCompletion(Model chatModel, ChatThread chatThread, SettingsManager settingsManager, [EnumeratorCancellation] CancellationToken token = default)
public override async IAsyncEnumerable<ContentStreamChunk> StreamChatCompletion(Model chatModel, ChatThread chatThread, SettingsManager settingsManager, [EnumeratorCancellation] CancellationToken token = default)
{
// Get the API key:
var requestedSecret = await RUST_SERVICE.GetAPIKey(this);
@@ -20,7 +20,7 @@ public sealed class ProviderHelmholtz(ILogger logger) : BaseProvider("https://ap
public override string InstanceName { get; set; } = "Helmholtz Blablador";
/// <inheritdoc />
public override async IAsyncEnumerable<string> StreamChatCompletion(Model chatModel, ChatThread chatThread, SettingsManager settingsManager, [EnumeratorCancellation] CancellationToken token = default)
public override async IAsyncEnumerable<ContentStreamChunk> StreamChatCompletion(Model chatModel, ChatThread chatThread, SettingsManager settingsManager, [EnumeratorCancellation] CancellationToken token = default)
{
// Get the API key:
var requestedSecret = await RUST_SERVICE.GetAPIKey(this);
@@ -25,7 +25,7 @@ public sealed class ProviderHuggingFace : BaseProvider
public override string InstanceName { get; set; } = "HuggingFace";
/// <inheritdoc />
public override async IAsyncEnumerable<string> StreamChatCompletion(Model chatModel, ChatThread chatThread, SettingsManager settingsManager, [EnumeratorCancellation] CancellationToken token = default)
public override async IAsyncEnumerable<ContentStreamChunk> StreamChatCompletion(Model chatModel, ChatThread chatThread, SettingsManager settingsManager, [EnumeratorCancellation] CancellationToken token = default)
{
// Get the API key:
var requestedSecret = await RUST_SERVICE.GetAPIKey(this);
+1 -1
View File
@@ -27,7 +27,7 @@ public interface IProvider
/// <param name="settingsManager">The settings manager instance to use.</param>
/// <param name="token">The cancellation token.</param>
/// <returns>The chat completion stream.</returns>
public IAsyncEnumerable<string> StreamChatCompletion(Model chatModel, ChatThread chatThread, SettingsManager settingsManager, CancellationToken token = default);
public IAsyncEnumerable<ContentStreamChunk> StreamChatCompletion(Model chatModel, ChatThread chatThread, SettingsManager settingsManager, CancellationToken token = default);
/// <summary>
/// Starts an image completion stream.
@@ -12,5 +12,17 @@ public interface IResponseStreamLine
/// Gets the content of the response line.
/// </summary>
/// <returns>The content of the response line.</returns>
public string GetContent();
public ContentStreamChunk GetContent();
/// <summary>
/// Checks if the response line contains any sources.
/// </summary>
/// <returns>True when the response line contains sources, false otherwise.</returns>
public bool ContainsSources() => false;
/// <summary>
/// Gets the sources of the response line.
/// </summary>
/// <returns>The sources of the response line.</returns>
public IList<ISource> GetSources() => [];
}
@@ -0,0 +1,17 @@
namespace AIStudio.Provider;
/// <summary>
/// Data model for a source used in the response.
/// </summary>
public interface ISource
{
/// <summary>
/// The title of the source.
/// </summary>
public string Title { get; }
/// <summary>
/// The URL of the source.
/// </summary>
public string URL { get; }
}
@@ -14,6 +14,7 @@ public enum LLMProviders
X = 8,
DEEP_SEEK = 11,
ALIBABA_CLOUD = 12,
PERPLEXITY = 14,
FIREWORKS = 5,
GROQ = 6,
@@ -9,6 +9,7 @@ using AIStudio.Provider.Helmholtz;
using AIStudio.Provider.HuggingFace;
using AIStudio.Provider.Mistral;
using AIStudio.Provider.OpenAI;
using AIStudio.Provider.Perplexity;
using AIStudio.Provider.SelfHosted;
using AIStudio.Provider.X;
using AIStudio.Settings;
@@ -38,6 +39,7 @@ public static class LLMProvidersExtensions
LLMProviders.X => "xAI",
LLMProviders.DEEP_SEEK => "DeepSeek",
LLMProviders.ALIBABA_CLOUD => "Alibaba Cloud",
LLMProviders.PERPLEXITY => "Perplexity",
LLMProviders.GROQ => "Groq",
LLMProviders.FIREWORKS => "Fireworks.ai",
@@ -86,6 +88,8 @@ public static class LLMProvidersExtensions
LLMProviders.DEEP_SEEK => Confidence.CHINA_NO_TRAINING.WithRegion("Asia").WithSources("https://cdn.deepseek.com/policies/en-US/deepseek-open-platform-terms-of-service.html").WithLevel(settingsManager.GetConfiguredConfidenceLevel(llmProvider)),
LLMProviders.ALIBABA_CLOUD => Confidence.CHINA_NO_TRAINING.WithRegion("Asia").WithSources("https://www.alibabacloud.com/help/en/model-studio/support/faq-about-alibaba-cloud-model-studio").WithLevel(settingsManager.GetConfiguredConfidenceLevel(llmProvider)),
LLMProviders.PERPLEXITY => Confidence.USA_NO_TRAINING.WithRegion("America, U.S.").WithSources("https://www.perplexity.ai/hub/legal/perplexity-api-terms-of-service").WithLevel(settingsManager.GetConfiguredConfidenceLevel(llmProvider)),
LLMProviders.SELF_HOSTED => Confidence.SELF_HOSTED.WithLevel(settingsManager.GetConfiguredConfidenceLevel(llmProvider)),
@@ -121,6 +125,7 @@ public static class LLMProvidersExtensions
LLMProviders.GWDG => false,
LLMProviders.DEEP_SEEK => false,
LLMProviders.HUGGINGFACE => false,
LLMProviders.PERPLEXITY => false,
//
// Self-hosted providers are treated as a special case anyway.
@@ -165,6 +170,7 @@ public static class LLMProvidersExtensions
LLMProviders.X => new ProviderX(logger) { InstanceName = instanceName },
LLMProviders.DEEP_SEEK => new ProviderDeepSeek(logger) { InstanceName = instanceName },
LLMProviders.ALIBABA_CLOUD => new ProviderAlibabaCloud(logger) { InstanceName = instanceName },
LLMProviders.PERPLEXITY => new ProviderPerplexity(logger) { InstanceName = instanceName },
LLMProviders.GROQ => new ProviderGroq(logger) { InstanceName = instanceName },
LLMProviders.FIREWORKS => new ProviderFireworks(logger) { InstanceName = instanceName },
@@ -194,6 +200,7 @@ public static class LLMProvidersExtensions
LLMProviders.X => "https://accounts.x.ai/sign-up",
LLMProviders.DEEP_SEEK => "https://platform.deepseek.com/sign_up",
LLMProviders.ALIBABA_CLOUD => "https://account.alibabacloud.com/register/intl_register.htm",
LLMProviders.PERPLEXITY => "https://www.perplexity.ai/account/api",
LLMProviders.GROQ => "https://console.groq.com/",
LLMProviders.FIREWORKS => "https://fireworks.ai/login",
@@ -216,6 +223,7 @@ public static class LLMProvidersExtensions
LLMProviders.FIREWORKS => "https://fireworks.ai/account/billing",
LLMProviders.DEEP_SEEK => "https://platform.deepseek.com/usage",
LLMProviders.ALIBABA_CLOUD => "https://usercenter2-intl.aliyun.com/billing",
LLMProviders.PERPLEXITY => "https://www.perplexity.ai/account/api/",
LLMProviders.HUGGINGFACE => "https://huggingface.co/settings/billing",
_ => string.Empty,
@@ -232,6 +240,7 @@ public static class LLMProvidersExtensions
LLMProviders.GOOGLE => true,
LLMProviders.DEEP_SEEK => true,
LLMProviders.ALIBABA_CLOUD => true,
LLMProviders.PERPLEXITY => true,
LLMProviders.HUGGINGFACE => true,
_ => false,
@@ -278,6 +287,7 @@ public static class LLMProvidersExtensions
LLMProviders.X => true,
LLMProviders.DEEP_SEEK => true,
LLMProviders.ALIBABA_CLOUD => true,
LLMProviders.PERPLEXITY => true,
LLMProviders.GROQ => true,
LLMProviders.FIREWORKS => true,
@@ -299,6 +309,7 @@ public static class LLMProvidersExtensions
LLMProviders.X => true,
LLMProviders.DEEP_SEEK => true,
LLMProviders.ALIBABA_CLOUD => true,
LLMProviders.PERPLEXITY => true,
LLMProviders.GROQ => true,
LLMProviders.FIREWORKS => true,
@@ -18,7 +18,7 @@ public sealed class ProviderMistral(ILogger logger) : BaseProvider("https://api.
public override string InstanceName { get; set; } = "Mistral";
/// <inheritdoc />
public override async IAsyncEnumerable<string> StreamChatCompletion(Provider.Model chatModel, ChatThread chatThread, SettingsManager settingsManager, [EnumeratorCancellation] CancellationToken token = default)
public override async IAsyncEnumerable<ContentStreamChunk> StreamChatCompletion(Provider.Model chatModel, ChatThread chatThread, SettingsManager settingsManager, [EnumeratorCancellation] CancellationToken token = default)
{
// Get the API key:
var requestedSecret = await RUST_SERVICE.GetAPIKey(this);
@@ -19,7 +19,7 @@ public class NoProvider : IProvider
public Task<IEnumerable<Model>> GetEmbeddingModels(string? apiKeyProvisional = null, CancellationToken token = default) => Task.FromResult<IEnumerable<Model>>([]);
public async IAsyncEnumerable<string> StreamChatCompletion(Model chatModel, ChatThread chatChatThread, SettingsManager settingsManager, [EnumeratorCancellation] CancellationToken token = default)
public async IAsyncEnumerable<ContentStreamChunk> StreamChatCompletion(Model chatModel, ChatThread chatChatThread, SettingsManager settingsManager, [EnumeratorCancellation] CancellationToken token = default)
{
await Task.FromResult(0);
yield break;
@@ -22,7 +22,7 @@ public sealed class ProviderOpenAI(ILogger logger) : BaseProvider("https://api.o
public override string InstanceName { get; set; } = "OpenAI";
/// <inheritdoc />
public override async IAsyncEnumerable<string> StreamChatCompletion(Model chatModel, ChatThread chatThread, SettingsManager settingsManager, [EnumeratorCancellation] CancellationToken token = default)
public override async IAsyncEnumerable<ContentStreamChunk> StreamChatCompletion(Model chatModel, ChatThread chatThread, SettingsManager settingsManager, [EnumeratorCancellation] CancellationToken token = default)
{
// Get the API key:
var requestedSecret = await RUST_SERVICE.GetAPIKey(this);
@@ -15,7 +15,7 @@ public readonly record struct ResponseStreamLine(string Id, string Object, uint
public bool ContainsContent() => this != default && this.Choices.Count > 0;
/// <inheritdoc />
public string GetContent() => this.Choices[0].Delta.Content;
public ContentStreamChunk GetContent() => new(this.Choices[0].Delta.Content, []);
}
/// <summary>
@@ -0,0 +1,148 @@
using System.Net.Http.Headers;
using System.Runtime.CompilerServices;
using System.Text;
using System.Text.Json;
using AIStudio.Chat;
using AIStudio.Provider.OpenAI;
using AIStudio.Settings;
namespace AIStudio.Provider.Perplexity;
public sealed class ProviderPerplexity(ILogger logger) : BaseProvider("https://api.perplexity.ai/", logger)
{
private static readonly Model[] KNOWN_MODELS =
[
new("sonar", "Sonar"),
new("sonar-pro", "Sonar Pro"),
new("sonar-reasoning", "Sonar Reasoning"),
new("sonar-reasoning-pro", "Sonar Reasoning Pro"),
new("sonar-deep-research", "Sonar Deep Research"),
];
#region Implementation of IProvider
/// <inheritdoc />
public override string Id => LLMProviders.PERPLEXITY.ToName();
/// <inheritdoc />
public override string InstanceName { get; set; } = "Perplexity";
/// <inheritdoc />
public override async IAsyncEnumerable<ContentStreamChunk> StreamChatCompletion(Model chatModel, ChatThread chatThread, SettingsManager settingsManager, [EnumeratorCancellation] CancellationToken token = default)
{
// Get the API key:
var requestedSecret = await RUST_SERVICE.GetAPIKey(this);
if(!requestedSecret.Success)
yield break;
// Prepare the system prompt:
var systemPrompt = new Message
{
Role = "system",
Content = chatThread.PrepareSystemPrompt(settingsManager, chatThread, this.logger),
};
// Prepare the Perplexity HTTP chat request:
var perplexityChatRequest = JsonSerializer.Serialize(new ChatRequest
{
Model = chatModel.Id,
// Build the messages:
// - First of all the system prompt
// - Then none-empty user and AI messages
Messages = [systemPrompt, ..chatThread.Blocks.Where(n => n.ContentType is ContentType.TEXT && !string.IsNullOrWhiteSpace((n.Content as ContentText)?.Text)).Select(n => new Message
{
Role = n.Role switch
{
ChatRole.USER => "user",
ChatRole.AI => "assistant",
ChatRole.AGENT => "assistant",
ChatRole.SYSTEM => "system",
_ => "user",
},
Content = n.Content switch
{
ContentText text => text.Text,
_ => string.Empty,
}
}).ToList()],
Stream = true,
}, JSON_SERIALIZER_OPTIONS);
async Task<HttpRequestMessage> RequestBuilder()
{
// Build the HTTP post request:
var request = new HttpRequestMessage(HttpMethod.Post, "chat/completions");
// Set the authorization header:
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", await requestedSecret.Secret.Decrypt(ENCRYPTION));
// Set the content:
request.Content = new StringContent(perplexityChatRequest, Encoding.UTF8, "application/json");
return request;
}
await foreach (var content in this.StreamChatCompletionInternal<ResponseStreamLine>("Perplexity", RequestBuilder, token))
yield return content;
}
#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
/// <inheritdoc />
public override async IAsyncEnumerable<ImageURL> StreamImageCompletion(Model imageModel, string promptPositive, string promptNegative = FilterOperator.String.Empty, ImageURL referenceImageURL = default, [EnumeratorCancellation] CancellationToken token = default)
{
yield break;
}
#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously
/// <inheritdoc />
public override Task<IEnumerable<Model>> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default)
{
return this.LoadModels();
}
/// <inheritdoc />
public override Task<IEnumerable<Model>> GetImageModels(string? apiKeyProvisional = null, CancellationToken token = default)
{
return Task.FromResult(Enumerable.Empty<Model>());
}
/// <inheritdoc />
public override Task<IEnumerable<Model>> GetEmbeddingModels(string? apiKeyProvisional = null, CancellationToken token = default)
{
return Task.FromResult(Enumerable.Empty<Model>());
}
public override IReadOnlyCollection<Capability> GetModelCapabilities(Model model)
{
var modelName = model.Id.ToLowerInvariant().AsSpan();
if(modelName.IndexOf("reasoning") is not -1 ||
modelName.IndexOf("deep-research") is not -1)
return
[
Capability.TEXT_INPUT,
Capability.MULTIPLE_IMAGE_INPUT,
Capability.TEXT_OUTPUT,
Capability.IMAGE_OUTPUT,
Capability.ALWAYS_REASONING,
];
return
[
Capability.TEXT_INPUT,
Capability.MULTIPLE_IMAGE_INPUT,
Capability.TEXT_OUTPUT,
Capability.IMAGE_OUTPUT,
];
}
#endregion
private Task<IEnumerable<Model>> LoadModels() => Task.FromResult<IEnumerable<Model>>(KNOWN_MODELS);
}
@@ -0,0 +1,45 @@
namespace AIStudio.Provider.Perplexity;
/// <summary>
/// Data model for a line in the response stream, for streaming completions.
/// </summary>
/// <param name="Id">The id of the response.</param>
/// <param name="Object">The object describing the response.</param>
/// <param name="Created">The timestamp of the response.</param>
/// <param name="Model">The model used for the response.</param>
/// <param name="SystemFingerprint">The system fingerprint; together with the seed, this allows you to reproduce the response.</param>
/// <param name="Choices">The choices made by the AI.</param>
public readonly record struct ResponseStreamLine(string Id, string Object, uint Created, string Model, string SystemFingerprint, IList<Choice> Choices, IList<SearchResult> SearchResults) : IResponseStreamLine
{
/// <inheritdoc />
public bool ContainsContent() => this != default && this.Choices.Count > 0;
/// <inheritdoc />
public ContentStreamChunk GetContent() => new(this.Choices[0].Delta.Content, this.GetSources());
/// <inheritdoc />
public bool ContainsSources() => this != default && this.SearchResults.Count > 0;
/// <inheritdoc />
public IList<ISource> GetSources() => this.SearchResults.Cast<ISource>().ToList();
}
/// <summary>
/// Data model for a choice made by the AI.
/// </summary>
/// <param name="Index">The index of the choice.</param>
/// <param name="Delta">The delta text of the choice.</param>
public readonly record struct Choice(int Index, Delta Delta);
/// <summary>
/// The delta text of a choice.
/// </summary>
/// <param name="Content">The content of the delta text.</param>
public readonly record struct Delta(string Content);
/// <summary>
/// Data model for a search result.
/// </summary>
/// <param name="Title">The title of the search result.</param>
/// <param name="URL">The URL of the search result.</param>
public sealed record SearchResult(string Title, string URL) : Source(Title, URL);
@@ -18,7 +18,7 @@ public sealed class ProviderSelfHosted(ILogger logger, Host host, string hostnam
public override string InstanceName { get; set; } = "Self-hosted";
/// <inheritdoc />
public override async IAsyncEnumerable<string> StreamChatCompletion(Provider.Model chatModel, ChatThread chatThread, SettingsManager settingsManager, [EnumeratorCancellation] CancellationToken token = default)
public override async IAsyncEnumerable<ContentStreamChunk> StreamChatCompletion(Provider.Model chatModel, ChatThread chatThread, SettingsManager settingsManager, [EnumeratorCancellation] CancellationToken token = default)
{
// Get the API key:
var requestedSecret = await RUST_SERVICE.GetAPIKey(this, isTrying: true);
@@ -0,0 +1,8 @@
namespace AIStudio.Provider;
/// <summary>
/// Data model for a source used in the response.
/// </summary>
/// <param name="Title">The title of the source.</param>
/// <param name="URL">The URL of the source.</param>
public record Source(string Title, string URL) : ISource;
@@ -0,0 +1,47 @@
using System.Text;
using AIStudio.Tools.PluginSystem;
namespace AIStudio.Provider;
public static class SourceExtensions
{
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(SourceExtensions).Namespace, nameof(SourceExtensions));
/// <summary>
/// Converts a list of sources to a markdown-formatted string.
/// </summary>
/// <param name="sources">The list of sources to convert.</param>
/// <returns>A markdown-formatted string representing the sources.</returns>
public static string ToMarkdown(this IList<Source> sources)
{
var sb = new StringBuilder();
sb.Append("## ");
sb.AppendLine(TB("Sources"));
var sourceNum = 0;
foreach (var source in sources)
{
sb.Append($"- [{++sourceNum}] ");
sb.Append('[');
sb.Append(source.Title);
sb.Append("](");
sb.Append(source.URL);
sb.AppendLine(")");
}
return sb.ToString();
}
/// <summary>
/// Merges a list of added sources into an existing list of sources, avoiding duplicates based on URL and Title.
/// </summary>
/// <param name="sources">The existing list of sources to merge into.</param>
/// <param name="addedSources">The list of sources to add.</param>
public static void MergeSources(this IList<Source> sources, IList<ISource> addedSources)
{
foreach (var addedSource in addedSources)
if (sources.All(s => s.URL != addedSource.URL && s.Title != addedSource.Title))
sources.Add((Source)addedSource);
}
}
@@ -20,7 +20,7 @@ public sealed class ProviderX(ILogger logger) : BaseProvider("https://api.x.ai/v
public override string InstanceName { get; set; } = "xAI";
/// <inheritdoc />
public override async IAsyncEnumerable<string> StreamChatCompletion(Model chatModel, ChatThread chatThread, SettingsManager settingsManager, [EnumeratorCancellation] CancellationToken token = default)
public override async IAsyncEnumerable<ContentStreamChunk> StreamChatCompletion(Model chatModel, ChatThread chatThread, SettingsManager settingsManager, [EnumeratorCancellation] CancellationToken token = default)
{
// Get the API key:
var requestedSecret = await RUST_SERVICE.GetAPIKey(this);