Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 42 additions & 4 deletions crates/forge_app/src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -522,6 +522,18 @@ pub fn enforce_strict_schema(schema: &mut serde_json::Value, strict_mode: bool)

normalize_array_items(map, strict_mode);

// A `null` literal inside a typed `enum` array (produced by
// schemars' AddNullable transform for Option<Enum> fields) is a
// non-standard representation that several OpenAI-compatible
// providers reject. For example, Moonshot AI (kimi) returns:
// "enum value (<nil>) does not match any type in [string]".
// Strip null entries from enum arrays unconditionally so the
// schema is valid regardless of whether strict mode is applied
// (e.g. when kimi is routed through OpenRouter).
if let Some(serde_json::Value::Array(enum_values)) = map.get_mut("enum") {
enum_values.retain(|v| !v.is_null());
}

if strict_mode
&& map
.get("nullable")
Expand All @@ -530,10 +542,6 @@ pub fn enforce_strict_schema(schema: &mut serde_json::Value, strict_mode: bool)
{
map.remove("nullable");

if let Some(serde_json::Value::Array(enum_values)) = map.get_mut("enum") {
enum_values.retain(|v| !v.is_null());
}

let description = map.remove("description");
let non_null_branch = serde_json::Value::Object(std::mem::take(map));
let null_branch = serde_json::json!({"type": "null"});
Expand Down Expand Up @@ -1327,6 +1335,36 @@ mod tests {
assert!(schema["properties"]["output_mode"].get("anyOf").is_none());
}

#[test]
fn test_null_enum_values_stripped_in_non_strict_mode() {
// schemars' AddNullable transform appends `null` to enum arrays for
// Option<Enum> fields. Several OpenAI-compatible providers (e.g.
// Moonshot/kimi via OpenRouter) reject a null literal inside a typed
// enum array. Null entries must be stripped even in non-strict mode so
// the schema is valid regardless of the provider routing.
let mut schema = json!({
"type": "object",
"properties": {
"output_mode": {
"nullable": true,
"type": "string",
"enum": ["content", "files_with_matches", "count", null]
}
}
});

enforce_strict_schema(&mut schema, false);

// The null entry is removed, but nullable is preserved (non-strict mode
// does not convert to anyOf).
assert_eq!(
schema["properties"]["output_mode"]["enum"],
json!(["content", "files_with_matches", "count"])
);
assert_eq!(schema["properties"]["output_mode"]["nullable"], json!(true));
assert!(schema["properties"]["output_mode"].get("anyOf").is_none());
}

#[test]
fn test_schema_valued_additional_properties_is_normalized() {
let mut schema = json!({
Expand Down
Loading