Add optional Codex image Base64 compatibility
Browse files- Dockerfile +41 -7
- codex-image-b64.patch +291 -0
Dockerfile
CHANGED
|
@@ -1,10 +1,44 @@
|
|
| 1 |
-
|
| 2 |
-
FROM calciumion/new-api:latest
|
| 3 |
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
|
| 8 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
USER root
|
| 10 |
-
|
|
|
|
| 1 |
+
FROM alpine:3.22 AS source
|
|
|
|
| 2 |
|
| 3 |
+
RUN apk add --no-cache git
|
| 4 |
+
RUN git clone --depth 1 --branch v1.0.0-rc.22 https://github.com/QuantumNous/new-api.git /src
|
| 5 |
+
COPY codex-image-b64.patch /tmp/codex-image-b64.patch
|
| 6 |
+
RUN git -C /src apply --check /tmp/codex-image-b64.patch \
|
| 7 |
+
&& git -C /src apply /tmp/codex-image-b64.patch
|
| 8 |
+
|
| 9 |
+
FROM oven/bun:1 AS web-builder
|
| 10 |
+
|
| 11 |
+
WORKDIR /build/web
|
| 12 |
+
COPY --from=source /src/web/package.json /src/web/bun.lock ./
|
| 13 |
+
RUN bun install --frozen-lockfile
|
| 14 |
+
COPY --from=source /src/web ./
|
| 15 |
+
COPY --from=source /src/VERSION /build/VERSION
|
| 16 |
+
RUN DISABLE_ESLINT_PLUGIN=true VITE_REACT_APP_VERSION=v1.0.0-rc.22 bun run build
|
| 17 |
+
|
| 18 |
+
FROM golang:1.26.1-alpine AS go-builder
|
| 19 |
|
| 20 |
+
ENV GO111MODULE=on CGO_ENABLED=0 GOOS=linux GOARCH=amd64 GOEXPERIMENT=greenteagc
|
| 21 |
+
WORKDIR /build
|
| 22 |
+
COPY --from=source /src/go.mod /src/go.sum ./
|
| 23 |
+
RUN go mod download
|
| 24 |
+
COPY --from=source /src ./
|
| 25 |
+
COPY --from=web-builder /build/web/dist ./web/dist
|
| 26 |
+
RUN go test ./relay/channel/openai \
|
| 27 |
+
&& go build -ldflags "-s -w -X 'github.com/QuantumNous/new-api/common.Version=v1.0.0-rc.22-codex-image-b64'" -o new-api
|
| 28 |
+
|
| 29 |
+
FROM debian:bookworm-slim
|
| 30 |
+
|
| 31 |
+
RUN apt-get update \
|
| 32 |
+
&& apt-get install -y --no-install-recommends ca-certificates tzdata libasan8 wget \
|
| 33 |
+
&& rm -rf /var/lib/apt/lists/* \
|
| 34 |
+
&& update-ca-certificates \
|
| 35 |
+
&& mkdir -p /data \
|
| 36 |
+
&& chmod 777 /data
|
| 37 |
+
|
| 38 |
+
COPY --from=go-builder /build/new-api /new-api
|
| 39 |
+
COPY --from=source /src/LICENSE /src/NOTICE /src/THIRD-PARTY-LICENSES.md /licenses/
|
| 40 |
+
ENV PORT=7860 VERSION=v1.0.0-rc.22-codex-image-b64
|
| 41 |
+
EXPOSE 7860
|
| 42 |
+
WORKDIR /data
|
| 43 |
USER root
|
| 44 |
+
ENTRYPOINT ["/new-api"]
|
codex-image-b64.patch
ADDED
|
@@ -0,0 +1,291 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
From a7a127e93518b4b91820d23b4b508a70f9834a98 Mon Sep 17 00:00:00 2001
|
| 2 |
+
From: Claude Code <wechatbot@dev.local>
|
| 3 |
+
Date: Wed, 29 Jul 2026 18:14:19 +0800
|
| 4 |
+
Subject: [PATCH] Add optional image URL base64 compatibility
|
| 5 |
+
|
| 6 |
+
---
|
| 7 |
+
.env.example | 2 +
|
| 8 |
+
relay/channel/openai/image_url_compat.go | 162 ++++++++++++++++++
|
| 9 |
+
relay/channel/openai/image_url_compat_test.go | 70 ++++++++
|
| 10 |
+
relay/channel/openai/relay_image.go | 10 ++
|
| 11 |
+
4 files changed, 244 insertions(+)
|
| 12 |
+
create mode 100644 relay/channel/openai/image_url_compat.go
|
| 13 |
+
create mode 100644 relay/channel/openai/image_url_compat_test.go
|
| 14 |
+
|
| 15 |
+
diff --git a/.env.example b/.env.example
|
| 16 |
+
index d62e114..76ea82f 100644
|
| 17 |
+
--- a/.env.example
|
| 18 |
+
+++ b/.env.example
|
| 19 |
+
@@ -1,5 +1,7 @@
|
| 20 |
+
# 端口号
|
| 21 |
+
# PORT=3000
|
| 22 |
+
+# Convert URL-based OpenAI image responses to b64_json for strict clients such as Codex.
|
| 23 |
+
+# CODEX_IMAGE_URL_TO_B64=false
|
| 24 |
+
# 前端基础URL
|
| 25 |
+
# FRONTEND_BASE_URL=https://your-frontend-url.com
|
| 26 |
+
|
| 27 |
+
diff --git a/relay/channel/openai/image_url_compat.go b/relay/channel/openai/image_url_compat.go
|
| 28 |
+
new file mode 100644
|
| 29 |
+
index 0000000..e0f222c
|
| 30 |
+
--- /dev/null
|
| 31 |
+
+++ b/relay/channel/openai/image_url_compat.go
|
| 32 |
+
@@ -0,0 +1,162 @@
|
| 33 |
+
+package openai
|
| 34 |
+
+
|
| 35 |
+
+import (
|
| 36 |
+
+ "encoding/base64"
|
| 37 |
+
+ "encoding/json"
|
| 38 |
+
+ "fmt"
|
| 39 |
+
+ "io"
|
| 40 |
+
+ "net/http"
|
| 41 |
+
+ "strings"
|
| 42 |
+
+ "time"
|
| 43 |
+
+
|
| 44 |
+
+ "github.com/QuantumNous/new-api/common"
|
| 45 |
+
+)
|
| 46 |
+
+
|
| 47 |
+
+const maxImageURLCompatBytes int64 = 40 * 1024 * 1024
|
| 48 |
+
+
|
| 49 |
+
+type imageURLCompatDownloadFunc func(string, ...string) (*http.Response, error)
|
| 50 |
+
+
|
| 51 |
+
+type imageURLCompatStats struct {
|
| 52 |
+
+ Converted int
|
| 53 |
+
+ DownloadedBytes int64
|
| 54 |
+
+ DownloadTime time.Duration
|
| 55 |
+
+ EncodeTime time.Duration
|
| 56 |
+
+ TotalTime time.Duration
|
| 57 |
+
+}
|
| 58 |
+
+
|
| 59 |
+
+func (s imageURLCompatStats) ServerTiming() string {
|
| 60 |
+
+ return fmt.Sprintf(
|
| 61 |
+
+ "image-url-download;dur=%.3f, image-b64-encode;dur=%.3f, image-url-compat;dur=%.3f",
|
| 62 |
+
+ float64(s.DownloadTime.Microseconds())/1000,
|
| 63 |
+
+ float64(s.EncodeTime.Microseconds())/1000,
|
| 64 |
+
+ float64(s.TotalTime.Microseconds())/1000,
|
| 65 |
+
+ )
|
| 66 |
+
+}
|
| 67 |
+
+
|
| 68 |
+
+func (s imageURLCompatStats) LogMessage() string {
|
| 69 |
+
+ return fmt.Sprintf(
|
| 70 |
+
+ "image URL compatibility converted=%d bytes=%d download_ms=%.3f encode_ms=%.3f total_ms=%.3f",
|
| 71 |
+
+ s.Converted,
|
| 72 |
+
+ s.DownloadedBytes,
|
| 73 |
+
+ float64(s.DownloadTime.Microseconds())/1000,
|
| 74 |
+
+ float64(s.EncodeTime.Microseconds())/1000,
|
| 75 |
+
+ float64(s.TotalTime.Microseconds())/1000,
|
| 76 |
+
+ )
|
| 77 |
+
+}
|
| 78 |
+
+
|
| 79 |
+
+func convertOpenAIImageURLsToBase64(responseBody []byte, download imageURLCompatDownloadFunc) ([]byte, imageURLCompatStats, error) {
|
| 80 |
+
+ var stats imageURLCompatStats
|
| 81 |
+
+ if !common.GetEnvOrDefaultBool("CODEX_IMAGE_URL_TO_B64", false) {
|
| 82 |
+
+ return responseBody, stats, nil
|
| 83 |
+
+ }
|
| 84 |
+
+
|
| 85 |
+
+ startedAt := time.Now()
|
| 86 |
+
+ var payload map[string]json.RawMessage
|
| 87 |
+
+ if err := common.Unmarshal(responseBody, &payload); err != nil {
|
| 88 |
+
+ return nil, stats, fmt.Errorf("decode image compatibility response: %w", err)
|
| 89 |
+
+ }
|
| 90 |
+
+ dataJSON, ok := payload["data"]
|
| 91 |
+
+ if !ok {
|
| 92 |
+
+ return responseBody, stats, nil
|
| 93 |
+
+ }
|
| 94 |
+
+
|
| 95 |
+
+ var images []map[string]json.RawMessage
|
| 96 |
+
+ if err := common.Unmarshal(dataJSON, &images); err != nil {
|
| 97 |
+
+ return nil, stats, fmt.Errorf("decode image compatibility data: %w", err)
|
| 98 |
+
+ }
|
| 99 |
+
+
|
| 100 |
+
+ for _, image := range images {
|
| 101 |
+
+ if jsonString(image["b64_json"]) != "" {
|
| 102 |
+
+ continue
|
| 103 |
+
+ }
|
| 104 |
+
+
|
| 105 |
+
+ var imageURL string
|
| 106 |
+
+ var sourceField string
|
| 107 |
+
+ for _, field := range []string{"url", "result_url", "image_url"} {
|
| 108 |
+
+ if value := jsonString(image[field]); value != "" {
|
| 109 |
+
+ imageURL = value
|
| 110 |
+
+ sourceField = field
|
| 111 |
+
+ break
|
| 112 |
+
+ }
|
| 113 |
+
+ }
|
| 114 |
+
+ if imageURL == "" {
|
| 115 |
+
+ continue
|
| 116 |
+
+ }
|
| 117 |
+
+
|
| 118 |
+
+ downloadStartedAt := time.Now()
|
| 119 |
+
+ imageResponse, err := download(imageURL, "OpenAI image URL compatibility")
|
| 120 |
+
+ if err != nil {
|
| 121 |
+
+ return nil, stats, fmt.Errorf("download image compatibility URL: %w", err)
|
| 122 |
+
+ }
|
| 123 |
+
+ imageBytes, readErr := readImageURLCompatResponse(imageResponse)
|
| 124 |
+
+ stats.DownloadTime += time.Since(downloadStartedAt)
|
| 125 |
+
+ if readErr != nil {
|
| 126 |
+
+ return nil, stats, readErr
|
| 127 |
+
+ }
|
| 128 |
+
+
|
| 129 |
+
+ encodeStartedAt := time.Now()
|
| 130 |
+
+ encodedJSON, err := common.Marshal(base64.StdEncoding.EncodeToString(imageBytes))
|
| 131 |
+
+ if err != nil {
|
| 132 |
+
+ return nil, stats, fmt.Errorf("encode image compatibility response: %w", err)
|
| 133 |
+
+ }
|
| 134 |
+
+ image["b64_json"] = encodedJSON
|
| 135 |
+
+ delete(image, sourceField)
|
| 136 |
+
+ stats.EncodeTime += time.Since(encodeStartedAt)
|
| 137 |
+
+ stats.Converted++
|
| 138 |
+
+ stats.DownloadedBytes += int64(len(imageBytes))
|
| 139 |
+
+ }
|
| 140 |
+
+
|
| 141 |
+
+ if stats.Converted == 0 {
|
| 142 |
+
+ return responseBody, stats, nil
|
| 143 |
+
+ }
|
| 144 |
+
+
|
| 145 |
+
+ encodeStartedAt := time.Now()
|
| 146 |
+
+ dataJSON, err := common.Marshal(images)
|
| 147 |
+
+ if err != nil {
|
| 148 |
+
+ return nil, stats, fmt.Errorf("encode image compatibility data: %w", err)
|
| 149 |
+
+ }
|
| 150 |
+
+ payload["data"] = dataJSON
|
| 151 |
+
+ convertedBody, err := common.Marshal(payload)
|
| 152 |
+
+ stats.EncodeTime += time.Since(encodeStartedAt)
|
| 153 |
+
+ stats.TotalTime = time.Since(startedAt)
|
| 154 |
+
+ if err != nil {
|
| 155 |
+
+ return nil, stats, fmt.Errorf("encode image compatibility payload: %w", err)
|
| 156 |
+
+ }
|
| 157 |
+
+ return convertedBody, stats, nil
|
| 158 |
+
+}
|
| 159 |
+
+
|
| 160 |
+
+func jsonString(raw json.RawMessage) string {
|
| 161 |
+
+ if len(raw) == 0 {
|
| 162 |
+
+ return ""
|
| 163 |
+
+ }
|
| 164 |
+
+ var value string
|
| 165 |
+
+ if err := common.Unmarshal(raw, &value); err != nil {
|
| 166 |
+
+ return ""
|
| 167 |
+
+ }
|
| 168 |
+
+ return strings.TrimSpace(value)
|
| 169 |
+
+}
|
| 170 |
+
+
|
| 171 |
+
+func readImageURLCompatResponse(response *http.Response) ([]byte, error) {
|
| 172 |
+
+ if response == nil || response.Body == nil {
|
| 173 |
+
+ return nil, fmt.Errorf("image compatibility download returned an empty response")
|
| 174 |
+
+ }
|
| 175 |
+
+ defer response.Body.Close()
|
| 176 |
+
+ if response.StatusCode < http.StatusOK || response.StatusCode >= http.StatusMultipleChoices {
|
| 177 |
+
+ return nil, fmt.Errorf("image compatibility download returned HTTP %d", response.StatusCode)
|
| 178 |
+
+ }
|
| 179 |
+
+ if response.ContentLength > maxImageURLCompatBytes {
|
| 180 |
+
+ return nil, fmt.Errorf("image compatibility download exceeds %d bytes", maxImageURLCompatBytes)
|
| 181 |
+
+ }
|
| 182 |
+
+
|
| 183 |
+
+ imageBytes, err := io.ReadAll(io.LimitReader(response.Body, maxImageURLCompatBytes+1))
|
| 184 |
+
+ if err != nil {
|
| 185 |
+
+ return nil, fmt.Errorf("read image compatibility download: %w", err)
|
| 186 |
+
+ }
|
| 187 |
+
+ if int64(len(imageBytes)) > maxImageURLCompatBytes {
|
| 188 |
+
+ return nil, fmt.Errorf("image compatibility download exceeds %d bytes", maxImageURLCompatBytes)
|
| 189 |
+
+ }
|
| 190 |
+
+ if len(imageBytes) == 0 {
|
| 191 |
+
+ return nil, fmt.Errorf("image compatibility download returned an empty body")
|
| 192 |
+
+ }
|
| 193 |
+
+ return imageBytes, nil
|
| 194 |
+
+}
|
| 195 |
+
diff --git a/relay/channel/openai/image_url_compat_test.go b/relay/channel/openai/image_url_compat_test.go
|
| 196 |
+
new file mode 100644
|
| 197 |
+
index 0000000..2722bba
|
| 198 |
+
--- /dev/null
|
| 199 |
+
+++ b/relay/channel/openai/image_url_compat_test.go
|
| 200 |
+
@@ -0,0 +1,70 @@
|
| 201 |
+
+package openai
|
| 202 |
+
+
|
| 203 |
+
+import (
|
| 204 |
+
+ "bytes"
|
| 205 |
+
+ "io"
|
| 206 |
+
+ "net/http"
|
| 207 |
+
+ "testing"
|
| 208 |
+
+
|
| 209 |
+
+ "github.com/QuantumNous/new-api/common"
|
| 210 |
+
+ "github.com/stretchr/testify/assert"
|
| 211 |
+
+ "github.com/stretchr/testify/require"
|
| 212 |
+
+)
|
| 213 |
+
+
|
| 214 |
+
+func TestConvertOpenAIImageURLsToBase64Disabled(t *testing.T) {
|
| 215 |
+
+ t.Setenv("CODEX_IMAGE_URL_TO_B64", "false")
|
| 216 |
+
+ body := []byte(`{"created":1,"data":[{"url":"https://example.com/image.png"}]}`)
|
| 217 |
+
+
|
| 218 |
+
+ converted, stats, err := convertOpenAIImageURLsToBase64(body, func(string, ...string) (*http.Response, error) {
|
| 219 |
+
+ t.Fatal("download must not run while compatibility is disabled")
|
| 220 |
+
+ return nil, nil
|
| 221 |
+
+ })
|
| 222 |
+
+
|
| 223 |
+
+ require.NoError(t, err)
|
| 224 |
+
+ assert.Equal(t, body, converted)
|
| 225 |
+
+ assert.Zero(t, stats.Converted)
|
| 226 |
+
+}
|
| 227 |
+
+
|
| 228 |
+
+func TestConvertOpenAIImageURLsToBase64PreservesResponseFields(t *testing.T) {
|
| 229 |
+
+ t.Setenv("CODEX_IMAGE_URL_TO_B64", "true")
|
| 230 |
+
+ image := []byte("test-image")
|
| 231 |
+
+ body := []byte(`{"created":1,"data":[{"url":"https://example.com/image.png","revised_prompt":"kept"}],"usage":{"total_tokens":7},"custom":{"kept":true}}`)
|
| 232 |
+
+
|
| 233 |
+
+ converted, stats, err := convertOpenAIImageURLsToBase64(body, func(rawURL string, reason ...string) (*http.Response, error) {
|
| 234 |
+
+ assert.Equal(t, "https://example.com/image.png", rawURL)
|
| 235 |
+
+ assert.Equal(t, []string{"OpenAI image URL compatibility"}, reason)
|
| 236 |
+
+ return &http.Response{
|
| 237 |
+
+ StatusCode: http.StatusOK,
|
| 238 |
+
+ ContentLength: int64(len(image)),
|
| 239 |
+
+ Body: io.NopCloser(bytes.NewReader(image)),
|
| 240 |
+
+ }, nil
|
| 241 |
+
+ })
|
| 242 |
+
+
|
| 243 |
+
+ require.NoError(t, err)
|
| 244 |
+
+ assert.Equal(t, 1, stats.Converted)
|
| 245 |
+
+ assert.Equal(t, int64(len(image)), stats.DownloadedBytes)
|
| 246 |
+
+
|
| 247 |
+
+ var payload map[string]any
|
| 248 |
+
+ require.NoError(t, common.Unmarshal(converted, &payload))
|
| 249 |
+
+ data := payload["data"].([]any)
|
| 250 |
+
+ item := data[0].(map[string]any)
|
| 251 |
+
+ assert.Equal(t, "dGVzdC1pbWFnZQ==", item["b64_json"])
|
| 252 |
+
+ assert.Equal(t, "kept", item["revised_prompt"])
|
| 253 |
+
+ assert.NotContains(t, item, "url")
|
| 254 |
+
+ assert.Equal(t, float64(7), payload["usage"].(map[string]any)["total_tokens"])
|
| 255 |
+
+ assert.Equal(t, true, payload["custom"].(map[string]any)["kept"])
|
| 256 |
+
+ assert.Contains(t, stats.ServerTiming(), "image-url-compat")
|
| 257 |
+
+}
|
| 258 |
+
+
|
| 259 |
+
+func TestReadImageURLCompatResponseRejectsOversize(t *testing.T) {
|
| 260 |
+
+ response := &http.Response{
|
| 261 |
+
+ StatusCode: http.StatusOK,
|
| 262 |
+
+ ContentLength: maxImageURLCompatBytes + 1,
|
| 263 |
+
+ Body: io.NopCloser(bytes.NewReader([]byte("unused"))),
|
| 264 |
+
+ }
|
| 265 |
+
+
|
| 266 |
+
+ _, err := readImageURLCompatResponse(response)
|
| 267 |
+
+
|
| 268 |
+
+ require.Error(t, err)
|
| 269 |
+
+ assert.Contains(t, err.Error(), "exceeds")
|
| 270 |
+
+}
|
| 271 |
+
diff --git a/relay/channel/openai/relay_image.go b/relay/channel/openai/relay_image.go
|
| 272 |
+
index e0f09aa..a6cfad1 100644
|
| 273 |
+
--- a/relay/channel/openai/relay_image.go
|
| 274 |
+
+++ b/relay/channel/openai/relay_image.go
|
| 275 |
+
@@ -39,6 +39,16 @@ func OpenaiImageHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http.
|
| 276 |
+
return nil, types.NewOpenAIError(err, types.ErrorCodeReadResponseBodyFailed, http.StatusInternalServerError)
|
| 277 |
+
}
|
| 278 |
+
|
| 279 |
+
+ responseBody, compatStats, err := convertOpenAIImageURLsToBase64(responseBody, service.DoDownloadRequest)
|
| 280 |
+
+ if err != nil {
|
| 281 |
+
+ return nil, types.NewOpenAIError(err, types.ErrorCodeBadResponseBody, http.StatusBadGateway)
|
| 282 |
+
+ }
|
| 283 |
+
+ if compatStats.Converted > 0 {
|
| 284 |
+
+ c.Header("Server-Timing", compatStats.ServerTiming())
|
| 285 |
+
+ c.Header("X-New-API-Image-Compat", "url-to-b64")
|
| 286 |
+
+ logger.LogInfo(c, compatStats.LogMessage())
|
| 287 |
+
+ }
|
| 288 |
+
+
|
| 289 |
+
var usageResp dto.SimpleResponse
|
| 290 |
+
err = common.Unmarshal(responseBody, &usageResp)
|
| 291 |
+
if err != nil {
|