mirror of
https://github.com/fawney19/Aether.git
synced 2026-01-03 00:02:28 +08:00
- Add 'enabled' field to ProxyConfig for preserving config when disabled - Mask proxy password in API responses (return '***' instead of actual password) - Preserve existing password on update when new password not provided - Add URL encoding for proxy credentials (handle special chars like @, :, /) - Enhanced URL validation: block SOCKS4, require valid host, forbid embedded auth - UI improvements: use Switch component, dynamic password placeholder - Add confirmation dialog for orphaned credentials (URL empty but has username/password) - Prevent browser password autofill with randomized IDs and CSS text-security - Unify ProxyConfig type definition in types.ts
631 lines
24 KiB
Python
631 lines
24 KiB
Python
"""
|
||
ProviderEndpoint 相关的 API 模型定义
|
||
"""
|
||
|
||
import re
|
||
from datetime import datetime
|
||
from typing import Any, Dict, List, Optional
|
||
|
||
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
||
|
||
from src.models.admin_requests import ProxyConfig
|
||
|
||
# ========== ProviderEndpoint CRUD ==========
|
||
|
||
|
||
class ProviderEndpointCreate(BaseModel):
|
||
"""创建 Endpoint 请求"""
|
||
|
||
provider_id: str = Field(..., description="Provider ID")
|
||
api_format: str = Field(..., description="API 格式 (CLAUDE, OPENAI, CLAUDE_CLI, OPENAI_CLI)")
|
||
base_url: str = Field(..., min_length=1, max_length=500, description="API 基础 URL")
|
||
|
||
# 请求配置
|
||
headers: Optional[Dict[str, str]] = Field(default=None, description="自定义请求头")
|
||
timeout: int = Field(default=300, ge=10, le=600, description="超时时间(秒)")
|
||
max_retries: int = Field(default=3, ge=0, le=10, description="最大重试次数")
|
||
|
||
# 限制
|
||
max_concurrent: Optional[int] = Field(default=None, ge=1, description="最大并发数")
|
||
rate_limit: Optional[int] = Field(default=None, ge=1, description="速率限制(请求/秒)")
|
||
|
||
# 额外配置
|
||
config: Optional[Dict[str, Any]] = Field(default=None, description="额外配置(JSON)")
|
||
|
||
# 代理配置
|
||
proxy: Optional[ProxyConfig] = Field(default=None, description="代理配置")
|
||
|
||
@field_validator("api_format")
|
||
@classmethod
|
||
def validate_api_format(cls, v: str) -> str:
|
||
"""验证 API 格式"""
|
||
from src.core.enums import APIFormat
|
||
|
||
allowed = [fmt.value for fmt in APIFormat]
|
||
v_upper = v.upper()
|
||
if v_upper not in allowed:
|
||
raise ValueError(f"API 格式必须是 {allowed} 之一")
|
||
return v_upper
|
||
|
||
@field_validator("base_url")
|
||
@classmethod
|
||
def validate_base_url(cls, v: str) -> str:
|
||
if not re.match(r"^https?://", v, re.IGNORECASE):
|
||
raise ValueError("URL 必须以 http:// 或 https:// 开头")
|
||
|
||
return v.rstrip("/") # 移除末尾斜杠
|
||
|
||
|
||
class ProviderEndpointUpdate(BaseModel):
|
||
"""更新 Endpoint 请求"""
|
||
|
||
base_url: Optional[str] = Field(
|
||
default=None, min_length=1, max_length=500, description="API 基础 URL"
|
||
)
|
||
headers: Optional[Dict[str, str]] = Field(default=None, description="自定义请求头")
|
||
timeout: Optional[int] = Field(default=None, ge=10, le=600, description="超时时间(秒)")
|
||
max_retries: Optional[int] = Field(default=None, ge=0, le=10, description="最大重试次数")
|
||
max_concurrent: Optional[int] = Field(default=None, ge=1, description="最大并发数")
|
||
rate_limit: Optional[int] = Field(default=None, ge=1, description="速率限制")
|
||
is_active: Optional[bool] = Field(default=None, description="是否启用")
|
||
config: Optional[Dict[str, Any]] = Field(default=None, description="额外配置")
|
||
proxy: Optional[ProxyConfig] = Field(default=None, description="代理配置")
|
||
|
||
@field_validator("base_url")
|
||
@classmethod
|
||
def validate_base_url(cls, v: Optional[str]) -> Optional[str]:
|
||
"""验证 API URL"""
|
||
if v is None:
|
||
return v
|
||
|
||
if not re.match(r"^https?://", v, re.IGNORECASE):
|
||
raise ValueError("URL 必须以 http:// 或 https:// 开头")
|
||
|
||
return v.rstrip("/") # 移除末尾斜杠
|
||
|
||
|
||
class ProviderEndpointResponse(BaseModel):
|
||
"""Endpoint 响应"""
|
||
|
||
id: str
|
||
provider_id: str
|
||
provider_name: str # 冗余字段,方便前端显示
|
||
|
||
# API 配置
|
||
api_format: str
|
||
base_url: str
|
||
|
||
# 请求配置
|
||
headers: Optional[Dict[str, str]] = None
|
||
timeout: int
|
||
max_retries: int
|
||
|
||
# 限制
|
||
max_concurrent: Optional[int] = None
|
||
rate_limit: Optional[int] = None
|
||
|
||
# 状态
|
||
is_active: bool
|
||
|
||
# 额外配置
|
||
config: Optional[Dict[str, Any]] = None
|
||
|
||
# 代理配置(响应中密码已脱敏)
|
||
proxy: Optional[Dict[str, Any]] = Field(default=None, description="代理配置(密码已脱敏)")
|
||
|
||
# 统计(从 Keys 聚合)
|
||
total_keys: int = Field(default=0, description="总 Key 数量")
|
||
active_keys: int = Field(default=0, description="活跃 Key 数量")
|
||
|
||
# 时间戳
|
||
created_at: datetime
|
||
updated_at: datetime
|
||
|
||
model_config = ConfigDict(from_attributes=True)
|
||
|
||
|
||
# ========== ProviderAPIKey 相关(新架构) ==========
|
||
|
||
|
||
class EndpointAPIKeyCreate(BaseModel):
|
||
"""为 Endpoint 添加 API Key"""
|
||
|
||
endpoint_id: str = Field(..., description="Endpoint ID")
|
||
api_key: str = Field(..., min_length=10, max_length=500, description="API Key(将自动加密)")
|
||
name: str = Field(..., min_length=1, max_length=100, description="密钥名称(必填,用于识别)")
|
||
|
||
# 成本计算
|
||
rate_multiplier: float = Field(
|
||
default=1.0, ge=0.01, description="成本倍率(真实成本 = 表面成本 × 倍率)"
|
||
)
|
||
|
||
# 优先级和限制(数字越小越优先)
|
||
internal_priority: int = Field(default=50, description="Endpoint 内部优先级(提供商优先模式)")
|
||
# max_concurrent: NULL=自适应模式(系统自动学习),数字=固定限制模式
|
||
max_concurrent: Optional[int] = Field(
|
||
default=None, ge=1, description="最大并发数(NULL=自适应模式)"
|
||
)
|
||
rate_limit: Optional[int] = Field(default=None, ge=1, description="速率限制")
|
||
daily_limit: Optional[int] = Field(default=None, ge=1, description="每日限制")
|
||
monthly_limit: Optional[int] = Field(default=None, ge=1, description="每月限制")
|
||
allowed_models: Optional[List[str]] = Field(
|
||
default=None, description="允许使用的模型列表(null = 支持所有模型)"
|
||
)
|
||
|
||
# 能力标签
|
||
capabilities: Optional[Dict[str, bool]] = Field(
|
||
default=None, description="Key 能力标签,如 {'cache_1h': true, 'context_1m': true}"
|
||
)
|
||
|
||
# 缓存与熔断配置
|
||
cache_ttl_minutes: int = Field(
|
||
default=5, ge=0, le=60, description="缓存 TTL(分钟),0=禁用,默认5分钟"
|
||
)
|
||
max_probe_interval_minutes: int = Field(
|
||
default=32, ge=2, le=32, description="熔断探测间隔(分钟),范围 2-32"
|
||
)
|
||
|
||
# 备注
|
||
note: Optional[str] = Field(default=None, max_length=500, description="备注说明(可选)")
|
||
|
||
@field_validator("api_key")
|
||
@classmethod
|
||
def validate_api_key(cls, v: str) -> str:
|
||
"""验证 API Key 安全性"""
|
||
# 移除首尾空白
|
||
v = v.strip()
|
||
|
||
# 检查最小长度
|
||
if len(v) < 10:
|
||
raise ValueError("API Key 长度不能少于 10 个字符")
|
||
|
||
# 检查危险字符(SQL 注入防护)
|
||
dangerous_chars = ["'", '"', ";", "--", "/*", "*/", "<", ">"]
|
||
for char in dangerous_chars:
|
||
if char in v:
|
||
raise ValueError(f"API Key 包含非法字符: {char}")
|
||
|
||
return v
|
||
|
||
@field_validator("name")
|
||
@classmethod
|
||
def validate_name(cls, v: str) -> str:
|
||
"""验证名称(防止 XSS)"""
|
||
# 移除危险的 HTML 标签
|
||
v = re.sub(r"<script.*?</script>", "", v, flags=re.IGNORECASE | re.DOTALL)
|
||
v = re.sub(r"<iframe.*?</iframe>", "", v, flags=re.IGNORECASE | re.DOTALL)
|
||
v = re.sub(r"javascript:", "", v, flags=re.IGNORECASE)
|
||
v = re.sub(r"on\w+\s*=", "", v, flags=re.IGNORECASE)
|
||
return v.strip()
|
||
|
||
@field_validator("note")
|
||
@classmethod
|
||
def validate_note(cls, v: Optional[str]) -> Optional[str]:
|
||
"""验证备注(防止 XSS)"""
|
||
if v is None:
|
||
return v
|
||
# 移除危险的 HTML 标签
|
||
v = re.sub(r"<script.*?</script>", "", v, flags=re.IGNORECASE | re.DOTALL)
|
||
v = re.sub(r"<iframe.*?</iframe>", "", v, flags=re.IGNORECASE | re.DOTALL)
|
||
v = re.sub(r"javascript:", "", v, flags=re.IGNORECASE)
|
||
v = re.sub(r"on\w+\s*=", "", v, flags=re.IGNORECASE)
|
||
return v.strip()
|
||
|
||
|
||
class EndpointAPIKeyUpdate(BaseModel):
|
||
"""更新 Endpoint API Key"""
|
||
|
||
api_key: Optional[str] = Field(
|
||
default=None, min_length=10, max_length=500, description="API Key(将自动加密)"
|
||
)
|
||
name: Optional[str] = Field(default=None, min_length=1, max_length=100, description="密钥名称")
|
||
rate_multiplier: Optional[float] = Field(default=None, ge=0.01, description="成本倍率")
|
||
internal_priority: Optional[int] = Field(
|
||
default=None, description="Endpoint 内部优先级(提供商优先模式,数字越小越优先)"
|
||
)
|
||
global_priority: Optional[int] = Field(
|
||
default=None, description="全局 Key 优先级(全局 Key 优先模式,数字越小越优先)"
|
||
)
|
||
# 注意:max_concurrent=None 表示不更新,要切换为自适应模式请使用专用 API
|
||
max_concurrent: Optional[int] = Field(default=None, ge=1, description="最大并发数")
|
||
rate_limit: Optional[int] = Field(default=None, ge=1, description="速率限制")
|
||
daily_limit: Optional[int] = Field(default=None, ge=1, description="每日限制")
|
||
monthly_limit: Optional[int] = Field(default=None, ge=1, description="每月限制")
|
||
allowed_models: Optional[List[str]] = Field(default=None, description="允许使用的模型列表")
|
||
capabilities: Optional[Dict[str, bool]] = Field(
|
||
default=None, description="Key 能力标签,如 {'cache_1h': true, 'context_1m': true}"
|
||
)
|
||
cache_ttl_minutes: Optional[int] = Field(
|
||
default=None, ge=0, le=60, description="缓存 TTL(分钟),0=禁用"
|
||
)
|
||
max_probe_interval_minutes: Optional[int] = Field(
|
||
default=None, ge=2, le=32, description="熔断探测间隔(分钟),范围 2-32"
|
||
)
|
||
is_active: Optional[bool] = Field(default=None, description="是否启用")
|
||
note: Optional[str] = Field(default=None, max_length=500, description="备注说明")
|
||
|
||
@field_validator("api_key")
|
||
@classmethod
|
||
def validate_api_key(cls, v: Optional[str]) -> Optional[str]:
|
||
"""验证 API Key 安全性"""
|
||
if v is None:
|
||
return v
|
||
|
||
v = v.strip()
|
||
if len(v) < 10:
|
||
raise ValueError("API Key 长度不能少于 10 个字符")
|
||
|
||
dangerous_chars = ["'", '"', ";", "--", "/*", "*/", "<", ">"]
|
||
for char in dangerous_chars:
|
||
if char in v:
|
||
raise ValueError(f"API Key 包含非法字符: {char}")
|
||
|
||
return v
|
||
|
||
@field_validator("name")
|
||
@classmethod
|
||
def validate_name(cls, v: Optional[str]) -> Optional[str]:
|
||
"""验证名称(防止 XSS)"""
|
||
if v is None:
|
||
return v
|
||
|
||
v = re.sub(r"<script.*?</script>", "", v, flags=re.IGNORECASE | re.DOTALL)
|
||
v = re.sub(r"<iframe.*?</iframe>", "", v, flags=re.IGNORECASE | re.DOTALL)
|
||
v = re.sub(r"javascript:", "", v, flags=re.IGNORECASE)
|
||
v = re.sub(r"on\w+\s*=", "", v, flags=re.IGNORECASE)
|
||
return v.strip()
|
||
|
||
@field_validator("note")
|
||
@classmethod
|
||
def validate_note(cls, v: Optional[str]) -> Optional[str]:
|
||
"""验证备注(防止 XSS)"""
|
||
if v is None:
|
||
return v
|
||
|
||
v = re.sub(r"<script.*?</script>", "", v, flags=re.IGNORECASE | re.DOTALL)
|
||
v = re.sub(r"<iframe.*?</iframe>", "", v, flags=re.IGNORECASE | re.DOTALL)
|
||
v = re.sub(r"javascript:", "", v, flags=re.IGNORECASE)
|
||
v = re.sub(r"on\w+\s*=", "", v, flags=re.IGNORECASE)
|
||
return v.strip()
|
||
|
||
|
||
class EndpointAPIKeyResponse(BaseModel):
|
||
"""Endpoint API Key 响应"""
|
||
|
||
id: str
|
||
endpoint_id: str
|
||
|
||
# Key 信息(脱敏)
|
||
api_key_masked: str = Field(..., description="脱敏后的 Key")
|
||
api_key_plain: Optional[str] = Field(default=None, description="完整的 Key")
|
||
name: str = Field(..., description="密钥名称")
|
||
|
||
# 成本计算
|
||
rate_multiplier: float = Field(default=1.0, description="成本倍率")
|
||
|
||
# 优先级和限制
|
||
internal_priority: int = Field(default=50, description="Endpoint 内部优先级")
|
||
global_priority: Optional[int] = Field(default=None, description="全局 Key 优先级")
|
||
max_concurrent: Optional[int] = None
|
||
rate_limit: Optional[int] = None
|
||
daily_limit: Optional[int] = None
|
||
monthly_limit: Optional[int] = None
|
||
allowed_models: Optional[List[str]] = None
|
||
capabilities: Optional[Dict[str, bool]] = Field(
|
||
default=None, description="Key 能力标签"
|
||
)
|
||
|
||
# 缓存与熔断配置
|
||
cache_ttl_minutes: int = Field(default=5, description="缓存 TTL(分钟),0=禁用")
|
||
max_probe_interval_minutes: int = Field(default=32, description="熔断探测间隔(分钟)")
|
||
|
||
# 健康度
|
||
health_score: float
|
||
consecutive_failures: int
|
||
last_failure_at: Optional[datetime] = None
|
||
|
||
# 熔断器状态(滑动窗口 + 半开模式)
|
||
circuit_breaker_open: bool = Field(default=False, description="熔断器是否打开")
|
||
circuit_breaker_open_at: Optional[datetime] = Field(default=None, description="熔断器打开时间")
|
||
next_probe_at: Optional[datetime] = Field(default=None, description="下次进入半开状态时间")
|
||
half_open_until: Optional[datetime] = Field(default=None, description="半开状态结束时间")
|
||
half_open_successes: Optional[int] = Field(default=0, description="半开状态成功次数")
|
||
half_open_failures: Optional[int] = Field(default=0, description="半开状态失败次数")
|
||
request_results_window: Optional[List[dict]] = Field(None, description="请求结果滑动窗口")
|
||
|
||
# 使用统计
|
||
request_count: int
|
||
success_count: int
|
||
error_count: int
|
||
success_rate: float = Field(default=0.0, description="成功率")
|
||
avg_response_time_ms: float = Field(default=0.0, description="平均响应时间(毫秒)")
|
||
|
||
# 状态
|
||
is_active: bool
|
||
|
||
# 自适应并发信息
|
||
is_adaptive: bool = Field(default=False, description="是否为自适应模式(max_concurrent=NULL)")
|
||
learned_max_concurrent: Optional[int] = Field(None, description="学习到的并发限制")
|
||
effective_limit: Optional[int] = Field(None, description="当前有效限制")
|
||
# 滑动窗口利用率采样
|
||
utilization_samples: Optional[List[dict]] = Field(None, description="利用率采样窗口")
|
||
last_probe_increase_at: Optional[datetime] = Field(None, description="上次探测性扩容时间")
|
||
concurrent_429_count: Optional[int] = None
|
||
rpm_429_count: Optional[int] = None
|
||
last_429_at: Optional[datetime] = None
|
||
last_429_type: Optional[str] = None
|
||
|
||
# 备注
|
||
note: Optional[str] = None
|
||
|
||
# 时间戳
|
||
last_used_at: Optional[datetime] = None
|
||
created_at: datetime
|
||
updated_at: datetime
|
||
|
||
model_config = ConfigDict(from_attributes=True)
|
||
|
||
|
||
# ========== 健康监控相关 ==========
|
||
|
||
|
||
class HealthStatusResponse(BaseModel):
|
||
"""健康状态响应(仅 Key 级别)"""
|
||
|
||
# Key 健康状态
|
||
key_id: str
|
||
key_health_score: float
|
||
key_consecutive_failures: int
|
||
key_last_failure_at: Optional[datetime] = None
|
||
key_is_active: bool
|
||
key_statistics: Optional[Dict[str, Any]] = None
|
||
|
||
# 熔断器状态(滑动窗口 + 半开模式)
|
||
circuit_breaker_open: bool = False
|
||
circuit_breaker_open_at: Optional[datetime] = None
|
||
next_probe_at: Optional[datetime] = None
|
||
half_open_until: Optional[datetime] = None
|
||
half_open_successes: int = 0
|
||
half_open_failures: int = 0
|
||
|
||
|
||
class HealthSummaryResponse(BaseModel):
|
||
"""健康状态摘要"""
|
||
|
||
endpoints: Dict[str, int] = Field(..., description="Endpoint 统计 (total, active, unhealthy)")
|
||
keys: Dict[str, int] = Field(..., description="Key 统计 (total, active, unhealthy)")
|
||
|
||
|
||
# ========== 并发控制相关 ==========
|
||
|
||
|
||
class ConcurrencyStatusResponse(BaseModel):
|
||
"""并发状态响应"""
|
||
|
||
endpoint_id: Optional[str] = None
|
||
endpoint_current_concurrency: int = Field(default=0, description="Endpoint 当前并发数")
|
||
endpoint_max_concurrent: Optional[int] = Field(default=None, description="Endpoint 最大并发数")
|
||
|
||
key_id: Optional[str] = None
|
||
key_current_concurrency: int = Field(default=0, description="Key 当前并发数")
|
||
key_max_concurrent: Optional[int] = Field(default=None, description="Key 最大并发数")
|
||
|
||
|
||
class ResetConcurrencyRequest(BaseModel):
|
||
"""重置并发计数请求"""
|
||
|
||
endpoint_id: Optional[str] = Field(default=None, description="Endpoint ID(可选)")
|
||
key_id: Optional[str] = Field(default=None, description="Key ID(可选)")
|
||
|
||
|
||
class KeyPriorityItem(BaseModel):
|
||
"""单个 Key 优先级项"""
|
||
|
||
key_id: str = Field(..., description="Key ID")
|
||
internal_priority: int = Field(..., ge=0, description="Endpoint 内部优先级(数字越小越优先)")
|
||
|
||
|
||
class BatchUpdateKeyPriorityRequest(BaseModel):
|
||
"""批量更新 Key 优先级请求"""
|
||
|
||
priorities: List[KeyPriorityItem] = Field(..., min_length=1, description="Key 优先级列表")
|
||
|
||
|
||
# ========== 提供商摘要(增强版) ==========
|
||
|
||
|
||
class ProviderUpdateRequest(BaseModel):
|
||
"""Provider 基础配置更新请求"""
|
||
|
||
display_name: Optional[str] = Field(None, min_length=1, max_length=100)
|
||
description: Optional[str] = None
|
||
website: Optional[str] = Field(None, max_length=500, description="主站网站")
|
||
priority: Optional[int] = None
|
||
weight: Optional[float] = Field(None, gt=0)
|
||
provider_priority: Optional[int] = Field(None, description="提供商优先级(数字越小越优先)")
|
||
is_active: Optional[bool] = None
|
||
billing_type: Optional[str] = Field(
|
||
None, description="计费类型:monthly_quota/pay_as_you_go/free_tier"
|
||
)
|
||
monthly_quota_usd: Optional[float] = Field(None, ge=0, description="订阅配额(美元)")
|
||
quota_reset_day: Optional[int] = Field(None, ge=1, le=31, description="配额重置日(1-31)")
|
||
quota_expires_at: Optional[datetime] = Field(None, description="配额过期时间")
|
||
rpm_limit: Optional[int] = Field(
|
||
None, ge=0, description="每分钟请求数限制(NULL=无限制,0=禁止请求)"
|
||
)
|
||
|
||
|
||
class ProviderWithEndpointsSummary(BaseModel):
|
||
"""Provider 和 Endpoints 摘要"""
|
||
|
||
# Provider 基本信息
|
||
id: str
|
||
name: str
|
||
display_name: str
|
||
description: Optional[str] = None
|
||
website: Optional[str] = None
|
||
provider_priority: int = Field(default=100, description="提供商优先级(数字越小越优先)")
|
||
is_active: bool
|
||
|
||
# 计费相关字段
|
||
billing_type: Optional[str] = None
|
||
monthly_quota_usd: Optional[float] = None
|
||
monthly_used_usd: Optional[float] = None
|
||
quota_reset_day: Optional[int] = Field(default=None, description="配额重置周期(天数)")
|
||
quota_last_reset_at: Optional[datetime] = Field(default=None, description="当前周期开始时间")
|
||
quota_expires_at: Optional[datetime] = Field(default=None, description="配额过期时间")
|
||
|
||
# RPM 限制
|
||
rpm_limit: Optional[int] = Field(
|
||
default=None, description="每分钟请求数限制(NULL=无限制,0=禁止请求)"
|
||
)
|
||
rpm_used: Optional[int] = Field(default=None, description="当前分钟已用请求数")
|
||
rpm_reset_at: Optional[datetime] = Field(default=None, description="RPM 重置时间")
|
||
|
||
# Endpoint 统计
|
||
total_endpoints: int = Field(default=0, description="总 Endpoint 数量")
|
||
active_endpoints: int = Field(default=0, description="活跃 Endpoint 数量")
|
||
|
||
# Key 统计(所有 Endpoints 的 Keys)
|
||
total_keys: int = Field(default=0, description="总 Key 数量")
|
||
active_keys: int = Field(default=0, description="活跃 Key 数量")
|
||
|
||
# Model 统计
|
||
total_models: int = Field(default=0, description="总模型数量")
|
||
active_models: int = Field(default=0, description="活跃模型数量")
|
||
|
||
# API 格式列表
|
||
api_formats: List[str] = Field(default=[], description="支持的 API 格式列表")
|
||
|
||
# Endpoint 健康度详情
|
||
endpoint_health_details: List[Dict[str, Any]] = Field(
|
||
default=[],
|
||
description="每个 Endpoint 的健康度详情 [{api_format: str, health_score: float, is_active: bool}]",
|
||
)
|
||
|
||
# 健康度统计
|
||
avg_health_score: float = Field(default=1.0, description="平均健康度")
|
||
unhealthy_endpoints: int = Field(
|
||
default=0, description="不健康的端点数量(health_score < 0.5)"
|
||
)
|
||
|
||
# 时间戳
|
||
created_at: datetime
|
||
updated_at: datetime
|
||
|
||
model_config = ConfigDict(from_attributes=True)
|
||
|
||
|
||
# ========== 健康监控可视化模型 ==========
|
||
|
||
|
||
class EndpointHealthEvent(BaseModel):
|
||
"""单个端点的请求事件"""
|
||
|
||
timestamp: datetime
|
||
status: str
|
||
status_code: Optional[int] = None
|
||
latency_ms: Optional[int] = None
|
||
error_type: Optional[str] = None
|
||
error_message: Optional[str] = None
|
||
|
||
|
||
class EndpointHealthMonitor(BaseModel):
|
||
"""端点健康监控信息"""
|
||
|
||
endpoint_id: str
|
||
api_format: str
|
||
is_active: bool
|
||
total_attempts: int
|
||
success_count: int
|
||
failed_count: int
|
||
skipped_count: int
|
||
success_rate: float = Field(default=1.0, description="最近事件窗口的成功率")
|
||
last_event_at: Optional[datetime] = None
|
||
events: List[EndpointHealthEvent] = Field(default_factory=list)
|
||
|
||
|
||
class ProviderEndpointHealthMonitorResponse(BaseModel):
|
||
"""Provider 下所有端点的健康监控"""
|
||
|
||
provider_id: str
|
||
provider_name: str
|
||
generated_at: datetime
|
||
endpoints: List[EndpointHealthMonitor] = Field(default_factory=list)
|
||
|
||
|
||
class ApiFormatHealthMonitor(BaseModel):
|
||
"""按 API 格式聚合的健康监控信息"""
|
||
|
||
api_format: str
|
||
total_attempts: int
|
||
success_count: int
|
||
failed_count: int
|
||
skipped_count: int
|
||
success_rate: float = Field(default=1.0, description="最近事件窗口的成功率")
|
||
provider_count: int = Field(default=0, description="参与统计的 Provider 数量")
|
||
key_count: int = Field(default=0, description="参与统计的 API Key 数量")
|
||
last_event_at: Optional[datetime] = None
|
||
events: List[EndpointHealthEvent] = Field(default_factory=list)
|
||
timeline: List[str] = Field(
|
||
default_factory=list,
|
||
description="Usage 表生成的健康时间线(healthy/warning/unhealthy/unknown)",
|
||
)
|
||
time_range_start: Optional[datetime] = Field(
|
||
default=None, description="时间线所覆盖区间的开始时间"
|
||
)
|
||
time_range_end: Optional[datetime] = Field(
|
||
default=None, description="时间线所覆盖区间的结束时间"
|
||
)
|
||
|
||
|
||
class ApiFormatHealthMonitorResponse(BaseModel):
|
||
"""所有 API 格式的健康监控汇总"""
|
||
|
||
generated_at: datetime
|
||
formats: List[ApiFormatHealthMonitor] = Field(default_factory=list)
|
||
|
||
|
||
# ========== 公开健康监控模型(不含敏感信息) ==========
|
||
|
||
|
||
class PublicHealthEvent(BaseModel):
|
||
"""公开版单个请求事件(不含敏感信息如 provider_id、key_id)"""
|
||
|
||
timestamp: datetime
|
||
status: str
|
||
status_code: Optional[int] = None
|
||
latency_ms: Optional[int] = None
|
||
error_type: Optional[str] = None
|
||
|
||
|
||
class PublicApiFormatHealthMonitor(BaseModel):
|
||
"""公开版 API 格式健康监控信息(不含敏感信息)"""
|
||
|
||
api_format: str
|
||
api_path: str = Field(default="/", description="该 API 格式的本站请求路径")
|
||
total_attempts: int = Field(default=0, description="总请求次数")
|
||
success_count: int = Field(default=0, description="成功次数")
|
||
failed_count: int = Field(default=0, description="失败次数")
|
||
skipped_count: int = Field(default=0, description="跳过次数")
|
||
success_rate: float = Field(default=1.0, description="成功率")
|
||
last_event_at: Optional[datetime] = None
|
||
events: List[PublicHealthEvent] = Field(default_factory=list, description="事件列表")
|
||
timeline: List[str] = Field(
|
||
default_factory=list,
|
||
description="Usage 表生成的健康时间线(healthy/warning/unhealthy/unknown)",
|
||
)
|
||
time_range_start: Optional[datetime] = Field(
|
||
default=None, description="时间线覆盖区间开始时间"
|
||
)
|
||
time_range_end: Optional[datetime] = Field(
|
||
default=None, description="时间线覆盖区间结束时间"
|
||
)
|
||
|
||
|
||
class PublicApiFormatHealthMonitorResponse(BaseModel):
|
||
"""公开版健康监控汇总(不含敏感信息)"""
|
||
|
||
generated_at: datetime
|
||
formats: List[PublicApiFormatHealthMonitor] = Field(default_factory=list)
|