From 061d32fae45a6b4d3f2bd6a8404aa37fc463c655 Mon Sep 17 00:00:00 2001 From: justhil <138752349+justhil@users.noreply.github.com> Date: Sat, 12 Sep 2026 18:12:50 +0000 Subject: [PATCH 1/4] fix(responses): normalize multimodal content in tool call output items --- src/handlers/responses.js | 16 +++++++++--- test/responses.test.js | 55 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+), 3 deletions(-) diff --git a/src/handlers/responses.js b/src/handlers/responses.js index 8e2ed368..7da2d3e3 100644 --- a/src/handlers/responses.js +++ b/src/handlers/responses.js @@ -38,9 +38,19 @@ function normalizeMessageContent(content) { if (typeof content === 'string') return content; if (!Array.isArray(content)) return stringifyMaybe(content); + const hasContentBlock = content.some(part => part && typeof part === 'object' && typeof part.type === 'string'); + if (!hasContentBlock) { + return stringifyMaybe(content); + } + const out = []; for (const part of content) { - if (!part || typeof part !== 'object') continue; + if (!part) continue; + if (typeof part === 'string') { + out.push({ type: 'text', text: part }); + continue; + } + if (typeof part !== 'object') continue; if (part.type === 'input_text' || part.type === 'output_text' || part.type === 'text') { out.push({ type: 'text', text: part.text || '' }); } else if (part.type === 'input_image') { @@ -423,7 +433,7 @@ export function responsesToChat(body) { messages.push({ role: 'tool', tool_call_id: item.call_id || item.id, - content: stringifyMaybe(item.output ?? ''), + content: normalizeMessageContent(item.output ?? ''), }); } else if (item.type === 'custom_tool_call') { flushToolCalls.add({ @@ -437,7 +447,7 @@ export function responsesToChat(body) { messages.push({ role: 'tool', tool_call_id: item.call_id || item.id, - content: stringifyMaybe(item.output ?? ''), + content: normalizeMessageContent(item.output ?? ''), }); } } diff --git a/test/responses.test.js b/test/responses.test.js index 8a5bf67c..cbaa5323 100644 --- a/test/responses.test.js +++ b/test/responses.test.js @@ -958,4 +958,59 @@ describe('H-1 (audit 2026-07-13): input_image string image_url normalizes to obj assert.equal(imgs[0].image_url.detail, 'high'); assert.equal(imgs[1].image_url.url, 'https://z/w.png'); }); + + it('normalizes multimodal function_call_output and custom_tool_call_output items', () => { + const { messages } = responsesToChat({ + model: 'gpt-4o', + input: [ + { + type: 'function_call_output', + call_id: 'call_img', + output: [ + { type: 'input_text', text: 'Screenshot captured' }, + { type: 'input_image', image_url: 'data:image/jpeg;base64,12345' }, + ], + }, + { + type: 'custom_tool_call_output', + call_id: 'call_custom_img', + output: [ + { type: 'input_image', image_url: 'https://example.com/test.png' }, + ], + }, + ], + }); + assert.equal(messages.length, 2); + assert.equal(messages[0].role, 'tool'); + assert.equal(messages[0].tool_call_id, 'call_img'); + assert.deepEqual(messages[0].content, [ + { type: 'text', text: 'Screenshot captured' }, + { type: 'image_url', image_url: { url: 'data:image/jpeg;base64,12345' } }, + ]); + assert.equal(messages[1].role, 'tool'); + assert.equal(messages[1].tool_call_id, 'call_custom_img'); + assert.deepEqual(messages[1].content, [ + { type: 'image_url', image_url: { url: 'https://example.com/test.png' } }, + ]); + }); + + it('preserves non-content array tool outputs as JSON string', () => { + const { messages } = responsesToChat({ + model: 'gpt-4o', + input: [ + { + type: 'function_call_output', + call_id: 'call_list', + output: ['file1.txt', 'file2.txt'], + }, + { + type: 'function_call_output', + call_id: 'call_objects', + output: [{ name: 'alice' }, { name: 'bob' }], + }, + ], + }); + assert.equal(messages[0].content, JSON.stringify(['file1.txt', 'file2.txt'])); + assert.equal(messages[1].content, JSON.stringify([{ name: 'alice' }, { name: 'bob' }])); + }); }); From db9404405ced0428ec1f4ffed9905a2aa4d02513 Mon Sep 17 00:00:00 2001 From: justhil <138752349+justhil@users.noreply.github.com> Date: Sat, 12 Sep 2026 18:22:49 +0000 Subject: [PATCH 2/4] fix(nginx): increase client_max_body_size and timeouts for multimodal payloads --- nginx.conf | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/nginx.conf b/nginx.conf index cfd3589f..a009d283 100644 --- a/nginx.conf +++ b/nginx.conf @@ -33,12 +33,20 @@ http { server { listen 80; + # Allow large payloads for multimodal images / base64 payloads + client_max_body_size 100m; + location / { limit_req zone=mylimit burst=20 nodelay; proxy_pass http://windsurf_backend; proxy_http_version 1.1; + # Timeouts for long-running streaming completions and thinking models + proxy_connect_timeout 60s; + proxy_send_timeout 600s; + proxy_read_timeout 600s; + # SSE and streaming requirements proxy_set_header Connection ''; proxy_buffering off; From 4e11c4a8b0461ecf786dc0d531ccbc65e816ac4c Mon Sep 17 00:00:00 2001 From: justhil <138752349+justhil@users.noreply.github.com> Date: Sun, 13 Sep 2026 14:43:31 +0000 Subject: [PATCH 3/4] fix(nginx): raise timeouts to 900s and constrain client_max_body_size to 32m --- nginx.conf | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/nginx.conf b/nginx.conf index a009d283..0d7b267f 100644 --- a/nginx.conf +++ b/nginx.conf @@ -34,7 +34,7 @@ http { listen 80; # Allow large payloads for multimodal images / base64 payloads - client_max_body_size 100m; + client_max_body_size 32m; location / { limit_req zone=mylimit burst=20 nodelay; @@ -43,9 +43,10 @@ http { proxy_http_version 1.1; # Timeouts for long-running streaming completions and thinking models + # Set to 900s to strictly exceed application-level DEVIN_TIMEOUT_MS (600s) proxy_connect_timeout 60s; - proxy_send_timeout 600s; - proxy_read_timeout 600s; + proxy_send_timeout 900s; + proxy_read_timeout 900s; # SSE and streaming requirements proxy_set_header Connection ''; From fc8c1833aab4a5cbadd8e0766c3a26e42d139da2 Mon Sep 17 00:00:00 2001 From: justhil <138752349+justhil@users.noreply.github.com> Date: Sun, 13 Sep 2026 23:35:22 +0000 Subject: [PATCH 4/4] chore(nginx): rephrase timeout comment as an invariant --- nginx.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nginx.conf b/nginx.conf index 0d7b267f..da2eaa72 100644 --- a/nginx.conf +++ b/nginx.conf @@ -43,7 +43,7 @@ http { proxy_http_version 1.1; # Timeouts for long-running streaming completions and thinking models - # Set to 900s to strictly exceed application-level DEVIN_TIMEOUT_MS (600s) + # 必须严格大于应用层 DEVIN_TIMEOUT_MS(默认 600s)。改任意一边都要重新核对。 proxy_connect_timeout 60s; proxy_send_timeout 900s; proxy_read_timeout 900s;