mirror of
https://github.com/lobehub/lobe-chat.git
synced 2026-06-16 12:36:07 +00:00
🐛 fix: add handling for content_part and reasoning_part events in fetchSSE (#10470)
feat: add handling for content_part and reasoning_part events in fetchSSE
This commit is contained in:
@@ -235,6 +235,135 @@ describe('fetchSSE', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('content_part and reasoning_part', () => {
|
||||
it('should handle content_part event with text and accumulate output', async () => {
|
||||
const mockOnMessageHandle = vi.fn();
|
||||
const mockOnFinish = vi.fn();
|
||||
|
||||
(fetchEventSource as any).mockImplementationOnce(
|
||||
async (url: string, options: FetchEventSourceInit) => {
|
||||
options.onopen!({ clone: () => ({ ok: true, headers: new Headers() }) } as any);
|
||||
options.onmessage!({
|
||||
event: 'content_part',
|
||||
data: JSON.stringify({ content: 'Hello', partType: 'text' }),
|
||||
} as any);
|
||||
options.onmessage!({
|
||||
event: 'content_part',
|
||||
data: JSON.stringify({ content: ' World', partType: 'text' }),
|
||||
} as any);
|
||||
},
|
||||
);
|
||||
|
||||
await fetchSSE('/', {
|
||||
onMessageHandle: mockOnMessageHandle,
|
||||
onFinish: mockOnFinish,
|
||||
responseAnimation: 'none',
|
||||
});
|
||||
|
||||
expect(mockOnMessageHandle).toHaveBeenNthCalledWith(1, {
|
||||
content: 'Hello',
|
||||
mimeType: undefined,
|
||||
partType: 'text',
|
||||
thoughtSignature: undefined,
|
||||
type: 'content_part',
|
||||
});
|
||||
expect(mockOnMessageHandle).toHaveBeenNthCalledWith(2, {
|
||||
content: ' World',
|
||||
mimeType: undefined,
|
||||
partType: 'text',
|
||||
thoughtSignature: undefined,
|
||||
type: 'content_part',
|
||||
});
|
||||
|
||||
// Verify output is accumulated correctly
|
||||
expect(mockOnFinish).toHaveBeenCalledWith('Hello World', {
|
||||
observationId: null,
|
||||
toolCalls: undefined,
|
||||
traceId: null,
|
||||
type: 'done',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle reasoning_part event with text and accumulate thinking', async () => {
|
||||
const mockOnMessageHandle = vi.fn();
|
||||
const mockOnFinish = vi.fn();
|
||||
|
||||
(fetchEventSource as any).mockImplementationOnce(
|
||||
async (url: string, options: FetchEventSourceInit) => {
|
||||
options.onopen!({ clone: () => ({ ok: true, headers: new Headers() }) } as any);
|
||||
options.onmessage!({
|
||||
event: 'reasoning_part',
|
||||
data: JSON.stringify({ content: 'Thinking:', partType: 'text' }),
|
||||
} as any);
|
||||
options.onmessage!({
|
||||
event: 'reasoning_part',
|
||||
data: JSON.stringify({ content: ' step 1', partType: 'text' }),
|
||||
} as any);
|
||||
options.onmessage!({
|
||||
event: 'content_part',
|
||||
data: JSON.stringify({ content: 'Final answer', partType: 'text' }),
|
||||
} as any);
|
||||
},
|
||||
);
|
||||
|
||||
await fetchSSE('/', {
|
||||
onMessageHandle: mockOnMessageHandle,
|
||||
onFinish: mockOnFinish,
|
||||
responseAnimation: 'none',
|
||||
});
|
||||
|
||||
// Verify reasoning is accumulated correctly
|
||||
expect(mockOnFinish).toHaveBeenCalledWith('Final answer', {
|
||||
observationId: null,
|
||||
reasoning: { content: 'Thinking: step 1' },
|
||||
toolCalls: undefined,
|
||||
traceId: null,
|
||||
type: 'done',
|
||||
});
|
||||
});
|
||||
|
||||
it('should not accumulate output for non-text content_part (e.g., image)', async () => {
|
||||
const mockOnMessageHandle = vi.fn();
|
||||
const mockOnFinish = vi.fn();
|
||||
|
||||
(fetchEventSource as any).mockImplementationOnce(
|
||||
async (url: string, options: FetchEventSourceInit) => {
|
||||
options.onopen!({ clone: () => ({ ok: true, headers: new Headers() }) } as any);
|
||||
options.onmessage!({
|
||||
event: 'content_part',
|
||||
data: JSON.stringify({
|
||||
content: 'base64imagedata',
|
||||
partType: 'image',
|
||||
mimeType: 'image/png',
|
||||
}),
|
||||
} as any);
|
||||
},
|
||||
);
|
||||
|
||||
await fetchSSE('/', {
|
||||
onMessageHandle: mockOnMessageHandle,
|
||||
onFinish: mockOnFinish,
|
||||
responseAnimation: 'none',
|
||||
});
|
||||
|
||||
expect(mockOnMessageHandle).toHaveBeenCalledWith({
|
||||
content: 'base64imagedata',
|
||||
mimeType: 'image/png',
|
||||
partType: 'image',
|
||||
thoughtSignature: undefined,
|
||||
type: 'content_part',
|
||||
});
|
||||
|
||||
// Output should be empty since image content is not accumulated
|
||||
expect(mockOnFinish).toHaveBeenCalledWith('', {
|
||||
observationId: null,
|
||||
toolCalls: undefined,
|
||||
traceId: null,
|
||||
type: 'done',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle grounding event', async () => {
|
||||
const mockOnMessageHandle = vi.fn();
|
||||
const mockOnFinish = vi.fn();
|
||||
|
||||
@@ -438,8 +438,27 @@ export const fetchSSE = async (url: string, options: RequestInit & FetchSSEOptio
|
||||
break;
|
||||
}
|
||||
|
||||
case 'reasoning_part':
|
||||
case 'reasoning_part': {
|
||||
// For reasoning_part, accumulate thinking content
|
||||
if (data.partType === 'text' && data.content) {
|
||||
thinking += data.content;
|
||||
}
|
||||
options.onMessageHandle?.({
|
||||
content: data.content,
|
||||
mimeType: data.mimeType,
|
||||
partType: data.partType,
|
||||
thoughtSignature: data.thoughtSignature,
|
||||
type: ev.event,
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case 'content_part': {
|
||||
// For content_part, accumulate text content to output
|
||||
// This is critical for Gemini 2.5 models which use content_part instead of text events
|
||||
if (data.partType === 'text' && data.content) {
|
||||
output += data.content;
|
||||
}
|
||||
options.onMessageHandle?.({
|
||||
content: data.content,
|
||||
mimeType: data.mimeType,
|
||||
|
||||
Reference in New Issue
Block a user