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,352 @@
"""
提供商策略管理 API 端点
"""
from __future__ import annotations
from datetime import datetime, timedelta, timezone
from typing import Any
from fastapi import APIRouter, Depends, HTTPException, Request
from fastapi.responses import JSONResponse
from pydantic import BaseModel, Field, ValidationError
from sqlalchemy import func
from sqlalchemy.orm import Session
from src.api.base.admin_adapter import AdminApiAdapter
from src.api.base.context import ApiRequestContext
from src.api.base.pipeline import get_pipeline
from src.core.enums import ProviderBillingType
from src.core.exceptions import InvalidRequestException, translate_pydantic_error
from src.core.logger import logger
from src.database import get_db
from src.models.database import Provider
from src.models.database_extensions import ProviderUsageTracking
router = APIRouter(prefix="/api/admin/provider-strategy", tags=["Provider Strategy"])
pipeline = get_pipeline()
class ProviderBillingUpdate(BaseModel):
billing_type: ProviderBillingType
monthly_quota_usd: float | None = None
quota_reset_day: int = Field(default=30, ge=1, le=365) # 重置周期(天数)
quota_last_reset_at: str | None = None # 当前周期开始时间
quota_expires_at: str | None = None
rpm_limit: int | None = Field(default=None, ge=0)
provider_priority: int = Field(default=100, ge=0, le=10000)
@router.put("/providers/{provider_id}/billing")
async def update_provider_billing(
provider_id: str,
request: Request,
db: Session = Depends(get_db),
) -> Any:
"""
更新提供商计费配置
更新指定提供商的计费策略、配额设置和优先级配置。
**路径参数**:
- `provider_id`: 提供商 ID
**请求体字段**:
- `billing_type`: 计费类型pay_as_you_go、subscription、prepaid、monthly_quota
- `monthly_quota_usd`: 月度配额(美元),可选
- `quota_reset_day`: 配额重置周期天数1-365默认 30
- `quota_last_reset_at`: 当前周期开始时间,可选(设置后会自动同步该周期内的历史使用量)
- `quota_expires_at`: 配额过期时间,可选
- `rpm_limit`: 每分钟请求数限制,可选
- `provider_priority`: 提供商优先级0-200默认 100
**返回字段**:
- `message`: 操作结果信息
- `provider`: 更新后的提供商信息
- `id`: 提供商 ID
- `name`: 提供商名称
- `billing_type`: 计费类型
- `provider_priority`: 提供商优先级
"""
adapter = AdminProviderBillingAdapter(provider_id=provider_id)
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
@router.get("/providers/{provider_id}/stats")
async def get_provider_stats(
provider_id: str,
request: Request,
hours: int = 24,
db: Session = Depends(get_db),
) -> Any:
"""
获取提供商统计数据
获取指定提供商的计费信息和使用统计数据。
**路径参数**:
- `provider_id`: 提供商 ID
**查询参数**:
- `hours`: 统计时间范围(小时),默认 24
**返回字段**:
- `provider_id`: 提供商 ID
- `provider_name`: 提供商名称
- `period_hours`: 统计时间范围
- `billing_info`: 计费信息
- `billing_type`: 计费类型
- `monthly_quota_usd`: 月度配额
- `monthly_used_usd`: 月度已使用
- `quota_remaining_usd`: 剩余配额
- `quota_expires_at`: 配额过期时间
- `usage_stats`: 使用统计
- `total_requests`: 总请求数
- `successful_requests`: 成功请求数
- `failed_requests`: 失败请求数
- `success_rate`: 成功率
- `avg_response_time_ms`: 平均响应时间(毫秒)
- `total_cost_usd`: 总成本(美元)
"""
adapter = AdminProviderStatsAdapter(provider_id=provider_id, hours=hours)
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
@router.delete("/providers/{provider_id}/quota")
async def reset_provider_quota(
provider_id: str,
request: Request,
db: Session = Depends(get_db),
) -> Any:
"""Reset provider quota usage to zero"""
adapter = AdminProviderResetQuotaAdapter(provider_id=provider_id)
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
@router.get("/strategies")
async def list_available_strategies(request: Request, db: Session = Depends(get_db)) -> Any:
"""
获取可用负载均衡策略列表
列出系统中所有已注册的负载均衡策略插件。
**返回字段**:
- `strategies`: 策略列表
- `name`: 策略名称
- `priority`: 策略优先级
- `version`: 策略版本
- `description`: 策略描述
- `author`: 策略作者
- `total`: 策略总数
"""
adapter = AdminListStrategiesAdapter()
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
class AdminProviderBillingAdapter(AdminApiAdapter):
def __init__(self, provider_id: str):
self.provider_id = provider_id
async def handle(self, context: ApiRequestContext) -> Any:
db = context.db
payload = context.ensure_json_body()
try:
config = ProviderBillingUpdate.model_validate(payload)
except ValidationError as e:
errors = e.errors()
if errors:
raise InvalidRequestException(translate_pydantic_error(errors[0]))
raise InvalidRequestException("请求数据验证失败")
provider = db.query(Provider).filter(Provider.id == self.provider_id).first()
if not provider:
raise HTTPException(status_code=404, detail="Provider not found")
provider.billing_type = config.billing_type
provider.monthly_quota_usd = config.monthly_quota_usd
provider.quota_reset_day = config.quota_reset_day
provider.provider_priority = config.provider_priority
from sqlalchemy import func
from src.models.database import Usage
if config.quota_last_reset_at:
new_reset_at = datetime.fromisoformat(config.quota_last_reset_at)
# 确保有时区信息,如果没有则假设为 UTC
if new_reset_at.tzinfo is None:
new_reset_at = new_reset_at.replace(tzinfo=timezone.utc)
provider.quota_last_reset_at = new_reset_at
# 自动同步该周期内的历史使用量
period_usage = (
db.query(func.coalesce(func.sum(Usage.total_cost_usd), 0))
.filter(
Usage.provider_id == self.provider_id,
Usage.created_at >= new_reset_at,
)
.scalar()
)
provider.monthly_used_usd = float(period_usage or 0)
logger.info(
f"Synced usage for provider {provider.name}: ${period_usage:.4f} since {new_reset_at}"
)
if config.quota_expires_at:
expires_at = datetime.fromisoformat(config.quota_expires_at)
# 确保有时区信息,如果没有则假设为 UTC
if expires_at.tzinfo is None:
expires_at = expires_at.replace(tzinfo=timezone.utc)
provider.quota_expires_at = expires_at
db.commit()
db.refresh(provider)
logger.info(f"Updated billing config for provider {provider.name}")
return JSONResponse(
{
"message": "Provider billing config updated successfully",
"provider": {
"id": provider.id,
"name": provider.name,
"billing_type": provider.billing_type.value,
"provider_priority": provider.provider_priority,
},
}
)
class AdminProviderStatsAdapter(AdminApiAdapter):
def __init__(self, provider_id: str, hours: int):
self.provider_id = provider_id
self.hours = hours
async def handle(self, context: ApiRequestContext) -> Any:
db = context.db
provider = db.query(Provider).filter(Provider.id == self.provider_id).first()
if not provider:
raise HTTPException(status_code=404, detail="Provider not found")
since = datetime.now(timezone.utc) - timedelta(hours=self.hours)
row = (
db.query(
func.coalesce(func.sum(ProviderUsageTracking.total_requests), 0),
func.coalesce(func.sum(ProviderUsageTracking.successful_requests), 0),
func.coalesce(func.sum(ProviderUsageTracking.failed_requests), 0),
func.coalesce(func.avg(ProviderUsageTracking.avg_response_time_ms), 0),
func.coalesce(func.sum(ProviderUsageTracking.total_cost_usd), 0),
)
.filter(
ProviderUsageTracking.provider_id == self.provider_id,
ProviderUsageTracking.window_start >= since,
)
.one()
)
total_requests = int(row[0])
total_success = int(row[1])
total_failures = int(row[2])
avg_response_time = float(row[3])
total_cost = float(row[4])
return JSONResponse(
{
"provider_id": self.provider_id,
"provider_name": provider.name,
"period_hours": self.hours,
"billing_info": {
"billing_type": provider.billing_type.value,
"monthly_quota_usd": (
float(provider.monthly_quota_usd)
if provider.monthly_quota_usd is not None
else None
),
"monthly_used_usd": float(provider.monthly_used_usd or 0),
"quota_remaining_usd": (
float(provider.monthly_quota_usd - provider.monthly_used_usd)
if provider.monthly_quota_usd is not None
else None
),
"quota_expires_at": (
provider.quota_expires_at.isoformat() if provider.quota_expires_at else None
),
},
"usage_stats": {
"total_requests": total_requests,
"successful_requests": total_success,
"failed_requests": total_failures,
"success_rate": total_success / total_requests if total_requests > 0 else 0,
"avg_response_time_ms": round(avg_response_time, 2),
"total_cost_usd": round(total_cost, 4),
},
}
)
class AdminProviderResetQuotaAdapter(AdminApiAdapter):
def __init__(self, provider_id: str):
self.provider_id = provider_id
async def handle(self, context: ApiRequestContext) -> Any:
db = context.db
provider = db.query(Provider).filter(Provider.id == self.provider_id).first()
if not provider:
raise HTTPException(status_code=404, detail="Provider not found")
if provider.billing_type != ProviderBillingType.MONTHLY_QUOTA:
raise HTTPException(status_code=400, detail="Only monthly quota providers can be reset")
old_used = provider.monthly_used_usd
provider.monthly_used_usd = 0.0
db.commit()
logger.info(f"Manually reset quota for provider {provider.name}")
return JSONResponse(
{
"message": "Provider quota reset successfully",
"provider_name": provider.name,
"previous_used": old_used,
"current_used": 0.0,
}
)
class AdminListStrategiesAdapter(AdminApiAdapter):
async def handle(self, context: ApiRequestContext) -> Any:
from src.plugins.manager import get_plugin_manager
plugin_manager = get_plugin_manager()
lb_plugins = plugin_manager.plugins.get("load_balancer", {})
strategies = []
for name, plugin in lb_plugins.items():
try:
strategies.append(
{
"name": getattr(plugin, "name", name),
"priority": getattr(plugin, "priority", 0),
"version": (
getattr(plugin.metadata, "version", "1.0.0")
if hasattr(plugin, "metadata")
else "1.0.0"
),
"description": (
getattr(plugin.metadata, "description", "")
if hasattr(plugin, "metadata")
else ""
),
"author": (
getattr(plugin.metadata, "author", "Unknown")
if hasattr(plugin, "metadata")
else "Unknown"
),
}
)
except Exception as exc: # pragma: no cover
logger.error(f"Error accessing plugin {name}: {exc}")
continue
strategies.sort(key=lambda x: x["priority"], reverse=True)
return JSONResponse({"strategies": strategies, "total": len(strategies)})