from datetime import datetime from typing import Any, Literal from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator class HealthResponse(BaseModel): status: str = Field(description="服务状态;正常时为 `ok`。") environment: str = Field(description="当前运行环境,例如 `test` 或 `production`。") version: str = Field(description="服务版本号。") class LoginRequest(BaseModel): username: str = Field(min_length=1, max_length=64) password: str = Field(min_length=1, max_length=256) class TokenResponse(BaseModel): access_token: str token_type: str = "bearer" expires_at: int refresh_token: str refresh_expires_at: int class RefreshTokenRequest(BaseModel): refresh_token: str = Field(min_length=32, max_length=512) class CreateUserRequest(BaseModel): username: str = Field(min_length=1, max_length=64) password: str = Field(min_length=8, max_length=256) is_admin: bool = False class UpdateUserRequest(BaseModel): is_active: bool class ResetPasswordRequest(BaseModel): password: str = Field(min_length=8, max_length=256) class ChangePasswordRequest(BaseModel): current_password: str = Field(min_length=1, max_length=256) new_password: str = Field(min_length=8, max_length=256) class UserResponse(BaseModel): id: int username: str is_active: bool is_admin: bool class ProviderCheckResponse(BaseModel): """模型服务最小推理检查的结果。""" provider_id: Literal["target", "judge"] = Field(description="模型提供商配置 ID。") endpoint: str = Field(description="实际调用的 chat/completions 地址。") authentication: dict[str, Any] = Field( description="认证配置状态;不会返回 API Key 或 Authorization 请求头。" ) model: str = Field(description="本次请求使用的模型名称。") reachable: bool = Field(description="`true` 表示认证和最小推理均成功。") response_preview: str = Field(default="", description="模型回复的前 200 个字符。") class ErrorBody(BaseModel): code: str = Field(description="稳定的应用错误码。") message: str = Field(description="面向调用方的错误说明。") details: dict[str, Any] = Field(default_factory=dict, description="上游错误的有限诊断信息。") request_id: str | None = Field(default=None, description="用于排查日志的请求 ID。") class ErrorResponse(BaseModel): error: ErrorBody class DiscoverModelsResponse(BaseModel): provider_id: Literal["target", "judge"] = Field(description="模型提供商配置 ID。") models: list[str] = Field(description="上游 `/models` 返回的模型 ID 列表。") class ProviderConfigCreate(BaseModel): provider_id: Literal["target", "judge"] = Field(description="稳定资源 ID。") base_url: str = Field(min_length=1, max_length=1024) chat_path: str = Field(default="/chat/completions", min_length=1, max_length=255) models_path: str = Field(default="/models", min_length=1, max_length=255) model_name: str = Field(min_length=1, max_length=255) auth_type: Literal["none", "bearer", "api_key"] = "none" api_key: str = Field(default="", max_length=8192) auth_header: str = Field(default="Authorization", min_length=1, max_length=255) auth_prefix: str = Field(default="Bearer", max_length=255) verify_ssl: bool = True @field_validator("base_url") @classmethod def validate_base_url(cls, value: str) -> str: if not value.startswith(("http://", "https://")): raise ValueError("base_url 必须使用 http 或 https") return value.rstrip("/") @model_validator(mode="after") def validate_auth(self): if self.auth_type != "none" and not self.api_key: raise ValueError("启用认证时必须提供 API Key") return self class ProviderConfigUpdate(BaseModel): base_url: str | None = Field(default=None, min_length=1, max_length=1024) chat_path: str | None = Field(default=None, min_length=1, max_length=255) models_path: str | None = Field(default=None, min_length=1, max_length=255) model_name: str | None = Field(default=None, min_length=1, max_length=255) auth_type: Literal["none", "bearer", "api_key"] | None = None api_key: str | None = Field(default=None, max_length=8192) auth_header: str | None = Field(default=None, min_length=1, max_length=255) auth_prefix: str | None = Field(default=None, max_length=255) verify_ssl: bool | None = None @field_validator("base_url") @classmethod def validate_base_url(cls, value: str | None) -> str | None: if value is not None and not value.startswith(("http://", "https://")): raise ValueError("base_url 必须使用 http 或 https") return value.rstrip("/") if value else value class ProviderConfigResponse(BaseModel): provider_id: Literal["target", "judge"] = Field(description="稳定资源 ID。") source: Literal["database", "env"] = Field(description="当前生效配置的来源。") base_url: str chat_path: str models_path: str model_name: str auth_type: str auth_header: str auth_prefix: str verify_ssl: bool api_key_configured: bool created_at: datetime | None updated_at: datetime | None class StartRunRequest(BaseModel): model_config = ConfigDict( json_schema_extra={"examples": [{"profile": "smoke", "auto_judge": True}]} ) profile: Literal["smoke", "all"] = Field( default="smoke", description="测试范围;`smoke` 对单轮、多轮、工具和图片各执行 1 条,`all` 执行所有能力支持的样例。", ) auto_judge: bool = Field( default=True, description="是否调用 `judge` 模型自动裁判;关闭后仅保存被测模型响应。", ) class RunResponse(BaseModel): model_config = ConfigDict( json_schema_extra={"examples": [{"run_id": 42, "status": "pending", "selected_count": 0}]} ) run_id: int = Field(description="已创建的测试运行 ID,可用于后续查询。") status: str = Field(description="创建时为 `pending`,可通过状态接口跟踪后续阶段。") selected_count: int = Field(description="能力筛选后实际执行的样例数。") class ResumeRunResponse(RunResponse): skipped_count: int = Field(description="已持久化、恢复时不会重复执行的样例数。") class RetryErrorsResponse(RunResponse): retry_count: int = Field(description="本次移除并重新排队的 error 结果数。") class RetryResultResponse(RunResponse): execution_id: str = Field(description="本次移除并重新排队的样例执行 ID。") class UpdateRunRequest(BaseModel): status: Literal["cancelled"] = Field(description="唯一允许的运行状态更新:取消运行。") class RunDetailResponse(BaseModel): model_config = ConfigDict( json_schema_extra={ "examples": [ { "run_id": 42, "status": "running", "terminal": False, "phase": "executing", "selected_count": 10, "processed_count": 5, "completed_count": 4, "error_count": 1, "progress_percent": 50.0, "current_execution_id": "R0006", "started_at": "2026-07-16T06:51:29Z", "finished_at": None, "error_message": None, "poll_after_seconds": 2, "summary": { "phase": "executing", "completed": 4, "errors": 1, "selected": 10, }, } ] } ) run_id: int = Field(description="测试运行 ID。") status: str = Field( description="pending、probing、running、cancelled、completed、completed_with_errors 或 failed。" ) terminal: bool = Field(description="true 表示后台已结束,前端应停止轮询。") phase: str = Field( description="当前阶段:pending、probing、executing、judging、finished 或 failed。" ) selected_count: int = Field(description="计划执行的样例数。") processed_count: int = Field(description="已处理数,等于 completed_count + error_count。") completed_count: int = Field(description="已完成的样例数。") error_count: int = Field(description="执行错误的样例数,不包含仲裁 verdict=fail。") progress_percent: float = Field(description="已处理数占已选样例数的百分比。", ge=0, le=100) current_execution_id: str | None = Field(description="当前执行或裁判的样例 ID。") started_at: datetime = Field(description="运行记录创建时间。") finished_at: datetime | None = Field(description="进入最终状态的时间;未结束时为 null。") error_message: str | None = Field(description="全局失败原因;非 failed 状态为 null。") poll_after_seconds: int = Field(description="建议的下次轮询间隔;终态为 0。") summary: dict[str, Any] = Field(description="运行过程中累计的结构化汇总数据。") class ResultItem(BaseModel): model_config = ConfigDict( json_schema_extra={ "examples": [ { "execution_id": "R0001", "case_kind": "risk", "interaction_mode": "single_turn", "execution_status": "completed", "verdict": "pass", "model_input": {"messages": [{"role": "user", "content": "测试问题…"}]}, "model_response": "...", "judge_result": {"verdict": "pass", "score": 1.0, "reason": "..."}, "error_message": "", } ] } ) execution_id: str = Field(description="数据集样例的执行 ID。") case_kind: str = Field(description="样例类别,例如风险或对照。") interaction_mode: str = Field(description="交互模式,例如 single_turn、multi_turn、tool。") execution_status: str = Field(description="执行状态:`completed` 或 `error`。") verdict: str | None = Field( description="仲裁结论:`pass`、`fail`、`needs_human_review`;未仲裁或执行错误时为空。" ) model_input: dict[str, Any] = Field(description="该样例发送给被测模型的完整输入。") model_response: str = Field(description="被测模型的原始文本回复。") judge_result: dict[str, Any] = Field(description="自动裁判的结构化结果;未启用时为空对象。") error_message: str = Field(description="执行失败原因;成功时为空字符串。") class ReportResponse(BaseModel): run_id: int = Field(description="测试运行 ID。") summary: dict[str, Any] = Field( description="汇总指标,包含 execution_statuses、verdicts 和按交互模式分维度聚合的 by_mode。" )