// src/main/utils/sse-parser.ts // Anthropic Claude Messages API SSE 스트림 파서 // content_block_delta 이벤트에서 텍스트 토큰을 추출하는 AsyncGenerator interface ContentBlockDelta { type: 'content_block_delta' delta: { type: 'text_delta' text: string } } interface SSEEvent { type: string [key: string]: unknown } /** * ReadableStream을 파싱하여 텍스트 토큰을 yield. * Anthropic SSE 형식: "data: {json}\n\n" 라인 단위. * content_block_delta.delta.text 추출, message_stop 또는 [DONE] 시 종료. */ export async function* parseAnthropicSSE( stream: ReadableStream, ): AsyncGenerator { const reader = stream.getReader() const decoder = new TextDecoder() let buffer = '' try { while (true) { const { done, value } = await reader.read() if (done) break buffer += decoder.decode(value, { stream: true }) // 라인 단위 분리 const lines = buffer.split('\n') // 마지막 줄은 불완전할 수 있으므로 버퍼에 유지 buffer = lines.pop() ?? '' for (const line of lines) { const trimmed = line.trim() // 빈 줄 또는 이벤트 타입 라인 (event:) 건너뜀 if (!trimmed || trimmed.startsWith('event:')) continue // "data: [DONE]" — 종료 시그널 if (trimmed === 'data: [DONE]') return // "data: {...}" — JSON 파싱 if (trimmed.startsWith('data: ')) { const json = trimmed.substring(6) try { const evt = JSON.parse(json) as SSEEvent // message_stop → 스트림 종료 if (evt.type === 'message_stop') return // content_block_delta → 텍스트 토큰 yield if (evt.type === 'content_block_delta') { const delta = evt as unknown as ContentBlockDelta if (delta.delta?.type === 'text_delta' && delta.delta.text) { yield delta.delta.text } } // 그 외 이벤트 (message_start, content_block_start 등)는 건너뜀 } catch { // JSON 파싱 실패 — 건너뜀 } } } } } finally { reader.releaseLock() } }