mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 01:10:23 +08:00
refactor: 移除 Python 后端源码,全面迁移至 Rust gateway 架构
- 删除全部 Python 源码 (src/) 及 Alembic 迁移脚本,归档至 _deprecated_py_src/ - 重构 Rust gateway ai_pipeline: 拆分 planner/finalize 模块,新增 contracts/adaptation 层 - 重组 handlers 模块为 admin/public/proxy/internal/shared 子模块结构 - 新增 executor 模块,引入 Rust 原生数据库迁移 (aether-data/migrations) - 简化 CI/Docker 构建流程,移除 base image 二级构建,统一为单一 app image - 移除 Python 相关基础设施文件 (entrypoint.sh, gunicorn_conf.py, Dockerfile.base)
This commit is contained in:
10
_deprecated_py_src/models/__init__.py
Normal file
10
_deprecated_py_src/models/__init__.py
Normal file
@@ -0,0 +1,10 @@
|
||||
"""
|
||||
统一的模型定义模块
|
||||
"""
|
||||
|
||||
from .api import * # noqa: F401, F403
|
||||
from .claude import * # noqa: F401, F403
|
||||
from .database import * # noqa: F401, F403
|
||||
from .openai import * # noqa: F401, F403
|
||||
|
||||
__all__ = ["claude", "database", "openai", "api"]
|
||||
755
_deprecated_py_src/models/admin_requests.py
Normal file
755
_deprecated_py_src/models/admin_requests.py
Normal file
@@ -0,0 +1,755 @@
|
||||
"""
|
||||
管理接口的 Pydantic 请求模型
|
||||
|
||||
提供完整的输入验证和安全过滤
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
from pydantic import BaseModel, Field, field_validator, model_validator
|
||||
|
||||
from src.core.enums import ProviderBillingType
|
||||
from src.core.validators import PasswordValidator
|
||||
|
||||
|
||||
class ProxyConfig(BaseModel):
|
||||
"""代理配置"""
|
||||
|
||||
# 模式 1: 手动配置代理 URL(原有)
|
||||
url: str | None = Field(None, description="代理 URL (http://, https://, socks5://)")
|
||||
username: str | None = Field(None, max_length=255, description="代理用户名")
|
||||
password: str | None = Field(None, max_length=500, description="代理密码")
|
||||
# 模式 2: ProxyNode(aether-proxy 注册的节点)
|
||||
node_id: str | None = Field(None, description="代理节点 ID")
|
||||
enabled: bool = Field(True, description="是否启用代理(false 时保留配置但不使用)")
|
||||
|
||||
@field_validator("url")
|
||||
@classmethod
|
||||
def validate_proxy_url(cls, v: str | None) -> str | None:
|
||||
"""验证代理 URL 格式"""
|
||||
if v is None:
|
||||
return None
|
||||
|
||||
from urllib.parse import urlparse
|
||||
|
||||
v = v.strip()
|
||||
if not v:
|
||||
return None
|
||||
|
||||
# 检查禁止的字符(防止注入)
|
||||
if "\n" in v or "\r" in v:
|
||||
raise ValueError("代理 URL 包含非法字符")
|
||||
|
||||
# 验证协议(不支持 SOCKS4)
|
||||
if not re.match(r"^(http|https|socks5)://", v, re.IGNORECASE):
|
||||
raise ValueError("代理 URL 必须以 http://, https:// 或 socks5:// 开头")
|
||||
|
||||
# 验证 URL 结构
|
||||
parsed = urlparse(v)
|
||||
if not parsed.netloc:
|
||||
raise ValueError("代理 URL 必须包含有效的 host")
|
||||
|
||||
# 禁止 URL 中内嵌认证信息,强制使用独立字段
|
||||
if parsed.username or parsed.password:
|
||||
raise ValueError("请勿在 URL 中包含用户名和密码,请使用独立的认证字段")
|
||||
|
||||
return v
|
||||
|
||||
@field_validator("node_id")
|
||||
@classmethod
|
||||
def validate_node_id(cls, v: str | None) -> str | None:
|
||||
if v is None:
|
||||
return None
|
||||
v = v.strip()
|
||||
return v or None
|
||||
|
||||
@model_validator(mode="after")
|
||||
def validate_proxy_mode(self) -> "ProxyConfig":
|
||||
if not self.enabled:
|
||||
return self
|
||||
|
||||
if not self.url and not self.node_id:
|
||||
raise ValueError("启用代理时,必须提供 url 或 node_id")
|
||||
|
||||
if self.url and self.node_id:
|
||||
raise ValueError("url 和 node_id 不能同时设置")
|
||||
|
||||
return self
|
||||
|
||||
|
||||
class FailoverRuleItem(BaseModel):
|
||||
"""故障转移规则条目"""
|
||||
|
||||
pattern: str = Field(..., min_length=1, max_length=500, description="正则表达式")
|
||||
description: str = Field("", max_length=200, description="规则描述")
|
||||
status_codes: list[int] | None = Field(
|
||||
default=None,
|
||||
description="HTTP 状态码列表(可选,为空时匹配所有状态码)",
|
||||
)
|
||||
|
||||
@field_validator("pattern")
|
||||
@classmethod
|
||||
def validate_pattern(cls, v: str) -> str:
|
||||
"""验证正则表达式语法"""
|
||||
import re as _re
|
||||
|
||||
try:
|
||||
_re.compile(v)
|
||||
except _re.error as e:
|
||||
raise ValueError(f"无效的正则表达式: {e}")
|
||||
return v
|
||||
|
||||
@field_validator("status_codes")
|
||||
@classmethod
|
||||
def validate_status_codes(cls, v: list[int] | None) -> list[int] | None:
|
||||
"""验证 HTTP 状态码"""
|
||||
if v is None:
|
||||
return v
|
||||
for code in v:
|
||||
if not (100 <= code <= 599):
|
||||
raise ValueError(f"无效的 HTTP 状态码: {code}")
|
||||
return v
|
||||
|
||||
|
||||
class FailoverRulesConfig(BaseModel):
|
||||
"""故障转移规则配置"""
|
||||
|
||||
success_failover_patterns: list[FailoverRuleItem] = Field(
|
||||
default_factory=list,
|
||||
description="成功响应转移规则: HTTP 200 但响应体匹配正则时触发转移",
|
||||
)
|
||||
error_stop_patterns: list[FailoverRuleItem] = Field(
|
||||
default_factory=list,
|
||||
description="错误终止规则: HTTP 非 200 且响应体匹配正则时停止转移",
|
||||
)
|
||||
|
||||
|
||||
class ScoringWeightsConfig(BaseModel):
|
||||
"""多维评分权重配置。"""
|
||||
|
||||
lru: float = Field(0.3, ge=0.0, le=1.0)
|
||||
latency: float = Field(0.25, ge=0.0, le=1.0)
|
||||
health: float = Field(0.2, ge=0.0, le=1.0)
|
||||
cost_remaining: float = Field(0.25, ge=0.0, le=1.0)
|
||||
|
||||
|
||||
def _allowed_pool_preset_names() -> set[str]:
|
||||
from src.services.provider.pool.dimensions import get_preset_names
|
||||
|
||||
return get_preset_names() | {"lru"}
|
||||
|
||||
|
||||
def _preset_mode_meta(name: str) -> tuple[set[str], str | None]:
|
||||
from src.services.provider.pool.dimensions import get_preset_dimension
|
||||
|
||||
dim = get_preset_dimension(name)
|
||||
if dim is None or not dim.modes:
|
||||
return set(), None
|
||||
|
||||
ordered_modes = [str(mode).strip().lower() for mode in dim.modes if str(mode).strip()]
|
||||
if not ordered_modes:
|
||||
return set(), None
|
||||
modes = set(ordered_modes)
|
||||
default_mode = str(dim.default_mode or "").strip().lower()
|
||||
if not default_mode or default_mode not in modes:
|
||||
default_mode = ordered_modes[0]
|
||||
return modes, default_mode
|
||||
|
||||
|
||||
class SchedulingPresetItem(BaseModel):
|
||||
"""调度预设条目(新格式:有序对象列表)。"""
|
||||
|
||||
preset: str
|
||||
enabled: bool = True
|
||||
mode: str | None = None
|
||||
|
||||
@field_validator("preset")
|
||||
@classmethod
|
||||
def validate_preset(cls, v: str) -> str:
|
||||
normalized = v.strip().lower()
|
||||
allowed = _allowed_pool_preset_names()
|
||||
if normalized not in allowed:
|
||||
raise ValueError(f"无效的 preset: {normalized}")
|
||||
return normalized
|
||||
|
||||
@field_validator("mode")
|
||||
@classmethod
|
||||
def normalize_mode(cls, v: str | None) -> str | None:
|
||||
if v is None:
|
||||
return None
|
||||
normalized = v.strip().lower()
|
||||
return normalized or None
|
||||
|
||||
@model_validator(mode="after")
|
||||
def validate_mode(self) -> "SchedulingPresetItem":
|
||||
allowed_modes, default_mode = _preset_mode_meta(self.preset)
|
||||
if not allowed_modes:
|
||||
self.mode = None
|
||||
return self
|
||||
|
||||
if self.mode is None:
|
||||
self.mode = default_mode
|
||||
return self
|
||||
|
||||
if self.mode not in allowed_modes:
|
||||
raise ValueError(
|
||||
f"preset={self.preset} 的 mode 必须是: {', '.join(sorted(allowed_modes))}"
|
||||
)
|
||||
return self
|
||||
|
||||
|
||||
class PoolAdvancedConfig(BaseModel):
|
||||
"""通用号池配置(适用于所有 Provider 类型)。"""
|
||||
|
||||
global_priority: int | None = Field(
|
||||
None,
|
||||
ge=0,
|
||||
le=999999,
|
||||
description="global_key 模式下号池整体优先级(数字越小越优先)",
|
||||
)
|
||||
sticky_session_ttl_seconds: int | None = Field(
|
||||
None,
|
||||
ge=60,
|
||||
le=86400,
|
||||
description="粘性会话 TTL(秒),同一对话始终路由到同一 Key。None = 禁用",
|
||||
)
|
||||
load_threshold_percent: int | None = Field(
|
||||
None,
|
||||
ge=10,
|
||||
le=100,
|
||||
description="负载率阈值(%),超过时该 Key 被降权。默认 80",
|
||||
)
|
||||
# 保留旧字段供向后兼容(新客户端不再发送)
|
||||
lru_enabled: bool = Field(True, description="LRU 调度(优先选择最久未用的 Key)")
|
||||
scheduling_mode: str | None = Field(
|
||||
None,
|
||||
pattern="^(lru|multi_score)$",
|
||||
description="号池调度模式:lru 或 multi_score",
|
||||
)
|
||||
scheduling_presets: list[SchedulingPresetItem] | list[str] | None = Field(
|
||||
None,
|
||||
description=(
|
||||
"调度预设列表(新格式:对象列表 [{preset, enabled, mode}];"
|
||||
"旧格式:字符串列表 ['quota_balanced', ...])"
|
||||
),
|
||||
)
|
||||
scoring_weights: ScoringWeightsConfig | None = Field(None, description="多维评分权重")
|
||||
latency_window_seconds: int | None = Field(
|
||||
None,
|
||||
ge=300,
|
||||
le=86400,
|
||||
description="延迟窗口(秒),仅 multi_score 生效",
|
||||
)
|
||||
latency_sample_limit: int | None = Field(
|
||||
None,
|
||||
ge=10,
|
||||
le=200,
|
||||
description="每个 Key 的延迟样本上限,仅 multi_score 生效",
|
||||
)
|
||||
cost_window_seconds: int | None = Field(
|
||||
None,
|
||||
ge=3600,
|
||||
le=86400,
|
||||
description="滚动成本窗口(秒)。默认 18000(5 小时)",
|
||||
)
|
||||
cost_limit_per_key_tokens: int | None = Field(
|
||||
None, ge=0, description="每个 Key 在窗口内的最大 token 用量。None = 不限"
|
||||
)
|
||||
cost_soft_threshold_percent: int | None = Field(
|
||||
None,
|
||||
ge=0,
|
||||
le=100,
|
||||
description="成本软阈值(%),超过时优先选用其他 Key。默认 80",
|
||||
)
|
||||
rate_limit_cooldown_seconds: int | None = Field(
|
||||
None, ge=10, le=3600, description="429 冷却时间(秒)。默认 300"
|
||||
)
|
||||
overload_cooldown_seconds: int | None = Field(
|
||||
None, ge=5, le=600, description="529 冷却时间(秒)。默认 30"
|
||||
)
|
||||
proactive_refresh_seconds: int | None = Field(
|
||||
None,
|
||||
ge=60,
|
||||
le=600,
|
||||
description="OAuth Token 提前刷新秒数。默认 180(3 分钟)",
|
||||
)
|
||||
health_policy_enabled: bool = Field(
|
||||
True, description="启用号池健康策略(按上游错误码自动冷却/禁用 Key)"
|
||||
)
|
||||
unschedulable_rules: list[dict] | None = Field(
|
||||
None,
|
||||
description="关键词临时不可调度规则: [{'keyword': '...', 'duration_minutes': 5}]",
|
||||
)
|
||||
batch_concurrency: int | None = Field(
|
||||
None,
|
||||
ge=1,
|
||||
le=32,
|
||||
description="批量操作并发数(前端批量刷新 OAuth/额度等)。默认 8",
|
||||
)
|
||||
probing_enabled: bool = Field(
|
||||
False, description="启用主动探测(定期刷新 Key 的账号状态与额度)"
|
||||
)
|
||||
probing_interval_minutes: int | None = Field(
|
||||
None,
|
||||
ge=1,
|
||||
le=1440,
|
||||
description="主动探测间隔(分钟)。默认 10",
|
||||
)
|
||||
auto_remove_banned_keys: bool = Field(
|
||||
False,
|
||||
description="检测到不可恢复账号异常时自动清除账号(不处理纯 Token 失效)",
|
||||
)
|
||||
|
||||
|
||||
class ClaudeCodeAdvancedConfig(BaseModel):
|
||||
"""Claude Code 特有配置。"""
|
||||
|
||||
max_sessions: int | None = Field(
|
||||
None, ge=1, le=1000, description="最大活跃会话数(为空表示不限制)"
|
||||
)
|
||||
session_idle_timeout_minutes: int | None = Field(
|
||||
None, ge=1, le=1440, description="会话空闲超时(分钟)"
|
||||
)
|
||||
session_id_masking_enabled: bool = Field(
|
||||
False, description="是否启用会话 ID 伪装(固定 metadata.user_id 中 session 片段)"
|
||||
)
|
||||
cache_ttl_override_enabled: bool = Field(
|
||||
False, description="是否启用 Cache TTL 强制替换(统一所有请求的 cache_control 类型)"
|
||||
)
|
||||
cache_ttl_override_target: str = Field(
|
||||
"ephemeral",
|
||||
description="Cache TTL 目标类型: ephemeral (5min) 或 1h",
|
||||
pattern="^(ephemeral|1h)$",
|
||||
)
|
||||
cli_only_enabled: bool = Field(
|
||||
False,
|
||||
description="是否仅允许 Claude Code CLI 客户端访问(非 CLI 流量返回 403)",
|
||||
)
|
||||
|
||||
@model_validator(mode="after")
|
||||
def normalize_session_control(self) -> "ClaudeCodeAdvancedConfig":
|
||||
# 未启用会话限制时,不保留超时配置,避免产生误导。
|
||||
if self.max_sessions is None:
|
||||
self.session_idle_timeout_minutes = None
|
||||
return self
|
||||
|
||||
# 启用会话限制但未设置超时时,回落到 5 分钟默认值。
|
||||
if self.session_idle_timeout_minutes is None:
|
||||
self.session_idle_timeout_minutes = 5
|
||||
return self
|
||||
|
||||
|
||||
class CreateProviderRequest(BaseModel):
|
||||
"""创建 Provider 请求"""
|
||||
|
||||
name: str = Field(..., min_length=1, max_length=100, description="提供商名称(唯一)")
|
||||
provider_type: str | None = Field(
|
||||
default="custom",
|
||||
max_length=20,
|
||||
description="Provider 类型:custom/claude_code/codex/gemini_cli/antigravity",
|
||||
)
|
||||
description: str | None = Field(None, max_length=1000, description="描述")
|
||||
website: str | None = Field(None, max_length=500, description="官网地址")
|
||||
|
||||
@field_validator("name")
|
||||
@classmethod
|
||||
def validate_name(cls, v: str) -> str:
|
||||
"""验证名称格式,防止注入攻击"""
|
||||
v = v.strip()
|
||||
|
||||
# 只允许安全的字符:字母、数字、下划线、连字符、空格、中文
|
||||
if not re.match(r"^[\w\s\u4e00-\u9fff-]+$", v):
|
||||
raise ValueError("名称只能包含字母、数字、下划线、连字符、空格和中文")
|
||||
|
||||
# 检查 SQL 注入关键字(不区分大小写)
|
||||
sql_keywords = [
|
||||
"SELECT",
|
||||
"INSERT",
|
||||
"UPDATE",
|
||||
"DELETE",
|
||||
"DROP",
|
||||
"CREATE",
|
||||
"ALTER",
|
||||
"TRUNCATE",
|
||||
"UNION",
|
||||
"EXEC",
|
||||
"EXECUTE",
|
||||
"--",
|
||||
"/*",
|
||||
"*/",
|
||||
]
|
||||
v_upper = v.upper()
|
||||
for keyword in sql_keywords:
|
||||
if keyword in v_upper:
|
||||
raise ValueError(f"名称包含非法关键字: {keyword}")
|
||||
|
||||
return v
|
||||
|
||||
billing_type: str | None = Field(
|
||||
ProviderBillingType.PAY_AS_YOU_GO.value, description="计费类型"
|
||||
)
|
||||
monthly_quota_usd: float | None = Field(None, ge=0, description="周期配额(美元)")
|
||||
quota_reset_day: int | None = Field(30, ge=1, le=365, description="配额重置周期(天数)")
|
||||
quota_last_reset_at: datetime | None = Field(None, description="当前周期开始时间")
|
||||
quota_expires_at: datetime | None = Field(None, description="配额过期时间")
|
||||
provider_priority: int | None = Field(
|
||||
None, ge=0, le=10000, description="提供商优先级(数字越小越优先,留空时新建自动置顶)"
|
||||
)
|
||||
keep_priority_on_conversion: bool = Field(
|
||||
False,
|
||||
description="格式转换时是否保持优先级(True=保持原优先级,False=需要转换时降级)",
|
||||
)
|
||||
is_active: bool | None = Field(True, description="是否启用")
|
||||
concurrent_limit: int | None = Field(None, ge=0, description="并发限制")
|
||||
# 请求配置(从 Endpoint 迁移)
|
||||
max_retries: int | None = Field(2, ge=0, le=999, description="最大重试次数")
|
||||
proxy: ProxyConfig | None = Field(None, description="代理配置")
|
||||
# 超时配置(秒),为空时使用全局配置
|
||||
stream_first_byte_timeout: float | None = Field(
|
||||
None, ge=1, le=300, description="流式请求首字节超时(秒)"
|
||||
)
|
||||
request_timeout: float | None = Field(
|
||||
None, ge=1, le=600, description="非流式请求整体超时(秒)"
|
||||
)
|
||||
pool_advanced: PoolAdvancedConfig | None = Field(
|
||||
None, description="号池高级配置(适用于所有 Provider 类型)"
|
||||
)
|
||||
claude_code_advanced: ClaudeCodeAdvancedConfig | None = Field(
|
||||
None, description="Claude Code 特有配置"
|
||||
)
|
||||
failover_rules: FailoverRulesConfig | None = Field(None, description="故障转移规则配置")
|
||||
config: dict[str, Any] | None = Field(None, description="其他配置")
|
||||
|
||||
@field_validator("provider_type")
|
||||
@classmethod
|
||||
def validate_provider_type(cls, v: str | None) -> str | None:
|
||||
if v is None:
|
||||
return "custom"
|
||||
v = v.strip()
|
||||
from src.core.provider_types import VALID_PROVIDER_TYPES
|
||||
|
||||
if v not in VALID_PROVIDER_TYPES:
|
||||
raise ValueError(
|
||||
f"无效的 provider_type,有效值为: {', '.join(sorted(VALID_PROVIDER_TYPES))}"
|
||||
)
|
||||
return v
|
||||
|
||||
@field_validator("name", "description")
|
||||
@classmethod
|
||||
def sanitize_text(cls, v: str | None) -> str | None:
|
||||
"""清理文本输入,防止 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) # 移除事件处理器
|
||||
|
||||
# 移除危险的 HTML 标签
|
||||
dangerous_tags = ["script", "iframe", "object", "embed", "link", "style"]
|
||||
for tag in dangerous_tags:
|
||||
v = re.sub(rf"<{tag}[^>]*>", "", v, flags=re.IGNORECASE)
|
||||
v = re.sub(rf"</{tag}>", "", v, flags=re.IGNORECASE)
|
||||
|
||||
return v.strip()
|
||||
|
||||
@field_validator("website")
|
||||
@classmethod
|
||||
def validate_website(cls, v: str | None) -> str | None:
|
||||
"""验证网站地址"""
|
||||
if v is None or v.strip() == "":
|
||||
return None
|
||||
|
||||
v = v.strip()
|
||||
|
||||
# 自动补全 https:// 前缀
|
||||
if not re.match(r"^https?://", v, re.IGNORECASE):
|
||||
v = f"https://{v}"
|
||||
|
||||
return v
|
||||
|
||||
@field_validator("billing_type")
|
||||
@classmethod
|
||||
def validate_billing_type(cls, v: str | None) -> str | None:
|
||||
"""验证计费类型"""
|
||||
if v is None:
|
||||
return ProviderBillingType.PAY_AS_YOU_GO.value
|
||||
|
||||
try:
|
||||
ProviderBillingType(v)
|
||||
return v
|
||||
except ValueError:
|
||||
valid_types = [t.value for t in ProviderBillingType]
|
||||
raise ValueError(f"无效的计费类型,有效值为: {', '.join(valid_types)}")
|
||||
|
||||
@model_validator(mode="after")
|
||||
def validate_claude_code_advanced_scope(self) -> "CreateProviderRequest":
|
||||
provider_type = (self.provider_type or "custom").strip()
|
||||
if self.claude_code_advanced is not None and provider_type != "claude_code":
|
||||
raise ValueError("claude_code_advanced 仅适用于 provider_type=claude_code")
|
||||
return self
|
||||
|
||||
|
||||
class UpdateProviderRequest(BaseModel):
|
||||
"""更新 Provider 请求"""
|
||||
|
||||
name: str | None = Field(None, min_length=1, max_length=100)
|
||||
provider_type: str | None = Field(
|
||||
None,
|
||||
max_length=20,
|
||||
description="Provider 类型:custom/claude_code/codex/gemini_cli/antigravity",
|
||||
)
|
||||
description: str | None = Field(None, max_length=1000)
|
||||
website: str | None = Field(None, max_length=500)
|
||||
billing_type: str | None = None
|
||||
monthly_quota_usd: float | None = Field(None, ge=0)
|
||||
quota_reset_day: int | None = Field(None, ge=1, le=365)
|
||||
quota_last_reset_at: datetime | None = None
|
||||
quota_expires_at: datetime | None = None
|
||||
provider_priority: int | None = Field(None, ge=0, le=10000)
|
||||
keep_priority_on_conversion: bool | None = Field(
|
||||
None,
|
||||
description="格式转换时是否保持优先级(True=保持原优先级,False=需要转换时降级)",
|
||||
)
|
||||
is_active: bool | None = None
|
||||
concurrent_limit: int | None = Field(None, ge=0)
|
||||
# 请求配置(从 Endpoint 迁移)
|
||||
max_retries: int | None = Field(None, ge=0, le=999, description="最大重试次数")
|
||||
proxy: ProxyConfig | None = Field(None, description="代理配置")
|
||||
# 超时配置(秒),为空时使用全局配置
|
||||
stream_first_byte_timeout: float | None = Field(
|
||||
None, ge=1, le=300, description="流式请求首字节超时(秒)"
|
||||
)
|
||||
request_timeout: float | None = Field(
|
||||
None, ge=1, le=600, description="非流式请求整体超时(秒)"
|
||||
)
|
||||
pool_advanced: PoolAdvancedConfig | None = Field(
|
||||
None, description="号池高级配置(适用于所有 Provider 类型)"
|
||||
)
|
||||
claude_code_advanced: ClaudeCodeAdvancedConfig | None = Field(
|
||||
None, description="Claude Code 特有配置"
|
||||
)
|
||||
failover_rules: FailoverRulesConfig | None = Field(None, description="故障转移规则配置")
|
||||
enable_format_conversion: bool | None = Field(
|
||||
None, description="是否允许格式转换(提供商级别开关)"
|
||||
)
|
||||
config: dict[str, Any] | None = None
|
||||
|
||||
# 复用相同的验证器
|
||||
_sanitize_text = field_validator("name", "description")(
|
||||
CreateProviderRequest.sanitize_text.__func__
|
||||
)
|
||||
_validate_website = field_validator("website")(CreateProviderRequest.validate_website.__func__)
|
||||
_validate_billing_type = field_validator("billing_type")(
|
||||
CreateProviderRequest.validate_billing_type.__func__
|
||||
)
|
||||
_validate_provider_type = field_validator("provider_type")(
|
||||
CreateProviderRequest.validate_provider_type.__func__
|
||||
)
|
||||
|
||||
@model_validator(mode="after")
|
||||
def validate_claude_code_advanced_scope(self) -> "UpdateProviderRequest":
|
||||
# 更新场景下 provider_type 可能不在 payload 中,最终校验由路由层结合数据库值完成。
|
||||
if self.claude_code_advanced is not None and self.provider_type is not None:
|
||||
provider_type = (self.provider_type or "custom").strip()
|
||||
if provider_type != "claude_code":
|
||||
raise ValueError("claude_code_advanced 仅适用于 provider_type=claude_code")
|
||||
return self
|
||||
|
||||
|
||||
class CreateEndpointRequest(BaseModel):
|
||||
"""创建 Endpoint 请求"""
|
||||
|
||||
provider_id: str = Field(..., description="Provider ID")
|
||||
name: str = Field(..., min_length=1, max_length=100, description="Endpoint 名称")
|
||||
base_url: str = Field(..., min_length=1, max_length=500, description="API 基础 URL")
|
||||
api_format: str = Field(
|
||||
..., description="Endpoint signature(如 openai:chat, claude:cli, gemini:video)"
|
||||
)
|
||||
custom_path: str | None = Field(None, max_length=200, description="自定义路径")
|
||||
priority: int | None = Field(100, ge=0, le=1000, description="优先级")
|
||||
is_active: bool | None = Field(True, description="是否启用")
|
||||
concurrent_limit: int | None = Field(None, ge=0, description="并发限制")
|
||||
config: dict[str, Any] | None = Field(None, description="其他配置")
|
||||
proxy: ProxyConfig | None = Field(None, description="代理配置")
|
||||
|
||||
@field_validator("name")
|
||||
@classmethod
|
||||
def validate_name(cls, v: str) -> str:
|
||||
"""验证名称"""
|
||||
if not re.match(r"^[a-zA-Z0-9_-]+$", v):
|
||||
raise ValueError("名称只能包含英文字母、数字、下划线和连字符")
|
||||
return v
|
||||
|
||||
@field_validator("base_url")
|
||||
@classmethod
|
||||
def validate_base_url(cls, v: str) -> str:
|
||||
"""验证 API URL"""
|
||||
if not re.match(r"^https?://", v, re.IGNORECASE):
|
||||
raise ValueError("URL 必须以 http:// 或 https:// 开头")
|
||||
|
||||
return v.rstrip("/") # 移除末尾斜杠
|
||||
|
||||
@field_validator("api_format")
|
||||
@classmethod
|
||||
def validate_api_format(cls, v: str) -> str:
|
||||
"""验证 API 格式"""
|
||||
from src.core.api_format import list_endpoint_definitions, resolve_endpoint_definition
|
||||
from src.core.api_format.signature import normalize_signature_key
|
||||
|
||||
normalized = normalize_signature_key(v)
|
||||
if resolve_endpoint_definition(normalized) is None:
|
||||
valid_formats = [d.signature_key for d in list_endpoint_definitions()]
|
||||
raise ValueError(f"无效的 api_format,有效值为: {', '.join(valid_formats)}")
|
||||
return normalized
|
||||
|
||||
@field_validator("custom_path")
|
||||
@classmethod
|
||||
def validate_custom_path(cls, v: str | None) -> str | None:
|
||||
"""验证自定义路径"""
|
||||
if v is None:
|
||||
return v
|
||||
|
||||
# 确保路径不包含危险字符
|
||||
if not re.match(r"^[/a-zA-Z0-9_-]+$", v):
|
||||
raise ValueError("路径只能包含字母、数字、斜杠、下划线和连字符")
|
||||
|
||||
return v
|
||||
|
||||
|
||||
class UpdateEndpointRequest(BaseModel):
|
||||
"""更新 Endpoint 请求"""
|
||||
|
||||
name: str | None = Field(None, min_length=1, max_length=100)
|
||||
base_url: str | None = Field(None, min_length=1, max_length=500)
|
||||
api_format: str | None = None
|
||||
custom_path: str | None = Field(None, max_length=200)
|
||||
priority: int | None = Field(None, ge=0, le=1000)
|
||||
is_active: bool | None = None
|
||||
concurrent_limit: int | None = Field(None, ge=0)
|
||||
config: dict[str, Any] | None = None
|
||||
proxy: ProxyConfig | None = Field(None, description="代理配置")
|
||||
|
||||
# 复用验证器
|
||||
_validate_name = field_validator("name")(CreateEndpointRequest.validate_name.__func__)
|
||||
_validate_base_url = field_validator("base_url")(
|
||||
CreateEndpointRequest.validate_base_url.__func__
|
||||
)
|
||||
_validate_api_format = field_validator("api_format")(
|
||||
CreateEndpointRequest.validate_api_format.__func__
|
||||
)
|
||||
_validate_custom_path = field_validator("custom_path")(
|
||||
CreateEndpointRequest.validate_custom_path.__func__
|
||||
)
|
||||
|
||||
|
||||
class CreateAPIKeyRequest(BaseModel):
|
||||
"""创建 API Key 请求"""
|
||||
|
||||
endpoint_id: str = Field(..., description="Endpoint ID")
|
||||
api_key: str = Field(..., min_length=1, max_length=10000, description="API Key")
|
||||
priority: int | None = Field(100, ge=0, le=1000, description="优先级")
|
||||
is_active: bool | None = Field(True, description="是否启用")
|
||||
rpm_limit: int | None = Field(None, ge=0, description="RPM 限制(NULL=自适应)")
|
||||
notes: str | None = Field(None, max_length=500, description="备注")
|
||||
|
||||
@field_validator("api_key")
|
||||
@classmethod
|
||||
def validate_api_key(cls, v: str) -> str:
|
||||
"""验证 API Key"""
|
||||
# 移除首尾空白
|
||||
v = v.strip()
|
||||
|
||||
# 检查危险字符(不应包含 SQL 注入字符)
|
||||
dangerous_chars = ["'", '"', ";", "--", "/*", "*/", "<", ">"]
|
||||
for char in dangerous_chars:
|
||||
if char in v:
|
||||
raise ValueError(f"API Key 包含非法字符: {char}")
|
||||
|
||||
return v
|
||||
|
||||
@field_validator("notes")
|
||||
@classmethod
|
||||
def sanitize_notes(cls, v: str | None) -> str | None:
|
||||
"""清理备注"""
|
||||
if v is None:
|
||||
return v
|
||||
# 复用文本清理逻辑
|
||||
return CreateProviderRequest.sanitize_text(v)
|
||||
|
||||
|
||||
class UpdateUserRequest(BaseModel):
|
||||
"""更新用户请求"""
|
||||
|
||||
username: str | None = Field(None, min_length=1, max_length=50)
|
||||
email: str | None = Field(None, max_length=100)
|
||||
password: str | None = Field(None, description="新密码(留空保持不变)")
|
||||
unlimited: bool | None = Field(None, description="是否无限制(true=无限制,false=有限制)")
|
||||
is_active: bool | None = None
|
||||
role: str | None = None
|
||||
allowed_providers: list[str] | None = Field(None, description="允许使用的提供商 ID 列表")
|
||||
allowed_api_formats: list[str] | None = Field(None, description="允许使用的 API 格式列表")
|
||||
allowed_models: list[str] | None = Field(None, description="允许使用的模型名称列表")
|
||||
rate_limit: int | None = Field(
|
||||
None,
|
||||
ge=0,
|
||||
description="每分钟请求限制;null 表示继承系统默认,0 表示不限制",
|
||||
)
|
||||
|
||||
@field_validator("username")
|
||||
@classmethod
|
||||
def validate_username(cls, v: str | None) -> str | None:
|
||||
"""验证用户名"""
|
||||
if v is None:
|
||||
return v
|
||||
|
||||
if not re.match(r"^[a-zA-Z0-9_.\-]+$", v):
|
||||
raise ValueError("用户名只能包含字母、数字、下划线、连字符和点号")
|
||||
|
||||
return v
|
||||
|
||||
@field_validator("email")
|
||||
@classmethod
|
||||
def validate_email(cls, v: str | None) -> str | None:
|
||||
"""验证邮箱"""
|
||||
if v is None:
|
||||
return 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()
|
||||
|
||||
@field_validator("password")
|
||||
@classmethod
|
||||
def validate_password(cls, v: str | None) -> str | None:
|
||||
"""验证密码"""
|
||||
if v is None:
|
||||
return v
|
||||
|
||||
valid, error_msg = PasswordValidator.validate_basic_input(v)
|
||||
if not valid:
|
||||
raise ValueError(error_msg or "密码格式无效")
|
||||
|
||||
return v
|
||||
|
||||
@field_validator("role")
|
||||
@classmethod
|
||||
def validate_role(cls, v: str | None) -> str | None:
|
||||
"""验证角色"""
|
||||
if v is None:
|
||||
return v
|
||||
|
||||
valid_roles = ["admin", "user"]
|
||||
if v not in valid_roles:
|
||||
raise ValueError(f"无效的角色,有效值为: {', '.join(valid_roles)}")
|
||||
|
||||
return v
|
||||
902
_deprecated_py_src/models/api.py
Normal file
902
_deprecated_py_src/models/api.py
Normal file
@@ -0,0 +1,902 @@
|
||||
"""
|
||||
API端点请求/响应模型定义
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from datetime import datetime
|
||||
from typing import Any, Literal
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
|
||||
|
||||
from ..core.enums import UserRole
|
||||
from ..core.validators import PasswordValidator
|
||||
|
||||
|
||||
# ========== 认证相关 ==========
|
||||
class LoginRequest(BaseModel):
|
||||
"""登录请求"""
|
||||
|
||||
email: str = Field(..., min_length=1, max_length=255, description="邮箱/用户名")
|
||||
password: str = Field(..., description="密码")
|
||||
auth_type: Literal["local", "ldap"] = Field(default="local", description="认证类型")
|
||||
|
||||
@field_validator("password")
|
||||
@classmethod
|
||||
def validate_password(cls, v: Any) -> Any:
|
||||
"""验证密码输入合法,保留原始内容。"""
|
||||
valid, error_msg = PasswordValidator.validate_login_input(v)
|
||||
if not valid:
|
||||
raise ValueError(error_msg or "密码格式无效")
|
||||
return v
|
||||
|
||||
@model_validator(mode="after")
|
||||
def validate_login(self) -> Any:
|
||||
"""根据认证类型校验并规范化登录标识"""
|
||||
identifier = self.email.strip()
|
||||
|
||||
if not identifier:
|
||||
raise ValueError("用户名/邮箱不能为空")
|
||||
|
||||
# 本地和 LDAP 登录都支持用户名或邮箱
|
||||
# 如果是邮箱格式,转换为小写
|
||||
email_pattern = r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"
|
||||
if re.match(email_pattern, identifier):
|
||||
self.email = identifier.lower()
|
||||
else:
|
||||
self.email = identifier
|
||||
|
||||
return self
|
||||
|
||||
|
||||
class LoginResponse(BaseModel):
|
||||
"""登录响应"""
|
||||
|
||||
access_token: str
|
||||
token_type: str = "bearer"
|
||||
expires_in: int = 86400 # Token有效期(秒),默认24小时
|
||||
user_id: str
|
||||
email: str | None = None
|
||||
username: str
|
||||
role: str
|
||||
|
||||
|
||||
class RefreshTokenResponse(BaseModel):
|
||||
"""刷新令牌响应"""
|
||||
|
||||
access_token: str
|
||||
token_type: str = "bearer"
|
||||
expires_in: int = 86400 # Token有效期(秒),默认24小时
|
||||
|
||||
|
||||
class RegisterRequest(BaseModel):
|
||||
"""注册请求"""
|
||||
|
||||
email: str | None = Field(None, max_length=255, description="邮箱地址(可选)")
|
||||
username: str = Field(..., min_length=3, max_length=50, description="用户名")
|
||||
password: str = Field(..., description="密码")
|
||||
|
||||
@field_validator("email")
|
||||
@classmethod
|
||||
def validate_email(cls, v: Any) -> Any:
|
||||
"""验证邮箱格式(如果提供)"""
|
||||
if v is None:
|
||||
return None
|
||||
v = v.strip()
|
||||
if not v:
|
||||
return None
|
||||
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: Any) -> Any:
|
||||
"""验证用户名格式"""
|
||||
v = v.strip()
|
||||
if not v:
|
||||
raise ValueError("用户名不能为空")
|
||||
if not re.match(r"^[a-zA-Z0-9_.\-]+$", v):
|
||||
raise ValueError("用户名只能包含字母、数字、下划线、连字符和点号")
|
||||
return v
|
||||
|
||||
@field_validator("password")
|
||||
@classmethod
|
||||
def validate_password(cls, v: Any) -> Any:
|
||||
"""基础校验(非空和算法限制),策略级别校验在服务层按系统配置执行。"""
|
||||
valid, error_msg = PasswordValidator.validate_basic_input(v)
|
||||
if not valid:
|
||||
raise ValueError(error_msg or "密码格式无效")
|
||||
return v
|
||||
|
||||
|
||||
class RegisterResponse(BaseModel):
|
||||
"""注册响应"""
|
||||
|
||||
user_id: str
|
||||
email: str | None = None
|
||||
username: str
|
||||
message: str
|
||||
|
||||
|
||||
class LogoutResponse(BaseModel):
|
||||
"""登出响应"""
|
||||
|
||||
message: str
|
||||
success: bool
|
||||
|
||||
|
||||
class SendVerificationCodeRequest(BaseModel):
|
||||
"""发送验证码请求"""
|
||||
|
||||
email: str = Field(..., min_length=3, max_length=255, description="邮箱地址")
|
||||
|
||||
@field_validator("email")
|
||||
@classmethod
|
||||
def validate_email(cls, v: Any) -> Any:
|
||||
"""验证邮箱格式"""
|
||||
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()
|
||||
|
||||
|
||||
class SendVerificationCodeResponse(BaseModel):
|
||||
"""发送验证码响应"""
|
||||
|
||||
message: str
|
||||
success: bool
|
||||
expire_minutes: int | None = None
|
||||
|
||||
|
||||
class VerifyEmailRequest(BaseModel):
|
||||
"""验证邮箱请求"""
|
||||
|
||||
email: str = Field(..., min_length=3, max_length=255, description="邮箱地址")
|
||||
code: str = Field(..., min_length=6, max_length=6, description="6位验证码")
|
||||
|
||||
@field_validator("email")
|
||||
@classmethod
|
||||
def validate_email(cls, v: Any) -> Any:
|
||||
"""验证邮箱格式"""
|
||||
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()
|
||||
|
||||
@field_validator("code")
|
||||
@classmethod
|
||||
def validate_code(cls, v: Any) -> Any:
|
||||
"""验证验证码格式"""
|
||||
v = v.strip()
|
||||
if not v.isdigit():
|
||||
raise ValueError("验证码必须是6位数字")
|
||||
if len(v) != 6:
|
||||
raise ValueError("验证码必须是6位数字")
|
||||
return v
|
||||
|
||||
|
||||
class VerifyEmailResponse(BaseModel):
|
||||
"""验证邮箱响应"""
|
||||
|
||||
message: str
|
||||
success: bool
|
||||
|
||||
|
||||
class VerificationStatusRequest(BaseModel):
|
||||
"""验证状态查询请求"""
|
||||
|
||||
email: str = Field(..., min_length=3, max_length=255, description="邮箱地址")
|
||||
|
||||
@field_validator("email")
|
||||
@classmethod
|
||||
def validate_email(cls, v: Any) -> Any:
|
||||
"""验证邮箱格式"""
|
||||
v = v.strip().lower()
|
||||
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
|
||||
|
||||
|
||||
class VerificationStatusResponse(BaseModel):
|
||||
"""验证状态响应"""
|
||||
|
||||
email: str
|
||||
has_pending_code: bool = Field(description="是否有待验证的验证码")
|
||||
is_verified: bool = Field(description="邮箱是否已验证")
|
||||
cooldown_remaining: int | None = Field(None, description="发送冷却剩余秒数")
|
||||
code_expires_in: int | None = Field(None, description="验证码剩余有效秒数")
|
||||
|
||||
|
||||
class RegistrationSettingsResponse(BaseModel):
|
||||
"""注册设置响应(公开接口返回)"""
|
||||
|
||||
enable_registration: bool
|
||||
require_email_verification: bool
|
||||
email_configured: bool = Field(description="是否配置了邮箱服务")
|
||||
password_policy_level: str = Field(description="密码策略等级:weak/medium/strong")
|
||||
|
||||
|
||||
# ========== 用户管理 ==========
|
||||
class CreateUserRequest(BaseModel):
|
||||
"""创建用户请求"""
|
||||
|
||||
username: str = Field(..., min_length=3, max_length=50, description="用户名")
|
||||
password: str = Field(..., description="密码")
|
||||
email: str | None = Field(None, max_length=255, description="邮箱地址(可选)")
|
||||
role: UserRole | None = Field(UserRole.USER, description="用户角色")
|
||||
initial_gift_usd: float | None = Field(
|
||||
default=None, description="初始赠款(USD),null 表示使用系统默认初始赠款"
|
||||
)
|
||||
unlimited: bool = Field(default=False, description="是否无限制")
|
||||
# 访问限制字段
|
||||
allowed_providers: list[str] | None = Field(
|
||||
default=None, description="允许使用的提供商ID列表,null表示无限制"
|
||||
)
|
||||
allowed_api_formats: list[str] | None = Field(
|
||||
default=None, description="允许使用的API格式列表,null表示无限制"
|
||||
)
|
||||
allowed_models: list[str] | None = Field(
|
||||
default=None, description="允许使用的模型名称列表,null表示无限制"
|
||||
)
|
||||
rate_limit: int | None = Field(
|
||||
default=None,
|
||||
ge=0,
|
||||
description="每分钟请求限制;null 表示继承系统默认,0 表示不限制",
|
||||
)
|
||||
|
||||
@field_validator("initial_gift_usd", mode="before")
|
||||
@classmethod
|
||||
def validate_initial_gift_usd(cls, v: Any) -> Any:
|
||||
"""验证初始赠款金额,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
|
||||
|
||||
@field_validator("email")
|
||||
@classmethod
|
||||
def validate_email(cls, v: str | None) -> str | None:
|
||||
"""验证邮箱格式(如果提供)"""
|
||||
if v is None:
|
||||
return None
|
||||
v = v.strip()
|
||||
if not v:
|
||||
return None
|
||||
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()
|
||||
|
||||
@field_validator("username")
|
||||
@classmethod
|
||||
def validate_username(cls, v: Any) -> Any:
|
||||
"""验证用户名格式"""
|
||||
v = v.strip()
|
||||
if not v:
|
||||
raise ValueError("用户名不能为空")
|
||||
if not re.match(r"^[a-zA-Z0-9_.\-]+$", v):
|
||||
raise ValueError("用户名只能包含字母、数字、下划线、连字符和点号")
|
||||
return v
|
||||
|
||||
@field_validator("allowed_api_formats")
|
||||
@classmethod
|
||||
def validate_allowed_api_formats(cls, v: list[str] | None) -> list[str] | None:
|
||||
"""校验并规范化 allowed_api_formats(endpoint signature: family:kind)。"""
|
||||
if v is None:
|
||||
return None
|
||||
from src.core.api_format import list_endpoint_definitions, resolve_endpoint_definition
|
||||
from src.core.api_format.signature import normalize_signature_key
|
||||
|
||||
allowed = [d.signature_key for d in list_endpoint_definitions()]
|
||||
out: list[str] = []
|
||||
seen: set[str] = set()
|
||||
for fmt in v:
|
||||
if not fmt:
|
||||
continue
|
||||
norm = normalize_signature_key(fmt)
|
||||
if resolve_endpoint_definition(norm) is None:
|
||||
raise ValueError(f"allowed_api_formats 必须是以下之一: {allowed},当前值: {fmt}")
|
||||
if norm in seen:
|
||||
continue
|
||||
seen.add(norm)
|
||||
out.append(norm)
|
||||
return out
|
||||
|
||||
@field_validator("password")
|
||||
@classmethod
|
||||
def validate_password(cls, v: Any) -> Any:
|
||||
"""基础校验(非空和算法限制),策略级别校验在服务层按系统配置执行。"""
|
||||
valid, error_msg = PasswordValidator.validate_basic_input(v)
|
||||
if not valid:
|
||||
raise ValueError(error_msg or "密码格式无效")
|
||||
return v
|
||||
|
||||
|
||||
class UpdateUserRequest(BaseModel):
|
||||
"""更新用户请求"""
|
||||
|
||||
email: str | None = None
|
||||
username: str | None = None
|
||||
password: str | None = Field(None, description="新密码(留空保持不变)")
|
||||
role: UserRole | None = None
|
||||
unlimited: bool | None = None
|
||||
allowed_providers: list[str] | None = None # 允许使用的提供商 ID 列表
|
||||
allowed_api_formats: list[str] | None = None # 允许使用的 API 格式列表
|
||||
allowed_models: list[str] | None = None # 允许使用的模型名称列表
|
||||
rate_limit: int | None = Field(
|
||||
default=None,
|
||||
ge=0,
|
||||
description="每分钟请求限制;null 表示继承系统默认,0 表示不限制",
|
||||
)
|
||||
is_active: bool | None = None
|
||||
|
||||
@field_validator("allowed_api_formats")
|
||||
@classmethod
|
||||
def validate_allowed_api_formats(cls, v: list[str] | None) -> list[str] | None:
|
||||
# 与 CreateUserRequest 保持一致
|
||||
return CreateUserRequest.validate_allowed_api_formats(v)
|
||||
|
||||
@field_validator("password")
|
||||
@classmethod
|
||||
def validate_password(cls, v: str | None) -> str | None:
|
||||
if v is None:
|
||||
return None
|
||||
return CreateUserRequest.validate_password(v)
|
||||
|
||||
|
||||
class CreateApiKeyRequest(BaseModel):
|
||||
"""创建API密钥请求"""
|
||||
|
||||
name: str | None = None
|
||||
allowed_providers: list[str] | None = None # 允许使用的提供商 ID 列表
|
||||
allowed_api_formats: list[str] | None = None # 允许使用的 API 格式列表
|
||||
allowed_models: list[str] | None = None # 允许使用的模型名称列表
|
||||
rate_limit: int | None = Field(
|
||||
None,
|
||||
ge=0,
|
||||
description="每分钟请求限制;独立Key: null=继承系统默认,0=不限制;普通Key: 0=不限制",
|
||||
)
|
||||
expire_days: int | None = None # None = 永不过期,数字 = 多少天后过期
|
||||
expires_at: str | None = None # ISO 日期字符串,如 "2025-12-31",优先于 expire_days
|
||||
initial_balance_usd: float | None = Field(
|
||||
None, description="初始余额(USD),仅用于独立Key,None = 无限制"
|
||||
)
|
||||
unlimited_balance: bool | None = Field(
|
||||
None, description="是否无限余额(编辑独立Key时用于切换额度模式)"
|
||||
)
|
||||
is_standalone: bool = Field(False, description="是否为独立余额Key(给非注册用户使用)")
|
||||
auto_delete_on_expiry: bool = Field(
|
||||
False, description="过期后是否自动删除(True=物理删除,False=仅禁用)"
|
||||
)
|
||||
|
||||
@field_validator("allowed_api_formats")
|
||||
@classmethod
|
||||
def validate_allowed_api_formats(cls, v: list[str] | None) -> list[str] | None:
|
||||
# 与 CreateUserRequest 保持一致
|
||||
return CreateUserRequest.validate_allowed_api_formats(v)
|
||||
|
||||
|
||||
class UserResponse(BaseModel):
|
||||
"""用户响应"""
|
||||
|
||||
id: str
|
||||
email: str | None = None
|
||||
username: str
|
||||
role: UserRole
|
||||
allowed_providers: list[str] | None = None # 允许使用的提供商 ID 列表
|
||||
allowed_api_formats: list[str] | None = None # 允许使用的 API 格式列表
|
||||
allowed_models: list[str] | None = None # 允许使用的模型名称列表
|
||||
rate_limit: int | None = None
|
||||
unlimited: bool = False
|
||||
is_active: bool
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
last_login_at: datetime | None
|
||||
|
||||
|
||||
class ApiKeyResponse(BaseModel):
|
||||
"""API密钥响应"""
|
||||
|
||||
id: str
|
||||
user_id: str
|
||||
key: str | None = None # 仅在创建时返回完整密钥
|
||||
key_display: str | None = None # 脱敏后的密钥显示
|
||||
name: str | None
|
||||
total_requests: int
|
||||
total_tokens: int
|
||||
total_cost_usd: float
|
||||
allowed_providers: list[str] | None
|
||||
allowed_models: list[str] | None
|
||||
rate_limit: int | None
|
||||
is_active: bool
|
||||
expires_at: datetime | None = None
|
||||
is_standalone: bool = False
|
||||
force_capabilities: dict[str, bool] | None = None # 强制开启的能力
|
||||
created_at: datetime
|
||||
last_used_at: datetime | None
|
||||
|
||||
|
||||
# ========== 提供商管理 ==========
|
||||
class ProviderCreate(BaseModel):
|
||||
"""创建提供商请求
|
||||
|
||||
架构说明:
|
||||
- Provider 仅包含提供商的元数据和计费配置
|
||||
- API格式、URL、认证等配置应在 ProviderEndpoint 中设置
|
||||
- API密钥应在 ProviderAPIKey 中设置
|
||||
"""
|
||||
|
||||
name: str = Field(..., min_length=1, max_length=100, description="提供商名称(唯一)")
|
||||
description: str | None = Field(None, description="提供商描述")
|
||||
website: str | None = Field(None, max_length=500, description="主站网站")
|
||||
|
||||
# Provider 级别的配置
|
||||
rate_limit: int | None = Field(None, description="每分钟请求限制")
|
||||
concurrent_limit: int | None = Field(None, description="并发请求限制")
|
||||
config: dict | None = Field(None, description="额外配置")
|
||||
is_active: bool = Field(False, description="是否启用(默认false,需要配置API密钥后才能启用)")
|
||||
|
||||
# 超时配置(秒),为空时使用全局配置
|
||||
stream_first_byte_timeout: float | None = Field(
|
||||
None, ge=1, le=300, description="流式请求首字节超时(秒)"
|
||||
)
|
||||
request_timeout: float | None = Field(
|
||||
None, ge=1, le=600, description="非流式请求整体超时(秒)"
|
||||
)
|
||||
|
||||
|
||||
class ProviderUpdate(BaseModel):
|
||||
"""更新提供商请求"""
|
||||
|
||||
name: str | None = Field(None, min_length=1, max_length=100)
|
||||
description: str | None = None
|
||||
website: str | None = Field(None, max_length=500)
|
||||
api_format: str | None = None
|
||||
base_url: str | None = None
|
||||
headers: dict | None = None
|
||||
max_retries: int | None = Field(None, ge=0, le=10)
|
||||
priority: int | None = None
|
||||
weight: float | None = Field(None, gt=0)
|
||||
rate_limit: int | None = None
|
||||
concurrent_limit: int | None = None
|
||||
config: dict | None = None
|
||||
is_active: bool | None = None
|
||||
|
||||
# 超时配置(秒),为空时使用全局配置
|
||||
stream_first_byte_timeout: float | None = Field(
|
||||
None, ge=1, le=300, description="流式请求首字节超时(秒)"
|
||||
)
|
||||
request_timeout: float | None = Field(
|
||||
None, ge=1, le=600, description="非流式请求整体超时(秒)"
|
||||
)
|
||||
|
||||
|
||||
class ProviderResponse(BaseModel):
|
||||
"""提供商响应"""
|
||||
|
||||
id: str
|
||||
name: str
|
||||
description: str | None
|
||||
website: str | None
|
||||
api_format: str
|
||||
base_url: str
|
||||
headers: dict | None
|
||||
max_retries: int
|
||||
priority: int
|
||||
weight: float
|
||||
rate_limit: int | None
|
||||
concurrent_limit: int | None
|
||||
config: dict | None
|
||||
is_active: bool
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
models_count: int = 0
|
||||
active_models_count: int = 0
|
||||
api_keys_count: int = 0
|
||||
|
||||
# 超时配置
|
||||
stream_first_byte_timeout: float | None = None
|
||||
request_timeout: float | None = None
|
||||
|
||||
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: list[dict] | None = Field(
|
||||
None,
|
||||
description="模型名称映射列表,格式: [{'name': 'alias1', 'priority': 1}, ...]",
|
||||
)
|
||||
global_model_id: str = Field(..., description="关联的 GlobalModel ID(必填)")
|
||||
# 按次计费配置 - 可选,为空时使用 GlobalModel 默认值
|
||||
price_per_request: float | None = Field(
|
||||
None, ge=0, description="每次请求固定费用,为空使用默认值"
|
||||
)
|
||||
# 阶梯计费配置 - 可选,为空时使用 GlobalModel 默认值
|
||||
tiered_pricing: dict | None = Field(
|
||||
None, description="阶梯计费配置,为空使用 GlobalModel 默认值"
|
||||
)
|
||||
# 能力配置 - 可选,为空时使用 GlobalModel 默认值
|
||||
supports_vision: bool | None = Field(None, description="是否支持图像输入,为空使用默认值")
|
||||
supports_function_calling: bool | None = Field(
|
||||
None, description="是否支持函数调用,为空使用默认值"
|
||||
)
|
||||
supports_streaming: bool | None = Field(None, description="是否支持流式输出,为空使用默认值")
|
||||
supports_extended_thinking: bool | None = Field(
|
||||
None, description="是否支持扩展思考,为空使用默认值"
|
||||
)
|
||||
is_active: bool = Field(True, description="是否启用")
|
||||
config: dict | None = Field(None, description="额外配置")
|
||||
|
||||
|
||||
class ModelUpdate(BaseModel):
|
||||
"""更新模型请求"""
|
||||
|
||||
provider_model_name: str | None = Field(None, min_length=1, max_length=200)
|
||||
provider_model_mappings: list[dict] | None = Field(
|
||||
None,
|
||||
description="模型名称映射列表,格式: [{'name': 'alias1', 'priority': 1}, ...]",
|
||||
)
|
||||
global_model_id: str | None = None
|
||||
# 按次计费配置
|
||||
price_per_request: float | None = Field(None, ge=0, description="每次请求固定费用")
|
||||
# 阶梯计费配置
|
||||
tiered_pricing: dict | None = Field(None, description="阶梯计费配置")
|
||||
supports_vision: bool | None = None
|
||||
supports_function_calling: bool | None = None
|
||||
supports_streaming: bool | None = None
|
||||
supports_extended_thinking: bool | None = None
|
||||
is_active: bool | None = None
|
||||
is_available: bool | None = None
|
||||
config: dict | None = None
|
||||
|
||||
|
||||
class ModelResponse(BaseModel):
|
||||
"""模型响应 - 包含 Model 配置和关联的 GlobalModel 信息
|
||||
|
||||
注意:价格和能力字段返回的是有效值(优先使用 Model 配置,否则使用 GlobalModel 默认值)
|
||||
"""
|
||||
|
||||
id: str
|
||||
provider_id: str
|
||||
global_model_id: str
|
||||
provider_model_name: str
|
||||
provider_model_mappings: list[dict] | None = None
|
||||
|
||||
# 按次计费配置
|
||||
price_per_request: float | None = None
|
||||
# 阶梯计费配置
|
||||
tiered_pricing: dict | None = None
|
||||
|
||||
# Provider 能力配置 - 可选,为空表示使用 GlobalModel 默认值
|
||||
supports_vision: bool | None
|
||||
supports_function_calling: bool | None
|
||||
supports_streaming: bool | None
|
||||
supports_extended_thinking: bool | None
|
||||
supports_image_generation: bool | None
|
||||
|
||||
# 有效值(合并 Model 配置和 GlobalModel 默认值后的结果)
|
||||
effective_tiered_pricing: dict | None = None
|
||||
effective_input_price: float | None = None
|
||||
effective_output_price: float | None = None
|
||||
effective_price_per_request: float | None = None
|
||||
effective_supports_vision: bool | None = None
|
||||
effective_supports_function_calling: bool | None = None
|
||||
effective_supports_streaming: bool | None = None
|
||||
effective_supports_extended_thinking: bool | None = None
|
||||
effective_supports_image_generation: bool | None = None
|
||||
|
||||
# 状态
|
||||
is_active: bool
|
||||
is_available: bool
|
||||
|
||||
# 时间戳
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
# 关联的 GlobalModel 信息
|
||||
global_model_name: str | None = None
|
||||
global_model_display_name: str | None = None
|
||||
|
||||
# 有效配置(合并 Model 和 GlobalModel 的 config)
|
||||
effective_config: dict | None = None
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class ModelDetailResponse(BaseModel):
|
||||
"""模型详细响应 - 包含所有字段(用于需要完整信息的场景)"""
|
||||
|
||||
id: str
|
||||
provider_id: str
|
||||
name: str
|
||||
display_name: str
|
||||
description: str | None
|
||||
icon_url: str | None
|
||||
tags: list[str] | None
|
||||
input_price_per_1m: float
|
||||
output_price_per_1m: float
|
||||
cache_creation_price_per_1m: float | None
|
||||
cache_read_price_per_1m: float | None
|
||||
supports_vision: bool
|
||||
supports_function_calling: bool
|
||||
supports_streaming: bool
|
||||
is_active: bool
|
||||
is_available: bool
|
||||
config: dict | None
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
# ========== 系统设置 ==========
|
||||
class SystemSettingsRequest(BaseModel):
|
||||
"""系统设置请求"""
|
||||
|
||||
default_provider: str | None = None
|
||||
default_model: str | None = None
|
||||
enable_usage_tracking: bool | None = None
|
||||
password_policy_level: Literal["weak", "medium", "strong"] | None = None
|
||||
|
||||
|
||||
class SystemSettingsResponse(BaseModel):
|
||||
"""系统设置响应"""
|
||||
|
||||
default_provider: str | None
|
||||
default_model: str | None
|
||||
enable_usage_tracking: bool
|
||||
password_policy_level: str
|
||||
|
||||
|
||||
# ========== 使用统计 ==========
|
||||
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
|
||||
description: str | None
|
||||
website: str | None
|
||||
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
|
||||
name: str
|
||||
display_name: str
|
||||
description: str | None = None
|
||||
tags: list[str] | None = None
|
||||
icon_url: str | None = None
|
||||
# 价格信息
|
||||
input_price_per_1m: float | None = None
|
||||
output_price_per_1m: float | None = None
|
||||
cache_creation_price_per_1m: float | None = None
|
||||
cache_read_price_per_1m: float | None = None
|
||||
# 功能支持
|
||||
supports_vision: bool | None = None
|
||||
supports_function_calling: bool | None = None
|
||||
supports_streaming: bool | None = 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: str | None = None
|
||||
is_active: bool = True
|
||||
# 按次计费配置
|
||||
default_price_per_request: float | None = None
|
||||
# 阶梯计费配置
|
||||
default_tiered_pricing: dict | None = None
|
||||
# Key 能力配置
|
||||
supported_capabilities: list[str] | None = None
|
||||
# 模型配置(JSON)
|
||||
config: dict | None = None
|
||||
# 调用次数
|
||||
usage_count: int = 0
|
||||
|
||||
|
||||
class PublicGlobalModelListResponse(BaseModel):
|
||||
"""公开的 GlobalModel 列表响应"""
|
||||
|
||||
models: list[PublicGlobalModelResponse]
|
||||
total: int
|
||||
|
||||
|
||||
# ========== 个人中心相关模型 ==========
|
||||
class UpdateProfileRequest(BaseModel):
|
||||
"""更新个人信息请求"""
|
||||
|
||||
email: str | None = None
|
||||
username: str | None = None
|
||||
|
||||
|
||||
class UpdatePreferencesRequest(BaseModel):
|
||||
"""更新偏好设置请求"""
|
||||
|
||||
avatar_url: str | None = None
|
||||
bio: str | None = None
|
||||
default_provider_id: int | None = None
|
||||
theme: str | None = None
|
||||
language: str | None = None
|
||||
timezone: str | None = None
|
||||
email_notifications: bool | None = None
|
||||
usage_alerts: bool | None = None
|
||||
announcement_notifications: bool | None = None
|
||||
|
||||
|
||||
class ChangePasswordRequest(BaseModel):
|
||||
"""修改密码请求"""
|
||||
|
||||
old_password: str | None = None # 可选:首次设置密码时不需要
|
||||
new_password: str = Field(..., description="新密码")
|
||||
|
||||
@field_validator("new_password")
|
||||
@classmethod
|
||||
def validate_new_password(cls, v: Any) -> Any:
|
||||
"""基础校验(非空和算法限制),策略级别校验在服务层按系统配置执行。"""
|
||||
valid, error_msg = PasswordValidator.validate_basic_input(v)
|
||||
if not valid:
|
||||
raise ValueError(error_msg or "密码格式无效")
|
||||
return v
|
||||
|
||||
|
||||
class UserSessionResponse(BaseModel):
|
||||
"""用户会话响应"""
|
||||
|
||||
id: str
|
||||
device_label: str
|
||||
device_type: str
|
||||
browser_name: str | None = None
|
||||
browser_version: str | None = None
|
||||
os_name: str | None = None
|
||||
os_version: str | None = None
|
||||
device_model: str | None = None
|
||||
ip_address: str | None = None
|
||||
last_seen_at: str | None = None
|
||||
created_at: str
|
||||
is_current: bool = False
|
||||
revoked_at: str | None = None
|
||||
revoke_reason: str | None = None
|
||||
|
||||
@classmethod
|
||||
def from_db(cls, session: Any, *, current_session_id: str | None = None) -> dict[str, Any]:
|
||||
return cls(
|
||||
id=session.id,
|
||||
device_label=session.device_label or "未知设备",
|
||||
device_type=session.device_type or "unknown",
|
||||
browser_name=session.browser_name,
|
||||
browser_version=session.browser_version,
|
||||
os_name=session.os_name,
|
||||
os_version=session.os_version,
|
||||
device_model=session.device_model,
|
||||
ip_address=session.ip_address,
|
||||
last_seen_at=session.last_seen_at.isoformat() if session.last_seen_at else None,
|
||||
created_at=session.created_at.isoformat(),
|
||||
is_current=bool(current_session_id and session.id == current_session_id),
|
||||
revoked_at=session.revoked_at.isoformat() if session.revoked_at else None,
|
||||
revoke_reason=session.revoke_reason,
|
||||
).model_dump()
|
||||
|
||||
|
||||
class UpdateSessionLabelRequest(BaseModel):
|
||||
"""更新会话显示名称"""
|
||||
|
||||
device_label: str = Field(..., min_length=1, max_length=120, description="设备名称")
|
||||
|
||||
@field_validator("device_label")
|
||||
@classmethod
|
||||
def validate_device_label(cls, v: Any) -> Any:
|
||||
normalized = str(v).strip()
|
||||
if not normalized:
|
||||
raise ValueError("设备名称不能为空")
|
||||
return normalized
|
||||
|
||||
|
||||
class CreateMyApiKeyRequest(BaseModel):
|
||||
"""创建我的API密钥请求"""
|
||||
|
||||
name: str
|
||||
rate_limit: int = Field(0, ge=0, description="该 Key 的每分钟请求限制,0 表示不限制")
|
||||
|
||||
|
||||
class UpdateMyApiKeyRequest(BaseModel):
|
||||
"""更新我的 API 密钥请求"""
|
||||
|
||||
name: str | None = None
|
||||
rate_limit: int | None = Field(
|
||||
None,
|
||||
ge=0,
|
||||
description="该 Key 的每分钟请求限制;0 表示不限制,null 表示不修改",
|
||||
)
|
||||
|
||||
|
||||
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: list[ProviderConfig] | None = 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: datetime | None = None
|
||||
end_time: datetime | None = None
|
||||
|
||||
|
||||
class UpdateAnnouncementRequest(BaseModel):
|
||||
"""更新公告请求"""
|
||||
|
||||
title: str | None = None
|
||||
content: str | None = None
|
||||
type: str | None = None
|
||||
priority: int | None = None
|
||||
is_active: bool | None = None
|
||||
is_pinned: bool | None = None
|
||||
start_time: datetime | None = None
|
||||
end_time: datetime | None = None
|
||||
64
_deprecated_py_src/models/api_key.py
Normal file
64
_deprecated_py_src/models/api_key.py
Normal file
@@ -0,0 +1,64 @@
|
||||
"""
|
||||
Provider API Key相关的API模型
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
|
||||
class ProviderAPIKeyBase(BaseModel):
|
||||
"""Provider API Key基础模型"""
|
||||
|
||||
name: str | None = Field(None, description="密钥名称/备注")
|
||||
api_key: str = Field(..., description="API密钥")
|
||||
rpm_limit: int | None = Field(None, description="RPM限制(每分钟请求数),NULL=自适应模式")
|
||||
priority: int = Field(0, description="优先级(越高越优先使用)")
|
||||
is_active: bool = Field(True, description="是否启用")
|
||||
expires_at: datetime | None = Field(None, description="过期时间")
|
||||
|
||||
|
||||
class ProviderAPIKeyCreate(ProviderAPIKeyBase):
|
||||
"""创建Provider API Key请求"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class ProviderAPIKeyUpdate(BaseModel):
|
||||
"""更新Provider API Key请求"""
|
||||
|
||||
name: str | None = None
|
||||
api_key: str | None = None
|
||||
rpm_limit: int | None = None
|
||||
priority: int | None = None
|
||||
is_active: bool | None = None
|
||||
expires_at: datetime | None = None
|
||||
|
||||
|
||||
class ProviderAPIKeyResponse(ProviderAPIKeyBase):
|
||||
"""Provider API Key响应"""
|
||||
|
||||
id: str
|
||||
provider_id: str
|
||||
request_count: int | None = Field(0, description="请求次数")
|
||||
error_count: int | None = Field(0, description="错误次数")
|
||||
last_used_at: datetime | None = Field(None, description="最后使用时间")
|
||||
last_error_at: datetime | None = Field(None, description="最后错误时间")
|
||||
last_error_msg: str | None = Field(None, description="最后错误信息")
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class ProviderAPIKeyStats(BaseModel):
|
||||
"""Provider API Key统计信息"""
|
||||
|
||||
id: str
|
||||
name: str | None
|
||||
request_count: int
|
||||
error_count: int
|
||||
success_rate: float
|
||||
last_used_at: datetime | None
|
||||
is_active: bool
|
||||
is_expired: bool
|
||||
118
_deprecated_py_src/models/claude.py
Normal file
118
_deprecated_py_src/models/claude.py
Normal file
@@ -0,0 +1,118 @@
|
||||
from typing import Any, Literal
|
||||
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
|
||||
|
||||
# 配置允许额外字段,以支持API的新特性
|
||||
class BaseModelWithExtras(BaseModel):
|
||||
model_config = ConfigDict(extra="allow")
|
||||
|
||||
|
||||
class ClaudeContentBlockText(BaseModelWithExtras):
|
||||
type: Literal["text"]
|
||||
text: str
|
||||
|
||||
|
||||
class ClaudeContentBlockImage(BaseModelWithExtras):
|
||||
type: Literal["image"]
|
||||
source: dict[str, Any]
|
||||
|
||||
|
||||
class ClaudeContentBlockToolUse(BaseModelWithExtras):
|
||||
type: Literal["tool_use"]
|
||||
id: str
|
||||
name: str
|
||||
input: dict[str, Any]
|
||||
|
||||
|
||||
class ClaudeContentBlockToolResult(BaseModelWithExtras):
|
||||
type: Literal["tool_result"]
|
||||
tool_use_id: str
|
||||
content: str | list[dict[str, Any]] | dict[str, Any]
|
||||
|
||||
|
||||
class ClaudeContentBlockThinking(BaseModelWithExtras):
|
||||
type: Literal["thinking"]
|
||||
thinking: str
|
||||
|
||||
|
||||
class ClaudeSystemContent(BaseModelWithExtras):
|
||||
type: Literal["text"]
|
||||
text: str
|
||||
|
||||
|
||||
class ClaudeMessage(BaseModelWithExtras):
|
||||
role: Literal["user", "assistant"]
|
||||
# 宽松的内容类型定义 - 接受字符串或任意字典列表
|
||||
# 作为转发代理,不应该严格限制内容块类型,以支持API的新特性
|
||||
content: str | list[dict[str, Any]]
|
||||
|
||||
|
||||
class ClaudeTool(BaseModelWithExtras):
|
||||
name: str
|
||||
description: str | None = None
|
||||
input_schema: dict[str, Any]
|
||||
|
||||
|
||||
class ClaudeThinkingConfig(BaseModelWithExtras):
|
||||
enabled: bool = True
|
||||
|
||||
|
||||
class ClaudeMessagesRequest(BaseModelWithExtras):
|
||||
model: str
|
||||
max_tokens: int
|
||||
messages: list[ClaudeMessage]
|
||||
# 宽松的system类型 - 接受字符串、字典列表或任意字典
|
||||
system: str | list[dict[str, Any]] | dict[str, Any] | None = None
|
||||
stop_sequences: list[str] | None = None
|
||||
stream: bool | None = False
|
||||
temperature: float | None = 1.0
|
||||
top_p: float | None = None
|
||||
top_k: int | None = None
|
||||
metadata: dict[str, Any] | None = None
|
||||
tools: list[dict[str, Any]] | None = None # 改为更宽松的类型
|
||||
tool_choice: dict[str, Any] | None = None
|
||||
thinking: dict[str, Any] | None = None # 改为更宽松的类型
|
||||
|
||||
|
||||
class ClaudeTokenCountRequest(BaseModelWithExtras):
|
||||
model: str
|
||||
messages: list[ClaudeMessage]
|
||||
# 宽松的类型定义以支持API新特性
|
||||
system: str | list[dict[str, Any]] | dict[str, Any] | None = None
|
||||
tools: list[dict[str, Any]] | None = None
|
||||
thinking: dict[str, Any] | None = None
|
||||
tool_choice: dict[str, Any] | None = None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 响应模型
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class ClaudeResponseUsage(BaseModelWithExtras):
|
||||
"""Claude 响应 token 使用量"""
|
||||
|
||||
input_tokens: int = 0
|
||||
output_tokens: int = 0
|
||||
cache_creation_input_tokens: int | None = None
|
||||
cache_read_input_tokens: int | None = None
|
||||
|
||||
|
||||
class ClaudeResponse(BaseModelWithExtras):
|
||||
"""
|
||||
Claude Messages API 响应模型
|
||||
|
||||
对应 POST /v1/messages 端点的响应体。
|
||||
"""
|
||||
|
||||
id: str
|
||||
model: str
|
||||
type: Literal["message"] = "message"
|
||||
role: Literal["assistant"] = "assistant"
|
||||
content: list[dict[str, Any]]
|
||||
stop_reason: str | None = None
|
||||
stop_sequence: str | None = None
|
||||
usage: ClaudeResponseUsage | None = None
|
||||
context_management: dict[str, Any] | None = None
|
||||
container: dict[str, Any] | None = None
|
||||
3254
_deprecated_py_src/models/database.py
Normal file
3254
_deprecated_py_src/models/database.py
Normal file
File diff suppressed because it is too large
Load Diff
118
_deprecated_py_src/models/database_extensions.py
Normal file
118
_deprecated_py_src/models/database_extensions.py
Normal file
@@ -0,0 +1,118 @@
|
||||
"""
|
||||
数据库模型扩展 - 新增的提供商策略相关表
|
||||
"""
|
||||
|
||||
import uuid
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from sqlalchemy import (
|
||||
Boolean,
|
||||
Column,
|
||||
DateTime,
|
||||
Float,
|
||||
ForeignKey,
|
||||
Index,
|
||||
Integer,
|
||||
String,
|
||||
UniqueConstraint,
|
||||
)
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
from .database import Base
|
||||
|
||||
|
||||
class ApiKeyProviderMapping(Base):
|
||||
"""
|
||||
API Key 和 Provider 的关联映射表
|
||||
|
||||
用途:管理员为特定的 API Key 指定提供商
|
||||
- 如果存在映射:该 API Key 只能使用指定的提供商(无负载均衡和故障转移)
|
||||
- 如果不存在映射:该 API Key 使用所有可用提供商(系统默认优先级,有负载均衡和故障转移)
|
||||
|
||||
注意:priority_adjustment 和 weight_multiplier 字段保留但在当前版本不使用
|
||||
"""
|
||||
|
||||
__tablename__ = "api_key_provider_mappings"
|
||||
|
||||
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()), index=True)
|
||||
api_key_id = Column(
|
||||
String(36), ForeignKey("api_keys.id", ondelete="CASCADE"), nullable=False, index=True
|
||||
)
|
||||
provider_id = Column(
|
||||
String(36), ForeignKey("providers.id", ondelete="CASCADE"), nullable=False, index=True
|
||||
)
|
||||
|
||||
# 管理员设置的优先级调整(非用户自己设置)
|
||||
priority_adjustment = Column(Integer, default=0) # 优先级调整值(可正可负)
|
||||
weight_multiplier = Column(Float, default=1.0) # 权重乘数(>0)
|
||||
|
||||
# 是否启用
|
||||
is_enabled = Column(Boolean, default=True, nullable=False)
|
||||
|
||||
# 时间戳
|
||||
created_at = Column(
|
||||
DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), nullable=False
|
||||
)
|
||||
updated_at = Column(
|
||||
DateTime(timezone=True),
|
||||
default=lambda: datetime.now(timezone.utc),
|
||||
onupdate=lambda: datetime.now(timezone.utc),
|
||||
nullable=False,
|
||||
)
|
||||
|
||||
# 关系
|
||||
api_key = relationship("ApiKey", back_populates="provider_mappings")
|
||||
provider = relationship("Provider", back_populates="api_key_mappings")
|
||||
|
||||
# 唯一约束
|
||||
__table_args__ = (
|
||||
UniqueConstraint("api_key_id", "provider_id", name="uq_apikey_provider"),
|
||||
Index("idx_apikey_provider_enabled", "api_key_id", "is_enabled"),
|
||||
)
|
||||
|
||||
|
||||
class ProviderUsageTracking(Base):
|
||||
"""提供商使用追踪 (用于RPM限流和健康检测)"""
|
||||
|
||||
__tablename__ = "provider_usage_tracking"
|
||||
|
||||
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()), index=True)
|
||||
provider_id = Column(
|
||||
String(36), ForeignKey("providers.id", ondelete="CASCADE"), nullable=False, index=True
|
||||
)
|
||||
|
||||
# 时间窗口
|
||||
window_start = Column(DateTime(timezone=True), nullable=False, index=True)
|
||||
window_end = Column(DateTime(timezone=True), nullable=False)
|
||||
|
||||
# 统计数据
|
||||
total_requests = Column(Integer, default=0)
|
||||
successful_requests = Column(Integer, default=0)
|
||||
failed_requests = Column(Integer, default=0)
|
||||
|
||||
# 性能数据
|
||||
avg_response_time_ms = Column(Float, default=0.0)
|
||||
total_response_time_ms = Column(Float, default=0.0) # 用于计算平均值
|
||||
|
||||
# 成本数据
|
||||
total_cost_usd = Column(Float, default=0.0)
|
||||
|
||||
# 时间戳
|
||||
created_at = Column(
|
||||
DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), nullable=False
|
||||
)
|
||||
updated_at = Column(
|
||||
DateTime(timezone=True),
|
||||
default=lambda: datetime.now(timezone.utc),
|
||||
onupdate=lambda: datetime.now(timezone.utc),
|
||||
nullable=False,
|
||||
)
|
||||
|
||||
# 关系
|
||||
provider = relationship("Provider", back_populates="usage_tracking")
|
||||
|
||||
# 索引
|
||||
__table_args__ = (
|
||||
Index("idx_provider_window", "provider_id", "window_start"),
|
||||
Index("idx_window_time", "window_start", "window_end"),
|
||||
)
|
||||
1278
_deprecated_py_src/models/endpoint_models.py
Normal file
1278
_deprecated_py_src/models/endpoint_models.py
Normal file
File diff suppressed because it is too large
Load Diff
165
_deprecated_py_src/models/gemini.py
Normal file
165
_deprecated_py_src/models/gemini.py
Normal file
@@ -0,0 +1,165 @@
|
||||
"""
|
||||
Google Gemini API 请求/响应模型
|
||||
|
||||
支持 Gemini API 的请求/响应格式
|
||||
作为 API 网关,采用宽松类型定义以支持 API 新特性透传
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
|
||||
class BaseModelWithExtras(BaseModel):
|
||||
"""允许额外字段的基础模型"""
|
||||
|
||||
model_config = ConfigDict(extra="allow")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 内容定义 - 使用宽松类型以支持透传
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class GeminiContent(BaseModelWithExtras):
|
||||
"""
|
||||
Gemini 消息内容
|
||||
|
||||
使用宽松类型定义,parts 接受任意字典列表以支持 API 新特性
|
||||
"""
|
||||
|
||||
role: str | None = None
|
||||
parts: list[dict[str, Any]]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 请求模型 - 只定义网关需要的字段,其余透传
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class GeminiRequest(BaseModelWithExtras):
|
||||
"""
|
||||
Gemini 统一请求模型
|
||||
|
||||
内部使用,统一处理 generateContent 和 streamGenerateContent
|
||||
|
||||
注意: Gemini API 通过 URL 端点区分流式/非流式请求:
|
||||
- generateContent - 非流式
|
||||
- streamGenerateContent - 流式
|
||||
请求体中不应包含 stream 字段
|
||||
|
||||
采用宽松类型定义,除必要字段外全部透传
|
||||
"""
|
||||
|
||||
model: str | None = Field(default=None, description="模型名称,从 URL 路径提取(内部使用)")
|
||||
contents: list[GeminiContent]
|
||||
# 以下字段全部使用 dict[str, Any] 透传,不做结构验证
|
||||
system_instruction: dict[str, Any] | None = Field(default=None, alias="systemInstruction")
|
||||
tools: list[dict[str, Any]] | None = None
|
||||
tool_config: dict[str, Any] | None = Field(default=None, alias="toolConfig")
|
||||
safety_settings: list[dict[str, Any]] | None = Field(default=None, alias="safetySettings")
|
||||
generation_config: dict[str, Any] | None = Field(default=None, alias="generationConfig")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 响应模型 - 用于解析上游响应提取必要信息(如 usage)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class GeminiUsageMetadata(BaseModelWithExtras):
|
||||
"""Token 使用量 - 用于计费统计"""
|
||||
|
||||
prompt_token_count: int = Field(default=0, alias="promptTokenCount")
|
||||
candidates_token_count: int = Field(default=0, alias="candidatesTokenCount")
|
||||
total_token_count: int = Field(default=0, alias="totalTokenCount")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 文件 API 模型
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class GeminiFileMetadata(BaseModelWithExtras):
|
||||
"""
|
||||
Gemini 文件元数据
|
||||
|
||||
用于上传文件时指定的元数据信息
|
||||
"""
|
||||
|
||||
display_name: str | None = Field(default=None, alias="displayName")
|
||||
|
||||
|
||||
class GeminiFileUploadRequest(BaseModelWithExtras):
|
||||
"""
|
||||
Gemini 文件上传请求
|
||||
|
||||
用于 media.upload API 的请求体
|
||||
"""
|
||||
|
||||
file: GeminiFileMetadata | None = None
|
||||
|
||||
|
||||
class GeminiFile(BaseModelWithExtras):
|
||||
"""
|
||||
Gemini 文件资源
|
||||
|
||||
表示已上传到 Gemini API 的文件
|
||||
"""
|
||||
|
||||
name: str | None = None # 文件名,格式:files/xxx
|
||||
display_name: str | None = Field(default=None, alias="displayName")
|
||||
mime_type: str | None = Field(default=None, alias="mimeType")
|
||||
size_bytes: str | None = Field(default=None, alias="sizeBytes")
|
||||
create_time: str | None = Field(default=None, alias="createTime")
|
||||
update_time: str | None = Field(default=None, alias="updateTime")
|
||||
expiration_time: str | None = Field(default=None, alias="expirationTime")
|
||||
sha256_hash: str | None = Field(default=None, alias="sha256Hash")
|
||||
uri: str | None = None # 文件 URI,用于在请求中引用
|
||||
download_uri: str | None = Field(default=None, alias="downloadUri")
|
||||
state: str | None = None # PROCESSING, ACTIVE, FAILED
|
||||
error: dict[str, Any] | None = None
|
||||
# 视频文件元数据
|
||||
video_metadata: dict[str, Any] | None = Field(default=None, alias="videoMetadata")
|
||||
|
||||
|
||||
class GeminiFileListResponse(BaseModelWithExtras):
|
||||
"""
|
||||
Gemini 文件列表响应
|
||||
|
||||
用于 files.list API 的响应体
|
||||
"""
|
||||
|
||||
files: list["GeminiFile"] | None = None
|
||||
next_page_token: str | None = Field(default=None, alias="nextPageToken")
|
||||
|
||||
|
||||
class GeminiFileUploadResponse(BaseModelWithExtras):
|
||||
"""
|
||||
Gemini 文件上传响应
|
||||
|
||||
用于 media.upload API 的响应体
|
||||
"""
|
||||
|
||||
file: GeminiFile | None = None
|
||||
|
||||
|
||||
class GeminiFilePart(BaseModelWithExtras):
|
||||
"""
|
||||
Gemini 文件引用部分
|
||||
|
||||
用于在请求内容中引用已上传的文件
|
||||
使用 file_data 字段引用文件 URI
|
||||
"""
|
||||
|
||||
file_data: dict[str, Any] | None = Field(default=None, alias="fileData")
|
||||
# fileData 格式:{"mimeType": "...", "fileUri": "..."}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Thought Signature 常量
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# 用于从其他模型迁移对话时绕过签名验证
|
||||
DUMMY_THOUGHT_SIGNATURE = "context_engineering_is_the_way_to_go"
|
||||
153
_deprecated_py_src/models/openai.py
Normal file
153
_deprecated_py_src/models/openai.py
Normal file
@@ -0,0 +1,153 @@
|
||||
"""
|
||||
OpenAI API 数据模型定义
|
||||
"""
|
||||
|
||||
from typing import Any
|
||||
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
|
||||
|
||||
# 配置允许额外字段,以支持 API 的新特性
|
||||
class BaseModelWithExtras(BaseModel):
|
||||
model_config = ConfigDict(extra="allow")
|
||||
|
||||
|
||||
class OpenAIMessage(BaseModelWithExtras):
|
||||
"""OpenAI消息模型"""
|
||||
|
||||
role: str
|
||||
content: str | list[dict[str, Any]] | None = None
|
||||
tool_calls: list[dict[str, Any]] | None = None
|
||||
tool_call_id: str | None = None
|
||||
name: str | None = None
|
||||
|
||||
|
||||
class OpenAIFunction(BaseModelWithExtras):
|
||||
"""OpenAI函数定义"""
|
||||
|
||||
name: str
|
||||
description: str | None = None
|
||||
parameters: dict[str, Any]
|
||||
|
||||
|
||||
class OpenAITool(BaseModelWithExtras):
|
||||
"""OpenAI工具定义"""
|
||||
|
||||
type: str = "function"
|
||||
function: OpenAIFunction
|
||||
|
||||
|
||||
class OpenAIRequest(BaseModelWithExtras):
|
||||
"""OpenAI请求模型"""
|
||||
|
||||
model: str
|
||||
messages: list[OpenAIMessage]
|
||||
max_tokens: int | None = None
|
||||
temperature: float | None = 1.0
|
||||
top_p: float | None = None
|
||||
stream: bool | None = False
|
||||
stop: str | list[str] | None = None
|
||||
tools: list[OpenAITool] | None = None
|
||||
tool_choice: str | dict[str, Any] | None = None
|
||||
presence_penalty: float | None = None
|
||||
frequency_penalty: float | None = None
|
||||
n: int | None = None
|
||||
seed: int | None = None
|
||||
response_format: dict[str, Any] | None = None
|
||||
logit_bias: dict[str, float] | None = None
|
||||
logprobs: bool | None = None
|
||||
top_logprobs: int | None = None
|
||||
user: str | None = None
|
||||
|
||||
|
||||
class ResponsesInputMessage(BaseModelWithExtras):
|
||||
"""Responses API 输入消息"""
|
||||
|
||||
type: str = "message"
|
||||
role: str
|
||||
content: list[dict[str, Any]]
|
||||
|
||||
|
||||
class ResponsesReasoningConfig(BaseModelWithExtras):
|
||||
"""Responses API 推理配置"""
|
||||
|
||||
effort: str = "high" # low, medium, high
|
||||
summary: str = "auto" # auto, off
|
||||
|
||||
|
||||
class ResponsesRequest(BaseModelWithExtras):
|
||||
"""OpenAI Responses API 请求模型(用于 Claude Code 等客户端)"""
|
||||
|
||||
model: str
|
||||
instructions: str | None = None
|
||||
input: list[ResponsesInputMessage]
|
||||
tools: list[dict[str, Any]] | None = None
|
||||
tool_choice: str | dict[str, Any] | None = "auto"
|
||||
parallel_tool_calls: bool | None = False
|
||||
reasoning: ResponsesReasoningConfig | None = None
|
||||
store: bool | None = False
|
||||
stream: bool | None = True
|
||||
include: list[str] | None = None
|
||||
prompt_cache_key: str | None = None
|
||||
# 其他参数
|
||||
max_tokens: int | None = None
|
||||
temperature: float | None = None
|
||||
top_p: float | None = None
|
||||
stop: str | list[str] | None = None
|
||||
|
||||
|
||||
class OpenAIUsage(BaseModelWithExtras):
|
||||
"""OpenAI使用统计"""
|
||||
|
||||
prompt_tokens: int
|
||||
completion_tokens: int
|
||||
total_tokens: int
|
||||
|
||||
|
||||
class OpenAIChoice(BaseModelWithExtras):
|
||||
"""OpenAI选择结果"""
|
||||
|
||||
index: int
|
||||
message: OpenAIMessage
|
||||
finish_reason: str | None = None
|
||||
logprobs: dict[str, Any] | None = None
|
||||
|
||||
|
||||
class OpenAIResponse(BaseModelWithExtras):
|
||||
"""OpenAI响应模型"""
|
||||
|
||||
id: str
|
||||
object: str = "chat.completion"
|
||||
created: int
|
||||
model: str
|
||||
choices: list[OpenAIChoice]
|
||||
usage: OpenAIUsage | None = None
|
||||
system_fingerprint: str | None = None
|
||||
|
||||
|
||||
class OpenAIStreamDelta(BaseModelWithExtras):
|
||||
"""OpenAI流式响应增量"""
|
||||
|
||||
role: str | None = None
|
||||
content: str | None = None
|
||||
tool_calls: list[dict[str, Any]] | None = None
|
||||
|
||||
|
||||
class OpenAIStreamChoice(BaseModelWithExtras):
|
||||
"""OpenAI流式响应选择"""
|
||||
|
||||
index: int
|
||||
delta: OpenAIStreamDelta
|
||||
finish_reason: str | None = None
|
||||
logprobs: dict[str, Any] | None = None
|
||||
|
||||
|
||||
class OpenAIStreamResponse(BaseModelWithExtras):
|
||||
"""OpenAI流式响应模型"""
|
||||
|
||||
id: str
|
||||
object: str = "chat.completion.chunk"
|
||||
created: int
|
||||
model: str
|
||||
choices: list[OpenAIStreamChoice]
|
||||
system_fingerprint: str | None = None
|
||||
362
_deprecated_py_src/models/pydantic_models.py
Normal file
362
_deprecated_py_src/models/pydantic_models.py
Normal file
@@ -0,0 +1,362 @@
|
||||
"""
|
||||
Pydantic 数据模型(阶段一统一模型管理)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
||||
|
||||
# ========== 阶梯计费相关模型 ==========
|
||||
|
||||
|
||||
class CacheTTLPricing(BaseModel):
|
||||
"""缓存时长定价配置"""
|
||||
|
||||
ttl_minutes: int = Field(..., ge=1, description="缓存时长(分钟)")
|
||||
cache_creation_price_per_1m: float = Field(
|
||||
..., ge=0, description="该时长的缓存创建价格/M tokens"
|
||||
)
|
||||
cache_read_price_per_1m: float | None = Field(
|
||||
None, ge=0, description="该时长的缓存读取价格/M tokens"
|
||||
)
|
||||
|
||||
|
||||
class PricingTier(BaseModel):
|
||||
"""单个价格阶梯配置"""
|
||||
|
||||
up_to: int | None = Field(
|
||||
None, ge=1, description="阶梯上限(tokens),null 表示无上限(最后一个阶梯)"
|
||||
)
|
||||
input_price_per_1m: float = Field(..., ge=0, description="输入价格/M tokens")
|
||||
output_price_per_1m: float = Field(..., ge=0, description="输出价格/M tokens")
|
||||
cache_creation_price_per_1m: float | None = Field(
|
||||
None, ge=0, description="缓存创建价格/M tokens"
|
||||
)
|
||||
cache_read_price_per_1m: float | None = Field(None, ge=0, description="缓存读取价格/M tokens")
|
||||
cache_ttl_pricing: list[CacheTTLPricing] | None = Field(
|
||||
None, description="按缓存时长分价格(可选)"
|
||||
)
|
||||
|
||||
|
||||
class TieredPricingConfig(BaseModel):
|
||||
"""阶梯计费配置"""
|
||||
|
||||
tiers: list[PricingTier] = Field(
|
||||
..., min_length=1, description="价格阶梯列表,按 up_to 升序排列"
|
||||
)
|
||||
|
||||
@model_validator(mode="after")
|
||||
def validate_tiers(self) -> TieredPricingConfig:
|
||||
"""验证阶梯配置的合法性"""
|
||||
tiers = self.tiers
|
||||
if not tiers:
|
||||
raise ValueError("至少需要一个价格阶梯")
|
||||
|
||||
# 检查阶梯顺序和唯一性
|
||||
prev_up_to = 0
|
||||
has_unlimited = False
|
||||
|
||||
for i, tier in enumerate(tiers):
|
||||
if has_unlimited:
|
||||
raise ValueError("无上限阶梯(up_to=null)必须是最后一个")
|
||||
|
||||
if tier.up_to is None:
|
||||
has_unlimited = True
|
||||
else:
|
||||
if tier.up_to <= prev_up_to:
|
||||
raise ValueError(
|
||||
f"阶梯 {i+1} 的 up_to ({tier.up_to}) 必须大于前一个阶梯 ({prev_up_to})"
|
||||
)
|
||||
prev_up_to = tier.up_to
|
||||
|
||||
# 验证缓存时长定价顺序
|
||||
if tier.cache_ttl_pricing:
|
||||
prev_ttl = 0
|
||||
for ttl_pricing in tier.cache_ttl_pricing:
|
||||
if ttl_pricing.ttl_minutes <= prev_ttl:
|
||||
raise ValueError(f"cache_ttl_pricing 必须按 ttl_minutes 升序排列")
|
||||
prev_ttl = ttl_pricing.ttl_minutes
|
||||
|
||||
# 最后一个阶梯必须是无上限的
|
||||
if not has_unlimited:
|
||||
raise ValueError("最后一个阶梯必须设置 up_to=null(无上限)")
|
||||
|
||||
return self
|
||||
|
||||
|
||||
# ========== 其他模型 ==========
|
||||
|
||||
|
||||
class ModelCapabilities(BaseModel):
|
||||
"""模型能力聚合"""
|
||||
|
||||
supports_vision: bool = False
|
||||
supports_function_calling: bool = False
|
||||
supports_streaming: bool = False
|
||||
|
||||
|
||||
class ModelPriceRange(BaseModel):
|
||||
"""统一模型价格区间"""
|
||||
|
||||
min_input: float | None = None
|
||||
max_input: float | None = None
|
||||
min_output: float | None = None
|
||||
max_output: float | None = None
|
||||
|
||||
|
||||
class ModelCatalogProviderDetail(BaseModel):
|
||||
"""统一模型目录中的关联提供商信息"""
|
||||
|
||||
provider_id: str
|
||||
provider_name: str
|
||||
model_id: str | None
|
||||
target_model: str
|
||||
input_price_per_1m: float | None
|
||||
output_price_per_1m: float | None
|
||||
cache_creation_price_per_1m: float | None
|
||||
cache_read_price_per_1m: float | None
|
||||
cache_1h_creation_price_per_1m: float | None = None # 1h 缓存创建价格
|
||||
price_per_request: float | None = None # 按次计费价格
|
||||
effective_tiered_pricing: dict[str, Any] | None = None # 有效阶梯计费配置(含继承)
|
||||
tier_count: int = 1 # 阶梯数量
|
||||
supports_vision: bool | None = None
|
||||
supports_function_calling: bool | None = None
|
||||
supports_streaming: bool | None = None
|
||||
is_active: bool
|
||||
|
||||
|
||||
class ModelCatalogItem(BaseModel):
|
||||
"""统一模型目录条目(基于 GlobalModel)"""
|
||||
|
||||
global_model_name: str # GlobalModel.name
|
||||
display_name: str # GlobalModel.display_name
|
||||
description: str | None # GlobalModel.description
|
||||
providers: list[ModelCatalogProviderDetail] # 支持该模型的 Provider 列表
|
||||
price_range: ModelPriceRange # 价格区间(从所有 Provider 的 Model 中聚合)
|
||||
total_providers: int
|
||||
capabilities: ModelCapabilities # 能力聚合(从所有 Provider 的 Model 中聚合)
|
||||
|
||||
|
||||
class ModelCatalogResponse(BaseModel):
|
||||
"""统一模型目录响应"""
|
||||
|
||||
models: list[ModelCatalogItem]
|
||||
total: int
|
||||
|
||||
|
||||
class ProviderModelPriceInfo(BaseModel):
|
||||
"""Provider 维度的模型价格信息"""
|
||||
|
||||
input_price_per_1m: float | None
|
||||
output_price_per_1m: float | None
|
||||
cache_creation_price_per_1m: float | None
|
||||
cache_read_price_per_1m: float | None
|
||||
price_per_request: float | None = None # 按次计费价格
|
||||
|
||||
|
||||
class ProviderAvailableSourceModel(BaseModel):
|
||||
"""Provider 支持的统一模型条目"""
|
||||
|
||||
global_model_name: str # GlobalModel.name
|
||||
display_name: str # GlobalModel.display_name
|
||||
provider_model_name: str # Model.provider_model_name (Provider 侧的模型名)
|
||||
model_id: str | None # Model.id
|
||||
price: ProviderModelPriceInfo
|
||||
capabilities: ModelCapabilities
|
||||
is_active: bool
|
||||
|
||||
|
||||
class ProviderAvailableSourceModelsResponse(BaseModel):
|
||||
"""Provider 可用统一模型响应"""
|
||||
|
||||
models: list[ProviderAvailableSourceModel]
|
||||
total: int
|
||||
|
||||
|
||||
# ========== GlobalModel 相关模型 ==========
|
||||
|
||||
|
||||
class GlobalModelCreate(BaseModel):
|
||||
"""创建 GlobalModel 请求"""
|
||||
|
||||
name: str = Field(..., min_length=1, max_length=100, description="统一模型名(唯一)")
|
||||
display_name: str = Field(..., min_length=1, max_length=100, description="显示名称")
|
||||
# 按次计费配置(可选,与阶梯计费叠加)
|
||||
default_price_per_request: float | None = Field(None, ge=0, description="每次请求固定费用")
|
||||
# 统一阶梯计费配置(必填)
|
||||
# 固定价格也用单阶梯表示: {"tiers": [{"up_to": null, "input_price_per_1m": X, ...}]}
|
||||
default_tiered_pricing: TieredPricingConfig = Field(
|
||||
..., description="阶梯计费配置(固定价格用单阶梯表示)"
|
||||
)
|
||||
# Key 能力配置 - 模型支持的能力列表(如 ["cache_1h", "context_1m"])
|
||||
supported_capabilities: list[str] | None = Field(None, description="支持的 Key 能力列表")
|
||||
# 模型配置(JSON格式)- 包含能力、规格、元信息等
|
||||
config: dict[str, Any] | None = Field(
|
||||
None, description="模型配置(streaming, vision, context_limit, description 等)"
|
||||
)
|
||||
is_active: bool | None = Field(True, description="是否激活")
|
||||
|
||||
|
||||
class GlobalModelUpdate(BaseModel):
|
||||
"""更新 GlobalModel 请求"""
|
||||
|
||||
display_name: str | None = Field(None, min_length=1, max_length=100)
|
||||
is_active: bool | None = None
|
||||
# 按次计费配置
|
||||
default_price_per_request: float | None = Field(None, ge=0, description="每次请求固定费用")
|
||||
# 阶梯计费配置
|
||||
default_tiered_pricing: TieredPricingConfig | None = Field(None, description="阶梯计费配置")
|
||||
# Key 能力配置 - 模型支持的能力列表(如 ["cache_1h", "context_1m"])
|
||||
supported_capabilities: list[str] | None = Field(None, description="支持的 Key 能力列表")
|
||||
# 模型配置(JSON格式)- 包含能力、规格、元信息等
|
||||
config: dict[str, Any] | None = Field(
|
||||
None, description="模型配置(streaming, vision, context_limit, description 等)"
|
||||
)
|
||||
|
||||
|
||||
class GlobalModelResponse(BaseModel):
|
||||
"""GlobalModel 响应"""
|
||||
|
||||
id: str
|
||||
name: str
|
||||
display_name: str
|
||||
is_active: bool
|
||||
# 按次计费配置
|
||||
default_price_per_request: float | None = Field(None, description="每次请求固定费用")
|
||||
# 阶梯计费配置
|
||||
default_tiered_pricing: TieredPricingConfig | None = Field(
|
||||
default=None, description="阶梯计费配置"
|
||||
)
|
||||
# Key 能力配置 - 模型支持的能力列表
|
||||
supported_capabilities: list[str] | None = Field(
|
||||
default=None, description="支持的 Key 能力列表"
|
||||
)
|
||||
# 模型配置(JSON格式)
|
||||
config: dict[str, Any] | None = Field(
|
||||
default=None, description="模型配置(streaming, vision, context_limit, description 等)"
|
||||
)
|
||||
# 统计数据(可选)
|
||||
provider_count: int | None = Field(default=0, description="支持的 Provider 总数量")
|
||||
active_provider_count: int | None = Field(default=0, description="可用的 Provider 数量")
|
||||
usage_count: int | None = Field(default=0, description="调用次数")
|
||||
created_at: datetime
|
||||
updated_at: datetime | None
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class GlobalModelWithStats(GlobalModelResponse):
|
||||
"""带统计信息的 GlobalModel"""
|
||||
|
||||
total_models: int = Field(..., description="关联的 Model 数量")
|
||||
total_providers: int = Field(..., description="支持的 Provider 数量")
|
||||
price_range: ModelPriceRange
|
||||
|
||||
|
||||
class GlobalModelListResponse(BaseModel):
|
||||
"""GlobalModel 列表响应"""
|
||||
|
||||
models: list[GlobalModelResponse]
|
||||
total: int
|
||||
|
||||
|
||||
class GlobalModelProvidersResponse(BaseModel):
|
||||
"""GlobalModel 关联提供商列表响应"""
|
||||
|
||||
providers: list[ModelCatalogProviderDetail]
|
||||
total: int
|
||||
|
||||
|
||||
class BatchAssignToProvidersRequest(BaseModel):
|
||||
"""批量为 Provider 添加 GlobalModel 实现"""
|
||||
|
||||
provider_ids: list[str] = Field(..., min_length=1, description="Provider ID 列表")
|
||||
create_models: bool = Field(default=False, description="是否自动创建 Model 记录")
|
||||
|
||||
|
||||
class BatchAssignToProvidersResponse(BaseModel):
|
||||
"""批量分配响应"""
|
||||
|
||||
success: list[dict]
|
||||
errors: list[dict]
|
||||
|
||||
|
||||
class BatchAssignModelsToProviderRequest(BaseModel):
|
||||
"""批量为 Provider 关联 GlobalModel"""
|
||||
|
||||
global_model_ids: list[str] = Field(..., min_length=1, description="GlobalModel ID 列表")
|
||||
|
||||
|
||||
class BatchAssignModelsToProviderResponse(BaseModel):
|
||||
"""批量关联 GlobalModel 到 Provider 的响应"""
|
||||
|
||||
success: list[dict]
|
||||
errors: list[dict]
|
||||
|
||||
|
||||
class ImportFromUpstreamRequest(BaseModel):
|
||||
"""从上游提供商导入模型请求"""
|
||||
|
||||
model_ids: list[str] = Field(..., min_length=1, description="上游模型 ID 列表")
|
||||
# 价格覆盖配置(应用于所有导入的模型)
|
||||
tiered_pricing: dict | None = Field(
|
||||
None,
|
||||
description="阶梯计费配置(可选),格式: {tiers: [{up_to, input_price_per_1m, output_price_per_1m, ...}]}",
|
||||
)
|
||||
price_per_request: float | None = Field(
|
||||
None, ge=0, description="按次计费价格(可选,单位:美元)"
|
||||
)
|
||||
|
||||
|
||||
class ImportFromUpstreamSuccessItem(BaseModel):
|
||||
"""导入成功的模型信息"""
|
||||
|
||||
model_id: str = Field(..., description="上游模型 ID")
|
||||
provider_model_id: str = Field(..., description="Provider Model ID")
|
||||
global_model_id: str = Field(..., description="GlobalModel ID")
|
||||
global_model_name: str = Field(..., description="GlobalModel 名称")
|
||||
created_global_model: bool = Field(
|
||||
False, description="是否新创建了 GlobalModel(始终为 false)"
|
||||
)
|
||||
|
||||
|
||||
class ImportFromUpstreamErrorItem(BaseModel):
|
||||
"""导入失败的模型信息"""
|
||||
|
||||
model_id: str = Field(..., description="上游模型 ID")
|
||||
error: str = Field(..., description="错误信息")
|
||||
|
||||
|
||||
class ImportFromUpstreamResponse(BaseModel):
|
||||
"""从上游提供商导入模型响应"""
|
||||
|
||||
success: list[ImportFromUpstreamSuccessItem]
|
||||
errors: list[ImportFromUpstreamErrorItem]
|
||||
|
||||
|
||||
__all__ = [
|
||||
"BatchAssignModelsToProviderRequest",
|
||||
"BatchAssignModelsToProviderResponse",
|
||||
"BatchAssignToProvidersRequest",
|
||||
"BatchAssignToProvidersResponse",
|
||||
"GlobalModelCreate",
|
||||
"GlobalModelListResponse",
|
||||
"GlobalModelResponse",
|
||||
"GlobalModelUpdate",
|
||||
"GlobalModelWithStats",
|
||||
"ImportFromUpstreamErrorItem",
|
||||
"ImportFromUpstreamRequest",
|
||||
"ImportFromUpstreamResponse",
|
||||
"ImportFromUpstreamSuccessItem",
|
||||
"ModelCapabilities",
|
||||
"ModelCatalogItem",
|
||||
"ModelCatalogProviderDetail",
|
||||
"ModelCatalogResponse",
|
||||
"ModelPriceRange",
|
||||
"ProviderAvailableSourceModel",
|
||||
"ProviderAvailableSourceModelsResponse",
|
||||
"ProviderModelPriceInfo",
|
||||
]
|
||||
40
_deprecated_py_src/models/status_snapshot.py
Normal file
40
_deprecated_py_src/models/status_snapshot.py
Normal file
@@ -0,0 +1,40 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class OAuthStatusSnapshotResponse(BaseModel):
|
||||
code: str = Field(default="none", description="OAuth 状态代码")
|
||||
label: str | None = Field(default=None, description="OAuth 状态标签")
|
||||
reason: str | None = Field(default=None, description="OAuth 状态原因")
|
||||
expires_at: int | None = Field(default=None, description="OAuth 过期时间(Unix 时间戳)")
|
||||
invalid_at: int | None = Field(default=None, description="OAuth 失效时间(Unix 时间戳)")
|
||||
source: str | None = Field(default=None, description="OAuth 状态来源")
|
||||
requires_reauth: bool = Field(default=False, description="是否需要重新授权")
|
||||
expiring_soon: bool = Field(default=False, description="是否即将过期")
|
||||
|
||||
|
||||
class AccountStatusSnapshotResponse(BaseModel):
|
||||
code: str = Field(default="ok", description="账号状态代码")
|
||||
label: str | None = Field(default=None, description="账号状态标签")
|
||||
reason: str | None = Field(default=None, description="账号状态原因")
|
||||
blocked: bool = Field(default=False, description="是否为账号级阻塞")
|
||||
source: str | None = Field(default=None, description="账号状态来源")
|
||||
recoverable: bool = Field(default=False, description="是否为可恢复状态")
|
||||
|
||||
|
||||
class QuotaStatusSnapshotResponse(BaseModel):
|
||||
code: str = Field(default="unknown", description="额度状态代码")
|
||||
label: str | None = Field(default=None, description="额度状态标签")
|
||||
reason: str | None = Field(default=None, description="额度状态原因")
|
||||
exhausted: bool = Field(default=False, description="额度是否耗尽")
|
||||
usage_ratio: float | None = Field(default=None, description="额度使用比例 [0, 1]")
|
||||
updated_at: int | None = Field(default=None, description="额度刷新时间(Unix 时间戳)")
|
||||
reset_seconds: float | None = Field(default=None, description="距离重置剩余秒数")
|
||||
plan_type: str | None = Field(default=None, description="额度读取到的套餐类型")
|
||||
|
||||
|
||||
class ProviderKeyStatusSnapshotResponse(BaseModel):
|
||||
oauth: OAuthStatusSnapshotResponse = Field(default_factory=OAuthStatusSnapshotResponse)
|
||||
account: AccountStatusSnapshotResponse = Field(default_factory=AccountStatusSnapshotResponse)
|
||||
quota: QuotaStatusSnapshotResponse = Field(default_factory=QuotaStatusSnapshotResponse)
|
||||
Reference in New Issue
Block a user