自建 Copilot Cli 代理:让 GitHub Copilot 真正“Bring Your Own Key“
双协议代理,不强行转换
很多代理项目的一个误区是"协议互转"——把 OpenAI 的请求格式转成 Anthropic 的,反之亦然。这在实际使用中极其脆弱:
- 字段映射永远滞后于官方 API 的更新
- 工具调用(Function Calling)、系统提示等高级特性很难对齐
- 流式响应的 SSE 格式差异巨大
我们的做法更简单也更可靠:分别暴露两套原生端点。
| 端点 | 协议 | 用途 |
|---|---|---|
POST /v1/chat/completions |
OpenAI | Copilot 客户端或任何 OpenAI SDK |
POST /v1/messages |
Anthropic | Claude Code、Claude Desktop 等 |
GET /v1/models |
OpenAI | 列出可用模型(含 AutoCopilot) |
请求到达后,代理只做三件事:
- 校验 API Key
- 根据模型名找到对应的 Provider 和 Key
- 用
HttpCompletionOption.ResponseHeadersRead直接透传流式响应
没有格式转换,没有中间缓冲,延迟几乎为零。
// ProxyService.cs 核心片段
var requestMessage = CreateProxyRequest(context, provider, actualModel);
var response = await _httpClient.SendAsync(
requestMessage,
HttpCompletionOption.ResponseHeadersRead, // 关键:不缓冲响应体
cancellationToken);
// 直接 pipe SSE 流
await response.Content.CopyToAsync(context.Response.Body);
2. AutoCopilot 影子模型
这是项目最具特色的设计。
传统代理需要在客户端指定确切的模型名,比如 gpt-4o 或 claude-3-5-sonnet。但 Copilot 客户端通常不会暴露模型选择,或者你希望在不修改客户端配置的情况下动态切换底层模型。
AutoCopilot 是一个虚拟模型名。你可以在管理后台随时把它绑定到任意一个已配置的模型:
{
"autoCopilot": {
"currentModel": "claude-3-5-sonnet",
"currentProvider": "anthropic"
}
}
切换即时生效,无需重启服务。所有请求 model: "AutoCopilot" 的调用都会被透明转发到当前绑定的真实模型。
这在实际工作中非常有用:
- 早上用
gpt-5.5写代码,随时切到deepseekv4-flash写文档 - 某个模型临时故障,秒级切换到备用模型
- A/B 测试不同模型在相同提示下的表现
3. 请求级指标监控
BYOK 的一个核心诉求是"成本可控"。如果只是转发请求,你根本不知道这个月花了多少钱、哪个模型最费 token、响应慢不慢。
我们在代理层拦截了每一次请求,记录了 14 个字段的详细指标:
| 指标 | 说明 |
|---|---|
timestamp |
请求时间 |
requested_model |
客户端请求的模型名 |
actual_model |
实际转发的模型名 |
provider |
openai / anthropic |
protocol |
chat.completions / messages |
is_streaming |
是否流式 |
prompt_tokens |
输入 token 数 |
completion_tokens |
输出 token 数 |
latency_ms |
首字节延迟(TTFT) |
total_duration_ms |
总耗时 |
tokens_per_second |
生成速度 |
is_cache_hit |
是否命中缓存 |
status_code |
HTTP 状态码 |
error |
错误信息 |
基于这些数据,管理后台提供了:
- 概览卡片:总请求数、成功率、总 token、预估成本
- 趋势图表:请求量、token 消耗、延迟分布、模型占比(基于 Chart.js)
- 请求日志:可筛选、可分页、可导出 CSV
成本估算是内置的——我们在代码中维护了一张各模型的价格表,根据 prompt_tokens 和 completion_tokens 自动计算。虽然不是 100% 精确(不含缓存折扣等),但足够做用量预警。
关键实现细节
流式响应的零拷贝转发
对于 SSE 流式响应,很多初学者会犯一个错误:把响应完整读进内存字符串,再逐行 WriteAsync。这在高并发下既耗内存又增加延迟。
正确的做法是让 HTTP 管道直接对接:
// 流式路径
if (isStreaming)
{
await response.Content.CopyToAsync(originalResponse.Body);
}
CopyToAsync 内部使用缓冲区循环读写,数据从 Provider 的 TCP 连接直接流向客户端,不经过完整的字符串解析。我们在旁边再开一个任务读取同样的流来解析 usage 数据,互不阻塞。
配置热重载
代理服务理论上要 7×24 运行,重启来加载新配置是不可接受的。
ConfigService 使用了一个简单的原子替换模式:
private AppConfiguration _config = new();
private readonly ReaderWriterLockSlim _lock = new();
public void UpdateConfiguration(AppConfiguration config)
{
_lock.EnterWriteLock();
try
{
_config = config; // 引用替换,瞬间完成
PersistToSqlite(config);
}
finally
{
_lock.ExitWriteLock();
}
}
读配置用读锁,更新配置用写锁,保证并发安全的同时切换是毫秒级的。
JSON 到 SQLite 的平滑迁移
项目早期使用 JSON 文件存储配置(Data/models.json)。随着功能增加,配置和指标需要更结构化的查询能力,我们迁移到了 SQLite。
但在 Program.cs 启动时,会检查是否存在旧的 JSON 文件:
if (File.Exists(configPath))
{
MigrateJsonToSqlite(configPath, configService);
File.Move(configPath, configPath + ".backup", overwrite: true);
}
适用场景与局限性
适合用
- 个人开发者:已有 OpenAI/Anthropic 额度,想把 Copilot 客户端接过来
- 小团队:统一管理模型访问和用量,避免每个人自己存 API Key
- 模型评测:快速切换不同模型,在相同代码库上对比效果
不适合用
更多推荐




所有评论(0)