mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-06 01:17:46 +08:00
Merge pull request #876 from MMEXA/codex/cli-alignment-20261001
对齐 Codex CLI 0.159.3 的画像、模型能力与原生协议
This commit is contained in:
Generated
+79
-39
@@ -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]]
|
||||
|
||||
-1
@@ -1 +0,0 @@
|
||||
test binary
|
||||
@@ -19,12 +19,12 @@ pub(crate) use crate::ai_serving::{
|
||||
GEMINI_FILES_UPLOAD_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_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_MEMORIES_SYNC_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,
|
||||
};
|
||||
|
||||
pub(crate) use aether_ai_serving::AiRequestedModelFamily as RequestedModelFamily;
|
||||
|
||||
@@ -6,12 +6,13 @@ use crate::ai_serving::planner::common::{
|
||||
GEMINI_FILES_DOWNLOAD_PLAN_KIND, GEMINI_FILES_GET_PLAN_KIND, GEMINI_FILES_LIST_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_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_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::ai_serving::planner::plan_builders::{
|
||||
build_gemini_stream_plan_from_decision, build_gemini_sync_plan_from_decision,
|
||||
@@ -101,7 +102,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
|
||||
| OPENAI_MEMORIES_SYNC_PLAN_KIND => {
|
||||
build_passthrough_sync_plan_from_decision(parts, payload)?
|
||||
}
|
||||
OPENAI_RESPONSES_COMPACT_SYNC_PLAN_KIND => {
|
||||
|
||||
@@ -17,7 +17,7 @@ use crate::ai_serving::transport::ProviderOutboundRequestContext;
|
||||
use crate::ai_serving::{
|
||||
ClientSurface, ExecutionRuntimeAuthContext, GatewayAuthApiKeySnapshot,
|
||||
GatewayCredentialCarrier, GatewayProviderTransportSnapshot, PlannerAppState,
|
||||
CODEX_RESPONSES_LITE_HEADER,
|
||||
CODEX_RESPONSES_LITE_HEADER, OPENAI_MEMORIES_SYNC_PLAN_KIND,
|
||||
};
|
||||
use crate::cache::CacheLoadObserver;
|
||||
use crate::client_session_affinity::client_session_affinity_from_api_request;
|
||||
@@ -123,6 +123,7 @@ 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(OPENAI_MEMORIES_SYNC_PLAN_KIND);
|
||||
let provider_api_format = decision
|
||||
.provider_api_format
|
||||
.clone()
|
||||
@@ -150,7 +151,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 +165,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 +267,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 +325,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 +352,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 +365,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 +400,15 @@ 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(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,
|
||||
|
||||
@@ -24,7 +24,9 @@ use crate::ai_serving::transport::{
|
||||
SameFormatProviderCompatibilityEditAction, SameFormatProviderHeadersInput,
|
||||
GEMINI_CLI_USER_AGENT, GROK_CHAT_PATH,
|
||||
};
|
||||
use crate::ai_serving::{CandidateFailureDiagnostic, GatewayProviderTransportSnapshot};
|
||||
use crate::ai_serving::{
|
||||
CandidateFailureDiagnostic, GatewayProviderTransportSnapshot, CODEX_RESPONSES_LITE_HEADER,
|
||||
};
|
||||
use crate::{AppState, GatewayError};
|
||||
|
||||
mod policy;
|
||||
@@ -255,7 +257,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 +279,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 +318,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 +603,11 @@ 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(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,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -163,7 +163,7 @@ pub(crate) use aether_ai_formats::api::{
|
||||
OPENAI_EMBEDDING_SYNC_PLAN_KIND, OPENAI_IMAGE_STREAM_PLAN_KIND,
|
||||
OPENAI_IMAGE_STREAM_SUCCESS_REPORT_KIND, OPENAI_IMAGE_SYNC_FINALIZE_REPORT_KIND,
|
||||
OPENAI_IMAGE_SYNC_PLAN_KIND, OPENAI_IMAGE_SYNC_SUCCESS_REPORT_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_STREAM_SUCCESS_REPORT_KIND,
|
||||
OPENAI_RESPONSES_COMPACT_SYNC_ERROR_REPORT_KIND,
|
||||
OPENAI_RESPONSES_COMPACT_SYNC_FINALIZE_REPORT_KIND, OPENAI_RESPONSES_COMPACT_SYNC_PLAN_KIND,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -2663,14 +2663,11 @@ mod tests {
|
||||
assert_eq!(plans.len(), 2);
|
||||
assert!(plans[0].url.ends_with("/models?client_version=0.145.2"));
|
||||
assert!(plans[1].url.ends_with("/models?client_version=0.146.0"));
|
||||
assert_eq!(
|
||||
plans[0].headers.get("user-agent").map(String::as_str),
|
||||
Some("codex_cli_rs/0.145.2")
|
||||
);
|
||||
assert_eq!(
|
||||
plans[1].headers.get("user-agent").map(String::as_str),
|
||||
Some("codex_cli_rs/0.146.0")
|
||||
);
|
||||
for (plan, version) in plans.iter().zip(["0.145.2", "0.146.0"]) {
|
||||
let ua = plan.headers.get("user-agent").expect("画像应包含 UA");
|
||||
assert!(ua.starts_with(&format!("codex_cli_rs/{version} (")));
|
||||
assert!(ua.ends_with(") unknown"));
|
||||
}
|
||||
|
||||
let target = target();
|
||||
assert_ne!(
|
||||
|
||||
@@ -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,120 @@ 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},"failover_rules":{"stop_status_codes":[400]}}),
|
||||
);
|
||||
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");
|
||||
let error_status = error.status();
|
||||
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();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -849,6 +849,12 @@ async fn gateway_handles_admin_provider_query_models_falls_back_to_codex_preset_
|
||||
"gpt-5.6-luna",
|
||||
"gpt-5.6-sol",
|
||||
"gpt-5.6-terra",
|
||||
"gpt-6-astra",
|
||||
"gpt-6-luna",
|
||||
"gpt-6-sol",
|
||||
"gpt-6.1-sol",
|
||||
"gpt-daybreak-blue-latest",
|
||||
"gpt-daybreak-red-latest",
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
|
||||
@@ -1270,10 +1270,9 @@ async fn run_versioned_codex_model_cards_frontdoor_scenario() {
|
||||
captured_catalog_plan.0,
|
||||
"https://chatgpt.example/backend-api/codex/models?client_version=0.145.2"
|
||||
);
|
||||
assert_eq!(
|
||||
captured_catalog_plan.1.as_deref(),
|
||||
Some("codex_cli_rs/0.145.2")
|
||||
);
|
||||
let captured_ua = captured_catalog_plan.1.expect("目录请求应包含 UA");
|
||||
assert!(captured_ua.starts_with("codex_cli_rs/0.145.2 ("));
|
||||
assert!(captured_ua.ends_with(") unknown"));
|
||||
|
||||
let fresh_response = client
|
||||
.get(format!("{gateway_url}/v1/models?client_version=0.145.2"))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -33,7 +33,7 @@ pub use crate::contracts::{
|
||||
OPENAI_IMAGE_STREAM_PLAN_KIND, OPENAI_IMAGE_STREAM_SUCCESS_REPORT_KIND,
|
||||
OPENAI_IMAGE_SYNC_ERROR_REPORT_KIND, OPENAI_IMAGE_SYNC_FINALIZE_REPORT_KIND,
|
||||
OPENAI_IMAGE_SYNC_PLAN_KIND, OPENAI_IMAGE_SYNC_SUCCESS_REPORT_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_STREAM_SUCCESS_REPORT_KIND,
|
||||
OPENAI_RESPONSES_COMPACT_SYNC_ERROR_REPORT_KIND,
|
||||
OPENAI_RESPONSES_COMPACT_SYNC_FINALIZE_REPORT_KIND, OPENAI_RESPONSES_COMPACT_SYNC_PLAN_KIND,
|
||||
|
||||
@@ -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["comp_hash"], "3000");
|
||||
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",
|
||||
] {
|
||||
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,963 @@
|
||||
[
|
||||
{
|
||||
"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,
|
||||
"comp_hash": "3000",
|
||||
"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,
|
||||
"comp_hash": "3000",
|
||||
"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,
|
||||
"comp_hash": "3000",
|
||||
"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,
|
||||
"comp_hash": "3000",
|
||||
"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,
|
||||
"comp_hash": "3000",
|
||||
"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,
|
||||
"comp_hash": "3000",
|
||||
"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,
|
||||
"comp_hash": "3000",
|
||||
"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,
|
||||
"comp_hash": "3000",
|
||||
"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,
|
||||
"comp_hash": "3000",
|
||||
"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,
|
||||
"comp_hash": "2911",
|
||||
"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,
|
||||
"comp_hash": "3000",
|
||||
"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!({
|
||||
|
||||
@@ -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
|
||||
@@ -1898,7 +1904,7 @@ mod tests {
|
||||
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]
|
||||
|
||||
@@ -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",
|
||||
"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"
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
# 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`。未复制提示词或账户套餐可见性;保留官方公开的 `comp_hash` 压缩兼容性标记,以支持 CLI Guardian 的压缩上下文复用;没有引入个人用户标识、已有会话 UUID、Cookie、访问令牌或账户凭据。运行时鉴权和账户字段仍由提供商密钥配置产生。
|
||||
|
||||
新增模型能力快照不等于授权访问该模型。可用模型应通过正式管理界面的上游模型查询、全局模型和提供商模型配置,以及密钥模型限制来设置。远端目录及实际账户权限决定上游是否支持模型,不能通过修改 `/models` 列表绕过。
|
||||
|
||||
六档推理强度描述 CLI 的选择项。`ultra` 是 CLI 的本地多代理模式:官方 `ModelInfo::resolve_reasoning_effort` 会把普通推理请求转换为有效的 `multi_agent_reasoning_effort`,否则依次采用 `max`、最高非 `ultra` 档位或 `medium`;本地模式仍保持 `ultra`。网关保留模型卡片的相关公开能力,由 CLI 完成该转换,不在原始 Responses 请求中擅自把 `ultra` 等同于 `max`,也不实现客户端的多代理编排。直接发送 `reasoning.effort=ultra` 与 CLI 选择 `ultra` 的请求不同,上游可能拒绝前者。
|
||||
|
||||
现场使用校验过发行资产摘要的官方 `0.159.3` CLI,分别对 `gpt-6-astra` 和 `gpt-6.1-sol` 选择 `ultra`,均成功完成;网关使用记录确认请求档位为模型指定的 `xhigh`。两模型的原始 Responses `low`、`medium`、`high`、`xhigh`、`max` 调用也均完成。直接发送原始 `ultra` 时,现场上游候选返回 HTTP 400,按现有重试策略最终返回 503;该结果不作为 CLI 的 `ultra` 模式验收失败。
|
||||
|
||||
同一现场的 WebSocket 验收对两模型分别完成两轮 `generate=false` 预热和 `previous_response_id` 续接,接收到了公开完成事件与 Codex metadata;这项检查覆盖连接、元数据和续接,不等同于完整生成、工具调用或所有重连场景。验证使用正式提供商开关,完成后恢复原配置。
|
||||
|
||||
记忆接口是 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
|
||||
RUST_MIN_STACK=16777216 cargo test --locked -p aether-ai-formats -p aether-oauth -p aether-model-fetch -p aether-provider-transport -p aether-usage-runtime --lib -- --test-threads=1
|
||||
RUST_MIN_STACK=16777216 cargo test --locked -p aether-gateway --lib -- --test-threads=1
|
||||
RUST_MIN_STACK=16777216 cargo test --locked -p aether-gateway --bins
|
||||
RUST_MIN_STACK=16777216 cargo test --locked -p aether-gateway --test admin_unsigned_identity_headers --test architecture_guard
|
||||
```
|
||||
|
||||
完整网关测试需要可执行的临时 PostgreSQL 工具(`AETHER_INITDB_BIN`、`AETHER_POSTGRES_BIN`、`AETHER_PG_CTL_BIN`)、权限受控的临时目录,以及不允许组写入的测试运行目录。Unix socket、更新元数据与私有运行文件测试会主动拒绝不安全的路径;测试目录应以 `umask 022` 或更严格的权限创建。
|
||||
|
||||
原生记忆端到端测试覆盖真实网关的权限、候选调度、执行计划、模型指令、原生 JSON、成功候选状态与明确停止重试策略下的上游错误响应;执行端使用本地测试服务器,实际账户网络可用性须在部署现场单独验证。
|
||||
Reference in New Issue
Block a user