02 — PostgreSQL 价格数据生命周期¶
概述¶
通过 UI 或 API 创建的模型(store_model_in_db=True)将 litellm_params 加密后存入 LiteLLM_ProxyModelTable。LiteLLM 在启动和 30 秒轮询时从 DB 读取并注册到 litellm.model_cost[UUID],供计费使用。
DB 存储结构¶
表:LiteLLM_ProxyModelTable(Prisma schema:proxy/schema.prisma)
| 字段 | 说明 |
|---|---|
model_name |
对外暴露的公开模型名,如 claude-opus-4-6 |
litellm_params |
加密的 JSON 对象,含 model、api_key、api_base、价格字段等 |
model_info |
附加 metadata,如 id、created_by 等 |
litellm_params 的内容示例(解密后):
{
"model": "Vendor2/Claude-4.6-Opus",
"api_key": "sk-xxx",
"api_base": "https://api.gpugeek.com",
"custom_llm_provider": "anthropic",
"input_cost_per_token": 0.000005,
"output_cost_per_token": 0.000025,
"cache_creation_input_token_cost": 0.00000625,
"cache_read_input_token_cost": 5e-7,
"cache_creation_input_token_cost_above_1hr": 0.00001,
"cache_creation_input_token_cost_above_200k_tokens": 0.0000125,
"cache_read_input_token_cost_above_200k_tokens": 0.000001,
"input_cost_per_token_above_200k_tokens": 0.00001,
"output_cost_per_token_above_200k_tokens": 0.0000375
}
加密:model_management_endpoints.py:288-295,使用 encrypt_value_helper() 对每个字段值加密(配置了加密密钥时),字符串值变为 base64-like 密文。
启动时加载流程¶
sequenceDiagram
participant Startup as proxy_startup_event\nproxy_server.py:742
participant BG as initialize_scheduled_background_jobs\nproxy_server.py:5574
participant PC as ProxyConfig.add_deployment\nproxy_server.py:4313
participant DB as PostgreSQL
participant Router as llm_router
participant MC as litellm.model_cost
Startup->>BG: 启动后台任务
BG->>PC: 立即调用 add_deployment\nproxy_server.py:5714
PC->>DB: prisma.litellm_proxymodeltable.find_many()
DB-->>PC: 返回加密的 model 列表
PC->>PC: decrypt_value_helper() 解密每个字段
PC->>PC: LiteLLM_Params(**decrypted_params)
alt llm_router 不存在(首次启动)
PC->>Router: 创建 Router(model_list=models)
Router->>Router: set_model_list()
Router->>Router: _create_deployment() [router.py:6173]
Router->>MC: register_model({UUID: model_info_with_cache_prices})
Router->>MC: register_model({provider/model: model_info_without_custom})
else llm_router 已存在(重新加载)
PC->>Router: ProxyConfig._add_deployment(db_models)\nproxy_server.py:3536
Router->>Router: upsert_deployment(Deployment)\nrouter.py:6744
Router->>Router: _create_deployment() [router.py:6173]
Router->>MC: register_model({UUID: model_info_with_cache_prices})
end
关键调用链(完整):
proxy_startup_event() [proxy_server.py:742]
└─ initialize_scheduled_background_jobs() [proxy_server.py:5574]
└─ proxy_config.add_deployment() [proxy_server.py:5714 立即调用]
└─ ProxyConfig.add_deployment() [proxy_server.py:4313]
├─ _get_models_from_db() [proxy_server.py:4300]
│ └─ prisma.find_many()
└─ _update_llm_router() [proxy_server.py:3616]
├─ _delete_deployment() 删除已从DB移除的模型
└─ ProxyConfig._add_deployment(db_models) [proxy_server.py:3536]
├─ decrypt_value_helper() 解密
├─ LiteLLM_Params(**_litellm_params)
└─ llm_router.upsert_deployment(Deployment)
└─ Router.upsert_deployment() [router.py:6744]
└─ Router.add_deployment() [router.py:6652]
└─ Router._add_deployment() [router.py:6465]
└─ Router._create_deployment() [router.py:6173]
├─ 合并 litellm_params 价格字段到 _model_info
└─ litellm.register_model({UUID: _model_info})
30 秒轮询更新¶
注册位置:proxy_server.py:5699-5711
if store_model_in_db is True:
scheduler.add_job(
proxy_config.add_deployment,
"interval",
seconds=30, # 每 30 秒轮询一次
args=[prisma_client, proxy_logging_obj],
id="add_deployment_job",
)
轮询走的路径与启动时相同,均从 ProxyConfig.add_deployment() 开始。
轮询是否会更新 litellm.model_cost[UUID]¶
会,但需要 litellm_params 发生变化。
upsert_deployment() 的对比逻辑(router.py:6760-6764):
if deployment.litellm_params == _deployment_on_router.litellm_params:
return None # 参数未变化,跳过,不更新 model_cost
# 参数有变化:删除旧 deployment,重新 add_deployment → _create_deployment → register_model
| 场景 | 结果 |
|---|---|
UI 保存后(clear_cache 已立即触发注册) |
轮询时参数相同,跳过 |
| DB 被外部工具直接修改 | 轮询检测到差异,重新注册 UUID |
| 字段未变化 | 永远跳过,model_cost 不变 |
CustomPricingLiteLLMParams 字段完整列表¶
定义:litellm/types/utils.py:2851-2904
这些字段构成 _CUSTOM_PRICING_KEYS frozenset,任意一个字段非 None 即触发 custom_pricing=True(UUID 计费路径)。
| 字段名 | 说明 |
|---|---|
input_cost_per_token |
基础输入 token 价格 |
output_cost_per_token |
基础输出 token 价格 |
cache_creation_input_token_cost |
缓存写入(5min,标准) |
cache_creation_input_token_cost_above_1hr |
缓存写入(1hr) |
cache_creation_input_token_cost_above_200k_tokens |
200k+ token 缓存写入阈值 |
cache_creation_input_audio_token_cost |
音频 token 缓存写入 |
cache_read_input_token_cost |
缓存读取 |
cache_read_input_token_cost_flex |
Flex tier 缓存读取 |
cache_read_input_token_cost_priority |
Priority tier 缓存读取 |
cache_read_input_token_cost_above_200k_tokens |
200k+ token 缓存读取 |
cache_read_input_audio_token_cost |
音频 token 缓存读取 |
input_cost_per_token_above_200k_tokens |
200k+ token 输入阈值 |
output_cost_per_token_above_200k_tokens |
200k+ token 输出阈值 |
input_cost_per_second |
按秒计费输入 |
output_cost_per_second |
按秒计费输出 |
input_cost_per_pixel |
图像输入(像素) |
output_cost_per_pixel |
图像输出(像素) |
input_cost_per_token_flex |
Flex tier 输入 |
input_cost_per_token_priority |
Priority tier 输入 |
output_cost_per_token_flex |
Flex tier 输出 |
output_cost_per_token_priority |
Priority tier 输出 |
input_cost_per_character |
字符计费输入 |
output_cost_per_character |
字符计费输出 |
input_cost_per_audio_token |
音频 token 输入 |
output_cost_per_audio_token |
音频 token 输出 |
input_cost_per_token_batches |
Batch 输入 |
output_cost_per_token_batches |
Batch 输出 |
output_cost_per_reasoning_token |
推理 token |
input_cost_per_query |
按查询计费 |
input_cost_per_image |
图像数量输入 |
output_cost_per_image |
图像数量输出 |
input_cost_per_audio_per_second |
音频秒数输入 |
input_cost_per_video_per_second |
视频秒数输入 |
search_context_cost_per_query |
搜索上下文(dict) |
citation_cost_per_token |
引用 token |
tiered_pricing |
分层定价(list) |
| …(共 49 个字段) |
重要陷阱¶
陷阱 1:清空 UI 字段不会从 DB 删除¶
用户在 UI 编辑时将某个价格字段清空(置为 null),前端发送 PATCH 请求时该字段被 Pydantic exclude_none=True 序列化时排除,后端合并逻辑:
# model_management_endpoints.py(约 180-200 行)
merged = db_model.litellm_params.model_dump(exclude_none=True) # 旧值
merged.update(new_encrypted_params) # 新值覆盖
# 若新值中没有该字段 → 旧值保留
结果:清空 ≠ 删除,DB 中旧值依然保留,use_custom_pricing_for_model() 仍返回 True。
解决方法:显式将字段值设为 0(而非留空),或通过直接操作 DB 删除字段。
陷阱 2:store_model_in_db=False 时无轮询¶
若未开启 store_model_in_db,add_deployment 定时任务不会注册,DB 模型无法被加载。