Files
Aether/_deprecated_py_src/services/provider/adapters/antigravity/plugin.py
fawney19 1d9c77522a 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)
2026-04-03 16:26:16 +08:00

389 lines
13 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.
"""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优先 paidTierfallback 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优先 paidTierfallback 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)