Files
sub2api-cn-relay-manager/docs/2026-05-21-BATCH_AUTO_IMPORT_SPEC.md
phamnazage-jpg afce3da3df docs(v2): refine batch import architecture
Expand the batch auto-import V2 spec and TDD plan with stability requirements, result state persistence, and result page design. Add a dedicated architecture document for run state, APIs, pages, and UI field layout, and sync the execution board to the new V2 scope.
2026-05-22 13:18:51 +08:00

559 lines
22 KiB
Markdown
Raw 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.
# SPEC: Batch Auto-Import by URL + Key (v2)
日期2026-05-21
技术架构:`docs/2026-05-22-BATCH_AUTO_IMPORT_V2_ARCHITECTURE.md`
## 1. Objective
让管理员只提供一批 `(base_url, api_key)` 对,就能自动完成:
1. **上游发现** — 调用 `GET {base_url}/v1/models` 与最小 smoke 请求,动态获取该 key 真正支持的模型列表
2. **名称纠错** — 自动把“人工填错的模型名”与上游真实返回做比对、归一化、纠偏
3. **能力画像** — 记录这个上游/模型对 OpenAI/Anthropic 兼容能力、Responses 支持、stream/tool 调用等差异
4. **宿主演化** — 将发现结果与宿主 channel / account 配置对比,自动扩展 `model_mapping`
5. **异步确认** — 对“建账号成功但宿主异步 probe / 调度尚未稳定”的场景做延迟确认,不把瞬时失败立即记成最终失败
6. **中转闭环验证** — 用托管 key 跑真实 `/v1/chat/completions` 验证,确认最终 `active/degraded/broken`
7. **状态可观测** — 持久化每个 run、item、模型、账号、provider 的阶段结果,并提供页面查看导入状态
目标不是“绝对零人工”,而是把人工输入压缩到最小,并把容易写错、容易误判的部分交给系统自动确认。
## 2. 为什么现在需要这个
当前 v1 依赖预定义 provider manifest`packs/openai-cn-pack/providers/*.json`),每个 provider 必须手动写好 `base_url / default_models / smoke_test_model / channel_template`。这带来三个问题:
- **新 key 无法即插即用**:每次接一个陌生 provider URL都得先查文档再写 manifest
- **模型列表人工维护**provider 上游升级模型pack 里不会自动同步
- **调试链路长**:假设备注 manifest → 导入 → 发现 channel 缺少模型 → 手动补 → 重新导入
- **模型名容易写错**:例如 `minimax-m27-highspeed``MiniMax-M2.7-highspeed`,人工输入极易出错
- **国产模型兼容差异大**很多“OpenAI-compatible”只兼容 `/chat/completions`,不兼容 `/responses``tools``stream_options`
- **宿主存在异步窗口**账号创建、Responses probe、调度预热、账号可选状态更新并非原子完成一次即时检查容易得到假阴性
- **长任务稳定性不足**:批量导入跨多个阶段,若没有状态持久化、重试边界和结果投影,失败后很难判断卡在哪一步
- **结果不可视**:当前主要靠 CLI、日志和 artifact 复盘,缺少专门页面查看导入状态和账号/模型明细
v2 需要把“探测 → 配置 → 注册 → 异步确认 → 验证”压缩成**一键闭环**。
## 3. 核心用户故事
> 作为管理员,我有了一批新的中转 keyURL + token我想在已经运行的宿主上快速开通这些模型。理想情况是我把这批 key 列出来,系统自动探测每个 key 支持什么模型、自动纠正模型名、自动识别兼容能力、自动配置宿主 channel、自动注册为可控 provider、自动异步确认账号和闭环状态并在控制面页面里直接告诉我哪些真正可用、哪些只是暂时不稳定、哪些需要特定兼容策略。
## 4. 技术方案
### 4.1 四阶段管道 + 运行态持久化
```
输入: [(base_url, api_key), ...]
Stage 0: Run Setup ──────────────────────────────────────────────
create import_run
→ persist operator input / retry policy / timestamps
→ assign run_id and item_ids
Stage 1: Probe ─────────────────────────────────────────────────
for each (url, key):
upstream_models = GET {url}/v1/models
→ extract model list
upstream_capabilities = probe endpoint compatibility
→ /models | /chat/completions | /responses | /messages
upstream_completion = POST {url}/v1/chat/completions (smoke)
→ HTTP status, latency, error_type, usable_model
classify: models_ok | models_fail | completion_fail | unreachable
normalize model ids and select smoke model automatically
Stage 2: Provision ──────────────────────────────────────────────
for each (url, key) where upstream_models != models_fail:
host_channel = find_or_create_channel(provider_id, url, capability_profile)
missing_models = normalized_models - host_channel.model_mapping.keys
if missing_models:
patch_channel(host_channel, add model_mapping entries)
managed_account = create_or_update_account(url, key, normalized_models)
register_provider_binding(provider_id, url, key, normalized_models, capability_profile)
Stage 3: Async Confirm ──────────────────────────────────────────
for each registered account:
async account confirm:
re-check account models
re-check account test (after host async probe settles)
re-check temporary 503/no available accounts windows
→ write confirmation_status: pending | confirmed | warning | failed
Stage 4: Validate ───────────────────────────────────────────────
for each confirmed account:
final_completion = POST host_gw/v1/chat/completions
via managed_account key
→ write access_status: active | broken | degraded
persist final run summary and UI-facing status projections
output: per-url status + summary
输出: BatchImportResult {
run_id: string
total: int
active: int
broken: int
degraded: int
details: [{url, normalized_models, capability_profile, confirmation_status, access_status, error}]
}
```
### 4.1.1 为什么必须引入异步确认
真实验收已经证明,“账号创建完成”不等于“立即可验证成功”:
1. 宿主对第三方 OpenAI 兼容上游的 `/responses` 能力探测是异步落库的
2. 账号刚创建后,第一次 `/accounts/:id/test` 可能仍走旧路径,返回临时 `403 Forbidden`
3. channel / group / subscription 已经写好后,第一次 `/v1/chat/completions` 也可能短暂命中 `503 no available accounts`
4. 几百毫秒到几秒后,同一条链路又会恢复为 `200`
因此 v2 不能继续用“创建后立刻同步 test 一次”的策略直接定生死。必须区分:
- **提交成功**
- **异步确认中**
- **最终确认成功/失败**
### 4.1.2 状态机
每个导入条目应至少具备以下状态机:
```
discovered
→ provisioned
→ confirming
→ confirmed_active
→ confirmed_warning
→ confirmed_broken
```
其中:
- `provisioned`:宿主资源已创建,但不能对外宣称 ready
- `confirming`:正在等待宿主异步 probe / account warm-up / gateway 调度稳定
- `confirmed_warning`:链路可用,但有 advisory 风险,例如 probe 403 race、兼容能力受限
- `confirmed_broken`:经过重试与延迟确认后仍不可用
每个状态转换都必须持久化,不能只留在内存中。控制面至少要能恢复:
- 当前 run 进行到哪个阶段
- 哪些 item 已完成
- 哪些 item 仍在 confirming
- 哪些 item 因 transient 错误进入下一次 retry
### 4.2 关键设计决策
#### Q1: 如何从 `/v1/models` 提取并纠正模型列表?
OpenAI-compatible 上游返回格式为:
```json
{
"data": [{"id": "gpt-4", "object": "model", ...}, ...]
}
```
提取策略:
-`data[].id` 作为上游原始模型名
- 保留 `raw_model_id`
- 同时生成 `normalized_model_id`
- 默认不过滤“看起来像 GPT”的名字而是把原始值完整记录下来再根据 provider host / capability profile 判断是否属于目标模型
归一化规则至少覆盖:
- 大小写归一
- 连字符 / 点号差异
- `vendor/model` 前缀剥离
- 常见别名映射
示例:
| raw | normalized |
|---|---|
| `MiniMax-M2.7-highspeed` | `minimax-m2.7-highspeed` |
| `minimax-m27-highspeed` | `minimax-m27-highspeed` |
| `deepseek-ai/DeepSeek-V4-Pro` | `deepseek-v4-pro` |
| `Kimi-K2.6` | `kimi-k2.6` |
系统不应再默认信任人工填入的模型名,而应优先信任 key 实探结果。
#### Q2: 如何把上游模型写入宿主 channel
宿主 channel 有两个相关字段:
- `model_mapping: map[string]string``{upstream_model: gateway_model}`
- `restrict_models: bool` — true 时 gateway 只路由 mapping 内的模型
策略:
- channel 中同时保留:
- `raw_model_id`
- `normalized_model_id`
- 最终对外 gateway model 名
- 默认行为是:
- `gateway_model = normalized_model_id`
- `upstream_model = raw_model_id`
- 若宿主侧必须保持原名路由,则至少要把 alias 关系落到 profile后续导入与对账都按 normalized 视角比较
- `model_pricing` 填默认值(`price_per_1m=0`, `max_batch=0`),不阻塞导入
- 如果 channel 不存在,创建新 channel`name = host_registered_{provider_id}`
#### Q3: Provider ID 如何生成?
自动生成规则:
-`base_url` 的 host 部分,规范化(去掉 `https://`、去除尾部 `/`
- 去除常见后缀(`.com``.cn`
- 转小写 + 中划线连接
- 示例:`https://api.deepseek.com``api-deepseek`
这样同一 URL 的多次导入会命中同一个 provider_id实现增量更新。
#### Q4: 如何避免重复 key 覆盖已有配置?
导入前执行 reconcile
- 如果 `base_url + key` 对应的 account 已存在,且 `upstream_models` 与已有 account 的 `credentials.model_mapping` 一致 → 跳过
- 如果 account 存在但模型列表变长了 → patch channel 扩展 model_mapping
- 如果 account 存在但 key 已失效 → 标记为 `broken`,新建 account
#### Q5: 验证 key 失效 vs 上游断连如何区分?
Stage 1 的 smoke test 需要区分错误类型:
- `401/403 unauthorized` → key 无效
- `429 rate_limit` → key 有额度但被限流 → 记录,不阻塞
- `502/503/connection_error` → 上游不可达 → 降级处理
- `200 + valid response` → key 可用
Stage 3 的 host relay smoke 测试结果才决定最终 `access_status`
#### Q6: 如何记录兼容能力,避免每次重新踩坑?
v2 必须引入 `capability_profile` 概念。至少记录:
```json
{
"supports_openai_models": true,
"supports_openai_chat_completions": true,
"supports_openai_responses": false,
"supports_anthropic_messages": false,
"supports_stream": true,
"supports_tools": "unknown",
"supports_reasoning_fields": "unknown",
"auth_style": "bearer",
"model_id_style": "vendor_prefixed | canonical | mixed",
"known_advisories": [
"responses_403_third_party",
"initial_account_probe_race",
"gateway_no_available_accounts_warmup"
]
}
```
这个 profile 的用途不是“好看”,而是后续快速匹配策略:
- 哪些 provider 需要跳过 `/responses`
- 哪些 provider 要优先走 raw `/chat/completions`
- 哪些 provider 要启用 completion retry
- 哪些 provider 的模型名要先归一化再对比
- 哪些 provider 需要 Anthropic 兼容入口
### 4.3 数据流
```
BatchImportRequest
├── base_url: string
├── api_key: string
├── access_mode: "subscription" | "self_service" (可选,默认 subscription)
└── requested_models: []string (可选,作为提示而不是信任源)
BatchImportResult
├── batch_id: string
├── total: int
├── active: int
├── broken: int
├── degraded: int
└── results: []ImportItemResult
ImportItemResult
├── base_url: string
├── provider_id: string (自动生成)
├── upstream_models: []string (Stage 1 发现的原始模型)
├── normalized_models: []string (归一化后的模型)
├── resolved_smoke_model: string
├── capability_profile: object
├── channel_id: int64 (Stage 2 创建/更新)
├── account_id: int64 (Stage 2 创建/更新)
├── probe_ok: bool (Stage 3 account test 最终结果)
├── confirmation_status: string
├── access_status: string (Stage 3 最终)
├── stage_status: string (discovered | provisioned | confirming | confirmed_*)
├── advisory_messages: []string
├── retry_count: int
├── last_error_stage: string | null
└── error: string | null
```
新增运行态持久化对象:
```text
ImportRun
- run_id
- mode
- access_mode
- total_items
- completed_items
- active_items
- degraded_items
- broken_items
- state (running | completed | completed_with_warnings | failed | cancelled)
- started_at
- updated_at
- finished_at
ImportRunItem
- run_id
- item_id
- base_url
- provider_id
- current_stage
- stage_status
- requested_models
- normalized_models
- resolved_smoke_model
- channel_id
- account_id
- confirmation_status
- access_status
- retry_count
- advisory_messages
- last_error_stage
- last_error
```
### 4.4 CLI 接口
```bash
# 单条
go run ./cmd/cli batch-import \
--host-base-url http://localhost:18097 \
--host-api-key <admin-key> \
--entry "https://api.deepseek.com,<deepseek-key>" \
--access-mode subscription
# 批量(文件,每行 url,key
go run ./cmd/cli batch-import \
--host-base-url http://localhost:18097 \
--host-api-key <admin-key> \
--batch-file ./keys.csv \
--access-mode subscription
# 批量stdin
cat keys.txt | xargs -I{} go run ./cmd/cli batch-import \
--host-base-url http://localhost:18097 \
--host-api-key <admin-key> \
--batch-stdin
```
`keys.csv` 格式:
```csv
https://api.deepseek.com,sk-xxx
https://api.completion.com,sk-yyy
```
CLI 输出必须引用 `run_id`,并能直接打印结果页入口:
```text
run_id: batch-20260522-001
result_page: /batch-import/runs/batch-20260522-001
```
### 4.5 结果查看 API 与页面
v2 不再只提供 CLI 输出,必须提供最小可用的控制面结果查看能力。
#### HTTP API
```text
GET /api/batch-import/runs
GET /api/batch-import/runs/{run_id}
GET /api/batch-import/runs/{run_id}/items
GET /api/batch-import/runs/{run_id}/items/{item_id}
```
用途:
- 列出最近批次
- 查看某个批次的整体统计
- 查看每条 URL / provider / account 的阶段结果
- 查看模型纠错、capability profile、advisory、retry 轨迹
#### 页面
至少提供一个简单结果页:
```text
/batch-import/runs
/batch-import/runs/{run_id}
```
页面最低要求:
- 批次列表页:
- run_id
- started_at / finished_at
- total / active / degraded / broken
- overall state
- 批次详情页:
- 每个 item 的 base_url / provider_id
- requested_models / normalized_models / resolved_smoke_model
- capability_profile 摘要
- channel_id / account_id
- confirmation_status / access_status
- advisory_messages
- last_error_stage / last_error
页面目标不是做复杂前端,而是让运营和开发能快速回答:
- 哪条导入卡住了
- 卡在哪一阶段
- 是模型名错、兼容不支持、probe race还是 completion 失败
- 这个 warning 是暂时性的还是最终要人工处理的
## 5. 宿主硬约束(继承自 v1
- 不修改宿主源码
- 不直接写宿主数据库
- 只通过宿主 HTTP Admin API 和 Gateway API 工作
- channel 完整收口字段必须同时存在:`model_mapping` + `model_pricing` + `restrict_models=true` + `billing_model_source=channel_mapped`
- `/v1/models``/v1/chat/completions` 是两个独立验收层
- 结果页与运行状态只能读取控制面自己的状态库,不读取宿主数据库
## 6. 访问闭环
Stage 3 的 `access_status` 决定真实可用性:
| access_status | 含义 | 用户可使用 |
|---|---|---|
| `active` | Stage1 probe OK + Stage2 account OK + Stage3 completion OK | ✅ |
| `degraded` | Stage1/2 OK但 Stage3 completion 异常 | ⚠️ 限流/不稳定 |
| `broken` | Stage1 probe 失败或 Stage2 account test 失败 | ❌ |
补充约束:
- `requested_models` 只是提示,不是验收依据
- 只有 `resolved_smoke_model` 经上游实探成功,才能作为最终 smoke 模型
- 对于第三方 upstream 的首次 `403 Forbidden` account probe`/models` 已命中且 capability profile 已识别为 `responses_unsupported`,应先进入 `warning/confirming`,而不是立即 `broken`
- 对于导入后瞬时 `503 no available accounts`,应先进入短暂 retry 窗口,而不是立即最终失败
## 7. 错误恢复策略
- Stage 1 失败:记录 `upstream_unreachable`,跳过 Stage 2/3
- Stage 2 部分失败:已完成资源保留(不自动回滚)
- Stage 3 首次失败:进入 `confirming`,按 capability profile 与 transient 分类决定是否重试
- Stage 4 最终失败access_status 降级,但已创建资源不删除
- 整批中断:按 `--mode strict | partial` 处理
- `strict`:任一 item 失败,整批停止,报告已完成的
- `partial`(默认):失败 item 单独记录,成功的继续
需要新增两类恢复策略:
1. **模型名纠错恢复**
- 若请求方显式填写了模型名,但 upstream `/models` 未返回该模型
- 系统应尝试 normalized 比对和 alias 命中
- 若仍未命中,则返回“推荐模型名”,不要盲目创建错误配置
2. **兼容能力恢复**
-`/responses` 失败但 `/chat/completions` 成功
- profile 应明确标记 `supports_openai_responses=false`
- 后续同类 provider 默认直接跳过 responses 探测
3. **运行态稳定性恢复**
- item 的阶段结果、retry_count、last_error_stage 必须持久化
- 控制面重启后,历史 run 结果仍应可查看
- 若未来支持 resume必须显式区分 resumed run 与原始 run
## 8. 与 v1 的关系
v2 **不取代** v1而是新增一条并行入口
| | v1 (Pack-Based) | v2 (Auto-Import) |
|---|---|---|
| 输入 | provider manifest | URL + API key |
| 模型来源 | pack 内置 | 上游动态探测 |
| 适用场景 | 已知 provider批量标准化导入 | 新 provider即插即用 |
| channel 配置 | manifest 预定义 | 自动发现 + 扩展 |
v2 的 provider binding 复用 v1 已有 `managed_resources``import_batches` 表,只是入口不同。
## 9. 项目结构变化
```
internal/
probe/ # 新增:上游探测模块
models.go # GET /v1/models 解析
aliases.go # 模型名归一化 / 别名比对
completion.go # smoke test POST /v1/chat/completions
capability.go # /responses / /messages / stream / tools 能力探测
classifier.go # 错误分类auth/rate_limit/upstream/unreachable
batch/ # 新增:批量导入编排
service.go # BatchImportService: 管道编排
provider_id.go # URL → provider_id 规范化
channel_evolution.go # model_mapping 扩展逻辑
confirmation.go # 异步确认状态机 / retry policy
capability_profile.go # provider/model 兼容能力画像持久化与决策
run_state.go # import run / item 持久化模型
status_projection.go # 列表页 / 详情页统计投影
host/sub2api/
channel.go # 新增: PatchChannel(channel_id, add_model_mapping)
app/
http_batch_import.go # 批量导入 API
http_batch_runs.go # run 列表 / 详情 API 与页面
cmd/
cli/
batch_import.go # 新增: batch-import 命令
tests/integration/
batch_import_test.go # 新增: 批量导入集成测试
```
## 10. 测试策略
### 单测
- `probe/models_test.go` — 模型列表解析,覆盖 OpenAI 格式变体
- `probe/aliases_test.go` — 模型名归一化、前缀剥离、常见拼写误差提示
- `probe/capability_test.go` — OpenAI/Anthropic/Responses 兼容能力探测
- `probe/classifier_test.go` — 错误类型分类
- `batch/provider_id_test.go` — URL → provider_id 规范化
- `batch/channel_evolution_test.go` — model_mapping 扩展差异计算
- `batch/confirmation_test.go` — 异步确认窗口、短暂 503 retry、advisory 降级
- `batch/capability_profile_test.go` — compatibility → routing strategy 决策
- `batch/run_state_test.go` — run/item 状态持久化与状态投影
- `batch/service_test.go` — 管道编排 mock 测试
- `app/http_batch_import_test.go` — 结果 API / 页面输出
### 集成测
- `tests/integration/batch_import_test.go`
- 两组 (url, key)probe + provision + validate 全流程
- strict 模式任一失败整批停止
- partial 模式失败 item 隔离
- 第一次 account test `403 Forbidden`,异步确认后转 warning/active
- 第一次 completion `503 no available accounts`,重试后转 active
- `requested_models` 填错时,能给出 `normalized_models/recommended_model`
- 导入过程中查询 run detail能看到阶段推进和 retry_count 变化
- 导入完成后页面/API 可查看 run summary 和 item 详情
## 11. 暂不做v2 范围外)
- 自动生成价格策略(先记录默认值和未确认状态)
- 自动发现 provider 的 channel pricingmodel pricing 留空,等用户配置)
- 多 key 之间的负载均衡策略
- 对账调度器( reconcile 由 v1 提供)
## 12. 成功标准
1. CLI `batch-import` 可接受单条和文件批量输入
2. Stage 1 probe 能在 10s 内返回上游模型列表(超时控制)
3. 重复导入同一 URL+key 时,不重复创建 channel/account幂等
4. Stage 3 completion 测试通过时,`access_status=active`
5. Stage 3 失败时access_status 正确降级broken/degraded
6. `strict` 模式下,任一 item 失败整批停止并报告
7. `partial` 模式下,成功的 item 不因失败 item 而中断
8. 结果页可查看每个 run / item 的状态、advisory、retry 轨迹和最终 access status
9. 控制面重启后,历史 run 结果仍可查看
10. 全流程不修改宿主源码,不写宿主数据库
## 13. 开放问题(已决策)
1. **provider_id 策略**:选 Bhost + hash`{normalized_host}-{url_hash_last8}`
2. **model_pricing 为空**:选 B自动补空 pricing填默认值不阻塞导入
3. **smoke test model**:选 C遍历 data 找第一个能完成 chat completion 的模型