mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-04 08:27: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:
@@ -967,6 +967,12 @@ fn build_codex_quota_status_snapshot(
|
||||
let credits_unlimited = metadata
|
||||
.get("credits_unlimited")
|
||||
.and_then(admin_provider_quota_pure::coerce_json_bool);
|
||||
let allowed = metadata
|
||||
.get("allowed")
|
||||
.and_then(admin_provider_quota_pure::coerce_json_bool);
|
||||
let limit_reached = metadata
|
||||
.get("limit_reached")
|
||||
.and_then(admin_provider_quota_pure::coerce_json_bool);
|
||||
let reset_credits = build_codex_reset_credits_status_snapshot(metadata, observed_at_unix_secs);
|
||||
|
||||
let windows = [
|
||||
@@ -996,6 +1002,8 @@ fn build_codex_quota_status_snapshot(
|
||||
&& credits_has_credits.is_none()
|
||||
&& credits_balance.is_none()
|
||||
&& credits_unlimited.is_none()
|
||||
&& allowed.is_none()
|
||||
&& limit_reached.is_none()
|
||||
&& reset_credits.is_none()
|
||||
&& observed_at_unix_secs.is_none()
|
||||
{
|
||||
@@ -1031,11 +1039,19 @@ fn build_codex_quota_status_snapshot(
|
||||
.filter_map(admin_provider_quota_pure::coerce_json_u64)
|
||||
.min();
|
||||
let reset_at = quota_windows_min_reset_at(&primary_windows);
|
||||
let exhausted_by_credits = primary_windows.is_empty()
|
||||
let explicitly_blocked = allowed == Some(false) || limit_reached == Some(true);
|
||||
let explicitly_available =
|
||||
!explicitly_blocked && (allowed == Some(true) || limit_reached == Some(false));
|
||||
let exhausted_by_credits = !explicitly_available
|
||||
&& primary_windows.is_empty()
|
||||
&& credits_unlimited != Some(true)
|
||||
&& credits_has_credits == Some(false);
|
||||
let exhausted_by_window = usage_ratio.is_some_and(|value| value >= 1.0 - 1e-6);
|
||||
let exhausted = exhausted_by_credits || exhausted_by_window;
|
||||
let exhausted_by_window =
|
||||
!explicitly_available && usage_ratio.is_some_and(|value| value >= 1.0 - 1e-6);
|
||||
let exhausted_by_signal = admin_provider_quota_pure::codex_rate_limit_metadata_exhausted(
|
||||
&Value::Object(metadata.clone()),
|
||||
);
|
||||
let exhausted = exhausted_by_signal || exhausted_by_credits || exhausted_by_window;
|
||||
|
||||
let mut credits = Map::new();
|
||||
if let Some(value) = credits_has_credits {
|
||||
@@ -1048,7 +1064,9 @@ fn build_codex_quota_status_snapshot(
|
||||
credits.insert("unlimited".to_string(), json!(value));
|
||||
}
|
||||
|
||||
let reason = if exhausted_by_credits {
|
||||
let reason = if exhausted_by_signal {
|
||||
Some("上游已拒绝继续使用该账号")
|
||||
} else if exhausted_by_credits {
|
||||
Some("无可用积分")
|
||||
} else if exhausted_by_window {
|
||||
Some("额度窗口已耗尽")
|
||||
@@ -1071,6 +1089,8 @@ fn build_codex_quota_status_snapshot(
|
||||
"reset_at": reset_at,
|
||||
"reset_seconds": reset_seconds,
|
||||
"plan_type": plan_type,
|
||||
"allowed": allowed,
|
||||
"limit_reached": limit_reached,
|
||||
"credits": if credits.is_empty() {
|
||||
Value::Null
|
||||
} else {
|
||||
@@ -3833,6 +3853,28 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sync_provider_key_quota_status_snapshot_honors_codex_limit_signal() {
|
||||
let payload = sync_provider_key_quota_status_snapshot(
|
||||
None,
|
||||
"codex",
|
||||
Some(&json!({
|
||||
"codex": {
|
||||
"updated_at": 1_775_800_000u64,
|
||||
"allowed": false,
|
||||
"limit_reached": true
|
||||
}
|
||||
})),
|
||||
"websocket_response_body",
|
||||
)
|
||||
.expect("explicit Codex limit signal should build a quota snapshot");
|
||||
|
||||
assert_eq!(payload.pointer("/quota/code"), Some(&json!("exhausted")));
|
||||
assert_eq!(payload.pointer("/quota/exhausted"), Some(&json!(true)));
|
||||
assert_eq!(payload.pointer("/quota/allowed"), Some(&json!(false)));
|
||||
assert_eq!(payload.pointer("/quota/limit_reached"), Some(&json!(true)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sync_provider_key_quota_status_snapshot_drops_codex_usage_state_when_window_resets() {
|
||||
let current_status_snapshot = json!({
|
||||
|
||||
Reference in New Issue
Block a user