package proxy import ( "encoding/json" "net/http" "strings" "time" "github.com/gin-gonic/gin" "openteam/server/internal/pkg/crypto" "openteam/server/internal/store" ) // tokenUsage is the normalized usage extracted from any protocol. type tokenUsage struct { input int64 output int64 cacheRead int64 cacheCreation int64 } // respStreamEvent is the streaming shape of the OpenAI Responses API used for // usage sniffing on passthrough responses streams. type respStreamEvent struct { Type string `json:"type"` Response *struct { Usage *struct { InputTokens int64 `json:"input_tokens"` OutputTokens int64 `json:"output_tokens"` } `json:"usage"` } `json:"response"` } // authenticate resolves the Bearer API key to a key + user. func (g *Gateway) authenticate(c *gin.Context) (*authContext, error) { auth := c.GetHeader("Authorization") token := "" if strings.HasPrefix(auth, "Bearer ") { token = strings.TrimPrefix(auth, "Bearer ") } else if strings.HasPrefix(auth, "sk-") { // Some clients send the raw key without the Bearer scheme. token = auth } else { return nil, errUnauthorized } var key store.ApiKey if err := g.db.Where("key_hash = ?", crypto.HashSHA256(token)).First(&key).Error; err != nil { return nil, errUnauthorized } if key.Status != "active" { return nil, errUnauthorized } if key.ExpiresAt != nil && key.ExpiresAt.Before(now()) { return nil, errUnauthorized } var user store.User if err := g.db.First(&user, key.UserID).Error; err != nil { return nil, errUnauthorized } if user.Status != "active" { return nil, errUnauthorized } return &authContext{key: &key, user: &user}, nil } var errUnauthorized = &unauthorizedError{} type unauthorizedError struct{} func (*unauthorizedError) Error() string { return "invalid API key" } // writeProxyError writes a gateway-generated error in the client's protocol. func (g *Gateway) writeProxyError(c *gin.Context, proto Protocol, status int, message string) { c.Header("Content-Type", "application/json") switch proto { case ProtocolAnthropic: c.AbortWithStatusJSON(status, map[string]any{ "type": "error", "error": map[string]any{"type": statusType(status), "message": message}, }) default: c.AbortWithStatusJSON(status, map[string]any{ "error": map[string]any{"message": message, "type": "gateway_error", "code": "gateway_error"}, }) } } // writeUpstreamError maps an upstream error body to the client protocol. func (g *Gateway) writeUpstreamError(c *gin.Context, proto Protocol, status int, body []byte) { switch proto { case ProtocolAnthropic: // Extract the upstream Claude error if present. var up struct { Error struct { Type string `json:"type"` Message string `json:"message"` } `json:"error"` } if json.Unmarshal(body, &up) == nil && up.Error.Message != "" { c.AbortWithStatusJSON(status, map[string]any{ "type": "error", "error": map[string]any{"type": up.Error.Type, "message": up.Error.Message}, }) return } c.AbortWithStatusJSON(status, map[string]any{ "type": "error", "error": map[string]any{"type": statusType(status), "message": upstreamMessage(status, body)}, }) default: var up struct { Error struct { Message string `json:"message"` Type string `json:"type"` Code string `json:"code"` Param string `json:"param"` } `json:"error"` } if json.Unmarshal(body, &up) == nil && up.Error.Message != "" { c.AbortWithStatusJSON(status, map[string]any{ "error": map[string]any{ "message": up.Error.Message, "type": up.Error.Type, "code": up.Error.Code, "param": up.Error.Param, }, }) return } c.AbortWithStatusJSON(status, map[string]any{ "error": map[string]any{ "message": upstreamMessage(status, body), "type": statusType(status), "code": statusType(status), }, }) } } func statusType(status int) string { switch { case status == 429: return "rate_limit_error" case status >= 500: return "api_error" case status >= 400: return "invalid_request_error" default: return "api_error" } } func upstreamMessage(status int, body []byte) string { msg := strings.TrimSpace(string(body)) if msg == "" { msg = http.StatusText(status) } return truncateText(msg, 512) } func truncateText(s string, n int) string { if len(s) <= n { return s } return s[:n] + "..." } // copyProxyHeaders forwards selected request headers upstream. func copyProxyHeaders(c *gin.Context, req *http.Request) { for _, h := range []string{"OpenAI-Organization", "OpenAI-Beta", "anthropic-version", "anthropic-beta", "X-Stainless-Lang", "X-Stainless-Package-Version"} { if v := c.GetHeader(h); v != "" { req.Header.Set(h, v) } } } // bodyStreamFlag determines streaming intent from the raw body + route. func bodyStreamFlag(body []byte, route Route) bool { var m struct { Stream bool `json:"stream"` } _ = json.Unmarshal(body, &m) return m.Stream } // setModelField rewrites the "model" key in a JSON object. func setModelField(body []byte, model string) ([]byte, error) { var obj map[string]json.RawMessage if err := json.Unmarshal(body, &obj); err != nil { return nil, err } m, _ := json.Marshal(model) obj["model"] = m return json.Marshal(obj) } // sameModel reports whether the body's model already equals upstreamModel. func sameModel(body []byte, upstreamModel string) bool { var m struct { Model string `json:"model"` } _ = json.Unmarshal(body, &m) return m.Model == upstreamModel } func contains(list []string, s string) bool { for _, v := range list { if v == s { return true } } return false } // upstreamPath returns the endpoint to POST to. When the channel serves the // client protocol natively the route's own path is used; otherwise the request // is converted and must hit the channel's conversion-target path. func upstreamPath(route Route, ch *store.Channel) string { if ch.SupportsFormat(string(route.Protocol)) { return route.UpstreamPath } if ch.Provider == "anthropic" { return "/v1/messages" } return "/v1/chat/completions" } // upstreamProvider returns the format family ("openai" | "anthropic") the // channel will actually speak for this request: the client's own family on // passthrough, otherwise the channel's conversion target. func upstreamProvider(route Route, ch *store.Channel) string { if ch.SupportsFormat(string(route.Protocol)) { return route.NativeProvider } return ch.Provider } // channelServesFormat reports whether the channel can handle the route's // protocol: natively, or via a conversion path that exists. Every protocol can // convert to either Claude or chat completions except one case: Responses-API // requests have no conversion into an openai-family upstream (the // responses->chat response side is unimplemented), so an openai-family channel // that does not declare responses support cannot serve them at all. func channelServesFormat(ch *store.Channel, route Route) bool { if ch.SupportsFormat(string(route.Protocol)) { return true } return !(route.Protocol == ProtocolOpenAIResponses && ch.Provider != "anthropic") } func now() time.Time { return time.Now() }