对齐 Codex CLI 0.159.3 的通用画像、模型能力与原生协议

This commit is contained in:
MMEXA
2026-10-01 17:54:04 +08:00
parent 54fbcc25a1
commit 14befeda2c
38 changed files with 1938 additions and 383 deletions
Generated
+79 -39
View File
@@ -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]]
@@ -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 => {
@@ -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,
@@ -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(),
@@ -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,
)
}
@@ -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,
+1
View File
@@ -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",
@@ -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",
@@ -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"}),
@@ -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"
@@ -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<Cow<'a, Value>>),
/// 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,
@@ -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::<Vec<_>>();
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<Cow<'_, Value>> {
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<String, Value> = 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;
@@ -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)
@@ -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("[email protected]".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("[email protected]".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::<serde_json::Value>::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::<serde_json::Value>().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();
},
);
}
+1
View File
@@ -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
+21 -5
View File
@@ -1,4 +1,6 @@
use std::sync::{OnceLock, RwLock};
use std::sync::{LazyLock, OnceLock, RwLock};
static OS_INFO: LazyLock<os_info::Info> = 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]
@@ -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,
@@ -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";
@@ -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),
@@ -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"
);
}
}
@@ -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<Vec<Value>> = 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<Value> = 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::<Vec<_>>(),
["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]
@@ -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
}
]
@@ -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<LocalSameFormatProviderSpec> {
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);
}
}
@@ -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
@@ -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!({
+11 -5
View File
@@ -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]
+15 -5
View File
@@ -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),
@@ -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::<BTreeMap<_, _>>();
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]
@@ -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,
@@ -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,
@@ -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));
@@ -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,
@@ -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<aether_ai_formats::ApiOperation>,
logical_turn_id: String,
original_turn_id: Option<String>,
original_client_session_id: Option<String>,
@@ -33,6 +34,7 @@ pub struct ProviderOutboundRequestContext {
impl ProviderOutboundRequestContext {
pub fn new(logical_turn_id: impl Into<String>, 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<aether_ai_formats::ApiOperation> {
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,
}
@@ -604,7 +604,7 @@ pub fn provider_type_admin_oauth_template(provider_type: &str) -> Option<Provide
authorize_url: "https://auth.openai.com/oauth/authorize",
token_url: "https://auth.openai.com/oauth/token",
client_id: "app_EMoamEEZ73f0CkXaXp7hrann",
scopes: &["openid", "email", "profile", "offline_access"],
scopes: aether_oauth::provider::providers::CODEX_OAUTH_SCOPES,
redirect_uri: "http://localhost:1455/auth/callback",
use_pkce: true,
}),
@@ -120,6 +120,43 @@ fn build_transport_request_url_inner(
return Some(url);
}
if params.api_operation == Some(ApiOperation::OpenAiMemoriesSummarize) {
if normalized_provider_api_format != "openai:responses" {
return None;
}
if let Some(path) = transport
.endpoint
.custom_path
.as_deref()
.map(str::trim)
.filter(|path| !path.is_empty())
{
// 显式操作模板也适用于原生同步操作。
if path.contains("{operation}") {
let path = expand_custom_path_template(path, build_path_params(params, false))?;
return build_passthrough_path_url(
&transport.endpoint.base_url,
&path,
params.request_query,
GATEWAY_CREDENTIAL_QUERY_KEYS,
);
}
// 仅描述 Responses 的自定义路径无法承接该操作。
return None;
}
// 以 Responses 的同一提供商根路径派生原生端点。
let mut url = Url::parse(&build_openai_responses_url(
&transport.endpoint.base_url,
strip_gateway_credential_query_parameters(params.request_query).as_deref(),
false,
))
.ok()?;
let root = url.path().strip_suffix("/responses")?;
let path = format!("{root}/memories/trace_summarize");
url.set_path(&path);
return Some(url.to_string());
}
let xai_base =
crate::xai::resolved_xai_upstream_base_url(transport, &normalized_provider_api_format);
let request_base_url = xai_base
@@ -558,6 +595,14 @@ pub fn transport_supports_api_operation(
provider_api_format: &str,
operation: Option<ApiOperation>,
) -> 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(
@@ -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<SameFormatProviderRequestBodyOutput> {
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<aether_ai_formats::ApiOperation>,
) -> Option<SameFormatProviderRequestBodyOutput> {
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<SameFormatProviderCompatibilityEdit>>,
reasoning_replay_policy: aether_ai_formats::OpenAiResponsesReasoningReplayPolicy,
native_memories: bool,
) -> Option<Value> {
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 {
@@ -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"
+33
View File
@@ -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、成功候选状态与上游错误响应;执行端使用本地测试服务器,实际账户网络可用性须在部署现场单独验证。