Qwen Code 出站 `session_id` 请求头:Routify 网关会话亲和标记的注入机制、安全边界与配置指南
Qwen Code 出站session_id请求头Routify 网关会话亲和标记的注入机制、安全边界与配置指南【免费下载链接】qwen-codeAn open-source AI coding agent that lives in your terminal.项目地址: https://gitcode.com/GitHub_Trending/qw/qwen-code导读Qwen Code本仓库开源项目运行于终端中的 AI 编码智能体在向特定 ModelRouter 网关端点发起 LLM 出站请求时会自动附加当前 CLI 会话的session_idHTTP 头用于会话亲和session affinity与流量标记。本文基于 docs/design/2026-09-03-outbound-session-id-header.md 设计文档结合 packages/core/src/core/outbound-session-id.ts 及对应测试的实现细节系统讲解该头的注入条件、精确主机匹配的安全边界、各模型 Provider 的接入方式以及后续演进中允许用户在customHeaders中通过${session_id}占位符自行注入会话标识的两步启用方案。背景与动机复用现有 session ID 而非再造标识符Routify 的 ModelRouter 接受session_id作为会话亲和session-affinity与流量标记traffic-marking值。Qwen Code 本身已经维护着一个稳定的会话 ID——它由 Config.getSessionId() 持有但在本功能落地之前这个 ID 只是本地元数据从未随 LLM 请求到达 ModelRouter。设计文档给出的动机非常明确复用这个已有的会话 ID让 Routify 在每个 CLI 会话生命周期内获得一个稳定的亲和值而无需再创造另一个标识符。这避免了多套标识体系带来的关联困难与维护成本——网关按会话粘滞路由、按会话统计流量而客户端侧无需新增任何状态。核心机制仅对三个 Routify 端点的精确主机匹配注入内置注入的触发条件Qwen Code 只有在出站 LLM 请求的主机名hostname是 ModelRouter 文档记载的三个 Routify 端点之一时才把当前会话 ID 作为session_idHTTP 头附加主机名说明routify.alibaba-inc.comRoutify 端点routify-online.alibaba-inc.comRoutify 在线端点routify-pub.alibaba-inc.comRoutify 公开端点这一固定集合定义在源码 outbound-session-id.ts 的SESSION_ID_HEADER_HOSTS常量中。核心判定函数buildSessionIdHeaders的逻辑见 outbound-session-id.tsexport function buildSessionIdHeaders( config: Config, destination: string | URL | Request, ): Recordstring, string { try { const url requestUrl(destination); if ( url?.protocol ! https: || !SESSION_ID_HEADER_HOSTS.includes(url.hostname.toLowerCase()) ) { return {}; } const sessionId config.getSessionId(); return sessionId ? { [SESSION_ID_HEADER]: sessionId } : {}; } catch (error) { // 解析失败即返回空对象fail closed return {}; } }安全边界fail-closed 的精确匹配设计文档强调会话 ID 是跨请求稳定的标识符因此实现上严格遵循以下约束强制 HTTPS非https:协议一律不注入精确主机匹配将解析后的请求主机名与上述三个主机做完全相等比较includestoLowerCase不使用后缀匹配suffix matching、通配符wildcards、路径匹配path matching也没有用户可配置的 allowlist解析失败即关闭fail closedURL 解析异常如not a URL时requestUrl返回undefined函数直接返回空对象绝不误注入主机名大小写归一比较前对 hostname 做toLowerCase()避免因大小写变体绕过精确匹配。这意味着sub.routify-pub.alibaba-inc.com、routify-preview.alibaba-inc.com、api.openai.com等“长得像”的主机都不会收到该头测试用例 outbound-session-id.test.ts 对上述拒绝场景逐一做了验证。内置头优先级会话 ID 不容异议在符合条件的请求上Qwen Code 的会话 ID会替换请求中任何自定义的session_id值确保亲和标记不会与当前活动会话相矛盾。其余所有已有请求头包括授权头都会被保留。这一“correlation customHeaders”的优先级在wrapFetchWithSessionId中被刻意实现先展开用户动态头再写入内置的session_id见 outbound-session-id.ts 及注释。重定向语义初始目标检查之后遵循标准 fetch 重定向行为Routify 响应可以通过重定向请求把该头继续转发出去内置机制不做额外干预。请求生命周期fetch 包装器与逐请求读取为什么必须在每次请求前读取OpenAI 兼容客户端与 Anthropic 客户端都会获得一个 fetch 包装器。包装器在每个 HTTP 请求即将发出之前调用Config.getSessionId()读取当前会话 ID见 wrapFetchWithSessionId。这一点至关重要/clear会开启一个新会话但不会重建 SDK 客户端。如果会话 ID 在客户端构造时就被“烘焙”进默认头那么/clear之后发出的请求仍会携带旧会话 ID网关侧就会把新会话的请求错误地归入旧会话。逐请求读取则天然规避了这个问题——测试 outbound-session-id.test.ts 专门验证了会话从session-1轮换到session-2后第二次请求头携带的是新值。包装器的请求头合并语义包装器在处理请求时会先合并两处请求头来源const headers new Headers( input instanceof Request ? input.headers : undefined, ); new Headers(init?.headers).forEach((value, key) headers.set(key, value));Request对象自带头 init.headers中显式指定的头会先合并init同名头覆盖 Request 头之后才写入session_id以及展开的动态占位符头最终以fetchLike(input, { ...init, headers })发出。对应测试覆盖了“保留 Request 对象携带的 Authorization 头”“合并 Request 与 init 头后注入”“空会话 ID 不发送”等场景见 outbound-session-id.test.ts。运行时 fetch 的兜底buildSessionAwareFetch支持两种形态优先包装调用方传入的runtimeFetch例如经buildRuntimeFetchOptions生成的代理感知 fetch若未提供则回退到globalThis.fetch见 outbound-session-id.ts。测试同时覆盖了这两种路径。Gemini 路径请求级 httpOptions与 OpenAI/Anthropic 不同Gemini 请求走的是 SDK 的请求级httpOptions.headers。会话头会在 generate、流式 generate 与 embedding 请求上分别重建——每次请求调用 buildHttpOptions动态展开占位符头与buildSessionIdHeaders的结果后合并进本次请求的 headers。需要特别注意的是Gemini 注入要求显式配置指向 Routify 的baseUrl使用 SDK 隐式默认端点时请求目标不是 Routify因此不注入该头llm-content-generator.ts 以httpOptions?.baseUrl ?? this.clientBaseUrl作为判定目标。Provider 覆盖矩阵设计文档明确列出各 Provider 的接入方式源码中也能逐一对应Provider接入方式源码位置默认 OpenAI 兼容 Provider覆盖 Routify 的 OpenAI 协议子类继承其客户端构造路径同样生效default.ts 中fetch: buildSessionAwareFetch(...)DashScope拥有独立的客户端构造函数被显式集成dashscope.ts 中fetch: buildSessionAwareFetch(...)Anthropic使用与 OpenAI 相同的逐请求 fetch 包装器anthropicContentGenerator.ts 中fetch: buildSessionAwareFetch(...)Gemini / Vertex当 base URL 指向 Routify 时使用请求级 HTTP optionsllm-content-generator.ts从源码结构还可以看到该包装器同样被复用在非 LLM 的 DashScope 工具调用上例如 web 搜索工具 web-search-dashscope.ts 就使用了buildSessionAwareFetch——这说明会话关联层是共享的、可复用的基础设施。明确不在范围内非 LLM 流量、其他域名、MCP 请求、工具 fetch、子进程、traceparent、请求 ID 以及 body 元数据均不参与本机制。源码级验证测试覆盖清单设计文档的 Verification 章节描述的测试场景在 outbound-session-id.test.ts 中均有对应实现精确主机与 HTTPS 匹配三个合法 Routify 主机命中L27-L35http://明文与非法 URL 拒绝L66-L87相似主机拒绝sub.routify-pub.alibaba-inc.com子域、routify-preview.alibaba-inc.com近似名、api.openai.com无关第三方均不注入无效 URL fail-closednot a URL直接透传原请求Request 与 init 头合并的保留与优先级Authorization 保留、同名头 init 覆盖 Request、session_id最后写入L98-L138空值处理会话 ID 为空字符串时不发送该头L89-L96会话轮换同一包装器下第二次请求携带新会话 IDL37-L64共享运行时 fetch 包装器显式传入 runtimeFetch 与回退globalThis.fetch两条路径L140-L169。此外Provider 层测试验证了 OpenAI 兼容构造路径确实安装了一个可工作的关联层Gemini 测试覆盖了构造函数目标、生成、embedding 以及连续请求观察到会话 ID 变化等场景。后续演进customHeaders中的${session_id}占位符设计文档将其标记为后续变更对应 issue #10995当前仓库中该能力已落地并在用户文档 docs/users/configuration/model-providers.md 中完整记载实现位于 outbound-dynamic-headers.ts。能力描述modelProviders[].generationConfig.customHeaders的值中可以包含占位符${session_id}它会在每个请求时用相同的Config.getSessionId()展开。适用于需要稳定按会话标识符的网关——例如 OpenCode Go 会拒绝缺少x-opencode-session的请求{ modelProviders: { openai: [ { baseUrl: https://your-gateway.example.com/v1, generationConfig: { customHeaders: { x-opencode-session: ${session_id} } } } ] } }由于值在请求时动态解析而非在 SDK 客户端构造时烘焙/new与/resume切换会话后无需重启即可自动轮换。两步启用与失败关闭语义⚠️ 仅配置 provider 条目是不够的——占位符在全局开关outboundCorrelation.allowDynamicHeaderValues打开之前是惰性的默认关闭{ outboundCorrelation: { allowDynamicHeaderValues: true } }在开关关闭、值为空或无法解析时包含占位符的头会被丢弃而不是发出字面量${session_id}永远不会出现在线路上。若用户配置了占位符但开关未开Qwen Code 会在启动时打印一条警告明确指出受影响的头名称与该设置项见 warnIfDynamicHeadersDisabled。resolveDynamicHeaderValue以三种方式 fail-closedoutbound-dynamic-headers.ts开关关闭、占位符解析为空、Config无法应答。该开关是全局的因为它本质是一个同意consent决策与“值发往哪里”分离展开后的值会把实时会话状态带给接收方因此开关只控制${session_id}是否允许展开而不负责识别头来自哪个配置来源。占位符的威胁模型回答设计文档指出这一后续演进正面回答了出站传播设计见 docs/design/telemetry-outbound-propagation-design.md第 12.7 节提出的威胁模型问题同意Consent占位符在全局开关outboundCorrelation.allowDynamicHeaderValues打开前是惰性的默认关闭关闭、空值或无法解析的值都会丢弃该头字面量${session_id}永不发送接收方集合Recipient set由用户把该头附加到哪些 Provider 决定。作用域来自 provider 条目本身而非独立 allowlist——用户在书写baseUrl时就已经选定了端点不应接收该值的 Provider 只要不携带该头即可去匿名化窗口De-anonymization window仅一个会话。该值在一次对话生命周期内保持稳定这种稳定性正是网关粘滞路由所依赖的特性并在/new与/resume时轮换。接收方最多能把同一次对话的请求归组而无法跨会话关联逐请求 UUID 伴生值Per-request UUID companion刻意不提供。逐请求值会破坏网关所需的会话亲和占位符集合被刻意封闭为一项未经同等级评审不应扩充。从实现上看占位符集合确实被定义为封闭列表PLACEHOLDERS目前仅含{ token: ${session_id}, resolve: (config) config.getSessionId() }一项且SESSION_ID_PLACEHOLDER_PATTERN会归一化$session_id、${session_id}、$QWEN_CODE_SESSION_ID、${QWEN_CODE_SESSION_ID}等等价拼写防止设置插值把一个未受保护的写法转成静态值绕过闸门outbound-dynamic-headers.ts。总结内置关联层与用户扩展层的分工session_id出站头的设计体现了一个清晰的分层思路内置层built-in不可配置仅对三个精确匹配的 Routify 端点、仅 HTTPS、fail-closed由 fetch 包装器在每次请求前注入保证网关会话亲和标记永远正确扩展层用户配置需显式同意customHeaders中的${session_id}占位符把同一会话标识符按用户意愿附加到任意自选 Provider默认关闭失败即丢弃绝不泄漏字面量。两层共享同一个Config.getSessionId()数据源、同一套逐请求解析语义、同一组轮换规则且内置层在所有路径上保持对 customHeaders 条目的优先级。对于希望深入源码的读者建议从 outbound-session-id.ts核心注入逻辑、outbound-dynamic-headers.ts占位符展开与闸门以及 outbound-session-id.test.ts完整测试矩阵三处入手即可完整掌握该机制的实现全貌。【免费下载链接】qwen-codeAn open-source AI coding agent that lives in your terminal.项目地址: https://gitcode.com/GitHub_Trending/qw/qwen-code创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
上一篇/下一篇内容由系统自动关联
返回资讯列表 →