☰
AI模型token费用对比:从价格归一化到比价网站实战
2026/10/22 15:45:55 网站建设 项目流程

之前一直在用 Codex 做 AI 编程,最头疼的就是 token 消耗太快。明明只是自动补全、跑个测试,一天下来账单数字就让人肉疼。更麻烦的是,OpenAI、Anthropic、DeepSeek、Gemini 这些模型家族的定价口径完全不一样,有的按百万 token 算,有的按千 token 算,输入输出价格差异又大,还有缓存命中、上下文缓存这些细碎计费项。每次估算项目成本,都要翻好几个官网,换算半天。

后来我花了不少时间,把主流 AI 模型的公开定价、计费口径和常见中转方式整理到一起,做了一个免费的 AI 充值比价网站。核心功能很简单:把不同平台的 token 单价拉到同一个口径下,用户输入一个场景,直接估算大概消耗多少钱,并对比各家方案。这篇文章把整个项目从数据采集、价格归一化、费用计算引擎到前后端部署的完整过程拆开讲,适合想做 AI 工具、或者自己开发中需要做成本测算的开发者。即便你不做网站,里面的价格计算模型和接口设计思路,也可以直接用到自己的项目里。

1. 为什么需要 AI 价格比价

1.1 痛点来源:Codex 与 token 计费

Codex 是 OpenAI 推出的 AI 编程代理,它和普通聊天机器人不太一样,会自主完成代码编写、命令执行、测试反馈等一系列操作。正因为它是“代理式”工作,一次任务可能触发很多轮模型调用,token 消耗会成倍增长。比如一个很小的代码重构,聊天式工具可能只消耗 5000 token,但 Codex 从解析代码、生成补丁、执行命令到分析报错,整体消耗可能达到几万甚至十几万 token。

这带来一个很现实的问题:token 用量很难预估。你在开工之前,根本不知道这个任务会花多少钱。而不同平台的计费差异非常大,某款模型的输入价格可能是另一款的十几倍。如果只是无脑选贵的模型,项目成本会迅速失控。

1.2 定价口径不统一

我做比价网站最核心的障碍,不是数据难找,而是“单位不统一”。

  • 有的平台按“每 1000 token”计费,比如 0.03 美元/1K tokens。
  • 有的平台按“每 100 万 token”计费,比如 3 美元/1M tokens。
  • 输入价格和输出价格通常不一样。
  • 缓存命中(cached input)价格往往远低于常规输入价格。
  • 还有的模型支持提示词缓存,写入缓存也要计费。

如果不把这些口径全部转换成同一个基准,比如统一为“美元 / 每 100 万 token”,比较起来完全没有意义。

1.3 网站要解决什么问题

这个网站的目标非常具体:

  1. 把主流模型的价格统一到同一计费口径,支持快速查询。
  2. 输入“任务类型 + 预估 token 量”,自动估算成本。
  3. 对比不同供应商在相同场景下的成本差异。
  4. 提供免费的查询和估算能力,供开发者日常使用。

这篇文章记录的就是从零实现这个比价网站的过程,重点讲数据模型设计、价格归一化逻辑、费用计算引擎的代码实现,以及部署上线时遇到的典型问题。

2. 整体架构与功能拆解

2.1 项目范围

既然是个人开发维护的免费工具,架构不能搞得太重。最终采用前后端分离的轻量方案:

  • 后端:Python + FastAPI,负责价格数据查询和费用估算接口。
  • 前端:原生 HTML + JavaScript(也可以替换成 Vue,但纯静态放在服务器上更省成本)。
  • 数据存储:SQLite,保存价格数据和更新记录。
  • 定时任务:APScheduler 或系统 cron,定期更新价格快照。

这个方案的好处是,服务器资源占用非常小,一台低配云主机就能跑起来,方便长期免费提供给大家使用。

2.2 核心功能模块

整个网站拆成下面几个模块:

模块职责关键点
数据采集模块收集官方公开价格数据优先使用公开页面/JSON,人工校验
价格归一化模块将不同单位统一为 USD/1M tokens核心逻辑,不能出错
费用估算引擎根据 token 量估算调用成本区分输入/输出/缓存
API 层对外提供查询接口支持列表查询、价格估算
前端展示模型价格表和成本计算器简单清晰,移动端可用
价格快照记录历史价格为后续价格趋势提供数据

2.3 目录结构

项目源码目录规划如下,后面所有代码都围绕这个结构展开:

ai-price-compare/ ├── app/ │ ├── main.py # FastAPI 入口 │ ├── database.py # SQLite 连接与建表 │ ├── models.py # Pydantic 数据模型 │ ├── price_service.py # 价格查询服务 │ ├── estimate_service.py # 费用估算引擎 │ └── scheduler.py # 定时更新任务 ├── data/ │ ├── prices.json # 原始价格数据 │ └── price_history.db # SQLite 数据库 ├── frontend/ │ ├── index.html # 首页 │ ├── app.js # 前端逻辑 │ └── style.css # 样式 ├── scripts/ │ └── update_prices.py # 手动更新价格脚本 └── requirements.txt

接下来我重点讲三个核心部分:数据模型、价格归一化、费用估算引擎。

3. 数据模型与存储设计

3.1 价格表结构

价格记录需要支持不同模型、不同供应商、不同计费类型。一张表如果只存“model、price”两个字段,完全不够用。我设计了下面这个表结构:

CREATE TABLE IF NOT EXISTS model_prices ( id INTEGER PRIMARY KEY AUTOINCREMENT, provider TEXT NOT NULL, -- 供应商,例如 openai model_name TEXT NOT NULL, -- 模型名称,例如 gpt-4o price_type TEXT NOT NULL, -- input / output / cached_input / cache_write price_per_1m REAL NOT NULL, -- 每 100 万 token 价格,单位美元 currency TEXT DEFAULT 'USD', -- 币种 effective_date TEXT NOT NULL, -- 生效日期 source_url TEXT, -- 来源地址 updated_at TEXT DEFAULT CURRENT_TIMESTAMP ); CREATE INDEX IF NOT EXISTS idx_model ON model_prices(model_name);

这里有几个字段需要特别说明:

  • price_type用来区分计费类型。同一种模型,输入、输出、缓存命中的价格差异非常大,必须分开存。
  • effective_date表示该价格从什么时候开始生效。AI 模型降价特别频繁,保留生效日期,后续才能做价格趋势。
  • source_url是价格来源链接,方便审计和人工核对。

3.2 Python 数据模型

在应用层,我用 Pydantic 定义模型,方便做接口参数校验:

# 文件路径:app/models.py from typing import Literal from pydantic import BaseModel class PriceRecord(BaseModel): provider: str model_name: str price_type: Literal["input", "output", "cached_input", "cache_write"] price_per_1m: float currency: str = "USD" effective_date: str source_url: str = "" class EstimateRequest(BaseModel): model_name: str input_tokens: int = 1000 output_tokens: int = 500 cached_input_tokens: int = 0 cache_write_tokens: int = 0 class EstimateResult(BaseModel): model_name: str total_cost_usd: float input_cost_usd: float output_cost_usd: float cached_input_cost_usd: float cache_write_cost_usd: float detail: str = ""

PriceRecord用于价格数据写入和查询,EstimateRequest是费用估算接口的请求体,EstimateResult是返回给前端的估算结果。

3.3 为什么必须区分 cached_input 和 cache_write

很多开发者做 token 成本计算时,只考虑“输入 + 输出”,忽略了缓存逻辑。但实际调用中,如果用了 prompt caching,缓存命中的输入价格可能只有常规输入价格的 25% 甚至 10%,而写入缓存的费用是单独计算的。

举个例子:

  • 普通输入:3 美元/1M tokens
  • 缓存命中输入:0.3 美元/1M tokens
  • 缓存写入:3.75 美元/1M tokens

如果在计算时不区分这三者,你估算的成本可能比实际成本高出好几倍。这在需要频繁调用同一批系统指令、上下文模板的 AI 编程场景中特别明显。

4. 价格归一化逻辑

4.1 为什么要归一化

不同平台公布价格时,单位差异很大:

  • 某平台写:$0.00003 / 1K tokens
  • 另一平台写:$3.00 / 1M tokens
  • 还有的平台写:¥0.015 / 1K tokens

如果直接把价格乘进代码里,很容易算错数量级。所以我写了一个normalize_price函数,统一转换为“美元 / 每 100 万 token”,也就是USD per 1M tokens。

# 文件路径:app/price_service.py from typing import Tuple def normalize_price( price_value: float, unit_tokens: int = 1000, currency: str = "USD", usd_rate: float = 1.0 ) -> float: """ 将任意平台公布的价格归一化为 USD / 1M tokens。 :param price_value: 原始价格数字 :param unit_tokens: 该价格对应的 token 数量,默认 1000 :param currency: 原始币种 :param usd_rate: 该币种兑美元汇率,默认 1.0 :return: 归一化后的 USD / 1M tokens 价格 """ # 先换算成每 1 个 token 的美元价格 price_per_token = price_value * usd_rate / unit_tokens # 再换算成每 1M token 的价格 return round(price_per_token * 1_000_000, 6) def normalize_cny_to_usd(price_cny_per_1k: float, usd_rate: float) -> float: """ 示例:如果是人民币价格,先按汇率换算。 这个函数只是示例思路,实际汇率需要从接口或配置获取。 """ price_usd_per_1k = price_cny_per_1k / usd_rate return normalize_price(price_usd_per_1k, unit_tokens=1000, currency="USD")

写进数据库前,我还会做一次边界检查,防止负数和明显异常的大数污染数据:

def validate_price(value: float) -> bool: if value < 0: return False if value > 1000: # 每 1M token 超过 1000 美元的项目需要人工复核 return False return True

4.2 构建模型价格字典

处理完归一化之后,为了方便查询,我在内存里维护一份模型价格字典,结构如下:

{ "gpt-4o": { "provider": "openai", "input": 2.50, "output": 10.00, "cached_input": 0.30, "cache_write": 3.75 }, "claude-3-5-sonnet": { "provider": "anthropic", "input": 3.00, "output": 15.00, "cached_input": 0.30, "cache_write": 3.75 }, "deepseek-chat": { "provider": "deepseek", "input": 0.27, "output": 1.10, "cached_input": 0.07, "cache_write": 1.10 } }

注意,真实价格随时间变化很快,上文这些价格只用于演示数据结构,实际部署一定要以官方最新公布的价格为准。千万不要把网上某一个时间点的价格写死进代码。

4.3 数据更新策略

价格数据更新的核心原则是“官方公开,半自动维护”。

全自动爬取官方定价页面风险较高,因为很多网站有反爬策略,页面结构频繁变化,而且模型价格调整需要人工确认。我的做法是:

  1. 定期从官方公开的 pricing 页面或 JSON 文件抓取价格数据。
  2. 抓取完之后,先经过自动校验,再通过一个管理后台或者手动脚本审核入库。
  3. 如果抓取失败,保留上一次成功的数据,不中断服务。

手动更新脚本示意如下:

# 文件路径:scripts/update_prices.py import json import sqlite3 # 这里只是示意,实际需要从官方源拉取 RAW_PRICES = [ { "provider": "openai", "model_name": "gpt-4o", "input": 2.50, "output": 10.00, "cached_input": 0.30, "cache_write": 3.75 } ] def update_prices(conn: sqlite3.Connection, prices: list[dict]) -> None: for item in prices: for price_type in ["input", "output", "cached_input", "cache_write"]: if price_type not in item: continue conn.execute( """ INSERT INTO model_prices (provider, model_name, price_type, price_per_1m, effective_date) VALUES (?, ?, ?, ?, date('now')) """, (item["provider"], item["model_name"], price_type, item[price_type]) ) conn.commit() if __name__ == "__main__": conn = sqlite3.connect("data/price_history.db") update_prices(conn, RAW_PRICES) conn.close()

5. 费用估算引擎

5.1 计费公式

费用估算的逻辑并不复杂,但很容易算漏。核心公式是:

总费用 = 输入 token 数 × 输入单价 + 缓存命中 token 数 × 缓存命中单价 + 缓存写入 token 数 × 缓存写入单价 + 输出 token 数 × 输出单价

这里有一个容易忽略的地方:如果请求命中了 prompt 缓存,那一部分 token 不应该再按普通输入价格计费。也就是说,普通输入和缓存命中输入要分开计算,不能把input_tokens和cached_input_tokens混在一起。

5.2 估算引擎实现

# 文件路径:app/estimate_service.py from app.models import EstimateRequest, EstimateResult from app.price_service import get_price_map def estimate_cost(req: EstimateRequest) -> EstimateResult: """ 根据模型名称和 token 用量估算费用。 """ price_map = get_price_map() model = price_map.get(req.model_name) if not model: return EstimateResult( model_name=req.model_name, total_cost_usd=0, input_cost_usd=0, output_cost_usd=0, cached_input_cost_usd=0, cache_write_cost_usd=0, detail=f"未找到模型 {req.model_name} 的价格,请检查模型名称。" ) input_cost = req.input_tokens / 1_000_000 * model.get("input", 0) output_cost = req.output_tokens / 1_000_000 * model.get("output", 0) cached_input_cost = req.cached_input_tokens / 1_000_000 * model.get("cached_input", 0) cache_write_cost = req.cache_write_tokens / 1_000_000 * model.get("cache_write", 0) total_cost = input_cost + output_cost + cached_input_cost + cache_write_cost return EstimateResult( model_name=req.model_name, total_cost_usd=round(total_cost, 6), input_cost_usd=round(input_cost, 6), output_cost_usd=round(output_cost, 6), cached_input_cost_usd=round(cached_input_cost, 6), cache_write_cost_usd=round(cache_write_cost, 6), detail="估算结果仅供参考,实际费用以供应商账单为准。" )

这里的关键是统一用/ 1_000_000把 token 数量换算成“百万 token”,再乘以price_per_1m。这个口径一旦统一,整个系统就不会出现数量级错误。

5.3 场景示例

假设我们要对比在 Codex 中接入不同模型时,重构一个模块的成本。场景参数如下:

  • 输入上下文:200,000 tokens
  • 其中缓存命中:150,000 tokens
  • 缓存写入:20,000 tokens
  • 输出:5,000 tokens

调用estimate_cost之后,前端会展示一张对比表。不同模型的成本差异会非常直观。比如某些模型常规输入价格高,但缓存命中价格很低,适合高频复用上下文的任务;有些模型输出价格便宜,适合大量生成代码的场景。

6. API 与前端部署实战

6.1 FastAPI 接口

API 层只需要两个接口:模型价格列表查询、费用估算。

# 文件路径:app/main.py from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from app.models import EstimateRequest, EstimateResult from app.price_service import list_prices from app.estimate_service import estimate_cost app = FastAPI(title="AI Price Compare API", version="1.0.0") app.add_middleware( CORSMiddleware, allow_origins=["*"], # 生产环境建议收紧 allow_methods=["GET", "POST"], allow_headers=["*"], ) @app.get("/api/price/list") def get_price_list(): return list_prices() @app.post("/api/price/estimate", response_model=EstimateResult) def get_cost_estimate(req: EstimateRequest): return estimate_cost(req)

要注意,allow_origins=["*"]只适用于开发环境,正式上线建议改成自己的域名,避免被任意页面跨域调用。

6.2 前端页面

前端我采用了最朴素的方式:一个输入区域 + 一个结果表格。用户选择模型、输入 token 参数,点击估算,前端把请求发到后端接口并渲染结果。

核心 JS 逻辑如下:

// 文件路径:frontend/app.js async function loadPrices() { const res = await fetch('/api/price/list'); const data = await res.json(); const select = document.getElementById('modelSelect'); data.forEach(item => { const option = document.createElement('option'); option.value = item.model_name; option.textContent = `${item.model_name} (${item.provider})`; select.appendChild(option); }); } async function estimate() { const modelName = document.getElementById('modelSelect').value; const inputTokens = parseInt(document.getElementById('inputTokens').value) || 0; const outputTokens = parseInt(document.getElementById('outputTokens').value) || 0; const cachedInputTokens = parseInt(document.getElementById('cachedInputTokens').value) || 0; const cacheWriteTokens = parseInt(document.getElementById('cacheWriteTokens').value) || 0; const res = await fetch('/api/price/estimate', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ model_name: modelName, input_tokens: inputTokens, output_tokens: outputTokens, cached_input_tokens: cachedInputTokens, cache_write_tokens: cacheWriteTokens }) }); const result = await res.json(); renderResult(result); }

前端页面不需要做太复杂的交互,重点是清晰展示:

  • 模型名称
  • 输入成本
  • 输出成本
  • 缓存命中成本
  • 总成本

我还在页面底部加了免责声明,提醒用户估算结果仅供参考,实际价格以各家官网为准。

6.3 本地运行

在项目根目录安装依赖并启动:

pip install -r requirements.txt uvicorn app.main:app --host 0.0.0.0 --port 8000

启动后,打开http://localhost:8000就能访问前端页面。接口文档会自动生成在http://localhost:8000/docs。

7. 开发 Codex 相关功能的踩坑记录

做这个网站期间,我大量使用了 Codex 来辅助开发,也遇到了几个很典型的报错,这里集中记录一下排查思路。

7.1 Codex CLI 无法启动

报错信息类似:

unable to locate the codex cli binary. set codex cli path or ensure the executable is in your PATH

这个问题的原因是系统找不到 Codex 的可执行文件。排查顺序:

  1. 确认是否安装了 Codex CLI,安装命令一般是npm install -g @openai/codex或通过官方安装脚本。
  2. 确认 Codex 可执行文件所在目录是否在PATH环境变量中。
  3. 如果 IDE 插件仍然报错,可以在插件设置里手动指定 Codex 路径,常见环境变量是CODEX_CLI_PATH。
# 查看 codex 路径 which codex # 临时加入 PATH(macOS/Linux) export PATH="$PATH:/usr/local/bin"

7.2 登录 token 交换失败

报错信息常见的有:

sign-in could not be completed token exchange failed token endpoint returned status 403 forbidden

这个报错通常是认证流程中,本地客户端拿临时授权码去换取访问令牌时,网络出口 IP 或地区不被目标服务支持。也可能是系统时间不准导致 JWT 校验失败。

排查步骤:

  1. 检查本机系统时间,误差过大会导致 token 校验失败。
  2. 检查网络环境,确认出口 IP 是否在服务允许范围内。
  3. 清空本地缓存配置后重新登录。
  4. 如果使用团队网关或代理,确认请求头中的认证信息没有被篡改。

7.3 模型不支持报错

报错信息类似:

the 'gpt-5.6-sol' model is not supported when using codex with a ...

这个原因是模型名写错,或者当前 Codex 版本还不支持该模型。解决方案是:

  • 检查 Codex 配置文件和启动命令中的模型名。
  • 升级 Codex CLI 到最新版本。
  • 如果已经通过第三方接口接入其他模型,确认模型名与供应商平台完全一致。

我在写比价网站时,就遇到过一次模型名大小写出错导致请求直接失败。后来所有模型名统一小写存储,并在查询时做大小写归一化,才彻底解决。

7.4 token 用量统计不准确

做网站之前,我习惯直接用 Codex 的上下文显示去估成本,后来发现完全不准。正确做法是累计读取接口返回的prompt_tokens、completion_tokens和cached_tokens字段,再做聚合。

def accumulate_usage(usage: dict, total: dict) -> None: total["prompt_tokens"] = total.get("prompt_tokens", 0) + usage.get("prompt_tokens", 0) total["completion_tokens"] = total.get("completion_tokens", 0) + usage.get("completion_tokens", 0) total["cached_tokens"] = total.get("cached_tokens", 0) + usage.get("prompt_tokens_details", {}).get("cached_tokens", 0)

8. 常见问题排查表

问题现象常见原因解决思路
接口返回模型价格为零数据表未初始化或价格缺失检查 SQLite 中 model_prices 表数据
前端跨域请求失败CORS 配置未生效检查 FastAPI CORSMiddleware 配置
估算结果数量级不对输入 token 单位混淆确认是否统一按百万 token 计算
token 用量和账单对不上忽略缓存 token 计费仔细阅读供应商计费文档
定时任务更新价格失败数据源反爬或接口变更降级为保留旧数据,并人工告警
Codex 登录报 403网络出口限制检查地区限制、系统时间和代理设置
Codex 模型不支持模型名写错或版本过旧升级 CLI,核对模型名

9. 最佳实践与工程建议

9.1 价格数据必须可配置化

我见过很多工具把价格直接写成常量硬编码在 Python 文件里。这在一两周内没问题,但 AI 模型降价、调价非常频繁,硬编码意味着每次调价都要改代码、发版。更合理的做法是:

  • 价格放在数据库或 JSON 配置文件中。
  • 启动时加载到内存缓存。
  • 提供手动更新入口和定时更新任务。

9.2 接口鉴权与频率限制

既然是免费公开服务,接口很容易被人写脚本循环请求,造成资源浪费。建议加一个简单的频率限制,比如同一个 IP 每分钟最多请求 30 次。FastAPI 生态里可以直接用slowapi实现。

# 示意代码:限制请求频率 from slowapi import Limiter from slowapi.util import get_remote_address limiter = Limiter(key_func=get_remote_address) # @app.get("/api/price/list") # @limiter.limit("30/minute")

9.3 不要暴露 API Key

任何比价工具,如果接入了真实供应商 API 获取实时价格,都必须把 Key 放在服务端,不能让浏览器直接请求供应商接口。我目前的方案全部基于公开价格数据,因此不涉及用户 Key。如果后续做“真实充值渠道比价”,难度和合规要求会完全不同,这里只是提醒:敏感凭证永远不要进前端代码。

9.4 日志与观测

上线之后,我加了两条最基本的日志:

  1. 每次估算请求记录模型名、token 参数、估算结果。
  2. 定时更新任务记录成功/失败状态、更新条数。

日志不只是排查问题用,更重要的是能观察用户都在查哪些模型。比如发现某个模型被反复查询但价格一直显示旧数据,就需要及时更新。

9.5 合规与免责声明

网站上一定要声明“价格信息仅供参考,实际以官方为准”。因为 AI 模型价格调整频繁,而且不同渠道的结算方式可能包含折扣、赠送额度等,任何第三方比价工具都不能保证 100% 准确。我在页面底部添加了免责声明,同时在 API 返回结果里也加了一个detail字段提醒用户。

10. 后续规划

目前这个比价网站已经可以正常使用,核心能力是:

  • 模型价格统一查询
  • 输入场景化费用估算
  • 缓存计费细节计算
  • 常见模型横向对比

后续可以继续扩展的方向包括:

  1. 价格历史趋势图,方便观察模型降价节奏。
  2. 把估算工具做成浏览器插件,在 ChatGPT、Codex 网页版中直接显示当前对话的预估成本。
  3. 增加按任务类型推荐模型的逻辑,比如“代码重构优先推荐 X 模型,长文总结推荐 Y 模型”。

如果你也在做 AI 成本控制相关工具,建议先从最小的价格归一化模块入手,把它做成一个函数库。这样无论后续是接命令行、做网页,还是做浏览器插件,都可以复用同一套计算逻辑。代码里的所有模型价格数据,也一定记得从官方及时核对。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询