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,19 @@
"""Internal routers still surfaced by the Python host."""
from fastapi import APIRouter
def _build_python_internal_router() -> APIRouter:
"""Internal APIs that still belong to the Python host/runtime."""
return APIRouter()
# Legacy internal gateway bridge 与 internal tunnel 模块仍保留在 `src.api.internal.*`
# 里给测试与过渡逻辑复用,但 Python host 已不再公开任何 `/api/internal/*` 路由。
python_internal_router = _build_python_internal_router()
router = python_internal_router
__all__ = [
"python_internal_router",
"router",
]

View File

@@ -0,0 +1,14 @@
from __future__ import annotations
import ipaddress
from fastapi import HTTPException, Request
def ensure_loopback(request: Request) -> None:
host = request.client.host if request.client else ""
try:
if not ipaddress.ip_address(host).is_loopback:
raise ValueError(host)
except ValueError as exc:
raise HTTPException(status_code=403, detail="loopback access only") from exc

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,43 @@
"""Compatibility re-export layer for gateway chat builders."""
from __future__ import annotations
from .gateway_chat_decision import (
_build_chat_stream_decision,
_build_chat_sync_decision,
_build_claude_chat_stream_decision,
_build_claude_chat_sync_decision,
_build_gemini_chat_stream_decision,
_build_gemini_chat_sync_decision,
_build_openai_chat_stream_decision,
_build_openai_chat_sync_decision,
)
from .gateway_chat_plan import (
_build_chat_stream_plan,
_build_chat_sync_plan,
_build_claude_chat_stream_plan,
_build_claude_chat_sync_plan,
_build_gemini_chat_stream_plan,
_build_gemini_chat_sync_plan,
_build_openai_chat_stream_plan,
_build_openai_chat_sync_plan,
)
__all__ = [
"_build_chat_sync_decision",
"_build_openai_chat_sync_decision",
"_build_chat_stream_decision",
"_build_openai_chat_stream_decision",
"_build_claude_chat_stream_decision",
"_build_gemini_chat_stream_decision",
"_build_claude_chat_sync_decision",
"_build_gemini_chat_sync_decision",
"_build_chat_sync_plan",
"_build_openai_chat_sync_plan",
"_build_chat_stream_plan",
"_build_openai_chat_stream_plan",
"_build_claude_chat_stream_plan",
"_build_gemini_chat_stream_plan",
"_build_claude_chat_sync_plan",
"_build_gemini_chat_sync_plan",
]

View File

@@ -0,0 +1,945 @@
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_chat_sync_decision(
*,
request: Request,
payload: GatewayExecuteRequest,
db: Session,
auth_context: GatewayAuthContext,
decision: GatewayRouteDecision,
expected_api_format: str,
decision_kind: str,
report_kind: str,
finalize_kind: str,
) -> GatewayExecutionDecisionResponse | None:
from src.api.handlers.base.chat_adapter_base import ChatAdapterBase
from src.api.handlers.base.chat_sync_executor import ChatSyncExecutor
from src.services.task.request_state import MutableRequestBodyState
if str(payload.method or "").strip().upper() != "POST":
return None
adapter, path_params = gateway_module._resolve_gateway_sync_adapter(decision, payload.path)
if not isinstance(adapter, ChatAdapterBase):
return None
if gateway_module._is_stream_request_payload(payload.body_json, path_params):
return None
user, api_key = gateway_module._load_gateway_auth_models(db, auth_context)
pipeline = gateway_module.get_pipeline()
await pipeline._check_user_rate_limit(request, db, user, api_key)
context = gateway_module._build_gateway_request_context(
request=request,
payload=payload,
db=db,
user=user,
api_key=api_key,
adapter=adapter,
path_params=path_params,
balance_remaining=auth_context.balance_remaining,
)
authorize_result = adapter.authorize(context)
if hasattr(authorize_result, "__await__"):
await authorize_result
original_request_body = await context.ensure_json_body_async()
if context.path_params:
original_request_body = adapter._merge_path_params(
original_request_body,
context.path_params,
)
request_obj = adapter._validate_request_body(original_request_body, context.path_params)
if isinstance(request_obj, JSONResponse):
return None
handler = adapter._create_handler(
db=db,
user=user,
api_key=api_key,
request_id=context.request_id,
client_ip=context.client_ip,
user_agent=context.user_agent,
start_time=context.start_time,
api_family=adapter.API_FAMILY.value if adapter.API_FAMILY else None,
endpoint_kind=adapter.ENDPOINT_KIND.value if adapter.ENDPOINT_KIND else None,
)
await handler._convert_request(request_obj)
model = handler.extract_model_from_request(
original_request_body,
context.path_params,
)
api_format = handler.allowed_api_formats[0]
capability_requirements = handler._resolve_capability_requirements(
model_name=model,
request_headers=context.original_headers,
request_body=original_request_body,
)
preferred_key_ids = await handler._resolve_preferred_key_ids(
model_name=model,
request_body=original_request_body,
)
candidate = await gateway_module._select_gateway_direct_candidate(
db=db,
redis_client=getattr(handler, "redis", None),
api_format=str(api_format),
model_name=str(model or "unknown"),
user_api_key=api_key,
request_id=context.request_id,
is_stream=False,
capability_requirements=capability_requirements or None,
preferred_key_ids=preferred_key_ids or None,
request_body=original_request_body,
)
if candidate is None:
return None
sync_executor = ChatSyncExecutor(handler)
prepared_plan = await sync_executor._build_sync_execution_plan(
candidate.provider,
candidate.endpoint,
candidate.key,
candidate,
model=str(model or "unknown"),
api_format=api_format,
original_headers=context.original_headers,
request_state=MutableRequestBodyState(original_request_body),
query_params=context.query_params,
client_content_encoding=context.client_content_encoding,
)
contract = prepared_plan.contract
contract_provider_api_format = str(contract.provider_api_format or "").strip().lower()
contract_client_api_format = str(contract.client_api_format or "").strip().lower()
if not prepared_plan.remote_eligible or contract_client_api_format != expected_api_format:
return None
provider_request_headers = {
str(header_name).strip().lower(): str(header_value).strip()
for header_name, header_value in dict(prepared_plan.headers or {}).items()
if str(header_name).strip() and str(header_value).strip()
}
provider_request_body = dict(prepared_plan.payload or {})
auth_header, auth_value = gateway_module._extract_gateway_upstream_auth(
provider_request_headers,
provider_api_format=contract_provider_api_format,
key=candidate.key,
)
prompt_cache_key = str(provider_request_body.get("prompt_cache_key") or "").strip() or None
mapped_model = getattr(sync_executor._ctx, "mapped_model_result", None)
has_envelope = prepared_plan.envelope is not None
needs_conversion = bool(prepared_plan.needs_conversion)
selected_report_kind = (
finalize_kind
if (
prepared_plan.upstream_is_stream
or needs_conversion
or has_envelope
or contract_provider_api_format != expected_api_format
)
else report_kind
)
decision_extra_headers: dict[str, str] = {}
decision_provider_request_headers = provider_request_headers or None
decision_provider_request_body = provider_request_body
report_context = {
"user_id": str(user.id),
"api_key_id": str(api_key.id),
"request_id": str(context.request_id),
"model": str(model or "unknown"),
"provider_name": str(contract.provider_name or "unknown"),
"provider_id": str(contract.provider_id or ""),
"endpoint_id": str(contract.endpoint_id or ""),
"key_id": str(contract.key_id or ""),
"candidate_id": str(contract.candidate_id or "") or None,
"provider_api_format": str(contract.provider_api_format or ""),
"client_api_format": str(contract.client_api_format or ""),
"mapped_model": str(mapped_model or "").strip() or None,
"original_headers": dict(context.original_headers),
"original_request_body": original_request_body,
"proxy_info": prepared_plan.proxy_info,
"has_envelope": has_envelope,
"envelope_name": gateway_module._gateway_report_context_envelope_name(
prepared_plan.envelope
),
"needs_conversion": needs_conversion,
}
report_context["provider_request_headers"] = provider_request_headers
report_context["provider_request_body"] = provider_request_body
return GatewayExecutionDecisionResponse(
action="executor_sync_decision",
decision_kind=decision_kind,
request_id=str(contract.request_id or context.request_id or ""),
candidate_id=str(contract.candidate_id or "").strip() or None,
provider_name=str(contract.provider_name or candidate.provider.name or ""),
provider_id=str(contract.provider_id or candidate.provider.id or ""),
endpoint_id=str(contract.endpoint_id or candidate.endpoint.id or ""),
key_id=str(contract.key_id or candidate.key.id or ""),
upstream_base_url=(
str(
prepared_plan.selected_base_url or getattr(candidate.endpoint, "base_url", "") or ""
).strip()
),
upstream_url=str(contract.url or "").strip() or None,
auth_header=auth_header,
auth_value=str(auth_value or "").strip(),
provider_api_format=contract_provider_api_format,
client_api_format=contract_client_api_format,
model_name=str(contract.model_name or model or "unknown"),
mapped_model=str(mapped_model or "").strip() or None,
prompt_cache_key=prompt_cache_key,
extra_headers=decision_extra_headers,
provider_request_headers=decision_provider_request_headers,
provider_request_body=decision_provider_request_body,
content_type=(
str(contract.content_type or provider_request_headers.get("content-type") or "").strip()
or "application/json"
),
proxy=gateway_module._serialize_gateway_sync_proxy(contract.proxy),
tls_profile=str(contract.tls_profile or "").strip() or None,
timeouts=gateway_module._serialize_gateway_sync_timeouts(contract.timeouts),
upstream_is_stream=prepared_plan.upstream_is_stream or None,
report_kind=selected_report_kind,
report_context=report_context,
auth_context=auth_context,
)
async def _build_openai_chat_sync_decision(
*,
request: Request,
payload: GatewayExecuteRequest,
db: Session,
auth_context: GatewayAuthContext,
decision: GatewayRouteDecision,
) -> GatewayExecutionDecisionResponse | None:
from src.api.handlers.base.chat_adapter_base import ChatAdapterBase
from src.config.settings import config
from src.services.provider.auth import get_provider_auth
from src.services.provider.behavior import get_provider_behavior
from src.services.provider.prompt_cache import maybe_patch_request_with_prompt_cache_key
from src.services.provider.stream_policy import (
get_upstream_stream_policy,
resolve_upstream_is_stream,
)
from src.services.provider.upstream_headers import build_upstream_extra_headers
from src.services.proxy_node.resolver import (
build_proxy_url_async,
get_system_proxy_config_async,
resolve_delegate_config_async,
resolve_effective_proxy,
resolve_proxy_info_async,
)
from src.services.request.execution_runtime_plan import (
ExecutionPlanTimeouts,
ExecutionProxySnapshot,
)
if str(payload.method or "").strip().upper() != "POST":
return None
adapter, path_params = gateway_module._resolve_gateway_sync_adapter(decision, payload.path)
if not isinstance(adapter, ChatAdapterBase):
return None
if gateway_module._is_stream_request_payload(payload.body_json, path_params):
return None
if not isinstance(payload.body_json, dict):
return None
user, api_key = gateway_module._load_gateway_auth_models(db, auth_context)
pipeline = gateway_module.get_pipeline()
await pipeline._check_user_rate_limit(request, db, user, api_key)
context = gateway_module._build_gateway_request_context(
request=request,
payload=payload,
db=db,
user=user,
api_key=api_key,
adapter=adapter,
path_params=path_params,
balance_remaining=auth_context.balance_remaining,
)
authorize_result = adapter.authorize(context)
if hasattr(authorize_result, "__await__"):
await authorize_result
original_request_body = await context.ensure_json_body_async()
if context.path_params:
original_request_body = adapter._merge_path_params(
original_request_body,
context.path_params,
)
request_obj = adapter._validate_request_body(original_request_body, context.path_params)
if isinstance(request_obj, JSONResponse):
return None
handler = adapter._create_handler(
db=db,
user=user,
api_key=api_key,
request_id=context.request_id,
client_ip=context.client_ip,
user_agent=context.user_agent,
start_time=context.start_time,
api_family=adapter.API_FAMILY.value if adapter.API_FAMILY else None,
endpoint_kind=adapter.ENDPOINT_KIND.value if adapter.ENDPOINT_KIND else None,
)
model = handler.extract_model_from_request(
original_request_body,
context.path_params,
)
client_api_format = str(handler.allowed_api_formats[0] or "").strip().lower()
if client_api_format != "openai:chat":
return None
capability_requirements = handler._resolve_capability_requirements(
model_name=model,
request_headers=context.original_headers,
request_body=original_request_body,
)
preferred_key_ids = await handler._resolve_preferred_key_ids(
model_name=model,
request_body=original_request_body,
)
candidate = await gateway_module._select_gateway_direct_candidate(
db=db,
redis_client=getattr(handler, "redis", None),
api_format=client_api_format,
model_name=str(model or "unknown"),
user_api_key=api_key,
request_id=context.request_id,
is_stream=False,
capability_requirements=capability_requirements or None,
preferred_key_ids=preferred_key_ids or None,
request_body=original_request_body,
)
if candidate is None:
return None
provider = candidate.provider
endpoint = candidate.endpoint
key = candidate.key
provider_api_format = str(endpoint.api_format or client_api_format or "").strip().lower()
if provider_api_format != "openai:chat":
return None
if getattr(endpoint, "custom_path", None):
return None
if getattr(endpoint, "header_rules", None) or getattr(endpoint, "body_rules", None):
return None
provider_type = str(getattr(provider, "provider_type", "") or "").strip().lower()
behavior = get_provider_behavior(
provider_type=provider_type,
endpoint_sig=provider_api_format,
)
if (
behavior.envelope is not None
or behavior.same_format_variant is not None
or behavior.cross_format_variant is not None
):
return None
upstream_policy = get_upstream_stream_policy(
endpoint,
provider_type=provider_type,
endpoint_sig=provider_api_format,
)
upstream_is_stream = resolve_upstream_is_stream(
client_is_stream=False,
policy=upstream_policy,
)
mapped_model = candidate.mapping_matched_model if candidate else None
if not mapped_model:
mapped_model = await handler._get_mapped_model(
source_model=model,
provider_id=str(provider.id),
api_format=provider_api_format,
)
provider_request_body = dict(original_request_body)
if mapped_model:
provider_request_body = handler.apply_mapped_model(provider_request_body, mapped_model)
if upstream_is_stream:
provider_request_body["stream"] = True
provider_request_body = maybe_patch_request_with_prompt_cache_key(
provider_request_body,
provider_api_format=provider_api_format,
provider_type=provider_type,
base_url=getattr(endpoint, "base_url", None),
user_api_key_id=str(getattr(api_key, "id", "") or ""),
request_headers=context.original_headers,
)
prompt_cache_key = str(provider_request_body.get("prompt_cache_key") or "").strip() or None
auth_info = await get_provider_auth(endpoint, key)
if auth_info is not None:
auth_header, auth_value = auth_info.as_tuple()
decrypted_auth_config = auth_info.decrypted_auth_config
else:
auth_header, auth_type = get_auth_config_for_endpoint(provider_api_format)
decrypted_key = crypto_service.decrypt(key.api_key)
auth_value = f"Bearer {decrypted_key}" if auth_type == "bearer" else decrypted_key
decrypted_auth_config = None
extra_headers = build_upstream_extra_headers(
provider_type=provider_type,
endpoint_sig=provider_api_format,
request_body=provider_request_body,
original_headers=context.original_headers,
decrypted_auth_config=decrypted_auth_config,
)
effective_proxy = resolve_effective_proxy(provider.proxy, getattr(key, "proxy", None))
proxy_info = await resolve_proxy_info_async(effective_proxy)
delegate_cfg = await resolve_delegate_config_async(effective_proxy)
is_tunnel_delegate = bool(delegate_cfg and delegate_cfg.get("tunnel"))
effective_proxy_for_contract = effective_proxy
if not effective_proxy_for_contract or not effective_proxy_for_contract.get("enabled", True):
effective_proxy_for_contract = await get_system_proxy_config_async()
proxy_url: str | None = None
if effective_proxy_for_contract and not is_tunnel_delegate:
proxy_url = await build_proxy_url_async(effective_proxy_for_contract)
proxy_snapshot = ExecutionProxySnapshot.from_proxy_info(
proxy_info,
proxy_url=proxy_url,
mode_override="tunnel" if is_tunnel_delegate else None,
node_id_override=(
str(delegate_cfg.get("node_id") or "").strip() or None if is_tunnel_delegate else None
),
)
request_timeout = provider.request_timeout or config.http_request_timeout
timeouts = ExecutionPlanTimeouts(
connect_ms=int(config.http_connect_timeout * 1000),
read_ms=int(config.http_read_timeout * 1000),
write_ms=int(config.http_write_timeout * 1000),
pool_ms=int(config.http_pool_timeout * 1000),
total_ms=int(request_timeout * 1000),
)
return GatewayExecutionDecisionResponse(
action="executor_sync_decision",
decision_kind="openai_chat_sync",
request_id=str(context.request_id),
candidate_id=str(
getattr(candidate, "request_candidate_id", "") or getattr(candidate, "id", "") or ""
)
or None,
provider_name=str(provider.name),
provider_id=str(provider.id),
endpoint_id=str(endpoint.id),
key_id=str(key.id),
upstream_base_url=str(getattr(endpoint, "base_url", "") or "").strip(),
auth_header=str(auth_header or "").strip() or "authorization",
auth_value=str(auth_value or "").strip(),
provider_api_format=provider_api_format,
client_api_format=client_api_format,
model_name=str(model or "unknown"),
mapped_model=str(mapped_model or "").strip() or None,
prompt_cache_key=prompt_cache_key,
extra_headers={str(k): str(v) for k, v in (extra_headers or {}).items()},
content_type="application/json",
proxy=gateway_module._serialize_gateway_sync_proxy(proxy_snapshot),
timeouts=gateway_module._serialize_gateway_sync_timeouts(timeouts),
upstream_is_stream=upstream_is_stream or None,
report_kind=(
"openai_chat_sync_finalize" if upstream_is_stream else "openai_chat_sync_success"
),
report_context={
"user_id": str(user.id),
"api_key_id": str(api_key.id),
"request_id": str(context.request_id),
"candidate_id": str(
getattr(candidate, "request_candidate_id", "") or getattr(candidate, "id", "") or ""
)
or None,
"model": str(model or "unknown"),
"provider_name": str(provider.name or "unknown"),
"provider_id": str(provider.id),
"endpoint_id": str(endpoint.id),
"key_id": str(key.id),
"provider_api_format": provider_api_format,
"client_api_format": client_api_format,
"mapped_model": str(mapped_model or "").strip() or None,
"original_headers": dict(context.original_headers),
"original_request_body": original_request_body,
"proxy_info": proxy_info,
"has_envelope": False,
"needs_conversion": False,
},
auth_context=auth_context,
)
async def _build_chat_stream_decision(
*,
request: Request,
payload: GatewayExecuteRequest,
db: Session,
auth_context: GatewayAuthContext,
decision: GatewayRouteDecision,
expected_api_format: str,
decision_kind: str,
report_kind: str,
) -> GatewayExecutionDecisionResponse | None:
from src.api.handlers.base.chat_adapter_base import ChatAdapterBase
from src.config.settings import config
from src.core.api_format.headers import set_accept_if_absent
from src.services.provider.auth import get_provider_auth
from src.services.provider.transport import build_provider_url
from src.services.proxy_node.resolver import (
build_proxy_url_async,
get_system_proxy_config_async,
resolve_delegate_config_async,
resolve_effective_proxy,
resolve_proxy_info_async,
)
from src.services.request.execution_runtime_plan import (
ExecutionPlan,
ExecutionPlanTimeouts,
ExecutionProxySnapshot,
build_execution_plan_body,
is_remote_execution_runtime_contract_eligible,
)
if str(payload.method or "").strip().upper() != "POST":
return None
adapter, path_params = gateway_module._resolve_gateway_sync_adapter(decision, payload.path)
if not isinstance(adapter, ChatAdapterBase):
return None
if not gateway_module._is_stream_request_payload(payload.body_json, path_params):
return None
user, api_key = gateway_module._load_gateway_auth_models(db, auth_context)
pipeline = gateway_module.get_pipeline()
await pipeline._check_user_rate_limit(request, db, user, api_key)
context = gateway_module._build_gateway_request_context(
request=request,
payload=payload,
db=db,
user=user,
api_key=api_key,
adapter=adapter,
path_params=path_params,
balance_remaining=auth_context.balance_remaining,
)
authorize_result = adapter.authorize(context)
if hasattr(authorize_result, "__await__"):
await authorize_result
original_request_body = await context.ensure_json_body_async()
if context.path_params:
original_request_body = adapter._merge_path_params(
original_request_body,
context.path_params,
)
request_obj = adapter._validate_request_body(original_request_body, context.path_params)
if isinstance(request_obj, JSONResponse):
return None
handler = adapter._create_handler(
db=db,
user=user,
api_key=api_key,
request_id=context.request_id,
client_ip=context.client_ip,
user_agent=context.user_agent,
start_time=context.start_time,
api_family=adapter.API_FAMILY.value if adapter.API_FAMILY else None,
endpoint_kind=adapter.ENDPOINT_KIND.value if adapter.ENDPOINT_KIND else None,
)
converted_request = await handler._convert_request(request_obj)
model = getattr(converted_request, "model", original_request_body.get("model", "unknown"))
api_format = str(handler.allowed_api_formats[0] or "").strip().lower()
if api_format != expected_api_format:
return None
capability_requirements = handler._resolve_capability_requirements(
model_name=model,
request_headers=context.original_headers,
request_body=original_request_body,
)
preferred_key_ids = await handler._resolve_preferred_key_ids(
model_name=model,
request_body=original_request_body,
)
candidate = await gateway_module._select_gateway_direct_candidate(
db=db,
redis_client=getattr(handler, "redis", None),
api_format=api_format,
model_name=str(model or "unknown"),
user_api_key=api_key,
request_id=context.request_id,
is_stream=True,
capability_requirements=capability_requirements or None,
preferred_key_ids=preferred_key_ids or None,
request_body=original_request_body,
)
if candidate is None:
return None
provider = candidate.provider
endpoint = candidate.endpoint
key = candidate.key
provider_api_format = str(endpoint.api_format or api_format or "").strip().lower()
prep = await handler._prepare_provider_request(
model=str(model or "unknown"),
provider=provider,
endpoint=endpoint,
key=key,
working_request_body=dict(original_request_body),
original_headers=context.original_headers,
client_api_format=api_format,
provider_api_format=provider_api_format,
candidate=candidate,
client_is_stream=True,
)
provider_api_format = str(prep.provider_api_format or "").strip().lower()
client_api_format = str(prep.client_api_format or "").strip().lower()
if (
not prep.upstream_is_stream
or gateway_module._stream_executor_requires_python_rewrite(
envelope=prep.envelope,
needs_conversion=bool(prep.needs_conversion),
provider_api_format=provider_api_format,
client_api_format=client_api_format,
)
or client_api_format != expected_api_format
):
return None
if provider_api_format != expected_api_format and not (
expected_api_format == "openai:chat"
and provider_api_format in {"claude:chat", "gemini:chat"}
):
return None
auth_info = prep.auth_info or await get_provider_auth(endpoint, key)
provider_payload, provider_headers = handler._request_builder.build(
prep.request_body,
context.original_headers,
endpoint,
key,
is_stream=prep.upstream_is_stream,
extra_headers=prep.extra_headers if prep.extra_headers else None,
pre_computed_auth=auth_info.as_tuple() if auth_info else None,
envelope=prep.envelope,
provider_api_format=prep.provider_api_format,
)
if not isinstance(provider_payload, dict):
return None
if prep.upstream_is_stream:
set_accept_if_absent(provider_headers)
provider_request_headers = {
str(k).lower(): str(v)
for k, v in dict(provider_headers or {}).items()
if str(k).strip() and str(v).strip()
}
provider_request_body = dict(provider_payload)
auth_header, auth_value = gateway_module._extract_gateway_upstream_auth(
provider_request_headers,
provider_api_format=provider_api_format,
key=key,
)
upstream_url = build_provider_url(
endpoint,
query_params=context.query_params,
path_params={"model": prep.url_model},
is_stream=prep.upstream_is_stream,
key=key,
decrypted_auth_config=auth_info.decrypted_auth_config if auth_info else None,
)
effective_proxy = resolve_effective_proxy(provider.proxy, getattr(key, "proxy", None))
proxy_info = await resolve_proxy_info_async(effective_proxy)
delegate_cfg = await resolve_delegate_config_async(effective_proxy)
effective_proxy_for_contract = effective_proxy
if not effective_proxy_for_contract or not effective_proxy_for_contract.get("enabled", True):
effective_proxy_for_contract = await get_system_proxy_config_async()
is_tunnel_delegate = bool(delegate_cfg and delegate_cfg.get("tunnel"))
proxy_url: str | None = None
if effective_proxy_for_contract and not is_tunnel_delegate:
proxy_url = await build_proxy_url_async(effective_proxy_for_contract)
proxy_snapshot = ExecutionProxySnapshot.from_proxy_info(
proxy_info,
proxy_url=proxy_url,
mode_override="tunnel" if is_tunnel_delegate else None,
node_id_override=(
str(delegate_cfg.get("node_id") or "").strip() or None if is_tunnel_delegate else None
),
)
timeouts = ExecutionPlanTimeouts(
connect_ms=int(config.http_connect_timeout * 1000),
read_ms=int(config.http_read_timeout * 1000),
write_ms=int(config.http_write_timeout * 1000),
pool_ms=int(config.http_pool_timeout * 1000),
total_ms=None,
)
contract = ExecutionPlan(
request_id=str(context.request_id or ""),
candidate_id=str(
getattr(candidate, "request_candidate_id", "") or getattr(candidate, "id", "") or ""
)
or None,
provider_name=str(provider.name),
provider_id=str(provider.id),
endpoint_id=str(endpoint.id),
key_id=str(key.id),
method="POST",
url=str(upstream_url),
headers=dict(provider_request_headers),
body=build_execution_plan_body(
provider_request_body,
content_type=str(provider_request_headers.get("content-type") or "").strip() or None,
),
stream=True,
provider_api_format=provider_api_format,
client_api_format=str(prep.client_api_format or api_format),
model_name=str(model or ""),
content_type=str(provider_request_headers.get("content-type") or "").strip() or None,
content_encoding=context.client_content_encoding,
proxy=proxy_snapshot,
tls_profile=prep.tls_profile,
timeouts=timeouts,
)
if not is_remote_execution_runtime_contract_eligible(contract):
return None
prompt_cache_key = str(provider_request_body.get("prompt_cache_key") or "").strip() or None
has_envelope = prep.envelope is not None
needs_conversion = bool(prep.needs_conversion)
decision_extra_headers: dict[str, str] = {}
decision_provider_request_headers = provider_request_headers or None
decision_provider_request_body = provider_request_body
report_context = {
"user_id": str(user.id),
"api_key_id": str(api_key.id),
"request_id": str(context.request_id),
"candidate_id": str(contract.candidate_id or "") or None,
"model": str(model or "unknown"),
"provider_name": str(contract.provider_name or provider.name or "unknown"),
"provider_id": str(contract.provider_id or provider.id or ""),
"endpoint_id": str(contract.endpoint_id or endpoint.id or ""),
"key_id": str(contract.key_id or key.id or ""),
"provider_api_format": provider_api_format,
"client_api_format": str(prep.client_api_format or api_format),
"mapped_model": str(prep.mapped_model or "").strip() or None,
"original_headers": dict(context.original_headers),
"original_request_body": original_request_body,
"proxy_info": proxy_info,
"has_envelope": has_envelope,
"envelope_name": gateway_module._gateway_report_context_envelope_name(prep.envelope),
"needs_conversion": needs_conversion,
}
report_context["provider_request_headers"] = provider_request_headers
report_context["provider_request_body"] = provider_request_body
return GatewayExecutionDecisionResponse(
action="executor_stream_decision",
decision_kind=decision_kind,
request_id=str(context.request_id),
candidate_id=str(contract.candidate_id or "") or None,
provider_name=str(contract.provider_name or provider.name or ""),
provider_id=str(contract.provider_id or provider.id or ""),
endpoint_id=str(contract.endpoint_id or endpoint.id or ""),
key_id=str(contract.key_id or key.id or ""),
upstream_base_url=str(getattr(endpoint, "base_url", "") or "").strip(),
upstream_url=str(upstream_url),
auth_header=auth_header,
auth_value=str(auth_value or "").strip(),
provider_api_format=provider_api_format,
client_api_format=str(prep.client_api_format or api_format),
model_name=str(model or "unknown"),
mapped_model=str(prep.mapped_model or "").strip() or None,
prompt_cache_key=prompt_cache_key,
extra_headers=decision_extra_headers,
provider_request_headers=decision_provider_request_headers,
provider_request_body=decision_provider_request_body,
content_type=(
str(provider_request_headers.get("content-type") or "").strip() or "application/json"
),
proxy=gateway_module._serialize_gateway_sync_proxy(proxy_snapshot),
tls_profile=str(prep.tls_profile or "").strip() or None,
timeouts=gateway_module._serialize_gateway_sync_timeouts(timeouts),
report_kind=report_kind,
report_context=report_context,
auth_context=auth_context,
)
async def _build_openai_chat_stream_decision(
*,
request: Request,
payload: GatewayExecuteRequest,
db: Session,
auth_context: GatewayAuthContext,
decision: GatewayRouteDecision,
) -> GatewayExecutionDecisionResponse | None:
return await _build_chat_stream_decision(
request=request,
payload=payload,
db=db,
auth_context=auth_context,
decision=decision,
expected_api_format="openai:chat",
decision_kind="openai_chat_stream",
report_kind="openai_chat_stream_success",
)
async def _build_claude_chat_stream_decision(
*,
request: Request,
payload: GatewayExecuteRequest,
db: Session,
auth_context: GatewayAuthContext,
decision: GatewayRouteDecision,
) -> GatewayExecutionDecisionResponse | None:
return await _build_chat_stream_decision(
request=request,
payload=payload,
db=db,
auth_context=auth_context,
decision=decision,
expected_api_format="claude:chat",
decision_kind="claude_chat_stream",
report_kind="claude_chat_stream_success",
)
async def _build_gemini_chat_stream_decision(
*,
request: Request,
payload: GatewayExecuteRequest,
db: Session,
auth_context: GatewayAuthContext,
decision: GatewayRouteDecision,
) -> GatewayExecutionDecisionResponse | None:
return await _build_chat_stream_decision(
request=request,
payload=payload,
db=db,
auth_context=auth_context,
decision=decision,
expected_api_format="gemini:chat",
decision_kind="gemini_chat_stream",
report_kind="gemini_chat_stream_success",
)
async def _build_claude_chat_sync_decision(
*,
request: Request,
payload: GatewayExecuteRequest,
db: Session,
auth_context: GatewayAuthContext,
decision: GatewayRouteDecision,
) -> GatewayExecutionDecisionResponse | None:
return await _build_chat_sync_decision(
request=request,
payload=payload,
db=db,
auth_context=auth_context,
decision=decision,
expected_api_format="claude:chat",
decision_kind="claude_chat_sync",
report_kind="claude_chat_sync_success",
finalize_kind="claude_chat_sync_finalize",
)
async def _build_gemini_chat_sync_decision(
*,
request: Request,
payload: GatewayExecuteRequest,
db: Session,
auth_context: GatewayAuthContext,
decision: GatewayRouteDecision,
) -> GatewayExecutionDecisionResponse | None:
return await _build_chat_sync_decision(
request=request,
payload=payload,
db=db,
auth_context=auth_context,
decision=decision,
expected_api_format="gemini:chat",
decision_kind="gemini_chat_sync",
report_kind="gemini_chat_sync_success",
finalize_kind="gemini_chat_sync_finalize",
)

View File

@@ -0,0 +1,591 @@
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_chat_sync_plan(
*,
request: Request,
payload: GatewayExecuteRequest,
db: Session,
auth_context: GatewayAuthContext,
decision: GatewayRouteDecision,
expected_api_format: str,
plan_kind: str,
report_kind: str,
finalize_kind: str,
) -> GatewayExecutionPlanResponse | None:
from src.api.handlers.base.chat_adapter_base import ChatAdapterBase
from src.api.handlers.base.chat_sync_executor import ChatSyncExecutor
from src.services.task.request_state import MutableRequestBodyState
if str(payload.method or "").strip().upper() != "POST":
return None
adapter, path_params = gateway_module._resolve_gateway_sync_adapter(decision, payload.path)
if not isinstance(adapter, ChatAdapterBase):
return None
if gateway_module._is_stream_request_payload(payload.body_json, path_params):
return None
user, api_key = gateway_module._load_gateway_auth_models(db, auth_context)
pipeline = gateway_module.get_pipeline()
await pipeline._check_user_rate_limit(request, db, user, api_key)
context = gateway_module._build_gateway_request_context(
request=request,
payload=payload,
db=db,
user=user,
api_key=api_key,
adapter=adapter,
path_params=path_params,
balance_remaining=auth_context.balance_remaining,
)
authorize_result = adapter.authorize(context)
if hasattr(authorize_result, "__await__"):
await authorize_result
original_request_body = await context.ensure_json_body_async()
if context.path_params:
original_request_body = adapter._merge_path_params(
original_request_body,
context.path_params,
)
request_obj = adapter._validate_request_body(original_request_body, context.path_params)
if isinstance(request_obj, JSONResponse):
return None
handler = adapter._create_handler(
db=db,
user=user,
api_key=api_key,
request_id=context.request_id,
client_ip=context.client_ip,
user_agent=context.user_agent,
start_time=context.start_time,
api_family=adapter.API_FAMILY.value if adapter.API_FAMILY else None,
endpoint_kind=adapter.ENDPOINT_KIND.value if adapter.ENDPOINT_KIND else None,
)
await handler._convert_request(request_obj)
model = handler.extract_model_from_request(
original_request_body,
context.path_params,
)
api_format = handler.allowed_api_formats[0]
capability_requirements = handler._resolve_capability_requirements(
model_name=model,
request_headers=context.original_headers,
request_body=original_request_body,
)
preferred_key_ids = await handler._resolve_preferred_key_ids(
model_name=model,
request_body=original_request_body,
)
candidate = await gateway_module._select_gateway_direct_candidate(
db=db,
redis_client=getattr(handler, "redis", None),
api_format=str(api_format),
model_name=str(model or "unknown"),
user_api_key=api_key,
request_id=context.request_id,
is_stream=False,
capability_requirements=capability_requirements or None,
preferred_key_ids=preferred_key_ids or None,
request_body=original_request_body,
)
if candidate is None:
return None
sync_executor = ChatSyncExecutor(handler)
prepared_plan = await sync_executor._build_sync_execution_plan(
candidate.provider,
candidate.endpoint,
candidate.key,
candidate,
model=str(model or "unknown"),
api_format=api_format,
original_headers=context.original_headers,
request_state=MutableRequestBodyState(original_request_body),
query_params=context.query_params,
client_content_encoding=context.client_content_encoding,
)
contract_provider_api_format = (
str(prepared_plan.contract.provider_api_format or "").strip().lower()
)
contract_client_api_format = str(prepared_plan.contract.client_api_format or "").strip().lower()
if not prepared_plan.remote_eligible or contract_client_api_format != expected_api_format:
return None
selected_report_kind = (
finalize_kind
if (
prepared_plan.upstream_is_stream
or prepared_plan.needs_conversion
or prepared_plan.envelope is not None
or contract_provider_api_format != expected_api_format
)
else report_kind
)
return GatewayExecutionPlanResponse(
action="executor_sync",
plan_kind=plan_kind,
plan=prepared_plan.contract.to_payload(),
report_kind=selected_report_kind,
report_context={
"user_id": str(user.id),
"api_key_id": str(api_key.id),
"request_id": str(context.request_id),
"model": str(model or "unknown"),
"provider_name": str(prepared_plan.contract.provider_name or "unknown"),
"provider_id": str(prepared_plan.contract.provider_id or ""),
"endpoint_id": str(prepared_plan.contract.endpoint_id or ""),
"key_id": str(prepared_plan.contract.key_id or ""),
"provider_api_format": str(prepared_plan.contract.provider_api_format or ""),
"client_api_format": str(prepared_plan.contract.client_api_format or ""),
"mapped_model": sync_executor._ctx.mapped_model_result,
"original_headers": dict(context.original_headers),
"original_request_body": original_request_body,
"provider_request_headers": dict(prepared_plan.headers),
"provider_request_body": prepared_plan.payload,
"proxy_info": prepared_plan.proxy_info,
"has_envelope": prepared_plan.envelope is not None,
"envelope_name": gateway_module._gateway_report_context_envelope_name(
prepared_plan.envelope
),
"needs_conversion": bool(prepared_plan.needs_conversion),
},
)
async def _build_openai_chat_sync_plan(
*,
request: Request,
payload: GatewayExecuteRequest,
db: Session,
auth_context: GatewayAuthContext,
decision: GatewayRouteDecision,
) -> GatewayExecutionPlanResponse | None:
return await _build_chat_sync_plan(
request=request,
payload=payload,
db=db,
auth_context=auth_context,
decision=decision,
expected_api_format="openai:chat",
plan_kind="openai_chat_sync",
report_kind="openai_chat_sync_success",
finalize_kind="openai_chat_sync_finalize",
)
async def _build_chat_stream_plan(
*,
request: Request,
payload: GatewayExecuteRequest,
db: Session,
auth_context: GatewayAuthContext,
decision: GatewayRouteDecision,
expected_api_format: str,
plan_kind: str,
report_kind: str,
) -> GatewayExecutionPlanResponse | None:
from src.api.handlers.base.chat_adapter_base import ChatAdapterBase
from src.config.settings import config
from src.core.api_format.headers import set_accept_if_absent
from src.services.provider.auth import get_provider_auth
from src.services.provider.transport import build_provider_url
from src.services.proxy_node.resolver import (
build_proxy_url_async,
get_system_proxy_config_async,
resolve_delegate_config_async,
resolve_effective_proxy,
resolve_proxy_info_async,
)
from src.services.request.execution_runtime_plan import (
ExecutionPlan,
ExecutionPlanTimeouts,
ExecutionProxySnapshot,
build_execution_plan_body,
is_remote_execution_runtime_contract_eligible,
)
if str(payload.method or "").strip().upper() != "POST":
return None
adapter, path_params = gateway_module._resolve_gateway_sync_adapter(decision, payload.path)
if not isinstance(adapter, ChatAdapterBase):
return None
if not gateway_module._is_stream_request_payload(payload.body_json, path_params):
return None
user, api_key = gateway_module._load_gateway_auth_models(db, auth_context)
pipeline = gateway_module.get_pipeline()
await pipeline._check_user_rate_limit(request, db, user, api_key)
context = gateway_module._build_gateway_request_context(
request=request,
payload=payload,
db=db,
user=user,
api_key=api_key,
adapter=adapter,
path_params=path_params,
balance_remaining=auth_context.balance_remaining,
)
authorize_result = adapter.authorize(context)
if hasattr(authorize_result, "__await__"):
await authorize_result
original_request_body = await context.ensure_json_body_async()
if context.path_params:
original_request_body = adapter._merge_path_params(
original_request_body,
context.path_params,
)
request_obj = adapter._validate_request_body(original_request_body, context.path_params)
if isinstance(request_obj, JSONResponse):
return None
handler = adapter._create_handler(
db=db,
user=user,
api_key=api_key,
request_id=context.request_id,
client_ip=context.client_ip,
user_agent=context.user_agent,
start_time=context.start_time,
api_family=adapter.API_FAMILY.value if adapter.API_FAMILY else None,
endpoint_kind=adapter.ENDPOINT_KIND.value if adapter.ENDPOINT_KIND else None,
)
converted_request = await handler._convert_request(request_obj)
model = getattr(converted_request, "model", original_request_body.get("model", "unknown"))
api_format = handler.allowed_api_formats[0]
capability_requirements = handler._resolve_capability_requirements(
model_name=model,
request_headers=context.original_headers,
request_body=original_request_body,
)
preferred_key_ids = await handler._resolve_preferred_key_ids(
model_name=model,
request_body=original_request_body,
)
candidate = await gateway_module._select_gateway_direct_candidate(
db=db,
redis_client=getattr(handler, "redis", None),
api_format=str(api_format),
model_name=str(model or "unknown"),
user_api_key=api_key,
request_id=context.request_id,
is_stream=True,
capability_requirements=capability_requirements or None,
preferred_key_ids=preferred_key_ids or None,
request_body=original_request_body,
)
if candidate is None:
return None
provider = candidate.provider
endpoint = candidate.endpoint
key = candidate.key
provider_api_format = str(endpoint.api_format or api_format or "")
prep = await handler._prepare_provider_request(
model=str(model or "unknown"),
provider=provider,
endpoint=endpoint,
key=key,
working_request_body=dict(original_request_body),
original_headers=context.original_headers,
client_api_format=str(api_format),
provider_api_format=provider_api_format,
candidate=candidate,
client_is_stream=True,
)
provider_api_format = str(prep.provider_api_format or "")
auth_info = prep.auth_info or await get_provider_auth(endpoint, key)
provider_payload, provider_headers = handler._request_builder.build(
prep.request_body,
context.original_headers,
endpoint,
key,
is_stream=prep.upstream_is_stream,
extra_headers=prep.extra_headers if prep.extra_headers else None,
pre_computed_auth=auth_info.as_tuple() if auth_info else None,
envelope=prep.envelope,
provider_api_format=prep.provider_api_format,
)
if prep.upstream_is_stream:
set_accept_if_absent(provider_headers)
upstream_url = build_provider_url(
endpoint,
query_params=context.query_params,
path_params={"model": prep.url_model},
is_stream=prep.upstream_is_stream,
key=key,
decrypted_auth_config=auth_info.decrypted_auth_config if auth_info else None,
)
effective_proxy = resolve_effective_proxy(provider.proxy, getattr(key, "proxy", None))
proxy_info = await resolve_proxy_info_async(effective_proxy)
delegate_cfg = await resolve_delegate_config_async(effective_proxy)
effective_proxy_for_contract = effective_proxy
if not effective_proxy_for_contract or not effective_proxy_for_contract.get("enabled", True):
effective_proxy_for_contract = await get_system_proxy_config_async()
is_tunnel_delegate = bool(delegate_cfg and delegate_cfg.get("tunnel"))
proxy_url: str | None = None
if effective_proxy_for_contract and not is_tunnel_delegate:
proxy_url = await build_proxy_url_async(effective_proxy_for_contract)
contract = ExecutionPlan(
request_id=str(context.request_id or ""),
candidate_id=str(
getattr(candidate, "request_candidate_id", "") or getattr(candidate, "id", "") or ""
)
or None,
provider_name=str(provider.name),
provider_id=str(provider.id),
endpoint_id=str(endpoint.id),
key_id=str(key.id),
method="POST",
url=upstream_url,
headers=dict(provider_headers),
body=build_execution_plan_body(
provider_payload,
content_type=str(provider_headers.get("content-type") or "").strip() or None,
),
stream=True,
provider_api_format=provider_api_format,
client_api_format=str(api_format),
model_name=str(model or ""),
content_type=str(provider_headers.get("content-type") or "").strip() or None,
content_encoding=context.client_content_encoding,
proxy=ExecutionProxySnapshot.from_proxy_info(
proxy_info,
proxy_url=proxy_url,
mode_override="tunnel" if is_tunnel_delegate else None,
node_id_override=(
str(delegate_cfg.get("node_id") or "").strip() or None
if is_tunnel_delegate
else None
),
),
tls_profile=prep.tls_profile,
timeouts=ExecutionPlanTimeouts(
connect_ms=int(config.http_connect_timeout * 1000),
read_ms=int(config.http_read_timeout * 1000),
write_ms=int(config.http_write_timeout * 1000),
pool_ms=int(config.http_pool_timeout * 1000),
total_ms=None,
),
)
if (
not prep.upstream_is_stream
or gateway_module._stream_executor_requires_python_rewrite(
envelope=prep.envelope,
needs_conversion=bool(prep.needs_conversion),
provider_api_format=provider_api_format,
client_api_format=str(contract.client_api_format or ""),
)
or not is_remote_execution_runtime_contract_eligible(contract)
or provider_api_format.strip().lower() != expected_api_format
or str(contract.client_api_format or "").strip().lower() != expected_api_format
):
return None
return GatewayExecutionPlanResponse(
action="executor_stream",
plan_kind=plan_kind,
plan=contract.to_payload(),
report_kind=report_kind,
report_context={
"user_id": str(user.id),
"api_key_id": str(api_key.id),
"request_id": str(context.request_id),
"model": str(model or "unknown"),
"provider_name": str(contract.provider_name or "unknown"),
"provider_id": str(contract.provider_id or ""),
"endpoint_id": str(contract.endpoint_id or ""),
"key_id": str(contract.key_id or ""),
"candidate_id": str(contract.candidate_id or ""),
"provider_api_format": str(contract.provider_api_format or ""),
"client_api_format": str(contract.client_api_format or ""),
"mapped_model": prep.mapped_model,
"original_headers": dict(context.original_headers),
"original_request_body": original_request_body,
"provider_request_headers": dict(provider_headers),
"provider_request_body": provider_payload,
"proxy_info": proxy_info,
"has_envelope": prep.envelope is not None,
"envelope_name": gateway_module._gateway_report_context_envelope_name(prep.envelope),
"needs_conversion": bool(prep.needs_conversion),
},
)
async def _build_openai_chat_stream_plan(
*,
request: Request,
payload: GatewayExecuteRequest,
db: Session,
auth_context: GatewayAuthContext,
decision: GatewayRouteDecision,
) -> GatewayExecutionPlanResponse | None:
return await _build_chat_stream_plan(
request=request,
payload=payload,
db=db,
auth_context=auth_context,
decision=decision,
expected_api_format="openai:chat",
plan_kind="openai_chat_stream",
report_kind="openai_chat_stream_success",
)
async def _build_claude_chat_stream_plan(
*,
request: Request,
payload: GatewayExecuteRequest,
db: Session,
auth_context: GatewayAuthContext,
decision: GatewayRouteDecision,
) -> GatewayExecutionPlanResponse | None:
return await _build_chat_stream_plan(
request=request,
payload=payload,
db=db,
auth_context=auth_context,
decision=decision,
expected_api_format="claude:chat",
plan_kind="claude_chat_stream",
report_kind="claude_chat_stream_success",
)
async def _build_gemini_chat_stream_plan(
*,
request: Request,
payload: GatewayExecuteRequest,
db: Session,
auth_context: GatewayAuthContext,
decision: GatewayRouteDecision,
) -> GatewayExecutionPlanResponse | None:
return await _build_chat_stream_plan(
request=request,
payload=payload,
db=db,
auth_context=auth_context,
decision=decision,
expected_api_format="gemini:chat",
plan_kind="gemini_chat_stream",
report_kind="gemini_chat_stream_success",
)
async def _build_claude_chat_sync_plan(
*,
request: Request,
payload: GatewayExecuteRequest,
db: Session,
auth_context: GatewayAuthContext,
decision: GatewayRouteDecision,
) -> GatewayExecutionPlanResponse | None:
return await _build_chat_sync_plan(
request=request,
payload=payload,
db=db,
auth_context=auth_context,
decision=decision,
expected_api_format="claude:chat",
plan_kind="claude_chat_sync",
report_kind="claude_chat_sync_success",
finalize_kind="claude_chat_sync_finalize",
)
async def _build_gemini_chat_sync_plan(
*,
request: Request,
payload: GatewayExecuteRequest,
db: Session,
auth_context: GatewayAuthContext,
decision: GatewayRouteDecision,
) -> GatewayExecutionPlanResponse | None:
return await _build_chat_sync_plan(
request=request,
payload=payload,
db=db,
auth_context=auth_context,
decision=decision,
expected_api_format="gemini:chat",
plan_kind="gemini_chat_sync",
report_kind="gemini_chat_sync_success",
finalize_kind="gemini_chat_sync_finalize",
)

View File

@@ -0,0 +1,43 @@
"""Compatibility re-export layer for gateway CLI builders."""
from __future__ import annotations
from .gateway_cli_decision import (
_build_claude_cli_stream_decision,
_build_claude_cli_sync_decision,
_build_cli_stream_decision,
_build_cli_sync_decision,
_build_gemini_cli_stream_decision,
_build_gemini_cli_sync_decision,
_build_openai_cli_stream_decision,
_build_openai_cli_sync_decision,
)
from .gateway_cli_plan import (
_build_claude_cli_stream_plan,
_build_claude_cli_sync_plan,
_build_cli_stream_plan,
_build_cli_sync_plan,
_build_gemini_cli_stream_plan,
_build_gemini_cli_sync_plan,
_build_openai_cli_stream_plan,
_build_openai_cli_sync_plan,
)
__all__ = [
"_build_cli_sync_decision",
"_build_cli_stream_decision",
"_build_openai_cli_sync_decision",
"_build_openai_cli_stream_decision",
"_build_claude_cli_sync_decision",
"_build_claude_cli_stream_decision",
"_build_gemini_cli_sync_decision",
"_build_gemini_cli_stream_decision",
"_build_cli_stream_plan",
"_build_cli_sync_plan",
"_build_openai_cli_stream_plan",
"_build_claude_cli_stream_plan",
"_build_gemini_cli_stream_plan",
"_build_openai_cli_sync_plan",
"_build_claude_cli_sync_plan",
"_build_gemini_cli_sync_plan",
]

View File

@@ -0,0 +1,945 @@
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_cli_sync_decision(
*,
request: Request,
payload: GatewayExecuteRequest,
db: Session,
auth_context: GatewayAuthContext,
decision: GatewayRouteDecision,
expected_api_format: str,
decision_kind: str,
report_kind: str,
finalize_kind: str,
) -> GatewayExecutionDecisionResponse | None:
from src.api.handlers.base.cli_adapter_base import CliAdapterBase
from src.config.settings import config
from src.services.proxy_node.resolver import (
build_proxy_url_async,
get_system_proxy_config_async,
resolve_delegate_config_async,
resolve_effective_proxy,
resolve_proxy_info_async,
)
from src.services.request.execution_runtime_plan import (
ExecutionPlanTimeouts,
ExecutionProxySnapshot,
should_bypass_remote_execution_runtime_url,
)
if str(payload.method or "").strip().upper() != "POST":
return None
adapter, path_params = gateway_module._resolve_gateway_sync_adapter(decision, payload.path)
if not isinstance(adapter, CliAdapterBase):
return None
if gateway_module._is_stream_request_payload(payload.body_json, path_params):
return None
if not isinstance(payload.body_json, dict):
return None
user, api_key = gateway_module._load_gateway_auth_models(db, auth_context)
pipeline = gateway_module.get_pipeline()
await pipeline._check_user_rate_limit(request, db, user, api_key)
context = gateway_module._build_gateway_request_context(
request=request,
payload=payload,
db=db,
user=user,
api_key=api_key,
adapter=adapter,
path_params=path_params,
balance_remaining=auth_context.balance_remaining,
)
authorize_result = adapter.authorize(context)
if hasattr(authorize_result, "__await__"):
await authorize_result
original_request_body = await context.ensure_json_body_async()
if context.path_params:
original_request_body = adapter._merge_path_params(
original_request_body,
context.path_params,
)
handler = adapter.HANDLER_CLASS(
db=db,
user=user,
api_key=api_key,
request_id=context.request_id,
client_ip=context.client_ip,
user_agent=context.user_agent,
start_time=context.start_time,
allowed_api_formats=adapter.allowed_api_formats,
adapter_detector=adapter.detect_capability_requirements,
perf_metrics=context.extra.get("perf"),
api_family=adapter.API_FAMILY.value if adapter.API_FAMILY else None,
endpoint_kind=adapter.ENDPOINT_KIND.value if adapter.ENDPOINT_KIND else None,
)
model = handler.extract_model_from_request(original_request_body, context.path_params)
client_api_format = str(handler.primary_api_format or "").strip().lower()
if client_api_format != expected_api_format:
return None
capability_requirements = handler._resolve_capability_requirements(
model_name=model,
request_headers=context.original_headers,
request_body=original_request_body,
)
preferred_key_ids = await handler._resolve_preferred_key_ids(
model_name=model,
request_body=original_request_body,
)
candidate = await gateway_module._select_gateway_direct_candidate(
db=db,
redis_client=getattr(handler, "redis", None),
api_format=client_api_format,
model_name=str(model or "unknown"),
user_api_key=api_key,
request_id=context.request_id,
is_stream=False,
capability_requirements=capability_requirements or None,
preferred_key_ids=preferred_key_ids or None,
request_body=original_request_body,
)
if candidate is None:
return None
provider = candidate.provider
endpoint = candidate.endpoint
key = candidate.key
provider_api_format = str(endpoint.api_format or client_api_format or "").strip().lower()
mapped_model = candidate.mapping_matched_model if candidate else None
if not mapped_model:
mapped_model = await handler._get_mapped_model(
source_model=str(model or "unknown"),
provider_id=str(provider.id),
)
upstream_request = await handler._build_upstream_request(
provider=provider,
endpoint=endpoint,
key=key,
request_body=dict(original_request_body),
original_headers=context.original_headers,
query_params=context.query_params,
client_api_format=client_api_format,
provider_api_format=provider_api_format,
fallback_model=str(model or "unknown"),
mapped_model=mapped_model,
client_is_stream=False,
needs_conversion=bool(getattr(candidate, "needs_conversion", False)),
output_limit=getattr(candidate, "output_limit", None),
)
if should_bypass_remote_execution_runtime_url(
upstream_request.url,
provider_api_format=provider_api_format,
client_api_format=client_api_format,
):
return None
upstream_is_stream = bool(upstream_request.upstream_is_stream)
provider_request_body = dict(upstream_request.payload or {})
provider_request_headers = {
str(k).lower(): str(v)
for k, v in dict(upstream_request.headers or {}).items()
if str(k).strip() and str(v).strip()
}
auth_header, auth_value = gateway_module._extract_gateway_upstream_auth(
provider_request_headers,
provider_api_format=provider_api_format,
key=key,
)
prompt_cache_key = str(provider_request_body.get("prompt_cache_key") or "").strip() or None
effective_proxy = resolve_effective_proxy(provider.proxy, getattr(key, "proxy", None))
proxy_info = await resolve_proxy_info_async(effective_proxy)
delegate_cfg = await resolve_delegate_config_async(effective_proxy)
is_tunnel_delegate = bool(delegate_cfg and delegate_cfg.get("tunnel"))
effective_proxy_for_contract = effective_proxy
if not effective_proxy_for_contract or not effective_proxy_for_contract.get("enabled", True):
effective_proxy_for_contract = await get_system_proxy_config_async()
proxy_url: str | None = None
if effective_proxy_for_contract and not is_tunnel_delegate:
proxy_url = await build_proxy_url_async(effective_proxy_for_contract)
proxy_snapshot = ExecutionProxySnapshot.from_proxy_info(
proxy_info,
proxy_url=proxy_url,
mode_override="tunnel" if is_tunnel_delegate else None,
node_id_override=(
str(delegate_cfg.get("node_id") or "").strip() or None if is_tunnel_delegate else None
),
)
request_timeout = provider.request_timeout or config.http_request_timeout
timeouts = ExecutionPlanTimeouts(
connect_ms=int(config.http_connect_timeout * 1000),
read_ms=int(config.http_read_timeout * 1000),
write_ms=int(config.http_write_timeout * 1000),
pool_ms=int(config.http_pool_timeout * 1000),
total_ms=int(request_timeout * 1000),
)
requires_finalize = (
upstream_is_stream
or bool(getattr(candidate, "needs_conversion", False))
or provider_api_format != client_api_format
or upstream_request.envelope is not None
)
has_envelope = upstream_request.envelope is not None
needs_conversion = bool(getattr(candidate, "needs_conversion", False))
decision_extra_headers: dict[str, str] = {}
decision_provider_request_headers = provider_request_headers or None
decision_provider_request_body = provider_request_body
report_context = {
"user_id": str(user.id),
"api_key_id": str(api_key.id),
"request_id": str(context.request_id),
"candidate_id": str(
getattr(candidate, "request_candidate_id", "") or getattr(candidate, "id", "") or ""
)
or None,
"model": str(model or "unknown"),
"provider_name": str(provider.name or "unknown"),
"provider_id": str(provider.id),
"endpoint_id": str(endpoint.id),
"key_id": str(key.id),
"provider_api_format": provider_api_format,
"client_api_format": client_api_format,
"mapped_model": str(mapped_model or "").strip() or None,
"original_headers": dict(context.original_headers),
"original_request_body": original_request_body,
"proxy_info": proxy_info,
"has_envelope": has_envelope,
"envelope_name": gateway_module._gateway_report_context_envelope_name(
upstream_request.envelope
),
"needs_conversion": needs_conversion,
}
report_context["provider_request_headers"] = provider_request_headers
report_context["provider_request_body"] = provider_request_body
return GatewayExecutionDecisionResponse(
action="executor_sync_decision",
decision_kind=decision_kind,
request_id=str(context.request_id),
candidate_id=str(
getattr(candidate, "request_candidate_id", "") or getattr(candidate, "id", "") or ""
)
or None,
provider_name=str(provider.name),
provider_id=str(provider.id),
endpoint_id=str(endpoint.id),
key_id=str(key.id),
upstream_base_url=str(getattr(endpoint, "base_url", "") or "").strip(),
upstream_url=str(upstream_request.url or "").strip() or None,
auth_header=str(auth_header or "").strip() or "authorization",
auth_value=str(auth_value or "").strip(),
provider_api_format=provider_api_format,
client_api_format=client_api_format,
model_name=str(model or "unknown"),
mapped_model=str(mapped_model or "").strip() or None,
prompt_cache_key=prompt_cache_key,
extra_headers=decision_extra_headers,
provider_request_headers=decision_provider_request_headers,
provider_request_body=decision_provider_request_body,
content_type=(
str(provider_request_headers.get("content-type") or "").strip() or "application/json"
),
proxy=gateway_module._serialize_gateway_sync_proxy(proxy_snapshot),
tls_profile=str(upstream_request.tls_profile or "").strip() or None,
timeouts=gateway_module._serialize_gateway_sync_timeouts(timeouts),
upstream_is_stream=upstream_is_stream or None,
report_kind=finalize_kind if requires_finalize else report_kind,
report_context=report_context,
auth_context=auth_context,
)
async def _build_cli_stream_decision(
*,
request: Request,
payload: GatewayExecuteRequest,
db: Session,
auth_context: GatewayAuthContext,
decision: GatewayRouteDecision,
expected_api_format: str,
decision_kind: str,
report_kind: str,
) -> GatewayExecutionDecisionResponse | None:
from src.api.handlers.base.cli_adapter_base import CliAdapterBase
from src.config.settings import config
from src.services.proxy_node.resolver import (
build_proxy_url_async,
get_system_proxy_config_async,
resolve_delegate_config_async,
resolve_effective_proxy,
resolve_proxy_info_async,
)
from src.services.request.execution_runtime_plan import (
ExecutionPlanTimeouts,
ExecutionProxySnapshot,
should_bypass_remote_execution_runtime_url,
)
if str(payload.method or "").strip().upper() != "POST":
return None
adapter, path_params = gateway_module._resolve_gateway_sync_adapter(decision, payload.path)
if not isinstance(adapter, CliAdapterBase):
return None
if not gateway_module._is_stream_request_payload(payload.body_json, path_params):
return None
if not isinstance(payload.body_json, dict):
return None
user, api_key = gateway_module._load_gateway_auth_models(db, auth_context)
pipeline = gateway_module.get_pipeline()
await pipeline._check_user_rate_limit(request, db, user, api_key)
context = gateway_module._build_gateway_request_context(
request=request,
payload=payload,
db=db,
user=user,
api_key=api_key,
adapter=adapter,
path_params=path_params,
balance_remaining=auth_context.balance_remaining,
)
authorize_result = adapter.authorize(context)
if hasattr(authorize_result, "__await__"):
await authorize_result
original_request_body = await context.ensure_json_body_async()
if context.path_params:
original_request_body = adapter._merge_path_params(
original_request_body,
context.path_params,
)
handler = adapter.HANDLER_CLASS(
db=db,
user=user,
api_key=api_key,
request_id=context.request_id,
client_ip=context.client_ip,
user_agent=context.user_agent,
start_time=context.start_time,
allowed_api_formats=adapter.allowed_api_formats,
adapter_detector=adapter.detect_capability_requirements,
perf_metrics=context.extra.get("perf"),
api_family=adapter.API_FAMILY.value if adapter.API_FAMILY else None,
endpoint_kind=adapter.ENDPOINT_KIND.value if adapter.ENDPOINT_KIND else None,
)
model = handler.extract_model_from_request(original_request_body, context.path_params)
client_api_format = str(handler.primary_api_format or "").strip().lower()
if client_api_format != expected_api_format:
return None
capability_requirements = handler._resolve_capability_requirements(
model_name=model,
request_headers=context.original_headers,
request_body=original_request_body,
)
preferred_key_ids = await handler._resolve_preferred_key_ids(
model_name=model,
request_body=original_request_body,
)
candidate = await gateway_module._select_gateway_direct_candidate(
db=db,
redis_client=getattr(handler, "redis", None),
api_format=client_api_format,
model_name=str(model or "unknown"),
user_api_key=api_key,
request_id=context.request_id,
is_stream=True,
capability_requirements=capability_requirements or None,
preferred_key_ids=preferred_key_ids or None,
request_body=original_request_body,
)
if candidate is None:
return None
provider = candidate.provider
endpoint = candidate.endpoint
key = candidate.key
provider_api_format = str(endpoint.api_format or client_api_format or "").strip().lower()
if provider_api_format != expected_api_format and not (
expected_api_format in {"openai:cli", "openai:compact"}
and provider_api_format in {"claude:cli", "gemini:cli"}
):
return None
mapped_model = candidate.mapping_matched_model if candidate else None
if not mapped_model:
mapped_model = await handler._get_mapped_model(
source_model=str(model or "unknown"),
provider_id=str(provider.id),
)
upstream_request = await handler._build_upstream_request(
provider=provider,
endpoint=endpoint,
key=key,
request_body=dict(original_request_body),
original_headers=context.original_headers,
query_params=context.query_params,
client_api_format=client_api_format,
provider_api_format=provider_api_format,
fallback_model=str(model or "unknown"),
mapped_model=mapped_model,
client_is_stream=True,
needs_conversion=bool(getattr(candidate, "needs_conversion", False)),
output_limit=getattr(candidate, "output_limit", None),
)
if should_bypass_remote_execution_runtime_url(
upstream_request.url,
provider_api_format=provider_api_format,
client_api_format=client_api_format,
):
return None
if not bool(upstream_request.upstream_is_stream):
return None
if gateway_module._stream_executor_requires_python_rewrite(
envelope=upstream_request.envelope,
needs_conversion=bool(getattr(candidate, "needs_conversion", False)),
provider_api_format=provider_api_format,
client_api_format=client_api_format,
):
return None
provider_request_body = dict(upstream_request.payload or {})
provider_request_headers = {
str(k).lower(): str(v)
for k, v in dict(upstream_request.headers or {}).items()
if str(k).strip() and str(v).strip()
}
auth_header, auth_value = gateway_module._extract_gateway_upstream_auth(
provider_request_headers,
provider_api_format=provider_api_format,
key=key,
)
prompt_cache_key = str(provider_request_body.get("prompt_cache_key") or "").strip() or None
effective_proxy = resolve_effective_proxy(provider.proxy, getattr(key, "proxy", None))
proxy_info = await resolve_proxy_info_async(effective_proxy)
delegate_cfg = await resolve_delegate_config_async(effective_proxy)
is_tunnel_delegate = bool(delegate_cfg and delegate_cfg.get("tunnel"))
effective_proxy_for_contract = effective_proxy
if not effective_proxy_for_contract or not effective_proxy_for_contract.get("enabled", True):
effective_proxy_for_contract = await get_system_proxy_config_async()
proxy_url: str | None = None
if effective_proxy_for_contract and not is_tunnel_delegate:
proxy_url = await build_proxy_url_async(effective_proxy_for_contract)
proxy_snapshot = ExecutionProxySnapshot.from_proxy_info(
proxy_info,
proxy_url=proxy_url,
mode_override="tunnel" if is_tunnel_delegate else None,
node_id_override=(
str(delegate_cfg.get("node_id") or "").strip() or None if is_tunnel_delegate else None
),
)
timeouts = ExecutionPlanTimeouts(
connect_ms=int(config.http_connect_timeout * 1000),
read_ms=int(config.http_read_timeout * 1000),
write_ms=int(config.http_write_timeout * 1000),
pool_ms=int(config.http_pool_timeout * 1000),
total_ms=None,
)
has_envelope = upstream_request.envelope is not None
needs_conversion = bool(getattr(candidate, "needs_conversion", False))
decision_extra_headers: dict[str, str] = {}
decision_provider_request_headers = provider_request_headers or None
decision_provider_request_body = provider_request_body
report_context = {
"user_id": str(user.id),
"api_key_id": str(api_key.id),
"request_id": str(context.request_id),
"candidate_id": str(
getattr(candidate, "request_candidate_id", "") or getattr(candidate, "id", "") or ""
)
or None,
"model": str(model or "unknown"),
"provider_name": str(provider.name or "unknown"),
"provider_id": str(provider.id),
"endpoint_id": str(endpoint.id),
"key_id": str(key.id),
"provider_api_format": provider_api_format,
"client_api_format": client_api_format,
"mapped_model": str(mapped_model or "").strip() or None,
"original_headers": dict(context.original_headers),
"original_request_body": original_request_body,
"proxy_info": proxy_info,
"has_envelope": has_envelope,
"envelope_name": gateway_module._gateway_report_context_envelope_name(
upstream_request.envelope
),
"needs_conversion": needs_conversion,
}
report_context["provider_request_headers"] = provider_request_headers
report_context["provider_request_body"] = provider_request_body
return GatewayExecutionDecisionResponse(
action="executor_stream_decision",
decision_kind=decision_kind,
request_id=str(context.request_id),
candidate_id=str(
getattr(candidate, "request_candidate_id", "") or getattr(candidate, "id", "") or ""
)
or None,
provider_name=str(provider.name),
provider_id=str(provider.id),
endpoint_id=str(endpoint.id),
key_id=str(key.id),
upstream_base_url=str(getattr(endpoint, "base_url", "") or "").strip(),
upstream_url=str(upstream_request.url or "").strip() or None,
auth_header=str(auth_header or "").strip() or "authorization",
auth_value=str(auth_value or "").strip(),
provider_api_format=provider_api_format,
client_api_format=client_api_format,
model_name=str(model or "unknown"),
mapped_model=str(mapped_model or "").strip() or None,
prompt_cache_key=prompt_cache_key,
extra_headers=decision_extra_headers,
provider_request_headers=decision_provider_request_headers,
provider_request_body=decision_provider_request_body,
content_type=(
str(provider_request_headers.get("content-type") or "").strip() or "application/json"
),
proxy=gateway_module._serialize_gateway_sync_proxy(proxy_snapshot),
tls_profile=str(upstream_request.tls_profile or "").strip() or None,
timeouts=gateway_module._serialize_gateway_sync_timeouts(timeouts),
report_kind=report_kind,
report_context=report_context,
auth_context=auth_context,
)
async def _build_openai_cli_sync_decision(
*,
request: Request,
payload: GatewayExecuteRequest,
db: Session,
auth_context: GatewayAuthContext,
decision: GatewayRouteDecision,
) -> GatewayExecutionDecisionResponse | None:
from src.api.handlers.base.cli_adapter_base import CliAdapterBase
from src.config.settings import config
from src.services.proxy_node.resolver import (
build_proxy_url_async,
get_system_proxy_config_async,
resolve_delegate_config_async,
resolve_effective_proxy,
resolve_proxy_info_async,
)
from src.services.request.execution_runtime_plan import (
ExecutionPlanTimeouts,
ExecutionProxySnapshot,
)
if str(payload.method or "").strip().upper() != "POST":
return None
adapter, path_params = gateway_module._resolve_gateway_sync_adapter(decision, payload.path)
if not isinstance(adapter, CliAdapterBase):
return None
if gateway_module._is_stream_request_payload(payload.body_json, path_params):
return None
if not isinstance(payload.body_json, dict):
return None
user, api_key = gateway_module._load_gateway_auth_models(db, auth_context)
pipeline = gateway_module.get_pipeline()
await pipeline._check_user_rate_limit(request, db, user, api_key)
context = gateway_module._build_gateway_request_context(
request=request,
payload=payload,
db=db,
user=user,
api_key=api_key,
adapter=adapter,
path_params=path_params,
balance_remaining=auth_context.balance_remaining,
)
authorize_result = adapter.authorize(context)
if hasattr(authorize_result, "__await__"):
await authorize_result
original_request_body = await context.ensure_json_body_async()
if context.path_params:
original_request_body = adapter._merge_path_params(
original_request_body,
context.path_params,
)
if decision.route_kind == "compact":
original_request_body.pop("stream", None)
handler = adapter.HANDLER_CLASS(
db=db,
user=user,
api_key=api_key,
request_id=context.request_id,
client_ip=context.client_ip,
user_agent=context.user_agent,
start_time=context.start_time,
allowed_api_formats=adapter.allowed_api_formats,
adapter_detector=adapter.detect_capability_requirements,
perf_metrics=context.extra.get("perf"),
api_family=adapter.API_FAMILY.value if adapter.API_FAMILY else None,
endpoint_kind=adapter.ENDPOINT_KIND.value if adapter.ENDPOINT_KIND else None,
)
model = handler.extract_model_from_request(original_request_body, context.path_params)
client_api_format = str(handler.primary_api_format or "").strip().lower()
if client_api_format not in {"openai:cli", "openai:compact"}:
return None
capability_requirements = handler._resolve_capability_requirements(
model_name=model,
request_headers=context.original_headers,
request_body=original_request_body,
)
preferred_key_ids = await handler._resolve_preferred_key_ids(
model_name=model,
request_body=original_request_body,
)
candidate = await gateway_module._select_gateway_direct_candidate(
db=db,
redis_client=getattr(handler, "redis", None),
api_format=client_api_format,
model_name=str(model or "unknown"),
user_api_key=api_key,
request_id=context.request_id,
is_stream=False,
capability_requirements=capability_requirements or None,
preferred_key_ids=preferred_key_ids or None,
request_body=original_request_body,
)
if candidate is None:
return None
provider = candidate.provider
endpoint = candidate.endpoint
key = candidate.key
provider_api_format = str(endpoint.api_format or client_api_format or "").strip().lower()
mapped_model = candidate.mapping_matched_model if candidate else None
if not mapped_model:
mapped_model = await handler._get_mapped_model(
source_model=str(model or "unknown"),
provider_id=str(provider.id),
)
upstream_request = await handler._build_upstream_request(
provider=provider,
endpoint=endpoint,
key=key,
request_body=dict(original_request_body),
original_headers=context.original_headers,
query_params=context.query_params,
client_api_format=client_api_format,
provider_api_format=provider_api_format,
fallback_model=str(model or "unknown"),
mapped_model=mapped_model,
client_is_stream=False,
needs_conversion=bool(getattr(candidate, "needs_conversion", False)),
output_limit=getattr(candidate, "output_limit", None),
)
upstream_is_stream = bool(upstream_request.upstream_is_stream)
provider_request_body = dict(upstream_request.payload or {})
provider_request_headers = {
str(k).lower(): str(v)
for k, v in dict(upstream_request.headers or {}).items()
if str(k).strip() and str(v).strip()
}
prompt_cache_key = str(provider_request_body.get("prompt_cache_key") or "").strip() or None
auth_header = ""
auth_value = ""
for header_name, header_value in provider_request_headers.items():
if header_name in {"authorization", "x-api-key", "x-goog-api-key"}:
auth_header = header_name
auth_value = header_value
break
if not auth_header:
auth_header, auth_type = get_auth_config_for_endpoint(provider_api_format)
decrypted_key = crypto_service.decrypt(key.api_key)
auth_value = f"Bearer {decrypted_key}" if auth_type == "bearer" else decrypted_key
effective_proxy = resolve_effective_proxy(provider.proxy, getattr(key, "proxy", None))
proxy_info = await resolve_proxy_info_async(effective_proxy)
delegate_cfg = await resolve_delegate_config_async(effective_proxy)
is_tunnel_delegate = bool(delegate_cfg and delegate_cfg.get("tunnel"))
effective_proxy_for_contract = effective_proxy
if not effective_proxy_for_contract or not effective_proxy_for_contract.get("enabled", True):
effective_proxy_for_contract = await get_system_proxy_config_async()
proxy_url: str | None = None
if effective_proxy_for_contract and not is_tunnel_delegate:
proxy_url = await build_proxy_url_async(effective_proxy_for_contract)
proxy_snapshot = ExecutionProxySnapshot.from_proxy_info(
proxy_info,
proxy_url=proxy_url,
mode_override="tunnel" if is_tunnel_delegate else None,
node_id_override=(
str(delegate_cfg.get("node_id") or "").strip() or None if is_tunnel_delegate else None
),
)
request_timeout = provider.request_timeout or config.http_request_timeout
timeouts = ExecutionPlanTimeouts(
connect_ms=int(config.http_connect_timeout * 1000),
read_ms=int(config.http_read_timeout * 1000),
write_ms=int(config.http_write_timeout * 1000),
pool_ms=int(config.http_pool_timeout * 1000),
total_ms=int(request_timeout * 1000),
)
requires_finalize = (
upstream_is_stream
or bool(getattr(candidate, "needs_conversion", False))
or provider_api_format != client_api_format
or upstream_request.envelope is not None
)
has_envelope = upstream_request.envelope is not None
needs_conversion = bool(getattr(candidate, "needs_conversion", False))
decision_extra_headers: dict[str, str] = {}
decision_provider_request_headers = provider_request_headers or None
decision_provider_request_body = provider_request_body
report_context = {
"user_id": str(user.id),
"api_key_id": str(api_key.id),
"request_id": str(context.request_id),
"candidate_id": str(
getattr(candidate, "request_candidate_id", "") or getattr(candidate, "id", "") or ""
)
or None,
"model": str(model or "unknown"),
"provider_name": str(provider.name or "unknown"),
"provider_id": str(provider.id),
"endpoint_id": str(endpoint.id),
"key_id": str(key.id),
"provider_api_format": provider_api_format,
"client_api_format": client_api_format,
"mapped_model": str(mapped_model or "").strip() or None,
"original_headers": dict(context.original_headers),
"original_request_body": original_request_body,
"proxy_info": proxy_info,
"has_envelope": has_envelope,
"envelope_name": gateway_module._gateway_report_context_envelope_name(
upstream_request.envelope
),
"needs_conversion": needs_conversion,
}
report_context["provider_request_headers"] = provider_request_headers
report_context["provider_request_body"] = provider_request_body
is_compact = decision.route_kind == "compact"
return GatewayExecutionDecisionResponse(
action="executor_sync_decision",
decision_kind="openai_compact_sync" if is_compact else "openai_cli_sync",
request_id=str(context.request_id),
candidate_id=str(
getattr(candidate, "request_candidate_id", "") or getattr(candidate, "id", "") or ""
)
or None,
provider_name=str(provider.name),
provider_id=str(provider.id),
endpoint_id=str(endpoint.id),
key_id=str(key.id),
upstream_base_url=str(getattr(endpoint, "base_url", "") or "").strip(),
upstream_url=str(upstream_request.url or "").strip() or None,
auth_header=str(auth_header or "").strip() or "authorization",
auth_value=str(auth_value or "").strip(),
provider_api_format=provider_api_format,
client_api_format=client_api_format,
model_name=str(model or "unknown"),
mapped_model=str(mapped_model or "").strip() or None,
prompt_cache_key=prompt_cache_key,
extra_headers=decision_extra_headers,
provider_request_headers=decision_provider_request_headers,
provider_request_body=decision_provider_request_body,
content_type=(
str(provider_request_headers.get("content-type") or "").strip() or "application/json"
),
proxy=gateway_module._serialize_gateway_sync_proxy(proxy_snapshot),
tls_profile=str(upstream_request.tls_profile or "").strip() or None,
timeouts=gateway_module._serialize_gateway_sync_timeouts(timeouts),
upstream_is_stream=upstream_is_stream or None,
report_kind=(
("openai_compact_sync_finalize" if is_compact else "openai_cli_sync_finalize")
if requires_finalize
else "openai_cli_sync_success"
),
report_context=report_context,
auth_context=auth_context,
)
async def _build_openai_cli_stream_decision(
*,
request: Request,
payload: GatewayExecuteRequest,
db: Session,
auth_context: GatewayAuthContext,
decision: GatewayRouteDecision,
) -> GatewayExecutionDecisionResponse | None:
is_compact = decision.route_kind == "compact"
return await _build_cli_stream_decision(
request=request,
payload=payload,
db=db,
auth_context=auth_context,
decision=decision,
expected_api_format="openai:compact" if is_compact else "openai:cli",
decision_kind="openai_compact_stream" if is_compact else "openai_cli_stream",
report_kind="openai_cli_stream_success",
)
async def _build_claude_cli_sync_decision(
*,
request: Request,
payload: GatewayExecuteRequest,
db: Session,
auth_context: GatewayAuthContext,
decision: GatewayRouteDecision,
) -> GatewayExecutionDecisionResponse | None:
return await _build_cli_sync_decision(
request=request,
payload=payload,
db=db,
auth_context=auth_context,
decision=decision,
expected_api_format="claude:cli",
decision_kind="claude_cli_sync",
report_kind="claude_cli_sync_success",
finalize_kind="claude_cli_sync_finalize",
)
async def _build_claude_cli_stream_decision(
*,
request: Request,
payload: GatewayExecuteRequest,
db: Session,
auth_context: GatewayAuthContext,
decision: GatewayRouteDecision,
) -> GatewayExecutionDecisionResponse | None:
return await _build_cli_stream_decision(
request=request,
payload=payload,
db=db,
auth_context=auth_context,
decision=decision,
expected_api_format="claude:cli",
decision_kind="claude_cli_stream",
report_kind="claude_cli_stream_success",
)
async def _build_gemini_cli_sync_decision(
*,
request: Request,
payload: GatewayExecuteRequest,
db: Session,
auth_context: GatewayAuthContext,
decision: GatewayRouteDecision,
) -> GatewayExecutionDecisionResponse | None:
return await _build_cli_sync_decision(
request=request,
payload=payload,
db=db,
auth_context=auth_context,
decision=decision,
expected_api_format="gemini:cli",
decision_kind="gemini_cli_sync",
report_kind="gemini_cli_sync_success",
finalize_kind="gemini_cli_sync_finalize",
)
async def _build_gemini_cli_stream_decision(
*,
request: Request,
payload: GatewayExecuteRequest,
db: Session,
auth_context: GatewayAuthContext,
decision: GatewayRouteDecision,
) -> GatewayExecutionDecisionResponse | None:
return await _build_cli_stream_decision(
request=request,
payload=payload,
db=db,
auth_context=auth_context,
decision=decision,
expected_api_format="gemini:cli",
decision_kind="gemini_cli_stream",
report_kind="gemini_cli_stream_success",
)

View File

@@ -0,0 +1,681 @@
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_cli_stream_plan(
*,
request: Request,
payload: GatewayExecuteRequest,
db: Session,
auth_context: GatewayAuthContext,
decision: GatewayRouteDecision,
expected_provider_formats: set[str],
plan_kind: str,
report_kind: str,
log_label: str,
) -> GatewayExecutionPlanResponse | None:
from loguru import logger
from src.api.handlers.base.cli_adapter_base import CliAdapterBase
from src.config.settings import config
from src.services.provider.transport import redact_url_for_log
from src.services.proxy_node.resolver import (
build_proxy_url_async,
resolve_delegate_config_async,
resolve_effective_proxy,
resolve_proxy_info_async,
)
from src.services.request.execution_runtime_plan import (
ExecutionPlan,
ExecutionPlanTimeouts,
ExecutionProxySnapshot,
build_execution_plan_body,
is_remote_execution_runtime_contract_eligible,
)
if str(payload.method or "").strip().upper() != "POST":
return None
adapter, path_params = gateway_module._resolve_gateway_sync_adapter(decision, payload.path)
if not isinstance(adapter, CliAdapterBase):
return None
if not gateway_module._is_stream_request_payload(payload.body_json, path_params):
return None
user, api_key = gateway_module._load_gateway_auth_models(db, auth_context)
pipeline = gateway_module.get_pipeline()
await pipeline._check_user_rate_limit(request, db, user, api_key)
context = gateway_module._build_gateway_request_context(
request=request,
payload=payload,
db=db,
user=user,
api_key=api_key,
adapter=adapter,
path_params=path_params,
balance_remaining=auth_context.balance_remaining,
)
authorize_result = adapter.authorize(context)
if hasattr(authorize_result, "__await__"):
await authorize_result
original_request_body = await context.ensure_json_body_async()
if context.path_params:
original_request_body = adapter._merge_path_params(
original_request_body,
context.path_params,
)
handler = adapter.HANDLER_CLASS(
db=db,
user=user,
api_key=api_key,
request_id=context.request_id,
client_ip=context.client_ip,
user_agent=context.user_agent,
start_time=context.start_time,
allowed_api_formats=adapter.allowed_api_formats,
adapter_detector=adapter.detect_capability_requirements,
perf_metrics=context.extra.get("perf"),
api_family=adapter.API_FAMILY.value if adapter.API_FAMILY else None,
endpoint_kind=adapter.ENDPOINT_KIND.value if adapter.ENDPOINT_KIND else None,
)
model = handler.extract_model_from_request(original_request_body, context.path_params)
client_api_format = handler.primary_api_format
capability_requirements = handler._resolve_capability_requirements(
model_name=model,
request_headers=context.original_headers,
request_body=original_request_body,
)
preferred_key_ids = await handler._resolve_preferred_key_ids(
model_name=model,
request_body=original_request_body,
)
candidate = await gateway_module._select_gateway_direct_candidate(
db=db,
redis_client=getattr(handler, "redis", None),
api_format=str(client_api_format),
model_name=str(model or "unknown"),
user_api_key=api_key,
request_id=context.request_id,
is_stream=True,
capability_requirements=capability_requirements or None,
preferred_key_ids=preferred_key_ids or None,
request_body=original_request_body,
)
if candidate is None:
return None
provider = candidate.provider
endpoint = candidate.endpoint
key = candidate.key
provider_api_format = str(endpoint.api_format or "")
mapped_model = candidate.mapping_matched_model if candidate else None
if not mapped_model:
mapped_model = await handler._get_mapped_model(
source_model=str(model or "unknown"),
provider_id=str(provider.id),
)
needs_conversion = bool(getattr(candidate, "needs_conversion", False))
upstream_request = await handler._build_upstream_request(
provider=provider,
endpoint=endpoint,
key=key,
request_body=dict(original_request_body),
original_headers=context.original_headers,
query_params=context.query_params,
client_api_format=str(client_api_format),
provider_api_format=provider_api_format,
fallback_model=str(model or "unknown"),
mapped_model=mapped_model,
client_is_stream=True,
needs_conversion=needs_conversion,
output_limit=candidate.output_limit if candidate else None,
)
effective_proxy = resolve_effective_proxy(provider.proxy, getattr(key, "proxy", None))
stream_proxy_info = await resolve_proxy_info_async(effective_proxy)
delegate_cfg = await resolve_delegate_config_async(effective_proxy)
is_tunnel_delegate = bool(delegate_cfg and delegate_cfg.get("tunnel"))
proxy_url: str | None = None
if effective_proxy and not is_tunnel_delegate:
proxy_url = await build_proxy_url_async(effective_proxy)
contract = ExecutionPlan(
request_id=str(context.request_id or ""),
candidate_id=str(
getattr(candidate, "request_candidate_id", "") or getattr(candidate, "id", "") or ""
)
or None,
provider_name=str(provider.name),
provider_id=str(provider.id),
endpoint_id=str(endpoint.id),
key_id=str(key.id),
method="POST",
url=upstream_request.url,
headers=dict(upstream_request.headers),
body=build_execution_plan_body(
upstream_request.payload,
content_type=str(upstream_request.headers.get("content-type") or "").strip() or None,
),
stream=True,
provider_api_format=provider_api_format,
client_api_format=str(client_api_format),
model_name=str(model or ""),
content_type=str(upstream_request.headers.get("content-type") or "").strip() or None,
content_encoding=context.client_content_encoding,
proxy=ExecutionProxySnapshot.from_proxy_info(
stream_proxy_info,
proxy_url=proxy_url,
mode_override="tunnel" if is_tunnel_delegate else None,
node_id_override=(
str(delegate_cfg.get("node_id") or "").strip() or None
if is_tunnel_delegate
else None
),
),
tls_profile=upstream_request.tls_profile,
timeouts=ExecutionPlanTimeouts(
connect_ms=int(config.http_connect_timeout * 1000),
read_ms=int(config.http_read_timeout * 1000),
write_ms=int(config.http_write_timeout * 1000),
pool_ms=int(config.http_pool_timeout * 1000),
total_ms=None,
),
)
if (
not upstream_request.upstream_is_stream
or gateway_module._stream_executor_requires_python_rewrite(
envelope=upstream_request.envelope,
needs_conversion=needs_conversion,
provider_api_format=provider_api_format,
client_api_format=str(client_api_format),
)
or not is_remote_execution_runtime_contract_eligible(contract)
or provider_api_format != str(client_api_format)
or provider_api_format not in expected_provider_formats
):
return None
logger.debug(
"[gateway] {} stream direct executor candidate accepted: path={} provider={} url={}",
log_label,
payload.path,
provider.name,
redact_url_for_log(upstream_request.url),
)
return GatewayExecutionPlanResponse(
action="executor_stream",
plan_kind=plan_kind,
plan=contract.to_payload(),
report_kind=report_kind,
report_context={
"user_id": str(user.id),
"api_key_id": str(api_key.id),
"request_id": str(context.request_id),
"model": str(model or "unknown"),
"provider_name": str(contract.provider_name or "unknown"),
"provider_id": str(contract.provider_id or ""),
"endpoint_id": str(contract.endpoint_id or ""),
"key_id": str(contract.key_id or ""),
"candidate_id": str(contract.candidate_id or ""),
"provider_api_format": str(contract.provider_api_format or ""),
"client_api_format": str(contract.client_api_format or ""),
"mapped_model": mapped_model,
"original_headers": dict(context.original_headers),
"original_request_body": original_request_body,
"provider_request_headers": dict(upstream_request.headers),
"provider_request_body": upstream_request.payload,
"proxy_info": stream_proxy_info,
"has_envelope": upstream_request.envelope is not None,
"envelope_name": gateway_module._gateway_report_context_envelope_name(
upstream_request.envelope
),
"needs_conversion": bool(needs_conversion),
},
)
async def _build_cli_sync_plan(
*,
request: Request,
payload: GatewayExecuteRequest,
db: Session,
auth_context: GatewayAuthContext,
decision: GatewayRouteDecision,
expected_client_formats: set[str],
plan_kind: str,
report_kind: str,
finalize_kind: str,
log_label: str,
) -> GatewayExecutionPlanResponse | None:
from loguru import logger
from src.api.handlers.base.cli_adapter_base import CliAdapterBase
from src.config.settings import config
from src.services.provider.transport import redact_url_for_log
from src.services.proxy_node.resolver import (
build_proxy_url_async,
resolve_delegate_config_async,
resolve_effective_proxy,
resolve_proxy_info_async,
)
from src.services.request.execution_runtime_plan import (
ExecutionPlan,
ExecutionPlanTimeouts,
ExecutionProxySnapshot,
build_execution_plan_body,
is_remote_execution_runtime_contract_eligible,
)
if str(payload.method or "").strip().upper() != "POST":
return None
adapter, path_params = gateway_module._resolve_gateway_sync_adapter(decision, payload.path)
if not isinstance(adapter, CliAdapterBase):
return None
if gateway_module._is_stream_request_payload(payload.body_json, path_params):
return None
user, api_key = gateway_module._load_gateway_auth_models(db, auth_context)
pipeline = gateway_module.get_pipeline()
await pipeline._check_user_rate_limit(request, db, user, api_key)
context = gateway_module._build_gateway_request_context(
request=request,
payload=payload,
db=db,
user=user,
api_key=api_key,
adapter=adapter,
path_params=path_params,
balance_remaining=auth_context.balance_remaining,
)
authorize_result = adapter.authorize(context)
if hasattr(authorize_result, "__await__"):
await authorize_result
original_request_body = await context.ensure_json_body_async()
if context.path_params:
original_request_body = adapter._merge_path_params(
original_request_body,
context.path_params,
)
if decision.route_kind == "compact":
original_request_body.pop("stream", None)
handler = adapter.HANDLER_CLASS(
db=db,
user=user,
api_key=api_key,
request_id=context.request_id,
client_ip=context.client_ip,
user_agent=context.user_agent,
start_time=context.start_time,
allowed_api_formats=adapter.allowed_api_formats,
adapter_detector=adapter.detect_capability_requirements,
perf_metrics=context.extra.get("perf"),
api_family=adapter.API_FAMILY.value if adapter.API_FAMILY else None,
endpoint_kind=adapter.ENDPOINT_KIND.value if adapter.ENDPOINT_KIND else None,
)
model = handler.extract_model_from_request(original_request_body, context.path_params)
client_api_format = handler.primary_api_format
capability_requirements = handler._resolve_capability_requirements(
model_name=model,
request_headers=context.original_headers,
request_body=original_request_body,
)
preferred_key_ids = await handler._resolve_preferred_key_ids(
model_name=model,
request_body=original_request_body,
)
candidate = await gateway_module._select_gateway_direct_candidate(
db=db,
redis_client=getattr(handler, "redis", None),
api_format=str(client_api_format),
model_name=str(model or "unknown"),
user_api_key=api_key,
request_id=context.request_id,
is_stream=False,
capability_requirements=capability_requirements or None,
preferred_key_ids=preferred_key_ids or None,
request_body=original_request_body,
)
if candidate is None:
return None
provider = candidate.provider
endpoint = candidate.endpoint
key = candidate.key
provider_api_format = str(endpoint.api_format or "")
mapped_model = candidate.mapping_matched_model if candidate else None
if not mapped_model:
mapped_model = await handler._get_mapped_model(
source_model=str(model or "unknown"),
provider_id=str(provider.id),
)
needs_conversion = bool(getattr(candidate, "needs_conversion", False))
upstream_request = await handler._build_upstream_request(
provider=provider,
endpoint=endpoint,
key=key,
request_body=dict(original_request_body),
original_headers=context.original_headers,
query_params=context.query_params,
client_api_format=str(client_api_format),
provider_api_format=provider_api_format,
fallback_model=str(model or "unknown"),
mapped_model=mapped_model,
client_is_stream=False,
needs_conversion=needs_conversion,
output_limit=candidate.output_limit if candidate else None,
)
_effective_proxy = resolve_effective_proxy(provider.proxy, getattr(key, "proxy", None))
sync_proxy_info = await resolve_proxy_info_async(_effective_proxy)
delegate_cfg = await resolve_delegate_config_async(_effective_proxy)
is_tunnel_delegate = bool(delegate_cfg and delegate_cfg.get("tunnel"))
proxy_url: str | None = None
if _effective_proxy and not is_tunnel_delegate:
proxy_url = await build_proxy_url_async(_effective_proxy)
contract = ExecutionPlan(
request_id=str(context.request_id or ""),
candidate_id=str(
getattr(candidate, "request_candidate_id", "") or getattr(candidate, "id", "") or ""
)
or None,
provider_name=str(provider.name),
provider_id=str(provider.id),
endpoint_id=str(endpoint.id),
key_id=str(key.id),
method="POST",
url=upstream_request.url,
headers=dict(upstream_request.headers),
body=build_execution_plan_body(
upstream_request.payload,
content_type=str(upstream_request.headers.get("content-type") or "").strip() or None,
),
stream=upstream_request.upstream_is_stream,
provider_api_format=provider_api_format,
client_api_format=str(client_api_format),
model_name=str(model or ""),
content_type=str(upstream_request.headers.get("content-type") or "").strip() or None,
content_encoding=context.client_content_encoding,
proxy=ExecutionProxySnapshot.from_proxy_info(
sync_proxy_info,
proxy_url=proxy_url,
mode_override="tunnel" if is_tunnel_delegate else None,
node_id_override=(
str(delegate_cfg.get("node_id") or "").strip() or None
if is_tunnel_delegate
else None
),
),
tls_profile=upstream_request.tls_profile,
timeouts=ExecutionPlanTimeouts(
connect_ms=int(config.http_connect_timeout * 1000),
read_ms=int(config.http_read_timeout * 1000),
write_ms=int(config.http_write_timeout * 1000),
pool_ms=int(config.http_pool_timeout * 1000),
total_ms=int((provider.request_timeout or config.http_request_timeout) * 1000),
),
)
normalized_client_api_format = str(client_api_format)
if (
not is_remote_execution_runtime_contract_eligible(contract)
or normalized_client_api_format not in expected_client_formats
):
return None
selected_report_kind = (
finalize_kind
if (
upstream_request.upstream_is_stream
or needs_conversion
or upstream_request.envelope is not None
or provider_api_format != normalized_client_api_format
)
else report_kind
)
logger.debug(
"[gateway] {} sync direct executor candidate accepted: path={} provider={} url={}",
log_label,
payload.path,
provider.name,
redact_url_for_log(upstream_request.url),
)
return GatewayExecutionPlanResponse(
action="executor_sync",
plan_kind=plan_kind,
plan=contract.to_payload(),
report_kind=selected_report_kind,
report_context={
"user_id": str(user.id),
"api_key_id": str(api_key.id),
"request_id": str(context.request_id),
"model": str(model or "unknown"),
"provider_name": str(contract.provider_name or "unknown"),
"provider_id": str(contract.provider_id or ""),
"endpoint_id": str(contract.endpoint_id or ""),
"key_id": str(contract.key_id or ""),
"provider_api_format": str(contract.provider_api_format or ""),
"client_api_format": str(contract.client_api_format or ""),
"mapped_model": mapped_model,
"original_headers": dict(context.original_headers),
"original_request_body": original_request_body,
"provider_request_headers": dict(upstream_request.headers),
"provider_request_body": upstream_request.payload,
"proxy_info": sync_proxy_info,
"has_envelope": upstream_request.envelope is not None,
"envelope_name": gateway_module._gateway_report_context_envelope_name(
upstream_request.envelope
),
"needs_conversion": bool(needs_conversion),
},
)
async def _build_openai_cli_stream_plan(
*,
request: Request,
payload: GatewayExecuteRequest,
db: Session,
auth_context: GatewayAuthContext,
decision: GatewayRouteDecision,
) -> GatewayExecutionPlanResponse | None:
is_compact = decision.route_kind == "compact"
return await _build_cli_stream_plan(
request=request,
payload=payload,
db=db,
auth_context=auth_context,
decision=decision,
expected_provider_formats={"openai:compact" if is_compact else "openai:cli"},
plan_kind="openai_compact_stream" if is_compact else "openai_cli_stream",
report_kind="openai_cli_stream_success",
log_label="openai compact" if is_compact else "openai cli",
)
async def _build_claude_cli_stream_plan(
*,
request: Request,
payload: GatewayExecuteRequest,
db: Session,
auth_context: GatewayAuthContext,
decision: GatewayRouteDecision,
) -> GatewayExecutionPlanResponse | None:
return await _build_cli_stream_plan(
request=request,
payload=payload,
db=db,
auth_context=auth_context,
decision=decision,
expected_provider_formats={"claude:cli"},
plan_kind="claude_cli_stream",
report_kind="claude_cli_stream_success",
log_label="claude cli",
)
async def _build_gemini_cli_stream_plan(
*,
request: Request,
payload: GatewayExecuteRequest,
db: Session,
auth_context: GatewayAuthContext,
decision: GatewayRouteDecision,
) -> GatewayExecutionPlanResponse | None:
return await _build_cli_stream_plan(
request=request,
payload=payload,
db=db,
auth_context=auth_context,
decision=decision,
expected_provider_formats={"gemini:cli"},
plan_kind="gemini_cli_stream",
report_kind="gemini_cli_stream_success",
log_label="gemini cli",
)
async def _build_openai_cli_sync_plan(
*,
request: Request,
payload: GatewayExecuteRequest,
db: Session,
auth_context: GatewayAuthContext,
decision: GatewayRouteDecision,
) -> GatewayExecutionPlanResponse | None:
return await _build_cli_sync_plan(
request=request,
payload=payload,
db=db,
auth_context=auth_context,
decision=decision,
expected_client_formats={"openai:cli", "openai:compact"},
plan_kind="openai_compact_sync" if decision.route_kind == "compact" else "openai_cli_sync",
report_kind="openai_cli_sync_success",
finalize_kind=(
"openai_compact_sync_finalize"
if decision.route_kind == "compact"
else "openai_cli_sync_finalize"
),
log_label="openai cli",
)
async def _build_claude_cli_sync_plan(
*,
request: Request,
payload: GatewayExecuteRequest,
db: Session,
auth_context: GatewayAuthContext,
decision: GatewayRouteDecision,
) -> GatewayExecutionPlanResponse | None:
return await _build_cli_sync_plan(
request=request,
payload=payload,
db=db,
auth_context=auth_context,
decision=decision,
expected_client_formats={"claude:cli"},
plan_kind="claude_cli_sync",
report_kind="claude_cli_sync_success",
finalize_kind="claude_cli_sync_finalize",
log_label="claude cli",
)
async def _build_gemini_cli_sync_plan(
*,
request: Request,
payload: GatewayExecuteRequest,
db: Session,
auth_context: GatewayAuthContext,
decision: GatewayRouteDecision,
) -> GatewayExecutionPlanResponse | None:
return await _build_cli_sync_plan(
request=request,
payload=payload,
db=db,
auth_context=auth_context,
decision=decision,
expected_client_formats={"gemini:cli"},
plan_kind="gemini_cli_sync",
report_kind="gemini_cli_sync_success",
finalize_kind="gemini_cli_sync_finalize",
log_label="gemini cli",
)

View File

@@ -0,0 +1,277 @@
from __future__ import annotations
import re
from typing import Any
from pydantic import BaseModel, Field
_GEMINI_MODEL_ROUTE_RE = re.compile(
r"^/(v1|v1beta)/models/[^/]+:(generateContent|streamGenerateContent|predictLongRunning)$"
)
_GEMINI_MODEL_OPERATION_CANCEL_RE = re.compile(r"^/v1beta/models/[^/]+/operations/[^/]+:cancel$")
_GEMINI_OPERATION_CANCEL_RE = re.compile(r"^/v1beta/operations/[^/]+:cancel$")
_GEMINI_MODEL_OPERATION_ROUTE_RE = re.compile(r"^/v1beta/models/[^/]+/operations/[^/]+$")
_GEMINI_FILES_ROUTE_RE = re.compile(r"^/v1beta/files(?:/.+)?$")
_GEMINI_FILES_DOWNLOAD_ROUTE_RE = re.compile(r"^/v1beta/files/(?P<file_id>[^/]+):download$")
_GEMINI_FILES_RESOURCE_ROUTE_RE = re.compile(r"^/v1beta/files/(?P<file_name>.+)$")
_GEMINI_SYNC_ROUTE_RE = re.compile(
r"^/(?P<version>v1|v1beta)/models/(?P<model>[^/]+):(?P<action>generateContent|streamGenerateContent)$"
)
_OPENAI_VIDEO_CANCEL_ROUTE_RE = re.compile(r"^/v1/videos/(?P<task_id>[^/]+)/cancel$")
_OPENAI_VIDEO_REMIX_ROUTE_RE = re.compile(r"^/v1/videos/(?P<task_id>[^/]+)/remix$")
_OPENAI_VIDEO_CONTENT_ROUTE_RE = re.compile(r"^/v1/videos/(?P<task_id>[^/]+)/content$")
_OPENAI_VIDEO_TASK_ROUTE_RE = re.compile(r"^/v1/videos/(?P<task_id>[^/]+)$")
_GEMINI_VIDEO_CREATE_ROUTE_RE = re.compile(r"^/v1beta/models/(?P<model>[^/]+):predictLongRunning$")
_GEMINI_VIDEO_MODEL_OPERATION_ANY_RE = re.compile(
r"^/v1beta/models/(?P<model>[^/]+)/operations/(?P<operation_id>[^/]+)(?P<cancel>:cancel)?$"
)
CONTROL_EXECUTED_HEADER = "x-aether-control-executed"
CONTROL_ACTION_HEADER = "x-aether-control-action"
CONTROL_ACTION_PROXY_PUBLIC = "proxy_public"
class GatewayResolveRequest(BaseModel):
trace_id: str | None = Field(None, max_length=128)
method: str = Field(..., min_length=1, max_length=16)
path: str = Field(..., min_length=1, max_length=2048)
query_string: str | None = Field(None, max_length=8192)
headers: dict[str, str] = Field(default_factory=dict)
has_body: bool = False
content_type: str | None = Field(None, max_length=512)
content_length: int | None = Field(None, ge=0)
class GatewayAuthContextRequest(BaseModel):
trace_id: str | None = Field(None, max_length=128)
query_string: str | None = Field(None, max_length=8192)
headers: dict[str, str] = Field(default_factory=dict)
auth_endpoint_signature: str = Field(..., min_length=1, max_length=128)
class GatewayRouteDecision(BaseModel):
action: str = "proxy_public"
route_class: str
public_path: str
public_query_string: str | None = None
route_family: str | None = None
route_kind: str | None = None
auth_endpoint_signature: str | None = None
executor_candidate: bool = False
auth_context: dict[str, Any] | None = None
class GatewayAuthContext(BaseModel):
user_id: str
api_key_id: str
balance_remaining: float | None = None
access_allowed: bool = True
class GatewayExecuteRequest(BaseModel):
trace_id: str | None = Field(None, max_length=128)
method: str = Field(..., min_length=1, max_length=16)
path: str = Field(..., min_length=1, max_length=2048)
query_string: str | None = Field(None, max_length=8192)
headers: dict[str, str] = Field(default_factory=dict)
body_json: dict[str, Any] = Field(default_factory=dict)
body_base64: str | None = None
auth_context: GatewayAuthContext | None = None
class GatewayExecutionPlanResponse(BaseModel):
action: str
plan_kind: str
plan: dict[str, Any]
report_kind: str | None = None
report_context: dict[str, Any] | None = None
auth_context: GatewayAuthContext | None = None
class GatewayExecutionDecisionResponse(BaseModel):
action: str
decision_kind: str
request_id: str
candidate_id: str | None = None
provider_name: str
provider_id: str
endpoint_id: str
key_id: str
upstream_base_url: str
upstream_url: str | None = None
provider_request_method: str | None = None
auth_header: str
auth_value: str
provider_api_format: str
client_api_format: str
model_name: str
mapped_model: str | None = None
prompt_cache_key: str | None = None
extra_headers: dict[str, str] = Field(default_factory=dict)
provider_request_headers: dict[str, str] | None = None
provider_request_body: dict[str, Any] | None = None
content_type: str | None = None
proxy: dict[str, Any] | None = None
tls_profile: str | None = None
timeouts: dict[str, Any] | None = None
upstream_is_stream: bool | None = None
report_kind: str | None = None
report_context: dict[str, Any] | None = None
auth_context: GatewayAuthContext | None = None
class GatewaySyncReportRequest(BaseModel):
trace_id: str | None = Field(None, max_length=128)
report_kind: str = Field(..., min_length=1, max_length=128)
report_context: dict[str, Any] = Field(default_factory=dict)
status_code: int = Field(..., ge=100, le=599)
headers: dict[str, str] = Field(default_factory=dict)
body_json: Any = None
client_body_json: Any = None
body_base64: str | None = None
telemetry: dict[str, Any] | None = None
class GatewayStreamReportRequest(BaseModel):
trace_id: str | None = Field(None, max_length=128)
report_kind: str = Field(..., min_length=1, max_length=128)
report_context: dict[str, Any] = Field(default_factory=dict)
status_code: int = Field(..., ge=100, le=599)
headers: dict[str, str] = Field(default_factory=dict)
body_base64: str | None = None
telemetry: dict[str, Any] | None = None
def classify_gateway_route(
method: str,
path: str,
headers: dict[str, str] | None = None,
) -> GatewayRouteDecision:
normalized_method = str(method or "").strip().upper() or "GET"
normalized_path = str(path or "").strip() or "/"
normalized_headers = {str(k).lower(): str(v) for k, v in (headers or {}).items()}
if not normalized_path.startswith("/"):
normalized_path = f"/{normalized_path}"
if normalized_method == "POST" and normalized_path == "/v1/chat/completions":
return _ai_route(
normalized_path,
family="openai",
kind="chat",
auth_endpoint_signature="openai:chat",
)
if normalized_method == "POST" and normalized_path in {
"/v1/responses",
"/v1/responses/compact",
}:
route_kind = "compact" if normalized_path.endswith("/compact") else "cli"
auth_endpoint_signature = "openai:compact" if route_kind == "compact" else "openai:cli"
return _ai_route(
normalized_path,
family="openai",
kind=route_kind,
auth_endpoint_signature=auth_endpoint_signature,
)
if normalized_method == "POST" and normalized_path == "/v1/messages":
is_claude_cli = _is_claude_cli_request(normalized_headers)
return _ai_route(
normalized_path,
family="claude",
kind="cli" if is_claude_cli else "chat",
auth_endpoint_signature="claude:cli" if is_claude_cli else "claude:chat",
)
if normalized_path.startswith("/v1/videos"):
return _ai_route(
normalized_path,
family="openai",
kind="video",
auth_endpoint_signature="openai:video",
)
if _GEMINI_MODEL_ROUTE_RE.match(normalized_path):
if normalized_path.endswith(":predictLongRunning"):
return _ai_route(
normalized_path,
family="gemini",
kind="video",
auth_endpoint_signature="gemini:video",
)
is_gemini_cli = _is_gemini_cli_request(normalized_headers)
return _ai_route(
normalized_path,
family="gemini",
kind="cli" if is_gemini_cli else "chat",
auth_endpoint_signature="gemini:cli" if is_gemini_cli else "gemini:chat",
)
if (
_GEMINI_MODEL_OPERATION_CANCEL_RE.match(normalized_path)
or _GEMINI_OPERATION_CANCEL_RE.match(normalized_path)
or _GEMINI_MODEL_OPERATION_ROUTE_RE.match(normalized_path)
or normalized_path == "/v1beta/operations"
or normalized_path.startswith("/v1beta/operations/")
):
return _ai_route(
normalized_path,
family="gemini",
kind="video",
auth_endpoint_signature="gemini:video",
)
if normalized_method == "POST" and normalized_path == "/upload/v1beta/files":
return _ai_route(
normalized_path,
family="gemini",
kind="files",
auth_endpoint_signature="gemini:chat",
)
if _GEMINI_FILES_ROUTE_RE.match(normalized_path):
return _ai_route(
normalized_path,
family="gemini",
kind="files",
auth_endpoint_signature="gemini:chat",
)
return GatewayRouteDecision(
route_class="passthrough",
public_path=normalized_path,
executor_candidate=False,
)
def _ai_route(
path: str,
*,
family: str,
kind: str,
auth_endpoint_signature: str,
) -> GatewayRouteDecision:
return GatewayRouteDecision(
route_class="ai_public",
public_path=path,
route_family=family,
route_kind=kind,
auth_endpoint_signature=auth_endpoint_signature,
executor_candidate=True,
)
def _is_claude_cli_request(headers: dict[str, str]) -> bool:
auth_header = str(headers.get("authorization") or "").strip().lower()
has_bearer = auth_header.startswith("bearer ")
has_api_key = bool(str(headers.get("x-api-key") or "").strip())
return has_bearer and not has_api_key
def _is_gemini_cli_request(headers: dict[str, str]) -> bool:
x_app = str(headers.get("x-app") or "").lower()
if "cli" in x_app:
return True
user_agent = str(headers.get("user-agent") or "").lower()
return "geminicli" in user_agent or "gemini-cli" in user_agent

View File

@@ -0,0 +1,853 @@
from __future__ import annotations
from typing import Any
from urllib.parse import parse_qsl
from fastapi import Request
from sqlalchemy.orm import Session
from src.core.api_format.headers import extract_client_api_key_for_endpoint_with_query
from src.services.auth.service import AuthService
from .gateway_contract import (
_GEMINI_FILES_DOWNLOAD_ROUTE_RE,
_GEMINI_FILES_RESOURCE_ROUTE_RE,
_GEMINI_MODEL_OPERATION_CANCEL_RE,
_GEMINI_OPERATION_CANCEL_RE,
_GEMINI_VIDEO_CREATE_ROUTE_RE,
_OPENAI_VIDEO_CANCEL_ROUTE_RE,
_OPENAI_VIDEO_CONTENT_ROUTE_RE,
_OPENAI_VIDEO_REMIX_ROUTE_RE,
_OPENAI_VIDEO_TASK_ROUTE_RE,
GatewayAuthContext,
GatewayExecuteRequest,
GatewayExecutionDecisionResponse,
GatewayExecutionPlanResponse,
GatewayResolveRequest,
GatewayRouteDecision,
classify_gateway_route,
)
def _gateway_module() -> Any:
from . import gateway as gateway_module
return gateway_module
def _parse_query_string(query_string: str | None) -> dict[str, str]:
return {
str(key): str(value) for key, value in parse_qsl(query_string or "", keep_blank_values=True)
}
async def _resolve_auth_context(
payload: GatewayResolveRequest,
decision: GatewayRouteDecision,
) -> dict[str, Any] | None:
gateway_module = _gateway_module()
return await gateway_module._resolve_auth_context_signature(
headers=payload.headers,
query_string=payload.query_string,
auth_endpoint_signature=str(decision.auth_endpoint_signature or ""),
)
async def _resolve_auth_context_signature(
*,
headers: dict[str, str] | None,
query_string: str | None,
auth_endpoint_signature: str,
) -> dict[str, Any] | None:
normalized_signature = str(auth_endpoint_signature or "").strip().lower()
if not normalized_signature:
return None
client_api_key = extract_client_api_key_for_endpoint_with_query(
headers or {},
_parse_query_string(query_string),
normalized_signature,
)
if not client_api_key:
return None
auth_result = await AuthService.authenticate_api_key_threadsafe(client_api_key)
if not auth_result or not auth_result.user or not auth_result.api_key:
return None
return GatewayAuthContext(
user_id=str(auth_result.user.id),
api_key_id=str(auth_result.api_key.id),
balance_remaining=auth_result.balance_remaining,
access_allowed=bool(auth_result.access_allowed),
).model_dump(exclude_none=True)
async def _resolve_gateway_execute_auth_context(
*,
payload: GatewayExecuteRequest,
decision: GatewayRouteDecision,
) -> GatewayAuthContext | None:
gateway_module = _gateway_module()
if payload.auth_context is not None:
return payload.auth_context
resolved = await gateway_module._resolve_auth_context_signature(
headers=payload.headers,
query_string=payload.query_string,
auth_endpoint_signature=str(decision.auth_endpoint_signature or ""),
)
if not resolved:
return None
return GatewayAuthContext.model_validate(resolved)
def _resolve_gateway_sync_adapter(
decision: GatewayRouteDecision,
path: str,
) -> tuple[Any | None, dict[str, Any]]:
gateway_module = _gateway_module()
if decision.route_class != "ai_public":
return None, {}
family = str(decision.route_family or "").strip().lower()
kind = str(decision.route_kind or "").strip().lower()
if family == "openai" and kind == "chat":
from src.api.handlers.openai import OpenAIChatAdapter
return OpenAIChatAdapter(), {}
if family == "openai" and kind == "cli":
from src.api.handlers.openai_cli import OpenAICliAdapter
return OpenAICliAdapter(), {}
if family == "openai" and kind == "compact":
from src.api.handlers.openai_cli import OpenAICompactAdapter
return OpenAICompactAdapter(), {}
if family == "claude" and kind == "chat":
from src.api.handlers.claude.adapter import ClaudeChatAdapter
return ClaudeChatAdapter(), {}
if family == "claude" and kind == "cli":
from src.api.handlers.claude_cli import ClaudeCliAdapter
return ClaudeCliAdapter(), {}
if family == "gemini" and kind == "chat":
from src.api.handlers.gemini.adapter import GeminiChatAdapter
return GeminiChatAdapter(), gateway_module._extract_gemini_path_params(path)
if family == "gemini" and kind == "cli":
from src.api.handlers.gemini_cli import GeminiCliAdapter
return GeminiCliAdapter(), gateway_module._extract_gemini_path_params(path)
if family == "openai" and kind == "video":
from src.api.handlers.openai.video_adapter import OpenAIVideoAdapter
return OpenAIVideoAdapter(), gateway_module._extract_openai_video_path_params(path)
if family == "gemini" and kind == "video":
from src.api.handlers.gemini.video_adapter import GeminiVeoAdapter
return GeminiVeoAdapter(), gateway_module._extract_gemini_video_path_params(path)
return None, {}
async def _build_gateway_stream_plan_response(
*,
request: Request,
payload: GatewayExecuteRequest,
db: Session,
) -> GatewayExecutionPlanResponse | None:
gateway_module = _gateway_module()
decision = classify_gateway_route(payload.method, payload.path, payload.headers)
auth_context = await gateway_module._resolve_gateway_execute_auth_context(
payload=payload,
decision=decision,
)
if auth_context is None or not auth_context.access_allowed:
return None
if decision.route_family == "openai" and decision.route_kind == "chat":
planned = await gateway_module._build_openai_chat_stream_plan(
request=request,
payload=payload,
db=db,
auth_context=auth_context,
decision=decision,
)
if planned is not None:
planned.auth_context = auth_context
return planned
if decision.route_family == "claude" and decision.route_kind == "chat":
planned = await gateway_module._build_claude_chat_stream_plan(
request=request,
payload=payload,
db=db,
auth_context=auth_context,
decision=decision,
)
if planned is not None:
planned.auth_context = auth_context
return planned
if decision.route_family == "gemini" and decision.route_kind == "chat":
planned = await gateway_module._build_gemini_chat_stream_plan(
request=request,
payload=payload,
db=db,
auth_context=auth_context,
decision=decision,
)
if planned is not None:
planned.auth_context = auth_context
return planned
if decision.route_family == "openai" and decision.route_kind in {"cli", "compact"}:
planned = await gateway_module._build_openai_cli_stream_plan(
request=request,
payload=payload,
db=db,
auth_context=auth_context,
decision=decision,
)
if planned is not None:
planned.auth_context = auth_context
return planned
if decision.route_family == "claude" and decision.route_kind == "cli":
planned = await gateway_module._build_claude_cli_stream_plan(
request=request,
payload=payload,
db=db,
auth_context=auth_context,
decision=decision,
)
if planned is not None:
planned.auth_context = auth_context
return planned
if decision.route_family == "gemini" and decision.route_kind == "cli":
planned = await gateway_module._build_gemini_cli_stream_plan(
request=request,
payload=payload,
db=db,
auth_context=auth_context,
decision=decision,
)
if planned is not None:
planned.auth_context = auth_context
return planned
if decision.route_family == "gemini" and decision.route_kind == "files":
download_match = _GEMINI_FILES_DOWNLOAD_ROUTE_RE.match(str(payload.path or "").strip())
if not download_match or str(payload.method or "").strip().upper() != "GET":
return None
gateway_request = gateway_module._build_gateway_forward_request(
request=request, payload=payload
)
user, api_key = gateway_module._load_gateway_auth_models(db, auth_context)
pipeline = gateway_module.get_pipeline()
await pipeline._check_user_rate_limit(gateway_request, db, user, api_key)
plan = await gateway_module._build_gemini_files_download_stream_plan(
request=gateway_request,
db=db,
user=user,
user_api_key=api_key,
file_id=download_match.group("file_id"),
)
return GatewayExecutionPlanResponse(
action="executor_stream",
plan_kind="gemini_files_download",
plan=plan,
auth_context=auth_context,
)
if decision.route_family == "openai" and decision.route_kind == "video":
content_match = _OPENAI_VIDEO_CONTENT_ROUTE_RE.match(str(payload.path or "").strip())
if not content_match or str(payload.method or "").strip().upper() != "GET":
return None
gateway_request = gateway_module._build_gateway_forward_request(
request=request, payload=payload
)
user, api_key = gateway_module._load_gateway_auth_models(db, auth_context)
pipeline = gateway_module.get_pipeline()
await pipeline._check_user_rate_limit(gateway_request, db, user, api_key)
plan = await gateway_module._build_openai_video_content_stream_plan(
request=gateway_request,
db=db,
user=user,
user_api_key=api_key,
task_id=content_match.group("task_id"),
)
return GatewayExecutionPlanResponse(
action="executor_stream",
plan_kind="openai_video_content",
plan=plan,
auth_context=auth_context,
)
return None
async def _build_gateway_stream_decision_response(
*,
request: Request,
payload: GatewayExecuteRequest,
db: Session,
auth_context: GatewayAuthContext,
decision: GatewayRouteDecision,
) -> GatewayExecutionDecisionResponse | None:
gateway_module = _gateway_module()
if decision.route_family == "openai" and decision.route_kind == "chat":
return await gateway_module._build_openai_chat_stream_decision(
request=request,
payload=payload,
db=db,
auth_context=auth_context,
decision=decision,
)
if decision.route_family == "claude" and decision.route_kind == "chat":
return await gateway_module._build_claude_chat_stream_decision(
request=request,
payload=payload,
db=db,
auth_context=auth_context,
decision=decision,
)
if decision.route_family == "gemini" and decision.route_kind == "chat":
return await gateway_module._build_gemini_chat_stream_decision(
request=request,
payload=payload,
db=db,
auth_context=auth_context,
decision=decision,
)
if decision.route_family == "openai" and decision.route_kind in {"cli", "compact"}:
return await gateway_module._build_openai_cli_stream_decision(
request=request,
payload=payload,
db=db,
auth_context=auth_context,
decision=decision,
)
if decision.route_family == "claude" and decision.route_kind == "cli":
return await gateway_module._build_claude_cli_stream_decision(
request=request,
payload=payload,
db=db,
auth_context=auth_context,
decision=decision,
)
if decision.route_family == "gemini" and decision.route_kind == "cli":
return await gateway_module._build_gemini_cli_stream_decision(
request=request,
payload=payload,
db=db,
auth_context=auth_context,
decision=decision,
)
if decision.route_family == "gemini" and decision.route_kind == "files":
normalized_path = str(payload.path or "").strip()
download_match = _GEMINI_FILES_DOWNLOAD_ROUTE_RE.match(normalized_path)
if str(payload.method or "").strip().upper() == "GET" and download_match:
gateway_request = gateway_module._build_gateway_forward_request(
request=request, payload=payload
)
user, api_key = gateway_module._load_gateway_auth_models(db, auth_context)
return await gateway_module._build_gemini_files_download_stream_decision(
request=gateway_request,
db=db,
user=user,
user_api_key=api_key,
file_id=str(download_match.group("file_id") or "").strip(),
)
if decision.route_family == "openai" and decision.route_kind == "video":
normalized_path = str(payload.path or "").strip()
content_match = _OPENAI_VIDEO_CONTENT_ROUTE_RE.match(normalized_path)
if str(payload.method or "").strip().upper() == "GET" and content_match:
gateway_request = gateway_module._build_gateway_forward_request(
request=request, payload=payload
)
user, api_key = gateway_module._load_gateway_auth_models(db, auth_context)
return await gateway_module._build_openai_video_content_stream_decision(
request=gateway_request,
db=db,
user=user,
user_api_key=api_key,
task_id=str(content_match.group("task_id") or "").strip(),
)
return None
async def _build_gateway_sync_plan_response(
*,
request: Request,
payload: GatewayExecuteRequest,
db: Session,
) -> GatewayExecutionPlanResponse | None:
gateway_module = _gateway_module()
decision = classify_gateway_route(payload.method, payload.path, payload.headers)
auth_context = await gateway_module._resolve_gateway_execute_auth_context(
payload=payload,
decision=decision,
)
if auth_context is None or not auth_context.access_allowed:
return None
if decision.route_family == "openai" and decision.route_kind == "chat":
planned = await gateway_module._build_openai_chat_sync_plan(
request=request,
payload=payload,
db=db,
auth_context=auth_context,
decision=decision,
)
if planned is not None:
planned.auth_context = auth_context
return planned
if decision.route_family == "claude" and decision.route_kind == "chat":
planned = await gateway_module._build_claude_chat_sync_plan(
request=request,
payload=payload,
db=db,
auth_context=auth_context,
decision=decision,
)
if planned is not None:
planned.auth_context = auth_context
return planned
if decision.route_family == "gemini" and decision.route_kind == "chat":
planned = await gateway_module._build_gemini_chat_sync_plan(
request=request,
payload=payload,
db=db,
auth_context=auth_context,
decision=decision,
)
if planned is not None:
planned.auth_context = auth_context
return planned
if decision.route_family == "openai" and decision.route_kind in {"cli", "compact"}:
planned = await gateway_module._build_openai_cli_sync_plan(
request=request,
payload=payload,
db=db,
auth_context=auth_context,
decision=decision,
)
if planned is not None:
planned.auth_context = auth_context
return planned
if decision.route_family == "claude" and decision.route_kind == "cli":
planned = await gateway_module._build_claude_cli_sync_plan(
request=request,
payload=payload,
db=db,
auth_context=auth_context,
decision=decision,
)
if planned is not None:
planned.auth_context = auth_context
return planned
if decision.route_family == "gemini" and decision.route_kind == "cli":
planned = await gateway_module._build_gemini_cli_sync_plan(
request=request,
payload=payload,
db=db,
auth_context=auth_context,
decision=decision,
)
if planned is not None:
planned.auth_context = auth_context
return planned
if decision.route_family == "openai" and decision.route_kind == "video":
normalized_path = str(payload.path or "").strip()
normalized_method = str(payload.method or "").strip().upper()
if normalized_method == "POST" and normalized_path == "/v1/videos":
planned = await gateway_module._build_openai_video_create_sync_plan(
request=request,
payload=payload,
db=db,
auth_context=auth_context,
decision=decision,
)
if planned is not None:
planned.auth_context = auth_context
return planned
remix_match = _OPENAI_VIDEO_REMIX_ROUTE_RE.match(normalized_path)
if normalized_method == "POST" and remix_match:
planned = await gateway_module._build_openai_video_remix_sync_plan(
request=request,
payload=payload,
db=db,
auth_context=auth_context,
task_id=str(remix_match.group("task_id") or "").strip(),
)
if planned is not None:
planned.auth_context = auth_context
return planned
cancel_match = _OPENAI_VIDEO_CANCEL_ROUTE_RE.match(normalized_path)
if normalized_method == "POST" and cancel_match:
planned = await gateway_module._build_openai_video_cancel_sync_plan(
request=request,
payload=payload,
db=db,
auth_context=auth_context,
task_id=str(cancel_match.group("task_id") or "").strip(),
)
if planned is not None:
planned.auth_context = auth_context
return planned
task_match = _OPENAI_VIDEO_TASK_ROUTE_RE.match(normalized_path)
if normalized_method == "DELETE" and task_match:
planned = await gateway_module._build_openai_video_delete_sync_plan(
request=request,
payload=payload,
db=db,
auth_context=auth_context,
task_id=str(task_match.group("task_id") or "").strip(),
)
if planned is not None:
planned.auth_context = auth_context
return planned
if decision.route_family == "gemini" and decision.route_kind == "video":
normalized_path = str(payload.path or "").strip()
normalized_method = str(payload.method or "").strip().upper()
create_match = _GEMINI_VIDEO_CREATE_ROUTE_RE.match(normalized_path)
if normalized_method == "POST" and create_match:
planned = await gateway_module._build_gemini_video_create_sync_plan(
request=request,
payload=payload,
db=db,
auth_context=auth_context,
model=str(create_match.group("model") or "").strip(),
)
if planned is not None:
planned.auth_context = auth_context
return planned
if normalized_method == "POST" and (
_GEMINI_MODEL_OPERATION_CANCEL_RE.match(normalized_path)
or _GEMINI_OPERATION_CANCEL_RE.match(normalized_path)
):
task_id = str(
gateway_module._extract_gemini_video_path_params(normalized_path).get("task_id")
or ""
)
planned = await gateway_module._build_gemini_video_cancel_sync_plan(
request=request,
payload=payload,
db=db,
auth_context=auth_context,
task_id=task_id,
)
if planned is not None:
planned.auth_context = auth_context
return planned
if decision.route_family == "gemini" and decision.route_kind == "files":
resource_match = _GEMINI_FILES_RESOURCE_ROUTE_RE.match(str(payload.path or "").strip())
download_match = _GEMINI_FILES_DOWNLOAD_ROUTE_RE.match(str(payload.path or "").strip())
method = str(payload.method or "").strip().upper()
if method == "POST" and str(payload.path or "").strip() == "/upload/v1beta/files":
gateway_request = gateway_module._build_gateway_forward_request(
request=request, payload=payload
)
user, api_key = gateway_module._load_gateway_auth_models(db, auth_context)
pipeline = gateway_module.get_pipeline()
await pipeline._check_user_rate_limit(gateway_request, db, user, api_key)
plan, report_context = await gateway_module._build_gemini_files_proxy_sync_plan(
request=gateway_request,
db=db,
method="POST",
upstream_path="/v1beta/files",
is_upload=True,
)
return GatewayExecutionPlanResponse(
action="executor_sync",
plan_kind="gemini_files_upload",
plan=plan,
report_kind="gemini_files_store_mapping",
report_context=report_context,
auth_context=auth_context,
)
if method == "GET" and str(payload.path or "").strip() == "/v1beta/files":
gateway_request = gateway_module._build_gateway_forward_request(
request=request, payload=payload
)
user, api_key = gateway_module._load_gateway_auth_models(db, auth_context)
pipeline = gateway_module.get_pipeline()
await pipeline._check_user_rate_limit(gateway_request, db, user, api_key)
plan, report_context = await gateway_module._build_gemini_files_proxy_sync_plan(
request=gateway_request,
db=db,
method="GET",
upstream_path="/v1beta/files",
)
return GatewayExecutionPlanResponse(
action="executor_sync",
plan_kind="gemini_files_list",
plan=plan,
report_kind="gemini_files_store_mapping",
report_context=report_context,
auth_context=auth_context,
)
if (
not resource_match
or download_match
or str(payload.path or "").strip() == "/v1beta/files"
):
return None
gateway_request = gateway_module._build_gateway_forward_request(
request=request, payload=payload
)
user, api_key = gateway_module._load_gateway_auth_models(db, auth_context)
pipeline = gateway_module.get_pipeline()
await pipeline._check_user_rate_limit(gateway_request, db, user, api_key)
file_name = resource_match.group("file_name")
if method == "GET":
plan, report_context = await gateway_module._build_gemini_files_get_sync_plan(
request=gateway_request,
db=db,
file_name=file_name,
)
return GatewayExecutionPlanResponse(
action="executor_sync",
plan_kind="gemini_files_get",
plan=plan,
report_kind="gemini_files_store_mapping",
report_context=report_context,
auth_context=auth_context,
)
if method == "DELETE":
normalized_file_name = (
file_name if str(file_name or "").startswith("files/") else f"files/{file_name}"
)
plan, _report_context = await gateway_module._build_gemini_files_proxy_sync_plan(
request=gateway_request,
db=db,
method="DELETE",
upstream_path=f"/v1beta/{normalized_file_name}",
)
return GatewayExecutionPlanResponse(
action="executor_sync",
plan_kind="gemini_files_delete",
plan=plan,
report_kind="gemini_files_delete_mapping",
report_context={"file_name": normalized_file_name},
auth_context=auth_context,
)
return None
async def _build_gateway_sync_decision_response(
*,
request: Request,
payload: GatewayExecuteRequest,
db: Session,
auth_context: GatewayAuthContext,
decision: GatewayRouteDecision,
) -> GatewayExecutionDecisionResponse | None:
gateway_module = _gateway_module()
if decision.route_family == "openai" and decision.route_kind == "chat":
return await gateway_module._build_openai_chat_sync_decision(
request=request,
payload=payload,
db=db,
auth_context=auth_context,
decision=decision,
)
if decision.route_family == "openai" and decision.route_kind in {"cli", "compact"}:
return await gateway_module._build_openai_cli_sync_decision(
request=request,
payload=payload,
db=db,
auth_context=auth_context,
decision=decision,
)
if decision.route_family == "claude" and decision.route_kind == "chat":
return await gateway_module._build_claude_chat_sync_decision(
request=request,
payload=payload,
db=db,
auth_context=auth_context,
decision=decision,
)
if decision.route_family == "claude" and decision.route_kind == "cli":
return await gateway_module._build_claude_cli_sync_decision(
request=request,
payload=payload,
db=db,
auth_context=auth_context,
decision=decision,
)
if decision.route_family == "gemini" and decision.route_kind == "chat":
return await gateway_module._build_gemini_chat_sync_decision(
request=request,
payload=payload,
db=db,
auth_context=auth_context,
decision=decision,
)
if decision.route_family == "gemini" and decision.route_kind == "cli":
return await gateway_module._build_gemini_cli_sync_decision(
request=request,
payload=payload,
db=db,
auth_context=auth_context,
decision=decision,
)
if decision.route_family == "openai" and decision.route_kind == "video":
normalized_path = str(payload.path or "").strip()
normalized_method = str(payload.method or "").strip().upper()
if normalized_method == "POST" and normalized_path == "/v1/videos":
return await gateway_module._build_openai_video_create_sync_decision(
request=request,
payload=payload,
db=db,
auth_context=auth_context,
decision=decision,
)
remix_match = _OPENAI_VIDEO_REMIX_ROUTE_RE.match(normalized_path)
if normalized_method == "POST" and remix_match:
return await gateway_module._build_openai_video_remix_sync_decision(
request=request,
payload=payload,
db=db,
auth_context=auth_context,
task_id=str(remix_match.group("task_id") or "").strip(),
)
cancel_match = _OPENAI_VIDEO_CANCEL_ROUTE_RE.match(normalized_path)
if normalized_method == "POST" and cancel_match:
return await gateway_module._build_openai_video_cancel_sync_decision(
request=request,
payload=payload,
db=db,
auth_context=auth_context,
task_id=str(cancel_match.group("task_id") or "").strip(),
)
task_match = _OPENAI_VIDEO_TASK_ROUTE_RE.match(normalized_path)
if normalized_method == "DELETE" and task_match:
return await gateway_module._build_openai_video_delete_sync_decision(
request=request,
payload=payload,
db=db,
auth_context=auth_context,
task_id=str(task_match.group("task_id") or "").strip(),
)
if decision.route_family == "gemini" and decision.route_kind == "video":
normalized_path = str(payload.path or "").strip()
normalized_method = str(payload.method or "").strip().upper()
create_match = _GEMINI_VIDEO_CREATE_ROUTE_RE.match(normalized_path)
if normalized_method == "POST" and create_match:
return await gateway_module._build_gemini_video_create_sync_decision(
request=request,
payload=payload,
db=db,
auth_context=auth_context,
model=str(create_match.group("model") or "").strip(),
)
if normalized_method == "POST" and (
_GEMINI_MODEL_OPERATION_CANCEL_RE.match(normalized_path)
or _GEMINI_OPERATION_CANCEL_RE.match(normalized_path)
):
task_id = str(
gateway_module._extract_gemini_video_path_params(normalized_path).get("task_id")
or ""
)
return await gateway_module._build_gemini_video_cancel_sync_decision(
request=request,
payload=payload,
db=db,
auth_context=auth_context,
task_id=task_id,
)
if decision.route_family == "gemini" and decision.route_kind == "files":
normalized_path = str(payload.path or "").strip()
method = str(payload.method or "").strip().upper()
resource_match = _GEMINI_FILES_RESOURCE_ROUTE_RE.match(normalized_path)
download_match = _GEMINI_FILES_DOWNLOAD_ROUTE_RE.match(normalized_path)
gateway_request = gateway_module._build_gateway_forward_request(
request=request, payload=payload
)
user, api_key = gateway_module._load_gateway_auth_models(db, auth_context)
pipeline = gateway_module.get_pipeline()
await pipeline._check_user_rate_limit(gateway_request, db, user, api_key)
if method == "GET" and normalized_path == "/v1beta/files":
return await gateway_module._build_gemini_files_proxy_sync_decision(
request=gateway_request,
db=db,
method="GET",
upstream_path="/v1beta/files",
decision_kind="gemini_files_list",
report_kind="gemini_files_store_mapping",
)
if (
method == "GET"
and resource_match
and not download_match
and normalized_path != "/v1beta/files"
):
file_name = str(resource_match.group("file_name") or "").strip()
return await gateway_module._build_gemini_files_proxy_sync_decision(
request=gateway_request,
db=db,
method="GET",
upstream_path=f"/v1beta/{file_name if file_name.startswith('files/') else f'files/{file_name}'}",
decision_kind="gemini_files_get",
report_kind="gemini_files_store_mapping",
)
if (
method == "DELETE"
and resource_match
and not download_match
and normalized_path != "/v1beta/files"
):
file_name = str(resource_match.group("file_name") or "").strip()
normalized_file_name = (
file_name if file_name.startswith("files/") else f"files/{file_name}"
)
return await gateway_module._build_gemini_files_proxy_sync_decision(
request=gateway_request,
db=db,
method="DELETE",
upstream_path=f"/v1beta/{normalized_file_name}",
decision_kind="gemini_files_delete",
report_kind="gemini_files_delete_mapping",
report_context={"file_name": normalized_file_name},
)
return None

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 ""),
}

View File

@@ -0,0 +1,41 @@
"""Compatibility re-export layer for gateway finalize helpers."""
from __future__ import annotations
from .gateway_finalize_chat import (
_finalize_gateway_chat_sync,
_run_gateway_chat_sync_finalize_background,
_run_gateway_chat_sync_finalize_background_with_session,
)
from .gateway_finalize_cli import (
_finalize_gateway_cli_sync,
_run_gateway_cli_sync_finalize_background,
_run_gateway_cli_sync_finalize_background_with_session,
)
from .gateway_finalize_common import (
_build_gateway_embedded_error_payload,
_build_gateway_sync_error_payload,
_extract_gateway_report_body_bytes,
_extract_gateway_sync_error_message,
_finalize_gateway_sync_response,
_gateway_module,
_resolve_gateway_finalize_db,
_resolve_gateway_sync_error_status_code,
)
__all__ = [
"_build_gateway_embedded_error_payload",
"_build_gateway_sync_error_payload",
"_extract_gateway_report_body_bytes",
"_extract_gateway_sync_error_message",
"_finalize_gateway_chat_sync",
"_finalize_gateway_cli_sync",
"_finalize_gateway_sync_response",
"_gateway_module",
"_resolve_gateway_finalize_db",
"_resolve_gateway_sync_error_status_code",
"_run_gateway_chat_sync_finalize_background",
"_run_gateway_chat_sync_finalize_background_with_session",
"_run_gateway_cli_sync_finalize_background",
"_run_gateway_cli_sync_finalize_background_with_session",
]

View File

@@ -0,0 +1,360 @@
from __future__ import annotations
import base64
import inspect
import time
import uuid
from typing import Any
from fastapi import BackgroundTasks, HTTPException, Request
from fastapi.responses import JSONResponse, Response
from sqlalchemy.orm import Session
from src.core.logger import logger
from src.database import create_session, get_db
from src.models.database import ApiKey, User
from .gateway_contract import GatewaySyncReportRequest
from .gateway_finalize_common import _gateway_module
async def _run_gateway_chat_sync_finalize_background(
payload: GatewaySyncReportRequest,
db: Session,
) -> None:
gateway_module = _gateway_module()
try:
await gateway_module._finalize_gateway_chat_sync(
payload,
db=db,
background_tasks=None,
allow_fast_path=False,
)
except Exception as exc:
logger.warning("gateway background chat finalize failed: {}", exc)
async def _run_gateway_chat_sync_finalize_background_with_session(
payload: GatewaySyncReportRequest,
) -> None:
gateway_module = _gateway_module()
db = create_session()
try:
await gateway_module._run_gateway_chat_sync_finalize_background(payload, db)
finally:
gateway_module._close_gateway_session(db)
async def _finalize_gateway_chat_sync(
payload: GatewaySyncReportRequest,
*,
db: Session,
background_tasks: BackgroundTasks | None = None,
allow_fast_path: bool = True,
) -> Response:
from src.api.handlers.base.chat_sync_executor import ChatSyncExecutor
from src.api.handlers.base.parsers import get_parser_for_format
from src.api.handlers.base.stream_context import is_format_converted
from src.api.handlers.base.utils import (
build_json_response_for_client,
filter_proxy_response_headers,
get_format_converter_registry,
resolve_client_accept_encoding,
)
from src.api.handlers.claude import ClaudeChatAdapter
from src.api.handlers.gemini import GeminiChatAdapter
from src.api.handlers.openai import OpenAIChatAdapter
from src.core.exceptions import EmbeddedErrorException
from src.models.database import Provider, ProviderAPIKey, ProviderEndpoint
from src.services.scheduling.schemas import ProviderCandidate
gateway_module = _gateway_module()
context = dict(payload.report_context or {})
user_id = str(context.get("user_id") or "").strip()
api_key_id = str(context.get("api_key_id") or "").strip()
provider_id = str(context.get("provider_id") or "").strip()
endpoint_id = str(context.get("endpoint_id") or "").strip()
key_id = str(context.get("key_id") or "").strip()
request_id = str(context.get("request_id") or payload.trace_id or uuid.uuid4().hex[:8]).strip()
model = str(context.get("model") or "unknown").strip() or "unknown"
provider_api_format = str(context.get("provider_api_format") or "").strip().lower()
client_api_format = str(context.get("client_api_format") or "").strip().lower()
if not all([user_id, api_key_id, provider_id, endpoint_id, key_id, client_api_format]):
raise HTTPException(status_code=400, detail="Missing gateway chat finalize context")
if allow_fast_path and background_tasks is not None:
fast_response = await gateway_module._maybe_build_gateway_core_sync_fast_success_response(
payload
)
if fast_response is not None:
background_tasks.add_task(
gateway_module._run_gateway_chat_sync_finalize_background,
payload.model_copy(deep=True),
db,
)
return fast_response
user = db.query(User).filter(User.id == user_id).first()
api_key = db.query(ApiKey).filter(ApiKey.id == api_key_id).first()
provider = db.query(Provider).filter(Provider.id == provider_id).first()
endpoint = db.query(ProviderEndpoint).filter(ProviderEndpoint.id == endpoint_id).first()
key = db.query(ProviderAPIKey).filter(ProviderAPIKey.id == key_id).first()
if not user or not api_key or not provider or not endpoint or not key:
raise HTTPException(status_code=400, detail="Invalid gateway chat finalize context")
if client_api_format == "claude:chat":
adapter = ClaudeChatAdapter()
elif client_api_format == "gemini:chat":
adapter = GeminiChatAdapter()
else:
adapter = OpenAIChatAdapter()
handler = adapter._create_handler(
db=db,
user=user,
api_key=api_key,
request_id=request_id,
client_ip="127.0.0.1",
user_agent=str(
(context.get("original_headers") or {}).get("user-agent") or "aether-gateway"
),
start_time=time.time(),
api_family=adapter.API_FAMILY.value if adapter.API_FAMILY else None,
endpoint_kind=adapter.ENDPOINT_KIND.value if adapter.ENDPOINT_KIND else None,
)
original_headers = dict(context.get("original_headers") or {})
original_request_body = dict(context.get("original_request_body") or {})
mapped_model = str(context.get("mapped_model") or "").strip() or None
candidate = ProviderCandidate(
provider=provider,
endpoint=endpoint,
key=key,
mapping_matched_model=mapped_model,
needs_conversion=is_format_converted(provider_api_format, client_api_format),
provider_api_format=provider_api_format,
)
prep = await handler._prepare_provider_request(
model=model,
provider=provider,
endpoint=endpoint,
key=key,
working_request_body=dict(original_request_body),
original_headers=original_headers,
client_api_format=client_api_format,
provider_api_format=provider_api_format,
candidate=candidate,
client_is_stream=False,
)
response_json: dict[str, Any] | None = None
provider_response_json: dict[str, Any] | None = None
if prep.upstream_is_stream and payload.body_base64 and payload.status_code < 400:
sync_executor = ChatSyncExecutor(handler)
sync_executor._ctx.provider_api_format_for_error = provider_api_format
sync_executor._ctx.client_api_format_for_error = client_api_format
sync_executor._ctx.needs_conversion_for_error = bool(prep.needs_conversion)
sync_executor._ctx.mapped_model_result = mapped_model
try:
response_json = await sync_executor._finalize_rust_stream_sync_result(
prepared_plan=prep,
provider=provider,
model=model,
response_body_bytes=gateway_module._extract_gateway_report_body_bytes(payload),
)
if isinstance(sync_executor._ctx.provider_response_json, dict):
provider_response_json = dict(sync_executor._ctx.provider_response_json)
except EmbeddedErrorException as exc:
payload = payload.model_copy(
update={
"status_code": int(exc.error_code or 400),
"body_json": gateway_module._build_gateway_embedded_error_payload(exc),
"body_base64": None,
}
)
except Exception as exc:
raise HTTPException(
status_code=502,
detail="Invalid upstream chat stream response",
) from exc
provider_error_parser = get_parser_for_format(provider_api_format or client_api_format or "")
is_error_response = payload.status_code >= 400
if isinstance(payload.body_json, dict):
try:
is_error_response = is_error_response or provider_error_parser.is_error_response(
dict(payload.body_json)
)
except Exception:
is_error_response = is_error_response or payload.body_json.get("error") is not None
client_accept_encoding = resolve_client_accept_encoding(original_headers, None)
if is_error_response:
error_status_code = gateway_module._resolve_gateway_sync_error_status_code(
payload,
provider_parser=provider_error_parser,
)
error_payload = gateway_module._build_gateway_sync_error_payload(
payload,
client_api_format=client_api_format,
provider_api_format=provider_api_format or client_api_format,
needs_conversion=bool(prep.needs_conversion),
)
client_response_headers = filter_proxy_response_headers(dict(payload.headers or {}))
client_response_headers["content-type"] = "application/json"
client_response = build_json_response_for_client(
status_code=error_status_code,
content=error_payload,
headers=client_response_headers,
client_accept_encoding=client_accept_encoding,
)
request_metadata: dict[str, Any] = {
"gateway_direct_executor": True,
"phase": "3c_trial",
}
proxy_info = context.get("proxy_info")
if isinstance(proxy_info, dict):
request_metadata["proxy"] = proxy_info
telemetry_writer = gateway_module._build_gateway_sync_telemetry_writer(
db=db,
request_id=request_id,
user_id=user_id,
api_key_id=api_key_id,
fallback_telemetry=handler.telemetry,
)
response_time_ms = 0
if isinstance(payload.telemetry, dict):
raw_elapsed_ms = payload.telemetry.get("elapsed_ms")
try:
response_time_ms = max(int(raw_elapsed_ms or 0), 0)
except (TypeError, ValueError):
response_time_ms = 0
await gateway_module._schedule_gateway_sync_telemetry(
background_tasks=background_tasks,
telemetry_writer=telemetry_writer,
operation="record_failure",
provider=str(context.get("provider_name") or provider.name or "unknown"),
model=model,
response_time_ms=response_time_ms,
status_code=error_status_code,
error_message=gateway_module._extract_gateway_sync_error_message(payload),
request_headers=original_headers,
request_body=original_request_body,
provider_request_body=context.get("provider_request_body"),
is_stream=False,
api_format=client_api_format,
api_family=adapter.API_FAMILY.value if adapter.API_FAMILY else None,
endpoint_kind=adapter.ENDPOINT_KIND.value if adapter.ENDPOINT_KIND else None,
provider_request_headers=dict(context.get("provider_request_headers") or {}),
response_headers=dict(payload.headers or {}),
client_response_headers=dict(client_response.headers),
provider_id=str(context.get("provider_id") or "") or None,
provider_endpoint_id=str(context.get("endpoint_id") or "") or None,
provider_api_key_id=str(context.get("key_id") or "") or None,
endpoint_api_format=provider_api_format or None,
has_format_conversion=is_format_converted(provider_api_format, client_api_format),
target_model=mapped_model,
metadata=request_metadata,
)
return client_response
if response_json is None:
if not isinstance(payload.body_json, dict):
raise HTTPException(status_code=502, detail="Invalid upstream chat response")
response_json = dict(payload.body_json)
if prep.envelope:
response_json = prep.envelope.unwrap_response(response_json)
prep.envelope.postprocess_unwrapped_response(model=model, data=response_json)
if prep.needs_conversion:
provider_response_json = dict(response_json)
registry = get_format_converter_registry()
response_json = registry.convert_response(
response_json,
provider_api_format,
client_api_format,
requested_model=model,
)
response_json = handler._normalize_response(response_json)
extract_usage = getattr(handler, "_extract_usage", None)
if callable(extract_usage):
usage_info = extract_usage(response_json)
else:
parser = getattr(handler, "parser", None)
parser_extract_usage = getattr(parser, "extract_usage_from_response", None)
usage_info = parser_extract_usage(response_json) if callable(parser_extract_usage) else {}
extract_response_metadata = getattr(handler, "_extract_response_metadata", None)
response_metadata = (
extract_response_metadata(response_json) if callable(extract_response_metadata) else None
)
client_response_headers = filter_proxy_response_headers(dict(payload.headers or {}))
client_response_headers["content-type"] = "application/json"
client_response = build_json_response_for_client(
status_code=payload.status_code,
content=response_json,
headers=client_response_headers,
client_accept_encoding=client_accept_encoding,
)
request_metadata: dict[str, Any] = {
"gateway_direct_executor": True,
"phase": "3c_trial",
}
proxy_info = context.get("proxy_info")
if isinstance(proxy_info, dict):
request_metadata["proxy"] = proxy_info
telemetry_writer = gateway_module._build_gateway_sync_telemetry_writer(
db=db,
request_id=request_id,
user_id=user_id,
api_key_id=api_key_id,
fallback_telemetry=handler.telemetry,
)
response_time_ms = 0
if isinstance(payload.telemetry, dict):
raw_elapsed_ms = payload.telemetry.get("elapsed_ms")
try:
response_time_ms = max(int(raw_elapsed_ms or 0), 0)
except (TypeError, ValueError):
response_time_ms = 0
await gateway_module._schedule_gateway_sync_telemetry(
background_tasks=background_tasks,
telemetry_writer=telemetry_writer,
operation="record_success",
provider=str(context.get("provider_name") or provider.name or "unknown"),
model=model,
input_tokens=int(usage_info.get("input_tokens", 0) or 0),
output_tokens=int(usage_info.get("output_tokens", 0) or 0),
response_time_ms=response_time_ms,
status_code=payload.status_code,
request_headers=original_headers,
request_body=original_request_body,
response_headers=dict(payload.headers or {}),
client_response_headers=dict(client_response.headers),
response_body=provider_response_json or response_json,
client_response_body=response_json if provider_response_json else None,
provider_request_headers=dict(context.get("provider_request_headers") or {}),
provider_request_body=context.get("provider_request_body"),
is_stream=False,
provider_id=str(context.get("provider_id") or "") or None,
provider_endpoint_id=str(context.get("endpoint_id") or "") or None,
provider_api_key_id=str(context.get("key_id") or "") or None,
api_format=client_api_format,
api_family=adapter.API_FAMILY.value if adapter.API_FAMILY else None,
endpoint_kind=adapter.ENDPOINT_KIND.value if adapter.ENDPOINT_KIND else None,
endpoint_api_format=provider_api_format or None,
has_format_conversion=is_format_converted(provider_api_format, client_api_format),
target_model=mapped_model,
metadata=gateway_module._build_gateway_usage_metadata(
request_metadata=request_metadata,
response_metadata=response_metadata if response_metadata else None,
),
)
return client_response

View File

@@ -0,0 +1,366 @@
from __future__ import annotations
import base64
import inspect
import time
import uuid
from typing import Any
from fastapi import BackgroundTasks, HTTPException, Request
from fastapi.responses import JSONResponse, Response
from sqlalchemy.orm import Session
from src.core.logger import logger
from src.database import create_session, get_db
from src.models.database import ApiKey, User
from .gateway_contract import GatewaySyncReportRequest
from .gateway_finalize_common import _gateway_module
async def _run_gateway_cli_sync_finalize_background(
payload: GatewaySyncReportRequest,
db: Session,
) -> None:
gateway_module = _gateway_module()
try:
await gateway_module._finalize_gateway_cli_sync(
payload,
db=db,
background_tasks=None,
allow_fast_path=False,
)
except Exception as exc:
logger.warning("gateway background cli finalize failed: {}", exc)
async def _run_gateway_cli_sync_finalize_background_with_session(
payload: GatewaySyncReportRequest,
) -> None:
gateway_module = _gateway_module()
db = create_session()
try:
await gateway_module._run_gateway_cli_sync_finalize_background(payload, db)
finally:
gateway_module._close_gateway_session(db)
async def _finalize_gateway_cli_sync(
payload: GatewaySyncReportRequest,
*,
db: Session,
background_tasks: BackgroundTasks | None = None,
allow_fast_path: bool = True,
) -> Response:
from src.api.handlers.base.parsers import get_parser_for_format
from src.api.handlers.base.stream_context import is_format_converted
from src.api.handlers.base.utils import (
build_json_response_for_client,
filter_proxy_response_headers,
get_format_converter_registry,
resolve_client_accept_encoding,
)
from src.api.handlers.claude_cli import ClaudeCliAdapter
from src.api.handlers.gemini_cli import GeminiCliAdapter
from src.api.handlers.openai_cli import OpenAICliAdapter, OpenAICompactAdapter
from src.core.exceptions import EmbeddedErrorException
from src.models.database import Provider, ProviderAPIKey, ProviderEndpoint
from src.services.scheduling.schemas import ProviderCandidate
gateway_module = _gateway_module()
context = dict(payload.report_context or {})
user_id = str(context.get("user_id") or "").strip()
api_key_id = str(context.get("api_key_id") or "").strip()
provider_id = str(context.get("provider_id") or "").strip()
endpoint_id = str(context.get("endpoint_id") or "").strip()
key_id = str(context.get("key_id") or "").strip()
request_id = str(context.get("request_id") or payload.trace_id or uuid.uuid4().hex[:8]).strip()
model = str(context.get("model") or "unknown").strip() or "unknown"
provider_api_format = str(context.get("provider_api_format") or "").strip().lower()
client_api_format = str(context.get("client_api_format") or "").strip().lower()
if not all([user_id, api_key_id, provider_id, endpoint_id, key_id, client_api_format]):
raise HTTPException(status_code=400, detail="Missing gateway CLI finalize context")
if allow_fast_path and background_tasks is not None:
fast_response = await gateway_module._maybe_build_gateway_core_sync_fast_success_response(
payload
)
if fast_response is not None:
background_tasks.add_task(
gateway_module._run_gateway_cli_sync_finalize_background,
payload.model_copy(deep=True),
db,
)
return fast_response
user = db.query(User).filter(User.id == user_id).first()
api_key = db.query(ApiKey).filter(ApiKey.id == api_key_id).first()
provider = db.query(Provider).filter(Provider.id == provider_id).first()
endpoint = db.query(ProviderEndpoint).filter(ProviderEndpoint.id == endpoint_id).first()
key = db.query(ProviderAPIKey).filter(ProviderAPIKey.id == key_id).first()
if not user or not api_key or not provider or not endpoint or not key:
raise HTTPException(status_code=400, detail="Invalid gateway CLI finalize context")
if client_api_format == "openai:compact":
adapter = OpenAICompactAdapter()
elif client_api_format == "claude:cli":
adapter = ClaudeCliAdapter()
elif client_api_format == "gemini:cli":
adapter = GeminiCliAdapter()
else:
adapter = OpenAICliAdapter()
handler = adapter.HANDLER_CLASS(
db=db,
user=user,
api_key=api_key,
request_id=request_id,
client_ip="127.0.0.1",
user_agent=str(
(context.get("original_headers") or {}).get("user-agent") or "aether-gateway"
),
start_time=time.time(),
allowed_api_formats=adapter.allowed_api_formats,
adapter_detector=adapter.detect_capability_requirements,
api_family=adapter.API_FAMILY.value if adapter.API_FAMILY else None,
endpoint_kind=adapter.ENDPOINT_KIND.value if adapter.ENDPOINT_KIND else None,
)
original_headers = dict(context.get("original_headers") or {})
original_request_body = dict(context.get("original_request_body") or {})
mapped_model = str(context.get("mapped_model") or "").strip() or None
candidate = ProviderCandidate(
provider=provider,
endpoint=endpoint,
key=key,
mapping_matched_model=mapped_model,
needs_conversion=is_format_converted(provider_api_format, client_api_format),
provider_api_format=provider_api_format,
)
upstream_request = await handler._build_upstream_request(
provider=provider,
endpoint=endpoint,
key=key,
request_body=dict(original_request_body),
original_headers=original_headers,
query_params=None,
client_api_format=client_api_format,
provider_api_format=provider_api_format,
fallback_model=model,
mapped_model=mapped_model,
client_is_stream=False,
needs_conversion=bool(candidate.needs_conversion),
output_limit=None,
)
response_json: dict[str, Any] | None = None
provider_response_json: dict[str, Any] | None = None
if upstream_request.upstream_is_stream and payload.body_base64 and payload.status_code < 400:
try:
response_json = await handler._aggregate_upstream_stream_sync_response(
body_bytes=gateway_module._extract_gateway_report_body_bytes(payload),
provider_api_format=provider_api_format,
client_api_format=client_api_format,
provider_name=str(provider.name),
provider_type=str(getattr(provider, "provider_type", "") or "").lower(),
model=model,
request_id=request_id,
envelope=upstream_request.envelope,
)
except EmbeddedErrorException as exc:
payload = payload.model_copy(
update={
"status_code": int(exc.error_code or 400),
"body_json": gateway_module._build_gateway_embedded_error_payload(exc),
"body_base64": None,
}
)
except Exception as exc:
raise HTTPException(
status_code=502,
detail="Invalid upstream CLI stream response",
) from exc
provider_error_parser = get_parser_for_format(provider_api_format or client_api_format or "")
is_error_response = payload.status_code >= 400
if isinstance(payload.body_json, dict):
try:
is_error_response = is_error_response or provider_error_parser.is_error_response(
dict(payload.body_json)
)
except Exception:
is_error_response = is_error_response or payload.body_json.get("error") is not None
client_accept_encoding = resolve_client_accept_encoding(original_headers, None)
if is_error_response:
error_status_code = gateway_module._resolve_gateway_sync_error_status_code(
payload,
provider_parser=provider_error_parser,
)
error_payload = gateway_module._build_gateway_sync_error_payload(
payload,
client_api_format=client_api_format,
provider_api_format=provider_api_format or client_api_format,
needs_conversion=bool(candidate.needs_conversion),
)
client_response_headers = filter_proxy_response_headers(dict(payload.headers or {}))
client_response_headers["content-type"] = "application/json"
client_response = build_json_response_for_client(
status_code=error_status_code,
content=error_payload,
headers=client_response_headers,
client_accept_encoding=client_accept_encoding,
)
request_metadata: dict[str, Any] = {
"gateway_direct_executor": True,
"phase": "3c_trial",
}
proxy_info = context.get("proxy_info")
if isinstance(proxy_info, dict):
request_metadata["proxy"] = proxy_info
telemetry_writer = gateway_module._build_gateway_sync_telemetry_writer(
db=db,
request_id=request_id,
user_id=user_id,
api_key_id=api_key_id,
fallback_telemetry=handler.telemetry,
)
response_time_ms = 0
if isinstance(payload.telemetry, dict):
raw_elapsed_ms = payload.telemetry.get("elapsed_ms")
try:
response_time_ms = max(int(raw_elapsed_ms or 0), 0)
except (TypeError, ValueError):
response_time_ms = 0
await gateway_module._schedule_gateway_sync_telemetry(
background_tasks=background_tasks,
telemetry_writer=telemetry_writer,
operation="record_failure",
provider=str(context.get("provider_name") or provider.name or "unknown"),
model=model,
response_time_ms=response_time_ms,
status_code=error_status_code,
error_message=gateway_module._extract_gateway_sync_error_message(payload),
request_headers=original_headers,
request_body=original_request_body,
provider_request_body=context.get("provider_request_body"),
is_stream=False,
api_format=client_api_format,
api_family=adapter.API_FAMILY.value if adapter.API_FAMILY else None,
endpoint_kind=adapter.ENDPOINT_KIND.value if adapter.ENDPOINT_KIND else None,
provider_request_headers=dict(context.get("provider_request_headers") or {}),
response_headers=dict(payload.headers or {}),
client_response_headers=dict(client_response.headers),
provider_id=str(context.get("provider_id") or "") or None,
provider_endpoint_id=str(context.get("endpoint_id") or "") or None,
provider_api_key_id=str(context.get("key_id") or "") or None,
endpoint_api_format=provider_api_format or None,
has_format_conversion=is_format_converted(provider_api_format, client_api_format),
target_model=mapped_model,
metadata=request_metadata,
)
return client_response
if response_json is None:
if not isinstance(payload.body_json, dict):
raise HTTPException(status_code=502, detail="Invalid upstream CLI response")
response_json = dict(payload.body_json)
if upstream_request.envelope:
response_json = upstream_request.envelope.unwrap_response(response_json)
upstream_request.envelope.postprocess_unwrapped_response(
model=model, data=response_json
)
if candidate.needs_conversion:
provider_response_json = dict(response_json)
registry = get_format_converter_registry()
response_json = registry.convert_response(
response_json,
provider_api_format,
client_api_format,
requested_model=model,
)
response_json = handler._normalize_response(response_json)
extract_usage = getattr(handler, "_extract_usage", None)
if callable(extract_usage):
usage_info = extract_usage(response_json)
else:
parser = getattr(handler, "parser", None)
parser_extract_usage = getattr(parser, "extract_usage_from_response", None)
usage_info = parser_extract_usage(response_json) if callable(parser_extract_usage) else {}
extract_response_metadata = getattr(handler, "_extract_response_metadata", None)
response_metadata = (
extract_response_metadata(response_json) if callable(extract_response_metadata) else None
)
client_response_headers = filter_proxy_response_headers(dict(payload.headers or {}))
client_response_headers["content-type"] = "application/json"
client_response = build_json_response_for_client(
status_code=payload.status_code,
content=response_json,
headers=client_response_headers,
client_accept_encoding=client_accept_encoding,
)
request_metadata: dict[str, Any] = {
"gateway_direct_executor": True,
"phase": "3c_trial",
}
proxy_info = context.get("proxy_info")
if isinstance(proxy_info, dict):
request_metadata["proxy"] = proxy_info
telemetry_writer = gateway_module._build_gateway_sync_telemetry_writer(
db=db,
request_id=request_id,
user_id=user_id,
api_key_id=api_key_id,
fallback_telemetry=handler.telemetry,
)
response_time_ms = 0
if isinstance(payload.telemetry, dict):
raw_elapsed_ms = payload.telemetry.get("elapsed_ms")
try:
response_time_ms = max(int(raw_elapsed_ms or 0), 0)
except (TypeError, ValueError):
response_time_ms = 0
await gateway_module._schedule_gateway_sync_telemetry(
background_tasks=background_tasks,
telemetry_writer=telemetry_writer,
operation="record_success",
provider=str(context.get("provider_name") or provider.name or "unknown"),
model=model,
input_tokens=int(usage_info.get("input_tokens", 0) or 0),
output_tokens=int(usage_info.get("output_tokens", 0) or 0),
response_time_ms=response_time_ms,
status_code=payload.status_code,
request_headers=original_headers,
request_body=original_request_body,
response_headers=dict(payload.headers or {}),
client_response_headers=dict(client_response.headers),
response_body=provider_response_json or response_json,
client_response_body=response_json if provider_response_json else None,
provider_request_headers=dict(context.get("provider_request_headers") or {}),
provider_request_body=context.get("provider_request_body"),
is_stream=False,
provider_id=str(context.get("provider_id") or "") or None,
provider_endpoint_id=str(context.get("endpoint_id") or "") or None,
provider_api_key_id=str(context.get("key_id") or "") or None,
api_format=client_api_format,
api_family=adapter.API_FAMILY.value if adapter.API_FAMILY else None,
endpoint_kind=adapter.ENDPOINT_KIND.value if adapter.ENDPOINT_KIND else None,
endpoint_api_format=provider_api_format or None,
has_format_conversion=is_format_converted(provider_api_format, client_api_format),
target_model=mapped_model,
metadata=gateway_module._build_gateway_usage_metadata(
request_metadata=request_metadata,
response_metadata=response_metadata if response_metadata else None,
),
)
return client_response

View File

@@ -0,0 +1,248 @@
from __future__ import annotations
import base64
import inspect
import time
import uuid
from typing import Any
from fastapi import BackgroundTasks, HTTPException, Request
from fastapi.responses import JSONResponse, Response
from sqlalchemy.orm import Session
from src.core.logger import logger
from src.database import create_session, get_db
from src.models.database import ApiKey, User
from .gateway_contract import GatewaySyncReportRequest
def _gateway_module() -> Any:
from . import gateway as gateway_module
return gateway_module
async def _finalize_gateway_sync_response(
payload: GatewaySyncReportRequest,
*,
db: Session,
background_tasks: BackgroundTasks | None = None,
) -> Response:
gateway_module = _gateway_module()
if payload.report_kind == "openai_chat_sync_finalize":
return await gateway_module._finalize_gateway_chat_sync(
payload,
db=db,
background_tasks=background_tasks,
)
if payload.report_kind == "claude_chat_sync_finalize":
return await gateway_module._finalize_gateway_chat_sync(
payload,
db=db,
background_tasks=background_tasks,
)
if payload.report_kind == "gemini_chat_sync_finalize":
return await gateway_module._finalize_gateway_chat_sync(
payload,
db=db,
background_tasks=background_tasks,
)
if payload.report_kind == "openai_cli_sync_finalize":
return await gateway_module._finalize_gateway_cli_sync(
payload,
db=db,
background_tasks=background_tasks,
)
if payload.report_kind == "openai_compact_sync_finalize":
return await gateway_module._finalize_gateway_cli_sync(
payload,
db=db,
background_tasks=background_tasks,
)
if payload.report_kind == "claude_cli_sync_finalize":
return await gateway_module._finalize_gateway_cli_sync(
payload,
db=db,
background_tasks=background_tasks,
)
if payload.report_kind == "gemini_cli_sync_finalize":
return await gateway_module._finalize_gateway_cli_sync(
payload,
db=db,
background_tasks=background_tasks,
)
if payload.report_kind == "openai_video_create_sync_finalize":
return await gateway_module._finalize_gateway_openai_video_create_sync(
payload,
db=db,
background_tasks=background_tasks,
)
if payload.report_kind == "openai_video_remix_sync_finalize":
return await gateway_module._finalize_gateway_openai_video_remix_sync(payload, db=db)
if payload.report_kind == "gemini_video_create_sync_finalize":
return await gateway_module._finalize_gateway_gemini_video_create_sync(
payload,
db=db,
background_tasks=background_tasks,
)
if payload.report_kind == "openai_video_cancel_sync_finalize":
return await gateway_module._finalize_gateway_openai_video_cancel_sync(payload, db=db)
if payload.report_kind == "openai_video_delete_sync_finalize":
return await gateway_module._finalize_gateway_openai_video_delete_sync(payload, db=db)
if payload.report_kind == "gemini_video_cancel_sync_finalize":
return await gateway_module._finalize_gateway_gemini_video_cancel_sync(payload, db=db)
raise HTTPException(status_code=400, detail="Unsupported gateway sync finalize kind")
def _resolve_gateway_finalize_db(
request: Request,
) -> tuple[Any, Any | None]:
gateway_module = _gateway_module()
overrides = getattr(getattr(request, "app", None), "dependency_overrides", None)
if isinstance(overrides, dict):
override = overrides.get(get_db)
if callable(override):
override_value = override()
if inspect.isgenerator(override_value):
generator = override_value
db = next(generator)
def _cleanup_override_generator() -> None:
try:
next(generator)
except StopIteration:
return
except Exception as exc:
logger.warning(
"gateway finalize override generator cleanup failed: {}",
exc,
)
return db, _cleanup_override_generator
return override_value, None
db = create_session()
return db, lambda: gateway_module._close_gateway_session(db)
def _extract_gateway_report_body_bytes(payload: Any) -> bytes:
if payload.body_base64:
try:
return base64.b64decode(payload.body_base64, validate=True)
except Exception as exc:
raise HTTPException(
status_code=400, detail="Invalid gateway report body payload"
) from exc
if hasattr(payload, "body_json") and payload.body_json is not None:
return JSONResponse(content=payload.body_json).body
return b""
def _build_gateway_embedded_error_payload(exc: Exception) -> dict[str, Any]:
from src.core.error_utils import extract_client_error_message
from src.core.exceptions import EmbeddedErrorException
message = extract_client_error_message(exc)
payload: dict[str, Any] = {
"error": {
"message": message,
}
}
if isinstance(exc, EmbeddedErrorException):
if exc.error_message and str(exc.error_message).strip():
payload["error"]["message"] = str(exc.error_message).strip()
if exc.error_code is not None:
payload["error"]["code"] = int(exc.error_code)
if exc.error_status:
payload["error"]["status"] = str(exc.error_status)
return payload
def _extract_gateway_sync_error_message(payload: GatewaySyncReportRequest) -> str:
if isinstance(payload.body_json, dict):
error_obj = payload.body_json.get("error")
if isinstance(error_obj, dict):
for key in ("message", "detail", "status", "type", "code"):
value = error_obj.get(key)
if isinstance(value, str) and value.strip():
return value.strip()
elif isinstance(error_obj, str) and error_obj.strip():
return error_obj.strip()
for key in ("message", "detail", "status", "type"):
value = payload.body_json.get(key)
if isinstance(value, str) and value.strip():
return value.strip()
if payload.body_base64:
try:
body_text = base64.b64decode(payload.body_base64, validate=True).decode(
"utf-8", errors="replace"
)
if body_text.strip():
return body_text[:4000]
except Exception:
pass
return f"HTTP {payload.status_code}"
def _resolve_gateway_sync_error_status_code(
payload: GatewaySyncReportRequest,
*,
provider_parser: Any | None = None,
) -> int:
status_code = int(getattr(payload, "status_code", 0) or 0)
if 400 <= status_code < 600:
return status_code
if isinstance(payload.body_json, dict):
if provider_parser is not None:
try:
parsed = provider_parser.parse_response(dict(payload.body_json), status_code or 200)
embedded_status = getattr(parsed, "embedded_status_code", None)
if isinstance(embedded_status, int) and 100 <= embedded_status < 600:
return embedded_status
except Exception:
pass
error_obj = payload.body_json.get("error")
if isinstance(error_obj, dict):
for key in ("code", "status"):
value = error_obj.get(key)
if isinstance(value, int) and 100 <= value < 600:
return value
if isinstance(value, str) and value.isdigit():
parsed_value = int(value)
if 100 <= parsed_value < 600:
return parsed_value
return status_code if 400 <= status_code < 600 else 400
def _build_gateway_sync_error_payload(
payload: GatewaySyncReportRequest,
*,
client_api_format: str,
provider_api_format: str,
needs_conversion: bool,
) -> dict[str, Any]:
from src.api.handlers.base.chat_error_utils import (
_build_client_error_response_best_effort,
_convert_error_response_best_effort,
)
if isinstance(payload.body_json, dict):
if needs_conversion and provider_api_format and client_api_format:
return _convert_error_response_best_effort(
dict(payload.body_json),
provider_api_format,
client_api_format,
)
return dict(payload.body_json)
return _build_client_error_response_best_effort(
_extract_gateway_sync_error_message(payload),
client_api_format or provider_api_format or "openai:chat",
)

View File

@@ -0,0 +1,95 @@
"""Compatibility re-export layer for gateway reporting helpers."""
from __future__ import annotations
from .gateway_reporting_background import (
_apply_gateway_stream_report,
_apply_gateway_sync_report,
_close_gateway_session,
_gateway_sync_report_requires_inline,
_resolve_gateway_background_db,
_run_gateway_stream_report_background,
_run_gateway_stream_report_background_with_session,
_run_gateway_sync_report_background,
_run_gateway_sync_report_background_with_session,
_run_gateway_video_finalize_submitted_background,
)
from .gateway_reporting_candidates import (
_ensure_gateway_request_candidate,
_mark_gateway_sync_candidate_terminal_state,
_record_gateway_direct_candidate_graph,
)
from .gateway_reporting_common import _gateway_module
from .gateway_reporting_failures import (
_record_gateway_chat_sync_failure,
_record_gateway_cli_sync_failure,
_record_gateway_sync_failure,
_record_gateway_video_sync_failure,
_resolve_gateway_failure_adapter,
)
from .gateway_reporting_success_stream import (
_GatewayReportStreamContext,
_iter_gateway_report_body_chunks,
_record_gateway_openai_chat_stream_success,
_record_gateway_passthrough_chat_stream_success,
_record_gateway_passthrough_cli_stream_success,
)
from .gateway_reporting_success_sync import (
_postprocess_gateway_report_provider_response,
_record_gateway_gemini_video_cancel_sync_success,
_record_gateway_gemini_video_create_sync_success,
_record_gateway_openai_chat_sync_success,
_record_gateway_openai_video_cancel_sync_success,
_record_gateway_openai_video_create_sync_success,
_record_gateway_openai_video_delete_sync_success,
_record_gateway_openai_video_remix_sync_success,
_record_gateway_passthrough_chat_sync_success,
_record_gateway_passthrough_cli_sync_success,
)
from .gateway_reporting_telemetry import (
_build_gateway_sync_telemetry_writer,
_build_gateway_usage_metadata,
_dispatch_gateway_sync_telemetry,
_schedule_gateway_sync_telemetry,
)
__all__ = [
"_GatewayReportStreamContext",
"_apply_gateway_stream_report",
"_apply_gateway_sync_report",
"_build_gateway_sync_telemetry_writer",
"_build_gateway_usage_metadata",
"_close_gateway_session",
"_dispatch_gateway_sync_telemetry",
"_ensure_gateway_request_candidate",
"_gateway_module",
"_gateway_sync_report_requires_inline",
"_iter_gateway_report_body_chunks",
"_record_gateway_gemini_video_create_sync_success",
"_mark_gateway_sync_candidate_terminal_state",
"_postprocess_gateway_report_provider_response",
"_record_gateway_gemini_video_cancel_sync_success",
"_record_gateway_chat_sync_failure",
"_record_gateway_cli_sync_failure",
"_record_gateway_direct_candidate_graph",
"_record_gateway_openai_chat_stream_success",
"_record_gateway_openai_chat_sync_success",
"_record_gateway_openai_video_create_sync_success",
"_record_gateway_openai_video_cancel_sync_success",
"_record_gateway_openai_video_delete_sync_success",
"_record_gateway_openai_video_remix_sync_success",
"_record_gateway_passthrough_chat_stream_success",
"_record_gateway_passthrough_chat_sync_success",
"_record_gateway_passthrough_cli_stream_success",
"_record_gateway_passthrough_cli_sync_success",
"_record_gateway_sync_failure",
"_record_gateway_video_sync_failure",
"_resolve_gateway_background_db",
"_resolve_gateway_failure_adapter",
"_run_gateway_stream_report_background",
"_run_gateway_stream_report_background_with_session",
"_run_gateway_sync_report_background",
"_run_gateway_sync_report_background_with_session",
"_run_gateway_video_finalize_submitted_background",
"_schedule_gateway_sync_telemetry",
]

View File

@@ -0,0 +1,265 @@
from __future__ import annotations
import inspect
import time
import uuid
from datetime import datetime, timezone
from typing import Any
from fastapi import BackgroundTasks
from sqlalchemy.orm import Session
from src.core.logger import logger
from src.models.database import ApiKey, RequestCandidate, User
from .gateway_contract import GatewayStreamReportRequest, GatewaySyncReportRequest
from .gateway_reporting_common import _gateway_module
_INLINE_SYNC_REPORT_KINDS = {
"openai_video_create_sync_success",
"openai_video_remix_sync_success",
"gemini_video_create_sync_success",
}
def _gateway_sync_report_requires_inline(payload: GatewaySyncReportRequest) -> bool:
return payload.report_kind in _INLINE_SYNC_REPORT_KINDS
async def _apply_gateway_sync_report(
payload: GatewaySyncReportRequest,
*,
db: Session,
) -> None:
gateway_module = _gateway_module()
if payload.report_kind == "gemini_files_store_mapping":
from src.api.public.gemini_files import _maybe_store_file_mapping_from_payload
await _maybe_store_file_mapping_from_payload(
status_code=payload.status_code,
headers=dict(payload.headers or {}),
content_bytes=gateway_module._extract_gateway_report_body_bytes(payload),
file_key_id=str(payload.report_context.get("file_key_id") or "") or None,
user_id=str(payload.report_context.get("user_id") or "") or None,
)
elif payload.report_kind == "gemini_files_delete_mapping" and payload.status_code < 300:
from src.services.gemini_files_mapping import delete_file_key_mapping
file_name = str(payload.report_context.get("file_name") or "").strip()
if file_name:
await delete_file_key_mapping(file_name)
elif payload.report_kind == "openai_chat_sync_success":
await gateway_module._record_gateway_openai_chat_sync_success(payload, db=db)
elif payload.report_kind == "claude_chat_sync_success":
await gateway_module._record_gateway_passthrough_chat_sync_success(payload, db=db)
elif payload.report_kind == "gemini_chat_sync_success":
await gateway_module._record_gateway_passthrough_chat_sync_success(payload, db=db)
elif payload.report_kind == "openai_chat_sync_error":
await gateway_module._record_gateway_chat_sync_failure(payload, db=db)
elif payload.report_kind == "claude_chat_sync_error":
await gateway_module._record_gateway_chat_sync_failure(payload, db=db)
elif payload.report_kind == "gemini_chat_sync_error":
await gateway_module._record_gateway_chat_sync_failure(payload, db=db)
elif payload.report_kind == "openai_cli_sync_success":
await gateway_module._record_gateway_passthrough_cli_sync_success(payload, db=db)
elif payload.report_kind == "claude_cli_sync_success":
await gateway_module._record_gateway_passthrough_cli_sync_success(payload, db=db)
elif payload.report_kind == "gemini_cli_sync_success":
await gateway_module._record_gateway_passthrough_cli_sync_success(payload, db=db)
elif payload.report_kind == "openai_video_delete_sync_success":
await gateway_module._record_gateway_openai_video_delete_sync_success(payload, db=db)
elif payload.report_kind == "openai_video_cancel_sync_success":
await gateway_module._record_gateway_openai_video_cancel_sync_success(payload, db=db)
elif payload.report_kind == "gemini_video_cancel_sync_success":
await gateway_module._record_gateway_gemini_video_cancel_sync_success(payload, db=db)
elif payload.report_kind == "openai_video_create_sync_success":
await gateway_module._record_gateway_openai_video_create_sync_success(payload, db=db)
elif payload.report_kind == "openai_video_remix_sync_success":
await gateway_module._record_gateway_openai_video_remix_sync_success(payload, db=db)
elif payload.report_kind == "gemini_video_create_sync_success":
await gateway_module._record_gateway_gemini_video_create_sync_success(payload, db=db)
elif payload.report_kind == "openai_cli_sync_error":
await gateway_module._record_gateway_cli_sync_failure(payload, db=db)
elif payload.report_kind == "openai_compact_sync_error":
await gateway_module._record_gateway_cli_sync_failure(payload, db=db)
elif payload.report_kind == "claude_cli_sync_error":
await gateway_module._record_gateway_cli_sync_failure(payload, db=db)
elif payload.report_kind == "gemini_cli_sync_error":
await gateway_module._record_gateway_cli_sync_failure(payload, db=db)
elif payload.report_kind == "openai_video_create_sync_error":
await gateway_module._record_gateway_video_sync_failure(payload, db=db)
elif payload.report_kind == "openai_video_remix_sync_error":
await gateway_module._record_gateway_video_sync_failure(payload, db=db)
elif payload.report_kind == "gemini_video_create_sync_error":
await gateway_module._record_gateway_video_sync_failure(payload, db=db)
elif payload.report_kind == "openai_video_delete_sync_error":
await gateway_module._record_gateway_video_sync_failure(payload, db=db)
elif payload.report_kind == "openai_video_cancel_sync_error":
await gateway_module._record_gateway_video_sync_failure(payload, db=db)
elif payload.report_kind == "gemini_video_cancel_sync_error":
await gateway_module._record_gateway_video_sync_failure(payload, db=db)
async def _run_gateway_sync_report_background(
payload: GatewaySyncReportRequest,
db: Session,
) -> None:
gateway_module = _gateway_module()
context = dict(payload.report_context or {})
candidate = None
try:
candidate = gateway_module._ensure_gateway_request_candidate(
db=db,
report_context=context,
trace_id=payload.trace_id,
initial_status="pending",
)
await gateway_module._apply_gateway_sync_report(payload, db=db)
except Exception as exc:
logger.warning("gateway background sync report failed: {}", exc)
finally:
try:
gateway_module._mark_gateway_sync_candidate_terminal_state(
db=db,
candidate=candidate,
payload=payload,
)
except Exception as exc:
logger.warning("gateway sync candidate finalize failed: {}", exc)
def _close_gateway_session(db: Session) -> None:
try:
db.close()
except Exception as exc:
logger.warning("gateway finalize session close failed: {}", exc)
def _resolve_gateway_background_db(app: Any | None) -> tuple[Any, Any | None]:
gateway_module = _gateway_module()
overrides = getattr(app, "dependency_overrides", None)
if isinstance(overrides, dict):
override = overrides.get(gateway_module.get_db)
if callable(override):
override_value = override()
if inspect.isgenerator(override_value):
generator = override_value
db = next(generator)
def _cleanup_override_generator() -> None:
try:
next(generator)
except StopIteration:
return
except Exception as exc:
logger.warning(
"gateway background override generator cleanup failed: {}",
exc,
)
return db, _cleanup_override_generator
return override_value, None
db = gateway_module.create_session()
return db, lambda: gateway_module._close_gateway_session(db)
async def _run_gateway_sync_report_background_with_session(
payload: GatewaySyncReportRequest,
app: Any | None,
) -> None:
db, cleanup = _resolve_gateway_background_db(app)
try:
await _run_gateway_sync_report_background(payload, db)
finally:
if cleanup is not None:
cleanup()
async def _apply_gateway_stream_report(
payload: GatewayStreamReportRequest,
*,
db: Session,
) -> None:
gateway_module = _gateway_module()
if payload.report_kind == "openai_chat_stream_success":
await gateway_module._record_gateway_openai_chat_stream_success(payload, db=db)
elif payload.report_kind == "claude_chat_stream_success":
await gateway_module._record_gateway_passthrough_chat_stream_success(payload, db=db)
elif payload.report_kind == "gemini_chat_stream_success":
await gateway_module._record_gateway_passthrough_chat_stream_success(payload, db=db)
elif payload.report_kind == "openai_cli_stream_success":
await gateway_module._record_gateway_passthrough_cli_stream_success(payload, db=db)
elif payload.report_kind == "claude_cli_stream_success":
await gateway_module._record_gateway_passthrough_cli_stream_success(payload, db=db)
elif payload.report_kind == "gemini_cli_stream_success":
await gateway_module._record_gateway_passthrough_cli_stream_success(payload, db=db)
async def _run_gateway_stream_report_background(
payload: GatewayStreamReportRequest,
db: Session,
) -> None:
gateway_module = _gateway_module()
try:
gateway_module._ensure_gateway_request_candidate(
db=db,
report_context=dict(payload.report_context or {}),
trace_id=payload.trace_id,
initial_status="streaming",
)
await gateway_module._apply_gateway_stream_report(payload, db=db)
except Exception as exc:
logger.warning("gateway background stream report failed: {}", exc)
async def _run_gateway_stream_report_background_with_session(
payload: GatewayStreamReportRequest,
app: Any | None,
) -> None:
db, cleanup = _resolve_gateway_background_db(app)
try:
await _run_gateway_stream_report_background(payload, db)
finally:
if cleanup is not None:
cleanup()
async def _run_gateway_video_finalize_submitted_background(
*,
db: Session,
request_id: str,
provider_name: str,
provider_id: str | None,
provider_endpoint_id: str | None,
provider_api_key_id: str | None,
response_time_ms: int,
status_code: int,
endpoint_api_format: str | None,
provider_request_headers: dict[str, Any],
response_headers: dict[str, Any],
response_body: dict[str, Any],
) -> None:
from src.services.usage.service import UsageService
try:
UsageService.finalize_submitted(
db,
request_id=request_id,
provider_name=provider_name,
provider_id=provider_id,
provider_endpoint_id=provider_endpoint_id,
provider_api_key_id=provider_api_key_id,
response_time_ms=response_time_ms,
status_code=status_code,
endpoint_api_format=endpoint_api_format,
provider_request_headers=provider_request_headers,
response_headers=response_headers,
response_body=response_body,
)
db.commit()
except Exception as exc:
db.rollback()
logger.warning("gateway background video finalize_submitted failed: {}", exc)

View File

@@ -0,0 +1,273 @@
from __future__ import annotations
import inspect
import time
import uuid
from datetime import datetime, timezone
from typing import Any
from fastapi import BackgroundTasks
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import Session
from src.core.logger import logger
from src.models.database import ApiKey, RequestCandidate, User
from .gateway_contract import GatewayStreamReportRequest, GatewaySyncReportRequest
from .gateway_reporting_common import _gateway_module
def _load_existing_gateway_direct_candidate_record_map(
*,
db: Session,
request_id: str,
) -> dict[tuple[int, int], str]:
if not request_id or not hasattr(db, "query"):
return {}
rows = (
db.query(RequestCandidate)
.filter(RequestCandidate.request_id == request_id)
.order_by(RequestCandidate.candidate_index, RequestCandidate.retry_index)
.all()
)
return {
(int(row.candidate_index or 0), int(row.retry_index or 0)): str(row.id)
for row in rows
}
def _record_gateway_direct_candidate_graph(
*,
db: Session,
candidate_resolver: Any,
candidates: list[Any],
request_id: str,
user_api_key: ApiKey,
required_capabilities: dict[str, bool] | None,
selected_candidate_index: int,
) -> None:
from src.services.request.candidate import RequestCandidateService
from src.services.scheduling.schemas import PoolCandidate
if not candidates or not request_id or not hasattr(db, "query"):
return
selected_candidate_index = max(0, min(selected_candidate_index, len(candidates) - 1))
selected_candidate = candidates[selected_candidate_index]
try:
user = getattr(user_api_key, "user", None)
except Exception:
user = None
user_id = str(getattr(user_api_key, "user_id", "") or getattr(user, "id", "") or "") or None
try:
candidate_record_map = _load_existing_gateway_direct_candidate_record_map(
db=db,
request_id=request_id,
)
if not candidate_record_map:
candidate_record_map = candidate_resolver.create_candidate_records(
candidates,
request_id,
user_id,
user_api_key,
required_capabilities,
expand_retries=False,
)
except IntegrityError as exc:
db.rollback()
candidate_record_map = _load_existing_gateway_direct_candidate_record_map(
db=db,
request_id=request_id,
)
if not candidate_record_map:
logger.warning(
"[Gateway] failed to create direct candidate graph for request {}: {}",
request_id,
exc,
)
return
logger.debug(
"[Gateway] reused existing direct candidate graph for request {} after duplicate insert",
request_id,
)
except Exception as exc:
db.rollback()
logger.warning(
"[Gateway] failed to create direct candidate graph for request {}: {}",
request_id,
exc,
)
return
selected_retry_index = 0
if isinstance(selected_candidate, PoolCandidate):
selected_retry_index = int(getattr(selected_candidate, "_pool_key_index", 0) or 0)
selected_record_id = candidate_record_map.get((selected_candidate_index, selected_retry_index))
if not selected_record_id:
selected_record_id = candidate_record_map.get((selected_candidate_index, 0))
selected_retry_index = 0
if not selected_record_id:
return
setattr(selected_candidate, "request_candidate_id", selected_record_id)
RequestCandidateService.mark_candidate_started(db, selected_record_id)
unused_record_ids = [
record_id
for (candidate_index, retry_index), record_id in candidate_record_map.items()
if (candidate_index, retry_index) != (selected_candidate_index, selected_retry_index)
]
if not unused_record_ids:
return
now = datetime.now(timezone.utc)
unused_candidates = (
db.query(RequestCandidate)
.filter(
RequestCandidate.id.in_(unused_record_ids),
RequestCandidate.status == "available",
)
.all()
)
for candidate in unused_candidates:
candidate.status = "unused"
candidate.finished_at = now
if unused_candidates:
db.flush()
def _ensure_gateway_request_candidate(
*,
db: Session,
report_context: dict[str, Any],
trace_id: str,
initial_status: str,
) -> RequestCandidate | None:
from src.services.request.candidate import RequestCandidateService
if not hasattr(db, "query"):
return None
request_id = str(report_context.get("request_id") or trace_id or "").strip()
if not request_id:
return None
candidate_id = str(report_context.get("candidate_id") or "").strip() or None
provider_id = str(report_context.get("provider_id") or "").strip() or None
endpoint_id = str(report_context.get("endpoint_id") or "").strip() or None
key_id = str(report_context.get("key_id") or "").strip() or None
user_id = str(report_context.get("user_id") or "").strip() or None
api_key_id = str(report_context.get("api_key_id") or "").strip() or None
client_api_format = str(report_context.get("client_api_format") or "").strip() or None
if not any([candidate_id, provider_id, endpoint_id, key_id, client_api_format]):
return None
candidate: RequestCandidate | None = None
if candidate_id:
candidate = db.query(RequestCandidate).filter(RequestCandidate.id == candidate_id).first()
if candidate is None:
lookup = db.query(RequestCandidate).filter(RequestCandidate.request_id == request_id)
if provider_id:
lookup = lookup.filter(RequestCandidate.provider_id == provider_id)
if endpoint_id:
lookup = lookup.filter(RequestCandidate.endpoint_id == endpoint_id)
if key_id:
lookup = lookup.filter(RequestCandidate.key_id == key_id)
candidate = lookup.order_by(
RequestCandidate.retry_index.desc(),
RequestCandidate.candidate_index.desc(),
RequestCandidate.created_at.desc(),
).first()
if candidate is None:
user = db.query(User).filter(User.id == user_id).first() if user_id else None
api_key = db.query(ApiKey).filter(ApiKey.id == api_key_id).first() if api_key_id else None
candidate_index = (
db.query(RequestCandidate).filter(RequestCandidate.request_id == request_id).count()
)
candidate = RequestCandidateService.create_candidate(
db,
request_id=request_id,
candidate_index=candidate_index,
candidate_id=candidate_id,
user_id=user_id,
api_key_id=api_key_id,
username=str(getattr(user, "username", "") or "") or None,
api_key_name=str(getattr(api_key, "name", "") or "") or None,
provider_id=provider_id,
endpoint_id=endpoint_id,
key_id=key_id,
status=initial_status,
extra_data={
"gateway_direct_executor": True,
"phase": "3c_trial",
"client_api_format": client_api_format,
"provider_api_format": str(report_context.get("provider_api_format") or "") or None,
},
)
current_status = str(candidate.status or "").strip().lower()
if candidate.started_at is None:
candidate.started_at = datetime.now(timezone.utc)
if initial_status == "streaming" and current_status in {
"",
"available",
"unused",
"skipped",
"pending",
"streaming",
}:
candidate.status = "streaming"
elif current_status in {"", "available", "unused", "skipped"}:
candidate.status = "pending"
db.commit()
return candidate
def _mark_gateway_sync_candidate_terminal_state(
*,
db: Session,
candidate: RequestCandidate | None,
payload: GatewaySyncReportRequest,
) -> None:
from src.services.request.candidate import RequestCandidateService
if candidate is None:
return
gateway_module = _gateway_module()
elapsed_ms = 0
if isinstance(payload.telemetry, dict):
raw_elapsed_ms = payload.telemetry.get("elapsed_ms")
try:
elapsed_ms = max(int(raw_elapsed_ms or 0), 0)
except (TypeError, ValueError):
elapsed_ms = 0
has_error_payload = (
isinstance(payload.body_json, dict) and payload.body_json.get("error") is not None
)
if payload.status_code >= 400 or has_error_payload:
RequestCandidateService.mark_candidate_failed(
db=db,
candidate_id=candidate.id,
error_type="gateway_error",
error_message=gateway_module._extract_gateway_sync_error_message(payload),
status_code=payload.status_code,
latency_ms=elapsed_ms or None,
)
return
RequestCandidateService.mark_candidate_success(
db=db,
candidate_id=candidate.id,
status_code=payload.status_code,
latency_ms=elapsed_ms,
extra_data={"gateway_direct_executor": True, "phase": "3c_trial"},
)

View File

@@ -0,0 +1,21 @@
from __future__ import annotations
import inspect
import time
import uuid
from datetime import datetime, timezone
from typing import Any
from fastapi import BackgroundTasks
from sqlalchemy.orm import Session
from src.core.logger import logger
from src.models.database import ApiKey, RequestCandidate, User
from .gateway_contract import GatewayStreamReportRequest, GatewaySyncReportRequest
def _gateway_module() -> Any:
from . import gateway as gateway_module
return gateway_module

View File

@@ -0,0 +1,165 @@
from __future__ import annotations
import inspect
import time
import uuid
from datetime import datetime, timezone
from typing import Any
from fastapi import BackgroundTasks
from sqlalchemy.orm import Session
from src.core.logger import logger
from src.models.database import ApiKey, RequestCandidate, User
from .gateway_contract import GatewayStreamReportRequest, GatewaySyncReportRequest
from .gateway_reporting_common import _gateway_module
def _resolve_gateway_failure_adapter(client_api_format: str, *, cli: bool) -> Any | None:
if cli:
from src.api.handlers.claude_cli import ClaudeCliAdapter
from src.api.handlers.gemini_cli import GeminiCliAdapter
from src.api.handlers.openai_cli import OpenAICliAdapter, OpenAICompactAdapter
if client_api_format == "openai:compact":
return OpenAICompactAdapter()
if client_api_format == "claude:cli":
return ClaudeCliAdapter()
if client_api_format == "gemini:cli":
return GeminiCliAdapter()
if client_api_format == "openai:cli":
return OpenAICliAdapter()
return None
from src.api.handlers.claude import ClaudeChatAdapter
from src.api.handlers.gemini import GeminiChatAdapter
from src.api.handlers.openai import OpenAIChatAdapter
if client_api_format == "claude:chat":
return ClaudeChatAdapter()
if client_api_format == "gemini:chat":
return GeminiChatAdapter()
if client_api_format == "openai:chat":
return OpenAIChatAdapter()
return None
async def _record_gateway_sync_failure(
payload: GatewaySyncReportRequest,
*,
db: Session,
cli: bool,
) -> None:
from src.api.handlers.base.stream_context import is_format_converted
from src.api.handlers.base.utils import filter_proxy_response_headers
gateway_module = _gateway_module()
context = dict(payload.report_context or {})
user_id = str(context.get("user_id") or "").strip()
api_key_id = str(context.get("api_key_id") or "").strip()
if not user_id or not api_key_id:
return
client_api_format = str(context.get("client_api_format") or "").strip().lower()
provider_api_format = str(context.get("provider_api_format") or "").strip().lower()
adapter = _resolve_gateway_failure_adapter(client_api_format, cli=cli)
if adapter is None:
return
user = db.query(User).filter(User.id == user_id).first()
api_key = db.query(ApiKey).filter(ApiKey.id == api_key_id).first()
if not user or not api_key:
return
request_id = str(context.get("request_id") or payload.trace_id or uuid.uuid4().hex[:8]).strip()
model = str(context.get("model") or "unknown").strip() or "unknown"
handler = adapter._create_handler(
db=db,
user=user,
api_key=api_key,
request_id=request_id,
client_ip="127.0.0.1",
user_agent=str(
(context.get("original_headers") or {}).get("user-agent") or "aether-gateway"
),
start_time=time.time(),
api_family=adapter.API_FAMILY.value if adapter.API_FAMILY else None,
endpoint_kind=adapter.ENDPOINT_KIND.value if adapter.ENDPOINT_KIND else None,
)
request_metadata: dict[str, Any] = {
"gateway_direct_executor": True,
"phase": "3c_trial",
}
proxy_info = context.get("proxy_info")
if isinstance(proxy_info, dict):
request_metadata["proxy"] = proxy_info
response_time_ms = 0
if isinstance(payload.telemetry, dict):
raw_elapsed_ms = payload.telemetry.get("elapsed_ms")
try:
response_time_ms = max(int(raw_elapsed_ms or 0), 0)
except (TypeError, ValueError):
response_time_ms = 0
client_response_headers = filter_proxy_response_headers(dict(payload.headers or {}))
client_response_headers["content-type"] = "application/json"
telemetry_writer = gateway_module._build_gateway_sync_telemetry_writer(
db=db,
request_id=request_id,
user_id=user_id,
api_key_id=api_key_id,
fallback_telemetry=handler.telemetry,
)
await gateway_module._dispatch_gateway_sync_telemetry(
telemetry_writer=telemetry_writer,
operation="record_failure",
provider=str(context.get("provider_name") or "unknown"),
model=model,
response_time_ms=response_time_ms,
status_code=payload.status_code,
error_message=gateway_module._extract_gateway_sync_error_message(payload),
request_headers=dict(context.get("original_headers") or {}),
request_body=dict(context.get("original_request_body") or {}),
provider_request_body=context.get("provider_request_body"),
is_stream=False,
api_format=client_api_format,
api_family=adapter.API_FAMILY.value if adapter.API_FAMILY else None,
endpoint_kind=adapter.ENDPOINT_KIND.value if adapter.ENDPOINT_KIND else None,
provider_request_headers=dict(context.get("provider_request_headers") or {}),
response_headers=dict(payload.headers or {}),
client_response_headers=client_response_headers,
provider_id=str(context.get("provider_id") or "") or None,
provider_endpoint_id=str(context.get("endpoint_id") or "") or None,
provider_api_key_id=str(context.get("key_id") or "") or None,
endpoint_api_format=provider_api_format or None,
has_format_conversion=is_format_converted(provider_api_format, client_api_format),
target_model=str(context.get("mapped_model") or "") or None,
metadata=request_metadata,
)
async def _record_gateway_chat_sync_failure(
payload: GatewaySyncReportRequest,
*,
db: Session,
) -> None:
await _record_gateway_sync_failure(payload, db=db, cli=False)
async def _record_gateway_cli_sync_failure(
payload: GatewaySyncReportRequest,
*,
db: Session,
) -> None:
await _record_gateway_sync_failure(payload, db=db, cli=True)
async def _record_gateway_video_sync_failure(
payload: GatewaySyncReportRequest,
*,
db: Session,
) -> None:
_ = (payload, db)

View File

@@ -0,0 +1,407 @@
from __future__ import annotations
import inspect
import time
import uuid
from datetime import datetime, timezone
from typing import Any
from fastapi import BackgroundTasks
from sqlalchemy.orm import Session
from src.core.logger import logger
from src.models.database import ApiKey, RequestCandidate, User
from .gateway_contract import GatewayStreamReportRequest, GatewaySyncReportRequest
from .gateway_reporting_common import _gateway_module
class _GatewayReportStreamContext:
async def __aenter__(self) -> _GatewayReportStreamContext:
return self
async def __aexit__(self, exc_type: Any, exc: Any, tb: Any) -> None:
return None
async def _iter_gateway_report_body_chunks(body_bytes: bytes) -> Any:
yield body_bytes
async def _record_gateway_openai_chat_stream_success(
payload: GatewayStreamReportRequest,
*,
db: Session,
) -> None:
from src.api.handlers.base.stream_context import StreamContext
from src.api.handlers.base.stream_processor import StreamProcessor
from src.api.handlers.base.stream_telemetry import StreamTelemetryRecorder
from src.api.handlers.openai import OpenAIChatAdapter
from src.services.system.config import SystemConfigService
gateway_module = _gateway_module()
if payload.status_code >= 400:
return
context = dict(payload.report_context or {})
user_id = str(context.get("user_id") or "").strip()
api_key_id = str(context.get("api_key_id") or "").strip()
if not user_id or not api_key_id:
return
body_bytes = gateway_module._extract_gateway_report_body_bytes(payload)
if not body_bytes:
return
user = db.query(User).filter(User.id == user_id).first()
api_key = db.query(ApiKey).filter(ApiKey.id == api_key_id).first()
if not user or not api_key:
return
adapter = OpenAIChatAdapter()
handler = adapter._create_handler(
db=db,
user=user,
api_key=api_key,
request_id=str(context.get("request_id") or payload.trace_id or uuid.uuid4().hex[:8]),
client_ip="127.0.0.1",
user_agent=str(
(context.get("original_headers") or {}).get("user-agent") or "aether-gateway"
),
start_time=time.time(),
api_family=adapter.API_FAMILY.value if adapter.API_FAMILY else None,
endpoint_kind=adapter.ENDPOINT_KIND.value if adapter.ENDPOINT_KIND else None,
)
ctx = StreamContext(
model=str(context.get("model") or "unknown"),
api_format=handler.allowed_api_formats[0],
api_family=adapter.API_FAMILY.value if adapter.API_FAMILY else None,
endpoint_kind=adapter.ENDPOINT_KIND.value if adapter.ENDPOINT_KIND else None,
)
ctx.request_id = handler.request_id
ctx.client_api_format = str(context.get("client_api_format") or "openai:chat")
ctx.provider_type = str(context.get("provider_name") or "openai")
ctx.update_provider_info(
provider_name=str(context.get("provider_name") or "openai"),
provider_id=str(context.get("provider_id") or ""),
endpoint_id=str(context.get("endpoint_id") or ""),
key_id=str(context.get("key_id") or ""),
provider_api_format=str(context.get("provider_api_format") or "openai:chat"),
)
ctx.mapped_model = str(context.get("mapped_model") or "") or None
ctx.provider_request_headers = dict(context.get("provider_request_headers") or {})
ctx.provider_request_body = context.get("provider_request_body")
ctx.response_headers = dict(payload.headers or {})
ctx.status_code = payload.status_code
ctx.record_parsed_chunks = SystemConfigService.should_log_body(db)
if str(context.get("candidate_id") or "").strip():
ctx.attempt_id = str(context.get("candidate_id"))
proxy_info = context.get("proxy_info")
if isinstance(proxy_info, dict):
ctx.proxy_info = dict(proxy_info)
ctx.set_proxy_timing(ctx.response_headers)
telemetry = payload.telemetry if isinstance(payload.telemetry, dict) else {}
ttfb_ms = telemetry.get("ttfb_ms")
if ttfb_ms is not None:
try:
ctx.first_byte_time_ms = max(int(ttfb_ms), 0)
except (TypeError, ValueError):
ctx.first_byte_time_ms = None
if ctx.proxy_info is not None and ctx.first_byte_time_ms is not None:
ctx.set_ttfb_ms(ctx.first_byte_time_ms)
stream_processor = StreamProcessor(
request_id=ctx.request_id,
default_parser=handler.parser,
on_streaming_start=None,
)
stream = stream_processor.create_response_stream(
ctx,
_iter_gateway_report_body_chunks(body_bytes),
_GatewayReportStreamContext(),
start_time=time.time(),
)
async for _chunk in stream:
pass
elapsed_ms = telemetry.get("elapsed_ms")
try:
response_elapsed_ms = max(int(elapsed_ms or 0), 0)
except (TypeError, ValueError):
response_elapsed_ms = 0
request_start_time = (
time.time() - (response_elapsed_ms / 1000.0) if response_elapsed_ms > 0 else time.time()
)
telemetry_recorder = StreamTelemetryRecorder(
request_id=ctx.request_id,
user_id=str(user.id),
api_key_id=str(api_key.id),
client_ip="127.0.0.1",
format_id=handler.FORMAT_ID,
)
await telemetry_recorder.record_stream_stats(
ctx,
dict(context.get("original_headers") or {}),
dict(context.get("original_request_body") or {}),
request_start_time,
)
async def _record_gateway_passthrough_chat_stream_success(
payload: GatewayStreamReportRequest,
*,
db: Session,
) -> None:
from src.api.handlers.base.stream_context import StreamContext
from src.api.handlers.base.stream_processor import StreamProcessor
from src.api.handlers.base.stream_telemetry import StreamTelemetryRecorder
from src.api.handlers.claude import ClaudeChatAdapter
from src.api.handlers.gemini import GeminiChatAdapter
from src.api.handlers.openai import OpenAIChatAdapter
from src.services.system.config import SystemConfigService
gateway_module = _gateway_module()
if payload.status_code >= 400:
return
context = dict(payload.report_context or {})
user_id = str(context.get("user_id") or "").strip()
api_key_id = str(context.get("api_key_id") or "").strip()
client_api_format = str(context.get("client_api_format") or "").strip().lower()
if not user_id or not api_key_id or not client_api_format:
return
body_bytes = gateway_module._extract_gateway_report_body_bytes(payload)
if not body_bytes:
return
user = db.query(User).filter(User.id == user_id).first()
api_key = db.query(ApiKey).filter(ApiKey.id == api_key_id).first()
if not user or not api_key:
return
if client_api_format == "claude:chat":
adapter = ClaudeChatAdapter()
elif client_api_format == "gemini:chat":
adapter = GeminiChatAdapter()
elif client_api_format == "openai:chat":
adapter = OpenAIChatAdapter()
else:
return
handler = adapter._create_handler(
db=db,
user=user,
api_key=api_key,
request_id=str(context.get("request_id") or payload.trace_id or uuid.uuid4().hex[:8]),
client_ip="127.0.0.1",
user_agent=str(
(context.get("original_headers") or {}).get("user-agent") or "aether-gateway"
),
start_time=time.time(),
api_family=adapter.API_FAMILY.value if adapter.API_FAMILY else None,
endpoint_kind=adapter.ENDPOINT_KIND.value if adapter.ENDPOINT_KIND else None,
)
ctx = StreamContext(
model=str(context.get("model") or "unknown"),
api_format=handler.allowed_api_formats[0],
api_family=adapter.API_FAMILY.value if adapter.API_FAMILY else None,
endpoint_kind=adapter.ENDPOINT_KIND.value if adapter.ENDPOINT_KIND else None,
)
ctx.request_id = handler.request_id
ctx.client_api_format = client_api_format
ctx.provider_type = str(context.get("provider_name") or "unknown")
ctx.update_provider_info(
provider_name=str(context.get("provider_name") or "unknown"),
provider_id=str(context.get("provider_id") or ""),
endpoint_id=str(context.get("endpoint_id") or ""),
key_id=str(context.get("key_id") or ""),
provider_api_format=str(context.get("provider_api_format") or client_api_format),
)
ctx.mapped_model = str(context.get("mapped_model") or "") or None
ctx.provider_request_headers = dict(context.get("provider_request_headers") or {})
ctx.provider_request_body = context.get("provider_request_body")
ctx.response_headers = dict(payload.headers or {})
ctx.status_code = payload.status_code
ctx.record_parsed_chunks = SystemConfigService.should_log_body(db)
if str(context.get("candidate_id") or "").strip():
ctx.attempt_id = str(context.get("candidate_id"))
proxy_info = context.get("proxy_info")
if isinstance(proxy_info, dict):
ctx.proxy_info = dict(proxy_info)
ctx.set_proxy_timing(ctx.response_headers)
telemetry = payload.telemetry if isinstance(payload.telemetry, dict) else {}
ttfb_ms = telemetry.get("ttfb_ms")
if ttfb_ms is not None:
try:
ctx.first_byte_time_ms = max(int(ttfb_ms), 0)
except (TypeError, ValueError):
ctx.first_byte_time_ms = None
if ctx.proxy_info is not None and ctx.first_byte_time_ms is not None:
ctx.set_ttfb_ms(ctx.first_byte_time_ms)
stream_processor = StreamProcessor(
request_id=ctx.request_id,
default_parser=handler.parser,
on_streaming_start=None,
)
stream = stream_processor.create_response_stream(
ctx,
_iter_gateway_report_body_chunks(body_bytes),
_GatewayReportStreamContext(),
start_time=time.time(),
)
async for _chunk in stream:
pass
elapsed_ms = telemetry.get("elapsed_ms")
try:
response_elapsed_ms = max(int(elapsed_ms or 0), 0)
except (TypeError, ValueError):
response_elapsed_ms = 0
request_start_time = (
time.time() - (response_elapsed_ms / 1000.0) if response_elapsed_ms > 0 else time.time()
)
telemetry_recorder = StreamTelemetryRecorder(
request_id=ctx.request_id,
user_id=str(user.id),
api_key_id=str(api_key.id),
client_ip="127.0.0.1",
format_id=handler.FORMAT_ID,
)
await telemetry_recorder.record_stream_stats(
ctx,
dict(context.get("original_headers") or {}),
dict(context.get("original_request_body") or {}),
request_start_time,
)
async def _record_gateway_passthrough_cli_stream_success(
payload: GatewayStreamReportRequest,
*,
db: Session,
) -> None:
from src.api.handlers.base.stream_context import StreamContext
from src.api.handlers.claude_cli import ClaudeCliAdapter
from src.api.handlers.gemini_cli import GeminiCliAdapter
from src.api.handlers.openai_cli import OpenAICliAdapter
from src.services.system.config import SystemConfigService
gateway_module = _gateway_module()
if payload.status_code >= 400:
return
context = dict(payload.report_context or {})
user_id = str(context.get("user_id") or "").strip()
api_key_id = str(context.get("api_key_id") or "").strip()
client_api_format = str(context.get("client_api_format") or "").strip().lower()
if not user_id or not api_key_id or not client_api_format:
return
body_bytes = gateway_module._extract_gateway_report_body_bytes(payload)
if not body_bytes:
return
user = db.query(User).filter(User.id == user_id).first()
api_key = db.query(ApiKey).filter(ApiKey.id == api_key_id).first()
if not user or not api_key:
return
if client_api_format == "claude:cli":
adapter = ClaudeCliAdapter()
elif client_api_format == "gemini:cli":
adapter = GeminiCliAdapter()
elif client_api_format == "openai:cli":
adapter = OpenAICliAdapter()
else:
return
handler = adapter.HANDLER_CLASS(
db=db,
user=user,
api_key=api_key,
request_id=str(context.get("request_id") or payload.trace_id or uuid.uuid4().hex[:8]),
client_ip="127.0.0.1",
user_agent=str(
(context.get("original_headers") or {}).get("user-agent") or "aether-gateway"
),
start_time=time.time(),
allowed_api_formats=adapter.allowed_api_formats,
adapter_detector=adapter.detect_capability_requirements,
api_family=adapter.API_FAMILY.value if adapter.API_FAMILY else None,
endpoint_kind=adapter.ENDPOINT_KIND.value if adapter.ENDPOINT_KIND else None,
)
ctx = StreamContext(
model=str(context.get("model") or "unknown"),
api_format=handler.primary_api_format,
api_family=adapter.API_FAMILY.value if adapter.API_FAMILY else None,
endpoint_kind=adapter.ENDPOINT_KIND.value if adapter.ENDPOINT_KIND else None,
request_id=handler.request_id,
user_id=user.id,
api_key_id=api_key.id,
)
ctx.client_api_format = client_api_format
ctx.provider_type = str(context.get("provider_name") or "unknown")
ctx.update_provider_info(
provider_name=str(context.get("provider_name") or "unknown"),
provider_id=str(context.get("provider_id") or ""),
endpoint_id=str(context.get("endpoint_id") or ""),
key_id=str(context.get("key_id") or ""),
provider_api_format=str(context.get("provider_api_format") or client_api_format),
)
ctx.mapped_model = str(context.get("mapped_model") or "") or None
ctx.provider_request_headers = dict(context.get("provider_request_headers") or {})
ctx.provider_request_body = context.get("provider_request_body")
ctx.response_headers = dict(payload.headers or {})
ctx.status_code = payload.status_code
ctx.record_parsed_chunks = SystemConfigService.should_log_body(db)
if str(context.get("candidate_id") or "").strip():
ctx.attempt_id = str(context.get("candidate_id"))
proxy_info = context.get("proxy_info")
if isinstance(proxy_info, dict):
ctx.proxy_info = dict(proxy_info)
ctx.set_proxy_timing(ctx.response_headers)
telemetry = payload.telemetry if isinstance(payload.telemetry, dict) else {}
ttfb_ms = telemetry.get("ttfb_ms")
if ttfb_ms is not None:
try:
ctx.first_byte_time_ms = max(int(ttfb_ms), 0)
except (TypeError, ValueError):
ctx.first_byte_time_ms = None
if ctx.proxy_info is not None and ctx.first_byte_time_ms is not None:
ctx.set_ttfb_ms(ctx.first_byte_time_ms)
elapsed_ms = telemetry.get("elapsed_ms")
try:
response_elapsed_ms = max(int(elapsed_ms or 0), 0)
except (TypeError, ValueError):
response_elapsed_ms = 0
if response_elapsed_ms > 0:
handler.start_time = time.time() - (response_elapsed_ms / 1000.0)
stream = handler._create_response_stream_with_prefetch(
ctx,
_iter_gateway_report_body_chunks(body_bytes),
_GatewayReportStreamContext(),
[],
)
async for _chunk in stream:
pass
await handler._record_stream_stats(
ctx,
dict(context.get("original_headers") or {}),
dict(context.get("original_request_body") or {}),
)

View File

@@ -0,0 +1,394 @@
from __future__ import annotations
import inspect
import time
import uuid
from datetime import datetime, timezone
from typing import Any
from fastapi import BackgroundTasks
from sqlalchemy.orm import Session
from src.core.logger import logger
from src.models.database import ApiKey, RequestCandidate, User
from .gateway_contract import GatewayStreamReportRequest, GatewaySyncReportRequest
from .gateway_reporting_common import _gateway_module
def _postprocess_gateway_report_provider_response(
context: dict[str, Any],
provider_response_json: dict[str, Any],
) -> None:
if str(context.get("envelope_name") or "").strip().lower() != "antigravity:v1internal":
return
try:
from src.services.provider.adapters.antigravity.envelope import (
_inject_claude_tool_ids_response,
cache_thought_signatures,
)
model = str(context.get("mapped_model") or context.get("model") or "")
_inject_claude_tool_ids_response(provider_response_json, model)
cache_thought_signatures(model, provider_response_json)
except Exception:
return
async def _record_gateway_openai_chat_sync_success(
payload: GatewaySyncReportRequest,
*,
db: Session,
) -> None:
from src.api.handlers.openai import OpenAIChatAdapter
if payload.status_code >= 400:
return
if not isinstance(payload.body_json, dict) or payload.body_json.get("error") is not None:
return
context = dict(payload.report_context or {})
user_id = str(context.get("user_id") or "").strip()
api_key_id = str(context.get("api_key_id") or "").strip()
if not user_id or not api_key_id:
return
user = db.query(User).filter(User.id == user_id).first()
api_key = db.query(ApiKey).filter(ApiKey.id == api_key_id).first()
if not user or not api_key:
return
adapter = OpenAIChatAdapter()
handler = adapter._create_handler(
db=db,
user=user,
api_key=api_key,
request_id=str(context.get("request_id") or payload.trace_id or uuid.uuid4().hex[:8]),
client_ip="127.0.0.1",
user_agent=str(
(context.get("original_headers") or {}).get("user-agent") or "aether-gateway"
),
start_time=time.time(),
api_family=adapter.API_FAMILY.value if adapter.API_FAMILY else None,
endpoint_kind=adapter.ENDPOINT_KIND.value if adapter.ENDPOINT_KIND else None,
)
provider_response_json = dict(payload.body_json)
_postprocess_gateway_report_provider_response(context, provider_response_json)
client_response_json = (
dict(payload.client_body_json) if isinstance(payload.client_body_json, dict) else None
)
response_json = handler._normalize_response(client_response_json or provider_response_json)
usage_info = handler._extract_usage(response_json)
request_metadata: dict[str, Any] = {
"gateway_direct_executor": True,
"phase": "3c_trial",
}
proxy_info = context.get("proxy_info")
if isinstance(proxy_info, dict):
request_metadata["proxy"] = proxy_info
response_time_ms = 0
if isinstance(payload.telemetry, dict):
raw_elapsed_ms = payload.telemetry.get("elapsed_ms")
try:
response_time_ms = max(int(raw_elapsed_ms or 0), 0)
except (TypeError, ValueError):
response_time_ms = 0
await handler.telemetry.record_success(
provider=str(context.get("provider_name") or "openai"),
model=str(context.get("model") or "unknown"),
input_tokens=int(usage_info.get("input_tokens", 0) or 0),
output_tokens=int(usage_info.get("output_tokens", 0) or 0),
response_time_ms=response_time_ms,
status_code=payload.status_code,
request_headers=dict(context.get("original_headers") or {}),
request_body=dict(context.get("original_request_body") or {}),
response_headers=dict(payload.headers or {}),
client_response_headers=dict(payload.headers or {}),
response_body=provider_response_json if client_response_json else response_json,
client_response_body=response_json if client_response_json else None,
provider_request_headers=dict(context.get("provider_request_headers") or {}),
provider_request_body=context.get("provider_request_body"),
is_stream=False,
provider_id=str(context.get("provider_id") or "") or None,
provider_endpoint_id=str(context.get("endpoint_id") or "") or None,
provider_api_key_id=str(context.get("key_id") or "") or None,
api_format=str(context.get("client_api_format") or "openai:chat"),
api_family=adapter.API_FAMILY.value if adapter.API_FAMILY else None,
endpoint_kind=adapter.ENDPOINT_KIND.value if adapter.ENDPOINT_KIND else None,
endpoint_api_format=str(context.get("provider_api_format") or "") or None,
has_format_conversion=client_response_json is not None,
target_model=str(context.get("mapped_model") or "") or None,
request_metadata=request_metadata,
)
async def _record_gateway_passthrough_chat_sync_success(
payload: GatewaySyncReportRequest,
*,
db: Session,
) -> None:
from src.api.handlers.claude import ClaudeChatAdapter
from src.api.handlers.gemini import GeminiChatAdapter
from src.api.handlers.openai import OpenAIChatAdapter
if payload.status_code >= 400:
return
if not isinstance(payload.body_json, dict) or payload.body_json.get("error") is not None:
return
context = dict(payload.report_context or {})
user_id = str(context.get("user_id") or "").strip()
api_key_id = str(context.get("api_key_id") or "").strip()
client_api_format = str(context.get("client_api_format") or "").strip().lower()
if not user_id or not api_key_id or not client_api_format:
return
user = db.query(User).filter(User.id == user_id).first()
api_key = db.query(ApiKey).filter(ApiKey.id == api_key_id).first()
if not user or not api_key:
return
if client_api_format == "claude:chat":
adapter = ClaudeChatAdapter()
elif client_api_format == "gemini:chat":
adapter = GeminiChatAdapter()
elif client_api_format == "openai:chat":
adapter = OpenAIChatAdapter()
else:
return
handler = adapter._create_handler(
db=db,
user=user,
api_key=api_key,
request_id=str(context.get("request_id") or payload.trace_id or uuid.uuid4().hex[:8]),
client_ip="127.0.0.1",
user_agent=str(
(context.get("original_headers") or {}).get("user-agent") or "aether-gateway"
),
start_time=time.time(),
api_family=adapter.API_FAMILY.value if adapter.API_FAMILY else None,
endpoint_kind=adapter.ENDPOINT_KIND.value if adapter.ENDPOINT_KIND else None,
)
provider_response_json = dict(payload.body_json)
_postprocess_gateway_report_provider_response(context, provider_response_json)
response_json = handler._normalize_response(provider_response_json)
usage_info = handler._extract_usage(response_json)
request_metadata: dict[str, Any] = {
"gateway_direct_executor": True,
"phase": "3c_trial",
}
proxy_info = context.get("proxy_info")
if isinstance(proxy_info, dict):
request_metadata["proxy"] = proxy_info
response_time_ms = 0
if isinstance(payload.telemetry, dict):
raw_elapsed_ms = payload.telemetry.get("elapsed_ms")
try:
response_time_ms = max(int(raw_elapsed_ms or 0), 0)
except (TypeError, ValueError):
response_time_ms = 0
await handler.telemetry.record_success(
provider=str(context.get("provider_name") or "unknown"),
model=str(context.get("model") or "unknown"),
input_tokens=int(usage_info.get("input_tokens", 0) or 0),
output_tokens=int(usage_info.get("output_tokens", 0) or 0),
response_time_ms=response_time_ms,
status_code=payload.status_code,
request_headers=dict(context.get("original_headers") or {}),
request_body=dict(context.get("original_request_body") or {}),
response_headers=dict(payload.headers or {}),
client_response_headers=dict(payload.headers or {}),
response_body=response_json,
provider_request_headers=dict(context.get("provider_request_headers") or {}),
provider_request_body=context.get("provider_request_body"),
cache_creation_tokens=int(
usage_info.get("cache_creation_input_tokens", 0)
or usage_info.get("cache_creation_tokens", 0)
or 0
),
cache_read_tokens=int(
usage_info.get("cache_read_input_tokens", 0)
or usage_info.get("cache_read_tokens", 0)
or 0
),
is_stream=False,
provider_id=str(context.get("provider_id") or "") or None,
provider_endpoint_id=str(context.get("endpoint_id") or "") or None,
provider_api_key_id=str(context.get("key_id") or "") or None,
api_format=client_api_format,
api_family=adapter.API_FAMILY.value if adapter.API_FAMILY else None,
endpoint_kind=adapter.ENDPOINT_KIND.value if adapter.ENDPOINT_KIND else None,
endpoint_api_format=str(context.get("provider_api_format") or "") or None,
has_format_conversion=False,
target_model=str(context.get("mapped_model") or "") or None,
request_metadata=request_metadata,
)
async def _record_gateway_passthrough_cli_sync_success(
payload: GatewaySyncReportRequest,
*,
db: Session,
) -> None:
from src.api.handlers.claude_cli import ClaudeCliAdapter
from src.api.handlers.gemini_cli import GeminiCliAdapter
from src.api.handlers.openai_cli import OpenAICliAdapter, OpenAICompactAdapter
if payload.status_code >= 400:
return
if not isinstance(payload.body_json, dict) or payload.body_json.get("error") is not None:
return
context = dict(payload.report_context or {})
user_id = str(context.get("user_id") or "").strip()
api_key_id = str(context.get("api_key_id") or "").strip()
client_api_format = str(context.get("client_api_format") or "openai:cli").strip().lower()
if not user_id or not api_key_id:
return
user = db.query(User).filter(User.id == user_id).first()
api_key = db.query(ApiKey).filter(ApiKey.id == api_key_id).first()
if not user or not api_key:
return
if client_api_format == "openai:compact":
adapter = OpenAICompactAdapter()
elif client_api_format == "claude:cli":
adapter = ClaudeCliAdapter()
elif client_api_format == "gemini:cli":
adapter = GeminiCliAdapter()
else:
adapter = OpenAICliAdapter()
handler = adapter.HANDLER_CLASS(
db=db,
user=user,
api_key=api_key,
request_id=str(context.get("request_id") or payload.trace_id or uuid.uuid4().hex[:8]),
client_ip="127.0.0.1",
user_agent=str(
(context.get("original_headers") or {}).get("user-agent") or "aether-gateway"
),
start_time=time.time(),
allowed_api_formats=adapter.allowed_api_formats,
adapter_detector=adapter.detect_capability_requirements,
api_family=adapter.API_FAMILY.value if adapter.API_FAMILY else None,
endpoint_kind=adapter.ENDPOINT_KIND.value if adapter.ENDPOINT_KIND else None,
)
provider_response_json = dict(payload.body_json)
_postprocess_gateway_report_provider_response(context, provider_response_json)
client_response_json = (
dict(payload.client_body_json) if isinstance(payload.client_body_json, dict) else None
)
response_json = dict(client_response_json or provider_response_json)
usage_info = handler.parser.extract_usage_from_response(response_json)
response_metadata = handler._extract_response_metadata(response_json)
request_metadata: dict[str, Any] = {
"gateway_direct_executor": True,
"phase": "3c_trial",
}
proxy_info = context.get("proxy_info")
if isinstance(proxy_info, dict):
request_metadata["proxy"] = proxy_info
response_time_ms = 0
if isinstance(payload.telemetry, dict):
raw_elapsed_ms = payload.telemetry.get("elapsed_ms")
try:
response_time_ms = max(int(raw_elapsed_ms or 0), 0)
except (TypeError, ValueError):
response_time_ms = 0
await handler.telemetry.record_success(
provider=str(context.get("provider_name") or "openai"),
model=str(context.get("model") or "unknown"),
input_tokens=int(usage_info.get("input_tokens", 0) or 0),
output_tokens=int(usage_info.get("output_tokens", 0) or 0),
response_time_ms=response_time_ms,
status_code=payload.status_code,
request_headers=dict(context.get("original_headers") or {}),
request_body=dict(context.get("original_request_body") or {}),
response_headers=dict(payload.headers or {}),
client_response_headers=dict(payload.headers or {}),
response_body=provider_response_json if client_response_json else response_json,
client_response_body=response_json if client_response_json else None,
provider_request_headers=dict(context.get("provider_request_headers") or {}),
provider_request_body=context.get("provider_request_body"),
cache_creation_tokens=int(usage_info.get("cache_creation_tokens", 0) or 0),
cache_read_tokens=int(usage_info.get("cache_read_tokens", 0) or 0),
is_stream=False,
provider_id=str(context.get("provider_id") or "") or None,
provider_endpoint_id=str(context.get("endpoint_id") or "") or None,
provider_api_key_id=str(context.get("key_id") or "") or None,
api_format=client_api_format,
api_family=adapter.API_FAMILY.value if adapter.API_FAMILY else None,
endpoint_kind=adapter.ENDPOINT_KIND.value if adapter.ENDPOINT_KIND else None,
endpoint_api_format=str(context.get("provider_api_format") or "") or None,
has_format_conversion=client_response_json is not None,
target_model=str(context.get("mapped_model") or "") or None,
response_metadata=response_metadata if response_metadata else None,
request_metadata=request_metadata,
)
async def _record_gateway_openai_video_delete_sync_success(
payload: GatewaySyncReportRequest,
*,
db: Session,
) -> None:
gateway_module = _gateway_module()
await gateway_module._finalize_gateway_openai_video_delete_sync(payload, db=db)
async def _record_gateway_openai_video_cancel_sync_success(
payload: GatewaySyncReportRequest,
*,
db: Session,
) -> None:
gateway_module = _gateway_module()
await gateway_module._finalize_gateway_openai_video_cancel_sync(payload, db=db)
async def _record_gateway_gemini_video_cancel_sync_success(
payload: GatewaySyncReportRequest,
*,
db: Session,
) -> None:
gateway_module = _gateway_module()
await gateway_module._finalize_gateway_gemini_video_cancel_sync(payload, db=db)
async def _record_gateway_openai_video_create_sync_success(
payload: GatewaySyncReportRequest,
*,
db: Session,
) -> None:
gateway_module = _gateway_module()
await gateway_module._finalize_gateway_openai_video_create_sync(payload, db=db)
async def _record_gateway_openai_video_remix_sync_success(
payload: GatewaySyncReportRequest,
*,
db: Session,
) -> None:
gateway_module = _gateway_module()
await gateway_module._finalize_gateway_openai_video_remix_sync(payload, db=db)
async def _record_gateway_gemini_video_create_sync_success(
payload: GatewaySyncReportRequest,
*,
db: Session,
) -> None:
gateway_module = _gateway_module()
await gateway_module._finalize_gateway_gemini_video_create_sync(payload, db=db)

View File

@@ -0,0 +1,125 @@
from __future__ import annotations
import inspect
import time
import uuid
from datetime import datetime, timezone
from typing import Any
from fastapi import BackgroundTasks
from sqlalchemy.orm import Session
from src.core.logger import logger
from src.models.database import ApiKey, RequestCandidate, User
from .gateway_contract import GatewayStreamReportRequest, GatewaySyncReportRequest
from .gateway_reporting_common import _gateway_module
def _build_gateway_sync_telemetry_writer(
*,
db: Session,
request_id: str,
user_id: str,
api_key_id: str,
fallback_telemetry: Any,
) -> Any:
from src.config.settings import config
from src.services.system.config import SystemConfigService
from src.services.usage.telemetry_writer import DbTelemetryWriter, QueueTelemetryWriter
if config.usage_queue_enabled and user_id and api_key_id:
try:
log_level = SystemConfigService.get_request_record_level(db).value
sensitive_headers = SystemConfigService.get_sensitive_headers(db) or []
max_request_body_size = int(
SystemConfigService.get_config(db, "max_request_body_size", 5242880) or 0
)
max_response_body_size = int(
SystemConfigService.get_config(db, "max_response_body_size", 5242880) or 0
)
return QueueTelemetryWriter(
request_id=request_id,
user_id=user_id,
api_key_id=api_key_id,
log_level=log_level,
sensitive_headers=sensitive_headers,
max_request_body_size=max_request_body_size,
max_response_body_size=max_response_body_size,
)
except Exception:
return DbTelemetryWriter(fallback_telemetry)
return DbTelemetryWriter(fallback_telemetry)
async def _dispatch_gateway_sync_telemetry(
*,
telemetry_writer: Any,
operation: str,
**kwargs: Any,
) -> None:
gateway_module = _gateway_module()
submitter = getattr(telemetry_writer, operation, None)
if not callable(submitter):
raise AttributeError(f"Telemetry writer missing operation: {operation}")
if bool(getattr(telemetry_writer, "supports_background_submission", lambda: False)()):
request_id = str(getattr(telemetry_writer, "request_id", "") or "unknown")
async def _run_in_background() -> None:
try:
await submitter(**kwargs)
except Exception as exc:
logger.warning(
"[gateway] background telemetry submission failed: request_id={}, operation={}, error={}",
request_id,
operation,
exc,
)
task = gateway_module.safe_create_task(_run_in_background())
if task is None:
await submitter(**kwargs)
return
await submitter(**kwargs)
async def _schedule_gateway_sync_telemetry(
*,
background_tasks: BackgroundTasks | None,
telemetry_writer: Any,
operation: str,
**kwargs: Any,
) -> None:
gateway_module = _gateway_module()
if background_tasks is not None:
background_tasks.add_task(
gateway_module._dispatch_gateway_sync_telemetry,
telemetry_writer=telemetry_writer,
operation=operation,
**kwargs,
)
return
await gateway_module._dispatch_gateway_sync_telemetry(
telemetry_writer=telemetry_writer,
operation=operation,
**kwargs,
)
def _build_gateway_usage_metadata(
*,
request_metadata: dict[str, Any] | None = None,
response_metadata: dict[str, Any] | None = None,
) -> dict[str, Any] | None:
metadata: dict[str, Any] | None = None
if request_metadata:
metadata = dict(request_metadata)
if response_metadata:
metadata.setdefault("response", response_metadata)
elif response_metadata:
metadata = dict(response_metadata)
return metadata

View File

@@ -0,0 +1,386 @@
from __future__ import annotations
from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException, Request
from fastapi.responses import JSONResponse, Response
from sqlalchemy.orm import Session
from src.database import get_db
from . import gateway as gateway_impl
from .gateway_contract import (
CONTROL_EXECUTED_HEADER,
GatewayAuthContextRequest,
GatewayExecuteRequest,
GatewayResolveRequest,
GatewayStreamReportRequest,
GatewaySyncReportRequest,
classify_gateway_route,
)
router = APIRouter(
prefix="/api/internal/gateway",
tags=["Internal - Gateway"],
include_in_schema=False,
)
def _ensure_legacy_internal_gateway_request(request: Request) -> Response | None:
if gateway_impl._request_allows_legacy_chat_cli_internal_gateway(request):
return None
return gateway_impl._build_retired_internal_gateway_response()
@router.post("/resolve")
async def resolve_gateway_route(
request: Request, payload: GatewayResolveRequest
) -> Response:
gateway_impl.ensure_loopback(request)
retired = _ensure_legacy_internal_gateway_request(request)
if retired is not None:
return retired
decision = classify_gateway_route(payload.method, payload.path, payload.headers)
decision.auth_context = await gateway_impl._resolve_auth_context(payload, decision)
return JSONResponse(status_code=200, content=decision.model_dump(exclude_none=True))
@router.post("/auth-context")
async def resolve_gateway_auth_context(
request: Request,
payload: GatewayAuthContextRequest,
) -> Response:
gateway_impl.ensure_loopback(request)
retired = _ensure_legacy_internal_gateway_request(request)
if retired is not None:
return retired
auth_context = await gateway_impl._resolve_auth_context_signature(
headers=payload.headers,
query_string=payload.query_string,
auth_endpoint_signature=payload.auth_endpoint_signature,
)
return JSONResponse(status_code=200, content={"auth_context": auth_context})
@router.post("/decision-sync")
async def decide_gateway_sync(
request: Request,
payload: GatewayExecuteRequest,
db: Session = Depends(get_db),
) -> Response:
gateway_impl.ensure_loopback(request)
retired = _ensure_legacy_internal_gateway_request(request)
if retired is not None:
return retired
decision = classify_gateway_route(payload.method, payload.path, payload.headers)
if not gateway_impl._allows_legacy_chat_cli_internal_route(request, decision):
return gateway_impl._build_retired_internal_gateway_response()
auth_context = await gateway_impl._resolve_gateway_execute_auth_context(
payload=payload,
decision=decision,
)
if auth_context is None or not auth_context.access_allowed:
return JSONResponse(status_code=200, content={"action": "fallback_plan"})
try:
resolved = await gateway_impl._build_gateway_sync_decision_response(
request=request,
payload=payload,
db=db,
auth_context=auth_context,
decision=decision,
)
except HTTPException as exc:
headers = dict(exc.headers or {})
headers[CONTROL_EXECUTED_HEADER] = "true"
return JSONResponse(
status_code=exc.status_code,
content={"detail": exc.detail},
headers=headers,
)
if resolved is None:
body: dict[str, object] = {"action": "fallback_plan"}
if payload.auth_context is None:
body["auth_context"] = auth_context.model_dump(exclude_none=True)
return JSONResponse(status_code=200, content=body)
if payload.auth_context is not None:
resolved.auth_context = None
return JSONResponse(status_code=200, content=resolved.model_dump(exclude_none=True))
@router.post("/decision-stream")
async def decide_gateway_stream(
request: Request,
payload: GatewayExecuteRequest,
db: Session = Depends(get_db),
) -> Response:
gateway_impl.ensure_loopback(request)
retired = _ensure_legacy_internal_gateway_request(request)
if retired is not None:
return retired
decision = classify_gateway_route(payload.method, payload.path, payload.headers)
if not gateway_impl._allows_legacy_chat_cli_internal_route(request, decision):
return gateway_impl._build_retired_internal_gateway_response()
auth_context = await gateway_impl._resolve_gateway_execute_auth_context(
payload=payload,
decision=decision,
)
if auth_context is None or not auth_context.access_allowed:
return JSONResponse(status_code=200, content={"action": "fallback_plan"})
try:
resolved = await gateway_impl._build_gateway_stream_decision_response(
request=request,
payload=payload,
db=db,
auth_context=auth_context,
decision=decision,
)
except HTTPException as exc:
headers = dict(exc.headers or {})
headers[CONTROL_EXECUTED_HEADER] = "true"
return JSONResponse(
status_code=exc.status_code,
content={"detail": exc.detail},
headers=headers,
)
if resolved is None:
body: dict[str, object] = {"action": "fallback_plan"}
if payload.auth_context is None:
body["auth_context"] = auth_context.model_dump(exclude_none=True)
return JSONResponse(status_code=200, content=body)
if payload.auth_context is not None:
resolved.auth_context = None
return JSONResponse(status_code=200, content=resolved.model_dump(exclude_none=True))
@router.post("/execute-sync")
async def execute_gateway_sync(
request: Request,
payload: GatewayExecuteRequest,
db: Session = Depends(get_db),
) -> Response:
retired = _ensure_legacy_internal_gateway_request(request)
if retired is not None:
return retired
return await gateway_impl._execute_gateway_control_request(
request=request,
payload=payload,
db=db,
require_stream=False,
)
@router.post("/execute-stream")
async def execute_gateway_stream(
request: Request,
payload: GatewayExecuteRequest,
db: Session = Depends(get_db),
) -> Response:
retired = _ensure_legacy_internal_gateway_request(request)
if retired is not None:
return retired
return await gateway_impl._execute_gateway_control_request(
request=request,
payload=payload,
db=db,
require_stream=True,
)
@router.post("/plan-stream")
async def plan_gateway_stream(
request: Request,
payload: GatewayExecuteRequest,
db: Session = Depends(get_db),
) -> Response:
gateway_impl.ensure_loopback(request)
retired = _ensure_legacy_internal_gateway_request(request)
if retired is not None:
return retired
decision = classify_gateway_route(payload.method, payload.path, payload.headers)
if not gateway_impl._allows_legacy_chat_cli_internal_route(request, decision):
return gateway_impl._build_retired_internal_gateway_response()
try:
planned = await gateway_impl._build_gateway_stream_plan_response(
request=request,
payload=payload,
db=db,
)
except HTTPException as exc:
headers = dict(exc.headers or {})
headers[CONTROL_EXECUTED_HEADER] = "true"
return JSONResponse(
status_code=exc.status_code,
content={"detail": exc.detail},
headers=headers,
)
if planned is None:
response = await gateway_impl._execute_gateway_control_request(
request=request,
payload=payload,
db=db,
require_stream=True,
)
if gateway_impl._is_gateway_control_executed_response(response):
return response
return gateway_impl._build_proxy_public_fallback_response()
if payload.auth_context is not None:
planned.auth_context = None
return JSONResponse(status_code=200, content=planned.model_dump(exclude_none=True))
@router.post("/plan-sync")
async def plan_gateway_sync(
request: Request,
payload: GatewayExecuteRequest,
db: Session = Depends(get_db),
) -> Response:
gateway_impl.ensure_loopback(request)
retired = _ensure_legacy_internal_gateway_request(request)
if retired is not None:
return retired
decision = classify_gateway_route(payload.method, payload.path, payload.headers)
if not gateway_impl._allows_legacy_chat_cli_internal_route(request, decision):
return gateway_impl._build_retired_internal_gateway_response()
try:
planned = await gateway_impl._build_gateway_sync_plan_response(
request=request,
payload=payload,
db=db,
)
except HTTPException as exc:
headers = dict(exc.headers or {})
headers[CONTROL_EXECUTED_HEADER] = "true"
return JSONResponse(
status_code=exc.status_code,
content={"detail": exc.detail},
headers=headers,
)
if planned is None:
response = await gateway_impl._execute_gateway_control_request(
request=request,
payload=payload,
db=db,
require_stream=False,
)
if gateway_impl._is_gateway_control_executed_response(response):
return response
return gateway_impl._build_proxy_public_fallback_response()
if payload.auth_context is not None:
planned.auth_context = None
return JSONResponse(status_code=200, content=planned.model_dump(exclude_none=True))
@router.post("/report-sync")
async def report_gateway_sync(
request: Request,
payload: GatewaySyncReportRequest,
background_tasks: BackgroundTasks,
) -> Response:
gateway_impl.ensure_loopback(request)
retired = _ensure_legacy_internal_gateway_request(request)
if retired is not None:
return retired
if not gateway_impl._allows_legacy_chat_cli_report_route(request, payload.report_kind):
return gateway_impl._build_retired_internal_gateway_response()
payload_copy = payload.model_copy(deep=True)
if gateway_impl._gateway_sync_report_requires_inline(payload_copy):
db, cleanup = gateway_impl._resolve_gateway_background_db(getattr(request, "app", None))
try:
await gateway_impl._run_gateway_sync_report_background(payload_copy, db)
finally:
if cleanup is not None:
cleanup()
return JSONResponse(status_code=200, content={"ok": True})
background_tasks.add_task(
gateway_impl._run_gateway_sync_report_background_with_session,
payload_copy,
getattr(request, "app", None),
)
return JSONResponse(status_code=200, content={"ok": True})
@router.post("/finalize-sync")
async def finalize_gateway_sync(
request: Request,
payload: GatewaySyncReportRequest,
background_tasks: BackgroundTasks,
) -> Response:
gateway_impl.ensure_loopback(request)
retired = _ensure_legacy_internal_gateway_request(request)
if retired is not None:
return retired
if not gateway_impl._allows_legacy_chat_cli_report_route(request, payload.report_kind):
return gateway_impl._build_retired_internal_gateway_response()
fast_response = await gateway_impl._maybe_build_gateway_core_sync_fast_success_response(payload)
if fast_response is not None:
if payload.report_kind in {
"openai_chat_sync_finalize",
"claude_chat_sync_finalize",
"gemini_chat_sync_finalize",
}:
background_tasks.add_task(
gateway_impl._run_gateway_chat_sync_finalize_background_with_session,
payload.model_copy(deep=True),
)
elif payload.report_kind in {
"openai_cli_sync_finalize",
"openai_compact_sync_finalize",
"claude_cli_sync_finalize",
"gemini_cli_sync_finalize",
}:
background_tasks.add_task(
gateway_impl._run_gateway_cli_sync_finalize_background_with_session,
payload.model_copy(deep=True),
)
fast_response.headers[CONTROL_EXECUTED_HEADER] = "true"
return fast_response
db, cleanup = gateway_impl._resolve_gateway_finalize_db(request)
try:
response = await gateway_impl._finalize_gateway_sync_response(
payload,
db=db,
background_tasks=background_tasks,
)
except Exception:
if cleanup is not None:
cleanup()
raise
if cleanup is not None:
background_tasks.add_task(cleanup)
response.headers[CONTROL_EXECUTED_HEADER] = "true"
return response
@router.post("/report-stream")
async def report_gateway_stream(
request: Request,
payload: GatewayStreamReportRequest,
background_tasks: BackgroundTasks,
) -> Response:
gateway_impl.ensure_loopback(request)
retired = _ensure_legacy_internal_gateway_request(request)
if retired is not None:
return retired
if not gateway_impl._allows_legacy_chat_cli_report_route(request, payload.report_kind):
return gateway_impl._build_retired_internal_gateway_response()
background_tasks.add_task(
gateway_impl._run_gateway_stream_report_background_with_session,
payload.model_copy(deep=True),
getattr(request, "app", None),
)
return JSONResponse(status_code=200, content={"ok": True})

View File

@@ -0,0 +1,594 @@
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()
LEGACY_CHAT_CLI_CONTROL_EXECUTE_FALLBACK_HEADER = "x-aether-control-execute-fallback"
LEGACY_CHAT_CLI_INTERNAL_GATEWAY_HEADER = "x-aether-legacy-internal-gateway"
_LEGACY_CHAT_CLI_INTERNAL_GATEWAY_TRUE_VALUES = {"1", "true", "yes", "on"}
_LEGACY_CHAT_CLI_REPORT_KIND_PREFIXES = (
"openai_chat_",
"claude_chat_",
"gemini_chat_",
"openai_cli_",
"openai_compact_",
"claude_cli_",
"gemini_cli_",
)
def _is_legacy_chat_cli_control_route(decision: GatewayRouteDecision) -> bool:
return decision.route_class == "ai_public" and decision.route_kind in {
"chat",
"cli",
"compact",
}
def _request_allows_legacy_chat_cli_internal_gateway(request: Request) -> bool:
for key, value in request.headers.items():
normalized_key = str(key or "").strip().lower()
if normalized_key not in {
LEGACY_CHAT_CLI_INTERNAL_GATEWAY_HEADER,
LEGACY_CHAT_CLI_CONTROL_EXECUTE_FALLBACK_HEADER,
}:
continue
return (
str(value or "").strip().lower()
in _LEGACY_CHAT_CLI_INTERNAL_GATEWAY_TRUE_VALUES
)
return False
def _allows_legacy_chat_cli_internal_route(
request: Request,
decision: GatewayRouteDecision,
) -> bool:
if not _is_legacy_chat_cli_control_route(decision):
return True
return _request_allows_legacy_chat_cli_internal_gateway(request)
def _is_legacy_chat_cli_report_kind(report_kind: str | None) -> bool:
normalized = str(report_kind or "").strip().lower()
if not normalized:
return False
return normalized.startswith(_LEGACY_CHAT_CLI_REPORT_KIND_PREFIXES)
def _allows_legacy_chat_cli_report_route(request: Request, report_kind: str | None) -> bool:
if not _is_legacy_chat_cli_report_kind(report_kind):
return True
return _request_allows_legacy_chat_cli_internal_gateway(request)
def _build_retired_internal_gateway_response() -> JSONResponse:
return JSONResponse(
status_code=410,
content={"detail": "legacy internal gateway route removed; use public proxy"},
)
def _allows_legacy_chat_cli_control_execute(
request: Request,
payload: GatewayExecuteRequest,
decision: GatewayRouteDecision,
) -> bool:
if not _is_legacy_chat_cli_control_route(decision):
return True
if not _allows_legacy_chat_cli_internal_route(request, decision):
return False
for key, value in (payload.headers or {}).items():
if str(key or "").strip().lower() != LEGACY_CHAT_CLI_CONTROL_EXECUTE_FALLBACK_HEADER:
continue
return str(value or "").strip().lower() in {"1", "true", "yes", "on"}
return False
async def _execute_gateway_control_request(
*,
request: Request,
payload: GatewayExecuteRequest,
db: Session,
require_stream: bool,
) -> Response:
gateway_module.ensure_loopback(request)
decision = classify_gateway_route(payload.method, payload.path, payload.headers)
if _is_gemini_files_route(decision):
return await _execute_gateway_files_control_request(
request=request,
payload=payload,
db=db,
require_stream=require_stream,
)
if not _allows_legacy_chat_cli_control_execute(request, payload, decision):
return gateway_module._build_retired_internal_gateway_response()
adapter, path_params = gateway_module._resolve_gateway_sync_adapter(decision, payload.path)
if adapter is None:
return gateway_module._build_proxy_public_fallback_response()
is_stream_request = _is_stream_request_payload(payload.body_json, path_params)
if is_stream_request != require_stream:
return gateway_module._build_proxy_public_fallback_response()
auth_context = await gateway_module._resolve_gateway_execute_auth_context(
payload=payload,
decision=decision,
)
if auth_context is None or not auth_context.access_allowed:
return gateway_module._build_proxy_public_fallback_response()
try:
effective_request = (
gateway_module._build_gateway_forward_request(request=request, payload=payload)
if _is_video_route(decision)
else request
)
user, api_key = gateway_module._load_gateway_auth_models(db, auth_context)
pipeline = gateway_module.get_pipeline()
await pipeline._check_user_rate_limit(effective_request, db, user, api_key)
context = gateway_module._build_gateway_request_context(
request=effective_request,
payload=payload,
db=db,
user=user,
api_key=api_key,
adapter=adapter,
path_params=path_params,
balance_remaining=auth_context.balance_remaining,
)
authorize_result = adapter.authorize(context)
if hasattr(authorize_result, "__await__"):
await authorize_result
response = await adapter.handle(context)
except HTTPException as exc:
headers = dict(exc.headers or {})
headers[CONTROL_EXECUTED_HEADER] = "true"
return JSONResponse(
status_code=exc.status_code,
content={"detail": exc.detail},
headers=headers,
)
response.headers[CONTROL_EXECUTED_HEADER] = "true"
return response
def _is_gateway_control_executed_response(response: Response) -> bool:
return str(response.headers.get(CONTROL_EXECUTED_HEADER) or "").strip().lower() == "true"
async def _execute_gateway_files_control_request(
*,
request: Request,
payload: GatewayExecuteRequest,
db: Session,
require_stream: bool,
) -> Response:
if require_stream:
return gateway_module._build_proxy_public_fallback_response()
decision = classify_gateway_route(payload.method, payload.path, payload.headers)
auth_context = await gateway_module._resolve_gateway_execute_auth_context(
payload=payload,
decision=decision,
)
if auth_context is None or not auth_context.access_allowed:
return gateway_module._build_proxy_public_fallback_response()
try:
user, api_key = gateway_module._load_gateway_auth_models(db, auth_context)
gateway_request = gateway_module._build_gateway_forward_request(
request=request, payload=payload
)
pipeline = gateway_module.get_pipeline()
await pipeline._check_user_rate_limit(gateway_request, db, user, api_key)
response = await _dispatch_gateway_files_handler(gateway_request, payload)
except HTTPException as exc:
headers = dict(exc.headers or {})
headers[CONTROL_EXECUTED_HEADER] = "true"
return JSONResponse(
status_code=exc.status_code,
content={"detail": exc.detail},
headers=headers,
)
response.headers[CONTROL_EXECUTED_HEADER] = "true"
return response
def _is_gemini_files_route(decision: GatewayRouteDecision) -> bool:
return (
decision.route_class == "ai_public"
and decision.route_family == "gemini"
and decision.route_kind == "files"
)
def _is_video_route(decision: GatewayRouteDecision) -> bool:
return decision.route_class == "ai_public" and decision.route_kind == "video"
async def _dispatch_gateway_files_handler(
request: Request,
payload: GatewayExecuteRequest,
) -> Response:
from src.api.public.gemini_files import (
delete_file,
download_file,
get_file,
list_files,
upload_file,
)
method = str(payload.method or "").strip().upper()
path = str(payload.path or "").strip()
if method == "POST" and path == "/upload/v1beta/files":
return await upload_file(request)
if method == "GET" and path == "/v1beta/files":
query_params = gateway_module._parse_query_string(payload.query_string)
page_size: int | None = None
if query_params.get("pageSize") not in {None, ""}:
try:
page_size = int(str(query_params["pageSize"]))
except ValueError as exc:
raise HTTPException(status_code=400, detail="Invalid pageSize") from exc
return await list_files(
request,
pageSize=page_size,
pageToken=query_params.get("pageToken"),
)
download_match = _GEMINI_FILES_DOWNLOAD_ROUTE_RE.match(path)
if method == "GET" and download_match:
return await download_file(download_match.group("file_id"), request)
resource_match = _GEMINI_FILES_RESOURCE_ROUTE_RE.match(path)
if resource_match:
file_name = resource_match.group("file_name")
if method == "GET":
return await get_file(file_name, request)
if method == "DELETE":
return await delete_file(file_name, request)
return gateway_module._build_proxy_public_fallback_response()
def _extract_gemini_path_params(path: str) -> dict[str, Any]:
match = _GEMINI_SYNC_ROUTE_RE.match(str(path or "").strip())
if not match:
return {}
action = str(match.group("action") or "").strip()
return {
"model": str(match.group("model") or "").strip(),
"stream": action == "streamGenerateContent",
}
def _extract_openai_video_path_params(path: str) -> dict[str, Any]:
normalized_path = str(path or "").strip()
for route_re, extra in (
(_OPENAI_VIDEO_CANCEL_ROUTE_RE, {"action": "cancel"}),
(_OPENAI_VIDEO_REMIX_ROUTE_RE, {}),
(_OPENAI_VIDEO_CONTENT_ROUTE_RE, {}),
(_OPENAI_VIDEO_TASK_ROUTE_RE, {}),
):
match = route_re.match(normalized_path)
if match:
params = {"task_id": str(match.group("task_id") or "").strip()}
params.update(extra)
return params
return {}
def _extract_gemini_video_path_params(path: str) -> dict[str, Any]:
normalized_path = str(path or "").strip()
create_match = _GEMINI_VIDEO_CREATE_ROUTE_RE.match(normalized_path)
if create_match:
return {"model": str(create_match.group("model") or "").strip()}
model_operation_match = _GEMINI_VIDEO_MODEL_OPERATION_ANY_RE.match(normalized_path)
if model_operation_match:
operation_name = (
f"models/{str(model_operation_match.group('model') or '').strip()}/operations/"
f"{str(model_operation_match.group('operation_id') or '').strip()}"
)
params = {"task_id": operation_name}
if model_operation_match.group("cancel"):
params["action"] = "cancel"
return params
if normalized_path == "/v1beta/operations":
return {}
if normalized_path.startswith("/v1beta/operations/"):
operation_id = normalized_path[len("/v1beta/operations/") :].strip()
if operation_id.endswith(":cancel"):
return {"task_id": operation_id[: -len(":cancel")], "action": "cancel"}
return {"task_id": operation_id}
return {}
def _is_stream_request_payload(
body_json: dict[str, Any],
path_params: dict[str, Any] | None = None,
) -> bool:
if bool((path_params or {}).get("stream")):
return True
return bool(body_json.get("stream"))
def _load_gateway_auth_models(
db: Session,
auth_context: GatewayAuthContext,
) -> tuple[User, ApiKey]:
user = db.query(User).filter(User.id == auth_context.user_id).first()
api_key = db.query(ApiKey).filter(ApiKey.id == auth_context.api_key_id).first()
if not user or not api_key:
raise HTTPException(status_code=401, detail="无效的API密钥")
if not user.is_active or user.is_deleted:
raise HTTPException(status_code=401, detail="无效的API密钥")
if not api_key.is_active:
raise HTTPException(status_code=401, detail="无效的API密钥")
if api_key.is_locked and not api_key.is_standalone:
raise HTTPException(status_code=403, detail="该密钥已被管理员锁定,请联系管理员")
if str(api_key.user_id) != str(user.id):
raise HTTPException(status_code=401, detail="无效的API密钥")
return user, api_key
def _build_gateway_request_context(
*,
request: Request,
payload: GatewayExecuteRequest,
db: Session,
user: User,
api_key: ApiKey,
adapter: Any,
path_params: dict[str, Any],
balance_remaining: float | None,
) -> ApiRequestContext:
request_id = str(
payload.trace_id or getattr(request.state, "request_id", "") or uuid.uuid4().hex[:8]
)
request.state.request_id = request_id
request.state.user_id = user.id
request.state.api_key_id = api_key.id
request.state.prefetched_balance_remaining = balance_remaining
original_headers = {str(k): str(v) for k, v in (payload.headers or {}).items()}
client_accept_encoding = str(original_headers.get("accept-encoding") or "").strip() or None
raw_body = gateway_module._extract_gateway_raw_body(payload)
return ApiRequestContext(
request=request,
db=db,
user=user,
api_key=api_key,
request_id=request_id,
start_time=time.time(),
request_method=str(payload.method or request.method or "GET").upper(),
request_path=str(payload.path or request.url.path or "/"),
client_ip=request.client.host if request.client else "127.0.0.1",
user_agent=str(original_headers.get("user-agent") or "unknown"),
original_headers=original_headers,
query_params=gateway_module._parse_query_string(payload.query_string),
request_content_type=str(original_headers.get("content-type") or "").strip() or None,
raw_body=raw_body,
json_body=(dict(payload.body_json) if payload.body_json else None),
balance_remaining=balance_remaining,
mode=getattr(getattr(adapter, "mode", None), "value", "standard"),
api_format_hint=(
adapter.allowed_api_formats[0]
if getattr(adapter, "allowed_api_formats", None)
else None
),
path_params=dict(path_params or {}),
client_content_encoding=normalize_content_encoding(
original_headers.get("content-encoding")
),
client_accept_encoding=client_accept_encoding,
)
def _build_gateway_forward_request(
*,
request: Request,
payload: GatewayExecuteRequest,
) -> Request:
body = gateway_module._decode_gateway_body(payload)
header_items = [
(str(key).encode("latin-1"), str(value).encode("latin-1"))
for key, value in (payload.headers or {}).items()
]
scope = {
"type": "http",
"http_version": "1.1",
"method": str(payload.method or "GET").upper(),
"scheme": "http",
"path": str(payload.path or "/"),
"raw_path": str(payload.path or "/").encode("utf-8"),
"query_string": str(payload.query_string or "").encode("utf-8"),
"headers": header_items,
"client": ("127.0.0.1", 0),
"server": ("127.0.0.1", 80),
"app": request.app,
"state": {"request_id": str(payload.trace_id or uuid.uuid4().hex[:8])},
}
received = False
async def receive() -> dict[str, object]:
nonlocal received
if received:
return {"type": "http.request", "body": b"", "more_body": False}
received = True
return {"type": "http.request", "body": body, "more_body": False}
return Request(scope, receive)
def _build_proxy_public_fallback_response() -> JSONResponse:
return JSONResponse(
status_code=409,
content={"action": CONTROL_ACTION_PROXY_PUBLIC},
headers={CONTROL_ACTION_HEADER: CONTROL_ACTION_PROXY_PUBLIC},
)
def _stream_executor_requires_python_rewrite(
*,
envelope: Any = None,
needs_conversion: bool,
provider_api_format: str | None = None,
client_api_format: str | None = None,
) -> bool:
provider_api_format = str(provider_api_format or "").strip().lower()
client_api_format = str(client_api_format or "").strip().lower()
if needs_conversion:
if (
envelope is None
and (
(
provider_api_format in {"claude:chat", "gemini:chat"}
and client_api_format == "openai:chat"
)
or (
provider_api_format in {"claude:cli", "gemini:cli"}
and client_api_format in {"openai:cli", "openai:compact"}
)
)
) or (
str(getattr(envelope, "name", "") or "").strip().lower() == "antigravity:v1internal"
and (
(provider_api_format == "gemini:chat" and client_api_format == "openai:chat")
or (
provider_api_format == "gemini:cli"
and client_api_format in {"openai:cli", "openai:compact"}
)
)
):
return False
return True
if envelope is None:
return False
try:
requires_rewrite = bool(envelope.force_stream_rewrite())
except Exception:
return True
if not requires_rewrite:
return False
envelope_name = str(getattr(envelope, "name", "") or "").strip().lower()
if (
envelope_name == "antigravity:v1internal"
and provider_api_format == client_api_format
and provider_api_format in {"gemini:chat", "gemini:cli"}
):
return False
if (
envelope_name == "kiro:generateassistantresponse"
and provider_api_format == "claude:cli"
and client_api_format == "claude:cli"
):
return False
return True
def _serialize_gateway_sync_proxy(proxy: Any) -> dict[str, Any] | None:
if proxy is None:
return None
raw = asdict(proxy)
return {key: value for key, value in raw.items() if value is not None}
def _serialize_gateway_sync_timeouts(timeouts: Any) -> dict[str, Any] | None:
if timeouts is None:
return None
raw = asdict(timeouts)
return {key: value for key, value in raw.items() if value is not None}
def _extract_gateway_upstream_auth(
provider_request_headers: dict[str, str],
*,
provider_api_format: str,
key: Any,
) -> tuple[str, str]:
normalized_headers = {
str(header_name).strip().lower(): str(header_value).strip()
for header_name, header_value in (provider_request_headers or {}).items()
if str(header_name).strip() and str(header_value).strip()
}
for header_name in ("authorization", "x-api-key", "x-goog-api-key"):
header_value = normalized_headers.get(header_name)
if header_value:
return header_name, header_value
auth_header, auth_type = get_auth_config_for_endpoint(provider_api_format)
decrypted_key = crypto_service.decrypt(key.api_key)
auth_value = f"Bearer {decrypted_key}" if auth_type == "bearer" else decrypted_key
return str(auth_header or "").strip() or "authorization", auth_value

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,97 @@
from __future__ import annotations
import asyncio
from typing import Any
from fastapi import APIRouter, HTTPException, Request
from pydantic import BaseModel, Field
from src.services.proxy_node.service import ProxyNodeService, build_heartbeat_ack
from .common import ensure_loopback
router = APIRouter(
prefix="/api/internal/tunnel",
tags=["Internal - Tunnel"],
include_in_schema=False,
)
class TunnelHeartbeatRequest(BaseModel):
node_id: str = Field(..., min_length=1, max_length=36)
heartbeat_interval: int | None = Field(None, ge=5, le=600)
active_connections: int | None = Field(None, ge=0)
total_requests: int | None = Field(None, ge=0)
avg_latency_ms: float | None = Field(None, ge=0)
failed_requests: int | None = Field(None, ge=0)
dns_failures: int | None = Field(None, ge=0)
stream_errors: int | None = Field(None, ge=0)
proxy_metadata: dict[str, Any] | None = None
proxy_version: str | None = Field(None, max_length=20)
class TunnelNodeStatusRequest(BaseModel):
node_id: str = Field(..., min_length=1, max_length=36)
connected: bool
conn_count: int = Field(0, ge=0)
@router.post("/heartbeat")
async def tunnel_heartbeat(
request: Request, payload: TunnelHeartbeatRequest
) -> dict[str, Any]:
ensure_loopback(request)
def _sync_apply() -> dict[str, Any]:
from src.database import create_session
db = create_session()
try:
node = ProxyNodeService.heartbeat(
db,
node_id=payload.node_id,
heartbeat_interval=payload.heartbeat_interval,
active_connections=payload.active_connections,
total_requests=payload.total_requests,
avg_latency_ms=payload.avg_latency_ms,
failed_requests=payload.failed_requests,
dns_failures=payload.dns_failures,
stream_errors=payload.stream_errors,
proxy_metadata=payload.proxy_metadata,
proxy_version=payload.proxy_version,
)
return build_heartbeat_ack(node)
finally:
db.close()
try:
return await asyncio.to_thread(_sync_apply)
except Exception as exc:
raise HTTPException(status_code=500, detail=f"heartbeat sync failed: {exc}") from exc
@router.post("/node-status")
async def tunnel_node_status(
request: Request, payload: TunnelNodeStatusRequest
) -> dict[str, Any]:
ensure_loopback(request)
def _sync_apply() -> dict[str, Any]:
from src.database import create_session
db = create_session()
try:
node = ProxyNodeService.update_tunnel_status(
db,
node_id=payload.node_id,
connected=payload.connected,
conn_count=payload.conn_count,
)
return {"updated": node is not None}
finally:
db.close()
try:
return await asyncio.to_thread(_sync_apply)
except Exception as exc:
raise HTTPException(status_code=500, detail=f"node status sync failed: {exc}") from exc