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 +4 -4
- data/CHANGELOG.md +18 -0
- data/README.md +71 -15
- data/docs/api_reference.md +66 -16
- data/docs/architecture.md +21 -9
- data/docs/free_model_benchmark_2026-07-12.md +134 -0
- data/lib/vibe_sort/client.rb +22 -8
- data/lib/vibe_sort/configuration.rb +32 -8
- data/lib/vibe_sort/providers/base.rb +15 -1
- data/lib/vibe_sort/sorter.rb +2 -1
- data/lib/vibe_sort/version.rb +1 -1
- data/lib/vibe_sort.rb +2 -0
- metadata +6 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 15fa0d0262e797d5c29705f56902c66beb746d438505cd6fbace4a999b6c780a
|
|
4
|
+
data.tar.gz: 4f407bf5defba0274c43a352b2274899eea9f1579c82b1eae03663ed2ec5cf6f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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,
|
|
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
|
|
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
|
[](https://badge.fury.io/rb/vibe-sort)
|
|
8
8
|
[](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,
|
|
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**:
|
|
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
|
|
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
|
-
|
|
125
|
+
Any supported provider can be selected with the `provider:` option:
|
|
125
126
|
|
|
126
127
|
```ruby
|
|
127
|
-
# OpenAI
|
|
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'
|
|
301
|
-
export ANTHROPIC_API_KEY='your-api-key-here'
|
|
302
|
-
export GEMINI_API_KEY='your-api-key-here'
|
|
303
|
-
export GROQ_API_KEY='your-api-key-here'
|
|
304
|
-
export XAI_API_KEY='your-api-key-here'
|
|
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/),
|
|
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
|
---
|
data/docs/api_reference.md
CHANGED
|
@@ -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:
|
|
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
|
|
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
|
|
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
|
-
#
|
|
35
|
-
client = VibeSort::Client.new(api_key: ENV['
|
|
37
|
+
# Provider inferred from the key prefix (sk-ant-... => :anthropic)
|
|
38
|
+
client = VibeSort::Client.new(api_key: ENV['ANTHROPIC_API_KEY'])
|
|
36
39
|
|
|
37
|
-
#
|
|
38
|
-
client = VibeSort::Client.new(provider: :
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
525
|
+
Current version: `0.4.0`
|
|
476
526
|
|
|
477
527
|
```ruby
|
|
478
528
|
VibeSort::VERSION
|
|
479
|
-
# => "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:
|
|
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
|
-
-
|
|
88
|
-
- Store model override and
|
|
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:
|
|
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 `:
|
|
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.
|
data/lib/vibe_sort/client.rb
CHANGED
|
@@ -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
|
|
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
|
|
18
|
-
# client = VibeSort::Client.new(api_key: ENV['
|
|
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
|
|
21
|
-
# client = VibeSort::Client.new(provider: :
|
|
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
|
-
|
|
26
|
-
|
|
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,
|
|
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
|
|
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
|
|
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:
|
|
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
|
|
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
|
data/lib/vibe_sort/sorter.rb
CHANGED
data/lib/vibe_sort/version.rb
CHANGED
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.
|
|
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
|
+
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
|
|
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
|
-
|
|
92
|
+
SpaceXAI Grok, or OpenRouter
|
|
92
93
|
test_files: []
|