Module: SavvyOpenrouter::RequestPlugins

Defined in:
lib/savvy_openrouter/request_plugins.rb

Overview

Optional chat / Responses request shaping (OpenRouter plugins). Pure Ruby.

require "savvy_openrouter/request_plugins"
SavvyOpenrouter::RequestPlugins.prepare_chat_body!(body, pdf_engine: "native")

Constant Summary collapse

RESPONSE_HEALING_PLUGIN =
{ id: "response-healing" }.freeze
FILE_PARSER_DEFAULT_ENGINE =
"cloudflare-ai"

Class Method Summary collapse

Class Method Details

.chat_messages_include_pdf_attachment?(messages) ⇒ Boolean

Returns:

  • (Boolean)


94
95
96
97
98
# File 'lib/savvy_openrouter/request_plugins.rb', line 94

def chat_messages_include_pdf_attachment?(messages)
  return false unless messages.is_a?(Array)

  messages.any? { |msg| message_includes_pdf_file?(msg) }
end

.content_part_is_pdf_file?(part) ⇒ Boolean

Returns:

  • (Boolean)


109
110
111
112
113
114
115
116
117
118
119
# File 'lib/savvy_openrouter/request_plugins.rb', line 109

def content_part_is_pdf_file?(part)
  return false unless part.is_a?(Hash)

  type = (part[:type] || part["type"]).to_s
  return false unless type == "file"

  file = part[:file] || part["file"]
  return false unless file.is_a?(Hash)

  pdf_file_attachment?(file)
end

.ensure_pdf_file_parser_plugin!(body, pdf_engine: nil) ⇒ Object

pdf_engine — optional override (e.g. from config: “native” for Gemini, “mistral-ocr” for scans). When nil, uses FILE_PARSER_DEFAULT_ENGINE.



59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
# File 'lib/savvy_openrouter/request_plugins.rb', line 59

def ensure_pdf_file_parser_plugin!(body, pdf_engine: nil)
  messages = body[:messages] || body["messages"]
  return unless chat_messages_include_pdf_attachment?(messages)

  engine = normalize_pdf_engine(pdf_engine)
  plugins = plugins_array_from(body)
  idx = plugins.find_index { |p| plugin_entry_id(p) == "file-parser" }

  if idx
    return if file_parser_pdf_engine_set?(plugins[idx])

    fp = plugins[idx].dup
    pdf_src = fp[:pdf] || fp["pdf"]
    merged_pdf =
      if pdf_src.is_a?(Hash)
        pdf_src.transform_keys(&:to_sym)
      else
        {}
      end
    merged_pdf[:engine] = engine
    fp[:pdf] = merged_pdf
    fp.delete("pdf")
    plugins[idx] = fp
    body[:plugins] = plugins
  else
    body[:plugins] = plugins + [{ id: "file-parser", pdf: { engine: engine } }]
  end
  body.delete("plugins")
end

.ensure_response_healing_for_chat!(body) ⇒ Object



32
33
34
35
36
37
38
# File 'lib/savvy_openrouter/request_plugins.rb', line 32

def ensure_response_healing_for_chat!(body)
  plugins_arr = plugins_array_from(body)
  return if response_healing_plugin_present?(plugins_arr)

  body[:plugins] = plugins_arr + [RESPONSE_HEALING_PLUGIN.dup]
  body.delete("plugins")
end

.ensure_response_healing_for_responses!(body) ⇒ Object



40
41
42
43
44
45
46
# File 'lib/savvy_openrouter/request_plugins.rb', line 40

def ensure_response_healing_for_responses!(body)
  plugins_arr = plugins_array_from(body)
  return if response_healing_plugin_present?(plugins_arr)

  body[:plugins] = plugins_arr + [RESPONSE_HEALING_PLUGIN.dup]
  body.delete("plugins")
end

.file_parser_pdf_engine_set?(plugin) ⇒ Boolean

Returns:

  • (Boolean)


142
143
144
145
146
147
148
149
150
# File 'lib/savvy_openrouter/request_plugins.rb', line 142

def file_parser_pdf_engine_set?(plugin)
  return false unless plugin.is_a?(Hash)

  pdf = plugin[:pdf] || plugin["pdf"]
  return false unless pdf.is_a?(Hash)

  eng = (pdf[:engine] || pdf["engine"]).to_s
  !eng.strip.empty?
end

.message_includes_pdf_file?(msg) ⇒ Boolean

Returns:

  • (Boolean)


100
101
102
103
104
105
106
107
# File 'lib/savvy_openrouter/request_plugins.rb', line 100

def message_includes_pdf_file?(msg)
  return false unless msg.is_a?(Hash)

  content = msg[:content] || msg["content"]
  return false unless content.is_a?(Array)

  content.any? { |part| content_part_is_pdf_file?(part) }
end

.normalize_pdf_engine(pdf_engine) ⇒ Object



89
90
91
92
# File 'lib/savvy_openrouter/request_plugins.rb', line 89

def normalize_pdf_engine(pdf_engine)
  s = pdf_engine.to_s.strip
  s.empty? ? FILE_PARSER_DEFAULT_ENGINE : s
end

.pdf_file_attachment?(file) ⇒ Boolean

Returns:

  • (Boolean)


121
122
123
124
125
126
127
128
129
130
131
# File 'lib/savvy_openrouter/request_plugins.rb', line 121

def pdf_file_attachment?(file)
  name = (file[:filename] || file["filename"]).to_s
  return true if name.match?(/\.pdf\z/i)

  data = file[:file_data] || file["file_data"] || file[:fileData] || file["fileData"]
  return false unless data.is_a?(String)

  return true if data.match?(%r{\Adata:application/pdf(?:;|\z)}i)

  pdf_url?(data)
end

.pdf_url?(url) ⇒ Boolean

Returns:

  • (Boolean)


133
134
135
136
137
138
139
140
# File 'lib/savvy_openrouter/request_plugins.rb', line 133

def pdf_url?(url)
  return false unless url.match?(%r{\Ahttps?://}i)

  uri = URI.parse(url)
  uri.path.to_s.match?(/\.pdf\z/i)
rescue URI::InvalidURIError
  false
end

.plugin_entry_id(entry) ⇒ Object



158
159
160
161
162
# File 'lib/savvy_openrouter/request_plugins.rb', line 158

def plugin_entry_id(entry)
  return unless entry.is_a?(Hash)

  (entry[:id] || entry["id"]).to_s
end

.plugin_present?(plugins, id:) ⇒ Boolean

Returns:

  • (Boolean)


152
153
154
155
156
# File 'lib/savvy_openrouter/request_plugins.rb', line 152

def plugin_present?(plugins, id:)
  return false unless plugins.is_a?(Array)

  plugins.any? { |p| plugin_entry_id(p) == id.to_s }
end

.plugins_array_from(body) ⇒ Object



48
49
50
51
# File 'lib/savvy_openrouter/request_plugins.rb', line 48

def plugins_array_from(body)
  p = body[:plugins] || body["plugins"]
  p.is_a?(Array) ? p.dup : []
end

.prepare_chat_body!(body, pdf_engine: nil) ⇒ Object



17
18
19
20
21
22
23
# File 'lib/savvy_openrouter/request_plugins.rb', line 17

def prepare_chat_body!(body, pdf_engine: nil)
  return body unless body.is_a?(Hash)

  ensure_response_healing_for_chat!(body) if SavvyOpenrouter::Patterns.chat_structured_requested?(body)
  ensure_pdf_file_parser_plugin!(body, pdf_engine: pdf_engine)
  body
end

.prepare_responses_body!(body) ⇒ Object



25
26
27
28
29
30
# File 'lib/savvy_openrouter/request_plugins.rb', line 25

def prepare_responses_body!(body)
  return body unless body.is_a?(Hash)

  ensure_response_healing_for_responses!(body) if SavvyOpenrouter::Patterns.responses_structured_requested?(body)
  body
end

.response_healing_plugin_present?(plugins) ⇒ Boolean

Returns:

  • (Boolean)


53
54
55
# File 'lib/savvy_openrouter/request_plugins.rb', line 53

def response_healing_plugin_present?(plugins)
  plugin_present?(plugins, id: "response-healing")
end