refactor: 移除 Python 后端源码,全面迁移至 Rust gateway 架构

- 删除全部 Python 源码 (src/) 及 Alembic 迁移脚本,归档至 _deprecated_py_src/
- 重构 Rust gateway ai_pipeline: 拆分 planner/finalize 模块,新增 contracts/adaptation 层
- 重组 handlers 模块为 admin/public/proxy/internal/shared 子模块结构
- 新增 executor 模块,引入 Rust 原生数据库迁移 (aether-data/migrations)
- 简化 CI/Docker 构建流程,移除 base image 二级构建,统一为单一 app image
- 移除 Python 相关基础设施文件 (entrypoint.sh, gunicorn_conf.py, Dockerfile.base)
This commit is contained in:
fawney19
2026-04-03 16:26:16 +08:00
parent 8f26e1a31f
commit 1d9c77522a
868 changed files with 1735 additions and 2433 deletions

View File

@@ -0,0 +1,3 @@
from .settings import Config, config
__all__ = ["Config", "config"]

View File

@@ -0,0 +1,326 @@
# Constants for better maintainability
# ==============================================================================
# 缓存相关常量
# ==============================================================================
# 缓存 TTL
class CacheTTL:
"""缓存过期时间配置(秒)"""
# 用户缓存 - 用户信息变更较频繁
USER = 60 # 1分钟
# Provider/Model 缓存 - 配置变更不频繁
PROVIDER = 300 # 5分钟
MODEL = 300 # 5分钟
# 缓存亲和性 - 对应 provider_api_key.cache_ttl_minutes 默认值
CACHE_AFFINITY = 300 # 5分钟
# L1 本地缓存(用于减少 Redis 访问)
L1_LOCAL = 3 # 3秒
# 活跃度热力图缓存 - 历史数据变化不频繁,查询成本高
ACTIVITY_HEATMAP = 600 # 10分钟
# 仪表盘统计缓存
DASHBOARD_STATS = 120 # 2分钟管理员
DASHBOARD_DAILY = 600 # 10分钟每日统计
# Admin usage pages (heavy DB aggregations / list queries)
ADMIN_USAGE_AGGREGATION = 60 # 60秒聚合统计变化不频繁适当延长减少 DB 压力)
ADMIN_USAGE_RECORDS = 15 # 15秒列表页短缓存活跃请求通过轮询接口实时更新
# Admin leaderboard (heavier, slower moving)
ADMIN_LEADERBOARD = 300 # 5分钟
# 并发锁 TTL - 防止死锁
CONCURRENCY_LOCK = 600 # 10分钟
# 缓存容量限制
class CacheSize:
"""缓存容量配置"""
# 默认 LRU 缓存大小
DEFAULT = 1000
# ==============================================================================
# 并发和限流常量
# ==============================================================================
class StreamDefaults:
"""流式处理默认值"""
# 预读字节上限(避免无换行响应导致内存增长)
# 64KB 基于:
# 1. SSE 单条消息通常远小于此值
# 2. 足够检测 HTML 和 JSON 错误响应
# 3. 不会占用过多内存
MAX_PREFETCH_BYTES = 64 * 1024 # 64KB
# 单行流式缓冲上限(避免上游长期不换行导致内存无限增长)
# 正常 SSE 行通常远小于该值,此值主要作为 OOM 安全护栏。
MAX_STREAM_BUFFER_BYTES = 16 * 1024 * 1024 # 16MB
# 流式总缓冲区硬上限(兜底防护)
# 保留单行检查逻辑不变,仅用于防止大量完整行短时间累积占用过高内存。
MAX_STREAM_BUFFER_TOTAL_BYTES = 32 * 1024 * 1024 # 32MB
# 流式转换空产出告警阈值
# 连续这么多次空行/非 data 行后记录警告日志
# 50 次约等于 50 行非 data SSE 数据,足够覆盖正常事件头
MAX_EMPTY_YIELDS_WARNING = 50
class RPMDefaults:
"""RPM每分钟请求数限制默认值
算法说明:边界记忆 + 渐进探测
- 触发 429 时记录边界last_rpm_peak新限制 = 边界 - 1
- 扩容时不超过边界,除非是探测性扩容(长时间无 429
- 这样可以快速收敛到真实限制附近,避免过度保守
初始值 50 RPM
- 系统会根据实际使用自动调整
"""
# 自适应 RPM 初始限制
INITIAL_LIMIT = 50 # 每分钟 50 次请求
# === 内存模式 RPM 计数器配置 ===
# 内存模式下的最大条目限制(防止内存泄漏)
# 每个条目约占 100 字节10000 条目 = ~1MB
# 计算依据1000 Key × 5 API 格式 × 2 (buffer) = 10000
# 可通过环境变量 RPM_MAX_MEMORY_ENTRIES 覆盖
MAX_MEMORY_RPM_ENTRIES = 10000
# 内存使用告警阈值(达到此比例时记录警告日志)
# 可通过环境变量 RPM_MEMORY_WARNING_THRESHOLD 覆盖
MEMORY_WARNING_THRESHOLD = 0.6 # 60%
# 429错误后的冷却时间分钟- 在此期间不会增加 RPM 限制
COOLDOWN_AFTER_429_MINUTES = 5
# 探测间隔上限(分钟)- 用于长期探测策略
MAX_PROBE_INTERVAL_MINUTES = 60
# === 基于滑动窗口的扩容参数 ===
# 滑动窗口大小(采样点数量)
UTILIZATION_WINDOW_SIZE = 20
# 滑动窗口时间范围(秒)- 只保留最近这段时间内的采样
UTILIZATION_WINDOW_SECONDS = 120 # 2分钟
# 利用率阈值 - 窗口内平均利用率 >= 此值时考虑扩容
UTILIZATION_THRESHOLD = 0.7 # 70%
# 高利用率采样比例 - 窗口内超过阈值的采样点比例 >= 此值时触发扩容
HIGH_UTILIZATION_RATIO = 0.6 # 60% 的采样点高于阈值
# 最小采样数 - 窗口内至少需要这么多采样才能做出扩容决策
MIN_SAMPLES_FOR_DECISION = 5
# 扩容步长 - 每次扩容增加的 RPM
INCREASE_STEP = 5 # 每次增加 5 RPM
# 最大 RPM 限制上限(不设上限,让系统自适应学习)
MAX_RPM_LIMIT = 10000
# 最小 RPM 限制下限
MIN_RPM_LIMIT = 5
# 缓存用户预留比例(默认 10%,新用户可用 90%
# 已被动态预留机制 (AdaptiveReservationDefaults) 替代,保留用于向后兼容
CACHE_RESERVATION_RATIO = 0.1
# === 探测性扩容参数 ===
# 探测性扩容间隔(分钟)- 长时间无 429 且有流量时尝试扩容
# 探测性扩容可以突破已知边界,尝试更高的 RPM
PROBE_INCREASE_INTERVAL_MINUTES = 30
# 探测性扩容最小请求数 - 在探测间隔内至少需要这么多请求
PROBE_INCREASE_MIN_REQUESTS = 10
# === 置信度学习参数 ===
# 无 header 时,需要多少次一致的 429 观察才确认限制
MIN_CONSISTENT_OBSERVATIONS = 3
# 有 header 时,需要多少次一致的 header 观察才确认限制
MIN_HEADER_CONFIRMATIONS = 2
# 观察值之间的最大允许偏差比例30% 以内视为一致)
OBSERVATION_CONSISTENCY_THRESHOLD = 0.3
# header 声明限制的安全边际(使用 95%
HEADER_LIMIT_SAFETY_MARGIN = 0.95
# 纯观察限制的安全边际(使用 90%
OBSERVATION_LIMIT_SAFETY_MARGIN = 0.90
# confidence 低于此阈值时不执行本地 RPM 限制(透传上游 429
ENFORCEMENT_CONFIDENCE_THRESHOLD = 0.6
# confidence 自然衰减速率:每分钟衰减的比例
CONFIDENCE_DECAY_PER_MINUTE = 0.005 # 每分钟 -0.5%,约 200 分钟(~3.3h)从 1.0 衰减到 0
# === RPM 计数器时间窗口配置 ===
# RPM 计数时间窗口(秒)
RPM_BUCKET_SECONDS = 60
# Redis key 过期时间(秒),需覆盖当前分钟与边界
RPM_KEY_TTL_SECONDS = 120
# 内存模式清理间隔(秒)
RPM_CLEANUP_INTERVAL_SECONDS = 300
# 向后兼容别名
ConcurrencyDefaults = RPMDefaults
class CircuitBreakerDefaults:
"""熔断器配置默认值(滑动窗口 + 半开状态模式)
新的熔断器基于滑动窗口错误率,而不是累计健康度。
支持半开状态,允许少量请求验证服务是否恢复。
"""
# === 滑动窗口配置 ===
# 滑动窗口大小(最近 N 次请求)
WINDOW_SIZE = 20
# 滑动窗口时间范围(秒)- 只保留最近这段时间内的请求记录
WINDOW_SECONDS = 300 # 5分钟
# 最小请求数 - 窗口内至少需要这么多请求才能做出熔断决策
MIN_REQUESTS_FOR_DECISION = 5
# 错误率阈值 - 窗口内错误率超过此值时触发熔断
ERROR_RATE_THRESHOLD = 0.5 # 50%
# === 半开状态配置 ===
# 半开状态持续时间(秒)- 在此期间允许少量请求通过
HALF_OPEN_DURATION_SECONDS = 30
# 半开状态成功阈值 - 达到此成功次数则关闭熔断器
HALF_OPEN_SUCCESS_THRESHOLD = 3
# 半开状态失败阈值 - 达到此失败次数则重新打开熔断器
HALF_OPEN_FAILURE_THRESHOLD = 2
# === 熔断恢复配置 ===
# 初始探测间隔(秒)- 熔断后多久进入半开状态
INITIAL_RECOVERY_SECONDS = 30
# 探测间隔退避倍数
RECOVERY_BACKOFF_MULTIPLIER = 2
# 最大探测间隔(秒)
MAX_RECOVERY_SECONDS = 300 # 5分钟
# === 旧参数(向后兼容,仍用于展示健康度)===
# 成功时健康度增量
SUCCESS_INCREMENT = 0.15
# 失败时健康度减量
FAILURE_DECREMENT = 0.03
# 探测成功后的快速恢复健康度
PROBE_RECOVERY_SCORE = 0.5
class AdaptiveReservationDefaults:
"""动态预留比例配置默认值
动态预留机制根据学习置信度和负载自动调整缓存用户预留比例,
解决固定 30% 预留在学习初期和负载变化时的不适应问题。
"""
# 探测阶段配置
PROBE_PHASE_REQUESTS = 100 # 探测阶段请求数阈值
PROBE_RESERVATION = 0.1 # 探测阶段预留比例10%
# 稳定阶段配置
STABLE_MIN_RESERVATION = 0.1 # 稳定阶段最小预留10%
STABLE_MAX_RESERVATION = 0.35 # 稳定阶段最大预留35%
# 置信度计算参数
SUCCESS_COUNT_FOR_FULL_CONFIDENCE = 50 # 连续成功多少次达到满置信
COOLDOWN_HOURS_FOR_FULL_CONFIDENCE = 24 # 429后多少小时达到满置信
# 负载阈值
LOW_LOAD_THRESHOLD = 0.5 # 低负载阈值50%
HIGH_LOAD_THRESHOLD = 0.8 # 高负载阈值80%
# ==============================================================================
# 超时和重试常量
# ==============================================================================
class TimeoutDefaults:
"""超时配置默认值(秒)
超时配置说明:
- 非流式请求超时由环境变量 HTTP_REQUEST_TIMEOUT 控制(默认 300 秒)
- 流式请求首字节超时由环境变量 STREAM_FIRST_BYTE_TIMEOUT 控制(默认 30 秒)
- 此处的常量仅用于无法访问 config 的场景(如模型查询测试)
"""
# HTTP 请求默认超时(用于模型查询等测试场景)
# 与 config.http_request_timeout 默认值保持一致
HTTP_REQUEST = 300 # 5分钟
# 数据库连接池获取超时
DB_POOL = 30
# Redis 操作超时
REDIS_OPERATION = 5
class RetryDefaults:
"""重试配置默认值"""
# 最大重试次数
MAX_RETRIES = 3
# 重试基础延迟(秒)
BASE_DELAY = 1.0
# 重试延迟倍数(指数退避)
DELAY_MULTIPLIER = 2.0
# ==============================================================================
# 消息格式常量
# ==============================================================================
# 角色常量
ROLE_USER = "user"
ROLE_ASSISTANT = "assistant"
ROLE_SYSTEM = "system"
ROLE_TOOL = "tool"
# 内容类型常量
CONTENT_TEXT = "text"
CONTENT_IMAGE = "image"
CONTENT_TOOL_USE = "tool_use"
CONTENT_TOOL_RESULT = "tool_result"
# 工具常量
TOOL_FUNCTION = "function"
# 停止原因常量
STOP_END_TURN = "end_turn"
STOP_MAX_TOKENS = "max_tokens"
STOP_TOOL_USE = "tool_use"
STOP_ERROR = "error"
# 事件类型常量
EVENT_MESSAGE_START = "message_start"
EVENT_MESSAGE_STOP = "message_stop"
EVENT_MESSAGE_DELTA = "message_delta"
EVENT_CONTENT_BLOCK_START = "content_block_start"
EVENT_CONTENT_BLOCK_STOP = "content_block_stop"
EVENT_CONTENT_BLOCK_DELTA = "content_block_delta"
EVENT_PING = "ping"
# Delta类型常量
DELTA_TEXT = "text_delta"
DELTA_INPUT_JSON = "input_json_delta"

View File

@@ -0,0 +1,754 @@
"""
服务器配置
从环境变量或 .env 文件加载配置
"""
import os
from pathlib import Path
from typing import Any
# 尝试加载 .env 文件
try:
from dotenv import load_dotenv
env_file = Path(".env")
if env_file.exists():
load_dotenv(env_file)
except ImportError:
# 如果没有安装 python-dotenv仍然可以从环境变量读取
pass
class Config:
_VALID_COOKIE_SAMESITE = {"lax", "strict", "none"}
def __init__(self) -> None:
# 服务器配置
self.host = os.getenv("HOST", "0.0.0.0")
self.port = int(os.getenv("PORT", "8084"))
self.log_level = os.getenv("LOG_LEVEL", "INFO")
self.worker_processes = int(
os.getenv("WEB_CONCURRENCY", os.getenv("GUNICORN_WORKERS", "1"))
)
# PostgreSQL 连接池计算相关配置
# PG_MAX_CONNECTIONS: PostgreSQL 的 max_connections 设置(默认 100
# PG_RESERVED_CONNECTIONS: 为其他应用/管理工具预留的连接数(默认 10
self.pg_max_connections = int(os.getenv("PG_MAX_CONNECTIONS", "100"))
self.pg_reserved_connections = int(os.getenv("PG_RESERVED_CONNECTIONS", "10"))
# 数据库配置 - 延迟验证,支持测试环境覆盖
self._database_url = os.getenv("DATABASE_URL")
# JWT配置
self.jwt_secret_key = os.getenv("JWT_SECRET_KEY", None)
self.jwt_algorithm = os.getenv("JWT_ALGORITHM", "HS256")
self.jwt_expiration_hours = int(os.getenv("JWT_EXPIRATION_HOURS", "24"))
# 加密密钥配置独立于JWT密钥用于敏感数据加密
self.encryption_key = os.getenv("ENCRYPTION_KEY", None)
# 环境配置 - 智能检测
# Docker 部署默认为生产环境,本地开发默认为开发环境
is_docker = (
os.path.exists("/.dockerenv")
or os.environ.get("DOCKER_CONTAINER", "false").lower() == "true"
)
default_env = "production" if is_docker else "development"
self.environment = os.getenv("ENVIRONMENT", default_env)
# Redis 依赖策略(生产默认必需,开发默认可选,可通过 REDIS_REQUIRED 覆盖)
redis_required_env = os.getenv("REDIS_REQUIRED")
if redis_required_env is not None:
self.require_redis = redis_required_env.lower() == "true"
else:
# 保持向后兼容:开发环境可选,生产环境必需
self.require_redis = self.environment not in {"development", "test", "testing"}
# CORS配置 - 使用环境变量配置允许的源
# 格式: 逗号分隔的域名列表,如 "http://localhost:3000,https://example.com"
cors_origins = os.getenv("CORS_ORIGINS", "")
if cors_origins:
self.cors_origins = [
origin.strip() for origin in cors_origins.split(",") if origin.strip()
]
else:
# 默认: 开发环境允许本地前端,生产环境不允许任何跨域
if self.environment == "development":
self.cors_origins = [
"http://localhost:3000",
"http://localhost:5173", # Vite 默认端口
"http://127.0.0.1:3000",
"http://127.0.0.1:5173",
]
else:
# 生产环境默认不允许跨域,必须显式配置
self.cors_origins = []
# CORS是否允许凭证(Cookie/Authorization header)
# 注意: allow_credentials=True 时不能使用 allow_origins=["*"]
self.cors_allow_credentials = os.getenv("CORS_ALLOW_CREDENTIALS", "true").lower() == "true"
# 应用时区配置(用于定时任务、账单日期等业务逻辑)
self.app_timezone = os.getenv("APP_TIMEZONE", "Asia/Shanghai")
self.auth_refresh_cookie_name = os.getenv(
"AUTH_REFRESH_COOKIE_NAME", "aether_refresh_token"
)
raw_refresh_cookie_samesite = os.getenv("AUTH_REFRESH_COOKIE_SAMESITE")
normalized_refresh_cookie_samesite = self._normalize_cookie_samesite(
raw_refresh_cookie_samesite
)
self._invalid_auth_refresh_cookie_samesite = (
raw_refresh_cookie_samesite
if raw_refresh_cookie_samesite is not None
and normalized_refresh_cookie_samesite is None
else None
)
# 生产默认使用 SameSite=None兼容前后端跨站点部署下的 refresh cookie。
self.auth_refresh_cookie_samesite = (
normalized_refresh_cookie_samesite
if normalized_refresh_cookie_samesite is not None
else ("none" if self.environment == "production" else "lax")
)
self.auth_refresh_cookie_secure = (
os.getenv(
"AUTH_REFRESH_COOKIE_SECURE",
"true" if self.environment == "production" else "false",
).lower()
== "true"
)
# 管理员账户配置(用于初始化)
self.admin_email = os.getenv("ADMIN_EMAIL", "admin@localhost")
self.admin_username = os.getenv("ADMIN_USERNAME", "admin")
# 管理员密码 - 必须在环境变量中设置
admin_password_env = os.getenv("ADMIN_PASSWORD")
if admin_password_env:
self.admin_password = admin_password_env
else:
# 未设置密码,启动时会报错
self.admin_password = ""
self._missing_admin_password = True
# API Key 配置
self.api_key_prefix = os.getenv("API_KEY_PREFIX", "sk")
# 支付回调安全配置(公开回调入口必须携带该共享密钥)
self.payment_callback_secret = os.getenv("PAYMENT_CALLBACK_SECRET", "").strip()
self.public_api_rate_limit = int(os.getenv("PUBLIC_API_RATE_LIMIT", "60"))
# 异常处理配置
# 设置为 True 时ProxyException 会传播到路由层以便记录 provider_request_headers
# 设置为 False 时,使用全局异常处理器统一处理
self.propagate_provider_exceptions = (
os.getenv("PROPAGATE_PROVIDER_EXCEPTIONS", "true").lower() == "true"
)
# 数据库连接池配置 - 智能自动调整
# 系统会根据 Worker 数量和 PostgreSQL 限制自动计算安全值
self.db_pool_size = int(os.getenv("DB_POOL_SIZE") or self._auto_pool_size())
self.db_max_overflow = int(os.getenv("DB_MAX_OVERFLOW") or self._auto_max_overflow())
self.db_pool_timeout = int(os.getenv("DB_POOL_TIMEOUT", "60"))
self.db_pool_recycle = int(os.getenv("DB_POOL_RECYCLE", "3600"))
self.db_pool_warn_threshold = int(os.getenv("DB_POOL_WARN_THRESHOLD", "70"))
# 并发控制配置
# CACHE_RESERVATION_RATIO: 缓存用户预留比例(默认 10%,新用户可用 90%
self.cache_reservation_ratio = float(os.getenv("CACHE_RESERVATION_RATIO", "0.1"))
# RPM 计数器时间窗口配置
from src.config.constants import RPMDefaults
self.rpm_bucket_seconds = int(
os.getenv("RPM_BUCKET_SECONDS", str(RPMDefaults.RPM_BUCKET_SECONDS))
)
self.rpm_key_ttl_seconds = int(
os.getenv("RPM_KEY_TTL_SECONDS", str(RPMDefaults.RPM_KEY_TTL_SECONDS))
)
self.rpm_cleanup_interval_seconds = int(
os.getenv("RPM_CLEANUP_INTERVAL_SECONDS", str(RPMDefaults.RPM_CLEANUP_INTERVAL_SECONDS))
)
# 限流降级策略配置
# RATE_LIMIT_FAIL_OPEN: 当限流服务Redis异常时的行为
#
# True (默认): fail-open - 放行请求(优先可用性)
# 风险Redis 故障期间无法限流,可能被滥用
# 适用API 网关作为关键基础设施,必须保持高可用
#
# False: fail-close - 拒绝所有请求(优先安全性)
# 风险Redis 故障会导致 API 网关不可用
# 适用:有严格速率限制要求的安全敏感场景
self.rate_limit_fail_open = os.getenv("RATE_LIMIT_FAIL_OPEN", "true").lower() == "true"
# HTTP 请求超时配置(秒)
self.http_connect_timeout = float(os.getenv("HTTP_CONNECT_TIMEOUT", "10.0"))
self.http_read_timeout = float(os.getenv("HTTP_READ_TIMEOUT", "3600.0"))
self.http_write_timeout = float(os.getenv("HTTP_WRITE_TIMEOUT", "3600.0"))
self.http_pool_timeout = float(os.getenv("HTTP_POOL_TIMEOUT", "10.0"))
# HTTP_REQUEST_TIMEOUT: 非流式请求整体超时(秒),默认 300 秒
self.http_request_timeout = float(os.getenv("HTTP_REQUEST_TIMEOUT", "300.0"))
# 内部 execution runtime 配置
# EXECUTION_RUNTIME_BACKEND:
# - rust: 当前默认,优先将可序列化的执行计划转发给 Rust execution runtime
# - python: 显式回退到现有 httpx 路径
#
# 兼容说明:
# - 旧 EXECUTOR_* 环境变量仍然可用
# - 代码中的 executor_* 属性继续保留为兼容别名
self._execution_runtime_backend = os.getenv(
"EXECUTION_RUNTIME_BACKEND",
os.getenv("EXECUTOR_BACKEND", "rust"),
).strip().lower()
self._execution_runtime_transport = os.getenv(
"EXECUTION_RUNTIME_TRANSPORT",
os.getenv("EXECUTOR_TRANSPORT", "unix_socket"),
).strip().lower()
self._execution_runtime_socket_path = os.getenv(
"EXECUTION_RUNTIME_SOCKET_PATH",
os.getenv("EXECUTOR_SOCKET_PATH", "/tmp/aether-executor.sock"),
).strip()
self._execution_runtime_base_url = os.getenv(
"EXECUTION_RUNTIME_BASE_URL",
os.getenv("EXECUTOR_BASE_URL", "http://127.0.0.1:5219"),
).strip()
self._execution_runtime_request_timeout = float(
os.getenv(
"EXECUTION_RUNTIME_REQUEST_TIMEOUT",
os.getenv("EXECUTOR_REQUEST_TIMEOUT", str(self.http_request_timeout)),
)
)
# HTTP 连接池配置
# HTTP_MAX_CONNECTIONS: 最大连接数,影响并发能力
# - 每个连接占用一个 socket过多会耗尽系统资源
# - 默认根据 Worker 数量自动计算:单 Worker 200多 Worker 按比例分配
# HTTP_KEEPALIVE_CONNECTIONS: 保活连接数,影响连接复用效率
# - 高频请求场景应该增大此值
# - 默认为 max_connections 的 30%(长连接场景更高效)
# HTTP_KEEPALIVE_EXPIRY: 保活过期时间(秒)
# - 过短会频繁重建连接,过长会占用资源
# - 默认 30 秒,生图等长连接场景可适当增大
self.http_max_connections = int(
os.getenv("HTTP_MAX_CONNECTIONS") or self._auto_http_max_connections()
)
self.http_keepalive_connections = int(
os.getenv("HTTP_KEEPALIVE_CONNECTIONS") or self._auto_http_keepalive_connections()
)
self.http_keepalive_expiry = float(os.getenv("HTTP_KEEPALIVE_EXPIRY", "30.0"))
# 上游传输优化配置
# ENABLE_HTTP2: 是否对上游请求启用 HTTP/2HPACK 头部压缩 + 多路复用)
# - 三家上游Claude/OpenAI/Gemini均已确认支持 HTTP/2
# - 出现兼容性问题时可通过环境变量快速回退到 HTTP/1.1
self.enable_http2 = os.getenv("ENABLE_HTTP2", "true").lower() == "true"
# 流式处理配置
# STREAM_PREFETCH_LINES: 预读行数,用于检测嵌套错误
# STREAM_STATS_DELAY: 统计记录延迟(秒),等待流完全关闭
# STREAM_FIRST_BYTE_TIMEOUT: 首字节超时(秒),等待首字节超过此时间触发故障转移
self.stream_prefetch_lines = int(os.getenv("STREAM_PREFETCH_LINES", "5"))
self.stream_stats_delay = float(os.getenv("STREAM_STATS_DELAY", "0.1"))
self.stream_first_byte_timeout = float(os.getenv("STREAM_FIRST_BYTE_TIMEOUT", "30.0"))
# Usage 队列配置Redis Streams
# 默认启用队列模式,通过 Redis Streams 异步写入 DB提升响应性能
self.usage_queue_enabled = os.getenv("USAGE_QUEUE_ENABLED", "true").lower() == "true"
# Python 宿主的 usage consumer 仅保留为显式回滚开关;默认 owner 为 Rust gateway。
self.usage_queue_python_consumer_enabled = (
os.getenv("USAGE_QUEUE_PYTHON_CONSUMER_ENABLED", "false").lower() == "true"
)
# Python 宿主的 quota scheduler 仅保留为显式回滚开关;默认 owner 为 Rust gateway。
self.quota_scheduler_python_enabled = (
os.getenv("QUOTA_SCHEDULER_PYTHON_ENABLED", "false").lower() == "true"
)
# Python 宿主的 model fetch scheduler 仅保留为显式回滚开关;默认 owner 为 Rust gateway。
self.model_fetch_scheduler_python_enabled = (
os.getenv("MODEL_FETCH_SCHEDULER_PYTHON_ENABLED", "false").lower() == "true"
)
# 队列事件是否包含 headers/bodies 由系统配置request_record_level决定
# 最终写入 DB 前仍会按 SystemConfigService 做脱敏与截断。
self.usage_queue_stream_key = os.getenv("USAGE_QUEUE_STREAM_KEY", "usage:events")
self.usage_queue_stream_group = os.getenv("USAGE_QUEUE_STREAM_GROUP", "usage_consumers")
# 主队列只做短暂缓冲;成功消费后会立即删除,不应把 Redis 当历史存储。
self.usage_queue_stream_maxlen = int(os.getenv("USAGE_QUEUE_STREAM_MAXLEN", "2000"))
self.usage_queue_dlq_key = os.getenv("USAGE_QUEUE_DLQ_KEY", "usage:events:dlq")
self.usage_queue_dlq_maxlen = int(os.getenv("USAGE_QUEUE_DLQ_MAXLEN", "5000"))
self.usage_queue_consumer_batch = int(os.getenv("USAGE_QUEUE_CONSUMER_BATCH", "200"))
self.usage_queue_consumer_block_ms = int(os.getenv("USAGE_QUEUE_CONSUMER_BLOCK_MS", "500"))
self.usage_queue_claim_idle_ms = int(os.getenv("USAGE_QUEUE_CLAIM_IDLE_MS", "30000"))
self.usage_queue_claim_interval_seconds = float(
os.getenv("USAGE_QUEUE_CLAIM_INTERVAL_SECONDS", "5")
)
self.usage_queue_max_retries = int(os.getenv("USAGE_QUEUE_MAX_RETRIES", "2"))
self.usage_queue_metrics_interval_seconds = float(
os.getenv("USAGE_QUEUE_METRICS_INTERVAL_SECONDS", "30")
)
# Admin analytics query defaults (protect DB from unbounded scans)
# ADMIN_USAGE_DEFAULT_DAYS:
# - 0: keep current behavior (no implicit time filter)
# - >0: when admin usage endpoints omit start_date/end_date, default to "last N days"
default_admin_usage_default_days = (
"0" if self.environment in {"development", "test", "testing"} else "30"
)
self.admin_usage_default_days = int(
os.getenv("ADMIN_USAGE_DEFAULT_DAYS", default_admin_usage_default_days)
)
# Thinking 整流器配置
# THINKING_RECTIFIER_ENABLED: 是否启用 Thinking 整流器
# 当遇到跨 Provider 的 thinking 签名错误时,自动整流请求体后重试
# 默认启用,设为 false 可禁用此功能
self.thinking_rectifier_enabled = (
os.getenv("THINKING_RECTIFIER_ENABLED", "true").lower() == "true"
)
# 请求体读取超时(秒)
# REQUEST_BODY_TIMEOUT: 等待客户端发送完整请求体的超时时间
# 默认 60 秒,防止客户端发送不完整请求导致连接卡死
self.request_body_timeout = float(os.getenv("REQUEST_BODY_TIMEOUT", "60.0"))
# 性能检测配置
# PERF_METRICS_ENABLED: 是否启用性能指标上报(监控插件)
# PERF_LOG_SLOW_MS: 慢请求日志阈值毫秒0 表示关闭
# PERF_SAMPLE_RATE: 采样率 (0-1),降低高频指标开销
self.perf_metrics_enabled = os.getenv("PERF_METRICS_ENABLED", "false").lower() == "true"
self.perf_log_slow_ms = int(os.getenv("PERF_LOG_SLOW_MS", "0"))
self.perf_sample_rate = float(os.getenv("PERF_SAMPLE_RATE", "1.0"))
# PERF_STORE_ENABLED: 是否将性能指标写入 Usage.request_metadata
# PERF_STORE_SAMPLE_RATE: 存储采样率 (0-1),用于降低写入压力
self.perf_store_enabled = os.getenv("PERF_STORE_ENABLED", "false").lower() == "true"
self.perf_store_sample_rate = float(os.getenv("PERF_STORE_SAMPLE_RATE", "1.0"))
# 解密缓存配置降低高频解密带来的CPU开销
# CRYPTO_DECRYPT_CACHE_ENABLED: 是否启用解密结果缓存
# CRYPTO_DECRYPT_CACHE_SIZE: 最大缓存条目数
# CRYPTO_DECRYPT_CACHE_TTL_SECONDS: 缓存TTL
self.crypto_decrypt_cache_enabled = (
os.getenv("CRYPTO_DECRYPT_CACHE_ENABLED", "true").lower() == "true"
)
self.crypto_decrypt_cache_size = int(os.getenv("CRYPTO_DECRYPT_CACHE_SIZE", "256"))
self.crypto_decrypt_cache_ttl_seconds = float(
os.getenv("CRYPTO_DECRYPT_CACHE_TTL_SECONDS", "60.0")
)
# 内部请求 User-Agent 配置(用于查询上游模型列表等)
# 可通过环境变量覆盖默认值,模拟对应 CLI 客户端
self.internal_user_agent_claude_cli = os.getenv(
"CLAUDE_CLI_USER_AGENT", "claude-code/1.0.1"
)
self.internal_user_agent_openai_cli = os.getenv("OPENAI_CLI_USER_AGENT", "openai-codex/1.0")
self.internal_user_agent_gemini_cli = os.getenv(
"GEMINI_CLI_USER_AGENT",
"GeminiCLI/0.1.5 (Windows; AMD64)",
)
# 邮箱验证配置
# VERIFICATION_CODE_EXPIRE_MINUTES: 验证码有效期(分钟)
# VERIFICATION_SEND_COOLDOWN: 发送冷却时间(秒)
self.verification_code_expire_minutes = int(
os.getenv("VERIFICATION_CODE_EXPIRE_MINUTES", "5")
)
self.verification_send_cooldown = int(os.getenv("VERIFICATION_SEND_COOLDOWN", "60"))
# 计费系统配置(多维度计费 / 异步任务)
# BILLING_REQUIRE_RULE: Video/Image/Audio 缺失 billing_rule 时是否拒绝请求(默认 false缺失则 cost=0 并告警)
# BILLING_STRICT_MODE: required 维度缺失时是否拒绝请求/标记任务失败(默认 false缺失则 cost=0 + 标记 incomplete
self.billing_require_rule = os.getenv("BILLING_REQUIRE_RULE", "false").lower() == "true"
self.billing_strict_mode = os.getenv("BILLING_STRICT_MODE", "false").lower() == "true"
# Usage.request_metadata 体积控制(用于降低 DB/CPU/内存压力)
# USAGE_METADATA_MAX_BYTES:
# - 0: unlimited (backward compatible)
# - >0: best-effort prune large keys when metadata JSON exceeds this size
default_usage_metadata_max_bytes = (
"0" if self.environment in {"development", "test", "testing"} else "65536"
)
self.usage_metadata_max_bytes = int(
os.getenv("USAGE_METADATA_MAX_BYTES", default_usage_metadata_max_bytes)
)
# 视频任务轮询配置
# VIDEO_POLL_INTERVAL_SECONDS: 轮询间隔(秒),默认 10 秒
# VIDEO_MAX_POLL_COUNT: 最大轮询次数,默认 360 次(约 1 小时)
# VIDEO_POLL_BATCH_SIZE: 每批处理任务数,默认 50
# VIDEO_POLL_CONCURRENCY: 并发轮询数,默认 10
# VIDEO_TASK_PYTHON_POLLER_ENABLED: Python 宿主是否继续托管 video poller。
# 默认 falseowner 迁移到 Rust gateway仅保留为显式回滚开关。
self.video_poll_interval_seconds = int(os.getenv("VIDEO_POLL_INTERVAL_SECONDS", "10"))
self.video_max_poll_count = int(os.getenv("VIDEO_MAX_POLL_COUNT", "360"))
self.video_poll_batch_size = int(os.getenv("VIDEO_POLL_BATCH_SIZE", "50"))
self.video_poll_concurrency = int(os.getenv("VIDEO_POLL_CONCURRENCY", "10"))
self.video_task_python_poller_enabled = (
os.getenv("VIDEO_TASK_PYTHON_POLLER_ENABLED", "false").lower() == "true"
)
# Management Token 速率限制(每分钟每 IP
self.management_token_rate_limit = int(os.getenv("MANAGEMENT_TOKEN_RATE_LIMIT", "30"))
# 每个用户最多可创建的 Management Token 数量
self.management_token_max_per_user = int(os.getenv("MANAGEMENT_TOKEN_MAX_PER_USER", "20"))
# 启动任务开关
# MAINTENANCE_STARTUP_TASKS_ENABLED: 是否在启动时执行维护调度器初始化任务(清理、统计回填等)
self.maintenance_startup_tasks_enabled = (
os.getenv("MAINTENANCE_STARTUP_TASKS_ENABLED", "true").lower() == "true"
)
# 已迁到 Rust gateway 的 maintenance job 默认不再由 Python scheduler 托管;
# 仅保留为显式回滚开关。
self.audit_cleanup_python_enabled = (
os.getenv("AUDIT_CLEANUP_PYTHON_ENABLED", "false").lower() == "true"
)
self.db_maintenance_python_enabled = (
os.getenv("DB_MAINTENANCE_PYTHON_ENABLED", "false").lower() == "true"
)
self.gemini_file_mapping_cleanup_python_enabled = (
os.getenv("GEMINI_FILE_MAPPING_CLEANUP_PYTHON_ENABLED", "false").lower() == "true"
)
self.provider_checkin_python_enabled = (
os.getenv("PROVIDER_CHECKIN_PYTHON_ENABLED", "false").lower() == "true"
)
self.request_candidate_cleanup_python_enabled = (
os.getenv("REQUEST_CANDIDATE_CLEANUP_PYTHON_ENABLED", "false").lower() == "true"
)
self.pending_cleanup_python_enabled = (
os.getenv("PENDING_CLEANUP_PYTHON_ENABLED", "false").lower() == "true"
)
self.pool_monitor_python_enabled = (
os.getenv("POOL_MONITOR_PYTHON_ENABLED", "false").lower() == "true"
)
self.http_client_idle_cleanup_python_enabled = (
os.getenv("HTTP_CLIENT_IDLE_CLEANUP_PYTHON_ENABLED", "false").lower() == "true"
)
self.stats_aggregation_python_enabled = (
os.getenv("STATS_AGGREGATION_PYTHON_ENABLED", "false").lower() == "true"
)
self.stats_hourly_aggregation_python_enabled = (
os.getenv("STATS_HOURLY_AGGREGATION_PYTHON_ENABLED", "false").lower() == "true"
)
self.usage_cleanup_python_enabled = (
os.getenv("USAGE_CLEANUP_PYTHON_ENABLED", "false").lower() == "true"
)
self.wallet_daily_usage_aggregation_python_enabled = (
os.getenv("WALLET_DAILY_USAGE_AGGREGATION_PYTHON_ENABLED", "false").lower()
== "true"
)
self.antigravity_ua_refresh_python_enabled = (
os.getenv("ANTIGRAVITY_UA_REFRESH_PYTHON_ENABLED", "false").lower() == "true"
)
# 启动预热配置(降低懒加载导致的首请求延迟)
# STARTUP_WARMUP_ENABLED: 是否启用启动期预热任务(默认 true
# STARTUP_WARMUP_GATE_READINESS: /readyz 是否等待预热完成(默认 true
# STARTUP_WARMUP_PROVIDER_TYPES: 预热时优先 bootstrap 的 provider_type 列表(逗号分隔)
self.startup_warmup_enabled = os.getenv("STARTUP_WARMUP_ENABLED", "true").lower() == "true"
self.startup_warmup_gate_readiness = (
os.getenv("STARTUP_WARMUP_GATE_READINESS", "true").lower() == "true"
)
warmup_provider_types_env = os.getenv("STARTUP_WARMUP_PROVIDER_TYPES", "").strip()
self.startup_warmup_provider_types = (
[
provider_type.strip()
for provider_type in warmup_provider_types_env.split(",")
if provider_type.strip()
]
if warmup_provider_types_env
else None
)
# API 文档配置
# DOCS_ENABLED: 是否启用 API 文档(/docs, /redoc, /openapi.json
# - 未设置: 开发环境启用,生产环境禁用
# - true: 强制启用
# - false: 强制禁用
docs_enabled_env = os.getenv("DOCS_ENABLED")
if docs_enabled_env is not None:
self.docs_enabled = docs_enabled_env.lower() == "true"
else:
# 默认:开发环境启用,生产环境禁用
self.docs_enabled = self.environment == "development"
# 验证连接池配置
self._validate_pool_config()
def _auto_pool_size(self) -> int:
"""
智能计算连接池大小 - 根据 Worker 数量和 PostgreSQL 限制计算
公式: (pg_max_connections - reserved) / workers / 2
除以 2 是因为还要预留 max_overflow 的空间
"""
available_connections = self.pg_max_connections - self.pg_reserved_connections
# 每个 Worker 可用的连接数pool_size + max_overflow
per_worker_total = available_connections // max(self.worker_processes, 1)
# pool_size 取总数的一半,另一半留给 overflow
pool_size = max(per_worker_total // 2, 5) # 最小 5 个连接
return min(pool_size, 15) # 最大 15 个连接
def _auto_max_overflow(self) -> int:
"""智能计算最大溢出连接数 - 与 pool_size 相同"""
return self.db_pool_size
def _auto_http_max_connections(self) -> int:
"""
智能计算 HTTP 最大连接数
计算依据:
1. 系统 socket 资源有限Linux 默认 ulimit -n 通常为 1024
2. 多 Worker 部署时每个进程独立连接池
3. 需要为数据库连接、Redis 连接等预留资源
公式: base_connections / workers
- 单 Worker: 100 连接
- 多 Worker: 按比例分配,确保总数不超过系统限制
范围: 30 - 100
"""
base_connections = 100
workers = max(self.worker_processes, 1)
per_worker = base_connections // workers
return max(30, min(per_worker, 100))
def _auto_http_keepalive_connections(self) -> int:
"""
智能计算 HTTP 保活连接数
计算依据:
1. 保活连接用于复用,减少 TCP 握手开销
2. 对于 API 网关场景,上游请求频繁,保活比例应较高
3. 生图等长连接场景,连接会被长时间占用
公式: max_connections * 0.3
- 30% 的比例在复用效率和资源占用间取得平衡
- 长连接场景建议手动调高到 50-70%
范围: 10 - max_connections
"""
# 保活连接数为最大连接数的 30%
keepalive = int(self.http_max_connections * 0.3)
# 最小 10 个保活连接,最大不超过 max_connections
return max(10, min(keepalive, self.http_max_connections))
def _validate_pool_config(self) -> None:
"""验证连接池配置是否安全"""
total_per_worker = self.db_pool_size + self.db_max_overflow
total_all_workers = total_per_worker * self.worker_processes
safe_limit = self.pg_max_connections - self.pg_reserved_connections
if total_all_workers > safe_limit:
# 记录警告(不抛出异常,避免阻止启动)
self._pool_config_warning = (
f"[WARN] 数据库连接池配置可能超过 PostgreSQL 限制: "
f"{self.worker_processes} workers x {total_per_worker} connections = "
f"{total_all_workers} > {safe_limit} (pg_max_connections - reserved). "
f"建议调整 DB_POOL_SIZE 或 PG_MAX_CONNECTIONS 环境变量。"
)
else:
self._pool_config_warning = None
@property
def database_url(self) -> str:
"""
数据库 URL延迟验证
在测试环境中可以通过依赖注入覆盖,而不会在导入时崩溃
"""
if not self._database_url:
raise ValueError(
"DATABASE_URL environment variable is required. "
"Example: postgresql://username:password@localhost:5432/dbname"
)
return self._database_url
@database_url.setter
def database_url(self, value: str) -> Any:
"""允许在测试中设置数据库 URL"""
self._database_url = value
@property
def execution_runtime_backend(self) -> str:
return self._execution_runtime_backend
@execution_runtime_backend.setter
def execution_runtime_backend(self, value: str) -> None:
self._execution_runtime_backend = str(value).strip().lower()
@property
def execution_runtime_transport(self) -> str:
return self._execution_runtime_transport
@execution_runtime_transport.setter
def execution_runtime_transport(self, value: str) -> None:
self._execution_runtime_transport = str(value).strip().lower()
@property
def execution_runtime_socket_path(self) -> str:
return self._execution_runtime_socket_path
@execution_runtime_socket_path.setter
def execution_runtime_socket_path(self, value: str) -> None:
self._execution_runtime_socket_path = str(value).strip()
@property
def execution_runtime_base_url(self) -> str:
return self._execution_runtime_base_url
@execution_runtime_base_url.setter
def execution_runtime_base_url(self, value: str) -> None:
self._execution_runtime_base_url = str(value).strip()
@property
def execution_runtime_request_timeout(self) -> float:
return self._execution_runtime_request_timeout
@execution_runtime_request_timeout.setter
def execution_runtime_request_timeout(self, value: float) -> None:
self._execution_runtime_request_timeout = float(value)
@property
def executor_backend(self) -> str:
return self.execution_runtime_backend
@executor_backend.setter
def executor_backend(self, value: str) -> None:
self.execution_runtime_backend = value
@property
def executor_transport(self) -> str:
return self.execution_runtime_transport
@executor_transport.setter
def executor_transport(self, value: str) -> None:
self.execution_runtime_transport = value
@property
def executor_socket_path(self) -> str:
return self.execution_runtime_socket_path
@executor_socket_path.setter
def executor_socket_path(self, value: str) -> None:
self.execution_runtime_socket_path = value
@property
def executor_base_url(self) -> str:
return self.execution_runtime_base_url
@executor_base_url.setter
def executor_base_url(self, value: str) -> None:
self.execution_runtime_base_url = value
@property
def executor_request_timeout(self) -> float:
return self.execution_runtime_request_timeout
@executor_request_timeout.setter
def executor_request_timeout(self, value: float) -> None:
self.execution_runtime_request_timeout = value
def log_startup_warnings(self) -> None:
"""
记录启动时的安全警告
这个方法应该在 logger 初始化后调用
"""
from src.core.logger import logger
# 连接池配置警告
if hasattr(self, "_pool_config_warning") and self._pool_config_warning:
logger.warning(self._pool_config_warning)
# 管理员密码检查(必须在环境变量中设置)
if hasattr(self, "_missing_admin_password") and self._missing_admin_password:
logger.error("必须设置 ADMIN_PASSWORD 环境变量!")
raise ValueError("ADMIN_PASSWORD environment variable must be set!")
# JWT 密钥警告
if not self.jwt_secret_key:
if self.environment == "production":
logger.error(
"生产环境未设置 JWT_SECRET_KEY! 这是严重的安全漏洞。"
"使用 'python generate_keys.py' 生成安全密钥。"
)
else:
logger.warning("JWT_SECRET_KEY 未设置,将使用默认密钥(仅限开发环境)")
# 加密密钥警告
if not self.encryption_key and self.environment != "production":
logger.warning("ENCRYPTION_KEY 未设置,使用开发环境默认密钥。生产环境必须设置。")
# CORS 配置警告(生产环境)
if self.environment == "production" and not self.cors_origins:
logger.warning("生产环境 CORS 未配置,前端将无法访问 API。请设置 CORS_ORIGINS。")
if self.environment == "production" and not self.payment_callback_secret:
logger.warning(
"生产环境未设置 PAYMENT_CALLBACK_SECRET支付回调将被拒绝。"
"如需启用支付回调,请配置共享密钥。"
)
def validate_security_config(self) -> list[str]:
"""
验证安全配置,返回错误列表
生产环境会阻止启动,开发环境仅警告
Returns:
错误消息列表(空列表表示验证通过)
"""
errors: list[str] = []
if self._invalid_auth_refresh_cookie_samesite:
errors.append("AUTH_REFRESH_COOKIE_SAMESITE must be one of: lax, strict, none.")
if self.auth_refresh_cookie_samesite == "none" and not self.auth_refresh_cookie_secure:
errors.append(
"AUTH_REFRESH_COOKIE_SECURE must be true when AUTH_REFRESH_COOKIE_SAMESITE=none."
)
if self.environment == "production":
# 生产环境必须设置 JWT 密钥
if not self.jwt_secret_key:
errors.append(
"JWT_SECRET_KEY must be set in production. "
"Use 'python generate_keys.py' to generate a secure key."
)
elif len(self.jwt_secret_key) < 32:
errors.append("JWT_SECRET_KEY must be at least 32 characters in production.")
# 生产环境必须设置加密密钥
if not self.encryption_key:
errors.append(
"ENCRYPTION_KEY must be set in production. "
"Use 'python generate_keys.py' to generate a secure key."
)
return errors
@classmethod
def _normalize_cookie_samesite(cls, value: str | None) -> str | None:
if value is None:
return None
normalized = value.strip().lower()
if normalized in cls._VALID_COOKIE_SAMESITE:
return normalized
return None
def __repr__(self) -> None:
"""配置信息字符串表示"""
return f"""
Configuration:
Server: {self.host}:{self.port}
Log Level: {self.log_level}
Environment: {self.environment}
"""
# 创建全局配置实例
config = Config()
# 在调试模式下记录配置(延迟到日志系统初始化后)
# 这个配置信息会在应用启动时通过日志系统输出