Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions nginx.conf
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
16 changes: 13 additions & 3 deletions src/handlers/responses.js
Original file line number Diff line number Diff line change
Expand Up @@ -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') {
Expand Down Expand Up @@ -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({
Expand All @@ -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 ?? ''),
});
}
}
Expand Down
55 changes: 55 additions & 0 deletions test/responses.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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' }]));
});
});