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)
身份验证 = GitHub OAuth 设备流(Device Flow, RFC 8628) ;OpenCode 使用自有 GitHub App(CLIENT_ID = Ov23li8tweQw6odWQebz),申请 scope 仅 read:user。
没有二次 Token 交换 :与许多 Copilot 客户端不同,OpenCode 不 调用 api.github.com/copilot_internal/v2/token 去换取短时 Copilot Token。它把设备流拿到的 GitHub OAuth access_token 直接作为 Authorization: Bearer <token> 打到 https://api.githubcopilot.com。git log -S copilot_internal 证实该文件历史上从未出现过该端点。
凭据存储 :refresh = access = access_token、expires = 0,写入 ~/.local/share/opencode/auth.json(权限 0600)。因 Copilot auth 未注册 refresh 方法,该 Token 被当作长期有效直接使用。
请求头 由三层拼装:request.ts(会话层)→ AI SDK combineHeaders(provider 层)→ copilot.ts 的 loader.fetch 包装器(最终覆盖 & 注入鉴权头 )。
请求体 依模型能力路由到三种端点:/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)
URL:POST {base}/chat/completions(:203, :320)。
4.3 /responses Body(openai-responses-language-model.ts:253-390, 782-788)
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 三种协议分别构造。
OpenCode → GitHub Copilot 请求流程分析:身份验证 / Token、Request Header 与 Request Body(2026-08-12)
0. 本次上游同步提示
本次从上游
anomalyco/opencode快进/合并进 10 个提交(git log origin/dev..upstream/dev),其中与 Copilot 请求处理 直接相关的一条:561afb401afix(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)
CLIENT_ID = Ov23li8tweQw6odWQebz),申请 scope 仅read:user。api.github.com/copilot_internal/v2/token去换取短时 Copilot Token。它把设备流拿到的 GitHub OAuthaccess_token直接作为Authorization: Bearer <token>打到https://api.githubcopilot.com。git log -S copilot_internal证实该文件历史上从未出现过该端点。refresh = access = access_token、expires = 0,写入~/.local/share/opencode/auth.json(权限0600)。因 Copilot auth 未注册refresh方法,该 Token 被当作长期有效直接使用。request.ts(会话层)→ AI SDKcombineHeaders(provider 层)→copilot.ts的loader.fetch包装器(最终覆盖 & 注入鉴权头)。/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: 模型列表 / 补全流2.2 步骤拆解(标注代码位置)
CLIENT_ID=Ov23li8tweQw6odWQebz;API_VERSION=2026-06-01copilot.ts:9-10DEVICE_CODE_URL=https://{domain}/login/device/code、ACCESS_TOKEN_URL=https://{domain}/login/oauth/access_token;domain默认github.com,企业版取用户输入域名copilot.ts:19-24, 222-232POST device/code,body{client_id, scope:"read:user"},头Accept/Content-Type: application/json、User-Agent: opencode/{version}copilot.ts:234-256POST 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-334type:"oauth",refresh=access=access_token,expires:0;企业版附enterpriseUrlcopilot.ts:286-305auth.json(Global.Path.data),0o600auth/index.ts:10, 14-21, 73-81loader.fetch包装器读取 oauth 信息,注入Authorization: Bearer <token>copilot.ts:100-179Base URL(
copilot.ts:26-28):https://api.githubcopilot.comhttps://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」分支:
x-session-affinityinput.sessionIDX-Session-Idinput.sessionIDx-parent-session-idinput.parentSessionID(存在时)User-Agentopencode/{InstallationVersion}request.ts:18...input.model.headersmodels.ts:157...headerschat.headers钩子输出层 B ——
chat.headers钩子(copilot.ts:360-411)X-GitHub-Api-Version2026-06-01X-Interaction-Typeagent-session-name-generationagent === "title"(标题生成)anthropic-betainterleaved-thinking-2025-05-14api.npm === "@ai-sdk/anthropic"(Claude 走 messages)x-initiatoragentparentID(子代理)层 C ——
loader.fetch传输包装器(最终注入,决定线上真实请求头)(copilot.ts:160-173)AuthorizationBearer <github_oauth_token>User-Agentopencode/{InstallationVersion}Openai-Intentconversation-editsx-initiatoragent/useragent(copilot.ts:107-158)Copilot-Vision-Requesttrueimage_url/input_image/image)时x-api-keydelete headers["x-api-key"](小写)authorizationAuthorization重复层 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)。因 CopilotapiKey="",此处不产生鉴权头,最终由层 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决定:api.npmapi.endpoint/v1/messages@ai-sdk/anthropic,url = {base}/v1messages/responses@ai-sdk/github-copilotresponses/chat/completions@ai-sdk/github-copilotchat进一步在
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)调整:maxOutputTokens=undefined(对齐官方 Copilot CLI);toolStreaming=false(Copilot 的/v1/messagesshim 拒绝eager_input_streaming字段)。4.2
/chat/completionsBody(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
/responsesBody(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/messagesBody(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){role,content}+ copilot 元数据:18-25content:string;多模态映射为数组:text/image_url(URL 或data:<mime>;base64,...):27-71text、reasoning_text、reasoning_opaque、tool_calls[{id,type:function,function:{name,arguments}}]:73-126{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. 关键代码位置索引
packages/opencode/src/plugin/github-copilot/copilot.ts:9-10;base:26-28;authorize:222-256;callback 轮询:262-334;凭据存储:286-305loader.fetch头注入:100-179;chat.params:340-354;小模型:355-359;chat.headers:360-411packages/opencode/src/auth/index.ts:10, 14-21, 73-89packages/opencode/src/plugin/github-copilot/models.ts:82-205, 216-259packages/opencode/src/session/llm/request.ts:56-206packages/core/src/github-copilot/copilot-provider.ts:52-97packages/core/src/github-copilot/chat/openai-compatible-chat-language-model.ts:87-329packages/core/src/github-copilot/chat/convert-to-openai-compatible-chat-messages.ts:13-170packages/core/src/github-copilot/responses/openai-responses-language-model.ts:152-420, 778-788packages/core/src/plugin/provider/github-copilot.ts:6-52packages/llm/src/providers/github-copilot.ts:8-696. 一句话总结
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三种协议分别构造。