fix(codex): 统一通用缓存键与原生会话身份

This commit is contained in:
MMEXA
2026-07-17 06:13:09 +08:00
parent 0be380243b
commit d9796d502b
2 changed files with 276 additions and 4 deletions
@@ -4,7 +4,9 @@ use super::{
apply_codex_openai_responses_special_body_edits, apply_codex_openai_special_headers,
codex_model_capabilities,
};
use crate::ai_serving::planner::standard::build_local_openai_responses_request_body;
use crate::ai_serving::planner::standard::{
build_cross_format_openai_responses_request_body, build_local_openai_responses_request_body,
};
use http::{HeaderMap, HeaderValue};
use serde_json::json;
@@ -180,11 +182,11 @@ fn does_not_synthesize_prompt_cache_key_from_api_key_identity() {
}
#[test]
fn keeps_existing_prompt_cache_key_for_codex_requests() {
fn adapts_generic_prompt_cache_key_to_codex_native_identity() {
let mut body = json!({
"model": "gpt-5",
"input": "hello",
"prompt_cache_key": "existing-key",
"prompt_cache_key": "ltm-pc-v2-5557e02f5c9b447a97673ba330dbe77a",
});
apply_codex_openai_responses_special_body_edits(
@@ -195,7 +197,222 @@ fn keeps_existing_prompt_cache_key_for_codex_requests() {
Some("key-123"),
);
assert_eq!(body["prompt_cache_key"], "existing-key");
let expected_identity = "d9c5d122-7c1c-5fb1-ba9d-656062eda44e";
assert_eq!(body["prompt_cache_key"], expected_identity);
assert_eq!(body["client_metadata"]["session_id"], expected_identity);
assert_eq!(body["client_metadata"]["thread_id"], expected_identity);
}
#[test]
fn preserves_native_codex_cache_identity_and_metadata() {
let mut body = json!({
"model": "gpt-5",
"input": "hello",
"prompt_cache_key": "guardian:parent-thread",
"client_metadata": {
"session_id": "native-session",
"thread_id": "native-thread",
"turn_id": "native-turn"
}
});
let expected = body.clone();
apply_codex_openai_responses_special_body_edits(
&mut body,
"codex",
"openai:responses",
None,
Some("key-123"),
);
assert_eq!(body["prompt_cache_key"], expected["prompt_cache_key"]);
assert_eq!(body["client_metadata"], expected["client_metadata"]);
}
#[test]
fn preserves_uuid_prompt_cache_key_while_completing_codex_identity() {
let identity = "172c39e6-c0a0-5a70-8b63-e0f8e0d185a3";
let mut body = json!({
"model": "gpt-5.6-luna",
"input": "hello",
"prompt_cache_key": identity
});
apply_codex_openai_responses_special_body_edits(
&mut body,
"codex",
"openai:responses",
None,
None,
);
assert_eq!(body["prompt_cache_key"], identity);
assert_eq!(body["client_metadata"]["session_id"], identity);
assert_eq!(body["client_metadata"]["thread_id"], identity);
}
#[test]
fn keeps_codex_prompt_cache_domains_distinct() {
let mut first = json!({
"model": "gpt-5.6-luna",
"input": "hello",
"prompt_cache_key": "tenant-a"
});
let mut second = json!({
"model": "gpt-5.6-luna",
"input": "hello",
"prompt_cache_key": "tenant-b"
});
for body in [&mut first, &mut second] {
apply_codex_openai_responses_special_body_edits(
body,
"codex",
"openai:responses",
None,
None,
);
}
assert_ne!(first["prompt_cache_key"], second["prompt_cache_key"]);
assert_eq!(
first["prompt_cache_key"],
first["client_metadata"]["session_id"]
);
assert_eq!(
second["prompt_cache_key"],
second["client_metadata"]["session_id"]
);
}
#[test]
fn completes_partial_and_null_codex_client_metadata() {
let mut partial = json!({
"model": "gpt-5.6-luna",
"input": "hello",
"prompt_cache_key": "generic-affinity",
"client_metadata": {
"thread_id": "native-thread",
"caller": "sdk"
}
});
let mut null_metadata = json!({
"model": "gpt-5.6-luna",
"input": "hello",
"prompt_cache_key": "generic-affinity",
"client_metadata": null
});
for body in [&mut partial, &mut null_metadata] {
apply_codex_openai_responses_special_body_edits(
body,
"codex",
"openai:responses",
None,
None,
);
}
assert_eq!(partial["client_metadata"]["thread_id"], "native-thread");
assert_eq!(partial["client_metadata"]["caller"], "sdk");
assert_eq!(
partial["client_metadata"]["session_id"],
partial["prompt_cache_key"]
);
assert_eq!(
null_metadata["client_metadata"]["session_id"],
null_metadata["prompt_cache_key"]
);
assert_eq!(
null_metadata["client_metadata"]["thread_id"],
null_metadata["prompt_cache_key"]
);
}
#[test]
fn leaves_malformed_codex_client_metadata_unchanged() {
let mut body = json!({
"model": "gpt-5.6-luna",
"input": "hello",
"prompt_cache_key": "generic-affinity",
"client_metadata": "invalid"
});
apply_codex_openai_responses_special_body_edits(
&mut body,
"codex",
"openai:responses",
None,
None,
);
assert_eq!(body["prompt_cache_key"], "generic-affinity");
assert_eq!(body["client_metadata"], "invalid");
}
#[test]
fn limits_prompt_cache_identity_adaptation_to_codex_responses_family() {
let original = json!({
"model": "gpt-5.6-luna",
"input": "hello",
"prompt_cache_key": "generic-affinity"
});
let mut standard_openai = original.clone();
let mut codex_compact = original.clone();
apply_codex_openai_responses_special_body_edits(
&mut standard_openai,
"openai",
"openai:responses",
None,
None,
);
apply_codex_openai_responses_special_body_edits(
&mut codex_compact,
"codex",
"openai:responses:compact",
None,
None,
);
assert_eq!(standard_openai, original);
assert_ne!(codex_compact["prompt_cache_key"], "generic-affinity");
assert!(codex_compact.get("client_metadata").is_none());
}
#[test]
fn chat_to_codex_responses_adapts_prompt_cache_identity_end_to_end() {
let body = json!({
"model": "gpt-5.6-luna",
"messages": [{"role": "user", "content": "hello"}],
"prompt_cache_key": "ltm-pc-v2-5557e02f5c9b447a97673ba330dbe77a"
});
let provider_request_body = build_cross_format_openai_responses_request_body(
&body,
"gpt-5.6-luna",
"openai:chat",
"openai:responses",
true,
false,
"codex",
None,
None,
&HeaderMap::new(),
false,
)
.expect("chat to Codex Responses request should build");
let expected_identity = "d9c5d122-7c1c-5fb1-ba9d-656062eda44e";
assert_eq!(provider_request_body["prompt_cache_key"], expected_identity);
assert_eq!(
provider_request_body["client_metadata"]["session_id"],
expected_identity
);
assert_eq!(
provider_request_body["client_metadata"]["thread_id"],
expected_identity
);
}
#[test]
@@ -7,6 +7,8 @@ use serde_json::{json, Value};
const CODEX_DEFAULT_REASONING_EFFORT: &str = "medium";
const CODEX_REASONING_ENCRYPTED_CONTENT_INCLUDE: &str = "reasoning.encrypted_content";
const CODEX_PROMPT_CACHE_IDENTITY_NAMESPACE: &str =
"https://github.com/fawney19/Aether/codex/prompt-cache-identity/v1/";
pub const CODEX_RESPONSES_LITE_HEADER: &str = "x-openai-internal-codex-responses-lite";
pub const CODEX_MODEL_CATALOG_METADATA_FIELD: &str = "codex_models";
const CODEX_OPENAI_RESPONSES_UNSUPPORTED_BODY_FIELDS: &[&str] = &[
@@ -1466,6 +1468,58 @@ fn wrap_codex_responses_string_input_for_backend(
);
}
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.
let Some(prompt_cache_key) = body_object
.get("prompt_cache_key")
.and_then(Value::as_str)
.filter(|value| !value.trim().is_empty())
.map(ToOwned::to_owned)
else {
return;
};
match body_object.get("client_metadata") {
Some(Value::Object(metadata)) if metadata.contains_key("session_id") => return,
Some(Value::Object(_) | Value::Null) | None => {}
Some(_) => return,
}
let cache_identity = uuid::Uuid::parse_str(&prompt_cache_key)
.unwrap_or_else(|_| {
uuid::Uuid::new_v5(
&uuid::Uuid::NAMESPACE_URL,
format!("{CODEX_PROMPT_CACHE_IDENTITY_NAMESPACE}{prompt_cache_key}").as_bytes(),
)
})
.to_string();
{
let metadata = body_object
.entry("client_metadata".to_string())
.or_insert_with(|| json!({}));
if metadata.is_null() {
*metadata = json!({});
}
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()));
}
body_object.insert(
"prompt_cache_key".to_string(),
Value::String(cache_identity),
);
}
pub fn apply_codex_openai_responses_special_body_edits(
provider_request_body: &mut Value,
provider_type: &str,
@@ -1532,6 +1586,7 @@ pub fn apply_codex_openai_responses_special_body_edits_with_source_model_and_cap
body_object.remove(*field);
}
}
adapt_codex_prompt_cache_identity_for_backend(body_object);
if is_openai_responses_compact_request(provider_api_format) {
body_object.remove("store");
} else if !body_rules_handle_path(body_rules, "store") {