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.
22 KiB
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) 对,就能自动完成:
- 上游发现 — 调用
GET {base_url}/v1/models与最小 smoke 请求,动态获取该 key 真正支持的模型列表 - 名称纠错 — 自动把“人工填错的模型名”与上游真实返回做比对、归一化、纠偏
- 能力画像 — 记录这个上游/模型对 OpenAI/Anthropic 兼容能力、Responses 支持、stream/tool 调用等差异
- 宿主演化 — 将发现结果与宿主 channel / account 配置对比,自动扩展
model_mapping - 异步确认 — 对“建账号成功但宿主异步 probe / 调度尚未稳定”的场景做延迟确认,不把瞬时失败立即记成最终失败
- 中转闭环验证 — 用托管 key 跑真实
/v1/chat/completions验证,确认最终active/degraded/broken - 状态可观测 — 持久化每个 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 为什么必须引入异步确认
真实验收已经证明,“账号创建完成”不等于“立即可验证成功”:
- 宿主对第三方 OpenAI 兼容上游的
/responses能力探测是异步落库的 - 账号刚创建后,第一次
/accounts/:id/test可能仍走旧路径,返回临时403 Forbidden - channel / group / subscription 已经写好后,第一次
/v1/chat/completions也可能短暂命中503 no available accounts - 几百毫秒到几秒后,同一条链路又会恢复为
200
因此 v2 不能继续用“创建后立刻同步 test 一次”的策略直接定生死。必须区分:
- 提交成功
- 异步确认中
- 最终确认成功/失败
4.1.2 状态机
每个导入条目应至少具备以下状态机:
discovered
→ provisioned
→ confirming
→ confirmed_active
→ confirmed_warning
→ confirmed_broken
其中:
provisioned:宿主资源已创建,但不能对外宣称 readyconfirming:正在等待宿主异步 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 上游返回格式为:
{
"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_idnormalized_model_id- 最终对外 gateway model 名
-
默认行为是:
gateway_model = normalized_model_idupstream_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 概念。至少记录:
{
"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
新增运行态持久化对象:
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 接口
# 单条
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 格式:
https://api.deepseek.com,sk-xxx
https://api.completion.com,sk-yyy
CLI 输出必须引用 run_id,并能直接打印结果页入口:
run_id: batch-20260522-001
result_page: /batch-import/runs/batch-20260522-001
4.5 结果查看 API 与页面
v2 不再只提供 CLI 输出,必须提供最小可用的控制面结果查看能力。
HTTP API
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 轨迹
页面
至少提供一个简单结果页:
/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 Forbiddenaccount 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 单独记录,成功的继续
需要新增两类恢复策略:
-
模型名纠错恢复
- 若请求方显式填写了模型名,但 upstream
/models未返回该模型 - 系统应尝试 normalized 比对和 alias 命中
- 若仍未命中,则返回“推荐模型名”,不要盲目创建错误配置
- 若请求方显式填写了模型名,但 upstream
-
兼容能力恢复
- 若
/responses失败但/chat/completions成功 - profile 应明确标记
supports_openai_responses=false - 后续同类 provider 默认直接跳过 responses 探测
- 若
-
运行态稳定性恢复
- 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. 成功标准
- CLI
batch-import可接受单条和文件批量输入 - Stage 1 probe 能在 10s 内返回上游模型列表(超时控制)
- 重复导入同一 URL+key 时,不重复创建 channel/account(幂等)
- Stage 3 completion 测试通过时,
access_status=active - Stage 3 失败时,access_status 正确降级(broken/degraded)
strict模式下,任一 item 失败整批停止并报告partial模式下,成功的 item 不因失败 item 而中断- 结果页可查看每个 run / item 的状态、advisory、retry 轨迹和最终 access status
- 控制面重启后,历史 run 结果仍可查看
- 全流程不修改宿主源码,不写宿主数据库
13. 开放问题(已决策)
- provider_id 策略:选 B(host + hash),
{normalized_host}-{url_hash_last8} - model_pricing 为空:选 B,自动补空 pricing(填默认值,不阻塞导入)
- smoke test model:选 C,遍历 data 找第一个能完成 chat completion 的模型