diff --git a/nginx.conf b/nginx.conf index cfd3589f..da2eaa72 100644 --- a/nginx.conf +++ b/nginx.conf @@ -33,12 +33,21 @@ http { server { listen 80; + # Allow large payloads for multimodal images / base64 payloads + client_max_body_size 32m; + 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 + # 必须严格大于应用层 DEVIN_TIMEOUT_MS(默认 600s)。改任意一边都要重新核对。 + proxy_connect_timeout 60s; + proxy_send_timeout 900s; + proxy_read_timeout 900s; + # SSE and streaming requirements proxy_set_header Connection ''; proxy_buffering off; 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' }])); + }); });