news 2026/9/19 12:51:40

Cherry Studio ai-core 提供商调用观测(Per-Provider-Call Observation)指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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/rerank
requestId调用身份,格式ai-core:<modality>:<uuid>,由运行时生成
providerId执行器配置的提供商 ID
modelId实际执行调用的模型 ID
usage提供商返回的用量(可用时)
imageCount图像模态特有:本次调用生成的图片数量
metrics.timeCompletionMs从调用开始到完成的耗时(毫秒)
completedAt完成时间戳(毫秒)

嵌入模态的usage.tokens表示批次令牌数;图像模态的usage为可选的输入/输出/总令牌数;重排序模态目前不携带 usage。

参数接入方式

onProviderCall作为可选参数注入到三个方法的高阶参数类型中,见packages/aiCore/src/core/runtime/types.ts

export type generateImageParams = Omit<Parameters<typeof generateImage>[0], 'model'> & { model: string | ImageModelV3 experimental_download?: Experimental_DownloadFunction onProviderCall?: RuntimeProviderCallHandler } export type EmbedManyParams = Omit<Parameters<typeof embedMany>[0], 'model'> & { model: string | EmbeddingModelV3 onProviderCall?: RuntimeProviderCallHandler } export type RerankParams<VALUE extends JSONObject | string = string> = Omit< Parameters<typeof rerank<VALUE>>[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 _rerank<VALUE>({ 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 模型(createMockProviderV3createMockEmbeddingModelcreateMockImageModelcreateMockRerankingModel)对观测行为进行了系统性验证:

  1. 嵌入:每次 SDK 批次发射一个事件。向embedMany传入 5 条文本、模型maxEmbeddingsPerCall = 2,AI SDK 内部拆分为 3 次底层doEmbed调用,观测事件同样为 3 个,且每个事件的usage.tokens分别为[2, 2, 1],3 个requestId互不相同。这印证了事件是按每次实际提供商调用(而非整个高层请求)粒度发射的
  2. 图像:每次 SDK 批次发射一个事件n = 5maxImagesPerCall = 2时底层调用 3 次,事件 3 个,imageCount[2, 2, 1]
  3. 重排序:仅在成功后发射。正常调用发射 1 个事件;当doRerankmock 为 reject(抛provider failed)时,事件列表为空。
  4. 观测处理器抛错不影响结果onProviderCall内主动throw new Error('analytics unavailable')embedMany依然正常 resolve,返回的embeddingsusage完整保留。

在 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 }) } }

它在generateImageembedManyrerank三处调用点分别以imageUsageContextusageContext传入(AiService.ts第 975、1150、1197 行附近)。

计费与用量记录的完整性

src/main/ai/hooks/billingHook.ts定义了可计费操作的覆盖矩阵AI_USAGE_RECORD_OPERATION_COVERAGE,明确标注了三种模态的捕获路径为ai-core-handler

export 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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/19 12:51:21

HTML与CSS基础实战:从文档结构到布局动画的完整指南

1. 从一行<!doctype html>说起&#xff1a;为什么每个前端人都绕不开这套基础打开任何一个网页&#xff0c;右键查看源代码&#xff0c;第一行大概率是<!doctype html>。这行看起来像注释又像标签的东西&#xff0c;是 HTML 文档的声明&#xff0c;告诉浏览器用标准…

作者头像 李华
网站建设 2026/9/19 12:46:30

Unity离线语音合成实战:讯飞SDK接入与NPC对话系统解耦

在Unity里做NPC对话系统&#xff0c;很多人的第一反应是接在线TTS服务&#xff0c;跑通确实快&#xff0c;但一旦项目要上展会、做离线演示、或者面向网络不稳定的场景&#xff0c;在线方案立刻变成累赘。我去年做一个展厅项目时就吃过这个亏&#xff1a;现场网络时断时续&…

作者头像 李华
网站建设 2026/9/19 12:43:57

AI视频生成工具真实能力与实战工作流指南

1. 这类工具的真实能力边界&#xff1a;别被“一键成片”宣传骗了“国外10个超好用的AI短视频生成网站推荐”——这个标题一出来&#xff0c;很多人第一反应是&#xff1a;终于不用剪辑了&#xff1f;真能输入几句话就出抖音爆款&#xff1f;我实测过37个标榜“AI视频生成”的海…

作者头像 李华
网站建设 2026/9/19 12:40:47

meshoptimizer|快速让 3D 网格更小更快的轻量优化工具库

meshoptimizer&#xff5c;快速让 3D 网格更小更快的轻量优化工具库 【免费下载链接】meshoptimizer Mesh optimization library that makes meshes smaller and faster to render 项目地址: https://gitcode.com/GitHub_Trending/me/meshoptimizer meshoptimizer 是一个…

作者头像 李华