package gateway import ( "errors" "fmt" "io" "net/http" "gitea.rutua.cn/kate522/CrossRelay/internal/chatcmpl" "gitea.rutua.cn/kate522/CrossRelay/internal/responses" ) // handleResponses serves POST /v1/responses in both streaming and // non-streaming modes. func (s *Server) handleResponses(w http.ResponseWriter, r *http.Request) { var req responses.Request if err := decodeJSON(r, &req, 64<<20); err != nil { writeResponsesError(w, http.StatusBadRequest, "invalid_request_error", "invalid request body: "+err.Error()) return } provider := s.store.ActiveCodex() if provider == nil { writeResponsesError(w, http.StatusBadRequest, "invalid_request_error", "no active provider: activate one in the CrossRelay app") return } model := provider.ResolveModel(req.Model) chatReq, err := responses.ConvertRequest(&req, model) if err != nil { writeResponsesError(w, http.StatusBadRequest, "invalid_request_error", err.Error()) return } rec := recordFrom(r) rec.update(func(e *LogEntry) { e.Model = model e.InputTokens = responses.EstimateTokens(chatReq) }) client := chatcmpl.NewClient(provider.BaseURL, provider.APIKey) if req.Stream { s.handleResponsesStream(w, r, client, chatReq, &req, model, rec) return } resp, err := client.Create(r.Context(), chatReq) if err != nil { s.failUpstreamResponses(w, rec, err) return } out := responses.ConvertResponse(resp, req.Model) rec.update(func(e *LogEntry) { if out.Usage != nil { e.OutputTokens = out.Usage.OutputTokens } }) writeJSON(w, http.StatusOK, out) } // handleResponsesStream relays a Chat Completions SSE stream as // Responses events. func (s *Server) handleResponsesStream(w http.ResponseWriter, r *http.Request, client *chatcmpl.Client, chatReq *chatcmpl.Request, req *responses.Request, model string, rec *record) { resp, err := client.CreateStream(r.Context(), chatReq) if err != nil { s.failUpstreamResponses(w, rec, err) return } defer resp.Body.Close() conv := responses.NewStreamConverter(w, req.Model, responses.EstimateTokens(chatReq)) if err := conv.WriteStart(); err != nil { return } stream := chatcmpl.NewStream(resp.Body) for { chunk, err := stream.Next() if errors.Is(err, io.EOF) { break } if err != nil { rec.update(func(e *LogEntry) { e.Error = err.Error() }) conv.WriteError("api_error", err.Error()) return } if err := conv.OnChunk(chunk); err != nil { rec.update(func(e *LogEntry) { e.Error = err.Error() }) conv.WriteError("api_error", err.Error()) return } } if err := conv.Finish(); err != nil { rec.update(func(e *LogEntry) { e.Error = err.Error() }) } } // failUpstreamResponses maps an upstream failure onto a Responses // error envelope. func (s *Server) failUpstreamResponses(w http.ResponseWriter, rec *record, err error) { rec.update(func(e *LogEntry) { e.Error = err.Error() }) var ue *chatcmpl.UpstreamError if errors.As(err, &ue) { switch { case ue.StatusCode == http.StatusUnauthorized || ue.StatusCode == http.StatusForbidden: writeResponsesError(w, http.StatusUnauthorized, "authentication_error", ue.Message) case ue.StatusCode == http.StatusTooManyRequests: writeResponsesError(w, http.StatusTooManyRequests, "rate_limit_error", ue.Message) case ue.StatusCode == http.StatusBadRequest: writeResponsesError(w, http.StatusBadRequest, "invalid_request_error", ue.Message) default: writeResponsesError(w, http.StatusBadGateway, "api_error", ue.Message) } return } writeResponsesError(w, http.StatusBadGateway, "api_error", fmt.Sprintf("upstream request failed: %v", err)) }