[经验分享] work Buddy自定义模型只能用OpenAI协议?自建LiteLLM 路由Anthropic

712
  • peterpan134

    环境: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_callstool_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_effortconfig 里 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 上」的场景兜底的。

  • peterpan134
    #1

    测试AgentRouter和Just Worker的模型

  • wxyz
    #2

    点赞

发表回复

登录后回复