1
0
Files
nex/openspec/specs/unified-proxy-handler/spec.md
lanyuanxiaoyao b7e205f4b6 refactor: 优化 URL 路径拼接,修复 /v1 重复问题
## 主要变更

**核心修改**:
- 路由定义:/:protocol/v1/*path → /:protocol/*path
- proxy_handler:nativePath 直接使用 path 参数,不添加 /v1 前缀
- OpenAI 适配器:DetectInterfaceType 和 BuildUrl 去掉 /v1 前缀
- Anthropic 适配器:保持 /v1 前缀(Claude Code 兼容)

**URL 格式变化**:
- OpenAI: /openai/v1/chat/completions → /openai/chat/completions
- Anthropic: /anthropic/v1/messages (保持不变)

**base_url 配置**:
- OpenAI: 配置到版本路径,如 https://api.openai.com/v1
- Anthropic: 不配置版本路径,如 https://api.anthropic.com

## 测试验证

- 所有单元测试通过
- 所有集成测试通过
- 真实 API 测试验证成功
- 跨协议转换正常工作

## 文档更新

- 更新 backend/README.md URL 格式说明
- 同步 OpenSpec 规范文件
2026-04-21 20:21:17 +08:00

9.5 KiB
Raw Blame History

Unified Proxy Handler

Purpose

实现统一的代理处理器ProxyHandler替代独立的 OpenAI 和 Anthropic Handler通过 ConversionEngine 和 RoutingService 支持多协议的请求转换与转发。

Requirements

Requirement: 实现统一代理 Handler

系统 SHALL 实现统一的 ProxyHandler替代现有的 OpenAIHandler 和 AnthropicHandler。

ProxyHandler SHALL 依赖 ConversionEngine、ProviderClient、RoutingService、StatsService。

Scenario: 从 URL 提取客户端协议

  • WHEN 收到 /{protocol}/{path} 格式的请求
  • THEN SHALL 从 URL 第一段提取 protocol 作为 clientProtocol
  • THEN SHALL 剥离前缀得到 nativePath
  • THEN nativePath SHALL 不添加任何前缀,直接使用 path 参数

Scenario: 协议前缀必须是已注册协议

  • WHEN 收到的 URL 前缀不是已注册的协议名称
  • THEN SHALL 返回 404 错误

Scenario: 接口类型识别

  • WHEN 提取 nativePath 后
  • THEN SHALL 通过 ConversionEngine.convertHttpRequest 内部调用 clientAdapter.detectInterfaceType(nativePath) 识别接口类型
  • THEN 未知路径 SHALL 使用透传模式

Requirement: 非流式请求处理流程

ProxyHandler SHALL 按以下流程处理非流式请求。

Scenario: 完整转换流程

  • WHEN 收到非流式请求
  • THEN SHALL 解析请求体为 JSON
  • THEN SHALL 调用 RoutingService.Route(modelName) 获取路由结果
  • THEN SHALL 从路由结果的 Provider.Protocol 获取 providerProtocol
  • THEN SHALL 构建 TargetProviderbase_url, api_key, model_name, adapter_config
  • THEN SHALL 调用 engine.convertHttpRequest(body, clientProtocol, providerProtocol, provider)
  • THEN SHALL 调用 providerClient.Send(ctx, requestSpec)
  • THEN SHALL 调用 engine.convertHttpResponse(response, clientProtocol, providerProtocol, interfaceType)
  • THEN SHALL 将转换后的响应返回给客户端

Scenario: 路由失败处理

  • WHEN RoutingService.Route 返回错误
  • THEN SHALL 使用 clientProtocol 对应的 Adapter.encodeError 编码错误响应
  • THEN SHALL 返回适当的 HTTP 状态码

Scenario: 上游请求失败处理

  • WHEN ProviderClient.Send 返回错误
  • THEN SHALL 使用 clientProtocol 对应的 Adapter.encodeError 编码错误响应
  • THEN SHALL 包装原始错误信息

Requirement: 流式请求处理流程

ProxyHandler SHALL 按以下流程处理流式请求。

Scenario: 流式转换流程

  • WHEN 请求中 stream=true 或接口类型为 CHAT 且请求体含 stream:true
  • THEN SHALL 执行与非流式相同的前置处理(路由、构建 TargetProvider
  • THEN SHALL 调用 engine.convertHttpRequest 获取 HTTPRequestSpec
  • THEN SHALL 调用 providerClient.SendStream(ctx, requestSpec) 获取流
  • THEN SHALL 调用 engine.createStreamConverter(clientProtocol, providerProtocol, provider)
  • THEN SHALL 设置响应 Header Content-Type: text/event-stream
  • THEN SHALL 对每个流 chunk 调用 converter.processChunk 并写入响应
  • THEN SHALL 在流结束时调用 converter.flush() 刷新缓冲

Scenario: 同协议透传流式

  • WHEN clientProtocol == providerProtocol
  • THEN SHALL 直接将上游 SSE 字节流写入响应
  • THEN SHALL NOT 做任何解析或转换

Scenario: 流式错误处理

  • WHEN 流过程中发生错误
  • THEN SHALL 记录错误日志
  • THEN SHALL 关闭响应流

Requirement: 统计记录

ProxyHandler SHALL 记录请求统计。

Scenario: 异步记录统计

  • WHEN 请求处理完成(成功或失败)
  • THEN SHALL 异步调用 StatsService.Record
  • THEN SHALL NOT 阻塞响应返回

Requirement: GET 请求透传

ProxyHandler SHALL 支持 GET 请求的扩展层接口代理。

Scenario: Models 接口<E68EA5><E58FA3>

  • WHEN 收到 GET /{protocol}/v1/models 请求
  • THEN SHALL 执行路由和协议识别
  • THEN SHALL 调用 engine.convertHttpRequestGET 请求 body 为空)
  • THEN SHALL 调用 providerClient.Send 发送请求
  • THEN SHALL 调用 engine.convertHttpResponse 转换响应格式
  • THEN SHALL 返回转换后的响应

Requirement: 代理请求路由

ProxyHandler SHALL 使用统一模型 ID 路由所有代理请求。

Scenario: 提取统一模型 ID

  • WHEN 收到 Chat、Embeddings 或 Rerank 接口的 POST 请求(含请求体)
  • THEN SHALL 调用客户端协议 adapter 的 ExtractModelName(body, ifaceType) 提取 model 值
  • THEN SHALL 调用 ParseUnifiedModelID 解析得到 providerID 和 modelName
  • THEN SHALL 调用 RoutingService.RouteByModelName(providerID, modelName) 路由

Scenario: GET 请求或无请求体

  • WHEN 收到 GET 请求或请求体为空或请求体中无法提取 model 字段
  • THEN SHALL 走 forwardPassthrough 透传到上游供应商(兼容未适配的客户端和无 body 请求)

Scenario: 无效的统一模型 ID

  • WHEN 请求体中 model 字段不是有效的统一模型 ID 格式(不含 /
  • THEN SHALL 走 forwardPassthrough 透传到上游供应商(兼容使用原始模型名的客户端)

Scenario: 模型不存在

  • WHEN 解析统一模型 ID 后,数据库中找不到对应的 provider_id + model_name 组合
  • THEN SHALL 返回错误响应,状态码为 404

Scenario: 模型已禁用

  • WHEN 解析统一模型 ID 后,对应的模型 enabled 为 false
  • THEN SHALL 返回错误响应,状态码为 404

Scenario: 供应商已禁用

  • WHEN 解析统一模型 ID 后,对应的供应商 enabled 为 false
  • THEN SHALL 返回错误响应,状态码为 404

Requirement: 同协议 Smart Passthrough

当客户端协议与供应商协议相同时ProxyHandler SHALL 使用 Smart Passthrough 处理 Chat、Embedding、Rerank 请求。

Scenario: 同协议非流式请求

  • WHEN 客户端协议 == 供应商协议,且为非流式请求
  • THEN SHALL 调用 adapter 的 RewriteRequestModelName(body, modelName, ifaceType) 将请求体中 model 从统一 ID 改写为上游模型名
  • THEN SHALL 构建 URL 和 Headers同当前透传逻辑
  • THEN SHALL 发送改写后的请求体到上游
  • THEN SHALL 调用 adapter 的 RewriteResponseModelName(resp.Body, unifiedModelID, ifaceType) 将响应中 model 从上游名改写为统一 ID
  • THEN SHALL NOT 对 body 做全量 decode → encode保持未改写字段的原始 bytes

Scenario: 同协议流式请求

  • WHEN 客户端协议 == 供应商协议,且为流式请求
  • THEN SHALL 对请求体做 RewriteRequestModelName 改写 model 字段
  • THEN SHALL 逐 SSE chunk 调用 RewriteResponseModelName 改写响应中 model 字段
  • THEN SHALL NOT 对 chunk 做全量 decode → encode

Scenario: Smart Passthrough 保真性

  • WHEN 客户端发送含未知参数的请求(如 {"model":"openai/gpt-4","some_new_param":"value"}
  • THEN 上游 SHALL 收到 {"model":"gpt-4","some_new_param":"value"}
  • THEN some_new_param SHALL 保持原始值不变,不丢失、不改变类型

Requirement: 跨协议完整转换

当客户端协议与供应商协议不同时ProxyHandler SHALL 使用全量转换路径。

Scenario: 跨协议非流式请求

  • WHEN 客户端协议 != 供应商协议
  • THEN SHALL 走 ConvertHttpRequest 全量转换encoder 中 provider.ModelName 覆盖 model
  • THEN SHALL 走 ConvertHttpResponse 全量转换modelOverride 参数覆写 canonical.Model

Scenario: 跨协议流式请求

  • WHEN 客户端协议 != 供应商协议,且为流式请求
  • THEN SHALL 走 CreateStreamConverter 全量转换modelOverride 参数覆写流式 canonical 事件中的 Model

Requirement: 模型列表本地聚合

ProxyHandler SHALL 从数据库聚合返回模型列表,不再透传上游。

Scenario: GET /v1/models

  • WHEN 收到 GET /{protocol}/v1/models 请求
  • THEN SHALL 从数据库查询所有 enabled 的模型(关联 enabled 的供应商)
  • THEN SHALL 组装 CanonicalModelList,每个模型的 ID 字段为统一模型 IDprovider_id/model_nameName 字段为 model_nameOwnedBy 字段为 provider_id
  • THEN SHALL 使用客户端协议的 adapter 编码响应
  • THEN SHALL NOT 请求上游供应商

Scenario: 无可用模型

  • WHEN 数据库中没有 enabled 的模型
  • THEN SHALL 返回空列表

Requirement: 模型详情本地查询

ProxyHandler SHALL 从数据库查询返回模型详情,不再透传上游。

Scenario: GET /v1/models/{unified_id}

  • WHEN 收到 GET /{protocol}/v1/models/{provider_id}/{model_name} 请求
  • THEN SHALL 调用 adapter 的 ExtractUnifiedModelID 提取统一模型 ID
  • THEN SHALL 解析统一模型 ID 得到 providerID 和 modelName
  • THEN SHALL 从数据库查询对应的模型和供应商
  • THEN SHALL 组装 CanonicalModelInfoID 字段为统一模型 IDprovider_id/model_nameName 字段为 model_nameOwnedBy 字段为 provider_id
  • THEN SHALL 使用客户端协议的 adapter 编码响应
  • THEN SHALL NOT 请求上游供应商

Scenario: 模型详情不存在

  • WHEN 统一模型 ID 对应的模型不存在或已禁用
  • THEN SHALL 返回错误响应,状态码为 404

Requirement: 统计记录

ProxyHandler SHALL 使用 providerID 和 modelName 记录使用统计。

Scenario: 异步记录统计

  • WHEN 代理请求成功完成
  • THEN SHALL 异步调用 StatsService.Record(providerID, modelName)