Gemini 1.5 Flash API 输入 token 消耗异常
结论
计算时漏掉了两个因素:
- 图像也会换算成 input tokens,不能只计算不足 400 tokens 的文本提示词。
- 如果使用多轮
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 随请求次数持续增加,基本可以确定聊天历史或累计内容被重复发送了。
排查顺序
- 确认循环中传入的是
frame,不是frames。 - 确认没有持续向同一个
contents列表追加图片。 - 不要用同一个
chat会话处理所有独立画面。 - 不要把 Base64 图片内容拼进文本提示词。
- 记录每次响应的
usage_metadata。 - 使用
count_tokens()统计文本和图片组成的完整请求。 - 记录每张图片的分辨率,并测试缩小图片后 token 数量的变化。
- 检查是否有其他程序、API key 或云项目在发起 Gemini 请求。
- 按账单中的 input token 档位分别乘以对应单价,不要套用输出 token 单价。
仅凭 700 次请求、每次不足 400 个文本 tokens,无法解释超过 2200 万 input tokens 的用量。这通常说明请求中包含大量视觉输入、超高分辨率图片、累计聊天历史或重复图片。账单中出现 “input is longer than 128k tokens” 时,应优先检查是否复用了同一个聊天会话,或是否一直在扩充请求内容。
备注:内容仅供参考。
