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.
This commit is contained in:
cneicy committed 2026-09-26 04:49:11 +08:00
1 parent aa6c3f50f4
commit ceddfd3d6b
828 files changed
+122253 -13702

No files matched your search

+94 -72
View File
@@ -1,12 +1,15 @@
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"
@@ -16,10 +19,25 @@ import (
"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
@@ -35,11 +53,15 @@ func OaiResponsesHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http
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 := relayconvert.NormalizeResponsesUsage(responsesResponse.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 {
@@ -64,6 +86,58 @@ func OaiResponsesHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http
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")
@@ -72,10 +146,7 @@ func OaiResponsesStreamHandler(c *gin.Context, info *relaycommon.RelayInfo, resp
defer service.CloseResponseBodyGracefully(resp)
var usage = &dto.Usage{}
var responseTextBuilder strings.Builder
imageCounter := &relaycommon.ImageGenerationCallCounter{}
imageCommitted := false
accumulator := service.NewResponsesUsageAccumulator(info)
helper.StreamScannerHandler(c, resp, info, func(data string, sr *helper.StreamResult) {
@@ -86,77 +157,28 @@ func OaiResponsesStreamHandler(c *gin.Context, info *relaycommon.RelayInfo, resp
sr.Error(err)
return
}
sendResponsesStreamData(c, streamResponse, data)
switch streamResponse.Type {
case "response.completed", "response.done":
if streamResponse.Response != nil {
if streamResponse.Response.Usage != nil {
incomingUsage := relayconvert.NormalizeResponsesUsage(streamResponse.Response.Usage)
usage = dto.MergeUsageNonZero(usage, incomingUsage)
}
if !imageCommitted {
if relaycommon.IsNonBillableResponsesStatus(streamResponse.Response.Status) {
imageCounter.Reset()
imageCounter.Commit(info)
imageCommitted = true
} else {
for i := range streamResponse.Response.Output {
idx := i
imageCounter.Observe(&streamResponse.Response.Output[i], &idx)
}
imageCounter.Commit(info)
imageCommitted = true
}
}
} else if !imageCommitted {
imageCounter.Commit(info)
imageCommitted = true
}
case "response.failed", "response.incomplete", "response.cancelled", "response.canceled":
if !imageCommitted {
imageCounter.Reset()
imageCounter.Commit(info)
imageCommitted = true
}
case "response.output_text.delta":
// 处理输出文本
responseTextBuilder.WriteString(streamResponse.Delta)
case dto.ResponsesOutputTypeItemDone:
if streamResponse.Item != nil {
switch streamResponse.Item.Type {
case dto.BuildInCallWebSearchCall:
info.CountBillableToolCall(dto.BuildInCallWebSearchCall, "")
case dto.BuildInCallFileSearchCall:
info.CountBillableToolCall(dto.BuildInCallFileSearchCall, "")
case dto.BuildInCallFunctionCall:
info.CountBillableToolCall(dto.BuildInCallFunctionCall, streamResponse.Item.Name)
case dto.ResponsesOutputTypeImageGenerationCall:
if !imageCommitted {
imageCounter.Observe(streamResponse.Item, streamResponse.OutputIndex)
}
}
}
if streamResponse.Response != nil {
data = string(rewriteSGLangResponsesCreatedAt(info, []byte(data), "response.created_at", streamResponse.Response.CreatedAt))
}
sendResponsesStreamData(c, streamResponse, data)
accumulator.Observe(&streamResponse)
})
if usage.CompletionTokens == 0 {
// 计算输出文本的 token 数量
tempStr := responseTextBuilder.String()
if len(tempStr) > 0 {
// 非正常结束,使用输出文本的 token 数量
completionTokens := service.CountTextToken(tempStr, info.UpstreamModelName)
usage.CompletionTokens = completionTokens
}
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 usage.PromptTokens == 0 && usage.CompletionTokens != 0 {
usage.PromptTokens = info.GetEstimatePromptTokens()
if !gjson.GetBytes(payload, path).Exists() {
return payload
}
usage.TotalTokens = usage.PromptTokens + usage.CompletionTokens
if usage.BillingUsage != nil {
usage.BillingUsage = dto.CloneBillingUsageWithEstimatedCompletion(usage.BillingUsage, usage.CompletionTokens)
patched, err := sjson.SetBytes(payload, path, int(createdAt))
if err != nil {
return payload
}
return usage, nil
return patched
}