Firebase AI SDK 多模态 Token 计数:ModalityTokenCount 接口深度解析
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();
}
从代码可以看到两个值得注意的细节:
- 请求清理:
cleanCountTokensRequestForWire会在发送前规范化请求参数(去掉不可序列化或服务端不接受的部分); - 后端适配:当使用 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)后端时则直接发送原始参数; - 响应最终通过
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 计量”的基石:
- 定义:
modality: Modality+tokenCount: number,源码见 packages/ai/src/types/responses.ts; - 模态取值:
TEXT/IMAGE/VIDEO/AUDIO/DOCUMENT/MODALITY_UNSPECIFIED,定义见 packages/ai/src/types/enums.ts; - 出现位置:
UsageMetadata的四个明细字段与CountTokensResponse.promptTokensDetails; - 验证依据:单元测试 packages/ai/src/methods/count-tokens.test.ts 证实了分模态数据的解析行为。
无论是做成本核算、缓存策略优化,还是排查“为什么这段多模态输入 Token 特别多”,先学会读懂 ModalityTokenCount,就能把用量数据从“一个数字”拆解成“每种内容的成本结构”,从而做出更精准的优化决策。