fix: 修正 conversion 代理路径和错误边界
This commit is contained in:
@@ -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 protocol(URL 前缀 / 配置映射 / 任意方式) │ │
|
||||
│ │ 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.1),Decoder 来自 provider 协议(解码上游 SSE),Encoder 来自 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 请求头构建
|
||||
|
||||
Reference in New Issue
Block a user