diff --git a/app/MindWork AI Studio/Dialogs/ProviderDialog.razor b/app/MindWork AI Studio/Dialogs/ProviderDialog.razor index a0cf55f4..d76438c8 100644 --- a/app/MindWork AI Studio/Dialogs/ProviderDialog.razor +++ b/app/MindWork AI Studio/Dialogs/ProviderDialog.razor @@ -135,8 +135,8 @@ @T("Please be aware: This is for experts only. You are responsible for verifying the correctness of the additional parameters you provide to the API call.") - @T("By default, AI Studio uses the OpenAI-compatible chat/completions API, provided it is supported by the underlying service and model.") - + @T("By default, AI Studio uses the OpenAI-compatible chat/completions API, provided that it is supported by the underlying service and model.") + diff --git a/app/MindWork AI Studio/Provider/Anthropic/ChatRequest.cs b/app/MindWork AI Studio/Provider/Anthropic/ChatRequest.cs index 3a9404b9..0402fc3c 100644 --- a/app/MindWork AI Studio/Provider/Anthropic/ChatRequest.cs +++ b/app/MindWork AI Studio/Provider/Anthropic/ChatRequest.cs @@ -1,3 +1,4 @@ +using System.Text.Json; using System.Text.Json.Serialization; using AIStudio.Provider.OpenAI; @@ -20,5 +21,6 @@ public readonly record struct ChatRequest( ) { -public IDictionary AdditionalApiParameters { get; init; } + [JsonExtensionData] + public IDictionary AdditionalApiParameters { get; init; } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Provider/Anthropic/ProviderAnthropic.cs b/app/MindWork AI Studio/Provider/Anthropic/ProviderAnthropic.cs index 298f54d8..3d1ed5ce 100644 --- a/app/MindWork AI Studio/Provider/Anthropic/ProviderAnthropic.cs +++ b/app/MindWork AI Studio/Provider/Anthropic/ProviderAnthropic.cs @@ -58,7 +58,7 @@ public sealed class ProviderAnthropic() : BaseProvider("https://api.anthropic.co }).ToList()], System = chatThread.PrepareSystemPrompt(settingsManager, chatThread), - MaxTokens = int.TryParse(apiParameters["max_tokens"], out int parsed) ? parsed : 4_096, + MaxTokens = apiParameters.TryGetValue("max_tokens", out var value) && value is int intValue ? intValue : 4096, // Right now, we only support streaming completions: Stream = true, diff --git a/app/MindWork AI Studio/Provider/BaseProvider.cs b/app/MindWork AI Studio/Provider/BaseProvider.cs index e89f711d..d7d0c0f5 100644 --- a/app/MindWork AI Studio/Provider/BaseProvider.cs +++ b/app/MindWork AI Studio/Provider/BaseProvider.cs @@ -107,7 +107,6 @@ public abstract class BaseProvider : IProvider, ISecretId var retry = 0; var response = default(HttpResponseMessage); var errorMessage = string.Empty; - var errorBody = string.Empty; while (retry++ < MAX_RETRIES) { using var request = await requestBuilder(); @@ -127,12 +126,11 @@ public abstract class BaseProvider : IProvider, ISecretId break; } - errorBody = await nextResponse.Content.ReadAsStringAsync(token); + var errorBody = await nextResponse.Content.ReadAsStringAsync(token); if (nextResponse.StatusCode is HttpStatusCode.Forbidden) { await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.Block, string.Format(TB("Tried to communicate with the LLM provider '{0}'. You might not be able to use this provider from your location. The provider message is: '{1}'"), this.InstanceName, nextResponse.ReasonPhrase))); - this.logger.LogError($"Failed request with status code {nextResponse.StatusCode} (message = '{nextResponse.ReasonPhrase}')."); - this.logger.LogError($"Error body: {errorBody}"); + this.logger.LogError("Failed request with status code {ResposeStatusCode} (message = '{ResponseReasonPhrase}', error body = '{ErrorBody}').", nextResponse.StatusCode, nextResponse.ReasonPhrase, errorBody); errorMessage = nextResponse.ReasonPhrase; break; } @@ -140,8 +138,7 @@ public abstract class BaseProvider : IProvider, ISecretId if(nextResponse.StatusCode is HttpStatusCode.BadRequest) { await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.CloudOff, string.Format(TB("Tried to communicate with the LLM provider '{0}'. The required message format might be changed. The provider message is: '{1}'"), this.InstanceName, nextResponse.ReasonPhrase))); - this.logger.LogError($"Failed request with status code {nextResponse.StatusCode} (message = '{nextResponse.ReasonPhrase}')."); - this.logger.LogError($"Error body: {errorBody}"); + this.logger.LogError("Failed request with status code {ResposeStatusCode} (message = '{ResponseReasonPhrase}', error body = '{ErrorBody}').", nextResponse.StatusCode, nextResponse.ReasonPhrase, errorBody); errorMessage = nextResponse.ReasonPhrase; break; } @@ -149,8 +146,7 @@ public abstract class BaseProvider : IProvider, ISecretId if(nextResponse.StatusCode is HttpStatusCode.NotFound) { await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.CloudOff, string.Format(TB("Tried to communicate with the LLM provider '{0}'. Something was not found. The provider message is: '{1}'"), this.InstanceName, nextResponse.ReasonPhrase))); - this.logger.LogError($"Failed request with status code {nextResponse.StatusCode} (message = '{nextResponse.ReasonPhrase}')."); - this.logger.LogError($"Error body: {errorBody}"); + this.logger.LogError("Failed request with status code {ResposeStatusCode} (message = '{ResponseReasonPhrase}', error body = '{ErrorBody}').", nextResponse.StatusCode, nextResponse.ReasonPhrase, errorBody); errorMessage = nextResponse.ReasonPhrase; break; } @@ -158,8 +154,7 @@ public abstract class BaseProvider : IProvider, ISecretId if(nextResponse.StatusCode is HttpStatusCode.Unauthorized) { await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.Key, string.Format(TB("Tried to communicate with the LLM provider '{0}'. The API key might be invalid. The provider message is: '{1}'"), this.InstanceName, nextResponse.ReasonPhrase))); - this.logger.LogError($"Failed request with status code {nextResponse.StatusCode} (message = '{nextResponse.ReasonPhrase}')."); - this.logger.LogError($"Error body: {errorBody}"); + this.logger.LogError("Failed request with status code {ResposeStatusCode} (message = '{ResponseReasonPhrase}', error body = '{ErrorBody}').", nextResponse.StatusCode, nextResponse.ReasonPhrase, errorBody); errorMessage = nextResponse.ReasonPhrase; break; } @@ -167,8 +162,7 @@ public abstract class BaseProvider : IProvider, ISecretId if(nextResponse.StatusCode is HttpStatusCode.InternalServerError) { await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.CloudOff, string.Format(TB("Tried to communicate with the LLM provider '{0}'. The server might be down or having issues. The provider message is: '{1}'"), this.InstanceName, nextResponse.ReasonPhrase))); - this.logger.LogError($"Failed request with status code {nextResponse.StatusCode} (message = '{nextResponse.ReasonPhrase}')."); - this.logger.LogError($"Error body: {errorBody}"); + this.logger.LogError("Failed request with status code {ResposeStatusCode} (message = '{ResponseReasonPhrase}', error body = '{ErrorBody}').", nextResponse.StatusCode, nextResponse.ReasonPhrase, errorBody); errorMessage = nextResponse.ReasonPhrase; break; } @@ -176,8 +170,7 @@ public abstract class BaseProvider : IProvider, ISecretId if(nextResponse.StatusCode is HttpStatusCode.ServiceUnavailable) { await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.CloudOff, string.Format(TB("Tried to communicate with the LLM provider '{0}'. The provider is overloaded. The message is: '{1}'"), this.InstanceName, nextResponse.ReasonPhrase))); - this.logger.LogError($"Failed request with status code {nextResponse.StatusCode} (message = '{nextResponse.ReasonPhrase}')."); - this.logger.LogError($"Error body: {errorBody}"); + this.logger.LogError("Failed request with status code {ResposeStatusCode} (message = '{ResponseReasonPhrase}', error body = '{ErrorBody}').", nextResponse.StatusCode, nextResponse.ReasonPhrase, errorBody); errorMessage = nextResponse.ReasonPhrase; break; } @@ -187,13 +180,13 @@ public abstract class BaseProvider : IProvider, ISecretId if(timeSeconds > 90) timeSeconds = 90; - this.logger.LogDebug($"Failed request with status code {nextResponse.StatusCode} (message = '{errorMessage}'). Retrying in {timeSeconds:0.00} seconds."); + this.logger.LogDebug("Failed request with status code {ResponseStatusCode} (message = '{ErrorMessage}'). Retrying in {TimeSeconds:0.00} seconds.", nextResponse.StatusCode, errorMessage, timeSeconds); await Task.Delay(TimeSpan.FromSeconds(timeSeconds), token); } if(retry >= MAX_RETRIES || !string.IsNullOrWhiteSpace(errorMessage)) { - await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.CloudOff, string.Format(TB("Tried to communicate with the LLM provider '{0}'. Even after {1} retries, there were some problems with the request. The provider message is: '{2}'. The error body is: '{3}'"), this.InstanceName, MAX_RETRIES, errorMessage, errorBody))); + await MessageBus.INSTANCE.SendError(new DataErrorMessage(Icons.Material.Filled.CloudOff, string.Format(TB("Tried to communicate with the LLM provider '{0}'. Even after {1} retries, there were some problems with the request. The provider message is: '{2}'."), this.InstanceName, MAX_RETRIES, errorMessage))); return new HttpRateLimitedStreamResult(false, true, errorMessage ?? $"Failed after {MAX_RETRIES} retries; no provider message available", response); } @@ -531,21 +524,21 @@ public abstract class BaseProvider : IProvider, ISecretId /// /// A JSON string (without surrounding braces) containing the API parameters to be parsed. /// Optional list of keys to remove from the final dictionary (case-insensitive). stream, model and messages are removed by default. - protected IDictionary ParseApiParameters( + protected IDictionary ParseApiParameters( string additionalUserProvidedParameters, IEnumerable? keysToRemove = null) { try { // we need to remove line breaks from the JSON string otherwise the server might have problems with parsing the call - var withoutLineBreak = additionalUserProvidedParameters.Replace("\n", string.Empty); + // var withoutLineBreak = additionalUserProvidedParameters.Replace("\n", string.Empty); - var json = $"{{{withoutLineBreak}}}"; + var json = $"{{{additionalUserProvidedParameters}}}"; var jsonDoc = JsonSerializer.Deserialize(json, JSON_SERIALIZER_OPTIONS); var dict = this.ConvertToDictionary(jsonDoc); // Some keys are always removed because we always set them - var finalKeysToRemove = keysToRemove?.ToList() ?? new List(); + var finalKeysToRemove = keysToRemove?.ToList() ?? []; finalKeysToRemove.Add("stream"); finalKeysToRemove.Add("model"); finalKeysToRemove.Add("messages"); @@ -563,13 +556,28 @@ public abstract class BaseProvider : IProvider, ISecretId } } - private IDictionary ConvertToDictionary(JsonElement element) + private IDictionary ConvertToDictionary(JsonElement element) { return element.EnumerateObject() - .ToDictionary( + .ToDictionary( p => p.Name, - p => p.Value.GetRawText() + p => this.ConvertJsonValue(p.Value) ?? throw new InvalidOperationException() ); } + + private object? ConvertJsonValue(JsonElement element) => element.ValueKind switch + { + JsonValueKind.String => element.GetString(), + JsonValueKind.Number => element.TryGetInt32(out int i) ? i : + element.TryGetInt64(out long l) ? l : + element.TryGetDouble(out double d) ? d : + element.GetDecimal(), + JsonValueKind.True => element.GetBoolean(), + JsonValueKind.False => element.GetBoolean(), + JsonValueKind.Null => null, + JsonValueKind.Object => this.ConvertToDictionary(element), + JsonValueKind.Array => element.EnumerateArray().Select(this.ConvertJsonValue).ToList(), + _ => throw new InvalidOperationException($"Unsupported JSON value kind: {element.ValueKind}") + }; } \ No newline at end of file diff --git a/app/MindWork AI Studio/Provider/Fireworks/ChatRequest.cs b/app/MindWork AI Studio/Provider/Fireworks/ChatRequest.cs index 0738ea32..596e5628 100644 --- a/app/MindWork AI Studio/Provider/Fireworks/ChatRequest.cs +++ b/app/MindWork AI Studio/Provider/Fireworks/ChatRequest.cs @@ -15,5 +15,6 @@ public readonly record struct ChatRequest( ) { - public IDictionary AdditionalApiParameters { get; init; } + [JsonExtensionData] + public IDictionary AdditionalApiParameters { get; init; } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Provider/Google/ChatRequest.cs b/app/MindWork AI Studio/Provider/Google/ChatRequest.cs index b8e1d2ed..77d22863 100644 --- a/app/MindWork AI Studio/Provider/Google/ChatRequest.cs +++ b/app/MindWork AI Studio/Provider/Google/ChatRequest.cs @@ -15,5 +15,6 @@ public readonly record struct ChatRequest( bool Stream ) { - public IDictionary AdditionalApiParameters { get; init; } + [JsonExtensionData] + public IDictionary AdditionalApiParameters { get; init; } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Provider/Groq/ChatRequest.cs b/app/MindWork AI Studio/Provider/Groq/ChatRequest.cs index 2725eae0..c505ad40 100644 --- a/app/MindWork AI Studio/Provider/Groq/ChatRequest.cs +++ b/app/MindWork AI Studio/Provider/Groq/ChatRequest.cs @@ -18,5 +18,6 @@ public readonly record struct ChatRequest( ) { - public IDictionary AdditionalApiParameters { get; init; } + [JsonExtensionData] + public IDictionary AdditionalApiParameters { get; init; } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Provider/Mistral/ChatRequest.cs b/app/MindWork AI Studio/Provider/Mistral/ChatRequest.cs index 0daaebf7..d5ed37b4 100644 --- a/app/MindWork AI Studio/Provider/Mistral/ChatRequest.cs +++ b/app/MindWork AI Studio/Provider/Mistral/ChatRequest.cs @@ -19,5 +19,6 @@ public readonly record struct ChatRequest( ) { - public IDictionary AdditionalApiParameters { get; init; } + [JsonExtensionData] + public IDictionary AdditionalApiParameters { get; init; } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Provider/Mistral/ProviderMistral.cs b/app/MindWork AI Studio/Provider/Mistral/ProviderMistral.cs index 13e35a08..a907378a 100644 --- a/app/MindWork AI Studio/Provider/Mistral/ProviderMistral.cs +++ b/app/MindWork AI Studio/Provider/Mistral/ProviderMistral.cs @@ -69,7 +69,7 @@ public sealed class ProviderMistral() : BaseProvider("https://api.mistral.ai/v1/ // Right now, we only support streaming completions: Stream = true, - SafePrompt = bool.TryParse(apiParameters["safe_prompt"], out bool safePrompt) && safePrompt, + SafePrompt = apiParameters.TryGetValue("safe_prompt", out var value) && value is bool and true, AdditionalApiParameters = apiParameters }, JSON_SERIALIZER_OPTIONS); diff --git a/app/MindWork AI Studio/Provider/OpenAI/ChatCompletionAPIRequest.cs b/app/MindWork AI Studio/Provider/OpenAI/ChatCompletionAPIRequest.cs index 1d79950e..6cf362d1 100644 --- a/app/MindWork AI Studio/Provider/OpenAI/ChatCompletionAPIRequest.cs +++ b/app/MindWork AI Studio/Provider/OpenAI/ChatCompletionAPIRequest.cs @@ -18,5 +18,6 @@ public record ChatCompletionAPIRequest( { } - public IDictionary? AdditionalApiParameters { get; init; } + [JsonExtensionData] + public IDictionary ? AdditionalApiParameters { get; init; } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Provider/OpenAI/ResponsesAPIRequest.cs b/app/MindWork AI Studio/Provider/OpenAI/ResponsesAPIRequest.cs index ee399e47..7b566bb8 100644 --- a/app/MindWork AI Studio/Provider/OpenAI/ResponsesAPIRequest.cs +++ b/app/MindWork AI Studio/Provider/OpenAI/ResponsesAPIRequest.cs @@ -21,5 +21,6 @@ public record ResponsesAPIRequest( { } - public IDictionary? AdditionalApiParameters { get; init; } + [JsonExtensionData] + public IDictionary ? AdditionalApiParameters { get; init; } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Provider/SelfHosted/ChatRequest.cs b/app/MindWork AI Studio/Provider/SelfHosted/ChatRequest.cs index 8599f72c..6e6dbdb5 100644 --- a/app/MindWork AI Studio/Provider/SelfHosted/ChatRequest.cs +++ b/app/MindWork AI Studio/Provider/SelfHosted/ChatRequest.cs @@ -15,5 +15,6 @@ public readonly record struct ChatRequest( ) { - public IDictionary AdditionalApiParameters { get; init; } + [JsonExtensionData] + public IDictionary AdditionalApiParameters { get; init; } } \ No newline at end of file