mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-24 10:40:07 +08:00
fix(OpenAIApiClient): fix multiple reasoning content handling (#8767)
* refactor(OpenAIApiClient): 优化思考状态处理逻辑 简化思考状态的跟踪逻辑,将isFirstThinkingChunk替换为更清晰的isThinking标志 * fix(openai): 提前检查推理状态 * fix(OpenAIApiClient): 调整reasoning_content检查逻辑位置以修复处理顺序问题 * refactor(OpenAIApiClient): 优化流式响应处理的状态管理逻辑 重构流式响应处理中的状态变量和逻辑,将isFirstTextChunk改为accumulatingText以更准确描述状态 调整thinking状态和文本累积状态的判断位置,提高代码可读性和维护性 * fix(openai): 修复文本内容处理中的逻辑错误 * fix(ThinkingTagExtractionMiddleware): 修复首次文本块未正确标记的问题 * fix(ThinkingTagExtractionMiddleware): 添加调试日志以跟踪chunk处理逻辑 添加详细的silly级别日志,帮助调试不同chunk类型的处理流程 * refactor(aiCore): 移除ThinkChunkMiddleware及相关逻辑 清理不再使用的ThinkChunkMiddleware中间件及其相关导入和插入逻辑 * style(middleware): 修改日志消息中的措辞从'pass through'为'passed' * refactor(ThinkingTagExtractionMiddleware): 优化思考标签提取中间件的状态管理 用 accumulatingText 状态替代 isFirstTextChunk,更清晰地管理文本积累状态 简化逻辑,在完成思考或提取到思考内容时更新状态 添加注释 * fix(ThinkingTagExtractionMiddleware): 修复accumulatingText默认值错误 将accumulatingText的默认值从true改为false,避免初始状态下错误地累积内容 * fix(aiCore): 修复思考标签提取中间件和OpenAI客户端的逻辑错误 修正ThinkingTagExtractionMiddleware中accumulatingText状态判断条件 优化OpenAIApiClient中思考和文本块的状态管理 添加测试用例验证多段思考和文本块的处理 * refactor(aiCore): 移除调试日志以减少日志噪音
This commit is contained in:
parent
84604a176b
commit
0be7d97c3f
@ -716,8 +716,8 @@ export class OpenAIAPIClient extends OpenAIBaseClient<
|
||||
isFinished = true
|
||||
}
|
||||
|
||||
let isFirstThinkingChunk = true
|
||||
let isFirstTextChunk = true
|
||||
let isThinking = false
|
||||
let accumulatingText = false
|
||||
return (context: ResponseChunkTransformerContext) => ({
|
||||
async transform(chunk: OpenAISdkRawChunk, controller: TransformStreamDefaultController<GenericChunk>) {
|
||||
const isOpenRouter = context.provider?.id === 'openrouter'
|
||||
@ -774,6 +774,15 @@ export class OpenAIAPIClient extends OpenAIBaseClient<
|
||||
contentSource = choice.message
|
||||
}
|
||||
|
||||
// 状态管理
|
||||
if (!contentSource?.content) {
|
||||
accumulatingText = false
|
||||
}
|
||||
// @ts-ignore - reasoning_content is not in standard OpenAI types but some providers use it
|
||||
if (!contentSource?.reasoning_content && !contentSource?.reasoning) {
|
||||
isThinking = false
|
||||
}
|
||||
|
||||
if (!contentSource) {
|
||||
if ('finish_reason' in choice && choice.finish_reason) {
|
||||
// For OpenRouter, don't emit completion signals immediately after finish_reason
|
||||
@ -811,30 +820,41 @@ export class OpenAIAPIClient extends OpenAIBaseClient<
|
||||
// @ts-ignore - reasoning_content is not in standard OpenAI types but some providers use it
|
||||
const reasoningText = contentSource.reasoning_content || contentSource.reasoning
|
||||
if (reasoningText) {
|
||||
if (isFirstThinkingChunk) {
|
||||
// logger.silly('since reasoningText is trusy, try to enqueue THINKING_START AND THINKING_DELTA')
|
||||
if (!isThinking) {
|
||||
// logger.silly('since isThinking is falsy, try to enqueue THINKING_START')
|
||||
controller.enqueue({
|
||||
type: ChunkType.THINKING_START
|
||||
} as ThinkingStartChunk)
|
||||
isFirstThinkingChunk = false
|
||||
isThinking = true
|
||||
}
|
||||
|
||||
// logger.silly('enqueue THINKING_DELTA')
|
||||
controller.enqueue({
|
||||
type: ChunkType.THINKING_DELTA,
|
||||
text: reasoningText
|
||||
})
|
||||
} else {
|
||||
isThinking = false
|
||||
}
|
||||
|
||||
// 处理文本内容
|
||||
if (contentSource.content) {
|
||||
if (isFirstTextChunk) {
|
||||
// logger.silly('since contentSource.content is trusy, try to enqueue TEXT_START and TEXT_DELTA')
|
||||
if (!accumulatingText) {
|
||||
// logger.silly('enqueue TEXT_START')
|
||||
controller.enqueue({
|
||||
type: ChunkType.TEXT_START
|
||||
} as TextStartChunk)
|
||||
isFirstTextChunk = false
|
||||
accumulatingText = true
|
||||
}
|
||||
// logger.silly('enqueue TEXT_DELTA')
|
||||
controller.enqueue({
|
||||
type: ChunkType.TEXT_DELTA,
|
||||
text: contentSource.content
|
||||
})
|
||||
} else {
|
||||
accumulatingText = false
|
||||
}
|
||||
|
||||
// 处理工具调用
|
||||
|
||||
@ -20,7 +20,6 @@ import { MIDDLEWARE_NAME as FinalChunkConsumerMiddlewareName } from './middlewar
|
||||
import { applyCompletionsMiddlewares } from './middleware/composer'
|
||||
import { MIDDLEWARE_NAME as McpToolChunkMiddlewareName } from './middleware/core/McpToolChunkMiddleware'
|
||||
import { MIDDLEWARE_NAME as RawStreamListenerMiddlewareName } from './middleware/core/RawStreamListenerMiddleware'
|
||||
import { MIDDLEWARE_NAME as ThinkChunkMiddlewareName } from './middleware/core/ThinkChunkMiddleware'
|
||||
import { MIDDLEWARE_NAME as WebSearchMiddlewareName } from './middleware/core/WebSearchMiddleware'
|
||||
import { MIDDLEWARE_NAME as ImageGenerationMiddlewareName } from './middleware/feat/ImageGenerationMiddleware'
|
||||
import { MIDDLEWARE_NAME as ThinkingTagExtractionMiddlewareName } from './middleware/feat/ThinkingTagExtractionMiddleware'
|
||||
@ -120,8 +119,6 @@ export default class AiProvider {
|
||||
logger.silly('ErrorHandlerMiddleware is removed')
|
||||
builder.remove(FinalChunkConsumerMiddlewareName)
|
||||
logger.silly('FinalChunkConsumerMiddleware is removed')
|
||||
builder.insertBefore(ThinkChunkMiddlewareName, MiddlewareRegistry[ThinkingTagExtractionMiddlewareName])
|
||||
logger.silly('ThinkingTagExtractionMiddleware is inserted')
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -70,12 +70,13 @@ export const ThinkingTagExtractionMiddleware: CompletionsMiddleware =
|
||||
let hasThinkingContent = false
|
||||
let thinkingStartTime = 0
|
||||
|
||||
let isFirstTextChunk = true
|
||||
let accumulatingText = false
|
||||
let accumulatedThinkingContent = ''
|
||||
const processedStream = resultFromUpstream.pipeThrough(
|
||||
new TransformStream<GenericChunk, GenericChunk>({
|
||||
transform(chunk: GenericChunk, controller) {
|
||||
logger.silly('chunk', chunk)
|
||||
|
||||
if (chunk.type === ChunkType.TEXT_DELTA) {
|
||||
const textChunk = chunk as TextDeltaChunk
|
||||
|
||||
@ -84,6 +85,13 @@ export const ThinkingTagExtractionMiddleware: CompletionsMiddleware =
|
||||
|
||||
for (const extractionResult of extractionResults) {
|
||||
if (extractionResult.complete && extractionResult.tagContentExtracted?.trim()) {
|
||||
// 完成思考
|
||||
// logger.silly(
|
||||
// 'since extractionResult.complete and extractionResult.tagContentExtracted is not empty, THINKING_COMPLETE chunk is generated'
|
||||
// )
|
||||
// 如果完成思考,更新状态
|
||||
accumulatingText = false
|
||||
|
||||
// 生成 THINKING_COMPLETE 事件
|
||||
const thinkingCompleteChunk: ThinkingCompleteChunk = {
|
||||
type: ChunkType.THINKING_COMPLETE,
|
||||
@ -96,7 +104,13 @@ export const ThinkingTagExtractionMiddleware: CompletionsMiddleware =
|
||||
hasThinkingContent = false
|
||||
thinkingStartTime = 0
|
||||
} else if (extractionResult.content.length > 0) {
|
||||
// logger.silly(
|
||||
// 'since extractionResult.content is not empty, try to generate THINKING_START/THINKING_DELTA chunk'
|
||||
// )
|
||||
if (extractionResult.isTagContent) {
|
||||
// 如果提取到思考内容,更新状态
|
||||
accumulatingText = false
|
||||
|
||||
// 第一次接收到思考内容时记录开始时间
|
||||
if (!hasThinkingContent) {
|
||||
hasThinkingContent = true
|
||||
@ -116,11 +130,17 @@ export const ThinkingTagExtractionMiddleware: CompletionsMiddleware =
|
||||
controller.enqueue(thinkingDeltaChunk)
|
||||
}
|
||||
} else {
|
||||
if (isFirstTextChunk) {
|
||||
// 如果没有思考内容,直接输出文本
|
||||
// logger.silly(
|
||||
// 'since extractionResult.isTagContent is falsy, try to generate TEXT_START/TEXT_DELTA chunk'
|
||||
// )
|
||||
// 在非组成文本状态下接收到非思考内容时,生成 TEXT_START chunk 并更新状态
|
||||
if (!accumulatingText) {
|
||||
// logger.silly('since accumulatingText is false, TEXT_START chunk is generated')
|
||||
controller.enqueue({
|
||||
type: ChunkType.TEXT_START
|
||||
})
|
||||
isFirstTextChunk = false
|
||||
accumulatingText = true
|
||||
}
|
||||
// 发送清理后的文本内容
|
||||
const cleanTextChunk: TextDeltaChunk = {
|
||||
@ -129,11 +149,20 @@ export const ThinkingTagExtractionMiddleware: CompletionsMiddleware =
|
||||
}
|
||||
controller.enqueue(cleanTextChunk)
|
||||
}
|
||||
} else {
|
||||
// logger.silly('since both condition is false, skip')
|
||||
}
|
||||
}
|
||||
} else if (chunk.type !== ChunkType.TEXT_START) {
|
||||
// logger.silly('since chunk.type is not TEXT_START and not TEXT_DELTA, pass through')
|
||||
|
||||
// logger.silly('since chunk.type is not TEXT_START and not TEXT_DELTA, accumulatingText is set to false')
|
||||
accumulatingText = false
|
||||
// 其他类型的chunk直接传递(包括 THINKING_DELTA, THINKING_COMPLETE 等)
|
||||
controller.enqueue(chunk)
|
||||
} else {
|
||||
// 接收到的 TEXT_START chunk 直接丢弃
|
||||
// logger.silly('since chunk.type is TEXT_START, passed')
|
||||
}
|
||||
},
|
||||
flush(controller) {
|
||||
|
||||
@ -9,10 +9,9 @@ import {
|
||||
import { FinishReason, MediaModality } from '@google/genai'
|
||||
import { FunctionCall } from '@google/genai'
|
||||
import AiProvider from '@renderer/aiCore'
|
||||
import { OpenAIAPIClient, ResponseChunkTransformerContext } from '@renderer/aiCore/clients'
|
||||
import { BaseApiClient, OpenAIAPIClient, ResponseChunkTransformerContext } from '@renderer/aiCore/clients'
|
||||
import { AnthropicAPIClient } from '@renderer/aiCore/clients/anthropic/AnthropicAPIClient'
|
||||
import { ApiClientFactory } from '@renderer/aiCore/clients/ApiClientFactory'
|
||||
import { BaseApiClient } from '@renderer/aiCore/clients/BaseApiClient'
|
||||
import { GeminiAPIClient } from '@renderer/aiCore/clients/gemini/GeminiAPIClient'
|
||||
import { OpenAIResponseAPIClient } from '@renderer/aiCore/clients/openai/OpenAIResponseAPIClient'
|
||||
import { GenericChunk } from '@renderer/aiCore/middleware/schemas'
|
||||
@ -35,13 +34,12 @@ import {
|
||||
OpenAISdkRawChunk,
|
||||
OpenAISdkRawContentSource
|
||||
} from '@renderer/types/sdk'
|
||||
import * as McpToolsModule from '@renderer/utils/mcp-tools'
|
||||
import { mcpToolCallResponseToGeminiMessage } from '@renderer/utils/mcp-tools'
|
||||
import * as McpToolsModule from '@renderer/utils/mcp-tools'
|
||||
import { cloneDeep } from 'lodash'
|
||||
import OpenAI from 'openai'
|
||||
import { ChatCompletionChunk } from 'openai/resources'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
// Mock the ApiClientFactory
|
||||
vi.mock('@renderer/aiCore/clients/ApiClientFactory', () => ({
|
||||
ApiClientFactory: {
|
||||
@ -1108,8 +1106,8 @@ const mockOpenaiApiClient = {
|
||||
isFinished = true
|
||||
}
|
||||
|
||||
let isFirstThinkingChunk = true
|
||||
let isFirstTextChunk = true
|
||||
let isThinking = false
|
||||
let accumulatingText = false
|
||||
return (context: ResponseChunkTransformerContext) => ({
|
||||
async transform(chunk: OpenAISdkRawChunk, controller: TransformStreamDefaultController<GenericChunk>) {
|
||||
// 持续更新usage信息
|
||||
@ -1146,6 +1144,15 @@ const mockOpenaiApiClient = {
|
||||
contentSource = choice.message
|
||||
}
|
||||
|
||||
// 状态管理
|
||||
if (!contentSource?.content) {
|
||||
accumulatingText = false
|
||||
}
|
||||
// @ts-ignore - reasoning_content is not in standard OpenAI types but some providers use it
|
||||
if (!contentSource?.reasoning_content && !contentSource?.reasoning) {
|
||||
isThinking = false
|
||||
}
|
||||
|
||||
if (!contentSource) {
|
||||
if ('finish_reason' in choice && choice.finish_reason) {
|
||||
emitCompletionSignals(controller)
|
||||
@ -1165,30 +1172,34 @@ const mockOpenaiApiClient = {
|
||||
// @ts-ignore - reasoning_content is not in standard OpenAI types but some providers use it
|
||||
const reasoningText = contentSource.reasoning_content || contentSource.reasoning
|
||||
if (reasoningText) {
|
||||
if (isFirstThinkingChunk) {
|
||||
if (!isThinking) {
|
||||
controller.enqueue({
|
||||
type: ChunkType.THINKING_START
|
||||
} as ThinkingStartChunk)
|
||||
isFirstThinkingChunk = false
|
||||
isThinking = true
|
||||
}
|
||||
controller.enqueue({
|
||||
type: ChunkType.THINKING_DELTA,
|
||||
text: reasoningText
|
||||
})
|
||||
} else {
|
||||
isThinking = false
|
||||
}
|
||||
|
||||
// 处理文本内容
|
||||
if (contentSource.content) {
|
||||
if (isFirstTextChunk) {
|
||||
if (!accumulatingText) {
|
||||
controller.enqueue({
|
||||
type: ChunkType.TEXT_START
|
||||
} as TextStartChunk)
|
||||
isFirstTextChunk = false
|
||||
accumulatingText = true
|
||||
}
|
||||
controller.enqueue({
|
||||
type: ChunkType.TEXT_DELTA,
|
||||
text: contentSource.content
|
||||
})
|
||||
} else {
|
||||
accumulatingText = false
|
||||
}
|
||||
|
||||
// 处理工具调用
|
||||
@ -2570,4 +2581,239 @@ describe('ApiService', () => {
|
||||
expect(filteredFirstResponseChunks).toEqual(expectedFirstResponseChunks)
|
||||
expect(mcpChunks).toEqual(expectedMcpResponseChunks)
|
||||
})
|
||||
|
||||
it('should handle multiple reasoning blocks and text blocks', async () => {
|
||||
const rawChunks = [
|
||||
{
|
||||
choices: [
|
||||
{
|
||||
delta: { content: '', reasoning_content: '\n', role: 'assistant' },
|
||||
index: 0,
|
||||
finish_reason: null
|
||||
}
|
||||
],
|
||||
created: 1754192522,
|
||||
id: 'chat-network/glm-4.5-GLM-4.5-Flash-2025-08-03-11-42-02',
|
||||
model: 'glm-4.5-flash',
|
||||
object: 'chat.completion',
|
||||
system_fingerprint: '3000y'
|
||||
},
|
||||
{
|
||||
choices: [{ delta: { reasoning_content: '开始', role: 'assistant' }, index: 0, finish_reason: null }],
|
||||
created: 1754192522,
|
||||
id: 'chat-network/glm-4.5-GLM-4.5-Flash-2025-08-03-11-42-02',
|
||||
model: 'glm-4.5-flash',
|
||||
object: 'chat.completion',
|
||||
system_fingerprint: '3000y'
|
||||
},
|
||||
{
|
||||
choices: [{ delta: { reasoning_content: '思考', role: 'assistant' }, index: 0, finish_reason: null }],
|
||||
created: 1754192522,
|
||||
id: 'chat-network/glm-4.5-GLM-4.5-Flash-2025-08-03-11-42-02',
|
||||
model: 'glm-4.5-flash',
|
||||
object: 'chat.completion',
|
||||
system_fingerprint: '3000y'
|
||||
},
|
||||
{
|
||||
choices: [
|
||||
{ delta: { content: '思考', reasoning_content: null, role: 'assistant' }, index: 0, finish_reason: null }
|
||||
],
|
||||
created: 1754192522,
|
||||
id: 'chat-network/glm-4.5-GLM-4.5-Flash-2025-08-03-11-42-02',
|
||||
model: 'glm-4.5-flash',
|
||||
object: 'chat.completion',
|
||||
system_fingerprint: '3000y'
|
||||
},
|
||||
{
|
||||
choices: [
|
||||
{ delta: { content: '完成', reasoning_content: null, role: 'assistant' }, index: 0, finish_reason: null }
|
||||
],
|
||||
created: 1754192522,
|
||||
id: 'chat-network/glm-4.5-GLM-4.5-Flash-2025-08-03-11-42-02',
|
||||
model: 'glm-4.5-flash',
|
||||
object: 'chat.completion',
|
||||
system_fingerprint: '3000y'
|
||||
},
|
||||
{
|
||||
choices: [{ delta: { reasoning_content: '再次', role: 'assistant' }, index: 0, finish_reason: null }],
|
||||
created: 1754192522,
|
||||
id: 'chat-network/glm-4.5-GLM-4.5-Flash-2025-08-03-11-42-02',
|
||||
model: 'glm-4.5-flash',
|
||||
object: 'chat.completion',
|
||||
system_fingerprint: '3000y'
|
||||
},
|
||||
{
|
||||
choices: [{ delta: { reasoning_content: '思考', role: 'assistant' }, index: 0, finish_reason: null }],
|
||||
created: 1754192522,
|
||||
id: 'chat-network/glm-4.5-GLM-4.5-Flash-2025-08-03-11-42-02',
|
||||
model: 'glm-4.5-flash',
|
||||
object: 'chat.completion',
|
||||
system_fingerprint: '3000y'
|
||||
},
|
||||
{
|
||||
choices: [
|
||||
{ delta: { content: '思考', reasoning_content: null, role: 'assistant' }, index: 0, finish_reason: null }
|
||||
],
|
||||
created: 1754192522,
|
||||
id: 'chat-network/glm-4.5-GLM-4.5-Flash-2025-08-03-11-42-02',
|
||||
model: 'glm-4.5-flash',
|
||||
object: 'chat.completion',
|
||||
system_fingerprint: '3000y'
|
||||
},
|
||||
{
|
||||
choices: [
|
||||
{ delta: { content: '完成', reasoning_content: null, role: 'assistant' }, index: 0, finish_reason: null }
|
||||
],
|
||||
created: 1754192522,
|
||||
id: 'chat-network/glm-4.5-GLM-4.5-Flash-2025-08-03-11-42-02',
|
||||
model: 'glm-4.5-flash',
|
||||
object: 'chat.completion',
|
||||
system_fingerprint: '3000y'
|
||||
},
|
||||
{
|
||||
choices: [
|
||||
{ delta: { content: '', reasoning_content: null, role: 'assistant' }, index: 0, finish_reason: 'stop' }
|
||||
],
|
||||
created: 1754192522,
|
||||
id: 'chat-network/glm-4.5-GLM-4.5-Flash-2025-08-03-11-42-02',
|
||||
model: 'glm-4.5-flash',
|
||||
object: 'chat.completion',
|
||||
system_fingerprint: '3000y'
|
||||
}
|
||||
]
|
||||
|
||||
async function* mockChunksGenerator(): AsyncGenerator<OpenAISdkRawChunk> {
|
||||
for (const chunk of rawChunks) {
|
||||
// since no reasoning_content field
|
||||
yield chunk as OpenAISdkRawChunk
|
||||
}
|
||||
}
|
||||
|
||||
const mockOpenaiApiClient_ = cloneDeep(mockOpenaiApiClient)
|
||||
|
||||
mockOpenaiApiClient_.createCompletions = vi.fn().mockImplementation(() => mockChunksGenerator())
|
||||
|
||||
const mockCreate = vi.mocked(ApiClientFactory.create)
|
||||
// @ts-ignore mockOpenaiApiClient_ is a OpenAIAPIClient
|
||||
mockCreate.mockReturnValue(mockOpenaiApiClient_ as unknown as OpenAIAPIClient)
|
||||
const AI = new AiProvider(mockProvider as Provider)
|
||||
|
||||
const result = await AI.completions({
|
||||
callType: 'test',
|
||||
messages: [],
|
||||
assistant: {
|
||||
id: '1',
|
||||
name: 'test',
|
||||
prompt: 'test',
|
||||
model: {
|
||||
id: 'gpt-4o',
|
||||
name: 'GPT-4o',
|
||||
supported_text_delta: true
|
||||
}
|
||||
} as Assistant,
|
||||
onChunk: mockOnChunk,
|
||||
enableReasoning: true,
|
||||
streamOutput: true
|
||||
})
|
||||
|
||||
const stream = result.stream! as ReadableStream<GenericChunk>
|
||||
const reader = stream.getReader()
|
||||
|
||||
const chunks: GenericChunk[] = []
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read()
|
||||
if (done) break
|
||||
chunks.push(value)
|
||||
}
|
||||
|
||||
reader.releaseLock()
|
||||
|
||||
const filteredChunks = chunks.map((chunk) => {
|
||||
if (chunk.type === ChunkType.THINKING_DELTA || chunk.type === ChunkType.THINKING_COMPLETE) {
|
||||
delete (chunk as any).thinking_millsec
|
||||
return chunk
|
||||
}
|
||||
return chunk
|
||||
})
|
||||
|
||||
const expectedChunks = [
|
||||
{
|
||||
type: ChunkType.THINKING_START
|
||||
},
|
||||
{
|
||||
type: ChunkType.THINKING_DELTA,
|
||||
text: '\n'
|
||||
},
|
||||
{
|
||||
type: ChunkType.THINKING_DELTA,
|
||||
text: '\n开始'
|
||||
},
|
||||
{
|
||||
type: ChunkType.THINKING_DELTA,
|
||||
text: '\n开始思考'
|
||||
},
|
||||
{
|
||||
type: ChunkType.THINKING_COMPLETE,
|
||||
text: '\n开始思考'
|
||||
},
|
||||
{
|
||||
type: ChunkType.TEXT_START
|
||||
},
|
||||
{
|
||||
type: ChunkType.TEXT_DELTA,
|
||||
text: '思考'
|
||||
},
|
||||
{
|
||||
type: ChunkType.TEXT_DELTA,
|
||||
text: '思考完成'
|
||||
},
|
||||
{
|
||||
type: ChunkType.TEXT_COMPLETE,
|
||||
text: '思考完成'
|
||||
},
|
||||
{
|
||||
type: ChunkType.THINKING_START
|
||||
},
|
||||
{
|
||||
type: ChunkType.THINKING_DELTA,
|
||||
text: '再次'
|
||||
},
|
||||
{
|
||||
type: ChunkType.THINKING_DELTA,
|
||||
text: '再次思考'
|
||||
},
|
||||
{
|
||||
type: ChunkType.THINKING_COMPLETE,
|
||||
text: '再次思考'
|
||||
},
|
||||
{
|
||||
type: ChunkType.TEXT_START
|
||||
},
|
||||
{
|
||||
type: ChunkType.TEXT_DELTA,
|
||||
text: '思考'
|
||||
},
|
||||
{
|
||||
type: ChunkType.TEXT_DELTA,
|
||||
text: '思考完成'
|
||||
},
|
||||
{
|
||||
type: ChunkType.TEXT_COMPLETE,
|
||||
text: '思考完成'
|
||||
},
|
||||
{
|
||||
type: ChunkType.LLM_RESPONSE_COMPLETE,
|
||||
response: {
|
||||
usage: {
|
||||
completion_tokens: 0,
|
||||
prompt_tokens: 0,
|
||||
total_tokens: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
expect(filteredChunks).toEqual(expectedChunks)
|
||||
})
|
||||
})
|
||||
|
||||
Loading…
Reference in New Issue
Block a user