open_router_enhanced 2.0.0 → 2.1.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 +4 -4
- data/Gemfile.lock +28 -23
- data/Rakefile +25 -14
- data/lib/open_router/client.rb +13 -13
- data/lib/open_router/completion_options.rb +2 -2
- data/lib/open_router/http.rb +7 -7
- data/lib/open_router/json_healer.rb +7 -0
- data/lib/open_router/model_registry.rb +39 -13
- data/lib/open_router/model_selector.rb +7 -7
- data/lib/open_router/response.rb +6 -1
- data/lib/open_router/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1f08650282b1232abfa779330c8f8ec9aad7bf6a984fd31e1a74ce1f3cad6962
|
|
4
|
+
data.tar.gz: e41ce6204abde21a34dd6c2976801666b8f4b9339cf81c845861f3837c15097c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1d15f1e25db721734f0f9d2e43d01f6546af170096e65253c1c8438c3362484d6bad28f36669e2b8c34856ca297bb07dc1ffb18c7dc4442e2fe8c93ad79474b1
|
|
7
|
+
data.tar.gz: 2db9c96b66dcb0eae4b03f463db956192b38ef7444c879f7fe72a767e31c378d1ed702ee38a4c3b76e187945e6d942ac5dcbf7e7733fe8621315bee729e74940
|
data/Gemfile.lock
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
open_router_enhanced (2.0.
|
|
4
|
+
open_router_enhanced (2.0.1)
|
|
5
5
|
activesupport (>= 6.0, < 9.0)
|
|
6
6
|
dotenv (>= 2.0, < 4.0)
|
|
7
7
|
faraday (>= 1.0, < 3.0)
|
|
@@ -11,7 +11,7 @@ PATH
|
|
|
11
11
|
GEM
|
|
12
12
|
remote: https://rubygems.org/
|
|
13
13
|
specs:
|
|
14
|
-
activesupport (8.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.
|
|
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.
|
|
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.
|
|
41
|
+
faraday (2.14.1)
|
|
42
42
|
faraday-net_http (>= 2.0, < 3.5)
|
|
43
43
|
json
|
|
44
44
|
logger
|
|
45
|
-
faraday-multipart (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
|
-
|
|
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.
|
|
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 (
|
|
65
|
-
parser (3.3.
|
|
66
|
+
parallel (2.0.1)
|
|
67
|
+
parser (3.3.11.1)
|
|
66
68
|
ast (~> 2.4.1)
|
|
67
69
|
racc
|
|
68
|
-
prism (1.
|
|
69
|
-
pry (0.
|
|
70
|
+
prism (1.9.0)
|
|
71
|
+
pry (0.16.0)
|
|
70
72
|
coderay (~> 1.1)
|
|
71
73
|
method_source (~> 1.0)
|
|
72
|
-
|
|
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.
|
|
76
|
-
regexp_parser (2.
|
|
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.
|
|
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.
|
|
91
|
-
rubocop (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 (
|
|
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.
|
|
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.
|
|
107
|
+
rubocop-ast (1.49.1)
|
|
103
108
|
parser (>= 3.3.7.2)
|
|
104
|
-
prism (~> 1.
|
|
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.
|
|
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)
|
data/Rakefile
CHANGED
|
@@ -30,6 +30,17 @@ task ci: %i[spec_all rubocop]
|
|
|
30
30
|
|
|
31
31
|
# Model exploration tasks
|
|
32
32
|
namespace :models do
|
|
33
|
+
desc "Fetch fresh model data from OpenRouter API and update local cache"
|
|
34
|
+
task :update do
|
|
35
|
+
require_relative "lib/open_router"
|
|
36
|
+
|
|
37
|
+
print "Fetching models from OpenRouter API..."
|
|
38
|
+
OpenRouter::ModelRegistry.refresh!
|
|
39
|
+
count = OpenRouter::ModelRegistry.all_models.size
|
|
40
|
+
puts " done. #{count} models cached."
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
|
|
33
44
|
desc "Display summary of available models"
|
|
34
45
|
task :summary do
|
|
35
46
|
require_relative "lib/open_router"
|
|
@@ -59,18 +70,18 @@ namespace :models do
|
|
|
59
70
|
end
|
|
60
71
|
|
|
61
72
|
# Cost analysis
|
|
62
|
-
input_costs = models.values.map { |spec| spec[:
|
|
63
|
-
output_costs = models.values.map { |spec| spec[:
|
|
73
|
+
input_costs = models.values.map { |spec| spec[:cost_per_token][:input] }.compact.sort
|
|
74
|
+
output_costs = models.values.map { |spec| spec[:cost_per_token][:output] }.compact.sort
|
|
64
75
|
|
|
65
|
-
puts "\n💰 Cost Analysis (per
|
|
76
|
+
puts "\n💰 Cost Analysis (per million tokens):"
|
|
66
77
|
puts " Input tokens:"
|
|
67
|
-
puts " Min: $#{format("%.
|
|
68
|
-
puts " Max: $#{format("%.
|
|
69
|
-
puts " Median: $#{format("%.
|
|
78
|
+
puts " Min: $#{format("%.4f", input_costs.min * 1_000_000)}"
|
|
79
|
+
puts " Max: $#{format("%.4f", input_costs.max * 1_000_000)}"
|
|
80
|
+
puts " Median: $#{format("%.4f", input_costs[input_costs.size / 2] * 1_000_000)}"
|
|
70
81
|
puts " Output tokens:"
|
|
71
|
-
puts " Min: $#{format("%.
|
|
72
|
-
puts " Max: $#{format("%.
|
|
73
|
-
puts " Median: $#{format("%.
|
|
82
|
+
puts " Min: $#{format("%.4f", output_costs.min * 1_000_000)}"
|
|
83
|
+
puts " Max: $#{format("%.4f", output_costs.max * 1_000_000)}"
|
|
84
|
+
puts " Median: $#{format("%.4f", output_costs[output_costs.size / 2] * 1_000_000)}"
|
|
74
85
|
|
|
75
86
|
# Context length analysis
|
|
76
87
|
context_lengths = models.values.map { |spec| spec[:context_length] }.compact.sort
|
|
@@ -269,8 +280,8 @@ namespace :models do
|
|
|
269
280
|
def self.display_model_info(model_id, specs, index)
|
|
270
281
|
puts "#{(index + 1).to_s.rjust(3)}. #{model_id}"
|
|
271
282
|
puts " Name: #{specs[:name]}" if specs[:name]
|
|
272
|
-
|
|
273
|
-
|
|
283
|
+
cpm = OpenRouter::ModelRegistry.cost_per_million(model_id)
|
|
284
|
+
puts " Cost: $#{format("%.4f", cpm[:input])}/M input, $#{format("%.4f", cpm[:output])}/M output"
|
|
274
285
|
puts " Context: #{format_number_with_commas(specs[:context_length])} tokens"
|
|
275
286
|
puts " Capabilities: #{specs[:capabilities].join(", ")}"
|
|
276
287
|
puts " Tier: #{specs[:performance_tier]}"
|
|
@@ -318,17 +329,17 @@ namespace :models do
|
|
|
318
329
|
def self.sort_by_strategy(candidates, strategy)
|
|
319
330
|
case strategy
|
|
320
331
|
when :cost
|
|
321
|
-
candidates.sort_by { |_, specs| specs[:
|
|
332
|
+
candidates.sort_by { |_, specs| specs[:cost_per_token][:input] }
|
|
322
333
|
when :performance
|
|
323
334
|
candidates.sort_by do |_, specs|
|
|
324
|
-
[specs[:performance_tier] == :premium ? 0 : 1, specs[:
|
|
335
|
+
[specs[:performance_tier] == :premium ? 0 : 1, specs[:cost_per_token][:input]]
|
|
325
336
|
end
|
|
326
337
|
when :latest
|
|
327
338
|
candidates.sort_by { |_, specs| -(specs[:created_at] || 0).to_i }
|
|
328
339
|
when :context
|
|
329
340
|
candidates.sort_by { |_, specs| -(specs[:context_length] || 0).to_i }
|
|
330
341
|
else
|
|
331
|
-
candidates.sort_by { |_, specs| specs[:
|
|
342
|
+
candidates.sort_by { |_, specs| specs[:cost_per_token][:input] }
|
|
332
343
|
end
|
|
333
344
|
end
|
|
334
345
|
end
|
data/lib/open_router/client.rb
CHANGED
|
@@ -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
|
-
|
|
20
|
-
|
|
21
|
-
OpenRouter.configuration.
|
|
22
|
-
|
|
23
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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
|
|
254
|
+
def response_format?
|
|
255
255
|
!response_format.nil?
|
|
256
256
|
end
|
|
257
257
|
|
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
|
|
@@ -135,7 +139,7 @@ module OpenRouter
|
|
|
135
139
|
|
|
136
140
|
models[model_id] = {
|
|
137
141
|
name: model_data["name"],
|
|
138
|
-
|
|
142
|
+
cost_per_token: {
|
|
139
143
|
input: model_data.dig("pricing", "prompt").to_f,
|
|
140
144
|
output: model_data.dig("pricing", "completion").to_f
|
|
141
145
|
},
|
|
@@ -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
|
|
@@ -254,12 +262,30 @@ module OpenRouter
|
|
|
254
262
|
model_info = get_model_info(model)
|
|
255
263
|
return 0 unless model_info
|
|
256
264
|
|
|
257
|
-
input_cost =
|
|
258
|
-
output_cost =
|
|
265
|
+
input_cost = input_tokens * model_info[:cost_per_token][:input]
|
|
266
|
+
output_cost = output_tokens * model_info[:cost_per_token][:output]
|
|
259
267
|
|
|
260
268
|
input_cost + output_cost
|
|
261
269
|
end
|
|
262
270
|
|
|
271
|
+
# Cost per 1,000 tokens — { input: Float, output: Float } or nil
|
|
272
|
+
def cost_per_thousand(model)
|
|
273
|
+
info = get_model_info(model)
|
|
274
|
+
return nil unless info
|
|
275
|
+
|
|
276
|
+
{ input: info[:cost_per_token][:input] * 1_000,
|
|
277
|
+
output: info[:cost_per_token][:output] * 1_000 }
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
# Cost per 1,000,000 tokens — { input: Float, output: Float } or nil
|
|
281
|
+
def cost_per_million(model)
|
|
282
|
+
info = get_model_info(model)
|
|
283
|
+
return nil unless info
|
|
284
|
+
|
|
285
|
+
{ input: info[:cost_per_token][:input] * 1_000_000,
|
|
286
|
+
output: info[:cost_per_token][:output] * 1_000_000 }
|
|
287
|
+
end
|
|
288
|
+
|
|
263
289
|
private
|
|
264
290
|
|
|
265
291
|
# Check if model specs meet the given requirements
|
|
@@ -271,11 +297,11 @@ module OpenRouter
|
|
|
271
297
|
end
|
|
272
298
|
|
|
273
299
|
# Check cost requirements
|
|
274
|
-
if requirements[:max_input_cost] && (specs[:
|
|
300
|
+
if requirements[:max_input_cost] && (specs[:cost_per_token][:input] > requirements[:max_input_cost])
|
|
275
301
|
return false
|
|
276
302
|
end
|
|
277
303
|
|
|
278
|
-
if requirements[:max_output_cost] && (specs[:
|
|
304
|
+
if requirements[:max_output_cost] && (specs[:cost_per_token][:output] > requirements[:max_output_cost])
|
|
279
305
|
return false
|
|
280
306
|
end
|
|
281
307
|
|
|
@@ -326,7 +352,7 @@ module OpenRouter
|
|
|
326
352
|
def calculate_model_cost(specs, _requirements)
|
|
327
353
|
# Simple cost calculation for sorting - could be made more sophisticated
|
|
328
354
|
# For now, just use input token cost as the primary metric
|
|
329
|
-
specs[:
|
|
355
|
+
specs[:cost_per_token][:input]
|
|
330
356
|
end
|
|
331
357
|
|
|
332
358
|
# Set up cleanup hook to manage cache size
|
|
@@ -355,9 +381,9 @@ module OpenRouter
|
|
|
355
381
|
total_size = Dir.glob(File.join(CACHE_DIR, "**/*"))
|
|
356
382
|
.select { |f| File.file?(f) }
|
|
357
383
|
.sum do |f|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
384
|
+
File.size(f)
|
|
385
|
+
rescue StandardError
|
|
386
|
+
0
|
|
361
387
|
end
|
|
362
388
|
total_size / (1024.0 * 1024.0)
|
|
363
389
|
end
|
|
@@ -343,7 +343,7 @@ module OpenRouter
|
|
|
343
343
|
all_candidates = filter_by_providers(ModelRegistry.all_models)
|
|
344
344
|
return nil if all_candidates.empty?
|
|
345
345
|
|
|
346
|
-
all_candidates.min_by { |_, specs| specs[:
|
|
346
|
+
all_candidates.min_by { |_, specs| specs[:cost_per_token][:input] }&.first
|
|
347
347
|
end
|
|
348
348
|
|
|
349
349
|
# Get detailed information about the current selection criteria
|
|
@@ -426,18 +426,18 @@ module OpenRouter
|
|
|
426
426
|
def apply_strategy_sorting(candidates)
|
|
427
427
|
case @strategy
|
|
428
428
|
when :cost
|
|
429
|
-
candidates.min_by { |_, specs| specs[:
|
|
429
|
+
candidates.min_by { |_, specs| specs[:cost_per_token][:input] }
|
|
430
430
|
when :performance
|
|
431
431
|
# Prefer premium tier, then by cost within tier
|
|
432
432
|
candidates.min_by do |_, specs|
|
|
433
|
-
[specs[:performance_tier] == :premium ? 0 : 1, specs[:
|
|
433
|
+
[specs[:performance_tier] == :premium ? 0 : 1, specs[:cost_per_token][:input]]
|
|
434
434
|
end
|
|
435
435
|
when :latest
|
|
436
436
|
candidates.max_by { |_, specs| (specs[:created_at] || 0).to_i }
|
|
437
437
|
when :context
|
|
438
438
|
candidates.max_by { |_, specs| (specs[:context_length] || 0).to_i }
|
|
439
439
|
else
|
|
440
|
-
candidates.min_by { |_, specs| specs[:
|
|
440
|
+
candidates.min_by { |_, specs| specs[:cost_per_token][:input] }
|
|
441
441
|
end
|
|
442
442
|
end
|
|
443
443
|
|
|
@@ -445,17 +445,17 @@ module OpenRouter
|
|
|
445
445
|
def apply_strategy_sorting_all(candidates)
|
|
446
446
|
case @strategy
|
|
447
447
|
when :cost
|
|
448
|
-
candidates.sort_by { |_, specs| specs[:
|
|
448
|
+
candidates.sort_by { |_, specs| specs[:cost_per_token][:input] }
|
|
449
449
|
when :performance
|
|
450
450
|
candidates.sort_by do |_, specs|
|
|
451
|
-
[specs[:performance_tier] == :premium ? 0 : 1, specs[:
|
|
451
|
+
[specs[:performance_tier] == :premium ? 0 : 1, specs[:cost_per_token][:input]]
|
|
452
452
|
end
|
|
453
453
|
when :latest
|
|
454
454
|
candidates.sort_by { |_, specs| -(specs[:created_at] || 0).to_i }
|
|
455
455
|
when :context
|
|
456
456
|
candidates.sort_by { |_, specs| -(specs[:context_length] || 0).to_i }
|
|
457
457
|
else
|
|
458
|
-
candidates.sort_by { |_, specs| specs[:
|
|
458
|
+
candidates.sort_by { |_, specs| specs[:cost_per_token][:input] }
|
|
459
459
|
end
|
|
460
460
|
end
|
|
461
461
|
end
|
data/lib/open_router/response.rb
CHANGED
|
@@ -110,7 +110,12 @@ module OpenRouter
|
|
|
110
110
|
end
|
|
111
111
|
end
|
|
112
112
|
|
|
113
|
-
|
|
113
|
+
# Use a flag rather than ||= so nil results don't trigger re-parsing on every call
|
|
114
|
+
unless @structured_output_computed
|
|
115
|
+
@structured_output = result
|
|
116
|
+
@structured_output_computed = true
|
|
117
|
+
end
|
|
118
|
+
@structured_output
|
|
114
119
|
when :gentle
|
|
115
120
|
# New gentle mode: best-effort parsing, no healing, no validation
|
|
116
121
|
content_to_parse = @forced_extraction ? extract_json_from_text(content) : content
|
data/lib/open_router/version.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: 2.
|
|
4
|
+
version: 2.1.0
|
|
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
|