package proxy import ( "net/http" "github.com/gin-gonic/gin" "github.com/openteam/server/internal/proxy/convert" "github.com/openteam/server/internal/store" ) // 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) g.recordRawRequest(c, u, body) 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) g.recordRawRequest(c, u, body) 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) g.recordRawRequest(c, u, body) 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 } // recordRawRequest 记录管理员原始请求体到 context(供 finishUsage 落库)。 // 仅当开关开启且用户为管理员时记录;响应侧以 c.Get("raw_request") 是否非空判断是否需要捕获响应。 func (g *Gateway) recordRawRequest(c *gin.Context, u *store.User, body []byte) { if g.logRaw && u.Role == store.RoleAdmin { c.Set("raw_request", string(body)) } } // 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" } }