mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-01-14 06:07:23 +08:00
* refactor(RuntimeExecutor, PluginEngine): streamline parameter handling and improve type definitions for model operations - Updated parameter handling in RuntimeExecutor methods to use specific types for generate and stream functions. - Refactored PluginEngine methods to enhance type safety and reduce redundancy in model resolution. - Introduced new type definitions for generate and stream parameters in types.ts for better clarity and maintainability. - Adjusted provider options mapping in buildProviderOptions to accommodate new provider types. * feat(googleToolsPlugin): enhance Google tools integration and update dependencies - Updated the `ai` package version to `5.0.38` and `@ai-sdk/gateway` to `1.0.20` in `package.json` and `yarn.lock`. - Introduced `googleToolsPlugin` with improved parameter handling and configuration options for Google tools. - Added support for `urlContext` in middleware configuration and plugin builder. - Refactored web search tool to streamline response handling and citation formatting. - Updated various service methods to include `enableUrlContext` capability. * fix: update tool response handling and clean up unused code - Modified the `processKnowledgeReferences` function to accept the full response object instead of just `knowledgeReferences`. - Cleaned up the `searchOrchestrationPlugin` by removing unnecessary blank lines. - Removed commented-out code in `telemetryPlugin` to improve readability. - Updated `KnowledgeSearchTool` to streamline the execution flow and return results more efficiently. - Adjusted `MessageKnowledgeSearch` components to reflect changes in the data structure returned from the knowledge search tool. - Enhanced `MemorySearchTool` by simplifying error handling and removing redundant code. * refactor: clean up KnowledgeSearchTool and WebSearchTool by removing commented-out code - Removed unnecessary commented-out code in `KnowledgeSearchTool` and `WebSearchTool` to improve code readability and maintainability. - Simplified the `processKnowledgeReferences` function by eliminating the console log statement for cleaner output. * chore: bump version to 1.0.0-alpha.14 in aiCore package.json * chore: update @ai-sdk/google-vertex to version 3.0.25 and add @ai-sdk/anthropic@2.0.15 to dependencies - Bumped the version of `@ai-sdk/google-vertex` in `package.json` and `yarn.lock` to 3.0.25. - Added `@ai-sdk/anthropic` version 2.0.15 to `yarn.lock` with updated dependencies. - Refactored `parameterBuilder.ts` to integrate new tools from `@ai-sdk/google-vertex` for enhanced functionality.
338 lines
12 KiB
TypeScript
338 lines
12 KiB
TypeScript
/**
|
||
* Telemetry Plugin for AI SDK Integration
|
||
*
|
||
* 在 transformParams 钩子中注入 experimental_telemetry 参数,
|
||
* 实现 AI SDK trace 与现有手动 trace 系统的统一
|
||
* 集成 AiSdkSpanAdapter 将 AI SDK trace 数据转换为现有格式
|
||
*/
|
||
|
||
import { definePlugin } from '@cherrystudio/ai-core'
|
||
import { loggerService } from '@logger'
|
||
import { Context, context as otelContext, Span, SpanContext, trace, Tracer } from '@opentelemetry/api'
|
||
import { currentSpan } from '@renderer/services/SpanManagerService'
|
||
import { webTraceService } from '@renderer/services/WebTraceService'
|
||
import { Assistant } from '@renderer/types'
|
||
|
||
import { AiSdkSpanAdapter } from '../trace/AiSdkSpanAdapter'
|
||
|
||
const logger = loggerService.withContext('TelemetryPlugin')
|
||
|
||
export interface TelemetryPluginConfig {
|
||
enabled?: boolean
|
||
recordInputs?: boolean
|
||
recordOutputs?: boolean
|
||
topicId: string
|
||
assistant: Assistant
|
||
}
|
||
|
||
/**
|
||
* 自定义 Tracer,集成适配器转换逻辑
|
||
*/
|
||
class AdapterTracer {
|
||
private originalTracer: Tracer
|
||
private topicId?: string
|
||
private modelName?: string
|
||
private parentSpanContext?: SpanContext
|
||
private cachedParentContext?: Context
|
||
|
||
constructor(originalTracer: Tracer, topicId?: string, modelName?: string, parentSpanContext?: SpanContext) {
|
||
this.originalTracer = originalTracer
|
||
this.topicId = topicId
|
||
this.modelName = modelName
|
||
this.parentSpanContext = parentSpanContext
|
||
// 预构建一个包含父 SpanContext 的 Context,便于复用
|
||
try {
|
||
this.cachedParentContext = this.parentSpanContext
|
||
? trace.setSpanContext(otelContext.active(), this.parentSpanContext)
|
||
: undefined
|
||
} catch {
|
||
this.cachedParentContext = undefined
|
||
}
|
||
|
||
logger.info('AdapterTracer created with parent context info', {
|
||
topicId,
|
||
modelName,
|
||
parentTraceId: this.parentSpanContext?.traceId,
|
||
parentSpanId: this.parentSpanContext?.spanId,
|
||
hasOriginalTracer: !!originalTracer
|
||
})
|
||
}
|
||
|
||
startActiveSpan<F extends (span: Span) => any>(name: string, fn: F): ReturnType<F>
|
||
startActiveSpan<F extends (span: Span) => any>(name: string, options: any, fn: F): ReturnType<F>
|
||
startActiveSpan<F extends (span: Span) => any>(name: string, options: any, context: any, fn: F): ReturnType<F>
|
||
startActiveSpan<F extends (span: Span) => any>(name: string, arg2?: any, arg3?: any, arg4?: any): ReturnType<F> {
|
||
logger.info('AdapterTracer.startActiveSpan called', {
|
||
spanName: name,
|
||
topicId: this.topicId,
|
||
modelName: this.modelName,
|
||
argCount: arguments.length
|
||
})
|
||
|
||
// 包装函数来添加span转换逻辑
|
||
const wrapFunction = (originalFn: F, span: Span): F => {
|
||
const wrappedFn = ((passedSpan: Span) => {
|
||
// 注入父子关系属性(兜底重建层级用)
|
||
try {
|
||
if (this.parentSpanContext) {
|
||
passedSpan.setAttribute('trace.parentSpanId', this.parentSpanContext.spanId)
|
||
passedSpan.setAttribute('trace.parentTraceId', this.parentSpanContext.traceId)
|
||
}
|
||
if (this.topicId) {
|
||
passedSpan.setAttribute('trace.topicId', this.topicId)
|
||
}
|
||
} catch (e) {
|
||
logger.debug('Failed to set trace parent attributes in startActiveSpan', e as Error)
|
||
}
|
||
// 包装span的end方法
|
||
const originalEnd = span.end.bind(span)
|
||
span.end = (endTime?: any) => {
|
||
logger.info('AI SDK span.end() called in startActiveSpan - about to convert span', {
|
||
spanName: name,
|
||
spanId: span.spanContext().spanId,
|
||
traceId: span.spanContext().traceId,
|
||
topicId: this.topicId,
|
||
modelName: this.modelName
|
||
})
|
||
|
||
// 调用原始 end 方法
|
||
originalEnd(endTime)
|
||
|
||
// 转换并保存 span 数据
|
||
try {
|
||
logger.info('Converting AI SDK span to SpanEntity (from startActiveSpan)', {
|
||
spanName: name,
|
||
spanId: span.spanContext().spanId,
|
||
traceId: span.spanContext().traceId,
|
||
topicId: this.topicId,
|
||
modelName: this.modelName
|
||
})
|
||
logger.info('span', span)
|
||
const spanEntity = AiSdkSpanAdapter.convertToSpanEntity({
|
||
span,
|
||
topicId: this.topicId,
|
||
modelName: this.modelName
|
||
})
|
||
|
||
// 保存转换后的数据
|
||
window.api.trace.saveEntity(spanEntity)
|
||
|
||
logger.info('AI SDK span converted and saved successfully (from startActiveSpan)', {
|
||
spanName: name,
|
||
spanId: span.spanContext().spanId,
|
||
traceId: span.spanContext().traceId,
|
||
topicId: this.topicId,
|
||
modelName: this.modelName,
|
||
hasUsage: !!spanEntity.usage,
|
||
usage: spanEntity.usage
|
||
})
|
||
} catch (error) {
|
||
logger.error('Failed to convert AI SDK span (from startActiveSpan)', error as Error, {
|
||
spanName: name,
|
||
spanId: span.spanContext().spanId,
|
||
traceId: span.spanContext().traceId,
|
||
topicId: this.topicId,
|
||
modelName: this.modelName
|
||
})
|
||
}
|
||
}
|
||
|
||
return originalFn(passedSpan)
|
||
}) as F
|
||
return wrappedFn
|
||
}
|
||
|
||
// 创建包含父 SpanContext 的上下文(如果有的话)
|
||
const createContextWithParent = () => {
|
||
if (this.cachedParentContext) {
|
||
return this.cachedParentContext
|
||
}
|
||
if (this.parentSpanContext) {
|
||
try {
|
||
const ctx = trace.setSpanContext(otelContext.active(), this.parentSpanContext)
|
||
logger.info('Created active context with parent SpanContext for startActiveSpan', {
|
||
spanName: name,
|
||
parentTraceId: this.parentSpanContext.traceId,
|
||
parentSpanId: this.parentSpanContext.spanId,
|
||
topicId: this.topicId
|
||
})
|
||
return ctx
|
||
} catch (error) {
|
||
logger.warn('Failed to create context with parent SpanContext in startActiveSpan', error as Error)
|
||
}
|
||
}
|
||
return otelContext.active()
|
||
}
|
||
|
||
// 根据参数数量确定调用方式,注入包含mainTraceId的上下文
|
||
if (typeof arg2 === 'function') {
|
||
return this.originalTracer.startActiveSpan(name, {}, createContextWithParent(), (span: Span) => {
|
||
return wrapFunction(arg2, span)(span)
|
||
})
|
||
} else if (typeof arg3 === 'function') {
|
||
return this.originalTracer.startActiveSpan(name, arg2, createContextWithParent(), (span: Span) => {
|
||
return wrapFunction(arg3, span)(span)
|
||
})
|
||
} else if (typeof arg4 === 'function') {
|
||
// 如果调用方提供了 context,则保留以维护嵌套关系;否则回退到父上下文
|
||
const ctx = arg3 ?? createContextWithParent()
|
||
return this.originalTracer.startActiveSpan(name, arg2, ctx, (span: Span) => {
|
||
return wrapFunction(arg4, span)(span)
|
||
})
|
||
} else {
|
||
throw new Error('Invalid arguments for startActiveSpan')
|
||
}
|
||
}
|
||
}
|
||
|
||
export function createTelemetryPlugin(config: TelemetryPluginConfig) {
|
||
const { enabled = true, recordInputs = true, recordOutputs = true, topicId } = config
|
||
|
||
return definePlugin({
|
||
name: 'telemetryPlugin',
|
||
enforce: 'pre', // 在其他插件之前执行,确保 telemetry 配置被正确注入
|
||
|
||
transformParams: (params, context) => {
|
||
if (!enabled) {
|
||
return params
|
||
}
|
||
|
||
// 获取共享的 tracer
|
||
const originalTracer = webTraceService.getTracer()
|
||
if (!originalTracer) {
|
||
logger.warn('No tracer available from WebTraceService')
|
||
return params
|
||
}
|
||
|
||
// 获取topicId和modelName
|
||
const effectiveTopicId = context.topicId || topicId
|
||
// 使用与父span创建时一致的modelName - 应该是完整的modelId
|
||
const modelName = config.assistant.model?.name || context.modelId
|
||
|
||
// 获取当前活跃的 span,确保 AI SDK spans 与手动 spans 在同一个 trace 中
|
||
let parentSpan: Span | undefined = undefined
|
||
let parentSpanContext: SpanContext | undefined = undefined
|
||
|
||
// 只有在有topicId时才尝试查找父span
|
||
if (effectiveTopicId) {
|
||
try {
|
||
// 从 SpanManagerService 获取当前的 span
|
||
logger.info('Attempting to find parent span', {
|
||
topicId: effectiveTopicId,
|
||
requestId: context.requestId,
|
||
modelName: modelName,
|
||
contextModelId: context.modelId,
|
||
providerId: context.providerId
|
||
})
|
||
|
||
parentSpan = currentSpan(effectiveTopicId, modelName)
|
||
if (parentSpan) {
|
||
// 直接使用父 span 的 SpanContext,避免手动拼装字段遗漏
|
||
parentSpanContext = parentSpan.spanContext()
|
||
logger.info('Found active parent span for AI SDK', {
|
||
parentSpanId: parentSpanContext.spanId,
|
||
parentTraceId: parentSpanContext.traceId,
|
||
topicId: effectiveTopicId,
|
||
requestId: context.requestId,
|
||
modelName: modelName
|
||
})
|
||
} else {
|
||
logger.warn('No active parent span found in SpanManagerService', {
|
||
topicId: effectiveTopicId,
|
||
requestId: context.requestId,
|
||
modelId: context.modelId,
|
||
modelName: modelName,
|
||
providerId: context.providerId,
|
||
// 更详细的调试信息
|
||
searchedModelName: modelName,
|
||
contextModelId: context.modelId,
|
||
isAnalyzing: context.isAnalyzing
|
||
})
|
||
}
|
||
} catch (error) {
|
||
logger.error('Error getting current span from SpanManagerService', error as Error, {
|
||
topicId: effectiveTopicId,
|
||
requestId: context.requestId,
|
||
modelName: modelName
|
||
})
|
||
}
|
||
} else {
|
||
logger.debug('No topicId provided, skipping parent span lookup', {
|
||
requestId: context.requestId,
|
||
contextTopicId: context.topicId,
|
||
configTopicId: topicId,
|
||
modelName: modelName
|
||
})
|
||
}
|
||
|
||
// 创建适配器包装的 tracer,传入获取到的父 SpanContext
|
||
const adapterTracer = new AdapterTracer(originalTracer, effectiveTopicId, modelName, parentSpanContext)
|
||
|
||
// 注入 AI SDK telemetry 配置
|
||
const telemetryConfig = {
|
||
isEnabled: true,
|
||
recordInputs,
|
||
recordOutputs,
|
||
tracer: adapterTracer, // 使用包装后的 tracer
|
||
functionId: `ai-request-${context.requestId}`,
|
||
metadata: {
|
||
providerId: context.providerId,
|
||
modelId: context.modelId,
|
||
topicId: effectiveTopicId,
|
||
requestId: context.requestId,
|
||
modelName: modelName,
|
||
// 确保topicId也作为标准属性传递
|
||
'trace.topicId': effectiveTopicId,
|
||
'trace.modelName': modelName,
|
||
// 添加父span信息用于调试
|
||
parentSpanId: parentSpanContext?.spanId,
|
||
parentTraceId: parentSpanContext?.traceId
|
||
}
|
||
}
|
||
|
||
// 如果有父span,尝试在telemetry配置中设置父上下文
|
||
if (parentSpan) {
|
||
try {
|
||
// 设置活跃上下文,确保 AI SDK spans 在正确的 trace 上下文中创建
|
||
const activeContext = trace.setSpan(otelContext.active(), parentSpan)
|
||
|
||
// 更新全局上下文
|
||
otelContext.with(activeContext, () => {
|
||
logger.debug('Updated active context with parent span')
|
||
})
|
||
|
||
logger.info('Set parent context for AI SDK spans', {
|
||
parentSpanId: parentSpanContext?.spanId,
|
||
parentTraceId: parentSpanContext?.traceId,
|
||
hasActiveContext: !!activeContext,
|
||
hasParentSpan: !!parentSpan
|
||
})
|
||
} catch (error) {
|
||
logger.warn('Failed to set parent context in telemetry config', error as Error)
|
||
}
|
||
}
|
||
|
||
logger.info('Injecting AI SDK telemetry config with adapter', {
|
||
requestId: context.requestId,
|
||
topicId: effectiveTopicId,
|
||
modelId: context.modelId,
|
||
modelName: modelName,
|
||
hasParentSpan: !!parentSpan,
|
||
parentSpanId: parentSpanContext?.spanId,
|
||
parentTraceId: parentSpanContext?.traceId,
|
||
functionId: telemetryConfig.functionId,
|
||
hasTracer: !!telemetryConfig.tracer,
|
||
tracerType: telemetryConfig.tracer?.constructor?.name || 'unknown'
|
||
})
|
||
|
||
return {
|
||
...params,
|
||
experimental_telemetry: telemetryConfig
|
||
}
|
||
}
|
||
})
|
||
}
|
||
|
||
// 默认导出便于使用
|
||
export default createTelemetryPlugin
|