Class: Ace::LLM::Atoms::ErrorClassifier

Inherits:
Object
  • Object
show all
Defined in:
lib/ace/llm/atoms/error_classifier.rb

Overview

ErrorClassifier categorizes errors to determine retry and fallback strategies This is an atom - it has no dependencies on other parts of this gem

Constant Summary collapse

RETRYABLE_WITH_BACKOFF =

Error classification types

:retryable_with_backoff
FALLBACK_IMMEDIATELY =
:fallback_immediately
SKIP_TO_NEXT =
:skip_to_next
TERMINAL =
:terminal
STATUS_CLASSIFICATIONS =

Map HTTP status codes to classification

{
  401 => SKIP_TO_NEXT,      # Unauthorized - skip to next provider
  403 => SKIP_TO_NEXT,      # Forbidden - skip to next provider
  404 => SKIP_TO_NEXT,      # Not Found - skip to next provider
  429 => RETRYABLE_WITH_BACKOFF, # Rate Limited - retry with backoff
  500 => RETRYABLE_WITH_BACKOFF, # Internal Server Error - retry
  502 => RETRYABLE_WITH_BACKOFF, # Bad Gateway - retry
  503 => RETRYABLE_WITH_BACKOFF, # Service Unavailable - retry
  504 => RETRYABLE_WITH_BACKOFF  # Gateway Timeout - retry
}.freeze
QUOTA_LIMIT_PATTERNS =
[
  "insufficient_quota",
  "insufficient quota",
  "quota exceeded",
  "quota has been exceeded",
  "quota limit",
  "quota exhausted",
  "out of credit",
  "credits exhausted",
  "insufficient credit",
  "billing hard limit",
  "spending limit",
  "usage limit reached",
  "rate window limit",
  "window limit reached"
].freeze

Class Method Summary collapse

Class Method Details

.classify(error) ⇒ Symbol

Classify an error for retry/fallback decisions

Parameters:

  • error (Exception)

    The error to classify

Returns:

  • (Symbol)

    Classification type (RETRYABLE_WITH_BACKOFF, FALLBACK_IMMEDIATELY, SKIP_TO_NEXT, TERMINAL)



47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
# File 'lib/ace/llm/atoms/error_classifier.rb', line 47

def self.classify(error)
  case error
  when Ace::LLM::AuthenticationError
    SKIP_TO_NEXT
  when Ace::LLM::ProviderError
    # Check if we can extract HTTP status from the error message
    classify_provider_error(error)
  when Faraday::TimeoutError
    FALLBACK_IMMEDIATELY
  when Faraday::ConnectionFailed
    RETRYABLE_WITH_BACKOFF
  when Faraday::ClientError
    classify_faraday_error(error)
  when Faraday::ServerError
    RETRYABLE_WITH_BACKOFF
  else
    TERMINAL
  end
end

.extract_status_code(error) ⇒ Integer?

Extract HTTP status code from various error types

Parameters:

  • error (Exception)

    The error

Returns:

  • (Integer, nil)

    HTTP status code if available



102
103
104
105
106
107
108
109
110
111
112
# File 'lib/ace/llm/atoms/error_classifier.rb', line 102

def self.extract_status_code(error)
  if error.respond_to?(:response) && error.response
    error.response[:status]
  elsif error.respond_to?(:http_status)
    error.http_status
  elsif error.is_a?(Ace::LLM::ProviderError)
    # Try to parse status from error message like "error (503):"
    match = error.message.match(/\((\d{3})\)/)
    match[1].to_i if match
  end
end

.fallback_immediately?(error) ⇒ Boolean

Determine if an error should trigger immediate fallback

Parameters:

  • error (Exception)

    The error to check

Returns:

  • (Boolean)

    True if should fallback immediately



78
79
80
81
# File 'lib/ace/llm/atoms/error_classifier.rb', line 78

def self.fallback_immediately?(error)
  classification = classify(error)
  classification == FALLBACK_IMMEDIATELY
end

.quota_or_credit_limited?(error) ⇒ Boolean

Determine if an error indicates quota/credit/window exhaustion and should move immediately to the next provider.

Parameters:

  • error (Exception)

    The error to check

Returns:

  • (Boolean)

    True if this is a quota/credit/window-limit condition



95
96
97
# File 'lib/ace/llm/atoms/error_classifier.rb', line 95

def self.quota_or_credit_limited?(error)
  quota_like_message?(error.message.to_s)
end

.retry_delay(error, attempt: 1, base_delay: 1.0) ⇒ Float

Get retry delay for an error based on retry-after header or default

Parameters:

  • error (Exception)

    The error

  • attempt (Integer) (defaults to: 1)

    Current retry attempt number

  • base_delay (Float) (defaults to: 1.0)

    Base delay in seconds

Returns:

  • (Float)

    Delay in seconds with jitter



119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
# File 'lib/ace/llm/atoms/error_classifier.rb', line 119

def self.retry_delay(error, attempt: 1, base_delay: 1.0)
  # Check for retry-after header (for 429 rate limits)
  if error.respond_to?(:response) && error.response
    headers = error.response[:headers]
    if headers && headers["retry-after"]
      return parse_retry_after(headers["retry-after"])
    end
  end

  # Exponential backoff with jitter: base_delay * 2^(attempt - 1) * (1 + jitter)
  # Jitter is 10-30% to prevent thundering herd
  exponential_delay = base_delay * (2**(attempt - 1))
  jitter = rand(0.1..0.3)
  exponential_delay * (1 + jitter)
end

.retryable?(error) ⇒ Boolean

Determine if an error is retryable

Parameters:

  • error (Exception)

    The error to check

Returns:

  • (Boolean)

    True if error should be retried



70
71
72
73
# File 'lib/ace/llm/atoms/error_classifier.rb', line 70

def self.retryable?(error)
  classification = classify(error)
  classification == RETRYABLE_WITH_BACKOFF
end

.skip_to_next?(error) ⇒ Boolean

Determine if an error should skip to next provider without retry

Parameters:

  • error (Exception)

    The error to check

Returns:

  • (Boolean)

    True if should skip to next provider



86
87
88
89
# File 'lib/ace/llm/atoms/error_classifier.rb', line 86

def self.skip_to_next?(error)
  classification = classify(error)
  classification == SKIP_TO_NEXT
end