mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 17:30:23 +08:00
- 删除 aether-hub、aether-proxy 独立项目及其 Dockerfile/配置 - 删除 crates/aether-executor 和 crates/aether-gateway 全部模块 - 新增 apps/ 目录作为应用入口 - 将 hub 概念重构为 gateway tunnel transport - 将 executor 重构为 execution runtime - 新增 tunnel.rs 合约定义和 testkit tunnel/execution_runtime 模块 - 更新 Python 服务层和测试适配新架构命名
3978 lines
141 KiB
Python
3978 lines
141 KiB
Python
"""Gateway internal route/control/dispatch 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 _make_internal_test_client(monkeypatch: pytest.MonkeyPatch) -> TestClient:
|
|
monkeypatch.setattr("src.api.internal.gateway.ensure_loopback", lambda request: None)
|
|
app = FastAPI()
|
|
app.include_router(router)
|
|
app.dependency_overrides[get_db] = lambda: object()
|
|
return TestClient(app, base_url="http://127.0.0.1")
|
|
|
|
|
|
def _assert_legacy_guard_response(response) -> None:
|
|
assert response.status_code == 410
|
|
assert response.json() == {
|
|
"detail": "legacy internal gateway route removed; use public proxy"
|
|
}
|
|
|
|
|
|
def test_build_gateway_sync_telemetry_writer_uses_queue_writer_when_enabled(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
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",
|
|
"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",
|
|
"access_allowed": True,
|
|
},
|
|
},
|
|
)
|
|
|
|
assert response.status_code == 409
|
|
assert response.headers[CONTROL_ACTION_HEADER] == CONTROL_ACTION_PROXY_PUBLIC
|
|
assert response.json() == {"action": CONTROL_ACTION_PROXY_PUBLIC}
|
|
monkeypatch.undo()
|
|
|
|
|
|
def test_execute_stream_route_returns_controlled_stream(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
app = FastAPI()
|
|
app.include_router(router)
|
|
app.dependency_overrides[get_db] = lambda: object()
|
|
monkeypatch.setattr("src.api.internal.gateway.ensure_loopback", lambda request: None)
|
|
|
|
class FakeAdapter:
|
|
mode = SimpleNamespace(value="standard")
|
|
allowed_api_formats = ["openai:chat"]
|
|
|
|
def authorize(self, context: object) -> None:
|
|
self.authorized_context = context
|
|
|
|
async def handle(self, context: object) -> StreamingResponse:
|
|
async def _iter() -> object:
|
|
yield b"data: one\n\n"
|
|
yield b"data: [DONE]\n\n"
|
|
|
|
return StreamingResponse(_iter(), media_type="text/event-stream")
|
|
|
|
fake_pipeline = SimpleNamespace(_check_user_rate_limit=AsyncMock(return_value=None))
|
|
monkeypatch.setattr(
|
|
"src.api.internal.gateway._resolve_gateway_sync_adapter",
|
|
lambda decision, path: (FakeAdapter(), {}),
|
|
)
|
|
monkeypatch.setattr(
|
|
"src.api.internal.gateway._load_gateway_auth_models",
|
|
lambda db, auth_context: (
|
|
SimpleNamespace(id="user-123"),
|
|
SimpleNamespace(id="key-123"),
|
|
),
|
|
)
|
|
monkeypatch.setattr("src.api.internal.gateway.get_pipeline", lambda: fake_pipeline)
|
|
|
|
client = _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"
|
|
|
|
def test_plan_sync_route_executes_control_when_direct_plan_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)
|
|
monkeypatch.setattr(
|
|
"src.api.internal.gateway._build_gateway_sync_plan_response",
|
|
AsyncMock(return_value=None),
|
|
)
|
|
execute_mock = AsyncMock(
|
|
return_value=JSONResponse(
|
|
status_code=202,
|
|
content={"ok": True, "via": "plan-sync"},
|
|
headers={CONTROL_EXECUTED_HEADER: "true"},
|
|
)
|
|
)
|
|
monkeypatch.setattr(
|
|
"src.api.internal.gateway._execute_gateway_control_request",
|
|
execute_mock,
|
|
)
|
|
|
|
client = _make_legacy_test_client(app)
|
|
response = client.post(
|
|
"/api/internal/gateway/plan-sync",
|
|
json={
|
|
"trace_id": "trace-plan-sync-control-123",
|
|
"method": "POST",
|
|
"path": "/v1/chat/completions",
|
|
"headers": {"content-type": "application/json"},
|
|
"body_json": {"model": "gpt-5", "messages": []},
|
|
},
|
|
)
|
|
|
|
assert response.status_code == 202
|
|
assert response.headers[CONTROL_EXECUTED_HEADER] == "true"
|
|
assert response.json() == {"ok": True, "via": "plan-sync"}
|
|
execute_mock.assert_awaited_once()
|
|
|
|
|
|
def test_plan_stream_route_executes_control_when_direct_plan_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)
|
|
monkeypatch.setattr(
|
|
"src.api.internal.gateway._build_gateway_stream_plan_response",
|
|
AsyncMock(return_value=None),
|
|
)
|
|
execute_mock = AsyncMock(
|
|
return_value=StreamingResponse(
|
|
iter([b"data: one\n\n", b"data: [DONE]\n\n"]),
|
|
media_type="text/event-stream",
|
|
headers={CONTROL_EXECUTED_HEADER: "true"},
|
|
)
|
|
)
|
|
monkeypatch.setattr(
|
|
"src.api.internal.gateway._execute_gateway_control_request",
|
|
execute_mock,
|
|
)
|
|
|
|
client = _make_legacy_test_client(app)
|
|
response = client.post(
|
|
"/api/internal/gateway/plan-stream",
|
|
json={
|
|
"trace_id": "trace-plan-stream-control-123",
|
|
"method": "POST",
|
|
"path": "/v1/chat/completions",
|
|
"headers": {"content-type": "application/json"},
|
|
"body_json": {"model": "gpt-5", "messages": [], "stream": True},
|
|
},
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
assert response.headers[CONTROL_EXECUTED_HEADER] == "true"
|
|
assert response.text == "data: one\n\ndata: [DONE]\n\n"
|
|
execute_mock.assert_awaited_once()
|
|
|
|
|
|
def test_plan_sync_route_requires_legacy_header(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
client = _make_internal_test_client(monkeypatch)
|
|
response = client.post(
|
|
"/api/internal/gateway/plan-sync",
|
|
json={
|
|
"trace_id": "trace-guard-plan-sync",
|
|
"method": "POST",
|
|
"path": "/v1/chat/completions",
|
|
"headers": {"content-type": "application/json"},
|
|
"body_json": {"model": "gpt-5", "messages": []},
|
|
},
|
|
)
|
|
|
|
_assert_legacy_guard_response(response)
|
|
|
|
|
|
def test_plan_stream_route_requires_legacy_header(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
client = _make_internal_test_client(monkeypatch)
|
|
response = client.post(
|
|
"/api/internal/gateway/plan-stream",
|
|
json={
|
|
"trace_id": "trace-guard-plan-stream",
|
|
"method": "POST",
|
|
"path": "/v1/chat/completions",
|
|
"headers": {"content-type": "application/json"},
|
|
"body_json": {"model": "gpt-5", "messages": [], "stream": True},
|
|
},
|
|
)
|
|
|
|
_assert_legacy_guard_response(response)
|
|
|
|
|
|
def test_report_sync_route_requires_legacy_header(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
client = _make_internal_test_client(monkeypatch)
|
|
response = client.post(
|
|
"/api/internal/gateway/report-sync",
|
|
json={
|
|
"trace_id": "trace-guard-report-sync",
|
|
"report_kind": "openai_chat_sync_finalize",
|
|
"report_context": {"user_id": "user-123", "api_key_id": "key-123"},
|
|
"status_code": 200,
|
|
"headers": {"content-type": "application/json"},
|
|
},
|
|
)
|
|
|
|
_assert_legacy_guard_response(response)
|
|
|
|
|
|
def test_finalize_sync_route_requires_legacy_header(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
client = _make_internal_test_client(monkeypatch)
|
|
response = client.post(
|
|
"/api/internal/gateway/finalize-sync",
|
|
json={
|
|
"trace_id": "trace-guard-finalize-sync",
|
|
"report_kind": "openai_chat_sync_finalize",
|
|
"report_context": {"user_id": "user-123", "api_key_id": "key-123"},
|
|
"status_code": 200,
|
|
"headers": {"content-type": "application/json"},
|
|
},
|
|
)
|
|
|
|
_assert_legacy_guard_response(response)
|
|
|
|
|
|
def test_execute_stream_route_falls_back_for_sync_payload(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
app = FastAPI()
|
|
app.include_router(router)
|
|
app.dependency_overrides[get_db] = lambda: object()
|
|
monkeypatch.setattr("src.api.internal.gateway.ensure_loopback", lambda request: None)
|
|
|
|
client = _make_legacy_test_client(app)
|
|
response = client.post(
|
|
"/api/internal/gateway/execute-stream",
|
|
json={
|
|
"trace_id": "trace-sync-456",
|
|
"method": "POST",
|
|
"path": "/v1/chat/completions",
|
|
"headers": {"user-agent": "pytest"},
|
|
"body_json": {"model": "gpt-5", "messages": []},
|
|
"auth_context": {
|
|
"user_id": "user-123",
|
|
"api_key_id": "key-123",
|
|
"access_allowed": True,
|
|
},
|
|
},
|
|
)
|
|
|
|
assert response.status_code == 410
|
|
assert response.json() == {
|
|
"detail": "legacy internal gateway route removed; use public proxy"
|
|
}
|
|
|
|
|
|
def test_execute_sync_route_handles_gemini_files_list(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
app = FastAPI()
|
|
app.include_router(router)
|
|
app.dependency_overrides[get_db] = lambda: object()
|
|
monkeypatch.setattr("src.api.internal.gateway.ensure_loopback", lambda request: None)
|
|
monkeypatch.setattr(
|
|
"src.api.internal.gateway._load_gateway_auth_models",
|
|
lambda db, auth_context: (
|
|
SimpleNamespace(id="user-123"),
|
|
SimpleNamespace(id="key-123"),
|
|
),
|
|
)
|
|
monkeypatch.setattr(
|
|
"src.api.internal.gateway.get_pipeline",
|
|
lambda: SimpleNamespace(_check_user_rate_limit=AsyncMock(return_value=None)),
|
|
)
|
|
|
|
async def _fake_list_files(
|
|
request: object,
|
|
pageSize: int | None = None,
|
|
pageToken: str | None = None,
|
|
) -> JSONResponse:
|
|
del request
|
|
assert pageSize == 25
|
|
assert pageToken == "page-2"
|
|
return JSONResponse(status_code=200, content={"files": [{"name": "files/abc"}]})
|
|
|
|
monkeypatch.setattr("src.api.public.gemini_files.list_files", _fake_list_files)
|
|
|
|
client = _make_legacy_test_client(app)
|
|
response = client.post(
|
|
"/api/internal/gateway/execute-sync",
|
|
json={
|
|
"trace_id": "trace-files-list",
|
|
"method": "GET",
|
|
"path": "/v1beta/files",
|
|
"query_string": "pageSize=25&pageToken=page-2",
|
|
"headers": {"x-goog-api-key": "client-key"},
|
|
"auth_context": {
|
|
"user_id": "user-123",
|
|
"api_key_id": "key-123",
|
|
"access_allowed": True,
|
|
},
|
|
},
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
assert response.headers[CONTROL_EXECUTED_HEADER] == "true"
|
|
assert response.json() == {"files": [{"name": "files/abc"}]}
|
|
|
|
|
|
def test_execute_sync_route_handles_gemini_files_upload_raw_body(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
app = FastAPI()
|
|
app.include_router(router)
|
|
app.dependency_overrides[get_db] = lambda: object()
|
|
monkeypatch.setattr("src.api.internal.gateway.ensure_loopback", lambda request: None)
|
|
monkeypatch.setattr(
|
|
"src.api.internal.gateway._load_gateway_auth_models",
|
|
lambda db, auth_context: (
|
|
SimpleNamespace(id="user-123"),
|
|
SimpleNamespace(id="key-123"),
|
|
),
|
|
)
|
|
monkeypatch.setattr(
|
|
"src.api.internal.gateway.get_pipeline",
|
|
lambda: SimpleNamespace(_check_user_rate_limit=AsyncMock(return_value=None)),
|
|
)
|
|
|
|
async def _fake_upload_file(request: object) -> JSONResponse:
|
|
body = await request.body()
|
|
assert body == b"upload-bytes"
|
|
assert request.headers["content-type"] == "application/octet-stream"
|
|
return JSONResponse(status_code=201, content={"uploaded": True})
|
|
|
|
monkeypatch.setattr("src.api.public.gemini_files.upload_file", _fake_upload_file)
|
|
|
|
client = _make_legacy_test_client(app)
|
|
response = client.post(
|
|
"/api/internal/gateway/execute-sync",
|
|
json={
|
|
"trace_id": "trace-files-upload",
|
|
"method": "POST",
|
|
"path": "/upload/v1beta/files",
|
|
"query_string": "uploadType=resumable",
|
|
"headers": {
|
|
"x-goog-api-key": "client-key",
|
|
"content-type": "application/octet-stream",
|
|
},
|
|
"body_base64": base64.b64encode(b"upload-bytes").decode("ascii"),
|
|
"auth_context": {
|
|
"user_id": "user-123",
|
|
"api_key_id": "key-123",
|
|
"access_allowed": True,
|
|
},
|
|
},
|
|
)
|
|
|
|
assert response.status_code == 201
|
|
assert response.headers[CONTROL_EXECUTED_HEADER] == "true"
|
|
assert response.json() == {"uploaded": True}
|
|
|
|
|
|
def test_execute_sync_route_handles_openai_video_remix_with_original_request(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
app = FastAPI()
|
|
app.include_router(router)
|
|
app.dependency_overrides[get_db] = lambda: object()
|
|
monkeypatch.setattr("src.api.internal.gateway.ensure_loopback", lambda request: None)
|
|
|
|
class FakeVideoAdapter:
|
|
mode = SimpleNamespace(value="standard")
|
|
allowed_api_formats = ["openai:video"]
|
|
|
|
def authorize(self, context: object) -> None:
|
|
self.authorized_context = context
|
|
|
|
async def handle(self, context: object) -> JSONResponse:
|
|
assert context.request.method == "POST"
|
|
assert context.request.url.path == "/v1/videos/task-123/remix"
|
|
assert context.path_params == {"task_id": "task-123"}
|
|
assert await context.ensure_json_body_async() == {"prompt": "remix this"}
|
|
return JSONResponse(status_code=200, content={"video": True})
|
|
|
|
monkeypatch.setattr(
|
|
"src.api.internal.gateway._resolve_gateway_sync_adapter",
|
|
lambda decision, path: (FakeVideoAdapter(), {"task_id": "task-123"}),
|
|
)
|
|
monkeypatch.setattr(
|
|
"src.api.internal.gateway._load_gateway_auth_models",
|
|
lambda db, auth_context: (
|
|
SimpleNamespace(id="user-123"),
|
|
SimpleNamespace(id="key-123"),
|
|
),
|
|
)
|
|
monkeypatch.setattr(
|
|
"src.api.internal.gateway.get_pipeline",
|
|
lambda: SimpleNamespace(_check_user_rate_limit=AsyncMock(return_value=None)),
|
|
)
|
|
|
|
client = _make_legacy_test_client(app)
|
|
response = client.post(
|
|
"/api/internal/gateway/execute-sync",
|
|
json={
|
|
"trace_id": "trace-video-remix",
|
|
"method": "POST",
|
|
"path": "/v1/videos/task-123/remix",
|
|
"headers": {
|
|
"content-type": "application/json",
|
|
"user-agent": "pytest",
|
|
},
|
|
"body_json": {"prompt": "remix this"},
|
|
"auth_context": {
|
|
"user_id": "user-123",
|
|
"api_key_id": "key-123",
|
|
"access_allowed": True,
|
|
},
|
|
},
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
assert response.headers[CONTROL_EXECUTED_HEADER] == "true"
|
|
assert response.json() == {"video": True}
|
|
|
|
|
|
def test_plan_stream_route_returns_execution_runtime_plan_for_gemini_files_download(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
app = FastAPI()
|
|
app.include_router(router)
|
|
app.dependency_overrides[get_db] = lambda: object()
|
|
monkeypatch.setattr("src.api.internal.gateway.ensure_loopback", lambda request: None)
|
|
|
|
fake_plan = {
|
|
"request_id": "plan-123",
|
|
"provider_id": "provider-123",
|
|
"endpoint_id": "endpoint-123",
|
|
"key_id": "key-123",
|
|
"method": "GET",
|
|
"url": "https://example.com/v1beta/files/file-123:download",
|
|
"headers": {"x-goog-api-key": "upstream-key"},
|
|
"body": {},
|
|
"stream": True,
|
|
"provider_api_format": "gemini:files",
|
|
"client_api_format": "gemini:files",
|
|
"model_name": "gemini-files",
|
|
}
|
|
|
|
monkeypatch.setattr(
|
|
"src.api.internal.gateway._load_gateway_auth_models",
|
|
lambda db, auth_context: (
|
|
SimpleNamespace(id="user-123"),
|
|
SimpleNamespace(id="key-123"),
|
|
),
|
|
)
|
|
monkeypatch.setattr(
|
|
"src.api.internal.gateway.get_pipeline",
|
|
lambda: SimpleNamespace(_check_user_rate_limit=AsyncMock(return_value=None)),
|
|
)
|
|
monkeypatch.setattr(
|
|
"src.api.internal.gateway._build_gemini_files_download_stream_plan",
|
|
AsyncMock(return_value=fake_plan),
|
|
)
|
|
|
|
client = _make_legacy_test_client(app)
|
|
response = client.post(
|
|
"/api/internal/gateway/plan-stream",
|
|
json={
|
|
"trace_id": "trace-files-plan",
|
|
"method": "GET",
|
|
"path": "/v1beta/files/file-123:download",
|
|
"query_string": "alt=media",
|
|
"headers": {"x-goog-api-key": "client-key"},
|
|
"auth_context": {
|
|
"user_id": "user-123",
|
|
"api_key_id": "key-123",
|
|
"access_allowed": True,
|
|
},
|
|
},
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
assert response.json() == {
|
|
"action": "executor_stream",
|
|
"plan_kind": "gemini_files_download",
|
|
"plan": fake_plan,
|
|
}
|
|
|
|
|
|
def test_plan_stream_route_returns_execution_runtime_plan_for_openai_video_content(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
app = FastAPI()
|
|
app.include_router(router)
|
|
app.dependency_overrides[get_db] = lambda: object()
|
|
monkeypatch.setattr("src.api.internal.gateway.ensure_loopback", lambda request: None)
|
|
|
|
fake_plan = {
|
|
"request_id": "plan-video-123",
|
|
"provider_id": "provider-video-123",
|
|
"endpoint_id": "endpoint-video-123",
|
|
"key_id": "key-video-123",
|
|
"method": "GET",
|
|
"url": "https://api.openai.com/v1/videos/ext-123/content",
|
|
"headers": {"authorization": "Bearer upstream-key"},
|
|
"body": {},
|
|
"stream": True,
|
|
"provider_api_format": "openai:video",
|
|
"client_api_format": "openai:video",
|
|
"model_name": "sora-2",
|
|
}
|
|
|
|
monkeypatch.setattr(
|
|
"src.api.internal.gateway._load_gateway_auth_models",
|
|
lambda db, auth_context: (
|
|
SimpleNamespace(id="user-123"),
|
|
SimpleNamespace(id="key-123"),
|
|
),
|
|
)
|
|
monkeypatch.setattr(
|
|
"src.api.internal.gateway.get_pipeline",
|
|
lambda: SimpleNamespace(_check_user_rate_limit=AsyncMock(return_value=None)),
|
|
)
|
|
monkeypatch.setattr(
|
|
"src.api.internal.gateway._build_openai_video_content_stream_plan",
|
|
AsyncMock(return_value=fake_plan),
|
|
)
|
|
|
|
client = _make_legacy_test_client(app)
|
|
response = client.post(
|
|
"/api/internal/gateway/plan-stream",
|
|
json={
|
|
"trace_id": "trace-video-plan",
|
|
"method": "GET",
|
|
"path": "/v1/videos/task-123/content",
|
|
"query_string": "variant=video",
|
|
"headers": {"authorization": "Bearer client-key"},
|
|
"auth_context": {
|
|
"user_id": "user-123",
|
|
"api_key_id": "key-123",
|
|
"access_allowed": True,
|
|
},
|
|
},
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
assert response.json() == {
|
|
"action": "executor_stream",
|
|
"plan_kind": "openai_video_content",
|
|
"plan": fake_plan,
|
|
}
|
|
|
|
|
|
def test_plan_sync_route_returns_execution_runtime_plan_for_gemini_files_get(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
app = FastAPI()
|
|
app.include_router(router)
|
|
app.dependency_overrides[get_db] = lambda: object()
|
|
monkeypatch.setattr("src.api.internal.gateway.ensure_loopback", lambda request: None)
|
|
|
|
fake_plan = {
|
|
"request_id": "plan-files-get-123",
|
|
"provider_id": "provider-files-123",
|
|
"endpoint_id": "endpoint-files-123",
|
|
"key_id": "file-key-123",
|
|
"method": "GET",
|
|
"url": "https://example.com/v1beta/files/files/abc-123",
|
|
"headers": {"x-goog-api-key": "upstream-key"},
|
|
"body": {},
|
|
"stream": False,
|
|
"provider_api_format": "gemini:files",
|
|
"client_api_format": "gemini:files",
|
|
"model_name": "gemini-files",
|
|
}
|
|
|
|
monkeypatch.setattr(
|
|
"src.api.internal.gateway._load_gateway_auth_models",
|
|
lambda db, auth_context: (
|
|
SimpleNamespace(id="user-123"),
|
|
SimpleNamespace(id="key-123"),
|
|
),
|
|
)
|
|
monkeypatch.setattr(
|
|
"src.api.internal.gateway.get_pipeline",
|
|
lambda: SimpleNamespace(_check_user_rate_limit=AsyncMock(return_value=None)),
|
|
)
|
|
monkeypatch.setattr(
|
|
"src.api.internal.gateway._build_gemini_files_get_sync_plan",
|
|
AsyncMock(return_value=(fake_plan, {"file_key_id": "file-key-123", "user_id": "user-123"})),
|
|
)
|
|
|
|
client = _make_legacy_test_client(app)
|
|
response = client.post(
|
|
"/api/internal/gateway/plan-sync",
|
|
json={
|
|
"trace_id": "trace-files-get-plan",
|
|
"method": "GET",
|
|
"path": "/v1beta/files/files/abc-123",
|
|
"query_string": "view=FULL",
|
|
"headers": {"x-goog-api-key": "client-key"},
|
|
"auth_context": {
|
|
"user_id": "user-123",
|
|
"api_key_id": "key-123",
|
|
"access_allowed": True,
|
|
},
|
|
},
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
assert response.json() == {
|
|
"action": "executor_sync",
|
|
"plan_kind": "gemini_files_get",
|
|
"plan": fake_plan,
|
|
"report_kind": "gemini_files_store_mapping",
|
|
"report_context": {"file_key_id": "file-key-123", "user_id": "user-123"},
|
|
}
|
|
|
|
|
|
def test_plan_sync_route_returns_execution_runtime_plan_for_openai_chat(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
app = FastAPI()
|
|
app.include_router(router)
|
|
app.dependency_overrides[get_db] = lambda: object()
|
|
monkeypatch.setattr("src.api.internal.gateway.ensure_loopback", lambda request: None)
|
|
|
|
fake_plan = {
|
|
"request_id": "plan-openai-chat-123",
|
|
"provider_id": "provider-openai-chat-123",
|
|
"endpoint_id": "endpoint-openai-chat-123",
|
|
"key_id": "key-openai-chat-123",
|
|
"provider_name": "openai",
|
|
"method": "POST",
|
|
"url": "https://api.openai.example/v1/chat/completions",
|
|
"headers": {"authorization": "Bearer upstream-key", "content-type": "application/json"},
|
|
"body": {"json_body": {"model": "gpt-5", "messages": []}},
|
|
"stream": False,
|
|
"provider_api_format": "openai:chat",
|
|
"client_api_format": "openai:chat",
|
|
"model_name": "gpt-5",
|
|
}
|
|
monkeypatch.setattr(
|
|
"src.api.internal.gateway._build_openai_chat_sync_plan",
|
|
AsyncMock(
|
|
return_value=GatewayExecutionPlanResponse(
|
|
action="executor_sync",
|
|
plan_kind="openai_chat_sync",
|
|
plan=fake_plan,
|
|
report_kind="openai_chat_sync_success",
|
|
report_context={"user_id": "user-123", "api_key_id": "key-123"},
|
|
)
|
|
),
|
|
)
|
|
|
|
client = _make_legacy_test_client(app)
|
|
response = client.post(
|
|
"/api/internal/gateway/plan-sync",
|
|
json={
|
|
"trace_id": "trace-openai-chat-plan",
|
|
"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 == 200
|
|
assert response.json() == {
|
|
"action": "executor_sync",
|
|
"plan_kind": "openai_chat_sync",
|
|
"plan": fake_plan,
|
|
"report_kind": "openai_chat_sync_success",
|
|
"report_context": {"user_id": "user-123", "api_key_id": "key-123"},
|
|
}
|
|
|
|
|
|
def test_decision_sync_route_returns_execution_runtime_decision_for_openai_chat(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
app = FastAPI()
|
|
app.include_router(router)
|
|
app.dependency_overrides[get_db] = lambda: object()
|
|
monkeypatch.setattr("src.api.internal.gateway.ensure_loopback", lambda request: None)
|
|
|
|
monkeypatch.setattr(
|
|
"src.api.internal.gateway._build_openai_chat_sync_decision",
|
|
AsyncMock(
|
|
return_value=GatewayExecutionDecisionResponse(
|
|
action="executor_sync_decision",
|
|
decision_kind="openai_chat_sync",
|
|
request_id="req-openai-chat-decision-123",
|
|
candidate_id="cand-openai-chat-decision-123",
|
|
provider_name="openai",
|
|
provider_id="provider-openai-chat-decision-123",
|
|
endpoint_id="endpoint-openai-chat-decision-123",
|
|
key_id="key-openai-chat-decision-123",
|
|
upstream_base_url="https://api.openai.example",
|
|
auth_header="authorization",
|
|
auth_value="Bearer upstream-key",
|
|
provider_api_format="openai:chat",
|
|
client_api_format="openai:chat",
|
|
model_name="gpt-5",
|
|
mapped_model="gpt-5-upstream",
|
|
prompt_cache_key="cache-key-123",
|
|
extra_headers={"x-extra-upstream": "1"},
|
|
content_type="application/json",
|
|
timeouts={"connect_ms": 1000, "total_ms": 5000},
|
|
report_kind="openai_chat_sync_success",
|
|
report_context={"user_id": "user-123", "api_key_id": "key-123"},
|
|
)
|
|
),
|
|
)
|
|
|
|
client = _make_legacy_test_client(app)
|
|
response = client.post(
|
|
"/api/internal/gateway/decision-sync",
|
|
json={
|
|
"trace_id": "trace-openai-chat-decision",
|
|
"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 == 200
|
|
assert response.json() == {
|
|
"action": "executor_sync_decision",
|
|
"decision_kind": "openai_chat_sync",
|
|
"request_id": "req-openai-chat-decision-123",
|
|
"candidate_id": "cand-openai-chat-decision-123",
|
|
"provider_name": "openai",
|
|
"provider_id": "provider-openai-chat-decision-123",
|
|
"endpoint_id": "endpoint-openai-chat-decision-123",
|
|
"key_id": "key-openai-chat-decision-123",
|
|
"upstream_base_url": "https://api.openai.example",
|
|
"auth_header": "authorization",
|
|
"auth_value": "Bearer upstream-key",
|
|
"provider_api_format": "openai:chat",
|
|
"client_api_format": "openai:chat",
|
|
"model_name": "gpt-5",
|
|
"mapped_model": "gpt-5-upstream",
|
|
"prompt_cache_key": "cache-key-123",
|
|
"extra_headers": {"x-extra-upstream": "1"},
|
|
"content_type": "application/json",
|
|
"timeouts": {"connect_ms": 1000, "total_ms": 5000},
|
|
"report_kind": "openai_chat_sync_success",
|
|
"report_context": {"user_id": "user-123", "api_key_id": "key-123"},
|
|
}
|
|
|
|
|
|
def test_decision_stream_route_returns_execution_runtime_decision_for_openai_chat(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
app = FastAPI()
|
|
app.include_router(router)
|
|
app.dependency_overrides[get_db] = lambda: object()
|
|
monkeypatch.setattr("src.api.internal.gateway.ensure_loopback", lambda request: None)
|
|
|
|
monkeypatch.setattr(
|
|
"src.api.internal.gateway._build_openai_chat_stream_decision",
|
|
AsyncMock(
|
|
return_value=GatewayExecutionDecisionResponse(
|
|
action="executor_stream_decision",
|
|
decision_kind="openai_chat_stream",
|
|
request_id="req-openai-chat-stream-decision-123",
|
|
candidate_id="cand-openai-chat-stream-decision-123",
|
|
provider_name="openai",
|
|
provider_id="provider-openai-chat-stream-decision-123",
|
|
endpoint_id="endpoint-openai-chat-stream-decision-123",
|
|
key_id="key-openai-chat-stream-decision-123",
|
|
upstream_base_url="https://api.openai.example",
|
|
auth_header="authorization",
|
|
auth_value="Bearer upstream-key",
|
|
provider_api_format="openai:chat",
|
|
client_api_format="openai:chat",
|
|
model_name="gpt-5",
|
|
mapped_model="gpt-5-upstream",
|
|
prompt_cache_key="cache-key-123",
|
|
extra_headers={"x-extra-upstream": "1"},
|
|
content_type="application/json",
|
|
timeouts={"connect_ms": 1000, "read_ms": 2000},
|
|
report_kind="openai_chat_stream_success",
|
|
report_context={"user_id": "user-123", "api_key_id": "key-123"},
|
|
)
|
|
),
|
|
)
|
|
|
|
client = _make_legacy_test_client(app)
|
|
response = client.post(
|
|
"/api/internal/gateway/decision-stream",
|
|
json={
|
|
"trace_id": "trace-openai-chat-stream-decision",
|
|
"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 == 200
|
|
assert response.json() == {
|
|
"action": "executor_stream_decision",
|
|
"decision_kind": "openai_chat_stream",
|
|
"request_id": "req-openai-chat-stream-decision-123",
|
|
"candidate_id": "cand-openai-chat-stream-decision-123",
|
|
"provider_name": "openai",
|
|
"provider_id": "provider-openai-chat-stream-decision-123",
|
|
"endpoint_id": "endpoint-openai-chat-stream-decision-123",
|
|
"key_id": "key-openai-chat-stream-decision-123",
|
|
"upstream_base_url": "https://api.openai.example",
|
|
"auth_header": "authorization",
|
|
"auth_value": "Bearer upstream-key",
|
|
"provider_api_format": "openai:chat",
|
|
"client_api_format": "openai:chat",
|
|
"model_name": "gpt-5",
|
|
"mapped_model": "gpt-5-upstream",
|
|
"prompt_cache_key": "cache-key-123",
|
|
"extra_headers": {"x-extra-upstream": "1"},
|
|
"content_type": "application/json",
|
|
"timeouts": {"connect_ms": 1000, "read_ms": 2000},
|
|
"report_kind": "openai_chat_stream_success",
|
|
"report_context": {"user_id": "user-123", "api_key_id": "key-123"},
|
|
}
|
|
|
|
|
|
def test_decision_sync_route_requires_legacy_header(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
client = _make_internal_test_client(monkeypatch)
|
|
response = client.post(
|
|
"/api/internal/gateway/decision-sync",
|
|
json={
|
|
"trace_id": "trace-guard-decision-sync",
|
|
"method": "POST",
|
|
"path": "/v1/chat/completions",
|
|
"headers": {"content-type": "application/json"},
|
|
"body_json": {"model": "gpt-5", "messages": []},
|
|
},
|
|
)
|
|
|
|
_assert_legacy_guard_response(response)
|
|
|
|
|
|
def test_decision_stream_route_requires_legacy_header(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
client = _make_internal_test_client(monkeypatch)
|
|
response = client.post(
|
|
"/api/internal/gateway/decision-stream",
|
|
json={
|
|
"trace_id": "trace-guard-decision-stream",
|
|
"method": "POST",
|
|
"path": "/v1/chat/completions",
|
|
"headers": {"content-type": "application/json"},
|
|
"body_json": {"model": "gpt-5", "messages": [], "stream": True},
|
|
},
|
|
)
|
|
|
|
_assert_legacy_guard_response(response)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("path", "headers", "decision_kind", "client_api_format", "builder_path", "model_name", "mapped_model", "upstream_url"),
|
|
[
|
|
(
|
|
"/v1/messages",
|
|
{
|
|
"content-type": "application/json",
|
|
"x-api-key": "client-key",
|
|
},
|
|
"claude_chat_stream",
|
|
"claude:chat",
|
|
"src.api.internal.gateway._build_claude_chat_stream_decision",
|
|
"claude-sonnet-4-5",
|
|
"claude-sonnet-4-5-upstream",
|
|
"https://api.anthropic.example/v1/messages",
|
|
),
|
|
(
|
|
"/v1beta/models/gemini-2.5-pro:streamGenerateContent",
|
|
{
|
|
"content-type": "application/json",
|
|
"x-goog-api-key": "client-key",
|
|
},
|
|
"gemini_chat_stream",
|
|
"gemini:chat",
|
|
"src.api.internal.gateway._build_gemini_chat_stream_decision",
|
|
"gemini-2.5-pro",
|
|
"gemini-2.5-pro-upstream",
|
|
(
|
|
"https://generativelanguage.googleapis.com/v1beta/models/"
|
|
"gemini-2.5-pro-upstream:streamGenerateContent"
|
|
),
|
|
),
|
|
],
|
|
)
|
|
def test_decision_stream_route_returns_execution_runtime_decision_for_claude_and_gemini_chat(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
path: str,
|
|
headers: dict[str, str],
|
|
decision_kind: str,
|
|
client_api_format: str,
|
|
builder_path: str,
|
|
model_name: str,
|
|
mapped_model: str,
|
|
upstream_url: 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)
|
|
|
|
monkeypatch.setattr(
|
|
builder_path,
|
|
AsyncMock(
|
|
return_value=GatewayExecutionDecisionResponse(
|
|
action="executor_stream_decision",
|
|
decision_kind=decision_kind,
|
|
request_id="req-stream-decision-123",
|
|
candidate_id="cand-stream-decision-123",
|
|
provider_name="provider-name",
|
|
provider_id="provider-stream-decision-123",
|
|
endpoint_id="endpoint-stream-decision-123",
|
|
key_id="key-stream-decision-123",
|
|
upstream_base_url=upstream_url.split("/v1", 1)[0],
|
|
upstream_url=upstream_url,
|
|
auth_header=headers.get("x-goog-api-key") and "x-goog-api-key" or "x-api-key",
|
|
auth_value="upstream-secret",
|
|
provider_api_format=client_api_format,
|
|
client_api_format=client_api_format,
|
|
model_name=model_name,
|
|
mapped_model=mapped_model,
|
|
prompt_cache_key="cache-key-123",
|
|
extra_headers={"x-extra-upstream": "1"},
|
|
content_type="application/json",
|
|
timeouts={"connect_ms": 1000, "read_ms": 2000},
|
|
report_kind=f"{decision_kind}_success",
|
|
report_context={"user_id": "user-123", "api_key_id": "key-123"},
|
|
)
|
|
),
|
|
)
|
|
|
|
body_json = (
|
|
{"model": model_name, "messages": [], "stream": True}
|
|
if decision_kind == "claude_chat_stream"
|
|
else {"contents": [{"role": "user", "parts": [{"text": "hello"}]}]}
|
|
)
|
|
|
|
client = _make_legacy_test_client(app)
|
|
response = client.post(
|
|
"/api/internal/gateway/decision-stream",
|
|
json={
|
|
"trace_id": "trace-stream-decision",
|
|
"method": "POST",
|
|
"path": path,
|
|
"headers": headers,
|
|
"body_json": body_json,
|
|
"auth_context": {
|
|
"user_id": "user-123",
|
|
"api_key_id": "key-123",
|
|
"access_allowed": True,
|
|
},
|
|
},
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
assert response.json() == {
|
|
"action": "executor_stream_decision",
|
|
"decision_kind": decision_kind,
|
|
"request_id": "req-stream-decision-123",
|
|
"candidate_id": "cand-stream-decision-123",
|
|
"provider_name": "provider-name",
|
|
"provider_id": "provider-stream-decision-123",
|
|
"endpoint_id": "endpoint-stream-decision-123",
|
|
"key_id": "key-stream-decision-123",
|
|
"upstream_base_url": upstream_url.split("/v1", 1)[0],
|
|
"upstream_url": upstream_url,
|
|
"auth_header": "x-goog-api-key" if "x-goog-api-key" in headers else "x-api-key",
|
|
"auth_value": "upstream-secret",
|
|
"provider_api_format": client_api_format,
|
|
"client_api_format": client_api_format,
|
|
"model_name": model_name,
|
|
"mapped_model": mapped_model,
|
|
"prompt_cache_key": "cache-key-123",
|
|
"extra_headers": {"x-extra-upstream": "1"},
|
|
"content_type": "application/json",
|
|
"timeouts": {"connect_ms": 1000, "read_ms": 2000},
|
|
"report_kind": f"{decision_kind}_success",
|
|
"report_context": {"user_id": "user-123", "api_key_id": "key-123"},
|
|
}
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
(
|
|
"path",
|
|
"headers",
|
|
"decision_kind",
|
|
"client_api_format",
|
|
"builder_path",
|
|
"model_name",
|
|
"mapped_model",
|
|
"upstream_url",
|
|
"auth_header",
|
|
"body_json",
|
|
),
|
|
[
|
|
(
|
|
"/v1/responses",
|
|
{
|
|
"content-type": "application/json",
|
|
"authorization": "Bearer client-key",
|
|
},
|
|
"openai_cli_stream",
|
|
"openai:cli",
|
|
"src.api.internal.gateway._build_openai_cli_stream_decision",
|
|
"gpt-5",
|
|
"gpt-5-upstream",
|
|
None,
|
|
"authorization",
|
|
{"model": "gpt-5", "input": "hello", "stream": True},
|
|
),
|
|
(
|
|
"/v1/messages",
|
|
{
|
|
"content-type": "application/json",
|
|
"authorization": "Bearer client-key",
|
|
},
|
|
"claude_cli_stream",
|
|
"claude:cli",
|
|
"src.api.internal.gateway._build_claude_cli_stream_decision",
|
|
"claude-code",
|
|
"claude-code-upstream",
|
|
"https://api.anthropic.example/v1/messages",
|
|
"authorization",
|
|
{"model": "claude-code", "messages": [], "stream": True},
|
|
),
|
|
(
|
|
"/v1beta/models/gemini-cli:streamGenerateContent",
|
|
{
|
|
"content-type": "application/json",
|
|
"user-agent": "GeminiCLI/1.0",
|
|
"x-goog-api-key": "client-key",
|
|
},
|
|
"gemini_cli_stream",
|
|
"gemini:cli",
|
|
"src.api.internal.gateway._build_gemini_cli_stream_decision",
|
|
"gemini-cli",
|
|
"gemini-cli-upstream",
|
|
(
|
|
"https://generativelanguage.googleapis.com/v1beta/models/"
|
|
"gemini-cli-upstream:streamGenerateContent"
|
|
),
|
|
"x-goog-api-key",
|
|
{"contents": [{"role": "user", "parts": [{"text": "hello"}]}]},
|
|
),
|
|
],
|
|
)
|
|
def test_decision_stream_route_returns_execution_runtime_decision_for_cli_variants(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
path: str,
|
|
headers: dict[str, str],
|
|
decision_kind: str,
|
|
client_api_format: str,
|
|
builder_path: str,
|
|
model_name: str,
|
|
mapped_model: str,
|
|
upstream_url: str | None,
|
|
auth_header: str,
|
|
body_json: dict[str, Any],
|
|
) -> None:
|
|
app = FastAPI()
|
|
app.include_router(router)
|
|
app.dependency_overrides[get_db] = lambda: object()
|
|
monkeypatch.setattr("src.api.internal.gateway.ensure_loopback", lambda request: None)
|
|
|
|
monkeypatch.setattr(
|
|
builder_path,
|
|
AsyncMock(
|
|
return_value=GatewayExecutionDecisionResponse(
|
|
action="executor_stream_decision",
|
|
decision_kind=decision_kind,
|
|
request_id="req-cli-stream-decision-123",
|
|
candidate_id="cand-cli-stream-decision-123",
|
|
provider_name="provider-name",
|
|
provider_id="provider-cli-stream-decision-123",
|
|
endpoint_id="endpoint-cli-stream-decision-123",
|
|
key_id="key-cli-stream-decision-123",
|
|
upstream_base_url=(
|
|
"https://api.openai.example"
|
|
if upstream_url is None
|
|
else upstream_url.split("/v1", 1)[0]
|
|
),
|
|
upstream_url=upstream_url,
|
|
auth_header=auth_header,
|
|
auth_value="upstream-secret",
|
|
provider_api_format=client_api_format,
|
|
client_api_format=client_api_format,
|
|
model_name=model_name,
|
|
mapped_model=mapped_model,
|
|
prompt_cache_key="cache-key-123",
|
|
extra_headers={"x-extra-upstream": "1"},
|
|
content_type="application/json",
|
|
timeouts={"connect_ms": 1000, "read_ms": 2000},
|
|
report_kind=f"{decision_kind}_success",
|
|
report_context={"user_id": "user-123", "api_key_id": "key-123"},
|
|
)
|
|
),
|
|
)
|
|
|
|
client = _make_legacy_test_client(app)
|
|
response = client.post(
|
|
"/api/internal/gateway/decision-stream",
|
|
json={
|
|
"trace_id": "trace-cli-stream-decision",
|
|
"method": "POST",
|
|
"path": path,
|
|
"headers": headers,
|
|
"body_json": body_json,
|
|
"auth_context": {
|
|
"user_id": "user-123",
|
|
"api_key_id": "key-123",
|
|
"access_allowed": True,
|
|
},
|
|
},
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
expected = {
|
|
"action": "executor_stream_decision",
|
|
"decision_kind": decision_kind,
|
|
"request_id": "req-cli-stream-decision-123",
|
|
"candidate_id": "cand-cli-stream-decision-123",
|
|
"provider_name": "provider-name",
|
|
"provider_id": "provider-cli-stream-decision-123",
|
|
"endpoint_id": "endpoint-cli-stream-decision-123",
|
|
"key_id": "key-cli-stream-decision-123",
|
|
"upstream_base_url": (
|
|
"https://api.openai.example"
|
|
if upstream_url is None
|
|
else upstream_url.split("/v1", 1)[0]
|
|
),
|
|
"auth_header": auth_header,
|
|
"auth_value": "upstream-secret",
|
|
"provider_api_format": client_api_format,
|
|
"client_api_format": client_api_format,
|
|
"model_name": model_name,
|
|
"mapped_model": mapped_model,
|
|
"prompt_cache_key": "cache-key-123",
|
|
"extra_headers": {"x-extra-upstream": "1"},
|
|
"content_type": "application/json",
|
|
"timeouts": {"connect_ms": 1000, "read_ms": 2000},
|
|
"report_kind": f"{decision_kind}_success",
|
|
"report_context": {"user_id": "user-123", "api_key_id": "key-123"},
|
|
}
|
|
if upstream_url is not None:
|
|
expected["upstream_url"] = upstream_url
|
|
assert response.json() == expected
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("path", "decision_kind", "client_api_format"),
|
|
[
|
|
("/v1/responses", "openai_cli_sync", "openai:cli"),
|
|
("/v1/responses/compact", "openai_compact_sync", "openai:compact"),
|
|
],
|
|
)
|
|
def test_decision_sync_route_returns_execution_runtime_decision_for_openai_cli_variants(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
path: str,
|
|
decision_kind: str,
|
|
client_api_format: 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)
|
|
|
|
monkeypatch.setattr(
|
|
"src.api.internal.gateway._build_openai_cli_sync_decision",
|
|
AsyncMock(
|
|
return_value=GatewayExecutionDecisionResponse(
|
|
action="executor_sync_decision",
|
|
decision_kind=decision_kind,
|
|
request_id="req-openai-cli-decision-123",
|
|
candidate_id="cand-openai-cli-decision-123",
|
|
provider_name="openai",
|
|
provider_id="provider-openai-cli-decision-123",
|
|
endpoint_id="endpoint-openai-cli-decision-123",
|
|
key_id="key-openai-cli-decision-123",
|
|
upstream_base_url="https://api.openai.example",
|
|
auth_header="authorization",
|
|
auth_value="Bearer upstream-key",
|
|
provider_api_format=client_api_format,
|
|
client_api_format=client_api_format,
|
|
model_name="gpt-5",
|
|
mapped_model="gpt-5-upstream",
|
|
prompt_cache_key="cache-key-123",
|
|
extra_headers={"x-extra-upstream": "1"},
|
|
content_type="application/json",
|
|
timeouts={"connect_ms": 1000, "total_ms": 5000},
|
|
report_kind="openai_cli_sync_success",
|
|
report_context={"user_id": "user-123", "api_key_id": "key-123"},
|
|
)
|
|
),
|
|
)
|
|
|
|
client = _make_legacy_test_client(app)
|
|
response = client.post(
|
|
"/api/internal/gateway/decision-sync",
|
|
json={
|
|
"trace_id": "trace-openai-cli-decision",
|
|
"method": "POST",
|
|
"path": path,
|
|
"headers": {
|
|
"content-type": "application/json",
|
|
"authorization": "Bearer client-key",
|
|
},
|
|
"body_json": {"model": "gpt-5", "input": "hello"},
|
|
"auth_context": {
|
|
"user_id": "user-123",
|
|
"api_key_id": "key-123",
|
|
"access_allowed": True,
|
|
},
|
|
},
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
assert response.json() == {
|
|
"action": "executor_sync_decision",
|
|
"decision_kind": decision_kind,
|
|
"request_id": "req-openai-cli-decision-123",
|
|
"candidate_id": "cand-openai-cli-decision-123",
|
|
"provider_name": "openai",
|
|
"provider_id": "provider-openai-cli-decision-123",
|
|
"endpoint_id": "endpoint-openai-cli-decision-123",
|
|
"key_id": "key-openai-cli-decision-123",
|
|
"upstream_base_url": "https://api.openai.example",
|
|
"auth_header": "authorization",
|
|
"auth_value": "Bearer upstream-key",
|
|
"provider_api_format": client_api_format,
|
|
"client_api_format": client_api_format,
|
|
"model_name": "gpt-5",
|
|
"mapped_model": "gpt-5-upstream",
|
|
"prompt_cache_key": "cache-key-123",
|
|
"extra_headers": {"x-extra-upstream": "1"},
|
|
"content_type": "application/json",
|
|
"timeouts": {"connect_ms": 1000, "total_ms": 5000},
|
|
"report_kind": "openai_cli_sync_success",
|
|
"report_context": {"user_id": "user-123", "api_key_id": "key-123"},
|
|
}
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("headers", "decision_kind", "client_api_format"),
|
|
[
|
|
(
|
|
{
|
|
"content-type": "application/json",
|
|
"x-api-key": "client-key",
|
|
},
|
|
"claude_chat_sync",
|
|
"claude:chat",
|
|
),
|
|
(
|
|
{
|
|
"content-type": "application/json",
|
|
"authorization": "Bearer client-key",
|
|
},
|
|
"claude_cli_sync",
|
|
"claude:cli",
|
|
),
|
|
],
|
|
)
|
|
def test_decision_sync_route_returns_execution_runtime_decision_for_claude_variants(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
headers: dict[str, str],
|
|
decision_kind: str,
|
|
client_api_format: 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)
|
|
|
|
target = (
|
|
"src.api.internal.gateway._build_claude_cli_sync_decision"
|
|
if decision_kind == "claude_cli_sync"
|
|
else "src.api.internal.gateway._build_claude_chat_sync_decision"
|
|
)
|
|
monkeypatch.setattr(
|
|
target,
|
|
AsyncMock(
|
|
return_value=GatewayExecutionDecisionResponse(
|
|
action="executor_sync_decision",
|
|
decision_kind=decision_kind,
|
|
request_id="req-claude-decision-123",
|
|
candidate_id="cand-claude-decision-123",
|
|
provider_name="claude",
|
|
provider_id="provider-claude-decision-123",
|
|
endpoint_id="endpoint-claude-decision-123",
|
|
key_id="key-claude-decision-123",
|
|
upstream_base_url="https://api.anthropic.example",
|
|
upstream_url="https://api.anthropic.example/v1/messages",
|
|
auth_header="x-api-key" if decision_kind == "claude_chat_sync" else "authorization",
|
|
auth_value="upstream-secret",
|
|
provider_api_format=client_api_format,
|
|
client_api_format=client_api_format,
|
|
model_name="claude-sonnet-4-5",
|
|
mapped_model="claude-sonnet-4-5-upstream",
|
|
prompt_cache_key="cache-key-123",
|
|
extra_headers={"x-extra-upstream": "1"},
|
|
content_type="application/json",
|
|
timeouts={"connect_ms": 1000, "total_ms": 5000},
|
|
report_kind=(
|
|
"claude_cli_sync_success"
|
|
if decision_kind == "claude_cli_sync"
|
|
else "claude_chat_sync_success"
|
|
),
|
|
report_context={"user_id": "user-123", "api_key_id": "key-123"},
|
|
)
|
|
),
|
|
)
|
|
|
|
client = _make_legacy_test_client(app)
|
|
response = client.post(
|
|
"/api/internal/gateway/decision-sync",
|
|
json={
|
|
"trace_id": "trace-claude-decision",
|
|
"method": "POST",
|
|
"path": "/v1/messages",
|
|
"headers": headers,
|
|
"body_json": {"model": "claude-sonnet-4-5", "messages": []},
|
|
"auth_context": {
|
|
"user_id": "user-123",
|
|
"api_key_id": "key-123",
|
|
"access_allowed": True,
|
|
},
|
|
},
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
assert response.json() == {
|
|
"action": "executor_sync_decision",
|
|
"decision_kind": decision_kind,
|
|
"request_id": "req-claude-decision-123",
|
|
"candidate_id": "cand-claude-decision-123",
|
|
"provider_name": "claude",
|
|
"provider_id": "provider-claude-decision-123",
|
|
"endpoint_id": "endpoint-claude-decision-123",
|
|
"key_id": "key-claude-decision-123",
|
|
"upstream_base_url": "https://api.anthropic.example",
|
|
"upstream_url": "https://api.anthropic.example/v1/messages",
|
|
"auth_header": "x-api-key" if decision_kind == "claude_chat_sync" else "authorization",
|
|
"auth_value": "upstream-secret",
|
|
"provider_api_format": client_api_format,
|
|
"client_api_format": client_api_format,
|
|
"model_name": "claude-sonnet-4-5",
|
|
"mapped_model": "claude-sonnet-4-5-upstream",
|
|
"prompt_cache_key": "cache-key-123",
|
|
"extra_headers": {"x-extra-upstream": "1"},
|
|
"content_type": "application/json",
|
|
"timeouts": {"connect_ms": 1000, "total_ms": 5000},
|
|
"report_kind": (
|
|
"claude_cli_sync_success"
|
|
if decision_kind == "claude_cli_sync"
|
|
else "claude_chat_sync_success"
|
|
),
|
|
"report_context": {"user_id": "user-123", "api_key_id": "key-123"},
|
|
}
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("path", "headers", "decision_kind", "client_api_format", "model_name", "mapped_model"),
|
|
[
|
|
(
|
|
"/v1beta/models/gemini-2.5-pro:generateContent",
|
|
{
|
|
"content-type": "application/json",
|
|
"x-goog-api-key": "client-key",
|
|
},
|
|
"gemini_chat_sync",
|
|
"gemini:chat",
|
|
"gemini-2.5-pro",
|
|
"gemini-2.5-pro-upstream",
|
|
),
|
|
(
|
|
"/v1beta/models/gemini-cli:generateContent",
|
|
{
|
|
"content-type": "application/json",
|
|
"user-agent": "GeminiCLI/1.0",
|
|
"x-goog-api-key": "client-key",
|
|
},
|
|
"gemini_cli_sync",
|
|
"gemini:cli",
|
|
"gemini-cli",
|
|
"gemini-cli-upstream",
|
|
),
|
|
],
|
|
)
|
|
def test_decision_sync_route_returns_execution_runtime_decision_for_gemini_variants(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
path: str,
|
|
headers: dict[str, str],
|
|
decision_kind: str,
|
|
client_api_format: str,
|
|
model_name: str,
|
|
mapped_model: 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)
|
|
|
|
monkeypatch.setattr(
|
|
(
|
|
"src.api.internal.gateway._build_gemini_cli_sync_decision"
|
|
if decision_kind == "gemini_cli_sync"
|
|
else "src.api.internal.gateway._build_gemini_chat_sync_decision"
|
|
),
|
|
AsyncMock(
|
|
return_value=GatewayExecutionDecisionResponse(
|
|
action="executor_sync_decision",
|
|
decision_kind=decision_kind,
|
|
request_id="req-gemini-decision-123",
|
|
candidate_id="cand-gemini-decision-123",
|
|
provider_name="gemini",
|
|
provider_id="provider-gemini-decision-123",
|
|
endpoint_id="endpoint-gemini-decision-123",
|
|
key_id="key-gemini-decision-123",
|
|
upstream_base_url="https://generativelanguage.googleapis.com",
|
|
upstream_url=(
|
|
"https://generativelanguage.googleapis.com/v1beta/models/"
|
|
f"{mapped_model}:generateContent"
|
|
),
|
|
auth_header="x-goog-api-key",
|
|
auth_value="upstream-key",
|
|
provider_api_format=client_api_format,
|
|
client_api_format=client_api_format,
|
|
model_name=model_name,
|
|
mapped_model=mapped_model,
|
|
prompt_cache_key="cache-key-123",
|
|
extra_headers={"x-extra-upstream": "1"},
|
|
content_type="application/json",
|
|
timeouts={"connect_ms": 1000, "total_ms": 5000},
|
|
report_kind=(
|
|
"gemini_cli_sync_success"
|
|
if decision_kind == "gemini_cli_sync"
|
|
else "gemini_chat_sync_success"
|
|
),
|
|
report_context={"user_id": "user-123", "api_key_id": "key-123"},
|
|
)
|
|
),
|
|
)
|
|
|
|
client = _make_legacy_test_client(app)
|
|
response = client.post(
|
|
"/api/internal/gateway/decision-sync",
|
|
json={
|
|
"trace_id": "trace-gemini-decision",
|
|
"method": "POST",
|
|
"path": path,
|
|
"headers": headers,
|
|
"body_json": {"contents": [{"role": "user", "parts": [{"text": "hello"}]}]},
|
|
"auth_context": {
|
|
"user_id": "user-123",
|
|
"api_key_id": "key-123",
|
|
"access_allowed": True,
|
|
},
|
|
},
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
assert response.json() == {
|
|
"action": "executor_sync_decision",
|
|
"decision_kind": decision_kind,
|
|
"request_id": "req-gemini-decision-123",
|
|
"candidate_id": "cand-gemini-decision-123",
|
|
"provider_name": "gemini",
|
|
"provider_id": "provider-gemini-decision-123",
|
|
"endpoint_id": "endpoint-gemini-decision-123",
|
|
"key_id": "key-gemini-decision-123",
|
|
"upstream_base_url": "https://generativelanguage.googleapis.com",
|
|
"upstream_url": (
|
|
"https://generativelanguage.googleapis.com/v1beta/models/"
|
|
f"{mapped_model}:generateContent"
|
|
),
|
|
"auth_header": "x-goog-api-key",
|
|
"auth_value": "upstream-key",
|
|
"provider_api_format": client_api_format,
|
|
"client_api_format": client_api_format,
|
|
"model_name": model_name,
|
|
"mapped_model": mapped_model,
|
|
"prompt_cache_key": "cache-key-123",
|
|
"extra_headers": {"x-extra-upstream": "1"},
|
|
"content_type": "application/json",
|
|
"timeouts": {"connect_ms": 1000, "total_ms": 5000},
|
|
"report_kind": (
|
|
"gemini_cli_sync_success"
|
|
if decision_kind == "gemini_cli_sync"
|
|
else "gemini_chat_sync_success"
|
|
),
|
|
"report_context": {"user_id": "user-123", "api_key_id": "key-123"},
|
|
}
|
|
|
|
|
|
def test_decision_sync_route_returns_execution_runtime_decision_for_gemini_files_get(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
app = FastAPI()
|
|
app.include_router(router)
|
|
app.dependency_overrides[get_db] = lambda: object()
|
|
monkeypatch.setattr("src.api.internal.gateway.ensure_loopback", lambda request: None)
|
|
monkeypatch.setattr(
|
|
"src.api.internal.gateway._load_gateway_auth_models",
|
|
lambda db, auth_context: (
|
|
SimpleNamespace(id=auth_context.user_id),
|
|
SimpleNamespace(id=auth_context.api_key_id),
|
|
),
|
|
)
|
|
monkeypatch.setattr(
|
|
"src.api.internal.gateway.get_pipeline",
|
|
lambda: SimpleNamespace(_check_user_rate_limit=AsyncMock(return_value=None)),
|
|
)
|
|
monkeypatch.setattr(
|
|
"src.api.internal.gateway._resolve_gateway_execute_auth_context",
|
|
AsyncMock(
|
|
return_value=GatewayAuthContext(
|
|
user_id="user-files-decision-123",
|
|
api_key_id="key-files-decision-123",
|
|
access_allowed=True,
|
|
)
|
|
),
|
|
)
|
|
monkeypatch.setattr(
|
|
"src.api.internal.gateway._build_gemini_files_proxy_sync_decision",
|
|
AsyncMock(
|
|
return_value=GatewayExecutionDecisionResponse(
|
|
action="executor_sync_decision",
|
|
decision_kind="gemini_files_get",
|
|
request_id="req-files-get-decision-123",
|
|
provider_name="gemini",
|
|
provider_id="provider-files-decision-123",
|
|
endpoint_id="endpoint-files-decision-123",
|
|
key_id="key-files-decision-123",
|
|
upstream_base_url="https://generativelanguage.googleapis.com",
|
|
upstream_url="https://generativelanguage.googleapis.com/v1beta/files/files/abc-123",
|
|
auth_header="",
|
|
auth_value="",
|
|
provider_api_format="gemini:files",
|
|
client_api_format="gemini:files",
|
|
model_name="gemini-files",
|
|
provider_request_headers={"x-goog-api-key": "provider-key"},
|
|
report_kind="gemini_files_store_mapping",
|
|
report_context={
|
|
"file_key_id": "key-files-decision-123",
|
|
"user_id": "user-files-decision-123",
|
|
},
|
|
)
|
|
),
|
|
)
|
|
|
|
client = _make_legacy_test_client(app)
|
|
response = client.post(
|
|
"/api/internal/gateway/decision-sync",
|
|
json={
|
|
"trace_id": "trace-files-get-decision-123",
|
|
"method": "GET",
|
|
"path": "/v1beta/files/files/abc-123",
|
|
"headers": {"x-goog-api-key": "client-key"},
|
|
"body_json": {},
|
|
},
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
assert response.json()["decision_kind"] == "gemini_files_get"
|
|
assert response.json()["provider_api_format"] == "gemini:files"
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
(
|
|
"method",
|
|
"path",
|
|
"body_json",
|
|
"builder_target",
|
|
"decision_kind",
|
|
"client_api_format",
|
|
"model_name",
|
|
"provider_request_method",
|
|
"provider_request_body",
|
|
"upstream_url",
|
|
),
|
|
[
|
|
(
|
|
"POST",
|
|
"/v1/videos",
|
|
{"model": "sora-2", "prompt": "hello"},
|
|
"src.api.internal.gateway._build_openai_video_create_sync_decision",
|
|
"openai_video_create_sync",
|
|
"openai:video",
|
|
"sora-2",
|
|
"POST",
|
|
{"model": "sora-2", "prompt": "hello"},
|
|
"https://api.openai.example/v1/videos",
|
|
),
|
|
(
|
|
"POST",
|
|
"/v1/videos/task-123/remix",
|
|
{"model": "sora-2", "prompt": "remix me"},
|
|
"src.api.internal.gateway._build_openai_video_remix_sync_decision",
|
|
"openai_video_remix_sync",
|
|
"openai:video",
|
|
"sora-2",
|
|
"POST",
|
|
{"model": "sora-2", "prompt": "remix me"},
|
|
"https://api.openai.example/v1/videos/ext-123/remix",
|
|
),
|
|
(
|
|
"POST",
|
|
"/v1/videos/task-123/cancel",
|
|
{},
|
|
"src.api.internal.gateway._build_openai_video_cancel_sync_decision",
|
|
"openai_video_cancel_sync",
|
|
"openai:video",
|
|
"sora-2",
|
|
"DELETE",
|
|
None,
|
|
"https://api.openai.example/v1/videos/ext-123",
|
|
),
|
|
(
|
|
"DELETE",
|
|
"/v1/videos/task-123",
|
|
{},
|
|
"src.api.internal.gateway._build_openai_video_delete_sync_decision",
|
|
"openai_video_delete_sync",
|
|
"openai:video",
|
|
"sora-2",
|
|
"DELETE",
|
|
None,
|
|
"https://api.openai.example/v1/videos/ext-123",
|
|
),
|
|
(
|
|
"POST",
|
|
"/v1beta/models/veo-3:predictLongRunning",
|
|
{"instances": [{"prompt": "hello"}]},
|
|
"src.api.internal.gateway._build_gemini_video_create_sync_decision",
|
|
"gemini_video_create_sync",
|
|
"gemini:video",
|
|
"veo-3",
|
|
"POST",
|
|
{"instances": [{"prompt": "hello"}]},
|
|
"https://generativelanguage.googleapis.com/v1beta/models/veo-3:predictLongRunning",
|
|
),
|
|
(
|
|
"POST",
|
|
"/v1beta/models/veo-3/operations/ext-123:cancel",
|
|
{},
|
|
"src.api.internal.gateway._build_gemini_video_cancel_sync_decision",
|
|
"gemini_video_cancel_sync",
|
|
"gemini:video",
|
|
"veo-3",
|
|
"POST",
|
|
{},
|
|
"https://generativelanguage.googleapis.com/v1beta/models/veo-3/operations/ext-123:cancel",
|
|
),
|
|
],
|
|
)
|
|
def test_decision_sync_route_returns_execution_runtime_decision_for_video_variants(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
method: str,
|
|
path: str,
|
|
body_json: dict[str, Any],
|
|
builder_target: str,
|
|
decision_kind: str,
|
|
client_api_format: str,
|
|
model_name: str,
|
|
provider_request_method: str,
|
|
provider_request_body: dict[str, Any] | None,
|
|
upstream_url: 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)
|
|
monkeypatch.setattr(
|
|
builder_target,
|
|
AsyncMock(
|
|
return_value=GatewayExecutionDecisionResponse(
|
|
action="executor_sync_decision",
|
|
decision_kind=decision_kind,
|
|
request_id="req-video-sync-decision-123",
|
|
provider_name="provider-name",
|
|
provider_id="provider-video-sync-decision-123",
|
|
endpoint_id="endpoint-video-sync-decision-123",
|
|
key_id="key-video-sync-decision-123",
|
|
upstream_base_url=upstream_url.split("/v1", 1)[0],
|
|
upstream_url=upstream_url,
|
|
provider_request_method=provider_request_method,
|
|
auth_header="",
|
|
auth_value="",
|
|
provider_api_format=client_api_format,
|
|
client_api_format=client_api_format,
|
|
model_name=model_name,
|
|
provider_request_headers={"authorization": "Bearer upstream-key"},
|
|
provider_request_body=provider_request_body,
|
|
content_type="application/json" if provider_request_body is not None else None,
|
|
timeouts={"connect_ms": 1000, "total_ms": 5000},
|
|
report_kind=f"{decision_kind}_finalize",
|
|
report_context={"user_id": "user-123", "api_key_id": "key-123"},
|
|
)
|
|
),
|
|
)
|
|
|
|
client = _make_legacy_test_client(app)
|
|
response = client.post(
|
|
"/api/internal/gateway/decision-sync",
|
|
json={
|
|
"trace_id": "trace-video-sync-decision-123",
|
|
"method": method,
|
|
"path": path,
|
|
"headers": {"authorization": "Bearer client-key"},
|
|
"body_json": body_json,
|
|
"auth_context": {
|
|
"user_id": "user-123",
|
|
"api_key_id": "key-123",
|
|
"access_allowed": True,
|
|
},
|
|
},
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
expected = {
|
|
"action": "executor_sync_decision",
|
|
"decision_kind": decision_kind,
|
|
"request_id": "req-video-sync-decision-123",
|
|
"provider_name": "provider-name",
|
|
"provider_id": "provider-video-sync-decision-123",
|
|
"endpoint_id": "endpoint-video-sync-decision-123",
|
|
"key_id": "key-video-sync-decision-123",
|
|
"upstream_base_url": upstream_url.split("/v1", 1)[0],
|
|
"upstream_url": upstream_url,
|
|
"provider_request_method": provider_request_method,
|
|
"auth_header": "",
|
|
"auth_value": "",
|
|
"provider_api_format": client_api_format,
|
|
"client_api_format": client_api_format,
|
|
"model_name": model_name,
|
|
"extra_headers": {},
|
|
"provider_request_headers": {"authorization": "Bearer upstream-key"},
|
|
"timeouts": {"connect_ms": 1000, "total_ms": 5000},
|
|
"report_kind": f"{decision_kind}_finalize",
|
|
"report_context": {"user_id": "user-123", "api_key_id": "key-123"},
|
|
}
|
|
if provider_request_body is not None:
|
|
expected["provider_request_body"] = provider_request_body
|
|
expected["content_type"] = "application/json"
|
|
|
|
assert response.json() == expected
|
|
|
|
|
|
def test_decision_stream_route_returns_execution_runtime_decision_for_gemini_files_download(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
app = FastAPI()
|
|
app.include_router(router)
|
|
app.dependency_overrides[get_db] = lambda: object()
|
|
monkeypatch.setattr("src.api.internal.gateway.ensure_loopback", lambda request: None)
|
|
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: SimpleNamespace(_check_user_rate_limit=AsyncMock(return_value=None)),
|
|
)
|
|
monkeypatch.setattr(
|
|
"src.api.internal.gateway._resolve_gateway_execute_auth_context",
|
|
AsyncMock(
|
|
return_value=GatewayAuthContext(
|
|
user_id="user-files-download-decision-123",
|
|
api_key_id="key-files-download-decision-123",
|
|
access_allowed=True,
|
|
)
|
|
),
|
|
)
|
|
monkeypatch.setattr(
|
|
"src.api.internal.gateway._build_gemini_files_download_stream_decision",
|
|
AsyncMock(
|
|
return_value=GatewayExecutionDecisionResponse(
|
|
action="executor_stream_decision",
|
|
decision_kind="gemini_files_download",
|
|
request_id="req-files-download-decision-123",
|
|
provider_name="gemini",
|
|
provider_id="provider-files-download-decision-123",
|
|
endpoint_id="endpoint-files-download-decision-123",
|
|
key_id="key-files-download-decision-123",
|
|
upstream_base_url="",
|
|
upstream_url=(
|
|
"https://generativelanguage.googleapis.com/"
|
|
"v1beta/files/files/abc-123:download?alt=media"
|
|
),
|
|
auth_header="",
|
|
auth_value="",
|
|
provider_api_format="gemini:files",
|
|
client_api_format="gemini:files",
|
|
model_name="gemini-files",
|
|
provider_request_headers={"x-goog-api-key": "provider-key"},
|
|
)
|
|
),
|
|
)
|
|
|
|
client = _make_legacy_test_client(app)
|
|
response = client.post(
|
|
"/api/internal/gateway/decision-stream",
|
|
json={
|
|
"trace_id": "trace-files-download-decision-123",
|
|
"method": "GET",
|
|
"path": "/v1beta/files/file-123:download",
|
|
"query_string": "alt=media",
|
|
"headers": {"x-goog-api-key": "client-key"},
|
|
"body_json": {},
|
|
},
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
assert response.json()["decision_kind"] == "gemini_files_download"
|
|
|
|
|
|
def test_decision_stream_route_returns_execution_runtime_decision_for_openai_video_content(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
app = FastAPI()
|
|
app.include_router(router)
|
|
app.dependency_overrides[get_db] = lambda: object()
|
|
monkeypatch.setattr("src.api.internal.gateway.ensure_loopback", lambda request: None)
|
|
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: SimpleNamespace(_check_user_rate_limit=AsyncMock(return_value=None)),
|
|
)
|
|
monkeypatch.setattr(
|
|
"src.api.internal.gateway._resolve_gateway_execute_auth_context",
|
|
AsyncMock(
|
|
return_value=GatewayAuthContext(
|
|
user_id="user-video-content-decision-123",
|
|
api_key_id="key-video-content-decision-123",
|
|
access_allowed=True,
|
|
)
|
|
),
|
|
)
|
|
monkeypatch.setattr(
|
|
"src.api.internal.gateway._build_openai_video_content_stream_decision",
|
|
AsyncMock(
|
|
return_value=GatewayExecutionDecisionResponse(
|
|
action="executor_stream_decision",
|
|
decision_kind="openai_video_content",
|
|
request_id="req-video-content-decision-123",
|
|
provider_name="openai",
|
|
provider_id="provider-video-content-decision-123",
|
|
endpoint_id="endpoint-video-content-decision-123",
|
|
key_id="key-video-content-decision-123",
|
|
upstream_base_url="",
|
|
upstream_url="https://cdn.example.com/video.mp4",
|
|
auth_header="",
|
|
auth_value="",
|
|
provider_api_format="openai:video",
|
|
client_api_format="openai:video",
|
|
model_name="sora-2",
|
|
provider_request_headers={},
|
|
)
|
|
),
|
|
)
|
|
|
|
client = _make_legacy_test_client(app)
|
|
response = client.post(
|
|
"/api/internal/gateway/decision-stream",
|
|
json={
|
|
"trace_id": "trace-video-content-decision-123",
|
|
"method": "GET",
|
|
"path": "/v1/videos/task-123/content",
|
|
"headers": {"authorization": "Bearer client-key"},
|
|
"body_json": {},
|
|
},
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
assert response.json()["decision_kind"] == "openai_video_content"
|
|
|
|
|
|
def test_plan_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)
|
|
|
|
build_plan = AsyncMock(
|
|
return_value=GatewayExecutionPlanResponse(
|
|
action="executor_sync",
|
|
plan_kind="openai_chat_sync",
|
|
plan={
|
|
"request_id": "plan-openai-chat-derived-auth-123",
|
|
"provider_name": "openai",
|
|
"method": "POST",
|
|
"url": "https://api.openai.example/v1/chat/completions",
|
|
"headers": {"content-type": "application/json"},
|
|
"body": {"json_body": {"model": "gpt-5", "messages": []}},
|
|
"stream": False,
|
|
"provider_api_format": "openai:chat",
|
|
"client_api_format": "openai:chat",
|
|
"model_name": "gpt-5",
|
|
},
|
|
report_kind="openai_chat_sync_success",
|
|
report_context={"user_id": "user-123", "api_key_id": "key-123"},
|
|
)
|
|
)
|
|
monkeypatch.setattr("src.api.internal.gateway._build_openai_chat_sync_plan", build_plan)
|
|
monkeypatch.setattr(
|
|
"src.api.internal.gateway._resolve_auth_context_signature",
|
|
AsyncMock(
|
|
return_value={
|
|
"user_id": "user-123",
|
|
"api_key_id": "key-123",
|
|
"access_allowed": True,
|
|
}
|
|
),
|
|
)
|
|
|
|
client = _make_legacy_test_client(app)
|
|
response = client.post(
|
|
"/api/internal/gateway/plan-sync",
|
|
json={
|
|
"trace_id": "trace-openai-chat-plan-derived-auth",
|
|
"method": "POST",
|
|
"path": "/v1/chat/completions",
|
|
"headers": {
|
|
"content-type": "application/json",
|
|
"authorization": "Bearer client-key",
|
|
},
|
|
"body_json": {"model": "gpt-5", "messages": []},
|
|
},
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
assert response.json()["action"] == "executor_sync"
|
|
assert response.json()["auth_context"] == {
|
|
"user_id": "user-123",
|
|
"api_key_id": "key-123",
|
|
"access_allowed": True,
|
|
}
|
|
assert build_plan.await_args.kwargs["auth_context"].user_id == "user-123"
|
|
assert build_plan.await_args.kwargs["auth_context"].api_key_id == "key-123"
|
|
|
|
|
|
def test_plan_sync_route_returns_execution_runtime_plan_for_openai_video_create(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
app = FastAPI()
|
|
app.include_router(router)
|
|
app.dependency_overrides[get_db] = lambda: object()
|
|
monkeypatch.setattr("src.api.internal.gateway.ensure_loopback", lambda request: None)
|
|
|
|
fake_plan = {
|
|
"request_id": "plan-openai-video-create-123",
|
|
"provider_id": "provider-openai-video-create-123",
|
|
"endpoint_id": "endpoint-openai-video-create-123",
|
|
"key_id": "key-openai-video-create-123",
|
|
"provider_name": "openai",
|
|
"method": "POST",
|
|
"url": "https://api.openai.example/v1/videos",
|
|
"headers": {"authorization": "Bearer upstream-key", "content-type": "application/json"},
|
|
"body": {"json_body": {"model": "sora-2", "prompt": "hello"}},
|
|
"stream": False,
|
|
"provider_api_format": "openai:video",
|
|
"client_api_format": "openai:video",
|
|
"model_name": "sora-2",
|
|
}
|
|
monkeypatch.setattr(
|
|
"src.api.internal.gateway._build_openai_video_create_sync_plan",
|
|
AsyncMock(
|
|
return_value=GatewayExecutionPlanResponse(
|
|
action="executor_sync",
|
|
plan_kind="openai_video_create_sync",
|
|
plan=fake_plan,
|
|
report_kind="openai_video_create_sync_finalize",
|
|
report_context={"user_id": "user-123", "api_key_id": "key-123"},
|
|
)
|
|
),
|
|
)
|
|
|
|
client = _make_legacy_test_client(app)
|
|
response = client.post(
|
|
"/api/internal/gateway/plan-sync",
|
|
json={
|
|
"trace_id": "trace-openai-video-create-plan",
|
|
"method": "POST",
|
|
"path": "/v1/videos",
|
|
"headers": {
|
|
"content-type": "application/json",
|
|
"authorization": "Bearer client-key",
|
|
},
|
|
"body_json": {"model": "sora-2", "prompt": "hello"},
|
|
"auth_context": {
|
|
"user_id": "user-123",
|
|
"api_key_id": "key-123",
|
|
"access_allowed": True,
|
|
},
|
|
},
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
assert response.json() == {
|
|
"action": "executor_sync",
|
|
"plan_kind": "openai_video_create_sync",
|
|
"plan": fake_plan,
|
|
"report_kind": "openai_video_create_sync_finalize",
|
|
"report_context": {"user_id": "user-123", "api_key_id": "key-123"},
|
|
}
|
|
|
|
|
|
def test_plan_sync_route_returns_execution_runtime_plan_for_openai_video_remix(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
app = FastAPI()
|
|
app.include_router(router)
|
|
app.dependency_overrides[get_db] = lambda: object()
|
|
monkeypatch.setattr("src.api.internal.gateway.ensure_loopback", lambda request: None)
|
|
|
|
fake_plan = {
|
|
"request_id": "plan-openai-video-remix-123",
|
|
"provider_id": "provider-openai-video-remix-123",
|
|
"endpoint_id": "endpoint-openai-video-remix-123",
|
|
"key_id": "key-openai-video-remix-123",
|
|
"provider_name": "openai",
|
|
"method": "POST",
|
|
"url": "https://api.openai.example/v1/videos/ext-123/remix",
|
|
"headers": {"authorization": "Bearer upstream-key", "content-type": "application/json"},
|
|
"body": {"json_body": {"prompt": "remix this"}},
|
|
"stream": False,
|
|
"provider_api_format": "openai:video",
|
|
"client_api_format": "openai:video",
|
|
"model_name": "sora-2",
|
|
}
|
|
monkeypatch.setattr(
|
|
"src.api.internal.gateway._build_openai_video_remix_sync_plan",
|
|
AsyncMock(
|
|
return_value=GatewayExecutionPlanResponse(
|
|
action="executor_sync",
|
|
plan_kind="openai_video_remix_sync",
|
|
plan=fake_plan,
|
|
report_kind="openai_video_remix_sync_finalize",
|
|
report_context={"user_id": "user-123", "api_key_id": "key-123"},
|
|
)
|
|
),
|
|
)
|
|
|
|
client = _make_legacy_test_client(app)
|
|
response = client.post(
|
|
"/api/internal/gateway/plan-sync",
|
|
json={
|
|
"trace_id": "trace-openai-video-remix-plan",
|
|
"method": "POST",
|
|
"path": "/v1/videos/task-123/remix",
|
|
"headers": {
|
|
"content-type": "application/json",
|
|
"authorization": "Bearer client-key",
|
|
},
|
|
"body_json": {"prompt": "remix this"},
|
|
"auth_context": {
|
|
"user_id": "user-123",
|
|
"api_key_id": "key-123",
|
|
"access_allowed": True,
|
|
},
|
|
},
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
assert response.json() == {
|
|
"action": "executor_sync",
|
|
"plan_kind": "openai_video_remix_sync",
|
|
"plan": fake_plan,
|
|
"report_kind": "openai_video_remix_sync_finalize",
|
|
"report_context": {"user_id": "user-123", "api_key_id": "key-123"},
|
|
}
|
|
|
|
|
|
def test_plan_sync_route_returns_execution_runtime_plan_for_gemini_video_create(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
app = FastAPI()
|
|
app.include_router(router)
|
|
app.dependency_overrides[get_db] = lambda: object()
|
|
monkeypatch.setattr("src.api.internal.gateway.ensure_loopback", lambda request: None)
|
|
|
|
fake_plan = {
|
|
"request_id": "plan-gemini-video-create-123",
|
|
"provider_id": "provider-gemini-video-create-123",
|
|
"endpoint_id": "endpoint-gemini-video-create-123",
|
|
"key_id": "key-gemini-video-create-123",
|
|
"provider_name": "gemini",
|
|
"method": "POST",
|
|
"url": "https://generativelanguage.googleapis.com/v1beta/models/veo-3:predictLongRunning",
|
|
"headers": {"x-goog-api-key": "upstream-key", "content-type": "application/json"},
|
|
"body": {"json_body": {"prompt": "make a video"}},
|
|
"stream": False,
|
|
"provider_api_format": "gemini:video",
|
|
"client_api_format": "gemini:video",
|
|
"model_name": "veo-3",
|
|
}
|
|
monkeypatch.setattr(
|
|
"src.api.internal.gateway._build_gemini_video_create_sync_plan",
|
|
AsyncMock(
|
|
return_value=GatewayExecutionPlanResponse(
|
|
action="executor_sync",
|
|
plan_kind="gemini_video_create_sync",
|
|
plan=fake_plan,
|
|
report_kind="gemini_video_create_sync_finalize",
|
|
report_context={"user_id": "user-123", "api_key_id": "key-123"},
|
|
)
|
|
),
|
|
)
|
|
|
|
client = _make_legacy_test_client(app)
|
|
response = client.post(
|
|
"/api/internal/gateway/plan-sync",
|
|
json={
|
|
"trace_id": "trace-gemini-video-create-plan",
|
|
"method": "POST",
|
|
"path": "/v1beta/models/veo-3:predictLongRunning",
|
|
"headers": {
|
|
"content-type": "application/json",
|
|
"x-goog-api-key": "client-key",
|
|
},
|
|
"body_json": {"prompt": "make a video"},
|
|
"auth_context": {
|
|
"user_id": "user-123",
|
|
"api_key_id": "key-123",
|
|
"access_allowed": True,
|
|
},
|
|
},
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
assert response.json() == {
|
|
"action": "executor_sync",
|
|
"plan_kind": "gemini_video_create_sync",
|
|
"plan": fake_plan,
|
|
"report_kind": "gemini_video_create_sync_finalize",
|
|
"report_context": {"user_id": "user-123", "api_key_id": "key-123"},
|
|
}
|
|
|
|
|
|
def test_plan_sync_route_returns_execution_runtime_plan_for_openai_video_cancel(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
app = FastAPI()
|
|
app.include_router(router)
|
|
app.dependency_overrides[get_db] = lambda: object()
|
|
monkeypatch.setattr("src.api.internal.gateway.ensure_loopback", lambda request: None)
|
|
|
|
fake_plan = {
|
|
"request_id": "plan-openai-video-cancel-123",
|
|
"provider_id": "provider-openai-video-cancel-123",
|
|
"endpoint_id": "endpoint-openai-video-cancel-123",
|
|
"key_id": "key-openai-video-cancel-123",
|
|
"provider_name": "openai",
|
|
"method": "DELETE",
|
|
"url": "https://api.openai.example/v1/videos/ext-123",
|
|
"headers": {"authorization": "Bearer upstream-key"},
|
|
"body": {},
|
|
"stream": False,
|
|
"provider_api_format": "openai:video",
|
|
"client_api_format": "openai:video",
|
|
"model_name": "sora-2",
|
|
}
|
|
monkeypatch.setattr(
|
|
"src.api.internal.gateway._build_openai_video_cancel_sync_plan",
|
|
AsyncMock(
|
|
return_value=GatewayExecutionPlanResponse(
|
|
action="executor_sync",
|
|
plan_kind="openai_video_cancel_sync",
|
|
plan=fake_plan,
|
|
report_kind="openai_video_cancel_sync_finalize",
|
|
report_context={"user_id": "user-123", "api_key_id": "key-123"},
|
|
)
|
|
),
|
|
)
|
|
|
|
client = _make_legacy_test_client(app)
|
|
response = client.post(
|
|
"/api/internal/gateway/plan-sync",
|
|
json={
|
|
"trace_id": "trace-openai-video-cancel-plan",
|
|
"method": "POST",
|
|
"path": "/v1/videos/task-123/cancel",
|
|
"headers": {
|
|
"authorization": "Bearer client-key",
|
|
},
|
|
"auth_context": {
|
|
"user_id": "user-123",
|
|
"api_key_id": "key-123",
|
|
"access_allowed": True,
|
|
},
|
|
},
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
assert response.json() == {
|
|
"action": "executor_sync",
|
|
"plan_kind": "openai_video_cancel_sync",
|
|
"plan": fake_plan,
|
|
"report_kind": "openai_video_cancel_sync_finalize",
|
|
"report_context": {"user_id": "user-123", "api_key_id": "key-123"},
|
|
}
|
|
|
|
|
|
def test_plan_sync_route_returns_execution_runtime_plan_for_openai_video_delete(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
app = FastAPI()
|
|
app.include_router(router)
|
|
app.dependency_overrides[get_db] = lambda: object()
|
|
monkeypatch.setattr("src.api.internal.gateway.ensure_loopback", lambda request: None)
|
|
|
|
fake_plan = {
|
|
"request_id": "plan-openai-video-delete-123",
|
|
"provider_id": "provider-openai-video-delete-123",
|
|
"endpoint_id": "endpoint-openai-video-delete-123",
|
|
"key_id": "key-openai-video-delete-123",
|
|
"provider_name": "openai",
|
|
"method": "DELETE",
|
|
"url": "https://api.openai.example/v1/videos/ext-123",
|
|
"headers": {"authorization": "Bearer upstream-key"},
|
|
"body": {},
|
|
"stream": False,
|
|
"provider_api_format": "openai:video",
|
|
"client_api_format": "openai:video",
|
|
"model_name": "sora-2",
|
|
}
|
|
monkeypatch.setattr(
|
|
"src.api.internal.gateway._build_openai_video_delete_sync_plan",
|
|
AsyncMock(
|
|
return_value=GatewayExecutionPlanResponse(
|
|
action="executor_sync",
|
|
plan_kind="openai_video_delete_sync",
|
|
plan=fake_plan,
|
|
report_kind="openai_video_delete_sync_finalize",
|
|
report_context={"user_id": "user-123", "api_key_id": "key-123"},
|
|
)
|
|
),
|
|
)
|
|
|
|
client = _make_legacy_test_client(app)
|
|
response = client.post(
|
|
"/api/internal/gateway/plan-sync",
|
|
json={
|
|
"trace_id": "trace-openai-video-delete-plan",
|
|
"method": "DELETE",
|
|
"path": "/v1/videos/task-123",
|
|
"headers": {
|
|
"authorization": "Bearer client-key",
|
|
},
|
|
"auth_context": {
|
|
"user_id": "user-123",
|
|
"api_key_id": "key-123",
|
|
"access_allowed": True,
|
|
},
|
|
},
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
assert response.json() == {
|
|
"action": "executor_sync",
|
|
"plan_kind": "openai_video_delete_sync",
|
|
"plan": fake_plan,
|
|
"report_kind": "openai_video_delete_sync_finalize",
|
|
"report_context": {"user_id": "user-123", "api_key_id": "key-123"},
|
|
}
|
|
|
|
|
|
def test_plan_sync_route_returns_execution_runtime_plan_for_gemini_video_cancel(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
app = FastAPI()
|
|
app.include_router(router)
|
|
app.dependency_overrides[get_db] = lambda: object()
|
|
monkeypatch.setattr("src.api.internal.gateway.ensure_loopback", lambda request: None)
|
|
|
|
fake_plan = {
|
|
"request_id": "plan-gemini-video-cancel-123",
|
|
"provider_id": "provider-gemini-video-cancel-123",
|
|
"endpoint_id": "endpoint-gemini-video-cancel-123",
|
|
"key_id": "key-gemini-video-cancel-123",
|
|
"provider_name": "gemini",
|
|
"method": "POST",
|
|
"url": "https://generativelanguage.googleapis.com/v1beta/models/veo-3/operations/ext-123:cancel",
|
|
"headers": {"authorization": "Bearer upstream-key", "content-type": "application/json"},
|
|
"body": {"json_body": {}},
|
|
"stream": False,
|
|
"provider_api_format": "gemini:video",
|
|
"client_api_format": "gemini:video",
|
|
"model_name": "veo-3",
|
|
}
|
|
monkeypatch.setattr(
|
|
"src.api.internal.gateway._build_gemini_video_cancel_sync_plan",
|
|
AsyncMock(
|
|
return_value=GatewayExecutionPlanResponse(
|
|
action="executor_sync",
|
|
plan_kind="gemini_video_cancel_sync",
|
|
plan=fake_plan,
|
|
report_kind="gemini_video_cancel_sync_finalize",
|
|
report_context={"user_id": "user-123", "api_key_id": "key-123"},
|
|
)
|
|
),
|
|
)
|
|
|
|
client = _make_legacy_test_client(app)
|
|
response = client.post(
|
|
"/api/internal/gateway/plan-sync",
|
|
json={
|
|
"trace_id": "trace-gemini-video-cancel-plan",
|
|
"method": "POST",
|
|
"path": "/v1beta/models/veo-3/operations/ext-123:cancel",
|
|
"headers": {
|
|
"x-goog-api-key": "client-key",
|
|
},
|
|
"body_json": {},
|
|
"auth_context": {
|
|
"user_id": "user-123",
|
|
"api_key_id": "key-123",
|
|
"access_allowed": True,
|
|
},
|
|
},
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
assert response.json() == {
|
|
"action": "executor_sync",
|
|
"plan_kind": "gemini_video_cancel_sync",
|
|
"plan": fake_plan,
|
|
"report_kind": "gemini_video_cancel_sync_finalize",
|
|
"report_context": {"user_id": "user-123", "api_key_id": "key-123"},
|
|
}
|
|
|
|
|
|
def test_plan_stream_route_returns_execution_runtime_plan_for_openai_chat(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
app = FastAPI()
|
|
app.include_router(router)
|
|
app.dependency_overrides[get_db] = lambda: object()
|
|
monkeypatch.setattr("src.api.internal.gateway.ensure_loopback", lambda request: None)
|
|
|
|
fake_plan = {
|
|
"request_id": "plan-openai-chat-stream-123",
|
|
"provider_id": "provider-openai-chat-stream-123",
|
|
"endpoint_id": "endpoint-openai-chat-stream-123",
|
|
"key_id": "key-openai-chat-stream-123",
|
|
"provider_name": "openai",
|
|
"method": "POST",
|
|
"url": "https://api.openai.example/v1/chat/completions",
|
|
"headers": {
|
|
"authorization": "Bearer upstream-key",
|
|
"content-type": "application/json",
|
|
"accept": "text/event-stream",
|
|
},
|
|
"body": {
|
|
"json_body": {
|
|
"model": "gpt-5",
|
|
"messages": [],
|
|
"stream": True,
|
|
}
|
|
},
|
|
"stream": True,
|
|
"provider_api_format": "openai:chat",
|
|
"client_api_format": "openai:chat",
|
|
"model_name": "gpt-5",
|
|
}
|
|
monkeypatch.setattr(
|
|
"src.api.internal.gateway._build_openai_chat_stream_plan",
|
|
AsyncMock(
|
|
return_value=GatewayExecutionPlanResponse(
|
|
action="executor_stream",
|
|
plan_kind="openai_chat_stream",
|
|
plan=fake_plan,
|
|
report_kind="openai_chat_stream_success",
|
|
report_context={"user_id": "user-123", "api_key_id": "key-123"},
|
|
)
|
|
),
|
|
)
|
|
|
|
client = _make_legacy_test_client(app)
|
|
response = client.post(
|
|
"/api/internal/gateway/plan-stream",
|
|
json={
|
|
"trace_id": "trace-openai-chat-stream-plan",
|
|
"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 == 200
|
|
assert response.json() == {
|
|
"action": "executor_stream",
|
|
"plan_kind": "openai_chat_stream",
|
|
"plan": fake_plan,
|
|
"report_kind": "openai_chat_stream_success",
|
|
"report_context": {"user_id": "user-123", "api_key_id": "key-123"},
|
|
}
|
|
|
|
|
|
def test_plan_stream_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)
|
|
|
|
build_plan = AsyncMock(
|
|
return_value=GatewayExecutionPlanResponse(
|
|
action="executor_stream",
|
|
plan_kind="openai_chat_stream",
|
|
plan={
|
|
"request_id": "plan-openai-chat-stream-derived-auth-123",
|
|
"provider_name": "openai",
|
|
"method": "POST",
|
|
"url": "https://api.openai.example/v1/chat/completions",
|
|
"headers": {
|
|
"content-type": "application/json",
|
|
"accept": "text/event-stream",
|
|
},
|
|
"body": {
|
|
"json_body": {"model": "gpt-5", "messages": [], "stream": True}
|
|
},
|
|
"stream": True,
|
|
"provider_api_format": "openai:chat",
|
|
"client_api_format": "openai:chat",
|
|
"model_name": "gpt-5",
|
|
},
|
|
report_kind="openai_chat_stream_success",
|
|
report_context={"user_id": "user-123", "api_key_id": "key-123"},
|
|
)
|
|
)
|
|
monkeypatch.setattr("src.api.internal.gateway._build_openai_chat_stream_plan", build_plan)
|
|
monkeypatch.setattr(
|
|
"src.api.internal.gateway._resolve_auth_context_signature",
|
|
AsyncMock(
|
|
return_value={
|
|
"user_id": "user-123",
|
|
"api_key_id": "key-123",
|
|
"access_allowed": True,
|
|
}
|
|
),
|
|
)
|
|
|
|
client = _make_legacy_test_client(app)
|
|
response = client.post(
|
|
"/api/internal/gateway/plan-stream",
|
|
json={
|
|
"trace_id": "trace-openai-chat-stream-plan-derived-auth",
|
|
"method": "POST",
|
|
"path": "/v1/chat/completions",
|
|
"headers": {
|
|
"content-type": "application/json",
|
|
"authorization": "Bearer client-key",
|
|
},
|
|
"body_json": {"model": "gpt-5", "messages": [], "stream": True},
|
|
},
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
assert response.json()["action"] == "executor_stream"
|
|
assert build_plan.await_args.kwargs["auth_context"].user_id == "user-123"
|
|
assert build_plan.await_args.kwargs["auth_context"].api_key_id == "key-123"
|
|
|
|
|
|
def test_plan_stream_route_returns_execution_runtime_plan_for_claude_chat(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
app = FastAPI()
|
|
app.include_router(router)
|
|
app.dependency_overrides[get_db] = lambda: object()
|
|
monkeypatch.setattr("src.api.internal.gateway.ensure_loopback", lambda request: None)
|
|
|
|
fake_plan = {
|
|
"request_id": "plan-claude-chat-stream-123",
|
|
"provider_id": "provider-claude-chat-stream-123",
|
|
"endpoint_id": "endpoint-claude-chat-stream-123",
|
|
"key_id": "key-claude-chat-stream-123",
|
|
"provider_name": "claude",
|
|
"method": "POST",
|
|
"url": "https://api.anthropic.example/v1/messages",
|
|
"headers": {
|
|
"x-api-key": "upstream-key",
|
|
"content-type": "application/json",
|
|
"accept": "text/event-stream",
|
|
},
|
|
"body": {
|
|
"json_body": {
|
|
"model": "claude-sonnet-4",
|
|
"messages": [],
|
|
"stream": True,
|
|
}
|
|
},
|
|
"stream": True,
|
|
"provider_api_format": "claude:chat",
|
|
"client_api_format": "claude:chat",
|
|
"model_name": "claude-sonnet-4",
|
|
}
|
|
monkeypatch.setattr(
|
|
"src.api.internal.gateway._build_claude_chat_stream_plan",
|
|
AsyncMock(
|
|
return_value=GatewayExecutionPlanResponse(
|
|
action="executor_stream",
|
|
plan_kind="claude_chat_stream",
|
|
plan=fake_plan,
|
|
report_kind="claude_chat_stream_success",
|
|
report_context={"user_id": "user-123", "api_key_id": "key-123"},
|
|
)
|
|
),
|
|
)
|
|
|
|
client = _make_legacy_test_client(app)
|
|
response = client.post(
|
|
"/api/internal/gateway/plan-stream",
|
|
json={
|
|
"trace_id": "trace-claude-chat-stream-plan",
|
|
"method": "POST",
|
|
"path": "/v1/messages",
|
|
"headers": {
|
|
"content-type": "application/json",
|
|
"x-api-key": "client-key",
|
|
},
|
|
"body_json": {"model": "claude-sonnet-4", "messages": [], "stream": True},
|
|
"auth_context": {
|
|
"user_id": "user-123",
|
|
"api_key_id": "key-123",
|
|
"access_allowed": True,
|
|
},
|
|
},
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
assert response.json() == {
|
|
"action": "executor_stream",
|
|
"plan_kind": "claude_chat_stream",
|
|
"plan": fake_plan,
|
|
"report_kind": "claude_chat_stream_success",
|
|
"report_context": {"user_id": "user-123", "api_key_id": "key-123"},
|
|
}
|
|
|
|
|
|
def test_plan_stream_route_returns_execution_runtime_plan_for_gemini_chat(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
app = FastAPI()
|
|
app.include_router(router)
|
|
app.dependency_overrides[get_db] = lambda: object()
|
|
monkeypatch.setattr("src.api.internal.gateway.ensure_loopback", lambda request: None)
|
|
|
|
fake_plan = {
|
|
"request_id": "plan-gemini-chat-stream-123",
|
|
"provider_id": "provider-gemini-chat-stream-123",
|
|
"endpoint_id": "endpoint-gemini-chat-stream-123",
|
|
"key_id": "key-gemini-chat-stream-123",
|
|
"provider_name": "gemini",
|
|
"method": "POST",
|
|
"url": "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-pro:streamGenerateContent",
|
|
"headers": {
|
|
"x-goog-api-key": "upstream-key",
|
|
"content-type": "application/json",
|
|
"accept": "text/event-stream",
|
|
},
|
|
"body": {"json_body": {"contents": [{"role": "user", "parts": [{"text": "hi"}]}]}},
|
|
"stream": True,
|
|
"provider_api_format": "gemini:chat",
|
|
"client_api_format": "gemini:chat",
|
|
"model_name": "gemini-2.5-pro",
|
|
}
|
|
monkeypatch.setattr(
|
|
"src.api.internal.gateway._build_gemini_chat_stream_plan",
|
|
AsyncMock(
|
|
return_value=GatewayExecutionPlanResponse(
|
|
action="executor_stream",
|
|
plan_kind="gemini_chat_stream",
|
|
plan=fake_plan,
|
|
report_kind="gemini_chat_stream_success",
|
|
report_context={"user_id": "user-123", "api_key_id": "key-123"},
|
|
)
|
|
),
|
|
)
|
|
|
|
client = _make_legacy_test_client(app)
|
|
response = client.post(
|
|
"/api/internal/gateway/plan-stream",
|
|
json={
|
|
"trace_id": "trace-gemini-chat-stream-plan",
|
|
"method": "POST",
|
|
"path": "/v1beta/models/gemini-2.5-pro:streamGenerateContent",
|
|
"headers": {
|
|
"content-type": "application/json",
|
|
"x-goog-api-key": "client-key",
|
|
},
|
|
"body_json": {"contents": [{"role": "user", "parts": [{"text": "hi"}]}]},
|
|
"auth_context": {
|
|
"user_id": "user-123",
|
|
"api_key_id": "key-123",
|
|
"access_allowed": True,
|
|
},
|
|
},
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
assert response.json() == {
|
|
"action": "executor_stream",
|
|
"plan_kind": "gemini_chat_stream",
|
|
"plan": fake_plan,
|
|
"report_kind": "gemini_chat_stream_success",
|
|
"report_context": {"user_id": "user-123", "api_key_id": "key-123"},
|
|
}
|
|
|
|
|
|
def test_plan_stream_route_returns_execution_runtime_plan_for_openai_cli(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
app = FastAPI()
|
|
app.include_router(router)
|
|
app.dependency_overrides[get_db] = lambda: object()
|
|
monkeypatch.setattr("src.api.internal.gateway.ensure_loopback", lambda request: None)
|
|
|
|
fake_plan = {
|
|
"request_id": "plan-openai-cli-stream-123",
|
|
"provider_id": "provider-openai-cli-stream-123",
|
|
"endpoint_id": "endpoint-openai-cli-stream-123",
|
|
"key_id": "key-openai-cli-stream-123",
|
|
"provider_name": "openai",
|
|
"method": "POST",
|
|
"url": "https://api.openai.example/v1/responses",
|
|
"headers": {
|
|
"authorization": "Bearer upstream-key",
|
|
"content-type": "application/json",
|
|
"accept": "text/event-stream",
|
|
},
|
|
"body": {"json_body": {"model": "gpt-5", "input": "hello", "stream": True}},
|
|
"stream": True,
|
|
"provider_api_format": "openai:cli",
|
|
"client_api_format": "openai:cli",
|
|
"model_name": "gpt-5",
|
|
}
|
|
monkeypatch.setattr(
|
|
"src.api.internal.gateway._build_openai_cli_stream_plan",
|
|
AsyncMock(
|
|
return_value=GatewayExecutionPlanResponse(
|
|
action="executor_stream",
|
|
plan_kind="openai_cli_stream",
|
|
plan=fake_plan,
|
|
report_kind="openai_cli_stream_success",
|
|
report_context={"user_id": "user-123", "api_key_id": "key-123"},
|
|
)
|
|
),
|
|
)
|
|
|
|
client = _make_legacy_test_client(app)
|
|
response = client.post(
|
|
"/api/internal/gateway/plan-stream",
|
|
json={
|
|
"trace_id": "trace-openai-cli-stream-plan",
|
|
"method": "POST",
|
|
"path": "/v1/responses",
|
|
"headers": {
|
|
"content-type": "application/json",
|
|
"authorization": "Bearer client-key",
|
|
},
|
|
"body_json": {"model": "gpt-5", "input": "hello", "stream": True},
|
|
"auth_context": {
|
|
"user_id": "user-123",
|
|
"api_key_id": "key-123",
|
|
"access_allowed": True,
|
|
},
|
|
},
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
assert response.json() == {
|
|
"action": "executor_stream",
|
|
"plan_kind": "openai_cli_stream",
|
|
"plan": fake_plan,
|
|
"report_kind": "openai_cli_stream_success",
|
|
"report_context": {"user_id": "user-123", "api_key_id": "key-123"},
|
|
}
|
|
|
|
|
|
def test_plan_stream_route_returns_execution_runtime_plan_for_claude_cli(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
app = FastAPI()
|
|
app.include_router(router)
|
|
app.dependency_overrides[get_db] = lambda: object()
|
|
monkeypatch.setattr("src.api.internal.gateway.ensure_loopback", lambda request: None)
|
|
|
|
fake_plan = {
|
|
"request_id": "plan-claude-cli-stream-123",
|
|
"provider_id": "provider-claude-cli-stream-123",
|
|
"endpoint_id": "endpoint-claude-cli-stream-123",
|
|
"key_id": "key-claude-cli-stream-123",
|
|
"provider_name": "claude",
|
|
"method": "POST",
|
|
"url": "https://api.anthropic.example/v1/messages",
|
|
"headers": {
|
|
"x-api-key": "upstream-key",
|
|
"content-type": "application/json",
|
|
"accept": "text/event-stream",
|
|
},
|
|
"body": {
|
|
"json_body": {
|
|
"model": "claude-sonnet-4",
|
|
"messages": [],
|
|
"stream": True,
|
|
}
|
|
},
|
|
"stream": True,
|
|
"provider_api_format": "claude:cli",
|
|
"client_api_format": "claude:cli",
|
|
"model_name": "claude-sonnet-4",
|
|
}
|
|
monkeypatch.setattr(
|
|
"src.api.internal.gateway._build_claude_cli_stream_plan",
|
|
AsyncMock(
|
|
return_value=GatewayExecutionPlanResponse(
|
|
action="executor_stream",
|
|
plan_kind="claude_cli_stream",
|
|
plan=fake_plan,
|
|
report_kind="claude_cli_stream_success",
|
|
report_context={"user_id": "user-123", "api_key_id": "key-123"},
|
|
)
|
|
),
|
|
)
|
|
|
|
client = _make_legacy_test_client(app)
|
|
response = client.post(
|
|
"/api/internal/gateway/plan-stream",
|
|
json={
|
|
"trace_id": "trace-claude-cli-stream-plan",
|
|
"method": "POST",
|
|
"path": "/v1/messages",
|
|
"headers": {
|
|
"content-type": "application/json",
|
|
"authorization": "Bearer client-key",
|
|
"anthropic-beta": "output-128k-2025-02-19",
|
|
},
|
|
"body_json": {
|
|
"model": "claude-sonnet-4",
|
|
"messages": [],
|
|
"stream": True,
|
|
},
|
|
"auth_context": {
|
|
"user_id": "user-123",
|
|
"api_key_id": "key-123",
|
|
"access_allowed": True,
|
|
},
|
|
},
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
assert response.json() == {
|
|
"action": "executor_stream",
|
|
"plan_kind": "claude_cli_stream",
|
|
"plan": fake_plan,
|
|
"report_kind": "claude_cli_stream_success",
|
|
"report_context": {"user_id": "user-123", "api_key_id": "key-123"},
|
|
}
|
|
|
|
|
|
def test_plan_stream_route_returns_execution_runtime_plan_for_gemini_cli(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
app = FastAPI()
|
|
app.include_router(router)
|
|
app.dependency_overrides[get_db] = lambda: object()
|
|
monkeypatch.setattr("src.api.internal.gateway.ensure_loopback", lambda request: None)
|
|
|
|
fake_plan = {
|
|
"request_id": "plan-gemini-cli-stream-123",
|
|
"provider_id": "provider-gemini-cli-stream-123",
|
|
"endpoint_id": "endpoint-gemini-cli-stream-123",
|
|
"key_id": "key-gemini-cli-stream-123",
|
|
"provider_name": "gemini",
|
|
"method": "POST",
|
|
"url": "https://generativelanguage.googleapis.com/v1beta/models/gemini-cli:streamGenerateContent",
|
|
"headers": {
|
|
"x-goog-api-key": "upstream-key",
|
|
"content-type": "application/json",
|
|
"accept": "text/event-stream",
|
|
},
|
|
"body": {"json_body": {"contents": [{"role": "user", "parts": [{"text": "hi"}]}]}},
|
|
"stream": True,
|
|
"provider_api_format": "gemini:cli",
|
|
"client_api_format": "gemini:cli",
|
|
"model_name": "gemini-cli",
|
|
}
|
|
monkeypatch.setattr(
|
|
"src.api.internal.gateway._build_gemini_cli_stream_plan",
|
|
AsyncMock(
|
|
return_value=GatewayExecutionPlanResponse(
|
|
action="executor_stream",
|
|
plan_kind="gemini_cli_stream",
|
|
plan=fake_plan,
|
|
report_kind="gemini_cli_stream_success",
|
|
report_context={"user_id": "user-123", "api_key_id": "key-123"},
|
|
)
|
|
),
|
|
)
|
|
|
|
client = _make_legacy_test_client(app)
|
|
response = client.post(
|
|
"/api/internal/gateway/plan-stream",
|
|
json={
|
|
"trace_id": "trace-gemini-cli-stream-plan",
|
|
"method": "POST",
|
|
"path": "/v1beta/models/gemini-cli:streamGenerateContent",
|
|
"headers": {
|
|
"content-type": "application/json",
|
|
"user-agent": "GeminiCLI/1.0",
|
|
"x-goog-api-key": "client-key",
|
|
},
|
|
"body_json": {"contents": [{"role": "user", "parts": [{"text": "hi"}]}]},
|
|
"auth_context": {
|
|
"user_id": "user-123",
|
|
"api_key_id": "key-123",
|
|
"access_allowed": True,
|
|
},
|
|
},
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
assert response.json() == {
|
|
"action": "executor_stream",
|
|
"plan_kind": "gemini_cli_stream",
|
|
"plan": fake_plan,
|
|
"report_kind": "gemini_cli_stream_success",
|
|
"report_context": {"user_id": "user-123", "api_key_id": "key-123"},
|
|
}
|
|
|
|
|
|
def test_plan_sync_route_returns_execution_runtime_plan_for_openai_cli(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
app = FastAPI()
|
|
app.include_router(router)
|
|
app.dependency_overrides[get_db] = lambda: object()
|
|
monkeypatch.setattr("src.api.internal.gateway.ensure_loopback", lambda request: None)
|
|
|
|
fake_plan = {
|
|
"request_id": "plan-openai-cli-123",
|
|
"provider_id": "provider-openai-cli-123",
|
|
"endpoint_id": "endpoint-openai-cli-123",
|
|
"key_id": "key-openai-cli-123",
|
|
"provider_name": "openai",
|
|
"method": "POST",
|
|
"url": "https://api.openai.example/v1/responses",
|
|
"headers": {"authorization": "Bearer upstream-key", "content-type": "application/json"},
|
|
"body": {"json_body": {"model": "gpt-5", "input": "hello"}},
|
|
"stream": False,
|
|
"provider_api_format": "openai:cli",
|
|
"client_api_format": "openai:cli",
|
|
"model_name": "gpt-5",
|
|
}
|
|
monkeypatch.setattr(
|
|
"src.api.internal.gateway._build_openai_cli_sync_plan",
|
|
AsyncMock(
|
|
return_value=GatewayExecutionPlanResponse(
|
|
action="executor_sync",
|
|
plan_kind="openai_cli_sync",
|
|
plan=fake_plan,
|
|
report_kind="openai_cli_sync_success",
|
|
report_context={"user_id": "user-123", "api_key_id": "key-123"},
|
|
)
|
|
),
|
|
)
|
|
|
|
client = _make_legacy_test_client(app)
|
|
response = client.post(
|
|
"/api/internal/gateway/plan-sync",
|
|
json={
|
|
"trace_id": "trace-openai-cli-plan",
|
|
"method": "POST",
|
|
"path": "/v1/responses",
|
|
"headers": {
|
|
"content-type": "application/json",
|
|
"authorization": "Bearer client-key",
|
|
},
|
|
"body_json": {"model": "gpt-5", "input": "hello"},
|
|
"auth_context": {
|
|
"user_id": "user-123",
|
|
"api_key_id": "key-123",
|
|
"access_allowed": True,
|
|
},
|
|
},
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
assert response.json() == {
|
|
"action": "executor_sync",
|
|
"plan_kind": "openai_cli_sync",
|
|
"plan": fake_plan,
|
|
"report_kind": "openai_cli_sync_success",
|
|
"report_context": {"user_id": "user-123", "api_key_id": "key-123"},
|
|
}
|
|
|
|
|
|
def test_plan_sync_route_returns_execution_runtime_plan_for_openai_compact(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
app = FastAPI()
|
|
app.include_router(router)
|
|
app.dependency_overrides[get_db] = lambda: object()
|
|
monkeypatch.setattr("src.api.internal.gateway.ensure_loopback", lambda request: None)
|
|
|
|
fake_plan = {
|
|
"request_id": "plan-openai-compact-123",
|
|
"provider_id": "provider-openai-compact-123",
|
|
"endpoint_id": "endpoint-openai-compact-123",
|
|
"key_id": "key-openai-compact-123",
|
|
"provider_name": "openai",
|
|
"method": "POST",
|
|
"url": "https://api.openai.example/v1/responses/compact",
|
|
"headers": {"authorization": "Bearer upstream-key", "content-type": "application/json"},
|
|
"body": {"json_body": {"model": "gpt-5", "input": "hello"}},
|
|
"stream": False,
|
|
"provider_api_format": "openai:compact",
|
|
"client_api_format": "openai:compact",
|
|
"model_name": "gpt-5",
|
|
}
|
|
monkeypatch.setattr(
|
|
"src.api.internal.gateway._build_openai_cli_sync_plan",
|
|
AsyncMock(
|
|
return_value=GatewayExecutionPlanResponse(
|
|
action="executor_sync",
|
|
plan_kind="openai_compact_sync",
|
|
plan=fake_plan,
|
|
report_kind="openai_cli_sync_success",
|
|
report_context={"user_id": "user-123", "api_key_id": "key-123"},
|
|
)
|
|
),
|
|
)
|
|
|
|
client = _make_legacy_test_client(app)
|
|
response = client.post(
|
|
"/api/internal/gateway/plan-sync",
|
|
json={
|
|
"trace_id": "trace-openai-compact-plan",
|
|
"method": "POST",
|
|
"path": "/v1/responses/compact",
|
|
"headers": {
|
|
"content-type": "application/json",
|
|
"authorization": "Bearer client-key",
|
|
},
|
|
"body_json": {"model": "gpt-5", "input": "hello"},
|
|
"auth_context": {
|
|
"user_id": "user-123",
|
|
"api_key_id": "key-123",
|
|
"access_allowed": True,
|
|
},
|
|
},
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
assert response.json() == {
|
|
"action": "executor_sync",
|
|
"plan_kind": "openai_compact_sync",
|
|
"plan": fake_plan,
|
|
"report_kind": "openai_cli_sync_success",
|
|
"report_context": {"user_id": "user-123", "api_key_id": "key-123"},
|
|
}
|
|
|
|
|
|
def test_plan_sync_route_returns_execution_runtime_plan_for_claude_chat(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
app = FastAPI()
|
|
app.include_router(router)
|
|
app.dependency_overrides[get_db] = lambda: object()
|
|
monkeypatch.setattr("src.api.internal.gateway.ensure_loopback", lambda request: None)
|
|
|
|
fake_plan = {
|
|
"request_id": "plan-claude-chat-123",
|
|
"provider_id": "provider-claude-chat-123",
|
|
"endpoint_id": "endpoint-claude-chat-123",
|
|
"key_id": "key-claude-chat-123",
|
|
"provider_name": "claude",
|
|
"method": "POST",
|
|
"url": "https://api.anthropic.example/v1/messages",
|
|
"headers": {"x-api-key": "upstream-key", "content-type": "application/json"},
|
|
"body": {"json_body": {"model": "claude-sonnet-4", "messages": []}},
|
|
"stream": False,
|
|
"provider_api_format": "claude:chat",
|
|
"client_api_format": "claude:chat",
|
|
"model_name": "claude-sonnet-4",
|
|
}
|
|
monkeypatch.setattr(
|
|
"src.api.internal.gateway._build_claude_chat_sync_plan",
|
|
AsyncMock(
|
|
return_value=GatewayExecutionPlanResponse(
|
|
action="executor_sync",
|
|
plan_kind="claude_chat_sync",
|
|
plan=fake_plan,
|
|
report_kind="claude_chat_sync_success",
|
|
report_context={"user_id": "user-123", "api_key_id": "key-123"},
|
|
)
|
|
),
|
|
)
|
|
|
|
client = _make_legacy_test_client(app)
|
|
response = client.post(
|
|
"/api/internal/gateway/plan-sync",
|
|
json={
|
|
"trace_id": "trace-claude-chat-plan",
|
|
"method": "POST",
|
|
"path": "/v1/messages",
|
|
"headers": {
|
|
"content-type": "application/json",
|
|
"x-api-key": "client-key",
|
|
},
|
|
"body_json": {"model": "claude-sonnet-4", "messages": []},
|
|
"auth_context": {
|
|
"user_id": "user-123",
|
|
"api_key_id": "key-123",
|
|
"access_allowed": True,
|
|
},
|
|
},
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
assert response.json() == {
|
|
"action": "executor_sync",
|
|
"plan_kind": "claude_chat_sync",
|
|
"plan": fake_plan,
|
|
"report_kind": "claude_chat_sync_success",
|
|
"report_context": {"user_id": "user-123", "api_key_id": "key-123"},
|
|
}
|
|
|
|
|
|
def test_plan_sync_route_returns_execution_runtime_plan_for_gemini_chat(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
app = FastAPI()
|
|
app.include_router(router)
|
|
app.dependency_overrides[get_db] = lambda: object()
|
|
monkeypatch.setattr("src.api.internal.gateway.ensure_loopback", lambda request: None)
|
|
|
|
fake_plan = {
|
|
"request_id": "plan-gemini-chat-123",
|
|
"provider_id": "provider-gemini-chat-123",
|
|
"endpoint_id": "endpoint-gemini-chat-123",
|
|
"key_id": "key-gemini-chat-123",
|
|
"provider_name": "gemini",
|
|
"method": "POST",
|
|
"url": "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-pro:generateContent",
|
|
"headers": {"x-goog-api-key": "upstream-key", "content-type": "application/json"},
|
|
"body": {"json_body": {"contents": [{"role": "user", "parts": [{"text": "hi"}]}]}},
|
|
"stream": False,
|
|
"provider_api_format": "gemini:chat",
|
|
"client_api_format": "gemini:chat",
|
|
"model_name": "gemini-2.5-pro",
|
|
}
|
|
monkeypatch.setattr(
|
|
"src.api.internal.gateway._build_gemini_chat_sync_plan",
|
|
AsyncMock(
|
|
return_value=GatewayExecutionPlanResponse(
|
|
action="executor_sync",
|
|
plan_kind="gemini_chat_sync",
|
|
plan=fake_plan,
|
|
report_kind="gemini_chat_sync_success",
|
|
report_context={"user_id": "user-123", "api_key_id": "key-123"},
|
|
)
|
|
),
|
|
)
|
|
|
|
client = _make_legacy_test_client(app)
|
|
response = client.post(
|
|
"/api/internal/gateway/plan-sync",
|
|
json={
|
|
"trace_id": "trace-gemini-chat-plan",
|
|
"method": "POST",
|
|
"path": "/v1beta/models/gemini-2.5-pro:generateContent",
|
|
"headers": {
|
|
"content-type": "application/json",
|
|
"x-goog-api-key": "client-key",
|
|
},
|
|
"body_json": {"contents": [{"role": "user", "parts": [{"text": "hi"}]}]},
|
|
"auth_context": {
|
|
"user_id": "user-123",
|
|
"api_key_id": "key-123",
|
|
"access_allowed": True,
|
|
},
|
|
},
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
assert response.json() == {
|
|
"action": "executor_sync",
|
|
"plan_kind": "gemini_chat_sync",
|
|
"plan": fake_plan,
|
|
"report_kind": "gemini_chat_sync_success",
|
|
"report_context": {"user_id": "user-123", "api_key_id": "key-123"},
|
|
}
|
|
|
|
|
|
def test_plan_sync_route_returns_execution_runtime_plan_for_claude_cli(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
app = FastAPI()
|
|
app.include_router(router)
|
|
app.dependency_overrides[get_db] = lambda: object()
|
|
monkeypatch.setattr("src.api.internal.gateway.ensure_loopback", lambda request: None)
|
|
|
|
fake_plan = {
|
|
"request_id": "plan-claude-cli-123",
|
|
"provider_id": "provider-claude-cli-123",
|
|
"endpoint_id": "endpoint-claude-cli-123",
|
|
"key_id": "key-claude-cli-123",
|
|
"provider_name": "claude",
|
|
"method": "POST",
|
|
"url": "https://api.anthropic.example/v1/messages",
|
|
"headers": {"x-api-key": "upstream-key", "content-type": "application/json"},
|
|
"body": {"json_body": {"model": "claude-code", "messages": []}},
|
|
"stream": False,
|
|
"provider_api_format": "claude:cli",
|
|
"client_api_format": "claude:cli",
|
|
"model_name": "claude-code",
|
|
}
|
|
monkeypatch.setattr(
|
|
"src.api.internal.gateway._build_claude_cli_sync_plan",
|
|
AsyncMock(
|
|
return_value=GatewayExecutionPlanResponse(
|
|
action="executor_sync",
|
|
plan_kind="claude_cli_sync",
|
|
plan=fake_plan,
|
|
report_kind="claude_cli_sync_success",
|
|
report_context={"user_id": "user-123", "api_key_id": "key-123"},
|
|
)
|
|
),
|
|
)
|
|
|
|
client = _make_legacy_test_client(app)
|
|
response = client.post(
|
|
"/api/internal/gateway/plan-sync",
|
|
json={
|
|
"trace_id": "trace-claude-cli-plan",
|
|
"method": "POST",
|
|
"path": "/v1/messages",
|
|
"headers": {
|
|
"content-type": "application/json",
|
|
"authorization": "Bearer cli-key",
|
|
},
|
|
"body_json": {"model": "claude-code", "messages": []},
|
|
"auth_context": {
|
|
"user_id": "user-123",
|
|
"api_key_id": "key-123",
|
|
"access_allowed": True,
|
|
},
|
|
},
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
assert response.json() == {
|
|
"action": "executor_sync",
|
|
"plan_kind": "claude_cli_sync",
|
|
"plan": fake_plan,
|
|
"report_kind": "claude_cli_sync_success",
|
|
"report_context": {"user_id": "user-123", "api_key_id": "key-123"},
|
|
}
|
|
|
|
|
|
def test_plan_sync_route_returns_execution_runtime_plan_for_gemini_cli(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
app = FastAPI()
|
|
app.include_router(router)
|
|
app.dependency_overrides[get_db] = lambda: object()
|
|
monkeypatch.setattr("src.api.internal.gateway.ensure_loopback", lambda request: None)
|
|
|
|
fake_plan = {
|
|
"request_id": "plan-gemini-cli-123",
|
|
"provider_id": "provider-gemini-cli-123",
|
|
"endpoint_id": "endpoint-gemini-cli-123",
|
|
"key_id": "key-gemini-cli-123",
|
|
"provider_name": "gemini",
|
|
"method": "POST",
|
|
"url": "https://generativelanguage.googleapis.com/v1beta/models/gemini-cli:generateContent",
|
|
"headers": {"x-goog-api-key": "upstream-key", "content-type": "application/json"},
|
|
"body": {"json_body": {"contents": []}},
|
|
"stream": False,
|
|
"provider_api_format": "gemini:cli",
|
|
"client_api_format": "gemini:cli",
|
|
"model_name": "gemini-cli",
|
|
}
|
|
monkeypatch.setattr(
|
|
"src.api.internal.gateway._build_gemini_cli_sync_plan",
|
|
AsyncMock(
|
|
return_value=GatewayExecutionPlanResponse(
|
|
action="executor_sync",
|
|
plan_kind="gemini_cli_sync",
|
|
plan=fake_plan,
|
|
report_kind="gemini_cli_sync_success",
|
|
report_context={"user_id": "user-123", "api_key_id": "key-123"},
|
|
)
|
|
),
|
|
)
|
|
|
|
client = _make_legacy_test_client(app)
|
|
response = client.post(
|
|
"/api/internal/gateway/plan-sync",
|
|
json={
|
|
"trace_id": "trace-gemini-cli-plan",
|
|
"method": "POST",
|
|
"path": "/v1beta/models/gemini-cli:generateContent",
|
|
"headers": {
|
|
"content-type": "application/json",
|
|
"user-agent": "GeminiCLI/1.0",
|
|
"x-goog-api-key": "client-key",
|
|
},
|
|
"body_json": {"contents": []},
|
|
"auth_context": {
|
|
"user_id": "user-123",
|
|
"api_key_id": "key-123",
|
|
"access_allowed": True,
|
|
},
|
|
},
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
assert response.json() == {
|
|
"action": "executor_sync",
|
|
"plan_kind": "gemini_cli_sync",
|
|
"plan": fake_plan,
|
|
"report_kind": "gemini_cli_sync_success",
|
|
"report_context": {"user_id": "user-123", "api_key_id": "key-123"},
|
|
}
|
|
|
|
|
|
def test_plan_sync_route_returns_execution_runtime_plan_for_gemini_files_list(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
app = FastAPI()
|
|
app.include_router(router)
|
|
app.dependency_overrides[get_db] = lambda: object()
|
|
monkeypatch.setattr("src.api.internal.gateway.ensure_loopback", lambda request: None)
|
|
|
|
fake_plan = {
|
|
"request_id": "plan-files-list-123",
|
|
"provider_id": "provider-files-123",
|
|
"endpoint_id": "endpoint-files-123",
|
|
"key_id": "file-key-123",
|
|
"method": "GET",
|
|
"url": "https://example.com/v1beta/files?pageSize=10",
|
|
"headers": {"x-goog-api-key": "upstream-key"},
|
|
"body": {},
|
|
"stream": False,
|
|
"provider_api_format": "gemini:files",
|
|
"client_api_format": "gemini:files",
|
|
"model_name": "gemini-files",
|
|
}
|
|
|
|
monkeypatch.setattr(
|
|
"src.api.internal.gateway._load_gateway_auth_models",
|
|
lambda db, auth_context: (
|
|
SimpleNamespace(id="user-123"),
|
|
SimpleNamespace(id="key-123"),
|
|
),
|
|
)
|
|
monkeypatch.setattr(
|
|
"src.api.internal.gateway.get_pipeline",
|
|
lambda: SimpleNamespace(_check_user_rate_limit=AsyncMock(return_value=None)),
|
|
)
|
|
monkeypatch.setattr(
|
|
"src.api.internal.gateway._build_gemini_files_proxy_sync_plan",
|
|
AsyncMock(return_value=(fake_plan, {"file_key_id": "file-key-123", "user_id": "user-123"})),
|
|
)
|
|
|
|
client = _make_legacy_test_client(app)
|
|
response = client.post(
|
|
"/api/internal/gateway/plan-sync",
|
|
json={
|
|
"trace_id": "trace-files-list-plan",
|
|
"method": "GET",
|
|
"path": "/v1beta/files",
|
|
"query_string": "pageSize=10",
|
|
"headers": {"x-goog-api-key": "client-key"},
|
|
"auth_context": {
|
|
"user_id": "user-123",
|
|
"api_key_id": "key-123",
|
|
"access_allowed": True,
|
|
},
|
|
},
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
assert response.json() == {
|
|
"action": "executor_sync",
|
|
"plan_kind": "gemini_files_list",
|
|
"plan": fake_plan,
|
|
"report_kind": "gemini_files_store_mapping",
|
|
"report_context": {"file_key_id": "file-key-123", "user_id": "user-123"},
|
|
}
|
|
|
|
|
|
def test_plan_sync_route_returns_execution_runtime_plan_for_gemini_files_upload(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
app = FastAPI()
|
|
app.include_router(router)
|
|
app.dependency_overrides[get_db] = lambda: object()
|
|
monkeypatch.setattr("src.api.internal.gateway.ensure_loopback", lambda request: None)
|
|
|
|
fake_plan = {
|
|
"request_id": "plan-files-upload-123",
|
|
"provider_id": "provider-files-123",
|
|
"endpoint_id": "endpoint-files-123",
|
|
"key_id": "file-key-123",
|
|
"method": "POST",
|
|
"url": "https://example.com/upload/v1beta/files?uploadType=resumable",
|
|
"headers": {"x-goog-api-key": "upstream-key", "content-type": "application/octet-stream"},
|
|
"body": {"body_bytes_b64": base64.b64encode(b"upload-body").decode("ascii")},
|
|
"stream": False,
|
|
"provider_api_format": "gemini:files",
|
|
"client_api_format": "gemini:files",
|
|
"model_name": "gemini-files",
|
|
}
|
|
|
|
monkeypatch.setattr(
|
|
"src.api.internal.gateway._load_gateway_auth_models",
|
|
lambda db, auth_context: (
|
|
SimpleNamespace(id="user-123"),
|
|
SimpleNamespace(id="key-123"),
|
|
),
|
|
)
|
|
monkeypatch.setattr(
|
|
"src.api.internal.gateway.get_pipeline",
|
|
lambda: SimpleNamespace(_check_user_rate_limit=AsyncMock(return_value=None)),
|
|
)
|
|
monkeypatch.setattr(
|
|
"src.api.internal.gateway._build_gemini_files_proxy_sync_plan",
|
|
AsyncMock(return_value=(fake_plan, {"file_key_id": "file-key-123", "user_id": "user-123"})),
|
|
)
|
|
|
|
client = _make_legacy_test_client(app)
|
|
response = client.post(
|
|
"/api/internal/gateway/plan-sync",
|
|
json={
|
|
"trace_id": "trace-files-upload-plan",
|
|
"method": "POST",
|
|
"path": "/upload/v1beta/files",
|
|
"query_string": "uploadType=resumable",
|
|
"headers": {
|
|
"x-goog-api-key": "client-key",
|
|
"content-type": "application/octet-stream",
|
|
},
|
|
"body_base64": base64.b64encode(b"upload-body").decode("ascii"),
|
|
"auth_context": {
|
|
"user_id": "user-123",
|
|
"api_key_id": "key-123",
|
|
"access_allowed": True,
|
|
},
|
|
},
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
assert response.json() == {
|
|
"action": "executor_sync",
|
|
"plan_kind": "gemini_files_upload",
|
|
"plan": fake_plan,
|
|
"report_kind": "gemini_files_store_mapping",
|
|
"report_context": {"file_key_id": "file-key-123", "user_id": "user-123"},
|
|
}
|
|
|
|
|
|
def test_plan_sync_route_returns_execution_runtime_plan_for_gemini_files_delete(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
app = FastAPI()
|
|
app.include_router(router)
|
|
app.dependency_overrides[get_db] = lambda: object()
|
|
monkeypatch.setattr("src.api.internal.gateway.ensure_loopback", lambda request: None)
|
|
|
|
fake_plan = {
|
|
"request_id": "plan-files-delete-123",
|
|
"provider_id": "provider-files-123",
|
|
"endpoint_id": "endpoint-files-123",
|
|
"key_id": "file-key-123",
|
|
"method": "DELETE",
|
|
"url": "https://example.com/v1beta/files/files/abc-123",
|
|
"headers": {"x-goog-api-key": "upstream-key"},
|
|
"body": {},
|
|
"stream": False,
|
|
"provider_api_format": "gemini:files",
|
|
"client_api_format": "gemini:files",
|
|
"model_name": "gemini-files",
|
|
}
|
|
|
|
monkeypatch.setattr(
|
|
"src.api.internal.gateway._load_gateway_auth_models",
|
|
lambda db, auth_context: (
|
|
SimpleNamespace(id="user-123"),
|
|
SimpleNamespace(id="key-123"),
|
|
),
|
|
)
|
|
monkeypatch.setattr(
|
|
"src.api.internal.gateway.get_pipeline",
|
|
lambda: SimpleNamespace(_check_user_rate_limit=AsyncMock(return_value=None)),
|
|
)
|
|
monkeypatch.setattr(
|
|
"src.api.internal.gateway._build_gemini_files_proxy_sync_plan",
|
|
AsyncMock(return_value=(fake_plan, {})),
|
|
)
|
|
|
|
client = _make_legacy_test_client(app)
|
|
response = client.post(
|
|
"/api/internal/gateway/plan-sync",
|
|
json={
|
|
"trace_id": "trace-files-delete-plan",
|
|
"method": "DELETE",
|
|
"path": "/v1beta/files/files/abc-123",
|
|
"headers": {"x-goog-api-key": "client-key"},
|
|
"auth_context": {
|
|
"user_id": "user-123",
|
|
"api_key_id": "key-123",
|
|
"access_allowed": True,
|
|
},
|
|
},
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
assert response.json() == {
|
|
"action": "executor_sync",
|
|
"plan_kind": "gemini_files_delete",
|
|
"plan": fake_plan,
|
|
"report_kind": "gemini_files_delete_mapping",
|
|
"report_context": {"file_name": "files/abc-123"},
|
|
}
|