渠道支持多 API 格式 + 协议全名展示

- Channel 新增 formats(jsonb: chat|responses|messages), 一个渠道可同时声明
  支持 OpenAI Chat Completions / OpenAI Responses API / Anthropic Messages
- 路由改为按渠道声明的 formats 决定直通/转换: 客户端协议在 formats 内直通,
  否则转换为其首选支持格式(chat > messages > responses)
- 兼容旧数据: formats 为空时按 provider 推断(openai→chat+responses, anthropic→messages, compatible→chat)
- 前端: 渠道表/表单展示 API 格式(支持多选), usage 明细显示协议全名
  (OpenAI Chat Completions / OpenAI Responses API / Anthropic Messages)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Sakurasan
2026-08-15 16:47:03 +08:00
co-authored by Claude
parent 6adadebaf0
commit 5eff32afd8
9 changed files with 200 additions and 57 deletions
+30 -23
View File
@@ -4,6 +4,7 @@ package proxy
import (
"errors"
"fmt"
"net/http"
"strings"
"time"
@@ -175,21 +176,6 @@ func (g *Gateway) checkBalance(c *gin.Context, u *store.User) bool {
// ---------------------------------------------------------------------------
// 协议分派
// upstreamProtoFor 根据渠道 provider 与客户端协议确定上游协议与路径。
func upstreamProtoFor(provider, clientProto string) string {
switch provider {
case store.ChannelProviderAnthropic:
return convert.ProtoMessages
case store.ChannelProviderOpenAI:
if clientProto == convert.ProtoMessages {
return convert.ProtoChat
}
return clientProto
default: // compatible:假定 OpenAI Chat 形状
return convert.ProtoChat
}
}
func upstreamPath(proto string) string {
switch proto {
case convert.ProtoMessages:
@@ -209,18 +195,39 @@ type upstreamPlan struct {
bodyConv func([]byte) ([]byte, error) // 非流式响应体转换(nil=直通)
}
// prepareUpstream 计算上游访问计划:协议匹配直通,否则转换。
func prepareUpstream(provider, clientProto string, body []byte) (*upstreamPlan, error) {
up := upstreamProtoFor(provider, clientProto)
plan := &upstreamPlan{path: upstreamPath(up), body: body}
if up != clientProto {
converted, err := convert.ConvertRequest(body, clientProto, up)
// conversionTarget 决定客户端协议在渠道上的处理方式:
// 渠道声明支持该协议则直通(返回原协议);否则转为其首选支持协议(chat > messages > responses)。
func conversionTarget(formats []string, clientProto string) string {
for _, f := range formats {
if f == clientProto {
return clientProto
}
}
for _, p := range []string{convert.ProtoChat, convert.ProtoMessages, convert.ProtoResponses} {
for _, f := range formats {
if f == p {
return p
}
}
}
return ""
}
// prepareUpstream 计算上游访问计划:渠道声明支持客户端协议则直通,否则转换。
func prepareUpstream(ch *store.Channel, clientProto string, body []byte) (*upstreamPlan, error) {
target := conversionTarget(ch.FormatsEffective(), clientProto)
if target == "" {
return nil, fmt.Errorf("channel %q declares no supported protocol format", ch.Name)
}
plan := &upstreamPlan{path: upstreamPath(target), body: body}
if target != clientProto {
converted, err := convert.ConvertRequest(body, clientProto, target)
if err != nil {
return nil, err
}
plan.body = converted
plan.lineConv = convert.NewStreamTransformer(up, clientProto)
plan.bodyConv = func(b []byte) ([]byte, error) { return convert.ConvertResponse(b, up, clientProto) }
plan.lineConv = convert.NewStreamTransformer(target, clientProto)
plan.bodyConv = func(b []byte) ([]byte, error) { return convert.ConvertResponse(b, target, clientProto) }
}
return plan, nil
}
+1 -1
View File
@@ -53,7 +53,7 @@ func (g *Gateway) doProxy(c *gin.Context, cands []*store.Channel, clientProto st
var lastStatus = http.StatusBadGateway
var lastBody = []byte("all upstream channels failed")
for _, ch := range cands {
plan, err := prepareUpstream(ch.Provider, clientProto, body)
plan, err := prepareUpstream(ch, clientProto, body)
if err != nil {
lastStatus, lastBody = http.StatusInternalServerError, []byte("conversion error: "+err.Error())
continue