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,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module SavvyOpenrouter
6
+ module Resources
7
+ class Guardrails < Base
8
+ def list(**params)
9
+ conn.get("/guardrails", params: params)
10
+ end
11
+
12
+ def create(**body)
13
+ conn.post("/guardrails", body: body, success: [201])
14
+ end
15
+
16
+ def get(id)
17
+ conn.get("/guardrails/#{id}")
18
+ end
19
+
20
+ def update(id, **body)
21
+ conn.patch("/guardrails/#{id}", body: body)
22
+ end
23
+
24
+ def delete(id)
25
+ conn.delete("/guardrails/#{id}")
26
+ end
27
+
28
+ def list_key_assignments(guardrail_id, **params)
29
+ conn.get("/guardrails/#{guardrail_id}/assignments/keys", params: params)
30
+ end
31
+
32
+ def bulk_assign_keys(guardrail_id, **body)
33
+ conn.post("/guardrails/#{guardrail_id}/assignments/keys", body: body)
34
+ end
35
+
36
+ def bulk_unassign_keys(guardrail_id, **body)
37
+ conn.post("/guardrails/#{guardrail_id}/assignments/keys/remove", body: body)
38
+ end
39
+
40
+ def list_member_assignments(guardrail_id, **params)
41
+ conn.get("/guardrails/#{guardrail_id}/assignments/members", params: params)
42
+ end
43
+
44
+ def bulk_assign_members(guardrail_id, **body)
45
+ conn.post("/guardrails/#{guardrail_id}/assignments/members", body: body)
46
+ end
47
+
48
+ def bulk_unassign_members(guardrail_id, **body)
49
+ conn.post("/guardrails/#{guardrail_id}/assignments/members/remove", body: body)
50
+ end
51
+
52
+ def list_all_key_assignments(**params)
53
+ conn.get("/guardrails/assignments/keys", params: params)
54
+ end
55
+
56
+ def list_all_member_assignments(**params)
57
+ conn.get("/guardrails/assignments/members", params: params)
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module SavvyOpenrouter
6
+ module Resources
7
+ class Models < Base
8
+ # Query params match OpenRouter GET /models (e.g. category, output_modalities, supported_parameters).
9
+ def list(**params)
10
+ query = stringify_query(params)
11
+ conn.get("/models", params: query.empty? ? nil : query)
12
+ end
13
+
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.
17
+ def first_ranked_free_text_model(category:, output_modalities: "text")
18
+ res = list(category: category, output_modalities: output_modalities)
19
+ data = res[:data] || []
20
+ data.find { |m| free_pricing?(m) }
21
+ end
22
+
23
+ def count
24
+ conn.get("/models/count")
25
+ end
26
+
27
+ def user
28
+ conn.get("/models/user")
29
+ end
30
+
31
+ def endpoints(author:, slug:)
32
+ conn.get("/models/#{author}/#{slug}/endpoints")
33
+ end
34
+
35
+ private
36
+
37
+ def stringify_query(params)
38
+ params.each_with_object({}) do |(k, v), acc|
39
+ next if v.nil?
40
+
41
+ acc[k.to_s] = v
42
+ end
43
+ end
44
+
45
+ def free_pricing?(model)
46
+ p = model[:pricing] || {}
47
+ zero_price?(p[:prompt]) && zero_price?(p[:completion])
48
+ end
49
+
50
+ def zero_price?(value)
51
+ return false if value.nil?
52
+
53
+ value.to_s.gsub(/[^\d.\-eE]/, "").to_f.zero?
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module SavvyOpenrouter
6
+ module Resources
7
+ class OAuth < Base
8
+ def exchange(**body)
9
+ conn.post("/auth/keys", body: body)
10
+ end
11
+
12
+ def create_auth_code(**body)
13
+ conn.post("/auth/keys/code", body: body)
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module SavvyOpenrouter
6
+ module Resources
7
+ class Organization < Base
8
+ def members
9
+ conn.get("/organization/members")
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module SavvyOpenrouter
6
+ module Resources
7
+ class Providers < Base
8
+ def list
9
+ conn.get("/providers")
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 Rerank < Base
8
+ def create(**params)
9
+ body = config.merge_chat_body(params)
10
+ conn.post("/rerank", body: body)
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module SavvyOpenrouter
6
+ module Resources
7
+ class Responses < Base
8
+ def create(**params)
9
+ body = config.merge_responses_body(params)
10
+ conn.post("/responses", body: body)
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module SavvyOpenrouter
6
+ module Resources
7
+ class Videos < Base
8
+ TERMINAL_STATUSES = %w[completed failed cancelled expired].freeze
9
+
10
+ def create(**params)
11
+ body = config.merge_video_body(params)
12
+ conn.post("/videos", body: body)
13
+ end
14
+
15
+ alias submit create
16
+
17
+ def get(job_id)
18
+ conn.get("/videos/#{job_id}")
19
+ end
20
+
21
+ alias retrieve get
22
+ alias poll get
23
+
24
+ def download(job_id, index: 0)
25
+ conn.get_raw("/videos/#{job_id}/content", params: { index: index })
26
+ end
27
+
28
+ def stream(job_id, index: 0, &block)
29
+ raise ArgumentError, "block required" unless block
30
+
31
+ conn.stream_get("/videos/#{job_id}/content", params: { index: index }, &block)
32
+ end
33
+
34
+ def poll_until(job_id, interval: 2, timeout: 600)
35
+ deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout
36
+ loop do
37
+ res = get(job_id)
38
+ st = res[:status].to_s
39
+ return res if TERMINAL_STATUSES.include?(st)
40
+
41
+ now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
42
+ raise TimeoutPollError, "Timed out waiting for video job #{job_id}" if now > deadline
43
+
44
+ sleep(interval)
45
+ end
46
+ end
47
+
48
+ def models
49
+ conn.get("/videos/models")
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module SavvyOpenrouter
6
+ module Resources
7
+ class Workspaces < Base
8
+ def list(**params)
9
+ conn.get("/workspaces", params: params)
10
+ end
11
+
12
+ def create(**body)
13
+ conn.post("/workspaces", body: body, success: [201])
14
+ end
15
+
16
+ def get(id)
17
+ conn.get("/workspaces/#{id}")
18
+ end
19
+
20
+ def update(id, **body)
21
+ conn.patch("/workspaces/#{id}", body: body)
22
+ end
23
+
24
+ def delete(id)
25
+ conn.delete("/workspaces/#{id}")
26
+ end
27
+
28
+ def bulk_add_members(workspace_id, **body)
29
+ conn.post("/workspaces/#{workspace_id}/members/add", body: body)
30
+ end
31
+
32
+ def bulk_remove_members(workspace_id, **body)
33
+ conn.post("/workspaces/#{workspace_id}/members/remove", body: body)
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SavvyOpenrouter
4
+ module Streaming
5
+ extend self
6
+
7
+ # Yields each SSE `data:` payload line (without `data:` prefix), skipping `[DONE]`.
8
+ def each_sse_data(chunk_enum, &block)
9
+ buffer = +""
10
+ chunk_enum.each do |chunk|
11
+ buffer << chunk
12
+ flush_sse!(buffer, &block)
13
+ end
14
+ end
15
+
16
+ private
17
+
18
+ def flush_sse!(buffer, &block)
19
+ while (idx = buffer.index("\n\n"))
20
+ blob = buffer.slice!(0..(idx + 1))
21
+ blob.each_line do |line|
22
+ line = line.strip
23
+ next if line.empty?
24
+ next unless line.start_with?("data:")
25
+
26
+ payload = line.sub(/\Adata:\s*/, "")
27
+ block.call(payload) unless payload == "[DONE]"
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SavvyOpenrouter
4
+ VERSION = "0.2.0"
5
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "savvy_openrouter/version"
4
+ require_relative "savvy_openrouter/errors"
5
+ require_relative "savvy_openrouter/configuration"
6
+ require_relative "savvy_openrouter/completion_retry_policy"
7
+ require_relative "savvy_openrouter/api_call_logger"
8
+ require_relative "savvy_openrouter/connection"
9
+ require_relative "savvy_openrouter/streaming"
10
+ require_relative "savvy_openrouter/client"
11
+
12
+ module SavvyOpenrouter
13
+ end
Binary file
@@ -0,0 +1,150 @@
1
+ module SavvyOpenrouter
2
+ VERSION: String
3
+
4
+ class Error < StandardError
5
+ attr_reader status_code: Integer?
6
+ attr_reader response_body: untyped
7
+ def initialize: (?String message, ?status_code: Integer?, ?response_body: untyped) -> void
8
+ end
9
+
10
+ class ConfigurationError < Error
11
+ end
12
+
13
+ class ApiError < Error
14
+ end
15
+
16
+ class BadRequestError < ApiError
17
+ end
18
+
19
+ class AuthenticationError < ApiError
20
+ end
21
+
22
+ class PaymentRequiredError < ApiError
23
+ end
24
+
25
+ class ForbiddenError < ApiError
26
+ end
27
+
28
+ class NotFoundError < ApiError
29
+ end
30
+
31
+ class RateLimitError < ApiError
32
+ end
33
+
34
+ class InternalServerError < ApiError
35
+ end
36
+
37
+ class BadGatewayError < ApiError
38
+ end
39
+
40
+ class TimeoutPollError < Error
41
+ end
42
+
43
+ class Configuration
44
+ attr_accessor api_key: String?
45
+ attr_accessor base_url: String
46
+ attr_accessor default_model: String?
47
+ attr_accessor http_referer: String?
48
+ attr_accessor app_title: String?
49
+ attr_reader defaults: Hash[String, untyped]
50
+ attr_reader video_defaults: Hash[String, untyped]
51
+ attr_reader responses_defaults: Hash[String, untyped]
52
+ attr_reader api_call_log: Hash[String, untyped]
53
+ attr_reader chat_retries: Hash[String, untyped]
54
+
55
+ def initialize: (?config_path: String?, **untyped options) -> void
56
+ def merge_chat_body: (Hash[Symbol | String, untyped] body) -> Hash[String, untyped]
57
+ def merge_video_body: (Hash[Symbol | String, untyped] body) -> Hash[String, untyped]
58
+ def merge_responses_body: (Hash[Symbol | String, untyped] body) -> Hash[String, untyped]
59
+ def self.load_file: (String path) -> Hash[String, untyped]
60
+ def self.discover_config_file: (?String cwd) -> String?
61
+ end
62
+
63
+ class ApiCallLogger
64
+ def initialize: (Hash[String, untyped] config) -> void
65
+ def enabled?: () -> bool
66
+ def max_body_limit: () -> Integer
67
+ def record: (Hash[String, untyped] attrs) -> untyped
68
+ def self.format_body_for_log: (untyped obj, ?max_bytes: Integer) -> String
69
+ end
70
+
71
+ class CompletionRetryPolicy
72
+ def initialize: (Hash[String, untyped] | false? raw) -> void
73
+ def max_attempts: () -> Integer
74
+ def enabled?: () -> bool
75
+ def retry_http_error?: (ApiError error) -> bool
76
+ def retry_response?: (Hash[Symbol | String, untyped] response) -> bool
77
+ def wait_after_attempt: (Integer attempt_number) -> void
78
+ end
79
+
80
+ class Connection
81
+ DEFAULT_SUCCESS: Array[Integer]
82
+
83
+ attr_reader config: Configuration
84
+
85
+ def initialize: (Configuration config) -> void
86
+ def get: (String path, ?params: Hash[Symbol | String, untyped]?, ?success: Array[Integer]?) -> untyped
87
+ def delete: (String path, ?params: Hash[Symbol | String, untyped]?, ?success: Array[Integer]?) -> untyped
88
+ def post: (String path, body: Hash[Symbol | String, untyped], ?success: Array[Integer]?) -> untyped
89
+ def patch: (String path, body: Hash[Symbol | String, untyped], ?success: Array[Integer]?) -> untyped
90
+ def put: (String path, body: Hash[Symbol | String, untyped], ?success: Array[Integer]?) -> untyped
91
+ def get_raw: (String path, ?params: Hash[Symbol | String, untyped]?, ?success: Array[Integer]?) -> String
92
+ def post_raw: (String path, body: Hash[Symbol | String, untyped], ?success: Array[Integer]?) -> String
93
+ def stream_get: (String path, ?params: Hash[Symbol | String, untyped]?) { (String chunk) -> void } -> void
94
+ def stream_post: (String path, Hash[Symbol | String, untyped] body) { (String chunk) -> void } -> void
95
+ end
96
+
97
+ class Client
98
+ attr_reader config: Configuration
99
+ attr_reader connection: Connection
100
+
101
+ def initialize: (?config_path: String?, **untyped options) -> void
102
+
103
+ def chat: -> Resources::Chat
104
+ def responses: -> Resources::Responses
105
+ def anthropic_messages: -> Resources::AnthropicMessages
106
+ def embeddings: -> Resources::Embeddings
107
+ def rerank: -> Resources::Rerank
108
+ def models: -> Resources::Models
109
+ def credits: -> Resources::Credits
110
+ def providers: -> Resources::Providers
111
+ def generations: -> Resources::Generations
112
+ def endpoints: -> Resources::Endpoints
113
+ def analytics: -> Resources::Analytics
114
+ def audio: -> Resources::Audio
115
+ def videos: -> Resources::Videos
116
+ def oauth: -> Resources::OAuth
117
+ def api_keys: -> Resources::ApiKeys
118
+ def organization: -> Resources::Organization
119
+ def guardrails: -> Resources::Guardrails
120
+ def workspaces: -> Resources::Workspaces
121
+ end
122
+
123
+ module Resources
124
+ class Chat
125
+ def completions: (messages: untyped, **untyped params) -> untyped
126
+ def completions_stream: (messages: untyped, **untyped params) ?{ (String sse_data_line) -> void } -> (Enumerator[String] | void)
127
+ end
128
+
129
+ class Models
130
+ def list: (**untyped params) -> untyped
131
+ def count: () -> untyped
132
+ def user: () -> untyped
133
+ def endpoints: (author: String, slug: String) -> untyped
134
+ def first_ranked_free_text_model: (category: String, ?output_modalities: String) -> Hash[Symbol, untyped]?
135
+ end
136
+
137
+ class Videos
138
+ def create: (**untyped params) -> untyped
139
+ def get: (String job_id) -> untyped
140
+ def download: (String job_id, ?index: Integer) -> String
141
+ def stream: (String job_id, ?index: Integer) { (String chunk) -> void } -> void
142
+ def poll_until: (String job_id, ?interval: Numeric, ?timeout: Numeric) -> untyped
143
+ def models: () -> untyped
144
+ end
145
+ end
146
+
147
+ module Streaming
148
+ def self.each_sse_data: (Enumerator[String] chunk_enum) { (String payload) -> void } -> void
149
+ end
150
+ end
metadata ADDED
@@ -0,0 +1,165 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: savvy_openrouter
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.0
5
+ platform: ruby
6
+ authors:
7
+ - Bryan Feller
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 2026-05-10 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: faraday
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '2.0'
19
+ - - "<"
20
+ - !ruby/object:Gem::Version
21
+ version: '4'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ requirements:
26
+ - - ">="
27
+ - !ruby/object:Gem::Version
28
+ version: '2.0'
29
+ - - "<"
30
+ - !ruby/object:Gem::Version
31
+ version: '4'
32
+ - !ruby/object:Gem::Dependency
33
+ name: rake
34
+ requirement: !ruby/object:Gem::Requirement
35
+ requirements:
36
+ - - "~>"
37
+ - !ruby/object:Gem::Version
38
+ version: '13.0'
39
+ type: :development
40
+ prerelease: false
41
+ version_requirements: !ruby/object:Gem::Requirement
42
+ requirements:
43
+ - - "~>"
44
+ - !ruby/object:Gem::Version
45
+ version: '13.0'
46
+ - !ruby/object:Gem::Dependency
47
+ name: rspec
48
+ requirement: !ruby/object:Gem::Requirement
49
+ requirements:
50
+ - - "~>"
51
+ - !ruby/object:Gem::Version
52
+ version: '3.0'
53
+ type: :development
54
+ prerelease: false
55
+ version_requirements: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - "~>"
58
+ - !ruby/object:Gem::Version
59
+ version: '3.0'
60
+ - !ruby/object:Gem::Dependency
61
+ name: rubocop
62
+ requirement: !ruby/object:Gem::Requirement
63
+ requirements:
64
+ - - "~>"
65
+ - !ruby/object:Gem::Version
66
+ version: '1.21'
67
+ type: :development
68
+ prerelease: false
69
+ version_requirements: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - "~>"
72
+ - !ruby/object:Gem::Version
73
+ version: '1.21'
74
+ - !ruby/object:Gem::Dependency
75
+ name: webmock
76
+ requirement: !ruby/object:Gem::Requirement
77
+ requirements:
78
+ - - "~>"
79
+ - !ruby/object:Gem::Version
80
+ version: '3.23'
81
+ type: :development
82
+ prerelease: false
83
+ version_requirements: !ruby/object:Gem::Requirement
84
+ requirements:
85
+ - - "~>"
86
+ - !ruby/object:Gem::Version
87
+ version: '3.23'
88
+ description: 'OpenRouter Ruby client: chat (streaming SSE), configurable retries for
89
+ non-streaming completions, optional DB-backed API call logging, Responses API defaults,
90
+ models listing with filters and free-tier hints, embeddings, rerank, audio, video,
91
+ OAuth, API keys, guardrails, workspaces, and related REST endpoints.'
92
+ email:
93
+ - brizzlefeller@gmail.com
94
+ executables:
95
+ - savvy_openrouter
96
+ extensions: []
97
+ extra_rdoc_files: []
98
+ files:
99
+ - ".rspec"
100
+ - ".rubocop.yml"
101
+ - CHANGELOG.md
102
+ - CODE_OF_CONDUCT.md
103
+ - LICENSE.txt
104
+ - README.md
105
+ - Rakefile
106
+ - exe/savvy_openrouter
107
+ - lib/generators/savvy_openrouter/install/install_generator.rb
108
+ - lib/generators/savvy_openrouter/install/templates/savvy_openrouter.yml
109
+ - lib/savvy_openrouter.rb
110
+ - lib/savvy_openrouter/api_call_logger.rb
111
+ - lib/savvy_openrouter/client.rb
112
+ - lib/savvy_openrouter/completion_retry_policy.rb
113
+ - lib/savvy_openrouter/configuration.rb
114
+ - lib/savvy_openrouter/connection.rb
115
+ - lib/savvy_openrouter/connection_instrumentation.rb
116
+ - lib/savvy_openrouter/errors.rb
117
+ - lib/savvy_openrouter/resources/analytics.rb
118
+ - lib/savvy_openrouter/resources/anthropic_messages.rb
119
+ - lib/savvy_openrouter/resources/api_keys.rb
120
+ - lib/savvy_openrouter/resources/audio.rb
121
+ - lib/savvy_openrouter/resources/base.rb
122
+ - lib/savvy_openrouter/resources/chat.rb
123
+ - lib/savvy_openrouter/resources/credits.rb
124
+ - lib/savvy_openrouter/resources/embeddings.rb
125
+ - lib/savvy_openrouter/resources/endpoints.rb
126
+ - lib/savvy_openrouter/resources/generations.rb
127
+ - lib/savvy_openrouter/resources/guardrails.rb
128
+ - lib/savvy_openrouter/resources/models.rb
129
+ - lib/savvy_openrouter/resources/oauth.rb
130
+ - lib/savvy_openrouter/resources/organization.rb
131
+ - lib/savvy_openrouter/resources/providers.rb
132
+ - lib/savvy_openrouter/resources/rerank.rb
133
+ - lib/savvy_openrouter/resources/responses.rb
134
+ - lib/savvy_openrouter/resources/videos.rb
135
+ - lib/savvy_openrouter/resources/workspaces.rb
136
+ - lib/savvy_openrouter/streaming.rb
137
+ - lib/savvy_openrouter/version.rb
138
+ - savvy_openrouter-0.1.0.gem
139
+ - sig/savvy_openrouter.rbs
140
+ homepage: https://github.com/brizzlefeller/savvy_openrouter
141
+ licenses:
142
+ - MIT
143
+ 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
147
+ rubygems_mfa_required: 'true'
148
+ rdoc_options: []
149
+ require_paths:
150
+ - lib
151
+ required_ruby_version: !ruby/object:Gem::Requirement
152
+ requirements:
153
+ - - ">="
154
+ - !ruby/object:Gem::Version
155
+ version: 3.1.0
156
+ required_rubygems_version: !ruby/object:Gem::Requirement
157
+ requirements:
158
+ - - ">="
159
+ - !ruby/object:Gem::Version
160
+ version: '0'
161
+ requirements: []
162
+ rubygems_version: 3.6.2
163
+ specification_version: 4
164
+ summary: Ruby client for the OpenRouter unified AI API.
165
+ test_files: []