AI 网关切换 OpenAI 兼容面并移除 chat 端点,新增模型黑白名单
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user