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:
AAEE86
2026-08-17 14:50:33 +08:00
committed by ZheFox
parent 9a0d346ff3
commit 71b54070e8
72 changed files with 10441 additions and 108 deletions
+11 -3
View File
@@ -1,13 +1,17 @@
use axum::body::Body;
use axum::extract::Request;
use axum::http::{header, HeaderValue, Response, StatusCode};
use axum::routing::{any, post};
use axum::routing::{any, get, post};
use axum::Router;
use super::{aliyun, claude, doubao, gemini, jina, openai};
use crate::api::response::build_local_http_error_response_with_request_path;
use crate::headers::extract_or_generate_trace_id;
use crate::{handlers::proxy::proxy_request, state::AppState, GatewayError};
use crate::{
handlers::proxy::{proxy_request, responses_websocket},
state::AppState,
GatewayError,
};
// Router registration patterns live here so AI public ingress has a single mount registry.
// They intentionally stay separate from manifest-facing route inventories in constants.rs,
@@ -51,7 +55,11 @@ const AI_ANY_ROUTE_PATTERNS: &[&str] = &[
pub(crate) fn mount_ai_routes(mut router: Router<AppState>) -> Router<AppState> {
for path in AI_POST_ROUTE_PATTERNS {
router = router.route(path, post(proxy_request));
router = if *path == "/v1/responses" {
router.route(path, get(responses_websocket).post(proxy_request))
} else {
router.route(path, post(proxy_request))
};
}
for path in CLAUDE_POST_ROUTE_PATTERNS {
router = router.route(
+34
View File
@@ -50,12 +50,40 @@ pub(crate) async fn health(State(state): State<AppState>) -> impl IntoResponse {
"rejected": snapshot.rejected,
})
});
let websocket_connection_concurrency =
state
.websocket_connection_concurrency_snapshot()
.map(|snapshot| {
json!({
"limit": snapshot.limit,
"in_flight": snapshot.in_flight,
"available_permits": snapshot.available_permits,
"high_watermark": snapshot.high_watermark,
"rejected": snapshot.rejected,
})
});
let distributed_websocket_connection_concurrency = state
.distributed_websocket_connection_concurrency_snapshot()
.await
.ok()
.flatten()
.map(|snapshot| {
json!({
"limit": snapshot.limit,
"in_flight": snapshot.in_flight,
"available_permits": snapshot.available_permits,
"high_watermark": snapshot.high_watermark,
"rejected": snapshot.rejected,
})
});
Json(json!({
"status": "ok",
"component": "aether-gateway",
"control_api_enabled": true,
"request_concurrency": request_concurrency,
"distributed_request_concurrency": distributed_request_concurrency,
"websocket_connection_concurrency": websocket_connection_concurrency,
"distributed_websocket_connection_concurrency": distributed_websocket_connection_concurrency,
}))
}
@@ -113,6 +141,12 @@ pub(crate) async fn frontdoor_manifest(State(state): State<AppState>) -> impl In
"execution_runtime_configured": state.execution_runtime_configured(),
"request_concurrency_enabled": state.request_concurrency_snapshot().is_some(),
"distributed_request_concurrency_enabled": state.distributed_request_gate.is_some(),
"websocket_connection_concurrency_enabled": state
.websocket_connection_concurrency_snapshot()
.is_some(),
"distributed_websocket_connection_concurrency_enabled": state
.distributed_websocket_connection_gate
.is_some(),
"frontdoor_cors_enabled": cors_enabled,
"frontdoor_cors_allow_credentials": cors_allow_credentials,
"frontdoor_cors_allowed_origins": cors_allowed_origins,