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/opencodedev 完成同步)。
本报告聚焦 GitHub Copilot provider,依据仓库实际代码,给出 鉴权与 Token 交换流程Request HeaderRequest 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_typesapplication/pdfsupports.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 调用 api.github.com/copilot_internal/v2/token 去换取短时 Copilot Token。它把设备流拿到的 GitHub OAuth access_token 直接作为 Authorization: Bearer <token> 打到 https://api.githubcopilot.comgit log -S copilot_internal 证实该文件历史上从未出现过该端点。
  3. 凭据存储:refresh = access = access_tokenexpires = 0,写入 ~/.local/share/opencode/auth.json(权限 0600)。因 Copilot auth 未注册 refresh 方法,该 Token 被当作长期有效直接使用。
  4. 请求头 由三层拼装:request.ts(会话层)→ AI SDK combineHeaders(provider 层)→ copilot.tsloader.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/codeACCESS_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/jsonUser-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_pendingslow_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):chatopenai-compatible-chat-language-model.ts:206,323;responsesopenai-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/jsonpostJsonToApi 自动添加。

GET /models(拉取模型列表)的请求头(copilot.ts:70-79models.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-50llm/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 聚合 textreasoning_textreasoning_opaquetool_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 三层叠加,最终由传输包装器注入 AuthorizationOpenai-Intentx-initiatorX-GitHub-Api-VersionCopilot-Vision-Request 等 Copilot 专属头;请求体按模型能力路由到 /chat/completions/responses/v1/messages 三种协议分别构造。

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions