vibe-sort 0.2.0 → 0.3.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 +25 -0
- data/README.md +59 -10
- data/docs/api_reference.md +68 -28
- data/docs/architecture.md +64 -46
- data/lib/vibe/sort.rb +4 -8
- data/lib/vibe_sort/client.rb +17 -7
- data/lib/vibe_sort/configuration.rb +15 -6
- data/lib/vibe_sort/providers/anthropic.rb +68 -0
- data/lib/vibe_sort/providers/base.rb +147 -0
- data/lib/vibe_sort/providers/gemini.rb +43 -0
- data/lib/vibe_sort/providers/groq.rb +21 -0
- data/lib/vibe_sort/providers/openai.rb +41 -0
- data/lib/vibe_sort/providers/space_x_ai.rb +21 -0
- data/lib/vibe_sort/sorter.rb +10 -103
- data/lib/vibe_sort/version.rb +1 -1
- data/lib/vibe_sort.rb +6 -0
- metadata +13 -8
- data/lib/vibe/sort/version.rb +0 -7
- data/sig/vibe/sort.rbs +0 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b152ab9be69894b49e7a1324253a4e0a4fe3d5a15ec61f50e8b67b3bd424e8ef
|
|
4
|
+
data.tar.gz: 7818eebe77421199fefeba102c3986da5e5665155cb7a916500377515a16b505
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ce32c1524e7afaa485bf600e082fa1bceed852acb617570635c35c2a299a9472f5d8f08df883e76df6e047e1ca1c3e8ca6d3d5d492e8a7bbf104c6d5fb152cfe
|
|
7
|
+
data.tar.gz: 5b95ce876ce4a67bba1662344b2c46c05ff7d6328708458d571e60f3b2e6d347c7ddb442dfd97103c41699f3d7941d4e901894bb8f9fb29d9c70ab1a4248234a
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,31 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.3.0] - 2026-07-10
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **Multi-Provider Support**: Sort via OpenAI, Anthropic Claude, Google Gemini, Groq, or SpaceXAI (xAI) Grok
|
|
15
|
+
- `provider:` option on `VibeSort::Client.new` (`:openai` default, `:anthropic`, `:gemini`, `:groq`, `:spacexai`)
|
|
16
|
+
- `model:` option to override each provider's default model
|
|
17
|
+
- `VibeSort::Providers::Base` adapter layer with per-provider subclasses
|
|
18
|
+
- Anthropic adapter uses the Messages API with structured outputs (JSON schema), guaranteeing the response shape
|
|
19
|
+
- Gemini adapter uses `generateContent` with JSON response mode
|
|
20
|
+
- Groq and SpaceXAI adapters reuse the OpenAI-compatible Chat Completions wire format
|
|
21
|
+
- Provider-specific test suites backed by WebMock
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
|
|
25
|
+
- `VibeSort::Sorter` is now a thin dispatcher to the configured provider adapter
|
|
26
|
+
- OpenAI default model updated from the deprecated `gpt-3.5-turbo-1106` to `gpt-4o-mini`
|
|
27
|
+
- Error messages are prefixed with the provider name (e.g. "Anthropic API error: ...")
|
|
28
|
+
- `temperature` is not sent to Anthropic (current Claude models reject the parameter)
|
|
29
|
+
|
|
30
|
+
### Notes
|
|
31
|
+
|
|
32
|
+
- Fully backward compatible: `VibeSort::Client.new(api_key: ...)` still defaults to OpenAI
|
|
33
|
+
- Still zero runtime dependencies beyond Faraday — all providers are called over plain HTTPS
|
|
34
|
+
|
|
10
35
|
## [0.2.0] - 2025-10-16
|
|
11
36
|
|
|
12
37
|
### Added
|
data/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# 🌀 VibeSort
|
|
2
2
|
|
|
3
|
-
> **AI-powered array sorting using OpenAI
|
|
3
|
+
> **AI-powered array sorting using OpenAI, Anthropic Claude, Google Gemini, Groq, or SpaceXAI Grok**
|
|
4
4
|
|
|
5
|
-
VibeSort is a proof-of-concept Ruby gem that demonstrates sorting
|
|
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.
|
|
6
6
|
|
|
7
7
|
[](https://badge.fury.io/rb/vibe-sort)
|
|
8
8
|
[](https://opensource.org/licenses/MIT)
|
|
@@ -13,9 +13,10 @@ 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
|
|
16
|
+
- 🤖 **AI-Powered Sorting**: Uses OpenAI, Anthropic Claude, Google Gemini, Groq, or SpaceXAI Grok models to sort arrays
|
|
17
|
+
- 🔌 **Multi-Provider**: Switch providers with a single `provider:` option — no extra dependencies
|
|
17
18
|
- 🎯 **Simple Interface**: Clean, intuitive API with a single `sort` method
|
|
18
|
-
- 🔧 **Configurable**: Supports custom temperature settings
|
|
19
|
+
- 🔧 **Configurable**: Supports custom model and temperature settings
|
|
19
20
|
- 🛡️ **Error Handling**: Comprehensive error handling with clear error messages
|
|
20
21
|
- 📊 **Structured Output**: Uses JSON mode for reliable, parsable responses
|
|
21
22
|
- 🔍 **Type Validation**: Validates input and output to ensure data integrity
|
|
@@ -25,7 +26,7 @@ This is a **proof-of-concept** and educational project. It is not intended for p
|
|
|
25
26
|
|
|
26
27
|
- Ruby **3.0** or newer (MRI)
|
|
27
28
|
- Bundler **2.0+**
|
|
28
|
-
- An
|
|
29
|
+
- An API key for at least one supported provider (OpenAI, Anthropic, or Google Gemini)
|
|
29
30
|
- Internet connectivity (each sort performs a remote API call)
|
|
30
31
|
|
|
31
32
|
## 📦 Installation
|
|
@@ -118,6 +119,46 @@ if result[:success]
|
|
|
118
119
|
end
|
|
119
120
|
```
|
|
120
121
|
|
|
122
|
+
### Choosing a Provider
|
|
123
|
+
|
|
124
|
+
VibeSort defaults to OpenAI, but any supported provider can be selected with the `provider:` option:
|
|
125
|
+
|
|
126
|
+
```ruby
|
|
127
|
+
# OpenAI (default)
|
|
128
|
+
client = VibeSort::Client.new(api_key: ENV['OPENAI_API_KEY'])
|
|
129
|
+
|
|
130
|
+
# Anthropic Claude
|
|
131
|
+
client = VibeSort::Client.new(provider: :anthropic, api_key: ENV['ANTHROPIC_API_KEY'])
|
|
132
|
+
|
|
133
|
+
# Google Gemini
|
|
134
|
+
client = VibeSort::Client.new(provider: :gemini, api_key: ENV['GEMINI_API_KEY'])
|
|
135
|
+
|
|
136
|
+
# Groq (fast open-model inference)
|
|
137
|
+
client = VibeSort::Client.new(provider: :groq, api_key: ENV['GROQ_API_KEY'])
|
|
138
|
+
|
|
139
|
+
# SpaceXAI (formerly xAI) Grok
|
|
140
|
+
client = VibeSort::Client.new(provider: :spacexai, api_key: ENV['XAI_API_KEY'])
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
| Provider | API | Default model | Override with `model:` |
|
|
144
|
+
|---|---|---|---|
|
|
145
|
+
| `:openai` | Chat Completions | `gpt-4o-mini` | any chat model |
|
|
146
|
+
| `:anthropic` | Messages | `claude-opus-4-8` | e.g. `claude-haiku-4-5` for cheaper sorts |
|
|
147
|
+
| `:gemini` | generateContent | `gemini-2.5-flash` | e.g. `gemini-2.5-pro` |
|
|
148
|
+
| `:groq` | Chat Completions (OpenAI-compatible) | `llama-3.3-70b-versatile` | any Groq-hosted model |
|
|
149
|
+
| `:spacexai` | Chat Completions (OpenAI-compatible) | `grok-4` | e.g. `grok-4.5` |
|
|
150
|
+
|
|
151
|
+
> **Groq ≠ Grok**: Groq is the independent fast-inference company (`api.groq.com`); Grok is SpaceXAI's model (`api.x.ai`). They are unrelated — VibeSort supports both.
|
|
152
|
+
|
|
153
|
+
```ruby
|
|
154
|
+
# Pick a specific model
|
|
155
|
+
client = VibeSort::Client.new(
|
|
156
|
+
provider: :anthropic,
|
|
157
|
+
api_key: ENV['ANTHROPIC_API_KEY'],
|
|
158
|
+
model: 'claude-haiku-4-5'
|
|
159
|
+
)
|
|
160
|
+
```
|
|
161
|
+
|
|
121
162
|
### Advanced Configuration
|
|
122
163
|
|
|
123
164
|
You can customize the model's behavior using the `temperature` parameter:
|
|
@@ -136,6 +177,8 @@ creative_client = VibeSort::Client.new(
|
|
|
136
177
|
)
|
|
137
178
|
```
|
|
138
179
|
|
|
180
|
+
> **Note**: current Anthropic Claude models no longer accept a `temperature` parameter, so it is ignored when `provider: :anthropic` is used.
|
|
181
|
+
|
|
139
182
|
### Error Handling
|
|
140
183
|
|
|
141
184
|
VibeSort provides detailed error information:
|
|
@@ -181,8 +224,10 @@ The `sort` method always returns a hash with the following structure:
|
|
|
181
224
|
VibeSort follows a clean, modular architecture:
|
|
182
225
|
|
|
183
226
|
- **`VibeSort::Client`**: Public interface for users
|
|
184
|
-
- **`VibeSort::Configuration`**: Manages API key and settings
|
|
185
|
-
- **`VibeSort::Sorter`**:
|
|
227
|
+
- **`VibeSort::Configuration`**: Manages provider, API key, model, and settings
|
|
228
|
+
- **`VibeSort::Sorter`**: Dispatches to the configured provider adapter
|
|
229
|
+
- **`VibeSort::Providers::Base`**: Shared prompt, HTTP plumbing (Faraday), and response validation
|
|
230
|
+
- **`VibeSort::Providers::{OpenAI, Anthropic, Gemini}`**: Provider-specific request/response mapping
|
|
186
231
|
- **`VibeSort::ApiError`**: Custom exception for API-related errors
|
|
187
232
|
|
|
188
233
|
See the [Architecture Documentation](docs/architecture.md) for more details.
|
|
@@ -249,10 +294,14 @@ Traditional sorting (e.g., Ruby's `Array#sort`) is:
|
|
|
249
294
|
|
|
250
295
|
## 🔑 Environment Variables
|
|
251
296
|
|
|
252
|
-
Set
|
|
297
|
+
Set the API key for your chosen provider as an environment variable:
|
|
253
298
|
|
|
254
299
|
```bash
|
|
255
|
-
export OPENAI_API_KEY='your-api-key-here'
|
|
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
|
|
256
305
|
```
|
|
257
306
|
|
|
258
307
|
Or use a `.env` file with the `dotenv` gem:
|
|
@@ -273,7 +322,7 @@ The gem is available as open source under the terms of the [MIT License](https:/
|
|
|
273
322
|
## 🙏 Acknowledgments
|
|
274
323
|
|
|
275
324
|
- Built with [Faraday](https://lostisland.github.io/faraday/) for HTTP requests
|
|
276
|
-
- Powered by [OpenAI](https://openai.com/)
|
|
325
|
+
- Powered by [OpenAI](https://openai.com/), [Anthropic](https://www.anthropic.com/), and [Google Gemini](https://ai.google.dev/) models
|
|
277
326
|
- Inspired by the absurdity and creativity of the AI era
|
|
278
327
|
|
|
279
328
|
---
|
data/docs/api_reference.md
CHANGED
|
@@ -6,30 +6,40 @@ The main public interface for the VibeSort gem.
|
|
|
6
6
|
|
|
7
7
|
### Class Methods
|
|
8
8
|
|
|
9
|
-
#### `new(api_key:, temperature: 0.0)`
|
|
9
|
+
#### `new(api_key:, temperature: 0.0, provider: :openai, model: nil)`
|
|
10
10
|
|
|
11
11
|
Creates a new VibeSort client instance.
|
|
12
12
|
|
|
13
13
|
**Parameters:**
|
|
14
14
|
|
|
15
|
-
- `api_key` (String, required):
|
|
15
|
+
- `api_key` (String, required): API key for the selected provider
|
|
16
16
|
- `temperature` (Float, optional): Model temperature setting (default: 0.0)
|
|
17
17
|
- Range: 0.0 to 2.0
|
|
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
|
+
- 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`
|
|
23
|
+
- `model` (String, optional): Model ID override (default: the provider's default model)
|
|
20
24
|
|
|
21
25
|
**Returns:** `VibeSort::Client` instance
|
|
22
26
|
|
|
23
27
|
**Raises:**
|
|
24
28
|
|
|
25
|
-
- `ArgumentError`: If api_key is nil or empty
|
|
29
|
+
- `ArgumentError`: If api_key is nil or empty, or the provider is unknown
|
|
26
30
|
|
|
27
31
|
**Example:**
|
|
28
32
|
|
|
29
33
|
```ruby
|
|
30
|
-
# Basic initialization
|
|
34
|
+
# Basic initialization (OpenAI, default provider)
|
|
31
35
|
client = VibeSort::Client.new(api_key: ENV['OPENAI_API_KEY'])
|
|
32
36
|
|
|
37
|
+
# Anthropic Claude
|
|
38
|
+
client = VibeSort::Client.new(provider: :anthropic, api_key: ENV['ANTHROPIC_API_KEY'])
|
|
39
|
+
|
|
40
|
+
# Google Gemini with a custom model
|
|
41
|
+
client = VibeSort::Client.new(provider: :gemini, api_key: ENV['GEMINI_API_KEY'], model: 'gemini-2.5-pro')
|
|
42
|
+
|
|
33
43
|
# With custom temperature
|
|
34
44
|
client = VibeSort::Client.new(
|
|
35
45
|
api_key: ENV['OPENAI_API_KEY'],
|
|
@@ -41,7 +51,7 @@ client = VibeSort::Client.new(
|
|
|
41
51
|
|
|
42
52
|
#### `sort(array)`
|
|
43
53
|
|
|
44
|
-
Sorts an array of numbers and/or strings using
|
|
54
|
+
Sorts an array of numbers and/or strings using the configured provider's API.
|
|
45
55
|
|
|
46
56
|
**Parameters:**
|
|
47
57
|
|
|
@@ -100,25 +110,28 @@ Configuration object for VibeSort. Usually not used directly by end users.
|
|
|
100
110
|
|
|
101
111
|
### Class Methods
|
|
102
112
|
|
|
103
|
-
#### `new(api_key:, temperature: 0.0)`
|
|
113
|
+
#### `new(api_key:, temperature: 0.0, provider: :openai, model: nil)`
|
|
104
114
|
|
|
105
115
|
Creates a new configuration object.
|
|
106
116
|
|
|
107
117
|
**Parameters:**
|
|
108
118
|
|
|
109
|
-
- `api_key` (String, required):
|
|
119
|
+
- `api_key` (String, required): API key for the selected provider
|
|
110
120
|
- `temperature` (Float, optional): Model temperature (default: 0.0)
|
|
121
|
+
- `provider` (Symbol or String, optional): `:openai` (default), `:anthropic`, `:gemini`, `:groq`, or `:spacexai`
|
|
122
|
+
- `model` (String, optional): Model ID override (nil uses the provider's default)
|
|
111
123
|
|
|
112
124
|
**Raises:**
|
|
113
125
|
|
|
114
|
-
- `ArgumentError`: If api_key is nil or empty
|
|
126
|
+
- `ArgumentError`: If api_key is nil or empty, or the provider is unknown
|
|
115
127
|
|
|
116
128
|
**Example:**
|
|
117
129
|
|
|
118
130
|
```ruby
|
|
119
131
|
config = VibeSort::Configuration.new(
|
|
120
132
|
api_key: "sk-...",
|
|
121
|
-
|
|
133
|
+
provider: :anthropic,
|
|
134
|
+
model: "claude-haiku-4-5"
|
|
122
135
|
)
|
|
123
136
|
```
|
|
124
137
|
|
|
@@ -136,11 +149,23 @@ Returns the configured temperature setting (read-only).
|
|
|
136
149
|
|
|
137
150
|
**Returns:** Float
|
|
138
151
|
|
|
152
|
+
#### `provider`
|
|
153
|
+
|
|
154
|
+
Returns the configured provider (read-only).
|
|
155
|
+
|
|
156
|
+
**Returns:** Symbol
|
|
157
|
+
|
|
158
|
+
#### `model`
|
|
159
|
+
|
|
160
|
+
Returns the configured model override, or nil when using the provider default (read-only).
|
|
161
|
+
|
|
162
|
+
**Returns:** String or nil
|
|
163
|
+
|
|
139
164
|
---
|
|
140
165
|
|
|
141
166
|
## VibeSort::Sorter
|
|
142
167
|
|
|
143
|
-
Internal class that
|
|
168
|
+
Internal class that dispatches the sorting operation to the configured provider adapter. Not intended for direct use.
|
|
144
169
|
|
|
145
170
|
### Class Methods
|
|
146
171
|
|
|
@@ -156,7 +181,7 @@ Creates a new sorter instance.
|
|
|
156
181
|
|
|
157
182
|
#### `perform(array)`
|
|
158
183
|
|
|
159
|
-
Performs the sorting operation via
|
|
184
|
+
Performs the sorting operation via the configured provider's API.
|
|
160
185
|
|
|
161
186
|
**Parameters:**
|
|
162
187
|
|
|
@@ -170,17 +195,30 @@ Performs the sorting operation via OpenAI API.
|
|
|
170
195
|
|
|
171
196
|
### Constants
|
|
172
197
|
|
|
173
|
-
#### `
|
|
198
|
+
#### `PROVIDER_CLASSES`
|
|
199
|
+
|
|
200
|
+
Maps provider symbols to adapter classes.
|
|
201
|
+
|
|
202
|
+
**Value:** `{ openai:, anthropic:, gemini:, groq:, spacexai: }`
|
|
203
|
+
|
|
204
|
+
---
|
|
174
205
|
|
|
175
|
-
|
|
206
|
+
## VibeSort::Providers
|
|
176
207
|
|
|
177
|
-
|
|
208
|
+
Internal provider adapters. `Providers::Base` holds the shared prompt, Faraday HTTP plumbing, and response validation; each subclass maps one provider's request/response wire format.
|
|
178
209
|
|
|
179
|
-
|
|
210
|
+
| Adapter | Endpoint | `DEFAULT_MODEL` |
|
|
211
|
+
|---|---|---|
|
|
212
|
+
| `Providers::OpenAI` | `https://api.openai.com/v1/chat/completions` | `gpt-4o-mini` |
|
|
213
|
+
| `Providers::Anthropic` | `https://api.anthropic.com/v1/messages` | `claude-opus-4-8` |
|
|
214
|
+
| `Providers::Gemini` | `https://generativelanguage.googleapis.com/v1beta/models/{model}:generateContent` | `gemini-2.5-flash` |
|
|
215
|
+
| `Providers::Groq` | `https://api.groq.com/openai/v1/chat/completions` | `llama-3.3-70b-versatile` |
|
|
216
|
+
| `Providers::SpaceXAI` | `https://api.x.ai/v1/chat/completions` | `grok-4` |
|
|
180
217
|
|
|
181
|
-
|
|
218
|
+
Notes:
|
|
182
219
|
|
|
183
|
-
|
|
220
|
+
- 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)
|
|
184
222
|
|
|
185
223
|
---
|
|
186
224
|
|
|
@@ -269,11 +307,13 @@ All sorting operations return a consistent hash structure:
|
|
|
269
307
|
|
|
270
308
|
### API Errors
|
|
271
309
|
|
|
310
|
+
The `{Provider}` prefix names the configured provider (OpenAI, Anthropic, Gemini, Groq, or SpaceXAI):
|
|
311
|
+
|
|
272
312
|
| Error Message Pattern | Cause | Solution |
|
|
273
313
|
|----------------------|-------|----------|
|
|
274
|
-
| "
|
|
275
|
-
| "
|
|
276
|
-
| "
|
|
314
|
+
| "{Provider} API error: Invalid API key" | Invalid or missing API key | Check API key |
|
|
315
|
+
| "{Provider} API error: Rate limit exceeded" | Too many requests | Wait and retry |
|
|
316
|
+
| "{Provider} API error: HTTP {status}" | HTTP error | Check API status |
|
|
277
317
|
|
|
278
318
|
### Response Parsing Errors
|
|
279
319
|
|
|
@@ -412,14 +452,14 @@ results = threads.map(&:value)
|
|
|
412
452
|
|
|
413
453
|
## Environment Variables
|
|
414
454
|
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
Your OpenAI API key. Required for all operations.
|
|
418
|
-
|
|
419
|
-
**Setup:**
|
|
455
|
+
Set the API key for whichever provider you use:
|
|
420
456
|
|
|
421
457
|
```bash
|
|
422
|
-
export OPENAI_API_KEY='sk-your-key-here'
|
|
458
|
+
export OPENAI_API_KEY='sk-your-key-here' # provider: :openai (default)
|
|
459
|
+
export ANTHROPIC_API_KEY='your-key-here' # provider: :anthropic
|
|
460
|
+
export GEMINI_API_KEY='your-key-here' # provider: :gemini
|
|
461
|
+
export GROQ_API_KEY='your-key-here' # provider: :groq
|
|
462
|
+
export XAI_API_KEY='your-key-here' # provider: :spacexai
|
|
423
463
|
```
|
|
424
464
|
|
|
425
465
|
**Usage:**
|
|
@@ -432,9 +472,9 @@ client = VibeSort::Client.new(api_key: ENV['OPENAI_API_KEY'])
|
|
|
432
472
|
|
|
433
473
|
## Version
|
|
434
474
|
|
|
435
|
-
Current version: `0.
|
|
475
|
+
Current version: `0.3.0`
|
|
436
476
|
|
|
437
477
|
```ruby
|
|
438
478
|
VibeSort::VERSION
|
|
439
|
-
# => "0.
|
|
479
|
+
# => "0.3.0"
|
|
440
480
|
```
|
data/docs/architecture.md
CHANGED
|
@@ -20,24 +20,36 @@ VibeSort follows a layered architecture with clear separation of concerns:
|
|
|
20
20
|
▼
|
|
21
21
|
┌─────────────────────────────────────────┐
|
|
22
22
|
│ VibeSort::Configuration │
|
|
23
|
+
│ - Provider selection │
|
|
23
24
|
│ - API key management │
|
|
25
|
+
│ - Model override │
|
|
24
26
|
│ - Temperature settings │
|
|
25
27
|
└─────────────────┬───────────────────────┘
|
|
26
28
|
│
|
|
27
29
|
▼
|
|
28
30
|
┌─────────────────────────────────────────┐
|
|
29
|
-
│ VibeSort::Sorter
|
|
31
|
+
│ VibeSort::Sorter (dispatcher) │
|
|
32
|
+
│ - Picks the provider adapter │
|
|
33
|
+
└─────────────────┬───────────────────────┘
|
|
34
|
+
│
|
|
35
|
+
▼
|
|
36
|
+
┌─────────────────────────────────────────┐
|
|
37
|
+
│ VibeSort::Providers::Base │
|
|
38
|
+
│ - Shared prompt │
|
|
30
39
|
│ - HTTP client (Faraday) │
|
|
31
|
-
│ -
|
|
32
|
-
|
|
33
|
-
│
|
|
40
|
+
│ - Response parsing & validation │
|
|
41
|
+
├─────────────────────────────────────────┤
|
|
42
|
+
│ OpenAI │ Anthropic │ Gemini │ │
|
|
43
|
+
│ Groq │ SpaceXAI │
|
|
44
|
+
│ (request/response wire formats) │
|
|
34
45
|
└─────────────────┬───────────────────────┘
|
|
35
46
|
│
|
|
36
47
|
▼
|
|
37
48
|
┌─────────────────────────────────────────┐
|
|
38
|
-
│
|
|
39
|
-
│
|
|
40
|
-
│
|
|
49
|
+
│ Provider API (HTTPS) │
|
|
50
|
+
│ api.openai.com │ api.anthropic.com │
|
|
51
|
+
│ generativelanguage.googleapis.com │
|
|
52
|
+
│ api.groq.com │ api.x.ai │
|
|
41
53
|
└─────────────────────────────────────────┘
|
|
42
54
|
```
|
|
43
55
|
|
|
@@ -55,7 +67,7 @@ VibeSort follows a layered architecture with clear separation of concerns:
|
|
|
55
67
|
- Return standardized response hashes
|
|
56
68
|
|
|
57
69
|
**Key Methods**:
|
|
58
|
-
- `initialize(api_key:, temperature: 0.0)`: Creates client with configuration
|
|
70
|
+
- `initialize(api_key:, temperature: 0.0, provider: :openai, model: nil)`: Creates client with configuration
|
|
59
71
|
- `sort(array)`: Sorts array and returns result hash
|
|
60
72
|
|
|
61
73
|
**Return Format**:
|
|
@@ -72,41 +84,46 @@ VibeSort follows a layered architecture with clear separation of concerns:
|
|
|
72
84
|
**Purpose**: Encapsulates configuration settings.
|
|
73
85
|
|
|
74
86
|
**Responsibilities**:
|
|
75
|
-
- Store API key
|
|
76
|
-
- Store temperature setting
|
|
77
|
-
- Validate API key presence
|
|
87
|
+
- Store provider selection and API key
|
|
88
|
+
- Store model override and temperature setting
|
|
89
|
+
- Validate API key presence and provider name
|
|
78
90
|
|
|
79
91
|
**Key Methods**:
|
|
80
|
-
- `initialize(api_key:, temperature:)`: Creates configuration
|
|
81
|
-
- Raises `ArgumentError` if API key is nil or
|
|
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
|
|
82
94
|
|
|
83
95
|
**Attributes**:
|
|
84
|
-
- `api_key`:
|
|
85
|
-
- `
|
|
96
|
+
- `api_key`: Provider API key (String)
|
|
97
|
+
- `provider`: `:openai`, `:anthropic`, `:gemini`, `:groq`, or `:spacexai` (Symbol)
|
|
98
|
+
- `model`: Model ID override, or nil for the provider default (String or nil)
|
|
99
|
+
- `temperature`: Model temperature (Float, 0.0-2.0; not sent to Anthropic)
|
|
86
100
|
|
|
87
101
|
### VibeSort::Sorter
|
|
88
102
|
|
|
89
|
-
**Purpose**:
|
|
90
|
-
|
|
91
|
-
**Responsibilities**:
|
|
92
|
-
- Build HTTP connection with Faraday
|
|
93
|
-
- Construct API request payload
|
|
94
|
-
- Send POST request to OpenAI
|
|
95
|
-
- Parse and validate JSON responses
|
|
96
|
-
- Extract sorted array from response
|
|
97
|
-
- Raise `ApiError` on failures
|
|
103
|
+
**Purpose**: Dispatches the sort to the configured provider adapter.
|
|
98
104
|
|
|
99
105
|
**Key Methods**:
|
|
100
106
|
- `initialize(config)`: Creates sorter with configuration
|
|
101
|
-
- `perform(array)`:
|
|
102
|
-
- `build_payload(array)`: Private - constructs request
|
|
103
|
-
- `handle_response(response)`: Private - processes response
|
|
104
|
-
- `parse_sorted_array(response)`: Private - extracts result
|
|
105
|
-
- `validate_sorted_array!(array)`: Private - validates output
|
|
107
|
+
- `perform(array)`: Looks up the adapter in `PROVIDER_CLASSES` and delegates
|
|
106
108
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
109
|
+
### VibeSort::Providers
|
|
110
|
+
|
|
111
|
+
**Purpose**: One adapter per LLM provider, all sharing a common base.
|
|
112
|
+
|
|
113
|
+
**`Providers::Base` responsibilities**:
|
|
114
|
+
- Hold the shared sorting prompt
|
|
115
|
+
- Build the HTTP connection with Faraday
|
|
116
|
+
- Parse and validate JSON responses (shared across providers)
|
|
117
|
+
- Raise `ApiError` on failures
|
|
118
|
+
|
|
119
|
+
**Adapter hooks** (implemented per provider): `provider_name`, `endpoint`, `headers`, `build_payload`, `extract_content`
|
|
120
|
+
|
|
121
|
+
**Adapters and default models**:
|
|
122
|
+
- `Providers::OpenAI` — Chat Completions, `gpt-4o-mini`
|
|
123
|
+
- `Providers::Anthropic` — Messages API with structured outputs (JSON schema), `claude-opus-4-8`
|
|
124
|
+
- `Providers::Gemini` — generateContent with JSON response mode, `gemini-2.5-flash`
|
|
125
|
+
- `Providers::Groq` — OpenAI-compatible (subclasses `Providers::OpenAI`), `llama-3.3-70b-versatile`
|
|
126
|
+
- `Providers::SpaceXAI` — OpenAI-compatible (subclasses `Providers::OpenAI`), `grok-4`
|
|
110
127
|
|
|
111
128
|
### VibeSort::ApiError
|
|
112
129
|
|
|
@@ -132,12 +149,12 @@ VibeSort follows a layered architecture with clear separation of concerns:
|
|
|
132
149
|
|
|
133
150
|
3. **Client creates Sorter**
|
|
134
151
|
- Passes Configuration object
|
|
135
|
-
- Sorter initializes Faraday connection
|
|
152
|
+
- Sorter picks the provider adapter, which initializes a Faraday connection
|
|
136
153
|
|
|
137
|
-
4. **
|
|
154
|
+
4. **Adapter builds the provider-specific request payload** (OpenAI example)
|
|
138
155
|
```json
|
|
139
156
|
{
|
|
140
|
-
"model": "gpt-
|
|
157
|
+
"model": "gpt-4o-mini",
|
|
141
158
|
"temperature": 0.0,
|
|
142
159
|
"response_format": { "type": "json_object" },
|
|
143
160
|
"messages": [
|
|
@@ -153,16 +170,16 @@ VibeSort follows a layered architecture with clear separation of concerns:
|
|
|
153
170
|
}
|
|
154
171
|
```
|
|
155
172
|
|
|
156
|
-
5. **
|
|
157
|
-
- URL: `https://api.openai.com/v1/chat/completions`
|
|
158
|
-
- Headers:
|
|
173
|
+
5. **Adapter sends POST request**
|
|
174
|
+
- URL: the adapter's endpoint (e.g. `https://api.openai.com/v1/chat/completions`)
|
|
175
|
+
- Headers: provider auth (Bearer token, `x-api-key`, or `x-goog-api-key`), Content-Type
|
|
159
176
|
- Body: JSON payload
|
|
160
177
|
|
|
161
|
-
6. **
|
|
178
|
+
6. **Provider processes request**
|
|
162
179
|
- Model analyzes the array
|
|
163
180
|
- Returns JSON with sorted array
|
|
164
181
|
|
|
165
|
-
7. **
|
|
182
|
+
7. **Adapter parses response** (OpenAI example)
|
|
166
183
|
```json
|
|
167
184
|
{
|
|
168
185
|
"choices": [{
|
|
@@ -227,8 +244,7 @@ VibeSort follows a layered architecture with clear separation of concerns:
|
|
|
227
244
|
## Dependencies
|
|
228
245
|
|
|
229
246
|
### Runtime
|
|
230
|
-
- **faraday** (~> 2.0): HTTP client library
|
|
231
|
-
- **faraday-json** (~> 1.0): JSON middleware for Faraday
|
|
247
|
+
- **faraday** (~> 2.0): HTTP client library — the only runtime dependency; all providers are called over plain HTTPS
|
|
232
248
|
|
|
233
249
|
### Development
|
|
234
250
|
- **rspec** (~> 3.0): Testing framework
|
|
@@ -236,8 +252,11 @@ VibeSort follows a layered architecture with clear separation of concerns:
|
|
|
236
252
|
|
|
237
253
|
## Design Decisions
|
|
238
254
|
|
|
239
|
-
### Why JSON Mode?
|
|
240
|
-
OpenAI
|
|
255
|
+
### Why JSON Mode / Structured Outputs?
|
|
256
|
+
Each adapter uses the strongest JSON guarantee its provider offers: OpenAI-compatible providers use JSON mode (`response_format: { type: "json_object" }`), Anthropic uses structured outputs with a JSON schema, and Gemini uses `responseMimeType: "application/json"`. This makes parsing reliable across providers.
|
|
257
|
+
|
|
258
|
+
### Why Raw HTTP Instead of Provider SDKs?
|
|
259
|
+
Three simple JSON POSTs don't justify three SDK dependencies. Keeping everything on Faraday keeps the gem lightweight and the adapters symmetric.
|
|
241
260
|
|
|
242
261
|
### Why Temperature 0.0 by Default?
|
|
243
262
|
Lower temperature (0.0) produces deterministic, consistent results. Sorting should be predictable, not creative!
|
|
@@ -312,8 +331,7 @@ Potential improvements for future versions:
|
|
|
312
331
|
|
|
313
332
|
1. **Batch Sorting**: Sort multiple arrays in one API call
|
|
314
333
|
2. **Caching**: Cache results for identical inputs
|
|
315
|
-
3. **
|
|
316
|
-
4. **Retry Logic**: Automatic retry on transient failures
|
|
334
|
+
3. **Retry Logic**: Automatic retry on transient failures
|
|
317
335
|
5. **Async Support**: Non-blocking API calls with callbacks
|
|
318
336
|
6. **Metrics**: Track API usage, costs, and performance
|
|
319
337
|
7. **Different Sort Orders**: Support descending order
|
data/lib/vibe/sort.rb
CHANGED
|
@@ -1,10 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
class Error < StandardError; end
|
|
8
|
-
# Your code goes here...
|
|
9
|
-
end
|
|
10
|
-
end
|
|
3
|
+
# Compatibility shim: Bundler autorequires "vibe/sort" for a gem named
|
|
4
|
+
# "vibe-sort", so `gem "vibe-sort"` in a Gemfile loads the real library
|
|
5
|
+
# without needing an explicit `require "vibe_sort"`.
|
|
6
|
+
require_relative "../vibe_sort"
|
data/lib/vibe_sort/client.rb
CHANGED
|
@@ -7,17 +7,26 @@ module VibeSort
|
|
|
7
7
|
|
|
8
8
|
# Initialize a new VibeSort client
|
|
9
9
|
#
|
|
10
|
-
# @param api_key [String]
|
|
11
|
-
# @param temperature [Float] Temperature for the model (default: 0.0)
|
|
12
|
-
#
|
|
10
|
+
# @param api_key [String] API key for the selected provider
|
|
11
|
+
# @param temperature [Float] Temperature for the model (default: 0.0).
|
|
12
|
+
# Ignored by providers whose current models do not accept it (Anthropic).
|
|
13
|
+
# @param provider [Symbol, String] LLM provider: :openai (default), :anthropic, or :gemini
|
|
14
|
+
# @param model [String, nil] Model ID override (nil uses the provider's default)
|
|
15
|
+
# @raise [ArgumentError] if api_key or provider is invalid
|
|
13
16
|
#
|
|
14
|
-
# @example
|
|
17
|
+
# @example OpenAI (default provider)
|
|
15
18
|
# client = VibeSort::Client.new(api_key: ENV['OPENAI_API_KEY'])
|
|
16
|
-
|
|
17
|
-
|
|
19
|
+
#
|
|
20
|
+
# @example Anthropic Claude
|
|
21
|
+
# client = VibeSort::Client.new(provider: :anthropic, api_key: ENV['ANTHROPIC_API_KEY'])
|
|
22
|
+
#
|
|
23
|
+
# @example Google Gemini with a custom model
|
|
24
|
+
# 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)
|
|
18
27
|
end
|
|
19
28
|
|
|
20
|
-
# Sort an array of numbers and/or strings using
|
|
29
|
+
# Sort an array of numbers and/or strings using the configured provider's API
|
|
21
30
|
#
|
|
22
31
|
# @param array [Array] Array of numbers and/or strings to sort
|
|
23
32
|
# @return [Hash] Result hash with keys:
|
|
@@ -44,6 +53,7 @@ module VibeSort
|
|
|
44
53
|
# @example API error
|
|
45
54
|
# result = client.sort([1, 2, 3]) # with invalid API key
|
|
46
55
|
# #=> { success: false, sorted_array: [], error: "OpenAI API error: Invalid API key" }
|
|
56
|
+
# # The error prefix names the configured provider (OpenAI, Anthropic, or Gemini)
|
|
47
57
|
def sort(array)
|
|
48
58
|
# Validate input
|
|
49
59
|
unless valid_input?(array)
|
|
@@ -2,20 +2,29 @@
|
|
|
2
2
|
|
|
3
3
|
module VibeSort
|
|
4
4
|
# Configuration class for VibeSort
|
|
5
|
-
# Holds API key and temperature settings
|
|
5
|
+
# Holds provider, API key, model, and temperature settings
|
|
6
6
|
class Configuration
|
|
7
|
-
|
|
7
|
+
PROVIDERS = %i[openai anthropic gemini groq spacexai].freeze
|
|
8
|
+
|
|
9
|
+
attr_reader :api_key, :temperature, :provider, :model
|
|
8
10
|
|
|
9
11
|
# Initialize a new Configuration
|
|
10
12
|
#
|
|
11
|
-
# @param api_key [String]
|
|
12
|
-
# @param temperature [Float] Temperature for the model (0.0 to 2.0)
|
|
13
|
-
#
|
|
14
|
-
|
|
13
|
+
# @param api_key [String] API key for the selected provider
|
|
14
|
+
# @param temperature [Float] Temperature for the model (0.0 to 2.0).
|
|
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
|
|
17
|
+
# @param model [String, nil] Model ID override (nil uses the provider's default)
|
|
18
|
+
# @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)
|
|
15
20
|
raise ArgumentError, "API key cannot be nil or empty" if api_key.nil? || api_key.empty?
|
|
16
21
|
|
|
22
|
+
@provider = provider.to_s.to_sym
|
|
23
|
+
raise ArgumentError, "Unknown provider: #{provider.inspect} (supported: #{PROVIDERS.join(", ")})" unless PROVIDERS.include?(@provider)
|
|
24
|
+
|
|
17
25
|
@api_key = api_key
|
|
18
26
|
@temperature = temperature
|
|
27
|
+
@model = model
|
|
19
28
|
end
|
|
20
29
|
end
|
|
21
30
|
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module VibeSort
|
|
4
|
+
module Providers
|
|
5
|
+
# Adapter for the Anthropic Messages API
|
|
6
|
+
#
|
|
7
|
+
# Uses structured outputs (output_config.format with a JSON schema) so the
|
|
8
|
+
# model is guaranteed to return the { "sorted_array": [...] } shape.
|
|
9
|
+
#
|
|
10
|
+
# Note: current Claude models (Opus 4.7+) reject the temperature parameter,
|
|
11
|
+
# so config.temperature is not forwarded to this provider.
|
|
12
|
+
class Anthropic < Base
|
|
13
|
+
ENDPOINT = "https://api.anthropic.com/v1/messages"
|
|
14
|
+
DEFAULT_MODEL = "claude-opus-4-8"
|
|
15
|
+
API_VERSION = "2023-06-01"
|
|
16
|
+
MAX_TOKENS = 4096
|
|
17
|
+
|
|
18
|
+
OUTPUT_SCHEMA = {
|
|
19
|
+
type: "object",
|
|
20
|
+
properties: {
|
|
21
|
+
sorted_array: {
|
|
22
|
+
type: "array",
|
|
23
|
+
items: { anyOf: [{ type: "number" }, { type: "string" }] }
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
required: ["sorted_array"],
|
|
27
|
+
additionalProperties: false
|
|
28
|
+
}.freeze
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def provider_name
|
|
33
|
+
"Anthropic"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def endpoint
|
|
37
|
+
ENDPOINT
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def headers
|
|
41
|
+
{
|
|
42
|
+
"x-api-key" => config.api_key,
|
|
43
|
+
"anthropic-version" => API_VERSION
|
|
44
|
+
}
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def build_payload(array)
|
|
48
|
+
{
|
|
49
|
+
model: model,
|
|
50
|
+
max_tokens: MAX_TOKENS,
|
|
51
|
+
system: SYSTEM_PROMPT,
|
|
52
|
+
output_config: { format: { type: "json_schema", schema: OUTPUT_SCHEMA } },
|
|
53
|
+
messages: [
|
|
54
|
+
{ role: "user", content: user_prompt(array) }
|
|
55
|
+
]
|
|
56
|
+
}
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def extract_content(response)
|
|
60
|
+
blocks = response.body["content"]
|
|
61
|
+
return nil unless blocks.is_a?(Array)
|
|
62
|
+
|
|
63
|
+
text_block = blocks.find { |block| block.is_a?(Hash) && block["type"] == "text" }
|
|
64
|
+
text_block && text_block["text"]
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module VibeSort
|
|
4
|
+
module Providers
|
|
5
|
+
# Base class for LLM provider adapters.
|
|
6
|
+
#
|
|
7
|
+
# Holds everything that is provider-agnostic: the sorting prompt, the
|
|
8
|
+
# Faraday connection plumbing, and validation of the returned array.
|
|
9
|
+
# Subclasses implement the provider-specific hooks: +provider_name+,
|
|
10
|
+
# +endpoint+, +headers+, +build_payload+, and +extract_content+.
|
|
11
|
+
class Base
|
|
12
|
+
SYSTEM_PROMPT = "You are an assistant that only sorts arrays. The array may contain numbers and strings. Sort the array in ascending order. Follow standard sorting rules: numbers should come before strings, and string sorting should be case-sensitive. Return a JSON object with a single key 'sorted_array' containing the sorted elements."
|
|
13
|
+
|
|
14
|
+
attr_reader :config
|
|
15
|
+
|
|
16
|
+
# @param config [VibeSort::Configuration] Configuration object
|
|
17
|
+
def initialize(config)
|
|
18
|
+
@config = config
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Perform the sorting operation via the provider's API
|
|
22
|
+
#
|
|
23
|
+
# @param array [Array] Array of numbers and/or strings to sort
|
|
24
|
+
# @return [Hash] Result hash with :success and :sorted_array keys
|
|
25
|
+
# @raise [VibeSort::ApiError] if the API request fails
|
|
26
|
+
def perform(array)
|
|
27
|
+
response = connection.post do |req|
|
|
28
|
+
req.body = build_payload(array)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
handle_response(response)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
# Model to use: explicit override from config, or the provider default
|
|
37
|
+
#
|
|
38
|
+
# @return [String] Model ID
|
|
39
|
+
def model
|
|
40
|
+
config.model || self.class::DEFAULT_MODEL
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# @param array [Array] Array to sort
|
|
44
|
+
# @return [String] User prompt sent to the model
|
|
45
|
+
def user_prompt(array)
|
|
46
|
+
"Please sort this array: #{array.to_json}"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# @return [Faraday::Connection] Faraday connection object
|
|
50
|
+
def connection
|
|
51
|
+
@connection ||= Faraday.new(url: endpoint) do |f|
|
|
52
|
+
f.request :json # Encodes request body as JSON
|
|
53
|
+
f.response :json # Decodes response body as JSON
|
|
54
|
+
headers.each { |name, value| f.headers[name] = value }
|
|
55
|
+
f.headers["Content-Type"] = "application/json"
|
|
56
|
+
f.adapter Faraday.default_adapter
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# @param response [Faraday::Response] HTTP response
|
|
61
|
+
# @return [Hash] Result hash
|
|
62
|
+
# @raise [VibeSort::ApiError] if response is invalid or parsing fails
|
|
63
|
+
def handle_response(response)
|
|
64
|
+
unless response.success?
|
|
65
|
+
error_message = extract_error_message(response)
|
|
66
|
+
raise ApiError.new("#{provider_name} API error: #{error_message}", response)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
parse_sorted_array(response)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Extract error message from failed response. Works for all three
|
|
73
|
+
# providers: OpenAI, Anthropic, and Gemini all return an "error"
|
|
74
|
+
# object with a "message" key on failure.
|
|
75
|
+
#
|
|
76
|
+
# @param response [Faraday::Response] HTTP response
|
|
77
|
+
# @return [String] Error message
|
|
78
|
+
def extract_error_message(response)
|
|
79
|
+
return "Unknown error" unless response.body.is_a?(Hash)
|
|
80
|
+
|
|
81
|
+
response.body.dig("error", "message") || "HTTP #{response.status}"
|
|
82
|
+
rescue StandardError
|
|
83
|
+
"HTTP #{response.status}"
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Parse the sorted array from the API response
|
|
87
|
+
#
|
|
88
|
+
# @param response [Faraday::Response] HTTP response
|
|
89
|
+
# @return [Hash] Success result with sorted array
|
|
90
|
+
# @raise [VibeSort::ApiError] if parsing fails or validation fails
|
|
91
|
+
def parse_sorted_array(response)
|
|
92
|
+
content = extract_content(response)
|
|
93
|
+
raise ApiError.new("Invalid response structure", response) if content.nil?
|
|
94
|
+
|
|
95
|
+
parsed_content = JSON.parse(content)
|
|
96
|
+
sorted_array = parsed_content["sorted_array"]
|
|
97
|
+
|
|
98
|
+
validate_sorted_array!(sorted_array)
|
|
99
|
+
|
|
100
|
+
{ success: true, sorted_array: sorted_array }
|
|
101
|
+
rescue JSON::ParserError => e
|
|
102
|
+
raise ApiError.new("Failed to parse JSON response: #{e.message}", response)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Validate that the sorted array is valid
|
|
106
|
+
#
|
|
107
|
+
# @param sorted_array [Object] Parsed sorted array
|
|
108
|
+
# @raise [VibeSort::ApiError] if validation fails
|
|
109
|
+
def validate_sorted_array!(sorted_array)
|
|
110
|
+
raise ApiError, "Response does not contain a valid 'sorted_array'" unless sorted_array.is_a?(Array)
|
|
111
|
+
|
|
112
|
+
return if sorted_array.all? { |item| item.is_a?(Numeric) || item.is_a?(String) }
|
|
113
|
+
|
|
114
|
+
raise ApiError, "Sorted array contains invalid values (must be numbers or strings)"
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# --- Provider-specific hooks ---
|
|
118
|
+
|
|
119
|
+
# @return [String] Human-readable provider name used in error messages
|
|
120
|
+
def provider_name
|
|
121
|
+
raise NotImplementedError
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# @return [String] Full URL of the API endpoint
|
|
125
|
+
def endpoint
|
|
126
|
+
raise NotImplementedError
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# @return [Hash] Provider-specific HTTP headers (auth etc.)
|
|
130
|
+
def headers
|
|
131
|
+
raise NotImplementedError
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# @param array [Array] Array to sort
|
|
135
|
+
# @return [Hash] Provider-specific request payload
|
|
136
|
+
def build_payload(array)
|
|
137
|
+
raise NotImplementedError
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# @param response [Faraday::Response] HTTP response
|
|
141
|
+
# @return [String, nil] Raw JSON text produced by the model
|
|
142
|
+
def extract_content(response)
|
|
143
|
+
raise NotImplementedError
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module VibeSort
|
|
4
|
+
module Providers
|
|
5
|
+
# Adapter for the Google Gemini API (generateContent)
|
|
6
|
+
class Gemini < Base
|
|
7
|
+
BASE_URL = "https://generativelanguage.googleapis.com/v1beta/models"
|
|
8
|
+
DEFAULT_MODEL = "gemini-2.5-flash"
|
|
9
|
+
|
|
10
|
+
private
|
|
11
|
+
|
|
12
|
+
def provider_name
|
|
13
|
+
"Gemini"
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# The Gemini endpoint embeds the model ID in the URL path
|
|
17
|
+
def endpoint
|
|
18
|
+
"#{BASE_URL}/#{model}:generateContent"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def headers
|
|
22
|
+
{ "x-goog-api-key" => config.api_key }
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def build_payload(array)
|
|
26
|
+
{
|
|
27
|
+
systemInstruction: { parts: [{ text: SYSTEM_PROMPT }] },
|
|
28
|
+
contents: [
|
|
29
|
+
{ role: "user", parts: [{ text: user_prompt(array) }] }
|
|
30
|
+
],
|
|
31
|
+
generationConfig: {
|
|
32
|
+
temperature: config.temperature,
|
|
33
|
+
responseMimeType: "application/json"
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def extract_content(response)
|
|
39
|
+
response.body.dig("candidates", 0, "content", "parts", 0, "text")
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module VibeSort
|
|
4
|
+
module Providers
|
|
5
|
+
# Adapter for the Groq API (OpenAI-compatible Chat Completions)
|
|
6
|
+
class Groq < OpenAI
|
|
7
|
+
ENDPOINT = "https://api.groq.com/openai/v1/chat/completions"
|
|
8
|
+
DEFAULT_MODEL = "llama-3.3-70b-versatile"
|
|
9
|
+
|
|
10
|
+
private
|
|
11
|
+
|
|
12
|
+
def provider_name
|
|
13
|
+
"Groq"
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def endpoint
|
|
17
|
+
ENDPOINT
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module VibeSort
|
|
4
|
+
module Providers
|
|
5
|
+
# Adapter for the OpenAI Chat Completions API
|
|
6
|
+
class OpenAI < Base
|
|
7
|
+
ENDPOINT = "https://api.openai.com/v1/chat/completions"
|
|
8
|
+
DEFAULT_MODEL = "gpt-4o-mini"
|
|
9
|
+
|
|
10
|
+
private
|
|
11
|
+
|
|
12
|
+
def provider_name
|
|
13
|
+
"OpenAI"
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def endpoint
|
|
17
|
+
ENDPOINT
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def headers
|
|
21
|
+
{ "Authorization" => "Bearer #{config.api_key}" }
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def build_payload(array)
|
|
25
|
+
{
|
|
26
|
+
model: model,
|
|
27
|
+
temperature: config.temperature,
|
|
28
|
+
response_format: { type: "json_object" },
|
|
29
|
+
messages: [
|
|
30
|
+
{ role: "system", content: SYSTEM_PROMPT },
|
|
31
|
+
{ role: "user", content: user_prompt(array) }
|
|
32
|
+
]
|
|
33
|
+
}
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def extract_content(response)
|
|
37
|
+
response.body.dig("choices", 0, "message", "content")
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module VibeSort
|
|
4
|
+
module Providers
|
|
5
|
+
# Adapter for the SpaceXAI (xAI) Grok API (OpenAI-compatible Chat Completions)
|
|
6
|
+
class SpaceXAI < OpenAI
|
|
7
|
+
ENDPOINT = "https://api.x.ai/v1/chat/completions"
|
|
8
|
+
DEFAULT_MODEL = "grok-4"
|
|
9
|
+
|
|
10
|
+
private
|
|
11
|
+
|
|
12
|
+
def provider_name
|
|
13
|
+
"SpaceXAI"
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def endpoint
|
|
17
|
+
ENDPOINT
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
data/lib/vibe_sort/sorter.rb
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module VibeSort
|
|
4
|
-
# Sorter
|
|
4
|
+
# Sorter dispatches the sorting operation to the configured provider adapter
|
|
5
5
|
class Sorter
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
PROVIDER_CLASSES = {
|
|
7
|
+
openai: Providers::OpenAI,
|
|
8
|
+
anthropic: Providers::Anthropic,
|
|
9
|
+
gemini: Providers::Gemini,
|
|
10
|
+
groq: Providers::Groq,
|
|
11
|
+
spacexai: Providers::SpaceXAI
|
|
12
|
+
}.freeze
|
|
8
13
|
|
|
9
14
|
attr_reader :config
|
|
10
15
|
|
|
@@ -15,111 +20,13 @@ module VibeSort
|
|
|
15
20
|
@config = config
|
|
16
21
|
end
|
|
17
22
|
|
|
18
|
-
# Perform the sorting operation via
|
|
23
|
+
# Perform the sorting operation via the configured provider's API
|
|
19
24
|
#
|
|
20
25
|
# @param array [Array] Array of numbers and/or strings to sort
|
|
21
26
|
# @return [Hash] Result hash with :success, :sorted_array, and optional :error keys
|
|
22
27
|
# @raise [VibeSort::ApiError] if the API request fails
|
|
23
28
|
def perform(array)
|
|
24
|
-
|
|
25
|
-
req.body = build_payload(array)
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
handle_response(response)
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
private
|
|
32
|
-
|
|
33
|
-
# Build the connection to OpenAI API
|
|
34
|
-
#
|
|
35
|
-
# @return [Faraday::Connection] Faraday connection object
|
|
36
|
-
def connection
|
|
37
|
-
@connection ||= Faraday.new(url: OPENAI_API_URL) do |f|
|
|
38
|
-
f.request :json # Encodes request body as JSON
|
|
39
|
-
f.response :json # Decodes response body as JSON
|
|
40
|
-
f.headers["Authorization"] = "Bearer #{config.api_key}"
|
|
41
|
-
f.headers["Content-Type"] = "application/json"
|
|
42
|
-
f.adapter Faraday.default_adapter
|
|
43
|
-
end
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
# Build the request payload for OpenAI API
|
|
47
|
-
#
|
|
48
|
-
# @param array [Array] Array to sort (numbers and/or strings)
|
|
49
|
-
# @return [Hash] Request payload
|
|
50
|
-
def build_payload(array)
|
|
51
|
-
{
|
|
52
|
-
model: DEFAULT_MODEL,
|
|
53
|
-
temperature: config.temperature,
|
|
54
|
-
response_format: { type: "json_object" },
|
|
55
|
-
messages: [
|
|
56
|
-
{
|
|
57
|
-
role: "system",
|
|
58
|
-
content: "You are an assistant that only sorts arrays. The array may contain numbers and strings. Sort the array in ascending order. Follow standard sorting rules: numbers should come before strings, and string sorting should be case-sensitive. Return a JSON object with a single key 'sorted_array' containing the sorted elements."
|
|
59
|
-
},
|
|
60
|
-
{
|
|
61
|
-
role: "user",
|
|
62
|
-
content: "Please sort this array: #{array.to_json}"
|
|
63
|
-
}
|
|
64
|
-
]
|
|
65
|
-
}
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
# Handle the API response
|
|
69
|
-
#
|
|
70
|
-
# @param response [Faraday::Response] HTTP response
|
|
71
|
-
# @return [Hash] Result hash
|
|
72
|
-
# @raise [VibeSort::ApiError] if response is invalid or parsing fails
|
|
73
|
-
def handle_response(response)
|
|
74
|
-
unless response.success?
|
|
75
|
-
error_message = extract_error_message(response)
|
|
76
|
-
raise ApiError.new("OpenAI API error: #{error_message}", response)
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
parse_sorted_array(response)
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
# Extract error message from failed response
|
|
83
|
-
#
|
|
84
|
-
# @param response [Faraday::Response] HTTP response
|
|
85
|
-
# @return [String] Error message
|
|
86
|
-
def extract_error_message(response)
|
|
87
|
-
return "Unknown error" unless response.body.is_a?(Hash)
|
|
88
|
-
|
|
89
|
-
response.body.dig("error", "message") || "HTTP #{response.status}"
|
|
90
|
-
rescue StandardError
|
|
91
|
-
"HTTP #{response.status}"
|
|
92
|
-
end
|
|
93
|
-
|
|
94
|
-
# Parse the sorted array from the API response
|
|
95
|
-
#
|
|
96
|
-
# @param response [Faraday::Response] HTTP response
|
|
97
|
-
# @return [Hash] Success result with sorted array
|
|
98
|
-
# @raise [VibeSort::ApiError] if parsing fails or validation fails
|
|
99
|
-
def parse_sorted_array(response)
|
|
100
|
-
content = response.body.dig("choices", 0, "message", "content")
|
|
101
|
-
raise ApiError.new("Invalid response structure", response) if content.nil?
|
|
102
|
-
|
|
103
|
-
parsed_content = JSON.parse(content)
|
|
104
|
-
sorted_array = parsed_content["sorted_array"]
|
|
105
|
-
|
|
106
|
-
validate_sorted_array!(sorted_array)
|
|
107
|
-
|
|
108
|
-
{ success: true, sorted_array: sorted_array }
|
|
109
|
-
rescue JSON::ParserError => e
|
|
110
|
-
raise ApiError.new("Failed to parse JSON response: #{e.message}", response)
|
|
111
|
-
end
|
|
112
|
-
|
|
113
|
-
# Validate that the sorted array is valid
|
|
114
|
-
#
|
|
115
|
-
# @param sorted_array [Object] Parsed sorted array
|
|
116
|
-
# @raise [VibeSort::ApiError] if validation fails
|
|
117
|
-
def validate_sorted_array!(sorted_array)
|
|
118
|
-
raise ApiError, "Response does not contain a valid 'sorted_array'" unless sorted_array.is_a?(Array)
|
|
119
|
-
|
|
120
|
-
return if sorted_array.all? { |item| item.is_a?(Numeric) || item.is_a?(String) }
|
|
121
|
-
|
|
122
|
-
raise ApiError, "Sorted array contains invalid values (must be numbers or strings)"
|
|
29
|
+
PROVIDER_CLASSES.fetch(config.provider).new(config).perform(array)
|
|
123
30
|
end
|
|
124
31
|
end
|
|
125
32
|
end
|
data/lib/vibe_sort/version.rb
CHANGED
data/lib/vibe_sort.rb
CHANGED
|
@@ -6,6 +6,12 @@ require "json"
|
|
|
6
6
|
require_relative "vibe_sort/version"
|
|
7
7
|
require_relative "vibe_sort/error"
|
|
8
8
|
require_relative "vibe_sort/configuration"
|
|
9
|
+
require_relative "vibe_sort/providers/base"
|
|
10
|
+
require_relative "vibe_sort/providers/openai"
|
|
11
|
+
require_relative "vibe_sort/providers/anthropic"
|
|
12
|
+
require_relative "vibe_sort/providers/gemini"
|
|
13
|
+
require_relative "vibe_sort/providers/groq"
|
|
14
|
+
require_relative "vibe_sort/providers/space_x_ai"
|
|
9
15
|
require_relative "vibe_sort/sorter"
|
|
10
16
|
require_relative "vibe_sort/client"
|
|
11
17
|
|
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.3.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:
|
|
11
|
+
date: 2026-07-10 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: faraday
|
|
@@ -24,8 +24,9 @@ dependencies:
|
|
|
24
24
|
- - "~>"
|
|
25
25
|
- !ruby/object:Gem::Version
|
|
26
26
|
version: '2.0'
|
|
27
|
-
description: A proof-of-concept Ruby gem that sorts
|
|
28
|
-
OpenAI Chat Completions
|
|
27
|
+
description: A proof-of-concept Ruby gem that sorts arrays by leveraging LLM APIs
|
|
28
|
+
(OpenAI Chat Completions, Anthropic Messages, Google Gemini). Demonstrates how AI
|
|
29
|
+
can be used for computational tasks.
|
|
29
30
|
email:
|
|
30
31
|
- chayut_o@hotmail.com
|
|
31
32
|
executables: []
|
|
@@ -45,14 +46,18 @@ files:
|
|
|
45
46
|
- docs/development.md
|
|
46
47
|
- docs/v0.2.0_UPDATE.md
|
|
47
48
|
- lib/vibe/sort.rb
|
|
48
|
-
- lib/vibe/sort/version.rb
|
|
49
49
|
- lib/vibe_sort.rb
|
|
50
50
|
- lib/vibe_sort/client.rb
|
|
51
51
|
- lib/vibe_sort/configuration.rb
|
|
52
52
|
- lib/vibe_sort/error.rb
|
|
53
|
+
- lib/vibe_sort/providers/anthropic.rb
|
|
54
|
+
- lib/vibe_sort/providers/base.rb
|
|
55
|
+
- lib/vibe_sort/providers/gemini.rb
|
|
56
|
+
- lib/vibe_sort/providers/groq.rb
|
|
57
|
+
- lib/vibe_sort/providers/openai.rb
|
|
58
|
+
- lib/vibe_sort/providers/space_x_ai.rb
|
|
53
59
|
- lib/vibe_sort/sorter.rb
|
|
54
60
|
- lib/vibe_sort/version.rb
|
|
55
|
-
- sig/vibe/sort.rbs
|
|
56
61
|
homepage: https://github.com/chayuto/vibe-sort
|
|
57
62
|
licenses:
|
|
58
63
|
- MIT
|
|
@@ -76,8 +81,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
76
81
|
- !ruby/object:Gem::Version
|
|
77
82
|
version: '0'
|
|
78
83
|
requirements: []
|
|
79
|
-
rubygems_version: 3.5.
|
|
84
|
+
rubygems_version: 3.5.22
|
|
80
85
|
signing_key:
|
|
81
86
|
specification_version: 4
|
|
82
|
-
summary: AI-powered array sorting using OpenAI
|
|
87
|
+
summary: AI-powered array sorting using OpenAI, Anthropic Claude, or Google Gemini
|
|
83
88
|
test_files: []
|
data/lib/vibe/sort/version.rb
DELETED