Files
sub2api/backend/internal/service/openai_image_dimensions.go
T
zh239ns bef520b191 fix(images): report decoded OAuth output dimensions
ChatGPT OAuth may normalize explicit image size controls to auto while returning smaller raster bytes. Decode final image headers so Images API metadata and tier billing reflect the actual output for both streaming and non-streaming OAuth responses.
2026-07-14 20:31:17 +08:00

93 lines
2.7 KiB
Go

package service
import (
"bufio"
"encoding/base64"
"encoding/binary"
"fmt"
"image"
_ "image/jpeg"
_ "image/png"
"io"
"strings"
)
const maxOpenAIImageDimensionProbeBytes int64 = 1 << 20
func detectOpenAIImageResultSize(encoded string) string {
payload := strings.TrimSpace(encoded)
if strings.HasPrefix(strings.ToLower(payload), "data:") {
comma := strings.IndexByte(payload, ',')
if comma < 0 || comma+1 >= len(payload) {
return ""
}
payload = strings.TrimSpace(payload[comma+1:])
}
if payload == "" {
return ""
}
for _, encoding := range []*base64.Encoding{base64.StdEncoding, base64.RawStdEncoding} {
decoded := base64.NewDecoder(encoding, strings.NewReader(payload))
buffered := bufio.NewReader(io.LimitReader(decoded, maxOpenAIImageDimensionProbeBytes))
prefix, _ := buffered.Peek(30)
if width, height, ok := detectOpenAIWebPDimensions(prefix); ok {
return fmt.Sprintf("%dx%d", width, height)
}
cfg, _, err := image.DecodeConfig(buffered)
if err != nil || cfg.Width <= 0 || cfg.Height <= 0 {
continue
}
return fmt.Sprintf("%dx%d", cfg.Width, cfg.Height)
}
return ""
}
func detectOpenAIWebPDimensions(header []byte) (int, int, bool) {
if len(header) < 16 || string(header[:4]) != "RIFF" || string(header[8:12]) != "WEBP" {
return 0, 0, false
}
switch string(header[12:16]) {
case "VP8X":
if len(header) < 30 {
return 0, 0, false
}
width := 1 + int(header[24]) + int(header[25])<<8 + int(header[26])<<16
height := 1 + int(header[27]) + int(header[28])<<8 + int(header[29])<<16
return width, height, width > 0 && height > 0
case "VP8 ":
if len(header) < 30 || string(header[23:26]) != "\x9d\x01\x2a" {
return 0, 0, false
}
width := int(binary.LittleEndian.Uint16(header[26:28]) & 0x3fff)
height := int(binary.LittleEndian.Uint16(header[28:30]) & 0x3fff)
return width, height, width > 0 && height > 0
case "VP8L":
if len(header) < 25 || header[20] != 0x2f {
return 0, 0, false
}
width := 1 + int(header[21]) + int(header[22]&0x3f)<<8
height := 1 + int(header[22]>>6) + int(header[23])<<2 + int(header[24]&0x0f)<<10
return width, height, width > 0 && height > 0
default:
return 0, 0, false
}
}
func reconcileOpenAIResponsesImageResultSizes(results []openAIResponsesImageResult, firstMeta *openAIResponsesImageResult) {
for i := range results {
// ChatGPT OAuth can normalize requested controls to "auto". The final
// image bytes are authoritative for response metadata and tier billing.
if actualSize := detectOpenAIImageResultSize(results[i].Result); actualSize != "" {
results[i].Size = actualSize
}
}
if firstMeta == nil || len(results) == 0 {
return
}
if size := strings.TrimSpace(results[0].Size); size != "" {
firstMeta.Size = size
}
}