Files
Aether/tests/api/handlers/base/test_chat_rust_stream.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

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"
)