Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 2 additions & 9 deletions llm/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -294,8 +294,8 @@ def chat(model: str, system: str, user: str,
IGNORED by MiniMax-M3 (verified 2026-07-06: low==high, M3 scales reasoning with
problem difficulty, not the knob). GLM-5.3 does not support disabling thinking, so
its truncation recovery uses ``low``. None omits the field. ``provider_order`` is
an optional OpenRouter route preference; when present, it is never silently dropped
after an API error because that would change the frozen experiment mid-run."""
an optional OpenRouter route preference. All request options are preserved through
transport retries so an API error cannot change the frozen experiment mid-run."""
messages = _request_messages(system, user, history=history, image=image)
kwargs = {}
if json_object:
Expand Down Expand Up @@ -348,13 +348,6 @@ def chat(model: str, system: str, user: str,
"embedded_code=%s error_type=%s", model,
e.body["id"], e.body["provider"], e.status_code,
metadata.get("error_type") if isinstance(metadata, dict) else None)
if kwargs and not order and not isinstance(e, _CompletionResponseError):
# A returned error/empty completion is not evidence that optional
# fields are unsupported. Preserve the exact request on its retry.
log.warning("%s erred with optional request fields (%s); dropping them "
"and retrying", model, name)
kwargs = {}
continue
body = getattr(e, "body", None) # v35.1: surface the provider's actual
detail = f" body={str(body)[:300]}" if body else "" # complaint — a 400's
if attempt == 3: # class name alone hides the cause
Expand Down
60 changes: 59 additions & 1 deletion tests/test_llm_response_errors.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
"""HTTP 200 may carry a provider error, including no completion choices."""
from copy import deepcopy

import httpx
import pytest
from openai import APIConnectionError, APIStatusError, APITimeoutError
from openai.types.chat import ChatCompletion

from llm import client as C
Expand All @@ -22,7 +24,10 @@ def install(monkeypatch, replies):

def create(**kwargs):
requests.append(deepcopy(kwargs))
return next(iterator)
reply = next(iterator)
if isinstance(reply, Exception):
raise reply
return reply

monkeypatch.setattr(C, '_client', SimpleNamespace(
chat=SimpleNamespace(completions=SimpleNamespace(create=create))))
Expand Down Expand Up @@ -83,6 +88,59 @@ def test_transient_error_then_success_keeps_exact_request_options(monkeypatch):
assert C.provider_counts() == {'test-model': {'test-provider': 1}}


def transport_error(kind):
request = httpx.Request('POST', 'https://example.invalid/v1/chat/completions')
if kind == 'connection':
return APIConnectionError(request=request)
if kind == 'timeout':
return APITimeoutError(request=request)
return APIStatusError('offline HTTP failure', response=httpx.Response(
kind, request=request), body={'message': 'offline provider diagnostic'})


@pytest.mark.parametrize('kind', [429, 503, 'connection', 'timeout'])
@pytest.mark.parametrize('provider_order', [[], ['Pinned']])
def test_transport_retry_preserves_complete_request(monkeypatch, kind, provider_order):
requests = install(monkeypatch, [transport_error(kind),
response({'choices': [good_choice()]})])
history = [C.durable_user_message('prior observation', b'\x89PNG\r\n\x1a\nprior'),
{'role': 'assistant', 'content': 'prior answer',
'reasoning': 'prior reasoning'}]
original_history = deepcopy(history)
answer = C.chat('test-model', 'system', 'user', history=history,
image=b'\xff\xd8current', max_tokens=1234, temperature=0.7,
reasoning_effort='max', top_p=0.9, json_object=True,
provider_order=provider_order, provider_allow_fallbacks=False,
provider_require_parameters=True)

assert answer == 'valid reply'
assert len(requests) == 2
assert requests[0]['extra_body']['reasoning'] == {'effort': 'max'}
assert requests[0]['response_format'] == {'type': 'json_object'}
assert requests[0]['top_p'] == 0.9
assert requests[1] == requests[0]
assert history == original_history
assert C.pop_last_reasoning() == 'owned reasoning'
assert C.provider_counts() == {'test-model': {'test-provider': 1}}


@pytest.mark.parametrize('code', [400, 422])
def test_fatal_transport_error_retains_options_until_exhaustion(monkeypatch, code):
requests = install(monkeypatch, [transport_error(code) for _ in range(4)])
with pytest.raises(C.LLMTransportError) as caught:
C.chat('test-model', 'system', 'user', reasoning_max_tokens=512,
top_p=0.8, json_object=True)

assert len(requests) == 4
assert requests[0]['extra_body']['reasoning'] == {'max_tokens': 512}
assert requests[0]['response_format'] == {'type': 'json_object'}
assert requests[0]['top_p'] == 0.8
assert all(item == requests[0] for item in requests)
assert caught.value.status_code == code
assert caught.value.recoverable is False
assert C.provider_counts() == {}


def test_error_with_partial_choice_is_not_a_successful_agent_turn(monkeypatch):
install(monkeypatch, [response({'error': {'code': 502, 'message': 'partial failure'},
'choices': [good_choice()]})] * 4)
Expand Down