use gproxy_protocol::{
Dialect, gemini as g,
openai::responses as r,
transform::{generate::gemini_responses::*, identity::*},
};
use serde_json::json;
fn flow() -> IdentityFlow {
IdentityFlow::new(IdNamespace::with_bytes([57; 16]))
}
fn policy() -> TargetIdPolicy {
TargetIdPolicy::new(Dialect::OpenAi)
}
fn context() -> GeminiResponseContext {
GeminiResponseContext {
request: serde_json::from_value(
json!({"model":"selected","input":"x","parallel_tool_calls":true,"tool_choice":"auto"}),
)
.unwrap(),
effective_parallel_tool_calls: true,
effective_tool_choice: r::ToolChoice::Mode(r::ToolChoiceMode::Auto),
usage: GeminiUsageFacts {
cache_write_tokens: Some(0),
cached_tokens: None,
},
created_at: 123,
effective_prompt_cache_options: None,
}
}
#[test]
fn direct_request_preserves_roles_media_tools_strict_controls_and_call_results() {
let input:g::GenerateContentRequestBody=serde_json::from_value(json!({"contents":[{"role":"user","parts":[{"text":"hi"},{"inlineData":{"mimeType":"image/png","data":"AQI="}}]},{"role":"model","parts":[{"functionCall":{"name":"f","args":{"x":1}}}]},{"role":"user","parts":[{"functionResponse":{"name":"f","response":{"result":2}}}]}],"systemInstruction":{"parts":[{"text":"rules"}]},"generationConfig":{"maxOutputTokens":32,"temperature":0.2,"thinkingConfig":{"thinkingLevel":"LOW"},"responseJsonSchema":{"type":"object","additionalProperties":false}},"tools":[{"functionDeclarations":[{"name":"f","description":"f","parametersJsonSchema":{"type":"object","additionalProperties":false}}]}],"toolConfig":{"functionCallingConfig":{"mode":"VALIDATED"}}})).unwrap();
let output = gemini_to_responses_request(input, "target", &mut flow(), &policy())
.unwrap()
.value;
let wire = serde_json::to_value(&output).unwrap();
assert_eq!(wire["instructions"], "rules");
assert_eq!(wire["input"][2]["type"], "function_call");
assert_eq!(wire["input"][2]["call_id"], wire["input"][3]["call_id"]);
assert_ne!(wire["input"][2]["id"], wire["input"][2]["call_id"]);
assert_eq!(wire["tools"][0]["strict"], true);
let back = responses_to_gemini_request(output, "model", GeminiReplayContext::default())
.unwrap()
.value;
assert_eq!(back.generation_config.unwrap().max_output_tokens, Some(32));
assert_eq!(
back.tool_config
.unwrap()
.function_calling_config
.unwrap()
.mode,
Some(g::FunctionCallingMode::Validated)
);
assert!(back.contents.iter().any(|content| {
content
.parts
.as_ref()
.unwrap()
.iter()
.any(|p| p.function_response.is_some())
}));
}
#[test]
fn response_status_thoughts_functions_and_usage_are_preserved_with_carried_signatures() {
let input:g::GenerateContentResponseBody=serde_json::from_value(json!({"responseId":"g1","modelVersion":"actual","candidates":[{"index":0,"finishReason":"MAX_TOKENS","content":{"role":"model","parts":[{"text":"think","thought":true,"thoughtSignature":"native"},{"text":"visible"},{"functionCall":{"name":"f","args":{"x":1}}}]}}],"usageMetadata":{"promptTokenCount":3,"cachedContentTokenCount":1,"candidatesTokenCount":2,"thoughtsTokenCount":1,"totalTokenCount":6}})).unwrap();
let output = gemini_to_responses_response(input, context(), &mut flow(), &policy())
.unwrap()
.value;
assert_eq!(output.model, "actual");
assert_eq!(output.status, Some(r::ResponseStatus::Incomplete));
assert_eq!(
output
.usage
.as_ref()
.unwrap()
.as_ref()
.unwrap()
.output_tokens,
3
);
let wire = serde_json::to_value(&output).unwrap();
assert_eq!(wire["output"][0]["encrypted_content"], "gemini:native");
assert_eq!(wire["output"][2]["type"], "function_call");
assert_eq!(wire["incomplete_details"]["reason"], "max_output_tokens");
let back = responses_to_gemini_response(output, GeminiReplayContext::default())
.unwrap()
.value;
let usage = back.usage_metadata.unwrap();
assert_eq!(usage.candidates_token_count, Some(2));
assert_eq!(usage.thoughts_token_count, Some(1));
assert_eq!(
back.candidates.unwrap()[0].finish_reason,
Some(g::FinishReason::MaxTokens)
);
}
#[test]
fn raw_string_input_multisystem_and_needed_facts_are_explicit() {
let input: r::GenerateContentRequestBody =
serde_json::from_value(json!({"input":"hello","instructions":"first","temperature":null}))
.unwrap();
let output = responses_to_gemini_request(input, "target", GeminiReplayContext::default())
.unwrap()
.value;
assert_eq!(
output.contents[0].parts.as_ref().unwrap()[0]
.text
.as_deref(),
Some("hello")
);
assert!(output.system_instruction.is_some());
let input:g::GenerateContentResponseBody=serde_json::from_value(json!({"candidates":[{"finishReason":"STOP","content":{"parts":[{"text":"x"}]}}],"usageMetadata":{"promptTokenCount":1,"candidatesTokenCount":1,"totalTokenCount":2}})).unwrap();
assert!(gemini_to_responses_response(input, context(), &mut flow(), &policy()).is_err());
}
#[test]
fn identity_transaction_rolls_back_and_multicandidate_never_merges() {
let input:g::GenerateContentRequestBody=serde_json::from_value(json!({"contents":[{"role":"model","parts":[{"functionCall":{"name":"f","id":"id","args":{}}}]},{"role":"user","parts":[{"inlineData":{"mimeType":"audio/ogg","data":"AQI="}}]}]})).unwrap();
let mut ids = flow();
assert!(gemini_to_responses_request(input, "target", &mut ids, &policy()).is_ok());
assert!(
ids.lookup_source(
&SourceIdentity::new(Dialect::Gemini, Some("id".into()), 0),
IdentityRole::ToolCall
)
.is_some()
);
let input: g::GenerateContentResponseBody = serde_json::from_value(
json!({"candidates":[{"finishReason":"STOP"},{"finishReason":"STOP"}]}),
)
.unwrap();
assert!(gemini_to_responses_response(input, context(), &mut ids, &policy()).is_err());
let input: g::GenerateContentResponseBody =
serde_json::from_value(json!({"candidates":[]})).unwrap();
assert!(gemini_to_responses_response(input, context(), &mut ids, &policy()).is_err());
}
#[test]
fn malformed_arguments_are_rejected_and_foreign_ciphertext_is_dropped() {
let input: r::GenerateContentRequestBody = serde_json::from_value(
json!({"input":[{"type":"function_call","call_id":"id","name":"f","arguments":"broken"}]}),
)
.unwrap();
assert!(responses_to_gemini_request(input, "target", GeminiReplayContext::default()).is_err());
// Ciphertext from another upstream is left out, never sent as a Gemini
// signature.
let input:r::GenerateContentRequestBody=serde_json::from_value(json!({"input":[{"type":"reasoning","id":"rs","summary":[],"encrypted_content":"foreign"},{"type":"reasoning","id":"rs2","summary":[{"type":"summary_text","text":"x"}],"encrypted_content":"claude:signature"}]})).unwrap();
let output = responses_to_gemini_request(input, "target", GeminiReplayContext::default())
.unwrap()
.value;
assert!(output.contents.is_empty());
}
#[test]
fn multimodal_tool_outputs_use_native_parts_and_scoped_reasoning_restores_exact_text() {
let input:g::GenerateContentRequestBody=serde_json::from_value(json!({"contents":[{"role":"model","parts":[{"functionCall":{"name":"f","id":"call","args":{}}}]},{"role":"user","parts":[{"functionResponse":{"name":"f","id":"call","response":{"result":1},"parts":[{"inlineData":{"mimeType":"image/png","data":"AQI="}}]}}]}]})).unwrap();
let output = gemini_to_responses_request(input, "target", &mut flow(), &policy())
.unwrap()
.value;
let wire = serde_json::to_value(&output).unwrap();
assert_eq!(wire["input"][1]["output"][1]["type"], "input_image");
let back = responses_to_gemini_request(output, "gemini", GeminiReplayContext::default())
.unwrap()
.value;
assert_eq!(
back.contents[1].parts.as_ref().unwrap()[0]
.function_response
.as_ref()
.unwrap()
.parts
.as_ref()
.unwrap()[0]
.inline_data
.as_ref()
.unwrap()
.data,
"AQI="
);
// A carried Gemini signature comes back from the client's own item,
// with the text it covers, and needs no saved state.
let input:r::GenerateContentRequestBody=serde_json::from_value(json!({"input":[{"type":"reasoning","id":"rs","summary":[],"content":[{"type":"reasoning_text","text":"exact"}],"encrypted_content":"gemini:sig"}]})).unwrap();
let output = responses_to_gemini_request(input, "gemini", GeminiReplayContext::default())
.unwrap()
.value;
assert_eq!(
serde_json::to_value(&output.contents[0].parts).unwrap(),
json!([{"thought":true,"text":"exact","thoughtSignature":"sig"}])
);
}
#[test]
fn response_logprobs_and_url_citations_map_to_native_gemini_metadata() {
let input:r::GenerateContentResponseBody=serde_json::from_value(json!({"id":"response","created_at":1,"model":"model","object":"response","status":"completed","output":[{"type":"message","id":"msg","role":"assistant","status":"completed","content":[{"type":"output_text","text":"x","annotations":[{"type":"url_citation","start_index":0,"end_index":1,"title":"title","url":"https://example"}],"logprobs":[{"token":"x","logprob":-0.1,"bytes":[120],"top_logprobs":[]}]}]}],"parallel_tool_calls":true,"temperature":null,"tool_choice":"auto","tools":[],"top_p":null,"error":null,"incomplete_details":null,"instructions":null,"metadata":null,"usage":{"input_tokens":1,"input_tokens_details":{"cached_tokens":0,"cache_write_tokens":0},"output_tokens":1,"output_tokens_details":{"reasoning_tokens":0},"total_tokens":2}})).unwrap();
let output = responses_to_gemini_response(input, GeminiReplayContext::default())
.unwrap()
.value;
let candidates = output.candidates.unwrap();
assert_eq!(
candidates[0]
.citation_metadata
.as_ref()
.unwrap()
.citation_sources
.as_ref()
.unwrap()[0]
.uri
.as_deref(),
Some("https://example")
);
assert_eq!(
candidates[0]
.logprobs_result
.as_ref()
.unwrap()
.chosen_candidates
.as_ref()
.unwrap()[0]
.token
.as_deref(),
Some("x")
);
}