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.
Files changed (43) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/.rubocop.yml +50 -0
  4. data/CHANGELOG.md +23 -0
  5. data/CODE_OF_CONDUCT.md +132 -0
  6. data/LICENSE.txt +21 -0
  7. data/README.md +234 -0
  8. data/Rakefile +12 -0
  9. data/exe/savvy_openrouter +32 -0
  10. data/lib/generators/savvy_openrouter/install/install_generator.rb +17 -0
  11. data/lib/generators/savvy_openrouter/install/templates/savvy_openrouter.yml +53 -0
  12. data/lib/savvy_openrouter/api_call_logger.rb +93 -0
  13. data/lib/savvy_openrouter/client.rb +105 -0
  14. data/lib/savvy_openrouter/completion_retry_policy.rb +135 -0
  15. data/lib/savvy_openrouter/configuration.rb +156 -0
  16. data/lib/savvy_openrouter/connection.rb +316 -0
  17. data/lib/savvy_openrouter/connection_instrumentation.rb +133 -0
  18. data/lib/savvy_openrouter/errors.rb +35 -0
  19. data/lib/savvy_openrouter/resources/analytics.rb +13 -0
  20. data/lib/savvy_openrouter/resources/anthropic_messages.rb +14 -0
  21. data/lib/savvy_openrouter/resources/api_keys.rb +33 -0
  22. data/lib/savvy_openrouter/resources/audio.rb +19 -0
  23. data/lib/savvy_openrouter/resources/base.rb +23 -0
  24. data/lib/savvy_openrouter/resources/chat.rb +45 -0
  25. data/lib/savvy_openrouter/resources/credits.rb +13 -0
  26. data/lib/savvy_openrouter/resources/embeddings.rb +18 -0
  27. data/lib/savvy_openrouter/resources/endpoints.rb +13 -0
  28. data/lib/savvy_openrouter/resources/generations.rb +17 -0
  29. data/lib/savvy_openrouter/resources/guardrails.rb +61 -0
  30. data/lib/savvy_openrouter/resources/models.rb +57 -0
  31. data/lib/savvy_openrouter/resources/oauth.rb +17 -0
  32. data/lib/savvy_openrouter/resources/organization.rb +13 -0
  33. data/lib/savvy_openrouter/resources/providers.rb +13 -0
  34. data/lib/savvy_openrouter/resources/rerank.rb +14 -0
  35. data/lib/savvy_openrouter/resources/responses.rb +14 -0
  36. data/lib/savvy_openrouter/resources/videos.rb +53 -0
  37. data/lib/savvy_openrouter/resources/workspaces.rb +37 -0
  38. data/lib/savvy_openrouter/streaming.rb +32 -0
  39. data/lib/savvy_openrouter/version.rb +5 -0
  40. data/lib/savvy_openrouter.rb +13 -0
  41. data/savvy_openrouter-0.1.0.gem +0 -0
  42. data/sig/savvy_openrouter.rbs +150 -0
  43. metadata +165 -0
@@ -0,0 +1,316 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "connection_instrumentation"
4
+
5
+ require "faraday"
6
+ require "json"
7
+ require "net/http"
8
+ require "uri"
9
+
10
+ module SavvyOpenrouter
11
+ class Connection
12
+ include Instrumentation
13
+
14
+ DEFAULT_SUCCESS = [200, 201, 202, 204].freeze
15
+
16
+ attr_reader :config
17
+
18
+ def initialize(config)
19
+ @config = config
20
+ @api_call_logger = ApiCallLogger.new(config.api_call_log)
21
+ base = normalize_base(config.base_url)
22
+ headers = build_headers
23
+ @conn = Faraday.new(url: base, headers: headers) do |faraday|
24
+ faraday.request :json
25
+ faraday.response :json, content_type: /\bjson/, parser_options: { symbolize_names: true }
26
+ faraday.adapter Faraday.default_adapter
27
+ end
28
+ @raw = Faraday.new(url: base, headers: headers) do |faraday|
29
+ faraday.adapter Faraday.default_adapter
30
+ end
31
+ end
32
+
33
+ def get(path, params: nil, success: DEFAULT_SUCCESS)
34
+ timed_json(:get, path, params: params, success: success)
35
+ end
36
+
37
+ def delete(path, params: nil, success: DEFAULT_SUCCESS)
38
+ timed_json(:delete, path, params: params, success: success)
39
+ end
40
+
41
+ def post(path, body:, success: DEFAULT_SUCCESS)
42
+ timed_json(:post, path, body: body, success: success)
43
+ end
44
+
45
+ def patch(path, body:, success: DEFAULT_SUCCESS)
46
+ timed_json(:patch, path, body: body, success: success)
47
+ end
48
+
49
+ def put(path, body:, success: DEFAULT_SUCCESS)
50
+ timed_json(:put, path, body: body, success: success)
51
+ end
52
+
53
+ def get_raw(path, params: nil, success: [200])
54
+ rel = rel_path(path)
55
+ started = monotonic_ms
56
+ response = @raw.get(rel) do |req|
57
+ req.params.update(params) if params
58
+ end
59
+ duration_ms = elapsed_ms(started)
60
+ record_faraday_raw(
61
+ method: "GET",
62
+ rel_path: rel,
63
+ params: params,
64
+ request_body: nil,
65
+ response: response,
66
+ duration_ms: duration_ms
67
+ )
68
+ status = response.status
69
+ return response.body.b.freeze if success.include?(status)
70
+
71
+ raise_api_error(status, response.body)
72
+ rescue SavvyOpenrouter::ApiError
73
+ raise
74
+ rescue StandardError => e
75
+ record_transport_error(
76
+ method: "GET",
77
+ rel_path: rel,
78
+ params: params,
79
+ request_body: nil,
80
+ duration_ms: elapsed_ms(started),
81
+ error: e
82
+ )
83
+ raise
84
+ end
85
+
86
+ def post_raw(path, body:, success: [200])
87
+ rel = rel_path(path)
88
+ started = monotonic_ms
89
+ response = @raw.post(rel) do |req|
90
+ req.headers["Content-Type"] = "application/json"
91
+ req.body = JSON.generate(stringify_body(body))
92
+ end
93
+ duration_ms = elapsed_ms(started)
94
+ record_faraday_raw(
95
+ method: "POST",
96
+ rel_path: rel,
97
+ params: nil,
98
+ request_body: body,
99
+ response: response,
100
+ duration_ms: duration_ms
101
+ )
102
+ status = response.status
103
+ return response.body.b.freeze if success.include?(status)
104
+
105
+ raise_api_error(status, response.body)
106
+ rescue SavvyOpenrouter::ApiError
107
+ raise
108
+ rescue StandardError => e
109
+ record_transport_error(
110
+ method: "POST",
111
+ rel_path: rel,
112
+ params: nil,
113
+ request_body: body,
114
+ duration_ms: elapsed_ms(started),
115
+ error: e
116
+ )
117
+ raise
118
+ end
119
+
120
+ def stream_get(path, params: nil, &block)
121
+ ensure_api_key!
122
+ uri = join_uri(path)
123
+ uri.query = URI.encode_www_form(params) if params && !params.empty?
124
+
125
+ req = Net::HTTP::Get.new(uri)
126
+ build_headers.each { |k, v| req[k] = v }
127
+
128
+ started = monotonic_ms
129
+ status, err_buf = stream_via_net_http(uri, req, &block)
130
+ duration_ms = elapsed_ms(started)
131
+ record_stream(
132
+ method: "GET",
133
+ rel_path: rel_path(path),
134
+ params: params,
135
+ request_body: nil,
136
+ status: status,
137
+ response_body: err_buf,
138
+ duration_ms: duration_ms
139
+ )
140
+ return if status == 200
141
+
142
+ raise_api_error(status, err_buf) if status
143
+ rescue SavvyOpenrouter::ApiError
144
+ raise
145
+ rescue StandardError => e
146
+ record_transport_error(
147
+ method: "GET",
148
+ rel_path: rel_path(path),
149
+ params: params,
150
+ request_body: nil,
151
+ duration_ms: elapsed_ms(started),
152
+ error: e
153
+ )
154
+ raise
155
+ end
156
+
157
+ def stream_post(path, body, &)
158
+ ensure_api_key!
159
+ uri = join_uri(path)
160
+ req = Net::HTTP::Post.new(uri)
161
+ build_headers.each { |k, v| req[k] = v }
162
+ req["Content-Type"] = "application/json"
163
+ req["Accept"] = "text/event-stream"
164
+ req.body = JSON.generate(stringify_body(body))
165
+
166
+ started = monotonic_ms
167
+ status, err_buf = stream_via_net_http(uri, req, &)
168
+ duration_ms = elapsed_ms(started)
169
+ record_stream(
170
+ method: "POST",
171
+ rel_path: rel_path(path),
172
+ params: nil,
173
+ request_body: body,
174
+ status: status,
175
+ response_body: err_buf,
176
+ duration_ms: duration_ms
177
+ )
178
+ return if status == 200
179
+
180
+ raise_api_error(status, err_buf) if status
181
+ rescue SavvyOpenrouter::ApiError
182
+ raise
183
+ rescue StandardError => e
184
+ record_transport_error(
185
+ method: "POST",
186
+ rel_path: rel_path(path),
187
+ params: nil,
188
+ request_body: body,
189
+ duration_ms: elapsed_ms(started),
190
+ error: e
191
+ )
192
+ raise
193
+ end
194
+
195
+ private
196
+
197
+ def stream_via_net_http(uri, req, &block)
198
+ status = nil
199
+ err_buf = nil
200
+ Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == "https") do |http|
201
+ http.request(req) do |res|
202
+ status = res.code.to_i
203
+ unless status == 200
204
+ buf = +""
205
+ res.read_body { |c| buf << c }
206
+ err_buf = buf
207
+ next
208
+ end
209
+
210
+ res.read_body(&block)
211
+ end
212
+ end
213
+ [status, err_buf]
214
+ end
215
+
216
+ def stringify_body(body)
217
+ case body
218
+ when Hash
219
+ body.transform_keys(&:to_s)
220
+ else
221
+ body
222
+ end
223
+ end
224
+
225
+ def parse_json_response(response, success:)
226
+ status = response.status
227
+ return response.body if success.include?(status)
228
+ return nil if status == 204
229
+
230
+ raise_api_error(status, response.body)
231
+ end
232
+
233
+ def raise_api_error(status, body)
234
+ payload = parse_error_payload(body)
235
+ message = extract_message(payload)
236
+ exception_class = error_class_for(status)
237
+ raise exception_class.new(
238
+ message,
239
+ status_code: status,
240
+ response_body: payload
241
+ )
242
+ end
243
+
244
+ def parse_error_payload(body)
245
+ case body
246
+ when Hash
247
+ body
248
+ when String
249
+ return {} if body.strip.empty?
250
+
251
+ JSON.parse(body, symbolize_names: true)
252
+ else
253
+ {}
254
+ end
255
+ rescue JSON::ParserError
256
+ { raw: body.to_s }
257
+ end
258
+
259
+ def extract_message(payload)
260
+ err = payload[:error] || payload["error"]
261
+ case err
262
+ when Hash
263
+ err[:message] || err["message"] || err.inspect
264
+ when String
265
+ err
266
+ else
267
+ "HTTP error (#{payload.inspect})"
268
+ end
269
+ end
270
+
271
+ def error_class_for(status)
272
+ case status
273
+ when 400 then BadRequestError
274
+ when 401 then AuthenticationError
275
+ when 402 then PaymentRequiredError
276
+ when 403 then ForbiddenError
277
+ when 404 then NotFoundError
278
+ when 429 then RateLimitError
279
+ when 500, 501 then InternalServerError
280
+ when 502 then BadGatewayError
281
+ else ApiError
282
+ end
283
+ end
284
+
285
+ def ensure_api_key!
286
+ return if config.api_key && !config.api_key.to_s.empty?
287
+
288
+ raise ConfigurationError, "OpenRouter api_key is missing; set OPENROUTER_API_KEY or pass api_key: to SavvyOpenrouter::Client"
289
+ end
290
+
291
+ def build_headers
292
+ ensure_api_key!
293
+ h = {
294
+ "Authorization" => "Bearer #{config.api_key}",
295
+ "Accept" => "application/json"
296
+ }
297
+ h["HTTP-Referer"] = config.http_referer if config.http_referer && !config.http_referer.to_s.empty?
298
+ h["X-Title"] = config.app_title if config.app_title && !config.app_title.to_s.empty?
299
+ h
300
+ end
301
+
302
+ def normalize_base(url)
303
+ url.to_s.chomp("/")
304
+ end
305
+
306
+ def rel_path(path)
307
+ path.to_s.delete_prefix("/")
308
+ end
309
+
310
+ def join_uri(path)
311
+ base = normalize_base(config.base_url)
312
+ rel = rel_path(path)
313
+ URI.parse("#{base}/#{rel}")
314
+ end
315
+ end
316
+ end
@@ -0,0 +1,133 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SavvyOpenrouter
4
+ class Connection
5
+ # Timing + persistence hooks for optional request logging ({api_call_log} config).
6
+ module Instrumentation
7
+ private
8
+
9
+ def timed_json(method_sym, path, params: nil, body: nil, success: DEFAULT_SUCCESS)
10
+ rel = rel_path(path)
11
+ started = monotonic_ms
12
+ response =
13
+ case method_sym
14
+ when :get then @conn.get(rel) { |req| req.params.update(params) if params }
15
+ when :delete then @conn.delete(rel) { |req| req.params.update(params) if params }
16
+ when :post then @conn.post(rel, body)
17
+ when :patch then @conn.patch(rel, body)
18
+ when :put then @conn.put(rel, body)
19
+ else raise ArgumentError, "unsupported #{method_sym}"
20
+ end
21
+ duration_ms = elapsed_ms(started)
22
+ record_faraday_json(method: method_sym.to_s.upcase, rel_path: rel, params: params, request_body: body, response: response,
23
+ duration_ms: duration_ms)
24
+ parse_json_response(response, success: success)
25
+ rescue SavvyOpenrouter::ApiError
26
+ raise
27
+ rescue StandardError => e
28
+ record_transport_error(method: method_sym.to_s.upcase, rel_path: rel, params: params, request_body: body,
29
+ duration_ms: elapsed_ms(started), error: e)
30
+ raise
31
+ end
32
+
33
+ def record_faraday_json(method:, rel_path:, params:, request_body:, response:, duration_ms:)
34
+ return unless @api_call_logger.enabled?
35
+
36
+ lim = @api_call_logger.max_body_limit
37
+ @api_call_logger.record(
38
+ "method" => method,
39
+ "path" => full_url(rel_path, params),
40
+ "status" => response.status,
41
+ "duration_ms" => duration_ms.round(3),
42
+ "request_body" => ApiCallLogger.format_body_for_log(request_body, max_bytes: lim),
43
+ "response_body" => ApiCallLogger.format_body_for_log(response.body, max_bytes: lim),
44
+ "error_class" => nil,
45
+ "error_message" => nil,
46
+ "streaming" => false
47
+ )
48
+ end
49
+
50
+ def record_faraday_raw(method:, rel_path:, params:, request_body:, response:, duration_ms:)
51
+ return unless @api_call_logger.enabled?
52
+
53
+ lim = @api_call_logger.max_body_limit
54
+ body_for_resp =
55
+ if response.body.is_a?(String) && response.body.encoding == Encoding::ASCII_8BIT
56
+ "[binary #{response.body.bytesize} bytes]"
57
+ else
58
+ response.body
59
+ end
60
+ @api_call_logger.record(
61
+ "method" => method,
62
+ "path" => full_url(rel_path, params),
63
+ "status" => response.status,
64
+ "duration_ms" => duration_ms.round(3),
65
+ "request_body" => ApiCallLogger.format_body_for_log(request_body, max_bytes: lim),
66
+ "response_body" => ApiCallLogger.format_body_for_log(body_for_resp, max_bytes: lim),
67
+ "error_class" => nil,
68
+ "error_message" => nil,
69
+ "streaming" => false
70
+ )
71
+ end
72
+
73
+ def record_stream(method:, rel_path:, params:, request_body:, status:, response_body:, duration_ms:)
74
+ return unless @api_call_logger.enabled?
75
+
76
+ lim = @api_call_logger.max_body_limit
77
+ preview =
78
+ if status == 200
79
+ "[stream completed]"
80
+ else
81
+ response_body
82
+ end
83
+ @api_call_logger.record(
84
+ "method" => method,
85
+ "path" => full_url(rel_path, params),
86
+ "status" => status,
87
+ "duration_ms" => duration_ms.round(3),
88
+ "request_body" => ApiCallLogger.format_body_for_log(request_body, max_bytes: lim),
89
+ "response_body" => ApiCallLogger.format_body_for_log(preview, max_bytes: lim),
90
+ "error_class" => nil,
91
+ "error_message" => nil,
92
+ "streaming" => true
93
+ )
94
+ end
95
+
96
+ def record_transport_error(method:, rel_path:, params:, request_body:, duration_ms:, error:)
97
+ return unless @api_call_logger.enabled?
98
+
99
+ lim = @api_call_logger.max_body_limit
100
+ msg = ApiCallLogger.format_body_for_log(error.message, max_bytes: lim)
101
+
102
+ @api_call_logger.record(
103
+ "method" => method,
104
+ "path" => full_url(rel_path, params),
105
+ "status" => nil,
106
+ "duration_ms" => duration_ms.round(3),
107
+ "request_body" => ApiCallLogger.format_body_for_log(request_body, max_bytes: lim),
108
+ "response_body" => "",
109
+ "error_class" => error.class.name,
110
+ "error_message" => msg,
111
+ "streaming" => false
112
+ )
113
+ end
114
+
115
+ def full_url(rel_path, params)
116
+ u = join_uri(rel_path)
117
+ if params && !params.empty?
118
+ flat = params.transform_keys(&:to_s)
119
+ u.query = URI.encode_www_form(flat)
120
+ end
121
+ u.to_s
122
+ end
123
+
124
+ def monotonic_ms
125
+ Process.clock_gettime(Process::CLOCK_MONOTONIC) * 1000
126
+ end
127
+
128
+ def elapsed_ms(started)
129
+ monotonic_ms - started
130
+ end
131
+ end
132
+ end
133
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SavvyOpenrouter
4
+ class Error < StandardError
5
+ attr_reader :status_code, :response_body
6
+
7
+ def initialize(message = nil, status_code: nil, response_body: nil)
8
+ super(message || self.class.name)
9
+ @status_code = status_code
10
+ @response_body = response_body
11
+ end
12
+ end
13
+
14
+ class ConfigurationError < Error; end
15
+
16
+ class ApiError < Error; end
17
+
18
+ class BadRequestError < ApiError; end
19
+
20
+ class AuthenticationError < ApiError; end
21
+
22
+ class PaymentRequiredError < ApiError; end
23
+
24
+ class ForbiddenError < ApiError; end
25
+
26
+ class NotFoundError < ApiError; end
27
+
28
+ class RateLimitError < ApiError; end
29
+
30
+ class InternalServerError < ApiError; end
31
+
32
+ class BadGatewayError < ApiError; end
33
+
34
+ class TimeoutPollError < Error; end
35
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module SavvyOpenrouter
6
+ module Resources
7
+ class Analytics < Base
8
+ def activity(**params)
9
+ conn.get("/activity", params: params)
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module SavvyOpenrouter
6
+ module Resources
7
+ class AnthropicMessages < Base
8
+ def create(**params)
9
+ body = config.merge_chat_body(params)
10
+ conn.post("/messages", body: body)
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module SavvyOpenrouter
6
+ module Resources
7
+ class ApiKeys < Base
8
+ def current
9
+ conn.get("/key")
10
+ end
11
+
12
+ def list(**params)
13
+ conn.get("/keys", params: params)
14
+ end
15
+
16
+ def create(**body)
17
+ conn.post("/keys", body: body, success: [201])
18
+ end
19
+
20
+ def get(hash)
21
+ conn.get("/keys/#{hash}")
22
+ end
23
+
24
+ def update(hash, **body)
25
+ conn.patch("/keys/#{hash}", body: body)
26
+ end
27
+
28
+ def delete(hash)
29
+ conn.delete("/keys/#{hash}")
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module SavvyOpenrouter
6
+ module Resources
7
+ class Audio < Base
8
+ def speech(**params)
9
+ body = config.merge_chat_body(params)
10
+ conn.post_raw("/audio/speech", body: body)
11
+ end
12
+
13
+ def transcribe(**params)
14
+ body = config.merge_chat_body(params)
15
+ conn.post("/audio/transcriptions", body: body)
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SavvyOpenrouter
4
+ module Resources
5
+ class Base
6
+ def initialize(client)
7
+ @client = client
8
+ end
9
+
10
+ private
11
+
12
+ attr_reader :client
13
+
14
+ def conn
15
+ client.connection
16
+ end
17
+
18
+ def config
19
+ client.config
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+ require_relative "../streaming"
5
+ require_relative "../completion_retry_policy"
6
+
7
+ module SavvyOpenrouter
8
+ module Resources
9
+ class Chat < Base
10
+ def completions(messages:, **params)
11
+ policy = CompletionRetryPolicy.new(config.chat_retries)
12
+ body = config.merge_chat_body({ messages: messages }.merge(params))
13
+ attempt = 0
14
+ last_response = nil
15
+ loop do
16
+ attempt += 1
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)
21
+
22
+ policy.wait_after_attempt(attempt)
23
+ next
24
+ end
25
+
26
+ return last_response unless policy.retry_response?(last_response)
27
+ return last_response if attempt >= policy.max_attempts
28
+
29
+ policy.wait_after_attempt(attempt)
30
+ end
31
+ end
32
+
33
+ # Yields each SSE `data:` payload string (JSON text from the model stream).
34
+ def completions_stream(messages:, **params, &block)
35
+ body = config.merge_chat_body({ messages: messages, stream: true }.merge(params))
36
+ return enum_for(:completions_stream, messages: messages, **params) unless block
37
+
38
+ chunk_enum = Enumerator.new do |y|
39
+ conn.stream_post("/chat/completions", body) { |ch| y << ch }
40
+ end
41
+ Streaming.each_sse_data(chunk_enum, &block)
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module SavvyOpenrouter
6
+ module Resources
7
+ class Credits < Base
8
+ def get
9
+ conn.get("/credits")
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module SavvyOpenrouter
6
+ module Resources
7
+ class Embeddings < Base
8
+ def create(**params)
9
+ body = config.merge_chat_body(params)
10
+ conn.post("/embeddings", body: body)
11
+ end
12
+
13
+ def models
14
+ conn.get("/embeddings/models")
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module SavvyOpenrouter
6
+ module Resources
7
+ class Endpoints < Base
8
+ def zdr(**params)
9
+ conn.get("/endpoints/zdr", params: params)
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module SavvyOpenrouter
6
+ module Resources
7
+ class Generations < Base
8
+ def get(id:)
9
+ conn.get("/generation", params: { id: id })
10
+ end
11
+
12
+ def content(**params)
13
+ conn.get("/generation/content", params: params)
14
+ end
15
+ end
16
+ end
17
+ end