streamingChatCompletionToMessage.ts 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171
  1. import { FunctionToolCall, Message, OutputMessage, OutputText, Refusal } from '../foundation';
  2. import { ChatCompletionChunk, ChatCompletionToolCall } from './interface';
  3. import { cloneDeep } from 'lodash';
  4. // 状态对象:记录每个请求 id + choice index 已处理的 chunk 数量
  5. export interface StreamingChatState {
  6. processedCountByIndex?: Record<string, number>;
  7. previousResult?: Message[]
  8. }
  9. export default function streamingChatCompletionToMessage(chatCompletionChunks: ChatCompletionChunk[], state?: StreamingChatState): { messages: Message[]; state?: StreamingChatState } { // There may be N answers, so the return value is a Message array
  10. const groupedChunks = groupByIndex(chatCompletionChunks);
  11. const results = groupedChunks.map((chatCompletionChunks: ChatCompletionChunk[], groupIndex: number) => {
  12. const id = chatCompletionChunks[0].id;
  13. const status = getStatus(chatCompletionChunks);
  14. // 基于 state 增量处理:仅处理新到达的 chunk 片段
  15. const stateKey = `${id}:${chatCompletionChunks[0]?.choices?.[0]?.index ?? groupIndex}`;
  16. const processedCount = state?.processedCountByIndex?.[stateKey] ?? 0;
  17. const start = processedCount > 0 ? Math.min(processedCount, chatCompletionChunks.length) : 0;
  18. const chunksToProcess = state ? chatCompletionChunks.slice(start) : chatCompletionChunks;
  19. // 若提供了 state 且本次没有新增内容,则跳过该 index
  20. if (state && chunksToProcess.length === 0) {
  21. return state.previousResult?.[groupIndex];
  22. }
  23. const previousResult = state?.previousResult?.[groupIndex];
  24. let textContent = '';
  25. let refusal = '';
  26. let functionCall = { name: '', arguments: '' };
  27. let toolCalls = [];
  28. (previousResult?.content as OutputMessage[])?.forEach((item: OutputMessage) => {
  29. item.content?.forEach((content: OutputText | Refusal | FunctionToolCall) => {
  30. if (content.type === 'output_text') {
  31. textContent += (content as OutputText).text;
  32. }
  33. if (content.type === 'refusal') {
  34. refusal += (content as Refusal).refusal;
  35. }
  36. });
  37. if (item.type === 'function_call' && !item.id) {
  38. // Chat Completion function call does not have id
  39. functionCall.name = (item as FunctionToolCall).name;
  40. functionCall.arguments = (item as FunctionToolCall).arguments;
  41. }
  42. if (item.type === 'tool_call' || (item.type === 'function_call' && item.id)) {
  43. toolCalls.push(item);
  44. }
  45. });
  46. chunksToProcess.map((chunk: ChatCompletionChunk) => {
  47. const delta = chunk.choices[0].delta;
  48. if (delta?.content) {
  49. textContent += delta.content;
  50. }
  51. if (delta?.refusal) {
  52. refusal += delta.refusal;
  53. }
  54. if (delta?.function_call) {
  55. if (delta.function_call.name) {
  56. functionCall.name += delta.function_call.name;
  57. }
  58. functionCall.arguments += delta.function_call.arguments;
  59. }
  60. if (delta?.tool_calls) {
  61. delta?.tool_calls.forEach((toolCall: ChatCompletionToolCall) => {
  62. // Chat Completion tool call may be function call or custom call
  63. const curToolCall = toolCalls.find((item: ChatCompletionToolCall) => item.id === toolCall.id);
  64. if (curToolCall) {
  65. if (toolCall?.function?.name) {
  66. curToolCall.name += toolCall.function.name;
  67. curToolCall.arguments += toolCall.function.arguments;
  68. } else if (toolCall?.custom?.name) {
  69. curToolCall.name += toolCall.custom.name;
  70. curToolCall.input += toolCall.custom.input;
  71. }
  72. curToolCall.status = status;
  73. } else {
  74. toolCalls.push({
  75. ...(toolCall as ChatCompletionToolCall)?.function,
  76. ...(toolCall as ChatCompletionToolCall)?.custom,
  77. type: (toolCall as ChatCompletionToolCall)?.function ? 'function_call' : 'custom_call',
  78. id: toolCall.id,
  79. });
  80. }
  81. });
  82. }
  83. });
  84. const outputMessage = [
  85. textContent !== '' && {
  86. type: 'output_text',
  87. text: textContent,
  88. },
  89. refusal !== '' && {
  90. type: 'refusal',
  91. refusal: refusal,
  92. },
  93. ].filter(Boolean);
  94. const outputResult = [
  95. outputMessage.length > 0 && {
  96. type: 'message',
  97. id: id,
  98. role: 'assistant',
  99. status: status,
  100. content: outputMessage
  101. },
  102. functionCall.name !== '' && {
  103. type: 'function_call',
  104. ...functionCall
  105. },
  106. ...toolCalls,
  107. ].filter(Boolean);
  108. // 更新 state:记录该 index 已处理到的 chunk 数量
  109. if (state && state.processedCountByIndex) {
  110. state.processedCountByIndex[stateKey] = chatCompletionChunks.length;
  111. } else {
  112. state = {
  113. processedCountByIndex: {
  114. [stateKey]: chatCompletionChunks.length,
  115. },
  116. };
  117. }
  118. return {
  119. id: id,
  120. role: "assistant",
  121. content: outputResult,
  122. status: status,
  123. };
  124. }).filter(Boolean) as Message[];
  125. state.previousResult = cloneDeep(results);
  126. return {
  127. messages: results,
  128. state: state
  129. };
  130. }
  131. const groupByIndex = (chatCompletionChunks: ChatCompletionChunk[]) => {
  132. const groupedChunks = [];
  133. chatCompletionChunks.forEach((chunk) => {
  134. // 确保每个 chunk 的 choices 都存在且为长度为 1 的数组
  135. // Make sure that each chunk's choices exists and is an array of length 1.
  136. chunk.choices.forEach((choice) => {
  137. const curIndex = choice.index;
  138. if (!groupedChunks[curIndex]) {
  139. groupedChunks[curIndex] = [];
  140. }
  141. groupedChunks[curIndex].push({
  142. ...chunk,
  143. choices: [choice],
  144. });
  145. });
  146. });
  147. return groupedChunks;
  148. };
  149. const getStatus = (chatCompletionChunks: ChatCompletionChunk[]) => {
  150. const lastChunk = chatCompletionChunks[chatCompletionChunks.length - 1];
  151. return lastChunk.choices[0].finish_reason !== null ? 'completed' : 'in_progress';
  152. };