using System.Text.Json;
using System.Text.Json.Nodes;
namespace AIStudio.Provider.OpenAI;
///
/// Translates a tool's parameter schema into the form OpenAI's strict mode requires.
///
///
/// Strict mode does not accept an optional argument the ordinary JSON Schema way. It insists that
/// every property appears in required, and an argument that may be left out has to say so
/// by allowing null instead — "type": ["string", "null"], and null among its enum
/// values where it has any.
/// Tool definitions are written the ordinary way, so this converts on the way out. Both forms mean
/// the same to a tool: a null argument and an absent one are treated alike.
///
public static class OpenAIStrictToolSchema
{
private const string NULL_TYPE = "null";
///
/// Converts one parameter schema, leaving it untouched when every argument is required
/// anyway.
///
public static JsonElement FromToolParameters(JsonElement parameters)
{
if (parameters.ValueKind is not JsonValueKind.Object)
return parameters;
if (JsonNode.Parse(parameters.GetRawText()) is not JsonObject schema)
return parameters;
if (schema["properties"] is not JsonObject properties)
return parameters;
var requiredNames = schema["required"] is JsonArray required
? required.Select(entry => entry?.GetValue()).Where(entry => entry is not null).ToHashSet(StringComparer.Ordinal)
: [];
var optionalPropertyNames = properties
.Select(property => property.Key)
.Where(propertyName => !requiredNames.Contains(propertyName))
.ToList();
if (optionalPropertyNames.Count is 0)
return parameters;
foreach (var propertyName in optionalPropertyNames)
{
if (properties[propertyName] is not JsonObject property)
continue;
AllowNullType(property);
AllowNullEnumValue(property);
}
//
// Every property is required in strict mode. The order follows the properties, so the
// schema stays stable across requests, which prompt caching depends on.
//
schema["required"] = new JsonArray([..properties.Select(property => JsonValue.Create(property.Key))]);
return JsonSerializer.Deserialize(schema.ToJsonString());
}
private static void AllowNullType(JsonObject property)
{
switch (property["type"])
{
case JsonValue singleType when singleType.TryGetValue(out var typeName) && !typeName.Equals(NULL_TYPE, StringComparison.Ordinal):
property["type"] = new JsonArray(JsonValue.Create(typeName), JsonValue.Create(NULL_TYPE));
break;
case JsonArray types when types.All(entry => entry?.GetValue() != NULL_TYPE):
types.Add(JsonValue.Create(NULL_TYPE));
break;
}
}
private static void AllowNullEnumValue(JsonObject property)
{
// Only where the property restricts its values at all: adding null to an absent enum
// would turn an unrestricted argument into one that may only be null.
if (property["enum"] is not JsonArray enumValues || enumValues.Any(entry => entry is null))
return;
enumValues.Insert(0, null);
}
}