Files
ai-video-fullstack/backend/task_backend_schemas.py
T

110 lines
4.1 KiB
Python
Raw Normal View History

"""Public task backend configuration; runtime credentials stay server-side."""
from typing import Any, Literal
from urllib.parse import urlsplit
from pydantic import ConfigDict, Field, field_validator, model_validator
from schemas import CamelModel, ToolBinding
class TaskBackendConfig(CamelModel):
model_config = ConfigDict(extra="forbid")
instructions: str = Field(default="", max_length=16000)
timeout_seconds: int = Field(default=600, ge=30, le=7200)
max_concurrency: Literal[1] = 1
class TaskSkill(CamelModel):
model_config = ConfigDict(extra="forbid")
name: str = Field(min_length=1, max_length=128)
content: str = Field(min_length=1, max_length=16000)
@field_validator("name", "content")
@classmethod
def require_nonempty(cls, value: str) -> str:
if not value.strip():
raise ValueError("技能名称和正文不能为空")
return value.strip()
class ReasonerBackendConfig(TaskBackendConfig):
model_resource_id: str = Field(min_length=1, max_length=40)
reasoning_effort: Literal["provider_default", "low", "medium", "high"] = "provider_default"
tool_bindings: list[ToolBinding] = Field(default_factory=list, max_length=50)
skills: list[TaskSkill] = Field(default_factory=list, max_length=20)
@model_validator(mode="after")
def unique_bindings_and_skills(self):
ids = [binding.tool_id for binding in self.tool_bindings]
if len(ids) != len(set(ids)):
raise ValueError("Reasoner 工具绑定不能重复")
names = [skill.name for skill in self.skills]
if len(names) != len(set(names)):
raise ValueError("技能名称不能重复")
return self
class OpenCodeBackendConfig(TaskBackendConfig):
url: str = Field(min_length=1, max_length=2048)
directory: str = Field(min_length=1, max_length=2048)
username: str = Field(default="opencode", max_length=128)
agent: str = Field(default="", max_length=128)
model_provider_id: str = Field(default="", max_length=128)
model_id: str = Field(default="", max_length=256)
skills_note: str = Field(default="", max_length=4000)
@field_validator("url")
@classmethod
def validate_url(cls, value: str) -> str:
parsed = urlsplit(value)
if parsed.scheme not in {"http", "https"} or not parsed.hostname or parsed.username or parsed.password or parsed.query or parsed.fragment:
raise ValueError("任务服务须使用不含凭证、查询参数的 HTTP(S) 地址")
return value.rstrip("/")
@field_validator("directory")
@classmethod
def require_directory(cls, value: str) -> str:
if not value.strip():
raise ValueError("必须填写远端工作目录")
return value.strip()
@model_validator(mode="after")
def require_model_pair(self):
if bool(self.model_provider_id) != bool(self.model_id):
raise ValueError("远端模型的 provider ID 和 model ID 必须同时填写")
return self
class TaskBackendUpsert(CamelModel):
name: str = Field(min_length=1, max_length=128)
type: Literal["reasoner", "opencode"]
description: str = Field(default="", max_length=2048)
config: ReasonerBackendConfig | OpenCodeBackendConfig
secrets: dict[str, Any] = Field(default_factory=dict)
enabled: bool = True
@model_validator(mode="before")
@classmethod
def select_config(cls, value):
if isinstance(value, dict) and value.get("type") in {"reasoner", "opencode"}:
value = dict(value)
config_class = ReasonerBackendConfig if value["type"] == "reasoner" else OpenCodeBackendConfig
raw = value.get("config") or {}
if isinstance(raw, CamelModel):
raw = raw.model_dump()
value["config"] = config_class.model_validate(raw)
return value
@field_validator("name")
@classmethod
def require_name(cls, value: str) -> str:
if not value.strip():
raise ValueError("后端名称不能为空")
return value.strip()
class TaskBackendOut(TaskBackendUpsert):
id: str
updated_at: str | None = None