mirror of
https://github.com/zyycn/codex-proxy-rs.git
synced 2026-11-06 08:06:19 +00:00
fix(openai): preserve client turn identifiers
Preserve turn IDs and parent references across request metadata, input history, headers, Images and Desktop context. Update contract coverage and API documentation for unchanged turn identity across accounts. Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
@@ -7,21 +7,18 @@ use sha2::{Digest, Sha256};
|
||||
use super::automatic_context::{locate_contexts, rewrite_git_context};
|
||||
use super::request::serialize_ascii_turn_metadata;
|
||||
|
||||
// 轮次及父子轮次引用保留客户端原值,避免改变官方轮次身份与 UUIDv7 语义
|
||||
const ID_FIELDS: &[&str] = &[
|
||||
"session_id",
|
||||
"thread_id",
|
||||
"turn_id",
|
||||
"window_id",
|
||||
"context_window_id",
|
||||
"parent_thread_id",
|
||||
"parent_turn_id",
|
||||
"root_turn_id",
|
||||
"forked_from_thread_id",
|
||||
"conversation_id",
|
||||
"x-codex-window-id",
|
||||
"x-codex-parent-thread-id",
|
||||
"x-client-request-id",
|
||||
"x-client-turn-id",
|
||||
"codexWindowId",
|
||||
"parentThreadId",
|
||||
];
|
||||
@@ -39,8 +36,6 @@ const ID_HEADERS: &[&str] = &[
|
||||
"x-codex-window-id",
|
||||
"x-codex-parent-thread-id",
|
||||
"x-client-request-id",
|
||||
"x-client-turn-id",
|
||||
"x-codex-image-turn-id",
|
||||
];
|
||||
|
||||
fn scoped_id(installation_id: &str, original: &str) -> String {
|
||||
@@ -206,16 +201,6 @@ pub(super) fn scope_request(
|
||||
let Some(installation_id) = installation_id else {
|
||||
return;
|
||||
};
|
||||
// 输入项的轮次引用与请求 metadata 共用账号域,保留 item/call ID 和业务元数据
|
||||
if let Some(Value::Array(input)) = body.get_mut("input") {
|
||||
for item in input {
|
||||
if let Some(Value::Object(metadata)) =
|
||||
item.get_mut("internal_chat_message_metadata_passthrough")
|
||||
{
|
||||
map_fields(metadata, installation_id, &["turn_id"]);
|
||||
}
|
||||
}
|
||||
}
|
||||
// 只处理官方自动 Git 上下文的关联字段,不扫描普通用户输入和工具输出
|
||||
let Ok(targets) = locate_contexts(body, true, || Ok(())) else {
|
||||
return;
|
||||
@@ -229,11 +214,7 @@ pub(super) fn scope_request(
|
||||
}
|
||||
let _ = rewrite_git_context(text, 1024 * 1024, |value| {
|
||||
Ok(value.as_object_mut().map_or(0, |object| {
|
||||
map_fields(
|
||||
object,
|
||||
installation_id,
|
||||
&["sessionId", "threadId", "clientUserMessageId"],
|
||||
)
|
||||
map_fields(object, installation_id, &["sessionId", "threadId"])
|
||||
}))
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//! 账号追踪域的稳定性、HTTP/WS 一致性、父子引用与工具端点归属回归
|
||||
//! 账号追踪域的稳定性、轮次原值透传、HTTP/WS 一致性与工具端点归属回归
|
||||
|
||||
use super::*;
|
||||
|
||||
@@ -39,6 +39,7 @@ fn payload(websocket: bool) -> ProtocolPayload {
|
||||
"model":"gpt-5.4", "input":input,
|
||||
"prompt_cache_key":"custom-cache-key", "client_metadata":{
|
||||
"session_id":"source-session", "thread_id":"source-thread", "turn_id":"source-turn",
|
||||
"x-client-turn-id":"source-turn",
|
||||
"x-codex-window-id":"source-window", "x-codex-parent-thread-id":"source-session",
|
||||
"x-codex-turn-metadata":full.to_string(),
|
||||
"ws_request_header_traceparent":"00-11111111111111111111111111111111-2222222222222222-01",
|
||||
@@ -50,6 +51,7 @@ fn payload(websocket: bool) -> ProtocolPayload {
|
||||
("thread_id".into(), json!("source-thread")),
|
||||
("turn_metadata".into(), json!(metadata().to_string())),
|
||||
("opaque_request_headers".into(), json!([
|
||||
["x-client-turn-id",STANDARD.encode("source-turn")],
|
||||
["traceparent",STANDARD.encode("00-11111111111111111111111111111111-2222222222222222-01")],
|
||||
["tracestate",STANDARD.encode("vendor=source")]
|
||||
])),
|
||||
@@ -57,7 +59,7 @@ fn payload(websocket: bool) -> ProtocolPayload {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn account_correlation_is_stable_across_transports_and_changes_only_with_account() {
|
||||
async fn account_correlation_preserves_turns_across_transports_and_account_switches() {
|
||||
let stores = [
|
||||
Arc::new(MemoryAccountStore::default()),
|
||||
Arc::new(MemoryAccountStore::default()),
|
||||
@@ -140,7 +142,6 @@ async fn account_correlation_is_stable_across_transports_and_changes_only_with_a
|
||||
for (field, original) in [
|
||||
("session_id", "source-session"),
|
||||
("thread_id", "source-thread"),
|
||||
("turn_id", "source-turn"),
|
||||
] {
|
||||
assert_scoped_id(flat[field].as_str().unwrap(), original);
|
||||
assert_eq!(flat[field], header[field]);
|
||||
@@ -148,10 +149,16 @@ async fn account_correlation_is_stable_across_transports_and_changes_only_with_a
|
||||
}
|
||||
assert_eq!(headers["session-id"].to_str().unwrap(), flat["session_id"]);
|
||||
assert_eq!(headers["thread-id"].to_str().unwrap(), flat["thread_id"]);
|
||||
assert_eq!(headers["x-client-turn-id"], "source-turn");
|
||||
assert_eq!(flat["x-client-turn-id"], "source-turn");
|
||||
assert_eq!(flat["turn_id"], "source-turn");
|
||||
for metadata in [&header, &full] {
|
||||
assert_eq!(metadata["turn_id"], "source-turn");
|
||||
assert_eq!(metadata["parent_turn_id"], "source-turn");
|
||||
assert_eq!(metadata["root_turn_id"], "source-turn");
|
||||
}
|
||||
assert_eq!(full["parent_thread_id"], flat["session_id"]);
|
||||
assert_eq!(full["forked_from_thread_id"], flat["session_id"]);
|
||||
assert_eq!(full["parent_turn_id"], flat["turn_id"]);
|
||||
assert_eq!(full["root_turn_id"], flat["turn_id"]);
|
||||
assert_eq!(full["window_id"], full["context_window_id"]);
|
||||
assert!(header.get("tool_namespaces_info").is_none());
|
||||
assert_eq!(full["tool_namespaces_info"]["inventory"], "body-only");
|
||||
@@ -162,7 +169,7 @@ async fn account_correlation_is_stable_across_transports_and_changes_only_with_a
|
||||
json!({"turn_id":flat["turn_id"], "content_item_kinds":["additional_content.codex_git_state"]}),
|
||||
);
|
||||
let previous_turn = &input[1]["internal_chat_message_metadata_passthrough"]["turn_id"];
|
||||
assert_scoped_id(previous_turn.as_str().unwrap(), "source-previous-turn");
|
||||
assert_eq!(previous_turn, "source-previous-turn");
|
||||
assert_ne!(previous_turn, &flat["turn_id"]);
|
||||
let mut expected_history = account_isolation::isolation_history();
|
||||
for (index, item) in expected_history
|
||||
@@ -176,11 +183,11 @@ async fn account_correlation_is_stable_across_transports_and_changes_only_with_a
|
||||
.unwrap()
|
||||
.entry("internal_chat_message_metadata_passthrough")
|
||||
.or_insert_with(|| json!({}));
|
||||
metadata["turn_id"] = if index == 3 {
|
||||
flat["turn_id"].clone()
|
||||
metadata["turn_id"] = json!(if index == 3 {
|
||||
"source-turn"
|
||||
} else {
|
||||
previous_turn.clone()
|
||||
};
|
||||
"source-previous-turn"
|
||||
});
|
||||
}
|
||||
assert_eq!(json!(&input[1..]), expected_history);
|
||||
let document =
|
||||
@@ -224,11 +231,11 @@ async fn account_correlation_is_stable_across_transports_and_changes_only_with_a
|
||||
assert_eq!(captures[4], captures[5]);
|
||||
assert_ne!(captures[0].0, captures[4].0);
|
||||
assert_ne!(captures[0].1, captures[4].1);
|
||||
assert_ne!(captures[0].2, captures[4].2);
|
||||
assert_eq!(captures[0].2, captures[4].2);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn image_generation_and_edit_share_the_selected_account_turn_identity() {
|
||||
async fn image_generation_and_edit_preserve_the_client_turn_identity() {
|
||||
let store = Arc::new(MemoryAccountStore::default());
|
||||
create_account(&store, "acct_provider_contract").await;
|
||||
let server = MockServer::start().await;
|
||||
@@ -263,7 +270,7 @@ async fn image_generation_and_edit_share_the_selected_account_turn_identity() {
|
||||
}
|
||||
let requests = server.received_requests().await.unwrap();
|
||||
let first = &requests[0].headers["x-codex-image-turn-id"];
|
||||
assert_scoped_id(first.to_str().unwrap(), "source-turn");
|
||||
assert_eq!(first, "source-turn");
|
||||
for request in &requests {
|
||||
assert_eq!(request.body.as_slice(), body.as_ref());
|
||||
assert_eq!(&request.headers["x-codex-image-turn-id"], first);
|
||||
|
||||
@@ -2789,13 +2789,7 @@ async fn image_endpoints_bypass_only_the_text_catalog_and_preserve_the_current_c
|
||||
Mock::given(method("POST"))
|
||||
.and(path(*endpoint))
|
||||
.and(header("originator", "codex_cli_rs"))
|
||||
.and(|request: &wiremock::Request| {
|
||||
request
|
||||
.headers
|
||||
.get("x-codex-image-turn-id")
|
||||
.and_then(|value| value.to_str().ok())
|
||||
.is_some_and(|value| uuid::Uuid::parse_str(value).is_ok())
|
||||
})
|
||||
.and(header("x-codex-image-turn-id", "turn_image_contract"))
|
||||
.and(header("version", "0.144.0"))
|
||||
.and(header("accept", "*/*"))
|
||||
.and(body_bytes(body.to_vec()))
|
||||
@@ -3636,7 +3630,7 @@ async fn standalone_search_preserves_wire_and_scopes_turn_metadata_to_the_select
|
||||
let metadata: Value = serde_json::from_slice(&metadata_values[0]).expect("turn metadata JSON");
|
||||
assert_scoped_id(metadata["session_id"].as_str().unwrap(), "session");
|
||||
assert_scoped_id(metadata["thread_id"].as_str().unwrap(), "thread");
|
||||
assert_scoped_id(metadata["turn_id"].as_str().unwrap(), "turn");
|
||||
assert_eq!(metadata["turn_id"], "turn");
|
||||
assert_eq!(metadata.get("future"), Some(&json!(true)));
|
||||
assert!(metadata.get("account_id").is_none());
|
||||
assert_ne!(
|
||||
|
||||
+5
-5
@@ -298,13 +298,13 @@ Responses 也不透传 `x-stainless-*`、`Origin`、`Referer`、`sec-ch-ua*` 和
|
||||
出站移除下游的 `traceparent`、`tracestate` 及 WS metadata 中对应的
|
||||
`ws_request_header_traceparent`、`ws_request_header_tracestate`
|
||||
|
||||
会话、线程、轮次、窗口及父子引用在出站时按账号安装身份稳定映射为 UUID;
|
||||
会话、线程、窗口及父子线程引用在出站时按账号安装身份稳定映射为 UUID;
|
||||
同账号保留引用关系,换账号使用不同标识。HTTP 请求头、正文 metadata、WS 副本及
|
||||
已识别的 Desktop 自动 Git 上下文使用一致映射。
|
||||
输入项的 `internal_chat_message_metadata_passthrough.turn_id` 同步映射,
|
||||
保留历史轮次与当前轮次的引用关系,其余输入项元数据保持不变。
|
||||
Images 的 `x-codex-image-turn-id`、turn metadata 和 Search 的会话 `id` 使用同一账号域;
|
||||
Search 只替换该 `id` 的原始字节,其余正文保持不变。
|
||||
轮次及父子轮次引用保留客户端原值,包括输入项的 `internal_chat_message_metadata_passthrough.turn_id`、
|
||||
请求 metadata 中的 `turn_id`、`parent_turn_id`、`root_turn_id`,以及 `x-client-turn-id`、
|
||||
Images 的 `x-codex-image-turn-id` 和 Desktop 自动 Git 上下文的 `clientUserMessageId`。
|
||||
输入项元数据保持不变。Search 的会话 `id` 按同一账号域映射,只替换该值的原始字节,其余正文保持不变。
|
||||
`prompt_cache_key` 同样按账号映射,保留同账号内的缓存共享关系;未提供时不补造。
|
||||
续接 response ID、历史 item/call ID、业务正文和未知扩展不参与映射,
|
||||
该处理减少显式追踪标识的跨账号复用,不保证会话内容或业务 metadata 无法关联
|
||||
|
||||
Reference in New Issue
Block a user