102 lines
3.8 KiB
Python
102 lines
3.8 KiB
Python
import httpx
|
|
|
|
from backend.app.services.llm import LLMService
|
|
|
|
|
|
def _build_http_status_error(status_code: int) -> httpx.HTTPStatusError:
|
|
request = httpx.Request("POST", "https://example.com/v1/chat/completions")
|
|
response = httpx.Response(status_code, request=request)
|
|
return httpx.HTTPStatusError(
|
|
message=f"status={status_code}",
|
|
request=request,
|
|
response=response,
|
|
)
|
|
|
|
|
|
def test_fail_fast_for_auth_status_errors():
|
|
assert LLMService._is_fail_fast_exception(_build_http_status_error(401))
|
|
assert LLMService._is_fail_fast_exception(_build_http_status_error(403))
|
|
assert not LLMService._is_fail_fast_exception(_build_http_status_error(500))
|
|
|
|
|
|
def test_fail_fast_for_empty_response_markers():
|
|
assert LLMService._is_fail_fast_exception(ValueError("primary_engine_empty_response"))
|
|
assert LLMService._is_fail_fast_exception(ValueError("LLM 返回空内容"))
|
|
assert LLMService._is_fail_fast_exception(ValueError("provider_circuit_open:openai:gpt-4o"))
|
|
|
|
|
|
def test_provider_circuit_breaker_opens_after_threshold():
|
|
service = LLMService()
|
|
circuit_key = service._provider_circuit_key(
|
|
provider="openai",
|
|
base_url="https://example.com/v1",
|
|
model="MiniMax-M2.5",
|
|
)
|
|
LLMService._provider_circuit_state.pop(circuit_key, None)
|
|
|
|
service._record_provider_failure(circuit_key, ValueError("LLM 返回空内容"))
|
|
assert not service._is_provider_circuit_open(circuit_key)
|
|
|
|
service._record_provider_failure(circuit_key, ValueError("LLM 返回空内容"))
|
|
assert service._is_provider_circuit_open(circuit_key)
|
|
|
|
service._record_provider_success(circuit_key)
|
|
assert not service._is_provider_circuit_open(circuit_key)
|
|
|
|
|
|
def test_extract_structured_text_from_reasoning_returns_json_candidate():
|
|
service = LLMService()
|
|
|
|
text = service._extract_structured_text_from_reasoning(
|
|
message={"reasoning_content": "先分析,再输出结果:{\"ok\": true, \"items\": [1, 2]}"},
|
|
choice={},
|
|
response={},
|
|
)
|
|
|
|
assert text == '{"ok": true, "items": [1, 2]}'
|
|
|
|
|
|
def test_extract_structured_text_from_reasoning_ignores_non_json_reasoning():
|
|
service = LLMService()
|
|
|
|
text = service._extract_structured_text_from_reasoning(
|
|
message={"reasoning_content": "先分析需求,再决定是否返回。"},
|
|
choice={},
|
|
response={},
|
|
)
|
|
|
|
assert text == ""
|
|
|
|
|
|
def test_prefer_fast_local_fallback_for_signal_source_functions():
|
|
assert LLMService._prefer_fast_local_fallback("signal_source_onboarding_plan")
|
|
assert LLMService._prefer_fast_local_fallback("openclaw_command_dispatch")
|
|
assert LLMService._prefer_fast_local_fallback("signal_source_ai_worker_execution")
|
|
assert not LLMService._prefer_fast_local_fallback("adaptive_rag_answer_normal")
|
|
|
|
|
|
def test_request_timeout_for_fast_local_fallback_functions(monkeypatch):
|
|
monkeypatch.delenv("LLM_FAST_LOCAL_FALLBACK_TIMEOUT", raising=False)
|
|
service = LLMService()
|
|
service.request_timeout = 90.0
|
|
|
|
assert service._request_timeout_for_function("signal_source_onboarding_plan") == 8.0
|
|
assert service._request_timeout_for_function("openclaw_command_dispatch") == 8.0
|
|
assert service._request_timeout_for_function("adaptive_rag_answer_normal") == 90.0
|
|
|
|
|
|
def test_request_timeout_for_fast_local_fallback_respects_runtime_cap(monkeypatch):
|
|
monkeypatch.setenv("LLM_FAST_LOCAL_FALLBACK_TIMEOUT", "20")
|
|
service = LLMService()
|
|
service.request_timeout = 8.0
|
|
|
|
assert service._request_timeout_for_function("signal_source_ai_worker_execution") == 8.0
|
|
|
|
|
|
def test_fast_local_fallback_functions_skip_provider_circuit_breaker():
|
|
service = LLMService()
|
|
|
|
assert not service._use_provider_circuit_breaker("signal_source_onboarding_plan")
|
|
assert not service._use_provider_circuit_breaker("openclaw_command_dispatch")
|
|
assert service._use_provider_circuit_breaker("adaptive_rag_answer_normal")
|