mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2025-11-23 13:50:20 +00:00
Standardized AdditionalApiParameters to use IDictionary<string, object> across providers and improved JSON handling/logging.
This commit is contained in:
parent
955f212c57
commit
2999bfb0a2
@ -135,8 +135,8 @@
|
|||||||
<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 and model.")</MudJustifiedText>
|
<MudJustifiedText>@T("By default, AI Studio uses the OpenAI-compatible chat/completions API, provided that 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. Remove trailing commas.") HelperTextOnFocus="true " @bind-Value="@ExpertProviderApiParameters" OnBlur="@OnInputChangeExpertSettings"></MudTextField>
|
||||||
</MudCollapse>
|
</MudCollapse>
|
||||||
</MudStack>
|
</MudStack>
|
||||||
|
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
using System.Text.Json;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
using AIStudio.Provider.OpenAI;
|
using AIStudio.Provider.OpenAI;
|
||||||
|
|
||||||
@ -20,5 +21,6 @@ public readonly record struct ChatRequest(
|
|||||||
)
|
)
|
||||||
{
|
{
|
||||||
|
|
||||||
public IDictionary<string, string> AdditionalApiParameters { get; init; }
|
[JsonExtensionData]
|
||||||
|
public IDictionary<string, object> AdditionalApiParameters { get; init; }
|
||||||
}
|
}
|
||||||
@ -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 = 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:
|
// Right now, we only support streaming completions:
|
||||||
Stream = true,
|
Stream = true,
|
||||||
|
|||||||
@ -107,7 +107,6 @@ public abstract class BaseProvider : IProvider, ISecretId
|
|||||||
var retry = 0;
|
var retry = 0;
|
||||||
var response = default(HttpResponseMessage);
|
var response = default(HttpResponseMessage);
|
||||||
var errorMessage = string.Empty;
|
var errorMessage = string.Empty;
|
||||||
var errorBody = string.Empty;
|
|
||||||
while (retry++ < MAX_RETRIES)
|
while (retry++ < MAX_RETRIES)
|
||||||
{
|
{
|
||||||
using var request = await requestBuilder();
|
using var request = await requestBuilder();
|
||||||
@ -127,12 +126,11 @@ public abstract class BaseProvider : IProvider, ISecretId
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
errorBody = await nextResponse.Content.ReadAsStringAsync(token);
|
var errorBody = await nextResponse.Content.ReadAsStringAsync(token);
|
||||||
if (nextResponse.StatusCode is HttpStatusCode.Forbidden)
|
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)));
|
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("Failed request with status code {ResposeStatusCode} (message = '{ResponseReasonPhrase}', error body = '{ErrorBody}').", nextResponse.StatusCode, nextResponse.ReasonPhrase, errorBody);
|
||||||
this.logger.LogError($"Error body: {errorBody}");
|
|
||||||
errorMessage = nextResponse.ReasonPhrase;
|
errorMessage = nextResponse.ReasonPhrase;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -140,8 +138,7 @@ public abstract class BaseProvider : IProvider, ISecretId
|
|||||||
if(nextResponse.StatusCode is HttpStatusCode.BadRequest)
|
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)));
|
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("Failed request with status code {ResposeStatusCode} (message = '{ResponseReasonPhrase}', error body = '{ErrorBody}').", nextResponse.StatusCode, nextResponse.ReasonPhrase, errorBody);
|
||||||
this.logger.LogError($"Error body: {errorBody}");
|
|
||||||
errorMessage = nextResponse.ReasonPhrase;
|
errorMessage = nextResponse.ReasonPhrase;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -149,8 +146,7 @@ public abstract class BaseProvider : IProvider, ISecretId
|
|||||||
if(nextResponse.StatusCode is HttpStatusCode.NotFound)
|
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)));
|
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("Failed request with status code {ResposeStatusCode} (message = '{ResponseReasonPhrase}', error body = '{ErrorBody}').", nextResponse.StatusCode, nextResponse.ReasonPhrase, errorBody);
|
||||||
this.logger.LogError($"Error body: {errorBody}");
|
|
||||||
errorMessage = nextResponse.ReasonPhrase;
|
errorMessage = nextResponse.ReasonPhrase;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -158,8 +154,7 @@ public abstract class BaseProvider : IProvider, ISecretId
|
|||||||
if(nextResponse.StatusCode is HttpStatusCode.Unauthorized)
|
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)));
|
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("Failed request with status code {ResposeStatusCode} (message = '{ResponseReasonPhrase}', error body = '{ErrorBody}').", nextResponse.StatusCode, nextResponse.ReasonPhrase, errorBody);
|
||||||
this.logger.LogError($"Error body: {errorBody}");
|
|
||||||
errorMessage = nextResponse.ReasonPhrase;
|
errorMessage = nextResponse.ReasonPhrase;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -167,8 +162,7 @@ public abstract class BaseProvider : IProvider, ISecretId
|
|||||||
if(nextResponse.StatusCode is HttpStatusCode.InternalServerError)
|
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)));
|
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("Failed request with status code {ResposeStatusCode} (message = '{ResponseReasonPhrase}', error body = '{ErrorBody}').", nextResponse.StatusCode, nextResponse.ReasonPhrase, errorBody);
|
||||||
this.logger.LogError($"Error body: {errorBody}");
|
|
||||||
errorMessage = nextResponse.ReasonPhrase;
|
errorMessage = nextResponse.ReasonPhrase;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -176,8 +170,7 @@ public abstract class BaseProvider : IProvider, ISecretId
|
|||||||
if(nextResponse.StatusCode is HttpStatusCode.ServiceUnavailable)
|
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)));
|
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("Failed request with status code {ResposeStatusCode} (message = '{ResponseReasonPhrase}', error body = '{ErrorBody}').", nextResponse.StatusCode, nextResponse.ReasonPhrase, errorBody);
|
||||||
this.logger.LogError($"Error body: {errorBody}");
|
|
||||||
errorMessage = nextResponse.ReasonPhrase;
|
errorMessage = nextResponse.ReasonPhrase;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -187,13 +180,13 @@ public abstract class BaseProvider : IProvider, ISecretId
|
|||||||
if(timeSeconds > 90)
|
if(timeSeconds > 90)
|
||||||
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);
|
await Task.Delay(TimeSpan.FromSeconds(timeSeconds), token);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(retry >= MAX_RETRIES || !string.IsNullOrWhiteSpace(errorMessage))
|
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);
|
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
|
|||||||
/// </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="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 IDictionary<string, string> ParseApiParameters(
|
protected IDictionary<string, object> ParseApiParameters(
|
||||||
string additionalUserProvidedParameters,
|
string additionalUserProvidedParameters,
|
||||||
IEnumerable<string>? keysToRemove = null)
|
IEnumerable<string>? keysToRemove = null)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// we need to remove line breaks from the JSON string otherwise the server might have problems with parsing the call
|
// 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<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() ?? [];
|
||||||
finalKeysToRemove.Add("stream");
|
finalKeysToRemove.Add("stream");
|
||||||
finalKeysToRemove.Add("model");
|
finalKeysToRemove.Add("model");
|
||||||
finalKeysToRemove.Add("messages");
|
finalKeysToRemove.Add("messages");
|
||||||
@ -563,13 +556,28 @@ public abstract class BaseProvider : IProvider, ISecretId
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private IDictionary<string, string> ConvertToDictionary(JsonElement element)
|
private IDictionary<string, object> ConvertToDictionary(JsonElement element)
|
||||||
{
|
{
|
||||||
return element.EnumerateObject()
|
return element.EnumerateObject()
|
||||||
.ToDictionary(
|
.ToDictionary<JsonProperty, string, object>(
|
||||||
p => p.Name,
|
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}")
|
||||||
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -15,5 +15,6 @@ public readonly record struct ChatRequest(
|
|||||||
)
|
)
|
||||||
{
|
{
|
||||||
|
|
||||||
public IDictionary<string, string> AdditionalApiParameters { get; init; }
|
[JsonExtensionData]
|
||||||
|
public IDictionary<string, object> AdditionalApiParameters { get; init; }
|
||||||
}
|
}
|
||||||
@ -15,5 +15,6 @@ public readonly record struct ChatRequest(
|
|||||||
bool Stream
|
bool Stream
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
public IDictionary<string, string> AdditionalApiParameters { get; init; }
|
[JsonExtensionData]
|
||||||
|
public IDictionary<string, object> AdditionalApiParameters { get; init; }
|
||||||
}
|
}
|
||||||
@ -18,5 +18,6 @@ public readonly record struct ChatRequest(
|
|||||||
)
|
)
|
||||||
{
|
{
|
||||||
|
|
||||||
public IDictionary<string, string> AdditionalApiParameters { get; init; }
|
[JsonExtensionData]
|
||||||
|
public IDictionary<string, object> AdditionalApiParameters { get; init; }
|
||||||
}
|
}
|
||||||
@ -19,5 +19,6 @@ public readonly record struct ChatRequest(
|
|||||||
)
|
)
|
||||||
{
|
{
|
||||||
|
|
||||||
public IDictionary<string, string> AdditionalApiParameters { get; init; }
|
[JsonExtensionData]
|
||||||
|
public IDictionary<string, object> AdditionalApiParameters { get; init; }
|
||||||
}
|
}
|
||||||
@ -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 = bool.TryParse(apiParameters["safe_prompt"], out bool safePrompt) && safePrompt,
|
SafePrompt = apiParameters.TryGetValue("safe_prompt", out var value) && value is bool and true,
|
||||||
AdditionalApiParameters = apiParameters
|
AdditionalApiParameters = apiParameters
|
||||||
}, JSON_SERIALIZER_OPTIONS);
|
}, JSON_SERIALIZER_OPTIONS);
|
||||||
|
|
||||||
|
|||||||
@ -18,5 +18,6 @@ public record ChatCompletionAPIRequest(
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public IDictionary<string, string>? AdditionalApiParameters { get; init; }
|
[JsonExtensionData]
|
||||||
|
public IDictionary<string, object> ? AdditionalApiParameters { get; init; }
|
||||||
}
|
}
|
||||||
@ -21,5 +21,6 @@ public record ResponsesAPIRequest(
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public IDictionary<string, string>? AdditionalApiParameters { get; init; }
|
[JsonExtensionData]
|
||||||
|
public IDictionary<string, object> ? AdditionalApiParameters { get; init; }
|
||||||
}
|
}
|
||||||
@ -15,5 +15,6 @@ public readonly record struct ChatRequest(
|
|||||||
)
|
)
|
||||||
{
|
{
|
||||||
|
|
||||||
public IDictionary<string, string> AdditionalApiParameters { get; init; }
|
[JsonExtensionData]
|
||||||
|
public IDictionary<string, object> AdditionalApiParameters { get; init; }
|
||||||
}
|
}
|
||||||
Loading…
Reference in New Issue
Block a user