fix(fallback): treat empty API choices as fallbackable error

When the primary model (e.g. DeepSeek during peak hours) returns an empty
choices response with HTTP 200, the error carries no status code or
structured error metadata. The existing _FALLBACK_ERROR_TOKENS had no
matching token, so _should_fallback() returned False and fallback models
were never tried.

Changes:
- Add 'empty' token to _FALLBACK_ERROR_TOKENS so 'Error: API returned
  empty choices.' text matches the fallback path
- Set error_kind='empty' in openai_compat_provider when returning
  the empty-choices error, making the classification explicit
- Add test coverage for both text-only and error_kind matching paths

Fixes: glebov reported primary never falls back when DeepSeek returns
       empty responses
This commit is contained in:
nanobot-contributor
2026-06-19 14:59:44 +08:00
committed by Xubin Ren
parent d36117de7a
commit c2c47f7a03
3 changed files with 61 additions and 2 deletions
+1
View File
@@ -42,6 +42,7 @@ _FALLBACK_ERROR_TOKENS = (
"timeout",
"timed out",
"connection",
"empty", # API returned empty choices (e.g. DeepSeek peak hours), transient
"insufficient_quota",
"insufficient quota",
"quota_exceeded",
+10 -2
View File
@@ -1094,7 +1094,11 @@ class OpenAICompatProvider(LLMProvider):
finish_reason=str(response_map.get("finish_reason") or "stop"),
usage=self._extract_usage(response_map),
)
return LLMResponse(content="Error: API returned empty choices.", finish_reason="error")
return LLMResponse(
content="Error: API returned empty choices.",
finish_reason="error",
error_kind="empty",
)
choice0 = self._maybe_mapping(choices[0]) or {}
msg0 = self._maybe_mapping(choice0.get("message")) or {}
@@ -1151,7 +1155,11 @@ class OpenAICompatProvider(LLMProvider):
)
if not response.choices:
return LLMResponse(content="Error: API returned empty choices.", finish_reason="error")
return LLMResponse(
content="Error: API returned empty choices.",
finish_reason="error",
error_kind="empty",
)
choice = response.choices[0]
msg = choice.message