open_router_enhanced 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.env.example +1 -0
- data/.rspec +3 -0
- data/.rubocop.yml +13 -0
- data/.rubocop_todo.yml +130 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +41 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/CONTRIBUTING.md +384 -0
- data/Gemfile +22 -0
- data/Gemfile.lock +138 -0
- data/LICENSE.txt +21 -0
- data/MIGRATION.md +556 -0
- data/README.md +1660 -0
- data/Rakefile +334 -0
- data/SECURITY.md +150 -0
- data/VCR_CONFIGURATION.md +80 -0
- data/docs/model_selection.md +637 -0
- data/docs/observability.md +430 -0
- data/docs/prompt_templates.md +422 -0
- data/docs/streaming.md +467 -0
- data/docs/structured_outputs.md +466 -0
- data/docs/tools.md +1016 -0
- data/examples/basic_completion.rb +122 -0
- data/examples/model_selection_example.rb +141 -0
- data/examples/observability_example.rb +199 -0
- data/examples/prompt_template_example.rb +184 -0
- data/examples/smart_completion_example.rb +89 -0
- data/examples/streaming_example.rb +176 -0
- data/examples/structured_outputs_example.rb +191 -0
- data/examples/tool_calling_example.rb +149 -0
- data/lib/open_router/client.rb +552 -0
- data/lib/open_router/http.rb +118 -0
- data/lib/open_router/json_healer.rb +263 -0
- data/lib/open_router/model_registry.rb +378 -0
- data/lib/open_router/model_selector.rb +462 -0
- data/lib/open_router/prompt_template.rb +290 -0
- data/lib/open_router/response.rb +371 -0
- data/lib/open_router/schema.rb +288 -0
- data/lib/open_router/streaming_client.rb +210 -0
- data/lib/open_router/tool.rb +221 -0
- data/lib/open_router/tool_call.rb +180 -0
- data/lib/open_router/usage_tracker.rb +277 -0
- data/lib/open_router/version.rb +5 -0
- data/lib/open_router.rb +123 -0
- data/sig/open_router.rbs +20 -0
- metadata +186 -0
data/Rakefile
ADDED
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "bundler/gem_tasks"
|
|
4
|
+
require "rspec/core/rake_task"
|
|
5
|
+
|
|
6
|
+
# Unit tests only (excludes VCR integration tests)
|
|
7
|
+
RSpec::Core::RakeTask.new(:spec) do |t|
|
|
8
|
+
t.exclude_pattern = "spec/vcr/**/*_spec.rb"
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# VCR integration tests
|
|
12
|
+
RSpec::Core::RakeTask.new(:spec_vcr) do |t|
|
|
13
|
+
t.pattern = "spec/vcr/**/*_spec.rb"
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# All tests (unit + VCR)
|
|
17
|
+
RSpec::Core::RakeTask.new(:spec_all) do |t|
|
|
18
|
+
# Run all specs
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
require "rubocop/rake_task"
|
|
22
|
+
|
|
23
|
+
RuboCop::RakeTask.new
|
|
24
|
+
|
|
25
|
+
# Default task runs unit tests + rubocop (fast feedback)
|
|
26
|
+
task default: %i[spec rubocop]
|
|
27
|
+
|
|
28
|
+
# Full CI task runs everything
|
|
29
|
+
task ci: %i[spec_all rubocop]
|
|
30
|
+
|
|
31
|
+
# Model exploration tasks
|
|
32
|
+
namespace :models do
|
|
33
|
+
desc "Display summary of available models"
|
|
34
|
+
task :summary do
|
|
35
|
+
require_relative "lib/open_router"
|
|
36
|
+
|
|
37
|
+
puts "\n🤖 OpenRouter Model Registry Summary"
|
|
38
|
+
puts "=" * 80
|
|
39
|
+
|
|
40
|
+
models = OpenRouter::ModelRegistry.all_models
|
|
41
|
+
|
|
42
|
+
# Overall stats
|
|
43
|
+
puts "\n📊 Overall Statistics:"
|
|
44
|
+
puts " Total models: #{models.size}"
|
|
45
|
+
|
|
46
|
+
# Provider breakdown
|
|
47
|
+
providers = models.keys.group_by { |id| id.split("/").first }
|
|
48
|
+
puts "\n🏢 Models by Provider:"
|
|
49
|
+
providers.sort_by { |_, models| -models.size }.each do |provider, provider_models|
|
|
50
|
+
puts " #{provider.ljust(20)} #{provider_models.size} models"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Capabilities breakdown
|
|
54
|
+
all_capabilities = models.values.flat_map { |spec| spec[:capabilities] }.uniq.sort
|
|
55
|
+
puts "\n⚡ Available Capabilities:"
|
|
56
|
+
all_capabilities.each do |cap|
|
|
57
|
+
count = models.values.count { |spec| spec[:capabilities].include?(cap) }
|
|
58
|
+
puts " #{cap.to_s.ljust(25)} #{count} models"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Cost analysis
|
|
62
|
+
input_costs = models.values.map { |spec| spec[:cost_per_1k_tokens][:input] }.compact.sort
|
|
63
|
+
output_costs = models.values.map { |spec| spec[:cost_per_1k_tokens][:output] }.compact.sort
|
|
64
|
+
|
|
65
|
+
puts "\n💰 Cost Analysis (per 1k tokens):"
|
|
66
|
+
puts " Input tokens:"
|
|
67
|
+
puts " Min: $#{format("%.6f", input_costs.min)}"
|
|
68
|
+
puts " Max: $#{format("%.6f", input_costs.max)}"
|
|
69
|
+
puts " Median: $#{format("%.6f", input_costs[input_costs.size / 2])}"
|
|
70
|
+
puts " Output tokens:"
|
|
71
|
+
puts " Min: $#{format("%.6f", output_costs.min)}"
|
|
72
|
+
puts " Max: $#{format("%.6f", output_costs.max)}"
|
|
73
|
+
puts " Median: $#{format("%.6f", output_costs[output_costs.size / 2])}"
|
|
74
|
+
|
|
75
|
+
# Context length analysis
|
|
76
|
+
context_lengths = models.values.map { |spec| spec[:context_length] }.compact.sort
|
|
77
|
+
puts "\n📏 Context Length Analysis:"
|
|
78
|
+
puts " Min: #{format_number_with_commas(context_lengths.min)} tokens"
|
|
79
|
+
puts " Max: #{format_number_with_commas(context_lengths.max)} tokens"
|
|
80
|
+
puts " Median: #{format_number_with_commas(context_lengths[context_lengths.size / 2])} tokens"
|
|
81
|
+
|
|
82
|
+
# Performance tier breakdown
|
|
83
|
+
tiers = models.values.group_by { |spec| spec[:performance_tier] }
|
|
84
|
+
puts "\n🎯 Performance Tiers:"
|
|
85
|
+
tiers.each do |tier, tier_models|
|
|
86
|
+
puts " #{tier.to_s.capitalize.ljust(10)} #{tier_models.size} models"
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
puts "\n#{"=" * 80}"
|
|
90
|
+
puts "💡 Use 'rake models:search' to find specific models"
|
|
91
|
+
puts " Example: rake models:search provider=anthropic capability=function_calling"
|
|
92
|
+
puts
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
desc "Search for models by criteria (provider, capability, cost, context, etc.)"
|
|
96
|
+
task :search do
|
|
97
|
+
require_relative "lib/open_router"
|
|
98
|
+
require "date"
|
|
99
|
+
|
|
100
|
+
args = parse_search_arguments
|
|
101
|
+
puts "\n🔍 Searching OpenRouter Models"
|
|
102
|
+
puts "=" * 80
|
|
103
|
+
|
|
104
|
+
selector = build_model_selector(args)
|
|
105
|
+
limit = args[:limit]&.to_i || 20
|
|
106
|
+
|
|
107
|
+
puts "\n#{"=" * 80}"
|
|
108
|
+
puts "Results (showing up to #{limit}):\n\n"
|
|
109
|
+
|
|
110
|
+
results = fetch_matching_models(selector, args, limit)
|
|
111
|
+
display_search_results(results)
|
|
112
|
+
|
|
113
|
+
puts
|
|
114
|
+
|
|
115
|
+
# Prevent rake from treating arguments as tasks
|
|
116
|
+
ARGV.drop(1).each { |arg| task(arg.to_sym) { nil } }
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Parse command-line arguments into a hash
|
|
120
|
+
def self.parse_search_arguments
|
|
121
|
+
ARGV.drop(1).each_with_object({}) do |arg, hash|
|
|
122
|
+
key, value = arg.split("=", 2)
|
|
123
|
+
hash[key.to_sym] = value if key && value
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Build a ModelSelector from parsed arguments
|
|
128
|
+
def self.build_model_selector(args)
|
|
129
|
+
selector = OpenRouter::ModelSelector.new
|
|
130
|
+
|
|
131
|
+
selector = apply_optimization_strategy(selector, args)
|
|
132
|
+
selector = apply_provider_filters(selector, args)
|
|
133
|
+
selector = apply_capability_filters(selector, args)
|
|
134
|
+
selector = apply_cost_filters(selector, args)
|
|
135
|
+
selector = apply_context_filter(selector, args)
|
|
136
|
+
apply_date_filter(selector, args)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Apply optimization strategy to selector
|
|
140
|
+
def self.apply_optimization_strategy(selector, args)
|
|
141
|
+
return selector unless args[:optimize]
|
|
142
|
+
|
|
143
|
+
strategy = args[:optimize].to_sym
|
|
144
|
+
selector = selector.optimize_for(strategy)
|
|
145
|
+
puts "📈 Optimizing for: #{strategy}"
|
|
146
|
+
selector
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Apply provider filters to selector
|
|
150
|
+
def self.apply_provider_filters(selector, args)
|
|
151
|
+
return selector unless args[:provider]
|
|
152
|
+
|
|
153
|
+
providers = args[:provider].split(",").map(&:strip)
|
|
154
|
+
selector = selector.require_providers(*providers)
|
|
155
|
+
puts "🏢 Provider: #{providers.join(", ")}"
|
|
156
|
+
selector
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Apply capability filters to selector
|
|
160
|
+
def self.apply_capability_filters(selector, args)
|
|
161
|
+
capability_arg = args[:capability] || args[:capabilities]
|
|
162
|
+
return selector unless capability_arg
|
|
163
|
+
|
|
164
|
+
caps = capability_arg.split(",").map { |c| c.strip.to_sym }
|
|
165
|
+
selector = selector.require(*caps)
|
|
166
|
+
puts "⚡ Required capabilities: #{caps.join(", ")}"
|
|
167
|
+
selector
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# Apply cost filters to selector
|
|
171
|
+
def self.apply_cost_filters(selector, args)
|
|
172
|
+
selector = apply_input_cost_filter(selector, args)
|
|
173
|
+
apply_output_cost_filter(selector, args)
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# Apply input cost filter
|
|
177
|
+
def self.apply_input_cost_filter(selector, args)
|
|
178
|
+
return selector unless args[:max_cost]
|
|
179
|
+
|
|
180
|
+
max_cost = args[:max_cost].to_f
|
|
181
|
+
selector = selector.within_budget(max_cost:)
|
|
182
|
+
puts "💰 Max cost (input): $#{format("%.6f", max_cost)}/1k tokens"
|
|
183
|
+
selector
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# Apply output cost filter
|
|
187
|
+
def self.apply_output_cost_filter(selector, args)
|
|
188
|
+
return selector unless args[:max_output_cost]
|
|
189
|
+
|
|
190
|
+
max_output_cost = args[:max_output_cost].to_f
|
|
191
|
+
selector = selector.within_budget(max_output_cost:)
|
|
192
|
+
puts "💰 Max cost (output): $#{format("%.6f", max_output_cost)}/1k tokens"
|
|
193
|
+
selector
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# Apply context length filter to selector
|
|
197
|
+
def self.apply_context_filter(selector, args)
|
|
198
|
+
return selector unless args[:min_context]
|
|
199
|
+
|
|
200
|
+
min_context = args[:min_context].to_i
|
|
201
|
+
selector = selector.min_context(min_context)
|
|
202
|
+
puts "📏 Min context: #{format_number_with_commas(min_context)} tokens"
|
|
203
|
+
selector
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# Apply date filter to selector
|
|
207
|
+
def self.apply_date_filter(selector, args)
|
|
208
|
+
if args[:newer_than]
|
|
209
|
+
date = Date.parse(args[:newer_than])
|
|
210
|
+
selector = selector.newer_than(date)
|
|
211
|
+
puts "📅 Released after: #{date}"
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
puts "🎯 Performance tier: #{args[:tier]}" if args[:tier]
|
|
215
|
+
selector
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# Fetch matching models based on selector and arguments
|
|
219
|
+
def self.fetch_matching_models(selector, args, limit)
|
|
220
|
+
if args[:fallbacks]
|
|
221
|
+
selector.choose_with_fallbacks(limit:)
|
|
222
|
+
else
|
|
223
|
+
fetch_sorted_candidates(selector, limit)
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# Fetch and sort all matching candidates
|
|
228
|
+
def self.fetch_sorted_candidates(selector, limit)
|
|
229
|
+
requirements = selector.instance_variable_get(:@requirements)
|
|
230
|
+
provider_preferences = selector.instance_variable_get(:@provider_preferences)
|
|
231
|
+
strategy = selector.instance_variable_get(:@strategy)
|
|
232
|
+
|
|
233
|
+
candidates = OpenRouter::ModelRegistry.models_meeting_requirements(requirements)
|
|
234
|
+
candidates = filter_by_provider_preferences(candidates, provider_preferences)
|
|
235
|
+
sorted = sort_by_strategy(candidates, strategy)
|
|
236
|
+
|
|
237
|
+
sorted.first(limit)
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
# Display search results
|
|
241
|
+
def self.display_search_results(results)
|
|
242
|
+
if results.empty?
|
|
243
|
+
display_no_results
|
|
244
|
+
else
|
|
245
|
+
display_model_results(results)
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
# Display message when no results found
|
|
250
|
+
def self.display_no_results
|
|
251
|
+
puts "❌ No models found matching your criteria"
|
|
252
|
+
puts "\n💡 Try relaxing your requirements or use different filters"
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
# Display list of model results
|
|
256
|
+
def self.display_model_results(results)
|
|
257
|
+
results.each_with_index do |(model_id, specs), index|
|
|
258
|
+
next unless specs
|
|
259
|
+
|
|
260
|
+
specs = OpenRouter::ModelRegistry.get_model_info(model_id) if model_id.is_a?(String) && !specs
|
|
261
|
+
display_model_info(model_id, specs, index)
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
puts "=" * 80
|
|
265
|
+
puts "Found #{results.size} matching model#{"s" if results.size != 1}"
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
# Display information for a single model
|
|
269
|
+
def self.display_model_info(model_id, specs, index)
|
|
270
|
+
puts "#{(index + 1).to_s.rjust(3)}. #{model_id}"
|
|
271
|
+
puts " Name: #{specs[:name]}" if specs[:name]
|
|
272
|
+
puts " Cost: $#{format("%.6f", specs[:cost_per_1k_tokens][:input])}/1k input, " \
|
|
273
|
+
"$#{format("%.6f", specs[:cost_per_1k_tokens][:output])}/1k output"
|
|
274
|
+
puts " Context: #{format_number_with_commas(specs[:context_length])} tokens"
|
|
275
|
+
puts " Capabilities: #{specs[:capabilities].join(", ")}"
|
|
276
|
+
puts " Tier: #{specs[:performance_tier]}"
|
|
277
|
+
|
|
278
|
+
display_release_date(specs[:created_at]) if specs[:created_at]
|
|
279
|
+
puts
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
# Display release date for a model
|
|
283
|
+
def self.display_release_date(created_at)
|
|
284
|
+
created = Time.at(created_at).strftime("%Y-%m-%d")
|
|
285
|
+
puts " Released: #{created}"
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
# Format number with comma separators
|
|
289
|
+
def self.format_number_with_commas(number)
|
|
290
|
+
number.to_s.reverse.gsub(/(\d{3})(?=\d)/, '\\1,').reverse
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
# Helper methods for search task
|
|
294
|
+
def self.filter_by_provider_preferences(candidates, preferences)
|
|
295
|
+
return candidates if preferences.empty?
|
|
296
|
+
|
|
297
|
+
filtered = candidates.dup
|
|
298
|
+
|
|
299
|
+
if preferences[:required]
|
|
300
|
+
required_providers = preferences[:required]
|
|
301
|
+
filtered = filtered.select do |model_id, _|
|
|
302
|
+
provider = model_id.split("/").first
|
|
303
|
+
required_providers.include?(provider)
|
|
304
|
+
end
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
if preferences[:avoided]
|
|
308
|
+
avoided_providers = preferences[:avoided]
|
|
309
|
+
filtered = filtered.reject do |model_id, _|
|
|
310
|
+
provider = model_id.split("/").first
|
|
311
|
+
avoided_providers.include?(provider)
|
|
312
|
+
end
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
filtered
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
def self.sort_by_strategy(candidates, strategy)
|
|
319
|
+
case strategy
|
|
320
|
+
when :cost
|
|
321
|
+
candidates.sort_by { |_, specs| specs[:cost_per_1k_tokens][:input] }
|
|
322
|
+
when :performance
|
|
323
|
+
candidates.sort_by do |_, specs|
|
|
324
|
+
[specs[:performance_tier] == :premium ? 0 : 1, specs[:cost_per_1k_tokens][:input]]
|
|
325
|
+
end
|
|
326
|
+
when :latest
|
|
327
|
+
candidates.sort_by { |_, specs| -(specs[:created_at] || 0).to_i }
|
|
328
|
+
when :context
|
|
329
|
+
candidates.sort_by { |_, specs| -(specs[:context_length] || 0).to_i }
|
|
330
|
+
else
|
|
331
|
+
candidates.sort_by { |_, specs| specs[:cost_per_1k_tokens][:input] }
|
|
332
|
+
end
|
|
333
|
+
end
|
|
334
|
+
end
|
data/SECURITY.md
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# Security Policy
|
|
2
|
+
|
|
3
|
+
## Supported Versions
|
|
4
|
+
|
|
5
|
+
We release patches for security vulnerabilities for the following versions:
|
|
6
|
+
|
|
7
|
+
| Version | Supported |
|
|
8
|
+
| ------- | ------------------ |
|
|
9
|
+
| 1.x.x | :white_check_mark: |
|
|
10
|
+
| < 1.0 | :x: |
|
|
11
|
+
|
|
12
|
+
## Reporting a Vulnerability
|
|
13
|
+
|
|
14
|
+
We take the security of OpenRouter Enhanced seriously. If you believe you have found a security vulnerability, please report it to us as described below.
|
|
15
|
+
|
|
16
|
+
### Where to Report
|
|
17
|
+
|
|
18
|
+
**Please do not report security vulnerabilities through public GitHub issues.**
|
|
19
|
+
|
|
20
|
+
Instead, please report them via email to:
|
|
21
|
+
|
|
22
|
+
- **Email**: opensource@ericstiens.dev (replace with actual contact)
|
|
23
|
+
- **Subject Line**: `[SECURITY] OpenRouter Enhanced - Brief Description`
|
|
24
|
+
|
|
25
|
+
Alternatively, you can use GitHub's private vulnerability reporting feature:
|
|
26
|
+
|
|
27
|
+
1. Go to the repository's Security tab
|
|
28
|
+
2. Click "Report a vulnerability"
|
|
29
|
+
3. Fill out the form with details
|
|
30
|
+
|
|
31
|
+
### What to Include
|
|
32
|
+
|
|
33
|
+
Please include the following information in your report:
|
|
34
|
+
|
|
35
|
+
- Type of issue
|
|
36
|
+
- Full paths of source file(s) related to the issue
|
|
37
|
+
- Location of the affected source code (tag/branch/commit or direct URL)
|
|
38
|
+
- Any special configuration required to reproduce the issue
|
|
39
|
+
- Step-by-step instructions to reproduce the issue
|
|
40
|
+
- Proof-of-concept or exploit code (if possible)
|
|
41
|
+
- Impact of the issue, including how an attacker might exploit it
|
|
42
|
+
|
|
43
|
+
### What to Expect
|
|
44
|
+
|
|
45
|
+
After you submit a report:
|
|
46
|
+
|
|
47
|
+
1. **Acknowledgment**: We will acknowledge receipt of your vulnerability report within 48 hours.
|
|
48
|
+
|
|
49
|
+
2. **Initial Assessment**: We will perform an initial assessment and respond with our evaluation within 5 business days.
|
|
50
|
+
|
|
51
|
+
3. **Updates**: We will keep you informed about our progress towards resolving the issue.
|
|
52
|
+
|
|
53
|
+
4. **Resolution**: Once the issue is resolved, we will:
|
|
54
|
+
- Release a security patch
|
|
55
|
+
- Publicly disclose the vulnerability (with credit to you, if desired)
|
|
56
|
+
- Add the issue to our security advisories
|
|
57
|
+
|
|
58
|
+
### Disclosure Policy
|
|
59
|
+
|
|
60
|
+
- We ask that you do not publicly disclose the vulnerability until we have had a chance to address it.
|
|
61
|
+
- We will coordinate with you on the disclosure timeline.
|
|
62
|
+
- We aim to resolve critical issues within 30 days of acknowledgment.
|
|
63
|
+
|
|
64
|
+
## Security Best Practices for Users
|
|
65
|
+
|
|
66
|
+
When using the OpenRouter Enhanced gem:
|
|
67
|
+
|
|
68
|
+
1. **API Keys**:
|
|
69
|
+
- Never hardcode API keys in your code
|
|
70
|
+
- Use environment variables or secure credential storage
|
|
71
|
+
- Rotate API keys regularly
|
|
72
|
+
- Never commit API keys to version control
|
|
73
|
+
|
|
74
|
+
2. **Dependency Management**:
|
|
75
|
+
- Keep the gem updated to the latest version
|
|
76
|
+
- Regularly run `bundle update` to get security patches
|
|
77
|
+
- Monitor security advisories for dependencies
|
|
78
|
+
|
|
79
|
+
3. **Input Validation**:
|
|
80
|
+
- Validate all user inputs before passing to the gem
|
|
81
|
+
- Be cautious with tool calling and structured outputs from untrusted sources
|
|
82
|
+
- Sanitize data before execution
|
|
83
|
+
|
|
84
|
+
4. **Network Security**:
|
|
85
|
+
- Use HTTPS for all API communications (enabled by default)
|
|
86
|
+
- Verify SSL certificates (enabled by default)
|
|
87
|
+
- Be cautious with proxy configurations
|
|
88
|
+
|
|
89
|
+
5. **Error Handling**:
|
|
90
|
+
- Avoid exposing detailed error messages to end users
|
|
91
|
+
- Log errors securely without exposing sensitive data
|
|
92
|
+
- Monitor for unusual error patterns
|
|
93
|
+
|
|
94
|
+
## Known Security Considerations
|
|
95
|
+
|
|
96
|
+
### API Key Storage
|
|
97
|
+
|
|
98
|
+
The gem requires an OpenRouter API key for operation. Users are responsible for:
|
|
99
|
+
- Securely storing their API keys
|
|
100
|
+
- Not committing keys to version control
|
|
101
|
+
- Using environment variables or secure vaults
|
|
102
|
+
|
|
103
|
+
### Tool Calling
|
|
104
|
+
|
|
105
|
+
When using tool calling features:
|
|
106
|
+
- Validate tool arguments before execution
|
|
107
|
+
- Implement proper authorization checks
|
|
108
|
+
- Sandbox tool execution where appropriate
|
|
109
|
+
- Never execute arbitrary code from LLM responses without validation
|
|
110
|
+
|
|
111
|
+
### Data Privacy
|
|
112
|
+
|
|
113
|
+
When using the gem:
|
|
114
|
+
- Be aware that data is sent to OpenRouter's API
|
|
115
|
+
- Review OpenRouter's privacy policy and terms of service
|
|
116
|
+
- Implement appropriate data handling for sensitive information
|
|
117
|
+
- Consider data residency requirements for your use case
|
|
118
|
+
|
|
119
|
+
## Security Update Process
|
|
120
|
+
|
|
121
|
+
1. Security issues are prioritized based on severity and impact
|
|
122
|
+
2. Patches are developed and tested in private
|
|
123
|
+
3. Security advisories are prepared
|
|
124
|
+
4. Patches are released via a new gem version
|
|
125
|
+
5. Security advisories are published
|
|
126
|
+
6. Users are notified through:
|
|
127
|
+
- GitHub Security Advisories
|
|
128
|
+
- RubyGems security notifications
|
|
129
|
+
- Project changelog
|
|
130
|
+
- Release notes
|
|
131
|
+
|
|
132
|
+
## Bug Bounty Program
|
|
133
|
+
|
|
134
|
+
We currently do not have a bug bounty program. However, we greatly appreciate security researchers who responsibly disclose vulnerabilities and will publicly acknowledge their contributions (with permission).
|
|
135
|
+
|
|
136
|
+
## Contact
|
|
137
|
+
|
|
138
|
+
For general security questions or concerns, please use the same contact methods as vulnerability reporting.
|
|
139
|
+
|
|
140
|
+
For non-security-related issues, please use the standard GitHub issues process.
|
|
141
|
+
|
|
142
|
+
## Acknowledgments
|
|
143
|
+
|
|
144
|
+
We would like to thank the following individuals for responsibly disclosing security issues:
|
|
145
|
+
|
|
146
|
+
(This section will be updated as security issues are responsibly disclosed and resolved)
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
Last updated: 2025-10-05
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# VCR Configuration
|
|
2
|
+
|
|
3
|
+
This document explains the VCR (Video Cassette Recorder) configuration for testing external API interactions.
|
|
4
|
+
|
|
5
|
+
## Configuration Overview
|
|
6
|
+
|
|
7
|
+
The VCR setup is configured in `spec/support/vcr.rb` with the following key features:
|
|
8
|
+
|
|
9
|
+
### Environment-Based Recording Modes
|
|
10
|
+
|
|
11
|
+
- **CI Environment** (`CI=true`, `GITHUB_ACTIONS=true`, or `CONTINUOUS_INTEGRATION=true`):
|
|
12
|
+
- Recording mode: `:none` (never record, use existing cassettes only)
|
|
13
|
+
- HTTP connections: Disabled (WebMock blocks all external requests)
|
|
14
|
+
- API Key: Uses dummy key from environment variable
|
|
15
|
+
|
|
16
|
+
- **Development Environment**:
|
|
17
|
+
- Recording mode: `:once` (record if cassette doesn't exist, otherwise use existing)
|
|
18
|
+
- HTTP connections: Allowed when no cassette exists (enables recording)
|
|
19
|
+
- API Key: Uses real API key from environment variable
|
|
20
|
+
|
|
21
|
+
### Override Options
|
|
22
|
+
|
|
23
|
+
- `VCR_RECORD_ALL=true`: Re-record all cassettes (development only)
|
|
24
|
+
- `VCR_RECORD_NEW=true`: Record new episodes only (development only)
|
|
25
|
+
|
|
26
|
+
### API Key Handling
|
|
27
|
+
|
|
28
|
+
- In CI: Uses dummy key `"dummy-api-key-for-testing-do-not-use"` set in GitHub Actions
|
|
29
|
+
- In development: Uses real API key from `OPENROUTER_API_KEY` environment variable
|
|
30
|
+
- All API keys are filtered from recordings for security
|
|
31
|
+
|
|
32
|
+
### Cassette Storage
|
|
33
|
+
|
|
34
|
+
Cassettes are stored in `spec/fixtures/vcr_cassettes/` and contain recorded HTTP interactions.
|
|
35
|
+
|
|
36
|
+
## Usage
|
|
37
|
+
|
|
38
|
+
### Running Tests with Different Modes
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
# Use existing cassettes (CI mode)
|
|
42
|
+
CI=true bundle exec rspec spec/vcr/
|
|
43
|
+
|
|
44
|
+
# Record new cassettes (development)
|
|
45
|
+
OPENROUTER_API_KEY=your-real-key bundle exec rspec spec/vcr/
|
|
46
|
+
|
|
47
|
+
# Re-record all cassettes
|
|
48
|
+
VCR_RECORD_ALL=true OPENROUTER_API_KEY=your-real-key bundle exec rspec spec/vcr/
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Test Tagging
|
|
52
|
+
|
|
53
|
+
Tests use `:vcr` metadata tag to enable VCR recording:
|
|
54
|
+
|
|
55
|
+
```ruby
|
|
56
|
+
RSpec.describe "API Tests", :vcr do
|
|
57
|
+
it "makes API call", vcr: { cassette_name: "custom_name" } do
|
|
58
|
+
# Test code that makes HTTP requests
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Security
|
|
64
|
+
|
|
65
|
+
- All API keys are automatically filtered from cassette recordings
|
|
66
|
+
- Real API keys are never committed to the repository
|
|
67
|
+
- CI uses dummy keys that cannot access real services
|
|
68
|
+
|
|
69
|
+
## WebMock Integration
|
|
70
|
+
|
|
71
|
+
VCR hooks into WebMock to:
|
|
72
|
+
- Block external HTTP requests by default
|
|
73
|
+
- Allow requests only when VCR cassettes are recording
|
|
74
|
+
- Ensure tests are deterministic and don't depend on external services
|
|
75
|
+
|
|
76
|
+
This configuration ensures that:
|
|
77
|
+
1. Tests are fast and reliable (no external dependencies)
|
|
78
|
+
2. CI environments never make real API calls
|
|
79
|
+
3. Development can record new interactions when needed
|
|
80
|
+
4. Security is maintained through API key filtering
|