import { PNG_BASE64, imageFormRequest, readSseEvents, startHangingImagesStreamUpstream, startImagesAcceptedTaskStreamFallbackUpstream, startImagesJsonUpstream, startImagesStreamFallbackUpstream, startStreamingImageUpstream } from './route-test-helpers'; import { registerRouteTestLifecycle } from './route-test-setup'; import assert from 'node:assert/strict'; import { describe, it } from 'node:test'; registerRouteTestLifecycle(); describe('POST /api/images Images API streaming', { concurrency: false }, () => { it('normalizes OtokAPI image stream events into the stable client SSE contract', async () => { const { POST } = await import('./route'); let upstreamBody = ''; const upstream = await startStreamingImageUpstream(async (body) => { upstreamBody = body; return [ { event: 'image.generation.chunk', data: { b64_json: 'partial-base64' } }, { event: 'image.generation.result', data: { data: [{ b64_json: PNG_BASE64 }], usage: { input_tokens: 1, output_tokens: 2, total_tokens: 3 } } } ]; }); try { const response = await POST( imageFormRequest({ apiBaseUrl: upstream.baseUrl, apiKey: 'test-key', stream: true }) ); assert.equal(response.status, 200); assert.equal(response.headers.get('content-type'), 'text/event-stream'); const events = await readSseEvents(response); assert.equal(events.length, 3); assert.equal(events[0].type, 'partial_image'); assert.equal(events[0].b64_json, 'partial-base64'); assert.equal(events[1].type, 'completed'); assert.equal(events[1].b64_json, PNG_BASE64); assert.equal(events[1].output_format, 'png'); assert.equal(events[2].type, 'done'); assert.equal((events[2].images as Array>)[0].b64_json, PNG_BASE64); const upstreamJson = JSON.parse(upstreamBody) as Record; assert.equal(upstreamJson.stream, true); assert.equal(upstreamJson.partial_images, 2); } finally { await upstream.close(); } }); it('uses a non-streaming channel request mode when page auto streaming has no SSE channel', async () => { const { POST } = await import('./route'); const upstreamBodies: string[] = []; let observedIdempotencyKey: string | string[] | undefined; const upstream = await startImagesJsonUpstream(async (body, _url, request) => { if (request.method === 'POST') { upstreamBodies.push(body); observedIdempotencyKey = request.headers['idempotency-key']; } return { data: [{ b64_json: PNG_BASE64 }] }; }); process.env.OPENAI_CHANNEL_1_ID = 'json-only'; process.env.OPENAI_CHANNEL_1_BASE_URL = upstream.baseUrl; process.env.OPENAI_CHANNEL_1_API_KEYS = 'test-key'; process.env.OPENAI_CHANNEL_1_REQUEST_MODES = 'images-non-stream'; try { const response = await POST( imageFormRequest({ streamMode: 'auto' }) ); assert.equal(response.status, 200); assert.notEqual(response.headers.get('content-type'), 'text/event-stream'); assert.equal(upstreamBodies.length, 1); const upstreamJson = JSON.parse(upstreamBodies[0] || '{}') as Record; assert.equal(upstreamJson.stream, false); assert.equal(Object.hasOwn(upstreamJson, 'partial_images'), false); assert.equal(observedIdempotencyKey, 'client-route-stream'); } finally { await upstream.close(); } }); it('passes force_request through page generation and lets upstream decide small sizes', async () => { const { POST } = await import('./route'); const upstreamBodies: string[] = []; const upstream = await startImagesJsonUpstream(async (body, _url, request) => { if (request.method === 'POST') { upstreamBodies.push(body); } return { data: [{ b64_json: PNG_BASE64 }] }; }); process.env.OPENAI_CHANNEL_1_ID = 'json-force'; process.env.OPENAI_CHANNEL_1_BASE_URL = upstream.baseUrl; process.env.OPENAI_CHANNEL_1_API_KEYS = 'test-key'; process.env.OPENAI_CHANNEL_1_REQUEST_MODES = 'images-non-stream'; try { const response = await POST( imageFormRequest({ streamMode: 'auto', size: '512x512', forceRequest: 'true', clientRequestId: 'force-small-size' }) ); assert.equal(response.status, 200); assert.equal(upstreamBodies.length, 1); const upstreamJson = JSON.parse(upstreamBodies[0] || '{}') as Record; assert.equal(upstreamJson.size, '512x512'); } finally { await upstream.close(); } }); it('uses the lower-cost non-streaming channel request mode for page auto streaming by default', async () => { const { POST } = await import('./route'); const upstreamBodies: string[] = []; const upstream = await startImagesJsonUpstream(async (body, _url, request) => { if (request.method === 'POST') { upstreamBodies.push(body); } return { data: [{ b64_json: PNG_BASE64 }] }; }); process.env.OPENAI_CHANNEL_1_ID = 'mixed'; process.env.OPENAI_CHANNEL_1_BASE_URL = upstream.baseUrl; process.env.OPENAI_CHANNEL_1_API_KEYS = 'test-key'; process.env.OPENAI_CHANNEL_1_REQUEST_MODES = 'images-non-stream,images-sse'; try { const response = await POST( imageFormRequest({ streamMode: 'auto' }) ); assert.equal(response.status, 200); assert.notEqual(response.headers.get('content-type'), 'text/event-stream'); assert.equal(upstreamBodies.length, 1); const upstreamJson = JSON.parse(upstreamBodies[0] || '{}') as Record; assert.equal(upstreamJson.stream, false); assert.equal(Object.hasOwn(upstreamJson, 'partial_images'), false); } finally { await upstream.close(); } }); it('retries accepted async image tasks with the same upstream idempotency key', async () => { const { POST } = await import('./route'); const observedIdempotencyKeys: Array = []; let upstreamCalls = 0; const upstream = await startImagesJsonUpstream(async (_body, _url, request) => { if (request.method !== 'POST') { return { data: [{ b64_json: PNG_BASE64 }] }; } upstreamCalls += 1; observedIdempotencyKeys.push(request.headers['idempotency-key']); if (upstreamCalls === 1) { return { object: 'image.task', status: 'pending', task_id: 'sync-gen-task', poll_url: '/api/image-tasks?ids=sync-gen-task' }; } return { data: [{ b64_json: PNG_BASE64 }] }; }); process.env.OPENAI_CHANNEL_1_ID = 'json-task'; process.env.OPENAI_CHANNEL_1_BASE_URL = upstream.baseUrl; process.env.OPENAI_CHANNEL_1_API_KEYS = 'test-key'; process.env.OPENAI_CHANNEL_1_REQUEST_MODES = 'images-non-stream'; try { const response = await POST( imageFormRequest({ streamMode: 'auto', clientRequestId: 'accepted-task-retry-key' }) ); assert.equal(response.status, 200); assert.notEqual(response.headers.get('content-type'), 'text/event-stream'); assert.equal(upstreamCalls, 2); assert.deepEqual(observedIdempotencyKeys, ['accepted-task-retry-key', 'accepted-task-retry-key']); } finally { await upstream.close(); } }); it('retries accepted async edit tasks with the same upstream idempotency key', async () => { const { POST } = await import('./route'); const observedIdempotencyKeys: Array = []; let upstreamCalls = 0; const upstream = await startImagesJsonUpstream(async (_body, _url, request) => { if (request.method !== 'POST') { return { data: [{ b64_json: PNG_BASE64 }] }; } upstreamCalls += 1; observedIdempotencyKeys.push(request.headers['idempotency-key']); if (upstreamCalls === 1) { return { object: 'image.task', status: 'pending', task_id: 'sync-edit-task', poll_url: '/api/image-tasks?ids=sync-edit-task' }; } return { data: [{ b64_json: PNG_BASE64 }] }; }); process.env.OPENAI_CHANNEL_1_ID = 'json-edit-task'; process.env.OPENAI_CHANNEL_1_BASE_URL = upstream.baseUrl; process.env.OPENAI_CHANNEL_1_API_KEYS = 'test-key'; process.env.OPENAI_CHANNEL_1_REQUEST_MODES = 'images-non-stream'; try { const response = await POST( imageFormRequest({ mode: 'edit', streamMode: 'auto', clientRequestId: 'accepted-edit-task-retry-key' }) ); assert.equal(response.status, 200); assert.notEqual(response.headers.get('content-type'), 'text/event-stream'); assert.equal(upstreamCalls, 2); assert.deepEqual(observedIdempotencyKeys, ['accepted-edit-task-retry-key', 'accepted-edit-task-retry-key']); } finally { await upstream.close(); } }); it('fails explicit page stream requests instead of falling back to non-streaming request modes', async () => { const { POST } = await import('./route'); const upstreamBodies: string[] = []; const upstream = await startImagesJsonUpstream(async (body, _url, request) => { if (request.method === 'POST') { upstreamBodies.push(body); } return { data: [{ b64_json: PNG_BASE64 }] }; }); process.env.OPENAI_CHANNEL_1_ID = 'json-only'; process.env.OPENAI_CHANNEL_1_BASE_URL = upstream.baseUrl; process.env.OPENAI_CHANNEL_1_API_KEYS = 'test-key'; process.env.OPENAI_CHANNEL_1_REQUEST_MODES = 'images-non-stream'; try { const response = await POST( imageFormRequest({ streamMode: 'stream', imageStreamingStrategy: 'openai-sse' }) ); assert.equal(response.status, 503); const body = (await response.json()) as { error?: string }; assert.match(body.error || '', /images-sse/); assert.equal(upstreamBodies.length, 0); } finally { await upstream.close(); } }); it('keeps the stable SSE contract for SDK-parsed multi-image results without partial events', async () => { const { POST } = await import('./route'); const upstream = await startStreamingImageUpstream(async () => [ { data: { data: [{ b64_json: PNG_BASE64 }, { b64_json: PNG_BASE64 }] } } ]); try { const response = await POST( imageFormRequest({ apiBaseUrl: upstream.baseUrl, apiKey: 'test-key', stream: true }) ); assert.equal(response.status, 200); assert.equal(response.headers.get('content-type'), 'text/event-stream'); const events = await readSseEvents(response); assert.deepEqual( events.map((event) => event.type), ['completed', 'completed', 'done'] ); assert.equal((events[2].images as Array>).length, 2); } finally { await upstream.close(); } }); it('normalizes JSON Images responses returned to stream requests into the stable SSE contract', async () => { const { POST } = await import('./route'); let upstreamBody = ''; const upstream = await startImagesJsonUpstream(async (body) => { if (!body) return { ok: true }; upstreamBody = body; return { data: [{ b64_json: PNG_BASE64 }] }; }); try { const response = await POST( imageFormRequest({ apiBaseUrl: upstream.baseUrl, apiKey: 'test-key', stream: true, imageStreamingStrategy: 'newapi-keepalive-sse' }) ); assert.equal(response.status, 200); assert.equal(response.headers.get('content-type'), 'text/event-stream'); const events = await readSseEvents(response); assert.deepEqual( events.map((event) => event.type), ['completed', 'done'] ); assert.equal(events[0].b64_json, PNG_BASE64); assert.equal((events[1].images as Array>)[0].b64_json, PNG_BASE64); const upstreamJson = JSON.parse(upstreamBody) as Record; assert.equal(upstreamJson.stream, true); assert.equal(upstreamJson.partial_images, 2); } finally { await upstream.close(); } }); it('allows Matsca-compatible Images API streams to request four partial images', async () => { let upstreamBody = ''; let upstreamAppId: string | string[] | undefined; let upstreamAppSecret: string | string[] | undefined; const upstream = await startStreamingImageUpstream(async (body, _url, request) => { upstreamBody = body; upstreamAppId = request.headers['x-app-id']; upstreamAppSecret = request.headers['x-app-secret']; return [ { event: 'image.generation.result', data: { data: [{ b64_json: PNG_BASE64 }] } } ]; }); try { process.env.OPENAI_CHANNEL_1_ID = 'matsca'; process.env.OPENAI_CHANNEL_1_API_KEYS = 'test-key'; process.env.OPENAI_CHANNEL_1_BASE_URL = upstream.baseUrl; process.env.OPENAI_CHANNEL_1_UPSTREAM_PROFILE = 'matsca'; process.env.OPENAI_CHANNEL_1_REQUEST_MODES = 'images-non-stream,images-sse'; process.env.OPENAI_CHANNEL_1_MATSCA_APP_ID = 'app-id'; process.env.OPENAI_CHANNEL_1_MATSCA_APP_SECRET = 'app-secret'; process.env.OPENAI_CHANNEL_RECOVERY_PROBE_ENABLED = 'false'; process.env.OPENAI_CHANNEL_REQUIRE_PROBE_FOR_RECOVERY = 'false'; const { resetServerChannelStateForTests } = await import('@/lib/server-channel-router'); resetServerChannelStateForTests(); const { POST } = await import('./route'); const response = await POST( imageFormRequest({ stream: true, partialImages: '4' }) ); assert.equal(response.status, 200); assert.equal(response.headers.get('content-type'), 'text/event-stream'); await readSseEvents(response); const upstreamJson = JSON.parse(upstreamBody) as Record; assert.equal(upstreamJson.partial_images, 4); assert.equal(upstreamAppId, 'app-id'); assert.equal(upstreamAppSecret, 'app-secret'); } finally { await upstream.close(); } }); it('keeps same-credential page SSE requests queued until the active stream is released', async () => { const upstream = await startHangingImagesStreamUpstream(); try { process.env.OPENAI_CHANNEL_1_ID = 'official'; process.env.OPENAI_CHANNEL_1_API_KEYS = 'test-key'; process.env.OPENAI_CHANNEL_1_BASE_URL = upstream.baseUrl; process.env.OPENAI_CHANNEL_1_REQUEST_MODES = 'images-non-stream,images-sse'; process.env.OPENAI_CHANNEL_RECOVERY_PROBE_ENABLED = 'false'; process.env.OPENAI_CHANNEL_REQUIRE_PROBE_FOR_RECOVERY = 'false'; process.env.OPENAI_MAX_STREAMS_PER_CREDENTIAL = '1'; process.env.OPENAI_CHANNEL_QUEUE_MAX_WAIT_MS = '1000'; const { resetServerChannelStateForTests } = await import('@/lib/server-channel-router'); resetServerChannelStateForTests(); const { POST } = await import('./route'); const firstResponse = await POST( imageFormRequest({ stream: true, clientRequestId: 'client-route-queue-1' }) ); await upstream.waitForStreamRequest(); assert.equal(upstream.calls.length, 1); const secondPromise = POST( imageFormRequest({ stream: true, clientRequestId: 'client-route-queue-2' }) ); await new Promise((resolve) => setTimeout(resolve, 50)); assert.equal(upstream.calls.length, 1); await firstResponse.body?.cancel(); const secondResponse = await secondPromise; assert.equal(secondResponse.status, 200); assert.equal(secondResponse.headers.get('X-Channel-Queue-Queued'), 'true'); assert.equal(upstream.calls.length, 2); await secondResponse.body?.cancel(); } finally { await upstream.close(); } }); it('allows Matsca-compatible JSON generation fields through server channels', async () => { let upstreamBody = ''; let upstreamAppId: string | string[] | undefined; let upstreamAppSecret: string | string[] | undefined; const upstream = await startImagesJsonUpstream(async (body, _url, request) => { if (request.method === 'POST' && request.url?.endsWith('/images/generations')) { upstreamBody = body; upstreamAppId = request.headers['x-app-id']; upstreamAppSecret = request.headers['x-app-secret']; } return { data: [{ b64_json: PNG_BASE64 }] }; }); try { process.env.OPENAI_CHANNEL_1_ID = 'matsca'; process.env.OPENAI_CHANNEL_1_API_KEYS = 'test-key'; process.env.OPENAI_CHANNEL_1_BASE_URL = upstream.baseUrl; process.env.OPENAI_CHANNEL_1_UPSTREAM_PROFILE = 'matsca'; process.env.OPENAI_CHANNEL_1_REQUEST_MODES = 'images-non-stream'; process.env.OPENAI_CHANNEL_1_MATSCA_APP_ID = 'app-id'; process.env.OPENAI_CHANNEL_1_MATSCA_APP_SECRET = 'app-secret'; process.env.OPENAI_CHANNEL_RECOVERY_PROBE_ENABLED = 'false'; process.env.OPENAI_CHANNEL_REQUIRE_PROBE_FOR_RECOVERY = 'false'; const { resetServerChannelStateForTests } = await import('@/lib/server-channel-router'); resetServerChannelStateForTests(); const { POST } = await import('./route'); const response = await POST( imageFormRequest({ streamMode: 'non_stream', n: '4', size: '123x456', background: 'transparent' }) ); assert.equal(response.status, 200); const upstreamJson = JSON.parse(upstreamBody) as Record; assert.equal(upstreamJson.n, 4); assert.equal(upstreamJson.size, '123x456'); assert.equal(upstreamJson.background, 'transparent'); assert.equal(upstreamAppId, 'app-id'); assert.equal(upstreamAppSecret, 'app-secret'); } finally { await upstream.close(); } }); it('returns upstream diagnostics when JSON Images responses omit image data', async () => { const { POST } = await import('./route'); const upstream = await startImagesJsonUpstream(async () => ({ data: [{ status: 'done' }] })); try { const response = await POST( imageFormRequest({ apiBaseUrl: upstream.baseUrl, apiKey: 'test-key', streamMode: 'non_stream' }) ); assert.equal(response.status, 502); const body = (await response.json()) as { error?: string; diagnostics?: { category?: string; structure?: unknown }; }; assert.match(body.error || '', /不是 OpenAI Images 格式/); assert.equal(body.diagnostics?.category, 'unknown_response_format'); assert.equal(JSON.stringify(body.diagnostics).includes('test-key'), false); } finally { await upstream.close(); } }); it('falls back from force-sse auto streaming without a final image and skips streaming for the same mark', async () => { const { POST } = await import('./route'); const { getServerChannelState } = await import('@/lib/server-channel-router'); const upstream = await startImagesStreamFallbackUpstream(); const otherUpstream = await startImagesStreamFallbackUpstream(); try { const first = await POST( imageFormRequest({ apiBaseUrl: upstream.baseUrl, apiKey: 'test-key', streamMode: 'auto', imageStreamingStrategy: 'force-sse', clientRequestId: 'client-route-auto-fallback-1' }) ); assert.equal(first.status, 200); assert.equal(first.headers.get('content-type'), 'text/event-stream'); const events = await readSseEvents(first); assert.deepEqual( events.map((event) => event.type), ['partial_image', 'completed', 'done'] ); assert.equal(events[2].fallback_used, true); assert.deepEqual( upstream.calls.map((call) => call.stream), [true, false] ); assert.equal(getServerChannelState().streamingAvailability.summary().mark_count, 1); const second = await POST( imageFormRequest({ apiBaseUrl: upstream.baseUrl, apiKey: 'test-key', streamMode: 'auto', imageStreamingStrategy: 'force-sse', clientRequestId: 'client-route-auto-fallback-2' }) ); assert.equal(second.status, 200); assert.notEqual(second.headers.get('content-type'), 'text/event-stream'); const body = (await second.json()) as { images?: Array> }; assert.equal(body.images?.[0]?.b64_json, PNG_BASE64); assert.deepEqual( upstream.calls.map((call) => call.stream), [true, false, false] ); const third = await POST( imageFormRequest({ apiBaseUrl: otherUpstream.baseUrl, apiKey: 'test-key', streamMode: 'auto', imageStreamingStrategy: 'force-sse', clientRequestId: 'client-route-auto-fallback-3' }) ); assert.equal(third.status, 200); assert.equal(third.headers.get('content-type'), 'text/event-stream'); await readSseEvents(third); assert.deepEqual( otherUpstream.calls.map((call) => call.stream), [true, false] ); } finally { await upstream.close(); await otherUpstream.close(); } }); it('retries accepted async image tasks after force-sse auto stream fallback with the same idempotency key', async () => { const { POST } = await import('./route'); const upstream = await startImagesAcceptedTaskStreamFallbackUpstream(); try { const response = await POST( imageFormRequest({ apiBaseUrl: upstream.baseUrl, apiKey: 'test-key', streamMode: 'auto', imageStreamingStrategy: 'force-sse', clientRequestId: 'client-route-auto-fallback-task' }) ); assert.equal(response.status, 200); assert.equal(response.headers.get('content-type'), 'text/event-stream'); const events = await readSseEvents(response); assert.deepEqual( events.map((event) => event.type), ['partial_image', 'completed', 'done'] ); assert.equal(events[2].fallback_used, true); assert.deepEqual( upstream.calls.map((call) => call.stream), [true, false, false] ); assert.deepEqual( upstream.calls.map((call) => call.idempotencyKey), [ 'client-route-auto-fallback-task', 'client-route-auto-fallback-task', 'client-route-auto-fallback-task' ] ); } finally { await upstream.close(); } }); it('does not mark force-sse auto streaming unavailable when the page SSE request is aborted', async () => { const { POST } = await import('./route'); const { getServerChannelState } = await import('@/lib/server-channel-router'); const upstream = await startHangingImagesStreamUpstream(); const abortController = new AbortController(); try { const response = await POST( imageFormRequest({ apiBaseUrl: upstream.baseUrl, apiKey: 'test-key', streamMode: 'auto', imageStreamingStrategy: 'force-sse', clientRequestId: 'client-route-auto-abort', signal: abortController.signal }) ); assert.equal(response.status, 200); assert.equal(response.headers.get('content-type'), 'text/event-stream'); const reader = response.body?.getReader(); assert.ok(reader); await upstream.waitForStreamRequest(); abortController.abort(); await reader.cancel(); await new Promise((resolve) => setTimeout(resolve, 25)); assert.deepEqual( upstream.calls.map((call) => call.stream), [true] ); assert.equal(getServerChannelState().streamingAvailability.summary().mark_count, 0); } finally { abortController.abort(); await upstream.close(); } }); it('returns an explicit SSE error when the upstream completed event has no image payload', async () => { const { POST } = await import('./route'); const upstream = await startStreamingImageUpstream(async () => [ { event: 'image.generation.result', data: { data: [{ status: 'done' }] } } ]); try { const response = await POST( imageFormRequest({ apiBaseUrl: upstream.baseUrl, apiKey: 'test-key', stream: true }) ); assert.equal(response.status, 200); assert.equal(response.headers.get('content-type'), 'text/event-stream'); const events = await readSseEvents(response); assert.equal(events.length, 1); assert.equal(events[0].type, 'error'); assert.match(String(events[0].error), /b64_json/); } finally { await upstream.close(); } }); it('turns upstream stream failures into the stable SSE error contract', async () => { const { POST } = await import('./route'); const upstream = await startStreamingImageUpstream(async () => [ { event: 'image.generation.chunk', data: { b64_json: 'partial-before-failure' } }, { abortAfter: true, data: { status: 'upstream connection closed' } } ]); try { const response = await POST( imageFormRequest({ apiBaseUrl: upstream.baseUrl, apiKey: 'test-key', stream: true }) ); assert.equal(response.status, 200); assert.equal(response.headers.get('content-type'), 'text/event-stream'); const events = await readSseEvents(response); assert.deepEqual( events.map((event) => event.type), ['partial_image', 'error'] ); assert.match(String(events[1].error), /terminated|aborted|closed|stream/i); } finally { await upstream.close(); } }); });