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,633 @@
import base64
from types import SimpleNamespace
from unittest.mock import AsyncMock
import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient
from fastapi.responses import JSONResponse, StreamingResponse
from src.api.internal.gateway import (
CONTROL_ACTION_HEADER,
CONTROL_ACTION_PROXY_PUBLIC,
CONTROL_EXECUTED_HEADER,
GatewayResolveRequest,
_is_streaming_sync_payload,
_resolve_auth_context,
_resolve_gateway_sync_adapter,
classify_gateway_route,
router,
)
from src.database import get_db
def test_classify_openai_chat_route_as_ai_public() -> None:
decision = classify_gateway_route("POST", "/v1/chat/completions")
assert decision.route_class == "ai_public"
assert decision.route_family == "openai"
assert decision.route_kind == "chat"
assert decision.auth_endpoint_signature == "openai:chat"
assert decision.executor_candidate is True
assert decision.action == "proxy_public"
def test_classify_gemini_files_download_route_as_ai_public() -> None:
decision = classify_gateway_route("GET", "/v1beta/files/file-123:download")
assert decision.route_class == "ai_public"
assert decision.route_family == "gemini"
assert decision.route_kind == "files"
assert decision.auth_endpoint_signature == "gemini:chat"
assert decision.executor_candidate is True
def test_classify_gemini_files_nested_metadata_route_as_ai_public() -> None:
decision = classify_gateway_route("GET", "/v1beta/files/files/abc-123")
assert decision.route_class == "ai_public"
assert decision.route_family == "gemini"
assert decision.route_kind == "files"
assert decision.executor_candidate is True
def test_classify_gemini_video_operation_route_as_ai_public() -> None:
decision = classify_gateway_route("GET", "/v1beta/models/veo-3/operations/op-123")
assert decision.route_class == "ai_public"
assert decision.route_family == "gemini"
assert decision.route_kind == "video"
assert decision.auth_endpoint_signature == "gemini:video"
assert decision.executor_candidate is True
def test_classify_non_ai_route_as_passthrough() -> None:
decision = classify_gateway_route("GET", "/api/admin/system/info")
assert decision.route_class == "passthrough"
assert decision.route_family is None
assert decision.route_kind is None
assert decision.executor_candidate is False
def test_classify_claude_cli_route_from_bearer_header() -> None:
decision = classify_gateway_route(
"POST",
"/v1/messages",
{"authorization": "Bearer sk-cli"},
)
assert decision.route_class == "ai_public"
assert decision.route_family == "claude"
assert decision.route_kind == "cli"
assert decision.auth_endpoint_signature == "claude:cli"
def test_classify_gemini_cli_route_from_user_agent() -> None:
decision = classify_gateway_route(
"POST",
"/v1beta/models/gemini-2.5-pro:generateContent",
{"user-agent": "GeminiCLI/1.2.3"},
)
assert decision.route_class == "ai_public"
assert decision.route_family == "gemini"
assert decision.route_kind == "cli"
assert decision.auth_endpoint_signature == "gemini:cli"
def test_resolve_sync_adapter_for_openai_chat_route() -> None:
decision = classify_gateway_route("POST", "/v1/chat/completions")
adapter, path_params = _resolve_gateway_sync_adapter(decision, "/v1/chat/completions")
assert adapter is not None
assert adapter.name == "openai.chat"
assert path_params == {}
def test_resolve_sync_adapter_for_gemini_route_extracts_model_path_params() -> None:
decision = classify_gateway_route("POST", "/v1beta/models/gemini-2.5-pro:generateContent")
adapter, path_params = _resolve_gateway_sync_adapter(
decision,
"/v1beta/models/gemini-2.5-pro:generateContent",
)
assert adapter is not None
assert adapter.name == "gemini.chat"
assert path_params == {"model": "gemini-2.5-pro", "stream": False}
def test_resolve_sync_adapter_rejects_non_sync_files_route() -> None:
decision = classify_gateway_route("GET", "/v1beta/files/file-123:download")
adapter, path_params = _resolve_gateway_sync_adapter(decision, "/v1beta/files/file-123:download")
assert adapter is None
assert path_params == {}
def test_is_streaming_sync_payload_detects_body_and_path_stream_flags() -> None:
assert _is_streaming_sync_payload({"stream": True}, {}) is True
assert _is_streaming_sync_payload({}, {"stream": True}) is True
assert _is_streaming_sync_payload({"stream": False}, {"stream": False}) is False
@pytest.mark.asyncio
async def test_resolve_auth_context_from_openai_bearer_header(
monkeypatch: pytest.MonkeyPatch,
) -> None:
payload = GatewayResolveRequest(
method="POST",
path="/v1/chat/completions",
headers={"Authorization": "Bearer sk-test"},
)
decision = classify_gateway_route(payload.method, payload.path, payload.headers)
monkeypatch.setattr(
"src.api.internal.gateway.AuthService.authenticate_api_key_threadsafe",
AsyncMock(
return_value=SimpleNamespace(
user=SimpleNamespace(id="user-123"),
api_key=SimpleNamespace(id="key-123"),
balance_remaining=42.5,
access_allowed=True,
)
),
)
auth_context = await _resolve_auth_context(payload, decision)
assert auth_context == {
"user_id": "user-123",
"api_key_id": "key-123",
"balance_remaining": 42.5,
"access_allowed": True,
}
def test_execute_sync_route_returns_controlled_response(monkeypatch: pytest.MonkeyPatch) -> None:
app = FastAPI()
app.include_router(router)
app.dependency_overrides[get_db] = lambda: object()
monkeypatch.setattr("src.api.internal.gateway.ensure_loopback", lambda request: None)
class FakeAdapter:
mode = SimpleNamespace(value="standard")
allowed_api_formats = ["openai:chat"]
def authorize(self, context: object) -> None:
self.authorized_context = context
async def handle(self, context: object) -> JSONResponse:
assert getattr(context, "path_params", {}) == {}
return JSONResponse(status_code=201, content={"ok": True, "request_id": context.request_id})
fake_adapter = FakeAdapter()
fake_pipeline = SimpleNamespace(_check_user_rate_limit=AsyncMock(return_value=None))
monkeypatch.setattr(
"src.api.internal.gateway._resolve_gateway_sync_adapter",
lambda decision, path: (fake_adapter, {}),
)
monkeypatch.setattr(
"src.api.internal.gateway._load_gateway_auth_models",
lambda db, auth_context: (
SimpleNamespace(id="user-123"),
SimpleNamespace(id="key-123"),
),
)
monkeypatch.setattr("src.api.internal.gateway.get_pipeline", lambda: fake_pipeline)
client = TestClient(app, base_url="http://127.0.0.1")
response = client.post(
"/api/internal/gateway/execute-sync",
json={
"trace_id": "trace-sync-123",
"method": "POST",
"path": "/v1/chat/completions",
"headers": {"user-agent": "pytest"},
"body_json": {"model": "gpt-5", "messages": []},
"auth_context": {
"user_id": "user-123",
"api_key_id": "key-123",
"balance_remaining": 12.5,
"access_allowed": True,
},
},
)
assert response.status_code == 201
assert response.headers[CONTROL_EXECUTED_HEADER] == "true"
assert response.json()["ok"] is True
assert response.json()["request_id"] == "trace-sync-123"
def test_execute_sync_route_falls_back_for_stream_payload() -> None:
app = FastAPI()
app.include_router(router)
app.dependency_overrides[get_db] = lambda: object()
monkeypatch = pytest.MonkeyPatch()
monkeypatch.setattr("src.api.internal.gateway.ensure_loopback", lambda request: None)
client = TestClient(app, base_url="http://127.0.0.1")
response = client.post(
"/api/internal/gateway/execute-sync",
json={
"trace_id": "trace-stream-123",
"method": "POST",
"path": "/v1/chat/completions",
"headers": {"user-agent": "pytest"},
"body_json": {"model": "gpt-5", "messages": [], "stream": True},
"auth_context": {
"user_id": "user-123",
"api_key_id": "key-123",
"access_allowed": True,
},
},
)
assert response.status_code == 409
assert response.headers[CONTROL_ACTION_HEADER] == CONTROL_ACTION_PROXY_PUBLIC
assert response.json() == {"action": CONTROL_ACTION_PROXY_PUBLIC}
monkeypatch.undo()
def test_execute_stream_route_returns_controlled_stream(monkeypatch: pytest.MonkeyPatch) -> None:
app = FastAPI()
app.include_router(router)
app.dependency_overrides[get_db] = lambda: object()
monkeypatch.setattr("src.api.internal.gateway.ensure_loopback", lambda request: None)
class FakeAdapter:
mode = SimpleNamespace(value="standard")
allowed_api_formats = ["openai:chat"]
def authorize(self, context: object) -> None:
self.authorized_context = context
async def handle(self, context: object) -> StreamingResponse:
async def _iter() -> object:
yield b"data: one\n\n"
yield b"data: [DONE]\n\n"
return StreamingResponse(_iter(), media_type="text/event-stream")
fake_pipeline = SimpleNamespace(_check_user_rate_limit=AsyncMock(return_value=None))
monkeypatch.setattr(
"src.api.internal.gateway._resolve_gateway_sync_adapter",
lambda decision, path: (FakeAdapter(), {}),
)
monkeypatch.setattr(
"src.api.internal.gateway._load_gateway_auth_models",
lambda db, auth_context: (
SimpleNamespace(id="user-123"),
SimpleNamespace(id="key-123"),
),
)
monkeypatch.setattr("src.api.internal.gateway.get_pipeline", lambda: fake_pipeline)
client = TestClient(app, base_url="http://127.0.0.1")
response = client.post(
"/api/internal/gateway/execute-stream",
json={
"trace_id": "trace-stream-123",
"method": "POST",
"path": "/v1/chat/completions",
"headers": {"user-agent": "pytest"},
"body_json": {"model": "gpt-5", "messages": [], "stream": True},
"auth_context": {
"user_id": "user-123",
"api_key_id": "key-123",
"balance_remaining": 12.5,
"access_allowed": True,
},
},
)
assert response.status_code == 200
assert response.headers[CONTROL_EXECUTED_HEADER] == "true"
assert response.text == "data: one\n\ndata: [DONE]\n\n"
def test_execute_stream_route_falls_back_for_sync_payload(monkeypatch: pytest.MonkeyPatch) -> None:
app = FastAPI()
app.include_router(router)
app.dependency_overrides[get_db] = lambda: object()
monkeypatch.setattr("src.api.internal.gateway.ensure_loopback", lambda request: None)
client = TestClient(app, base_url="http://127.0.0.1")
response = client.post(
"/api/internal/gateway/execute-stream",
json={
"trace_id": "trace-sync-456",
"method": "POST",
"path": "/v1/chat/completions",
"headers": {"user-agent": "pytest"},
"body_json": {"model": "gpt-5", "messages": []},
"auth_context": {
"user_id": "user-123",
"api_key_id": "key-123",
"access_allowed": True,
},
},
)
assert response.status_code == 409
assert response.headers[CONTROL_ACTION_HEADER] == CONTROL_ACTION_PROXY_PUBLIC
assert response.json() == {"action": CONTROL_ACTION_PROXY_PUBLIC}
def test_execute_sync_route_handles_gemini_files_list(monkeypatch: pytest.MonkeyPatch) -> None:
app = FastAPI()
app.include_router(router)
app.dependency_overrides[get_db] = lambda: object()
monkeypatch.setattr("src.api.internal.gateway.ensure_loopback", lambda request: None)
monkeypatch.setattr(
"src.api.internal.gateway._load_gateway_auth_models",
lambda db, auth_context: (
SimpleNamespace(id="user-123"),
SimpleNamespace(id="key-123"),
),
)
monkeypatch.setattr(
"src.api.internal.gateway.get_pipeline",
lambda: SimpleNamespace(_check_user_rate_limit=AsyncMock(return_value=None)),
)
async def _fake_list_files(
request: object,
pageSize: int | None = None,
pageToken: str | None = None,
) -> JSONResponse:
del request
assert pageSize == 25
assert pageToken == "page-2"
return JSONResponse(status_code=200, content={"files": [{"name": "files/abc"}]})
monkeypatch.setattr("src.api.public.gemini_files.list_files", _fake_list_files)
client = TestClient(app, base_url="http://127.0.0.1")
response = client.post(
"/api/internal/gateway/execute-sync",
json={
"trace_id": "trace-files-list",
"method": "GET",
"path": "/v1beta/files",
"query_string": "pageSize=25&pageToken=page-2",
"headers": {"x-goog-api-key": "client-key"},
"auth_context": {
"user_id": "user-123",
"api_key_id": "key-123",
"access_allowed": True,
},
},
)
assert response.status_code == 200
assert response.headers[CONTROL_EXECUTED_HEADER] == "true"
assert response.json() == {"files": [{"name": "files/abc"}]}
def test_execute_sync_route_handles_gemini_files_upload_raw_body(
monkeypatch: pytest.MonkeyPatch,
) -> None:
app = FastAPI()
app.include_router(router)
app.dependency_overrides[get_db] = lambda: object()
monkeypatch.setattr("src.api.internal.gateway.ensure_loopback", lambda request: None)
monkeypatch.setattr(
"src.api.internal.gateway._load_gateway_auth_models",
lambda db, auth_context: (
SimpleNamespace(id="user-123"),
SimpleNamespace(id="key-123"),
),
)
monkeypatch.setattr(
"src.api.internal.gateway.get_pipeline",
lambda: SimpleNamespace(_check_user_rate_limit=AsyncMock(return_value=None)),
)
async def _fake_upload_file(request: object) -> JSONResponse:
body = await request.body()
assert body == b"upload-bytes"
assert request.headers["content-type"] == "application/octet-stream"
return JSONResponse(status_code=201, content={"uploaded": True})
monkeypatch.setattr("src.api.public.gemini_files.upload_file", _fake_upload_file)
client = TestClient(app, base_url="http://127.0.0.1")
response = client.post(
"/api/internal/gateway/execute-sync",
json={
"trace_id": "trace-files-upload",
"method": "POST",
"path": "/upload/v1beta/files",
"query_string": "uploadType=resumable",
"headers": {
"x-goog-api-key": "client-key",
"content-type": "application/octet-stream",
},
"body_base64": base64.b64encode(b"upload-bytes").decode("ascii"),
"auth_context": {
"user_id": "user-123",
"api_key_id": "key-123",
"access_allowed": True,
},
},
)
assert response.status_code == 201
assert response.headers[CONTROL_EXECUTED_HEADER] == "true"
assert response.json() == {"uploaded": True}
def test_execute_sync_route_handles_openai_video_remix_with_original_request(
monkeypatch: pytest.MonkeyPatch,
) -> None:
app = FastAPI()
app.include_router(router)
app.dependency_overrides[get_db] = lambda: object()
monkeypatch.setattr("src.api.internal.gateway.ensure_loopback", lambda request: None)
class FakeVideoAdapter:
mode = SimpleNamespace(value="standard")
allowed_api_formats = ["openai:video"]
def authorize(self, context: object) -> None:
self.authorized_context = context
async def handle(self, context: object) -> JSONResponse:
assert context.request.method == "POST"
assert context.request.url.path == "/v1/videos/task-123/remix"
assert context.path_params == {"task_id": "task-123"}
assert await context.ensure_json_body_async() == {"prompt": "remix this"}
return JSONResponse(status_code=200, content={"video": True})
monkeypatch.setattr(
"src.api.internal.gateway._resolve_gateway_sync_adapter",
lambda decision, path: (FakeVideoAdapter(), {"task_id": "task-123"}),
)
monkeypatch.setattr(
"src.api.internal.gateway._load_gateway_auth_models",
lambda db, auth_context: (
SimpleNamespace(id="user-123"),
SimpleNamespace(id="key-123"),
),
)
monkeypatch.setattr(
"src.api.internal.gateway.get_pipeline",
lambda: SimpleNamespace(_check_user_rate_limit=AsyncMock(return_value=None)),
)
client = TestClient(app, base_url="http://127.0.0.1")
response = client.post(
"/api/internal/gateway/execute-sync",
json={
"trace_id": "trace-video-remix",
"method": "POST",
"path": "/v1/videos/task-123/remix",
"headers": {
"content-type": "application/json",
"user-agent": "pytest",
},
"body_json": {"prompt": "remix this"},
"auth_context": {
"user_id": "user-123",
"api_key_id": "key-123",
"access_allowed": True,
},
},
)
assert response.status_code == 200
assert response.headers[CONTROL_EXECUTED_HEADER] == "true"
assert response.json() == {"video": True}
def test_plan_stream_route_returns_executor_plan_for_gemini_files_download(
monkeypatch: pytest.MonkeyPatch,
) -> None:
app = FastAPI()
app.include_router(router)
app.dependency_overrides[get_db] = lambda: object()
monkeypatch.setattr("src.api.internal.gateway.ensure_loopback", lambda request: None)
fake_plan = {
"request_id": "plan-123",
"provider_id": "provider-123",
"endpoint_id": "endpoint-123",
"key_id": "key-123",
"method": "GET",
"url": "https://example.com/v1beta/files/file-123:download",
"headers": {"x-goog-api-key": "upstream-key"},
"body": {},
"stream": True,
"provider_api_format": "gemini:files",
"client_api_format": "gemini:files",
"model_name": "gemini-files",
}
monkeypatch.setattr(
"src.api.internal.gateway._load_gateway_auth_models",
lambda db, auth_context: (
SimpleNamespace(id="user-123"),
SimpleNamespace(id="key-123"),
),
)
monkeypatch.setattr(
"src.api.internal.gateway.get_pipeline",
lambda: SimpleNamespace(_check_user_rate_limit=AsyncMock(return_value=None)),
)
monkeypatch.setattr(
"src.api.internal.gateway._build_gemini_files_download_stream_plan",
AsyncMock(return_value=fake_plan),
)
client = TestClient(app, base_url="http://127.0.0.1")
response = client.post(
"/api/internal/gateway/plan-stream",
json={
"trace_id": "trace-files-plan",
"method": "GET",
"path": "/v1beta/files/file-123:download",
"query_string": "alt=media",
"headers": {"x-goog-api-key": "client-key"},
"auth_context": {
"user_id": "user-123",
"api_key_id": "key-123",
"access_allowed": True,
},
},
)
assert response.status_code == 200
assert response.json() == {
"action": "executor_stream",
"plan_kind": "gemini_files_download",
"plan": fake_plan,
}
def test_plan_stream_route_returns_executor_plan_for_openai_video_content(
monkeypatch: pytest.MonkeyPatch,
) -> None:
app = FastAPI()
app.include_router(router)
app.dependency_overrides[get_db] = lambda: object()
monkeypatch.setattr("src.api.internal.gateway.ensure_loopback", lambda request: None)
fake_plan = {
"request_id": "plan-video-123",
"provider_id": "provider-video-123",
"endpoint_id": "endpoint-video-123",
"key_id": "key-video-123",
"method": "GET",
"url": "https://api.openai.com/v1/videos/ext-123/content",
"headers": {"authorization": "Bearer upstream-key"},
"body": {},
"stream": True,
"provider_api_format": "openai:video",
"client_api_format": "openai:video",
"model_name": "sora-2",
}
monkeypatch.setattr(
"src.api.internal.gateway._load_gateway_auth_models",
lambda db, auth_context: (
SimpleNamespace(id="user-123"),
SimpleNamespace(id="key-123"),
),
)
monkeypatch.setattr(
"src.api.internal.gateway.get_pipeline",
lambda: SimpleNamespace(_check_user_rate_limit=AsyncMock(return_value=None)),
)
monkeypatch.setattr(
"src.api.internal.gateway._build_openai_video_content_stream_plan",
AsyncMock(return_value=fake_plan),
)
client = TestClient(app, base_url="http://127.0.0.1")
response = client.post(
"/api/internal/gateway/plan-stream",
json={
"trace_id": "trace-video-plan",
"method": "GET",
"path": "/v1/videos/task-123/content",
"query_string": "variant=video",
"headers": {"authorization": "Bearer client-key"},
"auth_context": {
"user_id": "user-123",
"api_key_id": "key-123",
"access_allowed": True,
},
},
)
assert response.status_code == 200
assert response.json() == {
"action": "executor_stream",
"plan_kind": "openai_video_content",
"plan": fake_plan,
}