mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-09 02:47:45 +08:00
对齐 Codex CLI 0.159.3 的通用画像、模型能力与原生协议
This commit is contained in:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user