修复跨区间缓存串数据与会话回收泄漏,收敛网关重试
CI / test (push) Successful in 29s

This commit is contained in:
2026-07-17 12:19:21 +08:00
parent 7019d4c5a6
commit 882eeade1e
6 changed files with 121 additions and 68 deletions
+26 -64
View File
@@ -27,27 +27,27 @@ type aiCandidate struct {
modelOcid string
}
// RespPassthrough 编排一次非流式直通调用:选渠道(priority→加权随机)→ 调用 →
// 可重试错误换渠道(整请求上限 3 次)并维护熔断;group 非空时只在同分组渠道内路由。
// 上游为 OpenAI-compatible /actions/v1/responses(实测可用,无 Oracle 文档合同)。
func (s *AiGatewayService) RespPassthrough(ctx context.Context, raw []byte, modelName, group string) ([]byte, ChatMeta, error) {
// routeRetry 统一编排「选渠道(priority→加权随机)→ 调用 → 可重试错误换渠道」,
// 整请求上限 3 次并维护熔断;group 非空时只在同分组渠道内路由。
func routeRetry[T any](ctx context.Context, s *AiGatewayService, modelName, group, capability string, once func(*aiCandidate) (T, error)) (T, ChatMeta, error) {
var zero T
meta := ChatMeta{}
excluded := map[uint]bool{}
var lastErr error
for attempt := 0; attempt < 3; attempt++ {
cand, err := s.pick(ctx, modelName, group, "CHAT", excluded)
cand, err := s.pick(ctx, modelName, group, capability, excluded)
if err != nil {
return nil, meta, firstErr(lastErr, err)
return zero, meta, firstErr(lastErr, err)
}
meta.ChannelID, meta.ChannelName = cand.ch.ID, cand.ch.Name
payload, err := s.passthroughOnce(ctx, cand, raw)
out, err := once(cand)
if err == nil {
s.markSuccess(ctx, cand.ch.ID)
return payload, meta, nil
return out, meta, nil
}
retry, penalize := switchable(err)
if !retry {
return nil, meta, err
return zero, meta, err
}
if penalize {
s.markFailure(ctx, cand.ch.ID)
@@ -56,7 +56,15 @@ func (s *AiGatewayService) RespPassthrough(ctx context.Context, raw []byte, mode
meta.Retries++
lastErr = err
}
return nil, meta, lastErr
return zero, meta, lastErr
}
// RespPassthrough 编排一次非流式直通调用。
// 上游为 OpenAI-compatible /actions/v1/responses(实测可用,无 Oracle 文档合同)。
func (s *AiGatewayService) RespPassthrough(ctx context.Context, raw []byte, modelName, group string) ([]byte, ChatMeta, error) {
return routeRetry(ctx, s, modelName, group, "CHAT", func(cand *aiCandidate) ([]byte, error) {
return s.passthroughOnce(ctx, cand, raw)
})
}
func (s *AiGatewayService) passthroughOnce(ctx context.Context, cand *aiCandidate, raw []byte) ([]byte, error) {
@@ -70,36 +78,13 @@ func (s *AiGatewayService) passthroughOnce(ctx context.Context, cand *aiCandidat
// RespPassthroughStream 编排流式直通:流建立成功即绑定渠道,建立失败按 switchable
// 换渠道重试;建立后的中断不重试、不计熔断(与 OpenStream 语义一致)。
func (s *AiGatewayService) RespPassthroughStream(ctx context.Context, raw []byte, modelName, group string) (io.ReadCloser, ChatMeta, error) {
meta := ChatMeta{}
excluded := map[uint]bool{}
var lastErr error
for attempt := 0; attempt < 3; attempt++ {
cand, err := s.pick(ctx, modelName, group, "CHAT", excluded)
if err != nil {
return nil, meta, firstErr(lastErr, err)
}
meta.ChannelID, meta.ChannelName = cand.ch.ID, cand.ch.Name
return routeRetry(ctx, s, modelName, group, "CHAT", func(cand *aiCandidate) (io.ReadCloser, error) {
cred, err := s.configs.credentialsByID(ctx, cand.ch.OciConfigID)
if err != nil {
return nil, meta, err
return nil, err
}
stream, err := s.client.GenAiCompatResponsesStream(ctx, cred, cand.ch.Region, raw, s.UpstreamWait())
if err == nil {
s.markSuccess(ctx, cand.ch.ID)
return stream, meta, nil
}
retry, penalize := switchable(err)
if !retry {
return nil, meta, err
}
if penalize {
s.markFailure(ctx, cand.ch.ID)
}
excluded[cand.ch.ID] = true
meta.Retries++
lastErr = err
}
return nil, meta, lastErr
return s.client.GenAiCompatResponsesStream(ctx, cred, cand.ch.Region, raw, s.UpstreamWait())
})
}
// firstErr 在换渠道后仍失败时优先返回上游错误(而非「无渠道」)。
@@ -221,34 +206,11 @@ func weightedPick(chs []model.AiChannel) model.AiChannel {
return chs[len(chs)-1]
}
// Embeddings 编排向量化调用:按 EMBEDDING 能力选渠道,可重试错误换渠道(整请求上限 3 次)
// Embeddings 编排向量化调用:按 EMBEDDING 能力选渠道,可重试错误换渠道。
func (s *AiGatewayService) Embeddings(ctx context.Context, req aiwire.EmbeddingsRequest, group string) (*aiwire.EmbeddingsResponse, ChatMeta, error) {
meta := ChatMeta{}
excluded := map[uint]bool{}
var lastErr error
for attempt := 0; attempt < 3; attempt++ {
cand, err := s.pick(ctx, req.Model, group, "EMBEDDING", excluded)
if err != nil {
return nil, meta, firstErr(lastErr, err)
}
meta.ChannelID, meta.ChannelName = cand.ch.ID, cand.ch.Name
resp, err := s.embedOnce(ctx, cand, req)
if err == nil {
s.markSuccess(ctx, cand.ch.ID)
return resp, meta, nil
}
retry, penalize := switchable(err)
if !retry {
return nil, meta, err
}
if penalize {
s.markFailure(ctx, cand.ch.ID)
}
excluded[cand.ch.ID] = true
meta.Retries++
lastErr = err
}
return nil, meta, lastErr
return routeRetry(ctx, s, req.Model, group, "EMBEDDING", func(cand *aiCandidate) (*aiwire.EmbeddingsResponse, error) {
return s.embedOnce(ctx, cand, req)
})
}
// embedOnce 调用渠道向量化并装配 OpenAI 形态响应。