mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-03 09:50:21 +08:00
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:
@@ -0,0 +1,388 @@
|
||||
"""Antigravity provider plugin — 统一注册入口。
|
||||
|
||||
将 Antigravity 对各通用 registry / capability registry 的注册集中在一个文件中:
|
||||
- Envelope (v1internal 信封)
|
||||
- Transport Hook (URL 构建)
|
||||
- Auth Enricher (OAuth enrichment)
|
||||
- Model Fetcher (模型获取)
|
||||
- Provider Format Capability(跨格式变体)
|
||||
|
||||
新增 provider 时参照此文件创建对应的 plugin.py 即可。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
from typing import Any
|
||||
from urllib.parse import urlencode
|
||||
|
||||
from src.core.logger import logger
|
||||
from src.services.provider.adapters.antigravity.constants import V1INTERNAL_PATH_TEMPLATE
|
||||
from src.services.provider.adapters.antigravity.url_availability import url_availability
|
||||
from src.services.provider.request_context import set_selected_base_url
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Transport Hook
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def build_antigravity_url(
|
||||
endpoint: Any,
|
||||
*,
|
||||
is_stream: bool,
|
||||
effective_query_params: dict[str, Any],
|
||||
**_kwargs: Any,
|
||||
) -> str:
|
||||
"""构建 Antigravity v1internal URL。
|
||||
|
||||
使用 url_availability 选择最优端点,构建 v1internal:generateContent 或
|
||||
v1internal:streamGenerateContent URL。
|
||||
"""
|
||||
ordered_urls = url_availability.get_ordered_urls(prefer_daily=True)
|
||||
base_url = ordered_urls[0] if ordered_urls else endpoint.base_url
|
||||
|
||||
# 存入 contextvars(供后续 Handler 层 envelope 获取)
|
||||
set_selected_base_url(str(base_url) if base_url is not None else None)
|
||||
|
||||
action = "streamGenerateContent" if is_stream else "generateContent"
|
||||
path = V1INTERNAL_PATH_TEMPLATE.format(action=action)
|
||||
|
||||
query_params = dict(effective_query_params)
|
||||
|
||||
# v1internal 流式请求同样支持 ?alt=sse
|
||||
if is_stream:
|
||||
query_params.setdefault("alt", "sse")
|
||||
|
||||
# 移除 v1internal 不支持的查询参数
|
||||
query_params.pop("beta", None)
|
||||
|
||||
url = f"{str(base_url).rstrip('/')}{path}"
|
||||
if query_params:
|
||||
query_string = urlencode(query_params, doseq=True)
|
||||
if query_string:
|
||||
url = f"{url}?{query_string}"
|
||||
|
||||
return url
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Auth Enricher
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
async def enrich_antigravity(
|
||||
auth_config: dict[str, Any],
|
||||
token_response: dict[str, Any],
|
||||
access_token: str,
|
||||
proxy_config: dict[str, Any] | None,
|
||||
) -> dict[str, Any]:
|
||||
"""Antigravity auth_config enrichment.
|
||||
|
||||
1. 通过 Google userinfo API 获取 email
|
||||
2. 通过 loadCodeAssist 获取 project_id / tier
|
||||
3. 未激活账号尝试 onboardUser
|
||||
4. fallback 到随机 project_id
|
||||
"""
|
||||
from src.core.provider_oauth_utils import fetch_google_email
|
||||
from src.services.provider.adapters.antigravity.client import (
|
||||
extract_project_id,
|
||||
extract_tier_id,
|
||||
generate_fallback_project_id,
|
||||
load_code_assist,
|
||||
onboard_user,
|
||||
)
|
||||
|
||||
# Email(仅在缺失时获取)
|
||||
if not auth_config.get("email"):
|
||||
email = await fetch_google_email(
|
||||
access_token,
|
||||
proxy_config=proxy_config,
|
||||
timeout_seconds=10.0,
|
||||
)
|
||||
if email:
|
||||
auth_config["email"] = email
|
||||
|
||||
# Project ID + Tier(需要 loadCodeAssist 时一起获取)
|
||||
need_project = not auth_config.get("project_id")
|
||||
# Tier 每次 enrich 都重新获取(确保归一化为 Free/Pro/Ultra)
|
||||
need_tier = True
|
||||
|
||||
if need_project or need_tier:
|
||||
project_id = auth_config.get("project_id", "")
|
||||
try:
|
||||
code_assist = await load_code_assist(access_token, proxy_config=proxy_config)
|
||||
|
||||
# 提取 tier 信息(对齐 sub2api:优先 paidTier,fallback currentTier)
|
||||
tier_str = _extract_tier_from_code_assist(code_assist)
|
||||
if tier_str:
|
||||
auth_config["tier"] = tier_str
|
||||
logger.info("[enrich] Antigravity tier: {}", tier_str)
|
||||
|
||||
if need_project:
|
||||
project_id = extract_project_id(code_assist)
|
||||
|
||||
# 未激活:尝试 onboardUser
|
||||
if not project_id and code_assist.get("allowedTiers"):
|
||||
tier_id = extract_tier_id(code_assist)
|
||||
logger.info("[enrich] Antigravity onboardUser tier={}", tier_id)
|
||||
project_id = await onboard_user(
|
||||
access_token,
|
||||
tier_id=tier_id,
|
||||
proxy_config=proxy_config,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning("[enrich] Antigravity loadCodeAssist/onboardUser 失败: {}", e)
|
||||
|
||||
if need_project:
|
||||
if project_id:
|
||||
auth_config["project_id"] = project_id
|
||||
logger.info("[enrich] Antigravity project_id: {}", project_id[:8] + "...")
|
||||
else:
|
||||
fallback = generate_fallback_project_id()
|
||||
auth_config["project_id"] = fallback
|
||||
logger.info("[enrich] Antigravity 随机 project_id fallback: {}", fallback)
|
||||
|
||||
return auth_config
|
||||
|
||||
|
||||
def _extract_tier_from_code_assist(code_assist: dict[str, Any]) -> str:
|
||||
"""从 loadCodeAssist 响应中提取用户层级,返回 Free/Pro/Ultra。
|
||||
|
||||
对齐 sub2api/CLIProxyAPI:优先 paidTier,fallback currentTier。
|
||||
兼容 tier 为字符串或 {"id": "...", "tierType": "..."} 两种格式。
|
||||
"""
|
||||
# 优先 paidTier(付费订阅级别)
|
||||
paid_tier = code_assist.get("paidTier")
|
||||
tier = _normalize_tier(_extract_tier_raw(paid_tier))
|
||||
if tier:
|
||||
return tier
|
||||
|
||||
# fallback currentTier
|
||||
current_tier = code_assist.get("currentTier")
|
||||
tier = _normalize_tier(_extract_tier_raw(current_tier))
|
||||
if tier:
|
||||
return tier
|
||||
|
||||
return "Free"
|
||||
|
||||
|
||||
def _extract_tier_raw(tier_obj: Any) -> str:
|
||||
"""从 tier 对象中提取原始标识,兼容字符串和字典两种格式。"""
|
||||
if isinstance(tier_obj, str) and tier_obj.strip():
|
||||
return tier_obj.strip()
|
||||
if isinstance(tier_obj, dict):
|
||||
for key in ("id", "tierType"):
|
||||
val = tier_obj.get(key)
|
||||
if isinstance(val, str) and val.strip():
|
||||
return val.strip()
|
||||
return ""
|
||||
|
||||
|
||||
def _normalize_tier(raw: str) -> str:
|
||||
"""将上游 tier 标识归一化为 Free/Pro/Ultra。
|
||||
|
||||
上游格式示例:
|
||||
- id: "free-tier", "g1-pro-tier", "g1-ultra-tier"
|
||||
- tierType: "FREE", "PAID"
|
||||
"""
|
||||
if not raw:
|
||||
return ""
|
||||
lower = raw.lower()
|
||||
if "ultra" in lower:
|
||||
return "Ultra"
|
||||
if "pro" in lower or "paid" in lower:
|
||||
return "Pro"
|
||||
if "free" in lower or "legacy" in lower:
|
||||
return "Free"
|
||||
return raw
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Model Fetcher
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# Antigravity 内部测试/调试模型,不应暴露给用户
|
||||
BLOCKED_MODELS: frozenset[str] = frozenset({"chat_23310", "chat_20706"})
|
||||
|
||||
|
||||
async def fetch_models_antigravity(
|
||||
ctx: Any,
|
||||
timeout_seconds: float,
|
||||
) -> tuple[list[dict], list[str], bool, dict[str, Any] | None]:
|
||||
"""Antigravity 模型获取策略。
|
||||
|
||||
调用 v1internal:fetchAvailableModels 获取可用模型列表,
|
||||
解析配额信息,过滤黑名单模型。
|
||||
"""
|
||||
from src.services.provider.adapters.antigravity.client import (
|
||||
AntigravityAccountForbiddenException,
|
||||
fetch_available_models,
|
||||
)
|
||||
|
||||
auth_config = ctx.auth_config or {}
|
||||
project_id = auth_config.get("project_id")
|
||||
if not isinstance(project_id, str) or not project_id.strip():
|
||||
return [], ["antigravity: missing auth_config.project_id (please re-auth)"], False, None
|
||||
|
||||
try:
|
||||
data = await fetch_available_models(
|
||||
ctx.api_key_value,
|
||||
project_id=project_id.strip(),
|
||||
proxy_config=ctx.proxy_config,
|
||||
timeout_seconds=timeout_seconds,
|
||||
)
|
||||
except AntigravityAccountForbiddenException:
|
||||
# 重新抛出,由上层(keys.py)处理自动停用逻辑
|
||||
raise
|
||||
except Exception as e:
|
||||
return [], [f"antigravity: fetchAvailableModels error: {e}"], False, None
|
||||
|
||||
raw_models = data.get("models")
|
||||
if not isinstance(raw_models, dict):
|
||||
return [], ["antigravity: invalid response (missing models)"], False, None
|
||||
|
||||
models: list[dict] = []
|
||||
quota_by_model: dict[str, dict[str, Any]] = {}
|
||||
|
||||
for model_id, model_data in raw_models.items():
|
||||
if not isinstance(model_id, str) or not model_id.strip():
|
||||
continue
|
||||
if model_id.strip() in BLOCKED_MODELS:
|
||||
continue
|
||||
if not isinstance(model_data, dict):
|
||||
model_data = {}
|
||||
|
||||
display_name = model_data.get("displayName")
|
||||
if not isinstance(display_name, str) or not display_name:
|
||||
display_name = model_id
|
||||
|
||||
models.append(
|
||||
{
|
||||
"id": model_id,
|
||||
"owned_by": "antigravity",
|
||||
"display_name": display_name,
|
||||
"api_format": "gemini:chat",
|
||||
}
|
||||
)
|
||||
|
||||
quota_info = model_data.get("quotaInfo")
|
||||
if not isinstance(quota_info, dict):
|
||||
# 没有 quotaInfo 视为配额耗尽
|
||||
quota_by_model[model_id] = {
|
||||
"remaining_fraction": 0.0,
|
||||
"used_percent": 100.0,
|
||||
}
|
||||
continue
|
||||
|
||||
remaining = quota_info.get("remainingFraction")
|
||||
reset_time = quota_info.get("resetTime")
|
||||
|
||||
remaining_fraction: float | None = None
|
||||
try:
|
||||
if remaining is not None:
|
||||
remaining_fraction = float(remaining)
|
||||
except Exception:
|
||||
remaining_fraction = None
|
||||
|
||||
if remaining_fraction is None:
|
||||
# remainingFraction 缺失视为配额耗尽
|
||||
payload: dict[str, Any] = {
|
||||
"remaining_fraction": 0.0,
|
||||
"used_percent": 100.0,
|
||||
}
|
||||
if isinstance(reset_time, str) and reset_time.strip():
|
||||
payload["reset_time"] = reset_time.strip()
|
||||
quota_by_model[model_id] = payload
|
||||
continue
|
||||
|
||||
used_percent = (1.0 - remaining_fraction) * 100.0
|
||||
if used_percent < 0:
|
||||
used_percent = 0.0
|
||||
if used_percent > 100: # noqa: PLR2004
|
||||
used_percent = 100.0
|
||||
|
||||
payload: dict[str, Any] = {
|
||||
"remaining_fraction": remaining_fraction,
|
||||
"used_percent": used_percent,
|
||||
}
|
||||
if isinstance(reset_time, str) and reset_time.strip():
|
||||
payload["reset_time"] = reset_time.strip()
|
||||
quota_by_model[model_id] = payload
|
||||
|
||||
upstream_metadata: dict[str, Any] | None = None
|
||||
if quota_by_model:
|
||||
upstream_metadata = {
|
||||
"antigravity": {
|
||||
"updated_at": int(time.time()),
|
||||
"quota_by_model": quota_by_model,
|
||||
}
|
||||
}
|
||||
|
||||
return models, [], True, upstream_metadata
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Export builder
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_AG_SKIP_KEYS = frozenset(
|
||||
{
|
||||
"access_token",
|
||||
"expires_at",
|
||||
"updated_at",
|
||||
"token_type",
|
||||
"scope",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def antigravity_export_builder(
|
||||
auth_config: dict[str, Any],
|
||||
upstream_metadata: dict[str, Any] | None,
|
||||
) -> dict[str, Any]:
|
||||
"""Antigravity 导出:保留 refresh_token / email / project_id / tier。"""
|
||||
return {
|
||||
k: v for k, v in auth_config.items() if k not in _AG_SKIP_KEYS and v is not None and v != ""
|
||||
}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Unified Registration
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def register_all() -> None:
|
||||
"""一次性注册 Antigravity 的所有 hooks 到各通用 registry。"""
|
||||
from src.core.api_format.capabilities import register_provider_behavior_variant
|
||||
from src.core.provider_oauth_utils import register_auth_enricher
|
||||
from src.services.model.upstream_fetcher import UpstreamModelsFetcherRegistry
|
||||
from src.services.provider.adapters.antigravity.envelope import antigravity_v1internal_envelope
|
||||
from src.services.provider.envelope import register_envelope
|
||||
from src.services.provider.export import register_export_builder
|
||||
from src.services.provider.transport import register_transport_hook
|
||||
|
||||
# Envelope
|
||||
register_envelope("antigravity", "gemini:chat", antigravity_v1internal_envelope)
|
||||
# Backward compat: allow existing endpoints that still use the old signature.
|
||||
register_envelope("antigravity", "gemini:cli", antigravity_v1internal_envelope)
|
||||
register_envelope("antigravity", "", antigravity_v1internal_envelope)
|
||||
|
||||
# Transport
|
||||
register_transport_hook("antigravity", "gemini:chat", build_antigravity_url)
|
||||
# Backward compat: allow existing endpoints that still use the old signature.
|
||||
register_transport_hook("antigravity", "gemini:cli", build_antigravity_url)
|
||||
|
||||
# Auth
|
||||
register_auth_enricher("antigravity", enrich_antigravity)
|
||||
|
||||
# Export
|
||||
register_export_builder("antigravity", antigravity_export_builder)
|
||||
|
||||
# Model Fetcher
|
||||
UpstreamModelsFetcherRegistry.register(
|
||||
provider_types=["antigravity"],
|
||||
fetcher=fetch_models_antigravity,
|
||||
)
|
||||
|
||||
# Provider Format Capability
|
||||
register_provider_behavior_variant("antigravity", cross_format=True)
|
||||
Reference in New Issue
Block a user