# 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 构建 TargetProvider(base_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 接口��理 - **WHEN** 收到 GET /{protocol}/v1/models 请求 - **THEN** SHALL 执行路由和协议识别 - **THEN** SHALL 调用 engine.convertHttpRequest(GET 请求 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 字段为统一模型 ID(`provider_id/model_name`),Name 字段为 model_name,OwnedBy 字段为 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 组装 `CanonicalModelInfo`,ID 字段为统一模型 ID(`provider_id/model_name`),Name 字段为 model_name,OwnedBy 字段为 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)`