mirror of
https://github.com/zyycn/codex-proxy-rs.git
synced 2026-11-06 16:06:08 +00:00
746 lines
26 KiB
Rust
746 lines
26 KiB
Rust
use super::*;
|
|
|
|
#[test]
|
|
fn ordered_codex_headers_should_preserve_fingerprint_priority_and_request_fields() {
|
|
let fingerprint = codex_proxy_core::gateway::fingerprint::Fingerprint::default_for_tests();
|
|
|
|
let headers = client::build_ordered_codex_headers(
|
|
&fingerprint,
|
|
"access-token",
|
|
Some("acct-1"),
|
|
Some("turn-1"),
|
|
"req-1",
|
|
);
|
|
let keys = headers.keys().cloned().collect::<Vec<_>>();
|
|
|
|
assert_eq!(headers["authorization"], "Bearer access-token");
|
|
assert_eq!(headers["chatgpt-account-id"], "acct-1");
|
|
assert_eq!(headers["x-client-request-id"], "req-1");
|
|
assert_eq!(headers["x-codex-turn-state"], "turn-1");
|
|
assert_eq!(headers["accept"], "text/event-stream");
|
|
assert_eq!(keys.first().map(String::as_str), Some("authorization"));
|
|
}
|
|
|
|
#[test]
|
|
fn websocket_connection_should_preserve_endpoint_and_header_order() {
|
|
let connection = codex_proxy_adapters::codex::websocket::connect::CodexWebSocketConnection::new(
|
|
"wss://chatgpt.com/backend-api/codex",
|
|
vec![
|
|
("authorization".to_string(), "Bearer token".to_string()),
|
|
("user-agent".to_string(), "Codex Desktop/test".to_string()),
|
|
],
|
|
);
|
|
|
|
assert_eq!(
|
|
(
|
|
connection.endpoint(),
|
|
connection.opening_audit_snapshot().header_order,
|
|
),
|
|
(
|
|
"wss://chatgpt.com/backend-api/codex",
|
|
vec!["authorization".to_string(), "user-agent".to_string()],
|
|
)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn websocket_connection_opening_audit_should_redact_sensitive_headers() {
|
|
let connection = codex_proxy_adapters::codex::websocket::connect::CodexWebSocketConnection::new(
|
|
"wss://chatgpt.com/backend-api/codex/responses?source=audit",
|
|
vec![
|
|
(
|
|
"authorization".to_string(),
|
|
"Bearer access-secret".to_string(),
|
|
),
|
|
("chatgpt-account-id".to_string(), "acct-secret".to_string()),
|
|
("user-agent".to_string(), "Codex Desktop/test".to_string()),
|
|
("x-client-request-id".to_string(), "req-secret".to_string()),
|
|
(
|
|
"x-codex-turn-metadata".to_string(),
|
|
"{\"secret\":true}".to_string(),
|
|
),
|
|
],
|
|
);
|
|
|
|
let snapshot = connection.opening_audit_snapshot();
|
|
|
|
assert_eq!(
|
|
snapshot.request_line,
|
|
"GET /backend-api/codex/responses?source=audit HTTP/1.1"
|
|
);
|
|
assert_eq!(
|
|
snapshot
|
|
.headers
|
|
.iter()
|
|
.map(|header| (header.name.as_str(), header.value.as_str()))
|
|
.collect::<Vec<_>>(),
|
|
vec![
|
|
("authorization", "<redacted>"),
|
|
("chatgpt-account-id", "<redacted>"),
|
|
("user-agent", "Codex Desktop/test"),
|
|
("x-client-request-id", "<redacted>"),
|
|
("x-codex-turn-metadata", "<redacted>"),
|
|
]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn websocket_connection_should_build_standard_opening_headers_around_business_headers() {
|
|
let connection =
|
|
codex_proxy_adapters::codex::websocket::connect::CodexWebSocketConnection::responses(
|
|
"https://chatgpt.com/backend-api",
|
|
"test-websocket-key",
|
|
vec![
|
|
(
|
|
"chatgpt-account-id".to_string(),
|
|
"chatgpt-account".to_string(),
|
|
),
|
|
(
|
|
"authorization".to_string(),
|
|
"Bearer access-token".to_string(),
|
|
),
|
|
("user-agent".to_string(), "Codex Desktop/test".to_string()),
|
|
(
|
|
"openai-beta".to_string(),
|
|
"responses_websockets=2026-02-06".to_string(),
|
|
),
|
|
],
|
|
);
|
|
|
|
let snapshot = connection.opening_audit_snapshot();
|
|
|
|
assert_eq!(
|
|
connection.endpoint(),
|
|
"wss://chatgpt.com/backend-api/codex/responses"
|
|
);
|
|
assert_eq!(
|
|
snapshot.header_order,
|
|
vec![
|
|
"Host",
|
|
"Connection",
|
|
"Upgrade",
|
|
"Sec-WebSocket-Version",
|
|
"Sec-WebSocket-Key",
|
|
"chatgpt-account-id",
|
|
"authorization",
|
|
"user-agent",
|
|
"openai-beta",
|
|
"sec-websocket-extensions",
|
|
]
|
|
);
|
|
assert_eq!(
|
|
snapshot
|
|
.headers
|
|
.iter()
|
|
.map(|header| (header.name.as_str(), header.value.as_str()))
|
|
.collect::<Vec<_>>(),
|
|
vec![
|
|
("Host", "chatgpt.com"),
|
|
("Connection", "Upgrade"),
|
|
("Upgrade", "websocket"),
|
|
("Sec-WebSocket-Version", "13"),
|
|
("Sec-WebSocket-Key", "test-websocket-key"),
|
|
("chatgpt-account-id", "<redacted>"),
|
|
("authorization", "<redacted>"),
|
|
("user-agent", "Codex Desktop/test"),
|
|
("openai-beta", "responses_websockets=2026-02-06"),
|
|
(
|
|
"sec-websocket-extensions",
|
|
"permessage-deflate; client_max_window_bits"
|
|
),
|
|
]
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn websocket_execute_response_create_request_should_capture_handshake_headers() {
|
|
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
|
let addr = listener.local_addr().unwrap();
|
|
let server = tokio::spawn(async move {
|
|
let (stream, _) = listener.accept().await.unwrap();
|
|
let mut websocket = accept_codex_test_websocket_with(stream, |_request, response| {
|
|
response.headers_mut().insert(
|
|
"sec-websocket-extensions",
|
|
"permessage-deflate".parse().unwrap(),
|
|
);
|
|
response
|
|
.headers_mut()
|
|
.insert("x-codex-turn-state", "turn-from-handshake".parse().unwrap());
|
|
response.headers_mut().insert(
|
|
"set-cookie",
|
|
"cf_clearance=ws; Domain=.chatgpt.com; Path=/"
|
|
.parse()
|
|
.unwrap(),
|
|
);
|
|
response
|
|
.headers_mut()
|
|
.insert("x-ratelimit-remaining-requests", "41".parse().unwrap());
|
|
})
|
|
.await;
|
|
let _message = websocket.next().await.unwrap().unwrap();
|
|
websocket
|
|
.send(Message::Text(
|
|
json!({
|
|
"type": "response.completed",
|
|
"response": {
|
|
"id": "resp_ws_headers",
|
|
"object": "response",
|
|
"usage": {
|
|
"input_tokens": 1,
|
|
"output_tokens": 1,
|
|
"total_tokens": 2
|
|
}
|
|
}
|
|
})
|
|
.to_string()
|
|
.into(),
|
|
))
|
|
.await
|
|
.unwrap();
|
|
websocket.close(None).await.unwrap();
|
|
});
|
|
let request = codex_proxy_core::protocol::codex::responses::CodexResponsesRequest::new_http_sse(
|
|
"gpt-5.5",
|
|
"be brief",
|
|
Vec::new(),
|
|
);
|
|
let prepared =
|
|
codex_proxy_adapters::codex::websocket::connect::CodexWebSocketConnection::responses_create_request(
|
|
&format!("http://{addr}"),
|
|
"dGhlIHNhbXBsZSBub25jZQ==",
|
|
vec![("authorization".to_string(), "Bearer access-token".to_string())],
|
|
&request,
|
|
)
|
|
.expect("payload should serialize");
|
|
|
|
let response =
|
|
codex_proxy_adapters::codex::websocket::connect::execute_response_create_request(&prepared)
|
|
.await
|
|
.expect("websocket exchange should succeed");
|
|
server.await.unwrap();
|
|
|
|
assert_eq!(response.turn_state.as_deref(), Some("turn-from-handshake"));
|
|
assert_eq!(
|
|
response.set_cookie_headers,
|
|
vec!["cf_clearance=ws; Domain=.chatgpt.com; Path=/".to_string()]
|
|
);
|
|
assert!(response
|
|
.rate_limit_headers
|
|
.iter()
|
|
.any(|(name, value)| name == "x-ratelimit-remaining-requests" && value == "41"));
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn codex_backend_client_websocket_should_forward_security_chain_headers_and_payload_fields() {
|
|
let received_headers = Arc::new(Mutex::new(Vec::<(String, String)>::new()));
|
|
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
|
let addr = listener.local_addr().unwrap();
|
|
let headers_for_server = Arc::clone(&received_headers);
|
|
let server = tokio::spawn(async move {
|
|
let (stream, _) = listener.accept().await.unwrap();
|
|
let mut websocket = accept_codex_test_websocket_with(stream, move |request, response| {
|
|
response.headers_mut().insert(
|
|
"sec-websocket-extensions",
|
|
"permessage-deflate".parse().unwrap(),
|
|
);
|
|
let headers = request
|
|
.headers()
|
|
.iter()
|
|
.map(|(name, value)| {
|
|
(
|
|
name.as_str().to_string(),
|
|
value.to_str().unwrap_or_default().to_string(),
|
|
)
|
|
})
|
|
.collect::<Vec<_>>();
|
|
*headers_for_server.lock().unwrap() = headers;
|
|
})
|
|
.await;
|
|
let message = websocket.next().await.unwrap().unwrap();
|
|
let payload = serde_json::from_str::<serde_json::Value>(&message.into_text().unwrap())
|
|
.expect("client payload should be json");
|
|
websocket
|
|
.send(Message::Text(
|
|
completed_websocket_response("resp_ws_security", 1, 1).into(),
|
|
))
|
|
.await
|
|
.unwrap();
|
|
websocket.close(None).await.unwrap();
|
|
payload
|
|
});
|
|
let mut request =
|
|
codex_proxy_core::protocol::codex::responses::CodexResponsesRequest::new_http_sse(
|
|
"gpt-5.5",
|
|
"be brief",
|
|
Vec::new(),
|
|
);
|
|
request.use_websocket = true;
|
|
request.prompt_cache_key = Some("client-thread".to_string());
|
|
request.client_metadata = Some(json!({
|
|
"safe": "yes",
|
|
"x-openai-subagent": "review",
|
|
"ignored_non_string": 42
|
|
}));
|
|
let backend = client::CodexBackendClient::new(
|
|
client::build_reqwest_client(false).unwrap(),
|
|
format!("http://{addr}"),
|
|
codex_proxy_core::gateway::fingerprint::Fingerprint::default_for_tests(),
|
|
);
|
|
|
|
let response = backend
|
|
.create_response(
|
|
&request,
|
|
client::CodexRequestContext {
|
|
access_token: "access-token",
|
|
account_id: Some("chatgpt-account"),
|
|
request_id: "req_ws_security",
|
|
turn_state: Some("turn-state"),
|
|
turn_metadata: Some("{\"thread_source\":\"subagent\"}"),
|
|
beta_features: Some("feature-a"),
|
|
include_timing_metrics: Some("true"),
|
|
version: Some("26.318.11754"),
|
|
codex_window_id: Some("cw_derived"),
|
|
parent_thread_id: Some("parent-456"),
|
|
cookie_header: None,
|
|
installation_id: Some("install-123"),
|
|
session_id: Some("cp_derived"),
|
|
},
|
|
)
|
|
.await
|
|
.expect("websocket response should succeed");
|
|
let payload = server.await.unwrap();
|
|
|
|
assert!(response.body.contains("resp_ws_security"));
|
|
assert_eq!(payload["prompt_cache_key"], "cp_derived");
|
|
assert_eq!(
|
|
payload["client_metadata"],
|
|
json!({
|
|
"safe": "yes",
|
|
"x-openai-subagent": "review",
|
|
"x-codex-installation-id": "install-123",
|
|
"x-codex-window-id": "cw_derived",
|
|
"x-codex-turn-metadata": "{\"thread_source\":\"subagent\"}",
|
|
"x-codex-parent-thread-id": "parent-456"
|
|
})
|
|
);
|
|
|
|
let headers = received_headers.lock().unwrap().clone();
|
|
assert!(headers
|
|
.iter()
|
|
.any(|(name, value)| name == "x-client-request-id" && value == "cp_derived"));
|
|
assert!(headers
|
|
.iter()
|
|
.any(|(name, value)| name == "x-codex-installation-id" && value == "install-123"));
|
|
assert!(headers
|
|
.iter()
|
|
.any(|(name, value)| name == "x-openai-internal-codex-residency" && value == "us"));
|
|
assert!(headers
|
|
.iter()
|
|
.any(|(name, value)| name == "x-codex-turn-state" && value == "turn-state"));
|
|
assert!(headers.iter().any(|(name, value)| {
|
|
name == "x-codex-turn-metadata" && value == "{\"thread_source\":\"subagent\"}"
|
|
}));
|
|
assert!(headers
|
|
.iter()
|
|
.any(|(name, value)| name == "x-codex-beta-features" && value == "feature-a"));
|
|
assert!(headers.iter().any(|(name, value)| {
|
|
name == "x-responsesapi-include-timing-metrics" && value == "true"
|
|
}));
|
|
assert!(headers
|
|
.iter()
|
|
.any(|(name, value)| name == "version" && value == "26.318.11754"));
|
|
assert!(headers
|
|
.iter()
|
|
.any(|(name, value)| name == "x-codex-parent-thread-id" && value == "parent-456"));
|
|
assert!(headers
|
|
.iter()
|
|
.any(|(name, value)| name == "x-openai-subagent" && value == "review"));
|
|
assert!(headers
|
|
.iter()
|
|
.any(|(name, value)| name == "session_id" && value == "cp_derived"));
|
|
assert!(headers.iter().all(|(name, _)| name != "content-type"));
|
|
assert!(headers.iter().all(|(name, _)| name != "accept"));
|
|
assert!(headers.iter().all(|(name, _)| name != "session-id"));
|
|
assert!(headers.iter().all(|(name, _)| name != "thread-id"));
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn codex_backend_client_should_send_desktop_headers_and_capture_response_metadata() {
|
|
let server = wiremock::MockServer::start().await;
|
|
let sse_body = concat!(
|
|
"event: response.completed\n",
|
|
"data: {\"response\":{\"id\":\"resp_1\",\"usage\":{\"input_tokens\":2,\"output_tokens\":3,\"input_tokens_details\":{\"cached_tokens\":1}}}}\n",
|
|
"\n",
|
|
);
|
|
wiremock::Mock::given(wiremock::matchers::method("POST"))
|
|
.and(wiremock::matchers::path("/codex/responses"))
|
|
.and(wiremock::matchers::header(
|
|
"authorization",
|
|
"Bearer access-token",
|
|
))
|
|
.and(wiremock::matchers::header(
|
|
"chatgpt-account-id",
|
|
"chatgpt-account",
|
|
))
|
|
.and(wiremock::matchers::header("originator", "Codex Desktop"))
|
|
.and(wiremock::matchers::header("x-client-request-id", "req_1"))
|
|
.and(wiremock::matchers::header("x-codex-turn-state", "turn_1"))
|
|
.and(wiremock::matchers::header("cookie", "cf_clearance=old"))
|
|
.and(wiremock::matchers::body_json(json!({
|
|
"model": "gpt-5.5",
|
|
"instructions": "",
|
|
"input": [],
|
|
"stream": true,
|
|
"store": false
|
|
})))
|
|
.respond_with(
|
|
wiremock::ResponseTemplate::new(200)
|
|
.insert_header("content-type", "text/event-stream")
|
|
.insert_header(
|
|
"set-cookie",
|
|
"cf_clearance=new; Domain=.chatgpt.com; Path=/",
|
|
)
|
|
.insert_header("x-codex-turn-state", "turn_2")
|
|
.set_body_string(sse_body),
|
|
)
|
|
.mount(&server)
|
|
.await;
|
|
let client = client::CodexBackendClient::new(
|
|
client::build_reqwest_client(false).unwrap(),
|
|
server.uri(),
|
|
codex_proxy_core::gateway::fingerprint::Fingerprint::default_for_tests(),
|
|
);
|
|
let mut request =
|
|
codex_proxy_core::protocol::codex::responses::CodexResponsesRequest::new_http_sse(
|
|
"gpt-5.5",
|
|
"",
|
|
Vec::new(),
|
|
);
|
|
request.force_http_sse = true;
|
|
|
|
let response = client
|
|
.create_response(
|
|
&request,
|
|
client::CodexRequestContext {
|
|
access_token: "access-token",
|
|
account_id: Some("chatgpt-account"),
|
|
request_id: "req_1",
|
|
turn_state: Some("turn_1"),
|
|
turn_metadata: None,
|
|
beta_features: None,
|
|
include_timing_metrics: None,
|
|
version: None,
|
|
codex_window_id: None,
|
|
parent_thread_id: None,
|
|
cookie_header: Some("cf_clearance=old"),
|
|
installation_id: None,
|
|
session_id: None,
|
|
},
|
|
)
|
|
.await
|
|
.unwrap();
|
|
|
|
assert_eq!(
|
|
response.usage,
|
|
Some(codex_proxy_core::protocol::codex::events::TokenUsage {
|
|
input_tokens: 2,
|
|
output_tokens: 3,
|
|
cached_tokens: 1,
|
|
reasoning_tokens: 0,
|
|
image_input_tokens: 0,
|
|
image_output_tokens: 0,
|
|
total_tokens: 5,
|
|
})
|
|
);
|
|
assert_eq!(response.turn_state.as_deref(), Some("turn_2"));
|
|
assert_eq!(
|
|
response.set_cookie_headers,
|
|
vec!["cf_clearance=new; Domain=.chatgpt.com; Path=/".to_string()]
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn codex_backend_client_usage_should_use_original_auxiliary_headers() {
|
|
let server = wiremock::MockServer::start().await;
|
|
wiremock::Mock::given(wiremock::matchers::method("GET"))
|
|
.and(wiremock::matchers::path("/api/codex/usage"))
|
|
.respond_with(wiremock::ResponseTemplate::new(200).set_body_json(json!({
|
|
"rate_limit": {
|
|
"limit_reached": false
|
|
}
|
|
})))
|
|
.mount(&server)
|
|
.await;
|
|
let client = client::CodexBackendClient::new(
|
|
client::build_reqwest_client(false).unwrap(),
|
|
server.uri(),
|
|
codex_proxy_core::gateway::fingerprint::Fingerprint::default_for_tests(),
|
|
);
|
|
|
|
let usage = client
|
|
.fetch_usage(client::CodexRequestContext {
|
|
access_token: "access-token",
|
|
account_id: Some("chatgpt-account"),
|
|
request_id: "req_aux",
|
|
turn_state: Some("turn-state"),
|
|
turn_metadata: Some("turn-meta"),
|
|
beta_features: Some("feature-a"),
|
|
include_timing_metrics: Some("true"),
|
|
version: Some("26.318.11754"),
|
|
codex_window_id: Some("cw_1"),
|
|
parent_thread_id: Some("parent-1"),
|
|
cookie_header: Some("cf_clearance=old"),
|
|
installation_id: Some("install-1"),
|
|
session_id: Some("session-1"),
|
|
})
|
|
.await
|
|
.unwrap();
|
|
|
|
assert_eq!(usage["rate_limit"]["limit_reached"], false);
|
|
let requests = server.received_requests().await.unwrap();
|
|
let headers = &requests[0].headers;
|
|
assert_eq!(
|
|
headers.get("accept").and_then(|value| value.to_str().ok()),
|
|
Some("application/json")
|
|
);
|
|
assert_eq!(
|
|
headers
|
|
.get("accept-encoding")
|
|
.and_then(|value| value.to_str().ok()),
|
|
Some("gzip, deflate")
|
|
);
|
|
assert!(headers.get("content-type").is_none());
|
|
assert!(headers.get("openai-beta").is_none());
|
|
assert!(headers.get("x-openai-internal-codex-residency").is_none());
|
|
assert!(headers.get("x-client-request-id").is_none());
|
|
assert_eq!(
|
|
headers
|
|
.get("x-codex-installation-id")
|
|
.and_then(|value| value.to_str().ok()),
|
|
Some("install-1")
|
|
);
|
|
assert!(headers.get("session_id").is_none());
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn codex_backend_client_models_should_use_original_auxiliary_headers() {
|
|
let server = wiremock::MockServer::start().await;
|
|
wiremock::Mock::given(wiremock::matchers::method("GET"))
|
|
.and(wiremock::matchers::path("/codex/models"))
|
|
.respond_with(wiremock::ResponseTemplate::new(200).set_body_json(json!({
|
|
"models": [
|
|
{"slug": "gpt-5.5", "title": "GPT 5.5"}
|
|
]
|
|
})))
|
|
.mount(&server)
|
|
.await;
|
|
let client = client::CodexBackendClient::new(
|
|
client::build_reqwest_client(false).unwrap(),
|
|
server.uri(),
|
|
codex_proxy_core::gateway::fingerprint::Fingerprint::default_for_tests(),
|
|
);
|
|
|
|
let models = client
|
|
.fetch_models(client::CodexRequestContext {
|
|
access_token: "access-token",
|
|
account_id: Some("chatgpt-account"),
|
|
request_id: "req_models",
|
|
turn_state: Some("turn-state"),
|
|
turn_metadata: None,
|
|
beta_features: None,
|
|
include_timing_metrics: None,
|
|
version: None,
|
|
codex_window_id: None,
|
|
parent_thread_id: None,
|
|
cookie_header: None,
|
|
installation_id: Some("install-1"),
|
|
session_id: Some("session-1"),
|
|
})
|
|
.await
|
|
.unwrap();
|
|
|
|
assert_eq!(models.len(), 1);
|
|
let requests = server.received_requests().await.unwrap();
|
|
let models_request = requests
|
|
.iter()
|
|
.find(|request| request.url.path() == "/codex/models")
|
|
.unwrap();
|
|
let headers = &models_request.headers;
|
|
assert_eq!(
|
|
headers.get("accept").and_then(|value| value.to_str().ok()),
|
|
Some("application/json")
|
|
);
|
|
assert!(headers.get("content-type").is_none());
|
|
assert!(headers.get("openai-beta").is_none());
|
|
assert!(headers.get("x-openai-internal-codex-residency").is_none());
|
|
assert!(headers.get("x-client-request-id").is_none());
|
|
assert_eq!(
|
|
headers
|
|
.get("x-codex-installation-id")
|
|
.and_then(|value| value.to_str().ok()),
|
|
Some("install-1")
|
|
);
|
|
assert!(headers.get("session_id").is_none());
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn codex_backend_client_should_send_http_sse_headers_in_fingerprint_order() {
|
|
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
|
let addr = listener.local_addr().unwrap();
|
|
let server = tokio::spawn(async move {
|
|
let (mut stream, _) = listener.accept().await.unwrap();
|
|
let request = read_http_request(&mut stream).await;
|
|
write_completed_sse_response(&mut stream).await;
|
|
request
|
|
});
|
|
|
|
let mut request =
|
|
codex_proxy_core::protocol::codex::responses::CodexResponsesRequest::new_http_sse(
|
|
"gpt-5.5",
|
|
"",
|
|
Vec::new(),
|
|
);
|
|
request.force_http_sse = true;
|
|
request.turn_metadata = Some("turn-meta".to_string());
|
|
request.beta_features = Some("beta-a".to_string());
|
|
request.include_timing_metrics = Some("true".to_string());
|
|
request.version = Some("26.519.81530".to_string());
|
|
request.codex_window_id = Some("cw_1".to_string());
|
|
request.parent_thread_id = Some("parent-1".to_string());
|
|
let client = client::CodexBackendClient::new(
|
|
client::build_reqwest_client(true).unwrap(),
|
|
format!("http://{addr}"),
|
|
codex_proxy_core::gateway::fingerprint::Fingerprint::default_for_tests(),
|
|
);
|
|
|
|
client
|
|
.create_response(
|
|
&request,
|
|
client::CodexRequestContext {
|
|
access_token: "access-token",
|
|
account_id: Some("chatgpt-account"),
|
|
request_id: "req_order",
|
|
turn_state: Some("turn-state"),
|
|
turn_metadata: request.turn_metadata.as_deref(),
|
|
beta_features: request.beta_features.as_deref(),
|
|
include_timing_metrics: request.include_timing_metrics.as_deref(),
|
|
version: request.version.as_deref(),
|
|
codex_window_id: request.codex_window_id.as_deref(),
|
|
parent_thread_id: request.parent_thread_id.as_deref(),
|
|
cookie_header: Some("cf_clearance=old"),
|
|
installation_id: Some("install-1"),
|
|
session_id: Some("session-1"),
|
|
},
|
|
)
|
|
.await
|
|
.unwrap();
|
|
|
|
let raw_request = server.await.unwrap();
|
|
let header_names = read_header_names(&raw_request);
|
|
assert_header_subsequence(
|
|
&header_names,
|
|
&[
|
|
"authorization",
|
|
"chatgpt-account-id",
|
|
"originator",
|
|
"user-agent",
|
|
"sec-ch-ua",
|
|
"sec-ch-ua-mobile",
|
|
"sec-ch-ua-platform",
|
|
"accept-encoding",
|
|
"accept-language",
|
|
"sec-fetch-site",
|
|
"sec-fetch-mode",
|
|
"sec-fetch-dest",
|
|
"content-type",
|
|
"cookie",
|
|
"accept",
|
|
"openai-beta",
|
|
"x-openai-internal-codex-residency",
|
|
"x-client-request-id",
|
|
"x-codex-installation-id",
|
|
"session_id",
|
|
"x-codex-window-id",
|
|
"x-codex-turn-state",
|
|
"x-codex-turn-metadata",
|
|
"x-codex-beta-features",
|
|
"x-responsesapi-include-timing-metrics",
|
|
"version",
|
|
"x-codex-parent-thread-id",
|
|
],
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn codex_backend_client_should_send_compact_headers_in_fingerprint_order() {
|
|
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
|
let addr = listener.local_addr().unwrap();
|
|
let server = tokio::spawn(async move {
|
|
let (mut stream, _) = listener.accept().await.unwrap();
|
|
let request = read_http_request(&mut stream).await;
|
|
write_compact_json_response(&mut stream).await;
|
|
request
|
|
});
|
|
let client = client::CodexBackendClient::new(
|
|
client::build_reqwest_client(true).unwrap(),
|
|
format!("http://{addr}"),
|
|
codex_proxy_core::gateway::fingerprint::Fingerprint::default_for_tests(),
|
|
);
|
|
|
|
client
|
|
.create_compact_response(
|
|
&codex_proxy_core::protocol::codex::responses::CodexCompactRequest {
|
|
model: "gpt-5.5".to_string(),
|
|
input: Vec::new(),
|
|
instructions: String::new(),
|
|
tools: None,
|
|
parallel_tool_calls: None,
|
|
reasoning: None,
|
|
text: None,
|
|
},
|
|
client::CodexRequestContext {
|
|
access_token: "access-token",
|
|
account_id: Some("chatgpt-account"),
|
|
request_id: "req_compact",
|
|
turn_state: None,
|
|
turn_metadata: None,
|
|
beta_features: None,
|
|
include_timing_metrics: None,
|
|
version: None,
|
|
codex_window_id: None,
|
|
parent_thread_id: None,
|
|
cookie_header: Some("cf_clearance=old"),
|
|
installation_id: Some("install-1"),
|
|
session_id: None,
|
|
},
|
|
)
|
|
.await
|
|
.unwrap();
|
|
|
|
let raw_request = server.await.unwrap();
|
|
let header_names = read_header_names(&raw_request);
|
|
assert_header_subsequence(
|
|
&header_names,
|
|
&[
|
|
"authorization",
|
|
"chatgpt-account-id",
|
|
"originator",
|
|
"user-agent",
|
|
"sec-ch-ua",
|
|
"sec-ch-ua-mobile",
|
|
"sec-ch-ua-platform",
|
|
"accept-encoding",
|
|
"accept-language",
|
|
"sec-fetch-site",
|
|
"sec-fetch-mode",
|
|
"sec-fetch-dest",
|
|
"content-type",
|
|
"cookie",
|
|
"openai-beta",
|
|
"x-openai-internal-codex-residency",
|
|
"x-client-request-id",
|
|
"x-codex-installation-id",
|
|
],
|
|
);
|
|
}
|