Remove legacy Python tests

This commit is contained in:
fawney19
2026-05-16 00:06:21 +08:00
parent 0e0a24862f
commit 43d891bee1
310 changed files with 0 additions and 73410 deletions
-1
View File
@@ -1 +0,0 @@
"""测试模块"""
@@ -1,235 +0,0 @@
from __future__ import annotations
from typing import Any
from unittest.mock import MagicMock
import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient
from src.database import get_db
def _normalize_state(adapter: Any) -> dict[str, Any]:
state: dict[str, Any] = {}
for key, value in dict(getattr(adapter, "__dict__", {})).items():
if hasattr(value, "model_dump"):
state[key] = value.model_dump()
elif hasattr(value, "filename"):
state[key] = {
"filename": value.filename,
"content_type": getattr(value, "content_type", None),
}
else:
state[key] = value
return state
def _build_app(
monkeypatch: pytest.MonkeyPatch,
*,
pipeline_result: Any,
) -> tuple[TestClient, list[dict[str, Any]]]:
from src.api.admin import gemini_files as mod
app = FastAPI()
app.include_router(mod.router)
app.dependency_overrides[get_db] = lambda: MagicMock()
calls: list[dict[str, Any]] = []
async def _fake_pipeline_run(
*,
adapter: Any,
http_request: object,
db: object,
mode: object,
api_format_hint: str | None = None,
path_params: dict[str, Any] | None = None,
) -> Any:
del http_request, db, api_format_hint, path_params
calls.append(
{
"adapter_type": type(adapter).__name__,
"mode": getattr(mode, "value", mode),
"adapter_state": _normalize_state(adapter),
}
)
return pipeline_result
monkeypatch.setattr(mod.pipeline, "run", _fake_pipeline_run)
return TestClient(app), calls
def test_admin_gemini_files_list_route_is_pipeline_shell(
monkeypatch: pytest.MonkeyPatch,
) -> None:
client, calls = _build_app(
monkeypatch,
pipeline_result={"items": [], "total": 0, "page": 2, "page_size": 50},
)
response = client.get(
"/api/admin/gemini-files/mappings?page=2&page_size=50&include_expired=true&search=demo"
)
assert response.status_code == 200
assert response.json() == {"items": [], "total": 0, "page": 2, "page_size": 50}
assert calls == [
{
"adapter_type": "AdminGeminiFilesListMappingsAdapter",
"mode": "admin",
"adapter_state": {
"page": 2,
"page_size": 50,
"include_expired": True,
"search": "demo",
},
}
]
def test_admin_gemini_files_stats_route_is_pipeline_shell(
monkeypatch: pytest.MonkeyPatch,
) -> None:
client, calls = _build_app(
monkeypatch,
pipeline_result={
"total_mappings": 1,
"active_mappings": 1,
"expired_mappings": 0,
"by_mime_type": {"text/plain": 1},
"capable_keys_count": 2,
},
)
response = client.get("/api/admin/gemini-files/stats")
assert response.status_code == 200
assert response.json() == {
"total_mappings": 1,
"active_mappings": 1,
"expired_mappings": 0,
"by_mime_type": {"text/plain": 1},
"capable_keys_count": 2,
}
assert calls == [
{
"adapter_type": "AdminGeminiFilesStatsAdapter",
"mode": "admin",
"adapter_state": {},
}
]
def test_admin_gemini_files_delete_mapping_route_is_pipeline_shell(
monkeypatch: pytest.MonkeyPatch,
) -> None:
client, calls = _build_app(
monkeypatch,
pipeline_result={"message": "Mapping deleted successfully", "file_name": "files/1"},
)
response = client.delete("/api/admin/gemini-files/mappings/mapping-1")
assert response.status_code == 200
assert response.json() == {
"message": "Mapping deleted successfully",
"file_name": "files/1",
}
assert calls == [
{
"adapter_type": "AdminGeminiFilesDeleteMappingAdapter",
"mode": "admin",
"adapter_state": {"mapping_id": "mapping-1"},
}
]
def test_admin_gemini_files_cleanup_route_is_pipeline_shell(
monkeypatch: pytest.MonkeyPatch,
) -> None:
client, calls = _build_app(
monkeypatch,
pipeline_result={"message": "Cleaned up 3 expired mappings", "deleted_count": 3},
)
response = client.delete("/api/admin/gemini-files/mappings")
assert response.status_code == 200
assert response.json() == {
"message": "Cleaned up 3 expired mappings",
"deleted_count": 3,
}
assert calls == [
{
"adapter_type": "AdminGeminiFilesCleanupMappingsAdapter",
"mode": "admin",
"adapter_state": {},
}
]
def test_admin_gemini_files_capable_keys_route_is_pipeline_shell(
monkeypatch: pytest.MonkeyPatch,
) -> None:
client, calls = _build_app(
monkeypatch,
pipeline_result=[{"id": "key-1", "name": "Key 1", "provider_name": "Gemini"}],
)
response = client.get("/api/admin/gemini-files/capable-keys")
assert response.status_code == 200
assert response.json() == [{"id": "key-1", "name": "Key 1", "provider_name": "Gemini"}]
assert calls == [
{
"adapter_type": "AdminGeminiFilesCapableKeysAdapter",
"mode": "admin",
"adapter_state": {},
}
]
def test_admin_gemini_files_upload_route_is_pipeline_shell(
monkeypatch: pytest.MonkeyPatch,
) -> None:
client, calls = _build_app(
monkeypatch,
pipeline_result={
"display_name": "example.txt",
"mime_type": "text/plain",
"size_bytes": 5,
"results": [],
"success_count": 0,
"fail_count": 0,
},
)
response = client.post(
"/api/admin/gemini-files/upload?key_ids=key-1,key-2",
files={"file": ("example.txt", b"hello", "text/plain")},
)
assert response.status_code == 200
assert response.json() == {
"display_name": "example.txt",
"mime_type": "text/plain",
"size_bytes": 5,
"results": [],
"success_count": 0,
"fail_count": 0,
}
assert calls == [
{
"adapter_type": "AdminGeminiFilesUploadAdapter",
"mode": "admin",
"adapter_state": {
"file": {
"filename": "example.txt",
"content_type": "text/plain",
},
"key_ids": "key-1,key-2",
},
}
]
@@ -1,164 +0,0 @@
from __future__ import annotations
from types import SimpleNamespace
from typing import Any
import pytest
@pytest.mark.asyncio
@pytest.mark.parametrize(
("route_name", "expected_adapter", "expected_state"),
[
("supported_types", "AdminProviderOAuthUnavailableAdapter", {"operation": "supported_types"}),
("start_oauth", "AdminProviderOAuthUnavailableAdapter", {"operation": "start_oauth"}),
("complete_oauth", "AdminProviderOAuthUnavailableAdapter", {"operation": "complete_oauth"}),
("refresh_oauth", "AdminProviderOAuthUnavailableAdapter", {"operation": "refresh_oauth"}),
(
"start_provider_oauth",
"AdminProviderOAuthUnavailableAdapter",
{"operation": "start_provider_oauth"},
),
(
"complete_provider_oauth",
"AdminProviderOAuthUnavailableAdapter",
{"operation": "complete_provider_oauth"},
),
(
"import_refresh_token",
"AdminProviderOAuthUnavailableAdapter",
{"operation": "import_refresh_token"},
),
(
"batch_import_oauth",
"AdminProviderOAuthUnavailableAdapter",
{"operation": "batch_import_oauth"},
),
(
"start_batch_import_oauth_task",
"AdminProviderOAuthUnavailableAdapter",
{"operation": "start_batch_import_oauth_task"},
),
(
"get_batch_import_oauth_task_status",
"AdminProviderOAuthUnavailableAdapter",
{"operation": "get_batch_import_oauth_task_status"},
),
(
"device_authorize",
"AdminProviderOAuthUnavailableAdapter",
{"operation": "device_authorize"},
),
("device_poll", "AdminProviderOAuthUnavailableAdapter", {"operation": "device_poll"}),
],
)
async def test_admin_provider_oauth_routes_use_pipeline_shell(
monkeypatch: pytest.MonkeyPatch,
route_name: str,
expected_adapter: str,
expected_state: dict[str, Any],
) -> None:
from src.api.admin import provider_oauth as mod
captured: dict[str, Any] = {}
async def fake_run(*, adapter, http_request, db, mode, **_kwargs):
captured.update(
{
"adapter": adapter,
"request": http_request,
"db": db,
"mode": mode,
}
)
if route_name == "supported_types":
return []
return {"ok": True}
monkeypatch.setattr(mod.pipeline, "run", fake_run)
request = SimpleNamespace(state=SimpleNamespace())
db = object()
if route_name == "supported_types":
result = await mod.supported_types(request=request, db=db, _=None)
elif route_name == "start_oauth":
result = await mod.start_oauth("key_1", request=request, db=db, _=None)
elif route_name == "complete_oauth":
result = await mod.complete_oauth(
"key_1",
mod.CompleteOAuthRequest(callback_url="http://localhost/?code=x&state=y"),
request=request,
db=db,
_=None,
)
elif route_name == "refresh_oauth":
result = await mod.refresh_oauth("key_1", request=request, db=db, _=None)
elif route_name == "start_provider_oauth":
result = await mod.start_provider_oauth("provider_1", request=request, db=db, _=None)
elif route_name == "complete_provider_oauth":
result = await mod.complete_provider_oauth(
"provider_1",
mod.ProviderCompleteOAuthRequest(callback_url="http://localhost/?code=x&state=y"),
request=request,
db=db,
_=None,
)
elif route_name == "import_refresh_token":
result = await mod.import_refresh_token(
"provider_1",
mod.ImportRefreshTokenRequest(refresh_token="refresh-token"),
request=request,
db=db,
_=None,
)
elif route_name == "batch_import_oauth":
result = await mod.batch_import_oauth(
"provider_1",
mod.BatchImportRequest(credentials="refresh-token"),
request=request,
db=db,
_=None,
)
elif route_name == "start_batch_import_oauth_task":
result = await mod.start_batch_import_oauth_task(
"provider_1",
mod.BatchImportRequest(credentials="refresh-token"),
request=request,
db=db,
_=None,
)
elif route_name == "get_batch_import_oauth_task_status":
result = await mod.get_batch_import_oauth_task_status(
"provider_1",
"task_1",
request=request,
db=db,
_=None,
)
elif route_name == "device_authorize":
result = await mod.device_authorize(
"provider_1",
mod.DeviceAuthorizeRequest(),
request=request,
db=db,
_=None,
)
else:
result = await mod.device_poll(
"provider_1",
mod.DevicePollRequest(session_id="session_1"),
request=request,
db=db,
_=None,
)
assert captured["request"] is request
assert captured["db"] is db
assert captured["mode"] == captured["adapter"].mode
assert type(captured["adapter"]).__name__ == expected_adapter
assert getattr(captured["adapter"], "__dict__", {}) == expected_state
if route_name == "supported_types":
assert result == []
else:
assert result == {"ok": True}
-410
View File
@@ -1,410 +0,0 @@
from __future__ import annotations
from typing import Any
from unittest.mock import MagicMock
import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient
from src.database import get_db
def _normalize_state(adapter: Any) -> dict[str, Any]:
state: dict[str, Any] = {}
for key, value in dict(getattr(adapter, "__dict__", {})).items():
if hasattr(value, "model_dump"):
state[key] = value.model_dump()
else:
state[key] = value
return state
def _build_app(
monkeypatch: pytest.MonkeyPatch,
*,
pipeline_result: Any,
) -> tuple[TestClient, list[dict[str, Any]]]:
from src.api.admin.provider_ops import routes as mod
app = FastAPI()
app.include_router(mod.router)
app.dependency_overrides[get_db] = lambda: MagicMock()
calls: list[dict[str, Any]] = []
async def _fake_pipeline_run(
*,
adapter: Any,
http_request: object,
db: object,
mode: object,
api_format_hint: str | None = None,
path_params: dict[str, Any] | None = None,
) -> Any:
del http_request, db, api_format_hint, path_params
calls.append(
{
"adapter_type": type(adapter).__name__,
"mode": getattr(mode, "value", mode),
"adapter_state": _normalize_state(adapter),
}
)
return pipeline_result
monkeypatch.setattr(mod.pipeline, "run", _fake_pipeline_run)
return TestClient(app), calls
def _architecture_payload() -> dict[str, Any]:
return {
"architecture_id": "generic_api",
"display_name": "Generic API",
"description": "generic",
"credentials_schema": {},
"supported_auth_types": [],
"supported_actions": [],
"default_connector": None,
}
def _status_payload() -> dict[str, Any]:
return {
"provider_id": "provider-1",
"is_configured": True,
"architecture_id": "generic_api",
"connection_status": {
"status": "connected",
"auth_type": "api_key",
"connected_at": None,
"expires_at": None,
"last_error": None,
},
"enabled_actions": ["balance"],
}
def _config_payload() -> dict[str, Any]:
return {
"provider_id": "provider-1",
"is_configured": True,
"architecture_id": "generic_api",
"base_url": "https://example.com",
"connector": {
"auth_type": "api_key",
"config": {},
"credentials": {},
},
}
def _verify_payload() -> dict[str, Any]:
return {
"success": True,
"message": "ok",
"data": {"verified": True},
"updated_credentials": {"token": "masked"},
}
def _action_payload() -> dict[str, Any]:
return {
"status": "success",
"action_type": "balance",
"data": {"balance": "1.23"},
"message": "ok",
"executed_at": "2026-03-26T00:00:00+00:00",
"response_time_ms": 12,
"cache_ttl_seconds": 60,
}
def test_provider_ops_architectures_route_is_pipeline_shell(
monkeypatch: pytest.MonkeyPatch,
) -> None:
client, calls = _build_app(monkeypatch, pipeline_result=[_architecture_payload()])
response = client.get("/api/admin/provider-ops/architectures")
assert response.status_code == 200
assert response.json() == [_architecture_payload()]
assert calls == [
{
"adapter_type": "AdminProviderOpsListArchitecturesAdapter",
"mode": "admin",
"adapter_state": {},
}
]
def test_provider_ops_architecture_detail_route_is_pipeline_shell(
monkeypatch: pytest.MonkeyPatch,
) -> None:
client, calls = _build_app(monkeypatch, pipeline_result=_architecture_payload())
response = client.get("/api/admin/provider-ops/architectures/generic_api")
assert response.status_code == 200
assert response.json() == _architecture_payload()
assert calls == [
{
"adapter_type": "AdminProviderOpsGetArchitectureAdapter",
"mode": "admin",
"adapter_state": {"architecture_id": "generic_api"},
}
]
def test_provider_ops_status_route_is_pipeline_shell(monkeypatch: pytest.MonkeyPatch) -> None:
client, calls = _build_app(monkeypatch, pipeline_result=_status_payload())
response = client.get("/api/admin/provider-ops/providers/provider-1/status")
assert response.status_code == 200
assert response.json() == _status_payload()
assert calls == [
{
"adapter_type": "AdminProviderOpsStatusAdapter",
"mode": "admin",
"adapter_state": {"provider_id": "provider-1"},
}
]
def test_provider_ops_config_route_is_pipeline_shell(monkeypatch: pytest.MonkeyPatch) -> None:
client, calls = _build_app(monkeypatch, pipeline_result=_config_payload())
response = client.get("/api/admin/provider-ops/providers/provider-1/config")
assert response.status_code == 200
assert response.json() == _config_payload()
assert calls == [
{
"adapter_type": "AdminProviderOpsConfigAdapter",
"mode": "admin",
"adapter_state": {"provider_id": "provider-1"},
}
]
def test_provider_ops_save_config_route_is_pipeline_shell(
monkeypatch: pytest.MonkeyPatch,
) -> None:
client, calls = _build_app(monkeypatch, pipeline_result={"success": True, "message": "ok"})
payload = {
"architecture_id": "generic_api",
"base_url": "https://example.com",
"connector": {
"auth_type": "api_key",
"config": {"region": "us"},
"credentials": {"api_key": "secret"},
},
"actions": {"balance": {"enabled": True, "config": {"refresh": True}}},
"schedule": {"balance": "0 * * * *"},
}
response = client.put("/api/admin/provider-ops/providers/provider-1/config", json=payload)
assert response.status_code == 200
assert response.json() == {"success": True, "message": "ok"}
assert calls == [
{
"adapter_type": "AdminProviderOpsSaveConfigAdapter",
"mode": "admin",
"adapter_state": {
"provider_id": "provider-1",
"payload": payload,
},
}
]
def test_provider_ops_verify_route_is_pipeline_shell(monkeypatch: pytest.MonkeyPatch) -> None:
client, calls = _build_app(monkeypatch, pipeline_result=_verify_payload())
payload = {
"architecture_id": "generic_api",
"base_url": "https://example.com",
"connector": {
"auth_type": "api_key",
"config": {},
"credentials": {"api_key": "secret"},
},
"actions": {},
"schedule": {},
}
response = client.post("/api/admin/provider-ops/providers/provider-1/verify", json=payload)
assert response.status_code == 200
assert response.json() == _verify_payload()
assert calls == [
{
"adapter_type": "AdminProviderOpsVerifyAuthAdapter",
"mode": "admin",
"adapter_state": {
"provider_id": "provider-1",
"payload": payload,
},
}
]
def test_provider_ops_delete_config_route_is_pipeline_shell(
monkeypatch: pytest.MonkeyPatch,
) -> None:
client, calls = _build_app(monkeypatch, pipeline_result={"success": True, "message": "ok"})
response = client.delete("/api/admin/provider-ops/providers/provider-1/config")
assert response.status_code == 200
assert response.json() == {"success": True, "message": "ok"}
assert calls == [
{
"adapter_type": "AdminProviderOpsDeleteConfigAdapter",
"mode": "admin",
"adapter_state": {"provider_id": "provider-1"},
}
]
def test_provider_ops_connect_route_is_pipeline_shell(monkeypatch: pytest.MonkeyPatch) -> None:
client, calls = _build_app(monkeypatch, pipeline_result={"success": True, "message": "ok"})
response = client.post(
"/api/admin/provider-ops/providers/provider-1/connect",
json={"credentials": {"api_key": "secret"}},
)
assert response.status_code == 200
assert response.json() == {"success": True, "message": "ok"}
assert calls == [
{
"adapter_type": "AdminProviderOpsConnectAdapter",
"mode": "admin",
"adapter_state": {
"provider_id": "provider-1",
"payload": {"credentials": {"api_key": "secret"}},
},
}
]
def test_provider_ops_disconnect_route_is_pipeline_shell(
monkeypatch: pytest.MonkeyPatch,
) -> None:
client, calls = _build_app(monkeypatch, pipeline_result={"success": True, "message": "ok"})
response = client.post("/api/admin/provider-ops/providers/provider-1/disconnect")
assert response.status_code == 200
assert response.json() == {"success": True, "message": "ok"}
assert calls == [
{
"adapter_type": "AdminProviderOpsDisconnectAdapter",
"mode": "admin",
"adapter_state": {"provider_id": "provider-1"},
}
]
def test_provider_ops_execute_action_route_is_pipeline_shell(
monkeypatch: pytest.MonkeyPatch,
) -> None:
client, calls = _build_app(monkeypatch, pipeline_result=_action_payload())
response = client.post(
"/api/admin/provider-ops/providers/provider-1/actions/balance",
json={"config": {"refresh": True}},
)
assert response.status_code == 200
assert response.json() == _action_payload()
assert calls == [
{
"adapter_type": "AdminProviderOpsExecuteActionAdapter",
"mode": "admin",
"adapter_state": {
"provider_id": "provider-1",
"action_type": "balance",
"payload": {"config": {"refresh": True}},
},
}
]
def test_provider_ops_get_balance_route_is_pipeline_shell(
monkeypatch: pytest.MonkeyPatch,
) -> None:
client, calls = _build_app(monkeypatch, pipeline_result=_action_payload())
response = client.get("/api/admin/provider-ops/providers/provider-1/balance?refresh=false")
assert response.status_code == 200
assert response.json() == _action_payload()
assert calls == [
{
"adapter_type": "AdminProviderOpsGetBalanceAdapter",
"mode": "admin",
"adapter_state": {
"provider_id": "provider-1",
"refresh": False,
},
}
]
def test_provider_ops_refresh_balance_route_is_pipeline_shell(
monkeypatch: pytest.MonkeyPatch,
) -> None:
client, calls = _build_app(monkeypatch, pipeline_result=_action_payload())
response = client.post("/api/admin/provider-ops/providers/provider-1/balance")
assert response.status_code == 200
assert response.json() == _action_payload()
assert calls == [
{
"adapter_type": "AdminProviderOpsRefreshBalanceAdapter",
"mode": "admin",
"adapter_state": {"provider_id": "provider-1"},
}
]
def test_provider_ops_checkin_route_is_pipeline_shell(monkeypatch: pytest.MonkeyPatch) -> None:
client, calls = _build_app(monkeypatch, pipeline_result=_action_payload())
response = client.post("/api/admin/provider-ops/providers/provider-1/checkin")
assert response.status_code == 200
assert response.json() == _action_payload()
assert calls == [
{
"adapter_type": "AdminProviderOpsCheckinAdapter",
"mode": "admin",
"adapter_state": {"provider_id": "provider-1"},
}
]
def test_provider_ops_batch_balance_route_is_pipeline_shell(
monkeypatch: pytest.MonkeyPatch,
) -> None:
client, calls = _build_app(
monkeypatch,
pipeline_result={"provider-1": _action_payload(), "provider-2": _action_payload()},
)
response = client.post(
"/api/admin/provider-ops/batch/balance?provider_ids=provider-1&provider_ids=provider-2"
)
assert response.status_code == 200
assert response.json() == {"provider-1": _action_payload(), "provider-2": _action_payload()}
assert calls == [
{
"adapter_type": "AdminProviderOpsBatchBalanceAdapter",
"mode": "admin",
"adapter_state": {"provider_ids": ["provider-1", "provider-2"]},
}
]
@@ -1,259 +0,0 @@
from __future__ import annotations
from io import BytesIO
from types import SimpleNamespace
from unittest.mock import AsyncMock
import pytest
from fastapi import HTTPException
from fastapi.responses import JSONResponse
from starlette.datastructures import UploadFile
class _FakeQuery:
def __init__(self, result: object) -> None:
self._result = result
def filter(self, *_args: object, **_kwargs: object) -> "_FakeQuery":
return self
def first(self) -> object:
return self._result
class _FakeDB:
def __init__(self, result: object) -> None:
self._result = result
def query(self, *_args: object, **_kwargs: object) -> _FakeQuery:
return _FakeQuery(self._result)
@pytest.mark.asyncio
async def test_admin_system_check_update_returns_unavailable_payload(
monkeypatch: pytest.MonkeyPatch,
) -> None:
from src.api.admin import system as mod
monkeypatch.setattr(mod, "_get_current_version", lambda: "1.2.3")
result = mod._build_check_update_unavailable_response()
assert result == {
"current_version": "1.2.3",
"latest_version": None,
"has_update": False,
"release_url": None,
"release_notes": None,
"published_at": None,
"error": "检查更新需要 Rust 管理后端",
}
@pytest.mark.asyncio
async def test_admin_system_aws_regions_uses_local_cache_when_present(
monkeypatch: pytest.MonkeyPatch,
) -> None:
from src.api.admin import system as mod
from src.core.cache_service import CacheService
monkeypatch.setattr(CacheService, "get", AsyncMock(return_value=["us-east-1", "us-west-2"]))
mod._aws_regions_mem_cache = None
result = await mod._get_aws_regions_response()
assert result == {"regions": ["us-east-1", "us-west-2"]}
@pytest.mark.asyncio
async def test_admin_system_aws_regions_raises_without_local_cache(
monkeypatch: pytest.MonkeyPatch,
) -> None:
from src.api.admin import system as mod
from src.core.cache_service import CacheService
monkeypatch.setattr(CacheService, "get", AsyncMock(return_value=None))
mod._aws_regions_mem_cache = None
with pytest.raises(HTTPException) as exc_info:
await mod._get_aws_regions_response()
assert exc_info.value.status_code == 503
assert exc_info.value.detail == "AWS regions requires Rust admin backend"
@pytest.mark.asyncio
async def test_admin_gemini_file_upload_requires_rust_uploader() -> None:
from src.api.admin import gemini_files as mod
upload = UploadFile(filename="example.txt", file=BytesIO(b"hello"), headers=None)
with pytest.raises(HTTPException) as exc_info:
await mod._upload_file_response(file=upload, key_ids="key_1")
assert exc_info.value.status_code == 503
assert exc_info.value.detail == "Admin Gemini file upload requires Rust uploader"
@pytest.mark.asyncio
async def test_admin_external_models_returns_cached_data(
monkeypatch: pytest.MonkeyPatch,
) -> None:
from src.api.admin.models import external as mod
monkeypatch.setattr(
mod,
"_get_cached_data",
AsyncMock(return_value={"openai": {"official": True, "models": []}}),
)
response = await mod._get_external_models_response()
assert isinstance(response, JSONResponse)
assert response.status_code == 200
assert b'"official":true' in response.body
@pytest.mark.asyncio
async def test_admin_external_models_raise_without_cache(
monkeypatch: pytest.MonkeyPatch,
) -> None:
from src.api.admin.models import external as mod
monkeypatch.setattr(mod, "_get_cached_data", AsyncMock(return_value=None))
with pytest.raises(HTTPException) as exc_info:
await mod._get_external_models_response()
assert exc_info.value.status_code == 503
assert exc_info.value.detail == "External models catalog requires Rust admin backend"
@pytest.mark.asyncio
async def test_admin_video_proxy_raises_when_google_proxy_is_required(
monkeypatch: pytest.MonkeyPatch,
) -> None:
from src.api.admin.video_tasks import routes as mod
from src.utils import auth_utils
task = SimpleNamespace(
id="task_1",
user_id="user_1",
video_url="https://generativelanguage.googleapis.com/v1/media/video.mp4",
)
request = SimpleNamespace(cookies={}, headers={"Authorization": "Bearer token"})
db = _FakeDB(task)
monkeypatch.setattr(
auth_utils,
"authenticate_user_from_bearer_token",
AsyncMock(return_value=SimpleNamespace(id="admin_1", role=mod.UserRole.ADMIN)),
)
with pytest.raises(HTTPException) as exc_info:
await mod._proxy_video_stream_response(task_id="task_1", request=request, token=None, db=db)
assert exc_info.value.status_code == 503
assert exc_info.value.detail == "Admin video proxy requires Rust/public download path"
@pytest.mark.asyncio
async def test_admin_usage_replay_requires_rust_maintenance_backend() -> None:
from src.api.admin.usage import routes as mod
adapter = mod.AdminUsageReplayAdapter(usage_id="usage_1")
with pytest.raises(HTTPException) as exc_info:
await adapter.handle(SimpleNamespace())
assert exc_info.value.status_code == 503
assert exc_info.value.detail == "Admin usage replay requires Rust maintenance backend"
@pytest.mark.asyncio
@pytest.mark.parametrize(
("call_factory"),
[
lambda mod: mod.start_oauth("key_1", request=SimpleNamespace(), db=None, _=None),
lambda mod: mod.complete_oauth(
"key_1",
mod.CompleteOAuthRequest(callback_url="http://localhost/?code=x&state=y"),
request=SimpleNamespace(),
db=None,
_=None,
),
lambda mod: mod.refresh_oauth("key_1", request=SimpleNamespace(), db=None, _=None),
lambda mod: mod.start_provider_oauth(
"provider_1", request=SimpleNamespace(), db=None, _=None
),
lambda mod: mod.complete_provider_oauth(
"provider_1",
mod.ProviderCompleteOAuthRequest(
callback_url="http://localhost/?code=x&state=y",
),
request=SimpleNamespace(),
db=None,
_=None,
),
lambda mod: mod.import_refresh_token(
"provider_1",
mod.ImportRefreshTokenRequest(refresh_token="refresh-token"),
request=SimpleNamespace(),
db=None,
_=None,
),
lambda mod: mod.batch_import_oauth(
"provider_1",
mod.BatchImportRequest(credentials="refresh-token"),
request=SimpleNamespace(),
db=None,
_=None,
),
lambda mod: mod.start_batch_import_oauth_task(
"provider_1",
mod.BatchImportRequest(credentials="refresh-token"),
request=SimpleNamespace(),
db=None,
_=None,
),
lambda mod: mod.get_batch_import_oauth_task_status(
"provider_1",
"task_1",
request=SimpleNamespace(),
db=None,
_=None,
),
lambda mod: mod.device_authorize(
"provider_1",
mod.DeviceAuthorizeRequest(),
request=SimpleNamespace(),
db=None,
_=None,
),
lambda mod: mod.device_poll(
"provider_1",
mod.DevicePollRequest(session_id="session_1"),
request=SimpleNamespace(),
db=None,
_=None,
),
],
)
async def test_admin_provider_oauth_routes_require_rust_maintenance_backend(
call_factory,
monkeypatch: pytest.MonkeyPatch,
) -> None:
from src.api.admin import provider_oauth as mod
async def _fake_pipeline_run(*, adapter: object, http_request: object, db: object, mode: object):
_ = http_request, db, mode
context = SimpleNamespace(add_audit_metadata=lambda **_kwargs: None)
return await adapter.handle(context) # type: ignore[attr-defined]
monkeypatch.setattr(mod.pipeline, "run", _fake_pipeline_run)
with pytest.raises(HTTPException) as exc_info:
await call_factory(mod)
assert exc_info.value.status_code == 503
assert exc_info.value.detail == "Admin provider OAuth requires Rust maintenance backend"
-257
View File
@@ -1,257 +0,0 @@
from __future__ import annotations
from types import SimpleNamespace
from unittest.mock import AsyncMock
import pytest
from starlette.requests import Request
def _make_request(path: str, method: str = "POST") -> Request:
scope = {
"type": "http",
"asgi": {"version": "3.0"},
"http_version": "1.1",
"method": method,
"scheme": "http",
"path": path,
"raw_path": path.encode(),
"query_string": b"",
"headers": [],
"client": ("127.0.0.1", 12345),
"server": ("testserver", 80),
}
return Request(scope)
@pytest.mark.asyncio
async def test_admin_external_models_route_uses_pipeline_shell(
monkeypatch: pytest.MonkeyPatch,
) -> None:
from src.api.admin.models import external as mod
captured: dict[str, object] = {}
async def fake_run(*, adapter, http_request, db, mode, **_kwargs):
captured.update(
{"adapter": adapter, "request": http_request, "db": db, "mode": mode},
)
return {"ok": True}
monkeypatch.setattr(mod.pipeline, "run", fake_run)
db = SimpleNamespace(name="db")
request = _make_request("/api/admin/models/external", method="GET")
result = await mod.get_external_models(request=request, db=db, _=SimpleNamespace())
assert result == {"ok": True}
assert isinstance(captured["adapter"], mod.AdminGetExternalModelsAdapter)
assert captured["request"] is request
assert captured["db"] is db
assert captured["mode"] == captured["adapter"].mode
@pytest.mark.asyncio
@pytest.mark.parametrize(
("path", "adapter_type"),
[
("/api/admin/system/version", "AdminSystemVersionAdapter"),
("/api/admin/system/check-update", "AdminSystemCheckUpdateAdapter"),
("/api/admin/system/aws-regions", "AdminAwsRegionsAdapter"),
],
)
async def test_admin_system_routes_use_pipeline_shell(
monkeypatch: pytest.MonkeyPatch,
path: str,
adapter_type: str,
) -> None:
from src.api.admin import system as mod
captured: dict[str, object] = {}
async def fake_run(*, adapter, http_request, db, mode, **_kwargs):
captured.update(
{"adapter": adapter, "request": http_request, "db": db, "mode": mode},
)
return {"ok": True}
monkeypatch.setattr(mod.pipeline, "run", fake_run)
db = SimpleNamespace(name="db")
request = _make_request(path, method="GET")
if path.endswith("/version"):
result = await mod.get_system_version(request=request, db=db)
elif path.endswith("/check-update"):
result = await mod.check_update(request=request, db=db)
else:
result = await mod.get_aws_regions(request=request, db=db)
assert result == {"ok": True}
assert type(captured["adapter"]).__name__ == adapter_type
assert captured["request"] is request
assert captured["db"] is db
assert captured["mode"] == captured["adapter"].mode
@pytest.mark.asyncio
async def test_admin_video_proxy_route_uses_pipeline_shell(
monkeypatch: pytest.MonkeyPatch,
) -> None:
from src.api.admin.video_tasks import routes as mod
captured: dict[str, object] = {}
async def fake_run(*, adapter, http_request, db, mode, **_kwargs):
captured.update(
{"adapter": adapter, "request": http_request, "db": db, "mode": mode},
)
return {"ok": True}
monkeypatch.setattr(mod.pipeline, "run", fake_run)
db = SimpleNamespace(name="db")
request = _make_request("/api/admin/video-tasks/task_1/video", method="GET")
result = await mod.proxy_video_stream(task_id="task_1", request=request, token="query-token", db=db)
assert result == {"ok": True}
assert isinstance(captured["adapter"], mod.VideoTaskProxyVideoAdapter)
assert captured["adapter"].task_id == "task_1"
assert captured["adapter"].token == "query-token"
assert captured["request"] is request
assert captured["db"] is db
assert captured["mode"] == captured["adapter"].mode
@pytest.mark.asyncio
async def test_admin_external_models_cache_route_uses_pipeline_shell(
monkeypatch: pytest.MonkeyPatch,
) -> None:
from src.api.admin.models import external as mod
captured: dict[str, object] = {}
async def fake_run(*, adapter, http_request, db, mode, **_kwargs):
captured.update(
{"adapter": adapter, "request": http_request, "db": db, "mode": mode},
)
return {"cleared": True}
monkeypatch.setattr(mod.pipeline, "run", fake_run)
db = SimpleNamespace(name="db")
request = _make_request("/api/admin/models/external/cache", method="DELETE")
result = await mod.clear_external_models_cache(request=request, db=db, _=SimpleNamespace())
assert result == {"cleared": True}
assert isinstance(captured["adapter"], mod.AdminClearExternalModelsCacheAdapter)
assert captured["request"] is request
assert captured["db"] is db
assert captured["mode"] == captured["adapter"].mode
@pytest.mark.asyncio
async def test_admin_external_models_adapters_delegate_to_helpers(
monkeypatch: pytest.MonkeyPatch,
) -> None:
from src.api.admin.models import external as mod
get_cached = AsyncMock(return_value={"ok": True})
clear_cached = AsyncMock(return_value={"cleared": True})
monkeypatch.setattr(mod, "_get_external_models_response", get_cached)
monkeypatch.setattr(mod, "_clear_external_models_cache_response", clear_cached)
get_result = await mod.AdminGetExternalModelsAdapter().handle(SimpleNamespace())
clear_result = await mod.AdminClearExternalModelsCacheAdapter().handle(SimpleNamespace())
assert get_result == {"ok": True}
assert clear_result == {"cleared": True}
get_cached.assert_awaited_once()
clear_cached.assert_awaited_once()
@pytest.mark.asyncio
async def test_provider_query_routes_use_pipeline_shell(
monkeypatch: pytest.MonkeyPatch,
) -> None:
from src.api.admin import provider_query as mod
from fastapi import HTTPException
captured: list[dict[str, object]] = []
async def fake_run(*, adapter, http_request, db, mode, **_kwargs):
captured.append({"adapter": adapter, "request": http_request, "db": db, "mode": mode})
return {"ok": True}
monkeypatch.setattr(mod.pipeline, "run", fake_run)
db = SimpleNamespace(name="db")
models_payload = mod.ModelsQueryRequest(provider_id="provider_1")
models_request = _make_request("/api/admin/provider-query/models")
with pytest.raises(HTTPException) as models_exc:
await mod.query_available_models(models_payload, models_request, db=db)
test_payload = mod.TestModelRequest(provider_id="provider_1", model_name="gpt-4o")
test_request = _make_request("/api/admin/provider-query/test-model")
with pytest.raises(HTTPException) as test_exc:
await mod.test_model(test_payload, test_request, db=db)
failover_payload = mod.TestModelFailoverRequest(
provider_id="provider_1",
mode="direct",
model_name="gpt-4o",
)
failover_request = _make_request("/api/admin/provider-query/test-model-failover")
with pytest.raises(HTTPException) as failover_exc:
await mod.test_model_failover(failover_payload, failover_request, db=db)
assert models_exc.value.status_code == 503
assert test_exc.value.status_code == 503
assert failover_exc.value.status_code == 503
assert "requires Rust maintenance backend" in str(models_exc.value.detail)
assert captured == []
@pytest.mark.asyncio
async def test_provider_query_adapters_delegate_to_helpers(
monkeypatch: pytest.MonkeyPatch,
) -> None:
from src.api.admin import provider_query as mod
models_helper = AsyncMock(return_value={"kind": "models"})
test_helper = AsyncMock(return_value={"kind": "test"})
failover_helper = AsyncMock(return_value={"kind": "failover"})
monkeypatch.setattr(mod, "_query_available_models_response", models_helper)
monkeypatch.setattr(mod, "_test_model_response", test_helper)
monkeypatch.setattr(mod, "_test_model_failover_response", failover_helper)
db = SimpleNamespace(name="db")
user = SimpleNamespace(id="user_1")
request = _make_request("/api/admin/provider-query/test-model-failover")
context = SimpleNamespace(db=db, user=user, request=request)
models_payload = mod.ModelsQueryRequest(provider_id="provider_1")
test_payload = mod.TestModelRequest(provider_id="provider_1", model_name="gpt-4o")
failover_payload = mod.TestModelFailoverRequest(
provider_id="provider_1",
mode="direct",
model_name="gpt-4o",
)
models_result = await mod.ProviderQueryModelsAdapter(payload=models_payload).handle(context)
test_result = await mod.ProviderQueryTestModelAdapter(payload=test_payload).handle(context)
failover_result = await mod.ProviderQueryTestModelFailoverAdapter(
payload=failover_payload
).handle(context)
assert models_result == {"kind": "models"}
assert test_result == {"kind": "test"}
assert failover_result == {"kind": "failover"}
models_helper.assert_awaited_once_with(models_payload, db)
test_helper.assert_awaited_once_with(test_payload, db, user)
failover_helper.assert_awaited_once_with(failover_payload, request, db, user)
@@ -1,271 +0,0 @@
from __future__ import annotations
import json
from types import SimpleNamespace
from typing import cast
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from src.api.handlers.base.cli_protocol import CliHandlerProtocol
from src.api.handlers.base.stream_context import StreamContext
from src.api.handlers.gemini_cli.handler import GeminiCliMessageHandler
from src.services.provider.adapters.antigravity.envelope import (
unwrap_v1internal_response,
wrap_v1internal_request,
)
def _make_handler() -> GeminiCliMessageHandler:
return GeminiCliMessageHandler(
db=MagicMock(), # type: ignore[arg-type]
user=SimpleNamespace(id=1), # type: ignore[arg-type]
api_key=SimpleNamespace(id=1), # type: ignore[arg-type]
request_id="req_1",
client_ip="127.0.0.1",
user_agent="pytest",
start_time=0.0,
)
def test_wrap_v1internal_request_removes_inner_model() -> None:
gemini_request = {
"model": "gemini-2.0-flash",
"contents": [{"role": "user", "parts": [{"text": "hi"}]}],
}
wrapped = wrap_v1internal_request(
gemini_request,
project_id="project-123",
model="gemini-2.0-flash",
)
assert wrapped["project"] == "project-123"
assert wrapped["model"] == "gemini-2.0-flash"
assert "request" in wrapped
assert "model" not in wrapped["request"]
def test_unwrap_v1internal_response() -> None:
v1_resp = {
"response": {"candidates": [{"content": {"parts": [{"text": "Hello"}]}}]},
"responseId": "resp-123",
}
unwrapped = unwrap_v1internal_response(v1_resp)
assert "response" not in unwrapped
assert "candidates" in unwrapped
assert unwrapped["_v1internal_response_id"] == "resp-123"
def test_convert_sse_line_unwraps_before_convert_stream_chunk() -> None:
handler = _make_handler()
ctx = StreamContext(model="gemini-2.0-flash", api_format="gemini:cli")
ctx.provider_type = "antigravity"
ctx.provider_api_format = "gemini:chat"
ctx.client_api_format = "gemini:cli"
v1_line = (
'data: {"response": {"candidates": [{"content": {"parts": [{"text": "Hi"}]}}]},'
' "responseId": "123"}'
)
seen: dict[str, object] = {}
class _DummyRegistry:
def convert_stream_chunk(
self, data_obj: object, *_args: object, **_kwargs: object
) -> list[str]:
seen["data_obj"] = data_obj
return []
with patch(
"src.api.handlers.base.cli_event_mixin.get_format_converter_registry",
return_value=_DummyRegistry(),
):
_lines, _events = handler._convert_sse_line(ctx, v1_line, [])
assert isinstance(seen.get("data_obj"), dict)
assert "response" not in seen["data_obj"] # type: ignore[operator]
assert "_v1internal_response_id" in seen["data_obj"] # type: ignore[operator]
def test_handle_sse_event_unwraps_for_antigravity() -> None:
handler = _make_handler()
ctx = StreamContext(model="gemini-2.0-flash", api_format="gemini:cli")
ctx.provider_type = "antigravity"
v1_data = {
"response": {
"candidates": [
{
"content": {"parts": [{"text": "Hello"}], "role": "model"},
"finishReason": "STOP",
}
],
"modelVersion": "gemini-2.0-flash-001",
},
"responseId": "123",
}
with patch.object(handler, "_process_event_data") as mock_process:
cast(CliHandlerProtocol, handler)._handle_sse_event(
ctx, None, json.dumps(v1_data), record_chunk=False
)
assert mock_process.call_count == 1
passed_data = mock_process.call_args[0][2]
assert isinstance(passed_data, dict)
assert "response" not in passed_data
assert "candidates" in passed_data
def test_handle_sse_event_caches_thought_signature_for_antigravity() -> None:
from src.core.api_format.conversion.thinking_cache import signature_cache
signature_cache.clear()
handler = _make_handler()
ctx = StreamContext(model="claude-sonnet-4-5", api_format="gemini:cli")
ctx.provider_type = "antigravity"
# 签名须 >= MIN_SIGNATURE_LENGTH(50),否则会被忽略
long_sig = "a" * 60
payload = {
"candidates": [
{
"content": {
"parts": [
{
"text": "t1",
"thought": True,
"thoughtSignature": long_sig,
}
]
}
}
]
}
with patch.object(handler, "_process_event_data") as _mock_process:
cast(CliHandlerProtocol, handler)._handle_sse_event(
ctx, None, json.dumps(payload), record_chunk=False
)
assert signature_cache.get_or_dummy("claude-sonnet-4-5", "t1") == long_sig
def test_provider_type_drives_antigravity_usage_path() -> None:
handler = _make_handler()
event = {"usageMetadata": {"promptTokenCount": 10, "candidatesTokenCount": 2}}
usage = handler._extract_usage_from_event(event, provider_type="antigravity")
assert usage["input_tokens"] == 10
assert usage["output_tokens"] == 2
@pytest.mark.asyncio
async def test_antigravity_forces_conversion_path_in_stream_with_prefetch() -> None:
handler = _make_handler()
ctx = StreamContext(model="gemini-2.0-flash", api_format="gemini:cli")
ctx.provider_type = "antigravity"
ctx.needs_conversion = False # same-format case normally would passthrough
class _AsyncIter:
def __init__(self, items: list[bytes]) -> None:
self._it = iter(items)
def __aiter__(self) -> _AsyncIter:
return self
async def __anext__(self) -> bytes:
try:
return next(self._it)
except StopIteration as e:
raise StopAsyncIteration from e
prefetched = [
b'data: {"response": {"candidates": []}, "responseId": "1"}\n',
]
byte_iter = _AsyncIter([]) # no more bytes after prefetch
response_ctx = SimpleNamespace(__aexit__=AsyncMock(return_value=None))
with patch.object(
handler,
"_convert_sse_line",
return_value=(["data: {}"], []),
) as mock_convert:
out = []
async for chunk in handler._create_response_stream_with_prefetch(
ctx, byte_iter, response_ctx, prefetched # type: ignore[arg-type]
):
out.append(chunk)
assert mock_convert.call_count >= 1
assert any(b"data: {}" in c for c in out)
def test_wrap_v1internal_request_injects_thought_signature_from_tool_cache() -> None:
from src.core.api_format.conversion.thinking_cache import signature_cache
signature_cache.clear()
tool_id = "toolu_123"
sig = "a" * 60
signature_cache.cache_tool_signature(tool_id, sig)
gemini_request = {
"model": "gemini-2.0-flash",
"contents": [
{"role": "user", "parts": [{"text": "hi"}]},
{"role": "model", "parts": [{"function_call": {"name": "do", "id": tool_id}}]},
],
}
wrapped = wrap_v1internal_request(
gemini_request,
project_id="project-123",
model="gemini-2.0-flash",
)
model_turn = wrapped["request"]["contents"][1]
part = model_turn["parts"][0]
assert part["thoughtSignature"] == sig
def test_wrap_v1internal_request_injects_session_signature_when_tool_cache_missing() -> None:
from src.core.api_format.conversion.thinking_cache import signature_cache
signature_cache.clear()
session_id = "sid-123"
sig = "b" * 60
signature_cache.cache_session_signature(session_id, sig, message_count=2)
gemini_request = {
"model": "gemini-2.0-flash",
"sessionId": session_id,
"contents": [
{"role": "user", "parts": [{"text": "hi"}]},
{
"role": "model",
"parts": [{"functionCall": {"name": "do", "id": "toolu_missing"}}],
},
],
}
wrapped = wrap_v1internal_request(
gemini_request,
project_id="project-123",
model="gemini-2.0-flash",
)
model_turn = wrapped["request"]["contents"][1]
part = model_turn["parts"][0]
assert part["thoughtSignature"] == sig
@@ -1,200 +0,0 @@
from __future__ import annotations
import copy
from types import SimpleNamespace
from typing import Any
import pytest
import src.api.handlers.base.chat_handler_base as chatmod
from src.api.handlers.base.chat_handler_base import ChatHandlerBase
from src.api.handlers.base.chat_sync_executor import ChatSyncExecutor
from src.api.handlers.base.stream_context import StreamContext
from src.services.task.request_state import MutableRequestBodyState
class _StopBuild(Exception):
pass
class _DummyAuthInfo:
auth_header = "authorization"
auth_value = "Bearer test"
decrypted_auth_config = None
def as_tuple(self) -> tuple[str, str]:
return self.auth_header, self.auth_value
class _CaptureBuilder:
def __init__(self) -> None:
self.request_body: dict[str, Any] | None = None
def build(self, request_body: dict[str, Any], *args: Any, **kwargs: Any) -> Any:
self.request_body = request_body
raise _StopBuild()
class _DummyChatHandler(ChatHandlerBase):
FORMAT_ID = "openai:chat"
def __init__(self) -> None:
self.request_id = "req-test"
self.api_key = SimpleNamespace(id="user-key-1")
self._request_builder = _CaptureBuilder()
self.allowed_api_formats = ["openai:chat"]
self.api_family = None
self.endpoint_kind = None
async def _convert_request(self, request: Any) -> Any:
return request
def _extract_usage(self, response: dict) -> dict[str, int]:
return {}
async def _get_mapped_model(
self,
source_model: str,
provider_id: str,
api_format: str | None = None,
) -> str | None:
del source_model, provider_id, api_format
return None
def apply_mapped_model(self, request_body: dict[str, Any], mapped_model: str) -> dict[str, Any]:
out = dict(request_body)
out["model"] = mapped_model
return out
def prepare_provider_request_body(self, request_body: dict[str, Any]) -> dict[str, Any]:
request_body["messages"][0]["content"] = "prepared"
return request_body
def finalize_provider_request(
self,
request_body: dict[str, Any],
*,
mapped_model: str | None,
provider_api_format: str | None,
) -> dict[str, Any]:
del mapped_model, provider_api_format
request_body["messages"].append({"role": "assistant", "content": "finalized"})
return request_body
def get_model_for_url(
self,
request_body: dict[str, Any],
mapped_model: str | None,
) -> str | None:
return mapped_model or str(request_body.get("model") or "")
def _patch_chat_upstream(monkeypatch: pytest.MonkeyPatch) -> None:
async def _fake_get_provider_auth(endpoint: Any, key: Any) -> _DummyAuthInfo:
return _DummyAuthInfo()
monkeypatch.setattr(chatmod, "get_provider_auth", _fake_get_provider_auth)
monkeypatch.setattr(
chatmod,
"get_provider_behavior",
lambda **kwargs: SimpleNamespace(
envelope=None,
same_format_variant=None,
cross_format_variant=None,
),
)
monkeypatch.setattr(chatmod, "get_upstream_stream_policy", lambda *args, **kwargs: None)
monkeypatch.setattr(
chatmod,
"resolve_upstream_is_stream",
lambda *, client_is_stream, policy: client_is_stream,
)
monkeypatch.setattr(chatmod, "enforce_stream_mode_for_upstream", lambda *args, **kwargs: None)
monkeypatch.setattr(
chatmod,
"maybe_patch_request_with_prompt_cache_key",
lambda request_body, **kwargs: request_body,
)
@pytest.mark.asyncio
async def test_chat_execute_stream_request_does_not_mutate_original_request_body(
monkeypatch: pytest.MonkeyPatch,
) -> None:
_patch_chat_upstream(monkeypatch)
handler = _DummyChatHandler()
ctx = StreamContext(model="gpt-test", api_format="openai:chat")
ctx.client_api_format = "openai:chat"
provider = SimpleNamespace(name="provider", id="provider-1", provider_type="", proxy=None)
endpoint = SimpleNamespace(id="endpoint-1", api_format="openai:chat", base_url="https://x")
key = SimpleNamespace(id="key-1", proxy=None)
candidate = SimpleNamespace(
mapping_matched_model=None, needs_conversion=False, output_limit=None
)
original_request_body = {
"model": "gpt-test",
"messages": [{"role": "user", "content": "hello"}],
}
snapshot = copy.deepcopy(original_request_body)
request_state = MutableRequestBodyState(original_request_body)
with pytest.raises(_StopBuild):
await handler._execute_stream_request(
ctx,
object(),
provider,
endpoint,
key,
request_state.build_attempt_body(),
{},
candidate=candidate,
)
assert original_request_body == snapshot
assert handler._request_builder.request_body is not None
assert handler._request_builder.request_body["messages"][0]["content"] == "prepared"
assert handler._request_builder.request_body["messages"][-1]["content"] == "finalized"
@pytest.mark.asyncio
async def test_chat_sync_request_func_does_not_mutate_original_request_body(
monkeypatch: pytest.MonkeyPatch,
) -> None:
_patch_chat_upstream(monkeypatch)
handler = _DummyChatHandler()
executor = ChatSyncExecutor(handler)
provider = SimpleNamespace(name="provider", id="provider-1", provider_type="", proxy=None)
endpoint = SimpleNamespace(id="endpoint-1", api_format="openai:chat", base_url="https://x")
key = SimpleNamespace(id="key-1", proxy=None)
candidate = SimpleNamespace(
mapping_matched_model=None, needs_conversion=False, output_limit=None
)
original_request_body = {
"model": "gpt-test",
"messages": [{"role": "user", "content": "hello"}],
}
snapshot = copy.deepcopy(original_request_body)
request_state = MutableRequestBodyState(original_request_body)
with pytest.raises(_StopBuild):
await executor._sync_request_func(
provider,
endpoint,
key,
candidate,
model="gpt-test",
api_format="openai:chat",
original_headers={},
request_state=request_state,
)
assert original_request_body == snapshot
assert handler._request_builder.request_body is not None
assert handler._request_builder.request_body["messages"][0]["content"] == "prepared"
assert handler._request_builder.request_body["messages"][-1]["content"] == "finalized"
@@ -1,566 +0,0 @@
from __future__ import annotations
from types import SimpleNamespace
import httpx
import pytest
import src.api.handlers.base.chat_sync_executor as chat_sync_mod
from src.api.handlers.base.chat_sync_executor import ChatSyncExecutor
from src.core.exceptions import EmbeddedErrorException
from src.core.exceptions import ProviderNotAvailableException
from src.services.request.execution_runtime_plan import (
ExecutionPlan,
ExecutionPlanBody,
PreparedExecutionPlan,
)
from src.services.request.execution_runtime_client import (
ExecutionRuntimeClientError,
ExecutionRuntimeSyncResult,
)
class _FakeEnvelope:
name = "fake-envelope"
def __init__(self) -> None:
self.status_codes: list[int] = []
self.postprocessed_payloads: list[dict[str, object]] = []
def on_http_status(self, *, base_url: str | None, status_code: int) -> None:
self.status_codes.append(status_code)
def on_connection_error(self, *, base_url: str | None, exc: Exception) -> None:
raise AssertionError("connection error hook should not be used in this test")
def unwrap_response(self, data: dict[str, object]) -> dict[str, object]:
return dict(data["payload"]) # type: ignore[index]
def postprocess_unwrapped_response(self, *, model: str, data: dict[str, object]) -> None:
self.postprocessed_payloads.append(dict(data))
class _FakeNormalizer:
def response_from_internal(
self,
internal_resp: object,
*,
requested_model: str,
) -> dict[str, object]:
return {
"aggregated": True,
"requested_model": requested_model,
"internal_id": getattr(internal_resp, "id", "missing"),
}
def _make_prepared_plan() -> PreparedExecutionPlan:
return PreparedExecutionPlan(
contract=ExecutionPlan(
request_id="req-test",
candidate_id=None,
provider_name="openai",
provider_id="prov-1",
endpoint_id="ep-1",
key_id="key-1",
method="POST",
url="https://example.com/v1/chat/completions",
headers={"content-type": "application/json"},
body=ExecutionPlanBody(json_body={"model": "gpt-4.1"}),
stream=False,
provider_api_format="openai:chat",
client_api_format="openai:chat",
model_name="gpt-4.1",
),
payload={"model": "gpt-4.1"},
headers={"content-type": "application/json"},
upstream_is_stream=False,
needs_conversion=False,
provider_type="openai",
request_timeout=30.0,
)
def _make_proxy_prepared_plan() -> PreparedExecutionPlan:
prepared = _make_prepared_plan()
prepared.contract.proxy = chat_sync_mod.ExecutionProxySnapshot(
enabled=True,
mode="http",
label="proxy.internal",
url="http://proxy.internal:8080",
)
prepared.proxy_config = {"url": "http://proxy.internal:8080"}
return prepared
def _make_tunnel_prepared_plan() -> PreparedExecutionPlan:
prepared = _make_prepared_plan()
prepared.contract.proxy = chat_sync_mod.ExecutionProxySnapshot(
enabled=True,
mode="tunnel",
node_id="node-1",
label="relay-node",
)
prepared.delegate_config = {"tunnel": True, "node_id": "node-1"}
prepared.proxy_config = {"node_id": "node-1"}
return prepared
def _make_upstream_stream_prepared_plan() -> PreparedExecutionPlan:
prepared = _make_prepared_plan()
prepared.contract.stream = True
prepared.upstream_is_stream = True
return prepared
def _make_tls_prepared_plan() -> PreparedExecutionPlan:
prepared = _make_prepared_plan()
prepared.contract.tls_profile = "claude_code_nodejs"
prepared.provider_type = "claude_code"
return prepared
def _make_executor() -> ChatSyncExecutor:
handler = SimpleNamespace(request_id="req-test")
executor = ChatSyncExecutor(handler)
executor._ctx.provider_api_format_for_error = "openai:chat"
executor._ctx.client_api_format_for_error = "openai:chat"
executor._ctx.needs_conversion_for_error = False
return executor
@pytest.mark.asyncio
async def test_execute_sync_plan_uses_rust_executor_when_available(
monkeypatch: pytest.MonkeyPatch,
) -> None:
executor = _make_executor()
prepared_plan = _make_prepared_plan()
monkeypatch.setattr(chat_sync_mod.config, "executor_backend", "rust")
async def _fake_execute_sync_json(self: object, plan: ExecutionPlan) -> ExecutionRuntimeSyncResult:
assert plan.request_id == "req-test"
return ExecutionRuntimeSyncResult(
status_code=200,
response_json={"id": "chatcmpl-1"},
headers={"content-type": "application/json"},
)
monkeypatch.setattr(
chat_sync_mod.ExecutionRuntimeClient,
"execute_sync_json",
_fake_execute_sync_json,
)
response = await executor._execute_sync_plan(
prepared_plan=prepared_plan,
provider=SimpleNamespace(name="provider"),
model="gpt-4.1",
)
assert response == {"id": "chatcmpl-1"}
assert executor._ctx.status_code == 200
assert executor._ctx.response_json == {"id": "chatcmpl-1"}
@pytest.mark.asyncio
async def test_execute_sync_plan_allows_supported_proxy_urls_for_rust(
monkeypatch: pytest.MonkeyPatch,
) -> None:
executor = _make_executor()
prepared_plan = _make_proxy_prepared_plan()
monkeypatch.setattr(chat_sync_mod.config, "executor_backend", "rust")
async def _fake_execute_sync_json(self: object, plan: ExecutionPlan) -> ExecutionRuntimeSyncResult:
assert plan.proxy is not None
assert plan.proxy.url == "http://proxy.internal:8080"
return ExecutionRuntimeSyncResult(
status_code=200,
response_json={"id": "chatcmpl-proxy"},
headers={"content-type": "application/json"},
)
monkeypatch.setattr(
chat_sync_mod.ExecutionRuntimeClient,
"execute_sync_json",
_fake_execute_sync_json,
)
response = await executor._execute_sync_plan(
prepared_plan=prepared_plan,
provider=SimpleNamespace(name="provider"),
model="gpt-4.1",
)
assert response == {"id": "chatcmpl-proxy"}
@pytest.mark.asyncio
async def test_execute_sync_plan_allows_tunnel_delegate_for_rust(
monkeypatch: pytest.MonkeyPatch,
) -> None:
executor = _make_executor()
prepared_plan = _make_tunnel_prepared_plan()
monkeypatch.setattr(chat_sync_mod.config, "executor_backend", "rust")
async def _fake_execute_sync_json(self: object, plan: ExecutionPlan) -> ExecutionRuntimeSyncResult:
assert plan.proxy is not None
assert plan.proxy.mode == "tunnel"
assert plan.proxy.node_id == "node-1"
return ExecutionRuntimeSyncResult(
status_code=200,
response_json={"id": "chatcmpl-tunnel"},
headers={"content-type": "application/json"},
)
monkeypatch.setattr(
chat_sync_mod.ExecutionRuntimeClient,
"execute_sync_json",
_fake_execute_sync_json,
)
response = await executor._execute_sync_plan(
prepared_plan=prepared_plan,
provider=SimpleNamespace(name="provider"),
model="gpt-4.1",
)
assert response == {"id": "chatcmpl-tunnel"}
@pytest.mark.asyncio
async def test_execute_sync_plan_allows_tls_profile_for_rust(
monkeypatch: pytest.MonkeyPatch,
) -> None:
executor = _make_executor()
prepared_plan = _make_tls_prepared_plan()
monkeypatch.setattr(chat_sync_mod.config, "executor_backend", "rust")
async def _fake_execute_sync_json(self: object, plan: ExecutionPlan) -> ExecutionRuntimeSyncResult:
assert plan.tls_profile == "claude_code_nodejs"
return ExecutionRuntimeSyncResult(
status_code=200,
response_json={"id": "chatcmpl-tls"},
headers={"content-type": "application/json"},
)
monkeypatch.setattr(
chat_sync_mod.ExecutionRuntimeClient,
"execute_sync_json",
_fake_execute_sync_json,
)
response = await executor._execute_sync_plan(
prepared_plan=prepared_plan,
provider=SimpleNamespace(name="provider"),
model="claude-3.7-sonnet",
)
assert response == {"id": "chatcmpl-tls"}
@pytest.mark.asyncio
async def test_execute_sync_plan_applies_envelope_postprocessing_after_rust(
monkeypatch: pytest.MonkeyPatch,
) -> None:
executor = _make_executor()
prepared_plan = _make_prepared_plan()
prepared_plan.envelope = _FakeEnvelope()
monkeypatch.setattr(chat_sync_mod.config, "executor_backend", "rust")
async def _fake_execute_sync_json(self: object, plan: ExecutionPlan) -> ExecutionRuntimeSyncResult:
return ExecutionRuntimeSyncResult(
status_code=200,
response_json={"payload": {"id": "wrapped-1", "message": "ok"}},
headers={"content-type": "application/json"},
)
monkeypatch.setattr(
chat_sync_mod.ExecutionRuntimeClient,
"execute_sync_json",
_fake_execute_sync_json,
)
response = await executor._execute_sync_plan(
prepared_plan=prepared_plan,
provider=SimpleNamespace(name="provider"),
model="gpt-4.1",
)
assert response == {"id": "wrapped-1", "message": "ok"}
assert prepared_plan.envelope.status_codes == [200]
assert prepared_plan.envelope.postprocessed_payloads == [
{"id": "wrapped-1", "message": "ok"}
]
@pytest.mark.asyncio
async def test_execute_sync_plan_applies_format_conversion_after_rust(
monkeypatch: pytest.MonkeyPatch,
) -> None:
executor = _make_executor()
prepared_plan = _make_prepared_plan()
prepared_plan.needs_conversion = True
prepared_plan.contract.provider_api_format = "gemini:chat"
prepared_plan.contract.client_api_format = "openai:chat"
executor._ctx.provider_api_format_for_error = "gemini:chat"
executor._ctx.client_api_format_for_error = "openai:chat"
executor._ctx.needs_conversion_for_error = True
monkeypatch.setattr(chat_sync_mod.config, "executor_backend", "rust")
class _FakeRegistry:
def convert_response(
self,
response_json: dict[str, object],
source_format: str,
target_format: str,
*,
requested_model: str,
) -> dict[str, object]:
assert source_format == "gemini:chat"
assert target_format == "openai:chat"
assert requested_model == "gpt-4.1"
return {
"converted": True,
"source_id": response_json["provider_id"],
}
async def _fake_execute_sync_json(self: object, plan: ExecutionPlan) -> ExecutionRuntimeSyncResult:
assert plan.provider_api_format == "gemini:chat"
return ExecutionRuntimeSyncResult(
status_code=200,
response_json={"provider_id": "gemini-1"},
headers={"content-type": "application/json"},
)
monkeypatch.setattr(
chat_sync_mod.ExecutionRuntimeClient,
"execute_sync_json",
_fake_execute_sync_json,
)
monkeypatch.setattr(chat_sync_mod, "get_format_converter_registry", lambda: _FakeRegistry())
response = await executor._execute_sync_plan(
prepared_plan=prepared_plan,
provider=SimpleNamespace(name="provider"),
model="gpt-4.1",
)
assert response == {"converted": True, "source_id": "gemini-1"}
assert executor._ctx.provider_response_json == {"provider_id": "gemini-1"}
@pytest.mark.asyncio
async def test_execute_sync_plan_aggregates_upstream_stream_after_rust(
monkeypatch: pytest.MonkeyPatch,
) -> None:
executor = _make_executor()
prepared_plan = _make_upstream_stream_prepared_plan()
monkeypatch.setattr(chat_sync_mod.config, "executor_backend", "rust")
class _FakeRegistry:
def get_normalizer(self, format_id: str) -> _FakeNormalizer:
assert format_id == "openai:chat"
return _FakeNormalizer()
captured_chunks: list[bytes] = []
async def _fake_aggregate(
byte_iter: object,
*,
provider_api_format: str,
provider_name: str,
model: str,
request_id: str,
envelope: object = None,
provider_parser: object = None,
) -> object:
async for chunk in byte_iter: # type: ignore[attr-defined]
captured_chunks.append(chunk)
assert provider_api_format == "openai:chat"
assert provider_name == "provider"
assert model == "gpt-4.1"
assert request_id == "req-test"
return SimpleNamespace(id="agg-1")
async def _fake_execute_sync_json(self: object, plan: ExecutionPlan) -> ExecutionRuntimeSyncResult:
assert plan.stream is True
return ExecutionRuntimeSyncResult(
status_code=200,
response_body_bytes=b"data: {\"id\":\"chunk-1\"}\n\ndata: [DONE]\n\n",
headers={"content-type": "text/event-stream"},
)
monkeypatch.setattr(
chat_sync_mod.ExecutionRuntimeClient,
"execute_sync_json",
_fake_execute_sync_json,
)
monkeypatch.setattr(chat_sync_mod, "get_format_converter_registry", lambda: _FakeRegistry())
monkeypatch.setattr(
"src.api.handlers.base.upstream_stream_bridge.aggregate_upstream_stream_to_internal_response",
_fake_aggregate,
)
response = await executor._execute_sync_plan(
prepared_plan=prepared_plan,
provider=SimpleNamespace(name="provider"),
model="gpt-4.1",
)
assert response == {
"aggregated": True,
"requested_model": "gpt-4.1",
"internal_id": "agg-1",
}
assert captured_chunks == [b"data: {\"id\":\"chunk-1\"}\n\ndata: [DONE]\n\n"]
assert executor._ctx.status_code == 200
@pytest.mark.asyncio
async def test_execute_sync_plan_turns_rust_http_error_into_httpx_status_error(
monkeypatch: pytest.MonkeyPatch,
) -> None:
executor = _make_executor()
prepared_plan = _make_prepared_plan()
monkeypatch.setattr(chat_sync_mod.config, "executor_backend", "rust")
async def _fake_execute_sync_json(self: object, plan: ExecutionPlan) -> ExecutionRuntimeSyncResult:
assert plan.url.endswith("/chat/completions")
return ExecutionRuntimeSyncResult(
status_code=429,
response_json={"error": {"message": "slow down"}},
headers={"content-type": "application/json"},
)
monkeypatch.setattr(
chat_sync_mod.ExecutionRuntimeClient,
"execute_sync_json",
_fake_execute_sync_json,
)
with pytest.raises(httpx.HTTPStatusError) as exc_info:
await executor._execute_sync_plan(
prepared_plan=prepared_plan,
provider=SimpleNamespace(name="provider"),
model="gpt-4.1",
)
assert exc_info.value.response.status_code == 429
assert '"message": "slow down"' in exc_info.value.upstream_response # type: ignore[attr-defined]
@pytest.mark.asyncio
async def test_execute_sync_plan_preserves_embedded_error_semantics_from_rust(
monkeypatch: pytest.MonkeyPatch,
) -> None:
executor = _make_executor()
prepared_plan = _make_prepared_plan()
monkeypatch.setattr(chat_sync_mod.config, "executor_backend", "rust")
async def _fake_execute_sync_json(self: object, plan: ExecutionPlan) -> ExecutionRuntimeSyncResult:
assert plan.provider_api_format == "openai:chat"
return ExecutionRuntimeSyncResult(
status_code=200,
response_json={
"error": {
"message": "bad request",
"type": "invalid_request_error",
"code": 400,
}
},
headers={"content-type": "application/json"},
)
monkeypatch.setattr(
chat_sync_mod.ExecutionRuntimeClient,
"execute_sync_json",
_fake_execute_sync_json,
)
with pytest.raises(EmbeddedErrorException) as exc_info:
await executor._execute_sync_plan(
prepared_plan=prepared_plan,
provider=SimpleNamespace(name="provider"),
model="gpt-4.1",
)
assert exc_info.value.error_message == "bad request"
assert exc_info.value.error_code == 400
@pytest.mark.asyncio
async def test_execute_sync_plan_raises_when_rust_unavailable(
monkeypatch: pytest.MonkeyPatch,
) -> None:
executor = _make_executor()
prepared_plan = _make_prepared_plan()
monkeypatch.setattr(chat_sync_mod.config, "executor_backend", "rust")
async def _fake_execute_sync_json(self: object, plan: ExecutionPlan) -> ExecutionRuntimeSyncResult:
assert plan.request_id == "req-test"
raise ExecutionRuntimeClientError("executor down")
monkeypatch.setattr(
chat_sync_mod.ExecutionRuntimeClient,
"execute_sync_json",
_fake_execute_sync_json,
)
with pytest.raises(ProviderNotAvailableException) as exc_info:
await executor._execute_sync_plan(
prepared_plan=prepared_plan,
provider=SimpleNamespace(name="provider"),
model="gpt-4.1",
)
assert exc_info.value.message == "执行器暂时不可用,请稍后重试"
assert exc_info.value.upstream_response == "executor down"
@pytest.mark.asyncio
async def test_execute_sync_plan_raises_when_remote_contract_is_ineligible(
monkeypatch: pytest.MonkeyPatch,
) -> None:
executor = _make_executor()
prepared_plan = _make_prepared_plan()
prepared_plan.contract.proxy = chat_sync_mod.ExecutionProxySnapshot(
enabled=True,
mode="tunnel",
label="relay-node",
)
monkeypatch.setattr(chat_sync_mod.config, "executor_backend", "rust")
async def _should_not_call_rust(self: object, plan: ExecutionPlan) -> ExecutionRuntimeSyncResult:
del self, plan
raise AssertionError("rust executor should not be called")
monkeypatch.setattr(
chat_sync_mod.ExecutionRuntimeClient,
"execute_sync_json",
_should_not_call_rust,
)
assert prepared_plan.remote_eligible is False
with pytest.raises(ProviderNotAvailableException) as exc_info:
await executor._execute_sync_plan(
prepared_plan=prepared_plan,
provider=SimpleNamespace(name="provider"),
model="gpt-4.1",
)
assert exc_info.value.message == "执行器暂时不可用,请稍后重试"
assert exc_info.value.upstream_response == (
"execution contract is not eligible for rust executor"
)
@@ -1,760 +0,0 @@
from __future__ import annotations
from collections.abc import AsyncGenerator
from types import SimpleNamespace
from typing import Any
import httpx
import pytest
import src.api.handlers.base.chat_handler_base as chatmod
import src.services.proxy_node.resolver as proxymod
from src.api.handlers.base.chat_handler_base import ChatHandlerBase
from src.api.handlers.base.stream_context import StreamContext
from src.core.exceptions import ProviderNotAvailableException
from src.services.request.execution_runtime_client import (
ExecutionRuntimeClientError,
ExecutionRuntimeStreamResult,
)
class _DummyAuthInfo:
auth_header = "authorization"
auth_value = "Bearer test"
decrypted_auth_config = None
def as_tuple(self) -> tuple[str, str]:
return self.auth_header, self.auth_value
class _PassBuilder:
def build(self, request_body: dict[str, Any], *args: Any, **kwargs: Any) -> Any:
return request_body, {"content-type": "application/json"}
class _DummyStreamResponseCtx:
def __init__(self) -> None:
self.closed = False
async def __aexit__(self, exc_type: object, exc: object, tb: object) -> None:
self.closed = True
class _FakeStreamProcessor:
def __init__(self) -> None:
self.prefetched_chunks: list[bytes] | None = None
self.response_ctx: _DummyStreamResponseCtx | None = None
async def prefetch_and_check_error(
self,
byte_iterator: Any,
provider: Any,
endpoint: Any,
ctx: Any,
max_prefetch_lines: int = 5,
max_prefetch_bytes: int = 65536,
) -> list[bytes]:
del provider, endpoint, ctx, max_prefetch_lines, max_prefetch_bytes
first = await anext(byte_iterator)
self.prefetched_chunks = [first]
return self.prefetched_chunks
async def create_response_stream(
self,
ctx: Any,
byte_iterator: Any,
response_ctx: _DummyStreamResponseCtx,
prefetched_chunks: list[bytes] | None = None,
*,
start_time: float | None = None,
) -> AsyncGenerator[bytes]:
del ctx, start_time
self.response_ctx = response_ctx
try:
for chunk in prefetched_chunks or []:
yield chunk
async for chunk in byte_iterator:
yield chunk
finally:
await response_ctx.__aexit__(None, None, None)
class _FakeParser:
def is_error_response(self, response_json: dict[str, Any]) -> bool:
del response_json
return False
class _FakeInternalUsage:
input_tokens = 3
output_tokens = 5
cache_read_tokens = 1
cache_write_tokens = 0
class _FakeInternalResponse:
def __init__(self) -> None:
self.id = "resp-sync"
self.model = ""
self.usage = _FakeInternalUsage()
class _FakeSourceNormalizer:
def response_to_internal(self, response_json: dict[str, Any]) -> _FakeInternalResponse:
assert response_json == {"id": "sync-1", "message": "hello"}
return _FakeInternalResponse()
class _FakeTargetNormalizer:
def stream_event_from_internal(
self,
event: dict[str, Any],
state: Any,
) -> list[dict[str, Any]]:
assert event == {"kind": "chunk"}
assert getattr(state, "message_id", "") == "resp-sync"
return [{"delta": "hello"}]
class _FakeRegistry:
def get_normalizer(self, format_id: str) -> Any:
if format_id == "provider:test":
return _FakeSourceNormalizer()
if format_id == "openai:chat":
return _FakeTargetNormalizer()
raise AssertionError(f"unexpected format: {format_id}")
class _DummyChatHandler(ChatHandlerBase):
FORMAT_ID = "openai:chat"
def __init__(self) -> None:
self.request_id = "req-test"
self.api_key = SimpleNamespace(id="user-key-1")
self._request_builder = _PassBuilder()
self.allowed_api_formats = ["openai:chat"]
self.api_family = None
self.endpoint_kind = None
self.start_time = 0.0
async def _convert_request(self, request: Any) -> Any:
return request
def _extract_usage(self, response: dict) -> dict[str, int]:
return {}
async def _get_mapped_model(
self,
source_model: str,
provider_id: str,
api_format: str | None = None,
) -> str | None:
del source_model, provider_id, api_format
return None
def apply_mapped_model(self, request_body: dict[str, Any], mapped_model: str) -> dict[str, Any]:
out = dict(request_body)
out["model"] = mapped_model
return out
def prepare_provider_request_body(self, request_body: dict[str, Any]) -> dict[str, Any]:
return dict(request_body)
def finalize_provider_request(
self,
request_body: dict[str, Any],
*,
mapped_model: str | None,
provider_api_format: str | None,
) -> dict[str, Any]:
del mapped_model, provider_api_format
return dict(request_body)
def get_model_for_url(
self,
request_body: dict[str, Any],
mapped_model: str | None,
) -> str | None:
return mapped_model or str(request_body.get("model") or "")
def _patch_stream_setup(
monkeypatch: pytest.MonkeyPatch,
*,
proxy_info: dict[str, Any] | None = None,
delegate_config: dict[str, Any] | None = None,
) -> None:
async def _fake_get_provider_auth(endpoint: Any, key: Any) -> _DummyAuthInfo:
del endpoint, key
return _DummyAuthInfo()
async def _fake_resolve_proxy_info(proxy_config: Any) -> Any:
del proxy_config
return proxy_info
async def _fake_resolve_delegate(proxy_config: Any) -> Any:
del proxy_config
return delegate_config
async def _fake_get_system_proxy() -> None:
return None
monkeypatch.setattr(chatmod, "get_provider_auth", _fake_get_provider_auth)
monkeypatch.setattr(
chatmod,
"get_provider_behavior",
lambda **kwargs: SimpleNamespace(
envelope=None,
same_format_variant=None,
cross_format_variant=None,
),
)
monkeypatch.setattr(chatmod, "build_provider_url", lambda *args, **kwargs: "https://upstream.test/v1/chat/completions")
monkeypatch.setattr(chatmod, "get_upstream_stream_policy", lambda *args, **kwargs: None)
monkeypatch.setattr(
chatmod,
"resolve_upstream_is_stream",
lambda *, client_is_stream, policy: client_is_stream,
)
monkeypatch.setattr(chatmod, "enforce_stream_mode_for_upstream", lambda *args, **kwargs: None)
monkeypatch.setattr(
chatmod,
"maybe_patch_request_with_prompt_cache_key",
lambda request_body, **kwargs: request_body,
)
monkeypatch.setattr(proxymod, "resolve_effective_proxy", lambda provider_proxy, key_proxy=None: None)
monkeypatch.setattr(proxymod, "resolve_proxy_info_async", _fake_resolve_proxy_info)
monkeypatch.setattr(proxymod, "get_proxy_label", lambda proxy_info: "direct")
monkeypatch.setattr(proxymod, "resolve_delegate_config_async", _fake_resolve_delegate)
monkeypatch.setattr(proxymod, "get_system_proxy_config_async", _fake_get_system_proxy)
monkeypatch.setattr(proxymod, "build_proxy_url_async", _fake_get_system_proxy)
async def _iter_chunks(chunks: list[bytes]) -> AsyncGenerator[bytes]:
for chunk in chunks:
yield chunk
@pytest.mark.asyncio
async def test_execute_stream_request_uses_rust_executor_when_available(
monkeypatch: pytest.MonkeyPatch,
) -> None:
_patch_stream_setup(monkeypatch)
monkeypatch.setattr(chatmod.config, "executor_backend", "rust")
handler = _DummyChatHandler()
stream_processor = _FakeStreamProcessor()
ctx = StreamContext(model="gpt-test", api_format="openai:chat")
ctx.client_api_format = "openai:chat"
provider = SimpleNamespace(
name="provider",
id="provider-1",
provider_type="",
proxy=None,
request_timeout=None,
)
endpoint = SimpleNamespace(id="endpoint-1", api_format="openai:chat", base_url="https://x")
key = SimpleNamespace(id="key-1", proxy=None)
candidate = SimpleNamespace(
request_candidate_id="cand-1",
mapping_matched_model=None,
needs_conversion=False,
output_limit=None,
)
dummy_ctx = _DummyStreamResponseCtx()
async def _fake_execute_stream(self: object, plan: object) -> ExecutionRuntimeStreamResult:
assert getattr(plan, "stream") is True
return ExecutionRuntimeStreamResult(
status_code=200,
headers={"content-type": "text/event-stream", "x-upstream-test": "true"},
byte_iterator=_iter_chunks(
[
b"data: {\"id\":\"chunk-1\"}\n\n",
b"data: [DONE]\n\n",
]
),
response_ctx=dummy_ctx,
)
monkeypatch.setattr(chatmod.ExecutionRuntimeClient, "execute_stream", _fake_execute_stream)
stream = await handler._execute_stream_request(
ctx,
stream_processor,
provider,
endpoint,
key,
{"model": "gpt-test", "messages": [{"role": "user", "content": "hello"}]},
{},
candidate=candidate,
)
received = [chunk async for chunk in stream]
assert received == [
b"data: {\"id\":\"chunk-1\"}\n\n",
b"data: [DONE]\n\n",
]
assert ctx.status_code == 200
assert ctx.response_headers["x-upstream-test"] == "true"
assert stream_processor.prefetched_chunks == [b"data: {\"id\":\"chunk-1\"}\n\n"]
assert dummy_ctx.closed is True
@pytest.mark.asyncio
async def test_execute_stream_request_uses_rust_sync_executor_for_non_stream_upstream(
monkeypatch: pytest.MonkeyPatch,
) -> None:
_patch_stream_setup(monkeypatch)
monkeypatch.setattr(chatmod.config, "executor_backend", "rust")
handler = _DummyChatHandler()
stream_processor = _FakeStreamProcessor()
stream_processor.on_streaming_start = None
ctx = StreamContext(model="gpt-test", api_format="openai:chat")
ctx.client_api_format = "openai:chat"
provider = SimpleNamespace(
name="provider",
id="provider-1",
provider_type="",
proxy=None,
request_timeout=None,
)
endpoint = SimpleNamespace(id="endpoint-1", api_format="openai:chat", base_url="https://x")
key = SimpleNamespace(id="key-1", proxy=None)
candidate = SimpleNamespace(
request_candidate_id="cand-1",
mapping_matched_model=None,
needs_conversion=False,
output_limit=None,
)
async def _fake_prepare_provider_request(self: object, **kwargs: Any) -> object:
del self, kwargs
return chatmod.ProviderRequestResult(
request_body={"model": "gpt-test", "messages": [{"role": "user", "content": "hello"}]},
url_model="gpt-test",
mapped_model=None,
envelope=None,
extra_headers={},
upstream_is_stream=False,
needs_conversion=False,
provider_api_format="provider:test",
client_api_format="openai:chat",
auth_info=_DummyAuthInfo(),
tls_profile=None,
)
async def _fake_execute_sync_json(self: object, plan: object) -> object:
del self
assert getattr(plan, "stream") is False
return SimpleNamespace(
status_code=200,
response_json={"id": "sync-1", "message": "hello"},
response_body_bytes=None,
headers={"content-type": "application/json"},
)
async def _should_not_call_stream(self: object, plan: object) -> ExecutionRuntimeStreamResult:
del self, plan
raise AssertionError("stream executor should not be used")
async def _should_not_get_http_client(*args: Any, **kwargs: Any) -> object:
raise AssertionError("python upstream client should not be used")
monkeypatch.setattr(
_DummyChatHandler,
"_prepare_provider_request",
_fake_prepare_provider_request,
)
monkeypatch.setattr(chatmod, "get_format_converter_registry", lambda: _FakeRegistry())
monkeypatch.setattr(
chatmod,
"iter_internal_response_as_stream_events",
lambda internal_resp: [{"kind": "chunk"}],
)
monkeypatch.setattr(chatmod, "get_parser_for_format", lambda _format: _FakeParser())
monkeypatch.setattr(chatmod.ExecutionRuntimeClient, "execute_sync_json", _fake_execute_sync_json)
monkeypatch.setattr(chatmod.ExecutionRuntimeClient, "execute_stream", _should_not_call_stream)
monkeypatch.setattr(
"src.clients.http_client.HTTPClientPool.get_upstream_client",
_should_not_get_http_client,
)
stream = await handler._execute_stream_request(
ctx,
stream_processor,
provider,
endpoint,
key,
{"model": "gpt-test", "messages": [{"role": "user", "content": "hello"}]},
{},
candidate=candidate,
)
received = [chunk async for chunk in stream]
assert received == [
b'data: {"delta": "hello"}\n\n',
b"data: [DONE]\n\n",
]
assert ctx.status_code == 200
assert ctx.input_tokens == 3
assert ctx.output_tokens == 5
assert ctx.cached_tokens == 1
@pytest.mark.asyncio
async def test_execute_stream_request_accepts_async_generator_stream_processor(
monkeypatch: pytest.MonkeyPatch,
) -> None:
_patch_stream_setup(monkeypatch)
monkeypatch.setattr(chatmod.config, "executor_backend", "rust")
handler = _DummyChatHandler()
stream_processor = _FakeStreamProcessor()
ctx = StreamContext(model="gpt-test", api_format="openai:chat")
ctx.client_api_format = "openai:chat"
provider = SimpleNamespace(name="provider", id="provider-1", provider_type="", proxy=None)
endpoint = SimpleNamespace(id="endpoint-1", api_format="openai:chat", base_url="https://x")
key = SimpleNamespace(id="key-1", proxy=None)
candidate = SimpleNamespace(
request_candidate_id="cand-1",
mapping_matched_model=None,
needs_conversion=False,
output_limit=None,
)
dummy_ctx = _DummyStreamResponseCtx()
async def _fake_execute_stream(self: object, plan: object) -> ExecutionRuntimeStreamResult:
assert getattr(plan, "stream") is True
return ExecutionRuntimeStreamResult(
status_code=200,
headers={"content-type": "text/event-stream"},
byte_iterator=_iter_chunks(
[
b"data: {\"id\":\"chunk-1\"}\n\n",
b"data: [DONE]\n\n",
]
),
response_ctx=dummy_ctx,
)
monkeypatch.setattr(chatmod.ExecutionRuntimeClient, "execute_stream", _fake_execute_stream)
stream = await handler._execute_stream_request(
ctx,
stream_processor,
provider,
endpoint,
key,
{"model": "gpt-test", "messages": [{"role": "user", "content": "hello"}]},
{},
candidate=candidate,
)
received = [chunk async for chunk in stream]
assert received == [
b"data: {\"id\":\"chunk-1\"}\n\n",
b"data: [DONE]\n\n",
]
assert dummy_ctx.closed is True
@pytest.mark.asyncio
async def test_execute_stream_request_allows_tunnel_delegate_for_rust(
monkeypatch: pytest.MonkeyPatch,
) -> None:
_patch_stream_setup(
monkeypatch,
proxy_info={"node_id": "node-1", "node_name": "relay-node", "mode": "tunnel"},
delegate_config={"tunnel": True, "node_id": "node-1"},
)
monkeypatch.setattr(chatmod.config, "executor_backend", "rust")
handler = _DummyChatHandler()
stream_processor = _FakeStreamProcessor()
ctx = StreamContext(model="gpt-test", api_format="openai:chat")
ctx.client_api_format = "openai:chat"
provider = SimpleNamespace(
name="provider",
id="provider-1",
provider_type="",
proxy={"enabled": True, "node_id": "node-1"},
)
endpoint = SimpleNamespace(id="endpoint-1", api_format="openai:chat", base_url="https://x")
key = SimpleNamespace(id="key-1", proxy=None)
candidate = SimpleNamespace(
request_candidate_id="cand-1",
mapping_matched_model=None,
needs_conversion=False,
output_limit=None,
)
dummy_ctx = _DummyStreamResponseCtx()
async def _fake_execute_stream(self: object, plan: object) -> ExecutionRuntimeStreamResult:
assert getattr(plan, "proxy") is not None
assert getattr(plan.proxy, "mode") == "tunnel"
assert getattr(plan.proxy, "node_id") == "node-1"
return ExecutionRuntimeStreamResult(
status_code=200,
headers={"content-type": "text/event-stream"},
byte_iterator=_iter_chunks([b"data: [DONE]\n\n"]),
response_ctx=dummy_ctx,
)
monkeypatch.setattr(chatmod.ExecutionRuntimeClient, "execute_stream", _fake_execute_stream)
stream = await handler._execute_stream_request(
ctx,
stream_processor,
provider,
endpoint,
key,
{"model": "gpt-test", "messages": [{"role": "user", "content": "hello"}]},
{},
candidate=candidate,
)
received = [chunk async for chunk in stream]
assert received == [b"data: [DONE]\n\n"]
assert dummy_ctx.closed is True
@pytest.mark.asyncio
async def test_execute_stream_request_allows_tls_profile_for_rust(
monkeypatch: pytest.MonkeyPatch,
) -> None:
_patch_stream_setup(monkeypatch)
monkeypatch.setattr(chatmod.config, "executor_backend", "rust")
handler = _DummyChatHandler()
stream_processor = _FakeStreamProcessor()
ctx = StreamContext(model="gpt-test", api_format="openai:chat")
ctx.client_api_format = "openai:chat"
provider = SimpleNamespace(name="provider", id="provider-1", provider_type="", proxy=None)
endpoint = SimpleNamespace(id="endpoint-1", api_format="openai:chat", base_url="https://x")
key = SimpleNamespace(id="key-1", proxy=None)
candidate = SimpleNamespace(
request_candidate_id="cand-1",
mapping_matched_model=None,
needs_conversion=False,
output_limit=None,
)
dummy_ctx = _DummyStreamResponseCtx()
async def _fake_prepare_provider_request(self: object, **kwargs: Any) -> object:
del self, kwargs
return chatmod.ProviderRequestResult(
request_body={"model": "gpt-test", "messages": [{"role": "user", "content": "hello"}]},
url_model="gpt-test",
mapped_model=None,
envelope=None,
extra_headers={},
upstream_is_stream=True,
needs_conversion=False,
provider_api_format="openai:chat",
client_api_format="openai:chat",
auth_info=_DummyAuthInfo(),
tls_profile="claude_code_nodejs",
)
monkeypatch.setattr(
_DummyChatHandler,
"_prepare_provider_request",
_fake_prepare_provider_request,
)
async def _fake_execute_stream(self: object, plan: object) -> ExecutionRuntimeStreamResult:
assert getattr(plan, "tls_profile") == "claude_code_nodejs"
return ExecutionRuntimeStreamResult(
status_code=200,
headers={"content-type": "text/event-stream"},
byte_iterator=_iter_chunks([b"data: [DONE]\n\n"]),
response_ctx=dummy_ctx,
)
monkeypatch.setattr(chatmod.ExecutionRuntimeClient, "execute_stream", _fake_execute_stream)
stream = await handler._execute_stream_request(
ctx,
stream_processor,
provider,
endpoint,
key,
{"model": "gpt-test", "messages": [{"role": "user", "content": "hello"}]},
{},
candidate=candidate,
)
received = [chunk async for chunk in stream]
assert received == [b"data: [DONE]\n\n"]
assert dummy_ctx.closed is True
@pytest.mark.asyncio
async def test_execute_stream_request_turns_rust_upstream_error_into_http_status_error(
monkeypatch: pytest.MonkeyPatch,
) -> None:
_patch_stream_setup(monkeypatch)
monkeypatch.setattr(chatmod.config, "executor_backend", "rust")
handler = _DummyChatHandler()
stream_processor = _FakeStreamProcessor()
ctx = StreamContext(model="gpt-test", api_format="openai:chat")
ctx.client_api_format = "openai:chat"
provider = SimpleNamespace(name="provider", id="provider-1", provider_type="", proxy=None)
endpoint = SimpleNamespace(id="endpoint-1", api_format="openai:chat", base_url="https://x")
key = SimpleNamespace(id="key-1", proxy=None)
candidate = SimpleNamespace(
request_candidate_id="cand-1",
mapping_matched_model=None,
needs_conversion=False,
output_limit=None,
)
dummy_ctx = _DummyStreamResponseCtx()
async def _fake_execute_stream(self: object, plan: object) -> ExecutionRuntimeStreamResult:
assert getattr(plan, "stream") is True
return ExecutionRuntimeStreamResult(
status_code=429,
headers={"content-type": "application/json"},
byte_iterator=_iter_chunks([b'{"error":{"message":"slow down"}}']),
response_ctx=dummy_ctx,
)
monkeypatch.setattr(chatmod.ExecutionRuntimeClient, "execute_stream", _fake_execute_stream)
with pytest.raises(httpx.HTTPStatusError) as exc_info:
await handler._execute_stream_request(
ctx,
stream_processor,
provider,
endpoint,
key,
{"model": "gpt-test", "messages": [{"role": "user", "content": "hello"}]},
{},
candidate=candidate,
)
assert exc_info.value.response.status_code == 429
assert "slow down" in exc_info.value.upstream_response # type: ignore[attr-defined]
assert dummy_ctx.closed is True
@pytest.mark.asyncio
async def test_execute_stream_request_raises_when_rust_unavailable(
monkeypatch: pytest.MonkeyPatch,
) -> None:
_patch_stream_setup(monkeypatch)
monkeypatch.setattr(chatmod.config, "executor_backend", "rust")
handler = _DummyChatHandler()
ctx = StreamContext(model="gpt-test", api_format="openai:chat")
ctx.client_api_format = "openai:chat"
provider = SimpleNamespace(name="provider", id="provider-1", provider_type="", proxy=None)
endpoint = SimpleNamespace(id="endpoint-1", api_format="openai:chat", base_url="https://x")
key = SimpleNamespace(id="key-1", proxy=None)
candidate = SimpleNamespace(
request_candidate_id="cand-1",
mapping_matched_model=None,
needs_conversion=False,
output_limit=None,
)
async def _fake_execute_stream(self: object, plan: object) -> ExecutionRuntimeStreamResult:
del plan
raise ExecutionRuntimeClientError("executor down")
async def _fake_get_upstream_client(*args: Any, **kwargs: Any) -> object:
raise AssertionError("python fallback should not be used")
monkeypatch.setattr(chatmod.ExecutionRuntimeClient, "execute_stream", _fake_execute_stream)
monkeypatch.setattr(
"src.clients.http_client.HTTPClientPool.get_upstream_client",
_fake_get_upstream_client,
)
with pytest.raises(ProviderNotAvailableException) as exc_info:
await handler._execute_stream_request(
ctx,
object(),
provider,
endpoint,
key,
{"model": "gpt-test", "messages": [{"role": "user", "content": "hello"}]},
{},
candidate=candidate,
)
assert exc_info.value.message == "执行器暂时不可用,请稍后重试"
assert exc_info.value.upstream_response == "executor down"
@pytest.mark.asyncio
async def test_execute_stream_request_raises_when_remote_contract_is_ineligible(
monkeypatch: pytest.MonkeyPatch,
) -> None:
_patch_stream_setup(monkeypatch)
monkeypatch.setattr(chatmod.config, "executor_backend", "rust")
handler = _DummyChatHandler()
ctx = StreamContext(model="gpt-test", api_format="openai:chat")
ctx.client_api_format = "openai:chat"
provider = SimpleNamespace(name="provider", id="provider-1", provider_type="", proxy=None)
endpoint = SimpleNamespace(id="endpoint-1", api_format="openai:chat", base_url="https://x")
key = SimpleNamespace(id="key-1", proxy=None)
candidate = SimpleNamespace(
request_candidate_id="cand-1",
mapping_matched_model=None,
needs_conversion=False,
output_limit=None,
)
async def _should_not_call_rust(self: object, plan: object) -> ExecutionRuntimeStreamResult:
del self, plan
raise AssertionError("rust executor should not be called")
async def _fake_get_upstream_client(*args: Any, **kwargs: Any) -> object:
raise AssertionError("python fallback should not be used")
monkeypatch.setattr(chatmod, "is_remote_execution_runtime_contract_eligible", lambda plan: False)
monkeypatch.setattr(chatmod.ExecutionRuntimeClient, "execute_stream", _should_not_call_rust)
monkeypatch.setattr(
"src.clients.http_client.HTTPClientPool.get_upstream_client",
_fake_get_upstream_client,
)
with pytest.raises(ProviderNotAvailableException) as exc_info:
await handler._execute_stream_request(
ctx,
object(),
provider,
endpoint,
key,
{"model": "gpt-test", "messages": [{"role": "user", "content": "hello"}]},
{},
candidate=candidate,
)
assert exc_info.value.message == "执行器暂时不可用,请稍后重试"
assert exc_info.value.upstream_response == (
"execution contract is not eligible for rust executor"
)
@@ -1,38 +0,0 @@
from typing import Any
from src.api.handlers.base.cli_event_mixin import CliEventMixin
from src.api.handlers.base.stream_context import StreamContext
class _DummyCliEventHandler(CliEventMixin):
request_id = "req-test"
def _process_event_data(
self,
ctx: StreamContext,
event_type: str,
data: dict[str, Any],
) -> None:
del ctx, event_type, data
def test_convert_sse_line_drops_done_for_openai_cli() -> None:
handler = _DummyCliEventHandler()
ctx = StreamContext(model="gpt-test", api_format="openai:cli")
ctx.client_api_format = "openai:cli"
lines, converted = handler._convert_sse_line(ctx, "data: [DONE]", [])
assert lines == []
assert converted == []
def test_convert_sse_line_keeps_done_for_openai_chat() -> None:
handler = _DummyCliEventHandler()
ctx = StreamContext(model="gpt-test", api_format="openai:chat")
ctx.client_api_format = "openai:chat"
lines, converted = handler._convert_sse_line(ctx, "data: [DONE]", [])
assert lines == ["data: [DONE]"]
assert converted == []
@@ -1,264 +0,0 @@
"""
CliMessageHandlerBase._convert_sse_line 单元测试
测试覆盖:
1. 基本转换(空行、非 data 行、JSON 解析失败)
2. 一入多出场景
3. 状态追踪
4. 错误处理
"""
import json
from unittest.mock import MagicMock, patch
import pytest
from src.api.handlers.base.stream_context import StreamContext
from src.core.api_format.conversion.stream_state import StreamState
# Mock CliMessageHandlerBase 用于测试
class MockCliHandler:
"""Mock handler for testing _convert_sse_line"""
def _convert_sse_line(
self,
ctx: StreamContext,
line: str,
events: list,
) -> list[str]:
"""复制自 CliMessageHandlerBase._convert_sse_line"""
from src.core.api_format.conversion import (
format_conversion_registry,
register_default_normalizers,
)
register_default_normalizers()
# 如果是空行或特殊控制行,直接返回
if not line or line.strip() == "" or line == "data: [DONE]":
return [line] if line else []
# 如果不是 data 行,直接透传
if not line.startswith("data:"):
return [line]
# 提取 data 内容
data_content = line[5:].strip()
# 尝试解析 JSON
try:
data_obj = json.loads(data_content)
except json.JSONDecodeError:
return [line]
# 初始化流式转换状态
# 使用客户端请求的模型名(ctx.model),而非映射后的模型名(ctx.mapped_model
if ctx.stream_conversion_state is None:
ctx.stream_conversion_state = StreamState(
model=ctx.model,
message_id=ctx.response_id or ctx.request_id,
)
provider_format = ctx.provider_api_format or ""
client_format = ctx.client_api_format or ""
try:
converted_events = format_conversion_registry.convert_stream_chunk(
data_obj,
provider_format,
client_format,
state=ctx.stream_conversion_state,
)
result = []
for evt in converted_events:
result.append(f"data: {json.dumps(evt, ensure_ascii=False)}")
return result
except Exception:
return [line]
class TestConvertSseLineBasic:
"""基本转换测试"""
def test_empty_line_returns_empty_list(self) -> None:
handler = MockCliHandler()
ctx = StreamContext(model="test", api_format="openai:chat")
result = handler._convert_sse_line(ctx, "", [])
assert result == []
def test_whitespace_line_returns_line(self) -> None:
handler = MockCliHandler()
ctx = StreamContext(model="test", api_format="openai:chat")
result = handler._convert_sse_line(ctx, " ", [])
assert result == [" "]
def test_done_marker_returns_as_is(self) -> None:
handler = MockCliHandler()
ctx = StreamContext(model="test", api_format="openai:chat")
result = handler._convert_sse_line(ctx, "data: [DONE]", [])
assert result == ["data: [DONE]"]
def test_non_data_line_passthrough(self) -> None:
handler = MockCliHandler()
ctx = StreamContext(model="test", api_format="openai:chat")
result = handler._convert_sse_line(ctx, "event: message_start", [])
assert result == ["event: message_start"]
def test_invalid_json_passthrough(self) -> None:
handler = MockCliHandler()
ctx = StreamContext(model="test", api_format="openai:chat")
result = handler._convert_sse_line(ctx, "data: {invalid json}", [])
assert result == ["data: {invalid json}"]
class TestConvertSseLineWithMockConverter:
"""使用 Mock 转换器的测试"""
def test_same_format_returns_original(self) -> None:
"""同格式无需转换"""
handler = MockCliHandler()
ctx = StreamContext(model="test", api_format="openai:chat")
ctx.provider_api_format = "openai:chat"
ctx.client_api_format = "openai:chat"
chunk = {"choices": [{"delta": {"content": "hello"}}]}
line = f"data: {json.dumps(chunk)}"
result = handler._convert_sse_line(ctx, line, [])
assert len(result) == 1
assert json.loads(result[0][6:]) == chunk
def test_state_initialization(self) -> None:
"""测试状态自动初始化
流式转换状态应使用用户请求的原始模型名(ctx.model),
而非映射后的模型名(ctx.mapped_model),确保返回给客户端的响应使用原始模型名。
"""
handler = MockCliHandler()
ctx = StreamContext(model="gpt-4", api_format="openai:chat")
ctx.provider_api_format = "openai:chat"
ctx.client_api_format = "openai:chat"
ctx.mapped_model = "claude-3-5-sonnet" # 映射后的模型名(发给上游的)
ctx.request_id = "req_123"
chunk = {"choices": [{"delta": {"content": "test"}}]}
line = f"data: {json.dumps(chunk)}"
handler._convert_sse_line(ctx, line, [])
# 验证状态已初始化,使用用户请求的原始模型名
assert ctx.stream_conversion_state is not None
assert ctx.stream_conversion_state.model == "gpt-4" # 应使用原始模型名,非 mapped_model
assert ctx.stream_conversion_state.message_id == "req_123"
class TestConvertSseLineOneInManyOut:
"""一入多出测试(需要注册转换器)"""
@pytest.fixture(autouse=True)
def setup_converters(self):
"""确保 Canonical normalizers 已注册"""
from src.core.api_format.conversion import register_default_normalizers
register_default_normalizers()
yield
def test_openai_to_claude_conversion(self) -> None:
"""测试 OpenAI -> Claude 流式转换"""
handler = MockCliHandler()
ctx = StreamContext(model="gpt-4", api_format="openai:chat")
ctx.provider_api_format = "openai:chat"
ctx.client_api_format = "claude:chat"
ctx.mapped_model = "claude-3-5-sonnet"
ctx.request_id = "req_test"
# 第一个 chunk:带 role
chunk1 = {
"id": "chatcmpl-123",
"object": "chat.completion.chunk",
"choices": [{"index": 0, "delta": {"role": "assistant"}, "finish_reason": None}],
}
line1 = f"data: {json.dumps(chunk1)}"
result1 = handler._convert_sse_line(ctx, line1, [])
# 应返回 message_start 事件
assert len(result1) >= 1
first_event = json.loads(result1[0][6:])
assert first_event.get("type") == "message_start"
def test_claude_to_openai_conversion(self) -> None:
"""测试 Claude -> OpenAI 流式转换"""
handler = MockCliHandler()
ctx = StreamContext(model="claude-3-5-sonnet", api_format="claude:chat")
ctx.provider_api_format = "claude:chat"
ctx.client_api_format = "openai:chat"
ctx.mapped_model = "gpt-4"
ctx.request_id = "msg_test"
# Claude message_start 事件
event = {"type": "message_start", "message": {"id": "msg_123", "role": "assistant"}}
line = f"data: {json.dumps(event)}"
result = handler._convert_sse_line(ctx, line, [])
# 应返回 OpenAI 格式的 chunk
assert len(result) >= 1
chunk = json.loads(result[0][6:])
assert "choices" in chunk
def test_multiple_chunks_state_persistence(self) -> None:
"""测试多个 chunk 之间状态持久化"""
handler = MockCliHandler()
ctx = StreamContext(model="gpt-4", api_format="openai:chat")
ctx.provider_api_format = "openai:chat"
ctx.client_api_format = "claude:chat"
ctx.mapped_model = "claude-3-5-sonnet"
# 第一个 chunk
chunk1 = {"choices": [{"delta": {"role": "assistant"}}]}
handler._convert_sse_line(ctx, f"data: {json.dumps(chunk1)}", [])
state_after_first = ctx.stream_conversion_state
# 第二个 chunk
chunk2 = {"choices": [{"delta": {"content": "hello"}}]}
handler._convert_sse_line(ctx, f"data: {json.dumps(chunk2)}", [])
# 状态应该是同一个对象
assert ctx.stream_conversion_state is state_after_first
class TestStreamContextIntegration:
"""StreamContext 集成测试"""
def test_stream_conversion_state_reset_on_retry(self) -> None:
"""测试重试时重置流式转换状态"""
ctx = StreamContext(model="test", api_format="openai:chat")
ctx.stream_conversion_state = StreamState(model="test", message_id="123")
ctx.reset_for_retry()
assert ctx.stream_conversion_state is None
def test_stream_conversion_state_field_exists(self) -> None:
"""测试 StreamContext 有 stream_conversion_state 字段"""
ctx = StreamContext(model="test", api_format="openai:chat")
assert hasattr(ctx, "stream_conversion_state")
assert ctx.stream_conversion_state is None
@@ -1,161 +0,0 @@
from __future__ import annotations
import asyncio
from collections.abc import AsyncGenerator
import pytest
from src.api.handlers.base.cli_monitor_mixin import CliMonitorMixin
from src.api.handlers.base.stream_context import StreamContext
class _DummyMonitor(CliMonitorMixin):
pass
class _RequestStub:
def __init__(self, responses: list[bool | Exception]):
self._responses = responses
async def is_disconnected(self) -> bool:
if self._responses:
value = self._responses.pop(0)
else:
value = False
if isinstance(value, Exception):
raise value
return value
async def _cancel_immediately() -> AsyncGenerator[bytes, None]:
if False:
yield b""
raise asyncio.CancelledError()
async def _yield_once_then_cancel(ctx: StreamContext) -> AsyncGenerator[bytes, None]:
ctx.append_text("partial output")
yield b"data: chunk\n\n"
raise asyncio.CancelledError()
async def _yield_once_then_hang(ctx: StreamContext) -> AsyncGenerator[bytes, None]:
ctx.append_text("partial output")
yield b"data: chunk\n\n"
await asyncio.sleep(3600)
async def _yield_after_delay_then_complete() -> AsyncGenerator[bytes, None]:
await asyncio.sleep(0.25)
yield b"data: first\n\n"
@pytest.mark.asyncio
async def test_create_monitored_stream_marks_client_disconnected_when_confirmed() -> None:
monitor = _DummyMonitor()
monitor.CANCEL_DISCONNECT_RETRY_DELAYS_SECONDS = ()
ctx = StreamContext(model="test-model", api_format="openai:cli", request_id="req-client")
request = _RequestStub([True])
monitored = monitor._create_monitored_stream(ctx, _cancel_immediately(), request)
with pytest.raises(asyncio.CancelledError):
async for _ in monitored:
pass
assert ctx.status_code == 499
assert ctx.error_message == "client_disconnected"
assert "cancel_origin=client_disconnected" in (ctx.upstream_response or "")
@pytest.mark.asyncio
async def test_create_monitored_stream_marks_server_cancelled_when_confirmed_connected() -> None:
monitor = _DummyMonitor()
monitor.CANCEL_DISCONNECT_RETRY_DELAYS_SECONDS = ()
ctx = StreamContext(model="test-model", api_format="openai:cli", request_id="req-server")
request = _RequestStub([False])
monitored = monitor._create_monitored_stream(ctx, _cancel_immediately(), request)
with pytest.raises(asyncio.CancelledError):
async for _ in monitored:
pass
assert ctx.status_code == 503
assert ctx.error_message == "server_cancelled"
assert "cancel_origin=server_cancelled" in (ctx.upstream_response or "")
@pytest.mark.asyncio
async def test_create_monitored_stream_marks_cancelled_unknown_when_disconnect_check_uncertain() -> (
None
):
monitor = _DummyMonitor()
monitor.CANCEL_DISCONNECT_RETRY_DELAYS_SECONDS = ()
ctx = StreamContext(model="test-model", api_format="openai:cli", request_id="req-unknown")
request = _RequestStub([asyncio.TimeoutError()])
monitored = monitor._create_monitored_stream(ctx, _cancel_immediately(), request)
with pytest.raises(asyncio.CancelledError):
async for _ in monitored:
pass
assert ctx.status_code == 503
assert ctx.error_message == "cancelled_unknown"
assert "cancel_origin=cancelled_unknown" in (ctx.upstream_response or "")
@pytest.mark.asyncio
async def test_create_monitored_stream_estimates_output_tokens_before_unknown_cancel_log() -> None:
monitor = _DummyMonitor()
monitor.CANCEL_DISCONNECT_RETRY_DELAYS_SECONDS = ()
ctx = StreamContext(model="test-model", api_format="openai:cli", request_id="req-estimate")
request = _RequestStub([asyncio.TimeoutError()])
monitored = monitor._create_monitored_stream(ctx, _yield_once_then_cancel(ctx), request)
with pytest.raises(asyncio.CancelledError):
async for _ in monitored:
pass
expected_output_tokens = max(1, len("partial output") // 4)
assert ctx.status_code == 503
assert ctx.error_message == "cancelled_unknown"
assert ctx.output_tokens == expected_output_tokens
assert f"output_tokens={expected_output_tokens}" in (ctx.upstream_response or "")
@pytest.mark.asyncio
async def test_create_monitored_stream_marks_idle_timeout_before_worker_timeout() -> None:
monitor = _DummyMonitor()
monitor.CANCEL_DISCONNECT_RETRY_DELAYS_SECONDS = ()
monitor.STREAM_IDLE_TIMEOUT_SECONDS = 1.0
ctx = StreamContext(model="test-model", api_format="openai:cli", request_id="req-idle-timeout")
monitored = monitor._create_monitored_stream(ctx, _yield_once_then_hang(ctx), None)
with pytest.raises(asyncio.CancelledError):
async for _ in monitored:
pass
expected_output_tokens = max(1, len("partial output") // 4)
assert ctx.status_code == 504
assert ctx.error_message == "stream_idle_timeout"
assert ctx.output_tokens == expected_output_tokens
assert "cancel_origin=stream_idle_timeout" in (ctx.upstream_response or "")
@pytest.mark.asyncio
async def test_create_monitored_stream_does_not_idle_timeout_before_first_chunk() -> None:
monitor = _DummyMonitor()
monitor.CANCEL_DISCONNECT_RETRY_DELAYS_SECONDS = ()
monitor.STREAM_IDLE_TIMEOUT_SECONDS = 0.05
ctx = StreamContext(model="test-model", api_format="openai:cli", request_id="req-first-chunk")
monitored = monitor._create_monitored_stream(ctx, _yield_after_delay_then_complete(), None)
chunks = [chunk async for chunk in monitored]
assert chunks == [b"data: first\n\n"]
assert ctx.status_code == 200
assert ctx.error_message is None
@@ -1,74 +0,0 @@
from __future__ import annotations
import pytest
from src.api.handlers.base.cli_stream_mixin import CliStreamMixin
from src.api.handlers.base.cli_sync_mixin import CliSyncMixin
class _StopExecution(Exception):
pass
class _DummySyncHandler(CliSyncMixin):
FORMAT_ID = "openai:cli"
def __init__(self) -> None:
self.allowed_api_formats = ["openai:compact"]
self.primary_api_format = "openai:compact"
self.pending_calls: list[dict[str, object]] = []
def extract_model_from_request(
self, request_body: dict[str, object], path_params: dict[str, object] | None
) -> str:
return str(request_body.get("model") or "unknown")
def _create_pending_usage(self, **kwargs: object) -> bool:
self.pending_calls.append(kwargs)
raise _StopExecution()
class _DummyStreamHandler(CliStreamMixin):
FORMAT_ID = "openai:cli"
def __init__(self) -> None:
self.allowed_api_formats = ["openai:compact"]
self.primary_api_format = "openai:compact"
self.pending_calls: list[dict[str, object]] = []
def extract_model_from_request(
self, request_body: dict[str, object], path_params: dict[str, object] | None
) -> str:
return str(request_body.get("model") or "unknown")
def _create_pending_usage(self, **kwargs: object) -> bool:
self.pending_calls.append(kwargs)
raise _StopExecution()
@pytest.mark.asyncio
async def test_sync_pending_usage_uses_primary_api_format() -> None:
handler = _DummySyncHandler()
with pytest.raises(_StopExecution):
await handler.process_sync( # type: ignore[misc]
original_request_body={"model": "gpt-5.3-codex"},
original_headers={},
)
assert handler.pending_calls
assert handler.pending_calls[0]["api_format"] == "openai:compact"
@pytest.mark.asyncio
async def test_stream_pending_usage_uses_primary_api_format() -> None:
handler = _DummyStreamHandler()
with pytest.raises(_StopExecution):
await handler.process_stream( # type: ignore[misc]
original_request_body={"model": "gpt-5.3-codex"},
original_headers={},
)
assert handler.pending_calls
assert handler.pending_calls[0]["api_format"] == "openai:compact"
@@ -1,155 +0,0 @@
from __future__ import annotations
import copy
from types import SimpleNamespace
from typing import Any
import pytest
import src.api.handlers.base.cli_request_mixin as request_mixmod
from src.api.handlers.base.cli_request_mixin import CliRequestMixin
from src.api.handlers.base.cli_stream_mixin import CliStreamMixin
from src.api.handlers.base.stream_context import StreamContext
from src.services.task.request_state import MutableRequestBodyState
class _StopBuild(Exception):
pass
class _DummyAuthInfo:
auth_header = "authorization"
auth_value = "Bearer test"
decrypted_auth_config = None
def as_tuple(self) -> tuple[str, str]:
return self.auth_header, self.auth_value
class _CaptureBuilder:
def __init__(self) -> None:
self.request_body: dict[str, Any] | None = None
def build(self, request_body: dict[str, Any], *args: Any, **kwargs: Any) -> Any:
self.request_body = request_body
raise _StopBuild()
class _DummyCliStreamHandler(CliRequestMixin, CliStreamMixin):
FORMAT_ID = "openai:cli"
def __init__(self) -> None:
self.primary_api_format = "openai:cli"
self.request_id = "req-test"
self.api_key = SimpleNamespace(id="user-key-1")
self._request_builder = _CaptureBuilder()
async def _get_mapped_model(self, source_model: str, provider_id: str) -> str | None:
return None
def apply_mapped_model(self, request_body: dict[str, Any], mapped_model: str) -> dict[str, Any]:
out = dict(request_body)
out["model"] = mapped_model
return out
def prepare_provider_request_body(self, request_body: dict[str, Any]) -> dict[str, Any]:
request_body["input"][0]["content"][0]["text"] = "prepared"
return request_body
def finalize_provider_request(
self,
request_body: dict[str, Any],
*,
mapped_model: str | None,
provider_api_format: str | None,
) -> dict[str, Any]:
request_body["input"][0]["content"].append({"type": "input_text", "text": "finalized"})
return request_body
def get_model_for_url(
self,
request_body: dict[str, Any],
mapped_model: str | None,
) -> str | None:
return mapped_model or str(request_body.get("model") or "")
@pytest.mark.asyncio
async def test_execute_stream_request_does_not_mutate_original_request_body(
monkeypatch: pytest.MonkeyPatch,
) -> None:
async def _fake_get_provider_auth(endpoint: Any, key: Any) -> _DummyAuthInfo:
return _DummyAuthInfo()
monkeypatch.setattr(request_mixmod, "get_provider_auth", _fake_get_provider_auth)
monkeypatch.setattr(
request_mixmod,
"get_provider_behavior",
lambda **kwargs: SimpleNamespace(
envelope=None,
same_format_variant=None,
cross_format_variant=None,
),
)
monkeypatch.setattr(request_mixmod, "get_upstream_stream_policy", lambda *args, **kwargs: None)
monkeypatch.setattr(
request_mixmod,
"resolve_upstream_is_stream",
lambda *, client_is_stream, policy: client_is_stream,
)
monkeypatch.setattr(
request_mixmod,
"enforce_stream_mode_for_upstream",
lambda *args, **kwargs: None,
)
monkeypatch.setattr(
request_mixmod,
"maybe_patch_request_with_prompt_cache_key",
lambda request_body, **kwargs: request_body,
)
handler = _DummyCliStreamHandler()
ctx = StreamContext(model="gpt-test", api_format="openai:cli")
ctx.client_api_format = "openai:cli"
provider = SimpleNamespace(name="provider", id="provider-1", provider_type="", proxy=None)
endpoint = SimpleNamespace(
id="endpoint-1",
api_format="openai:cli",
base_url="https://x",
custom_path=None,
)
key = SimpleNamespace(id="key-1", proxy=None)
candidate = SimpleNamespace(
mapping_matched_model=None, needs_conversion=False, output_limit=None
)
original_request_body = {
"model": "gpt-test",
"input": [
{
"role": "user",
"content": [
{"type": "input_text", "text": "hello"},
],
}
],
}
snapshot = copy.deepcopy(original_request_body)
request_state = MutableRequestBodyState(original_request_body)
with pytest.raises(_StopBuild):
await handler._execute_stream_request(
ctx,
provider,
endpoint,
key,
request_state.build_attempt_body(),
{},
candidate=candidate,
)
assert original_request_body == snapshot
assert handler._request_builder.request_body is not None
assert handler._request_builder.request_body["input"][0]["content"][0]["text"] == "prepared"
assert handler._request_builder.request_body["input"][0]["content"][-1]["text"] == "finalized"
@@ -1,821 +0,0 @@
from __future__ import annotations
import json
from collections.abc import AsyncGenerator
from types import SimpleNamespace
from typing import Any
import pytest
import src.api.handlers.base.cli_stream_mixin as cli_stream_mod
import src.api.handlers.base.cli_sync_mixin as cli_sync_mod
import src.services.proxy_node.resolver as proxymod
import src.services.task as taskmod
from src.api.handlers.base.cli_stream_mixin import CliStreamMixin
from src.api.handlers.base.cli_sync_mixin import CliSyncMixin
from src.api.handlers.base.stream_context import StreamContext
from src.core.exceptions import ProviderNotAvailableException
from src.services.request.execution_runtime_client import (
ExecutionRuntimeClientError,
ExecutionRuntimeStreamResult,
ExecutionRuntimeSyncResult,
)
class _DummyParser:
def extract_usage_from_response(self, response: dict[str, Any]) -> dict[str, int]:
del response
return {
"input_tokens": 0,
"output_tokens": 0,
"cache_read_tokens": 0,
"cache_creation_tokens": 0,
}
def extract_text_content(self, response: dict[str, Any]) -> str:
return str(response.get("id") or "")
class _DummyTelemetry:
async def record_success(self, **kwargs: Any) -> int:
del kwargs
return 0
async def record_failure(self, **kwargs: Any) -> None:
del kwargs
class _DummySyncHandler(CliSyncMixin):
FORMAT_ID = "openai:cli"
def __init__(self, *, upstream_is_stream: bool = False) -> None:
self.db = None
self.redis = None
self.user = SimpleNamespace(id="user-1")
self.api_key = SimpleNamespace(id="user-key-1")
self.request_id = "req-cli-sync"
self.client_ip = "127.0.0.1"
self.user_agent = "pytest"
self.start_time = 0.0
self.allowed_api_formats = ["openai:cli"]
self.primary_api_format = "openai:cli"
self.api_family = None
self.endpoint_kind = None
self.telemetry = _DummyTelemetry()
self.perf_metrics = None
self._parser = _DummyParser()
self._upstream_is_stream = upstream_is_stream
@property
def parser(self) -> _DummyParser:
return self._parser
def _create_pending_usage(self, **kwargs: object) -> bool:
del kwargs
return True
def _build_request_metadata(self, http_request: Any | None = None) -> dict[str, Any]:
del http_request
return {}
def _merge_scheduling_metadata(
self,
request_metadata: dict[str, Any] | None,
**kwargs: Any,
) -> dict[str, Any]:
del kwargs
return dict(request_metadata or {})
def _resolve_capability_requirements(
self,
model_name: str,
request_headers: dict[str, str] | None = None,
request_body: dict[str, Any] | None = None,
) -> dict[str, bool]:
del model_name, request_headers, request_body
return {}
async def _resolve_preferred_key_ids(
self,
model_name: str,
request_body: dict[str, Any] | None = None,
) -> list[str] | None:
del model_name, request_body
return None
def extract_model_from_request(
self,
request_body: dict[str, Any],
path_params: dict[str, Any] | None = None,
) -> str:
del path_params
return str(request_body.get("model") or "unknown")
async def _get_mapped_model(self, source_model: str, provider_id: str) -> str | None:
del source_model, provider_id
return None
async def _build_upstream_request(self, **kwargs: Any) -> Any:
payload = dict(kwargs["request_body"])
return SimpleNamespace(
payload=payload,
headers={"content-type": "application/json"},
url="https://upstream.test/v1/responses",
url_model=str(payload.get("model") or ""),
envelope=None,
upstream_is_stream=self._upstream_is_stream,
tls_profile=None,
selected_base_url=None,
)
def _extract_response_metadata(self, response_json: dict[str, Any]) -> dict[str, Any]:
return {"id": response_json.get("id")}
class _DummyStreamResponseCtx:
def __init__(self) -> None:
self.closed = False
async def __aexit__(self, exc_type: object, exc: object, tb: object) -> None:
self.closed = True
class _DummyCliStreamHandler(CliStreamMixin):
FORMAT_ID = "openai:cli"
def __init__(self, *, upstream_is_stream: bool) -> None:
self.request_id = "req-cli-stream"
self.api_key = SimpleNamespace(id="user-key-1")
self._upstream_is_stream = upstream_is_stream
async def _get_mapped_model(self, source_model: str, provider_id: str) -> str | None:
del source_model, provider_id
return None
async def _build_upstream_request(self, **kwargs: Any) -> Any:
payload = dict(kwargs["request_body"])
return SimpleNamespace(
payload=payload,
headers={"content-type": "application/json"},
url="https://upstream.test/v1/responses",
url_model=str(payload.get("model") or ""),
envelope=None,
upstream_is_stream=self._upstream_is_stream,
tls_profile=None,
selected_base_url=None,
)
def apply_mapped_model(self, request_body: dict[str, Any], mapped_model: str) -> dict[str, Any]:
out = dict(request_body)
out["model"] = mapped_model
return out
def _extract_response_metadata(self, response_json: dict[str, Any]) -> dict[str, Any]:
return {"id": response_json.get("id")}
def _record_converted_chunks(self, ctx: Any, converted_events: Any) -> None:
del ctx, converted_events
def _mark_first_output(self, ctx: Any, output_state: dict[str, Any]) -> None:
del ctx
output_state["first_yield"] = False
async def _prefetch_and_check_embedded_error(
self,
byte_iterator: Any,
provider: Any,
endpoint: Any,
ctx: Any,
) -> list[bytes]:
del provider, endpoint, ctx
first = await anext(byte_iterator)
return [first]
async def _create_response_stream_with_prefetch(
self,
ctx: Any,
byte_iterator: Any,
response_ctx: _DummyStreamResponseCtx,
prefetched_chunks: list[bytes],
) -> AsyncGenerator[bytes]:
del ctx
async def _gen() -> AsyncGenerator[bytes]:
try:
for chunk in prefetched_chunks:
yield chunk
async for chunk in byte_iterator:
yield chunk
finally:
await response_ctx.__aexit__(None, None, None)
return _gen()
async def _iter_chunks(chunks: list[bytes]) -> AsyncGenerator[bytes]:
for chunk in chunks:
yield chunk
def _patch_proxy_resolver(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(proxymod, "resolve_effective_proxy", lambda provider_proxy, key_proxy=None: None)
monkeypatch.setattr(proxymod, "get_proxy_label", lambda proxy_info: "direct")
async def _fake_resolve_proxy_info(proxy_config: Any) -> Any:
del proxy_config
return None
async def _fake_resolve_delegate(proxy_config: Any) -> Any:
del proxy_config
return None
async def _fake_build_proxy_url(proxy_config: Any) -> Any:
del proxy_config
return None
monkeypatch.setattr(proxymod, "resolve_proxy_info_async", _fake_resolve_proxy_info)
monkeypatch.setattr(proxymod, "resolve_delegate_config_async", _fake_resolve_delegate)
monkeypatch.setattr(proxymod, "build_proxy_url_async", _fake_build_proxy_url)
@pytest.mark.asyncio
async def test_cli_process_sync_uses_rust_executor_when_available(
monkeypatch: pytest.MonkeyPatch,
) -> None:
handler = _DummySyncHandler()
monkeypatch.setattr(cli_sync_mod.config, "executor_backend", "rust")
_patch_proxy_resolver(monkeypatch)
class _FakeTaskService:
def __init__(self, db: Any, redis: Any) -> None:
del db, redis
async def execute(self, **kwargs: Any) -> Any:
candidate = SimpleNamespace(
request_candidate_id="cand-1",
mapping_matched_model=None,
needs_conversion=False,
output_limit=None,
)
provider = SimpleNamespace(
name="provider",
id="provider-1",
provider_type="",
proxy=None,
request_timeout=None,
stream_first_byte_timeout=None,
)
endpoint = SimpleNamespace(id="endpoint-1", api_format="openai:cli")
key = SimpleNamespace(id="key-1", api_key="sk-test", proxy=None)
response = await kwargs["request_func"](provider, endpoint, key, candidate)
return SimpleNamespace(
response=response,
provider_name="provider",
request_candidate_id="cand-1",
provider_id="provider-1",
endpoint_id="endpoint-1",
key_id="key-1",
pool_summary=None,
)
async def _fake_execute_sync_json(self: object, plan: object) -> ExecutionRuntimeSyncResult:
assert getattr(plan, "provider_api_format") == "openai:cli"
return ExecutionRuntimeSyncResult(
status_code=200,
response_json={"id": "resp-rust-cli"},
headers={"content-type": "application/json"},
)
monkeypatch.setattr(taskmod, "TaskService", _FakeTaskService)
monkeypatch.setattr(
cli_sync_mod.ExecutionRuntimeClient,
"execute_sync_json",
_fake_execute_sync_json,
)
response = await handler.process_sync(
original_request_body={"model": "gpt-4.1", "input": "hello"},
original_headers={},
)
assert response.status_code == 200
assert json.loads(response.body) == {"id": "resp-rust-cli"}
@pytest.mark.asyncio
async def test_cli_process_sync_aggregates_upstream_stream_after_rust(
monkeypatch: pytest.MonkeyPatch,
) -> None:
handler = _DummySyncHandler(upstream_is_stream=True)
monkeypatch.setattr(cli_sync_mod.config, "executor_backend", "rust")
_patch_proxy_resolver(monkeypatch)
class _FakeTaskService:
def __init__(self, db: Any, redis: Any) -> None:
del db, redis
async def execute(self, **kwargs: Any) -> Any:
candidate = SimpleNamespace(
request_candidate_id="cand-1",
mapping_matched_model=None,
needs_conversion=False,
output_limit=None,
)
provider = SimpleNamespace(
name="provider",
id="provider-1",
provider_type="",
proxy=None,
request_timeout=None,
stream_first_byte_timeout=None,
)
endpoint = SimpleNamespace(id="endpoint-1", api_format="openai:cli")
key = SimpleNamespace(id="key-1", api_key="sk-test", proxy=None)
response = await kwargs["request_func"](provider, endpoint, key, candidate)
return SimpleNamespace(
response=response,
provider_name="provider",
request_candidate_id="cand-1",
provider_id="provider-1",
endpoint_id="endpoint-1",
key_id="key-1",
pool_summary=None,
)
class _FakeNormalizer:
def response_from_internal(self, response: Any, *, requested_model: str) -> dict[str, Any]:
return {
"aggregated": True,
"requested_model": requested_model,
"internal_id": response.id,
}
class _FakeRegistry:
def get_normalizer(self, format_id: str) -> _FakeNormalizer:
assert format_id == "openai:cli"
return _FakeNormalizer()
captured_chunks: list[bytes] = []
async def _fake_aggregate(
byte_iter: object,
*,
provider_api_format: str,
provider_name: str,
model: str,
request_id: str,
envelope: object = None,
provider_parser: object = None,
) -> object:
del envelope, provider_parser
async for chunk in byte_iter: # type: ignore[attr-defined]
captured_chunks.append(chunk)
assert provider_api_format == "openai:cli"
assert provider_name == "provider"
assert model == "gpt-4.1"
assert request_id == "req-cli-sync"
return SimpleNamespace(id="agg-cli-1")
async def _fake_execute_sync_json(self: object, plan: object) -> ExecutionRuntimeSyncResult:
assert getattr(plan, "provider_api_format") == "openai:cli"
assert getattr(plan, "stream") is True
return ExecutionRuntimeSyncResult(
status_code=200,
response_body_bytes=b"data: {\"id\":\"chunk-1\"}\n\ndata: [DONE]\n\n",
headers={"content-type": "text/event-stream"},
)
async def _fake_get_upstream_client(*args: Any, **kwargs: Any) -> object:
raise AssertionError("python fallback should not be used")
monkeypatch.setattr(taskmod, "TaskService", _FakeTaskService)
monkeypatch.setattr(
cli_sync_mod.ExecutionRuntimeClient,
"execute_sync_json",
_fake_execute_sync_json,
)
monkeypatch.setattr(cli_sync_mod, "get_format_converter_registry", lambda: _FakeRegistry())
monkeypatch.setattr(
cli_sync_mod,
"aggregate_upstream_stream_to_internal_response",
_fake_aggregate,
)
monkeypatch.setattr(
"src.clients.http_client.HTTPClientPool.get_upstream_client",
_fake_get_upstream_client,
)
response = await handler.process_sync(
original_request_body={"model": "gpt-4.1", "input": "hello"},
original_headers={},
)
assert response.status_code == 200
assert json.loads(response.body) == {
"aggregated": True,
"requested_model": "gpt-4.1",
"internal_id": "agg-cli-1",
}
assert captured_chunks == [b"data: {\"id\":\"chunk-1\"}\n\ndata: [DONE]\n\n"]
@pytest.mark.asyncio
async def test_cli_process_sync_raises_when_rust_unavailable(
monkeypatch: pytest.MonkeyPatch,
) -> None:
handler = _DummySyncHandler()
monkeypatch.setattr(cli_sync_mod.config, "executor_backend", "rust")
_patch_proxy_resolver(monkeypatch)
class _FakeTaskService:
def __init__(self, db: Any, redis: Any) -> None:
del db, redis
async def execute(self, **kwargs: Any) -> Any:
candidate = SimpleNamespace(
request_candidate_id="cand-1",
mapping_matched_model=None,
needs_conversion=False,
output_limit=None,
)
provider = SimpleNamespace(
name="provider",
id="provider-1",
provider_type="",
proxy=None,
request_timeout=None,
stream_first_byte_timeout=None,
)
endpoint = SimpleNamespace(id="endpoint-1", api_format="openai:cli")
key = SimpleNamespace(id="key-1", api_key="sk-test", proxy=None)
await kwargs["request_func"](provider, endpoint, key, candidate)
raise AssertionError("task service should not reach Python local execution")
async def _fake_execute_sync_json(self: object, plan: object) -> ExecutionRuntimeSyncResult:
del self, plan
raise ExecutionRuntimeClientError("executor down")
async def _fake_get_upstream_client(*args: Any, **kwargs: Any) -> object:
raise AssertionError("python fallback should not be used")
monkeypatch.setattr(taskmod, "TaskService", _FakeTaskService)
monkeypatch.setattr(
cli_sync_mod.ExecutionRuntimeClient,
"execute_sync_json",
_fake_execute_sync_json,
)
monkeypatch.setattr(
"src.clients.http_client.HTTPClientPool.get_upstream_client",
_fake_get_upstream_client,
)
with pytest.raises(ProviderNotAvailableException) as exc_info:
await handler.process_sync(
original_request_body={"model": "gpt-4.1", "input": "hello"},
original_headers={},
)
assert exc_info.value.message == "执行器暂时不可用,请稍后重试"
assert exc_info.value.upstream_response == "executor down"
@pytest.mark.asyncio
async def test_cli_process_sync_raises_when_remote_contract_is_ineligible(
monkeypatch: pytest.MonkeyPatch,
) -> None:
handler = _DummySyncHandler()
monkeypatch.setattr(cli_sync_mod.config, "executor_backend", "rust")
monkeypatch.setattr(cli_sync_mod, "is_remote_execution_runtime_contract_eligible", lambda plan: False)
_patch_proxy_resolver(monkeypatch)
class _FakeTaskService:
def __init__(self, db: Any, redis: Any) -> None:
del db, redis
async def execute(self, **kwargs: Any) -> Any:
candidate = SimpleNamespace(
request_candidate_id="cand-1",
mapping_matched_model=None,
needs_conversion=False,
output_limit=None,
)
provider = SimpleNamespace(
name="provider",
id="provider-1",
provider_type="",
proxy=None,
request_timeout=None,
stream_first_byte_timeout=None,
)
endpoint = SimpleNamespace(id="endpoint-1", api_format="openai:cli")
key = SimpleNamespace(id="key-1", api_key="sk-test", proxy=None)
await kwargs["request_func"](provider, endpoint, key, candidate)
raise AssertionError("task service should not complete after local upstream attempt")
async def _fake_execute_sync_json(self: object, plan: object) -> ExecutionRuntimeSyncResult:
raise AssertionError("rust executor should not be used when contract is ineligible")
async def _fake_get_upstream_client(*args: Any, **kwargs: Any) -> object:
raise AssertionError("python fallback should not be used")
monkeypatch.setattr(taskmod, "TaskService", _FakeTaskService)
monkeypatch.setattr(
cli_sync_mod.ExecutionRuntimeClient,
"execute_sync_json",
_fake_execute_sync_json,
)
monkeypatch.setattr(
"src.clients.http_client.HTTPClientPool.get_upstream_client",
_fake_get_upstream_client,
)
with pytest.raises(ProviderNotAvailableException) as exc_info:
await handler.process_sync(
original_request_body={"model": "gpt-4.1", "input": "hello"},
original_headers={},
)
assert exc_info.value.message == "CLI 请求暂不支持当前 Rust executor 契约"
assert exc_info.value.upstream_response == "remote_contract_ineligible"
@pytest.mark.asyncio
async def test_cli_execute_stream_request_uses_rust_sync_bridge(
monkeypatch: pytest.MonkeyPatch,
) -> None:
handler = _DummyCliStreamHandler(upstream_is_stream=False)
ctx = StreamContext(model="gpt-test", api_format="openai:cli")
ctx.client_api_format = "openai:cli"
monkeypatch.setattr(cli_stream_mod.config, "executor_backend", "rust")
_patch_proxy_resolver(monkeypatch)
async def _fake_execute_sync_json(self: object, plan: object) -> ExecutionRuntimeSyncResult:
assert getattr(plan, "stream") is False
return ExecutionRuntimeSyncResult(
status_code=200,
response_json={"id": "sync-bridge-rust"},
headers={"content-type": "application/json"},
)
async def _fake_streamify(**kwargs: Any) -> AsyncGenerator[bytes]:
assert kwargs["response_json"] == {"id": "sync-bridge-rust"}
yield b"data: cli-bridge\n\n"
monkeypatch.setattr(
cli_stream_mod.ExecutionRuntimeClient,
"execute_sync_json",
_fake_execute_sync_json,
)
monkeypatch.setattr(handler, "_streamify_sync_response", _fake_streamify)
provider = SimpleNamespace(
name="provider",
id="provider-1",
provider_type="",
proxy=None,
request_timeout=None,
stream_first_byte_timeout=None,
)
endpoint = SimpleNamespace(id="endpoint-1", api_format="openai:cli", base_url="https://x")
key = SimpleNamespace(id="key-1", proxy=None, auth_type="", api_key="sk-test")
candidate = SimpleNamespace(
request_candidate_id="cand-1",
mapping_matched_model=None,
needs_conversion=False,
output_limit=None,
)
stream = await handler._execute_stream_request(
ctx,
provider,
endpoint,
key,
{"model": "gpt-test", "input": "hello"},
{},
candidate=candidate,
)
if hasattr(stream, "__await__"):
stream = await stream
chunks = [chunk async for chunk in stream]
assert chunks == [b"data: cli-bridge\n\n"]
@pytest.mark.asyncio
@pytest.mark.parametrize("upstream_is_stream", [False, True])
async def test_cli_execute_stream_request_raises_when_rust_unavailable(
monkeypatch: pytest.MonkeyPatch,
upstream_is_stream: bool,
) -> None:
handler = _DummyCliStreamHandler(upstream_is_stream=upstream_is_stream)
ctx = StreamContext(model="gpt-test", api_format="openai:cli")
ctx.client_api_format = "openai:cli"
monkeypatch.setattr(cli_stream_mod.config, "executor_backend", "rust")
_patch_proxy_resolver(monkeypatch)
async def _fake_get_upstream_client(*args: Any, **kwargs: Any) -> object:
raise AssertionError("python fallback should not be used")
monkeypatch.setattr(
"src.clients.http_client.HTTPClientPool.get_upstream_client",
_fake_get_upstream_client,
)
provider = SimpleNamespace(
name="provider",
id="provider-1",
provider_type="",
proxy=None,
request_timeout=None,
stream_first_byte_timeout=None,
)
endpoint = SimpleNamespace(id="endpoint-1", api_format="openai:cli", base_url="https://x")
key = SimpleNamespace(id="key-1", proxy=None, api_key="sk-test", auth_type="")
candidate = SimpleNamespace(
request_candidate_id="cand-1",
mapping_matched_model=None,
needs_conversion=False,
output_limit=None,
)
if upstream_is_stream:
async def _fake_execute_stream(self: object, plan: object) -> ExecutionRuntimeStreamResult:
del self, plan
raise ExecutionRuntimeClientError("executor down")
monkeypatch.setattr(
cli_stream_mod.ExecutionRuntimeClient,
"execute_stream",
_fake_execute_stream,
)
else:
async def _fake_execute_sync_json(self: object, plan: object) -> ExecutionRuntimeSyncResult:
del self, plan
raise ExecutionRuntimeClientError("executor down")
monkeypatch.setattr(
cli_stream_mod.ExecutionRuntimeClient,
"execute_sync_json",
_fake_execute_sync_json,
)
with pytest.raises(ProviderNotAvailableException) as exc_info:
stream = await handler._execute_stream_request(
ctx,
provider,
endpoint,
key,
{"model": "gpt-test", "input": "hello"},
{},
candidate=candidate,
)
if hasattr(stream, "__await__"):
stream = await stream
_ = [chunk async for chunk in stream]
assert exc_info.value.message == "执行器暂时不可用,请稍后重试"
assert exc_info.value.upstream_response == "executor down"
@pytest.mark.asyncio
@pytest.mark.parametrize("upstream_is_stream", [False, True])
async def test_cli_execute_stream_request_raises_when_remote_contract_is_ineligible(
monkeypatch: pytest.MonkeyPatch,
upstream_is_stream: bool,
) -> None:
handler = _DummyCliStreamHandler(upstream_is_stream=upstream_is_stream)
ctx = StreamContext(model="gpt-test", api_format="openai:cli")
ctx.client_api_format = "openai:cli"
monkeypatch.setattr(cli_stream_mod.config, "executor_backend", "rust")
monkeypatch.setattr(cli_stream_mod, "is_remote_execution_runtime_contract_eligible", lambda plan: False)
_patch_proxy_resolver(monkeypatch)
async def _fake_get_upstream_client(*args: Any, **kwargs: Any) -> object:
raise AssertionError("python fallback should not be used")
async def _fake_execute_sync_json(self: object, plan: object) -> ExecutionRuntimeSyncResult:
raise AssertionError("rust sync executor should not be used when contract is ineligible")
async def _fake_execute_stream(self: object, plan: object) -> ExecutionRuntimeStreamResult:
raise AssertionError("rust stream executor should not be used when contract is ineligible")
monkeypatch.setattr(
"src.clients.http_client.HTTPClientPool.get_upstream_client",
_fake_get_upstream_client,
)
monkeypatch.setattr(
cli_stream_mod.ExecutionRuntimeClient,
"execute_sync_json",
_fake_execute_sync_json,
)
monkeypatch.setattr(
cli_stream_mod.ExecutionRuntimeClient,
"execute_stream",
_fake_execute_stream,
)
provider = SimpleNamespace(
name="provider",
id="provider-1",
provider_type="",
proxy=None,
request_timeout=None,
stream_first_byte_timeout=None,
)
endpoint = SimpleNamespace(id="endpoint-1", api_format="openai:cli", base_url="https://x")
key = SimpleNamespace(id="key-1", proxy=None, api_key="sk-test", auth_type="")
candidate = SimpleNamespace(
request_candidate_id="cand-1",
mapping_matched_model=None,
needs_conversion=False,
output_limit=None,
)
with pytest.raises(ProviderNotAvailableException) as exc_info:
stream = await handler._execute_stream_request(
ctx,
provider,
endpoint,
key,
{"model": "gpt-test", "input": "hello"},
{},
candidate=candidate,
)
if hasattr(stream, "__await__"):
stream = await stream
_ = [chunk async for chunk in stream]
assert exc_info.value.message == "CLI 请求暂不支持当前 Rust executor 契约"
assert exc_info.value.upstream_response == "remote_contract_ineligible"
@pytest.mark.asyncio
async def test_cli_execute_stream_request_uses_rust_native_stream(
monkeypatch: pytest.MonkeyPatch,
) -> None:
handler = _DummyCliStreamHandler(upstream_is_stream=True)
ctx = StreamContext(model="gpt-test", api_format="openai:cli")
ctx.client_api_format = "openai:cli"
monkeypatch.setattr(cli_stream_mod.config, "executor_backend", "rust")
_patch_proxy_resolver(monkeypatch)
async def _fake_execute_stream(self: object, plan: object) -> ExecutionRuntimeStreamResult:
assert getattr(plan, "stream") is True
return ExecutionRuntimeStreamResult(
status_code=200,
headers={"content-type": "text/event-stream", "x-upstream-test": "true"},
byte_iterator=_iter_chunks(
[
b"data: {\"id\":\"chunk-1\"}\n\n",
b"data: [DONE]\n\n",
]
),
response_ctx=_DummyStreamResponseCtx(),
)
monkeypatch.setattr(
cli_stream_mod.ExecutionRuntimeClient,
"execute_stream",
_fake_execute_stream,
)
provider = SimpleNamespace(
name="provider",
id="provider-1",
provider_type="",
proxy=None,
request_timeout=None,
stream_first_byte_timeout=None,
)
endpoint = SimpleNamespace(id="endpoint-1", api_format="openai:cli", base_url="https://x")
key = SimpleNamespace(id="key-1", proxy=None, api_key="sk-test")
candidate = SimpleNamespace(
request_candidate_id="cand-1",
mapping_matched_model=None,
needs_conversion=False,
output_limit=None,
)
stream = await handler._execute_stream_request(
ctx,
provider,
endpoint,
key,
{"model": "gpt-test", "input": "hello"},
{},
candidate=candidate,
)
if hasattr(stream, "__await__"):
stream = await stream
chunks = [chunk async for chunk in stream]
assert chunks == [
b"data: {\"id\":\"chunk-1\"}\n\n",
b"data: [DONE]\n\n",
]
assert ctx.status_code == 200
assert ctx.response_headers["x-upstream-test"] == "true"
@@ -1,32 +0,0 @@
import json
from src.api.handlers.base.cli_sse_helpers import _format_converted_events_to_sse
def test_format_converted_events_to_sse_uses_event_lines_for_openai_cli() -> None:
events = [
{
"type": "response.output_text.delta",
"item_id": "msg_123",
"output_index": 0,
"content_index": 0,
"delta": "Hi",
"logprobs": [],
"sequence_number": 1,
}
]
lines = _format_converted_events_to_sse(events, "openai:cli")
assert lines == [
"event: response.output_text.delta\n"
f"data: {json.dumps(events[0], ensure_ascii=False)}\n"
]
def test_format_converted_events_to_sse_keeps_data_only_for_openai_chat() -> None:
events = [{"id": "chatcmpl-123", "object": "chat.completion.chunk", "choices": []}]
lines = _format_converted_events_to_sse(events, "openai:chat")
assert lines == [f"data: {json.dumps(events[0], ensure_ascii=False)}\n"]
@@ -1,264 +0,0 @@
from __future__ import annotations
import hashlib
from types import SimpleNamespace
from typing import Any
import pytest
import src.api.handlers.base.cli_request_mixin as mixmod
from src.api.handlers.base.cli_request_mixin import CliRequestMixin
from src.api.handlers.base.request_builder import PassthroughRequestBuilder
from src.core.api_format.metadata import CODEX_DEFAULT_BODY_RULES
from src.services.provider.adapters.codex.context import (
CodexRequestContext,
set_codex_request_context,
)
from src.services.provider.prompt_cache import build_stable_codex_prompt_cache_key
class _DummyAuthInfo:
def __init__(self, decrypted_auth_config: dict[str, Any] | None = None) -> None:
self.auth_header = "Authorization"
self.auth_value = "Bearer upstream-token"
self.decrypted_auth_config = decrypted_auth_config
def as_tuple(self) -> tuple[str, str]:
return self.auth_header, self.auth_value
class _DummyCliRequestHandler(CliRequestMixin):
FORMAT_ID = "openai:cli"
def __init__(self) -> None:
self.api_key = SimpleNamespace(id="user-key-123")
self._request_builder = PassthroughRequestBuilder()
def _build_codex_provider() -> Any:
return SimpleNamespace(
id="provider-1",
provider_type="codex",
config=None,
proxy=None,
)
def _build_codex_endpoint(*, api_format: str) -> Any:
provider = _build_codex_provider()
return SimpleNamespace(
id="endpoint-1",
api_family="openai",
endpoint_kind="cli",
api_format=api_format,
base_url="https://chatgpt.com/backend-api/codex",
custom_path=None,
body_rules=list(CODEX_DEFAULT_BODY_RULES),
header_rules=None,
provider=provider,
)
def _build_key() -> Any:
return SimpleNamespace(id="key-1", api_key="unused", proxy=None)
def _build_headers() -> dict[str, str]:
return {
"accept": "application/json",
"content-type": "application/json",
"user-agent": "Codex Desktop/0.108.0-alpha.12",
"originator": "Codex Desktop",
"x-codex-turn-metadata": '{"turn_id":"abc"}',
"host": "aether.hetunai.cn",
"content-length": "123",
"x-forwarded-scheme": "https",
}
def _assert_common_codex_headers(headers: dict[str, str]) -> None:
assert headers["accept"] == "application/json"
assert headers["content-type"] == "application/json"
assert headers["user-agent"] == "Codex Desktop/0.108.0-alpha.12"
assert headers["originator"] == "Codex Desktop"
assert headers["x-codex-turn-metadata"] == '{"turn_id":"abc"}'
assert headers["Authorization"] == "Bearer upstream-token"
assert "host" not in headers
assert "content-length" not in headers
assert "x-forwarded-scheme" not in headers
def _expected_codex_header_id(prompt_cache_key: str) -> str:
return hashlib.sha256(prompt_cache_key.encode()).hexdigest()[:16]
@pytest.mark.asyncio
async def test_build_upstream_request_codex_cli_injects_prompt_cache_and_forces_stream(
monkeypatch: pytest.MonkeyPatch,
) -> None:
async def _fake_get_provider_auth(endpoint: Any, key: Any) -> _DummyAuthInfo:
return _DummyAuthInfo()
monkeypatch.setattr(mixmod, "get_provider_auth", _fake_get_provider_auth)
handler = _DummyCliRequestHandler()
endpoint = _build_codex_endpoint(api_format="openai:cli")
provider = endpoint.provider
key = _build_key()
result = await handler._build_upstream_request(
provider=provider,
endpoint=endpoint,
key=key,
request_body={
"model": "gpt-5",
"input": [],
"stream": False,
"max_output_tokens": 4096,
"temperature": 0.7,
"top_p": 0.8,
},
original_headers=_build_headers(),
query_params=None,
client_api_format="openai:cli",
provider_api_format="openai:cli",
fallback_model="gpt-5",
mapped_model=None,
client_is_stream=False,
)
assert result.url == "https://chatgpt.com/backend-api/codex/responses"
assert result.upstream_is_stream is True
assert result.payload["stream"] is True
assert result.payload["instructions"] == "You are GPT-5."
assert result.payload["store"] is False
assert result.payload["prompt_cache_key"] == build_stable_codex_prompt_cache_key("user-key-123")
assert "max_output_tokens" not in result.payload
assert "temperature" not in result.payload
assert "top_p" not in result.payload
short_id = _expected_codex_header_id(result.payload["prompt_cache_key"])
assert result.headers["session_id"] == short_id
assert result.headers["conversation_id"] == short_id
_assert_common_codex_headers(result.headers)
@pytest.mark.asyncio
async def test_build_upstream_request_codex_compact_drops_stream_and_injects_prompt_cache(
monkeypatch: pytest.MonkeyPatch,
) -> None:
async def _fake_get_provider_auth(endpoint: Any, key: Any) -> _DummyAuthInfo:
return _DummyAuthInfo({"account_id": "acc-1"})
monkeypatch.setattr(mixmod, "get_provider_auth", _fake_get_provider_auth)
handler = _DummyCliRequestHandler()
endpoint = _build_codex_endpoint(api_format="openai:compact")
provider = endpoint.provider
key = _build_key()
result = await handler._build_upstream_request(
provider=provider,
endpoint=endpoint,
key=key,
request_body={"model": "gpt-5", "input": [], "stream": True},
original_headers=_build_headers(),
query_params=None,
client_api_format="openai:cli",
provider_api_format="openai:compact",
fallback_model="gpt-5",
mapped_model=None,
client_is_stream=False,
)
assert result.url == "https://chatgpt.com/backend-api/codex/responses/compact"
assert result.upstream_is_stream is False
assert "stream" not in result.payload
assert result.payload["prompt_cache_key"] == build_stable_codex_prompt_cache_key("user-key-123")
assert result.payload["instructions"] == "You are GPT-5."
assert result.payload["store"] is False
short_id = _expected_codex_header_id(result.payload["prompt_cache_key"])
assert result.headers["chatgpt-account-id"] == "acc-1"
assert result.headers["session_id"] == short_id
assert "conversation_id" not in result.headers
_assert_common_codex_headers(result.headers)
@pytest.mark.asyncio
async def test_build_upstream_request_legacy_codex_compact_context_uses_compact_url(
monkeypatch: pytest.MonkeyPatch,
) -> None:
async def _fake_get_provider_auth(endpoint: Any, key: Any) -> _DummyAuthInfo:
return _DummyAuthInfo()
monkeypatch.setattr(mixmod, "get_provider_auth", _fake_get_provider_auth)
handler = _DummyCliRequestHandler()
endpoint = _build_codex_endpoint(api_format="openai:cli")
provider = endpoint.provider
key = _build_key()
try:
set_codex_request_context(CodexRequestContext(is_compact=True))
result = await handler._build_upstream_request(
provider=provider,
endpoint=endpoint,
key=key,
request_body={"model": "gpt-5", "input": [], "stream": True},
original_headers=_build_headers(),
query_params=None,
client_api_format="openai:cli",
provider_api_format="openai:cli",
fallback_model="gpt-5",
mapped_model=None,
client_is_stream=False,
)
finally:
set_codex_request_context(None)
assert result.url == "https://chatgpt.com/backend-api/codex/responses/compact"
assert result.upstream_is_stream is False
assert "stream" not in result.payload
assert result.payload["prompt_cache_key"] == build_stable_codex_prompt_cache_key("user-key-123")
assert result.payload["instructions"] == "You are GPT-5."
short_id = _expected_codex_header_id(result.payload["prompt_cache_key"])
assert result.headers["session_id"] == short_id
assert "conversation_id" not in result.headers
_assert_common_codex_headers(result.headers)
@pytest.mark.asyncio
async def test_build_upstream_request_codex_cli_preserves_explicit_prompt_cache_key(
monkeypatch: pytest.MonkeyPatch,
) -> None:
async def _fake_get_provider_auth(endpoint: Any, key: Any) -> _DummyAuthInfo:
return _DummyAuthInfo()
monkeypatch.setattr(mixmod, "get_provider_auth", _fake_get_provider_auth)
handler = _DummyCliRequestHandler()
endpoint = _build_codex_endpoint(api_format="openai:cli")
provider = endpoint.provider
key = _build_key()
result = await handler._build_upstream_request(
provider=provider,
endpoint=endpoint,
key=key,
request_body={
"model": "gpt-5",
"input": [],
"prompt_cache_key": "client-cache-key",
},
original_headers=_build_headers(),
query_params=None,
client_api_format="openai:cli",
provider_api_format="openai:cli",
fallback_model="gpt-5",
mapped_model=None,
client_is_stream=False,
)
assert result.url == "https://chatgpt.com/backend-api/codex/responses"
assert result.payload["prompt_cache_key"] == "client-cache-key"
assert result.payload["stream"] is True
_assert_common_codex_headers(result.headers)
@@ -1,192 +0,0 @@
from __future__ import annotations
from typing import Any
from unittest.mock import AsyncMock
import pytest
from src.api.handlers.base.endpoint_checker import EndpointCheckRequest, HttpRequestExecutor
from src.services.request.execution_runtime_client import (
ExecutionRuntimeStreamResult,
ExecutionRuntimeSyncResult,
)
class _DummyStreamContext:
def __init__(self) -> None:
self.closed = False
async def __aexit__(self, exc_type: object, exc: object, tb: object) -> None:
self.closed = True
@pytest.mark.asyncio
async def test_endpoint_checker_sync_prefers_rust_executor(
monkeypatch: pytest.MonkeyPatch,
) -> None:
from src.api.handlers.base import endpoint_checker as mod
from src.services.request import execution_runtime_client as rust_mod
monkeypatch.setattr(mod.config, "executor_backend", "rust")
executor = HttpRequestExecutor(timeout=15.0)
proxy_snapshot = object()
monkeypatch.setattr(
executor,
"_build_rust_proxy_snapshot",
AsyncMock(return_value=proxy_snapshot),
)
captured: dict[str, Any] = {}
async def _fake_execute_sync_json(
self: object,
plan: Any,
) -> ExecutionRuntimeSyncResult:
captured["plan"] = plan
return ExecutionRuntimeSyncResult(
status_code=200,
response_json={"id": "resp_1", "usage": {"prompt_tokens": 1, "completion_tokens": 2}},
headers={"content-type": "application/json"},
)
monkeypatch.setattr(rust_mod.ExecutionRuntimeClient, "execute_sync_json", _fake_execute_sync_json)
result = await executor.execute(
EndpointCheckRequest(
url="https://upstream.test/v1/chat/completions",
headers={"authorization": "Bearer test"},
json_body={"model": "gpt-test", "messages": [{"role": "user", "content": "hi"}]},
api_format="openai:chat",
provider_name="openai",
model_name="gpt-test",
api_key_id="key_1",
provider_id="provider_1",
)
)
assert result.status_code == 200
assert result.response_data == {
"id": "resp_1",
"usage": {"prompt_tokens": 1, "completion_tokens": 2},
}
assert captured["plan"].proxy is proxy_snapshot
assert captured["plan"].method == "POST"
assert captured["plan"].url == "https://upstream.test/v1/chat/completions"
@pytest.mark.asyncio
async def test_endpoint_checker_stream_prefers_rust_executor(
monkeypatch: pytest.MonkeyPatch,
) -> None:
from src.api.handlers.base import endpoint_checker as mod
from src.services.request import execution_runtime_client as rust_mod
monkeypatch.setattr(mod.config, "executor_backend", "rust")
executor = HttpRequestExecutor(timeout=15.0)
monkeypatch.setattr(
executor,
"_build_rust_proxy_snapshot",
AsyncMock(return_value=None),
)
async def _byte_iter() -> Any:
yield b'data: {"choices":[{"delta":{"content":"Hel'
yield b'lo"}}]}\n\n'
yield b'data: {"choices":[{"delta":{"content":" world"},"finish_reason":"stop"}]}\n\n'
stream_ctx = _DummyStreamContext()
async def _fake_execute_stream(self: object, plan: Any) -> ExecutionRuntimeStreamResult:
return ExecutionRuntimeStreamResult(
status_code=200,
headers={"content-type": "text/event-stream"},
byte_iterator=_byte_iter(),
response_ctx=stream_ctx,
)
monkeypatch.setattr(rust_mod.ExecutionRuntimeClient, "execute_stream", _fake_execute_stream)
result = await executor.execute(
EndpointCheckRequest(
url="https://upstream.test/v1/chat/completions",
headers={"authorization": "Bearer test"},
json_body={
"model": "gpt-test",
"messages": [{"role": "user", "content": "hi"}],
"stream": True,
},
api_format="openai:chat",
provider_name="openai",
model_name="gpt-test",
is_stream=True,
)
)
assert result.status_code == 200
assert result.response_data == {
"choices": [{"delta": {"content": " world"}, "finish_reason": "stop"}]
}
assert stream_ctx.closed is True
@pytest.mark.asyncio
async def test_endpoint_checker_proxy_snapshot_falls_back_to_system_proxy(
monkeypatch: pytest.MonkeyPatch,
) -> None:
from src.services.proxy_node import resolver as resolver_mod
executor = HttpRequestExecutor()
monkeypatch.setattr(
resolver_mod,
"get_system_proxy_config_async",
AsyncMock(return_value={"enabled": True, "url": "http://system-proxy.test:8080"}),
)
monkeypatch.setattr(
resolver_mod,
"resolve_delegate_config_async",
AsyncMock(return_value=None),
)
monkeypatch.setattr(
resolver_mod,
"build_proxy_url_async",
AsyncMock(return_value="http://system-proxy.test:8080"),
)
monkeypatch.setattr(
resolver_mod,
"resolve_proxy_info_async",
AsyncMock(return_value={"mode": "http", "label": "system-proxy"}),
)
snapshot = await executor._build_rust_proxy_snapshot(None)
assert snapshot is not None
assert snapshot.enabled is True
assert snapshot.url == "http://system-proxy.test:8080"
assert snapshot.mode == "http"
@pytest.mark.asyncio
async def test_endpoint_checker_returns_503_when_rust_executor_disabled(
monkeypatch: pytest.MonkeyPatch,
) -> None:
from src.api.handlers.base import endpoint_checker as mod
monkeypatch.setattr(mod.config, "executor_backend", "python")
executor = HttpRequestExecutor(timeout=5.0)
result = await executor.execute(
EndpointCheckRequest(
url="https://upstream.test/v1/chat/completions",
headers={"authorization": "Bearer test"},
json_body={"model": "gpt-test", "messages": [{"role": "user", "content": "hi"}]},
api_format="openai:chat",
provider_name="openai",
model_name="gpt-test",
)
)
assert result.status_code == 503
assert result.error_message == "端点检查仅支持 Rust executor"
@@ -1,113 +0,0 @@
from __future__ import annotations
from typing import Any
from src.api.handlers.base.parsers import OpenAICliResponseParser, OpenAIResponseParser
from src.api.handlers.base.response_parser import (
ParsedChunk,
ParsedResponse,
ResponseParser,
StreamStats,
)
from src.api.handlers.base.stream_context import StreamContext
from src.api.handlers.base.stream_processor import StreamProcessor
class _DummyParser(ResponseParser):
def parse_sse_line(self, line: str, stats: StreamStats) -> ParsedChunk | None:
return None
def parse_response(self, response: dict[str, Any], status_code: int) -> ParsedResponse:
return ParsedResponse(raw_response=response, status_code=status_code)
def extract_usage_from_response(self, response: dict[str, Any]) -> dict[str, int]:
return {}
def extract_text_content(self, response: dict[str, Any]) -> str:
return ""
def test_openai_response_parser_extracts_cached_tokens_from_prompt_tokens_details() -> None:
parser = OpenAIResponseParser()
usage = parser.extract_usage_from_response(
{
"usage": {
"prompt_tokens": 120,
"completion_tokens": 18,
"prompt_tokens_details": {"cached_tokens": 96},
}
}
)
assert usage["input_tokens"] == 120
assert usage["output_tokens"] == 18
assert usage["cache_read_tokens"] == 96
def test_openai_cli_response_parser_extracts_cached_tokens_from_input_tokens_details() -> None:
parser = OpenAICliResponseParser()
usage = parser.extract_usage_from_response(
{
"type": "response.completed",
"response": {
"usage": {
"input_tokens": 2048,
"output_tokens": 128,
"input_tokens_details": {"cached_tokens": 1792},
}
},
}
)
assert usage["input_tokens"] == 2048
assert usage["output_tokens"] == 128
assert usage["cache_read_tokens"] == 1792
def test_stream_processor_extracts_cached_tokens_from_openai_cli_converted_event() -> None:
processor = StreamProcessor(request_id="req_test", default_parser=_DummyParser())
ctx = StreamContext(model="gpt-5", api_format="openai:chat")
processor._extract_usage_from_converted_event(
ctx,
{
"type": "response.completed",
"response": {
"usage": {
"input_tokens": 4096,
"output_tokens": 64,
"input_tokens_details": {"cached_tokens": 3584},
}
},
},
"response.completed",
)
assert ctx.input_tokens == 4096
assert ctx.output_tokens == 64
assert ctx.cached_tokens == 3584
def test_stream_processor_extracts_cached_tokens_from_openai_chat_converted_event() -> None:
processor = StreamProcessor(request_id="req_test", default_parser=_DummyParser())
ctx = StreamContext(model="gpt-5", api_format="openai:chat")
processor._extract_usage_from_converted_event(
ctx,
{
"object": "chat.completion.chunk",
"choices": [],
"usage": {
"prompt_tokens": 512,
"completion_tokens": 21,
"prompt_tokens_details": {"cached_tokens": 480},
},
},
"chat.completion.chunk",
)
assert ctx.input_tokens == 512
assert ctx.output_tokens == 21
assert ctx.cached_tokens == 480
@@ -1,32 +0,0 @@
from __future__ import annotations
from src.services.task.request_state import MutableRequestBodyState
def test_mutable_request_body_state_keeps_original_and_attempts_isolated() -> None:
original = {
"model": "gpt-5",
"input": [{"role": "user", "content": [{"type": "input_text", "text": "hello"}]}],
}
state = MutableRequestBodyState(original)
first_attempt = state.build_attempt_body()
first_attempt["input"][0]["content"][0]["text"] = "attempt-1"
assert original["input"][0]["content"][0]["text"] == "hello"
assert state.current_body["input"][0]["content"][0]["text"] == "hello"
rectified = state.build_attempt_body()
rectified["input"][0]["content"][0]["text"] = "rectified"
state.mark_rectified(rectified, stage=1)
second_attempt = state.build_attempt_body()
second_attempt["input"][0]["content"][0]["text"] = "attempt-2"
assert state.is_rectified() is True
assert state.rectify_stage() == 1
assert state.current_body["input"][0]["content"][0]["text"] == "rectified"
assert original["input"][0]["content"][0]["text"] == "hello"
assert state.consume_rectified_this_turn() is True
assert state.consume_rectified_this_turn() is False
@@ -1,197 +0,0 @@
import os
import pytest
os.environ.setdefault("JWT_SECRET_KEY", "test-secret-key")
from src.api.handlers.base import stream_context
from src.api.handlers.base.stream_context import StreamContext
def test_collected_text_append_and_property() -> None:
ctx = StreamContext(model="test-model", api_format="openai:chat")
assert ctx.collected_text == ""
assert ctx.collected_text_length == 0
ctx.append_text("hello")
ctx.append_text(" ")
ctx.append_text("world")
assert ctx.collected_text == "hello world"
assert ctx.collected_text_length == len("hello world")
def test_collected_text_is_capped_but_total_length_is_preserved() -> None:
ctx = StreamContext(model="test-model", api_format="openai:chat")
cap = stream_context._MAX_COLLECTED_TEXT_CHARS
ctx.append_text("a" * (cap - 4))
ctx.append_text("b" * 10)
assert len(ctx.collected_text) == cap
assert ctx.collected_text == ("a" * (cap - 4)) + ("b" * 4)
assert ctx.collected_text_length == cap + 6
def test_reset_for_retry_clears_state() -> None:
ctx = StreamContext(model="test-model", api_format="openai:chat")
ctx.append_text("x")
ctx.update_usage(input_tokens=10, output_tokens=5)
ctx.parsed_chunks.append({"type": "chunk"})
ctx.chunk_count = 3
ctx.data_count = 2
ctx.has_completion = True
ctx.status_code = 418
ctx.error_message = "boom"
ctx.reset_for_retry()
assert ctx.collected_text == ""
assert ctx.input_tokens == 0
assert ctx.output_tokens == 0
assert ctx.parsed_chunks == []
assert ctx.chunk_count == 0
assert ctx.data_count == 0
assert ctx.has_completion is False
assert ctx.status_code == 200
assert ctx.error_message is None
def test_release_recorded_chunks_clears_both_chunk_lists() -> None:
ctx = StreamContext(model="test-model", api_format="openai:chat")
ctx.parsed_chunks.append({"type": "client"})
ctx.provider_parsed_chunks.append({"type": "provider"})
ctx.release_recorded_chunks()
assert ctx.parsed_chunks == []
assert ctx.provider_parsed_chunks == []
def test_managed_recorded_bodies_builds_then_releases_chunks() -> None:
ctx = StreamContext(model="test-model", api_format="openai:chat")
ctx.parsed_chunks.append({"type": "client"})
ctx.provider_parsed_chunks.append({"type": "provider"})
ctx.data_count = 1
with ctx.managed_recorded_bodies(123) as recorded_bodies:
assert recorded_bodies.response_body is not None
assert recorded_bodies.response_body["chunks"] == [{"type": "provider"}]
assert recorded_bodies.client_response_body is not None
assert recorded_bodies.client_response_body["chunks"] == [{"type": "client"}]
assert ctx.parsed_chunks == []
assert ctx.provider_parsed_chunks == []
assert recorded_bodies.response_body is None
assert recorded_bodies.client_response_body is None
def test_record_first_byte_time(monkeypatch: pytest.MonkeyPatch) -> None:
"""测试记录首字时间"""
ctx = StreamContext(model="claude-3", api_format="claude_messages")
start_time = 100.0
monkeypatch.setattr(stream_context.time, "time", lambda: 100.0123) # 12.3ms
# 记录首字时间
ctx.record_first_byte_time(start_time)
# 验证首字时间已记录
assert ctx.first_byte_time_ms == 12
def test_record_first_byte_time_idempotent(monkeypatch: pytest.MonkeyPatch) -> None:
"""测试首字时间只记录一次"""
ctx = StreamContext(model="claude-3", api_format="claude_messages")
start_time = 100.0
# 第一次记录
monkeypatch.setattr(stream_context.time, "time", lambda: 100.010)
ctx.record_first_byte_time(start_time)
first_value = ctx.first_byte_time_ms
# 第二次记录(应该被忽略)
monkeypatch.setattr(stream_context.time, "time", lambda: 100.020)
ctx.record_first_byte_time(start_time)
second_value = ctx.first_byte_time_ms
# 验证值没有改变
assert first_value == second_value
def test_reset_for_retry_clears_first_byte_time(monkeypatch: pytest.MonkeyPatch) -> None:
"""测试重试时清除首字时间"""
ctx = StreamContext(model="claude-3", api_format="claude_messages")
start_time = 100.0
# 记录首字时间
monkeypatch.setattr(stream_context.time, "time", lambda: 100.010)
ctx.record_first_byte_time(start_time)
assert ctx.first_byte_time_ms is not None
# 重置
ctx.reset_for_retry()
# 验证首字时间已清除
assert ctx.first_byte_time_ms is None
def test_get_log_summary_with_first_byte_time() -> None:
"""测试日志摘要包含首字时间"""
ctx = StreamContext(model="claude-3", api_format="claude_messages")
ctx.provider_name = "anthropic"
ctx.input_tokens = 100
ctx.output_tokens = 50
ctx.first_byte_time_ms = 123
summary = ctx.get_log_summary("request-id-123", 456)
# 验证包含首字时间和总时间(大写格式)
assert "TTFB: 123ms" in summary
assert "Total: 456ms" in summary
assert "in:100 out:50" in summary
def test_get_log_summary_without_first_byte_time() -> None:
"""测试日志摘要在没有首字时间时的格式"""
ctx = StreamContext(model="claude-3", api_format="claude_messages")
ctx.provider_name = "anthropic"
ctx.input_tokens = 100
ctx.output_tokens = 50
# first_byte_time_ms 保持为 None
summary = ctx.get_log_summary("request-id-123", 456)
# 验证不包含首字时间标记,但有总时间(使用大写 TTFB 和 Total)
assert "TTFB:" not in summary
assert "Total: 456ms" in summary
assert "in:100 out:50" in summary
def test_ensure_estimated_output_tokens_uses_collected_text() -> None:
ctx = StreamContext(model="test-model", api_format="openai:chat")
ctx.append_text("partial output")
changed = ctx.ensure_estimated_output_tokens()
assert changed is True
assert ctx.output_tokens == max(1, len("partial output") // 4)
def test_should_estimate_incomplete_tokens_for_interrupted_partial_stream() -> None:
ctx = StreamContext(model="test-model", api_format="openai:chat")
ctx.status_code = 503
ctx.chunk_count = 3
assert ctx.should_estimate_incomplete_tokens() is True
def test_should_estimate_incomplete_tokens_when_output_already_estimated() -> None:
"""ensure_estimated_output_tokens 已补了 output,但 input 仍为 0 时仍需估算。"""
ctx = StreamContext(model="test-model", api_format="openai:chat")
ctx.status_code = 503
ctx.chunk_count = 3
ctx.append_text("partial")
ctx.ensure_estimated_output_tokens()
assert ctx.output_tokens > 0
assert ctx.input_tokens == 0
assert ctx.should_estimate_incomplete_tokens() is True
@@ -1,123 +0,0 @@
import json
from typing import Any, AsyncIterator
import httpx
import pytest
from src.api.handlers.base.response_parser import (
ParsedChunk,
ParsedResponse,
ResponseParser,
StreamStats,
)
from src.api.handlers.base.stream_context import StreamContext
from src.api.handlers.base.stream_processor import StreamProcessor
from src.utils.sse_parser import SSEEventParser
class DummyParser(ResponseParser):
def parse_sse_line(self, line: str, stats: StreamStats) -> ParsedChunk | None:
return None
def parse_response(self, response: dict[str, Any], status_code: int) -> ParsedResponse:
return ParsedResponse(raw_response=response, status_code=status_code)
def extract_usage_from_response(self, response: dict[str, Any]) -> dict[str, int]:
return {}
def extract_text_content(self, response: dict[str, Any]) -> str:
return ""
def test_process_line_strips_newlines_and_finalizes_event() -> None:
ctx = StreamContext(model="test-model", api_format="openai:chat")
processor = StreamProcessor(request_id="test-request", default_parser=DummyParser())
sse_parser = SSEEventParser()
processor._process_line(ctx, sse_parser, 'data: {"type":"response.completed"}\n')
processor._process_line(ctx, sse_parser, "\n")
assert ctx.has_completion is True
def test_process_line_updates_openai_usage_from_usage_only_chunk() -> None:
ctx = StreamContext(model="test-model", api_format="openai:chat")
ctx.provider_api_format = "openai:chat"
processor = StreamProcessor(request_id="test-request", default_parser=DummyParser())
sse_parser = SSEEventParser()
usage_chunk = {
"id": "chatcmpl_test",
"object": "chat.completion.chunk",
"choices": [],
"usage": {"prompt_tokens": 10, "completion_tokens": 5, "total_tokens": 15},
}
processor._process_line(ctx, sse_parser, f"data: {json.dumps(usage_chunk)}\n")
processor._process_line(ctx, sse_parser, "\n")
assert ctx.input_tokens == 10
assert ctx.output_tokens == 5
def test_process_line_handles_openai_usage_chunk_followed_by_done_without_blank_line() -> None:
ctx = StreamContext(model="test-model", api_format="openai:chat")
ctx.provider_api_format = "openai:chat"
processor = StreamProcessor(request_id="test-request", default_parser=DummyParser())
sse_parser = SSEEventParser()
usage_chunk = {
"id": "chatcmpl_test",
"object": "chat.completion.chunk",
"choices": [],
"usage": {"prompt_tokens": 7, "completion_tokens": 3, "total_tokens": 10},
}
# Some SSE implementations may emit consecutive data lines without an empty separator.
processor._process_line(ctx, sse_parser, f"data: {json.dumps(usage_chunk)}\n")
processor._process_line(ctx, sse_parser, "data: [DONE]\n")
processor._process_line(ctx, sse_parser, "\n")
assert ctx.input_tokens == 7
assert ctx.output_tokens == 3
assert ctx.has_completion is True
class _DummyResponseCtx:
async def __aexit__(self, exc_type: type | None, exc: BaseException | None, tb: object) -> None:
return None
@pytest.mark.asyncio
async def test_create_response_stream_flushes_usage_on_remote_protocol_error() -> None:
ctx = StreamContext(model="test-model", api_format="openai:chat")
ctx.provider_api_format = "openai:chat"
processor = StreamProcessor(request_id="test-request", default_parser=DummyParser())
usage_chunk = {
"id": "chatcmpl_test",
"object": "chat.completion.chunk",
"choices": [],
"usage": {"prompt_tokens": 11, "completion_tokens": 4, "total_tokens": 15},
}
async def _iter_bytes_then_remote_protocol_error() -> AsyncIterator[bytes]:
yield f"data: {json.dumps(usage_chunk)}\n".encode("utf-8")
raise httpx.RemoteProtocolError("boom")
out = b""
async for b in processor.create_response_stream(
ctx=ctx,
byte_iterator=_iter_bytes_then_remote_protocol_error(),
response_ctx=_DummyResponseCtx(),
prefetched_chunks=[],
start_time=None,
):
out += b
# Stream ends gracefully (no exception), but usage is best-effort captured and request is marked failed.
assert b"data:" in out
assert ctx.input_tokens == 11
assert ctx.output_tokens == 4
assert ctx.status_code == 502
assert (ctx.error_message or "").startswith("upstream_stream_error:")
@@ -1,100 +0,0 @@
import json
from typing import Any
from unittest.mock import AsyncMock
import pytest
from src.api.handlers.base.response_parser import ParsedResponse, ResponseParser, StreamStats
from src.api.handlers.base.stream_context import StreamContext
from src.api.handlers.base.stream_processor import StreamProcessor
from src.core.api_format.conversion import register_default_normalizers
class DummyParser(ResponseParser):
def parse_sse_line(self, line: str, stats: StreamStats) -> Any | None: # noqa: ANN401
return None
def parse_response(self, response: dict[str, Any], status_code: int) -> ParsedResponse:
return ParsedResponse(raw_response=response, status_code=status_code)
def extract_usage_from_response(self, response: dict[str, Any]) -> dict[str, int]:
return {}
def extract_text_content(self, response: dict[str, Any]) -> str:
return ""
async def _empty_async_iter() -> Any:
if False: # pragma: no cover
yield b""
@pytest.mark.asyncio
async def test_create_response_stream_converts_claude_to_openai() -> None:
register_default_normalizers()
ctx = StreamContext(model="test-model", api_format="openai:chat")
ctx.client_api_format = "openai:chat"
ctx.provider_api_format = "claude:chat"
ctx.needs_conversion = True
processor = StreamProcessor(request_id="test-request", default_parser=DummyParser())
response_ctx = AsyncMock()
response_ctx.__aexit__ = AsyncMock(return_value=None)
message_start = {
"type": "message_start",
"message": {
"id": "msg_1",
"type": "message",
"role": "assistant",
"model": "claude-test",
"content": [],
"stop_reason": None,
"stop_sequence": None,
},
}
content_delta = {
"type": "content_block_delta",
"index": 0,
"delta": {"type": "text_delta", "text": "Hi"},
}
prefetched_chunks = [
b"event: message_start\n",
f"data: {json.dumps(message_start)}\n".encode("utf-8"),
b"\n",
f"data: {json.dumps(content_delta)}\n".encode("utf-8"),
b"\n",
]
out = b"".join(
[
chunk
async for chunk in processor.create_response_stream(
ctx,
byte_iterator=_empty_async_iter(),
response_ctx=response_ctx,
prefetched_chunks=prefetched_chunks,
)
]
)
text = out.decode("utf-8")
assert "event:" not in text
events = []
for line in text.splitlines():
if line.startswith("data: "):
if line == "data: [DONE]":
continue
events.append(json.loads(line[6:]))
assert len(events) >= 2
assert any(e.get("object") == "chat.completion.chunk" for e in events)
assert any(
e.get("choices", [{}])[0].get("delta", {}).get("content") == "Hi"
for e in events
if isinstance(e, dict)
)
@@ -1,90 +0,0 @@
import json
from typing import AsyncIterator
import pytest
from src.api.handlers.base.parsers import get_parser_for_format
from src.api.handlers.base.stream_context import StreamContext
from src.api.handlers.base.stream_processor import StreamProcessor
from src.core.api_format.conversion import register_default_normalizers
class _DummyResponseCtx:
async def __aexit__(self, exc_type: type | None, exc: BaseException | None, tb: object) -> None:
return None
async def _iter_bytes(chunks: list[bytes]) -> AsyncIterator[bytes]:
for c in chunks:
yield c
@pytest.mark.asyncio
async def test_stream_processor_converts_gemini_json_lines_without_data_prefix() -> None:
register_default_normalizers()
ctx = StreamContext(model="gemini-test", api_format="openai:chat")
ctx.provider_api_format = "gemini:chat"
ctx.client_api_format = "openai:chat"
ctx.needs_conversion = True
ctx.request_id = "req_test"
ctx.mapped_model = "gemini-test"
# Simulate Gemini JSON-array/chunks stream: wrapper lines + two JSON objects.
chunk1 = {
"candidates": [
{
"content": {"parts": [{"text": "Hello"}], "role": "model"},
}
]
}
chunk2 = {
"candidates": [
{
"content": {"parts": [{"text": "Hello world"}], "role": "model"},
"finishReason": "STOP",
}
],
"usageMetadata": {"promptTokenCount": 1, "candidatesTokenCount": 2, "totalTokenCount": 3},
}
upstream_lines = [
b"[\n",
(json.dumps(chunk1) + ",\n").encode("utf-8"),
(json.dumps(chunk2) + "\n").encode("utf-8"),
b"]\n",
]
processor = StreamProcessor(
request_id="req_test",
default_parser=get_parser_for_format("openai:chat"),
)
out = b""
async for b in processor.create_response_stream(
ctx=ctx,
byte_iterator=_iter_bytes(upstream_lines),
response_ctx=_DummyResponseCtx(),
prefetched_chunks=[],
start_time=None,
):
out += b
text = out.decode("utf-8", errors="replace")
data_lines = [ln for ln in text.splitlines() if ln.startswith("data: ")]
# OpenAI termination marker should be present (StreamProcessor will append if upstream doesn't send it).
assert "data: [DONE]" in data_lines
# Parse JSON events (excluding [DONE]) and validate we have expected deltas.
events = [json.loads(ln[6:]) for ln in data_lines if ln != "data: [DONE]"]
delta_contents: list[str] = []
for evt in events:
for choice in evt.get("choices", []) or []:
delta = choice.get("delta") or {}
if "content" in delta and delta["content"]:
delta_contents.append(delta["content"])
assert "Hello" in "".join(delta_contents)
assert " world" in "".join(delta_contents)
@@ -1,88 +0,0 @@
import json
from typing import Any
from unittest.mock import AsyncMock
import pytest
from src.api.handlers.base.response_parser import ParsedResponse, ResponseParser, StreamStats
from src.api.handlers.base.stream_context import StreamContext
from src.api.handlers.base.stream_processor import StreamProcessor
from src.core.api_format.conversion import register_default_normalizers
class _DummyParser(ResponseParser):
def parse_sse_line(self, line: str, stats: StreamStats) -> Any | None: # noqa: ANN401
return None
def parse_response(self, response: dict[str, Any], status_code: int) -> ParsedResponse:
return ParsedResponse(raw_response=response, status_code=status_code)
def extract_usage_from_response(self, response: dict[str, Any]) -> dict[str, int]:
return {}
def extract_text_content(self, response: dict[str, Any]) -> str:
return ""
async def _empty_async_iter() -> Any:
if False: # pragma: no cover
yield b""
@pytest.mark.asyncio
async def test_create_response_stream_converts_claude_to_openai_cli_with_event_lines() -> None:
register_default_normalizers()
ctx = StreamContext(model="test-model", api_format="openai:cli")
ctx.client_api_format = "openai:cli"
ctx.provider_api_format = "claude:chat"
ctx.needs_conversion = True
processor = StreamProcessor(request_id="test-request", default_parser=_DummyParser())
response_ctx = AsyncMock()
response_ctx.__aexit__ = AsyncMock(return_value=None)
message_start = {
"type": "message_start",
"message": {
"id": "msg_1",
"type": "message",
"role": "assistant",
"model": "claude-test",
"content": [],
"stop_reason": None,
"stop_sequence": None,
},
}
content_delta = {
"type": "content_block_delta",
"index": 0,
"delta": {"type": "text_delta", "text": "Hi"},
}
prefetched_chunks = [
b"event: message_start\n",
f"data: {json.dumps(message_start)}\n".encode("utf-8"),
b"\n",
f"data: {json.dumps(content_delta)}\n".encode("utf-8"),
b"\n",
]
out = b"".join(
[
chunk
async for chunk in processor.create_response_stream(
ctx,
byte_iterator=_empty_async_iter(),
response_ctx=response_ctx,
prefetched_chunks=prefetched_chunks,
)
]
)
text = out.decode("utf-8")
assert "event: response.output_item.added\n" in text
assert "event: response.content_part.added\n" in text
assert "event: response.output_text.delta\n" in text
assert "data: [DONE]" not in text
@@ -1,136 +0,0 @@
from __future__ import annotations
import os
import time
from types import SimpleNamespace
from typing import Any
from unittest.mock import AsyncMock
import pytest
os.environ.setdefault("JWT_SECRET_KEY", "test-secret-key")
from src.api.handlers.base import stream_telemetry as stream_telemetry_module
from src.api.handlers.base.stream_context import StreamContext
from src.api.handlers.base.stream_telemetry import StreamTelemetryRecorder
class _DummyDb:
def close(self) -> None:
pass
@pytest.mark.asyncio
async def test_record_stream_stats_estimates_tokens_for_failed_partial_stream(
monkeypatch: pytest.MonkeyPatch,
) -> None:
recorder = StreamTelemetryRecorder(
request_id="req-telemetry",
user_id="1",
api_key_id="2",
client_ip="127.0.0.1",
format_id="openai:chat",
)
recorder._get_telemetry_writer = AsyncMock( # type: ignore[method-assign]
return_value=SimpleNamespace(include_bodies=False)
)
recorder._dispatch_record = AsyncMock() # type: ignore[method-assign]
recorder._update_candidate_status = AsyncMock() # type: ignore[method-assign]
ctx = StreamContext(
model="test-model",
api_format="openai:chat",
request_id="req-telemetry",
user_id=1,
api_key_id=2,
)
ctx.provider_name = "test-provider"
ctx.status_code = 503
ctx.data_count = 2
ctx.chunk_count = 4
ctx.append_text("partial output")
monkeypatch.setattr(stream_telemetry_module, "get_db", lambda: iter([_DummyDb()]))
monkeypatch.setattr(
stream_telemetry_module.SystemConfigService,
"should_log_body",
lambda _db: False,
)
monkeypatch.setattr(stream_telemetry_module.config, "stream_stats_delay", 0)
await recorder.record_stream_stats(
ctx,
original_headers={},
original_request_body={"input": [{"content": "hello world"}]},
start_time=time.time(),
)
assert ctx.input_tokens > 0
assert ctx.output_tokens == max(1, len("partial output") // 4)
recorder._dispatch_record.assert_awaited_once() # type: ignore[attr-defined]
@pytest.mark.asyncio
async def test_record_stream_stats_releases_parsed_chunks_after_dispatch(
monkeypatch: pytest.MonkeyPatch,
) -> None:
recorder = StreamTelemetryRecorder(
request_id="req-release",
user_id="1",
api_key_id="2",
client_ip="127.0.0.1",
format_id="openai:chat",
)
recorder._get_telemetry_writer = AsyncMock( # type: ignore[method-assign]
return_value=SimpleNamespace(include_bodies=True)
)
recorder._update_candidate_status = AsyncMock() # type: ignore[method-assign]
dispatch_payloads: list[dict[str, Any]] = []
async def _capture_dispatch(*args: Any, **kwargs: Any) -> None:
dispatch_payloads.append(
{
"response_body": args[5],
"client_response_body": kwargs.get("client_response_body"),
}
)
recorder._dispatch_record = _capture_dispatch # type: ignore[method-assign]
ctx = StreamContext(
model="test-model",
api_format="openai:chat",
request_id="req-release",
user_id=1,
api_key_id=2,
)
ctx.provider_name = "test-provider"
ctx.parsed_chunks.extend([{"type": "chunk-1"}, {"type": "chunk-2"}])
ctx.data_count = 2
ctx.chunk_count = 2
monkeypatch.setattr(stream_telemetry_module, "get_db", lambda: iter([_DummyDb()]))
monkeypatch.setattr(
stream_telemetry_module.SystemConfigService,
"should_log_body",
lambda _db: True,
)
monkeypatch.setattr(stream_telemetry_module.config, "stream_stats_delay", 0)
await recorder.record_stream_stats(
ctx,
original_headers={},
original_request_body={"input": [{"content": "hello world"}]},
start_time=time.time(),
)
assert len(dispatch_payloads) == 1
response_body = dispatch_payloads[0]["response_body"]
assert response_body["chunks"] == [{"type": "chunk-1"}, {"type": "chunk-2"}]
assert response_body["metadata"]["stream"] is True
assert response_body["metadata"]["total_chunks"] == 2
assert response_body["metadata"]["data_count"] == 2
assert dispatch_payloads[0]["client_response_body"] is None
assert ctx.parsed_chunks == []
assert ctx.provider_parsed_chunks == []
@@ -1,145 +0,0 @@
from __future__ import annotations
import json
from collections.abc import AsyncIterator
import pytest
from src.api.handlers.base.upstream_stream_bridge import (
aggregate_upstream_stream_to_internal_response,
)
from src.config.constants import StreamDefaults
from src.core.api_format.conversion import register_default_normalizers
from src.core.api_format.conversion.internal import TextBlock
from src.core.exceptions import ProviderNotAvailableException
async def _iter_stream_lines(lines: list[str]) -> AsyncIterator[bytes]:
for line in lines:
yield line.encode("utf-8")
@pytest.mark.asyncio
async def test_aggregate_claude_stream_uses_message_start_usage_when_message_delta_absent() -> None:
register_default_normalizers()
lines = [
"data: "
+ json.dumps(
{
"type": "message_start",
"message": {
"id": "msg_bridge_usage",
"type": "message",
"role": "assistant",
"model": "claude-sonnet-4-5",
"content": [],
"usage": {
"input_tokens": 120,
"output_tokens": 0,
"cache_read_input_tokens": 11,
},
},
},
ensure_ascii=False,
)
+ "\n",
"data: "
+ json.dumps(
{
"type": "content_block_start",
"index": 0,
"content_block": {"type": "text", "text": ""},
},
ensure_ascii=False,
)
+ "\n",
"data: "
+ json.dumps(
{
"type": "content_block_delta",
"index": 0,
"delta": {"type": "text_delta", "text": "hello"},
},
ensure_ascii=False,
)
+ "\n",
"data: "
+ json.dumps({"type": "content_block_stop", "index": 0}, ensure_ascii=False)
+ "\n",
]
internal = await aggregate_upstream_stream_to_internal_response(
_iter_stream_lines(lines),
provider_api_format="claude:cli",
provider_name="claude_code",
model="claude-sonnet-4-5",
request_id="req_bridge_usage",
)
assert internal.usage is not None
assert internal.usage.input_tokens == 120
assert internal.usage.output_tokens == 0
assert internal.usage.cache_read_tokens == 11
assert len(internal.content) == 1
assert isinstance(internal.content[0], TextBlock)
assert internal.content[0].text == "hello"
@pytest.mark.asyncio
async def test_aggregate_stream_raises_when_buffer_exceeds_limit() -> None:
register_default_normalizers()
async def _iter_overflow_bytes() -> AsyncIterator[bytes]:
yield b"x" * (StreamDefaults.MAX_STREAM_BUFFER_BYTES + 1)
with pytest.raises(ProviderNotAvailableException):
await aggregate_upstream_stream_to_internal_response(
_iter_overflow_bytes(),
provider_api_format="claude:cli",
provider_name="claude_code",
model="claude-sonnet-4-5",
request_id="req_bridge_overflow",
)
@pytest.mark.asyncio
async def test_aggregate_stream_raises_when_total_buffer_exceeds_hard_limit(
monkeypatch: pytest.MonkeyPatch,
) -> None:
register_default_normalizers()
monkeypatch.setattr(StreamDefaults, "MAX_STREAM_BUFFER_BYTES", 64)
monkeypatch.setattr(StreamDefaults, "MAX_STREAM_BUFFER_TOTAL_BYTES", 80)
async def _iter_total_overflow_bytes() -> AsyncIterator[bytes]:
yield b":" + (b"a" * 30) + b"\n" + b":" + (b"b" * 30) + b"\n" + b":" + (b"c" * 30) + b"\n"
with pytest.raises(ProviderNotAvailableException):
await aggregate_upstream_stream_to_internal_response(
_iter_total_overflow_bytes(),
provider_api_format="claude:cli",
provider_name="claude_code",
model="claude-sonnet-4-5",
request_id="req_bridge_total_overflow",
)
@pytest.mark.asyncio
async def test_aggregate_stream_allows_large_chunk_with_multiple_complete_lines(
monkeypatch: pytest.MonkeyPatch,
) -> None:
register_default_normalizers()
monkeypatch.setattr(StreamDefaults, "MAX_STREAM_BUFFER_BYTES", 64)
async def _iter_multiline_bytes() -> AsyncIterator[bytes]:
yield b":" + (b"a" * 30) + b"\n" + b":" + (b"b" * 30) + b"\n" + b":" + (b"c" * 30) + b"\n"
internal = await aggregate_upstream_stream_to_internal_response(
_iter_multiline_bytes(),
provider_api_format="claude:cli",
provider_name="claude_code",
model="claude-sonnet-4-5",
request_id="req_bridge_multiline",
)
assert internal is not None
-191
View File
@@ -1,191 +0,0 @@
"""测试 handler 基础工具函数"""
import gzip
import json
import pytest
from src.api.handlers.base.utils import (
build_json_response_for_client,
build_sse_headers,
filter_proxy_response_headers,
resolve_client_accept_encoding,
resolve_client_content_encoding,
)
from src.core.usage_tokens import extract_cache_creation_tokens
class TestExtractCacheCreationTokens:
"""测试 extract_cache_creation_tokens 函数"""
def test_new_format_only(self) -> None:
"""测试只有新格式字段"""
usage = {
"claude_cache_creation_5_m_tokens": 100,
"claude_cache_creation_1_h_tokens": 200,
}
assert extract_cache_creation_tokens(usage) == 300
def test_new_format_5m_only(self) -> None:
"""测试只有 5 分钟缓存"""
usage = {
"claude_cache_creation_5_m_tokens": 150,
"claude_cache_creation_1_h_tokens": 0,
}
assert extract_cache_creation_tokens(usage) == 150
def test_new_format_1h_only(self) -> None:
"""测试只有 1 小时缓存"""
usage = {
"claude_cache_creation_5_m_tokens": 0,
"claude_cache_creation_1_h_tokens": 250,
}
assert extract_cache_creation_tokens(usage) == 250
def test_old_format_only(self) -> None:
"""测试只有旧格式字段"""
usage = {
"cache_creation_input_tokens": 500,
}
assert extract_cache_creation_tokens(usage) == 500
def test_both_formats_prefers_new(self) -> None:
"""测试同时存在时优先使用新格式"""
usage = {
"claude_cache_creation_5_m_tokens": 100,
"claude_cache_creation_1_h_tokens": 200,
"cache_creation_input_tokens": 999, # 应该被忽略
}
assert extract_cache_creation_tokens(usage) == 300
def test_empty_usage(self) -> None:
"""测试空字典"""
usage: dict[str, int] = {}
assert extract_cache_creation_tokens(usage) == 0
def test_all_zeros(self) -> None:
"""测试所有字段都为 0"""
usage = {
"claude_cache_creation_5_m_tokens": 0,
"claude_cache_creation_1_h_tokens": 0,
"cache_creation_input_tokens": 0,
}
assert extract_cache_creation_tokens(usage) == 0
def test_partial_new_format_with_old_format_fallback(self) -> None:
"""测试新格式字段不存在时回退到旧格式"""
usage = {
"cache_creation_input_tokens": 123,
}
assert extract_cache_creation_tokens(usage) == 123
def test_new_format_zero_should_not_fallback(self) -> None:
"""测试新格式字段存在但为 0 时,不应 fallback 到旧格式"""
usage = {
"claude_cache_creation_5_m_tokens": 0,
"claude_cache_creation_1_h_tokens": 0,
"cache_creation_input_tokens": 456,
}
# 新格式字段存在,即使值为 0 也应该使用新格式(返回 0)
# 而不是 fallback 到旧格式(返回 456
assert extract_cache_creation_tokens(usage) == 0
def test_unrelated_fields_ignored(self) -> None:
"""测试忽略无关字段"""
usage = {
"input_tokens": 1000,
"output_tokens": 2000,
"cache_read_input_tokens": 300,
"claude_cache_creation_5_m_tokens": 50,
"claude_cache_creation_1_h_tokens": 75,
}
assert extract_cache_creation_tokens(usage) == 125
class TestBuildSSEHeaders:
def test_default_headers(self) -> None:
headers = build_sse_headers()
assert headers["Cache-Control"] == "no-cache, no-transform"
assert headers["X-Accel-Buffering"] == "no"
def test_merge_extra_headers(self) -> None:
headers = build_sse_headers({"X-Test": "1", "Cache-Control": "custom"})
assert headers["X-Test"] == "1"
assert headers["Cache-Control"] == "custom"
class TestFilterProxyResponseHeaders:
def test_none_returns_empty(self) -> None:
assert filter_proxy_response_headers(None) == {}
def test_filters_blocklisted_headers_case_insensitive(self) -> None:
headers = {
"Content-Length": "123",
"content-encoding": "gzip",
"Transfer-Encoding": "chunked",
"Connection": "keep-alive",
"Keep-Alive": "timeout=5",
"Content-Type": "application/json",
"X-Request-Id": "abc",
"Anthropic-RateLimit-Requests-Remaining": "10",
}
result = filter_proxy_response_headers(headers)
assert "Content-Length" not in result
assert "content-encoding" not in result
assert "Transfer-Encoding" not in result
assert "Connection" not in result
assert "Keep-Alive" not in result
assert "Content-Type" not in result
assert result["X-Request-Id"] == "abc"
assert result["Anthropic-RateLimit-Requests-Remaining"] == "10"
class TestResolveClientEncoding:
def test_content_encoding_prefers_hint(self) -> None:
headers = {"content-encoding": "gzip"}
result = resolve_client_content_encoding(headers, hinted_content_encoding="br")
assert result == "br"
def test_content_encoding_fallback_to_headers(self) -> None:
headers = {"Content-Encoding": "gzip"}
result = resolve_client_content_encoding(headers)
assert result == "gzip"
def test_accept_encoding_prefers_hint(self) -> None:
headers = {"accept-encoding": "gzip"}
result = resolve_client_accept_encoding(headers, hinted_accept_encoding="br")
assert result == "br"
def test_accept_encoding_fallback_to_headers(self) -> None:
headers = {"Accept-Encoding": "gzip, deflate"}
result = resolve_client_accept_encoding(headers)
assert result == "gzip, deflate"
class TestBuildJsonResponseForClient:
def test_returns_gzip_response_when_client_accepts_gzip(self) -> None:
response = build_json_response_for_client(
status_code=200,
content={"ok": True},
headers={"content-type": "application/json"},
client_accept_encoding="gzip, deflate",
)
assert response.headers.get("content-encoding") == "gzip"
assert "accept-encoding" in response.headers.get("vary", "").lower()
decompressed = gzip.decompress(bytes(response.body))
assert json.loads(decompressed.decode("utf-8")) == {"ok": True}
def test_returns_plain_json_when_gzip_not_accepted(self) -> None:
response = build_json_response_for_client(
status_code=200,
content={"ok": True},
headers={"content-type": "application/json"},
client_accept_encoding="gzip;q=0, deflate",
)
assert response.headers.get("content-encoding") is None
assert json.loads(bytes(response.body).decode("utf-8")) == {"ok": True}
@@ -1,19 +0,0 @@
from src.api.handlers.gemini.stream_parser import GeminiStreamParser
def test_is_done_event_true_for_new_finish_reason_values() -> None:
parser = GeminiStreamParser()
event = {"candidates": [{"finishReason": "MALFORMED_FUNCTION_CALL"}]}
assert parser.is_done_event(event) is True
def test_is_done_event_false_for_unspecified() -> None:
parser = GeminiStreamParser()
event = {"candidates": [{"finishReason": "FINISH_REASON_UNSPECIFIED"}]}
assert parser.is_done_event(event) is False
def test_is_done_event_false_when_no_candidates_or_reason() -> None:
parser = GeminiStreamParser()
assert parser.is_done_event({}) is False
assert parser.is_done_event({"candidates": [{}]}) is False
@@ -1,251 +0,0 @@
from __future__ import annotations
from collections.abc import AsyncGenerator
from datetime import datetime, timezone
from types import SimpleNamespace
from unittest.mock import AsyncMock
import httpx
import pytest
from fastapi.responses import JSONResponse, StreamingResponse
import src.api.handlers.gemini.video_handler as video_mod
import src.services.proxy_node.resolver as resolver_mod
import src.services.request.execution_runtime_client as rust_client_mod
from src.api.handlers.gemini.video_handler import GeminiVeoHandler
from src.core.api_format.conversion.internal_video import VideoStatus
from src.core.exceptions import ProviderNotAvailableException
from src.services.request.execution_runtime_client import ExecutionRuntimeStreamResult
class _DummyStreamResponseCtx:
def __init__(self) -> None:
self.closed = False
async def __aexit__(self, exc_type: object, exc: object, tb: object) -> None:
self.closed = True
async def _iter_chunks(chunks: list[bytes]) -> AsyncGenerator[bytes]:
for chunk in chunks:
yield chunk
def _make_handler() -> GeminiVeoHandler:
return GeminiVeoHandler(
db=SimpleNamespace(),
user=SimpleNamespace(id="user-1"),
api_key=SimpleNamespace(id="api-key-1"),
request_id="req-gemini-video-test",
client_ip="127.0.0.1",
user_agent="pytest",
start_time=0.0,
)
@pytest.mark.asyncio
async def test_handle_create_task_uses_rust_sync_helper(
monkeypatch: pytest.MonkeyPatch,
) -> None:
handler = _make_handler()
monkeypatch.setattr(video_mod.UsageService, "create_pending_usage", lambda *args, **kwargs: None)
monkeypatch.setattr(
handler._normalizer,
"video_request_to_internal",
lambda body: SimpleNamespace(model=str(body.get("model") or "veo-3")),
)
candidate = SimpleNamespace(provider=SimpleNamespace(name="provider-1", id="prov-1"))
endpoint = SimpleNamespace(
id="ep-1",
provider_id="prov-1",
api_family="gemini",
endpoint_kind="video",
base_url="https://generativelanguage.googleapis.com",
body_rules=None,
)
provider_key = SimpleNamespace(id="key-1")
monkeypatch.setattr(
handler,
"_resolve_upstream_key",
AsyncMock(return_value=("upstream-key", endpoint, provider_key, None)),
)
monkeypatch.setattr(
handler,
"_build_upstream_url",
lambda base_url, model: f"https://generativelanguage.googleapis.com/v1beta/models/{model}:predictLongRunning",
)
monkeypatch.setattr(
handler,
"_build_upstream_headers",
lambda original_headers, upstream_key, endpoint, auth_info, **kwargs: {
"x-goog-api-key": upstream_key
},
)
async def _fake_rust_sync(**kwargs: object) -> httpx.Response:
assert kwargs["method"] == "POST"
assert kwargs["provider_id"] == "prov-1"
assert kwargs["endpoint_id"] == "ep-1"
assert kwargs["key_id"] == "key-1"
assert kwargs["body"] == {"model": "veo-3", "prompt": "hello"}
return httpx.Response(
200,
request=httpx.Request("POST", str(kwargs["url"])),
json={"name": "operations/ext-1"},
)
monkeypatch.setattr(handler, "_try_rust_sync_http_response", _fake_rust_sync)
async def _fake_submit_with_failover(**kwargs: object) -> JSONResponse:
response = await kwargs["submit_func"](candidate)
assert response.status_code == 200
assert response.json()["name"] == "operations/ext-1"
return JSONResponse(status_code=400, content={"error": {"message": "stop"}})
monkeypatch.setattr(handler, "_submit_with_failover", _fake_submit_with_failover)
response = await handler.handle_create_task(
http_request=SimpleNamespace(
headers={},
url=SimpleNamespace(scheme="https", netloc="example.com"),
),
original_headers={},
original_request_body={"model": "veo-3", "prompt": "hello"},
)
assert response.status_code == 400
@pytest.mark.asyncio
async def test_handle_download_content_uses_rust_executor_with_proxy_snapshot(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setattr(video_mod.config, "executor_backend", "rust")
handler = _make_handler()
dummy_ctx = _DummyStreamResponseCtx()
monkeypatch.setattr(
handler,
"_get_task_by_external_id",
lambda task_id: SimpleNamespace(
id=task_id,
status=VideoStatus.COMPLETED.value,
video_url="https://storage.example.com/video.mp4",
video_expires_at=datetime.now(timezone.utc).replace(year=2099),
model="veo-3",
),
)
endpoint = SimpleNamespace(
id="ep-1",
provider_id="prov-1",
api_family="gemini",
endpoint_kind="video",
base_url="https://generativelanguage.googleapis.com",
proxy={"enabled": True, "url": "http://proxy.local:8080"},
)
key = SimpleNamespace(id="key-1", api_key="encrypted", proxy=None)
monkeypatch.setattr(handler, "_get_endpoint_and_key", lambda task: (endpoint, key))
monkeypatch.setattr(video_mod.crypto_service, "decrypt", lambda _: "upstream-key")
monkeypatch.setattr(
video_mod,
"get_provider_auth",
AsyncMock(return_value=None),
)
monkeypatch.setattr(
video_mod,
"resolve_provider_proxy",
lambda endpoint, key: {"enabled": True, "url": "http://proxy.local:8080"},
)
monkeypatch.setattr(
resolver_mod,
"resolve_effective_proxy",
lambda provider_proxy, key_proxy=None: provider_proxy,
)
monkeypatch.setattr(
resolver_mod,
"get_system_proxy_config_async",
AsyncMock(return_value=None),
)
monkeypatch.setattr(
resolver_mod,
"resolve_delegate_config_async",
AsyncMock(return_value=None),
)
async def _fake_build_proxy_url_async(proxy_config: object) -> str:
assert proxy_config == {"enabled": True, "url": "http://proxy.local:8080"}
return "http://proxy.local:8080"
async def _fake_resolve_proxy_info_async(proxy_config: object) -> dict[str, str]:
assert proxy_config == {"enabled": True, "url": "http://proxy.local:8080"}
return {"url": "http://proxy.local:8080"}
monkeypatch.setattr(resolver_mod, "build_proxy_url_async", _fake_build_proxy_url_async)
monkeypatch.setattr(
resolver_mod,
"resolve_proxy_info_async",
_fake_resolve_proxy_info_async,
)
async def _fake_execute_stream(self: object, plan: object) -> ExecutionRuntimeStreamResult:
assert getattr(plan, "method") == "GET"
assert getattr(plan, "url") == "https://storage.example.com/video.mp4"
assert getattr(plan, "headers") == {"x-goog-api-key": "upstream-key"}
assert getattr(plan, "proxy").url == "http://proxy.local:8080"
return ExecutionRuntimeStreamResult(
status_code=200,
headers={"content-type": "video/mp4", "x-rust-download": "true"},
byte_iterator=_iter_chunks([b"gemini-", b"video"]),
response_ctx=dummy_ctx,
)
monkeypatch.setattr(rust_client_mod.ExecutionRuntimeClient, "execute_stream", _fake_execute_stream)
response = await handler.handle_download_content(
task_id="operations/ext-1",
http_request=SimpleNamespace(),
original_headers={},
query_params=None,
)
assert isinstance(response, StreamingResponse)
body = b"".join([chunk async for chunk in response.body_iterator])
assert body == b"gemini-video"
assert dummy_ctx.closed is True
@pytest.mark.asyncio
async def test_handle_download_content_raises_when_rust_backend_disabled(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setattr(video_mod.config, "executor_backend", "python")
handler = _make_handler()
monkeypatch.setattr(
handler,
"_get_task_by_external_id",
lambda task_id: SimpleNamespace(
id=task_id,
status=VideoStatus.COMPLETED.value,
video_url="https://storage.example.com/video.mp4",
video_expires_at=datetime.now(timezone.utc).replace(year=2099),
model="veo-3",
),
)
monkeypatch.setattr(
handler,
"_get_endpoint_and_key",
lambda task: (
SimpleNamespace(id="ep-1", provider_id="prov-1", proxy=None),
SimpleNamespace(id="key-1", api_key=None, proxy=None),
),
)
with pytest.raises(ProviderNotAvailableException):
await handler.handle_download_content(
task_id="operations/ext-1",
http_request=SimpleNamespace(),
original_headers={},
query_params=None,
)
@@ -1,189 +0,0 @@
from __future__ import annotations
from collections.abc import AsyncGenerator
from types import SimpleNamespace
from typing import Any
import pytest
from fastapi.responses import StreamingResponse
import src.api.handlers.openai.video_handler as video_mod
from src.api.handlers.openai.video_handler import OpenAIVideoHandler
from src.core.api_format.conversion.internal_video import VideoStatus
from src.core.exceptions import ProviderNotAvailableException
from src.services.request.execution_runtime_client import (
ExecutionRuntimeClientError,
ExecutionRuntimeStreamResult,
)
class _DummyStreamResponseCtx:
def __init__(self) -> None:
self.closed = False
async def __aexit__(self, exc_type: object, exc: object, tb: object) -> None:
self.closed = True
async def _iter_chunks(chunks: list[bytes]) -> AsyncGenerator[bytes]:
for chunk in chunks:
yield chunk
def _make_handler() -> OpenAIVideoHandler:
return OpenAIVideoHandler(
db=SimpleNamespace(),
user=SimpleNamespace(id="user-1"),
api_key=SimpleNamespace(id="api-key-1"),
request_id="req-video-test",
client_ip="127.0.0.1",
user_agent="pytest",
start_time=0.0,
)
@pytest.mark.asyncio
async def test_handle_download_content_uses_rust_executor_for_direct_video_url(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setattr(video_mod.config, "executor_backend", "rust")
handler = _make_handler()
dummy_ctx = _DummyStreamResponseCtx()
monkeypatch.setattr(
handler,
"_get_task",
lambda task_id: SimpleNamespace(
id=task_id,
status=VideoStatus.COMPLETED.value,
video_url="https://cdn.example.com/video.mp4",
model="sora-2",
),
)
async def _fake_execute_stream(self: object, plan: object) -> ExecutionRuntimeStreamResult:
assert getattr(plan, "method") == "GET"
assert getattr(plan, "url") == "https://cdn.example.com/video.mp4"
assert getattr(plan, "body").json_body is None
assert getattr(plan, "body").body_bytes_b64 is None
return ExecutionRuntimeStreamResult(
status_code=200,
headers={"content-type": "video/mp4", "x-rust-download": "true"},
byte_iterator=_iter_chunks([b"video-", b"bytes"]),
response_ctx=dummy_ctx,
)
monkeypatch.setattr(video_mod.ExecutionRuntimeClient, "execute_stream", _fake_execute_stream)
response = await handler.handle_download_content(
task_id="task-1",
http_request=SimpleNamespace(),
original_headers={},
query_params={"variant": "video"},
)
assert isinstance(response, StreamingResponse)
assert response.headers["x-rust-download"] == "true"
body = b"".join([chunk async for chunk in response.body_iterator])
assert body == b"video-bytes"
assert dummy_ctx.closed is True
@pytest.mark.asyncio
async def test_handle_download_content_uses_rust_executor_for_upstream_content_endpoint(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setattr(video_mod.config, "executor_backend", "rust")
handler = _make_handler()
dummy_ctx = _DummyStreamResponseCtx()
monkeypatch.setattr(
handler,
"_get_task",
lambda task_id: SimpleNamespace(
id=task_id,
status=VideoStatus.COMPLETED.value,
video_url=None,
external_task_id="ext-1",
model="sora-2",
),
)
monkeypatch.setattr(
handler,
"_get_endpoint_and_key",
lambda task: (
SimpleNamespace(id="ep-1", provider_id="prov-1", base_url="https://api.openai.com"),
SimpleNamespace(id="key-1", api_key="encrypted"),
),
)
monkeypatch.setattr(video_mod.crypto_service, "decrypt", lambda _: "upstream-key")
monkeypatch.setattr(
handler,
"_build_upstream_url",
lambda base_url, suffix=None: "https://api.openai.com/v1/videos/ext-1/content",
)
monkeypatch.setattr(
handler,
"_build_upstream_headers",
lambda original_headers, upstream_key, endpoint: {"authorization": f"Bearer {upstream_key}"},
)
async def _fake_execute_stream(self: object, plan: object) -> ExecutionRuntimeStreamResult:
assert getattr(plan, "method") == "GET"
assert getattr(plan, "url") == "https://api.openai.com/v1/videos/ext-1/content"
assert getattr(plan, "headers") == {"authorization": "Bearer upstream-key"}
assert getattr(plan, "provider_id") == "prov-1"
assert getattr(plan, "endpoint_id") == "ep-1"
assert getattr(plan, "key_id") == "key-1"
return ExecutionRuntimeStreamResult(
status_code=200,
headers={"content-type": "video/mp4", "x-rust-download": "true"},
byte_iterator=_iter_chunks([b"upstream-", b"video"]),
response_ctx=dummy_ctx,
)
monkeypatch.setattr(video_mod.ExecutionRuntimeClient, "execute_stream", _fake_execute_stream)
response = await handler.handle_download_content(
task_id="task-1",
http_request=SimpleNamespace(),
original_headers={},
query_params=None,
)
assert isinstance(response, StreamingResponse)
body = b"".join([chunk async for chunk in response.body_iterator])
assert body == b"upstream-video"
assert dummy_ctx.closed is True
@pytest.mark.asyncio
async def test_handle_download_content_raises_when_rust_executor_unavailable(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setattr(video_mod.config, "executor_backend", "rust")
handler = _make_handler()
monkeypatch.setattr(
handler,
"_get_task",
lambda task_id: SimpleNamespace(
id=task_id,
status=VideoStatus.COMPLETED.value,
video_url="https://cdn.example.com/video.mp4",
model="sora-2",
),
)
async def _failing_execute_stream(self: object, plan: object) -> ExecutionRuntimeStreamResult:
del self, plan
raise ExecutionRuntimeClientError("executor down")
monkeypatch.setattr(video_mod.ExecutionRuntimeClient, "execute_stream", _failing_execute_stream)
with pytest.raises(ProviderNotAvailableException):
await handler.handle_download_content(
task_id="task-1",
http_request=SimpleNamespace(),
original_headers={},
query_params={"variant": "video"},
)
@@ -1,103 +0,0 @@
from __future__ import annotations
from types import SimpleNamespace
from unittest.mock import AsyncMock
import httpx
import pytest
from fastapi.responses import JSONResponse
import src.api.handlers.openai.video_handler as video_mod
from src.api.handlers.openai.video_handler import OpenAIVideoHandler
def _make_handler() -> OpenAIVideoHandler:
return OpenAIVideoHandler(
db=SimpleNamespace(),
user=SimpleNamespace(id="user-1"),
api_key=SimpleNamespace(id="api-key-1"),
request_id="req-video-sync-test",
client_ip="127.0.0.1",
user_agent="pytest",
start_time=0.0,
)
@pytest.mark.asyncio
async def test_handle_create_task_uses_rust_sync_helper(
monkeypatch: pytest.MonkeyPatch,
) -> None:
handler = _make_handler()
monkeypatch.setattr(video_mod.UsageService, "create_pending_usage", lambda *args, **kwargs: None)
monkeypatch.setattr(
handler._normalizer,
"video_request_to_internal",
lambda body: SimpleNamespace(model=str(body.get("model") or "sora-2")),
)
candidate = SimpleNamespace(provider=SimpleNamespace(name="provider-1", id="prov-1"))
endpoint = SimpleNamespace(
id="ep-1",
api_family="openai",
endpoint_kind="video",
base_url="https://api.openai.com",
body_rules=None,
)
provider_key = SimpleNamespace(id="key-1")
monkeypatch.setattr(
handler,
"_resolve_upstream_key",
AsyncMock(return_value=("upstream-key", endpoint, provider_key)),
)
monkeypatch.setattr(
handler,
"_build_upstream_url",
lambda base_url: "https://api.openai.com/v1/videos",
)
monkeypatch.setattr(
handler,
"_build_upstream_headers",
lambda original_headers, upstream_key, endpoint, **kwargs: {
"authorization": f"Bearer {upstream_key}"
},
)
monkeypatch.setattr(
video_mod.HTTPClientPool,
"get_default_client_async",
AsyncMock(side_effect=AssertionError("python fallback should not run")),
)
async def _fake_rust_sync(**kwargs: object) -> httpx.Response:
assert kwargs["method"] == "POST"
assert kwargs["url"] == "https://api.openai.com/v1/videos"
assert kwargs["provider_id"] == "prov-1"
assert kwargs["endpoint_id"] == "ep-1"
assert kwargs["key_id"] == "key-1"
assert kwargs["body"] == {"model": "sora-2", "prompt": "hello"}
return httpx.Response(
200,
request=httpx.Request("POST", str(kwargs["url"])),
json={"id": "ext-1"},
)
create_failed = AsyncMock()
monkeypatch.setattr(handler, "_try_rust_sync_http_response", _fake_rust_sync)
monkeypatch.setattr(handler, "_create_failed_task_and_usage", create_failed)
async def _fake_submit_with_failover(**kwargs: object) -> JSONResponse:
response = await kwargs["submit_func"](candidate)
assert response.status_code == 200
assert response.json()["id"] == "ext-1"
return JSONResponse(status_code=400, content={"error": {"message": "stop"}})
monkeypatch.setattr(handler, "_submit_with_failover", _fake_submit_with_failover)
response = await handler.handle_create_task(
http_request=SimpleNamespace(),
original_headers={},
original_request_body={"model": "sora-2", "prompt": "hello"},
)
assert response.status_code == 400
create_failed.assert_awaited_once()
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -1,7 +0,0 @@
"""Compatibility shim for legacy imports.
Decision/plan tests now live in:
- test_chat_decision_plan.py
- test_cli_decision_plan.py
- test_decision_plan_misc.py
"""
@@ -1,158 +0,0 @@
"""Gateway internal decision/plan builder tests."""
import asyncio
import base64
import json
import time
from types import SimpleNamespace
from typing import Any
from unittest.mock import AsyncMock, MagicMock
import pytest
from fastapi import BackgroundTasks, FastAPI
from fastapi.responses import JSONResponse, StreamingResponse
from fastapi.testclient import TestClient
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from src.api.internal.gateway import (
CONTROL_ACTION_HEADER,
CONTROL_ACTION_PROXY_PUBLIC,
CONTROL_EXECUTED_HEADER,
GatewayAuthContext,
GatewayExecutionDecisionResponse,
GatewayExecuteRequest,
GatewayExecutionPlanResponse,
GatewayResolveRequest,
GatewayStreamReportRequest,
GatewaySyncReportRequest,
_dispatch_gateway_sync_telemetry,
_build_gateway_sync_error_payload,
_build_gateway_sync_telemetry_writer,
_run_gateway_stream_report_background,
_run_gateway_sync_report_background,
_stream_executor_requires_python_rewrite,
_build_claude_chat_sync_decision,
_build_claude_chat_stream_decision,
_build_claude_cli_sync_decision,
_build_claude_cli_stream_decision,
_build_gemini_files_download_stream_decision,
_build_gemini_files_proxy_sync_decision,
_build_gemini_chat_sync_decision,
_build_gemini_chat_stream_decision,
_build_gemini_cli_sync_decision,
_build_gemini_cli_stream_decision,
_build_openai_chat_sync_decision,
_build_openai_chat_stream_decision,
_build_openai_cli_stream_decision,
_build_openai_video_content_stream_decision,
_extract_gateway_sync_error_message,
_record_gateway_direct_candidate_graph,
_resolve_gateway_sync_error_status_code,
_build_openai_chat_sync_plan,
_build_openai_cli_sync_decision,
_build_openai_cli_stream_plan,
_build_openai_cli_sync_plan,
_is_streaming_sync_payload,
_resolve_auth_context,
_resolve_gateway_sync_adapter,
classify_gateway_route,
router,
)
from src.database import get_db
from src.models.database import Base, RequestCandidate
from src.services.orchestration.candidate_resolver import CandidateResolver
from src.services.scheduling.schemas import PoolCandidate, ProviderCandidate
from src.services.request.execution_runtime_plan import ExecutionPlan, ExecutionPlanBody, PreparedExecutionPlan
def _wait_until(predicate: Any, *, timeout: float = 1.0, interval: float = 0.01) -> None:
deadline = time.time() + timeout
while time.time() < deadline:
if predicate():
return
time.sleep(interval)
assert predicate()
def test_stream_executor_requires_python_rewrite_allows_antigravity_same_format() -> None:
class AntigravityEnvelope:
name = "antigravity:v1internal"
def force_stream_rewrite(self) -> bool:
return True
envelope = AntigravityEnvelope()
assert (
_stream_executor_requires_python_rewrite(
envelope=envelope,
needs_conversion=False,
provider_api_format="gemini:cli",
client_api_format="gemini:cli",
)
is False
)
assert (
_stream_executor_requires_python_rewrite(
envelope=envelope,
needs_conversion=False,
provider_api_format="gemini:chat",
client_api_format="gemini:chat",
)
is False
)
assert (
_stream_executor_requires_python_rewrite(
envelope=envelope,
needs_conversion=False,
provider_api_format="gemini:cli",
client_api_format="openai:cli",
)
is True
)
assert (
_stream_executor_requires_python_rewrite(
envelope=envelope,
needs_conversion=True,
provider_api_format="gemini:chat",
client_api_format="openai:chat",
)
is False
)
assert (
_stream_executor_requires_python_rewrite(
envelope=envelope,
needs_conversion=True,
provider_api_format="gemini:cli",
client_api_format="openai:cli",
)
is False
)
def test_stream_executor_requires_python_rewrite_allows_kiro_same_format() -> None:
class KiroEnvelope:
name = "kiro:generateAssistantResponse"
def force_stream_rewrite(self) -> bool:
return True
envelope = KiroEnvelope()
assert (
_stream_executor_requires_python_rewrite(
envelope=envelope,
needs_conversion=False,
provider_api_format="claude:cli",
client_api_format="claude:cli",
)
is False
)
assert (
_stream_executor_requires_python_rewrite(
envelope=envelope,
needs_conversion=False,
provider_api_format="claude:chat",
client_api_format="claude:chat",
)
is True
)
File diff suppressed because it is too large Load Diff
@@ -1,7 +0,0 @@
"""Compatibility shim for legacy imports.
Reporting tests now live in:
- test_gateway_reporting_background.py
- test_gateway_reporting_stream.py
- test_gateway_reporting_success.py
"""
@@ -1,497 +0,0 @@
"""Gateway internal report/finalize/trace route tests."""
import asyncio
import base64
import json
import time
from types import SimpleNamespace
from typing import Any
from unittest.mock import AsyncMock, MagicMock
import pytest
from fastapi import BackgroundTasks, FastAPI
from fastapi.responses import JSONResponse, StreamingResponse
from fastapi.testclient import TestClient
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from src.api.internal.gateway import (
CONTROL_ACTION_HEADER,
CONTROL_ACTION_PROXY_PUBLIC,
CONTROL_EXECUTED_HEADER,
GatewayAuthContext,
GatewayExecutionDecisionResponse,
GatewayExecuteRequest,
GatewayExecutionPlanResponse,
GatewayResolveRequest,
GatewayStreamReportRequest,
GatewaySyncReportRequest,
_dispatch_gateway_sync_telemetry,
_build_gateway_sync_error_payload,
_build_gateway_sync_telemetry_writer,
_run_gateway_stream_report_background,
_run_gateway_sync_report_background,
_stream_executor_requires_python_rewrite,
_build_claude_chat_sync_decision,
_build_claude_chat_stream_decision,
_build_claude_cli_sync_decision,
_build_claude_cli_stream_decision,
_build_gemini_files_download_stream_decision,
_build_gemini_files_proxy_sync_decision,
_build_gemini_chat_sync_decision,
_build_gemini_chat_stream_decision,
_build_gemini_cli_sync_decision,
_build_gemini_cli_stream_decision,
_build_openai_chat_sync_decision,
_build_openai_chat_stream_decision,
_build_openai_cli_stream_decision,
_build_openai_video_content_stream_decision,
_extract_gateway_sync_error_message,
_record_gateway_direct_candidate_graph,
_resolve_gateway_sync_error_status_code,
_build_openai_chat_sync_plan,
_build_openai_cli_sync_decision,
_build_openai_cli_stream_plan,
_build_openai_cli_sync_plan,
_is_streaming_sync_payload,
_resolve_auth_context,
_resolve_gateway_sync_adapter,
classify_gateway_route,
router,
)
from src.database import get_db
from src.models.database import Base, RequestCandidate
from src.services.orchestration.candidate_resolver import CandidateResolver
from src.services.scheduling.schemas import PoolCandidate, ProviderCandidate
from src.services.request.execution_runtime_plan import ExecutionPlan, ExecutionPlanBody, PreparedExecutionPlan
def _wait_until(predicate: Any, *, timeout: float = 1.0, interval: float = 0.01) -> None:
deadline = time.time() + timeout
while time.time() < deadline:
if predicate():
return
time.sleep(interval)
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)
app.dependency_overrides[get_db] = lambda: object()
monkeypatch.setattr("src.api.internal.gateway.ensure_loopback", lambda request: None)
seen: dict[str, object] = {}
async def _fake_store_mapping(**kwargs: object) -> None:
seen.update(kwargs)
monkeypatch.setattr(
"src.api.public.gemini_files._maybe_store_file_mapping_from_payload",
_fake_store_mapping,
)
client = _make_legacy_test_client(app)
response = client.post(
"/api/internal/gateway/report-sync",
json={
"trace_id": "trace-report-files-123",
"report_kind": "gemini_files_store_mapping",
"report_context": {
"file_key_id": "file-key-123",
"user_id": "user-123",
},
"status_code": 200,
"headers": {"content-type": "application/json"},
"body_json": {"name": "files/abc-123", "displayName": "Report File"},
},
)
assert response.status_code == 200
assert response.json() == {"ok": True}
_wait_until(lambda: "status_code" in seen)
assert seen["status_code"] == 200
assert seen["headers"] == {"content-type": "application/json"}
assert seen["file_key_id"] == "file-key-123"
assert seen["user_id"] == "user-123"
assert json.loads(seen["content_bytes"]) == {
"name": "files/abc-123",
"displayName": "Report File",
}
def test_report_sync_route_applies_gemini_files_delete_mapping(
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)
seen: list[str] = []
async def _fake_delete_mapping(file_name: str) -> None:
seen.append(file_name)
monkeypatch.setattr(
"src.services.gemini_files_mapping.delete_file_key_mapping",
_fake_delete_mapping,
)
client = _make_legacy_test_client(app)
response = client.post(
"/api/internal/gateway/report-sync",
json={
"trace_id": "trace-report-delete-files-123",
"report_kind": "gemini_files_delete_mapping",
"report_context": {"file_name": "files/abc-123"},
"status_code": 200,
"headers": {},
},
)
assert response.status_code == 200
assert response.json() == {"ok": True}
_wait_until(lambda: bool(seen))
assert seen == ["files/abc-123"]
def test_report_sync_route_uses_lazy_session(
monkeypatch: pytest.MonkeyPatch,
) -> None:
app = FastAPI()
app.include_router(router)
monkeypatch.setattr("src.api.internal.gateway.ensure_loopback", lambda request: None)
background_mock = AsyncMock(return_value=None)
monkeypatch.setattr(
"src.api.internal.gateway._run_gateway_sync_report_background_with_session",
background_mock,
)
monkeypatch.setattr(
"src.api.internal.gateway.create_session",
lambda: (_ for _ in ()).throw(AssertionError("create_session should not be called")),
)
client = _make_legacy_test_client(app)
response = client.post(
"/api/internal/gateway/report-sync",
json={
"trace_id": "trace-report-sync-lazy-123",
"report_kind": "openai_chat_sync_success",
"report_context": {"user_id": "user-123", "api_key_id": "key-123"},
"status_code": 200,
"headers": {"content-type": "application/json"},
"body_json": {"id": "chatcmpl-123", "object": "chat.completion", "choices": []},
},
)
assert response.status_code == 200
assert response.json() == {"ok": True}
background_mock.assert_awaited_once()
def test_report_sync_route_runs_video_create_success_inline(
monkeypatch: pytest.MonkeyPatch,
) -> None:
app = FastAPI()
app.include_router(router)
monkeypatch.setattr("src.api.internal.gateway.ensure_loopback", lambda request: None)
background_with_session_mock = AsyncMock(return_value=None)
run_background_mock = AsyncMock(return_value=None)
cleanup_mock = MagicMock()
monkeypatch.setattr(
"src.api.internal.gateway._run_gateway_sync_report_background_with_session",
background_with_session_mock,
)
monkeypatch.setattr(
"src.api.internal.gateway._run_gateway_sync_report_background",
run_background_mock,
)
monkeypatch.setattr(
"src.api.internal.gateway._resolve_gateway_background_db",
lambda app_obj: ("db-inline-123", cleanup_mock),
)
client = _make_legacy_test_client(app)
response = client.post(
"/api/internal/gateway/report-sync",
json={
"trace_id": "trace-openai-video-create-inline-123",
"report_kind": "openai_video_create_sync_success",
"report_context": {"user_id": "user-123", "api_key_id": "key-123"},
"status_code": 200,
"headers": {"content-type": "application/json"},
"body_json": {"id": "ext-video-123", "status": "submitted"},
"client_body_json": {
"id": "local-video-123",
"object": "video",
"status": "queued",
},
},
)
assert response.status_code == 200
assert response.json() == {"ok": True}
background_with_session_mock.assert_not_awaited()
run_background_mock.assert_awaited_once()
cleanup_mock.assert_called_once()
@pytest.mark.asyncio
@pytest.mark.asyncio
@pytest.mark.asyncio
async def test_run_gateway_sync_report_background_ensures_request_candidate(
monkeypatch: pytest.MonkeyPatch,
) -> None:
db = object()
ensure_mock = MagicMock(return_value=SimpleNamespace(id="cand-sync-123"))
finalize_mock = MagicMock()
apply_mock = AsyncMock(return_value=None)
monkeypatch.setattr(
"src.api.internal.gateway._ensure_gateway_request_candidate",
ensure_mock,
)
monkeypatch.setattr(
"src.api.internal.gateway._mark_gateway_sync_candidate_terminal_state",
finalize_mock,
)
monkeypatch.setattr(
"src.api.internal.gateway._apply_gateway_sync_report",
apply_mock,
)
payload = GatewaySyncReportRequest(
trace_id="trace-sync-candidate-123",
report_kind="openai_chat_sync_success",
report_context={
"request_id": "req-sync-candidate-123",
"candidate_id": "cand-sync-123",
"provider_id": "provider-123",
"endpoint_id": "endpoint-123",
"key_id": "key-123",
},
status_code=200,
headers={"content-type": "application/json"},
body_json={"id": "chatcmpl-123", "object": "chat.completion", "choices": []},
)
await _run_gateway_sync_report_background(payload, db)
ensure_mock.assert_called_once_with(
db=db,
report_context=dict(payload.report_context or {}),
trace_id="trace-sync-candidate-123",
initial_status="pending",
)
apply_mock.assert_awaited_once_with(payload, db=db)
finalize_mock.assert_called_once_with(
db=db,
candidate=ensure_mock.return_value,
payload=payload,
)
@pytest.mark.asyncio
async def test_run_gateway_sync_report_background_updates_direct_candidate_terminal_state(
monkeypatch: pytest.MonkeyPatch,
) -> 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-terminal-123",
name="client-key",
user_id="user-direct-terminal-123",
user=SimpleNamespace(id="user-direct-terminal-123", username="direct-terminal-user"),
)
candidates = [
ProviderCandidate(
provider=SimpleNamespace(id="provider-selected-terminal-123", name="openai", max_retries=1),
endpoint=SimpleNamespace(id="endpoint-selected-terminal-123"),
key=SimpleNamespace(id="key-selected-terminal-123"),
provider_api_format="openai:chat",
),
ProviderCandidate(
provider=SimpleNamespace(id="provider-unused-terminal-123", name="openai", max_retries=1),
endpoint=SimpleNamespace(id="endpoint-unused-terminal-123"),
key=SimpleNamespace(id="key-unused-terminal-123"),
provider_api_format="openai:chat",
),
]
_record_gateway_direct_candidate_graph(
db=db,
candidate_resolver=resolver,
candidates=candidates,
request_id="req-direct-terminal-123",
user_api_key=user_api_key,
required_capabilities=None,
selected_candidate_index=0,
)
monkeypatch.setattr(
"src.api.internal.gateway._apply_gateway_sync_report",
AsyncMock(return_value=None),
)
payload = GatewaySyncReportRequest(
trace_id="trace-direct-terminal-123",
report_kind="openai_chat_sync_success",
report_context={
"request_id": "req-direct-terminal-123",
"candidate_id": getattr(candidates[0], "request_candidate_id"),
"provider_id": "provider-selected-terminal-123",
"endpoint_id": "endpoint-selected-terminal-123",
"key_id": "key-selected-terminal-123",
"client_api_format": "openai:chat",
},
status_code=200,
headers={"content-type": "application/json"},
body_json={"id": "chatcmpl-123", "object": "chat.completion", "choices": []},
)
await _run_gateway_sync_report_background(payload, db)
rows = (
db.query(RequestCandidate)
.filter(RequestCandidate.request_id == "req-direct-terminal-123")
.order_by(RequestCandidate.candidate_index, RequestCandidate.retry_index)
.all()
)
assert len(rows) == 2
assert rows[0].status == "success"
assert rows[1].status == "unused"
@pytest.mark.asyncio
@pytest.mark.parametrize(
("report_kind", "recorder_attr"),
[
("openai_chat_sync_error", "_record_gateway_chat_sync_failure"),
("claude_chat_sync_error", "_record_gateway_chat_sync_failure"),
("gemini_chat_sync_error", "_record_gateway_chat_sync_failure"),
],
)
async def test_apply_gateway_sync_report_routes_chat_error_to_failure_recorder(
monkeypatch: pytest.MonkeyPatch,
report_kind: str,
recorder_attr: str,
) -> None:
db = object()
recorder = AsyncMock(return_value=None)
monkeypatch.setattr(
f"src.api.internal.gateway.{recorder_attr}",
recorder,
)
payload = GatewaySyncReportRequest(
trace_id="trace-chat-error-report-123",
report_kind=report_kind,
report_context={"user_id": "user-123", "api_key_id": "key-123"},
status_code=429,
headers={"content-type": "application/json"},
body_json={"error": {"message": "rate limited"}},
)
from src.api.internal.gateway import _apply_gateway_sync_report
await _apply_gateway_sync_report(payload, db=db)
recorder.assert_awaited_once_with(payload, db=db)
@pytest.mark.asyncio
@pytest.mark.parametrize(
("report_kind", "recorder_attr"),
[
("openai_cli_sync_error", "_record_gateway_cli_sync_failure"),
("openai_compact_sync_error", "_record_gateway_cli_sync_failure"),
("claude_cli_sync_error", "_record_gateway_cli_sync_failure"),
("gemini_cli_sync_error", "_record_gateway_cli_sync_failure"),
],
)
async def test_apply_gateway_sync_report_routes_cli_error_to_failure_recorder(
monkeypatch: pytest.MonkeyPatch,
report_kind: str,
recorder_attr: str,
) -> None:
db = object()
recorder = AsyncMock(return_value=None)
monkeypatch.setattr(
f"src.api.internal.gateway.{recorder_attr}",
recorder,
)
payload = GatewaySyncReportRequest(
trace_id="trace-cli-error-report-123",
report_kind=report_kind,
report_context={"user_id": "user-123", "api_key_id": "key-123"},
status_code=429,
headers={"content-type": "application/json"},
body_json={"error": {"message": "rate limited"}},
)
from src.api.internal.gateway import _apply_gateway_sync_report
await _apply_gateway_sync_report(payload, db=db)
recorder.assert_awaited_once_with(payload, db=db)
@pytest.mark.asyncio
@pytest.mark.parametrize(
"report_kind",
[
"openai_video_create_sync_error",
"openai_video_remix_sync_error",
"gemini_video_create_sync_error",
"openai_video_delete_sync_error",
"openai_video_cancel_sync_error",
"gemini_video_cancel_sync_error",
],
)
async def test_apply_gateway_sync_report_routes_video_error_to_failure_recorder(
monkeypatch: pytest.MonkeyPatch,
report_kind: str,
) -> None:
db = object()
recorder = AsyncMock(return_value=None)
monkeypatch.setattr(
"src.api.internal.gateway._record_gateway_video_sync_failure",
recorder,
)
payload = GatewaySyncReportRequest(
trace_id="trace-video-error-report-123",
report_kind=report_kind,
report_context={"user_id": "user-123", "api_key_id": "key-123"},
status_code=429,
headers={"content-type": "application/json"},
body_json={"error": {"message": "rate limited"}},
)
from src.api.internal.gateway import _apply_gateway_sync_report
await _apply_gateway_sync_report(payload, db=db)
recorder.assert_awaited_once_with(payload, db=db)
@@ -1,387 +0,0 @@
"""Gateway internal report/finalize/trace route tests."""
import asyncio
import base64
import json
import time
from types import SimpleNamespace
from typing import Any
from unittest.mock import AsyncMock, MagicMock
import pytest
from fastapi import BackgroundTasks, FastAPI
from fastapi.responses import JSONResponse, StreamingResponse
from fastapi.testclient import TestClient
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from src.api.internal.gateway import (
CONTROL_ACTION_HEADER,
CONTROL_ACTION_PROXY_PUBLIC,
CONTROL_EXECUTED_HEADER,
GatewayAuthContext,
GatewayExecutionDecisionResponse,
GatewayExecuteRequest,
GatewayExecutionPlanResponse,
GatewayResolveRequest,
GatewayStreamReportRequest,
GatewaySyncReportRequest,
_dispatch_gateway_sync_telemetry,
_build_gateway_sync_error_payload,
_build_gateway_sync_telemetry_writer,
_run_gateway_stream_report_background,
_run_gateway_sync_report_background,
_stream_executor_requires_python_rewrite,
_build_claude_chat_sync_decision,
_build_claude_chat_stream_decision,
_build_claude_cli_sync_decision,
_build_claude_cli_stream_decision,
_build_gemini_files_download_stream_decision,
_build_gemini_files_proxy_sync_decision,
_build_gemini_chat_sync_decision,
_build_gemini_chat_stream_decision,
_build_gemini_cli_sync_decision,
_build_gemini_cli_stream_decision,
_build_openai_chat_sync_decision,
_build_openai_chat_stream_decision,
_build_openai_cli_stream_decision,
_build_openai_video_content_stream_decision,
_extract_gateway_sync_error_message,
_record_gateway_direct_candidate_graph,
_resolve_gateway_sync_error_status_code,
_build_openai_chat_sync_plan,
_build_openai_cli_sync_decision,
_build_openai_cli_stream_plan,
_build_openai_cli_sync_plan,
_is_streaming_sync_payload,
_resolve_auth_context,
_resolve_gateway_sync_adapter,
classify_gateway_route,
router,
)
from src.database import get_db
from src.models.database import Base, RequestCandidate
from src.services.orchestration.candidate_resolver import CandidateResolver
from src.services.scheduling.schemas import PoolCandidate, ProviderCandidate
from src.services.request.execution_runtime_plan import ExecutionPlan, ExecutionPlanBody, PreparedExecutionPlan
def _wait_until(predicate: Any, *, timeout: float = 1.0, interval: float = 0.01) -> None:
deadline = time.time() + timeout
while time.time() < deadline:
if predicate():
return
time.sleep(interval)
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:
app = FastAPI()
app.include_router(router)
monkeypatch.setattr("src.api.internal.gateway.ensure_loopback", lambda request: None)
background_mock = AsyncMock(return_value=None)
monkeypatch.setattr(
"src.api.internal.gateway._run_gateway_stream_report_background_with_session",
background_mock,
)
monkeypatch.setattr(
"src.api.internal.gateway.create_session",
lambda: (_ for _ in ()).throw(AssertionError("create_session should not be called")),
)
client = _make_legacy_test_client(app)
response = client.post(
"/api/internal/gateway/report-stream",
json={
"trace_id": "trace-report-stream-lazy-123",
"report_kind": "openai_chat_stream_success",
"report_context": {"user_id": "user-123", "api_key_id": "key-123"},
"status_code": 200,
"headers": {"content-type": "text/event-stream"},
"body_base64": base64.b64encode(b"data: ok\n\n").decode("ascii"),
},
)
assert response.status_code == 200
assert response.json() == {"ok": True}
background_mock.assert_awaited_once()
@pytest.mark.asyncio
@pytest.mark.asyncio
@pytest.mark.asyncio
async def test_run_gateway_stream_report_background_ensures_request_candidate(
monkeypatch: pytest.MonkeyPatch,
) -> None:
db = object()
ensure_mock = MagicMock(return_value=SimpleNamespace(id="cand-stream-123"))
apply_mock = AsyncMock(return_value=None)
monkeypatch.setattr(
"src.api.internal.gateway._ensure_gateway_request_candidate",
ensure_mock,
)
monkeypatch.setattr(
"src.api.internal.gateway._apply_gateway_stream_report",
apply_mock,
)
payload = GatewayStreamReportRequest(
trace_id="trace-stream-candidate-123",
report_kind="openai_chat_stream_success",
report_context={
"request_id": "req-stream-candidate-123",
"candidate_id": "cand-stream-123",
"provider_id": "provider-123",
"endpoint_id": "endpoint-123",
"key_id": "key-123",
},
status_code=200,
headers={"content-type": "text/event-stream"},
body_base64=base64.b64encode(b"data: ok\n\n").decode("ascii"),
)
await _run_gateway_stream_report_background(payload, db)
ensure_mock.assert_called_once_with(
db=db,
report_context=dict(payload.report_context or {}),
trace_id="trace-stream-candidate-123",
initial_status="streaming",
)
apply_mock.assert_awaited_once_with(payload, db=db)
def test_report_stream_route_records_openai_chat_stream_success(
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)
record_mock = AsyncMock(return_value=None)
monkeypatch.setattr(
"src.api.internal.gateway._record_gateway_openai_chat_stream_success",
record_mock,
)
client = _make_legacy_test_client(app)
response = client.post(
"/api/internal/gateway/report-stream",
json={
"trace_id": "trace-openai-chat-stream-report",
"report_kind": "openai_chat_stream_success",
"report_context": {"user_id": "user-123", "api_key_id": "key-123"},
"status_code": 200,
"headers": {"content-type": "text/event-stream"},
"body_base64": base64.b64encode(b"data: hello\\n\\ndata: [DONE]\\n\\n").decode("ascii"),
"telemetry": {"elapsed_ms": 123, "ttfb_ms": 22},
},
)
assert response.status_code == 200
assert response.json() == {"ok": True}
_wait_until(lambda: record_mock.await_count == 1)
record_mock.assert_awaited_once()
def test_report_stream_route_records_claude_chat_stream_success(
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)
record_mock = AsyncMock(return_value=None)
monkeypatch.setattr(
"src.api.internal.gateway._record_gateway_passthrough_chat_stream_success",
record_mock,
)
client = _make_legacy_test_client(app)
response = client.post(
"/api/internal/gateway/report-stream",
json={
"trace_id": "trace-claude-chat-stream-report",
"report_kind": "claude_chat_stream_success",
"report_context": {
"user_id": "user-123",
"api_key_id": "key-123",
"client_api_format": "claude:chat",
},
"status_code": 200,
"headers": {"content-type": "text/event-stream"},
"body_base64": base64.b64encode(b"event: content_block_delta\n\n").decode("ascii"),
"telemetry": {"elapsed_ms": 66},
},
)
assert response.status_code == 200
assert response.json() == {"ok": True}
_wait_until(lambda: record_mock.await_count == 1)
record_mock.assert_awaited_once()
def test_report_stream_route_records_gemini_chat_stream_success(
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)
record_mock = AsyncMock(return_value=None)
monkeypatch.setattr(
"src.api.internal.gateway._record_gateway_passthrough_chat_stream_success",
record_mock,
)
client = _make_legacy_test_client(app)
response = client.post(
"/api/internal/gateway/report-stream",
json={
"trace_id": "trace-gemini-chat-stream-report",
"report_kind": "gemini_chat_stream_success",
"report_context": {
"user_id": "user-123",
"api_key_id": "key-123",
"client_api_format": "gemini:chat",
},
"status_code": 200,
"headers": {"content-type": "text/event-stream"},
"body_base64": base64.b64encode(b'data: {"candidates":[]}\n\n').decode("ascii"),
"telemetry": {"elapsed_ms": 44},
},
)
assert response.status_code == 200
assert response.json() == {"ok": True}
_wait_until(lambda: record_mock.await_count == 1)
record_mock.assert_awaited_once()
def test_report_stream_route_records_openai_cli_stream_success(
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)
record_mock = AsyncMock(return_value=None)
monkeypatch.setattr(
"src.api.internal.gateway._record_gateway_passthrough_cli_stream_success",
record_mock,
)
client = _make_legacy_test_client(app)
response = client.post(
"/api/internal/gateway/report-stream",
json={
"trace_id": "trace-openai-cli-stream-report",
"report_kind": "openai_cli_stream_success",
"report_context": {
"user_id": "user-123",
"api_key_id": "key-123",
"client_api_format": "openai:cli",
},
"status_code": 200,
"headers": {"content-type": "text/event-stream"},
"body_base64": base64.b64encode(
b'event: response.completed\ndata: {"type":"response.completed"}\n\n'
).decode("ascii"),
"telemetry": {"elapsed_ms": 66},
},
)
assert response.status_code == 200
assert response.json() == {"ok": True}
_wait_until(lambda: record_mock.await_count == 1)
record_mock.assert_awaited_once()
def test_report_stream_route_records_claude_cli_stream_success(
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)
record_mock = AsyncMock(return_value=None)
monkeypatch.setattr(
"src.api.internal.gateway._record_gateway_passthrough_cli_stream_success",
record_mock,
)
client = _make_legacy_test_client(app)
response = client.post(
"/api/internal/gateway/report-stream",
json={
"trace_id": "trace-claude-cli-stream-report",
"report_kind": "claude_cli_stream_success",
"report_context": {
"user_id": "user-123",
"api_key_id": "key-123",
"client_api_format": "claude:cli",
},
"status_code": 200,
"headers": {"content-type": "text/event-stream"},
"body_base64": base64.b64encode(
b'event: message_start\ndata: {"type":"message_start"}\n\n'
).decode("ascii"),
"telemetry": {"elapsed_ms": 44},
},
)
assert response.status_code == 200
assert response.json() == {"ok": True}
_wait_until(lambda: record_mock.await_count == 1)
record_mock.assert_awaited_once()
def test_report_stream_route_records_gemini_cli_stream_success(
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)
record_mock = AsyncMock(return_value=None)
monkeypatch.setattr(
"src.api.internal.gateway._record_gateway_passthrough_cli_stream_success",
record_mock,
)
client = _make_legacy_test_client(app)
response = client.post(
"/api/internal/gateway/report-stream",
json={
"trace_id": "trace-gemini-cli-stream-report",
"report_kind": "gemini_cli_stream_success",
"report_context": {
"user_id": "user-123",
"api_key_id": "key-123",
"client_api_format": "gemini:cli",
},
"status_code": 200,
"headers": {"content-type": "text/event-stream"},
"body_base64": base64.b64encode(b'data: {"candidates":[]}\n\n').decode("ascii"),
"telemetry": {"elapsed_ms": 44},
},
)
assert response.status_code == 200
assert response.json() == {"ok": True}
_wait_until(lambda: record_mock.await_count == 1)
record_mock.assert_awaited_once()
@@ -1,752 +0,0 @@
"""Gateway internal report/finalize/trace route tests."""
import asyncio
import base64
import json
import time
from types import SimpleNamespace
from typing import Any
from unittest.mock import AsyncMock, MagicMock
import pytest
from fastapi import BackgroundTasks, FastAPI
from fastapi.responses import JSONResponse, StreamingResponse
from fastapi.testclient import TestClient
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from src.api.internal.gateway import (
CONTROL_ACTION_HEADER,
CONTROL_ACTION_PROXY_PUBLIC,
CONTROL_EXECUTED_HEADER,
GatewayAuthContext,
GatewayExecutionDecisionResponse,
GatewayExecuteRequest,
GatewayExecutionPlanResponse,
GatewayResolveRequest,
GatewayStreamReportRequest,
GatewaySyncReportRequest,
_dispatch_gateway_sync_telemetry,
_build_gateway_sync_error_payload,
_build_gateway_sync_telemetry_writer,
_run_gateway_stream_report_background,
_run_gateway_sync_report_background,
_stream_executor_requires_python_rewrite,
_build_claude_chat_sync_decision,
_build_claude_chat_stream_decision,
_build_claude_cli_sync_decision,
_build_claude_cli_stream_decision,
_build_gemini_files_download_stream_decision,
_build_gemini_files_proxy_sync_decision,
_build_gemini_chat_sync_decision,
_build_gemini_chat_stream_decision,
_build_gemini_cli_sync_decision,
_build_gemini_cli_stream_decision,
_build_openai_chat_sync_decision,
_build_openai_chat_stream_decision,
_build_openai_cli_stream_decision,
_build_openai_video_content_stream_decision,
_extract_gateway_sync_error_message,
_record_gateway_direct_candidate_graph,
_resolve_gateway_sync_error_status_code,
_build_openai_chat_sync_plan,
_build_openai_cli_sync_decision,
_build_openai_cli_stream_plan,
_build_openai_cli_sync_plan,
_is_streaming_sync_payload,
_resolve_auth_context,
_resolve_gateway_sync_adapter,
classify_gateway_route,
router,
)
from src.database import get_db
from src.models.database import Base, RequestCandidate
from src.services.orchestration.candidate_resolver import CandidateResolver
from src.services.scheduling.schemas import PoolCandidate, ProviderCandidate
from src.services.request.execution_runtime_plan import ExecutionPlan, ExecutionPlanBody, PreparedExecutionPlan
def _wait_until(predicate: Any, *, timeout: float = 1.0, interval: float = 0.01) -> None:
deadline = time.time() + timeout
while time.time() < deadline:
if predicate():
return
time.sleep(interval)
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:
app = FastAPI()
app.include_router(router)
app.dependency_overrides[get_db] = lambda: object()
monkeypatch.setattr("src.api.internal.gateway.ensure_loopback", lambda request: None)
record_mock = AsyncMock(return_value=None)
monkeypatch.setattr(
"src.api.internal.gateway._record_gateway_openai_chat_sync_success",
record_mock,
)
client = _make_legacy_test_client(app)
response = client.post(
"/api/internal/gateway/report-sync",
json={
"trace_id": "trace-openai-chat-report",
"report_kind": "openai_chat_sync_success",
"report_context": {"user_id": "user-123", "api_key_id": "key-123"},
"status_code": 200,
"headers": {"content-type": "application/json"},
"body_json": {
"id": "chatcmpl-123",
"object": "chat.completion",
"choices": [],
},
"telemetry": {"elapsed_ms": 123},
},
)
assert response.status_code == 200
assert response.json() == {"ok": True}
_wait_until(lambda: record_mock.await_count == 1)
record_mock.assert_awaited_once()
def test_report_sync_route_records_openai_cli_sync_success(
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)
record_mock = AsyncMock(return_value=None)
monkeypatch.setattr(
"src.api.internal.gateway._record_gateway_passthrough_cli_sync_success",
record_mock,
)
client = _make_legacy_test_client(app)
response = client.post(
"/api/internal/gateway/report-sync",
json={
"trace_id": "trace-openai-cli-report",
"report_kind": "openai_cli_sync_success",
"report_context": {
"user_id": "user-123",
"api_key_id": "key-123",
"client_api_format": "openai:cli",
},
"status_code": 200,
"headers": {"content-type": "application/json"},
"body_json": {
"id": "resp_123",
"object": "response",
"output": [],
},
"telemetry": {"elapsed_ms": 91},
},
)
assert response.status_code == 200
assert response.json() == {"ok": True}
_wait_until(lambda: record_mock.await_count == 1)
record_mock.assert_awaited_once()
def test_report_sync_route_records_claude_cli_sync_success(
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)
record_mock = AsyncMock(return_value=None)
monkeypatch.setattr(
"src.api.internal.gateway._record_gateway_passthrough_cli_sync_success",
record_mock,
)
client = _make_legacy_test_client(app)
response = client.post(
"/api/internal/gateway/report-sync",
json={
"trace_id": "trace-claude-cli-report",
"report_kind": "claude_cli_sync_success",
"report_context": {
"user_id": "user-123",
"api_key_id": "key-123",
"client_api_format": "claude:cli",
},
"status_code": 200,
"headers": {"content-type": "application/json"},
"body_json": {
"id": "msg_123",
"type": "message",
"content": [],
},
},
)
assert response.status_code == 200
assert response.json() == {"ok": True}
_wait_until(lambda: record_mock.await_count == 1)
record_mock.assert_awaited_once()
def test_report_sync_route_records_gemini_cli_sync_success(
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)
record_mock = AsyncMock(return_value=None)
monkeypatch.setattr(
"src.api.internal.gateway._record_gateway_passthrough_cli_sync_success",
record_mock,
)
client = _make_legacy_test_client(app)
response = client.post(
"/api/internal/gateway/report-sync",
json={
"trace_id": "trace-gemini-cli-report",
"report_kind": "gemini_cli_sync_success",
"report_context": {
"user_id": "user-123",
"api_key_id": "key-123",
"client_api_format": "gemini:cli",
},
"status_code": 200,
"headers": {"content-type": "application/json"},
"body_json": {
"candidates": [],
},
},
)
assert response.status_code == 200
assert response.json() == {"ok": True}
_wait_until(lambda: record_mock.await_count == 1)
record_mock.assert_awaited_once()
@pytest.mark.parametrize(
("report_kind", "recorder_attr"),
[
("openai_video_create_sync_success", "_record_gateway_openai_video_create_sync_success"),
("openai_video_remix_sync_success", "_record_gateway_openai_video_remix_sync_success"),
("openai_video_delete_sync_success", "_record_gateway_openai_video_delete_sync_success"),
("openai_video_cancel_sync_success", "_record_gateway_openai_video_cancel_sync_success"),
("gemini_video_create_sync_success", "_record_gateway_gemini_video_create_sync_success"),
("gemini_video_cancel_sync_success", "_record_gateway_gemini_video_cancel_sync_success"),
],
)
def test_report_sync_route_records_video_sync_success_variants(
monkeypatch: pytest.MonkeyPatch,
report_kind: str,
recorder_attr: str,
) -> 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)
record_mock = AsyncMock(return_value=None)
monkeypatch.setattr(f"src.api.internal.gateway.{recorder_attr}", record_mock)
client = _make_legacy_test_client(app)
response = client.post(
"/api/internal/gateway/report-sync",
json={
"trace_id": f"trace-{report_kind}",
"report_kind": report_kind,
"report_context": {
"user_id": "user-123",
"api_key_id": "key-123",
"task_id": "task-123",
},
"status_code": 200,
"headers": {"content-type": "application/json"},
"body_json": {},
"telemetry": {"elapsed_ms": 33},
},
)
assert response.status_code == 200
assert response.json() == {"ok": True}
_wait_until(lambda: record_mock.await_count == 1)
record_mock.assert_awaited_once()
@pytest.mark.asyncio
@pytest.mark.parametrize(
("recorder_attr", "finalize_attr"),
[
(
"_record_gateway_openai_video_create_sync_success",
"_finalize_gateway_openai_video_create_sync",
),
(
"_record_gateway_openai_video_remix_sync_success",
"_finalize_gateway_openai_video_remix_sync",
),
(
"_record_gateway_gemini_video_create_sync_success",
"_finalize_gateway_gemini_video_create_sync",
),
],
)
async def test_video_sync_success_recorders_delegate_to_finalize(
monkeypatch: pytest.MonkeyPatch,
recorder_attr: str,
finalize_attr: str,
) -> None:
import src.api.internal.gateway as gateway_module
finalize_mock = AsyncMock(return_value=JSONResponse({"ok": True}))
monkeypatch.setattr(f"src.api.internal.gateway.{finalize_attr}", finalize_mock)
db = object()
payload = GatewaySyncReportRequest(
trace_id="trace-video-sync-success-123",
report_kind="video-sync-success",
report_context={"user_id": "user-123", "api_key_id": "key-123"},
status_code=200,
headers={"content-type": "application/json"},
body_json={"id": "ext-123"},
)
recorder = getattr(gateway_module, recorder_attr)
await recorder(payload, db=db)
finalize_mock.assert_awaited_once_with(payload, db=db)
def test_report_sync_route_records_claude_chat_sync_success(
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)
record_mock = AsyncMock(return_value=None)
monkeypatch.setattr(
"src.api.internal.gateway._record_gateway_passthrough_chat_sync_success",
record_mock,
)
client = _make_legacy_test_client(app)
response = client.post(
"/api/internal/gateway/report-sync",
json={
"trace_id": "trace-claude-chat-report",
"report_kind": "claude_chat_sync_success",
"report_context": {
"user_id": "user-123",
"api_key_id": "key-123",
"client_api_format": "claude:chat",
},
"status_code": 200,
"headers": {"content-type": "application/json"},
"body_json": {
"id": "msg_123",
"type": "message",
"content": [],
},
},
)
assert response.status_code == 200
assert response.json() == {"ok": True}
_wait_until(lambda: record_mock.await_count == 1)
record_mock.assert_awaited_once()
def test_report_sync_route_records_gemini_chat_sync_success(
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)
record_mock = AsyncMock(return_value=None)
monkeypatch.setattr(
"src.api.internal.gateway._record_gateway_passthrough_chat_sync_success",
record_mock,
)
client = _make_legacy_test_client(app)
response = client.post(
"/api/internal/gateway/report-sync",
json={
"trace_id": "trace-gemini-chat-report",
"report_kind": "gemini_chat_sync_success",
"report_context": {
"user_id": "user-123",
"api_key_id": "key-123",
"client_api_format": "gemini:chat",
},
"status_code": 200,
"headers": {"content-type": "application/json"},
"body_json": {
"candidates": [],
},
},
)
assert response.status_code == 200
assert response.json() == {"ok": True}
_wait_until(lambda: record_mock.await_count == 1)
record_mock.assert_awaited_once()
@pytest.mark.asyncio
@pytest.mark.asyncio
@pytest.mark.asyncio
async def test_record_gateway_openai_chat_sync_success_uses_provider_and_client_bodies(
monkeypatch: pytest.MonkeyPatch,
) -> None:
from src.api.internal import gateway as gateway_module
record_success = AsyncMock(return_value=None)
fake_handler = SimpleNamespace(
_normalize_response=lambda response: response,
_extract_usage=lambda response: {
"input_tokens": response.get("usage", {}).get("prompt_tokens", 0),
"output_tokens": response.get("usage", {}).get("completion_tokens", 0),
},
telemetry=SimpleNamespace(record_success=record_success),
)
class FakeOpenAIChatAdapter:
API_FAMILY = SimpleNamespace(value="openai")
ENDPOINT_KIND = SimpleNamespace(value="chat")
def _create_handler(self, **kwargs: Any) -> Any:
return fake_handler
fake_user = object()
fake_api_key = object()
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._values = iter((fake_user, fake_api_key))
def query(self, model: Any) -> FakeQuery:
return FakeQuery(next(self._values))
monkeypatch.setattr("src.api.handlers.openai.OpenAIChatAdapter", FakeOpenAIChatAdapter)
provider_body = {
"responseId": "resp-provider-123",
"candidates": [
{
"content": {"parts": [{"text": "provider"}], "role": "model"},
"finishReason": "STOP",
}
],
"usageMetadata": {
"promptTokenCount": 2,
"candidatesTokenCount": 3,
"totalTokenCount": 5,
},
}
client_body = {
"id": "chatcmpl-client-123",
"object": "chat.completion",
"choices": [
{
"index": 0,
"message": {"role": "assistant", "content": "client"},
"finish_reason": "stop",
}
],
"usage": {
"prompt_tokens": 2,
"completion_tokens": 3,
"total_tokens": 5,
},
}
payload = GatewaySyncReportRequest(
trace_id="trace-openai-chat-conversion-123",
report_kind="openai_chat_sync_success",
report_context={
"user_id": "user-123",
"api_key_id": "key-123",
"provider_name": "gemini",
"provider_api_format": "gemini:chat",
"client_api_format": "openai:chat",
"model": "gpt-5",
"mapped_model": "gpt-5",
"request_id": "req-123",
},
status_code=200,
headers={"content-type": "application/json"},
body_json=provider_body,
client_body_json=client_body,
telemetry={"elapsed_ms": 31},
)
await gateway_module._record_gateway_openai_chat_sync_success(payload, db=FakeDb())
kwargs = record_success.await_args.kwargs
assert kwargs["response_body"] == provider_body
assert kwargs["client_response_body"] == client_body
assert kwargs["has_format_conversion"] is True
@pytest.mark.asyncio
@pytest.mark.asyncio
@pytest.mark.asyncio
async def test_record_gateway_passthrough_cli_sync_success_uses_provider_and_client_bodies(
monkeypatch: pytest.MonkeyPatch,
) -> None:
from src.api.internal import gateway as gateway_module
record_success = AsyncMock(return_value=None)
class FakeCliHandler:
def __init__(self, **kwargs: Any) -> None:
self.parser = SimpleNamespace(
extract_usage_from_response=lambda response: {
"input_tokens": response.get("usage", {}).get("input_tokens", 0),
"output_tokens": response.get("usage", {}).get("output_tokens", 0),
"cache_creation_tokens": 0,
"cache_read_tokens": 0,
}
)
self.telemetry = SimpleNamespace(record_success=record_success)
def _extract_response_metadata(self, response: dict[str, Any]) -> dict[str, Any]:
return {"response_id": response.get("id")}
class FakeOpenAICliAdapter:
API_FAMILY = SimpleNamespace(value="openai")
ENDPOINT_KIND = SimpleNamespace(value="cli")
HANDLER_CLASS = FakeCliHandler
allowed_api_formats = ["openai:cli"]
@staticmethod
def detect_capability_requirements(*args: Any, **kwargs: Any) -> dict[str, Any]:
return {}
fake_user = object()
fake_api_key = object()
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._values = iter((fake_user, fake_api_key))
def query(self, model: Any) -> FakeQuery:
return FakeQuery(next(self._values))
monkeypatch.setattr("src.api.handlers.openai_cli.OpenAICliAdapter", FakeOpenAICliAdapter)
provider_body = {
"responseId": "resp-provider-cli-123",
"candidates": [
{
"content": {"parts": [{"text": "provider"}], "role": "model"},
"finishReason": "STOP",
}
],
"usageMetadata": {
"promptTokenCount": 2,
"candidatesTokenCount": 3,
"totalTokenCount": 5,
},
}
client_body = {
"id": "resp-client-cli-123",
"object": "response",
"output": [],
"usage": {
"input_tokens": 2,
"output_tokens": 3,
"total_tokens": 5,
},
}
payload = GatewaySyncReportRequest(
trace_id="trace-openai-cli-conversion-123",
report_kind="openai_cli_sync_success",
report_context={
"user_id": "user-123",
"api_key_id": "key-123",
"provider_name": "gemini",
"provider_api_format": "gemini:cli",
"client_api_format": "openai:cli",
"model": "gpt-5",
"mapped_model": "gpt-5",
"request_id": "req-cli-123",
},
status_code=200,
headers={"content-type": "application/json"},
body_json=provider_body,
client_body_json=client_body,
telemetry={"elapsed_ms": 29},
)
await gateway_module._record_gateway_passthrough_cli_sync_success(
payload, db=FakeDb()
)
kwargs = record_success.await_args.kwargs
assert kwargs["response_body"] == provider_body
assert kwargs["client_response_body"] == client_body
assert kwargs["has_format_conversion"] is True
@pytest.mark.asyncio
@pytest.mark.asyncio
@pytest.mark.asyncio
async def test_record_gateway_passthrough_cli_sync_success_postprocesses_antigravity_response(
monkeypatch: pytest.MonkeyPatch,
) -> None:
from src.api.internal import gateway as gateway_module
record_success = AsyncMock(return_value=None)
seen_cache: list[tuple[str, dict[str, Any]]] = []
class FakeCliHandler:
def __init__(self, **kwargs: Any) -> None:
self.parser = SimpleNamespace(
extract_usage_from_response=lambda response: {
"input_tokens": response.get("usageMetadata", {}).get("promptTokenCount", 0),
"output_tokens": response.get("usageMetadata", {}).get(
"candidatesTokenCount", 0
),
"cache_creation_tokens": 0,
"cache_read_tokens": 0,
}
)
self.telemetry = SimpleNamespace(record_success=record_success)
def _extract_response_metadata(self, response: dict[str, Any]) -> dict[str, Any]:
return {"response_id": response.get("_v1internal_response_id")}
class FakeGeminiCliAdapter:
API_FAMILY = SimpleNamespace(value="gemini")
ENDPOINT_KIND = SimpleNamespace(value="cli")
HANDLER_CLASS = FakeCliHandler
allowed_api_formats = ["gemini:cli"]
@staticmethod
def detect_capability_requirements(*args: Any, **kwargs: Any) -> dict[str, Any]:
return {}
fake_user = object()
fake_api_key = object()
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._values = iter((fake_user, fake_api_key))
def query(self, model: Any) -> FakeQuery:
return FakeQuery(next(self._values))
def _record_cache(model: str, response: dict[str, Any]) -> None:
seen_cache.append((model, response))
monkeypatch.setattr("src.api.handlers.gemini_cli.GeminiCliAdapter", FakeGeminiCliAdapter)
monkeypatch.setattr(
"src.services.provider.adapters.antigravity.envelope.cache_thought_signatures",
_record_cache,
)
provider_body = {
"_v1internal_response_id": "resp-antigravity-123",
"candidates": [
{
"content": {
"parts": [
{
"text": "thinking",
"thoughtSignature": "a" * 60,
}
],
"role": "model",
},
"finishReason": "STOP",
}
],
"usageMetadata": {
"promptTokenCount": 2,
"candidatesTokenCount": 3,
"totalTokenCount": 5,
},
}
payload = GatewaySyncReportRequest(
trace_id="trace-antigravity-cli-report-123",
report_kind="gemini_cli_sync_success",
report_context={
"user_id": "user-123",
"api_key_id": "key-123",
"provider_name": "antigravity",
"provider_api_format": "gemini:cli",
"client_api_format": "gemini:cli",
"model": "claude-sonnet-4-5",
"mapped_model": "claude-sonnet-4-5",
"request_id": "req-antigravity-cli-123",
"envelope_name": "antigravity:v1internal",
},
status_code=200,
headers={"content-type": "application/json"},
body_json=provider_body,
telemetry={"elapsed_ms": 17},
)
await gateway_module._record_gateway_passthrough_cli_sync_success(payload, db=FakeDb())
assert len(seen_cache) == 1
assert seen_cache[0][0] == "claude-sonnet-4-5"
assert seen_cache[0][1]["_v1internal_response_id"] == "resp-antigravity-123"
@@ -1,353 +0,0 @@
"""Gateway internal report/finalize/trace route tests."""
import asyncio
import base64
import json
import time
from types import SimpleNamespace
from typing import Any
from unittest.mock import AsyncMock, MagicMock
import pytest
from fastapi import BackgroundTasks, FastAPI
from fastapi.responses import JSONResponse, StreamingResponse
from fastapi.testclient import TestClient
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from src.api.internal.gateway import (
CONTROL_ACTION_HEADER,
CONTROL_ACTION_PROXY_PUBLIC,
CONTROL_EXECUTED_HEADER,
GatewayAuthContext,
GatewayExecutionDecisionResponse,
GatewayExecuteRequest,
GatewayExecutionPlanResponse,
GatewayResolveRequest,
GatewayStreamReportRequest,
GatewaySyncReportRequest,
_dispatch_gateway_sync_telemetry,
_build_gateway_sync_error_payload,
_build_gateway_sync_telemetry_writer,
_run_gateway_stream_report_background,
_run_gateway_sync_report_background,
_stream_executor_requires_python_rewrite,
_build_claude_chat_sync_decision,
_build_claude_chat_stream_decision,
_build_claude_cli_sync_decision,
_build_claude_cli_stream_decision,
_build_gemini_files_download_stream_decision,
_build_gemini_files_proxy_sync_decision,
_build_gemini_chat_sync_decision,
_build_gemini_chat_stream_decision,
_build_gemini_cli_sync_decision,
_build_gemini_cli_stream_decision,
_build_openai_chat_sync_decision,
_build_openai_chat_stream_decision,
_build_openai_cli_stream_decision,
_build_openai_video_content_stream_decision,
_extract_gateway_sync_error_message,
_record_gateway_direct_candidate_graph,
_resolve_gateway_sync_error_status_code,
_build_openai_chat_sync_plan,
_build_openai_cli_sync_decision,
_build_openai_cli_stream_plan,
_build_openai_cli_sync_plan,
_is_streaming_sync_payload,
_resolve_auth_context,
_resolve_gateway_sync_adapter,
classify_gateway_route,
router,
)
from src.database import get_db
from src.models.database import Base, RequestCandidate
from src.services.orchestration.candidate_resolver import CandidateResolver
from src.services.scheduling.schemas import PoolCandidate, ProviderCandidate
from src.services.request.execution_runtime_plan import ExecutionPlan, ExecutionPlanBody, PreparedExecutionPlan
def _wait_until(predicate: Any, *, timeout: float = 1.0, interval: float = 0.01) -> None:
deadline = time.time() + timeout
while time.time() < deadline:
if predicate():
return
time.sleep(interval)
assert predicate()
def test_extract_gateway_sync_error_message_prefers_nested_error_message() -> None:
payload = GatewaySyncReportRequest(
trace_id="trace-error-message-123",
report_kind="openai_chat_sync_finalize",
status_code=429,
headers={"content-type": "application/json"},
body_json={"error": {"message": "rate limited"}},
)
assert _extract_gateway_sync_error_message(payload) == "rate limited"
def test_extract_gateway_sync_error_message_falls_back_to_body_base64() -> None:
payload = GatewaySyncReportRequest(
trace_id="trace-error-body-123",
report_kind="openai_chat_sync_finalize",
status_code=502,
headers={"content-type": "text/plain"},
body_base64=base64.b64encode(b"upstream exploded").decode("ascii"),
)
assert _extract_gateway_sync_error_message(payload) == "upstream exploded"
def test_build_gateway_sync_error_payload_returns_provider_error_when_no_conversion() -> None:
payload = GatewaySyncReportRequest(
trace_id="trace-error-payload-123",
report_kind="openai_chat_sync_finalize",
status_code=400,
headers={"content-type": "application/json"},
body_json={"error": {"message": "bad request"}},
)
assert _build_gateway_sync_error_payload(
payload,
client_api_format="openai:chat",
provider_api_format="openai:chat",
needs_conversion=False,
) == {"error": {"message": "bad request"}}
def test_build_gateway_sync_error_payload_builds_generic_client_error_for_non_json() -> None:
payload = GatewaySyncReportRequest(
trace_id="trace-error-generic-123",
report_kind="openai_cli_sync_finalize",
status_code=504,
headers={"content-type": "text/plain"},
body_base64=base64.b64encode(b"gateway timeout").decode("ascii"),
)
result = _build_gateway_sync_error_payload(
payload,
client_api_format="openai:cli",
provider_api_format="openai:cli",
needs_conversion=False,
)
assert result["error"]["message"] == "gateway timeout"
def test_resolve_gateway_sync_error_status_code_prefers_embedded_code() -> None:
class _FakeParser:
def parse_response(self, response: dict[str, object], status_code: int) -> SimpleNamespace:
del response, status_code
return SimpleNamespace(embedded_status_code=429)
payload = GatewaySyncReportRequest(
trace_id="trace-error-status-123",
report_kind="openai_chat_sync_finalize",
status_code=200,
headers={"content-type": "application/json"},
body_json={"error": {"message": "rate limited", "code": 429}},
)
assert _resolve_gateway_sync_error_status_code(payload, provider_parser=_FakeParser()) == 429
def test_resolve_gateway_sync_error_status_code_defaults_embedded_200_to_400() -> None:
payload = GatewaySyncReportRequest(
trace_id="trace-error-status-default-123",
report_kind="openai_chat_sync_finalize",
status_code=200,
headers={"content-type": "application/json"},
body_json={"error": {"message": "bad request"}},
)
assert _resolve_gateway_sync_error_status_code(payload) == 400
def test_record_gateway_direct_candidate_graph_creates_full_candidate_graph() -> 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-123",
name="client-key",
user_id="user-direct-123",
user=SimpleNamespace(id="user-direct-123", username="direct-user"),
)
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:chat",
),
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:chat",
),
ProviderCandidate(
provider=SimpleNamespace(id="provider-skipped-123", name="openai", max_retries=1),
endpoint=SimpleNamespace(id="endpoint-skipped-123"),
key=SimpleNamespace(id="key-skipped-123"),
is_skipped=True,
skip_reason="capability_miss",
provider_api_format="openai:chat",
),
]
_record_gateway_direct_candidate_graph(
db=db,
candidate_resolver=resolver,
candidates=candidates,
request_id="req-direct-candidate-graph-123",
user_api_key=user_api_key,
required_capabilities={"tools": True},
selected_candidate_index=0,
)
rows = (
db.query(RequestCandidate)
.filter(RequestCandidate.request_id == "req-direct-candidate-graph-123")
.order_by(RequestCandidate.candidate_index, RequestCandidate.retry_index)
.all()
)
assert len(rows) == 3
assert rows[0].status == "pending"
assert rows[1].status == "unused"
assert rows[2].status == "skipped"
assert getattr(candidates[0], "request_candidate_id") == rows[0].id
assert rows[0].started_at is not None
assert rows[1].finished_at is not None
def test_record_gateway_direct_candidate_graph_uses_selected_pool_key_index() -> 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-pool-123",
name="client-key",
user_id="user-pool-123",
user=SimpleNamespace(id="user-pool-123", username="pool-user"),
)
pool_keys = [
SimpleNamespace(id="pool-key-0"),
SimpleNamespace(id="pool-key-1"),
]
candidate = PoolCandidate(
provider=SimpleNamespace(id="provider-pool-123", name="claude", max_retries=1),
endpoint=SimpleNamespace(id="endpoint-pool-123"),
key=pool_keys[1],
pool_keys=pool_keys,
provider_api_format="claude:cli",
)
candidate._pool_key_index = 1
_record_gateway_direct_candidate_graph(
db=db,
candidate_resolver=resolver,
candidates=[candidate],
request_id="req-direct-pool-graph-123",
user_api_key=user_api_key,
required_capabilities=None,
selected_candidate_index=0,
)
rows = (
db.query(RequestCandidate)
.filter(RequestCandidate.request_id == "req-direct-pool-graph-123")
.order_by(RequestCandidate.candidate_index, RequestCandidate.retry_index)
.all()
)
assert len(rows) == 2
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
@@ -1,21 +0,0 @@
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 == []
@@ -1,7 +0,0 @@
"""Compatibility shim for legacy imports.
Report/finalize/trace tests now live in:
- test_gateway_trace.py
- test_gateway_reporting.py
- test_gateway_finalize.py
"""
File diff suppressed because it is too large Load Diff
-325
View File
@@ -1,325 +0,0 @@
from __future__ import annotations
import json
from collections.abc import AsyncGenerator
from types import SimpleNamespace
from unittest.mock import AsyncMock
import pytest
from fastapi import Response
from fastapi.responses import StreamingResponse
import src.api.public.gemini_files as gemini_files_mod
import src.services.proxy_node.resolver as resolver_mod
import src.services.request.execution_runtime_client as rust_client_mod
from src.api.public.gemini_files import UpstreamContext
from src.config.settings import config
from src.services.request.execution_runtime_plan import ExecutionProxySnapshot
from src.services.request.execution_runtime_client import (
ExecutionRuntimeStreamResult,
ExecutionRuntimeSyncResult,
)
class _DummyStreamResponseCtx:
def __init__(self) -> None:
self.closed = False
async def __aexit__(self, exc_type: object, exc: object, tb: object) -> None:
self.closed = True
class _FakeDBContext:
def __init__(self, db: object) -> None:
self._db = db
def __enter__(self) -> object:
return self._db
def __exit__(self, exc_type: object, exc: object, tb: object) -> bool:
return False
async def _iter_chunks(chunks: list[bytes]) -> AsyncGenerator[bytes]:
for chunk in chunks:
yield chunk
@pytest.mark.asyncio
async def test_enrich_upstream_context_proxy_builds_tunnel_snapshot(
monkeypatch: pytest.MonkeyPatch,
) -> None:
ctx = UpstreamContext(
upstream_key="upstream-key",
base_url="https://generativelanguage.googleapis.com",
file_key_id="key-1",
user_id="user-1",
provider_id="prov-1",
endpoint_id="ep-1",
provider_proxy={"enabled": True, "node_id": "node-1"},
key_proxy=None,
)
monkeypatch.setattr(
resolver_mod,
"resolve_effective_proxy",
lambda provider_proxy, key_proxy=None: provider_proxy,
)
monkeypatch.setattr(
resolver_mod,
"get_system_proxy_config_async",
AsyncMock(return_value=None),
)
monkeypatch.setattr(
resolver_mod,
"resolve_delegate_config_async",
AsyncMock(return_value={"tunnel": True, "node_id": "node-1"}),
)
monkeypatch.setattr(
resolver_mod,
"resolve_proxy_info_async",
AsyncMock(return_value={"mode": "tunnel", "node_id": "node-1", "label": "Node 1"}),
)
async def _unexpected_build_proxy_url_async(proxy_config: object) -> str:
raise AssertionError(f"proxy url should not be built for tunnel: {proxy_config!r}")
monkeypatch.setattr(
resolver_mod,
"build_proxy_url_async",
_unexpected_build_proxy_url_async,
)
enriched = await gemini_files_mod._enrich_upstream_context_proxy(ctx)
assert enriched.proxy_config == {"enabled": True, "node_id": "node-1"}
assert enriched.delegate_config == {"tunnel": True, "node_id": "node-1"}
assert enriched.proxy_snapshot is not None
assert enriched.proxy_snapshot.mode == "tunnel"
assert enriched.proxy_snapshot.node_id == "node-1"
assert enriched.proxy_snapshot.url is None
@pytest.mark.asyncio
async def test_proxy_request_passes_proxy_snapshot_to_rust_executor(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setattr(config, "executor_backend", "rust")
proxy_snapshot = ExecutionProxySnapshot(
enabled=True,
mode="http",
url="http://proxy.local:8080",
)
async def _fake_execute_sync_json(self: object, plan: object) -> ExecutionRuntimeSyncResult:
assert getattr(plan, "method") == "GET"
assert getattr(plan, "provider_id") == "prov-1"
assert getattr(plan, "endpoint_id") == "ep-1"
assert getattr(plan, "proxy").url == "http://proxy.local:8080"
return ExecutionRuntimeSyncResult(
status_code=200,
headers={"content-type": "application/json", "x-rust-files": "true"},
response_json={"files": [{"name": "files/abc"}]},
)
monkeypatch.setattr(
rust_client_mod.ExecutionRuntimeClient,
"execute_sync_json",
_fake_execute_sync_json,
)
response = await gemini_files_mod._proxy_request(
"GET",
"https://generativelanguage.googleapis.com/v1beta/files",
{"x-goog-api-key": "upstream-key"},
file_key_id="key-1",
user_id="user-1",
provider_id="prov-1",
endpoint_id="ep-1",
proxy=proxy_snapshot,
)
assert response.status_code == 200
assert response.headers["x-rust-files"] == "true"
assert json.loads(response.body) == {"files": [{"name": "files/abc"}]}
@pytest.mark.asyncio
async def test_proxy_request_returns_503_when_rust_unavailable(
monkeypatch: pytest.MonkeyPatch,
) -> None:
async def _fake_try_rust_sync_proxy_request(*args: object, **kwargs: object) -> Response:
del args, kwargs
return gemini_files_mod._build_rust_unavailable_response()
monkeypatch.setattr(
gemini_files_mod,
"_try_rust_sync_proxy_request",
_fake_try_rust_sync_proxy_request,
)
response = await gemini_files_mod._proxy_request(
"GET",
"https://generativelanguage.googleapis.com/v1beta/files",
{"x-goog-api-key": "upstream-key"},
file_key_id="key-1",
user_id="user-1",
)
body = json.loads(response.body)
assert response.status_code == 503
assert body["error"]["code"] == 503
assert body["error"]["status"] == "UNAVAILABLE"
@pytest.mark.asyncio
async def test_download_file_uses_enriched_proxy_snapshot_for_regular_files(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setattr(config, "executor_backend", "rust")
dummy_ctx = _DummyStreamResponseCtx()
raw_ctx = UpstreamContext(
upstream_key="upstream-key",
base_url="https://generativelanguage.googleapis.com",
file_key_id="key-1",
user_id="user-1",
provider_id="prov-1",
endpoint_id="ep-1",
)
enriched_ctx = UpstreamContext(
upstream_key="upstream-key",
base_url="https://generativelanguage.googleapis.com",
file_key_id="key-1",
user_id="user-1",
provider_id="prov-1",
endpoint_id="ep-1",
proxy_config={"enabled": True, "url": "http://proxy.local:8080"},
delegate_config=None,
proxy_snapshot=ExecutionProxySnapshot(
enabled=True,
mode="http",
url="http://proxy.local:8080",
),
)
monkeypatch.setattr(gemini_files_mod, "_extract_gemini_api_key", lambda request: "client-key")
monkeypatch.setattr(
gemini_files_mod,
"create_session",
lambda: _FakeDBContext(SimpleNamespace()),
)
monkeypatch.setattr(
gemini_files_mod.AuthService,
"authenticate_api_key",
lambda db, key: (SimpleNamespace(id="user-1"), SimpleNamespace(id="user-api-key")),
)
monkeypatch.setattr(gemini_files_mod, "_ensure_balance_access", lambda db, user, api_key: None)
monkeypatch.setattr(
gemini_files_mod,
"_resolve_upstream_context",
AsyncMock(return_value=raw_ctx),
)
monkeypatch.setattr(
gemini_files_mod,
"_enrich_upstream_context_proxy",
AsyncMock(return_value=enriched_ctx),
)
async def _fake_execute_stream(self: object, plan: object) -> ExecutionRuntimeStreamResult:
assert getattr(plan, "method") == "GET"
assert getattr(plan, "url") == (
"https://generativelanguage.googleapis.com/v1beta/files/file-1:download?alt=media"
)
assert getattr(plan, "headers") == {"x-goog-api-key": "upstream-key"}
assert getattr(plan, "proxy").url == "http://proxy.local:8080"
return ExecutionRuntimeStreamResult(
status_code=200,
headers={"content-type": "application/octet-stream", "x-rust-files": "true"},
byte_iterator=_iter_chunks([b"file-", b"bytes"]),
response_ctx=dummy_ctx,
)
monkeypatch.setattr(
rust_client_mod.ExecutionRuntimeClient,
"execute_stream",
_fake_execute_stream,
)
response = await gemini_files_mod._download_file_response(
"file-1",
SimpleNamespace(
headers={},
query_params={"alt": "media"},
),
)
assert isinstance(response, StreamingResponse)
assert response.headers["x-rust-files"] == "true"
body = b"".join([chunk async for chunk in response.body_iterator])
assert body == b"file-bytes"
assert dummy_ctx.closed is True
@pytest.mark.asyncio
async def test_download_file_returns_503_when_rust_stream_unavailable(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setattr(config, "executor_backend", "rust")
raw_ctx = UpstreamContext(
upstream_key="upstream-key",
base_url="https://generativelanguage.googleapis.com",
file_key_id="key-1",
user_id="user-1",
provider_id="prov-1",
endpoint_id="ep-1",
)
enriched_ctx = UpstreamContext(
upstream_key="upstream-key",
base_url="https://generativelanguage.googleapis.com",
file_key_id="key-1",
user_id="user-1",
provider_id="prov-1",
endpoint_id="ep-1",
)
monkeypatch.setattr(gemini_files_mod, "_extract_gemini_api_key", lambda request: "client-key")
monkeypatch.setattr(
gemini_files_mod,
"create_session",
lambda: _FakeDBContext(SimpleNamespace()),
)
monkeypatch.setattr(
gemini_files_mod.AuthService,
"authenticate_api_key",
lambda db, key: (SimpleNamespace(id="user-1"), SimpleNamespace(id="user-api-key")),
)
monkeypatch.setattr(gemini_files_mod, "_ensure_balance_access", lambda db, user, api_key: None)
monkeypatch.setattr(
gemini_files_mod,
"_resolve_upstream_context",
AsyncMock(return_value=raw_ctx),
)
monkeypatch.setattr(
gemini_files_mod,
"_enrich_upstream_context_proxy",
AsyncMock(return_value=enriched_ctx),
)
async def _fake_execute_stream(self: object, plan: object) -> ExecutionRuntimeStreamResult:
del self, plan
raise rust_client_mod.ExecutionRuntimeClientError("executor unavailable")
monkeypatch.setattr(
rust_client_mod.ExecutionRuntimeClient,
"execute_stream",
_fake_execute_stream,
)
response = await gemini_files_mod._download_file_response(
"file-1",
SimpleNamespace(
headers={},
query_params={"alt": "media"},
),
)
body = json.loads(response.body)
assert response.status_code == 503
assert body["error"]["code"] == 503
assert body["error"]["status"] == "UNAVAILABLE"
-133
View File
@@ -1,133 +0,0 @@
from __future__ import annotations
from typing import Any
from unittest.mock import MagicMock
import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient
from src.database import get_db
def _build_app(
monkeypatch: pytest.MonkeyPatch,
*,
pipeline_result: Any,
) -> tuple[TestClient, list[dict[str, Any]]]:
from src.api.public import gemini_files as mod
app = FastAPI()
app.include_router(mod.router)
app.dependency_overrides[get_db] = lambda: MagicMock()
calls: list[dict[str, Any]] = []
async def _fake_pipeline_run(
*,
adapter: Any,
http_request: object,
db: object,
mode: object,
api_format_hint: str | None = None,
path_params: dict[str, Any] | None = None,
) -> Any:
del http_request, db, api_format_hint, path_params
calls.append(
{
"adapter_type": type(adapter).__name__,
"mode": getattr(mode, "value", mode),
"adapter_state": dict(getattr(adapter, "__dict__", {})),
}
)
return pipeline_result
monkeypatch.setattr(mod.pipeline, "run", _fake_pipeline_run)
return TestClient(app), calls
def test_gemini_files_upload_route_is_pipeline_shell(
monkeypatch: pytest.MonkeyPatch,
) -> None:
client, calls = _build_app(monkeypatch, pipeline_result={"file": {"name": "files/1"}})
response = client.post("/upload/v1beta/files")
assert response.status_code == 200
assert response.json() == {"file": {"name": "files/1"}}
assert calls == [
{
"adapter_type": "PublicGeminiFilesUploadAdapter",
"mode": "public",
"adapter_state": {},
}
]
def test_gemini_files_list_route_is_pipeline_shell(monkeypatch: pytest.MonkeyPatch) -> None:
client, calls = _build_app(monkeypatch, pipeline_result={"files": []})
response = client.get("/v1beta/files?pageSize=20&pageToken=next-1")
assert response.status_code == 200
assert response.json() == {"files": []}
assert calls == [
{
"adapter_type": "PublicGeminiFilesListAdapter",
"mode": "public",
"adapter_state": {
"page_size": 20,
"page_token": "next-1",
},
}
]
def test_gemini_files_download_route_is_pipeline_shell(
monkeypatch: pytest.MonkeyPatch,
) -> None:
client, calls = _build_app(monkeypatch, pipeline_result={"download": "ok"})
response = client.get("/v1beta/files/file-1:download?alt=media")
assert response.status_code == 200
assert response.json() == {"download": "ok"}
assert calls == [
{
"adapter_type": "PublicGeminiFilesDownloadAdapter",
"mode": "public",
"adapter_state": {"file_id": "file-1"},
}
]
def test_gemini_files_get_route_is_pipeline_shell(monkeypatch: pytest.MonkeyPatch) -> None:
client, calls = _build_app(monkeypatch, pipeline_result={"name": "files/file-1"})
response = client.get("/v1beta/files/file-1")
assert response.status_code == 200
assert response.json() == {"name": "files/file-1"}
assert calls == [
{
"adapter_type": "PublicGeminiFilesGetAdapter",
"mode": "public",
"adapter_state": {"file_name": "file-1"},
}
]
def test_gemini_files_delete_route_is_pipeline_shell(monkeypatch: pytest.MonkeyPatch) -> None:
client, calls = _build_app(monkeypatch, pipeline_result={"deleted": True})
response = client.delete("/v1beta/files/file-1")
assert response.status_code == 200
assert response.json() == {"deleted": True}
assert calls == [
{
"adapter_type": "PublicGeminiFilesDeleteAdapter",
"mode": "public",
"adapter_state": {"file_name": "file-1"},
}
]
-117
View File
@@ -1,117 +0,0 @@
from __future__ import annotations
from typing import Any
import pytest
from starlette.requests import Request
def _make_request(path: str, method: str = "POST", headers: list[tuple[bytes, bytes]] | None = None) -> Request:
scope = {
"type": "http",
"asgi": {"version": "3.0"},
"http_version": "1.1",
"method": method,
"scheme": "http",
"path": path,
"raw_path": path.encode(),
"query_string": b"",
"headers": headers or [],
"client": ("127.0.0.1", 12345),
"server": ("testserver", 80),
}
return Request(scope)
@pytest.mark.asyncio
@pytest.mark.parametrize(
("route", "model", "stream"),
[
("v1beta_generate", "gemini-2.5-flash", False),
("v1beta_stream", "gemini-2.5-flash", True),
("v1_generate", "gemini-2.5-flash", False),
("v1_stream", "gemini-2.5-flash", True),
],
)
async def test_public_gemini_routes_use_pipeline_shell(
monkeypatch: pytest.MonkeyPatch,
route: str,
model: str,
stream: bool,
) -> None:
from src.api.public import gemini as mod
captured: dict[str, Any] = {}
async def fake_run(*, adapter, http_request, db, mode, api_format_hint, path_params, **_kwargs):
captured.update(
{
"adapter": adapter,
"request": http_request,
"db": db,
"mode": mode,
"api_format_hint": api_format_hint,
"path_params": path_params,
}
)
return {"ok": True}
monkeypatch.setattr(mod.pipeline, "run", fake_run)
db = object()
request = _make_request(f"/{route}")
if route == "v1beta_generate":
result = await mod.generate_content(model=model, http_request=request, db=db)
elif route == "v1beta_stream":
result = await mod.stream_generate_content(model=model, http_request=request, db=db)
elif route == "v1_generate":
result = await mod.generate_content_v1(model=model, http_request=request, db=db)
else:
result = await mod.stream_generate_content_v1(model=model, http_request=request, db=db)
assert result == {"ok": True}
assert isinstance(captured["adapter"], mod.PublicGeminiContentAdapter)
assert captured["adapter"].model == model
assert captured["adapter"].stream is stream
assert captured["request"] is request
assert captured["db"] is db
assert captured["mode"] == captured["adapter"].mode
assert captured["api_format_hint"] == "gemini:chat"
assert captured["path_params"] == {"model": model, "stream": stream}
@pytest.mark.asyncio
async def test_public_gemini_shell_detects_cli_request_for_api_format_hint(
monkeypatch: pytest.MonkeyPatch,
) -> None:
from src.api.public import gemini as mod
captured: dict[str, Any] = {}
async def fake_run(*, adapter, http_request, db, mode, api_format_hint, path_params, **_kwargs):
captured.update(
{
"adapter": adapter,
"request": http_request,
"db": db,
"mode": mode,
"api_format_hint": api_format_hint,
"path_params": path_params,
}
)
return {"ok": True}
monkeypatch.setattr(mod.pipeline, "run", fake_run)
request = _make_request(
"/v1beta/models/gemini-2.5-flash:generateContent",
headers=[(b"x-app", b"gemini-cli")],
)
result = await mod.generate_content(model="gemini-2.5-flash", http_request=request, db=object())
assert result == {"ok": True}
assert isinstance(captured["adapter"], mod.PublicGeminiContentAdapter)
assert captured["api_format_hint"] == "gemini:cli"
assert captured["path_params"] == {"model": "gemini-2.5-flash", "stream": False}
-153
View File
@@ -1,153 +0,0 @@
from __future__ import annotations
from typing import Any
from unittest.mock import MagicMock
import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient
from src.database import get_db
def _build_app(
monkeypatch: pytest.MonkeyPatch,
module_name: str,
*,
pipeline_result: Any,
) -> tuple[TestClient, list[dict[str, Any]]]:
module = __import__(module_name, fromlist=["router", "pipeline"])
app = FastAPI()
app.include_router(module.router)
app.dependency_overrides[get_db] = lambda: MagicMock()
calls: list[dict[str, Any]] = []
async def _fake_pipeline_run(
*,
adapter: Any,
http_request: object,
db: object,
mode: object,
api_format_hint: str | None = None,
path_params: dict[str, Any] | None = None,
) -> Any:
del http_request, db, api_format_hint, path_params
calls.append(
{
"adapter_type": type(adapter).__name__,
"mode": getattr(mode, "value", mode),
"adapter_state": dict(getattr(adapter, "__dict__", {})),
}
)
return pipeline_result
monkeypatch.setattr(module.pipeline, "run", _fake_pipeline_run)
return TestClient(app), calls
def test_public_site_info_route_is_pipeline_shell(monkeypatch: pytest.MonkeyPatch) -> None:
client, calls = _build_app(
monkeypatch,
"src.api.public.catalog",
pipeline_result={"site_name": "Aether", "site_subtitle": "AI Gateway"},
)
response = client.get("/api/public/site-info")
assert response.status_code == 200
assert response.json() == {"site_name": "Aether", "site_subtitle": "AI Gateway"}
assert calls == [
{
"adapter_type": "PublicSiteInfoAdapter",
"mode": "public",
"adapter_state": {},
}
]
def test_public_modules_auth_status_route_is_pipeline_shell(
monkeypatch: pytest.MonkeyPatch,
) -> None:
client, calls = _build_app(
monkeypatch,
"src.api.public.modules",
pipeline_result=[{"name": "oauth", "display_name": "OAuth", "active": True}],
)
response = client.get("/api/modules/auth-status")
assert response.status_code == 200
assert response.json() == [{"name": "oauth", "display_name": "OAuth", "active": True}]
assert calls == [
{
"adapter_type": "PublicAuthModulesStatusAdapter",
"mode": "public",
"adapter_state": {},
}
]
def test_public_capabilities_route_is_pipeline_shell(monkeypatch: pytest.MonkeyPatch) -> None:
client, calls = _build_app(
monkeypatch,
"src.api.public.capabilities",
pipeline_result={"capabilities": []},
)
response = client.get("/api/capabilities")
assert response.status_code == 200
assert response.json() == {"capabilities": []}
assert calls == [
{
"adapter_type": "PublicCapabilitiesListAdapter",
"mode": "public",
"adapter_state": {},
}
]
def test_public_user_configurable_capabilities_route_is_pipeline_shell(
monkeypatch: pytest.MonkeyPatch,
) -> None:
client, calls = _build_app(
monkeypatch,
"src.api.public.capabilities",
pipeline_result={"capabilities": [{"name": "vision"}]},
)
response = client.get("/api/capabilities/user-configurable")
assert response.status_code == 200
assert response.json() == {"capabilities": [{"name": "vision"}]}
assert calls == [
{
"adapter_type": "PublicUserConfigurableCapabilitiesAdapter",
"mode": "public",
"adapter_state": {},
}
]
def test_public_model_capabilities_route_is_pipeline_shell(
monkeypatch: pytest.MonkeyPatch,
) -> None:
client, calls = _build_app(
monkeypatch,
"src.api.public.capabilities",
pipeline_result={"model": "gpt-5", "supported_capabilities": []},
)
response = client.get("/api/capabilities/model/gpt-5")
assert response.status_code == 200
assert response.json() == {"model": "gpt-5", "supported_capabilities": []}
assert calls == [
{
"adapter_type": "PublicModelCapabilitiesAdapter",
"mode": "public",
"adapter_state": {"model_name": "gpt-5"},
}
]
-130
View File
@@ -1,130 +0,0 @@
from __future__ import annotations
from typing import Any
from unittest.mock import MagicMock
import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient
from src.database import get_db
def _build_app(
monkeypatch: pytest.MonkeyPatch,
*,
pipeline_result: Any,
) -> tuple[TestClient, list[dict[str, Any]]]:
from src.api.public import models as mod
app = FastAPI()
app.include_router(mod.router)
app.dependency_overrides[get_db] = lambda: MagicMock()
calls: list[dict[str, Any]] = []
async def _fake_pipeline_run(
*,
adapter: Any,
http_request: object,
db: object,
mode: object,
api_format_hint: str | None = None,
path_params: dict[str, Any] | None = None,
) -> Any:
del http_request, db, api_format_hint, path_params
calls.append(
{
"adapter_type": type(adapter).__name__,
"mode": getattr(mode, "value", mode),
"adapter_state": dict(getattr(adapter, "__dict__", {})),
}
)
return pipeline_result
monkeypatch.setattr(mod.pipeline, "run", _fake_pipeline_run)
return TestClient(app), calls
def test_public_openai_models_route_is_pipeline_shell(monkeypatch: pytest.MonkeyPatch) -> None:
client, calls = _build_app(monkeypatch, pipeline_result={"object": "list", "data": []})
response = client.get("/v1/models?after_id=model-a&limit=12")
assert response.status_code == 200
assert response.json() == {"object": "list", "data": []}
assert calls == [
{
"adapter_type": "PublicModelsListAdapter",
"mode": "public",
"adapter_state": {
"before_id": None,
"after_id": "model-a",
"limit": 12,
"page_size": 50,
"page_token": None,
},
}
]
def test_public_model_detail_route_is_pipeline_shell(monkeypatch: pytest.MonkeyPatch) -> None:
client, calls = _build_app(monkeypatch, pipeline_result={"id": "gpt-5"})
response = client.get("/v1/models/gpt-5")
assert response.status_code == 200
assert response.json() == {"id": "gpt-5"}
assert calls == [
{
"adapter_type": "PublicModelDetailAdapter",
"mode": "public",
"adapter_state": {
"model_id": "gpt-5",
"force_gemini_name": False,
},
}
]
def test_public_gemini_models_route_is_pipeline_shell(monkeypatch: pytest.MonkeyPatch) -> None:
client, calls = _build_app(monkeypatch, pipeline_result={"models": []})
response = client.get("/v1beta/models?pageSize=25&pageToken=next-1")
assert response.status_code == 200
assert response.json() == {"models": []}
assert calls == [
{
"adapter_type": "PublicModelsListAdapter",
"mode": "public",
"adapter_state": {
"before_id": None,
"after_id": None,
"limit": 20,
"page_size": 25,
"page_token": "next-1",
},
}
]
def test_public_gemini_model_detail_route_is_pipeline_shell(
monkeypatch: pytest.MonkeyPatch,
) -> None:
client, calls = _build_app(monkeypatch, pipeline_result={"name": "models/gemini-2.5-pro"})
response = client.get("/v1beta/models/models/gemini-2.5-pro")
assert response.status_code == 200
assert response.json() == {"name": "models/gemini-2.5-pro"}
assert calls == [
{
"adapter_type": "PublicModelDetailAdapter",
"mode": "public",
"adapter_state": {
"model_id": "models/gemini-2.5-pro",
"force_gemini_name": True,
},
}
]
@@ -1,130 +0,0 @@
from __future__ import annotations
from types import SimpleNamespace
from unittest.mock import AsyncMock, MagicMock
import httpx
import pytest
from fastapi import HTTPException
def _build_provider_fixture() -> SimpleNamespace:
endpoint = SimpleNamespace(
id="endpoint_1",
base_url="https://upstream.test",
api_format="openai:chat",
is_active=True,
)
key = SimpleNamespace(
id="key_1",
is_active=True,
api_formats=None,
)
return SimpleNamespace(
id="provider_1",
name="openai",
endpoints=[endpoint],
api_keys=[key],
)
@pytest.mark.asyncio
async def test_test_connection_prefers_rust_executor(
monkeypatch: pytest.MonkeyPatch,
) -> None:
from src.api.public import system_catalog as mod
provider = _build_provider_fixture()
monkeypatch.setattr(mod, "_select_provider", lambda _db, _provider_name: provider)
monkeypatch.setattr(mod, "build_test_request_body", lambda *_args, **_kwargs: {"model": "gpt-test"})
monkeypatch.setattr(mod, "get_provider_auth", AsyncMock(return_value=None))
class _DummyBuilder:
def build(self, *_args: object, **_kwargs: object) -> tuple[dict[str, str], dict[str, str]]:
return {"model": "gpt-test"}, {"authorization": "Bearer test"}
monkeypatch.setattr(mod, "PassthroughRequestBuilder", lambda: _DummyBuilder())
monkeypatch.setattr(
mod,
"build_provider_url",
lambda *_args, **_kwargs: "https://upstream.test/v1/chat/completions",
)
proxy_snapshot = object()
monkeypatch.setattr(
mod,
"_build_test_connection_transport_context",
AsyncMock(return_value=({"enabled": True}, {"node_id": "node-1", "tunnel": True}, proxy_snapshot)),
)
rust_response = httpx.Response(
200,
request=httpx.Request("POST", "https://upstream.test/v1/chat/completions"),
json={"id": "resp_rust"},
)
rust_call = AsyncMock(return_value=rust_response)
monkeypatch.setattr(mod, "_try_rust_test_connection_response", rust_call)
result = await mod._test_connection_response(
request=SimpleNamespace(query_params={}),
db=MagicMock(),
provider=None,
model="gpt-test",
api_format=None,
)
assert result["status"] == "success"
assert result["response_id"] == "resp_rust"
rust_call.assert_awaited_once()
@pytest.mark.asyncio
async def test_test_connection_returns_503_when_rust_unavailable(
monkeypatch: pytest.MonkeyPatch,
) -> None:
from src.api.public import system_catalog as mod
provider = _build_provider_fixture()
monkeypatch.setattr(mod, "_select_provider", lambda _db, _provider_name: provider)
monkeypatch.setattr(mod, "build_test_request_body", lambda *_args, **_kwargs: {"model": "gpt-test"})
monkeypatch.setattr(mod, "get_provider_auth", AsyncMock(return_value=None))
class _DummyBuilder:
def build(self, *_args: object, **_kwargs: object) -> tuple[dict[str, str], dict[str, str]]:
return {"model": "gpt-test"}, {"authorization": "Bearer test"}
monkeypatch.setattr(mod, "PassthroughRequestBuilder", lambda: _DummyBuilder())
monkeypatch.setattr(
mod,
"build_provider_url",
lambda *_args, **_kwargs: "https://upstream.test/v1/chat/completions",
)
monkeypatch.setattr(
mod,
"_build_test_connection_transport_context",
AsyncMock(return_value=({"enabled": True}, {"node_id": "node-1", "tunnel": True}, None)),
)
monkeypatch.setattr(
mod,
"_try_rust_test_connection_response",
AsyncMock(
side_effect=HTTPException(
status_code=503,
detail="System catalog test-connection requires Rust executor",
)
),
)
with pytest.raises(HTTPException) as exc_info:
await mod._test_connection_response(
request=SimpleNamespace(query_params={}),
db=MagicMock(),
provider=None,
model="gpt-test",
api_format=None,
)
assert exc_info.value.status_code == 503
assert exc_info.value.detail == "System catalog test-connection requires Rust executor"
@@ -1,164 +0,0 @@
from __future__ import annotations
from typing import Any
from unittest.mock import MagicMock
import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient
from src.database import get_db
def _build_app(
monkeypatch: pytest.MonkeyPatch,
*,
pipeline_result: Any,
) -> tuple[TestClient, list[dict[str, Any]]]:
from src.api.public import system_catalog as mod
app = FastAPI()
app.include_router(mod.router)
app.dependency_overrides[get_db] = lambda: MagicMock()
calls: list[dict[str, Any]] = []
async def _fake_pipeline_run(
*,
adapter: Any,
http_request: object,
db: object,
mode: object,
api_format_hint: str | None = None,
path_params: dict[str, Any] | None = None,
) -> Any:
del http_request, db, api_format_hint, path_params
calls.append(
{
"adapter_type": type(adapter).__name__,
"mode": getattr(mode, "value", mode),
"adapter_state": dict(getattr(adapter, "__dict__", {})),
}
)
return pipeline_result
monkeypatch.setattr(mod.pipeline, "run", _fake_pipeline_run)
return TestClient(app), calls
def test_system_catalog_health_route_is_pipeline_shell(
monkeypatch: pytest.MonkeyPatch,
) -> None:
client, calls = _build_app(monkeypatch, pipeline_result={"status": "ok"})
response = client.get("/v1/health")
assert response.status_code == 200
assert response.json() == {"status": "ok"}
assert calls == [
{
"adapter_type": "PublicServiceHealthAdapter",
"mode": "public",
"adapter_state": {},
}
]
def test_system_catalog_simple_health_route_is_pipeline_shell(
monkeypatch: pytest.MonkeyPatch,
) -> None:
client, calls = _build_app(monkeypatch, pipeline_result={"status": "healthy"})
response = client.get("/health")
assert response.status_code == 200
assert response.json() == {"status": "healthy"}
assert calls == [
{
"adapter_type": "PublicSimpleHealthCheckAdapter",
"mode": "public",
"adapter_state": {},
}
]
def test_system_catalog_root_route_is_pipeline_shell(monkeypatch: pytest.MonkeyPatch) -> None:
client, calls = _build_app(monkeypatch, pipeline_result={"status": "running"})
response = client.get("/")
assert response.status_code == 200
assert response.json() == {"status": "running"}
assert calls == [
{
"adapter_type": "PublicRootCatalogAdapter",
"mode": "public",
"adapter_state": {},
}
]
def test_system_catalog_provider_list_route_is_pipeline_shell(
monkeypatch: pytest.MonkeyPatch,
) -> None:
client, calls = _build_app(monkeypatch, pipeline_result={"providers": []})
response = client.get("/v1/providers?include_models=true&include_endpoints=true&active_only=false")
assert response.status_code == 200
assert response.json() == {"providers": []}
assert calls == [
{
"adapter_type": "PublicProvidersListAdapter",
"mode": "public",
"adapter_state": {
"include_models": True,
"include_endpoints": True,
"active_only": False,
},
}
]
def test_system_catalog_provider_detail_route_is_pipeline_shell(
monkeypatch: pytest.MonkeyPatch,
) -> None:
client, calls = _build_app(monkeypatch, pipeline_result={"id": "provider-1"})
response = client.get("/v1/providers/provider-1?include_models=true")
assert response.status_code == 200
assert response.json() == {"id": "provider-1"}
assert calls == [
{
"adapter_type": "PublicProviderDetailAdapter",
"mode": "public",
"adapter_state": {
"provider_identifier": "provider-1",
"include_models": True,
"include_endpoints": False,
},
}
]
def test_system_catalog_test_connection_route_is_pipeline_shell(
monkeypatch: pytest.MonkeyPatch,
) -> None:
client, calls = _build_app(monkeypatch, pipeline_result={"status": "success"})
response = client.get("/v1/test-connection?provider=openai&model=gpt-5&api_format=openai:chat")
assert response.status_code == 200
assert response.json() == {"status": "success"}
assert calls == [
{
"adapter_type": "PublicTestConnectionAdapter",
"mode": "public",
"adapter_state": {
"provider": "openai",
"model": "gpt-5",
"api_format": "openai:chat",
},
}
]
-123
View File
@@ -1,123 +0,0 @@
from __future__ import annotations
from typing import Any
from unittest.mock import MagicMock
import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient
from src.database import get_db
def _build_app(monkeypatch: pytest.MonkeyPatch, *, pipeline_result: Any) -> tuple[TestClient, list[dict[str, Any]]]:
from src.api.public import videos as mod
app = FastAPI()
app.include_router(mod.router)
app.dependency_overrides[get_db] = lambda: MagicMock()
calls: list[dict[str, Any]] = []
async def _fake_pipeline_run(
*,
adapter: Any,
http_request: object,
db: object,
mode: object,
api_format_hint: str,
path_params: dict[str, Any] | None = None,
) -> Any:
del http_request, db
calls.append(
{
"adapter_type": type(adapter).__name__,
"mode": getattr(mode, "value", mode),
"api_format_hint": api_format_hint,
"path_params": path_params,
}
)
return pipeline_result
monkeypatch.setattr(mod.pipeline, "run", _fake_pipeline_run)
return TestClient(app), calls
def test_openai_video_create_route_is_pipeline_shell(monkeypatch: pytest.MonkeyPatch) -> None:
client, calls = _build_app(monkeypatch, pipeline_result={"ok": True})
response = client.post("/v1/videos", json={"model": "sora", "prompt": "hello"})
assert response.status_code == 200
assert response.json() == {"ok": True}
assert calls == [
{
"adapter_type": "OpenAIVideoAdapter",
"mode": "standard",
"api_format_hint": "openai:video",
"path_params": None,
}
]
def test_openai_video_download_route_passes_task_id_to_pipeline(
monkeypatch: pytest.MonkeyPatch,
) -> None:
client, calls = _build_app(monkeypatch, pipeline_result={"download": True})
response = client.get("/v1/videos/task-123/content")
assert response.status_code == 200
assert response.json() == {"download": True}
assert calls == [
{
"adapter_type": "OpenAIVideoAdapter",
"mode": "standard",
"api_format_hint": "openai:video",
"path_params": {"task_id": "task-123"},
}
]
def test_gemini_video_create_route_passes_model_to_pipeline(
monkeypatch: pytest.MonkeyPatch,
) -> None:
client, calls = _build_app(monkeypatch, pipeline_result={"ok": True})
response = client.post(
"/v1beta/models/veo-3:predictLongRunning",
json={"prompt": "hello"},
)
assert response.status_code == 200
assert response.json() == {"ok": True}
assert calls == [
{
"adapter_type": "GeminiVeoAdapter",
"mode": "standard",
"api_format_hint": "gemini:video",
"path_params": {"model": "veo-3"},
}
]
def test_gemini_video_cancel_route_reconstructs_operation_name(
monkeypatch: pytest.MonkeyPatch,
) -> None:
client, calls = _build_app(monkeypatch, pipeline_result={"ok": True})
response = client.post("/v1beta/models/veo-3/operations/op-1:cancel")
assert response.status_code == 200
assert response.json() == {"ok": True}
assert calls == [
{
"adapter_type": "GeminiVeoAdapter",
"mode": "standard",
"api_format_hint": "gemini:video",
"path_params": {
"task_id": "models/veo-3/operations/op-1",
"action": "cancel",
},
}
]
@@ -1,505 +0,0 @@
from __future__ import annotations
from contextlib import contextmanager
from datetime import datetime, timezone
from types import SimpleNamespace
from typing import Generator
from unittest.mock import MagicMock
import pytest
from fastapi import FastAPI, HTTPException
from fastapi.testclient import TestClient
from src.api.admin.api_keys.routes import (
AdminCreateStandaloneKeyAdapter,
AdminGetFullKeyAdapter,
AdminToggleApiKeyAdapter,
AdminUpdateApiKeyAdapter,
)
from src.api.admin.api_keys.routes import router as admin_api_keys_router
from src.api.admin.users.routes import (
AdminGetUserKeyFullKeyAdapter,
AdminToggleUserKeyLockAdapter,
AdminUpdateUserKeyAdapter,
)
from src.api.admin.users.routes import router as admin_users_router
from src.core.exceptions import InvalidRequestException, NotFoundException
from src.database import get_db
from src.models.api import CreateApiKeyRequest
def _patch_get_db_context(monkeypatch: pytest.MonkeyPatch, db: MagicMock) -> None:
@contextmanager
def _fake_ctx() -> Generator[MagicMock, None, None]:
yield db
monkeypatch.setattr("src.api.admin.users.routes.get_db_context", _fake_ctx)
monkeypatch.setattr("src.api.admin.api_keys.routes.get_db_context", _fake_ctx)
def _build_context(db: MagicMock) -> SimpleNamespace:
return SimpleNamespace(
db=db,
request=SimpleNamespace(state=SimpleNamespace()),
add_audit_metadata=lambda **_: None,
)
def _mock_query_first(db: MagicMock, value: object | None) -> None:
db.query.return_value.filter.return_value.first.return_value = value
def _build_admin_users_app(db: MagicMock, monkeypatch: pytest.MonkeyPatch) -> TestClient:
app = FastAPI()
app.include_router(admin_users_router)
app.dependency_overrides[get_db] = lambda: db
async def _fake_pipeline_run(
*, adapter: object, http_request: object, db: MagicMock, mode: object
) -> object:
_ = http_request, mode
try:
payload = await http_request.json()
except Exception:
payload = {}
context = SimpleNamespace(
db=db,
request=SimpleNamespace(state=SimpleNamespace()),
user=SimpleNamespace(id="admin-1"),
ensure_json_body=lambda: payload,
add_audit_metadata=lambda **_: None,
)
return await adapter.handle(context)
monkeypatch.setattr("src.api.admin.users.routes.pipeline.run", _fake_pipeline_run)
return TestClient(app)
def _build_admin_api_keys_app(db: MagicMock, monkeypatch: pytest.MonkeyPatch) -> TestClient:
app = FastAPI()
app.include_router(admin_api_keys_router)
app.dependency_overrides[get_db] = lambda: db
async def _fake_pipeline_run(
*, adapter: object, http_request: object, db: MagicMock, mode: object
) -> object:
_ = http_request, mode
context = SimpleNamespace(
db=db,
request=SimpleNamespace(state=SimpleNamespace()),
user=SimpleNamespace(id="admin-1"),
ensure_json_body=lambda: {},
add_audit_metadata=lambda **_: None,
)
return await adapter.handle(context)
monkeypatch.setattr("src.api.admin.api_keys.routes.pipeline.run", _fake_pipeline_run)
return TestClient(app)
@pytest.mark.asyncio
async def test_toggle_user_key_lock_adapter_success(monkeypatch: pytest.MonkeyPatch) -> None:
db = MagicMock()
api_key = SimpleNamespace(id="key-1", user_id="user-1", is_standalone=False, is_locked=False)
_mock_query_first(db, api_key)
_patch_get_db_context(monkeypatch, db)
adapter = AdminToggleUserKeyLockAdapter(user_id="user-1", key_id="key-1")
result = await adapter.handle(_build_context(db))
assert result["id"] == "key-1"
assert result["is_locked"] is True
assert "锁定" in result["message"]
db.commit.assert_called_once()
db.refresh.assert_called_once_with(api_key)
@pytest.mark.asyncio
async def test_toggle_user_key_lock_adapter_not_found_for_standalone_or_wrong_owner(
monkeypatch: pytest.MonkeyPatch,
) -> None:
db = MagicMock()
_mock_query_first(db, None)
_patch_get_db_context(monkeypatch, db)
adapter = AdminToggleUserKeyLockAdapter(user_id="user-1", key_id="key-standalone")
with pytest.raises(NotFoundException):
await adapter.handle(_build_context(db))
db.commit.assert_not_called()
@pytest.mark.asyncio
async def test_get_user_key_full_key_adapter_success(monkeypatch: pytest.MonkeyPatch) -> None:
db = MagicMock()
api_key = SimpleNamespace(
id="key-2",
user_id="user-1",
is_standalone=False,
key_encrypted="encrypted-value",
)
_mock_query_first(db, api_key)
monkeypatch.setattr("src.core.crypto.crypto_service.decrypt", lambda _v: "sk-user-full-key")
adapter = AdminGetUserKeyFullKeyAdapter(user_id="user-1", key_id="key-2")
result = await adapter.handle(_build_context(db))
assert result == {"key": "sk-user-full-key"}
@pytest.mark.asyncio
async def test_get_user_key_full_key_adapter_requires_encrypted_key() -> None:
db = MagicMock()
api_key = SimpleNamespace(
id="key-3",
user_id="user-1",
is_standalone=False,
key_encrypted=None,
)
_mock_query_first(db, api_key)
adapter = AdminGetUserKeyFullKeyAdapter(user_id="user-1", key_id="key-3")
with pytest.raises(InvalidRequestException):
await adapter.handle(_build_context(db))
@pytest.mark.asyncio
async def test_get_user_key_full_key_adapter_returns_500_on_decrypt_error(
monkeypatch: pytest.MonkeyPatch,
) -> None:
db = MagicMock()
api_key = SimpleNamespace(
id="key-4",
user_id="user-1",
is_standalone=False,
key_encrypted="encrypted-value",
)
_mock_query_first(db, api_key)
def _raise(_: str) -> str:
raise ValueError("decrypt failed")
monkeypatch.setattr("src.core.crypto.crypto_service.decrypt", _raise)
adapter = AdminGetUserKeyFullKeyAdapter(user_id="user-1", key_id="key-4")
with pytest.raises(HTTPException) as exc_info:
await adapter.handle(_build_context(db))
assert exc_info.value.status_code == 500
@pytest.mark.asyncio
async def test_standalone_toggle_adapters_reject_normal_user_key(
monkeypatch: pytest.MonkeyPatch,
) -> None:
db = MagicMock()
normal_key = SimpleNamespace(
id="key-user",
user_id="user-1",
is_standalone=False,
is_active=True,
is_locked=False,
key_encrypted="encrypted-value",
updated_at=datetime.now(timezone.utc),
)
_mock_query_first(db, normal_key)
_patch_get_db_context(monkeypatch, db)
context = _build_context(db)
with pytest.raises(InvalidRequestException):
await AdminToggleApiKeyAdapter(key_id="key-user").handle(context)
with pytest.raises(InvalidRequestException):
await AdminGetFullKeyAdapter(key_id="key-user").handle(context)
def test_user_key_lock_route_path_smoke(monkeypatch: pytest.MonkeyPatch) -> None:
db = MagicMock()
api_key = SimpleNamespace(id="key-5", user_id="user-2", is_standalone=False, is_locked=False)
_mock_query_first(db, api_key)
_patch_get_db_context(monkeypatch, db)
client = _build_admin_users_app(db, monkeypatch)
response = client.patch("/api/admin/users/user-2/api-keys/key-5/lock")
assert response.status_code == 200
assert response.json()["id"] == "key-5"
assert response.json()["is_locked"] is True
def test_user_key_full_key_route_path_smoke(monkeypatch: pytest.MonkeyPatch) -> None:
db = MagicMock()
api_key = SimpleNamespace(
id="key-6",
user_id="user-2",
is_standalone=False,
key_encrypted="enc",
)
_mock_query_first(db, api_key)
monkeypatch.setattr("src.core.crypto.crypto_service.decrypt", lambda _v: "sk-user-route-key")
client = _build_admin_users_app(db, monkeypatch)
response = client.get("/api/admin/users/user-2/api-keys/key-6/full-key")
assert response.status_code == 200
assert response.json() == {"key": "sk-user-route-key"}
@pytest.mark.asyncio
async def test_update_user_key_adapter_passes_rate_limit_and_name(
monkeypatch: pytest.MonkeyPatch,
) -> None:
captured: dict[str, object] = {}
def _update_user_key_sync(
user_id: str, key_id: str, request: object
) -> tuple[dict[str, object], dict[str, object]]:
captured["user_id"] = user_id
captured["key_id"] = key_id
captured["name"] = getattr(request, "name", None)
captured["rate_limit"] = getattr(request, "rate_limit", None)
return {"id": key_id, "name": captured["name"], "rate_limit": captured["rate_limit"]}, {}
monkeypatch.setattr("src.api.admin.users.routes._update_user_key_sync", _update_user_key_sync)
adapter = AdminUpdateUserKeyAdapter(user_id="user-1", key_id="key-7")
context = SimpleNamespace(
db=MagicMock(),
request=SimpleNamespace(state=SimpleNamespace()),
ensure_json_body=lambda: {"name": "Renamed Key", "rate_limit": 12},
add_audit_metadata=lambda **_: None,
)
result = await adapter.handle(context)
assert result["id"] == "key-7"
assert captured == {
"user_id": "user-1",
"key_id": "key-7",
"name": "Renamed Key",
"rate_limit": 12,
}
def test_update_user_key_route_path_smoke(monkeypatch: pytest.MonkeyPatch) -> None:
captured: dict[str, object] = {}
def _update_user_key_sync(
user_id: str, key_id: str, request: object
) -> tuple[dict[str, object], dict[str, object]]:
captured["user_id"] = user_id
captured["key_id"] = key_id
captured["name"] = getattr(request, "name", None)
captured["rate_limit"] = getattr(request, "rate_limit", None)
return {"id": key_id, "name": captured["name"], "rate_limit": captured["rate_limit"]}, {}
monkeypatch.setattr("src.api.admin.users.routes._update_user_key_sync", _update_user_key_sync)
client = _build_admin_users_app(MagicMock(), monkeypatch)
response = client.put(
"/api/admin/users/user-2/api-keys/key-8",
json={"name": "Updated", "rate_limit": 9},
)
assert response.status_code == 200
assert response.json()["rate_limit"] == 9
assert captured == {
"user_id": "user-2",
"key_id": "key-8",
"name": "Updated",
"rate_limit": 9,
}
def test_standalone_lock_route_removed(monkeypatch: pytest.MonkeyPatch) -> None:
client = _build_admin_api_keys_app(MagicMock(), monkeypatch)
response = client.patch("/api/admin/api-keys/key-1/lock")
assert response.status_code == 404
def test_standalone_list_route_does_not_expose_is_locked(monkeypatch: pytest.MonkeyPatch) -> None:
db = MagicMock()
_patch_get_db_context(monkeypatch, db)
api_key = SimpleNamespace(
id="sa-key-1",
user_id="admin-1",
name="Standalone Key",
get_display_key=lambda: "sk-stand...1234",
is_active=True,
is_standalone=True,
total_requests=0,
total_cost_usd=0,
rate_limit=None,
allowed_providers=None,
allowed_api_formats=None,
allowed_models=None,
last_used_at=None,
expires_at=None,
created_at=datetime.now(timezone.utc),
updated_at=None,
auto_delete_on_expiry=False,
)
query = db.query.return_value.filter.return_value
query.count.return_value = 1
query.order_by.return_value.offset.return_value.limit.return_value.all.return_value = [api_key]
monkeypatch.setattr(
"src.api.admin.api_keys.routes.WalletService.get_wallet",
lambda _db, user_id=None, api_key_id=None, user=None, api_key=None: SimpleNamespace(
id="w-1"
),
)
client = _build_admin_api_keys_app(db, monkeypatch)
response = client.get("/api/admin/api-keys")
assert response.status_code == 200
payload = response.json()
assert len(payload["api_keys"]) == 1
assert "is_locked" not in payload["api_keys"][0]
def test_standalone_detail_route_does_not_expose_is_locked(monkeypatch: pytest.MonkeyPatch) -> None:
db = MagicMock()
api_key = SimpleNamespace(
id="sa-key-2",
user_id="admin-1",
name="Standalone Key 2",
get_display_key=lambda: "sk-stand...5678",
is_active=True,
is_standalone=True,
total_requests=0,
total_cost_usd=0,
rate_limit=None,
allowed_providers=[],
allowed_api_formats=[],
allowed_models=[],
last_used_at=None,
expires_at=None,
created_at=datetime.now(timezone.utc),
updated_at=None,
)
_mock_query_first(db, api_key)
monkeypatch.setattr(
"src.api.admin.api_keys.routes.WalletService.get_wallet",
lambda _db, user_id=None, api_key_id=None, user=None, api_key=None: None,
)
client = _build_admin_api_keys_app(db, monkeypatch)
response = client.get("/api/admin/api-keys/sa-key-2")
assert response.status_code == 200
payload = response.json()
assert payload["id"] == "sa-key-2"
assert "is_locked" not in payload
@pytest.mark.asyncio
async def test_create_standalone_key_adapter_preserves_empty_restriction_lists(
monkeypatch: pytest.MonkeyPatch,
) -> None:
db = MagicMock()
_patch_get_db_context(monkeypatch, db)
captured: dict[str, object] = {}
created_key = SimpleNamespace(
id="sa-key-3",
name="Standalone Key 3",
get_display_key=lambda: "sk-stand...9012",
is_active=True,
rate_limit=None,
expires_at=None,
created_at=datetime.now(timezone.utc),
allowed_providers=[],
allowed_api_formats=[],
allowed_models=[],
)
def _create_api_key(**kwargs: object) -> tuple[SimpleNamespace, str]:
captured.update(kwargs)
return created_key, "sk-created"
monkeypatch.setattr(
"src.api.admin.api_keys.routes.ApiKeyService.create_api_key", _create_api_key
)
monkeypatch.setattr(
"src.api.admin.api_keys.routes.WalletService.initialize_api_key_wallet",
lambda *_a, **_k: SimpleNamespace(id="wallet-1"),
)
monkeypatch.setattr(
"src.api.admin.api_keys.routes.WalletService.serialize_wallet_summary",
lambda _wallet: {"id": "wallet-1"},
)
adapter = AdminCreateStandaloneKeyAdapter(
CreateApiKeyRequest(
name="Standalone Key 3",
initial_balance_usd=10,
allowed_providers=[],
allowed_api_formats=[],
allowed_models=[],
)
)
context = SimpleNamespace(
db=db,
user=SimpleNamespace(id="admin-1"),
request=SimpleNamespace(state=SimpleNamespace()),
add_audit_metadata=lambda **_: None,
)
result = await adapter.handle(context)
assert result["id"] == "sa-key-3"
assert captured["allowed_providers"] == []
assert captured["allowed_api_formats"] == []
assert captured["allowed_models"] == []
@pytest.mark.asyncio
async def test_update_standalone_key_adapter_preserves_empty_restriction_lists(
monkeypatch: pytest.MonkeyPatch,
) -> None:
db = MagicMock()
_patch_get_db_context(monkeypatch, db)
existing_key = SimpleNamespace(id="sa-key-4", is_standalone=True)
_mock_query_first(db, existing_key)
updated_key = SimpleNamespace(
id="sa-key-4",
name="Standalone Key 4",
get_display_key=lambda: "sk-stand...3456",
is_active=True,
rate_limit=None,
expires_at=None,
updated_at=datetime.now(timezone.utc),
)
captured: dict[str, object] = {}
def _update_api_key(_db: MagicMock, _key_id: str, **kwargs: object) -> SimpleNamespace:
captured.update(kwargs)
return updated_key
monkeypatch.setattr(
"src.api.admin.api_keys.routes.ApiKeyService.update_api_key", _update_api_key
)
monkeypatch.setattr(
"src.api.admin.api_keys.routes._ensure_standalone_wallet",
lambda *_a, **_k: SimpleNamespace(id="wallet-2"),
)
monkeypatch.setattr(
"src.api.admin.api_keys.routes.WalletService.serialize_wallet_summary",
lambda _wallet: {"id": "wallet-2"},
)
adapter = AdminUpdateApiKeyAdapter(
key_id="sa-key-4",
key_data=CreateApiKeyRequest(
allowed_providers=[],
allowed_api_formats=[],
allowed_models=[],
),
)
result = await adapter.handle(_build_context(db))
assert result["id"] == "sa-key-4"
assert captured["allowed_providers"] == []
assert captured["allowed_api_formats"] == []
assert captured["allowed_models"] == []
@@ -1,88 +0,0 @@
from __future__ import annotations
from types import SimpleNamespace
from unittest.mock import AsyncMock, MagicMock
import pytest
from src.api.admin.pool.routes import AdminBatchActionKeysAdapter
from src.api.admin.pool.schemas import BatchActionRequest
def _build_context(db: MagicMock) -> SimpleNamespace:
return SimpleNamespace(
db=db,
user=SimpleNamespace(username="admin-1"),
add_audit_metadata=lambda **_: None,
)
def _mock_provider_lookup(db: MagicMock, provider_id: str) -> None:
db.query.return_value.filter.return_value.first.return_value = SimpleNamespace(id=provider_id)
@pytest.mark.asyncio
async def test_batch_delete_submits_async_task(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Delete action now submits an async batch-delete task instead of
executing SQL synchronously."""
db = MagicMock()
provider_id = "provider-1"
_mock_provider_lookup(db, provider_id)
mock_submit = AsyncMock(return_value="task-abc-123")
monkeypatch.setattr(
"src.services.provider_keys.batch_delete_task.submit_batch_delete",
mock_submit,
)
key_ids = [f"key-{idx}" for idx in range(1200)]
adapter = AdminBatchActionKeysAdapter(
provider_id=provider_id,
body=BatchActionRequest(
key_ids=key_ids,
action="delete",
),
)
result = await adapter.handle(_build_context(db))
# Async task returns affected=0 and a task_id
assert result.affected == 0
assert result.task_id == "task-abc-123"
assert "1200" in result.message
mock_submit.assert_awaited_once_with(provider_id, list(dict.fromkeys(key_ids)))
@pytest.mark.asyncio
async def test_batch_delete_deduplicates_key_ids(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Duplicate key IDs should be deduplicated before submission."""
db = MagicMock()
provider_id = "provider-2"
_mock_provider_lookup(db, provider_id)
mock_submit = AsyncMock(return_value="task-xyz-456")
monkeypatch.setattr(
"src.services.provider_keys.batch_delete_task.submit_batch_delete",
mock_submit,
)
adapter = AdminBatchActionKeysAdapter(
provider_id=provider_id,
body=BatchActionRequest(
key_ids=["key-1", "key-2", "key-1", "key-3", "key-2"],
action="delete",
),
)
result = await adapter.handle(_build_context(db))
assert result.affected == 0
assert result.task_id == "task-xyz-456"
# Deduplicated: 3 unique keys
submitted_ids = mock_submit.call_args[0][1]
assert len(submitted_ids) == 3
assert submitted_ids == ["key-1", "key-2", "key-3"]
-128
View File
@@ -1,128 +0,0 @@
from __future__ import annotations
from types import SimpleNamespace
from unittest.mock import AsyncMock, MagicMock
import pytest
from src.api.admin.providers.routes import (
AdminDeleteProviderAdapter,
AdminProviderDeleteTaskStatusAdapter,
)
@pytest.mark.asyncio
async def test_delete_provider_adapter_submits_async_task_and_deactivates_provider(
monkeypatch: pytest.MonkeyPatch,
) -> None:
db = MagicMock()
provider = SimpleNamespace(id="provider-1", name="Provider 1", is_active=True)
db.query.return_value.filter.return_value.first.return_value = provider
submit_task_mock = AsyncMock(return_value="task-1")
invalidate_models_mock = AsyncMock()
invalidate_resolve_mock = AsyncMock()
invalidate_provider_cache_mock = AsyncMock()
monkeypatch.setattr(
"src.api.admin.providers.routes.submit_provider_delete",
submit_task_mock,
)
monkeypatch.setattr(
"src.api.admin.providers.routes.invalidate_models_list_cache",
invalidate_models_mock,
)
monkeypatch.setattr(
"src.api.admin.providers.routes.ModelCacheService.invalidate_all_resolve_cache",
invalidate_resolve_mock,
)
monkeypatch.setattr(
"src.api.admin.providers.routes.ProviderCacheService.invalidate_provider_cache",
invalidate_provider_cache_mock,
)
audit_calls: list[dict[str, object]] = []
context = SimpleNamespace(
db=db,
request=SimpleNamespace(state=SimpleNamespace()),
add_audit_metadata=lambda **kwargs: audit_calls.append(kwargs),
)
adapter = AdminDeleteProviderAdapter(provider_id="provider-1")
result = await adapter.handle(context)
assert result == {
"task_id": "task-1",
"status": "pending",
"message": "删除任务已提交,提供商已进入后台删除队列",
}
submit_task_mock.assert_awaited_once_with("provider-1")
assert provider.is_active is False
db.commit.assert_called_once()
invalidate_models_mock.assert_awaited_once()
invalidate_resolve_mock.assert_awaited_once()
invalidate_provider_cache_mock.assert_awaited_once_with("provider-1")
assert audit_calls[0]["action"] == "delete_provider"
assert audit_calls[1]["task_id"] == "task-1"
assert audit_calls[1]["provider_deactivated"] is True
@pytest.mark.asyncio
async def test_delete_provider_adapter_reuses_task_without_extra_commit(
monkeypatch: pytest.MonkeyPatch,
) -> None:
db = MagicMock()
provider = SimpleNamespace(id="provider-1", name="Provider 1", is_active=False)
db.query.return_value.filter.return_value.first.return_value = provider
submit_task_mock = AsyncMock(return_value="task-1")
monkeypatch.setattr(
"src.api.admin.providers.routes.submit_provider_delete",
submit_task_mock,
)
context = SimpleNamespace(
db=db,
request=SimpleNamespace(state=SimpleNamespace()),
add_audit_metadata=lambda **kwargs: None,
)
adapter = AdminDeleteProviderAdapter(provider_id="provider-1")
result = await adapter.handle(context)
assert result["task_id"] == "task-1"
db.commit.assert_not_called()
@pytest.mark.asyncio
async def test_delete_provider_task_status_adapter_returns_task_payload(
monkeypatch: pytest.MonkeyPatch,
) -> None:
task = SimpleNamespace(
task_id="task-1",
provider_id="provider-1",
status="running",
stage="deleting_keys",
total_keys=100,
deleted_keys=25,
total_endpoints=8,
deleted_endpoints=2,
message="deleted key batch 1/2",
)
monkeypatch.setattr(
"src.api.admin.providers.routes.get_provider_delete_task",
AsyncMock(return_value=task),
)
context = SimpleNamespace(db=MagicMock(), request=SimpleNamespace(state=SimpleNamespace()))
adapter = AdminProviderDeleteTaskStatusAdapter(provider_id="provider-1", task_id="task-1")
result = await adapter.handle(context)
assert result.task_id == "task-1"
assert result.status == "running"
assert result.stage == "deleting_keys"
assert result.deleted_keys == 25
assert result.deleted_endpoints == 2
-339
View File
@@ -1,339 +0,0 @@
from __future__ import annotations
from datetime import datetime, timezone
from decimal import Decimal
from types import SimpleNamespace
from typing import Any
import pytest
from src.api.admin.usage.routes import (
AdminUsageDetailAdapter,
AdminUsageRecordsAdapter,
_resolve_replay_model_name,
)
class _FakeQuery:
def __init__(
self,
*,
scalar_result: int | None = None,
all_result: list[Any] | None = None,
first_result: Any = None,
) -> None:
self.scalar_result = scalar_result
self.all_result = all_result or []
self.first_result = first_result
self.options_args: tuple[Any, ...] = ()
def outerjoin(self, *args: Any, **kwargs: Any) -> _FakeQuery:
return self
def join(self, *args: Any, **kwargs: Any) -> _FakeQuery:
return self
def filter(self, *args: Any, **kwargs: Any) -> _FakeQuery:
return self
def options(self, *args: Any) -> _FakeQuery:
self.options_args = args
return self
def order_by(self, *args: Any, **kwargs: Any) -> _FakeQuery:
return self
def offset(self, *args: Any, **kwargs: Any) -> _FakeQuery:
return self
def limit(self, *args: Any, **kwargs: Any) -> _FakeQuery:
return self
def scalar(self) -> int | None:
return self.scalar_result
def all(self) -> list[Any]:
return self.all_result
def first(self) -> Any:
return self.first_result
class _FakeDb:
def __init__(self, queries: list[_FakeQuery]) -> None:
self._queries = queries
self.query_calls: list[tuple[Any, ...]] = []
def query(self, *args: Any) -> _FakeQuery:
self.query_calls.append(args)
return self._queries[len(self.query_calls) - 1]
@pytest.mark.asyncio
async def test_admin_usage_records_returns_model_version_without_request_metadata(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setattr("src.utils.cache_decorator.get_redis_client_sync", lambda: None)
usage = SimpleNamespace(
id="usage-1",
request_id=None,
user_id="user-1",
api_key_id=None,
provider_name="google",
provider_id=None,
provider_endpoint_id=None,
provider_api_key_id=None,
model="gemini-2.5-pro",
target_model=None,
input_tokens=120,
output_tokens=80,
cache_creation_input_tokens=0,
cache_read_input_tokens=0,
total_tokens=200,
total_cost_usd=Decimal("1.25"),
actual_total_cost_usd=Decimal("1.25"),
rate_multiplier=Decimal("1.0"),
response_time_ms=850,
first_byte_time_ms=230,
created_at=datetime(2026, 3, 9, 8, 30, tzinfo=timezone.utc),
is_stream=False,
status_code=200,
error_message=None,
status="completed",
api_format="gemini:chat",
endpoint_api_format=None,
has_format_conversion=False,
input_price_per_1m=Decimal("0.10"),
output_price_per_1m=Decimal("0.30"),
cache_creation_price_per_1m=None,
cache_read_price_per_1m=None,
)
user = SimpleNamespace(id="user-1", email="user@example.com", username="tester")
count_query = _FakeQuery(scalar_result=1)
data_query = _FakeQuery(
all_result=[
(usage, user, None, None, None, "gemini-2.5-pro-001"),
]
)
db = _FakeDb([count_query, data_query])
context = SimpleNamespace(
db=db,
user=SimpleNamespace(id="admin-1"),
add_audit_metadata=lambda **_: None,
)
adapter = AdminUsageRecordsAdapter(
time_range=None,
search=None,
user_id=None,
username=None,
model=None,
provider=None,
api_format=None,
status=None,
limit=100,
offset=0,
)
result = await adapter.handle(context)
assert len(db.query_calls) == 2
assert len(db.query_calls[1]) == 6
assert getattr(db.query_calls[1][-1], "name", None) == "model_version"
record = result["records"][0]
assert record["model_version"] == "gemini-2.5-pro-001"
assert "request_metadata" not in record
usage_load_only = data_query.options_args[0]
usage_paths = {str(option.path) for option in usage_load_only.context}
assert "ORM Path[Mapper[Usage(usage)] -> Usage.request_metadata]" not in usage_paths
@pytest.mark.asyncio
async def test_admin_usage_detail_defers_large_body_columns_when_bodies_excluded(
monkeypatch: pytest.MonkeyPatch,
) -> None:
async def _fake_get_tiered_pricing_info(
self: AdminUsageDetailAdapter,
db: Any,
usage_record: Any,
) -> None:
return None
monkeypatch.setattr(
AdminUsageDetailAdapter,
"_get_tiered_pricing_info",
_fake_get_tiered_pricing_info,
)
monkeypatch.setattr(
AdminUsageDetailAdapter,
"_extract_video_billing_info",
lambda self, usage_record: None,
)
class _UsageRecord:
id = "usage-1"
request_id = "req-1"
user_id = "user-1"
api_key_id = "key-1"
provider_name = "openai"
api_format = "openai:cli"
model = "gpt-5.4"
target_model = None
input_tokens = 10
output_tokens = 20
total_tokens = 30
cache_creation_input_tokens = 0
cache_read_input_tokens = 0
cache_creation_input_tokens_5m = 0
cache_creation_input_tokens_1h = 0
input_cost_usd = Decimal("0.001")
output_cost_usd = Decimal("0.002")
total_cost_usd = Decimal("0.003")
cache_creation_cost_usd = Decimal("0")
cache_read_cost_usd = Decimal("0")
request_cost_usd = Decimal("0")
input_price_per_1m = Decimal("0.1")
output_price_per_1m = Decimal("0.2")
cache_creation_price_per_1m = None
cache_read_price_per_1m = None
price_per_request = None
request_type = "chat"
is_stream = True
status_code = 200
error_message = None
status = "completed"
response_time_ms = 1200
first_byte_time_ms = 200
created_at = datetime(2026, 3, 12, 7, 0, tzinfo=timezone.utc)
request_headers = {"x-test": "1"}
provider_request_headers = {"authorization": "***"}
response_headers = {"content-type": "text/event-stream"}
client_response_headers = {"content-type": "text/event-stream"}
request_metadata = {"trace_id": "trace-1"}
def get_request_body(self) -> Any:
raise AssertionError("request body should not be loaded")
def get_provider_request_body(self) -> Any:
raise AssertionError("provider request body should not be loaded")
def get_response_body(self) -> Any:
raise AssertionError("response body should not be loaded")
def get_client_response_body(self) -> Any:
raise AssertionError("client response body should not be loaded")
class _ApiKeyRecord:
id = "key-1"
name = "Primary"
def get_display_key(self) -> str:
return "sk-test"
usage_query = _FakeQuery(
first_result=(_UsageRecord(), True, True, True, True),
)
user_query = _FakeQuery(
first_result=SimpleNamespace(id="user-1", username="tester", email="u@example.com"),
)
api_key_query = _FakeQuery(first_result=_ApiKeyRecord())
db = _FakeDb([usage_query, user_query, api_key_query])
context = SimpleNamespace(
db=db,
user=SimpleNamespace(id="admin-1"),
add_audit_metadata=lambda **_: None,
)
adapter = AdminUsageDetailAdapter(usage_id="usage-1", include_bodies=False)
result = await adapter.handle(context) # type: ignore[arg-type]
assert result["request_body"] is None
assert result["provider_request_body"] is None
assert result["response_body"] is None
assert result["client_response_body"] is None
assert result["has_request_body"] is True
assert result["has_provider_request_body"] is True
assert result["has_response_body"] is True
assert result["has_client_response_body"] is True
deferred_paths = {
str(context.path)
for option in usage_query.options_args
for context in getattr(option, "context", ())
}
assert "ORM Path[Mapper[Usage(usage)] -> Usage.request_body]" in deferred_paths
assert "ORM Path[Mapper[Usage(usage)] -> Usage.provider_request_body]" in deferred_paths
assert "ORM Path[Mapper[Usage(usage)] -> Usage.response_body]" in deferred_paths
assert "ORM Path[Mapper[Usage(usage)] -> Usage.client_response_body]" in deferred_paths
assert "ORM Path[Mapper[Usage(usage)] -> Usage.request_body_compressed]" in deferred_paths
assert (
"ORM Path[Mapper[Usage(usage)] -> Usage.provider_request_body_compressed]" in deferred_paths
)
assert "ORM Path[Mapper[Usage(usage)] -> Usage.response_body_compressed]" in deferred_paths
assert (
"ORM Path[Mapper[Usage(usage)] -> Usage.client_response_body_compressed]" in deferred_paths
)
@pytest.mark.asyncio
async def test_resolve_replay_model_name_falls_back_to_source_model_when_mapping_missing(
monkeypatch: pytest.MonkeyPatch,
) -> None:
class _FakeMapper:
def __init__(self, db: Any) -> None:
self.db = db
async def get_mapping(self, source_model: str, provider_id: str) -> None:
return None
monkeypatch.setattr("src.services.model.mapper.ModelMapperMiddleware", _FakeMapper)
resolved_model, mapping_source = await _resolve_replay_model_name(
SimpleNamespace(),
source_model="gpt-4o-mini",
target_provider=SimpleNamespace(id="provider-2", name="OpenAI Compatible"),
target_endpoint=SimpleNamespace(id="endpoint-2", api_format="openai:responses"),
target_api_key=None,
)
assert resolved_model == "gpt-4o-mini"
assert mapping_source == "none"
@pytest.mark.asyncio
async def test_resolve_replay_model_name_reruns_mapping_for_same_endpoint_replay(
monkeypatch: pytest.MonkeyPatch,
) -> None:
class _FakeModel:
def select_provider_model_name(
self, affinity_key: str | None = None, api_format: str | None = None
) -> str:
assert affinity_key == "key-2"
assert api_format == "openai:responses"
return "provider-model-for-key-2"
class _FakeMapper:
def __init__(self, db: Any) -> None:
self.db = db
async def get_mapping(self, source_model: str, provider_id: str) -> Any:
assert source_model == "gpt-4o-mini"
assert provider_id == "provider-2"
return SimpleNamespace(model=_FakeModel())
monkeypatch.setattr("src.services.model.mapper.ModelMapperMiddleware", _FakeMapper)
resolved_model, mapping_source = await _resolve_replay_model_name(
SimpleNamespace(),
source_model="gpt-4o-mini",
target_provider=SimpleNamespace(id="provider-2", name="OpenAI Compatible"),
target_endpoint=SimpleNamespace(id="endpoint-2", api_format="openai:responses"),
target_api_key=SimpleNamespace(id="key-2"),
)
assert resolved_model == "provider-model-for-key-2"
assert mapping_source == "model_mapping"
-197
View File
@@ -1,197 +0,0 @@
from __future__ import annotations
from datetime import datetime, timezone
from types import SimpleNamespace
from typing import Any
from unittest.mock import MagicMock
import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient
from src.api.admin.users.routes import AdminCreateUserAdapter
from src.api.admin.users.routes import router as admin_users_router
from src.database import get_db
def _build_admin_users_app(db: MagicMock, monkeypatch: pytest.MonkeyPatch) -> TestClient:
app = FastAPI()
app.include_router(admin_users_router)
app.dependency_overrides[get_db] = lambda: db
async def _fake_pipeline_run(
*, adapter: Any, http_request: object, db: MagicMock, mode: object
) -> Any:
_ = http_request, mode
context = SimpleNamespace(
db=db,
request=SimpleNamespace(state=SimpleNamespace()),
user=SimpleNamespace(id="admin-1"),
ensure_json_body=lambda: {},
add_audit_metadata=lambda **_: None,
)
return await adapter.handle(context)
monkeypatch.setattr("src.api.admin.users.routes.pipeline.run", _fake_pipeline_run)
return TestClient(app)
def test_list_users_uses_wallet_batch_lookup(monkeypatch: pytest.MonkeyPatch) -> None:
db = MagicMock()
client = _build_admin_users_app(db, monkeypatch)
now = datetime.now(timezone.utc)
users = [
SimpleNamespace(
id="user-1",
email="u1@example.com",
username="user1",
role=SimpleNamespace(value="user"),
allowed_providers=None,
allowed_api_formats=None,
allowed_models=None,
rate_limit=None,
is_active=True,
created_at=now,
updated_at=now,
last_login_at=None,
),
SimpleNamespace(
id="user-2",
email="u2@example.com",
username="user2",
role=SimpleNamespace(value="admin"),
allowed_providers=None,
allowed_api_formats=None,
allowed_models=None,
rate_limit=None,
is_active=True,
created_at=now,
updated_at=None,
last_login_at=None,
),
]
wallets_by_user_id = {
"user-1": SimpleNamespace(limit_mode="unlimited"),
}
batch_getter = MagicMock(return_value=wallets_by_user_id)
monkeypatch.setattr(
"src.api.admin.users.routes.UserService.list_users", lambda *_a, **_k: users
)
monkeypatch.setattr(
"src.api.admin.users.routes.WalletService.get_wallets_by_user_ids",
batch_getter,
)
monkeypatch.setattr(
"src.api.admin.users.routes.WalletService.get_wallet",
lambda *_a, **_k: (_ for _ in ()).throw(AssertionError("不应回退到逐个钱包查询")),
)
response = client.get("/api/admin/users")
assert response.status_code == 200
assert response.json()[0]["unlimited"] is True
assert response.json()[1]["unlimited"] is False
batch_getter.assert_called_once()
assert batch_getter.call_args.args[1] == ["user-1", "user-2"]
def test_list_user_sessions_route_returns_sessions(monkeypatch: pytest.MonkeyPatch) -> None:
db = MagicMock()
client = _build_admin_users_app(db, monkeypatch)
sessions = [
{
"id": "session-1",
"device_label": "Chrome / macOS",
"device_type": "desktop",
"created_at": datetime.now(timezone.utc).isoformat(),
"is_current": False,
}
]
monkeypatch.setattr(
"src.api.admin.users.routes._list_user_sessions_sync",
lambda user_id: (
sessions,
{"action": "list_user_sessions", "target_user_id": user_id},
),
)
response = client.get("/api/admin/users/user-1/sessions")
assert response.status_code == 200
assert response.json() == sessions
def test_revoke_user_session_route_returns_message(monkeypatch: pytest.MonkeyPatch) -> None:
db = MagicMock()
client = _build_admin_users_app(db, monkeypatch)
monkeypatch.setattr(
"src.api.admin.users.routes._revoke_user_session_sync",
lambda user_id, session_id, admin_user_id: (
{"message": f"{user_id}:{session_id}:revoked"},
{"action": "revoke_user_session", "target_user_id": user_id, "session_id": session_id},
),
)
response = client.delete("/api/admin/users/user-1/sessions/session-1")
assert response.status_code == 200
assert response.json() == {"message": "user-1:session-1:revoked"}
def test_revoke_all_user_sessions_route_returns_count(monkeypatch: pytest.MonkeyPatch) -> None:
db = MagicMock()
client = _build_admin_users_app(db, monkeypatch)
monkeypatch.setattr(
"src.api.admin.users.routes._revoke_all_user_sessions_sync",
lambda user_id, admin_user_id: (
{"message": "done", "revoked_count": 2},
{"action": "revoke_all_user_sessions", "target_user_id": user_id, "revoked_count": 2},
),
)
response = client.delete("/api/admin/users/user-1/sessions")
assert response.status_code == 200
assert response.json() == {"message": "done", "revoked_count": 2}
@pytest.mark.asyncio
async def test_create_user_adapter_preserves_empty_restriction_lists(
monkeypatch: pytest.MonkeyPatch,
) -> None:
db = MagicMock()
captured: dict[str, Any] = {}
def _fake_create_user_sync(request: Any, role: Any) -> tuple[dict[str, Any], dict[str, Any]]:
captured["request"] = request
captured["role"] = role
return {"id": "user-3"}, {"action": "create_user", "target_user_id": "user-3"}
monkeypatch.setattr("src.api.admin.users.routes._create_user_sync", _fake_create_user_sync)
context = SimpleNamespace(
db=db,
request=SimpleNamespace(state=SimpleNamespace()),
ensure_json_body=lambda: {
"username": "user3",
"password": "Abcd12",
"email": "u3@example.com",
"role": "user",
"initial_gift_usd": 10,
"allowed_providers": [],
"allowed_api_formats": [],
"allowed_models": [],
},
add_audit_metadata=lambda **_: None,
)
result = await AdminCreateUserAdapter().handle(context)
assert result == {"id": "user-3"}
assert captured["request"].allowed_providers == []
assert captured["request"].allowed_api_formats == []
assert captured["request"].allowed_models == []
-220
View File
@@ -1,220 +0,0 @@
from __future__ import annotations
from types import SimpleNamespace
from typing import Any
from unittest.mock import AsyncMock, MagicMock
import pytest
from fastapi import FastAPI, HTTPException
from fastapi.testclient import TestClient
from src.api.auth.routes import _logout_with_refresh_cookie_fallback
from src.api.auth.routes import router as auth_router
from src.config import config
from src.database import get_db
def _build_auth_app(
db: MagicMock,
monkeypatch: pytest.MonkeyPatch,
pipeline_result: Any = None,
pipeline_exception: Exception | None = None,
) -> TestClient:
app = FastAPI()
app.include_router(auth_router)
app.dependency_overrides[get_db] = lambda: db
async def _fake_pipeline_run(
*, adapter: Any, http_request: object, db: MagicMock, mode: object
) -> Any:
_ = adapter, http_request, db, mode
if pipeline_exception is not None:
raise pipeline_exception
return pipeline_result
monkeypatch.setattr("src.api.auth.routes.pipeline.run", _fake_pipeline_run)
return TestClient(app)
def test_login_route_sets_refresh_cookie_and_hides_token(monkeypatch: pytest.MonkeyPatch) -> None:
db = MagicMock()
client = _build_auth_app(
db,
monkeypatch,
pipeline_result={
"access_token": "access-1",
"token_type": "bearer",
"expires_in": 86400,
"user_id": "user-1",
"username": "tester",
"role": "user",
"_refresh_token": "refresh-1",
},
)
response = client.post("/api/auth/login", json={"email": "user@example.com", "password": "pw"})
assert response.status_code == 200
assert response.json() == {
"access_token": "access-1",
"token_type": "bearer",
"expires_in": 86400,
"user_id": "user-1",
"username": "tester",
"role": "user",
}
set_cookie = response.headers.get("set-cookie", "")
assert config.auth_refresh_cookie_name in set_cookie
assert "refresh-1" in set_cookie
assert "HttpOnly" in set_cookie
def test_refresh_route_sets_cookie_and_hides_token(monkeypatch: pytest.MonkeyPatch) -> None:
db = MagicMock()
client = _build_auth_app(
db,
monkeypatch,
pipeline_result={
"access_token": "access-2",
"token_type": "bearer",
"expires_in": 86400,
"_refresh_token": "refresh-2",
},
)
response = client.post("/api/auth/refresh")
assert response.status_code == 200
assert response.json() == {
"access_token": "access-2",
"token_type": "bearer",
"expires_in": 86400,
}
set_cookie = response.headers.get("set-cookie", "")
assert config.auth_refresh_cookie_name in set_cookie
assert "refresh-2" in set_cookie
assert "HttpOnly" in set_cookie
def test_refresh_route_clears_cookie_on_error(monkeypatch: pytest.MonkeyPatch) -> None:
db = MagicMock()
client = _build_auth_app(
db,
monkeypatch,
pipeline_exception=HTTPException(status_code=401, detail="登录会话已失效,请重新登录"),
)
response = client.post("/api/auth/refresh")
assert response.status_code == 401
assert response.json()["error"]["message"] == "登录会话已失效,请重新登录"
set_cookie = response.headers.get("set-cookie", "")
assert config.auth_refresh_cookie_name in set_cookie
assert "Max-Age=0" in set_cookie
def test_logout_route_clears_cookie_on_error(monkeypatch: pytest.MonkeyPatch) -> None:
db = MagicMock()
client = _build_auth_app(
db,
monkeypatch,
pipeline_exception=HTTPException(status_code=401, detail="缺少认证令牌"),
)
response = client.post("/api/auth/logout")
assert response.status_code == 401
assert response.json()["error"]["message"] == "缺少认证令牌"
set_cookie = response.headers.get("set-cookie", "")
assert config.auth_refresh_cookie_name in set_cookie
assert "Max-Age=0" in set_cookie
def test_logout_route_uses_refresh_cookie_fallback_on_auth_error(
monkeypatch: pytest.MonkeyPatch,
) -> None:
db = MagicMock()
client = _build_auth_app(
db,
monkeypatch,
pipeline_exception=HTTPException(status_code=401, detail="Token已过期"),
)
async def _fake_fallback(_request: object, _db: MagicMock) -> dict[str, Any]:
return {"message": "登出成功", "success": True}
monkeypatch.setattr(
"src.api.auth.routes._logout_with_refresh_cookie_fallback",
_fake_fallback,
)
response = client.post(
"/api/auth/logout",
cookies={config.auth_refresh_cookie_name: "refresh-1"},
)
assert response.status_code == 200
assert response.json() == {"message": "登出成功", "success": True}
set_cookie = response.headers.get("set-cookie", "")
assert config.auth_refresh_cookie_name in set_cookie
assert "Max-Age=0" in set_cookie
@pytest.mark.asyncio
async def test_logout_with_refresh_cookie_fallback_revokes_session(
monkeypatch: pytest.MonkeyPatch,
) -> None:
db = MagicMock()
user = SimpleNamespace(id="user-1", email="user@example.com")
session = SimpleNamespace(id="session-1", is_revoked=False, is_expired=False)
db.query.return_value.filter.return_value.first.return_value = user
request = SimpleNamespace(
cookies={config.auth_refresh_cookie_name: "refresh-1"},
headers={},
query_params={},
state=SimpleNamespace(),
)
monkeypatch.setattr(
"src.api.auth.routes.AuthService.verify_token",
AsyncMock(
return_value={
"user_id": "user-1",
"session_id": "session-1",
}
),
)
monkeypatch.setattr(
"src.api.auth.routes.SessionService.extract_client_device_id",
lambda _request: "device-1",
)
monkeypatch.setattr(
"src.api.auth.routes.SessionService.get_session_for_user",
lambda *_args, **_kwargs: session,
)
assert_session_device_matches = MagicMock()
revoke_session = MagicMock()
log_event = MagicMock()
monkeypatch.setattr(
"src.api.auth.routes.SessionService.assert_session_device_matches",
assert_session_device_matches,
)
monkeypatch.setattr(
"src.api.auth.routes.SessionService.revoke_session",
revoke_session,
)
monkeypatch.setattr("src.api.auth.routes.AuditService.log_event", log_event)
monkeypatch.setattr("src.api.auth.routes.get_client_ip", lambda _request: "127.0.0.1")
monkeypatch.setattr(
"src.api.auth.routes.get_user_agent",
lambda _request: "pytest-agent",
)
result = await _logout_with_refresh_cookie_fallback(request, db)
assert result == {"message": "登出成功", "success": True}
assert_session_device_matches.assert_called_once_with(session, "device-1")
revoke_session.assert_called_once()
log_event.assert_called_once()
db.commit.assert_called_once()
assert request.state.tx_committed_by_route is True
@@ -1,146 +0,0 @@
from __future__ import annotations
from datetime import datetime, timezone
from types import SimpleNamespace
from typing import Any, cast
from unittest.mock import MagicMock
import pytest
from src.api.admin.endpoints.health import AdminApiFormatHealthMonitorAdapter
from src.api.public.catalog import PublicApiFormatHealthMonitorAdapter
def _build_query(result: object) -> MagicMock:
query = MagicMock()
query.join.return_value = query
query.distinct.return_value = query
query.filter.return_value = query
query.group_by.return_value = query
query.order_by.return_value = query
query.limit.return_value = query
query.all.return_value = result
return query
def _expr_texts(query: MagicMock) -> list[str]:
return [str(arg) for arg in query.filter.call_args.args]
@pytest.mark.asyncio
async def test_admin_api_format_health_monitor_filters_inactive_sources(
monkeypatch: pytest.MonkeyPatch,
) -> None:
endpoint_query = _build_query([("openai:compact", "ep-active", "provider-active")])
key_query = _build_query([("provider-active", ["openai:compact"])])
status_query = _build_query([("openai:compact", "success", 3)])
rows_query = _build_query([])
db = MagicMock()
db.query.side_effect = [endpoint_query, key_query, status_query, rows_query]
monkeypatch.setattr(
"src.api.admin.endpoints.health.EndpointHealthService._generate_timeline_from_usage",
lambda **_: {
"timeline": ["healthy"] * 100,
"time_range_start": None,
"time_range_end": None,
},
)
context = SimpleNamespace(
db=db,
request=SimpleNamespace(state=SimpleNamespace()),
add_audit_metadata=lambda **kwargs: None,
)
adapter = AdminApiFormatHealthMonitorAdapter(lookback_hours=6, per_format_limit=20)
await adapter.handle(cast(Any, context))
status_filters = _expr_texts(status_query)
rows_filters = _expr_texts(rows_query)
assert any("provider_endpoints.is_active" in expr for expr in status_filters)
assert any("providers.is_active" in expr for expr in status_filters)
assert any("provider_endpoints.is_active" in expr for expr in rows_filters)
assert any("providers.is_active" in expr for expr in rows_filters)
@pytest.mark.asyncio
async def test_public_api_format_health_monitor_uses_real_counts_not_sampled_events(
monkeypatch: pytest.MonkeyPatch,
) -> None:
now = datetime.now(timezone.utc)
active_formats_query = _build_query([("openai:compact",)])
endpoint_rows_query = _build_query([("openai:compact", "ep-active")])
status_query = _build_query(
[
("openai:compact", "success", 7),
("openai:compact", "failed", 3),
("openai:compact", "skipped", 5),
]
)
rows_query = _build_query(
[
SimpleNamespace(
status="failed",
status_code=500,
latency_ms=321,
error_type="provider_error",
finished_at=now,
started_at=None,
created_at=now,
),
SimpleNamespace(
status="success",
status_code=200,
latency_ms=123,
error_type=None,
finished_at=now,
started_at=None,
created_at=now,
),
]
)
db = MagicMock()
db.query.side_effect = [
active_formats_query,
endpoint_rows_query,
status_query,
rows_query,
]
monkeypatch.setattr(
"src.api.public.catalog.EndpointHealthService._generate_timeline_from_usage",
lambda **_: {
"timeline": ["healthy"] * 100,
"time_range_start": None,
"time_range_end": now,
},
)
monkeypatch.setattr(
"src.core.api_format.get_local_path_for_endpoint",
lambda api_format: f"/{api_format}",
)
context = SimpleNamespace(
db=db,
request=SimpleNamespace(state=SimpleNamespace()),
)
adapter = PublicApiFormatHealthMonitorAdapter(lookback_hours=6, per_format_limit=20)
result = await adapter.handle(cast(Any, context))
monitor = result["formats"][0]
assert monitor["api_format"] == "openai:compact"
assert monitor["total_attempts"] == 15
assert monitor["success_count"] == 7
assert monitor["failed_count"] == 3
assert monitor["skipped_count"] == 5
assert monitor["success_rate"] == pytest.approx(0.7)
assert len(monitor["events"]) == 2
rows_filters = _expr_texts(rows_query)
assert any("provider_endpoints.is_active" in expr for expr in rows_filters)
assert any("providers.is_active" in expr for expr in rows_filters)
-785
View File
@@ -1,785 +0,0 @@
import asyncio
import base64
import json
import time
from types import SimpleNamespace
from typing import Any
from unittest.mock import AsyncMock, MagicMock, Mock
import pytest
from fastapi import BackgroundTasks, FastAPI
from fastapi.responses import JSONResponse, StreamingResponse
from fastapi.testclient import TestClient
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from src.api.internal.gateway import (
CONTROL_ACTION_HEADER,
CONTROL_ACTION_PROXY_PUBLIC,
CONTROL_EXECUTED_HEADER,
GatewayAuthContext,
GatewayExecutionDecisionResponse,
GatewayExecuteRequest,
GatewayExecutionPlanResponse,
GatewayResolveRequest,
GatewayStreamReportRequest,
GatewaySyncReportRequest,
_dispatch_gateway_sync_telemetry,
_build_gateway_sync_error_payload,
_build_gateway_sync_telemetry_writer,
_run_gateway_stream_report_background,
_run_gateway_sync_report_background,
_stream_executor_requires_python_rewrite,
_build_claude_chat_sync_decision,
_build_claude_chat_stream_decision,
_build_claude_cli_sync_decision,
_build_claude_cli_stream_decision,
_build_gemini_files_download_stream_decision,
_build_gemini_files_proxy_sync_decision,
_build_gemini_chat_sync_decision,
_build_gemini_chat_stream_decision,
_build_gemini_cli_sync_decision,
_build_gemini_cli_stream_decision,
_build_openai_chat_sync_decision,
_build_openai_chat_stream_decision,
_build_openai_cli_stream_decision,
_build_openai_video_content_stream_decision,
_extract_gateway_sync_error_message,
_record_gateway_direct_candidate_graph,
_resolve_gateway_sync_error_status_code,
_build_openai_chat_sync_plan,
_build_openai_cli_sync_decision,
_build_openai_cli_stream_plan,
_build_openai_cli_sync_plan,
_is_streaming_sync_payload,
_resolve_auth_context,
_resolve_gateway_sync_adapter,
classify_gateway_route,
router,
)
from src.database import get_db
from src.models.database import Base, RequestCandidate
from src.services.orchestration.candidate_resolver import CandidateResolver
from src.services.scheduling.schemas import PoolCandidate, ProviderCandidate
from src.services.request.execution_runtime_plan import (
ExecutionPlan,
ExecutionPlanBody,
PreparedExecutionPlan,
)
def _wait_until(predicate: Any, *, timeout: float = 1.0, interval: float = 0.01) -> None:
deadline = time.time() + timeout
while time.time() < deadline:
if predicate():
return
time.sleep(interval)
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_build_gateway_sync_telemetry_writer_uses_queue_writer_when_enabled(
monkeypatch: pytest.MonkeyPatch,
) -> None:
from src.config.settings import config
from src.services.usage.telemetry_writer import QueueTelemetryWriter
monkeypatch.setattr(config, "usage_queue_enabled", True)
monkeypatch.setattr(
"src.services.system.config.SystemConfigService.get_request_record_level",
lambda db: SimpleNamespace(value="headers"),
)
monkeypatch.setattr(
"src.services.system.config.SystemConfigService.get_sensitive_headers",
lambda db: ["authorization"],
)
monkeypatch.setattr(
"src.services.system.config.SystemConfigService.get_config",
lambda db, key, default=None: default,
)
writer = _build_gateway_sync_telemetry_writer(
db=object(),
request_id="req-queue-writer-123",
user_id="user-queue-writer-123",
api_key_id="api-key-queue-writer-123",
fallback_telemetry=object(),
)
assert isinstance(writer, QueueTelemetryWriter)
assert writer.request_id == "req-queue-writer-123"
assert writer.include_headers is True
@pytest.mark.asyncio
async def test_dispatch_gateway_sync_telemetry_backgrounds_queue_submission(
monkeypatch: pytest.MonkeyPatch,
) -> None:
from src.api.internal import gateway as gateway_module
started = asyncio.Event()
release = asyncio.Event()
scheduled: list[asyncio.Task[Any]] = []
class FakeWriter:
request_id = "req-background-dispatch-123"
def supports_background_submission(self) -> bool:
return True
async def record_success(self, **kwargs: Any) -> None:
started.set()
await release.wait()
def fake_safe_create_task(coro: Any) -> asyncio.Task[Any]:
task = asyncio.create_task(coro)
scheduled.append(task)
return task
monkeypatch.setattr(gateway_module, "safe_create_task", fake_safe_create_task)
await _dispatch_gateway_sync_telemetry(
telemetry_writer=FakeWriter(),
operation="record_success",
provider="openai",
)
await asyncio.wait_for(started.wait(), timeout=0.2)
assert scheduled
assert scheduled[0].done() is False
release.set()
await scheduled[0]
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_auth_context_route_returns_openai_bearer_auth_context(
monkeypatch: pytest.MonkeyPatch,
) -> None:
app = FastAPI()
app.include_router(router)
monkeypatch.setattr("src.api.internal.gateway.ensure_loopback", lambda request: None)
monkeypatch.setattr(
"src.api.internal.gateway.AuthService.authenticate_api_key_threadsafe",
AsyncMock(
return_value=SimpleNamespace(
user=SimpleNamespace(id="user-auth-route-123"),
api_key=SimpleNamespace(id="api-key-auth-route-123"),
balance_remaining=3.5,
access_allowed=True,
)
),
)
client = _make_legacy_test_client(app)
response = client.post(
"/api/internal/gateway/auth-context",
json={
"trace_id": "trace-auth-route-123",
"query_string": "",
"headers": {"authorization": "Bearer sk-test"},
"auth_endpoint_signature": "openai:chat",
},
)
assert response.status_code == 200
assert response.json() == {
"auth_context": {
"user_id": "user-auth-route-123",
"api_key_id": "api-key-auth-route-123",
"balance_remaining": 3.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 = _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",
"x-aether-control-execute-fallback": "true",
},
"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_resolves_auth_context_when_missing(
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:
return JSONResponse(status_code=200, content={"request_id": context.request_id})
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._resolve_auth_context_signature",
AsyncMock(
return_value={
"user_id": "user-123",
"api_key_id": "key-123",
"balance_remaining": 9.5,
"access_allowed": True,
}
),
)
monkeypatch.setattr(
"src.api.internal.gateway._load_gateway_auth_models",
lambda db, auth_context: (
SimpleNamespace(id=auth_context.user_id),
SimpleNamespace(id=auth_context.api_key_id),
),
)
monkeypatch.setattr("src.api.internal.gateway.get_pipeline", lambda: fake_pipeline)
client = _make_legacy_test_client(app)
response = client.post(
"/api/internal/gateway/execute-sync",
json={
"trace_id": "trace-sync-derive-auth",
"method": "POST",
"path": "/v1/chat/completions",
"headers": {
"user-agent": "pytest",
"authorization": "Bearer client-key",
"x-aether-control-execute-fallback": "true",
},
"body_json": {"model": "gpt-5", "messages": []},
},
)
assert response.status_code == 200
assert response.headers[CONTROL_EXECUTED_HEADER] == "true"
assert response.json() == {"request_id": "trace-sync-derive-auth"}
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 = _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"},
"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 == 410
assert response.json() == {
"detail": "legacy internal gateway route removed; use public proxy"
}
monkeypatch.undo()
def test_execute_sync_route_requires_explicit_chat_cli_opt_in(
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)
resolve_sync_adapter = Mock(return_value=(Mock(), {}))
monkeypatch.setattr("src.api.internal.gateway._resolve_gateway_sync_adapter", resolve_sync_adapter)
client = _make_legacy_test_client(app)
response = client.post(
"/api/internal/gateway/execute-sync",
json={
"trace_id": "trace-sync-no-opt-in",
"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 == 410
assert response.json() == {
"detail": "legacy internal gateway route removed; use public proxy"
}
resolve_sync_adapter.assert_not_called()
def test_execute_sync_route_requires_legacy_internal_gateway_header(
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)
resolve_sync_adapter = Mock(return_value=(Mock(), {}))
monkeypatch.setattr("src.api.internal.gateway._resolve_gateway_sync_adapter", resolve_sync_adapter)
client = TestClient(app, base_url="http://127.0.0.1")
response = client.post(
"/api/internal/gateway/execute-sync",
json={
"trace_id": "trace-sync-no-legacy-header",
"method": "POST",
"path": "/v1/chat/completions",
"headers": {
"user-agent": "pytest",
"x-aether-control-execute-fallback": "true",
},
"body_json": {"model": "gpt-5", "messages": []},
"auth_context": {
"user_id": "user-123",
"api_key_id": "key-123",
"access_allowed": True,
},
},
)
assert response.status_code == 410
assert response.json() == {
"detail": "legacy internal gateway route removed; use public proxy"
}
resolve_sync_adapter.assert_not_called()
def test_decision_sync_route_requires_legacy_internal_gateway_header(
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)
build_decision = AsyncMock(return_value=MagicMock())
monkeypatch.setattr("src.api.internal.gateway._build_openai_chat_sync_decision", build_decision)
client = TestClient(app, base_url="http://127.0.0.1")
response = client.post(
"/api/internal/gateway/decision-sync",
json={
"trace_id": "trace-decision-no-legacy-header",
"method": "POST",
"path": "/v1/chat/completions",
"headers": {
"content-type": "application/json",
"authorization": "Bearer client-key",
},
"body_json": {"model": "gpt-5", "messages": []},
"auth_context": {
"user_id": "user-123",
"api_key_id": "key-123",
"access_allowed": True,
},
},
)
assert response.status_code == 410
assert response.json() == {
"detail": "legacy internal gateway route removed; use public proxy"
}
build_decision.assert_not_awaited()
def test_plan_stream_route_requires_legacy_internal_gateway_header(
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)
build_plan = AsyncMock(return_value=MagicMock())
monkeypatch.setattr("src.api.internal.gateway._build_openai_chat_stream_plan", build_plan)
client = TestClient(app, base_url="http://127.0.0.1")
response = client.post(
"/api/internal/gateway/plan-stream",
json={
"trace_id": "trace-plan-no-legacy-header",
"method": "POST",
"path": "/v1/chat/completions",
"headers": {
"content-type": "application/json",
"authorization": "Bearer client-key",
},
"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 == 410
assert response.json() == {
"detail": "legacy internal gateway route removed; use public proxy"
}
build_plan.assert_not_awaited()
def test_finalize_sync_route_requires_legacy_internal_gateway_header(
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)
finalize_mock = AsyncMock(return_value=JSONResponse(content={"ok": True}))
monkeypatch.setattr("src.api.internal.gateway._finalize_gateway_chat_sync", finalize_mock)
client = TestClient(app, base_url="http://127.0.0.1")
response = client.post(
"/api/internal/gateway/finalize-sync",
json={
"trace_id": "trace-finalize-no-legacy-header",
"report_kind": "openai_chat_sync_finalize",
"report_context": {
"user_id": "user-123",
"api_key_id": "key-123",
"client_api_format": "openai:chat",
},
"status_code": 200,
"headers": {"content-type": "application/json"},
"body_json": {"id": "upstream-123"},
},
)
assert response.status_code == 410
assert response.json() == {
"detail": "legacy internal gateway route removed; use public proxy"
}
finalize_mock.assert_not_awaited()
def test_report_sync_route_requires_legacy_internal_gateway_header(
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)
record_mock = AsyncMock(return_value=None)
monkeypatch.setattr(
"src.api.internal.gateway._record_gateway_openai_chat_sync_success",
record_mock,
)
client = TestClient(app, base_url="http://127.0.0.1")
response = client.post(
"/api/internal/gateway/report-sync",
json={
"trace_id": "trace-report-no-legacy-header",
"report_kind": "openai_chat_sync_success",
"report_context": {"user_id": "user-123", "api_key_id": "key-123"},
"status_code": 200,
"headers": {"content-type": "application/json"},
"body_json": {
"id": "chatcmpl-123",
"object": "chat.completion",
"choices": [],
},
},
)
assert response.status_code == 410
assert response.json() == {
"detail": "legacy internal gateway route removed; use public proxy"
}
assert record_mock.await_count == 0
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 = _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",
"x-aether-control-execute-fallback": "true",
},
"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"
@@ -1,120 +0,0 @@
from __future__ import annotations
from datetime import datetime, timezone
from types import SimpleNamespace
from unittest.mock import AsyncMock, MagicMock
import pytest
from src.api.monitoring.user import UserRateLimitStatusAdapter
def _build_query_with_keys(keys: list[object]) -> MagicMock:
query = MagicMock()
query.filter.return_value.order_by.return_value.all.return_value = keys
return query
@pytest.mark.asyncio
async def test_rate_limit_status_adapter_reports_user_and_key_layers(
monkeypatch: pytest.MonkeyPatch,
) -> None:
now = datetime(2026, 3, 13, 12, 0, tzinfo=timezone.utc)
db = MagicMock()
user = SimpleNamespace(id="user-1", rate_limit=None)
key = SimpleNamespace(id="key-1", name="Primary", is_standalone=False, rate_limit=10)
standalone = SimpleNamespace(
id="skey-1", name="Standalone", is_standalone=True, rate_limit=None
)
db.query.return_value = _build_query_with_keys([key, standalone])
limiter = MagicMock()
limiter.bucket_seconds = 60
limiter.get_reset_at.return_value = now
limiter.get_user_rpm_key.return_value = "rpm:user:user-1:bucket"
limiter.get_standalone_rpm_key.return_value = "rpm:ukey:skey-1:bucket"
limiter.get_key_rpm_key.side_effect = lambda key_id: f"rpm:key:{key_id}:bucket"
async def _get_scope_count(scope_key: str) -> int:
counts = {
"rpm:user:user-1:bucket": 55,
"rpm:key:key-1:bucket": 7,
"rpm:ukey:skey-1:bucket": 12,
}
return counts[scope_key]
limiter.get_scope_count = AsyncMock(side_effect=_get_scope_count)
monkeypatch.setattr(
"src.api.monitoring.user.get_user_rpm_limiter",
AsyncMock(return_value=limiter),
)
monkeypatch.setattr(
"src.api.monitoring.user.SystemConfigService.get_config",
lambda *_a, **_k: 60,
)
context = SimpleNamespace(db=db, user=user)
result = await UserRateLimitStatusAdapter().handle(context)
assert result["user_id"] == "user-1"
assert result["api_keys"][0] == {
"api_key_name": "Primary",
"limit": 10,
"remaining": 3,
"scope": "key",
"reset_time": now.isoformat(),
"window": "60s",
"user_limit": 60,
"user_remaining": 5,
"key_limit": 10,
"key_remaining": 3,
}
assert result["api_keys"][1] == {
"api_key_name": "Standalone",
"limit": 60,
"remaining": 48,
"scope": "user",
"reset_time": now.isoformat(),
"window": "60s",
"user_limit": 60,
"user_remaining": 48,
"key_limit": None,
"key_remaining": None,
}
@pytest.mark.asyncio
async def test_rate_limit_status_adapter_reports_unlimited_key_without_counts(
monkeypatch: pytest.MonkeyPatch,
) -> None:
db = MagicMock()
user = SimpleNamespace(id="user-1", rate_limit=0)
key = SimpleNamespace(id="key-1", name="Unlimited", is_standalone=False, rate_limit=0)
db.query.return_value = _build_query_with_keys([key])
limiter = MagicMock()
limiter.bucket_seconds = 60
limiter.get_reset_at.return_value = datetime.now(timezone.utc)
limiter.get_user_rpm_key.return_value = "rpm:user:user-1:bucket"
limiter.get_key_rpm_key.return_value = "rpm:key:key-1:bucket"
limiter.get_scope_count = AsyncMock()
monkeypatch.setattr(
"src.api.monitoring.user.get_user_rpm_limiter",
AsyncMock(return_value=limiter),
)
monkeypatch.setattr(
"src.api.monitoring.user.SystemConfigService.get_config",
lambda *_a, **_k: 60,
)
context = SimpleNamespace(db=db, user=user)
result = await UserRateLimitStatusAdapter().handle(context)
assert result["api_keys"][0]["limit"] is None
assert result["api_keys"][0]["remaining"] is None
assert result["api_keys"][0]["scope"] is None
limiter.get_scope_count.assert_not_awaited()
-232
View File
@@ -1,232 +0,0 @@
from __future__ import annotations
from decimal import Decimal
from types import SimpleNamespace
from unittest.mock import MagicMock
import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient
from src.api.admin.payments.routes import AdminPaymentOrderCreditAdapter
from src.api.payment.routes import router as payment_router
from src.config import config
from src.database import get_db
from src.models.database import PaymentOrder
from src.services.payment.gateway import get_payment_gateway
CALLBACK_SECRET = "test-callback-secret"
def _build_payment_app(db: MagicMock) -> TestClient:
app = FastAPI()
app.include_router(payment_router)
app.dependency_overrides[get_db] = lambda: db
return TestClient(app)
def _sign_payload(payload: dict[str, object]) -> str:
gateway = get_payment_gateway("alipay")
signature = gateway.build_callback_signature(payload=payload, callback_secret=CALLBACK_SECRET)
assert signature is not None
return signature
def test_specific_wechat_callback_route_is_not_shadowed(
monkeypatch: pytest.MonkeyPatch,
) -> None:
db = MagicMock()
client = _build_payment_app(db)
monkeypatch.setattr(config, "payment_callback_secret", CALLBACK_SECRET)
captured_kwargs: dict[str, object] = {}
def _fake_handle_callback(*args: object, **kwargs: object) -> dict[str, object]:
captured_kwargs.update(kwargs)
return {
"ok": True,
"credited": True,
"duplicate": False,
"payment_method_seen": kwargs["payment_method"],
}
monkeypatch.setattr("src.api.payment.routes.PaymentService.handle_callback", _fake_handle_callback)
callback_payload = {"callback_key": "cb-wechat", "amount_usd": 1.0}
response = client.post(
"/api/payment/callback/wechat",
json=callback_payload,
headers={
"x-payment-callback-token": CALLBACK_SECRET,
"x-payment-callback-signature": _sign_payload(callback_payload),
},
)
assert response.status_code == 200
payload = response.json()
assert payload["payment_method"] == "wechat"
assert payload["payment_method_seen"] == "wechat"
assert payload["request_path"] == "/api/payment/callback/wechat"
assert captured_kwargs["callback_signature"] == _sign_payload(callback_payload)
assert captured_kwargs["callback_secret"] == CALLBACK_SECRET
assert "signature_valid" not in captured_kwargs
db.commit.assert_called_once()
def test_generic_payment_callback_route_still_handles_custom_methods(
monkeypatch: pytest.MonkeyPatch,
) -> None:
db = MagicMock()
client = _build_payment_app(db)
monkeypatch.setattr(config, "payment_callback_secret", CALLBACK_SECRET)
captured_kwargs: dict[str, object] = {}
def _fake_handle_callback(*args: object, **kwargs: object) -> dict[str, object]:
captured_kwargs.update(kwargs)
return {
"ok": True,
"credited": False,
"duplicate": False,
"payment_method_seen": kwargs["payment_method"],
}
monkeypatch.setattr("src.api.payment.routes.PaymentService.handle_callback", _fake_handle_callback)
callback_payload = {"callback_key": "cb-generic", "amount_usd": 1.0}
response = client.post(
"/api/payment/callback/mockpay",
json=callback_payload,
headers={
"x-payment-callback-token": CALLBACK_SECRET,
"x-payment-callback-signature": _sign_payload(callback_payload),
},
)
assert response.status_code == 200
payload = response.json()
assert payload["payment_method"] == "mockpay"
assert payload["payment_method_seen"] == "mockpay"
assert captured_kwargs["callback_signature"] == _sign_payload(callback_payload)
assert captured_kwargs["callback_secret"] == CALLBACK_SECRET
assert "signature_valid" not in captured_kwargs
def test_callback_requires_shared_token(monkeypatch: pytest.MonkeyPatch) -> None:
db = MagicMock()
client = _build_payment_app(db)
monkeypatch.setattr(config, "payment_callback_secret", CALLBACK_SECRET)
response = client.post(
"/api/payment/callback/alipay",
json={"callback_key": "cb-missing-token", "amount_usd": 1.0},
)
assert response.status_code == 401
db.commit.assert_not_called()
def test_callback_rejects_invalid_shared_token(monkeypatch: pytest.MonkeyPatch) -> None:
db = MagicMock()
client = _build_payment_app(db)
monkeypatch.setattr(config, "payment_callback_secret", CALLBACK_SECRET)
callback_payload = {"callback_key": "cb-invalid-token", "amount_usd": 1.0}
response = client.post(
"/api/payment/callback/alipay",
json=callback_payload,
headers={
"x-payment-callback-token": "wrong-token",
"x-payment-callback-signature": _sign_payload(callback_payload),
},
)
assert response.status_code == 401
db.commit.assert_not_called()
def test_callback_rejects_missing_signature(monkeypatch: pytest.MonkeyPatch) -> None:
db = MagicMock()
client = _build_payment_app(db)
monkeypatch.setattr(config, "payment_callback_secret", CALLBACK_SECRET)
response = client.post(
"/api/payment/callback/alipay",
json={"callback_key": "cb-missing-signature", "amount_usd": 1.0},
headers={"x-payment-callback-token": CALLBACK_SECRET},
)
assert response.status_code == 401
db.commit.assert_not_called()
def test_callback_disabled_when_secret_not_configured(monkeypatch: pytest.MonkeyPatch) -> None:
db = MagicMock()
client = _build_payment_app(db)
monkeypatch.setattr(config, "payment_callback_secret", "")
response = client.post(
"/api/payment/callback/alipay",
json={"callback_key": "cb-secret-missing", "amount_usd": 1.0},
)
assert response.status_code == 503
db.commit.assert_not_called()
@pytest.mark.asyncio
async def test_admin_payment_credit_adapter_marks_manual_credit(
monkeypatch: pytest.MonkeyPatch,
) -> None:
db = MagicMock()
order = PaymentOrder(
id="po-credit",
order_no="order-credit",
wallet_id="w1",
user_id="u1",
amount_usd=Decimal("8.00000000"),
refunded_amount_usd=Decimal("0"),
refundable_amount_usd=Decimal("8.00000000"),
payment_method="alipay",
status="pending",
gateway_response={"existing": True},
)
adapter = AdminPaymentOrderCreditAdapter(order_id=order.id)
context = SimpleNamespace(
db=db,
raw_body=b"{}",
ensure_json_body=lambda: {
"pay_amount": 58.0,
"pay_currency": "CNY",
"exchange_rate": 7.25,
},
user=SimpleNamespace(id="admin-1"),
)
monkeypatch.setattr(
"src.api.admin.payments.routes.PaymentService.get_order",
lambda _db, order_id: order if order_id == "po-credit" else None,
)
captured: dict[str, object] = {}
def _fake_credit_order(_db: MagicMock, **kwargs: object) -> tuple[PaymentOrder, bool]:
captured.update(kwargs)
return order, True
monkeypatch.setattr(
"src.api.admin.payments.routes.PaymentService.credit_order",
_fake_credit_order,
)
result = await adapter.handle(context)
assert result["credited"] is True
assert result["order"]["id"] == "po-credit"
gateway_response = captured["gateway_response"]
assert isinstance(gateway_response, dict)
assert gateway_response["existing"] is True
assert gateway_response["manual_credit"] is True
assert gateway_response["credited_by"] == "admin-1"
db.commit.assert_called_once()
File diff suppressed because it is too large Load Diff
@@ -1,230 +0,0 @@
from __future__ import annotations
from itertools import count
from types import SimpleNamespace
from unittest.mock import MagicMock
import httpx
import pytest
from src.api.admin import provider_oauth as oauthmod
@pytest.mark.asyncio
async def test_standard_batch_import_releases_db_connection_before_network_await(
monkeypatch: pytest.MonkeyPatch,
) -> None:
release_calls: list[str] = []
monkeypatch.setattr(
oauthmod,
"_require_oauth_template",
lambda _provider_type: SimpleNamespace(
oauth=SimpleNamespace(
token_url="https://example.com/oauth/token",
client_id="client-id",
client_secret=None,
scopes=[],
)
),
)
monkeypatch.setattr(
oauthmod,
"_parse_standard_oauth_import_entries",
lambda _raw: [{"refresh_token": "r" * 120}],
)
monkeypatch.setattr(oauthmod, "_get_provider_api_formats", lambda _provider: [])
monkeypatch.setattr(
oauthmod,
"_release_batch_import_db_connection_before_await",
lambda _db: release_calls.append("release"),
)
async def _fake_post_oauth_token(**_kwargs: object) -> httpx.Response:
raise RuntimeError("upstream unavailable")
monkeypatch.setattr(oauthmod, "post_oauth_token", _fake_post_oauth_token)
db = MagicMock()
result = await oauthmod._batch_import_standard_oauth_internal(
provider_id="provider-1",
provider_type="codex",
provider=SimpleNamespace(endpoints=[]), # type: ignore[arg-type]
raw_credentials="ignored",
db=db,
concurrency=1,
)
assert result.total == 1
assert result.success == 0
assert result.failed == 1
assert release_calls
db.commit.assert_not_called()
@pytest.mark.asyncio
async def test_standard_batch_import_commits_successes_in_chunks(
monkeypatch: pytest.MonkeyPatch,
) -> None:
key_ids = count(1)
created_auth_configs: list[dict[str, object]] = []
monkeypatch.setattr(
oauthmod,
"_PROVIDER_OAUTH_BATCH_IMPORT_COMMIT_BATCH_SIZE",
2,
)
monkeypatch.setattr(
oauthmod,
"_require_oauth_template",
lambda _provider_type: SimpleNamespace(
oauth=SimpleNamespace(
token_url="https://example.com/oauth/token",
client_id="client-id",
client_secret=None,
scopes=[],
)
),
)
monkeypatch.setattr(
oauthmod,
"_parse_standard_oauth_import_entries",
lambda _raw: [{"refresh_token": f"r-{idx}" + ("x" * 120)} for idx in range(3)],
)
monkeypatch.setattr(
oauthmod, "_get_provider_api_formats", lambda _provider: ["responses"]
)
monkeypatch.setattr(
oauthmod,
"_release_batch_import_db_connection_before_await",
lambda _db: None,
)
async def _fake_post_oauth_token(**_kwargs: object) -> httpx.Response:
idx = next(key_ids)
return httpx.Response(
200,
json={
"access_token": f"access-{idx}",
"refresh_token": f"refresh-{idx}",
"expires_in": 3600,
},
request=httpx.Request("POST", "https://example.com/oauth/token"),
)
async def _fake_enrich_auth_config(**kwargs: object) -> dict[str, object]:
auth_config = dict(kwargs["auth_config"]) # type: ignore[call-overload]
auth_config["email"] = f"user-{next(key_ids)}@example.com"
auth_config["account_name"] = "Workspace Alpha"
return auth_config
created_ids = count(1)
monkeypatch.setattr(oauthmod, "post_oauth_token", _fake_post_oauth_token)
monkeypatch.setattr(oauthmod, "enrich_auth_config", _fake_enrich_auth_config)
monkeypatch.setattr(
oauthmod, "_check_duplicate_oauth_account", lambda *_args, **_kwargs: None
)
def _fake_create_oauth_key(*_args: object, **kwargs: object) -> SimpleNamespace:
created_auth_configs.append(dict(kwargs["auth_config"]))
return SimpleNamespace(id=f"key-{next(created_ids)}")
monkeypatch.setattr(
oauthmod,
"_create_oauth_key",
_fake_create_oauth_key,
)
db = MagicMock()
result = await oauthmod._batch_import_standard_oauth_internal(
provider_id="provider-1",
provider_type="example",
provider=SimpleNamespace(endpoints=[]), # type: ignore[arg-type]
raw_credentials="ignored",
db=db,
concurrency=1,
)
assert result.total == 3
assert result.success == 3
assert result.failed == 0
assert db.commit.call_count == 2
assert created_auth_configs[0]["account_name"] == "Workspace Alpha"
@pytest.mark.asyncio
async def test_kiro_batch_import_releases_db_connection_before_refresh(
monkeypatch: pytest.MonkeyPatch,
) -> None:
release_calls: list[str] = []
class FakeKiroAuthConfig:
def __init__(self, data: dict[str, object]) -> None:
self._data = dict(data)
self.provider_type = str(data.get("provider_type") or "")
self.email = (
data.get("email") if isinstance(data.get("email"), str) else None
)
self.auth_method = (
data.get("auth_method")
if isinstance(data.get("auth_method"), str)
else "social"
)
self.refresh_token = str(data.get("refresh_token") or "")
@staticmethod
def validate_required_fields(
_cred: dict[str, object],
) -> tuple[bool, str | None]:
return True, None
@classmethod
def from_dict(cls, data: dict[str, object]) -> "FakeKiroAuthConfig":
return cls(data)
def to_dict(self) -> dict[str, object]:
return dict(self._data)
monkeypatch.setattr(
oauthmod,
"_parse_kiro_import_input",
lambda _raw: [{"refresh_token": "r" * 120, "auth_method": "social"}],
)
monkeypatch.setattr(oauthmod, "_get_provider_api_formats", lambda _provider: [])
monkeypatch.setattr(
oauthmod,
"_release_batch_import_db_connection_before_await",
lambda _db: release_calls.append("release"),
)
monkeypatch.setattr(
"src.services.provider.adapters.kiro.models.credentials.KiroAuthConfig",
FakeKiroAuthConfig,
)
async def _fake_refresh_access_token(
*_args: object, **_kwargs: object
) -> tuple[str, object]:
raise RuntimeError("refresh token reused")
monkeypatch.setattr(
"src.services.provider.adapters.kiro.token_manager.refresh_access_token",
_fake_refresh_access_token,
)
db = MagicMock()
result = await oauthmod._batch_import_kiro_internal(
provider_id="provider-1",
provider=SimpleNamespace(endpoints=[]), # type: ignore[arg-type]
raw_credentials="ignored",
db=db,
concurrency=1,
)
assert result.total == 1
assert result.success == 0
assert result.failed == 1
assert release_calls
db.commit.assert_not_called()
@@ -1,38 +0,0 @@
from __future__ import annotations
import importlib
import sys
def test_python_host_import_does_not_load_public_compat_modules() -> None:
targets = [
"src.main",
"src.api.public",
"src.api.public.compat",
"src.api.public.support",
"src.api.public.models",
"src.api.public.capabilities",
"src.api.public.modules",
"src.api.public.openai",
"src.api.public.claude",
"src.api.public.gemini",
"src.api.public.videos",
"src.api.public.gemini_files",
"src.api.public.system_catalog",
]
for name in targets:
sys.modules.pop(name, None)
importlib.import_module("src.main")
assert "src.api.public.support" in sys.modules
assert "src.api.public.compat" not in sys.modules
assert "src.api.public.models" not in sys.modules
assert "src.api.public.capabilities" not in sys.modules
assert "src.api.public.modules" not in sys.modules
assert "src.api.public.openai" not in sys.modules
assert "src.api.public.claude" not in sys.modules
assert "src.api.public.gemini" not in sys.modules
assert "src.api.public.videos" not in sys.modules
assert "src.api.public.gemini_files" not in sys.modules
assert "src.api.public.system_catalog" not in sys.modules
-921
View File
@@ -1,921 +0,0 @@
from __future__ import annotations
from starlette.routing import Match
from src.api.announcements import router as python_announcement_router
from src.api.admin import python_admin_router
from src.api.auth import router as python_auth_router
from src.api.dashboard import router as python_dashboard_router
import src.api.internal as internal_module
from src.api.internal.gateway import router as legacy_gateway_bridge_router
from src.api.monitoring import router as python_monitoring_router
from src.api.payment import router as python_payment_router
from src.api.public import frontdoor_compat_router, router as python_public_router
from src.api.user_me import router as python_user_me_router
from src.api.wallet import router as python_wallet_router
import src.main as main_module
LEGACY_GATEWAY_BRIDGE_PATH_PREFIX = "/api/internal/gateway"
RUST_OWNED_ADMIN_PATHS = {
"/api/admin/modules/status",
"/api/admin/modules/status/{module_name}",
"/api/admin/modules/status/{module_name}/enabled",
"/api/admin/system/version",
"/api/admin/system/check-update",
"/api/admin/system/aws-regions",
"/api/admin/system/stats",
"/api/admin/system/settings",
"/api/admin/system/config/export",
"/api/admin/system/users/export",
"/api/admin/system/config/import",
"/api/admin/system/users/import",
"/api/admin/system/smtp/test",
"/api/admin/system/cleanup",
"/api/admin/system/purge/config",
"/api/admin/system/purge/users",
"/api/admin/system/purge/usage",
"/api/admin/system/purge/audit-logs",
"/api/admin/system/purge/request-bodies",
"/api/admin/system/purge/stats",
"/api/admin/system/configs",
"/api/admin/system/configs/{key}",
"/api/admin/system/api-formats",
"/api/admin/system/email/templates",
"/api/admin/system/email/templates/{template_type}",
"/api/admin/providers/",
"/api/admin/providers/summary",
"/api/admin/providers/{provider_id}",
"/api/admin/providers/{provider_id}/summary",
"/api/admin/providers/{provider_id}/health-monitor",
"/api/admin/providers/{provider_id}/mapping-preview",
"/api/admin/providers/{provider_id}/delete-task/{task_id}",
"/api/admin/providers/{provider_id}/pool-status",
"/api/admin/providers/{provider_id}/pool/clear-cooldown/{key_id}",
"/api/admin/providers/{provider_id}/pool/reset-cost/{key_id}",
"/api/admin/providers/{provider_id}/models",
"/api/admin/providers/{provider_id}/models/{model_id}",
"/api/admin/providers/{provider_id}/models/batch",
"/api/admin/providers/{provider_id}/available-source-models",
"/api/admin/providers/{provider_id}/assign-global-models",
"/api/admin/providers/{provider_id}/import-from-upstream",
"/api/admin/endpoints/providers/{provider_id}/endpoints",
"/api/admin/endpoints/defaults/{api_format}/body-rules",
"/api/admin/endpoints/{endpoint_id}",
"/api/admin/endpoints/keys/{key_id}",
"/api/admin/endpoints/keys/grouped-by-format",
"/api/admin/endpoints/keys/{key_id}/reveal",
"/api/admin/endpoints/keys/{key_id}/export",
"/api/admin/endpoints/keys/batch-delete",
"/api/admin/endpoints/keys/{key_id}/clear-oauth-invalid",
"/api/admin/endpoints/providers/{provider_id}/keys",
"/api/admin/endpoints/providers/{provider_id}/refresh-quota",
"/api/admin/endpoints/rpm/key/{key_id}",
"/api/admin/endpoints/health/summary",
"/api/admin/endpoints/health/status",
"/api/admin/endpoints/health/api-formats",
"/api/admin/endpoints/health/key/{key_id}",
"/api/admin/endpoints/health/keys/{key_id}",
"/api/admin/endpoints/health/keys",
"/api/admin/provider-oauth/supported-types",
"/api/admin/provider-oauth/keys/{key_id}/start",
"/api/admin/provider-oauth/keys/{key_id}/complete",
"/api/admin/provider-oauth/keys/{key_id}/refresh",
"/api/admin/provider-oauth/providers/{provider_id}/start",
"/api/admin/provider-oauth/providers/{provider_id}/complete",
"/api/admin/provider-oauth/providers/{provider_id}/import-refresh-token",
"/api/admin/provider-oauth/providers/{provider_id}/device-authorize",
"/api/admin/provider-oauth/providers/{provider_id}/device-poll",
"/api/admin/provider-oauth/providers/{provider_id}/batch-import",
"/api/admin/provider-oauth/providers/{provider_id}/batch-import/tasks",
"/api/admin/provider-oauth/providers/{provider_id}/batch-import/tasks/{task_id}",
"/api/admin/adaptive/keys",
"/api/admin/adaptive/keys/{key_id}/mode",
"/api/admin/adaptive/keys/{key_id}/stats",
"/api/admin/adaptive/keys/{key_id}/learning",
"/api/admin/adaptive/keys/{key_id}/limit",
"/api/admin/adaptive/summary",
"/api/admin/provider-ops/architectures",
"/api/admin/provider-ops/architectures/{architecture_id}",
"/api/admin/provider-ops/providers/{provider_id}/status",
"/api/admin/provider-ops/providers/{provider_id}/config",
"/api/admin/provider-ops/providers/{provider_id}/connect",
"/api/admin/provider-ops/providers/{provider_id}/disconnect",
"/api/admin/provider-ops/providers/{provider_id}/verify",
"/api/admin/provider-ops/providers/{provider_id}/actions/{action_type}",
"/api/admin/provider-ops/providers/{provider_id}/balance",
"/api/admin/provider-ops/providers/{provider_id}/checkin",
"/api/admin/provider-ops/batch/balance",
"/api/admin/billing/presets",
"/api/admin/billing/presets/apply",
"/api/admin/billing/rules",
"/api/admin/billing/rules/{rule_id}",
"/api/admin/billing/collectors",
"/api/admin/billing/collectors/{collector_id}",
"/api/admin/provider-strategy/providers/{provider_id}/billing",
"/api/admin/provider-strategy/providers/{provider_id}/stats",
"/api/admin/provider-strategy/strategies",
"/api/admin/provider-strategy/providers/{provider_id}/quota",
"/api/admin/provider-query/models",
"/api/admin/provider-query/test-model",
"/api/admin/provider-query/test-model-failover",
"/api/admin/payments/orders",
"/api/admin/payments/orders/{order_id}",
"/api/admin/payments/orders/{order_id}/expire",
"/api/admin/payments/orders/{order_id}/credit",
"/api/admin/payments/orders/{order_id}/fail",
"/api/admin/payments/callbacks",
"/api/admin/security/ip/blacklist",
"/api/admin/security/ip/blacklist/{ip_address}",
"/api/admin/security/ip/blacklist/stats",
"/api/admin/security/ip/whitelist",
"/api/admin/security/ip/whitelist/{ip_address}",
"/api/admin/security/ip/whitelist",
"/api/admin/stats/providers/quota-usage",
"/api/admin/stats/comparison",
"/api/admin/stats/errors/distribution",
"/api/admin/stats/performance/percentiles",
"/api/admin/stats/cost/forecast",
"/api/admin/stats/cost/savings",
"/api/admin/stats/leaderboard/api-keys",
"/api/admin/stats/leaderboard/models",
"/api/admin/stats/leaderboard/users",
"/api/admin/stats/time-series",
"/api/admin/monitoring/audit-logs",
"/api/admin/monitoring/system-status",
"/api/admin/monitoring/suspicious-activities",
"/api/admin/monitoring/user-behavior/{user_id}",
"/api/admin/monitoring/resilience-status",
"/api/admin/monitoring/resilience/circuit-history",
"/api/admin/monitoring/resilience/error-stats",
"/api/admin/monitoring/trace/{request_id}",
"/api/admin/monitoring/trace/stats/provider/{provider_id}",
"/api/admin/monitoring/cache/stats",
"/api/admin/monitoring/cache/affinity/{user_identifier}",
"/api/admin/monitoring/cache/affinities",
"/api/admin/monitoring/cache/users/{user_identifier}",
"/api/admin/monitoring/cache/affinity/{affinity_key}/{endpoint_id}/{model_id}/{api_format}",
"/api/admin/monitoring/cache",
"/api/admin/monitoring/cache/providers/{provider_id}",
"/api/admin/monitoring/cache/config",
"/api/admin/monitoring/cache/metrics",
"/api/admin/monitoring/cache/model-mapping/stats",
"/api/admin/monitoring/cache/model-mapping",
"/api/admin/monitoring/cache/model-mapping/{model_name}",
"/api/admin/monitoring/cache/model-mapping/provider/{provider_id}/{global_model_id}",
"/api/admin/monitoring/cache/redis-keys",
"/api/admin/monitoring/cache/redis-keys/{category}",
"/api/admin/usage/aggregation/stats",
"/api/admin/usage/stats",
"/api/admin/usage/heatmap",
"/api/admin/usage/records",
"/api/admin/usage/active",
"/api/admin/usage/cache-affinity/hit-analysis",
"/api/admin/usage/cache-affinity/interval-timeline",
"/api/admin/usage/cache-affinity/ttl-analysis",
"/api/admin/usage/{usage_id}/curl",
"/api/admin/usage/{usage_id}",
"/api/admin/usage/{usage_id}/replay",
"/api/admin/video-tasks",
"/api/admin/video-tasks/stats",
"/api/admin/video-tasks/{task_id}",
"/api/admin/video-tasks/{task_id}/cancel",
"/api/admin/video-tasks/{task_id}/video",
"/api/admin/wallets",
"/api/admin/wallets/ledger",
"/api/admin/wallets/refund-requests",
"/api/admin/wallets/{wallet_id}",
"/api/admin/wallets/{wallet_id}/transactions",
"/api/admin/wallets/{wallet_id}/refunds",
"/api/admin/wallets/{wallet_id}/adjust",
"/api/admin/wallets/{wallet_id}/recharge",
"/api/admin/wallets/{wallet_id}/refunds/{refund_id}/process",
"/api/admin/wallets/{wallet_id}/refunds/{refund_id}/complete",
"/api/admin/wallets/{wallet_id}/refunds/{refund_id}/fail",
"/api/admin/api-keys",
"/api/admin/api-keys/{key_id}",
"/api/admin/users",
"/api/admin/users/{user_id}",
"/api/admin/users/{user_id}/sessions",
"/api/admin/users/{user_id}/sessions/{session_id}",
"/api/admin/users/{user_id}/api-keys",
"/api/admin/users/{user_id}/api-keys/{key_id}",
"/api/admin/users/{user_id}/api-keys/{key_id}/lock",
"/api/admin/users/{user_id}/api-keys/{key_id}/full-key",
"/api/admin/pool/overview",
"/api/admin/pool/scheduling-presets",
"/api/admin/pool/{provider_id}/keys",
"/api/admin/pool/{provider_id}/keys/batch-delete-task/{task_id}",
"/api/admin/pool/{provider_id}/keys/batch-action",
"/api/admin/pool/{provider_id}/keys/batch-import",
"/api/admin/pool/{provider_id}/keys/cleanup-banned",
"/api/admin/pool/{provider_id}/keys/resolve-selection",
"/api/admin/proxy-nodes",
"/api/admin/proxy-nodes/register",
"/api/admin/proxy-nodes/heartbeat",
"/api/admin/proxy-nodes/unregister",
"/api/admin/proxy-nodes/manual",
"/api/admin/proxy-nodes/upgrade",
"/api/admin/proxy-nodes/test-url",
"/api/admin/proxy-nodes/{node_id}",
"/api/admin/proxy-nodes/{node_id}/test",
"/api/admin/proxy-nodes/{node_id}/config",
"/api/admin/proxy-nodes/{node_id}/events",
"/api/admin/models/catalog",
"/api/admin/models/external",
"/api/admin/models/external/cache",
"/api/admin/models/global",
"/api/admin/models/global/{global_model_id}",
"/api/admin/models/global/batch-delete",
"/api/admin/models/global/{global_model_id}/assign-to-providers",
"/api/admin/models/global/{global_model_id}/providers",
"/api/admin/models/global/{global_model_id}/routing",
}
def _route_paths(router: object) -> set[str]:
return {route.path for route in getattr(router, "routes", [])}
def _app_matches_http_route(path: str, method: str) -> bool:
scope = {
"type": "http",
"path": path,
"method": method,
"root_path": "",
}
return any(route.matches(scope)[0] is Match.FULL for route in main_module.app.routes)
def _router_matches_http_route(router: object, path: str, method: str) -> bool:
scope = {
"type": "http",
"path": path,
"method": method,
"root_path": "",
}
return any(route.matches(scope)[0] is Match.FULL for route in getattr(router, "routes", []))
def test_python_host_app_exposes_loopback_internal_gateway_bridge_routes() -> None:
host_route_paths = _route_paths(main_module.app)
legacy_bridge_paths = _route_paths(legacy_gateway_bridge_router)
assert "/api/internal/gateway/resolve" in legacy_bridge_paths
assert "/api/internal/gateway/auth-context" in legacy_bridge_paths
assert "/api/internal/gateway/decision-sync" in legacy_bridge_paths
assert "/api/internal/gateway/decision-stream" in legacy_bridge_paths
assert hasattr(internal_module, "legacy_gateway_bridge_router") is False
assert hasattr(internal_module, "LEGACY_GATEWAY_BRIDGE_PATH_PREFIXES") is False
assert not legacy_bridge_paths.issubset(host_route_paths)
assert not any(path.startswith(LEGACY_GATEWAY_BRIDGE_PATH_PREFIX) for path in host_route_paths)
def test_python_host_app_exposes_no_api_routes() -> None:
host_route_paths = _route_paths(main_module.app)
api_route_paths = sorted(path for path in host_route_paths if path.startswith("/api/"))
assert api_route_paths == []
def test_python_internal_router_excludes_gateway_bridge() -> None:
internal_route_paths = _route_paths(internal_module.python_internal_router)
assert not any(
path.startswith(LEGACY_GATEWAY_BRIDGE_PATH_PREFIX)
for path in internal_route_paths
)
assert not any(path.startswith("/api/internal/hub") for path in internal_route_paths)
def test_python_host_app_surface_keeps_shell_routes_and_rejects_removed_edges() -> None:
host_route_paths = _route_paths(main_module.app)
compat_route_paths = _route_paths(frontdoor_compat_router)
python_public_route_paths = _route_paths(python_public_router)
python_auth_route_paths = _route_paths(python_auth_router)
python_dashboard_route_paths = _route_paths(python_dashboard_router)
python_monitoring_route_paths = _route_paths(python_monitoring_router)
python_payment_route_paths = _route_paths(python_payment_router)
python_user_me_route_paths = _route_paths(python_user_me_router)
python_wallet_route_paths = _route_paths(python_wallet_router)
python_admin_route_paths = _route_paths(python_admin_router)
python_announcement_route_paths = _route_paths(python_announcement_router)
assert "/v1/chat/completions" in compat_route_paths
assert "/v1/messages" in compat_route_paths
assert "/v1beta/models/{model}:generateContent" in compat_route_paths
assert "/v1/videos" in compat_route_paths
assert "/v1beta/files" in compat_route_paths
assert "/v1/chat/completions" not in python_public_route_paths
assert "/v1/messages" not in python_public_route_paths
assert "/v1beta/models/{model}:generateContent" not in python_public_route_paths
assert "/v1/videos" not in python_public_route_paths
assert "/v1beta/files" not in python_public_route_paths
assert "/v1/models" not in python_public_route_paths
assert "/api/public/site-info" not in python_public_route_paths
assert "/api/public/providers" not in python_public_route_paths
assert "/api/public/models" not in python_public_route_paths
assert "/api/public/search/models" not in python_public_route_paths
assert "/api/public/stats" not in python_public_route_paths
assert "/api/public/global-models" not in python_public_route_paths
assert "/api/public/health/api-formats" not in python_public_route_paths
assert "/api/modules/auth-status" not in python_public_route_paths
assert "/api/capabilities" not in python_public_route_paths
assert "/api/capabilities/user-configurable" not in python_public_route_paths
assert "/api/capabilities/model/{model_name}" not in python_public_route_paths
assert "/api/auth/registration-settings" not in python_auth_route_paths
assert "/api/auth/settings" not in python_auth_route_paths
assert "/api/auth/login" not in python_auth_route_paths
assert "/api/auth/refresh" not in python_auth_route_paths
assert "/api/auth/register" not in python_auth_route_paths
assert "/api/auth/me" not in python_auth_route_paths
assert "/api/auth/logout" not in python_auth_route_paths
assert "/api/auth/send-verification-code" not in python_auth_route_paths
assert "/api/auth/verify-email" not in python_auth_route_paths
assert "/api/auth/verification-status" not in python_auth_route_paths
assert "/api/dashboard/stats" not in python_dashboard_route_paths
assert "/api/dashboard/recent-requests" not in python_dashboard_route_paths
assert "/api/dashboard/provider-status" not in python_dashboard_route_paths
assert "/api/dashboard/daily-stats" not in python_dashboard_route_paths
assert "/api/monitoring/my-audit-logs" not in python_monitoring_route_paths
assert "/api/monitoring/rate-limit-status" not in python_monitoring_route_paths
assert "/api/payment/callback/{payment_method}" not in python_payment_route_paths
assert "/api/wallet/balance" not in python_wallet_route_paths
assert "/api/wallet/transactions" not in python_wallet_route_paths
assert "/api/wallet/flow" not in python_wallet_route_paths
assert "/api/wallet/today-cost" not in python_wallet_route_paths
assert "/api/wallet/recharge" not in python_wallet_route_paths
assert "/api/wallet/recharge/{order_id}" not in python_wallet_route_paths
assert "/api/wallet/refunds" not in python_wallet_route_paths
assert "/api/wallet/refunds/{refund_id}" not in python_wallet_route_paths
assert "/api/users/me" not in python_user_me_route_paths
assert "/api/users/me/password" not in python_user_me_route_paths
assert "/api/users/me/sessions" not in python_user_me_route_paths
assert "/api/users/me/sessions/others" not in python_user_me_route_paths
assert "/api/users/me/sessions/{session_id}" not in python_user_me_route_paths
assert "/api/users/me/api-keys" not in python_user_me_route_paths
assert "/api/users/me/api-keys/{key_id}" not in python_user_me_route_paths
assert "/api/users/me/usage" not in python_user_me_route_paths
assert "/api/users/me/usage/active" not in python_user_me_route_paths
assert "/api/users/me/usage/interval-timeline" not in python_user_me_route_paths
assert "/api/users/me/usage/heatmap" not in python_user_me_route_paths
assert "/api/users/me/providers" not in python_user_me_route_paths
assert "/api/users/me/available-models" not in python_user_me_route_paths
assert "/api/users/me/endpoint-status" not in python_user_me_route_paths
assert "/api/users/me/api-keys/{api_key_id}/providers" not in python_user_me_route_paths
assert "/api/users/me/api-keys/{api_key_id}/capabilities" not in python_user_me_route_paths
assert "/api/users/me/preferences" not in python_user_me_route_paths
assert "/api/users/me/model-capabilities" not in python_user_me_route_paths
assert not (RUST_OWNED_ADMIN_PATHS & python_admin_route_paths)
assert not _router_matches_http_route(
python_announcement_router, "/api/announcements", "GET"
)
assert not _router_matches_http_route(
python_announcement_router, "/api/announcements/active", "GET"
)
assert not _router_matches_http_route(
python_announcement_router, "/api/announcements", "POST"
)
assert not _router_matches_http_route(
python_announcement_router, "/api/announcements/announcement-1", "PUT"
)
assert not _router_matches_http_route(
python_announcement_router, "/api/announcements/announcement-1", "DELETE"
)
assert not _router_matches_http_route(
python_announcement_router,
"/api/announcements/users/me/unread-count",
"GET",
)
assert not _router_matches_http_route(
python_announcement_router,
"/api/announcements/announcement-1/read-status",
"PATCH",
)
assert "/v1/chat/completions" not in host_route_paths
assert "/v1/messages" not in host_route_paths
assert "/v1beta/models/{model}:generateContent" not in host_route_paths
assert "/v1/videos" not in host_route_paths
assert "/v1beta/files" not in host_route_paths
assert "/v1/models" not in host_route_paths
assert "/v1/providers" not in host_route_paths
assert "/v1/test-connection" not in host_route_paths
assert "/api/public/site-info" not in host_route_paths
assert "/api/public/providers" not in host_route_paths
assert "/api/public/models" not in host_route_paths
assert "/api/public/search/models" not in host_route_paths
assert "/api/public/stats" not in host_route_paths
assert not _app_matches_http_route("/api/announcements", "GET")
assert not _app_matches_http_route("/api/announcements/active", "GET")
assert not _app_matches_http_route("/api/announcements", "POST")
assert not _app_matches_http_route("/api/announcements/announcement-1", "PUT")
assert not _app_matches_http_route("/api/announcements/announcement-1", "DELETE")
assert "/api/public/global-models" not in host_route_paths
assert "/api/public/health/api-formats" not in host_route_paths
assert "/api/modules/auth-status" not in host_route_paths
assert "/api/capabilities" not in host_route_paths
assert "/api/capabilities/user-configurable" not in host_route_paths
assert "/api/capabilities/model/{model_name}" not in host_route_paths
assert "/api/auth/registration-settings" not in host_route_paths
assert "/api/auth/settings" not in host_route_paths
assert "/api/auth/login" not in host_route_paths
assert "/api/auth/refresh" not in host_route_paths
assert "/api/auth/register" not in host_route_paths
assert "/api/auth/me" not in host_route_paths
assert "/api/auth/logout" not in host_route_paths
assert "/api/auth/send-verification-code" not in host_route_paths
assert "/api/auth/verify-email" not in host_route_paths
assert "/api/auth/verification-status" not in host_route_paths
assert "/api/dashboard/stats" not in host_route_paths
assert "/api/dashboard/recent-requests" not in host_route_paths
assert "/api/dashboard/provider-status" not in host_route_paths
assert "/api/dashboard/daily-stats" not in host_route_paths
assert "/api/monitoring/my-audit-logs" not in host_route_paths
assert "/api/monitoring/rate-limit-status" not in host_route_paths
assert "/api/payment/callback/{payment_method}" not in host_route_paths
assert "/api/wallet/balance" not in host_route_paths
assert "/api/wallet/transactions" not in host_route_paths
assert "/api/wallet/flow" not in host_route_paths
assert "/api/wallet/today-cost" not in host_route_paths
assert "/api/wallet/recharge" not in host_route_paths
assert "/api/wallet/recharge/{order_id}" not in host_route_paths
assert "/api/wallet/refunds" not in host_route_paths
assert "/api/wallet/refunds/{refund_id}" not in host_route_paths
assert not (RUST_OWNED_ADMIN_PATHS & host_route_paths)
assert "/health" not in host_route_paths
assert "/v1/health" not in host_route_paths
assert "/" not in host_route_paths
assert "/test-connection" not in host_route_paths
assert "/readyz" not in host_route_paths
assert _app_matches_http_route("/v1/chat/completions", "POST") is False
assert _app_matches_http_route("/v1/messages", "POST") is False
assert _app_matches_http_route("/v1beta/models/gemini-2.5-pro:generateContent", "POST") is False
assert _app_matches_http_route("/v1/videos", "POST") is False
assert _app_matches_http_route("/v1beta/files", "GET") is False
assert _app_matches_http_route("/v1/models", "GET") is False
assert _app_matches_http_route("/v1/providers", "GET") is False
assert _app_matches_http_route("/v1/test-connection", "GET") is False
assert _app_matches_http_route("/api/public/site-info", "GET") is False
assert _app_matches_http_route("/api/public/providers", "GET") is False
assert _app_matches_http_route("/api/public/models", "GET") is False
assert _app_matches_http_route("/api/public/search/models", "GET") is False
assert _app_matches_http_route("/api/public/stats", "GET") is False
assert _app_matches_http_route("/api/public/global-models", "GET") is False
assert _app_matches_http_route("/api/public/health/api-formats", "GET") is False
assert _app_matches_http_route("/api/modules/auth-status", "GET") is False
assert _app_matches_http_route("/api/capabilities", "GET") is False
assert _app_matches_http_route("/api/capabilities/user-configurable", "GET") is False
assert _app_matches_http_route("/api/capabilities/model/gpt-5", "GET") is False
assert _app_matches_http_route("/api/auth/registration-settings", "GET") is False
assert _app_matches_http_route("/api/auth/login", "POST") is False
assert _app_matches_http_route("/api/auth/refresh", "POST") is False
assert _app_matches_http_route("/api/auth/register", "POST") is False
assert _app_matches_http_route("/api/auth/me", "GET") is False
assert _app_matches_http_route("/api/auth/logout", "POST") is False
assert _app_matches_http_route("/api/auth/send-verification-code", "POST") is False
assert _app_matches_http_route("/api/auth/verify-email", "POST") is False
assert _app_matches_http_route("/api/auth/verification-status", "POST") is False
assert _app_matches_http_route("/api/admin/stats/comparison", "GET") is False
assert _app_matches_http_route("/api/admin/stats/errors/distribution", "GET") is False
assert _app_matches_http_route("/api/admin/stats/performance/percentiles", "GET") is False
assert _app_matches_http_route("/api/admin/stats/cost/forecast", "GET") is False
assert _app_matches_http_route("/api/admin/stats/time-series", "GET") is False
assert _app_matches_http_route("/api/admin/stats/leaderboard/users", "GET") is False
assert _app_matches_http_route("/api/auth/settings", "GET") is False
assert _app_matches_http_route("/api/dashboard/stats", "GET") is False
assert _app_matches_http_route("/api/dashboard/recent-requests", "GET") is False
assert _app_matches_http_route("/api/dashboard/provider-status", "GET") is False
assert _app_matches_http_route("/api/dashboard/daily-stats", "GET") is False
assert _app_matches_http_route("/api/monitoring/my-audit-logs", "GET") is False
assert _app_matches_http_route("/api/monitoring/rate-limit-status", "GET") is False
assert _app_matches_http_route("/api/payment/callback/alipay", "POST") is False
assert _app_matches_http_route("/api/wallet/balance", "GET") is False
assert _app_matches_http_route("/api/wallet/transactions", "GET") is False
assert _app_matches_http_route("/api/wallet/flow", "GET") is False
assert _app_matches_http_route("/api/wallet/today-cost", "GET") is False
assert _app_matches_http_route("/api/wallet/recharge", "GET") is False
assert _app_matches_http_route("/api/wallet/recharge", "POST") is False
assert _app_matches_http_route("/api/wallet/recharge/order-1", "GET") is False
assert _app_matches_http_route("/api/wallet/refunds", "GET") is False
assert _app_matches_http_route("/api/wallet/refunds", "POST") is False
assert _app_matches_http_route("/api/wallet/refunds/refund-1", "GET") is False
assert _app_matches_http_route("/api/users/me", "GET") is False
assert _app_matches_http_route("/api/users/me", "PUT") is False
assert _app_matches_http_route("/api/users/me/password", "PATCH") is False
assert _app_matches_http_route("/api/users/me/sessions", "GET") is False
assert _app_matches_http_route("/api/users/me/sessions/others", "DELETE") is False
assert _app_matches_http_route("/api/users/me/sessions/session-1", "PATCH") is False
assert _app_matches_http_route("/api/users/me/sessions/session-1", "DELETE") is False
assert _app_matches_http_route("/api/users/me/api-keys", "GET") is False
assert _app_matches_http_route("/api/users/me/api-keys", "POST") is False
assert _app_matches_http_route("/api/users/me/api-keys/key-1", "GET") is False
assert _app_matches_http_route("/api/users/me/api-keys/key-1", "DELETE") is False
assert _app_matches_http_route("/api/users/me/api-keys/key-1", "PUT") is False
assert _app_matches_http_route("/api/users/me/api-keys/key-1", "PATCH") is False
assert _app_matches_http_route("/api/users/me/usage", "GET") is False
assert _app_matches_http_route("/api/users/me/usage/active", "GET") is False
assert _app_matches_http_route("/api/users/me/usage/interval-timeline", "GET") is False
assert _app_matches_http_route("/api/users/me/usage/heatmap", "GET") is False
assert _app_matches_http_route("/api/users/me/providers", "GET") is False
assert _app_matches_http_route("/api/users/me/available-models", "GET") is False
assert _app_matches_http_route("/api/users/me/endpoint-status", "GET") is False
assert _app_matches_http_route("/api/users/me/api-keys/key-1/providers", "PUT") is False
assert _app_matches_http_route("/api/users/me/api-keys/key-1/capabilities", "PUT") is False
assert _app_matches_http_route("/api/users/me/preferences", "GET") is False
assert _app_matches_http_route("/api/users/me/preferences", "PUT") is False
assert _app_matches_http_route("/api/users/me/model-capabilities", "GET") is False
assert _app_matches_http_route("/api/users/me/model-capabilities", "PUT") is False
assert _app_matches_http_route("/api/announcements/announcement-1", "GET") is False
assert _app_matches_http_route("/api/announcements/users/me/unread-count", "GET") is False
assert (
_app_matches_http_route("/api/announcements/announcement-1/read-status", "PATCH")
is False
)
assert _app_matches_http_route("/api/admin/system/version", "GET") is False
assert _app_matches_http_route("/api/admin/system/settings", "GET") is False
assert _app_matches_http_route("/api/admin/system/config/export", "GET") is False
assert _app_matches_http_route("/api/admin/system/configs", "GET") is False
assert _app_matches_http_route("/api/admin/system/configs/smtp_password", "GET") is False
assert _app_matches_http_route("/api/admin/modules/status", "GET") is False
assert _app_matches_http_route("/api/admin/modules/status/auth", "GET") is False
assert _app_matches_http_route("/api/admin/modules/status/auth/enabled", "PUT") is False
assert (
_app_matches_http_route(
"/api/admin/provider-ops/providers/provider-openai/connect",
"POST",
)
is False
)
assert (
_app_matches_http_route(
"/api/admin/provider-ops/providers/provider-openai/disconnect",
"POST",
)
is False
)
assert (
_app_matches_http_route(
"/api/admin/provider-ops/providers/provider-openai/actions/query_balance",
"POST",
)
is False
)
assert _app_matches_http_route("/api/admin/provider-strategy/strategies", "GET") is False
assert (
_app_matches_http_route(
"/api/admin/provider-strategy/providers/provider-openai/billing",
"PUT",
)
is False
)
assert (
_app_matches_http_route(
"/api/admin/provider-strategy/providers/provider-openai/stats",
"GET",
)
is False
)
assert (
_app_matches_http_route(
"/api/admin/provider-strategy/providers/provider-openai/quota",
"DELETE",
)
is False
)
assert _app_matches_http_route("/api/admin/stats/providers/quota-usage", "GET") is False
assert (
_app_matches_http_route(
"/api/admin/provider-ops/providers/provider-openai/verify",
"POST",
)
is False
)
assert (
_app_matches_http_route(
"/api/admin/provider-ops/providers/provider-openai/balance",
"GET",
)
is False
)
assert (
_app_matches_http_route(
"/api/admin/provider-ops/providers/provider-openai/balance",
"POST",
)
is False
)
assert (
_app_matches_http_route(
"/api/admin/provider-ops/providers/provider-openai/checkin",
"POST",
)
is False
)
assert _app_matches_http_route("/api/admin/provider-ops/batch/balance", "POST") is False
assert _app_matches_http_route("/api/admin/billing/presets", "GET") is False
assert _app_matches_http_route("/api/admin/billing/presets/apply", "POST") is False
assert _app_matches_http_route("/api/admin/billing/rules", "GET") is False
assert _app_matches_http_route("/api/admin/billing/rules/rule-1", "GET") is False
assert _app_matches_http_route("/api/admin/billing/rules", "POST") is False
assert _app_matches_http_route("/api/admin/billing/rules/rule-1", "PUT") is False
assert _app_matches_http_route("/api/admin/billing/collectors", "GET") is False
assert _app_matches_http_route("/api/admin/billing/collectors/collector-1", "GET") is False
assert _app_matches_http_route("/api/admin/billing/collectors", "POST") is False
assert _app_matches_http_route("/api/admin/billing/collectors/collector-1", "PUT") is False
assert _app_matches_http_route("/api/admin/provider-query/models", "POST") is False
assert _app_matches_http_route("/api/admin/provider-query/test-model", "POST") is False
assert (
_app_matches_http_route("/api/admin/provider-query/test-model-failover", "POST")
is False
)
assert _app_matches_http_route("/api/admin/payments/orders", "GET") is False
assert _app_matches_http_route("/api/admin/payments/orders/order-1", "GET") is False
assert _app_matches_http_route("/api/admin/payments/orders/order-1/expire", "POST") is False
assert _app_matches_http_route("/api/admin/payments/orders/order-1/credit", "POST") is False
assert _app_matches_http_route("/api/admin/payments/orders/order-1/fail", "POST") is False
assert _app_matches_http_route("/api/admin/payments/callbacks", "GET") is False
assert _app_matches_http_route("/api/admin/usage/aggregation/stats", "GET") is False
assert _app_matches_http_route("/api/admin/usage/stats", "GET") is False
assert _app_matches_http_route("/api/admin/usage/heatmap", "GET") is False
assert _app_matches_http_route("/api/admin/usage/records", "GET") is False
assert _app_matches_http_route("/api/admin/usage/active", "GET") is False
assert _app_matches_http_route("/api/admin/usage/usage-1/curl", "GET") is False
assert _app_matches_http_route("/api/admin/usage/usage-1", "GET") is False
assert _app_matches_http_route("/api/admin/usage/usage-1/replay", "POST") is False
assert _app_matches_http_route("/api/admin/proxy-nodes", "GET") is False
assert _app_matches_http_route("/api/admin/proxy-nodes/node-1", "GET") is False
assert _app_matches_http_route("/api/admin/proxy-nodes/register", "POST") is False
assert _app_matches_http_route("/api/admin/proxy-nodes/heartbeat", "POST") is False
assert _app_matches_http_route("/api/admin/proxy-nodes/unregister", "POST") is False
assert _app_matches_http_route("/api/admin/proxy-nodes/manual", "POST") is False
assert _app_matches_http_route("/api/admin/proxy-nodes/upgrade", "POST") is False
assert _app_matches_http_route("/api/admin/proxy-nodes/test-url", "POST") is False
assert _app_matches_http_route("/api/admin/proxy-nodes/node-1", "PATCH") is False
assert _app_matches_http_route("/api/admin/proxy-nodes/node-1", "DELETE") is False
assert _app_matches_http_route("/api/admin/proxy-nodes/node-1/test", "POST") is False
assert _app_matches_http_route("/api/admin/proxy-nodes/node-1/config", "PUT") is False
assert _app_matches_http_route("/api/admin/proxy-nodes/node-1/events", "GET") is False
assert _app_matches_http_route("/api/admin/wallets", "GET") is False
assert _app_matches_http_route("/api/admin/wallets/ledger", "GET") is False
assert _app_matches_http_route("/api/admin/wallets/refund-requests", "GET") is False
assert _app_matches_http_route("/api/admin/wallets/wallet-1", "GET") is False
assert _app_matches_http_route("/api/admin/wallets/wallet-1/transactions", "GET") is False
assert _app_matches_http_route("/api/admin/wallets/wallet-1/refunds", "GET") is False
assert _app_matches_http_route("/api/admin/api-keys", "GET") is False
assert _app_matches_http_route("/api/admin/api-keys", "POST") is False
assert _app_matches_http_route("/api/admin/api-keys/key-1", "GET") is False
assert _app_matches_http_route("/api/admin/api-keys/key-1", "PUT") is False
assert _app_matches_http_route("/api/admin/api-keys/key-1", "PATCH") is False
assert _app_matches_http_route("/api/admin/api-keys/key-1", "DELETE") is False
assert _app_matches_http_route("/api/admin/users", "GET") is False
assert _app_matches_http_route("/api/admin/users", "POST") is False
assert _app_matches_http_route("/api/admin/users/user-1", "GET") is False
assert _app_matches_http_route("/api/admin/users/user-1", "PUT") is False
assert _app_matches_http_route("/api/admin/users/user-1", "DELETE") is False
assert _app_matches_http_route("/api/admin/users/user-1/sessions", "GET") is False
assert _app_matches_http_route("/api/admin/users/user-1/sessions", "DELETE") is False
assert _app_matches_http_route("/api/admin/users/user-1/sessions/session-1", "DELETE") is False
assert _app_matches_http_route("/api/admin/users/user-1/api-keys", "GET") is False
assert _app_matches_http_route("/api/admin/users/user-1/api-keys", "POST") is False
assert _app_matches_http_route("/api/admin/users/user-1/api-keys/key-1", "DELETE") is False
assert _app_matches_http_route("/api/admin/users/user-1/api-keys/key-1", "PUT") is False
assert _app_matches_http_route("/api/admin/users/user-1/api-keys/key-1/lock", "PATCH") is False
assert _app_matches_http_route("/api/admin/users/user-1/api-keys/key-1/full-key", "GET") is False
assert _app_matches_http_route("/api/admin/system/email/templates", "GET") is False
assert _app_matches_http_route("/api/admin/system/email/templates/verification", "GET") is False
assert _app_matches_http_route("/api/admin/providers/", "GET") is False
assert _app_matches_http_route("/api/admin/providers/", "POST") is False
assert _app_matches_http_route("/api/admin/providers/provider-openai", "PATCH") is False
assert _app_matches_http_route("/api/admin/providers/provider-openai/summary", "GET") is False
assert (
_app_matches_http_route(
"/api/admin/providers/provider-openai/health-monitor",
"GET",
)
is False
)
assert (
_app_matches_http_route(
"/api/admin/providers/provider-openai/mapping-preview",
"GET",
)
is False
)
assert (
_app_matches_http_route(
"/api/admin/providers/provider-openai/delete-task/task-1",
"GET",
)
is False
)
assert (
_app_matches_http_route(
"/api/admin/providers/provider-openai/pool-status",
"GET",
)
is False
)
assert (
_app_matches_http_route(
"/api/admin/providers/provider-openai/models/model-1",
"GET",
)
is False
)
assert (
_app_matches_http_route(
"/api/admin/providers/provider-openai/models/batch",
"POST",
)
is False
)
assert (
_app_matches_http_route(
"/api/admin/providers/provider-openai/assign-global-models",
"POST",
)
is False
)
assert (
_app_matches_http_route(
"/api/admin/providers/provider-openai/import-from-upstream",
"POST",
)
is False
)
assert (
_app_matches_http_route(
"/api/admin/endpoints/providers/provider-openai/endpoints",
"GET",
)
is False
)
assert (
_app_matches_http_route(
"/api/admin/endpoints/defaults/openai:responses/body-rules",
"GET",
)
is False
)
assert _app_matches_http_route("/api/admin/endpoints/endpoint-1", "GET") is False
assert _app_matches_http_route("/api/admin/endpoints/keys/key-1/export", "GET") is False
assert _app_matches_http_route("/api/admin/endpoints/keys/key-1/reveal", "GET") is False
assert (
_app_matches_http_route(
"/api/admin/endpoints/providers/provider-openai/keys",
"GET",
)
is False
)
assert (
_app_matches_http_route(
"/api/admin/endpoints/providers/provider-openai/refresh-quota",
"POST",
)
is False
)
assert _app_matches_http_route("/api/admin/endpoints/keys/batch-delete", "POST") is False
assert _app_matches_http_route("/api/admin/endpoints/rpm/key/key-1", "GET") is False
assert _app_matches_http_route("/api/admin/endpoints/health/status", "GET") is False
assert _app_matches_http_route("/api/admin/endpoints/health/key/key-1", "GET") is False
assert _app_matches_http_route("/api/admin/endpoints/health/keys/key-1", "PATCH") is False
assert _app_matches_http_route("/api/admin/provider-oauth/supported-types", "GET") is False
assert _app_matches_http_route("/api/admin/provider-oauth/keys/key-1/start", "POST") is False
assert (
_app_matches_http_route(
"/api/admin/provider-oauth/providers/provider-kiro/device-authorize",
"POST",
)
is False
)
assert (
_app_matches_http_route(
"/api/admin/provider-oauth/providers/provider-kiro/device-poll",
"POST",
)
is False
)
assert (
_app_matches_http_route(
"/api/admin/provider-oauth/providers/provider-codex/import-refresh-token",
"POST",
)
is False
)
assert (
_app_matches_http_route(
"/api/admin/provider-oauth/providers/provider-codex/batch-import",
"POST",
)
is False
)
assert (
_app_matches_http_route(
"/api/admin/provider-oauth/providers/provider-codex/batch-import/tasks",
"POST",
)
is False
)
assert (
_app_matches_http_route(
"/api/admin/provider-oauth/providers/provider-codex/batch-import/tasks/task-1",
"GET",
)
is False
)
assert _app_matches_http_route("/api/admin/provider-ops/architectures", "GET") is False
assert (
_app_matches_http_route(
"/api/admin/provider-ops/architectures/generic_api",
"GET",
)
is False
)
assert (
_app_matches_http_route(
"/api/admin/provider-ops/providers/provider-openai/status",
"GET",
)
is False
)
assert (
_app_matches_http_route(
"/api/admin/provider-ops/providers/provider-openai/config",
"GET",
)
is False
)
assert (
_app_matches_http_route(
"/api/admin/provider-ops/providers/provider-openai/config",
"PUT",
)
is False
)
assert (
_app_matches_http_route(
"/api/admin/provider-ops/providers/provider-openai/config",
"DELETE",
)
is False
)
assert _app_matches_http_route("/api/admin/video-tasks", "GET") is False
assert _app_matches_http_route("/api/admin/video-tasks/stats", "GET") is False
assert _app_matches_http_route("/api/admin/video-tasks/task-1", "GET") is False
assert _app_matches_http_route("/api/admin/video-tasks/task-1/cancel", "POST") is False
assert _app_matches_http_route("/api/admin/video-tasks/task-1/video", "GET") is False
assert _app_matches_http_route("/api/admin/adaptive/keys", "GET") is False
assert _app_matches_http_route("/api/admin/adaptive/keys/key-1/mode", "PATCH") is False
assert _app_matches_http_route("/api/admin/adaptive/keys/key-1/stats", "GET") is False
assert _app_matches_http_route("/api/admin/adaptive/keys/key-1/learning", "DELETE") is False
assert _app_matches_http_route("/api/admin/adaptive/keys/key-1/limit", "PATCH") is False
assert _app_matches_http_route("/api/admin/adaptive/summary", "GET") is False
assert _app_matches_http_route("/api/admin/models/catalog", "GET") is False
assert _app_matches_http_route("/api/admin/models/external", "GET") is False
assert _app_matches_http_route("/api/admin/models/external/cache", "DELETE") is False
assert _app_matches_http_route("/api/admin/models/global", "GET") is False
assert _app_matches_http_route("/api/admin/models/global", "POST") is False
assert _app_matches_http_route("/api/admin/models/global/test-id", "GET") is False
assert _app_matches_http_route("/api/admin/models/global/test-id", "PATCH") is False
assert _app_matches_http_route("/api/admin/models/global/test-id", "DELETE") is False
assert _app_matches_http_route("/api/admin/models/global/batch-delete", "POST") is False
assert (
_app_matches_http_route(
"/api/admin/models/global/test-id/assign-to-providers",
"POST",
)
is False
)
assert _app_matches_http_route("/api/admin/models/global/test-id/providers", "GET") is False
assert _app_matches_http_route("/api/admin/models/global/test-id/routing", "GET") is False
assert _app_matches_http_route("/health", "GET") is False
assert _app_matches_http_route("/v1/health", "GET") is False
assert _app_matches_http_route("/", "GET") is False
assert _app_matches_http_route("/test-connection", "GET") is False
assert _app_matches_http_route("/api/internal/gateway/auth-context", "POST") is False
assert _app_matches_http_route("/api/internal/gateway/resolve", "POST") is False
assert _app_matches_http_route("/api/internal/gateway/decision-sync", "POST") is False
assert _app_matches_http_route("/api/internal/tunnel/heartbeat", "POST") is False
assert _app_matches_http_route("/api/internal/tunnel/node-status", "POST") is False
assert _app_matches_http_route("/readyz", "GET") is False
tags = {tag.get("name") for tag in main_module.app.openapi().get("tags", [])}
assert "OpenAI API" not in tags
assert "Claude API" not in tags
assert "Gemini API" not in tags
assert "Gemini Files API" not in tags
assert "System Catalog" not in tags
def test_python_payment_host_surface_is_single_dynamic_callback_route() -> None:
host_route_paths = _route_paths(main_module.app)
assert "/api/payment/callback/{payment_method}" not in host_route_paths
assert "/api/payment/callback/alipay" not in host_route_paths
assert "/api/payment/callback/wechat" not in host_route_paths
assert _app_matches_http_route("/api/payment/callback/alipay", "POST") is False
assert _app_matches_http_route("/api/payment/callback/wechat", "POST") is False
-110
View File
@@ -1,110 +0,0 @@
from __future__ import annotations
from types import SimpleNamespace
from typing import Any
from unittest.mock import MagicMock
import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient
from src.api.user_me.routes import UpdateMyApiKeyAdapter
from src.api.user_me.routes import router as me_router
from src.database import get_db
def _build_me_app(db: MagicMock, monkeypatch: Any) -> TestClient:
app = FastAPI()
app.include_router(me_router)
app.dependency_overrides[get_db] = lambda: db
async def _fake_pipeline_run(
*, adapter: object, http_request: object, db: MagicMock, mode: object
) -> object:
_ = http_request, mode
try:
payload = await http_request.json()
except Exception:
payload = {}
context = SimpleNamespace(
db=db,
user=SimpleNamespace(id="user-1", email="u@example.com"),
request=SimpleNamespace(state=SimpleNamespace()),
ensure_json_body=lambda: payload,
add_audit_metadata=lambda **_: None,
)
return await adapter.handle(context)
monkeypatch.setattr("src.api.user_me.routes.pipeline.run", _fake_pipeline_run)
return TestClient(app)
async def _fake_update_my_api_key_sync(
user_id: str,
key_id: str,
request: object,
captured: dict[str, object],
) -> dict[str, object]:
captured["user_id"] = user_id
captured["key_id"] = key_id
captured["name"] = getattr(request, "name", None)
captured["rate_limit"] = getattr(request, "rate_limit", None)
return {"id": key_id, "name": captured["name"], "rate_limit": captured["rate_limit"]}
def test_update_my_api_key_route_path_smoke(monkeypatch: Any) -> None:
captured: dict[str, object] = {}
def _sync(user_id: str, key_id: str, request: object) -> dict[str, object]:
captured["user_id"] = user_id
captured["key_id"] = key_id
captured["name"] = getattr(request, "name", None)
captured["rate_limit"] = getattr(request, "rate_limit", None)
return {"id": key_id, "name": captured["name"], "rate_limit": captured["rate_limit"]}
monkeypatch.setattr("src.api.user_me.routes._update_my_api_key_sync", _sync)
client = _build_me_app(MagicMock(), monkeypatch)
response = client.put("/api/users/me/api-keys/key-1", json={"name": "Edited", "rate_limit": 6})
assert response.status_code == 200
assert response.json()["rate_limit"] == 6
assert captured == {
"user_id": "user-1",
"key_id": "key-1",
"name": "Edited",
"rate_limit": 6,
}
@pytest.mark.asyncio
async def test_update_my_api_key_adapter_passes_rate_limit_and_name(monkeypatch: Any) -> None:
captured: dict[str, object] = {}
def _sync(user_id: str, key_id: str, request: object) -> dict[str, object]:
captured["user_id"] = user_id
captured["key_id"] = key_id
captured["name"] = getattr(request, "name", None)
captured["rate_limit"] = getattr(request, "rate_limit", None)
return {"id": key_id, "name": captured["name"], "rate_limit": captured["rate_limit"]}
monkeypatch.setattr("src.api.user_me.routes._update_my_api_key_sync", _sync)
adapter = UpdateMyApiKeyAdapter(key_id="key-2")
context = SimpleNamespace(
db=MagicMock(),
user=SimpleNamespace(id="user-1"),
request=SimpleNamespace(state=SimpleNamespace()),
ensure_json_body=lambda: {"name": "Edited Again", "rate_limit": 15},
add_audit_metadata=lambda **_: None,
)
result = await adapter.handle(context)
assert result["id"] == "key-2"
assert captured == {
"user_id": "user-1",
"key_id": "key-2",
"name": "Edited Again",
"rate_limit": 15,
}
-192
View File
@@ -1,192 +0,0 @@
from __future__ import annotations
from types import SimpleNamespace
from unittest.mock import MagicMock
import pytest
from src.api.user_me.routes import GetUsageAdapter
from src.core.enums import UserRole
@pytest.mark.asyncio
async def test_get_usage_adapter_uses_coarse_summary_grouping(
monkeypatch: pytest.MonkeyPatch,
) -> None:
db = MagicMock()
query = MagicMock()
count_query = MagicMock()
count_query.scalar.return_value = 0
query.outerjoin.return_value = query
query.filter.return_value = query
query.group_by.return_value = query
query.with_entities.return_value = count_query
query.options.return_value = query
query.order_by.return_value = query
query.offset.return_value = query
query.limit.return_value = query
query.all.return_value = []
db.query.return_value = query
summary_getter = MagicMock(
return_value=[
{
"provider": "provider-a",
"model": "gpt-4o",
"requests": 2,
"input_tokens": 10,
"output_tokens": 5,
"total_tokens": 15,
"cache_read_tokens": 5,
"total_input_context": 15,
"total_cost_usd": 1.5,
"actual_total_cost_usd": 1.2,
"success_count": 2,
"success_response_time_sum_ms": 1000.0,
"success_response_time_count": 2,
},
{
"provider": "pending",
"model": "gpt-4o",
"requests": 99,
"input_tokens": 999,
"output_tokens": 999,
"total_tokens": 1998,
"cache_read_tokens": 50,
"total_input_context": 1049,
"total_cost_usd": 9.9,
"actual_total_cost_usd": 9.9,
"success_count": 0,
"success_response_time_sum_ms": 0.0,
"success_response_time_count": 0,
},
]
)
monkeypatch.setattr("src.api.user_me.routes.UsageService.get_usage_summary", summary_getter)
monkeypatch.setattr("src.api.user_me.routes.WalletService.get_wallet", lambda *_a, **_k: None)
monkeypatch.setattr(
"src.api.user_me.routes.WalletService.serialize_wallet_summary",
lambda _wallet: {"limit_mode": "finite"},
)
adapter = GetUsageAdapter(time_range=None, limit=20, offset=0)
context = SimpleNamespace(
db=db,
user=SimpleNamespace(id="user-1", role=UserRole.USER),
request=SimpleNamespace(state=SimpleNamespace()),
)
result = await adapter.handle(context)
assert result["total_requests"] == 2
assert result["total_tokens"] == 15
assert result["summary_by_model"] == [
{
"model": "gpt-4o",
"requests": 2,
"input_tokens": 10,
"output_tokens": 5,
"total_tokens": 15,
"cache_read_tokens": 5,
"cache_hit_rate": 33.33,
"total_cost_usd": 1.5,
}
]
assert result["summary_by_api_format"] == []
assert "total_actual_cost" not in result
assert result["summary_by_provider"] == [
{
"provider": "provider-a",
"requests": 2,
"total_tokens": 15,
"cache_read_tokens": 5,
"cache_hit_rate": 33.33,
"total_cost_usd": 1.5,
"success_rate": 100.0,
"avg_response_time_ms": 500.0,
}
]
assert summary_getter.call_args.kwargs["group_by"] is None
@pytest.mark.asyncio
async def test_get_usage_adapter_provider_success_rate_uses_success_count(
monkeypatch: pytest.MonkeyPatch,
) -> None:
db = MagicMock()
query = MagicMock()
count_query = MagicMock()
count_query.scalar.return_value = 0
query.outerjoin.return_value = query
query.filter.return_value = query
query.group_by.return_value = query
query.with_entities.return_value = count_query
query.options.return_value = query
query.order_by.return_value = query
query.offset.return_value = query
query.limit.return_value = query
query.all.return_value = []
db.query.return_value = query
summary_getter = MagicMock(
return_value=[
{
"provider": "provider-a",
"model": "gpt-4o",
"requests": 3,
"input_tokens": 30,
"output_tokens": 15,
"total_tokens": 45,
"cache_read_tokens": 15,
"total_input_context": 45,
"total_cost_usd": 4.5,
"actual_total_cost_usd": 4.5,
"success_count": 2,
"success_response_time_sum_ms": 600.0,
"success_response_time_count": 2,
},
{
"provider": "provider-a",
"model": "gpt-4.1",
"requests": 1,
"input_tokens": 10,
"output_tokens": 5,
"total_tokens": 15,
"cache_read_tokens": 5,
"total_input_context": 15,
"total_cost_usd": 1.5,
"actual_total_cost_usd": 1.5,
"success_count": 0,
"success_response_time_sum_ms": 0.0,
"success_response_time_count": 0,
},
]
)
monkeypatch.setattr("src.api.user_me.routes.UsageService.get_usage_summary", summary_getter)
monkeypatch.setattr("src.api.user_me.routes.WalletService.get_wallet", lambda *_a, **_k: None)
monkeypatch.setattr(
"src.api.user_me.routes.WalletService.serialize_wallet_summary",
lambda _wallet: {"limit_mode": "finite"},
)
adapter = GetUsageAdapter(time_range=None, limit=20, offset=0)
context = SimpleNamespace(
db=db,
user=SimpleNamespace(id="user-1", role=UserRole.USER),
request=SimpleNamespace(state=SimpleNamespace()),
)
result = await adapter.handle(context)
assert result["summary_by_provider"] == [
{
"provider": "provider-a",
"requests": 4,
"total_tokens": 60,
"cache_read_tokens": 20,
"cache_hit_rate": 33.33,
"total_cost_usd": 6.0,
"success_rate": 50.0,
"avg_response_time_ms": 300.0,
}
]
-202
View File
@@ -1,202 +0,0 @@
from __future__ import annotations
from decimal import Decimal
from types import SimpleNamespace
from unittest.mock import MagicMock
import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient
from src.api.wallet.routes import router as wallet_router
from src.database import get_db
def _build_wallet_app(
db: MagicMock,
monkeypatch: pytest.MonkeyPatch,
*,
payload: dict[str, object],
user_id: str = "user-1",
) -> TestClient:
app = FastAPI()
app.include_router(wallet_router)
app.dependency_overrides[get_db] = lambda: db
async def _fake_pipeline_run(*, adapter: object, http_request: object, db: MagicMock, mode: object) -> object:
_ = http_request, mode
context = SimpleNamespace(
db=db,
user=SimpleNamespace(id=user_id),
ensure_json_body=lambda: payload,
add_audit_metadata=lambda **_: None,
)
return await adapter.handle(context)
monkeypatch.setattr("src.api.wallet.routes.pipeline.run", _fake_pipeline_run)
return TestClient(app)
def test_create_refund_route_maps_uncredited_order_to_400(
monkeypatch: pytest.MonkeyPatch,
) -> None:
db = MagicMock()
payload = {"amount_usd": 2.0, "payment_order_id": "order-1"}
client = _build_wallet_app(db, monkeypatch, payload=payload)
wallet = SimpleNamespace(id="wallet-1")
payment_order = SimpleNamespace(id="order-1", wallet_id="wallet-1", payment_method="alipay")
monkeypatch.setattr(
"src.api.wallet.routes.WalletService.get_or_create_wallet",
lambda _db, user: wallet,
)
db.query.return_value.filter.return_value.first.return_value = payment_order
def _raise(*args: object, **kwargs: object) -> object:
raise ValueError("payment order is not refundable")
monkeypatch.setattr("src.api.wallet.routes.WalletService.create_refund_request", _raise)
response = client.post("/api/wallet/refunds", json=payload)
assert response.status_code == 400
assert "not refundable" in response.json()["detail"]
db.rollback.assert_called_once()
db.commit.assert_not_called()
def test_create_refund_route_maps_reserved_wallet_amount_to_400(
monkeypatch: pytest.MonkeyPatch,
) -> None:
db = MagicMock()
payload = {"amount_usd": 2.0}
client = _build_wallet_app(db, monkeypatch, payload=payload)
wallet = SimpleNamespace(id="wallet-1")
monkeypatch.setattr(
"src.api.wallet.routes.WalletService.get_or_create_wallet",
lambda _db, user: wallet,
)
def _raise(*args: object, **kwargs: object) -> object:
raise ValueError("refund amount exceeds available refundable recharge balance")
monkeypatch.setattr("src.api.wallet.routes.WalletService.create_refund_request", _raise)
response = client.post("/api/wallet/refunds", json=payload)
assert response.status_code == 400
assert "available refundable recharge balance" in response.json()["detail"]
db.rollback.assert_called_once()
db.commit.assert_not_called()
def test_create_refund_route_passes_default_order_refund_mode_and_commits(
monkeypatch: pytest.MonkeyPatch,
) -> None:
db = MagicMock()
payload = {"amount_usd": 2.0, "payment_order_id": "order-1", "reason": "test"}
client = _build_wallet_app(db, monkeypatch, payload=payload)
wallet = SimpleNamespace(id="wallet-1")
payment_order = SimpleNamespace(id="order-1", wallet_id="wallet-1", payment_method="alipay")
refund = SimpleNamespace(
id="refund-1",
refund_no="rf-1",
payment_order_id="order-1",
source_type="payment_order",
source_id="order-1",
refund_mode="original_channel",
amount_usd=Decimal("2.00000000"),
status="pending_approval",
reason="test",
failure_reason=None,
gateway_refund_id=None,
payout_method=None,
payout_reference=None,
payout_proof=None,
created_at="2026-03-07T00:00:00Z",
updated_at="2026-03-07T00:00:00Z",
processed_at=None,
completed_at=None,
)
monkeypatch.setattr(
"src.api.wallet.routes.WalletService.get_or_create_wallet",
lambda _db, user: wallet,
)
db.query.return_value.filter.return_value.first.return_value = payment_order
captured: dict[str, object] = {}
def _create_refund_request(_db: MagicMock, **kwargs: object) -> object:
captured.update(kwargs)
return refund
monkeypatch.setattr(
"src.api.wallet.routes.WalletService.create_refund_request",
_create_refund_request,
)
response = client.post("/api/wallet/refunds", json=payload)
assert response.status_code == 200
body = response.json()
assert body["id"] == "refund-1"
assert body["status"] == "pending_approval"
assert captured["refund_mode"] == "original_channel"
assert captured["source_type"] == "payment_order"
assert captured["source_id"] == "order-1"
assert captured["payment_order"] is payment_order
db.commit.assert_called_once()
db.refresh.assert_called_once_with(refund)
db.rollback.assert_not_called()
def test_create_refund_route_uses_offline_payout_for_manual_recharge(
monkeypatch: pytest.MonkeyPatch,
) -> None:
db = MagicMock()
payload = {"amount_usd": 2.0, "payment_order_id": "order-2"}
client = _build_wallet_app(db, monkeypatch, payload=payload)
wallet = SimpleNamespace(id="wallet-1")
payment_order = SimpleNamespace(id="order-2", wallet_id="wallet-1", payment_method="admin_manual")
refund = SimpleNamespace(
id="refund-2",
refund_no="rf-2",
payment_order_id="order-2",
source_type="payment_order",
source_id="order-2",
refund_mode="offline_payout",
amount_usd=Decimal("2.00000000"),
status="pending_approval",
reason=None,
failure_reason=None,
gateway_refund_id=None,
payout_method=None,
payout_reference=None,
payout_proof=None,
created_at="2026-03-07T00:00:00Z",
updated_at="2026-03-07T00:00:00Z",
processed_at=None,
completed_at=None,
)
monkeypatch.setattr(
"src.api.wallet.routes.WalletService.get_or_create_wallet",
lambda _db, user: wallet,
)
db.query.return_value.filter.return_value.first.return_value = payment_order
captured: dict[str, object] = {}
def _create_refund_request(_db: MagicMock, **kwargs: object) -> object:
captured.update(kwargs)
return refund
monkeypatch.setattr(
"src.api.wallet.routes.WalletService.create_refund_request",
_create_refund_request,
)
response = client.post("/api/wallet/refunds", json=payload)
assert response.status_code == 200
assert captured["refund_mode"] == "offline_payout"
-91
View File
@@ -1,91 +0,0 @@
import asyncio
import pytest
from src.clients import redis_client as redis_client_module
from src.clients.redis_client import RedisClientManager, RedisState
def _seed_open_circuit(manager: RedisClientManager) -> None:
manager._circuit_open_until = 9999999999.0
manager._consecutive_failures = 5
manager._last_error = "boom"
def test_reset_redis_circuit_breaker_resets_both_clients() -> None:
old_global = redis_client_module._redis_manager
old_usage = redis_client_module._usage_queue_redis_manager
try:
global_manager = RedisClientManager(client_name="global")
usage_manager = RedisClientManager(client_name="usage")
_seed_open_circuit(global_manager)
_seed_open_circuit(usage_manager)
redis_client_module._redis_manager = global_manager
redis_client_module._usage_queue_redis_manager = usage_manager
assert redis_client_module.reset_redis_circuit_breaker() is True
assert global_manager.get_state() == RedisState.NOT_INITIALIZED
assert usage_manager.get_state() == RedisState.NOT_INITIALIZED
finally:
redis_client_module._redis_manager = old_global
redis_client_module._usage_queue_redis_manager = old_usage
def test_reset_redis_circuit_breaker_returns_false_when_uninitialized() -> None:
old_global = redis_client_module._redis_manager
old_usage = redis_client_module._usage_queue_redis_manager
try:
redis_client_module._redis_manager = None
redis_client_module._usage_queue_redis_manager = None
assert redis_client_module.reset_redis_circuit_breaker() is False
finally:
redis_client_module._redis_manager = old_global
redis_client_module._usage_queue_redis_manager = old_usage
@pytest.mark.asyncio
async def test_get_redis_client_isolated_per_event_loop(monkeypatch: pytest.MonkeyPatch) -> None:
old_global = redis_client_module._redis_manager
try:
redis_client_module._redis_manager = RedisClientManager(client_name="global")
class DummyRedis:
def __init__(self, name: str) -> None:
self.name = name
self.closed = False
async def ping(self) -> bool:
return True
async def close(self) -> None:
self.closed = True
created_clients: list[DummyRedis] = []
async def fake_from_url(*args: object, **kwargs: object) -> DummyRedis:
client = DummyRedis(f"client-{len(created_clients)}")
created_clients.append(client)
return client
monkeypatch.setattr(redis_client_module.aioredis, "from_url", fake_from_url)
main_client = await redis_client_module.get_redis_client()
def _get_client_from_thread() -> object:
return asyncio.run(redis_client_module.get_redis_client())
thread_client = await asyncio.to_thread(_get_client_from_thread)
assert main_client is created_clients[0]
assert thread_client is created_clients[1]
assert thread_client is not main_client
assert redis_client_module.get_redis_client_sync() is main_client
await redis_client_module.close_redis_client()
assert created_clients[0].closed is True
assert created_clients[1].closed is True
finally:
redis_client_module._redis_manager = old_global
-7
View File
@@ -1,7 +0,0 @@
from __future__ import annotations
import os
# 测试运行在容器里时默认会被识别为 production,这里提供稳定的测试密钥。
os.environ.setdefault("JWT_SECRET_KEY", "test-jwt-secret-key-for-pytest-1234567890")
os.environ.setdefault("ENCRYPTION_KEY", "test-encryption-key-for-pytest-1234567890")
@@ -1,40 +0,0 @@
from __future__ import annotations
from fastapi import FastAPI
def _build_contract_app() -> FastAPI:
from src.api.public.claude import router as claude_router
from src.api.public.gemini import router as gemini_router
from src.api.public.openai import router as openai_router
app = FastAPI()
app.include_router(claude_router)
app.include_router(openai_router)
app.include_router(gemini_router)
return app
def test_public_api_routes_contract_paths_and_tags() -> None:
app = _build_contract_app()
schema = app.openapi()
paths = schema.get("paths") or {}
expected = [
("/v1/messages", "post", "Claude API"),
("/v1/messages/count_tokens", "post", "Claude API"),
("/v1/chat/completions", "post", "OpenAI API"),
("/v1/responses", "post", "OpenAI API"),
("/v1beta/models/{model}:generateContent", "post", "Gemini API"),
("/v1beta/models/{model}:streamGenerateContent", "post", "Gemini API"),
("/v1/models/{model}:generateContent", "post", "Gemini API"),
("/v1/models/{model}:streamGenerateContent", "post", "Gemini API"),
]
for path, method, expected_tag in expected:
assert path in paths, f"missing path {path}"
operations = paths.get(path) or {}
assert method in operations, f"missing {method.upper()} {path}"
tags = operations.get(method, {}).get("tags") or []
assert expected_tag in tags, f"{method.upper()} {path} missing tag {expected_tag}"
@@ -1,170 +0,0 @@
from __future__ import annotations
from types import SimpleNamespace
from typing import cast
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from src.models.database import Provider, ProviderAPIKey, ProviderEndpoint
from src.services.scheduling.aware_scheduler import CacheAwareScheduler, ProviderCandidate
def _make_candidate(
*,
key_id: str,
global_priority: int,
needs_conversion: bool,
provider_keep_priority_on_conversion: bool,
is_skipped: bool = False,
) -> ProviderCandidate:
provider = SimpleNamespace(
id=f"p_{key_id}",
name=f"prov_{key_id}",
provider_priority=1,
keep_priority_on_conversion=provider_keep_priority_on_conversion,
)
endpoint = SimpleNamespace(id=f"e_{key_id}")
key = SimpleNamespace(
id=key_id,
internal_priority=1,
api_key="sk-test-1234567890",
global_priority_by_format={"openai:chat": global_priority},
)
return ProviderCandidate(
provider=cast(Provider, provider),
endpoint=cast(ProviderEndpoint, endpoint),
key=cast(ProviderAPIKey, key),
is_cached=False,
is_skipped=is_skipped,
skip_reason="unhealthy" if is_skipped else None,
needs_conversion=needs_conversion,
provider_api_format="openai:chat",
)
@pytest.mark.asyncio
async def test_cache_affinity_hit_healthy_candidate_is_always_promoted_to_front() -> None:
"""契约:缓存亲和性命中且候选健康时,无条件置顶(覆盖降级分组/优先级)。"""
scheduler = CacheAwareScheduler()
scheduler.scheduling_mode = CacheAwareScheduler.SCHEDULING_MODE_CACHE_AFFINITY
scheduler.priority_mode = CacheAwareScheduler.PRIORITY_MODE_GLOBAL_KEY
db = MagicMock()
keep_1 = _make_candidate(
key_id="k_keep_1",
global_priority=1,
needs_conversion=False,
provider_keep_priority_on_conversion=False,
)
keep_2 = _make_candidate(
key_id="k_keep_2",
global_priority=2,
needs_conversion=False,
provider_keep_priority_on_conversion=False,
)
matched_demote = _make_candidate(
key_id="k_cached",
global_priority=0,
needs_conversion=True,
provider_keep_priority_on_conversion=False,
)
affinity = SimpleNamespace(
provider_id=matched_demote.provider.id,
endpoint_id=matched_demote.endpoint.id,
key_id=matched_demote.key.id,
request_count=7,
)
scheduler._affinity_manager = SimpleNamespace(get_affinity=AsyncMock(return_value=affinity))
with patch(
"src.services.system.config.SystemConfigService.is_keep_priority_on_conversion",
return_value=False,
):
result = await scheduler.reorder_candidates(
candidates=[keep_1, matched_demote, keep_2],
db=db,
affinity_key="a1",
api_format="openai:chat",
global_model_id="gm1",
)
assert [c.key.id for c in result] == ["k_cached", "k_keep_1", "k_keep_2"]
assert result[0] is matched_demote
assert result[0].is_cached is True
assert all(not c.is_cached for c in result[1:])
@pytest.mark.asyncio
async def test_cache_affinity_hit_skipped_candidate_is_promoted_within_its_group() -> None:
"""契约:缓存亲和性命中但候选被跳过时,只提升到其所属类别内最前面。"""
scheduler = CacheAwareScheduler()
scheduler.scheduling_mode = CacheAwareScheduler.SCHEDULING_MODE_CACHE_AFFINITY
scheduler.priority_mode = CacheAwareScheduler.PRIORITY_MODE_GLOBAL_KEY
db = MagicMock()
keep_1 = _make_candidate(
key_id="k_keep_1",
global_priority=1,
needs_conversion=False,
provider_keep_priority_on_conversion=False,
)
keep_2 = _make_candidate(
key_id="k_keep_2",
global_priority=2,
needs_conversion=False,
provider_keep_priority_on_conversion=False,
)
demote_other = _make_candidate(
key_id="k_demote_other",
global_priority=0,
needs_conversion=True,
provider_keep_priority_on_conversion=False,
)
matched_demote_skipped = _make_candidate(
key_id="k_cached",
global_priority=10,
needs_conversion=True,
provider_keep_priority_on_conversion=False,
is_skipped=True,
)
affinity = SimpleNamespace(
provider_id=matched_demote_skipped.provider.id,
endpoint_id=matched_demote_skipped.endpoint.id,
key_id=matched_demote_skipped.key.id,
request_count=3,
)
scheduler._affinity_manager = SimpleNamespace(get_affinity=AsyncMock(return_value=affinity))
with patch(
"src.services.system.config.SystemConfigService.is_keep_priority_on_conversion",
return_value=False,
):
result = await scheduler.reorder_candidates(
candidates=[keep_1, demote_other, keep_2, matched_demote_skipped],
db=db,
affinity_key="a1",
api_format="openai:chat",
global_model_id="gm1",
)
# keep 组(exact)整体在前;matched 在 demote 组内置顶(即使 global_priority 更差)
assert [c.key.id for c in result] == [
"k_keep_1",
"k_keep_2",
"k_cached",
"k_demote_other",
]
assert result[2] is matched_demote_skipped
assert result[2].is_cached is True
assert all(not c.is_cached for i, c in enumerate(result) if i != 2)
@@ -1,139 +0,0 @@
from __future__ import annotations
from types import SimpleNamespace
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from src.services.scheduling.aware_scheduler import CacheAwareScheduler
def _make_db() -> MagicMock:
db = MagicMock()
db.new = []
db.dirty = []
db.deleted = []
db.in_transaction.return_value = False
return db
def _make_global_model(*, gid: str, name: str) -> SimpleNamespace:
return SimpleNamespace(
id=gid,
name=name,
is_active=True,
config={},
supported_capabilities=[],
)
@pytest.mark.asyncio
async def test_list_all_candidates_returns_provider_batch_count_even_when_candidates_empty() -> (
None
):
"""契约:候选为空不代表 Provider 页为空(用于分页继续拉取下一页)。"""
scheduler = CacheAwareScheduler()
scheduler.scheduling_mode = CacheAwareScheduler.SCHEDULING_MODE_FIXED_ORDER
db = _make_db()
providers = [
SimpleNamespace(
id="p1",
name="p1",
is_active=True,
endpoints=[],
models=[],
provider_priority=1,
),
SimpleNamespace(
id="p2",
name="p2",
is_active=True,
endpoints=[],
models=[],
provider_priority=2,
),
]
# allowed_providers 会把本页的 provider 全过滤掉,导致 candidates 为空;但 provider_batch_count 应保留过滤前数量。
user_api_key = SimpleNamespace(
id="ak1",
allowed_providers=["not-matching"],
allowed_models=None,
allowed_api_formats=None,
user=None,
)
global_model = _make_global_model(gid="gm1", name="gpt-4o")
with patch.object(scheduler, "_ensure_initialized", new=AsyncMock(return_value=None)):
with patch.object(
scheduler._candidate_builder,
"_query_provider_refs",
return_value=[("p1", "p1"), ("p2", "p2")],
):
with patch.object(scheduler._candidate_builder, "_query_providers") as query_providers:
with patch(
"src.services.scheduling.aware_scheduler.ModelCacheService.get_global_model_by_name",
new=AsyncMock(return_value=global_model),
):
with patch(
"src.services.scheduling.aware_scheduler.SystemConfigService.is_format_conversion_enabled",
return_value=True,
):
candidates, global_model_id, provider_batch_count = (
await scheduler.list_all_candidates(
db=db,
api_format="openai:chat",
model_name="gpt-4o",
affinity_key=None,
user_api_key=user_api_key, # type: ignore[arg-type]
provider_offset=0,
provider_limit=20,
)
)
query_providers.assert_not_called()
assert candidates == []
assert global_model_id == "gm1"
assert provider_batch_count == 2
@pytest.mark.asyncio
async def test_list_all_candidates_returns_zero_provider_batch_count_when_provider_page_empty() -> (
None
):
scheduler = CacheAwareScheduler()
scheduler.scheduling_mode = CacheAwareScheduler.SCHEDULING_MODE_FIXED_ORDER
db = _make_db()
global_model = _make_global_model(gid="gm1", name="gpt-4o")
with patch.object(scheduler, "_ensure_initialized", new=AsyncMock(return_value=None)):
with patch.object(scheduler._candidate_builder, "_query_providers", return_value=[]):
with patch(
"src.services.scheduling.aware_scheduler.ModelCacheService.get_global_model_by_name",
new=AsyncMock(return_value=global_model),
):
with patch(
"src.services.scheduling.aware_scheduler.SystemConfigService.is_format_conversion_enabled",
return_value=True,
):
candidates, global_model_id, provider_batch_count = (
await scheduler.list_all_candidates(
db=db,
api_format="openai:chat",
model_name="gpt-4o",
affinity_key=None,
user_api_key=None,
provider_offset=0,
provider_limit=20,
)
)
assert candidates == []
assert global_model_id == "gm1"
assert provider_batch_count == 0
@@ -1,93 +0,0 @@
from __future__ import annotations
from types import SimpleNamespace
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from src.services.scheduling.aware_scheduler import CacheAwareScheduler
def _make_db() -> MagicMock:
db = MagicMock()
db.new = []
db.dirty = []
db.deleted = []
db.in_transaction.return_value = False
return db
def _make_global_model() -> SimpleNamespace:
return SimpleNamespace(
id="gm1",
name="gpt-4o",
is_active=True,
config={},
supported_capabilities=[],
)
@pytest.mark.asyncio
async def test_list_all_candidates_prefilters_provider_graph_by_allowed_providers() -> None:
scheduler = CacheAwareScheduler()
scheduler.scheduling_mode = CacheAwareScheduler.SCHEDULING_MODE_FIXED_ORDER
db = _make_db()
global_model = _make_global_model()
user_api_key = SimpleNamespace(
id="ak1",
allowed_providers=["provider-b"],
allowed_models=None,
allowed_api_formats=None,
user=None,
)
filtered_provider = SimpleNamespace(
id="provider-b",
name="provider-b",
endpoints=[],
models=[],
provider_priority=2,
)
with patch.object(scheduler, "_ensure_initialized", new=AsyncMock(return_value=None)):
with patch.object(
scheduler._candidate_builder,
"_query_provider_refs",
return_value=[("provider-a", "provider-a"), ("provider-b", "provider-b")],
) as refs_mock:
with patch.object(
scheduler._candidate_builder,
"_query_providers",
return_value=[filtered_provider],
) as providers_mock:
with patch(
"src.services.scheduling.aware_scheduler.ModelCacheService.get_global_model_by_name",
new=AsyncMock(return_value=global_model),
):
with patch(
"src.services.scheduling.aware_scheduler.SystemConfigService.is_format_conversion_enabled",
return_value=True,
):
with patch.object(
scheduler._candidate_builder,
"_build_candidates",
new=AsyncMock(return_value=[]),
):
candidates, global_model_id, provider_batch_count = (
await scheduler.list_all_candidates(
db=db,
api_format="openai:chat",
model_name="gpt-4o",
affinity_key=None,
user_api_key=user_api_key, # type: ignore[arg-type]
provider_offset=0,
provider_limit=20,
)
)
assert candidates == []
assert global_model_id == "gm1"
assert provider_batch_count == 2
refs_mock.assert_called_once()
providers_mock.assert_called_once_with(db=db, provider_ids=["provider-b"])
@@ -1,6 +0,0 @@
"""
Format conversion test fixtures.
Provides golden internal representations, format-specific fixtures,
stream fixtures, error fixtures, and assertion helpers.
"""
@@ -1,361 +0,0 @@
"""
Assertion helpers for format conversion tests.
Provides semantic comparison functions that check meaningful equivalence
while tolerating format-specific differences (extra fields, id regeneration, etc.).
"""
from __future__ import annotations
from collections.abc import Sequence
from src.core.api_format.conversion.internal import (
ContentBlock,
ImageBlock,
InternalMessage,
InternalRequest,
InternalResponse,
StopReason,
TextBlock,
ThinkingBlock,
ToolDefinition,
ToolResultBlock,
ToolUseBlock,
UnknownBlock,
)
from src.core.api_format.conversion.stream_events import (
ContentBlockStartEvent,
ContentDeltaEvent,
InternalStreamEvent,
MessageStopEvent,
ToolCallDeltaEvent,
)
def assert_internal_request_matches(
actual: InternalRequest,
expected: InternalRequest,
required_fields: set[str],
) -> None:
"""Verify that actual InternalRequest matches expected on required fields."""
if "model" in required_fields:
assert (
actual.model == expected.model
), f"model mismatch: {actual.model!r} != {expected.model!r}"
if "messages" in required_fields:
# Merge consecutive same-role messages before comparison,
# since normalizers may merge/split them during conversion.
actual_msgs = _merge_consecutive_same_role(actual.messages)
expected_msgs = _merge_consecutive_same_role(expected.messages)
assert len(actual_msgs) == len(
expected_msgs
), f"message count mismatch: {len(actual_msgs)} != {len(expected_msgs)}"
for i, (a, e) in enumerate(zip(actual_msgs, expected_msgs)):
assert a.role == e.role, f"message[{i}] role mismatch: {a.role} != {e.role}"
assert_content_blocks_match_unordered(a.content, e.content, context=f"message[{i}]")
if "system" in required_fields:
# Allow either system or instructions to carry the system prompt
actual_sys = actual.system or _join_instructions(actual.instructions)
expected_sys = expected.system or _join_instructions(expected.instructions)
assert actual_sys == expected_sys, f"system mismatch: {actual_sys!r} != {expected_sys!r}"
if "max_tokens" in required_fields:
assert (
actual.max_tokens == expected.max_tokens
), f"max_tokens mismatch: {actual.max_tokens} != {expected.max_tokens}"
if "stream" in required_fields:
assert actual.stream == expected.stream
if "tools" in required_fields:
assert_tools_match(actual.tools, expected.tools)
if "tool_choice" in required_fields:
if expected.tool_choice is not None:
assert actual.tool_choice is not None, "tool_choice is None but expected non-None"
assert (
actual.tool_choice.type == expected.tool_choice.type
), f"tool_choice.type mismatch: {actual.tool_choice.type} != {expected.tool_choice.type}"
def assert_internal_response_matches(
actual: InternalResponse,
expected: InternalResponse,
required_fields: set[str],
) -> None:
"""Verify that actual InternalResponse matches expected on required fields."""
if "content" in required_fields:
assert_content_blocks_match(actual.content, expected.content, context="response")
if "stop_reason" in required_fields:
assert (
actual.stop_reason == expected.stop_reason
), f"stop_reason mismatch: {actual.stop_reason} != {expected.stop_reason}"
if "usage" in required_fields and expected.usage is not None:
assert actual.usage is not None, "usage is None but expected non-None"
assert actual.usage.input_tokens == expected.usage.input_tokens
assert actual.usage.output_tokens == expected.usage.output_tokens
def assert_content_blocks_match(
actual_blocks: list[ContentBlock],
expected_blocks: list[ContentBlock],
*,
context: str = "",
) -> None:
"""Verify content block lists are semantically equivalent (ignoring extra)."""
# Filter out UnknownBlock (allowed to be lost)
actual_meaningful = [b for b in actual_blocks if not isinstance(b, UnknownBlock)]
expected_meaningful = [b for b in expected_blocks if not isinstance(b, UnknownBlock)]
assert len(actual_meaningful) == len(expected_meaningful), (
f"{context} block count mismatch: {len(actual_meaningful)} != {len(expected_meaningful)}"
f"\n actual types: {[type(b).__name__ for b in actual_meaningful]}"
f"\n expected types: {[type(b).__name__ for b in expected_meaningful]}"
)
for i, (a, e) in enumerate(zip(actual_meaningful, expected_meaningful)):
prefix = f"{context}.block[{i}]" if context else f"block[{i}]"
assert type(a) is type(
e
), f"{prefix} type mismatch: {type(a).__name__} != {type(e).__name__}"
if isinstance(a, TextBlock) and isinstance(e, TextBlock):
assert a.text == e.text, f"{prefix} text mismatch: {a.text!r} != {e.text!r}"
elif isinstance(a, ToolUseBlock) and isinstance(e, ToolUseBlock):
assert (
a.tool_name == e.tool_name
), f"{prefix} tool_name mismatch: {a.tool_name!r} != {e.tool_name!r}"
assert (
a.tool_input == e.tool_input
), f"{prefix} tool_input mismatch: {a.tool_input} != {e.tool_input}"
# tool_id may be regenerated, just verify non-empty
assert bool(a.tool_id), f"{prefix} tool_id is empty"
elif isinstance(a, ToolResultBlock) and isinstance(e, ToolResultBlock):
assert bool(a.tool_use_id), f"{prefix} tool_use_id is empty"
# content_text or output should be semantically equivalent
# Normalizers may parse JSON strings into dicts, so compare semantically
a_val = _normalize_tool_output(a)
e_val = _normalize_tool_output(e)
assert a_val == e_val, f"{prefix} tool result mismatch: {a_val!r} != {e_val!r}"
elif isinstance(a, ThinkingBlock) and isinstance(e, ThinkingBlock):
assert (
a.thinking == e.thinking
), f"{prefix} thinking mismatch: {a.thinking!r} != {e.thinking!r}"
elif isinstance(a, ImageBlock) and isinstance(e, ImageBlock):
if e.url:
assert a.url == e.url, f"{prefix} image url mismatch"
if e.data:
assert a.data == e.data, f"{prefix} image data mismatch"
if e.media_type:
assert a.media_type == e.media_type, f"{prefix} media_type mismatch"
def assert_tools_match(
actual: list[ToolDefinition] | None,
expected: list[ToolDefinition] | None,
) -> None:
"""Verify tool definitions match."""
if expected is None:
return
assert actual is not None, "tools is None but expected non-None"
assert len(actual) == len(expected), f"tools count mismatch: {len(actual)} != {len(expected)}"
for i, (a, e) in enumerate(zip(actual, expected)):
assert a.name == e.name, f"tool[{i}].name mismatch: {a.name!r} != {e.name!r}"
if e.description is not None:
assert a.description == e.description, f"tool[{i}].description mismatch"
if e.parameters is not None:
assert a.parameters == e.parameters, f"tool[{i}].parameters mismatch"
def assert_content_blocks_match_unordered(
actual_blocks: list[ContentBlock],
expected_blocks: list[ContentBlock],
*,
context: str = "",
) -> None:
"""Verify content blocks are semantically equivalent regardless of order.
Groups blocks by type and compares within each group. This tolerates
reordering that normalizers may introduce during roundtrip (e.g. placing
tool_result before or after text within the same message).
"""
actual_meaningful = [b for b in actual_blocks if not isinstance(b, UnknownBlock)]
expected_meaningful = [b for b in expected_blocks if not isinstance(b, UnknownBlock)]
assert len(actual_meaningful) == len(expected_meaningful), (
f"{context} block count mismatch: {len(actual_meaningful)} != {len(expected_meaningful)}"
f"\n actual types: {[type(b).__name__ for b in actual_meaningful]}"
f"\n expected types: {[type(b).__name__ for b in expected_meaningful]}"
)
def _group_by_type(blocks: Sequence[ContentBlock]) -> dict[type, list[ContentBlock]]:
groups: dict[type, list[ContentBlock]] = {}
for b in blocks:
groups.setdefault(type(b), []).append(b)
return groups
actual_groups = _group_by_type(actual_meaningful)
expected_groups = _group_by_type(expected_meaningful)
assert set(actual_groups.keys()) == set(expected_groups.keys()), (
f"{context} block type sets differ: "
f"{[t.__name__ for t in actual_groups]} != {[t.__name__ for t in expected_groups]}"
)
for btype in expected_groups:
a_list = actual_groups[btype]
e_list = expected_groups[btype]
assert len(a_list) == len(
e_list
), f"{context} {btype.__name__} count mismatch: {len(a_list)} != {len(e_list)}"
for i, (a, e) in enumerate(zip(a_list, e_list)):
prefix = f"{context}.{btype.__name__}[{i}]" if context else f"{btype.__name__}[{i}]"
if isinstance(a, TextBlock) and isinstance(e, TextBlock):
assert a.text == e.text, f"{prefix} text mismatch: {a.text!r} != {e.text!r}"
elif isinstance(a, ToolUseBlock) and isinstance(e, ToolUseBlock):
assert a.tool_name == e.tool_name, f"{prefix} tool_name mismatch"
assert a.tool_input == e.tool_input, f"{prefix} tool_input mismatch"
elif isinstance(a, ToolResultBlock) and isinstance(e, ToolResultBlock):
a_val = _normalize_tool_output(a)
e_val = _normalize_tool_output(e)
assert a_val == e_val, f"{prefix} tool result mismatch: {a_val!r} != {e_val!r}"
elif isinstance(a, ThinkingBlock) and isinstance(e, ThinkingBlock):
assert a.thinking == e.thinking, f"{prefix} thinking mismatch"
elif isinstance(a, ImageBlock) and isinstance(e, ImageBlock):
if e.url:
assert a.url == e.url, f"{prefix} image url mismatch"
if e.data:
assert a.data == e.data, f"{prefix} image data mismatch"
def _merge_consecutive_same_role(
messages: list[InternalMessage],
) -> list[InternalMessage]:
"""Merge consecutive messages with the same role into one (for semantic comparison)."""
if not messages:
return []
merged: list[InternalMessage] = []
for msg in messages:
if merged and merged[-1].role == msg.role:
merged[-1] = InternalMessage(
role=msg.role,
content=list(merged[-1].content) + list(msg.content),
)
else:
merged.append(InternalMessage(role=msg.role, content=list(msg.content)))
return merged
def assert_internal_requests_equivalent(
a: InternalRequest,
b: InternalRequest,
lossy_fields: set[str] | None = None,
) -> None:
"""Verify two InternalRequests are semantically equivalent after roundtrip."""
lossy = lossy_fields or set()
assert a.model == b.model
if "messages" not in lossy:
# Merge consecutive same-role messages before comparison,
# since normalizers may merge/split them during roundtrip.
a_msgs = _merge_consecutive_same_role(a.messages)
b_msgs = _merge_consecutive_same_role(b.messages)
assert len(a_msgs) == len(
b_msgs
), f"message count mismatch after merge: {len(a_msgs)} != {len(b_msgs)}"
for i, (am, bm) in enumerate(zip(a_msgs, b_msgs)):
assert am.role == bm.role, f"message[{i}] role mismatch after roundtrip"
# Use order-insensitive comparison: normalizers may reorder blocks
# within a message during roundtrip (e.g. tool_result before/after text).
assert_content_blocks_match_unordered(
am.content, bm.content, context=f"roundtrip.message[{i}]"
)
if "system" not in lossy:
a_sys = a.system or _join_instructions(a.instructions)
b_sys = b.system or _join_instructions(b.instructions)
assert a_sys == b_sys
if "max_tokens" not in lossy:
assert a.max_tokens == b.max_tokens
if "tools" not in lossy:
assert_tools_match(a.tools, b.tools)
def assert_stream_text_matches(
events: list[InternalStreamEvent],
expected_text: str,
) -> None:
"""Verify that stream events produce the expected text when concatenated."""
parts: list[str] = []
for evt in events:
if isinstance(evt, ContentDeltaEvent) and evt.text_delta:
parts.append(evt.text_delta)
actual = "".join(parts)
assert actual == expected_text, f"stream text mismatch: {actual!r} != {expected_text!r}"
def assert_stream_stop_reason_matches(
events: list[InternalStreamEvent],
expected: StopReason,
) -> None:
"""Verify that the stream ends with the expected stop reason."""
stop_events = [e for e in events if isinstance(e, MessageStopEvent)]
assert stop_events, "no MessageStopEvent found in stream events"
last = stop_events[-1]
assert (
last.stop_reason == expected
), f"stream stop_reason mismatch: {last.stop_reason} != {expected}"
def _join_instructions(instructions: list) -> str | None:
if not instructions:
return None
parts = [seg.text for seg in instructions if seg.text]
return "\n\n".join(parts) or None
def _normalize_tool_output(block: ToolResultBlock) -> object:
"""Normalize tool output for comparison (parse JSON strings to dicts)."""
import json
val = block.content_text if block.content_text is not None else block.output
if val is None:
return ""
if isinstance(val, str):
try:
return json.loads(val)
except (json.JSONDecodeError, TypeError):
return val
return val
def assert_stream_has_tool_call(
events: list[InternalStreamEvent],
expected_tool_name: str,
) -> None:
"""Verify that stream events contain a tool call with the expected name."""
from src.core.api_format.conversion.internal import ContentType
tool_starts = [
e
for e in events
if isinstance(e, ContentBlockStartEvent) and e.block_type == ContentType.TOOL_USE
]
assert tool_starts, "no tool call ContentBlockStartEvent found in stream events"
names = [e.tool_name for e in tool_starts]
assert (
expected_tool_name in names
), f"tool name {expected_tool_name!r} not found in stream tool starts: {names}"
# Verify there are ToolCallDeltaEvents with non-empty input
tool_deltas = [e for e in events if isinstance(e, ToolCallDeltaEvent)]
assert tool_deltas, "no ToolCallDeltaEvent found in stream events"
combined = "".join(d.input_delta for d in tool_deltas)
assert combined, "tool call input_delta is empty after concatenation"
@@ -1,258 +0,0 @@
"""
Error fixtures for each format.
Each fixture defines a format-specific error response and the expected
InternalError it should produce.
"""
from __future__ import annotations
from dataclasses import dataclass
from typing import Any
from src.core.api_format.conversion.internal import ErrorType
@dataclass
class ErrorFixture:
"""A format-specific error fixture."""
error_response: dict[str, Any]
expected_type: ErrorType
expected_message: str
# ===================================================================
# Claude error responses
# ===================================================================
_CLAUDE_ERRORS: dict[str, ErrorFixture] = {
"invalid_request": ErrorFixture(
error_response={
"type": "error",
"error": {
"type": "invalid_request_error",
"message": "max_tokens must be a positive integer",
},
},
expected_type=ErrorType.INVALID_REQUEST,
expected_message="max_tokens must be a positive integer",
),
"rate_limit": ErrorFixture(
error_response={
"type": "error",
"error": {
"type": "rate_limit_error",
"message": "Rate limit exceeded",
},
},
expected_type=ErrorType.RATE_LIMIT,
expected_message="Rate limit exceeded",
),
"auth_error": ErrorFixture(
error_response={
"type": "error",
"error": {
"type": "authentication_error",
"message": "Invalid API key",
},
},
expected_type=ErrorType.AUTHENTICATION,
expected_message="Invalid API key",
),
"overloaded": ErrorFixture(
error_response={
"type": "error",
"error": {
"type": "overloaded_error",
"message": "Overloaded",
},
},
expected_type=ErrorType.OVERLOADED,
expected_message="Overloaded",
),
"server_error": ErrorFixture(
error_response={
"type": "error",
"error": {
"type": "api_error",
"message": "Internal server error",
},
},
expected_type=ErrorType.SERVER_ERROR,
expected_message="Internal server error",
),
"not_found": ErrorFixture(
error_response={
"type": "error",
"error": {
"type": "not_found_error",
"message": "Model not found",
},
},
expected_type=ErrorType.NOT_FOUND,
expected_message="Model not found",
),
}
# ===================================================================
# OpenAI Chat error responses
# ===================================================================
_OPENAI_CHAT_ERRORS: dict[str, ErrorFixture] = {
"invalid_request": ErrorFixture(
error_response={
"error": {
"message": "Invalid value for max_tokens",
"type": "invalid_request_error",
"param": "max_tokens",
"code": None,
}
},
expected_type=ErrorType.INVALID_REQUEST,
expected_message="Invalid value for max_tokens",
),
"rate_limit": ErrorFixture(
error_response={
"error": {
"message": "Rate limit reached",
"type": "rate_limit_exceeded",
"param": None,
"code": "rate_limit_exceeded",
}
},
expected_type=ErrorType.RATE_LIMIT,
expected_message="Rate limit reached",
),
"auth_error": ErrorFixture(
error_response={
"error": {
"message": "Incorrect API key provided",
"type": "invalid_api_key",
"param": None,
"code": "invalid_api_key",
}
},
expected_type=ErrorType.AUTHENTICATION,
expected_message="Incorrect API key provided",
),
"server_error": ErrorFixture(
error_response={
"error": {
"message": "The server had an error",
"type": "server_error",
"param": None,
"code": "server_error",
}
},
expected_type=ErrorType.SERVER_ERROR,
expected_message="The server had an error",
),
}
# ===================================================================
# OpenAI CLI (Responses API) error responses
# ===================================================================
_OPENAI_CLI_ERRORS: dict[str, ErrorFixture] = {
"invalid_request": ErrorFixture(
error_response={
"error": {
"message": "Invalid input",
"type": "invalid_request_error",
"code": None,
}
},
expected_type=ErrorType.INVALID_REQUEST,
expected_message="Invalid input",
),
"rate_limit": ErrorFixture(
error_response={
"error": {
"message": "Rate limit reached",
"type": "rate_limit_exceeded",
"code": "rate_limit_exceeded",
}
},
expected_type=ErrorType.RATE_LIMIT,
expected_message="Rate limit reached",
),
"server_error": ErrorFixture(
error_response={
"error": {
"message": "Internal server error",
"type": "server_error",
"code": "server_error",
}
},
expected_type=ErrorType.SERVER_ERROR,
expected_message="Internal server error",
),
}
# ===================================================================
# Gemini error responses
# ===================================================================
_GEMINI_ERRORS: dict[str, ErrorFixture] = {
"invalid_request": ErrorFixture(
error_response={
"error": {
"code": 400,
"message": "Invalid value for field",
"status": "INVALID_ARGUMENT",
}
},
expected_type=ErrorType.INVALID_REQUEST,
expected_message="Invalid value for field",
),
"rate_limit": ErrorFixture(
error_response={
"error": {
"code": 429,
"message": "Resource exhausted",
"status": "RESOURCE_EXHAUSTED",
}
},
expected_type=ErrorType.RATE_LIMIT,
expected_message="Resource exhausted",
),
"auth_error": ErrorFixture(
error_response={
"error": {
"code": 401,
"message": "API key not valid",
"status": "UNAUTHENTICATED",
}
},
expected_type=ErrorType.AUTHENTICATION,
expected_message="API key not valid",
),
"server_error": ErrorFixture(
error_response={
"error": {
"code": 500,
"message": "Internal error encountered",
"status": "INTERNAL",
}
},
expected_type=ErrorType.SERVER_ERROR,
expected_message="Internal error encountered",
),
}
# ===================================================================
# Registry
# ===================================================================
ERROR_FIXTURES: dict[str, dict[str, ErrorFixture]] = {
"claude:chat": _CLAUDE_ERRORS,
"claude:cli": _CLAUDE_ERRORS,
"openai:chat": _OPENAI_CHAT_ERRORS,
"openai:cli": _OPENAI_CLI_ERRORS,
"gemini:chat": _GEMINI_ERRORS,
"gemini:cli": _GEMINI_ERRORS,
}
ERROR_ALL_FORMATS = list(ERROR_FIXTURES.keys())
File diff suppressed because it is too large Load Diff
@@ -1,425 +0,0 @@
"""
Internal golden fixtures.
Each fixture defines the canonical InternalRequest / InternalResponse
that all normalizers must produce (or consume) for a given scenario.
"""
from __future__ import annotations
from dataclasses import dataclass, field
from src.core.api_format.conversion.internal import (
ImageBlock,
InstructionSegment,
InternalMessage,
InternalRequest,
InternalResponse,
Role,
StopReason,
TextBlock,
ThinkingBlock,
ToolChoice,
ToolChoiceType,
ToolDefinition,
ToolResultBlock,
ToolUseBlock,
UsageInfo,
)
@dataclass
class GoldenFixture:
"""A golden internal fixture for a specific scenario."""
fixture_id: str
description: str
internal_request: InternalRequest
internal_response: InternalResponse
# Fields that MUST be correctly converted by every normalizer
required_fields: set[str] = field(default_factory=set)
# Fields that may be lost during conversion (format-specific extras)
lossy_fields: set[str] = field(default_factory=set)
# ---------------------------------------------------------------------------
# Shared constants
# ---------------------------------------------------------------------------
_MODEL = "test-model"
_SYSTEM = "You are a helpful assistant."
_TOOL_DEF = ToolDefinition(
name="get_weather",
description="Get the current weather for a location.",
parameters={
"type": "object",
"properties": {
"location": {"type": "string", "description": "City name"},
},
"required": ["location"],
},
)
_TOOL_ID = "tool_call_001"
_REQUIRED_REQUEST = {"model", "messages", "system"}
_REQUIRED_RESPONSE = {"content", "stop_reason"}
# ---------------------------------------------------------------------------
# simple_text: single-turn text conversation
# ---------------------------------------------------------------------------
SIMPLE_TEXT = GoldenFixture(
fixture_id="simple_text",
description="Single-turn text conversation with system prompt",
internal_request=InternalRequest(
model=_MODEL,
messages=[
InternalMessage(role=Role.USER, content=[TextBlock(text="Hello, how are you?")]),
],
instructions=[InstructionSegment(role=Role.SYSTEM, text=_SYSTEM)],
system=_SYSTEM,
max_tokens=1024,
stream=False,
),
internal_response=InternalResponse(
id="resp_001",
model=_MODEL,
content=[TextBlock(text="I'm doing well, thank you!")],
stop_reason=StopReason.END_TURN,
usage=UsageInfo(input_tokens=10, output_tokens=8, total_tokens=18),
),
required_fields={"model", "messages", "system", "max_tokens", "content", "stop_reason"},
)
# ---------------------------------------------------------------------------
# multi_turn: multi-turn conversation
# ---------------------------------------------------------------------------
MULTI_TURN = GoldenFixture(
fixture_id="multi_turn",
description="Multi-turn conversation with user/assistant alternation",
internal_request=InternalRequest(
model=_MODEL,
messages=[
InternalMessage(role=Role.USER, content=[TextBlock(text="What is 2+2?")]),
InternalMessage(role=Role.ASSISTANT, content=[TextBlock(text="4")]),
InternalMessage(role=Role.USER, content=[TextBlock(text="And 3+3?")]),
],
instructions=[InstructionSegment(role=Role.SYSTEM, text=_SYSTEM)],
system=_SYSTEM,
max_tokens=1024,
stream=False,
),
internal_response=InternalResponse(
id="resp_002",
model=_MODEL,
content=[TextBlock(text="6")],
stop_reason=StopReason.END_TURN,
usage=UsageInfo(input_tokens=20, output_tokens=1, total_tokens=21),
),
required_fields={"model", "messages", "system", "content", "stop_reason"},
)
# ---------------------------------------------------------------------------
# tool_use: tool call + tool result
# ---------------------------------------------------------------------------
TOOL_USE = GoldenFixture(
fixture_id="tool_use",
description="Single tool call with result in conversation history",
internal_request=InternalRequest(
model=_MODEL,
messages=[
InternalMessage(
role=Role.USER, content=[TextBlock(text="What is the weather in Tokyo?")]
),
InternalMessage(
role=Role.ASSISTANT,
content=[
TextBlock(text="Let me check the weather for you."),
ToolUseBlock(
tool_id=_TOOL_ID,
tool_name="get_weather",
tool_input={"location": "Tokyo"},
),
],
),
InternalMessage(
role=Role.USER,
content=[
ToolResultBlock(
tool_use_id=_TOOL_ID,
content_text='{"temperature": 22, "condition": "sunny"}',
),
],
),
InternalMessage(role=Role.USER, content=[TextBlock(text="Thanks!")]),
],
instructions=[InstructionSegment(role=Role.SYSTEM, text=_SYSTEM)],
system=_SYSTEM,
max_tokens=1024,
stream=False,
tools=[_TOOL_DEF],
),
internal_response=InternalResponse(
id="resp_003",
model=_MODEL,
content=[TextBlock(text="The weather in Tokyo is 22C and sunny.")],
stop_reason=StopReason.END_TURN,
usage=UsageInfo(input_tokens=50, output_tokens=12, total_tokens=62),
),
required_fields={"model", "messages", "system", "tools", "content", "stop_reason"},
)
# ---------------------------------------------------------------------------
# tool_use_response: response that contains a tool call (not end_turn)
# ---------------------------------------------------------------------------
TOOL_USE_RESPONSE = GoldenFixture(
fixture_id="tool_use_response",
description="Response that is a tool call (stop_reason=tool_use)",
internal_request=InternalRequest(
model=_MODEL,
messages=[
InternalMessage(
role=Role.USER, content=[TextBlock(text="What is the weather in Tokyo?")]
),
],
instructions=[InstructionSegment(role=Role.SYSTEM, text=_SYSTEM)],
system=_SYSTEM,
max_tokens=1024,
stream=False,
tools=[_TOOL_DEF],
),
internal_response=InternalResponse(
id="resp_004",
model=_MODEL,
content=[
ToolUseBlock(
tool_id=_TOOL_ID,
tool_name="get_weather",
tool_input={"location": "Tokyo"},
),
],
stop_reason=StopReason.TOOL_USE,
usage=UsageInfo(input_tokens=30, output_tokens=15, total_tokens=45),
),
required_fields={"model", "messages", "tools", "content", "stop_reason"},
)
# ---------------------------------------------------------------------------
# thinking: response with thinking block
# ---------------------------------------------------------------------------
THINKING = GoldenFixture(
fixture_id="thinking",
description="Response with thinking/reasoning content",
internal_request=InternalRequest(
model=_MODEL,
messages=[
InternalMessage(role=Role.USER, content=[TextBlock(text="Solve: 15 * 23")]),
],
instructions=[InstructionSegment(role=Role.SYSTEM, text=_SYSTEM)],
system=_SYSTEM,
max_tokens=2048,
stream=False,
),
internal_response=InternalResponse(
id="resp_005",
model=_MODEL,
content=[
ThinkingBlock(thinking="15 * 23 = 15 * 20 + 15 * 3 = 300 + 45 = 345"),
TextBlock(text="345"),
],
stop_reason=StopReason.END_TURN,
usage=UsageInfo(input_tokens=15, output_tokens=20, total_tokens=35),
),
required_fields={"model", "messages", "content", "stop_reason"},
)
# ---------------------------------------------------------------------------
# image_url: image input via URL
# ---------------------------------------------------------------------------
IMAGE_URL = GoldenFixture(
fixture_id="image_url",
description="Image input via URL",
internal_request=InternalRequest(
model=_MODEL,
messages=[
InternalMessage(
role=Role.USER,
content=[
ImageBlock(url="https://example.com/image.png", media_type="image/png"),
TextBlock(text="What is in this image?"),
],
),
],
instructions=[InstructionSegment(role=Role.SYSTEM, text=_SYSTEM)],
system=_SYSTEM,
max_tokens=1024,
stream=False,
),
internal_response=InternalResponse(
id="resp_006",
model=_MODEL,
content=[TextBlock(text="I see a cat.")],
stop_reason=StopReason.END_TURN,
usage=UsageInfo(input_tokens=100, output_tokens=5, total_tokens=105),
),
required_fields={"model", "messages", "content", "stop_reason"},
)
# ---------------------------------------------------------------------------
# image_base64: image input via base64
# ---------------------------------------------------------------------------
IMAGE_BASE64 = GoldenFixture(
fixture_id="image_base64",
description="Image input via base64 data",
internal_request=InternalRequest(
model=_MODEL,
messages=[
InternalMessage(
role=Role.USER,
content=[
ImageBlock(data="iVBORw0KGgo=", media_type="image/png"),
TextBlock(text="Describe this image."),
],
),
],
instructions=[InstructionSegment(role=Role.SYSTEM, text=_SYSTEM)],
system=_SYSTEM,
max_tokens=1024,
stream=False,
),
internal_response=InternalResponse(
id="resp_007",
model=_MODEL,
content=[TextBlock(text="A small icon.")],
stop_reason=StopReason.END_TURN,
usage=UsageInfo(input_tokens=80, output_tokens=3, total_tokens=83),
),
required_fields={"model", "messages", "content", "stop_reason"},
)
# ---------------------------------------------------------------------------
# empty_response: response with no content
# ---------------------------------------------------------------------------
EMPTY_RESPONSE = GoldenFixture(
fixture_id="empty_response",
description="Empty response (no content blocks)",
internal_request=InternalRequest(
model=_MODEL,
messages=[
InternalMessage(role=Role.USER, content=[TextBlock(text="Say nothing.")]),
],
instructions=[InstructionSegment(role=Role.SYSTEM, text=_SYSTEM)],
system=_SYSTEM,
max_tokens=1024,
stream=False,
),
internal_response=InternalResponse(
id="resp_008",
model=_MODEL,
content=[], # Normalizers typically drop empty text blocks
stop_reason=StopReason.END_TURN,
usage=UsageInfo(input_tokens=10, output_tokens=0, total_tokens=10),
),
required_fields={"model", "stop_reason"},
)
# ---------------------------------------------------------------------------
# tool_choice_auto: tool_choice=auto
# ---------------------------------------------------------------------------
TOOL_CHOICE_AUTO = GoldenFixture(
fixture_id="tool_choice_auto",
description="Request with tool_choice=auto",
internal_request=InternalRequest(
model=_MODEL,
messages=[
InternalMessage(role=Role.USER, content=[TextBlock(text="Help me.")]),
],
system=_SYSTEM,
max_tokens=1024,
stream=False,
tools=[_TOOL_DEF],
tool_choice=ToolChoice(type=ToolChoiceType.AUTO),
),
internal_response=InternalResponse(
id="resp_009",
model=_MODEL,
content=[TextBlock(text="Sure!")],
stop_reason=StopReason.END_TURN,
),
required_fields={"model", "messages", "tools", "tool_choice"},
)
# ---------------------------------------------------------------------------
# Registry of all golden fixtures
# ---------------------------------------------------------------------------
ALL_GOLDEN_FIXTURES: dict[str, GoldenFixture] = {
f.fixture_id: f
for f in [
SIMPLE_TEXT,
MULTI_TURN,
TOOL_USE,
TOOL_USE_RESPONSE,
THINKING,
IMAGE_URL,
IMAGE_BASE64,
EMPTY_RESPONSE,
TOOL_CHOICE_AUTO,
]
}
# Fixture IDs that all formats must support (core scenarios)
CORE_FIXTURE_IDS = ["simple_text", "multi_turn", "tool_use", "empty_response"]
# Fixture IDs for extended scenarios (some formats may not support)
EXTENDED_FIXTURE_IDS = [
"tool_use_response",
"thinking",
"image_url",
"image_base64",
"tool_choice_auto",
]
ALL_FIXTURE_IDS = CORE_FIXTURE_IDS + EXTENDED_FIXTURE_IDS
# ---------------------------------------------------------------------------
# Known normalizer limitations for extended fixtures.
#
# Maps (format_id, fixture_id, test_layer) -> reason string.
# test_layer: "to_internal", "from_internal", "roundtrip", "cross_request", "cross_response"
#
# These are documented limitations of the current normalizer implementations,
# NOT bugs to fix. Tests will skip these combinations.
# ---------------------------------------------------------------------------
KNOWN_LIMITATIONS: dict[tuple[str, str, str], str] = {}
# Formats where response_to_internal loses ThinkingBlock (source limitation)
_THINKING_RESPONSE_LOSSY_SOURCES = {"openai:cli"}
# Fixtures where the response's thinking block is lost when target format
# doesn't support ThinkingBlock in non-streaming responses.
_THINKING_RESPONSE_LOSSY_TARGETS = {"openai:cli"}
def is_cross_format_limited(
source: str,
target: str,
fixture_id: str,
layer: str,
) -> str | None:
"""Return a reason string if this cross-format combo is a known limitation, else None."""
# thinking response: openai:cli doesn't support ThinkingBlock in non-streaming
if fixture_id == "thinking" and layer == "cross_response":
if source in _THINKING_RESPONSE_LOSSY_SOURCES:
return f"{source} does not parse thinking content into ThinkingBlock"
if target in _THINKING_RESPONSE_LOSSY_TARGETS:
return f"{target} does not preserve ThinkingBlock in non-streaming responses"
return None
File diff suppressed because it is too large Load Diff
@@ -1,504 +0,0 @@
"""
Stream fixtures for each format.
Each fixture defines a sequence of format-specific SSE chunks and the
expected internal stream events / final text they should produce.
"""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Any
from src.core.api_format.conversion.internal import StopReason
from .golden_internal import _MODEL
@dataclass
class StreamFixture:
"""A stream fixture for a specific format and scenario."""
chunks: list[dict[str, Any]]
expected_text: str
expected_stop_reason: StopReason
# Fields that may differ across formats
lossy_fields: set[str] = field(default_factory=set)
# ===================================================================
# Claude Chat / CLI stream chunks
# ===================================================================
_CLAUDE_STREAM_TEXT_CHUNKS: list[dict[str, Any]] = [
{
"type": "message_start",
"message": {
"id": "msg_stream_001",
"type": "message",
"role": "assistant",
"model": _MODEL,
"content": [],
"stop_reason": None,
"usage": {"input_tokens": 10, "output_tokens": 0},
},
},
{
"type": "content_block_start",
"index": 0,
"content_block": {"type": "text", "text": ""},
},
# PLACEHOLDER_DELTAS
]
# Add text deltas
_CLAUDE_STREAM_TEXT_CHUNKS.extend(
[
{
"type": "content_block_delta",
"index": 0,
"delta": {"type": "text_delta", "text": "Hello, "},
},
{
"type": "content_block_delta",
"index": 0,
"delta": {"type": "text_delta", "text": "world!"},
},
{"type": "content_block_stop", "index": 0},
{
"type": "message_delta",
"delta": {"stop_reason": "end_turn"},
"usage": {"output_tokens": 5},
},
{"type": "message_stop"},
]
)
_CLAUDE_STREAM_TEXT = StreamFixture(
chunks=_CLAUDE_STREAM_TEXT_CHUNKS,
expected_text="Hello, world!",
expected_stop_reason=StopReason.END_TURN,
)
# Claude stream tool call
_CLAUDE_STREAM_TOOL_CALL = StreamFixture(
chunks=[
{
"type": "message_start",
"message": {
"id": "msg_stream_tc_001",
"type": "message",
"role": "assistant",
"model": _MODEL,
"content": [],
"stop_reason": None,
"usage": {"input_tokens": 20, "output_tokens": 0},
},
},
{
"type": "content_block_start",
"index": 0,
"content_block": {"type": "tool_use", "id": "tool_call_s01", "name": "get_weather"},
},
{
"type": "content_block_delta",
"index": 0,
"delta": {"type": "input_json_delta", "partial_json": '{"location":'},
},
{
"type": "content_block_delta",
"index": 0,
"delta": {"type": "input_json_delta", "partial_json": ' "Tokyo"}'},
},
{"type": "content_block_stop", "index": 0},
{
"type": "message_delta",
"delta": {"stop_reason": "tool_use"},
"usage": {"output_tokens": 10},
},
{"type": "message_stop"},
],
expected_text="",
expected_stop_reason=StopReason.TOOL_USE,
)
# ===================================================================
# OpenAI Chat stream chunks
# ===================================================================
_OPENAI_CHAT_STREAM_TEXT = StreamFixture(
chunks=[
{
"id": "chatcmpl-stream-001",
"object": "chat.completion.chunk",
"model": _MODEL,
"choices": [
{
"index": 0,
"delta": {"role": "assistant", "content": ""},
"finish_reason": None,
}
],
},
{
"id": "chatcmpl-stream-001",
"object": "chat.completion.chunk",
"model": _MODEL,
"choices": [{"index": 0, "delta": {"content": "Hello, "}, "finish_reason": None}],
},
{
"id": "chatcmpl-stream-001",
"object": "chat.completion.chunk",
"model": _MODEL,
"choices": [{"index": 0, "delta": {"content": "world!"}, "finish_reason": None}],
},
{
"id": "chatcmpl-stream-001",
"object": "chat.completion.chunk",
"model": _MODEL,
"choices": [{"index": 0, "delta": {}, "finish_reason": "stop"}],
},
],
expected_text="Hello, world!",
expected_stop_reason=StopReason.END_TURN,
)
# OpenAI Chat stream tool call
_OPENAI_CHAT_STREAM_TOOL_CALL = StreamFixture(
chunks=[
{
"id": "chatcmpl-stream-tc-001",
"object": "chat.completion.chunk",
"model": _MODEL,
"choices": [
{
"index": 0,
"delta": {
"role": "assistant",
"content": None,
"tool_calls": [
{
"index": 0,
"id": "call_tc_001",
"type": "function",
"function": {"name": "get_weather", "arguments": ""},
}
],
},
"finish_reason": None,
}
],
},
{
"id": "chatcmpl-stream-tc-001",
"object": "chat.completion.chunk",
"model": _MODEL,
"choices": [
{
"index": 0,
"delta": {
"tool_calls": [{"index": 0, "function": {"arguments": '{"location":'}}]
},
"finish_reason": None,
}
],
},
{
"id": "chatcmpl-stream-tc-001",
"object": "chat.completion.chunk",
"model": _MODEL,
"choices": [
{
"index": 0,
"delta": {"tool_calls": [{"index": 0, "function": {"arguments": ' "Tokyo"}'}}]},
"finish_reason": None,
}
],
},
{
"id": "chatcmpl-stream-tc-001",
"object": "chat.completion.chunk",
"model": _MODEL,
"choices": [{"index": 0, "delta": {}, "finish_reason": "tool_calls"}],
},
],
expected_text="",
expected_stop_reason=StopReason.TOOL_USE,
)
# ===================================================================
# OpenAI CLI (Responses API) stream chunks
# ===================================================================
_OPENAI_CLI_STREAM_TEXT = StreamFixture(
chunks=[
{
"type": "response.created",
"response": {
"id": "resp_stream_001",
"object": "response",
"model": _MODEL,
"status": "in_progress",
"output": [],
},
},
{
"type": "response.output_item.added",
"output_index": 0,
"item": {
"type": "message",
"id": "msg_stream_001",
"role": "assistant",
"status": "in_progress",
"content": [],
},
},
{
"type": "response.content_part.added",
"output_index": 0,
"content_index": 0,
"part": {"type": "output_text", "text": ""},
},
{
"type": "response.output_text.delta",
"output_index": 0,
"content_index": 0,
"delta": "Hello, ",
},
{
"type": "response.output_text.delta",
"output_index": 0,
"content_index": 0,
"delta": "world!",
},
{
"type": "response.output_text.done",
"output_index": 0,
"content_index": 0,
"text": "Hello, world!",
},
{
"type": "response.output_item.done",
"output_index": 0,
"item": {
"type": "message",
"id": "msg_stream_001",
"role": "assistant",
"status": "completed",
"content": [{"type": "output_text", "text": "Hello, world!"}],
},
},
{
"type": "response.completed",
"response": {
"id": "resp_stream_001",
"object": "response",
"model": _MODEL,
"status": "completed",
"output": [
{
"type": "message",
"id": "msg_stream_001",
"role": "assistant",
"status": "completed",
"content": [{"type": "output_text", "text": "Hello, world!"}],
}
],
"usage": {"input_tokens": 10, "output_tokens": 5, "total_tokens": 15},
},
},
],
expected_text="Hello, world!",
expected_stop_reason=StopReason.END_TURN,
)
# OpenAI CLI stream tool call
_OPENAI_CLI_STREAM_TOOL_CALL = StreamFixture(
chunks=[
{
"type": "response.created",
"response": {
"id": "resp_stream_tc_001",
"object": "response",
"model": _MODEL,
"status": "in_progress",
"output": [],
},
},
{
"type": "response.output_item.added",
"output_index": 0,
"item": {
"type": "function_call",
"call_id": "fc_001",
"id": "fc_001",
"name": "get_weather",
"status": "in_progress",
"arguments": "",
},
},
{
"type": "response.function_call_arguments.delta",
"output_index": 0,
"item_id": "fc_001",
"delta": '{"location":',
},
{
"type": "response.function_call_arguments.delta",
"output_index": 0,
"item_id": "fc_001",
"delta": ' "Tokyo"}',
},
{
"type": "response.function_call_arguments.done",
"output_index": 0,
"item_id": "fc_001",
"arguments": '{"location": "Tokyo"}',
},
{
"type": "response.output_item.done",
"output_index": 0,
"item": {
"type": "function_call",
"call_id": "fc_001",
"id": "fc_001",
"name": "get_weather",
"status": "completed",
"arguments": '{"location": "Tokyo"}',
},
},
{
"type": "response.completed",
"response": {
"id": "resp_stream_tc_001",
"object": "response",
"model": _MODEL,
"status": "completed",
"output": [
{
"type": "function_call",
"call_id": "fc_001",
"id": "fc_001",
"name": "get_weather",
"status": "completed",
"arguments": '{"location": "Tokyo"}',
}
],
"usage": {"input_tokens": 20, "output_tokens": 10, "total_tokens": 30},
},
},
],
expected_text="",
expected_stop_reason=StopReason.TOOL_USE,
)
# ===================================================================
# Gemini Chat / CLI stream chunks
# ===================================================================
_GEMINI_STREAM_TEXT = StreamFixture(
chunks=[
{
"candidates": [
{
"content": {"role": "model", "parts": [{"text": "Hello, "}]},
"index": 0,
}
],
"modelVersion": _MODEL,
},
{
"candidates": [
{
"content": {"role": "model", "parts": [{"text": "world!"}]},
"index": 0,
"finishReason": "STOP",
}
],
"usageMetadata": {
"promptTokenCount": 10,
"candidatesTokenCount": 5,
"totalTokenCount": 15,
},
"modelVersion": _MODEL,
},
],
expected_text="Hello, world!",
expected_stop_reason=StopReason.END_TURN,
)
# Gemini stream tool call (Gemini emits complete tool calls atomically)
_GEMINI_STREAM_TOOL_CALL = StreamFixture(
chunks=[
{
"candidates": [
{
"content": {
"role": "model",
"parts": [
{
"functionCall": {
"name": "get_weather",
"args": {"location": "Tokyo"},
}
},
],
},
"index": 0,
"finishReason": "STOP",
}
],
"usageMetadata": {
"promptTokenCount": 20,
"candidatesTokenCount": 10,
"totalTokenCount": 30,
},
"modelVersion": _MODEL,
},
],
expected_text="",
expected_stop_reason=StopReason.END_TURN,
)
# ===================================================================
# Registry
# ===================================================================
STREAM_FIXTURES: dict[str, dict[str, StreamFixture]] = {
"claude:chat": {
"stream_text": _CLAUDE_STREAM_TEXT,
"stream_tool_call": _CLAUDE_STREAM_TOOL_CALL,
},
"claude:cli": {
"stream_text": _CLAUDE_STREAM_TEXT,
"stream_tool_call": _CLAUDE_STREAM_TOOL_CALL,
},
"openai:chat": {
"stream_text": _OPENAI_CHAT_STREAM_TEXT,
"stream_tool_call": _OPENAI_CHAT_STREAM_TOOL_CALL,
},
"openai:cli": {
"stream_text": _OPENAI_CLI_STREAM_TEXT,
"stream_tool_call": _OPENAI_CLI_STREAM_TOOL_CALL,
},
"gemini:chat": {
"stream_text": _GEMINI_STREAM_TEXT,
"stream_tool_call": _GEMINI_STREAM_TOOL_CALL,
},
"gemini:cli": {
"stream_text": _GEMINI_STREAM_TEXT,
"stream_tool_call": _GEMINI_STREAM_TOOL_CALL,
},
}
STREAM_FIXTURE_IDS = ["stream_text", "stream_tool_call"]
STREAM_ALL_FORMATS = list(STREAM_FIXTURES.keys())
@@ -1 +0,0 @@
@@ -1,55 +0,0 @@
{
"contents": [
{
"parts": [
{
"text": "hi"
}
],
"role": "user"
}
],
"generation_config": {
"max_output_tokens": 12,
"stop_sequences": [
"A",
"B"
],
"temperature": 0.2,
"top_p": 1.0
},
"model": "claude-3-5-sonnet-latest",
"system_instruction": {
"parts": [
{
"text": "sys\n\ndev"
}
]
},
"tool_config": {
"function_calling_config": {
"mode": "AUTO"
}
},
"tools": [
{
"function_declarations": [
{
"description": "Get weather",
"name": "get_weather",
"parameters": {
"properties": {
"city": {
"type": "string"
}
},
"required": [
"city"
],
"type": "object"
}
}
]
}
]
}
@@ -1,45 +0,0 @@
{
"max_tokens": 12,
"messages": [
{
"content": "sys\n\ndev",
"role": "system"
},
{
"content": "hi",
"role": "user"
}
],
"model": "claude-3-5-sonnet-latest",
"stop": [
"A",
"B"
],
"stream": true,
"stream_options": {
"include_usage": true
},
"temperature": 0.2,
"tool_choice": "auto",
"tools": [
{
"function": {
"description": "Get weather",
"name": "get_weather",
"parameters": {
"properties": {
"city": {
"type": "string"
}
},
"required": [
"city"
],
"type": "object"
}
},
"type": "function"
}
],
"top_p": 1.0
}
@@ -1,37 +0,0 @@
{
"max_tokens": 12,
"messages": [
{
"content": "hi",
"role": "user"
}
],
"model": "gemini-1.5-flash",
"stop_sequences": [
"A",
"B"
],
"system": "sys\n\ndev",
"temperature": 0.2,
"tool_choice": {
"type": "auto"
},
"tools": [
{
"description": "Get weather",
"input_schema": {
"properties": {
"city": {
"type": "string"
}
},
"required": [
"city"
],
"type": "object"
},
"name": "get_weather"
}
],
"top_p": 1.0
}
@@ -1,41 +0,0 @@
{
"max_tokens": 12,
"messages": [
{
"content": "sys\n\ndev",
"role": "system"
},
{
"content": "hi",
"role": "user"
}
],
"model": "gemini-1.5-flash",
"stop": [
"A",
"B"
],
"temperature": 0.2,
"tool_choice": "auto",
"tools": [
{
"function": {
"description": "Get weather",
"name": "get_weather",
"parameters": {
"properties": {
"city": {
"type": "string"
}
},
"required": [
"city"
],
"type": "object"
}
},
"type": "function"
}
],
"top_p": 1.0
}
@@ -1,38 +0,0 @@
{
"max_tokens": 12,
"messages": [
{
"content": "hi",
"role": "user"
}
],
"model": "gpt-4o-mini",
"stop_sequences": [
"A",
"B"
],
"stream": true,
"system": "sys\n\ndev",
"temperature": 0.2,
"tool_choice": {
"type": "auto"
},
"tools": [
{
"description": "Get weather",
"input_schema": {
"properties": {
"city": {
"type": "string"
}
},
"required": [
"city"
],
"type": "object"
},
"name": "get_weather"
}
],
"top_p": 1.0
}
@@ -1,55 +0,0 @@
{
"contents": [
{
"parts": [
{
"text": "hi"
}
],
"role": "user"
}
],
"generation_config": {
"max_output_tokens": 12,
"stop_sequences": [
"A",
"B"
],
"temperature": 0.2,
"top_p": 1.0
},
"model": "gpt-4o-mini",
"system_instruction": {
"parts": [
{
"text": "sys\n\ndev"
}
]
},
"tool_config": {
"function_calling_config": {
"mode": "AUTO"
}
},
"tools": [
{
"function_declarations": [
{
"description": "Get weather",
"name": "get_weather",
"parameters": {
"properties": {
"city": {
"type": "string"
}
},
"required": [
"city"
],
"type": "object"
}
}
]
}
]
}
@@ -1,23 +0,0 @@
{
"candidates": [
{
"content": {
"parts": [
{
"text": "hello"
}
],
"role": "model"
},
"finishReason": "STOP",
"index": 0
}
],
"id": "msg_1",
"modelVersion": "claude-3-5-sonnet-latest",
"usageMetadata": {
"candidatesTokenCount": 7,
"promptTokenCount": 5,
"totalTokenCount": 12
}
}
@@ -1,20 +0,0 @@
{
"choices": [
{
"finish_reason": "stop",
"index": 0,
"message": {
"content": "hello",
"role": "assistant"
}
}
],
"id": "msg_1",
"model": "claude-3-5-sonnet-latest",
"object": "chat.completion",
"usage": {
"completion_tokens": 7,
"prompt_tokens": 5,
"total_tokens": 12
}
}
@@ -1,18 +0,0 @@
{
"content": [
{
"text": "hello",
"type": "text"
}
],
"id": "msg_unknown",
"model": "gemini-1.5-flash",
"role": "assistant",
"stop_reason": "end_turn",
"stop_sequence": null,
"type": "message",
"usage": {
"input_tokens": 5,
"output_tokens": 7
}
}
@@ -1,20 +0,0 @@
{
"choices": [
{
"finish_reason": "stop",
"index": 0,
"message": {
"content": "hello",
"role": "assistant"
}
}
],
"id": "chatcmpl-unknown",
"model": "gemini-1.5-flash",
"object": "chat.completion",
"usage": {
"completion_tokens": 7,
"prompt_tokens": 5,
"total_tokens": 12
}
}
@@ -1,18 +0,0 @@
{
"content": [
{
"text": "hello",
"type": "text"
}
],
"id": "msg_chatcmpl_1",
"model": "gpt-4o-mini",
"role": "assistant",
"stop_reason": "end_turn",
"stop_sequence": null,
"type": "message",
"usage": {
"input_tokens": 5,
"output_tokens": 7
}
}
@@ -1,23 +0,0 @@
{
"candidates": [
{
"content": {
"parts": [
{
"text": "hello"
}
],
"role": "model"
},
"finishReason": "STOP",
"index": 0
}
],
"id": "chatcmpl_1",
"modelVersion": "gpt-4o-mini",
"usageMetadata": {
"candidatesTokenCount": 7,
"promptTokenCount": 5,
"totalTokenCount": 12
}
}
@@ -1,36 +0,0 @@
[
{
"candidates": [
{
"content": {
"parts": [
{
"text": "hello"
}
],
"role": "model"
},
"index": 0
}
],
"modelVersion": "claude-3-5-sonnet-latest"
},
{
"candidates": [
{
"content": {
"parts": [],
"role": "model"
},
"finishReason": "STOP",
"index": 0
}
],
"modelVersion": "claude-3-5-sonnet-latest",
"usageMetadata": {
"candidatesTokenCount": 7,
"promptTokenCount": 5,
"totalTokenCount": 12
}
}
]
@@ -1,42 +0,0 @@
[
{
"choices": [
{
"delta": {
"role": "assistant"
},
"finish_reason": null,
"index": 0
}
],
"id": "msg_1",
"model": "claude-3-5-sonnet-latest",
"object": "chat.completion.chunk"
},
{
"choices": [
{
"delta": {
"content": "hello"
},
"finish_reason": null,
"index": 0
}
],
"id": "msg_1",
"model": "claude-3-5-sonnet-latest",
"object": "chat.completion.chunk"
},
{
"choices": [
{
"delta": {},
"finish_reason": "stop",
"index": 0
}
],
"id": "msg_1",
"model": "claude-3-5-sonnet-latest",
"object": "chat.completion.chunk"
}
]

Some files were not shown because too many files have changed in this diff Show More