110 lines
4.1 KiB
Python
110 lines
4.1 KiB
Python
"""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
|