use serde_json::Value;
pub fn count_optional_params(schema: &Value) -> usize {
let obj = match schema.as_object() {
Some(o) => o,
None => return 0,
};
let properties = match obj.get("properties").and_then(|v| v.as_object()) {
Some(p) => p,
None => return 0,
};
let required: std::collections::HashSet<&str> = obj
.get("required")
.and_then(|v| v.as_array())
.map(|arr| arr.iter().filter_map(|v| v.as_str()).collect())
.unwrap_or_default();
properties
.keys()
.filter(|k| !required.contains(k.as_str()))
.count()
}
pub fn count_union_type_params(schema: &Value) -> usize {
let obj = match schema.as_object() {
Some(o) => o,
None => return 0,
};
let properties = match obj.get("properties").and_then(|v| v.as_object()) {
Some(p) => p,
None => return 0,
};
properties
.values()
.filter(|prop| {
let prop_obj = match prop.as_object() {
Some(o) => o,
None => return false,
};
if prop_obj.contains_key("anyOf") {
return true;
}
if let Some(Value::Array(_)) = prop_obj.get("type") {
return true;
}
false
})
.count()
}
pub const ANTHROPIC_MAX_STRICT_TOOLS: usize = 20;
pub const ANTHROPIC_MAX_OPTIONAL_PARAMS: usize = 24;
pub const ANTHROPIC_MAX_UNION_TYPE_PARAMS: usize = 16;
#[derive(Debug, Clone)]
pub struct StrictBudgetCheck {
pub total_strict_tools: usize,
pub total_optional_params: usize,
pub total_union_type_params: usize,
pub exceeds_limits: bool,
}
pub fn check_anthropic_strict_budget<'a>(
tools: impl Iterator<Item = (bool, &'a Value)>,
) -> StrictBudgetCheck {
let mut total_strict_tools = 0usize;
let mut total_optional_params = 0usize;
let mut total_union_type_params = 0usize;
for (is_strict, schema) in tools {
if is_strict {
total_strict_tools += 1;
total_optional_params += count_optional_params(schema);
total_union_type_params += count_union_type_params(schema);
}
}
let exceeds_limits = total_strict_tools > ANTHROPIC_MAX_STRICT_TOOLS
|| total_optional_params > ANTHROPIC_MAX_OPTIONAL_PARAMS
|| total_union_type_params > ANTHROPIC_MAX_UNION_TYPE_PARAMS;
StrictBudgetCheck {
total_strict_tools,
total_optional_params,
total_union_type_params,
exceeds_limits,
}
}
pub fn strip_keys_recursive(value: Value, keys: &[&str]) -> Value {
match value {
Value::Object(mut obj) => {
for key in keys {
obj.remove(*key);
}
let sanitized = obj
.into_iter()
.map(|(k, v)| (k, strip_keys_recursive(v, keys)))
.collect();
Value::Object(sanitized)
}
Value::Array(arr) => Value::Array(
arr.into_iter()
.map(|v| strip_keys_recursive(v, keys))
.collect(),
),
other => other,
}
}
pub fn ensure_additional_properties_false(value: Value) -> Value {
match value {
Value::Object(mut obj) => {
let is_object_type = obj
.get("type")
.map(|t| match t {
Value::String(s) => s == "object",
Value::Array(arr) => arr.iter().any(|v| v.as_str() == Some("object")),
_ => false,
})
.unwrap_or(false);
if is_object_type && !obj.contains_key("additionalProperties") {
obj.insert("additionalProperties".into(), Value::Bool(false));
}
let sanitized = obj
.into_iter()
.map(|(k, v)| (k, ensure_additional_properties_false(v)))
.collect();
Value::Object(sanitized)
}
Value::Array(arr) => Value::Array(
arr.into_iter()
.map(ensure_additional_properties_false)
.collect(),
),
other => other,
}
}
pub fn convert_type_arrays_to_nullable(value: Value) -> Value {
match value {
Value::Object(mut obj) => {
if let Some(Value::Array(types)) = obj.get("type").cloned() {
let mut nullable = false;
let mut non_null_types = Vec::new();
for entry in types {
match entry {
Value::String(s) if s == "null" => nullable = true,
Value::String(s) => non_null_types.push(s),
_ => {}
}
}
if let Some(primary) = non_null_types.into_iter().next() {
obj.insert("type".into(), Value::String(primary));
if nullable {
obj.insert("nullable".into(), Value::Bool(true));
}
} else {
obj.remove("type");
}
}
let sanitized = obj
.into_iter()
.map(|(k, v)| (k, convert_type_arrays_to_nullable(v)))
.collect();
Value::Object(sanitized)
}
Value::Array(arr) => Value::Array(
arr.into_iter()
.map(convert_type_arrays_to_nullable)
.collect(),
),
other => other,
}
}
pub fn normalize_for_openai_strict(value: Value) -> Value {
match value {
Value::Object(mut obj) => {
let is_object_type = obj
.get("type")
.map(|t| matches!(t, Value::String(s) if s == "object"))
.unwrap_or(false);
if is_object_type {
if !obj.contains_key("additionalProperties") {
obj.insert("additionalProperties".into(), Value::Bool(false));
}
if let Some(properties) = obj.get("properties").cloned() {
if let Some(props_obj) = properties.as_object() {
let required: std::collections::HashSet<String> = obj
.get("required")
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str().map(|s| s.to_string()))
.collect()
})
.unwrap_or_default();
let all_keys: Vec<String> = props_obj.keys().cloned().collect();
let mut new_props = props_obj.clone();
for key in &all_keys {
if !required.contains(key) {
if let Some(prop) = new_props.get_mut(key) {
make_nullable(prop);
}
}
}
obj.insert("properties".into(), Value::Object(new_props));
let all_required: Vec<Value> =
all_keys.into_iter().map(Value::String).collect();
obj.insert("required".into(), Value::Array(all_required));
}
}
}
let sanitized = obj
.into_iter()
.map(|(k, v)| (k, normalize_for_openai_strict(v)))
.collect();
Value::Object(sanitized)
}
Value::Array(arr) => {
Value::Array(arr.into_iter().map(normalize_for_openai_strict).collect())
}
other => other,
}
}
fn make_nullable(prop: &mut Value) {
if let Some(obj) = prop.as_object_mut() {
match obj.get("type").cloned() {
Some(Value::String(s)) => {
obj.insert(
"type".into(),
Value::Array(vec![Value::String(s), Value::String("null".into())]),
);
}
Some(Value::Array(mut arr)) => {
let has_null = arr.iter().any(|v| v.as_str() == Some("null"));
if !has_null {
arr.push(Value::String("null".into()));
obj.insert("type".into(), Value::Array(arr));
}
}
_ => {
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_count_optional_params_all_required() {
let schema = json!({
"type": "object",
"properties": {
"a": {"type": "string"},
"b": {"type": "integer"}
},
"required": ["a", "b"]
});
assert_eq!(count_optional_params(&schema), 0);
}
#[test]
fn test_count_optional_params_some_optional() {
let schema = json!({
"type": "object",
"properties": {
"a": {"type": "string"},
"b": {"type": "integer"},
"c": {"type": "boolean"}
},
"required": ["a"]
});
assert_eq!(count_optional_params(&schema), 2);
}
#[test]
fn test_count_optional_params_no_required_array() {
let schema = json!({
"type": "object",
"properties": {
"a": {"type": "string"},
"b": {"type": "integer"}
}
});
assert_eq!(count_optional_params(&schema), 2);
}
#[test]
fn test_count_optional_params_no_properties() {
let schema = json!({"type": "object"});
assert_eq!(count_optional_params(&schema), 0);
}
#[test]
fn test_count_union_type_params_with_anyof() {
let schema = json!({
"type": "object",
"properties": {
"a": {"anyOf": [{"type": "string"}, {"type": "integer"}]},
"b": {"type": "string"}
}
});
assert_eq!(count_union_type_params(&schema), 1);
}
#[test]
fn test_count_union_type_params_with_type_array() {
let schema = json!({
"type": "object",
"properties": {
"a": {"type": ["string", "null"]},
"b": {"type": "string"}
}
});
assert_eq!(count_union_type_params(&schema), 1);
}
#[test]
fn test_budget_within_limits() {
let schema = json!({
"type": "object",
"properties": {
"a": {"type": "string"}
},
"required": ["a"]
});
let check = check_anthropic_strict_budget(vec![(true, &schema)].into_iter());
assert!(!check.exceeds_limits);
assert_eq!(check.total_strict_tools, 1);
assert_eq!(check.total_optional_params, 0);
}
#[test]
fn test_budget_exceeds_optional_params() {
let schema = json!({
"type": "object",
"properties": {
"a": {"type": "string"},
"b": {"type": "string"},
"c": {"type": "string"},
"d": {"type": "string"},
"e": {"type": "string"},
"f": {"type": "string"}
},
"required": []
});
let tools: Vec<(bool, &Value)> = (0..5).map(|_| (true, &schema)).collect();
let check = check_anthropic_strict_budget(tools.into_iter());
assert!(check.exceeds_limits);
assert_eq!(check.total_optional_params, 30);
}
#[test]
fn test_budget_exceeds_tool_count() {
let schema = json!({"type": "object", "properties": {}, "required": []});
let tools: Vec<(bool, &Value)> = (0..25).map(|_| (true, &schema)).collect();
let check = check_anthropic_strict_budget(tools.into_iter());
assert!(check.exceeds_limits);
assert_eq!(check.total_strict_tools, 25);
}
#[test]
fn test_budget_non_strict_tools_ignored() {
let schema = json!({
"type": "object",
"properties": {
"a": {"type": "string"},
"b": {"type": "string"}
}
});
let tools: Vec<(bool, &Value)> = (0..50).map(|_| (false, &schema)).collect();
let check = check_anthropic_strict_budget(tools.into_iter());
assert!(!check.exceeds_limits);
assert_eq!(check.total_strict_tools, 0);
assert_eq!(check.total_optional_params, 0);
}
#[test]
fn test_strip_keys_recursive() {
let schema = json!({
"type": "object",
"strict": true,
"additionalProperties": false,
"properties": {
"a": {
"type": "string",
"strict": true,
"additionalProperties": false
}
}
});
let result = strip_keys_recursive(schema, &["strict", "additionalProperties"]);
assert!(result.get("strict").is_none());
assert!(result.get("additionalProperties").is_none());
let a = &result["properties"]["a"];
assert!(a.get("strict").is_none());
assert!(a.get("additionalProperties").is_none());
}
#[test]
fn test_ensure_additional_properties_false() {
let schema = json!({
"type": "object",
"properties": {
"nested": {
"type": "object",
"properties": {
"x": {"type": "string"}
}
}
}
});
let result = ensure_additional_properties_false(schema);
assert_eq!(result["additionalProperties"], false);
assert_eq!(
result["properties"]["nested"]["additionalProperties"],
false
);
}
#[test]
fn test_ensure_additional_properties_preserves_existing() {
let schema = json!({
"type": "object",
"additionalProperties": true,
"properties": {}
});
let result = ensure_additional_properties_false(schema);
assert_eq!(result["additionalProperties"], true);
}
#[test]
fn test_convert_type_arrays_to_nullable() {
let schema = json!({
"type": "object",
"properties": {
"a": {"type": ["string", "null"]}
}
});
let result = convert_type_arrays_to_nullable(schema);
assert_eq!(result["properties"]["a"]["type"], "string");
assert_eq!(result["properties"]["a"]["nullable"], true);
}
#[test]
fn test_normalize_for_openai_strict() {
let schema = json!({
"type": "object",
"properties": {
"required_param": {"type": "string"},
"optional_param": {"type": "integer"}
},
"required": ["required_param"]
});
let result = normalize_for_openai_strict(schema);
let required = result["required"].as_array().unwrap();
assert!(required.contains(&json!("required_param")));
assert!(required.contains(&json!("optional_param")));
let opt_type = &result["properties"]["optional_param"]["type"];
assert!(opt_type.is_array());
let types = opt_type.as_array().unwrap();
assert!(types.contains(&json!("integer")));
assert!(types.contains(&json!("null")));
assert_eq!(result["properties"]["required_param"]["type"], "string");
assert_eq!(result["additionalProperties"], false);
}
#[test]
fn test_normalize_for_openai_strict_nested() {
let schema = json!({
"type": "object",
"properties": {
"config": {
"type": "object",
"properties": {
"name": {"type": "string"},
"value": {"type": "number"}
},
"required": ["name"]
}
},
"required": ["config"]
});
let result = normalize_for_openai_strict(schema);
let nested = &result["properties"]["config"];
assert_eq!(nested["additionalProperties"], false);
let nested_required = nested["required"].as_array().unwrap();
assert!(nested_required.contains(&json!("name")));
assert!(nested_required.contains(&json!("value")));
}
#[test]
fn test_make_nullable_string_type() {
let mut prop = json!({"type": "string", "description": "A name"});
make_nullable(&mut prop);
assert_eq!(prop["type"], json!(["string", "null"]));
}
#[test]
fn test_make_nullable_already_nullable() {
let mut prop = json!({"type": ["string", "null"]});
make_nullable(&mut prop);
let types = prop["type"].as_array().unwrap();
assert_eq!(types.len(), 2);
}
#[test]
fn test_make_nullable_array_type() {
let mut prop = json!({"type": ["string", "integer"]});
make_nullable(&mut prop);
let types = prop["type"].as_array().unwrap();
assert_eq!(types.len(), 3);
assert!(types.contains(&json!("null")));
}
}