Skip to content
Open
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
83 changes: 82 additions & 1 deletion src/utils/common.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,85 @@ function getHeaderValue(headers, headerName) {
return null;
}

function hasNonEmptyString(value) {
return typeof value === 'string' && value.trim().length > 0;
}

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 (
terminalContext &&
item.type === 'image_generation_call' &&
hasNonEmptyString(item.result) &&
!isCompletedStatus(item.status) &&
!isNonCompletedTerminalStatus(item.status)
) {
return {
...item,
status: 'completed'
};
}

return item;
}

function normalizeImageGenerationOutputItems(output, terminalContext = false) {
if (!Array.isArray(output)) {
return output;
}

return output.map(item => normalizeImageGenerationItemStatus(item, terminalContext));
}

export function normalizeResponsesImageGenerationStatus(payload) {
if (!payload || typeof payload !== 'object') {
return 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, isCompletedResponse);
}

if (normalized.item && typeof normalized.item === 'object') {
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, responseCompleted)
};
}

return normalized;
}

function parseRetryAfterMs(value, now = Date.now()) {
if (value === null || value === undefined) return null;

Expand Down Expand Up @@ -697,7 +776,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;
Expand Down Expand Up @@ -992,6 +1072,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);
Expand Down
133 changes: 133 additions & 0 deletions tests/responses-image-status-normalize.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
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');
});

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');
});
});