Skip to content

[opencode 请求流程分析] OpenCode 发送给 GitHub Copilot 的请求分析(2026-07-22) #13

Description

@nickhou1983

OpenCode 发送给 GitHub Copilot 的请求分析(2026-07-22)

只读分析,未修改任何代码。本次已将 fork 的 dev 与上游 anomalyco/opencode 同步(快进 10 个提交,最新 4438f69 zen: add security check)。

本次变更提示:同步进来的 10 个提交(zen 模型新增、chore: generatefix(app) 斜杠命令补全、fix(ui) 进度环、feat(app) 可拖拽项目行、Mistral tool-call-id 归一化等)均未触及 GitHub Copilot 的请求流程 / Header / Body,因此下述结论相对上一版无实质变化。

分析对象为 opencode 运行时(packages/opencode)中的 GitHub Copilot 集成。核心实现集中在 packages/opencode/src/plugin/github-copilot/copilot.ts(鉴权 + 自定义 fetch)与 models.ts(模型发现 + 端点路由),请求组装在 session/llm/request.tssession/llm.ts,Body 变换在 provider/transform.ts


一、GitHub 身份验证与 Token 交换流程

1.1 结论先行

opencode 采用 GitHub OAuth Device Flow(设备码流程) 获取一个 GitHub OAuth access_token,随后直接把该 token 作为 Authorization: Bearer <token> 发给 Copilot API。

关键点:opencode 不做 “GitHub token → 短时效 Copilot token” 的二次交换(即不请求 api.github.com/copilot_internal/v2/token)。对比同仓库的 codex.tsxai.tssnowflake-cortex.ts 都实现了 refreshAccessToken(...),而 github-copilot 插件没有任何刷新/交换逻辑——设备码换来的 GitHub OAuth token 被当作长期凭据直接使用(存储时 expires: 0copilot.ts:298)。

1.2 设备码换取流程(copilot.ts:222-336

  • 客户端标识 CLIENT_ID = "Ov23li8tweQw6odWQebz"copilot.ts:9),API 版本 API_VERSION = "2026-06-01"copilot.ts:10)。
  • 支持两种部署(copilot.ts:186-221):GitHub.comdomain = github.com)与 GitHub Enterprise(用户填入企业域名,normalizeDomain 归一化,copilot.ts:15-17,227-230)。
  • 端点由域名拼接(copilot.ts:19-24):
    • 设备码:https://<domain>/login/device/code
    • 换 token:https://<domain>/login/oauth/access_token

步骤:

  1. 请求设备码copilot.ts:234-256):POST /login/device/code,body { client_id, scope: "read:user" },Header Accept: application/jsonContent-Type: application/jsonUser-Agent: opencode/<version>。返回 verification_uriuser_codedevice_codeinterval
  2. 提示用户在浏览器打开 verification_uri 并输入 user_codecopilot.ts:258-261)。
  3. 轮询换取 tokencopilot.ts:262-334):循环 POST /login/oauth/access_token,body { client_id, device_code, grant_type: "urn:ietf:params:oauth:grant-type:device_code" }
    • 拿到 access_token 即成功,存储为 { type:"success", refresh: access_token, access: access_token, expires: 0, enterpriseUrl? }copilot.ts:286-306)——注意 refreshaccess 是同一个 GitHub OAuth token。
    • 处理 authorization_pending(按 interval 等待重试)、slow_down(按 RFC 8628 增加 5s,copilot.ts:313-327),并统一加了 OAUTH_POLLING_SAFETY_MARGIN_MS = 3000 的时钟漂移安全余量(copilot.ts:14)。

1.3 凭据注入(自定义 fetchcopilot.ts:96-181

auth.loader 返回一个覆盖版 fetch;仅当 auth.type === "oauth" 时生效(否则透传原生 fetchcopilot.ts:103-104)。它对每个发往 Copilot 的请求:

  • 解析 body,判定 isVision / isAgent(兼容 3 种 API 形态:Completions / Responses / Messages,copilot.ts:107-158)。
  • 组装 Header(copilot.ts:160-166),设置 Authorization: Bearer <access>copilot.ts:164access 即 OAuth token,见 auth-override.test.ts:60 断言 loader 返回 { access: 'test-token' })。
  • 删除可能冲突的 x-api-keyauthorization(小写)(copilot.ts:172-173)。

1.4 模型发现也走同一凭据(copilot.ts:62-92models.ts:213-256

登录后 provider.models(...)GET <base>/modelsmodels.ts:218),Header 带 Authorization: Bearer <access>User-Agent: opencode/<version>X-GitHub-Api-Version: 2026-06-01copilot.ts:73-77)。用返回的 capabilities 动态构建模型、价格、reasoning variants,并据 model_picker_enabled 过滤选择器可见模型(models.ts:254)。请求失败或未登录时回退到静态模型表(copilot.ts:86-91)。

Base URLcopilot.ts:26-28):公有云 https://api.githubcopilot.com;企业版 https://copilot-api.<企业域名>

sequenceDiagram
    participant U as 用户
    participant OC as opencode (copilot.ts)
    participant GH as github.com (OAuth)
    participant API as api.githubcopilot.com

    U->>OC: auth login (github-copilot)
    OC->>GH: POST /login/device/code {client_id, scope:read:user}
    GH-->>OC: device_code, user_code, verification_uri, interval
    OC-->>U: 打开 verification_uri, 输入 user_code
    loop 轮询 (interval + 3s)
        OC->>GH: POST /login/oauth/access_token {device_code, grant_type}
        GH-->>OC: access_token (或 authorization_pending / slow_down)
    end
    Note over OC: 存储 access=refresh=access_token, expires=0 (无二次交换)
    OC->>API: GET /models  (Authorization: Bearer <token>)
    API-->>OC: 模型 capabilities
    U->>OC: 发送 prompt
    OC->>API: POST /chat/completions | /responses | /v1/messages
    Note right of API: 自定义 fetch 注入 Bearer + Copilot 专属 Header
    API-->>OC: 流式响应 (SSE)
Loading

二、客户端发送给 GitHub Copilot 的 Request Header

最终 Header 由两处叠加而成:

  1. session/llm/request.ts:187-204 组装基础 Header 并传给 streamText({ headers })llm.ts:322)。
  2. copilot.ts 的自定义 fetch(160-173) 在真正发出 HTTP 前追加/覆盖 Copilot 专属 Header。
  3. 期间 chat.headers 钩子(copilot.ts:360-412,经 request.ts:134-146 触发) 注入版本/场景类 Header。

叠加优先级:fetch 里先放默认 x-initiator,再 ...init.headers(即 chat.headers 的结果覆盖之),最后强制写死 User-AgentAuthorizationOpenai-Intentcopilot.ts:160-166)。

Header 取值 来源(代码位置) 作用 / 说明
Authorization Bearer <GitHub OAuth token> copilot.ts:164(发现模型时 copilot.ts:74 鉴权;直接用 OAuth access token,无二次交换
User-Agent opencode/<InstallationVersion> copilot.ts:163request.ts:18,194,200 客户端标识(强制覆盖)
Openai-Intent conversation-edits copilot.ts:165 声明调用意图(对齐 Copilot CLI 行为)
X-GitHub-Api-Version 2026-06-01 copilot.ts:363(发现模型时 copilot.ts:76 Copilot API 版本;由 chat.headers 注入
x-initiator agentuser copilot.ts:161(body 推断)、copilot.ts:394,411(compaction/子代理强制 agent 标识请求由 agent 还是用户发起
Copilot-Vision-Request true copilot.ts:168-170 仅当消息含图片时添加(视觉请求)
X-Interaction-Type agent-session-name-generation copilot.ts:364-366 仅标题生成 agent(incoming.agent === "title"
anthropic-beta interleaved-thinking-2025-05-14 copilot.ts:368-370 仅当模型走 @ai-sdk/anthropic(Claude 经 /v1/messages
x-session-affinity / X-Session-Id <sessionID> request.ts:197-198 会话亲和/标识(非 opencode 网关分支)
x-parent-session-id <parentSessionID> request.ts:199 子代理会话才有
Content-Type / Accept application/json / SSE AI SDK(@ai-sdk/github-copilot@ai-sdk/anthropic)自动设置 由传输层补齐

值得注意:opencode 不发送 VS Code 版 Copilot 常见的 Copilot-Integration-IdEditor-VersionEditor-Plugin-Version 等 Header(全仓库 grep 无匹配),而是用 User-Agent: opencode/... + Openai-Intent + X-GitHub-Api-Version 来标识自身。


三、客户端发送给 GitHub Copilot 的 Request Body

3.1 端点路由(决定 Body 形态,models.ts:92-111

opencode 根据 Copilot /models 返回的 supported_endpoints 为每个模型选定端点与 AI SDK 适配包:

Copilot 端点 opencode endpoint api.npm api.url Body 形态
/v1/messages messages @ai-sdk/anthropic <base>/v1models.ts:109 Anthropic Messages
/responses responses @ai-sdk/github-copilot <base> OpenAI Responses
/chat/completions chat @ai-sdk/github-copilot <base> OpenAI Chat Completions

GPT-5 及以上(非 gpt-5-mini)默认走 Responses,其余走 Chat(llm/providers/github-copilot.ts:19-25shouldUseResponsesApi)。Body 主体由对应 AI SDK 适配包序列化,opencode 再叠加下述变换。

3.2 Body 主要字段(按端点)

  • Chat Completions{ model, messages, tools?, tool_choice?, temperature?, top_p?, max_tokens?/max_completion_tokens?, stream: true, stream_options, reasoning_effort? }
  • Responses{ model, input, tools?, tool_choice?, temperature?, max_output_tokens?, reasoning:{ effort, summary:"auto" }, include:["reasoning.encrypted_content"], store: false, stream: true }
  • Messages(Anthropic){ model, messages, system?, tools?, max_tokens, thinking?, stream: true }

(opencode 用 streamText 发起,故均为流式;并对 Copilot 打开 includeRawChunks,以从 provider 专属字段读取真实计费额度,llm.ts:295。)

3.3 opencode 对 Body 的关键变换

变换 说明 代码位置
GPT 模型省略 maxOutputTokens 对齐 Copilot CLI,gpt* 模型不发 max_tokens copilot.ts:343-346chat.params 钩子)
Anthropic 关闭 toolStreaming Copilot 的 /v1/messages shim 拒绝 eager_input_streaming,故关闭 copilot.ts:348-353
store = false Copilot / OpenAI 系一律不在服务端存储(Responses) transform.ts:1122-1130(含 @ai-sdk/github-copilot,行 1125)
剥离 Responses item ID store !== true 时删除 itemId,保持已签名请求体不可变 transform.ts:474-486
Reasoning / thinking 选项 Responses:reasoningEffort + reasoningSummary:"auto" + include: encrypted;Claude:仅 reasoningEffort(过滤掉 max/xhightransform.ts:940-946);Gemini:Copilot 目前只回 thinking(不设) transform.ts:847-871,1693-1697
提示缓存 providerOptions.copilot.copilot_cache_control = { type: "ephemeral" } transform.ts:352-354sdkKey 映射见 44-45)
注入 _noop 工具 重放历史 tool 调用但当前无启用工具时,Copilot 仍要求 tools 字段非空 request.ts:159-167

Body 组装的调用点见 session/llm.ts:280-324streamText({ providerOptions: ProviderTransform.providerOptions(...), tools, toolChoice, maxOutputTokens, messages, headers, model })


四、关键代码位置索引

  • packages/opencode/src/plugin/github-copilot/copilot.ts
    • CLIENT_ID/API_VERSION/base():9-10、26-28
    • 模型发现 + 鉴权头:62-92
    • 自定义 fetch(Header 注入 / 视觉 / 清理):96-181
    • 设备码 OAuth 流程:222-336
    • chat.params(Body)钩子:340-354
    • chat.headers(Header)钩子:360-412
  • packages/opencode/src/plugin/github-copilot/models.ts:端点路由 92-111、GET /models 213-256
  • packages/opencode/src/session/llm/request.ts:Header 组装 187-204、钩子触发 114/134-146、_noop 工具 159-167
  • packages/opencode/src/session/llm.tsstreamText 280、includeRawChunks 295、providerOptions/headers 316/322
  • packages/opencode/src/provider/transform.tssdkKey 44-45、缓存 352-354、剥离 itemId 474-486、store=false 1122-1130、reasoning 847-871/940-946/1693-1697
  • packages/opencode/test/plugin/auth-override.test.ts:60:验证 loader 使用 access 字段
  • packages/llm/src/providers/github-copilot.ts:19-25shouldUseResponsesApi

本报告由 opencode 请求流程分析任务自动生成,基于同步后的 dev(HEAD 对齐 upstream 4438f69)实际代码,未臆测。

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions