Private
Public Access
0
0

refactor(ai_client): classifier functions return ErrorInfo instead of ProviderError

The 6 error-classifier functions in ai_client.py, openai_compatible.py,
and qwen_adapter.py now return ErrorInfo (data-oriented) instead of
ProviderError. Each takes a source: str parameter for telemetry
provenance. ProviderError class is still used in production code paths
(Task 3.4) and will be removed in Task 3.7.
This commit is contained in:
2026-06-12 18:32:05 -04:00
parent 1c99724670
commit 0cad1e161f
3 changed files with 61 additions and 58 deletions
+41 -39
View File
@@ -61,6 +61,7 @@ PROVIDERS: List[str] = ["gemini", "anthropic", "gemini_cli", "deepseek", "minima
# existing call sites and the T3.1 test (which asserts
# hasattr(src.ai_client, '_require_warmed')) continue to work.
from src.module_loader import _require_warmed # noqa: E402,F401
from src.result_types import ErrorInfo, ErrorKind # noqa: E402,F401
_provider: str = "gemini"
@@ -358,42 +359,43 @@ def _load_credentials() -> dict[str, Any]:
f"Or set SLOP_CREDENTIALS env var to a custom path."
)
def _classify_anthropic_error(exc: Exception) -> ProviderError:
def _classify_anthropic_error(exc: Exception, source: str = "ai_client.anthropic") -> ErrorInfo:
try:
anthropic = _require_warmed("anthropic")
if isinstance(exc, anthropic.RateLimitError): return ProviderError("rate_limit", "anthropic", exc)
if isinstance(exc, anthropic.AuthenticationError): return ProviderError("auth", "anthropic", exc)
if isinstance(exc, anthropic.PermissionDeniedError): return ProviderError("auth", "anthropic", exc)
if isinstance(exc, anthropic.APIConnectionError): return ProviderError("network", "anthropic", exc)
if isinstance(exc, anthropic.RateLimitError): return ErrorInfo(kind=ErrorKind.RATE_LIMIT, message=str(exc), source=source, original=exc)
if isinstance(exc, anthropic.AuthenticationError): return ErrorInfo(kind=ErrorKind.AUTH, message=str(exc), source=source, original=exc)
if isinstance(exc, anthropic.PermissionDeniedError): return ErrorInfo(kind=ErrorKind.AUTH, message=str(exc), source=source, original=exc)
if isinstance(exc, anthropic.APIConnectionError): return ErrorInfo(kind=ErrorKind.NETWORK, message=str(exc), source=source, original=exc)
if isinstance(exc, anthropic.APIStatusError):
status = getattr(exc, "status_code", 0)
body = str(exc).lower()
if status == 429: return ProviderError("rate_limit", "anthropic", exc)
if status in (401, 403): return ProviderError("auth", "anthropic", exc)
if status == 402: return ProviderError("balance", "anthropic", exc)
if "credit" in body or "balance" in body or "billing" in body: return ProviderError("balance", "anthropic", exc)
if "quota" in body or "limit" in body or "exceeded" in body: return ProviderError("quota", "anthropic", exc)
if status == 429: return ErrorInfo(kind=ErrorKind.RATE_LIMIT, message=str(exc), source=source, original=exc)
if status in (401, 403): return ErrorInfo(kind=ErrorKind.AUTH, message=str(exc), source=source, original=exc)
if status == 402: return ErrorInfo(kind=ErrorKind.BALANCE, message=str(exc), source=source, original=exc)
if "credit" in body or "balance" in body or "billing" in body: return ErrorInfo(kind=ErrorKind.BALANCE, message=str(exc), source=source, original=exc)
if "quota" in body or "limit" in body or "exceeded" in body: return ErrorInfo(kind=ErrorKind.QUOTA, message=str(exc), source=source, original=exc)
except ImportError:
pass
return ProviderError("unknown", "anthropic", exc)
return ErrorInfo(kind=ErrorKind.UNKNOWN, message=str(exc), source=source, original=exc)
def _classify_gemini_error(exc: Exception) -> ProviderError:
def _classify_gemini_error(exc: Exception, source: str = "ai_client.gemini") -> ErrorInfo:
body = str(exc).lower()
try:
if isinstance(exc, gac.ResourceExhausted): return ProviderError("quota", "gemini", exc)
if isinstance(exc, gac.TooManyRequests): return ProviderError("rate_limit", "gemini", exc)
if isinstance(exc, (gac.Unauthenticated, gac.PermissionDenied)): return ProviderError("auth", "gemini", exc)
if isinstance(exc, gac.ServiceUnavailable): return ProviderError("network", "gemini", exc)
except ImportError:
gac = _require_warmed("google.api_core.exceptions")
if isinstance(exc, gac.ResourceExhausted): return ErrorInfo(kind=ErrorKind.QUOTA, message=str(exc), source=source, original=exc)
if isinstance(exc, gac.TooManyRequests): return ErrorInfo(kind=ErrorKind.RATE_LIMIT, message=str(exc), source=source, original=exc)
if isinstance(exc, (gac.Unauthenticated, gac.PermissionDenied)): return ErrorInfo(kind=ErrorKind.AUTH, message=str(exc), source=source, original=exc)
if isinstance(exc, gac.ServiceUnavailable): return ErrorInfo(kind=ErrorKind.NETWORK, message=str(exc), source=source, original=exc)
except (ImportError, AttributeError):
pass
if "429" in body or "quota" in body or "resource exhausted" in body: return ProviderError("quota", "gemini", exc)
if "rate" in body and "limit" in body: return ProviderError("rate_limit", "gemini", exc)
if "401" in body or "403" in body or "api key" in body or "unauthenticated" in body: return ProviderError("auth", "gemini", exc)
if "402" in body or "billing" in body or "balance" in body or "payment" in body: return ProviderError("balance", "gemini", exc)
if "connection" in body or "timeout" in body or "unreachable" in body: return ProviderError("network", "gemini", exc)
return ProviderError("unknown", "gemini", exc)
if "429" in body or "quota" in body or "resource exhausted" in body: return ErrorInfo(kind=ErrorKind.QUOTA, message=str(exc), source=source, original=exc)
if "rate" in body and "limit" in body: return ErrorInfo(kind=ErrorKind.RATE_LIMIT, message=str(exc), source=source, original=exc)
if "401" in body or "403" in body or "api key" in body or "unauthenticated" in body: return ErrorInfo(kind=ErrorKind.AUTH, message=str(exc), source=source, original=exc)
if "402" in body or "billing" in body or "balance" in body or "payment" in body: return ErrorInfo(kind=ErrorKind.BALANCE, message=str(exc), source=source, original=exc)
if "connection" in body or "timeout" in body or "unreachable" in body: return ErrorInfo(kind=ErrorKind.NETWORK, message=str(exc), source=source, original=exc)
return ErrorInfo(kind=ErrorKind.UNKNOWN, message=str(exc), source=source, original=exc)
def _classify_deepseek_error(exc: Exception) -> ProviderError:
def _classify_deepseek_error(exc: Exception, source: str = "ai_client.deepseek") -> ErrorInfo:
requests = _require_warmed("requests")
body = ""
if isinstance(exc, requests.exceptions.HTTPError) and exc.response is not None:
@@ -408,16 +410,16 @@ def _classify_deepseek_error(exc: Exception) -> ProviderError:
body = str(exc)
body_l = body.lower()
if "429" in body_l or "rate" in body_l: return ProviderError("rate_limit", "deepseek", Exception(body))
if "401" in body_l or "403" in body_l or "auth" in body_l or "api key" in body_l: return ProviderError("auth", "deepseek", Exception(body))
if "402" in body_l or "balance" in body_l or "billing" in body_l: return ProviderError("balance", "deepseek", Exception(body))
if "quota" in body_l or "limit exceeded" in body_l: return ProviderError("quota", "deepseek", Exception(body))
if "connection" in body_l or "timeout" in body_l or "network" in body_l: return ProviderError("network", "deepseek", Exception(body))
if "429" in body_l or "rate" in body_l: return ErrorInfo(kind=ErrorKind.RATE_LIMIT, message=body, source=source, original=exc)
if "401" in body_l or "403" in body_l or "auth" in body_l or "api key" in body_l: return ErrorInfo(kind=ErrorKind.AUTH, message=body, source=source, original=exc)
if "402" in body_l or "balance" in body_l or "billing" in body_l: return ErrorInfo(kind=ErrorKind.BALANCE, message=body, source=source, original=exc)
if "quota" in body_l or "limit exceeded" in body_l: return ErrorInfo(kind=ErrorKind.QUOTA, message=body, source=source, original=exc)
if "connection" in body_l or "timeout" in body_l or "network" in body_l: return ErrorInfo(kind=ErrorKind.NETWORK, message=body, source=source, original=exc)
# If we have a body for a 400 error, wrap it
if "400" in body_l or "bad request" in body_l: return ProviderError("unknown", "deepseek", Exception(f"DeepSeek Bad Request: {body}"))
return ProviderError("unknown", "deepseek", Exception(body))
if "400" in body_l or "bad request" in body_l: return ErrorInfo(kind=ErrorKind.UNKNOWN, message=f"DeepSeek Bad Request: {body}", source=source, original=exc)
return ErrorInfo(kind=ErrorKind.UNKNOWN, message=body, source=source, original=exc)
def _classify_minimax_error(exc: Exception) -> ProviderError:
def _classify_minimax_error(exc: Exception, source: str = "ai_client.minimax") -> ErrorInfo:
requests = _require_warmed("requests")
body = ""
if isinstance(exc, requests.exceptions.HTTPError) and exc.response is not None:
@@ -431,14 +433,14 @@ def _classify_minimax_error(exc: Exception) -> ProviderError:
body = str(exc)
body_l = body.lower()
if "429" in body_l or "rate" in body_l: return ProviderError("rate_limit", "minimax", Exception(body))
if "401" in body_l or "403" in body_l or "auth" in body_l or "api key" in body_l: return ProviderError("auth", "minimax", Exception(body))
if "402" in body_l or "balance" in body_l or "billing" in body_l: return ProviderError("balance", "minimax", Exception(body))
if "quota" in body_l or "limit exceeded" in body_l: return ProviderError("quota", "minimax", Exception(body))
if "connection" in body_l or "timeout" in body_l or "network" in body_l: return ProviderError("network", "minimax", Exception(body))
if "429" in body_l or "rate" in body_l: return ErrorInfo(kind=ErrorKind.RATE_LIMIT, message=body, source=source, original=exc)
if "401" in body_l or "403" in body_l or "auth" in body_l or "api key" in body_l: return ErrorInfo(kind=ErrorKind.AUTH, message=body, source=source, original=exc)
if "402" in body_l or "balance" in body_l or "billing" in body_l: return ErrorInfo(kind=ErrorKind.BALANCE, message=body, source=source, original=exc)
if "quota" in body_l or "limit exceeded" in body_l: return ErrorInfo(kind=ErrorKind.QUOTA, message=body, source=source, original=exc)
if "connection" in body_l or "timeout" in body_l or "network" in body_l: return ErrorInfo(kind=ErrorKind.NETWORK, message=body, source=source, original=exc)
if "400" in body_l or "bad request" in body_l: return ProviderError("unknown", "minimax", Exception(f"MiniMax Bad Request: {body}"))
return ProviderError("unknown", "minimax", Exception(body))
if "400" in body_l or "bad request" in body_l: return ErrorInfo(kind=ErrorKind.UNKNOWN, message=f"MiniMax Bad Request: {body}", source=source, original=exc)
return ErrorInfo(kind=ErrorKind.UNKNOWN, message=body, source=source, original=exc)
def set_provider(provider: str, model: str, validate: bool = True) -> None:
"""