UNPKG

@lobehub/chat

Version:

Lobe Chat - an open-source, high-performance chatbot framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.

245 lines (213 loc) 6.63 kB
import { ChatStreamCallbacks } from '@/libs/agent-runtime'; import { ModelTokensUsage } from '@/types/message'; import { AgentRuntimeErrorType } from '../../error'; /** * context in the stream to save temporarily data */ export interface StreamContext { id: string; /** * As pplx citations is in every chunk, but we only need to return it once * this flag is used to check if the pplx citation is returned,and then not return it again. * Same as Hunyuan and Wenxin */ returnedCitation?: boolean; thinking?: { id: string; name: string; }; tool?: { id: string; index: number; name: string; }; toolIndex?: number; usage?: ModelTokensUsage; } export interface StreamProtocolChunk { data: any; id?: string; type: // pure text | 'text' // Tools use | 'tool_calls' // Model Thinking | 'reasoning' // use for reasoning signature, maybe only anthropic | 'reasoning_signature' // flagged reasoning signature | 'flagged_reasoning_signature' // Search or Grounding | 'grounding' // stop signal | 'stop' // Error | 'error' // token usage | 'usage' // unknown data result | 'data'; } export interface StreamToolCallChunkData { function?: { arguments?: string; name?: string | null; }; id?: string; index: number; type: 'function' | string; } export interface StreamProtocolToolCallChunk { data: StreamToolCallChunkData[]; id: string; index: number; type: 'tool_calls'; } export const generateToolCallId = (index: number, functionName?: string) => `${functionName || 'unknown_tool_call'}_${index}`; const chatStreamable = async function* <T>(stream: AsyncIterable<T>) { for await (const response of stream) { yield response; } }; const ERROR_CHUNK_PREFIX = '%FIRST_CHUNK_ERROR%: '; // make the response to the streamable format export const convertIterableToStream = <T>(stream: AsyncIterable<T>) => { const iterable = chatStreamable(stream); // copy from https://github.com/vercel/ai/blob/d3aa5486529e3d1a38b30e3972b4f4c63ea4ae9a/packages/ai/streams/ai-stream.ts#L284 // and add an error handle let it = iterable[Symbol.asyncIterator](); return new ReadableStream<T>({ async cancel(reason) { await it.return?.(reason); }, async pull(controller) { const { done, value } = await it.next(); if (done) controller.close(); else controller.enqueue(value); }, async start(controller) { try { const { done, value } = await it.next(); if (done) controller.close(); else controller.enqueue(value); } catch (e) { const error = e as Error; controller.enqueue( (ERROR_CHUNK_PREFIX + JSON.stringify({ message: error.message, name: error.name, stack: error.stack })) as T, ); controller.close(); } }, }); }; /** * Create a transformer to convert the response into an SSE format */ export const createSSEProtocolTransformer = ( transformer: (chunk: any, stack: StreamContext) => StreamProtocolChunk | StreamProtocolChunk[], streamStack?: StreamContext, ) => new TransformStream({ transform: (chunk, controller) => { const result = transformer(chunk, streamStack || { id: '' }); const buffers = Array.isArray(result) ? result : [result]; buffers.forEach(({ type, id, data }) => { controller.enqueue(`id: ${id}\n`); controller.enqueue(`event: ${type}\n`); controller.enqueue(`data: ${JSON.stringify(data)}\n\n`); }); }, }); export function createCallbacksTransformer(cb: ChatStreamCallbacks | undefined) { const textEncoder = new TextEncoder(); let aggregatedResponse = ''; let currentType = ''; const callbacks = cb || {}; return new TransformStream({ async flush(): Promise<void> { if (callbacks.onCompletion) { await callbacks.onCompletion(aggregatedResponse); } if (callbacks.onFinal) { await callbacks.onFinal(aggregatedResponse); } }, async start(): Promise<void> { if (callbacks.onStart) await callbacks.onStart(); }, async transform(chunk: string, controller): Promise<void> { controller.enqueue(textEncoder.encode(chunk)); // track the type of the chunk if (chunk.startsWith('event:')) { currentType = chunk.split('event:')[1].trim(); } // if the message is a data chunk, handle the callback else if (chunk.startsWith('data:')) { const content = chunk.split('data:')[1].trim(); switch (currentType) { case 'text': { await callbacks.onText?.(content); await callbacks.onToken?.(JSON.parse(content)); break; } case 'tool_calls': { // TODO: make on ToolCall callback await callbacks.onToolCall?.(); } } } }, }); } export const FIRST_CHUNK_ERROR_KEY = '_isFirstChunkError'; export const createFirstErrorHandleTransformer = ( errorHandler?: (errorJson: any) => any, provider?: string, ) => { return new TransformStream({ transform(chunk, controller) { if (chunk.toString().startsWith(ERROR_CHUNK_PREFIX)) { const errorData = JSON.parse(chunk.toString().replace(ERROR_CHUNK_PREFIX, '')); controller.enqueue({ ...errorData, [FIRST_CHUNK_ERROR_KEY]: true, errorType: errorHandler?.(errorData) || AgentRuntimeErrorType.ProviderBizError, provider, }); } else { controller.enqueue(chunk); } }, }); }; /** * create a transformer to remove SSE format data */ export const createSSEDataExtractor = () => new TransformStream({ transform(chunk: Uint8Array, controller) { // 将 Uint8Array 转换为字符串 const text = new TextDecoder().decode(chunk, { stream: true }); // 处理多行数据的情况 const lines = text.split('\n'); for (const line of lines) { // 只处理以 "data: " 开头的行 if (line.startsWith('data: ')) { // 提取 "data: " 后面的实际数据 const jsonText = line.slice(6); // 跳过心跳消息 if (jsonText === '[DONE]') continue; try { // 解析 JSON 数据 const data = JSON.parse(jsonText); // 将解析后的数据传递给下一个处理器 controller.enqueue(data); } catch { console.warn('Failed to parse SSE data:', jsonText); } } } }, });