mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-04 16:37:46 +08:00
feat(codex): stabilize identity across retries
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user