# 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. 核心用户故事 > 作为管理员,我有了一批新的中转 key(URL + 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 \ --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 ``` ## 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 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. 全流程不修改宿主源码,不写宿主数据库 ## 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 的模型