Files
new-api-custom/relay/channel/openai/relay_responses.go
T
cneicy ceddfd3d6b feat: sync deployed v1.0.0-rc.38-custom.42
Sync production source including routing, billing, Astra detection and public uptime. Update custom build instructions and license packaging.
2026-09-26 04:49:11 +08:00

185 lines
6.3 KiB
Go

package openai
import (
"bufio"
"bytes"
"fmt"
"io"
"net/http"
"strings"
"github.com/QuantumNous/new-api/common"
"github.com/QuantumNous/new-api/constant"
"github.com/QuantumNous/new-api/logger"
relaycommon "github.com/QuantumNous/new-api/relay/common"
"github.com/QuantumNous/new-api/relay/helper"
"github.com/QuantumNous/new-api/relaykit/dto"
"github.com/QuantumNous/new-api/relaykit/relayconvert"
"github.com/QuantumNous/new-api/relaykit/types"
"github.com/QuantumNous/new-api/service"
"github.com/gin-gonic/gin"
"github.com/tidwall/gjson"
"github.com/tidwall/sjson"
)
func OaiResponsesHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http.Response) (*dto.Usage, *types.NewAPIError) {
defer service.CloseResponseBodyGracefully(resp)
if strings.HasPrefix(strings.ToLower(resp.Header.Get("Content-Type")), "text/event-stream") {
body, err := readBufferedResponsesSSE(resp.Body)
if err != nil {
return nil, types.NewOpenAIError(err, types.ErrorCodeBadResponseBody, http.StatusBadGateway)
}
converted := *resp
converted.Header = resp.Header.Clone()
converted.Header.Set("Content-Type", "application/json")
converted.Header.Del("Content-Length")
converted.Body = io.NopCloser(bytes.NewReader(body))
converted.ContentLength = int64(len(body))
return OaiResponsesHandler(c, info, &converted)
}
// read response body
var responsesResponse dto.OpenAIResponsesResponse
responseBody, err := io.ReadAll(resp.Body)
if err != nil {
return nil, types.NewOpenAIError(err, types.ErrorCodeReadResponseBodyFailed, http.StatusInternalServerError)
}
err = common.Unmarshal(responseBody, &responsesResponse)
if err != nil {
return nil, types.NewOpenAIError(err, types.ErrorCodeBadResponseBody, http.StatusInternalServerError)
}
if oaiError := responsesResponse.GetOpenAIError(); oaiError != nil && oaiError.Type != "" {
return nil, types.WithOpenAIError(*oaiError, resp.StatusCode)
}
info.ObserveResponseModel(responsesResponse.Model)
responseBody = rewriteSGLangResponsesCreatedAt(info, responseBody, "created_at", responsesResponse.CreatedAt)
// 写入新的 response body
service.IOCopyBytesGracefully(c, resp, responseBody)
// compute usage
usage := &dto.Usage{}
service.ApplyResponsesUsage(usage, responsesResponse.Usage)
// Count actual tool invocations from Output (not tool declarations).
for _, output := range responsesResponse.Output {
switch output.Type {
case dto.BuildInCallWebSearchCall:
info.CountBillableToolCall(dto.BuildInCallWebSearchCall, "")
case dto.BuildInCallFileSearchCall:
info.CountBillableToolCall(dto.BuildInCallFileSearchCall, "")
case dto.BuildInCallFunctionCall:
info.CountBillableToolCall(dto.BuildInCallFunctionCall, output.Name)
}
}
imageCounter := &relaycommon.ImageGenerationCallCounter{}
if !relaycommon.IsNonBillableResponsesStatus(responsesResponse.Status) {
for i := range responsesResponse.Output {
idx := i
imageCounter.Observe(&responsesResponse.Output[i], &idx)
}
}
imageCounter.Commit(info)
return usage, nil
}
// Some upstreams return SSE even for stream=false. Require a terminal response
// and reuse JSON delivery and settlement so upstream usage is charged once.
func readBufferedResponsesSSE(body io.Reader) ([]byte, error) {
scanner := helper.NewStreamScanner(body)
scanner.Split(bufio.ScanLines)
accumulator := relayconvert.NewResponsesBufferedAccumulator()
var data []string
for scanner.Scan() {
line := strings.TrimSuffix(scanner.Text(), "\r")
if payload, ok := strings.CutPrefix(line, "data:"); ok {
data = append(data, strings.TrimPrefix(payload, " "))
}
if line != "" || len(data) == 0 {
continue
}
payload := strings.Join(data, "\n")
data = nil
if payload == "[DONE]" {
break
}
var event dto.ResponsesStreamResponse
if err := common.UnmarshalJsonStr(payload, &event); err != nil {
return nil, err
}
accumulator.ProcessEvent(&event)
switch event.Type {
case "error", "response.error", "response.failed", "response.cancelled", "response.canceled":
return nil, fmt.Errorf("responses stream ended with %s", event.Type)
case "response.completed", "response.done", "response.incomplete":
if event.Response == nil {
return nil, fmt.Errorf("responses terminal event has no response")
}
result := []byte(gjson.Get(payload, "response").Raw)
if len(event.Response.Output) == 0 {
accumulator.SupplementResponseOutput(event.Response)
if len(event.Response.Output) != 0 {
output, err := common.Marshal(event.Response.Output)
if err != nil {
return nil, err
}
return sjson.SetRawBytes(result, "output", output)
}
}
return result, nil
}
}
if err := scanner.Err(); err != nil {
return nil, err
}
return nil, fmt.Errorf("responses stream ended without a terminal response")
}
func OaiResponsesStreamHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http.Response) (*dto.Usage, *types.NewAPIError) {
if resp == nil || resp.Body == nil {
logger.LogError(c, "invalid response or response body")
return nil, types.NewError(fmt.Errorf("invalid response"), types.ErrorCodeBadResponse)
}
defer service.CloseResponseBodyGracefully(resp)
accumulator := service.NewResponsesUsageAccumulator(info)
helper.StreamScannerHandler(c, resp, info, func(data string, sr *helper.StreamResult) {
// 检查当前数据是否包含 completed 状态和 usage 信息
var streamResponse dto.ResponsesStreamResponse
if err := common.UnmarshalJsonStr(data, &streamResponse); err != nil {
logger.LogError(c, "failed to unmarshal stream response: "+err.Error())
sr.Error(err)
return
}
if streamResponse.Response != nil {
data = string(rewriteSGLangResponsesCreatedAt(info, []byte(data), "response.created_at", streamResponse.Response.CreatedAt))
}
sendResponsesStreamData(c, streamResponse, data)
accumulator.Observe(&streamResponse)
})
common.SetContextKey(c, constant.ContextKeyResponseStreamStatus, info.StreamStatus)
info.StreamStatus.RequireTerminal()
return accumulator.Finish(), nil
}
func rewriteSGLangResponsesCreatedAt(info *relaycommon.RelayInfo, payload []byte, path string, createdAt dto.IntValue) []byte {
if info.GetChannelType() != constant.ChannelTypeSGLang {
return payload
}
if !gjson.GetBytes(payload, path).Exists() {
return payload
}
patched, err := sjson.SetBytes(payload, path, int(createdAt))
if err != nil {
return payload
}
return patched
}