Firebase AI SDK 多模态 Token 计数:ModalityTokenCount 接口深度解析

原创2026-10-14 18:42:192,268 阅读
文章标签:开发工具后端

Firebase AI SDK 多模态 Token 计数:ModalityTokenCount 接口深度解析

导读

ModalityTokenCount 是 Firebase JavaScript SDK 中 AI 模块(@firebase/ai)提供的一个核心数据结构,用于按**模态(modality)**维度描述一次模型请求中消耗的 Token 数量。它出现在 generateContent() 的用量元数据(UsageMetadata)以及 countTokens() 的响应中,是开发者理解多模态输入(文本、图片、视频、音频、文档)成本构成、排查 Token 超限问题、优化提示词的关键入口。读完本文,你将掌握该接口的定义、所有取值模态、其出现的完整上下文,以及如何在实际代码中读取和利用这些分模态的 Token 数据。

接口定义与源码位置

ModalityTokenCount 是一个导出(export)的 TypeScript 接口,其正式声明位于 packages/ai/src/types/responses.ts,与 API 参考文档 docs-devsite/ai.modalitytokencount.md 一一对应:

export interface ModalityTokenCount {
  /** The modality associated with this token count. */
  modality: Modality;
  /** The number of tokens counted. */
  tokenCount: number;
}

接口仅包含两个字段,结构非常简单,但含义明确:

属性 类型 说明
modality Modality 该 Token 计数所对应的内容模态(如文本、图片、视频等)
tokenCount number 该模态下被计数的 Token 数量

该接口在公共类型中标注为 @public,属于 SDK 对外公开的稳定 API,可直接从包入口导入使用。

模态(Modality)取值全集

modality 字段的类型 Modality 由 SDK 在 packages/ai/src/types/enums.ts 中定义为字符串字面量联合类型:

export const Modality = {
  /** Unspecified modality. */
  MODALITY_UNSPECIFIED: 'MODALITY_UNSPECIFIED',
  /** Plain text. */
  TEXT: 'TEXT',
  /** Image. */
  IMAGE: 'IMAGE',
  /** Video. */
  VIDEO: 'VIDEO',
  /** Audio. */
  AUDIO: 'AUDIO',
  /** Document (for example, PDF). */
  DOCUMENT: 'DOCUMENT'
} as const;

export type Modality = (typeof Modality)[keyof typeof Modality];

对应官方 API 文档见 docs-devsite/ai.md 中的 Modality 常量签名。各取值含义如下:

枚举值 含义
MODALITY_UNSPECIFIED 未指定的模态(通常表示该计数组未声明具体类型)
TEXT 纯文本
IMAGE 图片
VIDEO 视频
AUDIO 音频
DOCUMENT 文档(例如 PDF)

从源码结构看,SDK 内部使用 const 断言构造的对象配合 keyof typeof 推导出类型,既保证运行时值的存在,又提供编译期的字面量类型提示。由于是联合类型,你在分支判断时可以直接做字符串比较:

import { Modality } from '@firebase/ai';

if (detail.modality === Modality.IMAGE) {
  // 处理图片 Token 计数
}

出现在哪里:两种使用场景

ModalityTokenCount 本身不是独立出现的返回体,而是作为数组元素嵌在两个主要数据结构中。理解它出现的上下文,才能真正用好它。

场景一:生成内容响应的用量元数据(UsageMetadata)

UsageMetadata 接口(定义于 packages/ai/src/types/responses.ts)挂在每次 generateContent() / generateContentStream() 返回的 GenerateContentResponse 上。其中四个字段的类型均为 ModalityTokenCount[]:

字段 说明
promptTokensDetails 提示词(prompt)按模态拆分的 Token 明细
candidatesTokensDetails 模型生成内容(candidates)按模态拆分的 Token 明细
toolUsePromptTokensDetails 工具调用所消耗的 Token 按模态拆分明细
cacheTokensDetails 命中隐式缓存的内容按模态拆分明细

对应的参考文档为 docs-devsite/ai.usagemetadata.md,其中明确注明 cacheTokensDetails 提供“哪些部分内容被缓存”的细粒度视图,而 toolUsePromptTokensDetails 描述工具调用消费的 Token。这几个明细字段与同接口下的汇总数值字段(promptTokenCount、candidatesTokenCount、thoughtsTokenCount、toolUsePromptTokenCount、cachedContentTokenCount、totalTokenCount)配合使用:汇总字段给出总数,ModalityTokenCount[] 明细字段给出分模态构成。

场景二:countTokens() 的响应(CountTokensResponse)

GenerativeModel.countTokens() 用于在真正调用模型前估算 Token 消耗,其响应接口 CountTokensResponse(定义于 packages/ai/src/types/responses.ts)同样包含一个 promptTokensDetails?: ModalityTokenCount<a href="https://link.gitcode.com/i/389045b42142d1f3497246569090a2a5" target="_blank">] 字段,描述“提示词按模态消耗的 Token 拆分”。对应文档见 [docs-devsite/ai.counttokensresponse.md。

export interface CountTokensResponse {
  /** The total number of tokens counted across all instances from the request. */
  totalTokens: number;
  /** The breakdown, by modality, of how many tokens are consumed by the prompt. */
  promptTokensDetails?: ModalityTokenCount[];
}

调用链与底层实现

从 GenerativeModel.countTokens() 到分模态明细

countTokens() 的入口位于 packages/ai/src/models/generative-model.ts:

async countTokens(
  request: CountTokensRequest | string | Array<string | Part>,
  singleRequestOptions?: SingleRequestOptions
): Promise<CountTokensResponse> {
  const formattedParams = formatGenerateContentInput(request);
  return countTokens(
    this._apiSettings,
    this.model,
    formattedParams,
    this.chromeAdapter,
    { ...this.requestOptions, ...singleRequestOptions }
  );
}

其核心实现位于 packages/ai/src/methods/count-tokens.ts:

export async function countTokensOnCloud(
  apiSettings: ApiSettings,
  model: string,
  params: CountTokensRequest,
  singleRequestOptions?: SingleRequestOptions
): Promise<CountTokensResponse> {
  params = cleanCountTokensRequestForWire(params);
  let body: string = '';
  if (apiSettings.backend.backendType === BackendType.GOOGLE_AI) {
    const mappedParams = GoogleAIMapper.mapCountTokensRequest(params, model);
    body = JSON.stringify(mappedParams);
  } else {
    body = JSON.stringify(params);
  }
  const response = await makeRequest(
    { model, task: Task.COUNT_TOKENS, apiSettings, stream: false, singleRequestOptions },
    body
  );
  return response.json();
}

从代码可以看到两个值得注意的细节:

  1. 请求清理:cleanCountTokensRequestForWire 会在发送前规范化请求参数(去掉不可序列化或服务端不接受的部分);
  2. 后端适配:当使用 Gemini Developer API(BackendType.GOOGLE_AI)时,会通过 mapCountTokensRequest(见 packages/ai/src/googleai-mappers.ts)将请求映射为 GoogleAICountTokensRequest 包装格式(包含 generateContentRequest 外层结构,定义于 packages/ai/src/types/googleai.ts);使用 Agent Platform(Firebase Vertex AI)后端时则直接发送原始参数;
  3. 响应最终通过 response.json() 反序列化,promptTokensDetails 数组中的每个元素即解析为一个 ModalityTokenCount 对象。

测试用例对分模态数据的验证

仓库的单元测试 packages/ai/src/methods/count-tokens.test.ts 专门验证了分模态明细的解析逻辑:

it('total tokens with modality details', async () => {
  const mockResponse = getMockResponse(
    'vertexAI',
    'unary-success-detailed-token-response.json'
  );
  const makeRequestStub = vi
    .spyOn(mockRequest, 'makeRequest')
    .mockResolvedValue(mockResponse as Response);
  const result = await countTokens(fakeApiSettings, 'model', fakeRequestParams, fakeChromeAdapter);
  expect(result.totalTokens).to.equal(1837);
  expect(result.promptTokensDetails?.[0].modality).to.equal('IMAGE');
  expect(result.promptTokensDetails?.[0].tokenCount).to.equal(1806);
  // ...
});

该测试使用一份含分模态明细的 mock 响应,断言:总 Token 数为 1837,其中第一项明细的 modality 为 IMAGE、tokenCount 为 1806。这直接印证了 ModalityTokenCount 的两个字段从原始 JSON 到 TypeScript 对象的完整解析链路,也说明实际使用中 totalTokens 与各模态明细之和可以交叉核对。此外,该测试还验证了请求会携带 Task.COUNT_TOKENS 任务标识并以 contents 作为请求体。

实际使用示例

下面给出一个完整的读取分模态 Token 明细的代码示例,结合 countTokens() 与 generateContent() 两种入口:

import { getGenerativeModel, Modality } from '@firebase/ai';

const model = getGenerativeModel(/* app, modelId, options */);

// 方式一:调用 countTokens 预估消耗
const estimate = await model.countTokens('一段需要评估的文本');
console.log(`总 Token 数: ${estimate.totalTokens}`);
for (const detail of estimate.promptTokensDetails ?? []) {
  console.log(`模态 ${detail.modality}: ${detail.tokenCount} tokens`);
}

// 方式二:从生成响应中读取实际用量
const response = await model.generateContent({
  contents: [{ role: 'user', parts: [{ text: '描述这张图片' }, { inlineData: { mimeType: 'image/jpeg', data: '...' } }] }]
});
const usage = response.usageMetadata;
if (usage) {
  // 汇总数值
  console.log(`Prompt: ${usage.promptTokenCount}, Candidates: ${usage.candidatesTokenCount}`);
  // 分模态明细
  for (const d of usage.promptTokensDetails ?? []) {
    console.log(`Prompt ${d.modality}: ${d.tokenCount} tokens`);
  }
  // 缓存命中明细
  for (const d of usage.cacheTokensDetails ?? []) {
    if (d.modality === Modality.TEXT) {
      console.log(`缓存中的文本 Token: ${d.tokenCount}`);
    }
  }
}

使用注意点:

  • promptTokensDetails、cacheTokensDetails 等明细字段均为可选(?),服务端可能不返回,读取前务必做空值兜底(如上面的 ?? []);
  • modality 的取值来自 Modality 常量,建议用 Modality.TEXT、Modality.IMAGE 等具名常量做比较,避免魔法字符串;
  • 在 Gemini Developer API 与 Agent Platform 两种后端下,请求包装与字段映射路径不同(见上文调用链),但返回的 ModalityTokenCount 结构一致,上层代码无需区分后端。

小结

ModalityTokenCount 虽然只有两个字段,却是 Firebase AI SDK 中“多模态 Token 计量”的基石:

无论是做成本核算、缓存策略优化,还是排查“为什么这段多模态输入 Token 特别多”,先学会读懂 ModalityTokenCount,就能把用量数据从“一个数字”拆解成“每种内容的成本结构”,从而做出更精准的优化决策。

登录后查看全文
firebase-js-sdk