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]>
This commit is contained in:
RockChinQ
2026-10-15 19:48:35 +08:00
committed by GitHub
co-authored by Hyu
parent 502b38130f
commit 616981b0cf
30 changed files with 4556 additions and 405 deletions
+442
View File
@@ -0,0 +1,442 @@
"""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
+93
View File
@@ -0,0 +1,93 @@
"""Packaged into the isolated E2E plugin; calls only public SDK LLM APIs."""
from contextlib import aclosing
import json
from langbot_plugin.api.definition.components.runner.runner import Runner
from langbot_plugin.api.entities.builtin.provider.message import Message
from langbot_plugin.api.entities.builtin.runner.result import RunnerResult
from langbot_plugin.api.proxies.langbot_api import LangBotAPIProxy
class DefaultRunner(Runner):
async def run(self, ctx):
options = json.loads(ctx.input.to_text())
mode = options['mode']
cancel = mode.startswith('cancel-')
if cancel:
mode = mode.removeprefix('cancel-')
api = LangBotAPIProxy(self._plugin_runtime_handler) if mode.startswith('plugin-') else ctx.api
message_type = Message
if mode.startswith('legacy-'):
# Historical files are supplied verbatim from a released SDK tag.
# Its proxy and serializers use the real current runtime transport.
import legacy_api
import legacy_message
legacy_api.provider_message = legacy_message
api = legacy_api.LangBotAPIProxy(self._plugin_runtime_handler)
message_type = legacy_message.Message
extra = {
**options['extra'],
'extra_headers': {
**options['extra'].get('extra_headers', {}),
'x-e2e-case': options['case'],
},
}
kwargs = {
'llm_model_uuid': options['model'],
'messages': [
message_type.model_validate(m)
for m in options.get(
'messages',
[
{'role': 'user', 'content': 'Hello'},
],
)
],
'extra_args': extra,
}
if options.get('tool'):
from langbot_plugin.api.entities.builtin.resource.tool import LLMTool
async def lookup(**kwargs):
return 'result'
kwargs['funcs'] = [
LLMTool(
name='e2e_lookup',
human_desc='Lookup',
description='Lookup',
parameters={'type': 'object', 'properties': {}},
func=lookup,
)
]
result = {'text': '', 'usage': None}
if 'remove_think' in options:
kwargs['remove_think'] = options['remove_think']
try:
if mode.endswith('message'):
message = await api.invoke_llm(**kwargs)
assert isinstance(message, message_type)
result['text'] = message.content
elif mode.endswith('usage'):
response = await api.invoke_llm_with_usage(**kwargs)
result.update(
text=response.message.content, usage=response.usage.model_dump() if response.usage else None
)
else:
events = mode.endswith('events')
method = api.invoke_llm_stream_events if events else api.invoke_llm_stream
async with aclosing(method(**kwargs)) as stream:
async for item in stream:
chunk = item.chunk if events else item
if chunk:
result['text'] += chunk.content or ''
if events and item.usage:
result['usage'] = item.usage.model_dump()
if cancel and result['text']:
break
except Exception as exc:
result['error'] = type(exc).__name__ + ': ' + str(exc)
yield RunnerResult.message_completed(ctx.run_id, Message(role='assistant', content=json.dumps(result)))
yield RunnerResult.run_completed(ctx.run_id, finish_reason='stop')
@@ -10,6 +10,27 @@ from __future__ import annotations
import pytest
from unittest.mock import Mock, AsyncMock
from importlib import import_module
from types import SimpleNamespace
@pytest.mark.parametrize(
'call_args',
[
{'num_retries': 0},
{'max_retries': 0},
{'timeout': 3},
{'request_policy': {'stream_idle_timeout': 2}},
],
)
def test_llm_bridge_keeps_model_and_call_policy_layers_separate(call_args):
from langbot.pkg.provider.modelmgr.request_policy import RequestPolicy
model_args = {'temperature': 0.2, 'request_policy': {'retries': 2, 'read_timeout': 90}}
model = SimpleNamespace(model_entity=SimpleNamespace(extra_args=model_args))
merged = get_handler_module()._merge_model_extra_args(model, call_args, llm_policy=True)
assert RequestPolicy.resolve(model_args, merged) == RequestPolicy.resolve(model_args, call_args)
assert merged['temperature'] == 0.2
assert model_args['request_policy'] == {'retries': 2, 'read_timeout': 90}
def get_handler_module():
+75
View File
@@ -1,6 +1,7 @@
"""Replay synthetic HTTP/SSE traffic through the real Codex requester."""
import json
import asyncio
from collections import OrderedDict
from types import SimpleNamespace
from unittest.mock import AsyncMock
@@ -16,6 +17,32 @@ TOKENS = {'access_token': 'access-secret', 'account_id': 'account', 'connection_
MODEL = SimpleNamespace(model_entity=SimpleNamespace(name='codex-test', extra_args={}, reasoning_config=None))
@pytest.mark.asyncio
async def test_response_headers_obey_host_first_event_deadline(monkeypatch):
from langbot.pkg.provider.modelmgr.errors import RequesterError
from langbot.pkg.provider.modelmgr.request_policy import CallState, RequestPolicy, current_call
cancelled = asyncio.Event()
async def handler(request):
try:
await asyncio.sleep(10)
finally:
cancelled.set()
obj = requester(monkeypatch, handler)
state = CallState(RequestPolicy(first_event_timeout=0.02))
token = current_call.set(state)
try:
with pytest.raises(RequesterError) as caught:
await obj.invoke_llm(None, MODEL, [])
finally:
current_call.reset(token)
assert caught.value.code == 'timeout'
assert caught.value.phase == 'first_event'
assert cancelled.is_set()
def requester(monkeypatch, handler):
real_client = httpx.AsyncClient
monkeypatch.setattr(
@@ -198,3 +225,51 @@ def test_advanced_parameters_cannot_override_transport(monkeypatch, key):
obj = requester(monkeypatch, lambda request: httpx.Response(200))
with pytest.raises(ValueError, match='Unsupported Codex advanced'):
obj._body(None, MODEL, [], None, {key: 'secret'}, TOKENS)
@pytest.mark.asyncio
async def test_early_close_releases_nested_http_stream(monkeypatch):
closed = asyncio.Event()
class Body(httpx.AsyncByteStream):
async def __aiter__(self):
yield b'data: {"type":"response.output_text.delta","delta":"first"}\n\n'
await asyncio.Event().wait()
async def aclose(self):
closed.set()
obj = requester(monkeypatch, lambda request: httpx.Response(200, stream=Body()))
stream = obj.invoke_llm_stream(None, MODEL, [])
assert (await anext(stream)).content == 'first'
await stream.aclose()
assert closed.is_set()
@pytest.mark.asyncio
async def test_host_deadline_does_not_cancel_suspended_consumer(monkeypatch, runtime_provider, runtime_llm_model):
from langbot.pkg.provider.modelmgr.errors import RequesterError
from tests.unit_tests.provider.conftest import TEST_EXECUTION_CONTEXT
runtime_provider.requester = requester(
monkeypatch,
lambda request: stream(
[
{'type': 'response.output_text.delta', 'delta': 'first'},
{'type': 'response.completed', 'response': {'status': 'completed', 'output': []}},
]
),
)
runtime_llm_model.model_entity.extra_args = {}
result = runtime_provider.invoke_llm_stream(
None,
runtime_llm_model,
[],
extra_args={'request_policy': {'total_timeout': 0.05}},
execution_context=TEST_EXECUTION_CONTEXT,
)
assert (await anext(result)).content == 'first'
await asyncio.sleep(0.07)
with pytest.raises(RequesterError) as caught:
await anext(result)
assert caught.value.phase == 'total'
@@ -0,0 +1,285 @@
import asyncio
import json
import threading
from types import SimpleNamespace
from unittest.mock import Mock
import httpx
import pytest
from aiohttp import web
from langbot.pkg.provider.modelmgr.http_pool import _CompletedSSEStream, RequesterHTTPPool
from langbot.pkg.provider.modelmgr.errors import RequesterError
from langbot.pkg.provider.modelmgr.requesters.litellmchat import LiteLLMRequester
from langbot.pkg.provider.modelmgr.request_policy import CallState, RequestPolicy, current_call
from tests.unit_tests.provider.test_reasoning_control import _runtime_model
from scripts.llm_latency_probe import SSEMeter, cache_tokens
def test_request_timing_uses_high_resolution_monotonic_clock(monkeypatch):
from langbot.pkg.provider.modelmgr import request_policy
ticks = iter([100.0, 100.0004])
monkeypatch.setattr(request_policy.time, 'perf_counter', lambda: next(ticks))
state = CallState(RequestPolicy())
state.mark('request_prepared_ms')
assert state.metrics['request_prepared_ms'] == 0.4
def test_meter_accepts_split_utf8_and_usage_without_content():
meter = SSEMeter()
event = {'choices': [{'delta': {'content': '你好'}}]}
data = ('data: ' + json.dumps(event, ensure_ascii=False) + '\r\n\r\n').encode()
for byte in data:
meter.feed(bytes([byte]), 1)
meter.feed(b'data: {"choices":[],"usage":{"prompt_tokens_details":{"cached_tokens":12}}}\n\n', 2)
meter.feed(b'data: [DONE]\n\n', 3)
assert meter.first_content == 1 and meter.content_chars == 2
assert cache_tokens(meter.usage) == 12 and meter.done
assert cache_tokens(None) is None
@pytest.mark.asyncio
@pytest.mark.parametrize('provider,name', [('openai', 'gpt-4o'), ('openrouter', 'openai/gpt-4o')])
@pytest.mark.parametrize('drain', [False, True])
async def test_real_stream_reuses_socket_and_preserves_late_usage(monkeypatch, provider, name, drain):
monkeypatch.setenv('NO_PROXY', '127.0.0.1,localhost')
sockets = []
async def handle(request):
await request.read()
sockets.append(request.transport)
response = web.StreamResponse(headers={'Content-Type': 'text/event-stream'})
await response.prepare(request)
events = [
{'choices': [{'index': 0, 'delta': {'role': 'assistant', 'content': 'hello'}, 'finish_reason': None}]},
{'choices': [{'index': 0, 'delta': {}, 'finish_reason': 'stop'}]},
{
'choices': [],
'usage': {
'prompt_tokens': 14,
'completion_tokens': 1,
'total_tokens': 15,
'prompt_tokens_details': {'cached_tokens': 12},
},
},
]
for event in events:
await response.write(
(
'data: '
+ json.dumps(
{
'id': 'r',
'object': 'chat.completion.chunk',
'created': 1,
'model': name,
**event,
}
)
+ '\n\n'
).encode()
)
await asyncio.sleep(0.02)
await response.write(b'data: [DO')
await response.write(b'NE]\n\n')
await response.write_eof()
return response
app = web.Application()
app.router.add_post('/chat/completions', handle)
runner = web.AppRunner(app)
await runner.setup()
site = web.TCPSite(runner, '127.0.0.1', 0)
await site.start()
adapter = LiteLLMRequester(
SimpleNamespace(logger=Mock()),
{
'custom_llm_provider': provider,
'base_url': f'http://127.0.0.1:{site._server.sockets[0].getsockname()[1]}',
},
)
await adapter.initialize()
adapter._http_pool.drain_sse = drain
model = _runtime_model(adapter, name=name)
model.provider.token_mgr.get_token = lambda: 'test'
try:
for _ in range(2):
state = CallState(RequestPolicy())
token = current_call.set(state)
try:
chunks = [chunk async for chunk in adapter.invoke_llm_stream(None, model, [])]
assert ''.join(chunk.content or '' for chunk in chunks) == 'hello'
assert state.usage['prompt_tokens_details']['cached_tokens'] == 12
finally:
current_call.reset(token)
# OpenRouter's generic HTTP adapter already consumes EOF; the OpenAI
# SDK path is the regression that needs terminal draining.
assert (sockets[0] is sockets[1]) == (drain or provider == 'openrouter')
finally:
await adapter.aclose()
await runner.cleanup()
@pytest.mark.asyncio
async def test_terminal_drain_is_bounded_but_early_close_does_not_drain():
class Stream(httpx.AsyncByteStream):
def __init__(self, terminal):
self.terminal = terminal
self.tail_read = False
self.closed = False
async def __aiter__(self):
yield b'data: [DONE]\n\n' if self.terminal else b'data: {"choices":[]}\n\n'
self.tail_read = True
await asyncio.Event().wait()
async def aclose(self):
self.closed = True
for terminal in (False, True):
raw = Stream(terminal)
wrapped = _CompletedSSEStream(raw)
iterator = wrapped.__aiter__()
async with asyncio.timeout(1):
await anext(iterator)
await wrapped.aclose()
await iterator.aclose()
assert raw.closed and raw.tail_read == terminal
@pytest.mark.asyncio
async def test_cancellation_during_terminal_drain_is_not_swallowed():
reading = asyncio.Event()
class Stream(httpx.AsyncByteStream):
async def __aiter__(self):
yield b'data: [DONE]\n\n'
reading.set()
await asyncio.Event().wait()
async def aclose(self):
pass
wrapped = _CompletedSSEStream(Stream())
iterator = wrapped.__aiter__()
pending = asyncio.create_task(anext(iterator))
await reading.wait()
pending.cancel()
with pytest.raises(asyncio.CancelledError):
await pending
await iterator.aclose()
await wrapped.aclose()
@pytest.mark.asyncio
@pytest.mark.parametrize('retire', [False, True])
async def test_concurrent_pool_initialization_loads_tls_once_off_loop(monkeypatch, retire):
context = httpx.create_ssl_context()
entered, release = threading.Event(), threading.Event()
main_thread = threading.get_ident()
calls = []
def load():
assert threading.get_ident() != main_thread
calls.append(True)
entered.set()
assert release.wait(2)
return context
monkeypatch.setattr(httpx, 'create_ssl_context', load)
pool = RequesterHTTPPool()
async def lease():
async with pool.lease() as client:
return client
first, second = asyncio.create_task(lease()), asyncio.create_task(lease())
await asyncio.to_thread(entered.wait, 2)
if retire:
await pool.aclose()
release.set()
results = await asyncio.gather(first, second, return_exceptions=True)
assert len(calls) == 1
if retire:
assert all(isinstance(result, RequesterError) for result in results)
assert pool.client is None
else:
assert results[0] is results[1] is pool.client
assert not pool.client.is_closed
await pool.aclose()
@pytest.mark.asyncio
async def test_cancelling_tls_initialization_leaks_no_http_client(monkeypatch):
context = httpx.create_ssl_context()
entered, release = threading.Event(), threading.Event()
def load():
entered.set()
assert release.wait(2)
return context
monkeypatch.setattr(httpx, 'create_ssl_context', load)
pool = RequesterHTTPPool()
async def lease():
async with pool.lease():
pytest.fail('Cancelled initializer must not publish a client')
pending = asyncio.create_task(lease())
await asyncio.to_thread(entered.wait, 2)
pending.cancel()
try:
with pytest.raises(asyncio.CancelledError):
await pending
await pool.aclose()
assert pool.client is None and pool.active == 0
finally:
release.set()
@pytest.mark.asyncio
async def test_real_openai_stream_close_disconnects_upstream(monkeypatch):
monkeypatch.setenv('NO_PROXY', '127.0.0.1,localhost')
disconnected = asyncio.Event()
async def handle(request):
await request.read()
response = web.StreamResponse(headers={'Content-Type': 'text/event-stream'})
await response.prepare(request)
await response.write(
b'data: {"id":"r","object":"chat.completion.chunk","created":1,"model":"gpt-4o","choices":[{"index":0,"delta":{"content":"hello"},"finish_reason":null}]}\n\n'
)
for _ in range(100):
if request.transport is None or request.transport.is_closing():
disconnected.set()
break
await asyncio.sleep(0.02)
return response
app = web.Application()
app.router.add_post('/chat/completions', handle)
runner = web.AppRunner(app)
await runner.setup()
site = web.TCPSite(runner, '127.0.0.1', 0)
await site.start()
adapter = LiteLLMRequester(
SimpleNamespace(logger=Mock()),
{
'custom_llm_provider': 'openai',
'base_url': f'http://127.0.0.1:{site._server.sockets[0].getsockname()[1]}',
},
)
await adapter.initialize()
model = _runtime_model(adapter, name='gpt-4o')
model.provider.token_mgr.get_token = lambda: 'test'
stream = adapter.invoke_llm_stream(None, model, [])
try:
assert (await anext(stream)).content == 'hello'
await stream.aclose()
await asyncio.wait_for(disconnected.wait(), 3)
finally:
await stream.aclose()
await adapter.aclose()
await runner.cleanup()
@@ -0,0 +1,516 @@
"""Connection ownership, preparation reuse and provider-specific request policy."""
import asyncio
import copy
import threading
import json
from pathlib import Path
from collections import OrderedDict
from types import SimpleNamespace
from unittest.mock import AsyncMock, Mock, patch
import httpx
import pytest
import yaml
from aiohttp import web
from langbot.pkg.provider.modelmgr import reasoning
from langbot.pkg.provider.modelmgr.errors import RequesterError
from langbot.pkg.provider.modelmgr.http_pool import RequesterHTTPPool
from langbot.pkg.provider.modelmgr.prompt_caching import apply_prompt_caching
from langbot.pkg.provider.modelmgr.request_policy import CallState, RequestPolicy, current_call
from langbot.pkg.provider.modelmgr.requesters.litellmchat import LiteLLMRequester
from langbot.pkg.provider.modelmgr.requesters.litellmchat import _LeasedHTTPHandler
from langbot.pkg.provider.modelmgr.requesters import codex
from langbot_plugin.api.entities.builtin.provider.message import Message
from tests.unit_tests.provider.test_reasoning_control import _runtime_model, _requester
@pytest.mark.asyncio
async def test_pool_retirement_waits_for_stream_lease():
pool = RequesterHTTPPool()
async with pool.lease() as first:
async with pool.lease() as second:
assert first is second
await pool.aclose()
assert not first.is_closed
with pytest.raises(RequesterError, match='retired'):
async with pool.lease():
pass
assert not first.is_closed
assert first.is_closed
await pool.aclose()
@pytest.mark.asyncio
@pytest.mark.parametrize('provider,name', [('openai', 'gpt-4o'), ('openrouter', 'openai/gpt-4o')])
async def test_real_http_reuses_socket_with_rotating_key_and_timeout(monkeypatch, provider, name):
monkeypatch.setenv('NO_PROXY', '127.0.0.1,localhost')
sockets, headers, bodies = [], [], []
async def handle(request):
sockets.append(request.transport)
headers.append(request.headers['Authorization'])
bodies.append(await request.json())
return web.json_response(
{
'id': 'r',
'object': 'chat.completion',
'created': 1,
'model': 'gpt-4o',
'choices': [{'index': 0, 'message': {'role': 'assistant', 'content': 'ok'}, 'finish_reason': 'stop'}],
'usage': {'prompt_tokens': 1, 'completion_tokens': 1, 'total_tokens': 2},
}
)
app = web.Application()
app.router.add_post('/v1/chat/completions', handle)
runner = web.AppRunner(app)
await runner.setup()
site = web.TCPSite(runner, '127.0.0.1', 0)
await site.start()
port = site._server.sockets[0].getsockname()[1]
adapter = LiteLLMRequester(Mock(), {'custom_llm_provider': provider, 'base_url': f'http://127.0.0.1:{port}/v1'})
await adapter.initialize()
model = _runtime_model(adapter, name=name)
model.provider.token_mgr.get_token = Mock(side_effect=['first-key', 'second-key'])
try:
for seconds in (10, 20):
state = CallState(RequestPolicy(read_timeout=seconds), 'lb-scoped')
token = current_call.set(state)
try:
message, _ = await adapter.invoke_llm(
None,
model,
[Message(role='user', content='Hi')],
extra_args={'prompt_caching': {'mode': 'openai', 'retention': 'in_memory'}},
)
assert message.content == 'ok'
finally:
current_call.reset(token)
assert sockets[0] is sockets[1]
assert headers == ['Bearer first-key', 'Bearer second-key']
assert bodies[0]['prompt_cache_key'] == bodies[1]['prompt_cache_key']
assert bodies[0]['prompt_cache_retention'] == 'in_memory'
assert all('prompt_caching' not in body and 'reasoning_config' not in body for body in bodies)
finally:
await adapter.aclose()
await runner.cleanup()
@pytest.mark.asyncio
@pytest.mark.parametrize('cancel', [False, True])
async def test_stream_holds_pool_lease_until_consumer_close(cancel):
adapter = _requester('openai')
await adapter.initialize()
model = _runtime_model(adapter, name='gpt-4o')
model.provider.token_mgr.get_token = Mock(return_value='test')
closed = asyncio.Event()
async def raw():
try:
yield SimpleNamespace(
choices=[
SimpleNamespace(delta=SimpleNamespace(model_dump=lambda: {'content': 'hi'}), finish_reason=None)
]
)
await asyncio.Event().wait()
finally:
closed.set()
with patch('langbot.pkg.provider.modelmgr.requesters.litellmchat.acompletion', new=AsyncMock(return_value=raw())):
stream = adapter.invoke_llm_stream(None, model, [])
await anext(stream)
client = adapter._http_pool.client
await adapter.aclose()
assert not client.is_closed
if cancel:
pending = asyncio.create_task(anext(stream))
await asyncio.sleep(0)
pending.cancel()
with pytest.raises(asyncio.CancelledError):
await pending
else:
await stream.aclose()
assert closed.is_set() and client.is_closed
@pytest.mark.asyncio
async def test_preparation_reused_only_within_call_and_is_not_mutated():
adapter = _requester('openai')
adapter.ap.tool_mgr = SimpleNamespace(
generate_tools_for_openai=AsyncMock(
return_value=[{'type': 'function', 'function': {'name': 'tool', 'parameters': {'type': 'object'}}}]
)
)
model = _runtime_model(adapter, name='gpt-4o')
model.provider.token_mgr.get_token = Mock(side_effect=['key-1', 'key-2', 'key-3'])
messages, funcs = [Message(role='user', content='original')], [object()]
state = CallState(RequestPolicy(retries=1))
with patch.object(adapter, '_convert_messages', wraps=adapter._convert_messages) as convert:
token = current_call.set(state)
try:
first = await adapter._build_completion_args(model, messages, funcs)
first['messages'][0]['content'] = 'mutated'
first['tools'][0]['function']['name'] = 'mutated'
second = await adapter._build_completion_args(model, messages, funcs)
finally:
current_call.reset(token)
assert convert.call_count == 1
assert adapter.ap.tool_mgr.generate_tools_for_openai.await_count == 1
assert second['messages'][0]['content'] == 'original'
assert second['tools'][0]['function']['name'] == 'tool'
assert second['api_key'] == 'key-2'
await adapter._build_completion_args(model, messages, funcs)
assert convert.call_count == 2
assert messages[0].content == 'original'
@pytest.mark.asyncio
async def test_token_counter_runs_off_event_loop():
adapter = _requester('openai')
model = _runtime_model(adapter, name='gpt-4o')
model.provider.token_mgr.get_token = Mock(return_value='test')
main_thread = threading.get_ident()
def count(**kwargs):
assert threading.get_ident() != main_thread
return 12
with patch('litellm.token_counter', new=count):
assert await adapter.count_tokens(model, []) == 12
def test_metadata_cache_is_bounded_and_returns_copies():
adapter = _requester('openai')
compute = Mock(return_value={'levels': ['low']})
first = adapter._cached_metadata('model', compute)
first['levels'].append('bad')
assert adapter._cached_metadata('model', compute) == {'levels': ['low']}
assert compute.call_count == 1
for index in range(150):
adapter._cached_metadata(index, lambda: index)
assert len(adapter._metadata_cache) == 128
@pytest.mark.parametrize(
'config',
[
{'budget_tokens': True},
{'budget_tokens': 0},
{'budget_tokens': -1},
{'budget_tokens': 1.5},
{'level': 'high', 'budget_tokens': 1024},
{'level': 'disabled', 'budget_tokens': 1024},
],
)
def test_invalid_reasoning_budget(config):
with pytest.raises(ValueError):
reasoning.normalize_reasoning_config(config)
@pytest.mark.asyncio
@pytest.mark.parametrize(
'provider,name,budget,expected',
[
('anthropic', 'claude-sonnet-4-5', 2048, {'thinking': {'type': 'enabled', 'budget_tokens': 2048}}),
('openai', 'qwen3-32b', 2048, {'extra_body': {'enable_thinking': True, 'thinking_budget': 2048}}),
('gemini', 'gemini-2.5-flash', 2048, {'thinking': {'type': 'enabled', 'budget_tokens': 2048}}),
('openrouter', 'anthropic/claude-sonnet-4.5', 2048, {'extra_body': {'reasoning': {'max_tokens': 2048}}}),
],
)
async def test_budget_mapping_through_existing_extra_args(provider, name, budget, expected):
adapter = _requester(provider)
model = _runtime_model(adapter, name=name)
model.provider.token_mgr.get_token = Mock(return_value='test')
args = await adapter._build_completion_args(
model, [], extra_args={'reasoning_config': {'budget_tokens': budget}, 'max_tokens': 4096}
)
for key, value in expected.items():
assert args[key] == value
assert 'reasoning_config' not in args
assert model.reasoning_config_override is None
@pytest.mark.asyncio
@pytest.mark.parametrize(
'provider,name,budget',
[
('openai', 'gpt-5', 2048),
('anthropic', 'claude-opus-4-7', 2048),
('anthropic', 'claude-sonnet-4-5', 512),
('gemini', 'gemini-2.5-flash', 30000),
('gemini', 'gemini-2.5-flash-lite', 128),
],
)
async def test_budget_not_silently_dropped_on_unsupported_model(provider, name, budget):
adapter = _requester(provider)
model = _runtime_model(adapter, name=name)
model.provider.token_mgr.get_token = Mock(return_value='test')
with pytest.raises(RequesterError, match='budget'):
await adapter._build_completion_args(model, [], extra_args={'reasoning_config': {'budget_tokens': budget}})
def test_runner_level_replaces_call_and_model_budget():
adapter = _requester('anthropic')
model = _runtime_model(adapter, name='claude-sonnet-4-5')
model.model_entity.reasoning_config = {'budget_tokens': 2048}
call = {'reasoning_config': {'budget_tokens': 4096}}
assert reasoning.resolve_reasoning_config(model, call)['budget_tokens'] == 4096
model.reasoning_config_override = {'level': 'low'}
assert reasoning.resolve_reasoning_config(model, call) == {'level': 'low'}
def test_anthropic_cache_boundary_stable_and_input_unchanged():
messages = [{'role': 'system', 'content': 'stable instructions'}, {'role': 'user', 'content': 'first'}]
original = copy.deepcopy(messages)
args = {'messages': messages, 'prompt_caching': {'mode': 'anthropic', 'ttl': '1h'}}
apply_prompt_caching(args, None, provider='anthropic', family='anthropic')
prefix = args['messages'][0]
assert prefix['content'][-1]['cache_control'] == {'type': 'ephemeral', 'ttl': '1h'}
assert messages == original
assert 'cache_control' not in args['messages'][1]
args['messages'].append({'role': 'user', 'content': 'second'})
args['prompt_caching'] = {'mode': 'anthropic'}
apply_prompt_caching(args, None, provider='anthropic', family='anthropic')
assert args['messages'][0] == prefix
def test_explicit_cache_settings_win_and_default_does_nothing():
args = {
'model': 'openai/gpt-4o',
'extra_body': {'prompt_cache_key': 'explicit', 'prompt_cache_retention': '24h'},
'prompt_caching': {'mode': 'openai', 'retention': 'in_memory'},
}
apply_prompt_caching(args, CallState(RequestPolicy(), 'lb-scoped'), provider='openai', family='openai')
assert 'prompt_cache_key' not in args and 'prompt_cache_retention' not in args
args = {'messages': [{'role': 'system', 'content': 'same'}], 'prompt_caching': {'mode': 'provider_default'}}
apply_prompt_caching(args, None, provider='anthropic', family='anthropic')
assert args == {'messages': [{'role': 'system', 'content': 'same'}]}
def test_native_cache_options_bypass_model_filter_without_legacy_hints():
args = {
'model': 'openai/gpt-test',
'prompt_cache_options': {'mode': 'implicit', 'ttl': '30m'},
'prompt_caching': {'mode': 'openai', 'retention': '24h'},
}
apply_prompt_caching(args, CallState(RequestPolicy(), 'lb-scoped'), provider='openai', family='openai')
assert args == {
'model': 'openai/gpt-test',
'extra_body': {'prompt_cache_options': {'mode': 'implicit', 'ttl': '30m'}},
}
@pytest.mark.parametrize(
'config',
[
{'mode': 'typo'},
{'mode': 'anthropic', 'ttl': '24h'},
{'mode': 'openai', 'retention': 'forever'},
{'mode': 'provider_default', 'ttl': '1h'},
],
)
def test_invalid_cache_config(config):
with pytest.raises(RequesterError):
apply_prompt_caching(
{'model': 'test', 'messages': [], 'prompt_caching': config}, None, provider='openai', family='openai'
)
@pytest.mark.asyncio
async def test_gateway_transport_never_retries_or_closes_borrowed_client():
calls = 0
async def fail(request):
nonlocal calls
calls += 1
raise httpx.ConnectError('unavailable', request=request)
async with httpx.AsyncClient(transport=httpx.MockTransport(fail)) as client:
handler = _LeasedHTTPHandler(client, 10)
with pytest.raises(httpx.ConnectError):
await handler.post('https://example.com', json={})
await handler.close()
assert calls == 1 and not client.is_closed
@pytest.mark.asyncio
@pytest.mark.parametrize('provider', ['anthropic', 'openrouter', 'openrouter-metadata'])
async def test_anthropic_cache_and_budget_reach_upstream(provider):
bodies = []
async def handle(request):
bodies.append(json.loads(request.content))
if provider == 'anthropic':
payload = {
'id': 'msg_test',
'type': 'message',
'role': 'assistant',
'model': 'claude-sonnet-4-5',
'content': [{'type': 'text', 'text': 'ok'}],
'stop_reason': 'end_turn',
'usage': {'input_tokens': 1, 'output_tokens': 1, 'cache_read_input_tokens': 50},
}
else:
payload = {
'id': 'r',
'object': 'chat.completion',
'created': 1,
'model': 'anthropic/claude-sonnet-4.5',
'choices': [{'index': 0, 'message': {'role': 'assistant', 'content': 'ok'}, 'finish_reason': 'stop'}],
'usage': {
'prompt_tokens': 51,
'completion_tokens': 1,
'total_tokens': 52,
'prompt_tokens_details': {'cached_tokens': 50},
},
}
return httpx.Response(200, json=payload)
adapter = _requester('openai' if provider == 'openrouter-metadata' else provider)
if provider == 'openrouter-metadata':
metadata = yaml.safe_load(
(
Path(__file__).parents[3] / 'src/langbot/pkg/provider/modelmgr/requesters/openrouterchatcmpl.yaml'
).read_text(encoding='utf-8')
)
adapter.requester_cfg.update(
requester_name=metadata['metadata']['name'], custom_llm_provider=metadata['spec']['litellm_provider']
)
name = 'claude-sonnet-4-5' if provider == 'anthropic' else 'anthropic/claude-sonnet-4.5'
model = _runtime_model(adapter, name=name)
model.provider.token_mgr.get_token = Mock(return_value='test')
async with httpx.AsyncClient(transport=httpx.MockTransport(handle)) as client:
result, usage = await adapter.invoke_llm(
None,
model,
[
Message(role='system', content='stable'),
Message(role='user', content='hello'),
],
extra_args={
'client': _LeasedHTTPHandler(client, 10),
'reasoning_config': {'budget_tokens': 2048},
'max_tokens': 4096,
'prompt_caching': {'mode': 'anthropic', 'ttl': '1h'},
},
)
assert result.content == 'ok'
assert usage
body = bodies[0]
if provider == 'anthropic':
assert body['system'][-1]['cache_control'] == {'type': 'ephemeral', 'ttl': '1h'}
assert body['thinking']['budget_tokens'] == 2048
else:
assert body['messages'][0]['content'][-1]['cache_control'] == {'type': 'ephemeral', 'ttl': '1h'}
assert body['reasoning']['max_tokens'] == 2048
assert 'reasoning_config' not in body and 'prompt_caching' not in body
@pytest.mark.asyncio
async def test_codex_reuses_real_connection(monkeypatch):
monkeypatch.setenv('NO_PROXY', '127.0.0.1,localhost')
sockets = []
async def handle(request):
sockets.append(request.transport)
await request.read()
events = [
{'type': 'response.output_text.delta', 'delta': 'ok'},
{'type': 'response.completed', 'response': {'id': 'r', 'status': 'completed', 'output': []}},
]
return web.Response(
text=''.join('data: ' + json.dumps(event) + '\n\n' for event in events), content_type='text/event-stream'
)
app = web.Application()
app.router.add_post('/responses', handle)
runner = web.AppRunner(app)
await runner.setup()
site = web.TCPSite(runner, '127.0.0.1', 0)
await site.start()
monkeypatch.setattr(codex, 'BASE_URL', f'http://127.0.0.1:{site._server.sockets[0].getsockname()[1]}')
adapter = object.__new__(codex.CodexRequester)
adapter.workspace, adapter.provider = 'w', 'p'
adapter._replay = OrderedDict()
adapter._http_pool = RequesterHTTPPool()
adapter.auth = SimpleNamespace(
access=AsyncMock(
return_value={
'access_token': 'test',
'account_id': 'a',
'connection_id': 'c',
}
)
)
model = SimpleNamespace(model_entity=SimpleNamespace(name='test', extra_args={}, reasoning_config=None))
try:
for _ in range(2):
message, _ = await adapter.invoke_llm(None, model, [])
assert message.content == 'ok'
assert sockets[0] is sockets[1]
finally:
await adapter.aclose()
await runner.cleanup()
@pytest.mark.asyncio
async def test_explicit_client_and_redirected_provider_are_preserved():
adapter = _requester('openai')
await adapter.initialize()
explicit = object()
for args in ({'model': 'openai/gpt-4o', 'client': explicit}, {'model': 'anthropic/claude-sonnet-4-5'}):
async with adapter._client_args(args) as result:
assert result is args
assert adapter._http_pool.client is None
await adapter.aclose()
@pytest.mark.asyncio
async def test_codex_terminal_tail_is_bounded_and_releases_stream():
closed = asyncio.Event()
class HangingTail(httpx.AsyncByteStream):
async def __aiter__(self):
yield b'data: {"type":"response.completed"}\n\n'
await asyncio.Event().wait()
async def aclose(self):
closed.set()
async with httpx.AsyncClient(
transport=httpx.MockTransport(lambda request: httpx.Response(200, stream=HangingTail()))
) as client:
async with client.stream('GET', 'https://example.com') as response:
events = [event async for event in codex.sse_events(response)]
assert events == [{'type': 'response.completed'}]
assert closed.is_set()
@pytest.mark.asyncio
async def test_custom_tls_keeps_litellm_transport(monkeypatch):
import litellm
adapter = _requester('openai')
await adapter.initialize()
monkeypatch.setattr(litellm, 'ssl_verify', '/custom/ca.pem')
args = {'model': 'openai/gpt-4o'}
async with adapter._client_args(args) as result:
assert result is args
assert adapter._http_pool.client is None
await adapter.aclose()
@pytest.mark.asyncio
async def test_pooled_sdk_preserves_environment_fallbacks(monkeypatch):
monkeypatch.setenv('OPENAI_BASE_URL', 'https://gateway.example/v1')
monkeypatch.setenv('OPENAI_API_KEY', 'env-test-key')
adapter = _requester('openai')
await adapter.initialize()
try:
async with adapter._client_args({'model': 'openai/gpt-4o', 'api_key': ''}) as args:
assert str(args['client'].base_url) == 'https://gateway.example/v1/'
assert args['client'].api_key == 'env-test-key'
finally:
await adapter.aclose()
@@ -0,0 +1,549 @@
import asyncio
from types import SimpleNamespace
from unittest.mock import AsyncMock, Mock, patch
import httpx
import litellm
import pytest
from langbot.pkg.provider.modelmgr.errors import RequesterError
from langbot.pkg.provider.modelmgr.request_policy import (
CallState,
RequestPolicy,
current_call,
error_metadata,
opaque_conversation_id,
)
from langbot.pkg.provider.modelmgr.requesters.litellmchat import LiteLLMRequester
from langbot_plugin.api.entities.builtin.provider.message import Message, MessageChunk
from tests.unit_tests.provider.conftest import TEST_EXECUTION_CONTEXT
from tests.unit_tests.provider.test_litellmchat import MockRuntimeModel
def test_policy_precedence_zero_and_no_mutation():
model = {'num_retries': 3, 'request_policy': {'read_timeout': 42}}
policy = RequestPolicy.resolve({'request_policy': {'retries': 2}}, model, {'num_retries': 0})
assert policy.retries == 0
assert policy.read_timeout == 42
assert model['num_retries'] == 3
@pytest.mark.parametrize(
'policy',
[
{'retries': True},
{'retries': -1},
{'retries': 6},
{'read_timeout': 0},
{'total_timeout': float('nan')},
{'connect_timeout': float('inf')},
{'attribution': 'false'},
{'conversation_header': 'authorization'},
{'typo': 1},
],
)
def test_invalid_policy(policy):
with pytest.raises(RequesterError, match='模型请求失败'):
RequestPolicy.resolve({'request_policy': policy})
def test_retry_metadata_does_not_include_upstream_secrets():
response = httpx.Response(429, headers={'retry-after': '2', 'x-request-id': 'req-123'})
meta = error_metadata(SimpleNamespace(status_code=429, response=response))
assert meta == {'status_code': 429, 'retry_after': 2, 'request_id': 'req-123'}
adapter = LiteLLMRequester(Mock(), {})
exc = litellm.RateLimitError('secret-key-and-prompt', llm_provider='openai', model='test', response=response)
with pytest.raises(RequesterError) as caught:
adapter._handle_litellm_error(exc)
assert caught.value.retryable
assert caught.value.request_id == 'req-123'
assert 'secret-key' not in str(caught.value)
def test_quota_is_not_retryable():
adapter = LiteLLMRequester(Mock(), {})
exc = litellm.RateLimitError('quota', llm_provider='openai', model='test')
exc.body = {'error': {'code': 'insufficient_quota'}}
with pytest.raises(RequesterError) as caught:
adapter._handle_litellm_error(exc)
assert caught.value.code == 'quota_exceeded'
assert not caught.value.retryable
@pytest.mark.asyncio
async def test_headers_and_sdk_retry_ownership():
adapter = LiteLLMRequester(Mock(), {'base_url': 'https://openrouter.ai/api/v1'})
state = CallState(RequestPolicy(retries=2), 'lb-opaque')
token = current_call.set(state)
try:
args = await adapter._build_completion_args(
MockRuntimeModel(),
[],
extra_args={
'request_policy': {'retries': 2},
'num_retries': 2,
'extra_headers': {'x-openrouter-title': 'Custom app', 'x-other': 'keep'},
},
)
finally:
current_call.reset(token)
assert 'request_policy' not in args
assert args['num_retries'] == args['max_retries'] == 0
assert isinstance(args['timeout'], httpx.Timeout)
assert args['extra_headers'] == {
'x-openrouter-title': 'Custom app',
'x-other': 'keep',
'HTTP-Referer': 'https://langbot.app',
'x-session-id': 'lb-opaque',
}
@pytest.mark.asyncio
@pytest.mark.parametrize('url', ['https://gateway.example/v1', 'https://openrouter.ai.evil.example/v1'])
async def test_no_vendor_headers_on_unrelated_hosts(url):
adapter = LiteLLMRequester(Mock(), {'base_url': url})
args = await adapter._build_completion_args(MockRuntimeModel(), [])
assert not args.get('extra_headers')
assert args['num_retries'] == 0
def test_conversation_is_stable_and_scoped():
query = SimpleNamespace(session=None, launcher_type='person', launcher_id='123', bot_uuid='bot', pipeline_uuid='p')
first = opaque_conversation_id(query, TEST_EXECUTION_CONTEXT)
assert first == opaque_conversation_id(query, TEST_EXECUTION_CONTEXT)
query.bot_uuid = 'another-bot'
assert first != opaque_conversation_id(query, TEST_EXECUTION_CONTEXT)
assert opaque_conversation_id(None, TEST_EXECUTION_CONTEXT) is None
@pytest.mark.asyncio
async def test_runtime_retries_only_before_output(runtime_provider, runtime_llm_model):
provider = runtime_provider
attempts = 0
closed = []
async def stream(**kwargs):
nonlocal attempts
attempts += 1
try:
if attempts == 1:
raise RequesterError('transient', retryable=True)
yield MessageChunk(role='assistant', content='first')
raise RequesterError('after output', retryable=True)
finally:
closed.append(attempts)
provider.requester.invoke_llm_stream = stream
result = []
with pytest.raises(RequesterError, match='after output'):
async for chunk in provider.invoke_llm_stream(
None,
runtime_llm_model,
[],
extra_args={'request_policy': {'retries': 3, 'backoff_base': 0.001}},
execution_context=TEST_EXECUTION_CONTEXT,
):
result.append(chunk.content)
assert result == ['first']
assert attempts == 2
assert closed == [1, 2]
assert current_call.get() is None
@pytest.mark.asyncio
async def test_runtime_deadline_and_close(runtime_provider, runtime_llm_model):
closed = asyncio.Event()
async def stream(**kwargs):
try:
await asyncio.Event().wait()
yield MessageChunk(role='assistant', content='never')
finally:
closed.set()
runtime_provider.requester.invoke_llm_stream = stream
with pytest.raises(RequesterError) as caught:
async for _ in runtime_provider.invoke_llm_stream(
None,
runtime_llm_model,
[],
extra_args={'request_policy': {'first_event_timeout': 0.02}},
execution_context=TEST_EXECUTION_CONTEXT,
):
pass
assert caught.value.phase == 'first_event'
assert closed.is_set()
@pytest.mark.asyncio
async def test_retry_after_cannot_extend_total_budget():
state = CallState(RequestPolicy(retries=5, total_timeout=0.1))
state.metrics['attempts'] = 1
assert not await state.retry(RequesterError('busy', retryable=True, retry_after=30))
@pytest.mark.asyncio
async def test_cancellation_is_not_retried(runtime_provider, runtime_llm_model):
invoke = AsyncMock(side_effect=asyncio.CancelledError)
runtime_provider.requester.invoke_llm = invoke
with pytest.raises(asyncio.CancelledError):
await runtime_provider.invoke_llm(
None,
runtime_llm_model,
[],
extra_args={'request_policy': {'retries': 3}},
execution_context=TEST_EXECUTION_CONTEXT,
)
assert invoke.await_count == 1
@pytest.mark.asyncio
async def test_complete_usage_and_timings_persist(runtime_provider, runtime_llm_model):
from langbot_plugin.api.entities.builtin.pipeline.query import Query
query = Query.model_construct(
query_id=1, launcher_type='person', launcher_id=1, variables={}, bot_uuid='bot', pipeline_uuid='p', session=None
)
object.__setattr__(query, '_execution_context', TEST_EXECUTION_CONTEXT)
usage = {
'prompt_tokens': 20,
'completion_tokens': 10,
'total_tokens': 30,
'prompt_tokens_details': {'cached_tokens': 15},
'completion_tokens_details': {'reasoning_tokens': 8},
'cost': 0.01,
}
runtime_provider.requester.invoke_llm = AsyncMock(return_value=(Message(role='assistant', content='ok'), usage))
with patch(
'langbot.pkg.pipeline.monitoring_helper.MonitoringHelper.record_llm_call', new_callable=AsyncMock
) as record:
result = await runtime_provider.invoke_llm(query, runtime_llm_model, [])
assert result.content == 'ok'
details = record.call_args.kwargs['details']
assert details['usage'] == usage
assert details['timing']['attempts'] == 1
assert details['timing']['total_ms'] >= details['timing']['response_completed_ms']
assert query.variables['_llm_usage'] == usage
@pytest.mark.asyncio
async def test_hidden_reasoning_marks_raw_event_and_usage():
state = CallState(RequestPolicy())
adapter = LiteLLMRequester(Mock(), {})
async def raw():
yield SimpleNamespace(
choices=[
SimpleNamespace(
delta=SimpleNamespace(model_dump=lambda: {'reasoning_content': 'hidden'}), finish_reason=None
)
]
)
yield SimpleNamespace(choices=[], usage={'prompt_tokens': 3, 'completion_tokens': 2, 'total_tokens': 5})
token = current_call.set(state)
try:
with patch(
'langbot.pkg.provider.modelmgr.requesters.litellmchat.acompletion', new=AsyncMock(return_value=raw())
):
chunks = [c async for c in adapter.invoke_llm_stream(None, MockRuntimeModel(), [], remove_think=True)]
finally:
current_call.reset(token)
assert state.raw_started
assert 'first_event_ms' in state.metrics and 'first_reasoning_ms' in state.metrics
assert 'first_content_ms' not in state.metrics
assert state.usage['total_tokens'] == 5
assert not any(c.content for c in chunks)
@pytest.mark.asyncio
async def test_real_http_retry_count_and_request_body(runtime_provider, runtime_llm_model):
"""Exercise the pinned LiteLLM/OpenAI SDK against a local HTTP server."""
from aiohttp import web
received = []
async def handler(request):
received.append(await request.json())
if len(received) == 1:
return web.json_response({'error': {'message': 'busy', 'type': 'server_error'}}, status=503)
return web.json_response(
{
'id': 'chatcmpl-local',
'object': 'chat.completion',
'created': 1,
'model': 'gpt-4o',
'choices': [{'index': 0, 'message': {'role': 'assistant', 'content': 'ok'}, 'finish_reason': 'stop'}],
'usage': {'prompt_tokens': 4, 'completion_tokens': 1, 'total_tokens': 5},
}
)
app = web.Application()
app.router.add_post('/v1/chat/completions', handler)
runner = web.AppRunner(app)
await runner.setup()
site = web.TCPSite(runner, '127.0.0.1', 0)
await site.start()
port = site._server.sockets[0].getsockname()[1]
runtime_provider.requester = LiteLLMRequester(
Mock(),
{
'base_url': f'http://127.0.0.1:{port}/v1',
'custom_llm_provider': 'openai',
},
)
runtime_llm_model.model_entity.name = 'gpt-4o'
runtime_llm_model.model_entity.extra_args = {}
try:
with pytest.raises(RequesterError):
await runtime_provider.invoke_llm(
None,
runtime_llm_model,
[Message(role='user', content='hello')],
extra_args={'num_retries': 0},
execution_context=TEST_EXECUTION_CONTEXT,
)
assert len(received) == 1
received.clear()
result = await runtime_provider.invoke_llm(
None,
runtime_llm_model,
[Message(role='user', content='hello')],
extra_args={'request_policy': {'retries': 1, 'backoff_base': 0.001}},
execution_context=TEST_EXECUTION_CONTEXT,
)
assert result.content == 'ok'
assert len(received) == 2
assert all('request_policy' not in body and 'num_retries' not in body for body in received)
finally:
await runner.cleanup()
@pytest.mark.asyncio
async def test_stream_idle_timeout_after_output(runtime_provider, runtime_llm_model):
async def stream(**kwargs):
yield MessageChunk(role='assistant', content='first')
await asyncio.Event().wait()
runtime_provider.requester.invoke_llm_stream = stream
with pytest.raises(RequesterError) as caught:
async for _ in runtime_provider.invoke_llm_stream(
None,
runtime_llm_model,
[],
extra_args={'request_policy': {'stream_idle_timeout': 0.02}},
execution_context=TEST_EXECUTION_CONTEXT,
):
pass
assert caught.value.phase == 'stream_idle'
def test_details_migration_preserves_existing_records_and_is_idempotent():
import importlib
import sqlalchemy as sa
from alembic.migration import MigrationContext
from alembic.operations import Operations
migration = importlib.import_module('langbot.pkg.persistence.alembic.versions.0038_llm_call_details')
engine = sa.create_engine('sqlite://')
try:
with engine.begin() as connection:
connection.exec_driver_sql('CREATE TABLE monitoring_llm_calls (id TEXT PRIMARY KEY)')
connection.exec_driver_sql("INSERT INTO monitoring_llm_calls (id) VALUES ('old')")
with Operations.context(MigrationContext.configure(connection)):
migration.upgrade()
migration.upgrade()
assert connection.exec_driver_sql('SELECT id, details FROM monitoring_llm_calls').one() == ('old', None)
table = sa.Table('monitoring_llm_calls', sa.MetaData(), autoload_with=connection)
payload = {'usage': {'completion_tokens_details': {'reasoning_tokens': 8}}, 'timing': {'first_event_ms': 1}}
connection.execute(table.insert().values(id='new', details=payload))
assert connection.execute(sa.select(table.c.details).where(table.c.id == 'new')).scalar() == payload
finally:
engine.dispose()
@pytest.mark.asyncio
async def test_hidden_raw_events_reset_idle_not_first_visible_deadline(runtime_provider, runtime_llm_model):
adapter = LiteLLMRequester(Mock(), {})
runtime_provider.requester = adapter
runtime_llm_model.model_entity.extra_args = {}
async def raw():
for _ in range(5):
await asyncio.sleep(0.3)
yield SimpleNamespace(
choices=[
SimpleNamespace(delta=SimpleNamespace(model_dump=lambda: {'role': 'assistant'}), finish_reason=None)
]
)
yield SimpleNamespace(
choices=[
SimpleNamespace(delta=SimpleNamespace(model_dump=lambda: {'content': 'answer'}), finish_reason='stop')
]
)
with patch('langbot.pkg.provider.modelmgr.requesters.litellmchat.acompletion', new=AsyncMock(return_value=raw())):
chunks = [
chunk
async for chunk in runtime_provider.invoke_llm_stream(
None,
runtime_llm_model,
[],
extra_args={'request_policy': {'first_event_timeout': 1, 'stream_idle_timeout': 2}},
execution_context=TEST_EXECUTION_CONTEXT,
)
]
assert ''.join(chunk.content or '' for chunk in chunks) == 'answer'
@pytest.mark.asyncio
async def test_monitoring_service_persists_full_details():
from langbot.pkg.api.http.service.monitoring import MonitoringService
persistence = SimpleNamespace(execute_async=AsyncMock())
service = MonitoringService(SimpleNamespace(persistence_mgr=persistence))
details = {
'usage': {
'prompt_tokens': 4,
'completion_tokens': 3,
'total_tokens': 9,
'cache_read_input_tokens': 2,
'provider_extra': {'cost': 0.01},
},
'timing': {'attempts': 2},
'error': None,
}
await service.record_llm_call(
TEST_EXECUTION_CONTEXT,
bot_id='b',
bot_name='b',
pipeline_id='p',
pipeline_name='p',
session_id='s',
model_name='m',
input_tokens=4,
output_tokens=3,
duration=10,
details=details,
)
values = persistence.execute_async.call_args.args[0].compile().params
assert values['details'] == details
assert values['total_tokens'] == 9
def test_codex_usage_preserves_provider_fields():
from langbot.pkg.provider.modelmgr.requesters.codex import _usage
raw = {
'input_tokens': 4,
'output_tokens': 2,
'cost': 0.02,
'output_tokens_details': {'reasoning_tokens': 1},
'future_counter': 7,
}
normalized = _usage({'usage': raw})
assert all(normalized[key] == value for key, value in raw.items())
assert normalized['total_tokens'] == 6
@pytest.mark.asyncio
async def test_nonstream_uses_response_budget_not_first_event():
adapter = LiteLLMRequester(Mock(), {})
state = CallState(RequestPolicy(first_event_timeout=0.001, total_timeout=1))
async def completion(**kwargs):
await asyncio.sleep(0.02)
return 'complete'
token = current_call.set(state)
try:
with patch('langbot.pkg.provider.modelmgr.requesters.litellmchat.acompletion', new=completion):
assert await adapter._completion({'stream': False}) == 'complete'
finally:
current_call.reset(token)
@pytest.mark.asyncio
@pytest.mark.parametrize('cancel_read', [False, True])
async def test_litellm_stream_close_and_cancel_release_upstream(runtime_provider, runtime_llm_model, cancel_read):
adapter = LiteLLMRequester(Mock(), {})
runtime_provider.requester = adapter
runtime_llm_model.model_entity.extra_args = {}
closed = asyncio.Event()
reading = asyncio.Event()
async def raw():
try:
yield SimpleNamespace(
choices=[
SimpleNamespace(delta=SimpleNamespace(model_dump=lambda: {'content': 'first'}), finish_reason=None)
]
)
reading.set()
await asyncio.Event().wait()
finally:
closed.set()
completion = AsyncMock(return_value=raw())
with patch('langbot.pkg.provider.modelmgr.requesters.litellmchat.acompletion', new=completion):
stream = runtime_provider.invoke_llm_stream(
None,
runtime_llm_model,
[],
extra_args={'request_policy': {'retries': 3}},
execution_context=TEST_EXECUTION_CONTEXT,
)
assert (await anext(stream)).content == 'first'
if cancel_read:
task = asyncio.create_task(anext(stream))
await reading.wait()
task.cancel()
with pytest.raises(asyncio.CancelledError):
await task
else:
await stream.aclose()
assert closed.is_set()
assert completion.await_count == 1
assert current_call.get() is None
@pytest.mark.asyncio
async def test_cancel_during_backoff_does_not_start_another_attempt(runtime_provider, runtime_llm_model):
runtime_provider.requester.invoke_llm = AsyncMock(side_effect=RequesterError('busy', retryable=True))
sleeping = asyncio.Event()
async def backoff(delay):
sleeping.set()
await asyncio.Event().wait()
with patch('langbot.pkg.provider.modelmgr.request_policy.asyncio.sleep', new=backoff):
task = asyncio.create_task(
runtime_provider.invoke_llm(
None,
runtime_llm_model,
[],
extra_args={'request_policy': {'retries': 3}},
execution_context=TEST_EXECUTION_CONTEXT,
)
)
await sleeping.wait()
task.cancel()
with pytest.raises(asyncio.CancelledError):
await task
assert runtime_provider.requester.invoke_llm.await_count == 1
@pytest.mark.asyncio
async def test_legacy_none_chunk_is_ignored(runtime_provider, runtime_llm_model):
async def stream(**kwargs):
yield None
yield MessageChunk(role='assistant', content='ok')
runtime_provider.requester.invoke_llm_stream = stream
chunks = [
chunk
async for chunk in runtime_provider.invoke_llm_stream(
None, runtime_llm_model, [], execution_context=TEST_EXECUTION_CONTEXT
)
]
assert [chunk.content for chunk in chunks] == ['ok']