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,546 @@
from __future__ import annotations
import base64
import time
import uuid
from dataclasses import asdict
from datetime import datetime, timezone
from typing import Any
from fastapi import BackgroundTasks, HTTPException, Request
from fastapi.responses import JSONResponse, Response
from sqlalchemy.orm import Session
from src.api.base.context import ApiRequestContext
from src.api.base.pipeline import get_pipeline
from src.core.api_format.headers import extract_client_api_key_for_endpoint_with_query
from src.core.api_format.metadata import get_auth_config_for_endpoint
from src.core.crypto import crypto_service
from src.core.http_compression import normalize_content_encoding
from src.core.logger import logger
from src.database import create_session, get_db
from src.models.database import ApiKey, RequestCandidate, User
from src.services.auth.service import AuthService
from src.utils.async_utils import safe_create_task
from .common import ensure_loopback
from .gateway_contract import (
_GEMINI_FILES_DOWNLOAD_ROUTE_RE,
_GEMINI_FILES_RESOURCE_ROUTE_RE,
_GEMINI_MODEL_OPERATION_CANCEL_RE,
_GEMINI_OPERATION_CANCEL_RE,
_GEMINI_SYNC_ROUTE_RE,
_GEMINI_VIDEO_CREATE_ROUTE_RE,
_GEMINI_VIDEO_MODEL_OPERATION_ANY_RE,
_OPENAI_VIDEO_CANCEL_ROUTE_RE,
_OPENAI_VIDEO_CONTENT_ROUTE_RE,
_OPENAI_VIDEO_REMIX_ROUTE_RE,
_OPENAI_VIDEO_TASK_ROUTE_RE,
CONTROL_ACTION_HEADER,
CONTROL_ACTION_PROXY_PUBLIC,
CONTROL_EXECUTED_HEADER,
GatewayAuthContext,
GatewayExecuteRequest,
GatewayExecutionDecisionResponse,
GatewayExecutionPlanResponse,
GatewayResolveRequest,
GatewayRouteDecision,
GatewayStreamReportRequest,
GatewaySyncReportRequest,
classify_gateway_route,
)
class _GatewayProxy:
def __getattr__(self, name: str) -> Any:
from . import gateway as gateway_module
return getattr(gateway_module, name)
gateway_module = _GatewayProxy()
async def _build_gemini_files_download_stream_plan(
*,
request: Request,
db: Session,
user: User,
user_api_key: ApiKey,
file_id: str,
) -> dict[str, Any]:
from src.api.public.gemini_files import (
GEMINI_FILES_BASE_URL,
UpstreamContext,
_build_upstream_headers,
_build_upstream_url,
_enrich_upstream_context_proxy,
_find_video_task_by_id,
_resolve_files_model_name,
_select_provider_candidate,
resolve_provider_proxy,
)
from src.services.request.execution_runtime_plan import (
ExecutionPlan,
ExecutionPlanBody,
ExecutionPlanTimeouts,
ExecutionProxySnapshot,
)
proxy_snapshot = None
provider_id = ""
endpoint_id = ""
file_key_id = ""
if file_id.startswith("aev_"):
short_id = file_id[4:]
upstream_key, video_url = await _find_video_task_by_id(db, short_id, user.id)
if not upstream_key or not video_url:
raise HTTPException(
status_code=404,
detail={
"error": {
"code": 404,
"message": f"Video not found or not ready: {file_id}",
"status": "NOT_FOUND",
}
},
)
upstream_url = video_url
try:
from src.services.proxy_node.resolver import (
build_proxy_url_async,
get_system_proxy_config_async,
resolve_delegate_config_async,
resolve_proxy_info_async,
)
system_proxy = await get_system_proxy_config_async()
delegate_cfg = await resolve_delegate_config_async(system_proxy)
proxy_url: str | None = None
if system_proxy and not (delegate_cfg and delegate_cfg.get("tunnel")):
proxy_url = await build_proxy_url_async(system_proxy)
proxy_info = await resolve_proxy_info_async(system_proxy)
proxy_snapshot = ExecutionProxySnapshot.from_proxy_info(
proxy_info,
proxy_url=proxy_url,
mode_override="tunnel" if delegate_cfg and delegate_cfg.get("tunnel") else None,
node_id_override=(
str(delegate_cfg.get("node_id") or "").strip() or None
if delegate_cfg and delegate_cfg.get("tunnel")
else None
),
)
except Exception:
proxy_snapshot = None
else:
model_name = _resolve_files_model_name(db, user_api_key, user)
if not model_name:
raise HTTPException(
status_code=503,
detail={
"error": {
"code": 503,
"message": "No available model for Gemini Files API routing",
"status": "UNAVAILABLE",
}
},
)
candidate = await _select_provider_candidate(
db,
user_api_key,
model_name,
require_files_capability=True,
)
if not candidate:
raise HTTPException(
status_code=503,
detail={
"error": {
"code": 503,
"message": "No available Gemini key with 'gemini_files' capability enabled",
"status": "UNAVAILABLE",
}
},
)
try:
upstream_key = crypto_service.decrypt(candidate.key.api_key)
except Exception as exc:
raise HTTPException(
status_code=500,
detail={
"error": {
"code": 500,
"message": "Failed to decrypt provider key",
"status": "INTERNAL",
}
},
) from exc
regular_ctx = UpstreamContext(
upstream_key=upstream_key,
base_url=candidate.endpoint.base_url or GEMINI_FILES_BASE_URL,
file_key_id=str(candidate.key.id),
user_id=str(user.id),
provider_id=str(candidate.provider.id),
endpoint_id=str(candidate.endpoint.id),
provider_proxy=resolve_provider_proxy(endpoint=candidate.endpoint, key=candidate.key),
key_proxy=(
candidate.key.proxy
if isinstance(getattr(candidate.key, "proxy", None), dict)
else None
),
)
ctx = await _enrich_upstream_context_proxy(regular_ctx)
file_key_id = ctx.file_key_id
provider_id = ctx.provider_id
endpoint_id = ctx.endpoint_id
proxy_snapshot = ctx.proxy_snapshot
file_name = f"files/{file_id}" if not file_id.startswith("files/") else file_id
upstream_url = _build_upstream_url(
ctx.base_url,
f"/v1beta/{file_name}:download",
dict(request.query_params),
)
headers = _build_upstream_headers(dict(request.headers), upstream_key)
plan = ExecutionPlan(
request_id=str(getattr(request.state, "request_id", "") or uuid.uuid4().hex),
candidate_id=None,
provider_name="gemini",
provider_id=provider_id,
endpoint_id=endpoint_id,
key_id=str(file_key_id or ""),
method="GET",
url=upstream_url,
headers=headers,
body=ExecutionPlanBody(),
stream=True,
provider_api_format="gemini:files",
client_api_format="gemini:files",
model_name="gemini-files",
proxy=proxy_snapshot,
timeouts=ExecutionPlanTimeouts(
connect_ms=30_000,
read_ms=300_000,
write_ms=300_000,
pool_ms=30_000,
total_ms=None,
),
)
return plan.to_payload()
async def _build_gemini_files_download_stream_decision(
*,
request: Request,
db: Session,
user: User,
user_api_key: ApiKey,
file_id: str,
) -> GatewayExecutionDecisionResponse:
from src.api.public.gemini_files import (
GEMINI_FILES_BASE_URL,
UpstreamContext,
_build_upstream_headers,
_build_upstream_url,
_enrich_upstream_context_proxy,
_find_video_task_by_id,
_resolve_files_model_name,
_select_provider_candidate,
resolve_provider_proxy,
)
proxy_snapshot = None
provider_id = ""
endpoint_id = ""
file_key_id = ""
if file_id.startswith("aev_"):
short_id = file_id[4:]
upstream_key, video_url = await _find_video_task_by_id(db, short_id, user.id)
if not upstream_key or not video_url:
raise HTTPException(
status_code=404,
detail={
"error": {
"code": 404,
"message": f"Video not found or not ready: {file_id}",
"status": "NOT_FOUND",
}
},
)
upstream_url = video_url
try:
from src.services.proxy_node.resolver import (
build_proxy_url_async,
get_system_proxy_config_async,
resolve_delegate_config_async,
resolve_proxy_info_async,
)
from src.services.request.execution_runtime_plan import ExecutionProxySnapshot
system_proxy = await get_system_proxy_config_async()
delegate_cfg = await resolve_delegate_config_async(system_proxy)
proxy_url: str | None = None
if system_proxy and not (delegate_cfg and delegate_cfg.get("tunnel")):
proxy_url = await build_proxy_url_async(system_proxy)
proxy_info = await resolve_proxy_info_async(system_proxy)
proxy_snapshot = ExecutionProxySnapshot.from_proxy_info(
proxy_info,
proxy_url=proxy_url,
mode_override="tunnel" if delegate_cfg and delegate_cfg.get("tunnel") else None,
node_id_override=(
str(delegate_cfg.get("node_id") or "").strip() or None
if delegate_cfg and delegate_cfg.get("tunnel")
else None
),
)
except Exception:
proxy_snapshot = None
else:
model_name = _resolve_files_model_name(db, user_api_key, user)
if not model_name:
raise HTTPException(
status_code=503,
detail={
"error": {
"code": 503,
"message": "No available model for Gemini Files API routing",
"status": "UNAVAILABLE",
}
},
)
candidate = await _select_provider_candidate(
db,
user_api_key,
model_name,
require_files_capability=True,
)
if not candidate:
raise HTTPException(
status_code=503,
detail={
"error": {
"code": 503,
"message": "No available Gemini key with 'gemini_files' capability enabled",
"status": "UNAVAILABLE",
}
},
)
try:
upstream_key = crypto_service.decrypt(candidate.key.api_key)
except Exception as exc:
raise HTTPException(
status_code=500,
detail={
"error": {
"code": 500,
"message": "Failed to decrypt provider key",
"status": "INTERNAL",
}
},
) from exc
regular_ctx = UpstreamContext(
upstream_key=upstream_key,
base_url=candidate.endpoint.base_url or GEMINI_FILES_BASE_URL,
file_key_id=str(candidate.key.id),
user_id=str(user.id),
provider_id=str(candidate.provider.id),
endpoint_id=str(candidate.endpoint.id),
provider_proxy=resolve_provider_proxy(endpoint=candidate.endpoint, key=candidate.key),
key_proxy=(
candidate.key.proxy
if isinstance(getattr(candidate.key, "proxy", None), dict)
else None
),
)
ctx = await _enrich_upstream_context_proxy(regular_ctx)
file_key_id = ctx.file_key_id
provider_id = ctx.provider_id
endpoint_id = ctx.endpoint_id
proxy_snapshot = ctx.proxy_snapshot
file_name = f"files/{file_id}" if not file_id.startswith("files/") else file_id
upstream_url = _build_upstream_url(
ctx.base_url,
f"/v1beta/{file_name}:download",
dict(request.query_params),
)
headers = _build_upstream_headers(dict(request.headers), upstream_key)
return GatewayExecutionDecisionResponse(
action="executor_stream_decision",
decision_kind="gemini_files_download",
request_id=str(getattr(request.state, "request_id", "") or uuid.uuid4().hex),
provider_name="gemini",
provider_id=provider_id,
endpoint_id=endpoint_id,
key_id=str(file_key_id or ""),
upstream_base_url="",
upstream_url=upstream_url,
auth_header="",
auth_value="",
provider_api_format="gemini:files",
client_api_format="gemini:files",
model_name="gemini-files",
provider_request_headers=headers,
proxy=asdict(proxy_snapshot) if proxy_snapshot else None,
timeouts={
"connect_ms": 30_000,
"read_ms": 300_000,
"write_ms": 300_000,
"pool_ms": 30_000,
},
)
async def _build_gemini_files_get_sync_plan(
*,
request: Request,
db: Session,
file_name: str,
) -> tuple[dict[str, Any], dict[str, Any]]:
normalized_file_name = (
file_name if str(file_name or "").startswith("files/") else f"files/{file_name}"
)
return await _build_gemini_files_proxy_sync_plan(
request=request,
db=db,
method="GET",
upstream_path=f"/v1beta/{normalized_file_name}",
)
async def _build_gemini_files_proxy_sync_decision(
*,
request: Request,
db: Session,
method: str,
upstream_path: str,
decision_kind: str,
report_kind: str,
report_context: dict[str, Any] | None = None,
) -> GatewayExecutionDecisionResponse:
from src.api.public.gemini_files import (
_build_upstream_headers,
_build_upstream_url,
_enrich_upstream_context_proxy,
_resolve_upstream_context,
)
ctx = await _resolve_upstream_context(request, db)
ctx = await _enrich_upstream_context_proxy(ctx)
upstream_url = _build_upstream_url(
ctx.base_url,
upstream_path,
dict(request.query_params),
)
headers = _build_upstream_headers(dict(request.headers), ctx.upstream_key)
merged_report_context = {
"file_key_id": str(ctx.file_key_id or ""),
"user_id": str(ctx.user_id or ""),
}
if report_context:
merged_report_context.update(report_context)
return GatewayExecutionDecisionResponse(
action="executor_sync_decision",
decision_kind=decision_kind,
request_id=str(getattr(request.state, "request_id", "") or uuid.uuid4().hex),
provider_name="gemini",
provider_id=str(ctx.provider_id or ""),
endpoint_id=str(ctx.endpoint_id or ""),
key_id=str(ctx.file_key_id or ""),
upstream_base_url=str(ctx.base_url or ""),
upstream_url=upstream_url,
auth_header="",
auth_value="",
provider_api_format="gemini:files",
client_api_format="gemini:files",
model_name="gemini-files",
provider_request_headers=headers,
proxy=asdict(ctx.proxy_snapshot) if ctx.proxy_snapshot else None,
timeouts={
"connect_ms": 30_000,
"read_ms": 300_000,
"write_ms": 300_000,
"pool_ms": 30_000,
"total_ms": 300_000,
},
report_kind=report_kind,
report_context=merged_report_context,
)
async def _build_gemini_files_proxy_sync_plan(
*,
request: Request,
db: Session,
method: str,
upstream_path: str,
is_upload: bool = False,
) -> tuple[dict[str, Any], dict[str, Any]]:
from src.api.public.gemini_files import (
_build_upstream_headers,
_build_upstream_url,
_enrich_upstream_context_proxy,
_resolve_upstream_context,
)
from src.services.request.execution_runtime_plan import (
ExecutionPlan,
ExecutionPlanTimeouts,
build_execution_plan_body,
)
ctx = await _resolve_upstream_context(request, db)
ctx = await _enrich_upstream_context_proxy(ctx)
upstream_url = _build_upstream_url(
ctx.base_url,
upstream_path,
dict(request.query_params),
is_upload=is_upload,
)
headers = _build_upstream_headers(dict(request.headers), ctx.upstream_key)
body_bytes = await request.body()
content_type = str(headers.get("content-type") or "").strip() or None
plan = ExecutionPlan(
request_id=str(getattr(request.state, "request_id", "") or uuid.uuid4().hex),
candidate_id=None,
provider_name="gemini",
provider_id=str(ctx.provider_id or ""),
endpoint_id=str(ctx.endpoint_id or ""),
key_id=str(ctx.file_key_id or ""),
method=method.upper(),
url=upstream_url,
headers=headers,
body=(
build_execution_plan_body(body_bytes, content_type=content_type)
if body_bytes
else build_execution_plan_body(None)
),
stream=False,
provider_api_format="gemini:files",
client_api_format="gemini:files",
model_name="gemini-files",
proxy=ctx.proxy_snapshot,
timeouts=ExecutionPlanTimeouts(
connect_ms=30_000,
read_ms=300_000,
write_ms=300_000,
pool_ms=30_000,
total_ms=300_000,
),
)
return plan.to_payload(), {
"file_key_id": str(ctx.file_key_id or ""),
"user_id": str(ctx.user_id or ""),
}