savvy_openrouter 0.2.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/.rspec +3 -0
- data/.rubocop.yml +50 -0
- data/CHANGELOG.md +23 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/LICENSE.txt +21 -0
- data/README.md +234 -0
- data/Rakefile +12 -0
- data/exe/savvy_openrouter +32 -0
- data/lib/generators/savvy_openrouter/install/install_generator.rb +17 -0
- data/lib/generators/savvy_openrouter/install/templates/savvy_openrouter.yml +53 -0
- data/lib/savvy_openrouter/api_call_logger.rb +93 -0
- data/lib/savvy_openrouter/client.rb +105 -0
- data/lib/savvy_openrouter/completion_retry_policy.rb +135 -0
- data/lib/savvy_openrouter/configuration.rb +156 -0
- data/lib/savvy_openrouter/connection.rb +316 -0
- data/lib/savvy_openrouter/connection_instrumentation.rb +133 -0
- data/lib/savvy_openrouter/errors.rb +35 -0
- data/lib/savvy_openrouter/resources/analytics.rb +13 -0
- data/lib/savvy_openrouter/resources/anthropic_messages.rb +14 -0
- data/lib/savvy_openrouter/resources/api_keys.rb +33 -0
- data/lib/savvy_openrouter/resources/audio.rb +19 -0
- data/lib/savvy_openrouter/resources/base.rb +23 -0
- data/lib/savvy_openrouter/resources/chat.rb +45 -0
- data/lib/savvy_openrouter/resources/credits.rb +13 -0
- data/lib/savvy_openrouter/resources/embeddings.rb +18 -0
- data/lib/savvy_openrouter/resources/endpoints.rb +13 -0
- data/lib/savvy_openrouter/resources/generations.rb +17 -0
- data/lib/savvy_openrouter/resources/guardrails.rb +61 -0
- data/lib/savvy_openrouter/resources/models.rb +57 -0
- data/lib/savvy_openrouter/resources/oauth.rb +17 -0
- data/lib/savvy_openrouter/resources/organization.rb +13 -0
- data/lib/savvy_openrouter/resources/providers.rb +13 -0
- data/lib/savvy_openrouter/resources/rerank.rb +14 -0
- data/lib/savvy_openrouter/resources/responses.rb +14 -0
- data/lib/savvy_openrouter/resources/videos.rb +53 -0
- data/lib/savvy_openrouter/resources/workspaces.rb +37 -0
- data/lib/savvy_openrouter/streaming.rb +32 -0
- data/lib/savvy_openrouter/version.rb +5 -0
- data/lib/savvy_openrouter.rb +13 -0
- data/savvy_openrouter-0.1.0.gem +0 -0
- data/sig/savvy_openrouter.rbs +150 -0
- metadata +165 -0
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module SavvyOpenrouter
|
|
6
|
+
# Persists OpenRouter HTTP exchanges when configured via +api_call_log+ (YAML or Client kwargs).
|
|
7
|
+
# Failures while saving never raise into application code.
|
|
8
|
+
class ApiCallLogger
|
|
9
|
+
DEFAULT_MAX_BODY_BYTES = 65_536
|
|
10
|
+
|
|
11
|
+
CANONICAL_KEYS = %w[
|
|
12
|
+
method path status duration_ms request_body response_body error_class error_message streaming
|
|
13
|
+
].freeze
|
|
14
|
+
|
|
15
|
+
class << self
|
|
16
|
+
def format_body_for_log(obj, max_bytes: DEFAULT_MAX_BODY_BYTES)
|
|
17
|
+
str =
|
|
18
|
+
case obj
|
|
19
|
+
when nil then +""
|
|
20
|
+
when String then obj.b
|
|
21
|
+
else
|
|
22
|
+
JSON.generate(obj)
|
|
23
|
+
end
|
|
24
|
+
str = redact_secrets(str)
|
|
25
|
+
truncate_bytes(str, max_bytes)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def redact_secrets(str)
|
|
31
|
+
s = str.dup
|
|
32
|
+
s.gsub!(/sk-or-v1-[A-Za-z0-9_-]+/, "sk-or-v1-[REDACTED]")
|
|
33
|
+
s.gsub!(/Bearer\s+[A-Za-z0-9._-]+/i, "Bearer [REDACTED]")
|
|
34
|
+
s
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def truncate_bytes(str, max_bytes)
|
|
38
|
+
return str if str.bytesize <= max_bytes
|
|
39
|
+
|
|
40
|
+
"#{str.byteslice(0, max_bytes)}…(truncated)"
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def initialize(config)
|
|
45
|
+
@config = config.is_a?(Hash) ? Configuration.stringify_keys_static(config) : {}
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def enabled?
|
|
49
|
+
m = @config["model"]
|
|
50
|
+
!m.nil? && !m.to_s.strip.empty? &&
|
|
51
|
+
@config["columns"].is_a?(Hash) && !@config["columns"].empty?
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def max_body_limit
|
|
55
|
+
n = @config["max_body_bytes"]
|
|
56
|
+
n.is_a?(Integer) && n.positive? ? n : DEFAULT_MAX_BODY_BYTES
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# +attrs+ uses canonical string keys (see CANONICAL_KEYS). Column mapping is applied before create.
|
|
60
|
+
def record(attrs)
|
|
61
|
+
return unless enabled?
|
|
62
|
+
|
|
63
|
+
row = build_row(attrs)
|
|
64
|
+
return if row.empty?
|
|
65
|
+
|
|
66
|
+
constantize_model(@config["model"].to_s.strip).create!(row)
|
|
67
|
+
rescue StandardError => e
|
|
68
|
+
warn "[savvy_openrouter] api_call_log skipped: #{e.class}: #{e.message}" if $VERBOSE
|
|
69
|
+
nil
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
def build_row(attrs)
|
|
75
|
+
cols = @config["columns"] || {}
|
|
76
|
+
attrs = Configuration.stringify_keys_static(attrs)
|
|
77
|
+
cols.each_with_object({}) do |(canonical, column_name), acc|
|
|
78
|
+
next unless CANONICAL_KEYS.include?(canonical.to_s)
|
|
79
|
+
next unless attrs.key?(canonical.to_s)
|
|
80
|
+
|
|
81
|
+
acc[column_name.to_s] = attrs[canonical.to_s]
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def constantize_model(name)
|
|
86
|
+
raise NameError, "blank model" if name.empty?
|
|
87
|
+
raise NameError, "invalid model name #{name.inspect}" if name.include?("..") || /[^A-Za-z0-9_:]/.match?(name)
|
|
88
|
+
|
|
89
|
+
parts = name.delete_prefix("::").split("::")
|
|
90
|
+
parts.reduce(Object) { |mod, piece| mod.const_get(piece) }
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "configuration"
|
|
4
|
+
require_relative "connection"
|
|
5
|
+
require_relative "resources/chat"
|
|
6
|
+
require_relative "resources/responses"
|
|
7
|
+
require_relative "resources/anthropic_messages"
|
|
8
|
+
require_relative "resources/embeddings"
|
|
9
|
+
require_relative "resources/rerank"
|
|
10
|
+
require_relative "resources/models"
|
|
11
|
+
require_relative "resources/credits"
|
|
12
|
+
require_relative "resources/providers"
|
|
13
|
+
require_relative "resources/generations"
|
|
14
|
+
require_relative "resources/endpoints"
|
|
15
|
+
require_relative "resources/analytics"
|
|
16
|
+
require_relative "resources/audio"
|
|
17
|
+
require_relative "resources/videos"
|
|
18
|
+
require_relative "resources/oauth"
|
|
19
|
+
require_relative "resources/api_keys"
|
|
20
|
+
require_relative "resources/organization"
|
|
21
|
+
require_relative "resources/guardrails"
|
|
22
|
+
require_relative "resources/workspaces"
|
|
23
|
+
|
|
24
|
+
module SavvyOpenrouter
|
|
25
|
+
class Client
|
|
26
|
+
attr_reader :config, :connection
|
|
27
|
+
|
|
28
|
+
def initialize(config_path: nil, **options)
|
|
29
|
+
@config = Configuration.new(config_path: config_path, **options)
|
|
30
|
+
@connection = Connection.new(@config)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def chat
|
|
34
|
+
@chat ||= Resources::Chat.new(self)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def responses
|
|
38
|
+
@responses ||= Resources::Responses.new(self)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def anthropic_messages
|
|
42
|
+
@anthropic_messages ||= Resources::AnthropicMessages.new(self)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def embeddings
|
|
46
|
+
@embeddings ||= Resources::Embeddings.new(self)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def rerank
|
|
50
|
+
@rerank ||= Resources::Rerank.new(self)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def models
|
|
54
|
+
@models ||= Resources::Models.new(self)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def credits
|
|
58
|
+
@credits ||= Resources::Credits.new(self)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def providers
|
|
62
|
+
@providers ||= Resources::Providers.new(self)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def generations
|
|
66
|
+
@generations ||= Resources::Generations.new(self)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def endpoints
|
|
70
|
+
@endpoints ||= Resources::Endpoints.new(self)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def analytics
|
|
74
|
+
@analytics ||= Resources::Analytics.new(self)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def audio
|
|
78
|
+
@audio ||= Resources::Audio.new(self)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def videos
|
|
82
|
+
@videos ||= Resources::Videos.new(self)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def oauth
|
|
86
|
+
@oauth ||= Resources::OAuth.new(self)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def api_keys
|
|
90
|
+
@api_keys ||= Resources::ApiKeys.new(self)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def organization
|
|
94
|
+
@organization ||= Resources::Organization.new(self)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def guardrails
|
|
98
|
+
@guardrails ||= Resources::Guardrails.new(self)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def workspaces
|
|
102
|
+
@workspaces ||= Resources::Workspaces.new(self)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SavvyOpenrouter
|
|
4
|
+
# Controls optional retries for +Chat#completions+ (non-streaming): empty/zero-token
|
|
5
|
+
# completions and selected HTTP errors from OpenRouter.
|
|
6
|
+
class CompletionRetryPolicy
|
|
7
|
+
DEFAULT_ON = {
|
|
8
|
+
"zero_completion_tokens" => true,
|
|
9
|
+
"empty_assistant_content" => true,
|
|
10
|
+
"rate_limit" => true,
|
|
11
|
+
"bad_gateway" => true,
|
|
12
|
+
"internal_server_error" => true,
|
|
13
|
+
"service_unavailable" => true
|
|
14
|
+
}.freeze
|
|
15
|
+
|
|
16
|
+
def initialize(raw)
|
|
17
|
+
@raw =
|
|
18
|
+
case raw
|
|
19
|
+
when false, nil then {}
|
|
20
|
+
when Hash then Configuration.stringify_keys_static(raw)
|
|
21
|
+
else
|
|
22
|
+
raise ArgumentError, "chat_retries must be a Hash or false"
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def max_attempts
|
|
27
|
+
n = @raw["max_attempts"]
|
|
28
|
+
i = Integer(n, exception: false)
|
|
29
|
+
i&.positive? ? i : 1
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def enabled?
|
|
33
|
+
max_attempts > 1
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def retry_http_error?(error)
|
|
37
|
+
return false unless enabled?
|
|
38
|
+
return false unless error.is_a?(SavvyOpenrouter::ApiError)
|
|
39
|
+
|
|
40
|
+
code = error.status_code
|
|
41
|
+
return false unless code
|
|
42
|
+
|
|
43
|
+
flag =
|
|
44
|
+
case code
|
|
45
|
+
when 429 then "rate_limit"
|
|
46
|
+
when 502 then "bad_gateway"
|
|
47
|
+
when 500, 501 then "internal_server_error"
|
|
48
|
+
when 503 then "service_unavailable"
|
|
49
|
+
end
|
|
50
|
+
flag ? on?(flag) : false
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def retry_response?(response)
|
|
54
|
+
return false unless enabled?
|
|
55
|
+
|
|
56
|
+
(on?("zero_completion_tokens") && zero_completion_tokens?(response)) ||
|
|
57
|
+
(on?("empty_assistant_content") && empty_assistant_content?(response))
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def wait_after_attempt(attempt_number)
|
|
61
|
+
base = integer_opt(@raw["base_delay_ms"], 400)
|
|
62
|
+
max_d = integer_opt(@raw["max_delay_ms"], 10_000)
|
|
63
|
+
exponential = @raw["exponential_backoff"] != false
|
|
64
|
+
jitter_ratio = float_opt(@raw["jitter_ratio"], 0.15)
|
|
65
|
+
|
|
66
|
+
delay_ms =
|
|
67
|
+
if exponential
|
|
68
|
+
base * (2**(attempt_number - 1))
|
|
69
|
+
else
|
|
70
|
+
base
|
|
71
|
+
end
|
|
72
|
+
delay_ms = [delay_ms, max_d].min
|
|
73
|
+
jitter = jitter_ratio.clamp(0.0, 1.0) * delay_ms * rand
|
|
74
|
+
sleep((delay_ms + jitter) / 1000.0)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
private
|
|
78
|
+
|
|
79
|
+
def on?(key)
|
|
80
|
+
merged_on[key.to_s] == true
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def merged_on
|
|
84
|
+
@merged_on ||= DEFAULT_ON.merge(explicit_on)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def explicit_on
|
|
88
|
+
h = @raw["on"]
|
|
89
|
+
h.is_a?(Hash) ? Configuration.stringify_keys_static(h) : {}
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def zero_completion_tokens?(response)
|
|
93
|
+
usage = dig_usage(response)
|
|
94
|
+
return false unless usage
|
|
95
|
+
|
|
96
|
+
ct = usage[:completion_tokens] || usage["completion_tokens"]
|
|
97
|
+
ct.nil? ? false : ct.to_i.zero?
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def empty_assistant_content?(response)
|
|
101
|
+
msg = first_assistant_message(response)
|
|
102
|
+
return false unless msg
|
|
103
|
+
|
|
104
|
+
tool_calls = msg[:tool_calls] || msg["tool_calls"]
|
|
105
|
+
return false if tool_calls.is_a?(Array) && !tool_calls.empty?
|
|
106
|
+
|
|
107
|
+
content = msg[:content] || msg["content"]
|
|
108
|
+
return true if content.nil?
|
|
109
|
+
|
|
110
|
+
!content.is_a?(String) || content.strip.empty?
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def dig_usage(response)
|
|
114
|
+
response[:usage] || response["usage"]
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def first_assistant_message(response)
|
|
118
|
+
choices = response[:choices] || response["choices"]
|
|
119
|
+
return nil unless choices.is_a?(Array) && choices.first
|
|
120
|
+
|
|
121
|
+
choice = choices.first
|
|
122
|
+
choice[:message] || choice["message"]
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def integer_opt(val, default)
|
|
126
|
+
i = Integer(val, exception: false)
|
|
127
|
+
i&.positive? ? i : default
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def float_opt(val, default)
|
|
131
|
+
f = Float(val, exception: false)
|
|
132
|
+
f.nil? || f.negative? ? default : f
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yaml"
|
|
4
|
+
|
|
5
|
+
module SavvyOpenrouter
|
|
6
|
+
class Configuration
|
|
7
|
+
attr_accessor :api_key, :base_url, :default_model, :http_referer, :app_title
|
|
8
|
+
attr_reader :defaults, :video_defaults, :responses_defaults, :api_call_log, :chat_retries
|
|
9
|
+
|
|
10
|
+
alias llm_model default_model
|
|
11
|
+
alias llm_model= default_model=
|
|
12
|
+
|
|
13
|
+
def self.load_file(path)
|
|
14
|
+
return {} unless path && File.file?(path)
|
|
15
|
+
|
|
16
|
+
data = YAML.safe_load(File.read(path), permitted_classes: [], permitted_symbols: [], aliases: true)
|
|
17
|
+
data.is_a?(Hash) ? stringify_keys_static(data) : {}
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def self.stringify_keys_static(hash)
|
|
21
|
+
hash.each_with_object({}) do |(k, v), acc|
|
|
22
|
+
acc[k.to_s] = v.is_a?(Hash) ? stringify_keys_static(v) : v
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Precedence: explicit keyword args (highest) > YAML file > ENV (lowest).
|
|
27
|
+
def initialize(config_path: nil, **options)
|
|
28
|
+
@defaults = {}
|
|
29
|
+
@video_defaults = {}
|
|
30
|
+
@responses_defaults = {}
|
|
31
|
+
@api_call_log = {}
|
|
32
|
+
@chat_retries = {}
|
|
33
|
+
load_from_env!
|
|
34
|
+
yaml_path = config_path || self.class.discover_config_file
|
|
35
|
+
merge_hash!(self.class.load_file(yaml_path)) if yaml_path
|
|
36
|
+
apply_options!(options)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def self.discover_config_file(cwd = Dir.pwd)
|
|
40
|
+
%w[config/savvy_openrouter.yml .savvy_openrouter.yml].each do |rel|
|
|
41
|
+
path = File.join(cwd, rel)
|
|
42
|
+
return path if File.file?(path)
|
|
43
|
+
end
|
|
44
|
+
nil
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def merge_hash!(hash)
|
|
48
|
+
return unless hash.is_a?(Hash)
|
|
49
|
+
|
|
50
|
+
hash = self.class.stringify_keys_static(hash)
|
|
51
|
+
@api_key = hash["api_key"] if hash.key?("api_key")
|
|
52
|
+
@base_url = hash["base_url"] if hash.key?("base_url")
|
|
53
|
+
@default_model = hash["default_model"] || hash["llm_model"] if hash.key?("default_model") || hash.key?("llm_model")
|
|
54
|
+
@http_referer = hash["http_referer"] if hash.key?("http_referer")
|
|
55
|
+
@app_title = hash["app_title"] || hash["x_title"] if hash.key?("app_title") || hash.key?("x_title")
|
|
56
|
+
@defaults = @defaults.merge(self.class.stringify_keys_static(hash["defaults"] || {}))
|
|
57
|
+
vd = hash["video_defaults"] || hash["defaults_video"]
|
|
58
|
+
@video_defaults = @video_defaults.merge(self.class.stringify_keys_static(vd || {}))
|
|
59
|
+
rd = hash["responses_defaults"] || hash["defaults_responses"]
|
|
60
|
+
@responses_defaults = @responses_defaults.merge(self.class.stringify_keys_static(rd || {}))
|
|
61
|
+
assign_api_call_log(hash["api_call_log"]) if hash.key?("api_call_log")
|
|
62
|
+
assign_chat_retries(hash["chat_retries"]) if hash.key?("chat_retries")
|
|
63
|
+
assign_chat_retries(hash["completion_retries"]) if hash.key?("completion_retries")
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def merge_chat_body(body)
|
|
67
|
+
body = stringify_keys(body)
|
|
68
|
+
merged = @defaults.merge(body)
|
|
69
|
+
merged["model"] ||= @default_model if @default_model && merged["model"].nil?
|
|
70
|
+
merged
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def merge_video_body(body)
|
|
74
|
+
body = stringify_keys(body)
|
|
75
|
+
merged = @video_defaults.merge(body)
|
|
76
|
+
merged["model"] ||= @default_model if @default_model && merged["model"].nil?
|
|
77
|
+
merged
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Defaults only for `POST /responses` (Responses API beta): `plugins`, `tools`,
|
|
81
|
+
# `max_output_tokens`, `x_search_filter`, etc. Keeps web-search settings off chat/embeddings bodies.
|
|
82
|
+
def merge_responses_body(body)
|
|
83
|
+
body = stringify_keys(body)
|
|
84
|
+
merged = @responses_defaults.merge(body)
|
|
85
|
+
merged["model"] ||= @default_model if @default_model && merged["model"].nil?
|
|
86
|
+
merged
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
private
|
|
90
|
+
|
|
91
|
+
def load_from_env!
|
|
92
|
+
@api_key = ENV.fetch("OPENROUTER_API_KEY", nil)
|
|
93
|
+
@base_url = ENV.fetch("OPENROUTER_BASE_URL", "https://openrouter.ai/api/v1")
|
|
94
|
+
@default_model = ENV.fetch("OPENROUTER_DEFAULT_MODEL", nil)
|
|
95
|
+
@http_referer = ENV.fetch("OPENROUTER_HTTP_REFERER", nil)
|
|
96
|
+
@app_title = ENV.fetch("OPENROUTER_APP_TITLE", nil)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def apply_options!(options)
|
|
100
|
+
opts = options.dup
|
|
101
|
+
@defaults = @defaults.merge(stringify_keys(opts.delete(:defaults) || {}))
|
|
102
|
+
vd = opts.delete(:video_defaults) || opts.delete(:defaults_video)
|
|
103
|
+
@video_defaults = @video_defaults.merge(stringify_keys(vd || {}))
|
|
104
|
+
rd = opts.delete(:responses_defaults) || opts.delete(:defaults_responses)
|
|
105
|
+
@responses_defaults = @responses_defaults.merge(stringify_keys(rd || {}))
|
|
106
|
+
|
|
107
|
+
@api_key = opts.delete(:api_key) if opts.key?(:api_key)
|
|
108
|
+
@base_url = opts.delete(:base_url) if opts.key?(:base_url)
|
|
109
|
+
if opts.key?(:default_model) || opts.key?(:llm_model)
|
|
110
|
+
@default_model = opts.delete(:default_model) || opts.delete(:llm_model)
|
|
111
|
+
end
|
|
112
|
+
@http_referer = opts.delete(:http_referer) if opts.key?(:http_referer)
|
|
113
|
+
@app_title = opts.delete(:app_title) if opts.key?(:app_title)
|
|
114
|
+
assign_api_call_log(opts.delete(:api_call_log)) if opts.key?(:api_call_log)
|
|
115
|
+
if opts.key?(:chat_retries)
|
|
116
|
+
assign_chat_retries(opts.delete(:chat_retries))
|
|
117
|
+
elsif opts.key?(:completion_retries)
|
|
118
|
+
assign_chat_retries(opts.delete(:completion_retries))
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
return if opts.empty?
|
|
122
|
+
|
|
123
|
+
raise ArgumentError, "Unknown keywords: #{opts.keys.join(", ")}"
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def stringify_keys(hash)
|
|
127
|
+
self.class.stringify_keys_static(hash || {})
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def assign_api_call_log(value)
|
|
131
|
+
case value
|
|
132
|
+
when false, nil
|
|
133
|
+
@api_call_log = {}
|
|
134
|
+
when Hash
|
|
135
|
+
h = self.class.stringify_keys_static(value)
|
|
136
|
+
h["columns"] = self.class.stringify_keys_static(h["columns"]) if h["columns"].is_a?(Hash)
|
|
137
|
+
@api_call_log = h
|
|
138
|
+
else
|
|
139
|
+
raise ArgumentError, "api_call_log must be a Hash or false"
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def assign_chat_retries(value)
|
|
144
|
+
case value
|
|
145
|
+
when false, nil
|
|
146
|
+
@chat_retries = {}
|
|
147
|
+
when Hash
|
|
148
|
+
h = self.class.stringify_keys_static(value)
|
|
149
|
+
h["on"] = self.class.stringify_keys_static(h["on"]) if h["on"].is_a?(Hash)
|
|
150
|
+
@chat_retries = h
|
|
151
|
+
else
|
|
152
|
+
raise ArgumentError, "chat_retries must be a Hash or false"
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|