mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-10-06 15:02:20 +02:00
Show provider failures as inline chat stream errors
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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': {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user