mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-01 17:00:21 +08:00
feat: 扩展 Rust gateway 全功能模块,新增 billing/crypto/wallet crate 及完整数据层
- 新增 aether-billing、aether-crypto、aether-wallet 独立 crate - aether-data 扩展 repository 层:announcements、auth_modules、billing、 candidate_selection、gemini_file_mappings、global_models、management_tokens、 oauth_providers、proxy_nodes、quota、users、wallet 等模块 - aether-gateway 新增 api/auth/billing/control/middleware/scheduler/usage/ video_tasks/hooks/maintenance/model_fetch/provider_transport 等功能模块 - 重构 executor decision 和 gateway state 为模块目录结构 - 新增 gateway router、frontdoor 路由层及对应测试 - Python 侧 API 路由重构,新增 compat/support 模块 - 前端 Logo 组件更新及 Provider 管理页面调整
This commit is contained in:
@@ -2877,3 +2877,286 @@ async def test_build_openai_cli_stream_plan_rejects_force_rewrite_envelope(
|
||||
)
|
||||
|
||||
assert result is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_build_openai_cli_stream_plan_rejects_codex_transport_for_direct_executor(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
from src.api.handlers.openai_cli import OpenAICliAdapter
|
||||
|
||||
adapter = OpenAICliAdapter()
|
||||
fake_context = SimpleNamespace(
|
||||
path_params={},
|
||||
request_id="req-cli-stream-codex-transport-123",
|
||||
client_ip="127.0.0.1",
|
||||
user_agent="pytest",
|
||||
start_time=0.0,
|
||||
original_headers={"content-type": "application/json", "authorization": "Bearer test-key"},
|
||||
query_params={},
|
||||
client_content_encoding=None,
|
||||
extra={"perf": None},
|
||||
)
|
||||
fake_context.ensure_json_body_async = AsyncMock(
|
||||
return_value={"model": "gpt-5", "input": "hello", "stream": True}
|
||||
)
|
||||
fake_candidate = SimpleNamespace(
|
||||
provider=SimpleNamespace(
|
||||
id="provider-cli-stream-codex-transport-123",
|
||||
name="codex",
|
||||
proxy=None,
|
||||
),
|
||||
endpoint=SimpleNamespace(id="endpoint-cli-stream-codex-transport-123", api_format="openai:cli"),
|
||||
key=SimpleNamespace(id="key-cli-stream-codex-transport-123", proxy=None),
|
||||
mapping_matched_model="gpt-5",
|
||||
needs_conversion=False,
|
||||
output_limit=None,
|
||||
request_candidate_id="cand-cli-stream-codex-transport-123",
|
||||
)
|
||||
fake_upstream_request = SimpleNamespace(
|
||||
url="https://chatgpt.com/backend-api/codex/responses",
|
||||
headers={"content-type": "application/json", "accept": "text/event-stream"},
|
||||
payload={"model": "gpt-5", "input": "hello", "stream": True},
|
||||
upstream_is_stream=True,
|
||||
envelope=None,
|
||||
tls_profile=None,
|
||||
)
|
||||
|
||||
class FakeCliHandler:
|
||||
primary_api_format = "openai:cli"
|
||||
|
||||
def __init__(self, **kwargs: object) -> None:
|
||||
pass
|
||||
|
||||
def extract_model_from_request(
|
||||
self, body: dict[str, object], path_params: dict[str, str]
|
||||
) -> str:
|
||||
return "gpt-5"
|
||||
|
||||
def _resolve_capability_requirements(self, **kwargs: object) -> None:
|
||||
return None
|
||||
|
||||
async def _resolve_preferred_key_ids(self, **kwargs: object) -> None:
|
||||
return None
|
||||
|
||||
async def _get_mapped_model(self, **kwargs: object) -> str:
|
||||
return "gpt-5"
|
||||
|
||||
async def _build_upstream_request(self, **kwargs: object) -> SimpleNamespace:
|
||||
return fake_upstream_request
|
||||
|
||||
monkeypatch.setattr(adapter, "authorize", lambda context: None)
|
||||
monkeypatch.setattr(
|
||||
type(adapter),
|
||||
"HANDLER_CLASS",
|
||||
property(lambda self: FakeCliHandler),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
adapter,
|
||||
"_merge_path_params",
|
||||
lambda body, path_params: body,
|
||||
raising=False,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"src.api.internal.gateway._resolve_gateway_sync_adapter",
|
||||
lambda decision, path: (adapter, {}),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"src.api.internal.gateway._load_gateway_auth_models",
|
||||
lambda db, auth_context: (
|
||||
SimpleNamespace(id="user-cli-stream-codex-transport-123"),
|
||||
SimpleNamespace(id="api-key-cli-stream-codex-transport-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_gateway_request_context",
|
||||
lambda **kwargs: fake_context,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"src.api.internal.gateway._select_gateway_direct_candidate",
|
||||
AsyncMock(return_value=fake_candidate),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"src.services.proxy_node.resolver.resolve_proxy_info_async",
|
||||
AsyncMock(return_value=None),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"src.services.proxy_node.resolver.resolve_delegate_config_async",
|
||||
AsyncMock(return_value=None),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"src.services.proxy_node.resolver.build_proxy_url_async",
|
||||
AsyncMock(return_value=None),
|
||||
)
|
||||
|
||||
decision = classify_gateway_route("POST", "/v1/responses")
|
||||
auth_context = GatewayAuthContext(
|
||||
user_id="user-cli-stream-codex-transport-123",
|
||||
api_key_id="api-key-cli-stream-codex-transport-123",
|
||||
access_allowed=True,
|
||||
)
|
||||
payload = GatewayExecuteRequest(
|
||||
method="POST",
|
||||
path="/v1/responses",
|
||||
headers={"content-type": "application/json", "authorization": "Bearer test-key"},
|
||||
body_json={"model": "gpt-5", "input": "hello", "stream": True},
|
||||
auth_context=auth_context,
|
||||
)
|
||||
|
||||
result = await _build_openai_cli_stream_plan(
|
||||
request=SimpleNamespace(),
|
||||
payload=payload,
|
||||
db=object(),
|
||||
auth_context=auth_context,
|
||||
decision=decision,
|
||||
)
|
||||
|
||||
assert result is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_build_openai_cli_sync_plan_rejects_codex_transport_for_direct_executor(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
from src.api.handlers.openai_cli import OpenAICliAdapter
|
||||
|
||||
adapter = OpenAICliAdapter()
|
||||
fake_context = SimpleNamespace(
|
||||
path_params={},
|
||||
request_id="req-cli-sync-codex-transport-123",
|
||||
client_ip="127.0.0.1",
|
||||
user_agent="pytest",
|
||||
start_time=0.0,
|
||||
original_headers={"content-type": "application/json", "authorization": "Bearer test-key"},
|
||||
query_params={},
|
||||
client_content_encoding=None,
|
||||
extra={"perf": None},
|
||||
)
|
||||
fake_context.ensure_json_body_async = AsyncMock(
|
||||
return_value={"model": "gpt-5", "input": "hello"}
|
||||
)
|
||||
fake_candidate = SimpleNamespace(
|
||||
provider=SimpleNamespace(
|
||||
id="provider-cli-sync-codex-transport-123",
|
||||
name="codex",
|
||||
proxy=None,
|
||||
request_timeout=None,
|
||||
),
|
||||
endpoint=SimpleNamespace(id="endpoint-cli-sync-codex-transport-123", api_format="openai:cli"),
|
||||
key=SimpleNamespace(id="key-cli-sync-codex-transport-123", proxy=None),
|
||||
mapping_matched_model="gpt-5",
|
||||
needs_conversion=False,
|
||||
output_limit=None,
|
||||
request_candidate_id="cand-cli-sync-codex-transport-123",
|
||||
)
|
||||
fake_upstream_request = SimpleNamespace(
|
||||
url="https://chatgpt.com/backendapi/codex/responses",
|
||||
headers={"content-type": "application/json"},
|
||||
payload={"model": "gpt-5", "input": "hello"},
|
||||
upstream_is_stream=False,
|
||||
envelope=None,
|
||||
tls_profile=None,
|
||||
)
|
||||
|
||||
class FakeCliHandler:
|
||||
primary_api_format = "openai:cli"
|
||||
|
||||
def __init__(self, **kwargs: object) -> None:
|
||||
pass
|
||||
|
||||
def extract_model_from_request(
|
||||
self, body: dict[str, object], path_params: dict[str, str]
|
||||
) -> str:
|
||||
return "gpt-5"
|
||||
|
||||
def _resolve_capability_requirements(self, **kwargs: object) -> None:
|
||||
return None
|
||||
|
||||
async def _resolve_preferred_key_ids(self, **kwargs: object) -> None:
|
||||
return None
|
||||
|
||||
async def _get_mapped_model(self, **kwargs: object) -> str:
|
||||
return "gpt-5"
|
||||
|
||||
async def _build_upstream_request(self, **kwargs: object) -> SimpleNamespace:
|
||||
return fake_upstream_request
|
||||
|
||||
monkeypatch.setattr(adapter, "authorize", lambda context: None)
|
||||
monkeypatch.setattr(
|
||||
type(adapter),
|
||||
"HANDLER_CLASS",
|
||||
property(lambda self: FakeCliHandler),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
adapter,
|
||||
"_merge_path_params",
|
||||
lambda body, path_params: body,
|
||||
raising=False,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"src.api.internal.gateway._resolve_gateway_sync_adapter",
|
||||
lambda decision, path: (adapter, {}),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"src.api.internal.gateway._load_gateway_auth_models",
|
||||
lambda db, auth_context: (
|
||||
SimpleNamespace(id="user-cli-sync-codex-transport-123"),
|
||||
SimpleNamespace(id="api-key-cli-sync-codex-transport-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_gateway_request_context",
|
||||
lambda **kwargs: fake_context,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"src.api.internal.gateway._select_gateway_direct_candidate",
|
||||
AsyncMock(return_value=fake_candidate),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"src.services.proxy_node.resolver.resolve_proxy_info_async",
|
||||
AsyncMock(return_value=None),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"src.services.proxy_node.resolver.resolve_delegate_config_async",
|
||||
AsyncMock(return_value=None),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"src.services.proxy_node.resolver.build_proxy_url_async",
|
||||
AsyncMock(return_value=None),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"src.services.proxy_node.resolver.get_system_proxy_config_async",
|
||||
AsyncMock(return_value=None),
|
||||
)
|
||||
|
||||
decision = classify_gateway_route("POST", "/v1/responses")
|
||||
auth_context = GatewayAuthContext(
|
||||
user_id="user-cli-sync-codex-transport-123",
|
||||
api_key_id="api-key-cli-sync-codex-transport-123",
|
||||
access_allowed=True,
|
||||
)
|
||||
payload = GatewayExecuteRequest(
|
||||
method="POST",
|
||||
path="/v1/responses",
|
||||
headers={"content-type": "application/json", "authorization": "Bearer test-key"},
|
||||
body_json={"model": "gpt-5", "input": "hello"},
|
||||
auth_context=auth_context,
|
||||
)
|
||||
|
||||
result = await _build_openai_cli_sync_plan(
|
||||
request=SimpleNamespace(),
|
||||
payload=payload,
|
||||
db=object(),
|
||||
auth_context=auth_context,
|
||||
decision=decision,
|
||||
)
|
||||
|
||||
assert result is None
|
||||
|
||||
@@ -75,6 +75,14 @@ def _wait_until(predicate: Any, *, timeout: float = 1.0, interval: float = 0.01)
|
||||
assert predicate()
|
||||
|
||||
|
||||
def _make_legacy_test_client(app: FastAPI) -> TestClient:
|
||||
return TestClient(
|
||||
app,
|
||||
base_url="http://127.0.0.1",
|
||||
headers={"x-aether-legacy-internal-gateway": "true"},
|
||||
)
|
||||
|
||||
|
||||
def test_finalize_sync_route_finalizes_openai_video_create_response(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
@@ -96,7 +104,7 @@ def test_finalize_sync_route_finalizes_openai_video_create_response(
|
||||
finalize_mock,
|
||||
)
|
||||
|
||||
client = TestClient(app, base_url="http://127.0.0.1")
|
||||
client = _make_legacy_test_client(app)
|
||||
response = client.post(
|
||||
"/api/internal/gateway/finalize-sync",
|
||||
json={
|
||||
@@ -144,7 +152,7 @@ def test_finalize_sync_route_finalizes_openai_chat_response(
|
||||
finalize_mock,
|
||||
)
|
||||
|
||||
client = TestClient(app, base_url="http://127.0.0.1")
|
||||
client = _make_legacy_test_client(app)
|
||||
response = client.post(
|
||||
"/api/internal/gateway/finalize-sync",
|
||||
json={
|
||||
@@ -193,7 +201,7 @@ def test_finalize_sync_route_finalizes_openai_cli_response(
|
||||
finalize_mock,
|
||||
)
|
||||
|
||||
client = TestClient(app, base_url="http://127.0.0.1")
|
||||
client = _make_legacy_test_client(app)
|
||||
response = client.post(
|
||||
"/api/internal/gateway/finalize-sync",
|
||||
json={
|
||||
@@ -753,7 +761,7 @@ def test_finalize_sync_route_uses_chat_fast_path_without_db_session(
|
||||
lambda: (_ for _ in ()).throw(AssertionError("create_session should not be called")),
|
||||
)
|
||||
|
||||
client = TestClient(app, base_url="http://127.0.0.1")
|
||||
client = _make_legacy_test_client(app)
|
||||
response = client.post(
|
||||
"/api/internal/gateway/finalize-sync",
|
||||
json={
|
||||
@@ -802,7 +810,7 @@ def test_finalize_sync_route_uses_cli_fast_path_without_db_session(
|
||||
lambda: (_ for _ in ()).throw(AssertionError("create_session should not be called")),
|
||||
)
|
||||
|
||||
client = TestClient(app, base_url="http://127.0.0.1")
|
||||
client = _make_legacy_test_client(app)
|
||||
response = client.post(
|
||||
"/api/internal/gateway/finalize-sync",
|
||||
json={
|
||||
@@ -847,7 +855,7 @@ def test_finalize_sync_route_finalizes_openai_video_remix_response(
|
||||
finalize_mock,
|
||||
)
|
||||
|
||||
client = TestClient(app, base_url="http://127.0.0.1")
|
||||
client = _make_legacy_test_client(app)
|
||||
response = client.post(
|
||||
"/api/internal/gateway/finalize-sync",
|
||||
json={
|
||||
@@ -896,7 +904,7 @@ def test_finalize_sync_route_finalizes_gemini_video_create_response(
|
||||
finalize_mock,
|
||||
)
|
||||
|
||||
client = TestClient(app, base_url="http://127.0.0.1")
|
||||
client = _make_legacy_test_client(app)
|
||||
response = client.post(
|
||||
"/api/internal/gateway/finalize-sync",
|
||||
json={
|
||||
@@ -945,7 +953,7 @@ def test_finalize_sync_route_finalizes_openai_video_delete_response(
|
||||
finalize_mock,
|
||||
)
|
||||
|
||||
client = TestClient(app, base_url="http://127.0.0.1")
|
||||
client = _make_legacy_test_client(app)
|
||||
response = client.post(
|
||||
"/api/internal/gateway/finalize-sync",
|
||||
json={
|
||||
@@ -984,7 +992,7 @@ def test_finalize_sync_route_finalizes_openai_video_cancel_response(
|
||||
finalize_mock,
|
||||
)
|
||||
|
||||
client = TestClient(app, base_url="http://127.0.0.1")
|
||||
client = _make_legacy_test_client(app)
|
||||
response = client.post(
|
||||
"/api/internal/gateway/finalize-sync",
|
||||
json={
|
||||
@@ -1019,7 +1027,7 @@ def test_finalize_sync_route_finalizes_gemini_video_cancel_response(
|
||||
finalize_mock,
|
||||
)
|
||||
|
||||
client = TestClient(app, base_url="http://127.0.0.1")
|
||||
client = _make_legacy_test_client(app)
|
||||
response = client.post(
|
||||
"/api/internal/gateway/finalize-sync",
|
||||
json={
|
||||
@@ -1040,3 +1048,278 @@ def test_finalize_sync_route_finalizes_gemini_video_cancel_response(
|
||||
assert response.headers[CONTROL_EXECUTED_HEADER] == "true"
|
||||
assert response.json() == {}
|
||||
finalize_mock.assert_awaited_once()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_finalize_gateway_openai_video_create_sync_reuses_rust_owned_task(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
from src.api.internal import gateway as gateway_module
|
||||
|
||||
background_mock = AsyncMock(return_value=None)
|
||||
begin_pending_usage = MagicMock()
|
||||
|
||||
class FakeQuery:
|
||||
def __init__(self, value: Any) -> None:
|
||||
self._value = value
|
||||
|
||||
def filter(self, *args: Any, **kwargs: Any) -> "FakeQuery":
|
||||
return self
|
||||
|
||||
def first(self) -> Any:
|
||||
return self._value
|
||||
|
||||
class FakeDB:
|
||||
def __init__(self) -> None:
|
||||
self._mapping = {
|
||||
"User": SimpleNamespace(id="user-123"),
|
||||
"ApiKey": SimpleNamespace(id="key-123"),
|
||||
"VideoTask": SimpleNamespace(id="task-local-123"),
|
||||
}
|
||||
|
||||
def query(self, model: Any) -> FakeQuery:
|
||||
return FakeQuery(self._mapping.get(getattr(model, "__name__", "")))
|
||||
|
||||
def add(self, obj: Any) -> None:
|
||||
raise AssertionError("legacy task creation should be skipped")
|
||||
|
||||
class FakeOpenAIVideoHandler:
|
||||
FORMAT_ID = "openai:video"
|
||||
|
||||
def __init__(self, **kwargs: Any) -> None:
|
||||
self._normalizer = SimpleNamespace(
|
||||
video_task_from_internal=lambda task: {
|
||||
"id": task["id"],
|
||||
"object": "video",
|
||||
"status": "submitted",
|
||||
}
|
||||
)
|
||||
|
||||
def _task_to_internal(self, task: Any) -> dict[str, Any]:
|
||||
return {"id": task.id}
|
||||
|
||||
monkeypatch.setattr(
|
||||
"src.api.handlers.openai.video_handler.OpenAIVideoHandler",
|
||||
FakeOpenAIVideoHandler,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"src.api.internal.gateway._run_gateway_video_finalize_submitted_background",
|
||||
background_mock,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"src.services.usage.service.UsageService.begin_pending_usage",
|
||||
begin_pending_usage,
|
||||
)
|
||||
|
||||
payload = GatewaySyncReportRequest(
|
||||
trace_id="trace-openai-video-rust-owner-123",
|
||||
report_kind="openai_video_create_sync_success",
|
||||
report_context={
|
||||
"user_id": "user-123",
|
||||
"api_key_id": "key-123",
|
||||
"provider_id": "provider-123",
|
||||
"endpoint_id": "endpoint-123",
|
||||
"key_id": "provider-key-123",
|
||||
"request_id": "req-openai-video-rust-owner-123",
|
||||
"local_task_id": "task-local-123",
|
||||
"provider_name": "openai",
|
||||
"provider_api_format": "openai:video",
|
||||
"rust_video_task_persisted": True,
|
||||
},
|
||||
status_code=200,
|
||||
headers={"content-type": "application/json"},
|
||||
body_json={"id": "ext-video-123", "status": "submitted"},
|
||||
telemetry={"elapsed_ms": 42},
|
||||
)
|
||||
|
||||
response = await gateway_module._finalize_gateway_openai_video_create_sync(
|
||||
payload,
|
||||
db=FakeDB(),
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert json.loads(response.body) == {
|
||||
"id": "task-local-123",
|
||||
"object": "video",
|
||||
"status": "submitted",
|
||||
}
|
||||
begin_pending_usage.assert_not_called()
|
||||
background_mock.assert_awaited_once()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_finalize_gateway_openai_video_remix_sync_reuses_rust_owned_task(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
from src.api.internal import gateway as gateway_module
|
||||
|
||||
class FakeQuery:
|
||||
def __init__(self, value: Any) -> None:
|
||||
self._value = value
|
||||
|
||||
def filter(self, *args: Any, **kwargs: Any) -> "FakeQuery":
|
||||
return self
|
||||
|
||||
def first(self) -> Any:
|
||||
return self._value
|
||||
|
||||
class FakeDB:
|
||||
def __init__(self) -> None:
|
||||
self._mapping = {
|
||||
"User": SimpleNamespace(id="user-123"),
|
||||
"ApiKey": SimpleNamespace(id="key-123"),
|
||||
"VideoTask": SimpleNamespace(id="task-local-remix-123"),
|
||||
}
|
||||
|
||||
def query(self, model: Any) -> FakeQuery:
|
||||
return FakeQuery(self._mapping.get(getattr(model, "__name__", "")))
|
||||
|
||||
def add(self, obj: Any) -> None:
|
||||
raise AssertionError("legacy remix task creation should be skipped")
|
||||
|
||||
class FakeOpenAIVideoHandler:
|
||||
def __init__(self, **kwargs: Any) -> None:
|
||||
self._normalizer = SimpleNamespace(
|
||||
video_task_from_internal=lambda task: {
|
||||
"id": task["id"],
|
||||
"object": "video",
|
||||
"status": "submitted",
|
||||
}
|
||||
)
|
||||
|
||||
def _task_to_internal(self, task: Any) -> dict[str, Any]:
|
||||
return {"id": task.id}
|
||||
|
||||
def _get_task(self, task_id: str) -> Any:
|
||||
raise AssertionError("legacy remix source lookup should be skipped")
|
||||
|
||||
monkeypatch.setattr(
|
||||
"src.api.handlers.openai.video_handler.OpenAIVideoHandler",
|
||||
FakeOpenAIVideoHandler,
|
||||
)
|
||||
|
||||
payload = GatewaySyncReportRequest(
|
||||
trace_id="trace-openai-video-remix-rust-owner-123",
|
||||
report_kind="openai_video_remix_sync_success",
|
||||
report_context={
|
||||
"user_id": "user-123",
|
||||
"api_key_id": "key-123",
|
||||
"task_id": "task-source-123",
|
||||
"request_id": "req-openai-video-remix-rust-owner-123",
|
||||
"local_task_id": "task-local-remix-123",
|
||||
"rust_video_task_persisted": True,
|
||||
},
|
||||
status_code=200,
|
||||
headers={"content-type": "application/json"},
|
||||
body_json={"id": "ext-remix-task-123", "status": "submitted"},
|
||||
)
|
||||
|
||||
response = await gateway_module._finalize_gateway_openai_video_remix_sync(
|
||||
payload,
|
||||
db=FakeDB(),
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert json.loads(response.body) == {
|
||||
"id": "task-local-remix-123",
|
||||
"object": "video",
|
||||
"status": "submitted",
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_finalize_gateway_gemini_video_create_sync_reuses_rust_owned_task(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
from src.api.internal import gateway as gateway_module
|
||||
|
||||
background_mock = AsyncMock(return_value=None)
|
||||
begin_pending_usage = MagicMock()
|
||||
|
||||
class FakeQuery:
|
||||
def __init__(self, value: Any) -> None:
|
||||
self._value = value
|
||||
|
||||
def filter(self, *args: Any, **kwargs: Any) -> "FakeQuery":
|
||||
return self
|
||||
|
||||
def first(self) -> Any:
|
||||
return self._value
|
||||
|
||||
class FakeDB:
|
||||
def __init__(self) -> None:
|
||||
self._mapping = {
|
||||
"User": SimpleNamespace(id="user-123"),
|
||||
"ApiKey": SimpleNamespace(id="key-123"),
|
||||
"VideoTask": SimpleNamespace(short_id="short12345678"),
|
||||
}
|
||||
|
||||
def query(self, model: Any) -> FakeQuery:
|
||||
return FakeQuery(self._mapping.get(getattr(model, "__name__", "")))
|
||||
|
||||
def add(self, obj: Any) -> None:
|
||||
raise AssertionError("legacy gemini task creation should be skipped")
|
||||
|
||||
class FakeGeminiVeoHandler:
|
||||
FORMAT_ID = "gemini:video"
|
||||
|
||||
def __init__(self, **kwargs: Any) -> None:
|
||||
self._normalizer = SimpleNamespace(
|
||||
video_task_from_internal=lambda task: {
|
||||
"name": f"models/veo-3/operations/{task['id']}",
|
||||
"done": False,
|
||||
"metadata": {},
|
||||
}
|
||||
)
|
||||
|
||||
def _task_to_internal(self, task: Any) -> dict[str, Any]:
|
||||
return {"id": task.short_id}
|
||||
|
||||
monkeypatch.setattr(
|
||||
"src.api.handlers.gemini.video_handler.GeminiVeoHandler",
|
||||
FakeGeminiVeoHandler,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"src.api.internal.gateway._run_gateway_video_finalize_submitted_background",
|
||||
background_mock,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"src.services.usage.service.UsageService.begin_pending_usage",
|
||||
begin_pending_usage,
|
||||
)
|
||||
|
||||
payload = GatewaySyncReportRequest(
|
||||
trace_id="trace-gemini-video-rust-owner-123",
|
||||
report_kind="gemini_video_create_sync_success",
|
||||
report_context={
|
||||
"user_id": "user-123",
|
||||
"api_key_id": "key-123",
|
||||
"provider_id": "provider-123",
|
||||
"endpoint_id": "endpoint-123",
|
||||
"key_id": "provider-key-123",
|
||||
"request_id": "req-gemini-video-rust-owner-123",
|
||||
"model": "veo-3",
|
||||
"local_short_id": "short12345678",
|
||||
"provider_name": "gemini",
|
||||
"provider_api_format": "gemini:video",
|
||||
"rust_video_task_persisted": True,
|
||||
},
|
||||
status_code=200,
|
||||
headers={"content-type": "application/json"},
|
||||
body_json={"name": "operations/ext-video-123"},
|
||||
telemetry={"elapsed_ms": 48},
|
||||
)
|
||||
|
||||
response = await gateway_module._finalize_gateway_gemini_video_create_sync(
|
||||
payload,
|
||||
db=FakeDB(),
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert json.loads(response.body) == {
|
||||
"name": "models/veo-3/operations/short12345678",
|
||||
"done": False,
|
||||
"metadata": {},
|
||||
}
|
||||
begin_pending_usage.assert_not_called()
|
||||
background_mock.assert_awaited_once()
|
||||
|
||||
@@ -75,6 +75,14 @@ def _wait_until(predicate: Any, *, timeout: float = 1.0, interval: float = 0.01)
|
||||
assert predicate()
|
||||
|
||||
|
||||
def _make_legacy_test_client(app: FastAPI) -> TestClient:
|
||||
return TestClient(
|
||||
app,
|
||||
base_url="http://127.0.0.1",
|
||||
headers={"x-aether-legacy-internal-gateway": "true"},
|
||||
)
|
||||
|
||||
|
||||
def test_report_sync_route_applies_gemini_files_mapping(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
app = FastAPI()
|
||||
app.include_router(router)
|
||||
@@ -91,7 +99,7 @@ def test_report_sync_route_applies_gemini_files_mapping(monkeypatch: pytest.Monk
|
||||
_fake_store_mapping,
|
||||
)
|
||||
|
||||
client = TestClient(app, base_url="http://127.0.0.1")
|
||||
client = _make_legacy_test_client(app)
|
||||
response = client.post(
|
||||
"/api/internal/gateway/report-sync",
|
||||
json={
|
||||
@@ -138,7 +146,7 @@ def test_report_sync_route_applies_gemini_files_delete_mapping(
|
||||
_fake_delete_mapping,
|
||||
)
|
||||
|
||||
client = TestClient(app, base_url="http://127.0.0.1")
|
||||
client = _make_legacy_test_client(app)
|
||||
response = client.post(
|
||||
"/api/internal/gateway/report-sync",
|
||||
json={
|
||||
@@ -172,7 +180,7 @@ def test_report_sync_route_uses_lazy_session(
|
||||
lambda: (_ for _ in ()).throw(AssertionError("create_session should not be called")),
|
||||
)
|
||||
|
||||
client = TestClient(app, base_url="http://127.0.0.1")
|
||||
client = _make_legacy_test_client(app)
|
||||
response = client.post(
|
||||
"/api/internal/gateway/report-sync",
|
||||
json={
|
||||
@@ -213,7 +221,7 @@ def test_report_sync_route_runs_video_create_success_inline(
|
||||
lambda app_obj: ("db-inline-123", cleanup_mock),
|
||||
)
|
||||
|
||||
client = TestClient(app, base_url="http://127.0.0.1")
|
||||
client = _make_legacy_test_client(app)
|
||||
response = client.post(
|
||||
"/api/internal/gateway/report-sync",
|
||||
json={
|
||||
|
||||
@@ -75,6 +75,14 @@ def _wait_until(predicate: Any, *, timeout: float = 1.0, interval: float = 0.01)
|
||||
assert predicate()
|
||||
|
||||
|
||||
def _make_legacy_test_client(app: FastAPI) -> TestClient:
|
||||
return TestClient(
|
||||
app,
|
||||
base_url="http://127.0.0.1",
|
||||
headers={"x-aether-legacy-internal-gateway": "true"},
|
||||
)
|
||||
|
||||
|
||||
def test_report_stream_route_uses_lazy_session(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
@@ -91,7 +99,7 @@ def test_report_stream_route_uses_lazy_session(
|
||||
lambda: (_ for _ in ()).throw(AssertionError("create_session should not be called")),
|
||||
)
|
||||
|
||||
client = TestClient(app, base_url="http://127.0.0.1")
|
||||
client = _make_legacy_test_client(app)
|
||||
response = client.post(
|
||||
"/api/internal/gateway/report-stream",
|
||||
json={
|
||||
@@ -170,7 +178,7 @@ def test_report_stream_route_records_openai_chat_stream_success(
|
||||
record_mock,
|
||||
)
|
||||
|
||||
client = TestClient(app, base_url="http://127.0.0.1")
|
||||
client = _make_legacy_test_client(app)
|
||||
response = client.post(
|
||||
"/api/internal/gateway/report-stream",
|
||||
json={
|
||||
@@ -203,7 +211,7 @@ def test_report_stream_route_records_claude_chat_stream_success(
|
||||
record_mock,
|
||||
)
|
||||
|
||||
client = TestClient(app, base_url="http://127.0.0.1")
|
||||
client = _make_legacy_test_client(app)
|
||||
response = client.post(
|
||||
"/api/internal/gateway/report-stream",
|
||||
json={
|
||||
@@ -240,7 +248,7 @@ def test_report_stream_route_records_gemini_chat_stream_success(
|
||||
record_mock,
|
||||
)
|
||||
|
||||
client = TestClient(app, base_url="http://127.0.0.1")
|
||||
client = _make_legacy_test_client(app)
|
||||
response = client.post(
|
||||
"/api/internal/gateway/report-stream",
|
||||
json={
|
||||
@@ -277,7 +285,7 @@ def test_report_stream_route_records_openai_cli_stream_success(
|
||||
record_mock,
|
||||
)
|
||||
|
||||
client = TestClient(app, base_url="http://127.0.0.1")
|
||||
client = _make_legacy_test_client(app)
|
||||
response = client.post(
|
||||
"/api/internal/gateway/report-stream",
|
||||
json={
|
||||
@@ -316,7 +324,7 @@ def test_report_stream_route_records_claude_cli_stream_success(
|
||||
record_mock,
|
||||
)
|
||||
|
||||
client = TestClient(app, base_url="http://127.0.0.1")
|
||||
client = _make_legacy_test_client(app)
|
||||
response = client.post(
|
||||
"/api/internal/gateway/report-stream",
|
||||
json={
|
||||
@@ -355,7 +363,7 @@ def test_report_stream_route_records_gemini_cli_stream_success(
|
||||
record_mock,
|
||||
)
|
||||
|
||||
client = TestClient(app, base_url="http://127.0.0.1")
|
||||
client = _make_legacy_test_client(app)
|
||||
response = client.post(
|
||||
"/api/internal/gateway/report-stream",
|
||||
json={
|
||||
|
||||
@@ -75,6 +75,14 @@ def _wait_until(predicate: Any, *, timeout: float = 1.0, interval: float = 0.01)
|
||||
assert predicate()
|
||||
|
||||
|
||||
def _make_legacy_test_client(app: FastAPI) -> TestClient:
|
||||
return TestClient(
|
||||
app,
|
||||
base_url="http://127.0.0.1",
|
||||
headers={"x-aether-legacy-internal-gateway": "true"},
|
||||
)
|
||||
|
||||
|
||||
def test_report_sync_route_records_openai_chat_sync_success(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
@@ -88,7 +96,7 @@ def test_report_sync_route_records_openai_chat_sync_success(
|
||||
record_mock,
|
||||
)
|
||||
|
||||
client = TestClient(app, base_url="http://127.0.0.1")
|
||||
client = _make_legacy_test_client(app)
|
||||
response = client.post(
|
||||
"/api/internal/gateway/report-sync",
|
||||
json={
|
||||
@@ -125,7 +133,7 @@ def test_report_sync_route_records_openai_cli_sync_success(
|
||||
record_mock,
|
||||
)
|
||||
|
||||
client = TestClient(app, base_url="http://127.0.0.1")
|
||||
client = _make_legacy_test_client(app)
|
||||
response = client.post(
|
||||
"/api/internal/gateway/report-sync",
|
||||
json={
|
||||
@@ -166,7 +174,7 @@ def test_report_sync_route_records_claude_cli_sync_success(
|
||||
record_mock,
|
||||
)
|
||||
|
||||
client = TestClient(app, base_url="http://127.0.0.1")
|
||||
client = _make_legacy_test_client(app)
|
||||
response = client.post(
|
||||
"/api/internal/gateway/report-sync",
|
||||
json={
|
||||
@@ -206,7 +214,7 @@ def test_report_sync_route_records_gemini_cli_sync_success(
|
||||
record_mock,
|
||||
)
|
||||
|
||||
client = TestClient(app, base_url="http://127.0.0.1")
|
||||
client = _make_legacy_test_client(app)
|
||||
response = client.post(
|
||||
"/api/internal/gateway/report-sync",
|
||||
json={
|
||||
@@ -254,7 +262,7 @@ def test_report_sync_route_records_video_sync_success_variants(
|
||||
record_mock = AsyncMock(return_value=None)
|
||||
monkeypatch.setattr(f"src.api.internal.gateway.{recorder_attr}", record_mock)
|
||||
|
||||
client = TestClient(app, base_url="http://127.0.0.1")
|
||||
client = _make_legacy_test_client(app)
|
||||
response = client.post(
|
||||
"/api/internal/gateway/report-sync",
|
||||
json={
|
||||
@@ -335,7 +343,7 @@ def test_report_sync_route_records_claude_chat_sync_success(
|
||||
record_mock,
|
||||
)
|
||||
|
||||
client = TestClient(app, base_url="http://127.0.0.1")
|
||||
client = _make_legacy_test_client(app)
|
||||
response = client.post(
|
||||
"/api/internal/gateway/report-sync",
|
||||
json={
|
||||
@@ -375,7 +383,7 @@ def test_report_sync_route_records_gemini_chat_sync_success(
|
||||
record_mock,
|
||||
)
|
||||
|
||||
client = TestClient(app, base_url="http://127.0.0.1")
|
||||
client = _make_legacy_test_client(app)
|
||||
response = client.post(
|
||||
"/api/internal/gateway/report-sync",
|
||||
json={
|
||||
|
||||
@@ -273,3 +273,81 @@ def test_record_gateway_direct_candidate_graph_uses_selected_pool_key_index() ->
|
||||
assert rows[0].status == "unused"
|
||||
assert rows[1].status == "pending"
|
||||
assert getattr(candidate, "request_candidate_id") == rows[1].id
|
||||
|
||||
|
||||
def test_record_gateway_direct_candidate_graph_is_idempotent_for_same_request_id() -> None:
|
||||
engine = create_engine("sqlite:///:memory:")
|
||||
Base.metadata.create_all(engine, tables=[RequestCandidate.__table__])
|
||||
SessionLocal = sessionmaker(bind=engine)
|
||||
|
||||
with SessionLocal() as db:
|
||||
resolver = CandidateResolver(db=db, cache_scheduler=SimpleNamespace())
|
||||
user_api_key = SimpleNamespace(
|
||||
id="api-key-direct-idempotent-123",
|
||||
name="client-key",
|
||||
user_id="user-direct-idempotent-123",
|
||||
user=SimpleNamespace(id="user-direct-idempotent-123", username="direct-user"),
|
||||
)
|
||||
request_id = "req-direct-candidate-idempotent-123"
|
||||
first_candidates = [
|
||||
ProviderCandidate(
|
||||
provider=SimpleNamespace(id="provider-selected-123", name="openai", max_retries=1),
|
||||
endpoint=SimpleNamespace(id="endpoint-selected-123"),
|
||||
key=SimpleNamespace(id="key-selected-123"),
|
||||
provider_api_format="openai:cli",
|
||||
),
|
||||
ProviderCandidate(
|
||||
provider=SimpleNamespace(id="provider-unused-123", name="openai", max_retries=1),
|
||||
endpoint=SimpleNamespace(id="endpoint-unused-123"),
|
||||
key=SimpleNamespace(id="key-unused-123"),
|
||||
provider_api_format="openai:cli",
|
||||
),
|
||||
]
|
||||
|
||||
_record_gateway_direct_candidate_graph(
|
||||
db=db,
|
||||
candidate_resolver=resolver,
|
||||
candidates=first_candidates,
|
||||
request_id=request_id,
|
||||
user_api_key=user_api_key,
|
||||
required_capabilities=None,
|
||||
selected_candidate_index=0,
|
||||
)
|
||||
first_selected_record_id = getattr(first_candidates[0], "request_candidate_id")
|
||||
|
||||
second_candidates = [
|
||||
ProviderCandidate(
|
||||
provider=SimpleNamespace(id="provider-selected-123", name="openai", max_retries=1),
|
||||
endpoint=SimpleNamespace(id="endpoint-selected-123"),
|
||||
key=SimpleNamespace(id="key-selected-123"),
|
||||
provider_api_format="openai:cli",
|
||||
),
|
||||
ProviderCandidate(
|
||||
provider=SimpleNamespace(id="provider-unused-123", name="openai", max_retries=1),
|
||||
endpoint=SimpleNamespace(id="endpoint-unused-123"),
|
||||
key=SimpleNamespace(id="key-unused-123"),
|
||||
provider_api_format="openai:cli",
|
||||
),
|
||||
]
|
||||
|
||||
_record_gateway_direct_candidate_graph(
|
||||
db=db,
|
||||
candidate_resolver=resolver,
|
||||
candidates=second_candidates,
|
||||
request_id=request_id,
|
||||
user_api_key=user_api_key,
|
||||
required_capabilities=None,
|
||||
selected_candidate_index=0,
|
||||
)
|
||||
|
||||
rows = (
|
||||
db.query(RequestCandidate)
|
||||
.filter(RequestCandidate.request_id == request_id)
|
||||
.order_by(RequestCandidate.candidate_index, RequestCandidate.retry_index)
|
||||
.all()
|
||||
)
|
||||
|
||||
assert len(rows) == 2
|
||||
assert rows[0].status == "pending"
|
||||
assert rows[1].status == "unused"
|
||||
assert getattr(second_candidates[0], "request_candidate_id") == first_selected_record_id
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def test_no_python_modules_outside_internal_gateway_reference_internal_gateway_urls() -> None:
|
||||
repo_root = Path(__file__).resolve().parents[3]
|
||||
scan_roots = [repo_root / "src" / "api", repo_root / "src" / "services"]
|
||||
needle = "/api/internal/gateway"
|
||||
offenders: list[str] = []
|
||||
|
||||
for root in scan_roots:
|
||||
for path in root.rglob("*.py"):
|
||||
rel = path.relative_to(repo_root).as_posix()
|
||||
if rel.startswith("src/api/internal/"):
|
||||
continue
|
||||
text = path.read_text(encoding="utf-8")
|
||||
if needle in text:
|
||||
offenders.append(rel)
|
||||
|
||||
assert offenders == []
|
||||
@@ -75,6 +75,29 @@ def _wait_until(predicate: Any, *, timeout: float = 1.0, interval: float = 0.01)
|
||||
assert predicate()
|
||||
|
||||
|
||||
def _make_legacy_test_client(app: FastAPI) -> TestClient:
|
||||
return TestClient(
|
||||
app,
|
||||
base_url="http://127.0.0.1",
|
||||
headers={"x-aether-legacy-internal-gateway": "true"},
|
||||
)
|
||||
|
||||
|
||||
def _make_internal_test_client(monkeypatch: pytest.MonkeyPatch) -> TestClient:
|
||||
monkeypatch.setattr("src.api.internal.gateway.ensure_loopback", lambda request: None)
|
||||
app = FastAPI()
|
||||
app.include_router(router)
|
||||
app.dependency_overrides[get_db] = lambda: object()
|
||||
return TestClient(app, base_url="http://127.0.0.1")
|
||||
|
||||
|
||||
def _assert_legacy_guard_response(response) -> None:
|
||||
assert response.status_code == 410
|
||||
assert response.json() == {
|
||||
"detail": "legacy internal gateway route removed; use public proxy"
|
||||
}
|
||||
|
||||
|
||||
def test_build_gateway_sync_telemetry_writer_uses_queue_writer_when_enabled(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
@@ -314,7 +337,7 @@ def test_auth_context_route_returns_openai_bearer_auth_context(
|
||||
),
|
||||
)
|
||||
|
||||
client = TestClient(app, base_url="http://127.0.0.1")
|
||||
client = _make_legacy_test_client(app)
|
||||
response = client.post(
|
||||
"/api/internal/gateway/auth-context",
|
||||
json={
|
||||
@@ -371,14 +394,17 @@ def test_execute_sync_route_returns_controlled_response(monkeypatch: pytest.Monk
|
||||
)
|
||||
monkeypatch.setattr("src.api.internal.gateway.get_pipeline", lambda: fake_pipeline)
|
||||
|
||||
client = TestClient(app, base_url="http://127.0.0.1")
|
||||
client = _make_legacy_test_client(app)
|
||||
response = client.post(
|
||||
"/api/internal/gateway/execute-sync",
|
||||
json={
|
||||
"trace_id": "trace-sync-123",
|
||||
"method": "POST",
|
||||
"path": "/v1/chat/completions",
|
||||
"headers": {"user-agent": "pytest"},
|
||||
"headers": {
|
||||
"user-agent": "pytest",
|
||||
"x-aether-control-execute-fallback": "true",
|
||||
},
|
||||
"body_json": {"model": "gpt-5", "messages": []},
|
||||
"auth_context": {
|
||||
"user_id": "user-123",
|
||||
@@ -438,7 +464,7 @@ def test_execute_sync_route_resolves_auth_context_when_missing(
|
||||
)
|
||||
monkeypatch.setattr("src.api.internal.gateway.get_pipeline", lambda: fake_pipeline)
|
||||
|
||||
client = TestClient(app, base_url="http://127.0.0.1")
|
||||
client = _make_legacy_test_client(app)
|
||||
response = client.post(
|
||||
"/api/internal/gateway/execute-sync",
|
||||
json={
|
||||
@@ -448,6 +474,7 @@ def test_execute_sync_route_resolves_auth_context_when_missing(
|
||||
"headers": {
|
||||
"user-agent": "pytest",
|
||||
"authorization": "Bearer client-key",
|
||||
"x-aether-control-execute-fallback": "true",
|
||||
},
|
||||
"body_json": {"model": "gpt-5", "messages": []},
|
||||
},
|
||||
@@ -465,14 +492,17 @@ def test_execute_sync_route_falls_back_for_stream_payload() -> None:
|
||||
monkeypatch = pytest.MonkeyPatch()
|
||||
monkeypatch.setattr("src.api.internal.gateway.ensure_loopback", lambda request: None)
|
||||
|
||||
client = TestClient(app, base_url="http://127.0.0.1")
|
||||
client = _make_legacy_test_client(app)
|
||||
response = client.post(
|
||||
"/api/internal/gateway/execute-sync",
|
||||
json={
|
||||
"trace_id": "trace-stream-123",
|
||||
"method": "POST",
|
||||
"path": "/v1/chat/completions",
|
||||
"headers": {"user-agent": "pytest"},
|
||||
"headers": {
|
||||
"user-agent": "pytest",
|
||||
"x-aether-control-execute-fallback": "true",
|
||||
},
|
||||
"body_json": {"model": "gpt-5", "messages": [], "stream": True},
|
||||
"auth_context": {
|
||||
"user_id": "user-123",
|
||||
@@ -522,14 +552,17 @@ def test_execute_stream_route_returns_controlled_stream(monkeypatch: pytest.Monk
|
||||
)
|
||||
monkeypatch.setattr("src.api.internal.gateway.get_pipeline", lambda: fake_pipeline)
|
||||
|
||||
client = TestClient(app, base_url="http://127.0.0.1")
|
||||
client = _make_legacy_test_client(app)
|
||||
response = client.post(
|
||||
"/api/internal/gateway/execute-stream",
|
||||
json={
|
||||
"trace_id": "trace-stream-123",
|
||||
"method": "POST",
|
||||
"path": "/v1/chat/completions",
|
||||
"headers": {"user-agent": "pytest"},
|
||||
"headers": {
|
||||
"user-agent": "pytest",
|
||||
"x-aether-control-execute-fallback": "true",
|
||||
},
|
||||
"body_json": {"model": "gpt-5", "messages": [], "stream": True},
|
||||
"auth_context": {
|
||||
"user_id": "user-123",
|
||||
@@ -567,7 +600,7 @@ def test_plan_sync_route_executes_control_when_direct_plan_missing(
|
||||
execute_mock,
|
||||
)
|
||||
|
||||
client = TestClient(app, base_url="http://127.0.0.1")
|
||||
client = _make_legacy_test_client(app)
|
||||
response = client.post(
|
||||
"/api/internal/gateway/plan-sync",
|
||||
json={
|
||||
@@ -608,7 +641,7 @@ def test_plan_stream_route_executes_control_when_direct_plan_missing(
|
||||
execute_mock,
|
||||
)
|
||||
|
||||
client = TestClient(app, base_url="http://127.0.0.1")
|
||||
client = _make_legacy_test_client(app)
|
||||
response = client.post(
|
||||
"/api/internal/gateway/plan-stream",
|
||||
json={
|
||||
@@ -626,13 +659,77 @@ def test_plan_stream_route_executes_control_when_direct_plan_missing(
|
||||
execute_mock.assert_awaited_once()
|
||||
|
||||
|
||||
def test_plan_sync_route_requires_legacy_header(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
client = _make_internal_test_client(monkeypatch)
|
||||
response = client.post(
|
||||
"/api/internal/gateway/plan-sync",
|
||||
json={
|
||||
"trace_id": "trace-guard-plan-sync",
|
||||
"method": "POST",
|
||||
"path": "/v1/chat/completions",
|
||||
"headers": {"content-type": "application/json"},
|
||||
"body_json": {"model": "gpt-5", "messages": []},
|
||||
},
|
||||
)
|
||||
|
||||
_assert_legacy_guard_response(response)
|
||||
|
||||
|
||||
def test_plan_stream_route_requires_legacy_header(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
client = _make_internal_test_client(monkeypatch)
|
||||
response = client.post(
|
||||
"/api/internal/gateway/plan-stream",
|
||||
json={
|
||||
"trace_id": "trace-guard-plan-stream",
|
||||
"method": "POST",
|
||||
"path": "/v1/chat/completions",
|
||||
"headers": {"content-type": "application/json"},
|
||||
"body_json": {"model": "gpt-5", "messages": [], "stream": True},
|
||||
},
|
||||
)
|
||||
|
||||
_assert_legacy_guard_response(response)
|
||||
|
||||
|
||||
def test_report_sync_route_requires_legacy_header(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
client = _make_internal_test_client(monkeypatch)
|
||||
response = client.post(
|
||||
"/api/internal/gateway/report-sync",
|
||||
json={
|
||||
"trace_id": "trace-guard-report-sync",
|
||||
"report_kind": "openai_chat_sync_finalize",
|
||||
"report_context": {"user_id": "user-123", "api_key_id": "key-123"},
|
||||
"status_code": 200,
|
||||
"headers": {"content-type": "application/json"},
|
||||
},
|
||||
)
|
||||
|
||||
_assert_legacy_guard_response(response)
|
||||
|
||||
|
||||
def test_finalize_sync_route_requires_legacy_header(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
client = _make_internal_test_client(monkeypatch)
|
||||
response = client.post(
|
||||
"/api/internal/gateway/finalize-sync",
|
||||
json={
|
||||
"trace_id": "trace-guard-finalize-sync",
|
||||
"report_kind": "openai_chat_sync_finalize",
|
||||
"report_context": {"user_id": "user-123", "api_key_id": "key-123"},
|
||||
"status_code": 200,
|
||||
"headers": {"content-type": "application/json"},
|
||||
},
|
||||
)
|
||||
|
||||
_assert_legacy_guard_response(response)
|
||||
|
||||
|
||||
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")
|
||||
client = _make_legacy_test_client(app)
|
||||
response = client.post(
|
||||
"/api/internal/gateway/execute-stream",
|
||||
json={
|
||||
@@ -649,9 +746,10 @@ def test_execute_stream_route_falls_back_for_sync_payload(monkeypatch: pytest.Mo
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 409
|
||||
assert response.headers[CONTROL_ACTION_HEADER] == CONTROL_ACTION_PROXY_PUBLIC
|
||||
assert response.json() == {"action": CONTROL_ACTION_PROXY_PUBLIC}
|
||||
assert response.status_code == 410
|
||||
assert response.json() == {
|
||||
"detail": "legacy internal gateway route removed; use public proxy"
|
||||
}
|
||||
|
||||
|
||||
def test_execute_sync_route_handles_gemini_files_list(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
@@ -683,7 +781,7 @@ def test_execute_sync_route_handles_gemini_files_list(monkeypatch: pytest.Monkey
|
||||
|
||||
monkeypatch.setattr("src.api.public.gemini_files.list_files", _fake_list_files)
|
||||
|
||||
client = TestClient(app, base_url="http://127.0.0.1")
|
||||
client = _make_legacy_test_client(app)
|
||||
response = client.post(
|
||||
"/api/internal/gateway/execute-sync",
|
||||
json={
|
||||
@@ -732,7 +830,7 @@ def test_execute_sync_route_handles_gemini_files_upload_raw_body(
|
||||
|
||||
monkeypatch.setattr("src.api.public.gemini_files.upload_file", _fake_upload_file)
|
||||
|
||||
client = TestClient(app, base_url="http://127.0.0.1")
|
||||
client = _make_legacy_test_client(app)
|
||||
response = client.post(
|
||||
"/api/internal/gateway/execute-sync",
|
||||
json={
|
||||
@@ -796,7 +894,7 @@ def test_execute_sync_route_handles_openai_video_remix_with_original_request(
|
||||
lambda: SimpleNamespace(_check_user_rate_limit=AsyncMock(return_value=None)),
|
||||
)
|
||||
|
||||
client = TestClient(app, base_url="http://127.0.0.1")
|
||||
client = _make_legacy_test_client(app)
|
||||
response = client.post(
|
||||
"/api/internal/gateway/execute-sync",
|
||||
json={
|
||||
@@ -860,7 +958,7 @@ def test_plan_stream_route_returns_executor_plan_for_gemini_files_download(
|
||||
AsyncMock(return_value=fake_plan),
|
||||
)
|
||||
|
||||
client = TestClient(app, base_url="http://127.0.0.1")
|
||||
client = _make_legacy_test_client(app)
|
||||
response = client.post(
|
||||
"/api/internal/gateway/plan-stream",
|
||||
json={
|
||||
@@ -924,7 +1022,7 @@ def test_plan_stream_route_returns_executor_plan_for_openai_video_content(
|
||||
AsyncMock(return_value=fake_plan),
|
||||
)
|
||||
|
||||
client = TestClient(app, base_url="http://127.0.0.1")
|
||||
client = _make_legacy_test_client(app)
|
||||
response = client.post(
|
||||
"/api/internal/gateway/plan-stream",
|
||||
json={
|
||||
@@ -988,7 +1086,7 @@ def test_plan_sync_route_returns_executor_plan_for_gemini_files_get(
|
||||
AsyncMock(return_value=(fake_plan, {"file_key_id": "file-key-123", "user_id": "user-123"})),
|
||||
)
|
||||
|
||||
client = TestClient(app, base_url="http://127.0.0.1")
|
||||
client = _make_legacy_test_client(app)
|
||||
response = client.post(
|
||||
"/api/internal/gateway/plan-sync",
|
||||
json={
|
||||
@@ -1051,7 +1149,7 @@ def test_plan_sync_route_returns_executor_plan_for_openai_chat(
|
||||
),
|
||||
)
|
||||
|
||||
client = TestClient(app, base_url="http://127.0.0.1")
|
||||
client = _make_legacy_test_client(app)
|
||||
response = client.post(
|
||||
"/api/internal/gateway/plan-sync",
|
||||
json={
|
||||
@@ -1118,7 +1216,7 @@ def test_decision_sync_route_returns_executor_decision_for_openai_chat(
|
||||
),
|
||||
)
|
||||
|
||||
client = TestClient(app, base_url="http://127.0.0.1")
|
||||
client = _make_legacy_test_client(app)
|
||||
response = client.post(
|
||||
"/api/internal/gateway/decision-sync",
|
||||
json={
|
||||
@@ -1201,7 +1299,7 @@ def test_decision_stream_route_returns_executor_decision_for_openai_chat(
|
||||
),
|
||||
)
|
||||
|
||||
client = TestClient(app, base_url="http://127.0.0.1")
|
||||
client = _make_legacy_test_client(app)
|
||||
response = client.post(
|
||||
"/api/internal/gateway/decision-stream",
|
||||
json={
|
||||
@@ -1247,6 +1345,38 @@ def test_decision_stream_route_returns_executor_decision_for_openai_chat(
|
||||
}
|
||||
|
||||
|
||||
def test_decision_sync_route_requires_legacy_header(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
client = _make_internal_test_client(monkeypatch)
|
||||
response = client.post(
|
||||
"/api/internal/gateway/decision-sync",
|
||||
json={
|
||||
"trace_id": "trace-guard-decision-sync",
|
||||
"method": "POST",
|
||||
"path": "/v1/chat/completions",
|
||||
"headers": {"content-type": "application/json"},
|
||||
"body_json": {"model": "gpt-5", "messages": []},
|
||||
},
|
||||
)
|
||||
|
||||
_assert_legacy_guard_response(response)
|
||||
|
||||
|
||||
def test_decision_stream_route_requires_legacy_header(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
client = _make_internal_test_client(monkeypatch)
|
||||
response = client.post(
|
||||
"/api/internal/gateway/decision-stream",
|
||||
json={
|
||||
"trace_id": "trace-guard-decision-stream",
|
||||
"method": "POST",
|
||||
"path": "/v1/chat/completions",
|
||||
"headers": {"content-type": "application/json"},
|
||||
"body_json": {"model": "gpt-5", "messages": [], "stream": True},
|
||||
},
|
||||
)
|
||||
|
||||
_assert_legacy_guard_response(response)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("path", "headers", "decision_kind", "client_api_format", "builder_path", "model_name", "mapped_model", "upstream_url"),
|
||||
[
|
||||
@@ -1333,7 +1463,7 @@ def test_decision_stream_route_returns_executor_decision_for_claude_and_gemini_c
|
||||
else {"contents": [{"role": "user", "parts": [{"text": "hello"}]}]}
|
||||
)
|
||||
|
||||
client = TestClient(app, base_url="http://127.0.0.1")
|
||||
client = _make_legacy_test_client(app)
|
||||
response = client.post(
|
||||
"/api/internal/gateway/decision-stream",
|
||||
json={
|
||||
@@ -1494,7 +1624,7 @@ def test_decision_stream_route_returns_executor_decision_for_cli_variants(
|
||||
),
|
||||
)
|
||||
|
||||
client = TestClient(app, base_url="http://127.0.0.1")
|
||||
client = _make_legacy_test_client(app)
|
||||
response = client.post(
|
||||
"/api/internal/gateway/decision-stream",
|
||||
json={
|
||||
@@ -1591,7 +1721,7 @@ def test_decision_sync_route_returns_executor_decision_for_openai_cli_variants(
|
||||
),
|
||||
)
|
||||
|
||||
client = TestClient(app, base_url="http://127.0.0.1")
|
||||
client = _make_legacy_test_client(app)
|
||||
response = client.post(
|
||||
"/api/internal/gateway/decision-sync",
|
||||
json={
|
||||
@@ -1708,7 +1838,7 @@ def test_decision_sync_route_returns_executor_decision_for_claude_variants(
|
||||
),
|
||||
)
|
||||
|
||||
client = TestClient(app, base_url="http://127.0.0.1")
|
||||
client = _make_legacy_test_client(app)
|
||||
response = client.post(
|
||||
"/api/internal/gateway/decision-sync",
|
||||
json={
|
||||
@@ -1839,7 +1969,7 @@ def test_decision_sync_route_returns_executor_decision_for_gemini_variants(
|
||||
),
|
||||
)
|
||||
|
||||
client = TestClient(app, base_url="http://127.0.0.1")
|
||||
client = _make_legacy_test_client(app)
|
||||
response = client.post(
|
||||
"/api/internal/gateway/decision-sync",
|
||||
json={
|
||||
@@ -1946,7 +2076,7 @@ def test_decision_sync_route_returns_executor_decision_for_gemini_files_get(
|
||||
),
|
||||
)
|
||||
|
||||
client = TestClient(app, base_url="http://127.0.0.1")
|
||||
client = _make_legacy_test_client(app)
|
||||
response = client.post(
|
||||
"/api/internal/gateway/decision-sync",
|
||||
json={
|
||||
@@ -2097,7 +2227,7 @@ def test_decision_sync_route_returns_executor_decision_for_video_variants(
|
||||
),
|
||||
)
|
||||
|
||||
client = TestClient(app, base_url="http://127.0.0.1")
|
||||
client = _make_legacy_test_client(app)
|
||||
response = client.post(
|
||||
"/api/internal/gateway/decision-sync",
|
||||
json={
|
||||
@@ -2198,7 +2328,7 @@ def test_decision_stream_route_returns_executor_decision_for_gemini_files_downlo
|
||||
),
|
||||
)
|
||||
|
||||
client = TestClient(app, base_url="http://127.0.0.1")
|
||||
client = _make_legacy_test_client(app)
|
||||
response = client.post(
|
||||
"/api/internal/gateway/decision-stream",
|
||||
json={
|
||||
@@ -2266,7 +2396,7 @@ def test_decision_stream_route_returns_executor_decision_for_openai_video_conten
|
||||
),
|
||||
)
|
||||
|
||||
client = TestClient(app, base_url="http://127.0.0.1")
|
||||
client = _make_legacy_test_client(app)
|
||||
response = client.post(
|
||||
"/api/internal/gateway/decision-stream",
|
||||
json={
|
||||
@@ -2322,7 +2452,7 @@ def test_plan_sync_route_resolves_auth_context_when_missing(
|
||||
),
|
||||
)
|
||||
|
||||
client = TestClient(app, base_url="http://127.0.0.1")
|
||||
client = _make_legacy_test_client(app)
|
||||
response = client.post(
|
||||
"/api/internal/gateway/plan-sync",
|
||||
json={
|
||||
@@ -2384,7 +2514,7 @@ def test_plan_sync_route_returns_executor_plan_for_openai_video_create(
|
||||
),
|
||||
)
|
||||
|
||||
client = TestClient(app, base_url="http://127.0.0.1")
|
||||
client = _make_legacy_test_client(app)
|
||||
response = client.post(
|
||||
"/api/internal/gateway/plan-sync",
|
||||
json={
|
||||
@@ -2450,7 +2580,7 @@ def test_plan_sync_route_returns_executor_plan_for_openai_video_remix(
|
||||
),
|
||||
)
|
||||
|
||||
client = TestClient(app, base_url="http://127.0.0.1")
|
||||
client = _make_legacy_test_client(app)
|
||||
response = client.post(
|
||||
"/api/internal/gateway/plan-sync",
|
||||
json={
|
||||
@@ -2516,7 +2646,7 @@ def test_plan_sync_route_returns_executor_plan_for_gemini_video_create(
|
||||
),
|
||||
)
|
||||
|
||||
client = TestClient(app, base_url="http://127.0.0.1")
|
||||
client = _make_legacy_test_client(app)
|
||||
response = client.post(
|
||||
"/api/internal/gateway/plan-sync",
|
||||
json={
|
||||
@@ -2582,7 +2712,7 @@ def test_plan_sync_route_returns_executor_plan_for_openai_video_cancel(
|
||||
),
|
||||
)
|
||||
|
||||
client = TestClient(app, base_url="http://127.0.0.1")
|
||||
client = _make_legacy_test_client(app)
|
||||
response = client.post(
|
||||
"/api/internal/gateway/plan-sync",
|
||||
json={
|
||||
@@ -2646,7 +2776,7 @@ def test_plan_sync_route_returns_executor_plan_for_openai_video_delete(
|
||||
),
|
||||
)
|
||||
|
||||
client = TestClient(app, base_url="http://127.0.0.1")
|
||||
client = _make_legacy_test_client(app)
|
||||
response = client.post(
|
||||
"/api/internal/gateway/plan-sync",
|
||||
json={
|
||||
@@ -2710,7 +2840,7 @@ def test_plan_sync_route_returns_executor_plan_for_gemini_video_cancel(
|
||||
),
|
||||
)
|
||||
|
||||
client = TestClient(app, base_url="http://127.0.0.1")
|
||||
client = _make_legacy_test_client(app)
|
||||
response = client.post(
|
||||
"/api/internal/gateway/plan-sync",
|
||||
json={
|
||||
@@ -2785,7 +2915,7 @@ def test_plan_stream_route_returns_executor_plan_for_openai_chat(
|
||||
),
|
||||
)
|
||||
|
||||
client = TestClient(app, base_url="http://127.0.0.1")
|
||||
client = _make_legacy_test_client(app)
|
||||
response = client.post(
|
||||
"/api/internal/gateway/plan-stream",
|
||||
json={
|
||||
@@ -2860,7 +2990,7 @@ def test_plan_stream_route_resolves_auth_context_when_missing(
|
||||
),
|
||||
)
|
||||
|
||||
client = TestClient(app, base_url="http://127.0.0.1")
|
||||
client = _make_legacy_test_client(app)
|
||||
response = client.post(
|
||||
"/api/internal/gateway/plan-stream",
|
||||
json={
|
||||
@@ -2927,7 +3057,7 @@ def test_plan_stream_route_returns_executor_plan_for_claude_chat(
|
||||
),
|
||||
)
|
||||
|
||||
client = TestClient(app, base_url="http://127.0.0.1")
|
||||
client = _make_legacy_test_client(app)
|
||||
response = client.post(
|
||||
"/api/internal/gateway/plan-stream",
|
||||
json={
|
||||
@@ -2997,7 +3127,7 @@ def test_plan_stream_route_returns_executor_plan_for_gemini_chat(
|
||||
),
|
||||
)
|
||||
|
||||
client = TestClient(app, base_url="http://127.0.0.1")
|
||||
client = _make_legacy_test_client(app)
|
||||
response = client.post(
|
||||
"/api/internal/gateway/plan-stream",
|
||||
json={
|
||||
@@ -3067,7 +3197,7 @@ def test_plan_stream_route_returns_executor_plan_for_openai_cli(
|
||||
),
|
||||
)
|
||||
|
||||
client = TestClient(app, base_url="http://127.0.0.1")
|
||||
client = _make_legacy_test_client(app)
|
||||
response = client.post(
|
||||
"/api/internal/gateway/plan-stream",
|
||||
json={
|
||||
@@ -3143,7 +3273,7 @@ def test_plan_stream_route_returns_executor_plan_for_claude_cli(
|
||||
),
|
||||
)
|
||||
|
||||
client = TestClient(app, base_url="http://127.0.0.1")
|
||||
client = _make_legacy_test_client(app)
|
||||
response = client.post(
|
||||
"/api/internal/gateway/plan-stream",
|
||||
json={
|
||||
@@ -3218,7 +3348,7 @@ def test_plan_stream_route_returns_executor_plan_for_gemini_cli(
|
||||
),
|
||||
)
|
||||
|
||||
client = TestClient(app, base_url="http://127.0.0.1")
|
||||
client = _make_legacy_test_client(app)
|
||||
response = client.post(
|
||||
"/api/internal/gateway/plan-stream",
|
||||
json={
|
||||
@@ -3285,7 +3415,7 @@ def test_plan_sync_route_returns_executor_plan_for_openai_cli(
|
||||
),
|
||||
)
|
||||
|
||||
client = TestClient(app, base_url="http://127.0.0.1")
|
||||
client = _make_legacy_test_client(app)
|
||||
response = client.post(
|
||||
"/api/internal/gateway/plan-sync",
|
||||
json={
|
||||
@@ -3351,7 +3481,7 @@ def test_plan_sync_route_returns_executor_plan_for_openai_compact(
|
||||
),
|
||||
)
|
||||
|
||||
client = TestClient(app, base_url="http://127.0.0.1")
|
||||
client = _make_legacy_test_client(app)
|
||||
response = client.post(
|
||||
"/api/internal/gateway/plan-sync",
|
||||
json={
|
||||
@@ -3417,7 +3547,7 @@ def test_plan_sync_route_returns_executor_plan_for_claude_chat(
|
||||
),
|
||||
)
|
||||
|
||||
client = TestClient(app, base_url="http://127.0.0.1")
|
||||
client = _make_legacy_test_client(app)
|
||||
response = client.post(
|
||||
"/api/internal/gateway/plan-sync",
|
||||
json={
|
||||
@@ -3483,7 +3613,7 @@ def test_plan_sync_route_returns_executor_plan_for_gemini_chat(
|
||||
),
|
||||
)
|
||||
|
||||
client = TestClient(app, base_url="http://127.0.0.1")
|
||||
client = _make_legacy_test_client(app)
|
||||
response = client.post(
|
||||
"/api/internal/gateway/plan-sync",
|
||||
json={
|
||||
@@ -3549,7 +3679,7 @@ def test_plan_sync_route_returns_executor_plan_for_claude_cli(
|
||||
),
|
||||
)
|
||||
|
||||
client = TestClient(app, base_url="http://127.0.0.1")
|
||||
client = _make_legacy_test_client(app)
|
||||
response = client.post(
|
||||
"/api/internal/gateway/plan-sync",
|
||||
json={
|
||||
@@ -3615,7 +3745,7 @@ def test_plan_sync_route_returns_executor_plan_for_gemini_cli(
|
||||
),
|
||||
)
|
||||
|
||||
client = TestClient(app, base_url="http://127.0.0.1")
|
||||
client = _make_legacy_test_client(app)
|
||||
response = client.post(
|
||||
"/api/internal/gateway/plan-sync",
|
||||
json={
|
||||
@@ -3685,7 +3815,7 @@ def test_plan_sync_route_returns_executor_plan_for_gemini_files_list(
|
||||
AsyncMock(return_value=(fake_plan, {"file_key_id": "file-key-123", "user_id": "user-123"})),
|
||||
)
|
||||
|
||||
client = TestClient(app, base_url="http://127.0.0.1")
|
||||
client = _make_legacy_test_client(app)
|
||||
response = client.post(
|
||||
"/api/internal/gateway/plan-sync",
|
||||
json={
|
||||
@@ -3751,7 +3881,7 @@ def test_plan_sync_route_returns_executor_plan_for_gemini_files_upload(
|
||||
AsyncMock(return_value=(fake_plan, {"file_key_id": "file-key-123", "user_id": "user-123"})),
|
||||
)
|
||||
|
||||
client = TestClient(app, base_url="http://127.0.0.1")
|
||||
client = _make_legacy_test_client(app)
|
||||
response = client.post(
|
||||
"/api/internal/gateway/plan-sync",
|
||||
json={
|
||||
@@ -3821,7 +3951,7 @@ def test_plan_sync_route_returns_executor_plan_for_gemini_files_delete(
|
||||
AsyncMock(return_value=(fake_plan, {})),
|
||||
)
|
||||
|
||||
client = TestClient(app, base_url="http://127.0.0.1")
|
||||
client = _make_legacy_test_client(app)
|
||||
response = client.post(
|
||||
"/api/internal/gateway/plan-sync",
|
||||
json={
|
||||
|
||||
Reference in New Issue
Block a user