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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +249 -0
- data/Gemfile.lock +28 -23
- data/README.md +83 -0
- data/docs/model_selection.md +30 -0
- data/docs/plugins.md +34 -0
- data/docs/responses_api.md +34 -0
- data/docs/streaming.md +32 -0
- data/docs/structured_outputs.md +31 -0
- data/docs/tools.md +32 -1
- data/lib/open_router/client.rb +183 -80
- data/lib/open_router/completion_options.rb +265 -0
- data/lib/open_router/http.rb +7 -7
- data/lib/open_router/json_healer.rb +7 -0
- data/lib/open_router/model_registry.rb +15 -7
- data/lib/open_router/streaming_client.rb +27 -13
- data/lib/open_router/version.rb +1 -1
- data/lib/open_router.rb +1 -0
- metadata +3 -2
data/lib/open_router/http.rb
CHANGED
|
@@ -78,30 +78,30 @@ module OpenRouter
|
|
|
78
78
|
|
|
79
79
|
def conn(multipart: false)
|
|
80
80
|
Faraday.new do |f|
|
|
81
|
-
f.options[: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
|
-
|
|
87
|
+
configuration.faraday_config&.call(f)
|
|
88
88
|
end
|
|
89
89
|
end
|
|
90
90
|
|
|
91
91
|
def uri(path:)
|
|
92
|
-
base =
|
|
93
|
-
ver =
|
|
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
|
-
|
|
95
|
+
[base, ver, p].reject(&:empty?).join("/")
|
|
96
96
|
end
|
|
97
97
|
|
|
98
98
|
def headers
|
|
99
99
|
{
|
|
100
|
-
"Authorization" => "Bearer #{
|
|
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(
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
|
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
|
-
|
|
359
|
-
|
|
360
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
100
|
+
options,
|
|
87
101
|
accumulate_response: false,
|
|
88
|
-
**
|
|
102
|
+
**kwargs
|
|
89
103
|
) do |chunk|
|
|
90
104
|
content = extract_content_from_chunk(chunk)
|
|
91
105
|
block.call(content) if content
|
data/lib/open_router/version.rb
CHANGED
data/lib/open_router.rb
CHANGED
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:
|
|
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:
|
|
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
|