111 lines
3.2 KiB
Python
111 lines
3.2 KiB
Python
"""请求 / 响应模型。"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import re
|
||
|
||
from pydantic import BaseModel, Field, field_validator
|
||
|
||
from .config import PROFILE_RE
|
||
|
||
|
||
class ProviderIn(BaseModel):
|
||
id: str = Field(min_length=1, max_length=64)
|
||
name: str = Field(min_length=1, max_length=100)
|
||
adapter: str = Field(default="openai_compat")
|
||
base_url: str = Field(min_length=1)
|
||
api_key: str = ""
|
||
default_model: str = ""
|
||
enabled: bool = True
|
||
note: str = ""
|
||
|
||
@field_validator("id")
|
||
@classmethod
|
||
def check_id(cls, v: str) -> str:
|
||
v = v.strip()
|
||
if not PROFILE_RE.match(v):
|
||
raise ValueError("供应商 ID 只能包含字母数字下划线和短横线")
|
||
return v
|
||
|
||
@field_validator("adapter")
|
||
@classmethod
|
||
def check_adapter(cls, v: str) -> str:
|
||
if v not in ("openai_compat", "agnes"):
|
||
raise ValueError("不支持的适配器类型")
|
||
return v
|
||
|
||
@field_validator("base_url")
|
||
@classmethod
|
||
def check_base_url(cls, v: str) -> str:
|
||
v = v.strip().rstrip("/")
|
||
if not v.startswith("http://") and not v.startswith("https://"):
|
||
raise ValueError("Base URL 必须以 http:// 或 https:// 开头")
|
||
return v
|
||
|
||
|
||
class DiscoverIn(BaseModel):
|
||
"""手动填写 Base URL + Key 时的临时发现(供应商尚未保存)。"""
|
||
|
||
adapter: str = "openai_compat"
|
||
base_url: str = Field(min_length=1)
|
||
api_key: str = Field(min_length=1)
|
||
|
||
check_adapter = field_validator("adapter")(ProviderIn.check_adapter.__func__)
|
||
check_base_url = field_validator("base_url")(ProviderIn.check_base_url.__func__)
|
||
|
||
|
||
class ModelUpsertIn(BaseModel):
|
||
model_ids: list[str] = Field(default_factory=list)
|
||
enabled: bool = True
|
||
note: str = ""
|
||
|
||
|
||
class ModelPatchIn(BaseModel):
|
||
enabled: bool | None = None
|
||
note: str | None = None
|
||
|
||
|
||
class ModelBulkIn(BaseModel):
|
||
ids: list[str] = Field(default_factory=list)
|
||
enabled: bool
|
||
|
||
|
||
class GenerateIn(BaseModel):
|
||
provider_id: str = Field(min_length=1)
|
||
model: str = Field(min_length=1)
|
||
prompt: str = Field(min_length=1)
|
||
size: str = "1024x1024"
|
||
ratio: str = ""
|
||
n: int = Field(default=1, ge=1, le=4)
|
||
images: list[str] = Field(default_factory=list)
|
||
profile_name: str | None = None
|
||
|
||
@field_validator("size")
|
||
@classmethod
|
||
def check_size(cls, v: str) -> str:
|
||
v = v.strip().upper()
|
||
if v in ("1K", "2K", "3K", "4K"):
|
||
return v
|
||
match = re.fullmatch(r"([1-9]\d{0,4})[X×]([1-9]\d{0,4})", v)
|
||
if not match:
|
||
raise ValueError("尺寸必须为正整数 WxH 或 1K/2K/3K/4K")
|
||
return f"{int(match[1])}x{int(match[2])}"
|
||
|
||
@field_validator("profile_name")
|
||
@classmethod
|
||
def check_profile(cls, v: str | None) -> str | None:
|
||
if v is None or v == "":
|
||
return None
|
||
v = v.strip()
|
||
if not PROFILE_RE.match(v):
|
||
raise ValueError("profile_name 只能包含字母数字下划线和短横线")
|
||
return v
|
||
|
||
@field_validator("prompt")
|
||
@classmethod
|
||
def check_prompt(cls, v: str) -> str:
|
||
v = v.strip()
|
||
if not v:
|
||
raise ValueError("提示词不能为空")
|
||
return v
|