M5: 渠道体系(负载均衡+并发控制+健康检查+故障转移)

- channel.Candidates 按模型绑定取候选 + Pick 加权随机负载均衡
- TryAcquire 每渠道并发信号量, 满载溢出到其他候选
- HealthMonitor 后台定时探测, 连续失败进 cooldown, 恢复放回(可配 interval/threshold)
- doProxy 遍历候选故障转移: 网络错误/429/5xx/超时且未写出响应头时安全重试;
  400 等业务错误透传, 流式写出首字节后放弃重试
- 单测覆盖候选过滤/加权/并发/健康状态机
- E2E: 杀上游自动切换、cooldown、恢复、并发溢出 10/10

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Sakurasan
2026-08-15 15:57:48 +08:00
co-authored by Claude
parent ec4de8d913
commit 0637ce0a51
11 changed files with 454 additions and 97 deletions
+9 -24
View File
@@ -24,20 +24,15 @@ func (g *Gateway) chatCompletions(c *gin.Context) {
c.Set("protocol", convert.ProtoChat)
c.Set("model_name", br.Model)
ch, err := g.selectChannel(c, br.Model)
if err != nil {
cands := g.candidateChannels(br.Model)
if len(cands) == 0 {
apiError(c, http.StatusServiceUnavailable, "no_channel", "No available upstream channel")
g.recordError(c, nil, nil, now(), "no_channel")
return
}
plan, err := prepareUpstream(ch.Provider, convert.ProtoChat, body)
if err != nil {
apiError(c, http.StatusInternalServerError, "conversion_error", "Failed to convert request: "+err.Error())
return
}
sink := &usageSink{}
c.Set("usage_raw", &sinkHolder{sink: sink})
g.doProxy(c, ch, plan, br.Stream, sink)
g.doProxy(c, cands, convert.ProtoChat, body, br.Stream, sink)
}
// responses POST /v1/responses(OpenAI Responses API)
@@ -57,20 +52,15 @@ func (g *Gateway) responses(c *gin.Context) {
c.Set("protocol", convert.ProtoResponses)
c.Set("model_name", br.Model)
ch, err := g.selectChannel(c, br.Model)
if err != nil {
cands := g.candidateChannels(br.Model)
if len(cands) == 0 {
apiError(c, http.StatusServiceUnavailable, "no_channel", "No available upstream channel")
g.recordError(c, nil, nil, now(), "no_channel")
return
}
plan, err := prepareUpstream(ch.Provider, convert.ProtoResponses, body)
if err != nil {
apiError(c, http.StatusInternalServerError, "conversion_error", "Failed to convert request: "+err.Error())
return
}
sink := &usageSink{}
c.Set("usage_raw", &sinkHolder{sink: sink})
g.doProxy(c, ch, plan, br.Stream, sink)
g.doProxy(c, cands, convert.ProtoResponses, body, br.Stream, sink)
}
// messages POST /v1/messages(Anthropic Messages API)
@@ -90,20 +80,15 @@ func (g *Gateway) messages(c *gin.Context) {
c.Set("protocol", convert.ProtoMessages)
c.Set("model_name", br.Model)
ch, err := g.selectChannel(c, br.Model)
if err != nil {
cands := g.candidateChannels(br.Model)
if len(cands) == 0 {
apiError(c, http.StatusServiceUnavailable, "no_channel", "No available upstream channel")
g.recordError(c, nil, nil, now(), "no_channel")
return
}
plan, err := prepareUpstream(ch.Provider, convert.ProtoMessages, body)
if err != nil {
apiError(c, http.StatusInternalServerError, "conversion_error", "Failed to convert request: "+err.Error())
return
}
sink := &usageSink{}
c.Set("usage_raw", &sinkHolder{sink: sink})
g.doProxy(c, ch, plan, br.Stream, sink)
g.doProxy(c, cands, convert.ProtoMessages, body, br.Stream, sink)
}
// usageSinkHolder 桥接:gin context 里保存 sink 引用,供 finishUsage 读取最终 usage。