reve_ai 0.1.1 → 0.2.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3521245e81bb791e68e3bf8bbdd9d4f1c325b621a164e9126091cc7ec349ee99
4
- data.tar.gz: fc4c447d73bf93ad39909b7c51c444b22fe393bb5aa06ba1300a3dac64171fcd
3
+ metadata.gz: 44c9a9191d46d7088bf8888415b2727f2e5587bfb3e6ff6f3d321225aa5f4dad
4
+ data.tar.gz: 1f16304c1c0fd6fc1751a34a6e0dd1853f6f4f4f1f5c48df13e9f50993bc1ae4
5
5
  SHA512:
6
- metadata.gz: dbc3e027851edffb5dc34b1f53719855857f43f512f403ccac146a455d820798f67e40787b210cb46b6032030396b619f5f750875c7c9beebb411afd47d13886
7
- data.tar.gz: 326cc725907f9275e966fdb34072bb03da90909b0d52ed376f092d701d7726bc9148697b8c08c997784fd4c9a6b60d58e2b4a0393fe00ceb4de312b3cb471c00
6
+ metadata.gz: 299ccdcdb8b24b512a165b9b4d765e2d4681b25fd0ddcb0649ed129442634dd68c5447d9b18980561308929e45516dfec95add5585e9304fe6b614cb67cedacd
7
+ data.tar.gz: 732e06bbe67ac1dd62aaff55bb97ab19406fb1b8c0ebf2a2c64141da183eeb0b3b77380720ff58af5954aa38c39bb4294f9e894acbcf84632f5110ea949f247c
data/CHANGELOG.md CHANGED
@@ -5,6 +5,36 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.2.1] - 2026-08-05
9
+
10
+ ### Deprecated
11
+
12
+ - Documented the Reve public API shutdown and the gem's deprecated, unmaintained status
13
+ - Added a RubyGems post-install warning with the API sunset date and repository README link
14
+
15
+ ## [0.2.0] - 2026-07-17
16
+
17
+ ### Added
18
+
19
+ - API v2 image creation via `client.v2.images.create` (`POST /v2/image/create`): ordered `references`
20
+ (`{ data: }` / `{ ref: }` objects addressed from the prompt as `<frame>N</frame>`), the full
21
+ 18-value aspect-ratio set including `auto`, prompts up to 4000 characters, and up to 8 references
22
+ - Experimental layout endpoints under `client.v2.layouts`: `extract` (`POST /v2/image/extract_layout`),
23
+ `create` (`POST /v2/image/create_layout`), and `render` (`POST /v2/image/render_layout`)
24
+ - Effects listing via `client.effects.list` (`GET /v1/image/effect`) with an optional `source` filter
25
+ - `postprocessing:`, `test_time_scaling:`, `accept:`, and `breadcrumb:` options on the v1
26
+ `create`/`edit`/`remix` and v2 endpoints
27
+ - Binary image responses: `accept: "image/png"`, `"image/jpeg"`, or `"image/webp"` returns raw image
28
+ bytes with metadata in `X-Reve-*` headers
29
+ - `ImageResponse#layout` and `ReveAI::LayoutResponse` for the structured layouts returned by v2 endpoints
30
+ - `APIError#params` for the error-specific `params` object returned by the API, and `APIError#error_code`
31
+ now falls back to the `X-Reve-Error-Code` header (binary error responses)
32
+ - GET support in the HTTP layer; retries now cover GET requests as well as POST
33
+
34
+ ### Fixed
35
+
36
+ - Remix `<img>N</img>` YARD documentation corrected to 0-based indexing, matching the API
37
+
8
38
  ## [0.1.0] - 2026-01-04
9
39
 
10
40
  ### Added
data/README.md CHANGED
@@ -5,6 +5,12 @@ Ruby client for the [Reve image generation API](https://api.reve.com/console/doc
5
5
  [![Gem Version](https://badge.fury.io/rb/reve_ai.svg)](https://badge.fury.io/rb/reve_ai)
6
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
7
 
8
+ ## API sunset notice
9
+
10
+ > **Reve's public API sunsets Friday, August 14, 2026.** Existing approved API users retain access through the sunset window; all other users can no longer access it, and new API onboarding is closed. Unused paid credits begin full-refund processing Monday, August 17, 2026.
11
+
12
+ This gem is deprecated and will be unmaintained after the API shutdown. No further feature development or maintenance is planned.
13
+
8
14
  ## Installation
9
15
 
10
16
  ```
@@ -37,6 +43,18 @@ response.credits_used # => 18
37
43
  response.credits_remaining # => 982
38
44
  ```
39
45
 
46
+ Save the image to a file:
47
+
48
+ ```ruby
49
+ require "base64"
50
+
51
+ image_data = Base64.decode64(response.image)
52
+
53
+ File.open("output.png", "wb") do |f|
54
+ f.write(image_data)
55
+ end
56
+ ```
57
+
40
58
  With aspect ratio:
41
59
 
42
60
  ```ruby
@@ -76,7 +94,7 @@ response.image # => "base64editeddata..."
76
94
  response.version # => "reve-edit@20250915"
77
95
  ```
78
96
 
79
- Available versions for edit: `latest`, `latest-fast`, `reve-edit@20250915`, `reve-edit-fast@20251030`
97
+ Available versions for edit: `latest`, `latest-fast`, `reve-edit@20250915`, `reve-edit-fast@20251030`, `reve-edit-passthrough@20260625`
80
98
 
81
99
  ### Remix Images
82
100
 
@@ -104,6 +122,227 @@ response.version # => "reve-remix@20250915"
104
122
 
105
123
  Available versions for remix: `latest`, `latest-fast`, `reve-remix@20250915`, `reve-remix-fast@20251030`
106
124
 
125
+ ### Postprocessing and Effects
126
+
127
+ The v1 `create`/`edit`/`remix` methods and `client.v2.images.create` accept these optional keyword arguments (`client.v2.layouts.render` also supports `postprocessing:` and `accept:`, and every endpoint supports `breadcrumb:`):
128
+
129
+ | Option | Values | Description |
130
+ |--------|--------|-------------|
131
+ | `postprocessing:` | Array of operation Hashes | Operations applied to the generated image (see below) |
132
+ | `test_time_scaling:` | `1`-`15` | Spend more effort (and credits) on the request; clamped server-side. Not recommended for v2 models |
133
+ | `accept:` | `"image/png"`, `"image/jpeg"`, `"image/webp"` | Return raw image bytes instead of JSON; metadata moves to `X-Reve-*` headers |
134
+ | `breadcrumb:` | String | Request-tracking tag, searchable on the Reve Usage page; ignored by the API |
135
+
136
+ Supported postprocessing operations:
137
+
138
+ | Operation | Parameters | Notes |
139
+ |-----------|------------|-------|
140
+ | `upscale` | `upscale_factor` (integer, 1-4) | Adds credits cost; a 4x upscale is large |
141
+ | `remove_background` | none | Adds credits cost; works best with a clear subject |
142
+ | `fit_image` | `max_dim`, `max_width`, and/or `max_height` (max 4096) | Free; scales down preserving aspect ratio |
143
+ | `effect` | `effect_name`, optional `effect_parameters` | Applies an effect saved in your project |
144
+
145
+ ```ruby
146
+ response = client.images.create(
147
+ prompt: "A beautiful sunset over mountains",
148
+ postprocessing: [
149
+ { process: "upscale", upscale_factor: 2 },
150
+ { process: "fit_image", max_dim: 2048 }
151
+ ],
152
+ test_time_scaling: 3,
153
+ breadcrumb: "homepage-hero"
154
+ )
155
+ ```
156
+
157
+ Effect parameter overrides use a nested `{ filterId => { uniformId => value } }` format; omitted parameters fall back to the effect's saved defaults:
158
+
159
+ ```ruby
160
+ response = client.images.create(
161
+ prompt: "A high-quality photo of a wine bottle",
162
+ postprocessing: [
163
+ {
164
+ process: "effect",
165
+ effect_name: "adjustments",
166
+ effect_parameters: {
167
+ "adjustments" => {
168
+ "u_exposure" => 42,
169
+ "u_contrast" => -38,
170
+ "u_vibrance" => 64
171
+ }
172
+ }
173
+ }
174
+ ]
175
+ )
176
+ ```
177
+
178
+ List the effects available to your project — the returned `name` values are valid `effect_name` arguments:
179
+
180
+ ```ruby
181
+ response = client.effects.list
182
+
183
+ response.body[:effects].each do |effect|
184
+ puts "#{effect[:name]} (#{effect[:source]})" # source: "saved" (project) or "builtin" (preset)
185
+ end
186
+
187
+ # Optional source filter: "all" (default), "project" (saved only), "preset" (builtin only)
188
+ presets = client.effects.list(source: "preset")
189
+ ```
190
+
191
+ ### API v2
192
+
193
+ The v2 API collapses the three v1 workflows into a single endpoint, `client.v2.images.create`, which takes ordered reference images and returns a structured `layout` alongside the image. The v1 endpoints above remain live and unchanged.
194
+
195
+ - Prompts up to 4000 characters, and up to 8 reference images
196
+ - The full 18-value aspect-ratio set, including `auto` (the default) — see [Validation Constraints](#validation-constraints)
197
+ - References are objects with exactly one of `data:` (base64-encoded image) or `ref:` (`"id:<uuid>"` for a stored image or generation, `"reference:@<name>"` for a named project reference)
198
+ - Address a reference from the prompt as `<frame>N</frame>` (0-based: the first reference is `<frame>0</frame>`)
199
+ - v2 images are significantly larger than v1 — cap the output with a free `fit_image` step: `postprocessing: [{ process: "fit_image", max_dim: 2048 }]`
200
+
201
+ Generate an image from text:
202
+
203
+ ```ruby
204
+ response = client.v2.images.create(prompt: "A beautiful sunset over mountains")
205
+
206
+ response.image # => "base64encodeddata..."
207
+ response.version # => "latest" (v2 responses currently always report "latest")
208
+ ```
209
+
210
+ Edit an image (pass it as the first reference and address it as `<frame>0</frame>`):
211
+
212
+ ```ruby
213
+ require "base64"
214
+
215
+ image_data = Base64.strict_encode64(File.read("my-image.png"))
216
+
217
+ response = client.v2.images.create(
218
+ prompt: "Add dramatic clouds to the sky of <frame>0</frame>",
219
+ references: [{ data: image_data }]
220
+ )
221
+ ```
222
+
223
+ Combine multiple references:
224
+
225
+ ```ruby
226
+ person = Base64.strict_encode64(File.read("person.png"))
227
+ background = Base64.strict_encode64(File.read("background.png"))
228
+
229
+ response = client.v2.images.create(
230
+ prompt: "The person from <frame>0</frame> standing in the scene from <frame>1</frame>",
231
+ references: [{ data: person }, { data: background }],
232
+ aspect_ratio: "21:9"
233
+ )
234
+ ```
235
+
236
+ Every v2 JSON response includes the layout the model generated for the image:
237
+
238
+ ```ruby
239
+ response = client.v2.images.create(prompt: "A serene mountain landscape at sunset")
240
+
241
+ response.layout # => { prompt: "...", regions: [...], width: 4672, height: 3520 }
242
+
243
+ response.layout[:regions].each do |region|
244
+ puts "#{region[:label]}: #{region[:prompt]}"
245
+ end
246
+ ```
247
+
248
+ Request a binary image instead of JSON with `accept:` — `response.image` then holds the raw bytes, and the metadata arrives via `X-Reve-*` headers:
249
+
250
+ ```ruby
251
+ response = client.v2.images.create(
252
+ prompt: "A beautiful sunset over mountains",
253
+ accept: "image/webp"
254
+ )
255
+
256
+ File.binwrite("mountains.webp", response.image) # no Base64 decoding needed
257
+
258
+ response.credits_used # => 150
259
+ ```
260
+
261
+ Pass `version:` to pin a model alias — v2 aliases observed: `latest` (default), `reve-v2-create@260601`.
262
+
263
+ #### Migrating from v1 to v2
264
+
265
+ All three v1 workflows map to `client.v2.images.create`:
266
+
267
+ Create — keep `prompt` and `aspect_ratio`:
268
+
269
+ ```ruby
270
+ # v1
271
+ client.images.create(prompt: "A serene mountain landscape at sunset",
272
+ aspect_ratio: "16:9", version: "latest")
273
+
274
+ # v2
275
+ client.v2.images.create(prompt: "A serene mountain landscape at sunset",
276
+ aspect_ratio: "16:9")
277
+ ```
278
+
279
+ Edit — the edited image becomes `references[0]`, addressed as `<frame>0</frame>`:
280
+
281
+ ```ruby
282
+ # v1
283
+ client.images.edit(edit_instruction: "Remove the people in the background.",
284
+ reference_image: image_b64)
285
+
286
+ # v2
287
+ client.v2.images.create(prompt: "Remove the people in the background of <frame>0</frame>.",
288
+ references: [{ data: image_b64 }])
289
+ ```
290
+
291
+ Remix — `<img>N</img>` tags become `<frame>N</frame>` (both 0-based) and bare base64 strings become `{ data: }` objects:
292
+
293
+ ```ruby
294
+ # v1
295
+ client.images.remix(prompt: "The woman from <img>0</img> driving the car from <img>1</img>.",
296
+ reference_images: [woman_b64, car_b64], aspect_ratio: "1:1")
297
+
298
+ # v2
299
+ client.v2.images.create(prompt: "The woman from <frame>0</frame> driving the car from <frame>1</frame>.",
300
+ references: [{ data: woman_b64 }, { data: car_b64 }], aspect_ratio: "1:1")
301
+ ```
302
+
303
+ #### Experimental Layout Endpoints
304
+
305
+ The layout endpoints are **experimental**: they require care and experimentation to achieve good results, are best suited for agents and custom tooling, and may change. For simple generation and prompt-based editing, use `client.v2.images.create`. They return JSON only (`render` also supports `accept:`) and commonly take 10-40 seconds (`render`: 40-80 seconds) — keep timeouts at 120 seconds or more.
306
+
307
+ A layout is a Hash with an optional overall `prompt`, optional `width`/`height`, and a `regions` array; each region carries a `label`, a regional `prompt`, and a normalized `bbox` (`x0`, `y0`, `x1`, `y1` in 0.0-1.0, top-left origin).
308
+
309
+ Extract a layout from an image:
310
+
311
+ ```ruby
312
+ image = { data: Base64.strict_encode64(File.read("photo.jpg")) }
313
+
314
+ response = client.v2.layouts.extract(image: image)
315
+ response.layout # => { prompt: "...", regions: [...], width: 4672, height: 3520 }
316
+ ```
317
+
318
+ Create a layout from a prompt, without rendering an image:
319
+
320
+ ```ruby
321
+ response = client.v2.layouts.create(prompt: "a person at a cafe", aspect_ratio: "3:2")
322
+ layout = response.layout
323
+ ```
324
+
325
+ Render an image from a layout:
326
+
327
+ ```ruby
328
+ response = client.v2.layouts.render(layout: layout)
329
+ response.image # => "base64encodeddata..."
330
+ ```
331
+
332
+ Unlike v2 create, the layout endpoints take compound references — each entry may contain any subset of `image:`, `layout:`, and `prompt:`:
333
+
334
+ ```ruby
335
+ response = client.v2.layouts.create(
336
+ prompt: "Put the bottle on a wooden table",
337
+ references: [
338
+ { image: { data: bottle_b64 }, prompt: "the product to feature" },
339
+ { layout: saved_layout }
340
+ ]
341
+ )
342
+ ```
343
+
344
+ `create` also accepts `commands:` — ordered imperative edits (`add`, `place`, `shift`, `remove`, `keep`, `change`) with normalized positions; see the [API docs](https://api.reve.com/console/docs) for the command shapes.
345
+
107
346
  ### Rails
108
347
 
109
348
  Create `config/initializers/reve_ai.rb`:
@@ -155,6 +394,19 @@ rescue ReveAI::ConnectionError => e
155
394
  end
156
395
  ```
157
396
 
397
+ Every API error also exposes the details returned by the API:
398
+
399
+ ```ruby
400
+ rescue ReveAI::APIError => e
401
+ e.error_code # => "PROMPT_TOO_LONG"
402
+ e.params # => Hash of error-specific parameters, or nil
403
+ e.status # => 400
404
+ e.request_id # => "rsid-..."
405
+ end
406
+ ```
407
+
408
+ For binary image requests (`accept: "image/*"`), error responses carry a small grey image and the error code arrives in the `X-Reve-Error-Code` header — `#error_code` reads it from either the response body or the header.
409
+
158
410
  ### Content Moderation
159
411
 
160
412
  The API may flag content violations:
@@ -173,20 +425,38 @@ end
173
425
  |--------|---------|-------------|
174
426
  | `api_key` | `ENV["REVE_AI_API_KEY"]` | Your Reve API key |
175
427
  | `base_url` | `https://api.reve.com` | API base URL |
176
- | `timeout` | `120` | Request timeout in seconds |
428
+ | `timeout` | `120` | Request timeout in seconds (the API requires at least 120) |
177
429
  | `open_timeout` | `30` | Connection timeout in seconds |
178
430
  | `max_retries` | `2` | Number of retries for failed requests |
179
431
  | `logger` | `nil` | Logger instance for debugging |
180
432
  | `debug` | `false` | Enable debug logging |
181
433
 
434
+ The API requires client timeouts of at least 120 seconds: image generation and rendering commonly take 40-80 seconds, and the layout endpoints 10-40 seconds. The gem's default of 120 seconds complies. Treat shorter client-side timeouts as cancellations — the server may still finish the request after the client disconnects.
435
+
182
436
  ### Validation Constraints
183
437
 
438
+ v1 endpoints (`create`, `edit`, `remix`):
439
+
184
440
  | Constraint | Value |
185
441
  |------------|-------|
186
442
  | Max prompt length | 2560 characters |
187
443
  | Max reference images (remix) | 6 |
188
444
  | Valid aspect ratios | 16:9, 9:16, 3:2, 2:3, 4:3, 3:4, 1:1 |
189
445
 
446
+ v2 endpoints (`client.v2`):
447
+
448
+ | Constraint | Value |
449
+ |------------|-------|
450
+ | Max prompt length | 4000 characters |
451
+ | Max reference images | 8 |
452
+ | Valid aspect ratios | 4:1, 3:1, 21:9, 2:1, 17:9, 16:9, 3:2, 4:3, 5:4, 1:1, 4:5, 3:4, 2:3, 9:16, 1:2, 1:3, 1:4, auto (default: auto) |
453
+
454
+ Input image limits (any endpoint that accepts images):
455
+
456
+ - Formats: WEBP, JPEG, PNG, GIF, TIFF, AVIF — base64-encoded in JSON
457
+ - Per image: at most 40 MB and 33,554,432 pixels, with neither dimension exceeding 8192 pixels
458
+ - Per call: at most 100 MB and 50,331,648 pixels across all images
459
+
190
460
  ## Development
191
461
 
192
462
  ```
@@ -195,15 +465,13 @@ bundle exec rake test
195
465
  bundle exec rubocop
196
466
  ```
197
467
 
198
- ## Release
468
+ ## Release
199
469
 
200
- ```sh
201
- bundle exec rake release
202
- ```
470
+ This gem is deprecated, and no further feature releases are planned.
203
471
 
204
472
  ## Contributing
205
473
 
206
- Bug reports and pull requests are welcome on GitHub at https://github.com/dpaluy/reve_ai.
474
+ This project is deprecated and will be unmaintained after the API shutdown. Contributions for new features or ongoing maintenance are no longer being accepted.
207
475
 
208
476
  ## License
209
477
 
@@ -64,13 +64,40 @@ module ReveAI
64
64
  #
65
65
  # @example Remix images
66
66
  # result = client.images.remix(
67
- # prompt: "Combine <img>1</img> and <img>2</img> into one scene",
67
+ # prompt: "Combine <img>0</img> and <img>1</img> into one scene",
68
68
  # reference_images: [image1_base64, image2_base64]
69
69
  # )
70
70
  def images
71
71
  @images ||= Resources::Images.new(self)
72
72
  end
73
73
 
74
+ # Returns the Effects resource for listing available effects.
75
+ #
76
+ # @return [Resources::Effects] Effects listing interface
77
+ # @see Resources::Effects
78
+ #
79
+ # @example List all effects available to the project
80
+ # result = client.effects.list
81
+ # result.body[:effects].each { |effect| puts effect[:name] }
82
+ def effects
83
+ @effects ||= Resources::Effects.new(self)
84
+ end
85
+
86
+ # Returns the v2 API namespace (image create and layout endpoints).
87
+ #
88
+ # @return [Resources::V2] v2 API operations interface
89
+ # @see Resources::V2
90
+ #
91
+ # @example Generate an image with the v2 API
92
+ # result = client.v2.images.create(
93
+ # prompt: "Remove the people in the background of <frame>0</frame>.",
94
+ # references: [{ data: base64_encoded_image }]
95
+ # )
96
+ # result.layout # => { prompt: "...", regions: [...] }
97
+ def v2
98
+ @v2 ||= Resources::V2.new(self)
99
+ end
100
+
74
101
  # Returns the HTTP client for making API requests.
75
102
  #
76
103
  # @return [HTTP::Client] HTTP client instance
@@ -34,7 +34,7 @@ module ReveAI
34
34
  # @return [Integer] Default number of retry attempts for failed requests
35
35
  DEFAULT_MAX_RETRIES = 2
36
36
 
37
- # @return [Array<String>] Valid aspect ratios for image generation
37
+ # @return [Array<String>] Valid aspect ratios for legacy v1 image generation
38
38
  VALID_ASPECT_RATIOS = %w[16:9 9:16 3:2 2:3 4:3 3:4 1:1].freeze
39
39
 
40
40
  # @return [Integer] Maximum allowed prompt length in characters
@@ -43,6 +43,15 @@ module ReveAI
43
43
  # @return [Integer] Maximum number of reference images for remix operations
44
44
  MAX_REFERENCE_IMAGES = 6
45
45
 
46
+ # @return [Array<String>] Valid aspect ratios for image generation
47
+ ASPECT_RATIOS = %w[4:1 3:1 21:9 2:1 17:9 16:9 3:2 4:3 5:4 1:1 4:5 3:4 2:3 9:16 1:2 1:3 1:4 auto].freeze
48
+
49
+ # @return [Integer] Maximum allowed prompt length in characters for v2 endpoints
50
+ V2_MAX_PROMPT_LENGTH = 4000
51
+
52
+ # @return [Integer] Maximum number of references for v2 create operations
53
+ V2_MAX_REFERENCES = 8
54
+
46
55
  # @return [String, nil] Reve API key for authentication
47
56
  attr_accessor :api_key
48
57
 
@@ -72,22 +72,27 @@ module ReveAI
72
72
  # @return [Integer, nil] HTTP status code
73
73
  attr_reader :status
74
74
 
75
- # @return [Hash] Response body parsed as Hash
75
+ # @return [Hash, String] Response body parsed as Hash, or raw String
76
+ # for binary error responses (e.g., grey image bodies)
76
77
  attr_reader :body
77
78
 
78
79
  # @return [Hash] Response headers
79
80
  attr_reader :headers
80
81
 
82
+ # @return [Hash, nil] Additional error details from the response body
83
+ attr_reader :params
84
+
81
85
  # Creates a new API error instance.
82
86
  #
83
87
  # @param message [String, nil] Error message
84
88
  # @param status [Integer, nil] HTTP status code
85
- # @param body [Hash, nil] Response body
89
+ # @param body [Hash, String, nil] Response body
86
90
  # @param headers [Hash, nil] Response headers
87
91
  def initialize(message = nil, status: nil, body: nil, headers: nil)
88
92
  @status = status
89
93
  @body = body || {}
90
94
  @headers = headers || {}
95
+ @params = @body.is_a?(Hash) ? @body[:params] : nil
91
96
  super(message)
92
97
  end
93
98
 
@@ -100,11 +105,17 @@ module ReveAI
100
105
  headers["x-reve-request-id"]
101
106
  end
102
107
 
103
- # Returns the error code from the response body.
108
+ # Returns the error code for this error.
109
+ #
110
+ # Read from the response body when present, falling back to the
111
+ # X-Reve-Error-Code header: with an image Accept header, the API answers
112
+ # errors with a small grey image body and no JSON error code.
104
113
  #
105
114
  # @return [String, nil] Error code (e.g., "PROMPT_TOO_LONG", "INVALID_API_KEY")
106
115
  def error_code
107
- body[:error_code]
116
+ return body[:error_code] if body.is_a?(Hash) && body[:error_code]
117
+
118
+ headers["x-reve-error-code"]
108
119
  end
109
120
  end
110
121