mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 09:20:22 +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 服务层和测试适配新架构命名
761 lines
25 KiB
Python
761 lines
25 KiB
Python
from __future__ import annotations
|
|
|
|
from collections.abc import AsyncGenerator
|
|
from types import SimpleNamespace
|
|
from typing import Any
|
|
|
|
import httpx
|
|
import pytest
|
|
|
|
import src.api.handlers.base.chat_handler_base as chatmod
|
|
import src.services.proxy_node.resolver as proxymod
|
|
from src.api.handlers.base.chat_handler_base import ChatHandlerBase
|
|
from src.api.handlers.base.stream_context import StreamContext
|
|
from src.core.exceptions import ProviderNotAvailableException
|
|
from src.services.request.execution_runtime_client import (
|
|
ExecutionRuntimeClientError,
|
|
ExecutionRuntimeStreamResult,
|
|
)
|
|
|
|
|
|
class _DummyAuthInfo:
|
|
auth_header = "authorization"
|
|
auth_value = "Bearer test"
|
|
decrypted_auth_config = None
|
|
|
|
def as_tuple(self) -> tuple[str, str]:
|
|
return self.auth_header, self.auth_value
|
|
|
|
|
|
class _PassBuilder:
|
|
def build(self, request_body: dict[str, Any], *args: Any, **kwargs: Any) -> Any:
|
|
return request_body, {"content-type": "application/json"}
|
|
|
|
|
|
class _DummyStreamResponseCtx:
|
|
def __init__(self) -> None:
|
|
self.closed = False
|
|
|
|
async def __aexit__(self, exc_type: object, exc: object, tb: object) -> None:
|
|
self.closed = True
|
|
|
|
|
|
class _FakeStreamProcessor:
|
|
def __init__(self) -> None:
|
|
self.prefetched_chunks: list[bytes] | None = None
|
|
self.response_ctx: _DummyStreamResponseCtx | None = None
|
|
|
|
async def prefetch_and_check_error(
|
|
self,
|
|
byte_iterator: Any,
|
|
provider: Any,
|
|
endpoint: Any,
|
|
ctx: Any,
|
|
max_prefetch_lines: int = 5,
|
|
max_prefetch_bytes: int = 65536,
|
|
) -> list[bytes]:
|
|
del provider, endpoint, ctx, max_prefetch_lines, max_prefetch_bytes
|
|
first = await anext(byte_iterator)
|
|
self.prefetched_chunks = [first]
|
|
return self.prefetched_chunks
|
|
|
|
async def create_response_stream(
|
|
self,
|
|
ctx: Any,
|
|
byte_iterator: Any,
|
|
response_ctx: _DummyStreamResponseCtx,
|
|
prefetched_chunks: list[bytes] | None = None,
|
|
*,
|
|
start_time: float | None = None,
|
|
) -> AsyncGenerator[bytes]:
|
|
del ctx, start_time
|
|
self.response_ctx = response_ctx
|
|
try:
|
|
for chunk in prefetched_chunks or []:
|
|
yield chunk
|
|
async for chunk in byte_iterator:
|
|
yield chunk
|
|
finally:
|
|
await response_ctx.__aexit__(None, None, None)
|
|
|
|
|
|
class _FakeParser:
|
|
def is_error_response(self, response_json: dict[str, Any]) -> bool:
|
|
del response_json
|
|
return False
|
|
|
|
|
|
class _FakeInternalUsage:
|
|
input_tokens = 3
|
|
output_tokens = 5
|
|
cache_read_tokens = 1
|
|
cache_write_tokens = 0
|
|
|
|
|
|
class _FakeInternalResponse:
|
|
def __init__(self) -> None:
|
|
self.id = "resp-sync"
|
|
self.model = ""
|
|
self.usage = _FakeInternalUsage()
|
|
|
|
|
|
class _FakeSourceNormalizer:
|
|
def response_to_internal(self, response_json: dict[str, Any]) -> _FakeInternalResponse:
|
|
assert response_json == {"id": "sync-1", "message": "hello"}
|
|
return _FakeInternalResponse()
|
|
|
|
|
|
class _FakeTargetNormalizer:
|
|
def stream_event_from_internal(
|
|
self,
|
|
event: dict[str, Any],
|
|
state: Any,
|
|
) -> list[dict[str, Any]]:
|
|
assert event == {"kind": "chunk"}
|
|
assert getattr(state, "message_id", "") == "resp-sync"
|
|
return [{"delta": "hello"}]
|
|
|
|
|
|
class _FakeRegistry:
|
|
def get_normalizer(self, format_id: str) -> Any:
|
|
if format_id == "provider:test":
|
|
return _FakeSourceNormalizer()
|
|
if format_id == "openai:chat":
|
|
return _FakeTargetNormalizer()
|
|
raise AssertionError(f"unexpected format: {format_id}")
|
|
|
|
|
|
class _DummyChatHandler(ChatHandlerBase):
|
|
FORMAT_ID = "openai:chat"
|
|
|
|
def __init__(self) -> None:
|
|
self.request_id = "req-test"
|
|
self.api_key = SimpleNamespace(id="user-key-1")
|
|
self._request_builder = _PassBuilder()
|
|
self.allowed_api_formats = ["openai:chat"]
|
|
self.api_family = None
|
|
self.endpoint_kind = None
|
|
self.start_time = 0.0
|
|
|
|
async def _convert_request(self, request: Any) -> Any:
|
|
return request
|
|
|
|
def _extract_usage(self, response: dict) -> dict[str, int]:
|
|
return {}
|
|
|
|
async def _get_mapped_model(
|
|
self,
|
|
source_model: str,
|
|
provider_id: str,
|
|
api_format: str | None = None,
|
|
) -> str | None:
|
|
del source_model, provider_id, api_format
|
|
return None
|
|
|
|
def apply_mapped_model(self, request_body: dict[str, Any], mapped_model: str) -> dict[str, Any]:
|
|
out = dict(request_body)
|
|
out["model"] = mapped_model
|
|
return out
|
|
|
|
def prepare_provider_request_body(self, request_body: dict[str, Any]) -> dict[str, Any]:
|
|
return dict(request_body)
|
|
|
|
def finalize_provider_request(
|
|
self,
|
|
request_body: dict[str, Any],
|
|
*,
|
|
mapped_model: str | None,
|
|
provider_api_format: str | None,
|
|
) -> dict[str, Any]:
|
|
del mapped_model, provider_api_format
|
|
return dict(request_body)
|
|
|
|
def get_model_for_url(
|
|
self,
|
|
request_body: dict[str, Any],
|
|
mapped_model: str | None,
|
|
) -> str | None:
|
|
return mapped_model or str(request_body.get("model") or "")
|
|
|
|
|
|
def _patch_stream_setup(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
*,
|
|
proxy_info: dict[str, Any] | None = None,
|
|
delegate_config: dict[str, Any] | None = None,
|
|
) -> None:
|
|
async def _fake_get_provider_auth(endpoint: Any, key: Any) -> _DummyAuthInfo:
|
|
del endpoint, key
|
|
return _DummyAuthInfo()
|
|
|
|
async def _fake_resolve_proxy_info(proxy_config: Any) -> Any:
|
|
del proxy_config
|
|
return proxy_info
|
|
|
|
async def _fake_resolve_delegate(proxy_config: Any) -> Any:
|
|
del proxy_config
|
|
return delegate_config
|
|
|
|
async def _fake_get_system_proxy() -> None:
|
|
return None
|
|
|
|
monkeypatch.setattr(chatmod, "get_provider_auth", _fake_get_provider_auth)
|
|
monkeypatch.setattr(
|
|
chatmod,
|
|
"get_provider_behavior",
|
|
lambda **kwargs: SimpleNamespace(
|
|
envelope=None,
|
|
same_format_variant=None,
|
|
cross_format_variant=None,
|
|
),
|
|
)
|
|
monkeypatch.setattr(chatmod, "build_provider_url", lambda *args, **kwargs: "https://upstream.test/v1/chat/completions")
|
|
monkeypatch.setattr(chatmod, "get_upstream_stream_policy", lambda *args, **kwargs: None)
|
|
monkeypatch.setattr(
|
|
chatmod,
|
|
"resolve_upstream_is_stream",
|
|
lambda *, client_is_stream, policy: client_is_stream,
|
|
)
|
|
monkeypatch.setattr(chatmod, "enforce_stream_mode_for_upstream", lambda *args, **kwargs: None)
|
|
monkeypatch.setattr(
|
|
chatmod,
|
|
"maybe_patch_request_with_prompt_cache_key",
|
|
lambda request_body, **kwargs: request_body,
|
|
)
|
|
monkeypatch.setattr(proxymod, "resolve_effective_proxy", lambda provider_proxy, key_proxy=None: None)
|
|
monkeypatch.setattr(proxymod, "resolve_proxy_info_async", _fake_resolve_proxy_info)
|
|
monkeypatch.setattr(proxymod, "get_proxy_label", lambda proxy_info: "direct")
|
|
monkeypatch.setattr(proxymod, "resolve_delegate_config_async", _fake_resolve_delegate)
|
|
monkeypatch.setattr(proxymod, "get_system_proxy_config_async", _fake_get_system_proxy)
|
|
monkeypatch.setattr(proxymod, "build_proxy_url_async", _fake_get_system_proxy)
|
|
|
|
|
|
async def _iter_chunks(chunks: list[bytes]) -> AsyncGenerator[bytes]:
|
|
for chunk in chunks:
|
|
yield chunk
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_execute_stream_request_uses_rust_executor_when_available(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
_patch_stream_setup(monkeypatch)
|
|
monkeypatch.setattr(chatmod.config, "executor_backend", "rust")
|
|
|
|
handler = _DummyChatHandler()
|
|
stream_processor = _FakeStreamProcessor()
|
|
ctx = StreamContext(model="gpt-test", api_format="openai:chat")
|
|
ctx.client_api_format = "openai:chat"
|
|
|
|
provider = SimpleNamespace(
|
|
name="provider",
|
|
id="provider-1",
|
|
provider_type="",
|
|
proxy=None,
|
|
request_timeout=None,
|
|
)
|
|
endpoint = SimpleNamespace(id="endpoint-1", api_format="openai:chat", base_url="https://x")
|
|
key = SimpleNamespace(id="key-1", proxy=None)
|
|
candidate = SimpleNamespace(
|
|
request_candidate_id="cand-1",
|
|
mapping_matched_model=None,
|
|
needs_conversion=False,
|
|
output_limit=None,
|
|
)
|
|
|
|
dummy_ctx = _DummyStreamResponseCtx()
|
|
|
|
async def _fake_execute_stream(self: object, plan: object) -> ExecutionRuntimeStreamResult:
|
|
assert getattr(plan, "stream") is True
|
|
return ExecutionRuntimeStreamResult(
|
|
status_code=200,
|
|
headers={"content-type": "text/event-stream", "x-upstream-test": "true"},
|
|
byte_iterator=_iter_chunks(
|
|
[
|
|
b"data: {\"id\":\"chunk-1\"}\n\n",
|
|
b"data: [DONE]\n\n",
|
|
]
|
|
),
|
|
response_ctx=dummy_ctx,
|
|
)
|
|
|
|
monkeypatch.setattr(chatmod.ExecutionRuntimeClient, "execute_stream", _fake_execute_stream)
|
|
|
|
stream = await handler._execute_stream_request(
|
|
ctx,
|
|
stream_processor,
|
|
provider,
|
|
endpoint,
|
|
key,
|
|
{"model": "gpt-test", "messages": [{"role": "user", "content": "hello"}]},
|
|
{},
|
|
candidate=candidate,
|
|
)
|
|
|
|
received = [chunk async for chunk in stream]
|
|
|
|
assert received == [
|
|
b"data: {\"id\":\"chunk-1\"}\n\n",
|
|
b"data: [DONE]\n\n",
|
|
]
|
|
assert ctx.status_code == 200
|
|
assert ctx.response_headers["x-upstream-test"] == "true"
|
|
assert stream_processor.prefetched_chunks == [b"data: {\"id\":\"chunk-1\"}\n\n"]
|
|
assert dummy_ctx.closed is True
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_execute_stream_request_uses_rust_sync_executor_for_non_stream_upstream(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
_patch_stream_setup(monkeypatch)
|
|
monkeypatch.setattr(chatmod.config, "executor_backend", "rust")
|
|
|
|
handler = _DummyChatHandler()
|
|
stream_processor = _FakeStreamProcessor()
|
|
stream_processor.on_streaming_start = None
|
|
ctx = StreamContext(model="gpt-test", api_format="openai:chat")
|
|
ctx.client_api_format = "openai:chat"
|
|
|
|
provider = SimpleNamespace(
|
|
name="provider",
|
|
id="provider-1",
|
|
provider_type="",
|
|
proxy=None,
|
|
request_timeout=None,
|
|
)
|
|
endpoint = SimpleNamespace(id="endpoint-1", api_format="openai:chat", base_url="https://x")
|
|
key = SimpleNamespace(id="key-1", proxy=None)
|
|
candidate = SimpleNamespace(
|
|
request_candidate_id="cand-1",
|
|
mapping_matched_model=None,
|
|
needs_conversion=False,
|
|
output_limit=None,
|
|
)
|
|
|
|
async def _fake_prepare_provider_request(self: object, **kwargs: Any) -> object:
|
|
del self, kwargs
|
|
return chatmod.ProviderRequestResult(
|
|
request_body={"model": "gpt-test", "messages": [{"role": "user", "content": "hello"}]},
|
|
url_model="gpt-test",
|
|
mapped_model=None,
|
|
envelope=None,
|
|
extra_headers={},
|
|
upstream_is_stream=False,
|
|
needs_conversion=False,
|
|
provider_api_format="provider:test",
|
|
client_api_format="openai:chat",
|
|
auth_info=_DummyAuthInfo(),
|
|
tls_profile=None,
|
|
)
|
|
|
|
async def _fake_execute_sync_json(self: object, plan: object) -> object:
|
|
del self
|
|
assert getattr(plan, "stream") is False
|
|
return SimpleNamespace(
|
|
status_code=200,
|
|
response_json={"id": "sync-1", "message": "hello"},
|
|
response_body_bytes=None,
|
|
headers={"content-type": "application/json"},
|
|
)
|
|
|
|
async def _should_not_call_stream(self: object, plan: object) -> ExecutionRuntimeStreamResult:
|
|
del self, plan
|
|
raise AssertionError("stream executor should not be used")
|
|
|
|
async def _should_not_get_http_client(*args: Any, **kwargs: Any) -> object:
|
|
raise AssertionError("python upstream client should not be used")
|
|
|
|
monkeypatch.setattr(
|
|
_DummyChatHandler,
|
|
"_prepare_provider_request",
|
|
_fake_prepare_provider_request,
|
|
)
|
|
monkeypatch.setattr(chatmod, "get_format_converter_registry", lambda: _FakeRegistry())
|
|
monkeypatch.setattr(
|
|
chatmod,
|
|
"iter_internal_response_as_stream_events",
|
|
lambda internal_resp: [{"kind": "chunk"}],
|
|
)
|
|
monkeypatch.setattr(chatmod, "get_parser_for_format", lambda _format: _FakeParser())
|
|
monkeypatch.setattr(chatmod.ExecutionRuntimeClient, "execute_sync_json", _fake_execute_sync_json)
|
|
monkeypatch.setattr(chatmod.ExecutionRuntimeClient, "execute_stream", _should_not_call_stream)
|
|
monkeypatch.setattr(
|
|
"src.clients.http_client.HTTPClientPool.get_upstream_client",
|
|
_should_not_get_http_client,
|
|
)
|
|
|
|
stream = await handler._execute_stream_request(
|
|
ctx,
|
|
stream_processor,
|
|
provider,
|
|
endpoint,
|
|
key,
|
|
{"model": "gpt-test", "messages": [{"role": "user", "content": "hello"}]},
|
|
{},
|
|
candidate=candidate,
|
|
)
|
|
|
|
received = [chunk async for chunk in stream]
|
|
|
|
assert received == [
|
|
b'data: {"delta": "hello"}\n\n',
|
|
b"data: [DONE]\n\n",
|
|
]
|
|
assert ctx.status_code == 200
|
|
assert ctx.input_tokens == 3
|
|
assert ctx.output_tokens == 5
|
|
assert ctx.cached_tokens == 1
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_execute_stream_request_accepts_async_generator_stream_processor(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
_patch_stream_setup(monkeypatch)
|
|
monkeypatch.setattr(chatmod.config, "executor_backend", "rust")
|
|
|
|
handler = _DummyChatHandler()
|
|
stream_processor = _FakeStreamProcessor()
|
|
ctx = StreamContext(model="gpt-test", api_format="openai:chat")
|
|
ctx.client_api_format = "openai:chat"
|
|
|
|
provider = SimpleNamespace(name="provider", id="provider-1", provider_type="", proxy=None)
|
|
endpoint = SimpleNamespace(id="endpoint-1", api_format="openai:chat", base_url="https://x")
|
|
key = SimpleNamespace(id="key-1", proxy=None)
|
|
candidate = SimpleNamespace(
|
|
request_candidate_id="cand-1",
|
|
mapping_matched_model=None,
|
|
needs_conversion=False,
|
|
output_limit=None,
|
|
)
|
|
|
|
dummy_ctx = _DummyStreamResponseCtx()
|
|
|
|
async def _fake_execute_stream(self: object, plan: object) -> ExecutionRuntimeStreamResult:
|
|
assert getattr(plan, "stream") is True
|
|
return ExecutionRuntimeStreamResult(
|
|
status_code=200,
|
|
headers={"content-type": "text/event-stream"},
|
|
byte_iterator=_iter_chunks(
|
|
[
|
|
b"data: {\"id\":\"chunk-1\"}\n\n",
|
|
b"data: [DONE]\n\n",
|
|
]
|
|
),
|
|
response_ctx=dummy_ctx,
|
|
)
|
|
|
|
monkeypatch.setattr(chatmod.ExecutionRuntimeClient, "execute_stream", _fake_execute_stream)
|
|
|
|
stream = await handler._execute_stream_request(
|
|
ctx,
|
|
stream_processor,
|
|
provider,
|
|
endpoint,
|
|
key,
|
|
{"model": "gpt-test", "messages": [{"role": "user", "content": "hello"}]},
|
|
{},
|
|
candidate=candidate,
|
|
)
|
|
|
|
received = [chunk async for chunk in stream]
|
|
|
|
assert received == [
|
|
b"data: {\"id\":\"chunk-1\"}\n\n",
|
|
b"data: [DONE]\n\n",
|
|
]
|
|
assert dummy_ctx.closed is True
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_execute_stream_request_allows_tunnel_delegate_for_rust(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
_patch_stream_setup(
|
|
monkeypatch,
|
|
proxy_info={"node_id": "node-1", "node_name": "relay-node", "mode": "tunnel"},
|
|
delegate_config={"tunnel": True, "node_id": "node-1"},
|
|
)
|
|
monkeypatch.setattr(chatmod.config, "executor_backend", "rust")
|
|
|
|
handler = _DummyChatHandler()
|
|
stream_processor = _FakeStreamProcessor()
|
|
ctx = StreamContext(model="gpt-test", api_format="openai:chat")
|
|
ctx.client_api_format = "openai:chat"
|
|
|
|
provider = SimpleNamespace(
|
|
name="provider",
|
|
id="provider-1",
|
|
provider_type="",
|
|
proxy={"enabled": True, "node_id": "node-1"},
|
|
)
|
|
endpoint = SimpleNamespace(id="endpoint-1", api_format="openai:chat", base_url="https://x")
|
|
key = SimpleNamespace(id="key-1", proxy=None)
|
|
candidate = SimpleNamespace(
|
|
request_candidate_id="cand-1",
|
|
mapping_matched_model=None,
|
|
needs_conversion=False,
|
|
output_limit=None,
|
|
)
|
|
|
|
dummy_ctx = _DummyStreamResponseCtx()
|
|
|
|
async def _fake_execute_stream(self: object, plan: object) -> ExecutionRuntimeStreamResult:
|
|
assert getattr(plan, "proxy") is not None
|
|
assert getattr(plan.proxy, "mode") == "tunnel"
|
|
assert getattr(plan.proxy, "node_id") == "node-1"
|
|
return ExecutionRuntimeStreamResult(
|
|
status_code=200,
|
|
headers={"content-type": "text/event-stream"},
|
|
byte_iterator=_iter_chunks([b"data: [DONE]\n\n"]),
|
|
response_ctx=dummy_ctx,
|
|
)
|
|
|
|
monkeypatch.setattr(chatmod.ExecutionRuntimeClient, "execute_stream", _fake_execute_stream)
|
|
|
|
stream = await handler._execute_stream_request(
|
|
ctx,
|
|
stream_processor,
|
|
provider,
|
|
endpoint,
|
|
key,
|
|
{"model": "gpt-test", "messages": [{"role": "user", "content": "hello"}]},
|
|
{},
|
|
candidate=candidate,
|
|
)
|
|
|
|
received = [chunk async for chunk in stream]
|
|
|
|
assert received == [b"data: [DONE]\n\n"]
|
|
assert dummy_ctx.closed is True
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_execute_stream_request_allows_tls_profile_for_rust(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
_patch_stream_setup(monkeypatch)
|
|
monkeypatch.setattr(chatmod.config, "executor_backend", "rust")
|
|
|
|
handler = _DummyChatHandler()
|
|
stream_processor = _FakeStreamProcessor()
|
|
ctx = StreamContext(model="gpt-test", api_format="openai:chat")
|
|
ctx.client_api_format = "openai:chat"
|
|
|
|
provider = SimpleNamespace(name="provider", id="provider-1", provider_type="", proxy=None)
|
|
endpoint = SimpleNamespace(id="endpoint-1", api_format="openai:chat", base_url="https://x")
|
|
key = SimpleNamespace(id="key-1", proxy=None)
|
|
candidate = SimpleNamespace(
|
|
request_candidate_id="cand-1",
|
|
mapping_matched_model=None,
|
|
needs_conversion=False,
|
|
output_limit=None,
|
|
)
|
|
|
|
dummy_ctx = _DummyStreamResponseCtx()
|
|
|
|
async def _fake_prepare_provider_request(self: object, **kwargs: Any) -> object:
|
|
del self, kwargs
|
|
return chatmod.ProviderRequestResult(
|
|
request_body={"model": "gpt-test", "messages": [{"role": "user", "content": "hello"}]},
|
|
url_model="gpt-test",
|
|
mapped_model=None,
|
|
envelope=None,
|
|
extra_headers={},
|
|
upstream_is_stream=True,
|
|
needs_conversion=False,
|
|
provider_api_format="openai:chat",
|
|
client_api_format="openai:chat",
|
|
auth_info=_DummyAuthInfo(),
|
|
tls_profile="claude_code_nodejs",
|
|
)
|
|
|
|
monkeypatch.setattr(
|
|
_DummyChatHandler,
|
|
"_prepare_provider_request",
|
|
_fake_prepare_provider_request,
|
|
)
|
|
|
|
async def _fake_execute_stream(self: object, plan: object) -> ExecutionRuntimeStreamResult:
|
|
assert getattr(plan, "tls_profile") == "claude_code_nodejs"
|
|
return ExecutionRuntimeStreamResult(
|
|
status_code=200,
|
|
headers={"content-type": "text/event-stream"},
|
|
byte_iterator=_iter_chunks([b"data: [DONE]\n\n"]),
|
|
response_ctx=dummy_ctx,
|
|
)
|
|
|
|
monkeypatch.setattr(chatmod.ExecutionRuntimeClient, "execute_stream", _fake_execute_stream)
|
|
|
|
stream = await handler._execute_stream_request(
|
|
ctx,
|
|
stream_processor,
|
|
provider,
|
|
endpoint,
|
|
key,
|
|
{"model": "gpt-test", "messages": [{"role": "user", "content": "hello"}]},
|
|
{},
|
|
candidate=candidate,
|
|
)
|
|
|
|
received = [chunk async for chunk in stream]
|
|
|
|
assert received == [b"data: [DONE]\n\n"]
|
|
assert dummy_ctx.closed is True
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_execute_stream_request_turns_rust_upstream_error_into_http_status_error(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
_patch_stream_setup(monkeypatch)
|
|
monkeypatch.setattr(chatmod.config, "executor_backend", "rust")
|
|
|
|
handler = _DummyChatHandler()
|
|
stream_processor = _FakeStreamProcessor()
|
|
ctx = StreamContext(model="gpt-test", api_format="openai:chat")
|
|
ctx.client_api_format = "openai:chat"
|
|
|
|
provider = SimpleNamespace(name="provider", id="provider-1", provider_type="", proxy=None)
|
|
endpoint = SimpleNamespace(id="endpoint-1", api_format="openai:chat", base_url="https://x")
|
|
key = SimpleNamespace(id="key-1", proxy=None)
|
|
candidate = SimpleNamespace(
|
|
request_candidate_id="cand-1",
|
|
mapping_matched_model=None,
|
|
needs_conversion=False,
|
|
output_limit=None,
|
|
)
|
|
|
|
dummy_ctx = _DummyStreamResponseCtx()
|
|
|
|
async def _fake_execute_stream(self: object, plan: object) -> ExecutionRuntimeStreamResult:
|
|
assert getattr(plan, "stream") is True
|
|
return ExecutionRuntimeStreamResult(
|
|
status_code=429,
|
|
headers={"content-type": "application/json"},
|
|
byte_iterator=_iter_chunks([b'{"error":{"message":"slow down"}}']),
|
|
response_ctx=dummy_ctx,
|
|
)
|
|
|
|
monkeypatch.setattr(chatmod.ExecutionRuntimeClient, "execute_stream", _fake_execute_stream)
|
|
|
|
with pytest.raises(httpx.HTTPStatusError) as exc_info:
|
|
await handler._execute_stream_request(
|
|
ctx,
|
|
stream_processor,
|
|
provider,
|
|
endpoint,
|
|
key,
|
|
{"model": "gpt-test", "messages": [{"role": "user", "content": "hello"}]},
|
|
{},
|
|
candidate=candidate,
|
|
)
|
|
|
|
assert exc_info.value.response.status_code == 429
|
|
assert "slow down" in exc_info.value.upstream_response # type: ignore[attr-defined]
|
|
assert dummy_ctx.closed is True
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_execute_stream_request_raises_when_rust_unavailable(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
_patch_stream_setup(monkeypatch)
|
|
monkeypatch.setattr(chatmod.config, "executor_backend", "rust")
|
|
|
|
handler = _DummyChatHandler()
|
|
ctx = StreamContext(model="gpt-test", api_format="openai:chat")
|
|
ctx.client_api_format = "openai:chat"
|
|
|
|
provider = SimpleNamespace(name="provider", id="provider-1", provider_type="", proxy=None)
|
|
endpoint = SimpleNamespace(id="endpoint-1", api_format="openai:chat", base_url="https://x")
|
|
key = SimpleNamespace(id="key-1", proxy=None)
|
|
candidate = SimpleNamespace(
|
|
request_candidate_id="cand-1",
|
|
mapping_matched_model=None,
|
|
needs_conversion=False,
|
|
output_limit=None,
|
|
)
|
|
|
|
async def _fake_execute_stream(self: object, plan: object) -> ExecutionRuntimeStreamResult:
|
|
del plan
|
|
raise ExecutionRuntimeClientError("executor down")
|
|
|
|
async def _fake_get_upstream_client(*args: Any, **kwargs: Any) -> object:
|
|
raise AssertionError("python fallback should not be used")
|
|
|
|
monkeypatch.setattr(chatmod.ExecutionRuntimeClient, "execute_stream", _fake_execute_stream)
|
|
monkeypatch.setattr(
|
|
"src.clients.http_client.HTTPClientPool.get_upstream_client",
|
|
_fake_get_upstream_client,
|
|
)
|
|
|
|
with pytest.raises(ProviderNotAvailableException) as exc_info:
|
|
await handler._execute_stream_request(
|
|
ctx,
|
|
object(),
|
|
provider,
|
|
endpoint,
|
|
key,
|
|
{"model": "gpt-test", "messages": [{"role": "user", "content": "hello"}]},
|
|
{},
|
|
candidate=candidate,
|
|
)
|
|
|
|
assert exc_info.value.message == "执行器暂时不可用,请稍后重试"
|
|
assert exc_info.value.upstream_response == "executor down"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_execute_stream_request_raises_when_remote_contract_is_ineligible(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
_patch_stream_setup(monkeypatch)
|
|
monkeypatch.setattr(chatmod.config, "executor_backend", "rust")
|
|
|
|
handler = _DummyChatHandler()
|
|
ctx = StreamContext(model="gpt-test", api_format="openai:chat")
|
|
ctx.client_api_format = "openai:chat"
|
|
|
|
provider = SimpleNamespace(name="provider", id="provider-1", provider_type="", proxy=None)
|
|
endpoint = SimpleNamespace(id="endpoint-1", api_format="openai:chat", base_url="https://x")
|
|
key = SimpleNamespace(id="key-1", proxy=None)
|
|
candidate = SimpleNamespace(
|
|
request_candidate_id="cand-1",
|
|
mapping_matched_model=None,
|
|
needs_conversion=False,
|
|
output_limit=None,
|
|
)
|
|
|
|
async def _should_not_call_rust(self: object, plan: object) -> ExecutionRuntimeStreamResult:
|
|
del self, plan
|
|
raise AssertionError("rust executor should not be called")
|
|
|
|
async def _fake_get_upstream_client(*args: Any, **kwargs: Any) -> object:
|
|
raise AssertionError("python fallback should not be used")
|
|
|
|
monkeypatch.setattr(chatmod, "is_remote_execution_runtime_contract_eligible", lambda plan: False)
|
|
monkeypatch.setattr(chatmod.ExecutionRuntimeClient, "execute_stream", _should_not_call_rust)
|
|
monkeypatch.setattr(
|
|
"src.clients.http_client.HTTPClientPool.get_upstream_client",
|
|
_fake_get_upstream_client,
|
|
)
|
|
|
|
with pytest.raises(ProviderNotAvailableException) as exc_info:
|
|
await handler._execute_stream_request(
|
|
ctx,
|
|
object(),
|
|
provider,
|
|
endpoint,
|
|
key,
|
|
{"model": "gpt-test", "messages": [{"role": "user", "content": "hello"}]},
|
|
{},
|
|
candidate=candidate,
|
|
)
|
|
|
|
assert exc_info.value.message == "执行器暂时不可用,请稍后重试"
|
|
assert exc_info.value.upstream_response == (
|
|
"execution contract is not eligible for rust executor"
|
|
)
|