mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 01:10:23 +08:00
refactor: 移除 Python 后端源码,全面迁移至 Rust gateway 架构
- 删除全部 Python 源码 (src/) 及 Alembic 迁移脚本,归档至 _deprecated_py_src/ - 重构 Rust gateway ai_pipeline: 拆分 planner/finalize 模块,新增 contracts/adaptation 层 - 重组 handlers 模块为 admin/public/proxy/internal/shared 子模块结构 - 新增 executor 模块,引入 Rust 原生数据库迁移 (aether-data/migrations) - 简化 CI/Docker 构建流程,移除 base image 二级构建,统一为单一 app image - 移除 Python 相关基础设施文件 (entrypoint.sh, gunicorn_conf.py, Dockerfile.base)
This commit is contained in:
19
_deprecated_py_src/api/internal/__init__.py
Normal file
19
_deprecated_py_src/api/internal/__init__.py
Normal 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",
|
||||
]
|
||||
14
_deprecated_py_src/api/internal/common.py
Normal file
14
_deprecated_py_src/api/internal/common.py
Normal 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
|
||||
1255
_deprecated_py_src/api/internal/gateway.py
Normal file
1255
_deprecated_py_src/api/internal/gateway.py
Normal file
File diff suppressed because it is too large
Load Diff
43
_deprecated_py_src/api/internal/gateway_chat.py
Normal file
43
_deprecated_py_src/api/internal/gateway_chat.py
Normal 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",
|
||||
]
|
||||
945
_deprecated_py_src/api/internal/gateway_chat_decision.py
Normal file
945
_deprecated_py_src/api/internal/gateway_chat_decision.py
Normal 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",
|
||||
)
|
||||
591
_deprecated_py_src/api/internal/gateway_chat_plan.py
Normal file
591
_deprecated_py_src/api/internal/gateway_chat_plan.py
Normal 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",
|
||||
)
|
||||
43
_deprecated_py_src/api/internal/gateway_cli.py
Normal file
43
_deprecated_py_src/api/internal/gateway_cli.py
Normal 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",
|
||||
]
|
||||
945
_deprecated_py_src/api/internal/gateway_cli_decision.py
Normal file
945
_deprecated_py_src/api/internal/gateway_cli_decision.py
Normal 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",
|
||||
)
|
||||
681
_deprecated_py_src/api/internal/gateway_cli_plan.py
Normal file
681
_deprecated_py_src/api/internal/gateway_cli_plan.py
Normal 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",
|
||||
)
|
||||
277
_deprecated_py_src/api/internal/gateway_contract.py
Normal file
277
_deprecated_py_src/api/internal/gateway_contract.py
Normal 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
|
||||
853
_deprecated_py_src/api/internal/gateway_decision_plan.py
Normal file
853
_deprecated_py_src/api/internal/gateway_decision_plan.py
Normal 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
|
||||
546
_deprecated_py_src/api/internal/gateway_files.py
Normal file
546
_deprecated_py_src/api/internal/gateway_files.py
Normal 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 ""),
|
||||
}
|
||||
41
_deprecated_py_src/api/internal/gateway_finalize.py
Normal file
41
_deprecated_py_src/api/internal/gateway_finalize.py
Normal 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",
|
||||
]
|
||||
360
_deprecated_py_src/api/internal/gateway_finalize_chat.py
Normal file
360
_deprecated_py_src/api/internal/gateway_finalize_chat.py
Normal 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
|
||||
366
_deprecated_py_src/api/internal/gateway_finalize_cli.py
Normal file
366
_deprecated_py_src/api/internal/gateway_finalize_cli.py
Normal 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
|
||||
248
_deprecated_py_src/api/internal/gateway_finalize_common.py
Normal file
248
_deprecated_py_src/api/internal/gateway_finalize_common.py
Normal 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",
|
||||
)
|
||||
95
_deprecated_py_src/api/internal/gateway_reporting.py
Normal file
95
_deprecated_py_src/api/internal/gateway_reporting.py
Normal 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",
|
||||
]
|
||||
265
_deprecated_py_src/api/internal/gateway_reporting_background.py
Normal file
265
_deprecated_py_src/api/internal/gateway_reporting_background.py
Normal 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)
|
||||
273
_deprecated_py_src/api/internal/gateway_reporting_candidates.py
Normal file
273
_deprecated_py_src/api/internal/gateway_reporting_candidates.py
Normal 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"},
|
||||
)
|
||||
21
_deprecated_py_src/api/internal/gateway_reporting_common.py
Normal file
21
_deprecated_py_src/api/internal/gateway_reporting_common.py
Normal 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
|
||||
165
_deprecated_py_src/api/internal/gateway_reporting_failures.py
Normal file
165
_deprecated_py_src/api/internal/gateway_reporting_failures.py
Normal 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)
|
||||
@@ -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 {}),
|
||||
)
|
||||
@@ -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)
|
||||
125
_deprecated_py_src/api/internal/gateway_reporting_telemetry.py
Normal file
125
_deprecated_py_src/api/internal/gateway_reporting_telemetry.py
Normal 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
|
||||
386
_deprecated_py_src/api/internal/gateway_routes.py
Normal file
386
_deprecated_py_src/api/internal/gateway_routes.py
Normal 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})
|
||||
594
_deprecated_py_src/api/internal/gateway_shared.py
Normal file
594
_deprecated_py_src/api/internal/gateway_shared.py
Normal 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
|
||||
1360
_deprecated_py_src/api/internal/gateway_video.py
Normal file
1360
_deprecated_py_src/api/internal/gateway_video.py
Normal file
File diff suppressed because it is too large
Load Diff
97
_deprecated_py_src/api/internal/tunnel.py
Normal file
97
_deprecated_py_src/api/internal/tunnel.py
Normal 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
|
||||
Reference in New Issue
Block a user