# 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. 核心用户故事 > 作为管理员,我有了一批新的中转 key(URL + 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 \ --entry "https://api.deepseek.com," \ --access-mode subscription # 批量(文件,每行 url,key) go run ./cmd/cli batch-import \ --host-base-url http://localhost:18097 \ --host-api-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 \ --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 pricing(model 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 策略**:选 B(host + hash),`{normalized_host}-{url_hash_last8}` 2. **model_pricing 为空**:选 B,自动补空 pricing(填默认值,不阻塞导入) 3. **smoke test model**:选 C,遍历 data 找第一个能完成 chat completion 的模型