Module: DataRedactor::Integrations::RubyLLM
- Defined in:
- lib/data_redactor/integrations/ruby_llm.rb
Overview
Outbound redaction for the ruby_llm gem (crmne/ruby_llm), built on
RubyLLM's public request hook — nothing here monkeypatches RubyLLM.
chat.before_request { |payload| ... } (ruby_llm 2.0+) hands a callback the
fully rendered request payload — after all RubyLLM formatting and
with_provider_options merging, immediately before it is posted — and
discards the callback's return value, so hooks edit the payload in place.
RubyLLM.hook builds such a callback; RubyLLM.chat and RubyLLM.attach! register it for you.
Because the payload is walked with DataRedactor.redact_deep!, this scrubs every String leaf of the request: the user prompt, the system prompt, the conversation history, tool definitions, and — the case per-call redaction cannot reach — tool results. An agent that reads a file or runs a command feeds that output back as a tool message, and it is inlined as a String in the next request's payload; the user never typed it, so only a request hook sees it before it leaves.
Redaction is applied to the rendered payload and nothing is persisted, so the conversation you keep is untouched: this scrubs the wire, per request.
Limits, stated plainly
- Per chat. RubyLLM has no global callback registry, so a chat you did not build with RubyLLM.chat or pass to RubyLLM.attach! is not redacted.
- Chat only. Embeddings, moderation, image generation and transcription do not run request hooks.
- Base64 attachments (PDFs, images, audio inlined as base64) and URL-referenced files are not redacted — the bytes are encoded or remote, so patterns cannot see into them.
- A digits-only tool-call id would be redacted, breaking the
tool_use_idcorrelation a provider validates:_counts as a boundary, sotoolu_012345678matches a national-ID pattern. Real Anthropic and OpenAI ids are mixed alphanumeric and unaffected; add:idtoskip_keys:if that ever changes. - Redacting tool results can break an agent that needs a value a tool
returned. Scope with
only:/except:when that matters.
Constant Summary collapse
- SUPPORTED_VERSION =
The
ruby_llmline that exposesChat#before_request. 1.x has no request hook at all — redact per call with DataRedactor.redact there. ">= 2.0.0.pre"- DEFAULT_SKIP_KEYS =
Payload keys left untouched by default.
modelis structural, not content: dated model ids carry an eight-digit suffix (claude-haiku-4-5-20251001), which the national-ID patterns match, and a redacted model id is rejected by the provider. Nothing else is skipped by default — an unlisted key is always redacted, so a field we have never seen cannot leak silently. [:model].freeze
Class Method Summary collapse
-
.attach!(target, only: nil, except: nil, placeholder: DataRedactor::PLACEHOLDER_DEFAULT, skip_keys: DEFAULT_SKIP_KEYS) ⇒ Object
Register RubyLLM.hook on a chat you did not build.
-
.chat(only: nil, except: nil, placeholder: DataRedactor::PLACEHOLDER_DEFAULT, skip_keys: DEFAULT_SKIP_KEYS, **kwargs, &block) ⇒ RubyLLM::Chat
Build a chat with redaction already attached.
-
.hook(only: nil, except: nil, placeholder: DataRedactor::PLACEHOLDER_DEFAULT, skip_keys: DEFAULT_SKIP_KEYS) ⇒ Proc
Build the
before_requestcallback. -
.install!(*_args, **_kwargs) ⇒ Object
deprecated
Deprecated.
Transparent app-wide mode is gone. It prepended
RubyLLM::Protocol#render; ruby_llm 2.0 exposes a public request hook, so redaction no longer patches RubyLLM. Use RubyLLM.chat or RubyLLM.attach!. This method exists only to say so and will be deleted in the next minor.
Class Method Details
.attach!(target, only: nil, except: nil, placeholder: DataRedactor::PLACEHOLDER_DEFAULT, skip_keys: DEFAULT_SKIP_KEYS) ⇒ Object
Register hook on a chat you did not build.
Accepts whatever holds the chat, so callers need not know which object
carries the hook: a RubyLLM::Chat, a RubyLLM::Agent, or an
acts_as_chat record (its #to_llm chat is memoized, so one call covers
the record). The agent's wrapped #chat is always used rather than its
delegated before_request, because in Rails mode that delegator forwards
to a record which has no such method.
119 120 121 122 123 124 125 |
# File 'lib/data_redactor/integrations/ruby_llm.rb', line 119 def attach!(target, only: nil, except: nil, placeholder: DataRedactor::PLACEHOLDER_DEFAULT, skip_keys: DEFAULT_SKIP_KEYS) resolve(target).before_request( &hook(only: only, except: except, placeholder: placeholder, skip_keys: skip_keys) ) target end |
.chat(only: nil, except: nil, placeholder: DataRedactor::PLACEHOLDER_DEFAULT, skip_keys: DEFAULT_SKIP_KEYS, **kwargs, &block) ⇒ RubyLLM::Chat
Build a chat with redaction already attached.
A drop-in for RubyLLM.chat: every argument is forwarded to it
untouched, and the returned chat is the real thing, so the fluent API
keeps working (.with_temperature(0.2).ask(...)).
93 94 95 96 97 |
# File 'lib/data_redactor/integrations/ruby_llm.rb', line 93 def chat(only: nil, except: nil, placeholder: DataRedactor::PLACEHOLDER_DEFAULT, skip_keys: DEFAULT_SKIP_KEYS, **kwargs, &block) attach!(::RubyLLM.chat(**kwargs, &block), only: only, except: except, placeholder: placeholder, skip_keys: skip_keys) end |
.hook(only: nil, except: nil, placeholder: DataRedactor::PLACEHOLDER_DEFAULT, skip_keys: DEFAULT_SKIP_KEYS) ⇒ Proc
Build the before_request callback.
It redacts the payload in place, which is what RubyLLM's contract requires: hooks mutate what they are given and their return value is discarded.
143 144 145 146 147 148 149 |
# File 'lib/data_redactor/integrations/ruby_llm.rb', line 143 def hook(only: nil, except: nil, placeholder: DataRedactor::PLACEHOLDER_DEFAULT, skip_keys: DEFAULT_SKIP_KEYS) lambda do |payload| DataRedactor.redact_deep!(payload, only: only, except: except, placeholder: placeholder, skip_keys: skip_keys) end end |
.install!(*_args, **_kwargs) ⇒ Object
156 157 158 159 160 161 |
# File 'lib/data_redactor/integrations/ruby_llm.rb', line 156 def install!(*_args, **_kwargs) raise "DataRedactor::Integrations::RubyLLM.install! has been removed: ruby_llm 2.0 exposes a " \ "public request hook, so redaction no longer patches RubyLLM. Build chats with " \ "DataRedactor::Integrations::RubyLLM.chat(...), or attach to an existing chat, agent, " \ "or record with DataRedactor::Integrations::RubyLLM.attach!(target)." end |