Files
sub2api-cn-relay-manager/docs/2026-05-21-BATCH_AUTO_IMPORT_SPEC.md
phamnazage-jpg 66da64dbe3 docs(v2): add batch auto-import spec and tdd plan with resolved open questions
- Add BATCH_AUTO_IMPORT_SPEC.md: 3-stage pipeline (probe/provision/validate),
  provider_id=host+hash, smoke_model=find-first-usable, pricing=defaults
- Add BATCH_AUTO_IMPORT_TDD_PLAN.md: 5-stage implementation plan, 10 tasks
- Update EXECUTION_BOARD.md: add v2 section with resolved open questions
2026-05-22 06:51:44 +08:00

281 lines
11 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
## 1. Objective
让管理员只提供一批 `(base_url, api_key)` 对,就能自动完成:
1. **上游探测** — 调用 `GET {base_url}/v1/models` 动态获取该 key 支持的模型列表
2. **宿主演化** — 将发现的模型与宿主 channel 配置对比,自动扩展 `model_mapping`
3. **供应商注册** — 把 URL+key 注册为可控可管的 provider
4. **中转闭环验证** — 用该 key 跑一次 `/v1/chat/completions` 确认真实可用
全程**无需预置 provider manifest**,不依赖 pack零人工判断。
## 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 缺少模型 → 手动补 → 重新导入
v2 把"探测 → 配置 → 注册 → 验证"压缩成**一键闭环**。
## 3. 核心用户故事
> 作为管理员,我有了一批新的中转 keyURL + token我想在已经运行的宿主上快速开通这些模型。理想情况是我把这批 key 列出来,系统自动探测每个 key 支持什么模型、自动配置宿主 channel、自动注册为可控 provider、自动跑一遍真实 completion 测试,最后告诉我哪些真正可用。
## 4. 技术方案
### 4.1 三阶段管道
```
输入: [(base_url, api_key), ...]
Stage 1: Probe ─────────────────────────────────────────────────
for each (url, key):
upstream_models = GET {url}/v1/models
→ extract model list
upstream_completion = POST {url}/v1/chat/completions (smoke)
→ HTTP status, latency, error_type
classify: models_ok | models_fail | completion_fail | unreachable
Stage 2: Provision ──────────────────────────────────────────────
for each (url, key) where upstream_models != models_fail:
host_channel = find_or_create_channel(provider_id, url)
missing_models = upstream_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)
probe_result = account_test(managed_account, smoke_test_model)
register_provider_binding(provider_id, url, key, upstream_models)
Stage 3: Validate ───────────────────────────────────────────────
for each registered (url, key):
final_completion = POST host_gw/v1/chat/completions
via managed_account key
→ write access_status: active | broken | degraded
output: per-url status + summary
输出: BatchImportResult {
total: int
active: int
broken: int
degraded: int
details: [{url, upstream_models, channel_config, access_status, error}]
}
```
### 4.2 关键设计决策
#### Q1: 如何从 `/v1/models` 提取模型列表?
OpenAI-compatible 上游返回格式为:
```json
{
"data": [{"id": "gpt-4", "object": "model", ...}, ...]
}
```
提取策略:
-`data[].id` 作为模型名
- 过滤掉以 `gpt-` / `claude-` / `text-` / `embedding-` 开头的明显非目标模型
- 保留其余作为"发现的模型列表"
#### Q2: 如何把上游模型写入宿主 channel
宿主 channel 有两个相关字段:
- `model_mapping: map[string]string``{upstream_model: gateway_model}`
- `restrict_models: bool` — true 时 gateway 只路由 mapping 内的模型
策略:
- `model_mapping[key] = key`(一对一映射,上游模型名即 gateway 模型名)
- `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`
### 4.3 数据流
```
BatchImportRequest
├── base_url: string
├── api_key: string
└── access_mode: "subscription" | "self_service" (可选,默认 subscription)
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 发现)
├── channel_id: int64 (Stage 2 创建/更新)
├── account_id: int64 (Stage 2 创建/更新)
├── probe_ok: bool (Stage 2 account test)
├── access_status: string (Stage 3 最终)
└── error: string | null
```
### 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
```
## 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 失败 | ❌ |
## 7. 错误恢复策略
- Stage 1 失败:记录 `upstream_unreachable`,跳过 Stage 2/3
- Stage 2 部分失败:已完成资源保留(不自动回滚)
- Stage 3 失败access_status 降级,但已创建资源不删除
- 整批中断:按 `--mode strict | partial` 处理
- `strict`:任一 item 失败,整批停止,报告已完成的
- `partial`(默认):失败 item 单独记录,成功的继续
## 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 解析
completion.go # smoke test POST /v1/chat/completions
classifier.go # 错误分类auth/rate_limit/upstream/unreachable
batch/ # 新增:批量导入编排
service.go # BatchImportService: 管道编排
provider_id.go # URL → provider_id 规范化
channel_evolution.go # model_mapping 扩展逻辑
host/sub2api/
channel.go # 新增: PatchChannel(channel_id, add_model_mapping)
cmd/
cli/
batch_import.go # 新增: batch-import 命令
tests/integration/
batch_import_test.go # 新增: 批量导入集成测试
```
## 10. 测试策略
### 单测
- `probe/models_test.go` — 模型列表解析,覆盖 OpenAI 格式变体
- `probe/classifier_test.go` — 错误类型分类
- `batch/provider_id_test.go` — URL → provider_id 规范化
- `batch/channel_evolution_test.go` — model_mapping 扩展差异计算
- `batch/service_test.go` — 管道编排 mock 测试
### 集成测
- `tests/integration/batch_import_test.go`
- 两组 (url, key)probe + provision + validate 全流程
- strict 模式任一失败整批停止
- partial 模式失败 item 隔离
## 11. 暂不做v2 范围外)
- Web UI / HTTP API 入口CLI 先跑通)
- 自动发现 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. 全流程不修改宿主源码,不写宿主数据库
## 13. 开放问题(已决策)
1. **provider_id 策略**:选 Bhost + hash`{normalized_host}-{url_hash_last8}`
2. **model_pricing 为空**:选 B自动补空 pricing填默认值不阻塞导入
3. **smoke test model**:选 C遍历 data 找第一个能完成 chat completion 的模型