最后更新:2026-04-16 | 模块:通用适配器架构
决策 rationale 见 ../adr/002-native-first.md,数据结构定义见 ../CODE_WIKI.md
适配层只做格式转换,不碰语义。所有请求统一收敛为内部 Envelope 和 Block,出站再还原为厂商原生格式。
核心挑战:不同厂商 API 在请求格式、响应结构、流式协议、认证方式上差异显著,需要一套适配器架构来屏蔽这些差异,同时避免传统"归一化"方案导致的能力损失。
设计目标:
- 零能力损失 — 任何原生能力完整保留和透传
- 双向任意接口 — 适配任意上游 Provider,暴露任意格式 API
- 薄适配 — 适配层只负责 Parse / Serialize,不做业务逻辑(分块、排列、缓存策略在更上层处理)
- 配置优先 — 80% 的入站协议可通过 YAML 配置声明字段映射
- 原生透传 — 出站时尽量还原为厂商最原生的格式
Envelope 是内部最小公共格式,只包含后续路由和处理必需的字段,其余全部放入 Raw。
完整类型定义详见 ../CODE_WIKI.md。
关键设计:
Content用json.RawMessage,因为 LLM content 可能是string或多模态数组Raw字段保存所有未标准化字段,出站时按需透传CacheControl在入站时通常为空,由后续CacheInjector填充
Block 是 Chunker 的输出,Arranger 和后续模块的操作单位。
完整类型定义详见 ../CODE_WIKI.md。
package inbound
type InboundAdapter interface {
Match(method, path string) bool
Parse(req *http.Request) (*envelope.Envelope, error)
Name() string
}package inbound
type Registry struct {
adapters []InboundAdapter
}
func NewRegistry(adapters ...InboundAdapter) *Registry {
return &Registry{adapters: adapters}
}
func (r *Registry) Parse(req *http.Request) (*envelope.Envelope, InboundAdapter, error) {
for _, adapter := range r.adapters {
if adapter.Match(req.Method, req.URL.Path) {
env, err := adapter.Parse(req)
return env, adapter, err
}
}
return nil, nil, fmt.Errorf("unsupported inbound protocol: %s %s", req.Method, req.URL.Path)
}用于 80% 与 OpenAI 格式接近的厂商 API。
package inbound
type ConfigDrivenAdapter struct {
config AdapterConfig
}
type AdapterConfig struct {
Name string `yaml:"name"`
MatchMethod string `yaml:"match_method"`
MatchPath string `yaml:"match_path"`
FieldMap map[string]string `yaml:"field_map"`
ContentType string `yaml:"content_type"`
}MVP v0.1 唯一必须手写的入站适配器。
package inbound
type OpenAIAdapter struct{}
func (a *OpenAIAdapter) Name() string { return "openai" }
func (a *OpenAIAdapter) Match(method, path string) bool {
return method == http.MethodPost && path == "/v1/chat/completions"
}
func (a *OpenAIAdapter) Parse(req *http.Request) (*envelope.Envelope, error) {
body, err := io.ReadAll(req.Body)
if err != nil {
return nil, err
}
defer req.Body.Close()
var payload struct {
envelope.Envelope
Stop interface{} `json:"stop,omitempty"`
PresencePenalty *float64 `json:"presence_penalty,omitempty"`
FrequencyPenalty *float64 `json:"frequency_penalty,omitempty"`
}
if err := json.Unmarshal(body, &payload); err != nil {
return nil, err
}
env := &payload.Envelope
env.Raw = make(map[string]json.RawMessage)
var rawMap map[string]json.RawMessage
if err := json.Unmarshal(body, &rawMap); err == nil {
for k, v := range rawMap {
if !isStandardField(k) {
env.Raw[k] = v
}
}
}
return env, nil
}package outbound
type OutboundAdapter interface {
Name() string
BuildRequest(blocks []block.Block, env *envelope.Envelope) ([]byte, error)
ParseStreamChunk(chunk []byte) (*StreamEvent, error)
ParseResponse(body []byte) (*Response, error)
}
type StreamEvent struct {
Type string
Delta json.RawMessage
Done bool
Usage *Usage
}
type Response struct {
Choices []Choice
Usage *Usage
Raw map[string]json.RawMessage
}
type Choice struct {
Index int
Message *envelope.Message
FinishReason string
}
type Usage struct {
PromptTokens int
CompletionTokens int
CacheReadTokens int
CacheWriteTokens int
}package outbound
type Registry struct {
adapters map[string]OutboundAdapter
}
func NewRegistry(adapters ...OutboundAdapter) *Registry {
r := &Registry{adapters: make(map[string]OutboundAdapter)}
for _, a := range adapters {
r.adapters[a.Name()] = a
}
return r
}
func (r *Registry) Get(name string) (OutboundAdapter, error) {
a, ok := r.adapters[name]
if !ok {
return nil, fmt.Errorf("unsupported outbound adapter: %s", name)
}
return a, nil
}package outbound
type AnthropicAdapter struct{}
func (a *AnthropicAdapter) Name() string { return "anthropic" }
func (a *AnthropicAdapter) BuildRequest(
blocks []block.Block,
env *envelope.Envelope,
) ([]byte, error) {
req := make(map[string]interface{})
var messages []envelope.Message
var systemBlocks []envelope.Message
var tools []envelope.Tool
for _, b := range blocks {
switch b.Type {
case block.BlockSystem:
systemBlocks = append(systemBlocks, b.Messages...)
case block.BlockTool:
tools = append(tools, b.Tools...)
case block.BlockHistory, block.BlockQuery:
messages = append(messages, b.Messages...)
}
}
if len(systemBlocks) > 0 {
if len(systemBlocks) == 1 {
req["system"] = systemBlocks[0].Content
} else {
var sysContents []map[string]string
for _, m := range systemBlocks {
sysContents = append(sysContents, map[string]string{
"type": "text",
"text": string(m.Content),
})
}
req["system"] = sysContents
}
}
req["messages"] = messages
if len(tools) > 0 {
req["tools"] = tools
}
for k, v := range env.Raw {
var val interface{}
if err := json.Unmarshal(v, &val); err == nil {
req[k] = val
}
}
return json.Marshal(req)
}| 分类 | 定义 | 代表厂商 | 接入策略 |
|---|---|---|---|
| 国际原生协议派 | 协议与 OpenAI 差异显著 | OpenAI, Anthropic, Google Gemini, AWS Bedrock | 手写专属 Inbound + Outbound Adapter。MVP v0.1 暂不接入,接口已预留 |
| 国内云服务原生派 | 自有原生协议 + 半生不熟的兼容层 | 阿里 DashScope, 腾讯 Hunyuan, 百度千帆, 火山 Ark, 智谱 GLM, 华为盘古, MiniMax | 优先手写原生适配器 |
| 优质兼容派 | 原生协议就是 OpenAI 兼容 | DeepSeek V3.2, Moonshot, Baichuan, Stepfun, Mistral, Cohere, Groq | OpenAIAdapter 直接覆盖。MVP v0.1 只接入 DeepSeek V3.2 |
| 混合/Wrapper 派 | 提供自研 SDK,底层仍是标准 HTTP | Azure OpenAI, Vertex AI | ConfigDrivenAdapter + 认证中间件 |
所有主流厂商的核心 Chat / Completions HTTP API 本质上都是无状态的。 单次对话请求都要求客户端在 messages 字段中发送完整的上下文历史。服务端不保存对话状态。
少数厂商的 Python SDK 在客户端封装了"有状态的对话对象"(如 Google Gemini 的 ChatSession),但 SDK 底层发向 TokenRouter 的仍然是一个包含完整历史的单次 HTTP 请求。
这意味着:TokenRouter 作为 HTTP 代理,只需单次请求级别的 Parse + Build,无需引入 session 存储或对话历史管理。
平台的核心商业价值来自结构整合 + 缓存注入,要求在请求发送到厂商之前完成:
- Chunker 决定前缀边界
- Arranger 按固定顺序排列 Block
- Canonicalizer 生成字节级确定性的请求体
- Cache Injector 在精确位置插入
cache_control等标记
如果走 SDK 层,上述步骤会被严重削弱:SDK 通常不接受「已序列化好的原始 JSON 字符串」,且无法保证内部序列化后的字节与 Canonicalizer 输出完全一致。
结论:TokenRouter 必须掌握最终发送到厂商的每一个字节,因此出站只能走 HTTP 层。
[入站请求]
│
▼
[Inbound Adapter] ──→ Envelope
│
▼
[Chunker] ──→ []Block
│
▼
[Arranger] ──→ 排序后的 []Block
│
▼
[Canonicalizer] ──→ 确定性 JSON bytes
│
▼
[CacheInjector] ──→ 注入 cache_control
│
▼
[Outbound Adapter] ──→ 厂商原生请求体
│
▼
[HTTP Proxy] ──→ 上游 Provider
适配层只负责首尾的格式转换:
- Inbound:HTTP Request →
Envelope - Outbound:
[]Block+Raw→ HTTP Request Body
中间的 Chunker、Arranger、Canonicalizer、CacheInjector 处理的是统一的 Envelope / Block 结构,与具体厂商协议无关。