Merge Error Fixes

This commit is contained in:
Peer Schütt 2026-06-03 16:43:06 +02:00
parent 5ae8d7e2a7
commit 1c0c2a3855
10 changed files with 69 additions and 78 deletions

View File

@ -165,7 +165,7 @@
@if (this.SettingsManager.IsToolSelectionVisible(this.Component)) @if (this.SettingsManager.IsToolSelectionVisible(this.Component))
{ {
<ToolSelection Component="@this.Component" LLMProvider="@this.providerSettings" SelectedToolIds="@this.selectedToolIds" SelectedToolIdsChanged="@this.SelectedToolIdsChanged" Disabled="@this.isProcessing" /> <ToolSelection Component="@this.Component" LLMProvider="@this.ProviderSettings" SelectedToolIds="@this.selectedToolIds" SelectedToolIdsChanged="@this.SelectedToolIdsChanged" Disabled="@this.isProcessing" />
} }
<MudSpacer /> <MudSpacer />

View File

@ -124,7 +124,7 @@
<ProfileSelection MarginLeft="" CurrentProfile="@this.currentProfile" CurrentProfileChanged="@this.ProfileWasChanged" Disabled="@(!this.currentChatTemplate.AllowProfileUsage)" DisabledText="@T("Profile usage is disabled according to your chat template settings.")"/> <ProfileSelection MarginLeft="" CurrentProfile="@this.currentProfile" CurrentProfileChanged="@this.ProfileWasChanged" Disabled="@(!this.currentChatTemplate.AllowProfileUsage)" DisabledText="@T("Profile usage is disabled according to your chat template settings.")"/>
<ToolSelection Component="Components.CHAT" LLMProvider="@this.Provider" SelectedToolIds="@this.selectedToolIds" SelectedToolIdsChanged="@this.SelectedToolIdsChanged" Disabled="@this.isStreaming" /> <ToolSelection Component="Components.CHAT" LLMProvider="@this.Provider" SelectedToolIds="@this.selectedToolIds" SelectedToolIdsChanged="@this.SelectedToolIdsChanged" Disabled="@this.IsCurrentChatStreaming" />
@if (PreviewFeatures.PRE_RAG_2024.IsEnabled(this.SettingsManager)) @if (PreviewFeatures.PRE_RAG_2024.IsEnabled(this.SettingsManager))
{ {

View File

@ -78,6 +78,7 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
private Guid loadedParameterWorkspaceId = Guid.Empty; private Guid loadedParameterWorkspaceId = Guid.Empty;
private Guid foregroundChatId = Guid.Empty; private Guid foregroundChatId = Guid.Empty;
private int workspaceHeaderSyncVersion; private int workspaceHeaderSyncVersion;
private CancellationTokenSource? cancellationTokenSource;
// Unfortunately, we need the input field reference to blur the focus away. Without // Unfortunately, we need the input field reference to blur the focus away. Without
// this, we cannot clear the input field. // this, we cannot clear the input field.
@ -702,7 +703,7 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
// ProviderSettings = this.Provider, // ProviderSettings = this.Provider,
// IsForeground = true, // IsForeground = true,
//}); //});
using (this.cancellationTokenSource = new()) using (this.cancellationTokenSource = new CancellationTokenSource())
{ {
this.StateHasChanged(); this.StateHasChanged();
this.ChatThread!.RuntimeComponent = Tools.Components.CHAT; this.ChatThread!.RuntimeComponent = Tools.Components.CHAT;
@ -719,12 +720,8 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
// Save the chat: // Save the chat:
if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_AUTOMATICALLY) if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_AUTOMATICALLY)
{ {
ChatThread = this.ChatThread!, await this.SaveThread();
AIText = aiText, }
LastUserPrompt = lastUserPrompt,
ProviderSettings = this.Provider,
IsForeground = true,
});
await this.SyncForegroundChatAsync(); await this.SyncForegroundChatAsync();
this.StateHasChanged(); this.StateHasChanged();
@ -1133,4 +1130,4 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
} }
#endregion #endregion
} }

View File

@ -957,7 +957,6 @@ public abstract class BaseProvider : IProvider, ISecretId
/// <param name="chatModel">The selected chat model.</param> /// <param name="chatModel">The selected chat model.</param>
/// <param name="chatThread">The current chat thread.</param> /// <param name="chatThread">The current chat thread.</param>
/// <param name="settingsManager">The settings manager.</param> /// <param name="settingsManager">The settings manager.</param>
/// <param name="messagesFactory">Builds the provider-specific base messages.</param>
/// <param name="requestFactory">Builds the provider-specific request body.</param> /// <param name="requestFactory">Builds the provider-specific request body.</param>
/// <param name="storeType">The secret store type.</param> /// <param name="storeType">The secret store type.</param>
/// <param name="isTryingSecret">Whether the API key is optional.</param> /// <param name="isTryingSecret">Whether the API key is optional.</param>
@ -969,19 +968,19 @@ public abstract class BaseProvider : IProvider, ISecretId
/// <typeparam name="TDelta">The delta stream line type.</typeparam> /// <typeparam name="TDelta">The delta stream line type.</typeparam>
/// <typeparam name="TAnnotation">The annotation stream line type.</typeparam> /// <typeparam name="TAnnotation">The annotation stream line type.</typeparam>
/// <returns>The streamed content chunks.</returns> /// <returns>The streamed content chunks.</returns>
protected async IAsyncEnumerable<ContentStreamChunk> StreamOpenAICompatibleChatCompletion<TDelta, TAnnotation>( protected async IAsyncEnumerable<ContentStreamChunk> StreamOpenAICompatibleChatCompletion<TRequest, TDelta, TAnnotation>(
string providerName, string providerName,
Model chatModel, Model chatModel,
ChatThread chatThread, ChatThread chatThread,
SettingsManager settingsManager, SettingsManager settingsManager,
Func<Task<IList<IMessageBase>>> messagesFactory, Func<TextMessage, IDictionary<string, object>, IList<object>?, Task<TRequest>> requestFactory,
Func<TextMessage, IDictionary<string, object>, Task<TRequest>> requestFactory,
SecretStoreType storeType = SecretStoreType.LLM_PROVIDER, SecretStoreType storeType = SecretStoreType.LLM_PROVIDER,
bool isTryingSecret = false, bool isTryingSecret = false,
string systemPromptRole = "system", string systemPromptRole = "system",
string requestPath = "chat/completions", string requestPath = "chat/completions",
Action<HttpRequestHeaders>? headersAction = null, Action<HttpRequestHeaders>? headersAction = null,
[EnumeratorCancellation] CancellationToken token = default) [EnumeratorCancellation] CancellationToken token = default)
where TRequest : ChatCompletionAPIRequest
where TDelta : IResponseStreamLine where TDelta : IResponseStreamLine
where TAnnotation : IAnnotationStreamLine where TAnnotation : IAnnotationStreamLine
{ {
@ -1000,7 +999,6 @@ public abstract class BaseProvider : IProvider, ISecretId
// Parse the API parameters: // Parse the API parameters:
var apiParameters = this.ParseAdditionalApiParameters(); var apiParameters = this.ParseAdditionalApiParameters();
var baseMessages = await messagesFactory();
var toolRegistry = Program.SERVICE_PROVIDER.GetService<ToolRegistry>(); var toolRegistry = Program.SERVICE_PROVIDER.GetService<ToolRegistry>();
var toolExecutor = Program.SERVICE_PROVIDER.GetService<ToolExecutor>(); var toolExecutor = Program.SERVICE_PROVIDER.GetService<ToolExecutor>();
var currentAssistantContent = chatThread.Blocks.LastOrDefault(x => x.Role is ChatRole.AI)?.Content as ContentText; var currentAssistantContent = chatThread.Blocks.LastOrDefault(x => x.Role is ChatRole.AI)?.Content as ContentText;
@ -1023,7 +1021,12 @@ public abstract class BaseProvider : IProvider, ISecretId
var toolCallCount = 0; var toolCallCount = 0;
while (true) while (true)
{ {
var requestDto = await requestFactory(systemPrompt, [..baseMessages, ..internalMessages], apiParameters, false, providerTools); ChatCompletionAPIRequest requestDtoBase = await requestFactory(systemPrompt, apiParameters, providerTools);
var requestDto = requestDtoBase with
{
Messages = [..requestDtoBase.Messages, ..internalMessages],
Stream = false,
};
var response = await this.ExecuteChatCompletionRequest(requestDto, requestPath, requestedSecret, headersAction, token); var response = await this.ExecuteChatCompletionRequest(requestDto, requestPath, requestedSecret, headersAction, token);
var responseMessage = response?.Choices.FirstOrDefault()?.Message; var responseMessage = response?.Choices.FirstOrDefault()?.Message;
if (responseMessage is null) if (responseMessage is null)
@ -1106,7 +1109,7 @@ public abstract class BaseProvider : IProvider, ISecretId
} }
// Prepare the provider HTTP chat request: // Prepare the provider HTTP chat request:
var providerChatRequest = JsonSerializer.Serialize(await requestFactory(systemPrompt, baseMessages, apiParameters, true, null), JSON_SERIALIZER_OPTIONS); var providerChatRequest = JsonSerializer.Serialize(await requestFactory(systemPrompt, apiParameters, null), JSON_SERIALIZER_OPTIONS);
async Task<HttpRequestMessage> RequestBuilder() async Task<HttpRequestMessage> RequestBuilder()
{ {
@ -1143,7 +1146,7 @@ public abstract class BaseProvider : IProvider, ISecretId
headersAction?.Invoke(request.Headers); headersAction?.Invoke(request.Headers);
request.Content = new StringContent(JsonSerializer.Serialize(requestDto, JSON_SERIALIZER_OPTIONS), Encoding.UTF8, "application/json"); request.Content = new StringContent(JsonSerializer.Serialize(requestDto, JSON_SERIALIZER_OPTIONS), Encoding.UTF8, "application/json");
using var response = await this.httpClient.SendAsync(request, token); using var response = await this.HttpClient.SendAsync(request, token);
if (!response.IsSuccessStatusCode) if (!response.IsSuccessStatusCode)
{ {
var responseBody = await response.Content.ReadAsStringAsync(token); var responseBody = await response.Content.ReadAsStringAsync(token);
@ -1477,4 +1480,4 @@ public abstract class BaseProvider : IProvider, ISecretId
_ => string.Empty, _ => string.Empty,
}; };
} }

View File

@ -12,7 +12,6 @@ using AIStudio.Tools.ToolCallingSystem;
using AIStudio.Tools.Services; using AIStudio.Tools.Services;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using AIStudio.Tools.PluginSystem;
namespace AIStudio.Provider.OpenAI; namespace AIStudio.Provider.OpenAI;
@ -24,8 +23,6 @@ public sealed class ProviderOpenAI() : BaseProvider(LLMProviders.OPEN_AI, new Ur
private static readonly ILogger<ProviderOpenAI> LOGGER = Program.LOGGER_FACTORY.CreateLogger<ProviderOpenAI>(); private static readonly ILogger<ProviderOpenAI> LOGGER = Program.LOGGER_FACTORY.CreateLogger<ProviderOpenAI>();
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(ProviderOpenAI).Namespace, nameof(ProviderOpenAI)); private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(ProviderOpenAI).Namespace, nameof(ProviderOpenAI));
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(ProviderOpenAI).Namespace, nameof(ProviderOpenAI));
#region Implementation of IProvider #region Implementation of IProvider
/// <inheritdoc /> /// <inheritdoc />
@ -121,44 +118,48 @@ public sealed class ProviderOpenAI() : BaseProvider(LLMProviders.OPEN_AI, new Ur
if (!usingResponsesAPI) if (!usingResponsesAPI)
{ {
await foreach (var content in this.StreamOpenAICompatibleChatCompletion<ChatCompletionDeltaStreamLine, ChatCompletionAnnotationStreamLine>( await foreach (var content in this.StreamOpenAICompatibleChatCompletion<ChatCompletionAPIRequest, ChatCompletionDeltaStreamLine, ChatCompletionAnnotationStreamLine>(
"OpenAI", "OpenAI",
chatModel, chatModel,
chatThread, chatThread,
settingsManager, settingsManager,
() => chatThread.Blocks.BuildMessagesAsync( async (systemPrompt, apiParameters, tools) =>
this.Provider,
chatModel,
role => role switch
{
ChatRole.USER => "user",
ChatRole.AI => "assistant",
ChatRole.AGENT => "assistant",
ChatRole.SYSTEM => systemPromptRole,
_ => "user",
},
text => new SubContentText
{
Text = text,
},
async attachment => new SubContentImageUrlNested
{
ImageUrl = new SubContentImageUrlData
{
Url = await attachment.TryAsBase64(token: token) is (true, var base64Content)
? $"data:{attachment.DetermineMimeType()};base64,{base64Content}"
: string.Empty,
},
}),
(systemPrompt, messages, apiParameters, stream, tools) => Task.FromResult(new ChatCompletionAPIRequest
{ {
Model = chatModel.Id, var messages = await chatThread.Blocks.BuildMessagesAsync(
Messages = [systemPrompt, ..messages], this.Provider,
Stream = stream, chatModel,
Tools = tools, role => role switch
ParallelToolCalls = tools is null ? null : true, {
AdditionalApiParameters = apiParameters, ChatRole.USER => "user",
}), ChatRole.AI => "assistant",
ChatRole.AGENT => "assistant",
ChatRole.SYSTEM => systemPromptRole,
_ => "user",
},
text => new SubContentText
{
Text = text,
},
async attachment => new SubContentImageUrlNested
{
ImageUrl = new SubContentImageUrlData
{
Url = await attachment.TryAsBase64(token: token) is (true, var base64Content)
? $"data:{attachment.DetermineMimeType()};base64,{base64Content}"
: string.Empty,
},
});
return new ChatCompletionAPIRequest
{
Model = chatModel.Id,
Messages = [systemPrompt, ..messages],
Stream = true,
Tools = tools,
ParallelToolCalls = tools is null ? null : true,
AdditionalApiParameters = apiParameters,
};
},
systemPromptRole: systemPromptRole, systemPromptRole: systemPromptRole,
requestPath: "chat/completions", requestPath: "chat/completions",
token: token)) token: token))
@ -174,19 +175,6 @@ public sealed class ProviderOpenAI() : BaseProvider(LLMProviders.OPEN_AI, new Ur
Content = chatThread.PrepareSystemPrompt(settingsManager), Content = chatThread.PrepareSystemPrompt(settingsManager),
}; };
//
// Prepare the tools we want to use:
//
IList<ProviderTool> providerTools = modelCapabilities.Contains(Capability.WEB_SEARCH) switch
{
true => [ ProviderTools.WEB_SEARCH ],
_ => []
};
// Parse the API parameters:
var apiParameters = this.ParseAdditionalApiParameters("input", "store", "tools");
// Build the list of messages: // Build the list of messages:
var messages = await chatThread.Blocks.BuildMessagesAsync( var messages = await chatThread.Blocks.BuildMessagesAsync(
this.Provider, chatModel, this.Provider, chatModel,
@ -279,7 +267,6 @@ public sealed class ProviderOpenAI() : BaseProvider(LLMProviders.OPEN_AI, new Ur
Store = false, Store = false,
// Tools we want to use: // Tools we want to use:
ProviderTools = providerTools,
Tools = providerTools, Tools = providerTools,
// Additional API parameters: // Additional API parameters:
@ -438,7 +425,7 @@ public sealed class ProviderOpenAI() : BaseProvider(LLMProviders.OPEN_AI, new Ur
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", await requestedSecret.Secret.Decrypt(ENCRYPTION)); request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", await requestedSecret.Secret.Decrypt(ENCRYPTION));
request.Content = new StringContent(JsonSerializer.Serialize(requestDto, JSON_SERIALIZER_OPTIONS), Encoding.UTF8, "application/json"); request.Content = new StringContent(JsonSerializer.Serialize(requestDto, JSON_SERIALIZER_OPTIONS), Encoding.UTF8, "application/json");
using var response = await this.httpClient.SendAsync(request, token); using var response = await this.HttpClient.SendAsync(request, token);
if (!response.IsSuccessStatusCode) if (!response.IsSuccessStatusCode)
{ {
var responseBody = await response.Content.ReadAsStringAsync(token); var responseBody = await response.Content.ReadAsStringAsync(token);

View File

@ -10,13 +10,11 @@ namespace AIStudio.Provider.OpenAI;
/// <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 provider-side tools and local function tools to use for the request.</param> /// <param name="Tools">The provider-side tools and local function 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<object> Input, IList<object> Input,
bool Stream, bool Stream,
bool Store, bool Store,
[property: JsonPropertyName("tools")] IList<ProviderTool> ProviderTools)
IList<object> Tools) IList<object> Tools)
{ {
public ResponsesAPIRequest() : this(string.Empty, [], true, false, []) public ResponsesAPIRequest() : this(string.Empty, [], true, false, [])

View File

@ -34,4 +34,9 @@ public enum SecretStoreType
/// Data source secrets. Uses the "data-source::" prefix. /// Data source secrets. Uses the "data-source::" prefix.
/// </summary> /// </summary>
DATA_SOURCE, DATA_SOURCE,
}
/// <summary>
/// Tool setting secrets. Uses the "tool::" prefix.
/// </summary>
TOOL_SETTINGS,
}

View File

@ -17,7 +17,8 @@ public static class SecretStoreTypeExtensions
SecretStoreType.TRANSCRIPTION_PROVIDER => "transcription", SecretStoreType.TRANSCRIPTION_PROVIDER => "transcription",
SecretStoreType.IMAGE_PROVIDER => "image", SecretStoreType.IMAGE_PROVIDER => "image",
SecretStoreType.DATA_SOURCE => "data-source", SecretStoreType.DATA_SOURCE => "data-source",
SecretStoreType.TOOL_SETTINGS => "tool",
_ => "provider", _ => "provider",
}; };
} }

View File

@ -4,7 +4,7 @@ namespace AIStudio.Tools.ToolCallingSystem;
internal sealed record ToolSettingsSecretId(string ToolId, string FieldName) : ISecretId internal sealed record ToolSettingsSecretId(string ToolId, string FieldName) : ISecretId
{ {
public string SecretId => $"tool::{this.ToolId}"; public string SecretId => this.ToolId;
public string SecretName => this.FieldName; public string SecretName => this.FieldName;
} }

View File

@ -23,7 +23,7 @@ public sealed class ToolSettingsService(SettingsManager settingsManager, RustSer
if (fieldDefinition.Secret) if (fieldDefinition.Secret)
{ {
var response = await rustService.GetSecret(new ToolSettingsSecretId(definition.Id, fieldName), isTrying: true); var response = await rustService.GetSecret(new ToolSettingsSecretId(definition.Id, fieldName), SecretStoreType.TOOL_SETTINGS, isTrying: true);
if (response.Success) if (response.Success)
values[fieldName] = await response.Secret.Decrypt(Program.ENCRYPTION); values[fieldName] = await response.Secret.Decrypt(Program.ENCRYPTION);
@ -99,9 +99,9 @@ public sealed class ToolSettingsService(SettingsManager settingsManager, RustSer
{ {
var secretId = new ToolSettingsSecretId(definition.Id, fieldName); var secretId = new ToolSettingsSecretId(definition.Id, fieldName);
if (string.IsNullOrWhiteSpace(value)) if (string.IsNullOrWhiteSpace(value))
await rustService.DeleteSecret(secretId); await rustService.DeleteSecret(secretId, SecretStoreType.TOOL_SETTINGS);
else else
await rustService.SetSecret(secretId, value); await rustService.SetSecret(secretId, value, SecretStoreType.TOOL_SETTINGS);
continue; continue;
} }