feat(tunnel): 请求体流式传输 & OpenAI CLI 请求 key 排序优化

Hub 端:
- open_local_stream 不再接收 body 参数,改为通过 push_local_request_body 分块推送
- 请求体按 32KB 分帧发送,避免大请求一次性压缩和传输
- local_relay 改为流式解析 envelope 和转发请求体

Proxy 端:
- stream_handler 改为流式传输请求体到上游,不再预先收集完整 body
- upstream_client 请求体类型从 Full<Bytes> 改为 UnsyncBoxBody 以支持流式传输
- dispatcher 将 StreamEnd/StreamError 事件转发给 stream handler

Python 端:
- hub_transport relay envelope 改为异步生成器流式发送
- 提取 reorder_openai_cli_request_prefix_keys 为公共函数
- Codex passthrough 路径也应用稳定的前缀 key 排序
This commit is contained in:
fawney19
2026-03-17 22:07:09 +08:00
parent 59840fa419
commit 0342f609d0
12 changed files with 541 additions and 125 deletions
+53
View File
@@ -58,6 +58,31 @@ def test_patch_openai_cli_request_for_codex_preserves_existing_prompt_cache_key(
assert out["prompt_cache_key"] == "client-cache-key"
def test_patch_openai_cli_request_for_codex_reorders_stable_prefix_keys() -> None:
req = {
"temperature": 0.7,
"input": [],
"metadata": {"request_id": "abc"},
"model": "gpt-test",
"tools": [{"type": "function", "name": "demo"}],
"instructions": "keep",
"store": True,
"_aether_compact": True,
}
out = patch_openai_cli_request_for_codex(req)
assert list(out.keys()) == [
"model",
"instructions",
"tools",
"input",
"temperature",
"metadata",
"store",
]
def test_patch_openai_cli_request_for_codex_does_not_inject_prompt_cache_key() -> None:
req = {"model": "gpt-test", "input": [], "_aether_compact": True}
@@ -151,6 +176,34 @@ def test_openai_cli_normalizer_codex_variant_keeps_instructions_missing_for_defa
assert patched["instructions"] == "You are GPT-5."
def test_openai_cli_normalizer_patch_for_codex_reorders_stable_prefix_keys() -> None:
from src.core.api_format.conversion.normalizers.openai_cli import OpenAICliNormalizer
normalizer = OpenAICliNormalizer()
out = normalizer.patch_for_variant(
{
"temperature": 0.7,
"input": [],
"metadata": {"request_id": "abc"},
"model": "gpt-test",
"tools": [{"type": "function", "name": "demo"}],
"instructions": "keep",
"_aether_compact": True,
},
"codex",
)
assert out is not None
assert list(out.keys()) == [
"model",
"instructions",
"tools",
"input",
"temperature",
"metadata",
]
def test_codex_envelope_extra_headers_does_not_inject_synthetic_headers() -> None:
from src.services.provider.adapters.codex.envelope import codex_oauth_envelope
+33 -1
View File
@@ -15,6 +15,7 @@ class _FakeRelayClient:
def __init__(self, response: httpx.Response) -> None:
self.response = response
self.sent_request: httpx.Request | None = None
self.sent_body: bytes | None = None
def build_request(self, method: str, url: str, **kwargs: Any) -> httpx.Request:
return httpx.Request(method, url, **kwargs)
@@ -22,6 +23,7 @@ class _FakeRelayClient:
async def send(self, request: httpx.Request, *, stream: bool = False) -> httpx.Response:
_ = stream
self.sent_request = request
self.sent_body = await request.aread()
self.response.request = request
return self.response
@@ -56,7 +58,7 @@ async def test_transport_encodes_local_relay_envelope(monkeypatch: pytest.Monkey
await response.aclose()
assert fake_client.sent_request is not None
payload = fake_client.sent_request.content
payload = fake_client.sent_body
assert payload is not None
meta_len = struct.unpack("!I", payload[:4])[0]
meta = json.loads(payload[4 : 4 + meta_len].decode("utf-8"))
@@ -88,3 +90,33 @@ async def test_transport_maps_relay_timeout_to_read_timeout(
with pytest.raises(httpx.ReadTimeout, match="relay timed out"):
await transport.handle_async_request(request)
@pytest.mark.asyncio
async def test_transport_streams_request_body_from_async_generator(
monkeypatch: pytest.MonkeyPatch,
) -> None:
transport = HubTunnelTransport("node-1", timeout=12.0)
fake_client = _FakeRelayClient(httpx.Response(200, content=b"ok"))
monkeypatch.setattr("src.services.proxy_node.hub_transport.get_hub_config", _relay_config)
monkeypatch.setattr(transport, "_relay_client", fake_client)
async def body() -> Any:
yield b'{"hello":'
yield b'"world"}'
request = httpx.Request(
"POST",
"https://example.com/v1/chat/completions",
headers={"content-type": "application/json"},
content=body(),
)
response = await transport.handle_async_request(request)
assert response.status_code == 200
await response.aclose()
payload = fake_client.sent_body
assert payload is not None
meta_len = struct.unpack("!I", payload[:4])[0]
assert payload[4 + meta_len :] == b'{"hello":"world"}'