merge(main): 解决 usage 展示与生命周期同步冲突

This commit is contained in:
MMEXA
2026-07-18 05:38:38 +08:00
111 changed files with 8984 additions and 1715 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,
@@ -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,148 @@ 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 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)
.filter(|value| !value.trim().is_empty())
.map(ToOwned::to_owned)
else {
return;
};
match body_object.get("client_metadata") {
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,
}
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.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(
"prompt_cache_key".to_string(),
Value::String(cache_identity),
);
}
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,
@@ -1532,6 +1676,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") {
@@ -2,6 +2,7 @@ use std::collections::BTreeMap;
use serde_json::Value;
use crate::formats::shared::stream_core::common::openai_stream_terminal_error_body;
use crate::formats::shared::AiSurfaceFinalizeError;
use crate::provider_compat::kiro_stream::KiroToClaudeCliStreamState;
@@ -719,6 +720,27 @@ fn extract_stream_error_event_body(body: &[u8]) -> Option<Value> {
{
return Some(normalize_provider_private_error_body(event));
}
if let Some(mut error_body) = openai_stream_terminal_error_body(&event) {
let response_failed_without_provider_type = event
.get("type")
.and_then(Value::as_str)
.is_some_and(|event_type| event_type.eq_ignore_ascii_case("response.failed"))
&& event
.pointer("/response/error/type")
.or_else(|| event.pointer("/error/type"))
.and_then(Value::as_str)
.map(str::trim)
.is_none_or(str::is_empty);
if response_failed_without_provider_type {
if let Some(error) = error_body.get_mut("error").and_then(Value::as_object_mut) {
error.insert(
"type".to_string(),
Value::String("stream_terminal_error".to_string()),
);
}
}
return Some(error_body);
}
current_event_type = None;
}
None
@@ -1179,6 +1201,53 @@ data: {"message":"bad"}
assert!(stream_body_contains_error_event(body));
}
#[test]
fn extracts_openai_response_failed_error_without_losing_provider_fields() {
let body = br#"event: response.failed
data: {"type":"response.failed","response":{"status":"failed","error":{"type":"invalid_request","message":"cyber policy rejected the request","code":"cyber_policy_violation","param":"input","details":{"policy_category":"cybersecurity","appeal_allowed":true}}}}
"#;
let error_body = extract_provider_private_stream_error_body(None, body)
.expect("response.failed should expose its provider error body");
assert_eq!(
error_body,
json!({
"error": {
"type": "invalid_request",
"message": "cyber policy rejected the request",
"code": "cyber_policy_violation",
"param": "input",
"details": {
"policy_category": "cybersecurity",
"appeal_allowed": true
}
}
})
);
}
#[test]
fn response_failed_without_provider_type_uses_stream_terminal_error_category() {
let body = br#"event: response.failed
data: {"type":"response.failed","response":{"status":"failed","error":{"message":"cyber policy rejected the request","code":"cyber_policy"}}}
"#;
let error_body = extract_provider_private_stream_error_body(None, body)
.expect("response.failed should expose its provider error body");
assert_eq!(
error_body.pointer("/error/type"),
Some(&json!("stream_terminal_error"))
);
assert_eq!(
error_body.pointer("/error/code"),
Some(&json!("cyber_policy"))
);
}
#[test]
fn windsurf_sync_error_message_is_not_normalized_as_success() {
let report_context = json!({