Chat request refactoring for OpenAI-compatible providers (#722)
Some checks are pending
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg,updater, dmg) (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, nsis) (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, appimage,deb) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Determine run mode (push) Waiting to run
Build and Release / Read metadata (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,updater, dmg) (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, nsis) (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, appimage,deb) (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions

This commit is contained in:
Peer Schütt 2026-04-13 13:33:17 +02:00 committed by GitHub
parent a3bf308a76
commit d494fe4bc7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
24 changed files with 398 additions and 736 deletions

View File

@ -1,7 +1,5 @@
using System.Net.Http.Headers; using System.Net.Http.Headers;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Text;
using System.Text.Json;
using AIStudio.Chat; using AIStudio.Chat;
using AIStudio.Provider.OpenAI; using AIStudio.Provider.OpenAI;
@ -24,26 +22,17 @@ public sealed class ProviderAlibabaCloud() : BaseProvider(LLMProviders.ALIBABA_C
/// <inheritdoc /> /// <inheritdoc />
public override async IAsyncEnumerable<ContentStreamChunk> 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: await foreach (var content in this.StreamOpenAICompatibleChatCompletion<ChatCompletionAPIRequest, ChatCompletionDeltaStreamLine, NoChatCompletionAnnotationStreamLine>(
var requestedSecret = await RUST_SERVICE.GetAPIKey(this, SecretStoreType.LLM_PROVIDER); "AlibabaCloud",
if(!requestedSecret.Success) chatModel,
yield break; chatThread,
settingsManager,
// Prepare the system prompt: async (systemPrompt, apiParameters) =>
var systemPrompt = new TextMessage
{ {
Role = "system",
Content = chatThread.PrepareSystemPrompt(settingsManager),
};
// Parse the API parameters:
var apiParameters = this.ParseAdditionalApiParameters();
// Build the list of messages: // Build the list of messages:
var messages = await chatThread.Blocks.BuildMessagesUsingNestedImageUrlAsync(this.Provider, chatModel); var messages = await chatThread.Blocks.BuildMessagesUsingNestedImageUrlAsync(this.Provider, chatModel);
// Prepare the AlibabaCloud HTTP chat request: return new ChatCompletionAPIRequest
var alibabaCloudChatRequest = JsonSerializer.Serialize(new ChatCompletionAPIRequest
{ {
Model = chatModel.Id, Model = chatModel.Id,
@ -54,22 +43,9 @@ public sealed class ProviderAlibabaCloud() : BaseProvider(LLMProviders.ALIBABA_C
Stream = true, Stream = true,
AdditionalApiParameters = apiParameters AdditionalApiParameters = apiParameters
}, JSON_SERIALIZER_OPTIONS); };
},
async Task<HttpRequestMessage> RequestBuilder() token: token))
{
// 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(alibabaCloudChatRequest, Encoding.UTF8, "application/json");
return request;
}
await foreach (var content in this.StreamChatCompletionInternal<ChatCompletionDeltaStreamLine, NoChatCompletionAnnotationStreamLine>("AlibabaCloud", RequestBuilder, token))
yield return content; yield return content;
} }

View File

@ -565,6 +565,78 @@ public abstract class BaseProvider : IProvider, ISecretId
streamReader.Dispose(); streamReader.Dispose();
} }
/// <summary>
/// Streams the chat completion from an OpenAI-compatible provider using the Chat Completion API.
/// </summary>
/// <param name="providerName">The provider name for logging and error reporting.</param>
/// <param name="chatModel">The selected chat model.</param>
/// <param name="chatThread">The current chat thread.</param>
/// <param name="settingsManager">The settings manager.</param>
/// <param name="requestFactory">Builds the provider-specific request body.</param>
/// <param name="storeType">The secret store type.</param>
/// <param name="isTryingSecret">Whether the API key is optional.</param>
/// <param name="systemPromptRole">The system prompt role to use.</param>
/// <param name="requestPath">The request path, relative to the provider base URL.</param>
/// <param name="headersAction">Optional additional headers to add.</param>
/// <param name="token">The cancellation token.</param>
/// <typeparam name="TRequest">The request DTO type.</typeparam>
/// <typeparam name="TDelta">The delta stream line type.</typeparam>
/// <typeparam name="TAnnotation">The annotation stream line type.</typeparam>
/// <returns>The streamed content chunks.</returns>
protected async IAsyncEnumerable<ContentStreamChunk> StreamOpenAICompatibleChatCompletion<TRequest, TDelta, TAnnotation>(
string providerName,
Model chatModel,
ChatThread chatThread,
SettingsManager settingsManager,
Func<TextMessage, IDictionary<string, object>, Task<TRequest>> requestFactory,
SecretStoreType storeType = SecretStoreType.LLM_PROVIDER,
bool isTryingSecret = false,
string systemPromptRole = "system",
string requestPath = "chat/completions",
Action<HttpRequestHeaders>? headersAction = null,
[EnumeratorCancellation] CancellationToken token = default)
where TDelta : IResponseStreamLine
where TAnnotation : IAnnotationStreamLine
{
// Get the API key:
var requestedSecret = await RUST_SERVICE.GetAPIKey(this, storeType, isTrying: isTryingSecret);
if(!requestedSecret.Success && !isTryingSecret)
yield break;
// Prepare the system prompt:
var systemPrompt = new TextMessage
{
Role = systemPromptRole,
Content = chatThread.PrepareSystemPrompt(settingsManager),
};
// Parse the API parameters:
var apiParameters = this.ParseAdditionalApiParameters();
// Prepare the provider HTTP chat request:
var providerChatRequest = JsonSerializer.Serialize(await requestFactory(systemPrompt, apiParameters), JSON_SERIALIZER_OPTIONS);
async Task<HttpRequestMessage> RequestBuilder()
{
// Build the HTTP post request:
var request = new HttpRequestMessage(HttpMethod.Post, requestPath);
// Set the authorization header:
if (requestedSecret.Success)
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", await requestedSecret.Secret.Decrypt(ENCRYPTION));
// Set provider-specific headers:
headersAction?.Invoke(request.Headers);
// Set the content:
request.Content = new StringContent(providerChatRequest, Encoding.UTF8, "application/json");
return request;
}
await foreach (var content in this.StreamChatCompletionInternal<TDelta, TAnnotation>(providerName, RequestBuilder, token))
yield return content;
}
protected async Task<string> PerformStandardTranscriptionRequest(RequestedSecret requestedSecret, Model transcriptionModel, string audioFilePath, Host host = Host.NONE, CancellationToken token = default) protected async Task<string> PerformStandardTranscriptionRequest(RequestedSecret requestedSecret, Model transcriptionModel, string audioFilePath, Host host = Host.NONE, CancellationToken token = default)
{ {
try try

View File

@ -1,7 +1,5 @@
using System.Net.Http.Headers; using System.Net.Http.Headers;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Text;
using System.Text.Json;
using AIStudio.Chat; using AIStudio.Chat;
using AIStudio.Provider.OpenAI; using AIStudio.Provider.OpenAI;
@ -24,26 +22,17 @@ public sealed class ProviderDeepSeek() : BaseProvider(LLMProviders.DEEP_SEEK, "h
/// <inheritdoc /> /// <inheritdoc />
public override async IAsyncEnumerable<ContentStreamChunk> 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: await foreach (var content in this.StreamOpenAICompatibleChatCompletion<ChatCompletionAPIRequest, ChatCompletionDeltaStreamLine, NoChatCompletionAnnotationStreamLine>(
var requestedSecret = await RUST_SERVICE.GetAPIKey(this, SecretStoreType.LLM_PROVIDER); "DeepSeek",
if(!requestedSecret.Success) chatModel,
yield break; chatThread,
settingsManager,
// Prepare the system prompt: async (systemPrompt, apiParameters) =>
var systemPrompt = new TextMessage
{ {
Role = "system",
Content = chatThread.PrepareSystemPrompt(settingsManager),
};
// Parse the API parameters:
var apiParameters = this.ParseAdditionalApiParameters();
// Build the list of messages: // Build the list of messages:
var messages = await chatThread.Blocks.BuildMessagesUsingDirectImageUrlAsync(this.Provider, chatModel); var messages = await chatThread.Blocks.BuildMessagesUsingDirectImageUrlAsync(this.Provider, chatModel);
// Prepare the DeepSeek HTTP chat request: return new ChatCompletionAPIRequest
var deepSeekChatRequest = JsonSerializer.Serialize(new ChatCompletionAPIRequest
{ {
Model = chatModel.Id, Model = chatModel.Id,
@ -54,22 +43,9 @@ public sealed class ProviderDeepSeek() : BaseProvider(LLMProviders.DEEP_SEEK, "h
Stream = true, Stream = true,
AdditionalApiParameters = apiParameters AdditionalApiParameters = apiParameters
}, JSON_SERIALIZER_OPTIONS); };
},
async Task<HttpRequestMessage> RequestBuilder() token: token))
{
// 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(deepSeekChatRequest, Encoding.UTF8, "application/json");
return request;
}
await foreach (var content in this.StreamChatCompletionInternal<ChatCompletionDeltaStreamLine, NoChatCompletionAnnotationStreamLine>("DeepSeek", RequestBuilder, token))
yield return content; yield return content;
} }

View File

@ -1,20 +0,0 @@
using System.Text.Json.Serialization;
namespace AIStudio.Provider.Fireworks;
/// <summary>
/// The Fireworks chat request model.
/// </summary>
/// <param name="Model">Which model to use for chat completion.</param>
/// <param name="Messages">The chat messages.</param>
/// <param name="Stream">Whether to stream the chat completion.</param>
public readonly record struct ChatRequest(
string Model,
IList<IMessageBase> Messages,
bool Stream
)
{
// Attention: The "required" modifier is not supported for [JsonExtensionData].
[JsonExtensionData]
public IDictionary<string, object> AdditionalApiParameters { get; init; } = new Dictionary<string, object>();
}

View File

@ -1,7 +1,4 @@
using System.Net.Http.Headers;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Text;
using System.Text.Json;
using AIStudio.Chat; using AIStudio.Chat;
using AIStudio.Provider.OpenAI; using AIStudio.Provider.OpenAI;
@ -24,26 +21,17 @@ public class ProviderFireworks() : BaseProvider(LLMProviders.FIREWORKS, "https:/
/// <inheritdoc /> /// <inheritdoc />
public override async IAsyncEnumerable<ContentStreamChunk> 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: await foreach (var content in this.StreamOpenAICompatibleChatCompletion<ChatCompletionAPIRequest, ResponseStreamLine, ChatCompletionAnnotationStreamLine>(
var requestedSecret = await RUST_SERVICE.GetAPIKey(this, SecretStoreType.LLM_PROVIDER); "Fireworks",
if(!requestedSecret.Success) chatModel,
yield break; chatThread,
settingsManager,
// Prepare the system prompt: async (systemPrompt, apiParameters) =>
var systemPrompt = new TextMessage
{ {
Role = "system",
Content = chatThread.PrepareSystemPrompt(settingsManager),
};
// Parse the API parameters:
var apiParameters = this.ParseAdditionalApiParameters();
// Build the list of messages: // Build the list of messages:
var messages = await chatThread.Blocks.BuildMessagesUsingNestedImageUrlAsync(this.Provider, chatModel); var messages = await chatThread.Blocks.BuildMessagesUsingNestedImageUrlAsync(this.Provider, chatModel);
// Prepare the Fireworks HTTP chat request: return new ChatCompletionAPIRequest
var fireworksChatRequest = JsonSerializer.Serialize(new ChatRequest
{ {
Model = chatModel.Id, Model = chatModel.Id,
@ -55,22 +43,9 @@ public class ProviderFireworks() : BaseProvider(LLMProviders.FIREWORKS, "https:/
// Right now, we only support streaming completions: // Right now, we only support streaming completions:
Stream = true, Stream = true,
AdditionalApiParameters = apiParameters AdditionalApiParameters = apiParameters
}, JSON_SERIALIZER_OPTIONS); };
},
async Task<HttpRequestMessage> RequestBuilder() token: token))
{
// 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(fireworksChatRequest, Encoding.UTF8, "application/json");
return request;
}
await foreach (var content in this.StreamChatCompletionInternal<ResponseStreamLine, ChatCompletionAnnotationStreamLine>("Fireworks", RequestBuilder, token))
yield return content; yield return content;
} }

View File

@ -1,7 +1,5 @@
using System.Net.Http.Headers; using System.Net.Http.Headers;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Text;
using System.Text.Json;
using AIStudio.Chat; using AIStudio.Chat;
using AIStudio.Provider.OpenAI; using AIStudio.Provider.OpenAI;
@ -24,26 +22,17 @@ public sealed class ProviderGWDG() : BaseProvider(LLMProviders.GWDG, "https://ch
/// <inheritdoc /> /// <inheritdoc />
public override async IAsyncEnumerable<ContentStreamChunk> 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: await foreach (var content in this.StreamOpenAICompatibleChatCompletion<ChatCompletionAPIRequest, ChatCompletionDeltaStreamLine, ChatCompletionAnnotationStreamLine>(
var requestedSecret = await RUST_SERVICE.GetAPIKey(this, SecretStoreType.LLM_PROVIDER); "GWDG",
if(!requestedSecret.Success) chatModel,
yield break; chatThread,
settingsManager,
// Prepare the system prompt: async (systemPrompt, apiParameters) =>
var systemPrompt = new TextMessage
{ {
Role = "system",
Content = chatThread.PrepareSystemPrompt(settingsManager),
};
// Parse the API parameters:
var apiParameters = this.ParseAdditionalApiParameters();
// Build the list of messages: // Build the list of messages:
var messages = await chatThread.Blocks.BuildMessagesUsingNestedImageUrlAsync(this.Provider, chatModel); var messages = await chatThread.Blocks.BuildMessagesUsingNestedImageUrlAsync(this.Provider, chatModel);
// Prepare the GWDG HTTP chat request: return new ChatCompletionAPIRequest
var gwdgChatRequest = JsonSerializer.Serialize(new ChatCompletionAPIRequest
{ {
Model = chatModel.Id, Model = chatModel.Id,
@ -54,22 +43,9 @@ public sealed class ProviderGWDG() : BaseProvider(LLMProviders.GWDG, "https://ch
Stream = true, Stream = true,
AdditionalApiParameters = apiParameters AdditionalApiParameters = apiParameters
}, JSON_SERIALIZER_OPTIONS); };
},
async Task<HttpRequestMessage> RequestBuilder() token: token))
{
// 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(gwdgChatRequest, Encoding.UTF8, "application/json");
return request;
}
await foreach (var content in this.StreamChatCompletionInternal<ChatCompletionDeltaStreamLine, ChatCompletionAnnotationStreamLine>("GWDG", RequestBuilder, token))
yield return content; yield return content;
} }

View File

@ -1,20 +0,0 @@
using System.Text.Json.Serialization;
namespace AIStudio.Provider.Google;
/// <summary>
/// The Google chat request model.
/// </summary>
/// <param name="Model">Which model to use for chat completion.</param>
/// <param name="Messages">The chat messages.</param>
/// <param name="Stream">Whether to stream the chat completion.</param>
public readonly record struct ChatRequest(
string Model,
IList<IMessageBase> Messages,
bool Stream
)
{
// Attention: The "required" modifier is not supported for [JsonExtensionData].
[JsonExtensionData]
public IDictionary<string, object> AdditionalApiParameters { get; init; } = new Dictionary<string, object>();
}

View File

@ -24,26 +24,17 @@ public class ProviderGoogle() : BaseProvider(LLMProviders.GOOGLE, "https://gener
/// <inheritdoc /> /// <inheritdoc />
public override async IAsyncEnumerable<ContentStreamChunk> 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: await foreach (var content in this.StreamOpenAICompatibleChatCompletion<ChatCompletionAPIRequest, ChatCompletionDeltaStreamLine, NoChatCompletionAnnotationStreamLine>(
var requestedSecret = await RUST_SERVICE.GetAPIKey(this, SecretStoreType.LLM_PROVIDER); "Google",
if(!requestedSecret.Success) chatModel,
yield break; chatThread,
settingsManager,
// Prepare the system prompt: async (systemPrompt, apiParameters) =>
var systemPrompt = new TextMessage
{ {
Role = "system",
Content = chatThread.PrepareSystemPrompt(settingsManager),
};
// Parse the API parameters:
var apiParameters = this.ParseAdditionalApiParameters();
// Build the list of messages: // Build the list of messages:
var messages = await chatThread.Blocks.BuildMessagesUsingNestedImageUrlAsync(this.Provider, chatModel); var messages = await chatThread.Blocks.BuildMessagesUsingNestedImageUrlAsync(this.Provider, chatModel);
// Prepare the Google HTTP chat request: return new ChatCompletionAPIRequest
var geminiChatRequest = JsonSerializer.Serialize(new ChatRequest
{ {
Model = chatModel.Id, Model = chatModel.Id,
@ -55,22 +46,9 @@ public class ProviderGoogle() : BaseProvider(LLMProviders.GOOGLE, "https://gener
// Right now, we only support streaming completions: // Right now, we only support streaming completions:
Stream = true, Stream = true,
AdditionalApiParameters = apiParameters AdditionalApiParameters = apiParameters
}, JSON_SERIALIZER_OPTIONS); };
},
async Task<HttpRequestMessage> RequestBuilder() token: token))
{
// 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(geminiChatRequest, Encoding.UTF8, "application/json");
return request;
}
await foreach (var content in this.StreamChatCompletionInternal<ChatCompletionDeltaStreamLine, NoChatCompletionAnnotationStreamLine>("Google", RequestBuilder, token))
yield return content; yield return content;
} }

View File

@ -1,22 +0,0 @@
using System.Text.Json.Serialization;
namespace AIStudio.Provider.Groq;
/// <summary>
/// The Groq chat request model.
/// </summary>
/// <param name="Model">Which model to use for chat completion.</param>
/// <param name="Messages">The chat messages.</param>
/// <param name="Stream">Whether to stream the chat completion.</param>
/// <param name="Seed">The seed for the chat completion.</param>
public readonly record struct ChatRequest(
string Model,
IList<IMessageBase> Messages,
bool Stream,
int Seed
)
{
// Attention: The "required" modifier is not supported for [JsonExtensionData].
[JsonExtensionData]
public IDictionary<string, object> AdditionalApiParameters { get; init; } = new Dictionary<string, object>();
}

View File

@ -1,7 +1,5 @@
using System.Net.Http.Headers; using System.Net.Http.Headers;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Text;
using System.Text.Json;
using AIStudio.Chat; using AIStudio.Chat;
using AIStudio.Provider.OpenAI; using AIStudio.Provider.OpenAI;
@ -24,26 +22,20 @@ public class ProviderGroq() : BaseProvider(LLMProviders.GROQ, "https://api.groq.
/// <inheritdoc /> /// <inheritdoc />
public override async IAsyncEnumerable<ContentStreamChunk> 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: await foreach (var content in this.StreamOpenAICompatibleChatCompletion<ChatCompletionAPIRequest, ChatCompletionDeltaStreamLine, ChatCompletionAnnotationStreamLine>(
var requestedSecret = await RUST_SERVICE.GetAPIKey(this, SecretStoreType.LLM_PROVIDER); "Groq",
if(!requestedSecret.Success) chatModel,
yield break; chatThread,
settingsManager,
// Prepare the system prompt: async (systemPrompt, apiParameters) =>
var systemPrompt = new TextMessage
{ {
Role = "system", if (TryPopIntParameter(apiParameters, "seed", out var parsedSeed))
Content = chatThread.PrepareSystemPrompt(settingsManager), apiParameters["seed"] = parsedSeed;
};
// Parse the API parameters:
var apiParameters = this.ParseAdditionalApiParameters();
// Build the list of messages: // Build the list of messages:
var messages = await chatThread.Blocks.BuildMessagesUsingNestedImageUrlAsync(this.Provider, chatModel); var messages = await chatThread.Blocks.BuildMessagesUsingNestedImageUrlAsync(this.Provider, chatModel);
// Prepare the OpenAI HTTP chat request: return new ChatCompletionAPIRequest
var groqChatRequest = JsonSerializer.Serialize(new ChatRequest
{ {
Model = chatModel.Id, Model = chatModel.Id,
@ -55,22 +47,9 @@ public class ProviderGroq() : BaseProvider(LLMProviders.GROQ, "https://api.groq.
// Right now, we only support streaming completions: // Right now, we only support streaming completions:
Stream = true, Stream = true,
AdditionalApiParameters = apiParameters AdditionalApiParameters = apiParameters
}, JSON_SERIALIZER_OPTIONS); };
},
async Task<HttpRequestMessage> RequestBuilder() token: token))
{
// 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(groqChatRequest, Encoding.UTF8, "application/json");
return request;
}
await foreach (var content in this.StreamChatCompletionInternal<ChatCompletionDeltaStreamLine, ChatCompletionAnnotationStreamLine>("Groq", RequestBuilder, token))
yield return content; yield return content;
} }

View File

@ -1,7 +1,5 @@
using System.Net.Http.Headers; using System.Net.Http.Headers;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Text;
using System.Text.Json;
using AIStudio.Chat; using AIStudio.Chat;
using AIStudio.Provider.OpenAI; using AIStudio.Provider.OpenAI;
@ -24,26 +22,17 @@ public sealed class ProviderHelmholtz() : BaseProvider(LLMProviders.HELMHOLTZ, "
/// <inheritdoc /> /// <inheritdoc />
public override async IAsyncEnumerable<ContentStreamChunk> 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: await foreach (var content in this.StreamOpenAICompatibleChatCompletion<ChatCompletionAPIRequest, ChatCompletionDeltaStreamLine, ChatCompletionAnnotationStreamLine>(
var requestedSecret = await RUST_SERVICE.GetAPIKey(this, SecretStoreType.LLM_PROVIDER); "Helmholtz",
if(!requestedSecret.Success) chatModel,
yield break; chatThread,
settingsManager,
// Prepare the system prompt: async (systemPrompt, apiParameters) =>
var systemPrompt = new TextMessage
{ {
Role = "system",
Content = chatThread.PrepareSystemPrompt(settingsManager),
};
// Parse the API parameters:
var apiParameters = this.ParseAdditionalApiParameters();
// Build the list of messages: // Build the list of messages:
var messages = await chatThread.Blocks.BuildMessagesUsingNestedImageUrlAsync(this.Provider, chatModel); var messages = await chatThread.Blocks.BuildMessagesUsingNestedImageUrlAsync(this.Provider, chatModel);
// Prepare the Helmholtz HTTP chat request: return new ChatCompletionAPIRequest
var helmholtzChatRequest = JsonSerializer.Serialize(new ChatCompletionAPIRequest
{ {
Model = chatModel.Id, Model = chatModel.Id,
@ -54,22 +43,9 @@ public sealed class ProviderHelmholtz() : BaseProvider(LLMProviders.HELMHOLTZ, "
Stream = true, Stream = true,
AdditionalApiParameters = apiParameters AdditionalApiParameters = apiParameters
}, JSON_SERIALIZER_OPTIONS); };
},
async Task<HttpRequestMessage> RequestBuilder() token: token))
{
// 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(helmholtzChatRequest, Encoding.UTF8, "application/json");
return request;
}
await foreach (var content in this.StreamChatCompletionInternal<ChatCompletionDeltaStreamLine, ChatCompletionAnnotationStreamLine>("Helmholtz", RequestBuilder, token))
yield return content; yield return content;
} }

View File

@ -1,7 +1,4 @@
using System.Net.Http.Headers; using System.Runtime.CompilerServices;
using System.Runtime.CompilerServices;
using System.Text;
using System.Text.Json;
using AIStudio.Chat; using AIStudio.Chat;
using AIStudio.Provider.OpenAI; using AIStudio.Provider.OpenAI;
@ -29,52 +26,30 @@ public sealed class ProviderHuggingFace : BaseProvider
/// <inheritdoc /> /// <inheritdoc />
public override async IAsyncEnumerable<ContentStreamChunk> 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: await foreach (var content in this.StreamOpenAICompatibleChatCompletion<ChatCompletionAPIRequest, ChatCompletionDeltaStreamLine, ChatCompletionAnnotationStreamLine>(
var requestedSecret = await RUST_SERVICE.GetAPIKey(this, SecretStoreType.LLM_PROVIDER); "HuggingFace",
if(!requestedSecret.Success) chatModel,
yield break; chatThread,
settingsManager,
// Prepare the system prompt: async (systemPrompt, apiParameters) =>
var systemPrompt = new TextMessage
{ {
Role = "system",
Content = chatThread.PrepareSystemPrompt(settingsManager),
};
// Parse the API parameters:
var apiParameters = this.ParseAdditionalApiParameters();
// Build the list of messages: // Build the list of messages:
var message = await chatThread.Blocks.BuildMessagesUsingNestedImageUrlAsync(this.Provider, chatModel); var messages = await chatThread.Blocks.BuildMessagesUsingNestedImageUrlAsync(this.Provider, chatModel);
// Prepare the HuggingFace HTTP chat request: return new ChatCompletionAPIRequest
var huggingfaceChatRequest = JsonSerializer.Serialize(new ChatCompletionAPIRequest
{ {
Model = chatModel.Id, Model = chatModel.Id,
// Build the messages: // Build the messages:
// - First of all the system prompt // - First of all the system prompt
// - Then none-empty user and AI messages // - Then none-empty user and AI messages
Messages = [systemPrompt, ..message], Messages = [systemPrompt, ..messages],
Stream = true, Stream = true,
AdditionalApiParameters = apiParameters AdditionalApiParameters = apiParameters
}, JSON_SERIALIZER_OPTIONS); };
},
async Task<HttpRequestMessage> RequestBuilder() token: token))
{
// 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(huggingfaceChatRequest, Encoding.UTF8, "application/json");
return request;
}
await foreach (var content in this.StreamChatCompletionInternal<ChatCompletionDeltaStreamLine, ChatCompletionAnnotationStreamLine>("HuggingFace", RequestBuilder, token))
yield return content; yield return content;
} }

View File

@ -1,25 +0,0 @@
using System.Text.Json.Serialization;
namespace AIStudio.Provider.Mistral;
/// <summary>
/// The OpenAI chat request model.
/// </summary>
/// <param name="Model">Which model to use for chat completion.</param>
/// <param name="Messages">The chat messages.</param>
/// <param name="Stream">Whether to stream the chat completion.</param>
/// <param name="RandomSeed">The seed for the chat completion.</param>
/// <param name="SafePrompt">Whether to inject a safety prompt before all conversations.</param>
public readonly record struct ChatRequest(
string Model,
IList<IMessageBase> Messages,
bool Stream,
[property: JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
int? RandomSeed,
bool SafePrompt = false
)
{
// Attention: The "required" modifier is not supported for [JsonExtensionData].
[JsonExtensionData]
public IDictionary<string, object> AdditionalApiParameters { get; init; } = new Dictionary<string, object>();
}

View File

@ -1,7 +1,5 @@
using System.Net.Http.Headers; using System.Net.Http.Headers;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Text;
using System.Text.Json;
using AIStudio.Chat; using AIStudio.Chat;
using AIStudio.Provider.OpenAI; using AIStudio.Provider.OpenAI;
@ -22,28 +20,23 @@ public sealed class ProviderMistral() : BaseProvider(LLMProviders.MISTRAL, "http
/// <inheritdoc /> /// <inheritdoc />
public override async IAsyncEnumerable<ContentStreamChunk> 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: await foreach (var content in this.StreamOpenAICompatibleChatCompletion<ChatCompletionAPIRequest, ChatCompletionDeltaStreamLine, NoChatCompletionAnnotationStreamLine>(
var requestedSecret = await RUST_SERVICE.GetAPIKey(this, SecretStoreType.LLM_PROVIDER); "Mistral",
if(!requestedSecret.Success) chatModel,
yield break; chatThread,
settingsManager,
// Prepare the system prompt: async (systemPrompt, apiParameters) =>
var systemPrompt = new TextMessage
{ {
Role = "system", if (TryPopBoolParameter(apiParameters, "safe_prompt", out var parsedSafePrompt))
Content = chatThread.PrepareSystemPrompt(settingsManager), apiParameters["safe_prompt"] = parsedSafePrompt;
};
// Parse the API parameters: if (TryPopIntParameter(apiParameters, "random_seed", out var parsedRandomSeed))
var apiParameters = this.ParseAdditionalApiParameters(); apiParameters["random_seed"] = parsedRandomSeed;
var safePrompt = TryPopBoolParameter(apiParameters, "safe_prompt", out var parsedSafePrompt) && parsedSafePrompt;
var randomSeed = TryPopIntParameter(apiParameters, "random_seed", out var parsedRandomSeed) ? parsedRandomSeed : (int?)null;
// Build the list of messages: // Build the list of messages:
var messages = await chatThread.Blocks.BuildMessagesUsingDirectImageUrlAsync(this.Provider, chatModel); var messages = await chatThread.Blocks.BuildMessagesUsingDirectImageUrlAsync(this.Provider, chatModel);
// Prepare the Mistral HTTP chat request: return new ChatCompletionAPIRequest
var mistralChatRequest = JsonSerializer.Serialize(new ChatRequest
{ {
Model = chatModel.Id, Model = chatModel.Id,
@ -54,26 +47,10 @@ public sealed class ProviderMistral() : BaseProvider(LLMProviders.MISTRAL, "http
// Right now, we only support streaming completions: // Right now, we only support streaming completions:
Stream = true, Stream = true,
RandomSeed = randomSeed,
SafePrompt = safePrompt,
AdditionalApiParameters = apiParameters AdditionalApiParameters = apiParameters
}, JSON_SERIALIZER_OPTIONS); };
},
token: token))
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(mistralChatRequest, Encoding.UTF8, "application/json");
return request;
}
await foreach (var content in this.StreamChatCompletionInternal<ChatCompletionDeltaStreamLine, NoChatCompletionAnnotationStreamLine>("Mistral", RequestBuilder, token))
yield return content; yield return content;
} }

View File

@ -79,9 +79,9 @@ public sealed class ProviderOpenAI() : BaseProvider(LLMProviders.OPEN_AI, "https
// //
// Prepare the tools we want to use: // Prepare the tools we want to use:
// //
IList<Tool> tools = modelCapabilities.Contains(Capability.WEB_SEARCH) switch IList<ProviderTool> providerTools = modelCapabilities.Contains(Capability.WEB_SEARCH) switch
{ {
true => [ Tools.WEB_SEARCH ], true => [ ProviderTools.WEB_SEARCH ],
_ => [] _ => []
}; };
@ -178,7 +178,7 @@ public sealed class ProviderOpenAI() : BaseProvider(LLMProviders.OPEN_AI, "https
Store = false, Store = false,
// Tools we want to use: // Tools we want to use:
Tools = tools, ProviderTools = providerTools,
// Additional API parameters: // Additional API parameters:
AdditionalApiParameters = apiParameters AdditionalApiParameters = apiParameters

View File

@ -1,7 +1,7 @@
namespace AIStudio.Provider.OpenAI; namespace AIStudio.Provider.OpenAI;
/// <summary> /// <summary>
/// Represents a tool used by the AI model. /// Represents a tool executed on the provider side.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// Right now, only our OpenAI provider is using tools. Thus, this class is located in the /// Right now, only our OpenAI provider is using tools. Thus, this class is located in the
@ -9,4 +9,4 @@ namespace AIStudio.Provider.OpenAI;
/// be moved into the provider namespace. /// be moved into the provider namespace.
/// </remarks> /// </remarks>
/// <param name="Type">The type of the tool.</param> /// <param name="Type">The type of the tool.</param>
public record Tool(string Type); public record ProviderTool(string Type);

View File

@ -1,14 +1,14 @@
namespace AIStudio.Provider.OpenAI; namespace AIStudio.Provider.OpenAI;
/// <summary> /// <summary>
/// Known tools for LLM providers. /// Known provider-side tools for LLM providers.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// Right now, only our OpenAI provider is using tools. Thus, this class is located in the /// Right now, only our OpenAI provider is using tools. Thus, this class is located in the
/// OpenAI namespace. In the future, when other providers also support tools, this class can /// OpenAI namespace. In the future, when other providers also support tools, this class can
/// be moved into the provider namespace. /// be moved into the provider namespace.
/// </remarks> /// </remarks>
public static class Tools public static class ProviderTools
{ {
public static readonly Tool WEB_SEARCH = new("web_search"); public static readonly ProviderTool WEB_SEARCH = new("web_search");
} }

View File

@ -9,13 +9,13 @@ namespace AIStudio.Provider.OpenAI;
/// <param name="Input">The chat messages.</param> /// <param name="Input">The chat messages.</param>
/// <param name="Stream">Whether to stream the response.</param> /// <param name="Stream">Whether to stream the response.</param>
/// <param name="Store">Whether to store the response on the server (usually OpenAI's infrastructure).</param> /// <param name="Store">Whether to store the response on the server (usually OpenAI's infrastructure).</param>
/// <param name="Tools">The tools to use for the request.</param> /// <param name="ProviderTools">The provider-side tools to use for the request.</param>
public record ResponsesAPIRequest( public record ResponsesAPIRequest(
string Model, string Model,
IList<IMessageBase> Input, IList<IMessageBase> Input,
bool Stream, bool Stream,
bool Store, bool Store,
IList<Tool> Tools) [property: JsonPropertyName("tools")] IList<ProviderTool> ProviderTools)
{ {
public ResponsesAPIRequest() : this(string.Empty, [], true, false, []) public ResponsesAPIRequest() : this(string.Empty, [], true, false, [])
{ {

View File

@ -1,7 +1,5 @@
using System.Net.Http.Headers; using System.Net.Http.Headers;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Text;
using System.Text.Json;
using AIStudio.Chat; using AIStudio.Chat;
using AIStudio.Provider.OpenAI; using AIStudio.Provider.OpenAI;
@ -27,26 +25,17 @@ public sealed class ProviderOpenRouter() : BaseProvider(LLMProviders.OPEN_ROUTER
/// <inheritdoc /> /// <inheritdoc />
public override async IAsyncEnumerable<ContentStreamChunk> 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: await foreach (var content in this.StreamOpenAICompatibleChatCompletion<ChatCompletionAPIRequest, ChatCompletionDeltaStreamLine, NoChatCompletionAnnotationStreamLine>(
var requestedSecret = await RUST_SERVICE.GetAPIKey(this, SecretStoreType.LLM_PROVIDER); "OpenRouter",
if(!requestedSecret.Success) chatModel,
yield break; chatThread,
settingsManager,
// Prepare the system prompt: async (systemPrompt, apiParameters) =>
var systemPrompt = new TextMessage
{ {
Role = "system",
Content = chatThread.PrepareSystemPrompt(settingsManager),
};
// Parse the API parameters:
var apiParameters = this.ParseAdditionalApiParameters();
// Build the list of messages: // Build the list of messages:
var messages = await chatThread.Blocks.BuildMessagesUsingNestedImageUrlAsync(this.Provider, chatModel); var messages = await chatThread.Blocks.BuildMessagesUsingNestedImageUrlAsync(this.Provider, chatModel);
// Prepare the OpenRouter HTTP chat request: return new ChatCompletionAPIRequest
var openRouterChatRequest = JsonSerializer.Serialize(new ChatCompletionAPIRequest
{ {
Model = chatModel.Id, Model = chatModel.Id,
@ -58,26 +47,15 @@ public sealed class ProviderOpenRouter() : BaseProvider(LLMProviders.OPEN_ROUTER
// Right now, we only support streaming completions: // Right now, we only support streaming completions:
Stream = true, Stream = true,
AdditionalApiParameters = apiParameters AdditionalApiParameters = apiParameters
}, JSON_SERIALIZER_OPTIONS); };
},
async Task<HttpRequestMessage> RequestBuilder() headersAction: headers =>
{ {
// 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 custom headers for project identification: // Set custom headers for project identification:
request.Headers.Add("HTTP-Referer", PROJECT_WEBSITE); headers.Add("HTTP-Referer", PROJECT_WEBSITE);
request.Headers.Add("X-Title", PROJECT_NAME); headers.Add("X-Title", PROJECT_NAME);
},
// Set the content: token: token))
request.Content = new StringContent(openRouterChatRequest, Encoding.UTF8, "application/json");
return request;
}
await foreach (var content in this.StreamChatCompletionInternal<ChatCompletionDeltaStreamLine, NoChatCompletionAnnotationStreamLine>("OpenRouter", RequestBuilder, token))
yield return content; yield return content;
} }

View File

@ -1,7 +1,4 @@
using System.Net.Http.Headers;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Text;
using System.Text.Json;
using AIStudio.Chat; using AIStudio.Chat;
using AIStudio.Provider.OpenAI; using AIStudio.Provider.OpenAI;
@ -33,26 +30,17 @@ public sealed class ProviderPerplexity() : BaseProvider(LLMProviders.PERPLEXITY,
/// <inheritdoc /> /// <inheritdoc />
public override async IAsyncEnumerable<ContentStreamChunk> 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: await foreach (var content in this.StreamOpenAICompatibleChatCompletion<ChatCompletionAPIRequest, ResponseStreamLine, NoChatCompletionAnnotationStreamLine>(
var requestedSecret = await RUST_SERVICE.GetAPIKey(this, SecretStoreType.LLM_PROVIDER); "Perplexity",
if(!requestedSecret.Success) chatModel,
yield break; chatThread,
settingsManager,
// Prepare the system prompt: async (systemPrompt, apiParameters) =>
var systemPrompt = new TextMessage
{ {
Role = "system",
Content = chatThread.PrepareSystemPrompt(settingsManager),
};
// Parse the API parameters:
var apiParameters = this.ParseAdditionalApiParameters();
// Build the list of messages: // Build the list of messages:
var messages = await chatThread.Blocks.BuildMessagesUsingNestedImageUrlAsync(this.Provider, chatModel); var messages = await chatThread.Blocks.BuildMessagesUsingNestedImageUrlAsync(this.Provider, chatModel);
// Prepare the Perplexity HTTP chat request: return new ChatCompletionAPIRequest
var perplexityChatRequest = JsonSerializer.Serialize(new ChatCompletionAPIRequest
{ {
Model = chatModel.Id, Model = chatModel.Id,
@ -62,22 +50,9 @@ public sealed class ProviderPerplexity() : BaseProvider(LLMProviders.PERPLEXITY,
Messages = [systemPrompt, ..messages], Messages = [systemPrompt, ..messages],
Stream = true, Stream = true,
AdditionalApiParameters = apiParameters AdditionalApiParameters = apiParameters
}, JSON_SERIALIZER_OPTIONS); };
},
async Task<HttpRequestMessage> RequestBuilder() token: token))
{
// 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, NoChatCompletionAnnotationStreamLine>("Perplexity", RequestBuilder, token))
yield return content; yield return content;
} }

View File

@ -1,20 +0,0 @@
using System.Text.Json.Serialization;
namespace AIStudio.Provider.SelfHosted;
/// <summary>
/// The chat request model.
/// </summary>
/// <param name="Model">Which model to use for chat completion.</param>
/// <param name="Messages">The chat messages.</param>
/// <param name="Stream">Whether to stream the chat completion.</param>
public readonly record struct ChatRequest(
string Model,
IList<IMessageBase> Messages,
bool Stream
)
{
// Attention: The "required" modifier is not supported for [JsonExtensionData].
[JsonExtensionData]
public IDictionary<string, object> AdditionalApiParameters { get; init; } = new Dictionary<string, object>();
}

View File

@ -1,7 +1,5 @@
using System.Net.Http.Headers; using System.Net.Http.Headers;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Text;
using System.Text.Json;
using AIStudio.Chat; using AIStudio.Chat;
using AIStudio.Provider.OpenAI; using AIStudio.Provider.OpenAI;
@ -25,19 +23,13 @@ public sealed class ProviderSelfHosted(Host host, string hostname) : BaseProvide
/// <inheritdoc /> /// <inheritdoc />
public override async IAsyncEnumerable<ContentStreamChunk> 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: await foreach (var content in this.StreamOpenAICompatibleChatCompletion<ChatCompletionAPIRequest, ChatCompletionDeltaStreamLine, ChatCompletionAnnotationStreamLine>(
var requestedSecret = await RUST_SERVICE.GetAPIKey(this, SecretStoreType.LLM_PROVIDER, isTrying: true); "self-hosted provider",
chatModel,
// Prepare the system prompt: chatThread,
var systemPrompt = new TextMessage settingsManager,
async (systemPrompt, apiParameters) =>
{ {
Role = "system",
Content = chatThread.PrepareSystemPrompt(settingsManager),
};
// Parse the API parameters:
var apiParameters = this.ParseAdditionalApiParameters();
// Build the list of messages. The image format depends on the host: // Build the list of messages. The image format depends on the host:
// - Ollama uses the direct image URL format: { "type": "image_url", "image_url": "data:..." } // - Ollama uses the direct image URL format: { "type": "image_url", "image_url": "data:..." }
// - LM Studio, vLLM, and llama.cpp use the nested image URL format: { "type": "image_url", "image_url": { "url": "data:..." } } // - LM Studio, vLLM, and llama.cpp use the nested image URL format: { "type": "image_url", "image_url": { "url": "data:..." } }
@ -47,8 +39,7 @@ public sealed class ProviderSelfHosted(Host host, string hostname) : BaseProvide
_ => await chatThread.Blocks.BuildMessagesUsingNestedImageUrlAsync(this.Provider, chatModel), _ => await chatThread.Blocks.BuildMessagesUsingNestedImageUrlAsync(this.Provider, chatModel),
}; };
// Prepare the OpenAI HTTP chat request: return new ChatCompletionAPIRequest
var providerChatRequest = JsonSerializer.Serialize(new ChatRequest
{ {
Model = chatModel.Id, Model = chatModel.Id,
@ -60,23 +51,11 @@ public sealed class ProviderSelfHosted(Host host, string hostname) : BaseProvide
// Right now, we only support streaming completions: // Right now, we only support streaming completions:
Stream = true, Stream = true,
AdditionalApiParameters = apiParameters AdditionalApiParameters = apiParameters
}, JSON_SERIALIZER_OPTIONS); };
},
async Task<HttpRequestMessage> RequestBuilder() isTryingSecret: true,
{ requestPath: host.ChatURL(),
// Build the HTTP post request: token: token))
var request = new HttpRequestMessage(HttpMethod.Post, host.ChatURL());
// Set the authorization header:
if (requestedSecret.Success)
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", await requestedSecret.Secret.Decrypt(ENCRYPTION));
// Set the content:
request.Content = new StringContent(providerChatRequest, Encoding.UTF8, "application/json");
return request;
}
await foreach (var content in this.StreamChatCompletionInternal<ChatCompletionDeltaStreamLine, ChatCompletionAnnotationStreamLine>("self-hosted provider", RequestBuilder, token))
yield return content; yield return content;
} }

View File

@ -1,7 +1,5 @@
using System.Net.Http.Headers; using System.Net.Http.Headers;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Text;
using System.Text.Json;
using AIStudio.Chat; using AIStudio.Chat;
using AIStudio.Provider.OpenAI; using AIStudio.Provider.OpenAI;
@ -24,26 +22,17 @@ public sealed class ProviderX() : BaseProvider(LLMProviders.X, "https://api.x.ai
/// <inheritdoc /> /// <inheritdoc />
public override async IAsyncEnumerable<ContentStreamChunk> 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: await foreach (var content in this.StreamOpenAICompatibleChatCompletion<ChatCompletionAPIRequest, ChatCompletionDeltaStreamLine, NoChatCompletionAnnotationStreamLine>(
var requestedSecret = await RUST_SERVICE.GetAPIKey(this, SecretStoreType.LLM_PROVIDER); "xAI",
if(!requestedSecret.Success) chatModel,
yield break; chatThread,
settingsManager,
// Prepare the system prompt: async (systemPrompt, apiParameters) =>
var systemPrompt = new TextMessage
{ {
Role = "system",
Content = chatThread.PrepareSystemPrompt(settingsManager),
};
// Parse the API parameters:
var apiParameters = this.ParseAdditionalApiParameters();
// Build the list of messages: // Build the list of messages:
var messages = await chatThread.Blocks.BuildMessagesUsingNestedImageUrlAsync(this.Provider, chatModel); var messages = await chatThread.Blocks.BuildMessagesUsingNestedImageUrlAsync(this.Provider, chatModel);
// Prepare the xAI HTTP chat request: return new ChatCompletionAPIRequest
var xChatRequest = JsonSerializer.Serialize(new ChatCompletionAPIRequest
{ {
Model = chatModel.Id, Model = chatModel.Id,
@ -55,22 +44,9 @@ public sealed class ProviderX() : BaseProvider(LLMProviders.X, "https://api.x.ai
// Right now, we only support streaming completions: // Right now, we only support streaming completions:
Stream = true, Stream = true,
AdditionalApiParameters = apiParameters AdditionalApiParameters = apiParameters
}, JSON_SERIALIZER_OPTIONS); };
},
async Task<HttpRequestMessage> RequestBuilder() token: token))
{
// 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(xChatRequest, Encoding.UTF8, "application/json");
return request;
}
await foreach (var content in this.StreamChatCompletionInternal<ChatCompletionDeltaStreamLine, NoChatCompletionAnnotationStreamLine>("xAI", RequestBuilder, token))
yield return content; yield return content;
} }

View File

@ -26,6 +26,7 @@
- Improved the validation of additional API parameters in the advanced provider settings to help catch formatting mistakes earlier. - Improved the validation of additional API parameters in the advanced provider settings to help catch formatting mistakes earlier.
- Improved the app startup resilience by allowing AI Studio to continue without Qdrant if it fails to initialize. - Improved the app startup resilience by allowing AI Studio to continue without Qdrant if it fails to initialize.
- Improved the translation assistant by updating the system and user prompts. - Improved the translation assistant by updating the system and user prompts.
- Improved OpenAI-compatible providers by refactoring their streaming request handling to be more consistent and reliable.
- Fixed an issue where assistants hidden via configuration plugins still appear in "Send to ..." menus. Thanks, Gunnar, for reporting this issue. - Fixed an issue where assistants hidden via configuration plugins still appear in "Send to ..." menus. Thanks, Gunnar, for reporting this issue.
- Fixed an issue with chat templates that could stop working because the stored validation result for attached files was reused. AI Studio now checks attached files again when you use a chat template. - Fixed an issue with chat templates that could stop working because the stored validation result for attached files was reused. AI Studio now checks attached files again when you use a chat template.
- Fixed an issue with voice recording where AI Studio could log errors and keep the feature available even though required parts failed to initialize. Voice recording is now disabled automatically for the current session in that case. - Fixed an issue with voice recording where AI Studio could log errors and keep the feature available even though required parts failed to initialize. Voice recording is now disabled automatically for the current session in that case.