Skip to content

Commit 183128d

Browse files
Marvaecaozhiyuan
authored andcommitted
fix(responses): estimate input image bytes from data URLs
1 parent 8a6a6f2 commit 183128d

2 files changed

Lines changed: 37 additions & 8 deletions

File tree

src/routes/responses/utils.ts

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,9 @@ export const hasVisionInput = (payload: ResponsesPayload): boolean => {
7777
return values.some((item) => containsVisionContent(item))
7878
}
7979

80-
const IMAGE_DATA_URL_PATTERN = /^data:(image\/[a-zA-Z0-9.+-]+);base64,(.*)$/s
80+
const DATA_URL_PREFIX = "data:"
81+
const BASE64_MARKER = ";base64,"
82+
const IMAGE_MEDIA_TYPE_PATTERN = /^image\/[a-zA-Z0-9.+-]+$/
8183

8284
export const sanitizeOversizedInputImages = (
8385
payload: ResponsesPayload,
@@ -163,13 +165,24 @@ const getInputImageDataUrl = (
163165
return null
164166
}
165167

166-
const match = record.image_url.match(IMAGE_DATA_URL_PATTERN)
167-
if (!match) {
168+
const imageUrl = record.image_url
169+
const base64MarkerIndex = imageUrl.indexOf(BASE64_MARKER)
170+
if (
171+
!imageUrl.startsWith(DATA_URL_PREFIX)
172+
|| base64MarkerIndex <= DATA_URL_PREFIX.length
173+
) {
174+
return null
175+
}
176+
177+
const mediaType = imageUrl.slice(DATA_URL_PREFIX.length, base64MarkerIndex)
178+
if (!IMAGE_MEDIA_TYPE_PATTERN.test(mediaType)) {
168179
return null
169180
}
170181

171-
const mediaType = match[1]
172-
const decodedBytes = Buffer.byteLength(match[2], "base64")
182+
const decodedBytes = getBase64DecodedByteLength(
183+
imageUrl,
184+
base64MarkerIndex + BASE64_MARKER.length,
185+
)
173186

174187
return {
175188
decodedBytes,
@@ -178,6 +191,19 @@ const getInputImageDataUrl = (
178191
}
179192
}
180193

194+
const getBase64DecodedByteLength = (
195+
value: string,
196+
base64Start: number,
197+
): number => {
198+
const base64Length = value.length - base64Start
199+
const padding =
200+
value.endsWith("==") ? 2
201+
: value.endsWith("=") ? 1
202+
: 0
203+
204+
return Math.max(0, Math.floor((base64Length * 3) / 4) - padding)
205+
}
206+
181207
const replaceInputImageWithText = (
182208
image: InputImageDataUrl,
183209
reason: string,

tests/responses-image-sanitizer.test.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ import {
1010
const imageDataUrl = (base64Length: number): string =>
1111
`data:image/png;base64,${"A".repeat(base64Length)}`
1212

13+
const tinyPngDataUrl =
14+
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+/p9sAAAAASUVORK5CYII="
15+
1316
const makePayload = (imageUrl: string): ResponsesPayload =>
1417
({
1518
input: [
@@ -26,17 +29,17 @@ const makePayload = (imageUrl: string): ResponsesPayload =>
2629

2730
describe("sanitizeOversizedInputImages", () => {
2831
test("replaces oversized input images with text markers", () => {
29-
const payload = makePayload(imageDataUrl(16))
32+
const payload = makePayload(tinyPngDataUrl)
3033

31-
const sanitized = sanitizeOversizedInputImages(payload, 8)
34+
const sanitized = sanitizeOversizedInputImages(payload, 67)
3235

3336
expect(sanitized).toBe(1)
3437
expect(payload.input).toEqual([
3538
{
3639
content: [
3740
{ text: "look", type: "input_text" },
3841
{
39-
text: "[omitted input image: image/png, 12 bytes, max 8 bytes]",
42+
text: "[omitted input image: image/png, 68 bytes, max 67 bytes]",
4043
type: "input_text",
4144
},
4245
],

0 commit comments

Comments
 (0)