use gproxy_protocol::{
Dialect,
transform::{
TransformErrorKind,
generate::claude_responses::*,
identity::{IdNamespace, IdentityFlow, TargetIdPolicy},
},
wire::{claude::generate_content as c, openai::responses as r},
};
use serde_json::{Value, json};
fn flow() -> IdentityFlow {
IdentityFlow::new(IdNamespace::with_bytes([13; 16]))
}
fn context() -> ClaudeResponseContext {
ClaudeResponseContext {request:serde_json::from_value(json!({"model":"original","input":"hi","parallel_tool_calls":false,"tool_choice":"auto","metadata":{"formal":"x"},"unknown":{"bad":1}})).unwrap(),
effective_parallel_tool_calls:false,effective_tool_choice:r::ToolChoice::Mode(r::ToolChoiceMode::Auto),effective_prompt_cache_options:None,
usage:ResponsesUsageFacts::default(),created_at:123,
}
}
fn claude(content: Value, stop: &str) -> c::GenerateContentResponseBody {
serde_json::from_value(json!({"id":"claude-message","type":"message","role":"assistant","model":"actual-model","content":content,"stop_reason":stop,"usage":{"input_tokens":3,"output_tokens":5,"cache_creation_input_tokens":2,"cache_read_input_tokens":4,"output_tokens_details":{"thinking_tokens":1,"unknown":true},"unknown":true},"unknown":{"bad":1}})).unwrap()
}
fn response(output: Value, status: &str) -> r::GenerateContentResponseBody {
serde_json::from_value(json!({"id":"response-id","object":"response","created_at":123,"model":"actual-model","output":output,"status":status,"error":null,"incomplete_details":null,"instructions":null,"metadata":null,"temperature":null,"top_p":null,"parallel_tool_calls":false,"tool_choice":"auto","tools":[],"usage":{"input_tokens":9,"input_tokens_details":{"cache_write_tokens":2,"cached_tokens":4},"output_tokens":5,"output_tokens_details":{"reasoning_tokens":1},"total_tokens":14}})).unwrap()
}
#[test]
fn claude_text_thinking_tools_preserve_formal_data_and_identity_roles() {
let input = claude(
json!([
{"type":"thinking","thinking":"reason","signature":"CLAUDE-ONLY","unknown":true},
{"type":"text","text":"answer","unknown":true},
{"type":"tool_use","id":"native/a","name":"lookup","input":{"unknown":{"formal":true}},"unknown":true},
{"type":"tool_use","id":"native-a","name":"lookup","input":{}}
]),
"tool_use",
);
let mut ids = flow();
let converted = claude_to_responses_response(
input,
context(),
&mut ids,
&TargetIdPolicy::new(Dialect::OpenAi),
)
.unwrap();
let value = converted.value;
assert_eq!(value.created_at, 123);
assert_eq!(value.model, "actual-model");
assert!(!value.parallel_tool_calls);
assert_eq!(value.metadata.unwrap()["formal"], json!("x"));
let r::ResponseOutputItem::Reasoning(thinking) = &value.output[0] else {
panic!()
};
assert_eq!(thinking.content.as_ref().unwrap()[0].text, "reason");
assert_eq!(
thinking.encrypted_content,
Some(Some("claude:CLAUDE-ONLY".into()))
);
let mut emitted = Vec::new();
for item in &value.output[2..] {
let r::ResponseOutputItem::FunctionCall(call) = item else {
panic!()
};
assert_ne!(call.id.as_deref(), Some(call.call_id.as_str()));
emitted.push(call.call_id.clone());
assert!(call.rest.is_empty());
}
assert_eq!(emitted, vec!["native/a", "native-a"]);
let r::ResponseOutputItem::FunctionCall(call) = &value.output[2] else {
panic!()
};
assert_eq!(
serde_json::from_str::<Value>(&call.arguments).unwrap(),
json!({"unknown":{"formal":true}})
);
let usage = value.usage.unwrap().unwrap();
assert_eq!(
(usage.input_tokens, usage.output_tokens, usage.total_tokens),
(9, 5, 14)
);
assert!(usage.input_tokens_details.rest.is_empty());
}
#[test]
fn max_tokens_and_refusal_do_not_turn_into_normal_end_turn() {
let mut ids = flow();
let short = claude_to_responses_response(
claude(json!([{"type":"text","text":"part"}]), "max_tokens"),
context(),
&mut ids,
&TargetIdPolicy::new(Dialect::OpenAi),
)
.unwrap()
.value;
assert_eq!(short.status, Some(r::ResponseStatus::Incomplete));
assert_eq!(
short.incomplete_details.unwrap().reason,
Some(r::ResponseIncompleteReason::MaxOutputTokens)
);
let mut ids = flow();
let refusal = claude_to_responses_response(
claude(json!([{"type":"text","text":"declined"}]), "refusal"),
context(),
&mut ids,
&TargetIdPolicy::new(Dialect::OpenAi),
)
.unwrap()
.value;
let r::ResponseOutputItem::Message(message) = &refusal.output[0] else {
panic!()
};
assert!(matches!(&message.content[0], r::OutputContent::Refusal(_)));
for stop in ["pause_turn", "compaction", "model_context_window_exceeded"] {
assert!(
claude_to_responses_response(
claude(json!([]), stop),
context(),
&mut flow(),
&TargetIdPolicy::new(Dialect::OpenAi)
)
.is_ok()
);
}
}
#[test]
fn actual_usage_supplements_are_required_checked_and_not_zero_filled() {
let mut source = claude(json!([]), "end_turn");
source.usage.cache_read_input_tokens = None;
assert_eq!(
claude_to_responses_response(
source.clone(),
context(),
&mut flow(),
&TargetIdPolicy::new(Dialect::OpenAi)
)
.unwrap_err()
.kind(),
TransformErrorKind::MissingMetadata
);
let mut facts = context();
facts.usage.cached_tokens = Some(4);
assert_eq!(
claude_to_responses_response(
source,
facts,
&mut flow(),
&TargetIdPolicy::new(Dialect::OpenAi)
)
.unwrap()
.value
.usage
.unwrap()
.unwrap()
.input_tokens,
9
);
let mut facts = context();
facts.usage.reasoning_tokens = Some(2);
assert!(
claude_to_responses_response(
claude(json!([]), "end_turn"),
facts,
&mut flow(),
&TargetIdPolicy::new(Dialect::OpenAi)
)
.is_ok()
);
let mut source = claude(json!([]), "end_turn");
source.usage.input_tokens = i64::MAX;
assert!(
claude_to_responses_response(
source,
context(),
&mut flow(),
&TargetIdPolicy::new(Dialect::OpenAi)
)
.is_err()
);
}
#[test]
fn failed_response_or_late_context_error_never_commits_identity_flow() {
let original = flow();
let mut ids = original.clone();
let mut facts = context();
facts.effective_parallel_tool_calls = true;
let source = claude(
json!([{"type":"tool_use","id":"call","name":"lookup","input":{}}]),
"tool_use",
);
assert!(
claude_to_responses_response(
source,
facts,
&mut ids,
&TargetIdPolicy::new(Dialect::OpenAi)
)
.is_err()
);
assert_eq!(format!("{ids:?}"), format!("{original:?}"));
for status in ["failed", "queued", "in_progress", "cancelled"] {
assert!(
responses_to_claude_response(
response(json!([]), status),
&mut ids,
&TargetIdPolicy::new(Dialect::Claude)
)
.is_err()
);
assert_eq!(format!("{ids:?}"), format!("{original:?}"));
}
}
#[test]
fn responses_function_calls_use_call_id_and_preserve_mixed_refusal_text() {
let source = response(
json!([
{"type":"message","id":"item-message","role":"assistant","status":"completed","content":[{"type":"output_text","text":"prefix","annotations":[],"logprobs":[]},{"type":"refusal","refusal":"declined"}]},
{"type":"function_call","id":"item-distinct","call_id":"actual-call","name":"lookup","arguments":"{\"formal\":true}","status":"completed"}
]),
"completed",
);
let result =
responses_to_claude_response(source, &mut flow(), &TargetIdPolicy::new(Dialect::Claude))
.unwrap()
.value;
assert_eq!(result.stop_reason, c::StopReason::ToolUse);
assert_eq!(result.content.len(), 3);
let c::ResponseContentBlock::ToolUse(call) = &result.content[2] else {
panic!()
};
assert_eq!(call.id, "actual-call");
assert_eq!(call.input["formal"], json!(true));
assert_eq!(result.usage.input_tokens, 3);
assert_eq!(result.usage.cache_read_input_tokens, Some(Some(4)));
}
#[test]
fn missing_or_inconsistent_usage_and_nonterminal_items_are_errors() {
let mut source = response(json!([]), "completed");
source.usage = None;
assert_eq!(
responses_to_claude_response(source, &mut flow(), &TargetIdPolicy::new(Dialect::Claude))
.unwrap_err()
.kind(),
TransformErrorKind::MissingMetadata
);
let mut source = response(json!([]), "completed");
source
.usage
.as_mut()
.unwrap()
.as_mut()
.unwrap()
.total_tokens = 99;
assert!(
responses_to_claude_response(source, &mut flow(), &TargetIdPolicy::new(Dialect::Claude))
.is_ok()
);
let source = response(
json!([{"type":"message","id":"msg","role":"assistant","status":"in_progress","content":[]}]),
"completed",
);
assert!(
responses_to_claude_response(source, &mut flow(), &TargetIdPolicy::new(Dialect::Claude))
.is_err()
);
}
#[test]
fn mcp_native_execution_maps_call_and_success_or_failure_result_both_ways() {
for is_error in [false, true] {
let source = claude(
json!([{"type":"mcp_tool_use","id":"mcp-native","name":"lookup","server_name":"server","input":{"q":1}},
{"type":"mcp_tool_result","tool_use_id":"mcp-native","is_error":is_error,"content":"result"}]),
"end_turn",
);
let mapped = claude_to_responses_response(
source,
context(),
&mut flow(),
&TargetIdPolicy::new(Dialect::OpenAi),
)
.unwrap()
.value;
assert_eq!(mapped.output.len(), 1);
let r::ResponseOutputItem::McpCall(call) = &mapped.output[0] else {
panic!()
};
assert_eq!(call.id, "mcp-native");
assert_eq!(
call.status,
Some(if is_error {
r::McpCallStatus::Failed
} else {
r::McpCallStatus::Completed
})
);
let back = responses_to_claude_response(
mapped,
&mut flow(),
&TargetIdPolicy::new(Dialect::Claude),
)
.unwrap()
.value;
assert_eq!(back.stop_reason, c::StopReason::EndTurn);
assert_eq!(back.content.len(), 2);
let c::ResponseContentBlock::McpToolResult(result) = &back.content[1] else {
panic!()
};
assert_eq!(result.tool_use_id, "mcp-native");
assert_eq!(result.is_error, is_error);
}
}
#[test]
fn claude_thinking_carries_its_signature_in_encrypted_content() {
let source = claude(
json!([
{"type":"thinking","thinking":"original thought","signature":"native-signature"},
{"type":"text","text":"answer"}
]),
"end_turn",
);
let mapped = claude_to_responses_response(
source,
context(),
&mut flow(),
&TargetIdPolicy::new(Dialect::OpenAi),
)
.unwrap()
.value;
let r::ResponseOutputItem::Reasoning(item) = &mapped.output[0] else {
panic!()
};
assert_eq!(
item.encrypted_content,
Some(Some("claude:native-signature".into()))
);
assert_eq!(item.content.as_ref().unwrap()[0].text, "original thought");
let back =
responses_to_claude_response(mapped, &mut flow(), &TargetIdPolicy::new(Dialect::Claude))
.unwrap()
.value;
assert!(
back.content
.iter()
.all(|block| !matches!(block, c::ResponseContentBlock::Thinking(_)))
);
}