跳转至

03 — UI 价格展示与修改

概述

LiteLLM 管理 UI 展示的价格信息来自两个来源的合并:DB 中存储的自定义价格(优先)和 JSON 文件中的标准价格(补充)。理解这两个来源的优先级对于正确配置价格至关重要。


/model/info 端点数据来源

端点GET /model/infoGET /v1/model/info 定义litellm/proxy/proxy_server.py:9914-10055model_info_v1 函数)

数据组装流程

flowchart TD
    A[GET /model/info] --> B[llm_router.get_model_list\n从 router.model_list 内存取]
    B --> C[_get_proxy_model_info model\nproxy_server.py:9871]
    C --> D[model_info = model.get 'model_info' \n来自 DB/config 原始值]
    C --> E[litellm_model_info = get_litellm_model_info model \n调用 litellm.get_model_info\n即查 litellm.model_cost]
    E --> F{litellm_model_info 为空?}
    F -- 是 --> G[fallback: 用 litellm_params.model 再查]
    G --> H{仍为空?}
    H -- 是 --> I[fallback: 取 model 最后一个 / 段查]
    H -- 否 --> J
    F -- 否 --> J[for k,v in litellm_model_info:\n  if k not in model_info:\n    model_info k = v]
    I --> J
    J --> K[model 'model_info' = model_info\n注入到响应]
    K --> L[model 'litellm_params' 保持原样\n不注入 JSON 价格]

关键代码proxy_server.py:9902-9905):

for k, v in litellm_model_info.items():
    if k not in model_info:
        model_info[k] = v   # JSON 价格只补充缺失字段,不覆盖 DB 中已有的值
model["model_info"] = model_info
# model["litellm_params"] 不做任何修改

UI 各字段来源对照表

UI 显示区域 字段来源 覆盖规则
Model Info → Input/Output 价格 model_info.input_cost_per_token DB 值优先,JSON 补充缺失字段
Model Info → cache 价格 model_info.cache_creation_input_token_cost 同上
LiteLLM Params → input_cost_per_token litellm_params.input_cost_per_token 纯 DB 原始值,无 JSON 注入
LiteLLM Params → cache_creation_input_token_cost litellm_params.cache_creation_input_token_cost 纯 DB 原始值

结论: - Model Info 区域:可能混合了 DB 值和 JSON 值(DB 优先) - LiteLLM Params 区域:完全来自 DB,是计费的实际数据源


前端字段优先级逻辑

编辑表单初始值设置(model_info_view.tsx:595-600):

// 价格字段展示:litellm_params 中有值则用,否则 fallback 到 model_info
input_cost: localModelData.litellm_params.input_cost_per_token
  ? localModelData.litellm_params.input_cost_per_token * 1_000_000
  : localModelData.model_info?.input_cost_per_token * 1_000_000 || null,

output_cost: localModelData.litellm_params?.output_cost_per_token
  ? localModelData.litellm_params.output_cost_per_token * 1_000_000
  : localModelData.model_info?.output_cost_per_token * 1_000_000 || null,

前端显示单位转换:UI 以 "\(/1M tokens" 显示,内部存储为 "\)/token"(相差 1,000,000 倍)。


编辑保存的完整流程

API 端点

前端调用networking.tsx:3849-3882):

const url = `${proxyBaseUrl}/model/${modelId}/update`;
const response = await fetch(url, {
    method: "PATCH",
    body: JSON.stringify(formValues),
});

后端端点PATCH /model/{model_id}/update 定义litellm/proxy/management_endpoints/model_management_endpoints.py:145-272update_db_model

保存流程图

sequenceDiagram
    participant UI as 前端 UI
    participant API as PATCH /model/id/update
    participant DB as PostgreSQL
    participant Router as llm_router
    participant MC as litellm.model_cost

    UI->>UI: input_cost / 1_000_000 → input_cost_per_token
    UI->>API: PATCH { litellm_params: { input_cost_per_token, cache_creation_... } }
    API->>DB: 读取旧 litellm_params(已加密)
    API->>API: 解密旧值 → merged = old_params
    API->>API: 加密新值
    API->>API: merged.update(new_encrypted_params)
    API->>DB: 写回 merged litellm_params
    API->>API: clear_cache()
    Note over API: 立即触发重载,不等30s
    API->>Router: 删除所有 db_model=True 的 deployments
    API->>Router: proxy_config.add_deployment()
    Router->>Router: _create_deployment()
    Router->>MC: register_model UUID → model_info 含新价格
    API-->>UI: 200 OK

clear_cache() 的行为

定义model_management_endpoints.py:1315-1368

async def clear_cache():
    # 1. 删除 router 中所有 db_model=True 的 deployment
    for m in db_model_list:
        llm_router.delete_deployment(model_id=m.model_info.id)
    # 2. 立即重载
    await proxy_config.add_deployment(prisma_client, proxy_logging_obj)

效果:UI 保存后立即生效litellm.model_cost[UUID] 立刻更新,无需等待 30 秒轮询。


前端表单字段清单与 cache 字段的输入通道

原生表单字段

ui/litellm-dashboard/src/components/model_info_view.tsxForm.Item name= 的全部字段(编辑模型对话框):

Form.Item name 行号 对应 litellm_params 字段
model_name 621
litellm_model_name 632 model
input_cost 643 input_cost_per_token(×1/1_000_000)
output_cost 660 output_cost_per_token(×1/1_000_000)
api_base 677 api_base
custom_llm_provider 690 custom_llm_provider
organization 703 organization
tpm / rpm 716 / 729 tpm / rpm
max_retries / timeout / stream_timeout 742 / 755 / 768 同名
model_access_group 781 model_info.access_groups
guardrails 840 guardrails
tags 889 tags
health_check_model 938 model_info.health_check_model
model_info 1004 整个 model_info JSON 文本框
litellm_extra_params 1034 整个 litellm_params 补充 JSON 文本框

价格字段:仅 input_cost / output_cost 两个原生输入框,对应 input_cost_per_token / output_cost_per_token

cache 价格字段的输入路径

所有 cache 相关字段(cache_creation_input_token_costcache_read_input_token_costcache_creation_input_token_cost_above_1hr 等)通过 LiteLLM Params JSON 文本框litellm_extra_params,行 1034)以 JSON 形式输入:

{
  "cache_creation_input_token_cost": 0.00000625,
  "cache_read_input_token_cost": 0.0000005,
  "cache_creation_input_token_cost_above_1hr": 0.00001
}

前端合并逻辑

handleModelUpdatemodel_info_view.tsx:229-244):

let updatedLitellmParams = {
  ...values.litellm_params,
  ...parsedExtraParams,                                    // ← JSON 文本框解析结果
  model: values.litellm_model_name,
  api_base: values.api_base,
  custom_llm_provider: values.custom_llm_provider,
  organization: values.organization,
  tpm: values.tpm,
  rpm: values.rpm,
  max_retries: values.max_retries,
  timeout: values.timeout,
  stream_timeout: values.stream_timeout,
  input_cost_per_token: values.input_cost / 1_000_000,
  output_cost_per_token: values.output_cost / 1_000_000,
  tags: values.tags,
};

parsedExtraParams 中的 cache 字段位于展开链第二位,后续显式 key 集合中不含任何 cache_* 字段,所以 JSON 文本框输入的 cache 字段会保留到 PATCH 请求体中。

后端字段类型保障

PATCH 端点通过 updateLiteLLMParams Pydantic 模型接收请求(model_management_endpoints.py:105-112):

encrypted_params = {
    k: encrypt_value_helper(v)
    for k, v in updated_patch.litellm_params.model_dump(
        exclude_none=True
    ).items()
}
merged_deployment_dict["litellm_params"].update(encrypted_params)

类型链:

updateLiteLLMParams (types/router.py:371)
  → GenericLiteLLMParams (types/router.py:165)
       ├─ CredentialLiteLLMParams
       └─ CustomPricingLiteLLMParams  ← 声明全部 49 个价格字段(含 cache 字段)

  model_config = ConfigDict(extra="allow", ...)  (types/router.py:194)

两项关键保障: 1. cache 字段是 CustomPricingLiteLLMParams 声明字段,非 None 值经 model_dump(exclude_none=True) 后会正常序列化 2. extra="allow" 确保未声明字段(如自定义扩展)也会保留

因此 JSON 文本框中输入的 cache 字段能完整经过 Pydantic 反序列化与 model_dump 写入 DB。

encrypt_value_helper 对非字符串的处理

加密工具(encrypt_decrypt_utils.py:19-36):

def encrypt_value_helper(value: str, new_encryption_key: Optional[str] = None):
    signing_key = new_encryption_key or _get_salt_key()
    if isinstance(value, str):
        encrypted_value = encrypt_value(value=value, signing_key=signing_key)
        return base64.urlsafe_b64encode(encrypted_value).decode("utf-8")
    return value   # float / int 原样返回

价格字段(float)不会被加密,直接以原值写入 DB 的 litellm_params JSON。因此 SQL 直查结果里 cache_creation_input_token_cost: 0.00000625 是可读的明文数字。


清空字段的陷阱

问题

用户在 UI 将某个价格字段清空并保存:

前端发送 PATCH:
{ "litellm_params": { "input_cost_per_token": null } }

Pydantic exclude_none=True 序列化后:
{ "litellm_params": {} }  ← 字段消失

后端 merge 逻辑:
merged = old_params  ← 包含旧 input_cost_per_token
merged.update({})   ← 空 dict,不覆盖任何字段

DB 结果:旧值保留!

后果

  • DB 中 input_cost_per_token 仍然存在
  • use_custom_pricing_for_model() 仍返回 True
  • litellm.model_cost[UUID] 的 cache 价格字段(如果之前就缺失)仍然缺失

解决方法

将字段值显式设为 0(而非留空),或直接通过 DB 操作删除 JSON 中的字段:

UPDATE "LiteLLM_ProxyModelTable"
SET litellm_params = litellm_params - 'input_cost_per_token'
WHERE model_name = 'claude-opus-4-6';

完整字段写入验证

可通过以下 SQL 验证 DB 中实际存储的字段(加密版本):

SELECT model_name, litellm_params
FROM "LiteLLM_ProxyModelTable"
WHERE model_name = 'claude-opus-4-6';

litellm_params 中有价格字段(无论是否加密),即说明走 UUID 计费路径。字段值是 base64-like 密文是正常的(由 encrypt_value_helper 加密)。