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")