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:
@@ -35,7 +35,10 @@ pub(super) fn classify_ai_public_route(
|
||||
"openai:rerank",
|
||||
true,
|
||||
))
|
||||
} else if method == http::Method::POST
|
||||
} else if (method == http::Method::POST
|
||||
|| (method == http::Method::GET
|
||||
&& normalized_path == "/v1/responses"
|
||||
&& is_websocket_upgrade_request(headers)))
|
||||
&& matches!(normalized_path, "/v1/responses" | "/v1/responses/compact")
|
||||
{
|
||||
if normalized_path.ends_with("/compact") {
|
||||
@@ -199,6 +202,24 @@ fn claude_request_auth_channel(headers: &http::HeaderMap) -> &'static str {
|
||||
}
|
||||
}
|
||||
|
||||
fn is_websocket_upgrade_request(headers: &http::HeaderMap) -> bool {
|
||||
let has_upgrade_connection = headers
|
||||
.get(http::header::CONNECTION)
|
||||
.and_then(|value| value.to_str().ok())
|
||||
.is_some_and(|value| {
|
||||
value
|
||||
.split(',')
|
||||
.map(str::trim)
|
||||
.any(|value| value.eq_ignore_ascii_case("upgrade"))
|
||||
});
|
||||
let has_websocket_upgrade = headers
|
||||
.get(http::header::UPGRADE)
|
||||
.and_then(|value| value.to_str().ok())
|
||||
.is_some_and(|value| value.eq_ignore_ascii_case("websocket"));
|
||||
|
||||
has_upgrade_connection && has_websocket_upgrade
|
||||
}
|
||||
|
||||
fn is_gemini_operation_method(method: &http::Method, normalized_path: &str) -> bool {
|
||||
method == http::Method::GET
|
||||
|| (method == http::Method::POST && normalized_path.ends_with(":cancel"))
|
||||
@@ -242,3 +263,32 @@ fn classify_antigravity_v1internal_route(
|
||||
execution_runtime_candidate,
|
||||
))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use axum::http::header::{CONNECTION, UPGRADE};
|
||||
use axum::http::{HeaderMap, HeaderValue, Method};
|
||||
|
||||
use super::classify_ai_public_route;
|
||||
|
||||
#[test]
|
||||
fn classifies_websocket_upgrade_on_responses_route() {
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert(CONNECTION, HeaderValue::from_static("keep-alive, Upgrade"));
|
||||
headers.insert(UPGRADE, HeaderValue::from_static("websocket"));
|
||||
|
||||
let route = classify_ai_public_route(&Method::GET, "/v1/responses", &headers)
|
||||
.expect("Responses WebSocket should be an AI public route");
|
||||
assert_eq!(route.route_class, "ai_public");
|
||||
assert_eq!(route.route_family, "openai");
|
||||
assert_eq!(route.route_kind, "responses");
|
||||
assert_eq!(route.auth_endpoint_signature, "openai:responses");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn does_not_classify_plain_get_as_responses_websocket() {
|
||||
assert!(
|
||||
classify_ai_public_route(&Method::GET, "/v1/responses", &HeaderMap::new()).is_none()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user