Files
Aether/src/models/api.py

654 lines
21 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
API端点请求/响应模型定义
"""
import re
from datetime import datetime
from typing import Any, Dict, List, Optional
from pydantic import BaseModel, ConfigDict, Field, field_validator
from ..core.enums import UserRole
# ========== 认证相关 ==========
class LoginRequest(BaseModel):
"""登录请求"""
email: str = Field(..., min_length=3, max_length=255, description="邮箱地址")
password: str = Field(..., min_length=1, max_length=128, description="密码")
@classmethod
@field_validator("email")
def validate_email(cls, v):
"""验证邮箱格式"""
email_pattern = r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"
if not re.match(email_pattern, v):
raise ValueError("邮箱格式无效")
return v.lower()
@classmethod
@field_validator("password")
def validate_password(cls, v):
"""验证密码不为空且去除前后空格"""
v = v.strip()
if not v:
raise ValueError("密码不能为空")
return v
class LoginResponse(BaseModel):
"""登录响应"""
access_token: str
refresh_token: str # 刷新令牌
token_type: str = "bearer"
expires_in: int = 86400 # Token有效期默认24小时
user_id: str
email: str
username: str
role: str
class RefreshTokenRequest(BaseModel):
"""刷新令牌请求"""
refresh_token: str = Field(..., description="刷新令牌")
class RefreshTokenResponse(BaseModel):
"""刷新令牌响应"""
access_token: str
refresh_token: str # 返回新的刷新令牌
token_type: str = "bearer"
expires_in: int = 86400 # Token有效期默认24小时
class RegisterRequest(BaseModel):
"""注册请求"""
email: str = Field(..., min_length=3, max_length=255, description="邮箱地址")
username: str = Field(..., min_length=2, max_length=50, description="用户名")
password: str = Field(..., min_length=6, max_length=128, description="密码")
@classmethod
@field_validator("email")
def validate_email(cls, v):
"""验证邮箱格式"""
email_pattern = r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"
if not re.match(email_pattern, v):
raise ValueError("邮箱格式无效")
return v.lower()
@classmethod
@field_validator("username")
def validate_username(cls, v):
"""验证用户名格式"""
v = v.strip()
if not v:
raise ValueError("用户名不能为空")
if not re.match(r"^[a-zA-Z0-9_-]+$", v):
raise ValueError("用户名只能包含字母、数字、下划线和短横线")
return v
@classmethod
@field_validator("password")
def validate_password(cls, v):
"""验证密码强度"""
if len(v) < 6:
raise ValueError("密码至少需要6个字符")
if not re.search(r"[A-Z]", v):
raise ValueError("密码必须包含至少一个大写字母")
if not re.search(r"[a-z]", v):
raise ValueError("密码必须包含至少一个小写字母")
if not re.search(r"\d", v):
raise ValueError("密码必须包含至少一个数字")
return v
class RegisterResponse(BaseModel):
"""注册响应"""
user_id: str
email: str
username: str
message: str
class LogoutResponse(BaseModel):
"""登出响应"""
message: str
success: bool
# ========== 用户管理 ==========
class CreateUserRequest(BaseModel):
"""创建用户请求"""
username: str = Field(..., min_length=2, max_length=50, description="用户名")
password: str = Field(..., min_length=6, max_length=128, description="密码")
email: str = Field(..., min_length=3, max_length=255, description="邮箱地址")
role: Optional[UserRole] = Field(UserRole.USER, description="用户角色")
quota_usd: Optional[float] = Field(default=10.0, description="USD配额null表示无限制")
@field_validator("quota_usd", mode="before")
@classmethod
def validate_quota_usd(cls, v):
"""验证配额值允许null表示无限制"""
if v is None:
return None
if isinstance(v, (int, float)) and v >= 0 and v <= 10000:
return float(v)
if isinstance(v, (int, float)):
raise ValueError("配额必须在 0-10000 范围内")
return v
@classmethod
@field_validator("email")
def validate_email(cls, v):
"""验证邮箱格式"""
v = v.strip()
if not v:
raise ValueError("邮箱不能为空")
email_pattern = r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"
if not re.match(email_pattern, v):
raise ValueError("邮箱格式无效")
return v.lower()
@classmethod
@field_validator("username")
def validate_username(cls, v):
"""验证用户名格式"""
v = v.strip()
if not v:
raise ValueError("用户名不能为空")
if not re.match(r"^[a-zA-Z0-9_-]+$", v):
raise ValueError("用户名只能包含字母、数字、下划线和短横线")
return v
@classmethod
@field_validator("password")
def validate_password(cls, v):
"""验证密码强度"""
if len(v) < 6:
raise ValueError("密码至少需要6个字符")
if not re.search(r"[A-Z]", v):
raise ValueError("密码必须包含至少一个大写字母")
if not re.search(r"[a-z]", v):
raise ValueError("密码必须包含至少一个小写字母")
if not re.search(r"\d", v):
raise ValueError("密码必须包含至少一个数字")
return v
class UpdateUserRequest(BaseModel):
"""更新用户请求"""
email: Optional[str] = None
username: Optional[str] = None
password: Optional[str] = None
role: Optional[UserRole] = None
allowed_providers: Optional[List[str]] = None # 允许使用的提供商 ID 列表
allowed_endpoints: Optional[List[str]] = None # 允许使用的端点 ID 列表
allowed_models: Optional[List[str]] = None # 允许使用的模型名称列表
quota_usd: Optional[float] = None
is_active: Optional[bool] = None
@field_validator("quota_usd", mode="before")
@classmethod
def validate_quota_usd(cls, v):
"""验证配额值允许null表示无限制"""
if v is None:
return None
if isinstance(v, (int, float)) and v >= 0 and v <= 10000:
return float(v)
if isinstance(v, (int, float)):
raise ValueError("配额必须在 0-10000 范围内")
return v
class CreateApiKeyRequest(BaseModel):
"""创建API密钥请求"""
name: Optional[str] = None
allowed_providers: Optional[List[str]] = None # 允许使用的提供商 ID 列表
allowed_endpoints: Optional[List[str]] = None # 允许使用的端点 ID 列表
allowed_api_formats: Optional[List[str]] = None # 允许使用的 API 格式列表
allowed_models: Optional[List[str]] = None # 允许使用的模型名称列表
rate_limit: Optional[int] = 100
expire_days: Optional[int] = None # None = 永不过期,数字 = 多少天后过期
initial_balance_usd: Optional[float] = Field(
None, description="初始余额USD仅用于独立KeyNone = 无限制"
)
is_standalone: bool = Field(False, description="是否为独立余额Key给非注册用户使用")
auto_delete_on_expiry: bool = Field(
False, description="过期后是否自动删除True=物理删除False=仅禁用)"
)
class UserResponse(BaseModel):
"""用户响应"""
id: str
email: str
username: str
role: UserRole
allowed_providers: Optional[List[str]] = None # 允许使用的提供商 ID 列表
allowed_endpoints: Optional[List[str]] = None # 允许使用的端点 ID 列表
allowed_models: Optional[List[str]] = None # 允许使用的模型名称列表
quota_usd: float
used_usd: float
is_active: bool
created_at: datetime
updated_at: datetime
last_login_at: Optional[datetime]
class ApiKeyResponse(BaseModel):
"""API密钥响应"""
id: str
user_id: str
key: Optional[str] = None # 仅在创建时返回完整密钥
key_display: Optional[str] = None # 脱敏后的密钥显示
name: Optional[str]
total_requests: int
total_tokens: int
total_cost_usd: float
allowed_providers: Optional[List[str]]
allowed_models: Optional[List[str]]
rate_limit: int
is_active: bool
expires_at: Optional[datetime] = None
balance_used_usd: float = 0.0
current_balance_usd: Optional[float] = None # NULL = 无限制
is_standalone: bool = False
force_capabilities: Optional[Dict[str, bool]] = None # 强制开启的能力
created_at: datetime
last_used_at: Optional[datetime]
# ========== 提供商管理 ==========
class ProviderCreate(BaseModel):
"""创建提供商请求
新架构说明:
- Provider 仅包含提供商的元数据和计费配置
- API格式、URL、认证等配置应在 ProviderEndpoint 中设置
- API密钥应在 ProviderAPIKey 中设置
"""
name: str = Field(..., min_length=1, max_length=100, description="提供商唯一标识")
display_name: str = Field(..., min_length=1, max_length=100, description="显示名称")
description: Optional[str] = Field(None, description="提供商描述")
website: Optional[str] = Field(None, max_length=500, description="主站网站")
# Provider 级别的配置
rate_limit: Optional[int] = Field(None, description="每分钟请求限制")
concurrent_limit: Optional[int] = Field(None, description="并发请求限制")
config: Optional[dict] = Field(None, description="额外配置")
is_active: bool = Field(False, description="是否启用默认false需要配置API密钥后才能启用")
class ProviderUpdate(BaseModel):
"""更新提供商请求"""
display_name: Optional[str] = Field(None, min_length=1, max_length=100)
description: Optional[str] = None
website: Optional[str] = Field(None, max_length=500)
api_format: Optional[str] = None
base_url: Optional[str] = None
headers: Optional[dict] = None
timeout: Optional[int] = Field(None, ge=1, le=600)
max_retries: Optional[int] = Field(None, ge=0, le=10)
priority: Optional[int] = None
weight: Optional[float] = Field(None, gt=0)
rate_limit: Optional[int] = None
concurrent_limit: Optional[int] = None
config: Optional[dict] = None
is_active: Optional[bool] = None
class ProviderResponse(BaseModel):
"""提供商响应"""
id: str
name: str
display_name: str
description: Optional[str]
website: Optional[str]
api_format: str
base_url: str
headers: Optional[dict]
timeout: int
max_retries: int
priority: int
weight: float
rate_limit: Optional[int]
concurrent_limit: Optional[int]
config: Optional[dict]
is_active: bool
created_at: datetime
updated_at: datetime
models_count: int = 0
active_models_count: int = 0
api_keys_count: int = 0
model_config = ConfigDict(from_attributes=True)
# ========== 模型管理 ==========
class ModelCreate(BaseModel):
"""创建模型请求 - 价格和能力字段可选,为空时使用 GlobalModel 默认值"""
provider_model_name: str = Field(
..., min_length=1, max_length=200, description="Provider 侧的主模型名称"
)
provider_model_mappings: Optional[List[dict]] = Field(
None,
description="模型名称映射列表,格式: [{'name': 'alias1', 'priority': 1}, ...]",
)
global_model_id: str = Field(..., description="关联的 GlobalModel ID必填")
# 按次计费配置 - 可选,为空时使用 GlobalModel 默认值
price_per_request: Optional[float] = Field(
None, ge=0, description="每次请求固定费用,为空使用默认值"
)
# 阶梯计费配置 - 可选,为空时使用 GlobalModel 默认值
tiered_pricing: Optional[dict] = Field(
None, description="阶梯计费配置,为空使用 GlobalModel 默认值"
)
# 能力配置 - 可选,为空时使用 GlobalModel 默认值
supports_vision: Optional[bool] = Field(None, description="是否支持图像输入,为空使用默认值")
supports_function_calling: Optional[bool] = Field(
None, description="是否支持函数调用,为空使用默认值"
)
supports_streaming: Optional[bool] = Field(None, description="是否支持流式输出,为空使用默认值")
supports_extended_thinking: Optional[bool] = Field(
None, description="是否支持扩展思考,为空使用默认值"
)
is_active: bool = Field(True, description="是否启用")
config: Optional[dict] = Field(None, description="额外配置")
class ModelUpdate(BaseModel):
"""更新模型请求"""
provider_model_name: Optional[str] = Field(None, min_length=1, max_length=200)
provider_model_mappings: Optional[List[dict]] = Field(
None,
description="模型名称映射列表,格式: [{'name': 'alias1', 'priority': 1}, ...]",
)
global_model_id: Optional[str] = None
# 按次计费配置
price_per_request: Optional[float] = Field(None, ge=0, description="每次请求固定费用")
# 阶梯计费配置
tiered_pricing: Optional[dict] = Field(None, description="阶梯计费配置")
supports_vision: Optional[bool] = None
supports_function_calling: Optional[bool] = None
supports_streaming: Optional[bool] = None
supports_extended_thinking: Optional[bool] = None
is_active: Optional[bool] = None
is_available: Optional[bool] = None
config: Optional[dict] = None
class ModelResponse(BaseModel):
"""模型响应 - 包含 Model 配置和关联的 GlobalModel 信息
注意:价格和能力字段返回的是有效值(优先使用 Model 配置,否则使用 GlobalModel 默认值)
"""
id: str
provider_id: str
global_model_id: Optional[str]
provider_model_name: str
provider_model_mappings: Optional[List[dict]] = None
# 按次计费配置
price_per_request: Optional[float] = None
# 阶梯计费配置
tiered_pricing: Optional[dict] = None
# Provider 能力配置 - 可选,为空表示使用 GlobalModel 默认值
supports_vision: Optional[bool]
supports_function_calling: Optional[bool]
supports_streaming: Optional[bool]
supports_extended_thinking: Optional[bool]
supports_image_generation: Optional[bool]
# 有效值(合并 Model 配置和 GlobalModel 默认值后的结果)
effective_tiered_pricing: Optional[dict] = None
effective_input_price: Optional[float] = None
effective_output_price: Optional[float] = None
effective_price_per_request: Optional[float] = None
effective_supports_vision: Optional[bool] = None
effective_supports_function_calling: Optional[bool] = None
effective_supports_streaming: Optional[bool] = None
effective_supports_extended_thinking: Optional[bool] = None
effective_supports_image_generation: Optional[bool] = None
# 状态
is_active: bool
is_available: bool
# 时间戳
created_at: datetime
updated_at: datetime
# 关联的 GlobalModel 信息(如果有)
global_model_name: Optional[str] = None
global_model_display_name: Optional[str] = None
model_config = ConfigDict(from_attributes=True)
class ModelDetailResponse(BaseModel):
"""模型详细响应 - 包含所有字段(用于需要完整信息的场景)"""
id: str
provider_id: str
name: str
display_name: str
description: Optional[str]
icon_url: Optional[str]
tags: Optional[List[str]]
input_price_per_1m: float
output_price_per_1m: float
cache_creation_price_per_1m: Optional[float]
cache_read_price_per_1m: Optional[float]
supports_vision: bool
supports_function_calling: bool
supports_streaming: bool
is_active: bool
is_available: bool
config: Optional[dict]
created_at: datetime
updated_at: datetime
model_config = ConfigDict(from_attributes=True)
# ========== 系统设置 ==========
class SystemSettingsRequest(BaseModel):
"""系统设置请求"""
default_provider: Optional[str] = None
default_model: Optional[str] = None
enable_usage_tracking: Optional[bool] = None
class SystemSettingsResponse(BaseModel):
"""系统设置响应"""
default_provider: Optional[str]
default_model: Optional[str]
enable_usage_tracking: bool
# ========== 使用统计 ==========
class UsageStatsResponse(BaseModel):
"""使用统计响应"""
total_requests: int
total_tokens: int
total_cost_usd: float
daily_requests: int
daily_tokens: int
daily_cost_usd: float
model_usage: Dict[str, Dict[str, Any]]
provider_usage: Dict[str, Dict[str, Any]]
# ========== 公开API响应模型 ==========
class PublicProviderResponse(BaseModel):
"""公开的提供商信息响应"""
id: str
name: str
display_name: str
description: Optional[str]
website: Optional[str]
is_active: bool
provider_priority: int # 提供商优先级(数字越小越优先)
# 统计信息
models_count: int
active_models_count: int
endpoints_count: int # 端点总数
active_endpoints_count: int # 活跃端点数
class PublicModelResponse(BaseModel):
"""公开的模型信息响应"""
id: str
provider_id: str
provider_name: str
provider_display_name: str
name: str
display_name: str
description: Optional[str] = None
tags: Optional[List[str]] = None
icon_url: Optional[str] = None
# 价格信息
input_price_per_1m: Optional[float] = None
output_price_per_1m: Optional[float] = None
cache_creation_price_per_1m: Optional[float] = None
cache_read_price_per_1m: Optional[float] = None
# 功能支持
supports_vision: Optional[bool] = None
supports_function_calling: Optional[bool] = None
supports_streaming: Optional[bool] = None
is_active: bool = True
class ProviderStatsResponse(BaseModel):
"""提供商统计信息响应"""
total_providers: int
active_providers: int
total_models: int
active_models: int
supported_formats: List[str]
class PublicGlobalModelResponse(BaseModel):
"""公开的 GlobalModel 信息响应(用户可见)"""
id: str
name: str
display_name: Optional[str] = None
is_active: bool = True
# 按次计费配置
default_price_per_request: Optional[float] = None
# 阶梯计费配置
default_tiered_pricing: Optional[dict] = None
# Key 能力配置
supported_capabilities: Optional[List[str]] = None
# 模型配置JSON
config: Optional[dict] = None
class PublicGlobalModelListResponse(BaseModel):
"""公开的 GlobalModel 列表响应"""
models: List[PublicGlobalModelResponse]
total: int
# ========== 个人中心相关模型 ==========
class UpdateProfileRequest(BaseModel):
"""更新个人信息请求"""
email: Optional[str] = None
username: Optional[str] = None
class UpdatePreferencesRequest(BaseModel):
"""更新偏好设置请求"""
avatar_url: Optional[str] = None
bio: Optional[str] = None
default_provider_id: Optional[int] = None
theme: Optional[str] = None
language: Optional[str] = None
timezone: Optional[str] = None
email_notifications: Optional[bool] = None
usage_alerts: Optional[bool] = None
announcement_notifications: Optional[bool] = None
class ChangePasswordRequest(BaseModel):
"""修改密码请求"""
old_password: str
new_password: str
class CreateMyApiKeyRequest(BaseModel):
"""创建我的API密钥请求"""
name: str
class ProviderConfig(BaseModel):
"""提供商配置"""
provider_id: str = Field(..., description="提供商ID")
priority: int = Field(100, description="优先级(越高越优先)")
weight: float = Field(1.0, description="负载均衡权重")
enabled: bool = Field(True, description="是否启用")
class UpdateApiKeyProvidersRequest(BaseModel):
"""更新API密钥可用提供商请求"""
allowed_providers: Optional[List[ProviderConfig]] = None # 提供商配置列表
# ========== 公告相关模型 ==========
class CreateAnnouncementRequest(BaseModel):
"""创建公告请求"""
title: str
content: str # 支持Markdown
type: str = "info" # info, warning, maintenance, important
priority: int = 0
is_pinned: bool = False
start_time: Optional[datetime] = None
end_time: Optional[datetime] = None
class UpdateAnnouncementRequest(BaseModel):
"""更新公告请求"""
title: Optional[str] = None
content: Optional[str] = None
type: Optional[str] = None
priority: Optional[int] = None
is_active: Optional[bool] = None
is_pinned: Optional[bool] = None
start_time: Optional[datetime] = None
end_time: Optional[datetime] = None