Files
Aether/_deprecated_py_src/services/task/service.py

692 lines
27 KiB
Python
Raw Normal View History

from __future__ import annotations
from collections.abc import Awaitable, Callable
from types import SimpleNamespace
from typing import Any
from sqlalchemy.orm import Session
from src.models.database import ApiKey
from src.services.candidate.recorder import CandidateRecorder
from src.services.task.core.context import TaskMode
from src.services.task.core.protocol import AttemptKind, AttemptResult
from src.services.task.core.schema import ExecutionResult, TaskStatusResult
from src.services.task.execute.error_handler import TaskErrorOperationsService
from src.services.task.execute.failure import TaskFailureOperationsService
from src.services.task.execute.pool import TaskPoolOperationsService
from src.services.task.execute.sync_execute import SyncTaskExecutionService
from src.services.task.request_state import RequestBodyState
from src.services.task.submit.submit_service import AsyncTaskSubmitService
from src.services.task.video.facade import TaskVideoFacadeService
from src.services.task.video.operations import VideoTaskOperationsService
async def pool_on_error(
provider: Any,
key: Any,
status_code: int,
cause: Any,
) -> None:
"""Notify the pool manager about an upstream error (health policy)."""
try:
from src.services.provider.pool.config import parse_pool_config
from src.services.provider.pool.health_policy import apply_health_policy
pool_cfg = parse_pool_config(getattr(provider, "config", None))
if pool_cfg is None:
return
error_text = ""
resp_headers: dict[str, str] = {}
if getattr(cause, "response", None) is not None:
try:
error_text = (cause.response.text or "")[:4000]
except Exception:
pass
try:
resp_headers = dict(cause.response.headers)
except Exception:
pass
elif isinstance(getattr(cause, "error_message", None), str):
error_text = str(getattr(cause, "error_message", "") or "")[:4000]
await apply_health_policy(
provider_id=str(provider.id),
key_id=str(key.id),
status_code=status_code,
error_body=error_text,
response_headers=resp_headers,
config=pool_cfg,
)
except Exception:
pass
class TaskService:
"""
Unified task service facade (Phase 3).
Phase 3.1 scope:
- Provide a single entrypoint for SYNC tasks.
- Keep behavior consistent with the pre-Phase-3 implementation.
- Return a structured `ExecutionResult` for downstream compatibility.
"""
def __init__(self, db: Session, redis_client: Any | None = None) -> None:
self.db = db
self.redis = redis_client
self._candidate_recorder = CandidateRecorder(db)
# 兼容历史注入点_execute_facade_ops/_submit_facade_ops
# 不再依赖独立门面类,默认直接绑定 TaskService 内部实现。
self._execute_facade_ops = SimpleNamespace(
execute=self._execute_internal,
_get_candidate_keys=self._candidate_recorder.get_candidate_keys,
)
pool_ops = TaskPoolOperationsService()
error_ops = TaskErrorOperationsService(db, pool_ops=pool_ops)
failure_ops = TaskFailureOperationsService()
self._sync_ops = SyncTaskExecutionService(
db,
redis_client,
recorder=self._candidate_recorder,
pool_ops=pool_ops,
error_ops=error_ops,
failure_ops=failure_ops,
)
self._submit_ops = AsyncTaskSubmitService(
db,
redis_client,
apply_pool_reorder=pool_ops.apply_pool_reorder,
expand_pool_candidates_for_async_submit=pool_ops.expand_pool_candidates_for_async_submit,
)
self._video_ops = VideoTaskOperationsService(db, redis_client)
self._submit_facade_ops = self._submit_ops
self._video_facade_ops = TaskVideoFacadeService(self._video_ops)
async def execute(
self,
*,
task_type: str, # chat/cli/video/image
task_mode: TaskMode,
api_format: str,
model_name: str,
user_api_key: ApiKey,
request_func: Callable[..., Any],
request_id: str | None = None,
is_stream: bool = False,
capability_requirements: dict[str, bool] | None = None,
preferred_key_ids: list[str] | None = None,
request_body_state: RequestBodyState | None = None,
request_headers: dict[str, Any] | None = None,
request_body: dict[str, Any] | None = None,
# ASYNC-only (video submit)
extract_external_task_id: Any | None = None,
supported_auth_types: set[str] | None = None,
allow_format_conversion: bool = False,
max_candidates: int | None = None,
feat: 流式空闲超时、健康监控查询优化、限流桶内存上限与维护清理修复 Close #233 Co-authored-by: AAEE86 <ppk0227@hotmail.com> - cli_monitor_mixin: 引入 STREAM_IDLE_TIMEOUT_SECONDS(可通过环境变量配置), 流传输开始后若超出空闲窗口无新 chunk 则提前取消并返回 504,避免长时间挂起 - stream_context: 新增 managed_recorded_bodies 上下文管理器,确保 chunks 在 telemetry 完成后及时释放;stream_telemetry 使用该接口统一管理 response body 构建 - health endpoint: 将状态聚合改为 GROUP BY 直接统计,事件列表按 api_format 单独查询,避免单次 limit 拉取大量记录导致的遗漏与性能问题;同时过滤不活跃 provider/endpoint,与公开健康接口保持一致 - endpoint health service: 修正时间线数据按 endpoint_id 而非 key_id 聚合 - token_bucket: 引入 max_buckets/bucket_expiry 上限与定时清理,防止内存无限增长; 修复 refill_rate=0 时 get_reset_time 除零异常;新增 _is_unlimited_rate_limit 判断 - maintenance_scheduler: 调整清理顺序(先删整行再按窗口清理),新增 newer_than 边界参数,避免同一行在同一轮中被重复改写 - sync_execute: 新增 create_pending_usage 开关,允许已预创建记录的调用方跳过重复创建 - quota_reader / provider_ops balance: 小幅修复与健壮性提升 - Dockerfile: 添加 MALLOC_ARENA_MAX=2 环境变量以降低 gunicorn worker RSS - 补充相关测试覆盖
2026-03-18 23:38:26 +08:00
create_pending_usage: bool = True,
) -> ExecutionResult:
"""兼容入口:默认绑定到 TaskService 内部执行路由。"""
return await self._execute_facade_ops.execute(
task_type=task_type,
task_mode=task_mode,
api_format=api_format,
model_name=model_name,
user_api_key=user_api_key,
request_func=request_func,
request_id=request_id,
is_stream=is_stream,
capability_requirements=capability_requirements,
preferred_key_ids=preferred_key_ids,
request_body_state=request_body_state,
request_headers=request_headers,
request_body=request_body,
extract_external_task_id=extract_external_task_id,
supported_auth_types=supported_auth_types,
allow_format_conversion=allow_format_conversion,
max_candidates=max_candidates,
feat: 流式空闲超时、健康监控查询优化、限流桶内存上限与维护清理修复 Close #233 Co-authored-by: AAEE86 <ppk0227@hotmail.com> - cli_monitor_mixin: 引入 STREAM_IDLE_TIMEOUT_SECONDS(可通过环境变量配置), 流传输开始后若超出空闲窗口无新 chunk 则提前取消并返回 504,避免长时间挂起 - stream_context: 新增 managed_recorded_bodies 上下文管理器,确保 chunks 在 telemetry 完成后及时释放;stream_telemetry 使用该接口统一管理 response body 构建 - health endpoint: 将状态聚合改为 GROUP BY 直接统计,事件列表按 api_format 单独查询,避免单次 limit 拉取大量记录导致的遗漏与性能问题;同时过滤不活跃 provider/endpoint,与公开健康接口保持一致 - endpoint health service: 修正时间线数据按 endpoint_id 而非 key_id 聚合 - token_bucket: 引入 max_buckets/bucket_expiry 上限与定时清理,防止内存无限增长; 修复 refill_rate=0 时 get_reset_time 除零异常;新增 _is_unlimited_rate_limit 判断 - maintenance_scheduler: 调整清理顺序(先删整行再按窗口清理),新增 newer_than 边界参数,避免同一行在同一轮中被重复改写 - sync_execute: 新增 create_pending_usage 开关,允许已预创建记录的调用方跳过重复创建 - quota_reader / provider_ops balance: 小幅修复与健壮性提升 - Dockerfile: 添加 MALLOC_ARENA_MAX=2 环境变量以降低 gunicorn worker RSS - 补充相关测试覆盖
2026-03-18 23:38:26 +08:00
create_pending_usage=create_pending_usage,
)
async def _execute_internal(
self,
*,
task_type: str, # chat/cli/video/image
task_mode: TaskMode,
api_format: str,
model_name: str,
user_api_key: ApiKey,
request_func: Callable[..., Any],
request_id: str | None = None,
is_stream: bool = False,
capability_requirements: dict[str, bool] | None = None,
preferred_key_ids: list[str] | None = None,
request_body_state: RequestBodyState | None = None,
request_headers: dict[str, Any] | None = None,
request_body: dict[str, Any] | None = None,
extract_external_task_id: Any | None = None,
supported_auth_types: set[str] | None = None,
allow_format_conversion: bool = False,
max_candidates: int | None = None,
feat: 流式空闲超时、健康监控查询优化、限流桶内存上限与维护清理修复 Close #233 Co-authored-by: AAEE86 <ppk0227@hotmail.com> - cli_monitor_mixin: 引入 STREAM_IDLE_TIMEOUT_SECONDS(可通过环境变量配置), 流传输开始后若超出空闲窗口无新 chunk 则提前取消并返回 504,避免长时间挂起 - stream_context: 新增 managed_recorded_bodies 上下文管理器,确保 chunks 在 telemetry 完成后及时释放;stream_telemetry 使用该接口统一管理 response body 构建 - health endpoint: 将状态聚合改为 GROUP BY 直接统计,事件列表按 api_format 单独查询,避免单次 limit 拉取大量记录导致的遗漏与性能问题;同时过滤不活跃 provider/endpoint,与公开健康接口保持一致 - endpoint health service: 修正时间线数据按 endpoint_id 而非 key_id 聚合 - token_bucket: 引入 max_buckets/bucket_expiry 上限与定时清理,防止内存无限增长; 修复 refill_rate=0 时 get_reset_time 除零异常;新增 _is_unlimited_rate_limit 判断 - maintenance_scheduler: 调整清理顺序(先删整行再按窗口清理),新增 newer_than 边界参数,避免同一行在同一轮中被重复改写 - sync_execute: 新增 create_pending_usage 开关,允许已预创建记录的调用方跳过重复创建 - quota_reader / provider_ops balance: 小幅修复与健壮性提升 - Dockerfile: 添加 MALLOC_ARENA_MAX=2 环境变量以降低 gunicorn worker RSS - 补充相关测试覆盖
2026-03-18 23:38:26 +08:00
create_pending_usage: bool = True,
) -> ExecutionResult:
if task_mode == TaskMode.ASYNC:
if extract_external_task_id is None:
raise ValueError("extract_external_task_id is required for task_mode=ASYNC")
outcome = await self.submit_with_failover(
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,
task_type=task_type,
submit_func=request_func,
extract_external_task_id=extract_external_task_id,
supported_auth_types=supported_auth_types,
allow_format_conversion=allow_format_conversion,
capability_requirements=capability_requirements,
max_candidates=max_candidates,
request_body=request_body,
)
candidate_keys = []
if request_id:
try:
candidate_keys = self._execute_facade_ops._get_candidate_keys(request_id)
except Exception:
candidate_keys = []
selected_idx = -1
if candidate_keys:
for ck in candidate_keys:
if str(getattr(ck, "status", "")) == "success":
idx_val = getattr(ck, "candidate_index", -1)
selected_idx = int(idx_val) if idx_val is not None else -1
break
attempt_count = 0
if candidate_keys:
attempt_count = sum(
1
for ck in candidate_keys
if str(getattr(ck, "status", ""))
in {"pending", "success", "failed", "cancelled"}
)
attempt_result = AttemptResult(
kind=AttemptKind.ASYNC_SUBMIT,
http_status=int(outcome.upstream_status_code or 200),
http_headers=dict(outcome.upstream_headers or {}),
provider_task_id=str(outcome.external_task_id),
response_body=outcome.upstream_payload,
)
return ExecutionResult(
success=True,
attempt_result=attempt_result,
candidate=outcome.candidate,
candidate_index=selected_idx,
retry_index=0,
provider_id=str(outcome.candidate.provider.id),
provider_name=str(outcome.candidate.provider.name),
endpoint_id=str(outcome.candidate.endpoint.id),
key_id=str(outcome.candidate.key.id),
candidate_keys=candidate_keys,
attempt_count=attempt_count,
request_candidate_id=None,
)
_ = task_type # reserved for future routing (chat/cli/video/image)
return await self._sync_ops.execute_sync_unified(
api_format=api_format,
model_name=model_name,
user_api_key=user_api_key,
request_func=request_func,
request_id=request_id,
is_stream=is_stream,
capability_requirements=capability_requirements,
preferred_key_ids=preferred_key_ids,
request_body_state=request_body_state,
request_headers=request_headers,
request_body=request_body,
feat: 流式空闲超时、健康监控查询优化、限流桶内存上限与维护清理修复 Close #233 Co-authored-by: AAEE86 <ppk0227@hotmail.com> - cli_monitor_mixin: 引入 STREAM_IDLE_TIMEOUT_SECONDS(可通过环境变量配置), 流传输开始后若超出空闲窗口无新 chunk 则提前取消并返回 504,避免长时间挂起 - stream_context: 新增 managed_recorded_bodies 上下文管理器,确保 chunks 在 telemetry 完成后及时释放;stream_telemetry 使用该接口统一管理 response body 构建 - health endpoint: 将状态聚合改为 GROUP BY 直接统计,事件列表按 api_format 单独查询,避免单次 limit 拉取大量记录导致的遗漏与性能问题;同时过滤不活跃 provider/endpoint,与公开健康接口保持一致 - endpoint health service: 修正时间线数据按 endpoint_id 而非 key_id 聚合 - token_bucket: 引入 max_buckets/bucket_expiry 上限与定时清理,防止内存无限增长; 修复 refill_rate=0 时 get_reset_time 除零异常;新增 _is_unlimited_rate_limit 判断 - maintenance_scheduler: 调整清理顺序(先删整行再按窗口清理),新增 newer_than 边界参数,避免同一行在同一轮中被重复改写 - sync_execute: 新增 create_pending_usage 开关,允许已预创建记录的调用方跳过重复创建 - quota_reader / provider_ops balance: 小幅修复与健壮性提升 - Dockerfile: 添加 MALLOC_ARENA_MAX=2 环境变量以降低 gunicorn worker RSS - 补充相关测试覆盖
2026-03-18 23:38:26 +08:00
create_pending_usage=create_pending_usage,
)
async def execute_sync_candidates(
self,
*,
api_format: str,
model_name: str,
candidates: list[Any],
request_func: Callable[..., Any],
request_id: str | None = None,
current_user: Any | None = None,
user_api_key: ApiKey | None = None,
is_stream: bool = False,
capability_requirements: dict[str, bool] | None = None,
request_body_state: RequestBodyState | None = None,
request_headers: dict[str, Any] | None = None,
request_body: dict[str, Any] | None = None,
affinity_key: str | None = None,
create_pending_usage: bool = False,
enable_cache_affinity: bool = False,
is_cancelled: Callable[[], Awaitable[bool]] | None = None,
) -> ExecutionResult:
"""Execute a pre-built candidate set through the unified SYNC runtime."""
from uuid import uuid4
from src.models.database import User
from src.services.candidate.failover import FailoverEngine
from src.services.candidate.policy import RetryPolicy, SkipPolicy
from src.services.candidate.resolver import CandidateResolver
from src.services.orchestration.error_classifier import ErrorClassifier
from src.services.orchestration.request_dispatcher import RequestDispatcher
from src.services.provider.format import normalize_endpoint_signature
from src.services.rate_limit.adaptive_rpm import get_adaptive_rpm_manager
from src.services.rate_limit.concurrency_manager import get_concurrency_manager
from src.services.request.candidate import RequestCandidateService
from src.services.request.executor import RequestExecutor
from src.services.scheduling.aware_scheduler import (
CacheAwareScheduler,
get_cache_aware_scheduler,
)
from src.services.system.config import SystemConfigService
from src.services.task.execute.exception_classification import (
CandidateErrorAction,
classify_candidate_error_action,
)
from src.services.task.execute.state_transition import (
SyncExecutionState,
resolve_execution_error_transition,
)
from src.services.usage.service import UsageService
if not request_id:
request_id = str(uuid4())
api_format_norm = normalize_endpoint_signature(api_format)
priority_mode = SystemConfigService.get_config(
self.db,
"provider_priority_mode",
CacheAwareScheduler.PRIORITY_MODE_PROVIDER,
)
scheduling_mode = SystemConfigService.get_config(
self.db,
"scheduling_mode",
CacheAwareScheduler.SCHEDULING_MODE_CACHE_AFFINITY,
)
cache_scheduler = await get_cache_aware_scheduler(
self.redis,
priority_mode=priority_mode,
scheduling_mode=scheduling_mode,
)
await cache_scheduler._ensure_initialized()
concurrency_manager = await get_concurrency_manager()
adaptive_manager = get_adaptive_rpm_manager()
request_executor = RequestExecutor(
db=self.db,
concurrency_manager=concurrency_manager,
adaptive_manager=adaptive_manager,
)
candidate_resolver = CandidateResolver(
db=self.db,
cache_scheduler=cache_scheduler,
)
error_classifier = ErrorClassifier(
db=self.db,
cache_scheduler=cache_scheduler,
adaptive_manager=adaptive_manager,
)
request_dispatcher = RequestDispatcher(
db=self.db,
request_executor=request_executor,
cache_scheduler=cache_scheduler if enable_cache_affinity else None,
)
pool_ops = self._sync_ops._pool_ops
error_ops = self._sync_ops._error_ops
failure_ops = self._sync_ops._failure_ops
resolved_user = current_user
if resolved_user is None and user_api_key is not None:
try:
resolved_user = user_api_key.user if hasattr(user_api_key, "user") else None
except Exception:
resolved_user = None
if resolved_user is None and getattr(user_api_key, "user_id", None):
resolved_user = self.db.query(User).filter(User.id == user_api_key.user_id).first()
user_id: str | None = None
if resolved_user is not None and getattr(resolved_user, "id", None):
user_id = str(resolved_user.id)
elif user_api_key is not None and getattr(user_api_key, "user_id", None):
user_id = str(user_api_key.user_id)
username_snapshot = getattr(resolved_user, "username", None) if resolved_user else None
api_key_name_snapshot = getattr(user_api_key, "name", None) if user_api_key else None
resolved_affinity_key = affinity_key
if not resolved_affinity_key:
api_key_id = getattr(user_api_key, "id", None) if user_api_key is not None else None
resolved_affinity_key = str(api_key_id) if api_key_id else f"internal-test:{request_id}"
if create_pending_usage:
try:
UsageService.create_pending_usage(
db=self.db,
request_id=request_id,
user=resolved_user,
api_key=user_api_key,
model=model_name,
is_stream=is_stream,
api_format=api_format_norm,
request_headers=request_headers,
request_body=request_body,
)
except Exception as exc:
from src.core.logger import logger as _logger
_logger.warning("创建 pending 使用记录失败: {}", str(exc))
all_candidates = list(candidates)
# 号池排序涉及大量 Redis 操作,提前释放 DB 连接避免连接池压力
from src.services.scheduling.utils import release_db_connection_before_await
release_db_connection_before_await(self.db)
all_candidates, pool_traces = await pool_ops.apply_pool_reorder(
all_candidates, request_body=request_body
)
candidate_record_map = await candidate_resolver.create_candidate_records_async(
all_candidates=all_candidates,
request_id=request_id,
user_id=user_id,
user_api_key=user_api_key,
required_capabilities=capability_requirements,
)
max_attempts = candidate_resolver.count_total_attempts(all_candidates)
execution_state = SyncExecutionState(
candidate_record_map=candidate_record_map,
request_body_state=request_body_state,
last_candidate=all_candidates[-1] if all_candidates else None,
)
async def _attempt(candidate: Any) -> AttemptResult:
execution_state.touch_candidate(candidate)
candidate_index = int(getattr(candidate, "_utf_candidate_index", -1))
retry_index = int(getattr(candidate, "_utf_retry_index", 0))
candidate_record_id = str(getattr(candidate, "_utf_candidate_record_id", "") or "")
attempt_counter = int(getattr(candidate, "_utf_attempt_count", 0))
max_attempts_local = int(getattr(candidate, "_utf_max_attempts", max_attempts))
if not candidate_record_id:
from src.services.scheduling.schemas import PoolCandidate
pool_extra = (
getattr(candidate.key, "_pool_extra_data", None)
if isinstance(getattr(candidate.key, "_pool_extra_data", None), dict)
else {}
)
extra_data: dict[str, Any] = {
"needs_conversion": bool(getattr(candidate, "needs_conversion", False)),
"provider_api_format": getattr(candidate, "provider_api_format", None) or None,
"mapping_matched_model": getattr(candidate, "mapping_matched_model", None)
or None,
**pool_extra,
}
if isinstance(candidate, PoolCandidate):
extra_data["pool_group_id"] = str(candidate.provider.id)
extra_data["pool_key_index"] = int(
getattr(candidate, "_pool_key_index", 0) or 0
)
candidate_record = RequestCandidateService.create_candidate(
db=self.db,
request_id=request_id,
candidate_index=candidate_index,
retry_index=retry_index,
user_id=user_id,
api_key_id=(getattr(user_api_key, "id", None) if user_api_key else None),
username=username_snapshot,
api_key_name=api_key_name_snapshot,
provider_id=str(candidate.provider.id),
endpoint_id=str(candidate.endpoint.id),
key_id=str(candidate.key.id),
status="available",
is_cached=bool(getattr(candidate, "is_cached", False)),
extra_data=extra_data,
)
self.db.flush()
candidate_record_id = str(candidate_record.id)
execution_state.candidate_record_map[(candidate_index, retry_index)] = (
candidate_record_id
)
setattr(candidate, "_utf_candidate_record_id", candidate_record_id)
(
response,
_provider_name,
attempt_id,
_provider_id,
_endpoint_id,
_key_id,
_first_byte_time_ms,
) = await request_dispatcher.dispatch(
candidate=candidate,
candidate_index=candidate_index,
retry_index=retry_index,
candidate_record_id=candidate_record_id,
user_api_key=user_api_key,
user_id=user_id,
request_func=request_func,
request_id=request_id,
api_format=api_format_norm,
model_name=model_name,
affinity_key=resolved_affinity_key,
global_model_id=model_name,
attempt_counter=attempt_counter,
max_attempts=max_attempts_local,
is_stream=is_stream,
)
_ = (attempt_id, _provider_name, _provider_id, _endpoint_id, _key_id)
await pool_ops.pool_on_success(candidate, request_body)
if is_stream:
return AttemptResult(
kind=AttemptKind.STREAM,
http_status=200,
http_headers={},
stream_iterator=response,
)
return AttemptResult(
kind=AttemptKind.SYNC_RESPONSE,
http_status=200,
http_headers={},
response_body=response,
)
async def _handle_exec_err(
*,
exec_err: Any,
candidate: Any,
candidate_index: int,
retry_index: int,
max_retries_for_candidate: int,
record_id: str | None,
attempt_count: int,
max_attempts: int | None,
) -> tuple[Any, int | None]:
execution_state.track_execution_error(exec_err=exec_err, candidate=candidate)
candidate_record_id = execution_state.resolve_candidate_record_id(
candidate_index=candidate_index,
record_id=record_id,
)
raw_action = await error_ops.handle_candidate_error(
exec_err=exec_err,
candidate=candidate,
candidate_record_id=candidate_record_id,
retry_index=retry_index,
max_retries_for_candidate=max_retries_for_candidate,
affinity_key=resolved_affinity_key,
api_format=api_format_norm,
global_model_id=model_name,
request_id=request_id,
attempt=attempt_count,
max_attempts=int(max_attempts or 0),
request_body_state=request_body_state,
error_classifier=error_classifier,
)
action = classify_candidate_error_action(raw_action)
if action == CandidateErrorAction.RAISE_ERROR:
execution_state.raise_classified_error(
fallback_error=exec_err,
failure_ops=failure_ops,
model_name=model_name,
api_format=api_format_norm,
)
return resolve_execution_error_transition(
action=action,
state=execution_state,
max_retries_for_candidate=max_retries_for_candidate,
retry_index=retry_index,
).as_failover_tuple()
engine = FailoverEngine(
self.db,
error_classifier=error_classifier,
recorder=self._candidate_recorder,
)
result = await engine.execute(
candidates=all_candidates,
attempt_func=_attempt,
retry_policy=RetryPolicy.for_sync_task(),
skip_policy=SkipPolicy(),
request_id=request_id,
user_id=user_id,
api_key_id=(
str(user_api_key.id) if user_api_key and getattr(user_api_key, "id", None) else None
),
username=username_snapshot,
api_key_name=api_key_name_snapshot,
candidate_record_map=candidate_record_map,
max_attempts=max_attempts,
execution_error_handler=_handle_exec_err,
is_cancelled=is_cancelled,
)
if result.success:
if pool_traces and result.key_id:
try:
attempted_key_ids: set[str] = set()
for ck in result.candidate_keys or []:
status = str(getattr(ck, "status", "") or "").strip().lower()
if status in {"", "available", "pending", "skipped", "unused"}:
continue
kid = getattr(ck, "key_id", None)
if isinstance(kid, str) and kid:
attempted_key_ids.add(kid)
if not attempted_key_ids:
attempted_key_ids.add(str(result.key_id))
for pt in pool_traces:
summary = pt.build_summary(
result.key_id,
attempted_key_ids=attempted_key_ids,
)
if summary:
result.pool_summary = summary
break
except Exception:
pass
return result
failure_ops.raise_all_failed_exception(
request_id,
max_attempts,
execution_state.last_candidate,
model_name,
api_format_norm,
execution_state.last_error,
)
async def submit_with_failover(
self,
*,
api_format: str,
model_name: str,
affinity_key: str,
user_api_key: ApiKey,
request_id: str | None,
task_type: str,
submit_func: Any,
extract_external_task_id: Any,
supported_auth_types: set[str] | None = None,
allow_format_conversion: bool = False,
capability_requirements: dict[str, bool] | None = None,
max_candidates: int | None = None,
request_body: dict[str, Any] | None = None,
) -> Any:
"""
Unified ASYNC submit entrypoint (Phase 3.2).
兼容入口默认直接绑定 AsyncTaskSubmitService
"""
return await self._submit_facade_ops.submit_with_failover(
api_format=api_format,
model_name=model_name,
affinity_key=affinity_key,
user_api_key=user_api_key,
request_id=request_id,
task_type=task_type,
submit_func=submit_func,
extract_external_task_id=extract_external_task_id,
supported_auth_types=supported_auth_types,
allow_format_conversion=allow_format_conversion,
capability_requirements=capability_requirements,
max_candidates=max_candidates,
request_body=request_body,
)
# ====================
# Phase 3.1: Async task helpers (poll/finalize)
# ====================
async def poll(self, task_id: str, *, user_id: str) -> TaskStatusResult:
return await self._video_facade_ops.poll(task_id, user_id=user_id)
async def poll_now(self, task_id: str, *, user_id: str) -> TaskStatusResult:
return await self._video_facade_ops.poll_now(task_id, user_id=user_id)
async def cancel(
self,
task_id: str,
*,
user_id: str,
original_headers: dict[str, str] | None = None,
) -> Any:
return await self._video_facade_ops.cancel(
task_id,
user_id=user_id,
original_headers=original_headers,
)
async def finalize_video_task(self, task: Any) -> bool:
return await self._video_facade_ops.finalize_video_task(task)
async def finalize(self, task_id: str) -> bool:
return await self._video_facade_ops.finalize(task_id)