ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

LiteLLM 网关 Token 成本归因实战:每笔 API 调用都能查到谁花的钱

LiteLLM 网关 Token 成本归因实战:每笔 API 调用都能查到谁花的钱 1. 多租户网关里Token 成本归因到底难在哪LiteLLM 网关做统一入口之后最容易被忽略的一件事就是调用量上来了钱却说不清是谁花的。LiteLLM 是一个把多家模型 API 收敛成 OpenAI 兼容接口的代理网关支持虚拟 Key、预算限额、消费日志落库适合团队内部做多租户的模型调用管理。它本身自带LiteLLM_SpendLogs表和一套 UI 账单但很多人第一次对账就会发现数字对不上——不是网关算错了而是读账本的姿势不对。我这边网关跑了一段时间后需求很朴素每笔 API 调用都要能回答三个问题——谁在调、调的什么模型、花了多少钱。听起来像 UI 里点两下的事实际做下来要处理请求头注入、日志落库、按 Key 和用户维度聚合还要把 endpoint 统一到一个稳定的 API 通道上才能让归因链路复用。这篇就把这套链路拆开讲从配置到回调钩子到验证动作一步步给可复制的片段。先说清楚归因的核心机制不然后面全是坑。LiteLLM 的计费归属看的是 Key 所属的user_id不是key_alias。key_alias只是个给人看的标签写什么都行不参与计费。很多团队是管理员统一代建 Keykey_alias按人名写但user_id全是管理员本人于是系统眼里全公司的消费都挂在管理员头上。这是第一个必须纠正的认知。第二个机制是 Key 的存储方式。LiteLLM 对 Key token 做 SHA-256 hash 存储LiteLLM_VerificationToken.token字段存的是 64 位 hex 的 hash 值不是明文。你拿配置里的sk-xxx去等值匹配永远查不到。SpendLogs.api_key存的也是 hash 前缀大约 12 位UI 上显示的 Key ID 就是这个前缀。所以查账要用前缀匹配不是明文等值。第三个机制是删除行为。VerificationToken没有软删除字段删 Key 是硬删除归属信息一并蒸发但SpendLogs里的调用记录还在只是 JOIN 不上 Key 表了。这意味着离职人员的 Key 一删历史账就成了孤儿记录得靠LEFT JOIN反查。把这三个机制记住后面的配置和 SQL 才有意义。归因链路的目标就是请求进来时带上用户标识落库时把标识写进日志聚合时按 Key 前缀和用户维度还原账单。下面从环境准备开始。2. 前置准备TaoToken 统一 Key 与 LiteLLM 接入在讲归因配置之前先把上游通道定下来。归因链路要复用前提是上游 endpoint 稳定、Key 统一管理。我这边把 LiteLLM 的上游 endpoint 改到 TaoToken 的统一 Key/API 通道这样所有下游应用RAG 知识库、Claude Code、内部 agent 平台都走同一个出口归因只需要在 LiteLLM 这一层做不用每个应用各写一套。TaoToken 的官网入口是 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 基址是 https://taotoken.net/api 。注意 API 地址不带 UTM 参数配置里写干净的基址就行。控制台和 Key 管理在 https://taotoken.net/console API Keys 页面在 https://taotoken.net/api-keys 接入文档在 https://taotoken.net/doc 。如果你要验证模型是否可用可以用模型对话页面 https://taotoken.net/models 先跑一条请求如果是长期编码或 Agent 场景Coding Plan 页面在 https://taotoken.net/coding-plan 。前置准备分三步。第一步在 TaoToken 控制台创建一把统一 Key记下 Key 值。第二步确认 LiteLLM 的数据库后端是 Postgres因为归因依赖SpendLogs和VerificationToken两张表SQLite 也能跑但生产环境建议 Postgres。第三步确认 LiteLLM 版本v1.86.0存在 UI 日期筛选翻倍的已知问题对账时要注意后面排障章节会细讲。环境变量建议这样组织把上游 Key 和数据库连接分开export TAOTOKEN_API_KEYsk-你的统一Key export DATABASE_URLpostgresql://litellm:password127.0.0.1:5432/litellm export LITELLM_MASTER_KEYsk-你的master-key export LITELLM_SALT_KEYsk-你的salt-keyLITELLM_SALT_KEY很关键它决定 Key 的 hash 方式。如果你中途换了 salt历史 Key 的 hash 前缀就对不上了归因会断档。所以这个值一旦定下来就别改。接下来是 LiteLLM 的config.yaml。归因相关的配置集中在general_settings、litellm_settings和model_list三块。model_list里把上游指向 TaoToken 的 API 基址general_settings里开启数据库和日志litellm_settings里挂回调钩子。下面给一份可复制的完整配置。3. 可复制配置config.yaml 归因链路这份config.yaml是归因链路的核心路径按你实际部署位置调整我这边放在/etc/litellm/config.yaml。配置里包含上游模型定义、数据库落库、回调钩子和请求头透传四部分。model_list: - model_name: claude-sonnet-4-6 litellm_params: model: anthropic/claude-sonnet-4-6 api_base: https://taotoken.net/api api_key: os.environ/TAOTOKEN_API_KEY - model_name: gpt-4o-mini litellm_params: model: openai/gpt-4o-mini api_base: https://taotoken.net/api api_key: os.environ/TAOTOKEN_API_KEY - model_name: deepseek-chat litellm_params: model: deepseek/deepseek-chat api_base: https://taotoken.net/api api_key: os.environ/TAOTOKEN_API_KEY general_settings: master_key: os.environ/LITELLM_MASTER_KEY database_url: os.environ/DATABASE_URL salt_key: os.environ/LITELLM_SALT_KEY store_model_in_db: true store_prompts_in_spend_logs: true forward_client_headers_to_llm_api: true litellm_settings: drop_params: true set_verbose: false success_callback: [custom_attribution] failure_callback: [custom_attribution] callbacks: [custom_attribution] callback_settings: custom_attribution: module_path: callbacks.attribution function_name: log_attribution几个参数要解释清楚。store_prompts_in_spend_logs: true会把 prompt 和 completion 的 token 数写进SpendLogs.metadata归因时能按 token 维度拆。forward_client_headers_to_llm_api: true让客户端请求头透传到上游但归因真正依赖的是 LiteLLM 自己记录的user字段和metadata。success_callback和failure_callback都挂上自定义回调成功和失败的调用都要落库否则失败请求的 token 消耗会漏账。callback_settings里的module_path指向你的回调模块。这个模块要放在 LiteLLM 能 import 到的路径下我这边放在项目根目录的callbacks/attribution.py。下面给回调钩子的代码。# callbacks/attribution.py import json import logging from typing import Any, Dict, Optional logger logging.getLogger(litellm.attribution) def log_attribution( kwargs: Dict[str, Any], response_obj: Optional[Any], start_time: Any, end_time: Any, ) - None: LiteLLM 自定义回调把用户标识、Key 前缀、模型、token 数写入日志。 成功和失败都会触发失败时 response_obj 为 None。 try: metadata kwargs.get(litellm_params, {}).get(metadata, {}) or {} user_id metadata.get(user_api_key_user_id) or kwargs.get(user) or unknown key_alias metadata.get(user_api_key_alias) or no-alias key_hash metadata.get(user_api_key_hash) or key_prefix key_hash[:12] if key_hash else no-key model kwargs.get(model) or unknown call_type kwargs.get(call_type) or completion usage {} if response_obj is not None: usage getattr(response_obj, usage, None) or {} if not isinstance(usage, dict): usage usage.model_dump() if hasattr(usage, model_dump) else {} record { user_id: user_id, key_alias: key_alias, key_prefix: key_prefix, model: model, call_type: call_type, prompt_tokens: usage.get(prompt_tokens, 0), completion_tokens: usage.get(completion_tokens, 0), total_tokens: usage.get(total_tokens, 0), } logger.info(attribution_record%s, json.dumps(record, ensure_asciiFalse)) except Exception as exc: logger.warning(attribution callback failed: %s, exc)这个回调的作用是把归因需要的字段从metadata里抽出来打成一条结构化日志。user_api_key_user_id是 LiteLLM 在鉴权时注入的user_api_key_alias是 Key 的别名user_api_key_hash是 Key 的 hash取前 12 位就是 UI 上显示的 Key ID。注意metadata的字段名在不同版本可能略有差异如果取不到值先打印整个metadata看实际结构。配置和回调都就位后启动 LiteLLMlitellm --config /etc/litellm/config.yaml --port 4000 --detailed_debug启动日志里如果看到custom_attribution注册成功说明回调挂上了。接下来要验证归因链路是否真的把用户标识写进了SpendLogs。4. 验证请求多用户调用后核对账单归属验证分两步先发起多用户调用再查库核对归属。这一步是整个归因链路的验收动作不能跳过。先创建两个用户和两把 Key模拟多租户场景。用 master key 调管理接口# 创建用户 alice curl -X POST http://127.0.0.1:4000/user/new \ -H Authorization: Bearer $LITELLM_MASTER_KEY \ -H Content-Type: application/json \ -d {user_email: aliceexample.com, user_alias: alice} # 创建用户 bob curl -X POST http://127.0.0.1:4000/user/new \ -H Authorization: Bearer $LITELLM_MASTER_KEY \ -H Content-Type: application/json \ -d {user_email: bobexample.com, user_alias: bob}拿到两个user_id后分别给 alice 和 bob 生成 Key注意user_id要填实际使用者不要都填管理员# 给 alice 生成 Key curl -X POST http://127.0.0.1:4000/key/generate \ -H Authorization: Bearer $LITELLM_MASTER_KEY \ -H Content-Type: application/json \ -d {user_id: alice-user-id, key_alias: alice-key, models: [claude-sonnet-4-6]} # 给 bob 生成 Key curl -X POST http://127.0.0.1:4000/key/generate \ -H Authorization: Bearer $LITELLM_MASTER_KEY \ -H Content-Type: application/json \ -d {user_id: bob-user-id, key_alias: bob-key, models: [gpt-4o-mini]}然后用各自的 Key 发起调用。alice 调 claudebob 调 gpt-4o-mini# alice 调用 curl -X POST http://127.0.0.1:4000/v1/chat/completions \ -H Authorization: Bearer sk-alice-key \ -H Content-Type: application/json \ -d {model: claude-sonnet-4-6, messages: [{role: user, content: 用一句话解释什么是网关}]} # bob 调用 curl -X POST http://127.0.0.1:4000/v1/chat/completions \ -H Authorization: Bearer sk-bob-key \ -H Content-Type: application/json \ -d {model: gpt-4o-mini, messages: [{role: user, content: 用一句话解释什么是成本归因}]}调用成功后查SpendLogs核对归属。这条 SQL 按 Key 前缀和用户维度聚合是归因的核心查询SELECT LEFT(api_key, 12) AS key_prefix, user AS user_id, model, COUNT(*) AS calls, ROUND(SUM(spend)::numeric, 4) AS spend, SUM((metadata::json-prompt_tokens)::int) AS prompt_tokens, SUM((metadata::json-completion_tokens)::int) AS completion_tokens FROM LiteLLM_SpendLogs WHERE startTime NOW() - INTERVAL 1 hour GROUP BY LEFT(api_key, 12), user, model ORDER BY spend DESC;预期结果是 alice 的 Key 前缀对应 claude 模型bob 的 Key 前缀对应 gpt-4o-miniuser_id分别是 alice 和 bob 的user_id。如果user字段是空的或者全是管理员说明 Key 生成时user_id填错了回到第 1 节的机制计费归属看user_id不看key_alias。再验证一下回调日志有没有落。看 LiteLLM 进程日志里有没有attribution_record开头的行grep attribution_record /var/log/litellm/litellm.log | tail -5如果日志里有记录但SpendLogs里user字段为空说明回调拿到了数据但 LiteLLM 自己的落库没写user这时候要检查store_prompts_in_spend_logs和forward_client_headers_to_llm_api是否都开了。两个都开了还不行就升级 LiteLLM 版本早期版本对user字段的写入有差异。验证通过后归因链路就算跑通了。接下来是排障这部分是实战里最容易卡住的地方。5. 常见报错排查401、local proxy failed 与 OAuth归因链路跑起来之后报错主要集中在鉴权、代理和 OAuth 三类。下面按真实报错对照排查。第一类401 Unauthorized。这个报错在归因场景下通常不是 Key 错了而是 Key 的user_id和请求里的user字段冲突。LiteLLM 鉴权时会用 Key 绑定的user_id覆盖请求体里的user如果你在请求里手动传了user字段可能被忽略。排查动作查VerificationToken表确认 Key 绑定的user_idSELECT token, user_id, key_alias, models, spend FROM LiteLLM_VerificationToken WHERE LEFT(token, 12) 你的key前缀;如果user_id是管理员说明 Key 生成时填错了重新生成一把绑定实际使用者的 Key。注意token字段是 hash用前缀匹配别拿明文查。第二类local proxy failed或APIConnectionError。这个报错说明 LiteLLM 到上游的请求失败了。归因场景下常见原因是api_base配错比如把 TaoToken 的 API 基址写成了带路径的地址。正确写法是https://taotoken.net/api不要带/v1后缀LiteLLM 会自己拼。排查动作用 curl 直接测上游连通性curl -X POST https://taotoken.net/api/v1/chat/completions \ -H Authorization: Bearer $TAOTOKEN_API_KEY \ -H Content-Type: application/json \ -d {model: gpt-4o-mini, messages: [{role: user, content: ping}]}如果这条通说明上游没问题问题在 LiteLLM 配置如果不通检查 Key 和网络。注意local proxy failed有时是 LiteLLM 内部代理超时调大request_timeout参数。第三类OAuth相关报错比如OAuth token expired或invalid_client。这类报错出现在用 OAuth 方式接入上游时。如果你走的是 TaoToken 的统一 Key一般不会遇到 OAuth但如果下游应用比如 Claude Code用 OAuth 方式连 LiteLLM就要检查 LiteLLM 的 OAuth 配置。排查动作确认 LiteLLM 的general_settings里有没有开oauth2_config没开的话下游 OAuth 请求会被拒。归因场景下建议统一用 Key 鉴权OAuth 的 token 刷新会干扰归因字段的注入。第四类reading choices报错完整信息类似Error reading choices from response。这个报错说明上游返回的结构和 LiteLLM 预期的不一致。常见于上游返回了错误 JSON 但 HTTP 状态是 200。排查动作开--detailed_debug看原始响应确认上游返回体里有没有choices字段。如果是 TaoToken 通道正常返回是 OpenAI 兼容格式有choices如果没有检查model名是否拼错比如把anthropic/claude-sonnet-4-6写成了claude-sonnet-4-6而没在model_list里注册。第五类UI 日期筛选数字翻倍。这是 LiteLLMv1.86.0的已知问题根因是DailyUserSpend聚合表按 UTC 整日分桶前端把浏览器时区偏移传给后端东八区查询起始日期往前扩了一天查一天变成查两个整日桶。排查动作用 SQL 直查SpendLogs对比 UI 数字SELECT 本地日 AS scope, COUNT(*) AS calls, ROUND(SUM(spend)::numeric, 2) AS spend FROM LiteLLM_SpendLogs WHERE startTime 2026-08-20 00:00:00 AND startTime 2026-08-21 00:00:00 UNION ALL SELECT UTC日, COUNT(*), ROUND(SUM(spend)::numeric, 2) FROM LiteLLM_SpendLogs WHERE startTime 2026-08-20 08:00:00 AND startTime 2026-08-21 08:00:00;处理建议是对账一律 SQL 直查SpendLogs别信 UI 日期筛选。长期方案是升级到含修复的版本但每日分桶仍是 UTC 口径东八区看日报仍有 8 小时错位。不建议改容器 TZ 硬扛分桶逻辑写死了 UTC改了反而让新旧数据口径断档。第六类孤儿记录查不到归属。Key 删了之后SpendLogs里的记录 JOIN 不上VerificationToken用LEFT JOIN反查SELECT LEFT(sl.api_key, 12) AS key_prefix, MAX(sl.user) AS user_id, COUNT(*) AS call_count, ROUND(SUM(sl.spend)::numeric, 2) AS total_spend, MIN(sl.startTime) AS first_call, MAX(sl.startTime) AS last_call FROM LiteLLM_SpendLogs sl LEFT JOIN LiteLLM_VerificationToken vt ON sl.api_key vt.token WHERE vt.token IS NULL GROUP BY sl.api_key ORDER BY total_spend DESC;vt.token IS NULL的就是已删除 Key 的历史消费sl.user字段里还留着调用时提交的用户标识多数情况能救回来。血泪教训是删 Key 之前先把 Key 前缀、key_alias、归属人记档一张内部表格的事别等对账时抓瞎。排障做完归因链路基本稳定了。最后把 CTA 分流说清楚方便你按场景选入口。6. 归因链路复用与入口选择归因链路跑通之后复用是关键。我这边把 LiteLLM 的上游 endpoint 统一到 TaoToken 的 API 通道所有下游应用走同一个出口归因只需要在 LiteLLM 这一层做。这样新增应用时不用改归因代码只要在 LiteLLM 里生成一把绑定实际使用者的 Key 就行。如果你在排障或接入阶段卡住了先看 API Keys 页面 https://taotoken.net/api-keys 确认 Key 状态再看接入文档 https://taotoken.net/doc 对照配置。文档里有完整的config.yaml示例和回调写法比对着改能省不少时间。如果你要验证某个模型是否可用或者想先跑一条请求确认通道没问题用模型对话页面 https://taotoken.net/models 直接发一条消息看返回不用先配 LiteLLM。这一步能快速排除上游问题。如果你是长期编码或 Agent 场景调用量大、需要预算控制看 Coding Plan 页面 https://taotoken.net/coding-plan 里面有按量或包月的方案说明。归因链路和 Coding Plan 不冲突LiteLLM 这一层的归因照做上游走哪个通道不影响SpendLogs的落库。最后沉淀三条团队规范供参考。Key 谁用谁建管理员代建的必须把user_id改成实际使用者key_alias该写还得写但别拿它对账。删 Key 前先导出该 Key 的消费明细和前缀存档避免孤儿记录。对账和日报一律以SpendLogs直查为准不依赖 UI 筛选时间字段是startTime大写 S 带双引号小写会报列不存在。把这几件事做到位每笔 API 调用都能查到谁花的钱。归因链路本身不复杂复杂的是那些机制细节——key_alias不参与计费、token 是 hash 存的、删 Key 是硬删除、代理后面的 IP 不可信、UI 日期筛选有 bug。这些坑踩过一遍账本就读明白了。
返回列表