ruby-openrouter 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 +7 -0
- data/README.md +230 -0
- data/lib/ruby_openrouter/client.rb +72 -0
- data/lib/ruby_openrouter/configuration.rb +10 -0
- data/lib/ruby_openrouter/error.rb +20 -0
- data/lib/ruby_openrouter/http.rb +96 -0
- data/lib/ruby_openrouter/version.rb +3 -0
- data/lib/ruby_openrouter.rb +25 -0
- metadata +76 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 40e456245000d212db0fb5c0c4eec0aa832d090a18b6b3b716065dd46145b303
|
|
4
|
+
data.tar.gz: 1fbb0f12e01b79875184a02ddd8419da8a56338d10d6cf97c369590a7f4c9094
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 8284009feb1cfdd07cddd1e465639e162552e89aed58e47a8c07eeed10198e86ee888372fb53574a33248e06c82e9f52e9dd841df11d7ceef174e5526a65ef73
|
|
7
|
+
data.tar.gz: 439e38643558e050df3d598f961993e08350b18836665296feb00b75ebf8a6e5ac9a5d2eeabb80abe42914a94b49dda7324a08c3518899f38b42d45f57f6623b
|
data/README.md
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
# ruby-openrouter
|
|
2
|
+
|
|
3
|
+
A minimal, conversational Ruby client for the [OpenRouter](https://openrouter.ai) API — access hundreds of LLMs (Claude, GPT-4, Gemini, Llama, and more) through a single, clean interface.
|
|
4
|
+
|
|
5
|
+
```ruby
|
|
6
|
+
client = RubyOpenrouter::Client.new(model: "anthropic/claude-3.5-sonnet")
|
|
7
|
+
client.system("You are a concise assistant.")
|
|
8
|
+
|
|
9
|
+
puts client.user("What is Ruby?")
|
|
10
|
+
#=> "Ruby is a dynamic, open-source programming language..."
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Features
|
|
16
|
+
|
|
17
|
+
- **Conversational by default** — message history is maintained automatically across turns
|
|
18
|
+
- **Streaming support** — receive tokens in real time via a simple block
|
|
19
|
+
- **Multi-turn context** — system, user, and assistant messages compose naturally
|
|
20
|
+
- **Error hierarchy** — typed exceptions for auth, rate limits, and server errors
|
|
21
|
+
- **Zero magic** — one client, one model, clear methods
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Installation
|
|
26
|
+
|
|
27
|
+
Add to your Gemfile:
|
|
28
|
+
|
|
29
|
+
```ruby
|
|
30
|
+
gem "ruby-openrouter"
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Or install directly:
|
|
34
|
+
|
|
35
|
+
```sh
|
|
36
|
+
gem install ruby-openrouter
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## Quick Start
|
|
42
|
+
|
|
43
|
+
```ruby
|
|
44
|
+
require "ruby_openrouter"
|
|
45
|
+
|
|
46
|
+
client = RubyOpenrouter::Client.new(
|
|
47
|
+
model: "openai/gpt-4o",
|
|
48
|
+
api_key: ENV["OPENROUTER_API_KEY"]
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
reply = client.user("Explain quantum entanglement in one sentence.")
|
|
52
|
+
puts reply
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## Configuration
|
|
58
|
+
|
|
59
|
+
### Per-client
|
|
60
|
+
|
|
61
|
+
Pass options directly when instantiating:
|
|
62
|
+
|
|
63
|
+
```ruby
|
|
64
|
+
client = RubyOpenrouter::Client.new(
|
|
65
|
+
model: "anthropic/claude-3.5-sonnet",
|
|
66
|
+
api_key: ENV["OPENROUTER_API_KEY"],
|
|
67
|
+
site_url: "https://myapp.com", # sent as HTTP-Referer for attribution
|
|
68
|
+
site_name: "MyApp", # sent as X-Title
|
|
69
|
+
timeout: 60
|
|
70
|
+
)
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Global
|
|
74
|
+
|
|
75
|
+
Set defaults once at startup (e.g., in an initializer):
|
|
76
|
+
|
|
77
|
+
```ruby
|
|
78
|
+
RubyOpenrouter.configure do |config|
|
|
79
|
+
config.api_key = ENV["OPENROUTER_API_KEY"]
|
|
80
|
+
config.site_url = "https://myapp.com"
|
|
81
|
+
config.site_name = "MyApp"
|
|
82
|
+
config.timeout = 30
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# All clients created afterwards will use these defaults
|
|
86
|
+
client = RubyOpenrouter::Client.new(model: "openai/gpt-4o-mini")
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## Usage
|
|
92
|
+
|
|
93
|
+
### System prompt
|
|
94
|
+
|
|
95
|
+
```ruby
|
|
96
|
+
client.system("You are a Ruby expert. Keep answers short.")
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Calling `#system` again replaces the existing system message — there is always at most one.
|
|
100
|
+
|
|
101
|
+
### Chat
|
|
102
|
+
|
|
103
|
+
```ruby
|
|
104
|
+
reply = client.user("What is a Proc?")
|
|
105
|
+
puts reply #=> "A Proc is a block of code..."
|
|
106
|
+
|
|
107
|
+
# Continue the conversation — history is kept automatically
|
|
108
|
+
reply = client.user("How does it differ from a lambda?")
|
|
109
|
+
puts reply #=> "Unlike a lambda, a Proc does not..."
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### Streaming
|
|
113
|
+
|
|
114
|
+
Pass a block to `#user` to receive tokens as they arrive:
|
|
115
|
+
|
|
116
|
+
```ruby
|
|
117
|
+
client.user("Write a haiku about Ruby.") do |chunk|
|
|
118
|
+
print chunk
|
|
119
|
+
end
|
|
120
|
+
# Streams: "Elegant syntax flows / Objects dance in harmony / Matz smiles warmly"
|
|
121
|
+
puts
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
The full response is appended to conversation history even when streaming.
|
|
125
|
+
|
|
126
|
+
### Few-shot examples
|
|
127
|
+
|
|
128
|
+
Seed the conversation with pre-written assistant turns using `#assistant`:
|
|
129
|
+
|
|
130
|
+
```ruby
|
|
131
|
+
client.user("Translate: hello")
|
|
132
|
+
client.assistant("Hola")
|
|
133
|
+
|
|
134
|
+
client.user("Translate: goodbye")
|
|
135
|
+
client.assistant("Adiós")
|
|
136
|
+
|
|
137
|
+
puts client.user("Translate: thank you")
|
|
138
|
+
#=> "Gracias"
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### Reset
|
|
142
|
+
|
|
143
|
+
Clear the conversation history while keeping the system prompt:
|
|
144
|
+
|
|
145
|
+
```ruby
|
|
146
|
+
client.system("You are a chef.")
|
|
147
|
+
client.user("What is mise en place?")
|
|
148
|
+
|
|
149
|
+
client.reset # clears user/assistant turns, keeps system prompt
|
|
150
|
+
|
|
151
|
+
client.user("What is a roux?") # fresh start, still a chef
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### List available models
|
|
155
|
+
|
|
156
|
+
```ruby
|
|
157
|
+
# From a client instance
|
|
158
|
+
models = client.models
|
|
159
|
+
models.each { |m| puts m["id"] }
|
|
160
|
+
|
|
161
|
+
# Or at the module level
|
|
162
|
+
models = RubyOpenrouter.models(api_key: ENV["OPENROUTER_API_KEY"])
|
|
163
|
+
models.first(5).each { |m| puts "#{m["id"]} — #{m["name"]}" }
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
## Error Handling
|
|
169
|
+
|
|
170
|
+
```ruby
|
|
171
|
+
begin
|
|
172
|
+
reply = client.user("Hello!")
|
|
173
|
+
rescue RubyOpenrouter::AuthenticationError => e
|
|
174
|
+
puts "Invalid API key: #{e.message}"
|
|
175
|
+
rescue RubyOpenrouter::RateLimitError => e
|
|
176
|
+
puts "Rate limited (status #{e.status}), retry later"
|
|
177
|
+
rescue RubyOpenrouter::ServerError => e
|
|
178
|
+
puts "OpenRouter server error: #{e.message}"
|
|
179
|
+
rescue RubyOpenrouter::APIError => e
|
|
180
|
+
puts "API error #{e.status}: #{e.message}"
|
|
181
|
+
rescue RubyOpenrouter::ConfigurationError => e
|
|
182
|
+
puts "Configuration problem: #{e.message}"
|
|
183
|
+
end
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
| Exception | HTTP status |
|
|
187
|
+
|---|---|
|
|
188
|
+
| `AuthenticationError` | 401 |
|
|
189
|
+
| `BadRequestError` | 400 |
|
|
190
|
+
| `RateLimitError` | 429 |
|
|
191
|
+
| `ServerError` | 5xx |
|
|
192
|
+
| `APIError` | any other 4xx/5xx |
|
|
193
|
+
| `ConfigurationError` | — (missing `api_key`) |
|
|
194
|
+
|
|
195
|
+
---
|
|
196
|
+
|
|
197
|
+
## Model IDs
|
|
198
|
+
|
|
199
|
+
OpenRouter uses `provider/model` identifiers. Some popular ones:
|
|
200
|
+
|
|
201
|
+
| Model | ID |
|
|
202
|
+
|---|---|
|
|
203
|
+
| Claude 3.5 Sonnet | `anthropic/claude-3.5-sonnet` |
|
|
204
|
+
| GPT-4o | `openai/gpt-4o` |
|
|
205
|
+
| GPT-4o mini | `openai/gpt-4o-mini` |
|
|
206
|
+
| Gemini 1.5 Pro | `google/gemini-pro-1.5` |
|
|
207
|
+
| Llama 3.3 70B | `meta-llama/llama-3.3-70b-instruct` |
|
|
208
|
+
|
|
209
|
+
See all available models at [openrouter.ai/models](https://openrouter.ai/models) or via `client.models`.
|
|
210
|
+
|
|
211
|
+
---
|
|
212
|
+
|
|
213
|
+
## Development
|
|
214
|
+
|
|
215
|
+
```sh
|
|
216
|
+
git clone https://github.com/deyvin/ruby-openrouter
|
|
217
|
+
cd ruby-openrouter
|
|
218
|
+
bundle install
|
|
219
|
+
|
|
220
|
+
bundle exec rspec # run tests
|
|
221
|
+
bundle exec rubocop # lint
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
Tests use [WebMock](https://github.com/bblimke/webmock) — no real API calls are made.
|
|
225
|
+
|
|
226
|
+
---
|
|
227
|
+
|
|
228
|
+
## License
|
|
229
|
+
|
|
230
|
+
MIT
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
module RubyOpenrouter
|
|
2
|
+
class Client
|
|
3
|
+
def initialize(model:, api_key: nil, site_url: nil, site_name: nil, timeout: nil)
|
|
4
|
+
@model = model
|
|
5
|
+
@config = build_config(api_key: api_key, site_url: site_url, site_name: site_name, timeout: timeout)
|
|
6
|
+
@messages = []
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def system(content)
|
|
10
|
+
@messages.reject! { |m| m[:role] == "system" }
|
|
11
|
+
@messages.unshift(role: "system", content: content)
|
|
12
|
+
self
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def user(content, &block)
|
|
16
|
+
@messages << { role: "user", content: content }
|
|
17
|
+
if block
|
|
18
|
+
accumulated = ""
|
|
19
|
+
Http.stream("/chat/completions", body: request_body(stream: true), config: @config) do |chunk|
|
|
20
|
+
accumulated += chunk
|
|
21
|
+
block.call(chunk)
|
|
22
|
+
end
|
|
23
|
+
@messages << { role: "assistant", content: accumulated } unless accumulated.empty?
|
|
24
|
+
nil
|
|
25
|
+
else
|
|
26
|
+
text = complete
|
|
27
|
+
@messages << { role: "assistant", content: text }
|
|
28
|
+
text
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def assistant(content)
|
|
33
|
+
@messages << { role: "assistant", content: content }
|
|
34
|
+
self
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def reset
|
|
38
|
+
system_msg = @messages.find { |m| m[:role] == "system" }
|
|
39
|
+
@messages = system_msg ? [system_msg] : []
|
|
40
|
+
self
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def models
|
|
44
|
+
Http.get("/models", config: @config).fetch("data", [])
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def complete
|
|
50
|
+
body = request_body
|
|
51
|
+
resp = Http.post("/chat/completions", body: body, config: @config)
|
|
52
|
+
resp.dig("choices", 0, "message", "content").to_s
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def request_body(stream: false)
|
|
56
|
+
body = { model: @model, messages: @messages }
|
|
57
|
+
body[:stream] = true if stream
|
|
58
|
+
body
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def build_config(api_key:, site_url:, site_name:, timeout:)
|
|
62
|
+
base = RubyOpenrouter.configuration
|
|
63
|
+
cfg = Configuration.new
|
|
64
|
+
cfg.api_key = api_key || base.api_key || raise(ConfigurationError, "api_key is required")
|
|
65
|
+
cfg.base_url = base.base_url
|
|
66
|
+
cfg.timeout = timeout || base.timeout
|
|
67
|
+
cfg.site_url = site_url || base.site_url
|
|
68
|
+
cfg.site_name = site_name || base.site_name
|
|
69
|
+
cfg
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
module RubyOpenrouter
|
|
2
|
+
class Error < StandardError; end
|
|
3
|
+
|
|
4
|
+
class ConfigurationError < Error; end
|
|
5
|
+
|
|
6
|
+
class APIError < Error
|
|
7
|
+
attr_reader :status, :code
|
|
8
|
+
|
|
9
|
+
def initialize(message, status: nil, code: nil)
|
|
10
|
+
super(message)
|
|
11
|
+
@status = status
|
|
12
|
+
@code = code
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
class AuthenticationError < APIError; end
|
|
17
|
+
class BadRequestError < APIError; end
|
|
18
|
+
class RateLimitError < APIError; end
|
|
19
|
+
class ServerError < APIError; end
|
|
20
|
+
end
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
require "faraday"
|
|
2
|
+
require "faraday/retry"
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "uri"
|
|
5
|
+
require "json"
|
|
6
|
+
|
|
7
|
+
module RubyOpenrouter
|
|
8
|
+
module Http
|
|
9
|
+
def self.connection(config)
|
|
10
|
+
Faraday.new(url: config.base_url.chomp("/") + "/") do |conn|
|
|
11
|
+
conn.request :retry, max: 2, interval: 0.5, retry_statuses: [429, 500, 502, 503]
|
|
12
|
+
conn.request :json
|
|
13
|
+
conn.response :json
|
|
14
|
+
conn.headers["Authorization"] = "Bearer #{config.api_key}"
|
|
15
|
+
conn.headers["Content-Type"] = "application/json"
|
|
16
|
+
conn.headers["HTTP-Referer"] = config.site_url if config.site_url
|
|
17
|
+
conn.headers["X-Title"] = config.site_name if config.site_name
|
|
18
|
+
conn.options.timeout = config.timeout
|
|
19
|
+
conn.adapter Faraday.default_adapter
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def self.post(path, body:, config:)
|
|
24
|
+
resp = connection(config).post(relative(path), body)
|
|
25
|
+
handle_error(resp)
|
|
26
|
+
resp.body
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def self.get(path, config:)
|
|
30
|
+
resp = connection(config).get(relative(path))
|
|
31
|
+
handle_error(resp)
|
|
32
|
+
resp.body
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def self.relative(path)
|
|
36
|
+
path.sub(%r{^/}, "")
|
|
37
|
+
end
|
|
38
|
+
private_class_method :relative
|
|
39
|
+
|
|
40
|
+
def self.stream(path, body:, config:, &block)
|
|
41
|
+
uri = URI.join(config.base_url + "/", path.sub(%r{^/}, ""))
|
|
42
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
43
|
+
http.use_ssl = uri.scheme == "https"
|
|
44
|
+
http.read_timeout = config.timeout
|
|
45
|
+
|
|
46
|
+
request = Net::HTTP::Post.new(uri.request_uri)
|
|
47
|
+
request["Authorization"] = "Bearer #{config.api_key}"
|
|
48
|
+
request["Content-Type"] = "application/json"
|
|
49
|
+
request["HTTP-Referer"] = config.site_url if config.site_url
|
|
50
|
+
request["X-Title"] = config.site_name if config.site_name
|
|
51
|
+
request.body = JSON.generate(body)
|
|
52
|
+
|
|
53
|
+
buffer = ""
|
|
54
|
+
http.request(request) do |response|
|
|
55
|
+
raise_for_status(response.code.to_i, nil)
|
|
56
|
+
response.read_body do |raw_chunk|
|
|
57
|
+
buffer += raw_chunk
|
|
58
|
+
while (line_end = buffer.index("\n"))
|
|
59
|
+
line = buffer.slice!(0..line_end).strip
|
|
60
|
+
buffer = buffer.lstrip
|
|
61
|
+
next unless line.start_with?("data: ")
|
|
62
|
+
|
|
63
|
+
payload = line.delete_prefix("data: ")
|
|
64
|
+
next if payload == "[DONE]"
|
|
65
|
+
|
|
66
|
+
chunk = JSON.parse(payload) rescue next
|
|
67
|
+
text = chunk.dig("choices", 0, "delta", "content")
|
|
68
|
+
block.call(text) if text && !text.empty?
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def self.handle_error(resp)
|
|
75
|
+
raise_for_status(resp.status, resp.body)
|
|
76
|
+
end
|
|
77
|
+
private_class_method :handle_error
|
|
78
|
+
|
|
79
|
+
def self.raise_for_status(status, body)
|
|
80
|
+
return if status < 400
|
|
81
|
+
|
|
82
|
+
message = body.is_a?(Hash) ? body.dig("error", "message").to_s : body.to_s
|
|
83
|
+
code = body.is_a?(Hash) ? body.dig("error", "code") : nil
|
|
84
|
+
|
|
85
|
+
klass = case status
|
|
86
|
+
when 401 then AuthenticationError
|
|
87
|
+
when 400 then BadRequestError
|
|
88
|
+
when 429 then RateLimitError
|
|
89
|
+
when 500..599 then ServerError
|
|
90
|
+
else APIError
|
|
91
|
+
end
|
|
92
|
+
raise klass.new(message, status: status, code: code)
|
|
93
|
+
end
|
|
94
|
+
private_class_method :raise_for_status
|
|
95
|
+
end
|
|
96
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
require_relative "ruby_openrouter/version"
|
|
2
|
+
require_relative "ruby_openrouter/configuration"
|
|
3
|
+
require_relative "ruby_openrouter/error"
|
|
4
|
+
require_relative "ruby_openrouter/http"
|
|
5
|
+
require_relative "ruby_openrouter/client"
|
|
6
|
+
|
|
7
|
+
module RubyOpenrouter
|
|
8
|
+
class << self
|
|
9
|
+
def configure
|
|
10
|
+
yield(configuration)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def configuration
|
|
14
|
+
@configuration ||= Configuration.new
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def models(api_key: nil)
|
|
18
|
+
cfg = Configuration.new
|
|
19
|
+
cfg.api_key = api_key || configuration.api_key || raise(ConfigurationError, "api_key is required")
|
|
20
|
+
cfg.base_url = configuration.base_url
|
|
21
|
+
cfg.timeout = configuration.timeout
|
|
22
|
+
Http.get("/models", config: cfg).fetch("data", [])
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: ruby-openrouter
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Deyvin
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 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.7'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '2.7'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: faraday-retry
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '2.2'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '2.2'
|
|
40
|
+
description: A minimal conversational Ruby client for OpenRouter — chat completions
|
|
41
|
+
with streaming, model listing, and multi-turn history.
|
|
42
|
+
email:
|
|
43
|
+
- deyvin@gmail.com
|
|
44
|
+
executables: []
|
|
45
|
+
extensions: []
|
|
46
|
+
extra_rdoc_files: []
|
|
47
|
+
files:
|
|
48
|
+
- README.md
|
|
49
|
+
- lib/ruby_openrouter.rb
|
|
50
|
+
- lib/ruby_openrouter/client.rb
|
|
51
|
+
- lib/ruby_openrouter/configuration.rb
|
|
52
|
+
- lib/ruby_openrouter/error.rb
|
|
53
|
+
- lib/ruby_openrouter/http.rb
|
|
54
|
+
- lib/ruby_openrouter/version.rb
|
|
55
|
+
homepage: https://github.com/deyvin/ruby-openrouter
|
|
56
|
+
licenses:
|
|
57
|
+
- MIT
|
|
58
|
+
metadata: {}
|
|
59
|
+
rdoc_options: []
|
|
60
|
+
require_paths:
|
|
61
|
+
- lib
|
|
62
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
63
|
+
requirements:
|
|
64
|
+
- - ">="
|
|
65
|
+
- !ruby/object:Gem::Version
|
|
66
|
+
version: '3.1'
|
|
67
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
68
|
+
requirements:
|
|
69
|
+
- - ">="
|
|
70
|
+
- !ruby/object:Gem::Version
|
|
71
|
+
version: '0'
|
|
72
|
+
requirements: []
|
|
73
|
+
rubygems_version: 3.6.9
|
|
74
|
+
specification_version: 4
|
|
75
|
+
summary: Ruby client for the OpenRouter API
|
|
76
|
+
test_files: []
|