package proxy import ( "encoding/json" "net/http" "github.com/gin-gonic/gin" "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 { openAIError(c, http.StatusBadRequest, "invalid_request_error", "failed to read request body") return } c.Set("protocol", "chat") c.Set("model_name", br.Model) ch, err := g.ch.Select() if err != nil { openAIError(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.doPassthrough(c, ch, "/v1/chat/completions", body, br.Stream, func(raw json.RawMessage) { sink.push(raw) }) } // 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 { openAIError(c, http.StatusBadRequest, "invalid_request_error", "failed to read request body") return } c.Set("protocol", "responses") c.Set("model_name", br.Model) ch, err := g.ch.Select() if err != nil { openAIError(c, http.StatusServiceUnavailable, "no_channel", "No available upstream channel") g.recordError(c, nil, nil, now(), "no_channel") return } // M1 仅支持 OpenAI 原生渠道直通;Anthropic 渠道的转换在 M3 if ch.Provider != store.ChannelProviderOpenAI { openAIError(c, http.StatusNotImplemented, "conversion_pending", "Responses protocol on this channel requires format conversion (planned in M3)") return } sink := &usageSink{} c.Set("usage_raw", &sinkHolder{sink: sink}) g.doPassthrough(c, ch, "/v1/responses", body, br.Stream, func(raw json.RawMessage) { sink.push(raw) }) } // usageSinkHolder 桥接:gin context 里保存 sink 引用,供 finishUsage 读取最终 usage。 type sinkHolder struct { sink *usageSink } // openAIError 按 OpenAI 错误格式返回(PLANNING §4.1.4)。 func openAIError(c *gin.Context, status int, code, message string) { 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: return "permission_error" case http.StatusNotFound: return "invalid_request_error" case http.StatusBadRequest: return "invalid_request_error" case http.StatusPaymentRequired: return "insufficient_quota" case http.StatusTooManyRequests: return "rate_limit_error" default: return "api_error" } }