mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-03 01:40:21 +08:00
201 lines
6.5 KiB
Python
201 lines
6.5 KiB
Python
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
import copy
|
||
|
|
from types import SimpleNamespace
|
||
|
|
from typing import Any
|
||
|
|
|
||
|
|
import pytest
|
||
|
|
|
||
|
|
import src.api.handlers.base.chat_handler_base as chatmod
|
||
|
|
from src.api.handlers.base.chat_handler_base import ChatHandlerBase
|
||
|
|
from src.api.handlers.base.chat_sync_executor import ChatSyncExecutor
|
||
|
|
from src.api.handlers.base.stream_context import StreamContext
|
||
|
|
from src.services.task.request_state import MutableRequestBodyState
|
||
|
|
|
||
|
|
|
||
|
|
class _StopBuild(Exception):
|
||
|
|
pass
|
||
|
|
|
||
|
|
|
||
|
|
class _DummyAuthInfo:
|
||
|
|
auth_header = "authorization"
|
||
|
|
auth_value = "Bearer test"
|
||
|
|
decrypted_auth_config = None
|
||
|
|
|
||
|
|
def as_tuple(self) -> tuple[str, str]:
|
||
|
|
return self.auth_header, self.auth_value
|
||
|
|
|
||
|
|
|
||
|
|
class _CaptureBuilder:
|
||
|
|
def __init__(self) -> None:
|
||
|
|
self.request_body: dict[str, Any] | None = None
|
||
|
|
|
||
|
|
def build(self, request_body: dict[str, Any], *args: Any, **kwargs: Any) -> Any:
|
||
|
|
self.request_body = request_body
|
||
|
|
raise _StopBuild()
|
||
|
|
|
||
|
|
|
||
|
|
class _DummyChatHandler(ChatHandlerBase):
|
||
|
|
FORMAT_ID = "openai:chat"
|
||
|
|
|
||
|
|
def __init__(self) -> None:
|
||
|
|
self.request_id = "req-test"
|
||
|
|
self.api_key = SimpleNamespace(id="user-key-1")
|
||
|
|
self._request_builder = _CaptureBuilder()
|
||
|
|
self.allowed_api_formats = ["openai:chat"]
|
||
|
|
self.api_family = None
|
||
|
|
self.endpoint_kind = None
|
||
|
|
|
||
|
|
async def _convert_request(self, request: Any) -> Any:
|
||
|
|
return request
|
||
|
|
|
||
|
|
def _extract_usage(self, response: dict) -> dict[str, int]:
|
||
|
|
return {}
|
||
|
|
|
||
|
|
async def _get_mapped_model(
|
||
|
|
self,
|
||
|
|
source_model: str,
|
||
|
|
provider_id: str,
|
||
|
|
api_format: str | None = None,
|
||
|
|
) -> str | None:
|
||
|
|
del source_model, provider_id, api_format
|
||
|
|
return None
|
||
|
|
|
||
|
|
def apply_mapped_model(self, request_body: dict[str, Any], mapped_model: str) -> dict[str, Any]:
|
||
|
|
out = dict(request_body)
|
||
|
|
out["model"] = mapped_model
|
||
|
|
return out
|
||
|
|
|
||
|
|
def prepare_provider_request_body(self, request_body: dict[str, Any]) -> dict[str, Any]:
|
||
|
|
request_body["messages"][0]["content"] = "prepared"
|
||
|
|
return request_body
|
||
|
|
|
||
|
|
def finalize_provider_request(
|
||
|
|
self,
|
||
|
|
request_body: dict[str, Any],
|
||
|
|
*,
|
||
|
|
mapped_model: str | None,
|
||
|
|
provider_api_format: str | None,
|
||
|
|
) -> dict[str, Any]:
|
||
|
|
del mapped_model, provider_api_format
|
||
|
|
request_body["messages"].append({"role": "assistant", "content": "finalized"})
|
||
|
|
return request_body
|
||
|
|
|
||
|
|
def get_model_for_url(
|
||
|
|
self,
|
||
|
|
request_body: dict[str, Any],
|
||
|
|
mapped_model: str | None,
|
||
|
|
) -> str | None:
|
||
|
|
return mapped_model or str(request_body.get("model") or "")
|
||
|
|
|
||
|
|
|
||
|
|
def _patch_chat_upstream(monkeypatch: pytest.MonkeyPatch) -> None:
|
||
|
|
async def _fake_get_provider_auth(endpoint: Any, key: Any) -> _DummyAuthInfo:
|
||
|
|
return _DummyAuthInfo()
|
||
|
|
|
||
|
|
monkeypatch.setattr(chatmod, "get_provider_auth", _fake_get_provider_auth)
|
||
|
|
monkeypatch.setattr(
|
||
|
|
chatmod,
|
||
|
|
"get_provider_behavior",
|
||
|
|
lambda **kwargs: SimpleNamespace(
|
||
|
|
envelope=None,
|
||
|
|
same_format_variant=None,
|
||
|
|
cross_format_variant=None,
|
||
|
|
),
|
||
|
|
)
|
||
|
|
monkeypatch.setattr(chatmod, "get_upstream_stream_policy", lambda *args, **kwargs: None)
|
||
|
|
monkeypatch.setattr(
|
||
|
|
chatmod,
|
||
|
|
"resolve_upstream_is_stream",
|
||
|
|
lambda *, client_is_stream, policy: client_is_stream,
|
||
|
|
)
|
||
|
|
monkeypatch.setattr(chatmod, "enforce_stream_mode_for_upstream", lambda *args, **kwargs: None)
|
||
|
|
monkeypatch.setattr(
|
||
|
|
chatmod,
|
||
|
|
"maybe_patch_request_with_prompt_cache_key",
|
||
|
|
lambda request_body, **kwargs: request_body,
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.mark.asyncio
|
||
|
|
async def test_chat_execute_stream_request_does_not_mutate_original_request_body(
|
||
|
|
monkeypatch: pytest.MonkeyPatch,
|
||
|
|
) -> None:
|
||
|
|
_patch_chat_upstream(monkeypatch)
|
||
|
|
|
||
|
|
handler = _DummyChatHandler()
|
||
|
|
ctx = StreamContext(model="gpt-test", api_format="openai:chat")
|
||
|
|
ctx.client_api_format = "openai:chat"
|
||
|
|
|
||
|
|
provider = SimpleNamespace(name="provider", id="provider-1", provider_type="", proxy=None)
|
||
|
|
endpoint = SimpleNamespace(id="endpoint-1", api_format="openai:chat", base_url="https://x")
|
||
|
|
key = SimpleNamespace(id="key-1", proxy=None)
|
||
|
|
candidate = SimpleNamespace(
|
||
|
|
mapping_matched_model=None, needs_conversion=False, output_limit=None
|
||
|
|
)
|
||
|
|
|
||
|
|
original_request_body = {
|
||
|
|
"model": "gpt-test",
|
||
|
|
"messages": [{"role": "user", "content": "hello"}],
|
||
|
|
}
|
||
|
|
snapshot = copy.deepcopy(original_request_body)
|
||
|
|
request_state = MutableRequestBodyState(original_request_body)
|
||
|
|
|
||
|
|
with pytest.raises(_StopBuild):
|
||
|
|
await handler._execute_stream_request(
|
||
|
|
ctx,
|
||
|
|
object(),
|
||
|
|
provider,
|
||
|
|
endpoint,
|
||
|
|
key,
|
||
|
|
request_state.build_attempt_body(),
|
||
|
|
{},
|
||
|
|
candidate=candidate,
|
||
|
|
)
|
||
|
|
|
||
|
|
assert original_request_body == snapshot
|
||
|
|
assert handler._request_builder.request_body is not None
|
||
|
|
assert handler._request_builder.request_body["messages"][0]["content"] == "prepared"
|
||
|
|
assert handler._request_builder.request_body["messages"][-1]["content"] == "finalized"
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.mark.asyncio
|
||
|
|
async def test_chat_sync_request_func_does_not_mutate_original_request_body(
|
||
|
|
monkeypatch: pytest.MonkeyPatch,
|
||
|
|
) -> None:
|
||
|
|
_patch_chat_upstream(monkeypatch)
|
||
|
|
|
||
|
|
handler = _DummyChatHandler()
|
||
|
|
executor = ChatSyncExecutor(handler)
|
||
|
|
|
||
|
|
provider = SimpleNamespace(name="provider", id="provider-1", provider_type="", proxy=None)
|
||
|
|
endpoint = SimpleNamespace(id="endpoint-1", api_format="openai:chat", base_url="https://x")
|
||
|
|
key = SimpleNamespace(id="key-1", proxy=None)
|
||
|
|
candidate = SimpleNamespace(
|
||
|
|
mapping_matched_model=None, needs_conversion=False, output_limit=None
|
||
|
|
)
|
||
|
|
|
||
|
|
original_request_body = {
|
||
|
|
"model": "gpt-test",
|
||
|
|
"messages": [{"role": "user", "content": "hello"}],
|
||
|
|
}
|
||
|
|
snapshot = copy.deepcopy(original_request_body)
|
||
|
|
request_state = MutableRequestBodyState(original_request_body)
|
||
|
|
|
||
|
|
with pytest.raises(_StopBuild):
|
||
|
|
await executor._sync_request_func(
|
||
|
|
provider,
|
||
|
|
endpoint,
|
||
|
|
key,
|
||
|
|
candidate,
|
||
|
|
model="gpt-test",
|
||
|
|
api_format="openai:chat",
|
||
|
|
original_headers={},
|
||
|
|
request_state=request_state,
|
||
|
|
)
|
||
|
|
|
||
|
|
assert original_request_body == snapshot
|
||
|
|
assert handler._request_builder.request_body is not None
|
||
|
|
assert handler._request_builder.request_body["messages"][0]["content"] == "prepared"
|
||
|
|
assert handler._request_builder.request_body["messages"][-1]["content"] == "finalized"
|