Files
Aether/src/models/database.py
fawney19 3e50c157be feat: add HTTP/SOCKS5 proxy support for API endpoints
- Add proxy field to ProviderEndpoint database model with migration
- Add ProxyConfig Pydantic model for proxy URL validation
- Extend HTTP client pool with create_client_with_proxy method
- Integrate proxy configuration in chat_handler_base.py and cli_handler_base.py
- Update admin API endpoints to support proxy configuration CRUD
- Add proxy configuration UI in frontend EndpointFormDialog

Fixes #28
2025-12-18 14:46:47 +08:00

1404 lines
54 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

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

"""
数据库模型定义
"""
import hashlib
import secrets
import uuid
from datetime import datetime, timezone
from enum import Enum as PyEnum
from typing import Optional
import bcrypt
from sqlalchemy import (
JSON,
BigInteger,
Boolean,
Column,
DateTime,
Enum,
Float,
ForeignKey,
Index,
Integer,
LargeBinary,
String,
Text,
UniqueConstraint,
)
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.orm import declarative_base, relationship
from ..config import config
from ..core.enums import ProviderBillingType, UserRole
Base = declarative_base()
class User(Base):
"""用户模型"""
__tablename__ = "users"
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()), index=True)
email = Column(String(255), unique=True, index=True, nullable=False)
username = Column(String(100), unique=True, index=True, nullable=False)
password_hash = Column(String(255), nullable=False)
role = Column(
Enum(
UserRole,
name="userrole",
create_type=False,
values_callable=lambda x: [e.value for e in x],
),
default=UserRole.USER,
nullable=False,
)
# 访问限制NULL 表示不限制,允许访问所有资源)
allowed_providers = Column(JSON, nullable=True) # 允许使用的提供商 ID 列表
allowed_endpoints = Column(JSON, nullable=True) # 允许使用的端点 ID 列表
allowed_models = Column(JSON, nullable=True) # 允许使用的模型名称列表
# Key 能力配置
model_capability_settings = Column(JSON, nullable=True) # 用户针对特定模型的能力配置
# 示例: {"claude-sonnet-4-20250514": {"cache_1h": true}}
# 配额管理
quota_usd = Column(Float, nullable=True) # 美元配额(NULL 表示无限制)
used_usd = Column(Float, default=0.0) # 当前周期已使用美元
total_usd = Column(Float, default=0.0) # 累积消费总额
# 状态
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")
preferences = relationship(
"UserPreference", back_populates="user", cascade="all, delete-orphan", passive_deletes=True
)
quotas = relationship(
"UserQuota", 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)
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):
"""设置密码"""
self.password_hash = bcrypt.hashpw(password.encode("utf-8"), bcrypt.gensalt()).decode(
"utf-8"
)
def verify_password(self, password: str) -> bool:
"""验证密码"""
if not self.password_hash:
return False
return bcrypt.checkpw(password.encode("utf-8"), self.password_hash.encode("utf-8"))
class ApiKey(Base):
"""API密钥模型"""
__tablename__ = "api_keys"
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()), index=True)
user_id = Column(String(36), ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
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(Float, default=0.0)
# 余额管理(仅用于独立余额 Key
balance_used_usd = Column(Float, default=0.0) # 已使用余额USD用于统计
current_balance_usd = Column(Float, nullable=True) # 当前余额USDNULL 表示无限制
is_standalone = Column(
Boolean, default=False, nullable=False
) # 是否为独立余额 Key给非注册用户使用
# 访问限制NULL 表示不限制,允许访问所有资源)
allowed_providers = Column(JSON, nullable=True) # 允许使用的提供商 ID 列表
allowed_endpoints = Column(JSON, nullable=True) # 允许使用的端点 ID 列表
allowed_api_formats = Column(JSON, nullable=True) # 允许使用的 API 格式列表
allowed_models = Column(JSON, nullable=True) # 允许使用的模型名称列表
rate_limit = Column(Integer, default=100) # 每分钟请求限制
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)
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")
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"
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()), index=True)
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)
# 请求信息
request_id = Column(String(100), unique=True, index=True, nullable=False)
provider = Column(String(100), nullable=False)
model = Column(String(100), nullable=False)
target_model = Column(String(100), nullable=True, comment="映射后的目标模型名(若无映射则为空)")
# Provider 侧追踪信息(记录最终成功的 Provider/Endpoint/Key
provider_id = Column(String(36), ForeignKey("providers.id", ondelete="SET NULL"), nullable=True)
provider_endpoint_id = Column(
String(36), ForeignKey("provider_endpoints.id", ondelete="SET NULL"), nullable=True
)
provider_api_key_id = Column(
String(36), ForeignKey("provider_api_keys.id", ondelete="SET NULL"), nullable=True
)
# Token统计
input_tokens = Column(Integer, default=0)
output_tokens = Column(Integer, default=0)
total_tokens = Column(Integer, default=0)
# 缓存相关 tokens (for Claude models)
cache_creation_input_tokens = Column(Integer, default=0)
cache_read_input_tokens = Column(Integer, default=0)
# 成本计算
input_cost_usd = Column(Float, default=0.0)
output_cost_usd = Column(Float, default=0.0)
cache_cost_usd = Column(Float, default=0.0) # 总缓存成本(兼容旧数据)
cache_creation_cost_usd = Column(Float, default=0.0) # 缓存创建成本
cache_read_cost_usd = Column(Float, default=0.0) # 缓存读取成本
request_cost_usd = Column(Float, default=0.0) # 按次计费成本
total_cost_usd = Column(Float, default=0.0)
# 真实成本计算(表面成本 × 倍率)
actual_input_cost_usd = Column(Float, default=0.0) # 真实输入成本
actual_output_cost_usd = Column(Float, default=0.0) # 真实输出成本
actual_cache_creation_cost_usd = Column(Float, default=0.0) # 真实缓存创建成本
actual_cache_read_cost_usd = Column(Float, default=0.0) # 真实缓存读取成本
actual_request_cost_usd = Column(Float, default=0.0) # 真实按次计费成本
actual_total_cost_usd = Column(Float, default=0.0) # 真实总成本
rate_multiplier = Column(Float, default=1.0) # 使用的倍率(来自 ProviderAPIKey
# 历史价格记录每1M tokens的美元价格记录请求时的实际价格
input_price_per_1m = Column(Float, nullable=True) # 输入单价
output_price_per_1m = Column(Float, nullable=True) # 输出单价
cache_creation_price_per_1m = Column(Float, nullable=True) # 缓存创建单价
cache_read_price_per_1m = Column(Float, nullable=True) # 缓存读取单价
price_per_request = Column(Float, nullable=True) # 按次计费单价(历史记录)
# 请求详情
request_type = Column(String(50)) # chat, completion, embedding等
api_format = Column(String(50), nullable=True) # API 格式: CLAUDE, OPENAI 等
is_stream = Column(Boolean, default=False) # 是否为流式请求
status_code = Column(Integer)
error_message = Column(Text, nullable=True)
response_time_ms = Column(Integer) # 总响应时间(毫秒)
first_byte_time_ms = Column(Integer, nullable=True) # 首字时间/TTFB毫秒
# 请求状态追踪
# pending: 请求开始处理中
# streaming: 流式响应进行中
# completed: 请求成功完成
# failed: 请求失败
status = Column(String(20), default="completed", nullable=False, index=True)
# 完整请求和响应记录
request_headers = Column(JSON, nullable=True) # 客户端请求头
request_body = Column(JSON, nullable=True) # 请求体7天内未压缩
provider_request_headers = Column(JSON, nullable=True) # 向提供商发送的请求头
response_headers = Column(JSON, nullable=True) # 响应头
response_body = Column(JSON, nullable=True) # 响应体7天内未压缩
# 压缩存储字段7天后自动压缩到这里
request_body_compressed = Column(LargeBinary, nullable=True) # gzip压缩的请求体
response_body_compressed = Column(LargeBinary, nullable=True) # gzip压缩的响应体
# 元数据
request_metadata = Column(JSON, nullable=True) # 存储额外信息
# 时间戳
created_at = Column(
DateTime(timezone=True),
default=lambda: datetime.now(timezone.utc),
nullable=False,
index=True,
)
# 关系
user = relationship("User", back_populates="usage_records")
api_key = relationship("ApiKey", back_populates="usage_records")
provider_obj = relationship("Provider") # 使用 provider_obj 避免与 provider 字段名冲突
provider_endpoint = relationship("ProviderEndpoint")
provider_api_key = relationship("ProviderAPIKey")
def get_request_body(self):
"""获取请求体(自动解压)"""
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_response_body(self):
"""获取响应体(自动解压)"""
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
class UserQuota(Base):
"""用户配额历史记录"""
__tablename__ = "user_quotas"
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()), index=True)
user_id = Column(String(36), ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
# 配额类型
quota_type = Column(String(50), nullable=False) # monthly, daily, custom
# 配额值
quota_usd = Column(Float, nullable=False)
# 时间范围
period_start = Column(DateTime(timezone=True), nullable=False)
period_end = Column(DateTime(timezone=True), nullable=False)
# 使用情况
used_usd = Column(Float, default=0.0)
# 状态
is_active = 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="quotas")
class SystemConfig(Base):
"""系统配置表"""
__tablename__ = "system_configs"
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()), index=True)
key = Column(String(100), unique=True, nullable=False)
value = Column(JSON, nullable=False)
description = Column(Text, nullable=True)
# 时间戳
created_at = Column(
DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), nullable=False
)
updated_at = Column(
DateTime(timezone=True),
default=lambda: datetime.now(timezone.utc),
onupdate=lambda: datetime.now(timezone.utc),
nullable=False,
)
class Provider(Base):
"""提供商配置表"""
__tablename__ = "providers"
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()), index=True)
name = Column(String(100), unique=True, nullable=False, index=True) # 提供商唯一标识
display_name = Column(String(100), nullable=False) # 显示名称
description = Column(Text, nullable=True) # 提供商描述
website = Column(String(500), nullable=True) # 主站网站
# 计费类型配置
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(Float, nullable=True) # 月卡总额度
monthly_used_usd = Column(Float, default=0.0) # 本月已用额度
quota_reset_day = Column(Integer, default=30) # 额度重置周期(天数)例如7=每周30=每月
quota_last_reset_at = Column(DateTime(timezone=True), nullable=True) # 上次额度重置时间
quota_expires_at = Column(DateTime(timezone=True), nullable=True) # 月卡过期时间
# RPM限制NULL=无限制0=禁止请求
rpm_limit = Column(Integer, nullable=True) # 每分钟请求数限制NULL=无限制0=禁止请求)
rpm_used = Column(Integer, default=0) # 当前分钟已用请求数
rpm_reset_at = Column(DateTime(timezone=True), nullable=True) # RPM重置时间
# 提供商优先级 (数字越小越优先,用于提供商优先模式下的 Provider 排序)
# 0-10: 急需消耗(如即将过期的月卡)
# 11-50: 优先消耗(月卡)
# 51-100: 正常消费(按量付费)
# 101+: 备用(高成本或限制严格的)
provider_priority = Column(Integer, default=100)
# 状态
is_active = Column(Boolean, default=True, nullable=False)
# 限制
rate_limit = Column(Integer, nullable=True) # 每分钟请求限制
concurrent_limit = Column(Integer, nullable=True) # 并发请求限制
# 配置
config = Column(JSON, nullable=True) # 额外配置如Azure deployment name等
# 时间戳
created_at = Column(
DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), nullable=False
)
updated_at = Column(
DateTime(timezone=True),
default=lambda: datetime.now(timezone.utc),
onupdate=lambda: datetime.now(timezone.utc),
nullable=False,
)
# 关系
models = relationship("Model", back_populates="provider", cascade="all, delete-orphan")
endpoints = relationship(
"ProviderEndpoint", back_populates="provider", cascade="all, delete-orphan"
)
api_key_mappings = relationship(
"ApiKeyProviderMapping", back_populates="provider", cascade="all, delete-orphan"
)
usage_tracking = relationship(
"ProviderUsageTracking", back_populates="provider", cascade="all, delete-orphan"
)
class ProviderEndpoint(Base):
"""提供商端点 - 一个提供商可以有多个 API 格式端点"""
__tablename__ = "provider_endpoints"
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()), index=True)
provider_id = Column(String(36), ForeignKey("providers.id", ondelete="CASCADE"), nullable=False)
# API 格式和配置
api_format = Column(String(50), nullable=False) # 存储 APIFormat 枚举值的字符串
base_url = Column(String(500), nullable=False)
# 请求配置
headers = Column(JSON, nullable=True) # 额外请求头
timeout = Column(Integer, default=300) # 超时(秒)
max_retries = Column(Integer, default=3) # 最大重试次数
# 限制
max_concurrent = Column(
Integer, nullable=True, default=None
) # 该端点的最大并发数NULL=不限制)
rate_limit = Column(Integer, nullable=True) # 每分钟请求限制
# 状态
is_active = Column(Boolean, default=True, nullable=False)
# 路径配置
custom_path = Column(
String(200), nullable=True
) # 自定义请求路径,为空则使用 API 格式的默认路径
# 额外配置
config = Column(JSON, nullable=True) # 端点特定配置(不推荐使用,优先使用专用字段)
# 代理配置
proxy = Column(JSONB, nullable=True) # 代理配置: {url, username, password}
# 时间戳
created_at = Column(
DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), nullable=False
)
updated_at = Column(
DateTime(timezone=True),
default=lambda: datetime.now(timezone.utc),
onupdate=lambda: datetime.now(timezone.utc),
nullable=False,
)
# 关系
provider = relationship("Provider", back_populates="endpoints")
api_keys = relationship(
"ProviderAPIKey", back_populates="endpoint", cascade="all, delete-orphan"
)
# 唯一约束和索引在表定义后
__table_args__ = (
UniqueConstraint("provider_id", "api_format", name="uq_provider_api_format"),
Index("idx_endpoint_format_active", "api_format", "is_active"),
)
class GlobalModel(Base):
"""全局统一模型定义 - 包含价格和能力配置
设计原则:
- 定义模型的基本信息和价格配置(价格为必填项)
- Provider 级别的 Model 可以覆盖这些默认值
- 如果 Model 的价格/能力字段为空,则使用 GlobalModel 的值
"""
__tablename__ = "global_models"
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()), index=True)
name = Column(String(100), unique=True, nullable=False, index=True) # 统一模型名(唯一)
display_name = Column(String(100), nullable=False)
# 按次计费配置(每次请求的固定费用,美元)- 可选,与按 token 计费叠加
default_price_per_request = Column(Float, nullable=True, default=None) # 每次请求固定费用
# 统一阶梯计费配置JSON格式- 必填
# 固定价格也用单阶梯表示: {"tiers": [{"up_to": null, "input_price_per_1m": X, ...}]}
# 结构示例:
# {
# "tiers": [
# {
# "up_to": 128000, # 阶梯上限tokensnull 表示无上限
# "input_price_per_1m": 2.50,
# "output_price_per_1m": 10.00,
# "cache_creation_price_per_1m": 3.75, # 可选
# "cache_read_price_per_1m": 0.30, # 可选
# "cache_ttl_pricing": [ # 可选:按缓存时长分价格
# {"ttl_minutes": 5, "cache_read_price_per_1m": 0.30},
# {"ttl_minutes": 60, "cache_read_price_per_1m": 0.50}
# ]
# },
# {"up_to": null, "input_price_per_1m": 1.25, ...}
# ]
# }
default_tiered_pricing = Column(JSON, nullable=False)
# Key 能力配置 - 模型支持的能力列表(如 ["cache_1h", "context_1m"]
# Key 只能启用模型支持的能力
supported_capabilities = Column(JSON, nullable=True, default=list)
# 模型配置JSON格式- 包含能力、规格、元信息等
# 结构示例:
# {
# # 能力配置
# "streaming": true,
# "vision": true,
# "function_calling": true,
# "extended_thinking": false,
# "image_generation": false,
# # 规格参数
# "context_limit": 200000,
# "output_limit": 8192,
# # 元信息
# "description": "...",
# "icon_url": "...",
# "official_url": "...",
# "knowledge_cutoff": "2024-04",
# "family": "claude-3.5",
# "release_date": "2024-10-22",
# "input_modalities": ["text", "image"],
# "output_modalities": ["text"],
# }
config = Column(JSONB, nullable=True, default=dict)
# 状态
is_active = Column(Boolean, default=True, nullable=False)
# 统计计数器(优化性能,避免实时查询)
usage_count = Column(Integer, default=0, nullable=False, index=True)
# 时间戳
created_at = Column(
DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), nullable=False
)
updated_at = Column(
DateTime(timezone=True),
default=lambda: datetime.now(timezone.utc),
onupdate=lambda: datetime.now(timezone.utc),
nullable=False,
)
# 关系
models = relationship("Model", back_populates="global_model")
class Model(Base):
"""Provider 模型配置表 - Provider 如何使用某个 GlobalModel
设计原则 (方案 A):
- 每个 Model 必须关联一个 GlobalModel (global_model_id 不可为空)
- Model 表示 Provider 对某个 GlobalModel 的具体实现
- provider_model_name 是 Provider 侧的实际模型名称 (可能与 GlobalModel.name 不同)
- 价格和能力配置可为空,为空时使用 GlobalModel 的默认值
"""
__tablename__ = "models"
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()), index=True)
provider_id = Column(String(36), ForeignKey("providers.id"), nullable=False)
global_model_id = Column(String(36), ForeignKey("global_models.id"), nullable=False, index=True)
# Provider 映射配置
provider_model_name = Column(String(200), nullable=False) # Provider 侧的主模型名称
# 模型名称别名列表(带优先级),用于同一模型在 Provider 侧有多个名称变体的场景
# 格式: [{"name": "Claude-Sonnet-4.5", "priority": 1}, {"name": "Claude-Sonnet-4-5", "priority": 2}]
# 为空时只使用 provider_model_name
provider_model_aliases = Column(JSON, nullable=True, default=None)
# 按次计费配置(每次请求的固定费用,美元)- 可为空,为空时使用 GlobalModel 的默认值
price_per_request = Column(Float, nullable=True) # 每次请求固定费用
# 阶梯计费配置JSON格式- 可为空,为空时使用 GlobalModel 的默认值
tiered_pricing = Column(JSON, nullable=True, default=None)
# Provider 能力配置 - 可为空,为空时使用 GlobalModel 的默认值
supports_vision = Column(Boolean, nullable=True)
supports_function_calling = Column(Boolean, nullable=True)
supports_streaming = Column(Boolean, nullable=True)
supports_extended_thinking = Column(Boolean, nullable=True)
supports_image_generation = Column(Boolean, nullable=True)
# 状态
is_active = Column(Boolean, default=True, nullable=False)
is_available = Column(Boolean, default=True) # 是否当前可用
# 扩展配置
config = Column(JSON, nullable=True)
# 时间戳
created_at = Column(
DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), nullable=False
)
updated_at = Column(
DateTime(timezone=True),
default=lambda: datetime.now(timezone.utc),
onupdate=lambda: datetime.now(timezone.utc),
nullable=False,
)
# 关系
provider = relationship("Provider", back_populates="models")
global_model = relationship("GlobalModel", back_populates="models")
# 唯一约束:同一个提供商下的 provider_model_name 不能重复
__table_args__ = (
UniqueConstraint("provider_id", "provider_model_name", name="uq_provider_model"),
)
# 辅助方法:获取有效的阶梯计费配置
def get_effective_tiered_pricing(self) -> dict | None:
"""获取有效的阶梯计费配置"""
if self.tiered_pricing is not None:
return self.tiered_pricing
if self.global_model:
return self.global_model.default_tiered_pricing
return None
def _get_first_tier(self) -> dict | None:
"""获取第一个阶梯(用于获取默认价格)"""
tiered = self.get_effective_tiered_pricing()
if tiered and tiered.get("tiers"):
return tiered["tiers"][0]
return None
def get_effective_input_price(self) -> float:
"""获取有效的输入价格(从第一个阶梯)"""
tier = self._get_first_tier()
if tier:
return tier.get("input_price_per_1m", 0.0)
return 0.0
def get_effective_output_price(self) -> float:
"""获取有效的输出价格(从第一个阶梯)"""
tier = self._get_first_tier()
if tier:
return tier.get("output_price_per_1m", 0.0)
return 0.0
def get_effective_cache_creation_price(self) -> float | None:
"""获取有效的缓存创建价格(从第一个阶梯)"""
tier = self._get_first_tier()
if tier:
return tier.get("cache_creation_price_per_1m")
return None
def get_effective_cache_read_price(self) -> float | None:
"""获取有效的缓存读取价格(从第一个阶梯)"""
tier = self._get_first_tier()
if tier:
return tier.get("cache_read_price_per_1m")
return None
def get_effective_1h_cache_creation_price(self) -> float | None:
"""获取有效的 1h 缓存创建价格(从第一个阶梯)"""
tier = self._get_first_tier()
if tier:
cache_ttl_pricing = tier.get("cache_ttl_pricing") or []
for ttl_entry in cache_ttl_pricing:
if ttl_entry.get("ttl_minutes") == 60:
return ttl_entry.get("cache_creation_price_per_1m")
return None
def get_effective_price_per_request(self) -> float | None:
"""获取有效的按次计费价格"""
if self.price_per_request is not None:
return self.price_per_request
if self.global_model:
return self.global_model.default_price_per_request
return None
def _get_effective_capability(self, attr_name: str, default: bool = False) -> bool:
"""获取有效的能力配置(通用辅助方法)"""
local_value = getattr(self, attr_name, None)
if local_value is not None:
return bool(local_value)
if self.global_model:
config_key_map = {
"supports_vision": "vision",
"supports_function_calling": "function_calling",
"supports_streaming": "streaming",
"supports_extended_thinking": "extended_thinking",
"supports_image_generation": "image_generation",
}
config_key = config_key_map.get(attr_name)
if config_key:
global_config = getattr(self.global_model, "config", None)
if isinstance(global_config, dict):
global_value = global_config.get(config_key)
if global_value is not None:
return bool(global_value)
return default
def get_effective_supports_vision(self) -> bool:
return self._get_effective_capability("supports_vision", False)
def get_effective_supports_function_calling(self) -> bool:
return self._get_effective_capability("supports_function_calling", False)
def get_effective_supports_streaming(self) -> bool:
return self._get_effective_capability("supports_streaming", True)
def get_effective_supports_extended_thinking(self) -> bool:
return self._get_effective_capability("supports_extended_thinking", False)
def get_effective_supports_image_generation(self) -> bool:
return self._get_effective_capability("supports_image_generation", False)
def select_provider_model_name(
self, affinity_key: Optional[str] = None, api_format: Optional[str] = None
) -> str:
"""按优先级选择要使用的 Provider 模型名称
如果配置了 provider_model_aliases按优先级选择数字越小越优先
相同优先级的别名通过哈希分散实现负载均衡(与 Key 调度策略一致);
否则返回 provider_model_name。
Args:
affinity_key: 用于哈希分散的亲和键(如用户 API Key 哈希),确保同一用户稳定选择同一别名
api_format: 当前请求的 API 格式(如 CLAUDE、OPENAI 等),用于过滤适用的别名
"""
import hashlib
if not self.provider_model_aliases:
return self.provider_model_name
raw_aliases = self.provider_model_aliases
if not isinstance(raw_aliases, list) or len(raw_aliases) == 0:
return self.provider_model_name
aliases: list[dict] = []
for raw in raw_aliases:
if not isinstance(raw, dict):
continue
name = raw.get("name")
if not isinstance(name, str) or not name.strip():
continue
# 检查 api_formats 作用域(如果配置了且当前有 api_format
alias_api_formats = raw.get("api_formats")
if api_format and alias_api_formats:
# 如果配置了作用域,只有匹配时才生效
if isinstance(alias_api_formats, list) and api_format not in alias_api_formats:
continue
raw_priority = raw.get("priority", 1)
try:
priority = int(raw_priority)
except Exception:
priority = 1
if priority < 1:
priority = 1
aliases.append({"name": name.strip(), "priority": priority})
if not aliases:
return self.provider_model_name
# 按优先级排序(数字越小越优先)
sorted_aliases = sorted(aliases, key=lambda x: x["priority"])
# 获取最高优先级(最小数字)
highest_priority = sorted_aliases[0]["priority"]
# 获取所有最高优先级的别名
top_priority_aliases = [
alias for alias in sorted_aliases
if alias["priority"] == highest_priority
]
# 如果有多个相同优先级的别名,通过哈希分散选择
if len(top_priority_aliases) > 1 and affinity_key:
# 为每个别名计算哈希得分,选择得分最小的
def hash_score(alias: dict) -> int:
combined = f"{affinity_key}:{alias['name']}"
return int(hashlib.md5(combined.encode()).hexdigest(), 16)
selected = min(top_priority_aliases, key=hash_score)
elif len(top_priority_aliases) > 1:
# 没有 affinity_key 时,使用确定性选择(按名称排序后取第一个)
# 避免随机选择导致同一请求重试时选择不同的模型名称
selected = min(top_priority_aliases, key=lambda x: x["name"])
else:
selected = top_priority_aliases[0]
return selected["name"]
def get_all_provider_model_names(self) -> list[str]:
"""获取所有可用的 Provider 模型名称(主名称 + 别名)"""
names = [self.provider_model_name]
if self.provider_model_aliases:
for alias in self.provider_model_aliases:
if isinstance(alias, dict) and alias.get("name"):
names.append(alias["name"])
return names
class ProviderAPIKey(Base):
"""Provider API密钥表 - 归属于特定 ProviderEndpoint"""
__tablename__ = "provider_api_keys"
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()), index=True)
# 外键关系
endpoint_id = Column(
String(36), ForeignKey("provider_endpoints.id", ondelete="CASCADE"), nullable=False
)
# API密钥信息
api_key = Column(String(500), nullable=False) # API密钥加密存储
name = Column(String(100), nullable=False) # 密钥名称(必填,用于识别)
note = Column(String(500), nullable=True) # 备注说明(可选)
# 成本计算
rate_multiplier = Column(
Float, default=1.0, nullable=False
) # 成本倍率(真实成本 = 表面成本 × 倍率)
# 优先级配置 (数字越小越优先)
internal_priority = Column(
Integer, default=50
) # Endpoint 内部优先级(用于提供商优先模式,同 Endpoint 内 Keys 的排序,同优先级参与负载均衡)
global_priority = Column(
Integer, nullable=True
) # 全局 Key 优先级(用于全局 Key 优先模式,跨 Provider 的 Key 排序NULL=未配置使用默认排序)
# 并发限制配置
# max_concurrent 决定并发控制模式:
# - NULL: 自适应模式,系统自动学习并调整(使用 learned_max_concurrent
# - 数字: 固定限制模式,使用用户指定的值
max_concurrent = Column(Integer, nullable=True, default=None)
rate_limit = Column(Integer, nullable=True) # 速率限制(每分钟请求数)
daily_limit = Column(Integer, nullable=True) # 每日请求限制
monthly_limit = Column(Integer, nullable=True) # 每月请求限制
# 模型权限控制
allowed_models = Column(JSON, nullable=True) # 允许使用的模型列表null = 支持所有模型)
# Key 能力标签
capabilities = Column(JSON, nullable=True) # Key 拥有的能力
# 示例: {"cache_1h": true, "context_1m": true}
# 自适应并发调整(仅当 max_concurrent = NULL 时生效)
learned_max_concurrent = Column(
Integer, nullable=True
) # 学习到的并发限制(自适应模式下的有效值)
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_concurrent_peak = Column(Integer, nullable=True) # 触发429时的并发数
adjustment_history = Column(JSON, nullable=True) # 并发调整历史
# 基于滑动窗口的利用率追踪
utilization_samples = Column(
JSON, nullable=True
) # 利用率采样窗口 [{"ts": timestamp, "util": 0.8}, ...]
last_probe_increase_at = Column(
DateTime(timezone=True), nullable=True
) # 上次探测性扩容时间
# 健康度追踪(基于滑动窗口)
health_score = Column(Float, default=1.0) # 0.0-1.0(保留用于展示,实际熔断基于滑动窗口)
consecutive_failures = Column(Integer, default=0)
last_failure_at = Column(DateTime(timezone=True), nullable=True) # 最后失败时间
# 滑动窗口:记录最近 N 次请求的结果 [{"ts": timestamp, "ok": true/false}, ...]
request_results_window = Column(JSON, nullable=True)
# 缓存与熔断配置
cache_ttl_minutes = Column(
Integer, default=5, nullable=False
) # 缓存TTL(分钟)0表示不支持缓存默认5分钟
max_probe_interval_minutes = Column(
Integer, default=32, nullable=False
) # 最大探测间隔(分钟)默认32分钟硬上限
# 熔断器字段(滑动窗口 + 半开状态模式)
circuit_breaker_open = Column(Boolean, default=False, nullable=False) # 熔断器是否打开
circuit_breaker_open_at = Column(DateTime(timezone=True), nullable=True) # 熔断器打开时间
next_probe_at = Column(DateTime(timezone=True), nullable=True) # 下次探测时间
# 半开状态:允许少量请求通过验证服务是否恢复
half_open_until = Column(DateTime(timezone=True), nullable=True) # 半开状态结束时间
half_open_successes = Column(Integer, default=0) # 半开状态下的成功次数
half_open_failures = Column(Integer, default=0) # 半开状态下的失败次数
# 使用统计
request_count = Column(Integer, default=0) # 请求次数
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) # 过期时间
# 时间戳
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,
)
# 关系
endpoint = relationship("ProviderEndpoint", back_populates="api_keys")
class UserPreference(Base):
"""用户偏好设置表"""
__tablename__ = "user_preferences"
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()), index=True)
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)
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()), index=True)
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)
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()), index=True)
user_id = Column(String(36), ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
announcement_id = Column(String(36), ForeignKey("announcements.id"), nullable=False)
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"
class AuditLog(Base):
"""审计日志模型"""
__tablename__ = "audit_logs"
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()), index=True)
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="CASCADE"), nullable=True)
api_key_id = Column(String(36), ForeignKey("api_keys.id", ondelete="CASCADE"), nullable=True)
# 候选信息
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
)
key_id = Column(
String(36), ForeignKey("provider_api_keys.id", ondelete="CASCADE"), nullable=True
)
# 状态信息
status = Column(String(20), nullable=False) # 'pending', 'success', 'failed', 'skipped'
skip_reason = Column(Text, nullable=True) # 跳过/失败原因
is_cached = Column(Boolean, default=False) # 是否为缓存亲和性候选
# 执行结果信息(当 status = success/failed 时)
status_code = Column(Integer, nullable=True) # HTTP 状态码
error_type = Column(String(50), nullable=True) # 错误类型
error_message = Column(Text, nullable=True) # 错误消息
latency_ms = Column(Integer, nullable=True) # 延迟(毫秒)
concurrent_requests = Column(Integer, nullable=True) # 并发请求数
# 元数据
extra_data = Column(JSON, nullable=True)
required_capabilities = Column(JSON, nullable=True) # 请求实际需要的能力标签
# 时间戳
created_at = Column(
DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), nullable=False
)
started_at = Column(DateTime(timezone=True), nullable=True) # 开始执行时间
finished_at = Column(DateTime(timezone=True), nullable=True) # 完成时间
# 唯一约束和索引
__table_args__ = (
UniqueConstraint(
"request_id", "candidate_index", "retry_index", name="uq_request_candidate_with_retry"
),
Index("idx_request_candidates_request_id", "request_id"),
Index("idx_request_candidates_status", "status"),
Index("idx_request_candidates_provider_id", "provider_id"),
)
# 关系
user = relationship("User")
api_key = relationship("ApiKey")
provider = relationship("Provider")
endpoint = relationship("ProviderEndpoint")
key = relationship("ProviderAPIKey")
# ==================== 统计数据模型 ====================
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(Float, default=0.0, nullable=False)
actual_total_cost = Column(Float, default=0.0, nullable=False) # 倍率后成本
input_cost = Column(Float, default=0.0, nullable=False)
output_cost = Column(Float, default=0.0, nullable=False)
cache_creation_cost = Column(Float, default=0.0, nullable=False)
cache_read_cost = Column(Float, default=0.0, nullable=False)
# 性能统计
avg_response_time_ms = Column(Float, default=0.0, nullable=False)
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)
# 时间戳
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 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(Float, default=0.0, nullable=False)
all_time_actual_cost = Column(Float, default=0.0, nullable=False)
# 累计用户/API Key 统计 (快照)
total_users = Column(Integer, default=0, nullable=False)
active_users = Column(Integer, default=0, nullable=False)
total_api_keys = Column(Integer, default=0, nullable=False)
active_api_keys = Column(Integer, default=0, nullable=False)
# 时间戳
created_at = Column(
DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), nullable=False
)
updated_at = Column(
DateTime(timezone=True),
default=lambda: datetime.now(timezone.utc),
onupdate=lambda: datetime.now(timezone.utc),
nullable=False,
)
class StatsUserDaily(Base):
"""用户每日统计快照 - 用于用户仪表盘"""
__tablename__ = "stats_user_daily"
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
# 用户关联
user_id = Column(String(36), ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
# 统计日期 (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(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("user_id", "date", name="uq_stats_user_daily"),
Index("idx_stats_user_daily_user_date", "user_id", "date"),
)
# 关系
user = relationship("User")
# 导入扩展的数据库模型
from .database_extensions import ApiKeyProviderMapping, ProviderUsageTracking