feat(gateway): add Codex Live transport

This commit is contained in:
ZheFox
2026-08-20 21:59:05 +08:00
parent 6916e9da76
commit 4185ad1b1e
35 changed files with 6683 additions and 85 deletions
+11 -10
View File
@@ -50,15 +50,16 @@ pub(crate) use aether_ai_formats::api::{
resolve_claude_stream_spec, resolve_claude_sync_spec, resolve_gemini_stream_spec,
resolve_gemini_sync_spec, resolve_local_image_stream_spec, resolve_local_image_sync_spec,
resolve_local_same_format_stream_spec, resolve_local_same_format_sync_spec,
resolve_openai_embedding_sync_spec, sanitize_request_path_and_query, AiControlPlanRequest,
CanonicalContentPart, CanonicalStreamEvent, CanonicalStreamFrame, ClaudeClientEmitter,
ExecutionRuntimeAuthContext, LocalCoreSyncErrorKind, LocalOpenAiImageSpec,
LocalSameFormatProviderFamily, LocalSameFormatProviderSpec, LocalStandardSourceFamily,
LocalStandardSourceMode, LocalStandardSpec, OpenAIChatClientEmitter,
OpenAIResponsesClientEmitter, StreamingStandardTerminalObserver, CLAUDE_CHAT_STREAM_PLAN_KIND,
CLAUDE_CLI_STREAM_PLAN_KIND, EXECUTION_RUNTIME_STREAM_DECISION_ACTION,
EXECUTION_RUNTIME_SYNC_DECISION_ACTION, GEMINI_CHAT_STREAM_PLAN_KIND,
GEMINI_CLI_STREAM_PLAN_KIND, GEMINI_EMBEDDING_SYNC_PLAN_KIND, GEMINI_FILES_DOWNLOAD_PLAN_KIND,
resolve_openai_embedding_sync_spec, sanitize_request_path_and_query,
sanitize_request_query_string, AiControlPlanRequest, CanonicalContentPart,
CanonicalStreamEvent, CanonicalStreamFrame, ClaudeClientEmitter, ExecutionRuntimeAuthContext,
LocalCoreSyncErrorKind, LocalOpenAiImageSpec, LocalSameFormatProviderFamily,
LocalSameFormatProviderSpec, LocalStandardSourceFamily, LocalStandardSourceMode,
LocalStandardSpec, OpenAIChatClientEmitter, OpenAIResponsesClientEmitter,
StreamingStandardTerminalObserver, CLAUDE_CHAT_STREAM_PLAN_KIND, CLAUDE_CLI_STREAM_PLAN_KIND,
EXECUTION_RUNTIME_STREAM_DECISION_ACTION, EXECUTION_RUNTIME_SYNC_DECISION_ACTION,
GEMINI_CHAT_STREAM_PLAN_KIND, GEMINI_CLI_STREAM_PLAN_KIND, GEMINI_EMBEDDING_SYNC_PLAN_KIND,
GEMINI_FILES_DOWNLOAD_PLAN_KIND, GEMINI_INTERACTIONS_STREAM_PLAN_KIND,
GEMINI_VIDEO_CANCEL_SYNC_PLAN_KIND, OPENAI_CHAT_STREAM_PLAN_KIND,
OPENAI_EMBEDDING_SYNC_PLAN_KIND, OPENAI_IMAGE_STREAM_PLAN_KIND,
OPENAI_IMAGE_SYNC_FINALIZE_REPORT_KIND, OPENAI_IMAGE_SYNC_PLAN_KIND,
@@ -68,10 +69,10 @@ pub(crate) use aether_ai_formats::api::{
OPENAI_VIDEO_REMIX_SYNC_PLAN_KIND,
};
pub(crate) use aether_ai_formats::protocol::stream::CanonicalUsage as StreamingCanonicalUsage;
pub(crate) use aether_ai_formats::CODEX_RESPONSES_LITE_HEADER;
/// Codex client identity headers re-exported for out-of-crate probe binaries,
/// which must reach `aether_ai_formats` through this seam.
pub use aether_ai_formats::{CODEX_CLIENT_ORIGINATOR, CODEX_CLIENT_USER_AGENT};
pub(crate) use aether_ai_formats::{CODEX_RESPONSES_LITE_HEADER, UPSTREAM_IS_STREAM_KEY};
pub(crate) fn parse_direct_request_body(
parts: &http::request::Parts,
@@ -486,8 +486,7 @@ pub(crate) async fn attach_routing_policy_to_local_requested_model_input(
body_json: &Value,
client_api_format: &str,
) -> Result<(), GatewayError> {
input.original_client_session_id = routing_header_value_str(&parts.headers, "session-id")
.or_else(|| routing_header_value_str(&parts.headers, "session_id"));
input.original_client_session_id = original_client_session_id_from_headers(&parts.headers);
let explicit_group = routing_header_value_str(&parts.headers, ROUTING_GROUP_HEADER);
let selected_group = match state.routing_group_read_repository() {
Some(repository) => {
@@ -738,6 +737,12 @@ pub(crate) async fn attach_routing_policy_to_local_requested_model_input(
Ok(())
}
fn original_client_session_id_from_headers(headers: &HeaderMap) -> Option<String> {
routing_header_value_str(headers, "session-id")
.or_else(|| routing_header_value_str(headers, "session_id"))
.or_else(|| routing_header_value_str(headers, "x-session-id"))
}
fn try_attach_static_default_routing_policy_to_input(
input: &mut LocalRequestedModelDecisionInput,
parts: &http::request::Parts,
@@ -1101,6 +1106,38 @@ mod tests {
GatewayProviderTransportProvider,
};
#[test]
fn original_client_session_id_accepts_live_header_as_fallback() {
let headers = HeaderMap::from_iter([(
HeaderName::from_static("x-session-id"),
HeaderValue::from_static("live-thread-1"),
)]);
assert_eq!(
original_client_session_id_from_headers(&headers).as_deref(),
Some("live-thread-1")
);
}
#[test]
fn original_client_session_id_prefers_responses_headers_over_live_fallback() {
let headers = HeaderMap::from_iter([
(
HeaderName::from_static("session-id"),
HeaderValue::from_static("responses-session"),
),
(
HeaderName::from_static("x-session-id"),
HeaderValue::from_static("live-thread"),
),
]);
assert_eq!(
original_client_session_id_from_headers(&headers).as_deref(),
Some("responses-session")
);
}
#[test]
fn explicit_routing_selection_cache_key_is_principal_specific() {
let first = routing_group_selection_cache_key(
@@ -61,6 +61,34 @@ pub(crate) fn request_identity_response_encoding_when_redacted(
}
}
/// Removes credential-bearing URL components before attaching an upstream URL
/// to a diagnostic event. Endpoint query parameters remain untouched on the
/// wire, but they can contain API keys or signed tokens and must not reach
/// logs.
pub(crate) fn sanitize_upstream_url_for_log(raw: &str) -> String {
if let Ok(mut url) = url::Url::parse(raw) {
if !matches!(url.scheme(), "http" | "https") || url.host_str().is_none() {
return "<invalid-upstream-url>".to_string();
}
let _ = url.set_username("");
let _ = url.set_password(None);
url.set_query(None);
url.set_fragment(None);
return url.to_string();
}
let suffix_offset = raw
.char_indices()
.find_map(|(offset, character)| matches!(character, '?' | '#').then_some(offset))
.unwrap_or(raw.len());
let path = &raw[..suffix_offset];
if path.starts_with('/') && !path.starts_with("//") && !path.contains('@') {
path.to_string()
} else {
"<invalid-upstream-url>".to_string()
}
}
pub(crate) async fn resolve_provider_chat_pii_redaction<'a>(
state: &AppState,
parts: &http::request::Parts,
@@ -240,7 +268,32 @@ fn redaction_mask_error_to_gateway_error(error: RedactionMaskError) -> GatewayEr
mod tests {
use serde_json::json;
use super::ChatPiiRedactionFeatureSettings;
use super::{sanitize_upstream_url_for_log, ChatPiiRedactionFeatureSettings};
#[test]
fn upstream_url_log_projection_removes_all_credential_carriers() {
assert_eq!(
sanitize_upstream_url_for_log(
"https://user:[email protected]/v1/responses?api-version=2026-08-01&token=secret#fragment"
),
"https://api.example.test/v1/responses"
);
assert_eq!(
sanitize_upstream_url_for_log("/v1/responses?key=secret#fragment"),
"/v1/responses"
);
for invalid in [
"https://user:secret@invalid host/v1/responses",
"//user:[email protected]/v1/responses?token=hidden",
"not-a-url?token=hidden",
"data:text/plain,secret",
] {
assert_eq!(
sanitize_upstream_url_for_log(invalid),
"<invalid-upstream-url>"
);
}
}
#[test]
fn chat_pii_redaction_feature_settings_only_control_enablement() {
@@ -10,6 +10,7 @@ use super::super::{
AiStreamAttempt,
};
use crate::ai_serving::planner::common::enforce_provider_body_stream_policy;
use crate::ai_serving::planner::redaction::sanitize_upstream_url_for_log;
use crate::ai_serving::provider_adaptation_requires_eventstream_accept;
use crate::ai_serving::transport::{
build_standard_plan_fallback_headers, build_standard_plan_fallback_openai_chat_url,
@@ -233,6 +234,19 @@ pub(crate) fn build_openai_responses_stream_plan_from_decision(
},
);
let log_downstream_query = parts
.uri
.query()
.and_then(crate::ai_serving::api::sanitize_request_query_string);
let log_decision_upstream_base_url = payload
.upstream_base_url
.as_deref()
.map(sanitize_upstream_url_for_log);
let log_decision_upstream_url = payload
.upstream_url
.as_deref()
.map(sanitize_upstream_url_for_log);
let log_plan_url = sanitize_upstream_url_for_log(plan.url.as_str());
debug!(
event_name = "local_openai_responses_stream_plan_built",
log_type = "debug",
@@ -242,11 +256,11 @@ pub(crate) fn build_openai_responses_stream_plan_from_decision(
endpoint_id = %plan.endpoint_id,
key_id = %plan.key_id,
downstream_path = %parts.uri.path(),
downstream_query = ?parts.uri.query(),
downstream_query = ?log_downstream_query,
url_source,
decision_upstream_base_url = ?payload.upstream_base_url,
decision_upstream_url = ?payload.upstream_url,
plan_url = %plan.url,
decision_upstream_base_url = ?log_decision_upstream_base_url,
decision_upstream_url = ?log_decision_upstream_url,
plan_url = %log_plan_url,
client_api_format = %plan.client_api_format,
provider_api_format = %plan.provider_api_format,
upstream_is_stream = effective_upstream_is_stream,
@@ -10,6 +10,7 @@ use super::super::{
AiSyncAttempt,
};
use crate::ai_serving::planner::common::enforce_provider_body_stream_policy;
use crate::ai_serving::planner::redaction::sanitize_upstream_url_for_log;
use crate::ai_serving::transport::{
build_standard_plan_fallback_headers, build_standard_plan_fallback_openai_chat_url,
build_standard_plan_fallback_openai_responses_url, StandardPlanFallbackAcceptPolicy,
@@ -200,6 +201,19 @@ pub(crate) fn build_openai_responses_sync_plan_from_decision(
},
);
let log_downstream_query = parts
.uri
.query()
.and_then(crate::ai_serving::api::sanitize_request_query_string);
let log_decision_upstream_base_url = payload
.upstream_base_url
.as_deref()
.map(sanitize_upstream_url_for_log);
let log_decision_upstream_url = payload
.upstream_url
.as_deref()
.map(sanitize_upstream_url_for_log);
let log_plan_url = sanitize_upstream_url_for_log(plan.url.as_str());
debug!(
event_name = "local_openai_responses_sync_plan_built",
log_type = "debug",
@@ -209,11 +223,11 @@ pub(crate) fn build_openai_responses_sync_plan_from_decision(
endpoint_id = %plan.endpoint_id,
key_id = %plan.key_id,
downstream_path = %parts.uri.path(),
downstream_query = ?parts.uri.query(),
downstream_query = ?log_downstream_query,
url_source,
decision_upstream_base_url = ?payload.upstream_base_url,
decision_upstream_url = ?payload.upstream_url,
plan_url = %plan.url,
decision_upstream_base_url = ?log_decision_upstream_base_url,
decision_upstream_url = ?log_decision_upstream_url,
plan_url = %log_plan_url,
client_api_format = %plan.client_api_format,
provider_api_format = %plan.provider_api_format,
upstream_is_stream = payload.upstream_is_stream,
@@ -3,6 +3,7 @@ use tracing::debug;
use crate::ai_serving::build_request_trace_proxy_value;
use crate::ai_serving::planner::decision_input::apply_provider_request_routing_policy_to_decision_with_websocket_mode;
use crate::ai_serving::planner::redaction::sanitize_upstream_url_for_log;
use crate::ai_serving::planner::report_context::{
build_local_execution_report_context, insert_native_client_envelope_name,
insert_provider_stream_event_api_format, LocalExecutionReportContextParts,
@@ -203,6 +204,12 @@ pub(crate) async fn maybe_build_local_openai_responses_decision_payload_for_cand
&resolved.transport,
);
let log_base_url = sanitize_upstream_url_for_log(resolved.transport.endpoint.base_url.as_str());
let log_request_query = parts
.uri
.query()
.and_then(crate::ai_serving::api::sanitize_request_query_string);
let log_upstream_url = sanitize_upstream_url_for_log(resolved.upstream_url.as_str());
debug!(
event_name = "local_openai_responses_decision_payload_built",
log_type = "debug",
@@ -219,9 +226,9 @@ pub(crate) async fn maybe_build_local_openai_responses_decision_payload_for_cand
client_api_format = spec_metadata.api_format,
provider_api_format = %resolved.provider_api_format,
request_path = %parts.uri.path(),
request_query = ?parts.uri.query(),
upstream_base_url = %resolved.transport.endpoint.base_url,
upstream_url = %resolved.upstream_url,
request_query = ?log_request_query,
upstream_base_url = %log_base_url,
upstream_url = %log_upstream_url,
upstream_is_stream = resolved.upstream_is_stream,
has_envelope = resolved.envelope_name.is_some(),
"gateway built local openai responses decision payload"
@@ -24,6 +24,7 @@ use crate::ai_serving::planner::gemini_cli::{
};
use crate::ai_serving::planner::redaction::{
request_identity_response_encoding_when_redacted, resolve_provider_chat_pii_redaction,
sanitize_upstream_url_for_log,
};
use crate::ai_serving::planner::spec_metadata::local_openai_responses_spec_metadata;
use crate::ai_serving::planner::standard::{
@@ -865,6 +866,17 @@ pub(crate) async fn resolve_local_openai_responses_candidate_payload_parts_with_
let (execution_strategy, conversion_mode) =
ai_local_execution_contract_for_formats(spec_metadata.api_format, provider_api_format);
let log_base_url = sanitize_upstream_url_for_log(transport.endpoint.base_url.as_str());
let log_custom_path = transport
.endpoint
.custom_path
.as_deref()
.map(sanitize_upstream_url_for_log);
let log_request_query = parts
.uri
.query()
.and_then(crate::ai_serving::api::sanitize_request_query_string);
let log_upstream_url = sanitize_upstream_url_for_log(upstream_url.as_str());
debug!(
event_name = "local_openai_responses_upstream_url_resolved",
@@ -880,12 +892,12 @@ pub(crate) async fn resolve_local_openai_responses_candidate_payload_parts_with_
provider_api_format = %provider_api_format,
execution_strategy = execution_strategy.as_str(),
conversion_mode = conversion_mode.as_str(),
base_url = %transport.endpoint.base_url,
custom_path = ?transport.endpoint.custom_path,
base_url = %log_base_url,
custom_path = ?log_custom_path,
request_path = %parts.uri.path(),
request_query = ?parts.uri.query(),
request_query = ?log_request_query,
mapped_model = %mapped_model,
upstream_url = %upstream_url,
upstream_url = %log_upstream_url,
upstream_is_stream,
"gateway resolved local openai responses upstream url"
);
@@ -1998,6 +2010,7 @@ async fn build_kiro_openai_responses_payload_parts(
};
let (execution_strategy, conversion_mode) =
ai_local_execution_contract_for_formats(client_api_format, provider_api_format);
let log_upstream_url = sanitize_upstream_url_for_log(upstream_url.as_str());
debug!(
event_name = "local_openai_responses_kiro_upstream_url_resolved",
@@ -2013,7 +2026,7 @@ async fn build_kiro_openai_responses_payload_parts(
provider_api_format = %provider_api_format,
execution_strategy = execution_strategy.as_str(),
conversion_mode = conversion_mode.as_str(),
upstream_url = %upstream_url,
upstream_url = %log_upstream_url,
upstream_is_stream,
"gateway resolved local openai responses kiro upstream url"
);
@@ -234,6 +234,10 @@ pub(crate) struct ResponsesWebSocketDecision {
pub(crate) execution: AiExecutionDecision,
pub(crate) adapter: ResponsesWebSocketAdapter,
pub(crate) normalization: ResponsesWebSocketBodyNormalization,
/// Effective key auth after applying the endpoint API-format override.
/// Protocol companions such as Codex Live must not infer this from a URL
/// or from the presence of one particular generated header.
pub(crate) effective_auth_type: String,
}
/// The scheduler identity a continuation is allowed to reuse.
@@ -909,6 +913,10 @@ pub(crate) async fn maybe_build_responses_websocket_decision(
// Captured before `attempt` is consumed so a later continuation turn can
// reproduce this candidate's body normalization without re-planning.
let transport = std::sync::Arc::clone(&attempt.eligible.transport);
let effective_auth_type =
aether_provider_transport::auth::resolve_local_auth_type_for_transport_format(
transport.as_ref(),
);
let candidate_provider_api_format = attempt.eligible.provider_api_format.clone();
let payload = match maybe_build_local_openai_responses_decision_payload_for_candidate_with_websocket_mode(
state,
@@ -1013,6 +1021,7 @@ pub(crate) async fn maybe_build_responses_websocket_decision(
execution: payload,
adapter,
normalization,
effective_auth_type,
};
// The decision report context now carries the lease identity. The
// WebSocket ownership layer takes over before any further await.