Module: PWN::AI::HttpRetry
- Defined in:
- lib/pwn/ai/http_retry.rb
Overview
Shared REST timeout / 429 / ReadTimeout policy for every AI provider. Default wall clock per attempt is 180s with up to 5 attempts (≈900s total). Short quiet sidecar hops stay single-shot.
Constant Summary collapse
- DEFAULT_TIMEOUT_S =
180- DEFAULT_MAX_ATTEMPTS =
5
Class Method Summary collapse
- .authors ⇒ Object
- .help ⇒ Object
- .max_attempts(opts = {}) ⇒ Object
- .quota_exhausted?(opts = {}) ⇒ Boolean
- .quota_message(opts = {}) ⇒ Object
-
.report_event(opts = {}) ⇒ Object
Tees provider REST events into the open pwn-ai DEBUG RN log and STDERR.
- .retry_after_s(opts = {}) ⇒ Object
- .retryable?(opts = {}) ⇒ Boolean
- .timeout_s(opts = {}) ⇒ Object
Class Method Details
.authors ⇒ Object
101 102 103 |
# File 'lib/pwn/ai/http_retry.rb', line 101 public_class_method def self. "AUTHOR(S):\n 0day Inc. <[email protected]>\n" end |
.help ⇒ Object
105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 |
# File 'lib/pwn/ai/http_retry.rb', line 105 public_class_method def self.help puts "USAGE: # Run timeout s and return its result #{self}.timeout_s( timeout: 'optional - seconds to wait before giving up' ) # Run max attempts and return its result #{self}.max_attempts( max_attempts: 'optional - max attempts value consumed by #max_attempts', quiet: 'optional - quiet value consumed by #max_attempts', timeout: 'optional - seconds to wait before giving up' ) # Run retryable and return its result #{self}.retryable?( error: 'optional - error value consumed by #retryable?', message: 'required - message value consumed by #retryable?' ) # Seconds to sleep after a 429 (Retry-After, else exponential 2..60). #{self}.retry_after_s( retry_count: 'optional - 1-based attempt used for exponential fallback', response: 'optional - RestClient response with Retry-After header' ) # True when a 429 body is billing/quota, not a retryable rate limit. #{self}.quota_exhausted?( error: 'optional - exception whose message/response body is inspected', message: 'optional - raw error text when error is omitted' ) # Operator-facing line for a billing/quota 429 (do not retry). #{self}.quota_message( error: 'optional - exception whose response body may contain the billing URL', message: 'optional - raw error text when error is omitted' ) # Tees provider REST events into the open pwn-ai DEBUG RN log and STDERR #{self}.report_event( label: 'required - label value consumed by #report_event', error: 'optional - error value consumed by #report_event', extra: 'optional - extra value consumed by #report_event', http_method: 'optional - http method value consumed by #report_event', rest_call: 'optional - rest call value consumed by #report_event', which_self: 'optional - which self value consumed by #report_event (defaults to self)', quiet: 'optional - quiet value consumed by #report_event' ) # Print the AUTHOR(S) string for this module. #{self}.authors " constants.sort end |
.max_attempts(opts = {}) ⇒ Object
19 20 21 22 23 24 25 26 27 28 |
# File 'lib/pwn/ai/http_retry.rb', line 19 public_class_method def self.max_attempts(opts = {}) return 1 unless opts.is_a?(Hash) n = opts[:max_attempts].to_i return n if n.positive? return 1 if opts[:quiet] return 1 if opts[:timeout].to_i.positive? && opts[:timeout].to_i < DEFAULT_TIMEOUT_S DEFAULT_MAX_ATTEMPTS end |
.quota_exhausted?(opts = {}) ⇒ Boolean
53 54 55 56 57 58 59 60 61 62 63 64 |
# File 'lib/pwn/ai/http_retry.rb', line 53 public_class_method def self.quota_exhausted?(opts = {}) err = opts[:error] blob = err.respond_to?(:message) ? err..to_s : err.to_s blob = opts[:message].to_s if blob.empty? if err.respond_to?(:response) && err.response blob = "#{blob} #{err.response}" blob = "#{blob} #{err.response.body}" if err.response.respond_to?(:body) end blob.match?(/insufficient_quota|credit_balance_exhausted|exceeded your current quota|billing_not_active|spend.?limit|no credits remaining/i) rescue StandardError false end |
.quota_message(opts = {}) ⇒ Object
66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 |
# File 'lib/pwn/ai/http_retry.rb', line 66 public_class_method def self.(opts = {}) err = opts[:error] blob = '' blob = err..to_s if err.respond_to?(:message) if err.respond_to?(:response) && err.response blob = "#{blob} #{err.response}" blob = "#{blob} #{err.response.body}" if err.response.respond_to?(:body) end blob = opts[:message].to_s if blob.strip.empty? url = blob[%r{https://platform\.openai\.com/settings/organization/billing/?}] || 'https://platform.openai.com/settings/organization/billing/' 'OpenAI API has no credits (insufficient_quota / credit_balance_exhausted). ' \ "Add prepaid API credits at #{url} — a ChatGPT Plus/Pro plan does not fund api.openai.com. " \ 'Then retry; /model openai stays selected.' end |
.report_event(opts = {}) ⇒ Object
Tees provider REST events into the open pwn-ai DEBUG RN log and STDERR.
83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 |
# File 'lib/pwn/ai/http_retry.rb', line 83 public_class_method def self.report_event(opts = {}) return unless opts.is_a?(Hash) label = opts[:label].to_s label = 'ai' if label.empty? err = opts[:error] extra = opts[:extra].to_s meth = opts[:http_method].to_s.upcase call = opts[:rest_call].to_s klass = err ? err.class : 'Error' msg = err ? err. : extra line = "[pwn-ai/#{label}] #{klass}: #{msg} (#{meth} #{call} #{extra})".strip which = opts[:which_self] || self PWN::Plugins::Log.progress(msg: line, which_self: which, cap: 0) if defined?(PWN::Plugins::Log) && PWN::Plugins::Log.debug_enabled? warn line unless opts[:quiet] line end |
.retry_after_s(opts = {}) ⇒ Object
39 40 41 42 43 44 45 46 47 48 49 50 51 |
# File 'lib/pwn/ai/http_retry.rb', line 39 public_class_method def self.retry_after_s(opts = {}) retry_count = [opts[:retry_count].to_i, 1].max headers = {} resp = opts[:response] headers = resp.headers if resp.respond_to?(:headers) && resp.headers ra = headers[:retry_after] || headers['retry-after'] || headers['Retry-After'] if ra.to_s.match?(/\A\d+(\.\d+)?\z/) n = ra.to_f return n if n.positive? end [2**retry_count, 60].min.to_f end |
.retryable?(opts = {}) ⇒ Boolean
30 31 32 33 34 35 36 37 |
# File 'lib/pwn/ai/http_retry.rb', line 30 public_class_method def self.retryable?(opts = {}) err = opts[:error] msg = err.respond_to?(:message) ? err..to_s : err.to_s msg = opts[:message].to_s if msg.empty? msg.match?(/HTTP 50[234]|Gateway Time-out|stream absolute timeout|ReadTimeout|Net::ReadTimeout|Timed out reading/i) rescue StandardError false end |
.timeout_s(opts = {}) ⇒ Object
12 13 14 15 16 17 |
# File 'lib/pwn/ai/http_retry.rb', line 12 public_class_method def self.timeout_s(opts = {}) return DEFAULT_TIMEOUT_S unless opts.is_a?(Hash) t = opts[:timeout].to_i t.positive? ? t : DEFAULT_TIMEOUT_S end |