I let Codex test the AdditionalApiParameters and check for inconsistencies. It found some.

This commit is contained in:
Peer Schütt 2026-03-11 16:06:20 +01:00
parent 1c52d6f199
commit d73aa84e9c
4 changed files with 109 additions and 14 deletions

View File

@ -29,6 +29,9 @@ public sealed class ProviderAnthropic() : BaseProvider(LLMProviders.ANTHROPIC, "
// Parse the API parameters: // Parse the API parameters:
var apiParameters = this.ParseAdditionalApiParameters("system"); var apiParameters = this.ParseAdditionalApiParameters("system");
var maxTokens = 4_096;
if (TryPopIntParameter(apiParameters, "max_tokens", out var parsedMaxTokens))
maxTokens = parsedMaxTokens;
// Build the list of messages: // Build the list of messages:
var messages = await chatThread.Blocks.BuildMessagesAsync( var messages = await chatThread.Blocks.BuildMessagesAsync(
@ -73,7 +76,7 @@ public sealed class ProviderAnthropic() : BaseProvider(LLMProviders.ANTHROPIC, "
Messages = [..messages], Messages = [..messages],
System = chatThread.PrepareSystemPrompt(settingsManager), System = chatThread.PrepareSystemPrompt(settingsManager),
MaxTokens = apiParameters.TryGetValue("max_tokens", out var value) && value is int intValue ? intValue : 4_096, MaxTokens = maxTokens,
// Right now, we only support streaming completions: // Right now, we only support streaming completions:
Stream = true, Stream = true,

View File

@ -731,7 +731,7 @@ public abstract class BaseProvider : IProvider, ISecretId
/// <param name="keysToRemove">Optional list of keys to remove from the final dictionary /// <param name="keysToRemove">Optional list of keys to remove from the final dictionary
/// (case-insensitive). The parameters stream, model, and messages are removed by default.</param> /// (case-insensitive). The parameters stream, model, and messages are removed by default.</param>
protected IDictionary<string, object> ParseAdditionalApiParameters( protected IDictionary<string, object> ParseAdditionalApiParameters(
params List<string> keysToRemove) params string[] keysToRemove)
{ {
if(string.IsNullOrWhiteSpace(this.AdditionalJsonApiParameters)) if(string.IsNullOrWhiteSpace(this.AdditionalJsonApiParameters))
return new Dictionary<string, object>(); return new Dictionary<string, object>();
@ -744,14 +744,23 @@ public abstract class BaseProvider : IProvider, ISecretId
var dict = ConvertToDictionary(jsonDoc); var dict = ConvertToDictionary(jsonDoc);
// Some keys are always removed because we set them: // Some keys are always removed because we set them:
keysToRemove.Add("stream"); var removeSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
keysToRemove.Add("model"); if (keysToRemove.Length > 0)
keysToRemove.Add("messages"); removeSet.UnionWith(keysToRemove);
removeSet.Add("stream");
removeSet.Add("model");
removeSet.Add("messages");
// Remove the specified keys (case-insensitive): // Remove the specified keys (case-insensitive):
var removeSet = new HashSet<string>(keysToRemove, StringComparer.OrdinalIgnoreCase); if (removeSet.Count > 0)
foreach (var key in removeSet) {
foreach (var key in dict.Keys.ToList())
{
if (removeSet.Contains(key))
dict.Remove(key); dict.Remove(key);
}
}
return dict; return dict;
} }
@ -762,6 +771,85 @@ public abstract class BaseProvider : IProvider, ISecretId
} }
} }
protected static bool TryPopIntParameter(IDictionary<string, object> parameters, string key, out int value)
{
value = default;
if (!TryPopParameter(parameters, key, out var raw) || raw is null)
return false;
switch (raw)
{
case int i:
value = i;
return true;
case long l when l is >= int.MinValue and <= int.MaxValue:
value = (int)l;
return true;
case double d when d is >= int.MinValue and <= int.MaxValue:
value = (int)d;
return true;
case decimal m when m is >= int.MinValue and <= int.MaxValue:
value = (int)m;
return true;
}
return false;
}
protected static bool TryPopBoolParameter(IDictionary<string, object> parameters, string key, out bool value)
{
value = default;
if (!TryPopParameter(parameters, key, out var raw) || raw is null)
return false;
switch (raw)
{
case bool b:
value = b;
return true;
case string s when bool.TryParse(s, out var parsed):
value = parsed;
return true;
case int i:
value = i != 0;
return true;
case long l:
value = l != 0;
return true;
case double d:
value = Math.Abs(d) > double.Epsilon;
return true;
case decimal m:
value = m != 0;
return true;
}
return false;
}
private static bool TryPopParameter(IDictionary<string, object> parameters, string key, out object? value)
{
value = null;
if (parameters.Count == 0)
return false;
var foundKey = parameters.Keys.FirstOrDefault(k => string.Equals(k, key, StringComparison.OrdinalIgnoreCase));
if (foundKey is null)
return false;
value = parameters[foundKey];
parameters.Remove(foundKey);
return true;
}
private static IDictionary<string, object> ConvertToDictionary(JsonElement element) private static IDictionary<string, object> ConvertToDictionary(JsonElement element)
{ {
return element.EnumerateObject() return element.EnumerateObject()

View File

@ -14,7 +14,8 @@ public readonly record struct ChatRequest(
string Model, string Model,
IList<IMessageBase> Messages, IList<IMessageBase> Messages,
bool Stream, bool Stream,
int RandomSeed, [property: JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
int? RandomSeed,
bool SafePrompt = false bool SafePrompt = false
) )
{ {

View File

@ -36,6 +36,8 @@ public sealed class ProviderMistral() : BaseProvider(LLMProviders.MISTRAL, "http
// Parse the API parameters: // Parse the API parameters:
var apiParameters = this.ParseAdditionalApiParameters(); var apiParameters = this.ParseAdditionalApiParameters();
var safePrompt = TryPopBoolParameter(apiParameters, "safe_prompt", out var parsedSafePrompt) && parsedSafePrompt;
var randomSeed = TryPopIntParameter(apiParameters, "random_seed", out var parsedRandomSeed) ? parsedRandomSeed : (int?)null;
// Build the list of messages: // Build the list of messages:
var messages = await chatThread.Blocks.BuildMessagesUsingDirectImageUrlAsync(this.Provider, chatModel); var messages = await chatThread.Blocks.BuildMessagesUsingDirectImageUrlAsync(this.Provider, chatModel);
@ -52,7 +54,8 @@ public sealed class ProviderMistral() : BaseProvider(LLMProviders.MISTRAL, "http
// Right now, we only support streaming completions: // Right now, we only support streaming completions:
Stream = true, Stream = true,
SafePrompt = apiParameters.TryGetValue("safe_prompt", out var value) && value is true, RandomSeed = randomSeed,
SafePrompt = safePrompt,
AdditionalApiParameters = apiParameters AdditionalApiParameters = apiParameters
}, JSON_SERIALIZER_OPTIONS); }, JSON_SERIALIZER_OPTIONS);