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,5 @@
"""GeminiCLI provider adapter package."""
from .plugin import register_all
__all__ = ["register_all"]

View File

@@ -0,0 +1,221 @@
"""GeminiCLI upstream client helpers."""
from __future__ import annotations
import asyncio
from typing import Any
from src.clients.http_client import HTTPClientPool
from src.core.logger import logger
from src.services.provider.adapters.gemini_cli.constants import (
PROD_BASE_URL,
get_v1internal_extra_headers,
)
from src.services.provider.adapters.gemini_cli.rust_http import (
execute_gemini_cli_rust_http_request,
)
_CODE_ASSIST_METADATA = {
"ideType": "ANTIGRAVITY",
"platform": "PLATFORM_UNSPECIFIED",
"pluginType": "GEMINI",
}
def _extract_tier_raw(tier_obj: Any) -> str:
"""Extract raw tier string from loadCodeAssist response objects."""
if isinstance(tier_obj, str) and tier_obj.strip():
return tier_obj.strip()
if isinstance(tier_obj, dict):
for key in ("id", "tierType"):
value = tier_obj.get(key)
if isinstance(value, str) and value.strip():
return value.strip()
return ""
def extract_plan_type(data: dict[str, Any]) -> str | None:
"""Best-effort normalized plan type for GeminiCLI OAuth accounts."""
from src.core.oauth_plan import normalize_oauth_plan_type
for key in ("paidTier", "currentTier"):
raw = _extract_tier_raw(data.get(key))
normalized = normalize_oauth_plan_type(raw)
if normalized:
return normalized
return None
def extract_project_id(data: dict[str, Any]) -> str:
"""Extract project_id from loadCodeAssist/onboardUser responses."""
raw = data.get("cloudaicompanionProject")
if isinstance(raw, str) and raw.strip():
return raw.strip()
if isinstance(raw, dict):
project_id = raw.get("id", "")
if isinstance(project_id, str) and project_id.strip():
return project_id.strip()
return ""
def extract_tier_id(data: dict[str, Any]) -> str:
"""Choose a tier ID for onboarding when the account is not activated."""
allowed_tiers = data.get("allowedTiers")
if not isinstance(allowed_tiers, list):
return ""
for tier in allowed_tiers:
if isinstance(tier, dict) and tier.get("isDefault") is True:
tier_id = tier.get("id", "")
if isinstance(tier_id, str) and tier_id.strip():
return tier_id.strip()
for tier in allowed_tiers:
if isinstance(tier, dict):
tier_id = tier.get("id", "")
if isinstance(tier_id, str) and tier_id.strip():
return tier_id.strip()
return ""
async def load_code_assist(
access_token: str,
proxy_config: dict[str, Any] | None = None,
*,
timeout_seconds: float = 10.0,
) -> dict[str, Any]:
"""Load GeminiCLI account metadata from v1internal:loadCodeAssist."""
if not access_token:
raise ValueError("missing access_token")
headers = {
"Authorization": f"Bearer {access_token}",
"Content-Type": "application/json",
**get_v1internal_extra_headers(),
}
url = f"{PROD_BASE_URL.rstrip('/')}/v1internal:loadCodeAssist"
body = {"metadata": _CODE_ASSIST_METADATA}
resp = await execute_gemini_cli_rust_http_request(
method="POST",
url=url,
headers=headers,
body=body,
proxy_config=proxy_config,
request_id="gemini-cli:load-code-assist",
provider_api_format="gemini_cli:load_code_assist",
timeout_seconds=timeout_seconds,
content_type="application/json",
)
if resp is None:
client = await HTTPClientPool.get_proxy_client(proxy_config)
resp = await client.post(
url,
json=body,
headers=headers,
timeout=timeout_seconds,
)
if 200 <= resp.status_code < 300:
data = resp.json()
return data if isinstance(data, dict) else {}
raise RuntimeError(
f"loadCodeAssist failed: status={resp.status_code} body={resp.text[:200] if resp.text else ''}"
)
async def onboard_user(
access_token: str,
*,
tier_id: str,
proxy_config: dict[str, Any] | None = None,
timeout_seconds: float = 30.0,
max_attempts: int = 5,
poll_interval: float = 2.0,
) -> str:
"""Activate GeminiCLI user and fetch project_id via v1internal:onboardUser."""
if not access_token:
raise ValueError("missing access_token")
if not tier_id:
raise ValueError("missing tier_id")
headers = {
"Authorization": f"Bearer {access_token}",
"Content-Type": "application/json",
**get_v1internal_extra_headers(),
}
body = {
"tierId": tier_id,
"metadata": _CODE_ASSIST_METADATA,
}
url = f"{PROD_BASE_URL.rstrip('/')}/v1internal:onboardUser"
for attempt in range(1, max_attempts + 1):
resp = await execute_gemini_cli_rust_http_request(
method="POST",
url=url,
headers=headers,
body=body,
proxy_config=proxy_config,
request_id=f"gemini-cli:onboard-user:{tier_id}:{attempt}",
provider_api_format="gemini_cli:onboard_user",
timeout_seconds=timeout_seconds,
content_type="application/json",
)
if resp is None:
client = await HTTPClientPool.get_proxy_client(proxy_config)
resp = await client.post(url, json=body, headers=headers, timeout=timeout_seconds)
if not (200 <= resp.status_code < 300):
raise RuntimeError(
f"onboardUser failed: status={resp.status_code} body={resp.text[:200] if resp.text else ''}"
)
data = resp.json()
if not isinstance(data, dict):
raise RuntimeError(f"onboardUser: unexpected response type: {type(data)}")
if data.get("done") is True:
response_data = data.get("response")
if isinstance(response_data, dict):
return extract_project_id(response_data)
return ""
if attempt < max_attempts:
await asyncio.sleep(poll_interval)
raise RuntimeError(f"onboardUser: timeout after {max_attempts} attempts")
async def enrich_project_id(
access_token: str,
proxy_config: dict[str, Any] | None = None,
) -> str | None:
"""Best-effort project_id resolution for GeminiCLI OAuth keys."""
code_assist = await load_code_assist(access_token, proxy_config=proxy_config)
project_id = extract_project_id(code_assist)
if project_id:
return project_id
tier_id = extract_tier_id(code_assist)
if tier_id:
try:
project_id = await onboard_user(
access_token,
tier_id=tier_id,
proxy_config=proxy_config,
)
if project_id:
return project_id
except Exception as exc:
logger.warning("GeminiCLI onboardUser failed: {}", exc)
return None
__all__ = [
"enrich_project_id",
"extract_plan_type",
"extract_project_id",
"extract_tier_id",
"load_code_assist",
"onboard_user",
]

View File

@@ -0,0 +1,25 @@
"""GeminiCLI provider constants."""
from __future__ import annotations
from src.config.settings import config
from src.core.provider_templates.fixed_providers import FIXED_PROVIDERS
from src.core.provider_templates.types import ProviderType
PROD_BASE_URL = FIXED_PROVIDERS[ProviderType.GEMINI_CLI].api_base_url
V1INTERNAL_PATH_TEMPLATE = "/v1internal:{action}"
def get_v1internal_extra_headers() -> dict[str, str]:
"""Headers required by GeminiCLI upstream requests."""
return {
"Accept-Encoding": "identity",
"User-Agent": config.internal_user_agent_gemini_cli,
}
__all__ = [
"PROD_BASE_URL",
"V1INTERNAL_PATH_TEMPLATE",
"get_v1internal_extra_headers",
]

View File

@@ -0,0 +1,96 @@
"""GeminiCLI v1internal request envelope."""
from __future__ import annotations
from typing import Any
from src.services.provider.adapters.gemini_cli.constants import get_v1internal_extra_headers
from src.services.provider.request_context import get_selected_base_url
def wrap_v1internal_request(
gemini_request: dict[str, Any],
*,
project_id: str,
model: str,
) -> dict[str, Any]:
"""Wrap a Gemini request into GeminiCLI v1internal format."""
inner_request = dict(gemini_request)
inner_request.pop("model", None)
inner_request.pop("stream", None)
return {
"model": model,
"project": project_id,
"request": inner_request,
}
class GeminiCliV1InternalEnvelope:
name = "gemini_cli:v1internal"
def extra_headers(self) -> dict[str, str] | None:
return get_v1internal_extra_headers()
def wrap_request(
self,
request_body: dict[str, Any],
*,
model: str,
url_model: str | None,
decrypted_auth_config: dict[str, Any] | None,
) -> tuple[dict[str, Any], str | None]:
_ = url_model
project_id = (decrypted_auth_config or {}).get("project_id")
if not isinstance(project_id, str) or not project_id:
from src.core.exceptions import ProviderNotAvailableException
raise ProviderNotAvailableException(
"GeminiCLI OAuth 配置缺少 project_id请重新授权",
provider_name="gemini_cli",
upstream_response="missing auth_config.project_id",
)
wrapped = wrap_v1internal_request(
request_body,
project_id=project_id,
model=model,
)
return wrapped, None
def unwrap_response(self, data: Any) -> Any:
if not isinstance(data, dict):
return data
response_obj = data.get("response")
if "candidates" not in data and isinstance(response_obj, dict):
return response_obj
return data
def postprocess_unwrapped_response(self, *, model: str, data: Any) -> None:
_ = model, data
return
def capture_selected_base_url(self) -> str | None:
return get_selected_base_url()
def on_http_status(self, *, base_url: str | None, status_code: int) -> None:
_ = base_url, status_code
return
def on_connection_error(self, *, base_url: str | None, exc: Exception) -> None:
_ = base_url, exc
return
def force_stream_rewrite(self) -> bool:
return False
gemini_cli_v1internal_envelope = GeminiCliV1InternalEnvelope()
__all__ = [
"GeminiCliV1InternalEnvelope",
"gemini_cli_v1internal_envelope",
"wrap_v1internal_request",
]

View File

@@ -0,0 +1,158 @@
"""GeminiCLI provider plugin — unified registration entry."""
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.gemini_cli.constants import V1INTERNAL_PATH_TEMPLATE
from src.services.provider.preset_models import get_preset_models
from src.services.provider.request_context import set_selected_base_url
async def fetch_models_gemini_cli(
ctx: Any,
timeout_seconds: float,
) -> tuple[list[dict], list[str], bool, dict[str, Any] | None]:
"""GeminiCLI model fetcher.
Upstream does not expose a stable public models endpoint for OAuth CLI access,
so we currently return a curated preset model catalog and enrich account metadata
from loadCodeAssist when possible.
"""
from src.services.provider.adapters.gemini_cli.client import (
extract_plan_type,
load_code_assist,
)
models = get_preset_models("gemini_cli")
upstream_metadata: dict[str, Any] | None = None
access_token = str(getattr(ctx, "api_key_value", "") or "").strip()
if access_token:
try:
code_assist = await load_code_assist(
access_token,
proxy_config=getattr(ctx, "proxy_config", None),
timeout_seconds=timeout_seconds,
)
provider_meta: dict[str, Any] = {"updated_at": int(time.time())}
plan_type = extract_plan_type(code_assist)
if plan_type:
provider_meta["plan_type"] = plan_type
project_id = (getattr(ctx, "auth_config", None) or {}).get("project_id")
if isinstance(project_id, str) and project_id:
provider_meta["project_id"] = project_id
upstream_metadata = {"gemini_cli": provider_meta}
except Exception as exc:
logger.debug("GeminiCLI model metadata fetch failed: {}", exc)
return models, [], True, upstream_metadata
def build_gemini_cli_url(
endpoint: Any,
*,
is_stream: bool,
effective_query_params: dict[str, Any],
**_kwargs: Any,
) -> str:
"""Build GeminiCLI v1internal URL."""
base_url = str(getattr(endpoint, "base_url", "") or "").rstrip("/")
set_selected_base_url(base_url)
action = "streamGenerateContent" if is_stream else "generateContent"
path = V1INTERNAL_PATH_TEMPLATE.format(action=action)
url = f"{base_url}{path}"
if is_stream:
effective_query_params.setdefault("alt", "sse")
if effective_query_params:
query_string = urlencode(effective_query_params, doseq=True)
if query_string:
url = f"{url}?{query_string}"
return url
async def enrich_gemini_cli(
auth_config: dict[str, Any],
token_response: dict[str, Any],
access_token: str,
proxy_config: dict[str, Any] | None,
) -> dict[str, Any]:
"""GeminiCLI auth_config enrichment: email + project_id."""
from src.core.provider_oauth_utils import fetch_google_email
from src.services.provider.adapters.gemini_cli.client import (
enrich_project_id,
extract_plan_type,
load_code_assist,
)
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
try:
code_assist = await load_code_assist(access_token, proxy_config=proxy_config)
except Exception as exc:
code_assist = None
logger.warning("[enrich] GeminiCLI loadCodeAssist failed: {}", exc)
if code_assist and not auth_config.get("tier"):
plan_type = extract_plan_type(code_assist)
if plan_type:
auth_config["tier"] = plan_type
if not auth_config.get("project_id"):
try:
project_id = (code_assist and code_assist.get("cloudaicompanionProject")) or None
if isinstance(project_id, dict):
project_id = project_id.get("id")
if isinstance(project_id, str) and project_id.strip():
auth_config["project_id"] = project_id.strip()
else:
project_id = await enrich_project_id(access_token, proxy_config=proxy_config)
if project_id:
auth_config["project_id"] = project_id
logger.info("[enrich] GeminiCLI project_id: {}", project_id[:8] + "...")
except Exception as exc:
logger.warning("[enrich] GeminiCLI project_id enrichment failed: {}", exc)
return auth_config
def register_all() -> None:
"""Register all GeminiCLI hooks into shared registries."""
from src.core.provider_oauth_utils import register_auth_enricher
from src.services.model.upstream_fetcher import UpstreamModelsFetcherRegistry
from src.services.provider.adapters.gemini_cli.envelope import gemini_cli_v1internal_envelope
from src.services.provider.envelope import register_envelope
from src.services.provider.transport import register_transport_hook
register_envelope("gemini_cli", "gemini:cli", gemini_cli_v1internal_envelope)
register_envelope("gemini_cli", "gemini:chat", gemini_cli_v1internal_envelope)
register_envelope("gemini_cli", "", gemini_cli_v1internal_envelope)
register_transport_hook("gemini_cli", "gemini:cli", build_gemini_cli_url)
register_transport_hook("gemini_cli", "gemini:chat", build_gemini_cli_url)
register_auth_enricher("gemini_cli", enrich_gemini_cli)
UpstreamModelsFetcherRegistry.register(
provider_types=["gemini_cli"],
fetcher=fetch_models_gemini_cli,
)
__all__ = [
"build_gemini_cli_url",
"enrich_gemini_cli",
"fetch_models_gemini_cli",
"register_all",
]

View File

@@ -0,0 +1,275 @@
"""Gemini CLI quota / RESOURCE_EXHAUSTED helpers."""
from __future__ import annotations
import json
import re
import time
from datetime import datetime, timezone
from typing import Any
from uuid import UUID
_DURATION_TOKEN_RE = re.compile(r"(\d+(?:\.\d+)?)([dhms])")
_RESET_AFTER_RE = re.compile(r"reset after\s+([^.,;]+)", re.IGNORECASE)
def _parse_json(error_text: str | None) -> dict[str, Any] | None:
if not isinstance(error_text, str) or not error_text.strip():
return None
try:
data = json.loads(error_text)
except Exception:
return None
return data if isinstance(data, dict) else None
def _parse_duration_seconds(raw: Any) -> int | None:
if isinstance(raw, (int, float)):
return max(1, int(raw))
if not isinstance(raw, str):
return None
text = raw.strip().lower()
if not text:
return None
total_seconds = 0.0
matched = False
for amount_text, unit in _DURATION_TOKEN_RE.findall(text):
matched = True
amount = float(amount_text)
if unit == "d":
total_seconds += amount * 86400
elif unit == "h":
total_seconds += amount * 3600
elif unit == "m":
total_seconds += amount * 60
elif unit == "s":
total_seconds += amount
if not matched:
return None
return max(1, int(total_seconds))
def _iter_error_details(payload: dict[str, Any]) -> list[dict[str, Any]]:
error_obj = payload.get("error")
if not isinstance(error_obj, dict):
return []
details = error_obj.get("details")
if not isinstance(details, list):
return []
return [item for item in details if isinstance(item, dict)]
def _error_status(payload: dict[str, Any]) -> str:
error_obj = payload.get("error")
if not isinstance(error_obj, dict):
return ""
status = error_obj.get("status")
return status.strip() if isinstance(status, str) else ""
def _error_message(payload: dict[str, Any]) -> str:
error_obj = payload.get("error")
if not isinstance(error_obj, dict):
return ""
message = error_obj.get("message")
return message.strip() if isinstance(message, str) else ""
def _error_reason(payload: dict[str, Any]) -> str:
for detail in _iter_error_details(payload):
reason = detail.get("reason")
if isinstance(reason, str) and reason.strip():
return reason.strip()
return ""
def _looks_like_uuid(value: str | None) -> bool:
if not isinstance(value, str):
return False
text = value.strip()
if not text:
return False
try:
UUID(text)
except Exception:
return False
return True
def is_resource_exhausted_error(error_text: str | None) -> bool:
payload = _parse_json(error_text)
if payload is None:
return False
status = _error_status(payload).upper()
reason = _error_reason(payload).upper()
if status == "RESOURCE_EXHAUSTED" or reason == "QUOTA_EXHAUSTED":
return True
message = _error_message(payload).lower()
return ("exhausted your capacity" in message) or ("quota" in message and "exhaust" in message)
def parse_quota_reset_timestamp(error_text: str | None) -> int | None:
payload = _parse_json(error_text)
if payload is None:
return None
for detail in _iter_error_details(payload):
metadata = detail.get("metadata")
if not isinstance(metadata, dict):
continue
raw = metadata.get("quotaResetTimeStamp") or metadata.get("quotaResetTimestamp")
if not isinstance(raw, str) or not raw.strip():
continue
text = raw.strip()
try:
if text.endswith("Z"):
text = text[:-1] + "+00:00"
parsed = datetime.fromisoformat(text)
if parsed.tzinfo is None:
parsed = parsed.replace(tzinfo=timezone.utc)
return int(parsed.timestamp())
except Exception:
continue
return None
def parse_quota_reset_delay_seconds(error_text: str | None) -> int | None:
payload = _parse_json(error_text)
if payload is None:
return None
for detail in _iter_error_details(payload):
metadata = detail.get("metadata")
if not isinstance(metadata, dict):
continue
delay = metadata.get("quotaResetDelay")
parsed = _parse_duration_seconds(delay)
if parsed is not None:
return parsed
return None
def parse_quota_reset_message_seconds(error_text: str | None) -> int | None:
payload = _parse_json(error_text)
if payload is None:
return None
message = _error_message(payload)
if not message:
return None
matched = _RESET_AFTER_RE.search(message)
if not matched:
return None
return _parse_duration_seconds(matched.group(1))
def extract_error_model_name(error_text: str | None, *, fallback: str | None = None) -> str | None:
payload = _parse_json(error_text)
if payload is not None:
for detail in _iter_error_details(payload):
metadata = detail.get("metadata")
if not isinstance(metadata, dict):
continue
model = metadata.get("model")
if isinstance(model, str) and model.strip():
return model.strip()
fallback_text = str(fallback or "").strip()
if fallback_text and not _looks_like_uuid(fallback_text):
return fallback_text
return None
def extract_quota_cooldown_seconds(
error_text: str | None, *, now_ts: int | None = None
) -> int | None:
now = int(now_ts or time.time())
reset_at = parse_quota_reset_timestamp(error_text)
if reset_at is not None:
return max(1, reset_at - now)
delay = parse_quota_reset_delay_seconds(error_text)
if delay is not None:
return max(1, delay)
message_delay = parse_quota_reset_message_seconds(error_text)
if message_delay is not None:
return max(1, message_delay)
return None
def build_quota_exhausted_metadata(
*,
model_name: str,
error_text: str | None,
current_namespace: dict[str, Any] | None = None,
now_ts: int | None = None,
) -> dict[str, Any] | None:
normalized_model = str(model_name or "").strip()
if not normalized_model:
return None
if not is_resource_exhausted_error(error_text):
return None
now = int(now_ts or time.time())
reset_at = parse_quota_reset_timestamp(error_text)
if reset_at is None:
delay = parse_quota_reset_delay_seconds(error_text)
if delay is not None:
reset_at = now + delay
if reset_at is None:
message_delay = parse_quota_reset_message_seconds(error_text)
if message_delay is not None:
reset_at = now + message_delay
if reset_at is None:
return None
payload = _parse_json(error_text) or {}
namespace = dict(current_namespace) if isinstance(current_namespace, dict) else {}
quota_by_model_raw = namespace.get("quota_by_model")
quota_by_model = dict(quota_by_model_raw) if isinstance(quota_by_model_raw, dict) else {}
model_entry_raw = quota_by_model.get(normalized_model)
model_entry = dict(model_entry_raw) if isinstance(model_entry_raw, dict) else {}
model_entry["is_exhausted"] = True
model_entry["remaining_fraction"] = 0.0
model_entry["used_percent"] = 100.0
model_entry["updated_at"] = now
model_entry["reset_at"] = reset_at
model_entry["reset_time"] = datetime.fromtimestamp(reset_at, timezone.utc).isoformat()
model_entry["reset_seconds"] = max(0, reset_at - now)
reason = _error_reason(payload) or _error_status(payload) or _error_message(payload)
if reason:
model_entry["reason"] = reason
quota_by_model[normalized_model] = model_entry
namespace["quota_by_model"] = quota_by_model
namespace["updated_at"] = now
status = _error_status(payload)
if status:
namespace["last_error_status"] = status
if reason:
namespace["last_error_reason"] = reason
return {"gemini_cli": namespace}
__all__ = [
"extract_error_model_name",
"build_quota_exhausted_metadata",
"extract_quota_cooldown_seconds",
"is_resource_exhausted_error",
"parse_quota_reset_message_seconds",
"parse_quota_reset_delay_seconds",
"parse_quota_reset_timestamp",
]

View File

@@ -0,0 +1,108 @@
"""Shared Rust executor HTTP helper for Gemini CLI side calls."""
from __future__ import annotations
import json
from typing import Any
import httpx
from src.config.settings import config
from src.core.logger import logger
async def execute_gemini_cli_rust_http_request(
*,
method: str,
url: str,
headers: dict[str, str],
body: Any,
proxy_config: dict[str, Any] | None,
request_id: str,
provider_api_format: str,
timeout_seconds: float,
content_type: str | None = None,
) -> httpx.Response | None:
from src.services.request.execution_runtime_plan import (
ExecutionPlan,
ExecutionPlanBody,
ExecutionPlanTimeouts,
build_execution_plan_body,
build_proxy_snapshot,
)
from src.services.request.execution_runtime_client import (
ExecutionRuntimeClient,
ExecutionRuntimeClientError,
)
if config.execution_runtime_backend != "rust":
return None
final_headers = dict(headers)
if (
body is not None
and content_type
and not any(str(key).lower() == "content-type" for key in final_headers)
):
final_headers["content-type"] = content_type
timeout_ms = max(int(timeout_seconds * 1000), 1_000)
try:
proxy_snapshot = await build_proxy_snapshot(proxy_config, label="GeminiCLI")
result = await ExecutionRuntimeClient().execute_sync_json(
ExecutionPlan(
request_id=request_id,
candidate_id=None,
provider_name="gemini_cli",
provider_id="",
endpoint_id="",
key_id="",
method=method,
url=url,
headers=final_headers,
body=(
build_execution_plan_body(body, content_type=content_type)
if body is not None
else ExecutionPlanBody()
),
stream=False,
provider_api_format=provider_api_format,
client_api_format=provider_api_format,
model_name="gemini_cli",
content_type=content_type,
proxy=proxy_snapshot,
timeouts=ExecutionPlanTimeouts(
connect_ms=timeout_ms,
read_ms=timeout_ms,
write_ms=timeout_ms,
pool_ms=timeout_ms,
total_ms=timeout_ms,
),
)
)
except (ExecutionRuntimeClientError, httpx.HTTPError, json.JSONDecodeError) as exc:
logger.warning("GeminiCLI Rust HTTP fallback {} {}: {}", method, url, exc)
return None
except Exception as exc:
logger.warning("GeminiCLI Rust HTTP unexpected fallback {} {}: {}", method, url, exc)
return None
response_headers = dict(result.headers)
if result.response_json is not None:
response_headers.setdefault("content-type", "application/json")
response_body = json.dumps(result.response_json, ensure_ascii=False).encode("utf-8")
elif result.response_body_bytes is not None:
response_body = result.response_body_bytes
else:
response_body = b""
return httpx.Response(
status_code=result.status_code,
request=httpx.Request(method, url, headers=final_headers),
headers=response_headers,
content=response_body,
)
__all__ = ["execute_gemini_cli_rust_http_request"]