众所周知,GitHub Copilot 付费订阅的计费方式不是按 token 用量计费的,而是按一个叫做 premium requests 的次数来计费。而且这个次数还会根据不同模型乘以不同的权重倍率——比如 Claude Opus 4.5 每次请求乘以 3x,用起来刷刷地掉。
那到底什么请求才算一次 premium request?
X-Initiator:一个重要信号,不是公开契约
根据 opencode#8721、opencode#8030、Neovim 社区的抓包分析(homeward-sky.top),以及 VSCode 官方源码,可以合理推断:客户端会用 X-Initiator 标记来区分「用户发起」和「agent/tool loop 发起」的请求。
X-Initiator: user → 计费
X-Initiator: agent → 不计费但要注意:GitHub 官方文档并没有把 X-Initiator 当作公开稳定的计费接口来承诺。 官方公开说法仍然是:
Copilot Chat uses one premium request per user prompt, multiplied by the model’s rate.
所以更稳妥的表述应该是:X-Initiator 目前看起来是一个重要的客户端侧实现信号,而不是可以长期依赖的公开契约。
VSCode 官方的做法
看一下 VSCode 官方 Copilot 插件(microsoft/vscode-copilot-chat)的实现:
// toolCallingLoop.ts:469
userInitiatedRequest: iterationNumber === 0
&& !isContinuation
&& !this.options.request.subAgentInvocationId简单说:
| 场景 | X-Initiator | 计费? |
|---|---|---|
| 你发的第一条消息 | user | 是 |
| 工具调用循环 | agent | 否 |
| Continuation | agent | 否 |
| 子代理调用 | agent | 否 |
在 VSCode 的实现里,每次发送新的 prompt 算一次 premium request,之后的 tool call、子代理、continuation 都不计费。
OpenCode 的问题(v1.2.27 源码审计)
基于 OpenCode v1.2.27(2026-03-16 发布)的源码审计。
OpenCode 内置的 CopilotAuthPlugin 有两层 X-Initiator 判断逻辑:
第一层:auth.loader.fetch(copilot.ts:69-119),在发送 HTTP 请求时根据 body 最后一条消息的 role 判断:
// Completions API / Responses API
isAgent: last?.role !== "user"
// Messages API(稍好,额外检查了 tool_result)
isAgent: !(last?.role === "user" && hasNonToolCalls)第二层:chat.headers hook(copilot.ts:312-344),为两种特定场景打补丁:
// 补丁 1:compaction 消息 → 标 agent
if (parts?.data.parts?.some((part) => part.type === "compaction")) {
output.headers["x-initiator"] = "agent"
return
}
// 补丁 2:subagent session → 标 agent
if (!session || !session.data.parentID) return
output.headers["x-initiator"] = "agent"问题:6 条 synthetic user message 路径
OpenCode 内部会在多种场景下创建 role: "user" 的合成消息。我们审计了 v1.2.27 全部源码,找到了 6 条会产生 synthetic user message 的路径:
| # | 位置 | 触发场景 | 第一层判断 | 第二层兜底 | 会误扣? |
|---|---|---|---|---|---|
| ① | prompt.ts:507-525 | task tool 执行后的 summary(“Summarize the task tool output…“) | ❌ isAgent=false | ❌ 不是 compaction 也不是 subagent | ⚠️ 会误扣 |
| ② | prompt.ts:1508-1530 | 用户手动执行 shell 命令后(“The following tool was executed by the user”) | ❌ isAgent=false | ❌ | ⚠️ 会误扣 |
| ③ | compaction.ts:239-290 | compaction 后 replay/continue | ❌ isAgent=false | ✅ compaction 补丁兜底 | ✅ 已保护 |
| ④ | compaction.ts:309-318 | compaction 创建入口 | ❌ isAgent=false | ✅ compaction 补丁兜底 | ✅ 已保护 |
| ⑤ | plan.ts:46-64, 104-113 | plan 工具切换 agent(“The plan has been approved…“) | ❌ isAgent=false | ❌ | ⚠️ 会误扣 |
| ⑥ | message-v2.ts:763-765 | tool result 含图片时注入 | ❌ isAgent=false | ❌ | ⚠️ 会误扣 |
结论:v1.2.27 内置逻辑只保护了 compaction 和 subagent session(③④),还有 4 条路径会误扣。 其中 ① task tool summary 在长 session 中触发频率最高。
相关 Issue / PR:
- opencode#8030 — 首次报告 synthetic message 误扣
- opencode#8700 — subagent 误扣(已通过
parentID检查修复) - opencode#8721 — 更全面的修复 PR(仍为 draft,未合并)
- opencode#17431 — compaction 补丁(2026-03-14 合入)
激进方案:全标 agent(别这么干)
最直接的想法是把所有请求都标成 X-Initiator: agent,理论上全部免费。
但这风险很大。GitHub 检测到你 100% 都是 agent 请求、没有任何 user 请求,和正常使用行为完全不一样,封号概率不低。
我们的方案:Session 级别标记
思路是在 session 级别追踪:每个 session 第一次请求标 user,后续全部标 agent。
这和 VSCode 官方行为不完全一样(官方每次新 prompt 都算一次),但比 OpenCode 当前乱标的情况好太多了。说白了就是投机取巧,但至少不像全标 agent 那么激进。
效果
一个 session 内不管多少轮对话、多少次 tool call 和子代理调用,只消耗 1 次 premium request × 模型倍率。
为什么能覆盖全部 6 条路径?
我们的插件工作在 chat.headers hook 层,它不关心消息内容,只关心 session ID。不管触发的是哪条 synthetic user message 路径,只要 session 已经被标记过,就一律设为 agent。这比内置逻辑的”按消息内容猜”更可靠。
完整的覆盖链路(基于 v1.2.27 源码审计确认):
① 所有 LLM 请求 → llm.ts:133 调用 Plugin.trigger("chat.headers")
② hooks 按顺序执行(plugin/index.ts:48-53, 62-103):
内置 CopilotAuthPlugin.chat.headers → 可能设 x-initiator
自定义插件.chat.headers → 最后执行,覆盖上面的值
③ hook 输出 → llm.ts:222 spread 进 streamText() 的 headers
④ SDK 调用 → auth.loader.fetch(request, init)
⑤ copilot.ts:122-124:
const headers = {
"x-initiator": isAgent ? "agent" : "user", // 内置逻辑先设
...(init?.headers), // hook 输出覆盖 ✅
}
⑥ 最终发出的 HTTP 请求 → 以我们插件的值为准关键点:
plugin/index.ts:48先加载INTERNAL_PLUGINS(Codex → Copilot → Gitlab)plugin/index.ts:62再加载用户插件 → 用户插件的 hook 最后执行,最后写入output.headers的值生效copilot.ts:124的...(init?.headers)spread 在x-initiator初始值之后,覆盖了内置的 body 分析逻辑
插件实现
用 OpenCode 的插件系统写一个全局插件。带文件日志和请求计数,方便随时验证效果:
import type { Hooks, PluginInput } from "@opencode-ai/plugin"
import { appendFileSync } from "fs"
const LOG_FILE = "/tmp/copilot-initiator.log"
function log(msg: string) {
const ts = new Date().toISOString().slice(11, 23)
try { appendFileSync(LOG_FILE, `[${ts}] ${msg}\n`) } catch {}
}
export default async function CopilotInitiatorPlugin(
_input: PluginInput
): Promise<Hooks> {
const seenSessions = new Set<string>()
const stats = { user: 0, agent: 0 }
log("=== Plugin loaded ===")
return {
"chat.headers": async (input, output) => {
if (!input.model.providerID.includes("github-copilot")) return
const sessionID = input.sessionID
const sid = sessionID.slice(0, 12)
const before = output.headers["x-initiator"] ?? "unset"
if (seenSessions.has(sessionID)) {
output.headers["x-initiator"] = "agent"
stats.agent++
log(`session=${sid} before=${before} after=agent (total: ${stats.user}u/${stats.agent}a)`)
} else {
seenSessions.add(sessionID)
output.headers["x-initiator"] = "user"
stats.user++
log(`session=${sid} before=${before} after=user [FIRST] (total: ${stats.user}u/${stats.agent}a)`)
}
},
}
}日志写入
/tmp/copilot-initiator.log(appendFileSync),不走 stderr/stdout,不会被 TUI 吞掉。随时cat /tmp/copilot-initiator.log就能看。before字段记录了内置CopilotAuthPlugin设置的值(正常请求为unset,compaction/subagent 可能是agent),after是我们覆盖后的最终值。
安装
mkdir -p ~/.opencode/plugins
# 把上面的代码保存为 ~/.opencode/plugins/copilot-initiator.ts
# 重启 opencode 生效OpenCode 会自动扫描以下目录的 {plugin,plugins}/*.{ts,js}:
~/.config/opencode/项目/.opencode/~/.opencode/← 推荐放这里,全局生效
推荐搭配 OpenCode v1.2.27
npm install -g opencode-ai@1.2.27为什么选这个版本:即使你的插件某些边缘路径没覆盖到,v1.2.27 内置的 compaction 补丁(#17431)和 subagent session 检测也能兜底。npm 全局包不会自动更新,装完就锁定在这个版本。
效果验证(亲测)
实测日志
以下是实际测试的日志输出(OpenCode v1.2.27 + Claude Haiku 4.5):
# 正常启动 opencode 即可,日志自动写入 /tmp/copilot-initiator.log
opencode
# 另开终端查看
cat /tmp/copilot-initiator.log[06:22:28.836] === Plugin loaded ===
[06:25:59.579] session=ses_30aad5d2 before=unset after=user [FIRST] (total: 1u/0a)
[06:25:59.587] session=ses_30aad5d2 before=unset after=agent (total: 1u/1a)
[06:37:36.613] session=ses_30aad5d2 before=unset after=agent (total: 1u/2a)逐行解读:
- 第 1 行:插件加载成功
- 第 2 行:用户第一次发消息 → 标
user(计费 1 次) - 第 3 行:OpenCode 自动发的 title 生成请求 → 被覆盖为
agent(免费) - 第 4 行:用户第二轮对话 → 仍然是
agent(免费)
关键观察:
before=unset:证明内置CopilotAuthPlugin的chat.headershook 在普通请求路径上不设置x-initiator(只有 compaction/subagent 才设)- 实际的
x-initiator值完全由我们的插件控制 - 同一 session 内 3 次 LLM 请求,只有 1 次标
user
方法 1:看日志(推荐)
# 实时跟踪
tail -f /tmp/copilot-initiator.log
# 查看统计
grep -c "after=user" /tmp/copilot-initiator.log # user 标记次数
grep -c "after=agent" /tmp/copilot-initiator.log # agent 标记次数整个 session 中只有第一条是 user,后续不管是 tool call、compaction、subagent、plan 切换,全部是 agent。total 计数器让你一目了然覆盖比例。
方法 2:看 GitHub 的 Premium Request 用量
- 打开 GitHub Settings → Copilot → Usage
- 记录当前 premium requests 用量
- 在同一个 session 里进行多轮对话(包括触发 tool call、子代理等)
- 刷新用量页面,确认只增加了 1 次 × 模型倍率
方法 3:抓包验证
⚠️ 实测发现 opencode 内置的 fetch 不走系统代理(
HTTPS_PROXY环境变量无效),mitmproxy 无法拦截。如果需要抓包,可以用系统级抓包工具(如 macOS 的 Proxyman 或 Charles 的 system proxy 模式),或者用上面的文件日志方案替代。
一些补充
premium requests 不可能完全降到零:VSCode 的补全、聊天等客户端和 OpenCode 共享配额;每开一个新 session 还是要消耗一次。另外 GPT-5 mini、GPT-4.1、GPT-4o 这些 included models 在付费计划下本身不消耗 premium requests,不受这个影响。
日常建议:能用免费模型就用免费模型,需要质量的时候再切 Claude;不用 VSCode Copilot 扩展的话关掉省点配额;尽量在一个 session 里把事情做完,少开新 session。
总结
| 方案 | 效果 | 风险 |
|---|---|---|
| 不优化(OpenCode v1.2.27 默认) | compaction 和 subagent 已保护,但 task summary、shell 执行、plan 切换、图片注入仍会误扣 | 无 |
| 全标 agent | 理论全免费 | 极高,容易封号 |
| Session 级标记(本文方案) | 每个 session 只消耗 1 次,覆盖全部 6 条 synthetic message 路径 | 有一定风险,本质是投机取巧 |
说实话这个方案也是钻空子。虽然比全标 agent 温和不少,但和官方预期的行为还是有差距——VSCode 里每次新 prompt 都会算一次,我们是把整个 session 压成了一次。X-Initiator 也不是官方公开承诺的稳定接口,GitHub 随时可以加服务端验证来堵住这些漏洞。
这类操作早晚可能被官方堵上,自行评估风险吧。
相关链接:
- GitHub Copilot Billing - Model Multipliers
- GitHub Copilot 官方计费文档
- opencode PR #8721 - Fix excessive premium request consumption(Draft,未合并)
- opencode Issue #8030 - Excessive premium requests
- opencode Issue #8700 - Subagent burns premium requests
- opencode PR #17431 - Compaction tracked as agent initiated(2026-03-14 合入)
- Neovim Copilot 深度分析 - X-Initiator 抓包
- CopilotChat.nvim PR #1520 - 修复 X-Initiator

