自建 Copilot Cli 代理:让 GitHub Copilot 真正“Bring Your Own Key“
双协议代理不强行转换很多代理项目的一个误区是协议互转——把 OpenAI 的请求格式转成 Anthropic 的反之亦然。这在实际使用中极其脆弱字段映射永远滞后于官方 API 的更新工具调用Function Calling、系统提示等高级特性很难对齐流式响应的 SSE 格式差异巨大我们的做法更简单也更可靠分别暴露两套原生端点。端点协议用途POST /v1/chat/completionsOpenAICopilot 客户端或任何 OpenAI SDKPOST /v1/messagesAnthropicClaude Code、Claude Desktop 等GET /v1/modelsOpenAI列出可用模型含 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实际转发的模型名provideropenai / anthropicprotocolchat.completions / messagesis_streaming是否流式prompt_tokens输入 token 数completion_tokens输出 token 数latency_ms首字节延迟TTFTtotal_duration_ms总耗时tokens_per_second生成速度is_cache_hit是否命中缓存status_codeHTTP 状态码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模型评测快速切换不同模型在相同代码库上对比效果不适合用