vibe-sort 0.3.1 → 0.4.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2f7d0e027ef929afd808fecba7f64a697a87ff6be489bce20bf9a8da73334847
4
- data.tar.gz: b59cb7de762f093a8a5c0ac702841b4bad42d68599a346135a69867af54b6407
3
+ metadata.gz: 15fa0d0262e797d5c29705f56902c66beb746d438505cd6fbace4a999b6c780a
4
+ data.tar.gz: 4f407bf5defba0274c43a352b2274899eea9f1579c82b1eae03663ed2ec5cf6f
5
5
  SHA512:
6
- metadata.gz: 53ac2db8b8b227fd151542ed86a350aab7bf6e3a740347cef034348f175465f3e17b7f32b6ca868da4c300bc0ad7b681b5fcdc84375e58838115e0f0d34ec0ac
7
- data.tar.gz: 604e37c539b045cf9ecc3ff12a88fa467514506fb9149a2e02d85278f5a2dbd468ecf0cacf9259922dca87d21cdff4fd7f247cfad8dcf5752e5289ae82a2662c
6
+ metadata.gz: a3be092377c5248cc964703f289c464cbec676d59e95db6f7c1cd52e7256d17f8dcfeb143b0b980865d3ee03e3d75b75350e8587cb235def5569984bdcd8775d
7
+ data.tar.gz: 527d7a06f586edb938df467e5ad4e00caf18ea50a5ed14467fb9110a749ee604d49fe79d6fc3e91165858fe7090673610004724f71b91fbc8c2a88ed60e3e532
data/CHANGELOG.md CHANGED
@@ -7,6 +7,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.4.0] - 2026-07-12
11
+
12
+ ### Added
13
+
14
+ - **OpenRouter provider** (`provider: :openrouter`): access hundreds of models through OpenRouter's OpenAI-compatible API. Default model `openai/gpt-4o-mini`; sends OpenRouter's recommended attribution headers
15
+ - **API key format detection**: when `provider:` is omitted, the provider is inferred from the `api_key` prefix (`sk-ant-` → Anthropic, `sk-or-` → OpenRouter, `gsk_` → Groq, `AIza`/`AQ.` → Gemini, `xai-` → SpaceXAI, `sk-` → OpenAI). Unrecognized key formats still default to OpenAI
16
+ - **`extra_params:` option** on `VibeSort::Client.new`: a hash of provider-native request parameters deep-merged into the payload last, so it can override anything the adapter builds (e.g. `max_tokens`, `response_format`, Gemini's nested `generationConfig`, OpenRouter provider-routing preferences)
17
+ - Free-tier model benchmark results (`docs/free_model_benchmark_2026-07-12.md`)
18
+
19
+ ### Changed
20
+
21
+ - Passing an explicit `provider:` that disagrees with the detected key format now prints a one-line warning to stderr; the explicit provider still wins
22
+
23
+ ### Notes
24
+
25
+ - Behavior change (hence the minor bump): a provider-prefixed key with no `provider:` argument previously always hit the OpenAI API and failed; it now routes to the detected provider and works. Unrecognized keys with no `provider:` still default to OpenAI, and all explicit-provider call sites behave identically
26
+ - Still no runtime dependencies beyond Faraday
27
+
10
28
  ## [0.3.1] - 2026-07-11
11
29
 
12
30
  ### Changed
data/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # VibeSort
2
2
 
3
- > **AI-powered array sorting using OpenAI, Anthropic Claude, Google Gemini, Groq, or SpaceXAI Grok**
3
+ > **AI-powered array sorting using OpenAI, Anthropic Claude, Google Gemini, Groq, SpaceXAI Grok, or OpenRouter**
4
4
 
5
- VibeSort is a proof-of-concept Ruby gem that demonstrates sorting arrays by leveraging LLM APIs. Instead of using traditional sorting algorithms, it asks an AI model to do the work. Supports the OpenAI Chat Completions API, the Anthropic Messages API, the Google Gemini API, and the OpenAI-compatible Groq and SpaceXAI (xAI) APIs, with zero dependencies beyond Faraday.
5
+ VibeSort is a proof-of-concept Ruby gem that demonstrates sorting arrays by leveraging LLM APIs. Instead of using traditional sorting algorithms, it asks an AI model to do the work. Supports the OpenAI Chat Completions API, the Anthropic Messages API, the Google Gemini API, and the OpenAI-compatible Groq, SpaceXAI (xAI), and OpenRouter APIs, with zero dependencies beyond Faraday.
6
6
 
7
7
  [![Gem Version](https://badge.fury.io/rb/vibe-sort.svg)](https://badge.fury.io/rb/vibe-sort)
8
8
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
@@ -13,10 +13,11 @@ This is a **proof-of-concept** and educational project. It is not intended for p
13
13
 
14
14
  ## Features
15
15
 
16
- - **AI-Powered Sorting**: Uses OpenAI, Anthropic Claude, Google Gemini, Groq, or SpaceXAI Grok models to sort arrays
16
+ - **AI-Powered Sorting**: Uses OpenAI, Anthropic Claude, Google Gemini, Groq, SpaceXAI Grok, or OpenRouter models to sort arrays
17
17
  - **Multi-Provider**: Switch providers with a single `provider:` option, no extra dependencies
18
+ - **Key Format Detection**: Omit `provider:` and VibeSort infers it from your API key's prefix
18
19
  - **Simple Interface**: Clean, intuitive API with a single `sort` method
19
- - **Configurable**: Supports custom model and temperature settings
20
+ - **Configurable**: Custom model, temperature, and an `extra_params:` escape hatch for any provider-native request parameter
20
21
  - **Error Handling**: Comprehensive error handling with clear error messages
21
22
  - **Structured Output**: Uses JSON mode for reliable, parsable responses
22
23
  - **Type Validation**: Validates input and output to ensure data integrity
@@ -26,7 +27,7 @@ This is a **proof-of-concept** and educational project. It is not intended for p
26
27
 
27
28
  - Ruby **3.0** or newer (MRI)
28
29
  - Bundler **2.0+**
29
- - An API key for at least one supported provider (OpenAI, Anthropic, Google Gemini, Groq, or SpaceXAI)
30
+ - An API key for at least one supported provider (OpenAI, Anthropic, Google Gemini, Groq, SpaceXAI, or OpenRouter)
30
31
  - Internet connectivity (each sort performs a remote API call)
31
32
 
32
33
  ## Installation
@@ -121,11 +122,11 @@ end
121
122
 
122
123
  ### Choosing a Provider
123
124
 
124
- VibeSort defaults to OpenAI, but any supported provider can be selected with the `provider:` option:
125
+ Any supported provider can be selected with the `provider:` option:
125
126
 
126
127
  ```ruby
127
- # OpenAI (default)
128
- client = VibeSort::Client.new(api_key: ENV['OPENAI_API_KEY'])
128
+ # OpenAI
129
+ client = VibeSort::Client.new(provider: :openai, api_key: ENV['OPENAI_API_KEY'])
129
130
 
130
131
  # Anthropic Claude
131
132
  client = VibeSort::Client.new(provider: :anthropic, api_key: ENV['ANTHROPIC_API_KEY'])
@@ -138,6 +139,9 @@ client = VibeSort::Client.new(provider: :groq, api_key: ENV['GROQ_API_KEY'])
138
139
 
139
140
  # SpaceXAI (formerly xAI) Grok
140
141
  client = VibeSort::Client.new(provider: :spacexai, api_key: ENV['XAI_API_KEY'])
142
+
143
+ # OpenRouter (hundreds of models behind one API)
144
+ client = VibeSort::Client.new(provider: :openrouter, api_key: ENV['OPENROUTER_API_KEY'])
141
145
  ```
142
146
 
143
147
  | Provider | API | Default model | Override with `model:` |
@@ -147,9 +151,12 @@ client = VibeSort::Client.new(provider: :spacexai, api_key: ENV['XAI_API_KEY'])
147
151
  | `:gemini` | generateContent | `gemini-2.5-flash` | e.g. `gemini-2.5-pro` |
148
152
  | `:groq` | Chat Completions (OpenAI-compatible) | `llama-3.3-70b-versatile` | any Groq-hosted model |
149
153
  | `:spacexai` | Chat Completions (OpenAI-compatible) | `grok-4` | e.g. `grok-4.5` |
154
+ | `:openrouter` | Chat Completions (OpenAI-compatible) | `openai/gpt-4o-mini` | any OpenRouter model ID |
150
155
 
151
156
  > **Groq is not Grok**: Groq is the independent fast-inference company (`api.groq.com`); Grok is SpaceXAI's model (`api.x.ai`). They are unrelated, and VibeSort supports both.
152
157
 
158
+ > **OpenRouter JSON mode caveat**: OpenRouter forwards `response_format: { type: "json_object" }` to the underlying model, but only some models honor it. Models that ignore it produce a "Failed to parse JSON response" error. Stick to models that support JSON mode, or use `extra_params:` to adjust the request.
159
+
153
160
  ```ruby
154
161
  # Pick a specific model
155
162
  client = VibeSort::Client.new(
@@ -159,6 +166,53 @@ client = VibeSort::Client.new(
159
166
  )
160
167
  ```
161
168
 
169
+ ### Provider Detection (provider: is optional)
170
+
171
+ If you omit `provider:`, VibeSort infers it from your API key's prefix:
172
+
173
+ ```ruby
174
+ VibeSort::Client.new(api_key: 'sk-ant-...') # => :anthropic
175
+ VibeSort::Client.new(api_key: 'sk-or-...') # => :openrouter
176
+ VibeSort::Client.new(api_key: 'gsk_...') # => :groq
177
+ VibeSort::Client.new(api_key: 'AIza...') # => :gemini (classic key)
178
+ VibeSort::Client.new(api_key: 'AQ....') # => :gemini (newer key format)
179
+ VibeSort::Client.new(api_key: 'xai-...') # => :spacexai
180
+ VibeSort::Client.new(api_key: 'sk-...') # => :openai
181
+ ```
182
+
183
+ Key prefixes are conventions, not contracts, so detection is deliberately soft:
184
+
185
+ - Unrecognized key formats (proxies, gateways, self-hosted endpoints) default to `:openai` — same behavior as before v0.4.0
186
+ - An explicit `provider:` always wins; if it disagrees with the detected format you get a one-line stderr warning, never an error
187
+
188
+ ### Extra Request Parameters
189
+
190
+ `extra_params:` deep-merges a hash of provider-native parameters into the request payload **last**, so it can add to or override anything VibeSort builds:
191
+
192
+ ```ruby
193
+ # Cap completion length (OpenAI/Groq/SpaceXAI/OpenRouter wire format)
194
+ client = VibeSort::Client.new(
195
+ api_key: ENV['OPENAI_API_KEY'],
196
+ extra_params: { max_tokens: 200 }
197
+ )
198
+
199
+ # Gemini uses its own parameter names — nested hashes merge recursively
200
+ client = VibeSort::Client.new(
201
+ provider: :gemini,
202
+ api_key: ENV['GEMINI_API_KEY'],
203
+ extra_params: { generationConfig: { maxOutputTokens: 256 } }
204
+ )
205
+
206
+ # OpenRouter provider-routing preferences
207
+ client = VibeSort::Client.new(
208
+ provider: :openrouter,
209
+ api_key: ENV['OPENROUTER_API_KEY'],
210
+ extra_params: { provider: { sort: 'throughput' } }
211
+ )
212
+ ```
213
+
214
+ Keys are provider-native and passed through untranslated: `max_tokens` means whatever your provider says it means, and Gemini users write Gemini's camelCase names. Overriding core keys (`response_format`, `temperature`, even `model`) is allowed — it's an escape hatch, use with care.
215
+
162
216
  ### Advanced Configuration
163
217
 
164
218
  You can customize the model's behavior using the `temperature` parameter:
@@ -225,9 +279,10 @@ VibeSort follows a clean, modular architecture:
225
279
 
226
280
  - **`VibeSort::Client`**: Public interface for users
227
281
  - **`VibeSort::Configuration`**: Manages provider, API key, model, and settings
282
+ - **`VibeSort::KeyDetector`**: Best-effort provider inference from API key prefixes
228
283
  - **`VibeSort::Sorter`**: Dispatches to the configured provider adapter
229
284
  - **`VibeSort::Providers::Base`**: Shared prompt, HTTP plumbing (Faraday), and response validation
230
- - **`VibeSort::Providers::{OpenAI, Anthropic, Gemini, Groq, SpaceXAI}`**: Provider-specific request/response mapping
285
+ - **`VibeSort::Providers::{OpenAI, Anthropic, Gemini, Groq, SpaceXAI, OpenRouter}`**: Provider-specific request/response mapping
231
286
  - **`VibeSort::ApiError`**: Custom exception for API-related errors
232
287
 
233
288
  See the [Architecture Documentation](docs/architecture.md) for more details.
@@ -297,11 +352,12 @@ Traditional sorting (e.g., Ruby's `Array#sort`) is:
297
352
  Set the API key for your chosen provider as an environment variable:
298
353
 
299
354
  ```bash
300
- export OPENAI_API_KEY='your-api-key-here' # provider: :openai (default)
301
- export ANTHROPIC_API_KEY='your-api-key-here' # provider: :anthropic
302
- export GEMINI_API_KEY='your-api-key-here' # provider: :gemini
303
- export GROQ_API_KEY='your-api-key-here' # provider: :groq
304
- export XAI_API_KEY='your-api-key-here' # provider: :spacexai
355
+ export OPENAI_API_KEY='your-api-key-here' # provider: :openai (fallback default)
356
+ export ANTHROPIC_API_KEY='your-api-key-here' # provider: :anthropic
357
+ export GEMINI_API_KEY='your-api-key-here' # provider: :gemini
358
+ export GROQ_API_KEY='your-api-key-here' # provider: :groq
359
+ export XAI_API_KEY='your-api-key-here' # provider: :spacexai
360
+ export OPENROUTER_API_KEY='your-api-key-here' # provider: :openrouter
305
361
  ```
306
362
 
307
363
  Or use a `.env` file with the `dotenv` gem:
@@ -322,7 +378,7 @@ The gem is available as open source under the terms of the [MIT License](https:/
322
378
  ## Acknowledgments
323
379
 
324
380
  - Built with [Faraday](https://lostisland.github.io/faraday/) for HTTP requests
325
- - Powered by [OpenAI](https://openai.com/), [Anthropic](https://www.anthropic.com/), [Google Gemini](https://ai.google.dev/), [Groq](https://groq.com/), and [SpaceXAI](https://x.ai/) models
381
+ - Powered by [OpenAI](https://openai.com/), [Anthropic](https://www.anthropic.com/), [Google Gemini](https://ai.google.dev/), [Groq](https://groq.com/), [SpaceXAI](https://x.ai/), and [OpenRouter](https://openrouter.ai/) models
326
382
  - Inspired by the absurdity and creativity of the AI era
327
383
 
328
384
  ---
@@ -6,7 +6,7 @@ The main public interface for the VibeSort gem.
6
6
 
7
7
  ### Class Methods
8
8
 
9
- #### `new(api_key:, temperature: 0.0, provider: :openai, model: nil)`
9
+ #### `new(api_key:, temperature: 0.0, provider: nil, model: nil, extra_params: {})`
10
10
 
11
11
  Creates a new VibeSort client instance.
12
12
 
@@ -18,24 +18,27 @@ Creates a new VibeSort client instance.
18
18
  - Lower values (0.0-0.3): More deterministic and consistent
19
19
  - Higher values (0.7-2.0): More random and creative
20
20
  - Ignored by providers whose current models do not accept it (Anthropic)
21
- - `provider` (Symbol or String, optional): LLM provider (default: `:openai`)
22
- - Supported: `:openai`, `:anthropic`, `:gemini`, `:groq`, `:spacexai`
21
+ - `provider` (Symbol or String, optional): LLM provider
22
+ - Supported: `:openai`, `:anthropic`, `:gemini`, `:groq`, `:spacexai`, `:openrouter`
23
+ - When omitted, inferred from the `api_key` prefix via `VibeSort::KeyDetector`; unrecognized formats default to `:openai`
24
+ - When explicit and the key prefix suggests a different provider, a one-line warning is printed to stderr and the explicit provider is used
23
25
  - `model` (String, optional): Model ID override (default: the provider's default model)
26
+ - `extra_params` (Hash, optional): Provider-native request parameters deep-merged into the payload last, so they can override anything the adapter builds (default: `{}`)
24
27
 
25
28
  **Returns:** `VibeSort::Client` instance
26
29
 
27
30
  **Raises:**
28
31
 
29
- - `ArgumentError`: If api_key is nil or empty, or the provider is unknown
32
+ - `ArgumentError`: If api_key is nil or empty, or an explicit provider is unknown
30
33
 
31
34
  **Example:**
32
35
 
33
36
  ```ruby
34
- # Basic initialization (OpenAI, default provider)
35
- client = VibeSort::Client.new(api_key: ENV['OPENAI_API_KEY'])
37
+ # Provider inferred from the key prefix (sk-ant-... => :anthropic)
38
+ client = VibeSort::Client.new(api_key: ENV['ANTHROPIC_API_KEY'])
36
39
 
37
- # Anthropic Claude
38
- client = VibeSort::Client.new(provider: :anthropic, api_key: ENV['ANTHROPIC_API_KEY'])
40
+ # Explicit provider
41
+ client = VibeSort::Client.new(provider: :openai, api_key: ENV['OPENAI_API_KEY'])
39
42
 
40
43
  # Google Gemini with a custom model
41
44
  client = VibeSort::Client.new(provider: :gemini, api_key: ENV['GEMINI_API_KEY'], model: 'gemini-2.5-pro')
@@ -45,6 +48,14 @@ client = VibeSort::Client.new(
45
48
  api_key: ENV['OPENAI_API_KEY'],
46
49
  temperature: 0.2
47
50
  )
51
+
52
+ # With extra provider-native request parameters
53
+ client = VibeSort::Client.new(
54
+ provider: :openrouter,
55
+ api_key: ENV['OPENROUTER_API_KEY'],
56
+ model: 'meta-llama/llama-3.3-70b-instruct',
57
+ extra_params: { max_tokens: 200 }
58
+ )
48
59
  ```
49
60
 
50
61
  ### Instance Methods
@@ -110,7 +121,7 @@ Configuration object for VibeSort. Usually not used directly by end users.
110
121
 
111
122
  ### Class Methods
112
123
 
113
- #### `new(api_key:, temperature: 0.0, provider: :openai, model: nil)`
124
+ #### `new(api_key:, temperature: 0.0, provider: nil, model: nil, extra_params: {})`
114
125
 
115
126
  Creates a new configuration object.
116
127
 
@@ -118,8 +129,9 @@ Creates a new configuration object.
118
129
 
119
130
  - `api_key` (String, required): API key for the selected provider
120
131
  - `temperature` (Float, optional): Model temperature (default: 0.0)
121
- - `provider` (Symbol or String, optional): `:openai` (default), `:anthropic`, `:gemini`, `:groq`, or `:spacexai`
132
+ - `provider` (Symbol, String, or nil, optional): `:openai`, `:anthropic`, `:gemini`, `:groq`, `:spacexai`, or `:openrouter`; nil (default) infers the provider from the api_key prefix, falling back to `:openai`
122
133
  - `model` (String, optional): Model ID override (nil uses the provider's default)
134
+ - `extra_params` (Hash, optional): Provider-native request parameters deep-merged into the payload last (default: `{}`)
123
135
 
124
136
  **Raises:**
125
137
 
@@ -161,6 +173,40 @@ Returns the configured model override, or nil when using the provider default (r
161
173
 
162
174
  **Returns:** String or nil
163
175
 
176
+ #### `extra_params`
177
+
178
+ Returns the configured extra request parameters hash (read-only).
179
+
180
+ **Returns:** Hash
181
+
182
+ ---
183
+
184
+ ## VibeSort::KeyDetector
185
+
186
+ Best-effort provider inference from API key prefixes. Prefixes are conventions, not contracts — detection is only a soft default and a source of non-fatal warnings, never validation.
187
+
188
+ ### Class Methods
189
+
190
+ #### `detect(api_key)`
191
+
192
+ **Parameters:**
193
+
194
+ - `api_key` (String, required): API key to inspect
195
+
196
+ **Returns:** Symbol (detected provider) or nil (unrecognized format)
197
+
198
+ **Prefix map (ordered — specific prefixes checked before `sk-`):**
199
+
200
+ | Prefix | Provider |
201
+ |---|---|
202
+ | `sk-ant-` | `:anthropic` |
203
+ | `sk-or-` | `:openrouter` |
204
+ | `sk-` | `:openai` |
205
+ | `gsk_` | `:groq` |
206
+ | `AIza` | `:gemini` (classic key) |
207
+ | `AQ.` | `:gemini` (newer key format) |
208
+ | `xai-` | `:spacexai` |
209
+
164
210
  ---
165
211
 
166
212
  ## VibeSort::Sorter
@@ -199,7 +245,7 @@ Performs the sorting operation via the configured provider's API.
199
245
 
200
246
  Maps provider symbols to adapter classes.
201
247
 
202
- **Value:** `{ openai:, anthropic:, gemini:, groq:, spacexai: }`
248
+ **Value:** `{ openai:, anthropic:, gemini:, groq:, spacexai:, openrouter: }`
203
249
 
204
250
  ---
205
251
 
@@ -214,11 +260,14 @@ Internal provider adapters. `Providers::Base` holds the shared prompt, Faraday H
214
260
  | `Providers::Gemini` | `https://generativelanguage.googleapis.com/v1beta/models/{model}:generateContent` | `gemini-2.5-flash` |
215
261
  | `Providers::Groq` | `https://api.groq.com/openai/v1/chat/completions` | `llama-3.3-70b-versatile` |
216
262
  | `Providers::SpaceXAI` | `https://api.x.ai/v1/chat/completions` | `grok-4` |
263
+ | `Providers::OpenRouter` | `https://openrouter.ai/api/v1/chat/completions` | `openai/gpt-4o-mini` |
217
264
 
218
265
  Notes:
219
266
 
220
267
  - The Anthropic adapter uses structured outputs (JSON schema) and does not send `temperature` (rejected by current Claude models)
221
- - The Groq and SpaceXAI adapters subclass `Providers::OpenAI` (OpenAI-compatible APIs)
268
+ - The Groq, SpaceXAI, and OpenRouter adapters subclass `Providers::OpenAI` (OpenAI-compatible APIs)
269
+ - The OpenRouter adapter sends OpenRouter's recommended attribution headers (`HTTP-Referer`, `X-Title`)
270
+ - `Providers::Base` deep-merges `config.extra_params` into every payload last, so it can override anything the adapter builds
222
271
 
223
272
  ---
224
273
 
@@ -307,7 +356,7 @@ All sorting operations return a consistent hash structure:
307
356
 
308
357
  ### API Errors
309
358
 
310
- The `{Provider}` prefix names the configured provider (OpenAI, Anthropic, Gemini, Groq, or SpaceXAI):
359
+ The `{Provider}` prefix names the configured provider (OpenAI, Anthropic, Gemini, Groq, SpaceXAI, or OpenRouter):
311
360
 
312
361
  | Error Message Pattern | Cause | Solution |
313
362
  |----------------------|-------|----------|
@@ -455,11 +504,12 @@ results = threads.map(&:value)
455
504
  Set the API key for whichever provider you use:
456
505
 
457
506
  ```bash
458
- export OPENAI_API_KEY='sk-your-key-here' # provider: :openai (default)
507
+ export OPENAI_API_KEY='sk-your-key-here' # provider: :openai (fallback default)
459
508
  export ANTHROPIC_API_KEY='your-key-here' # provider: :anthropic
460
509
  export GEMINI_API_KEY='your-key-here' # provider: :gemini
461
510
  export GROQ_API_KEY='your-key-here' # provider: :groq
462
511
  export XAI_API_KEY='your-key-here' # provider: :spacexai
512
+ export OPENROUTER_API_KEY='your-key-here' # provider: :openrouter
463
513
  ```
464
514
 
465
515
  **Usage:**
@@ -472,9 +522,9 @@ client = VibeSort::Client.new(api_key: ENV['OPENAI_API_KEY'])
472
522
 
473
523
  ## Version
474
524
 
475
- Current version: `0.3.0`
525
+ Current version: `0.4.0`
476
526
 
477
527
  ```ruby
478
528
  VibeSort::VERSION
479
- # => "0.3.0"
529
+ # => "0.4.0"
480
530
  ```
data/docs/architecture.md CHANGED
@@ -20,9 +20,10 @@ VibeSort follows a layered architecture with clear separation of concerns:
20
20
 
21
21
  ┌─────────────────────────────────────────┐
22
22
  │ VibeSort::Configuration │
23
- │ - Provider selection
23
+ │ - Provider selection (explicit or
24
+ │ inferred via KeyDetector) │
24
25
  │ - API key management │
25
- │ - Model override
26
+ │ - Model override, extra_params
26
27
  │ - Temperature settings │
27
28
  └─────────────────┬───────────────────────┘
28
29
 
@@ -37,10 +38,11 @@ VibeSort follows a layered architecture with clear separation of concerns:
37
38
  │ VibeSort::Providers::Base │
38
39
  │ - Shared prompt │
39
40
  │ - HTTP client (Faraday) │
41
+ │ - extra_params deep merge │
40
42
  │ - Response parsing & validation │
41
43
  ├─────────────────────────────────────────┤
42
44
  │ OpenAI │ Anthropic │ Gemini │ │
43
- │ Groq │ SpaceXAI
45
+ │ Groq │ SpaceXAI OpenRouter │
44
46
  │ (request/response wire formats) │
45
47
  └─────────────────┬───────────────────────┘
46
48
 
@@ -50,6 +52,7 @@ VibeSort follows a layered architecture with clear separation of concerns:
50
52
  │ api.openai.com │ api.anthropic.com │
51
53
  │ generativelanguage.googleapis.com │
52
54
  │ api.groq.com │ api.x.ai │
55
+ │ openrouter.ai │
53
56
  └─────────────────────────────────────────┘
54
57
  ```
55
58
 
@@ -67,7 +70,7 @@ VibeSort follows a layered architecture with clear separation of concerns:
67
70
  - Return standardized response hashes
68
71
 
69
72
  **Key Methods**:
70
- - `initialize(api_key:, temperature: 0.0, provider: :openai, model: nil)`: Creates client with configuration
73
+ - `initialize(api_key:, temperature: 0.0, provider: nil, model: nil, extra_params: {})`: Creates client with configuration
71
74
  - `sort(array)`: Sorts array and returns result hash
72
75
 
73
76
  **Return Format**:
@@ -84,19 +87,26 @@ VibeSort follows a layered architecture with clear separation of concerns:
84
87
  **Purpose**: Encapsulates configuration settings.
85
88
 
86
89
  **Responsibilities**:
87
- - Store provider selection and API key
88
- - Store model override and temperature setting
90
+ - Resolve the provider: explicit argument wins (with a non-fatal stderr warning on a mismatched key prefix); otherwise inferred from the key prefix via `KeyDetector`, defaulting to `:openai`
91
+ - Store API key, model override, temperature, and extra_params
89
92
  - Validate API key presence and provider name
90
93
 
91
94
  **Key Methods**:
92
- - `initialize(api_key:, temperature: 0.0, provider: :openai, model: nil)`: Creates configuration
93
- - Raises `ArgumentError` if API key is nil/empty or provider is unknown
95
+ - `initialize(api_key:, temperature: 0.0, provider: nil, model: nil, extra_params: {})`: Creates configuration
96
+ - Raises `ArgumentError` if API key is nil/empty or an explicit provider is unknown
94
97
 
95
98
  **Attributes**:
96
99
  - `api_key`: Provider API key (String)
97
- - `provider`: `:openai`, `:anthropic`, `:gemini`, `:groq`, or `:spacexai` (Symbol)
100
+ - `provider`: `:openai`, `:anthropic`, `:gemini`, `:groq`, `:spacexai`, or `:openrouter` (Symbol)
98
101
  - `model`: Model ID override, or nil for the provider default (String or nil)
99
102
  - `temperature`: Model temperature (Float, 0.0-2.0; not sent to Anthropic)
103
+ - `extra_params`: Provider-native request parameters deep-merged into the payload last (Hash)
104
+
105
+ ### VibeSort::KeyDetector
106
+
107
+ **Purpose**: Best-effort provider inference from API key prefixes (`sk-ant-` → `:anthropic`, `sk-or-` → `:openrouter`, `gsk_` → `:groq`, `AIza`/`AQ.` → `:gemini`, `xai-` → `:spacexai`, `sk-` → `:openai`).
108
+
109
+ Prefixes are conventions, not contracts — providers ship multiple key formats concurrently and new formats appear without notice. Detection is therefore only a soft default and a source of non-fatal warnings, never validation: `detect` returns nil for unrecognized formats, and callers fall back to `:openai`.
100
110
 
101
111
  ### VibeSort::Sorter
102
112
 
@@ -113,6 +123,7 @@ VibeSort follows a layered architecture with clear separation of concerns:
113
123
  **`Providers::Base` responsibilities**:
114
124
  - Hold the shared sorting prompt
115
125
  - Build the HTTP connection with Faraday
126
+ - Deep-merge `config.extra_params` into the adapter payload last (nested hashes merge recursively; arrays and scalars replace)
116
127
  - Parse and validate JSON responses (shared across providers)
117
128
  - Raise `ApiError` on failures
118
129
 
@@ -124,6 +135,7 @@ VibeSort follows a layered architecture with clear separation of concerns:
124
135
  - `Providers::Gemini`: generateContent with JSON response mode, `gemini-2.5-flash`
125
136
  - `Providers::Groq`: OpenAI-compatible (subclasses `Providers::OpenAI`), `llama-3.3-70b-versatile`
126
137
  - `Providers::SpaceXAI`: OpenAI-compatible (subclasses `Providers::OpenAI`), `grok-4`
138
+ - `Providers::OpenRouter`: OpenAI-compatible (subclasses `Providers::OpenAI`), `openai/gpt-4o-mini`, adds OpenRouter's recommended attribution headers
127
139
 
128
140
  ### VibeSort::ApiError
129
141
 
@@ -0,0 +1,134 @@
1
+ # Free-Tier Model Benchmark — 2026-07-12
2
+
3
+ An empirical snapshot of how free LLM tiers handle vibe-sort's sorting contract, gathered
4
+ while designing v0.4.0 (OpenRouter provider, key-format detection, `extra_params` —
5
+ see [v0.4.0_PLAN.md](v0.4.0_PLAN.md)). All requests used free models / free-tier quota only.
6
+
7
+ **TL;DR:** Google's current Gemini flash models all sort correctly through the gem's exact
8
+ payload, with `gemini-3.1-flash-lite` the clear free-tier pick. OpenRouter's `:free` pool was
9
+ too congested on a Saturday afternoon (AEST) to be usable — 14 of 17 tested models failed with
10
+ rate limits or upstream errors, and one burned 10 minutes and 20k tokens to return nothing.
11
+
12
+ ---
13
+
14
+ ## Methodology
15
+
16
+ - **Task:** sort `[42, "banana", 7, "Apple", 3.14, "cherry", 1]`
17
+ → expected `[1, 3.14, 7, 42, "Apple", "banana", "cherry"]`
18
+ - **Payloads:** byte-for-byte what the gem sends — `Providers::OpenAI#build_payload` for
19
+ OpenRouter (system prompt + `temperature: 0.0` + `response_format: {type: "json_object"}`),
20
+ `Providers::Gemini#build_payload` for Gemini (`systemInstruction` +
21
+ `generationConfig.responseMimeType: "application/json"`).
22
+ - **Success criteria:** HTTP 200, content parses as JSON with a `sorted_array` key
23
+ (the gem's contract), array exactly matches the expected sort.
24
+ - **Fallbacks tried on rejection:** OpenRouter — retry without `response_format`;
25
+ Gemini — retry with system prompt folded into the user turn and no JSON mime type.
26
+ - **Pacing/retries:** 3.5–8s between requests; 5xx retried up to 3×; 429 handling varied by
27
+ pass (see Lessons). Plain Ruby `net/http`, no SDKs.
28
+ - **Cost:** $0. OpenRouter restricted to `$0`-priced models; Gemini restricted to
29
+ free-tier models with non-zero quota.
30
+
31
+ ---
32
+
33
+ ## Gemini API (free tier)
34
+
35
+ 7 models tested — every model whose free-tier quota is non-zero.
36
+
37
+ | Model | Outcome | Latency | Tokens in/out (+thinking) | Notes |
38
+ |---|---|---|---|---|
39
+ | gemini-3.1-flash-lite | ✅ correct | **2.08s** | 87/35 (+0) | Fastest; no thinking tokens; best quota (15 RPM / 500 RPD) |
40
+ | gemini-2.5-flash | ✅ correct | 2.66s | 87/49 (+247) | Gem's current `DEFAULT_MODEL` — still works |
41
+ | gemini-3.5-flash | ✅ correct | 5.98s | 87/50 (+418) | Most thinking tokens |
42
+ | gemini-3-flash-preview | ✅ correct | 24.14s | 87/49 (+321) | Correct but slow |
43
+ | gemma-4-26b-a4b-it | ❌ bad JSON | 28.6s | 87/49 (+318) | Ignored JSON mime type; replied with markdown reasoning; 1×500 on the way |
44
+ | gemma-4-31b-it | ❌ bad JSON | 9.8s | 87/30 (+256) | Same failure shape; also 1×500 |
45
+ | gemini-2.5-flash-lite | ❌ HTTP 404 | — | — | "No longer available to new users" (still listed by the models endpoint) |
46
+
47
+ ### Observations
48
+
49
+ - **All four current Gemini-branded flash models pass the gem's contract as-is** — JSON mode
50
+ honored, sort correct, numbers-before-strings and case-sensitivity respected.
51
+ - **Gemma models are incompatible with the gem today:** they accept the request but ignore
52
+ `responseMimeType: "application/json"` and return reasoning-style markdown. They'd need
53
+ `extra_params` surgery plus response post-processing — not worth it.
54
+ - **The 500s are real:** 2 of 7 models needed a 5xx retry on their very first request.
55
+ Any Gemini integration should retry 5xx at least once.
56
+ - **429 retries are a trap on the free tier:** every retry counts against RPM *and* RPD, so a
57
+ retry loop converts one throttled request into three consumed ones. After the daily caps
58
+ (20 RPD on most flash models) tripped, throttling also turned intermittent — consecutive
59
+ requests to the same model went 429, 429, 200. Policy that survives: never auto-retry a
60
+ free-tier 429; skip and move on.
61
+ - An input-length scaling test (5/20/50/100 elements) was attempted but the RPD caps were
62
+ exhausted before it could run. Future work, pending quota reset — only
63
+ `gemini-3.1-flash-lite` (500 RPD) realistically supports this kind of testing for free.
64
+
65
+ ---
66
+
67
+ ## OpenRouter (free models)
68
+
69
+ 26 models on openrouter.ai were $0-priced with text input (of 345 listed). 17 were tested
70
+ before the sweep was aborted; 9 were never reached. Tested on a Saturday afternoon AEST —
71
+ peak congestion for the shared free pool, which colours everything below.
72
+
73
+ | Model | Outcome | Notes |
74
+ |---|---|---|
75
+ | nvidia/nemotron-3-super-120b-a12b:free | ❌ empty content | **600s, 20,673 completion tokens, no answer** — presumably all reasoning |
76
+ | nvidia/nemotron-3.5-content-safety:free | ❌ bad JSON | Replied "User Safety: safe" — it's a classifier, not a chat model |
77
+ | nvidia/nemotron-nano-12b-v2-vl:free | ❌ upstream timeout | "Upstream idle timeout exceeded" after 121s |
78
+ | nvidia/nemotron-3-nano-30b-a3b:free | ❌ upstream error | NVIDIA "ResourceExhausted: worker limit reached" |
79
+ | nvidia/nemotron-3-nano-omni-30b-a3b-reasoning:free | ❌ upstream error | Same |
80
+ | nvidia/nemotron-3-ultra-550b-a55b:free | ❌ upstream error | Same |
81
+ | nvidia/nemotron-nano-9b-v2:free | ⏹ aborted | Stalled the sweep twice with a slow-drip connection (>20 min) |
82
+ | liquid/lfm-2.5-1.2b-instruct:free | ❌ HTTP 502 | Upstream down |
83
+ | liquid/lfm-2.5-1.2b-thinking:free | ❌ HTTP 502 | Upstream down |
84
+ | google/lyria-3-clip-preview | ❌ HTTP 400 | Music model; listed as $0 text-input but not a chat model |
85
+ | google/lyria-3-pro-preview | ❌ HTTP 400 | Same |
86
+ | cognitivecomputations/dolphin-mistral-24b-venice-edition:free | ❌ HTTP 429 | Free pool congested (retried in a 2nd pass ~1h later — still 429) |
87
+ | cohere/north-mini-code:free | ❌ HTTP 429 | Explicit "High demand" RPM-limit message |
88
+ | google/gemma-4-26b-a4b-it:free | ❌ HTTP 429 | Congested both passes |
89
+ | google/gemma-4-31b-it:free | ❌ HTTP 429 | Congested both passes |
90
+ | meta-llama/llama-3.2-3b-instruct:free | ❌ HTTP 429 | Congested both passes |
91
+ | meta-llama/llama-3.3-70b-instruct:free | ❌ HTTP 429 | Congested both passes |
92
+ | nousresearch/hermes-3-llama-3.1-405b:free | ❌ HTTP 429 | Congested both passes |
93
+ | openai/gpt-oss-120b:free, openai/gpt-oss-20b:free, openrouter/free, poolside/laguna-m.1:free, poolside/laguna-xs-2.1:free, qwen/qwen3-coder:free, qwen/qwen3-next-80b-a3b-instruct:free, tencent/hy3:free | ⏸ not tested | Sweep aborted before reaching them (blocked behind the stalling Nemotron) |
94
+
95
+ ### Observations
96
+
97
+ - **Zero of 17 tested free models completed a correct sort** during this window. This is a
98
+ statement about the free pool's availability on a weekend afternoon, not about the models —
99
+ nearly every failure was infrastructure (429/5xx/upstream), not model output.
100
+ - **The transport layer of a vibe-sort OpenRouter adapter is validated**, though: auth,
101
+ routing, the OpenAI-compatible payload, and OpenRouter's error shape
102
+ (`error.message` — including passed-through upstream provider errors) all behaved exactly
103
+ as `Providers::Base#extract_error_message` expects.
104
+ - **Free-model quirks to document:** `$0` pricing does not imply chat-usable (Lyria = music,
105
+ content-safety = classifier); reasoning models can consume enormous token counts and still
106
+ return empty content; per-request wall time is unbounded in practice (slow-drip responses
107
+ blow through per-read timeouts), so an overall deadline is the only safe timeout.
108
+ - Free-model requests are also account-capped on OpenRouter (daily request limits tied to
109
+ credit balance) — fine for smoke tests, unusable for CI.
110
+
111
+ ---
112
+
113
+ ## Implications for vibe-sort v0.4.0
114
+
115
+ 1. **OpenRouter adapter: ship it.** The mechanics are proven even though the free pool was
116
+ congested; with any paid model (or an off-peak free window) it should behave like the
117
+ other OpenAI-compatible adapters. Don't default to a `:free` model — congestion would make
118
+ the gem look broken.
119
+ 2. **Gemini `DEFAULT_MODEL`:** `gemini-2.5-flash` still works, but Google has begun retiring
120
+ 2.5-era models for new users (2.5-flash-lite already 404s). `gemini-3.1-flash-lite` is
121
+ faster here, spends no thinking tokens on this task, and has the widest free quota —
122
+ a strong candidate for the next default.
123
+ 3. **`extra_params` earns its place:** overriding `response_format`, capping `max_tokens` on
124
+ reasoning models, and OpenRouter provider-routing preferences were all wanted during this
125
+ exercise.
126
+ 4. **Never auto-retry 429s against free tiers** in examples/docs; retries multiply quota
127
+ consumption and prolong the lockout.
128
+
129
+ ## Reproduction
130
+
131
+ Standalone Ruby scripts (plain `net/http`, no gem dependencies) sent the exact adapter
132
+ payloads; per-model raw results (latency, token usage, full error messages, returned arrays)
133
+ were captured as JSON. API keys were session-scoped test keys, not committed. Numbers above
134
+ are single-shot measurements on free tiers — directional, not statistical.
@@ -10,20 +10,34 @@ module VibeSort
10
10
  # @param api_key [String] API key for the selected provider
11
11
  # @param temperature [Float] Temperature for the model (default: 0.0).
12
12
  # Ignored by providers whose current models do not accept it (Anthropic).
13
- # @param provider [Symbol, String] LLM provider: :openai (default), :anthropic, or :gemini
13
+ # @param provider [Symbol, String, nil] LLM provider: :openai, :anthropic, :gemini,
14
+ # :groq, :spacexai, or :openrouter. When omitted, the provider is inferred from
15
+ # the api_key prefix (e.g. "sk-ant-..." routes to Anthropic), falling back to
16
+ # :openai for unrecognized key formats.
14
17
  # @param model [String, nil] Model ID override (nil uses the provider's default)
18
+ # @param extra_params [Hash] Provider-native request parameters deep-merged into
19
+ # the request payload last, so they can override anything the adapter builds
15
20
  # @raise [ArgumentError] if api_key or provider is invalid
16
21
  #
17
- # @example OpenAI (default provider)
18
- # client = VibeSort::Client.new(api_key: ENV['OPENAI_API_KEY'])
22
+ # @example Provider inferred from the key prefix
23
+ # client = VibeSort::Client.new(api_key: ENV['ANTHROPIC_API_KEY']) # sk-ant-... => :anthropic
19
24
  #
20
- # @example Anthropic Claude
21
- # client = VibeSort::Client.new(provider: :anthropic, api_key: ENV['ANTHROPIC_API_KEY'])
25
+ # @example OpenAI (explicit)
26
+ # client = VibeSort::Client.new(provider: :openai, api_key: ENV['OPENAI_API_KEY'])
22
27
  #
23
28
  # @example Google Gemini with a custom model
24
29
  # client = VibeSort::Client.new(provider: :gemini, api_key: ENV['GEMINI_API_KEY'], model: 'gemini-2.5-pro')
25
- def initialize(api_key:, temperature: 0.0, provider: :openai, model: nil)
26
- @config = Configuration.new(api_key: api_key, temperature: temperature, provider: provider, model: model)
30
+ #
31
+ # @example OpenRouter with extra request parameters
32
+ # client = VibeSort::Client.new(
33
+ # provider: :openrouter,
34
+ # api_key: ENV['OPENROUTER_API_KEY'],
35
+ # model: 'meta-llama/llama-3.3-70b-instruct',
36
+ # extra_params: { max_tokens: 200 }
37
+ # )
38
+ def initialize(api_key:, temperature: 0.0, provider: nil, model: nil, extra_params: {})
39
+ @config = Configuration.new(api_key: api_key, temperature: temperature, provider: provider, model: model,
40
+ extra_params: extra_params)
27
41
  end
28
42
 
29
43
  # Sort an array of numbers and/or strings using the configured provider's API
@@ -53,7 +67,7 @@ module VibeSort
53
67
  # @example API error
54
68
  # result = client.sort([1, 2, 3]) # with invalid API key
55
69
  # #=> { success: false, sorted_array: [], error: "OpenAI API error: Invalid API key" }
56
- # # The error prefix names the configured provider (OpenAI, Anthropic, or Gemini)
70
+ # # The error prefix names the configured provider (OpenAI, Anthropic, Gemini, ...)
57
71
  def sort(array)
58
72
  # Validate input
59
73
  unless valid_input?(array)
@@ -2,29 +2,53 @@
2
2
 
3
3
  module VibeSort
4
4
  # Configuration class for VibeSort
5
- # Holds provider, API key, model, and temperature settings
5
+ # Holds provider, API key, model, temperature, and extra request parameters
6
6
  class Configuration
7
- PROVIDERS = %i[openai anthropic gemini groq spacexai].freeze
7
+ PROVIDERS = %i[openai anthropic gemini groq spacexai openrouter].freeze
8
8
 
9
- attr_reader :api_key, :temperature, :provider, :model
9
+ attr_reader :api_key, :temperature, :provider, :model, :extra_params
10
10
 
11
11
  # Initialize a new Configuration
12
12
  #
13
13
  # @param api_key [String] API key for the selected provider
14
14
  # @param temperature [Float] Temperature for the model (0.0 to 2.0).
15
15
  # Ignored by providers whose current models do not accept it (Anthropic).
16
- # @param provider [Symbol, String] LLM provider: :openai (default), :anthropic, :gemini, :groq, or :spacexai
16
+ # @param provider [Symbol, String, nil] LLM provider: :openai, :anthropic, :gemini,
17
+ # :groq, :spacexai, or :openrouter. When nil (default), the provider is inferred
18
+ # from the api_key prefix, falling back to :openai for unrecognized keys.
17
19
  # @param model [String, nil] Model ID override (nil uses the provider's default)
20
+ # @param extra_params [Hash] Provider-native request parameters deep-merged into
21
+ # the payload last, so they can override anything (e.g. { max_tokens: 100 })
18
22
  # @raise [ArgumentError] if api_key is nil or empty, or provider is unknown
19
- def initialize(api_key:, temperature: 0.0, provider: :openai, model: nil)
23
+ def initialize(api_key:, temperature: 0.0, provider: nil, model: nil, extra_params: {})
20
24
  raise ArgumentError, "API key cannot be nil or empty" if api_key.nil? || api_key.empty?
21
25
 
22
- @provider = provider.to_s.to_sym
23
- raise ArgumentError, "Unknown provider: #{provider.inspect} (supported: #{PROVIDERS.join(", ")})" unless PROVIDERS.include?(@provider)
24
-
26
+ @provider = resolve_provider(provider, api_key)
25
27
  @api_key = api_key
26
28
  @temperature = temperature
27
29
  @model = model
30
+ @extra_params = extra_params
31
+ end
32
+
33
+ private
34
+
35
+ # Resolve the provider: explicit argument wins (with a non-fatal warning if the
36
+ # key prefix suggests a different provider); otherwise infer from the key prefix,
37
+ # defaulting to :openai for unrecognized formats.
38
+ #
39
+ # @param provider [Symbol, String, nil] Explicit provider argument, if any
40
+ # @param api_key [String] API key used for prefix inference
41
+ # @return [Symbol] Resolved provider
42
+ # @raise [ArgumentError] if an explicit provider is unknown
43
+ def resolve_provider(provider, api_key)
44
+ detected = KeyDetector.detect(api_key)
45
+ return detected || :openai if provider.nil?
46
+
47
+ explicit = provider.to_s.to_sym
48
+ raise ArgumentError, "Unknown provider: #{provider.inspect} (supported: #{PROVIDERS.join(", ")})" unless PROVIDERS.include?(explicit)
49
+
50
+ warn "vibe-sort: api_key format suggests :#{detected} but provider is :#{explicit}" if detected && detected != explicit
51
+ explicit
28
52
  end
29
53
  end
30
54
  end
@@ -25,7 +25,7 @@ module VibeSort
25
25
  # @raise [VibeSort::ApiError] if the API request fails
26
26
  def perform(array)
27
27
  response = connection.post do |req|
28
- req.body = build_payload(array)
28
+ req.body = deep_merge(build_payload(array), config.extra_params)
29
29
  end
30
30
 
31
31
  handle_response(response)
@@ -33,6 +33,20 @@ module VibeSort
33
33
 
34
34
  private
35
35
 
36
+ # Merge the user's extra_params into the adapter payload last, so they can
37
+ # override anything (response_format, temperature, even model). Nested
38
+ # hashes merge recursively (e.g. Gemini's generationConfig); arrays and
39
+ # scalars replace.
40
+ #
41
+ # @param base [Hash] Adapter-built payload
42
+ # @param extra [Hash] User-supplied provider-native parameters
43
+ # @return [Hash] Merged payload
44
+ def deep_merge(base, extra)
45
+ base.merge(extra) do |_key, old_val, new_val|
46
+ old_val.is_a?(Hash) && new_val.is_a?(Hash) ? deep_merge(old_val, new_val) : new_val
47
+ end
48
+ end
49
+
36
50
  # Model to use: explicit override from config, or the provider default
37
51
  #
38
52
  # @return [String] Model ID
@@ -8,7 +8,8 @@ module VibeSort
8
8
  anthropic: Providers::Anthropic,
9
9
  gemini: Providers::Gemini,
10
10
  groq: Providers::Groq,
11
- spacexai: Providers::SpaceXAI
11
+ spacexai: Providers::SpaceXAI,
12
+ openrouter: Providers::OpenRouter
12
13
  }.freeze
13
14
 
14
15
  attr_reader :config
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module VibeSort
4
- VERSION = "0.3.1"
4
+ VERSION = "0.4.0"
5
5
  end
data/lib/vibe_sort.rb CHANGED
@@ -5,6 +5,7 @@ require "json"
5
5
 
6
6
  require_relative "vibe_sort/version"
7
7
  require_relative "vibe_sort/error"
8
+ require_relative "vibe_sort/key_detector"
8
9
  require_relative "vibe_sort/configuration"
9
10
  require_relative "vibe_sort/providers/base"
10
11
  require_relative "vibe_sort/providers/openai"
@@ -12,6 +13,7 @@ require_relative "vibe_sort/providers/anthropic"
12
13
  require_relative "vibe_sort/providers/gemini"
13
14
  require_relative "vibe_sort/providers/groq"
14
15
  require_relative "vibe_sort/providers/space_x_ai"
16
+ require_relative "vibe_sort/providers/open_router"
15
17
  require_relative "vibe_sort/sorter"
16
18
  require_relative "vibe_sort/client"
17
19
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: vibe-sort
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.1
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chayut Orapinpatipat
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-07-11 00:00:00.000000000 Z
11
+ date: 2026-07-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: faraday
@@ -26,8 +26,8 @@ dependencies:
26
26
  version: '2.0'
27
27
  description: A proof-of-concept Ruby gem that sorts arrays by leveraging LLM APIs
28
28
  (OpenAI Chat Completions, Anthropic Messages, Google Gemini, and the OpenAI-compatible
29
- Groq and SpaceXAI Grok APIs). Demonstrates how AI can be used for computational
30
- tasks.
29
+ Groq, SpaceXAI Grok, and OpenRouter APIs). Infers the provider from the API key
30
+ format. Demonstrates how AI can be used for computational tasks.
31
31
  email:
32
32
  - chayut_o@hotmail.com
33
33
  executables: []
@@ -45,6 +45,7 @@ files:
45
45
  - docs/api_reference.md
46
46
  - docs/architecture.md
47
47
  - docs/development.md
48
+ - docs/free_model_benchmark_2026-07-12.md
48
49
  - docs/v0.2.0_UPDATE.md
49
50
  - lib/vibe/sort.rb
50
51
  - lib/vibe_sort.rb
@@ -88,5 +89,5 @@ rubygems_version: 3.5.22
88
89
  signing_key:
89
90
  specification_version: 4
90
91
  summary: AI-powered array sorting using OpenAI, Anthropic Claude, Google Gemini, Groq,
91
- or SpaceXAI Grok
92
+ SpaceXAI Grok, or OpenRouter
92
93
  test_files: []