🦞 从原生认证到回归 CopilotX:一场和 Anthropic SDK 的拉锯战

下午打算让小龙虾帮忙干活,发现它不说话了。

检查了一下,CopilotX 的 Azure VM 没响应——我这个月微软员工 150 美刀的 Azure 订阅额度用超了,虚机直接停了。

好消息是:OpenClaw 自己就能登录 GitHub Copilot。坏消息是:登录成功不等于能用。

背景:CopilotX 与 OpenClaw 的关系

如果你是第一次来,先简单介绍一下这几个角色。不熟悉的读者可以看看前两篇:让小龙虾跑起来和 CopilotX v2.3.2。

  • OpenClaw(小龙虾):开源个人 AI 助手,能同时接入 Telegram、Discord、WhatsApp 等消息渠道
  • CopilotX:我写的 API 代理,部署在 Azure VM(api.polly.wang),把 GitHub Copilot 订阅转成标准 Anthropic Messages API
  • 关系:OpenClaw 调用 CopilotX 获取 Claude 的能力

这套架构跑了半个多月,很爽——直到今天 Azure VM 停了。

CopilotX vs github-copilot:两种吃 Copilot 的方式

让 OpenClaw 用上 GitHub Copilot 的模型,其实有两条路:

CopilotX(自建代理)github-copilot(原生认证)
原理Azure VM 上跑 FastAPI,转发请求到 Copilot 内部 APIOpenClaw 内置 Device Flow OAuth,直接调 GitHub API
部署需要一台服务器(api.polly.wang)不需要,纯本地
认证CopilotX 自己登录 GitHub,客户端用 API Key在浏览器里授权一次,token 自动管理
API 格式Anthropic Messages(/v1/messages)OpenAI Completions(Copilot 原生格式)
依赖Azure VM 必须在线只要 GitHub 不挂就行
适合场景多客户端共享、自定义路由单机使用、快速切换

之前选 CopilotX 是因为 OpenClaw 初始版本不支持 Copilot,我只能通过代理桥接。但现在 OpenClaw 已经内置了原生的 github-copilot provider——不需要中间人了。

实操:三步完成切换

Step 1:更新 OpenClaw

我的本地版本是 v2026.2.14,但配置格式要求 v2026.2.17,直接报版本不匹配:

Error: Configuration version mismatch. Config: 2026.2.17, Installed: 2026.2.14

更新一下:

cd X-Workspace/openclaw
git stash && git pull && pnpm install && pnpm build

跳到了 v2026.2.26,问题解决。

Step 2:登录 GitHub Copilot

这一步我踩了个坑。直觉告诉我应该用:

# ❌ 这是错的
openclaw models auth login --provider github-copilot

报错 Unknown provider: github-copilot。试了半天 copilot-proxy 插件也是条死路(那是本地 VS Code 代理,需要 VS Code 扩展运行在 localhost:3000,完全不是我要的东西)。

正确的命令是一个独立子命令,不走 --provider 参数:

# ✅ 这才对
pnpm openclaw models auth login-github-copilot

跑起来后,熟悉的 GitHub Device Flow:

🔑 GitHub Copilot Authentication
Please visit: https://github.com/login/device
Enter code: 223C-2FDC
Waiting for authorization...

浏览器里输入设备码,点授权,终端里自动完成:

✅ GitHub Copilot authentication successful!
Auth profile saved: github-copilot:github (github-copilot/token)

Step 3:切换默认模型

登录成功后,OpenClaw 的配置中已经有了 github-copilot 这个 provider。但默认模型还指向不可用的 copilotx/claude-opus-4-6。

编辑 ~/.openclaw/openclaw.json,把默认模型改成:

{
  "agents": {
    "defaults": {
      "model": {
        "primary": "github-copilot/claude-opus-4-6"
      }
    }
  }
}

看起来万事俱备。重启 gateway,准备跟小龙虾聊天——

技术原理:为什么一条命令就够了

好奇心驱使我翻了一下 OpenClaw 的源码,看看 login-github-copilot 背后到底做了什么。

Device Flow OAuth

这是 GitHub 为无浏览器设备设计的 OAuth 流程,整个过程不需要回调 URL:

OpenClaw                        GitHub                     浏览器
  │                               │                           │
  │─── POST /login/device/code ──►│                           │
  │◄── device_code + user_code ───│                           │
  │                               │                           │
  │  显示 "请访问 github.com/login/device 输入 223C-2FDC"     │
  │                               │         用户输入设备码 ──►│
  │─── POST /login/oauth/access_token(轮询)──►│             │
  │◄── authorization_pending ─────│                           │
  │─── POST(再次轮询)──────────►│                           │
  │◄── access_token ──────────────│                           │

OpenClaw 用的 Client ID 是 Iv1.b507a08c87ecfe98,scope 是 read:user——这和 VS Code 里 GitHub Copilot 扩展用的 ID 非常接近。

Token 交换:关键一步

拿到 GitHub access_token 后,还不能直接调 Copilot API。需要一步token 交换:

POST https://api.github.com/copilot_internal/v2/token
Authorization: token ghp_xxxxx

返回的是一个短期 Copilot token(通常几十分钟有效期),包含在一串分号分隔的字符串里。OpenClaw 会:

  1. 缓存到 ~/.openclaw/credentials/github-copilot.token.json
  2. 从 token 中提取 proxy-ep 字段,自动发现 API 端点
  3. 过期前 5 分钟自动刷新

默认的 API 端点是 https://api.individual.githubcopilot.com,但 token 里的 proxy-ep 可能指向不同的区域代理。

这就是 CopilotX 做的事情的简化版——CopilotX 本质上也是做同样的 token 交换,只不过它部署在服务器上,给多个客户端共享。而 OpenClaw 的原生实现把这一切搬到了本地。

剧情反转:400 "model not supported"

以为万事大吉,启动 OpenClaw gateway——

Error: 400 - The requested model is not supported

Claude 模型全部 400。但 GPT-4o 能用。

排查过程

这是一场长达半天的系统性排查。我怀疑过所有的环节,逐一排除:

1. 端点问题?

Copilot token 里的 proxy-ep 指向 proxy.enterprise.githubcopilot.com,会被 OpenClaw 转成 api.enterprise.githubcopilot.com。企业端点可能没有 Claude。

于是我在 openclaw.json 里强制覆盖 baseUrl:

{
  "models": {
    "providers": {
      "github-copilot": {
        "baseUrl": "https://api.githubcopilot.com"
      }
    }
  }
}

2. curl 测试?

用同一个 token,直接 curl api.githubcopilot.com/v1/messages:

curl -X POST https://api.githubcopilot.com/v1/messages \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"model":"claude-sonnet-4.5","max_tokens":100,"stream":true,
       "messages":[{"role":"user","content":"Hi"}]}'

200。 完美返回。

加上 tools、system prompt、thinking、cache_control、anthropic-beta header——全部 200。

3. pi-ai SDK 内部测试?

写了个脚本直接测 pi-ai 的 ModelRegistry,确认 baseUrl 和 api 字段都正确解析了。没问题。

4. 最终定位

写了个脚本直接调 pi-ai 的 streamAnthropic() 函数——400,model not supported。

同样的 URL、同样的 token、同样的 payload,curl 返回 200,pi-ai 的 Anthropic SDK 返回 400。

未解之谜

问题出在 @anthropic-ai/sdk 这个 JavaScript SDK 内部。它在构造 HTTP 请求时,做了一些 curl 没做的事情——可能是额外的 header、不同的序列化方式、或者某种请求签名。而 Copilot 的后端对这些差异很敏感。

我花了半天时间,但无法继续深入 SDK 的内部实现(编译后的代码不好调试)。最终决定:放弃原生方案,回归 CopilotX。

回归 CopilotX:本地启动

既然 Azure VM 挂了,那就在本地跑 CopilotX:

cd X-Workspace/CopilotX
pip install -e .
copilotx auth login    # 重新登录
copilotx serve         # 启动本地代理

CopilotX 的输出让人安心:

🚀 CopilotX v2.3.2
✅ Copilot Token valid (29m remaining, auto-refresh)
🔗 Anthropic API: http://127.0.0.1:24680/v1/messages
📋 Models: claude-opus-4.6, claude-sonnet-4.6, claude-sonnet-4, ...

然后把 openclaw.json 里的 baseUrl 改回 CopilotX:

{
  "models": {
    "providers": {
      "copilotx": {
        "baseUrl": "http://localhost:24680",
        "api": "anthropic-messages"
      }
    }
  }
}

重启 gateway——小龙虾立刻活蹦乱跳。

为了以后方便切换本地/远程,我还写了个小脚本:

python scripts/switch_copilotx.py          # 一键 toggle
python scripts/switch_copilotx.py local    # 切本地
python scripts/switch_copilotx.py remote   # 切远程 (api.polly.wang)
python scripts/switch_copilotx.py status   # 看当前

插曲:切了配置但小龙虾还是连不上

Azure VM 恢复后,我用脚本切到远程——结果小龙虾还是报 Connection error。

查了半天才发现:OpenClaw 在 agent 目录下有一个模型缓存文件 ~/.openclaw/agents/main/agent/models.json,里面硬编码了 baseUrl。我的脚本只改了 openclaw.json,但 gateway 运行时读的是缓存——还在连 localhost:24680。

# 罪魁祸首:缓存了旧的 baseUrl
cat ~/.openclaw/agents/main/agent/models.json | grep baseUrl
# → "baseUrl": "http://localhost:24680"  ← 早就该是远程了!

修复很简单:切换时顺手删掉缓存。我更新了 switch 脚本,每次切换自动清理这个文件。

教训:配置系统有缓存层时,改了源头不等于改了运行时。

为什么 CopilotX 能行而原生不行?

这是个有趣的对比:

CopilotXOpenClaw 原生 github-copilot
请求格式OpenAI Chat Completions(/chat/completions)Anthropic Messages(/v1/messages)via pi-ai SDK
HTTP 库Python httpx(直接构造请求)JS @anthropic-ai/sdk(SDK 抽象层)
结果✅ 200❌ 400

CopilotX 用的是 OpenAI 格式 + Python httpx,请求完全可控;而 OpenClaw 的原生 provider 走 pi-ai 的 Anthropic SDK,SDK 内部的请求构造对 Copilot 后端来说有些"不对劲"。

本质区别:CopilotX 说的是 Copilot 听得懂的"方言"(OpenAI Chat Completions),而 Anthropic SDK 说的是 Copilot 不完全兼容的"方言"(Anthropic Messages + SDK 内部的额外 header 和格式)。

经验总结

  1. 自己写的代码最可控。CopilotX 不过几百行 Python,但因为每一行我都清楚在做什么,debug 和修改都是分钟级。而踩进第三方 SDK 的黑盒里,半天也出不来。

  2. curl 通过 ≠ SDK 通过。这是我这次最大的教训。curl 成功只能证明 API 端点和认证没问题,但 SDK 封装可能引入你看不到的差异。

  3. 先给自己留后路。CopilotX 的本地模式是个意外惊喜——不需要 Azure VM,本地 copilotx serve 就能跑。这让"Plan B"的切换成本几乎为零。

  4. 调试到极限也要知道何时收手。半天的排查让我精确定位到了问题(Anthropic SDK 内部的请求构造),但继续深入的边际回报很低。有时候绕过问题比解决问题更务实。

  5. 改了配置不等于改了运行时。OpenClaw 有 agent-level 的 models.json 缓存,改了 openclaw.json 不会自动同步。任何有缓存层的系统,都要想清楚"我的改动到底在哪一层生效"。

版本线

日期事件博文
2026-02-11OpenClaw + CopilotX 首次联通让小龙虾跑起来
2026-02-18CopilotX v2.3.2 错误透传修复当 400 变成了「你猜怎么了」
2026-02-26Azure VM 停服 → 尝试原生认证 → 发现 Anthropic SDK 兼容问题 → 回归本地 CopilotX本文

Comments