c21fc0a272
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).
8.2 KiB
8.2 KiB
Task 1 报告 — RegistryV4 schema + SQLite 存储层 + 统一错误码
实现摘要
按简报与设计文档 v1.1.0(4.2、4.3、5.1、8.2、9.5 节)在仓库内新建独立子包
EvoScientist/model_registry/,未改动任何现有文件(顶层 EvoScientist/__init__.py
采用惰性导出,无需修改)。
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_modecombined 要求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)。
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 直接拒绝。
- 23 个稳定错误码常量 +
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(多节点不共享持久卷时启动失败)。
- 数据库
hashing.py—configuration_hash(provider, model)- 覆盖 adapter、base_url、upstream_model_id、Provider 与 Model 全部运行参数(含声明
能力与限制),
json.dumps(sort_keys=True, separators=(",", ":"))规范化后 SHA-256。
- 覆盖 adapter、base_url、upstream_model_id、Provider 与 Model 全部运行参数(含声明
能力与限制),
文件清单
新增(无修改既有文件):
EvoScientist/model_registry/__init__.pyEvoScientist/model_registry/schemas.pyEvoScientist/model_registry/errors.pyEvoScientist/model_registry/store.pyEvoScientist/model_registry/hashing.pytests/test_model_registry_schemas.pytests/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 ... # 已格式化
自我审查发现(已处理)
- 测试副作用污染真实配置目录:初版
test_default_config_dir未注入路径,运行时在 真实~/.config/evoscientist/创建了空的model-runtime.sqlite3。已确认该库所有表 为空(确为测试副产物)后删除(含 -wal/-shm),并把测试改为 monkeypatchstore.DEFAULT_CONFIG_DIR到 tmp_path,此后测试不再触碰真实 home。 - 共享
Field()实例:初版_TEMPERATURE/_TOP_P在三个模型间复用同一 FieldInfo, 已改为各字段独立Field(...),规避 pydantic 共享元数据的潜在风险。 - docstring 混入中文:
save_registry一处 docstring 误用中文,已改为英文以符合 仓库注释惯例。 - 并发 CAS 断言:
sorted()大小写排序导致误报,改为不区分大小写排序。 - ruff 修复:
datetime.UTC别名、导入排序、pytest.raises增加match=。
遗留疑虑
connection块为可选:6.2 正文的"至少包含"清单未列connection,但 glm-5.2 示例契约包含它。schema 将其建模为可选字段,Task 2 落地五种 Adapter 契约时若确认 每个契约都有 connection,可考虑收紧为必填。- 激活就绪判定中的"满足 AuthSpec":4.3 要求激活时认证状态满足 AuthSpec,但
Adapter 契约属 Task 2。当前 store 仅做存储层判定(
mode=none或凭据已配置); AuthSpec 级别校验(如 credential_kind 匹配)需在 Task 2/3 的 API 层补充。 - 幂等语义解释:简报称
write_credential_version为"幂等 prepare",实现为"重写 与当前版本完全相同的密钥时返回现有 revision";不同密钥轮换仍产生新 revision。若 后续任务对幂等键有不同约定(如客户端提供 request id),需再对齐。 save_registry参数为 keyword-only:与简报签名save_registry(expected_revision, registry, credential_writes=[])语义一致,仅调用 形式略异。MODEL_DISABLED用于 defaults 引用未启用模型的保存错误(9.2 第 7 条属 422 校验, 总表无更贴切码);引用不存在模型由 schema 层先行拒绝,store 内同名分支仅作防御。