From fb2688fd37e8fa776ed7a44c231702097693bc47 Mon Sep 17 00:00:00 2001 From: arcdrake22 Date: Sun, 2 Aug 2026 21:29:54 +0200 Subject: [PATCH] fix(providers): fall back to chat completions on serde body rejections DeepSeek's new Responses endpoint (deepseek-v4-flash) intermittently rejects valid request bodies with serde deserialization errors such as 'input: invalid type: string ..., expected a sequence'. These were not classified as compatibility errors, so affected conversations died instead of falling back to Chat Completions. The wire format is correct (input serializes as a list), so this is a server-side Responses compatibility issue; Chat Completions is strictly more permissive, making fallback safe. Extend the fallback classifier to recognize serde body-parsing markers. Repeated failures still trip the existing circuit breaker. --- nanobot/providers/openai_compat_provider.py | 10 ++++ .../test_responses_circuit_breaker.py | 52 +++++++++++++++++++ 2 files changed, 62 insertions(+) diff --git a/nanobot/providers/openai_compat_provider.py b/nanobot/providers/openai_compat_provider.py index 7b6238f22..da3c61218 100644 --- a/nanobot/providers/openai_compat_provider.py +++ b/nanobot/providers/openai_compat_provider.py @@ -1082,6 +1082,16 @@ class OpenAICompatProvider(LLMProvider): "not supported", "unknown parameter", "unrecognized request argument", + # Serde-style body rejection: the endpoint could not parse the + # Responses wire format (e.g. DeepSeek's Responses gateway + # rejecting an input item shape with "Failed to deserialize the + # JSON body ... expected a sequence"). These are compatibility + # failures: fall back to Chat Completions for the same model. + "failed to deserialize", + "invalid type", + "expected a sequence", + "expected a struct", + "unknown field", ) return any(marker in body_text for marker in compatibility_markers) diff --git a/tests/providers/test_responses_circuit_breaker.py b/tests/providers/test_responses_circuit_breaker.py index 1b6c95cac..bb8f7e164 100644 --- a/tests/providers/test_responses_circuit_breaker.py +++ b/tests/providers/test_responses_circuit_breaker.py @@ -150,3 +150,55 @@ def test_reasoning_effort_key_is_case_insensitive(provider): for _ in range(_RESPONSES_FAILURE_THRESHOLD): provider._record_responses_failure("o3", "High") assert provider._should_use_responses_api("o3", "high") is False + + +# ====================================================================== +# _should_fallback_from_responses_error +# ====================================================================== + + +class _FakeAPIError(Exception): + def __init__(self, status_code, body): + super().__init__(str(body)) + self.status_code = status_code + self.body = body + self.response = None + + +def test_serde_deserialize_error_triggers_fallback(): + # DeepSeek Responses gateway rejecting the wire body (observed Aug 2026). + err = _FakeAPIError(400, { + "message": ( + "Failed to deserialize the JSON body into the target type: " + "input: invalid type: string \"Michael topped up DeepSeek ...\", " + "expected a sequence at line 1 column 268612" + ), + "type": "invalid_request_error", + "param": None, + }) + assert OpenAICompatProvider._should_fallback_from_responses_error(err) is True + + +def test_invalid_type_error_triggers_fallback(): + err = _FakeAPIError(422, "input[0]: invalid type: map, expected a string") + assert OpenAICompatProvider._should_fallback_from_responses_error(err) is True + + +def test_unknown_field_error_triggers_fallback(): + err = _FakeAPIError(400, "unknown field `foo`, expected one of `input`, `instructions`") + assert OpenAICompatProvider._should_fallback_from_responses_error(err) is True + + +def test_legacy_compatibility_markers_still_trigger_fallback(): + err = _FakeAPIError(400, "parameter `instructions` is unsupported") + assert OpenAICompatProvider._should_fallback_from_responses_error(err) is True + + +def test_unrelated_400_does_not_trigger_fallback(): + err = _FakeAPIError(400, {"message": "rate limit exceeded", "type": "rate_limit_error"}) + assert OpenAICompatProvider._should_fallback_from_responses_error(err) is False + + +def test_server_error_does_not_trigger_fallback(): + err = _FakeAPIError(500, {"message": "internal server error"}) + assert OpenAICompatProvider._should_fallback_from_responses_error(err) is False