feat: 引入 Rust executor/gateway sidecar 及 Python 侧双后端适配

- 新增 Rust workspace crates: aether-contracts, aether-executor, aether-gateway
- aether-executor: 支持 Unix Socket/TCP 双传输模式,处理同步/流式上游请求
- aether-gateway: 作为本地主入口代理,集成 /api/internal/gateway/resolve 认证预解析
- Python 侧新增 ExecutionPlan 契约和 RustExecutorClient,各 handler 支持
  executor_backend=rust 时将可序列化请求转发给 Rust executor 执行
- 重构 dev.sh 支持 executor/gateway 进程编排与生命周期管理
- 新增 internal gateway 路由,提供 resolve/passthrough 端点
- handler 层(chat/cli/video/endpoint_checker 等)全面适配 Rust executor 回退逻辑
- pipeline 层支持 trusted auth context 跳过重复认证
- 新增 Rust CI workflow 及对应测试用例
This commit is contained in:
fawney19
2026-03-21 12:57:09 +08:00
parent 46737d32f8
commit d735b6316f
79 changed files with 19032 additions and 522 deletions

View File

@@ -0,0 +1,198 @@
from __future__ import annotations
from collections.abc import AsyncGenerator
from types import SimpleNamespace
from typing import Any
import pytest
from fastapi.responses import Response, StreamingResponse
import src.api.handlers.openai.video_handler as video_mod
from src.api.handlers.openai.video_handler import OpenAIVideoHandler
from src.core.api_format.conversion.internal_video import VideoStatus
from src.services.request.rust_executor_client import (
RustExecutorClientError,
RustExecutorStreamResult,
)
class _DummyStreamResponseCtx:
def __init__(self) -> None:
self.closed = False
async def __aexit__(self, exc_type: object, exc: object, tb: object) -> None:
self.closed = True
async def _iter_chunks(chunks: list[bytes]) -> AsyncGenerator[bytes]:
for chunk in chunks:
yield chunk
def _make_handler() -> OpenAIVideoHandler:
return OpenAIVideoHandler(
db=SimpleNamespace(),
user=SimpleNamespace(id="user-1"),
api_key=SimpleNamespace(id="api-key-1"),
request_id="req-video-test",
client_ip="127.0.0.1",
user_agent="pytest",
start_time=0.0,
)
@pytest.mark.asyncio
async def test_handle_download_content_uses_rust_executor_for_direct_video_url(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setattr(video_mod.config, "executor_backend", "rust")
handler = _make_handler()
dummy_ctx = _DummyStreamResponseCtx()
monkeypatch.setattr(
handler,
"_get_task",
lambda task_id: SimpleNamespace(
id=task_id,
status=VideoStatus.COMPLETED.value,
video_url="https://cdn.example.com/video.mp4",
model="sora-2",
),
)
async def _fake_execute_stream(self: object, plan: object) -> RustExecutorStreamResult:
assert getattr(plan, "method") == "GET"
assert getattr(plan, "url") == "https://cdn.example.com/video.mp4"
assert getattr(plan, "body").json_body is None
assert getattr(plan, "body").body_bytes_b64 is None
return RustExecutorStreamResult(
status_code=200,
headers={"content-type": "video/mp4", "x-rust-download": "true"},
byte_iterator=_iter_chunks([b"video-", b"bytes"]),
response_ctx=dummy_ctx,
)
monkeypatch.setattr(video_mod.RustExecutorClient, "execute_stream", _fake_execute_stream)
response = await handler.handle_download_content(
task_id="task-1",
http_request=SimpleNamespace(),
original_headers={},
query_params={"variant": "video"},
)
assert isinstance(response, StreamingResponse)
assert response.headers["x-rust-download"] == "true"
body = b"".join([chunk async for chunk in response.body_iterator])
assert body == b"video-bytes"
assert dummy_ctx.closed is True
@pytest.mark.asyncio
async def test_handle_download_content_uses_rust_executor_for_upstream_content_endpoint(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setattr(video_mod.config, "executor_backend", "rust")
handler = _make_handler()
dummy_ctx = _DummyStreamResponseCtx()
monkeypatch.setattr(
handler,
"_get_task",
lambda task_id: SimpleNamespace(
id=task_id,
status=VideoStatus.COMPLETED.value,
video_url=None,
external_task_id="ext-1",
model="sora-2",
),
)
monkeypatch.setattr(
handler,
"_get_endpoint_and_key",
lambda task: (
SimpleNamespace(id="ep-1", provider_id="prov-1", base_url="https://api.openai.com"),
SimpleNamespace(id="key-1", api_key="encrypted"),
),
)
monkeypatch.setattr(video_mod.crypto_service, "decrypt", lambda _: "upstream-key")
monkeypatch.setattr(
handler,
"_build_upstream_url",
lambda base_url, suffix=None: "https://api.openai.com/v1/videos/ext-1/content",
)
monkeypatch.setattr(
handler,
"_build_upstream_headers",
lambda original_headers, upstream_key, endpoint: {"authorization": f"Bearer {upstream_key}"},
)
async def _fake_execute_stream(self: object, plan: object) -> RustExecutorStreamResult:
assert getattr(plan, "method") == "GET"
assert getattr(plan, "url") == "https://api.openai.com/v1/videos/ext-1/content"
assert getattr(plan, "headers") == {"authorization": "Bearer upstream-key"}
assert getattr(plan, "provider_id") == "prov-1"
assert getattr(plan, "endpoint_id") == "ep-1"
assert getattr(plan, "key_id") == "key-1"
return RustExecutorStreamResult(
status_code=200,
headers={"content-type": "video/mp4", "x-rust-download": "true"},
byte_iterator=_iter_chunks([b"upstream-", b"video"]),
response_ctx=dummy_ctx,
)
monkeypatch.setattr(video_mod.RustExecutorClient, "execute_stream", _fake_execute_stream)
response = await handler.handle_download_content(
task_id="task-1",
http_request=SimpleNamespace(),
original_headers={},
query_params=None,
)
assert isinstance(response, StreamingResponse)
body = b"".join([chunk async for chunk in response.body_iterator])
assert body == b"upstream-video"
assert dummy_ctx.closed is True
@pytest.mark.asyncio
async def test_handle_download_content_falls_back_to_python_proxy_when_rust_unavailable(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setattr(video_mod.config, "executor_backend", "rust")
handler = _make_handler()
monkeypatch.setattr(
handler,
"_get_task",
lambda task_id: SimpleNamespace(
id=task_id,
status=VideoStatus.COMPLETED.value,
video_url="https://cdn.example.com/video.mp4",
model="sora-2",
),
)
async def _failing_execute_stream(self: object, plan: object) -> RustExecutorStreamResult:
del self, plan
raise RustExecutorClientError("executor down")
fallback_response = Response(content=b"python-fallback", media_type="video/mp4")
async def _fake_proxy_direct_url(url: str, task_id: str) -> Response:
assert url == "https://cdn.example.com/video.mp4"
assert task_id == "task-1"
return fallback_response
monkeypatch.setattr(video_mod.RustExecutorClient, "execute_stream", _failing_execute_stream)
monkeypatch.setattr(handler, "_proxy_direct_url", _fake_proxy_direct_url)
response = await handler.handle_download_content(
task_id="task-1",
http_request=SimpleNamespace(),
original_headers={},
query_params={"variant": "video"},
)
assert response is fallback_response

View File

@@ -0,0 +1,103 @@
from __future__ import annotations
from types import SimpleNamespace
from unittest.mock import AsyncMock
import httpx
import pytest
from fastapi.responses import JSONResponse
import src.api.handlers.openai.video_handler as video_mod
from src.api.handlers.openai.video_handler import OpenAIVideoHandler
def _make_handler() -> OpenAIVideoHandler:
return OpenAIVideoHandler(
db=SimpleNamespace(),
user=SimpleNamespace(id="user-1"),
api_key=SimpleNamespace(id="api-key-1"),
request_id="req-video-sync-test",
client_ip="127.0.0.1",
user_agent="pytest",
start_time=0.0,
)
@pytest.mark.asyncio
async def test_handle_create_task_uses_rust_sync_helper(
monkeypatch: pytest.MonkeyPatch,
) -> None:
handler = _make_handler()
monkeypatch.setattr(video_mod.UsageService, "create_pending_usage", lambda *args, **kwargs: None)
monkeypatch.setattr(
handler._normalizer,
"video_request_to_internal",
lambda body: SimpleNamespace(model=str(body.get("model") or "sora-2")),
)
candidate = SimpleNamespace(provider=SimpleNamespace(name="provider-1", id="prov-1"))
endpoint = SimpleNamespace(
id="ep-1",
api_family="openai",
endpoint_kind="video",
base_url="https://api.openai.com",
body_rules=None,
)
provider_key = SimpleNamespace(id="key-1")
monkeypatch.setattr(
handler,
"_resolve_upstream_key",
AsyncMock(return_value=("upstream-key", endpoint, provider_key)),
)
monkeypatch.setattr(
handler,
"_build_upstream_url",
lambda base_url: "https://api.openai.com/v1/videos",
)
monkeypatch.setattr(
handler,
"_build_upstream_headers",
lambda original_headers, upstream_key, endpoint, **kwargs: {
"authorization": f"Bearer {upstream_key}"
},
)
monkeypatch.setattr(
video_mod.HTTPClientPool,
"get_default_client_async",
AsyncMock(side_effect=AssertionError("python fallback should not run")),
)
async def _fake_rust_sync(**kwargs: object) -> httpx.Response:
assert kwargs["method"] == "POST"
assert kwargs["url"] == "https://api.openai.com/v1/videos"
assert kwargs["provider_id"] == "prov-1"
assert kwargs["endpoint_id"] == "ep-1"
assert kwargs["key_id"] == "key-1"
assert kwargs["body"] == {"model": "sora-2", "prompt": "hello"}
return httpx.Response(
200,
request=httpx.Request("POST", str(kwargs["url"])),
json={"id": "ext-1"},
)
create_failed = AsyncMock()
monkeypatch.setattr(handler, "_try_rust_sync_http_response", _fake_rust_sync)
monkeypatch.setattr(handler, "_create_failed_task_and_usage", create_failed)
async def _fake_submit_with_failover(**kwargs: object) -> JSONResponse:
response = await kwargs["submit_func"](candidate)
assert response.status_code == 200
assert response.json()["id"] == "ext-1"
return JSONResponse(status_code=400, content={"error": {"message": "stop"}})
monkeypatch.setattr(handler, "_submit_with_failover", _fake_submit_with_failover)
response = await handler.handle_create_task(
http_request=SimpleNamespace(),
original_headers={},
original_request_body={"model": "sora-2", "prompt": "hello"},
)
assert response.status_code == 400
create_failed.assert_awaited_once()