Skip to content

[opencode 请求流程分析] OpenCode → GitHub Copilot 请求分析:身份验证/Token 交换、Request Header 与 Request Body(2026-08-03) #24

Description

@nickhou1983

OpenCode → GitHub Copilot 请求分析:身份验证 / Token 交换、Request Header 与 Request Body(2026-08-03)

只读分析,基于当前仓库 dev 分支代码(已与上游 anomalyco/opencode 完成同步)。所有结论以仓库实际代码为准,并标注 path:line

0. 本次同步变更提示

本次从上游 anomalyco/opencode 快进同步进来 3 个提交:

SHA 说明 是否影响 Copilot 请求流程
1882c33827 fix gemini reasoning tokens not counted 否(仅改 packages/console/app/src/routes/zen/util/provider/google.ts,属 console/zen 计费侧)
01624c8c87 chore: generate 否(生成物)
0891ecd8e0 docs(go): update DeepSeek privacy policy (anomalyco#40120) 否(文档 / i18n)

结论:本次同步未触及 GitHub Copilot 的鉴权、Header 或 Body 构造逻辑。 以下分析对同步前后一致有效。

1. 关键结论(TL;DR)

  • 鉴权用的是 GitHub OAuth「设备码流程」拿到的 access token,并被“原样”作为 Authorization: Bearer 直连 https://api.githubcopilot.com
  • 不存在二次 Token 交换:仓库中没有 copilot_internal/v2/token 之类的换取端点,github-copilot 的 loader没有任何 refresh 逻辑(与 xai.ts / codex.ts / snowflake-cortex.ts 形成鲜明对比)。token 以 expires: 0 落盘,长期复用。
  • Header 分三层叠加request.ts 通用层 → chat.headers 插件钩子层 → loader.fetch 最终注入层(Authorization / User-Agent / Openai-Intent 在这里定型,并删除小写 authorization / x-api-key)。
  • Body 由 AI SDK 按端点构造,Copilot 三条协议线:/chat/completions(OpenAI 兼容)、/responses(GPT‑5 类)、/v1/messages(Anthropic 线)。chat.params 会对 gpt 模型去掉 max_tokens、对 anthropic 线关闭 toolStreaming
  • OpenCode 不发送官方编辑器插件那套 Copilot-Integration-Id / Editor-Version / Editor-Plugin-Version,仅用 User-Agent: opencode/<version> + X-GitHub-Api-Version + x-initiator + Openai-Intent 等最小集合标识自己。

2. GitHub 身份验证与 Token 交换流程

核心文件:packages/opencode/src/plugin/github-copilot/copilot.ts
凭据存储:packages/opencode/src/auth/index.ts

2.1 常量与端点

位置
OAuth App Client ID Ov23li8tweQw6odWQebz copilot.ts:9
API 版本 2026-06-01 copilot.ts:10
设备码端点 https://{domain}/login/device/code copilot.ts:21
令牌端点 https://{domain}/login/oauth/access_token copilot.ts:22
推理 API Base https://api.githubcopilot.com(企业版:https://copilot-api.{enterprise} copilot.ts:26-28
OAuth Scope read:user copilot.ts:244
Grant Type urn:ietf:params:oauth:grant-type:device_code copilot.ts:274

domain 默认 github.com;选择「GitHub Enterprise」时由用户输入的 URL 规范化而来(normalizeDomaincopilot.ts:15-17,225-230)。

2.2 时序图

sequenceDiagram
    autonumber
    participant U as 用户
    participant OC as OpenCode (copilot.ts)
    participant GH as github.com (OAuth)
    participant CAPI as api.githubcopilot.com

    U->>OC: 选择 /login GitHub Copilot(github.com 或 Enterprise)
    OC->>GH: POST /login/device/code {client_id, scope:"read:user"}
    GH-->>OC: {device_code, user_code, verification_uri, interval}
    OC-->>U: 展示 user_code + 打开 verification_uri
    loop 轮询直到授权(间隔 interval + 3s 安全余量)
        OC->>GH: POST /login/oauth/access_token {client_id, device_code, grant_type}
        GH-->>OC: authorization_pending / slow_down / {access_token}
    end
    Note over OC: 落盘 auth.json:{refresh=access_token, access=access_token, expires:0}
    OC->>CAPI: GET /models  (Authorization: Bearer <access_token>, X-GitHub-Api-Version, User-Agent)
    CAPI-->>OC: 模型清单 → 决定 chat/responses/messages 端点与定价
    OC->>CAPI: POST /chat/completions | /responses | /v1/messages(推理请求,仍用同一 Bearer)
Loading

2.3 关键步骤说明

  1. 发起设备码copilot.ts:234-256):POST /login/device/code,body {client_id, scope:"read:user"},请求头带 Accept: application/jsonContent-Type: application/jsonUser-Agent: opencode/<version>
  2. 轮询换取 access tokencopilot.ts:262-334):POST /login/oauth/access_token。处理 authorization_pending(继续轮询)、slow_down(按 RFC 8628 §3.5 增加 5s 或采用服务端返回的 interval)、其它 error(失败)。轮询间隔统一加 OAUTH_POLLING_SAFETY_MARGIN_MS = 3000copilot.ts:14)以规避时钟漂移。
  3. 落盘copilot.ts:286-305):成功后返回 {type:"success", refresh: access_token, access: access_token, expires: 0};企业版额外写入 enterpriseUrl。凭据经 Auth.set 写入 ~/.local/share/opencode/auth.json,权限 0o600auth/index.ts:73-81)。Oauth 结构见 auth/index.ts:14-21refresh / access / expires / accountId? / enterpriseUrl?)。
  4. 拉取模型清单copilot.ts:62-92models.ts:213-256):以 Authorization: Bearer <access> + X-GitHub-Api-Version + User-Agent 请求 {base}/models,据 supported_endpoints 决定每个模型走 messages / responses / chatmodels.ts:92-99),并换算定价、能力、变体。

2.4 「Token 交换」的真相:无二次换取

很多 Copilot 客户端会把 GitHub OAuth token 再向 https://api.github.com/copilot_internal/v2/token 换一个短期 Copilot token。OpenCode 没有这一步

  • 全仓 grep copilot_internal / v2/token / Copilot-Integration-Id 均无命中。
  • github-copilot 的 loadercopilot.ts:96-181)拿到 getAuth()直接 Authorization: Bearer ${info.access}copilot.ts:164),既不判断 expires 也不 refresh。
  • 对照组:xai.ts:462-524openai/codex.ts:322-388snowflake-cortex.ts:286-366 都实现了 refreshAccessToken + 单飞刷新;github-copilot 完全没有。这解释了为何落盘时 expires: 0 —— 该字段对 Copilot 分支不参与任何过期判定,GitHub OAuth token 被长期直用。

影响:凭据轮换/失效完全依赖 GitHub 侧;一旦该 access token 失效,只能重新走 /login 设备码流程。


3. 客户端发送给 GitHub Copilot 的 Request Header

3.1 叠加管线(三层)

flowchart LR
    A["request.ts 通用基座头<br/>x-session-affinity / X-Session-Id / User-Agent"]
      --> B["chat.headers 钩子(copilot.ts:360-412)<br/>X-GitHub-Api-Version / x-initiator / anthropic-beta / X-Interaction-Type"]
      --> C["AI SDK config.headers()<br/>+ withUserAgentSuffix ai-sdk/openai-compatible/VERSION(copilot-provider.ts:66)"]
      --> D["loader.fetch 最终注入(copilot.ts:160-178)<br/>Authorization / User-Agent / Openai-Intent / Copilot-Vision-Request;删除小写 authorization、x-api-key"]
      --> E[[api.githubcopilot.com]]
Loading
  • 第 1 层request.ts:187-204):非 opencode provider 走 else 分支,注入 x-session-affinityX-Session-Id、可选 x-parent-session-idUser-AgentUSER_AGENT = opencode/${InstallationVersion}request.ts:18),再并入 input.model.headers 与钩子产出的 headers
  • 第 2 层chat.headerscopilot.ts:360-412):仅当 providerIDgithub-copilot 时生效。
  • 第 3 层loader.fetchcopilot.ts:100-179):真正发出前的最终改写,Authorization 在此定型。

3.2 最终发出的 Header 明细

Header 取值 来源(代码位置) 作用
Authorization Bearer <GitHub OAuth access token> copilot.ts:164(推理)/ copilot.ts:74(/models) 鉴权凭据(原样直用,无二次交换)
User-Agent opencode/<version> copilot.ts:163request.ts:18version.ts:6 客户端标识(最终以 opencode 版本为准,覆盖 AI SDK 后缀)
X-GitHub-Api-Version 2026-06-01 copilot.ts:363(钩子)/ copilot.ts:76(/models) 指定 Copilot API 版本
x-initiator agentuser copilot.ts:161(body 探测)+ copilot.ts:394,411(compaction/子会话强制 agent) 标记本轮由用户还是 agent 发起(计费/风控口径)
Openai-Intent conversation-edits copilot.ts:165 声明调用意图
Copilot-Vision-Request true(仅含图片时) copilot.ts:168-170 声明多模态/视觉请求
X-Interaction-Type agent-session-name-generation(仅 title agent) copilot.ts:364-366 标记「会话标题生成」这类轻量交互
anthropic-beta interleaved-thinking-2025-05-14(仅 @ai-sdk/anthropic 线) copilot.ts:368-370 /v1/messages 时开启交错思考
x-session-affinity / X-Session-Id 当前 sessionID request.ts:197-198 会话亲和 / 路由
x-parent-session-id 父会话 ID(子会话时) request.ts:199 子会话溯源

清理动作(copilot.ts:172-173):发出前 delete headers["x-api-key"]delete headers["authorization"](小写),避免其它 provider 的鉴权头串入,确保只保留大写 Authorization

注:AI SDK 的 config.headers() 会用 withUserAgentSuffix 追加 ai-sdk/openai-compatible/<VERSION>copilot-provider.ts:66),但随后被 loader.fetchopencode/<version> 覆盖(copilot.ts:163),故最终 User-Agent 以 opencode 版本为准。

3.3 x-initiator 的判定(body 探测)

loader.fetch 会解析 init.body,按三种 API 形态判断 isAgent / isVisioncopilot.ts:107-158):

  • Completionsbody.messages + url 含 completions):最后一条非 user,或为「合成附件提示」→ agent;任一消息含 image_url → vision。
  • Responsesbody.input):最后一项非 user 或合成附件 → agent;含 input_image → vision。
  • Messagesbody.messages,Anthropic 线):最后一条不是「纯 user 文本」或为合成附件 → agent;含 image(含嵌套在 tool_result 里的图片)→ vision。

此外 chat.headers 钩子会把「压缩续写(compaction)」与「子会话(有 parentID)」强制标为 x-initiator: agentcopilot.ts:385-411)。合成附件提示常量为 MessageV2.SYNTHETIC_ATTACHMENT_PROMPTcopilot.ts:31-43)。


4. 客户端发送给 GitHub Copilot 的 Request Body

4.1 端点 / 协议路由

/models 返回的 supported_endpoints 决定(models.ts:92-111):

Copilot 端点 AI SDK 包(api.npm 触发条件 最终 URL
messages @ai-sdk/anthropic supported_endpoints/v1/messages {base}/v1/messagesapi.url = {base}/v1models.ts:109
responses @ai-sdk/github-copilot /responses {base}/responses
chat @ai-sdk/github-copilot /chat/completions {base}/chat/completions

@ai-sdk/github-copilot,GPT‑5 类模型(非 gpt-5-mini)优先走 Responses、否则 Chat(core/src/plugin/provider/github-copilot.ts:42-48);并隐藏与 Responses 冲突的 gpt-5-chat-latest 别名(同文件 :9-19)。

4.2 Chat Completions 请求体

构造于 packages/core/src/github-copilot/chat/openai-compatible-chat-language-model.ts:139-189getArgs):

字段 来源
model this.modelId
messages convertToOpenAICompatibleChatMessages(prompt):179
max_tokens maxOutputTokens(gpt 模型会被 chat.params 置空,见 4.5)
temperature / top_p 标准采样参数
frequency_penalty / presence_penalty 惩罚项
response_format JSON 时 json_schema(支持结构化输出)或 json_object:153-165
stop / seed 停止序列 / 随机种子
reasoning_effort / verbosity 由 provider options 解析(:175-176
thinking_budget 思考预算(:186
tools / tool_choice prepareTools(...) 产出(:130-137,182-183
user provider options(:145

流式(doStream:308-314)在上述基础上追加 stream: true,并在严格兼容模式下带 stream_options: { include_usage: true };请求发往 POST {base}/chat/completions:203-204,320-321)。

4.3 Responses 请求体

构造于 packages/core/src/github-copilot/responses/openai-responses-language-model.ts:254-388modelinput(非 messages,为 Responses 事件项数组)、temperaturetop_pmax_output_tokenstextformat/verbosity)、store默认 true:206,285)、userinstructionsservice_tierincludereasoning{ effort, summary }:297-299)、toolstool_choice,流式 stream: true:788)。

4.4 消息形态与 Messages(Anthropic)线

  • 系统提示注入:非 OpenAI‑OAuth / 非 workflow 场景,系统提示以 role:"system" 前置到消息数组(request.ts:101-112);首段+其余会被压成两段(request.ts:74-78)。
  • Anthropic 线api.npm === "@ai-sdk/anthropic" 时由 @ai-sdk/anthropic 构造 /v1/messages 请求体,并叠加 anthropic-beta: interleaved-thinking-2025-05-14 头(copilot.ts:368-370)。

4.5 针对 Copilot 的 Body 级调整

chat.params 钩子(copilot.ts:340-353):

  1. gpt 模型去掉 maxOutputTokensoutput.maxOutputTokens = undefined)——对齐官方 GitHub Copilot CLI 行为(:344-346)。
  2. Anthropic 线关闭 toolStreaming:351-353)——Copilot 的 /v1/messages shim 会拒绝工具定义里的 eager_input_streaming 字段("Extra inputs are not permitted"),故关闭以避免注入。

request.ts 侧的 Copilot 专属处理:当无启用工具但历史里存在工具调用回放时,注入一个 _noop 占位工具(request.ts:159-175),因为 Copilot 在回放工具调用时要求 tools 字段存在。小工具模型(标题生成)用 experimental.provider.small_modelUTILITY_MODELSgpt-5.4-nano / gpt-4.1 / gpt-4o / gpt-4o-mini)中挑选(copilot.ts:11,355-359)。


5. 关键代码位置索引

主题 文件:行
OAuth 设备码流程 / 常量 / loader.fetch / 钩子 packages/opencode/src/plugin/github-copilot/copilot.ts(9-28, 96-181, 222-336, 340-412)
凭据存储(auth.json,0600) packages/opencode/src/auth/index.ts(14-21, 58-92)
模型清单拉取与端点路由 packages/opencode/src/plugin/github-copilot/models.ts(82-111, 213-256)
请求装配:基座头 / 系统提示 / chat.params / chat.headers / _noop 工具 packages/opencode/src/session/llm/request.ts(18, 60-146, 159-205)
Chat Completions 请求体与传输 packages/core/src/github-copilot/chat/openai-compatible-chat-language-model.ts(87-210, 306-330)
Responses 请求体 packages/core/src/github-copilot/responses/openai-responses-language-model.ts(254-388, 780-790)
Copilot provider 装配(chat/responses 选择、隐藏别名、SDK 装配) packages/core/src/plugin/provider/github-copilot.ts(6-51)
AI SDK provider 配置(config.headers/url、UA 后缀) packages/core/src/github-copilot/copilot-provider.ts(53-82)
版本常量 opencode/<version> packages/core/src/installation/version.ts(6)

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions