From 14befeda2c8c02566fe9c5cd3105f6a9b0fd6bf5 Mon Sep 17 00:00:00 2001 From: MMEXA Date: Thu, 1 Oct 2026 17:54:04 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AF=B9=E9=BD=90=20Codex=20CLI=200.159.3=20?= =?UTF-8?q?=E7=9A=84=E9=80=9A=E7=94=A8=E7=94=BB=E5=83=8F=E3=80=81=E6=A8=A1?= =?UTF-8?q?=E5=9E=8B=E8=83=BD=E5=8A=9B=E4=B8=8E=E5=8E=9F=E7=94=9F=E5=8D=8F?= =?UTF-8?q?=E8=AE=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cargo.lock | 118 ++- .../planner/decision/control_plan.rs | 4 +- .../src/ai_serving/planner/decision_input.rs | 36 +- .../passthrough/provider/family/request.rs | 16 +- .../passthrough/provider/request/body.rs | 3 +- .../src/ai_serving/transport.rs | 2 +- apps/aether-gateway/src/constants.rs | 1 + apps/aether-gateway/src/control/route/ai.rs | 5 + .../src/execution_runtime/fallback.rs | 9 + .../src/frontdoor_loop_guard.rs | 1 + .../proxy/websocket/responses/adapter.rs | 7 +- .../websocket/responses/adapters/codex.rs | 96 +- .../proxy/websocket/responses/connection.rs | 1 + .../src/tests/ai_execute/sync/search.rs | 416 +++++--- crates/aether-ai/formats/Cargo.toml | 1 + crates/aether-ai/formats/src/codex_profile.rs | 26 +- crates/aether-ai/formats/src/contracts/mod.rs | 5 +- .../formats/src/contracts/plan_kinds.rs | 1 + .../formats/src/contracts/report_kinds.rs | 8 + .../src/contracts/request_dimensions.rs | 6 + .../src/formats/openai/responses/codex.rs | 205 ++-- .../responses/codex_models_0_159_3.json | 952 ++++++++++++++++++ .../formats/src/formats/shared/passthrough.rs | 21 + .../formats/src/formats/shared/routing.rs | 23 +- .../src/formats/shared/sync_products.rs | 22 + crates/aether-model-fetch/src/logic.rs | 16 +- crates/aether-model-fetch/src/transport.rs | 20 +- .../src/provider/providers/codex.rs | 31 + .../src/provider/providers/generic.rs | 2 +- .../src/provider/providers/mod.rs | 2 +- .../transport/src/codex_fingerprint.rs | 30 + crates/aether-provider/transport/src/lib.rs | 1 + .../transport/src/outbound_request_policy.rs | 12 + .../transport/src/provider_types.rs | 2 +- .../transport/src/request_url/mod.rs | 112 +++ .../transport/src/same_format_provider/mod.rs | 73 +- crates/aether-usage/runtime/src/report.rs | 2 + docs/operations/codex-cli-alignment.md | 33 + 38 files changed, 1938 insertions(+), 383 deletions(-) create mode 100644 crates/aether-ai/formats/src/formats/openai/responses/codex_models_0_159_3.json create mode 100644 docs/operations/codex-cli-alignment.md diff --git a/Cargo.lock b/Cargo.lock index ab01ea5dc..d7fc27398 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -55,7 +55,7 @@ dependencies = [ "aether-provider-pool", "aether-provider-transport", "axum", - "base64", + "base64 0.22.1", "chrono", "http", "regex", @@ -82,8 +82,9 @@ name = "aether-ai-formats" version = "0.1.0" dependencies = [ "aether-contracts", - "base64", + "base64 0.22.1", "http", + "os_info", "regex", "serde", "serde_json", @@ -103,7 +104,7 @@ dependencies = [ "aether-pool-core", "aether-scheduler-core", "async-trait", - "base64", + "base64 0.22.1", "http", "serde", "serde_json", @@ -134,7 +135,7 @@ name = "aether-contracts" version = "0.1.0" dependencies = [ "aes-gcm", - "base64", + "base64 0.22.1", "bytes", "flate2", "hmac", @@ -151,7 +152,7 @@ version = "0.1.0" dependencies = [ "aes", "aws-lc-rs", - "base64", + "base64 0.22.1", "cbc", "hmac", "pbkdf2", @@ -193,7 +194,7 @@ dependencies = [ "aether-contracts", "aether-routing-core", "async-trait", - "base64", + "base64 0.22.1", "bcrypt", "chrono", "chrono-tz", @@ -294,7 +295,7 @@ dependencies = [ "async-trait", "aws-lc-rs", "axum", - "base64", + "base64 0.22.1", "bcrypt", "brotli", "bytes", @@ -389,7 +390,7 @@ version = "0.1.0" dependencies = [ "aether-admission-core", "aether-contracts", - "base64", + "base64 0.22.1", "bytes", "http", "serde", @@ -477,7 +478,7 @@ dependencies = [ "aether-scheduler-core", "async-trait", "aws-lc-rs", - "base64", + "base64 0.22.1", "regex", "serde_json", "tokio", @@ -491,7 +492,7 @@ version = "0.1.0" dependencies = [ "aether-contracts", "async-trait", - "base64", + "base64 0.22.1", "http", "reqwest 0.12.28", "serde", @@ -548,7 +549,7 @@ dependencies = [ "async-trait", "aws-lc-rs", "axum", - "base64", + "base64 0.22.1", "chrono", "crypto_box", "ed25519-dalek", @@ -682,7 +683,7 @@ dependencies = [ "anyhow", "arc-swap", "axum", - "base64", + "base64 0.22.1", "bytes", "clap", "crossterm 0.28.1", @@ -733,7 +734,7 @@ dependencies = [ "aether-data-contracts", "aether-runtime-state", "async-trait", - "base64", + "base64 0.22.1", "futures-util", "serde", "serde_json", @@ -850,7 +851,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -861,7 +862,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -1031,7 +1032,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b52af3cb4058c895d37317bb27508dccc8e5f2d39454016b297bf4a400597b8" dependencies = [ "axum-core", - "base64", + "base64 0.22.1", "bytes", "form_urlencoded", "futures-util", @@ -1094,6 +1095,12 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" + [[package]] name = "base64ct" version = "1.8.3" @@ -1106,7 +1113,7 @@ version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b1866ecef4f2d06a0bb77880015fdf2b89e25a1c2e5addacb87e459c86dc67e" dependencies = [ - "base64", + "base64 0.22.1", "blowfish", "getrandom 0.2.17", "subtle", @@ -1982,7 +1989,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -2579,7 +2586,7 @@ version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" dependencies = [ - "base64", + "base64 0.22.1", "bytes", "futures-channel", "futures-util", @@ -2590,7 +2597,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2 0.5.10", + "socket2 0.6.3", "tokio", "tower-layer", "tower-service", @@ -2737,12 +2744,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.13.0" +version = "2.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" dependencies = [ "equivalent", - "hashbrown 0.16.1", + "hashbrown 0.17.1", "serde", "serde_core", ] @@ -3225,7 +3232,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -3318,7 +3325,7 @@ checksum = "d354792e39fa5f0009e47623cf8b15b099bf9a652fa55c6f817fe28ac84fea50" dependencies = [ "async-trait", "aws-lc-rs", - "base64", + "base64 0.22.1", "bytes", "chrono", "crc-fast", @@ -3334,7 +3341,7 @@ dependencies = [ "md-5 0.11.0", "parking_lot", "percent-encoding", - "quick-xml", + "quick-xml 0.41.0", "rand 0.10.2", "reqwest 0.13.4", "rustls-pki-types", @@ -3402,6 +3409,17 @@ dependencies = [ "num-traits", ] +[[package]] +name = "os_info" +version = "3.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0e1ac5fde8d43c34139135df8ea9ee9465394b2d8d20f032d38998f64afffc3" +dependencies = [ + "log", + "plist", + "windows-sys 0.52.0", +] + [[package]] name = "palette" version = "0.7.7" @@ -3647,6 +3665,19 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" +[[package]] +name = "plist" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896bade328c13f7042a297ea5ac5b0951f6cf989dea5f32c2fd98da398195cb" +dependencies = [ + "base64 0.23.1", + "indexmap", + "quick-xml 0.42.0", + "serde", + "time", +] + [[package]] name = "poly1305" version = "0.8.0" @@ -3729,6 +3760,15 @@ dependencies = [ "serde", ] +[[package]] +name = "quick-xml" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41b1177fdf999d2321d3fb46ff47159d9c1fb9ad66a4879f8c50a0b504615e9b" +dependencies = [ + "memchr", +] + [[package]] name = "quinn" version = "0.11.9" @@ -3742,7 +3782,7 @@ dependencies = [ "quinn-udp", "rustc-hash", "rustls", - "socket2 0.5.10", + "socket2 0.6.3", "thiserror 2.0.18", "tokio", "tracing", @@ -3780,7 +3820,7 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.5.10", + "socket2 0.6.3", "tracing", "windows-sys 0.60.2", ] @@ -4079,7 +4119,7 @@ version = "0.12.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" dependencies = [ - "base64", + "base64 0.22.1", "bytes", "futures-core", "futures-util", @@ -4121,7 +4161,7 @@ version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" dependencies = [ - "base64", + "base64 0.22.1", "bytes", "futures-core", "futures-util", @@ -4235,7 +4275,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.12.1", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -4294,7 +4334,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -4620,7 +4660,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -4667,7 +4707,7 @@ version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6" dependencies = [ - "base64", + "base64 0.22.1", "bigdecimal", "bytes", "chrono", @@ -4744,7 +4784,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" dependencies = [ "atoi", - "base64", + "base64 0.22.1", "bigdecimal", "bitflags 2.13.1", "byteorder", @@ -4788,7 +4828,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" dependencies = [ "atoi", - "base64", + "base64 0.22.1", "bigdecimal", "bitflags 2.13.1", "byteorder", @@ -4979,7 +5019,7 @@ dependencies = [ "parking_lot", "rustix 1.1.4", "signal-hook", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -5010,7 +5050,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4676b37242ccbd1aabf56edb093a4827dc49086c0ffd764a5705899e0f35f8f7" dependencies = [ "anyhow", - "base64", + "base64 0.22.1", "bitflags 2.13.1", "fancy-regex", "filedescriptor", @@ -6007,7 +6047,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.61.2", ] [[package]] diff --git a/apps/aether-gateway/src/ai_serving/planner/decision/control_plan.rs b/apps/aether-gateway/src/ai_serving/planner/decision/control_plan.rs index a101dfdf5..9d372237e 100644 --- a/apps/aether-gateway/src/ai_serving/planner/decision/control_plan.rs +++ b/apps/aether-gateway/src/ai_serving/planner/decision/control_plan.rs @@ -101,7 +101,9 @@ fn build_sync_plan_payload_from_decision( OPENAI_RESPONSES_SYNC_PLAN_KIND => { build_openai_responses_sync_plan_from_decision(parts, body_json, payload, false)? } - OPENAI_IMAGE_SYNC_PLAN_KIND | OPENAI_SEARCH_SYNC_PLAN_KIND => { + OPENAI_IMAGE_SYNC_PLAN_KIND + | OPENAI_SEARCH_SYNC_PLAN_KIND + | aether_ai_formats::contracts::OPENAI_MEMORIES_SYNC_PLAN_KIND => { build_passthrough_sync_plan_from_decision(parts, payload)? } OPENAI_RESPONSES_COMPACT_SYNC_PLAN_KIND => { diff --git a/apps/aether-gateway/src/ai_serving/planner/decision_input.rs b/apps/aether-gateway/src/ai_serving/planner/decision_input.rs index efa915eb0..277fdea03 100644 --- a/apps/aether-gateway/src/ai_serving/planner/decision_input.rs +++ b/apps/aether-gateway/src/ai_serving/planner/decision_input.rs @@ -123,6 +123,8 @@ pub(crate) fn apply_provider_request_routing_policy_to_decision_with_websocket_m transport: Option<&GatewayProviderTransportSnapshot>, websocket_continuation: bool, ) -> Result<(), GatewayError> { + let native_memories = decision.decision_kind.as_deref() + == Some(aether_ai_formats::contracts::OPENAI_MEMORIES_SYNC_PLAN_KIND); let provider_api_format = decision .provider_api_format .clone() @@ -150,7 +152,12 @@ pub(crate) fn apply_provider_request_routing_policy_to_decision_with_websocket_m input.requested_model.as_str(), ) }); - crate::ai_serving::apply_codex_openai_responses_lite_header_for_request_body_with_capabilities( + if native_memories { + decision + .provider_request_headers + .retain(|name, _| !name.eq_ignore_ascii_case(CODEX_RESPONSES_LITE_HEADER)); + } else { + crate::ai_serving::apply_codex_openai_responses_lite_header_for_request_body_with_capabilities( &mut decision.provider_request_headers, decision.provider_request_body.as_ref(), provider_type.as_str(), @@ -159,6 +166,7 @@ pub(crate) fn apply_provider_request_routing_policy_to_decision_with_websocket_m input.requested_model.as_str(), model_capabilities.as_ref(), ); + } let Some(context) = input.routing_context.as_ref() else { // Cache identity headers are projected only at the terminal boundary. Any non-empty @@ -260,7 +268,7 @@ pub(crate) fn apply_provider_request_routing_policy_to_decision_with_websocket_m provider_headers.insert(HeaderName::from_static(name), value); } } - if original_provider_request_body.is_some() { + if original_provider_request_body.is_some() && !native_memories { let provider_model = provider_request_body .get("model") .and_then(Value::as_str) @@ -318,6 +326,12 @@ pub(crate) fn apply_provider_request_routing_policy_to_decision_with_websocket_m } .map_err(|_| invalid_routing_provider_contract())?; } + if native_memories { + crate::ai_serving::transport::enforce_same_format_provider_api_operation_body_policy( + &mut provider_request_body, + Some(crate::ai_serving::ApiOperation::OpenAiMemoriesSummarize), + ); + } let provider_model = provider_request_body .get("model") .and_then(Value::as_str) @@ -339,7 +353,11 @@ pub(crate) fn apply_provider_request_routing_policy_to_decision_with_websocket_m provider_type.as_str(), provider_api_format.as_str(), ); - crate::ai_serving::apply_codex_openai_responses_lite_header_for_request_body_with_capabilities( + if native_memories { + provider_request_headers + .retain(|name, _| !name.eq_ignore_ascii_case(CODEX_RESPONSES_LITE_HEADER)); + } else { + crate::ai_serving::apply_codex_openai_responses_lite_header_for_request_body_with_capabilities( &mut provider_request_headers, Some(&provider_request_body), provider_type.as_str(), @@ -348,6 +366,7 @@ pub(crate) fn apply_provider_request_routing_policy_to_decision_with_websocket_m input.requested_model.as_str(), model_capabilities.as_ref(), ); + } crate::ai_serving::apply_codex_openai_compact_terminal_headers( &mut provider_request_headers, provider_type.as_str(), @@ -382,6 +401,17 @@ fn apply_provider_outbound_request_policies_to_decision( let Some(context) = input.provider_outbound_context.as_ref() else { return; }; + let native_context; + let context = if decision.decision_kind.as_deref() + == Some(aether_ai_formats::contracts::OPENAI_MEMORIES_SYNC_PLAN_KIND) + { + native_context = context + .clone() + .with_api_operation(crate::ai_serving::ApiOperation::OpenAiMemoriesSummarize); + &native_context + } else { + context + }; let results = crate::ai_serving::transport::apply_provider_outbound_request_policies( transport, provider_api_format, diff --git a/apps/aether-gateway/src/ai_serving/planner/passthrough/provider/family/request.rs b/apps/aether-gateway/src/ai_serving/planner/passthrough/provider/family/request.rs index 4bd50da98..b2a5d3c60 100644 --- a/apps/aether-gateway/src/ai_serving/planner/passthrough/provider/family/request.rs +++ b/apps/aether-gateway/src/ai_serving/planner/passthrough/provider/family/request.rs @@ -255,7 +255,9 @@ pub(crate) async fn resolve_local_same_format_provider_candidate_payload_parts( // re-enforce stream-field policy afterward. // Kiro behavior classification already hard-requires upstream streaming, // and the Kiro envelope does not use a top-level body stream field. - if prepared.kiro_auth.is_none() { + if prepared.kiro_auth.is_none() + && spec.operation != Some(crate::ai_serving::ApiOperation::OpenAiMemoriesSummarize) + { enforce_provider_body_stream_policy( &mut base_provider_request_body, prepared.provider_api_format.as_str(), @@ -275,7 +277,8 @@ pub(crate) async fn resolve_local_same_format_provider_candidate_payload_parts( prepared.mapped_model.as_str(), source_model, ); - if let Err(violation) = + if spec.operation != Some(crate::ai_serving::ApiOperation::OpenAiMemoriesSummarize) { + if let Err(violation) = crate::ai_serving::finalize_openai_provider_request_with_codex_model_capabilities_and_reasoning_replay_policy( &mut base_provider_request_body, crate::ai_serving::OpenAiProviderRequestFinalization { @@ -313,6 +316,7 @@ pub(crate) async fn resolve_local_same_format_provider_candidate_payload_parts( .await; return Ok(None); } + } // Same-format requests skip `apply_transport_request_body_semantics`, so the opt-in // Claude Code body mimicry has to be applied here as well. @@ -597,6 +601,14 @@ pub(crate) async fn resolve_local_same_format_provider_candidate_payload_parts( source_model, codex_model_capabilities.as_ref(), ); + if spec.operation == Some(crate::ai_serving::ApiOperation::OpenAiMemoriesSummarize) { + provider_request_headers.retain(|name, _| { + !name.eq_ignore_ascii_case( + aether_ai_formats::formats::openai::responses::codex::CODEX_RESPONSES_LITE_HEADER, + ) + }); + provider_request_headers.insert("accept".to_string(), "application/json".to_string()); + } crate::ai_serving::transport::xai::insert_cli_identity_headers_if_needed( transport.as_ref(), prepared.provider_api_format.as_str(), diff --git a/apps/aether-gateway/src/ai_serving/planner/passthrough/provider/request/body.rs b/apps/aether-gateway/src/ai_serving/planner/passthrough/provider/request/body.rs index b14fa0438..2e48c9ca5 100644 --- a/apps/aether-gateway/src/ai_serving/planner/passthrough/provider/request/body.rs +++ b/apps/aether-gateway/src/ai_serving/planner/passthrough/provider/request/body.rs @@ -3,7 +3,7 @@ use serde_json::Value; use super::super::LocalSameFormatProviderSpec; use crate::ai_serving::transport::{ build_same_format_provider_request_body as build_same_format_provider_request_body_impl, - build_same_format_provider_request_body_with_compatibility_report_and_reasoning_replay_policy as build_same_format_provider_request_body_with_compatibility_report_impl, + build_same_format_provider_request_body_for_operation as build_same_format_provider_request_body_with_compatibility_report_impl, SameFormatProviderFamily, SameFormatProviderRequestBodyInput, SameFormatProviderRequestBodyOutput, }; @@ -69,6 +69,7 @@ pub(crate) fn build_same_format_provider_request_body_with_compatibility_report( enable_model_directives, }, reasoning_replay_policy, + spec.operation, ) } diff --git a/apps/aether-gateway/src/ai_serving/transport.rs b/apps/aether-gateway/src/ai_serving/transport.rs index 8c81cb17d..627c3b739 100644 --- a/apps/aether-gateway/src/ai_serving/transport.rs +++ b/apps/aether-gateway/src/ai_serving/transport.rs @@ -78,7 +78,7 @@ pub(crate) use aether_provider_transport::{ build_local_openai_chat_upstream_url, build_local_openai_responses_upstream_url, build_openai_image_headers, build_openai_image_upstream_url, build_passthrough_headers, build_request_trace_proxy_value, build_same_format_provider_headers, - build_same_format_provider_request_body, + build_same_format_provider_request_body, build_same_format_provider_request_body_for_operation, build_same_format_provider_request_body_with_compatibility_report, build_same_format_provider_request_body_with_compatibility_report_and_reasoning_replay_policy, build_same_format_provider_upstream_url, build_standard_plan_fallback_headers, diff --git a/apps/aether-gateway/src/constants.rs b/apps/aether-gateway/src/constants.rs index 9a2bcbe14..e9ebd5f1c 100644 --- a/apps/aether-gateway/src/constants.rs +++ b/apps/aether-gateway/src/constants.rs @@ -126,6 +126,7 @@ pub(crate) const RUST_FRONTDOOR_OWNED_ROUTE_PATTERNS: &[&str] = &[ "/v1/messages/count_tokens", "/v1/responses", "/v1/responses/compact", + "/v1/memories/trace_summarize", "/v1/realtime", "/v1/realtime/calls", "/v1/live", diff --git a/apps/aether-gateway/src/control/route/ai.rs b/apps/aether-gateway/src/control/route/ai.rs index 856f62543..960620a89 100644 --- a/apps/aether-gateway/src/control/route/ai.rs +++ b/apps/aether-gateway/src/control/route/ai.rs @@ -88,6 +88,11 @@ pub(super) fn classify_ai_public_route( true, )) } + } else if method == http::Method::POST && normalized_path == "/v1/memories/trace_summarize" { + Some( + classified("ai_public", "openai", "memories", "openai:responses", true) + .with_api_operation(ApiOperation::OpenAiMemoriesSummarize), + ) } else if method == http::Method::POST && normalized_path == "/v1/alpha/search" { Some(classified( "ai_public", diff --git a/apps/aether-gateway/src/execution_runtime/fallback.rs b/apps/aether-gateway/src/execution_runtime/fallback.rs index 86d178abf..7907ded6a 100644 --- a/apps/aether-gateway/src/execution_runtime/fallback.rs +++ b/apps/aether-gateway/src/execution_runtime/fallback.rs @@ -191,6 +191,7 @@ pub(crate) fn resolve_core_sync_error_finalize_report_kind( let report_kind = match plan_kind { "openai_chat_sync" => "openai_chat_sync_finalize", "openai_responses_sync" => "openai_responses_sync_finalize", + "openai_memories_sync" => "openai_memories_sync_finalize", "openai_responses_compact_sync" => "openai_responses_compact_sync_finalize", "claude_chat_sync" => "claude_chat_sync_finalize", "gemini_chat_sync" => "gemini_chat_sync_finalize", @@ -576,6 +577,14 @@ mod tests { error: None, }; + assert_eq!( + resolve_core_sync_error_finalize_report_kind( + "openai_memories_sync", + &result, + Some(&serde_json::json!({"error":{"message":"synthetic"}})) + ), + Some("openai_memories_sync_finalize".to_string()) + ); for body_json in [ serde_json::json!({"status": "failed", "error": null}), serde_json::json!({"type": "error"}), diff --git a/apps/aether-gateway/src/frontdoor_loop_guard.rs b/apps/aether-gateway/src/frontdoor_loop_guard.rs index b3824be3d..852693229 100644 --- a/apps/aether-gateway/src/frontdoor_loop_guard.rs +++ b/apps/aether-gateway/src/frontdoor_loop_guard.rs @@ -45,6 +45,7 @@ pub(crate) fn frontdoor_self_loop_public_ai_path(path: &str) -> bool { | "/v1/rerank" | "/v1/responses" | "/v1/responses/compact" + | "/v1/memories/trace_summarize" | "/v1/realtime" | "/v1/realtime/calls" | "/v1/live" diff --git a/apps/aether-gateway/src/handlers/proxy/websocket/responses/adapter.rs b/apps/aether-gateway/src/handlers/proxy/websocket/responses/adapter.rs index 3b3d6a2d8..209189eac 100644 --- a/apps/aether-gateway/src/handlers/proxy/websocket/responses/adapter.rs +++ b/apps/aether-gateway/src/handlers/proxy/websocket/responses/adapter.rs @@ -2,6 +2,7 @@ use async_trait::async_trait; use serde_json::Value; +use std::borrow::Cow; use super::adapters::CODEX_RESPONSES_WEBSOCKET_ADAPTER; use crate::ai_serving::AiExecutionDecision; @@ -59,7 +60,7 @@ pub(super) enum ResponsesWebSocketRelayDirective<'a> { ForwardOriginal, /// The provider frame was a private batch envelope. Forward each retained /// event in document order by serializing the complete borrowed value. - ForwardEvents(Vec<&'a Value>), + ForwardEvents(Vec>), /// The entire frame was an explicitly recognized provider-private /// envelope and therefore has no public event to relay. SuppressProviderPrivate, @@ -88,8 +89,8 @@ pub(super) trait ResponsesWebSocketProtocolAdapter: Send + Sync { /// observably ambiguous to the client. fn rebind_safety_for_upstream_event(&self, event: &Value) -> ResponsesWebSocketRebindSafety; - /// Selects the public relay shape without projecting a provider event - /// through an Aether-owned field or event-type allowlist. + /// 公开事件保持完整;提供商私有元数据仅投影到客户端使用的公开字段, + /// 不公开账户信息。 fn relay_directive_for_upstream_event<'a>( &self, _event: &'a Value, diff --git a/apps/aether-gateway/src/handlers/proxy/websocket/responses/adapters/codex.rs b/apps/aether-gateway/src/handlers/proxy/websocket/responses/adapters/codex.rs index d1ebad819..cbededa4a 100644 --- a/apps/aether-gateway/src/handlers/proxy/websocket/responses/adapters/codex.rs +++ b/apps/aether-gateway/src/handlers/proxy/websocket/responses/adapters/codex.rs @@ -2,6 +2,7 @@ use async_trait::async_trait; use serde_json::{Map, Value}; +use std::borrow::Cow; use super::super::adapter::{ is_standard_responses_event, ResponsesWebSocketAdapterObservation, @@ -222,7 +223,7 @@ fn codex_relay_directive(event: &Value) -> ResponsesWebSocketRelayDirective<'_> Some(Value::Array(chunks)) if is_explicit_codex_batch_envelope(event) => { let public_events = chunks .iter() - .filter(|chunk| !is_codex_private_leaf_event(chunk)) + .filter_map(codex_public_event) .collect::>(); if public_events.is_empty() { ResponsesWebSocketRelayDirective::SuppressProviderPrivate @@ -233,13 +234,51 @@ fn codex_relay_directive(event: &Value) -> ResponsesWebSocketRelayDirective<'_> // A malformed or future shape is not proven private. Preserve it // opaquely rather than guessing at a provider schema. Some(_) => ResponsesWebSocketRelayDirective::ForwardOriginal, - None if is_codex_private_leaf_event(event) => { - ResponsesWebSocketRelayDirective::SuppressProviderPrivate - } + None if is_codex_private_leaf_event(event) => match codex_public_event(event) { + Some(projected) => ResponsesWebSocketRelayDirective::ForwardEvents(vec![projected]), + None => ResponsesWebSocketRelayDirective::SuppressProviderPrivate, + }, None => ResponsesWebSocketRelayDirective::ForwardOriginal, } } +fn codex_public_event(event: &Value) -> Option> { + if !is_codex_private_leaf_event(event) { + return Some(Cow::Borrowed(event)); + } + if event.get("type").and_then(Value::as_str) != Some("codex.response.metadata") { + // 配额属于选中的上游账户,不能代表网关用户配额;仅交给 + // 账户级熔断和持久化路径处理。 + return None; + } + let headers = event.get("headers")?.as_object()?; + let public_headers: Map = headers + .iter() + .filter_map(|(name, value)| { + let name = name.to_ascii_lowercase(); + if matches!( + name.as_str(), + "x-models-etag" + | "x-codex-turn-state" + | "openai-model" + | "x-codex-safety-buffering-enabled" + | "x-codex-safety-buffering-faster-model" + ) && value.as_str().is_some() + { + Some((name, value.clone())) + } else { + None + } + }) + .collect(); + if public_headers.is_empty() { + return None; + } + Some(Cow::Owned(serde_json::json!({ + "type": "codex.response.metadata", "headers": public_headers + }))) +} + /// Recognizes only Codex's private batch container. A type-less object must /// contain exactly `chunks`; unknown siblings could be future public protocol /// data and therefore force opaque forwarding. A named Codex private root may @@ -549,6 +588,55 @@ mod tests { } } + #[test] + fn codex_metadata_relays_cli_catalog_and_turn_state_without_account_fields() { + let event = json!({ + "type": "codex.response.metadata", + "headers": { + "X-Models-Etag": "catalog-v2", + "x-codex-turn-state": "synthetic-turn-state", + "openai-model": "gpt-6.1-sol", + "x-codex-safety-buffering-enabled": "true", + "x-codex-safety-buffering-faster-model": "gpt-6-luna", + "chatgpt-account-id": "synthetic-private-account", + "set-cookie": "synthetic-private-cookie", + "authorization": "synthetic-private-token" + }, + "account_hint": "private", + "metadata": {"user_id": "private"} + }); + let ResponsesWebSocketRelayDirective::ForwardEvents(events) = + CodexResponsesWebSocketAdapter.relay_directive_for_upstream_event(&event) + else { + panic!("CLI metadata must reach the client"); + }; + assert_eq!(events.len(), 1); + assert_eq!( + *events[0], + json!({ + "type": "codex.response.metadata", + "headers": {"x-models-etag": "catalog-v2", "x-codex-turn-state": "synthetic-turn-state", "openai-model": "gpt-6.1-sol", "x-codex-safety-buffering-enabled": "true", "x-codex-safety-buffering-faster-model": "gpt-6-luna"} + }) + ); + } + + #[test] + fn codex_batch_retains_safe_metadata_in_public_event_order() { + let event = json!({"chunks": [ + {"type": "codex.response.metadata", "headers": {"x-models-etag": "catalog-v3"}}, + {"type": "codex.rate_limits", "plan_type": "private-plan"}, + {"type": "response.created", "response": {"id": "resp_synthetic"}, "future": 42} + ]}); + let ResponsesWebSocketRelayDirective::ForwardEvents(events) = + CodexResponsesWebSocketAdapter.relay_directive_for_upstream_event(&event) + else { + panic!("batch must retain public events"); + }; + assert_eq!(events.len(), 2); + assert_eq!(events[0]["headers"]["x-models-etag"], "catalog-v3"); + assert_eq!(events[1]["future"], 42); + } + #[test] fn mixed_codex_batch_forwards_whole_non_private_events_in_order() { let adapter = CodexResponsesWebSocketAdapter; diff --git a/apps/aether-gateway/src/handlers/proxy/websocket/responses/connection.rs b/apps/aether-gateway/src/handlers/proxy/websocket/responses/connection.rs index 23774c5f5..d09e268a2 100644 --- a/apps/aether-gateway/src/handlers/proxy/websocket/responses/connection.rs +++ b/apps/aether-gateway/src/handlers/proxy/websocket/responses/connection.rs @@ -610,6 +610,7 @@ pub(super) async fn relay_bound_connection( } Some(ResponsesWebSocketRelayDirective::ForwardEvents(events)) => { for event in events { + let event = event.as_ref(); let text = match bound .redaction_restorer .restore_provider_frame_text(event) diff --git a/apps/aether-gateway/src/tests/ai_execute/sync/search.rs b/apps/aether-gateway/src/tests/ai_execute/sync/search.rs index 5b2ec2067..c8b6d1947 100644 --- a/apps/aether-gateway/src/tests/ai_execute/sync/search.rs +++ b/apps/aether-gateway/src/tests/ai_execute/sync/search.rs @@ -45,6 +45,153 @@ where } } +fn hash_api_key(value: &str) -> String { + let mut hasher = Sha256::new(); + hasher.update(value.as_bytes()); + format!("{:x}", hasher.finalize()) +} + +fn auth_snapshot() -> StoredAuthApiKeySnapshot { + StoredAuthApiKeySnapshot::new( + "user-search-1".to_string(), + "alice".to_string(), + Some("alice@example.com".to_string()), + "user".to_string(), + "local".to_string(), + true, + false, + Some(json!(["openai", "codex"])), + Some(json!(["openai:responses"])), + None, + "api-key-search-1".to_string(), + Some("search-client".to_string()), + true, + false, + false, + Some(60), + Some(5), + Some(4_102_444_800_i64), + Some(json!(["openai", "codex"])), + Some(json!(["openai:responses"])), + None, + ) + .expect("auth snapshot should build") +} + +fn candidate_row(api_format: &str) -> StoredMinimalCandidateSelectionRow { + StoredMinimalCandidateSelectionRow { + provider_id: "provider-codex-search-1".to_string(), + provider_name: "codex".to_string(), + provider_type: "codex".to_string(), + provider_priority: 10, + provider_is_active: true, + endpoint_id: "endpoint-codex-search-1".to_string(), + endpoint_api_format: api_format.to_string(), + endpoint_api_family: Some("openai".to_string()), + endpoint_kind: Some(api_format.split_once(':').expect("format").1.to_string()), + endpoint_is_active: true, + key_id: "key-codex-search-1".to_string(), + key_name: "oauth".to_string(), + key_auth_type: "oauth".to_string(), + key_is_active: true, + key_api_formats: Some(vec!["openai:responses".to_string()]), + key_allowed_models: None, + key_capabilities: None, + key_internal_priority: 5, + key_global_priority_by_format: Some(json!({"openai:search": 1})), + model_id: "model-codex-search-1".to_string(), + global_model_id: "global-model-codex-search-1".to_string(), + global_model_name: "gpt-5.6-sol".to_string(), + global_model_mappings: None, + global_model_supports_streaming: Some(false), + model_provider_model_name: "gpt-5.6-sol".to_string(), + model_provider_model_mappings: Some(vec![StoredProviderModelMapping { + name: "gpt-5.6-sol".to_string(), + priority: 1, + api_formats: Some(vec!["openai:responses".to_string()]), + endpoint_ids: None, + operations: None, + }]), + model_supports_streaming: Some(false), + model_is_active: true, + model_is_available: true, + } +} + +fn provider() -> StoredProviderCatalogProvider { + StoredProviderCatalogProvider::new( + "provider-codex-search-1".to_string(), + "codex".to_string(), + Some("https://chatgpt.com".to_string()), + "codex".to_string(), + ) + .expect("provider should build") + .with_transport_fields( + true, + false, + false, + None, + Some(1), + None, + Some(900.0), + None, + None, + ) +} + +fn endpoint(api_format: &str) -> StoredProviderCatalogEndpoint { + StoredProviderCatalogEndpoint::new( + "endpoint-codex-search-1".to_string(), + "provider-codex-search-1".to_string(), + api_format.to_string(), + Some("openai".to_string()), + Some(api_format.split_once(':').expect("format").1.to_string()), + true, + ) + .expect("endpoint should build") + .with_transport_fields( + "https://chatgpt.com/backend-api/codex".to_string(), + None, + None, + Some(1), + None, + None, + None, + None, + ) + .expect("endpoint transport should build") +} + +fn key() -> StoredProviderCatalogKey { + let auth_config = encrypt_python_fernet_plaintext( + DEVELOPMENT_ENCRYPTION_KEY, + r#"{"provider_type":"codex","account_id":"account-search-1","is_fedramp":true}"#, + ) + .expect("auth config should encrypt"); + StoredProviderCatalogKey::new( + "key-codex-search-1".to_string(), + "provider-codex-search-1".to_string(), + "oauth".to_string(), + "oauth".to_string(), + None, + true, + ) + .expect("key should build") + .with_transport_fields( + Some(json!(["openai:responses"])), + encrypt_python_fernet_plaintext(DEVELOPMENT_ENCRYPTION_KEY, "codex-search-access-token") + .expect("access token should encrypt"), + Some(auth_config), + None, + Some(json!({"openai:search": 1})), + None, + Some(4_102_444_800), + None, + None, + ) + .expect("key transport should build") +} + #[test] fn gateway_executes_codex_search_with_responses_permission_and_search_contract() { run_search_sync_test( @@ -54,156 +201,6 @@ fn gateway_executes_codex_search_with_responses_permission_and_search_contract() } async fn gateway_executes_codex_search_with_responses_permission_and_search_contract_impl() { - fn hash_api_key(value: &str) -> String { - let mut hasher = Sha256::new(); - hasher.update(value.as_bytes()); - format!("{:x}", hasher.finalize()) - } - - fn auth_snapshot() -> StoredAuthApiKeySnapshot { - StoredAuthApiKeySnapshot::new( - "user-search-1".to_string(), - "alice".to_string(), - Some("alice@example.com".to_string()), - "user".to_string(), - "local".to_string(), - true, - false, - Some(json!(["openai", "codex"])), - Some(json!(["openai:responses"])), - None, - "api-key-search-1".to_string(), - Some("search-client".to_string()), - true, - false, - false, - Some(60), - Some(5), - Some(4_102_444_800_i64), - Some(json!(["openai", "codex"])), - Some(json!(["openai:responses"])), - None, - ) - .expect("auth snapshot should build") - } - - fn candidate_row() -> StoredMinimalCandidateSelectionRow { - StoredMinimalCandidateSelectionRow { - provider_id: "provider-codex-search-1".to_string(), - provider_name: "codex".to_string(), - provider_type: "codex".to_string(), - provider_priority: 10, - provider_is_active: true, - endpoint_id: "endpoint-codex-search-1".to_string(), - endpoint_api_format: "openai:search".to_string(), - endpoint_api_family: Some("openai".to_string()), - endpoint_kind: Some("search".to_string()), - endpoint_is_active: true, - key_id: "key-codex-search-1".to_string(), - key_name: "oauth".to_string(), - key_auth_type: "oauth".to_string(), - key_is_active: true, - key_api_formats: Some(vec!["openai:responses".to_string()]), - key_allowed_models: None, - key_capabilities: None, - key_internal_priority: 5, - key_global_priority_by_format: Some(json!({"openai:search": 1})), - model_id: "model-codex-search-1".to_string(), - global_model_id: "global-model-codex-search-1".to_string(), - global_model_name: "gpt-5.6-sol".to_string(), - global_model_mappings: None, - global_model_supports_streaming: Some(false), - model_provider_model_name: "gpt-5.6-sol".to_string(), - model_provider_model_mappings: Some(vec![StoredProviderModelMapping { - name: "gpt-5.6-sol".to_string(), - priority: 1, - api_formats: Some(vec!["openai:responses".to_string()]), - endpoint_ids: None, - operations: None, - }]), - model_supports_streaming: Some(false), - model_is_active: true, - model_is_available: true, - } - } - - fn provider() -> StoredProviderCatalogProvider { - StoredProviderCatalogProvider::new( - "provider-codex-search-1".to_string(), - "codex".to_string(), - Some("https://chatgpt.com".to_string()), - "codex".to_string(), - ) - .expect("provider should build") - .with_transport_fields( - true, - false, - false, - None, - Some(1), - None, - Some(900.0), - None, - None, - ) - } - - fn endpoint() -> StoredProviderCatalogEndpoint { - StoredProviderCatalogEndpoint::new( - "endpoint-codex-search-1".to_string(), - "provider-codex-search-1".to_string(), - "openai:search".to_string(), - Some("openai".to_string()), - Some("search".to_string()), - true, - ) - .expect("endpoint should build") - .with_transport_fields( - "https://chatgpt.com/backend-api/codex".to_string(), - None, - None, - Some(1), - None, - None, - None, - None, - ) - .expect("endpoint transport should build") - } - - fn key() -> StoredProviderCatalogKey { - let auth_config = encrypt_python_fernet_plaintext( - DEVELOPMENT_ENCRYPTION_KEY, - r#"{"provider_type":"codex","account_id":"account-search-1","is_fedramp":true}"#, - ) - .expect("auth config should encrypt"); - StoredProviderCatalogKey::new( - "key-codex-search-1".to_string(), - "provider-codex-search-1".to_string(), - "oauth".to_string(), - "oauth".to_string(), - None, - true, - ) - .expect("key should build") - .with_transport_fields( - Some(json!(["openai:responses"])), - encrypt_python_fernet_plaintext( - DEVELOPMENT_ENCRYPTION_KEY, - "codex-search-access-token", - ) - .expect("access token should encrypt"), - Some(auth_config), - None, - Some(json!({"openai:search": 1})), - None, - Some(4_102_444_800), - None, - None, - ) - .expect("key transport should build") - } - let seen_plans = Arc::new(Mutex::new(Vec::::new())); let seen_plans_clone = Arc::clone(&seen_plans); let execution_runtime = Router::new().route( @@ -316,7 +313,7 @@ async fn gateway_executes_codex_search_with_responses_permission_and_search_cont auth_snapshot(), )])); let candidate_repository = Arc::new(InMemoryMinimalCandidateSelectionReadRepository::seed({ - let primary = candidate_row(); + let primary = candidate_row("openai:search"); let mut backup = primary.clone(); backup.provider_id = "provider-codex-search-2".to_string(); backup.provider_name = "codex-backup".to_string(); @@ -338,7 +335,7 @@ async fn gateway_executes_codex_search_with_responses_permission_and_search_cont vec![primary, backup] }, { - let primary = endpoint(); + let primary = endpoint("openai:search"); let mut backup = primary.clone(); backup.id = "endpoint-codex-search-2".to_string(); backup.provider_id = "provider-codex-search-2".to_string(); @@ -598,3 +595,118 @@ async fn gateway_executes_codex_search_with_responses_permission_and_search_cont gateway_handle.abort(); execution_runtime_handle.abort(); } + +#[test] +fn gateway_executes_codex_memories_with_responses_permission_and_native_json() { + run_search_sync_test( + "gateway_executes_codex_memories_with_responses_permission_and_native_json", + || async { + let response_body = json!({"output":[{"trace_summary":"synthetic trace", "memory_summary":"synthetic memory"}],"future_response_field":{"enabled":true}}); + let seen = Arc::new(Mutex::new(None)); + let captured = Arc::clone(&seen); + let expected = response_body.clone(); + let runtime = Router::new().route("/v1/execute/sync", any(move |request: Request| { + let captured = Arc::clone(&captured); + let response_body = expected.clone(); + async move { + let bytes = to_bytes(request.into_body(), usize::MAX).await.expect("read plan"); + let plan: serde_json::Value = serde_json::from_slice(&bytes).expect("parse plan"); + let request_id = plan["request_id"].clone(); + *captured.lock().expect("capture lock") = Some(plan); + let (status_code, response_body) = if request_id == json!("trace-memory-error") { + (400, json!({"error":{"type":"invalid_request_error","message":"synthetic invalid trace","code":"invalid_trace"},"future_error_field":{"enabled":true}})) + } else { (200, response_body) }; + Json(json!({"request_id":request_id,"status_code":status_code,"headers":{"content-type":"application/json"},"body":{"json_body":response_body},"telemetry":{"elapsed_ms":1}})) + } + })); + let client_key = "sk-synthetic-memory-client"; + let auth = Arc::new(InMemoryAuthApiKeySnapshotRepository::seed(vec![( + Some(hash_api_key(client_key)), + auth_snapshot(), + )])); + let candidates = Arc::new(InMemoryMinimalCandidateSelectionReadRepository::seed(vec![ + candidate_row("openai:responses"), + ])); + let mut memory_provider = provider(); + memory_provider.config = + Some(json!({"codex":{"fingerprint_convergence_enabled":true}})); + let catalog = Arc::new(InMemoryProviderCatalogReadRepository::seed( + vec![memory_provider], + vec![endpoint("openai:responses")], + vec![key()], + )); + let request_candidates = Arc::new(InMemoryRequestCandidateRepository::default()); + let data = crate::data::GatewayDataState::with_auth_candidate_selection_provider_catalog_and_request_candidate_repository_for_tests(auth,candidates,catalog,Arc::clone(&request_candidates),DEVELOPMENT_ENCRYPTION_KEY) + .with_system_config_values_for_tests([(crate::system_features::ENABLE_MODEL_DIRECTIVES_CONFIG_KEY.to_string(),json!(true))]); + let (runtime_url, runtime_handle) = start_server(runtime).await; + let state = build_state_with_execution_runtime_override(runtime_url) + .with_data_state_for_tests(data); + let (url, gateway_handle) = start_server(build_router_with_state(state)).await; + let input = json!({"model":"gpt-5.6-sol-max","traces":[{"id":"synthetic-trace","metadata":{"source_path":"/synthetic/trace.json"},"items":[{"type":"message","role":"user","content":[]}]}],"reasoning":{"effort":"low"},"future_request_field":{"enabled":true}}); + let response = reqwest::Client::new() + .post(format!("{url}/v1/memories/trace_summarize")) + .header(http::header::AUTHORIZATION, format!("Bearer {client_key}")) + .header(TRACE_ID_HEADER, "trace-memory-1") + .json(&input) + .send() + .await + .expect("send request"); + let status = response.status(); + let body: serde_json::Value = response.json().await.expect("read response"); + assert_eq!(status, StatusCode::OK, "{body}"); + assert_eq!(body, response_body); + let plan = seen + .lock() + .expect("capture lock") + .clone() + .expect("captured plan"); + assert_eq!( + plan["url"], + "https://chatgpt.com/backend-api/codex/memories/trace_summarize" + ); + assert_eq!(plan["stream"], false); + assert_eq!(plan["client_api_format"], "openai:responses"); + assert_eq!(plan["provider_api_format"], "openai:responses"); + assert_eq!(plan["headers"]["originator"], "codex_cli_rs"); + assert_eq!( + plan["headers"]["authorization"], + "Bearer codex-search-access-token" + ); + assert_eq!(plan["headers"]["accept"], "application/json"); + assert!(plan["headers"] + .get("x-openai-internal-codex-responses-lite") + .is_none()); + let mut expected_input = input; + expected_input["model"] = json!("gpt-5.6-sol"); + expected_input["reasoning"]["effort"] = json!("max"); + assert_eq!(plan["body"]["json_body"], expected_input); + let stored = request_candidates + .list_by_request_id("trace-memory-1") + .await + .expect("read candidates"); + assert_eq!(stored.len(), 1); + assert_eq!(stored[0].status, RequestCandidateStatus::Success); + let error = reqwest::Client::new() + .post(format!("{url}/v1/memories/trace_summarize")) + .header(http::header::AUTHORIZATION, format!("Bearer {client_key}")) + .header(TRACE_ID_HEADER, "trace-memory-error") + .json(&expected_input) + .send() + .await + .expect("error response"); + assert_eq!(error.status(), StatusCode::BAD_REQUEST); + assert_eq!( + error.json::().await.expect("error JSON"), + json!({"error":{"type":"invalid_request_error","message":"synthetic invalid trace","code":"invalid_trace"},"future_error_field":{"enabled":true}}) + ); + let error_candidates = request_candidates + .list_by_request_id("trace-memory-error") + .await + .expect("error candidate"); + assert_eq!(error_candidates.len(), 1); + assert_eq!(error_candidates[0].status, RequestCandidateStatus::Failed); + gateway_handle.abort(); + runtime_handle.abort(); + }, + ); +} diff --git a/crates/aether-ai/formats/Cargo.toml b/crates/aether-ai/formats/Cargo.toml index 4e3f2154b..4d6ea21f7 100644 --- a/crates/aether-ai/formats/Cargo.toml +++ b/crates/aether-ai/formats/Cargo.toml @@ -10,6 +10,7 @@ description = "Pure AI API format, surface, planning, and finalize logic for Aet aether-contracts.workspace = true base64.workspace = true http.workspace = true +os_info = { version = "=3.12.0", default-features = false } regex.workspace = true serde.workspace = true serde_json.workspace = true diff --git a/crates/aether-ai/formats/src/codex_profile.rs b/crates/aether-ai/formats/src/codex_profile.rs index 1338a9f8c..5d5c4eb89 100644 --- a/crates/aether-ai/formats/src/codex_profile.rs +++ b/crates/aether-ai/formats/src/codex_profile.rs @@ -1,4 +1,6 @@ -use std::sync::{OnceLock, RwLock}; +use std::sync::{LazyLock, OnceLock, RwLock}; + +static OS_INFO: LazyLock = LazyLock::new(os_info::get); /// 当前支持的 Codex 客户端类型。 #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -32,7 +34,19 @@ impl CodexClientProfile { Ok(Self { client_kind: CodexClientKind::Cli, codex_version: version.to_owned(), - user_agent: format!("{}/{}", originator, version), + // 按 CLI 格式使用当前网关的公开平台信息。无客户端终端时使用官方 + // unknown 标识,不复制调用方终端后缀、安装标识或个人身份。 + user_agent: format!( + "{}/{} ({} {}; {}) unknown", + originator, + version, + OS_INFO.os_type(), + OS_INFO.version(), + OS_INFO.architecture().unwrap_or(std::env::consts::ARCH), + ) + .chars() + .map(|ch| if matches!(ch, ' '..='~') { ch } else { '_' }) + .collect(), originator, }) } @@ -40,8 +54,8 @@ impl CodexClientProfile { impl Default for CodexClientProfile { fn default() -> Self { - // 远程发布检查不可用时仍保持现有线上行为,避免启动或请求被版本服务拖住。 - Self::cli("0.153.4").expect("built-in Codex CLI profile must be valid") + // 最新已核验稳定版本;后台版本刷新继续作为版本真源。 + Self::cli("0.159.3").expect("built-in Codex CLI profile must be valid") } } @@ -97,7 +111,9 @@ mod tests { let profile = CodexClientProfile::cli("0.200.1").expect("valid version"); assert_eq!(profile.client_kind, CodexClientKind::Cli); assert_eq!(profile.originator, "codex_cli_rs"); - assert_eq!(profile.user_agent, "codex_cli_rs/0.200.1"); + assert!(profile.user_agent.starts_with("codex_cli_rs/0.200.1 (")); + assert!(profile.user_agent.ends_with(") unknown")); + assert!(profile.user_agent.contains(std::env::consts::ARCH)); } #[test] diff --git a/crates/aether-ai/formats/src/contracts/mod.rs b/crates/aether-ai/formats/src/contracts/mod.rs index 900c0afd9..8bee53926 100644 --- a/crates/aether-ai/formats/src/contracts/mod.rs +++ b/crates/aether-ai/formats/src/contracts/mod.rs @@ -22,7 +22,7 @@ pub use plan_kinds::{ GEMINI_INTERACTIONS_SYNC_PLAN_KIND, GEMINI_VIDEO_CANCEL_SYNC_PLAN_KIND, GEMINI_VIDEO_CREATE_SYNC_PLAN_KIND, OPENAI_CHAT_STREAM_PLAN_KIND, OPENAI_CHAT_SYNC_PLAN_KIND, OPENAI_EMBEDDING_SYNC_PLAN_KIND, OPENAI_IMAGE_STREAM_PLAN_KIND, OPENAI_IMAGE_SYNC_PLAN_KIND, - OPENAI_REALTIME_STREAM_PLAN_KIND, OPENAI_RERANK_SYNC_PLAN_KIND, + OPENAI_MEMORIES_SYNC_PLAN_KIND, OPENAI_REALTIME_STREAM_PLAN_KIND, OPENAI_RERANK_SYNC_PLAN_KIND, OPENAI_RESPONSES_COMPACT_STREAM_PLAN_KIND, OPENAI_RESPONSES_COMPACT_SYNC_PLAN_KIND, OPENAI_RESPONSES_STREAM_PLAN_KIND, OPENAI_RESPONSES_SYNC_PLAN_KIND, OPENAI_SEARCH_SYNC_PLAN_KIND, OPENAI_VIDEO_CANCEL_SYNC_PLAN_KIND, @@ -49,7 +49,8 @@ pub use report_kinds::{ OPENAI_EMBEDDING_SYNC_ERROR_REPORT_KIND, OPENAI_EMBEDDING_SYNC_FINALIZE_REPORT_KIND, OPENAI_EMBEDDING_SYNC_SUCCESS_REPORT_KIND, OPENAI_IMAGE_STREAM_SUCCESS_REPORT_KIND, OPENAI_IMAGE_SYNC_ERROR_REPORT_KIND, OPENAI_IMAGE_SYNC_FINALIZE_REPORT_KIND, - OPENAI_IMAGE_SYNC_SUCCESS_REPORT_KIND, OPENAI_RESPONSES_COMPACT_STREAM_SUCCESS_REPORT_KIND, + OPENAI_IMAGE_SYNC_SUCCESS_REPORT_KIND, OPENAI_MEMORIES_SYNC_SUCCESS_REPORT_KIND, + OPENAI_RESPONSES_COMPACT_STREAM_SUCCESS_REPORT_KIND, OPENAI_RESPONSES_COMPACT_SYNC_ERROR_REPORT_KIND, OPENAI_RESPONSES_COMPACT_SYNC_FINALIZE_REPORT_KIND, OPENAI_RESPONSES_COMPACT_SYNC_SUCCESS_REPORT_KIND, OPENAI_RESPONSES_STREAM_SUCCESS_REPORT_KIND, diff --git a/crates/aether-ai/formats/src/contracts/plan_kinds.rs b/crates/aether-ai/formats/src/contracts/plan_kinds.rs index 0da34706f..5bbaf89d9 100644 --- a/crates/aether-ai/formats/src/contracts/plan_kinds.rs +++ b/crates/aether-ai/formats/src/contracts/plan_kinds.rs @@ -5,6 +5,7 @@ pub const GEMINI_FILES_DELETE_PLAN_KIND: &str = "gemini_files_delete"; pub const GEMINI_FILES_DOWNLOAD_PLAN_KIND: &str = "gemini_files_download"; pub const OPENAI_IMAGE_STREAM_PLAN_KIND: &str = "openai_image_stream"; pub const OPENAI_IMAGE_SYNC_PLAN_KIND: &str = "openai_image_sync"; +pub const OPENAI_MEMORIES_SYNC_PLAN_KIND: &str = "openai_memories_sync"; pub const OPENAI_VIDEO_CONTENT_PLAN_KIND: &str = "openai_video_content"; pub const OPENAI_VIDEO_CANCEL_SYNC_PLAN_KIND: &str = "openai_video_cancel_sync"; pub const OPENAI_VIDEO_REMIX_SYNC_PLAN_KIND: &str = "openai_video_remix_sync"; diff --git a/crates/aether-ai/formats/src/contracts/report_kinds.rs b/crates/aether-ai/formats/src/contracts/report_kinds.rs index 80d23275a..5bf91c8bd 100644 --- a/crates/aether-ai/formats/src/contracts/report_kinds.rs +++ b/crates/aether-ai/formats/src/contracts/report_kinds.rs @@ -31,6 +31,8 @@ pub const OPENAI_RESPONSES_COMPACT_SYNC_SUCCESS_REPORT_KIND: &str = "openai_responses_compact_sync_success"; pub const OPENAI_EMBEDDING_SYNC_SUCCESS_REPORT_KIND: &str = "openai_embedding_sync_success"; pub const OPENAI_SEARCH_SYNC_SUCCESS_REPORT_KIND: &str = "openai_search_sync_success"; +pub const OPENAI_MEMORIES_SYNC_FINALIZE_REPORT_KIND: &str = "openai_memories_sync_finalize"; +pub const OPENAI_MEMORIES_SYNC_SUCCESS_REPORT_KIND: &str = "openai_memories_sync_success"; pub const GEMINI_EMBEDDING_SYNC_SUCCESS_REPORT_KIND: &str = "gemini_embedding_sync_success"; pub const OPENAI_IMAGE_SYNC_SUCCESS_REPORT_KIND: &str = "openai_image_sync_success"; pub const CLAUDE_CLI_SYNC_SUCCESS_REPORT_KIND: &str = "claude_cli_sync_success"; @@ -62,6 +64,9 @@ pub const GEMINI_CLI_SYNC_ERROR_REPORT_KIND: &str = "gemini_cli_sync_error"; pub fn implicit_sync_finalize_report_kind(plan_kind: &str) -> Option<&'static str> { match plan_kind { + super::plan_kinds::OPENAI_MEMORIES_SYNC_PLAN_KIND => { + Some(OPENAI_MEMORIES_SYNC_FINALIZE_REPORT_KIND) + } OPENAI_CHAT_SYNC_PLAN_KIND => Some(OPENAI_CHAT_SYNC_FINALIZE_REPORT_KIND), CLAUDE_CHAT_SYNC_PLAN_KIND => Some(CLAUDE_CHAT_SYNC_FINALIZE_REPORT_KIND), GEMINI_CHAT_SYNC_PLAN_KIND => Some(GEMINI_CHAT_SYNC_FINALIZE_REPORT_KIND), @@ -80,6 +85,7 @@ pub fn implicit_sync_finalize_report_kind(plan_kind: &str) -> Option<&'static st pub fn core_error_default_client_api_format(report_kind: &str) -> Option<&'static str> { match report_kind { + OPENAI_MEMORIES_SYNC_FINALIZE_REPORT_KIND => Some("openai:responses"), OPENAI_CHAT_SYNC_FINALIZE_REPORT_KIND => Some("openai:chat"), CLAUDE_CHAT_SYNC_FINALIZE_REPORT_KIND => Some("claude:messages"), GEMINI_CHAT_SYNC_FINALIZE_REPORT_KIND => Some("gemini:generate_content"), @@ -98,6 +104,7 @@ pub fn core_error_default_client_api_format(report_kind: &str) -> Option<&'stati pub fn core_error_background_report_kind(report_kind: &str) -> Option<&'static str> { match report_kind { + OPENAI_MEMORIES_SYNC_FINALIZE_REPORT_KIND => Some(OPENAI_RESPONSES_SYNC_ERROR_REPORT_KIND), OPENAI_CHAT_SYNC_FINALIZE_REPORT_KIND => Some(OPENAI_CHAT_SYNC_ERROR_REPORT_KIND), CLAUDE_CHAT_SYNC_FINALIZE_REPORT_KIND => Some(CLAUDE_CHAT_SYNC_ERROR_REPORT_KIND), GEMINI_CHAT_SYNC_FINALIZE_REPORT_KIND => Some(GEMINI_CHAT_SYNC_ERROR_REPORT_KIND), @@ -124,6 +131,7 @@ pub fn core_error_background_report_kind(report_kind: &str) -> Option<&'static s pub fn core_success_background_report_kind(report_kind: &str) -> Option<&'static str> { match report_kind { + OPENAI_MEMORIES_SYNC_FINALIZE_REPORT_KIND => Some(OPENAI_MEMORIES_SYNC_SUCCESS_REPORT_KIND), OPENAI_CHAT_SYNC_FINALIZE_REPORT_KIND => Some(OPENAI_CHAT_SYNC_SUCCESS_REPORT_KIND), CLAUDE_CHAT_SYNC_FINALIZE_REPORT_KIND => Some(CLAUDE_CHAT_SYNC_SUCCESS_REPORT_KIND), GEMINI_CHAT_SYNC_FINALIZE_REPORT_KIND => Some(GEMINI_CHAT_SYNC_SUCCESS_REPORT_KIND), diff --git a/crates/aether-ai/formats/src/contracts/request_dimensions.rs b/crates/aether-ai/formats/src/contracts/request_dimensions.rs index 2ac98aa3c..3ed0879c8 100644 --- a/crates/aether-ai/formats/src/contracts/request_dimensions.rs +++ b/crates/aether-ai/formats/src/contracts/request_dimensions.rs @@ -34,6 +34,7 @@ pub enum ApiOperation { ClaudeMessagesCreate, ClaudeCountTokens, OpenAiResponsesCompact, + OpenAiMemoriesSummarize, } impl ApiOperation { @@ -42,6 +43,7 @@ impl ApiOperation { Self::ClaudeMessagesCreate => "messages", Self::ClaudeCountTokens => "count_tokens", Self::OpenAiResponsesCompact => "compact", + Self::OpenAiMemoriesSummarize => "trace_summarize", } } } @@ -55,5 +57,9 @@ mod tests { assert_eq!(ClientSurface::ClaudeCode.as_str(), "claude_code"); assert_eq!(ApiOperation::ClaudeMessagesCreate.as_str(), "messages"); assert_eq!(ApiOperation::ClaudeCountTokens.as_str(), "count_tokens"); + assert_eq!( + ApiOperation::OpenAiMemoriesSummarize.as_str(), + "trace_summarize" + ); } } diff --git a/crates/aether-ai/formats/src/formats/openai/responses/codex.rs b/crates/aether-ai/formats/src/formats/openai/responses/codex.rs index 548a65894..b2f8b8b43 100644 --- a/crates/aether-ai/formats/src/formats/openai/responses/codex.rs +++ b/crates/aether-ai/formats/src/formats/openai/responses/codex.rs @@ -380,149 +380,22 @@ fn bundled_codex_model_card(spec: BundledCodexModelCardSpec<'_>) -> Value { }) } -fn bundled_gpt_5_6_codex_model_card( - model_id: &str, - display_name: &str, - description: &str, - default_reasoning_level: &str, - priority: u64, - multi_agent_version: &str, - supports_ultra: bool, -) -> Value { - let mut efforts = vec!["low", "medium", "high", "xhigh", "max"]; - if supports_ultra { - efforts.push("ultra"); - } - let mut card = bundled_codex_model_card(BundledCodexModelCardSpec { - model_id, - display_name, - description, - default_reasoning_level, - default_reasoning_summary: "none", - use_responses_lite: true, - efforts: &efforts, - default_verbosity: "low", - supports_priority_tier: true, - }); - let object = card - .as_object_mut() - .expect("bundled Codex model card must be an object"); - object.extend( - json!({ - "shell_type": "shell_command", - "supports_image_detail_original": true, - "supports_search_tool": true, - "input_modalities": ["text", "image"], - "context_window": 372_000, - "max_context_window": 372_000, - "comp_hash": "3000", - "experimental_supported_tools": [], - "visibility": "list", - "supported_in_api": true, - "priority": priority, - "additional_speed_tiers": ["fast"], - "multi_agent_version": multi_agent_version, - "tool_mode": "code_mode_only", - "prefer_websockets": true, - "reasoning_summary_format": "experimental", - "include_skills_usage_instructions": false, - "apply_patch_tool_type": "freeform", - "web_search_tool_type": "text_and_image", - "truncation_policy": { "mode": "tokens", "limit": 10_000 }, - "minimal_client_version": "0.144.0", - }) - .as_object() - .expect("bundled Codex model card extension must be an object") - .clone(), - ); - card -} - -fn bundled_codex_auto_review_model_card() -> Value { - let mut card = bundled_codex_model_card(BundledCodexModelCardSpec { - model_id: "codex-auto-review", - display_name: "Codex Auto Review", - description: "Automatic approval review model for Codex.", - default_reasoning_level: "medium", - default_reasoning_summary: "none", - use_responses_lite: false, - efforts: &["low", "medium", "high", "xhigh"], - default_verbosity: "low", - supports_priority_tier: false, - }); - let object = card - .as_object_mut() - .expect("bundled Codex model card must be an object"); - object.extend( - json!({ - "shell_type": "shell_command", - "supports_image_detail_original": true, - "supports_search_tool": true, - "input_modalities": ["text", "image"], - "context_window": 272_000, - "max_context_window": 1_000_000, - "experimental_supported_tools": [], - "visibility": "hide", - "supported_in_api": true, - "priority": 43, - "additional_speed_tiers": [], - "prefer_websockets": true, - "reasoning_summary_format": "experimental", - "include_skills_usage_instructions": false, - "apply_patch_tool_type": "freeform", - "web_search_tool_type": "text_and_image", - "truncation_policy": { "mode": "tokens", "limit": 10_000 }, - "minimal_client_version": "0.98.0", - }) - .as_object() - .expect("bundled Codex model card extension must be an object") - .clone(), - ); - card -} - pub fn bundled_codex_model_cards() -> &'static [Value] { static CARDS: OnceLock> = OnceLock::new(); CARDS.get_or_init(|| { - vec![ - bundled_gpt_5_6_codex_model_card( - "gpt-5.6-sol", - "GPT-5.6-Sol", - "Latest frontier agentic coding model.", - "low", - 1, - "v2", - true, - ), - bundled_gpt_5_6_codex_model_card( - "gpt-5.6-terra", - "GPT-5.6-Terra", - "Balanced agentic coding model for everyday work.", - "medium", - 2, - "v2", - true, - ), - bundled_gpt_5_6_codex_model_card( - "gpt-5.6-luna", - "GPT-5.6-Luna", - "Fast and affordable agentic coding model.", - "medium", - 3, - "v1", - false, - ), - bundled_codex_model_card(BundledCodexModelCardSpec { - model_id: "gpt-5.5", - display_name: "GPT-5.5", - description: "Frontier model for complex coding, research, and real-world work.", - default_reasoning_level: "medium", - default_reasoning_summary: "none", - use_responses_lite: false, - efforts: &["low", "medium", "high", "xhigh"], - default_verbosity: "low", - supports_priority_tier: true, - }), + // 公开能力快照来自 openai/codex rust-v0.159.3 的 + // codex-rs/models-manager/models.json;账户远端目录继续优先。 + let mut cards: Vec = serde_json::from_str(include_str!("codex_models_0_159_3.json")) + .expect("bundled Codex model capability snapshot must be valid JSON"); + for card in &mut cards { + let object = card.as_object_mut().expect("model card must be an object"); + object.insert("id".to_string(), object["slug"].clone()); + object.insert("object".to_string(), json!("model")); + object.insert("owned_by".to_string(), json!("openai")); + object.insert("api_formats".to_string(), json!(["openai:responses"])); + } + // 保留现有配置路由使用的旧模型标识。 + cards.extend(vec![ bundled_codex_model_card(BundledCodexModelCardSpec { model_id: "gpt-5.4", display_name: "GPT-5.4", @@ -556,8 +429,8 @@ pub fn bundled_codex_model_cards() -> &'static [Value] { default_verbosity: "low", supports_priority_tier: false, }), - bundled_codex_auto_review_model_card(), - ] + ]); + cards }) } @@ -2183,10 +2056,46 @@ mod tests { #[test] fn codex_client_user_agent_matches_originator_and_version() { let profile = crate::codex_client_profile(); - assert_eq!( - profile.user_agent, - format!("{}/{}", profile.originator, profile.codex_version) - ); + assert!(profile.user_agent.starts_with(&format!( + "{}/{} (", + profile.originator, profile.codex_version + ))); + assert!(profile.user_agent.ends_with(") unknown")); + } + + #[test] + fn bundled_latest_cli_models_preserve_capabilities_without_account_data() { + for model in ["gpt-6-astra", "gpt-6.1-sol"] { + let card = bundled_codex_model_cards() + .iter() + .find(|card| card["slug"] == model) + .unwrap(); + assert_eq!(card["context_window"], 272_000); + assert_eq!(card["max_context_window"], 872_000); + assert_eq!(card["minimal_client_version"], "0.153.0"); + assert_eq!( + card["supported_reasoning_levels"] + .as_array() + .unwrap() + .iter() + .map(|level| level["effort"].as_str().unwrap()) + .collect::>(), + ["low", "medium", "high", "xhigh", "max", "ultra"] + ); + for private in [ + "account_id", + "user_id", + "installation_id", + "cookie", + "authorization", + "comp_hash", + ] { + assert!( + card.get(private).is_none(), + "unexpected account field: {private}" + ); + } + } } #[test] @@ -2824,7 +2733,7 @@ mod tests { "codex-auto-review", None, ); - assert!(!capabilities.use_responses_lite); + assert!(capabilities.use_responses_lite); assert_eq!( capabilities.default_reasoning_effort.as_deref(), Some("medium") @@ -2832,7 +2741,7 @@ mod tests { assert_eq!(capabilities.default_reasoning_summary, None); assert!(capabilities.supports_parallel_tool_calls); assert_eq!(capabilities.default_verbosity.as_deref(), Some("low")); - assert!(capabilities.supported_service_tiers.is_empty()); + assert_eq!(capabilities.supported_service_tiers, vec!["priority"]); } #[test] diff --git a/crates/aether-ai/formats/src/formats/openai/responses/codex_models_0_159_3.json b/crates/aether-ai/formats/src/formats/openai/responses/codex_models_0_159_3.json new file mode 100644 index 000000000..c046504ed --- /dev/null +++ b/crates/aether-ai/formats/src/formats/openai/responses/codex_models_0_159_3.json @@ -0,0 +1,952 @@ +[ + { + "slug": "gpt-6-astra", + "prefer_websockets": true, + "support_verbosity": true, + "default_verbosity": "low", + "apply_patch_tool_type": "freeform", + "web_search_tool_type": "text_and_image", + "input_modalities": [ + "text", + "image" + ], + "supports_image_detail_original": true, + "truncation_policy": { + "mode": "tokens", + "limit": 10000 + }, + "supports_parallel_tool_calls": true, + "tool_mode": "code_mode_only", + "multi_agent_version": "v2", + "multi_agent_reasoning_effort": "xhigh", + "use_responses_lite": true, + "supports_reasoning_effort_updates": true, + "include_skills_usage_instructions": false, + "include_apps_usage_instructions": false, + "include_plugin_usage_instructions": false, + "guardian": null, + "node_repl_auto_review_required": true, + "node_repl_disabled": false, + "requires_sandboxed_review": false, + "auto_review_model_override": null, + "model_specialty": null, + "context_window": 272000, + "max_context_window": 872000, + "auto_compact_token_limit": null, + "default_reasoning_summary": "none", + "display_name": "GPT-6-Astra", + "description": "Frontier intelligence for the most demanding work.", + "default_reasoning_level": "low", + "supported_reasoning_levels": [ + { + "effort": "low", + "description": "Fast responses with lighter reasoning" + }, + { + "effort": "medium", + "description": "Balances speed and reasoning depth for everyday tasks" + }, + { + "effort": "high", + "description": "Greater reasoning depth for complex problems" + }, + { + "effort": "xhigh", + "description": "Extra high reasoning depth for complex problems" + }, + { + "effort": "max", + "description": "Maximum reasoning depth for the hardest problems" + }, + { + "effort": "ultra", + "description": "Maximum reasoning with automatic task delegation" + } + ], + "shell_type": "shell_command", + "visibility": "list", + "minimal_client_version": "0.153.0", + "supported_in_api": true, + "availability_nux": null, + "upgrade": null, + "priority": 2, + "experimental_supported_tools": [ + "send_user_message_async", + "clock" + ], + "supports_search_tool": true, + "supports_experimental_context": false, + "default_service_tier": null, + "service_tiers": [ + { + "id": "priority", + "name": "Fast", + "description": "2x speed, increased usage" + } + ], + "additional_speed_tiers": [ + "fast" + ], + "supports_reasoning_summary_parameter": true, + "supports_reasoning_summaries": true + }, + { + "slug": "gpt-6.1-sol", + "prefer_websockets": true, + "support_verbosity": true, + "default_verbosity": "low", + "apply_patch_tool_type": "freeform", + "web_search_tool_type": "text_and_image", + "input_modalities": [ + "text", + "image" + ], + "supports_image_detail_original": true, + "truncation_policy": { + "mode": "tokens", + "limit": 10000 + }, + "supports_parallel_tool_calls": true, + "tool_mode": "code_mode_only", + "multi_agent_version": "v2", + "multi_agent_reasoning_effort": "xhigh", + "use_responses_lite": true, + "include_skills_usage_instructions": false, + "include_apps_usage_instructions": false, + "include_plugin_usage_instructions": false, + "guardian": null, + "node_repl_auto_review_required": true, + "node_repl_disabled": false, + "requires_sandboxed_review": false, + "auto_review_model_override": null, + "model_specialty": null, + "context_window": 272000, + "max_context_window": 872000, + "auto_compact_token_limit": null, + "default_reasoning_summary": "none", + "display_name": "GPT-6.1-Sol", + "description": "Latest workhorse model for coding and everyday work.", + "default_reasoning_level": "low", + "supported_reasoning_levels": [ + { + "effort": "low", + "description": "Fast responses with lighter reasoning" + }, + { + "effort": "medium", + "description": "Balances speed and reasoning depth for everyday tasks" + }, + { + "effort": "high", + "description": "Greater reasoning depth for complex problems" + }, + { + "effort": "xhigh", + "description": "Extra high reasoning depth for complex problems" + }, + { + "effort": "max", + "description": "Maximum reasoning depth for the hardest problems" + }, + { + "effort": "ultra", + "description": "Maximum reasoning with automatic task delegation" + } + ], + "shell_type": "shell_command", + "visibility": "list", + "minimal_client_version": "0.153.0", + "supported_in_api": true, + "availability_nux": { + "message": "Maximize usage with GPT-6.1 Sol. Try it on complex work for near-Astra performance at a lower cost." + }, + "upgrade": null, + "priority": 1, + "experimental_supported_tools": [ + "send_user_message_async", + "clock" + ], + "supports_search_tool": true, + "supports_experimental_context": false, + "default_service_tier": null, + "service_tiers": [ + { + "id": "priority", + "name": "Fast", + "description": "2x speed, increased usage" + } + ], + "additional_speed_tiers": [ + "fast" + ], + "supports_reasoning_summary_parameter": true, + "supports_reasoning_summaries": true, + "supports_reasoning_effort_updates": true + }, + { + "slug": "gpt-6-sol", + "prefer_websockets": true, + "support_verbosity": true, + "default_verbosity": "low", + "apply_patch_tool_type": "freeform", + "web_search_tool_type": "text_and_image", + "input_modalities": [ + "text", + "image" + ], + "supports_image_detail_original": true, + "truncation_policy": { + "mode": "tokens", + "limit": 10000 + }, + "supports_parallel_tool_calls": true, + "tool_mode": "code_mode_only", + "multi_agent_version": "v2", + "multi_agent_reasoning_effort": null, + "use_responses_lite": true, + "include_skills_usage_instructions": false, + "include_apps_usage_instructions": false, + "include_plugin_usage_instructions": false, + "guardian": null, + "node_repl_auto_review_required": true, + "node_repl_disabled": false, + "requires_sandboxed_review": false, + "auto_review_model_override": null, + "model_specialty": null, + "context_window": 272000, + "max_context_window": 872000, + "auto_compact_token_limit": null, + "default_reasoning_summary": "none", + "display_name": "GPT-6-Sol", + "description": "Previous generation workhorse model.", + "default_reasoning_level": "medium", + "supported_reasoning_levels": [ + { + "effort": "low", + "description": "Fast responses with lighter reasoning" + }, + { + "effort": "medium", + "description": "Balances speed and reasoning depth for everyday tasks" + }, + { + "effort": "high", + "description": "Greater reasoning depth for complex problems" + }, + { + "effort": "xhigh", + "description": "Extra high reasoning depth for complex problems" + }, + { + "effort": "max", + "description": "Maximum reasoning depth for the hardest problems" + }, + { + "effort": "ultra", + "description": "Maximum reasoning with automatic task delegation" + } + ], + "shell_type": "shell_command", + "visibility": "list", + "minimal_client_version": "0.155.0", + "supported_in_api": true, + "availability_nux": null, + "upgrade": null, + "priority": 3, + "experimental_supported_tools": [ + "send_user_message_async", + "clock" + ], + "supports_search_tool": true, + "supports_experimental_context": false, + "default_service_tier": "priority", + "service_tiers": [ + { + "id": "priority", + "name": "Fast", + "description": "1.5x speed" + } + ], + "additional_speed_tiers": [ + "fast" + ], + "supports_reasoning_summary_parameter": true, + "supports_reasoning_summaries": true + }, + { + "slug": "gpt-6-luna", + "prefer_websockets": true, + "support_verbosity": true, + "default_verbosity": "low", + "apply_patch_tool_type": "freeform", + "web_search_tool_type": "text_and_image", + "input_modalities": [ + "text", + "image" + ], + "supports_image_detail_original": true, + "truncation_policy": { + "mode": "tokens", + "limit": 10000 + }, + "supports_parallel_tool_calls": true, + "tool_mode": "code_mode_only", + "multi_agent_version": "v2", + "multi_agent_reasoning_effort": null, + "use_responses_lite": true, + "include_skills_usage_instructions": false, + "include_apps_usage_instructions": false, + "include_plugin_usage_instructions": false, + "guardian": null, + "node_repl_auto_review_required": false, + "node_repl_disabled": false, + "requires_sandboxed_review": false, + "auto_review_model_override": null, + "model_specialty": null, + "context_window": 272000, + "max_context_window": 872000, + "auto_compact_token_limit": null, + "default_reasoning_summary": "none", + "display_name": "GPT-6-Luna", + "description": "Fast and affordable model for easier tasks.", + "default_reasoning_level": "medium", + "supported_reasoning_levels": [ + { + "effort": "low", + "description": "Fast responses with lighter reasoning" + }, + { + "effort": "medium", + "description": "Balances speed and reasoning depth for everyday tasks" + }, + { + "effort": "high", + "description": "Greater reasoning depth for complex problems" + }, + { + "effort": "xhigh", + "description": "Extra high reasoning depth for complex problems" + }, + { + "effort": "max", + "description": "Maximum reasoning depth for the hardest problems" + } + ], + "shell_type": "shell_command", + "visibility": "list", + "minimal_client_version": "0.155.0", + "supported_in_api": true, + "availability_nux": null, + "upgrade": null, + "priority": 4, + "experimental_supported_tools": [ + "send_user_message_async", + "clock" + ], + "supports_search_tool": true, + "supports_experimental_context": false, + "default_service_tier": "priority", + "service_tiers": [ + { + "id": "priority", + "name": "Fast", + "description": "1.5x speed" + } + ], + "additional_speed_tiers": [ + "fast" + ], + "supports_reasoning_summary_parameter": true, + "supports_reasoning_summaries": true + }, + { + "slug": "gpt-5.6-sol", + "prefer_websockets": true, + "support_verbosity": true, + "default_verbosity": "low", + "apply_patch_tool_type": "freeform", + "web_search_tool_type": "text_and_image", + "input_modalities": [ + "text", + "image" + ], + "supports_image_detail_original": true, + "truncation_policy": { + "mode": "tokens", + "limit": 10000 + }, + "supports_parallel_tool_calls": true, + "tool_mode": "code_mode_only", + "multi_agent_version": "v2", + "multi_agent_reasoning_effort": null, + "use_responses_lite": true, + "supports_reasoning_effort_updates": false, + "include_skills_usage_instructions": false, + "include_apps_usage_instructions": true, + "include_plugin_usage_instructions": true, + "guardian": null, + "node_repl_auto_review_required": false, + "node_repl_disabled": false, + "requires_sandboxed_review": false, + "auto_review_model_override": null, + "model_specialty": null, + "context_window": 272000, + "max_context_window": 872000, + "auto_compact_token_limit": null, + "default_reasoning_summary": "none", + "display_name": "GPT-5.6-Sol", + "description": "Older generation workhorse model.", + "default_reasoning_level": "low", + "supported_reasoning_levels": [ + { + "effort": "low", + "description": "Fast responses with lighter reasoning" + }, + { + "effort": "medium", + "description": "Balances speed and reasoning depth for everyday tasks" + }, + { + "effort": "high", + "description": "Greater reasoning depth for complex problems" + }, + { + "effort": "xhigh", + "description": "Extra high reasoning depth for complex problems" + }, + { + "effort": "max", + "description": "Maximum reasoning depth for the hardest problems" + }, + { + "effort": "ultra", + "description": "Maximum reasoning with automatic task delegation" + } + ], + "shell_type": "shell_command", + "visibility": "list", + "minimal_client_version": "0.144.0", + "supported_in_api": true, + "availability_nux": null, + "upgrade": { + "model": "gpt-6-sol", + "migration_markdown": "Meet GPT-6 Sol\n\nOur latest Sol is more intelligent and more efficient so your usage limits go further. This model is a great daily driver for complex tasks, especially coding.\n", + "retirement_at": null + }, + "priority": 5, + "experimental_supported_tools": [], + "supports_search_tool": true, + "supports_experimental_context": false, + "default_service_tier": null, + "service_tiers": [ + { + "id": "priority", + "name": "Fast", + "description": "1.5x speed, increased usage" + } + ], + "additional_speed_tiers": [ + "fast" + ], + "supports_reasoning_summary_parameter": true, + "supports_reasoning_summaries": true + }, + { + "slug": "gpt-5.6-terra", + "prefer_websockets": true, + "support_verbosity": true, + "default_verbosity": "low", + "apply_patch_tool_type": "freeform", + "web_search_tool_type": "text_and_image", + "input_modalities": [ + "text", + "image" + ], + "supports_image_detail_original": true, + "truncation_policy": { + "mode": "tokens", + "limit": 10000 + }, + "supports_parallel_tool_calls": true, + "tool_mode": "code_mode_only", + "multi_agent_version": "v2", + "multi_agent_reasoning_effort": null, + "use_responses_lite": true, + "supports_reasoning_effort_updates": false, + "include_skills_usage_instructions": false, + "include_apps_usage_instructions": true, + "include_plugin_usage_instructions": true, + "guardian": null, + "node_repl_auto_review_required": false, + "node_repl_disabled": false, + "requires_sandboxed_review": false, + "auto_review_model_override": null, + "model_specialty": null, + "context_window": 272000, + "max_context_window": 872000, + "auto_compact_token_limit": null, + "default_reasoning_summary": "none", + "display_name": "GPT-5.6-Terra", + "description": "Older balanced model for straightforward work.", + "default_reasoning_level": "medium", + "supported_reasoning_levels": [ + { + "effort": "low", + "description": "Fast responses with lighter reasoning" + }, + { + "effort": "medium", + "description": "Balances speed and reasoning depth for everyday tasks" + }, + { + "effort": "high", + "description": "Greater reasoning depth for complex problems" + }, + { + "effort": "xhigh", + "description": "Extra high reasoning depth for complex problems" + }, + { + "effort": "max", + "description": "Maximum reasoning depth for the hardest problems" + }, + { + "effort": "ultra", + "description": "Maximum reasoning with automatic task delegation" + } + ], + "shell_type": "shell_command", + "visibility": "list", + "minimal_client_version": "0.144.0", + "supported_in_api": true, + "availability_nux": null, + "upgrade": { + "model": "gpt-6-sol", + "migration_markdown": "Meet GPT-6 Sol\n\nOur latest Sol is more intelligent and more efficient so your usage limits go further. This model is a great daily driver for complex tasks, especially coding.\n", + "retirement_at": null + }, + "priority": 8, + "experimental_supported_tools": [], + "supports_search_tool": true, + "supports_experimental_context": false, + "default_service_tier": null, + "service_tiers": [ + { + "id": "priority", + "name": "Fast", + "description": "1.5x speed, increased usage" + } + ], + "additional_speed_tiers": [ + "fast" + ], + "supports_reasoning_summary_parameter": true, + "supports_reasoning_summaries": true + }, + { + "slug": "gpt-5.6-luna", + "prefer_websockets": true, + "support_verbosity": true, + "default_verbosity": "low", + "apply_patch_tool_type": "freeform", + "web_search_tool_type": "text_and_image", + "input_modalities": [ + "text", + "image" + ], + "supports_image_detail_original": true, + "truncation_policy": { + "mode": "tokens", + "limit": 10000 + }, + "supports_parallel_tool_calls": true, + "tool_mode": "code_mode_only", + "multi_agent_version": "v1", + "multi_agent_reasoning_effort": null, + "use_responses_lite": true, + "supports_reasoning_effort_updates": false, + "include_skills_usage_instructions": false, + "include_apps_usage_instructions": true, + "include_plugin_usage_instructions": true, + "guardian": null, + "node_repl_auto_review_required": false, + "node_repl_disabled": false, + "requires_sandboxed_review": false, + "auto_review_model_override": null, + "model_specialty": null, + "context_window": 272000, + "max_context_window": 872000, + "auto_compact_token_limit": null, + "default_reasoning_summary": "none", + "display_name": "GPT-5.6-Luna", + "description": "Older fast and efficient model.", + "default_reasoning_level": "medium", + "supported_reasoning_levels": [ + { + "effort": "low", + "description": "Fast responses with lighter reasoning" + }, + { + "effort": "medium", + "description": "Balances speed and reasoning depth for everyday tasks" + }, + { + "effort": "high", + "description": "Greater reasoning depth for complex problems" + }, + { + "effort": "xhigh", + "description": "Extra high reasoning depth for complex problems" + }, + { + "effort": "max", + "description": "Maximum reasoning depth for the hardest problems" + } + ], + "shell_type": "shell_command", + "visibility": "list", + "minimal_client_version": "0.144.0", + "supported_in_api": true, + "availability_nux": null, + "upgrade": { + "model": "gpt-6-luna", + "migration_markdown": "Meet GPT-6 Luna\n\nOur latest Luna is significantly more efficient, so your usage limits go even further. Reach for it for any job that doesn't require frontier intelligence.\n", + "retirement_at": null + }, + "priority": 9, + "experimental_supported_tools": [], + "supports_search_tool": true, + "supports_experimental_context": false, + "default_service_tier": null, + "service_tiers": [ + { + "id": "priority", + "name": "Fast", + "description": "1.5x speed, increased usage" + } + ], + "additional_speed_tiers": [ + "fast" + ], + "supports_reasoning_summary_parameter": true, + "supports_reasoning_summaries": true + }, + { + "slug": "gpt-daybreak-blue-latest", + "prefer_websockets": true, + "support_verbosity": true, + "default_verbosity": "low", + "apply_patch_tool_type": "freeform", + "web_search_tool_type": "text_and_image", + "input_modalities": [ + "text", + "image" + ], + "supports_image_detail_original": true, + "truncation_policy": { + "mode": "tokens", + "limit": 10000 + }, + "supports_parallel_tool_calls": true, + "tool_mode": "code_mode_only", + "multi_agent_version": "v2", + "use_responses_lite": true, + "include_skills_usage_instructions": false, + "include_apps_usage_instructions": true, + "include_plugin_usage_instructions": true, + "node_repl_auto_review_required": false, + "node_repl_disabled": false, + "auto_review_model_override": null, + "model_specialty": "cyber", + "context_window": 272000, + "max_context_window": 872000, + "auto_compact_token_limit": null, + "default_reasoning_summary": "none", + "display_name": "Daybreak Blue", + "description": "Latest frontier agentic coding model for broad defensive cybersecurity work.", + "default_reasoning_level": "low", + "supported_reasoning_levels": [ + { + "effort": "low", + "description": "Fast responses with lighter reasoning" + }, + { + "effort": "medium", + "description": "Balances speed and reasoning depth for everyday tasks" + }, + { + "effort": "high", + "description": "Greater reasoning depth for complex problems" + }, + { + "effort": "xhigh", + "description": "Extra high reasoning depth for complex problems" + }, + { + "effort": "max", + "description": "Maximum reasoning depth for the hardest problems" + }, + { + "effort": "ultra", + "description": "Maximum reasoning with automatic task delegation" + } + ], + "shell_type": "shell_command", + "visibility": "hide", + "minimal_client_version": "0.142.2", + "supported_in_api": true, + "availability_nux": null, + "upgrade": null, + "priority": 11, + "experimental_supported_tools": [], + "supports_search_tool": true, + "default_service_tier": null, + "service_tiers": [], + "additional_speed_tiers": [], + "supports_reasoning_summary_parameter": true, + "supports_reasoning_summaries": true + }, + { + "slug": "gpt-daybreak-red-latest", + "prefer_websockets": true, + "support_verbosity": true, + "default_verbosity": "high", + "apply_patch_tool_type": "freeform", + "web_search_tool_type": "text_and_image", + "input_modalities": [ + "text", + "image" + ], + "supports_image_detail_original": true, + "truncation_policy": { + "mode": "tokens", + "limit": 10000 + }, + "supports_parallel_tool_calls": true, + "tool_mode": "code_mode_only", + "multi_agent_version": "v2", + "use_responses_lite": true, + "include_skills_usage_instructions": false, + "include_apps_usage_instructions": false, + "include_plugin_usage_instructions": false, + "node_repl_auto_review_required": false, + "node_repl_disabled": false, + "auto_review_model_override": null, + "model_specialty": "cyber", + "context_window": 372000, + "max_context_window": 372000, + "auto_compact_token_limit": null, + "default_reasoning_summary": "none", + "display_name": "Daybreak Red", + "description": "Cyber-permissive variant of our latest frontier agentic coding model for advanced, authorized cybersecurity research.", + "default_reasoning_level": "medium", + "supported_reasoning_levels": [ + { + "effort": "low", + "description": "Fast responses with lighter reasoning" + }, + { + "effort": "medium", + "description": "Balances speed and reasoning depth for everyday tasks" + }, + { + "effort": "high", + "description": "Greater reasoning depth for complex problems" + }, + { + "effort": "xhigh", + "description": "Extra high reasoning depth for complex problems" + }, + { + "effort": "max", + "description": "Maximum reasoning depth for the hardest problems" + }, + { + "effort": "ultra", + "description": "Maximum reasoning with automatic task delegation" + } + ], + "shell_type": "shell_command", + "visibility": "hide", + "minimal_client_version": "0.142.2", + "supported_in_api": true, + "availability_nux": null, + "upgrade": null, + "priority": 12, + "experimental_supported_tools": [], + "supports_search_tool": true, + "default_service_tier": null, + "service_tiers": [], + "additional_speed_tiers": [], + "supports_reasoning_summary_parameter": true, + "supports_reasoning_summaries": true + }, + { + "slug": "gpt-5.5", + "prefer_websockets": true, + "support_verbosity": true, + "default_verbosity": "low", + "apply_patch_tool_type": "freeform", + "web_search_tool_type": "text_and_image", + "input_modalities": [ + "text", + "image" + ], + "supports_image_detail_original": true, + "truncation_policy": { + "mode": "tokens", + "limit": 10000 + }, + "supports_parallel_tool_calls": true, + "tool_mode": null, + "multi_agent_version": null, + "multi_agent_reasoning_effort": null, + "use_responses_lite": false, + "supports_reasoning_effort_updates": false, + "include_skills_usage_instructions": true, + "include_apps_usage_instructions": true, + "include_plugin_usage_instructions": true, + "guardian": null, + "node_repl_auto_review_required": false, + "node_repl_disabled": false, + "requires_sandboxed_review": false, + "auto_review_model_override": null, + "model_specialty": null, + "context_window": 272000, + "max_context_window": 272000, + "auto_compact_token_limit": null, + "default_reasoning_summary": "none", + "display_name": "GPT-5.5", + "description": "Legacy coding model.", + "default_reasoning_level": "medium", + "supported_reasoning_levels": [ + { + "effort": "low", + "description": "Fast responses with lighter reasoning" + }, + { + "effort": "medium", + "description": "Balances speed and reasoning depth for everyday tasks" + }, + { + "effort": "high", + "description": "Greater reasoning depth for complex problems" + }, + { + "effort": "xhigh", + "description": "Extra high reasoning depth for complex problems" + } + ], + "shell_type": "shell_command", + "visibility": "list", + "minimal_client_version": "0.124.0", + "supported_in_api": true, + "availability_nux": null, + "upgrade": { + "model": "gpt-6-sol", + "migration_markdown": "Meet GPT-6 Sol\n\nOur latest Sol is more intelligent and more efficient so your usage limits go further. This model is a great daily driver for complex tasks, especially coding.\n", + "retirement_at": null + }, + "priority": 13, + "experimental_supported_tools": [], + "supports_search_tool": true, + "supports_experimental_context": false, + "default_service_tier": null, + "service_tiers": [ + { + "id": "priority", + "name": "Fast", + "description": "1.5x speed, increased usage" + } + ], + "additional_speed_tiers": [ + "fast" + ], + "supports_reasoning_summary_parameter": true, + "supports_reasoning_summaries": true + }, + { + "slug": "codex-auto-review", + "prefer_websockets": true, + "support_verbosity": true, + "default_verbosity": "low", + "apply_patch_tool_type": "freeform", + "web_search_tool_type": "text_and_image", + "input_modalities": [ + "text", + "image" + ], + "supports_image_detail_original": true, + "truncation_policy": { + "mode": "tokens", + "limit": 10000 + }, + "supports_parallel_tool_calls": true, + "tool_mode": "code_mode_only", + "multi_agent_version": "v1", + "multi_agent_reasoning_effort": null, + "use_responses_lite": true, + "supports_reasoning_effort_updates": false, + "include_skills_usage_instructions": false, + "include_apps_usage_instructions": false, + "include_plugin_usage_instructions": false, + "guardian": null, + "node_repl_auto_review_required": false, + "node_repl_disabled": false, + "requires_sandboxed_review": false, + "auto_review_model_override": null, + "model_specialty": null, + "context_window": 272000, + "max_context_window": 872000, + "auto_compact_token_limit": null, + "default_reasoning_summary": "none", + "display_name": "Codex Auto Review", + "description": "Automatic approval review model for Codex.", + "default_reasoning_level": "medium", + "supported_reasoning_levels": [ + { + "effort": "low", + "description": "Fast responses with lighter reasoning" + }, + { + "effort": "medium", + "description": "Balances speed and reasoning depth for everyday tasks" + }, + { + "effort": "high", + "description": "Greater reasoning depth for complex problems" + }, + { + "effort": "xhigh", + "description": "Extra high reasoning depth for complex problems" + }, + { + "effort": "max", + "description": "Maximum reasoning depth for the hardest problems" + } + ], + "shell_type": "shell_command", + "visibility": "hide", + "minimal_client_version": "0.98.0", + "supported_in_api": true, + "availability_nux": null, + "upgrade": null, + "priority": 43, + "experimental_supported_tools": [], + "supports_search_tool": true, + "supports_experimental_context": false, + "default_service_tier": null, + "service_tiers": [ + { + "id": "priority", + "name": "Fast", + "description": "1.5x speed, increased usage" + } + ], + "additional_speed_tiers": [ + "fast" + ], + "supports_reasoning_summary_parameter": true, + "supports_reasoning_summaries": true + } +] diff --git a/crates/aether-ai/formats/src/formats/shared/passthrough.rs b/crates/aether-ai/formats/src/formats/shared/passthrough.rs index 2d8c8ed82..6c72d1860 100644 --- a/crates/aether-ai/formats/src/formats/shared/passthrough.rs +++ b/crates/aether-ai/formats/src/formats/shared/passthrough.rs @@ -7,6 +7,7 @@ use crate::contracts::{ GEMINI_EMBEDDING_SYNC_SUCCESS_REPORT_KIND, GEMINI_INTERACTIONS_STREAM_PLAN_KIND, GEMINI_INTERACTIONS_STREAM_SUCCESS_REPORT_KIND, GEMINI_INTERACTIONS_SYNC_PLAN_KIND, GEMINI_INTERACTIONS_SYNC_SUCCESS_REPORT_KIND, OPENAI_EMBEDDING_SYNC_PLAN_KIND, + OPENAI_MEMORIES_SYNC_PLAN_KIND, OPENAI_MEMORIES_SYNC_SUCCESS_REPORT_KIND, OPENAI_REALTIME_STREAM_PLAN_KIND, OPENAI_RERANK_SYNC_PLAN_KIND, OPENAI_SEARCH_SYNC_PLAN_KIND, OPENAI_SEARCH_SYNC_SUCCESS_REPORT_KIND, }; @@ -29,6 +30,14 @@ pub struct LocalSameFormatProviderSpec { pub fn resolve_sync_spec(plan_kind: &str) -> Option { match plan_kind { + OPENAI_MEMORIES_SYNC_PLAN_KIND => Some(LocalSameFormatProviderSpec { + api_format: "openai:responses", + decision_kind: OPENAI_MEMORIES_SYNC_PLAN_KIND, + report_kind: OPENAI_MEMORIES_SYNC_SUCCESS_REPORT_KIND, + family: LocalSameFormatProviderFamily::Standard, + require_streaming: false, + operation: Some(ApiOperation::OpenAiMemoriesSummarize), + }), CLAUDE_CHAT_SYNC_PLAN_KIND => Some(LocalSameFormatProviderSpec { api_format: "claude:messages", decision_kind: CLAUDE_CHAT_SYNC_PLAN_KIND, @@ -260,4 +269,16 @@ mod tests { assert_eq!(spec.report_kind, "openai_search_sync_success"); assert!(!spec.require_streaming); } + + #[test] + fn memories_uses_responses_permissions_and_a_native_sync_operation() { + let spec = resolve_sync_spec("openai_memories_sync").expect("memory spec"); + assert_eq!(spec.api_format, "openai:responses"); + assert_eq!( + spec.operation, + Some(crate::ApiOperation::OpenAiMemoriesSummarize) + ); + assert_eq!(spec.report_kind, "openai_memories_sync_success"); + assert!(!spec.require_streaming); + } } diff --git a/crates/aether-ai/formats/src/formats/shared/routing.rs b/crates/aether-ai/formats/src/formats/shared/routing.rs index c85aec418..ab9f25717 100644 --- a/crates/aether-ai/formats/src/formats/shared/routing.rs +++ b/crates/aether-ai/formats/src/formats/shared/routing.rs @@ -11,13 +11,13 @@ use crate::contracts::{ GEMINI_INTERACTIONS_STREAM_PLAN_KIND, GEMINI_INTERACTIONS_SYNC_PLAN_KIND, GEMINI_VIDEO_CANCEL_SYNC_PLAN_KIND, GEMINI_VIDEO_CREATE_SYNC_PLAN_KIND, OPENAI_CHAT_STREAM_PLAN_KIND, OPENAI_CHAT_SYNC_PLAN_KIND, OPENAI_EMBEDDING_SYNC_PLAN_KIND, - OPENAI_IMAGE_STREAM_PLAN_KIND, OPENAI_IMAGE_SYNC_PLAN_KIND, OPENAI_REALTIME_STREAM_PLAN_KIND, - OPENAI_RERANK_SYNC_PLAN_KIND, OPENAI_RESPONSES_COMPACT_STREAM_PLAN_KIND, - OPENAI_RESPONSES_COMPACT_SYNC_PLAN_KIND, OPENAI_RESPONSES_STREAM_PLAN_KIND, - OPENAI_RESPONSES_SYNC_PLAN_KIND, OPENAI_SEARCH_SYNC_PLAN_KIND, - OPENAI_VIDEO_CANCEL_SYNC_PLAN_KIND, OPENAI_VIDEO_CONTENT_PLAN_KIND, - OPENAI_VIDEO_CREATE_SYNC_PLAN_KIND, OPENAI_VIDEO_DELETE_SYNC_PLAN_KIND, - OPENAI_VIDEO_REMIX_SYNC_PLAN_KIND, + OPENAI_IMAGE_STREAM_PLAN_KIND, OPENAI_IMAGE_SYNC_PLAN_KIND, OPENAI_MEMORIES_SYNC_PLAN_KIND, + OPENAI_REALTIME_STREAM_PLAN_KIND, OPENAI_RERANK_SYNC_PLAN_KIND, + OPENAI_RESPONSES_COMPACT_STREAM_PLAN_KIND, OPENAI_RESPONSES_COMPACT_SYNC_PLAN_KIND, + OPENAI_RESPONSES_STREAM_PLAN_KIND, OPENAI_RESPONSES_SYNC_PLAN_KIND, + OPENAI_SEARCH_SYNC_PLAN_KIND, OPENAI_VIDEO_CANCEL_SYNC_PLAN_KIND, + OPENAI_VIDEO_CONTENT_PLAN_KIND, OPENAI_VIDEO_CREATE_SYNC_PLAN_KIND, + OPENAI_VIDEO_DELETE_SYNC_PLAN_KIND, OPENAI_VIDEO_REMIX_SYNC_PLAN_KIND, }; use crate::formats::openai::image::request::is_openai_image_stream_request; @@ -193,6 +193,14 @@ pub fn resolve_execution_runtime_sync_plan_kind_with_client_surface( return None; } + if route_family == Some("openai") + && route_kind == Some("memories") + && *method == Method::POST + && path == "/v1/memories/trace_summarize" + { + return Some(OPENAI_MEMORIES_SYNC_PLAN_KIND); + } + if route_family == Some("openai") && route_kind == Some("video") && *method == Method::POST @@ -580,6 +588,7 @@ pub fn supports_sync_execution_decision_kind(plan_kind: &str) -> bool { matches!( plan_kind, OPENAI_CHAT_SYNC_PLAN_KIND + | OPENAI_MEMORIES_SYNC_PLAN_KIND | OPENAI_EMBEDDING_SYNC_PLAN_KIND | OPENAI_RERANK_SYNC_PLAN_KIND | OPENAI_SEARCH_SYNC_PLAN_KIND diff --git a/crates/aether-ai/formats/src/formats/shared/sync_products.rs b/crates/aether-ai/formats/src/formats/shared/sync_products.rs index 93fe07764..616777eb3 100644 --- a/crates/aether-ai/formats/src/formats/shared/sync_products.rs +++ b/crates/aether-ai/formats/src/formats/shared/sync_products.rs @@ -746,6 +746,9 @@ fn maybe_build_standard_same_format_sync_body( } let body_json = body_json?; + if report_kind == "openai_memories_sync_finalize" { + return Some(body_json.clone()); + } if is_error_like_sync_body(body_json) { return None; } @@ -1949,6 +1952,7 @@ fn is_openai_responses_finalize_kind(report_kind: &str) -> bool { fn standard_same_format_api_format(report_kind: &str) -> Option<&'static str> { match report_kind { + "openai_memories_sync_finalize" => Some("openai:responses"), "openai_chat_sync_finalize" => Some("openai:chat"), "claude_chat_sync_finalize" => Some("claude:messages"), "gemini_chat_sync_finalize" => Some("gemini:generate_content"), @@ -4163,6 +4167,24 @@ mod tests { use base64::Engine as _; use serde_json::json; + #[test] + fn native_memories_response_keeps_output_array_and_future_fields() { + let body = json!({"output":[{"trace_summary":"synthetic", "memory_summary":"memory"}], "future_response_field":{"enabled":true}}); + let context = json!({"provider_api_format":"openai:responses","client_api_format":"openai:responses","needs_conversion":false,"requested_model":"gpt-6.1-sol-max"}); + assert_eq!( + maybe_build_standard_same_format_sync_body_from_normalized_payload( + "openai_memories_sync_finalize", + 200, + Some(&context), + Some(&body), + None + ) + .expect("native response") + .expect("body"), + body + ); + } + #[test] fn converts_openai_images_sync_body_to_gemini_image_body() { let provider_body_json = json!({ diff --git a/crates/aether-model-fetch/src/logic.rs b/crates/aether-model-fetch/src/logic.rs index e0eb69759..3ed021ea1 100644 --- a/crates/aether-model-fetch/src/logic.rs +++ b/crates/aether-model-fetch/src/logic.rs @@ -1860,14 +1860,20 @@ mod tests { assert_eq!( model_ids, vec![ + "gpt-6-astra", + "gpt-6.1-sol", + "gpt-6-sol", + "gpt-6-luna", "gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna", + "gpt-daybreak-blue-latest", + "gpt-daybreak-red-latest", "gpt-5.5", + "codex-auto-review", "gpt-5.4", "gpt-5.4-mini", "gpt-5.2", - "codex-auto-review", ] ); let sol = models @@ -1886,7 +1892,7 @@ mod tests { ); assert_eq!(sol["multi_agent_version"], "v2"); assert_eq!(sol["supports_image_detail_original"], true); - assert_eq!(sol["context_window"], 372_000); + assert_eq!(sol["context_window"], 272_000); for model_id in ["gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna"] { let model = models @@ -1894,11 +1900,11 @@ mod tests { .find(|model| model["id"] == model_id) .expect("GPT-5.6 Codex preset"); assert_eq!(model["shell_type"], "shell_command"); - assert_eq!(model["comp_hash"], "3000"); + assert!(model.get("comp_hash").is_none()); assert_eq!(model["experimental_supported_tools"], json!([])); assert_eq!(model["tool_mode"], "code_mode_only"); assert_eq!(model["prefer_websockets"], true); - assert_eq!(model["reasoning_summary_format"], "experimental"); + assert!(model.get("reasoning_summary_format").is_none()); assert_eq!(model["truncation_policy"]["limit"], 10_000); assert_eq!(model["minimal_client_version"], "0.144.0"); assert!(model.get("effective_context_window_percent").is_none()); @@ -1924,7 +1930,7 @@ mod tests { assert_eq!(auto_review["supported_in_api"], true); assert_eq!(auto_review["default_reasoning_level"], "medium"); assert_eq!(auto_review["default_reasoning_summary"], "none"); - assert_eq!(auto_review["use_responses_lite"], false); + assert_eq!(auto_review["use_responses_lite"], true); } #[test] diff --git a/crates/aether-model-fetch/src/transport.rs b/crates/aether-model-fetch/src/transport.rs index cc1d165e7..3f71ae349 100644 --- a/crates/aether-model-fetch/src/transport.rs +++ b/crates/aether-model-fetch/src/transport.rs @@ -129,6 +129,12 @@ pub async fn build_standard_models_fetch_execution_plan_for_client_version( provider_type == "codex" && api_format.starts_with("openai:"); let is_deepseek_anthropic_models_fetch = api_format.starts_with("claude:") && deepseek_anthropic_models_fetch_uses_openai_auth(&transport.endpoint.base_url); + if is_codex_openai_models_fetch { + if let Some(version) = codex_client_version { + aether_ai_formats::CodexClientProfile::cli(version) + .map_err(|error| error.to_string())?; + } + } let mut headers = standard_models_fetch_headers(&api_format, &provider_type, codex_client_version); if is_codex_openai_models_fetch { @@ -636,10 +642,9 @@ fn standard_models_fetch_headers( return BTreeMap::from([ ( "user-agent".to_string(), - format!( - "{}/{client_version}", - aether_ai_formats::codex_client_originator() - ), + aether_ai_formats::CodexClientProfile::cli(client_version) + .expect("validated Codex catalog client version") + .user_agent, ), ( "originator".to_string(), @@ -1059,7 +1064,12 @@ mod tests { ); assert_eq!( plan.headers.get("user-agent").map(String::as_str), - Some("codex_cli_rs/0.145.2") + Some( + aether_ai_formats::CodexClientProfile::cli("0.145.2") + .unwrap() + .user_agent + .as_str() + ) ); assert_eq!( plan.headers.get("originator").map(String::as_str), diff --git a/crates/aether-oauth/src/provider/providers/codex.rs b/crates/aether-oauth/src/provider/providers/codex.rs index 6b64a48e4..a9313774f 100644 --- a/crates/aether-oauth/src/provider/providers/codex.rs +++ b/crates/aether-oauth/src/provider/providers/codex.rs @@ -3,6 +3,16 @@ use super::generic::{ }; use crate::provider::ProviderOAuthAdapter; +/// Codex CLI 的公开 OAuth 权限范围,不包含账户专属值。 +pub const CODEX_OAUTH_SCOPES: &[&str] = &[ + "openid", + "profile", + "email", + "offline_access", + "api.connectors.read", + "api.connectors.invoke", +]; + #[derive(Debug, Clone)] pub struct CodexProviderOAuthAdapter { inner: GenericProviderOAuthAdapter, @@ -45,6 +55,7 @@ impl ProviderOAuthAdapter for CodexProviderOAuthAdapter { query.append_pair("prompt", "login"); query.append_pair("id_token_add_organizations", "true"); query.append_pair("codex_cli_simplified_flow", "true"); + query.append_pair("originator", "codex_cli_rs"); } response.authorize_url = url.to_string(); Ok(response) @@ -156,6 +167,26 @@ mod tests { assert!(response .authorize_url .contains("codex_cli_simplified_flow=true")); + let url = url::Url::parse(&response.authorize_url).unwrap(); + let query = url.query_pairs().collect::>(); + assert_eq!( + query.get("scope").map(|value| value.as_ref()), + Some("openid profile email offline_access api.connectors.read api.connectors.invoke") + ); + assert_eq!( + query.get("originator").map(|value| value.as_ref()), + Some("codex_cli_rs") + ); + assert_eq!( + query + .get("code_challenge_method") + .map(|value| value.as_ref()), + Some("S256") + ); + assert_eq!( + query.get("state").map(|value| value.as_ref()), + Some("state-1") + ); } #[tokio::test] diff --git a/crates/aether-oauth/src/provider/providers/generic.rs b/crates/aether-oauth/src/provider/providers/generic.rs index cfe85b19d..42cf619fa 100644 --- a/crates/aether-oauth/src/provider/providers/generic.rs +++ b/crates/aether-oauth/src/provider/providers/generic.rs @@ -92,7 +92,7 @@ pub const GENERIC_PROVIDER_OAUTH_TEMPLATES: &[GenericProviderOAuthTemplate] = &[ client_id: "app_EMoamEEZ73f0CkXaXp7hrann", client_id_env: None, client_secret_env: None, - scopes: &["openid", "email", "profile", "offline_access"], + scopes: super::codex::CODEX_OAUTH_SCOPES, redirect_uri: "http://localhost:1455/auth/callback", use_pkce: true, uses_json_payload: false, diff --git a/crates/aether-oauth/src/provider/providers/mod.rs b/crates/aether-oauth/src/provider/providers/mod.rs index f0cfeea68..69dfe6193 100644 --- a/crates/aether-oauth/src/provider/providers/mod.rs +++ b/crates/aether-oauth/src/provider/providers/mod.rs @@ -12,7 +12,7 @@ pub use claude_code::{ CLAUDE_CODE_COOKIE_SCOPE, CLAUDE_CODE_OAUTH_SCOPES, CLAUDE_CODE_PROVIDER_TYPE, CLAUDE_CODE_REDIRECT_URI, CLAUDE_CODE_TOKEN_URL, CLAUDE_CODE_WEB_BASE_URL, }; -pub use codex::CodexProviderOAuthAdapter; +pub use codex::{CodexProviderOAuthAdapter, CODEX_OAUTH_SCOPES}; pub use generic::{ derive_codex_identity_fingerprint, GenericProviderOAuthAdapter, GenericProviderOAuthTemplate, ANTIGRAVITY_OAUTH_CLIENT_ID_ENV, ANTIGRAVITY_OAUTH_CLIENT_SECRET_ENV, diff --git a/crates/aether-provider/transport/src/codex_fingerprint.rs b/crates/aether-provider/transport/src/codex_fingerprint.rs index ab0b4355a..1667aaba2 100644 --- a/crates/aether-provider/transport/src/codex_fingerprint.rs +++ b/crates/aether-provider/transport/src/codex_fingerprint.rs @@ -107,6 +107,12 @@ pub(crate) fn apply_codex_fingerprint_convergence_policy( } let is_responses = aether_ai_formats::is_openai_responses_format(provider_api_format); + if context.api_operation() == Some(aether_ai_formats::ApiOperation::OpenAiMemoriesSummarize) { + return ProviderOutboundRequestPolicyResult::skipped( + policy, + ProviderOutboundRequestPolicyReason::NativeOperationExcluded, + ); + } let is_live = aether_ai_formats::api_format_alias_matches(provider_api_format, "codex:live"); if !is_responses && !is_live { return ProviderOutboundRequestPolicyResult::skipped( @@ -567,6 +573,30 @@ mod tests { } } + #[test] + fn native_memories_excludes_responses_fingerprint_body_mutations() { + let transport = sample_transport(); + let context = + ProviderOutboundRequestContext::new("synthetic-memory-turn", 1_700_000_000_123) + .with_api_operation(aether_ai_formats::ApiOperation::OpenAiMemoriesSummarize); + let mut headers = BTreeMap::new(); + let mut body = json!({"model":"gpt-6.1-sol","traces":[],"future":42}); + let original = body.clone(); + let result = apply_codex_fingerprint_convergence_policy( + &transport, + "openai:responses", + &context, + &mut headers, + &mut body, + ); + assert_eq!( + result.reason, + ProviderOutboundRequestPolicyReason::NativeOperationExcluded + ); + assert_eq!(body, original); + assert!(headers.is_empty()); + } + #[test] fn provider_config_switch_is_opt_in_and_codex_only() { assert!(!codex_fingerprint_convergence_enabled("codex", None)); diff --git a/crates/aether-provider/transport/src/lib.rs b/crates/aether-provider/transport/src/lib.rs index 21411f369..0f12952e7 100644 --- a/crates/aether-provider/transport/src/lib.rs +++ b/crates/aether-provider/transport/src/lib.rs @@ -154,6 +154,7 @@ pub use rules::{ }; pub use same_format_provider::{ build_same_format_provider_headers, build_same_format_provider_request_body, + build_same_format_provider_request_body_for_operation, build_same_format_provider_request_body_with_compatibility_report, build_same_format_provider_request_body_with_compatibility_report_and_reasoning_replay_policy, build_same_format_provider_upstream_url, classify_same_format_provider_request_behavior, diff --git a/crates/aether-provider/transport/src/outbound_request_policy.rs b/crates/aether-provider/transport/src/outbound_request_policy.rs index d6948286d..397cd3fca 100644 --- a/crates/aether-provider/transport/src/outbound_request_policy.rs +++ b/crates/aether-provider/transport/src/outbound_request_policy.rs @@ -23,6 +23,7 @@ pub const PROVIDER_OUTBOUND_CONTEXT_MAX_VALUE_BYTES: usize = 256; #[derive(Debug, Clone, PartialEq, Eq)] pub struct ProviderOutboundRequestContext { + api_operation: Option, logical_turn_id: String, original_turn_id: Option, original_client_session_id: Option, @@ -33,6 +34,7 @@ pub struct ProviderOutboundRequestContext { impl ProviderOutboundRequestContext { pub fn new(logical_turn_id: impl Into, turn_started_at_unix_ms: u64) -> Self { Self { + api_operation: None, logical_turn_id: canonical_required_value(logical_turn_id.into(), "logical_turn_id"), original_turn_id: None, original_client_session_id: None, @@ -68,6 +70,15 @@ impl ProviderOutboundRequestContext { self.logical_turn_id.as_str() } + pub fn with_api_operation(mut self, operation: aether_ai_formats::ApiOperation) -> Self { + self.api_operation = Some(operation); + self + } + + pub fn api_operation(&self) -> Option { + self.api_operation + } + pub fn original_turn_id(&self) -> Option<&str> { self.original_turn_id.as_deref() } @@ -106,6 +117,7 @@ pub enum ProviderOutboundRequestPolicyReason { AgentIdentityExcluded, UnsupportedApiFormat, CompactOperationExcluded, + NativeOperationExcluded, Disabled, RequestBodyNotObject, } diff --git a/crates/aether-provider/transport/src/provider_types.rs b/crates/aether-provider/transport/src/provider_types.rs index cac278079..a40df9356 100644 --- a/crates/aether-provider/transport/src/provider_types.rs +++ b/crates/aether-provider/transport/src/provider_types.rs @@ -604,7 +604,7 @@ pub fn provider_type_admin_oauth_template(provider_type: &str) -> Option, ) -> bool { + if operation == Some(ApiOperation::OpenAiMemoriesSummarize) { + return aether_ai_formats::normalize_api_format_alias(provider_api_format) + == "openai:responses" + && !crate::kiro::is_kiro_provider_transport(transport) + && !crate::grok::is_grok_provider_transport(transport) + && !is_antigravity_provider_transport(transport) + && !is_gemini_cli_provider_transport(transport); + } if operation != Some(ApiOperation::ClaudeCountTokens) { return true; } @@ -1221,6 +1266,73 @@ mod tests { assert_eq!(url, "https://api.openai.example/v1/responses?tenant=demo"); } + #[test] + fn memories_url_respects_operation_templates_and_rejects_incompatible_paths() { + let params = TransportRequestUrlParams { + provider_api_format: "openai:responses", + mapped_model: Some("gpt-6.1-sol"), + upstream_is_stream: false, + request_query: Some("key=synthetic&tenant=demo"), + kiro_api_region: None, + api_operation: Some(ApiOperation::OpenAiMemoriesSummarize), + }; + let transport = sample_transport( + "codex", + "openai:responses", + "https://example.com", + Some("/native/{operation}"), + ); + assert_eq!( + build_transport_request_url(&transport, params).as_deref(), + Some("https://example.com/native/trace_summarize?tenant=demo") + ); + let incompatible = sample_transport( + "codex", + "openai:responses", + "https://example.com", + Some("/native/responses"), + ); + assert!(build_transport_request_url(&incompatible, params).is_none()); + for provider in ["kiro", "grok", "antigravity", "gemini_cli"] { + let private = + sample_transport(provider, "openai:responses", "https://example.com", None); + assert!( + build_transport_request_url(&private, params).is_none(), + "{provider}" + ); + } + } + + #[test] + fn memories_url_uses_the_configured_provider_root_and_removes_gateway_auth() { + for (provider, base, expected) in [ + ( + "codex", + "https://chatgpt.com/backend-api/codex", + "https://chatgpt.com/backend-api/codex/memories/trace_summarize?tenant=demo", + ), + ( + "custom", + "https://example.com/v1/responses", + "https://example.com/v1/memories/trace_summarize?tenant=demo", + ), + ] { + let transport = sample_transport(provider, "openai:responses", base, None); + let result = build_transport_request_url( + &transport, + TransportRequestUrlParams { + provider_api_format: "openai:responses", + mapped_model: Some("gpt-6.1-sol"), + upstream_is_stream: false, + request_query: Some("key=synthetic-secret&tenant=demo"), + kiro_api_region: None, + api_operation: Some(ApiOperation::OpenAiMemoriesSummarize), + }, + ); + assert_eq!(result.as_deref(), Some(expected)); + } + } + #[test] fn builds_openai_search_url_for_codex_provider_root() { let transport = sample_transport( diff --git a/crates/aether-provider/transport/src/same_format_provider/mod.rs b/crates/aether-provider/transport/src/same_format_provider/mod.rs index 3f3fa6358..7f443498f 100644 --- a/crates/aether-provider/transport/src/same_format_provider/mod.rs +++ b/crates/aether-provider/transport/src/same_format_provider/mod.rs @@ -273,7 +273,10 @@ pub fn classify_same_format_provider_request_behavior_for_operation( ); let operation_requires_sync = matches!( api_operation, - Some(aether_ai_formats::ApiOperation::ClaudeCountTokens) + Some( + aether_ai_formats::ApiOperation::ClaudeCountTokens + | aether_ai_formats::ApiOperation::OpenAiMemoriesSummarize + ) ); let upstream_is_stream = !operation_requires_sync && aether_ai_formats::resolve_upstream_is_stream_for_provider( @@ -321,6 +324,7 @@ pub fn build_same_format_provider_request_body( input, None, aether_ai_formats::OpenAiResponsesReasoningReplayPolicy::OpenAiItemIds, + false, ) } @@ -337,11 +341,34 @@ pub fn build_same_format_provider_request_body_with_compatibility_report_and_rea input: SameFormatProviderRequestBodyInput<'_>, reasoning_replay_policy: aether_ai_formats::OpenAiResponsesReasoningReplayPolicy, ) -> Option { + build_same_format_provider_request_body_for_operation(input, reasoning_replay_policy, None) +} + +pub fn build_same_format_provider_request_body_for_operation( + input: SameFormatProviderRequestBodyInput<'_>, + reasoning_replay_policy: aether_ai_formats::OpenAiResponsesReasoningReplayPolicy, + api_operation: Option, +) -> Option { + let native_memories = + api_operation == Some(aether_ai_formats::ApiOperation::OpenAiMemoriesSummarize); + if native_memories + && (!aether_ai_formats::api_format_alias_matches( + input.provider_api_format, + "openai:responses", + ) || !aether_ai_formats::api_format_alias_matches( + input.client_api_format, + "openai:responses", + ) || input.kiro_auth_config.is_some() + || input.is_claude_code) + { + return None; + } let mut compatibility_edits = Vec::new(); let body = build_same_format_provider_request_body_inner( input, Some(&mut compatibility_edits), reasoning_replay_policy, + native_memories, )?; Some(SameFormatProviderRequestBodyOutput { body, @@ -355,7 +382,10 @@ pub fn enforce_same_format_provider_api_operation_body_policy( ) -> bool { if !matches!( api_operation, - Some(aether_ai_formats::ApiOperation::ClaudeCountTokens) + Some( + aether_ai_formats::ApiOperation::ClaudeCountTokens + | aether_ai_formats::ApiOperation::OpenAiMemoriesSummarize + ) ) { return false; } @@ -367,6 +397,7 @@ fn build_same_format_provider_request_body_inner( input: SameFormatProviderRequestBodyInput<'_>, mut compatibility_edits: Option<&mut Vec>, reasoning_replay_policy: aether_ai_formats::OpenAiResponsesReasoningReplayPolicy, + native_memories: bool, ) -> Option { if let Some(kiro_auth_config) = input.kiro_auth_config { let body = build_kiro_provider_request_body( @@ -523,6 +554,11 @@ fn build_same_format_provider_request_body_inner( "applied configured provider body rules", ); } + if native_memories { + // 记忆端点使用原生 JSON,不注入 Responses 的 input/store/include/stream, + // 也不通过 Responses 规则归一化 traces。 + return Some(provider_request_body); + } if matches!(input.family, SameFormatProviderFamily::Gemini) && aether_ai_formats::api_format_alias_matches( input.provider_api_format, @@ -992,6 +1028,39 @@ mod tests { }; use serde_json::json; + #[test] + fn memories_preserves_native_traces_and_does_not_apply_responses_stream_policy() { + let body = json!({"model": "memory-global", "reasoning": {"effort": "high"}, + "traces": [{"id": "synthetic-trace", "items": [{"future": 7}]}], + "future_request": {"opaque": [1, 2, 3]}}); + let input = SameFormatProviderRequestBodyInput { + body_json: &body, + mapped_model: "memory-upstream", + client_api_format: "openai:responses", + provider_api_format: "openai:responses", + source_model: Some("memory-global"), + family: SameFormatProviderFamily::Standard, + body_rules: None, + request_headers: None, + upstream_is_stream: false, + force_body_stream_field: true, + kiro_auth_config: None, + is_claude_code: false, + enable_model_directives: false, + }; + let result = build_same_format_provider_request_body_for_operation( + input, + aether_ai_formats::OpenAiResponsesReasoningReplayPolicy::OpenAiItemIds, + Some(aether_ai_formats::ApiOperation::OpenAiMemoriesSummarize), + ) + .unwrap(); + let mut expected = body.clone(); + expected["model"] = json!("memory-upstream"); + assert_eq!(result.body, expected); + assert!(result.body.get("input").is_none()); + assert!(result.body.get("stream").is_none()); + } + fn sample_transport(provider_type: &str) -> GatewayProviderTransportSnapshot { GatewayProviderTransportSnapshot { provider: GatewayProviderTransportProvider { diff --git a/crates/aether-usage/runtime/src/report.rs b/crates/aether-usage/runtime/src/report.rs index d6591116d..8ee4d916a 100644 --- a/crates/aether-usage/runtime/src/report.rs +++ b/crates/aether-usage/runtime/src/report.rs @@ -299,6 +299,7 @@ pub fn is_local_ai_sync_report_kind(report_kind: &str) -> bool { | "claude_chat_sync_error" | "gemini_chat_sync_error" | "openai_responses_sync_success" + | "openai_memories_sync_success" | "openai_responses_compact_sync_success" | "openai_responses_sync_error" | "openai_responses_compact_sync_error" @@ -946,6 +947,7 @@ mod tests { )); assert!(is_local_ai_sync_report_kind("openai_image_sync_success")); assert!(is_local_ai_sync_report_kind("openai_search_sync_success")); + assert!(is_local_ai_sync_report_kind("openai_memories_sync_success")); assert!(is_local_ai_sync_report_kind("openai_image_sync_error")); assert!(is_local_ai_sync_report_kind( "openai_embedding_sync_success" diff --git a/docs/operations/codex-cli-alignment.md b/docs/operations/codex-cli-alignment.md new file mode 100644 index 000000000..259f33f7e --- /dev/null +++ b/docs/operations/codex-cli-alignment.md @@ -0,0 +1,33 @@ +# Codex CLI 通用协议对齐 + +本次对齐以官方 `openai/codex` 稳定标签 `rust-v0.159.3` 为可发布版本依据,同时核查最新 `main` 的协议实现。稳定标签提交为 `01fc69f4026735edfdf6789820549727a4867b11`,核查的 main 提交为 `444da310e108da16aaeb18fd790b0ac464f08aca`。 + +网关承接的是客户端与上游之间的协议,不复制 CLI 的本地工具执行、终端界面或个人账户状态。 + +| 对象 | 当前行为 | +| --- | --- | +| 客户端画像 | 默认版本更新为 0.159.3,现有后台 npm 稳定版本刷新继续生效;UA 按官方格式使用网关公开 OS、版本和架构,无终端时采用官方 `unknown` 标识 | +| OAuth | Codex 模板共享六项官方 scope,包含连接器读取及调用权限,并携带 `originator=codex_cli_rs`;其他 OAuth 类型保持独立模板 | +| 模型目录发现 | UA 与查询参数 `client_version` 使用同一版本;拒绝非法版本,保留现有保护头和运行时账户鉴权 | +| 模型能力 | 从官方公开模型目录提取能力快照,涵盖 `gpt-6-astra`、`gpt-6.1-sol`、六档推理强度及其他模型;账户返回的模型目录继续优先 | +| WebSocket 元数据 | 仅公开模型目录 ETag、轮次状态、实际模型与安全缓冲头;保留公开事件及未知公开字段、事件顺序 | +| 上游账户配额 | `codex.rate_limits` 继续进入账户级熔断与持久化路径,不当作网关用户自己的配额公开 | +| 原生记忆接口 | `POST /v1/memories/trace_summarize` 复用 Responses 权限及调度,执行原生同步操作,保留 traces、output 数组和未来字段,不注入 Responses 的 input/store/include 或流式默认值 | + +公开快照来源为 `codex-rs/models-manager/models.json`。未复制提示词、账户套餐可见性或编译哈希;没有引入个人用户标识、已有会话 UUID、Cookie、访问令牌或账户凭据。运行时鉴权和账户字段仍由提供商密钥配置产生。 + +新增模型能力快照不等于授权访问该模型。可用模型应通过正式管理界面的上游模型查询、全局模型和提供商模型配置,以及密钥模型限制来设置。远端目录及实际账户权限决定上游是否支持模型,不能通过修改 `/models` 列表绕过。 + +记忆接口是 CLI 可选功能对应的协议。是否启动记忆任务仍由 CLI 自己的 feature/config 决定。它要求提供商支持该原生端点;Kiro、Grok、Antigravity 和 Gemini CLI 等私有适配器不能通过格式标签冒充支持。配置自定义路径时须使用 `/memories/{operation}` 等模板;仅描述 Responses 的固定路径会被拒绝。 + +Apps 文件上传属于 CLI 的 ChatGPT Apps 专用路径,普通自定义 API 提供商不会启动该流程;本次不将其伪装成通用 Responses 路由。`aether-vscodex` 的 app-server UI 协议版本属于独立客户端,不覆盖成 CLI 版本。 + +验证命令: + +```bash +cargo fmt --all -- --check +cargo test --locked -p aether-ai-formats -p aether-oauth -p aether-model-fetch -p aether-provider-transport -p aether-usage-runtime --lib +cargo test --locked -p aether-gateway --lib +``` + +原生记忆端到端测试覆盖真实网关的权限、候选调度、执行计划、模型指令、原生 JSON、成功候选状态与上游错误响应;执行端使用本地测试服务器,实际账户网络可用性须在部署现场单独验证。