refactor: 移除 Python 后端源码,全面迁移至 Rust gateway 架构

- 删除全部 Python 源码 (src/) 及 Alembic 迁移脚本,归档至 _deprecated_py_src/
- 重构 Rust gateway ai_pipeline: 拆分 planner/finalize 模块,新增 contracts/adaptation 层
- 重组 handlers 模块为 admin/public/proxy/internal/shared 子模块结构
- 新增 executor 模块,引入 Rust 原生数据库迁移 (aether-data/migrations)
- 简化 CI/Docker 构建流程,移除 base image 二级构建,统一为单一 app image
- 移除 Python 相关基础设施文件 (entrypoint.sh, gunicorn_conf.py, Dockerfile.base)
This commit is contained in:
fawney19
2026-04-03 16:26:16 +08:00
parent 8f26e1a31f
commit 1d9c77522a
868 changed files with 1735 additions and 2433 deletions

View File

@@ -0,0 +1,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",
]

View 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

View 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",
]

View 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",
]

View 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

View 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",
]

View 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))

View 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__()

View 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",
]