mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-09 02:47:45 +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:
@@ -37,4 +37,5 @@ pub use types::{
|
||||
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,
|
||||
WEBSOCKET_MODE_METADATA_KEY, WEBSOCKET_TRANSPORT_METADATA_KEY,
|
||||
};
|
||||
|
||||
@@ -9,6 +9,8 @@ pub const PROVIDER_ACTUAL_SERVICE_TIER_METADATA_KEY: &str = "provider_actual_ser
|
||||
pub const PROVIDER_CACHE_TTL_MINUTES_METADATA_KEY: &str = "provider_cache_ttl_minutes";
|
||||
pub const ROUTING_CANDIDATE_SKIP_REASON_METADATA_KEY: &str = "routing_candidate_skip_reason";
|
||||
pub const ROUTING_FAILURE_DIAGNOSTIC_METADATA_KEY: &str = "routing_failure_diagnostic";
|
||||
pub const WEBSOCKET_MODE_METADATA_KEY: &str = "websocket_mode";
|
||||
pub const WEBSOCKET_TRANSPORT_METADATA_KEY: &str = "websocket_transport";
|
||||
|
||||
pub fn extract_provider_reasoning_effort_from_body(value: Option<&Value>) -> Option<String> {
|
||||
let object = value.and_then(Value::as_object)?;
|
||||
@@ -539,6 +541,11 @@ impl StoredRequestUsageAudit {
|
||||
usage_request_metadata_client_family(self.request_metadata.as_ref())
|
||||
}
|
||||
|
||||
pub fn is_websocket(&self) -> bool {
|
||||
self.request_metadata_bool(WEBSOCKET_MODE_METADATA_KEY)
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn billing_snapshot_resolved_number(&self, key: &str) -> Option<f64> {
|
||||
self.request_metadata_object()
|
||||
.and_then(|metadata| metadata.get("billing_snapshot"))
|
||||
@@ -2394,7 +2401,8 @@ mod tests {
|
||||
extract_provider_actual_service_tier_from_response,
|
||||
extract_provider_service_tier_from_body, resolve_provider_cache_ttl_minutes,
|
||||
StoredRequestUsageAudit, UpsertUsageRecord, UsageBodyCaptureState, UsageBodyCaptureStorage,
|
||||
UsageBodyField, UsageProviderPerformanceQuery,
|
||||
UsageBodyField, UsageProviderPerformanceQuery, WEBSOCKET_MODE_METADATA_KEY,
|
||||
WEBSOCKET_TRANSPORT_METADATA_KEY,
|
||||
};
|
||||
use serde_json::{json, Value};
|
||||
|
||||
@@ -2663,6 +2671,19 @@ mod tests {
|
||||
assert_eq!(usage.settlement_price_per_request(), Some(0.02));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn websocket_transport_uses_typed_request_metadata() {
|
||||
let mut usage = sample_usage();
|
||||
assert!(!usage.is_websocket());
|
||||
|
||||
usage.request_metadata = Some(json!({
|
||||
WEBSOCKET_MODE_METADATA_KEY: true,
|
||||
WEBSOCKET_TRANSPORT_METADATA_KEY: "responses",
|
||||
}));
|
||||
|
||||
assert!(usage.is_websocket());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn settlement_accessors_fall_back_to_billing_snapshot_and_legacy_output_price() {
|
||||
let mut usage = sample_usage();
|
||||
|
||||
Reference in New Issue
Block a user