From 21fff0d88189d8c09130ef015aadd6f8306763ac Mon Sep 17 00:00:00 2001 From: SantaDiegoKairos Date: Tue, 12 May 2026 17:01:59 +0300 Subject: [PATCH 1/2] fix: complete Responses image generation calls with results --- src/utils/common.js | 56 ++++++++++++++- .../responses-image-status-normalize.test.js | 71 +++++++++++++++++++ 2 files changed, 126 insertions(+), 1 deletion(-) create mode 100644 tests/responses-image-status-normalize.test.js diff --git a/src/utils/common.js b/src/utils/common.js index d13fc1f93..d9725ffae 100644 --- a/src/utils/common.js +++ b/src/utils/common.js @@ -92,6 +92,58 @@ function getHeaderValue(headers, headerName) { return null; } +function hasNonEmptyString(value) { + return typeof value === 'string' && value.trim().length > 0; +} + +function normalizeImageGenerationItemStatus(item) { + if (!item || typeof item !== 'object') { + return item; + } + + if (item.type === 'image_generation_call' && hasNonEmptyString(item.result)) { + return { + ...item, + status: 'completed' + }; + } + + return item; +} + +function normalizeImageGenerationOutputItems(output) { + if (!Array.isArray(output)) { + return output; + } + + return output.map(normalizeImageGenerationItemStatus); +} + +export function normalizeResponsesImageGenerationStatus(payload) { + if (!payload || typeof payload !== 'object') { + return payload; + } + + const normalized = { ...payload }; + + if (Array.isArray(normalized.output)) { + normalized.output = normalizeImageGenerationOutputItems(normalized.output); + } + + if (normalized.item && typeof normalized.item === 'object') { + normalized.item = normalizeImageGenerationItemStatus(normalized.item); + } + + if (normalized.response && typeof normalized.response === 'object') { + normalized.response = { + ...normalized.response, + output: normalizeImageGenerationOutputItems(normalized.response.output) + }; + } + + return normalized; +} + function parseRetryAfterMs(value, now = Date.now()) { if (value === null || value === undefined) return null; @@ -697,7 +749,8 @@ export async function handleStreamRequest(res, service, model, requestBody, from // 处理 chunkToSend 可能是数组或对象的情况 const chunksToSend = Array.isArray(chunkToSend) ? chunkToSend : [chunkToSend]; - for (const chunk of chunksToSend) { + for (const rawChunk of chunksToSend) { + const chunk = normalizeResponsesImageGenerationStatus(rawChunk); // 再次检查客户端连接状态 if (clientDisconnected.value) { break; @@ -992,6 +1045,7 @@ export async function handleUnaryRequest(res, service, model, requestBody, fromP logger.info(`[Response Convert] Converting response from ${toProvider} to ${fromProvider}`); clientResponse = convertData(nativeResponse, 'response', toProvider, fromProvider, model); } + clientResponse = normalizeResponsesImageGenerationStatus(clientResponse); // 监控钩子:非流式响应 const hookRequestId = getPluginHookRequestId(CONFIG); diff --git a/tests/responses-image-status-normalize.test.js b/tests/responses-image-status-normalize.test.js new file mode 100644 index 000000000..b878a4912 --- /dev/null +++ b/tests/responses-image-status-normalize.test.js @@ -0,0 +1,71 @@ +import { normalizeResponsesImageGenerationStatus } from '../src/utils/common.js'; + +describe('Responses image generation status normalization', () => { + test('marks completed response output image calls with result as completed', () => { + const response = normalizeResponsesImageGenerationStatus({ + id: 'resp_1', + status: 'completed', + output: [{ + id: 'ig_1', + type: 'image_generation_call', + status: 'generating', + result: 'iVBORw0KGgo=', + output_format: 'png' + }] + }); + + expect(response.output[0]).toMatchObject({ + id: 'ig_1', + type: 'image_generation_call', + status: 'completed', + result: 'iVBORw0KGgo=' + }); + }); + + test('marks stream terminal image items with result as completed', () => { + const event = normalizeResponsesImageGenerationStatus({ + type: 'response.output_item.done', + output_index: 0, + item: { + id: 'ig_1', + type: 'image_generation_call', + status: 'generating', + result: 'iVBORw0KGgo=' + } + }); + + expect(event.item.status).toBe('completed'); + }); + + test('marks completed event output image calls with result as completed', () => { + const event = normalizeResponsesImageGenerationStatus({ + type: 'response.completed', + response: { + id: 'resp_1', + status: 'completed', + output: [{ + id: 'ig_1', + type: 'image_generation_call', + status: 'generating', + result: 'iVBORw0KGgo=' + }] + } + }); + + expect(event.response.output[0].status).toBe('completed'); + }); + + test('does not mark in-progress image calls without result as completed', () => { + const event = normalizeResponsesImageGenerationStatus({ + type: 'response.output_item.added', + output_index: 0, + item: { + id: 'ig_1', + type: 'image_generation_call', + status: 'in_progress' + } + }); + + expect(event.item.status).toBe('in_progress'); + }); +}); From 10c9751723ab55a44382ef91943ecf323cbc251e Mon Sep 17 00:00:00 2001 From: SantaDiegoKairos Date: Fri, 15 May 2026 20:21:42 +0300 Subject: [PATCH 2/2] fix: make image generation completion normalization status-aware --- src/utils/common.js | 41 +++++++++--- .../responses-image-status-normalize.test.js | 62 +++++++++++++++++++ 2 files changed, 96 insertions(+), 7 deletions(-) diff --git a/src/utils/common.js b/src/utils/common.js index d9725ffae..1549f04bb 100644 --- a/src/utils/common.js +++ b/src/utils/common.js @@ -96,12 +96,34 @@ function hasNonEmptyString(value) { return typeof value === 'string' && value.trim().length > 0; } -function normalizeImageGenerationItemStatus(item) { +const IMAGE_GENERATION_NON_COMPLETED_STATUSES = new Set([ + 'failed', + 'incomplete', + 'cancelled', + 'canceled', + 'error' +]); + +function isCompletedStatus(value) { + return typeof value === 'string' && value.toLowerCase() === 'completed'; +} + +function isNonCompletedTerminalStatus(value) { + return typeof value === 'string' && IMAGE_GENERATION_NON_COMPLETED_STATUSES.has(value.toLowerCase()); +} + +function normalizeImageGenerationItemStatus(item, terminalContext = false) { if (!item || typeof item !== 'object') { return item; } - if (item.type === 'image_generation_call' && hasNonEmptyString(item.result)) { + if ( + terminalContext && + item.type === 'image_generation_call' && + hasNonEmptyString(item.result) && + !isCompletedStatus(item.status) && + !isNonCompletedTerminalStatus(item.status) + ) { return { ...item, status: 'completed' @@ -111,12 +133,12 @@ function normalizeImageGenerationItemStatus(item) { return item; } -function normalizeImageGenerationOutputItems(output) { +function normalizeImageGenerationOutputItems(output, terminalContext = false) { if (!Array.isArray(output)) { return output; } - return output.map(normalizeImageGenerationItemStatus); + return output.map(item => normalizeImageGenerationItemStatus(item, terminalContext)); } export function normalizeResponsesImageGenerationStatus(payload) { @@ -125,19 +147,24 @@ export function normalizeResponsesImageGenerationStatus(payload) { } const normalized = { ...payload }; + const eventType = normalized.type; + const isOutputItemDone = eventType === 'response.output_item.done'; + const isResponseCompletedEvent = eventType === 'response.completed'; + const isCompletedResponse = isCompletedStatus(normalized.status); if (Array.isArray(normalized.output)) { - normalized.output = normalizeImageGenerationOutputItems(normalized.output); + normalized.output = normalizeImageGenerationOutputItems(normalized.output, isCompletedResponse); } if (normalized.item && typeof normalized.item === 'object') { - normalized.item = normalizeImageGenerationItemStatus(normalized.item); + normalized.item = normalizeImageGenerationItemStatus(normalized.item, isOutputItemDone); } if (normalized.response && typeof normalized.response === 'object') { + const responseCompleted = isResponseCompletedEvent || isCompletedStatus(normalized.response.status); normalized.response = { ...normalized.response, - output: normalizeImageGenerationOutputItems(normalized.response.output) + output: normalizeImageGenerationOutputItems(normalized.response.output, responseCompleted) }; } diff --git a/tests/responses-image-status-normalize.test.js b/tests/responses-image-status-normalize.test.js index b878a4912..6b664a09f 100644 --- a/tests/responses-image-status-normalize.test.js +++ b/tests/responses-image-status-normalize.test.js @@ -68,4 +68,66 @@ describe('Responses image generation status normalization', () => { expect(event.item.status).toBe('in_progress'); }); + + test('does not mark non-terminal stream image items as completed even with a result', () => { + const event = normalizeResponsesImageGenerationStatus({ + type: 'response.output_item.added', + output_index: 0, + item: { + id: 'ig_1', + type: 'image_generation_call', + status: 'in_progress', + result: 'partial-or-provider-specific-value' + } + }); + + expect(event.item.status).toBe('in_progress'); + }); + + test('does not mark non-completed response output image calls as completed', () => { + const response = normalizeResponsesImageGenerationStatus({ + id: 'resp_1', + status: 'in_progress', + output: [{ + id: 'ig_1', + type: 'image_generation_call', + status: 'in_progress', + result: 'partial-or-provider-specific-value' + }] + }); + + expect(response.output[0].status).toBe('in_progress'); + }); + + test('preserves failed image calls even when a terminal payload includes a result', () => { + const event = normalizeResponsesImageGenerationStatus({ + type: 'response.completed', + response: { + id: 'resp_1', + status: 'completed', + output: [{ + id: 'ig_1', + type: 'image_generation_call', + status: 'failed', + result: 'provider-error-or-debug-payload' + }] + } + }); + + expect(event.response.output[0].status).toBe('failed'); + }); + + test('marks terminal image calls without a status but with result as completed', () => { + const event = normalizeResponsesImageGenerationStatus({ + type: 'response.output_item.done', + output_index: 0, + item: { + id: 'ig_1', + type: 'image_generation_call', + result: 'iVBORw0KGgo=' + } + }); + + expect(event.item.status).toBe('completed'); + }); });