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
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module OpenRouter
|
|
6
|
+
class ToolCallError < Error; end
|
|
7
|
+
|
|
8
|
+
class ToolCall
|
|
9
|
+
attr_reader :id, :type, :function_name, :arguments_string
|
|
10
|
+
|
|
11
|
+
def initialize(tool_call_data)
|
|
12
|
+
@id = tool_call_data["id"]
|
|
13
|
+
@type = tool_call_data["type"]
|
|
14
|
+
|
|
15
|
+
raise ToolCallError, "Invalid tool call data: missing function" unless tool_call_data["function"]
|
|
16
|
+
|
|
17
|
+
@function_name = tool_call_data["function"]["name"]
|
|
18
|
+
@arguments_string = tool_call_data["function"]["arguments"]
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Parse the arguments JSON string into a Ruby hash
|
|
22
|
+
def arguments
|
|
23
|
+
@arguments ||= begin
|
|
24
|
+
JSON.parse(@arguments_string)
|
|
25
|
+
rescue JSON::ParserError => e
|
|
26
|
+
raise ToolCallError, "Failed to parse tool call arguments: #{e.message}"
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Get the function name (alias for consistency)
|
|
31
|
+
def name
|
|
32
|
+
@function_name
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Execute the tool call with a provided block
|
|
36
|
+
# The block should accept (name, arguments) and return the result
|
|
37
|
+
def execute(&block)
|
|
38
|
+
raise ArgumentError, "Block required for tool execution" unless block_given?
|
|
39
|
+
|
|
40
|
+
begin
|
|
41
|
+
result = block.call(@function_name, arguments)
|
|
42
|
+
ToolResult.new(self, result)
|
|
43
|
+
rescue StandardError => e
|
|
44
|
+
ToolResult.new(self, nil, e.message)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Convert this tool call to a message format for conversation continuation
|
|
49
|
+
def to_message
|
|
50
|
+
{
|
|
51
|
+
role: "assistant",
|
|
52
|
+
content: nil,
|
|
53
|
+
tool_calls: [
|
|
54
|
+
{
|
|
55
|
+
id: @id,
|
|
56
|
+
type: @type,
|
|
57
|
+
function: {
|
|
58
|
+
name: @function_name,
|
|
59
|
+
arguments: @arguments_string
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
]
|
|
63
|
+
}
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Convert a tool result to a tool message for the conversation
|
|
67
|
+
def to_result_message(result)
|
|
68
|
+
content = case result
|
|
69
|
+
when String then result
|
|
70
|
+
when nil then ""
|
|
71
|
+
else result.to_json
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
{
|
|
75
|
+
role: "tool",
|
|
76
|
+
tool_call_id: @id,
|
|
77
|
+
content: content
|
|
78
|
+
}
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def to_h
|
|
82
|
+
{
|
|
83
|
+
id: @id,
|
|
84
|
+
type: @type,
|
|
85
|
+
function: {
|
|
86
|
+
name: @function_name,
|
|
87
|
+
arguments: @arguments_string
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def to_json(*args)
|
|
93
|
+
to_h.to_json(*args)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Validate against a provided array of tools (Tool instances or hashes)
|
|
97
|
+
def valid?(tools:)
|
|
98
|
+
# tools is now a required keyword argument
|
|
99
|
+
|
|
100
|
+
schema = find_schema_for_call(tools)
|
|
101
|
+
return true unless schema # No validation if tool not found
|
|
102
|
+
|
|
103
|
+
return JSON::Validator.validate(schema, arguments) if validation_available?
|
|
104
|
+
|
|
105
|
+
# Fallback: shallow required check
|
|
106
|
+
required = Array(schema[:required]).map(&:to_s)
|
|
107
|
+
required.all? { |k| arguments.key?(k) }
|
|
108
|
+
rescue StandardError
|
|
109
|
+
false
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def validation_errors(tools:)
|
|
113
|
+
# tools is now a required keyword argument
|
|
114
|
+
|
|
115
|
+
schema = find_schema_for_call(tools)
|
|
116
|
+
return [] unless schema # No errors if tool not found
|
|
117
|
+
|
|
118
|
+
return JSON::Validator.fully_validate(schema, arguments) if validation_available?
|
|
119
|
+
|
|
120
|
+
# Fallback: check required fields
|
|
121
|
+
required = Array(schema[:required]).map(&:to_s)
|
|
122
|
+
missing = required.reject { |k| arguments.key?(k) }
|
|
123
|
+
missing.any? ? ["Missing required keys: #{missing.join(", ")}"] : []
|
|
124
|
+
rescue StandardError => e
|
|
125
|
+
["Validation error: #{e.message}"]
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
private
|
|
129
|
+
|
|
130
|
+
# Check if JSON schema validation is available
|
|
131
|
+
def validation_available?
|
|
132
|
+
!!defined?(JSON::Validator)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def find_schema_for_call(tools)
|
|
136
|
+
tool = Array(tools).find do |t|
|
|
137
|
+
t_name = t.is_a?(OpenRouter::Tool) ? t.name : t.dig(:function, :name)
|
|
138
|
+
t_name == @function_name
|
|
139
|
+
end
|
|
140
|
+
return nil unless tool
|
|
141
|
+
|
|
142
|
+
params = tool.is_a?(OpenRouter::Tool) ? tool.parameters : tool.dig(:function, :parameters)
|
|
143
|
+
params.is_a?(Hash) ? params : nil
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Represents the result of executing a tool call
|
|
148
|
+
class ToolResult
|
|
149
|
+
attr_reader :tool_call, :result, :error
|
|
150
|
+
|
|
151
|
+
def initialize(tool_call, result = nil, error = nil)
|
|
152
|
+
@tool_call = tool_call
|
|
153
|
+
@result = result
|
|
154
|
+
@error = error
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def success?
|
|
158
|
+
@error.nil?
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def failure?
|
|
162
|
+
!success?
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Convert to message format for conversation continuation
|
|
166
|
+
def to_message
|
|
167
|
+
@tool_call.to_result_message(@error || @result)
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# Create a failed result
|
|
171
|
+
def self.failure(tool_call, error)
|
|
172
|
+
new(tool_call, nil, error)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# Create a successful result
|
|
176
|
+
def self.success(tool_call, result)
|
|
177
|
+
new(tool_call, result, nil)
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenRouter
|
|
4
|
+
# Tracks token usage and costs across API calls
|
|
5
|
+
class UsageTracker
|
|
6
|
+
attr_reader :total_prompt_tokens, :total_completion_tokens, :total_cached_tokens,
|
|
7
|
+
:total_cost, :request_count, :model_usage, :session_start
|
|
8
|
+
|
|
9
|
+
def initialize
|
|
10
|
+
reset!
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Reset all tracking counters
|
|
14
|
+
def reset!
|
|
15
|
+
@total_prompt_tokens = 0
|
|
16
|
+
@total_completion_tokens = 0
|
|
17
|
+
@total_cached_tokens = 0
|
|
18
|
+
@total_cost = 0.0
|
|
19
|
+
@request_count = 0
|
|
20
|
+
@model_usage = Hash.new { |h, k| h[k] = create_model_stats }
|
|
21
|
+
@session_start = Time.now
|
|
22
|
+
@request_history = []
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Track usage from a response
|
|
26
|
+
#
|
|
27
|
+
# @param response [Response] The response object to track
|
|
28
|
+
# @param model [String] The model used (optional, will try to get from response)
|
|
29
|
+
def track(response, model: nil)
|
|
30
|
+
return unless response
|
|
31
|
+
|
|
32
|
+
model ||= response.model
|
|
33
|
+
prompt_tokens = response.prompt_tokens
|
|
34
|
+
completion_tokens = response.completion_tokens
|
|
35
|
+
cached_tokens = response.cached_tokens
|
|
36
|
+
cost = response.cost_estimate || estimate_cost(model, prompt_tokens, completion_tokens)
|
|
37
|
+
|
|
38
|
+
# Update totals
|
|
39
|
+
@total_prompt_tokens += prompt_tokens
|
|
40
|
+
@total_completion_tokens += completion_tokens
|
|
41
|
+
@total_cached_tokens += cached_tokens
|
|
42
|
+
@total_cost += cost if cost
|
|
43
|
+
@request_count += 1
|
|
44
|
+
|
|
45
|
+
# Update per-model stats
|
|
46
|
+
if model
|
|
47
|
+
@model_usage[model][:prompt_tokens] += prompt_tokens
|
|
48
|
+
@model_usage[model][:completion_tokens] += completion_tokens
|
|
49
|
+
@model_usage[model][:cached_tokens] += cached_tokens
|
|
50
|
+
@model_usage[model][:cost] += cost if cost
|
|
51
|
+
@model_usage[model][:requests] += 1
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Store in history
|
|
55
|
+
@request_history << {
|
|
56
|
+
timestamp: Time.now,
|
|
57
|
+
model: model,
|
|
58
|
+
prompt_tokens: prompt_tokens,
|
|
59
|
+
completion_tokens: completion_tokens,
|
|
60
|
+
cached_tokens: cached_tokens,
|
|
61
|
+
cost: cost,
|
|
62
|
+
response_id: response.id
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
self
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Get total tokens used
|
|
69
|
+
def total_tokens
|
|
70
|
+
@total_prompt_tokens + @total_completion_tokens
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Get average tokens per request
|
|
74
|
+
def average_tokens_per_request
|
|
75
|
+
return 0 if @request_count.zero?
|
|
76
|
+
|
|
77
|
+
total_tokens.to_f / @request_count
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Get average cost per request
|
|
81
|
+
def average_cost_per_request
|
|
82
|
+
return 0 if @request_count.zero?
|
|
83
|
+
|
|
84
|
+
@total_cost / @request_count
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Get session duration in seconds
|
|
88
|
+
def session_duration
|
|
89
|
+
Time.now - @session_start
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Get tokens per second
|
|
93
|
+
def tokens_per_second
|
|
94
|
+
duration = session_duration
|
|
95
|
+
return 0 if duration.zero?
|
|
96
|
+
|
|
97
|
+
total_tokens.to_f / duration
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Get most used model
|
|
101
|
+
def most_used_model
|
|
102
|
+
return nil if @model_usage.empty?
|
|
103
|
+
|
|
104
|
+
@model_usage.max_by { |_, stats| stats[:requests] }&.first
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Get most expensive model
|
|
108
|
+
def most_expensive_model
|
|
109
|
+
return nil if @model_usage.empty?
|
|
110
|
+
|
|
111
|
+
@model_usage.max_by { |_, stats| stats[:cost] }&.first
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Get cache hit rate
|
|
115
|
+
def cache_hit_rate
|
|
116
|
+
return 0 if @total_prompt_tokens.zero?
|
|
117
|
+
|
|
118
|
+
(@total_cached_tokens.to_f / @total_prompt_tokens) * 100
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Get usage summary
|
|
122
|
+
def summary
|
|
123
|
+
{
|
|
124
|
+
session: {
|
|
125
|
+
start: @session_start,
|
|
126
|
+
duration_seconds: session_duration,
|
|
127
|
+
requests: @request_count
|
|
128
|
+
},
|
|
129
|
+
tokens: {
|
|
130
|
+
total: total_tokens,
|
|
131
|
+
prompt: @total_prompt_tokens,
|
|
132
|
+
completion: @total_completion_tokens,
|
|
133
|
+
cached: @total_cached_tokens,
|
|
134
|
+
cache_hit_rate: "#{cache_hit_rate.round(2)}%"
|
|
135
|
+
},
|
|
136
|
+
cost: {
|
|
137
|
+
total: @total_cost.round(4),
|
|
138
|
+
average_per_request: average_cost_per_request.round(4)
|
|
139
|
+
},
|
|
140
|
+
performance: {
|
|
141
|
+
tokens_per_second: tokens_per_second.round(2),
|
|
142
|
+
average_tokens_per_request: average_tokens_per_request.round(0)
|
|
143
|
+
},
|
|
144
|
+
models: {
|
|
145
|
+
most_used: most_used_model,
|
|
146
|
+
most_expensive: most_expensive_model,
|
|
147
|
+
breakdown: model_breakdown
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Get model usage breakdown
|
|
153
|
+
def model_breakdown
|
|
154
|
+
@model_usage.transform_values do |stats|
|
|
155
|
+
{
|
|
156
|
+
requests: stats[:requests],
|
|
157
|
+
tokens: stats[:prompt_tokens] + stats[:completion_tokens],
|
|
158
|
+
cost: stats[:cost].round(4),
|
|
159
|
+
cached_tokens: stats[:cached_tokens]
|
|
160
|
+
}
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# Export usage history as CSV
|
|
165
|
+
def export_csv
|
|
166
|
+
require "csv"
|
|
167
|
+
|
|
168
|
+
CSV.generate do |csv|
|
|
169
|
+
csv << ["Timestamp", "Model", "Prompt Tokens", "Completion Tokens", "Cached Tokens", "Cost", "Response ID"]
|
|
170
|
+
@request_history.each do |entry|
|
|
171
|
+
csv << [
|
|
172
|
+
entry[:timestamp].iso8601,
|
|
173
|
+
entry[:model],
|
|
174
|
+
entry[:prompt_tokens],
|
|
175
|
+
entry[:completion_tokens],
|
|
176
|
+
entry[:cached_tokens],
|
|
177
|
+
entry[:cost]&.round(4),
|
|
178
|
+
entry[:response_id]
|
|
179
|
+
]
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# Get request history
|
|
185
|
+
def history(limit: nil)
|
|
186
|
+
limit ? @request_history.last(limit) : @request_history
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# Pretty print summary to console
|
|
190
|
+
def print_summary
|
|
191
|
+
summary_data = summary
|
|
192
|
+
|
|
193
|
+
puts "\n#{"=" * 60}"
|
|
194
|
+
puts " OpenRouter Usage Summary"
|
|
195
|
+
puts "=" * 60
|
|
196
|
+
|
|
197
|
+
puts "\n📊 Session"
|
|
198
|
+
puts " Started: #{summary_data[:session][:start].strftime("%Y-%m-%d %H:%M:%S")}"
|
|
199
|
+
puts " Duration: #{format_duration(summary_data[:session][:duration_seconds])}"
|
|
200
|
+
puts " Requests: #{summary_data[:session][:requests]}"
|
|
201
|
+
|
|
202
|
+
puts "\n🔤 Tokens"
|
|
203
|
+
puts " Total: #{format_number(summary_data[:tokens][:total])}"
|
|
204
|
+
puts " Prompt: #{format_number(summary_data[:tokens][:prompt])}"
|
|
205
|
+
puts " Completion: #{format_number(summary_data[:tokens][:completion])}"
|
|
206
|
+
puts " Cached: #{format_number(summary_data[:tokens][:cached])} (#{summary_data[:tokens][:cache_hit_rate]})"
|
|
207
|
+
|
|
208
|
+
puts "\n💰 Cost"
|
|
209
|
+
puts " Total: $#{summary_data[:cost][:total]}"
|
|
210
|
+
puts " Average/Request: $#{summary_data[:cost][:average_per_request]}"
|
|
211
|
+
|
|
212
|
+
puts "\n⚡ Performance"
|
|
213
|
+
puts " Tokens/Second: #{summary_data[:performance][:tokens_per_second]}"
|
|
214
|
+
puts " Average Tokens/Request: #{summary_data[:performance][:average_tokens_per_request]}"
|
|
215
|
+
|
|
216
|
+
if summary_data[:models][:breakdown].any?
|
|
217
|
+
puts "\n🤖 Models Used"
|
|
218
|
+
summary_data[:models][:breakdown].each do |model, stats|
|
|
219
|
+
puts " #{model}:"
|
|
220
|
+
puts " Requests: #{stats[:requests]}"
|
|
221
|
+
puts " Tokens: #{format_number(stats[:tokens])}"
|
|
222
|
+
puts " Cost: $#{stats[:cost]}"
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
puts "\n#{"=" * 60}"
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
private
|
|
230
|
+
|
|
231
|
+
def create_model_stats
|
|
232
|
+
{
|
|
233
|
+
prompt_tokens: 0,
|
|
234
|
+
completion_tokens: 0,
|
|
235
|
+
cached_tokens: 0,
|
|
236
|
+
cost: 0.0,
|
|
237
|
+
requests: 0
|
|
238
|
+
}
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
# Estimate cost if not available from response
|
|
242
|
+
def estimate_cost(model, prompt_tokens, completion_tokens)
|
|
243
|
+
return 0 unless model
|
|
244
|
+
|
|
245
|
+
# Try to get pricing from model registry
|
|
246
|
+
model_data = ModelRegistry.get_model(model)
|
|
247
|
+
return 0 unless model_data
|
|
248
|
+
|
|
249
|
+
pricing = model_data["pricing"]
|
|
250
|
+
return 0 unless pricing
|
|
251
|
+
|
|
252
|
+
prompt_cost = (prompt_tokens / 1_000_000.0) * pricing["prompt"].to_f
|
|
253
|
+
completion_cost = (completion_tokens / 1_000_000.0) * pricing["completion"].to_f
|
|
254
|
+
|
|
255
|
+
prompt_cost + completion_cost
|
|
256
|
+
rescue StandardError
|
|
257
|
+
0
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
def format_number(num)
|
|
261
|
+
num.to_s.reverse.gsub(/(\d{3})(?=\d)/, '\\1,').reverse
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
def format_duration(seconds)
|
|
265
|
+
hours = seconds / 3600
|
|
266
|
+
minutes = (seconds % 3600) / 60
|
|
267
|
+
seconds %= 60
|
|
268
|
+
|
|
269
|
+
parts = []
|
|
270
|
+
parts << "#{hours.to_i}h" if hours >= 1
|
|
271
|
+
parts << "#{minutes.to_i}m" if minutes >= 1
|
|
272
|
+
parts << "#{seconds.to_i}s"
|
|
273
|
+
|
|
274
|
+
parts.join(" ")
|
|
275
|
+
end
|
|
276
|
+
end
|
|
277
|
+
end
|
data/lib/open_router.rb
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "faraday"
|
|
4
|
+
require "faraday/multipart"
|
|
5
|
+
|
|
6
|
+
begin
|
|
7
|
+
require "faraday_middleware"
|
|
8
|
+
module OpenRouter; HAS_JSON_MW = true; end
|
|
9
|
+
rescue LoadError
|
|
10
|
+
module OpenRouter; HAS_JSON_MW = false; end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
module OpenRouter
|
|
14
|
+
class Error < StandardError; end
|
|
15
|
+
class ConfigurationError < Error; end
|
|
16
|
+
class CapabilityError < Error; end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
require_relative "open_router/http"
|
|
20
|
+
require_relative "open_router/tool"
|
|
21
|
+
require_relative "open_router/tool_call"
|
|
22
|
+
require_relative "open_router/schema"
|
|
23
|
+
require_relative "open_router/json_healer"
|
|
24
|
+
require_relative "open_router/response"
|
|
25
|
+
require_relative "open_router/model_registry"
|
|
26
|
+
require_relative "open_router/model_selector"
|
|
27
|
+
require_relative "open_router/prompt_template"
|
|
28
|
+
require_relative "open_router/usage_tracker"
|
|
29
|
+
require_relative "open_router/client"
|
|
30
|
+
require_relative "open_router/streaming_client"
|
|
31
|
+
require_relative "open_router/version"
|
|
32
|
+
|
|
33
|
+
module OpenRouter
|
|
34
|
+
class Configuration
|
|
35
|
+
attr_writer :access_token
|
|
36
|
+
attr_accessor :api_version, :extra_headers, :faraday_config, :log_errors, :request_timeout, :uri_base
|
|
37
|
+
|
|
38
|
+
# Healing configuration
|
|
39
|
+
attr_accessor :auto_heal_responses, :healer_model, :max_heal_attempts
|
|
40
|
+
|
|
41
|
+
# Cache configuration
|
|
42
|
+
attr_accessor :cache_ttl
|
|
43
|
+
|
|
44
|
+
# Model registry configuration
|
|
45
|
+
attr_accessor :model_registry_timeout, :model_registry_retries
|
|
46
|
+
|
|
47
|
+
# Capability validation configuration
|
|
48
|
+
attr_accessor :strict_mode
|
|
49
|
+
|
|
50
|
+
# Automatic forcing configuration
|
|
51
|
+
attr_accessor :auto_force_on_unsupported_models
|
|
52
|
+
|
|
53
|
+
# Default structured output mode configuration
|
|
54
|
+
attr_accessor :default_structured_output_mode
|
|
55
|
+
|
|
56
|
+
DEFAULT_API_VERSION = "v1"
|
|
57
|
+
DEFAULT_REQUEST_TIMEOUT = 120
|
|
58
|
+
DEFAULT_URI_BASE = "https://openrouter.ai/api"
|
|
59
|
+
DEFAULT_CACHE_TTL = 7 * 24 * 60 * 60 # 7 days in seconds
|
|
60
|
+
DEFAULT_MODEL_REGISTRY_TIMEOUT = 30
|
|
61
|
+
DEFAULT_MODEL_REGISTRY_RETRIES = 3
|
|
62
|
+
|
|
63
|
+
def initialize
|
|
64
|
+
self.access_token = nil
|
|
65
|
+
self.api_version = DEFAULT_API_VERSION
|
|
66
|
+
self.extra_headers = {}
|
|
67
|
+
self.log_errors = false
|
|
68
|
+
self.request_timeout = DEFAULT_REQUEST_TIMEOUT
|
|
69
|
+
self.uri_base = DEFAULT_URI_BASE
|
|
70
|
+
|
|
71
|
+
# Healing defaults
|
|
72
|
+
self.auto_heal_responses = false
|
|
73
|
+
self.healer_model = "openai/gpt-4o-mini"
|
|
74
|
+
self.max_heal_attempts = 2
|
|
75
|
+
|
|
76
|
+
# Cache defaults
|
|
77
|
+
self.cache_ttl = ENV.fetch("OPENROUTER_CACHE_TTL", DEFAULT_CACHE_TTL).to_i
|
|
78
|
+
|
|
79
|
+
# Model registry defaults
|
|
80
|
+
self.model_registry_timeout = ENV.fetch("OPENROUTER_REGISTRY_TIMEOUT", DEFAULT_MODEL_REGISTRY_TIMEOUT).to_i
|
|
81
|
+
self.model_registry_retries = ENV.fetch("OPENROUTER_REGISTRY_RETRIES", DEFAULT_MODEL_REGISTRY_RETRIES).to_i
|
|
82
|
+
|
|
83
|
+
# Capability validation defaults
|
|
84
|
+
self.strict_mode = ENV.fetch("OPENROUTER_STRICT_MODE", "false").downcase == "true"
|
|
85
|
+
|
|
86
|
+
# Auto forcing defaults
|
|
87
|
+
self.auto_force_on_unsupported_models = ENV.fetch("OPENROUTER_AUTO_FORCE", "true").downcase == "true"
|
|
88
|
+
|
|
89
|
+
# Default structured output mode
|
|
90
|
+
self.default_structured_output_mode = ENV.fetch("OPENROUTER_DEFAULT_MODE", "strict").to_sym
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def access_token
|
|
94
|
+
return @access_token if @access_token
|
|
95
|
+
|
|
96
|
+
raise ConfigurationError, "OpenRouter access token missing!"
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def faraday(&block)
|
|
100
|
+
self.faraday_config = block
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def site_name=(value)
|
|
104
|
+
@extra_headers["X-Title"] = value
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def site_url=(value)
|
|
108
|
+
@extra_headers["HTTP-Referer"] = value
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
class << self
|
|
113
|
+
attr_writer :configuration
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def self.configuration
|
|
117
|
+
@configuration ||= OpenRouter::Configuration.new
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def self.configure
|
|
121
|
+
yield(configuration)
|
|
122
|
+
end
|
|
123
|
+
end
|
data/sig/open_router.rbs
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
module OpenRouter
|
|
2
|
+
class Client
|
|
3
|
+
include OpenRouter::HTTP
|
|
4
|
+
|
|
5
|
+
def initialize: () { (OpenRouter::Configuration) -> void } -> void
|
|
6
|
+
|
|
7
|
+
def complete: (
|
|
8
|
+
messages: Array[Hash[Symbol, String]],
|
|
9
|
+
?model: String,
|
|
10
|
+
?providers: Array[String],
|
|
11
|
+
?transforms: Array[String],
|
|
12
|
+
?extras: Hash[Symbol, untyped],
|
|
13
|
+
?stream: Proc
|
|
14
|
+
) -> Hash[String, untyped]
|
|
15
|
+
|
|
16
|
+
def models: () -> Array[Hash[String, untyped]]
|
|
17
|
+
|
|
18
|
+
def query_generation_stats: (generation_id: String) -> Hash[String, untyped]
|
|
19
|
+
end
|
|
20
|
+
end
|