快照时间:2026-10-22 | 系统:Debian GNU/Linux 12 (bookworm)、x86_64 VPS 链路:客户端 → Cloudflare → Caddy → LiteLLM → Groq / ChatGPT 说明:不含任何密钥、连接串或密码,只记变量名;文中
<...>为脱敏占位符。
LiteLLM 启动后只监听 127.0.0.1:4000。
这个地址在服务器外面没有任何意义。它只保证一件事:没有人能从公网直接碰到模型网关。
真正让它变成一个可调用 API 的,是 Caddy 的两行配置,加上 Cloudflare 那边的一条 A 记录。
从客户端发出一次请求,到 Groq 的回复返回,中间要走过四跳。这篇记录的是这四跳怎么配通、怎么验证,以及跑起来之后那张运维台账上还没兑现的几行。
0. 起点:从「能跑」到「说得清」
素材是一份 429 行的运维笔记,由部署验证记录与运维快照合并而成。合并时做过一次剪辑——只保留已验证配置,不含排错阶段的错误配置。
这句话值得停一下。一份自觉做过剪辑的笔记,它的空白并不等于没发生,只是没被写进来。
第 003 期写过从零部署的完整流程。这一期不复述安装步骤,只讲三件事:现在配的是什么、为什么这样配、怎么验证它确实在按预期工作。
读状态时用一个四档的尺子:
- A 已实测:有命令输出或返回码,可以复读。
- B 可复读:数据在快照里,但没有复测过。
- C 已配置未验证:配置写下了,效果没验。
- D 无证据:有现象,没有解释。
纪律只有一条:状态词后面必须能指到一个证据位置。写「正常」不够,要能回答它是 A 还是 C。
1. 架构:一次调用要走过四跳
客户端 ──HTTPS 443──▶ Caddy ┄┄▶ LiteLLM(127.0.0.1:4000,Docker,host 网络)
┄┄ 虚线 = 部署后未再核对 ├─▶ Groq GROQ_API_KEY
├─▶ ChatGPT Codex OAuth 令牌(chatgpt/auth.json)
└─▶ Neon Postgres DATABASE_URL
四跳分别是:
- 客户端把请求发到
https://llm.example.com/v1/chat/completions,DNS 解析到 Cloudflare; - Cloudflare 转发到 VPS 的 443,Caddy 接住并完成 TLS 终止;
- Caddy 按域名规则,把请求转给
127.0.0.1:4000; - LiteLLM 按
config.yaml里的model_name选中上游,用对应的密钥发出去。
第二跳和第三跳之间是这台机器上唯一的公网入口。也就是说,Caddy 配置的那几行,决定了整个网关对外暴露什么。
图里那条虚线不是排版选择。实线表示部署当时验证过,虚线表示从那以后没人再看过——它对应后面要说的 P0-4。
2. LiteLLM 配置:config.yaml 里定义了什么
所有文件都在 /opt/litellm:
mkdir -p /opt/litellm && cd /opt/litellm
/opt/litellm/
├── docker-compose.yml
├── config.yaml
└── .env
配置文件 config.yaml 只读挂载进容器。它只有三层结构:
model_list:
- model_name: groq-gpt-oss-120b # 对外暴露的名字,客户端用它调用
litellm_params:
model: groq/openai/gpt-oss-120b # 上游真实模型 ID
api_key: "os.environ/GROQ_API_KEY" # 从环境变量取,不落明文
# groq-gpt-oss-20b / groq-qwen3.8-27b / groq-qwen3.6-27b /
# groq-compound / groq-compound-mini 同构,共 6 个
model_name 是给客户端看的,litellm_params.model 是给上游看的。两者可以完全不同——这正是这层网关的价值:换供应商时改下面一行,客户端的调用代码不动。
当前挂了 8 个模型,分两类:
| 来源 | 模型名 | 说明 |
|---|---|---|
| Groq | groq-gpt-oss-120b、groq-gpt-oss-20b、groq-qwen3.8-27b、groq-qwen3.6-27b、groq-compound、groq-compound-mini |
6 个,用 GROQ_API_KEY |
| ChatGPT Codex | chatgpt/gpt-6.1-sol、chatgpt/gpt-6-luna |
2 个,用 OAuth 令牌,调用时须写 chatgpt/ 前缀全名 |
api_key 这一行有个容易踩的坑。验证通过的写法只有一种:
api_key: "os.environ/GROQ_API_KEY"
引号不能去,os.environ/ 前缀不能省。改成裸写 os.environ/GROQ_API_KEY(去掉引号)或直接填入密钥,都可能出现 Invalid API Key。这不是风格问题——YAML 在没有引号时对某些字符的处理与 LiteLLM 的解析方式叠加,会让这个值不再是它看起来的样子。
密钥本身放在同一个目录的 .env 里,权限 600。三个变量:
GROQ_API_KEY:Groq 的 Key;LITELLM_MASTER_KEY:自设管理密钥,必须以sk-开头;DATABASE_URL:Neon 直连串,主机名不含-pooler,带sslmode=require。
docker-compose.yml 全文如下:
services:
litellm:
image: ghcr.io/berriai/litellm:main-latest
container_name: litellm
restart: unless-stopped
environment:
- CHATGPT_TOKEN_DIR=/tokens/chatgpt
env_file:
- .env
volumes:
- ./config.yaml:/app/config.yaml:ro
- ./chatgpt:/tokens/chatgpt
network_mode: host
mem_limit: 1200m
command:
- "--config"
- "/app/config.yaml"
- "--host"
- "127.0.0.1"
其中有四处值得单独说:
network_mode: host:容器直接用宿主机网络,所以不需要 ports: 映射。代价是端口直接开在宿主上,好处是 --host 127.0.0.1 能直接生效。
--host 127.0.0.1:让 LiteLLM 只绑定回环地址。部署基线没有这个参数,那时它监听宿主机全部接口——这是后来加的加固。
mem_limit: 1200m:限制容器内存。与它相关的还有一段崩溃史,放在第 6 节。
./chatgpt 不能加 :ro:这个目录里放的是 OAuth 令牌,约 10 天会刷新一次,刷新时要写回 auth.json。挂成只读,第一次刷新就会失败。
改完配置怎么生效,取决于改的是哪个文件:
| 改了什么 | 命令 |
|---|---|
只改了 config.yaml |
docker restart litellm |
改了 .env 或 docker-compose.yml |
docker compose up -d |
docker restart 不会重新读取 .env 与 compose 文件。这条规则看起来琐碎,但它是最常见的「改了没生效」的原因。
三个文件齐了就启动:
cd /opt/litellm
docker compose up -d
docker compose ps
ss -ltnp | grep ':4000'
最后一条应该看到 127.0.0.1:4000 在监听。如果看到 0.0.0.0:4000,说明 --host 参数没生效——那意味着 4000 又回到了对所有接口开放的状态。
3. Caddy 反代:把 127.0.0.1:4000 变成一个域名
Caddy 用系统包安装,自动 HTTPS 是它自带的能力:
apt update && apt install -y caddy
Caddyfile 只有两行:
llm.example.com {
reverse_proxy 127.0.0.1:4000
}
第一行是域名,第二行是转发目标。Caddy 会自动为这个域名申请并续期证书——不需要单独的 certbot,也没有证书路径要填。
它有两个前提:80 和 443 必须由 Caddy 独占。装之前先查一次:
ss -ltnp | grep -E ':80|:443'
被别的服务占着,就先调整那个服务的端口,否则 Caddy 起不来。
域名这一侧,Cloudflare 需要两件事:一条 A 记录指向 VPS 公网 IP,以及 SSL/TLS 模式设为 Full (strict)。后者要求源站已有有效证书,而 Caddy 正好提供了它。
改完 Caddyfile 的标准动作是三步:
caddy fmt --overwrite /etc/caddy/Caddyfile
caddy validate --config /etc/caddy/Caddyfile
systemctl restart caddy
验证从外到内,三条命令:
curl -I https://llm.example.com
# 期望 HTTP/2 200,且响应头里有 via: 1.1 Caddy
curl https://llm.example.com/v1/models -H "Authorization: Bearer $LITELLM_MASTER_KEY"
# 期望返回模型列表,说明反代 + 鉴权都通了
curl -s http://127.0.0.1:4000/health/liveliness
# 期望 "I'm alive!"
第一条命令里 via: 1.1 Caddy 这个头是关键判据。它证明响应确实经过了 Caddy,而不是被别的东西接走了——同一台机器上如果有多个服务,这个头能省掉很多猜测。
4. 调用:curl、SDK 与 virtual key
链路通了之后,调用本身是标准的 OpenAI 兼容格式。本机验证前先把变量读进当前 shell:
cd /opt/litellm && set -a && source .env && set +a
本机验证:
curl http://127.0.0.1:4000/v1/models \
-H "Authorization: Bearer $LITELLM_MASTER_KEY"
curl http://127.0.0.1:4000/v1/chat/completions \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $LITELLM_MASTER_KEY" \
-d '{"model":"groq-gpt-oss-20b","messages":[{"role":"user","content":"Reply with OK"}],"max_tokens":100}'
公网调用把地址换成 https://llm.example.com 即可。客户端用 SDK 时:
from openai import OpenAI
client = OpenAI(
api_key="你的_virtual_key",
base_url="https://llm.example.com/v1"
)
response = client.chat.completions.create(
model="groq-gpt-oss-20b",
messages=[{"role": "user", "content": "Reply with OK"}]
)
print(response.choices[0].message.content)
注意 api_key 那一栏填的是 virtual key,不是 master key。
这条分工值得写死:master key 只用来管理和测试,客户端一律用 virtual key。原因很直接——master key 能创建、停用、删除所有 key,一旦发出去就收不回来;virtual key 可以单独限定模型、限速、设过期时间,废掉一个不影响其他设备。
创建 key 的命令:
export MK=$(grep '^LITELLM_MASTER_KEY=' /opt/litellm/.env | cut -d= -f2- | tr -d '"')
# 长期设备 key:限定模型、限制并发
curl -s http://localhost:4000/key/generate \
-H "Authorization: Bearer $MK" -H 'Content-Type: application/json' \
-d '{"key_alias":"laptop","models":["chatgpt/gpt-6.1-sol","chatgpt/gpt-6-luna"],"rpm_limit":20,"max_parallel_requests":2}'
# 临时 key:到期自动失效
curl -s http://localhost:4000/key/generate \
-H "Authorization: Bearer $MK" -H 'Content-Type: application/json' \
-d '{"key_alias":"temp-24h","models":["chatgpt/gpt-6-luna"],"duration":"24h","rpm_limit":5}'
管理操作有四个:查看、停用、恢复、删除。
curl -s "http://localhost:4000/key/info?key=sk-xxx" -H "Authorization: Bearer $MK"
curl -s -X POST http://localhost:4000/key/block -H "Authorization: Bearer $MK" -H 'Content-Type: application/json' -d '{"key":"sk-xxx"}'
curl -s -X POST http://localhost:4000/key/unblock -H "Authorization: Bearer $MK" -H 'Content-Type: application/json' -d '{"key":"sk-xxx"}'
curl -s -X POST http://localhost:4000/key/delete -H "Authorization: Bearer $MK" -H 'Content-Type: application/json' -d '{"keys":["sk-xxx"]}'
三条纪律:
- 新 key 的完整值只在创建时显示一次,当场保存,之后只能看到掩码;
- chatgpt 模型要在
models字段里写chatgpt/前缀的完整名字,写短名会被拒绝; /key/delete的字段名以http://localhost:4000/docs的 Swagger 为准,不同版本有过差异。
5. 收紧:端口层两道锁,域名层零道锁
上面的配置跑通之后,有一个不对称的地方需要看清。
端口层有两道锁。LiteLLM 用 --host 127.0.0.1 只听回环;ufw 对 4000 显式 DENY。任何来自公网的直连都到不了。
域名层一道都没有。Caddy 那条 reverse_proxy 127.0.0.1:4000 是全量转发——https://llm.example.com/ 下面所有路径都会送给 LiteLLM。
单独看,每个组件都正常:数据库连上了,Caddy 在跑,/key/* 接口可用。连起来看:
.env里写入DATABASE_URL;- LiteLLM 检测到数据库,启用 virtual key 管理,
/key/generate、/key/info、/key/delete一并可用; - Caddy 仍然是部署时那条全量反代;
- 于是管理接口在公网上可达,唯一的门槛是
LITELLM_MASTER_KEY。
没有任何一个组件发生故障。这是一次纯粹的组合风险:三个都正常的东西拼在一起,得到一个不安全的整体。
这里要把能说和不能说分清楚。
能说:暴露面存在,且只有一层口令。 不能说:它已经被利用了。 也不能说:没人试过——快照里没有访问日志,这两个方向都没有证据。
最便宜的验证方式是:
cat /etc/caddy/Caddyfile
如果它仍然是 reverse_proxy 127.0.0.1:4000,就收紧成按路径放行:
llm.example.com {
@public path /v1/* /health/liveliness
handle @public {
reverse_proxy 127.0.0.1:4000
}
handle {
respond 404
}
}
管理接口只在服务器本机用——通过 localhost:4000 直接访问,不经过域名。这样公网只剩调用路径。
改完照旧三步:caddy fmt、caddy validate、systemctl reload caddy。
6. 运行时:十一次崩溃与一次归因不能
首次接入数据库前后,容器反复重启 11 次。日志是空的。
日志为空有一个技术解释:强杀时 Python 来不及 flush,缓冲区跟着进程一起没了。所以「没有日志」不等于「没有发生」。
当时 mem_limit 与 memswap_limit 都设成 1200m。去掉 memswap_limit 之后恢复正常。
到这一步,最容易写下的句子是「原因是内存不足,去掉 swap 限制解决了」。但这句是错的,至少是没有证据的。
因为在那段时间里,还有另一件事发生了:首次数据库迁移跑完了。
两个变量同时改变,一个结果恢复正常——这在方法上叫归因不能。到底是内存限制、还是迁移完成,或者两者共同作用,现有材料区分不出来。
要补的证据是三样:
dmesg -T | grep -iE 'oom|killed process',看有没有 OOM kill 记录docker inspect的OOMKilled字段- 前台带
PYTHONUNBUFFERED=1跑一次,让日志在崩溃前有机会落盘
还有一个残留风险容易被忽略:去掉 memswap_limit 之后,容器实际可用的是 1200m 内存加 2400m swap。真逼近上限时,它不会干脆地 OOM,而是先进入 swap 抖动——症状更隐蔽,也更难与「这个模型有点慢」区分开。
决策规则写在笔记里:稳态在 900MB 以内,可以考虑把 memswap_limit 加回去;逼近 1200MB,就把 mem_limit 调到 1400m。用一周的观察换一个决定,而不是在崩溃当天拍脑袋。
7. 备份与回滚:803 字节与一分钟
/opt/litellm 下有四个备份文件。
它们的身份全是推测:docker-compose.yml.bak 与 .bak2 被推定为「加内存限制前」和「加 --host 前」,config.yaml.bak 被推定为「加入 chatgpt 模型之前」。没有一处能证明。
最典型的是 config.yaml.bak:它和 config.yaml 同为 803 字节,时间只差一分钟。
803 字节相等,意味着内容很可能也相等;时间只差一分钟,意味着它大概率是改完之后立刻复制的。把它当成「回到加 chatgpt 之前」的回滚点,会退回一个并不存在的中间态。
备份的价值等于时间戳乘以内容可证明性。缺后一项,它就只是另一个文件。
安全层面还有两个细节,都不报错、不告警:
第一个,.env.bak 可能带着 DATABASE_URL。用一条命令确认:
grep -c '^DATABASE_URL=' .env.bak # 应输出 0
输出 0,才说明「退回无数据库状态」这个回滚方案成立。如果输出 1,回滚之后数据库仍然连着,只是你以为它断了。
第二个,cp 出来的文件默认权限是 644,而原件是 600。密钥副本比原件更松——这类漏洞不会让任何服务出问题,只会让某一天的事故后果变重。
所以第一件事是做一份带日期的基线:
cd /opt/litellm && B=$(date +%Y%m%d)
cp -p docker-compose.yml docker-compose.yml.good-$B
cp -p config.yaml config.yaml.good-$B
cp -p .env .env.good-$B && chmod 600 .env.good-$B
不是因为今天特殊,而是因为「今天正常」这件事本身也会过期:再过两周,你同样说不清那份 .bak 是什么时候的。
回滚本身有四条路径:退回无数据库状态(cp .env.bak .env 后 docker compose up -d --force-recreate,前提是上一条命令输出 0)、退回基线备份、临时停用容器、彻底取消自启。退回无数据库不会影响 Neon 里已创建的项目,之后可以随时再接。
8. 会到期的东西:令牌、额度、时区、告警
配置里最贵的东西不是已经坏掉的,是那些写着「以后会自己坏掉」的。
令牌。ChatGPT 的访问令牌约 10 天有效,登录后会自动刷新。但刷新令牌只能用一次——如果把 auth.json 复制到第二个地方,第一次刷新之后两份副本就会分叉,只有一个能继续用。
所以在这里,「备份即安全」是反的。给令牌做备份,等于给自己埋一个 10 天后才响的故障。笔记里那句「只保留这一份」,是一条比它看起来更硬的纪律。
额度。Neon 免费计划是每项目 100 CU 小时、500MB 存储、空闲 5 分钟休眠。问题在于 LiteLLM 的后台查询可能让它一直不休眠——一个空闲的数据库被自己的监控叫醒,然后把额度用完。额度耗尽后计算资源会被暂停,表现出来就是「数据库忽然连不上」。
这件事的麻烦之处在于:它不是故障,是计费。监控里看不到红色。
计量。chatgpt 系列模型没有定价,所以花费记录恒为 0。max_budget 这个保护装置装在了不导电的线路上——它永远不会触发。这条链上真正起作用的限流只剩 rpm_limit 与 tpm_limit。
时区。cron 按服务器本地时间执行,而本地时间与 UTC 有时差。一条 timedatectl 就能把这个变量消掉,代价是几秒钟。
告警。sshd 的 fail2ban jail 在运行,累计失败数百次,封禁 0 次。这句话有两种读法:要么攻击确实弱,要么 jail 没作用在 <ssh端口> 上。失败数百次说明有人在敲门,如果规则生效,总该有人被拦在外面。
最可疑的是端口迁移留下的缝:jail 匹配 ssh.service,ufw 删掉了 22 的规则,sshd 改听 <ssh端口>。服务名与端口解耦之后,封禁可能落在一个没人经过的位置。
验证不需要等攻击者:
sudo fail2ban-client set sshd banip 192.0.2.1 # TEST-NET-1,不会误伤真实地址
sudo iptables -S | grep 192.0.2.1 # 或 sudo nft list ruleset
sudo fail2ban-client set sshd unbanip 192.0.2.1
结论现在只能停在「机制未验证」。不写「已失效」——那需要证据,而证据只需要一次 banip 就能拿到,只是还没拿。
一个从不报警的告警器,比没有告警器更危险。因为它提供的是「已经被保护了」的错觉。
9. 运维台账:按「关闭缺口的成本」排序
日常要看的几样东西,都在 /opt/litellm 下执行:
docker inspect litellm --format 'Status={{.State.Status}} RestartCount={{.RestartCount}} OOMKilled={{.State.OOMKilled}}'
docker stats --no-stream litellm
docker logs --tail 100 litellm 2>&1
free -h && swapon --show
健康检查两条:
curl -s http://localhost:4000/health/liveliness # 应返回 "I'm alive!"
curl -s http://localhost:4000/health/readiness # 应能看到数据库已连接
启停与重载的几条:
docker compose ps # 容器状态
docker compose restart litellm # 只重启,不重读 .env
docker compose up -d # 应用 .env / compose 的改动
docker compose stop litellm # 停止,保留容器
docker start litellm # 再启动
防火墙只放行三个入口:
ufw allow <ssh端口>/tcp
ufw allow 80/tcp
ufw allow 443/tcp
ufw enable
ufw status numbered
4000 不在列表里。它只听回环,公网本来也到不了;显式 DENY 是为了在规则被误改时留下痕迹。
待办原本按 P0 / P1 / P2 分:今天、本周、持续观察。这个分法按的是紧急度。换一个轴——关闭一个缺口需要多少成本。
| 待办 | 关闭哪个缺口 | 成本 | 不做的后果 |
|---|---|---|---|
| 核对 Caddyfile | 管理面是否暴露(D 级 → A 级) | 一条 cat |
继续在公网可达,且不知道 |
| 基线备份 | 「正常」没有可回滚点 | 四条 cp |
下一次变更没有退路 |
| 收紧权限 | 密钥副本 644 | 两次 chmod |
事故后果变重 |
| 验证数据库 | readiness 是否真连上(C → A) | 一次 curl 加日志 |
以为接了,其实没接 |
| 实测 chatgpt 模型 | 模型上架 ≠ 可用 | 两次 /v1/responses |
客户端拿到 not supported |
| 验证 fail2ban | 告警器是否真会响 | 一次 banip 测试 |
继续提供「已保护」的错觉 |
排序之后会发现一件事:最靠前的几项,成本都是一条命令。它们之所以一直没做,不是因为难,是因为做了也不会让任何东西变好——只会让「不知道」变成「知道」。而后者在平时没有反馈。
常见故障的入口也在这张台账上:
| 现象 | 先查什么 |
|---|---|
改了 .env 不生效 |
是不是用了 docker restart,应改用 docker compose up -d |
返回 There are no healthy deployments for this model |
docker exec litellm ls -l /tokens/chatgpt,确认令牌挂载与 CHATGPT_TOKEN_DIR |
日志出现 refresh token failed |
刷新令牌失效,需要重新走一次设备码登录 |
| 数据库连不上 | 连接串是否带了 -pooler、Neon 额度是否耗尽 |
| 公网域名不通 | DNS、ufw 443、Cloudflare SSL 模式、journalctl -u caddy -f |
还有这一期与第 003 期的三处分歧。
第一,客户端凭据。003 的正文写「客户端只使用 LiteLLM Master Key」,文末建议却是「为不同客户端创建独立 API Key」。同一篇里两个口径。现在统一为:master key 只做管理和测试,客户端一律用 virtual key。
第二,镜像更新。003 建议「定期更新 Docker 镜像」,现在写的是「不要随意 docker pull,升级单独做,先备份,升级后核对数据库迁移与模型」。
第三,防火墙。003 的步骤是 ufw allow 22/80/443,现在是放行 <ssh端口>、80、443,22 已经没有服务。
这三处不是纠错清单。它们说明的是一件事:部署教程会过期,台账不会。教程写的是某个时刻的正确动作,而系统在之后每一天都在变——变到某个时刻,那份教程里的每条命令都还能执行,但已经不是当下该做的事。
10. 复盘:一套配置能证明什么
- 运行时长不是稳定性证据。
RestartCount=0只说明到第 12 分钟还没死过。判断稳定需要的是时间跨度,不是某个瞬间的计数。 - 状态表是断言,不是证据。「正常」「运行中」「已配置」都是结论的措辞,除非后面能指到一个 A 级证据,否则它只是把「我不知道」换了个说法。
- 风险在组合处生成。数据库、Caddy、LiteLLM 各自正常,合起来打开了管理面。单组件巡检发现不了这类问题,因为没有任何一个组件出错。
- 沉默的告警等于没有告警。失败数百次而封禁 0 次,要么规则没生效,要么规则作用在了错误的位置。两种都要求同一件事:测一次。
- 同期变更会消灭归因。11 次崩溃的两种可能发生在同一时段,事后再也分不开。要保留归因能力,一次只改一个变量。
- 备份的可靠性由来源决定。同为 803 字节、相差一分钟的两个文件,证明不了任何关于「之前」的事。
- 会到期的东西要写进日历。令牌 10 天、额度按月、时区跟着机器。它们不会报错,只会在某天变成故障。
- 教程与台账的差别在视角。教程回答「怎么做到」,台账回答「现在是什么」。前者会过期,后者要一直更新。
11. 现状与下一步
已确证:LiteLLM 在第 12 分钟存活;4000 只监听回环;ufw 对 4000 是 DENY;Groq 链路在部署阶段从本机、容器、公网三处验证通过;Caddy 当时成功签发证书并返回 via: 1.1 Caddy。
未确证:readiness 与 Neon 是否真正连通;两个 chatgpt 模型能否调用;Caddy 当前内容;fail2ban 的封禁机制;11 次崩溃的根因;ufw 的默认策略;云安全组是否与主机一致。
顺序按依赖排,不按紧急度:
- 核对 Caddyfile——它决定后面所有事是否紧急
- 做基线——先有可回滚点,再动任何配置
- 验证数据库与 chatgpt 模型,把 C 级证据升成 A 级
- 验证告警真的会响
- 用一周的数据再决定
memswap_limit与定时重启
配置能跑起来是一件事,知道它为什么能跑、在什么条件下会不跑,是另一件事。这篇记的是后者。
参考
- 第 003 期《Debian 12 VPS 部署 Groq → LiteLLM → Caddy API 反代全流程》
- LiteLLM 官方文档:
/key/*接口字段以实例的/docsSwagger 为准
本文所有状态与数字取自 /opt/litellm 下的运维笔记与配置文件,快照时间为 2026-10-22;不含密钥、连接串与密码。文中 <...> 为脱敏占位符。