open_router_enhanced 1.2.5 → 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.
@@ -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
@@ -33,24 +33,34 @@ module OpenRouter
33
33
  # Enhanced streaming completion with better event handling and response reconstruction
34
34
  #
35
35
  # @param messages [Array<Hash>] Array of message hashes
36
- # @param model [String|Array] Model identifier or array of models for fallback
36
+ # @param options [CompletionOptions, Hash, nil] Options object or hash with configuration
37
37
  # @param accumulate_response [Boolean] Whether to accumulate and return complete response
38
- # @param extras [Hash] Additional parameters for the completion request
38
+ # @param kwargs [Hash] Additional options (merged with options parameter)
39
39
  # @param block [Proc] Optional block to call for each chunk (in addition to registered callbacks)
40
40
  # @return [Response, nil] Complete response if accumulate_response is true, nil otherwise
41
- def stream_complete(messages, model: "openrouter/auto", accumulate_response: true, **extras, &block)
41
+ #
42
+ # @example Simple usage (unchanged)
43
+ # client.stream_complete(messages, model: "gpt-4")
44
+ #
45
+ # @example With CompletionOptions
46
+ # opts = CompletionOptions.new(model: "gpt-4", temperature: 0.7)
47
+ # client.stream_complete(messages, opts)
48
+ #
49
+ # @example Options with overrides
50
+ # client.stream_complete(messages, base_opts, temperature: 0.9)
51
+ def stream_complete(messages, options = nil, accumulate_response: true, **kwargs, &block)
52
+ opts = normalize_options(options, kwargs)
42
53
  response_accumulator = ResponseAccumulator.new if accumulate_response
43
54
 
44
55
  # Set up streaming handler (pass optional per-call block)
45
56
  stream_handler = build_stream_handler(response_accumulator, &block)
46
57
 
47
58
  # Trigger start callback
48
- trigger_streaming_callbacks(:on_start, { model: model, messages: messages })
59
+ trigger_streaming_callbacks(:on_start, { model: opts.model, messages: messages })
49
60
 
50
61
  begin
51
- # Execute the streaming request
52
- # Note: extras must be passed as a hash, not splatted, to match complete()'s signature
53
- complete(messages, model: model, stream: stream_handler, extras: extras)
62
+ # Execute the streaming request using parent's complete method
63
+ complete(messages, opts, stream: stream_handler)
54
64
 
55
65
  # Return accumulated response if requested
56
66
  if accumulate_response && response_accumulator
@@ -70,22 +80,26 @@ module OpenRouter
70
80
  # Stream with a simple block interface
71
81
  #
72
82
  # @param messages [Array<Hash>] Array of message hashes
73
- # @param model [String|Array] Model identifier
83
+ # @param options [CompletionOptions, Hash, nil] Options object or hash with configuration
84
+ # @param kwargs [Hash] Additional options (merged with options parameter)
74
85
  # @param block [Proc] Block to call for each content chunk
75
- # @param extras [Hash] Additional parameters
76
86
  #
77
- # @example
87
+ # @example Simple usage (unchanged)
78
88
  # client.stream(messages, model: "openai/gpt-4o-mini") do |chunk|
79
89
  # print chunk
80
90
  # end
81
- def stream(messages, model: "openrouter/auto", **extras, &block)
91
+ #
92
+ # @example With CompletionOptions
93
+ # opts = CompletionOptions.new(model: "gpt-4", temperature: 0.7)
94
+ # client.stream(messages, opts) { |chunk| print chunk }
95
+ def stream(messages, options = nil, **kwargs, &block)
82
96
  raise ArgumentError, "Block required for streaming" unless block_given?
83
97
 
84
98
  stream_complete(
85
99
  messages,
86
- model: model,
100
+ options,
87
101
  accumulate_response: false,
88
- **extras
102
+ **kwargs
89
103
  ) do |chunk|
90
104
  content = extract_content_from_chunk(chunk)
91
105
  block.call(content) if content
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module OpenRouter
4
- VERSION = "1.2.5"
4
+ VERSION = "2.0.1"
5
5
  end
data/lib/open_router.rb CHANGED
@@ -17,6 +17,7 @@ module OpenRouter
17
17
  end
18
18
 
19
19
  require_relative "open_router/http"
20
+ require_relative "open_router/completion_options"
20
21
  require_relative "open_router/tool"
21
22
  require_relative "open_router/tool_call_base"
22
23
  require_relative "open_router/tool_call"
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: 1.2.5
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-27 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
@@ -149,6 +149,7 @@ files:
149
149
  - examples/tool_loop_example.rb
150
150
  - lib/open_router.rb
151
151
  - lib/open_router/client.rb
152
+ - lib/open_router/completion_options.rb
152
153
  - lib/open_router/http.rb
153
154
  - lib/open_router/json_healer.rb
154
155
  - lib/open_router/model_registry.rb