Module: OpenAICompatibleErrors::Headers

Defined in:
lib/openai_compatible_errors/headers.rb

Constant Summary collapse

MAX_RETRY_AFTER_MS =
86_400_000
MAX_HEADER_VALUE_CHARS =
512
REQUEST_ID_NAMES =
%w[x-request-id request-id x-correlation-id trace-id].freeze
RETRY_NAMES =
%w[retry-after retry-after-ms x-retry-after-ms].freeze

Class Method Summary collapse

Class Method Details

.materialize(input) ⇒ Object



14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
# File 'lib/openai_compatible_errors/headers.rb', line 14

def materialize(input)
  pairs = []
  if input.is_a?(Hash)
    input.each_pair { |key, value| pairs << [key, value] }
  elsif input.respond_to?(:each_header)
    begin
      input.each_header { |key, value| pairs << [key, value] }
    rescue StandardError
      pairs = []
    end
  elsif input.respond_to?(:each_pair)
    begin
      input.each_pair { |key, value| pairs << [key, value] }
    rescue StandardError
      pairs = []
    end
  end

  result = Hash.new { |hash, key| hash[key] = [] }
  pairs.each do |key, value|
    name = key.to_s.strip.downcase
    next unless name.match?(/\A[a-z0-9][a-z0-9_-]{0,127}\z/)

    values = value.is_a?(Array) ? value : [value]
    values.each do |item|
      text = item.is_a?(String) ? item : item.to_s
      next if text.empty? || text.bytesize > MAX_HEADER_VALUE_CHARS

      result[name] << text
    end
  end
  result
end

.parse_milliseconds(value) ⇒ Object



91
92
93
94
95
# File 'lib/openai_compatible_errors/headers.rb', line 91

def parse_milliseconds(value)
  return nil unless value.match?(/\A\d{1,12}\z/)

  [value.to_i, MAX_RETRY_AFTER_MS].min
end

.parse_retry_after(value, now:) ⇒ Object



97
98
99
100
101
102
103
104
105
106
107
108
109
# File 'lib/openai_compatible_errors/headers.rb', line 97

def parse_retry_after(value, now:)
  if value.match?(/\A\d{1,8}\z/)
    return [value.to_i * 1_000, MAX_RETRY_AFTER_MS].min
  end
  return nil unless value.bytesize <= 128

  begin
    seconds = Time.httpdate(value).to_f - now.to_f
    return [[(seconds * 1_000).ceil, 0].max, MAX_RETRY_AFTER_MS].min
  rescue ArgumentError
    nil
  end
end

.request_id(input) ⇒ Object



52
53
54
55
56
57
58
59
60
61
62
63
64
# File 'lib/openai_compatible_errors/headers.rb', line 52

def request_id(input)
  headers = materialize(input)
  REQUEST_ID_NAMES.each do |name|
    headers[name].each do |candidate|
      value = candidate.strip
      next unless value.match?(/\A[A-Za-z0-9][A-Za-z0-9._:\/-]{0,255}\z/)
      next if value.match?(/bearer|api[_ -]?key|secret|token/i)

      return value
    end
  end
  nil
end

.retry_after_ms(input, now: Time.now) ⇒ Object

Returns a bounded delay. A malformed present hint returns the maximum sentinel so a conservative retry policy fails closed.



68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
# File 'lib/openai_compatible_errors/headers.rb', line 68

def retry_after_ms(input, now: Time.now)
  headers = materialize(input)
  candidates = []
  malformed = false
  RETRY_NAMES.each do |name|
    headers[name].each do |value|
      parsed =
        if name.end_with?("-ms")
          parse_milliseconds(value)
        else
          parse_retry_after(value, now: now)
        end
      if parsed.nil?
        malformed = true
      else
        candidates << parsed
      end
    end
  end
  return candidates.max unless candidates.empty?
  malformed ? MAX_RETRY_AFTER_MS : nil
end

.values(input, name) ⇒ Object



48
49
50
# File 'lib/openai_compatible_errors/headers.rb', line 48

def values(input, name)
  materialize(input)[name.to_s.downcase]
end