AI 网关切换 OpenAI 兼容面并移除 chat 端点,新增模型黑白名单
CI / test (push) Successful in 31s
Release / release (push) Successful in 54s

This commit is contained in:
2026-07-12 17:48:28 +08:00
parent 7706f59549
commit 489cb49cb3
34 changed files with 2602 additions and 2901 deletions
+25 -25
View File
@@ -3,6 +3,7 @@ package service
import (
"context"
"errors"
"io"
"math/rand"
"time"
@@ -26,24 +27,25 @@ type aiCandidate struct {
modelOcid string
}
// Chat 编排非流式调用:选渠道 → 调用 → 可重试错误换渠道(整请求上限 3 次)。
// group 非空时只在同分组渠道内路由(取自调用密钥)
func (s *AiGatewayService) Chat(ctx context.Context, ir aiwire.ChatRequest, group string) (*aiwire.ChatResponse, ChatMeta, error) {
// 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) {
meta := ChatMeta{}
excluded := map[uint]bool{}
var lastErr error
for attempt := 0; attempt < 3; attempt++ {
cand, err := s.pick(ctx, ir.Model, group, "CHAT", excluded)
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
resp, err := s.callOnce(ctx, cand, ir)
payload, err := s.passthroughOnce(ctx, cand, raw)
if err == nil {
s.markSuccess(ctx, cand.ch.ID)
return resp, meta, nil
return payload, meta, nil
}
retry, penalize := s.noteCallErr(ctx, cand, err)
retry, penalize := switchable(err)
if !retry {
return nil, meta, err
}
@@ -57,22 +59,22 @@ func (s *AiGatewayService) Chat(ctx context.Context, ir aiwire.ChatRequest, grou
return nil, meta, lastErr
}
func (s *AiGatewayService) callOnce(ctx context.Context, cand *aiCandidate, ir aiwire.ChatRequest) (*aiwire.ChatResponse, error) {
func (s *AiGatewayService) passthroughOnce(ctx context.Context, cand *aiCandidate, raw []byte) ([]byte, error) {
cred, err := s.configs.credentialsByID(ctx, cand.ch.OciConfigID)
if err != nil {
return nil, err
}
return s.client.GenAiChat(ctx, cred, cand.ch.Region, cand.modelOcid, ir)
return s.client.GenAiCompatResponses(ctx, cred, cand.ch.Region, raw)
}
// OpenStream 编排流式调用:流建立成功即绑定渠道,建立失败可换渠道重试。
// group 语义与 Chat 相同
func (s *AiGatewayService) OpenStream(ctx context.Context, ir aiwire.ChatRequest, group string) (oci.GenAiStream, ChatMeta, error) {
// 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, ir.Model, group, "CHAT", excluded)
cand, err := s.pick(ctx, modelName, group, "CHAT", excluded)
if err != nil {
return nil, meta, firstErr(lastErr, err)
}
@@ -81,12 +83,12 @@ func (s *AiGatewayService) OpenStream(ctx context.Context, ir aiwire.ChatRequest
if err != nil {
return nil, meta, err
}
stream, err := s.client.GenAiChatStream(ctx, cred, cand.ch.Region, cand.modelOcid, ir)
stream, err := s.client.GenAiCompatResponsesStream(ctx, cred, cand.ch.Region, raw)
if err == nil {
s.markSuccess(ctx, cand.ch.ID)
return stream, meta, nil
}
retry, penalize := s.noteCallErr(ctx, cand, err)
retry, penalize := switchable(err)
if !retry {
return nil, meta, err
}
@@ -108,12 +110,11 @@ func firstErr(lastErr, pickErr error) error {
return pickErr
}
// noteCallErr 汇总一次调用失败:模型级不可用(微调基座 400 / 实体不存在 404)先标记
// 剔除该 (渠道, 模型),换渠道重试且不计熔断——这是模型×区域供给问题而非渠道健康问题;
// 429 / 5xx / 网络错误换渠道并计熔断;其余 4xx 直接透传。
func (s *AiGatewayService) noteCallErr(ctx context.Context, cand *aiCandidate, err error) (retry, penalize bool) {
// switchable 判断调用失败是否换渠道重试、是否计入熔断:模型级不可用(微调基座
// 400 / 实体不存在 404)换渠道但不计熔断——这是模型×区域供给问题而非渠道健康
// 问题,持久解决靠加入模型黑名单;429 / 5xx / 网络错误换渠道并计熔断;其余 4xx 直接透传。
func switchable(err error) (retry, penalize bool) {
if oci.IsModelUnavailable(err) {
s.markModelUnusable(ctx, cand.ch.ID, cand.modelOcid, oci.CompactError(err))
return true, false
}
if status, ok := oci.ServiceStatus(err); ok {
@@ -154,11 +155,10 @@ func (s *AiGatewayService) pick(ctx context.Context, modelName, group, capabilit
return &aiCandidate{ch: chosen, modelOcid: ocids[chosen.ID]}, nil
}
// modelChannels 查出提供该模型的渠道 ID 及各自的模型 OCID(剔除不可按需调用标记);
// capability=CHAT 时兼容存量空串(加列前只同步对话模型);unusable 需兼容 NULL
// (AutoMigrate 加列后、首次重同步前的存量行)。
// modelChannels 查出提供该模型的渠道 ID 及各自的模型 OCID;
// capability=CHAT 时兼容存量空串(加列前只同步对话模型)
func (s *AiGatewayService) modelChannels(ctx context.Context, modelName, capability string) (map[uint]string, []uint, error) {
q := s.db.WithContext(ctx).Where("name = ? AND (unusable = ? OR unusable IS NULL)", modelName, false)
q := s.db.WithContext(ctx).Where("name = ?", modelName)
if capability == "CHAT" {
q = q.Where("capability IN ?", []string{"CHAT", ""})
} else {
@@ -234,7 +234,7 @@ func (s *AiGatewayService) Embeddings(ctx context.Context, req aiwire.Embeddings
s.markSuccess(ctx, cand.ch.ID)
return resp, meta, nil
}
retry, penalize := s.noteCallErr(ctx, cand, err)
retry, penalize := switchable(err)
if !retry {
return nil, meta, err
}