Show provider failures as inline chat stream errors

This commit is contained in:
pewdiepie-archdaemon
2026-09-23 01:13:46 +00:00
parent 7e798fe925
commit 86f376ac3a
3 changed files with 69 additions and 3 deletions
+2
View File
@@ -4152,6 +4152,8 @@ def setup_chat_routes(
yield f'data: {json.dumps({"type": "chat_terminal", "data": _terminal_metrics})}\n\n'
yield chunk
elif chunk.startswith("event: "):
if chunk.startswith("event: error"):
_stream_set(session, status="error")
yield chunk
elif chunk == "data: [DONE]\n\n":
if _chat_terminal_saved:
+17 -2
View File
@@ -6792,10 +6792,25 @@ async def stream_preview(*, endpoint_url, model, messages, headers, turn_contrac
break
else:
yield event({'delta': '\nThe preview reached its round limit. Please narrow the request.'})
except httpx.HTTPStatusError as exc:
status = exc.response.status_code
if status == 402:
detail = 'Payment required by the selected model provider (HTTP 402). Check its billing or credits, or choose another model.'
elif status in (401, 403):
detail = f'The selected model provider rejected access (HTTP {status}). Check its credentials and permissions.'
elif status == 429:
detail = 'The selected model provider is rate limiting requests (HTTP 429). Wait before retrying or choose another model.'
elif status >= 500:
detail = f'The selected model provider is unavailable (HTTP {status}). Retry later or choose another model.'
else:
detail = f'The selected model provider rejected the request (HTTP {status}). Check the provider or choose another model.'
logging.getLogger(__name__).warning('Clean v3 provider request failed with HTTP %s', status)
yield f'event: error\ndata: {json.dumps({"status": status, "error": detail})}\n\n'
return
except Exception:
import logging
logging.getLogger(__name__).exception('Clean v3 preview failed')
yield event({'delta': '\nThe v3 test encountered an error. No fallback model or fabricated tool call was used.'})
yield f'event: error\ndata: {json.dumps({"status": 500, "error": "The model request failed unexpectedly. Check the server log and retry."})}\n\n'
return
elapsed = time.monotonic() - started
ttft = first_token - started if first_token else None
yield event({'type': 'metrics', 'data': {
+50 -1
View File
@@ -4516,6 +4516,53 @@ async def test_stream_emits_incremental_text_and_persistable_history(monkeypatch
assert raw[-1] == 'data: [DONE]\n\n'
@pytest.mark.asyncio
@pytest.mark.parametrize('status,expected', [
(402, 'billing or credits'),
(401, 'credentials and permissions'),
(429, 'rate limiting'),
(503, 'unavailable'),
])
async def test_preview_provider_http_failure_is_terminal_error_not_assistant_text(monkeypatch, status, expected):
import httpx
import src.clean_agent_preview as module
class Response:
status_code = status
async def __aenter__(self): return self
async def __aexit__(self, *args): pass
def raise_for_status(self):
request = httpx.Request('POST', 'https://provider.example/v1/chat/completions')
response = httpx.Response(status, request=request)
raise httpx.HTTPStatusError('provider secret must not be shown', request=request, response=response)
class Client:
def __init__(self, **kwargs): pass
async def __aenter__(self): return self
async def __aexit__(self, *args): pass
def stream(self, *args, **kwargs): return Response()
monkeypatch.setattr(module.httpx, 'AsyncClient', Client)
contract = resolve_full_inventory_contract(schemas=[], policy=ToolPolicy())
raw = [chunk async for chunk in stream_preview(
endpoint_url='https://provider.example/v1/chat/completions', model='test',
messages=[{'role': 'user', 'content': 'hello'}], headers={},
turn_contract=contract, session_id='test', owner='test',
disabled_tools=set(), tool_policy=ToolPolicy(),
)]
assert raw[-1].startswith('event: error\ndata: ')
assert all('"delta"' not in chunk for chunk in raw)
assert all('"type": "metrics"' not in chunk for chunk in raw)
assert 'data: [DONE]' not in raw
payload = json.loads(raw[-1].split('data: ', 1)[1])
assert payload['status'] == status
assert expected in payload['error']
assert 'provider secret' not in raw[-1]
@pytest.mark.asyncio
async def test_ajax_c375_clean_runtime_uses_progressive_thinking_without_leaking(monkeypatch):
import src.clean_agent_preview as module
@@ -6003,7 +6050,9 @@ async def test_context_recovery_is_bounded_and_not_used_for_other_errors(
disabled_tools=set(), tool_policy=ToolPolicy())]
assert len(requests) == expected_requests
assert not any('"type": "tool_start"' in chunk for chunk in raw)
assert any('encountered an error' in chunk for chunk in raw)
assert raw[-1].startswith('event: error\ndata: ')
assert json.loads(raw[-1].split('data: ', 1)[1])['status'] == status
assert all('"delta"' not in chunk for chunk in raw)
@pytest.mark.asyncio