Module: DataRedactor::Integrations::OpenAI

Defined in:
lib/data_redactor/integrations/openai.rb

Overview

Adapter for OpenAI Chat Completions payloads. Scrubs PII and secrets from a request’s ‘messages` before they leave the process, and from a response’s ‘choices[].message.content` before they’re logged or stored.

Operates on plain Ruby Hashes/Arrays with either String or Symbol keys, so it works with the ‘openai` gem, a raw HTTP client, or parsed JSON — no runtime dependency on any SDK. Inputs are never mutated; a deep copy is returned.

Examples:

Scrub a request before sending

require "data_redactor/integrations/openai"

messages = [{ role: "user", content: "my email is alice@example.com" }]
safe = DataRedactor::Integrations::OpenAI.redact_messages(messages)
client.chat(parameters: { model: "gpt-4o", messages: safe })

Scrub a response before logging

resp = client.chat(parameters: { ... })
logger.info DataRedactor::Integrations::OpenAI.redact_response(resp)

Class Method Summary collapse

Class Method Details

.redact_messages(messages, only: nil, except: nil, placeholder: DataRedactor::PLACEHOLDER_DEFAULT) ⇒ Array<Hash>, Hash

Redact an OpenAI ‘messages` array before sending the request. Returns a deep copy; the input is not mutated.

Each message’s ‘content` may be a String or an array of parts (`{ type: “text”, text: “…” }`); only the `text` field of `text` parts is redacted. Non-text parts (e.g. `image_url`) pass through untouched. A `{ role: “system”, content: … }` entry is redacted like any other message (OpenAI carries the system prompt in the array).

Examples:

OpenAI.redact_messages([{ role: "user", content: "ssn 123-45-6789" }])
#=> [{ role: "user", content: "ssn [REDACTED]" }]

Parameters:

  • messages (Array<Hash>, Hash)

    either a bare array of ‘{ role:, content: }` hashes, or a request Hash containing a `messages` key. Keys may be String or Symbol.

  • only (defaults to: nil)

    forwarded to DataRedactor.redact

  • except (defaults to: nil)

    forwarded to DataRedactor.redact

  • placeholder (defaults to: DataRedactor::PLACEHOLDER_DEFAULT)

    forwarded to DataRedactor.redact

Returns:

  • (Array<Hash>, Hash)

    a deep copy of the input with text leaves redacted; an Array if an Array was given, a Hash if a Hash was given.



48
49
50
51
52
53
54
55
56
57
58
59
# File 'lib/data_redactor/integrations/openai.rb', line 48

def redact_messages(messages, only: nil, except: nil, placeholder: DataRedactor::PLACEHOLDER_DEFAULT)
  redact = ->(s) { DataRedactor.redact(s, only: only, except: except, placeholder: placeholder) }

  if messages.is_a?(Hash)
    out = LLMSupport.deep_copy(messages)
    list = LLMSupport.fetch(out, :messages)
    LLMSupport.put(out, :messages, redact_message_list(list, redact)) if list.is_a?(Array)
    out
  else
    redact_message_list(LLMSupport.deep_copy(messages), redact)
  end
end

.redact_response(response, only: nil, except: nil, placeholder: DataRedactor::PLACEHOLDER_DEFAULT) ⇒ Hash

Redact an OpenAI Chat Completions response before logging or storing it. Returns a deep copy; the input is not mutated.

Walks ‘choices[].message.content` and redacts each (String content), leaving the rest of the response (id, usage, finish_reason) intact.

Examples:

OpenAI.redact_response(
  "choices" => [{ "message" => { "content" => "card 4111111111111111" } }]
)
#=> {"choices"=>[{"message"=>{"content"=>"card [REDACTED]"}}]}

Parameters:

  • response (Hash)

    a response Hash with a ‘choices` array, each choice carrying a `message` Hash with a `content` String. Keys may be String or Symbol.

  • only (defaults to: nil)

    forwarded to DataRedactor.redact

  • except (defaults to: nil)

    forwarded to DataRedactor.redact

  • placeholder (defaults to: DataRedactor::PLACEHOLDER_DEFAULT)

    forwarded to DataRedactor.redact

Returns:

  • (Hash)

    a deep copy of the response with message content redacted.



79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
# File 'lib/data_redactor/integrations/openai.rb', line 79

def redact_response(response, only: nil, except: nil, placeholder: DataRedactor::PLACEHOLDER_DEFAULT)
  redact = ->(s) { DataRedactor.redact(s, only: only, except: except, placeholder: placeholder) }
  out = LLMSupport.deep_copy(response)
  return out unless out.is_a?(Hash)

  choices = LLMSupport.fetch(out, :choices)
  return out unless choices.is_a?(Array)

  choices.each do |choice|
    next unless choice.is_a?(Hash)

    message = LLMSupport.fetch(choice, :message)
    next unless message.is_a?(Hash)

    content = LLMSupport.fetch(message, :content)
    LLMSupport.put(message, :content, redact.call(content)) if content.is_a?(String)
  end
  out
end