Skip to content

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

Description

@nickhou1983

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

分析对象:nickhou1983/opencode(fork,已与上游 anomalyco/opencode 的 dev 完成同步)。
本报告聚焦 GitHub Copilot provider,依据仓库实际代码,给出 鉴权与 Token 交换流程、Request Header 与 Request Body 的详细拆解。所有结论均标注 文件:行号。


0. 本次上游同步提示

本次从上游 anomalyco/opencode 快进/合并进 10 个提交(git log origin/dev..upstream/dev),其中与 Copilot 请求处理 直接相关的一条:

提交 说明 影响
561afb401a fix(opencode): detect Copilot PDF input support (#41522) packages/opencode/src/plugin/github-copilot/models.ts 新增 PDF 能力探测:当模型 capabilities.limits.vision.supported_media_types 含 application/pdf 且 supports.vision=true 时,capabilities.input.pdf=true。→ 决定 Request Body 是否允许携带 PDF 附件。

其余 9 条为 console(zen 文档/go usage 端点)、beta 构建与 DeepSeek 采样默认值等,不影响 Copilot 的请求构造。


1. 关键结论(TL;DR)

  1. 身份验证 = GitHub OAuth 设备流(Device Flow, RFC 8628);OpenCode 使用自有 GitHub App(CLIENT_ID = Ov23li8tweQw6odWQebz),申请 scope 仅 read:user。
  2. 没有二次 Token 交换:与许多 Copilot 客户端不同,OpenCode 不 调用 github.com/ghapi/copilot_internal/v2/token 去换取短时 Copilot Token。它把设备流拿到的 GitHub OAuth access_token 直接作为 Authorization: Bearer <token> 打到 https://api.githubcopilot.com。git log -S copilot_internal 证实该文件历史上从未出现过该端点。
  3. 凭据存储:refresh = access = access_token、expires = 0,写入 ~/.local/share/opencode/auth.json(权限 0600)。因 Copilot auth 未注册 refresh 方法,该 Token 被当作长期有效直接使用。
  4. 请求头 由三层拼装:request.ts(会话层)→ AI SDK combineHeaders(provider 层)→ copilot.ts 的 loader.fetch 包装器(最终覆盖 & 注入鉴权头)。
  5. 请求体 依模型能力路由到三种端点:/chat/completions(OpenAI 兼容)、/responses(GPT‑5 类)、/v1/messages(Claude 类,走 @ai-sdk/anthropic)。

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

2.1 时序图

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

    Note over OC: authorize() copilot.ts:222
    OC->>GH: POST /login/device/code<br/>{client_id, scope:"read:user"}  (copilot.ts:234)
    GH-->>OC: {device_code, user_code,<br/>verification_uri, interval}
    OC-->>U: 显示 verification_uri + user_code (copilot.ts:258)
    U->>GH: 浏览器输入 user_code 授权

    loop 轮询 callback() copilot.ts:262
        OC->>GH: POST /login/oauth/access_token<br/>{client_id, device_code,<br/>grant_type: device_code} (copilot.ts:264)
        GH-->>OC: authorization_pending / slow_down / access_token
    end
    GH-->>OC: {access_token}  (copilot.ts:286)
    Note over OC: 存储 refresh=access=token, expires=0<br/>auth.json (auth/index.ts:14, set:73)

    Note over OC,CAPI: 之后每次推理请求
    OC->>CAPI: GET /models & POST /chat|responses|messages<br/>Authorization: Bearer <github_oauth_token><br/>(loader.fetch copilot.ts:164)
    CAPI-->>OC: 模型列表 / 补全流
Loading

2.2 步骤拆解(标注代码位置)

步骤 说明 代码位置
① 常量 GitHub App CLIENT_ID=Ov23li8tweQw6odWQebz;API_VERSION=2026-06-01 copilot.ts:9-10
② 端点推导 DEVICE_CODE_URL=https://{domain}/login/device/code、ACCESS_TOKEN_URL=https://{domain}/login/oauth/access_token;domain 默认 github.com,企业版取用户输入域名 copilot.ts:19-24, 222-232
③ 申请设备码 POST device/code,body {client_id, scope:"read:user"},头 Accept/Content-Type: application/json、User-Agent: opencode/{version} copilot.ts:234-256
④ 轮询取 Token POST access_token,body {client_id, device_code, grant_type:"urn:ietf:params:oauth:grant-type:device_code"};处理 authorization_pending、slow_down(RFC 8628 §3.5,+5s 或采用服务端 interval)、+3s 安全余量 copilot.ts:262-334
⑤ 存储凭据 type:"oauth",refresh=access=access_token,expires:0;企业版附 enterpriseUrl copilot.ts:286-305
⑥ 落盘 auth.json(Global.Path.data),0o600 auth/index.ts:10, 14-21, 73-81
⑦ 注入调用 loader.fetch 包装器读取 oauth 信息,注入 Authorization: Bearer <token> copilot.ts:100-179

Base URL(copilot.ts:26-28):

  • 公有云:https://api.githubcopilot.com
  • 企业版(Data residency / 自托管):https://copilot-api.<归一化域名>(如 copilot-api.company.ghe.com)

Token 交换的准确表述:OpenCode 对 Copilot 只有一次 OAuth 换取(设备码 → GitHub access_token),不存在「GitHub Token → Copilot 会话 Token」的二次交换;expires:0 + 未注册 refresh ⇒ 直接长期复用该 OAuth Token。(对比:packages/opencode/src/plugin/openai/codex.ts 才有标准 exchangeCodeForTokens 的 refresh 流程。)


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

请求头分三层叠加,后层覆盖前层:

层 A —— 会话层 LLMRequestPrep.prepare(packages/opencode/src/session/llm/request.ts:187-204)

Copilot 属「非 opencode provider」分支:

Header 取值来源 说明
x-session-affinity input.sessionID 会话亲和
X-Session-Id input.sessionID 会话 ID
x-parent-session-id input.parentSessionID(存在时) 子会话/子代理标记
User-Agent opencode/{InstallationVersion} request.ts:18
...input.model.headers 模型级自定义头(Copilot 默认空) models.ts:157
...headers 下方 chat.headers 钩子输出 合并

层 B —— chat.headers 钩子(copilot.ts:360-411)

Header 取值 触发条件
X-GitHub-Api-Version 2026-06-01 始终
X-Interaction-Type agent-session-name-generation agent === "title"(标题生成)
anthropic-beta interleaved-thinking-2025-05-14 模型 api.npm === "@ai-sdk/anthropic"(Claude 走 messages)
x-initiator agent 会话含 compaction / 自动压缩续写 synthetic part,或存在 parentID(子代理)

层 C —— loader.fetch 传输包装器(最终注入,决定线上真实请求头)(copilot.ts:160-173)

Header 取值 说明
Authorization Bearer <github_oauth_token> 鉴权(核心)
User-Agent opencode/{InstallationVersion} 覆盖
Openai-Intent conversation-edits Copilot 后端意图标识
x-initiator agent / user 依 body 末条消息判定:非 user、或图片合成 prompt ⇒ agent(copilot.ts:107-158)
Copilot-Vision-Request true body 检出图片(image_url / input_image / image)时
x-api-key 删除 delete headers["x-api-key"]
authorization(小写) 删除 避免与大写 Authorization 重复

层 D —— AI SDK / provider 基础头

  • combineHeaders(this.config.headers(), options.headers):chat 见 openai-compatible-chat-language-model.ts:206,323;responses 见 openai-responses-language-model.ts:406,785。
  • config.headers() 来自 createOpenaiCompatible:{...(apiKey?{Authorization}:{}), ...options.headers} 再 withUserAgentSuffix(..., "ai-sdk/openai-compatible/0.1.0")(copilot-provider.ts:60-66)。因 Copilot apiKey="",此处不产生鉴权头,最终由层 C 注入。
  • Content-Type: application/json 由 postJsonToApi 自动添加。

GET /models(拉取模型列表)的请求头(copilot.ts:70-79 → models.ts:216-229)

Authorization: Bearer <token>、User-Agent: opencode/{version}、X-GitHub-Api-Version: 2026-06-01,附上 provider.options.headers;5 秒超时。


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

4.1 端点路由(决定 Body 形态)

models.ts:95-114 依 /models 返回的 supported_endpoints 决定:

上游端点 OpenCode api.npm api.endpoint Body 协议
/v1/messages @ai-sdk/anthropic,url = {base}/v1 messages Anthropic Messages
/responses @ai-sdk/github-copilot responses OpenAI Responses
/chat/completions @ai-sdk/github-copilot chat OpenAI Chat

进一步在 core/src/plugin/provider/github-copilot.ts:27-50 与 llm/src/providers/github-copilot.ts:19-25 决定:gpt-<N> 且 N≥5、非 gpt-5-mini ⇒ 走 /responses,否则 /chat。

请求前经 chat.params 钩子(copilot.ts:340-354)调整:

  • GPT 模型 ⇒ maxOutputTokens=undefined(对齐官方 Copilot CLI);
  • Anthropic 模型 ⇒ toolStreaming=false(Copilot 的 /v1/messages shim 拒绝 eager_input_streaming 字段)。

4.2 /chat/completions Body(openai-compatible-chat-language-model.ts:139-189, 308-314)

{
  "model": "<api.id>",
  "user": "<可选>",
  "max_tokens": <maxOutputTokens>,        // GPT 被 chat.params 置空
  "temperature": <num>, "top_p": <num>,
  "frequency_penalty": <num>, "presence_penalty": <num>,
  "response_format": { "type": "json_object" | "json_schema", ... }, // 可选
  "stop": [...], "seed": <num>,
  "reasoning_effort": "<low|medium|high|...>",  // 推理模型
  "verbosity": "<low|medium|high>",
  "messages": [ /* 见 4.5 */ ],
  "tools": [...], "tool_choice": ...,
  "thinking_budget": <num>,
  "stream": true,                          // doStream
  "stream_options": { "include_usage": true } // 仅严格兼容模式
}

URL:POST {base}/chat/completions(:203, :320)。

4.3 /responses Body(openai-responses-language-model.ts:253-390, 782-788)

{
  "model": "<api.id>",
  "input": [ /* convertToOpenAIResponsesInput 结构化输入 */ ],
  "temperature": <num>, "top_p": <num>,   // 推理模型会被剔除(:311-330)
  "max_output_tokens": <num>,
  "text": { "format": {...}, "verbosity": "..." }, // 可选
  "max_tool_calls": <num>, "metadata": {...},
  "parallel_tool_calls": <bool>,
  "previous_response_id": "<id>",
  "store": true,                           // 默认 true(:206, :285)
  "instructions": "<system>", "service_tier": "...",
  "include": ["reasoning.encrypted_content", ...],
  "prompt_cache_key": "...", "safety_identifier": "...",
  "reasoning": { "effort": "...", "summary": "auto" }, // 推理模型
  "truncation": "auto",                    // 需要时
  "tools": [...], "tool_choice": ...,
  "stream": true                            // doStream
}

URL:POST {base}/responses(:396, :782)。

4.4 /v1/messages Body(Claude 类)

由 @ai-sdk/anthropic 构造标准 Anthropic Messages Body,base 为 {copilot_base}/v1;OpenCode 侧仅通过钩子附加 anthropic-beta 头、关闭 toolStreaming,不重写 body 结构。

4.5 消息体转换(convert-to-openai-compatible-chat-messages.ts)

角色 处理 位置
system 原样 {role,content} + copilot 元数据 :18-25
user 纯文本走 content:string;多模态映射为数组:text / image_url(URL 或 data:<mime>;base64,...) :27-71
assistant 聚合 text、reasoning_text、reasoning_opaque、tool_calls[{id,type:function,function:{name,arguments}}] :73-126
tool {role:"tool", tool_call_id, content};JSON 类输出 JSON.stringify :128-160

图片 media type 为 image/* 时回退 image/jpeg(:47);非图片文件抛 UnsupportedFunctionalityError(:59-63)。

工具兜底:Copilot 在「回放历史工具调用但当前无启用工具」时,request.ts:159-175 注入一个永不调用的 _noop 工具,以满足 Copilot 对 tools 字段的要求。


5. 关键代码位置索引

  • 鉴权/设备流/Token/传输头:packages/opencode/src/plugin/github-copilot/copilot.ts
    • 常量 :9-10;base :26-28;authorize :222-256;callback 轮询 :262-334;凭据存储 :286-305
    • loader.fetch 头注入 :100-179;chat.params :340-354;小模型 :355-359;chat.headers :360-411
  • 凭据存储 schema:packages/opencode/src/auth/index.ts:10, 14-21, 73-89
  • 模型/端点路由/PDF 能力:packages/opencode/src/plugin/github-copilot/models.ts:82-205, 216-259
  • 会话层请求头/系统提示/工具:packages/opencode/src/session/llm/request.ts:56-206
  • OpenAI 兼容 provider 工厂:packages/core/src/github-copilot/copilot-provider.ts:52-97
  • Chat 语言模型(Body/URL):packages/core/src/github-copilot/chat/openai-compatible-chat-language-model.ts:87-329
  • Chat 消息转换:packages/core/src/github-copilot/chat/convert-to-openai-compatible-chat-messages.ts:13-170
  • Responses 语言模型(Body/URL):packages/core/src/github-copilot/responses/openai-responses-language-model.ts:152-420, 778-788
  • V2 core 插件(responses vs chat 选择):packages/core/src/plugin/provider/github-copilot.ts:6-52
  • LLM 路由库:packages/llm/src/providers/github-copilot.ts:8-69

6. 一句话总结

OpenCode 走 GitHub 设备流拿一次 OAuth Token 并直接当 Bearer 打到 api.githubcopilot.com(无二次 Token 交换);请求头由 request.ts → AI SDK → loader.fetch 三层叠加,最终由传输包装器注入 Authorization、Openai-Intent、x-initiator、X-GitHub-Api-Version、Copilot-Vision-Request 等 Copilot 专属头;请求体按模型能力路由到 /chat/completions、/responses 或 /v1/messages 三种协议分别构造。

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions