Chapter 02

统一的 completion() 接口

LiteLLM 最核心的 API 只有一个函数:completion()。理解它的参数约定、model 前缀、返回结构,就理解了这个库的 70%。

安装与第一次调用

LiteLLM 是纯 Python 包,依赖很轻。安装:

pip install litellm
# 或者如果你用 uv (推荐)
uv add litellm

目前主流版本在 1.50+ 以上,API 比较稳定。

准备一把真实的 OpenAI key,直接运行:

import os
from litellm import completion

os.environ["OPENAI_API_KEY"] = "sk-..."

resp = completion(
    model="gpt-4o-mini",
    messages=[{"role": "user", "content": "Hello, who are you?"}],
)
print(resp.choices[0].message.content)

如果你用过 OpenAI SDK,这段代码几乎一模一样——这正是 LiteLLM 的用意:它把调用和返回都伪装成 OpenAI 的样子,你原来怎么写还怎么写。

提示 LiteLLM 不需要 client = OpenAI() 这种"客户端对象"。它是函数式的:from litellm import completion,然后直接调。所有 key 通过环境变量或函数参数传入。这是它和原生 SDK 最明显的风格差异。

model 字符串:provider 前缀的魔法

LiteLLM 能知道"去调哪一家",全靠 model 字符串里的前缀。它有两种命名约定:

约定一:OpenAI 家的裸模型名(无前缀)

OpenAI 是 LiteLLM 的默认 provider,所以 OpenAI 的模型名不带前缀:

completion(model="gpt-4o", ...)
completion(model="gpt-4o-mini", ...)
completion(model="o1-preview", ...)
completion(model="o3-mini", ...)

约定二:其他厂商 provider/model-id

非 OpenAI 一律加前缀,中间一个斜杠:

Provider前缀示例 model 字符串
Anthropic Claudeanthropic/anthropic/claude-sonnet-4-5
Google Geminigemini/gemini/gemini-2.0-flash
Azure OpenAIazure/azure/my-gpt4-deployment
AWS Bedrockbedrock/bedrock/anthropic.claude-3-5-sonnet-20241022-v2:0
Vertex AIvertex_ai/vertex_ai/gemini-2.0-flash
Ollama 本地ollama/ollama/llama3.2
Together AItogether_ai/together_ai/meta-llama/Llama-3-70b
Groqgroq/groq/llama-3.3-70b-versatile
DeepSeekdeepseek/deepseek/deepseek-chat
Moonshotmoonshot/moonshot/moonshot-v1-8k
智谱zhipu/zhipu/glm-4
任意 OpenAI 兼容openai/ + base_urlopenai/qwen-max

注意两个坑:

常见错误 新手经常写成 model="anthropic-claude-sonnet-4-5"model="claude/claude-sonnet-4-5",都会报 BadRequestError: LLM Provider NOT provided。正确的是 斜杠分隔:anthropic/claude-sonnet-4-5

API Key 的三种传入方式

LiteLLM 找 key 的顺序(从高到低优先级):

  1. 函数参数 api_key="..." 显式传入(优先级最高)
  2. 特殊环境变量,比如 OPENAI_API_KEYANTHROPIC_API_KEYGEMINI_API_KEY
  3. 调用 litellm.api_key = "..." 全局设置(不推荐,线程不安全)

生产环境几乎都用第 2 种——环境变量。但有时你需要在同一进程里调多个账号(比如一个客户的 OpenAI key + 另一个客户的 key),这时就用第 1 种:

resp = completion(
    model="gpt-4o-mini",
    messages=[...],
    api_key="sk-tenant-A-key",          # 这次用这把 key
    api_base="https://api.openai.com/v1", # 可选: 自定义 base
)

每家 provider 对应的环境变量名在文档里列得很清楚,日常高频的几个:

OPENAI_API_KEY
OpenAI,同官方 SDK 约定。
ANTHROPIC_API_KEY
Anthropic Claude。
GEMINI_API_KEY
Google AI Studio 的 Gemini(不是 Vertex)。Vertex 用 VERTEXAI_PROJECT + ADC 凭证。
AZURE_API_KEY / AZURE_API_BASE / AZURE_API_VERSION
Azure OpenAI 必须三件套齐全,否则报错。
AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY / AWS_REGION_NAME
Bedrock 走 AWS 签名,标准 IAM 凭证。
DEEPSEEK_API_KEY / GROQ_API_KEY / TOGETHER_API_KEY
各家自取其名,规律是 {PROVIDER}_API_KEY

messages:对话的 "universal container"

LiteLLM 的 messages 永远是 OpenAI 格式的列表:

messages = [
    {"role": "system",    "content": "你是一个严谨的数学家。"},
    {"role": "user",      "content": "1+1 等于几?"},
    {"role": "assistant", "content": "等于 2。"},
    {"role": "user",      "content": "为什么?"},
]

四种 role 全家桶:

system
系统指令/人设,通常只有一条,放在最前面。
user
用户轮,内容可以是字符串,也可以是多模态数组([{type:"text"...}, {type:"image_url"...}])。
assistant
模型上一轮的回复。多轮对话时要把历史回填进来。
tool
工具调用的结果(第 5 章详讲)。

LiteLLM 在底下做的三件事

你看到的是统一的 OpenAI 格式,但 LiteLLM 在发请求前会根据 provider 做翻译。以 system prompt 为例:

Provider原生 system 写法LiteLLM 怎么翻译
OpenAI放在 messages[0],role 为 system原样发送
Anthropic独立字段 system: "...",不能在 messages 里把 role=system 的那条抽出来,填进请求体 system 字段
Gemini独立字段 system_instruction同样抽出来填进 system_instruction
Bedrock Converse独立字段 system: [{text: "..."}]抽出来包装成 list 格式

正是因为有这层翻译,你才能写一次 system 到处跑。但这种翻译也有代价:多 system 不保证都被保留。如果你的 messages 里有两条 role=system(比如为了实验做 prompt stacking),有些 provider 只会接受第一条。跨 provider 代码建议只放一条 system。

多模态:图片与音频

OpenAI 格式里,多模态的 content 是一个数组,每个元素一个 part:

messages = [{
    "role": "user",
    "content": [
        {"type": "text", "text": "这张图里有什么?"},
        {"type": "image_url",
         "image_url": {"url": "https://example.com/cat.jpg"}},
    ],
}]

resp = completion(model="gpt-4o", messages=messages)
print(resp.choices[0].message.content)

同一份 messages,换成 model="anthropic/claude-sonnet-4-5",LiteLLM 会自动把 URL 下载并转成 base64 塞进 Claude 格式(Claude 原生不支持 image URL,只吃 base64 或它家的 files API)。这是第 6 章的重头戏。

核心参数全景

除了 modelmessages,还有一堆常用参数。LiteLLM 把它们按"OpenAI 名字"对齐,不管底层 provider 叫什么:

OpenAI 名字作用底层各家叫什么
temperature随机性 0~2各家都叫 temperature,但范围可能是 0~1
max_tokens最大输出 tokenAnthropic 必填;Gemini 叫 maxOutputTokens;LiteLLM 统一成 max_tokens
top_p核采样大部分通用
stream是否流式各家都有,协议格式不同
stop停止词,可传字符串或列表Anthropic 叫 stop_sequences
n返回几个 candidate很多厂商不支持,传了会忽略
response_format强制 JSON第 6 章详讲
tools函数调用定义第 5 章详讲
seed可复现采样部分厂商支持
user终端用户 ID(审计用)OpenAI/Bedrock 支持
timeout请求超时(秒)LiteLLM 自己实现

厂商专属参数怎么传

有时你必须用某家的独有参数,比如 Gemini 的 topK、Anthropic 的 top_k。LiteLLM 提供 extra_body 直通:

resp = completion(
    model="gemini/gemini-2.0-flash",
    messages=messages,
    temperature=0.3,
    extra_body={"topK": 40},  # 原样塞进请求体
)

这是"抽象层的后门"——当你确实需要厂商特性,又不想绕开 LiteLLM 时的逃生出口。副作用是用了 extra_body 的代码就不再是 provider-agnostic 了,换家会 silently 失效。

drop_params:参数兼容的遮羞布

你可能遇到这种场景:你给 Anthropic 传了 frequency_penalty=0.5,Anthropic 根本不支持这个参数——直接报错。LiteLLM 默认会把不支持的参数丢给 provider,让底层报错;但如果你打开 drop_params,它会静默丢弃不支持的参数。

import litellm
litellm.drop_params = True   # 全局打开

# 或者按调用打开
resp = completion(
    model="anthropic/claude-sonnet-4-5",
    messages=messages,
    frequency_penalty=0.5,   # Anthropic 不支持
    presence_penalty=0.5,    # Anthropic 不支持
    drop_params=True,         # 会被悄悄丢掉, 不报错
)
什么时候该开,什么时候不该开
适合开:你写的是跨 provider 的通用代码,传的参数是"锦上添花",丢了也没事(如 presence_penalty 这种调优参数)。
不适合开:你传的是语义关键参数(如 toolsresponse_format),被丢了业务就崩了——你应该让它报错,而不是拿着个废返回继续跑。

返回对象:ModelResponse 大拆解

不管底层是谁,completion() 返回的都是一个 ModelResponse 对象。它 高度兼容 OpenAI SDK 的返回结构,你可以把它当成 OpenAI 的 ChatCompletion 来用:

resp = completion(model="gpt-4o-mini", messages=messages)

print(type(resp))
# <class 'litellm.types.utils.ModelResponse'>

print(resp)
# 类似:
# ModelResponse(
#   id='chatcmpl-AbC123',
#   choices=[Choices(
#     finish_reason='stop',
#     index=0,
#     message=Message(content='...', role='assistant', tool_calls=None)
#   )],
#   created=1730000000,
#   model='gpt-4o-mini-2024-07-18',
#   object='chat.completion',
#   system_fingerprint='fp_xxx',
#   usage=Usage(completion_tokens=23, prompt_tokens=45, total_tokens=68)
# )

最常用的五个路径

resp.choices[0].message.content
文本内容。注意是 choices[0]——即使只有一个 candidate 也要用下标。
resp.choices[0].finish_reason
为什么停止:stop(自然结束)/ length(达到 max_tokens)/ tool_calls(要调工具)/ content_filter(被审核拦)。
resp.usage.prompt_tokens / completion_tokens / total_tokens
token 用量,跨厂商统一。这对计费和上下文窗口监控非常有用。
resp.choices[0].message.tool_calls
如果 finish_reason 是 tool_calls,这里是函数调用的列表(第 5 章)。没调工具时为 None。
resp._hidden_params["response_cost"]
LiteLLM 给你算好的本次费用(USD)。第 9 章讲价目表怎么维护。

ModelResponse 支持像 dict 一样用

有些老代码风格喜欢下标访问,LiteLLM 的 ModelResponse 同时支持:

# 对象访问
print(resp.choices[0].message.content)

# 字典访问 (兼容 OpenAI v0 风格代码)
print(resp["choices"][0]["message"]["content"])

# 转成 dict
data = resp.model_dump()   # Pydantic 风格
# 或者 resp.json() 拿 JSON 字符串

这就是为什么你用 OpenAI SDK 的旧 Agent 框架换到 LiteLLM 底层,几乎不用改代码——它主动伪装得非常彻底。

换模型的正确姿势

回到第 1 章最后那个 "哇!" 时刻,我们把它改写得更工业一点。看一段"先用 GPT,内部 A/B 看看 Claude 和 Gemini 的表现"的代码:

from litellm import completion
import os, time

# 环境变量通常放在 .env 里, 通过 python-dotenv 加载
os.environ["OPENAI_API_KEY"]    = os.getenv("OPENAI_API_KEY")
os.environ["ANTHROPIC_API_KEY"] = os.getenv("ANTHROPIC_API_KEY")
os.environ["GEMINI_API_KEY"]    = os.getenv("GEMINI_API_KEY")

def classify(text: str, model: str) -> dict:
    """把一段客服工单分到 [bug, feature, billing, other] 四类"""
    resp = completion(
        model=model,
        messages=[
            {"role": "system",
             "content": "只输出四个类别之一: bug/feature/billing/other"},
            {"role": "user", "content": text},
        ],
        temperature=0,
        max_tokens=10,
        timeout=10,
    )
    return {
        "label": resp.choices[0].message.content.strip(),
        "in_tokens":  resp.usage.prompt_tokens,
        "out_tokens": resp.usage.completion_tokens,
        "cost_usd":   resp._hidden_params.get("response_cost", 0),
    }

ticket = "Your app crashed on Android 14 when I opened my profile page"

for m in ["gpt-4o-mini", "anthropic/claude-haiku-4-5", "gemini/gemini-2.0-flash"]:
    t = time.time()
    r = classify(ticket, m)
    r["latency_s"] = round(time.time() - t, 2)
    print(m, r)

输出大致是(数字每次会变):

gpt-4o-mini                       {'label': 'bug', 'in_tokens': 38, 'out_tokens': 1, 'cost_usd': 0.0000059, 'latency_s': 0.53}
anthropic/claude-haiku-4-5        {'label': 'bug', 'in_tokens': 41, 'out_tokens': 1, 'cost_usd': 0.0000105, 'latency_s': 0.71}
gemini/gemini-2.0-flash           {'label': 'bug', 'in_tokens': 40, 'out_tokens': 1, 'cost_usd': 0.0000043, 'latency_s': 0.48}

三家模型、一套业务逻辑,价格-延迟-准确率一眼就能对比。这套对比能力本来要花一周写的胶水代码,现在只要一个循环——这就是 provider 抽象层的直接收益。

同步 vs 异步:acompletion

到这里都是同步代码。如果你在 FastAPI / aiohttp / Celery worker 里,应该用异步版本 acompletion:

import asyncio
from litellm import acompletion

async def main():
    resp = await acompletion(
        model="gpt-4o-mini",
        messages=[{"role": "user", "content": "Hi"}],
    )
    print(resp.choices[0].message.content)

asyncio.run(main())

签名几乎完全一样,只是前面加了个 await。底层是 httpx.AsyncClient 和 provider 自己的 SDK 异步方法。并发时性能差异很大——第 4 章会做详细 benchmark。

本地 mock:不联网测代码

写代码阶段,你不想每 debug 一次就烧几毛钱(或者根本没网)。LiteLLM 有个 mock_response 参数,返回写死的字符串:

resp = completion(
    model="gpt-4o-mini",
    messages=[{"role": "user", "content": "任何东西"}],
    mock_response="这是假返回, 用来测 pipeline 逻辑",
)
print(resp.choices[0].message.content)
# 这是假返回, 用来测 pipeline 逻辑

带 mock 的响应同样有 usage_hidden_params 这些字段(数字是估算的),所以下游处理链不会崩。写单元测试、在 CI 里跑都很爽——见第 11 章的测试章节。

最容易踩的六个坑

  1. model 字符串拼错:claude/claude-sonnet-4-5(错,应该 anthropic/)。错误信息 LLM Provider NOT provided 就是它。
  2. 忘了 max_tokens:Anthropic 的 max_tokens必填的,不传会直接报 BadRequestError。跨 provider 代码建议永远显式传一个值(比如 1024)。
  3. temperature 范围不同:OpenAI 是 0~2,Anthropic 是 0~1。你传 1.5 给 Claude 会被拒。安全值是 0~1。
  4. n > 1 默认不支持:很多厂商不支持多 candidate,要么用 num_retries 多次调用,要么看 provider 文档。
  5. 多 system message:有些 provider 会把多个 system 合并,有些只取第一条。最安全是只放一条。
  6. 使用 SDK 的 client.chat.completions 风格:LiteLLM 是函数式的 completion(...),不是对象方法。如果你非要 OpenAI 对象风格,用下一节讲的"Drop-in replacement"。

Drop-in replacement:你不想改代码怎么办

假设你已经有一份几千行的 OpenAI SDK 代码,全都是 client.chat.completions.create(...) 这种写法,你不想改。LiteLLM 提供了一个"兼容层":

from litellm import OpenAI   # 注意是从 litellm 导入, 不是 openai

client = OpenAI()
resp = client.chat.completions.create(
    model="anthropic/claude-sonnet-4-5",   # 换成 Claude, 只改这一行
    messages=[{"role": "user", "content": "hi"}],
    max_tokens=256,
)
print(resp.choices[0].message.content)

LiteLLM 的 OpenAI 类提供了 OpenAI SDK 的完整方法签名,但底层走 completion()。最少代码改动迁移时非常实用——但要注意这不是 litellm 官方强力推荐的主力写法,它的维护优先级低于 completion()

另一条更彻底的路是用第 10 章要讲的 Proxy 模式:你的代码继续用官方 openai SDK,只是把 base_url 指向你部署的 LiteLLM Proxy。那时就连 import 都不用改。

本章小结