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

This commit is contained in:
MMEXA
2026-10-01 17:54:04 +08:00
parent 54fbcc25a1
commit 14befeda2c
38 changed files with 1938 additions and 383 deletions
@@ -107,6 +107,12 @@ pub(crate) fn apply_codex_fingerprint_convergence_policy(
}
let is_responses = aether_ai_formats::is_openai_responses_format(provider_api_format);
if context.api_operation() == Some(aether_ai_formats::ApiOperation::OpenAiMemoriesSummarize) {
return ProviderOutboundRequestPolicyResult::skipped(
policy,
ProviderOutboundRequestPolicyReason::NativeOperationExcluded,
);
}
let is_live = aether_ai_formats::api_format_alias_matches(provider_api_format, "codex:live");
if !is_responses && !is_live {
return ProviderOutboundRequestPolicyResult::skipped(
@@ -567,6 +573,30 @@ mod tests {
}
}
#[test]
fn native_memories_excludes_responses_fingerprint_body_mutations() {
let transport = sample_transport();
let context =
ProviderOutboundRequestContext::new("synthetic-memory-turn", 1_700_000_000_123)
.with_api_operation(aether_ai_formats::ApiOperation::OpenAiMemoriesSummarize);
let mut headers = BTreeMap::new();
let mut body = json!({"model":"gpt-6.1-sol","traces":[],"future":42});
let original = body.clone();
let result = apply_codex_fingerprint_convergence_policy(
&transport,
"openai:responses",
&context,
&mut headers,
&mut body,
);
assert_eq!(
result.reason,
ProviderOutboundRequestPolicyReason::NativeOperationExcluded
);
assert_eq!(body, original);
assert!(headers.is_empty());
}
#[test]
fn provider_config_switch_is_opt_in_and_codex_only() {
assert!(!codex_fingerprint_convergence_enabled("codex", None));
@@ -154,6 +154,7 @@ pub use rules::{
};
pub use same_format_provider::{
build_same_format_provider_headers, build_same_format_provider_request_body,
build_same_format_provider_request_body_for_operation,
build_same_format_provider_request_body_with_compatibility_report,
build_same_format_provider_request_body_with_compatibility_report_and_reasoning_replay_policy,
build_same_format_provider_upstream_url, classify_same_format_provider_request_behavior,
@@ -23,6 +23,7 @@ pub const PROVIDER_OUTBOUND_CONTEXT_MAX_VALUE_BYTES: usize = 256;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ProviderOutboundRequestContext {
api_operation: Option<aether_ai_formats::ApiOperation>,
logical_turn_id: String,
original_turn_id: Option<String>,
original_client_session_id: Option<String>,
@@ -33,6 +34,7 @@ pub struct ProviderOutboundRequestContext {
impl ProviderOutboundRequestContext {
pub fn new(logical_turn_id: impl Into<String>, turn_started_at_unix_ms: u64) -> Self {
Self {
api_operation: None,
logical_turn_id: canonical_required_value(logical_turn_id.into(), "logical_turn_id"),
original_turn_id: None,
original_client_session_id: None,
@@ -68,6 +70,15 @@ impl ProviderOutboundRequestContext {
self.logical_turn_id.as_str()
}
pub fn with_api_operation(mut self, operation: aether_ai_formats::ApiOperation) -> Self {
self.api_operation = Some(operation);
self
}
pub fn api_operation(&self) -> Option<aether_ai_formats::ApiOperation> {
self.api_operation
}
pub fn original_turn_id(&self) -> Option<&str> {
self.original_turn_id.as_deref()
}
@@ -106,6 +117,7 @@ pub enum ProviderOutboundRequestPolicyReason {
AgentIdentityExcluded,
UnsupportedApiFormat,
CompactOperationExcluded,
NativeOperationExcluded,
Disabled,
RequestBodyNotObject,
}
@@ -604,7 +604,7 @@ pub fn provider_type_admin_oauth_template(provider_type: &str) -> Option<Provide
authorize_url: "https://auth.openai.com/oauth/authorize",
token_url: "https://auth.openai.com/oauth/token",
client_id: "app_EMoamEEZ73f0CkXaXp7hrann",
scopes: &["openid", "email", "profile", "offline_access"],
scopes: aether_oauth::provider::providers::CODEX_OAUTH_SCOPES,
redirect_uri: "http://localhost:1455/auth/callback",
use_pkce: true,
}),
@@ -120,6 +120,43 @@ fn build_transport_request_url_inner(
return Some(url);
}
if params.api_operation == Some(ApiOperation::OpenAiMemoriesSummarize) {
if normalized_provider_api_format != "openai:responses" {
return None;
}
if let Some(path) = transport
.endpoint
.custom_path
.as_deref()
.map(str::trim)
.filter(|path| !path.is_empty())
{
// 显式操作模板也适用于原生同步操作。
if path.contains("{operation}") {
let path = expand_custom_path_template(path, build_path_params(params, false))?;
return build_passthrough_path_url(
&transport.endpoint.base_url,
&path,
params.request_query,
GATEWAY_CREDENTIAL_QUERY_KEYS,
);
}
// 仅描述 Responses 的自定义路径无法承接该操作。
return None;
}
// 以 Responses 的同一提供商根路径派生原生端点。
let mut url = Url::parse(&build_openai_responses_url(
&transport.endpoint.base_url,
strip_gateway_credential_query_parameters(params.request_query).as_deref(),
false,
))
.ok()?;
let root = url.path().strip_suffix("/responses")?;
let path = format!("{root}/memories/trace_summarize");
url.set_path(&path);
return Some(url.to_string());
}
let xai_base =
crate::xai::resolved_xai_upstream_base_url(transport, &normalized_provider_api_format);
let request_base_url = xai_base
@@ -558,6 +595,14 @@ pub fn transport_supports_api_operation(
provider_api_format: &str,
operation: Option<ApiOperation>,
) -> bool {
if operation == Some(ApiOperation::OpenAiMemoriesSummarize) {
return aether_ai_formats::normalize_api_format_alias(provider_api_format)
== "openai:responses"
&& !crate::kiro::is_kiro_provider_transport(transport)
&& !crate::grok::is_grok_provider_transport(transport)
&& !is_antigravity_provider_transport(transport)
&& !is_gemini_cli_provider_transport(transport);
}
if operation != Some(ApiOperation::ClaudeCountTokens) {
return true;
}
@@ -1221,6 +1266,73 @@ mod tests {
assert_eq!(url, "https://api.openai.example/v1/responses?tenant=demo");
}
#[test]
fn memories_url_respects_operation_templates_and_rejects_incompatible_paths() {
let params = TransportRequestUrlParams {
provider_api_format: "openai:responses",
mapped_model: Some("gpt-6.1-sol"),
upstream_is_stream: false,
request_query: Some("key=synthetic&tenant=demo"),
kiro_api_region: None,
api_operation: Some(ApiOperation::OpenAiMemoriesSummarize),
};
let transport = sample_transport(
"codex",
"openai:responses",
"https://example.com",
Some("/native/{operation}"),
);
assert_eq!(
build_transport_request_url(&transport, params).as_deref(),
Some("https://example.com/native/trace_summarize?tenant=demo")
);
let incompatible = sample_transport(
"codex",
"openai:responses",
"https://example.com",
Some("/native/responses"),
);
assert!(build_transport_request_url(&incompatible, params).is_none());
for provider in ["kiro", "grok", "antigravity", "gemini_cli"] {
let private =
sample_transport(provider, "openai:responses", "https://example.com", None);
assert!(
build_transport_request_url(&private, params).is_none(),
"{provider}"
);
}
}
#[test]
fn memories_url_uses_the_configured_provider_root_and_removes_gateway_auth() {
for (provider, base, expected) in [
(
"codex",
"https://chatgpt.com/backend-api/codex",
"https://chatgpt.com/backend-api/codex/memories/trace_summarize?tenant=demo",
),
(
"custom",
"https://example.com/v1/responses",
"https://example.com/v1/memories/trace_summarize?tenant=demo",
),
] {
let transport = sample_transport(provider, "openai:responses", base, None);
let result = build_transport_request_url(
&transport,
TransportRequestUrlParams {
provider_api_format: "openai:responses",
mapped_model: Some("gpt-6.1-sol"),
upstream_is_stream: false,
request_query: Some("key=synthetic-secret&tenant=demo"),
kiro_api_region: None,
api_operation: Some(ApiOperation::OpenAiMemoriesSummarize),
},
);
assert_eq!(result.as_deref(), Some(expected));
}
}
#[test]
fn builds_openai_search_url_for_codex_provider_root() {
let transport = sample_transport(
@@ -273,7 +273,10 @@ pub fn classify_same_format_provider_request_behavior_for_operation(
);
let operation_requires_sync = matches!(
api_operation,
Some(aether_ai_formats::ApiOperation::ClaudeCountTokens)
Some(
aether_ai_formats::ApiOperation::ClaudeCountTokens
| aether_ai_formats::ApiOperation::OpenAiMemoriesSummarize
)
);
let upstream_is_stream = !operation_requires_sync
&& aether_ai_formats::resolve_upstream_is_stream_for_provider(
@@ -321,6 +324,7 @@ pub fn build_same_format_provider_request_body(
input,
None,
aether_ai_formats::OpenAiResponsesReasoningReplayPolicy::OpenAiItemIds,
false,
)
}
@@ -337,11 +341,34 @@ pub fn build_same_format_provider_request_body_with_compatibility_report_and_rea
input: SameFormatProviderRequestBodyInput<'_>,
reasoning_replay_policy: aether_ai_formats::OpenAiResponsesReasoningReplayPolicy,
) -> Option<SameFormatProviderRequestBodyOutput> {
build_same_format_provider_request_body_for_operation(input, reasoning_replay_policy, None)
}
pub fn build_same_format_provider_request_body_for_operation(
input: SameFormatProviderRequestBodyInput<'_>,
reasoning_replay_policy: aether_ai_formats::OpenAiResponsesReasoningReplayPolicy,
api_operation: Option<aether_ai_formats::ApiOperation>,
) -> Option<SameFormatProviderRequestBodyOutput> {
let native_memories =
api_operation == Some(aether_ai_formats::ApiOperation::OpenAiMemoriesSummarize);
if native_memories
&& (!aether_ai_formats::api_format_alias_matches(
input.provider_api_format,
"openai:responses",
) || !aether_ai_formats::api_format_alias_matches(
input.client_api_format,
"openai:responses",
) || input.kiro_auth_config.is_some()
|| input.is_claude_code)
{
return None;
}
let mut compatibility_edits = Vec::new();
let body = build_same_format_provider_request_body_inner(
input,
Some(&mut compatibility_edits),
reasoning_replay_policy,
native_memories,
)?;
Some(SameFormatProviderRequestBodyOutput {
body,
@@ -355,7 +382,10 @@ pub fn enforce_same_format_provider_api_operation_body_policy(
) -> bool {
if !matches!(
api_operation,
Some(aether_ai_formats::ApiOperation::ClaudeCountTokens)
Some(
aether_ai_formats::ApiOperation::ClaudeCountTokens
| aether_ai_formats::ApiOperation::OpenAiMemoriesSummarize
)
) {
return false;
}
@@ -367,6 +397,7 @@ fn build_same_format_provider_request_body_inner(
input: SameFormatProviderRequestBodyInput<'_>,
mut compatibility_edits: Option<&mut Vec<SameFormatProviderCompatibilityEdit>>,
reasoning_replay_policy: aether_ai_formats::OpenAiResponsesReasoningReplayPolicy,
native_memories: bool,
) -> Option<Value> {
if let Some(kiro_auth_config) = input.kiro_auth_config {
let body = build_kiro_provider_request_body(
@@ -523,6 +554,11 @@ fn build_same_format_provider_request_body_inner(
"applied configured provider body rules",
);
}
if native_memories {
// 记忆端点使用原生 JSON,不注入 Responses 的 input/store/include/stream,
// 也不通过 Responses 规则归一化 traces。
return Some(provider_request_body);
}
if matches!(input.family, SameFormatProviderFamily::Gemini)
&& aether_ai_formats::api_format_alias_matches(
input.provider_api_format,
@@ -992,6 +1028,39 @@ mod tests {
};
use serde_json::json;
#[test]
fn memories_preserves_native_traces_and_does_not_apply_responses_stream_policy() {
let body = json!({"model": "memory-global", "reasoning": {"effort": "high"},
"traces": [{"id": "synthetic-trace", "items": [{"future": 7}]}],
"future_request": {"opaque": [1, 2, 3]}});
let input = SameFormatProviderRequestBodyInput {
body_json: &body,
mapped_model: "memory-upstream",
client_api_format: "openai:responses",
provider_api_format: "openai:responses",
source_model: Some("memory-global"),
family: SameFormatProviderFamily::Standard,
body_rules: None,
request_headers: None,
upstream_is_stream: false,
force_body_stream_field: true,
kiro_auth_config: None,
is_claude_code: false,
enable_model_directives: false,
};
let result = build_same_format_provider_request_body_for_operation(
input,
aether_ai_formats::OpenAiResponsesReasoningReplayPolicy::OpenAiItemIds,
Some(aether_ai_formats::ApiOperation::OpenAiMemoriesSummarize),
)
.unwrap();
let mut expected = body.clone();
expected["model"] = json!("memory-upstream");
assert_eq!(result.body, expected);
assert!(result.body.get("input").is_none());
assert!(result.body.get("stream").is_none());
}
fn sample_transport(provider_type: &str) -> GatewayProviderTransportSnapshot {
GatewayProviderTransportSnapshot {
provider: GatewayProviderTransportProvider {