Files
openteam/server/internal/proxy/handlers.go
T
SakurasanandClaude b365779188 模型限制: 系统配置全局开放/禁止 + 用户级限制 + 网关拦截
- User 新增 allowed_models/denied_models(用户级模型限制)
- 系统配置 model_allowlist/model_denylist 全局策略, 保存即时失效网关缓存
- 网关 checkModelAllowed: 用户级 > 全局(禁止命中→403, 白名单非空→仅白名单)
- 三个协议处理器均校验, 错误按客户端协议格式返回
- 配置页"模型限制"卡片(全局允许/禁止多选); 用户编辑支持允许/禁止模型

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-16 01:34:12 +08:00

146 lines
3.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package proxy
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/openteam/server/internal/proxy/convert"
)
// chatCompletions POST /v1/chat/completions
func (g *Gateway) chatCompletions(c *gin.Context) {
u, ok := g.resolveUser(c)
if !ok {
return
}
if !g.checkBalance(c, u) {
return
}
br, body, err := parseBody(c)
if err != nil {
apiError(c, http.StatusBadRequest, "invalid_request_error", "failed to read request body")
return
}
c.Set("protocol", convert.ProtoChat)
c.Set("model_name", br.Model)
if !g.checkModelAllowed(u, br.Model) {
apiError(c, http.StatusForbidden, "model_not_allowed", "模型未对你开放,请联系管理员")
return
}
cands := g.candidateChannels(br.Model)
if len(cands) == 0 {
apiError(c, http.StatusServiceUnavailable, "no_channel", "No available upstream channel")
g.recordError(c, nil, nil, now(), "no_channel")
return
}
sink := &usageSink{}
c.Set("usage_raw", &sinkHolder{sink: sink})
g.doProxy(c, cands, convert.ProtoChat, body, br.Stream, sink)
}
// responses POST /v1/responses(OpenAI Responses API)
func (g *Gateway) responses(c *gin.Context) {
u, ok := g.resolveUser(c)
if !ok {
return
}
if !g.checkBalance(c, u) {
return
}
br, body, err := parseBody(c)
if err != nil {
apiError(c, http.StatusBadRequest, "invalid_request_error", "failed to read request body")
return
}
c.Set("protocol", convert.ProtoResponses)
c.Set("model_name", br.Model)
if !g.checkModelAllowed(u, br.Model) {
apiError(c, http.StatusForbidden, "model_not_allowed", "模型未对你开放,请联系管理员")
return
}
cands := g.candidateChannels(br.Model)
if len(cands) == 0 {
apiError(c, http.StatusServiceUnavailable, "no_channel", "No available upstream channel")
g.recordError(c, nil, nil, now(), "no_channel")
return
}
sink := &usageSink{}
c.Set("usage_raw", &sinkHolder{sink: sink})
g.doProxy(c, cands, convert.ProtoResponses, body, br.Stream, sink)
}
// messages POST /v1/messages(Anthropic Messages API)
func (g *Gateway) messages(c *gin.Context) {
u, ok := g.resolveUser(c)
if !ok {
return
}
if !g.checkBalance(c, u) {
return
}
br, body, err := parseBody(c)
if err != nil {
apiError(c, http.StatusBadRequest, "invalid_request_error", "failed to read request body")
return
}
c.Set("protocol", convert.ProtoMessages)
c.Set("model_name", br.Model)
if !g.checkModelAllowed(u, br.Model) {
apiError(c, http.StatusForbidden, "model_not_allowed", "模型未对你开放,请联系管理员")
return
}
cands := g.candidateChannels(br.Model)
if len(cands) == 0 {
apiError(c, http.StatusServiceUnavailable, "no_channel", "No available upstream channel")
g.recordError(c, nil, nil, now(), "no_channel")
return
}
sink := &usageSink{}
c.Set("usage_raw", &sinkHolder{sink: sink})
g.doProxy(c, cands, convert.ProtoMessages, body, br.Stream, sink)
}
// usageSinkHolder 桥接:gin context 里保存 sink 引用,供 finishUsage 读取最终 usage。
type sinkHolder struct {
sink *usageSink
}
// apiError 按客户端协议返回错误体(PLANNING §5.1.4)。
func apiError(c *gin.Context, status int, code, message string) {
if p, _ := c.Get("protocol"); p == convert.ProtoMessages {
// Anthropic 格式
c.AbortWithStatusJSON(status, gin.H{
"type": "error",
"error": gin.H{"type": errorTypeFor(status), "message": message},
})
return
}
// OpenAI 格式
c.AbortWithStatusJSON(status, gin.H{
"error": gin.H{
"message": message,
"type": errorTypeFor(status),
"param": nil,
"code": code,
},
})
}
func errorTypeFor(status int) string {
switch status {
case http.StatusUnauthorized:
return "authentication_error"
case http.StatusForbidden, http.StatusPaymentRequired:
return "permission_error"
case http.StatusNotFound, http.StatusBadRequest:
return "invalid_request_error"
case http.StatusTooManyRequests:
return "rate_limit_error"
default:
return "api_error"
}
}