1
0
Files
nex/docs/conversion_design.md

47 KiB
Raw Blame History

LLM API Protocol Conversion Layer — Architecture Design

语言无关的 HTTP 层 SDK。以 Hub-and-Spoke 架构实现多协议间的双向转换,覆盖完整 HTTP 接口体系,同协议自动透传,为新协议和多模态预留扩展点。


目录

  1. 设计目标与约束
  2. 架构总览
  3. 接口体系分层
  4. Canonical Model规范模型
  5. Protocol Adapter 接口
  6. Conversion Engine转换引擎
  7. 流式转换架构
  8. 扩展点设计
  9. 错误处理
  10. 附录 A模块依赖
  11. 附录 B接口速查
  12. 附录 C字段晋升规范
  13. 附录 D协议适配清单模板
  14. 附录 E协议适配文档索引

1. 设计目标与约束

1.1 核心目标

目标 说明
完整 HTTP 接口体系转换 覆盖 /models、/embeddings、/rerank 等全部接口的 URL 路由映射、请求头转换、请求体/响应体格式转换
输入输出解耦 客户端协议和服务端协议独立指定,任意组合
同协议透传 client == provider 时跳过转换,零语义损失、零序列化开销
尽力转换 能对接的参数尽可能对接,不能对接的各自忽略,保障最大覆盖面
协议可扩展 添加新协议只需实现 Adapter不修改核心引擎
流式优先 SSE 流式转换作为核心能力,与非流式同等地位
Tool Calling 核心 工具调用是编程场景的一等公民
语言无关 不绑定编程语言,用伪类型描述接口

1.2 约束

约束 说明
部署形态 HTTP 层 SDK处理 HTTP 请求/响应转换URL + Headers + Body不包含 HTTP 服务器启动/监听
当前模态 仅文本(含 Tool Calling接口体系覆盖 /models、/embeddings、/rerank
Provider 必传 每次转换调用需传入 TargetProvider提供目标上游的地址、认证、模型名等信息
适配器注册 所有 ProtocolAdapter 通过代码注册,不支持动态增减
有状态特性 初始不实现,架构预留扩展点

1.3 设计决策溯源

决策 依据
HTTP 层 SDK 编程工具启动时调用 /models 等接口,缺失会报错或功能降级
Hub-and-Spoke 以规范格式为枢纽,将 O(n²) 的协议对转换降为 O(n)
自定义 Canonical Model 不选用厂商格式,避免语义损失和厂商锁定
协议识别外部化 协议识别是调用方职责,引擎不绑定具体识别方式
接口分层 + 尽力转换 不同接口差异程度不同,分层处理最大化覆盖面
同协议透传 零转换的性能优势已验证

2. 架构总览

2.1 分层架构

┌─────────────────────────────────────────────────────────────────────────┐
│                         上层 HTTP 框架(用户自选)                          │
│           Express / FastAPI / Axum / Gin / 任意框架                       │
│                                                                         │
│  ┌─────────────────────────────────────────────────────────────────┐   │
│  │              协议识别 + 前缀剥离(调用方职责)                      │   │
│  │                                                                 │   │
│  │  入站: /{protocol}/{native_path}                                 │   │
│  │                                                                 │   │
│  │  /<protocol_a>/v1/chat/completions → client=protocol_a, /v1/... │   │
│  │  /<protocol_b>/v1/messages       → client=protocol_b, /v1/...   │   │
│  │                                                                 │   │
│  │  Step 1: 识别 client protocolURL 前缀 / 配置映射 / 任意方式)   │   │
│  │  Step 2: 剥离前缀 → 得到 nativePath                              │   │
│  │  Step 3: 确定 provider protocol配置决定                       │   │
│  └─────────────────────────────┬───────────────────────────────────┘   │
└───────────────────────────────┬─────────────────────────────────────────┘
                                │ nativePath + clientProtocol + providerProtocol
                                ▼
┌─────────────────────────────────────────────────────────────────────────┐
│                         Conversion Engine (SDK)                         │
│                                                                         │
│                    ┌────────────┼────────────┐                          │
│                    ▼            ▼            ▼                          │
│           ┌──────────────┐ ┌─────────┐ ┌──────────┐                   │
│           │  Passthrough  │ │核心接口层│ │扩展接口层 │                   │
│           │  同协议直通   │ │  Chat   │ │ Models   │                   │
│           │  或未知接口   │ │  流式   │ │ Embed    │                   │
│           │  直接转发    │ │ Tool    │ │ Rerank   │                   │
│           └──────────────┘ │ Thinking│ └──────────┘                   │
│                          └────┬────┘                                  │
│                    ┌──────────▼───────────────┐                         │
│                    │     ProtocolAdapter 层    │                         │
│                    │  ┌────────┐  ┌─────────┐  │                         │
│                    │  │Protocol│  │Protocol │  │                         │
│                    │  │  A     │  │  B      │  │                         │
│                    │  └────────┘  └─────────┘  │                         │
│                    └──────────────┬────────────┘                         │
│                    ┌──────────────▼────────────┐                         │
│                    │      Canonical Model       │                         │
│                    └───────────────────────────┘                         │
└─────────────────────────────────────────────────────────────────────────┘

2.2 URL 路由规则

调用方负责识别协议并剥离前缀,将 nativePathclientProtocolproviderProtocol 传入引擎。调用方可自行决定识别方式URL 前缀、配置映射等)。

入站 URL                                    调用方剥离前缀后        引擎出站
──────────────────────────────────────────────────────────────────────────────
/<protocol_a>/v1/chat/completions       →  /v1/chat/completions →  目标协议路径
/<protocol_b>/v1/messages               →  /v1/messages         →  目标协议路径
/<protocol_a>/v1/models                 →  /v1/models           →  /v1/models通常不变

出站到上游 API 时使用服务端协议原生路径(无前缀)。

2.3 请求处理流程

每个 HTTP 请求的转换流程:

客户端入站       调用方(协议识别+前缀剥离)       SDK 内部处理                  上游出站
┌──────────────────┐                                                            ┌──────────────────┐
│ URL:              │  调用方完成:                   1. 接口识别: CHAT            │ URL:              │
│   /<protocol>/    │  · clientProtocol             2. 同协议? ──yes──▶ 直接转发│   目标协议        │
│   v1/...          │  · nativePath                    └──no──▶ 继续转换       │   原生路径         │
│ Headers:          │  · providerProtocol           3. URL 映射: 目标路径       │ Headers:          │
│   协议原生格式     │                               4. Header 构建: 目标格式    │   目标协议格式     │
│ Body:             │                               5. Body 转换: D→C→E        │ Body:             │
│   协议原生格式     │                                                           │   目标协议格式     │
└──────────────────┘                                                            └──────────────────┘

响应方向同理含流式。D=Decoder, C=Canonical, E=Encoder。

同协议透传client == provider 时,仅重建 Header 后原样转发到上游。 未知接口透传无法识别的路径URL+Header 适配后 Body 原样转发。


3. 接口体系分层

3.1 分层策略

层级 接口 转换方式
核心层 Chat 接口(各协议的核心对话接口) Canonical Model 深度转换
扩展层 /models, /models/{model}, /embeddings, /rerank 轻量级字段映射
透传层 /files, /count_tokens, /batches, /fine-tuning, /audio/, /images/, /video/*, /moderations 等未知路径 URL+Header 适配后 Body 原样转发

接口纳入原则:只有 ≥2 个协议提供相同功能的接口才纳入转换层(核心层或扩展层),其余走透传。

尽力转换策略

  • 双方都支持且可映射 → 完整转换(如 /models
  • 服务端协议不支持该接口 → 透传到上游或返回空响应/错误(可配置)
  • 未知接口 → 始终透传

各协议的接口清单详见各自的协议适配文档(附录 E


4. Canonical Model规范模型

协议无关的统一内部表示Hub-and-Spoke 架构的枢纽。核心层使用完整 Canonical Model 深度转换;扩展层使用轻量 Canonical Models 做字段映射;透传层不使用。

设计原则:超集设计、类型化 discriminated union、前向兼容。Canonical Model 面向当前已适配协议的公共语义进行抽象,字段随协议扩展演进(详见附录 C字段晋升规范

适配新协议时:对照本节定义逐字段确认映射关系。协议特有字段不纳入 Canonical同协议透传时自然保留跨协议时丢弃详见附录 C 字段分类规则)。

4.1 CanonicalRequest

CanonicalRequest {
    model: String
    system: Union<None, String, Array<SystemBlock>>

    SystemBlock {
        text: String
    }

    messages: Array<CanonicalMessage>
    tools: Option<Array<CanonicalTool>>
    tool_choice: Option<ToolChoice>
    parameters: RequestParameters
    thinking: Option<ThinkingConfig>
    stream: Boolean
    user_id: Option<String>
    output_format: Option<OutputFormat>
    parallel_tool_use: Option<Boolean>       // true = 允许并行, false = 禁止并行
}

4.2 RequestParameters

RequestParameters {
    max_tokens: Option<Integer>
    temperature: Option<Float>
    top_p: Option<Float>
    top_k: Option<Integer>
    frequency_penalty: Option<Float>
    presence_penalty: Option<Float>
    stop_sequences: Option<Array<String>>
}

4.3 CanonicalMessage / ContentBlock

CanonicalMessage {
    role: Enum<system, user, assistant, tool>
    content: Array<ContentBlock>
}

ContentBlock = Union<
    TextBlock,          { type: "text", text: String }
    ToolUseBlock,       { type: "tool_use", id: String, name: String, input: Object }
    ToolResultBlock,    { type: "tool_result", tool_use_id: String,
                          content: Union<String, Array<ContentBlock>>,
                          is_error: Option<Boolean> }
    ThinkingBlock,      { type: "thinking", thinking: String }
    ImageBlock,         { type: "image", source: ... }   // 多模态预留
    AudioBlock,         { type: "audio", source: ... }   // 多模态预留
    VideoBlock,         { type: "video", source: ... }   // 多模态预留
    FileBlock           { type: "file", source: ... }    // 多模态预留
>

4.4 CanonicalTool / ToolChoice

CanonicalTool {
    name: String
    description: Option<String>
    input_schema: Object
}

ToolChoice = Union<
    {type: "auto"},
    {type: "none"},
    {type: "any"},
    {type: "tool", name: String}
>

4.5 ThinkingConfig

ThinkingConfig {
    type: Enum<enabled, disabled>
    budget_tokens: Option<Integer>               // token 级控制
    effort: Option<Enum<low, medium, high, xhigh>> // 抽象级别控制
}

4.6 OutputFormat

OutputFormat = Union<
    { type: "json_object" },
    { type: "json_schema", json_schema: { name: String, schema: Object, strict: Option<Boolean> } },
    { type: "text" }
>

4.7 CanonicalResponse

CanonicalResponse {
    id: String
    model: String
    content: Array<ContentBlock>
    stop_reason: Option<StopReason>       // end_turn | max_tokens | tool_use | stop_sequence | content_filter | refusal
    usage: CanonicalUsage
}

CanonicalUsage {
    input_tokens: Integer
    output_tokens: Integer
    cache_read_tokens: Option<Integer>
    cache_creation_tokens: Option<Integer>
    reasoning_tokens: Option<Integer>
}

4.8 CanonicalStreamEvent

采用类型化事件模型,具有显式的 start/stop 生命周期,语义明确,便于任意协议间的双向转换:

CanonicalStreamEvent = Union<
    MessageStartEvent,         { type: "message_start", message: {id, model, usage} }
    ContentBlockStartEvent,    { type: "content_block_start", index, content_block }
    ContentBlockDeltaEvent,    { type: "content_block_delta", index, delta }
    ContentBlockStopEvent,     { type: "content_block_stop", index }
    MessageDeltaEvent,         { type: "message_delta", delta: {stop_reason}, usage }
    MessageStopEvent,          { type: "message_stop" }
    ErrorEvent,                { type: "error", error: {type, message} }
    PingEvent                  { type: "ping" }
>

content_block: Union<
    {type: "text", text: ""},
    {type: "tool_use", id, name, input: {}},
    {type: "thinking", thinking: ""},
    {type: "image"} | {type: "audio"} | {type: "video"}  // 预留
>

delta: Union<
    {type: "text_delta", text},
    {type: "input_json_delta", partial_json},
    {type: "thinking_delta", thinking},
    {type: "image_delta", data} | {type: "audio_delta", data}  // 预留
>

4.9 扩展层 Canonical Models

// /models
CanonicalModelList { models: Array<CanonicalModel> }
CanonicalModel { id, name, created, owned_by }

// /models/{model}
CanonicalModelInfo { id, name, created, owned_by }

// /embeddings
CanonicalEmbeddingRequest { model, input, encoding_format?, dimensions? }
CanonicalEmbeddingResponse { data: [{index, embedding}], model, usage }

// /rerank
CanonicalRerankRequest { model, query, documents, top_n?, return_documents? }
CanonicalRerankResponse { results: [{index, relevance_score, document?}], model }

4.10 设计说明

system 为什么独立于 messages 不同协议对系统消息的处理方式不同:有的用顶层字段,有的嵌入 messages 数组。独立 system 语义更清晰Decoder 提取、Encoder 注入,适配时按各自协议规范处理即可。

流式事件为什么用类型化事件模型? 类型化事件有显式的 start/stop 生命周期语义明确。delta 模型需要状态机推断语义,增加双向转换的复杂度。新协议适配时,如协议原生支持类似生命周期,直接映射;如使用 delta 模型,需在 StreamDecoder 中用状态机转换。

user_idoutput_formatparallel_tool_use 为什么独立为顶层字段? 这些是多个协议共同表达的公共语义,只是字段名、嵌套位置或语义方向(正/反)有差异。统一为协议中立的顶层字段,避免命名偏向。适配新协议时,对照各字段在 Canonical 中的定义实现提取Decoder和注入Encoder即可。


5. Protocol Adapter 接口

5.1 TargetProvider

每次转换调用传入的目标上游信息。Adapter 通过 Provider 获取认证和配置,不需要理解其他协议的 Header 格式

TargetProvider {
    base_url: String                          // 上游 API 地址
    api_key: String                           // 上游认证密钥
    model_name: String                        // 目标模型名(调用方完成映射后传入)
    adapter_config: Map<String, Any>          // 协议专属配置
}

adapter_config 由各 Adapter 自行定义所需的 key引擎不感知含义原样透传。各 Adapter 需在协议适配文档中声明自己从 adapter_config 读取的 key 列表和默认值。

5.2 ProtocolAdapter

每种协议的完整适配器,融入所有接口类型的处理能力。所有 Adapter 通过代码注册,不支持动态增减。新协议可通过空函数(如不支持的扩展层接口直接透传)渐进适配,接口集中定义方便明确所有应实现的内容。

interface ProtocolAdapter {
    protocolName(): String
    protocolVersion(): String
    supportsPassthrough(): Boolean        // 同协议透传开关,默认 true

    // HTTP 级别
    buildUrl(nativePath: String, interfaceType: InterfaceType): String   // 始终返回有效 URL未知接口返回 nativePath
    buildHeaders(provider: TargetProvider): Map<String, String>
    supportsInterface(interfaceType: InterfaceType): Boolean

    // 核心层Chat
    decodeRequest(raw): CanonicalRequest
    encodeRequest(canonical, provider): RawRequest
    decodeResponse(raw): CanonicalResponse
    encodeResponse(canonical): RawResponse

    // 核心层:流式
    createStreamDecoder(): StreamDecoder
    createStreamEncoder(): StreamEncoder

    // 错误编码
    encodeError(error: ConversionError): RawResponse

    // 扩展层
    decodeModelsResponse(raw): CanonicalModelList
    encodeModelsResponse(canonical): RawResponse
    decodeModelInfoResponse(raw): CanonicalModelInfo
    encodeModelInfoResponse(canonical): RawResponse
    decodeEmbeddingRequest(raw): CanonicalEmbeddingRequest
    encodeEmbeddingRequest(canonical, provider): RawRequest
    decodeEmbeddingResponse(raw): CanonicalEmbeddingResponse
    encodeEmbeddingResponse(canonical): RawResponse
    decodeRerankRequest(raw): CanonicalRerankRequest
    encodeRerankRequest(canonical, provider): RawRequest
    decodeRerankResponse(raw): CanonicalRerankResponse
    encodeRerankResponse(canonical): RawResponse
}

buildHeaders 的设计Adapter 只需从 provider 中提取自己协议需要的认证和配置信息,构建自己的 Header 格式。不再需要理解其他协议的 Header。

5.3 InterfaceType

InterfaceType = Enum<
    CHAT, MODELS, MODEL_INFO, EMBEDDINGS, RERANK,
    AUDIO, IMAGES          // 预留:多模态扩展时启用
>

5.4 StreamDecoder / StreamEncoder

interface StreamDecoder {
    processChunk(rawChunk): Array<CanonicalStreamEvent>
    flush(): Array<CanonicalStreamEvent>
}

interface StreamEncoder {
    encodeEvent(event): Array<RawSSEChunk>
    flush(): Array<RawSSEChunk>
}

5.5 AdapterRegistry

interface AdapterRegistry {
    register(adapter: ProtocolAdapter): void
    get(protocolName: String): ProtocolAdapter
    listProtocols(): Array<String>
}

6. Conversion Engine转换引擎

6.1 ConversionEngine

ConversionEngine 是无状态的格式转换工具,仅做协议间的编解码和字段映射,不持有会话状态,线程安全,可复用。

协议识别clientProtocolproviderProtocol 由调用方确定并传入引擎(详见 §2.2)。

class ConversionEngine {
    registry: AdapterRegistry
    middlewareChain: MiddlewareChain

    registerAdapter(adapter): void
    use(middleware): void

    isPassthrough(clientProtocol, providerProtocol): Boolean {
        return clientProtocol == providerProtocol && registry.get(clientProtocol).supportsPassthrough()
    }

    // 非流式请求转换
    convertHttpRequest(request, clientProtocol, providerProtocol, provider): HttpRequest {
        nativePath = request.url
        interfaceType = detectInterfaceType(nativePath)

        if isPassthrough(clientProtocol, providerProtocol):
            providerAdapter = registry.get(providerProtocol)
            return {url: provider.base_url + nativePath, method: request.method,
                    headers: providerAdapter.buildHeaders(provider), body: request.body}

        clientAdapter = registry.get(clientProtocol)
        providerAdapter = registry.get(providerProtocol)

        providerUrl = providerAdapter.buildUrl(nativePath, interfaceType)
        providerHeaders = providerAdapter.buildHeaders(provider)
        providerBody = convertBody(interfaceType, clientAdapter, providerAdapter, provider, request.body)

        return {url: provider.base_url + providerUrl, method: request.method,
                headers: providerHeaders, body: providerBody}
    }

    // 非流式响应转换
    convertHttpResponse(response, clientProtocol, providerProtocol, interfaceType): HttpResponse {
        if isPassthrough(clientProtocol, providerProtocol): return response

        clientAdapter = registry.get(clientProtocol)
        providerAdapter = registry.get(providerProtocol)
        providerBody = convertResponseBody(interfaceType, clientAdapter, providerAdapter, response.body)

        return {status: response.status, headers: response.headers, body: providerBody}
    }

    // 流式转换:从 provider 协议解码,编码为 client 协议
    createStreamConverter(clientProtocol, providerProtocol, provider): StreamConverter {
        if isPassthrough(clientProtocol, providerProtocol):
            providerAdapter = registry.get(providerProtocol)
            return new PassthroughStreamConverter(providerAdapter.buildHeaders(provider))

        providerAdapter = registry.get(providerProtocol)
        clientAdapter = registry.get(clientProtocol)
        return new CanonicalStreamConverter(
            providerAdapter.createStreamDecoder(), clientAdapter.createStreamEncoder(), middlewareChain)
    }
}

6.2 Body 转换分发

function convertBody(interfaceType, clientAdapter, providerAdapter, provider, body):
    switch interfaceType:
        case CHAT:
            canonical = clientAdapter.decodeRequest(body)
            canonical = middlewareChain.apply(canonical)
            return providerAdapter.encodeRequest(canonical, provider)
        case MODELS:
            return body                   // GET 请求,无 body
        case MODEL_INFO:
            return body                   // GET 请求,无 body
        case EMBEDDINGS:
            if !clientAdapter.supportsInterface(EMBEDDINGS)
                || !providerAdapter.supportsInterface(EMBEDDINGS):
                return body               // 尽力转换:不支持则透传
            return providerAdapter.encodeEmbeddingRequest(
                clientAdapter.decodeEmbeddingRequest(body), provider)
        case RERANK:
            // 同 EMBEDDINGS 模式
        default:
            return body                   // 透传层:原样转发

function convertResponseBody(interfaceType, clientAdapter, providerAdapter, body):
    // 结构与 convertBody 对称CHAT 走 Canonical 深度转换,扩展层走轻量映射,默认透传
    // 各接口的具体响应转换逻辑详见各协议适配文档(附录 E

6.3 StreamConverter

interface StreamConverter {
    processChunk(rawChunk): Array<RawSSEChunk>
    flush(): Array<RawSSEChunk>
}

class PassthroughStreamConverter implements StreamConverter {
    headers: Map<String, String>
    constructor(headers) { this.headers = headers }
    processChunk(rawChunk): Array<RawSSEChunk> { return [rawChunk] }
    flush(): Array<RawSSEChunk> { return [] }
}

class CanonicalStreamConverter implements StreamConverter {
    decoder: StreamDecoder
    encoder: StreamEncoder
    middleware: MiddlewareChain

    processChunk(rawChunk):
        events = decoder.processChunk(rawChunk).map(e => middleware.applyStreamEvent(e))
        return events.flatMap(e => encoder.encodeEvent(e))

    flush():
        return decoder.flush().flatMap(e => encoder.encodeEvent(e)) + encoder.flush()
}

6.4 Middleware

引擎内部的拦截钩子,在 decode → encode 之间对 Canonical 进行变换。

interface ConversionMiddleware {
    intercept(canonical, clientProtocol, providerProtocol, context): canonical | error
    interceptStreamEvent?(event, clientProtocol, providerProtocol, context): event | error
}

ConversionContext { conversionId, interfaceType, timestamp, metadata }
  • intercept 返回修改后的 canonical或返回 ConversionError 以中断转换
  • interceptStreamEvent 同理,返回错误可中断流式转换
  • 多个 Middleware 按注册顺序链式执行,任一中断则后续不再执行

6.5 使用示例

engine = new ConversionEngine()
engine.registerAdapter(new ProtocolAAdapter())
engine.registerAdapter(new ProtocolBAdapter())

// 场景1: 跨协议 Chat 转换
// 入站: /protocol_a/v1/chat/completions
provider = TargetProvider {
    base_url: "https://api.protocol-b.com",
    api_key: "xxx",
    model_name: "model-b",
    adapter_config: { ... }
}
out = engine.convertHttpRequest(inRequest, "protocol_a", "protocol_b", provider)
// 出站: 目标协议路径 + 目标协议 headers + 转换后的 body

// 场景2: /models 跨协议
out = engine.convertHttpRequest(inRequest, "protocol_a", "protocol_b", provider)
// URL: /v1/models通常不变, headers 按目标协议格式重建

// 场景3: 同协议透传
out = engine.convertHttpRequest(inRequest, "protocol_a", "protocol_a", provider)
// client == provider → 剥离前缀, 用 provider 重建 headers 后原样转发

// 场景4: 流式转换(从 provider 协议解码,编码为 client 协议)
converter = engine.createStreamConverter("protocol_a", "protocol_b", provider)
for chunk in upstreamSSE {
    for out in converter.processChunk(chunk) { sendToClient(out) }
}
converter.flush()

7. 流式转换架构

7.1 转换管道

上游 SSE 流
    │
    ├── 同协议: PassthroughStreamConverter用 provider 重建 Headers 后逐块转发)
    │
    └── 跨协议: CanonicalStreamConverter
          StreamDecoder                          StreamEncoder
          ┌───────────┐                         ┌───────────┐
          │ SSE Parser│                         │SSE Writer │
          └─────┬─────┘                         └─────▲─────┘
                │                                     │
          ┌─────▼─────┐     CanonicalEvent[]    ┌─────┴─────┐
          │ Event      │──────────────────────▶│ Event      │
          │ Translator │     ┌──────────┐      │ Translator │
          │ (状态机)    │     │Middleware│      │            │
          └───────────┘     └──────────┘      └───────────┘

7.2 StreamDecoder 通用状态

StreamDecoder 需要跟踪以下通用状态。具体协议的 Decoder 可根据需要扩展:

StreamDecoderState {
    messageStarted: Boolean
    openBlocks: Set<Integer>
    currentBlockType: Map<Integer, String>
    currentBlockId: Map<Integer, String>
    utf8Remainder: Option<ByteArray>          // UTF-8 跨 chunk 安全
    accumulatedUsage: Option<CanonicalUsage>
}

协议特有状态:某些协议的 SSE 格式需要额外状态(如工具调用参数累积、工具调用索引映射等),在各自的协议适配文档中定义。

7.3 事件映射概述

StreamDecoder 将协议原生 SSE 事件翻译为 CanonicalStreamEventStreamEncoder 做反向翻译。映射复杂度取决于协议的 SSE 格式:

  • 类型化事件协议(事件有明确的 start/delta/stop 生命周期):与 CanonicalStreamEvent 接近 1:1 映射,状态机最简单
  • Delta 模型协议(只有增量 delta chunk无显式生命周期需要状态机推断 block 边界,管理工具调用索引和参数累积
  • 缓冲策略:部分协议的 Encoder 需要缓冲 ContentBlockStart 事件,等待首次 ContentBlockDelta 时合并输出

各协议的具体事件映射表详见各自的协议适配文档(附录 E


8. 扩展点设计

8.1 新协议接入

  1. 实现 ProtocolAdapterURL 映射 + Header 映射 + 各接口编解码)
  2. 注册到 AdapterRegistry
  3. 完成

按附录 D 的清单模板逐项确认,所有项目确认后即可与引擎对接。

8.2 多模态扩展

Canonical Model 已预留 ImageBlock / AudioBlock / VideoBlock / FileBlock。实现路径

  1. 在各 ProtocolAdapter 中实现多模态块的编解码
  2. 在 StreamDecoder/StreamEncoder 中处理多模态增量数据

8.3 有状态特性扩展

interface StatefulMiddleware extends ConversionMiddleware {
    stateStore: SessionStateStore
}

适用场景:跨轮次保留协议特有的状态(如 thinking signature

8.4 特性降级策略

当 Canonical 公共字段在目标协议中无直接等价物时:

  • 有语义等价物 → 自动映射
  • 无等价物 → 丢弃,日志 warn
  • 有替代方案 → 降级策略处理(如注入合成工具实现 JSON 模式)

各协议的具体降级规则详见各自的协议适配文档(附录 E

8.5 自定义接口支持

interface CustomInterfaceHandler {
    interfaceType(): InterfaceType
    matchUrl(url): Boolean
    convertRequest(client, provider, raw): raw
    convertResponse(client, provider, raw): raw
}
engine.registerCustomHandler(handler)

9. 错误处理

9.1 错误分类

ConversionError { code: ErrorCode, message, clientProtocol?, providerProtocol?,
                  interfaceType?, details?, cause? }

ErrorCode = Enum<
    INVALID_INPUT,              // 请求格式不符合协议规范
    MISSING_REQUIRED_FIELD,     // 缺少必填字段
    INCOMPATIBLE_FEATURE,       // 特性在目标协议不可用
    FIELD_MAPPING_FAILURE,      // 字段映射逻辑错误
    TOOL_CALL_PARSE_ERROR,      // 工具调用参数解析失败
    JSON_PARSE_ERROR,           // JSON 解析失败
    STREAM_STATE_ERROR,         // 流式状态机异常
    UTF8_DECODE_ERROR,          // UTF-8 解码错误
    PROTOCOL_CONSTRAINT_VIOLATION, // 违反协议约束
    ENCODING_FAILURE,           // 编码失败
    INTERFACE_NOT_SUPPORTED     // 目标协议不支持此接口
>

9.2 错误处理策略

ErrorHandler { mode: "strict" | "lenient" }

strict:  任何错误抛出异常
lenient: 尽力继续
    INCOMPATIBLE_FEATURE → 降级继续
    INTERFACE_NOT_SUPPORTED → 透传或返回空响应
    TOOL_CALL_PARSE_ERROR → 保留原始内容继续
    PROTOCOL_CONSTRAINT_VIOLATION → 自动修复

不支持接口的处理INTERFACE_NOT_SUPPORTED

策略 适用场景 实现
透传 上游可能有自己的实现 URL+Header 适配后 Body 原样转发
返回空响应 不影响核心功能 返回空列表 {data: []}
返回错误 客户端明确需要此功能 返回 501 或协议格式错误

具体策略通过配置或 Middleware 决定。

9.3 错误响应格式

转换失败时,错误响应用**客户端协议client protocol**的格式编码。由 clientAdapter.encodeError(error) 完成。各协议的错误响应 JSON 结构和 HTTP 状态码映射详见各自的协议适配文档(附录 E

Middleware 中断转换时同理,引擎调用 clientAdapter.encodeError 将 ConversionError 编码为客户端可理解的格式。


附录 A模块依赖

┌──────────────────────────────────────────────────┐
│                ConversionEngine                   │
│  门面HTTP 转换 / 透传判断 / 流式转换              │
│  无状态;协议识别见 §2.2                            │
├──────────────────────────────────────────────────┤
│              TargetProvider                       │
│  base_url / api_key / model_name / adapter_config │
├──────────────────┬───────────────────────────────┤
│ AdapterRegistry  │       MiddlewareChain          │
├──────────────────┴───────────────────────────────┤
│  StreamConverter: Passthrough | Canonical         │
├──────────────────────────────────────────────────┤
│  ProtocolAdapter: 各协议实现                      │
│  · buildHeaders(provider) · URL 映射             │
│  · Chat/Models/ModelInfo/Embeddings/Rerank/... 编解码  │
│  · encodeError · StreamDecoder / StreamEncoder    │
├──────────────────────────────────────────────────┤
│  Canonical Model (Core + Extended)                │
├──────────────────────────────────────────────────┤
│  Error Handling                                   │
├──────────────────────────────────────────────────┤
│  Utility: UTF-8 Buffer / SSE Parser / Detector   │
└──────────────────────────────────────────────────┘

附录 B接口速查

// ─── 核心入口 ───
ConversionEngine
  .registerAdapter(adapter)
  .use(middleware)
  .isPassthrough(clientProtocol, providerProtocol): Boolean
  .convertHttpRequest(request, clientProtocol, providerProtocol, provider): HttpRequest
  .convertHttpResponse(response, clientProtocol, providerProtocol, interfaceType): HttpResponse
  .createStreamConverter(clientProtocol, providerProtocol, provider): StreamConverter

// ─── 目标上游信息 ───
TargetProvider
  .base_url: String
  .api_key: String
  .model_name: String
  .adapter_config: Map<String, Any>

// ─── URL 路由 ───
// 协议识别见 §2.2;出站: provider.base_url + 目标协议原生路径

// ─── 协议适配器 ───
ProtocolAdapter
  .protocolName() / .protocolVersion() / .supportsPassthrough()
  .buildUrl(nativePath, type) / .buildHeaders(provider) / .supportsInterface(type)
  .decodeRequest(raw) / .encodeRequest(canonical, provider)
  .decodeResponse(raw) / .encodeResponse(canonical)
  .createStreamDecoder() / .createStreamEncoder()
  .encodeError(error): RawResponse
  .decodeModelsResponse / .encodeModelsResponse
  .decodeModelInfoResponse / .encodeModelInfoResponse
  .decodeEmbeddingRequest / .encodeEmbeddingRequest(canonical, provider) / ...Response
  .decodeRerankRequest / .encodeRerankRequest(canonical, provider) / ...Response

// ─── 流式处理 ───
StreamConverter: .processChunk(raw) / .flush()
  ├─ PassthroughStreamConverter   [raw] → [raw](用 provider 重建 Headers
  └─ CanonicalStreamConverter     decode → middleware → encode

// ─── 接口类型 ───
InterfaceType = CHAT | MODELS | MODEL_INFO | EMBEDDINGS | RERANK | AUDIO | IMAGES

附录 C字段晋升规范

原则

Canonical Model 是活的公共契约,不是固定不变的。其字段集反映的是当前已适配协议的公共语义,随协议扩展而演进。

字段分类

分类 判定条件 处理方式
公共字段 ≥2 个协议表达相同含义 升级为 Canonical Model 的正式字段
协议特有字段 仅 1 个协议使用 不在中间层传播;同协议透传时自然保留;跨协议时丢弃
未知字段 新协议带来的新概念 先按特有字段处理;当第二个协议也出现时晋升

晋升流程

1. 发现:适配新协议时,识别出无法映射到现有 Canonical 字段的语义
2. 判定:该语义是否已被 ≥1 个现有协议所表达?
   - 是 → 晋升为 Canonical 公共字段
   - 否 → 暂不纳入,记录在适配清单中
3. 设计:确定字段名(协议中立)、类型、在 Canonical 中的位置
4. 更新:修改 Canonical Model 定义,所有现有 Adapter 的 Decoder/Encoder 同步更新
5. 文档:更新本节说明

晋升示例

字段 晋升原因
user_id 多协议均支持用户标识,但字段名和嵌套位置不同
output_format 多协议均支持控制输出格式,但字段名不同
parallel_tool_use 多协议均支持并行工具调用控制,但语义方向(允许/禁止)不同
top_k 多协议Gemini、OpenAI 兼容厂商、Ollama 等)均支持,但字段名和嵌套位置不同
frequency_penalty / presence_penalty OpenAI 生态广泛使用,多协议均支持类似概念
stop_reason: refusal OpenAI 和 Gemini 均有安全拒绝/内容过滤场景,语义一致

降级规则

当公共字段的目标协议不支持时:

  • 有语义等价物 → 自动映射
  • 无等价物 → 丢弃,日志 warn
  • 有替代方案 → 降级策略处理

附录 D协议适配清单模板

适配新协议时,按以下清单逐项确认。所有项目确认后即可与引擎对接。

方法调用时机:理解各方法何时被引擎调用,有助于正确实现:

  • decodeRequest / encodeRequest:引擎在 convertBody(CHAT) 中调用§6.2),对每个 Chat 请求执行 decode → middleware → encode 管道
  • decodeResponse / encodeResponse:引擎在 convertResponseBody(CHAT) 中调用,对每个 Chat 响应执行反向管道
  • decodeXxxRequest / encodeXxxRequest:扩展层接口仅在 supportsInterface 返回 true 时被调用§6.2 convertBody 分支);返回 false 时引擎直接透传 body
  • createStreamDecoder / createStreamEncoder:引擎在 createStreamConverter 中调用§6.1Decoder 来自 provider 协议(解码上游 SSEEncoder 来自 client 协议(编码给客户端)
  • buildHeaders:每次请求出站时调用,同协议透传也会调用
  • encodeError:转换失败或 Middleware 中断时调用,使用 client 协议格式编码错误响应

D.1 协议基本信息

项目 说明
协议名称 用于 URL 前缀和 Adapter 注册的唯一标识
协议版本 当前适配的 API 版本
Base URL API 服务地址
认证方式 Header 名称和格式

D.2 接口识别

项目 说明
URL 路径模式 列出所有接口的 URL 路径和对应的 InterfaceType
接口能力矩阵 每种 InterfaceType 的支持状态(supportsInterface
URL 映射表 每种 InterfaceType 的目标 URL 路径(buildUrl

D.3 请求头构建

项目 说明
认证头 如何从 provider.api_key 构建认证 Header
必需 Header 协议要求的固定 Header如 Content-Type
可选 Header 根据功能动态添加的 Header
adapter_config 契约 定义本 Adapter 从 provider.adapter_config 读取的 key 列表和默认值

D.4 核心层 — Chat 请求编解码

逐字段对照 §4.1 CanonicalRequest 和 §4.2 RequestParameters 确认映射关系。

Decoder协议 → Canonical

项目 说明
系统消息 如何提取为 canonical.system(顶层字段 / messages 中 / 不支持)
消息角色 协议角色与 Canonical 角色§4.3system/user/assistant/tool的映射
内容块 逐类型对照 §4.3 ContentBlock 联合体,确认每种类型的解码规则
工具定义 对照 §4.4 CanonicalTool确认字段名映射
工具选择 对照 §4.4 ToolChoice 联合体,确认各变体的映射规则
参数映射 对照 §4.2 RequestParameters逐字段确认映射和类型转换
公共字段 逐字段对照 §4.1 中 user_idoutput_formatparallel_tool_usethinking,确认提取规则
协议特有字段 仅本协议使用的字段列表,以及处理方式(忽略/记录)
协议约束 消息顺序要求、角色交替要求、必填字段等

EncoderCanonical → 协议)

项目 说明
模型名称 使用 provider.model_name 覆盖 canonical.model
系统消息注入 如何将 canonical.system 编码为协议格式
消息编码 各角色的编码规则(特别注意 role 映射、content 结构差异)
角色约束处理 是否需要 enforceAlternation具体策略
工具编码 对照 §4.4 确认 tools/tool_choice 的编码规则
参数编码 对照 §4.2 确认参数映射
公共字段编码 逐字段确认注入规则
降级处理 对照 §8.4 三级降级策略(自动映射/丢弃/替代方案),确认每个不支持字段的处理方式

D.5 核心层 — Chat 响应编解码

逐字段对照 §4.7 CanonicalResponse 确认映射关系。

Decoder协议 → Canonical

项目 说明
响应结构 协议响应的顶层结构解析
内容块解码 逐类型对照 §4.3 ContentBlock 联合体,确认解码规则
停止原因 协议原生值与 §4.7 StopReason 枚举的映射表
Token 用量 对照 §4.7 CanonicalUsage逐字段确认映射和命名差异
推理内容 ThinkingBlock 的解码规则
协议特有内容 仅本协议返回的特有字段的处理(忽略/丢弃/记录)

EncoderCanonical → 协议)

项目 说明
响应结构 如何从 CanonicalResponse 构建协议响应的顶层结构
内容块编码 各 ContentBlock 类型到协议格式的编码规则
停止原因 §4.7 StopReason 到协议原生值的映射表
Token 用量 CanonicalUsage 到协议 usage 字段的编码规则
推理内容 ThinkingBlock 的编码规则
降级处理 Canonical 中协议不支持的 stop_reason 等值的降级映射

D.6 核心层 — 流式编解码

对照 §4.8 CanonicalStreamEvent 和 §7.2 StreamDecoderState 确认映射和状态设计。

StreamDecoder协议 SSE → Canonical 事件)

项目 说明
SSE 格式 协议的 SSE 事件格式named events vs delta chunks
事件映射表 每种协议 SSE 事件 → §4.8 CanonicalStreamEvent 的映射规则
状态机设计 在 §7.2 通用状态基础上,声明本协议需要的额外状态
UTF-8 安全 是否需要处理跨 chunk 的 UTF-8 截断
特殊情况 工具调用参数乱序、无限空白检测、延迟字段等

StreamEncoderCanonical 事件 → 协议 SSE

项目 说明
事件映射表 §4.8 每个 CanonicalStreamEvent → 协议 SSE chunk 的映射规则
缓冲策略 哪些事件需要缓冲、何时输出
SSE 格式 event type / data 字段的编码方式
结束标记 如何输出流结束信号(如 [DONE]

D.7 扩展层接口

对每种扩展层接口逐项确认。当前扩展层 InterfaceType 见 §5.3 枚举MODELS、MODEL_INFO、EMBEDDINGS、RERANK随协议扩展可能增加。

项目 说明
接口是否存在 该协议是否原生支持此接口
URL 路径 接口的 URL 路径
请求格式 对照 §4.9 扩展层 Canonical Models确认请求体映射
响应格式 对照 §4.9 扩展层 Canonical Models确认响应体映射
不支持时策略 透传 / 返回空响应 / 返回错误

D.8 错误编码

项目 说明
错误响应格式 协议的错误响应 JSON 结构
encodeError §9.1 ConversionError → 协议错误格式的编码规则
HTTP 状态码 协议常用的错误状态码映射

D.9 自检清单

章节 检查项
D.2 [ ] 所有 InterfaceType 的 supportsInterface 返回值已确定
D.2 [ ] 所有 InterfaceType 的 buildUrl 映射已确定
D.3 [ ] buildHeaders(provider) 已实现adapter_config 契约已文档化
D.4 [ ] Chat 请求的 Decoder 和 Encoder 已实现(逐字段对照 §4.1/§4.2
D.4 [ ] 角色映射和消息顺序约束已处理
D.4 [ ] 工具调用tool_use / tool_result的编解码已处理
D.4 [ ] 协议特有字段已识别并确定处理方式(忽略/降级)
D.5 [ ] Chat 响应的 Decoder 和 Encoder 已实现(逐字段对照 §4.7
D.5 [ ] stop_reason 映射表已确认
D.5 [ ] usage 字段映射已确认
D.6 [ ] 流式 StreamDecoder 和 StreamEncoder 已实现(对照 §4.8
D.7 [ ] 扩展层接口的编解码已实现(支持的接口)
D.8 [ ] encodeError 已实现

附录 E协议适配文档索引

协议 适配文档
OpenAI conversion_openai.md
Anthropic conversion_anthropic.md

新增协议时,按附录 D 模板编撰适配文档,并将链接添加到上表。