跳转到内容

请求整流与多协议转换

请求整流与多协议转换

学习目标

  • 理解 CC Switch 代理层的请求处理流水线
  • 掌握 Claude 接管时的模型别名整流策略
  • 了解 thinking 预算整流器的实现
  • 识别 SSE 流式转换中的边界情况

前置知识

下文聚焦 CC Switch 的具体实现方式。


项目实践

请求处理流水线

CC Switch 代理层的请求处理经过以下阶段:

客户端请求 → 请求头整流 → 路径路由 → 模型名映射 → Token 注入 → 上游转发
客户端响应 ← 流式转换 ← 响应体整流 ← 错误处理 ← 上游响应

关键模块(src-tauri/src/proxy/):

模块职责
handlers.rs请求入口、路径解析、应用类型检测
forwarder.rs上游请求转发、响应流代理
response_processor.rs响应体格式转换
model_mapper.rs模型名整流(客户端别名 → 上游真实名)
providers/各供应商特定的认证和配置适配
thinking_rectifier.rsthinking 预算整流

Claude 模型别名整流

这是 CC Switch 代理接管中最关键的整流工作之一。

问题:Claude Code 的模型菜单显示的是环境变量中的模型名。如果上游供应商使用不同的模型名,Claude Code 的模型菜单会显示错误的名称。

解决方案:代理在接管 Claude Live 配置时,写入一组稳定的角色别名

写入 Live:
ANTHROPIC_DEFAULT_HAIKU_MODEL = "claude-haiku-4-5"
ANTHROPIC_DEFAULT_SONNET_MODEL = "claude-sonnet-4-6"
ANTHROPIC_DEFAULT_OPUS_MODEL = "claude-opus-4-7"
代理内部映射:
"claude-haiku-4-5" → 上游真实模型名(如 gemini-2.5-flash)
"claude-sonnet-4-6" → 上游真实模型名(如 claude-sonnet-4-6-20250514)
"claude-opus-4-7" → 上游真实模型名

这样 Claude Code 看到的始终是熟悉的模型别名,而代理负责将这些别名映射到当前供应商的真实模型。

1M 上下文标记:如果上游模型支持 1M 上下文窗口,CC Switch 在写入客户端别名时附加 [1M] 标记(如 claude-sonnet-4-6 [1M]),代理在解析时识别并剥离该标记。

认证策略分流

不同类型的供应商使用不同的认证方式,CC Switch 在接管时采用不同的策略:

传统 API Key 供应商PreserveExistingOrAuthToken):

  • 保留 Live 中已存在的 Token 字段名
  • 写入占位符 PROXY_MANAGED
  • 代理从数据库读取真实 Token 后,根据原始字段名注入

托管 OAuth 供应商ManagedAccount,如 GitHub Copilot):

  • 清除所有 Token 字段(不保留原始字段名)
  • 统一写入 ANTHROPIC_API_KEY = "PROXY_MANAGED"
  • 代理使用内部的 CopilotAuthManager 获取有效 Token 并注入

为什么区分这两种策略:OAuth 供应商(如 Copilot)的 Token 是通过设备码流程动态获取的,不存储在 Live 配置中。如果按传统方式保留原始字段名,代理可能写入错误的认证格式。

Thinking 预算整流

Claude 的 thinking 模式需要向供应商发送 thinking 参数。CC Switch 实现了 thinking 预算整流器:

thinking_budget_rectifier {
// 客户端可能请求的 thinking 预算超过了供应商支持的最大值
if client_thinking_budget > provider_max_budget {
// 整流为供应商最大值
outgoing_thinking_budget = provider_max_budget
}
// 或者客户端不支持 thinking,但供应商返回了 thinking 内容
if !client_supports_thinking && response_has_thinking {
// 过滤掉 thinking 内容块
filter_thinking_blocks()
}
}

设计要点:整流器在请求发送前和响应返回后都需要运行。只整流请求而不整流响应会导致客户端收到不理解的响应格式。

Codex wire_api 强制整流

Codex 支持两种 wire API:chat(Chat Completions)和 responses(Responses API)。

CC Switch 代理在接管 Codex 时强制设置 wire_api = "responses"

# 写入 Codex config.toml
base_url = "http://127.0.0.1:15721/v1"
wire_api = "responses"
model = "<上游真实模型名>"

原因:代理层统一以 Responses API 格式接收来自 Codex 客户端的请求,然后在内部根据上游供应商的能力转换为适当的格式。这简化了代理的路由逻辑——只需要处理一种入站协议。


问题与规避

陷阱 1:模型名整流遗漏导致上游拒绝请求

问题:代理将请求转发到上游时,如果忘记将客户端模型别名转换为上游真实模型名,上游可能返回 “model not found” 错误。

CC Switch 的解决方案model_mapper 模块在每次请求发送前强制执行模型名转换。如果映射表中不存在该模型,回退到供应商的默认模型。

陷阱 2:OAuth 供应商的 Token 过期

问题:托管 OAuth 供应商(如 Copilot)的 Token 有有效期,过期后代理继续使用该 Token 会导致 401 错误。

CC Switch 的解决方案CopilotAuthManager 在每次请求前检查 Token 有效性,如果过期则自动刷新。刷新逻辑独立于请求处理流水线,通过 RwLock 保护共享状态。

陷阱 3:响应整流遗漏特定字段

问题:上游返回的响应可能包含客户端不期望的字段(如 thinking 内容块),如果不整流直接转发,客户端可能崩溃。

CC Switch 的解决方案

  • response_processor.rs 中维护一个”上游特性 → 客户端兼容性”矩阵
  • 根据客户端能力决定哪些字段需要过滤、转换或透传
  • 对于未知字段,记录警告日志但不中断响应流

设计取舍

为什么强制 Codex 使用 Responses API?

入站协议优势劣势
Responses API功能更丰富(支持工具、模板等),与 Claude Messages API 更接近,整流逻辑更统一Codex 旧版本可能不支持
Chat Completions更简单,广泛支持功能有限,与 Claude 差异大

CC Switch 选择 Responses API 作为统一入站协议,因为它与 Claude 的 Messages API 在结构上更接近(都有 content 数组、工具调用等),整流逻辑可以复用更多代码。

替代方案:客户端协议适配层

另一种方案是在客户端侧做协议适配——让前端直接发送不同协议的请求,代理只做转发。

局限性

  • 需要前端理解所有协议格式,前端复杂度增加
  • 代理无法做统一的模型名映射和 Token 注入
  • 热切换时前端需要主动切换请求格式

因此,CC Switch 选择在代理层做所有整流工作,前端保持简单。


参考来源