mirror of
https://github.com/zyycn/codex-proxy-rs.git
synced 2026-11-07 00:05:49 +00:00
fix(openai): scope passthrough turn metadata installation identity
Same-account requests preserved opaque x-codex-turn-metadata headers after account scoping, allowing the downstream installation ID to overwrite the rebuilt header. Apply the existing turn metadata scoping to each opaque value before serialization, retaining repeated values and extension fields. Verify HTTP and WebSocket header/body identity consistency for same-account and cross-account requests, including mixed-case repeated metadata headers. Match the Responses metadata and compatibility projections in openai/codex@c6c09fe29f, codex-rs/core/src/responses_metadata.rs, lines 324-409. Refs #226 Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
@@ -489,6 +489,17 @@ pub(crate) fn scope_request_to_account(
|
||||
{
|
||||
request.body_mut().remove(*key);
|
||||
}
|
||||
} else {
|
||||
// 同账号的透传 turn metadata 也会覆盖重建的请求头,安装身份须同步改写。
|
||||
for (name, value) in &mut request.passthrough_headers {
|
||||
if name == "x-codex-turn-metadata"
|
||||
&& let Ok(raw) = value.to_str()
|
||||
&& let Some(scoped) = scope_turn_metadata(raw, installation_id, false)
|
||||
&& let Ok(scoped) = HeaderValue::from_str(&scoped)
|
||||
{
|
||||
*value = scoped;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for key in INSTALLATION_ID_KEYS {
|
||||
|
||||
@@ -4559,6 +4559,7 @@ async fn opaque_provider_options_do_not_change_openai_account_selection() {
|
||||
#[tokio::test]
|
||||
async fn http_account_scoping_drops_downstream_installation_header() {
|
||||
for owner in ["acct_scope_same", "acct_scope_old"] {
|
||||
let raw = r#"{"installation_id":"client-installation","future":true}"#;
|
||||
let request = capture_scoped_http_request(
|
||||
"req_installation_http",
|
||||
"acct_scope_same",
|
||||
@@ -4569,16 +4570,25 @@ async fn http_account_scoping_drops_downstream_installation_header() {
|
||||
("installation_id".to_owned(), json!("client-installation")),
|
||||
(
|
||||
"client_metadata".to_owned(),
|
||||
json!({"x-codex-installation-id": "client-installation"}),
|
||||
json!({
|
||||
"x-codex-installation-id": "client-installation",
|
||||
"x-codex-turn-metadata": raw
|
||||
}),
|
||||
),
|
||||
]),
|
||||
Map::from_iter([
|
||||
("turn_metadata".to_owned(), json!(raw)),
|
||||
(
|
||||
"opaque_request_headers".to_owned(),
|
||||
json!([
|
||||
[
|
||||
"x-codex-installation-id",
|
||||
STANDARD.encode(b"client-installation")
|
||||
],
|
||||
["x-codex-turn-metadata", STANDARD.encode(raw)]
|
||||
]),
|
||||
),
|
||||
]),
|
||||
Map::from_iter([(
|
||||
"opaque_request_headers".to_owned(),
|
||||
json!([[
|
||||
"x-codex-installation-id",
|
||||
STANDARD.encode(b"client-installation")
|
||||
]]),
|
||||
)]),
|
||||
)
|
||||
.await;
|
||||
let body = captured_request_body(&request);
|
||||
@@ -4589,22 +4599,36 @@ async fn http_account_scoping_drops_downstream_installation_header() {
|
||||
assert!(uuid::Uuid::parse_str(installation_id).is_ok());
|
||||
assert_eq!(body["installation_id"], installation_id);
|
||||
assert!(captured_header_values(&request, "x-codex-installation-id").is_empty());
|
||||
let headers = captured_header_values(&request, "x-codex-turn-metadata");
|
||||
assert_eq!(headers.len(), 1);
|
||||
for encoded in [
|
||||
std::str::from_utf8(&headers[0]).expect("header metadata"),
|
||||
body["client_metadata"]["x-codex-turn-metadata"]
|
||||
.as_str()
|
||||
.expect("body metadata"),
|
||||
] {
|
||||
assert_eq!(
|
||||
serde_json::from_str::<Value>(encoded).expect("turn metadata JSON"),
|
||||
json!({"installation_id": installation_id, "future": true}),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn websocket_account_scoping_drops_downstream_installation_header() {
|
||||
for owner in ["acct_scope_same", "acct_scope_old"] {
|
||||
let raw = r#"{"installation_id":"client-installation","future":true}"#;
|
||||
let store = Arc::new(MemoryAccountStore::default());
|
||||
create_account(&store, "acct_scope_same").await;
|
||||
let listener = TcpListener::bind("127.0.0.1:0").await.expect("listener");
|
||||
let base_url = format!("http://{}", listener.local_addr().expect("address"));
|
||||
let server = tokio::spawn(async move {
|
||||
let (socket, _) = listener.accept().await.expect("accept websocket");
|
||||
let mut installation_header = None;
|
||||
let mut received_headers = None;
|
||||
let mut socket =
|
||||
crate::transport::accept_codex_test_websocket_with(socket, |request, _| {
|
||||
installation_header = request.headers().get("x-codex-installation-id").cloned();
|
||||
received_headers = Some(request.headers().clone());
|
||||
})
|
||||
.await;
|
||||
let message = socket.next().await.expect("request").expect("valid frame");
|
||||
@@ -4620,7 +4644,7 @@ async fn websocket_account_scoping_drops_downstream_installation_header() {
|
||||
))
|
||||
.await
|
||||
.expect("complete response");
|
||||
(installation_header, body)
|
||||
(received_headers.expect("opening headers"), body)
|
||||
});
|
||||
let payload = ProtocolPayload::json_object(
|
||||
"openai",
|
||||
@@ -4630,18 +4654,27 @@ async fn websocket_account_scoping_drops_downstream_installation_header() {
|
||||
("installation_id".to_owned(), json!("client-installation")),
|
||||
(
|
||||
"client_metadata".to_owned(),
|
||||
json!({"x-codex-installation-id": "client-installation"}),
|
||||
json!({
|
||||
"x-codex-installation-id": "client-installation",
|
||||
"x-codex-turn-metadata": raw
|
||||
}),
|
||||
),
|
||||
]),
|
||||
)
|
||||
.expect("payload")
|
||||
.with_context(Map::from_iter([(
|
||||
"opaque_request_headers".to_owned(),
|
||||
json!([[
|
||||
"x-codex-installation-id",
|
||||
STANDARD.encode(b"client-installation")
|
||||
]]),
|
||||
)]));
|
||||
.with_context(Map::from_iter([
|
||||
("turn_metadata".to_owned(), json!(raw)),
|
||||
(
|
||||
"opaque_request_headers".to_owned(),
|
||||
json!([
|
||||
[
|
||||
"x-codex-installation-id",
|
||||
STANDARD.encode(b"client-installation")
|
||||
],
|
||||
["x-codex-turn-metadata", STANDARD.encode(raw)]
|
||||
]),
|
||||
),
|
||||
]));
|
||||
let mut stream = provider_with_base_url(&store, base_url)
|
||||
.execute(
|
||||
planned_request(
|
||||
@@ -4662,7 +4695,20 @@ async fn websocket_account_scoping_drops_downstream_installation_header() {
|
||||
assert_ne!(installation_id, "client-installation");
|
||||
assert!(uuid::Uuid::parse_str(installation_id).is_ok());
|
||||
assert_eq!(body["installation_id"], installation_id);
|
||||
assert!(header.is_none());
|
||||
assert!(!header.contains_key("x-codex-installation-id"));
|
||||
for encoded in [
|
||||
header["x-codex-turn-metadata"]
|
||||
.to_str()
|
||||
.expect("header metadata"),
|
||||
body["client_metadata"]["x-codex-turn-metadata"]
|
||||
.as_str()
|
||||
.expect("body metadata"),
|
||||
] {
|
||||
assert_eq!(
|
||||
serde_json::from_str::<Value>(encoded).expect("turn metadata JSON"),
|
||||
json!({"installation_id": installation_id, "future": true}),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5270,6 +5316,10 @@ async fn account_change_drops_only_account_bound_opaque_headers() {
|
||||
"x-codex-turn-metadata",
|
||||
STANDARD.encode(br#"{"installation_id":"client-installation","safe":true}"#)
|
||||
],
|
||||
[
|
||||
"X-Codex-Turn-Metadata",
|
||||
STANDARD.encode(br#"{"installationId":"second-client-installation","safe":false}"#)
|
||||
],
|
||||
["x-openai-future", STANDARD.encode(b"keep-on-switch")]
|
||||
]),
|
||||
)]);
|
||||
@@ -5298,6 +5348,21 @@ async fn account_change_drops_only_account_bound_opaque_headers() {
|
||||
captured_header_values(&same_account, "x-codex-turn-state"),
|
||||
vec![b"turn-first".to_vec(), b"turn-\x80".to_vec()]
|
||||
);
|
||||
let body = captured_request_body(&same_account);
|
||||
let installation_id = body["client_metadata"]["x-codex-installation-id"]
|
||||
.as_str()
|
||||
.expect("account installation ID");
|
||||
let metadata = captured_header_values(&same_account, "x-codex-turn-metadata");
|
||||
assert_eq!(metadata.len(), 2);
|
||||
for (raw, expected) in metadata.iter().zip([
|
||||
json!({"installation_id": installation_id, "safe": true}),
|
||||
json!({"installationId": installation_id, "safe": false}),
|
||||
]) {
|
||||
assert_eq!(
|
||||
serde_json::from_slice::<Value>(raw).expect("turn metadata JSON"),
|
||||
expected,
|
||||
);
|
||||
}
|
||||
assert_eq!(
|
||||
captured_header_values(&same_account, "x-openai-future"),
|
||||
vec![b"keep-on-switch".to_vec()]
|
||||
|
||||
Reference in New Issue
Block a user