275 lines
11 KiB
Python
275 lines
11 KiB
Python
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。"
|
|
)
|