Chapter 09

成本追踪与预算:把花钱变成可观测的事

LLM 费用的可怕之处是:一次性无感、月末吓破胆。每个 provider 的计价方式都不同、模型每周还在调价、用户谁都能发 prompt——没有追踪系统,预算就是祈祷。LiteLLM 把"花了多少、谁花的、该不该花"三件事都做成内建。

从一个真实故事开始

2024 年中,某家公司在 OpenAI 上一个月跑出 7 万美元账单——因为某个内部脚本把 GPT-4 调成死循环,没人发现。管理员只有"月底出账"这一个时机看见问题。事后复盘,补了两个最基本的事:每次调用都打 cost 日志,按业务/用户设预算硬限。这两件事 LiteLLM 都自带。

token 和价格是怎么算的

LLM 定价的基本单位是 1M token 多少美元,分 input 和 output 两种价:

cost = (input_tokens / 1e6) * input_price + (output_tokens / 1e6) * output_price

举例(2026 年初参考价,以实际为准):

模型input/Moutput/M一次典型调用(2K in / 500 out)
gpt-4o$2.50$10.00$0.010
gpt-4o-mini$0.15$0.60$0.0006
claude-sonnet-4$3.00$15.00$0.0135
claude-haiku-4$1.00$5.00$0.0045
gemini-2.5-flash$0.075$0.30$0.0003
deepseek-chat$0.14$0.28$0.00042

差距能到 50 倍。选错模型 = 白扔几万美元。这就是为什么 Router 里按业务选模型那么重要。

model_prices.json:LiteLLM 的价格表

LiteLLM 内部维护一份所有 provider × 所有模型的价格表 model_prices_and_context_window_backup.json,随版本更新。调用完它会自动:

  1. 从 response 拿 usage.prompt_tokens / completion_tokens
  2. 在价格表里查 model(含 provider 前缀)。
  3. 算出 response_cost 写进 _hidden_params
from litellm import completion

resp = completion(
    model="gpt-4o-mini",
    messages=[{"role": "user", "content": "hello"}],
)

print(resp.usage.prompt_tokens, resp.usage.completion_tokens)
print(resp._hidden_params["response_cost"])    # 0.0000123 (美元)
print(resp._hidden_params["response_ms"])      # 延迟
print(resp._hidden_params["model"])            # 真实路由到的模型
为什么价格要放 JSON 而不是硬编码?provider 改价、你自己谈到企业折扣、内部部署的自费模型——这些都要能改。价格表是数据,不是逻辑,就该 JSON。

completion_cost():独立算价

如果你不是通过 LiteLLM 发起的调用,但想复用价格表:

from litellm import completion_cost

# 给 response 对象算
cost = completion_cost(completion_response=resp)

# 只给 token 数算
cost = completion_cost(
    model="claude-sonnet-4-5",
    prompt="...",         # 或 prompt_tokens=2000
    completion="...",     # 或 completion_tokens=500
)

print(f"${cost:.6f}")

常用场景:估预算("下个任务 10w 请求大概多少钱")、做 A/B("A 模型 vs B 模型,谁的单位任务成本低")。

token_counter:调用前先估

发请求前你就想知道这个 prompt 多少 token,避免超 context:

from litellm import token_counter, get_max_tokens

n = token_counter(
    model="gpt-4o",
    messages=[{"role": "user", "content": long_text}],
)
ctx = get_max_tokens("gpt-4o")      # 128000

if n > ctx * 0.8:
    text = truncate(long_text)    # 自己的裁剪逻辑

底层用 tiktoken(OpenAI 模型)和各家官方 tokenizer。对 Anthropic / Gemini 是估算,不是 100% 精确——它们没公开 tokenizer,LiteLLM 用近似算法。

自定义价格:内部部署 / 企业折扣

你在公司自己部署了 Llama-3-70B,成本只算 GPU 折旧——$0.5/M;或者跟 Azure 谈到 20% 折扣。这些改官方表就不合适了,要注入自定义价格:

import litellm

# 方式 1: 挂自定义模型的价格
litellm.register_model({
    "my-vllm-llama3-70b": {
        "max_tokens": 8192,
        "input_cost_per_token": 0.0000005,     # $0.5/M
        "output_cost_per_token": 0.0000015,    # $1.5/M
        "litellm_provider": "openai",               # 走 openai 兼容协议
        "mode": "chat",
    }
})

# 方式 2: 给现有模型打折
litellm.register_model({
    "gpt-4o": {
        "input_cost_per_token": 0.0000020,     # 原价 $2.5/M, 谈到 $2.0/M
        "output_cost_per_token": 0.0000080,
    }
})
可以在启动时从配置中心拉价格表。价格谈判变动、DevOps 上调折旧,都只改一处配置,服务代码不动。

成本回调:把每次花费落盘

想做"每次调用实时打到 DB / Kafka / Prometheus",用 success_callback:

import litellm

def track_cost(kwargs, completion_response, start_time, end_time):
    """每次成功调用后被自动叫到"""
    cost = completion_response._hidden_params.get("response_cost", 0)
    model = completion_response.model
    user = kwargs.get("user", "anonymous")
    tags = kwargs.get("metadata", {}).get("tags", [])

    db.insert({
        "ts": end_time,
        "user": user,
        "model": model,
        "cost": cost,
        "latency_ms": (end_time - start_time).total_seconds() * 1000,
        "tags": tags,
        "input_tokens": completion_response.usage.prompt_tokens,
        "output_tokens": completion_response.usage.completion_tokens,
    })

litellm.success_callback = [track_cost]

completion(
    model="gpt-4o",
    messages=msgs,
    user="alice@company.com",
    metadata={"tags": ["chat", "support"]},
)

把这三字段(user / model / tags)打透,下游任何仪表盘都够用了。

流式响应的 cost 追踪

流式响应默认不带 usage(所有 chunk 加起来才知道花了多少),这是个常见坑。两个解:

# 解法 1: 要求 provider 最后一 chunk 带 usage
resp = completion(
    model="gpt-4o",
    messages=msgs,
    stream=True,
    stream_options={"include_usage": True},    # ← OpenAI/Azure 支持
)

chunks = []
for c in resp:
    chunks.append(c)

# 解法 2: LiteLLM 的 stream_chunk_builder 重建完整 response
from litellm import stream_chunk_builder
full = stream_chunk_builder(chunks, messages=msgs)
print(full._hidden_params["response_cost"])

这件事放进你的 success_callback自动处理,业务代码不用管。

Budget Manager:代码级限额

SDK 层的 BudgetManager 给用户/项目设个硬上限,超了就抛异常:

from litellm import BudgetManager

bm = BudgetManager(project_name="my-app")

bm.create_budget(
    total_budget=100.0,     # 上限 $100
    user="alice",
    duration="monthly",    # daily/weekly/monthly/yearly
)

# 调用前先查
if bm.get_current_cost(user="alice") >= bm.get_total_budget("alice"):
    raise Exception("budget exceeded")

resp = completion(model="gpt-4o", messages=msgs, user="alice")

# 调用后更新
bm.update_cost(completion_obj=resp, user="alice")
bm.save_data()   # 持久化

这是代码级的软限——业务代码要配合查。做硬限(网关拦截、超额 429),需要 Proxy Server,下一章讲。

按 tag 分摊成本

公司有"客服/搜索/内容生成/内部 RPA"几条线,每条线该独立核算。用 metadata.tags 区分、在 callback 里归类即可:

completion(model="gpt-4o",
           messages=msgs,
           user="alice",
           metadata={"tags": ["line:support", "feature:chat", "env:prod"]})
-- 下游分析
SELECT
  tag,
  SUM(cost) AS spend,
  COUNT(*)  AS calls,
  AVG(latency_ms) AS avg_lat
FROM llm_logs, UNNEST(tags) tag
WHERE ts >= '2026-05-01'
GROUP BY tag
ORDER BY spend DESC;

Router 级的预算

Router 上有一个更有用的东西 budget_duration——给某个 deployment 独立设预算:

model_list = [{
    "model_name": "expensive-o3",
    "litellm_params": {"model": "openai/o3", "api_key": "..."},
    "model_info": {
        "base_model": "o3",
    },
    # 这个 deployment 每天最多烧 $50
    "max_budget": 50.0,
    "budget_duration": "1d",
}]

router = Router(model_list=model_list, redis_host="...")

效果:超预算时这个 deployment 自动从路由池里跳过,fallback 接管。比"业务代码自己查额度"稳得多——Redis 共享状态,多实例也准

调用前估算 + 事后校准

一个健康的 LLM 团队,成本管理是两条腿走路:

估算(pre-call)校准(post-call)
工具token_counter + 价格表response_cost + callback
用处选模型、切长文档、拒超长 prompt实际账单、按业务分摊、监控
准确度±5%,够决策精确,等于账单
时机请求发出前请求完成后

生产级监控模板

一个最小但够用的 callback 组合(写 Prometheus + 日志 + 告警):

import time
from prometheus_client import Counter, Histogram

COST  = Counter("llm_cost_usd_total", "LLM cost (USD)", ["model", "user"])
CALLS = Counter("llm_calls_total",   "LLM calls",     ["model", "status"])
LAT   = Histogram("llm_latency_seconds", "LLM latency", ["model"])

def on_success(kwargs, resp, t0, t1):
    model = resp.model
    user = kwargs.get("user", "anon")
    cost = resp._hidden_params.get("response_cost", 0)
    COST.labels(model, user).inc(cost)
    CALLS.labels(model, "ok").inc()
    LAT.labels(model).observe((t1 - t0).total_seconds())

def on_failure(kwargs, exc, t0, t1):
    model = kwargs.get("model", "?")
    CALLS.labels(model, type(exc).__name__).inc()

litellm.success_callback = [on_success]
litellm.failure_callback = [on_failure]

在 Grafana 上做三张图:

常见坑位

  1. 流式忘了 stream_options:cost 永远是 0,以为免费。生产所有流式调用必须 include_usage 或用 stream_chunk_builder。
  2. 价格表过期:pip 版本锁死半年,provider 已经降价 2 次——你还在按老价格核算成本偏高。季度更新 LiteLLM 版本或定期 pull 最新 JSON。
  3. 没打 user 字段:所有调用归到 anonymous,出事了抓不到人。gateway 层强制塞 user
  4. callback 阻塞主流程:同步写 DB,DB 挂了 LLM 也挂。callback 里所有 IO 放 async queue,失败不影响主调用。
  5. 用 GPT-4o 做分类任务:同样准确度 gpt-4o-mini 够用,省 10 倍。用 eval 数据逼自己选便宜模型
  6. tokens 没裁剪就塞 context:用户粘了一整本小说,context 占满 120K,一次 $0.3。入口 token_counter 硬拦
  7. 预算只设月,不设天:前 3 天烧完全月预算,后 27 天服务挂。日限 + 月限双设
  8. cache_hit 的 cost 没归零:部分版本命中缓存 cost 仍返真调用的值,导致月末账单对不上。生产校验 cache_hit=True 时手动把 cost 设 0

本章小结