Standardized AdditionalApiParameters type across providers to use IDictionary<string, string> and improved JSON parsing reliability.

This commit is contained in:
Peer Schütt 2025-10-23 09:40:36 +02:00
parent 17bc2f3e46
commit 955f212c57
12 changed files with 39 additions and 51 deletions

View File

@ -135,7 +135,7 @@
<MudDivider /> <MudDivider />
<MudCollapse Expanded="@this.showExpertProviderSettings"> <MudCollapse Expanded="@this.showExpertProviderSettings">
<MudJustifiedText>@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.")</MudJustifiedText> <MudJustifiedText>@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.")</MudJustifiedText>
<MudJustifiedText>@T("By default, AI Studio uses the OpenAI-compatible chat/completions API, provided it is supported by the underlying service.")</MudJustifiedText> <MudJustifiedText>@T("By default, AI Studio uses the OpenAI-compatible chat/completions API, provided it is supported by the underlying service and model.")</MudJustifiedText>
<MudTextField T="string" Label=@T("Additional API parameters") Variant="Variant.Outlined" Lines="3" AutoGrow="true" HelperText=@T("Add the parameters in proper json formatting. E.g. \"temperature\": 0.5. Trailing commas have to be removed.") HelperTextOnFocus="true " @bind-Value="@ExpertProviderApiParameters" OnBlur="@OnInputChangeExpertSettings"></MudTextField> <MudTextField T="string" Label=@T("Additional API parameters") Variant="Variant.Outlined" Lines="3" AutoGrow="true" HelperText=@T("Add the parameters in proper json formatting. E.g. \"temperature\": 0.5. Trailing commas have to be removed.") HelperTextOnFocus="true " @bind-Value="@ExpertProviderApiParameters" OnBlur="@OnInputChangeExpertSettings"></MudTextField>
</MudCollapse> </MudCollapse>
</MudStack> </MudStack>

View File

@ -20,6 +20,5 @@ public readonly record struct ChatRequest(
) )
{ {
[JsonExtensionData] public IDictionary<string, string> AdditionalApiParameters { get; init; }
public Dictionary<string, object> AdditionalApiParameters { get; init; }
} }

View File

@ -58,7 +58,7 @@ public sealed class ProviderAnthropic() : BaseProvider("https://api.anthropic.co
}).ToList()], }).ToList()],
System = chatThread.PrepareSystemPrompt(settingsManager, chatThread), System = chatThread.PrepareSystemPrompt(settingsManager, chatThread),
MaxTokens = apiParameters["max_tokens"] as int? ?? 4_096, MaxTokens = int.TryParse(apiParameters["max_tokens"], out int parsed) ? parsed : 4_096,
// Right now, we only support streaming completions: // Right now, we only support streaming completions:
Stream = true, Stream = true,

View File

@ -530,49 +530,46 @@ public abstract class BaseProvider : IProvider, ISecretId
/// optionally merging additional parameters and removing specific keys. /// optionally merging additional parameters and removing specific keys.
/// </summary> /// </summary>
/// <param name="additionalUserProvidedParameters">A JSON string (without surrounding braces) containing the API parameters to be parsed.</param> /// <param name="additionalUserProvidedParameters">A JSON string (without surrounding braces) containing the API parameters to be parsed.</param>
/// <param name="defaultParameters">Optional additional parameters to merge into the result. These will overwrite existing keys.</param>
/// <param name="keysToRemove">Optional list of keys to remove from the final dictionary (case-insensitive). stream, model and messages are removed by default.</param> /// <param name="keysToRemove">Optional list of keys to remove from the final dictionary (case-insensitive). stream, model and messages are removed by default.</param>
protected Dictionary<string, object?> ParseApiParameters( protected IDictionary<string, string> ParseApiParameters(
string additionalUserProvidedParameters, string additionalUserProvidedParameters,
IEnumerable<string>? keysToRemove = null) IEnumerable<string>? 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 json = $"{{{additionalUserProvidedParameters}}}"; var json = $"{{{withoutLineBreak}}}";
var jsonDoc = JsonSerializer.Deserialize<JsonElement>(json, JSON_SERIALIZER_OPTIONS); var jsonDoc = JsonSerializer.Deserialize<JsonElement>(json, JSON_SERIALIZER_OPTIONS);
var dict = this.ConvertToDictionary(jsonDoc); var dict = this.ConvertToDictionary(jsonDoc);
// Some keys are always removed because we always set them // Some keys are always removed because we always set them
var finalKeysToRemove = keysToRemove?.ToList() ?? new List<string>(); var finalKeysToRemove = keysToRemove?.ToList() ?? new List<string>();
finalKeysToRemove.Add("stream"); finalKeysToRemove.Add("stream");
finalKeysToRemove.Add("model"); finalKeysToRemove.Add("model");
finalKeysToRemove.Add("messages"); finalKeysToRemove.Add("messages");
var removeSet = new HashSet<string>(finalKeysToRemove, StringComparer.OrdinalIgnoreCase); var removeSet = new HashSet<string>(finalKeysToRemove, StringComparer.OrdinalIgnoreCase);
var toRemove = dict.Keys.Where(k => removeSet.Contains(k)).ToList(); var toRemove = dict.Keys.Where(k => removeSet.Contains(k)).ToList();
foreach (var k in toRemove) foreach (var k in toRemove)
dict.Remove(k); dict.Remove(k);
return dict; return dict;
}
catch (JsonException ex)
{
throw new ArgumentException("Invalid JSON in additionalUserProvidedParameters", ex);
}
} }
private Dictionary<string, object?> ConvertToDictionary(JsonElement element) private IDictionary<string, string> ConvertToDictionary(JsonElement element)
{ {
return element.EnumerateObject() return element.EnumerateObject()
.ToDictionary( .ToDictionary(
p => p.Name, p => p.Name,
p => this.ConvertJsonElement(p.Value) p => p.Value.GetRawText()
); );
} }
private object? ConvertJsonElement(JsonElement element) => element.ValueKind switch
{
JsonValueKind.String => element.GetString(),
JsonValueKind.Number => element.TryGetInt32(out var i) ? (object)i : element.GetDouble(),
JsonValueKind.True => true,
JsonValueKind.False => false,
JsonValueKind.Object => this.ConvertToDictionary(element),
JsonValueKind.Array => element.EnumerateArray().Select(this.ConvertJsonElement).ToList(),
JsonValueKind.Null => null,
_ => element.ToString()
};
} }

View File

@ -15,6 +15,5 @@ public readonly record struct ChatRequest(
) )
{ {
[JsonExtensionData] public IDictionary<string, string> AdditionalApiParameters { get; init; }
public Dictionary<string, object> AdditionalApiParameters { get; init; }
} }

View File

@ -15,7 +15,5 @@ public readonly record struct ChatRequest(
bool Stream bool Stream
) )
{ {
public IDictionary<string, string> AdditionalApiParameters { get; init; }
[JsonExtensionData]
public Dictionary<string, object> AdditionalApiParameters { get; init; }
} }

View File

@ -18,6 +18,5 @@ public readonly record struct ChatRequest(
) )
{ {
[JsonExtensionData] public IDictionary<string, string> AdditionalApiParameters { get; init; }
public Dictionary<string, object> AdditionalApiParameters { get; init; }
} }

View File

@ -19,6 +19,5 @@ public readonly record struct ChatRequest(
) )
{ {
[JsonExtensionData] public IDictionary<string, string> AdditionalApiParameters { get; init; }
public Dictionary<string, object> AdditionalApiParameters { get; init; }
} }

View File

@ -69,7 +69,7 @@ public sealed class ProviderMistral() : BaseProvider("https://api.mistral.ai/v1/
// Right now, we only support streaming completions: // Right now, we only support streaming completions:
Stream = true, Stream = true,
SafePrompt = apiParameters["safe_prompt"] as bool? ?? false, SafePrompt = bool.TryParse(apiParameters["safe_prompt"], out bool safePrompt) && safePrompt,
AdditionalApiParameters = apiParameters AdditionalApiParameters = apiParameters
}, JSON_SERIALIZER_OPTIONS); }, JSON_SERIALIZER_OPTIONS);

View File

@ -18,6 +18,5 @@ public record ChatCompletionAPIRequest(
{ {
} }
[JsonExtensionData] public IDictionary<string, string>? AdditionalApiParameters { get; init; }
public Dictionary<string, object>? AdditionalApiParameters { get; init; }
} }

View File

@ -21,6 +21,5 @@ public record ResponsesAPIRequest(
{ {
} }
[JsonExtensionData] public IDictionary<string, string>? AdditionalApiParameters { get; init; }
public Dictionary<string, object>? AdditionalApiParameters { get; init; }
} }

View File

@ -15,6 +15,5 @@ public readonly record struct ChatRequest(
) )
{ {
[JsonExtensionData] public IDictionary<string, string> AdditionalApiParameters { get; init; }
public Dictionary<string, object> AdditionalApiParameters { get; init; }
} }