1
0

fix: 修正 conversion 代理路径和错误边界

This commit is contained in:
2026-04-25 23:12:54 +08:00
parent f5c82b6980
commit 2c043c6cf7
25 changed files with 2020 additions and 214 deletions

View File

@@ -31,7 +31,7 @@
|------|------|
| **完整 HTTP 接口体系转换** | 覆盖 /models、/embeddings、/rerank 等全部接口的 URL 路由映射、请求头转换、请求体/响应体格式转换 |
| **输入输出解耦** | 客户端协议和服务端协议独立指定,任意组合 |
| **同协议透传** | client == provider 时跳过转换,零语义损失、零序列化开销 |
| **同协议透传** | client == provider 时跳过 Canonical 全量转换,保持协议语义 |
| **尽力转换** | 能对接的参数尽可能对接,不能对接的各自忽略,保障最大覆盖面 |
| **协议可扩展** | 添加新协议只需实现 Adapter不修改核心引擎 |
| **流式优先** | SSE 流式转换作为核心能力,与非流式同等地位 |
@@ -75,8 +75,8 @@
│ │ │ │
│ │ 入站: /{protocol}/{native_path} │ │
│ │ │ │
│ │ /<protocol_a>/v1/chat/completions → client=protocol_a, /v1/... │ │
│ │ /<protocol_b>/v1/messages → client=protocol_b, /v1/... │ │
│ │ /openai/chat/completions → client=openai, /chat/completions │ │
│ │ /anthropic/v1/messages → client=anthropic, /v1/messages │ │
│ │ │ │
│ │ Step 1: 识别 client protocolURL 前缀 / 配置映射 / 任意方式) │ │
│ │ Step 2: 剥离前缀 → 得到 nativePath │ │
@@ -112,17 +112,18 @@
### 2.2 URL 路由规则
调用方负责识别协议并剥离前缀,将 `nativePath``clientProtocol``providerProtocol` 传入引擎。调用方可自行决定识别方式URL 前缀、配置映射等)
调用方负责识别协议并剥离前缀,将 `nativePath``clientProtocol``providerProtocol` 传入引擎。网关只剥离第一段协议前缀,不对剩余路径做版本号归一化;`/v1` 是否存在属于协议原生路径,由对应 ProtocolAdapter 按本地 API reference 识别和映射
```
入站 URL 调用方剥离前缀后 引擎出站
──────────────────────────────────────────────────────────────────────────────
/<protocol_a>/v1/chat/completions → /v1/chat/completions → 目标协议路径
/<protocol_b>/v1/messages → /v1/messages目标协议路径
/<protocol_a>/v1/models → /v1/models → /v1/models通常不变
/openai/chat/completions → /chat/completions → /chat/completions
/openai/models → /models /models
/anthropic/v1/messages → /v1/messages → /v1/messages
/anthropic/v1/models → /v1/models → /v1/models
```
出站到上游 API 时使用服务端协议原生路径(无前缀)。
出站到上游 API 时使用服务端协议原生路径(无网关协议前缀),并由 `provider.BaseURL + providerAdapter.BuildUrl(nativePath, interfaceType)` 组合得到真实 URL。OpenAI `base_url` 配置到版本路径一级(如 `https://api.openai.com/v1`Anthropic `base_url` 配置到域名级(如 `https://api.anthropic.com`)。
### 2.3 请求处理流程
@@ -146,14 +147,14 @@
│ 响应处理 │ 原样返回 │ modelOverride非空时 │ Decode→modelOverride │
│ │ │ RewriteResponseModelName(body) │ →Encode │
│ │ │ │ │
│ 流式处理 │ chunk→[chunk] │ chunk→RewriteResponseModelName │ Decode→Middleware │
│ │ │ →[rewritten] │ →modelOverride→Encode │
│ 流式处理 │ raw SSE frame原样透传 │ SSE frame→仅改写data JSON中的model │ Decode→Middleware │
│ │ 保留[DONE]和frame边界 │ 解析失败则输出原frame继续处理 │ →modelOverride→Encode │
│ │ │ │ │
│ 性能开销 │ 最低 │ 低仅JSON字段改写 │ 高(完整序列化) │
└────────────────┴─────────────────────────┴────────────────────────────────────────┘
```
**智能透传的设计动机**:同协议场景下,若仅需改写 `model` 字段(如客户端请求模型 "X",上游需要模型 "Y"),无需完整解码/编码。直接在 JSON 层面手术式改写该字段,既保留原始请求的所有细节,又避免序列化开销
**智能透传的设计动机**:同协议场景下,若仅需改写 `model` 字段(如客户端请求模型 "X",上游需要模型 "Y"),无需进入 Canonical 全量解码/编码。直接在 JSON 层面改写该字段,保持未改写字段的 JSON 内容和类型不变;实现不承诺保留原始字节顺序、空白或对象字段顺序
#### 2.3.2 完整请求处理流程
@@ -162,7 +163,7 @@
┌──────────────┐ ┌──────────────┐
│ URL: │ 调用方完成: 1. 接口识别 │ URL: │
│ /<protocol>/ │ · clientProtocol 2. IsPassthrough? │ 目标协议 │
v1/... │ · nativePath ├─ yes ─┬─ 无 modelOverride → 透传车道 │ 原生路径 │
│ ... │ · nativePath ├─ yes ─┬─ 无 modelOverride → 透传车道 │ 原生路径 │
│ Headers: │ · providerProtocol │ └─ 有 modelOverride → 智能透传车道│ Headers: │
│ 协议原生格式 │ │ │ 目标协议格式 │
│ Body: │ └─ no → 完整转换车道 │ Body: │
@@ -174,7 +175,7 @@
**同协议透传**client == provider 时,仅重建 Header 后原样转发到上游。
**智能透传**:同协议且需改写 model 字段时,最小化 JSON 改写后转发。
**未知接口透传**无法识别的路径URL+Header 适配后 Body 原样转发。
**未知接口透传**无法识别的路径URL+Header 适配后 Body 原样转发;即使请求体存在顶层 `model`,也不做通用猜测
---
@@ -458,11 +459,13 @@ interface ProtocolAdapter {
**`buildHeaders` 的设计**Adapter 只需从 `provider` 中提取自己协议需要的认证和配置信息,构建自己的 Header 格式。不再需要理解其他协议的 Header。
**URL 事实来源**Adapter 的 `detectInterfaceType``buildUrl` 必须以本地 API reference 为事实来源。OpenAI 参考 `docs/api_reference/openai`(忽略 `responses` 目录),其 nativePath 不包含 `/v1`,例如 `/chat/completions``/models``/embeddings`。Anthropic 参考 `docs/api_reference/anthropic`,其 nativePath 保留 `/v1`,例如 `/v1/messages``/v1/models`。不得根据其他协议是否包含 `/v1` 推断当前协议路径。
**智能透传方法的契约**
- `rewriteRequestModelName` / `rewriteResponseModelName` 必须**幂等**(多次调用结果相同)
- Rewrite 方法必须**最小化**(仅修改 model 字段,不触碰其他字段)
- Rewrite 失败时,引擎使用宽容策略:记录警告日志,使用原始 body 继续处理
- `extractModelName` 支持的接口类型CHAT、EMBEDDINGS、RERANK这些接口的请求体包含 model 字段)
- `extractModelName` 支持 adapter 明确适配的接口类型CHAT、EMBEDDINGS、RERANK这些接口的请求体包含 model 字段)。PASSTHROUGH 或未适配接口返回错误或空结果,调用方按无 model 请求透传,不做顶层 `model` 猜测。
### 5.3 InterfaceType
@@ -713,7 +716,7 @@ interface StreamConverter {
| 转换器 | 触发条件 | processChunk | flush |
|--------|---------|--------------|-------|
| `PassthroughStreamConverter` | 同协议 + 无 modelOverride | `[rawChunk]` | `[]` |
| `SmartPassthroughStreamConverter` | 同协议 + 有 modelOverride | `[rewriteResponseModelName(rawChunk)]` | `[]` |
| `SmartPassthroughStreamConverter` | 同协议 + 有 modelOverride | 按 SSE frame 改写 `data` JSON 中的 model失败输出原 frame | 输出缓存中的未完整 frame |
| `CanonicalStreamConverter` | 不同协议 | Decode→Middleware→modelOverride→Encode | decoder.flush()→encoder.flush() |
#### 6.3.3 PassthroughStreamConverter
@@ -732,16 +735,30 @@ class SmartPassthroughStreamConverter implements StreamConverter {
adapter: ProtocolAdapter
modelOverride: String
interfaceType: InterfaceType
buffer: ByteArray
processChunk(rawChunk): Array<RawSSEChunk> {
if rawChunk为空: return []
rewrittenChunk = adapter.rewriteResponseModelName(rawChunk, modelOverride, interfaceType)
if rewrite失败:
log.warn("智能透传改写失败,使用原始 chunk")
return [rawChunk]
return [rewrittenChunk]
buffer.append(rawChunk)
frames = splitCompleteSSEFrames(buffer)
result = []
for frame in frames:
payload = extractDataPayload(frame)
if payload == "[DONE]":
result.append(frame)
continue
rewrittenPayload = adapter.rewriteResponseModelName(payload, modelOverride, interfaceType)
if rewrite失败:
log.warn("智能透传改写失败,使用原始 SSE frame")
result.append(frame)
else:
result.append(rebuildSSEFrameWithData(frame, rewrittenPayload))
return result
}
flush(): Array<RawSSEChunk> {
if buffer为空: return []
return [buffer.drain()] // 未完整 frame 原样输出
}
flush(): Array<RawSSEChunk> { return [] }
}
```
@@ -794,7 +811,9 @@ function createStreamConverter(clientProtocol, providerProtocol, modelOverride,
if isPassthrough(clientProtocol, providerProtocol):
if modelOverride非空:
adapter = registry.get(clientProtocol)
// 解析 SSE frame仅改写 data JSON 中的 model解析失败输出原 frame
return new SmartPassthroughStreamConverter(adapter, modelOverride, interfaceType)
// raw passthrough 保留 SSE frame 边界和 [DONE]
return new PassthroughStreamConverter()
providerAdapter = registry.get(providerProtocol)
@@ -849,7 +868,7 @@ engine.registerAdapter(new OpenAIAdapter())
engine.registerAdapter(new AnthropicAdapter())
// 场景1: 跨协议 Chat 转换
// 入站: /openai/v1/chat/completions
// 入站: /openai/chat/completions
provider = TargetProvider {
base_url: "https://api.anthropic.com",
api_key: "xxx",
@@ -881,7 +900,7 @@ converter.flush()
// 场景6: 同协议流式智能透传
converter = engine.createStreamConverter("openai", "openai", "gpt-4-turbo", CHAT)
// 使用 SmartPassthroughStreamConverter逐 chunk 改写 model 字段
// 使用 SmartPassthroughStreamConverter按 SSE frame 改写 data JSON 中的 model 字段
```
---
@@ -894,10 +913,10 @@ converter = engine.createStreamConverter("openai", "openai", "gpt-4-turbo", CHAT
上游 SSE 流
├─ 同协议 + 无 modelOverride: PassthroughStreamConverter
chunk → [chunk]
raw SSE frame → [raw SSE frame]
├─ 同协议 + 有 modelOverride: SmartPassthroughStreamConverter
chunk → [rewriteResponseModelName(chunk)]
SSE frame → data JSON model rewrite → [SSE frame]
└─ 不同协议: CanonicalStreamConverter
StreamDecoder StreamEncoder
@@ -1025,7 +1044,8 @@ ErrorCode = Enum<
UTF8_DECODE_ERROR, // UTF-8 解码错误
PROTOCOL_CONSTRAINT_VIOLATION, // 违反协议约束
ENCODING_FAILURE, // 编码失败
INTERFACE_NOT_SUPPORTED // 目标协议不支持此接口
INTERFACE_NOT_SUPPORTED, // 目标协议不支持此接口
UNSUPPORTED_MULTIMODAL // 多模态内容块暂不支持跨协议转换
>
```
@@ -1050,7 +1070,7 @@ ErrorCode = Enum<
│ │ - interceptStreamEvent 返回 error → continue │
├─────────────────┼───────────────────────────────────────────────────┤
│ 智能透传 │ 宽容模式:重写失败则使用原始 body/chunk │
│ │ - Rewrite 失败 → log.warn + 返回原始 body/chunk
│ │ - Rewrite 失败 → log.warn + 返回原始 body/SSE frame
├─────────────────┼───────────────────────────────────────────────────┤
│ 请求中间件 │ 严格模式:返回错误则中断整个转换 │
│ │ - intercept 返回 error → 返回 error │
@@ -1072,11 +1092,24 @@ ErrorCode = Enum<
具体策略由 `supportsInterface` 返回值决定:返回 false 时引擎直接透传 body。
**多模态处理**`UNSUPPORTED_MULTIMODAL`Canonical Model 保留 image、audio、video、file 内容块占位,但当前跨协议多模态编解码暂未实现。跨协议完整转换遇到这些内容块时返回 `UNSUPPORTED_MULTIMODAL`;同协议 raw passthrough 和 smart passthrough 不拒绝多模态字段,仍按原协议请求透传或仅改写 model。
### 9.3 错误响应格式
转换失败时,错误响应用**客户端协议client protocol**的格式编码。由 `clientAdapter.encodeError(error)` 完成。各协议的错误响应 JSON 结构和 HTTP 状态码映射详见各自的协议适配文档(附录 E
ConversionEngine 的 `encodeError` 是协议 Adapter 的错误编码能力,用于 SDK 内部或非代理场景把 `ConversionError` 编码为客户端协议格式。各协议的错误响应 JSON 结构和 HTTP 状态码映射详见各自的协议适配文档(附录 E
Middleware 中断转换时同理,引擎调用 clientAdapter.encodeError 将 ConversionError 编码为客户端可理解的格式。
ProxyHandler 对外遵循更明确的代理错误边界:
| 场景 | 响应策略 |
|------|---------|
| 网关层错误 | 返回应用统一 JSON`{"error": "...", "code": "..."}` |
| 上游已返回 HTTP 非 2xx | 透传上游 status、过滤 hop-by-hop 后的 headers、body |
| 未收到上游 HTTP 响应 | 返回 `UPSTREAM_UNAVAILABLE`HTTP 502 |
| 网关层 JSON 解析失败 | 返回 `INVALID_JSON`HTTP 400 |
| 路由统一模型失败 | 返回 `MODEL_NOT_FOUND`HTTP 404 |
| 跨协议转换失败 | 返回 `CONVERSION_FAILED` 或具体转换错误码 |
因此,代理接口中的网关层错误不会再编码成 OpenAI/Anthropic 协议错误格式;只有上游已经返回的错误响应才按上游原样透传。
#### 9.3.1 EncodeError Fallback 行为
@@ -1179,7 +1212,7 @@ ProtocolAdapter
// ─── 流式处理 ───
StreamConverter: .processChunk(raw) / .flush()
├─ PassthroughStreamConverter [raw] → [raw]
├─ SmartPassthroughStreamConverter [raw] → [rewrite(raw)]
├─ SmartPassthroughStreamConverter [SSE frame] → [rewrite(data JSON)]
└─ CanonicalStreamConverter decode → middleware → modelOverride → encode
// ─── 中间件 ───
@@ -1248,7 +1281,7 @@ Canonical Model 是**活的公共契约**,不是固定不变的。其字段集
- `decodeXxxRequest` / `encodeXxxRequest`:扩展层接口仅在 `supportsInterface` 返回 true 时被调用§6.2 convertBody 分支);返回 false 时引擎直接透传 body
- `createStreamDecoder` / `createStreamEncoder`:引擎在 `createStreamConverter` 中调用§6.1Decoder 来自 provider 协议(解码上游 SSEEncoder 来自 client 协议(编码给客户端)
- `buildHeaders`:每次请求出站时调用,同协议透传也会调用
- `encodeError`:转换失败或 Middleware 中断时调用,使用 client 协议格式编码错误响应
- `encodeError`:转换失败或 Middleware 中断时的协议错误编码能力ProxyHandler 的网关层错误使用应用统一格式,不调用协议错误编码
### D.1 协议基本信息
@@ -1268,7 +1301,7 @@ Canonical Model 是**活的公共契约**,不是固定不变的。其字段集
| URL 映射表 | 每种 InterfaceType 的目标 URL 路径(`buildUrl` |
**重要**`detectInterfaceType` 由各协议 Adapter 实现,因为不同协议有不同的 URL 路径约定。例如:
- OpenAI: `/v1/chat/completions` → CHAT
- OpenAI: `/chat/completions` → CHAT
- Anthropic: `/v1/messages` → CHAT
### D.3 请求头构建