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))
{
<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 />

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.")"/>
<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))
{

View File

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

View File

@ -957,7 +957,6 @@ public abstract class BaseProvider : IProvider, ISecretId
/// <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="messagesFactory">Builds the provider-specific base messages.</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>
@ -969,19 +968,19 @@ public abstract class BaseProvider : IProvider, ISecretId
/// <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<TDelta, TAnnotation>(
protected async IAsyncEnumerable<ContentStreamChunk> StreamOpenAICompatibleChatCompletion<TRequest, TDelta, TAnnotation>(
string providerName,
Model chatModel,
ChatThread chatThread,
SettingsManager settingsManager,
Func<Task<IList<IMessageBase>>> messagesFactory,
Func<TextMessage, IDictionary<string, object>, Task<TRequest>> requestFactory,
Func<TextMessage, IDictionary<string, object>, IList<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 TRequest : ChatCompletionAPIRequest
where TDelta : IResponseStreamLine
where TAnnotation : IAnnotationStreamLine
{
@ -1000,7 +999,6 @@ public abstract class BaseProvider : IProvider, ISecretId
// Parse the API parameters:
var apiParameters = this.ParseAdditionalApiParameters();
var baseMessages = await messagesFactory();
var toolRegistry = Program.SERVICE_PROVIDER.GetService<ToolRegistry>();
var toolExecutor = Program.SERVICE_PROVIDER.GetService<ToolExecutor>();
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;
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 responseMessage = response?.Choices.FirstOrDefault()?.Message;
if (responseMessage is null)
@ -1106,7 +1109,7 @@ public abstract class BaseProvider : IProvider, ISecretId
}
// 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()
{
@ -1143,7 +1146,7 @@ public abstract class BaseProvider : IProvider, ISecretId
headersAction?.Invoke(request.Headers);
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)
{
var responseBody = await response.Content.ReadAsStringAsync(token);
@ -1477,4 +1480,4 @@ public abstract class BaseProvider : IProvider, ISecretId
_ => string.Empty,
};
}
}

View File

@ -12,7 +12,6 @@ using AIStudio.Tools.ToolCallingSystem;
using AIStudio.Tools.Services;
using Microsoft.Extensions.DependencyInjection;
using AIStudio.Tools.PluginSystem;
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 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
/// <inheritdoc />
@ -121,44 +118,48 @@ public sealed class ProviderOpenAI() : BaseProvider(LLMProviders.OPEN_AI, new Ur
if (!usingResponsesAPI)
{
await foreach (var content in this.StreamOpenAICompatibleChatCompletion<ChatCompletionDeltaStreamLine, ChatCompletionAnnotationStreamLine>(
await foreach (var content in this.StreamOpenAICompatibleChatCompletion<ChatCompletionAPIRequest, ChatCompletionDeltaStreamLine, ChatCompletionAnnotationStreamLine>(
"OpenAI",
chatModel,
chatThread,
settingsManager,
() => chatThread.Blocks.BuildMessagesAsync(
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
async (systemPrompt, apiParameters, tools) =>
{
Model = chatModel.Id,
Messages = [systemPrompt, ..messages],
Stream = stream,
Tools = tools,
ParallelToolCalls = tools is null ? null : true,
AdditionalApiParameters = apiParameters,
}),
var messages = await chatThread.Blocks.BuildMessagesAsync(
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,
},
});
return new ChatCompletionAPIRequest
{
Model = chatModel.Id,
Messages = [systemPrompt, ..messages],
Stream = true,
Tools = tools,
ParallelToolCalls = tools is null ? null : true,
AdditionalApiParameters = apiParameters,
};
},
systemPromptRole: systemPromptRole,
requestPath: "chat/completions",
token: token))
@ -174,19 +175,6 @@ public sealed class ProviderOpenAI() : BaseProvider(LLMProviders.OPEN_AI, new Ur
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:
var messages = await chatThread.Blocks.BuildMessagesAsync(
this.Provider, chatModel,
@ -279,7 +267,6 @@ public sealed class ProviderOpenAI() : BaseProvider(LLMProviders.OPEN_AI, new Ur
Store = false,
// Tools we want to use:
ProviderTools = providerTools,
Tools = providerTools,
// 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.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)
{
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="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="ProviderTools">The provider-side tools to use for the request.</param>
public record ResponsesAPIRequest(
string Model,
IList<object> Input,
bool Stream,
bool Store,
[property: JsonPropertyName("tools")] IList<ProviderTool> ProviderTools)
IList<object> Tools)
{
public ResponsesAPIRequest() : this(string.Empty, [], true, false, [])

View File

@ -34,4 +34,9 @@ public enum SecretStoreType
/// Data source secrets. Uses the "data-source::" prefix.
/// </summary>
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.IMAGE_PROVIDER => "image",
SecretStoreType.DATA_SOURCE => "data-source",
SecretStoreType.TOOL_SETTINGS => "tool",
_ => "provider",
};
}
}

View File

@ -4,7 +4,7 @@ namespace AIStudio.Tools.ToolCallingSystem;
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;
}

View File

@ -23,7 +23,7 @@ public sealed class ToolSettingsService(SettingsManager settingsManager, RustSer
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)
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);
if (string.IsNullOrWhiteSpace(value))
await rustService.DeleteSecret(secretId);
await rustService.DeleteSecret(secretId, SecretStoreType.TOOL_SETTINGS);
else
await rustService.SetSecret(secretId, value);
await rustService.SetSecret(secretId, value, SecretStoreType.TOOL_SETTINGS);
continue;
}