- 将 Provider 下游配置由单个 target_format 扩展为多个 target_formats - 保留旧字段 target_format 的启动期迁移逻辑,并标注为弃用兼容代码 - 收敛对外配置与接口返回,Provider 相关响应及 /v1/models 仅暴露 target_formats - 前端 Provider 表单改为多选下游,支持互斥项自动替换 - 新建 Provider 默认选中 openai_chat、codex、claude_chat - 更新示例配置、README、4+1 架构文档及相关测试
8.1 KiB
4+1 Architecture
1. Context
这个项目是一个协议翻译型 LLM Proxy。
它的目标不是“支持所有厂商的所有接口”,而是用一套干净的首版架构,稳定支持:
- 上游协议族
openai_chatopenai_responsesclaude_chatcodex
- 下游协议面
POST /v1/chat/completionsPOST /v1/responsesPOST /v1/messagesGET /v1/models
这个版本不保留 Gemini / Antigravity。配置加载阶段会把旧字段 target_format 规范化为 target_formats,这段弃用兼容逻辑后续会删除;除此之外不再继续扩展旧字段兼容逻辑。
2. Logical View
2.1 Core Pipeline
系统按下面的统一链路工作:
downstream request
-> controller
-> provider lookup
-> header_hook / request_guard
-> translator.translate_request()
-> executor
-> decoder
-> translator.translate_response()
-> response_guard
-> encoder
-> downstream response
补充:hook 在 retry 场景下还可以读取上一轮失败摘要,用于做轻量级重试决策:
last_status_codelast_error_type
2.2 Major Components
ProxyController- 校验下游 route 和
target_formats - 根据 route family 解析本次请求命中的
target_format - 构造标准错误体
- 校验下游 route 和
ProviderManager- 加载 provider 配置
- 维护
provider/model -> provider映射
AuthGroupManager- 加载
auth_groups - 选择
auth_entry - 持久化冷却、禁用与配额运行态
- 加载
ProxyService- 组装整条代理链路
- 使用本次解析出的
target_format选择 translator 和 encoder
ExecutorRegistry- 负责 HTTP / WebSocket 上游连接
Decoder- 将上游流拆成统一事件
TranslatorRegistry- 负责
source_format -> target_format协议适配
- 负责
Encoder- 将统一 chunk 编码成下游协议
Hook- 只负责 header 和 guard
Hook 组件除了 header / guard,还会收到最小重试上下文:
retryauth_group_nameauth_entry_idlast_status_codelast_error_type
2.3 Protocol Families
| family | 用途 |
|---|---|
openai_chat |
OpenAI Chat Completions 语义 |
openai_responses |
OpenAI Responses 语义 |
claude_chat |
Anthropic Messages 语义 |
codex |
OpenAI Responses 家族下的 Codex 特化语义 |
codex 的设计原则:
- 共用
/v1/responses - 共用 Responses encoder
- 保留请求规范化差异
3. Process View
3.1 Downstream Route Contract
target_formats 和 route family 强绑定:
| route | allowed target_formats |
|---|---|
/v1/chat/completions |
openai_chat |
/v1/responses |
openai_responses, codex |
/v1/messages |
claude_chat |
controller 会根据 route 在 target_formats 里解析出本次请求实际命中的 target_format。
- 不匹配时,controller 直接返回 400,不会继续请求上游
- 当前
openai_responses和codex在同一个 provider 内互斥,因为它们共用/v1/responses,但请求语义不同
GET /v1/models 除了模型 id,还会返回模型所属 provider 的:
source_formattarget_formatstransport
3.2 Provider Runtime Contract
Provider 公共运行时字段只有:
nameapitransportsource_formattarget_formatsapi_keyauth_groupproxytimeout_secondsmax_retriesverify_sslmodel_listhook
其中:
source_format- 上游真实协议
target_formats- 当前 provider 对下游暴露的协议集合
transport- HTTP 或 WebSocket
没有公共 stream_format 字段。
Hook 运行时上下文还会暴露最小重试状态:
retryauth_group_nameauth_entry_idlast_status_codelast_error_type
其中 last_error_type 使用 HookErrorType 枚举,当前值为:
TIMEOUTCONNECTION_ERRORWEBSOCKET_ERRORTRANSPORT_ERROR
3.3 Internal Stream Detection
流式识别完全是内部实现细节:
transport = websocket- 按 WebSocket JSON 消息处理
- HTTP
Content-Type = text/event-stream- 按 SSE JSON 处理
- HTTP
Content-Type含ndjson/jsonl- 按 NDJSON 处理
- 其他
- 按非流式处理
- 如果请求声明为流式,但首块看起来像 SSE
- 触发首块探测兜底
这层能力保留在 executor / decoder 中,不暴露给用户配置。
4. Development View
4.1 Directory Responsibilities
src/presentation/- HTTP route、管理页面、API controller
src/services/- 代理主流程和业务服务
src/config/- 配置加载、schema、provider runtime
src/executors/- transport executor
src/proxy_core/- decoder、encoder、shared contracts
src/translators/- protocol translators
src/hooks/- hook contracts
4.2 Key Files
- src/services/proxy_service.py
- 主代理 orchestration
- src/config/provider_config.py
- Provider schema
- src/executors/registry.py
- HTTP / WebSocket executors
- src/proxy_core/decoders.py
- 流式解码
- src/proxy_core/encoder.py
- 下游编码
- src/translators/registry.py
- 4x4 translator registry
- src/presentation/templates/providers.html
- Provider 页面、多选
target_formats与互斥说明
- Provider 页面、多选
5. Physical View
部署上是单体服务:
- 一个 Flask 应用
- 一个配置文件
- 多个 provider 指向多个真实上游
- 下游统一接入这个代理
Client / Agent / IDE
|
v
LLM Proxy
|
+--> OpenAI Chat upstream
+--> OpenAI Responses upstream
+--> Claude Messages upstream
+--> Codex upstream
6. Scenarios
6.1 OpenAI Chat Downstream -> Responses Upstream
sequenceDiagram
participant Client
participant Controller
participant Service
participant Translator
participant Executor
Client->>Controller: POST /v1/chat/completions
Controller->>Service: proxy_request()
Service->>Translator: openai_responses -> openai_chat
Translator-->>Service: translated upstream request
Service->>Executor: execute HTTP request
Executor-->>Service: stream events
Service->>Translator: translate stream events
Translator-->>Service: openai_chat chunks
Service-->>Controller: SSE response
Controller-->>Client: chat.completion.chunk stream
6.2 Claude Downstream -> OpenAI Chat Upstream
sequenceDiagram
participant Client
participant Controller
participant Service
participant Translator
participant Executor
Client->>Controller: POST /v1/messages
Controller->>Service: proxy_request()
Service->>Translator: openai_chat -> claude_chat
Translator-->>Service: upstream chat request
Service->>Executor: execute HTTP request
Executor-->>Service: chat SSE stream
Service->>Translator: translate to claude events
Translator-->>Service: message_start/content_block_delta/message_stop
Service-->>Client: Claude-style SSE
7. Design Decisions
7.1 Why only four protocol families
因为项目当前的目标客户端只需要:
- OpenCode
- Codex
- Claude Code
- Cherry Studio
Gemini / Antigravity 这类协议面会显著增加配置复杂度,但对当前目标收益很低,因此本版直接移除。
7.2 Why no public stream_format
因为流格式判断应该是代理内部责任,而不是用户负担。
用户只需要清楚:
- 上游是什么协议
- 下游要暴露成什么协议集合
- 上游通过 HTTP 还是 WebSocket 连接
上游到底是 SSE、NDJSON 还是非流式,由 executor / decoder 自动判断。
7.3 Why keep codex separate
因为它虽然共用 Responses 路由和编码器,但请求语义仍有差异;把它保留成独立 family,能让配置和 translator 逻辑更清晰。