众所周知,GitHub Copilot 付费订阅的计费方式不是按 token 用量计费的,而是按一个叫做 premium requests 的次数来计费。而且这个次数还会根据不同模型乘以不同的权重倍率——比如 Claude Opus 4.5 每次请求乘以 3x,用起来刷刷地掉。

那到底什么请求才算一次 premium request?

X-Initiator:一个重要信号,不是公开契约

根据 opencode#8721opencode#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
Continuationagent
子代理调用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.fetchcopilot.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 hookcopilot.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-525task 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-290compaction 后 replay/continueisAgent=false✅ compaction 补丁兜底✅ 已保护
compaction.ts:309-318compaction 创建入口isAgent=false✅ compaction 补丁兜底✅ 已保护
plan.ts:46-64, 104-113plan 工具切换 agent(“The plan has been approved…“)isAgent=false⚠️ 会误扣
message-v2.ts:763-765tool result 含图片时注入isAgent=false⚠️ 会误扣

结论:v1.2.27 内置逻辑只保护了 compaction 和 subagent session(③④),还有 4 条路径会误扣。 其中 ① task tool summary 在长 session 中触发频率最高。

相关 Issue / PR:

激进方案:全标 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.logappendFileSync),不走 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}

  1. ~/.config/opencode/
  2. 项目/.opencode/
  3. ~/.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:证明内置 CopilotAuthPluginchat.headers hook 在普通请求路径上不设置 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 切换,全部是 agenttotal 计数器让你一目了然覆盖比例。

方法 2:看 GitHub 的 Premium Request 用量

  1. 打开 GitHub Settings → Copilot → Usage
  2. 记录当前 premium requests 用量
  3. 在同一个 session 里进行多轮对话(包括触发 tool call、子代理等)
  4. 刷新用量页面,确认只增加了 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 随时可以加服务端验证来堵住这些漏洞。

这类操作早晚可能被官方堵上,自行评估风险吧。


相关链接: