Files
Aether/_deprecated_py_src/models/database.py
fawney19 1d9c77522a 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)
2026-04-03 16:26:16 +08:00

3255 lines
126 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
数据库模型定义
"""
from __future__ import annotations
import hashlib
import secrets
import uuid
from datetime import date, datetime, timezone
from enum import Enum as PyEnum
from typing import Any, ClassVar
import bcrypt
from sqlalchemy import (
JSON,
BigInteger,
Boolean,
CheckConstraint,
Column,
Date,
DateTime,
Enum,
Float,
ForeignKey,
Index,
Integer,
LargeBinary,
Numeric,
String,
Text,
UniqueConstraint,
event,
inspect,
text,
)
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.orm import backref, declarative_base, relationship
from ..config import config
from ..core.enums import AuthSource, ProviderBillingType, UserRole
Base = declarative_base()
class ExportMixin:
"""配置导出 Mixin -- 基于排除列表自动收集字段。"""
_export_exclude: ClassVar[frozenset[str]] = frozenset()
def to_export_dict(self) -> dict[str, Any]:
"""将模型实例转为可导出的字典(排除 _export_exclude 中的字段)。"""
result: dict[str, Any] = {}
for col in self.__table__.columns: # type: ignore[attr-defined]
if col.name in self._export_exclude:
continue
value = getattr(self, col.name)
if isinstance(value, PyEnum):
value = value.value
result[col.name] = value
return result
@classmethod
def get_export_fields(cls) -> frozenset[str]:
"""返回可导出字段名集合。"""
return frozenset(
col.name
for col in cls.__table__.columns # type: ignore[attr-defined]
if col.name not in cls._export_exclude
)
class User(Base):
"""用户模型"""
__tablename__ = "users"
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
# OAuth 用户可能没有邮箱Postgres unique 允许多个 NULL
email = Column(String(255), unique=True, index=True, nullable=True)
# 注意:所有创建用户的入口必须显式写入 true/false禁止依赖默认值
email_verified = Column(Boolean, nullable=False)
username = Column(String(100), unique=True, index=True, nullable=False)
# OAuth 用户可能没有本地密码
password_hash = Column(String(255), nullable=True)
role = Column(
Enum(
UserRole,
name="userrole",
create_type=False,
values_callable=lambda x: [e.value for e in x],
),
default=UserRole.USER,
nullable=False,
)
auth_source = Column(
Enum(
AuthSource,
name="authsource",
create_type=False,
values_callable=lambda x: [e.value for e in x],
),
default=AuthSource.LOCAL,
nullable=False,
)
# LDAP 标识(仅 auth_source=ldap 时使用,用于在邮箱变更/用户名冲突时稳定关联本地账户)
ldap_dn = Column(String(512), nullable=True, index=True)
ldap_username = Column(String(255), nullable=True, index=True)
# 访问限制NULL 表示不限制,允许访问所有资源)
allowed_providers = Column(JSON, nullable=True) # 允许使用的提供商 ID 列表
allowed_api_formats = Column(JSON, nullable=True) # 允许使用的 API 格式列表
allowed_models = Column(JSON, nullable=True) # 允许使用的模型名称列表
rate_limit = Column(
Integer, nullable=True, default=None
) # 每分钟请求限制NULL=继承系统默认0=不限制N=N RPM
# Key 能力配置
model_capability_settings = Column(JSON, nullable=True) # 用户针对特定模型的能力配置
# 示例: {"claude-sonnet-4-20250514": {"cache_1h": true}}
# 状态
is_active = Column(Boolean, default=True, nullable=False)
is_deleted = Column(Boolean, default=False, 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,
)
last_login_at = Column(DateTime(timezone=True), nullable=True)
# 关系 - CASCADE delete: 让数据库处理级联删除
api_keys = relationship("ApiKey", back_populates="user", cascade="all, delete-orphan")
management_tokens = relationship(
"ManagementToken", back_populates="user", cascade="all, delete-orphan"
)
sessions = relationship("UserSession", back_populates="user", cascade="all, delete-orphan")
preferences = relationship(
"UserPreference", back_populates="user", cascade="all, delete-orphan", passive_deletes=True
)
announcement_reads = relationship(
"AnnouncementRead",
back_populates="user",
cascade="all, delete-orphan",
passive_deletes=True,
)
# 关系 - SET NULL: 保留历史记录,让数据库处理 SET NULL
usage_records = relationship("Usage", back_populates="user", passive_deletes=True)
wallet = relationship("Wallet", back_populates="user", uselist=False, passive_deletes=True)
payment_orders = relationship("PaymentOrder", back_populates="user", passive_deletes=True)
refund_requests = relationship(
"RefundRequest",
back_populates="user",
passive_deletes=True,
foreign_keys="RefundRequest.user_id",
)
authored_announcements = relationship(
"Announcement",
back_populates="author",
foreign_keys="Announcement.author_id",
passive_deletes=True,
)
audit_logs = relationship("AuditLog", back_populates="user", passive_deletes=True)
def set_password(self, password: str) -> None:
"""设置密码"""
try:
self.password_hash = bcrypt.hashpw(password.encode("utf-8"), bcrypt.gensalt()).decode(
"utf-8"
)
except ValueError as exc:
raise ValueError("密码长度不能超过72字节") from exc
def verify_password(self, password: str) -> bool:
"""验证密码"""
if not self.password_hash:
return False
try:
return bcrypt.checkpw(password.encode("utf-8"), self.password_hash.encode("utf-8"))
except ValueError:
return False
class UserSession(Base):
"""用户登录会话(设备级)。"""
__tablename__ = "user_sessions"
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
user_id = Column(
String(36), ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True
)
# 客户端标识
client_device_id = Column(String(128), nullable=False, index=True)
device_label = Column(String(120), nullable=True)
device_type = Column(String(20), nullable=False, default="unknown")
browser_name = Column(String(50), nullable=True)
browser_version = Column(String(50), nullable=True)
os_name = Column(String(50), nullable=True)
os_version = Column(String(50), nullable=True)
device_model = Column(String(100), nullable=True)
# 请求上下文
ip_address = Column(String(45), nullable=True)
user_agent = Column(String(1000), nullable=True)
client_hints = Column(JSON, nullable=True)
# Refresh Token 仅存储哈希,不保存明文
refresh_token_hash = Column(String(64), nullable=False)
# 上一个 refresh token 的哈希,用于并发刷新的宽限窗口
prev_refresh_token_hash = Column(String(64), nullable=True)
rotated_at = Column(DateTime(timezone=True), nullable=True)
# 状态
last_seen_at = Column(
DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), nullable=False
)
expires_at = Column(DateTime(timezone=True), nullable=False)
revoked_at = Column(DateTime(timezone=True), nullable=True)
revoke_reason = Column(String(100), nullable=True)
# 时间戳
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,
)
# 关系
user = relationship("User", back_populates="sessions")
__table_args__ = (
Index("idx_user_sessions_user_active", "user_id", "revoked_at", "expires_at"),
Index("idx_user_sessions_user_device", "user_id", "client_device_id"),
)
@staticmethod
def hash_refresh_token(token: str) -> str:
"""对 refresh token 做 SHA256 哈希。"""
return hashlib.sha256(token.encode()).hexdigest()
def set_refresh_token(self, token: str) -> None:
"""设置 refresh token 哈希,同时保存上一个哈希用于并发宽限。"""
self.prev_refresh_token_hash = self.refresh_token_hash
self.rotated_at = datetime.now(timezone.utc)
self.refresh_token_hash = self.hash_refresh_token(token)
# 并发刷新宽限窗口(秒)
REFRESH_GRACE_SECONDS = 10
def verify_refresh_token(self, token: str) -> tuple[bool, bool]:
"""校验 refresh token 是否匹配(常量时间比较)。
Returns:
(is_valid, is_prev): is_valid 表示 token 是否通过校验,
is_prev 表示匹配的是上一个宽限窗口内的旧token。
"""
import hmac
token_hash = self.hash_refresh_token(token)
current_hash: str | None = self.refresh_token_hash
if current_hash and hmac.compare_digest(current_hash, token_hash):
return True, False
# 检查宽限窗口内的上一个 token
prev_hash: str | None = self.prev_refresh_token_hash
rotated_at: datetime | None = self.rotated_at
if prev_hash and rotated_at:
if rotated_at.tzinfo is None:
rotated_at = rotated_at.replace(tzinfo=timezone.utc)
elapsed = (datetime.now(timezone.utc) - rotated_at).total_seconds()
if elapsed <= self.REFRESH_GRACE_SECONDS:
if hmac.compare_digest(prev_hash, token_hash):
return True, True
return False, False
@property
def is_revoked(self) -> bool:
return self.revoked_at is not None
@property
def is_expired(self) -> bool:
expires_at: datetime | None = self.expires_at
if expires_at is None:
return True
if expires_at.tzinfo is None:
expires_at = expires_at.replace(tzinfo=timezone.utc)
return expires_at <= datetime.now(timezone.utc)
class ApiKey(Base):
"""API密钥模型"""
__tablename__ = "api_keys"
__table_args__ = (
CheckConstraint(
"(NOT is_standalone) OR (NOT is_locked)",
name="ck_api_keys_standalone_not_locked",
),
)
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
user_id = Column(
String(36), ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True
)
key_hash = Column(String(64), unique=True, index=True, nullable=False) # API密钥的SHA256哈希
key_encrypted = Column(Text, nullable=True) # 加密后的完整密钥,用于查看
name = Column(String(100), nullable=True) # 密钥名称,便于用户管理
# 使用统计
total_requests = Column(Integer, default=0)
total_cost_usd = Column(Numeric(20, 8), default=0.0)
# 钱包体系:余额/额度由 wallets 表统一管理
is_standalone = Column(
Boolean, default=False, nullable=False
) # 是否为独立余额 Key给非注册用户使用
# 访问限制NULL 表示不限制,允许访问所有资源)
allowed_providers = Column(JSON, nullable=True) # 允许使用的提供商 ID 列表
allowed_api_formats = Column(JSON, nullable=True) # 允许使用的 API 格式列表
allowed_models = Column(JSON, nullable=True) # 允许使用的模型名称列表
rate_limit = Column(
Integer, default=None, nullable=True
) # 每分钟请求限制独立Key: NULL=继承系统默认普通Key: 0=不限制
concurrent_limit = Column(Integer, default=5, nullable=True) # 并发请求限制
# Key 能力配置
force_capabilities = Column(JSON, nullable=True) # 强制开启的能力
# 示例: {"cache_1h": true} - 强制所有支持的模型都用 1h 缓存
# 状态
is_active = Column(Boolean, default=True, nullable=False)
is_locked = Column(Boolean, default=False, nullable=False) # 仅普通用户Key可锁定
last_used_at = Column(DateTime(timezone=True), nullable=True)
expires_at = Column(DateTime(timezone=True), nullable=True) # 过期时间
auto_delete_on_expiry = Column(Boolean, default=False, 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,
)
# 关系
user = relationship("User", back_populates="api_keys")
usage_records = relationship("Usage", back_populates="api_key", passive_deletes=True)
wallet = relationship("Wallet", back_populates="api_key", uselist=False, passive_deletes=True)
provider_mappings = relationship(
"ApiKeyProviderMapping", back_populates="api_key", cascade="all, delete-orphan"
)
@staticmethod
def generate_key() -> str:
"""生成API密钥使用加密安全的随机数生成器"""
import string
# 只使用字母和数字,避免特殊字符
alphabet = string.ascii_letters + string.digits
random_part = "".join(secrets.choice(alphabet) for _ in range(32))
return f"{config.api_key_prefix}-{random_part}"
@staticmethod
def hash_key(api_key: str) -> str:
"""对API密钥进行哈希"""
return hashlib.sha256(api_key.encode()).hexdigest()
def set_key(self, api_key: str) -> None:
"""
设置API密钥(用于测试和数据初始化)
Args:
api_key: 明文API密钥
注意: 此方法会设置 key_hash 和 key_encrypted
"""
from src.core.crypto import crypto_service
# 设置哈希(用于验证)
self.key_hash = self.hash_key(api_key)
# 设置加密的完整密钥(用于显示和管理)
self.key_encrypted = crypto_service.encrypt(api_key)
def verify_key(self, api_key: str) -> bool:
"""
验证API密钥是否匹配(用于测试)
Args:
api_key: 明文API密钥
Returns:
bool: 密钥是否匹配
"""
return self.key_hash == self.hash_key(api_key)
def get_display_key(self) -> str:
"""获取用于显示的脱敏密钥(前缀...后4位"""
from src.core.crypto import crypto_service
if self.key_encrypted:
try:
# 使用静默模式,避免在显示场景打印错误日志
full_key = crypto_service.decrypt(self.key_encrypted, silent=True)
# 格式sk-SpJ3y...sdf4
prefix = full_key[:10] if len(full_key) >= 10 else full_key[: len(full_key) // 2]
suffix = full_key[-4:] if len(full_key) >= 4 else ""
return f"{prefix}...{suffix}"
except Exception:
pass
# 降级:无法解密时返回占位符
return "sk-****"
class Usage(Base):
"""使用记录模型"""
__tablename__ = "usage"
__table_args__ = (
# Composite indexes for common query patterns (analytics / list pages)
Index("idx_usage_user_created", "user_id", "created_at"),
Index("idx_usage_status_user_created", "status", "user_id", "created_at"),
Index("idx_usage_apikey_created", "api_key_id", "created_at"),
Index("idx_usage_provider_model_created", "provider_name", "model", "created_at"),
Index("idx_usage_provider_created", "provider_name", "created_at"),
Index("idx_usage_model_created", "model", "created_at"),
Index("idx_usage_provider_key", "provider_id", "provider_api_key_id"),
)
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
user_id = Column(String(36), ForeignKey("users.id", ondelete="SET NULL"), nullable=True)
api_key_id = Column(String(36), ForeignKey("api_keys.id", ondelete="SET NULL"), nullable=True)
wallet_id = Column(
String(36), ForeignKey("wallets.id", ondelete="SET NULL"), nullable=True, index=True
)
# 归属快照(删除用户/Key 后仍可追溯)
username = Column(String(100), nullable=True, comment="用户名快照")
api_key_name = Column(String(200), nullable=True, comment="API Key 名称快照")
# 请求信息
request_id = Column(String(100), unique=True, index=True, nullable=False)
provider_name = Column(String(100), nullable=False) # Provider 名称(非外键)
model = Column(String(100), nullable=False)
target_model = Column(
String(100), nullable=True, comment="映射后的目标模型名(若无映射则为空)"
)
# Provider 侧追踪信息(记录最终成功的 Provider/Endpoint/Key
provider_id = Column(String(36), ForeignKey("providers.id", ondelete="SET NULL"), nullable=True)
provider_endpoint_id = Column(
String(36),
ForeignKey("provider_endpoints.id", ondelete="SET NULL"),
nullable=True,
index=True,
)
provider_api_key_id = Column(
String(36),
ForeignKey("provider_api_keys.id", ondelete="SET NULL"),
nullable=True,
index=True,
)
# Token统计
input_tokens = Column(Integer, default=0)
output_tokens = Column(Integer, default=0)
total_tokens = Column(Integer, default=0)
# 缓存相关 tokens (for Claude models)
cache_creation_input_tokens = Column(Integer, default=0)
cache_read_input_tokens = Column(Integer, default=0)
cache_creation_input_tokens_5m = Column(Integer, default=0) # 5min TTL 缓存创建
cache_creation_input_tokens_1h = Column(Integer, default=0) # 1h TTL 缓存创建
# 成本计算
input_cost_usd = Column(Numeric(20, 8), default=0.0)
output_cost_usd = Column(Numeric(20, 8), default=0.0)
cache_cost_usd = Column(Numeric(20, 8), default=0.0) # 总缓存成本
cache_creation_cost_usd = Column(Numeric(20, 8), default=0.0) # 缓存创建成本
cache_read_cost_usd = Column(Numeric(20, 8), default=0.0) # 缓存读取成本
request_cost_usd = Column(Numeric(20, 8), default=0.0) # 按次计费成本
total_cost_usd = Column(Numeric(20, 8), default=0.0)
# 真实成本计算(表面成本 × 倍率)
actual_input_cost_usd = Column(Numeric(20, 8), default=0.0) # 真实输入成本
actual_output_cost_usd = Column(Numeric(20, 8), default=0.0) # 真实输出成本
actual_cache_creation_cost_usd = Column(Numeric(20, 8), default=0.0) # 真实缓存创建成本
actual_cache_read_cost_usd = Column(Numeric(20, 8), default=0.0) # 真实缓存读取成本
actual_request_cost_usd = Column(Numeric(20, 8), default=0.0) # 真实按次计费成本
actual_total_cost_usd = Column(Numeric(20, 8), default=0.0) # 真实总成本
rate_multiplier = Column(Numeric(10, 6), default=1.0) # 使用的倍率(来自 ProviderAPIKey
# 历史价格记录每1M tokens的美元价格记录请求时的实际价格
input_price_per_1m = Column(Numeric(20, 8), nullable=True) # 输入单价
output_price_per_1m = Column(Numeric(20, 8), nullable=True) # 输出单价
cache_creation_price_per_1m = Column(Numeric(20, 8), nullable=True) # 缓存创建单价
cache_read_price_per_1m = Column(Numeric(20, 8), nullable=True) # 缓存读取单价
price_per_request = Column(Numeric(20, 8), nullable=True) # 按次计费单价(历史记录)
# 请求详情
request_type = Column(String(50)) # chat, completion, embedding等
api_format = Column(String(50), nullable=True) # API 格式: CLAUDE, OPENAI 等(用户请求格式)
api_family = Column(String(50), nullable=True) # 协议族: claude, openai, gemini
endpoint_kind = Column(String(50), nullable=True) # 端点类型: chat, cli, video
endpoint_api_format = Column(String(50), nullable=True) # 端点原生 API 格式
provider_api_family = Column(String(50), nullable=True) # 提供商协议族
provider_endpoint_kind = Column(String(50), nullable=True) # 提供商端点类型
has_format_conversion = Column(Boolean, nullable=True, default=False) # 是否发生了格式转换
is_stream = Column(Boolean, default=False) # 是否为流式请求
status_code = Column(Integer)
error_message = Column(Text, nullable=True)
error_category = Column(String(50), nullable=True, index=True)
response_time_ms = Column(Integer) # 总响应时间(毫秒)
first_byte_time_ms = Column(Integer, nullable=True) # 首字时间/TTFB毫秒
# 请求状态追踪
# pending: 请求开始处理中
# streaming: 流式响应进行中
# completed: 请求成功完成
# failed: 请求失败
# cancelled: 客户端主动断开连接
status = Column(String(20), default="completed", nullable=False, index=True)
# 结算状态(与 status 解耦)
# - pending: 等待结算(请求已创建,但账务尚未进入最终状态)
# - settled: 已结算cost 已写入,且钱包侧结算动作已完成)
# - void: 作废(明确不收费)
billing_status = Column(String(20), default="pending", nullable=False, index=True)
finalized_at = Column(DateTime(timezone=True), nullable=True) # 结算完成时间(可选)
wallet_balance_before = Column(Numeric(20, 8), nullable=True) # 结算前可用总余额快照
wallet_balance_after = Column(Numeric(20, 8), nullable=True) # 结算后可用总余额快照
wallet_recharge_balance_before = Column(Numeric(20, 8), nullable=True) # 结算前充值余额
wallet_recharge_balance_after = Column(Numeric(20, 8), nullable=True) # 结算后充值余额
wallet_gift_balance_before = Column(Numeric(20, 8), nullable=True) # 结算前赠款余额
wallet_gift_balance_after = Column(Numeric(20, 8), nullable=True) # 结算后赠款余额
# 完整请求和响应记录
request_headers = Column(JSON, nullable=True) # 客户端请求头
request_body = Column(JSON, nullable=True) # 客户端原始请求体7天内未压缩
provider_request_headers = Column(JSON, nullable=True) # 向提供商发送的请求头
provider_request_body = Column(JSON, nullable=True) # 发给提供商的请求体(格式转换后)
response_headers = Column(JSON, nullable=True) # 提供商响应头
response_body = Column(JSON, nullable=True) # 提供商原始响应体7天内未压缩
client_response_headers = Column(JSON, nullable=True) # 返回给客户端的响应头
client_response_body = Column(JSON, nullable=True) # 返回给客户端的响应体(格式转换后)
# 压缩存储字段7天后自动压缩到这里
request_body_compressed = Column(LargeBinary, nullable=True) # gzip压缩的客户端请求体
provider_request_body_compressed = Column(LargeBinary, nullable=True) # gzip压缩的提供商请求体
response_body_compressed = Column(LargeBinary, nullable=True) # gzip压缩的提供商响应体
client_response_body_compressed = Column(LargeBinary, nullable=True) # gzip压缩的客户端响应体
# 元数据
request_metadata = Column(JSON, nullable=True) # 存储额外信息
# 时间戳
created_at = Column(
DateTime(timezone=True),
default=lambda: datetime.now(timezone.utc),
nullable=False,
index=True,
)
# 关系
user = relationship("User", back_populates="usage_records")
api_key = relationship("ApiKey", back_populates="usage_records")
wallet = relationship("Wallet", back_populates="usage_records")
provider_obj = relationship("Provider") # 使用 provider_obj 避免与 provider 字段名冲突
provider_endpoint = relationship("ProviderEndpoint")
provider_api_key = relationship("ProviderAPIKey")
def get_request_body(self) -> Any:
"""获取客户端原始请求体(自动解压)"""
if self.request_body is not None:
return self.request_body
if self.request_body_compressed is not None:
from src.utils.compression import decompress_json
return decompress_json(self.request_body_compressed)
return None
def get_provider_request_body(self) -> Any:
"""获取发给提供商的请求体(自动解压)"""
if self.provider_request_body is not None:
return self.provider_request_body
if self.provider_request_body_compressed is not None:
from src.utils.compression import decompress_json
return decompress_json(self.provider_request_body_compressed)
return None
def get_response_body(self) -> Any:
"""获取提供商原始响应体(自动解压)"""
if self.response_body is not None:
return self.response_body
if self.response_body_compressed is not None:
from src.utils.compression import decompress_json
return decompress_json(self.response_body_compressed)
return None
def get_client_response_body(self) -> Any:
"""获取返回给客户端的响应体(自动解压)"""
if self.client_response_body is not None:
return self.client_response_body
if self.client_response_body_compressed is not None:
from src.utils.compression import decompress_json
return decompress_json(self.client_response_body_compressed)
return None
class Wallet(Base):
"""统一钱包模型(用户钱包 / 独立 API Key 钱包)"""
__tablename__ = "wallets"
__table_args__ = (
CheckConstraint(
# 活跃钱包必须归属唯一 ownerowner 被删除后允许双 NULL孤立钱包由清理策略回收
"(user_id IS NOT NULL AND api_key_id IS NULL) "
"OR (user_id IS NULL AND api_key_id IS NOT NULL) "
"OR (user_id IS NULL AND api_key_id IS NULL)",
name="ck_wallet_single_owner",
),
CheckConstraint("gift_balance >= 0", name="ck_wallets_gift_balance_non_negative"),
# user_id/api_key_id 的 unique=True 已隐含唯一索引,无需额外 Index
Index("idx_wallets_status", "status"),
)
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
user_id = Column(
String(36), ForeignKey("users.id", ondelete="SET NULL"), nullable=True, unique=True
)
api_key_id = Column(
String(36), ForeignKey("api_keys.id", ondelete="SET NULL"), nullable=True, unique=True
)
# balance: 充值余额(可退款余额)
balance = Column(Numeric(20, 8), nullable=False, default=0)
# gift_balance: 赠款余额(不可退款)
gift_balance = Column(Numeric(20, 8), nullable=False, default=0)
# finite: 按余额校验unlimited: 忽略余额放行,但仍统计消费
limit_mode = Column(String(20), nullable=False, default="finite")
currency = Column(String(3), nullable=False, default="USD")
status = Column(String(20), nullable=False, default="active")
total_recharged = Column(Numeric(20, 8), nullable=False, default=0)
total_consumed = Column(Numeric(20, 8), nullable=False, default=0)
total_refunded = Column(Numeric(20, 8), nullable=False, default=0)
total_adjusted = Column(Numeric(20, 8), nullable=False, default=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,
)
user = relationship("User", back_populates="wallet")
api_key = relationship("ApiKey", back_populates="wallet")
usage_records = relationship("Usage", back_populates="wallet")
transactions = relationship(
"WalletTransaction", back_populates="wallet", cascade="all, delete-orphan"
)
daily_usage_ledgers = relationship(
"WalletDailyUsageLedger", back_populates="wallet", cascade="all, delete-orphan"
)
payment_orders = relationship("PaymentOrder", back_populates="wallet")
refund_requests = relationship("RefundRequest", back_populates="wallet")
class WalletTransaction(Base):
"""钱包资金流水(只记录资金动作,不重复记录每次请求消费)"""
__tablename__ = "wallet_transactions"
__table_args__ = (
CheckConstraint(
"balance_before = recharge_balance_before + gift_balance_before",
name="ck_wallet_tx_balance_before_consistent",
),
CheckConstraint(
"balance_after = recharge_balance_after + gift_balance_after",
name="ck_wallet_tx_balance_after_consistent",
),
Index("idx_wallet_tx_wallet_created", "wallet_id", "created_at"),
Index("idx_wallet_tx_link", "link_type", "link_id"),
Index("idx_wallet_tx_category_created", "category", "created_at"),
Index("idx_wallet_tx_reason_created", "reason_code", "created_at"),
)
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
wallet_id = Column(String(36), ForeignKey("wallets.id", ondelete="CASCADE"), nullable=False)
category = Column(String(20), nullable=False)
reason_code = Column(String(40), nullable=False)
amount = Column(Numeric(20, 8), nullable=False)
# 总可用余额(充值+赠款)快照
balance_before = Column(Numeric(20, 8), nullable=False)
balance_after = Column(Numeric(20, 8), nullable=False)
# 分账户快照(审计用)
recharge_balance_before = Column(Numeric(20, 8), nullable=False)
recharge_balance_after = Column(Numeric(20, 8), nullable=False)
gift_balance_before = Column(Numeric(20, 8), nullable=False)
gift_balance_after = Column(Numeric(20, 8), nullable=False)
link_type = Column(String(30), nullable=True)
link_id = Column(String(100), nullable=True)
operator_id = Column(
String(36), ForeignKey("users.id", ondelete="SET NULL"), nullable=True, index=True
)
description = Column(Text, nullable=True)
created_at = Column(
DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), nullable=False
)
wallet = relationship("Wallet", back_populates="transactions")
operator = relationship("User")
class WalletDailyUsageLedger(Base):
"""钱包按天汇总的消费流水投影。"""
__tablename__ = "wallet_daily_usage_ledgers"
__table_args__ = (
UniqueConstraint(
"wallet_id",
"billing_date",
"billing_timezone",
name="uq_wallet_daily_usage_ledgers_wallet_date_tz",
),
Index("idx_wallet_daily_usage_wallet_date", "wallet_id", "billing_date"),
Index("idx_wallet_daily_usage_date", "billing_date"),
)
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
wallet_id = Column(String(36), ForeignKey("wallets.id", ondelete="CASCADE"), nullable=False)
billing_date = Column(Date, nullable=False)
billing_timezone = Column(String(64), nullable=False, default="Asia/Shanghai")
total_cost_usd = Column(Numeric(20, 8), nullable=False, default=0)
total_requests = Column(Integer, nullable=False, default=0)
input_tokens = Column(BigInteger, nullable=False, default=0)
output_tokens = Column(BigInteger, nullable=False, default=0)
cache_creation_tokens = Column(BigInteger, nullable=False, default=0)
cache_read_tokens = Column(BigInteger, nullable=False, default=0)
first_finalized_at = Column(DateTime(timezone=True), nullable=True)
last_finalized_at = Column(DateTime(timezone=True), nullable=True)
aggregated_at = Column(DateTime(timezone=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,
)
wallet = relationship("Wallet", back_populates="daily_usage_ledgers")
class PaymentOrder(Base):
"""充值订单"""
__tablename__ = "payment_orders"
__table_args__ = (
UniqueConstraint("order_no", name="uq_payment_orders_order_no"),
Index("idx_payment_orders_wallet_created", "wallet_id", "created_at"),
Index("idx_payment_orders_user_created", "user_id", "created_at"),
Index("idx_payment_orders_status", "status"),
Index("idx_payment_orders_gateway_order_id", "gateway_order_id"),
)
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
order_no = Column(String(64), nullable=False)
wallet_id = Column(String(36), ForeignKey("wallets.id", ondelete="RESTRICT"), nullable=False)
user_id = Column(String(36), ForeignKey("users.id", ondelete="SET NULL"), nullable=True)
amount_usd = Column(Numeric(20, 8), nullable=False)
pay_amount = Column(Numeric(20, 2), nullable=True)
pay_currency = Column(String(3), nullable=True)
exchange_rate = Column(Numeric(18, 8), nullable=True)
refunded_amount_usd = Column(Numeric(20, 8), nullable=False, default=0)
refundable_amount_usd = Column(Numeric(20, 8), nullable=False, default=0)
payment_method = Column(String(30), nullable=False)
gateway_order_id = Column(String(128), nullable=True)
gateway_response = Column(JSONB, nullable=True)
status = Column(String(20), nullable=False, default="pending")
created_at = Column(
DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), nullable=False
)
paid_at = Column(DateTime(timezone=True), nullable=True)
credited_at = Column(DateTime(timezone=True), nullable=True)
expires_at = Column(DateTime(timezone=True), nullable=True)
wallet = relationship("Wallet", back_populates="payment_orders")
user = relationship("User", back_populates="payment_orders")
callbacks = relationship("PaymentCallback", back_populates="payment_order")
refund_requests = relationship("RefundRequest", back_populates="payment_order")
class PaymentCallback(Base):
"""支付回调日志(幂等与审计)"""
__tablename__ = "payment_callbacks"
__table_args__ = (
UniqueConstraint("callback_key", name="uq_payment_callbacks_callback_key"),
Index("idx_payment_callbacks_order", "order_no"),
Index("idx_payment_callbacks_gateway_order", "gateway_order_id"),
Index("idx_payment_callbacks_created", "created_at"),
)
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
payment_order_id = Column(
String(36), ForeignKey("payment_orders.id", ondelete="SET NULL"), nullable=True, index=True
)
payment_method = Column(String(30), nullable=False)
callback_key = Column(String(128), nullable=False)
order_no = Column(String(64), nullable=True)
gateway_order_id = Column(String(128), nullable=True)
payload_hash = Column(String(128), nullable=True)
signature_valid = Column(Boolean, nullable=False, default=False)
status = Column(String(20), nullable=False, default="received")
payload = Column(JSONB, nullable=True)
error_message = Column(Text, nullable=True)
created_at = Column(
DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), nullable=False
)
processed_at = Column(DateTime(timezone=True), nullable=True)
payment_order = relationship("PaymentOrder", back_populates="callbacks")
class RefundRequest(Base):
"""退款申请(原路退款 / 非原路人工打款)"""
__tablename__ = "refund_requests"
__table_args__ = (
UniqueConstraint("refund_no", name="uq_refund_requests_refund_no"),
UniqueConstraint("idempotency_key", name="uq_refund_requests_idempotency_key"),
Index("idx_refund_wallet_created", "wallet_id", "created_at"),
Index("idx_refund_user_created", "user_id", "created_at"),
Index("idx_refund_status", "status"),
)
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
refund_no = Column(String(64), nullable=False)
wallet_id = Column(String(36), ForeignKey("wallets.id", ondelete="RESTRICT"), nullable=False)
user_id = Column(String(36), ForeignKey("users.id", ondelete="SET NULL"), nullable=True)
payment_order_id = Column(
String(36), ForeignKey("payment_orders.id", ondelete="SET NULL"), nullable=True, index=True
)
source_type = Column(String(30), nullable=False) # payment_order/manual_recharge/card_recharge
source_id = Column(String(100), nullable=True)
refund_mode = Column(String(30), nullable=False) # original_channel/offline_payout
amount_usd = Column(Numeric(20, 8), nullable=False)
status = Column(String(30), nullable=False, default="pending_approval")
reason = Column(Text, nullable=True)
requested_by = Column(
String(36), ForeignKey("users.id", ondelete="SET NULL"), nullable=True, index=True
)
approved_by = Column(
String(36), ForeignKey("users.id", ondelete="SET NULL"), nullable=True, index=True
)
processed_by = Column(
String(36), ForeignKey("users.id", ondelete="SET NULL"), nullable=True, index=True
)
gateway_refund_id = Column(String(128), nullable=True)
payout_method = Column(String(50), nullable=True)
payout_reference = Column(String(255), nullable=True)
payout_proof = Column(JSONB, nullable=True)
failure_reason = Column(Text, nullable=True)
idempotency_key = Column(String(128), nullable=True)
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,
)
processed_at = Column(DateTime(timezone=True), nullable=True)
completed_at = Column(DateTime(timezone=True), nullable=True)
wallet = relationship("Wallet", back_populates="refund_requests")
user = relationship("User", back_populates="refund_requests", foreign_keys=[user_id])
payment_order = relationship("PaymentOrder", back_populates="refund_requests")
class SystemConfig(Base):
"""系统配置表"""
__tablename__ = "system_configs"
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
key = Column(String(100), unique=True, nullable=False)
value = Column(JSON, nullable=False)
description = Column(Text, nullable=True)
# 时间戳
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,
)
class LDAPConfig(Base):
"""LDAP认证配置表 - 单行配置"""
__tablename__ = "ldap_configs"
id = Column(Integer, primary_key=True, autoincrement=True)
server_url = Column(String(255), nullable=False) # ldap://host:389 或 ldaps://host:636
bind_dn = Column(Text, nullable=False) # 绑定账号 DN可能很长
bind_password_encrypted = Column(Text, nullable=True) # 加密的绑定密码(允许 NULL 表示已清除)
base_dn = Column(Text, nullable=False) # 用户搜索基础 DN可能很长
user_search_filter = Column(
Text, default="(uid={username})", nullable=False
) # 用户搜索过滤器(可能很复杂)
username_attr = Column(
String(50), default="uid", nullable=False
) # 用户名属性 (uid/sAMAccountName)
email_attr = Column(String(50), default="mail", nullable=False) # 邮箱属性
display_name_attr = Column(String(50), default="cn", nullable=False) # 显示名称属性
is_enabled = Column(Boolean, default=False, nullable=False) # 是否启用 LDAP 认证
is_exclusive = Column(
Boolean, default=False, nullable=False
) # 是否仅允许 LDAP 登录(禁用本地认证)
use_starttls = Column(Boolean, default=False, nullable=False) # 是否使用 STARTTLS
connect_timeout = Column(Integer, default=10, 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,
)
def set_bind_password(self, password: str) -> None:
"""
设置并加密绑定密码
Args:
password: 明文密码
"""
from src.core.crypto import crypto_service
self.bind_password_encrypted = crypto_service.encrypt(password)
def get_bind_password(self) -> str:
"""
获取解密后的绑定密码
Returns:
str: 解密后的明文密码
Raises:
DecryptionException: 解密失败时抛出异常
"""
from src.core.crypto import crypto_service
if not self.bind_password_encrypted:
return ""
return crypto_service.decrypt(self.bind_password_encrypted)
class OAuthProvider(Base):
"""OAuth Provider 配置表(按 provider_type 唯一)"""
__tablename__ = "oauth_providers"
# 使用 provider_type 作为主键,便于通过 URL 参数直接定位配置
provider_type = Column(String(50), primary_key=True)
display_name = Column(String(100), nullable=False)
client_id = Column(Text, nullable=False) # 某些 OAuth 提供商可能使用很长的 client_id
client_secret_encrypted = Column(Text, nullable=True) # 允许 NULL 表示尚未配置/已清除
# 可选覆盖端点(需在业务层做白名单校验)
authorization_url_override = Column(String(500), nullable=True)
token_url_override = Column(String(500), nullable=True)
userinfo_url_override = Column(String(500), nullable=True)
# 可选覆盖 scopesJSON 列表)
scopes = Column(JSON, nullable=True)
# 服务端控制 redirect_uri 与前端回调 URL
redirect_uri = Column(String(500), nullable=False)
frontend_callback_url = Column(String(500), nullable=False)
# Provider 特定配置/映射
attribute_mapping = Column(JSON, nullable=True)
extra_config = Column(JSON, nullable=True)
is_enabled = Column(Boolean, default=False, 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,
)
def set_client_secret(self, secret: str) -> None:
"""设置并加密 client_secret"""
from src.core.crypto import crypto_service
self.client_secret_encrypted = crypto_service.encrypt(secret)
def get_client_secret(self) -> str:
"""获取解密后的 client_secret未配置时返回空串"""
from src.core.crypto import crypto_service
if not self.client_secret_encrypted:
return ""
return crypto_service.decrypt(self.client_secret_encrypted)
class UserOAuthLink(Base):
"""用户与 OAuth Provider 的绑定关系"""
__tablename__ = "user_oauth_links"
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
user_id = Column(
String(36),
ForeignKey("users.id", ondelete="CASCADE"),
nullable=False,
index=True,
)
provider_type = Column(
String(50),
ForeignKey("oauth_providers.provider_type", ondelete="CASCADE"),
nullable=False,
index=True,
)
provider_user_id = Column(String(255), nullable=False)
provider_username = Column(String(255), nullable=True)
provider_email = Column(String(255), nullable=True)
extra_data = Column(JSON, nullable=True)
linked_at = Column(
DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), nullable=False
)
last_login_at = Column(DateTime(timezone=True), nullable=True)
__table_args__ = (
UniqueConstraint("provider_type", "provider_user_id", name="uq_oauth_provider_user"),
UniqueConstraint("user_id", "provider_type", name="uq_user_oauth_provider"),
)
class Provider(ExportMixin, Base):
"""提供商配置表"""
__tablename__ = "providers"
_export_exclude = frozenset(
{
"id",
"monthly_used_usd",
"quota_last_reset_at",
"quota_expires_at",
"created_at",
"updated_at",
}
)
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
name = Column(String(100), unique=True, nullable=False, index=True) # 提供商名称(唯一)
description = Column(Text, nullable=True) # 提供商描述
website = Column(String(500), nullable=True) # 主站网站
# Provider 类型(用于模板化固定 Provider / 自定义 Provider
# - custom: 自定义
# - claude_code / codex / gemini_cli / antigravity: 固定类型
provider_type = Column(String(20), default="custom", nullable=False)
# 计费类型配置
billing_type = Column(
Enum(
ProviderBillingType,
name="providerbillingtype",
create_type=False,
values_callable=lambda x: [e.value for e in x],
),
default=ProviderBillingType.PAY_AS_YOU_GO,
nullable=False,
)
# 月卡配置
monthly_quota_usd = Column(Numeric(20, 8), nullable=True) # 月卡总额度
monthly_used_usd = Column(Numeric(20, 8), default=0.0) # 本月已用额度
quota_reset_day = Column(Integer, default=30) # 额度重置周期(天数)例如7=每周30=每月
quota_last_reset_at = Column(DateTime(timezone=True), nullable=True) # 上次额度重置时间
quota_expires_at = Column(DateTime(timezone=True), nullable=True) # 月卡过期时间
# 提供商优先级 (数字越小越优先,用于提供商优先模式下的 Provider 排序)
# 0-10: 急需消耗(如即将过期的月卡)
# 11-50: 优先消耗(月卡)
# 51-100: 正常消费(按量付费)
# 101+: 备用(高成本或限制严格的)
provider_priority = Column(Integer, default=100)
# 格式转换时是否保持优先级(默认 False
# - False: 需要格式转换时,该提供商的候选会被降级到不需要转换的候选之后
# - True: 即使需要格式转换,也保持原优先级排名
# 注意:如果系统配置 keep_priority_on_conversion=true此字段被忽略所有提供商都保持优先级
keep_priority_on_conversion = Column(Boolean, default=False, nullable=False)
# 是否允许格式转换(默认 False
# - True: 该提供商可以作为格式转换的目标(全局开关关闭时也可跳过端点检查)
# - False: 默认不作为格式转换目标;此时需要端点 format_acceptance_config 显式允许才可跨格式
# 优先级逻辑:
# - 全局开关 ON -> 强制允许跨格式(忽略此字段与端点检查)
# - 全局开关 OFF -> 若此字段 ON -> 允许跨格式(跳过端点检查)
# - 否则 -> 由端点 format_acceptance_config 决定是否允许
enable_format_conversion = Column(Boolean, default=False, nullable=False)
# 状态
is_active = Column(Boolean, default=True, nullable=False)
# 限制
concurrent_limit = Column(Integer, nullable=True) # 并发请求限制
# 请求配置
max_retries = Column(Integer, default=2, nullable=True) # 最大重试次数
proxy = Column(JSONB, nullable=True) # 代理配置: {url, username, password, enabled}
# 超时配置(秒),为 None 时使用全局配置
stream_first_byte_timeout = Column(Float, nullable=True) # 流式请求首字节超时
request_timeout = Column(Float, nullable=True) # 非流式请求整体超时
# 配置
config = Column(JSON, nullable=True) # 额外配置如Azure deployment name等
# 时间戳
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,
)
# 关系
models = relationship("Model", back_populates="provider", cascade="all, delete-orphan")
endpoints = relationship(
"ProviderEndpoint", back_populates="provider", cascade="all, delete-orphan"
)
api_keys = relationship(
"ProviderAPIKey", back_populates="provider", cascade="all, delete-orphan"
)
api_key_mappings = relationship(
"ApiKeyProviderMapping", back_populates="provider", cascade="all, delete-orphan"
)
usage_tracking = relationship(
"ProviderUsageTracking", back_populates="provider", cascade="all, delete-orphan"
)
class ProviderEndpoint(ExportMixin, Base):
"""提供商端点 - 一个提供商可以有多个 API 格式端点"""
__tablename__ = "provider_endpoints"
_export_exclude = frozenset(
{
"id",
"provider_id",
"api_family",
"endpoint_kind",
"created_at",
"updated_at",
}
)
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
provider_id = Column(String(36), ForeignKey("providers.id", ondelete="CASCADE"), nullable=False)
# API 格式和配置
# 新模式:存储 endpoint signature keyfamily:kind如 "openai:chat"
api_format = Column(String(50), nullable=False)
# 新架构字段Phase 1/3用于将 api_format 拆分为结构化维度
api_family = Column(String(50), nullable=True) # openai/claude/gemini
endpoint_kind = Column(String(50), nullable=True) # chat/cli/video/...
base_url = Column(String(500), nullable=False)
# 请求配置
header_rules = Column(JSON, nullable=True) # 请求头规则 [{action, key, value, from, to}]
body_rules = Column(JSON, nullable=True) # 请求体规则 [{action, path, value, from, to}]
max_retries = Column(Integer, default=2) # 最大重试次数
# 状态
is_active = Column(Boolean, default=True, nullable=False)
# 路径配置
custom_path = Column(
String(200), nullable=True
) # 自定义请求路径,为空则使用 API 格式的默认路径
# 额外配置
config = Column(JSON, nullable=True) # 端点特定配置(不推荐使用,优先使用专用字段)
# 格式转换配置
format_acceptance_config = Column(
JSON,
nullable=True,
default=None,
comment="格式接受策略配置(跨格式转换开关/白黑名单等)",
)
# 代理配置
proxy = Column(JSONB, nullable=True) # 代理配置: {url, username, password}
# 时间戳
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="endpoints")
# 唯一约束和索引在表定义后
__table_args__ = (
UniqueConstraint("provider_id", "api_format", name="uq_provider_api_format"),
Index("idx_endpoint_format_active", "api_format", "is_active"),
Index("idx_provider_family_kind", "provider_id", "api_family", "endpoint_kind"),
)
class ProxyNodeStatus(PyEnum):
"""代理节点状态"""
ONLINE = "online"
OFFLINE = "offline"
class ProxyNode(Base):
"""代理节点表aether-proxy 自动注册 + 手动添加)"""
__tablename__ = "proxy_nodes"
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
name = Column(String(100), nullable=False) # 节点名
ip = Column(String(512), nullable=False) # 公网 IP 或手动节点的主机名(含协议前缀)
port = Column(Integer, nullable=False) # 代理端口
region = Column(String(100), nullable=True) # 区域标签
# 手动节点专用字段
is_manual = Column(Boolean, default=False, nullable=False, comment="是否为手动添加的代理节点")
proxy_url = Column(String(500), nullable=True, comment="手动节点的完整代理 URL")
proxy_username = Column(String(255), nullable=True, comment="手动节点的代理用户名")
proxy_password = Column(String(500), nullable=True, comment="手动节点的代理密码")
status = Column(
Enum(
ProxyNodeStatus,
name="proxynodestatus",
create_type=False,
values_callable=lambda x: [e.value for e in x],
),
default=ProxyNodeStatus.ONLINE,
nullable=False,
)
registered_by = Column(
String(36),
ForeignKey("users.id", ondelete="SET NULL"),
nullable=True,
index=True,
comment="注册该节点的管理员用户 ID可空",
)
last_heartbeat_at = Column(DateTime(timezone=True), nullable=True)
heartbeat_interval = Column(Integer, default=30, nullable=False)
# 性能指标(心跳上报)
active_connections = Column(Integer, default=0, nullable=False)
total_requests = Column(BigInteger, default=0, nullable=False)
avg_latency_ms = Column(Float, nullable=True, comment="平均连接建立延迟(ms), DNS+TCP/TLS+TTFB")
failed_requests = Column(BigInteger, default=0, nullable=False, comment="累计失败请求数")
dns_failures = Column(BigInteger, default=0, nullable=False, comment="累计 DNS 失败数")
stream_errors = Column(BigInteger, default=0, nullable=False, comment="累计流错误数")
proxy_metadata = Column(JSON, nullable=True, comment="aether-proxy 上报元数据(版本等)")
# 硬件信息注册时上报JSON 可扩展)
hardware_info = Column(
JSON,
nullable=True,
comment="硬件信息 (cpu_cores, total_memory_mb, os_info, fd_limit, ...)",
)
estimated_max_concurrency = Column(
Integer, nullable=True, comment="基于硬件估算的最大并发连接数"
)
# 隧道模式proxy 主动连接 Aether 的 WebSocket 隧道)
tunnel_mode = Column(
Boolean, default=False, nullable=False, comment="是否使用 WebSocket 隧道模式"
)
tunnel_connected = Column(Boolean, default=False, nullable=False, comment="隧道是否已连接")
tunnel_connected_at = Column(
DateTime(timezone=True), nullable=True, comment="隧道最近一次建立时间"
)
# 管理端远程配置(通过心跳下发给 aether-proxy
remote_config = Column(
JSON,
nullable=True,
comment="管理端下发的远程配置 (allowed_ports, log_level, heartbeat_interval, timestamp_tolerance)",
)
config_version = Column(
Integer, default=0, nullable=False, comment="远程配置版本号,每次更新 +1"
)
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,
)
__table_args__ = (UniqueConstraint("ip", "port", name="uq_proxy_node_ip_port"),)
class ProxyNodeEvent(Base):
"""代理节点连接事件表 -- 记录 tunnel 连接/断开/错误事件,用于连接稳定性分析"""
__tablename__ = "proxy_node_events"
id = Column(BigInteger, primary_key=True, autoincrement=True)
node_id = Column(
String(36),
ForeignKey("proxy_nodes.id", ondelete="CASCADE"),
nullable=False,
index=True,
)
event_type = Column(
String(20),
nullable=False,
comment="事件类型: connected, disconnected, error",
)
detail = Column(String(500), nullable=True, comment="事件详情(如断开原因)")
created_at = Column(
DateTime(timezone=True),
default=lambda: datetime.now(timezone.utc),
nullable=False,
)
__table_args__ = (Index("idx_proxy_node_events_node_created", "node_id", "created_at"),)
class GlobalModel(ExportMixin, Base):
"""全局统一模型定义 - 包含价格和能力配置
设计原则:
- 定义模型的基本信息和价格配置(价格为必填项)
- Provider 级别的 Model 可以覆盖这些默认值
- 如果 Model 的价格/能力字段为空,则使用 GlobalModel 的值
"""
__tablename__ = "global_models"
_export_exclude = frozenset(
{
"id",
"usage_count",
"created_at",
"updated_at",
}
)
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
name = Column(String(100), unique=True, nullable=False, index=True) # 统一模型名(唯一)
display_name = Column(String(100), nullable=False)
# 按次计费配置(每次请求的固定费用,美元)- 可选,与按 token 计费叠加
default_price_per_request = Column(
Numeric(20, 8), nullable=True, default=None
) # 每次请求固定费用
# 统一阶梯计费配置JSON格式- 必填
# 固定价格也用单阶梯表示: {"tiers": [{"up_to": null, "input_price_per_1m": X, ...}]}
# 结构示例:
# {
# "tiers": [
# {
# "up_to": 128000, # 阶梯上限tokensnull 表示无上限
# "input_price_per_1m": 2.50,
# "output_price_per_1m": 10.00,
# "cache_creation_price_per_1m": 3.75, # 可选
# "cache_read_price_per_1m": 0.30, # 可选
# "cache_ttl_pricing": [ # 可选:按缓存时长分价格
# {"ttl_minutes": 5, "cache_creation_price_per_1m": 3.75, "cache_read_price_per_1m": 0.30},
# {"ttl_minutes": 60, "cache_creation_price_per_1m": 6.00, "cache_read_price_per_1m": 0.50}
# ]
# },
# {"up_to": null, "input_price_per_1m": 1.25, ...}
# ]
# }
default_tiered_pricing = Column(JSON, nullable=False)
# Key 能力配置 - 模型支持的能力列表(如 ["cache_1h", "context_1m"]
# Key 只能启用模型支持的能力
supported_capabilities = Column(JSON, nullable=True, default=list)
# 模型配置JSON格式- 包含能力、规格、元信息等
# 结构示例:
# {
# # 能力配置
# "streaming": true,
# "vision": true,
# "function_calling": true,
# "extended_thinking": false,
# "image_generation": false,
# # 规格参数
# "context_limit": 200000,
# "output_limit": 8192,
# # 元信息
# "description": "...",
# "icon_url": "...",
# "official_url": "...",
# "knowledge_cutoff": "2024-04",
# "family": "claude-3.5",
# "release_date": "2024-10-22",
# "input_modalities": ["text", "image"],
# "output_modalities": ["text"],
# }
config = Column(JSONB, nullable=True, default=dict)
# 状态
is_active = Column(Boolean, default=True, nullable=False)
# 统计计数器(优化性能,避免实时查询)
usage_count = Column(Integer, default=0, nullable=False, index=True)
# 时间戳
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,
)
# 关系
models = relationship("Model", back_populates="global_model")
class Model(ExportMixin, Base):
"""Provider 模型配置表 - Provider 如何使用某个 GlobalModel
设计原则:
- Model 表示 Provider 对某个模型的具体实现
- global_model_id 必填,必须关联到一个 GlobalModel
- provider_model_name 是 Provider 侧的实际模型名称 (可能与 GlobalModel.name 不同)
- 价格和能力配置可为空,为空时使用 GlobalModel 的默认值
"""
__tablename__ = "models"
_export_exclude = frozenset(
{
"id",
"provider_id",
"global_model_id",
"is_available",
"created_at",
"updated_at",
}
)
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
provider_id = Column(String(36), ForeignKey("providers.id"), nullable=False)
# 必须关联一个 GlobalModel
global_model_id = Column(String(36), ForeignKey("global_models.id"), nullable=False, index=True)
# Provider 映射配置
provider_model_name = Column(String(200), nullable=False) # Provider 侧的主模型名称
# 模型名称映射列表(带优先级),用于同一模型在 Provider 侧有多个名称变体的场景
# 格式: [{"name": "Claude-Sonnet-4.5", "priority": 1}, {"name": "Claude-Sonnet-4-5", "priority": 2}]
# 为空时只使用 provider_model_name
provider_model_mappings = Column(JSON, nullable=True, default=None)
# 按次计费配置(每次请求的固定费用,美元)- 可为空,为空时使用 GlobalModel 的默认值
price_per_request = Column(Numeric(20, 8), nullable=True) # 每次请求固定费用
# 阶梯计费配置JSON格式- 可为空,为空时使用 GlobalModel 的默认值
tiered_pricing = Column(JSON, nullable=True, default=None)
# Provider 能力配置 - 可为空,为空时使用 GlobalModel 的默认值
supports_vision = Column(Boolean, nullable=True)
supports_function_calling = Column(Boolean, nullable=True)
supports_streaming = Column(Boolean, nullable=True)
supports_extended_thinking = Column(Boolean, nullable=True)
supports_image_generation = Column(Boolean, nullable=True)
# 状态
is_active = Column(Boolean, default=True, nullable=False)
is_available = Column(Boolean, default=True) # 是否当前可用
# 扩展配置
config = Column(JSON, nullable=True)
# 时间戳
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="models")
global_model = relationship("GlobalModel", back_populates="models")
# 唯一约束:同一个提供商下的 provider_model_name 不能重复
__table_args__ = (
UniqueConstraint("provider_id", "provider_model_name", name="uq_provider_model"),
)
# 辅助方法:获取有效的阶梯计费配置
def get_effective_tiered_pricing(self) -> dict | None:
"""获取有效的阶梯计费配置"""
if self.tiered_pricing is not None:
return self.tiered_pricing
if self.global_model:
return self.global_model.default_tiered_pricing
return None
def _get_first_tier(self) -> dict | None:
"""获取第一个阶梯(用于获取默认价格)"""
tiered = self.get_effective_tiered_pricing()
if tiered and tiered.get("tiers"):
return tiered["tiers"][0]
return None
def get_effective_input_price(self) -> float:
"""获取有效的输入价格(从第一个阶梯)"""
tier = self._get_first_tier()
if tier:
return tier.get("input_price_per_1m", 0.0)
return 0.0
def get_effective_output_price(self) -> float:
"""获取有效的输出价格(从第一个阶梯)"""
tier = self._get_first_tier()
if tier:
return tier.get("output_price_per_1m", 0.0)
return 0.0
def get_effective_cache_creation_price(self) -> float | None:
"""获取有效的缓存创建价格(从第一个阶梯)"""
tier = self._get_first_tier()
if tier:
return tier.get("cache_creation_price_per_1m")
return None
def get_effective_cache_read_price(self) -> float | None:
"""获取有效的缓存读取价格(从第一个阶梯)"""
tier = self._get_first_tier()
if tier:
return tier.get("cache_read_price_per_1m")
return None
def get_effective_1h_cache_creation_price(self) -> float | None:
"""获取有效的 1h 缓存创建价格(从第一个阶梯)"""
tier = self._get_first_tier()
if tier:
cache_ttl_pricing = tier.get("cache_ttl_pricing") or []
for ttl_entry in cache_ttl_pricing:
if ttl_entry.get("ttl_minutes") == 60:
return ttl_entry.get("cache_creation_price_per_1m")
return None
def get_effective_price_per_request(self) -> float | None:
"""获取有效的按次计费价格"""
if self.price_per_request is not None:
return self.price_per_request
if self.global_model:
return self.global_model.default_price_per_request
return None
def _get_effective_capability(self, attr_name: str, default: bool = False) -> bool:
"""获取有效的能力配置(通用辅助方法)"""
local_value = getattr(self, attr_name, None)
if local_value is not None:
return bool(local_value)
if self.global_model:
config_key_map = {
"supports_vision": "vision",
"supports_function_calling": "function_calling",
"supports_streaming": "streaming",
"supports_extended_thinking": "extended_thinking",
"supports_image_generation": "image_generation",
}
config_key = config_key_map.get(attr_name)
if config_key:
global_config = getattr(self.global_model, "config", None)
if isinstance(global_config, dict):
global_value = global_config.get(config_key)
if global_value is not None:
return bool(global_value)
return default
def get_effective_supports_vision(self) -> bool:
return self._get_effective_capability("supports_vision", False)
def get_effective_supports_function_calling(self) -> bool:
return self._get_effective_capability("supports_function_calling", False)
def get_effective_supports_streaming(self) -> bool:
return self._get_effective_capability("supports_streaming", True)
def get_effective_supports_extended_thinking(self) -> bool:
return self._get_effective_capability("supports_extended_thinking", False)
def get_effective_supports_image_generation(self) -> bool:
return self._get_effective_capability("supports_image_generation", False)
def get_effective_config(self) -> dict | None:
"""获取有效的 config合并 Model 和 GlobalModel 的 config
合并策略:
- GlobalModel.config 作为基础
- Model.config 覆盖 GlobalModel.config
- 深度合并 billing 子字段
"""
global_config = {}
if self.global_model and self.global_model.config:
global_config = dict(self.global_model.config)
if not self.config:
return global_config if global_config else None
# 深度合并 config
result = dict(global_config)
for key, value in self.config.items():
if key == "billing" and isinstance(value, dict) and isinstance(result.get(key), dict):
# 深度合并 billing
result[key] = {**result[key], **value}
else:
result[key] = value
return result if result else None
def select_provider_model_name(
self, affinity_key: str | None = None, api_format: str | None = None
) -> str:
"""按优先级选择要使用的 Provider 模型名称
如果配置了 provider_model_mappings按优先级选择数字越小越优先
相同优先级的映射通过哈希分散实现负载均衡(与 Key 调度策略一致);
否则返回 provider_model_name。
Args:
affinity_key: 用于哈希分散的亲和键(如用户 API Key 哈希),确保同一用户稳定选择同一映射
api_format: 当前请求的 endpoint signature"openai:chat"),用于过滤适用的映射
"""
import hashlib
if not self.provider_model_mappings:
return self.provider_model_name
raw_mappings = self.provider_model_mappings
if not isinstance(raw_mappings, list) or len(raw_mappings) == 0:
return self.provider_model_name
mappings: list[dict] = []
for raw in raw_mappings:
if not isinstance(raw, dict):
continue
name = raw.get("name")
if not isinstance(name, str) or not name.strip():
continue
# 检查 api_formats 作用域(如果配置了且当前有 api_format
mapping_api_formats = raw.get("api_formats")
if api_format and mapping_api_formats:
# 如果配置了作用域,只有匹配时才生效
if isinstance(mapping_api_formats, list):
target = str(api_format).strip().lower()
allowed = {str(fmt).strip().lower() for fmt in mapping_api_formats if fmt}
if target not in allowed:
continue
raw_priority = raw.get("priority", 1)
try:
priority = int(raw_priority)
except Exception:
priority = 1
if priority < 1:
priority = 1
mappings.append({"name": name.strip(), "priority": priority})
if not mappings:
return self.provider_model_name
# 按优先级排序(数字越小越优先)
sorted_mappings = sorted(mappings, key=lambda x: x["priority"])
# 获取最高优先级(最小数字)
highest_priority = sorted_mappings[0]["priority"]
# 获取所有最高优先级的映射
top_priority_mappings = [
mapping for mapping in sorted_mappings if mapping["priority"] == highest_priority
]
# 如果有多个相同优先级的映射,通过哈希分散选择
if len(top_priority_mappings) > 1 and affinity_key:
# 为每个映射计算哈希得分,选择得分最小的
def hash_score(mapping: dict) -> int:
combined = f"{affinity_key}:{mapping['name']}"
return int(hashlib.md5(combined.encode()).hexdigest(), 16)
selected = min(top_priority_mappings, key=hash_score)
elif len(top_priority_mappings) > 1:
# 没有 affinity_key 时,使用确定性选择(按名称排序后取第一个)
# 避免随机选择导致同一请求重试时选择不同的模型名称
selected = min(top_priority_mappings, key=lambda x: x["name"])
else:
selected = top_priority_mappings[0]
return selected["name"]
def get_all_provider_model_names(self) -> list[str]:
"""获取所有可用的 Provider 模型名称(主名称 + 映射名称)"""
names = [self.provider_model_name]
if self.provider_model_mappings:
for mapping in self.provider_model_mappings:
if isinstance(mapping, dict) and mapping.get("name"):
names.append(mapping["name"])
return names
class BillingRule(Base):
"""计费规则表(单条 formula 规则,支持 Model 覆盖 GlobalModel"""
__tablename__ = "billing_rules"
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
# 规则关联(两者必有其一)
global_model_id = Column(
String(36), ForeignKey("global_models.id", ondelete="CASCADE"), nullable=True, index=True
)
model_id = Column(
String(36), ForeignKey("models.id", ondelete="CASCADE"), nullable=True, index=True
)
name = Column(String(100), nullable=False)
# 注CLI 在计费域里恒等于 chat不单独存 "cli"
task_type = Column(String(20), nullable=False, default="chat")
# Formula 表达式及其配置
expression = Column(Text, nullable=False)
variables = Column(JSONB, nullable=False, default=dict)
dimension_mappings = Column(JSONB, nullable=False, default=dict)
is_enabled = Column(Boolean, nullable=False, default=True)
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,
)
global_model = relationship("GlobalModel", foreign_keys=[global_model_id])
model = relationship("Model", foreign_keys=[model_id])
__table_args__ = (
CheckConstraint(
"(global_model_id IS NOT NULL AND model_id IS NULL) OR "
"(global_model_id IS NULL AND model_id IS NOT NULL)",
name="chk_billing_rules_model_ref",
),
# 同级同 task_type 只允许一条启用规则partial unique index
Index(
"uq_billing_rules_global_model_task",
"global_model_id",
"task_type",
unique=True,
postgresql_where=text("is_enabled = TRUE AND global_model_id IS NOT NULL"),
),
Index(
"uq_billing_rules_model_task",
"model_id",
"task_type",
unique=True,
postgresql_where=text("is_enabled = TRUE AND model_id IS NOT NULL"),
),
)
class DimensionCollector(Base):
"""维度收集器配置表(从请求/响应/元数据/派生计算收集维度)。"""
__tablename__ = "dimension_collectors"
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
api_format = Column(String(50), nullable=False)
task_type = Column(String(20), nullable=False)
dimension_name = Column(String(100), nullable=False)
# 来源配置
# - response / request / metadata / computed
source_type = Column(String(20), nullable=False)
source_path = Column(String(200), nullable=True) # computed 允许为空
# 值类型与转换
value_type = Column(String(20), nullable=False, default="float") # float/int/string
transform_expression = Column(Text, nullable=True) # computed 时为派生公式
default_value = Column(String(100), nullable=True)
priority = Column(Integer, nullable=False, default=0)
is_enabled = Column(Boolean, nullable=False, default=True)
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,
)
__table_args__ = (
CheckConstraint(
"(source_type = 'computed' AND source_path IS NULL AND transform_expression IS NOT NULL) OR "
"(source_type != 'computed' AND source_path IS NOT NULL)",
name="chk_dimension_collectors_source_config",
),
# 同维度 + 同优先级 + enabled 才唯一(允许禁用旧配置后重建)
Index(
"uq_dimension_collectors_enabled",
"api_format",
"task_type",
"dimension_name",
"priority",
unique=True,
postgresql_where=text("is_enabled = TRUE"),
),
)
class ProviderAPIKey(ExportMixin, Base):
"""Provider API密钥表 - 直接归属于 Provider支持多种 API 格式"""
__tablename__ = "provider_api_keys"
_export_exclude = frozenset(
{
"id",
"provider_id",
"api_key",
"auth_config",
"learned_rpm_limit",
"concurrent_429_count",
"rpm_429_count",
"last_429_at",
"last_429_type",
"last_rpm_peak",
"adjustment_history",
"utilization_samples",
"last_probe_increase_at",
"health_by_format",
"circuit_breaker_by_format",
"request_count",
"total_tokens",
"total_cost_usd",
"success_count",
"error_count",
"total_response_time_ms",
"last_used_at",
"last_error_at",
"last_error_msg",
"expires_at",
"last_models_fetch_at",
"last_models_fetch_error",
"upstream_metadata",
"oauth_invalid_at",
"oauth_invalid_reason",
"created_at",
"updated_at",
}
)
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
# 外键关系 - 直接关联 Provider
provider_id = Column(
String(36), ForeignKey("providers.id", ondelete="CASCADE"), nullable=False, index=True
)
# API 格式支持列表(核心字段)
# None 表示支持所有格式,空列表 [] 表示不支持任何格式
api_formats = Column(JSON, nullable=True, default=list) # ["claude:chat", "claude:cli"]
# 认证类型
# - "api_key": 标准 API Key 认证(默认)
# - "service_account": GCP Service Account JSON 认证
# - "oauth": OAuth access_token / refresh_token 认证
auth_type = Column(String(20), default="api_key", nullable=False)
# API密钥加密存储
# - auth_type="api_key" 时:存储 API Key 字符串
# - auth_type="service_account"/"oauth" 时:可为占位符,敏感凭证存在 auth_config 中
api_key = Column(Text, nullable=False) # 使用 Text 支持加密后的 OAuth token
# 认证配置(加密存储)
# - auth_type="api_key" 时:可为空
# - auth_type="service_account" 时:存储加密后的 Service Account JSON
# - auth_type="oauth" 时:存储加密后的 {refresh_token, expires_at, ...}
auth_config = Column(Text, nullable=True)
name = Column(String(100), nullable=False) # 密钥名称(必填,用于识别)
note = Column(String(500), nullable=True) # 备注说明(可选)
# 成本计算
rate_multipliers = Column(
JSON, nullable=True
) # 按 endpoint signature 的成本倍率 {"claude:cli": 1.0, "openai:cli": 0.8}
# 优先级配置 (数字越小越优先)
internal_priority = Column(
Integer, default=50
) # Endpoint 内部优先级(用于提供商优先模式,同 Endpoint 内 Keys 的排序,同优先级参与负载均衡)
global_priority_by_format = Column(
JSON, nullable=True
) # 按 endpoint signature 的全局优先级 {"claude:chat": 1, "claude:cli": 2}
# RPM 限制配置(自适应学习)
# rpm_limit 决定 RPM 控制模式:
# - NULL: 自适应模式,系统自动学习并调整(使用 learned_rpm_limit
# - 数字: 固定限制模式,使用用户指定的值
rpm_limit = Column(Integer, nullable=True, default=None)
# 模型权限控制
allowed_models = Column(JSON, nullable=True) # 允许使用的模型列表null = 支持所有模型)
# Key 能力标签
capabilities = Column(JSON, nullable=True) # Key 拥有的能力
# 示例: {"cache_1h": true, "context_1m": true}
# 自适应 RPM 调整(仅当 rpm_limit = NULL 时生效)
learned_rpm_limit = Column(Integer, nullable=True) # 学习到的 RPM 限制(自适应模式下的有效值)
concurrent_429_count = Column(Integer, default=0, nullable=False) # 因并发导致的429次数
rpm_429_count = Column(Integer, default=0, nullable=False) # 因RPM导致的429次数
last_429_at = Column(DateTime(timezone=True), nullable=True) # 最后429时间
last_429_type = Column(String(50), nullable=True) # 最后429类型: concurrent/rpm/unknown
last_rpm_peak = Column(Integer, nullable=True) # 触发429时的RPM峰值
adjustment_history = Column(JSON, nullable=True) # RPM调整历史
# 基于滑动窗口的利用率追踪
utilization_samples = Column(
JSON, nullable=True
) # 利用率采样窗口 [{"ts": timestamp, "util": 0.8}, ...]
last_probe_increase_at = Column(DateTime(timezone=True), nullable=True) # 上次探测性扩容时间
# 健康度追踪(按 endpoint signature 存储)
# 结构: {"claude:chat": {"health_score": 1.0, "consecutive_failures": 0, ...}, ...}
health_by_format = Column(JSON, nullable=True, default=dict)
# 缓存与熔断配置
cache_ttl_minutes = Column(
Integer, default=5, nullable=False
) # 缓存TTL(分钟)0表示不支持缓存默认5分钟
max_probe_interval_minutes = Column(
Integer, default=32, nullable=False
) # 最大探测间隔(分钟)默认32分钟硬上限
# 熔断器状态(按 endpoint signature 存储)
# 结构: {"claude:chat": {"open": false, "open_at": null, ...}, ...}
circuit_breaker_by_format = Column(JSON, nullable=True, default=dict)
# 使用统计
request_count = Column(Integer, default=0) # 请求次数
total_tokens = Column(BigInteger, default=0, nullable=False) # 累计 Token 数
total_cost_usd = Column(Numeric(20, 8), default=0.0, nullable=False) # 累计成本
success_count = Column(Integer, default=0) # 成功次数
error_count = Column(Integer, default=0) # 错误次数
total_response_time_ms = Column(Integer, default=0) # 总响应时间(用于计算平均值)
last_used_at = Column(DateTime(timezone=True), nullable=True) # 最后使用时间
last_error_at = Column(DateTime(timezone=True), nullable=True) # 最后错误时间
last_error_msg = Column(Text, nullable=True) # 最后错误信息
# 状态
is_active = Column(Boolean, default=True, nullable=False)
expires_at = Column(DateTime(timezone=True), nullable=True) # 过期时间
# 自动获取模型配置
auto_fetch_models = Column(Boolean, default=False, nullable=False) # 是否启用自动获取模型
last_models_fetch_at = Column(DateTime(timezone=True), nullable=True) # 最后获取时间
last_models_fetch_error = Column(Text, nullable=True) # 最后获取错误信息
locked_models = Column(JSON, nullable=True) # 被锁定的模型列表(刷新时不会被删除)
# 模型过滤规则(支持 * 和 ? 通配符,如 "gpt-*", "claude-?-sonnet"
model_include_patterns = Column(JSON, nullable=True) # 包含规则列表,空表示不过滤(包含所有)
model_exclude_patterns = Column(JSON, nullable=True) # 排除规则列表,空表示不排除
# 上游元数据(由响应头解析器采集,如 Codex 额度信息)
upstream_metadata = Column(JSON, nullable=True, default=dict)
# OAuth 失效状态(账号被封、授权撤销、刷新失败等)
oauth_invalid_at = Column(DateTime(timezone=True), nullable=True) # 失效时间
oauth_invalid_reason = Column(String(255), nullable=True) # 失效原因
status_snapshot = Column(JSON, nullable=True, default=None) # 结构化状态快照(兼容旧字段)
# Key 级别的代理配置(覆盖 Provider 级别的代理设置)
# 结构: {"node_id": "xxx", "enabled": true} 或 {"url": "socks5://...", "enabled": true}
# null 表示使用 Provider 级别代理(默认行为)
proxy = Column(JSON, nullable=True, default=None)
# 每个账号持久化的请求指纹配置TLS impersonate + HTTP header 维度)
fingerprint = Column(JSON, nullable=True, default=None)
# 时间戳
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,
)
__table_args__ = (Index("idx_provider_api_keys_provider_active", "provider_id", "is_active"),)
# 关系
provider = relationship("Provider", back_populates="api_keys")
_PROVIDER_API_KEY_STATUS_SNAPSHOT_FIELDS: tuple[str, ...] = (
"auth_type",
"auth_config",
"expires_at",
"oauth_invalid_at",
"oauth_invalid_reason",
"upstream_metadata",
"provider_id",
)
def _sync_provider_api_key_status_snapshot(
target: ProviderAPIKey,
*,
connection: Any | None,
force: bool,
) -> None:
state = inspect(target)
if state is None:
return
if not force:
relevant_changed = any(
state.attrs[field].history.has_changes()
for field in _PROVIDER_API_KEY_STATUS_SNAPSHOT_FIELDS
)
if not relevant_changed and getattr(target, "status_snapshot", None) is not None:
return
from src.services.provider_keys.status_snapshot_store import sync_provider_key_status_snapshot
sync_provider_key_status_snapshot(target, connection=connection)
@event.listens_for(ProviderAPIKey, "before_insert")
def _provider_api_key_before_insert(mapper: Any, connection: Any, target: ProviderAPIKey) -> None:
_sync_provider_api_key_status_snapshot(target, connection=connection, force=True)
@event.listens_for(ProviderAPIKey, "before_update")
def _provider_api_key_before_update(mapper: Any, connection: Any, target: ProviderAPIKey) -> None:
_sync_provider_api_key_status_snapshot(target, connection=connection, force=False)
def _generate_short_id(length: int = 12) -> str:
"""生成 Gemini 风格的短 ID小写字母+数字)"""
import secrets
import string
alphabet = string.ascii_lowercase + string.digits
return "".join(secrets.choice(alphabet) for _ in range(length))
class VideoTask(Base):
"""视频生成任务"""
__tablename__ = "video_tasks"
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
# Gemini 风格的短 ID用于对外暴露如 operations/xxx
short_id = Column(String(16), unique=True, index=True, default=_generate_short_id)
request_id = Column(
String(100), unique=True, index=True, nullable=False
) # 关联 Usage/RequestCandidate
external_task_id = Column(String(200))
# 关联
user_id = Column(String(36), ForeignKey("users.id", ondelete="SET NULL"), nullable=True)
api_key_id = Column(
String(36), ForeignKey("api_keys.id", ondelete="SET NULL"), nullable=True, index=True
)
# 归属快照(删除用户/Key 后仍可追溯)
username = Column(String(100), nullable=True, comment="用户名快照")
api_key_name = Column(String(200), nullable=True, comment="API Key 名称快照")
provider_id = Column(String(36), ForeignKey("providers.id"), index=True)
endpoint_id = Column(String(36), ForeignKey("provider_endpoints.id"), index=True)
key_id = Column(String(36), ForeignKey("provider_api_keys.id", ondelete="SET NULL"), index=True)
# 格式转换追踪
client_api_format = Column(String(50), nullable=False)
provider_api_format = Column(String(50), nullable=False)
format_converted = Column(Boolean, default=False)
# 任务配置
model = Column(String(100), nullable=False)
prompt = Column(Text, nullable=False)
original_request_body = Column(JSON)
converted_request_body = Column(JSON)
# 视频参数 (统一内部格式)
duration_seconds = Column(Integer, default=4)
resolution = Column(String(20), default="720p")
aspect_ratio = Column(String(10), default="16:9")
size = Column(String(20))
# 状态
status = Column(String(20), default="pending")
progress_percent = Column(Integer, default=0)
progress_message = Column(String(500))
# 结果
video_url = Column(String(2000))
video_urls = Column(JSON)
thumbnail_url = Column(String(2000))
video_size_bytes = Column(BigInteger)
video_duration_seconds = Column(Float) # 实际视频时长(秒)
video_expires_at = Column(DateTime(timezone=True))
# 存储 (可选)
stored_video_path = Column(String(500))
storage_provider = Column(String(50))
# 错误
error_code = Column(String(50))
error_message = Column(Text)
retry_count = Column(Integer, default=0)
max_retries = Column(Integer, default=3)
# 轮询配置
poll_interval_seconds = Column(Integer, default=10)
next_poll_at = Column(DateTime(timezone=True)) # 索引在 __table_args__ 中定义
poll_count = Column(Integer, default=0)
max_poll_count = Column(Integer, default=360)
# Remix 支持
remixed_from_task_id = Column(
String(36), ForeignKey("video_tasks.id", ondelete="SET NULL"), nullable=True, index=True
)
# 使用追踪(候选 key、请求头等
request_metadata = Column(JSON, nullable=True) # 存储候选 key 列表、请求头等追踪信息
# 示例: {
# "candidate_keys": [{"key_id": "xxx", "endpoint_id": "yyy", "priority": 1}, ...],
# "selected_key_index": 0,
# "client_ip": "1.2.3.4",
# "user_agent": "...",
# "request_headers": {...}
# }
# 时间戳
created_at = Column(
DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), nullable=False
)
submitted_at = Column(DateTime(timezone=True))
completed_at = Column(DateTime(timezone=True))
updated_at = Column(
DateTime(timezone=True),
default=lambda: datetime.now(timezone.utc),
onupdate=lambda: datetime.now(timezone.utc),
nullable=False,
)
# 关系
user = relationship("User", backref=backref("video_tasks", passive_deletes=True))
remixed_from = relationship("VideoTask", remote_side=[id], backref="remixes")
# 复合索引和唯一约束
__table_args__ = (
Index("idx_video_tasks_user_status", "user_id", "status"),
Index("idx_video_tasks_next_poll", "next_poll_at"),
Index("idx_video_tasks_external_id", "external_task_id"),
UniqueConstraint("user_id", "external_task_id", name="uq_video_tasks_user_external_id"),
)
class UserPreference(Base):
"""用户偏好设置表"""
__tablename__ = "user_preferences"
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
user_id = Column(
String(36), ForeignKey("users.id", ondelete="CASCADE"), unique=True, nullable=False
)
# 个人信息
avatar_url = Column(String(500), nullable=True) # 头像URL
bio = Column(Text, nullable=True) # 个人简介
# 偏好设置
default_provider_id = Column(String(36), ForeignKey("providers.id"), nullable=True, index=True)
theme = Column(String(20), default="light") # light/dark/auto
language = Column(String(10), default="zh-CN")
timezone = Column(String(50), default="Asia/Shanghai")
# 通知设置
email_notifications = Column(Boolean, default=True)
usage_alerts = Column(Boolean, default=True)
announcement_notifications = Column(Boolean, default=True)
# 时间戳
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,
)
# 关系
user = relationship("User", back_populates="preferences")
default_provider = relationship("Provider")
class Announcement(Base):
"""公告表"""
__tablename__ = "announcements"
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
title = Column(String(200), nullable=False)
content = Column(Text, nullable=False) # 支持 Markdown
type = Column(String(20), default="info") # info, warning, maintenance, important
priority = Column(Integer, default=0) # 优先级,数字越大越重要
# 发布信息
author_id = Column(
String(36), ForeignKey("users.id", ondelete="SET NULL"), nullable=True, index=True
)
is_active = Column(Boolean, default=True, index=True)
is_pinned = Column(Boolean, default=False) # 置顶
# 时间范围
start_time = Column(DateTime(timezone=True), nullable=True)
end_time = Column(DateTime(timezone=True), nullable=True)
# 时间戳
created_at = Column(
DateTime(timezone=True),
default=lambda: datetime.now(timezone.utc),
nullable=False,
index=True,
)
updated_at = Column(
DateTime(timezone=True),
default=lambda: datetime.now(timezone.utc),
onupdate=lambda: datetime.now(timezone.utc),
nullable=False,
)
# 关系
author = relationship("User", back_populates="authored_announcements")
reads = relationship(
"AnnouncementRead", back_populates="announcement", cascade="all, delete-orphan"
)
class AnnouncementRead(Base):
"""公告已读记录表"""
__tablename__ = "announcement_reads"
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
user_id = Column(String(36), ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
announcement_id = Column(String(36), ForeignKey("announcements.id"), nullable=False, index=True)
read_at = Column(
DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), nullable=False
)
# 唯一约束
__table_args__ = (UniqueConstraint("user_id", "announcement_id", name="uq_user_announcement"),)
# 关系
user = relationship("User", back_populates="announcement_reads")
announcement = relationship("Announcement", back_populates="reads")
class AuditEventType(PyEnum):
"""审计事件类型"""
# 认证相关
LOGIN_SUCCESS = "login_success"
LOGIN_FAILED = "login_failed"
LOGOUT = "logout"
API_KEY_CREATED = "api_key_created"
API_KEY_DELETED = "api_key_deleted"
API_KEY_USED = "api_key_used"
# 请求相关
REQUEST_SUCCESS = "request_success"
REQUEST_FAILED = "request_failed"
REQUEST_RATE_LIMITED = "request_rate_limited"
REQUEST_QUOTA_EXCEEDED = "request_quota_exceeded"
# 管理操作
USER_CREATED = "user_created"
USER_UPDATED = "user_updated"
USER_DELETED = "user_deleted"
PROVIDER_ADDED = "provider_added"
PROVIDER_UPDATED = "provider_updated"
PROVIDER_REMOVED = "provider_removed"
# 安全事件
SUSPICIOUS_ACTIVITY = "suspicious_activity"
UNAUTHORIZED_ACCESS = "unauthorized_access"
DATA_EXPORT = "data_export"
CONFIG_CHANGED = "config_changed"
# Management Token 相关
MANAGEMENT_TOKEN_CREATED = "management_token_created"
MANAGEMENT_TOKEN_UPDATED = "management_token_updated"
MANAGEMENT_TOKEN_DELETED = "management_token_deleted"
MANAGEMENT_TOKEN_USED = "management_token_used"
MANAGEMENT_TOKEN_EXPIRED = "management_token_expired"
MANAGEMENT_TOKEN_IP_BLOCKED = "management_token_ip_blocked"
class ManagementToken(Base):
"""Management Token 模型 - 用于程序化管理 API 调用"""
__tablename__ = "management_tokens"
# Token 格式常量
TOKEN_PREFIX = "ae_"
TOKEN_RANDOM_LENGTH = 40
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
user_id = Column(String(36), ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
# Token 信息
token_hash = Column(String(64), unique=True, index=True, nullable=False) # SHA256 哈希
token_prefix = Column(String(12), nullable=True) # Token 前缀用于显示(如 ae_xxxxxxxx
name = Column(String(100), nullable=False) # Token 名称
description = Column(Text, nullable=True) # 描述
# IP 白名单(可选)
allowed_ips = Column(JSON, nullable=True) # 允许的 IP 列表NULL = 不限制
# 格式: ["192.168.1.1", "10.0.0.0/24"]
# 有效期
expires_at = Column(DateTime(timezone=True), nullable=True) # NULL = 永不过期
# 使用统计
last_used_at = Column(DateTime(timezone=True), nullable=True)
last_used_ip = Column(String(45), nullable=True)
usage_count = Column(Integer, default=0) # 使用次数
# 状态
is_active = 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,
)
# 关系
user = relationship("User", back_populates="management_tokens")
# 索引和约束
__table_args__ = (
Index("idx_management_tokens_user_id", "user_id"),
Index("idx_management_tokens_is_active", "is_active"),
UniqueConstraint("user_id", "name", name="uq_management_tokens_user_name"),
# IP 白名单必须为 NULL不限制或非空数组禁止空数组
# 注意JSON 类型的 NULL 可能被序列化为 JSON 'null',需要同时处理
CheckConstraint(
"allowed_ips IS NULL OR allowed_ips::text = 'null' OR json_array_length(allowed_ips) > 0",
name="check_allowed_ips_not_empty",
),
)
@staticmethod
def generate_token() -> str:
"""生成 Management Token使用加密安全的随机数"""
import string
alphabet = string.ascii_letters + string.digits
random_part = "".join(
secrets.choice(alphabet) for _ in range(ManagementToken.TOKEN_RANDOM_LENGTH)
)
return f"{ManagementToken.TOKEN_PREFIX}{random_part}"
@staticmethod
def hash_token(token: str) -> str:
"""对 Token 进行 SHA256 哈希
安全性说明(当前方案是安全的):
- Token 熵为 62^40约 2^238暴力破解在计算上不可行
- 结合速率限制(默认 30 次/分钟/IP在线攻击不可行
- 不需要盐值:盐值用于防止彩虹表攻击,但 Token 是高熵随机值,
不存在可预计算的"常见值",因此彩虹表攻击不适用
"""
return hashlib.sha256(token.encode()).hexdigest()
def set_token(self, token: str) -> None:
"""设置 Token只存储哈希和前缀用于显示"""
self.token_hash = self.hash_token(token)
# 存储前缀用于显示ae_ + 4 个字符,共 7 个字符)
self.token_prefix = token[:7] if len(token) > 7 else token
def get_display_token(self) -> str:
"""获取用于显示的脱敏 Token显示前缀 + 掩码)"""
if self.token_prefix:
return f"{self.token_prefix}...****"
return "ae_****"
def is_ip_allowed(self, client_ip: str) -> bool:
"""检查 IP 是否在白名单中
安全策略:
- None 或不设置表示不限制(允许所有 IP
- 非空列表表示只允许列表中的 IP
- 无效的白名单条目会被记录并跳过
- 无效的客户端 IP 直接拒绝
- 支持 IPv4 映射的 IPv6 地址规范化
"""
if self.allowed_ips is None:
return True # 未设置白名单,不限制
import ipaddress
from src.core.logger import logger
# 防御性检查:空列表应该在数据库层被拒绝,但这里再检查一次
if not self.allowed_ips:
logger.critical(f"Management Token {self.id} - allowed_ips 为空列表(违反数据库约束)")
return False # fail-safe
def normalize_ip(ip_str: str) -> ipaddress.IPv4Address | ipaddress.IPv6Address | None:
"""规范化 IP 地址,将 IPv4 映射的 IPv6 转换为 IPv4"""
try:
ip = ipaddress.ip_address(ip_str)
if isinstance(ip, ipaddress.IPv6Address) and ip.ipv4_mapped:
return ip.ipv4_mapped
return ip
except ValueError:
return None
# 规范化客户端 IP
client = normalize_ip(client_ip)
if client is None:
logger.error(f"Management Token {self.id} - 拒绝无效的客户端 IP: {client_ip}")
return False
valid_entries = 0
for allowed in self.allowed_ips:
try:
if "/" in allowed:
# CIDR 格式
network = ipaddress.ip_network(allowed, strict=False)
valid_entries += 1
if client in network:
return True
else:
# 精确 IP
allowed_ip = normalize_ip(allowed)
if allowed_ip is None:
logger.error(f"Management Token {self.id} - 白名单包含无效条目: {allowed}")
continue
valid_entries += 1
if client == allowed_ip:
return True
except ValueError:
logger.error(f"Management Token {self.id} - 白名单包含无效条目: {allowed}")
continue
# 如果白名单全部无效,记录严重错误并拒绝
if valid_entries == 0:
logger.critical(f"Management Token {self.id} - 白名单全部无效,拒绝所有访问")
return False
@property
def is_expired(self) -> bool:
"""检查 Token 是否已过期(时区安全)"""
if not self.expires_at:
return False
expires = self.expires_at
if expires.tzinfo is None:
# 数据库中的时间应该有时区信息,如果没有则表示数据完整性问题
from src.core.logger import logger
logger.error(f"Management Token {self.id} expires_at 缺少时区信息(数据完整性问题)")
expires = expires.replace(tzinfo=timezone.utc)
return expires < datetime.now(timezone.utc)
class AuditLog(Base):
"""审计日志模型"""
__tablename__ = "audit_logs"
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
event_type = Column(String(50), nullable=False, index=True)
user_id = Column(
String(36), ForeignKey("users.id", ondelete="SET NULL"), nullable=True, index=True
)
api_key_id = Column(String(36), nullable=True)
# 事件详情
description = Column(Text, nullable=False)
ip_address = Column(String(45), nullable=True)
user_agent = Column(String(500), nullable=True)
request_id = Column(String(100), nullable=True, index=True)
# 相关数据
event_metadata = Column(JSON, nullable=True)
# 响应信息
status_code = Column(Integer, nullable=True)
error_message = Column(Text, nullable=True)
# 时间戳
created_at = Column(
DateTime(timezone=True),
default=lambda: datetime.now(timezone.utc),
nullable=False,
index=True,
)
# 关系
user = relationship("User", back_populates="audit_logs")
class RequestCandidate(Base):
"""请求候选记录 - 追踪所有候选(包括未使用的)"""
__tablename__ = "request_candidates"
# 主键
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
# 关联字段
request_id = Column(String(100), nullable=False, index=True)
user_id = Column(
String(36), ForeignKey("users.id", ondelete="SET NULL"), nullable=True, index=True
)
api_key_id = Column(
String(36), ForeignKey("api_keys.id", ondelete="SET NULL"), nullable=True, index=True
)
# 归属快照(删除用户/Key 后仍可追溯)
username = Column(String(100), nullable=True, comment="用户名快照")
api_key_name = Column(String(200), nullable=True, comment="API Key 名称快照")
# 候选信息
candidate_index = Column(Integer, nullable=False) # 候选序号从0开始
retry_index = Column(Integer, nullable=False, default=0) # 重试序号从0开始
provider_id = Column(String(36), ForeignKey("providers.id", ondelete="CASCADE"), nullable=True)
endpoint_id = Column(
String(36),
ForeignKey("provider_endpoints.id", ondelete="CASCADE"),
nullable=True,
index=True,
)
key_id = Column(String(36), nullable=True, index=True, comment="Provider Key ID 快照")
# 状态信息
status = Column(
String(20), nullable=False
) # 'pending', 'streaming', 'success', 'failed', 'cancelled', 'skipped'
skip_reason = Column(Text, nullable=True) # 跳过/失败原因
is_cached = Column(Boolean, default=False) # 是否为缓存亲和性候选
# 执行结果信息(当 status = success/failed 时)
status_code = Column(Integer, nullable=True) # HTTP 状态码
error_type = Column(String(50), nullable=True) # 错误类型
error_message = Column(Text, nullable=True) # 错误消息
latency_ms = Column(Integer, nullable=True) # 延迟(毫秒)
concurrent_requests = Column(Integer, nullable=True) # 并发请求数
# 元数据
extra_data = Column(JSON, nullable=True)
required_capabilities = Column(JSON, nullable=True) # 请求实际需要的能力标签
# 时间戳
created_at = Column(
DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), nullable=False
)
started_at = Column(DateTime(timezone=True), nullable=True) # 开始执行时间
finished_at = Column(DateTime(timezone=True), nullable=True) # 完成时间
# 唯一约束和索引
__table_args__ = (
UniqueConstraint(
"request_id", "candidate_index", "retry_index", name="uq_request_candidate_with_retry"
),
Index("idx_request_candidates_request_id", "request_id"),
Index("idx_request_candidates_status", "status"),
Index("idx_request_candidates_provider_id", "provider_id"),
Index("idx_request_candidates_created_at", "created_at"),
# 复合索引: 按 request_id + status 查询 fallback/retry 场景
Index("idx_rc_request_id_status", "request_id", "status"),
# 复合索引: 按 provider 聚合统计provider_id + status + created_at
Index("idx_rc_provider_status_created", "provider_id", "status", "created_at"),
)
# 关系
user = relationship("User")
api_key = relationship("ApiKey")
provider = relationship("Provider")
endpoint = relationship("ProviderEndpoint")
# ==================== 统计数据模型 ====================
class StatsHourly(Base):
"""小时级统计快照 - 用于时间序列查询"""
__tablename__ = "stats_hourly"
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
# 小时起点 (UTC)
hour_utc = Column(DateTime(timezone=True), nullable=False, unique=True, index=True)
# 请求统计
total_requests = Column(Integer, default=0, nullable=False)
success_requests = Column(Integer, default=0, nullable=False)
error_requests = Column(Integer, default=0, nullable=False)
# Token 统计
input_tokens = Column(BigInteger, default=0, nullable=False)
output_tokens = Column(BigInteger, default=0, nullable=False)
cache_creation_tokens = Column(BigInteger, default=0, nullable=False)
cache_read_tokens = Column(BigInteger, default=0, nullable=False)
# 成本统计 (USD)
total_cost = Column(Numeric(20, 8), default=0.0, nullable=False)
actual_total_cost = Column(Numeric(20, 8), default=0.0, nullable=False)
# 性能统计
avg_response_time_ms = Column(Float, default=0.0, nullable=False)
# 完成标记
is_complete = Column(Boolean, default=False, nullable=False)
aggregated_at = Column(DateTime(timezone=True), nullable=True)
# 时间戳
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,
)
__table_args__ = (Index("idx_stats_hourly_hour", "hour_utc"),)
class StatsHourlyUser(Base):
"""小时级用户维度统计"""
__tablename__ = "stats_hourly_user"
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
hour_utc = Column(DateTime(timezone=True), nullable=False, index=True)
user_id = Column(String(36), nullable=False, index=True)
total_requests = Column(Integer, default=0, nullable=False)
success_requests = Column(Integer, default=0, nullable=False)
error_requests = Column(Integer, default=0, nullable=False)
input_tokens = Column(BigInteger, default=0, nullable=False)
output_tokens = Column(BigInteger, default=0, nullable=False)
total_cost = Column(Numeric(20, 8), default=0.0, 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,
)
__table_args__ = (
UniqueConstraint("hour_utc", "user_id", name="uq_stats_hourly_user"),
Index("idx_stats_hourly_user_hour", "hour_utc"),
Index("idx_stats_hourly_user_user_hour", "user_id", "hour_utc"),
)
class StatsHourlyModel(Base):
"""小时级模型维度统计"""
__tablename__ = "stats_hourly_model"
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
hour_utc = Column(DateTime(timezone=True), nullable=False, index=True)
model = Column(String(100), nullable=False, index=True)
total_requests = Column(Integer, default=0, nullable=False)
input_tokens = Column(BigInteger, default=0, nullable=False)
output_tokens = Column(BigInteger, default=0, nullable=False)
total_cost = Column(Numeric(20, 8), default=0.0, nullable=False)
avg_response_time_ms = Column(Float, default=0.0, 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,
)
__table_args__ = (
UniqueConstraint("hour_utc", "model", name="uq_stats_hourly_model"),
Index("idx_stats_hourly_model_hour", "hour_utc"),
Index("idx_stats_hourly_model_model_hour", "model", "hour_utc"),
)
class StatsHourlyProvider(Base):
"""小时级提供商维度统计"""
__tablename__ = "stats_hourly_provider"
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
hour_utc = Column(DateTime(timezone=True), nullable=False, index=True)
provider_name = Column(String(100), nullable=False, index=True)
total_requests = Column(Integer, default=0, nullable=False)
input_tokens = Column(BigInteger, default=0, nullable=False)
output_tokens = Column(BigInteger, default=0, nullable=False)
total_cost = Column(Numeric(20, 8), default=0.0, 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,
)
__table_args__ = (
UniqueConstraint("hour_utc", "provider_name", name="uq_stats_hourly_provider"),
Index("idx_stats_hourly_provider_hour", "hour_utc"),
)
class StatsDaily(Base):
"""每日统计快照 - 用于快速查询历史数据"""
__tablename__ = "stats_daily"
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
# 统计日期 (UTC)
date = Column(DateTime(timezone=True), nullable=False, unique=True, index=True)
# 请求统计
total_requests = Column(Integer, default=0, nullable=False)
success_requests = Column(Integer, default=0, nullable=False)
error_requests = Column(Integer, default=0, nullable=False)
# Token 统计
input_tokens = Column(BigInteger, default=0, nullable=False)
output_tokens = Column(BigInteger, default=0, nullable=False)
cache_creation_tokens = Column(BigInteger, default=0, nullable=False)
cache_read_tokens = Column(BigInteger, default=0, nullable=False)
# 成本统计 (USD)
total_cost = Column(Numeric(20, 8), default=0.0, nullable=False)
actual_total_cost = Column(Numeric(20, 8), default=0.0, nullable=False) # 倍率后成本
input_cost = Column(Numeric(20, 8), default=0.0, nullable=False)
output_cost = Column(Numeric(20, 8), default=0.0, nullable=False)
cache_creation_cost = Column(Numeric(20, 8), default=0.0, nullable=False)
cache_read_cost = Column(Numeric(20, 8), default=0.0, nullable=False)
# 性能统计
avg_response_time_ms = Column(Float, default=0.0, nullable=False)
p50_response_time_ms = Column(Integer, nullable=True)
p90_response_time_ms = Column(Integer, nullable=True)
p99_response_time_ms = Column(Integer, nullable=True)
p50_first_byte_time_ms = Column(Integer, nullable=True)
p90_first_byte_time_ms = Column(Integer, nullable=True)
p99_first_byte_time_ms = Column(Integer, nullable=True)
fallback_count = Column(Integer, default=0, nullable=False) # Provider 切换次数
# 使用维度统计
unique_models = Column(Integer, default=0, server_default="0", nullable=False)
unique_providers = Column(Integer, default=0, server_default="0", nullable=False)
# 完成标记
is_complete = Column(Boolean, default=False, nullable=False)
aggregated_at = Column(DateTime(timezone=True), nullable=True)
# 时间戳
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,
)
class StatsDailyModel(Base):
"""每日模型统计快照 - 用于快速查询每日模型维度数据"""
__tablename__ = "stats_daily_model"
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
# 统计日期 (UTC)
date = Column(DateTime(timezone=True), nullable=False, index=True)
# 模型名称
model = Column(String(100), nullable=False)
# 请求统计
total_requests = Column(Integer, default=0, nullable=False)
# Token 统计
input_tokens = Column(BigInteger, default=0, nullable=False)
output_tokens = Column(BigInteger, default=0, nullable=False)
cache_creation_tokens = Column(BigInteger, default=0, nullable=False)
cache_read_tokens = Column(BigInteger, default=0, nullable=False)
# 成本统计 (USD)
total_cost = Column(Numeric(20, 8), default=0.0, nullable=False)
# 性能统计
avg_response_time_ms = Column(Float, default=0.0, 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,
)
# 唯一约束:每个模型每天只有一条记录
__table_args__ = (
UniqueConstraint("date", "model", name="uq_stats_daily_model"),
Index("idx_stats_daily_model_date", "date"),
Index("idx_stats_daily_model_date_model", "date", "model"),
)
class StatsDailyProvider(Base):
"""每日供应商统计快照 - 用于快速查询每日供应商维度数据"""
__tablename__ = "stats_daily_provider"
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
# 统计日期 (UTC)
date = Column(DateTime(timezone=True), nullable=False, index=True)
# 供应商名称
provider_name = Column(String(100), nullable=False)
# 请求统计
total_requests = Column(Integer, default=0, nullable=False)
# Token 统计
input_tokens = Column(BigInteger, default=0, nullable=False)
output_tokens = Column(BigInteger, default=0, nullable=False)
cache_creation_tokens = Column(BigInteger, default=0, nullable=False)
cache_read_tokens = Column(BigInteger, default=0, nullable=False)
# 成本统计 (USD)
total_cost = Column(Numeric(20, 8), default=0.0, 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,
)
# 唯一约束:每个供应商每天只有一条记录
__table_args__ = (
UniqueConstraint("date", "provider_name", name="uq_stats_daily_provider"),
Index("idx_stats_daily_provider_date", "date"),
Index("idx_stats_daily_provider_date_provider", "date", "provider_name"),
)
class StatsDailyApiKey(Base):
"""API Key 每日统计"""
__tablename__ = "stats_daily_api_key"
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
api_key_id = Column(String(36), ForeignKey("api_keys.id", ondelete="SET NULL"), nullable=True)
api_key_name = Column(
String(200), nullable=True, comment="API Key 名称快照(删除 Key 后仍可追溯)"
)
date = Column(DateTime(timezone=True), nullable=False, index=True)
total_requests = Column(Integer, default=0, nullable=False)
success_requests = Column(Integer, default=0, nullable=False)
error_requests = Column(Integer, default=0, nullable=False)
input_tokens = Column(BigInteger, default=0, nullable=False)
output_tokens = Column(BigInteger, default=0, nullable=False)
cache_creation_tokens = Column(BigInteger, default=0, nullable=False)
cache_read_tokens = Column(BigInteger, default=0, nullable=False)
total_cost = Column(Numeric(20, 8), default=0.0, 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,
)
__table_args__ = (
UniqueConstraint("api_key_id", "date", name="uq_stats_daily_api_key"),
Index("idx_stats_daily_api_key_date", "date"),
Index("idx_stats_daily_api_key_key_date", "api_key_id", "date"),
Index("idx_stats_daily_api_key_date_requests", "date", "total_requests"),
Index("idx_stats_daily_api_key_date_cost", "date", "total_cost"),
)
api_key = relationship("ApiKey")
class StatsDailyError(Base):
"""每日错误统计"""
__tablename__ = "stats_daily_error"
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
date = Column(DateTime(timezone=True), nullable=False, index=True)
error_category = Column(String(50), nullable=False)
provider_name = Column(String(100), nullable=True)
model = Column(String(100), nullable=True)
count = Column(Integer, default=0, 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,
)
__table_args__ = (
UniqueConstraint(
"date",
"error_category",
"provider_name",
"model",
name="uq_stats_daily_error",
),
Index("idx_stats_daily_error_date", "date"),
Index("idx_stats_daily_error_category", "date", "error_category"),
)
class StatsSummary(Base):
"""全局统计汇总 - 单行记录,存储截止到昨天的累计数据"""
__tablename__ = "stats_summary"
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
# 统计截止日期 (不含当天)
cutoff_date = Column(DateTime(timezone=True), nullable=False)
# 累计请求统计
all_time_requests = Column(Integer, default=0, nullable=False)
all_time_success_requests = Column(Integer, default=0, nullable=False)
all_time_error_requests = Column(Integer, default=0, nullable=False)
# 累计 Token 统计
all_time_input_tokens = Column(BigInteger, default=0, nullable=False)
all_time_output_tokens = Column(BigInteger, default=0, nullable=False)
all_time_cache_creation_tokens = Column(BigInteger, default=0, nullable=False)
all_time_cache_read_tokens = Column(BigInteger, default=0, nullable=False)
# 累计成本统计 (USD)
all_time_cost = Column(Numeric(20, 8), default=0.0, nullable=False)
all_time_actual_cost = Column(Numeric(20, 8), default=0.0, nullable=False)
# 累计用户/API Key 统计 (快照)
total_users = Column(Integer, default=0, nullable=False)
active_users = Column(Integer, default=0, nullable=False)
total_api_keys = Column(Integer, default=0, nullable=False)
active_api_keys = Column(Integer, default=0, 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,
)
class StatsUserDaily(Base):
"""用户每日统计快照 - 用于用户仪表盘"""
__tablename__ = "stats_user_daily"
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
# 用户关联
user_id = Column(String(36), ForeignKey("users.id", ondelete="SET NULL"), nullable=True)
username = Column(String(100), nullable=True, comment="用户名快照(删除用户后仍可追溯)")
# 统计日期 (UTC)
date = Column(DateTime(timezone=True), nullable=False, index=True)
# 请求统计
total_requests = Column(Integer, default=0, nullable=False)
success_requests = Column(Integer, default=0, nullable=False)
error_requests = Column(Integer, default=0, nullable=False)
# Token 统计
input_tokens = Column(BigInteger, default=0, nullable=False)
output_tokens = Column(BigInteger, default=0, nullable=False)
cache_creation_tokens = Column(BigInteger, default=0, nullable=False)
cache_read_tokens = Column(BigInteger, default=0, nullable=False)
# 成本统计 (USD)
total_cost = Column(Numeric(20, 8), default=0.0, 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,
)
# 唯一约束:每个用户每天只有一条记录
__table_args__ = (
UniqueConstraint("user_id", "date", name="uq_stats_user_daily"),
Index("idx_stats_user_daily_user_date", "user_id", "date"),
)
# 关系
user = relationship("User")
class GeminiFileMapping(Base):
"""
Gemini Files API 文件与 Provider Key 的映射关系
用于持久化存储 file_id → key_id 的绑定关系,
确保后续 generateContent 请求使用上传时的同一 Key。
Gemini 文件有 48 小时有效期,此表中的记录也会在过期后被清理。
"""
__tablename__ = "gemini_file_mappings"
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
# 文件名(如 files/abc123xyz
file_name = Column(String(255), nullable=False, unique=True, index=True)
# Provider Key ID关联到 provider_api_keys 表)
key_id = Column(
String(36),
ForeignKey("provider_api_keys.id", ondelete="CASCADE"),
nullable=False,
index=True,
)
# 用户 ID用于权限验证可选
user_id = Column(
String(36), ForeignKey("users.id", ondelete="CASCADE"), nullable=True, index=True
)
# 文件元数据(可选,用于调试)
display_name = Column(String(255), nullable=True)
mime_type = Column(String(100), nullable=True)
# 源文件哈希(用于关联相同源文件的不同上传,可选)
# 当同一源文件上传到多个 Key 时,可通过此字段找到所有等效文件
source_hash = Column(String(64), nullable=True, index=True)
# 时间戳
created_at = Column(
DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), nullable=False
)
# 过期时间Gemini 文件 48 小时后过期)
expires_at = Column(DateTime(timezone=True), nullable=False, index=True)
# 关系
key = relationship("ProviderAPIKey")
user = relationship("User")
__table_args__ = (
Index("idx_gemini_file_mappings_expires", "expires_at"),
Index("idx_gemini_file_mappings_source_hash", "source_hash"),
)
class UserModelUsageCount(Base):
"""用户-模型维度调用次数计数器
每个用户对每个模型维护一个原子递增的计数器,
避免从 Usage 表聚合查询,查询性能 O(N) 其中 N 是用户使用过的模型数。
"""
__tablename__ = "user_model_usage_counts"
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
user_id = Column(String(36), ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
model = Column(String(100), nullable=False)
usage_count = Column(Integer, default=0, 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,
)
__table_args__ = (
UniqueConstraint("user_id", "model", name="uq_user_model_usage_count"),
Index("idx_user_model_usage_user", "user_id"),
Index("idx_user_model_usage_model", "model"),
)
# 导入扩展的数据库模型
from .database_extensions import ApiKeyProviderMapping, ProviderUsageTracking