/** * Merge streaming assistant chunks from SSE Message events. * Upstream may send deltas, cumulative snapshots, or multi-part content arrays. */ export function mergeStreamingChunk(previous, incoming) { const prev = String(previous ?? ''); const next = String(incoming ?? ''); if (!next) return prev; if (!prev) return next; if (next === prev) return prev; if (next.startsWith(prev)) return next; if (prev.startsWith(next)) return prev; if (prev.endsWith(next)) return prev; const maxOverlap = Math.min(prev.length, next.length); for (let size = maxOverlap; size > 0; size -= 1) { if (prev.endsWith(next.slice(0, size))) { return prev + next.slice(size); } } return prev + next; } function findLastContentIndex(content, type) { for (let index = content.length - 1; index >= 0; index -= 1) { if (content[index]?.type === type) return index; } return -1; } function mergeTextLikeBlock(block, incomingValue, field) { block[field] = mergeStreamingChunk(block[field], incomingValue); } export function mergeMessageContent(existingContent, incomingContent) { const merged = [...existingContent]; for (const item of incomingContent) { if (item.type === 'text') { const index = findLastContentIndex(merged, 'text'); if (index >= 0) { mergeTextLikeBlock(merged[index], item.text, 'text'); continue; } merged.push({ ...item }); continue; } if (item.type === 'thinking') { const index = findLastContentIndex(merged, 'thinking'); if (index >= 0) { mergeTextLikeBlock(merged[index], item.thinking, 'thinking'); continue; } merged.push({ ...item }); continue; } const tail = merged[merged.length - 1]; if (tail && JSON.stringify(tail) === JSON.stringify(item)) continue; merged.push({ ...item }); } return merged; }