Files
LLM_Proxy/docs/architecture-4plus1.md
Von bd7552f23b feat: 支持 Provider 多下游 target_formats 并收敛对外配置接口
- 将 Provider 下游配置由单个 target_format 扩展为多个 target_formats
- 保留旧字段 target_format 的启动期迁移逻辑,并标注为弃用兼容代码
- 收敛对外配置与接口返回,Provider 相关响应及 /v1/models 仅暴露 target_formats
- 前端 Provider 表单改为多选下游,支持互斥项自动替换
- 新建 Provider 默认选中 openai_chat、codex、claude_chat
- 更新示例配置、README、4+1 架构文档及相关测试
2026-04-06 09:46:28 +08:00

311 lines
8.1 KiB
Markdown

# 4+1 Architecture
## 1. Context
这个项目是一个协议翻译型 LLM Proxy。
它的目标不是“支持所有厂商的所有接口”,而是用一套干净的首版架构,稳定支持:
- 上游协议族
- `openai_chat`
- `openai_responses`
- `claude_chat`
- `codex`
- 下游协议面
- `POST /v1/chat/completions`
- `POST /v1/responses`
- `POST /v1/messages`
- `GET /v1/models`
这个版本不保留 Gemini / Antigravity。配置加载阶段会把旧字段 `target_format` 规范化为 `target_formats`,这段弃用兼容逻辑后续会删除;除此之外不再继续扩展旧字段兼容逻辑。
## 2. Logical View
### 2.1 Core Pipeline
系统按下面的统一链路工作:
```text
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_code`
- `last_error_type`
### 2.2 Major Components
- `ProxyController`
- 校验下游 route 和 `target_formats`
- 根据 route family 解析本次请求命中的 `target_format`
- 构造标准错误体
- `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,还会收到最小重试上下文:
- `retry`
- `auth_group_name`
- `auth_entry_id`
- `last_status_code`
- `last_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_format`
- `target_formats`
- `transport`
### 3.2 Provider Runtime Contract
Provider 公共运行时字段只有:
- `name`
- `api`
- `transport`
- `source_format`
- `target_formats`
- `api_key`
- `auth_group`
- `proxy`
- `timeout_seconds`
- `max_retries`
- `verify_ssl`
- `model_list`
- `hook`
其中:
- `source_format`
- 上游真实协议
- `target_formats`
- 当前 provider 对下游暴露的协议集合
- `transport`
- HTTP 或 WebSocket
没有公共 `stream_format` 字段。
Hook 运行时上下文还会暴露最小重试状态:
- `retry`
- `auth_group_name`
- `auth_entry_id`
- `last_status_code`
- `last_error_type`
其中 `last_error_type` 使用 `HookErrorType` 枚举,当前值为:
- `TIMEOUT`
- `CONNECTION_ERROR`
- `WEBSOCKET_ERROR`
- `TRANSPORT_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](/d:/001Code/008llm/003LLM_Proxy/src/services/proxy_service.py)
- 主代理 orchestration
- [src/config/provider_config.py](/d:/001Code/008llm/003LLM_Proxy/src/config/provider_config.py)
- Provider schema
- [src/executors/registry.py](/d:/001Code/008llm/003LLM_Proxy/src/executors/registry.py)
- HTTP / WebSocket executors
- [src/proxy_core/decoders.py](/d:/001Code/008llm/003LLM_Proxy/src/proxy_core/decoders.py)
- 流式解码
- [src/proxy_core/encoder.py](/d:/001Code/008llm/003LLM_Proxy/src/proxy_core/encoder.py)
- 下游编码
- [src/translators/registry.py](/d:/001Code/008llm/003LLM_Proxy/src/translators/registry.py)
- 4x4 translator registry
- [src/presentation/templates/providers.html](/d:/001Code/008llm/003LLM_Proxy/src/presentation/templates/providers.html)
- Provider 页面、多选 `target_formats` 与互斥说明
## 5. Physical View
部署上是单体服务:
- 一个 Flask 应用
- 一个配置文件
- 多个 provider 指向多个真实上游
- 下游统一接入这个代理
```text
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
```mermaid
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
```mermaid
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 逻辑更清晰。