渠道支持多 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:
@@ -33,7 +33,7 @@ func (h *Handler) AdminChannels(c *gin.Context) {
|
||||
masked = "****"
|
||||
}
|
||||
out = append(out, gin.H{
|
||||
"id": ch.ID, "name": ch.Name, "provider": ch.Provider, "base_url": ch.BaseURL,
|
||||
"id": ch.ID, "name": ch.Name, "provider": ch.Provider, "formats": ch.FormatsEffective(), "base_url": ch.BaseURL,
|
||||
"api_key_masked": masked, "weight": ch.Weight, "priority": ch.Priority,
|
||||
"timeout_ms": ch.TimeoutMS, "max_concurrency": ch.MaxConcurrency,
|
||||
"health_status": ch.HealthStatus, "enabled": ch.Enabled,
|
||||
@@ -44,21 +44,52 @@ func (h *Handler) AdminChannels(c *gin.Context) {
|
||||
}
|
||||
|
||||
type channelBody struct {
|
||||
Name string `json:"name" binding:"required,min=1,max=64"`
|
||||
Provider string `json:"provider" binding:"required"`
|
||||
BaseURL string `json:"base_url" binding:"required"`
|
||||
APIKey string `json:"api_key"`
|
||||
Weight *int `json:"weight"`
|
||||
Priority *int `json:"priority"`
|
||||
TimeoutMS *int `json:"timeout_ms"`
|
||||
MaxConcurrency *int `json:"max_concurrency"`
|
||||
Enabled *bool `json:"enabled"`
|
||||
Name string `json:"name" binding:"required,min=1,max=64"`
|
||||
Provider string `json:"provider" binding:"required"`
|
||||
Formats []string `json:"formats"` // 原生支持的协议 chat|responses|messages,空则按 provider 推断
|
||||
BaseURL string `json:"base_url" binding:"required"`
|
||||
APIKey string `json:"api_key"`
|
||||
Weight *int `json:"weight"`
|
||||
Priority *int `json:"priority"`
|
||||
TimeoutMS *int `json:"timeout_ms"`
|
||||
MaxConcurrency *int `json:"max_concurrency"`
|
||||
Enabled *bool `json:"enabled"`
|
||||
}
|
||||
|
||||
func validateProvider(p string) bool {
|
||||
return p == store.ChannelProviderOpenAI || p == store.ChannelProviderAnthropic || p == store.ChannelProviderCompatible
|
||||
}
|
||||
|
||||
var validFormats = map[string]bool{
|
||||
store.FormatChat: true, store.FormatResponses: true, store.FormatMessages: true,
|
||||
}
|
||||
|
||||
// resolveFormats 渠道协议格式:显式给出则校验去重;空则按 provider 推断默认。
|
||||
func resolveFormats(provider string, formats []string) ([]string, error) {
|
||||
if len(formats) == 0 {
|
||||
switch provider {
|
||||
case store.ChannelProviderAnthropic:
|
||||
return []string{store.FormatMessages}, nil
|
||||
case store.ChannelProviderOpenAI:
|
||||
return []string{store.FormatChat, store.FormatResponses}, nil
|
||||
default:
|
||||
return []string{store.FormatChat}, nil
|
||||
}
|
||||
}
|
||||
seen := map[string]bool{}
|
||||
out := make([]string, 0, len(formats))
|
||||
for _, f := range formats {
|
||||
if !validFormats[f] {
|
||||
return nil, fmt.Errorf("unsupported format %q", f)
|
||||
}
|
||||
if !seen[f] {
|
||||
seen[f] = true
|
||||
out = append(out, f)
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// AdminCreateChannel POST /api/v1/admin/channels
|
||||
func (h *Handler) AdminCreateChannel(c *gin.Context) {
|
||||
var req channelBody
|
||||
@@ -74,13 +105,18 @@ func (h *Handler) AdminCreateChannel(c *gin.Context) {
|
||||
resp.Fail(c, http.StatusBadRequest, "api_key required")
|
||||
return
|
||||
}
|
||||
formats, err := resolveFormats(req.Provider, req.Formats)
|
||||
if err != nil {
|
||||
resp.Fail(c, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
enc, err := h.a.Enc.Encrypt(req.APIKey)
|
||||
if err != nil {
|
||||
resp.Fail(c, http.StatusInternalServerError, "failed to encrypt api key")
|
||||
return
|
||||
}
|
||||
ch := store.Channel{
|
||||
Name: req.Name, Provider: req.Provider, BaseURL: strings.TrimRight(req.BaseURL, "/"),
|
||||
Name: req.Name, Provider: req.Provider, Formats: formats, BaseURL: strings.TrimRight(req.BaseURL, "/"),
|
||||
APIKeyEnc: enc, Weight: intOr(req.Weight, 1), Priority: intOr(req.Priority, 0),
|
||||
TimeoutMS: intOr(req.TimeoutMS, 120000), MaxConcurrency: intOr(req.MaxConcurrency, 16),
|
||||
HealthStatus: store.ChannelHealthHealthy, Enabled: boolOr(req.Enabled, true),
|
||||
@@ -100,16 +136,17 @@ func (h *Handler) AdminUpdateChannel(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
var body struct {
|
||||
Name *string `json:"name"`
|
||||
Provider *string `json:"provider"`
|
||||
BaseURL *string `json:"base_url"`
|
||||
APIKey *string `json:"api_key"`
|
||||
Weight *int `json:"weight"`
|
||||
Priority *int `json:"priority"`
|
||||
TimeoutMS *int `json:"timeout_ms"`
|
||||
MaxConcurrency *int `json:"max_concurrency"`
|
||||
HealthStatus *string `json:"health_status"`
|
||||
Enabled *bool `json:"enabled"`
|
||||
Name *string `json:"name"`
|
||||
Provider *string `json:"provider"`
|
||||
Formats *[]string `json:"formats"`
|
||||
BaseURL *string `json:"base_url"`
|
||||
APIKey *string `json:"api_key"`
|
||||
Weight *int `json:"weight"`
|
||||
Priority *int `json:"priority"`
|
||||
TimeoutMS *int `json:"timeout_ms"`
|
||||
MaxConcurrency *int `json:"max_concurrency"`
|
||||
HealthStatus *string `json:"health_status"`
|
||||
Enabled *bool `json:"enabled"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
resp.Fail(c, http.StatusBadRequest, "invalid input")
|
||||
@@ -160,11 +197,30 @@ func (h *Handler) AdminUpdateChannel(c *gin.Context) {
|
||||
if body.Enabled != nil {
|
||||
updates["enabled"] = *body.Enabled
|
||||
}
|
||||
if body.Formats != nil {
|
||||
prov := ch.Provider
|
||||
if body.Provider != nil {
|
||||
prov = *body.Provider
|
||||
}
|
||||
formats, ferr := resolveFormats(prov, *body.Formats)
|
||||
if ferr != nil {
|
||||
resp.Fail(c, http.StatusBadRequest, ferr.Error())
|
||||
return
|
||||
}
|
||||
updates["formats"] = formats
|
||||
}
|
||||
if len(updates) > 0 {
|
||||
if err := h.a.DB.Model(&ch).Updates(updates).Error; err != nil {
|
||||
resp.Fail(c, http.StatusInternalServerError, "failed to update channel")
|
||||
return
|
||||
}
|
||||
// jsonb 序列化走模型字段更新
|
||||
if f, ok := updates["formats"]; ok {
|
||||
if err := h.a.DB.Model(&ch).Update("formats", f).Error; err != nil {
|
||||
resp.Fail(c, http.StatusInternalServerError, "failed to update formats")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
resp.OK(c, gin.H{"ok": true})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user