Add AdditionalApiParameters input validation and normalization

This commit is contained in:
Peer Schütt 2026-03-11 16:06:42 +01:00
parent d73aa84e9c
commit 6d2a8fc8c9
2 changed files with 168 additions and 4 deletions

View File

@ -160,7 +160,7 @@
<MudJustifiedText Class="mb-5"> <MudJustifiedText Class="mb-5">
@T("Please be aware: This section is for experts only. You are responsible for verifying the correctness of the additional parameters you provide to the API call. By default, AI Studio uses the OpenAI-compatible chat completions API, when that it is supported by the underlying service and model.") @T("Please be aware: This section is for experts only. You are responsible for verifying the correctness of the additional parameters you provide to the API call. By default, AI Studio uses the OpenAI-compatible chat completions API, when that it is supported by the underlying service and model.")
</MudJustifiedText> </MudJustifiedText>
<MudTextField T="string" Label=@T("Additional API parameters") Variant="Variant.Outlined" Lines="4" AutoGrow="true" MaxLines="10" HelperText=@T("""Add the parameters in proper JSON formatting, e.g., "temperature": 0.5. Remove trailing commas. The usual surrounding curly brackets {} must not be used, though.""") Placeholder="@GetPlaceholderExpertSettings" @bind-Value="@this.AdditionalJsonApiParameters" OnBlur="@this.OnInputChangeExpertSettings"/> <MudTextField T="string" Label=@T("Additional API parameters") Variant="Variant.Outlined" Lines="4" AutoGrow="true" MaxLines="10" HelperText=@T("""Add the parameters in proper JSON formatting, e.g., "temperature": 0.5. Remove trailing commas. The usual surrounding curly brackets {} must not be used, though.""") Placeholder="@GetPlaceholderExpertSettings" @bind-Value="@this.AdditionalJsonApiParameters" Immediate="true" Validation="@this.ValidateAdditionalJsonApiParameters" OnBlur="@this.OnInputChangeExpertSettings"/>
</MudCollapse> </MudCollapse>
</MudStack> </MudStack>
</MudForm> </MudForm>
@ -181,4 +181,4 @@
} }
</MudButton> </MudButton>
</DialogActions> </DialogActions>
</MudDialog> </MudDialog>

View File

@ -1,3 +1,6 @@
using System.Text;
using System.Text.Json;
using AIStudio.Components; using AIStudio.Components;
using AIStudio.Provider; using AIStudio.Provider;
using AIStudio.Provider.HuggingFace; using AIStudio.Provider.HuggingFace;
@ -334,7 +337,168 @@ public partial class ProviderDialog : MSGComponentBase, ISecretId
private void OnInputChangeExpertSettings() private void OnInputChangeExpertSettings()
{ {
this.AdditionalJsonApiParameters = this.AdditionalJsonApiParameters.Trim().TrimEnd(',', ' '); this.AdditionalJsonApiParameters = NormalizeAdditionalJsonApiParameters(this.AdditionalJsonApiParameters)
.Trim()
.TrimEnd(',', ' ');
}
private string? ValidateAdditionalJsonApiParameters(string additionalParams)
{
if (string.IsNullOrWhiteSpace(additionalParams))
return null;
var normalized = NormalizeAdditionalJsonApiParameters(additionalParams);
if (!string.Equals(normalized, additionalParams, StringComparison.Ordinal))
this.AdditionalJsonApiParameters = normalized;
var json = $"{{{normalized}}}";
try
{
if (!this.TryValidateJsonObjectWithDuplicateCheck(json, out var errorMessage))
return errorMessage;
return null;
}
catch (JsonException)
{
return T("Invalid JSON.");
}
}
private static string NormalizeAdditionalJsonApiParameters(string input)
{
var sb = new StringBuilder(input.Length);
var inString = false;
var escape = false;
for (var i = 0; i < input.Length; i++)
{
var c = input[i];
if (inString)
{
sb.Append(c);
if (escape)
{
escape = false;
continue;
}
if (c == '\\')
{
escape = true;
continue;
}
if (c == '"')
inString = false;
continue;
}
if (c == '"')
{
inString = true;
sb.Append(c);
continue;
}
if (TryReadToken(input, i, "True", out var tokenLength))
{
sb.Append("true");
i += tokenLength - 1;
continue;
}
if (TryReadToken(input, i, "False", out tokenLength))
{
sb.Append("false");
i += tokenLength - 1;
continue;
}
if (TryReadToken(input, i, "Null", out tokenLength))
{
sb.Append("null");
i += tokenLength - 1;
continue;
}
sb.Append(c);
}
return sb.ToString();
}
private static bool TryReadToken(string input, int startIndex, string token, out int tokenLength)
{
tokenLength = 0;
if (startIndex + token.Length > input.Length)
return false;
if (!input.AsSpan(startIndex, token.Length).SequenceEqual(token))
return false;
var beforeIndex = startIndex - 1;
if (beforeIndex >= 0 && IsIdentifierChar(input[beforeIndex]))
return false;
var afterIndex = startIndex + token.Length;
if (afterIndex < input.Length && IsIdentifierChar(input[afterIndex]))
return false;
tokenLength = token.Length;
return true;
}
private static bool IsIdentifierChar(char c) => char.IsLetterOrDigit(c) || c == '_';
private bool TryValidateJsonObjectWithDuplicateCheck(string json, out string? errorMessage)
{
errorMessage = null;
var bytes = Encoding.UTF8.GetBytes(json);
var reader = new Utf8JsonReader(bytes, new JsonReaderOptions
{
AllowTrailingCommas = false,
CommentHandling = JsonCommentHandling.Disallow
});
var objectStack = new Stack<HashSet<string>>();
while (reader.Read())
{
switch (reader.TokenType)
{
case JsonTokenType.StartObject:
objectStack.Push(new HashSet<string>(StringComparer.Ordinal));
break;
case JsonTokenType.EndObject:
if (objectStack.Count > 0)
objectStack.Pop();
break;
case JsonTokenType.PropertyName:
if (objectStack.Count == 0)
{
errorMessage = T("Additional API parameters must form a JSON object.");
return false;
}
var name = reader.GetString() ?? string.Empty;
if (!objectStack.Peek().Add(name))
{
errorMessage = string.Format(T("Duplicate key '{0}' found."), name);
return false;
}
break;
}
}
if (objectStack.Count != 0)
{
errorMessage = T("Invalid JSON.");
return false;
}
return true;
} }
private string GetExpertStyles => this.showExpertSettings ? "border-2 border-dashed rounded pa-2" : string.Empty; private string GetExpertStyles => this.showExpertSettings ? "border-2 border-dashed rounded pa-2" : string.Empty;
@ -345,4 +509,4 @@ public partial class ProviderDialog : MSGComponentBase, ISecretId
"top_p": 0.9, "top_p": 0.9,
"frequency_penalty": 0.0 "frequency_penalty": 0.0
"""; """;
} }