Files
Aether/tests/api/internal_gateway/test_chat_decision_plan.py
fawney19 8f26e1a31f refactor: 移除独立 hub/proxy/executor/gateway crate,统一为 gateway tunnel 架构
- 删除 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 服务层和测试适配新架构命名
2026-04-03 14:59:58 +08:00

2045 lines
73 KiB
Python

"""Gateway internal decision/plan builder tests."""
import asyncio
import base64
import json
import time
from types import SimpleNamespace
from typing import Any
from unittest.mock import AsyncMock, MagicMock
import pytest
from fastapi import BackgroundTasks, FastAPI
from fastapi.responses import JSONResponse, StreamingResponse
from fastapi.testclient import TestClient
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from src.api.internal.gateway import (
CONTROL_ACTION_HEADER,
CONTROL_ACTION_PROXY_PUBLIC,
CONTROL_EXECUTED_HEADER,
GatewayAuthContext,
GatewayExecutionDecisionResponse,
GatewayExecuteRequest,
GatewayExecutionPlanResponse,
GatewayResolveRequest,
GatewayStreamReportRequest,
GatewaySyncReportRequest,
_dispatch_gateway_sync_telemetry,
_build_gateway_sync_error_payload,
_build_gateway_sync_telemetry_writer,
_run_gateway_stream_report_background,
_run_gateway_sync_report_background,
_stream_executor_requires_python_rewrite,
_build_claude_chat_sync_decision,
_build_claude_chat_stream_decision,
_build_claude_cli_sync_decision,
_build_claude_cli_stream_decision,
_build_gemini_files_download_stream_decision,
_build_gemini_files_proxy_sync_decision,
_build_gemini_chat_sync_decision,
_build_gemini_chat_stream_decision,
_build_gemini_cli_sync_decision,
_build_gemini_cli_stream_decision,
_build_openai_chat_sync_decision,
_build_openai_chat_stream_decision,
_build_openai_cli_stream_decision,
_build_openai_video_content_stream_decision,
_extract_gateway_sync_error_message,
_record_gateway_direct_candidate_graph,
_resolve_gateway_sync_error_status_code,
_build_openai_chat_sync_plan,
_build_openai_cli_sync_decision,
_build_openai_cli_stream_plan,
_build_openai_cli_sync_plan,
_is_streaming_sync_payload,
_resolve_auth_context,
_resolve_gateway_sync_adapter,
classify_gateway_route,
router,
)
from src.database import get_db
from src.models.database import Base, RequestCandidate
from src.services.orchestration.candidate_resolver import CandidateResolver
from src.services.scheduling.schemas import PoolCandidate, ProviderCandidate
from src.services.request.execution_runtime_plan import ExecutionPlan, ExecutionPlanBody, PreparedExecutionPlan
def _wait_until(predicate: Any, *, timeout: float = 1.0, interval: float = 0.01) -> None:
deadline = time.time() + timeout
while time.time() < deadline:
if predicate():
return
time.sleep(interval)
assert predicate()
async def test_build_openai_chat_sync_plan_uses_finalize_for_cross_format_candidate(
monkeypatch: pytest.MonkeyPatch,
) -> None:
from src.api.handlers.openai import OpenAIChatAdapter
adapter = OpenAIChatAdapter()
fake_handler = SimpleNamespace(
allowed_api_formats=["openai:chat"],
_convert_request=AsyncMock(return_value={"model": "gpt-5", "messages": []}),
extract_model_from_request=lambda body, path_params: "gpt-5",
_resolve_capability_requirements=lambda **kwargs: None,
_resolve_preferred_key_ids=AsyncMock(return_value=None),
)
fake_context = SimpleNamespace(
path_params={},
request_id="req-chat-finalize-123",
client_ip="127.0.0.1",
user_agent="pytest",
start_time=0.0,
original_headers={"content-type": "application/json", "authorization": "Bearer test-key"},
query_params={},
client_content_encoding=None,
)
fake_context.ensure_json_body_async = AsyncMock(
return_value={"model": "gpt-5", "messages": []}
)
fake_candidate = SimpleNamespace(
provider=SimpleNamespace(id="provider-chat-finalize-123", name="gemini"),
endpoint=SimpleNamespace(id="endpoint-chat-finalize-123", api_format="gemini:chat"),
key=SimpleNamespace(id="key-chat-finalize-123"),
mapping_matched_model="gemini-2.5-pro",
request_candidate_id="cand-chat-finalize-123",
)
prepared_plan = PreparedExecutionPlan(
contract=ExecutionPlan(
request_id="req-chat-finalize-123",
candidate_id="cand-chat-finalize-123",
provider_name="gemini",
provider_id="provider-chat-finalize-123",
endpoint_id="endpoint-chat-finalize-123",
key_id="key-chat-finalize-123",
method="POST",
url="https://api.gemini.example/v1beta/models/gemini-2.5-pro:generateContent",
headers={"content-type": "application/json"},
body=ExecutionPlanBody(json_body={"contents": []}),
stream=False,
provider_api_format="gemini:chat",
client_api_format="openai:chat",
model_name="gpt-5",
),
payload={"contents": []},
headers={"content-type": "application/json"},
upstream_is_stream=False,
needs_conversion=True,
provider_type="gemini",
request_timeout=30.0,
envelope=object(),
proxy_info={"mode": "direct"},
)
class FakeChatSyncExecutor:
def __init__(self, handler: object) -> None:
self._ctx = SimpleNamespace(mapped_model_result="gemini-2.5-pro")
async def _build_sync_execution_plan(self, *args: object, **kwargs: object) -> PreparedExecutionPlan:
return prepared_plan
monkeypatch.setattr(adapter, "authorize", lambda context: None)
monkeypatch.setattr(
adapter,
"_create_handler",
lambda **kwargs: fake_handler,
raising=False,
)
monkeypatch.setattr(
adapter,
"_validate_request_body",
lambda body, path_params: body,
raising=False,
)
monkeypatch.setattr(
adapter,
"_merge_path_params",
lambda body, path_params: body,
raising=False,
)
monkeypatch.setattr(
"src.api.internal.gateway._resolve_gateway_sync_adapter",
lambda decision, path: (adapter, {}),
)
monkeypatch.setattr(
"src.api.internal.gateway._load_gateway_auth_models",
lambda db, auth_context: (
SimpleNamespace(id="user-chat-finalize-123"),
SimpleNamespace(id="api-key-chat-finalize-123"),
),
)
monkeypatch.setattr(
"src.api.internal.gateway.get_pipeline",
lambda: SimpleNamespace(_check_user_rate_limit=AsyncMock(return_value=None)),
)
monkeypatch.setattr(
"src.api.internal.gateway._build_gateway_request_context",
lambda **kwargs: fake_context,
)
monkeypatch.setattr(
"src.api.internal.gateway._select_gateway_direct_candidate",
AsyncMock(return_value=fake_candidate),
)
monkeypatch.setattr(
"src.api.handlers.base.chat_sync_executor.ChatSyncExecutor",
FakeChatSyncExecutor,
)
decision = classify_gateway_route("POST", "/v1/chat/completions")
auth_context = GatewayAuthContext(
user_id="user-chat-finalize-123",
api_key_id="api-key-chat-finalize-123",
access_allowed=True,
)
payload = GatewayExecuteRequest(
method="POST",
path="/v1/chat/completions",
headers={"content-type": "application/json", "authorization": "Bearer test-key"},
body_json={"model": "gpt-5", "messages": []},
auth_context=auth_context,
)
result = await _build_openai_chat_sync_plan(
request=SimpleNamespace(),
payload=payload,
db=object(),
auth_context=auth_context,
decision=decision,
)
assert result is not None
assert result.report_kind == "openai_chat_sync_finalize"
assert result.report_context["provider_api_format"] == "gemini:chat"
assert result.report_context["client_api_format"] == "openai:chat"
assert result.report_context["mapped_model"] == "gemini-2.5-pro"
async def test_build_openai_chat_sync_decision_allows_upstream_stream_finalize(
monkeypatch: pytest.MonkeyPatch,
) -> None:
from src.api.handlers.openai import OpenAIChatAdapter
adapter = OpenAIChatAdapter()
fake_context = SimpleNamespace(
path_params={},
request_id="req-chat-upstream-stream-decision-123",
client_ip="127.0.0.1",
user_agent="pytest",
start_time=0.0,
original_headers={"content-type": "application/json", "authorization": "Bearer test-key"},
query_params={},
client_content_encoding=None,
extra={},
)
fake_context.ensure_json_body_async = AsyncMock(
return_value={"model": "gpt-5", "messages": []}
)
fake_candidate = SimpleNamespace(
provider=SimpleNamespace(
id="provider-chat-upstream-stream-decision-123",
name="openai",
provider_type="openai",
proxy=None,
request_timeout=30.0,
),
endpoint=SimpleNamespace(id="endpoint-chat-upstream-stream-decision-123", api_format="openai:chat"),
key=SimpleNamespace(id="key-chat-upstream-stream-decision-123", proxy=None),
mapping_matched_model="gpt-5",
request_candidate_id="cand-chat-upstream-stream-decision-123",
)
monkeypatch.setattr(adapter, "authorize", lambda context: None)
monkeypatch.setattr(
adapter,
"_create_handler",
lambda **kwargs: SimpleNamespace(
allowed_api_formats=["openai:chat"],
extract_model_from_request=lambda body, path_params: "gpt-5",
apply_mapped_model=lambda body, mapped_model: {**body, "model": mapped_model},
_resolve_capability_requirements=lambda **kwargs: None,
_resolve_preferred_key_ids=AsyncMock(return_value=None),
_get_mapped_model=AsyncMock(return_value="gpt-5"),
),
raising=False,
)
monkeypatch.setattr(
adapter,
"_validate_request_body",
lambda body, path_params: body,
raising=False,
)
monkeypatch.setattr(
adapter,
"_merge_path_params",
lambda body, path_params: body,
raising=False,
)
monkeypatch.setattr(
"src.api.internal.gateway._resolve_gateway_sync_adapter",
lambda decision, path: (adapter, {}),
)
monkeypatch.setattr(
"src.api.internal.gateway._load_gateway_auth_models",
lambda db, auth_context: (
SimpleNamespace(id="user-chat-upstream-stream-decision-123"),
SimpleNamespace(id="api-key-chat-upstream-stream-decision-123"),
),
)
monkeypatch.setattr(
"src.api.internal.gateway.get_pipeline",
lambda: SimpleNamespace(_check_user_rate_limit=AsyncMock(return_value=None)),
)
monkeypatch.setattr(
"src.api.internal.gateway._build_gateway_request_context",
lambda **kwargs: fake_context,
)
monkeypatch.setattr(
"src.api.internal.gateway._select_gateway_direct_candidate",
AsyncMock(return_value=fake_candidate),
)
monkeypatch.setattr(
"src.services.provider.behavior.get_provider_behavior",
lambda **kwargs: SimpleNamespace(
envelope=None,
same_format_variant=None,
cross_format_variant=None,
),
)
monkeypatch.setattr(
"src.services.provider.stream_policy.resolve_upstream_is_stream",
lambda **kwargs: True,
)
monkeypatch.setattr(
"src.services.provider.prompt_cache.maybe_patch_request_with_prompt_cache_key",
lambda body, **kwargs: {**body, "prompt_cache_key": "cache-key-123"},
)
monkeypatch.setattr(
"src.services.provider.auth.get_provider_auth",
AsyncMock(
return_value=SimpleNamespace(
as_tuple=lambda: ("authorization", "Bearer upstream-key"),
decrypted_auth_config=None,
)
),
)
monkeypatch.setattr(
"src.services.provider.upstream_headers.build_upstream_extra_headers",
lambda **kwargs: {"x-extra-upstream": "1"},
)
monkeypatch.setattr(
"src.services.proxy_node.resolver.resolve_proxy_info_async",
AsyncMock(return_value=None),
)
monkeypatch.setattr(
"src.services.proxy_node.resolver.get_system_proxy_config_async",
AsyncMock(return_value=None),
)
monkeypatch.setattr(
"src.services.proxy_node.resolver.resolve_delegate_config_async",
AsyncMock(return_value=None),
)
monkeypatch.setattr(
"src.services.proxy_node.resolver.build_proxy_url_async",
AsyncMock(return_value=None),
)
decision = classify_gateway_route("POST", "/v1/chat/completions")
auth_context = GatewayAuthContext(
user_id="user-chat-upstream-stream-decision-123",
api_key_id="api-key-chat-upstream-stream-decision-123",
access_allowed=True,
)
payload = GatewayExecuteRequest(
method="POST",
path="/v1/chat/completions",
headers={"content-type": "application/json", "authorization": "Bearer test-key"},
body_json={"model": "gpt-5", "messages": []},
auth_context=auth_context,
)
result = await _build_openai_chat_sync_decision(
request=SimpleNamespace(),
payload=payload,
db=object(),
auth_context=auth_context,
decision=decision,
)
assert result is not None
assert result.upstream_is_stream is True
assert result.report_kind == "openai_chat_sync_finalize"
assert result.mapped_model == "gpt-5"
assert result.prompt_cache_key == "cache-key-123"
assert result.extra_headers == {"x-extra-upstream": "1"}
assert result.provider_request_headers is None
assert result.provider_request_body is None
@pytest.mark.asyncio
async def test_build_claude_chat_sync_decision_uses_finalize_for_cross_format_candidate(
monkeypatch: pytest.MonkeyPatch,
) -> None:
from src.api.handlers.claude import ClaudeChatAdapter
adapter = ClaudeChatAdapter()
fake_handler = SimpleNamespace(
allowed_api_formats=["claude:chat"],
_convert_request=AsyncMock(return_value={"model": "claude-sonnet-4-5", "messages": []}),
extract_model_from_request=lambda body, path_params: "claude-sonnet-4-5",
_resolve_capability_requirements=lambda **kwargs: None,
_resolve_preferred_key_ids=AsyncMock(return_value=None),
)
fake_context = SimpleNamespace(
path_params={},
request_id="req-claude-chat-decision-finalize-123",
client_ip="127.0.0.1",
user_agent="pytest",
start_time=0.0,
original_headers={"content-type": "application/json", "x-api-key": "client-key"},
query_params={},
client_content_encoding=None,
)
fake_context.ensure_json_body_async = AsyncMock(
return_value={"model": "claude-sonnet-4-5", "messages": []}
)
fake_candidate = SimpleNamespace(
provider=SimpleNamespace(id="provider-claude-chat-decision-finalize-123", name="gemini"),
endpoint=SimpleNamespace(
id="endpoint-claude-chat-decision-finalize-123",
base_url="https://api.gemini.example",
),
key=SimpleNamespace(id="key-claude-chat-decision-finalize-123", api_key="enc-key"),
request_candidate_id="cand-claude-chat-decision-finalize-123",
)
prepared_plan = PreparedExecutionPlan(
contract=ExecutionPlan(
request_id="req-claude-chat-decision-finalize-123",
candidate_id="cand-claude-chat-decision-finalize-123",
provider_name="gemini",
provider_id="provider-claude-chat-decision-finalize-123",
endpoint_id="endpoint-claude-chat-decision-finalize-123",
key_id="key-claude-chat-decision-finalize-123",
method="POST",
url="https://api.gemini.example/v1beta/models/gemini-2.5-pro:generateContent",
headers={
"content-type": "application/json",
"x-goog-api-key": "upstream-key",
"x-provider-extra": "1",
},
body=ExecutionPlanBody(
json_body={"contents": [{"role": "user", "parts": [{"text": "hello"}]}]}
),
stream=False,
provider_api_format="gemini:chat",
client_api_format="claude:chat",
model_name="claude-sonnet-4-5",
tls_profile="custom-profile",
),
payload={"contents": [{"role": "user", "parts": [{"text": "hello"}]}]},
headers={
"content-type": "application/json",
"x-goog-api-key": "upstream-key",
"x-provider-extra": "1",
},
upstream_is_stream=False,
needs_conversion=True,
provider_type="gemini",
request_timeout=30.0,
envelope=object(),
proxy_info={"mode": "direct"},
)
class FakeChatSyncExecutor:
def __init__(self, handler: object) -> None:
self._ctx = SimpleNamespace(mapped_model_result="gemini-2.5-pro")
async def _build_sync_execution_plan(
self, *args: object, **kwargs: object
) -> PreparedExecutionPlan:
return prepared_plan
monkeypatch.setattr(adapter, "authorize", lambda context: None)
monkeypatch.setattr(adapter, "_create_handler", lambda **kwargs: fake_handler, raising=False)
monkeypatch.setattr(
adapter, "_validate_request_body", lambda body, path_params: body, raising=False
)
monkeypatch.setattr(
adapter, "_merge_path_params", lambda body, path_params: body, raising=False
)
monkeypatch.setattr(
"src.api.internal.gateway._resolve_gateway_sync_adapter",
lambda decision, path: (adapter, {}),
)
monkeypatch.setattr(
"src.api.internal.gateway._load_gateway_auth_models",
lambda db, auth_context: (
SimpleNamespace(id="user-claude-chat-decision-finalize-123"),
SimpleNamespace(id="api-key-claude-chat-decision-finalize-123"),
),
)
monkeypatch.setattr(
"src.api.internal.gateway.get_pipeline",
lambda: SimpleNamespace(_check_user_rate_limit=AsyncMock(return_value=None)),
)
monkeypatch.setattr(
"src.api.internal.gateway._build_gateway_request_context",
lambda **kwargs: fake_context,
)
monkeypatch.setattr(
"src.api.internal.gateway._select_gateway_direct_candidate",
AsyncMock(return_value=fake_candidate),
)
monkeypatch.setattr(
"src.api.handlers.base.chat_sync_executor.ChatSyncExecutor",
FakeChatSyncExecutor,
)
decision = classify_gateway_route("POST", "/v1/messages", {"x-api-key": "client-key"})
auth_context = GatewayAuthContext(
user_id="user-claude-chat-decision-finalize-123",
api_key_id="api-key-claude-chat-decision-finalize-123",
access_allowed=True,
)
payload = GatewayExecuteRequest(
method="POST",
path="/v1/messages",
headers={"content-type": "application/json", "x-api-key": "client-key"},
body_json={"model": "claude-sonnet-4-5", "messages": []},
auth_context=auth_context,
)
result = await _build_claude_chat_sync_decision(
request=SimpleNamespace(),
payload=payload,
db=object(),
auth_context=auth_context,
decision=decision,
)
assert result is not None
assert result.report_kind == "claude_chat_sync_finalize"
assert result.provider_api_format == "gemini:chat"
assert result.client_api_format == "claude:chat"
assert result.mapped_model == "gemini-2.5-pro"
assert (
result.upstream_url
== "https://api.gemini.example/v1beta/models/gemini-2.5-pro:generateContent"
)
assert result.tls_profile == "custom-profile"
assert result.provider_request_headers == {
"content-type": "application/json",
"x-goog-api-key": "upstream-key",
"x-provider-extra": "1",
}
assert result.provider_request_body == {
"contents": [{"role": "user", "parts": [{"text": "hello"}]}]
}
@pytest.mark.asyncio
async def test_build_gemini_chat_sync_decision_uses_finalize_for_cross_format_candidate(
monkeypatch: pytest.MonkeyPatch,
) -> None:
from src.api.handlers.gemini import GeminiChatAdapter
adapter = GeminiChatAdapter()
fake_handler = SimpleNamespace(
allowed_api_formats=["gemini:chat"],
_convert_request=AsyncMock(return_value={"contents": []}),
extract_model_from_request=lambda body, path_params: "gemini-2.5-pro",
_resolve_capability_requirements=lambda **kwargs: None,
_resolve_preferred_key_ids=AsyncMock(return_value=None),
)
fake_context = SimpleNamespace(
path_params={},
request_id="req-gemini-chat-decision-finalize-123",
client_ip="127.0.0.1",
user_agent="pytest",
start_time=0.0,
original_headers={"content-type": "application/json", "x-goog-api-key": "client-key"},
query_params={},
client_content_encoding=None,
)
fake_context.ensure_json_body_async = AsyncMock(return_value={"contents": []})
fake_candidate = SimpleNamespace(
provider=SimpleNamespace(id="provider-gemini-chat-decision-finalize-123", name="openai"),
endpoint=SimpleNamespace(
id="endpoint-gemini-chat-decision-finalize-123",
base_url="https://api.openai.example",
),
key=SimpleNamespace(id="key-gemini-chat-decision-finalize-123", api_key="enc-key"),
request_candidate_id="cand-gemini-chat-decision-finalize-123",
)
prepared_plan = PreparedExecutionPlan(
contract=ExecutionPlan(
request_id="req-gemini-chat-decision-finalize-123",
candidate_id="cand-gemini-chat-decision-finalize-123",
provider_name="openai",
provider_id="provider-gemini-chat-decision-finalize-123",
endpoint_id="endpoint-gemini-chat-decision-finalize-123",
key_id="key-gemini-chat-decision-finalize-123",
method="POST",
url="https://api.openai.example/v1/chat/completions",
headers={
"content-type": "application/json",
"authorization": "Bearer upstream-key",
"x-provider-extra": "1",
},
body=ExecutionPlanBody(
json_body={"model": "gpt-5.1", "messages": [{"role": "user", "content": "hello"}]}
),
stream=False,
provider_api_format="openai:chat",
client_api_format="gemini:chat",
model_name="gemini-2.5-pro",
tls_profile="custom-profile",
),
payload={"model": "gpt-5.1", "messages": [{"role": "user", "content": "hello"}]},
headers={
"content-type": "application/json",
"authorization": "Bearer upstream-key",
"x-provider-extra": "1",
},
upstream_is_stream=False,
needs_conversion=True,
provider_type="openai",
request_timeout=30.0,
envelope=object(),
proxy_info={"mode": "direct"},
)
class FakeChatSyncExecutor:
def __init__(self, handler: object) -> None:
self._ctx = SimpleNamespace(mapped_model_result="gpt-5.1")
async def _build_sync_execution_plan(
self, *args: object, **kwargs: object
) -> PreparedExecutionPlan:
return prepared_plan
monkeypatch.setattr(adapter, "authorize", lambda context: None)
monkeypatch.setattr(adapter, "_create_handler", lambda **kwargs: fake_handler, raising=False)
monkeypatch.setattr(
adapter, "_validate_request_body", lambda body, path_params: body, raising=False
)
monkeypatch.setattr(
adapter, "_merge_path_params", lambda body, path_params: body, raising=False
)
monkeypatch.setattr(
"src.api.internal.gateway._resolve_gateway_sync_adapter",
lambda decision, path: (adapter, {}),
)
monkeypatch.setattr(
"src.api.internal.gateway._load_gateway_auth_models",
lambda db, auth_context: (
SimpleNamespace(id="user-gemini-chat-decision-finalize-123"),
SimpleNamespace(id="api-key-gemini-chat-decision-finalize-123"),
),
)
monkeypatch.setattr(
"src.api.internal.gateway.get_pipeline",
lambda: SimpleNamespace(_check_user_rate_limit=AsyncMock(return_value=None)),
)
monkeypatch.setattr(
"src.api.internal.gateway._build_gateway_request_context",
lambda **kwargs: fake_context,
)
monkeypatch.setattr(
"src.api.internal.gateway._select_gateway_direct_candidate",
AsyncMock(return_value=fake_candidate),
)
monkeypatch.setattr(
"src.api.handlers.base.chat_sync_executor.ChatSyncExecutor",
FakeChatSyncExecutor,
)
decision = classify_gateway_route(
"POST",
"/v1beta/models/gemini-2.5-pro:generateContent",
{"x-goog-api-key": "client-key"},
)
auth_context = GatewayAuthContext(
user_id="user-gemini-chat-decision-finalize-123",
api_key_id="api-key-gemini-chat-decision-finalize-123",
access_allowed=True,
)
payload = GatewayExecuteRequest(
method="POST",
path="/v1beta/models/gemini-2.5-pro:generateContent",
headers={"content-type": "application/json", "x-goog-api-key": "client-key"},
body_json={"contents": []},
auth_context=auth_context,
)
result = await _build_gemini_chat_sync_decision(
request=SimpleNamespace(),
payload=payload,
db=object(),
auth_context=auth_context,
decision=decision,
)
assert result is not None
assert result.report_kind == "gemini_chat_sync_finalize"
assert result.provider_api_format == "openai:chat"
assert result.client_api_format == "gemini:chat"
assert result.mapped_model == "gpt-5.1"
assert result.upstream_url == "https://api.openai.example/v1/chat/completions"
assert result.tls_profile == "custom-profile"
assert result.provider_request_headers == {
"content-type": "application/json",
"authorization": "Bearer upstream-key",
"x-provider-extra": "1",
}
assert result.provider_request_body == {
"model": "gpt-5.1",
"messages": [{"role": "user", "content": "hello"}],
}
def test_stream_executor_requires_python_rewrite_allows_claude_to_openai_chat_conversion() -> None:
assert (
_stream_executor_requires_python_rewrite(
envelope=None,
needs_conversion=True,
provider_api_format="claude:chat",
client_api_format="openai:chat",
)
is False
)
assert (
_stream_executor_requires_python_rewrite(
envelope=None,
needs_conversion=True,
provider_api_format="gemini:chat",
client_api_format="openai:chat",
)
is False
)
assert (
_stream_executor_requires_python_rewrite(
envelope=None,
needs_conversion=True,
provider_api_format="claude:cli",
client_api_format="openai:cli",
)
is False
)
assert (
_stream_executor_requires_python_rewrite(
envelope=None,
needs_conversion=True,
provider_api_format="gemini:cli",
client_api_format="openai:cli",
)
is False
)
@pytest.mark.asyncio
async def test_build_openai_chat_stream_decision_preserves_exact_provider_request_for_same_format(
monkeypatch: pytest.MonkeyPatch,
) -> None:
from src.api.handlers.openai import OpenAIChatAdapter
adapter = OpenAIChatAdapter()
fake_context = SimpleNamespace(
path_params={},
request_id="req-openai-chat-stream-exact-123",
client_ip="127.0.0.1",
user_agent="pytest",
start_time=0.0,
original_headers={"content-type": "application/json", "authorization": "Bearer test-key"},
query_params={},
client_content_encoding=None,
extra={},
)
fake_context.ensure_json_body_async = AsyncMock(
return_value={"model": "gpt-5", "messages": [], "stream": True}
)
fake_candidate = SimpleNamespace(
provider=SimpleNamespace(
id="provider-openai-chat-stream-exact-123",
name="openai",
provider_type="openai",
proxy=None,
),
endpoint=SimpleNamespace(
id="endpoint-openai-chat-stream-exact-123",
api_format="openai:chat",
base_url="https://api.openai.example",
custom_path=None,
),
key=SimpleNamespace(id="key-openai-chat-stream-exact-123", proxy=None, api_key="enc-key"),
mapping_matched_model="gpt-5-upstream",
request_candidate_id="cand-openai-chat-stream-exact-123",
)
fake_prep = SimpleNamespace(
request_body={"model": "gpt-5-upstream", "messages": [], "stream": True},
url_model="gpt-5-upstream",
mapped_model="gpt-5-upstream",
envelope=None,
extra_headers={"x-provider-extra": "1"},
upstream_is_stream=True,
needs_conversion=False,
provider_api_format="openai:chat",
client_api_format="openai:chat",
auth_info=SimpleNamespace(
as_tuple=lambda: ("authorization", "Bearer upstream-key"),
decrypted_auth_config=None,
),
tls_profile="custom-profile",
)
class FakeChatHandler:
allowed_api_formats = ["openai:chat"]
def __init__(self, **kwargs: object) -> None:
self._request_builder = SimpleNamespace(
build=lambda *args, **kwargs: (
{
"model": "gpt-5-upstream",
"messages": [],
"stream": True,
"metadata": {"decision": "exact"},
},
{
"content-type": "application/json",
"authorization": "Bearer upstream-key",
"x-provider-extra": "1",
},
)
)
async def _convert_request(self, request_obj: object) -> object:
return SimpleNamespace(model="gpt-5")
def extract_model_from_request(
self, body: dict[str, object], path_params: dict[str, str]
) -> str:
return "gpt-5"
def _resolve_capability_requirements(self, **kwargs: object) -> None:
return None
async def _resolve_preferred_key_ids(self, **kwargs: object) -> None:
return None
async def _prepare_provider_request(self, **kwargs: object) -> SimpleNamespace:
return fake_prep
monkeypatch.setattr(adapter, "authorize", lambda context: None)
monkeypatch.setattr(adapter, "_create_handler", lambda **kwargs: FakeChatHandler(), raising=False)
monkeypatch.setattr(
adapter, "_validate_request_body", lambda body, path_params: body, raising=False
)
monkeypatch.setattr(
adapter, "_merge_path_params", lambda body, path_params: body, raising=False
)
monkeypatch.setattr(
"src.api.internal.gateway._resolve_gateway_sync_adapter",
lambda decision, path: (adapter, {}),
)
monkeypatch.setattr(
"src.api.internal.gateway._load_gateway_auth_models",
lambda db, auth_context: (
SimpleNamespace(id="user-openai-chat-stream-exact-123"),
SimpleNamespace(id="api-key-openai-chat-stream-exact-123"),
),
)
monkeypatch.setattr(
"src.api.internal.gateway.get_pipeline",
lambda: SimpleNamespace(_check_user_rate_limit=AsyncMock(return_value=None)),
)
monkeypatch.setattr(
"src.api.internal.gateway._build_gateway_request_context",
lambda **kwargs: fake_context,
)
monkeypatch.setattr(
"src.api.internal.gateway._select_gateway_direct_candidate",
AsyncMock(return_value=fake_candidate),
)
monkeypatch.setattr(
"src.services.proxy_node.resolver.resolve_proxy_info_async",
AsyncMock(return_value=None),
)
monkeypatch.setattr(
"src.services.proxy_node.resolver.get_system_proxy_config_async",
AsyncMock(return_value=None),
)
monkeypatch.setattr(
"src.services.proxy_node.resolver.resolve_delegate_config_async",
AsyncMock(return_value=None),
)
monkeypatch.setattr(
"src.services.proxy_node.resolver.build_proxy_url_async",
AsyncMock(return_value=None),
)
decision = classify_gateway_route("POST", "/v1/chat/completions")
auth_context = GatewayAuthContext(
user_id="user-openai-chat-stream-exact-123",
api_key_id="api-key-openai-chat-stream-exact-123",
access_allowed=True,
)
payload = GatewayExecuteRequest(
method="POST",
path="/v1/chat/completions",
headers={"content-type": "application/json", "authorization": "Bearer test-key"},
body_json={"model": "gpt-5", "messages": [], "stream": True},
auth_context=auth_context,
)
result = await _build_openai_chat_stream_decision(
request=SimpleNamespace(),
payload=payload,
db=object(),
auth_context=auth_context,
decision=decision,
)
assert result is not None
assert result.report_kind == "openai_chat_stream_success"
assert result.provider_api_format == "openai:chat"
assert result.client_api_format == "openai:chat"
assert result.mapped_model == "gpt-5-upstream"
assert result.upstream_url == "https://api.openai.example/v1/chat/completions"
assert result.tls_profile == "custom-profile"
assert result.extra_headers == {}
assert result.provider_request_headers == {
"content-type": "application/json",
"authorization": "Bearer upstream-key",
"x-provider-extra": "1",
"accept": "text/event-stream",
}
assert result.provider_request_body == {
"model": "gpt-5-upstream",
"messages": [],
"stream": True,
"metadata": {"decision": "exact"},
}
@pytest.mark.asyncio
async def test_build_openai_chat_stream_decision_allows_claude_cross_format_exact_provider_request(
monkeypatch: pytest.MonkeyPatch,
) -> None:
from src.api.handlers.openai import OpenAIChatAdapter
adapter = OpenAIChatAdapter()
fake_context = SimpleNamespace(
path_params={},
request_id="req-openai-chat-stream-claude-xfmt-123",
client_ip="127.0.0.1",
user_agent="pytest",
start_time=0.0,
original_headers={"content-type": "application/json", "authorization": "Bearer test-key"},
query_params={},
client_content_encoding=None,
extra={},
)
fake_context.ensure_json_body_async = AsyncMock(
return_value={"model": "gpt-5", "messages": [], "stream": True}
)
fake_candidate = SimpleNamespace(
provider=SimpleNamespace(
id="provider-openai-chat-stream-claude-xfmt-123",
name="claude",
provider_type="anthropic",
proxy=None,
),
endpoint=SimpleNamespace(
id="endpoint-openai-chat-stream-claude-xfmt-123",
api_format="claude:chat",
base_url="https://api.claude.example",
custom_path=None,
),
key=SimpleNamespace(id="key-openai-chat-stream-claude-xfmt-123", proxy=None, api_key="enc-key"),
mapping_matched_model="claude-sonnet-4-5",
request_candidate_id="cand-openai-chat-stream-claude-xfmt-123",
)
fake_prep = SimpleNamespace(
request_body={"model": "claude-sonnet-4-5", "messages": [], "stream": True},
url_model="claude-sonnet-4-5",
mapped_model="claude-sonnet-4-5",
envelope=None,
extra_headers={"x-provider-extra": "1"},
upstream_is_stream=True,
needs_conversion=True,
provider_api_format="claude:chat",
client_api_format="openai:chat",
auth_info=SimpleNamespace(
as_tuple=lambda: ("authorization", "Bearer upstream-key"),
decrypted_auth_config=None,
),
tls_profile="custom-profile",
)
class FakeChatHandler:
allowed_api_formats = ["openai:chat"]
def __init__(self, **kwargs: object) -> None:
self._request_builder = SimpleNamespace(
build=lambda *args, **kwargs: (
{
"model": "claude-sonnet-4-5",
"messages": [],
"stream": True,
},
{
"content-type": "application/json",
"authorization": "Bearer upstream-key",
"x-provider-extra": "1",
},
)
)
async def _convert_request(self, request_obj: object) -> object:
return SimpleNamespace(model="gpt-5")
def extract_model_from_request(
self, body: dict[str, object], path_params: dict[str, str]
) -> str:
return "gpt-5"
def _resolve_capability_requirements(self, **kwargs: object) -> None:
return None
async def _resolve_preferred_key_ids(self, **kwargs: object) -> None:
return None
async def _prepare_provider_request(self, **kwargs: object) -> SimpleNamespace:
return fake_prep
monkeypatch.setattr(adapter, "authorize", lambda context: None)
monkeypatch.setattr(adapter, "_create_handler", lambda **kwargs: FakeChatHandler(), raising=False)
monkeypatch.setattr(
adapter, "_validate_request_body", lambda body, path_params: body, raising=False
)
monkeypatch.setattr(
adapter, "_merge_path_params", lambda body, path_params: body, raising=False
)
monkeypatch.setattr(
"src.api.internal.gateway._resolve_gateway_sync_adapter",
lambda decision, path: (adapter, {}),
)
monkeypatch.setattr(
"src.api.internal.gateway._load_gateway_auth_models",
lambda db, auth_context: (
SimpleNamespace(id="user-openai-chat-stream-claude-xfmt-123"),
SimpleNamespace(id="api-key-openai-chat-stream-claude-xfmt-123"),
),
)
monkeypatch.setattr(
"src.api.internal.gateway.get_pipeline",
lambda: SimpleNamespace(_check_user_rate_limit=AsyncMock(return_value=None)),
)
monkeypatch.setattr(
"src.api.internal.gateway._build_gateway_request_context",
lambda **kwargs: fake_context,
)
monkeypatch.setattr(
"src.api.internal.gateway._select_gateway_direct_candidate",
AsyncMock(return_value=fake_candidate),
)
monkeypatch.setattr(
"src.services.proxy_node.resolver.resolve_proxy_info_async",
AsyncMock(return_value=None),
)
monkeypatch.setattr(
"src.services.proxy_node.resolver.get_system_proxy_config_async",
AsyncMock(return_value=None),
)
monkeypatch.setattr(
"src.services.proxy_node.resolver.resolve_delegate_config_async",
AsyncMock(return_value=None),
)
monkeypatch.setattr(
"src.services.proxy_node.resolver.build_proxy_url_async",
AsyncMock(return_value=None),
)
decision = classify_gateway_route("POST", "/v1/chat/completions")
auth_context = GatewayAuthContext(
user_id="user-openai-chat-stream-claude-xfmt-123",
api_key_id="api-key-openai-chat-stream-claude-xfmt-123",
access_allowed=True,
)
payload = GatewayExecuteRequest(
method="POST",
path="/v1/chat/completions",
headers={"content-type": "application/json", "authorization": "Bearer test-key"},
body_json={"model": "gpt-5", "messages": [], "stream": True},
auth_context=auth_context,
)
result = await _build_openai_chat_stream_decision(
request=SimpleNamespace(),
payload=payload,
db=object(),
auth_context=auth_context,
decision=decision,
)
assert result is not None
assert result.report_kind == "openai_chat_stream_success"
assert result.provider_api_format == "claude:chat"
assert result.client_api_format == "openai:chat"
assert result.mapped_model == "claude-sonnet-4-5"
assert result.upstream_url == "https://api.claude.example/v1/messages"
assert result.tls_profile == "custom-profile"
assert result.provider_request_headers == {
"content-type": "application/json",
"authorization": "Bearer upstream-key",
"x-provider-extra": "1",
"accept": "text/event-stream",
}
assert result.provider_request_body == {
"model": "claude-sonnet-4-5",
"messages": [],
"stream": True,
}
@pytest.mark.asyncio
async def test_build_openai_chat_stream_decision_allows_gemini_cross_format_exact_provider_request(
monkeypatch: pytest.MonkeyPatch,
) -> None:
from src.api.handlers.openai import OpenAIChatAdapter
adapter = OpenAIChatAdapter()
fake_context = SimpleNamespace(
path_params={},
request_id="req-openai-chat-stream-gemini-xfmt-123",
client_ip="127.0.0.1",
user_agent="pytest",
start_time=0.0,
original_headers={"content-type": "application/json", "authorization": "Bearer test-key"},
query_params={},
client_content_encoding=None,
extra={},
)
fake_context.ensure_json_body_async = AsyncMock(
return_value={"model": "gpt-5", "messages": [], "stream": True}
)
fake_candidate = SimpleNamespace(
provider=SimpleNamespace(
id="provider-openai-chat-stream-gemini-xfmt-123",
name="gemini",
provider_type="google",
proxy=None,
),
endpoint=SimpleNamespace(
id="endpoint-openai-chat-stream-gemini-xfmt-123",
api_format="gemini:chat",
base_url="https://generativelanguage.googleapis.com",
custom_path=None,
),
key=SimpleNamespace(id="key-openai-chat-stream-gemini-xfmt-123", proxy=None, api_key="enc-key"),
mapping_matched_model="gemini-2.5-pro-upstream",
request_candidate_id="cand-openai-chat-stream-gemini-xfmt-123",
)
fake_prep = SimpleNamespace(
request_body={"contents": [], "stream": True},
url_model="gemini-2.5-pro-upstream",
mapped_model="gemini-2.5-pro-upstream",
envelope=None,
extra_headers={"x-provider-extra": "1"},
upstream_is_stream=True,
needs_conversion=True,
provider_api_format="gemini:chat",
client_api_format="openai:chat",
auth_info=SimpleNamespace(
as_tuple=lambda: ("x-goog-api-key", "upstream-key"),
decrypted_auth_config=None,
),
tls_profile="custom-profile",
)
class FakeRequestBuilder:
def build(self, *args: object, **kwargs: object) -> tuple[dict[str, object], dict[str, str]]:
return (
{
"contents": [],
"generationConfig": {"temperature": 0.2},
},
{
"content-type": "application/json",
"x-goog-api-key": "upstream-key",
"x-provider-extra": "1",
},
)
class FakeChatHandler:
allowed_api_formats = ["openai:chat"]
def __init__(self, **kwargs: object) -> None:
self._request_builder = FakeRequestBuilder()
async def _convert_request(self, request_obj: object) -> object:
return SimpleNamespace(model="gpt-5")
def extract_model_from_request(
self, body: dict[str, object], path_params: dict[str, str]
) -> str:
return "gpt-5"
def _resolve_capability_requirements(self, **kwargs: object) -> None:
return None
async def _resolve_preferred_key_ids(self, **kwargs: object) -> None:
return None
async def _prepare_provider_request(self, **kwargs: object) -> SimpleNamespace:
return fake_prep
monkeypatch.setattr(adapter, "authorize", lambda context: None)
monkeypatch.setattr(adapter, "_create_handler", lambda **kwargs: FakeChatHandler(), raising=False)
monkeypatch.setattr(
adapter, "_validate_request_body", lambda body, path_params: body, raising=False
)
monkeypatch.setattr(
adapter, "_merge_path_params", lambda body, path_params: body, raising=False
)
monkeypatch.setattr(
"src.api.internal.gateway._resolve_gateway_sync_adapter",
lambda decision, path: (adapter, {}),
)
monkeypatch.setattr(
"src.api.internal.gateway._load_gateway_auth_models",
lambda db, auth_context: (
SimpleNamespace(id="user-openai-chat-stream-gemini-xfmt-123"),
SimpleNamespace(id="api-key-openai-chat-stream-gemini-xfmt-123"),
),
)
monkeypatch.setattr(
"src.api.internal.gateway.get_pipeline",
lambda: SimpleNamespace(_check_user_rate_limit=AsyncMock(return_value=None)),
)
monkeypatch.setattr(
"src.api.internal.gateway._build_gateway_request_context",
lambda **kwargs: fake_context,
)
monkeypatch.setattr(
"src.api.internal.gateway._select_gateway_direct_candidate",
AsyncMock(return_value=fake_candidate),
)
monkeypatch.setattr(
"src.services.proxy_node.resolver.resolve_proxy_info_async",
AsyncMock(return_value=None),
)
monkeypatch.setattr(
"src.services.proxy_node.resolver.get_system_proxy_config_async",
AsyncMock(return_value=None),
)
monkeypatch.setattr(
"src.services.proxy_node.resolver.resolve_delegate_config_async",
AsyncMock(return_value=None),
)
monkeypatch.setattr(
"src.services.proxy_node.resolver.build_proxy_url_async",
AsyncMock(return_value=None),
)
decision = classify_gateway_route("POST", "/v1/chat/completions")
auth_context = GatewayAuthContext(
user_id="user-openai-chat-stream-gemini-xfmt-123",
api_key_id="api-key-openai-chat-stream-gemini-xfmt-123",
access_allowed=True,
)
payload = GatewayExecuteRequest(
method="POST",
path="/v1/chat/completions",
headers={"content-type": "application/json", "authorization": "Bearer test-key"},
body_json={"model": "gpt-5", "messages": [], "stream": True},
auth_context=auth_context,
)
result = await _build_openai_chat_stream_decision(
request=SimpleNamespace(),
payload=payload,
db=object(),
auth_context=auth_context,
decision=decision,
)
assert result is not None
assert result.report_kind == "openai_chat_stream_success"
assert result.provider_api_format == "gemini:chat"
assert result.client_api_format == "openai:chat"
assert result.mapped_model == "gemini-2.5-pro-upstream"
assert result.upstream_url == (
"https://generativelanguage.googleapis.com/"
"v1beta/models/gemini-2.5-pro-upstream:streamGenerateContent?alt=sse"
)
assert result.tls_profile == "custom-profile"
assert result.provider_request_headers == {
"content-type": "application/json",
"x-goog-api-key": "upstream-key",
"x-provider-extra": "1",
"accept": "text/event-stream",
}
assert result.provider_request_body == {
"contents": [],
"generationConfig": {"temperature": 0.2},
}
@pytest.mark.asyncio
async def test_build_claude_chat_stream_decision_preserves_exact_provider_request_for_same_format(
monkeypatch: pytest.MonkeyPatch,
) -> None:
from src.api.handlers.claude import ClaudeChatAdapter
adapter = ClaudeChatAdapter()
fake_context = SimpleNamespace(
path_params={},
request_id="req-claude-chat-stream-exact-123",
client_ip="127.0.0.1",
user_agent="pytest",
start_time=0.0,
original_headers={"content-type": "application/json", "x-api-key": "client-key"},
query_params={},
client_content_encoding=None,
extra={},
)
fake_context.ensure_json_body_async = AsyncMock(
return_value={"model": "claude-sonnet-4", "messages": [], "stream": True}
)
fake_candidate = SimpleNamespace(
provider=SimpleNamespace(
id="provider-claude-chat-stream-exact-123",
name="claude",
provider_type="claude",
proxy=None,
),
endpoint=SimpleNamespace(
id="endpoint-claude-chat-stream-exact-123",
api_format="claude:chat",
base_url="https://api.anthropic.example",
custom_path=None,
),
key=SimpleNamespace(id="key-claude-chat-stream-exact-123", proxy=None, api_key="enc-key"),
mapping_matched_model="claude-sonnet-4-upstream",
request_candidate_id="cand-claude-chat-stream-exact-123",
)
fake_prep = SimpleNamespace(
request_body={"model": "claude-sonnet-4-upstream", "messages": [], "stream": True},
url_model="claude-sonnet-4-upstream",
mapped_model="claude-sonnet-4-upstream",
envelope=None,
extra_headers={"x-provider-extra": "1"},
upstream_is_stream=True,
needs_conversion=False,
provider_api_format="claude:chat",
client_api_format="claude:chat",
auth_info=SimpleNamespace(
as_tuple=lambda: ("x-api-key", "upstream-secret"),
decrypted_auth_config=None,
),
tls_profile="custom-profile",
)
class FakeChatHandler:
allowed_api_formats = ["claude:chat"]
def __init__(self, **kwargs: object) -> None:
self._request_builder = SimpleNamespace(
build=lambda *args, **kwargs: (
{
"model": "claude-sonnet-4-upstream",
"messages": [],
"stream": True,
"metadata": {"decision": "exact"},
},
{
"content-type": "application/json",
"x-api-key": "upstream-secret",
"x-provider-extra": "1",
},
)
)
async def _convert_request(self, request_obj: object) -> object:
return SimpleNamespace(model="claude-sonnet-4")
def extract_model_from_request(
self, body: dict[str, object], path_params: dict[str, str]
) -> str:
return "claude-sonnet-4"
def _resolve_capability_requirements(self, **kwargs: object) -> None:
return None
async def _resolve_preferred_key_ids(self, **kwargs: object) -> None:
return None
async def _prepare_provider_request(self, **kwargs: object) -> SimpleNamespace:
return fake_prep
monkeypatch.setattr(adapter, "authorize", lambda context: None)
monkeypatch.setattr(adapter, "_create_handler", lambda **kwargs: FakeChatHandler(), raising=False)
monkeypatch.setattr(
adapter, "_validate_request_body", lambda body, path_params: body, raising=False
)
monkeypatch.setattr(
adapter, "_merge_path_params", lambda body, path_params: body, raising=False
)
monkeypatch.setattr(
"src.api.internal.gateway._resolve_gateway_sync_adapter",
lambda decision, path: (adapter, {}),
)
monkeypatch.setattr(
"src.api.internal.gateway._load_gateway_auth_models",
lambda db, auth_context: (
SimpleNamespace(id="user-claude-chat-stream-exact-123"),
SimpleNamespace(id="api-key-claude-chat-stream-exact-123"),
),
)
monkeypatch.setattr(
"src.api.internal.gateway.get_pipeline",
lambda: SimpleNamespace(_check_user_rate_limit=AsyncMock(return_value=None)),
)
monkeypatch.setattr(
"src.api.internal.gateway._build_gateway_request_context",
lambda **kwargs: fake_context,
)
monkeypatch.setattr(
"src.api.internal.gateway._select_gateway_direct_candidate",
AsyncMock(return_value=fake_candidate),
)
monkeypatch.setattr(
"src.services.proxy_node.resolver.resolve_proxy_info_async",
AsyncMock(return_value=None),
)
monkeypatch.setattr(
"src.services.proxy_node.resolver.get_system_proxy_config_async",
AsyncMock(return_value=None),
)
monkeypatch.setattr(
"src.services.proxy_node.resolver.resolve_delegate_config_async",
AsyncMock(return_value=None),
)
monkeypatch.setattr(
"src.services.proxy_node.resolver.build_proxy_url_async",
AsyncMock(return_value=None),
)
decision = classify_gateway_route(
"POST",
"/v1/messages",
{"content-type": "application/json", "x-api-key": "client-key"},
)
auth_context = GatewayAuthContext(
user_id="user-claude-chat-stream-exact-123",
api_key_id="api-key-claude-chat-stream-exact-123",
access_allowed=True,
)
payload = GatewayExecuteRequest(
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=auth_context,
)
result = await _build_claude_chat_stream_decision(
request=SimpleNamespace(),
payload=payload,
db=object(),
auth_context=auth_context,
decision=decision,
)
assert result is not None
assert result.report_kind == "claude_chat_stream_success"
assert result.provider_api_format == "claude:chat"
assert result.client_api_format == "claude:chat"
assert result.mapped_model == "claude-sonnet-4-upstream"
assert result.upstream_url == "https://api.anthropic.example/v1/messages"
assert result.tls_profile == "custom-profile"
assert result.extra_headers == {}
assert result.provider_request_headers == {
"content-type": "application/json",
"x-api-key": "upstream-secret",
"x-provider-extra": "1",
"accept": "text/event-stream",
}
assert result.provider_request_body == {
"model": "claude-sonnet-4-upstream",
"messages": [],
"stream": True,
"metadata": {"decision": "exact"},
}
@pytest.mark.asyncio
async def test_build_gemini_chat_stream_decision_preserves_exact_provider_request_for_same_format(
monkeypatch: pytest.MonkeyPatch,
) -> None:
from src.api.handlers.gemini import GeminiChatAdapter
adapter = GeminiChatAdapter()
fake_context = SimpleNamespace(
path_params={"model": "gemini-2.5-pro", "stream": True},
request_id="req-gemini-chat-stream-exact-123",
client_ip="127.0.0.1",
user_agent="pytest",
start_time=0.0,
original_headers={"content-type": "application/json", "x-goog-api-key": "client-key"},
query_params={},
client_content_encoding=None,
extra={},
)
fake_context.ensure_json_body_async = AsyncMock(
return_value={"contents": [{"role": "user", "parts": [{"text": "hello"}]}]}
)
fake_candidate = SimpleNamespace(
provider=SimpleNamespace(
id="provider-gemini-chat-stream-exact-123",
name="gemini",
provider_type="gemini",
proxy=None,
),
endpoint=SimpleNamespace(
id="endpoint-gemini-chat-stream-exact-123",
api_format="gemini:chat",
base_url="https://generativelanguage.googleapis.com",
custom_path=None,
),
key=SimpleNamespace(id="key-gemini-chat-stream-exact-123", proxy=None, api_key="enc-key"),
mapping_matched_model="gemini-2.5-pro-upstream",
request_candidate_id="cand-gemini-chat-stream-exact-123",
)
fake_prep = SimpleNamespace(
request_body={"contents": [{"role": "user", "parts": [{"text": "hello"}]}]},
url_model="gemini-2.5-pro-upstream",
mapped_model="gemini-2.5-pro-upstream",
envelope=None,
extra_headers={"x-provider-extra": "1"},
upstream_is_stream=True,
needs_conversion=False,
provider_api_format="gemini:chat",
client_api_format="gemini:chat",
auth_info=SimpleNamespace(
as_tuple=lambda: ("x-goog-api-key", "upstream-key"),
decrypted_auth_config=None,
),
tls_profile="custom-profile",
)
class FakeChatHandler:
allowed_api_formats = ["gemini:chat"]
def __init__(self, **kwargs: object) -> None:
self._request_builder = SimpleNamespace(
build=lambda *args, **kwargs: (
{
"contents": [{"role": "user", "parts": [{"text": "hello"}]}],
"generationConfig": {"temperature": 0.2},
"prompt_cache_key": "cache-key-123",
},
{
"content-type": "application/json",
"x-goog-api-key": "upstream-key",
"x-provider-extra": "1",
},
)
)
async def _convert_request(self, request_obj: object) -> object:
return SimpleNamespace(model="gemini-2.5-pro")
def extract_model_from_request(
self, body: dict[str, object], path_params: dict[str, str]
) -> str:
return "gemini-2.5-pro"
def _resolve_capability_requirements(self, **kwargs: object) -> None:
return None
async def _resolve_preferred_key_ids(self, **kwargs: object) -> None:
return None
async def _prepare_provider_request(self, **kwargs: object) -> SimpleNamespace:
return fake_prep
monkeypatch.setattr(adapter, "authorize", lambda context: None)
monkeypatch.setattr(adapter, "_create_handler", lambda **kwargs: FakeChatHandler(), raising=False)
monkeypatch.setattr(
adapter,
"_validate_request_body",
lambda body, path_params: body,
raising=False,
)
monkeypatch.setattr(
adapter,
"_merge_path_params",
lambda body, path_params: body,
raising=False,
)
monkeypatch.setattr(
"src.api.internal.gateway._resolve_gateway_sync_adapter",
lambda decision, path: (adapter, {"model": "gemini-2.5-pro", "stream": True}),
)
monkeypatch.setattr(
"src.api.internal.gateway._load_gateway_auth_models",
lambda db, auth_context: (
SimpleNamespace(id="user-gemini-chat-stream-exact-123"),
SimpleNamespace(id="api-key-gemini-chat-stream-exact-123"),
),
)
monkeypatch.setattr(
"src.api.internal.gateway.get_pipeline",
lambda: SimpleNamespace(_check_user_rate_limit=AsyncMock(return_value=None)),
)
monkeypatch.setattr(
"src.api.internal.gateway._build_gateway_request_context",
lambda **kwargs: fake_context,
)
monkeypatch.setattr(
"src.api.internal.gateway._select_gateway_direct_candidate",
AsyncMock(return_value=fake_candidate),
)
monkeypatch.setattr(
"src.services.proxy_node.resolver.resolve_proxy_info_async",
AsyncMock(return_value=None),
)
monkeypatch.setattr(
"src.services.proxy_node.resolver.get_system_proxy_config_async",
AsyncMock(return_value=None),
)
monkeypatch.setattr(
"src.services.proxy_node.resolver.resolve_delegate_config_async",
AsyncMock(return_value=None),
)
monkeypatch.setattr(
"src.services.proxy_node.resolver.build_proxy_url_async",
AsyncMock(return_value=None),
)
decision = classify_gateway_route(
"POST",
"/v1beta/models/gemini-2.5-pro:streamGenerateContent",
{"content-type": "application/json", "x-goog-api-key": "client-key"},
)
auth_context = GatewayAuthContext(
user_id="user-gemini-chat-stream-exact-123",
api_key_id="api-key-gemini-chat-stream-exact-123",
access_allowed=True,
)
payload = GatewayExecuteRequest(
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": "hello"}]}]},
auth_context=auth_context,
)
result = await _build_gemini_chat_stream_decision(
request=SimpleNamespace(),
payload=payload,
db=object(),
auth_context=auth_context,
decision=decision,
)
assert result is not None
assert result.report_kind == "gemini_chat_stream_success"
assert result.provider_api_format == "gemini:chat"
assert result.client_api_format == "gemini:chat"
assert result.mapped_model == "gemini-2.5-pro-upstream"
assert result.upstream_url == (
"https://generativelanguage.googleapis.com/"
"v1beta/models/gemini-2.5-pro-upstream:streamGenerateContent?alt=sse"
)
assert result.tls_profile == "custom-profile"
assert result.extra_headers == {}
assert result.provider_request_headers == {
"content-type": "application/json",
"x-goog-api-key": "upstream-key",
"x-provider-extra": "1",
"accept": "text/event-stream",
}
assert result.provider_request_body == {
"contents": [{"role": "user", "parts": [{"text": "hello"}]}],
"generationConfig": {"temperature": 0.2},
"prompt_cache_key": "cache-key-123",
}
@pytest.mark.asyncio
async def test_build_gemini_chat_stream_decision_allows_antigravity_force_rewrite_envelope(
monkeypatch: pytest.MonkeyPatch,
) -> None:
from src.api.handlers.gemini import GeminiChatAdapter
class AntigravityEnvelope:
name = "antigravity:v1internal"
def force_stream_rewrite(self) -> bool:
return True
adapter = GeminiChatAdapter()
fake_context = SimpleNamespace(
path_params={"model": "gemini-2.5-pro"},
request_id="req-antigravity-gemini-chat-stream-exact-123",
client_ip="127.0.0.1",
user_agent="pytest",
start_time=0.0,
original_headers={
"content-type": "application/json",
"authorization": "Bearer client-key",
},
query_params={},
client_content_encoding=None,
extra={},
)
fake_context.ensure_json_body_async = AsyncMock(
return_value={
"contents": [{"role": "user", "parts": [{"text": "hello"}]}],
"stream": True,
}
)
fake_candidate = SimpleNamespace(
provider=SimpleNamespace(
id="provider-antigravity-gemini-chat-stream-exact-123",
name="antigravity",
provider_type="antigravity",
proxy=None,
),
endpoint=SimpleNamespace(
id="endpoint-antigravity-gemini-chat-stream-exact-123",
api_format="gemini:chat",
base_url="https://generativelanguage.googleapis.com",
custom_path=None,
),
key=SimpleNamespace(
id="key-antigravity-gemini-chat-stream-exact-123",
proxy=None,
api_key="enc-key",
),
mapping_matched_model="claude-sonnet-4-5",
request_candidate_id="cand-antigravity-gemini-chat-stream-exact-123",
)
fake_prep = SimpleNamespace(
request_body={"contents": [{"role": "user", "parts": [{"text": "hello"}]}]},
url_model="claude-sonnet-4-5",
mapped_model="claude-sonnet-4-5",
envelope=AntigravityEnvelope(),
extra_headers={"x-provider-extra": "1"},
upstream_is_stream=True,
needs_conversion=False,
provider_api_format="gemini:chat",
client_api_format="gemini:chat",
auth_info=SimpleNamespace(
as_tuple=lambda: ("authorization", "Bearer upstream-secret"),
decrypted_auth_config=None,
),
tls_profile="custom-profile",
)
class FakeChatHandler:
allowed_api_formats = ["gemini:chat"]
def __init__(self, **kwargs: object) -> None:
self._request_builder = SimpleNamespace(
build=lambda *args, **kwargs: (
{
"contents": [{"role": "user", "parts": [{"text": "hello"}]}],
"generationConfig": {"temperature": 0.2},
},
{
"content-type": "application/json",
"authorization": "Bearer upstream-secret",
"x-provider-extra": "1",
},
)
)
async def _convert_request(self, request_obj: object) -> object:
return SimpleNamespace(model="gemini-2.5-pro")
def extract_model_from_request(
self, body: dict[str, object], path_params: dict[str, str]
) -> str:
return "gemini-2.5-pro"
def _resolve_capability_requirements(self, **kwargs: object) -> None:
return None
async def _resolve_preferred_key_ids(self, **kwargs: object) -> None:
return None
async def _prepare_provider_request(self, **kwargs: object) -> SimpleNamespace:
return fake_prep
monkeypatch.setattr(adapter, "authorize", lambda context: None)
monkeypatch.setattr(adapter, "_create_handler", lambda **kwargs: FakeChatHandler(), raising=False)
monkeypatch.setattr(
adapter, "_validate_request_body", lambda body, path_params: body, raising=False
)
monkeypatch.setattr(
adapter, "_merge_path_params", lambda body, path_params: body, raising=False
)
monkeypatch.setattr(
"src.api.internal.gateway._resolve_gateway_sync_adapter",
lambda decision, path: (adapter, {"model": "gemini-2.5-pro"}),
)
monkeypatch.setattr(
"src.api.internal.gateway._load_gateway_auth_models",
lambda db, auth_context: (
SimpleNamespace(id="user-antigravity-gemini-chat-stream-exact-123"),
SimpleNamespace(id="api-key-antigravity-gemini-chat-stream-exact-123"),
),
)
monkeypatch.setattr(
"src.api.internal.gateway.get_pipeline",
lambda: SimpleNamespace(_check_user_rate_limit=AsyncMock(return_value=None)),
)
monkeypatch.setattr(
"src.api.internal.gateway._build_gateway_request_context",
lambda **kwargs: fake_context,
)
monkeypatch.setattr(
"src.api.internal.gateway._select_gateway_direct_candidate",
AsyncMock(return_value=fake_candidate),
)
monkeypatch.setattr(
"src.services.proxy_node.resolver.resolve_proxy_info_async",
AsyncMock(return_value=None),
)
monkeypatch.setattr(
"src.services.proxy_node.resolver.get_system_proxy_config_async",
AsyncMock(return_value=None),
)
monkeypatch.setattr(
"src.services.proxy_node.resolver.resolve_delegate_config_async",
AsyncMock(return_value=None),
)
monkeypatch.setattr(
"src.services.proxy_node.resolver.build_proxy_url_async",
AsyncMock(return_value=None),
)
decision = classify_gateway_route(
"POST",
"/v1beta/models/gemini-2.5-pro:streamGenerateContent",
{"content-type": "application/json", "authorization": "Bearer client-key"},
)
auth_context = GatewayAuthContext(
user_id="user-antigravity-gemini-chat-stream-exact-123",
api_key_id="api-key-antigravity-gemini-chat-stream-exact-123",
access_allowed=True,
)
payload = GatewayExecuteRequest(
method="POST",
path="/v1beta/models/gemini-2.5-pro:streamGenerateContent",
headers={"content-type": "application/json", "authorization": "Bearer client-key"},
body_json={
"contents": [{"role": "user", "parts": [{"text": "hello"}]}],
"stream": True,
},
auth_context=auth_context,
)
result = await _build_gemini_chat_stream_decision(
request=SimpleNamespace(),
payload=payload,
db=object(),
auth_context=auth_context,
decision=decision,
)
assert result is not None
assert result.report_kind == "gemini_chat_stream_success"
assert result.provider_api_format == "gemini:chat"
assert result.client_api_format == "gemini:chat"
assert result.mapped_model == "claude-sonnet-4-5"
assert result.upstream_url == (
"https://generativelanguage.googleapis.com/"
"v1beta/models/claude-sonnet-4-5:streamGenerateContent?alt=sse"
)
assert result.provider_request_headers == {
"content-type": "application/json",
"authorization": "Bearer upstream-secret",
"x-provider-extra": "1",
"accept": "text/event-stream",
}
assert result.provider_request_body == {
"contents": [{"role": "user", "parts": [{"text": "hello"}]}],
"generationConfig": {"temperature": 0.2},
}
@pytest.mark.asyncio
async def test_build_openai_chat_sync_plan_allows_upstream_stream_finalize(
monkeypatch: pytest.MonkeyPatch,
) -> None:
from src.api.handlers.openai import OpenAIChatAdapter
adapter = OpenAIChatAdapter()
fake_context = SimpleNamespace(
path_params={},
request_id="req-chat-upstream-stream-123",
client_ip="127.0.0.1",
user_agent="pytest",
start_time=0.0,
original_headers={"content-type": "application/json", "authorization": "Bearer test-key"},
query_params={},
client_content_encoding=None,
extra={},
)
fake_context.ensure_json_body_async = AsyncMock(
return_value={"model": "gpt-5", "messages": []}
)
fake_candidate = SimpleNamespace(
provider=SimpleNamespace(
id="provider-chat-upstream-stream-123",
name="openai",
provider_type="openai",
proxy=None,
request_timeout=30.0,
),
endpoint=SimpleNamespace(id="endpoint-chat-upstream-stream-123", api_format="openai:chat"),
key=SimpleNamespace(id="key-chat-upstream-stream-123", proxy=None),
mapping_matched_model="gpt-5",
needs_conversion=False,
output_limit=None,
request_candidate_id="cand-chat-upstream-stream-123",
)
prepared_plan = PreparedExecutionPlan(
contract=ExecutionPlan(
request_id="plan-chat-upstream-stream-123",
candidate_id="cand-chat-upstream-stream-123",
provider_name="openai",
provider_id="provider-chat-upstream-stream-123",
endpoint_id="endpoint-chat-upstream-stream-123",
key_id="key-chat-upstream-stream-123",
method="POST",
url="https://api.openai.example/v1/chat/completions",
headers={"content-type": "application/json"},
body=ExecutionPlanBody(json_body={"model": "gpt-5", "messages": []}),
stream=True,
provider_api_format="openai:chat",
client_api_format="openai:chat",
model_name="gpt-5",
),
payload={"model": "gpt-5", "messages": []},
headers={"content-type": "application/json"},
upstream_is_stream=True,
needs_conversion=False,
provider_type="openai",
request_timeout=30.0,
envelope=None,
proxy_info={"mode": "direct"},
)
class FakeChatSyncExecutor:
def __init__(self, handler: object) -> None:
self._ctx = SimpleNamespace(mapped_model_result="gpt-5")
async def _build_sync_execution_plan(self, *args: object, **kwargs: object) -> PreparedExecutionPlan:
return prepared_plan
monkeypatch.setattr(adapter, "authorize", lambda context: None)
monkeypatch.setattr(
adapter,
"_create_handler",
lambda **kwargs: SimpleNamespace(
allowed_api_formats=["openai:chat"],
_convert_request=AsyncMock(return_value=SimpleNamespace(model="gpt-5")),
extract_model_from_request=lambda body, path_params: "gpt-5",
_resolve_capability_requirements=lambda **kwargs: None,
_resolve_preferred_key_ids=AsyncMock(return_value=None),
),
raising=False,
)
monkeypatch.setattr(
adapter,
"_validate_request_body",
lambda body, path_params: body,
raising=False,
)
monkeypatch.setattr(
adapter,
"_merge_path_params",
lambda body, path_params: body,
raising=False,
)
monkeypatch.setattr(
"src.api.internal.gateway._resolve_gateway_sync_adapter",
lambda decision, path: (adapter, {}),
)
monkeypatch.setattr(
"src.api.internal.gateway._load_gateway_auth_models",
lambda db, auth_context: (
SimpleNamespace(id="user-chat-upstream-stream-123"),
SimpleNamespace(id="api-key-chat-upstream-stream-123"),
),
)
monkeypatch.setattr(
"src.api.internal.gateway.get_pipeline",
lambda: SimpleNamespace(_check_user_rate_limit=AsyncMock(return_value=None)),
)
monkeypatch.setattr(
"src.api.internal.gateway._build_gateway_request_context",
lambda **kwargs: fake_context,
)
monkeypatch.setattr(
"src.api.internal.gateway._select_gateway_direct_candidate",
AsyncMock(return_value=fake_candidate),
)
monkeypatch.setattr(
"src.api.handlers.base.chat_sync_executor.ChatSyncExecutor",
FakeChatSyncExecutor,
)
decision = classify_gateway_route("POST", "/v1/chat/completions")
auth_context = GatewayAuthContext(
user_id="user-chat-upstream-stream-123",
api_key_id="api-key-chat-upstream-stream-123",
access_allowed=True,
)
payload = GatewayExecuteRequest(
method="POST",
path="/v1/chat/completions",
headers={"content-type": "application/json", "authorization": "Bearer test-key"},
body_json={"model": "gpt-5", "messages": []},
auth_context=auth_context,
)
result = await _build_openai_chat_sync_plan(
request=SimpleNamespace(),
payload=payload,
db=object(),
auth_context=auth_context,
decision=decision,
)
assert result is not None
assert result.report_kind == "openai_chat_sync_finalize"
assert result.plan["stream"] is True