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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 57341544b7a583956dde5f307ad0705fd01f70994ea5fc634557ddc2ec045b55
4
- data.tar.gz: 823b1d1d124ae00d2abaae240d74917df07452b6161619ec6e398362f266c8ea
3
+ metadata.gz: b152ab9be69894b49e7a1324253a4e0a4fe3d5a15ec61f50e8b67b3bd424e8ef
4
+ data.tar.gz: 7818eebe77421199fefeba102c3986da5e5665155cb7a916500377515a16b505
5
5
  SHA512:
6
- metadata.gz: b0158f11c6899b503b183b50933d14485f19c4bcee3dbf75fb8e8ff3149eb279e8ec5af52a8bd6fea1cd15ca35255dde7a5884a1e0ec133b51d6c3de9f4fad36
7
- data.tar.gz: d59a672c3b38922cf213ad0011bea552b02b0b66d79e600f42df37dd213e6683ef512f0d7413a98930b8f588e54942868dd3f421dec24e9d86c9f95a268ce9a2
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's GPT models**
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 number arrays by leveraging the OpenAI Chat Completions API. Instead of using traditional sorting algorithms, it asks GPT to do the work!
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
  [![Gem Version](https://badge.fury.io/rb/vibe-sort.svg)](https://badge.fury.io/rb/vibe-sort)
8
8
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
@@ -13,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's GPT models to sort arrays
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 for model behavior
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 OpenAI API key with access to the Chat Completions API
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`**: Handles OpenAI API communication via Faraday
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 your OpenAI API key as an environment variable:
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/) GPT models
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
  ---
@@ -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): Your OpenAI API key
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 OpenAI's API.
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): OpenAI API key
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
- temperature: 0.0
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 handles API communication. Not intended for direct use.
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 OpenAI API.
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
- #### `OPENAI_API_URL`
198
+ #### `PROVIDER_CLASSES`
199
+
200
+ Maps provider symbols to adapter classes.
201
+
202
+ **Value:** `{ openai:, anthropic:, gemini:, groq:, spacexai: }`
203
+
204
+ ---
174
205
 
175
- The OpenAI API endpoint URL.
206
+ ## VibeSort::Providers
176
207
 
177
- **Value:** `"https://api.openai.com/v1/chat/completions"`
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
- #### `DEFAULT_MODEL`
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
- The default GPT model used for sorting.
218
+ Notes:
182
219
 
183
- **Value:** `"gpt-3.5-turbo-1106"`
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
- | "OpenAI API error: Invalid API key" | Invalid or missing API key | Check API key |
275
- | "OpenAI API error: Rate limit exceeded" | Too many requests | Wait and retry |
276
- | "OpenAI API error: HTTP {status}" | HTTP error | Check API status |
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
- ### OPENAI_API_KEY
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.1.0`
475
+ Current version: `0.3.0`
436
476
 
437
477
  ```ruby
438
478
  VibeSort::VERSION
439
- # => "0.1.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
- │ - Request building
32
- │ - Response parsing │
33
- - Validation
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
- OpenAI API
39
- https://api.openai.com/v1/chat/
40
- completions
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 securely
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 empty
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`: OpenAI API key (String)
85
- - `temperature`: Model temperature (Float, 0.0-2.0)
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**: Handles communication with OpenAI API.
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)`: Executes sort via API
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
- **Constants**:
108
- - `OPENAI_API_URL`: API endpoint
109
- - `DEFAULT_MODEL`: "gpt-3.5-turbo-1106" (supports JSON mode)
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. **Sorter builds request payload**
154
+ 4. **Adapter builds the provider-specific request payload** (OpenAI example)
138
155
  ```json
139
156
  {
140
- "model": "gpt-3.5-turbo-1106",
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. **Sorter sends POST request**
157
- - URL: `https://api.openai.com/v1/chat/completions`
158
- - Headers: Authorization (Bearer token), Content-Type
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. **OpenAI processes request**
178
+ 6. **Provider processes request**
162
179
  - Model analyzes the array
163
180
  - Returns JSON with sorted array
164
181
 
165
- 7. **Sorter parses response**
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's JSON mode (`response_format: { type: "json_object" }`) ensures the model always returns valid JSON, making parsing more reliable.
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. **Custom Models**: Allow users to specify different GPT models
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
- require_relative "sort/version"
4
-
5
- module Vibe
6
- module Sort
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"
@@ -7,17 +7,26 @@ module VibeSort
7
7
 
8
8
  # Initialize a new VibeSort client
9
9
  #
10
- # @param api_key [String] OpenAI API key
11
- # @param temperature [Float] Temperature for the model (default: 0.0)
12
- # @raise [ArgumentError] if api_key is invalid
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
- def initialize(api_key:, temperature: 0.0)
17
- @config = Configuration.new(api_key: api_key, temperature: temperature)
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 OpenAI API
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
- attr_reader :api_key, :temperature
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] OpenAI API key
12
- # @param temperature [Float] Temperature for the model (0.0 to 2.0)
13
- # @raise [ArgumentError] if api_key is nil or empty
14
- def initialize(api_key:, temperature: 0.0)
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
@@ -1,10 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module VibeSort
4
- # Sorter class handles the API call to OpenAI
4
+ # Sorter dispatches the sorting operation to the configured provider adapter
5
5
  class Sorter
6
- OPENAI_API_URL = "https://api.openai.com/v1/chat/completions"
7
- DEFAULT_MODEL = "gpt-3.5-turbo-1106"
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 OpenAI API
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
- response = connection.post do |req|
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module VibeSort
4
- VERSION = "0.2.0"
4
+ VERSION = "0.3.0"
5
5
  end
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.2.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: 2025-10-16 00:00:00.000000000 Z
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 number arrays by leveraging the
28
- OpenAI Chat Completions API. Demonstrates how AI can be used for computational tasks.
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.11
84
+ rubygems_version: 3.5.22
80
85
  signing_key:
81
86
  specification_version: 4
82
- summary: AI-powered array sorting using OpenAI's GPT models
87
+ summary: AI-powered array sorting using OpenAI, Anthropic Claude, or Google Gemini
83
88
  test_files: []
@@ -1,7 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Vibe
4
- module Sort
5
- VERSION = "0.1.0"
6
- end
7
- end
data/sig/vibe/sort.rbs DELETED
@@ -1,6 +0,0 @@
1
- module Vibe
2
- module Sort
3
- VERSION: String
4
- # See the writing guide of rbs: https://github.com/ruby/rbs#guides
5
- end
6
- end