personal memory agent
0

Configure Feed

Select the types of activity you want to include in your feed.

fix(providers): match the wrapper the cloud lane raises

The cloud lane raises openhands.sdk.llm.exceptions.types.LLMBadRequestError, not litellm.exceptions.BadRequestError, so real Gemini 400s recorded unknown; the wrapper carries no status_code.

The match folds into the existing cloud-gated rejection arm as a name-equality condition, kept below the _CONTEXT_WINDOW_PATTERNS message check so an LLMBadRequestError carrying a context-overflow message still classifies as context_window_exceeded (cap 2, retryable) rather than being mislabeled a defect in sol. The litellm status-code condition stays reachable in the same arm.

+79 -6
+7 -5
solstone/think/providers/shared.py
··· 363 363 if "internalservererror" in exc_name_lower or "servererror" in exc_name_lower: 364 364 return "provider_unavailable" 365 365 366 - if ( 367 - is_cloud_provider(provider) 368 - and _module_matches(exc_module, "litellm.exceptions") 369 - and exc_name == "BadRequestError" 370 - and status_code == 400 366 + if is_cloud_provider(provider) and ( 367 + exc_name == "LLMBadRequestError" 368 + or ( 369 + _module_matches(exc_module, "litellm.exceptions") 370 + and exc_name == "BadRequestError" 371 + and status_code == 400 372 + ) 371 373 ): 372 374 return "provider_request_rejected" 373 375
+72 -1
tests/test_provider_error_classification.py
··· 7 7 8 8 from solstone.think.providers import openhands 9 9 from solstone.think.providers.cli import ProviderKeyMissingError, QuotaExhaustedError 10 - from solstone.think.providers.shared import classify_provider_error 10 + from solstone.think.providers.shared import ( 11 + _CONTEXT_WINDOW_PATTERNS, 12 + classify_provider_error, 13 + ) 14 + 15 + _OPENHANDS_BAD_REQUEST_GOOGLE_OUTAGE = ( 16 + "litellm.BadRequestError: GeminiException BadRequestError - " 17 + '{"error": {"code": 400, "message": "Request contains an invalid argument.", ' 18 + '"status": "INVALID_ARGUMENT"}}' 19 + ) 11 20 12 21 13 22 def _require_attrs(module, *names: str): ··· 88 97 89 98 assert exc.status_code == 400 90 99 assert classify_provider_error(exc, "google") == "provider_request_rejected" 100 + 101 + 102 + def test_classifies_openhands_bad_request_google_request_rejected(): 103 + from openhands.sdk.llm.exceptions.types import LLMBadRequestError 104 + 105 + exc = LLMBadRequestError(_OPENHANDS_BAD_REQUEST_GOOGLE_OUTAGE) 106 + 107 + assert classify_provider_error(exc, "google") == "provider_request_rejected" 108 + 109 + 110 + @pytest.mark.parametrize("message", _CONTEXT_WINDOW_PATTERNS) 111 + def test_classifies_openhands_bad_request_context_window_messages_before_rejection( 112 + message, 113 + ): 114 + from openhands.sdk.llm.exceptions.types import LLMBadRequestError 115 + 116 + exc = LLMBadRequestError(message) 117 + 118 + assert classify_provider_error(exc, "google") == "context_window_exceeded" 119 + 120 + 121 + def test_preserves_existing_openhands_llm_exception_classifications(): 122 + from openhands.sdk.llm.exceptions.types import ( 123 + LLMAuthenticationError, 124 + LLMContextWindowExceedError, 125 + LLMContextWindowTooSmallError, 126 + LLMNoResponseError, 127 + LLMRateLimitError, 128 + LLMResponseError, 129 + LLMServiceUnavailableError, 130 + LLMTimeoutError, 131 + ) 132 + 133 + cases = [ 134 + (LLMAuthenticationError("auth failed"), "provider_key_invalid"), 135 + (LLMContextWindowExceedError("context window"), "context_window_exceeded"), 136 + (LLMContextWindowTooSmallError(8192), "context_window_exceeded"), 137 + (LLMRateLimitError("rate limit"), "provider_quota_exceeded"), 138 + (LLMServiceUnavailableError("unavailable"), "provider_unavailable"), 139 + (LLMTimeoutError("timeout"), "chat_timeout"), 140 + (LLMNoResponseError("no response"), "provider_response_invalid"), 141 + (LLMResponseError("bad response"), "provider_response_invalid"), 142 + ] 143 + 144 + for exc, expected in cases: 145 + assert classify_provider_error(exc, "google") == expected 146 + 147 + 148 + def test_openhands_bad_request_local_not_request_rejected(): 149 + from openhands.sdk.llm.exceptions.types import LLMBadRequestError 150 + 151 + exc = LLMBadRequestError(_OPENHANDS_BAD_REQUEST_GOOGLE_OUTAGE) 152 + 153 + assert classify_provider_error(exc, "local") != "provider_request_rejected" 154 + 155 + 156 + def test_bare_openhands_llm_error_not_request_rejected(): 157 + from openhands.sdk.llm.exceptions.types import LLMError 158 + 159 + assert classify_provider_error(LLMError("boom"), "google") != ( 160 + "provider_request_rejected" 161 + ) 91 162 92 163 93 164 def test_preserves_existing_litellm_cloud_exception_classifications():