Fallback also triggers on thread/start crash with explicit HTTP code

Live test found the gap: a nonexistent model fails in
thread/start, host.py exits non-zero without JSON, so the
executor result has host_status=None and the worker never
fell back (task failed, no worker_fallback audit). When
host_status is absent, worker now extracts an explicit signal
from stdout/stderr/runner error: 404/429/5xx codes, or
'model not found' style text (counted as 404). Generic
failures with no explicit code still do not fall back, and
401 in stderr still never falls back. Tests: fallback 9 pass,
cloud 333 pass, host 27 pass.
This commit is contained in:
host committed 2026-10-26 05:30:04 +08:00
1 parent 52048d7633
commit 6cee452750
2 files changed
+102 -1

No files matched your search

+63
View File
@@ -261,6 +261,69 @@ class FallbackRetryTestCase(unittest.TestCase):
self.assertEqual(len(self.calls), 2)
self.assertEqual(result["events"][0]["reason"], "timeout")
def test_thread_start_crash_with_404_in_stderr_fallback_succeeds(self):
# 坏模型最常见的真实失败:thread/start 抛错、host.py 非 0
# 退出、不打印 JSON,host_status 为 None;stderr 里有显式
# 404 时必须 fallback,第二次换模型成功(主机实测回归)。
def fake(cmd):
if _model_of(cmd) == "gpt-6-sol":
return _host_json("A", "completed", "done by fallback")
return SimpleNamespace(
returncode=1, stdout="",
stderr="RuntimeError: thread/start 失败: {'error': "
"{'code': 404, 'message': 'model not found'}}")
_insert_task(self.db_path, "A")
worker = self._worker(
runner=self._recording(fake),
model="gpt-6.1-sol",
fallback_models=["gpt-6-sol"],
max_retries=0)
result = worker.run_one()
self.assertEqual(result["status"], "completed")
self.assertEqual(result["actual_model"], "gpt-6-sol")
self.assertEqual(len(self.calls), 2)
self.assertEqual(result["events"][0]["reason"], "404")
row, cost, audits = _execution(self.db_path, "A")
self.assertEqual(cost[1], 1)
self.assertIn("worker_fallback", audits)
def test_thread_start_crash_with_401_in_stderr_never_fallbacks(self):
# stderr 里是 401 时绝不 fallback,只调一次。
def fake(cmd):
return SimpleNamespace(
returncode=1, stdout="",
stderr="RuntimeError: thread/start 失败: {'error': "
"{'code': 401, 'message': 'unauthorized'}}")
_insert_task(self.db_path, "A")
worker = self._worker(
runner=self._recording(fake),
model="gpt-6.1-sol",
fallback_models=["gpt-6-sol"],
max_retries=0)
result = worker.run_one()
self.assertEqual(result["status"], "failed")
self.assertEqual(len(self.calls), 1)
self.assertEqual(result["events"], [])
def test_thread_start_crash_without_code_never_fallbacks(self):
# 没有显式状态码的通用失败不猜测:不 fallback,只调一次。
def fake(cmd):
return SimpleNamespace(
returncode=1, stdout="", stderr="host execution failed")
_insert_task(self.db_path, "A")
worker = self._worker(
runner=self._recording(fake),
model="gpt-6.1-sol",
fallback_models=["gpt-6-sol"],
max_retries=0)
result = worker.run_one()
self.assertEqual(result["status"], "failed")
self.assertEqual(len(self.calls), 1)
self.assertEqual(result["events"], [])
if __name__ == "__main__":
unittest.main()