mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-01 17:00:21 +08:00
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:
@@ -5,9 +5,11 @@ import sys
|
||||
import types
|
||||
from types import SimpleNamespace
|
||||
from typing import Any, cast
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
import pytest
|
||||
|
||||
from src.config import config as app_config
|
||||
from src.services.provider_keys.codex_usage_parser import (
|
||||
CodexUsageParseError,
|
||||
parse_codex_wham_usage_response,
|
||||
@@ -17,6 +19,7 @@ from src.services.provider_keys.quota_refresh.antigravity_refresher import (
|
||||
)
|
||||
from src.services.provider_keys.quota_refresh.codex_refresher import refresh_codex_key_quota
|
||||
from src.services.provider_keys.quota_refresh.kiro_refresher import refresh_kiro_key_quota
|
||||
from src.services.request.rust_executor_client import RustExecutorSyncResult
|
||||
|
||||
|
||||
class _FakeDB:
|
||||
@@ -149,6 +152,73 @@ async def test_codex_refresher_http_non_200_returns_error(
|
||||
assert result["status_code"] == 503
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_codex_refresher_prefers_rust_executor(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
from src.services.provider_keys.quota_refresh import codex_refresher as module
|
||||
from src.services.request import rust_executor_client as rust_module
|
||||
|
||||
monkeypatch.setattr(app_config, "executor_backend", "rust")
|
||||
|
||||
key = SimpleNamespace(
|
||||
id="k1", name="K1", api_key="enc", auth_type="api_key", auth_config=None, proxy=None
|
||||
)
|
||||
provider = SimpleNamespace(id="provider-1", proxy=None)
|
||||
endpoint = SimpleNamespace(id="endpoint-1")
|
||||
metadata_updates: dict[str, dict[str, Any]] = {}
|
||||
state_updates: dict[str, dict[str, Any]] = {}
|
||||
captured: dict[str, Any] = {}
|
||||
|
||||
async def _fake_auth_info(_endpoint: Any, _key: Any) -> Any:
|
||||
return None
|
||||
|
||||
_install_module(
|
||||
monkeypatch,
|
||||
"src.services.proxy_node.resolver",
|
||||
{
|
||||
"resolve_effective_proxy": lambda provider_proxy, key_proxy: {"enabled": True, "url": "http://proxy.test:8080"},
|
||||
"build_proxy_client_kwargs": lambda proxy, timeout: {"timeout": timeout},
|
||||
"get_system_proxy_config_async": AsyncMock(return_value=None),
|
||||
"resolve_delegate_config_async": AsyncMock(return_value=None),
|
||||
"build_proxy_url_async": AsyncMock(return_value="http://proxy.test:8080"),
|
||||
"resolve_proxy_info_async": AsyncMock(return_value={"mode": "http", "label": "proxy"}),
|
||||
},
|
||||
)
|
||||
monkeypatch.setattr(module, "get_provider_auth", _fake_auth_info)
|
||||
monkeypatch.setattr(module.crypto_service, "decrypt", lambda _v: "sk-test")
|
||||
monkeypatch.setattr(
|
||||
module, "parse_codex_wham_usage_response", lambda _data: {"used_percent": 12.5}
|
||||
)
|
||||
|
||||
async def _fake_execute_sync_json(self: object, plan: Any) -> RustExecutorSyncResult:
|
||||
captured["plan"] = plan
|
||||
return RustExecutorSyncResult(
|
||||
status_code=200,
|
||||
response_json={"ok": True},
|
||||
headers={"content-type": "application/json"},
|
||||
)
|
||||
|
||||
monkeypatch.setattr(rust_module.RustExecutorClient, "execute_sync_json", _fake_execute_sync_json)
|
||||
|
||||
result = await refresh_codex_key_quota(
|
||||
db=cast(Any, _FakeDB()),
|
||||
provider=cast(Any, provider),
|
||||
key=cast(Any, key),
|
||||
endpoint=cast(Any, endpoint),
|
||||
codex_wham_usage_url="https://example.test/wham/usage",
|
||||
metadata_updates=metadata_updates,
|
||||
state_updates=state_updates,
|
||||
)
|
||||
|
||||
assert result["status"] == "success"
|
||||
assert captured["plan"].method == "GET"
|
||||
assert captured["plan"].url == "https://example.test/wham/usage"
|
||||
assert captured["plan"].proxy is not None
|
||||
assert captured["plan"].proxy.url == "http://proxy.test:8080"
|
||||
assert metadata_updates["k1"]["codex"]["used_percent"] == 12.5
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_codex_refresher_http_401_marks_auth_invalid_without_disabling_key(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
import pytest
|
||||
|
||||
from src.services.provider_ops.service import ProviderOpsService
|
||||
from src.services.provider_ops.types import ConnectorAuthType
|
||||
from src.services.provider_ops.types import ConnectorAuthType, ProviderActionType
|
||||
from src.services.request.rust_executor_client import RustExecutorSyncResult
|
||||
|
||||
|
||||
class _FakeDB:
|
||||
@@ -51,6 +53,43 @@ class _FakeRegistry:
|
||||
return self._architecture
|
||||
|
||||
|
||||
class _SuccessResult:
|
||||
def __init__(self) -> None:
|
||||
self.success = True
|
||||
self.quota = 200.0
|
||||
self.extra = {"window": "day"}
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return {"success": True, "quota": self.quota}
|
||||
|
||||
|
||||
class _SuccessArchitecture:
|
||||
default_action_configs = {ProviderActionType.QUERY_BALANCE: {"quota_divisor": 100}}
|
||||
|
||||
def get_verify_endpoint(self) -> str:
|
||||
return "/verify"
|
||||
|
||||
async def prepare_verify_config(
|
||||
self,
|
||||
_base_url: str,
|
||||
_config: dict[str, Any],
|
||||
_credentials: dict[str, Any],
|
||||
) -> dict[str, Any]:
|
||||
return {}
|
||||
|
||||
def build_verify_headers(
|
||||
self,
|
||||
_config: dict[str, Any],
|
||||
_credentials: dict[str, Any],
|
||||
) -> dict[str, str]:
|
||||
return {"authorization": "Bearer test"}
|
||||
|
||||
def parse_verify_response(self, status_code: int, data: dict[str, Any]) -> _SuccessResult:
|
||||
assert status_code == 200
|
||||
assert data == {"ok": True}
|
||||
return _SuccessResult()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_verify_auth_returns_failure_when_prepare_verify_config_raises_value_error(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
@@ -72,3 +111,57 @@ async def test_verify_auth_returns_failure_when_prepare_verify_config_raises_val
|
||||
)
|
||||
|
||||
assert result == {"success": False, "message": "invalid refresh token"}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_verify_auth_prefers_rust_executor(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
from src.services.provider_ops import service as module
|
||||
from src.services.request import rust_executor_client as rust_module
|
||||
|
||||
service = ProviderOpsService(_FakeDB())
|
||||
architecture = _SuccessArchitecture()
|
||||
|
||||
monkeypatch.setattr(
|
||||
module,
|
||||
"get_registry",
|
||||
lambda: _FakeRegistry(architecture),
|
||||
)
|
||||
monkeypatch.setattr(module.config, "executor_backend", "rust")
|
||||
monkeypatch.setattr(
|
||||
"src.services.proxy_node.resolver.resolve_ops_proxy_config_async",
|
||||
AsyncMock(return_value=(None, "node-1")),
|
||||
)
|
||||
|
||||
cache_balance = AsyncMock()
|
||||
monkeypatch.setattr(service, "_cache_balance_from_verify", cache_balance)
|
||||
|
||||
captured: dict[str, Any] = {}
|
||||
|
||||
async def _fake_execute_sync_json(self: object, plan: Any) -> RustExecutorSyncResult:
|
||||
captured["plan"] = plan
|
||||
return RustExecutorSyncResult(
|
||||
status_code=200,
|
||||
response_json={"ok": True},
|
||||
headers={"content-type": "application/json"},
|
||||
)
|
||||
|
||||
monkeypatch.setattr(rust_module.RustExecutorClient, "execute_sync_json", _fake_execute_sync_json)
|
||||
|
||||
result = await service.verify_auth(
|
||||
base_url="https://example.com",
|
||||
architecture_id="sub2api",
|
||||
auth_type=ConnectorAuthType.SESSION_LOGIN,
|
||||
config={},
|
||||
credentials={"access_token": "token"},
|
||||
provider_id="provider-1",
|
||||
)
|
||||
|
||||
assert result == {"success": True, "quota": 200.0}
|
||||
assert captured["plan"].method == "GET"
|
||||
assert captured["plan"].url == "https://example.com/verify"
|
||||
assert captured["plan"].proxy is not None
|
||||
assert captured["plan"].proxy.mode == "tunnel"
|
||||
assert captured["plan"].proxy.node_id == "node-1"
|
||||
cache_balance.assert_awaited_once_with("provider-1", 2.0, {"window": "day"})
|
||||
|
||||
173
tests/services/test_video_cancel_rust_executor.py
Normal file
173
tests/services/test_video_cancel_rust_executor.py
Normal file
@@ -0,0 +1,173 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
import pytest
|
||||
|
||||
import src.services.task.video.cancel as cancel_mod
|
||||
import src.services.request.rust_executor_client as rust_client_mod
|
||||
from src.core.api_format.conversion.internal_video import VideoStatus
|
||||
from src.services.request.rust_executor_client import RustExecutorSyncResult
|
||||
from src.services.task.video.cancel import VideoTaskCancelService
|
||||
|
||||
|
||||
class _Query:
|
||||
def __init__(self, result: object) -> None:
|
||||
self._result = result
|
||||
|
||||
def filter(self, *args: object, **kwargs: object) -> "_Query":
|
||||
del args, kwargs
|
||||
return self
|
||||
|
||||
def first(self) -> object:
|
||||
return self._result
|
||||
|
||||
|
||||
class _FakeDB:
|
||||
def __init__(self, *results: object) -> None:
|
||||
self._results = list(results)
|
||||
self.committed = False
|
||||
|
||||
def query(self, model: object) -> _Query:
|
||||
del model
|
||||
return _Query(self._results.pop(0))
|
||||
|
||||
def commit(self) -> None:
|
||||
self.committed = True
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_video_cancel_service_uses_rust_for_openai_delete(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
endpoint = SimpleNamespace(
|
||||
id="ep-1",
|
||||
provider_id="prov-1",
|
||||
api_family="openai",
|
||||
endpoint_kind="video",
|
||||
base_url="https://api.openai.com",
|
||||
)
|
||||
key = SimpleNamespace(id="key-1", api_key="encrypted")
|
||||
db = _FakeDB(endpoint, key)
|
||||
service = VideoTaskCancelService(db)
|
||||
task = SimpleNamespace(
|
||||
id="task-1",
|
||||
status=VideoStatus.PROCESSING.value,
|
||||
external_task_id="ext-1",
|
||||
endpoint_id="ep-1",
|
||||
key_id="key-1",
|
||||
request_id="req-1",
|
||||
model="sora-2",
|
||||
completed_at=None,
|
||||
updated_at=None,
|
||||
)
|
||||
|
||||
monkeypatch.setattr(cancel_mod.config, "executor_backend", "rust")
|
||||
monkeypatch.setattr(
|
||||
rust_client_mod.RustExecutorClient,
|
||||
"execute_sync_json",
|
||||
AsyncMock(
|
||||
return_value=RustExecutorSyncResult(
|
||||
status_code=204,
|
||||
headers={},
|
||||
response_json=None,
|
||||
response_body_bytes=None,
|
||||
)
|
||||
),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"src.core.crypto.crypto_service.decrypt",
|
||||
lambda value: "upstream-key",
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"src.core.api_format.build_upstream_headers_for_endpoint",
|
||||
lambda *args, **kwargs: {"authorization": "Bearer upstream-key"},
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"src.services.provider.transport.build_provider_url",
|
||||
lambda endpoint, is_stream=False, key=None: "https://api.openai.com/v1/videos",
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"src.clients.http_client.HTTPClientPool.get_default_client_async",
|
||||
AsyncMock(side_effect=AssertionError("python fallback should not run")),
|
||||
)
|
||||
monkeypatch.setattr(cancel_mod.UsageService, "finalize_void", lambda *args, **kwargs: True)
|
||||
|
||||
result = await service.cancel_task(
|
||||
task=task,
|
||||
task_id="task-1",
|
||||
original_headers={"x-test": "1"},
|
||||
)
|
||||
|
||||
assert result is None
|
||||
assert db.committed is True
|
||||
assert task.status == VideoStatus.CANCELLED.value
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_video_cancel_service_uses_rust_for_gemini_cancel(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
endpoint = SimpleNamespace(
|
||||
id="ep-1",
|
||||
provider_id="prov-1",
|
||||
api_family="gemini",
|
||||
endpoint_kind="video",
|
||||
base_url="https://generativelanguage.googleapis.com",
|
||||
)
|
||||
key = SimpleNamespace(id="key-1", api_key="encrypted")
|
||||
db = _FakeDB(endpoint, key)
|
||||
service = VideoTaskCancelService(db)
|
||||
task = SimpleNamespace(
|
||||
id="task-1",
|
||||
status=VideoStatus.PROCESSING.value,
|
||||
external_task_id="operations/ext-1",
|
||||
endpoint_id="ep-1",
|
||||
key_id="key-1",
|
||||
request_id="req-1",
|
||||
model="veo-3",
|
||||
completed_at=None,
|
||||
updated_at=None,
|
||||
)
|
||||
|
||||
monkeypatch.setattr(cancel_mod.config, "executor_backend", "rust")
|
||||
execute_sync = AsyncMock(
|
||||
return_value=RustExecutorSyncResult(
|
||||
status_code=200,
|
||||
headers={"content-type": "application/json"},
|
||||
response_json={"ok": True},
|
||||
response_body_bytes=None,
|
||||
)
|
||||
)
|
||||
monkeypatch.setattr(rust_client_mod.RustExecutorClient, "execute_sync_json", execute_sync)
|
||||
monkeypatch.setattr(
|
||||
"src.core.crypto.crypto_service.decrypt",
|
||||
lambda value: "upstream-key",
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"src.core.api_format.build_upstream_headers_for_endpoint",
|
||||
lambda *args, **kwargs: {"x-goog-api-key": "upstream-key"},
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"src.services.provider.auth.get_provider_auth",
|
||||
AsyncMock(return_value=SimpleNamespace(auth_header="authorization", auth_value="Bearer token")),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"src.clients.http_client.HTTPClientPool.get_default_client_async",
|
||||
AsyncMock(side_effect=AssertionError("python fallback should not run")),
|
||||
)
|
||||
monkeypatch.setattr(cancel_mod.UsageService, "finalize_void", lambda *args, **kwargs: True)
|
||||
|
||||
result = await service.cancel_task(
|
||||
task=task,
|
||||
task_id="task-1",
|
||||
original_headers={"x-test": "1"},
|
||||
)
|
||||
|
||||
assert result is None
|
||||
assert db.committed is True
|
||||
assert task.status == VideoStatus.CANCELLED.value
|
||||
plan = execute_sync.await_args.args[0]
|
||||
assert plan.method == "POST"
|
||||
assert plan.body.json_body == {}
|
||||
@@ -6,7 +6,8 @@ import pytest
|
||||
|
||||
from src.core.api_format.conversion.internal_video import InternalVideoPollResult, VideoStatus
|
||||
from src.models.database import VideoTask
|
||||
from src.services.task.video.poller_adapter import VideoTaskPollerAdapter
|
||||
from src.services.request.rust_executor_client import RustExecutorSyncResult
|
||||
from src.services.task.video.poller_adapter import VideoPollContext, VideoTaskPollerAdapter
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -78,3 +79,140 @@ async def test_update_task_after_poll_skips_terminal_cancelled_task(
|
||||
|
||||
finalize.assert_not_awaited()
|
||||
session.commit.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_video_poller_try_rust_payload_passes_proxy_snapshot(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
from src.services.request import rust_executor_client as rust_mod
|
||||
from src.services.task.video import poller_adapter as mod
|
||||
|
||||
adapter = VideoTaskPollerAdapter()
|
||||
monkeypatch.setattr(mod.config, "executor_backend", "rust")
|
||||
|
||||
proxy_snapshot = object()
|
||||
captured: dict[str, object] = {}
|
||||
|
||||
async def _fake_execute_sync_json(self: object, plan: object) -> RustExecutorSyncResult:
|
||||
captured["plan"] = plan
|
||||
return RustExecutorSyncResult(status_code=200, response_json={"id": "op_1", "done": False})
|
||||
|
||||
monkeypatch.setattr(rust_mod.RustExecutorClient, "execute_sync_json", _fake_execute_sync_json)
|
||||
|
||||
ctx = VideoPollContext(
|
||||
task_id="task-1",
|
||||
external_task_id="vid_1",
|
||||
provider_api_format="openai:video",
|
||||
base_url="https://api.openai.com",
|
||||
upstream_key="upstream",
|
||||
headers={"authorization": "Bearer test"},
|
||||
poll_count=0,
|
||||
retry_count=0,
|
||||
poll_interval_seconds=15,
|
||||
max_poll_count=10,
|
||||
current_status=VideoStatus.PROCESSING.value,
|
||||
proxy_snapshot=proxy_snapshot,
|
||||
)
|
||||
|
||||
payload = await adapter._try_rust_poll_payload(
|
||||
ctx=ctx,
|
||||
url="https://api.openai.com/v1/videos/vid_1",
|
||||
)
|
||||
|
||||
assert payload == {"id": "op_1", "done": False}
|
||||
assert getattr(captured["plan"], "method") == "GET"
|
||||
assert getattr(captured["plan"], "proxy") is proxy_snapshot
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_video_poller_openai_poll_prefers_rust_payload(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
from src.services.task.video import poller_adapter as mod
|
||||
|
||||
adapter = VideoTaskPollerAdapter()
|
||||
|
||||
rust_poll = AsyncMock(return_value={"id": "vid_1", "status": "processing"})
|
||||
monkeypatch.setattr(adapter, "_try_rust_poll_payload", rust_poll)
|
||||
|
||||
normalized = InternalVideoPollResult(status=VideoStatus.PROCESSING, progress_percent=42)
|
||||
normalizer = MagicMock(return_value=normalized)
|
||||
monkeypatch.setattr(adapter._openai_normalizer, "video_poll_to_internal", normalizer)
|
||||
|
||||
get_upstream_client = AsyncMock(side_effect=AssertionError("python upstream client should not be used"))
|
||||
monkeypatch.setattr(mod.HTTPClientPool, "get_upstream_client", get_upstream_client)
|
||||
|
||||
ctx = VideoPollContext(
|
||||
task_id="task-1",
|
||||
external_task_id="vid_1",
|
||||
provider_api_format="openai:video",
|
||||
base_url="https://api.openai.com",
|
||||
upstream_key="upstream",
|
||||
headers={"authorization": "Bearer test"},
|
||||
poll_count=0,
|
||||
retry_count=0,
|
||||
poll_interval_seconds=15,
|
||||
max_poll_count=10,
|
||||
current_status=VideoStatus.PROCESSING.value,
|
||||
)
|
||||
|
||||
result = await adapter._poll_openai_with_context(ctx)
|
||||
|
||||
assert result is normalized
|
||||
normalizer.assert_called_once_with({"id": "vid_1", "status": "processing"})
|
||||
get_upstream_client.assert_not_awaited()
|
||||
rust_poll.assert_awaited_once()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_video_poller_openai_poll_fallback_uses_transport_aware_client(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
from src.services.task.video import poller_adapter as mod
|
||||
|
||||
adapter = VideoTaskPollerAdapter()
|
||||
|
||||
monkeypatch.setattr(adapter, "_try_rust_poll_payload", AsyncMock(return_value=None))
|
||||
|
||||
response = MagicMock()
|
||||
response.status_code = 200
|
||||
response.json.return_value = {"id": "vid_2", "status": "processing"}
|
||||
|
||||
client = MagicMock()
|
||||
client.get = AsyncMock(return_value=response)
|
||||
|
||||
get_upstream_client = AsyncMock(return_value=client)
|
||||
monkeypatch.setattr(mod.HTTPClientPool, "get_upstream_client", get_upstream_client)
|
||||
|
||||
normalized = InternalVideoPollResult(status=VideoStatus.PROCESSING, progress_percent=7)
|
||||
normalizer = MagicMock(return_value=normalized)
|
||||
monkeypatch.setattr(adapter._openai_normalizer, "video_poll_to_internal", normalizer)
|
||||
|
||||
ctx = VideoPollContext(
|
||||
task_id="task-2",
|
||||
external_task_id="vid_2",
|
||||
provider_api_format="openai:video",
|
||||
base_url="https://api.openai.com",
|
||||
upstream_key="upstream",
|
||||
headers={"authorization": "Bearer test"},
|
||||
poll_count=0,
|
||||
retry_count=0,
|
||||
poll_interval_seconds=15,
|
||||
max_poll_count=10,
|
||||
current_status=VideoStatus.PROCESSING.value,
|
||||
proxy_config={"enabled": True, "url": "http://proxy.test:8080"},
|
||||
delegate_config={"node_id": "node-1", "tunnel": True},
|
||||
)
|
||||
|
||||
result = await adapter._poll_openai_with_context(ctx)
|
||||
|
||||
assert result is normalized
|
||||
get_upstream_client.assert_awaited_once_with(
|
||||
{"node_id": "node-1", "tunnel": True},
|
||||
proxy_config={"enabled": True, "url": "http://proxy.test:8080"},
|
||||
)
|
||||
client.get.assert_awaited_once_with(
|
||||
"https://api.openai.com/v1/videos/vid_2",
|
||||
headers={"authorization": "Bearer test"},
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user