Cherry Studio ai-core 提供商调用观测(Per-Provider-Call Observation)指南
Cherry Studio ai-core 提供商调用观测Per-Provider-Call Observation指南【免费下载链接】cherry-studio Cherry Studio 是一款支持多个 LLM 提供商的桌面客户端项目地址: https://gitcode.com/CherryHQ/cherry-studio引言Cherry Studio 的多提供商桌面客户端依赖自研的cherrystudio/ai-core运行时来统一调度文本、图像、嵌入和重排序调用。本指南围绕该包最新的 patch 变更——observe-ai-core-provider-calls——展开它为图像、嵌入和重排序运行时引入了可选的按提供商调用per-provider-call观测钩子携带调用身份invocation identity、可用时的用量usage以及按调用计量的耗时和性能投影。通过本文你将掌握如何通过onProviderCall回调捕获每次提供商调用的元数据理解其底层实现与事件模型并将其接入 Cherry Studio 的用量与计费体系。变更内容一览变更包cherrystudio/ai-corepackages/aiCore变更类型patch向后兼容的增量增强核心能力为以下三种运行时新增可选的观测钩子不改变现有调用行为generateImage图像生成embedMany批量嵌入rerank重排序事件携带字段调用身份requestId、提供商与模型标识、可用时的用量usage、完成耗时timeCompletionMs与完成时间戳completedAt用于按调用级别的用量和性能投影。该变更对应的 changeset 文件为.changeset/observe-ai-core-provider-calls.md声明了对cherrystudio/ai-core的 patch 级改动。钩子类型与事件模型在packages/aiCore/src/core/runtime/types.ts中定义了完整的观测契约RuntimeProviderCallHandler观测回调类型签名(event: RuntimeProviderCallEvent) void。RuntimeProviderCallEvent按模态区分的判别联合discriminated union分别对应嵌入、图像与重排序export type RuntimeProviderCallEvent | { modality: embedding requestId: string providerId: string modelId: string usage?: { tokens: number } metrics: { timeCompletionMs: number } completedAt: number } | { modality: image requestId: string providerId: string modelId: string imageCount: number usage?: { inputTokens?: number; outputTokens?: number; totalTokens?: number } metrics: { timeCompletionMs: number } completedAt: number } | { modality: rerank requestId: string providerId: string modelId: string metrics: { timeCompletionMs: number } completedAt: number }各字段含义字段说明modality调用模态embedding/image/rerankrequestId调用身份格式ai-core:modality:uuid由运行时生成providerId执行器配置的提供商 IDmodelId实际执行调用的模型 IDusage提供商返回的用量可用时imageCount图像模态特有本次调用生成的图片数量metrics.timeCompletionMs从调用开始到完成的耗时毫秒completedAt完成时间戳毫秒嵌入模态的usage.tokens表示批次令牌数图像模态的usage为可选的输入/输出/总令牌数重排序模态目前不携带 usage。参数接入方式onProviderCall作为可选参数注入到三个方法的高阶参数类型中见packages/aiCore/src/core/runtime/types.tsexport type generateImageParams OmitParameterstypeof generateImage[0], model { model: string | ImageModelV3 experimental_download?: Experimental_DownloadFunction onProviderCall?: RuntimeProviderCallHandler } export type EmbedManyParams OmitParameterstypeof embedMany[0], model { model: string | EmbeddingModelV3 onProviderCall?: RuntimeProviderCallHandler } export type RerankParamsVALUE extends JSONObject | string string Omit Parameterstypeof rerankVALUE[0], model { model: string | RerankingModelV3 onProviderCall?: RuntimeProviderCallHandler }这些类型都保留了对model的字符串或模型对象二选一支持字符串 ID 会通过执行器的提供商注册表解析为具体模型。底层实现原理RuntimeExecutorpackages/aiCore/src/core/runtime/executor.ts负责实际的钩子注入三个方法的实现各有特点图像生成generateImage通过 AI SDK 的wrapImageModel中间件包装解析后的图像模型const observedModel onProviderCall ? wrapImageModel({ model: resolvedModel, middleware: { specificationVersion: v3, wrapGenerate: async ({ doGenerate, model: activeModel }) { const startedAt performance.now() const result await doGenerate() emitProviderCall(onProviderCall, { modality: image, requestId: ai-core:image:${crypto.randomUUID()}, providerId: this.config.providerId, modelId: activeModel.modelId, imageCount: result.images.length, ...(result.usage ? { usage: result.usage } : {}), metrics: { timeCompletionMs: Math.max(0, Math.round(performance.now() - startedAt)) }, completedAt: Date.now() }) return result } } }) : resolvedModel批量嵌入embedMany通过wrapEmbeddingModel中间件实现同样的计时与事件发射模式const observedModel onProviderCall ? wrapEmbeddingModel({ model: embeddingModel, middleware: { specificationVersion: v3, wrapEmbed: async ({ doEmbed, model }) { const startedAt performance.now() const result await doEmbed() emitProviderCall(onProviderCall, { modality: embedding, requestId: ai-core:embedding:${crypto.randomUUID()}, providerId: this.config.providerId, modelId: model.modelId, ...(result.usage ? { usage: result.usage } : {}), metrics: { timeCompletionMs: Math.max(0, Math.round(performance.now() - startedAt)) }, completedAt: Date.now() }) return result } } }) : embeddingModel重排序rerank与上述两个不同重排序直接在_rerank调用成功后发射事件目前 AI SDK 尚未提供等价的 wrap 中间件const startedAt performance.now() const result await _rerankVALUE({ model: rerankingModel, ...options }) emitProviderCall(onProviderCall, { modality: rerank, requestId: ai-core:rerank:${crypto.randomUUID()}, providerId: this.config.providerId, modelId: rerankingModel.modelId, metrics: { timeCompletionMs: Math.max(0, Math.round(performance.now() - startedAt)) }, completedAt: Date.now() })注意rerank仅在提供商调用成功返回后才发射事件若调用抛错事件不会发出。安全发射机制所有模态的事件发射都经由统一的emitProviderCall辅助函数function emitProviderCall(handler: RuntimeProviderCallHandler | undefined, event: RuntimeProviderCallEvent): void { try { handler?.(event) } catch { // Usage observation is best-effort and must never change a successful AI result. } }这段注释明确了设计意图用量观测是尽力而为best-effort的绝不允许改变已经成功的 AI 调用结果。即使观测处理器内部抛出异常也会被静默吞掉原调用结果不受任何影响。测试用例验证packages/aiCore/src/core/runtime/__tests__/providerCall.test.ts使用test-utils提供的 mock 模型createMockProviderV3、createMockEmbeddingModel、createMockImageModel、createMockRerankingModel对观测行为进行了系统性验证嵌入每次 SDK 批次发射一个事件。向embedMany传入 5 条文本、模型maxEmbeddingsPerCall 2AI SDK 内部拆分为 3 次底层doEmbed调用观测事件同样为 3 个且每个事件的usage.tokens分别为[2, 2, 1]3 个requestId互不相同。这印证了事件是按每次实际提供商调用而非整个高层请求粒度发射的。图像每次 SDK 批次发射一个事件。n 5、maxImagesPerCall 2时底层调用 3 次事件 3 个imageCount为[2, 2, 1]。重排序仅在成功后发射。正常调用发射 1 个事件当doRerankmock 为 reject抛provider failed时事件列表为空。观测处理器抛错不影响结果。onProviderCall内主动throw new Error(analytics unavailable)embedMany依然正常 resolve返回的embeddings与usage完整保留。在 Cherry Studio 主进程中的接入实践观测钩子并非孤立能力而是已被 Cherry Studio 主进程的 AI 服务src/main/ai/AiService.ts实际消费。其接入模式如下捕获上下文的工厂函数createProviderCallHandler将每次事件落盘为一条用量记录function createProviderCallHandler(context: AiUsageCaptureContext): RuntimeProviderCallHandler { return (event: RuntimeProviderCallEvent) { aiUsageRecordService.recordInvocation({ requestId: event.requestId, context, modality: event.modality, ...(event.modality embedding event.usage ? { usage: { inputTokens: event.usage.tokens, totalTokens: event.usage.tokens } } : event.modality image event.usage ? { usage: { ...(event.usage.inputTokens ! undefined ? { inputTokens: event.usage.inputTokens } : {}), ...(event.usage.outputTokens ! undefined ? { outputTokens: event.usage.outputTokens } : {}), ...(event.usage.totalTokens ! undefined ? { totalTokens: event.usage.totalTokens } : {}) } } : {}), ...(event.modality image ? { imageCount: event.imageCount } : {}), metrics: event.metrics, completedAt: event.completedAt }) } }它在generateImage、embedMany、rerank三处调用点分别以imageUsageContext和usageContext传入AiService.ts第 975、1150、1197 行附近。计费与用量记录的完整性src/main/ai/hooks/billingHook.ts定义了可计费操作的覆盖矩阵AI_USAGE_RECORD_OPERATION_COVERAGE明确标注了三种模态的捕获路径为ai-core-handlerexport const AI_USAGE_RECORD_OPERATION_COVERAGE { streamText: { status: recorded, modality: language, capture: language-middleware }, generateText: { status: recorded, modality: language, capture: language-middleware }, embedMany: { status: recorded, modality: embedding, capture: ai-core-handler }, generateImage: { status: recorded, modality: image, capture: ai-core-handler }, rerank: { status: recorded, modality: rerank, capture: ai-core-handler } } as const语言模态streamText/generateText通过语言模型中间件捕获而嵌入、图像、重排序正是通过本文介绍的ai-core-handler路径捕获。请求 ID 命名空间docs/references/ai/ai-usage-records.md记录了请求 ID 的命名空间约定其中 aiCore 提供商处理器的事件 ID 格式为ai-core:modality:uuid这与此前executor.ts中的实现完全吻合。自定义接入示例若要自行接入观测钩子可按以下模式调用import { RuntimeExecutor } from cherrystudio/ai-core const executor RuntimeExecutor.create(openai, provider, { apiKey: sk-... }) // 图像生成观测 await executor.generateImage({ model: dall-e-3, prompt: a cat, n: 1, onProviderCall: (event) { console.log(event.modality, event.requestId, event.imageCount, event.metrics.timeCompletionMs) } }) // 批量嵌入观测 await executor.embedMany({ model: text-embedding-3-small, values: [document 1, document 2], onProviderCall: (event) { if (event.modality embedding event.usage) { console.log(tokens: ${event.usage.tokens}) } } }) // 重排序观测仅在成功后触发 await executor.rerank({ model: reranker, query: query, documents: [a, b], onProviderCall: (event) { console.log(event.modality, event.metrics.timeCompletionMs, event.completedAt) } })局限性与注意事项rerank模态目前不携带usage字段事件仅在调用成功后发射失败调用不会产生观测事件。embedding/image事件仅在提供商返回了usage时才会带上usage字段通过条件展开...(result.usage ? { usage: result.usage } : {})。观测处理器为同步函数建议在其中执行轻量逻辑如记录日志、写入用量库重活应异步化避免拖慢主流程。钩子是可选的不传onProviderCall时wrapImageModel/wrapEmbeddingModel包装与计时逻辑整体跳过对调用路径零开销。相关文件索引变更声明.changeset/observe-ai-core-provider-calls.md运行时实现packages/aiCore/src/core/runtime/executor.ts事件与参数类型定义packages/aiCore/src/core/runtime/types.ts观测行为测试packages/aiCore/src/core/runtime/__tests__/providerCall.test.ts主进程接入src/main/ai/AiService.ts计费覆盖矩阵src/main/ai/hooks/billingHook.ts用量记录文档docs/references/ai/ai-usage-records.md【免费下载链接】cherry-studio Cherry Studio 是一款支持多个 LLM 提供商的桌面客户端项目地址: https://gitcode.com/CherryHQ/cherry-studio创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考