- sse-parser.ts: Anthropic SSE 파서 (content_block_delta.text 추출) - CloudSyncService.invokeFunctionStream(): raw fetch + SSE ReadableStream - PremiumLLMService.chatStream(): stream=true, 토큰 단위 yield - SSE 실패 시 자동 비스트리밍 fallback - AbortController 연결 (취소 지원)
77 lines
2.2 KiB
TypeScript
77 lines
2.2 KiB
TypeScript
// 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<Uint8Array>을 파싱하여 텍스트 토큰을 yield.
|
|
* Anthropic SSE 형식: "data: {json}\n\n" 라인 단위.
|
|
* content_block_delta.delta.text 추출, message_stop 또는 [DONE] 시 종료.
|
|
*/
|
|
export async function* parseAnthropicSSE(
|
|
stream: ReadableStream<Uint8Array>,
|
|
): AsyncGenerator<string> {
|
|
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()
|
|
}
|
|
}
|