环境:Windows 11 + WorkBuddy(CodeBuddy 系 CLI)+ New API 架构中转站(Cloudflare 前置)
思路通用:任何「只会 OpenAI 协议的客户端」+「只剩 Anthropic 端点的中转站」 都适用,不限 WorkBuddy

TL;DR

中转站的 /v1/chat/completions 被 Cloudflare 按路径拉黑(怎么绕都是 403),全站唯一活着的是 /v1/messages(Anthropic 协议),而我的 CLI 只会说 OpenAI 协议——看起来是个死局。

解法:本机跑一个 LiteLLM 当翻译官,对内开 OpenAI 端点,对外说 Anthropic 话:

CLI ──OpenAI 格式──▶ 127.0.0.1:3456(LiteLLM 桥)──Anthropic 格式──▶ 中转站 /v1/messages

全程本地回环,流式、工具调用全能翻,实测三天稳定。总共 4 步,10 分钟搞定。


一、先确认你真是「死局」:路径封锁的判定

同一个 key、同一个 IP,只换路径对比:

请求结果
GET /v1/models200(key 有效、账号正常)
POST /v1/chat/completions403,返回 Cloudflare HTML 拦截页
POST /v1/messages200,正常出内容

路径变体全灭,无一幸免:

路径结果
/v1/chat/completions/、/v1//chat/completions、?x=1、/openai/v1/chat/completions全 403
/chat/completions(去掉 v1)200 但是 SPA 首页 HTML,没法用

结论:WAF 按路径拉黑了 OpenAI 端点,与 key、UA、额度全都无关。报错文案里那句 "check your account and provider settings" 纯属误导,别去查账号。

二、排查差点翻车:curl 的 403 不一定是真的

这坑必须单独讲,因为我差点被它带沟里。

晚间我用 curl 复测 /v1/messages,全 403 且响应体为空(content-type: application/octet-stream),和被封的 OpenAI 端点表现几乎一样——我以为全站沦陷,方案差点推翻。

后来换 Node fetch 和 Python urllib 复测同一端点,全 200,内容正常。

真相:该站的 Cloudflare 除了按路径拉黑,还按 TLS 指纹(JA3)拉黑 curl 这个客户端。

识别特征:403 + 空 body + application/octet-stream(真路径封锁返回的是 HTML 拦截页)。

方法论(重要):测连通性必须用目标客户端的实际协议栈——CLI 跑在 Node 上就用 Node 的 fetch 测,桥跑在 Python 上就用 urllib 测。curl 的结论只代表 curl 自己,--noproxy "*" 排除代理干扰后仍要这么干。

三、方案原理

┌──────────┐   OpenAI 格式    ┌─────────────────┐   Anthropic 格式   ┌──────────┐
│   CLI    │ ───────────────▶ │  LiteLLM 本地桥  │ ────────────────▶ │  中转站   │
│(只会OpenAI)│  127.0.0.1:3456 │  (协议翻译层)     │  /v1/messages     │(只认Anthropic)│
└──────────┘                  └─────────────────┘                    └──────────┘
  • 对 CLI 侧:一座标准 OpenAI 兼容网关,baseURL 填 http://127.0.0.1:3456/v1
  • 对中转站:一个说 Anthropic 协议的客户端,出站打 /v1/messages
  • 请求体、SSE 流式帧、tool_calls ↔ tool_use,全部双向翻译

四、实施步骤(Windows,可跟做)

Step 0:前置验证(1 分钟,别跳过)

装任何东西之前,先用 Python urllib(不是 curl,理由见第二节)确认 Anthropic 端点活着、且 Python 的 TLS 指纹没被拉黑:

import json, urllib.request
req = urllib.request.Request(
    "https://api.<你的中转站域名>/v1/messages",
    data=json.dumps({"model": "<模型名>", "max_tokens": 16,
                     "messages": [{"role": "user", "content": "Reply OK"}]}).encode(),
    headers={"x-api-key": "er-你的key",
             "anthropic-version": "2023-06-01",
             "content-type": "application/json"})
print(urllib.request.urlopen(req, timeout=60).status)  # 200 才继续

这一步同时验证了两件事:端点活着 + Python 栈能过 Cloudflare。后者是本方案唯一的硬前提——如果 Python 也被 TLS 指纹拦,此路不通。

Step 1:建隔离环境装 LiteLLM

& "C:\<你的Python路径>\python.exe" -m venv "C:\litellm-bridge"
& "C:\litellm-bridge\Scripts\pip.exe" install -i https://pypi.tuna.tsinghua.edu.cn/simple "litellm[proxy]"

独立 venv,不污染系统环境。国内走清华镜像,1 分多钟装完。

Step 2:写桥的配置

C:\litellm-bridge\config.yaml:

model_list:
  - model_name: gpt-5.6-luna            # CLI 侧看到的模型名
    litellm_params:
      model: anthropic/gpt-5.6-luna     # anthropic/ 前缀 = 出站走 Anthropic 协议(关键!)
      api_base: https://api.<你的中转站域名>
      api_key: er-xxxxxxxx              # 中转站发给你的 key
  - model_name: gpt-5.6-sol
    litellm_params:
      model: anthropic/gpt-5.6-sol
      api_base: https://api.<你的中转站域名>
      api_key: er-xxxxxxxx
  # 有几个模型抄几段

litellm_settings:
  drop_params: true        # 关键!见踩坑 3
  telemetry: false
  request_timeout: 600

general_settings:
  master_key: sk-bridge-你自己随便造一个   # 桥自己的 key,CLI 侧填这个

Step 3:写启动脚本

C:\litellm-bridge\启动.bat(桌面建个快捷方式,双击就开、关窗就停):

@echo off
set HTTP_PROXY=
set HTTPS_PROXY=
set ALL_PROXY=
set NO_PROXY=*
title LiteLLM Bridge - 127.0.0.1:3456
"C:\litellm-bridge\Scripts\litellm.exe" --config "C:\litellm-bridge\config.yaml" --host 127.0.0.1 --port 3456
pause

注意调用的是 litellm.exe,不是 python -m litellm——后者已失效,见踩坑 1。
开头清空代理变量也是必须的,见踩坑 2。

Step 4:客户端指向桥

任何 OpenAI 兼容客户端,把 baseURL 指到 http://127.0.0.1:3456/v1,key 填 master_key,模型名照旧。

WorkBuddy(CodeBuddy 系)为例,~/.workbuddy/models.json 加条目(改完即时生效,无需重启):

{
  "id": "gpt-5.6-luna",
  "name": "gpt-5.6-luna(本地桥接)",
  "vendor": "Custom",
  "url": "http://127.0.0.1:3456/v1",
  "apiKey": "sk-bridge-你自己随便造一个",
  "supportsToolCall": true,
  "supportsImages": true,
  "supportsReasoning": true,
  "useCustomProtocol": false
}

五、验证清单(全绿才算通)

# 非流式:确认翻译和路由正常
curl.exe --noproxy "*" -sS http://127.0.0.1:3456/v1/chat/completions `
  -H "Authorization: Bearer sk-bridge-xxx" -H "Content-Type: application/json" `
  -d '{"model":"gpt-5.6-luna","max_tokens":16,"messages":[{"role":"user","content":"Reply OK"}]}'

# 流式:确认 SSE 帧正常且以 data: [DONE] 收尾
#   加 "stream": true,检查输出里有 chat.completion.chunk 帧和 [DONE]

# 工具调用:确认 tool_calls 翻译正常(agent 类 CLI 的命脉)
curl.exe --noproxy "*" -sS http://127.0.0.1:3456/v1/chat/completions `
  -H "Authorization: Bearer sk-bridge-xxx" -H "Content-Type: application/json" `
  -d '{"model":"gpt-5.6-luna","max_tokens":200,"messages":[{"role":"user","content":"Beijing weather? Use the tool"}],"tools":[{"type":"function","function":{"name":"get_weather","parameters":{"type":"object","properties":{"city":{"type":"string"}}}}},"tool_choice":{"type":"function","function":{"name":"get_weather"}}}'

我的实测结果(3 个模型 × 3 项测试):非流式 200 出内容、流式 200 标准 OpenAI SSE + [DONE]、工具调用 200 且 tool_calls 参数翻译正确。另外带 reasoning_effort 参数也不报错(被 drop_params 兜住了)。

六、踩坑汇总

#坑症状解法
1python -m litellm 已死No module named litellm.__main__; 'litellm' is a package and cannot be directly executed1.100+ 版本删了 __main__ 入口,必须用 Scripts\litellm.exe
2代理环境变量劫持出站桥进程启动正常,请求全部超时/连接失败启动前清空 HTTP_PROXY/HTTPS_PROXY/ALL_PROXY(bat 里 set XXX=),设 NO_PROXY=*
3CLI 发的 OpenAI 特有参数被 Anthropic 端拒绝400 报参数不认识(如 reasoning_effort)config 里 drop_params: true,自动丢弃目标端不认的参数
4curl 测出假 403403 + 空 body + octet-streamTLS 指纹拦截,换 Node fetch / Python urllib 复测再下结论
5中转站模型名悄悄变了503 no available channel / model_not_found桥配置里的 model: 用实测 200 的名字,别照抄旧文档
6LiteLLM 首次启动卡一下日志报拉远程价格表超时无碍,自动落回本地备份,不用管

七、局限与说明

  • 唯一硬前提:Python 栈的 TLS 指纹没被中转站拉黑(Step 0 已验证)
  • 桥是个常驻进程,用的时候得开着(双击 bat,黑窗别关);不用就关,零后台占用
  • 多一跳本机回环,延迟增加可忽略(<1ms)
  • key 安全:master_key 只在本机回环上暴露,中转站真实 key 只存在于桥配置里,不进客户端配置——某种意义上比直连还干净一点

结语

中转站的 Cloudflare 规则你控制不了,但「客户端协议」和「本机 127.0.0.1」都是你的地盘。打不过就绕,绕不过就翻译——LiteLLM 这类网关本来就是干这个的,拿来救「只剩 Anthropic 端点」的场,属于物尽其用。

有更好解法(比如客户端原生支持 Anthropic 协议的,像 opencode 换 @ai-sdk/anthropic 就能直连)当然优先原生,本方案是给「客户端协议焊死在 OpenAI 上」的场景兜底的。

主楼