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

+66 -33
View File
@@ -5,7 +5,6 @@ import (
"fmt"
"io"
"net/http"
"strconv"
"strings"
"time"
@@ -22,13 +21,6 @@ import (
"github.com/tidwall/sjson"
)
func updateOpenAIImageCount(info *relaycommon.RelayInfo, count int64) {
if info == nil || !info.PriceData.UsePrice || count <= 0 || count > int64(dto.MaxImageN) {
return
}
info.PriceData.AddOtherRatio("n", float64(count))
}
// OpenaiImageHandler handles non-streaming OpenAI image responses
// (generations/edits), returning the parsed usage for billing.
func OpenaiImageHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http.Response) (*dto.Usage, *types.NewAPIError) {
@@ -49,7 +41,7 @@ func OpenaiImageHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http.
return nil, types.WithOpenAIError(*oaiError, resp.StatusCode)
}
updateOpenAIImageCount(info, gjson.GetBytes(responseBody, "data.#").Int())
info.UpdateImageCount(openaiImageResponseCount(responseBody))
// 写入新的 response body
service.IOCopyBytesGracefully(c, resp, responseBody)
@@ -59,14 +51,54 @@ func OpenaiImageHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http.
return &usageResp.Usage, nil
}
// openaiImageResponseCount counts billable images in an OpenAI-format image
// response body. An object-shaped data is one image when it carries url or
// b64_json. For arrays the count is the larger of the url-bearing and the
// b64_json-bearing entry counts: a standard response uses one response_format
// so this equals the entry count, an upstream that splits one image into a url
// entry and a b64_json entry bills once, and entries without any image payload
// bill nothing. A zero result leaves the requested quantity in place because
// UpdateImageCount ignores non-positive counts.
func openaiImageResponseCount(responseBody []byte) int64 {
data := gjson.GetBytes(responseBody, "data")
if data.IsObject() {
if openaiImageDataHasField(data, "url") || openaiImageDataHasField(data, "b64_json") {
return 1
}
return 0
}
if !data.IsArray() {
return 0
}
var urls, b64s int64
data.ForEach(func(_, item gjson.Result) bool {
if openaiImageDataHasField(item, "url") {
urls++
}
if openaiImageDataHasField(item, "b64_json") {
b64s++
}
return true
})
return max(urls, b64s)
}
// openaiImageDataHasField reports whether an image data entry carries a
// non-empty string value for field.
func openaiImageDataHasField(item gjson.Result, field string) bool {
value := item.Get(field)
return value.Type == gjson.String && value.Raw != `""`
}
// normalizeOpenAIUsage maps the OpenAI Images usage shape (input_tokens /
// output_tokens / input_tokens_details) onto the canonical prompt/completion
// fields. It is used only on the OpenAI image relay paths (generations/edits,
// streaming and non-streaming): the image API never returns prompt_tokens /
// completion_tokens, so the overwrite (=) semantics here are equivalent to the
// previous additive (+=) behavior while avoiding any future double-counting if
// both field sets are ever populated. Do not reuse this on chat/embedding paths
// without revisiting the overwrite semantics.
// output_tokens / input_tokens_details / output_tokens_details) onto the
// canonical prompt/completion fields. It is used only on the OpenAI image
// relay paths (generations/edits, streaming and non-streaming): the image
// API never returns prompt_tokens / completion_tokens, so the overwrite (=)
// semantics here are equivalent to the previous additive (+=) behavior while
// avoiding any future double-counting if both field sets are ever populated.
// Do not reuse this on chat/embedding paths without revisiting the overwrite
// semantics.
func normalizeOpenAIUsage(usage *dto.Usage) {
if usage == nil {
return
@@ -78,12 +110,10 @@ func normalizeOpenAIUsage(usage *dto.Usage) {
usage.CompletionTokens = usage.OutputTokens
}
if usage.InputTokensDetails != nil {
usage.PromptTokensDetails.CachedTokens = usage.InputTokensDetails.CachedTokens
usage.PromptTokensDetails.CachedCreationTokens = usage.InputTokensDetails.CachedCreationTokens
usage.PromptTokensDetails.CacheWriteTokens = usage.InputTokensDetails.CacheWriteTokens
usage.PromptTokensDetails.ImageTokens = usage.InputTokensDetails.ImageTokens
usage.PromptTokensDetails.TextTokens = usage.InputTokensDetails.TextTokens
usage.PromptTokensDetails.AudioTokens = usage.InputTokensDetails.AudioTokens
usage.PromptTokensDetails = usage.InputTokensDetails.Clone()
}
if usage.OutputTokensDetails != nil {
usage.CompletionTokenDetails = *usage.OutputTokensDetails
}
if usage.TotalTokens == 0 {
usage.TotalTokens = usage.PromptTokens + usage.CompletionTokens
@@ -156,12 +186,8 @@ func OpenaiImageStreamHandler(c *gin.Context, info *relaycommon.RelayInfo, resp
if info.StreamStatus != nil {
upstreamFinished := info.StreamStatus.EndReason == relaycommon.StreamEndReasonDone ||
info.StreamStatus.EndReason == relaycommon.StreamEndReasonEOF
requestedN := 1.0
if n, ok := info.PriceData.OtherRatios()["n"]; ok {
requestedN = n
}
if upstreamFinished || float64(completedImages) > requestedN {
updateOpenAIImageCount(info, completedImages)
if upstreamFinished || completedImages > int64(info.RequestedImageCount()) {
info.UpdateImageCount(completedImages)
}
}
return usage, nil
@@ -252,8 +278,7 @@ func openaiImageJSONAsStreamHandler(c *gin.Context, info *relaycommon.RelayInfo,
normalizeOpenAIUsage(&usageResp.Usage)
applyUsagePostProcessing(info, &usageResp.Usage, responseBody)
imageCount := gjson.GetBytes(responseBody, "data.#").Int()
updateOpenAIImageCount(info, imageCount)
info.UpdateImageCount(openaiImageResponseCount(responseBody))
helper.SetEventStreamHeaders(c)
c.Status(http.StatusOK)
@@ -275,8 +300,15 @@ func openaiImageJSONAsStreamHandler(c *gin.Context, info *relaycommon.RelayInfo,
}
}
for i := range imageCount {
image := gjson.GetBytes(responseBody, "data."+strconv.FormatInt(i, 10))
// gjson.Result.Array returns the element list for an array and a single
// element for an object-shaped data, keeping document-relative indexes so
// the zero-copy field forwarding below stays valid for both shapes.
// Entries without url or b64_json carry no image and are not forwarded.
emitted := 0
for _, image := range gjson.GetBytes(responseBody, "data").Array() {
if !openaiImageDataHasField(image, "url") && !openaiImageDataHasField(image, "b64_json") {
continue
}
payload := []byte(`{"type":"image_generation.completed"}`)
payload, err = sjson.SetBytes(payload, "created_at", created)
if err != nil {
@@ -311,6 +343,7 @@ func openaiImageJSONAsStreamHandler(c *gin.Context, info *relaycommon.RelayInfo,
}
return &usageResp.Usage, nil
}
emitted++
}
if err := writeOpenaiImageStreamDone(c); err != nil {
if info != nil && info.StreamStatus != nil {
@@ -319,7 +352,7 @@ func openaiImageJSONAsStreamHandler(c *gin.Context, info *relaycommon.RelayInfo,
return &usageResp.Usage, nil
}
if info != nil {
info.ReceivedResponseCount += int(imageCount)
info.ReceivedResponseCount += emitted
if info.StreamStatus == nil {
info.StreamStatus = relaycommon.NewStreamStatus()
}