feat: enhance plugin system with new reasoning and text plugins

- Introduced `reasonPlugin` and `textPlugin` to improve chunk processing and handling of reasoning content.
- Updated `transformStream` method signatures for better type safety and usability.
- Enhanced `ThinkingTimeMiddleware` to accurately track thinking time using `performance.now()`.
- Refactored `ThinkingBlock` component to utilize block thinking time directly, improving performance and clarity.
- Added logging for middleware builder to assist in debugging and monitoring middleware configurations.
This commit is contained in:
MyPrototypeWhat 2025-06-25 19:00:54 +08:00
parent e4c0ea035f
commit f23a026a28
11 changed files with 99 additions and 43 deletions

View File

@ -18,7 +18,9 @@ export function createContext(providerId: string, modelId: string, originalParam
}
// 插件构建器 - 便于创建插件
export function definePlugin(plugin: AiPlugin): AiPlugin {
export function definePlugin(plugin: AiPlugin): AiPlugin
export function definePlugin<T extends (...args: any[]) => AiPlugin>(pluginFactory: T): T
export function definePlugin(plugin: AiPlugin | ((...args: any[]) => AiPlugin)) {
return plugin
}

View File

@ -127,7 +127,7 @@ export class PluginManager {
stopStream: () => void
}) => TransformStream<TextStreamPart<TOOLS>, TextStreamPart<TOOLS>>
> {
return this.plugins.map((plugin) => plugin.transformStream?.()).filter(Boolean) as Array<
return this.plugins.map((plugin) => plugin.transformStream).filter(Boolean) as Array<
(options: {
tools?: TOOLS
stopStream: () => void

View File

@ -33,8 +33,8 @@ export interface AiPlugin {
onError?: (error: Error, context: AiRequestContext) => void | Promise<void>
// 【Stream】流处理 - 直接使用 AI SDK
transformStream?: <TOOLS extends ToolSet>() => (options: {
tools?: TOOLS
transformStream?: <TOOLS extends ToolSet>(options: {
tools: TOOLS
stopStream: () => void
}) => TransformStream<TextStreamPart<TOOLS>, TextStreamPart<TOOLS>>

View File

@ -88,7 +88,9 @@ export class AiSdkToChunkAdapter {
case 'reasoning':
this.onChunk({
type: ChunkType.THINKING_DELTA,
text: chunk.textDelta || ''
text: chunk.textDelta || '',
// 自定义字段
thinking_millsec: chunk.thinking_millsec || 0
})
break
case 'redacted-reasoning':

View File

@ -13,7 +13,6 @@ import {
ProviderConfigFactory,
type ProviderId,
type ProviderSettingsMap,
smoothStream,
StreamTextParams
} from '@cherrystudio/ai-core'
import { isDedicatedImageGenerationModel } from '@renderer/config/models'
@ -26,6 +25,8 @@ import AiSdkToChunkAdapter from './AiSdkToChunkAdapter'
import LegacyAiProvider from './index'
import { AiSdkMiddlewareConfig, buildAiSdkMiddlewares } from './middleware/aisdk/AiSdkMiddlewareBuilder'
import { CompletionsResult } from './middleware/schemas'
import reasonPlugin from './plugins/reasonPlugin'
import textPlugin from './plugins/textPlugin'
import { getAiSdkProviderId } from './provider/factory'
/**
@ -111,7 +112,13 @@ export default class ModernAiProvider {
// TODO:如果后续在调用completions时需要切换provider的话,
// 初始化时不构建中间件,等到需要时再构建
const config = providerToAiSdkConfig(provider)
this.modernExecutor = createExecutor(config.providerId, config.options)
this.modernExecutor = createExecutor(config.providerId, config.options, [
reasonPlugin({
delayInMs: 80,
chunkingRegex: /([\u4E00-\u9FFF]{3})|\S+\s+/
}),
textPlugin
])
}
public async completions(
@ -160,7 +167,7 @@ export default class ModernAiProvider {
// 动态构建中间件数组
const middlewares = buildAiSdkMiddlewares(finalConfig)
console.log('构建的中间件:', middlewares.length)
console.log('构建的中间件:', middlewares)
// 创建带有中间件的执行器
if (middlewareConfig.onChunk) {
@ -168,14 +175,7 @@ export default class ModernAiProvider {
const adapter = new AiSdkToChunkAdapter(middlewareConfig.onChunk)
const streamResult = await this.modernExecutor.streamText(
modelId,
{
...params,
experimental_transform: smoothStream({
delayInMs: 80,
// 中文3个字符一个chunk,英文一个单词一个chunk
chunking: /([\u4E00-\u9FFF]{3})|\S+\s+/
})
},
params,
middlewares.length > 0 ? { middlewares } : undefined
)

View File

@ -138,7 +138,7 @@ export function buildAiSdkMiddlewares(config: AiSdkMiddlewareConfig): LanguageMo
middleware: simulateStreamingMiddleware()
})
}
console.log('builder.build()', builder.buildNamed())
return builder.build()
}

View File

@ -22,14 +22,14 @@ export default function thinkingTimeMiddleware(): LanguageModelV1Middleware {
if (chunk.type === 'reasoning' || chunk.type === 'redacted-reasoning') {
if (!hasThinkingContent) {
hasThinkingContent = true
thinkingStartTime = Date.now()
thinkingStartTime = performance.now()
}
accumulatedThinkingContent += chunk.textDelta || ''
// 将所有 chunk 原样传递下去
controller.enqueue(chunk)
controller.enqueue({ ...chunk, thinking_millsec: performance.now() - thinkingStartTime })
} else {
if (hasThinkingContent && thinkingStartTime > 0) {
const thinkingTime = Date.now() - thinkingStartTime
const thinkingTime = performance.now() - thinkingStartTime
const thinkingCompleteChunk = {
type: 'reasoning-signature',
text: accumulatedThinkingContent,

View File

@ -0,0 +1,42 @@
import { definePlugin } from '@cherrystudio/ai-core'
export default definePlugin(({ delayInMs, chunkingRegex }: { delayInMs: number; chunkingRegex: RegExp }) => ({
name: 'reasonPlugin',
transformStream: () => {
let buffer = ''
const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))
const detectChunk = (buffer: string) => {
const match = chunkingRegex.exec(buffer)
if (!match) {
return null
}
return buffer.slice(0, match.index) + match?.[0]
}
return new TransformStream({
async transform(chunk, controller) {
if (chunk.type !== 'reasoning') {
if (buffer.length > 0) {
controller.enqueue({ type: 'reasoning', textDelta: buffer, thinking_millsec: chunk.thinking_millsec })
buffer = ''
}
controller.enqueue(chunk)
return
}
buffer += chunk.textDelta
let match
while ((match = detectChunk(buffer)) != null) {
controller.enqueue({ type: 'reasoning', textDelta: match, thinking_millsec: chunk.thinking_millsec })
buffer = buffer.slice(match.length)
await delay(delayInMs)
}
}
})
}
}))

View File

@ -0,0 +1,10 @@
import { definePlugin, smoothStream } from '@cherrystudio/ai-core'
export default definePlugin({
name: 'textPlugin',
transformStream: smoothStream({
delayInMs: 80,
// 中文3个字符一个chunk,英文一个单词一个chunk
chunking: /([\u4E00-\u9FFF]{3})|\S+\s+/
})
})

View File

@ -107,33 +107,33 @@ const ThinkingBlock: React.FC<Props> = ({ block }) => {
}
const ThinkingTimeSeconds = memo(
({ blockThinkingTime, isThinking }: { blockThinkingTime?: number; isThinking: boolean }) => {
({ blockThinkingTime, isThinking }: { blockThinkingTime: number; isThinking: boolean }) => {
const { t } = useTranslation()
const [thinkingTime, setThinkingTime] = useState(blockThinkingTime || 0)
// console.log('blockThinkingTime', blockThinkingTime)
// const [thinkingTime, setThinkingTime] = useState(blockThinkingTime || 0)
// FIXME: 这里统计的和请求处统计的有一定误差
useEffect(() => {
let timer: NodeJS.Timeout | null = null
if (isThinking) {
timer = setInterval(() => {
setThinkingTime((prev) => prev + 100)
}, 100)
} else if (timer) {
// 立即清除计时器
clearInterval(timer)
timer = null
}
// useEffect(() => {
// let timer: NodeJS.Timeout | null = null
// if (isThinking) {
// timer = setInterval(() => {
// setThinkingTime((prev) => prev + 100)
// }, 100)
// } else if (timer) {
// // 立即清除计时器
// clearInterval(timer)
// timer = null
// }
return () => {
if (timer) {
clearInterval(timer)
timer = null
}
}
}, [isThinking])
// return () => {
// if (timer) {
// clearInterval(timer)
// timer = null
// }
// }
// }, [isThinking])
const thinkingTimeSeconds = useMemo(() => (thinkingTime / 1000).toFixed(1), [thinkingTime])
const thinkingTimeSeconds = useMemo(() => (blockThinkingTime / 1000).toFixed(1), [blockThinkingTime])
return t(isThinking ? 'chat.thinking' : 'chat.deeply_thought', {
seconds: thinkingTimeSeconds

View File

@ -72,7 +72,7 @@ export interface MainTextMessageBlock extends BaseMessageBlock {
export interface ThinkingMessageBlock extends BaseMessageBlock {
type: MessageBlockType.THINKING
content: string
thinking_millsec?: number
thinking_millsec: number
}
// 翻译块