gproxy-protocol 4.0.0

GPROXY v4 connection modelling, operation declarations, vendor wire types and pairwise conversion
Documentation
use gproxy_protocol::openai::responses::tools::*;

fn round_trip(value: serde_json::Value) -> serde_json::Value {
    serde_json::to_value(serde_json::from_value::<Tool>(value).unwrap()).unwrap()
}

#[test]
fn filters_have_typed_operator_and_mixed_in_values() {
    let value = serde_json::json!({"type":"file_search","vector_store_ids":["vs"],"filters":{"type":"in","key":"status","value":["new",1]},"future":7});
    let tool: Tool = serde_json::from_value(value).unwrap();
    let Tool::FileSearch(file) = tool else {
        panic!()
    };
    let Some(Some(FileSearchFilter::Comparison(filter))) = file.filters else {
        panic!()
    };
    assert!(matches!(filter.type_, ComparisonFilterType::In));
    assert!(file.rest.contains_key("future"));
    let invalid = serde_json::json!({"type":"file_search","vector_store_ids":["vs"],"filters":{"type":"in","key":"status","value":[true]}});
    assert!(serde_json::from_value::<Tool>(invalid).is_err());
}

#[test]
fn compound_filter_and_or_are_recursive_and_unknown_is_rest() {
    let value = serde_json::json!({"type":"file_search","vector_store_ids":["vs"],"filters":{"type":"and","filters":[{"type":"eq","key":"a","value":1},{"type":"or","filters":[{"type":"ne","key":"b","value":false}],"nested_future":true}]}});
    let Tool::FileSearch(file) = serde_json::from_value::<Tool>(value).unwrap() else {
        panic!()
    };
    assert!(matches!(
        file.filters,
        Some(Some(FileSearchFilter::Compound(_)))
    ));
}

#[test]
fn shell_supports_all_three_typed_environments() {
    for (env, expected) in [
        (
            serde_json::json!({"type":"container_auto","memory_limit":"1g"}),
            "auto",
        ),
        (
            serde_json::json!({"type":"local","skills":[{"name":"s","description":"d","path":"/s"}]}),
            "local",
        ),
        (
            serde_json::json!({"type":"container_reference","container_id":"c"}),
            "reference",
        ),
    ] {
        let value =
            serde_json::json!({"type":"shell","environment":env,"allowed_callers":["direct"]});
        let Tool::Shell(shell) = serde_json::from_value::<Tool>(value).unwrap() else {
            panic!()
        };
        assert!(matches!(
            (expected, shell.environment),
            ("auto", Some(Some(ShellEnvironment::Auto(_))))
                | ("local", Some(Some(ShellEnvironment::Local(_))))
                | ("reference", Some(Some(ShellEnvironment::Reference(_))))
        ));
    }
}

#[test]
fn code_interpreter_auto_container_and_memory_limits_are_typed() {
    let value = serde_json::json!({"type":"code_interpreter","container":{"type":"auto","file_ids":["f"],"memory_limit":"64g","network_policy":{"type":"disabled"}},"allowed_callers":["programmatic"]});
    let Tool::CodeInterpreter(tool) = serde_json::from_value(value).unwrap() else {
        panic!()
    };
    assert!(
        matches!(tool.container, CodeInterpreterContainer::Auto(auto) if matches!(auto.memory_limit, Some(Some(MemoryLimit::SixtyFourG))))
    );
}

#[test]
fn mcp_allowed_tools_and_approval_support_array_and_filter_forms() {
    for (allowed_tools, approval) in [
        (serde_json::json!(["read"]), serde_json::json!("never")),
        (
            serde_json::json!({"read_only":true,"tool_names":["write"]}),
            serde_json::json!({"always":{"read_only":false}}),
        ),
    ] {
        let value = serde_json::json!({"type":"mcp","server_label":"srv","allowed_tools":allowed_tools,"require_approval":approval,"headers":{"x":"y"},"connector_id":"connector_gmail","future":1});
        let Tool::Mcp(tool) = serde_json::from_value::<Tool>(value).unwrap() else {
            panic!()
        };
        assert!(tool.rest.contains_key("future"));
        assert!(tool.headers.is_some());
    }
}

#[test]
fn web_search_preview_long_tag_and_location_are_preserved() {
    let value = serde_json::json!({"type":"web_search_preview_2025_03_11","search_context_size":"high","search_content_types":["text","image"],"user_location":{"type":"approximate","country":"US"}});
    let encoded = round_trip(value.clone());
    assert_eq!(encoded, value);
}

#[test]
fn image_mask_and_custom_grammar_are_typed() {
    let image = serde_json::json!({"type":"image_generation","input_fidelity":"high","input_image_mask":{"image_url":"https://x"},"moderation":"low","model":"any-model","size":"any-size"});
    let Tool::ImageGeneration(image) = serde_json::from_value::<Tool>(image).unwrap() else {
        panic!()
    };
    assert!(matches!(
        image.input_image_mask,
        Some(InputImageMask {
            image_url: Some(_),
            ..
        })
    ));
    let custom = serde_json::json!({"type":"custom","name":"parse","format":{"type":"grammar","definition":"start: WORD","syntax":"lark"}});
    let Tool::Custom(custom) = serde_json::from_value::<Tool>(custom).unwrap() else {
        panic!()
    };
    assert!(matches!(custom.format, Some(CustomToolFormat::Grammar(_))));
}

#[test]
fn namespace_function_and_custom_keep_distinct_optional_fields() {
    let value = serde_json::json!({"type":"namespace","name":"crm","description":"CRM","tools":[{"name":"lookup","type":"function","parameters":null,"strict":null},{"name":"emit","type":"custom","format":{"type":"text"}}]});
    let Tool::Namespace(namespace) = serde_json::from_value::<Tool>(value).unwrap() else {
        panic!()
    };
    assert_eq!(namespace.tools.len(), 2);
    assert!(matches!(
        namespace.tools[0],
        NamespaceToolDefinition::Function(_)
    ));
    assert!(matches!(
        namespace.tools[1],
        NamespaceToolDefinition::Custom(_)
    ));
}

#[test]
fn documented_nullable_fields_preserve_missing_null_and_value() {
    use serde_json::{Value, json};

    let cases: Vec<(Value, &[&str])> = vec![
        (
            json!({"type":"function","name":"f","parameters":{},"strict":false,"description":"d","allowed_callers":["direct"],"output_schema":{}}),
            &["/description", "/allowed_callers", "/output_schema"],
        ),
        (
            json!({"type":"file_search","vector_store_ids":[],"filters":{"type":"eq","key":"a","value":1}}),
            &["/filters"],
        ),
        (
            json!({"type":"web_search","filters":{"allowed_domains":["example.com"]},"user_location":{"city":"c","country":"US","region":"r","timezone":"UTC"}}),
            &[
                "/filters",
                "/filters/allowed_domains",
                "/user_location",
                "/user_location/city",
                "/user_location/country",
                "/user_location/region",
                "/user_location/timezone",
            ],
        ),
        (
            json!({"type":"web_search_preview","user_location":{"type":"approximate","city":"c","country":"US","region":"r","timezone":"UTC"}}),
            &[
                "/user_location",
                "/user_location/city",
                "/user_location/country",
                "/user_location/region",
                "/user_location/timezone",
            ],
        ),
        (
            json!({"type":"code_interpreter","container":{"type":"auto","memory_limit":"4g"},"allowed_callers":["programmatic"]}),
            &["/container/memory_limit", "/allowed_callers"],
        ),
        (
            json!({"type":"custom","name":"c","allowed_callers":["direct"]}),
            &["/allowed_callers"],
        ),
        (
            json!({"type":"apply_patch","allowed_callers":["direct"]}),
            &["/allowed_callers"],
        ),
        (
            json!({"type":"shell","allowed_callers":["direct"],"environment":{"type":"container_auto","memory_limit":"16g"}}),
            &[
                "/allowed_callers",
                "/environment",
                "/environment/memory_limit",
            ],
        ),
        (
            json!({"type":"image_generation","input_fidelity":"high"}),
            &["/input_fidelity"],
        ),
        (
            json!({"type":"mcp","server_label":"m","allowed_callers":["direct"],"allowed_tools":["f"],"headers":{"x":"y"},"require_approval":"never"}),
            &[
                "/allowed_callers",
                "/allowed_tools",
                "/headers",
                "/require_approval",
            ],
        ),
        (
            json!({"type":"tool_search","description":"d","parameters":[1,"arbitrary",true]}),
            &["/description", "/parameters"],
        ),
        (
            json!({"type":"namespace","name":"n","description":"d","tools":[{"type":"function","name":"f","allowed_callers":["direct"],"description":"d","output_schema":{},"parameters":[1],"strict":false},{"type":"custom","name":"c","allowed_callers":["programmatic"]}]}),
            &[
                "/tools/0/allowed_callers",
                "/tools/0/description",
                "/tools/0/output_schema",
                "/tools/0/parameters",
                "/tools/0/strict",
                "/tools/1/allowed_callers",
            ],
        ),
    ];
    for (value, paths) in cases {
        assert_eq!(round_trip(value.clone()), value);
        for path in paths {
            let (parent, key) = path.rsplit_once('/').unwrap();
            let mut missing = value.clone();
            missing
                .pointer_mut(parent)
                .unwrap()
                .as_object_mut()
                .unwrap()
                .remove(key);
            assert_eq!(round_trip(missing.clone()), missing, "missing {path}");
            let mut null = value.clone();
            *null.pointer_mut(path).unwrap() = Value::Null;
            assert_eq!(round_trip(null.clone()), null, "null {path}");
        }
    }
}

#[test]
fn required_nullable_function_fields_remain_required_in_wire_and_builder() {
    use serde_json::json;

    for (parameters, strict) in [(json!(null), json!(null)), (json!({}), json!(false))] {
        let value = json!({"type":"function","name":"f","parameters":parameters,"strict":strict});
        assert_eq!(round_trip(value.clone()), value);
        for key in ["parameters", "strict"] {
            let mut missing = value.clone();
            missing.as_object_mut().unwrap().remove(key);
            assert!(
                serde_json::from_value::<Tool>(missing).is_err(),
                "missing {key}"
            );
        }
    }
    let tool = Tool::Function(FunctionTool::builder("f".into(), None, None).build());
    assert_eq!(
        serde_json::to_value(tool).unwrap(),
        json!({"type":"function","name":"f","parameters":null,"strict":null})
    );
    let tool = Tool::Function(
        FunctionTool::builder("f".into(), None, None)
            .description(None)
            .build(),
    );
    assert_eq!(
        serde_json::to_value(tool).unwrap(),
        json!({"type":"function","name":"f","parameters":null,"strict":null,"description":null})
    );
}

#[test]
fn computer_environment_and_allowed_callers_reject_unknown_values() {
    for value in [
        serde_json::json!({"type":"computer_use_preview","display_height":1,"display_width":1,"environment":"anything"}),
        serde_json::json!({"type":"function","name":"f","parameters":{},"strict":false,"allowed_callers":["anything"]}),
        serde_json::json!({"type":"custom","name":"c","allowed_callers":["anything"]}),
    ] {
        assert!(serde_json::from_value::<Tool>(value).is_err());
    }
}