savvy_openrouter 0.2.0 → 0.4.2

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.
@@ -0,0 +1,164 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "uri"
4
+ require_relative "patterns/structured_output"
5
+
6
+ module SavvyOpenrouter
7
+ # Optional chat / Responses request shaping (OpenRouter plugins). Pure Ruby.
8
+ #
9
+ # require "savvy_openrouter/request_plugins"
10
+ # SavvyOpenrouter::RequestPlugins.prepare_chat_body!(body, pdf_engine: "native")
11
+ module RequestPlugins
12
+ RESPONSE_HEALING_PLUGIN = { id: "response-healing" }.freeze
13
+ FILE_PARSER_DEFAULT_ENGINE = "cloudflare-ai"
14
+
15
+ module_function
16
+
17
+ def prepare_chat_body!(body, pdf_engine: nil)
18
+ return body unless body.is_a?(Hash)
19
+
20
+ ensure_response_healing_for_chat!(body) if SavvyOpenrouter::Patterns.chat_structured_requested?(body)
21
+ ensure_pdf_file_parser_plugin!(body, pdf_engine: pdf_engine)
22
+ body
23
+ end
24
+
25
+ def prepare_responses_body!(body)
26
+ return body unless body.is_a?(Hash)
27
+
28
+ ensure_response_healing_for_responses!(body) if SavvyOpenrouter::Patterns.responses_structured_requested?(body)
29
+ body
30
+ end
31
+
32
+ def ensure_response_healing_for_chat!(body)
33
+ plugins_arr = plugins_array_from(body)
34
+ return if response_healing_plugin_present?(plugins_arr)
35
+
36
+ body[:plugins] = plugins_arr + [RESPONSE_HEALING_PLUGIN.dup]
37
+ body.delete("plugins")
38
+ end
39
+
40
+ def ensure_response_healing_for_responses!(body)
41
+ plugins_arr = plugins_array_from(body)
42
+ return if response_healing_plugin_present?(plugins_arr)
43
+
44
+ body[:plugins] = plugins_arr + [RESPONSE_HEALING_PLUGIN.dup]
45
+ body.delete("plugins")
46
+ end
47
+
48
+ def plugins_array_from(body)
49
+ p = body[:plugins] || body["plugins"]
50
+ p.is_a?(Array) ? p.dup : []
51
+ end
52
+
53
+ def response_healing_plugin_present?(plugins)
54
+ plugin_present?(plugins, id: "response-healing")
55
+ end
56
+
57
+ # +pdf_engine+ — optional override (e.g. from config: +"native"+ for Gemini, +"mistral-ocr"+ for scans).
58
+ # When nil, uses {FILE_PARSER_DEFAULT_ENGINE}.
59
+ def ensure_pdf_file_parser_plugin!(body, pdf_engine: nil)
60
+ messages = body[:messages] || body["messages"]
61
+ return unless chat_messages_include_pdf_attachment?(messages)
62
+
63
+ engine = normalize_pdf_engine(pdf_engine)
64
+ plugins = plugins_array_from(body)
65
+ idx = plugins.find_index { |p| plugin_entry_id(p) == "file-parser" }
66
+
67
+ if idx
68
+ return if file_parser_pdf_engine_set?(plugins[idx])
69
+
70
+ fp = plugins[idx].dup
71
+ pdf_src = fp[:pdf] || fp["pdf"]
72
+ merged_pdf =
73
+ if pdf_src.is_a?(Hash)
74
+ pdf_src.transform_keys(&:to_sym)
75
+ else
76
+ {}
77
+ end
78
+ merged_pdf[:engine] = engine
79
+ fp[:pdf] = merged_pdf
80
+ fp.delete("pdf")
81
+ plugins[idx] = fp
82
+ body[:plugins] = plugins
83
+ else
84
+ body[:plugins] = plugins + [{ id: "file-parser", pdf: { engine: engine } }]
85
+ end
86
+ body.delete("plugins")
87
+ end
88
+
89
+ def normalize_pdf_engine(pdf_engine)
90
+ s = pdf_engine.to_s.strip
91
+ s.empty? ? FILE_PARSER_DEFAULT_ENGINE : s
92
+ end
93
+
94
+ def chat_messages_include_pdf_attachment?(messages)
95
+ return false unless messages.is_a?(Array)
96
+
97
+ messages.any? { |msg| message_includes_pdf_file?(msg) }
98
+ end
99
+
100
+ def message_includes_pdf_file?(msg)
101
+ return false unless msg.is_a?(Hash)
102
+
103
+ content = msg[:content] || msg["content"]
104
+ return false unless content.is_a?(Array)
105
+
106
+ content.any? { |part| content_part_is_pdf_file?(part) }
107
+ end
108
+
109
+ def content_part_is_pdf_file?(part)
110
+ return false unless part.is_a?(Hash)
111
+
112
+ type = (part[:type] || part["type"]).to_s
113
+ return false unless type == "file"
114
+
115
+ file = part[:file] || part["file"]
116
+ return false unless file.is_a?(Hash)
117
+
118
+ pdf_file_attachment?(file)
119
+ end
120
+
121
+ def pdf_file_attachment?(file)
122
+ name = (file[:filename] || file["filename"]).to_s
123
+ return true if name.match?(/\.pdf\z/i)
124
+
125
+ data = file[:file_data] || file["file_data"] || file[:fileData] || file["fileData"]
126
+ return false unless data.is_a?(String)
127
+
128
+ return true if data.match?(%r{\Adata:application/pdf(?:;|\z)}i)
129
+
130
+ pdf_url?(data)
131
+ end
132
+
133
+ def pdf_url?(url)
134
+ return false unless url.match?(%r{\Ahttps?://}i)
135
+
136
+ uri = URI.parse(url)
137
+ uri.path.to_s.match?(/\.pdf\z/i)
138
+ rescue URI::InvalidURIError
139
+ false
140
+ end
141
+
142
+ def file_parser_pdf_engine_set?(plugin)
143
+ return false unless plugin.is_a?(Hash)
144
+
145
+ pdf = plugin[:pdf] || plugin["pdf"]
146
+ return false unless pdf.is_a?(Hash)
147
+
148
+ eng = (pdf[:engine] || pdf["engine"]).to_s
149
+ !eng.strip.empty?
150
+ end
151
+
152
+ def plugin_present?(plugins, id:)
153
+ return false unless plugins.is_a?(Array)
154
+
155
+ plugins.any? { |p| plugin_entry_id(p) == id.to_s }
156
+ end
157
+
158
+ def plugin_entry_id(entry)
159
+ return unless entry.is_a?(Hash)
160
+
161
+ (entry[:id] || entry["id"]).to_s
162
+ end
163
+ end
164
+ end
@@ -11,6 +11,14 @@ module SavvyOpenrouter
11
11
 
12
12
  attr_reader :client
13
13
 
14
+ def logical_model_from_body(body)
15
+ return nil unless body.is_a?(Hash)
16
+
17
+ m = body[:model] || body["model"]
18
+ s = m.to_s.strip
19
+ s.empty? ? nil : s
20
+ end
21
+
14
22
  def conn
15
23
  client.connection
16
24
  end
@@ -10,35 +10,45 @@ module SavvyOpenrouter
10
10
  def completions(messages:, **params)
11
11
  policy = CompletionRetryPolicy.new(config.chat_retries)
12
12
  body = config.merge_chat_body({ messages: messages }.merge(params))
13
- attempt = 0
14
- last_response = nil
15
- loop do
16
- attempt += 1
13
+ lm = logical_model_from_body(body)
14
+ conn.with_call_context(endpoint: "chat_completions", logical_model: lm) do
15
+ attempt = 0
16
+ last_response = nil
17
17
  begin
18
- last_response = conn.post("/chat/completions", body: body)
19
- rescue SavvyOpenrouter::ApiError => e
20
- raise e if attempt >= policy.max_attempts || !policy.retry_http_error?(e)
18
+ loop do
19
+ attempt += 1
20
+ begin
21
+ last_response = conn.post("/chat/completions", body: body)
22
+ rescue SavvyOpenrouter::ApiError => e
23
+ raise e if attempt >= policy.max_attempts || !policy.retry_http_error?(e)
21
24
 
22
- policy.wait_after_attempt(attempt)
23
- next
24
- end
25
+ policy.wait_after_attempt(attempt)
26
+ next
27
+ end
25
28
 
26
- return last_response unless policy.retry_response?(last_response)
27
- return last_response if attempt >= policy.max_attempts
29
+ return last_response unless policy.retry_response?(last_response)
30
+ return last_response if attempt >= policy.max_attempts
28
31
 
29
- policy.wait_after_attempt(attempt)
32
+ policy.wait_after_attempt(attempt)
33
+ end
34
+ ensure
35
+ conn.flush_deferred_chat_log!
36
+ end
30
37
  end
31
38
  end
32
39
 
33
40
  # Yields each SSE `data:` payload string (JSON text from the model stream).
34
41
  def completions_stream(messages:, **params, &block)
35
42
  body = config.merge_chat_body({ messages: messages, stream: true }.merge(params))
36
- return enum_for(:completions_stream, messages: messages, **params) unless block
43
+ lm = logical_model_from_body(body)
44
+ conn.with_call_context(endpoint: "chat_completions", logical_model: lm) do
45
+ return enum_for(:completions_stream, messages: messages, **params) unless block
37
46
 
38
- chunk_enum = Enumerator.new do |y|
39
- conn.stream_post("/chat/completions", body) { |ch| y << ch }
47
+ chunk_enum = Enumerator.new do |y|
48
+ conn.stream_post("/chat/completions", body) { |ch| y << ch }
49
+ end
50
+ Streaming.each_sse_data(chunk_enum, &block)
40
51
  end
41
- Streaming.each_sse_data(chunk_enum, &block)
42
52
  end
43
53
  end
44
54
  end
@@ -7,11 +7,16 @@ module SavvyOpenrouter
7
7
  class Embeddings < Base
8
8
  def create(**params)
9
9
  body = config.merge_chat_body(params)
10
- conn.post("/embeddings", body: body)
10
+ lm = logical_model_from_body(body)
11
+ conn.with_call_context(endpoint: "embeddings", logical_model: lm) do
12
+ conn.post("/embeddings", body: body)
13
+ end
11
14
  end
12
15
 
13
16
  def models
14
- conn.get("/embeddings/models")
17
+ conn.with_call_context(endpoint: "embeddings_models", logical_model: nil) do
18
+ conn.get("/embeddings/models")
19
+ end
15
20
  end
16
21
  end
17
22
  end
@@ -6,11 +6,15 @@ module SavvyOpenrouter
6
6
  module Resources
7
7
  class Generations < Base
8
8
  def get(id:)
9
- conn.get("/generation", params: { id: id })
9
+ conn.with_call_context(endpoint: "generation", logical_model: nil) do
10
+ conn.get("/generation", params: { id: id })
11
+ end
10
12
  end
11
13
 
12
14
  def content(**params)
13
- conn.get("/generation/content", params: params)
15
+ conn.with_call_context(endpoint: "generation_content", logical_model: nil) do
16
+ conn.get("/generation/content", params: params)
17
+ end
14
18
  end
15
19
  end
16
20
  end
@@ -8,28 +8,38 @@ module SavvyOpenrouter
8
8
  # Query params match OpenRouter GET /models (e.g. category, output_modalities, supported_parameters).
9
9
  def list(**params)
10
10
  query = stringify_query(params)
11
- conn.get("/models", params: query.empty? ? nil : query)
11
+ conn.with_call_context(endpoint: "models", logical_model: nil) do
12
+ conn.get("/models", params: query.empty? ? nil : query)
13
+ end
12
14
  end
13
15
 
14
- # Uses GET /models with filters, then returns the first model whose prompt + completion pricing are zero.
15
- # OpenRouter returns models in curated rank order within a category; first matching free model aligns with
16
- # site “top free” picks when combined with output_modalities=text.
16
+ # Uses GET /models with category + free-price filters, then returns the first model whose prompt and
17
+ # completion pricing are both zero. OpenRouter returns models in curated rank order; the first free
18
+ # match aligns with site “top free” picks when combined with output_modalities=text.
19
+ # Models scheduled for removal (expiration_date) are still eligible — callers often want the current
20
+ # top free pick even when it is temporary.
17
21
  def first_ranked_free_text_model(category:, output_modalities: "text")
18
- res = list(category: category, output_modalities: output_modalities)
22
+ res = list(category: category, output_modalities: output_modalities, max_price: 0)
19
23
  data = res[:data] || []
20
24
  data.find { |m| free_pricing?(m) }
21
25
  end
22
26
 
23
27
  def count
24
- conn.get("/models/count")
28
+ conn.with_call_context(endpoint: "models_count", logical_model: nil) do
29
+ conn.get("/models/count")
30
+ end
25
31
  end
26
32
 
27
33
  def user
28
- conn.get("/models/user")
34
+ conn.with_call_context(endpoint: "models_user", logical_model: nil) do
35
+ conn.get("/models/user")
36
+ end
29
37
  end
30
38
 
31
39
  def endpoints(author:, slug:)
32
- conn.get("/models/#{author}/#{slug}/endpoints")
40
+ conn.with_call_context(endpoint: "models_endpoints", logical_model: nil) do
41
+ conn.get("/models/#{author}/#{slug}/endpoints")
42
+ end
33
43
  end
34
44
 
35
45
  private
@@ -1,13 +1,39 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "base"
4
+ require_relative "../responses_retry_policy"
4
5
 
5
6
  module SavvyOpenrouter
6
7
  module Resources
7
8
  class Responses < Base
8
9
  def create(**params)
10
+ policy = ResponsesRetryPolicy.new(config.responses_retries)
9
11
  body = config.merge_responses_body(params)
10
- conn.post("/responses", body: body)
12
+ lm = logical_model_from_body(body)
13
+ conn.with_call_context(endpoint: "responses", logical_model: lm) do
14
+ attempt = 0
15
+ last_response = nil
16
+ begin
17
+ loop do
18
+ attempt += 1
19
+ begin
20
+ last_response = conn.post("/responses", body: body)
21
+ rescue SavvyOpenrouter::ApiError => e
22
+ raise e if attempt >= policy.max_attempts || !policy.retry_http_error?(e)
23
+
24
+ policy.wait_after_attempt(attempt)
25
+ next
26
+ end
27
+
28
+ return last_response unless policy.retry_response?(last_response)
29
+ return last_response if attempt >= policy.max_attempts
30
+
31
+ policy.wait_after_attempt(attempt)
32
+ end
33
+ ensure
34
+ conn.flush_deferred_responses_log!
35
+ end
36
+ end
11
37
  end
12
38
  end
13
39
  end
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SavvyOpenrouter
4
+ # Retries for +Resources::Responses#create+ (zero output tokens / selected HTTP errors).
5
+ class ResponsesRetryPolicy
6
+ DEFAULT_ON = {
7
+ "zero_output_tokens" => true,
8
+ "rate_limit" => true,
9
+ "bad_gateway" => true,
10
+ "internal_server_error" => true,
11
+ "service_unavailable" => true
12
+ }.freeze
13
+
14
+ def initialize(raw)
15
+ @raw =
16
+ case raw
17
+ when false, nil then {}
18
+ when Hash then Configuration.stringify_keys_static(raw)
19
+ else
20
+ raise ArgumentError, "responses_retries must be a Hash or false"
21
+ end
22
+ end
23
+
24
+ def max_attempts
25
+ n = @raw["max_attempts"]
26
+ i = Integer(n, exception: false)
27
+ i&.positive? ? i : 1
28
+ end
29
+
30
+ def enabled?
31
+ max_attempts > 1
32
+ end
33
+
34
+ def retry_http_error?(error)
35
+ return false unless enabled?
36
+ return false unless error.is_a?(SavvyOpenrouter::ApiError)
37
+
38
+ code = error.status_code
39
+ return false unless code
40
+
41
+ flag =
42
+ case code
43
+ when 429 then "rate_limit"
44
+ when 502 then "bad_gateway"
45
+ when 500, 501 then "internal_server_error"
46
+ when 503 then "service_unavailable"
47
+ end
48
+ flag ? on?(flag) : false
49
+ end
50
+
51
+ def retry_response?(response)
52
+ return false unless enabled?
53
+
54
+ on?("zero_output_tokens") && responses_zero_output_retry?(response)
55
+ end
56
+
57
+ def wait_after_attempt(attempt_number)
58
+ base = integer_opt(@raw["base_delay_ms"], 400)
59
+ max_d = integer_opt(@raw["max_delay_ms"], 10_000)
60
+ exponential = @raw["exponential_backoff"] != false
61
+ jitter_ratio = float_opt(@raw["jitter_ratio"], 0.15)
62
+
63
+ delay_ms =
64
+ if exponential
65
+ base * (2**(attempt_number - 1))
66
+ else
67
+ base
68
+ end
69
+ delay_ms = [delay_ms, max_d].min
70
+ jitter = jitter_ratio.clamp(0.0, 1.0) * delay_ms * rand
71
+ sleep((delay_ms + jitter) / 1000.0)
72
+ end
73
+
74
+ private
75
+
76
+ def on?(key)
77
+ DEFAULT_ON.merge(explicit_on)[key.to_s] == true
78
+ end
79
+
80
+ def explicit_on
81
+ h = @raw["on"]
82
+ h.is_a?(Hash) ? Configuration.stringify_keys_static(h) : {}
83
+ end
84
+
85
+ # Aligns with OpenRouter zero-completion / empty Responses output heuristics.
86
+ def responses_zero_output_retry?(json)
87
+ return false unless json.is_a?(Hash)
88
+
89
+ usage = json[:usage] || json["usage"]
90
+ return false unless usage.is_a?(Hash)
91
+
92
+ out = usage[:output_tokens] || usage["output_tokens"]
93
+ return false if out.nil?
94
+ return false if out.to_i.positive?
95
+
96
+ status = (json[:status] || json["status"]).to_s
97
+ return true if %w[failed cancelled incomplete queued in_progress].include?(status)
98
+
99
+ status == "completed"
100
+ end
101
+
102
+ def integer_opt(val, default)
103
+ i = Integer(val, exception: false)
104
+ i&.positive? ? i : default
105
+ end
106
+
107
+ def float_opt(val, default)
108
+ f = Float(val, exception: false)
109
+ f.nil? || f.negative? ? default : f
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SavvyOpenrouter
4
+ # Raised when json_object/json_schema was requested but the successful response body
5
+ # does not contain usable JSON in assistant/output text. Optional +require "savvy_openrouter/patterns"+.
6
+ #
7
+ # reason: +:empty_content+, +:invalid_json+, +:no_choices+
8
+ class StructuredOutputError < StandardError
9
+ attr_reader :reason, :response_body
10
+
11
+ def initialize(message, reason:, response_body: nil)
12
+ @reason = reason
13
+ @response_body = response_body
14
+ super(message)
15
+ end
16
+ end
17
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SavvyOpenrouter
4
- VERSION = "0.2.0"
4
+ VERSION = "0.4.2"
5
5
  end
@@ -4,6 +4,7 @@ require_relative "savvy_openrouter/version"
4
4
  require_relative "savvy_openrouter/errors"
5
5
  require_relative "savvy_openrouter/configuration"
6
6
  require_relative "savvy_openrouter/completion_retry_policy"
7
+ require_relative "savvy_openrouter/responses_retry_policy"
7
8
  require_relative "savvy_openrouter/api_call_logger"
8
9
  require_relative "savvy_openrouter/connection"
9
10
  require_relative "savvy_openrouter/streaming"
@@ -40,6 +40,21 @@ module SavvyOpenrouter
40
40
  class TimeoutPollError < Error
41
41
  end
42
42
 
43
+ class StructuredOutputError < StandardError
44
+ attr_reader reason: Symbol
45
+ attr_reader response_body: untyped
46
+ def initialize: (String message, reason: Symbol, ?response_body: untyped) -> void
47
+ end
48
+
49
+ class ResponsesRetryPolicy
50
+ def initialize: (Hash[String, untyped] | false? raw) -> void
51
+ def max_attempts: () -> Integer
52
+ def enabled?: () -> bool
53
+ def retry_http_error?: (ApiError error) -> bool
54
+ def retry_response?: (Hash[Symbol | String, untyped] response) -> bool
55
+ def wait_after_attempt: (Integer attempt_number) -> void
56
+ end
57
+
43
58
  class Configuration
44
59
  attr_accessor api_key: String?
45
60
  attr_accessor base_url: String
@@ -51,6 +66,8 @@ module SavvyOpenrouter
51
66
  attr_reader responses_defaults: Hash[String, untyped]
52
67
  attr_reader api_call_log: Hash[String, untyped]
53
68
  attr_reader chat_retries: Hash[String, untyped]
69
+ attr_reader responses_retries: Hash[String, untyped]
70
+ attr_reader file_parser_pdf_engine: String?
54
71
 
55
72
  def initialize: (?config_path: String?, **untyped options) -> void
56
73
  def merge_chat_body: (Hash[Symbol | String, untyped] body) -> Hash[String, untyped]
@@ -64,6 +81,8 @@ module SavvyOpenrouter
64
81
  def initialize: (Hash[String, untyped] config) -> void
65
82
  def enabled?: () -> bool
66
83
  def max_body_limit: () -> Integer
84
+ def chat_attempts_final?: () -> bool
85
+ def responses_attempts_final?: () -> bool
67
86
  def record: (Hash[String, untyped] attrs) -> untyped
68
87
  def self.format_body_for_log: (untyped obj, ?max_bytes: Integer) -> String
69
88
  end
@@ -81,8 +100,13 @@ module SavvyOpenrouter
81
100
  DEFAULT_SUCCESS: Array[Integer]
82
101
 
83
102
  attr_reader config: Configuration
103
+ attr_reader api_call_logger: ApiCallLogger
84
104
 
85
105
  def initialize: (Configuration config) -> void
106
+ def with_call_context: (?Hash[Symbol | String, untyped] context) { () -> untyped } -> untyped
107
+ def record_manual_api_call: (Hash[Symbol | String, untyped] attrs) -> untyped
108
+ def flush_deferred_chat_log!: () -> void
109
+ def flush_deferred_responses_log!: () -> void
86
110
  def get: (String path, ?params: Hash[Symbol | String, untyped]?, ?success: Array[Integer]?) -> untyped
87
111
  def delete: (String path, ?params: Hash[Symbol | String, untyped]?, ?success: Array[Integer]?) -> untyped
88
112
  def post: (String path, body: Hash[Symbol | String, untyped], ?success: Array[Integer]?) -> untyped
@@ -100,6 +124,9 @@ module SavvyOpenrouter
100
124
 
101
125
  def initialize: (?config_path: String?, **untyped options) -> void
102
126
 
127
+ def api_call_logger: () -> ApiCallLogger
128
+ def record_api_call: (Hash[Symbol | String, untyped] attrs) -> untyped
129
+
103
130
  def chat: -> Resources::Chat
104
131
  def responses: -> Resources::Responses
105
132
  def anthropic_messages: -> Resources::AnthropicMessages
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: savvy_openrouter
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.4.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Bryan Feller
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2026-05-10 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: faraday
@@ -112,8 +112,13 @@ files:
112
112
  - lib/savvy_openrouter/completion_retry_policy.rb
113
113
  - lib/savvy_openrouter/configuration.rb
114
114
  - lib/savvy_openrouter/connection.rb
115
+ - lib/savvy_openrouter/connection_api_call_recording.rb
116
+ - lib/savvy_openrouter/connection_api_call_recording_transport.rb
115
117
  - lib/savvy_openrouter/connection_instrumentation.rb
116
118
  - lib/savvy_openrouter/errors.rb
119
+ - lib/savvy_openrouter/patterns.rb
120
+ - lib/savvy_openrouter/patterns/structured_output.rb
121
+ - lib/savvy_openrouter/request_plugins.rb
117
122
  - lib/savvy_openrouter/resources/analytics.rb
118
123
  - lib/savvy_openrouter/resources/anthropic_messages.rb
119
124
  - lib/savvy_openrouter/resources/api_keys.rb
@@ -133,17 +138,18 @@ files:
133
138
  - lib/savvy_openrouter/resources/responses.rb
134
139
  - lib/savvy_openrouter/resources/videos.rb
135
140
  - lib/savvy_openrouter/resources/workspaces.rb
141
+ - lib/savvy_openrouter/responses_retry_policy.rb
136
142
  - lib/savvy_openrouter/streaming.rb
143
+ - lib/savvy_openrouter/structured_output_error.rb
137
144
  - lib/savvy_openrouter/version.rb
138
- - savvy_openrouter-0.1.0.gem
139
145
  - sig/savvy_openrouter.rbs
140
- homepage: https://github.com/brizzlefeller/savvy_openrouter
146
+ homepage: https://github.com/bfeller/savvy_openrouter
141
147
  licenses:
142
148
  - MIT
143
149
  metadata:
144
- homepage_uri: https://github.com/brizzlefeller/savvy_openrouter
145
- source_code_uri: https://github.com/brizzlefeller/savvy_openrouter
146
- changelog_uri: https://github.com/brizzlefeller/savvy_openrouter/blob/master/CHANGELOG.md
150
+ homepage_uri: https://github.com/bfeller/savvy_openrouter
151
+ source_code_uri: https://github.com/bfeller/savvy_openrouter
152
+ changelog_uri: https://github.com/bfeller/savvy_openrouter/blob/master/CHANGELOG.md
147
153
  rubygems_mfa_required: 'true'
148
154
  rdoc_options: []
149
155
  require_paths:
@@ -159,7 +165,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
159
165
  - !ruby/object:Gem::Version
160
166
  version: '0'
161
167
  requirements: []
162
- rubygems_version: 3.6.2
168
+ rubygems_version: 4.0.3
163
169
  specification_version: 4
164
170
  summary: Ruby client for the OpenRouter unified AI API.
165
171
  test_files: []
Binary file