Files
LangBot/tests/e2e/test_llm_request_chain.py
T
RockChinQandHyu 616981b0cf perf(llm): harden request policy and optimize connections, reasoning and prompt caching (#2661)
* fix(llm): centralize request policy and preserve stream diagnostics

* test(llm): dispose SQLite engine after migration regression

* perf(llm): reuse connections and unify reasoning and prompt cache policy

* fix(llm): preserve explicit native prompt cache options

* perf(llm): fix measured TLS setup and streaming reuse overhead

* fix(llm): resolve plugin request-chain regressions found by E2E

---------

Co-authored-by: Hyu <[email protected]>
2026-10-15 19:48:35 +08:00

443 lines
18 KiB
Python

"""Real HTTP -> Core -> Plugin Runtime -> Runner -> Core -> HTTP provider tests.
Only the remote provider is deterministic. No requester, SDK action transport,
model manager, persistence or monitoring implementation is mocked.
"""
import json
import importlib.metadata
import os
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
import shutil
import select
import socket
import sqlite3
import threading
import time
import pytest
import yaml
import httpx
from packaging.version import Version
from tests.e2e import test_runner_plugin_runtime as runtime
# Reuse the real process lifecycle; override the package/config and port below.
runner_runtime_ports = runtime.runner_runtime_ports
runner_runtime_process = runtime.runner_runtime_process
runner_langbot_process = runtime.runner_langbot_process
runner_client = runtime.runner_client
pytestmark = pytest.mark.e2e
RUNNER = runtime.QA_RUNNER_ID
USAGE = {
'prompt_tokens': 24,
'completion_tokens': 4,
'total_tokens': 28,
'prompt_tokens_details': {'cached_tokens': 16},
'completion_tokens_details': {'reasoning_tokens': 2},
}
@pytest.fixture(scope='session')
def runner_e2e_tmpdir(tmp_path_factory):
# Retain logs/SQLite in pytest's bounded temp history for failure diagnosis.
return tmp_path_factory.mktemp('llm-chain')
@pytest.fixture(scope='session')
def runner_e2e_port():
return runtime._free_port()
@pytest.fixture(scope='session')
def runner_e2e_config_path(runner_e2e_tmpdir, runner_e2e_port, runner_runtime_ports):
path = runtime.runner_e2e_config_path.__wrapped__(runner_e2e_tmpdir, runner_e2e_port, runner_runtime_ports)
root = runner_e2e_tmpdir / 'agent-runner-qa-package'
manifest = root / 'components/runner/default.yaml'
data = yaml.safe_load(manifest.read_text(encoding='utf-8'))
data['spec']['permissions'] = {'models': ['invoke', 'stream', 'count_tokens']}
data['spec']['config'] = [
{'name': 'model', 'label': {'en_US': 'Model'}, 'type': 'llm-model-selector', 'required': True},
]
manifest.write_text(yaml.safe_dump(data), encoding='utf-8')
shutil.copyfile(Path(__file__).parent / 'utils/llm_probe_runner.py', root / 'components/runner/default.py')
if legacy_root := os.environ.get('LLM_E2E_LEGACY_API_DIR'):
for filename in ('legacy_api.py', 'legacy_message.py'):
shutil.copyfile(Path(legacy_root) / filename, root / filename)
shutil.make_archive(str(runner_e2e_tmpdir / 'agent-runner-qa'), 'zip', root_dir=root)
return path
@pytest.fixture(scope='module')
def provider_wire():
records = []
class Handler(BaseHTTPRequestHandler):
protocol_version = 'HTTP/1.1'
def log_message(self, *args):
pass
def do_POST(self):
body = json.loads(self.rfile.read(int(self.headers['Content-Length'])))
scenario = self.headers.get('x-e2e-case', 'normal')
row = {'body': body, 'headers': dict(self.headers), 'case': scenario, 'closed': False}
records.append(row)
attempt = sum(x['case'] == scenario for x in records)
status = 503 if scenario.startswith('retry') and attempt == 1 else 200
if scenario == 'auth':
status = 401
if status != 200:
data = json.dumps({'error': {'message': 'injected failure', 'type': 'api_error'}}).encode()
self.send_response(status)
self.send_header('Content-Type', 'application/json')
self.send_header('Content-Length', str(len(data)))
self.send_header('Retry-After', '0')
self.end_headers()
self.wfile.write(data)
return
if scenario in ('first-timeout', 'total-timeout'):
time.sleep(1)
try:
if not body.get('stream'):
data = json.dumps(
{
'id': 'e2e',
'object': 'chat.completion',
'created': 1,
'model': body['model'],
'choices': [
{
'index': 0,
'message': {'role': 'assistant', 'content': 'hello world'},
'finish_reason': 'stop',
}
],
'usage': USAGE,
}
).encode()
self.send_response(200)
self.send_header('Content-Type', 'application/json')
self.send_header('Content-Length', str(len(data)))
self.end_headers()
self.wfile.write(data)
return
self.send_response(200)
self.send_header('Content-Type', 'text/event-stream')
self.send_header('Transfer-Encoding', 'chunked')
self.end_headers()
def send(event):
data = ('data: ' + (event if isinstance(event, str) else json.dumps(event)) + '\n\n').encode()
self.wfile.write(f'{len(data):X}\r\n'.encode() + data + b'\r\n')
self.wfile.flush()
base = {'id': 'e2e', 'object': 'chat.completion.chunk', 'created': 1, 'model': body['model']}
if scenario.startswith('reasoning-'):
send(
{
**base,
'choices': [
{'index': 0, 'delta': {'reasoning_content': 'private thought'}, 'finish_reason': None}
],
}
)
for index, content in enumerate(['hello ', 'world']):
send({**base, 'choices': [{'index': 0, 'delta': {'content': content}, 'finish_reason': None}]})
if index == 0 and (scenario in ('idle-timeout', 'disconnect') or scenario.startswith('cancel-')):
if scenario == 'disconnect':
self.close_connection = True
return
deadline = time.monotonic() + 1
while time.monotonic() < deadline:
if select.select([self.connection], [], [], 0.02)[0]:
if not self.connection.recv(1, socket.MSG_PEEK):
row['closed'] = True
return
send({**base, 'choices': [{'index': 0, 'delta': {}, 'finish_reason': 'stop'}]})
send({**base, 'choices': [], 'usage': USAGE})
send('[DONE]')
self.wfile.write(b'0\r\n\r\n')
self.wfile.flush()
except (BrokenPipeError, ConnectionResetError, ConnectionAbortedError):
row['closed'] = True
server = ThreadingHTTPServer(('127.0.0.1', 0), Handler)
thread = threading.Thread(target=server.serve_forever, daemon=True)
thread.start()
yield f'http://127.0.0.1:{server.server_port}/v1', records
server.shutdown()
server.server_close()
thread.join(timeout=5)
@pytest.fixture(scope='module')
def chain(request, provider_wire):
request.getfixturevalue('runner_langbot_process')
port = request.getfixturevalue('runner_e2e_port')
client = httpx.Client(base_url=f'http://127.0.0.1:{port}', timeout=90, trust_env=False)
request.addfinalizer(client.close)
tmpdir = request.getfixturevalue('runner_e2e_tmpdir')
token = runtime._init_and_auth(client)
runtime._ensure_qa_plugin(client, token, tmpdir / 'agent-runner-qa.zip')
client.headers['Authorization'] = f'Bearer {token}'
def post(path, body):
response = client.post('/api/v1/' + path, json=body)
assert response.status_code == 200, response.text
payload = response.json()
assert payload['code'] == 0, payload
return payload['data']
provider = post(
'provider/providers',
{
'name': 'LLM chain E2E',
'requester': 'openai-chat-completions',
'base_url': provider_wire[0],
'api_keys': ['e2e-not-a-secret'],
},
)['uuid']
model = post(
'provider/models/llm',
{
'name': 'gpt-4o',
'provider_uuid': provider,
'abilities': ['func_call', 'vision'],
'extra_args': {
'temperature': 0.1,
'request_policy': {'retries': 1, 'backoff_base': 0.01, 'backoff_max': 0.01},
},
},
)['uuid']
agent = post(
'agents',
{
'kind': 'agent',
'name': 'LLM chain E2E',
'config': {
'runner': {'id': RUNNER},
'runner_config': {RUNNER: {'model': model}},
},
},
)['uuid']
def invoke(mode, case='normal', extra=None, *, configuration=None, options=None):
selected_model, selected_agent = model, agent
if configuration:
configured_provider = post(
'provider/providers',
{
'name': case,
'requester': configuration.get('requester', 'openai-chat-completions'),
'base_url': provider_wire[0],
'api_keys': ['e2e-not-a-secret'],
},
)['uuid']
selected_model = post(
'provider/models/llm',
{
'name': configuration.get('name', 'gpt-4o'),
'provider_uuid': configured_provider,
'abilities': ['func_call', 'vision'],
'extra_args': configuration.get('extra_args', {}),
},
)['uuid']
selected_agent = post(
'agents',
{
'kind': 'agent',
'name': case,
'config': {
'runner': {'id': RUNNER},
'runner_config': {RUNNER: {'model': selected_model}},
},
},
)['uuid']
result = post(
f'agents/{selected_agent}/debug',
{
'text': json.dumps(
{'mode': mode, 'model': selected_model, 'case': case, 'extra': extra or {}, **(options or {})}
),
'conversation_id': 'e2e-' + case,
'event_type': 'message.received',
},
)
assert result.get('final_text'), result
return json.loads(result['final_text'])
return invoke, provider_wire[1], tmpdir
def test_multimodal_and_tool_definitions_survive_plugin_transport(chain):
invoke, records, _ = chain
image = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+jRZkAAAAASUVORK5CYII='
messages = [
{
'role': 'user',
'content': [
{'type': 'text', 'text': 'Describe this image'},
{'type': 'image_url', 'image_url': {'url': image}},
],
}
]
result = invoke('runner-events', 'multimodal', options={'messages': messages, 'tool': True})
assert result['text'] == 'hello world', result
wire = records[-1]['body']
assert wire['messages'][0]['content'][1]['image_url']['url'] == image
assert wire['tools'][0]['function']['name'] == 'e2e_lookup'
def test_openrouter_budget_cache_and_explicit_attribution(chain):
invoke, records, _ = chain
result = invoke(
'runner-events',
'openrouter-budget-cache',
configuration={
'requester': 'openrouter-chat-completions',
'name': 'anthropic/claude-sonnet-4',
'extra_args': {
'reasoning_config': {'budget_tokens': 2048},
'prompt_caching': {'mode': 'anthropic'},
'max_tokens': 4096,
},
},
extra={
'extra_headers': {'HTTP-Referer': 'https://langbot.app', 'X-OpenRouter-Title': 'LangBot'},
'request_policy': {'conversation_header': 'x-langbot-conversation-id'},
},
options={'messages': [{'role': 'system', 'content': 'Stable prefix'}, {'role': 'user', 'content': 'Hello'}]},
)
assert result['text'] == 'hello world', result
wire = records[-1]
assert wire['body']['reasoning']['max_tokens'] == 2048
assert wire['body']['messages'][0]['content'][0]['cache_control']['type'] == 'ephemeral'
headers = {k.lower(): v for k, v in wire['headers'].items()}
assert headers['http-referer'] == 'https://langbot.app'
assert headers['x-openrouter-title'] == 'LangBot'
assert headers['x-langbot-conversation-id'].startswith('lb-')
assert 'prompt_caching' not in wire['body'] and 'reasoning_config' not in wire['body']
def test_openai_cache_options_and_call_override(chain):
invoke, records, _ = chain
result = invoke(
'runner-usage',
'openai-cache',
configuration={
'extra_args': {'temperature': 0.1, 'prompt_caching': {'mode': 'openai', 'retention': 'in_memory'}},
},
extra={'temperature': 0.7, 'prompt_cache_key': 'explicit-test-cache'},
)
assert result['text'] == 'hello world', result
assert records[-1]['body']['temperature'] == 0.7
assert records[-1]['body']['prompt_cache_key'] == 'explicit-test-cache'
assert records[-1]['body']['prompt_cache_retention'] == 'in_memory'
@pytest.mark.parametrize(
'mode',
[
'runner-message',
'runner-usage',
'runner-stream',
'runner-events',
'plugin-message',
'plugin-usage',
'plugin-stream',
'plugin-events',
],
)
def test_public_api_shapes_and_usage(chain, mode):
invoke, records, _ = chain
result = invoke(mode, mode)
assert result['text'] == 'hello world', result
assert not result.get('error'), result
if mode.endswith(('usage', 'events')):
assert result['usage']['prompt_tokens_details']['cached_tokens'] == 16
assert result['usage']['completion_tokens_details']['reasoning_tokens'] == 2
wire = records[-1]['body']
assert wire['temperature'] == 0.1
assert 'request_policy' not in wire
@pytest.mark.parametrize(
'case,extra,attempts,error',
[
('retry-once', {}, 2, False),
('retry-disabled', {'num_retries': 0}, 1, True),
('retry-max-disabled', {'max_retries': 0}, 1, True),
('auth', {}, 1, True),
('first-timeout', {'request_policy': {'first_event_timeout': 0.2, 'retries': 0}}, 1, True),
('total-timeout', {'request_policy': {'total_timeout': 0.2, 'retries': 0}}, 1, True),
('idle-timeout', {'request_policy': {'stream_idle_timeout': 0.2}}, 1, True),
('disconnect', {}, 1, True),
],
)
def test_failures_retry_boundaries_and_recovery(chain, case, extra, attempts, error):
invoke, records, _ = chain
result = invoke('runner-events', case, extra)
assert bool(result.get('error')) == error, result
assert sum(row['case'] == case for row in records) == attempts
assert invoke('runner-events', 'recover-' + case)['text'] == 'hello world'
@pytest.mark.parametrize('surface', ['runner-events', 'runner-stream', 'plugin-events', 'plugin-stream'])
def test_cancellation_releases_upstream_and_next_request_works(chain, surface):
invoke, records, tmpdir = chain
case = 'cancel-' + surface
result = invoke(case, case)
assert result['text'] == 'hello ', result
time.sleep(1.5)
cancelled = [row for row in records if row['case'] == case]
with sqlite3.connect(tmpdir / 'data/langbot.db') as db:
diagnostics = db.execute('SELECT session_id, details FROM monitoring_llm_calls').fetchall()
assert len(cancelled) == 1
assert invoke('runner-events', 'after-' + case)['text'] == 'hello world'
if (
not cancelled[0]['closed']
and Version(importlib.metadata.version('langbot-plugin')) <= Version('0.7.11')
and not os.environ.get('LLM_E2E_REQUIRE_STREAM_CLOSE')
):
pytest.xfail('Published SDK <=0.7.11 omits aclosing in public proxy wrappers; requires companion SDK fix')
assert cancelled[0]['closed'], diagnostics[-2:]
@pytest.mark.parametrize('remove_think', [True, False])
def test_reasoning_visibility_through_runner(chain, remove_think):
result = chain[0]('runner-events', f'reasoning-{remove_think}', options={'remove_think': remove_think})
assert result['text'].endswith('hello world'), result
assert ('private thought' in result['text']) == (not remove_think)
def test_invalid_budget_fails_before_network_and_recovers(chain):
invoke, records, _ = chain
result = invoke('runner-events', 'invalid-budget', {'reasoning_config': {'budget_tokens': 1}})
assert result.get('error'), result
assert not any(row['case'] == 'invalid-budget' for row in records)
assert invoke('runner-events', 'after-invalid-budget')['text'] == 'hello world'
def test_monitoring_persists_complete_usage_and_structured_errors(chain):
invoke, _, tmpdir = chain
invoke('runner-events', 'monitoring')
with sqlite3.connect(tmpdir / 'data/langbot.db') as db:
rows = db.execute('SELECT details FROM monitoring_llm_calls').fetchall()
details = [json.loads(row[0]) for row in rows if row[0]]
assert any(x.get('usage', {}).get('prompt_tokens_details', {}).get('cached_tokens') == 16 for x in details)
assert any(x.get('timing', {}).get('transport_setup_ms') is not None for x in details)
assert any(x.get('error', {}).get('code') for x in details if x.get('error'))
@pytest.mark.skipif(not os.environ.get('LLM_E2E_LEGACY_API_DIR'), reason='Historical SDK sources not supplied')
@pytest.mark.parametrize('mode', ['legacy-message', 'legacy-usage', 'legacy-stream', 'legacy-events'])
def test_historical_sdk_proxy_and_message_entities(chain, mode):
result = chain[0](mode, mode)
assert result['text'] == 'hello world', result
assert not result.get('error'), result
if mode.endswith(('usage', 'events')):
assert result['usage']['prompt_tokens_details']['cached_tokens'] == 16