fix: expose Router upstream errors

This commit is contained in:
sj
2026-08-20 10:44:19 +08:00
parent 0e5cece447
commit 89a384776e
2 changed files with 292 additions and 7 deletions
@@ -1,6 +1,7 @@
from __future__ import annotations
import asyncio
from types import SimpleNamespace
from typing import Any
import pytest
@@ -99,3 +100,169 @@ def test_new_analysis_decision_discards_main_agent_workflow_and_clarification()
assert normalized.workflow_id is None
assert normalized.clarification_question is None
assert normalized.analysis_request_kind == "execution"
def test_route_decision_retries_transient_model_failures(
monkeypatch: pytest.MonkeyPatch,
) -> None:
class TransientPaymentGatewayFailure(RuntimeError):
status_code = 402
class TransientLLM:
calls = 0
async def acall(self, *_: Any, **__: Any) -> Any:
self.calls += 1
if self.calls < 3:
raise TransientPaymentGatewayFailure("temporary billing gateway failure")
return SimpleNamespace(
content=(
'{"goal":"new_analysis","needs":["analysis_intent"],'
'"subject":"执行数据准备","analysis_request_kind":"execution"}'
)
)
async def no_sleep(_: float) -> None:
return None
llm = TransientLLM()
runtime = RouterRuntime(llm, _PassThroughTools(), max_iterations=5) # type: ignore[arg-type]
monkeypatch.setattr(asyncio, "sleep", no_sleep)
decision, calls, trace = asyncio.run(
runtime._decide_route(
query="执行数据准备",
history=[],
session_id="retry-session",
job_id="job-retry",
goal_ledger=None,
)
)
assert llm.calls == 3
assert calls == 1
assert trace == []
assert decision is not None
assert decision.goal == "new_analysis"
def test_route_decision_does_not_retry_authentication_failure() -> None:
class AuthenticationFailure(RuntimeError):
status_code = 401
class UnauthorizedLLM:
calls = 0
async def acall(self, *_: Any, **__: Any) -> Any:
self.calls += 1
raise AuthenticationFailure("unauthorized")
llm = UnauthorizedLLM()
runtime = RouterRuntime(llm, _PassThroughTools(), max_iterations=5) # type: ignore[arg-type]
decision, calls, trace = asyncio.run(
runtime._decide_route(
query="执行数据准备",
history=[],
session_id="auth-session",
job_id="job-auth",
goal_ledger=None,
)
)
assert llm.calls == 1
assert decision is None
assert calls == 1
assert trace == []
def test_route_failure_exposes_sanitized_upstream_reason(
monkeypatch: pytest.MonkeyPatch,
) -> None:
class StructuredPaymentGatewayFailure(RuntimeError):
status_code = 402
request_id = "req-router-402"
body = {
"error": {
"code": "request_policy_rejected",
"message": (
"the concrete upstream reason; "
"Authorization: Bearer must-not-leak"
),
"api_key": "must-not-leak",
}
}
class FailingLLM:
_default_model = "deepseek-v4-flash"
calls = 0
async def acall(self, *_: Any, **__: Any) -> Any:
self.calls += 1
raise StructuredPaymentGatewayFailure("generic SDK error")
async def no_sleep(_: float) -> None:
return None
llm = FailingLLM()
runtime = RouterRuntime(llm, _PassThroughTools(), max_iterations=5) # type: ignore[arg-type]
monkeypatch.setattr(asyncio, "sleep", no_sleep)
outcome = asyncio.run(
runtime.run(
query="执行数据准备",
history=[],
session_id="failure-detail-session",
job_id="job-failure-detail",
goal_ledger=None,
)
)
assert llm.calls == 3
assert outcome.status == "agent_unavailable"
assert "HTTP 402" in outcome.message
assert "request_policy_rejected" in outcome.message
assert "the concrete upstream reason" in outcome.message
assert "deepseek-v4-flash" in outcome.message
assert "3 次" in outcome.message
assert "req-router-402" in outcome.message
assert "must-not-leak" not in outcome.message
def test_route_failure_reads_flat_openai_error_body(
monkeypatch: pytest.MonkeyPatch,
) -> None:
class FlatOpenAIError(RuntimeError):
status_code = 402
body = {
"code": "invalid_request_error",
"message": "Insufficient Balance",
"type": "unknown_error",
}
class FailingLLM:
_default_model = "deepseek-v4-flash"
async def acall(self, *_: Any, **__: Any) -> Any:
raise FlatOpenAIError("serialized SDK fallback")
async def no_sleep(_: float) -> None:
return None
runtime = RouterRuntime(FailingLLM(), _PassThroughTools(), max_iterations=5) # type: ignore[arg-type]
monkeypatch.setattr(asyncio, "sleep", no_sleep)
outcome = asyncio.run(
runtime.run(
query="执行数据准备",
history=[],
session_id="flat-openai-error-session",
job_id="job-flat-openai-error",
goal_ledger=None,
)
)
assert outcome.status == "agent_unavailable"
assert "错误码 invalid_request_error" in outcome.message
assert outcome.message.endswith("Insufficient Balance")
assert "serialized SDK fallback" not in outcome.message