Class: SavvyOpenrouter::ApiCallLogger

Inherits:
Object
  • Object
show all
Defined in:
lib/savvy_openrouter/api_call_logger.rb

Overview

Persists OpenRouter HTTP exchanges when configured via api_call_log (YAML or Client kwargs). Failures while saving never raise into application code.

Column map (columns hash): every source_key => db_column entry is a whitelist. If attrs includes source_key, its value is copied to the row (after optional coercion). This allows app-specific passthrough keys (e.g. bill_forward_event_id) without extending the gem enum.

Documented source keys populated by Connection/resources: method, path, status, http_status, duration_ms, request_body, response_body, error_class, error_message, streaming, endpoint, logical_model, generation_id, success, cost, usage, request_json, response_json.

Constant Summary collapse

DEFAULT_MAX_BODY_BYTES =
65_536
RESERVED_CONFIG_KEYS =

Reserved keys in api_call_log YAML — never treated as column sources.

%w[model columns max_body_bytes chat_attempts responses_attempts].freeze
CANONICAL_KEYS =

Keys the gem may set automatically (subset); full set is any key allowed in columns.

%w[
  method path status http_status duration_ms request_body response_body error_class error_message streaming
  endpoint logical_model generation_id success cost usage request_json response_json
].freeze

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(config) ⇒ ApiCallLogger

Returns a new instance of ApiCallLogger.



113
114
115
# File 'lib/savvy_openrouter/api_call_logger.rb', line 113

def initialize(config)
  @config = config.is_a?(Hash) ? Configuration.stringify_keys_static(config) : {}
end

Class Method Details

.blank_to_nil(raw) ⇒ Object



55
56
57
58
# File 'lib/savvy_openrouter/api_call_logger.rb', line 55

def blank_to_nil(raw)
  t = raw.to_s.strip
  t.empty? ? nil : t
end

.cost_from_usage(usage) ⇒ Object



85
86
87
88
89
90
91
92
93
94
95
# File 'lib/savvy_openrouter/api_call_logger.rb', line 85

def cost_from_usage(usage)
  return unless usage.is_a?(Hash)

  u = usage.transform_keys(&:to_s)
  v = u["cost"] || u["total_cost"]
  return if v.nil?

  BigDecimal(v.to_s)
rescue ArgumentError
  nil
end

.error_message_from_json_string(str) ⇒ Object



73
74
75
76
77
78
79
80
81
82
83
# File 'lib/savvy_openrouter/api_call_logger.rb', line 73

def error_message_from_json_string(str)
  return unless str.is_a?(String)

  stripped = str.strip
  return unless stripped.start_with?("{", "[")

  parsed = JSON.parse(stripped, symbolize_names: true)
  error_message_from_response_body(parsed) if parsed.is_a?(Hash)
rescue JSON::ParserError
  nil
end

.error_message_from_response_body(body) ⇒ Object

Best-effort OpenRouter-style error.message for JSON error bodies (symbol or string keys).



61
62
63
64
65
66
67
68
69
70
71
# File 'lib/savvy_openrouter/api_call_logger.rb', line 61

def error_message_from_response_body(body)
  return unless body.is_a?(Hash)

  err = body[:error] || body["error"]
  case err
  when Hash
    blank_to_nil((err[:message] || err["message"]).to_s)
  when String
    blank_to_nil(err)
  end
end

.format_body_for_log(obj, max_bytes: DEFAULT_MAX_BODY_BYTES) ⇒ Object



31
32
33
34
35
36
37
38
39
40
41
# File 'lib/savvy_openrouter/api_call_logger.rb', line 31

def format_body_for_log(obj, max_bytes: DEFAULT_MAX_BODY_BYTES)
  str =
    case obj
    when nil then +""
    when String then obj.b
    else
      JSON.generate(obj)
    end
  str = redact_secrets(str)
  truncate_bytes(str, max_bytes)
end

.generation_id_from(response:, parsed_body:) ⇒ Object



43
44
45
46
47
48
49
50
51
52
53
# File 'lib/savvy_openrouter/api_call_logger.rb', line 43

def generation_id_from(response:, parsed_body:)
  h = response.respond_to?(:headers) ? response.headers : nil
  if h
    raw = h["x-generation-id"] || h["X-Generation-Id"] || h["X-GENERATION-ID"]
    gid = blank_to_nil(raw&.to_s)
    return gid if gid
  end
  return unless parsed_body.is_a?(Hash)

  blank_to_nil((parsed_body[:id] || parsed_body["id"]).to_s)
end

Instance Method Details

#chat_attempts_final?Boolean

Returns:

  • (Boolean)


128
129
130
# File 'lib/savvy_openrouter/api_call_logger.rb', line 128

def chat_attempts_final?
  @config["chat_attempts"].to_s == "final"
end

#enabled?Boolean

Returns:

  • (Boolean)


117
118
119
120
121
# File 'lib/savvy_openrouter/api_call_logger.rb', line 117

def enabled?
  m = @config["model"]
  !m.nil? && !m.to_s.strip.empty? &&
    @config["columns"].is_a?(Hash) && !@config["columns"].empty?
end

#max_body_limitObject



123
124
125
126
# File 'lib/savvy_openrouter/api_call_logger.rb', line 123

def max_body_limit
  n = @config["max_body_bytes"]
  n.is_a?(Integer) && n.positive? ? n : DEFAULT_MAX_BODY_BYTES
end

#record(attrs) ⇒ Object

attrs string-keyed hashes; column mapping selects and renames fields for create!.



137
138
139
140
141
142
143
144
145
146
147
# File 'lib/savvy_openrouter/api_call_logger.rb', line 137

def record(attrs)
  return unless enabled?

  row = build_row(attrs)
  return if row.empty?

  constantize_model(@config["model"].to_s.strip).create!(row)
rescue StandardError => e
  warn "[savvy_openrouter] api_call_log skipped: #{e.class}: #{e.message}" if $VERBOSE
  nil
end

#responses_attempts_final?Boolean

Returns:

  • (Boolean)


132
133
134
# File 'lib/savvy_openrouter/api_call_logger.rb', line 132

def responses_attempts_final?
  @config["responses_attempts"].to_s == "final"
end