Class: Insika::ProviderErrorClassifier

Inherits:
Object
  • Object
show all
Defined in:
lib/insika/provider_error_classifier.rb

Overview

Classifies provider/transport failures by ACTION (B9). Four kinds — the hermes/openclaw structural rule: NON-retryable checked first, so an error we do not recognize defaults to :fatal (a retry would hammer a poisoned credential; a fatal is retried only after the operator fixes the cause):

:fatal              401/402/403/400 (auth, billing, permanent quota,
                  bad request, context too long) — retrying does not help.
:retryable          5xx/529/socket/timeout — the same call may succeed
                  moments later.
:rate_limited_short a 429 that says "back off briefly" (RPM-scale).
:rate_limited_long  a 429 with a long retry-after — quota-scale.

The classification is STRING-based (class names, no constant references): the core loads without ruby_llm, and the smoke-shim's fake RubyLLM is a drop-in. retry_after is read from the provider's own Retry-After header when the error carries a response, else a per-kind default.

Defined Under Namespace

Classes: Classification

Constant Summary collapse

KINDS =
%i[fatal retryable rate_limited_short rate_limited_long].freeze
SHORT_RETRY_LIMIT =

Above this a 429 means quota, not RPM (this is what tells the two apart — a short 429 wants a quick retry; a long one is a billing event).

60
DEFAULTS =

seconds

{
  retryable: 5,
  rate_limited_short: 10,
  rate_limited_long: 300
}.freeze
FATAL_ERROR_NAMES =

RubyLLM's taxonomy (error.rb), matched by class name so the core stays ruby_llm-free at load time.

%w[
  RubyLLM::ContextLengthExceededError RubyLLM::BadRequestError
  RubyLLM::UnauthorizedError RubyLLM::PaymentRequiredError RubyLLM::ForbiddenError
].freeze
RATE_LIMITED_ERROR_NAME =
"RubyLLM::RateLimitError"
RETRYABLE_ERROR_NAMES =
%w[
  RubyLLM::ServerError RubyLLM::ServiceUnavailableError RubyLLM::OverloadedError
].freeze
RUBY_LLM_ERROR_NAMES =
(
  FATAL_ERROR_NAMES + [RATE_LIMITED_ERROR_NAME] +
  RETRYABLE_ERROR_NAMES + ["RubyLLM::Error"]
).freeze
TRANSPORT_NAME_PATTERNS =

Transport failures while talking to the provider: connection refused/reset, DNS, TLS, timeouts (Faraday wraps its own names; the stdlib ones surface from raw sockets).

[
  /\AFaraday::/,
  /\ASocketError\z/,
  /\AIOError\z/,
  /\AErrno::/,
  /\ANet::(Read|Open)Timeout\z/,
  /\ATimeout::Error\z/,
  /\AOpenSSL::SSL::SSLError\z/
].freeze

Class Method Summary collapse

Class Method Details

.classify(error) ⇒ Object

-> Classification



72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
# File 'lib/insika/provider_error_classifier.rb', line 72

def classify(error)
  names = class_names(error)

  # Non-retryable first (the structural rule): a known fatal is NEVER
  # retried, and an unknown error defaults to fatal — never to retry.
  return fatal if (names & FATAL_ERROR_NAMES).any?

  return rate_limited(error) if names.include?(RATE_LIMITED_ERROR_NAME)

  return retryable if (names & RETRYABLE_ERROR_NAMES).any?
  return retryable if transport?(names)

  # A generic RubyLLM::Error (or a raw HTTP error) still carries the
  # status: 429 and 5xx are retryable regardless of the wrapping class.
  case http_status(error)
  when 429      then rate_limited(error)
  when 500..599 then retryable
  else fatal
  end
end

.provider_error?(error) ⇒ Boolean

True when the error came from the provider call itself (RubyLLM family or transport) — the executor routes these to the :ruby_llm stage with a wrapped classification instead of :unknown.

Returns:

  • (Boolean)


96
97
98
99
# File 'lib/insika/provider_error_classifier.rb', line 96

def provider_error?(error)
  names = class_names(error)
  (names & RUBY_LLM_ERROR_NAMES).any? || transport?(names)
end

.wrap(error) ⇒ Object

The typed ProviderError the executor stores and emits.



102
103
104
105
106
107
108
# File 'lib/insika/provider_error_classifier.rb', line 102

def wrap(error)
  c = classify(error)
  Insika::ProviderError.new(
    error.message || error.class.name,
    kind: c.kind, retryable: c.retryable, retry_after: c.retry_after
  )
end