gproxy-protocol 4.1.4

GPROXY v4 connection modelling, operation declarations, vendor wire types and pairwise conversion
Documentation
use gproxy_protocol::{
    Dialect,
    transform::{
        generate::{
            chat_responses, claude_chat, claude_gemini, claude_responses, gemini_chat,
            gemini_responses,
        },
        identity::{IdNamespace, IdentityFlow, TargetIdPolicy},
    },
    wire::{
        claude::generate_content as c,
        gemini as g,
        openai::{chat as h, responses as r},
    },
};
use serde_json::{Value, json};
fn flow() -> IdentityFlow {
    IdentityFlow::new(IdNamespace::with_bytes([91; 16]))
}
fn policy(d: Dialect) -> TargetIdPolicy {
    TargetIdPolicy::new(d)
}
fn wire<T: serde::Serialize>(v: T) -> Value {
    serde_json::to_value(v).unwrap()
}
fn has_text(v: &Value) {
    assert!(v.to_string().contains("keep this text"), "{v}");
    assert!(!v.to_string().contains("foreign-extension"));
}

#[test]
fn claude_controls_and_opaque_blocks_do_not_veto_supported_content() {
    let input: c::GenerateContentRequestBody = serde_json::from_value(json!({
        "model":"source", "max_tokens":64, "top_k":17,
        "thinking":{"type":"enabled","budget_tokens":12,"display":"omitted"},
        "messages":[{"role":"user","content":[
            {"type":"text","text":"keep this text"},
            {"type":"compaction","content":"opaque context"}
        ]}], "foreign-extension":true
    }))
    .unwrap();
    let chat = wire(
        claude_chat::claude_to_openai(&input, "target")
            .unwrap()
            .value,
    );
    has_text(&chat);
    assert_eq!(chat["max_completion_tokens"], 64);
    assert!(chat.get("top_k").is_none());
    let responses = wire(
        claude_responses::claude_to_responses_request(
            input.clone(),
            "target",
            &mut flow(),
            &policy(Dialect::OpenAi),
        )
        .unwrap()
        .value,
    );
    has_text(&responses);
    assert_eq!(responses["max_output_tokens"], 64);
    let gemini = wire(
        claude_gemini::claude_to_gemini_request(
            input,
            "target",
            Default::default(),
            &mut flow(),
            &policy(Dialect::Gemini),
        )
        .unwrap()
        .value,
    );
    has_text(&gemini);
    assert_eq!(gemini["generationConfig"]["topK"], 17);
    assert_eq!(
        gemini["generationConfig"]["thinkingConfig"]["thinkingBudget"],
        12
    );
}

#[test]
fn chat_controls_are_omitted_only_where_the_target_has_no_field() {
    let input: h::GenerateContentRequestBody = serde_json::from_value(json!({
        "model":"source", "max_completion_tokens":-3, "frequency_penalty":0.4,"seed":17,
        "messages":[{"role":"user","content":"keep this text"}],"foreign-extension":true
    }))
    .unwrap();
    let claude = wire(
        claude_chat::openai_to_claude(&input, "target")
            .unwrap()
            .value,
    );
    has_text(&claude);
    assert_eq!(claude["max_tokens"], -3);
    assert!(claude.get("frequency_penalty").is_none());
    let gemini = wire(
        gemini_chat::openai_to_gemini_request(&input, "target", &Default::default())
            .unwrap()
            .value,
    );
    has_text(&gemini);
    assert_eq!(gemini["generationConfig"]["frequencyPenalty"], 0.4);
    let responses = wire(
        chat_responses::chat_to_responses_request(
            input,
            "target",
            &mut flow(),
            &policy(Dialect::OpenAi),
        )
        .unwrap()
        .value,
    );
    has_text(&responses);
    assert_eq!(responses["max_output_tokens"], -3);
}

#[test]
fn gemini_parts_and_hosted_tools_preserve_independent_supported_fields() {
    let input:g::GenerateContentRequestBody=serde_json::from_value(json!({
        "contents":[{"role":"user","parts":[{"text":"keep this text","mediaResolution":{"level":"MEDIA_RESOLUTION_HIGH"}}]}],
        "generationConfig":{"maxOutputTokens":64,"thinkingConfig":{"thinkingBudget":12,"includeThoughts":true}},
        "tools":[{"googleSearch":{},"functionDeclarations":[{"name":"lookup","description":"lookup","parameters":{"type":"OBJECT"}}]}],
        "foreign-extension":true
    })).unwrap();
    let chat = wire(
        gemini_chat::gemini_to_openai_request(
            input.clone(),
            "target",
            &mut flow(),
            &policy(Dialect::OpenAiChat),
        )
        .unwrap()
        .value,
    );
    has_text(&chat);
    assert_eq!(chat["tools"][0]["function"]["name"], "lookup");
    let claude = wire(
        claude_gemini::gemini_to_claude_request(
            input.clone(),
            "target",
            None,
            &mut flow(),
            &policy(Dialect::Claude),
        )
        .unwrap()
        .value,
    );
    has_text(&claude);
    assert_eq!(claude["tools"][0]["type"], "web_search_20250305");
    assert_eq!(claude["tools"][1]["name"], "lookup");
    let responses = wire(
        gemini_responses::gemini_to_responses_request(
            input,
            "target",
            &mut flow(),
            &policy(Dialect::OpenAi),
        )
        .unwrap()
        .value,
    );
    has_text(&responses);
    assert_eq!(responses["tools"][0]["type"], "web_search");
    assert_eq!(responses["tools"][1]["name"], "lookup");
}

#[test]
fn responses_hosted_tools_do_not_discard_the_function_catalog() {
    let input: r::GenerateContentRequestBody = serde_json::from_value(json!({
        "model":"source","max_output_tokens":64,"background":true,
        "input":"keep this text",
        "tools":[{"type":"code_interpreter","container":{"type":"auto"}},
                 {"type":"function","name":"lookup","parameters":{"type":"object"},"strict":false}],
        "foreign-extension":true
    }))
    .unwrap();
    let chat = wire(
        chat_responses::responses_to_chat_request(input.clone(), "target")
            .unwrap()
            .value,
    );
    has_text(&chat);
    assert_eq!(chat["tools"].as_array().unwrap().len(), 1);
    assert_eq!(chat["tools"][0]["function"]["name"], "lookup");
    let claude = wire(
        claude_responses::responses_to_claude_request(input.clone(), "target", Default::default())
            .unwrap()
            .value,
    );
    has_text(&claude);
    assert_eq!(claude["tools"].as_array().unwrap().len(), 1);
    let gemini = wire(
        gemini_responses::responses_to_gemini_request(input, "target", Default::default())
            .unwrap()
            .value,
    );
    has_text(&gemini);
    assert_eq!(
        gemini["tools"][0]["functionDeclarations"][0]["name"],
        "lookup"
    );
}

#[test]
fn malformed_json_arguments_still_fail_when_decoding_is_needed() {
    let input:h::GenerateContentRequestBody=serde_json::from_value(json!({"model":"s","max_tokens":16,"messages":[{"role":"assistant","tool_calls":[{"type":"function","id":"call","function":{"name":"lookup","arguments":"{broken"}}]}]})).unwrap();
    assert!(claude_chat::openai_to_claude(&input, "target").is_err());
}

#[test]
fn omitted_stream_block_does_not_poison_later_text_or_completion() {
    use gproxy_protocol::transform::generate::{
        claude_chat::stream::{ClaudeToChatContext, ClaudeToChatStream},
        stream::{chat::ChatStreamCollector, claude::synthesize_claude_stream},
    };
    let source:c::GenerateContentResponseBody=serde_json::from_value(json!({
        "id":"msg","model":"source","type":"message","role":"assistant","stop_reason":"end_turn","stop_sequence":null,
        "content":[{"type":"server_tool_use","id":"server","name":"web_search","input":{"query":"q"}}, {"type":"text","text":"keep this text"}],
        "usage":{"input_tokens":3,"output_tokens":2,"cache_creation_input_tokens":0,"cache_read_input_tokens":0}
    })).unwrap();
    let events = synthesize_claude_stream(source, Default::default()).unwrap();
    let mut converter = ClaudeToChatStream::new(
        ClaudeToChatContext { created: 1 },
        flow(),
        Default::default(),
    )
    .unwrap();
    let mut collector = ChatStreamCollector::new(flow(), policy(Dialect::OpenAiChat));
    for event in events.value {
        for chunk in converter.push(event).unwrap().value {
            collector.push(chunk).unwrap();
        }
    }
    for chunk in converter.finish().unwrap().chunks {
        collector.push(chunk).unwrap();
    }
    collector.push_done().unwrap();
    let output = collector.finish().unwrap().value;
    assert_eq!(
        output.choices[0].message.content.as_deref(),
        Some("keep this text")
    );
    assert!(output.choices[0].message.tool_calls.is_none());
}