Files
lingma-openai-gateway/.omc/plans/app-main-split-plan.md
GitHub Actions 0e146e60d9 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>
2026-04-21 08:05:09 +08:00

354 lines
11 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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-1Helper 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"`