vibe-sort 0.2.0 → 0.3.1

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.
@@ -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.1"
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.1
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-11 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: faraday
@@ -24,8 +24,10 @@ 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, and the OpenAI-compatible
29
+ Groq and SpaceXAI Grok APIs). Demonstrates how AI can be used for computational
30
+ tasks.
29
31
  email:
30
32
  - chayut_o@hotmail.com
31
33
  executables: []
@@ -45,14 +47,18 @@ files:
45
47
  - docs/development.md
46
48
  - docs/v0.2.0_UPDATE.md
47
49
  - lib/vibe/sort.rb
48
- - lib/vibe/sort/version.rb
49
50
  - lib/vibe_sort.rb
50
51
  - lib/vibe_sort/client.rb
51
52
  - lib/vibe_sort/configuration.rb
52
53
  - lib/vibe_sort/error.rb
54
+ - lib/vibe_sort/providers/anthropic.rb
55
+ - lib/vibe_sort/providers/base.rb
56
+ - lib/vibe_sort/providers/gemini.rb
57
+ - lib/vibe_sort/providers/groq.rb
58
+ - lib/vibe_sort/providers/openai.rb
59
+ - lib/vibe_sort/providers/space_x_ai.rb
53
60
  - lib/vibe_sort/sorter.rb
54
61
  - lib/vibe_sort/version.rb
55
- - sig/vibe/sort.rbs
56
62
  homepage: https://github.com/chayuto/vibe-sort
57
63
  licenses:
58
64
  - MIT
@@ -60,6 +66,8 @@ metadata:
60
66
  homepage_uri: https://github.com/chayuto/vibe-sort
61
67
  source_code_uri: https://github.com/chayuto/vibe-sort
62
68
  changelog_uri: https://github.com/chayuto/vibe-sort/blob/main/CHANGELOG.md
69
+ documentation_uri: https://rubydoc.info/gems/vibe-sort
70
+ bug_tracker_uri: https://github.com/chayuto/vibe-sort/issues
63
71
  rubygems_mfa_required: 'true'
64
72
  post_install_message:
65
73
  rdoc_options: []
@@ -76,8 +84,9 @@ required_rubygems_version: !ruby/object:Gem::Requirement
76
84
  - !ruby/object:Gem::Version
77
85
  version: '0'
78
86
  requirements: []
79
- rubygems_version: 3.5.11
87
+ rubygems_version: 3.5.22
80
88
  signing_key:
81
89
  specification_version: 4
82
- summary: AI-powered array sorting using OpenAI's GPT models
90
+ summary: AI-powered array sorting using OpenAI, Anthropic Claude, Google Gemini, Groq,
91
+ or SpaceXAI Grok
83
92
  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