Repository navigation
Description
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 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 步骤拆解(标注代码位置)
| 步骤 | 说明 | 代码位置 |
|---|---|---|
| ① 常量 | 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)。因 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 决定:
| 上游端点 | 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/messagesshim 拒绝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