Files
Aether/_deprecated_py_src/models/database.py

3255 lines
126 KiB
Python
Raw Normal View History

2025-12-10 20:52:44 +08:00
"""
数据库模型定义
"""
from __future__ import annotations
2025-12-10 20:52:44 +08:00
import hashlib
import secrets
import uuid
from datetime import date, datetime, timezone
2025-12-10 20:52:44 +08:00
from enum import Enum as PyEnum
from typing import Any, ClassVar
2025-12-10 20:52:44 +08:00
import bcrypt
from sqlalchemy import (
JSON,
BigInteger,
Boolean,
CheckConstraint,
2025-12-10 20:52:44 +08:00
Column,
Date,
2025-12-10 20:52:44 +08:00
DateTime,
Enum,
Float,
ForeignKey,
Index,
Integer,
LargeBinary,
Numeric,
2025-12-10 20:52:44 +08:00
String,
Text,
UniqueConstraint,
event,
inspect,
text,
2025-12-10 20:52:44 +08:00
)
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.orm import backref, declarative_base, relationship
2025-12-10 20:52:44 +08:00
from ..config import config
2026-01-02 16:17:24 +08:00
from ..core.enums import AuthSource, ProviderBillingType, UserRole
2025-12-10 20:52:44 +08:00
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
)
2025-12-10 20:52:44 +08:00
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)
2025-12-10 20:52:44 +08:00
username = Column(String(100), unique=True, index=True, nullable=False)
# OAuth 用户可能没有本地密码
password_hash = Column(String(255), nullable=True)
2025-12-10 20:52:44 +08:00
role = Column(
Enum(
UserRole,
name="userrole",
create_type=False,
values_callable=lambda x: [e.value for e in x],
),
default=UserRole.USER,
nullable=False,
)
2026-01-02 16:17:24 +08:00
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,
)
2025-12-10 20:52:44 +08:00
# LDAP 标识(仅 auth_source=ldap 时使用,用于在邮箱变更/用户名冲突时稳定关联本地账户)
ldap_dn = Column(String(512), nullable=True, index=True)
ldap_username = Column(String(255), nullable=True, index=True)
2025-12-10 20:52:44 +08:00
# 访问限制NULL 表示不限制,允许访问所有资源)
allowed_providers = Column(JSON, nullable=True) # 允许使用的提供商 ID 列表
allowed_api_formats = Column(JSON, nullable=True) # 允许使用的 API 格式列表
2025-12-10 20:52:44 +08:00
allowed_models = Column(JSON, nullable=True) # 允许使用的模型名称列表
rate_limit = Column(
Integer, nullable=True, default=None
) # 每分钟请求限制NULL=继承系统默认0=不限制N=N RPM
2025-12-10 20:52:44 +08:00
# 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")
2025-12-10 20:52:44 +08:00
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",
)
2025-12-10 20:52:44 +08:00
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:
2025-12-10 20:52:44 +08:00
"""设置密码"""
try:
self.password_hash = bcrypt.hashpw(password.encode("utf-8"), bcrypt.gensalt()).decode(
"utf-8"
)
except ValueError as exc:
raise ValueError("密码长度不能超过72字节") from exc
2025-12-10 20:52:44 +08:00
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)
2025-12-10 20:52:44 +08:00
class ApiKey(Base):
"""API密钥模型"""
__tablename__ = "api_keys"
__table_args__ = (
CheckConstraint(
"(NOT is_standalone) OR (NOT is_locked)",
name="ck_api_keys_standalone_not_locked",
),
)
2025-12-10 20:52:44 +08:00
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
)
2025-12-10 20:52:44 +08:00
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)
2025-12-10 20:52:44 +08:00
# 钱包体系:余额/额度由 wallets 表统一管理
2025-12-10 20:52:44 +08:00
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=不限制
2025-12-10 20:52:44 +08:00
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可锁定
2025-12-10 20:52:44 +08:00
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)
2025-12-10 20:52:44 +08:00
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"),
)
2025-12-10 20:52:44 +08:00
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
2025-12-10 20:52:44 +08:00
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
)
2025-12-10 20:52:44 +08:00
# 归属快照(删除用户/Key 后仍可追溯)
username = Column(String(100), nullable=True, comment="用户名快照")
api_key_name = Column(String(200), nullable=True, comment="API Key 名称快照")
2025-12-10 20:52:44 +08:00
# 请求信息
request_id = Column(String(100), unique=True, index=True, nullable=False)
provider_name = Column(String(100), nullable=False) # Provider 名称(非外键)
2025-12-10 20:52:44 +08:00
model = Column(String(100), nullable=False)
target_model = Column(
String(100), nullable=True, comment="映射后的目标模型名(若无映射则为空)"
)
2025-12-10 20:52:44 +08:00
# 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,
2025-12-10 20:52:44 +08:00
)
provider_api_key_id = Column(
String(36),
ForeignKey("provider_api_keys.id", ondelete="SET NULL"),
nullable=True,
index=True,
2025-12-10 20:52:44 +08:00
)
# 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 缓存创建
2025-12-10 20:52:44 +08:00
# 成本计算
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)
2025-12-10 20:52:44 +08:00
# 真实成本计算(表面成本 × 倍率)
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
2025-12-10 20:52:44 +08:00
# 历史价格记录每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) # 按次计费单价(历史记录)
2025-12-10 20:52:44 +08:00
# 请求详情
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) # 是否发生了格式转换
2025-12-10 20:52:44 +08:00
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毫秒
2025-12-10 20:52:44 +08:00
# 请求状态追踪
# pending: 请求开始处理中
# streaming: 流式响应进行中
# completed: 请求成功完成
# failed: 请求失败
# cancelled: 客户端主动断开连接
2025-12-10 20:52:44 +08:00
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) # 结算后赠款余额
2025-12-10 20:52:44 +08:00
# 完整请求和响应记录
request_headers = Column(JSON, nullable=True) # 客户端请求头
request_body = Column(JSON, nullable=True) # 客户端原始请求体7天内未压缩
2025-12-10 20:52:44 +08:00
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) # 返回给客户端的响应体(格式转换后)
2025-12-10 20:52:44 +08:00
# 压缩存储字段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压缩的客户端响应体
2025-12-10 20:52:44 +08:00
# 元数据
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")
2025-12-10 20:52:44 +08:00
provider_obj = relationship("Provider") # 使用 provider_obj 避免与 provider 字段名冲突
provider_endpoint = relationship("ProviderEndpoint")
provider_api_key = relationship("ProviderAPIKey")
def get_request_body(self) -> Any:
"""获取客户端原始请求体(自动解压)"""
2025-12-10 20:52:44 +08:00
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:
"""获取提供商原始响应体(自动解压)"""
2025-12-10 20:52:44 +08:00
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
2025-12-10 20:52:44 +08:00
class Wallet(Base):
"""统一钱包模型(用户钱包 / 独立 API Key 钱包)"""
2025-12-10 20:52:44 +08:00
__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"),
)
2025-12-10 20:52:44 +08:00
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
)
2025-12-10 20:52:44 +08:00
# 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")
2025-12-10 20:52:44 +08:00
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)
2025-12-10 20:52:44 +08:00
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,
)
2025-12-10 20:52:44 +08:00
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")
2025-12-10 20:52:44 +08:00
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)
2025-12-10 20:52:44 +08:00
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)
2025-12-10 20:52:44 +08:00
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")
2025-12-10 20:52:44 +08:00
class SystemConfig(Base):
"""系统配置表"""
__tablename__ = "system_configs"
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
2025-12-10 20:52:44 +08:00
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,
)
2026-01-02 16:17:24 +08:00
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可能很长
2026-01-02 16:17:24 +08:00
user_search_filter = Column(
Text, default="(uid={username})", nullable=False
) # 用户搜索过滤器(可能很复杂)
username_attr = Column(
String(50), default="uid", nullable=False
) # 用户名属性 (uid/sAMAccountName)
2026-01-02 16:17:24 +08:00
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
2026-01-04 16:27:02 +08:00
connect_timeout = Column(Integer, default=10, nullable=False) # 连接超时时间(秒)
2026-01-02 16:17:24 +08:00
# 时间戳
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):
2025-12-10 20:52:44 +08:00
"""提供商配置表"""
__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) # 提供商名称(唯一)
2025-12-10 20:52:44 +08:00
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)
2025-12-10 20:52:44 +08:00
# 计费类型配置
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) # 本月已用额度
2025-12-10 20:52:44 +08:00
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)
2025-12-10 20:52:44 +08:00
# 状态
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) # 非流式请求整体超时
2025-12-10 20:52:44 +08:00
# 配置
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"
)
2025-12-10 20:52:44 +08:00
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):
2025-12-10 20:52:44 +08:00
"""提供商端点 - 一个提供商可以有多个 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()))
2025-12-10 20:52:44 +08:00
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/...
2025-12-10 20:52:44 +08:00
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) # 最大重试次数
2025-12-10 20:52:44 +08:00
# 状态
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}
2025-12-10 20:52:44 +08:00
# 时间戳
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"),
2025-12-10 20:52:44 +08:00
)
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"),)
2025-12-10 20:52:44 +08:00
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):
2025-12-10 20:52:44 +08:00
"""全局统一模型定义 - 包含价格和能力配置
设计原则:
- 定义模型的基本信息和价格配置价格为必填项
- 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()))
2025-12-10 20:52:44 +08:00
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
) # 每次请求固定费用
2025-12-10 20:52:44 +08:00
# 统一阶梯计费配置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}
2025-12-10 20:52:44 +08:00
# ]
# },
# {"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)
2025-12-10 20:52:44 +08:00
# 状态
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):
2025-12-10 20:52:44 +08:00
"""Provider 模型配置表 - Provider 如何使用某个 GlobalModel
设计原则:
- Model 表示 Provider 对某个模型的具体实现
- global_model_id 必填必须关联到一个 GlobalModel
2025-12-10 20:52:44 +08:00
- 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()))
2025-12-10 20:52:44 +08:00
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)
2025-12-10 20:52:44 +08:00
# 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)
2025-12-10 20:52:44 +08:00
# 按次计费配置(每次请求的固定费用,美元)- 可为空,为空时使用 GlobalModel 的默认值
price_per_request = Column(Numeric(20, 8), nullable=True) # 每次请求固定费用
2025-12-10 20:52:44 +08:00
# 阶梯计费配置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)
2025-12-10 20:52:44 +08:00
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)
2025-12-10 20:52:44 +08:00
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 模型名称
2025-12-10 20:52:44 +08:00
如果配置了 provider_model_mappings按优先级选择数字越小越优先
相同优先级的映射通过哈希分散实现负载均衡 Key 调度策略一致
否则返回 provider_model_name
2025-12-10 20:52:44 +08:00
Args:
affinity_key: 用于哈希分散的亲和键如用户 API Key 哈希确保同一用户稳定选择同一映射
api_format: 当前请求的 endpoint signature "openai:chat"用于过滤适用的映射
"""
import hashlib
2025-12-10 20:52:44 +08:00
if not self.provider_model_mappings:
return self.provider_model_name
2025-12-10 20:52:44 +08:00
raw_mappings = self.provider_model_mappings
if not isinstance(raw_mappings, list) or len(raw_mappings) == 0:
return self.provider_model_name
2025-12-10 20:52:44 +08:00
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
2025-12-10 20:52:44 +08:00
# 检查 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
2025-12-10 20:52:44 +08:00
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 格式"""
2025-12-10 20:52:44 +08:00
__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()))
2025-12-10 20:52:44 +08:00
# 外键关系 - 直接关联 Provider
provider_id = Column(
String(36), ForeignKey("providers.id", ondelete="CASCADE"), nullable=False, index=True
2025-12-10 20:52:44 +08:00
)
# 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)
2025-12-10 20:52:44 +08:00
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}
2025-12-10 20:52:44 +08:00
# 优先级配置 (数字越小越优先)
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}
2025-12-10 20:52:44 +08:00
# RPM 限制配置(自适应学习)
# rpm_limit 决定 RPM 控制模式:
# - NULL: 自适应模式,系统自动学习并调整(使用 learned_rpm_limit
2025-12-10 20:52:44 +08:00
# - 数字: 固定限制模式,使用用户指定的值
rpm_limit = Column(Integer, nullable=True, default=None)
2025-12-10 20:52:44 +08:00
# 模型权限控制
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 限制(自适应模式下的有效值)
2025-12-10 20:52:44 +08:00
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调整历史
2025-12-10 20:52:44 +08:00
# 基于滑动窗口的利用率追踪
utilization_samples = Column(
JSON, nullable=True
) # 利用率采样窗口 [{"ts": timestamp, "util": 0.8}, ...]
last_probe_increase_at = Column(DateTime(timezone=True), nullable=True) # 上次探测性扩容时间
2025-12-10 20:52:44 +08:00
# 健康度追踪(按 endpoint signature 存储)
# 结构: {"claude:chat": {"health_score": 1.0, "consecutive_failures": 0, ...}, ...}
health_by_format = Column(JSON, nullable=True, default=dict)
2025-12-10 20:52:44 +08:00
# 缓存与熔断配置
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)
2025-12-10 20:52:44 +08:00
# 使用统计
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) # 累计成本
2025-12-10 20:52:44 +08:00
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)
2025-12-10 20:52:44 +08:00
# 时间戳
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"),)
2025-12-10 20:52:44 +08:00
# 关系
provider = relationship("Provider", back_populates="api_keys")
2025-12-10 20:52:44 +08:00
_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"),
)
2025-12-10 20:52:44 +08:00
class UserPreference(Base):
"""用户偏好设置表"""
__tablename__ = "user_preferences"
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
2025-12-10 20:52:44 +08:00
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)
2025-12-10 20:52:44 +08:00
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()))
2025-12-10 20:52:44 +08:00
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
)
2025-12-10 20:52:44 +08:00
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()))
2025-12-10 20:52:44 +08:00
user_id = Column(String(36), ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
announcement_id = Column(String(36), ForeignKey("announcements.id"), nullable=False, index=True)
2025-12-10 20:52:44 +08:00
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)
2025-12-10 20:52:44 +08:00
class AuditLog(Base):
"""审计日志模型"""
__tablename__ = "audit_logs"
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
2025-12-10 20:52:44 +08:00
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
)
2025-12-10 20:52:44 +08:00
# 归属快照(删除用户/Key 后仍可追溯)
username = Column(String(100), nullable=True, comment="用户名快照")
api_key_name = Column(String(200), nullable=True, comment="API Key 名称快照")
2025-12-10 20:52:44 +08:00
# 候选信息
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,
2025-12-10 20:52:44 +08:00
)
key_id = Column(String(36), nullable=True, index=True, comment="Provider Key ID 快照")
2025-12-10 20:52:44 +08:00
# 状态信息
status = Column(
String(20), nullable=False
) # 'pending', 'streaming', 'success', 'failed', 'cancelled', 'skipped'
2025-12-10 20:52:44 +08:00
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"),
2025-12-10 20:52:44 +08:00
)
# 关系
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"),
)
2025-12-10 20:52:44 +08:00
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)
2025-12-10 20:52:44 +08:00
# 性能统计
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)
2025-12-10 20:52:44 +08:00
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)
2025-12-10 20:52:44 +08:00
# 时间戳
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"),
)
2025-12-10 20:52:44 +08:00
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)
2025-12-10 20:52:44 +08:00
# 累计用户/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="用户名快照(删除用户后仍可追溯)")
2025-12-10 20:52:44 +08:00
# 统计日期 (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)
2025-12-10 20:52:44 +08:00
# 时间戳
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"),
)
2025-12-10 20:52:44 +08:00
# 导入扩展的数据库模型
from .database_extensions import ApiKeyProviderMapping, ProviderUsageTracking