reve_ai 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 280bf1673c22b411163ea4dad09eae4a7ca07a06dceefee1d10b54cc2a07a097
4
+ data.tar.gz: 288d3493fcd73b1827003b785c29fd0c7ce53a76a302769e03054686508d2dc4
5
+ SHA512:
6
+ metadata.gz: 604e38817990a60d470e0c24b96033c88b27774012e6dac07e6392c7f413d02fd8c123a92978868eb972790dea4ad23f11ce73226c11db88f0c3a5482aff7e52
7
+ data.tar.gz: 202c65bafdcd96b002cff7a083f10b7efd4c05e187b0add59831833e8d6dfd88e7aa0fac49037b986a5aedb4bcf19eceb3b6d4a679741a2acb3bd7577bc5a27f
data/README.md ADDED
@@ -0,0 +1,204 @@
1
+ # ReveAI
2
+
3
+ Ruby client for the [Reve image generation API](https://api.reve.com/console/docs).
4
+
5
+ [![Gem Version](https://badge.fury.io/rb/reve_ai.svg)](https://badge.fury.io/rb/reve_ai)
6
+ [![ci](https://github.com/dpaluy/reve_ai/actions/workflows/ci.yml/badge.svg)](https://github.com/dpaluy/reve_ai/actions/workflows/ci.yml)
7
+
8
+ ## Installation
9
+
10
+ ```
11
+ bundle add reve_ai
12
+ ```
13
+
14
+ ## Usage
15
+
16
+ Configure once:
17
+
18
+ ```ruby
19
+ ReveAI.configure do |config|
20
+ config.api_key = ENV.fetch("REVE_AI_API_KEY")
21
+ end
22
+ ```
23
+
24
+ ### Create Image
25
+
26
+ Generate an image from a text prompt:
27
+
28
+ ```ruby
29
+ client = ReveAI::Client.new
30
+
31
+ response = client.images.create(prompt: "A beautiful sunset over mountains")
32
+
33
+ response.image # => "base64encodeddata..."
34
+ response.version # => "reve-create@20250915"
35
+ response.request_id # => "rsid-..."
36
+ response.credits_used # => 18
37
+ response.credits_remaining # => 982
38
+ ```
39
+
40
+ With aspect ratio:
41
+
42
+ ```ruby
43
+ response = client.images.create(
44
+ prompt: "A beautiful sunset over mountains",
45
+ aspect_ratio: "16:9" # Options: 16:9, 9:16, 3:2, 2:3, 4:3, 3:4, 1:1 (default: 3:2)
46
+ )
47
+ ```
48
+
49
+ With specific model version:
50
+
51
+ ```ruby
52
+ response = client.images.create(
53
+ prompt: "A beautiful sunset over mountains",
54
+ version: "reve-create@20250915" # Or "latest" (default)
55
+ )
56
+ ```
57
+
58
+ ### Edit Image
59
+
60
+ Modify an existing image using text instructions:
61
+
62
+ ```ruby
63
+ require "base64"
64
+
65
+ client = ReveAI::Client.new
66
+
67
+ # Load and encode the image
68
+ image_data = Base64.strict_encode64(File.read("my-image.png"))
69
+
70
+ response = client.images.edit(
71
+ edit_instruction: "Add dramatic clouds to the sky",
72
+ reference_image: image_data
73
+ )
74
+
75
+ response.image # => "base64editeddata..."
76
+ response.version # => "reve-edit@20250915"
77
+ ```
78
+
79
+ Available versions for edit: `latest`, `latest-fast`, `reve-edit@20250915`, `reve-edit-fast@20251030`
80
+
81
+ ### Remix Images
82
+
83
+ Combine text prompts with reference images to create new variations:
84
+
85
+ ```ruby
86
+ require "base64"
87
+
88
+ client = ReveAI::Client.new
89
+
90
+ # Load and encode reference images
91
+ image1 = Base64.strict_encode64(File.read("person.png"))
92
+ image2 = Base64.strict_encode64(File.read("background.png"))
93
+
94
+ # Use <img>N</img> tags to reference specific images by index
95
+ response = client.images.remix(
96
+ prompt: "The person from <img>0</img> standing in the scene from <img>1</img>",
97
+ reference_images: [image1, image2],
98
+ aspect_ratio: "16:9" # Optional
99
+ )
100
+
101
+ response.image # => "base64remixeddata..."
102
+ response.version # => "reve-remix@20250915"
103
+ ```
104
+
105
+ Available versions for remix: `latest`, `latest-fast`, `reve-remix@20250915`, `reve-remix-fast@20251030`
106
+
107
+ ### Rails
108
+
109
+ Create `config/initializers/reve_ai.rb`:
110
+
111
+ ```ruby
112
+ ReveAI.configure do |c|
113
+ c.api_key = Rails.application.credentials.dig(:reve, :api_key)
114
+ # c.base_url = "https://api.reve.com"
115
+ # c.timeout = 120
116
+ # c.open_timeout = 30
117
+ # c.max_retries = 2
118
+ end
119
+ ```
120
+
121
+ ### Error Handling
122
+
123
+ The gem provides detailed error classes for different scenarios:
124
+
125
+ ```ruby
126
+ begin
127
+ client.images.create(prompt: "A sunset")
128
+ rescue ReveAI::ValidationError => e
129
+ # Input validation failed (prompt too long, invalid aspect ratio, etc.)
130
+ puts "Validation error: #{e.message}"
131
+ rescue ReveAI::UnauthorizedError => e
132
+ # Invalid API key (401)
133
+ puts "Auth error: #{e.message}"
134
+ rescue ReveAI::InsufficientCreditsError => e
135
+ # Budget has run out (402)
136
+ puts "Out of credits: #{e.message}"
137
+ rescue ReveAI::UnprocessableEntityError => e
138
+ # Inputs could not be understood (422)
139
+ puts "Unprocessable: #{e.message}"
140
+ rescue ReveAI::RateLimitError => e
141
+ # Rate limited (429) - check retry_after
142
+ puts "Rate limited. Retry after: #{e.retry_after} seconds"
143
+ rescue ReveAI::BadRequestError => e
144
+ # Invalid request parameters (400)
145
+ puts "Bad request: #{e.message}"
146
+ rescue ReveAI::ServerError => e
147
+ # Server-side error (5xx)
148
+ puts "Server error: #{e.message}"
149
+ rescue ReveAI::TimeoutError => e
150
+ # Request timed out
151
+ puts "Timeout: #{e.message}"
152
+ rescue ReveAI::ConnectionError => e
153
+ # Connection failed
154
+ puts "Connection error: #{e.message}"
155
+ end
156
+ ```
157
+
158
+ ### Content Moderation
159
+
160
+ The API may flag content violations:
161
+
162
+ ```ruby
163
+ response = client.images.create(prompt: "...")
164
+
165
+ if response.content_violation?
166
+ puts "Content was flagged by moderation"
167
+ end
168
+ ```
169
+
170
+ ### Configuration Options
171
+
172
+ | Option | Default | Description |
173
+ |--------|---------|-------------|
174
+ | `api_key` | `ENV["REVE_AI_API_KEY"]` | Your Reve API key |
175
+ | `base_url` | `https://api.reve.com` | API base URL |
176
+ | `timeout` | `120` | Request timeout in seconds |
177
+ | `open_timeout` | `30` | Connection timeout in seconds |
178
+ | `max_retries` | `2` | Number of retries for failed requests |
179
+ | `logger` | `nil` | Logger instance for debugging |
180
+ | `debug` | `false` | Enable debug logging |
181
+
182
+ ### Validation Constraints
183
+
184
+ | Constraint | Value |
185
+ |------------|-------|
186
+ | Max prompt length | 2560 characters |
187
+ | Max reference images (remix) | 6 |
188
+ | Valid aspect ratios | 16:9, 9:16, 3:2, 2:3, 4:3, 3:4, 1:1 |
189
+
190
+ ## Development
191
+
192
+ ```
193
+ bundle install
194
+ bundle exec rake test
195
+ bundle exec rubocop
196
+ ```
197
+
198
+ ## Contributing
199
+
200
+ Bug reports and pull requests are welcome on GitHub at https://github.com/dpaluy/reve_ai.
201
+
202
+ ## License
203
+
204
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rake/testtask"
5
+ require "rubocop/rake_task"
6
+
7
+ Rake::TestTask.new(:test) do |t|
8
+ t.libs << "test"
9
+ t.libs << "lib"
10
+ t.test_files = FileList["test/**/*_test.rb"]
11
+ end
12
+
13
+ RuboCop::RakeTask.new
14
+
15
+ task default: %i[test rubocop]
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ReveAI
4
+ # HTTP client for the Reve image generation API.
5
+ #
6
+ # Provides access to all Reve API endpoints through resource objects.
7
+ # Configure globally via {ReveAI.configure} or pass parameters directly
8
+ # to the constructor.
9
+ #
10
+ # @example Using global configuration
11
+ # ReveAI.configure do |config|
12
+ # config.api_key = ENV["REVE_AI_API_KEY"]
13
+ # end
14
+ #
15
+ # client = ReveAI::Client.new
16
+ # result = client.images.create(prompt: "A cat wearing a hat")
17
+ #
18
+ # @example Using per-instance configuration
19
+ # client = ReveAI::Client.new(
20
+ # api_key: "your-key",
21
+ # timeout: 60
22
+ # )
23
+ #
24
+ # @see Resources::Images
25
+ # @see Configuration
26
+ class Client
27
+ # @return [Configuration] Configuration instance for this client
28
+ attr_reader :configuration
29
+
30
+ # Creates a new Reve API client.
31
+ #
32
+ # @param api_key [String, nil] API key (defaults to global config or ENV["REVE_AI_API_KEY"])
33
+ # @param options [Hash] Additional configuration options
34
+ # @option options [String] :base_url Base URL for API requests
35
+ # @option options [Integer] :timeout Request timeout in seconds
36
+ # @option options [Integer] :open_timeout Connection timeout in seconds
37
+ # @option options [Integer] :max_retries Number of retry attempts
38
+ # @option options [Logger] :logger Logger instance for debugging
39
+ # @option options [Boolean] :debug Enable debug logging
40
+ #
41
+ # @raise [ConfigurationError] if api_key is missing or empty
42
+ #
43
+ # @example
44
+ # client = ReveAI::Client.new(api_key: "your-api-key")
45
+ def initialize(api_key: nil, **options)
46
+ @configuration = build_configuration(api_key, options)
47
+ validate_configuration!
48
+ end
49
+
50
+ # Returns the Images resource for image generation operations.
51
+ #
52
+ # @return [Resources::Images] Image operations interface
53
+ # @see Resources::Images
54
+ #
55
+ # @example Generate an image
56
+ # result = client.images.create(prompt: "A sunset over mountains")
57
+ # puts result.base64 # Base64 encoded PNG
58
+ #
59
+ # @example Edit an image
60
+ # result = client.images.edit(
61
+ # edit_instruction: "Make the sky more blue",
62
+ # reference_image: base64_encoded_image
63
+ # )
64
+ #
65
+ # @example Remix images
66
+ # result = client.images.remix(
67
+ # prompt: "Combine <img>1</img> and <img>2</img> into one scene",
68
+ # reference_images: [image1_base64, image2_base64]
69
+ # )
70
+ def images
71
+ @images ||= Resources::Images.new(self)
72
+ end
73
+
74
+ # Returns the HTTP client for making API requests.
75
+ #
76
+ # @return [HTTP::Client] HTTP client instance
77
+ # @api private
78
+ def http_client
79
+ @http_client ||= HTTP::Client.new(configuration)
80
+ end
81
+
82
+ private
83
+
84
+ # Builds configuration from global config and options.
85
+ #
86
+ # @param api_key [String, nil] API key override
87
+ # @param options [Hash] Configuration options
88
+ # @return [Configuration] Merged configuration
89
+ # @api private
90
+ def build_configuration(api_key, options)
91
+ config = ReveAI.configuration&.dup || Configuration.new
92
+ config.api_key = api_key if api_key
93
+ options.each { |key, value| config.public_send("#{key}=", value) }
94
+ config
95
+ end
96
+
97
+ # Validates that required configuration is present.
98
+ #
99
+ # @raise [ConfigurationError] if API key is missing
100
+ # @api private
101
+ def validate_configuration!
102
+ raise ConfigurationError, "API key is required" unless configuration.valid?
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ReveAI
4
+ # Configuration settings for the ReveAI client.
5
+ #
6
+ # Stores API credentials and connection settings. Can be configured globally
7
+ # via {ReveAI.configure} or per-instance when creating a {Client}.
8
+ #
9
+ # @example Global configuration
10
+ # ReveAI.configure do |config|
11
+ # config.api_key = ENV["REVE_AI_API_KEY"]
12
+ # config.timeout = 120
13
+ # config.debug = true
14
+ # end
15
+ #
16
+ # @example Environment variable
17
+ # # Set REVE_AI_API_KEY environment variable
18
+ # export REVE_AI_API_KEY="your-api-key"
19
+ #
20
+ # # API key is automatically loaded
21
+ # client = ReveAI::Client.new
22
+ #
23
+ # @see Client
24
+ class Configuration
25
+ # @return [String] Default base URL for the Reve API
26
+ DEFAULT_BASE_URL = "https://api.reve.com"
27
+
28
+ # @return [Integer] Default request timeout in seconds (2 minutes for image generation)
29
+ DEFAULT_TIMEOUT = 120
30
+
31
+ # @return [Integer] Default connection open timeout in seconds
32
+ DEFAULT_OPEN_TIMEOUT = 30
33
+
34
+ # @return [Integer] Default number of retry attempts for failed requests
35
+ DEFAULT_MAX_RETRIES = 2
36
+
37
+ # @return [Array<String>] Valid aspect ratios for image generation
38
+ VALID_ASPECT_RATIOS = %w[16:9 9:16 3:2 2:3 4:3 3:4 1:1].freeze
39
+
40
+ # @return [Integer] Maximum allowed prompt length in characters
41
+ MAX_PROMPT_LENGTH = 2560
42
+
43
+ # @return [Integer] Maximum number of reference images for remix operations
44
+ MAX_REFERENCE_IMAGES = 6
45
+
46
+ # @return [String, nil] Reve API key for authentication
47
+ attr_accessor :api_key
48
+
49
+ # @return [String] Base URL for API requests (defaults to https://api.reve.com)
50
+ attr_accessor :base_url
51
+
52
+ # @return [Integer] Request timeout in seconds
53
+ attr_accessor :timeout
54
+
55
+ # @return [Integer] Connection open timeout in seconds
56
+ attr_accessor :open_timeout
57
+
58
+ # @return [Integer] Number of retry attempts for transient failures
59
+ attr_accessor :max_retries
60
+
61
+ # @return [Logger, nil] Logger instance for debug output
62
+ attr_accessor :logger
63
+
64
+ # @return [Boolean] Enable debug logging of HTTP requests/responses
65
+ attr_accessor :debug
66
+
67
+ # Creates a new configuration with default values.
68
+ #
69
+ # Automatically loads API key from the REVE_AI_API_KEY environment variable
70
+ # if present.
71
+ #
72
+ # @example
73
+ # config = ReveAI::Configuration.new
74
+ # config.api_key = "your-api-key"
75
+ # config.timeout = 60
76
+ def initialize
77
+ @api_key = ENV.fetch("REVE_AI_API_KEY", nil)
78
+ @base_url = DEFAULT_BASE_URL
79
+ @timeout = DEFAULT_TIMEOUT
80
+ @open_timeout = DEFAULT_OPEN_TIMEOUT
81
+ @max_retries = DEFAULT_MAX_RETRIES
82
+ @logger = nil
83
+ @debug = false
84
+ end
85
+
86
+ # Checks if the configuration has a valid API key.
87
+ #
88
+ # @return [Boolean] true if api_key is present and not empty
89
+ #
90
+ # @example
91
+ # config = ReveAI::Configuration.new
92
+ # config.valid? # => false
93
+ #
94
+ # config.api_key = "your-key"
95
+ # config.valid? # => true
96
+ def valid?
97
+ !api_key.nil? && !api_key.empty?
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,194 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ReveAI
4
+ # Base error class for all ReveAI errors.
5
+ #
6
+ # All library exceptions inherit from this class.
7
+ #
8
+ # @example Catching all ReveAI errors
9
+ # begin
10
+ # client.images.create(prompt: "A cat")
11
+ # rescue ReveAI::Error => e
12
+ # puts "ReveAI error: #{e.message}"
13
+ # end
14
+ class Error < StandardError; end
15
+
16
+ # Raised when configuration is invalid or missing.
17
+ #
18
+ # @example
19
+ # client = ReveAI::Client.new(api_key: nil)
20
+ # # => ReveAI::ConfigurationError: API key is required
21
+ class ConfigurationError < Error; end
22
+
23
+ # Raised when input validation fails before making an API call.
24
+ #
25
+ # @example Invalid prompt
26
+ # client.images.create(prompt: "")
27
+ # # => ReveAI::ValidationError: Prompt is required
28
+ #
29
+ # @example Invalid aspect ratio
30
+ # client.images.create(prompt: "A cat", aspect_ratio: "5:3")
31
+ # # => ReveAI::ValidationError: Invalid aspect_ratio '5:3'. Must be one of: 16:9, 9:16, ...
32
+ class ValidationError < Error; end
33
+
34
+ # Base class for network-level errors.
35
+ #
36
+ # Indicates connection or transport failures rather than API errors.
37
+ class NetworkError < Error; end
38
+
39
+ # Raised when a request times out.
40
+ #
41
+ # @example
42
+ # # Request exceeded timeout
43
+ # client.images.create(prompt: "A complex scene...")
44
+ # # => ReveAI::TimeoutError: Request timed out: execution expired
45
+ class TimeoutError < NetworkError; end
46
+
47
+ # Raised when unable to establish a connection to the API.
48
+ #
49
+ # @example
50
+ # # DNS failure or network unreachable
51
+ # # => ReveAI::ConnectionError: Connection failed: getaddrinfo: nodename nor servname provided
52
+ class ConnectionError < NetworkError; end
53
+
54
+ # Base class for API errors with HTTP status and response details.
55
+ #
56
+ # All API-related exceptions inherit from this class and include
57
+ # HTTP status code, response body, and headers for debugging.
58
+ #
59
+ # @example Handling API errors
60
+ # begin
61
+ # client.images.create(prompt: "...")
62
+ # rescue ReveAI::UnauthorizedError => e
63
+ # puts "Auth failed: #{e.message}"
64
+ # puts "Status: #{e.status}"
65
+ # rescue ReveAI::RateLimitError => e
66
+ # puts "Rate limited, retry after #{e.retry_after} seconds"
67
+ # rescue ReveAI::APIError => e
68
+ # puts "API error (#{e.status}): #{e.message}"
69
+ # puts "Request ID: #{e.request_id}"
70
+ # end
71
+ class APIError < Error
72
+ # @return [Integer, nil] HTTP status code
73
+ attr_reader :status
74
+
75
+ # @return [Hash] Response body parsed as Hash
76
+ attr_reader :body
77
+
78
+ # @return [Hash] Response headers
79
+ attr_reader :headers
80
+
81
+ # Creates a new API error instance.
82
+ #
83
+ # @param message [String, nil] Error message
84
+ # @param status [Integer, nil] HTTP status code
85
+ # @param body [Hash, nil] Response body
86
+ # @param headers [Hash, nil] Response headers
87
+ def initialize(message = nil, status: nil, body: nil, headers: nil)
88
+ @status = status
89
+ @body = body || {}
90
+ @headers = headers || {}
91
+ super(message)
92
+ end
93
+
94
+ # Returns the request ID from response headers.
95
+ #
96
+ # Useful for debugging and support requests.
97
+ #
98
+ # @return [String, nil] Request ID if present
99
+ def request_id
100
+ headers["x-reve-request-id"]
101
+ end
102
+
103
+ # Returns the error code from the response body.
104
+ #
105
+ # @return [String, nil] Error code (e.g., "PROMPT_TOO_LONG", "INVALID_API_KEY")
106
+ def error_code
107
+ body[:error_code]
108
+ end
109
+ end
110
+
111
+ # Raised on 400 Bad Request responses.
112
+ #
113
+ # Indicates invalid request parameters or malformed request body.
114
+ #
115
+ # @example
116
+ # # Prompt too long
117
+ # client.images.create(prompt: "x" * 10000)
118
+ # # => ReveAI::BadRequestError: Prompt exceeds maximum length
119
+ class BadRequestError < APIError; end
120
+
121
+ # Raised on 401 Unauthorized responses.
122
+ #
123
+ # Indicates invalid or missing API key.
124
+ #
125
+ # @example
126
+ # client = ReveAI::Client.new(api_key: "invalid-key")
127
+ # client.images.create(prompt: "A cat")
128
+ # # => ReveAI::UnauthorizedError: Invalid API key
129
+ class UnauthorizedError < APIError; end
130
+
131
+ # Raised on 402 Payment Required responses.
132
+ #
133
+ # Indicates the account has run out of credits.
134
+ #
135
+ # @example
136
+ # # Account has insufficient credits
137
+ # client.images.create(prompt: "A cat")
138
+ # # => ReveAI::InsufficientCreditsError: Your budget has run out
139
+ class InsufficientCreditsError < APIError; end
140
+
141
+ # Raised on 403 Forbidden responses.
142
+ #
143
+ # Indicates the API key lacks permission for the requested operation.
144
+ class ForbiddenError < APIError; end
145
+
146
+ # Raised on 404 Not Found responses.
147
+ #
148
+ # Indicates the requested resource does not exist.
149
+ class NotFoundError < APIError; end
150
+
151
+ # Raised on 422 Unprocessable Entity responses.
152
+ #
153
+ # Indicates the inputs could not be understood or processed.
154
+ #
155
+ # @example
156
+ # # Invalid reference image format
157
+ # client.images.edit(edit_instruction: "...", reference_image: "not-valid-base64")
158
+ # # => ReveAI::UnprocessableEntityError: The inputs could not be understood
159
+ class UnprocessableEntityError < APIError; end
160
+
161
+ # Raised on 429 Too Many Requests responses.
162
+ #
163
+ # Indicates the rate limit has been exceeded. Check {#retry_after}
164
+ # to determine when to retry.
165
+ #
166
+ # @example
167
+ # begin
168
+ # client.images.create(prompt: "A cat")
169
+ # rescue ReveAI::RateLimitError => e
170
+ # sleep e.retry_after
171
+ # retry
172
+ # end
173
+ class RateLimitError < APIError
174
+ # Returns the retry-after header value in seconds.
175
+ #
176
+ # Indicates how long to wait before retrying the request.
177
+ #
178
+ # @return [Integer, nil] Seconds to wait before retrying
179
+ def retry_after
180
+ headers["retry-after"]&.to_i
181
+ end
182
+ end
183
+
184
+ # Raised on 5xx server error responses.
185
+ #
186
+ # Indicates an internal server error on the Reve API side.
187
+ # These are typically transient and can be retried.
188
+ #
189
+ # @example
190
+ # # Server error
191
+ # client.images.create(prompt: "A cat")
192
+ # # => ReveAI::ServerError: Internal server error
193
+ class ServerError < APIError; end
194
+ end