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.
@@ -0,0 +1,208 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "faraday"
4
+ require "faraday/retry"
5
+ require "json"
6
+
7
+ module ReveAI
8
+ # HTTP layer for API communication.
9
+ module HTTP
10
+ # Low-level HTTP client using Faraday.
11
+ #
12
+ # Handles connection management, request/response processing, error handling,
13
+ # and automatic retries for transient failures.
14
+ #
15
+ # @api private
16
+ class Client
17
+ # @return [Hash{Integer => Class}] Mapping of HTTP status codes to error classes
18
+ ERROR_CODE_MAP = {
19
+ 400 => BadRequestError,
20
+ 401 => UnauthorizedError,
21
+ 402 => InsufficientCreditsError,
22
+ 403 => ForbiddenError,
23
+ 404 => NotFoundError,
24
+ 422 => UnprocessableEntityError,
25
+ 429 => RateLimitError
26
+ }.freeze
27
+
28
+ # @return [Array<Integer>] HTTP status codes that trigger automatic retry
29
+ RETRY_STATUSES = [429, 500, 502, 503, 504].freeze
30
+
31
+ # @return [Configuration] Configuration instance for this client
32
+ attr_reader :configuration
33
+
34
+ # Creates a new HTTP client.
35
+ #
36
+ # @param configuration [Configuration] Configuration instance
37
+ # @api private
38
+ def initialize(configuration)
39
+ @configuration = configuration
40
+ end
41
+
42
+ # Makes a POST request to the API.
43
+ #
44
+ # @param path [String] API endpoint path (e.g., "/v1/image/create")
45
+ # @param body [Hash] Request body to send as JSON
46
+ #
47
+ # @return [Response] Parsed API response
48
+ #
49
+ # @raise [TimeoutError] if request times out
50
+ # @raise [ConnectionError] if connection fails
51
+ # @raise [NetworkError] for other network errors
52
+ # @raise [BadRequestError] on 400 responses
53
+ # @raise [UnauthorizedError] on 401 responses
54
+ # @raise [InsufficientCreditsError] on 402 responses
55
+ # @raise [ForbiddenError] on 403 responses
56
+ # @raise [NotFoundError] on 404 responses
57
+ # @raise [UnprocessableEntityError] on 422 responses
58
+ # @raise [RateLimitError] on 429 responses
59
+ # @raise [ServerError] on 5xx responses
60
+ #
61
+ # @api private
62
+ def post(path, body = {})
63
+ normalized_path = path.sub(%r{^/}, "")
64
+ response = connection.post(normalized_path) { |req| req.body = JSON.generate(body) }
65
+ handle_response(response)
66
+ rescue Faraday::TimeoutError => e
67
+ raise TimeoutError, "Request timed out: #{e.message}"
68
+ rescue Faraday::ConnectionFailed => e
69
+ handle_connection_failed(e)
70
+ rescue Faraday::Error => e
71
+ raise NetworkError, "Network error: #{e.message}"
72
+ end
73
+
74
+ private
75
+
76
+ # Handles connection failed errors.
77
+ #
78
+ # Distinguishes between timeout errors (which may appear as connection failures)
79
+ # and actual connection failures.
80
+ #
81
+ # @param error [Faraday::ConnectionFailed] The connection error
82
+ # @raise [TimeoutError] if error indicates timeout
83
+ # @raise [ConnectionError] otherwise
84
+ # @api private
85
+ def handle_connection_failed(error)
86
+ raise TimeoutError, "Request timed out: #{error.message}" if error.message.include?("execution expired")
87
+
88
+ raise ConnectionError, "Connection failed: #{error.message}"
89
+ end
90
+
91
+ # Returns the Faraday connection, creating it if needed.
92
+ #
93
+ # @return [Faraday::Connection] Configured Faraday connection
94
+ # @api private
95
+ def connection
96
+ @connection ||= build_connection
97
+ end
98
+
99
+ # Builds a new Faraday connection with all middleware.
100
+ #
101
+ # @return [Faraday::Connection] New connection instance
102
+ # @api private
103
+ def build_connection
104
+ Faraday.new(url: configuration.base_url) do |conn|
105
+ configure_retry(conn)
106
+ configure_headers(conn)
107
+ configure_timeouts(conn)
108
+ configure_logging(conn)
109
+ conn.adapter Faraday.default_adapter
110
+ end
111
+ end
112
+
113
+ # Configures retry middleware.
114
+ #
115
+ # @param conn [Faraday::Connection] Connection to configure
116
+ # @api private
117
+ def configure_retry(conn)
118
+ conn.request :retry, max: configuration.max_retries, interval: 0.5,
119
+ backoff_factor: 2, retry_statuses: RETRY_STATUSES, methods: [:post]
120
+ end
121
+
122
+ # Configures request headers.
123
+ #
124
+ # @param conn [Faraday::Connection] Connection to configure
125
+ # @api private
126
+ def configure_headers(conn)
127
+ conn.headers["Authorization"] = "Bearer #{configuration.api_key}"
128
+ conn.headers["Content-Type"] = "application/json"
129
+ conn.headers["Accept"] = "application/json"
130
+ conn.headers["User-Agent"] = user_agent
131
+ end
132
+
133
+ # Configures connection and read timeouts.
134
+ #
135
+ # @param conn [Faraday::Connection] Connection to configure
136
+ # @api private
137
+ def configure_timeouts(conn)
138
+ conn.options.timeout = configuration.timeout
139
+ conn.options.open_timeout = configuration.open_timeout
140
+ end
141
+
142
+ # Configures response logging if debug mode is enabled.
143
+ #
144
+ # @param conn [Faraday::Connection] Connection to configure
145
+ # @api private
146
+ def configure_logging(conn)
147
+ conn.response :logger, configuration.logger if configuration.debug && configuration.logger
148
+ end
149
+
150
+ # Returns the User-Agent header value.
151
+ #
152
+ # @return [String] User-Agent string
153
+ # @api private
154
+ def user_agent
155
+ "reve-ai-ruby/#{ReveAI::VERSION} Ruby/#{RUBY_VERSION}"
156
+ end
157
+
158
+ # Processes the HTTP response.
159
+ #
160
+ # @param response [Faraday::Response] Raw Faraday response
161
+ # @return [Response] Wrapped response on success
162
+ # @raise [APIError] on error responses
163
+ # @api private
164
+ def handle_response(response)
165
+ body = parse_body(response.body)
166
+ return build_success_response(response, body) if response.status.between?(200, 299)
167
+
168
+ raise_api_error(response.status, body, response.headers.to_h)
169
+ end
170
+
171
+ # Builds a successful response object.
172
+ #
173
+ # @param response [Faraday::Response] Raw response
174
+ # @param body [Hash] Parsed body
175
+ # @return [Response] Response wrapper
176
+ # @api private
177
+ def build_success_response(response, body)
178
+ Response.new(status: response.status, headers: response.headers.to_h, body: body)
179
+ end
180
+
181
+ # Parses the response body as JSON.
182
+ #
183
+ # @param body [String, nil] Raw response body
184
+ # @return [Hash] Parsed body, or empty hash if nil/empty
185
+ # @api private
186
+ def parse_body(body)
187
+ return {} if body.nil? || body.empty?
188
+
189
+ JSON.parse(body, symbolize_names: true)
190
+ rescue JSON::ParserError
191
+ { raw: body }
192
+ end
193
+
194
+ # Raises the appropriate API error for a status code.
195
+ #
196
+ # @param status [Integer] HTTP status code
197
+ # @param body [Hash] Parsed response body
198
+ # @param headers [Hash] Response headers
199
+ # @raise [APIError] Appropriate error subclass
200
+ # @api private
201
+ def raise_api_error(status, body, headers)
202
+ error_class = ERROR_CODE_MAP[status] || (status >= 500 ? ServerError : APIError)
203
+ message = body[:message] || body[:error] || "Unknown error"
204
+ raise error_class.new(message, status: status, body: body, headers: headers)
205
+ end
206
+ end
207
+ end
208
+ end
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ReveAI
4
+ # API resource classes.
5
+ #
6
+ # Each resource class provides methods for a related set of API operations.
7
+ module Resources
8
+ # Base class for API resources with common validation methods.
9
+ #
10
+ # Provides HTTP client access and input validation for all resource classes.
11
+ #
12
+ # @abstract Subclass and implement endpoint-specific methods
13
+ # @api private
14
+ class Base
15
+ # @return [Client] The client instance for this resource
16
+ attr_reader :client
17
+
18
+ # Creates a new resource instance.
19
+ #
20
+ # @param client [Client] The API client
21
+ # @api private
22
+ def initialize(client)
23
+ @client = client
24
+ end
25
+
26
+ protected
27
+
28
+ # Returns the HTTP client for making requests.
29
+ #
30
+ # @return [HTTP::Client] HTTP client instance
31
+ # @api private
32
+ def http_client
33
+ client.http_client
34
+ end
35
+
36
+ # Returns the client configuration.
37
+ #
38
+ # @return [Configuration] Configuration instance
39
+ # @api private
40
+ def configuration
41
+ client.configuration
42
+ end
43
+
44
+ # Makes a POST request to the API.
45
+ #
46
+ # @param path [String] API endpoint path
47
+ # @param body [Hash] Request body
48
+ # @return [Response] API response
49
+ # @api private
50
+ def post(path, body = {})
51
+ http_client.post(path, body)
52
+ end
53
+
54
+ # Validates a text prompt.
55
+ #
56
+ # @param prompt [String] The prompt to validate
57
+ # @param field_name [String] Name for error messages (default: "Prompt")
58
+ # @raise [ValidationError] if prompt is nil, empty, or exceeds max length
59
+ # @api private
60
+ def validate_prompt!(prompt, field_name: "Prompt")
61
+ raise ValidationError, "#{field_name} is required" if prompt.nil? || prompt.empty?
62
+
63
+ max_length = Configuration::MAX_PROMPT_LENGTH
64
+ return unless prompt.length > max_length
65
+
66
+ raise ValidationError, "#{field_name} exceeds maximum length of #{max_length} characters"
67
+ end
68
+
69
+ # Validates an aspect ratio value.
70
+ #
71
+ # @param aspect_ratio [String, nil] The aspect ratio to validate
72
+ # @raise [ValidationError] if aspect ratio is invalid
73
+ # @api private
74
+ def validate_aspect_ratio!(aspect_ratio)
75
+ return if aspect_ratio.nil?
76
+
77
+ valid_ratios = Configuration::VALID_ASPECT_RATIOS
78
+ return if valid_ratios.include?(aspect_ratio)
79
+
80
+ raise ValidationError, "Invalid aspect_ratio '#{aspect_ratio}'. Must be one of: #{valid_ratios.join(", ")}"
81
+ end
82
+
83
+ # Validates a single reference image.
84
+ #
85
+ # @param image [String] Base64 encoded image data
86
+ # @raise [ValidationError] if image is nil or empty
87
+ # @api private
88
+ def validate_reference_image!(image)
89
+ raise ValidationError, "Reference image is required" if image.nil? || image.empty?
90
+ end
91
+
92
+ # Validates an array of reference images.
93
+ #
94
+ # @param images [Array<String>] Array of base64 encoded images
95
+ # @raise [ValidationError] if images is nil, empty, exceeds max, or contains empty elements
96
+ # @api private
97
+ def validate_reference_images!(images)
98
+ raise ValidationError, "Reference images are required" if images.nil? || images.empty?
99
+
100
+ max_images = Configuration::MAX_REFERENCE_IMAGES
101
+ raise ValidationError, "Maximum #{max_images} reference images allowed" if images.length > max_images
102
+
103
+ images.each_with_index do |image, index|
104
+ raise ValidationError, "Reference image at index #{index} is empty" if image.nil? || image.empty?
105
+ end
106
+ end
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,184 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ReveAI
4
+ module Resources
5
+ # Image generation, editing, and remixing operations.
6
+ #
7
+ # Provides methods for creating images from text prompts, editing existing
8
+ # images, and remixing multiple reference images into new compositions.
9
+ #
10
+ # @example Generate an image from text
11
+ # client = ReveAI::Client.new(api_key: "your-key")
12
+ # result = client.images.create(
13
+ # prompt: "A sunset over mountains with a lake in the foreground",
14
+ # aspect_ratio: "16:9"
15
+ # )
16
+ # puts result.base64 # Base64 encoded PNG
17
+ #
18
+ # @example Edit an existing image
19
+ # result = client.images.edit(
20
+ # edit_instruction: "Make the sky more dramatic with storm clouds",
21
+ # reference_image: base64_encoded_original
22
+ # )
23
+ #
24
+ # @example Remix multiple images
25
+ # result = client.images.remix(
26
+ # prompt: "Combine the style of <img>1</img> with the subject of <img>2</img>",
27
+ # reference_images: [style_image_base64, subject_image_base64]
28
+ # )
29
+ #
30
+ # @note All images are returned as base64 encoded PNG data.
31
+ # @see https://api.reve.com/console/docs Reve API Documentation
32
+ class Images < Base
33
+ # @return [String] API endpoint for image creation
34
+ CREATE_ENDPOINT = "/v1/image/create"
35
+
36
+ # @return [String] API endpoint for image editing
37
+ EDIT_ENDPOINT = "/v1/image/edit"
38
+
39
+ # @return [String] API endpoint for image remixing
40
+ REMIX_ENDPOINT = "/v1/image/remix"
41
+
42
+ # Generates an image from a text prompt.
43
+ #
44
+ # @param prompt [String] Text description of the desired image (max 2560 chars)
45
+ # @param aspect_ratio [String, nil] Output aspect ratio (defaults to API default)
46
+ # @param version [String, nil] Model version to use (defaults to "latest")
47
+ #
48
+ # @option aspect_ratio [String] "16:9" Widescreen landscape
49
+ # @option aspect_ratio [String] "9:16" Portrait (phone)
50
+ # @option aspect_ratio [String] "3:2" Classic landscape
51
+ # @option aspect_ratio [String] "2:3" Classic portrait
52
+ # @option aspect_ratio [String] "4:3" Standard landscape
53
+ # @option aspect_ratio [String] "3:4" Standard portrait
54
+ # @option aspect_ratio [String] "1:1" Square
55
+ #
56
+ # @return [ImageResponse] Response containing base64 encoded image
57
+ #
58
+ # @raise [ValidationError] if prompt is empty or exceeds max length
59
+ # @raise [ValidationError] if aspect_ratio is invalid
60
+ # @raise [BadRequestError] if API rejects the request
61
+ # @raise [UnauthorizedError] if API key is invalid
62
+ # @raise [InsufficientCreditsError] if account has no credits
63
+ # @raise [RateLimitError] if rate limit is exceeded
64
+ #
65
+ # @example Basic usage
66
+ # result = client.images.create(prompt: "A cat wearing a top hat")
67
+ #
68
+ # @example With aspect ratio
69
+ # result = client.images.create(
70
+ # prompt: "A panoramic mountain landscape",
71
+ # aspect_ratio: "16:9"
72
+ # )
73
+ #
74
+ # @example Save to file
75
+ # result = client.images.create(prompt: "A sunset")
76
+ # File.binwrite("image.png", Base64.decode64(result.base64))
77
+ #
78
+ # @see https://api.reve.com/console/docs#/Image/create_v1_image_create_post
79
+ def create(prompt:, aspect_ratio: nil, version: nil)
80
+ validate_prompt!(prompt)
81
+ validate_aspect_ratio!(aspect_ratio)
82
+
83
+ body = { prompt: prompt }
84
+ body[:aspect_ratio] = aspect_ratio if aspect_ratio
85
+ body[:version] = version if version
86
+
87
+ response = post(CREATE_ENDPOINT, body)
88
+ ImageResponse.new(status: response.status, headers: response.headers, body: response.body)
89
+ end
90
+
91
+ # Edits an existing image using text instructions.
92
+ #
93
+ # @param edit_instruction [String] Text description of how to edit the image (max 2560 chars)
94
+ # @param reference_image [String] Base64 encoded image to edit
95
+ # @param aspect_ratio [String, nil] Output aspect ratio (defaults to reference image ratio)
96
+ # @param version [String, nil] Model version to use (defaults to "latest")
97
+ #
98
+ # @return [ImageResponse] Response containing base64 encoded edited image
99
+ #
100
+ # @raise [ValidationError] if edit_instruction is empty or exceeds max length
101
+ # @raise [ValidationError] if reference_image is empty
102
+ # @raise [ValidationError] if aspect_ratio is invalid
103
+ # @raise [UnprocessableEntityError] if reference_image is not valid base64
104
+ # @raise [BadRequestError] if API rejects the request
105
+ # @raise [UnauthorizedError] if API key is invalid
106
+ #
107
+ # @example Change colors
108
+ # result = client.images.edit(
109
+ # edit_instruction: "Change the car color from red to blue",
110
+ # reference_image: original_image_base64
111
+ # )
112
+ #
113
+ # @example Add elements
114
+ # result = client.images.edit(
115
+ # edit_instruction: "Add a rainbow in the sky",
116
+ # reference_image: landscape_base64
117
+ # )
118
+ #
119
+ # @see https://api.reve.com/console/docs#/Image/edit_v1_image_edit_post
120
+ def edit(edit_instruction:, reference_image:, aspect_ratio: nil, version: nil)
121
+ validate_prompt!(edit_instruction, field_name: "Edit instruction")
122
+ validate_reference_image!(reference_image)
123
+
124
+ body = { edit_instruction: edit_instruction, reference_image: reference_image }
125
+ body[:aspect_ratio] = aspect_ratio if aspect_ratio
126
+ body[:version] = version if version
127
+
128
+ response = post(EDIT_ENDPOINT, body)
129
+ ImageResponse.new(status: response.status, headers: response.headers, body: response.body)
130
+ end
131
+
132
+ # Creates a new image by remixing multiple reference images.
133
+ #
134
+ # Use `<img>N</img>` tags in the prompt to reference specific images,
135
+ # where N is the 1-based index into the reference_images array.
136
+ #
137
+ # @param prompt [String] Text description with optional image references (max 2560 chars)
138
+ # @param reference_images [Array<String>] Array of base64 encoded images (1-6 images)
139
+ # @param aspect_ratio [String, nil] Output aspect ratio (defaults to model's choice)
140
+ # @param version [String, nil] Model version to use (defaults to "latest")
141
+ #
142
+ # @return [ImageResponse] Response containing base64 encoded remixed image
143
+ #
144
+ # @raise [ValidationError] if prompt is empty or exceeds max length
145
+ # @raise [ValidationError] if reference_images is empty or exceeds 6 images
146
+ # @raise [ValidationError] if any reference image is empty
147
+ # @raise [ValidationError] if aspect_ratio is invalid
148
+ # @raise [BadRequestError] if API rejects the request
149
+ #
150
+ # @example Combine two images
151
+ # result = client.images.remix(
152
+ # prompt: "Combine the landscape from <img>1</img> with the sky from <img>2</img>",
153
+ # reference_images: [landscape_base64, sky_base64]
154
+ # )
155
+ #
156
+ # @example Style transfer
157
+ # result = client.images.remix(
158
+ # prompt: "Apply the artistic style of <img>1</img> to the photo <img>2</img>",
159
+ # reference_images: [artwork_base64, photo_base64]
160
+ # )
161
+ #
162
+ # @example Multiple references
163
+ # result = client.images.remix(
164
+ # prompt: "Create a scene with the dog from <img>1</img>, " \
165
+ # "the background from <img>2</img>, and lighting from <img>3</img>",
166
+ # reference_images: [dog_base64, background_base64, lighting_ref_base64]
167
+ # )
168
+ #
169
+ # @see https://api.reve.com/console/docs#/Image/remix_v1_image_remix_post
170
+ def remix(prompt:, reference_images:, aspect_ratio: nil, version: nil)
171
+ validate_prompt!(prompt)
172
+ validate_reference_images!(reference_images)
173
+ validate_aspect_ratio!(aspect_ratio)
174
+
175
+ body = { prompt: prompt, reference_images: reference_images }
176
+ body[:aspect_ratio] = aspect_ratio if aspect_ratio
177
+ body[:version] = version if version
178
+
179
+ response = post(REMIX_ENDPOINT, body)
180
+ ImageResponse.new(status: response.status, headers: response.headers, body: response.body)
181
+ end
182
+ end
183
+ end
184
+ end
@@ -0,0 +1,136 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ReveAI
4
+ # Base response wrapper for API responses.
5
+ #
6
+ # Provides access to HTTP status, headers, and parsed response body.
7
+ #
8
+ # @see ImageResponse
9
+ class Response
10
+ # @return [Integer] HTTP status code
11
+ attr_reader :status
12
+
13
+ # @return [Hash] Response headers
14
+ attr_reader :headers
15
+
16
+ # @return [Hash] Parsed response body
17
+ attr_reader :body
18
+
19
+ # Creates a new response wrapper.
20
+ #
21
+ # @param status [Integer] HTTP status code
22
+ # @param headers [Hash] Response headers
23
+ # @param body [Hash] Parsed response body
24
+ def initialize(status:, headers:, body:)
25
+ @status = status
26
+ @headers = headers
27
+ @body = body
28
+ end
29
+
30
+ # Checks if the response indicates success (2xx status).
31
+ #
32
+ # @return [Boolean] true if status is between 200 and 299
33
+ def success?
34
+ status >= 200 && status < 300
35
+ end
36
+
37
+ # Returns the request ID for this response.
38
+ #
39
+ # Useful for debugging and support requests.
40
+ #
41
+ # @return [String, nil] Request ID from body or headers
42
+ def request_id
43
+ body[:request_id] || headers["x-reve-request-id"]
44
+ end
45
+ end
46
+
47
+ # Response wrapper for image generation API responses.
48
+ #
49
+ # Provides convenient accessors for image data, version info,
50
+ # content policy status, and credit usage.
51
+ #
52
+ # @example Accessing image data
53
+ # result = client.images.create(prompt: "A sunset")
54
+ # png_data = Base64.decode64(result.base64)
55
+ # File.binwrite("image.png", png_data)
56
+ #
57
+ # @example Checking content policy
58
+ # result = client.images.create(prompt: "...")
59
+ # if result.content_violation?
60
+ # puts "Content policy violated"
61
+ # end
62
+ #
63
+ # @example Tracking credit usage
64
+ # result = client.images.create(prompt: "A cat")
65
+ # puts "Used #{result.credits_used} credits, #{result.credits_remaining} remaining"
66
+ #
67
+ # @see Response
68
+ class ImageResponse < Response
69
+ # Returns the base64 encoded image data.
70
+ #
71
+ # The image is in PNG format. Use Base64.decode64 to get raw bytes.
72
+ #
73
+ # @return [String, nil] Base64 encoded PNG image data
74
+ #
75
+ # @example Save to file
76
+ # require "base64"
77
+ # png_bytes = Base64.decode64(result.image)
78
+ # File.binwrite("output.png", png_bytes)
79
+ def image
80
+ body[:image]
81
+ end
82
+
83
+ # Alias for {#image}.
84
+ #
85
+ # @return [String, nil] Base64 encoded PNG image data
86
+ # @see #image
87
+ def base64
88
+ image
89
+ end
90
+
91
+ # Returns the model version used for generation.
92
+ #
93
+ # @return [String, nil] Model version (e.g., "reve-create@20250915")
94
+ #
95
+ # @example
96
+ # result.version # => "reve-create@20250915"
97
+ def version
98
+ body[:version] || headers["x-reve-version"]
99
+ end
100
+
101
+ # Checks if the generated image violates content policy.
102
+ #
103
+ # @return [Boolean] true if content policy was violated
104
+ #
105
+ # @example
106
+ # if result.content_violation?
107
+ # puts "Warning: Content policy violated"
108
+ # end
109
+ def content_violation?
110
+ body[:content_violation] == true ||
111
+ headers["x-reve-content-violation"] == "true"
112
+ end
113
+
114
+ # Returns the number of credits used for this request.
115
+ #
116
+ # @return [Integer, nil] Credits consumed by this generation
117
+ #
118
+ # @example
119
+ # puts "This request used #{result.credits_used} credits"
120
+ def credits_used
121
+ body[:credits_used] || headers["x-reve-credits-used"]&.to_i
122
+ end
123
+
124
+ # Returns the number of credits remaining after this request.
125
+ #
126
+ # @return [Integer, nil] Remaining credit balance
127
+ #
128
+ # @example
129
+ # if result.credits_remaining < 100
130
+ # puts "Warning: Low credit balance"
131
+ # end
132
+ def credits_remaining
133
+ body[:credits_remaining] || headers["x-reve-credits-remaining"]&.to_i
134
+ end
135
+ end
136
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ReveAI
4
+ # @return [String] Current gem version
5
+ VERSION = "0.1.0"
6
+ end