Files
EvoScientist/.superpowers/sdd/briefs/task-1-report.md
T
m4 c21fc0a272 feat(model-registry): add RegistryV4 schema, SQLite store, and unified error codes
New independent subpackage EvoScientist/model_registry implementing the
frozen unified model configuration design (v1.1.0, sections 4.2, 4.3,
5.1, 8.2, 9.5):

- schemas.py: single Pydantic v2 RegistryV4 schema (ModelRef, ProviderConfig,
  ModelConfig, AuthConfig) plus AdapterParameterSpec/AuthSpec/ParameterRule,
  ModelAvailability, ResolvedModelConfig (no secrets), CredentialStatus
- errors.py: all 23 stable error codes from the 9.5 table with HTTP status
  mapping and the unified {code, message, details, request_id} payload
- store.py: ModelRuntimeStore over model-runtime.sqlite3 (0700 dir, 0600
  file, WAL, foreign keys, busy_timeout) with BEGIN IMMEDIATE revision CAS,
  bootstrap->active atomic transition, immutable credential versions with
  masked status, model_verifications upsert, run_runtime_snapshots partial
  unique index, delegation_jtis, and shared-storage lock probe
- hashing.py: normalized SHA-256 configuration_hash

No existing module behavior changed. 89 new tests; full suite passes
(2922 passed, 10 skipped).
2026-07-20 20:43:28 +08:00

8.2 KiB
Raw Blame History

Task 1 报告 — RegistryV4 schema + SQLite 存储层 + 统一错误码

实现摘要

按简报与设计文档 v1.1.0(4.2、4.3、5.1、8.2、9.5 节)在仓库内新建独立子包 EvoScientist/model_registry/,未改动任何现有文件(顶层 EvoScientist/__init__.py 采用惰性导出,无需修改)。

  1. schemas.py — 唯一 RegistryV4 schema(Pydantic v2)
    • ProviderId/ModelKey/CredentialId/AdapterId 共用锚定全匹配模式 \A[a-z0-9][a-z0-9._-]{0,63}\z(pydantic 的 pattern 约束默认是子串搜索,必须锚定)。
    • upstream_model_id 仅限长 1–300、保留大小写;ValidatedEndpoint 限长 2048 且要求 http(s) scheme(EndpointPolicy 属后续任务)。
    • RegistryV4 { version: Literal[4], revision: PositiveInt, state, defaults, providers }; schema 级校验:Provider ID 唯一、Provider 内模型 key 唯一、defaults 引用必须存在、 active 状态下 primary 非空且引用已启用模型(auxiliary 同理)。
    • ProviderConfig.runtime:timeout_seconds [10,600](缺省 120)、max_retries [0,5] (缺省 2)、default_temperature [0,2]|null、default_top_p (0,1]|null、 default_reasoning_effort 缺省 auto。
    • ModelConfig.runtime:limit_mode combined 要求 context_window_tokens、input_only 要求 max_input_tokens(model_validator);min_effective_input_tokens 缺省 4096、 下限 1024;三个 fixed_*_reserve_tokens 非负;temperature/top_p/reasoning_effort 与 declared_capabilities 按 4.3 定义。
    • AuthConfig:mode=none 时 credential_id 必须为 null(model_validator)。
    • 6.2/6.4/9.1 结构:AdapterParameterSpec(含 connection 可选块,对应 6.2 示例 chat_model/model_field/base_url_field)、AuthSpec、ParameterRule、 ModelAvailability(VerificationInfo)、ResolvedModelConfig (auth_ref={mode, credential_id?, credential_revision?},无任何 secret 字段)、 CredentialStatus、CredentialWrite(9.2 的 operation: replace)。
  2. errors.py — 统一错误码与载荷
    • 23 个稳定错误码常量 + ERROR_HTTP_STATUS 映射,覆盖 9.5 总表全部 17 组 (409×6、401×1、404×1、422×15)。
    • ErrorPayload {code, message, details:[{path, code}], request_id} 与 9.5 结构一致; ModelRegistryError 携带 code/http_status/payload(),未知 code 直接拒绝。
  3. store.py — ModelRuntimeStore
    • 数据库 <config_dir>/model-runtime.sqlite3(默认 ~/.config/evoscientist,可注入); 目录 0700、文件 0600、WAL、外键、busy_timeout=30000。
    • 手写 DDL(CREATE TABLE IF NOT EXISTS,无 alembic):registry_state(单行)、 credential_pointers、credential_versions((credential_id, revision) 主键)、 model_verifications(五元组主键,upsert 只留最近一次)、run_runtime_snapshots (含部分唯一索引 UNIQUE(deployment_id, thread_id, run_request_id) WHERE status IN ('prepared','bound'))、delegation_jtis。
    • load_registry() 无行时返回 bootstrap/revision=1 空 RegistryV4; save_registry(expected_revision=, registry=, credential_writes=) 在 BEGIN IMMEDIATE 事务内校验 revision(不符抛 REGISTRY_REVISION_CONFLICT)、 写入不可变凭据版本、registry revision+1;首次同时具备已启用模型+有效 primary+已配置 凭据(或 mode=none)时原子转为 active;任一失败整体回滚。每次保存强制执行 9.2 第 7 条(defaults 必须引用已启用模型,违反抛 MODEL_DISABLED)。
    • 凭据:write_credential_version(递增 revision;重写相同当前密钥幂等返回原 revision)、resolve_credential(不存在/已销毁抛 RUN_CREDENTIAL_REVISION_UNAVAILABLE)、retire_credential_version、 credential_status(hint 末 4 位 ...abcd,短于 4 字符的密钥 hint 为 null 绝不泄露; 绝不返回明文)。
    • 快照:insert_run_snapshot/set_run_snapshot_status/get_run_snapshot (状态机 prepared|bound|expired|aborted,部分唯一索引行为由测试覆盖)。
    • check_shared_storage():探测 BEGIN IMMEDIATE 写锁能力,失败抛 SharedStorageError(多节点不共享持久卷时启动失败)。
  4. hashing.py — configuration_hash(provider, model)
    • 覆盖 adapter、base_url、upstream_model_id、Provider 与 Model 全部运行参数(含声明 能力与限制),json.dumps(sort_keys=True, separators=(",", ":")) 规范化后 SHA-256。

文件清单

新增(无修改既有文件):

  • EvoScientist/model_registry/__init__.py
  • EvoScientist/model_registry/schemas.py
  • EvoScientist/model_registry/errors.py
  • EvoScientist/model_registry/store.py
  • EvoScientist/model_registry/hashing.py
  • tests/test_model_registry_schemas.py
  • tests/test_model_registry_store.py
  • .superpowers/sdd/briefs/task-1-report.md(本文件)

测试命令与输出

TDD 流程:先写两个测试文件并确认失败(ModuleNotFoundError: No module named 'EvoScientist.model_registry'),再实现。

$ .venv/bin/python -m pytest tests/test_model_registry_schemas.py tests/test_model_registry_store.py -x -q
........................................................................ [ 80%]
.................                                                        [100%]
89 passed in 0.22s

全量回归(无既有失败,无回归):

$ .venv/bin/python -m pytest tests/ -x -q
........sssss.......................................                     [100%]
2922 passed, 10 skipped, 1 warning in 73.00s

(warning 为 test_langgraph_dev_http.py 的 StarletteDeprecationWarning,既有、与本任务无关。)

lint 与格式:

$ .venv/bin/ruff check EvoScientist/model_registry tests/test_model_registry_schemas.py tests/test_model_registry_store.py
All checks passed!
$ .venv/bin/ruff format --check ...   # 已格式化

自我审查发现(已处理)

  1. 测试副作用污染真实配置目录:初版 test_default_config_dir 未注入路径,运行时在 真实 ~/.config/evoscientist/ 创建了空的 model-runtime.sqlite3。已确认该库所有表 为空(确为测试副产物)后删除(含 -wal/-shm),并把测试改为 monkeypatch store.DEFAULT_CONFIG_DIR 到 tmp_path,此后测试不再触碰真实 home。
  2. 共享 Field() 实例:初版 _TEMPERATURE/_TOP_P 在三个模型间复用同一 FieldInfo, 已改为各字段独立 Field(...),规避 pydantic 共享元数据的潜在风险。
  3. docstring 混入中文:save_registry 一处 docstring 误用中文,已改为英文以符合 仓库注释惯例。
  4. 并发 CAS 断言:sorted() 大小写排序导致误报,改为不区分大小写排序。
  5. ruff 修复:datetime.UTC 别名、导入排序、pytest.raises 增加 match=。

遗留疑虑

  1. connection 块为可选:6.2 正文的"至少包含"清单未列 connection,但 glm-5.2 示例契约包含它。schema 将其建模为可选字段,Task 2 落地五种 Adapter 契约时若确认 每个契约都有 connection,可考虑收紧为必填。
  2. 激活就绪判定中的"满足 AuthSpec":4.3 要求激活时认证状态满足 AuthSpec,但 Adapter 契约属 Task 2。当前 store 仅做存储层判定(mode=none 或凭据已配置); AuthSpec 级别校验(如 credential_kind 匹配)需在 Task 2/3 的 API 层补充。
  3. 幂等语义解释:简报称 write_credential_version 为"幂等 prepare",实现为"重写 与当前版本完全相同的密钥时返回现有 revision";不同密钥轮换仍产生新 revision。若 后续任务对幂等键有不同约定(如客户端提供 request id),需再对齐。
  4. save_registry 参数为 keyword-only:与简报签名 save_registry(expected_revision, registry, credential_writes=[]) 语义一致,仅调用 形式略异。
  5. MODEL_DISABLED 用于 defaults 引用未启用模型的保存错误(9.2 第 7 条属 422 校验, 总表无更贴切码);引用不存在模型由 schema 层先行拒绝,store 内同名分支仅作防御。