mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-08 18:37:46 +08:00
feat(gateway): Codex/OpenAI Responses WebSocket 代理模式
在 /v1/responses 上支持 WebSocket 升级,把客户端帧中继到上游 Codex / OpenAI Responses WebSocket 端点,同时保持既有的路由、鉴权、配额与用量 语义: - 路由与准入:control/route/ai.rs 识别 WebSocket 升级请求; websocket/ingress.rs 复用 API Key 鉴权、IP 规则与并发许可,并引入 独立的 WebSocket 连接许可 - 中继:websocket/responses/* 按 connection / session / turn 分层, 帧解析归一化、socket 写入有界、continuation 保持调度亲和性 - 配额:orchestration/codex_quota_breaker.rs 在账号配额耗尽时熔断并 自动恢复,不再直接断开客户端连接 - 用量:每个 turn 的终态用量落库,request_metadata 记录 websocket_mode / websocket_transport,管理端与 usage 视图暴露 is_websocket - 管理端:provider 可配置 Responses WebSocket 开关
This commit is contained in:
@@ -10,7 +10,8 @@ use aether_data_contracts::repository::usage::{
|
||||
PROVIDER_ACTUAL_SERVICE_TIER_METADATA_KEY, PROVIDER_CACHE_TTL_MINUTES_METADATA_KEY,
|
||||
PROVIDER_REASONING_EFFORT_METADATA_KEY, PROVIDER_SERVICE_TIER_METADATA_KEY,
|
||||
REQUESTED_REASONING_EFFORT_METADATA_KEY, ROUTING_CANDIDATE_SKIP_REASON_METADATA_KEY,
|
||||
ROUTING_FAILURE_DIAGNOSTIC_METADATA_KEY,
|
||||
ROUTING_FAILURE_DIAGNOSTIC_METADATA_KEY, WEBSOCKET_MODE_METADATA_KEY,
|
||||
WEBSOCKET_TRANSPORT_METADATA_KEY,
|
||||
};
|
||||
use serde_json::{json, Map, Value};
|
||||
|
||||
@@ -348,6 +349,8 @@ fn copy_allowed_metadata_fields(source: &Map<String, Value>, target: &mut Map<St
|
||||
copy_bool(source, target, UPSTREAM_IS_STREAM_KEY);
|
||||
copy_non_null_value(source, target, "client_session_affinity");
|
||||
copy_bool(source, target, "api_key_is_standalone");
|
||||
copy_bool(source, target, WEBSOCKET_MODE_METADATA_KEY);
|
||||
copy_non_empty_string(source, target, WEBSOCKET_TRANSPORT_METADATA_KEY);
|
||||
copy_non_empty_string(source, target, "request_path");
|
||||
copy_non_empty_string(source, target, "request_query_string");
|
||||
copy_non_empty_string(source, target, "request_path_and_query");
|
||||
@@ -402,6 +405,8 @@ fn move_allowed_metadata_fields(mut source: Map<String, Value>, target: &mut Map
|
||||
remove_bool(&mut source, target, UPSTREAM_IS_STREAM_KEY);
|
||||
remove_non_null_value(&mut source, target, "client_session_affinity");
|
||||
remove_bool(&mut source, target, "api_key_is_standalone");
|
||||
remove_bool(&mut source, target, WEBSOCKET_MODE_METADATA_KEY);
|
||||
remove_non_empty_string(&mut source, target, WEBSOCKET_TRANSPORT_METADATA_KEY);
|
||||
remove_non_empty_string(&mut source, target, "request_path");
|
||||
remove_non_empty_string(&mut source, target, "request_query_string");
|
||||
remove_non_empty_string(&mut source, target, "request_path_and_query");
|
||||
@@ -907,6 +912,24 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sanitizes_websocket_transport_metadata() {
|
||||
let metadata = sanitize_usage_request_metadata(Some(json!({
|
||||
"websocket_mode": true,
|
||||
"websocket_transport": "responses",
|
||||
"untrusted_field": "drop-me",
|
||||
})))
|
||||
.expect("WebSocket metadata should remain");
|
||||
|
||||
assert_eq!(
|
||||
metadata,
|
||||
json!({
|
||||
"websocket_mode": true,
|
||||
"websocket_transport": "responses",
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sanitizes_request_path_query_metadata() {
|
||||
let metadata = sanitize_usage_request_metadata(Some(json!({
|
||||
|
||||
Reference in New Issue
Block a user