PAYATHON 2026

Gemini 1.5 Flash API 输入 token 消耗异常

支付小周

结论

计算时漏掉了两个因素:

  1. 图像也会换算成 input tokens,不能只计算不足 400 tokens 的文本提示词。
  2. 如果使用多轮 chat、重复传入历史消息,或把不断累积的 frames 列表发给 API,之前的图像和文本会在后续请求中再次作为输入计费。

“超过 128k tokens”是指某一次请求的完整输入上下文超过 128k,其中包括文本、图像和聊天历史,并非最新输入的那段文字超过 128k。

账单也说明,输入单价不是按 $0.30 / 1M tokens 计算的:

10,987,345 / 1,000,000 × $0.075 ≈ $0.824
11,993,645 / 1,000,000 × $0.15  ≈ $1.799

计算结果与账单中的约 $0.82 和 $1.78 基本一致。$0.30 / 1M tokens 很可能是当时的输出价格或其他计费档位,不能直接拿来计算 input tokens。

为什么一张图片不等于几个文本 token

Gemini 会把多模态请求中的图片划分并编码成视觉 tokens。具体数量取决于图片尺寸、长宽比、模型,以及后端采用的图像处理规则。

以 Gemini 1.5 系列常见的计算方式为例,小图可能按固定数量的 tokens 处理;较大的图片则可能被切成多个区域,每个区域分别计入 tokens。因此,一张高清视频帧产生的输入量可能远高于提示词本身。

下面这种估算并不完整:

700 × 400 text tokens

更准确的计算方式是:

总输入 tokens
= 每次请求的文本 tokens
+ 每次请求的图像 tokens
+ 每次请求携带的历史上下文 tokens

通过图像对象上传图片时,通常会按视觉输入计算。如果先把图片编码成 Base64,再直接拼进文本提示词,那段 Base64 可能被当成大量文本处理,应避免这样使用。

最需要检查的代码问题

问题中只给出了部分代码,因此无法确定具体是哪一行导致用量增加。常见原因主要有以下几种。

1. 使用同一个聊天会话处理全部画面

例如:

chat = model.start_chat()

for frame in frames:
    response = chat.send_message([
        prompt,
        frame,
    ])

聊天对象通常会保留历史。后面的请求可能再次包含之前发送的图片和回复,导致输入上下文不断增长:

第 1 次:frame_1
第 2 次:frame_1 + response_1 + frame_2
第 3 次:frame_1 + response_1 + frame_2 + response_2 + frame_3
...

即使每次新增的内容不多,累计计费也会很快增加。上下文超过 128k 后,后续请求就会进入长上下文计费档位。

如果各个视频帧之间互不相关,就不需要保留聊天历史,应该使用无状态请求。

2. 每次发送了整个 frames 列表

下面的写法会反复发送之前提取的所有图片:

for frame in frames:
    response = model.generate_content([
        prompt,
        *frames,
    ])

在循环中持续扩充请求内容也会产生同样的问题:

contents = [prompt]

for frame in frames:
    contents.append(frame)
    response = model.generate_content(contents)

每次请求只需包含当前画面:

for frame in frames:
    contents = [prompt, frame]
    response = model.generate_content(contents)

3. 视频帧分辨率过高

OpenCV 读取视频帧时,通常会保留原始分辨率。4K、8K 或超宽画面可能产生较多视觉 tokens。

如果任务只是粗略统计画面中的男性和女性人数,可以在人物仍然清晰可辨的前提下缩小图片:

import cv2
from PIL import Image

def prepare_frame(frame, max_side=768):
    height, width = frame.shape[:2]
    scale = min(max_side / max(height, width), 1.0)

    if scale < 1.0:
        frame = cv2.resize(
            frame,
            (int(width * scale), int(height * scale)),
            interpolation=cv2.INTER_AREA,
        )

    frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
    return Image.fromarray(frame_rgb)

降低分辨率也可能影响人物识别精度。384、768 或其他尺寸都不是固定的最佳值,需要根据视频内容实际测试。

推荐的无状态请求方式

下面的示例沿用原代码中 google.generativeai 风格的 SDK。具体字段名可能随 SDK 版本变化,应以当前安装的版本为准。

import google.generativeai as genai

genai.configure(api_key=api_key)

model = genai.GenerativeModel("gemini-1.5-flash")

prompt = """
Count the visible women and men in this image.
Return JSON only:
{"women": 0, "men": 0}
"""

for frame in frames:
    image = prepare_frame(frame)

    # 每次只发送当前图片,不使用累计聊天历史
    response = model.generate_content([
        prompt,
        image,
    ])

    usage = getattr(response, "usage_metadata", None)
    if usage:
        print("Usage:", usage)

    print(response.text)

如果程序确实需要使用 start_chat(),至少要为每个独立画面创建一个新会话:

for frame in frames:
    image = prepare_frame(frame)

    chat = model.start_chat(history=[])
    response = chat.send_message([
        prompt,
        image,
    ])

不过,对于彼此独立的图片分类任务,直接调用 generate_content() 更清楚,也更不容易意外带上历史记录。

如何确认每张图片实际消耗多少 tokens

不要只根据提示词长度估算。发送前可以调用 token 统计接口,收到响应后再记录实际 usage:

contents = [prompt, image]

token_info = model.count_tokens(contents)
print("Estimated input tokens:", token_info)

response = model.generate_content(contents)
print("Actual usage:", response.usage_metadata)

建议至少记录这些信息:

print({
    "frame_index": frame_index,
    "width": image.width,
    "height": image.height,
    "estimated_tokens": token_info,
    "actual_usage": response.usage_metadata,
})

先单独发送一张图片,再连续测试 10 张。如果每次的用量保持稳定,说明请求是无状态的。如果 input tokens 随请求次数持续增加,基本可以确定聊天历史或累计内容被重复发送了。

排查顺序

  1. 确认循环中传入的是 frame,不是 frames。
  2. 确认没有持续向同一个 contents 列表追加图片。
  3. 不要用同一个 chat 会话处理所有独立画面。
  4. 不要把 Base64 图片内容拼进文本提示词。
  5. 记录每次响应的 usage_metadata。
  6. 使用 count_tokens() 统计文本和图片组成的完整请求。
  7. 记录每张图片的分辨率,并测试缩小图片后 token 数量的变化。
  8. 检查是否有其他程序、API key 或云项目在发起 Gemini 请求。
  9. 按账单中的 input token 档位分别乘以对应单价,不要套用输出 token 单价。

仅凭 700 次请求、每次不足 400 个文本 tokens,无法解释超过 2200 万 input tokens 的用量。这通常说明请求中包含大量视觉输入、超高分辨率图片、累计聊天历史或重复图片。账单中出现 “input is longer than 128k tokens” 时,应优先检查是否复用了同一个聊天会话,或是否一直在扩充请求内容。

备注:内容仅供参考。