diff --git a/flow-state/vite.config.js b/flow-state/vite.config.js index 9319bc7..458ad65 100644 --- a/flow-state/vite.config.js +++ b/flow-state/vite.config.js @@ -198,24 +198,57 @@ function llmGatewayProxy() { body: req.method === 'GET' || req.method === 'HEAD' ? undefined : body, }); - const contentType = upstream.headers.get('content-type'); + const contentType = upstream.headers.get('content-type') || ''; + const isStream = contentType.includes('event-stream') || contentType.includes('text/event-stream'); + // Stream SSE straight through so streamObject sees incremental chunks. + if (isStream && upstream.body) { + res.writeHead(upstream.status, { + 'content-type': contentType, + 'access-control-allow-origin': '*', + 'cache-control': 'no-cache', + }); + const reader = upstream.body.getReader(); + let totalBytes = 0; + const chunks = []; + try { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + totalBytes += value.byteLength; + if (chunks.length < 64) chunks.push(Buffer.from(value).toString('utf8')); + res.write(value); + } + } finally { + try { await reader.cancel(); } catch { /* ignore */ } + } + res.end(); + const elapsed = Date.now() - started; + safeAppend({ + ts: new Date().toISOString(), + dir: '←', + status: upstream.status, + elapsedMs: elapsed, + contentType, + body: chunks.join('').slice(0, 16384) || `[stream ${totalBytes} bytes]`, + }); + console.info(`[llm-proxy] ← OK (stream) ${req.method} ${pathAfter} → ${upstream.status} (${elapsed}ms, ${totalBytes} bytes)`); + return; + } + + // Non-streaming: buffer then forward + const buf = Buffer.from(await upstream.arrayBuffer()); res.writeHead(upstream.status, { ...(contentType ? { 'content-type': contentType } : {}), 'access-control-allow-origin': '*', }); - - const buf = Buffer.from(await upstream.arrayBuffer()); res.end(buf); - // Log response (sanitized — still no token in body) const elapsed = Date.now() - started; let respLog = ''; try { - const ct = contentType || ''; - if (ct.includes('json') || ct.includes('event-stream') || buf.length < 32768) { + if (contentType.includes('json') || buf.length < 32768) { const text = buf.toString('utf8'); - // For SSE streams, keep first 16k; for JSON, keep parsed form - if (ct.includes('json')) { + if (contentType.includes('json')) { try { respLog = JSON.stringify(JSON.parse(text)).slice(0, 16384); } catch { respLog = text.slice(0, 8192); } } else { respLog = text.slice(0, 8192); @@ -234,7 +267,6 @@ function llmGatewayProxy() { body: respLog, }); - // Also emit to vite's console for live tailing const tag = upstream.status >= 400 ? '← ERR' : '← OK'; console.info(`[llm-proxy] ${tag} ${req.method} ${pathAfter} → ${upstream.status} (${elapsed}ms, ${buf.length} bytes)`); } catch (err) {