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