openrouter_client 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 83e60f6533fc86470014efb28c27ba4f714322de55613bb1a990f5c623922f35
4
+ data.tar.gz: 592ff75e2be878e65655321a402148e6386499176d388de7f0a8fad76bc537d5
5
+ SHA512:
6
+ metadata.gz: c2dfb5c991c3a135601517d73b30c5eb95466033ee35c1d837ecc262fd6b42c3e21125990359d7e3ce69838395c03bc350369ffdb3861af34401f07d8a7444b0
7
+ data.tar.gz: 3c1335769301ebb1a617d89a6189b3fadce8b7f7b6cf55256471bf6abc351a36de0a6a96d083cd94c7fdfec52e99af414b9e336c34a8c195734a4d5d49675a9b
data/.env.example ADDED
@@ -0,0 +1 @@
1
+ OPENROUTER_API_KEY=
data/.rubocop.yml ADDED
@@ -0,0 +1,68 @@
1
+ AllCops:
2
+ TargetRubyVersion: 3.3.9
3
+ NewCops: enable
4
+ SuggestExtensions: false
5
+
6
+ Style/Documentation:
7
+ Enabled: false
8
+
9
+ Bundler/OrderedGems:
10
+ Enabled: false
11
+
12
+ Style/StringLiterals:
13
+ EnforcedStyle: double_quotes
14
+
15
+ Style/FrozenStringLiteralComment:
16
+ SafeAutoCorrect: true
17
+ EnforcedStyle: always_true
18
+
19
+ Metrics/MethodLength:
20
+ Enabled: false
21
+
22
+ Style/AccessorGrouping:
23
+ Enabled: false
24
+
25
+ Metrics/AbcSize:
26
+ Enabled: false
27
+
28
+ Metrics/CyclomaticComplexity:
29
+ Enabled: false
30
+
31
+ Metrics/ParameterLists:
32
+ Enabled: false
33
+
34
+ Metrics/ClassLength:
35
+ Enabled: false
36
+
37
+ Metrics/PerceivedComplexity:
38
+ Enabled: false
39
+
40
+ Style/HashSyntax:
41
+ Enabled: false
42
+
43
+ Layout/LineLength:
44
+ Max: 150
45
+
46
+ Naming/VariableNumber:
47
+ Exclude:
48
+ - "test/**/*_test.rb"
49
+
50
+ Naming/PredicateMethod:
51
+ AllowedMethods:
52
+ - destroy!
53
+
54
+ Lint/UnusedMethodArgument:
55
+ Exclude:
56
+ - "test/**/*_test.rb"
57
+
58
+ Naming/FileName:
59
+ Exclude:
60
+ - "lib/openrouter.rb"
61
+
62
+ Style/RedundantInitialize:
63
+ Exclude:
64
+ - "rbi/**/*.rbi"
65
+
66
+ Naming/BlockForwarding:
67
+ Exclude:
68
+ - "rbi/**/*.rbi"
data/.ruby-version ADDED
@@ -0,0 +1,2 @@
1
+ 3.3.9
2
+
data/Gemfile ADDED
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gemspec
6
+
7
+ gem "minitest", "~> 5.0"
8
+ gem "rake", "~> 13.0"
9
+ gem "rubocop", "~> 1.21"
10
+ gem "dotenv"
11
+ gem "sorbet"
12
+ gem "tapioca"
13
+ gem "vcr"
14
+ gem "webmock"
data/Gemfile.lock ADDED
@@ -0,0 +1,124 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ openrouter_client (0.1.0)
5
+ faraday (>= 1)
6
+
7
+ GEM
8
+ remote: https://rubygems.org/
9
+ specs:
10
+ addressable (2.8.8)
11
+ public_suffix (>= 2.0.2, < 8.0)
12
+ ast (2.4.3)
13
+ benchmark (0.5.0)
14
+ bigdecimal (4.0.1)
15
+ crack (1.0.1)
16
+ bigdecimal
17
+ rexml
18
+ dotenv (3.2.0)
19
+ erubi (1.13.1)
20
+ faraday (2.14.0)
21
+ faraday-net_http (>= 2.0, < 3.5)
22
+ json
23
+ logger
24
+ faraday-net_http (3.4.2)
25
+ net-http (~> 0.5)
26
+ hashdiff (1.2.1)
27
+ json (2.18.0)
28
+ language_server-protocol (3.17.0.5)
29
+ lint_roller (1.1.0)
30
+ logger (1.7.0)
31
+ minitest (5.27.0)
32
+ net-http (0.9.1)
33
+ uri (>= 0.11.1)
34
+ netrc (0.11.0)
35
+ parallel (1.27.0)
36
+ parser (3.3.10.0)
37
+ ast (~> 2.4.1)
38
+ racc
39
+ prism (1.7.0)
40
+ public_suffix (7.0.2)
41
+ racc (1.8.1)
42
+ rainbow (3.1.1)
43
+ rake (13.3.1)
44
+ rbi (0.3.8)
45
+ prism (~> 1.0)
46
+ rbs (>= 3.4.4)
47
+ rbs (3.10.0)
48
+ logger
49
+ regexp_parser (2.11.3)
50
+ rexml (3.4.4)
51
+ rubocop (1.82.1)
52
+ json (~> 2.3)
53
+ language_server-protocol (~> 3.17.0.2)
54
+ lint_roller (~> 1.1.0)
55
+ parallel (~> 1.10)
56
+ parser (>= 3.3.0.2)
57
+ rainbow (>= 2.2.2, < 4.0)
58
+ regexp_parser (>= 2.9.3, < 3.0)
59
+ rubocop-ast (>= 1.48.0, < 2.0)
60
+ ruby-progressbar (~> 1.7)
61
+ unicode-display_width (>= 2.4.0, < 4.0)
62
+ rubocop-ast (1.49.0)
63
+ parser (>= 3.3.7.2)
64
+ prism (~> 1.7)
65
+ ruby-progressbar (1.13.0)
66
+ sorbet (0.6.12874)
67
+ sorbet-static (= 0.6.12874)
68
+ sorbet-runtime (0.6.12874)
69
+ sorbet-static (0.6.12874-aarch64-linux)
70
+ sorbet-static (0.6.12874-universal-darwin)
71
+ sorbet-static (0.6.12874-x86_64-linux)
72
+ sorbet-static-and-runtime (0.6.12874)
73
+ sorbet (= 0.6.12874)
74
+ sorbet-runtime (= 0.6.12874)
75
+ spoom (1.6.3)
76
+ erubi (>= 1.10.0)
77
+ prism (>= 0.28.0)
78
+ rbi (>= 0.3.3)
79
+ rexml (>= 3.2.6)
80
+ sorbet-static-and-runtime (>= 0.5.10187)
81
+ thor (>= 0.19.2)
82
+ tapioca (0.16.11)
83
+ benchmark
84
+ bundler (>= 2.2.25)
85
+ netrc (>= 0.11.0)
86
+ parallel (>= 1.21.0)
87
+ rbi (~> 0.2)
88
+ sorbet-static-and-runtime (>= 0.5.11087)
89
+ spoom (>= 1.2.0)
90
+ thor (>= 1.2.0)
91
+ yard-sorbet
92
+ thor (1.4.0)
93
+ unicode-display_width (3.2.0)
94
+ unicode-emoji (~> 4.1)
95
+ unicode-emoji (4.2.0)
96
+ uri (1.1.1)
97
+ vcr (6.4.0)
98
+ webmock (3.26.1)
99
+ addressable (>= 2.8.0)
100
+ crack (>= 0.3.2)
101
+ hashdiff (>= 0.4.0, < 2.0.0)
102
+ yard (0.9.38)
103
+ yard-sorbet (0.9.0)
104
+ sorbet-runtime
105
+ yard
106
+
107
+ PLATFORMS
108
+ aarch64-linux
109
+ universal-darwin
110
+ x86_64-linux
111
+
112
+ DEPENDENCIES
113
+ dotenv
114
+ minitest (~> 5.0)
115
+ openrouter_client!
116
+ rake (~> 13.0)
117
+ rubocop (~> 1.21)
118
+ sorbet
119
+ tapioca
120
+ vcr
121
+ webmock
122
+
123
+ BUNDLED WITH
124
+ 2.7.2
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 851 Labs
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
data/README.md ADDED
@@ -0,0 +1,378 @@
1
+ # OpenRouter Client
2
+
3
+ A Ruby client for the [OpenRouter API](https://openrouter.ai), providing access to hundreds of AI models through a unified interface.
4
+
5
+ ## Installation
6
+
7
+ Install the gem and add to the application's Gemfile by executing:
8
+
9
+ ```bash
10
+ bundle add openrouter_client
11
+ ```
12
+
13
+ If bundler is not being used to manage dependencies, install the gem by executing:
14
+
15
+ ```bash
16
+ gem install openrouter_client
17
+ ```
18
+
19
+ ## Usage
20
+
21
+ ### Configuration
22
+
23
+ Configure the client once at boot (e.g., in Rails an initializer) using `OpenRouter.configure`.
24
+
25
+ ```ruby
26
+ OpenRouter.configure do |config|
27
+ config.api_key = "your-key" # Optional. Defaults to ENV["OPENROUTER_API_KEY"]
28
+ config.api_base = "https://openrouter.ai/api/v1" # Optional (default)
29
+ config.request_timeout = 120 # Optional (default: 120 seconds)
30
+ config.site_url = "https://myapp.com" # Optional. For app attribution
31
+ config.site_name = "My App" # Optional. For app attribution
32
+ end
33
+ ```
34
+
35
+ ### Create a Chat Completion
36
+
37
+ The simplest way to use OpenRouter is to create a chat completion:
38
+
39
+ ```ruby
40
+ completion = OpenRouter::Completion.create!(
41
+ messages: [
42
+ { role: "user", content: "What is the meaning of life?" }
43
+ ],
44
+ model: "openai/gpt-4"
45
+ )
46
+
47
+ puts completion.content # => "The meaning of life..."
48
+ puts completion.model # => "openai/gpt-4"
49
+ puts completion.id # => "gen-xxxxx"
50
+ ```
51
+
52
+ ### With System Messages
53
+
54
+ ```ruby
55
+ completion = OpenRouter::Completion.create!(
56
+ messages: [
57
+ { role: "system", content: "You are a helpful assistant." },
58
+ { role: "user", content: "Hello!" }
59
+ ],
60
+ model: "anthropic/claude-3-opus"
61
+ )
62
+ ```
63
+
64
+ ### Streaming Responses
65
+
66
+ Use `stream!` for SSE streaming. It yields each chunk and returns the final completion:
67
+
68
+ ```ruby
69
+ completion = OpenRouter::Completion.stream!(
70
+ messages: [{ role: "user", content: "Tell me a story" }],
71
+ model: "openai/gpt-4"
72
+ ) do |chunk|
73
+ # chunk is a Hash with the streamed data
74
+ print chunk.dig("choices", 0, "delta", "content")
75
+ end
76
+
77
+ puts "\n---"
78
+ puts "Final content: #{completion.content}"
79
+ ```
80
+
81
+ ### Advanced Parameters
82
+
83
+ ```ruby
84
+ completion = OpenRouter::Completion.create!(
85
+ messages: [{ role: "user", content: "Write a haiku" }],
86
+ model: "openai/gpt-4",
87
+ temperature: 0.7,
88
+ max_tokens: 100,
89
+ top_p: 0.9,
90
+ frequency_penalty: 0.5,
91
+ presence_penalty: 0.5,
92
+ stop: ["\n\n"],
93
+ seed: 42
94
+ )
95
+ ```
96
+
97
+ ### Tool Calling
98
+
99
+ ```ruby
100
+ completion = OpenRouter::Completion.create!(
101
+ messages: [{ role: "user", content: "What's the weather in Tokyo?" }],
102
+ model: "openai/gpt-4",
103
+ tools: [
104
+ {
105
+ type: "function",
106
+ function: {
107
+ name: "get_weather",
108
+ description: "Get the weather for a location",
109
+ parameters: {
110
+ type: "object",
111
+ properties: {
112
+ location: { type: "string", description: "City name" }
113
+ },
114
+ required: ["location"]
115
+ }
116
+ }
117
+ }
118
+ ]
119
+ )
120
+
121
+ if completion.has_tool_calls?
122
+ completion.tool_calls.each do |tool_call|
123
+ puts "Function: #{tool_call['function']['name']}"
124
+ puts "Arguments: #{tool_call['function']['arguments']}"
125
+ end
126
+ end
127
+ ```
128
+
129
+ ### Model Fallbacks
130
+
131
+ ```ruby
132
+ completion = OpenRouter::Completion.create!(
133
+ messages: [{ role: "user", content: "Hello" }],
134
+ models: ["openai/gpt-4", "anthropic/claude-3-opus", "google/gemini-pro"],
135
+ route: "fallback"
136
+ )
137
+ ```
138
+
139
+ ### Image Generation
140
+
141
+ Generate images with models that support image output:
142
+
143
+ ```ruby
144
+ completion = OpenRouter::Completion.create!(
145
+ model: "google/gemini-2.5-flash-image-preview",
146
+ messages: [{ role: "user", content: "Generate a beautiful sunset over mountains" }],
147
+ modalities: ["image", "text"]
148
+ )
149
+
150
+ # Check if images were generated
151
+ if completion.images?
152
+ completion.images.each do |image|
153
+ # Images are base64-encoded data URLs
154
+ image_url = image.dig("image_url", "url")
155
+ puts "Generated image: #{image_url[0..50]}..."
156
+ end
157
+ end
158
+
159
+ puts completion.content # Text response (if any)
160
+ ```
161
+
162
+ #### Image Configuration (Gemini models)
163
+
164
+ ```ruby
165
+ completion = OpenRouter::Completion.create!(
166
+ model: "google/gemini-2.5-flash-image-preview",
167
+ messages: [{ role: "user", content: "Create a futuristic cityscape" }],
168
+ modalities: ["image", "text"],
169
+ image_config: {
170
+ aspect_ratio: "16:9", # 1:1, 2:3, 3:2, 4:3, 9:16, 16:9, etc.
171
+ image_size: "4K" # 1K, 2K, or 4K
172
+ }
173
+ )
174
+ ```
175
+
176
+ ### Additional API Options
177
+
178
+ Pass any additional OpenRouter API parameters:
179
+
180
+ ```ruby
181
+ completion = OpenRouter::Completion.create!(
182
+ messages: [{ role: "user", content: "Hello" }],
183
+ model: "openai/gpt-4",
184
+ # Reasoning configuration
185
+ reasoning: { effort: "high" },
186
+ # Web search plugin
187
+ plugins: [{ id: "web", enabled: true }],
188
+ # Provider preferences
189
+ provider: {
190
+ order: ["OpenAI", "Anthropic"],
191
+ allow_fallbacks: true
192
+ }
193
+ )
194
+ ```
195
+
196
+ ### Models
197
+
198
+ List, search, and find models:
199
+
200
+ ```ruby
201
+ # Get all models
202
+ models = OpenRouter::Model.all
203
+ puts "Total models: #{models.count}"
204
+
205
+ # Find a specific model (returns nil if not found)
206
+ model = OpenRouter::Model.find_by(id: "openai/gpt-4")
207
+
208
+ # Find a model (raises NotFoundError if not found)
209
+ model = OpenRouter::Model.find("openai/gpt-4")
210
+ model = OpenRouter::Model.find_by!(id: "openai/gpt-4")
211
+
212
+ # Model attributes
213
+ puts model.name # => "GPT-4"
214
+ puts model.context_length # => 8192
215
+ puts model.input_price # => 0.00003 (per token)
216
+ puts model.output_price # => 0.00006 (per token)
217
+ puts model.free? # => false
218
+
219
+ # Search models
220
+ results = OpenRouter::Model.search(query: "claude")
221
+
222
+ # Find by provider
223
+ openai_models = OpenRouter::Model.by_provider(provider: "openai")
224
+
225
+ # Find free models
226
+ free_models = OpenRouter::Model.free
227
+
228
+ # Use a model to create a completion
229
+ model = OpenRouter::Model.find("openai/gpt-4")
230
+ completion = model.complete(messages: [{ role: "user", content: "Hello!" }])
231
+ ```
232
+
233
+ ### Generation Stats
234
+
235
+ Query detailed usage information for a completion:
236
+
237
+ ```ruby
238
+ completion = OpenRouter::Completion.create!(
239
+ messages: [{ role: "user", content: "Hello" }],
240
+ model: "openai/gpt-4"
241
+ )
242
+
243
+ # Query generation stats (may take a moment to be available)
244
+ generation = OpenRouter::Generation.find_by(id: completion.id)
245
+
246
+ if generation
247
+ puts generation.model # => "openai/gpt-4"
248
+ puts generation.native_prompt_tokens # => 5
249
+ puts generation.native_completion_tokens # => 10
250
+ puts generation.total_cost # => 0.00045
251
+ puts generation.provider # => "openai"
252
+ puts generation.tokens_per_second # => 50.0
253
+ end
254
+ ```
255
+
256
+ ### Credits
257
+
258
+ Check your credit balance:
259
+
260
+ ```ruby
261
+ credit = OpenRouter::Credit.fetch
262
+ puts credit.total_credits # => 100.0
263
+ puts credit.total_usage # => 25.0
264
+ puts credit.remaining # => 75.0
265
+
266
+ # Convenience method
267
+ remaining = OpenRouter::Credit.remaining
268
+
269
+ # Check credit status
270
+ credit.low? # => true if less than 10% remaining
271
+ credit.exhausted? # => true if no credits left
272
+ ```
273
+
274
+ ### API Keys
275
+
276
+ Manage API keys programmatically:
277
+
278
+ ```ruby
279
+ # Get current key info
280
+ current_key = OpenRouter::ApiKey.current
281
+ puts current_key.name
282
+ puts current_key.usage
283
+
284
+ # List all keys
285
+ keys = OpenRouter::ApiKey.all
286
+
287
+ # Create a new key
288
+ new_key = OpenRouter::ApiKey.create!(name: "Production Key", limit: 100.0)
289
+ puts new_key.key # Only shown once!
290
+
291
+ # Update a key
292
+ new_key.update!(name: "Updated Name", limit: 200.0)
293
+
294
+ # Delete a key
295
+ new_key.destroy!
296
+ ```
297
+
298
+ ### Error Handling
299
+
300
+ The gem raises typed exceptions for different error conditions:
301
+
302
+ ```ruby
303
+ begin
304
+ OpenRouter::Completion.create!(
305
+ messages: [{ role: "user", content: "Hello" }],
306
+ model: "openai/gpt-4"
307
+ )
308
+ rescue OpenRouter::UnauthorizedError
309
+ # Invalid or missing API key (401)
310
+ rescue OpenRouter::ForbiddenError
311
+ # Access denied (403)
312
+ rescue OpenRouter::NotFoundError
313
+ # Resource not found (404)
314
+ rescue OpenRouter::RateLimitError
315
+ # Rate limited (429)
316
+ rescue OpenRouter::PaymentRequiredError
317
+ # Insufficient credits (402)
318
+ rescue OpenRouter::BadRequestError
319
+ # Invalid request (400)
320
+ rescue OpenRouter::ServerError
321
+ # Server error (5xx)
322
+ end
323
+ ```
324
+
325
+ ### Response Helpers
326
+
327
+ Completions have several helper methods:
328
+
329
+ ```ruby
330
+ completion.content # Message content
331
+ completion.role # "assistant"
332
+ completion.finish_reason # "stop", "length", "tool_calls", etc.
333
+ completion.prompt_tokens # Tokens in prompt
334
+ completion.completion_tokens # Tokens in response
335
+ completion.total_tokens # Total tokens
336
+
337
+ completion.stopped? # Finished due to stop sequence
338
+ completion.truncated? # Finished due to max_tokens
339
+ completion.tool_calls? # Contains tool calls
340
+ completion.images? # Contains generated images
341
+ completion.images # Array of generated images
342
+ completion.tool_calls # Array of tool calls
343
+ ```
344
+
345
+ ## Development
346
+
347
+ After checking out the repo, run `bin/setup` to install dependencies. You can run `bin/console` for an interactive prompt that will allow you to experiment.
348
+
349
+ For local development, copy the example environment file and set your API key so `bin/console` can load it automatically:
350
+
351
+ ```bash
352
+ cp .env.example .env
353
+ echo 'OPENROUTER_API_KEY=your_api_key_here' >> .env
354
+ ```
355
+
356
+ ### Available Scripts
357
+
358
+ ```bash
359
+ bin/setup # Install dependencies
360
+ bin/console # Interactive Ruby console with gem loaded
361
+ bin/test # Run tests
362
+ bin/vcr # Run tests with VCR recording (creates cassettes)
363
+ bin/format # Run RuboCop with auto-correct
364
+ ```
365
+
366
+ ### Running Tests
367
+
368
+ ```bash
369
+ bin/test # Run all tests
370
+
371
+ bin/vcr # Run tests and record VCR cassettes
372
+ ```
373
+
374
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to rubygems.org.
375
+
376
+ ## License
377
+
378
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rake/testtask"
5
+
6
+ Rake::TestTask.new(:test) do |t|
7
+ t.libs << "test"
8
+ t.libs << "lib"
9
+ t.test_files = FileList["test/**/*_test.rb"]
10
+ end
11
+
12
+ require "rubocop/rake_task"
13
+
14
+ RuboCop::RakeTask.new
15
+
16
+ task default: %i[test rubocop]