"""T08 专项提供方准入契约:逐任务/用途验收与冻结权重修订。 Owner:本文件(docs/v2/11-judgment-providers.md §8.1 的 profile 契约)。 `contracts/prescreen.py` 继续持有运行侧对象(Judgment / JudgmentConfig / ScreeningRun);把 profile 契约独立出来有两个理由: 1. 11 §8.1 规定 profile 必须带 tokenizer / 预算字段,而 `prescreen.py` 有一条 静态底线(`test_prescreen_contracts.test_no_credentials_in_contract_module`: 契约模块不得出现凭据字样)。把预算字段放进独立模块,比放宽那条安全断言更合适。 2. T08 的准入规则(task_validations)与运行对象生命周期不同:配置可换修订, 运行不得被就地改写(BR-20)。 依赖方向:本模块**不导入** `services/`。 """ from __future__ import annotations from typing import Literal from pydantic import Field, model_validator from contracts.prescreen import ID_MAX, PATH_MAX, Contract JudgmentTask = Literal[ "extraction_grounding", "experience_applicability", "trajectory_quality", "evidence_support", ] JudgmentUsage = Literal["offline", "shadow", "runtime"] class TaskValidation(Contract): """逐任务、逐用途的验收登记(docs/v2/11-judgment-providers.md §8.1)。 一条登记只证明"该任务在该用途下通过过验收",不是模型整体合格,也不能 由"服务能连通"或"别的任务通过"顶替(AC-38)。offline 通过与 runtime 启用必须分开登记,否则离线质检可以直接当生产准入。 """ task: JudgmentTask rubric_version: str = Field(min_length=1, max_length=ID_MAX) dataset_ref: str = Field(min_length=1, max_length=PATH_MAX) report_ref: str = Field(min_length=1, max_length=PATH_MAX) decision: Literal["passed", "failed", "unverified"] allowed_usage: JudgmentUsage # 独立评测来源:同模型自评或其生成标签不算独立评测(AC-41) independent: bool = False class JudgmentProfile(Contract): """专项提供方配置(T08)。只描述"能跑什么",不代表已经跑通。 `checkpoint_ref` 必须可复现:远程不可变版本不可得时填 "unresolved", 此时运行期不允许隐式换权重(AC-39)。 """ id: str = Field(min_length=1, max_length=ID_MAX) revision: int = Field(ge=1) provider: Literal["jev", "local"] engine: str = Field(min_length=1, max_length=ID_MAX) model_version: str = Field(min_length=1, max_length=ID_MAX) adapter_version: str = Field(min_length=1, max_length=ID_MAX) prompt_version: str = Field(min_length=1, max_length=ID_MAX) mapping_version: str = Field(min_length=1, max_length=ID_MAX) checkpoint_ref: str = Field(min_length=1, max_length=ID_MAX) tokenizer_revision: str = Field(min_length=1, max_length=ID_MAX) input_token_limit: int = Field(gt=0) option_token_limit: int = Field(gt=0) language_scope: str = Field(min_length=1, max_length=ID_MAX) deployment_mode: Literal["in_process", "internal_http", "external_http"] # 外发授权:未批准时 jev 一律拒绝发送,且不据此降低本地要求(AC-16/19) egress_approved: bool = False readiness: Literal["ready", "unready"] = "unready" task_validations: list[TaskValidation] = Field(default_factory=list, max_length=32) is_default: bool = False @model_validator(mode="after") def _check_profile(self) -> JudgmentProfile: if self.provider == "local" and self.deployment_mode == "external_http": raise ValueError("本地模型不得走外部出口;外发只属于 jev 路径") if self.provider == "jev" and self.deployment_mode != "external_http": raise ValueError("jev 属外部服务,部署模式必须是 external_http") seen = {(item.task, item.allowed_usage) for item in self.task_validations} if len(seen) != len(self.task_validations): raise ValueError("同一任务与用途只能登记一条验收结果,不得重复累计") return self