open_router_enhanced 2.0.0 → 2.0.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 925d55d16a222d6a954c449307c480ab85bf3092138b172cd8a2561f40aeeba4
4
- data.tar.gz: 7f427cb96bdd2a98c355096db04babb861a8bb2a13ab3f7f940c5397a9d4f651
3
+ metadata.gz: d443d948a07c5b55d6366e135354b2faa07a8edc38cb2791237a6a4a92bd229a
4
+ data.tar.gz: 37a93b36720b58bf1ee1c4809aa4d54e4d29e06ba27a63c9285a78fa7074eb66
5
5
  SHA512:
6
- metadata.gz: c4cea4727bfa5df23b123e695cd95fe6106f6a57a5184e8b3af9f7c043e3198702ea433fc7d553220c3def45d1adc899b4e53ff4b7fa3ff4b005cf76a580c681
7
- data.tar.gz: 9544496653c0716f7c704d10f7376e21e20bc4f98fe784114822640ffc957142e3fed056bcdc6d135e333010e2489910c44f7a445aa50ac3dc18eb9fe52bb4ee
6
+ metadata.gz: 78fb6b74df5a7cb901ecac23fe503c667035e4794d6e7d9b97d4ad703e1f0a40347bd3274b86ba13c35501c8afc0b3c29ec5cb7b632eb93013af5a5328403285
7
+ data.tar.gz: 51d704a3035cf8211ac0d9533931d0b1a9bc9ebe49d21f192dd49b7dbb423eadcbf5adcc7b99d3212002947f2646337122e99660a589fa54be22ab3a356555eb
data/Gemfile.lock CHANGED
@@ -11,7 +11,7 @@ PATH
11
11
  GEM
12
12
  remote: https://rubygems.org/
13
13
  specs:
14
- activesupport (8.1.1)
14
+ activesupport (8.1.3)
15
15
  base64
16
16
  bigdecimal
17
17
  concurrent-ruby (~> 1.0, >= 1.3.1)
@@ -24,11 +24,11 @@ GEM
24
24
  securerandom (>= 0.3)
25
25
  tzinfo (~> 2.0, >= 2.0.5)
26
26
  uri (>= 0.13.1)
27
- addressable (2.8.8)
27
+ addressable (2.9.0)
28
28
  public_suffix (>= 2.0.2, < 8.0)
29
29
  ast (2.4.3)
30
30
  base64 (0.3.0)
31
- bigdecimal (4.0.1)
31
+ bigdecimal (4.1.1)
32
32
  coderay (1.1.3)
33
33
  concurrent-ruby (1.3.6)
34
34
  connection_pool (3.0.2)
@@ -38,42 +38,47 @@ GEM
38
38
  diff-lcs (1.6.2)
39
39
  dotenv (3.2.0)
40
40
  drb (2.2.3)
41
- faraday (2.14.0)
41
+ faraday (2.14.1)
42
42
  faraday-net_http (>= 2.0, < 3.5)
43
43
  json
44
44
  logger
45
- faraday-multipart (1.1.1)
45
+ faraday-multipart (1.2.0)
46
46
  multipart-post (~> 2.0)
47
47
  faraday-net_http (3.4.2)
48
48
  net-http (~> 0.5)
49
49
  hashdiff (1.2.1)
50
50
  i18n (1.14.8)
51
51
  concurrent-ruby (~> 1.0)
52
- json (2.18.0)
52
+ io-console (0.8.2)
53
+ json (2.19.3)
53
54
  json-schema (4.3.1)
54
55
  addressable (>= 2.8)
55
56
  language_server-protocol (3.17.0.5)
56
57
  lint_roller (1.1.0)
57
58
  logger (1.7.0)
58
59
  method_source (1.1.0)
59
- minitest (6.0.0)
60
+ minitest (6.0.4)
61
+ drb (~> 2.0)
60
62
  prism (~> 1.5)
61
63
  multipart-post (2.4.1)
62
64
  net-http (0.9.1)
63
65
  uri (>= 0.11.1)
64
- parallel (1.27.0)
65
- parser (3.3.10.0)
66
+ parallel (2.0.1)
67
+ parser (3.3.11.1)
66
68
  ast (~> 2.4.1)
67
69
  racc
68
- prism (1.7.0)
69
- pry (0.15.2)
70
+ prism (1.9.0)
71
+ pry (0.16.0)
70
72
  coderay (~> 1.1)
71
73
  method_source (~> 1.0)
72
- public_suffix (7.0.0)
74
+ reline (>= 0.6.0)
75
+ public_suffix (7.0.5)
73
76
  racc (1.8.1)
74
77
  rainbow (3.1.1)
75
- rake (13.3.1)
76
- regexp_parser (2.11.3)
78
+ rake (13.4.2)
79
+ regexp_parser (2.12.0)
80
+ reline (0.6.3)
81
+ io-console (~> 0.5)
77
82
  rexml (3.4.4)
78
83
  rspec (3.13.2)
79
84
  rspec-core (~> 3.13.0)
@@ -84,24 +89,24 @@ GEM
84
89
  rspec-expectations (3.13.5)
85
90
  diff-lcs (>= 1.2.0, < 2.0)
86
91
  rspec-support (~> 3.13.0)
87
- rspec-mocks (3.13.7)
92
+ rspec-mocks (3.13.8)
88
93
  diff-lcs (>= 1.2.0, < 2.0)
89
94
  rspec-support (~> 3.13.0)
90
- rspec-support (3.13.6)
91
- rubocop (1.82.1)
95
+ rspec-support (3.13.7)
96
+ rubocop (1.86.1)
92
97
  json (~> 2.3)
93
98
  language_server-protocol (~> 3.17.0.2)
94
99
  lint_roller (~> 1.1.0)
95
- parallel (~> 1.10)
100
+ parallel (>= 1.10)
96
101
  parser (>= 3.3.0.2)
97
102
  rainbow (>= 2.2.2, < 4.0)
98
103
  regexp_parser (>= 2.9.3, < 3.0)
99
- rubocop-ast (>= 1.48.0, < 2.0)
104
+ rubocop-ast (>= 1.49.0, < 2.0)
100
105
  ruby-progressbar (~> 1.7)
101
106
  unicode-display_width (>= 2.4.0, < 4.0)
102
- rubocop-ast (1.48.0)
107
+ rubocop-ast (1.49.1)
103
108
  parser (>= 3.3.7.2)
104
- prism (~> 1.4)
109
+ prism (~> 1.7)
105
110
  ruby-progressbar (1.13.0)
106
111
  securerandom (0.4.1)
107
112
  tzinfo (2.0.6)
@@ -111,7 +116,7 @@ GEM
111
116
  unicode-emoji (4.2.0)
112
117
  uri (1.1.1)
113
118
  vcr (6.4.0)
114
- webmock (3.26.1)
119
+ webmock (3.26.2)
115
120
  addressable (>= 2.8.0)
116
121
  crack (>= 0.3.2)
117
122
  hashdiff (>= 0.4.0, < 2.0.0)
@@ -12,15 +12,19 @@ module OpenRouter
12
12
  class Client
13
13
  include OpenRouter::HTTP
14
14
 
15
- attr_reader :callbacks, :usage_tracker
15
+ attr_reader :callbacks, :usage_tracker, :configuration
16
16
 
17
17
  # Initializes the client with optional configurations.
18
18
  def initialize(access_token: nil, request_timeout: nil, uri_base: nil, extra_headers: {}, track_usage: true)
19
- OpenRouter.configuration.access_token = access_token if access_token
20
- OpenRouter.configuration.request_timeout = request_timeout if request_timeout
21
- OpenRouter.configuration.uri_base = uri_base if uri_base
22
- OpenRouter.configuration.extra_headers = extra_headers if extra_headers.any?
23
- yield(OpenRouter.configuration) if block_given?
19
+ # Build a per-instance configuration to avoid mutating the global singleton,
20
+ # which would cause credential leakage across Client instances in concurrent use.
21
+ @configuration = OpenRouter.configuration.dup
22
+ @configuration.extra_headers = OpenRouter.configuration.extra_headers.dup
23
+ @configuration.access_token = access_token if access_token
24
+ @configuration.request_timeout = request_timeout if request_timeout
25
+ @configuration.uri_base = uri_base if uri_base
26
+ @configuration.extra_headers = @configuration.extra_headers.merge(extra_headers) if extra_headers.any?
27
+ yield(@configuration) if block_given?
24
28
 
25
29
  # Instance-level tracking of capability warnings to avoid memory leaks
26
30
  @capability_warnings_shown = Set.new
@@ -40,10 +44,6 @@ module OpenRouter
40
44
  @usage_tracker = UsageTracker.new if @track_usage
41
45
  end
42
46
 
43
- def configuration
44
- OpenRouter.configuration
45
- end
46
-
47
47
  # Register a callback for a specific event
48
48
  #
49
49
  # @param event [Symbol] The event to register for (:before_request, :after_response, :on_tool_call, :on_error, :on_stream_chunk, :on_healing)
@@ -185,7 +185,7 @@ module OpenRouter
185
185
 
186
186
  parameters = { model: opts.model, input: input }
187
187
  parameters[:reasoning] = opts.reasoning if opts.reasoning
188
- parameters[:tools] = serialize_tools_for_responses(opts.tools) if opts.has_tools?
188
+ parameters[:tools] = serialize_tools_for_responses(opts.tools) if opts.tools?
189
189
  parameters[:tool_choice] = opts.tool_choice if opts.tool_choice
190
190
  # Prefer max_completion_tokens over max_tokens (consistent with complete() method)
191
191
  parameters[:max_output_tokens] = opts.max_completion_tokens || opts.max_tokens if opts.max_completion_tokens || opts.max_tokens
@@ -503,7 +503,7 @@ module OpenRouter
503
503
  # @param parameters [Hash] Request parameters hash
504
504
  # @param opts [CompletionOptions] Options object
505
505
  def configure_tool_calling!(parameters, opts)
506
- return unless opts.has_tools?
506
+ return unless opts.tools?
507
507
 
508
508
  warn_if_unsupported(opts.model, :function_calling, "tool calling")
509
509
  parameters[:tools] = serialize_tools(opts.tools)
@@ -516,7 +516,7 @@ module OpenRouter
516
516
  # @param opts [CompletionOptions] Options object
517
517
  # @return [Boolean] Whether forced extraction mode is being used
518
518
  def configure_structured_outputs!(parameters, opts)
519
- return false unless opts.has_response_format?
519
+ return false unless opts.response_format?
520
520
 
521
521
  force_extraction = determine_forced_extraction_mode(opts.model, opts.force_structured_output)
522
522
 
@@ -244,14 +244,14 @@ module OpenRouter
244
244
  # Check if this options object has any tools defined
245
245
  #
246
246
  # @return [Boolean]
247
- def has_tools?
247
+ def tools?
248
248
  tools.is_a?(Array) && !tools.empty?
249
249
  end
250
250
 
251
251
  # Check if response format is configured
252
252
  #
253
253
  # @return [Boolean]
254
- def has_response_format?
254
+ def response_format?
255
255
  !response_format.nil?
256
256
  end
257
257
 
@@ -78,30 +78,30 @@ module OpenRouter
78
78
 
79
79
  def conn(multipart: false)
80
80
  Faraday.new do |f|
81
- f.options[:timeout] = OpenRouter.configuration.request_timeout
81
+ f.options[:timeout] = configuration.request_timeout
82
82
  f.request(:multipart) if multipart
83
83
  # NOTE: Removed MiddlewareErrors reference - was undefined and @log_errors was never set
84
84
  f.response :raise_error
85
85
  f.response :json if OpenRouter::HAS_JSON_MW
86
86
 
87
- OpenRouter.configuration.faraday_config&.call(f)
87
+ configuration.faraday_config&.call(f)
88
88
  end
89
89
  end
90
90
 
91
91
  def uri(path:)
92
- base = OpenRouter.configuration.uri_base.sub(%r{/\z}, "")
93
- ver = OpenRouter.configuration.api_version.to_s.sub(%r{^/}, "").sub(%r{/\z}, "")
92
+ base = configuration.uri_base.sub(%r{/\z}, "")
93
+ ver = configuration.api_version.to_s.sub(%r{^/}, "").sub(%r{/\z}, "")
94
94
  p = path.to_s.sub(%r{^/}, "")
95
- "#{base}/#{ver}/#{p}"
95
+ [base, ver, p].reject(&:empty?).join("/")
96
96
  end
97
97
 
98
98
  def headers
99
99
  {
100
- "Authorization" => "Bearer #{OpenRouter.configuration.access_token}",
100
+ "Authorization" => "Bearer #{configuration.access_token}",
101
101
  "Content-Type" => "application/json",
102
102
  "X-Title" => "OpenRouter Ruby Client",
103
103
  "HTTP-Referer" => "https://github.com/OlympiaAI/open_router"
104
- }.merge(OpenRouter.configuration.extra_headers)
104
+ }.merge(configuration.extra_headers)
105
105
  end
106
106
 
107
107
  def multipart_parameters(parameters)
@@ -22,6 +22,11 @@ module OpenRouter
22
22
 
23
23
  # Enhanced heal method that supports different healing contexts
24
24
  def heal(raw_text, schema, context: :generic)
25
+ # Guard against re-entrant healing triggered by on_healing callbacks
26
+ raise StructuredOutputError, "Recursive healing detected — cannot heal from within a healing callback" if (Thread.current[:openrouter_heal_depth] || 0).positive?
27
+
28
+ Thread.current[:openrouter_heal_depth] = 1
29
+
25
30
  candidate_json = extract_json_candidate(raw_text)
26
31
  raise StructuredOutputError, "No JSON-like content found in the response." if candidate_json.nil?
27
32
 
@@ -53,6 +58,8 @@ module OpenRouter
53
58
  # Escalate to LLM-based healing with proper context
54
59
  candidate_json = fix_with_healer_model(candidate_json, schema, e.message, e.class, original_content, context)
55
60
  end
61
+ ensure
62
+ Thread.current[:openrouter_heal_depth] = 0
56
63
  end
57
64
 
58
65
  private
@@ -15,6 +15,8 @@ module OpenRouter
15
15
  CACHE_METADATA_FILE = File.join(CACHE_DIR, "cache_metadata.json")
16
16
  MAX_CACHE_SIZE_MB = 50 # Maximum cache size in megabytes
17
17
 
18
+ REGISTRY_MUTEX = Mutex.new
19
+
18
20
  class << self
19
21
  # Fetch models from OpenRouter API using Faraday for consistent SSL handling
20
22
  def fetch_models_from_api
@@ -83,9 +85,11 @@ module OpenRouter
83
85
 
84
86
  # Clear local cache (both files and memory)
85
87
  def clear_cache!
86
- FileUtils.rm_rf(CACHE_DIR) if Dir.exist?(CACHE_DIR)
87
- @processed_models = nil
88
- @all_models = nil
88
+ REGISTRY_MUTEX.synchronize do
89
+ FileUtils.rm_rf(CACHE_DIR) if Dir.exist?(CACHE_DIR)
90
+ @processed_models = nil
91
+ @all_models = nil
92
+ end
89
93
  end
90
94
 
91
95
  # Refresh models data from API
@@ -246,7 +250,11 @@ module OpenRouter
246
250
 
247
251
  # Get all registered models (fetch from API if needed)
248
252
  def all_models
249
- @all_models ||= fetch_and_cache_models
253
+ return @all_models if @all_models # fast path without lock
254
+
255
+ REGISTRY_MUTEX.synchronize do
256
+ @all_models ||= fetch_and_cache_models
257
+ end
250
258
  end
251
259
 
252
260
  # Calculate estimated cost for a request
@@ -355,9 +363,9 @@ module OpenRouter
355
363
  total_size = Dir.glob(File.join(CACHE_DIR, "**/*"))
356
364
  .select { |f| File.file?(f) }
357
365
  .sum do |f|
358
- File.size(f)
359
- rescue StandardError
360
- 0
366
+ File.size(f)
367
+ rescue StandardError
368
+ 0
361
369
  end
362
370
  total_size / (1024.0 * 1024.0)
363
371
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module OpenRouter
4
- VERSION = "2.0.0"
4
+ VERSION = "2.0.1"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: open_router_enhanced
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.0
4
+ version: 2.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Eric Stiens
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-12-28 00:00:00.000000000 Z
11
+ date: 2026-04-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport