mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 17:30:23 +08:00
- 删除全部 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)
1256 lines
47 KiB
Python
1256 lines
47 KiB
Python
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, VideoTask
|
|
from src.services.auth.service import AuthService
|
|
from src.utils.async_utils import safe_create_task
|
|
|
|
from .common import ensure_loopback
|
|
from .gateway_chat import (
|
|
_build_chat_stream_decision,
|
|
_build_chat_stream_plan,
|
|
_build_chat_sync_decision,
|
|
_build_chat_sync_plan,
|
|
_build_claude_chat_stream_decision,
|
|
_build_claude_chat_stream_plan,
|
|
_build_claude_chat_sync_decision,
|
|
_build_claude_chat_sync_plan,
|
|
_build_gemini_chat_stream_decision,
|
|
_build_gemini_chat_stream_plan,
|
|
_build_gemini_chat_sync_decision,
|
|
_build_gemini_chat_sync_plan,
|
|
_build_openai_chat_stream_decision,
|
|
_build_openai_chat_stream_plan,
|
|
_build_openai_chat_sync_decision,
|
|
_build_openai_chat_sync_plan,
|
|
)
|
|
from .gateway_cli import (
|
|
_build_claude_cli_stream_decision,
|
|
_build_claude_cli_stream_plan,
|
|
_build_claude_cli_sync_decision,
|
|
_build_claude_cli_sync_plan,
|
|
_build_cli_stream_decision,
|
|
_build_cli_stream_plan,
|
|
_build_cli_sync_decision,
|
|
_build_cli_sync_plan,
|
|
_build_gemini_cli_stream_decision,
|
|
_build_gemini_cli_stream_plan,
|
|
_build_gemini_cli_sync_decision,
|
|
_build_gemini_cli_sync_plan,
|
|
_build_openai_cli_stream_decision,
|
|
_build_openai_cli_stream_plan,
|
|
_build_openai_cli_sync_decision,
|
|
_build_openai_cli_sync_plan,
|
|
)
|
|
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,
|
|
)
|
|
from .gateway_decision_plan import (
|
|
_build_gateway_stream_decision_response,
|
|
_build_gateway_stream_plan_response,
|
|
_build_gateway_sync_decision_response,
|
|
_build_gateway_sync_plan_response,
|
|
_parse_query_string,
|
|
_resolve_auth_context,
|
|
_resolve_auth_context_signature,
|
|
_resolve_gateway_execute_auth_context,
|
|
_resolve_gateway_sync_adapter,
|
|
)
|
|
from .gateway_files import (
|
|
_build_gemini_files_download_stream_decision,
|
|
_build_gemini_files_download_stream_plan,
|
|
_build_gemini_files_get_sync_plan,
|
|
_build_gemini_files_proxy_sync_decision,
|
|
_build_gemini_files_proxy_sync_plan,
|
|
)
|
|
from .gateway_finalize import (
|
|
_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,
|
|
_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,
|
|
)
|
|
from .gateway_reporting import (
|
|
_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_sync_report_requires_inline,
|
|
_mark_gateway_sync_candidate_terminal_state,
|
|
_postprocess_gateway_report_provider_response,
|
|
_record_gateway_chat_sync_failure,
|
|
_record_gateway_cli_sync_failure,
|
|
_record_gateway_direct_candidate_graph,
|
|
_record_gateway_gemini_video_cancel_sync_success,
|
|
_record_gateway_gemini_video_create_sync_success,
|
|
_record_gateway_openai_chat_stream_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_stream_success,
|
|
_record_gateway_passthrough_chat_sync_success,
|
|
_record_gateway_passthrough_cli_stream_success,
|
|
_record_gateway_passthrough_cli_sync_success,
|
|
_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,
|
|
)
|
|
from .gateway_shared import (
|
|
LEGACY_CHAT_CLI_INTERNAL_GATEWAY_HEADER,
|
|
_allows_legacy_chat_cli_internal_route,
|
|
_allows_legacy_chat_cli_report_route,
|
|
_build_retired_internal_gateway_response,
|
|
_build_gateway_forward_request,
|
|
_build_gateway_request_context,
|
|
_build_proxy_public_fallback_response,
|
|
_dispatch_gateway_files_handler,
|
|
_execute_gateway_control_request,
|
|
_execute_gateway_files_control_request,
|
|
_extract_gateway_upstream_auth,
|
|
_extract_gemini_path_params,
|
|
_extract_gemini_video_path_params,
|
|
_extract_openai_video_path_params,
|
|
_is_gateway_control_executed_response,
|
|
_is_gemini_files_route,
|
|
_is_stream_request_payload,
|
|
_is_video_route,
|
|
_request_allows_legacy_chat_cli_internal_gateway,
|
|
_load_gateway_auth_models,
|
|
_serialize_gateway_sync_proxy,
|
|
_serialize_gateway_sync_timeouts,
|
|
_stream_executor_requires_python_rewrite,
|
|
)
|
|
from .gateway_video import (
|
|
_build_gemini_video_cancel_sync_decision,
|
|
_build_gemini_video_cancel_sync_plan,
|
|
_build_gemini_video_create_sync_decision,
|
|
_build_gemini_video_create_sync_plan,
|
|
_build_openai_video_cancel_sync_decision,
|
|
_build_openai_video_cancel_sync_plan,
|
|
_build_openai_video_content_stream_decision,
|
|
_build_openai_video_content_stream_plan,
|
|
_build_openai_video_create_sync_decision,
|
|
_build_openai_video_create_sync_plan,
|
|
_build_openai_video_delete_sync_decision,
|
|
_build_openai_video_delete_sync_plan,
|
|
_build_openai_video_remix_sync_decision,
|
|
_build_openai_video_remix_sync_plan,
|
|
)
|
|
|
|
|
|
def _is_streaming_sync_payload(
|
|
body_json: dict[str, Any],
|
|
path_params: dict[str, Any] | None = None,
|
|
) -> bool:
|
|
return _is_stream_request_payload(body_json, path_params)
|
|
|
|
|
|
def _decode_gateway_body(payload: GatewayExecuteRequest) -> bytes:
|
|
if not payload.body_base64:
|
|
return b""
|
|
try:
|
|
return base64.b64decode(payload.body_base64, validate=True)
|
|
except Exception as exc:
|
|
raise HTTPException(status_code=400, detail="Invalid gateway body payload") from exc
|
|
|
|
|
|
def _extract_gateway_raw_body(payload: GatewayExecuteRequest) -> bytes:
|
|
if payload.body_base64:
|
|
return _decode_gateway_body(payload)
|
|
if payload.body_json:
|
|
return JSONResponse(content=payload.body_json).body
|
|
return b""
|
|
|
|
|
|
async def _select_gateway_direct_candidate(
|
|
*,
|
|
db: Session,
|
|
redis_client: Any | None,
|
|
api_format: str,
|
|
model_name: str,
|
|
user_api_key: ApiKey,
|
|
request_id: str,
|
|
is_stream: bool,
|
|
capability_requirements: dict[str, bool] | None,
|
|
preferred_key_ids: list[str] | None,
|
|
request_body: dict[str, Any] | None,
|
|
) -> Any | None:
|
|
from src.services.candidate.resolver import CandidateResolver
|
|
from src.services.scheduling.aware_scheduler import (
|
|
CacheAwareScheduler,
|
|
get_cache_aware_scheduler,
|
|
)
|
|
from src.services.system.config import SystemConfigService
|
|
from src.services.task.execute.pool import TaskPoolOperationsService
|
|
|
|
priority_mode = SystemConfigService.get_config(
|
|
db,
|
|
"provider_priority_mode",
|
|
CacheAwareScheduler.PRIORITY_MODE_PROVIDER,
|
|
)
|
|
scheduling_mode = SystemConfigService.get_config(
|
|
db,
|
|
"scheduling_mode",
|
|
CacheAwareScheduler.SCHEDULING_MODE_CACHE_AFFINITY,
|
|
)
|
|
cache_scheduler = await get_cache_aware_scheduler(
|
|
redis_client,
|
|
priority_mode=priority_mode,
|
|
scheduling_mode=scheduling_mode,
|
|
)
|
|
await cache_scheduler._ensure_initialized()
|
|
|
|
candidate_resolver = CandidateResolver(
|
|
db=db,
|
|
cache_scheduler=cache_scheduler,
|
|
)
|
|
candidates, _global_model_id = await candidate_resolver.fetch_candidates(
|
|
api_format=api_format,
|
|
model_name=model_name,
|
|
affinity_key=str(user_api_key.id),
|
|
user_api_key=user_api_key,
|
|
request_id=request_id,
|
|
is_stream=is_stream,
|
|
capability_requirements=capability_requirements,
|
|
preferred_key_ids=preferred_key_ids,
|
|
request_body=request_body,
|
|
)
|
|
candidates, _pool_traces = await TaskPoolOperationsService().apply_pool_reorder(
|
|
candidates,
|
|
request_body=request_body,
|
|
)
|
|
if candidates:
|
|
_record_gateway_direct_candidate_graph(
|
|
db=db,
|
|
candidate_resolver=candidate_resolver,
|
|
candidates=candidates,
|
|
request_id=request_id,
|
|
user_api_key=user_api_key,
|
|
required_capabilities=capability_requirements,
|
|
selected_candidate_index=0,
|
|
)
|
|
return candidates[0] if candidates else None
|
|
|
|
|
|
def _gateway_sync_report_context_flag(context: dict[str, Any], key: str) -> bool | None:
|
|
value = context.get(key)
|
|
if isinstance(value, bool):
|
|
return value
|
|
return None
|
|
|
|
|
|
def _gateway_report_context_envelope_name(envelope: Any) -> str | None:
|
|
name = getattr(envelope, "name", None)
|
|
if isinstance(name, str):
|
|
normalized = name.strip()
|
|
if normalized:
|
|
return normalized
|
|
return None
|
|
|
|
|
|
async def _aggregate_gateway_sync_response_bytes(
|
|
*,
|
|
body_bytes: bytes,
|
|
provider_api_format: str,
|
|
client_api_format: str,
|
|
provider_name: str,
|
|
model: str,
|
|
request_id: str,
|
|
) -> dict[str, Any]:
|
|
from src.api.handlers.base.parsers import get_parser_for_format
|
|
from src.api.handlers.base.upstream_stream_bridge import (
|
|
aggregate_upstream_stream_to_internal_response,
|
|
)
|
|
from src.api.handlers.base.utils import get_format_converter_registry
|
|
|
|
provider_parser = get_parser_for_format(provider_api_format) if provider_api_format else None
|
|
|
|
async def _byte_iter() -> Any:
|
|
yield body_bytes
|
|
|
|
internal_resp = await aggregate_upstream_stream_to_internal_response(
|
|
_byte_iter(),
|
|
provider_api_format=provider_api_format,
|
|
provider_name=provider_name,
|
|
model=model,
|
|
request_id=request_id,
|
|
envelope=None,
|
|
provider_parser=provider_parser,
|
|
)
|
|
|
|
registry = get_format_converter_registry()
|
|
tgt_norm = registry.get_normalizer(client_api_format) if client_api_format else None
|
|
if tgt_norm is None:
|
|
raise RuntimeError(f"未注册 Normalizer: {client_api_format}")
|
|
|
|
response_json = tgt_norm.response_from_internal(
|
|
internal_resp,
|
|
requested_model=model,
|
|
)
|
|
return response_json if isinstance(response_json, dict) else {}
|
|
|
|
|
|
async def _maybe_build_gateway_core_sync_fast_success_response(
|
|
payload: GatewaySyncReportRequest,
|
|
) -> Response | None:
|
|
from src.api.handlers.base.parsers import get_parser_for_format
|
|
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.core.exceptions import EmbeddedErrorException
|
|
|
|
if payload.status_code >= 400:
|
|
return None
|
|
|
|
context = dict(payload.report_context or {})
|
|
if _gateway_sync_report_context_flag(context, "has_envelope") is not False:
|
|
return None
|
|
|
|
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 provider_api_format or not client_api_format:
|
|
return None
|
|
|
|
provider_parser = get_parser_for_format(provider_api_format or client_api_format or "")
|
|
if isinstance(payload.body_json, dict):
|
|
try:
|
|
if provider_parser.is_error_response(dict(payload.body_json)):
|
|
return None
|
|
except Exception:
|
|
if payload.body_json.get("error") is not None:
|
|
return None
|
|
|
|
try:
|
|
needs_conversion = _gateway_sync_report_context_flag(context, "needs_conversion")
|
|
if needs_conversion is None:
|
|
needs_conversion = provider_api_format != client_api_format
|
|
|
|
if payload.body_base64:
|
|
response_json = await _aggregate_gateway_sync_response_bytes(
|
|
body_bytes=_extract_gateway_report_body_bytes(payload),
|
|
provider_api_format=provider_api_format,
|
|
client_api_format=client_api_format,
|
|
provider_name=str(context.get("provider_name") or "unknown"),
|
|
model=str(context.get("model") or "unknown"),
|
|
request_id=str(
|
|
context.get("request_id") or payload.trace_id or uuid.uuid4().hex[:8]
|
|
),
|
|
)
|
|
elif isinstance(payload.body_json, dict):
|
|
response_json = dict(payload.body_json)
|
|
if needs_conversion and provider_api_format and client_api_format:
|
|
registry = get_format_converter_registry()
|
|
response_json = registry.convert_response(
|
|
response_json,
|
|
provider_api_format,
|
|
client_api_format,
|
|
requested_model=str(context.get("model") or "unknown"),
|
|
)
|
|
else:
|
|
return None
|
|
except EmbeddedErrorException:
|
|
return None
|
|
except Exception:
|
|
return None
|
|
|
|
if not isinstance(response_json, dict):
|
|
return None
|
|
|
|
original_headers = dict(context.get("original_headers") or {})
|
|
client_response_headers = filter_proxy_response_headers(dict(payload.headers or {}))
|
|
client_response_headers["content-type"] = "application/json"
|
|
return build_json_response_for_client(
|
|
status_code=payload.status_code,
|
|
content=response_json,
|
|
headers=client_response_headers,
|
|
client_accept_encoding=resolve_client_accept_encoding(original_headers, None),
|
|
)
|
|
|
|
|
|
def _coerce_gateway_video_local_timestamp(value: Any) -> datetime | None:
|
|
if value is None:
|
|
return None
|
|
try:
|
|
timestamp = float(value)
|
|
except (TypeError, ValueError):
|
|
return None
|
|
try:
|
|
return datetime.fromtimestamp(timestamp, tz=timezone.utc)
|
|
except (OverflowError, OSError, ValueError):
|
|
return None
|
|
|
|
|
|
def _resolve_gateway_video_response_time_ms(payload: GatewaySyncReportRequest) -> int:
|
|
telemetry = payload.telemetry if isinstance(payload.telemetry, dict) else {}
|
|
raw_elapsed_ms = telemetry.get("elapsed_ms")
|
|
try:
|
|
return max(int(raw_elapsed_ms or 0), 0)
|
|
except (TypeError, ValueError):
|
|
return 0
|
|
|
|
|
|
def _gateway_report_context_uses_rust_video_task_owner(context: dict[str, Any]) -> bool:
|
|
return bool(context.get("rust_video_task_persisted"))
|
|
|
|
|
|
def _find_gateway_existing_video_task(
|
|
*,
|
|
db: Session,
|
|
request_id: str | None = None,
|
|
local_task_id: str | None = None,
|
|
local_short_id: str | None = None,
|
|
) -> VideoTask | None:
|
|
if local_task_id:
|
|
task = db.query(VideoTask).filter(VideoTask.id == local_task_id).first()
|
|
if task is not None:
|
|
return task
|
|
if local_short_id:
|
|
task = db.query(VideoTask).filter(VideoTask.short_id == local_short_id).first()
|
|
if task is not None:
|
|
return task
|
|
if request_id:
|
|
return db.query(VideoTask).filter(VideoTask.request_id == request_id).first()
|
|
return None
|
|
|
|
|
|
async def _finalize_gateway_openai_video_create_sync(
|
|
payload: GatewaySyncReportRequest,
|
|
*,
|
|
db: Session,
|
|
background_tasks: BackgroundTasks | None = None,
|
|
) -> Response:
|
|
from sqlalchemy.exc import IntegrityError
|
|
|
|
from src.api.handlers.openai.video_handler import OpenAIVideoHandler
|
|
from src.models.database import Provider, ProviderAPIKey, ProviderEndpoint
|
|
from src.services.billing.rule_service import BillingRuleService
|
|
from src.services.scheduling.schemas import ProviderCandidate
|
|
from src.services.usage.service import UsageService
|
|
|
|
if payload.status_code >= 400:
|
|
return JSONResponse(
|
|
status_code=payload.status_code,
|
|
content=payload.body_json if isinstance(payload.body_json, dict) else {},
|
|
headers=dict(payload.headers or {}),
|
|
)
|
|
if not isinstance(payload.body_json, dict):
|
|
raise HTTPException(status_code=502, detail="Invalid upstream response from video provider")
|
|
|
|
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()
|
|
local_task_id = str(context.get("local_task_id") or "").strip()
|
|
local_created_at = _coerce_gateway_video_local_timestamp(context.get("local_created_at"))
|
|
rust_owned_task = _gateway_report_context_uses_rust_video_task_owner(context)
|
|
if not all([user_id, api_key_id]) or (
|
|
not rust_owned_task and not all([provider_id, endpoint_id, key_id])
|
|
):
|
|
raise HTTPException(status_code=400, detail="Missing gateway video finalize context")
|
|
|
|
external_task_id = str(payload.body_json.get("id") or "").strip()
|
|
if not external_task_id:
|
|
raise HTTPException(status_code=502, detail="Upstream video response missing task id")
|
|
|
|
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:
|
|
raise HTTPException(status_code=400, detail="Invalid gateway video finalize context")
|
|
|
|
handler = OpenAIVideoHandler(
|
|
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(),
|
|
)
|
|
|
|
original_request_body = dict(context.get("original_request_body") or {})
|
|
original_headers = dict(context.get("original_headers") or {})
|
|
response_time_ms = _resolve_gateway_video_response_time_ms(payload)
|
|
|
|
if rust_owned_task:
|
|
task = _find_gateway_existing_video_task(
|
|
db=db,
|
|
request_id=request_id,
|
|
local_task_id=local_task_id or None,
|
|
)
|
|
if task is not None:
|
|
logger.info(
|
|
"gateway reusing rust-owned openai video task: request_id={}, task_id={}",
|
|
request_id,
|
|
task.id,
|
|
)
|
|
response_body = handler._normalizer.video_task_from_internal(handler._task_to_internal(task))
|
|
if background_tasks is not None:
|
|
background_tasks.add_task(
|
|
_run_gateway_video_finalize_submitted_background,
|
|
db=db,
|
|
request_id=request_id,
|
|
provider_name=str(context.get("provider_name") or "openai"),
|
|
provider_id=provider_id or None,
|
|
provider_endpoint_id=endpoint_id or None,
|
|
provider_api_key_id=key_id or None,
|
|
response_time_ms=response_time_ms,
|
|
status_code=payload.status_code,
|
|
endpoint_api_format=str(context.get("provider_api_format") or handler.FORMAT_ID)
|
|
or None,
|
|
provider_request_headers=dict(context.get("provider_request_headers") or {}),
|
|
response_headers=dict(payload.headers or {}),
|
|
response_body=response_body,
|
|
)
|
|
else:
|
|
await _run_gateway_video_finalize_submitted_background(
|
|
db=db,
|
|
request_id=request_id,
|
|
provider_name=str(context.get("provider_name") or "openai"),
|
|
provider_id=provider_id or None,
|
|
provider_endpoint_id=endpoint_id or None,
|
|
provider_api_key_id=key_id or None,
|
|
response_time_ms=response_time_ms,
|
|
status_code=payload.status_code,
|
|
endpoint_api_format=str(context.get("provider_api_format") or handler.FORMAT_ID)
|
|
or None,
|
|
provider_request_headers=dict(context.get("provider_request_headers") or {}),
|
|
response_headers=dict(payload.headers or {}),
|
|
response_body=response_body,
|
|
)
|
|
return JSONResponse(response_body)
|
|
|
|
logger.warning(
|
|
"gateway rust-owned openai video task missing, falling back to legacy create: request_id={}, local_task_id={}",
|
|
request_id,
|
|
local_task_id,
|
|
)
|
|
|
|
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 provider or not endpoint or not key:
|
|
raise HTTPException(status_code=400, detail="Invalid gateway video finalize context")
|
|
|
|
internal_request = handler._normalizer.video_request_to_internal(original_request_body)
|
|
candidate = ProviderCandidate(provider=provider, endpoint=endpoint, key=key)
|
|
|
|
rule_lookup = BillingRuleService.find_rule(
|
|
db,
|
|
provider_id=provider.id,
|
|
model_name=internal_request.model,
|
|
task_type="video",
|
|
)
|
|
billing_rule_snapshot = handler._build_billing_rule_snapshot(rule_lookup)
|
|
|
|
task = handler._create_task_record(
|
|
external_task_id=external_task_id,
|
|
candidate=candidate,
|
|
original_request_body=original_request_body,
|
|
internal_request=internal_request,
|
|
original_headers=original_headers,
|
|
billing_rule_snapshot=billing_rule_snapshot,
|
|
)
|
|
if local_task_id:
|
|
task.id = local_task_id
|
|
if local_created_at is not None:
|
|
task.created_at = local_created_at
|
|
task.submitted_at = local_created_at
|
|
task.updated_at = local_created_at
|
|
|
|
try:
|
|
db.add(task)
|
|
UsageService.begin_pending_usage(
|
|
db,
|
|
request_id=request_id,
|
|
user=user,
|
|
api_key=api_key,
|
|
model=internal_request.model,
|
|
is_stream=False,
|
|
request_type="video",
|
|
api_format=handler.FORMAT_ID,
|
|
request_headers=original_headers,
|
|
request_body=original_request_body,
|
|
)
|
|
|
|
response_body = handler._normalizer.video_task_from_internal(handler._task_to_internal(task))
|
|
|
|
db.commit()
|
|
except IntegrityError as exc:
|
|
db.rollback()
|
|
raise HTTPException(status_code=409, detail="Task already exists") from exc
|
|
except Exception:
|
|
db.rollback()
|
|
raise
|
|
|
|
if background_tasks is not None:
|
|
background_tasks.add_task(
|
|
_run_gateway_video_finalize_submitted_background,
|
|
db=db,
|
|
request_id=request_id,
|
|
provider_name=str(context.get("provider_name") or "openai"),
|
|
provider_id=provider_id or None,
|
|
provider_endpoint_id=endpoint_id or None,
|
|
provider_api_key_id=key_id or None,
|
|
response_time_ms=response_time_ms,
|
|
status_code=payload.status_code,
|
|
endpoint_api_format=str(context.get("provider_api_format") or handler.FORMAT_ID)
|
|
or None,
|
|
provider_request_headers=dict(context.get("provider_request_headers") or {}),
|
|
response_headers=dict(payload.headers or {}),
|
|
response_body=response_body,
|
|
)
|
|
else:
|
|
await _run_gateway_video_finalize_submitted_background(
|
|
db=db,
|
|
request_id=request_id,
|
|
provider_name=str(context.get("provider_name") or "openai"),
|
|
provider_id=provider_id or None,
|
|
provider_endpoint_id=endpoint_id or None,
|
|
provider_api_key_id=key_id or None,
|
|
response_time_ms=response_time_ms,
|
|
status_code=payload.status_code,
|
|
endpoint_api_format=str(context.get("provider_api_format") or handler.FORMAT_ID)
|
|
or None,
|
|
provider_request_headers=dict(context.get("provider_request_headers") or {}),
|
|
response_headers=dict(payload.headers or {}),
|
|
response_body=response_body,
|
|
)
|
|
|
|
return JSONResponse(response_body)
|
|
|
|
|
|
async def _finalize_gateway_openai_video_remix_sync(
|
|
payload: GatewaySyncReportRequest,
|
|
*,
|
|
db: Session,
|
|
) -> Response:
|
|
from sqlalchemy.exc import IntegrityError
|
|
|
|
from src.api.handlers.openai.video_handler import OpenAIVideoHandler
|
|
from src.core.api_format.conversion.internal_video import InternalVideoTask, VideoStatus
|
|
from src.models.database import Provider
|
|
from src.services.scheduling.schemas import ProviderCandidate
|
|
|
|
if payload.status_code >= 400:
|
|
return JSONResponse(
|
|
status_code=payload.status_code,
|
|
content=payload.body_json if isinstance(payload.body_json, dict) else {},
|
|
headers=dict(payload.headers or {}),
|
|
)
|
|
if not isinstance(payload.body_json, dict):
|
|
raise HTTPException(status_code=502, detail="Invalid upstream response from video provider")
|
|
|
|
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()
|
|
task_id = str(context.get("task_id") or "").strip()
|
|
request_id = str(context.get("request_id") or payload.trace_id or uuid.uuid4().hex[:8]).strip()
|
|
local_task_id = str(context.get("local_task_id") or "").strip()
|
|
local_created_at = _coerce_gateway_video_local_timestamp(context.get("local_created_at"))
|
|
if not all([user_id, api_key_id, task_id]):
|
|
raise HTTPException(status_code=400, detail="Missing gateway video remix finalize context")
|
|
|
|
external_task_id = str(payload.body_json.get("id") or "").strip()
|
|
if not external_task_id:
|
|
raise HTTPException(status_code=502, detail="Upstream video response missing task id")
|
|
|
|
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:
|
|
raise HTTPException(status_code=400, detail="Invalid gateway video remix finalize context")
|
|
|
|
handler = OpenAIVideoHandler(
|
|
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(),
|
|
)
|
|
|
|
if _gateway_report_context_uses_rust_video_task_owner(context):
|
|
task = _find_gateway_existing_video_task(
|
|
db=db,
|
|
request_id=request_id,
|
|
local_task_id=local_task_id or None,
|
|
)
|
|
if task is not None:
|
|
logger.info(
|
|
"gateway reusing rust-owned openai remix task: request_id={}, task_id={}",
|
|
request_id,
|
|
task.id,
|
|
)
|
|
response_body = handler._normalizer.video_task_from_internal(handler._task_to_internal(task))
|
|
return JSONResponse(response_body)
|
|
|
|
logger.warning(
|
|
"gateway rust-owned openai remix task missing, falling back to legacy create: request_id={}, local_task_id={}",
|
|
request_id,
|
|
local_task_id,
|
|
)
|
|
|
|
original_task = handler._get_task(task_id)
|
|
if original_task.status != VideoStatus.COMPLETED.value:
|
|
raise HTTPException(
|
|
status_code=409,
|
|
detail=f"Can only remix completed videos (current status: {original_task.status})",
|
|
)
|
|
if not original_task.external_task_id:
|
|
raise HTTPException(status_code=500, detail="Original task missing external_task_id")
|
|
|
|
endpoint, key = handler._get_endpoint_and_key(original_task)
|
|
provider = db.query(Provider).filter(Provider.id == original_task.provider_id).first()
|
|
if provider is None:
|
|
raise HTTPException(status_code=500, detail="Provider not found")
|
|
|
|
original_request_body = dict(context.get("original_request_body") or {})
|
|
original_headers = dict(context.get("original_headers") or {})
|
|
try:
|
|
internal_request = handler._normalizer.video_request_to_internal(
|
|
{
|
|
"prompt": original_request_body.get("prompt", ""),
|
|
"model": original_task.model,
|
|
"size": original_task.size,
|
|
"seconds": original_task.duration_seconds,
|
|
}
|
|
)
|
|
except ValueError as exc:
|
|
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
|
|
|
billing_rule_snapshot = None
|
|
if isinstance(original_task.request_metadata, dict):
|
|
billing_rule_snapshot = original_task.request_metadata.get("billing_rule_snapshot")
|
|
|
|
candidate = ProviderCandidate(provider=provider, endpoint=endpoint, key=key)
|
|
task = handler._create_task_record(
|
|
external_task_id=external_task_id,
|
|
candidate=candidate,
|
|
original_request_body={
|
|
**original_request_body,
|
|
"remix_video_id": task_id,
|
|
},
|
|
internal_request=internal_request,
|
|
original_headers=original_headers,
|
|
billing_rule_snapshot=billing_rule_snapshot,
|
|
)
|
|
if local_task_id:
|
|
task.id = local_task_id
|
|
if local_created_at is not None:
|
|
task.created_at = local_created_at
|
|
task.submitted_at = local_created_at
|
|
task.updated_at = local_created_at
|
|
|
|
try:
|
|
db.add(task)
|
|
db.flush()
|
|
db.commit()
|
|
db.refresh(task)
|
|
except IntegrityError as exc:
|
|
db.rollback()
|
|
raise HTTPException(status_code=409, detail="Task already exists") from exc
|
|
except Exception:
|
|
db.rollback()
|
|
raise
|
|
|
|
internal_task = InternalVideoTask(
|
|
id=task.id,
|
|
external_id=external_task_id,
|
|
status=VideoStatus.SUBMITTED,
|
|
created_at=task.created_at,
|
|
original_request=internal_request,
|
|
)
|
|
response_body = handler._normalizer.video_task_from_internal(internal_task)
|
|
return JSONResponse(response_body)
|
|
|
|
|
|
async def _finalize_gateway_gemini_video_create_sync(
|
|
payload: GatewaySyncReportRequest,
|
|
*,
|
|
db: Session,
|
|
background_tasks: BackgroundTasks | None = None,
|
|
) -> Response:
|
|
from sqlalchemy.exc import IntegrityError
|
|
|
|
from src.api.handlers.base.video_handler_base import normalize_gemini_operation_id
|
|
from src.api.handlers.gemini.video_handler import GeminiVeoHandler
|
|
from src.core.api_format.conversion.internal_video import InternalVideoTask, VideoStatus
|
|
from src.models.database import Provider, ProviderAPIKey, ProviderEndpoint
|
|
from src.services.billing.rule_service import BillingRuleService
|
|
from src.services.scheduling.schemas import ProviderCandidate
|
|
from src.services.usage.service import UsageService
|
|
|
|
if payload.status_code >= 400:
|
|
return JSONResponse(
|
|
status_code=payload.status_code,
|
|
content=payload.body_json if isinstance(payload.body_json, dict) else {},
|
|
headers=dict(payload.headers or {}),
|
|
)
|
|
if not isinstance(payload.body_json, dict):
|
|
raise HTTPException(status_code=502, detail="Invalid upstream response from video provider")
|
|
|
|
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 "").strip()
|
|
local_short_id = str(context.get("local_short_id") or "").strip()
|
|
local_created_at = _coerce_gateway_video_local_timestamp(context.get("local_created_at"))
|
|
rust_owned_task = _gateway_report_context_uses_rust_video_task_owner(context)
|
|
if not all([user_id, api_key_id, model]) or (
|
|
not rust_owned_task and not all([provider_id, endpoint_id, key_id])
|
|
):
|
|
raise HTTPException(status_code=400, detail="Missing gateway video finalize context")
|
|
|
|
operation_name = str(payload.body_json.get("name") or payload.body_json.get("id") or "").strip()
|
|
if not operation_name:
|
|
raise HTTPException(
|
|
status_code=502, detail="Upstream video response missing operation name"
|
|
)
|
|
external_task_id = normalize_gemini_operation_id(operation_name)
|
|
|
|
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:
|
|
raise HTTPException(status_code=400, detail="Invalid gateway video finalize context")
|
|
|
|
handler = GeminiVeoHandler(
|
|
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(),
|
|
)
|
|
|
|
original_request_body = dict(context.get("original_request_body") or {})
|
|
original_headers = dict(context.get("original_headers") or {})
|
|
response_time_ms = _resolve_gateway_video_response_time_ms(payload)
|
|
|
|
if rust_owned_task:
|
|
task = _find_gateway_existing_video_task(
|
|
db=db,
|
|
request_id=request_id,
|
|
local_short_id=local_short_id or None,
|
|
)
|
|
if task is not None:
|
|
logger.info(
|
|
"gateway reusing rust-owned gemini video task: request_id={}, short_id={}",
|
|
request_id,
|
|
task.short_id,
|
|
)
|
|
response_body = handler._normalizer.video_task_from_internal(handler._task_to_internal(task))
|
|
if background_tasks is not None:
|
|
background_tasks.add_task(
|
|
_run_gateway_video_finalize_submitted_background,
|
|
db=db,
|
|
request_id=request_id,
|
|
provider_name=str(context.get("provider_name") or "gemini"),
|
|
provider_id=provider_id or None,
|
|
provider_endpoint_id=endpoint_id or None,
|
|
provider_api_key_id=key_id or None,
|
|
response_time_ms=response_time_ms,
|
|
status_code=payload.status_code,
|
|
endpoint_api_format=str(context.get("provider_api_format") or handler.FORMAT_ID)
|
|
or None,
|
|
provider_request_headers=dict(context.get("provider_request_headers") or {}),
|
|
response_headers=dict(payload.headers or {}),
|
|
response_body=response_body,
|
|
)
|
|
else:
|
|
await _run_gateway_video_finalize_submitted_background(
|
|
db=db,
|
|
request_id=request_id,
|
|
provider_name=str(context.get("provider_name") or "gemini"),
|
|
provider_id=provider_id or None,
|
|
provider_endpoint_id=endpoint_id or None,
|
|
provider_api_key_id=key_id or None,
|
|
response_time_ms=response_time_ms,
|
|
status_code=payload.status_code,
|
|
endpoint_api_format=str(context.get("provider_api_format") or handler.FORMAT_ID)
|
|
or None,
|
|
provider_request_headers=dict(context.get("provider_request_headers") or {}),
|
|
response_headers=dict(payload.headers or {}),
|
|
response_body=response_body,
|
|
)
|
|
return JSONResponse(response_body)
|
|
|
|
logger.warning(
|
|
"gateway rust-owned gemini video task missing, falling back to legacy create: request_id={}, local_short_id={}",
|
|
request_id,
|
|
local_short_id,
|
|
)
|
|
|
|
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 provider or not endpoint or not key:
|
|
raise HTTPException(status_code=400, detail="Invalid gateway video finalize context")
|
|
|
|
request_with_model = {**original_request_body, "model": model}
|
|
try:
|
|
internal_request = handler._normalizer.video_request_to_internal(request_with_model)
|
|
except ValueError as exc:
|
|
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
|
|
|
candidate = ProviderCandidate(provider=provider, endpoint=endpoint, key=key)
|
|
rule_lookup = BillingRuleService.find_rule(
|
|
db,
|
|
provider_id=provider.id,
|
|
model_name=internal_request.model,
|
|
task_type="video",
|
|
)
|
|
billing_rule_snapshot = handler._build_billing_rule_snapshot(rule_lookup)
|
|
|
|
task = handler._create_task_record(
|
|
external_task_id=external_task_id,
|
|
candidate=candidate,
|
|
original_request_body=original_request_body,
|
|
converted_request_body=original_request_body,
|
|
internal_request=internal_request,
|
|
original_headers=original_headers,
|
|
billing_rule_snapshot=billing_rule_snapshot,
|
|
format_converted=False,
|
|
)
|
|
if local_short_id:
|
|
task.short_id = local_short_id
|
|
if local_created_at is not None:
|
|
task.created_at = local_created_at
|
|
task.submitted_at = local_created_at
|
|
task.updated_at = local_created_at
|
|
|
|
try:
|
|
db.add(task)
|
|
UsageService.begin_pending_usage(
|
|
db,
|
|
request_id=request_id,
|
|
user=user,
|
|
api_key=api_key,
|
|
model=internal_request.model,
|
|
is_stream=False,
|
|
request_type="video",
|
|
api_format=handler.FORMAT_ID,
|
|
request_headers=original_headers,
|
|
request_body=original_request_body,
|
|
)
|
|
db.flush()
|
|
db.commit()
|
|
db.refresh(task)
|
|
except IntegrityError as exc:
|
|
db.rollback()
|
|
raise HTTPException(status_code=409, detail="Task already exists") from exc
|
|
except Exception:
|
|
db.rollback()
|
|
raise
|
|
|
|
internal_task = InternalVideoTask(
|
|
id=task.short_id,
|
|
external_id=external_task_id,
|
|
status=VideoStatus.SUBMITTED,
|
|
created_at=task.created_at,
|
|
original_request=internal_request,
|
|
)
|
|
response_body = handler._normalizer.video_task_from_internal(internal_task)
|
|
|
|
if background_tasks is not None:
|
|
background_tasks.add_task(
|
|
_run_gateway_video_finalize_submitted_background,
|
|
db=db,
|
|
request_id=request_id,
|
|
provider_name=str(context.get("provider_name") or "gemini"),
|
|
provider_id=provider_id or None,
|
|
provider_endpoint_id=endpoint_id or None,
|
|
provider_api_key_id=key_id or None,
|
|
response_time_ms=response_time_ms,
|
|
status_code=payload.status_code,
|
|
endpoint_api_format=str(context.get("provider_api_format") or handler.FORMAT_ID)
|
|
or None,
|
|
provider_request_headers=dict(context.get("provider_request_headers") or {}),
|
|
response_headers=dict(payload.headers or {}),
|
|
response_body=response_body,
|
|
)
|
|
else:
|
|
await _run_gateway_video_finalize_submitted_background(
|
|
db=db,
|
|
request_id=request_id,
|
|
provider_name=str(context.get("provider_name") or "gemini"),
|
|
provider_id=provider_id or None,
|
|
provider_endpoint_id=endpoint_id or None,
|
|
provider_api_key_id=key_id or None,
|
|
response_time_ms=response_time_ms,
|
|
status_code=payload.status_code,
|
|
endpoint_api_format=str(context.get("provider_api_format") or handler.FORMAT_ID)
|
|
or None,
|
|
provider_request_headers=dict(context.get("provider_request_headers") or {}),
|
|
response_headers=dict(payload.headers or {}),
|
|
response_body=response_body,
|
|
)
|
|
|
|
return JSONResponse(response_body)
|
|
|
|
|
|
async def _finalize_gateway_openai_video_delete_sync(
|
|
payload: GatewaySyncReportRequest,
|
|
*,
|
|
db: Session,
|
|
) -> Response:
|
|
from src.api.handlers.openai.video_handler import OpenAIVideoHandler
|
|
from src.core.api_format.conversion.internal_video import VideoStatus
|
|
|
|
if payload.status_code >= 400 and payload.status_code != 404:
|
|
return JSONResponse(
|
|
status_code=payload.status_code,
|
|
content=payload.body_json if isinstance(payload.body_json, dict) else {},
|
|
headers=dict(payload.headers or {}),
|
|
)
|
|
|
|
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()
|
|
task_id = str(context.get("task_id") or "").strip()
|
|
if not all([user_id, api_key_id, task_id]):
|
|
raise HTTPException(status_code=400, detail="Missing gateway video delete finalize context")
|
|
|
|
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:
|
|
raise HTTPException(status_code=400, detail="Invalid gateway video delete finalize context")
|
|
|
|
handler = OpenAIVideoHandler(
|
|
db=db,
|
|
user=user,
|
|
api_key=api_key,
|
|
request_id=str(payload.trace_id or uuid.uuid4().hex[:8]),
|
|
client_ip="127.0.0.1",
|
|
user_agent="aether-gateway",
|
|
start_time=time.time(),
|
|
)
|
|
task = handler._get_task(task_id)
|
|
if task.status not in (VideoStatus.COMPLETED.value, VideoStatus.FAILED.value):
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail=f"Can only delete completed or failed videos (current status: {task.status})",
|
|
)
|
|
|
|
db.delete(task)
|
|
db.commit()
|
|
return JSONResponse({"id": task_id, "object": "video", "deleted": True})
|
|
|
|
|
|
async def _finalize_gateway_openai_video_cancel_sync(
|
|
payload: GatewaySyncReportRequest,
|
|
*,
|
|
db: Session,
|
|
) -> Response:
|
|
from datetime import datetime, timezone
|
|
|
|
from src.api.handlers.openai.video_handler import OpenAIVideoHandler
|
|
from src.core.api_format.conversion.internal_video import VideoStatus
|
|
from src.services.usage.service import UsageService
|
|
|
|
if payload.status_code >= 400:
|
|
return JSONResponse(
|
|
status_code=payload.status_code,
|
|
content=payload.body_json if isinstance(payload.body_json, dict) else {},
|
|
headers=dict(payload.headers or {}),
|
|
)
|
|
|
|
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()
|
|
task_id = str(context.get("task_id") or "").strip()
|
|
if not all([user_id, api_key_id, task_id]):
|
|
raise HTTPException(status_code=400, detail="Missing gateway video cancel finalize context")
|
|
|
|
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:
|
|
raise HTTPException(status_code=400, detail="Invalid gateway video cancel finalize context")
|
|
|
|
handler = OpenAIVideoHandler(
|
|
db=db,
|
|
user=user,
|
|
api_key=api_key,
|
|
request_id=str(payload.trace_id or uuid.uuid4().hex[:8]),
|
|
client_ip="127.0.0.1",
|
|
user_agent="aether-gateway",
|
|
start_time=time.time(),
|
|
)
|
|
task = handler._get_task(task_id)
|
|
if task.status in (
|
|
VideoStatus.COMPLETED.value,
|
|
VideoStatus.FAILED.value,
|
|
VideoStatus.CANCELLED.value,
|
|
VideoStatus.EXPIRED.value,
|
|
):
|
|
raise HTTPException(
|
|
status_code=409,
|
|
detail=f"Task cannot be cancelled in status: {task.status}",
|
|
)
|
|
|
|
now = datetime.now(timezone.utc)
|
|
task.status = VideoStatus.CANCELLED.value
|
|
task.completed_at = getattr(task, "completed_at", None) or now
|
|
task.updated_at = now
|
|
|
|
try:
|
|
UsageService.finalize_void(
|
|
db,
|
|
request_id=task.request_id,
|
|
reason="cancelled_by_user",
|
|
finalized_at=task.completed_at,
|
|
)
|
|
except Exception:
|
|
pass
|
|
|
|
db.commit()
|
|
return JSONResponse({})
|
|
|
|
|
|
async def _finalize_gateway_gemini_video_cancel_sync(
|
|
payload: GatewaySyncReportRequest,
|
|
*,
|
|
db: Session,
|
|
) -> Response:
|
|
from datetime import datetime, timezone
|
|
|
|
from src.api.handlers.gemini.video_handler import GeminiVeoHandler
|
|
from src.core.api_format.conversion.internal_video import VideoStatus
|
|
from src.services.usage.service import UsageService
|
|
|
|
if payload.status_code >= 400:
|
|
return JSONResponse(
|
|
status_code=payload.status_code,
|
|
content=payload.body_json if isinstance(payload.body_json, dict) else {},
|
|
headers=dict(payload.headers or {}),
|
|
)
|
|
|
|
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()
|
|
task_id = str(context.get("task_id") or "").strip()
|
|
if not all([user_id, api_key_id, task_id]):
|
|
raise HTTPException(status_code=400, detail="Missing gateway video cancel finalize context")
|
|
|
|
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:
|
|
raise HTTPException(status_code=400, detail="Invalid gateway video cancel finalize context")
|
|
|
|
handler = GeminiVeoHandler(
|
|
db=db,
|
|
user=user,
|
|
api_key=api_key,
|
|
request_id=str(payload.trace_id or uuid.uuid4().hex[:8]),
|
|
client_ip="127.0.0.1",
|
|
user_agent="aether-gateway",
|
|
start_time=time.time(),
|
|
)
|
|
task = handler._get_task_by_external_id(task_id)
|
|
if task.status in (
|
|
VideoStatus.COMPLETED.value,
|
|
VideoStatus.FAILED.value,
|
|
VideoStatus.CANCELLED.value,
|
|
VideoStatus.EXPIRED.value,
|
|
):
|
|
raise HTTPException(
|
|
status_code=409,
|
|
detail=f"Task cannot be cancelled in status: {task.status}",
|
|
)
|
|
|
|
now = datetime.now(timezone.utc)
|
|
task.status = VideoStatus.CANCELLED.value
|
|
task.completed_at = getattr(task, "completed_at", None) or now
|
|
task.updated_at = now
|
|
|
|
try:
|
|
UsageService.finalize_void(
|
|
db,
|
|
request_id=task.request_id,
|
|
reason="cancelled_by_user",
|
|
finalized_at=task.completed_at,
|
|
)
|
|
except Exception:
|
|
pass
|
|
|
|
db.commit()
|
|
return JSONResponse({})
|
|
|
|
|
|
from .gateway_routes import router
|