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:
var apiParameters = this.ParseAdditionalApiParameters("system");
var maxTokens = 4_096;
if (TryPopIntParameter(apiParameters, "max_tokens", out var parsedMaxTokens))
maxTokens = parsedMaxTokens;
// Build the list of messages:
var messages = await chatThread.Blocks.BuildMessagesAsync(
@ -73,7 +76,7 @@ public sealed class ProviderAnthropic() : BaseProvider(LLMProviders.ANTHROPIC, "
Messages = [..messages],
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:
Stream = true,
@ -188,4 +191,4 @@ public sealed class ProviderAnthropic() : BaseProvider(LLMProviders.ANTHROPIC, "
var modelResponse = await response.Content.ReadFromJsonAsync<ModelsResponse>(JSON_SERIALIZER_OPTIONS, token);
return modelResponse.Data;
}
}
}

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
/// (case-insensitive). The parameters stream, model, and messages are removed by default.</param>
protected IDictionary<string, object> ParseAdditionalApiParameters(
params List<string> keysToRemove)
params string[] keysToRemove)
{
if(string.IsNullOrWhiteSpace(this.AdditionalJsonApiParameters))
return new Dictionary<string, object>();
@ -744,14 +744,23 @@ public abstract class BaseProvider : IProvider, ISecretId
var dict = ConvertToDictionary(jsonDoc);
// Some keys are always removed because we set them:
keysToRemove.Add("stream");
keysToRemove.Add("model");
keysToRemove.Add("messages");
var removeSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
if (keysToRemove.Length > 0)
removeSet.UnionWith(keysToRemove);
removeSet.Add("stream");
removeSet.Add("model");
removeSet.Add("messages");
// Remove the specified keys (case-insensitive):
var removeSet = new HashSet<string>(keysToRemove, StringComparer.OrdinalIgnoreCase);
foreach (var key in removeSet)
dict.Remove(key);
if (removeSet.Count > 0)
{
foreach (var key in dict.Keys.ToList())
{
if (removeSet.Contains(key))
dict.Remove(key);
}
}
return dict;
}
@ -761,6 +770,85 @@ public abstract class BaseProvider : IProvider, ISecretId
return new Dictionary<string, object>();
}
}
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)
{
@ -785,4 +873,4 @@ public abstract class BaseProvider : IProvider, ISecretId
_ => string.Empty,
};
}
}

View File

@ -14,11 +14,12 @@ public readonly record struct ChatRequest(
string Model,
IList<IMessageBase> Messages,
bool Stream,
int RandomSeed,
[property: JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
int? RandomSeed,
bool SafePrompt = false
)
{
// Attention: The "required" modifier is not supported for [JsonExtensionData].
[JsonExtensionData]
public IDictionary<string, object> AdditionalApiParameters { get; init; } = new Dictionary<string, object>();
}
}

View File

@ -36,6 +36,8 @@ public sealed class ProviderMistral() : BaseProvider(LLMProviders.MISTRAL, "http
// Parse the API parameters:
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:
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:
Stream = true,
SafePrompt = apiParameters.TryGetValue("safe_prompt", out var value) && value is true,
RandomSeed = randomSeed,
SafePrompt = safePrompt,
AdditionalApiParameters = apiParameters
}, JSON_SERIALIZER_OPTIONS);
@ -165,4 +168,4 @@ public sealed class ProviderMistral() : BaseProvider(LLMProviders.MISTRAL, "http
var modelResponse = await response.Content.ReadFromJsonAsync<ModelsResponse>(token);
return modelResponse;
}
}
}