mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-01 17:00:21 +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:
22
_deprecated_py_src/services/request/__init__.py
Normal file
22
_deprecated_py_src/services/request/__init__.py
Normal file
@@ -0,0 +1,22 @@
|
||||
"""
|
||||
请求处理服务模块
|
||||
|
||||
包含候选选择、执行、execution runtime 契约与客户端等功能。
|
||||
|
||||
注意:
|
||||
- RequestBuilder 已移至 src.api.handlers.base.request_builder,请直接从该模块导入
|
||||
- record_failed_request 已移至 src.services.usage.recorder,请直接从该模块导入
|
||||
"""
|
||||
|
||||
from src.services.request.candidate import RequestCandidateService
|
||||
from src.services.request.execution_runtime_client import ExecutionRuntimeClient
|
||||
from src.services.request.execution_runtime_plan import ExecutionPlan, PreparedExecutionPlan
|
||||
from src.services.request.executor import RequestExecutor
|
||||
|
||||
__all__ = [
|
||||
"RequestCandidateService",
|
||||
"ExecutionRuntimeClient",
|
||||
"ExecutionPlan",
|
||||
"PreparedExecutionPlan",
|
||||
"RequestExecutor",
|
||||
]
|
||||
403
_deprecated_py_src/services/request/candidate.py
Normal file
403
_deprecated_py_src/services/request/candidate.py
Normal file
@@ -0,0 +1,403 @@
|
||||
"""
|
||||
请求候选记录服务 - 管理候选队列
|
||||
"""
|
||||
|
||||
import uuid
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from src.core.batch_committer import get_batch_committer
|
||||
from src.core.logger import logger
|
||||
from src.models.database import RequestCandidate
|
||||
|
||||
|
||||
class RequestCandidateService:
|
||||
"""请求候选记录服务"""
|
||||
|
||||
@staticmethod
|
||||
def _persist_candidate_update(db: Session, *, immediate: bool) -> None:
|
||||
if immediate:
|
||||
db.commit()
|
||||
return
|
||||
db.flush()
|
||||
get_batch_committer().mark_dirty(db)
|
||||
|
||||
@staticmethod
|
||||
def create_candidate(
|
||||
db: Session,
|
||||
request_id: str,
|
||||
candidate_index: int,
|
||||
candidate_id: str | None = None,
|
||||
retry_index: int = 0, # 新增:重试序号
|
||||
user_id: str | None = None,
|
||||
api_key_id: str | None = None,
|
||||
username: str | None = None,
|
||||
api_key_name: str | None = None,
|
||||
provider_id: str | None = None,
|
||||
endpoint_id: str | None = None,
|
||||
key_id: str | None = None,
|
||||
status: str = "available",
|
||||
skip_reason: str | None = None,
|
||||
is_cached: bool = False,
|
||||
extra_data: dict | None = None,
|
||||
required_capabilities: dict | None = None,
|
||||
) -> RequestCandidate:
|
||||
"""
|
||||
创建候选记录
|
||||
|
||||
Args:
|
||||
db: 数据库会话
|
||||
request_id: 请求ID
|
||||
candidate_index: 候选序号
|
||||
retry_index: 重试序号(从0开始)
|
||||
user_id: 用户ID
|
||||
api_key_id: API Key ID
|
||||
username: 用户名快照
|
||||
api_key_name: API Key 名称快照
|
||||
provider_id: Provider ID
|
||||
endpoint_id: Endpoint ID
|
||||
key_id: API Key ID
|
||||
status: 候选状态 ('available', 'used', 'skipped', 'success', 'failed')
|
||||
skip_reason: 跳过原因
|
||||
is_cached: 是否为缓存亲和性候选
|
||||
extra_data: 额外数据
|
||||
required_capabilities: 请求需要的能力标签
|
||||
"""
|
||||
candidate = RequestCandidate(
|
||||
id=str(candidate_id or uuid.uuid4()),
|
||||
request_id=request_id,
|
||||
candidate_index=candidate_index,
|
||||
retry_index=retry_index, # 新增
|
||||
user_id=user_id,
|
||||
api_key_id=api_key_id,
|
||||
username=username,
|
||||
api_key_name=api_key_name,
|
||||
provider_id=provider_id,
|
||||
endpoint_id=endpoint_id,
|
||||
key_id=key_id,
|
||||
status=status,
|
||||
skip_reason=skip_reason,
|
||||
is_cached=is_cached,
|
||||
extra_data=extra_data or {},
|
||||
required_capabilities=required_capabilities,
|
||||
created_at=datetime.now(timezone.utc),
|
||||
)
|
||||
db.add(candidate)
|
||||
db.flush() # 只flush,不立即 commit
|
||||
# 标记为批量提交(非关键数据,可延迟)
|
||||
get_batch_committer().mark_dirty(db)
|
||||
return candidate
|
||||
|
||||
@staticmethod
|
||||
def mark_candidate_started(db: Session, candidate_id: str) -> None:
|
||||
"""
|
||||
标记候选开始执行
|
||||
|
||||
Args:
|
||||
db: 数据库会话
|
||||
candidate_id: 候选ID
|
||||
"""
|
||||
candidate = db.query(RequestCandidate).filter(RequestCandidate.id == candidate_id).first()
|
||||
if candidate:
|
||||
candidate.status = "pending"
|
||||
candidate.started_at = datetime.now(timezone.utc)
|
||||
# 中间态改为 flush:最终 success/failed 仍会立即提交,
|
||||
# 但开始执行这一跳不再单独制造一次事务往返。
|
||||
RequestCandidateService._persist_candidate_update(db, immediate=False)
|
||||
|
||||
@staticmethod
|
||||
def update_candidate_status(db: Session, candidate_id: str, status: str) -> None:
|
||||
"""
|
||||
更新候选状态(通用方法)
|
||||
|
||||
Args:
|
||||
db: 数据库会话
|
||||
candidate_id: 候选ID
|
||||
status: 新状态(pending, available, success, failed, skipped)
|
||||
"""
|
||||
candidate = db.query(RequestCandidate).filter(RequestCandidate.id == candidate_id).first()
|
||||
if candidate:
|
||||
candidate.status = status
|
||||
# 如果状态变更为 pending,记录开始时间
|
||||
if status == "pending" and not candidate.started_at:
|
||||
candidate.started_at = datetime.now(timezone.utc)
|
||||
RequestCandidateService._persist_candidate_update(
|
||||
db, immediate=status not in {"pending", "streaming"}
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def mark_candidate_streaming(
|
||||
db: Session,
|
||||
candidate_id: str,
|
||||
concurrent_requests: int | None = None,
|
||||
) -> None:
|
||||
"""
|
||||
标记候选为流式传输中
|
||||
|
||||
用于流式请求:连接建立成功后,流开始传输时调用。
|
||||
此时请求尚未完成,需要等流传输完毕后再调用 mark_candidate_success。
|
||||
|
||||
注意:streaming 阶段不设置 status_code,最终状态码由
|
||||
mark_candidate_success / mark_candidate_failed 在流结束时写入。
|
||||
|
||||
Args:
|
||||
db: 数据库会话
|
||||
candidate_id: 候选ID
|
||||
concurrent_requests: 并发请求数
|
||||
"""
|
||||
candidate = db.query(RequestCandidate).filter(RequestCandidate.id == candidate_id).first()
|
||||
if candidate:
|
||||
candidate.status = "streaming"
|
||||
candidate.concurrent_requests = concurrent_requests
|
||||
# streaming 状态不设置 finished_at 和 status_code,因为请求还在进行中
|
||||
RequestCandidateService._persist_candidate_update(db, immediate=False)
|
||||
|
||||
@staticmethod
|
||||
def mark_candidate_success(
|
||||
db: Session,
|
||||
candidate_id: str,
|
||||
status_code: int,
|
||||
latency_ms: int,
|
||||
concurrent_requests: int | None = None,
|
||||
extra_data: dict | None = None,
|
||||
) -> None:
|
||||
"""
|
||||
标记候选执行成功
|
||||
|
||||
Args:
|
||||
db: 数据库会话
|
||||
candidate_id: 候选ID
|
||||
status_code: HTTP 状态码
|
||||
latency_ms: 延迟(毫秒)
|
||||
concurrent_requests: 并发请求数
|
||||
extra_data: 额外数据
|
||||
"""
|
||||
candidate = db.query(RequestCandidate).filter(RequestCandidate.id == candidate_id).first()
|
||||
if candidate:
|
||||
candidate.status = "success"
|
||||
candidate.status_code = status_code
|
||||
candidate.latency_ms = latency_ms
|
||||
candidate.concurrent_requests = concurrent_requests
|
||||
candidate.finished_at = datetime.now(timezone.utc)
|
||||
# 成功时清空错误字段(可能是整流重试后成功,之前记录过错误)
|
||||
candidate.error_type = None
|
||||
candidate.error_message = None
|
||||
if extra_data:
|
||||
candidate.extra_data = {**(candidate.extra_data or {}), **extra_data}
|
||||
# 关键状态更新:立即提交,不使用批量提交
|
||||
# 原因:前端需要实时看到请求成功/失败状态
|
||||
db.commit()
|
||||
|
||||
@staticmethod
|
||||
def mark_candidate_failed(
|
||||
db: Session,
|
||||
candidate_id: str,
|
||||
error_type: str,
|
||||
error_message: str,
|
||||
status_code: int | None = None,
|
||||
latency_ms: int | None = None,
|
||||
concurrent_requests: int | None = None,
|
||||
extra_data: dict | None = None,
|
||||
) -> None:
|
||||
"""
|
||||
标记候选执行失败
|
||||
|
||||
Args:
|
||||
db: 数据库会话
|
||||
candidate_id: 候选ID
|
||||
error_type: 错误类型
|
||||
error_message: 错误消息
|
||||
status_code: HTTP 状态码(如果有)
|
||||
latency_ms: 延迟(毫秒)
|
||||
concurrent_requests: 并发请求数
|
||||
extra_data: 额外数据
|
||||
"""
|
||||
candidate = db.query(RequestCandidate).filter(RequestCandidate.id == candidate_id).first()
|
||||
if candidate:
|
||||
candidate.status = "failed"
|
||||
candidate.error_type = error_type
|
||||
candidate.error_message = error_message
|
||||
candidate.status_code = status_code
|
||||
candidate.latency_ms = latency_ms
|
||||
candidate.concurrent_requests = concurrent_requests
|
||||
candidate.finished_at = datetime.now(timezone.utc)
|
||||
if extra_data:
|
||||
candidate.extra_data = {**(candidate.extra_data or {}), **extra_data}
|
||||
# 关键状态更新:立即提交,不使用批量提交
|
||||
# 原因:前端需要实时看到请求成功/失败状态
|
||||
db.commit()
|
||||
|
||||
@staticmethod
|
||||
def mark_candidate_cancelled(
|
||||
db: Session,
|
||||
candidate_id: str,
|
||||
status_code: int = 499,
|
||||
latency_ms: int | None = None,
|
||||
concurrent_requests: int | None = None,
|
||||
extra_data: dict | None = None,
|
||||
) -> None:
|
||||
"""
|
||||
标记候选被客户端取消
|
||||
|
||||
客户端主动断开连接不算系统失败,使用 cancelled 状态。
|
||||
|
||||
Args:
|
||||
db: 数据库会话
|
||||
candidate_id: 候选ID
|
||||
status_code: HTTP 状态码(通常是 499)
|
||||
latency_ms: 延迟(毫秒)
|
||||
concurrent_requests: 并发请求数
|
||||
extra_data: 额外数据
|
||||
"""
|
||||
candidate = db.query(RequestCandidate).filter(RequestCandidate.id == candidate_id).first()
|
||||
if candidate:
|
||||
candidate.status = "cancelled"
|
||||
candidate.status_code = status_code
|
||||
candidate.latency_ms = latency_ms
|
||||
candidate.concurrent_requests = concurrent_requests
|
||||
candidate.finished_at = datetime.now(timezone.utc)
|
||||
if extra_data:
|
||||
candidate.extra_data = {**(candidate.extra_data or {}), **extra_data}
|
||||
db.commit()
|
||||
|
||||
@staticmethod
|
||||
def mark_candidate_skipped(
|
||||
db: Session,
|
||||
candidate_id: str,
|
||||
skip_reason: str | None = None,
|
||||
*,
|
||||
status_code: int | None = None,
|
||||
concurrent_requests: int | None = None,
|
||||
extra_data: dict | None = None,
|
||||
) -> None:
|
||||
"""
|
||||
标记候选为已跳过
|
||||
|
||||
Args:
|
||||
db: 数据库会话
|
||||
candidate_id: 候选ID
|
||||
skip_reason: 跳过原因
|
||||
status_code: HTTP 状态码(可选)
|
||||
concurrent_requests: 并发请求数(这里实际记录 RPM 计数)
|
||||
extra_data: 额外数据(合并写入)
|
||||
"""
|
||||
candidate = db.query(RequestCandidate).filter(RequestCandidate.id == candidate_id).first()
|
||||
if candidate:
|
||||
candidate.status = "skipped"
|
||||
candidate.skip_reason = skip_reason
|
||||
candidate.finished_at = datetime.now(timezone.utc)
|
||||
|
||||
if status_code is not None:
|
||||
candidate.status_code = int(status_code)
|
||||
if concurrent_requests is not None:
|
||||
candidate.concurrent_requests = int(concurrent_requests)
|
||||
|
||||
if extra_data:
|
||||
base = candidate.extra_data if isinstance(candidate.extra_data, dict) else {}
|
||||
candidate.extra_data = {**base, **extra_data}
|
||||
|
||||
db.flush() # 只 flush,不立即 commit
|
||||
get_batch_committer().mark_dirty(db)
|
||||
|
||||
@staticmethod
|
||||
def get_candidates_by_request_id(db: Session, request_id: str) -> list[RequestCandidate]:
|
||||
"""
|
||||
获取请求的所有候选记录
|
||||
|
||||
Args:
|
||||
db: 数据库会话
|
||||
request_id: 请求ID
|
||||
|
||||
Returns:
|
||||
候选记录列表,按 candidate_index 排序
|
||||
"""
|
||||
return (
|
||||
db.query(RequestCandidate)
|
||||
.filter(RequestCandidate.request_id == request_id)
|
||||
.order_by(RequestCandidate.candidate_index)
|
||||
.all()
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def get_candidate_stats_by_provider(db: Session, provider_id: str, limit: int = 100) -> dict:
|
||||
"""
|
||||
获取 Provider 的候选统计
|
||||
|
||||
Args:
|
||||
db: 数据库会话
|
||||
provider_id: Provider ID
|
||||
limit: 最近记录数量限制
|
||||
|
||||
Returns:
|
||||
统计信息字典
|
||||
"""
|
||||
candidates = (
|
||||
db.query(RequestCandidate)
|
||||
.filter(RequestCandidate.provider_id == provider_id)
|
||||
.order_by(RequestCandidate.created_at.desc())
|
||||
.limit(limit)
|
||||
.all()
|
||||
)
|
||||
|
||||
total_candidates = len(candidates)
|
||||
success_count = sum(1 for c in candidates if c.status == "success")
|
||||
failed_count = sum(1 for c in candidates if c.status == "failed")
|
||||
cancelled_count = sum(1 for c in candidates if c.status == "cancelled")
|
||||
skipped_count = sum(1 for c in candidates if c.status == "skipped")
|
||||
pending_count = sum(1 for c in candidates if c.status == "pending")
|
||||
available_count = sum(1 for c in candidates if c.status == "available")
|
||||
|
||||
# 计算失败率(只统计已完成的候选,即成功或失败的,cancelled 不算失败)
|
||||
completed_count = success_count + failed_count
|
||||
failure_rate = (failed_count / completed_count * 100) if completed_count > 0 else 0
|
||||
|
||||
return {
|
||||
"total_attempts": total_candidates, # 前端使用 total_attempts 字段
|
||||
"success_count": success_count,
|
||||
"failed_count": failed_count,
|
||||
"cancelled_count": cancelled_count, # 客户端取消数
|
||||
"skipped_count": skipped_count,
|
||||
"pending_count": pending_count,
|
||||
"available_count": available_count, # 尚未被调度的候选数
|
||||
"failure_rate": round(failure_rate, 2),
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def calculate_candidate_ttfb(
|
||||
db: Session,
|
||||
candidate_id: str,
|
||||
request_start_time: float,
|
||||
global_first_byte_time_ms: int,
|
||||
) -> int:
|
||||
"""
|
||||
计算候选自身的首字节时间 (TTFB)
|
||||
|
||||
请求链路追踪中的 TTFB 应该是"该候选自身"的首字时间,
|
||||
而不是整个请求从开始到收到首字节的时间。
|
||||
|
||||
Args:
|
||||
db: 数据库会话
|
||||
candidate_id: 候选 ID
|
||||
request_start_time: 请求开始时间(Unix timestamp,秒)
|
||||
global_first_byte_time_ms: 全局首字节时间(相对于 request_start_time 的毫秒数)
|
||||
|
||||
Returns:
|
||||
候选自身的 TTFB(毫秒),如果计算失败则返回 global_first_byte_time_ms
|
||||
"""
|
||||
try:
|
||||
candidate = (
|
||||
db.query(RequestCandidate).filter(RequestCandidate.id == candidate_id).first()
|
||||
)
|
||||
if candidate and candidate.started_at:
|
||||
started_at = candidate.started_at
|
||||
if started_at.tzinfo is None:
|
||||
started_at = started_at.replace(tzinfo=timezone.utc)
|
||||
# 使用整数毫秒计算,避免浮点精度问题
|
||||
request_start_epoch_ms = round(request_start_time * 1000)
|
||||
started_at_epoch_ms = round(started_at.timestamp() * 1000)
|
||||
first_byte_epoch_ms = request_start_epoch_ms + global_first_byte_time_ms
|
||||
return max(0, int(first_byte_epoch_ms - started_at_epoch_ms))
|
||||
except Exception as e:
|
||||
logger.debug("计算候选 TTFB 失败: {}", e)
|
||||
return global_first_byte_time_ms
|
||||
277
_deprecated_py_src/services/request/execution_runtime_client.py
Normal file
277
_deprecated_py_src/services/request/execution_runtime_client.py
Normal file
@@ -0,0 +1,277 @@
|
||||
"""
|
||||
Rust execution runtime 客户端主入口。
|
||||
|
||||
旧的 `rust_executor_client.py` 仍然保留,作为兼容入口。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import json
|
||||
from collections.abc import AsyncIterator
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any
|
||||
|
||||
import httpx
|
||||
|
||||
from src.config.settings import config
|
||||
from src.services.request.execution_runtime_plan import ExecutionPlan
|
||||
|
||||
|
||||
class ExecutionRuntimeClientError(RuntimeError):
|
||||
"""Rust execution runtime 客户端错误。"""
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class ExecutionRuntimeSyncResult:
|
||||
status_code: int
|
||||
response_json: Any = None
|
||||
headers: dict[str, str] = field(default_factory=dict)
|
||||
provider_response_json: Any = None
|
||||
response_body_bytes: bytes | None = None
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class ExecutionRuntimeStreamResult:
|
||||
status_code: int
|
||||
headers: dict[str, str]
|
||||
byte_iterator: AsyncIterator[bytes]
|
||||
response_ctx: Any
|
||||
|
||||
|
||||
class _ExecutionRuntimeManagedStreamContext:
|
||||
def __init__(self, client: httpx.AsyncClient, response_ctx: Any) -> None:
|
||||
self._client = client
|
||||
self._response_ctx = response_ctx
|
||||
self._closed = False
|
||||
|
||||
async def __aexit__(self, exc_type: object, exc: object, tb: object) -> None:
|
||||
if self._closed:
|
||||
return
|
||||
self._closed = True
|
||||
try:
|
||||
await self._response_ctx.__aexit__(exc_type, exc, tb)
|
||||
finally:
|
||||
await self._client.aclose()
|
||||
|
||||
|
||||
class ExecutionRuntimeClient:
|
||||
"""Python 控制面访问 Rust execution runtime 的轻量客户端。"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
transport: str | None = None,
|
||||
base_url: str | None = None,
|
||||
socket_path: str | None = None,
|
||||
request_timeout: float | None = None,
|
||||
) -> None:
|
||||
self.transport = (transport or config.execution_runtime_transport).strip().lower()
|
||||
self.base_url = (base_url or config.execution_runtime_base_url).strip()
|
||||
self.socket_path = (socket_path or config.execution_runtime_socket_path).strip()
|
||||
self.request_timeout = (
|
||||
request_timeout
|
||||
if request_timeout is not None
|
||||
else config.execution_runtime_request_timeout
|
||||
)
|
||||
|
||||
def _build_client(self, *, streaming: bool = False) -> httpx.AsyncClient:
|
||||
if streaming:
|
||||
timeout = httpx.Timeout(
|
||||
connect=min(self.request_timeout, 30.0),
|
||||
read=None,
|
||||
write=self.request_timeout,
|
||||
pool=self.request_timeout,
|
||||
)
|
||||
else:
|
||||
timeout = httpx.Timeout(self.request_timeout)
|
||||
if self.transport == "unix_socket":
|
||||
if not self.socket_path:
|
||||
raise ExecutionRuntimeClientError(
|
||||
"EXECUTION_RUNTIME_SOCKET_PATH is required for unix_socket"
|
||||
)
|
||||
transport = httpx.AsyncHTTPTransport(uds=self.socket_path, retries=0)
|
||||
return httpx.AsyncClient(
|
||||
transport=transport,
|
||||
base_url=self.base_url,
|
||||
timeout=timeout,
|
||||
)
|
||||
|
||||
if self.transport != "tcp":
|
||||
raise ExecutionRuntimeClientError(
|
||||
f"Unsupported execution-runtime transport: {self.transport}"
|
||||
)
|
||||
|
||||
return httpx.AsyncClient(
|
||||
base_url=self.base_url,
|
||||
timeout=timeout,
|
||||
transport=httpx.AsyncHTTPTransport(retries=0),
|
||||
)
|
||||
|
||||
async def execute_sync_json(self, plan: ExecutionPlan) -> ExecutionRuntimeSyncResult:
|
||||
async with self._build_client() as client:
|
||||
response = await client.post(
|
||||
"/v1/execute/sync",
|
||||
json=plan.to_payload(),
|
||||
)
|
||||
response.raise_for_status()
|
||||
payload = response.json()
|
||||
|
||||
status_code = int(payload.get("status_code") or 200)
|
||||
headers = payload.get("headers") or {}
|
||||
if not isinstance(headers, dict):
|
||||
raise ExecutionRuntimeClientError("Execution runtime response headers must be an object")
|
||||
|
||||
response_json = payload.get("response_json")
|
||||
provider_response_json = payload.get("provider_response_json")
|
||||
body_payload = payload.get("body")
|
||||
body_bytes_b64 = None
|
||||
if response_json is None and isinstance(body_payload, dict):
|
||||
response_json = body_payload.get("json_body")
|
||||
body_bytes_b64 = body_payload.get("body_bytes_b64")
|
||||
|
||||
response_body_bytes: bytes | None = None
|
||||
if body_bytes_b64 is not None:
|
||||
if not isinstance(body_bytes_b64, str):
|
||||
raise ExecutionRuntimeClientError(
|
||||
"Execution runtime body_bytes_b64 must be a string"
|
||||
)
|
||||
try:
|
||||
response_body_bytes = base64.b64decode(body_bytes_b64)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
raise ExecutionRuntimeClientError(
|
||||
"Execution runtime body_bytes_b64 must be valid base64"
|
||||
) from exc
|
||||
|
||||
return ExecutionRuntimeSyncResult(
|
||||
status_code=status_code,
|
||||
response_json=response_json,
|
||||
headers={str(k): str(v) for k, v in headers.items()},
|
||||
provider_response_json=provider_response_json,
|
||||
response_body_bytes=response_body_bytes,
|
||||
)
|
||||
|
||||
async def execute_stream(self, plan: ExecutionPlan) -> ExecutionRuntimeStreamResult:
|
||||
client = self._build_client(streaming=True)
|
||||
response_ctx = client.stream(
|
||||
"POST",
|
||||
"/v1/execute/stream",
|
||||
json=plan.to_payload(),
|
||||
)
|
||||
try:
|
||||
response = await response_ctx.__aenter__()
|
||||
response.raise_for_status()
|
||||
line_iter = response.aiter_lines()
|
||||
headers_frame = await self._read_first_stream_frame(line_iter)
|
||||
payload = headers_frame.get("payload")
|
||||
if not isinstance(payload, dict) or payload.get("kind") != "headers":
|
||||
raise ExecutionRuntimeClientError(
|
||||
"Execution runtime stream must start with headers frame"
|
||||
)
|
||||
|
||||
status_code = int(payload.get("status_code") or 200)
|
||||
headers = payload.get("headers") or {}
|
||||
if not isinstance(headers, dict):
|
||||
raise ExecutionRuntimeClientError(
|
||||
"Execution runtime stream headers must be an object"
|
||||
)
|
||||
|
||||
async def _byte_iter() -> AsyncIterator[bytes]:
|
||||
async for line in line_iter:
|
||||
if not line:
|
||||
continue
|
||||
frame = self._decode_stream_frame(line)
|
||||
frame_payload = frame["payload"]
|
||||
kind = str(frame_payload.get("kind") or "").strip().lower()
|
||||
if kind == "data":
|
||||
chunk_b64 = frame_payload.get("chunk_b64")
|
||||
if isinstance(chunk_b64, str):
|
||||
if chunk_b64:
|
||||
try:
|
||||
yield base64.b64decode(chunk_b64)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
raise ExecutionRuntimeClientError(
|
||||
"Execution runtime stream chunk_b64 must be valid base64"
|
||||
) from exc
|
||||
continue
|
||||
|
||||
text = frame_payload.get("text")
|
||||
if isinstance(text, str):
|
||||
if text:
|
||||
yield text.encode("utf-8")
|
||||
continue
|
||||
|
||||
if kind == "error":
|
||||
error = frame_payload.get("error") or {}
|
||||
message = str(error.get("message") or "execution runtime stream error")
|
||||
raise httpx.ReadError(message)
|
||||
|
||||
if kind == "telemetry":
|
||||
continue
|
||||
|
||||
if kind == "eof":
|
||||
break
|
||||
|
||||
raise ExecutionRuntimeClientError(
|
||||
f"Unexpected execution runtime stream frame kind: {kind}"
|
||||
)
|
||||
|
||||
return ExecutionRuntimeStreamResult(
|
||||
status_code=status_code,
|
||||
headers={str(k): str(v) for k, v in headers.items()},
|
||||
byte_iterator=_byte_iter(),
|
||||
response_ctx=_ExecutionRuntimeManagedStreamContext(client, response_ctx),
|
||||
)
|
||||
except Exception:
|
||||
try:
|
||||
await response_ctx.__aexit__(None, None, None)
|
||||
finally:
|
||||
await client.aclose()
|
||||
raise
|
||||
|
||||
@staticmethod
|
||||
def _decode_stream_frame(line: str) -> dict[str, Any]:
|
||||
try:
|
||||
frame = json.loads(line)
|
||||
except json.JSONDecodeError as exc:
|
||||
raise ExecutionRuntimeClientError(
|
||||
"Execution runtime stream frame must be valid JSON"
|
||||
) from exc
|
||||
if not isinstance(frame, dict):
|
||||
raise ExecutionRuntimeClientError("Execution runtime stream frame must be an object")
|
||||
payload = frame.get("payload")
|
||||
if not isinstance(payload, dict):
|
||||
raise ExecutionRuntimeClientError(
|
||||
"Execution runtime stream frame payload must be an object"
|
||||
)
|
||||
return frame
|
||||
|
||||
async def _read_first_stream_frame(
|
||||
self,
|
||||
line_iter: AsyncIterator[str],
|
||||
) -> dict[str, Any]:
|
||||
async for line in line_iter:
|
||||
if not line:
|
||||
continue
|
||||
return self._decode_stream_frame(line)
|
||||
raise ExecutionRuntimeClientError(
|
||||
"Execution runtime stream ended before headers frame"
|
||||
)
|
||||
|
||||
|
||||
# Compatibility aliases for older call sites still using executor terminology.
|
||||
RustExecutorClientError = ExecutionRuntimeClientError
|
||||
RustExecutorSyncResult = ExecutionRuntimeSyncResult
|
||||
RustExecutorStreamResult = ExecutionRuntimeStreamResult
|
||||
RustExecutorClient = ExecutionRuntimeClient
|
||||
|
||||
__all__ = [
|
||||
"ExecutionRuntimeClient",
|
||||
"ExecutionRuntimeClientError",
|
||||
"ExecutionRuntimeStreamResult",
|
||||
"ExecutionRuntimeSyncResult",
|
||||
"RustExecutorClient",
|
||||
"RustExecutorClientError",
|
||||
"RustExecutorStreamResult",
|
||||
"RustExecutorSyncResult",
|
||||
]
|
||||
274
_deprecated_py_src/services/request/execution_runtime_plan.py
Normal file
274
_deprecated_py_src/services/request/execution_runtime_plan.py
Normal file
@@ -0,0 +1,274 @@
|
||||
"""
|
||||
Execution runtime 计划契约
|
||||
|
||||
用于在 Python 控制面和 Rust execution runtime 之间传递稳定的请求执行信息。
|
||||
当前阶段先服务于非流式 chat 路径的计划构建与本地执行拆分。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import json
|
||||
from dataclasses import asdict, dataclass
|
||||
from typing import Any
|
||||
|
||||
|
||||
def _drop_none(value: Any) -> Any:
|
||||
"""递归移除 None 字段,便于序列化为紧凑 payload。"""
|
||||
if isinstance(value, dict):
|
||||
return {key: _drop_none(item) for key, item in value.items() if item is not None}
|
||||
if isinstance(value, list):
|
||||
return [_drop_none(item) for item in value]
|
||||
return value
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class ExecutionPlanTimeouts:
|
||||
connect_ms: int | None = None
|
||||
read_ms: int | None = None
|
||||
write_ms: int | None = None
|
||||
pool_ms: int | None = None
|
||||
total_ms: int | None = None
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class ExecutionPlanBody:
|
||||
json_body: Any = None
|
||||
body_bytes_b64: str | None = None
|
||||
body_ref: str | None = None
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class ExecutionProxySnapshot:
|
||||
enabled: bool
|
||||
mode: str | None = None
|
||||
node_id: str | None = None
|
||||
label: str | None = None
|
||||
url: str | None = None
|
||||
extra: dict[str, Any] | None = None
|
||||
|
||||
@classmethod
|
||||
def from_proxy_info(
|
||||
cls,
|
||||
proxy_info: dict[str, Any] | None,
|
||||
*,
|
||||
proxy_url: str | None = None,
|
||||
mode_override: str | None = None,
|
||||
node_id_override: str | None = None,
|
||||
extra: dict[str, Any] | None = None,
|
||||
) -> ExecutionProxySnapshot | None:
|
||||
if not proxy_info and not proxy_url:
|
||||
return None
|
||||
mode = (
|
||||
mode_override
|
||||
or str(proxy_info.get("type") or proxy_info.get("mode") or "").strip()
|
||||
or None
|
||||
)
|
||||
if not mode and proxy_url:
|
||||
mode = proxy_url.split("://", 1)[0].strip().lower() or None
|
||||
return cls(
|
||||
enabled=True,
|
||||
mode=mode,
|
||||
node_id=node_id_override
|
||||
or str((proxy_info or {}).get("node_id") or "").strip()
|
||||
or None,
|
||||
label=str((proxy_info or {}).get("label") or "").strip() or None,
|
||||
url=str(proxy_url or (proxy_info or {}).get("url") or "").strip() or None,
|
||||
extra=extra or None,
|
||||
)
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class ExecutionPlan:
|
||||
request_id: str
|
||||
candidate_id: str | None
|
||||
provider_name: str
|
||||
provider_id: str
|
||||
endpoint_id: str
|
||||
key_id: str
|
||||
method: str
|
||||
url: str
|
||||
headers: dict[str, str]
|
||||
body: ExecutionPlanBody
|
||||
stream: bool
|
||||
provider_api_format: str
|
||||
client_api_format: str
|
||||
model_name: str
|
||||
content_type: str | None = None
|
||||
content_encoding: str | None = None
|
||||
proxy: ExecutionProxySnapshot | None = None
|
||||
tls_profile: str | None = None
|
||||
timeouts: ExecutionPlanTimeouts | None = None
|
||||
|
||||
def to_payload(self) -> dict[str, Any]:
|
||||
return _drop_none(asdict(self))
|
||||
|
||||
|
||||
_REMOTE_EXECUTION_RUNTIME_BYPASS_FORMATS = {"openai:cli", "openai:compact"}
|
||||
|
||||
|
||||
def should_bypass_remote_execution_runtime_url(
|
||||
url: str | None,
|
||||
*,
|
||||
provider_api_format: str | None = None,
|
||||
client_api_format: str | None = None,
|
||||
) -> bool:
|
||||
normalized_url = str(url or "").strip().lower()
|
||||
if not normalized_url:
|
||||
return False
|
||||
|
||||
normalized_provider_api_format = str(provider_api_format or "").strip().lower()
|
||||
normalized_client_api_format = str(client_api_format or "").strip().lower()
|
||||
if (
|
||||
normalized_provider_api_format not in _REMOTE_EXECUTION_RUNTIME_BYPASS_FORMATS
|
||||
and normalized_client_api_format not in _REMOTE_EXECUTION_RUNTIME_BYPASS_FORMATS
|
||||
):
|
||||
return False
|
||||
|
||||
return "/backend-api/codex" in normalized_url or "/backendapi/codex" in normalized_url
|
||||
|
||||
|
||||
def should_bypass_remote_execution_runtime(contract: ExecutionPlan) -> bool:
|
||||
return should_bypass_remote_execution_runtime_url(
|
||||
contract.url,
|
||||
provider_api_format=contract.provider_api_format,
|
||||
client_api_format=contract.client_api_format,
|
||||
)
|
||||
|
||||
|
||||
def is_remote_execution_runtime_proxy_supported(proxy: ExecutionProxySnapshot | None) -> bool:
|
||||
proxy_mode = str((proxy.mode if proxy else "") or "").strip().lower()
|
||||
return (
|
||||
proxy is None
|
||||
or (str(proxy.url or "").strip() != "" and proxy_mode not in {"tunnel"})
|
||||
or (proxy_mode == "tunnel" and str(proxy.node_id or "").strip() != "")
|
||||
)
|
||||
|
||||
|
||||
def is_remote_execution_runtime_contract_eligible(contract: ExecutionPlan) -> bool:
|
||||
content_encoding = str(contract.content_encoding or "").strip().lower()
|
||||
has_json_body = contract.body.json_body is not None
|
||||
has_raw_body = bool(str(contract.body.body_bytes_b64 or "").strip())
|
||||
has_body = has_json_body or has_raw_body
|
||||
return (
|
||||
((not has_body and content_encoding == "") or has_body)
|
||||
and (not has_json_body or content_encoding in {"", "gzip"} or has_raw_body)
|
||||
and is_remote_execution_runtime_proxy_supported(contract.proxy)
|
||||
and not should_bypass_remote_execution_runtime(contract)
|
||||
)
|
||||
|
||||
|
||||
def build_execution_plan_body(
|
||||
payload: Any,
|
||||
*,
|
||||
content_type: str | None = None,
|
||||
) -> ExecutionPlanBody:
|
||||
normalized_content_type = str(content_type or "").strip().lower()
|
||||
|
||||
if isinstance(payload, dict):
|
||||
return ExecutionPlanBody(json_body=payload)
|
||||
|
||||
if isinstance(payload, list) and "json" in normalized_content_type:
|
||||
return ExecutionPlanBody(json_body=payload)
|
||||
|
||||
if isinstance(payload, (bytes, bytearray, memoryview)):
|
||||
return ExecutionPlanBody(body_bytes_b64=base64.b64encode(bytes(payload)).decode("ascii"))
|
||||
|
||||
if isinstance(payload, str):
|
||||
return ExecutionPlanBody(
|
||||
body_bytes_b64=base64.b64encode(payload.encode("utf-8")).decode("ascii")
|
||||
)
|
||||
|
||||
if payload is None:
|
||||
return ExecutionPlanBody()
|
||||
|
||||
return ExecutionPlanBody(
|
||||
body_bytes_b64=base64.b64encode(
|
||||
json.dumps(payload, ensure_ascii=False, separators=(",", ":")).encode("utf-8")
|
||||
).decode("ascii")
|
||||
)
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class PreparedExecutionPlan:
|
||||
"""本地执行所需的运行时上下文;`contract` 是可序列化的稳定边界。"""
|
||||
|
||||
contract: ExecutionPlan
|
||||
payload: dict[str, Any]
|
||||
headers: dict[str, str]
|
||||
upstream_is_stream: bool
|
||||
needs_conversion: bool
|
||||
provider_type: str
|
||||
request_timeout: float
|
||||
delegate_config: dict[str, Any] | None = None
|
||||
proxy_config: dict[str, Any] | None = None
|
||||
envelope: Any = None
|
||||
selected_base_url: str | None = None
|
||||
client_content_encoding: str | None = None
|
||||
proxy_info: dict[str, Any] | None = None
|
||||
|
||||
@property
|
||||
def remote_eligible(self) -> bool:
|
||||
return is_remote_execution_runtime_contract_eligible(self.contract)
|
||||
|
||||
|
||||
async def build_proxy_snapshot(
|
||||
proxy_config: dict[str, Any] | None,
|
||||
*,
|
||||
label: str = "adapter",
|
||||
) -> ExecutionProxySnapshot | None:
|
||||
if not proxy_config:
|
||||
return None
|
||||
|
||||
try:
|
||||
from src.services.proxy_node.resolver import (
|
||||
build_proxy_url_async,
|
||||
resolve_delegate_config_async,
|
||||
resolve_proxy_info_async,
|
||||
)
|
||||
|
||||
delegate_cfg = await resolve_delegate_config_async(proxy_config)
|
||||
proxy_url: str | None = None
|
||||
if proxy_config and not (delegate_cfg and delegate_cfg.get("tunnel")):
|
||||
proxy_url = await build_proxy_url_async(proxy_config)
|
||||
proxy_info = await resolve_proxy_info_async(proxy_config)
|
||||
return 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 as exc:
|
||||
from src.core.logger import logger
|
||||
|
||||
logger.warning("Build {} proxy snapshot failed: {}", label, exc)
|
||||
return None
|
||||
|
||||
|
||||
# Compatibility aliases for older call sites still using executor terminology.
|
||||
should_bypass_remote_executor_url = should_bypass_remote_execution_runtime_url
|
||||
should_bypass_remote_executor = should_bypass_remote_execution_runtime
|
||||
is_remote_proxy_supported = is_remote_execution_runtime_proxy_supported
|
||||
is_remote_contract_eligible = is_remote_execution_runtime_contract_eligible
|
||||
|
||||
__all__ = [
|
||||
"ExecutionPlan",
|
||||
"ExecutionPlanBody",
|
||||
"ExecutionPlanTimeouts",
|
||||
"ExecutionProxySnapshot",
|
||||
"PreparedExecutionPlan",
|
||||
"build_execution_plan_body",
|
||||
"build_proxy_snapshot",
|
||||
"is_remote_contract_eligible",
|
||||
"is_remote_execution_runtime_contract_eligible",
|
||||
"is_remote_execution_runtime_proxy_supported",
|
||||
"is_remote_proxy_supported",
|
||||
"should_bypass_remote_execution_runtime",
|
||||
"should_bypass_remote_execution_runtime_url",
|
||||
"should_bypass_remote_executor",
|
||||
"should_bypass_remote_executor_url",
|
||||
]
|
||||
251
_deprecated_py_src/services/request/executor.py
Normal file
251
_deprecated_py_src/services/request/executor.py
Normal file
@@ -0,0 +1,251 @@
|
||||
"""
|
||||
封装请求执行逻辑,包含并发控制与链路追踪。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import math
|
||||
import time
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from src.core.api_format.signature import make_signature_key
|
||||
from src.core.exceptions import ConcurrencyLimitError
|
||||
from src.core.logger import logger
|
||||
from src.services.health.monitor import get_health_monitor
|
||||
from src.services.provider.format import normalize_endpoint_signature
|
||||
from src.services.rate_limit.adaptive_reservation import get_adaptive_reservation_manager
|
||||
from src.services.rate_limit.adaptive_rpm import get_adaptive_rpm_manager
|
||||
from src.services.request.candidate import RequestCandidateService
|
||||
from src.services.request.model_test_debug import (
|
||||
get_candidate_model_test_debug,
|
||||
merge_model_test_debug,
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ExecutionContext:
|
||||
candidate_id: str
|
||||
candidate_index: int
|
||||
provider_id: str
|
||||
endpoint_id: str
|
||||
key_id: str
|
||||
user_id: str | None
|
||||
api_key_id: str | None
|
||||
is_cached_user: bool
|
||||
start_time: float | None = None
|
||||
elapsed_ms: int | None = None
|
||||
concurrent_requests: int | None = None
|
||||
rpm_current: int | None = None
|
||||
rpm_limit: int | None = None
|
||||
rpm_available_for_new: int | None = None
|
||||
reservation_ratio: float | None = None
|
||||
reservation_phase: str | None = None
|
||||
reservation_confidence: float | None = None
|
||||
reservation_load_factor: float | None = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class ExecutionResult:
|
||||
response: Any
|
||||
context: ExecutionContext
|
||||
|
||||
|
||||
class ExecutionError(Exception):
|
||||
def __init__(self, cause: Exception, context: ExecutionContext):
|
||||
super().__init__(str(cause))
|
||||
self.cause = cause
|
||||
self.context = context
|
||||
|
||||
|
||||
class RequestExecutor:
|
||||
def __init__(self, db: Session, concurrency_manager: Any, adaptive_manager: Any) -> None:
|
||||
self.db = db
|
||||
self.concurrency_manager = concurrency_manager
|
||||
self.adaptive_manager = adaptive_manager
|
||||
|
||||
async def execute(
|
||||
self,
|
||||
*,
|
||||
candidate: Any,
|
||||
candidate_id: str,
|
||||
candidate_index: int,
|
||||
user_api_key: Any | None,
|
||||
user_id: str | None = None,
|
||||
request_func: Callable[..., Any],
|
||||
request_id: str | None,
|
||||
api_format: str,
|
||||
model_name: str,
|
||||
is_stream: bool = False,
|
||||
) -> ExecutionResult:
|
||||
provider = candidate.provider
|
||||
endpoint = candidate.endpoint
|
||||
key = candidate.key
|
||||
is_cached_user = bool(candidate.is_cached)
|
||||
|
||||
# 标记候选开始执行
|
||||
RequestCandidateService.mark_candidate_started(
|
||||
db=self.db,
|
||||
candidate_id=candidate_id,
|
||||
)
|
||||
|
||||
context = ExecutionContext(
|
||||
candidate_id=candidate_id,
|
||||
candidate_index=candidate_index,
|
||||
provider_id=provider.id,
|
||||
endpoint_id=endpoint.id,
|
||||
key_id=key.id,
|
||||
user_id=user_id if user_id is not None else getattr(user_api_key, "user_id", None),
|
||||
api_key_id=getattr(user_api_key, "id", None),
|
||||
is_cached_user=is_cached_user,
|
||||
)
|
||||
|
||||
try:
|
||||
# 计算动态预留比例
|
||||
reservation_manager = get_adaptive_reservation_manager()
|
||||
# 获取当前 RPM 计数用于计算负载
|
||||
# 注意:key 侧返回的是 RPM 计数(不会在请求结束时减少,靠 TTL 过期)
|
||||
try:
|
||||
current_key_rpm = await self.concurrency_manager.get_key_rpm_count(
|
||||
key_id=key.id,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.debug("获取 RPM 计数失败(用于预留计算): {}", e)
|
||||
current_key_rpm = 0
|
||||
|
||||
# 在获取 guard 之前记录当前 RPM 计数,便于并发拒绝场景落库
|
||||
context.concurrent_requests = current_key_rpm
|
||||
context.rpm_current = current_key_rpm
|
||||
|
||||
# 获取有效的 RPM 限制(自适应或固定)
|
||||
effective_key_limit = get_adaptive_rpm_manager().get_effective_limit(key)
|
||||
|
||||
reservation_result = reservation_manager.calculate_reservation(
|
||||
key=key,
|
||||
current_usage=current_key_rpm,
|
||||
effective_limit=effective_key_limit,
|
||||
)
|
||||
dynamic_reservation_ratio = reservation_result.ratio
|
||||
|
||||
context.rpm_limit = effective_key_limit
|
||||
context.reservation_ratio = dynamic_reservation_ratio
|
||||
context.reservation_phase = reservation_result.phase
|
||||
context.reservation_confidence = reservation_result.confidence
|
||||
context.reservation_load_factor = reservation_result.load_factor
|
||||
|
||||
if effective_key_limit is not None and not is_cached_user:
|
||||
context.rpm_available_for_new = max(
|
||||
1, math.floor(effective_key_limit * (1 - dynamic_reservation_ratio))
|
||||
)
|
||||
|
||||
logger.debug(
|
||||
"[Executor] 动态预留: key={}..., ratio={:.0%}, phase={}, confidence={:.0%}",
|
||||
key.id[:8],
|
||||
dynamic_reservation_ratio,
|
||||
reservation_result.phase,
|
||||
reservation_result.confidence,
|
||||
)
|
||||
|
||||
async with self.concurrency_manager.rpm_guard(
|
||||
key_id=key.id,
|
||||
key_rpm_limit=effective_key_limit,
|
||||
is_cached_user=is_cached_user,
|
||||
cache_reservation_ratio=dynamic_reservation_ratio,
|
||||
):
|
||||
# 获取当前 RPM 计数(guard 内再次获取以获得最新值)
|
||||
try:
|
||||
key_rpm_count = await self.concurrency_manager.get_key_rpm_count(
|
||||
key_id=key.id,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.debug("获取 RPM 计数失败(guard 内): {}", e)
|
||||
key_rpm_count = None
|
||||
|
||||
if key_rpm_count is not None:
|
||||
context.concurrent_requests = key_rpm_count # 用于记录,实际是 RPM 计数
|
||||
context.start_time = time.time()
|
||||
|
||||
response = await request_func(provider, endpoint, key, candidate)
|
||||
|
||||
context.elapsed_ms = int((time.time() - context.start_time) * 1000)
|
||||
|
||||
fam = str(getattr(endpoint, "api_family", "")).strip().lower()
|
||||
kind = str(getattr(endpoint, "endpoint_kind", "")).strip().lower()
|
||||
provider_format_str = make_signature_key(fam, kind) if fam and kind else ""
|
||||
client_format_str = normalize_endpoint_signature(api_format)
|
||||
health_format = provider_format_str or client_format_str
|
||||
|
||||
await asyncio.to_thread(
|
||||
get_health_monitor().record_success,
|
||||
db=self.db,
|
||||
key_id=key.id,
|
||||
api_format=health_format,
|
||||
response_time_ms=context.elapsed_ms,
|
||||
)
|
||||
|
||||
# 自适应模式:rpm_limit = NULL
|
||||
if key.rpm_limit is None and key_rpm_count is not None:
|
||||
self.adaptive_manager.handle_success(
|
||||
db=self.db,
|
||||
key=key,
|
||||
current_rpm=key_rpm_count,
|
||||
)
|
||||
|
||||
# 根据是否为流式请求,标记不同状态
|
||||
if is_stream:
|
||||
# 流式请求:标记为 streaming 状态
|
||||
# 此时连接已建立但流传输尚未完成
|
||||
# success 状态会在流完成后由 _record_stream_stats 方法标记
|
||||
RequestCandidateService.mark_candidate_streaming(
|
||||
db=self.db,
|
||||
candidate_id=candidate_id,
|
||||
concurrent_requests=key_rpm_count,
|
||||
)
|
||||
else:
|
||||
# 非流式请求:标记为 success 状态
|
||||
from src.services.proxy_node.resolver import (
|
||||
resolve_effective_proxy,
|
||||
resolve_proxy_info_async,
|
||||
)
|
||||
|
||||
_eff_proxy = resolve_effective_proxy(
|
||||
getattr(provider, "proxy", None), getattr(key, "proxy", None)
|
||||
)
|
||||
_extra: dict[str, Any] = {
|
||||
"is_cached_user": is_cached_user,
|
||||
"model_name": model_name,
|
||||
"api_format": api_format,
|
||||
}
|
||||
_pi = await resolve_proxy_info_async(_eff_proxy)
|
||||
if _pi:
|
||||
_extra["proxy"] = _pi
|
||||
_extra = (
|
||||
merge_model_test_debug(
|
||||
_extra,
|
||||
get_candidate_model_test_debug(candidate),
|
||||
)
|
||||
or _extra
|
||||
)
|
||||
RequestCandidateService.mark_candidate_success(
|
||||
db=self.db,
|
||||
candidate_id=candidate_id,
|
||||
status_code=200,
|
||||
latency_ms=context.elapsed_ms,
|
||||
concurrent_requests=key_rpm_count,
|
||||
extra_data=_extra,
|
||||
)
|
||||
|
||||
return ExecutionResult(response=response, context=context)
|
||||
except ConcurrencyLimitError as exc:
|
||||
raise ExecutionError(exc, context) from exc
|
||||
except Exception as exc:
|
||||
context.elapsed_ms = (
|
||||
int((time.time() - context.start_time) * 1000)
|
||||
if context.start_time is not None
|
||||
else None
|
||||
)
|
||||
raise ExecutionError(exc, context) from exc
|
||||
41
_deprecated_py_src/services/request/executor_plan.py
Normal file
41
_deprecated_py_src/services/request/executor_plan.py
Normal file
@@ -0,0 +1,41 @@
|
||||
"""
|
||||
旧 executor 命名兼容入口。
|
||||
|
||||
当前主实现已经迁到 `execution_runtime_plan.py`。
|
||||
"""
|
||||
|
||||
from src.services.request.execution_runtime_plan import (
|
||||
ExecutionPlan,
|
||||
ExecutionPlanBody,
|
||||
ExecutionPlanTimeouts,
|
||||
ExecutionProxySnapshot,
|
||||
PreparedExecutionPlan,
|
||||
build_execution_plan_body,
|
||||
build_proxy_snapshot,
|
||||
is_remote_contract_eligible,
|
||||
is_remote_execution_runtime_contract_eligible,
|
||||
is_remote_execution_runtime_proxy_supported,
|
||||
is_remote_proxy_supported,
|
||||
should_bypass_remote_execution_runtime,
|
||||
should_bypass_remote_execution_runtime_url,
|
||||
should_bypass_remote_executor,
|
||||
should_bypass_remote_executor_url,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"ExecutionPlan",
|
||||
"ExecutionPlanBody",
|
||||
"ExecutionPlanTimeouts",
|
||||
"ExecutionProxySnapshot",
|
||||
"PreparedExecutionPlan",
|
||||
"build_execution_plan_body",
|
||||
"build_proxy_snapshot",
|
||||
"is_remote_contract_eligible",
|
||||
"is_remote_execution_runtime_contract_eligible",
|
||||
"is_remote_execution_runtime_proxy_supported",
|
||||
"is_remote_proxy_supported",
|
||||
"should_bypass_remote_execution_runtime",
|
||||
"should_bypass_remote_execution_runtime_url",
|
||||
"should_bypass_remote_executor",
|
||||
"should_bypass_remote_executor_url",
|
||||
]
|
||||
57
_deprecated_py_src/services/request/model_test_debug.py
Normal file
57
_deprecated_py_src/services/request/model_test_debug.py
Normal file
@@ -0,0 +1,57 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from copy import deepcopy
|
||||
from typing import Any
|
||||
|
||||
MODEL_TEST_DEBUG_KEY = "model_test_debug"
|
||||
MODEL_TEST_DEBUG_ATTR = "_model_test_debug"
|
||||
|
||||
|
||||
def normalize_model_test_debug_payload(debug_payload: Any) -> dict[str, Any] | None:
|
||||
if not isinstance(debug_payload, dict):
|
||||
return None
|
||||
|
||||
normalized: dict[str, Any] = {}
|
||||
for key in (
|
||||
"request_url",
|
||||
"request_headers",
|
||||
"request_body",
|
||||
"response_headers",
|
||||
"response_body",
|
||||
):
|
||||
value = debug_payload.get(key)
|
||||
if value is None:
|
||||
continue
|
||||
normalized[key] = deepcopy(value)
|
||||
|
||||
return normalized or None
|
||||
|
||||
|
||||
def set_candidate_model_test_debug(candidate: Any, debug_payload: Any) -> None:
|
||||
normalized = normalize_model_test_debug_payload(debug_payload)
|
||||
if normalized is None:
|
||||
return
|
||||
setattr(candidate, MODEL_TEST_DEBUG_ATTR, normalized)
|
||||
|
||||
|
||||
def get_candidate_model_test_debug(candidate: Any) -> dict[str, Any] | None:
|
||||
return normalize_model_test_debug_payload(getattr(candidate, MODEL_TEST_DEBUG_ATTR, None))
|
||||
|
||||
|
||||
def merge_model_test_debug(
|
||||
extra_data: dict[str, Any] | None,
|
||||
debug_payload: Any,
|
||||
) -> dict[str, Any] | None:
|
||||
normalized = normalize_model_test_debug_payload(debug_payload)
|
||||
if normalized is None:
|
||||
return extra_data
|
||||
|
||||
merged = dict(extra_data or {})
|
||||
merged[MODEL_TEST_DEBUG_KEY] = normalized
|
||||
return merged
|
||||
|
||||
|
||||
def get_model_test_debug_from_extra_data(extra_data: Any) -> dict[str, Any] | None:
|
||||
if not isinstance(extra_data, dict):
|
||||
return None
|
||||
return normalize_model_test_debug_payload(extra_data.get(MODEL_TEST_DEBUG_KEY))
|
||||
376
_deprecated_py_src/services/request/result.py
Normal file
376
_deprecated_py_src/services/request/result.py
Normal file
@@ -0,0 +1,376 @@
|
||||
"""
|
||||
统一的请求结果和元数据结构
|
||||
|
||||
设计原则:
|
||||
1. RequestMetadata: 描述请求执行的上下文(Provider、Endpoint、Key、API格式等)
|
||||
2. RequestResult: 封装请求的完整结果(成功/失败、响应、元数据、费用等)
|
||||
3. 确保 api_format 在整个链路中始终可用
|
||||
|
||||
使用场景:
|
||||
- ProviderService 创建 RequestMetadata
|
||||
- TaskService 在异常时补充 RequestMetadata
|
||||
- ChatHandlerBase 使用 RequestResult 记录 Usage
|
||||
- ChatAdapterBase 使用 RequestResult 处理异常响应
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import AsyncIterator
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
from typing import Any
|
||||
|
||||
|
||||
class RequestStatus(Enum):
|
||||
"""请求状态"""
|
||||
|
||||
SUCCESS = "success"
|
||||
FAILED = "failed"
|
||||
PARTIAL = "partial" # 流式请求部分成功
|
||||
CANCELLED = "cancelled" # 客户端主动断开连接
|
||||
|
||||
|
||||
@dataclass
|
||||
class RequestMetadata:
|
||||
"""
|
||||
请求元数据 - 描述请求执行的上下文
|
||||
|
||||
必填字段:
|
||||
- api_format: API 格式,必须在请求开始时就确定
|
||||
- provider: Provider 名称
|
||||
- model: 模型名称
|
||||
|
||||
可选字段:
|
||||
- provider_id, provider_endpoint_id, provider_api_key_id: Provider 追踪信息
|
||||
- provider_request_headers, provider_response_headers: 请求/响应头
|
||||
- attempt_id: 请求尝试 ID
|
||||
- original_model: 用户请求的原始模型名(映射前)
|
||||
"""
|
||||
|
||||
# 必填字段 - 在请求开始时就应该确定
|
||||
api_format: str
|
||||
provider: str = "unknown"
|
||||
model: str = "unknown"
|
||||
|
||||
# 结构化格式维度(从 Adapter 层透传,优先于从 api_format 字符串解析)
|
||||
api_family: str | None = None # 协议族: claude, openai, gemini
|
||||
endpoint_kind: str | None = None # 端点类型: chat, cli, video
|
||||
|
||||
# Provider 追踪信息
|
||||
provider_id: str | None = None
|
||||
provider_endpoint_id: str | None = None
|
||||
provider_api_key_id: str | None = None
|
||||
|
||||
# 请求/响应头
|
||||
provider_request_headers: dict[str, str] = field(default_factory=dict)
|
||||
provider_response_headers: dict[str, str] = field(default_factory=dict)
|
||||
|
||||
# 其他元数据
|
||||
attempt_id: str | None = None
|
||||
original_model: str | None = None # 用户请求的原始模型名(用于价格计算)
|
||||
|
||||
# Provider 响应元数据(存储 provider 返回的额外信息,如 Gemini 的 modelVersion)
|
||||
response_metadata: dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
def with_provider_info(
|
||||
self,
|
||||
provider: str,
|
||||
provider_id: str,
|
||||
provider_endpoint_id: str,
|
||||
provider_api_key_id: str,
|
||||
) -> RequestMetadata:
|
||||
"""返回包含 Provider 信息的新 RequestMetadata"""
|
||||
return RequestMetadata(
|
||||
api_format=self.api_format,
|
||||
provider=provider,
|
||||
model=self.model,
|
||||
api_family=self.api_family,
|
||||
endpoint_kind=self.endpoint_kind,
|
||||
provider_id=provider_id,
|
||||
provider_endpoint_id=provider_endpoint_id,
|
||||
provider_api_key_id=provider_api_key_id,
|
||||
provider_request_headers=self.provider_request_headers,
|
||||
provider_response_headers=self.provider_response_headers,
|
||||
attempt_id=self.attempt_id,
|
||||
original_model=self.original_model,
|
||||
response_metadata=self.response_metadata,
|
||||
)
|
||||
|
||||
def with_response_headers(self, headers: dict[str, str]) -> RequestMetadata:
|
||||
"""返回包含响应头的新 RequestMetadata"""
|
||||
return RequestMetadata(
|
||||
api_format=self.api_format,
|
||||
provider=self.provider,
|
||||
model=self.model,
|
||||
api_family=self.api_family,
|
||||
endpoint_kind=self.endpoint_kind,
|
||||
provider_id=self.provider_id,
|
||||
provider_endpoint_id=self.provider_endpoint_id,
|
||||
provider_api_key_id=self.provider_api_key_id,
|
||||
provider_request_headers=self.provider_request_headers,
|
||||
provider_response_headers=headers,
|
||||
attempt_id=self.attempt_id,
|
||||
original_model=self.original_model,
|
||||
response_metadata=self.response_metadata,
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class UsageInfo:
|
||||
"""Token 使用量信息"""
|
||||
|
||||
input_tokens: int = 0
|
||||
output_tokens: int = 0
|
||||
cache_creation_input_tokens: int = 0
|
||||
cache_read_input_tokens: int = 0
|
||||
|
||||
|
||||
@dataclass
|
||||
class CostInfo:
|
||||
"""费用信息"""
|
||||
|
||||
input_cost_usd: float = 0.0
|
||||
output_cost_usd: float = 0.0
|
||||
cache_creation_cost_usd: float = 0.0
|
||||
cache_read_cost_usd: float = 0.0
|
||||
cache_cost_usd: float = 0.0
|
||||
total_cost_usd: float = 0.0
|
||||
|
||||
# 实际费用(乘以 rate_multiplier 后)
|
||||
actual_input_cost_usd: float = 0.0
|
||||
actual_output_cost_usd: float = 0.0
|
||||
actual_cache_creation_cost_usd: float = 0.0
|
||||
actual_cache_read_cost_usd: float = 0.0
|
||||
actual_total_cost_usd: float = 0.0
|
||||
|
||||
|
||||
@dataclass
|
||||
class RequestResult:
|
||||
"""
|
||||
请求结果 - 封装请求的完整结果
|
||||
|
||||
用于:
|
||||
- 成功请求:包含响应数据、使用量、费用
|
||||
- 失败请求:包含错误信息、状态码
|
||||
- 流式请求:包含流生成器和元数据
|
||||
"""
|
||||
|
||||
# 状态
|
||||
status: RequestStatus
|
||||
|
||||
# 元数据(必须存在)
|
||||
metadata: RequestMetadata
|
||||
|
||||
# 响应相关
|
||||
response_data: Any | None = None # 成功时的响应数据
|
||||
stream: AsyncIterator[str] | None = None # 流式响应
|
||||
|
||||
# 使用量和费用
|
||||
usage: UsageInfo = field(default_factory=UsageInfo)
|
||||
cost: CostInfo = field(default_factory=CostInfo)
|
||||
|
||||
# 错误信息
|
||||
status_code: int = 200
|
||||
error_message: str | None = None
|
||||
error_type: str | None = None
|
||||
|
||||
# 计时
|
||||
response_time_ms: int = 0
|
||||
|
||||
# 请求信息(用于记录)
|
||||
is_stream: bool = False
|
||||
request_headers: dict[str, str] = field(default_factory=dict)
|
||||
request_body: dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
@property
|
||||
def is_success(self) -> bool:
|
||||
return self.status == RequestStatus.SUCCESS
|
||||
|
||||
@property
|
||||
def is_failed(self) -> bool:
|
||||
return self.status == RequestStatus.FAILED
|
||||
|
||||
@property
|
||||
def is_cancelled(self) -> bool:
|
||||
return self.status == RequestStatus.CANCELLED
|
||||
|
||||
@classmethod
|
||||
def success(
|
||||
cls,
|
||||
metadata: RequestMetadata,
|
||||
response_data: Any,
|
||||
usage: UsageInfo,
|
||||
response_time_ms: int,
|
||||
is_stream: bool = False,
|
||||
) -> RequestResult:
|
||||
"""创建成功的请求结果"""
|
||||
return cls(
|
||||
status=RequestStatus.SUCCESS,
|
||||
metadata=metadata,
|
||||
response_data=response_data,
|
||||
usage=usage,
|
||||
status_code=200,
|
||||
response_time_ms=response_time_ms,
|
||||
is_stream=is_stream,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def failed(
|
||||
cls,
|
||||
metadata: RequestMetadata,
|
||||
status_code: int,
|
||||
error_message: str,
|
||||
error_type: str,
|
||||
response_time_ms: int,
|
||||
is_stream: bool = False,
|
||||
) -> RequestResult:
|
||||
"""创建失败的请求结果"""
|
||||
return cls(
|
||||
status=RequestStatus.FAILED,
|
||||
metadata=metadata,
|
||||
status_code=status_code,
|
||||
error_message=error_message,
|
||||
error_type=error_type,
|
||||
response_time_ms=response_time_ms,
|
||||
is_stream=is_stream,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def cancelled(
|
||||
cls,
|
||||
metadata: RequestMetadata,
|
||||
response_time_ms: int,
|
||||
usage: UsageInfo | None = None,
|
||||
is_stream: bool = False,
|
||||
) -> RequestResult:
|
||||
"""创建客户端取消的请求结果"""
|
||||
return cls(
|
||||
status=RequestStatus.CANCELLED,
|
||||
metadata=metadata,
|
||||
status_code=499,
|
||||
error_message="client_disconnected",
|
||||
error_type="client_disconnected",
|
||||
response_time_ms=response_time_ms,
|
||||
usage=usage or UsageInfo(),
|
||||
is_stream=is_stream,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_exception(
|
||||
cls,
|
||||
exception: Exception,
|
||||
api_format: str,
|
||||
model: str,
|
||||
response_time_ms: int,
|
||||
is_stream: bool = False,
|
||||
) -> RequestResult:
|
||||
"""从异常创建失败的请求结果"""
|
||||
# 尝试从异常中提取 metadata
|
||||
existing_metadata = getattr(exception, "request_metadata", None)
|
||||
|
||||
def get_meta_value(meta: Any, key: str, default: Any | None = None) -> Any:
|
||||
"""从 metadata 中提取值,支持字典和对象两种形式"""
|
||||
if meta is None:
|
||||
return default
|
||||
if isinstance(meta, dict):
|
||||
return meta.get(key, default)
|
||||
return getattr(meta, key, default)
|
||||
|
||||
if existing_metadata:
|
||||
# 如果异常已有 metadata,使用它但确保 api_format 存在
|
||||
metadata = RequestMetadata(
|
||||
api_format=get_meta_value(existing_metadata, "api_format") or api_format,
|
||||
provider=get_meta_value(existing_metadata, "provider", "unknown") or "unknown",
|
||||
model=get_meta_value(existing_metadata, "model", model) or model,
|
||||
api_family=get_meta_value(existing_metadata, "api_family"),
|
||||
endpoint_kind=get_meta_value(existing_metadata, "endpoint_kind"),
|
||||
provider_id=get_meta_value(existing_metadata, "provider_id"),
|
||||
provider_endpoint_id=get_meta_value(existing_metadata, "provider_endpoint_id"),
|
||||
provider_api_key_id=get_meta_value(existing_metadata, "provider_api_key_id"),
|
||||
provider_request_headers=get_meta_value(
|
||||
existing_metadata, "provider_request_headers", {}
|
||||
),
|
||||
provider_response_headers=get_meta_value(
|
||||
existing_metadata, "provider_response_headers", {}
|
||||
),
|
||||
attempt_id=get_meta_value(existing_metadata, "attempt_id"),
|
||||
original_model=get_meta_value(existing_metadata, "original_model"),
|
||||
response_metadata=get_meta_value(existing_metadata, "response_metadata", {}),
|
||||
)
|
||||
else:
|
||||
# 创建最小的 metadata
|
||||
metadata = RequestMetadata(
|
||||
api_format=api_format,
|
||||
provider="unknown",
|
||||
model=model,
|
||||
)
|
||||
|
||||
# 确定状态码和错误类型
|
||||
from src.core.exceptions import (
|
||||
ProviderAuthException,
|
||||
ProviderNotAvailableException,
|
||||
ProviderRateLimitException,
|
||||
ProviderTimeoutException,
|
||||
)
|
||||
|
||||
if isinstance(exception, ProviderAuthException):
|
||||
status_code = 503
|
||||
error_type = "provider_auth_error"
|
||||
elif isinstance(exception, ProviderRateLimitException):
|
||||
status_code = 429
|
||||
error_type = "rate_limit_exceeded"
|
||||
elif isinstance(exception, ProviderTimeoutException):
|
||||
status_code = 504
|
||||
error_type = "timeout_error"
|
||||
elif isinstance(exception, ProviderNotAvailableException):
|
||||
status_code = 503
|
||||
error_type = "provider_unavailable"
|
||||
else:
|
||||
status_code = 500
|
||||
error_type = "internal_error"
|
||||
|
||||
# 构建错误消息:优先使用友好的 message 属性
|
||||
# upstream_response 仅用于调试/链路追踪,不作为客户端错误消息
|
||||
error_message = getattr(exception, "message", None)
|
||||
if not error_message or not isinstance(error_message, str):
|
||||
error_message = str(exception)
|
||||
|
||||
return cls(
|
||||
status=RequestStatus.FAILED,
|
||||
metadata=metadata,
|
||||
status_code=status_code,
|
||||
error_message=error_message,
|
||||
error_type=error_type,
|
||||
response_time_ms=response_time_ms,
|
||||
is_stream=is_stream,
|
||||
)
|
||||
|
||||
|
||||
class StreamWithMetadata:
|
||||
"""带元数据的流式响应包装器"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
stream: AsyncIterator[str],
|
||||
metadata: RequestMetadata,
|
||||
response_headers_container: dict[str, Any] | None = None,
|
||||
):
|
||||
self.stream = stream
|
||||
self.metadata = metadata
|
||||
self.response_headers_container = response_headers_container
|
||||
self._metadata_updated = False
|
||||
|
||||
def update_metadata_with_response_headers(self) -> None:
|
||||
"""使用实际的响应头更新元数据"""
|
||||
if self.response_headers_container and "headers" in self.response_headers_container:
|
||||
if not self._metadata_updated:
|
||||
self.metadata = self.metadata.with_response_headers(
|
||||
self.response_headers_container["headers"]
|
||||
)
|
||||
self._metadata_updated = True
|
||||
|
||||
def __aiter__(self) -> None:
|
||||
return self.stream
|
||||
|
||||
async def __anext__(self) -> None:
|
||||
return await self.stream.__anext__()
|
||||
27
_deprecated_py_src/services/request/rust_executor_client.py
Normal file
27
_deprecated_py_src/services/request/rust_executor_client.py
Normal file
@@ -0,0 +1,27 @@
|
||||
"""
|
||||
旧 executor 命名兼容入口。
|
||||
|
||||
当前主实现已经迁到 `execution_runtime_client.py`。
|
||||
"""
|
||||
|
||||
from src.services.request.execution_runtime_client import (
|
||||
ExecutionRuntimeClient,
|
||||
ExecutionRuntimeClientError,
|
||||
ExecutionRuntimeStreamResult,
|
||||
ExecutionRuntimeSyncResult,
|
||||
RustExecutorClient,
|
||||
RustExecutorClientError,
|
||||
RustExecutorStreamResult,
|
||||
RustExecutorSyncResult,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"ExecutionRuntimeClient",
|
||||
"ExecutionRuntimeClientError",
|
||||
"ExecutionRuntimeStreamResult",
|
||||
"ExecutionRuntimeSyncResult",
|
||||
"RustExecutorClient",
|
||||
"RustExecutorClientError",
|
||||
"RustExecutorStreamResult",
|
||||
"RustExecutorSyncResult",
|
||||
]
|
||||
Reference in New Issue
Block a user