docs(v2): align reuse and account lifecycle contracts

This commit is contained in:
phamnazage-jpg
2026-05-22 14:15:41 +08:00
parent d68fb9daa3
commit f718f2d888
7 changed files with 432 additions and 17 deletions

View File

@@ -14,6 +14,7 @@ V2 的目标不是“又一条导入命令”,而是把这件事做成**稳定
5. **异步确认**:吸收宿主异步 probe、首次 `403/503` 的预热窗口
6. **闭环验证**:以宿主网关真实 `/v1/chat/completions` 结果作为最终可用性判断
7. **结果可视**:提供 run 列表、run 详情、item 详情,而不是只靠日志和 artifact
8. **重复导入复用**:已成功导入且模型已覆盖的 provider再次添加时应自动复用而不是重复创建
## 2. Scope
@@ -166,14 +167,18 @@ BatchImportRunItemView
- item_id: string
- base_url: string
- provider_id: string
- api_key_fingerprint: string
- requested_models: []string
- raw_models: []string
- normalized_models: []string
- canonical_model_families: []string
- resolved_smoke_model: string | null
- recommended_models: []string
- current_stage: string
- confirmation_status: string
- access_status: string
- matched_account_state: string
- account_resolution: string
- retry_count: int
- last_retry_at: string | null
- advisory_messages: []string
@@ -181,6 +186,9 @@ BatchImportRunItemView
- last_error: string | null
- channel_id: int64 | null
- account_id: int64 | null
- provision_reused: bool
- reused_from_provider_id: string | null
- reused_from_account_id: int64 | null
- capability_profile: object
```
@@ -235,6 +243,22 @@ Stage 5: Project
1. **transport profile**:这个 upstream 支不支持 `/models``/chat/completions``/responses``/messages`
2. **model profiles**:这个 upstream 下的具体模型,在 stream/tools/reasoning 字段上是否可用
### 6.1.1 为什么还要有 canonical model family
不同中转对同一个模型的命名可能有轻微差异,但 API 和能力集本质一致,例如:
- `kimi 2.6`
- `kimi-2.6`
- `kimi-k2.6`
- `Kimi-K2.6`
V2 不能把这些名字当成完全不同的模型,而要继续归并到同一个 `canonical_model_family`,用于:
- 重复导入复用判断
- 模型覆盖判断
- 别名 patch 判断
- 推荐模型名输出
### 6.2 Canonical schema
```json
@@ -255,6 +279,7 @@ Stage 5: Project
{
"raw_model_id": "deepseek-ai/DeepSeek-V4-Pro",
"normalized_model_id": "deepseek-v4-pro",
"canonical_model_family": "deepseek-v4-pro",
"supports_stream": true,
"supports_tools": "unknown",
"supports_reasoning_fields": "unknown",
@@ -272,7 +297,109 @@ Stage 5: Project
- 决定推荐 smoke model
- 决定后续快速匹配“哪个模型在哪种兼容层下靠谱”
## 7. Channel / Account Evolution Contract
### 6.4 Canonical model family 规则
V2 对模型名做三层处理:
1. `raw_model_id`
2. `normalized_model_id`
3. `canonical_model_family`
示例:
| raw_model_id | normalized_model_id | canonical_model_family |
|---|---|---|
| `kimi 2.6` | `kimi-2.6` | `kimi-2.6` |
| `kimi-k2.6` | `kimi-k2.6` | `kimi-2.6` |
| `Kimi-K2.6` | `kimi-k2.6` | `kimi-2.6` |
| `deepseek-ai/DeepSeek-V4-Pro` | `deepseek-v4-pro` | `deepseek-v4-pro` |
约束:
- `canonical_model_family` 用于跨中转识别“是否同一个模型族”
- `normalized_model_id` 用于控制面和 channel 落盘
- `raw_model_id` 用于保留 upstream 原始路由
## 7. Existing Provider Reuse / Idempotent Re-import
### 7.1 目标
如果某个 provider 已成功导入,且现有模型族已覆盖本次请求模型,则再次添加时应:
- 不重复创建 channel/account/provider
- 直接复用既有成功链路
- 必要时仅 patch 新 alias / 新模型映射
### 7.2 预检查顺序
每个 item 在 Stage 2 前必须按顺序执行:
1.`host_id + provider_id` 查现有 provider
2.`host_id + base_url + api_key_fingerprint` 查现有 account
3. 比较:
- `canonical_model_families`
- `normalized_models`
- 既有 `access_status`
- 既有账号健康状态
### 7.3 决策表
| 场景 | 行为 |
|---|---|
| provider 已存在,`access_status=active`,且既有 `canonical_model_families` 覆盖本次请求 | 直接复用,不再 provision |
| 命中现有 account且账号状态为 `active` | 标记为重复已启用账号,直接复用并提示 `duplicate_active_account` |
| 命中现有 account且账号状态为 `disabled``deprecated`,但 key 仍健康 | 走 `reactivated` 路径,快速启用已有账号,不新建账号 |
| provider 已存在,账号健康,但只缺少部分 alias / mapping | 只 patch不重建 |
| provider 已存在,但 key 已失效或 `access_status=broken` | 不复用,进入 repair/replace |
| 同 host 同 URL但 access_mode 不同 | 不直接复用 access 结果,按 mode 分别确认 |
### 7.4 复用后的 item 投影
若命中复用item 仍要生成新的 V2 记录,并写明:
- `provision_reused = true`
- `reused_from_provider_id`
- `reused_from_account_id`
- `matched_account_state`
- `account_resolution`
### 7.4.1 已存在账号的处理原则
V2 必须同时回答两件事:
1. 这次 provider 是否被复用
2. 命中的既有账号当前是什么状态
对于 `host_id + base_url + api_key_fingerprint` 命中的账号:
- `active`
- 不重复创建账号
- `matched_account_state=active`
- `account_resolution=reused`
- UI 文案显示“重复,已启用”
- `disabled` / `deprecated`
- 优先尝试启用已有账号
- `matched_account_state=disabled|deprecated`
- `account_resolution=reactivated`
- UI 文案显示“已弃用,已快速启用”
- `broken`
- 不直接复用
- `matched_account_state=broken`
- `account_resolution=replaced`
- 进入 repair/replace 流程
### 7.5 Key fingerprint
V2 不以原始 key 字符串作为重复匹配依据,而保存:
- `api_key_fingerprint`
用于区分:
- 同一把 key 的重复导入
- 同 URL 下新增另一把 key
## 8. Channel / Account Evolution Contract
V2 不再使用“薄 patch 接口”表达 channel 更新。宿主 patch 必须以完整 contract 表达:
@@ -291,7 +418,7 @@ ChannelPatchContract
- patch 不得破坏旧模型
- `PatchChannel(addModels []string)` 这类接口不再作为 V2 canonical contract
## 8. Async Confirmation Mechanism
## 9. Async Confirmation Mechanism
### 8.1 为什么 V2 必须有后台 confirmer
@@ -334,7 +461,7 @@ V2 第一版即要求:
- 页面能看到 item 停在哪个阶段
- CLI `--confirm-wait-timeout` 只是“等待窗口”,不是确认机制本身
## 9. Single Source of Truth
## 10. Single Source of Truth
### 9.1 Canonical runtime tables
@@ -362,7 +489,7 @@ V2 运行态只认三类表:
- `access_closure_records`
- 宿主数据库
## 10. Result API and Pages
## 11. Result API and Pages
### 10.1 API
@@ -394,7 +521,7 @@ Legacy API `/api/import-batches/*` 保留,但标为 v1/legacy。
- 重试过几次
- 当前 warning 的原因是什么
## 11. CLI Contract
## 12. CLI Contract
```bash
go run ./cmd/cli batch-import \
@@ -419,7 +546,7 @@ CLI 输出必须至少包含:
- `access_status`
- 推荐模型名(若发生纠错)
## 12. Error Policy
## 13. Error Policy
### Blocking
@@ -439,7 +566,7 @@ CLI 输出必须至少包含:
- `confirmation_status=advisory` 不自动等于 `access_status=degraded`
- 只有 Validation Engine 可以把 item 标成 `active/degraded/broken`
## 13. Success Criteria
## 14. Success Criteria
1. `access_mode` 输入契约完整,`subscription` / `self_service` 都可单独落地
2. run / item 状态、重试、warning、错误阶段能持久化并在重启后恢复可见
@@ -448,18 +575,21 @@ CLI 输出必须至少包含:
5. 第三方兼容 upstream 的 `/responses` 误判和宿主异步窗口不会把可用链路直接打成最终失败
6. 页面可以清楚地区分 `confirmed/advisory/failed``active/degraded/broken`
7. OpenAPI、SPEC、TDD、Architecture 对同一字段和同一状态枚举保持一致
8. 已成功导入的 provider 再次添加时,若模型族已覆盖,应自动复用,不重复创建
9. 同模型在不同中转下的轻微命名差异,能通过 `canonical_model_family` 快速识别为同一模型族
## 14. Non-goals for first implementation
## 15. Non-goals for first implementation
- 多 key 自动调度
- 实时推送
- 自动定价策略
- 自动负载均衡
## 15. Final decisions
## 16. Final decisions
1. `provider_id` 采用 `normalized_host + url_hash_last8`
2. `requested_models` 仅作提示,不作为事实源
3. `Validation Engine``access_status` 唯一写入方
4. V2 runtime canonical tables 为 `import_runs/import_run_items/import_run_item_events`
5. `ConfirmationWorker` 是 V2 必备组件,不是可选增强
6. 同模型跨中转匹配以 `canonical_model_family` 为准,而不是只看原始模型名

View File

@@ -11,11 +11,12 @@
1. URL + key 自动发现模型
2. 模型名归一化与推荐纠错
3. provider/model 兼容画像建模
4. 宿主资源演化与 provider 绑定
5. 后台异步确认与有限重试
6. 最终 gateway completion 验证
7. run/item 状态持久化与结果页可读
3. 跨中转同模型快速匹配与复用
4. provider/model 兼容画像建模
5. 宿主资源演化与 provider 绑定
6. 后台异步确认与有限重试
7. 最终 gateway completion 验证
8. run/item 状态持久化与结果页可读
## 2. Canonical Contract
@@ -173,6 +174,7 @@ type AliasResult struct {
func NormalizeModelID(raw string) string
func CanonicalModelID(raw string) string
func CanonicalModelFamily(raw string) string
func BuildAliasTable(rawModels []string) map[string]AliasResult
func ResolveRequestedModel(requested string, rawModels []string) (resolved string, ok bool)
func RecommendModels(requested []string, rawModels []string) []string
@@ -183,6 +185,7 @@ func RecommendModels(requested []string, rawModels []string) []string
```go
func TestNormalizeModelID_MinimaxCanonical(t *testing.T)
func TestNormalizeModelID_DeepSeekVendorPrefix(t *testing.T)
func TestCanonicalModelFamily_KimiVariantsCollapseToSameFamily(t *testing.T)
func TestResolveRequestedModel_UsesNormalizedAlias(t *testing.T)
func TestRecommendModels_ReturnsCanonicalCandidates(t *testing.T)
```
@@ -319,6 +322,44 @@ func TestModelMappingDelta_AddsRawToCanonicalMappings(t *testing.T)
func TestModelMappingDelta_SetsRestrictModelsAndBillingSource(t *testing.T)
```
### 5.4 `internal/batch/reuse_policy.go`
职责:判断已存在 provider/account 是否可直接复用。
```go
type ReuseDecision struct {
ReuseProvision bool
PatchOnly bool
ReplaceAccount bool
ReactivateAccount bool
MatchedAccountState string
AccountResolution string
ReusedFromProviderID string
ReusedFromAccountID *int64
}
func DecideReuse(existing ExistingProviderSnapshot, incoming IncomingProviderSnapshot) ReuseDecision
```
判断依据:
- `host_id + provider_id`
- `base_url + api_key_fingerprint`
- `canonical_model_families`
- 现有 `access_status`
- 现有 key/account 健康状态
单测:
```go
func TestDecideReuse_FullyCoveredAndActive_ReusesProvision(t *testing.T)
func TestDecideReuse_MissingFamilies_PatchOnly(t *testing.T)
func TestDecideReuse_BrokenProvider_RequestsReplacement(t *testing.T)
func TestDecideReuse_SameFamilyDifferentAlias_TreatedAsCovered(t *testing.T)
func TestDecideReuse_ExistingActiveAccount_MarksDuplicateReused(t *testing.T)
func TestDecideReuse_DisabledAccount_RequestsReactivation(t *testing.T)
```
## 6. Stage 3: State Store
### 6.1 `internal/batch/run_state.go`
@@ -351,12 +392,16 @@ type ImportRunItemState struct {
ItemID string
BaseURL string
ProviderID string
APIKeyFingerprint string
CurrentStage ItemStage
ConfirmationStatus ConfirmationStatus
AccessStatus AccessStatus
MatchedAccountState string
AccountResolution string
RequestedModels []string
RawModels []string
NormalizedModels []string
CanonicalModelFamilies []string
ResolvedSmokeModel *string
RecommendedModels []string
CapabilityProfileJSON string
@@ -373,6 +418,9 @@ type ImportRunItemState struct {
LastError *string
LegacyBatchID *int64
LegacyProviderID *string
ProvisionReused bool
ReusedFromProviderID *string
ReusedFromAccountID *int64
CreatedAt time.Time
UpdatedAt time.Time
}
@@ -397,6 +445,7 @@ func TestRunStateStore_CreateAndUpdateRun(t *testing.T)
func TestRunStateStore_UpsertItemStoresProjectionFields(t *testing.T)
func TestRunStateStore_EventTrailCanBeQueried(t *testing.T)
func TestRunStateStore_LeaseFieldsPersist(t *testing.T)
func TestRunStateStore_AccountReuseFieldsPersist(t *testing.T)
```
## 7. Stage 4: Batch Service
@@ -418,6 +467,7 @@ func (s *BatchImportService) StartRun(ctx context.Context, req BatchImportRunReq
职责:
- 创建 run + item
- 先执行 reuse preflight决定是复用、patch 还是 replace
- 先落 probe/provision 结果
- 入队 confirm不在请求线程里承担全部确认责任
- CLI/HTTP 只负责“发起”和“可选等待窗口”
@@ -428,6 +478,7 @@ func (s *BatchImportService) StartRun(ctx context.Context, req BatchImportRunReq
func TestBatchImport_StartRun_PersistsInitialState(t *testing.T)
func TestBatchImport_RequestedModelMiss_UsesDiscoveredModel(t *testing.T)
func TestBatchImport_ProvisionWritesLegacyLinks(t *testing.T)
func TestBatchImport_ExistingActiveProviderAndCoveredFamilies_ReusesProvision(t *testing.T)
```
## 8. Stage 5: Confirmation Worker

View File

@@ -131,6 +131,8 @@ V2 API 只暴露 3 层资源:
- `has_warning`
- `true/false`
- `provider_id`
- `matched_account_state`
- `account_resolution`
- `q`
- 搜索 `provider_id / base_url / item_id`
- `limit`
@@ -235,11 +237,16 @@ V2 API 只暴露 3 层资源:
"item_id": "item_01",
"base_url": "https://kimi.a7m.com.cn/v1",
"provider_id": "kimi-a7m-7d7ac291",
"api_key_fingerprint": "sha256:8d8c4b5f",
"requested_models": ["kimi-k2.6"],
"canonical_model_families": ["kimi-k2.6"],
"resolved_smoke_model": "kimi-k2.6",
"current_stage": "done",
"confirmation_status": "advisory",
"access_status": "active",
"matched_account_state": "active",
"account_resolution": "reused",
"provision_reused": true,
"retry_count": 2,
"last_retry_at": "2026-05-22T12:20:05+08:00",
"advisory_messages": [
@@ -260,14 +267,21 @@ V2 API 只暴露 3 层资源:
"item_id": "item_01",
"base_url": "https://kimi.a7m.com.cn/v1",
"provider_id": "kimi-a7m-7d7ac291",
"api_key_fingerprint": "sha256:8d8c4b5f",
"requested_models": ["kimi-k2.6"],
"raw_models": ["kimi-k2.6"],
"normalized_models": ["kimi-k2.6"],
"canonical_model_families": ["kimi-k2.6"],
"recommended_models": [],
"resolved_smoke_model": "kimi-k2.6",
"current_stage": "done",
"confirmation_status": "advisory",
"access_status": "active",
"matched_account_state": "deprecated",
"account_resolution": "reactivated",
"provision_reused": true,
"reused_from_provider_id": "kimi-a7m-7d7ac291",
"reused_from_account_id": 4,
"retry_count": 2,
"last_retry_at": "2026-05-22T12:20:05+08:00",
"channel_id": 12,
@@ -294,6 +308,7 @@ V2 API 只暴露 3 层资源:
{
"raw_model_id": "kimi-k2.6",
"normalized_model_id": "kimi-k2.6",
"canonical_model_family": "kimi-k2.6",
"supports_stream": true,
"supports_tools": "unknown",
"supports_reasoning_fields": "unknown",
@@ -345,6 +360,31 @@ V2 API 只暴露 3 层资源:
| `degraded` | 黄色 `degraded` |
| `broken` | 红色 `broken` |
### 7.4 Reuse badge
| 字段 | 页面 badge |
|---|---|
| `provision_reused=true` | 青色 `reused` |
| `provision_reused=false` | 不显示 |
### 7.5 Account state badge
| 字段 | 页面 badge |
|---|---|
| `matched_account_state=active` | 绿色 `已启用` |
| `matched_account_state=disabled` | 灰色 `已停用` |
| `matched_account_state=deprecated` | 黄色 `已弃用` |
| `matched_account_state=broken` | 红色 `已损坏` |
### 7.6 Account resolution badge
| 字段 | 页面 badge |
|---|---|
| `account_resolution=created` | 蓝色 `新建` |
| `account_resolution=reused` | 青色 `复用` |
| `account_resolution=reactivated` | 绿色 `已快速启用` |
| `account_resolution=replaced` | 红色 `已替换` |
## 8. 分页与排序建议
### 8.1 Runs 列表
@@ -371,6 +411,8 @@ V2 API 只暴露 3 层资源:
2. item 详情才返回 `capability_profile``events`
3. 不在 handler 里拼装 legacy 表结果
4. 任何页面要显示的 warning 文案,都从 projection 层统一生成
5. `provision_reused``reused_from_provider_id` 等复用字段必须来自 projection不允许前端自行推断
6. `matched_account_state``account_resolution` 也必须来自 projection不允许前端根据 account_id 是否存在自行推断
## 10. 最小实现建议

View File

@@ -26,6 +26,7 @@ V2 必须同时满足:
3. **可解释**warning/broken 必须给出可读原因
4. **可分层**Probe、Provision、Confirm、Validate 各司其职
5. **可兼容**:针对第三方 OpenAI-compatible 上游,显式记录 transport + model capability
6. **可复用**:重复导入命中同 URL + 同模型家族时,优先复用已有 provider/account而不是重复创建
## 3. Canonical runtime model
@@ -66,6 +67,7 @@ operator input
Batch Import API / CLI
BatchImportService
├── Reuse Preflight
├── Probe Layer
├── Capability Profiler
├── Provision Adapter
@@ -95,6 +97,7 @@ BatchImportService
职责:
- 执行 Reuse Preflight
- 执行 Stage 1 Probe
- 执行 Stage 2 Provision
- 把 item 推进到 `confirm`
@@ -170,6 +173,21 @@ Run 级 projection 规则:
- `degraded`
- `broken`
`item.matched_account_state`
- `none`
- `active`
- `disabled`
- `deprecated`
- `broken`
`item.account_resolution`
- `created`
- `reused`
- `reactivated`
- `replaced`
### 5.3 State ownership
| 字段 | 写入者 |
@@ -242,6 +260,7 @@ V2 不再把 capability 当成“一个 key 一个总画像”,而是拆成:
{
"raw_model_id": "kimi-k2.6",
"normalized_model_id": "kimi-k2.6",
"canonical_model_family": "kimi-k2.6",
"supports_stream": true,
"supports_tools": "unknown",
"supports_reasoning_fields": "unknown",
@@ -251,6 +270,22 @@ V2 不再把 capability 当成“一个 key 一个总画像”,而是拆成:
}
```
### 7.4 Canonical model family 的作用
`normalized_model_id` 只能解决字符串归一化,不能稳定回答跨中转的“是否同一模型家族”。
V2 需要额外记录 `canonical_model_family`,用于识别这类情况:
- `kimi 2.6`
- `kimi-2.6`
- `kimi-k2.6`
它们可能属于同一个模型家族,应支持:
1. 重复导入时快速匹配
2. 已存在 provider 只 patch 别名映射,不重复 provision
3. 结果页解释“为何这次被直接复用”
## 8. State store schema
### 8.1 `import_runs`
@@ -278,15 +313,22 @@ item_id TEXT PRIMARY KEY
run_id TEXT NOT NULL
base_url TEXT NOT NULL
provider_id TEXT NOT NULL
api_key_fingerprint TEXT NOT NULL
requested_models_json TEXT NOT NULL
raw_models_json TEXT NOT NULL
normalized_models_json TEXT NOT NULL
canonical_model_families_json TEXT NOT NULL
resolved_smoke_model TEXT NULL
recommended_models_json TEXT NOT NULL
capability_profile_json TEXT NOT NULL
current_stage TEXT NOT NULL
confirmation_status TEXT NOT NULL
access_status TEXT NOT NULL
matched_account_state TEXT NOT NULL
account_resolution TEXT NOT NULL
provision_reused INTEGER NOT NULL
reused_from_provider_id TEXT NULL
reused_from_account_id INTEGER NULL
channel_id INTEGER NULL
account_id INTEGER NULL
retry_count INTEGER NOT NULL
@@ -309,8 +351,54 @@ updated_at DATETIME NOT NULL
- `resolved_smoke_model` 可为 `NULL`,因为 Stage 1 可能失败
- `channel_id/account_id` 可为 `NULL`,因为 Stage 2 可能未开始
- `access_status` 初始必须允许 `unknown`
- `api_key_fingerprint` 只存指纹,不存明文 key
- `canonical_model_families_json` 是 Reuse Preflight 的核心输入,而不是附带展示字段
- `matched_account_state` 用于结果页直接展示“重复已启用 / 已弃用待启用 / 已重新启用”
- `account_resolution` 用于解释这次 item 最终是创建、复用、快速启用还是替换
### 8.3 `import_run_item_events`
### 8.3 为什么必须有 key fingerprint
只靠 `provider_id` 不能稳定区分:
- 同一个 URL 下是否是同一把 key
- 是真正的重复导入,还是同 URL 的新账号
因此 V2 必须显式落:
- `api_key_fingerprint`
- `provision_reused`
- `reused_from_provider_id`
- `reused_from_account_id`
这样 Reuse Preflight 才能先按:
1. `host_id + provider_id`
2. `host_id + base_url + api_key_fingerprint`
3. `canonical_model_families`
做稳定判定。
### 8.4 已存在账号状态的标准化
对命中的既有账号V2 需要统一投影为:
- `active`
- 当前账号已启用,可直接使用
- `disabled`
- 当前账号被停用,但可尝试快速启用
- `deprecated`
- 当前账号不推荐继续调度,但仍可由 operator 重新启用
- `broken`
- 当前账号不可直接复用
对应本次导入的处理结果:
- `created`
- `reused`
- `reactivated`
- `replaced`
### 8.5 `import_run_item_events`
```text
event_id TEXT PRIMARY KEY
@@ -331,7 +419,7 @@ created_at DATETIME NOT NULL
- `advisory_added`
- `validation_result`
### 8.4 为什么必须有 event 表
### 8.6 为什么必须有 event 表
仅靠 `retry_count` 不足以支撑结果页要求。
页面要能展示:
@@ -421,6 +509,14 @@ ValidationService 只做一件事:
- `账号创建后宿主异步探测尚未稳定,首次 /test 已按 advisory 处理`
- `gateway_warmup_retry_succeeded`
- `初次调度出现 no available accounts短暂重试后已恢复`
- `provision_reused`
- `已检测到同 URL + 同模型家族 + 健康账号,系统直接复用已有 provider`
- `patch_only_new_aliases`
- `模型属于已覆盖家族,仅补充别名映射与定价,不重复创建资源`
- `duplicate_active_account`
- `该账号已存在且处于启用状态,本次未重复创建,直接复用`
- `deprecated_account_reactivated`
- `该账号此前处于弃用/停用状态,本次已快速启用并重新确认`
## 12. Result pages
@@ -457,11 +553,16 @@ Item 行至少包含:
- `item_id`
- `base_url`
- `provider_id`
- `api_key_fingerprint`
- `requested_models`
- `canonical_model_families`
- `resolved_smoke_model`
- `current_stage`
- `confirmation_status`
- `access_status`
- `matched_account_state`
- `account_resolution`
- `provision_reused`
- `retry_count`
- `last_error_stage`
- `last_error`
@@ -470,9 +571,14 @@ Item 详情至少包含:
- `raw_models`
- `normalized_models`
- `canonical_model_families`
- `recommended_models`
- `transport_profile`
- `model_profiles`
- `matched_account_state`
- `account_resolution`
- `reused_from_provider_id`
- `reused_from_account_id`
- `channel_id`
- `account_id`
- `advisory_messages`

View File

@@ -34,7 +34,12 @@ V2 的目标不是替换 v1 的执行链,而是新增一套**面向长任务
- item 可以记录 `legacy_provider_id`
- 仅用于追溯,不用于投影
5. **按 SQLite 友好方式设计**
5. **支持重复导入复用**
- 明确记录 `api_key_fingerprint`
- 明确记录 `provision_reused` 与复用来源
- 支撑“同 URL + 同模型家族”直接复用
6. **按 SQLite 友好方式设计**
- 不使用复杂 JSON 索引
- 关键筛选字段保留标量列
- 复杂结构用 `TEXT` JSON 保存
@@ -94,9 +99,11 @@ CREATE TABLE import_run_items (
run_id TEXT NOT NULL,
base_url TEXT NOT NULL,
provider_id TEXT NOT NULL,
api_key_fingerprint TEXT NOT NULL,
requested_models_json TEXT NOT NULL DEFAULT '[]',
raw_models_json TEXT NOT NULL DEFAULT '[]',
normalized_models_json TEXT NOT NULL DEFAULT '[]',
canonical_model_families_json TEXT NOT NULL DEFAULT '[]',
recommended_models_json TEXT NOT NULL DEFAULT '[]',
resolved_smoke_model TEXT NULL,
capability_profile_json TEXT NOT NULL DEFAULT '{}',
@@ -104,6 +111,11 @@ CREATE TABLE import_run_items (
current_stage TEXT NOT NULL,
confirmation_status TEXT NOT NULL,
access_status TEXT NOT NULL,
matched_account_state TEXT NOT NULL DEFAULT 'none',
account_resolution TEXT NOT NULL DEFAULT 'created',
provision_reused INTEGER NOT NULL DEFAULT 0,
reused_from_provider_id TEXT NULL,
reused_from_account_id INTEGER NULL,
channel_id INTEGER NULL,
account_id INTEGER NULL,
@@ -139,6 +151,7 @@ CREATE TABLE import_run_items (
```sql
CREATE INDEX idx_import_run_items_run_id ON import_run_items(run_id);
CREATE INDEX idx_import_run_items_provider_id ON import_run_items(provider_id);
CREATE INDEX idx_import_run_items_key_fingerprint ON import_run_items(api_key_fingerprint);
CREATE INDEX idx_import_run_items_current_stage ON import_run_items(current_stage);
CREATE INDEX idx_import_run_items_confirmation_status ON import_run_items(confirmation_status);
CREATE INDEX idx_import_run_items_access_status ON import_run_items(access_status);
@@ -151,9 +164,12 @@ CREATE INDEX idx_import_run_items_lease_until ON import_run_items(lease_until);
下列字段必须保留标量列,不能只藏在 JSON 里:
- `provider_id`
- `api_key_fingerprint`
- `current_stage`
- `confirmation_status`
- `access_status`
- `matched_account_state`
- `account_resolution`
- `next_retry_at`
- `lease_until`
@@ -163,6 +179,45 @@ CREATE INDEX idx_import_run_items_lease_until ON import_run_items(lease_until);
- 结果页列表要按这些字段筛选
- SQLite 下从 JSON 中筛选成本高、代码复杂度高
## 4.4 重复导入复用预检查
V2 需要显式支持:
- 已成功导入的 provider 再次添加时自动复用
- 同模型不同别名只 patch mapping
因此 migration 必须支撑以下预检查顺序:
1. `host_id + provider_id`
2. `host_id + base_url + api_key_fingerprint`
3. `canonical_model_families_json` 与现有 provider 覆盖关系比较
结果分三类:
- `reused`
- `provision_reused=1`
- 写入 `reused_from_provider_id`
- 视情况写入 `reused_from_account_id`
- `patch_only`
- 不重建 provider/account
- 仅更新 alias/model_mapping/model_pricing
- `replace`
- 原 provider broken 或 key 失效
- 重新 provision
同时对命中的既有账号,还要额外落两类语义:
- `matched_account_state`
- `active | disabled | deprecated | broken`
- `account_resolution`
- `created | reused | reactivated | replaced`
这样结果页才能直接显示:
- “重复,已启用”
- “已弃用,已快速启用”
- “已损坏,已替换”
## 5. `0008_batch_import_run_events.sql`
### 5.1 `import_run_item_events`
@@ -325,6 +380,7 @@ LIMIT ?;
1. **幂等**
- `run_id``item_id` 由控制面生成,不能依赖数据库自增主键做外部 API 标识
- `api_key_fingerprint + provider_id + canonical_model_families` 是重复导入复用的关键判定输入
2. **可恢复**
- 任何阶段切换前后都要先写 item

View File

@@ -169,6 +169,9 @@
- capability 从 upstream 总画像升级为 transport + model profiles
- 结果页字段、状态库存储字段、retry/event trail 已统一
- OpenAPI 已补齐 `/api/batch-import/runs*`legacy `/api/import-batches/*` 降级为 v1/legacy
- 已补充重复导入自动复用策略:按 `provider_id + api_key_fingerprint + canonical_model_family` 判断 `reused / patch_only / replace`
- 已补充同模型别名归一化契约:例如 `kimi 2.6 / kimi-2.6 / kimi-k2.6` 可归并到同一模型家族并快速复用
- 已补充多账号重复导入与弃用账号再启用策略active 账号提示“重复已启用”disabled/deprecated 账号显示原状态并走 `reactivated` 快速启用路径
**当前剩余项**
- [x] 按收口后的 canonical contract 输出数据库 migration 草案

View File

@@ -736,6 +736,8 @@ components:
type: string
normalized_model_id:
type: string
canonical_model_family:
type: string
supports_stream:
type: string
supports_tools:
@@ -762,10 +764,16 @@ components:
type: string
provider_id:
type: string
api_key_fingerprint:
type: string
requested_models:
type: array
items:
type: string
canonical_model_families:
type: array
items:
type: string
resolved_smoke_model:
type: string
nullable: true
@@ -778,6 +786,14 @@ components:
access_status:
type: string
enum: [unknown, active, degraded, broken]
matched_account_state:
type: string
enum: [none, active, disabled, deprecated, broken]
account_resolution:
type: string
enum: [created, reused, reactivated, replaced]
provision_reused:
type: boolean
retry_count:
type: integer
last_retry_at:
@@ -825,10 +841,21 @@ components:
type: array
items:
type: string
canonical_model_families:
type: array
items:
type: string
recommended_models:
type: array
items:
type: string
reused_from_provider_id:
type: string
nullable: true
reused_from_account_id:
type: integer
format: int64
nullable: true
channel_id:
type: integer
format: int64