feat(codex): stabilize identity across retries

This commit is contained in:
elky
2026-09-01 15:33:40 +08:00
parent b538aa2d66
commit a39048ecce
19 changed files with 1731 additions and 156 deletions
@@ -1092,6 +1092,7 @@ async fn proxy_request_inner(
),
}
let (mut parts, body) = request.into_parts();
crate::ai_serving::codex_context::install_codex_fingerprint_context_slot(&mut parts);
let redaction_slot = crate::privacy::RedactionSessionSlot::default();
parts.extensions.insert(redaction_slot.clone());
parts
@@ -49,6 +49,8 @@ pub(super) enum LiveAuthMode {
pub(super) struct PlannedLiveCandidate {
pub(super) execution: AiExecutionDecision,
pub(super) pinned_candidate: ResponsesWebSocketPinnedCandidate,
pub(super) codex_fingerprint_context:
aether_provider_transport::CodexFingerprintConvergenceContext,
pub(super) client_model: String,
pub(super) provider_model: String,
pub(super) auth_mode: LiveAuthMode,
@@ -228,8 +230,9 @@ async fn plan_live_candidate_inner(
if validate_model(client_model).is_err() || client_model.len() > MAX_LIVE_MODEL_BYTES {
return Ok(None);
}
let parts = build_live_planning_parts(headers, remote_addr);
let mut parts = build_live_planning_parts(headers, remote_addr);
let body = json!({"model": client_model, "input": []});
crate::ai_serving::codex_context::install_codex_fingerprint_context_slot(&mut parts);
let execution = maybe_build_pinned_stream_local_same_format_provider_decision_payload(
state,
&parts,
@@ -338,6 +341,8 @@ async fn plan_live_candidate_inner(
Ok(Some(PlannedLiveCandidate {
execution,
pinned_candidate,
codex_fingerprint_context:
crate::ai_serving::codex_context::resolve_codex_fingerprint_context(&parts, &body),
client_model: client_model.to_string(),
provider_model,
auth_mode,
@@ -560,7 +565,11 @@ pub(super) fn build_live_stream_admission_attempt(
remote_addr: &SocketAddr,
upstream_url: String,
) -> Result<Option<AiStreamAttempt>, GatewayError> {
let parts = build_live_planning_parts(headers, remote_addr);
let mut parts = build_live_planning_parts(headers, remote_addr);
crate::ai_serving::codex_context::restore_codex_logical_turn_context(
&mut parts,
&candidate.codex_fingerprint_context,
);
let body = json!({"model": candidate.client_model.as_str(), "input": []});
let mut execution = candidate.execution.clone();
execution.upstream_url = Some(upstream_url);
@@ -922,6 +931,11 @@ mod tests {
"key-1",
)
.unwrap(),
codex_fingerprint_context:
aether_provider_transport::CodexFingerprintConvergenceContext::new(
"test-live-turn",
1,
),
client_model: "global-model".to_string(),
provider_model: "provider-model".to_string(),
auth_mode,
@@ -718,6 +718,11 @@ mod tests {
PlannedLiveCandidate {
execution,
pinned_candidate: binding.pinned_candidate.clone(),
codex_fingerprint_context:
aether_provider_transport::CodexFingerprintConvergenceContext::new(
"test-live-turn",
1,
),
client_model: binding.client_model.clone(),
provider_model: binding.provider_model.clone(),
auth_mode: binding.auth_mode,
@@ -1,5 +1,6 @@
//! Client-side Responses WebSocket event forwarding and follow-up planning.
use aether_provider_transport::CodexFingerprintConvergenceContext;
use axum::extract::ws::{Message as AxumWsMessage, WebSocket};
use futures_util::SinkExt;
use serde_json::Value;
@@ -248,7 +249,14 @@ pub(super) async fn forward_client_message(
// derive one strong live control snapshot that every stage below
// shares. The connection's Upgrade-time decision is only the
// immutable identity seed.
let planning_parts = build_planning_parts(context);
let logical_turn_id = Uuid::now_v7().to_string();
let mut planning_parts = build_planning_parts(context);
let codex_fingerprint_context =
crate::ai_serving::codex_context::attach_codex_logical_turn_context(
&mut planning_parts,
&client_event,
&logical_turn_id,
);
let turn_control = match resolve_responses_websocket_turn_control(
state,
context,
@@ -453,6 +461,8 @@ pub(super) async fn forward_client_message(
context,
planning_parts,
client_event,
logical_turn_id,
codex_fingerprint_context,
turn_control,
turn_redaction_session,
)
@@ -466,6 +476,8 @@ pub(super) async fn forward_client_message(
planning_parts,
client_event,
requested_model,
logical_turn_id,
codex_fingerprint_context,
turn_control,
raw_responses_lite_static_config
.expect("independent turns always retain their raw static config"),
@@ -513,6 +525,8 @@ async fn forward_pinned_continuation(
context: &WebSocketRequestContext,
planning_parts: http::request::Parts,
client_event: Value,
logical_turn_id: String,
codex_fingerprint_context: CodexFingerprintConvergenceContext,
turn_control: ResponsesWebSocketTurnControl,
turn_redaction_session: Option<RedactionSession>,
) -> RelayDisposition {
@@ -556,7 +570,6 @@ async fn forward_pinned_continuation(
};
let turn_request_id = Uuid::new_v4().to_string();
let logical_turn_id = Uuid::new_v4().to_string();
let planned = match await_owned_responses_websocket_plan(spawn_owned_responses_websocket_plan(
state.clone(),
planning_parts,
@@ -766,6 +779,7 @@ async fn forward_pinned_continuation(
bound.body_normalization = normalization;
bound.turn_state.begin(
LogicalTurn::new(client_event, turn_index, logical_turn_id)
.with_codex_fingerprint_context(codex_fingerprint_context)
.with_provider_store(provider_event.get("store") == Some(&Value::Bool(true)))
.with_turn_control(turn_control),
turn,
@@ -800,12 +814,13 @@ async fn forward_replanned_response_create(
planning_parts: http::request::Parts,
client_event: Value,
requested_model: String,
logical_turn_id: String,
codex_fingerprint_context: CodexFingerprintConvergenceContext,
turn_control: ResponsesWebSocketTurnControl,
raw_responses_lite_static_config: ResponsesLiteStaticConfig,
turn_redaction_session: Option<RedactionSession>,
) -> RelayDisposition {
let turn_request_id = Uuid::new_v4().to_string();
let logical_turn_id = Uuid::new_v4().to_string();
let now_unix_secs = current_unix_secs();
let excluded_key_ids = bound.exhausted_exclusions.key_ids(now_unix_secs);
let excluded_codex_account_ids = bound.exhausted_exclusions.codex_account_ids(now_unix_secs);
@@ -992,6 +1007,7 @@ async fn forward_replanned_response_create(
bound.body_normalization = normalization;
bound.turn_state.begin(
LogicalTurn::new(client_event.clone(), turn_index, logical_turn_id.clone())
.with_codex_fingerprint_context(codex_fingerprint_context.clone())
.with_provider_store(provider_event.get("store") == Some(&Value::Bool(true)))
.with_turn_control(turn_control),
turn,
@@ -1076,6 +1092,7 @@ async fn forward_replanned_response_create(
bound.binding_identity = replacement.binding_identity;
bound.turn_state.begin(
LogicalTurn::new(client_event, turn_index, logical_turn_id)
.with_codex_fingerprint_context(codex_fingerprint_context)
.with_provider_store(provider_event.get("store") == Some(&Value::Bool(true)))
.with_turn_control(turn_control),
turn,
@@ -146,6 +146,7 @@ pub(super) async fn retry_active_turn_after_quota_exhaustion(
};
let turn_index = active.turn_index;
let logical_turn_id = active.logical_turn_id.clone();
let codex_fingerprint_context = active.codex_fingerprint_context.clone();
let turn_attempt = active.turn_attempt;
let retry_exclusion_until_unix_secs = bound
@@ -154,7 +155,13 @@ pub(super) async fn retry_active_turn_after_quota_exhaustion(
let exhausted_key = record_exhausted_bound_key(bound, retry_exclusion_until_unix_secs);
let exhausted_key_id = exhausted_key.as_ref().map(|(key_id, _)| key_id.clone());
let planning_parts = build_planning_parts(context);
let mut planning_parts = build_planning_parts(context);
if let Some(codex_fingerprint_context) = codex_fingerprint_context.as_ref() {
crate::ai_serving::codex_context::restore_codex_logical_turn_context(
&mut planning_parts,
codex_fingerprint_context,
);
}
let turn_request_id = Uuid::new_v4().to_string();
let now_unix_secs = current_unix_secs();
let excluded_key_ids = bound.exhausted_exclusions.key_ids(now_unix_secs);
@@ -444,7 +444,14 @@ async fn bootstrap_responses_websocket(
let raw_responses_lite_static_config =
ResponsesLiteStaticConfig::from_response_create(&first_event);
let planning_parts = build_planning_parts(context);
let first_logical_turn_id = Uuid::now_v7().to_string();
let mut planning_parts = build_planning_parts(context);
let first_codex_fingerprint_context =
crate::ai_serving::codex_context::attach_codex_logical_turn_context(
&mut planning_parts,
&first_event,
&first_logical_turn_id,
);
let turn_control = match resolve_responses_websocket_turn_control(
&state,
context,
@@ -860,7 +867,6 @@ async fn bootstrap_responses_websocket(
return None;
}
};
let first_logical_turn_id = Uuid::new_v4().to_string();
let first_turn_decision = prepare_responses_websocket_turn_decision(
&decision,
context.trace_id.clone(),
@@ -954,6 +960,7 @@ async fn bootstrap_responses_websocket(
}
bound.turn_state.begin(
LogicalTurn::new(first_event, 1, first_logical_turn_id)
.with_codex_fingerprint_context(first_codex_fingerprint_context)
.with_provider_store(first_provider_event.get("store") == Some(&Value::Bool(true)))
.with_turn_control(turn_control),
first_turn,
@@ -5,6 +5,7 @@
//! 非法组合只能靠调用点的 if 和「记得同时改另外两个字段」来避免。这里把它收敛成
//! 一个枚举:合法组合由类型保证,转换只能走受控 API。
use aether_provider_transport::CodexFingerprintConvergenceContext;
use serde_json::Value;
use super::control::ResponsesWebSocketTurnControl;
@@ -26,6 +27,9 @@ pub(super) struct LogicalTurn {
pub(super) provider_store: bool,
pub(super) turn_index: u64,
pub(super) logical_turn_id: String,
/// Immutable Codex client identity for every provider attempt belonging to
/// this logical turn. A transparent re-plan must never mint a new turn.
pub(super) codex_fingerprint_context: Option<CodexFingerprintConvergenceContext>,
pub(super) turn_attempt: u32,
pub(super) retry_attempted: bool,
pub(super) retry_unsafe_reason: Option<&'static str>,
@@ -42,6 +46,7 @@ impl LogicalTurn {
provider_store: false,
turn_index,
logical_turn_id,
codex_fingerprint_context: None,
turn_attempt: 1,
retry_attempted: false,
retry_unsafe_reason: None,
@@ -54,6 +59,14 @@ impl LogicalTurn {
self
}
pub(super) fn with_codex_fingerprint_context(
mut self,
context: CodexFingerprintConvergenceContext,
) -> Self {
self.codex_fingerprint_context = Some(context);
self
}
pub(super) fn with_provider_store(mut self, provider_store: bool) -> Self {
self.provider_store = provider_store;
self