mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-08 10: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:
@@ -38,6 +38,7 @@ pub struct PoolMemberSignals {
|
||||
pub quota_reset_seconds: Option<f64>,
|
||||
pub account_blocked: bool,
|
||||
pub quota_exhausted: bool,
|
||||
pub quota_hard_blocked: bool,
|
||||
pub health_score: Option<f64>,
|
||||
pub latency_avg_ms: Option<f64>,
|
||||
pub catalog_lru_score: Option<f64>,
|
||||
@@ -224,7 +225,9 @@ fn schedule_pool_group<Candidate>(
|
||||
continue;
|
||||
}
|
||||
|
||||
if pool_config.skip_exhausted_accounts && item.key_context.quota_exhausted {
|
||||
if item.key_context.quota_hard_blocked
|
||||
|| (pool_config.skip_exhausted_accounts && item.key_context.quota_exhausted)
|
||||
{
|
||||
skipped.push(PoolSkippedCandidate {
|
||||
candidate: item.candidate,
|
||||
skip_reason: POOL_ACCOUNT_EXHAUSTED_SKIP_REASON,
|
||||
@@ -901,6 +904,30 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pool_scheduler_always_skips_hard_quota_blocks() {
|
||||
let ready = sample_candidate("provider-pool", "endpoint-1", "key-ready", 10, true);
|
||||
let mut hard_blocked =
|
||||
sample_candidate("provider-pool", "endpoint-1", "key-blocked", 10, true);
|
||||
hard_blocked.key_context.quota_exhausted = true;
|
||||
hard_blocked.key_context.quota_hard_blocked = true;
|
||||
|
||||
let outcome = run_pool_scheduler(vec![ready, hard_blocked], &BTreeMap::new(), "seed");
|
||||
|
||||
assert_eq!(
|
||||
outcome
|
||||
.candidates
|
||||
.iter()
|
||||
.map(|item| item.candidate.as_str())
|
||||
.collect::<Vec<_>>(),
|
||||
vec!["key-ready"]
|
||||
);
|
||||
assert_eq!(
|
||||
outcome.skipped_candidates[0].skip_reason,
|
||||
POOL_ACCOUNT_EXHAUSTED_SKIP_REASON
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pool_scheduler_promotes_sticky_hit_before_other_sorted_keys() {
|
||||
let key_a = sample_candidate("provider-pool", "endpoint-1", "key-a", 10, true)
|
||||
|
||||
Reference in New Issue
Block a user