fix(codex): 补齐缓存身份终态请求头

This commit is contained in:
MMEXA
2026-07-17 08:11:16 +08:00
parent d9796d502b
commit 5b332da7d7
6 changed files with 406 additions and 30 deletions
+1
View File
@@ -173,6 +173,7 @@ pub use crate::formats::{
codex::{
apply_codex_openai_compact_terminal_headers,
apply_codex_openai_responses_chat_body_edits,
apply_codex_openai_responses_identity_headers,
apply_codex_openai_responses_lite_header_for_request_body_with_capabilities,
apply_codex_openai_responses_lite_header_with_capabilities,
apply_codex_openai_responses_special_body_edits,
@@ -1468,10 +1468,17 @@ fn wrap_codex_responses_string_input_for_backend(
);
}
fn non_empty_json_string(value: Option<&Value>) -> Option<&str> {
value
.and_then(Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
}
fn adapt_codex_prompt_cache_identity_for_backend(body_object: &mut serde_json::Map<String, Value>) {
// The Codex backend scopes prompt caching through client_metadata.session_id, while the
// standard OpenAI contract permits arbitrary prompt_cache_key strings. Adapt only requests
// that carry a cache key but do not already carry a native Codex session identity.
// The standard OpenAI contract permits arbitrary prompt_cache_key strings, while the Codex
// backend's native session identity is UUID-shaped. Adapt only requests that carry a cache key
// but do not already carry a native Codex session identity.
let Some(prompt_cache_key) = body_object
.get("prompt_cache_key")
.and_then(Value::as_str)
@@ -1482,8 +1489,19 @@ fn adapt_codex_prompt_cache_identity_for_backend(body_object: &mut serde_json::M
};
match body_object.get("client_metadata") {
Some(Value::Object(metadata)) if metadata.contains_key("session_id") => return,
Some(Value::Object(_) | Value::Null) | None => {}
Some(Value::Object(metadata)) => {
match metadata.get("session_id") {
Some(Value::String(session_id)) if !session_id.trim().is_empty() => return,
None | Some(Value::Null) => {}
Some(_) => return,
}
match metadata.get("thread_id") {
None | Some(Value::Null) => {}
Some(Value::String(thread_id)) if !thread_id.trim().is_empty() => {}
Some(_) => return,
}
}
Some(Value::Null) | None => {}
Some(_) => return,
}
@@ -1506,12 +1524,16 @@ fn adapt_codex_prompt_cache_identity_for_backend(body_object: &mut serde_json::M
let Some(metadata) = metadata.as_object_mut() else {
return;
};
metadata
.entry("session_id".to_string())
.or_insert_with(|| Value::String(cache_identity.clone()));
metadata
.entry("thread_id".to_string())
.or_insert_with(|| Value::String(cache_identity.clone()));
metadata.insert(
"session_id".to_string(),
Value::String(cache_identity.clone()),
);
if metadata.get("thread_id").is_none_or(Value::is_null) {
metadata.insert(
"thread_id".to_string(),
Value::String(cache_identity.clone()),
);
}
}
body_object.insert(
@@ -1520,6 +1542,74 @@ fn adapt_codex_prompt_cache_identity_for_backend(body_object: &mut serde_json::M
);
}
fn valid_codex_identity_header(value: &str) -> Option<String> {
let value = value.trim();
if value.is_empty() || value.parse::<http::HeaderValue>().is_err() {
return None;
}
Some(value.to_string())
}
fn codex_prompt_cache_header_identity(provider_request_body: &Value) -> Option<(String, String)> {
let prompt_cache_key = non_empty_json_string(provider_request_body.get("prompt_cache_key"))?;
match provider_request_body.get("client_metadata") {
Some(Value::Object(metadata)) => match metadata.get("session_id") {
Some(Value::String(session_id)) if !session_id.trim().is_empty() => {
let session_id = valid_codex_identity_header(session_id)?;
let thread_id = match metadata.get("thread_id") {
None | Some(Value::Null) => session_id.clone(),
Some(Value::String(thread_id)) if !thread_id.trim().is_empty() => {
valid_codex_identity_header(thread_id)?
}
Some(_) => return None,
};
return Some((session_id, thread_id));
}
None | Some(Value::Null) => {}
Some(_) => return None,
},
Some(Value::Null) | None => {}
Some(_) => return None,
}
let cache_identity = uuid::Uuid::parse_str(prompt_cache_key).ok()?.to_string();
Some((cache_identity.clone(), cache_identity))
}
fn insert_btree_header_if_missing(
headers: &mut BTreeMap<String, String>,
header_name: &str,
header_value: String,
) {
if headers.iter().any(|(name, value)| {
name.trim().eq_ignore_ascii_case(header_name) && !value.trim().is_empty()
}) {
return;
}
remove_btree_header(headers, header_name);
headers.insert(header_name.to_string(), header_value);
}
pub fn apply_codex_openai_responses_identity_headers(
provider_request_headers: &mut BTreeMap<String, String>,
provider_request_body: &Value,
provider_type: &str,
provider_api_format: &str,
) {
// Codex projects its body session identity into compatibility HTTP headers. Existing explicit
// headers remain authoritative; only missing values are completed here.
if !is_codex_openai_responses_request(provider_type, provider_api_format) {
return;
}
let Some((session_id, thread_id)) = codex_prompt_cache_header_identity(provider_request_body)
else {
return;
};
insert_btree_header_if_missing(provider_request_headers, "session-id", session_id);
insert_btree_header_if_missing(provider_request_headers, "thread-id", thread_id);
}
pub fn apply_codex_openai_responses_special_body_edits(
provider_request_body: &mut Value,
provider_type: &str,