refactor: extract Phase 1 gateway helpers
Move tool bridge and responses adapter helpers out of app.main so the main entrypoint can shrink without changing route orchestration behavior. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
353
.omc/plans/app-main-split-plan.md
Normal file
353
.omc/plans/app-main-split-plan.md
Normal file
@@ -0,0 +1,353 @@
|
||||
# app/main.py 渐进拆分计划
|
||||
|
||||
- 日期:2026-04-21
|
||||
- 目标文件:`app/main.py`
|
||||
- 当前判断:**适合拆分,但不适合一次性大拆;建议按阶段渐进拆分**。
|
||||
|
||||
## 1. 目标
|
||||
|
||||
把 `app/main.py` 从“单文件总编排”逐步收敛为“组合根 + 路由/辅助模块”,在不破坏以下关键行为的前提下,降低文件复杂度并提高后续维护性:
|
||||
|
||||
- OpenAI / Anthropic / Responses 三条协议路径行为一致
|
||||
- session cache 命中、回写、失效语义保持不变
|
||||
- 单请求固定实例绑定不变
|
||||
- streaming 路径中的 in-flight ticket 释放语义不变
|
||||
- SSE 帧格式、finish reason / stop reason 行为不变
|
||||
- 现有测试尽量少改,尤其避免首轮就大面积修改对 `app.main` 的 patch 点
|
||||
|
||||
## 2. 当前结构判断
|
||||
|
||||
`app/main.py` 当前可以分成这些职责块:
|
||||
|
||||
1. **应用启动与全局装配**
|
||||
- `app/main.py:46-154`
|
||||
- 包括 `settings`、`pool`、`stats_collector`、`chat_guard`、`session_cache`、`lifespan`、middleware
|
||||
|
||||
2. **鉴权包装与告警**
|
||||
- `app/main.py:157-196`
|
||||
|
||||
3. **健康检查与通用请求辅助逻辑**
|
||||
- `app/main.py:199-353`
|
||||
|
||||
4. **共享 tool / stream / bridge helper**
|
||||
- `app/main.py:356-752`
|
||||
|
||||
5. **OpenAI Chat 主编排**
|
||||
- `app/main.py:769-1192`
|
||||
|
||||
6. **Responses API 适配层**
|
||||
- `app/main.py:1197-1640`
|
||||
|
||||
7. **Anthropic Messages 适配层**
|
||||
- `app/main.py:1679-2180`
|
||||
|
||||
8. **admin / internal / metrics 路由**
|
||||
- `app/main.py:2183-2356`
|
||||
|
||||
## 3. 风险判断
|
||||
|
||||
### 3.1 高风险区域(第一阶段不要碰)
|
||||
|
||||
以下区域**不建议作为第一刀拆分目标**:
|
||||
|
||||
1. `app/main.py:906` 左右的 OpenAI streaming generator
|
||||
2. `app/main.py:1886` 左右的 Anthropic streaming generator
|
||||
3. `v1_chat_completions` 主编排逻辑
|
||||
4. `v1_messages` 主编排逻辑
|
||||
5. session cache lookup / write-back / invalidate 的共享编排逻辑
|
||||
|
||||
### 3.2 原因
|
||||
|
||||
这些区域都同时依赖:
|
||||
|
||||
- route-local 状态
|
||||
- `pool` / `chat_guard` / `session_cache` / `stats_collector`
|
||||
- session continuity
|
||||
- 流式 finally 中的 ticket 释放与写回时机
|
||||
- OpenAI / Anthropic / Responses 之间的共享行为约束
|
||||
|
||||
这类代码即使功能不变,单纯移动位置也容易引发细微回归。
|
||||
|
||||
## 4. 建议的目标结构
|
||||
|
||||
建议最终逐步演进到以下结构:
|
||||
|
||||
```text
|
||||
app/
|
||||
main.py # 组合根:app 创建、lifespan、router 注册、共享单例
|
||||
http/
|
||||
lifecycle.py # middleware / startup posture / pool guards(可后置)
|
||||
chat_shared.py # 跨协议的 prompt/tool/stream helper
|
||||
openai_chat.py # /v1/chat/completions
|
||||
openai_responses.py # /responses 与 /v1/responses
|
||||
anthropic_messages.py # /v1/messages* 与 anthropic helper
|
||||
admin_routes.py # /internal/*, /metrics, /healthz, /v1/models(按需要划分)
|
||||
```
|
||||
|
||||
> 注意:这个结构是**目标结构**,不是第一阶段必须一步到位完成的结构。
|
||||
|
||||
## 5. 分阶段执行计划
|
||||
|
||||
### Phase 0:保护性准备(只做分析,不改行为)
|
||||
|
||||
目标:为后续拆分建立安全边界。
|
||||
|
||||
动作:
|
||||
|
||||
1. 梳理并固定当前回归验证命令
|
||||
- `python3 -m unittest tests/test_tool_call_bridge.py`
|
||||
- `python3 -m unittest discover -s tests -p "test_*.py"`
|
||||
|
||||
2. 在实际动代码前,对准备修改的关键符号做 impact analysis
|
||||
- 尤其是:
|
||||
- `v1_chat_completions`
|
||||
- `v1_messages`
|
||||
- `_messages_to_prompt`
|
||||
- `_responses_to_chat_request`
|
||||
- `_openai_tool_call`
|
||||
- `_anthropic_tool_use_block`
|
||||
|
||||
3. 先确认测试里对 `app.main` 的 patch 点,避免首轮拆分后直接把测试打碎
|
||||
|
||||
完成标准:
|
||||
- 有固定回归命令
|
||||
- 清楚哪些符号必须在首轮保留兼容出口
|
||||
|
||||
---
|
||||
|
||||
### Phase 1:提取纯 helper(最低风险)
|
||||
|
||||
目标:在不改主路由编排的前提下,先减轻 `app/main.py` 的噪音和长度。
|
||||
|
||||
建议新文件:
|
||||
|
||||
#### 1) `app/http/tool_bridge.py`
|
||||
建议迁移函数:
|
||||
- `_json_string`
|
||||
- `_openai_forced_tool_name`
|
||||
- `_anthropic_forced_tool_name`
|
||||
- `_json_object_from_text`
|
||||
- `_tool_code_single_arg_name`
|
||||
- `_tool_code_object_from_text`
|
||||
- `_forced_tool_event_from_text`
|
||||
- `_openai_tool_call`
|
||||
- `_anthropic_tool_use_block`
|
||||
- `_anthropic_tool_result_block`
|
||||
|
||||
#### 2) `app/http/responses_adapter.py`
|
||||
建议迁移函数:
|
||||
- `_responses_input_to_messages`
|
||||
- `_responses_to_chat_request`
|
||||
- `_responses_id_from_chat_id`
|
||||
- `_responses_usage_from_chat`
|
||||
- `_responses_non_stream_from_chat_payload`
|
||||
- `_sse_data`
|
||||
|
||||
#### 3) `app/http/tool_policy.py`(可选)
|
||||
如果首轮还想再减一点,可迁移:
|
||||
- `_include_usage`
|
||||
- `_tool_allowlist`
|
||||
- `_openai_tool_name`
|
||||
- `_anthropic_tool_name`
|
||||
- `_filter_allowed_tools`
|
||||
- `_ensure_tool_choice_allowed`
|
||||
- `_openai_tool_config`
|
||||
- `_anthropic_tool_config`
|
||||
- `_openai_has_tooling_context`
|
||||
- `_anthropic_content_has_tool_blocks`
|
||||
- `_anthropic_has_tooling_context`
|
||||
- `_resolve_ask_mode`
|
||||
|
||||
首轮兼容策略:
|
||||
- `app.main` 中先保留同名导入出口,例如:
|
||||
- `from .http.tool_bridge import _openai_tool_call, ...`
|
||||
- 这样即使测试仍然 patch `app.main._openai_tool_call`,改动面也最小
|
||||
|
||||
完成标准:
|
||||
- `app/main.py` 明显变短
|
||||
- 路由逻辑不变
|
||||
- 现有测试全过
|
||||
- 首轮不改 streaming 主体
|
||||
|
||||
---
|
||||
|
||||
### Phase 2:提取 Responses 路由(低到中风险)
|
||||
|
||||
目标:把 `/responses` 和 `/v1/responses` 的适配层单独放出去。
|
||||
|
||||
建议新文件:
|
||||
- `app/http/openai_responses.py`
|
||||
|
||||
建议包含:
|
||||
- `v1_responses`
|
||||
- `_responses_stream_from_chat_stream`
|
||||
- 以及它依赖的 responses helper(如果 Phase 1 已迁移则直接复用)
|
||||
|
||||
注意事项:
|
||||
- `v1_responses` 当前是直接包装 `v1_chat_completions`
|
||||
- 拆分时优先保持这个关系不变,不要同步重构 chat 主路径
|
||||
- 如果测试直接 patch `main.v1_chat_completions`,则需要确保新模块仍从 `app.main` 可拿到兼容入口,或同步最小化调整测试
|
||||
|
||||
完成标准:
|
||||
- `/responses` 逻辑从 `main.py` 分离
|
||||
- `v1_chat_completions` 仍保持原行为
|
||||
- responses 相关测试不回归
|
||||
|
||||
---
|
||||
|
||||
### Phase 3:提取 admin / health / metrics 路由(低风险)
|
||||
|
||||
目标:把非核心协议路径先搬走。
|
||||
|
||||
建议新文件:
|
||||
- `app/http/admin_routes.py`
|
||||
|
||||
可迁移内容:
|
||||
- `healthz`
|
||||
- `v1_models`(可按需一起搬)
|
||||
- `/internal/auto-login/*`
|
||||
- `/internal/session/export`
|
||||
- `/internal/models/raw`
|
||||
- `/internal/stats`
|
||||
- `/metrics`
|
||||
|
||||
注意事项:
|
||||
- 这些路由依赖全局 `settings` / `pool` / 鉴权 wrapper
|
||||
- 首轮可以通过“从 `main` 注入依赖”或“保留共享单例模块”来降低改动面
|
||||
|
||||
完成标准:
|
||||
- 运营/admin 路由从主文件剥离
|
||||
- 对 chat/messages 主编排零行为影响
|
||||
|
||||
---
|
||||
|
||||
### Phase 4:提取 Anthropic 路由与 helper(中风险)
|
||||
|
||||
目标:将 `/v1/messages*` 独立为单独模块。
|
||||
|
||||
建议新文件:
|
||||
- `app/http/anthropic_messages.py`
|
||||
|
||||
建议迁移:
|
||||
- `_anthropic_error`
|
||||
- `_anthropic_stop_reason`
|
||||
- `v1_messages_count_tokens`
|
||||
- `v1_messages`
|
||||
|
||||
前提:
|
||||
- Phase 1 已把共享 tool / prompt / policy helper 先抽出
|
||||
- 已明确哪些共享状态通过参数传入,哪些保持模块共享
|
||||
|
||||
注意:
|
||||
- 暂时不重构 Anthropic stream generator 内部逻辑,只做“整体迁移”而不是“逻辑改写”
|
||||
|
||||
完成标准:
|
||||
- Anthropic 适配层从主文件分离
|
||||
- 与 OpenAI 的共享行为仍保持一致
|
||||
|
||||
---
|
||||
|
||||
### Phase 5:最后再考虑提取 OpenAI Chat 主路由(最高风险)
|
||||
|
||||
目标:在前几阶段都稳定之后,再处理核心编排。
|
||||
|
||||
建议新文件:
|
||||
- `app/http/openai_chat.py`
|
||||
|
||||
建议迁移:
|
||||
- `v1_chat_completions`
|
||||
- 仅与其强耦合、且不适合保留在 `main.py` 的少量辅助逻辑
|
||||
|
||||
关键原则:
|
||||
- 不要在这一阶段同时改 session/cache/streaming 逻辑
|
||||
- 只做“位置迁移 + 依赖显式化”
|
||||
- 如需引入 service 层,也要在这个阶段之后再单独评估,不要和文件拆分绑定进行
|
||||
|
||||
完成标准:
|
||||
- `app/main.py` 基本收敛为组合根
|
||||
- 主编排仍行为一致
|
||||
- 全量测试通过
|
||||
|
||||
## 6. 每阶段的验证要求
|
||||
|
||||
每一阶段完成后,至少执行:
|
||||
|
||||
```bash
|
||||
python3 -m unittest tests/test_tool_call_bridge.py
|
||||
python3 -m unittest discover -s tests -p "test_*.py"
|
||||
```
|
||||
|
||||
如果本地服务可启动,建议补一轮 smoke:
|
||||
|
||||
```bash
|
||||
uvicorn app.main:app --reload --port 8317
|
||||
curl -s http://127.0.0.1:8317/healthz
|
||||
```
|
||||
|
||||
如果是改动了 `/responses` 或 `/v1/messages` 路径,应额外做协议 smoke,确认:
|
||||
- SSE 帧格式不变
|
||||
- stop reason / finish reason 不变
|
||||
- tool call / tool_use bridge 不变
|
||||
|
||||
## 7. 兼容策略
|
||||
|
||||
为减少首轮测试与调用方震荡,建议:
|
||||
|
||||
1. **先迁移实现,再从 `app.main` re-export 同名符号**
|
||||
- 例如:`from .http.responses_adapter import _responses_to_chat_request`
|
||||
2. 首轮不要改函数名
|
||||
3. 首轮不要顺手重命名模块级全局变量
|
||||
4. 首轮不要引入新的抽象层(例如 service / manager / context object)
|
||||
|
||||
原则:
|
||||
- 第一轮目标是“降噪和减重”,不是“顺便重构架构”
|
||||
|
||||
## 8. 不建议做的事
|
||||
|
||||
以下动作不建议与本次拆分绑定:
|
||||
|
||||
- 同时重写 streaming generator 内部结构
|
||||
- 同时改 session cache 语义
|
||||
- 同时改 pool / guard / stats 注入方式
|
||||
- 同时大改测试结构
|
||||
- 同时引入新的 service 层 / context 容器 / 抽象基类
|
||||
|
||||
这些都应该是后续独立变更,不要混在第一次拆分里。
|
||||
|
||||
## 9. 推荐的首个落地 PR 范围
|
||||
|
||||
如果要开始实际实施,**建议第一批只做一个小 PR**:
|
||||
|
||||
### PR-1:Helper extraction only
|
||||
|
||||
内容:
|
||||
- 新增 `app/http/tool_bridge.py`
|
||||
- 新增 `app/http/responses_adapter.py`
|
||||
- `app/main.py` 改为导入这些 helper
|
||||
- 保留 `app.main` 的兼容出口
|
||||
- 不动 `v1_chat_completions` / `v1_messages` 的主逻辑
|
||||
|
||||
预期收益:
|
||||
- `app/main.py` 先减少几百行
|
||||
- 风险最可控
|
||||
- 为后续路由级拆分打基础
|
||||
|
||||
## 10. 后续记录方式
|
||||
|
||||
建议后续每完成一个 phase,就在本文件底部追加一段进展记录,例如:
|
||||
|
||||
```md
|
||||
## Progress Log
|
||||
- 2026-04-21: 创建拆分计划
|
||||
- 2026-04-22: 完成 Phase 1,抽离 responses helper 与 tool bridge helper
|
||||
- 2026-04-23: 运行全量 unittest 通过
|
||||
```
|
||||
|
||||
这样后续可以持续在同一份计划上回填,不需要再重新整理上下文。
|
||||
|
||||
## Progress Log
|
||||
- 2026-04-21: 创建拆分计划。
|
||||
- 2026-04-21: 完成 Phase 1 helper extraction,新增 `app/http/tool_bridge.py`、`app/http/responses_adapter.py`,并在 `app.main` 保留兼容导入出口。
|
||||
- 2026-04-21: 修复 Phase 1 后暴露的 tool bridge 回归;放宽 tool event allow 判断,仅在存在显式 tool 列表时做名称过滤,并保留 forced-tool 回退语义。
|
||||
- 2026-04-21: 调整 OpenAI 流式 forced-tool 回退,先缓冲 `tool_code` 文本,能解析为结构化 tool call 时只输出 `tool_calls` chunk,不能解析时再回放文本。
|
||||
- 2026-04-21: 验证通过:`python3 -m py_compile app/main.py app/http/tool_bridge.py app/http/responses_adapter.py`、`python3 -m unittest tests/test_tool_call_bridge.py`、`python3 -m unittest discover -s tests -p "test_*.py"`。
|
||||
Reference in New Issue
Block a user