mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-07 18:07:47 +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:
@@ -2,7 +2,10 @@ use std::collections::BTreeMap;
|
||||
|
||||
use aether_ai_formats::UPSTREAM_IS_STREAM_KEY;
|
||||
use aether_contracts::{ExecutionPlan, ExecutionTelemetry};
|
||||
use aether_data_contracts::repository::usage::{UpsertUsageRecord, UsageBodyCaptureState};
|
||||
use aether_data_contracts::repository::usage::{
|
||||
UpsertUsageRecord, UsageBodyCaptureState, WEBSOCKET_MODE_METADATA_KEY,
|
||||
WEBSOCKET_TRANSPORT_METADATA_KEY,
|
||||
};
|
||||
use aether_data_contracts::DataLayerError;
|
||||
use base64::Engine as _;
|
||||
use serde_json::{json, Map, Value};
|
||||
@@ -2114,6 +2117,18 @@ fn build_runtime_request_metadata_seed_from_parts(
|
||||
Value::Bool(api_key_is_standalone),
|
||||
);
|
||||
}
|
||||
if let Some(websocket_mode) = context_bool(context, WEBSOCKET_MODE_METADATA_KEY) {
|
||||
metadata.insert(
|
||||
WEBSOCKET_MODE_METADATA_KEY.to_string(),
|
||||
Value::Bool(websocket_mode),
|
||||
);
|
||||
}
|
||||
if let Some(websocket_transport) = context_string(context, WEBSOCKET_TRANSPORT_METADATA_KEY) {
|
||||
metadata.insert(
|
||||
WEBSOCKET_TRANSPORT_METADATA_KEY.to_string(),
|
||||
Value::String(websocket_transport),
|
||||
);
|
||||
}
|
||||
let provider_source_bytes = provider_request_body_base64.and_then(decoded_base64_len_hint);
|
||||
append_runtime_body_capture_metadata(
|
||||
&mut metadata,
|
||||
@@ -3763,6 +3778,8 @@ mod tests {
|
||||
Some(&json!({
|
||||
"candidate_id": "cand-pending-event-1",
|
||||
"candidate_index": 3,
|
||||
"websocket_mode": true,
|
||||
"websocket_transport": "responses",
|
||||
"original_request_body": {"messages": [{"content": "omit me"}]},
|
||||
"provider_request_body": {
|
||||
"input": [{"type": "compaction_trigger"}]
|
||||
@@ -3785,6 +3802,22 @@ mod tests {
|
||||
assert!(record.provider_request_body.is_none());
|
||||
assert_eq!(record.candidate_id.as_deref(), Some("cand-pending-event-1"));
|
||||
assert_eq!(record.candidate_index, Some(3));
|
||||
assert_eq!(
|
||||
record
|
||||
.request_metadata
|
||||
.as_ref()
|
||||
.and_then(Value::as_object)
|
||||
.and_then(|metadata| metadata.get("websocket_mode")),
|
||||
Some(&json!(true))
|
||||
);
|
||||
assert_eq!(
|
||||
record
|
||||
.request_metadata
|
||||
.as_ref()
|
||||
.and_then(Value::as_object)
|
||||
.and_then(|metadata| metadata.get("websocket_transport")),
|
||||
Some(&json!("responses"))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user