imago 0.1.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 +7 -0
- data/.rubocop.yml +48 -0
- data/.ruby-version +1 -0
- data/README.md +190 -0
- data/Rakefile +10 -0
- data/imago.gemspec +35 -0
- data/lib/imago/client.rb +43 -0
- data/lib/imago/errors.rb +25 -0
- data/lib/imago/providers/base.rb +82 -0
- data/lib/imago/providers/gemini.rb +105 -0
- data/lib/imago/providers/openai.rb +79 -0
- data/lib/imago/providers/xai.rb +63 -0
- data/lib/imago/version.rb +5 -0
- data/lib/imago.rb +20 -0
- metadata +86 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 7ab8cd4f924113a9ba92f0f6918b137c0132ebda6fe9d8d2006a62e158d1f987
|
|
4
|
+
data.tar.gz: 78f6726394dac041f2d82d1e068012767c94a616267801ac4cf597540e070884
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: d9b288487b3d77b17b70fe5289a9b63744e454f2bf01358304b846e0eb6cef28326287ac319887a1e539ea7f15a16ad22fc19f6579da47118d0fa6685b8d51e0
|
|
7
|
+
data.tar.gz: 2f3d13bd6d3bce751755c6a41e333139413cc28a1f31e6fde0c09c7671a592a9d87d731336200733112ad15d4886376708c48f25decf1008126866615d6f8bfa
|
data/.rubocop.yml
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
require:
|
|
2
|
+
- rubocop-rspec
|
|
3
|
+
|
|
4
|
+
AllCops:
|
|
5
|
+
TargetRubyVersion: 3.0
|
|
6
|
+
NewCops: enable
|
|
7
|
+
SuggestExtensions: false
|
|
8
|
+
Exclude:
|
|
9
|
+
- 'vendor/**/*'
|
|
10
|
+
- 'tmp/**/*'
|
|
11
|
+
|
|
12
|
+
Capybara:
|
|
13
|
+
Enabled: false
|
|
14
|
+
|
|
15
|
+
Capybara/RSpec:
|
|
16
|
+
Enabled: false
|
|
17
|
+
|
|
18
|
+
Capybara/RSpec/PredicateMatcher:
|
|
19
|
+
Enabled: false
|
|
20
|
+
|
|
21
|
+
FactoryBot:
|
|
22
|
+
Enabled: false
|
|
23
|
+
|
|
24
|
+
RSpecRails:
|
|
25
|
+
Enabled: false
|
|
26
|
+
|
|
27
|
+
Style/Documentation:
|
|
28
|
+
Enabled: false
|
|
29
|
+
|
|
30
|
+
Metrics/BlockLength:
|
|
31
|
+
Exclude:
|
|
32
|
+
- 'spec/**/*'
|
|
33
|
+
- '*.gemspec'
|
|
34
|
+
|
|
35
|
+
RSpec/ExampleLength:
|
|
36
|
+
Max: 15
|
|
37
|
+
|
|
38
|
+
RSpec/MultipleExpectations:
|
|
39
|
+
Max: 5
|
|
40
|
+
|
|
41
|
+
RSpec/NestedGroups:
|
|
42
|
+
Max: 4
|
|
43
|
+
|
|
44
|
+
Layout/LineLength:
|
|
45
|
+
Max: 120
|
|
46
|
+
|
|
47
|
+
RSpec/MessageSpies:
|
|
48
|
+
EnforcedStyle: receive
|
data/.ruby-version
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ruby-4.0.0
|
data/README.md
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
# Imago
|
|
2
|
+
|
|
3
|
+
A unified Ruby interface for multiple image generation AI providers. Generate images using OpenAI (DALL-E), Google Gemini (Imagen), and xAI (Grok) through a single, consistent API.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
Add this line to your application's Gemfile:
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
gem 'imago'
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
And then execute:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
bundle install
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Or install it yourself as:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
gem install imago
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Configuration
|
|
26
|
+
|
|
27
|
+
Set API keys via environment variables:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
export OPENAI_API_KEY="your-openai-key"
|
|
31
|
+
export GEMINI_API_KEY="your-gemini-key"
|
|
32
|
+
export XAI_API_KEY="your-xai-key"
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Or pass them directly when creating a client.
|
|
36
|
+
|
|
37
|
+
## Usage
|
|
38
|
+
|
|
39
|
+
### Basic Usage
|
|
40
|
+
|
|
41
|
+
```ruby
|
|
42
|
+
require 'imago'
|
|
43
|
+
|
|
44
|
+
# Create a client for OpenAI
|
|
45
|
+
client = Imago.new(provider: :openai)
|
|
46
|
+
|
|
47
|
+
# Generate an image
|
|
48
|
+
result = client.generate("A serene mountain landscape at sunset")
|
|
49
|
+
puts result[:images].first[:url]
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Specifying a Model
|
|
53
|
+
|
|
54
|
+
```ruby
|
|
55
|
+
# Use a specific model
|
|
56
|
+
client = Imago.new(provider: :openai, model: 'dall-e-2')
|
|
57
|
+
|
|
58
|
+
result = client.generate("A cat wearing a hat")
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Passing API Key Directly
|
|
62
|
+
|
|
63
|
+
```ruby
|
|
64
|
+
client = Imago.new(
|
|
65
|
+
provider: :openai,
|
|
66
|
+
api_key: 'sk-your-api-key'
|
|
67
|
+
)
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Provider-Specific Options
|
|
71
|
+
|
|
72
|
+
Each provider supports different options:
|
|
73
|
+
|
|
74
|
+
#### OpenAI (DALL-E)
|
|
75
|
+
|
|
76
|
+
```ruby
|
|
77
|
+
client = Imago.new(provider: :openai)
|
|
78
|
+
|
|
79
|
+
result = client.generate("A futuristic city", {
|
|
80
|
+
size: '1024x1024', # '256x256', '512x512', '1024x1024', '1792x1024', '1024x1792'
|
|
81
|
+
quality: 'hd', # 'standard' or 'hd'
|
|
82
|
+
n: 1, # Number of images (1-10)
|
|
83
|
+
response_format: 'url' # 'url' or 'b64_json'
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
# Access the generated image
|
|
87
|
+
result[:images].each do |image|
|
|
88
|
+
puts image[:url]
|
|
89
|
+
puts image[:revised_prompt] # DALL-E 3 may revise your prompt
|
|
90
|
+
end
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
#### Google Gemini (Imagen)
|
|
94
|
+
|
|
95
|
+
```ruby
|
|
96
|
+
client = Imago.new(provider: :gemini)
|
|
97
|
+
|
|
98
|
+
result = client.generate("A tropical beach", {
|
|
99
|
+
n: 1, # Number of images
|
|
100
|
+
aspect_ratio: '16:9', # Aspect ratio
|
|
101
|
+
negative_prompt: 'people', # What to exclude
|
|
102
|
+
seed: 12345 # For reproducibility
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
# Gemini returns base64-encoded images
|
|
106
|
+
result[:images].each do |image|
|
|
107
|
+
puts image[:base64]
|
|
108
|
+
puts image[:mime_type]
|
|
109
|
+
end
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
#### xAI (Grok)
|
|
113
|
+
|
|
114
|
+
```ruby
|
|
115
|
+
client = Imago.new(provider: :xai)
|
|
116
|
+
|
|
117
|
+
result = client.generate("A robot playing chess", {
|
|
118
|
+
n: 1,
|
|
119
|
+
response_format: 'url' # 'url' or 'b64_json'
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
result[:images].each do |image|
|
|
123
|
+
puts image[:url]
|
|
124
|
+
end
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### Listing Available Models
|
|
128
|
+
|
|
129
|
+
```ruby
|
|
130
|
+
client = Imago.new(provider: :openai)
|
|
131
|
+
|
|
132
|
+
# Returns available image generation models
|
|
133
|
+
models = client.models
|
|
134
|
+
# => ["dall-e-3", "dall-e-2", "gpt-image-1"]
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
For providers with a models API (OpenAI, Gemini), this fetches from the API and caches the result. For providers without such an endpoint (xAI), it returns a curated list of known models.
|
|
138
|
+
|
|
139
|
+
## Supported Providers
|
|
140
|
+
|
|
141
|
+
Model list last updated: 01/18/2026.
|
|
142
|
+
|
|
143
|
+
| Provider | Models | API Key Env Var |
|
|
144
|
+
|----------|--------|-----------------|
|
|
145
|
+
| OpenAI | `dall-e-3`, `dall-e-2`, `gpt-image-1`, `gpt-image-1.5`, `gpt-image-1-mini` | `OPENAI_API_KEY` |
|
|
146
|
+
| Gemini | `imagen-3.0-generate-002`, `imagen-3.0-generate-001`, `gemini-2.0-flash-exp-image-generation`, `gemini-2.5-flash-image`, `gemini-3-pro-image-preview` | `GEMINI_API_KEY` |
|
|
147
|
+
| xAI | `grok-2-image`, `grok-2-image-1212` | `XAI_API_KEY` |
|
|
148
|
+
|
|
149
|
+
## Error Handling
|
|
150
|
+
|
|
151
|
+
Imago provides specific error classes for different failure scenarios:
|
|
152
|
+
|
|
153
|
+
```ruby
|
|
154
|
+
begin
|
|
155
|
+
client = Imago.new(provider: :openai)
|
|
156
|
+
result = client.generate("A cat")
|
|
157
|
+
rescue Imago::AuthenticationError => e
|
|
158
|
+
puts "Invalid API key: #{e.message}"
|
|
159
|
+
rescue Imago::RateLimitError => e
|
|
160
|
+
puts "Rate limited: #{e.message}"
|
|
161
|
+
rescue Imago::InvalidRequestError => e
|
|
162
|
+
puts "Bad request: #{e.message}"
|
|
163
|
+
rescue Imago::ApiError => e
|
|
164
|
+
puts "API error: #{e.message}"
|
|
165
|
+
puts "Status code: #{e.status_code}"
|
|
166
|
+
rescue Imago::ConfigurationError => e
|
|
167
|
+
puts "Configuration error: #{e.message}"
|
|
168
|
+
rescue Imago::ProviderNotFoundError => e
|
|
169
|
+
puts "Unknown provider: #{e.message}"
|
|
170
|
+
end
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
## Development
|
|
174
|
+
|
|
175
|
+
After checking out the repo, run `bundle install` to install dependencies. Then, run `rake spec` to run the tests. You can also run `rake rubocop` for linting.
|
|
176
|
+
|
|
177
|
+
```bash
|
|
178
|
+
bundle install
|
|
179
|
+
bundle exec rake spec # Run tests
|
|
180
|
+
bundle exec rake rubocop # Run linter
|
|
181
|
+
bundle exec rake # Run both
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
## Contributing
|
|
185
|
+
|
|
186
|
+
Bug reports and pull requests are welcome on GitHub.
|
|
187
|
+
|
|
188
|
+
## License
|
|
189
|
+
|
|
190
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
data/imago.gemspec
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'lib/imago/version'
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.name = 'imago'
|
|
7
|
+
spec.version = Imago::VERSION
|
|
8
|
+
spec.authors = ['Your Name']
|
|
9
|
+
spec.email = ['your.email@example.com']
|
|
10
|
+
|
|
11
|
+
spec.summary = 'A unified Ruby interface for multiple image generation AI providers'
|
|
12
|
+
spec.description = 'Imago provides a simple, unified API to generate images using various AI providers ' \
|
|
13
|
+
'including OpenAI (DALL-E), Google Gemini, and xAI (Grok).'
|
|
14
|
+
spec.homepage = 'https://github.com/example/imago'
|
|
15
|
+
spec.license = 'MIT'
|
|
16
|
+
spec.required_ruby_version = '>= 3.0.0'
|
|
17
|
+
|
|
18
|
+
spec.metadata['homepage_uri'] = spec.homepage
|
|
19
|
+
spec.metadata['source_code_uri'] = spec.homepage
|
|
20
|
+
spec.metadata['changelog_uri'] = "#{spec.homepage}/blob/main/CHANGELOG.md"
|
|
21
|
+
spec.metadata['rubygems_mfa_required'] = 'true'
|
|
22
|
+
|
|
23
|
+
spec.files = Dir.chdir(__dir__) do
|
|
24
|
+
`git ls-files -z`.split("\x0").reject do |f|
|
|
25
|
+
(File.expand_path(f) == __FILE__) ||
|
|
26
|
+
f.start_with?('bin/', 'test/', 'spec/', 'features/', '.git', '.github', 'appveyor', 'Gemfile')
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
spec.bindir = 'exe'
|
|
30
|
+
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
|
|
31
|
+
spec.require_paths = ['lib']
|
|
32
|
+
|
|
33
|
+
spec.add_dependency 'faraday', '~> 2.0'
|
|
34
|
+
spec.add_dependency 'faraday-multipart', '~> 1.0'
|
|
35
|
+
end
|
data/lib/imago/client.rb
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Imago
|
|
4
|
+
class Client
|
|
5
|
+
PROVIDERS = {
|
|
6
|
+
openai: Providers::OpenAI,
|
|
7
|
+
gemini: Providers::Gemini,
|
|
8
|
+
xai: Providers::XAI
|
|
9
|
+
}.freeze
|
|
10
|
+
|
|
11
|
+
attr_reader :provider, :model
|
|
12
|
+
|
|
13
|
+
def initialize(provider:, model: nil, api_key: nil)
|
|
14
|
+
@provider_name = provider.to_sym
|
|
15
|
+
@model = model
|
|
16
|
+
@api_key = api_key
|
|
17
|
+
|
|
18
|
+
validate_provider!
|
|
19
|
+
@provider = build_provider
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def generate(prompt, opts = {})
|
|
23
|
+
provider.generate(prompt, opts)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def models
|
|
27
|
+
provider.models
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def validate_provider!
|
|
33
|
+
return if PROVIDERS.key?(@provider_name)
|
|
34
|
+
|
|
35
|
+
raise ProviderNotFoundError, "Unknown provider: #{@provider_name}. " \
|
|
36
|
+
"Available providers: #{PROVIDERS.keys.join(', ')}"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def build_provider
|
|
40
|
+
PROVIDERS[@provider_name].new(model: @model, api_key: @api_key)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
data/lib/imago/errors.rb
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Imago
|
|
4
|
+
class Error < StandardError; end
|
|
5
|
+
|
|
6
|
+
class ConfigurationError < Error; end
|
|
7
|
+
|
|
8
|
+
class ApiError < Error
|
|
9
|
+
attr_reader :status_code, :response_body
|
|
10
|
+
|
|
11
|
+
def initialize(message, status_code: nil, response_body: nil)
|
|
12
|
+
@status_code = status_code
|
|
13
|
+
@response_body = response_body
|
|
14
|
+
super(message)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
class AuthenticationError < ApiError; end
|
|
19
|
+
|
|
20
|
+
class RateLimitError < ApiError; end
|
|
21
|
+
|
|
22
|
+
class InvalidRequestError < ApiError; end
|
|
23
|
+
|
|
24
|
+
class ProviderNotFoundError < Error; end
|
|
25
|
+
end
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Imago
|
|
4
|
+
module Providers
|
|
5
|
+
class Base
|
|
6
|
+
attr_reader :model, :api_key
|
|
7
|
+
|
|
8
|
+
def initialize(model: nil, api_key: nil)
|
|
9
|
+
@model = model || default_model
|
|
10
|
+
@api_key = api_key || fetch_api_key
|
|
11
|
+
validate_api_key!
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def generate(_prompt, _opts = {})
|
|
15
|
+
raise NotImplementedError, "#{self.class} must implement #generate"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def models
|
|
19
|
+
raise NotImplementedError, "#{self.class} must implement #models"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
protected
|
|
23
|
+
|
|
24
|
+
def default_model
|
|
25
|
+
raise NotImplementedError, "#{self.class} must implement #default_model"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def env_key_name
|
|
29
|
+
raise NotImplementedError, "#{self.class} must implement #env_key_name"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def fetch_api_key
|
|
33
|
+
ENV.fetch(env_key_name, nil)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def validate_api_key!
|
|
37
|
+
return if api_key && !api_key.empty?
|
|
38
|
+
|
|
39
|
+
raise ConfigurationError,
|
|
40
|
+
"API key is required. Set #{env_key_name} environment variable or pass api_key option."
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def connection(base_url)
|
|
44
|
+
Faraday.new(url: base_url) do |conn|
|
|
45
|
+
conn.request :json
|
|
46
|
+
conn.response :json
|
|
47
|
+
conn.adapter Faraday.default_adapter
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def handle_response(response)
|
|
52
|
+
return response.body if response.status.between?(200, 299)
|
|
53
|
+
|
|
54
|
+
raise_response_error(response)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def raise_response_error(response)
|
|
58
|
+
error_class = error_class_for_status(response.status)
|
|
59
|
+
message = error_message_for_status(response)
|
|
60
|
+
raise error_class.new(message, status_code: response.status, response_body: response.body)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def error_class_for_status(status)
|
|
64
|
+
case status
|
|
65
|
+
when 401 then AuthenticationError
|
|
66
|
+
when 429 then RateLimitError
|
|
67
|
+
when 400..499 then InvalidRequestError
|
|
68
|
+
else ApiError
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def error_message_for_status(response)
|
|
73
|
+
case response.status
|
|
74
|
+
when 401 then 'Invalid API key'
|
|
75
|
+
when 429 then 'Rate limit exceeded'
|
|
76
|
+
when 400..499 then "Request failed: #{response.body}"
|
|
77
|
+
else "API error: #{response.body}"
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Imago
|
|
4
|
+
module Providers
|
|
5
|
+
class Gemini < Base
|
|
6
|
+
BASE_URL = 'https://generativelanguage.googleapis.com/v1beta'
|
|
7
|
+
|
|
8
|
+
KNOWN_IMAGE_MODELS = %w[
|
|
9
|
+
imagen-3.0-generate-002
|
|
10
|
+
imagen-3.0-generate-001
|
|
11
|
+
gemini-2.0-flash-exp-image-generation
|
|
12
|
+
gemini-2.5-flash-image
|
|
13
|
+
gemini-3-pro-image-preview
|
|
14
|
+
].freeze
|
|
15
|
+
|
|
16
|
+
def generate(prompt, opts = {})
|
|
17
|
+
conn = connection(BASE_URL)
|
|
18
|
+
endpoint = "models/#{model}:generateContent"
|
|
19
|
+
|
|
20
|
+
response = conn.post(endpoint) do |req|
|
|
21
|
+
req.params['key'] = api_key
|
|
22
|
+
req.body = build_request_body(prompt, opts)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
parse_generate_response(handle_response(response))
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def models
|
|
29
|
+
@models ||= fetch_models
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
protected
|
|
33
|
+
|
|
34
|
+
def default_model
|
|
35
|
+
'gemini-3-pro-image-preview'
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def env_key_name
|
|
39
|
+
'GEMINI_API_KEY'
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def build_request_body(prompt, opts)
|
|
45
|
+
body = { contents: [{ parts: [{ text: build_prompt(prompt, opts) }] }] }
|
|
46
|
+
body[:generationConfig] = build_generation_config(opts) if generation_config_present?(opts)
|
|
47
|
+
body
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def build_prompt(prompt, opts)
|
|
51
|
+
return prompt unless opts[:negative_prompt]
|
|
52
|
+
|
|
53
|
+
"#{prompt}. Avoid: #{opts[:negative_prompt]}"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def generation_config_present?(opts)
|
|
57
|
+
opts[:n] || opts[:sample_count] || opts[:aspect_ratio] || opts[:seed]
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def build_generation_config(opts)
|
|
61
|
+
config = {}
|
|
62
|
+
config[:candidateCount] = opts[:sample_count] || opts[:n] if opts[:n] || opts[:sample_count]
|
|
63
|
+
config[:seed] = opts[:seed] if opts[:seed]
|
|
64
|
+
config[:aspectRatio] = opts[:aspect_ratio] if opts[:aspect_ratio]
|
|
65
|
+
config
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def parse_generate_response(body)
|
|
69
|
+
candidates = body['candidates'] || []
|
|
70
|
+
images = candidates.flat_map { |candidate| extract_images_from_candidate(candidate) }
|
|
71
|
+
{ images: images }
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def extract_images_from_candidate(candidate)
|
|
75
|
+
parts = candidate.dig('content', 'parts') || []
|
|
76
|
+
parts.filter_map do |part|
|
|
77
|
+
next unless part['inlineData']
|
|
78
|
+
|
|
79
|
+
{ base64: part['inlineData']['data'], mime_type: part['inlineData']['mimeType'] }.compact
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def fetch_models
|
|
84
|
+
conn = connection(BASE_URL)
|
|
85
|
+
response = conn.get('models') do |req|
|
|
86
|
+
req.params['key'] = api_key
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
body = handle_response(response)
|
|
90
|
+
filter_image_models(body['models'] || [])
|
|
91
|
+
rescue ApiError
|
|
92
|
+
KNOWN_IMAGE_MODELS
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def filter_image_models(models)
|
|
96
|
+
image_model_names = models
|
|
97
|
+
.select { |m| m['supportedGenerationMethods']&.include?('generateContent') }
|
|
98
|
+
.map { |m| m['name'].sub('models/', '') }
|
|
99
|
+
.select { |name| name.include?('imagen') || name.include?('image') }
|
|
100
|
+
|
|
101
|
+
image_model_names.empty? ? KNOWN_IMAGE_MODELS : image_model_names
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Imago
|
|
4
|
+
module Providers
|
|
5
|
+
class OpenAI < Base
|
|
6
|
+
BASE_URL = 'https://api.openai.com/v1'
|
|
7
|
+
|
|
8
|
+
KNOWN_IMAGE_MODELS = %w[
|
|
9
|
+
dall-e-3
|
|
10
|
+
dall-e-2
|
|
11
|
+
gpt-image-1
|
|
12
|
+
gpt-image-1.5
|
|
13
|
+
gpt-image-1-mini
|
|
14
|
+
].freeze
|
|
15
|
+
|
|
16
|
+
def generate(prompt, opts = {})
|
|
17
|
+
conn = connection(BASE_URL)
|
|
18
|
+
response = conn.post('images/generations') do |req|
|
|
19
|
+
req.headers['Authorization'] = "Bearer #{api_key}"
|
|
20
|
+
req.body = build_request_body(prompt, opts)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
parse_generate_response(handle_response(response))
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def models
|
|
27
|
+
@models ||= fetch_models
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
protected
|
|
31
|
+
|
|
32
|
+
def default_model
|
|
33
|
+
'gpt-image-1.5'
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def env_key_name
|
|
37
|
+
'OPENAI_API_KEY'
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def build_request_body(prompt, opts)
|
|
43
|
+
{
|
|
44
|
+
model: model,
|
|
45
|
+
prompt: prompt
|
|
46
|
+
}.merge(opts)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def parse_generate_response(body)
|
|
50
|
+
images = body['data']&.map { |img| parse_image(img) }
|
|
51
|
+
{ images: images || [], created: body['created'] }
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def parse_image(img)
|
|
55
|
+
{ url: img['url'], base64: img['b64_json'], revised_prompt: img['revised_prompt'] }.compact
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def fetch_models
|
|
59
|
+
conn = connection(BASE_URL)
|
|
60
|
+
response = conn.get('models') do |req|
|
|
61
|
+
req.headers['Authorization'] = "Bearer #{api_key}"
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
body = handle_response(response)
|
|
65
|
+
filter_image_models(body['data'] || [])
|
|
66
|
+
rescue ApiError
|
|
67
|
+
KNOWN_IMAGE_MODELS
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def filter_image_models(models)
|
|
71
|
+
image_model_ids = models
|
|
72
|
+
.map { |m| m['id'] }
|
|
73
|
+
.select { |id| id.include?('dall-e') || id.include?('image') }
|
|
74
|
+
|
|
75
|
+
image_model_ids.empty? ? KNOWN_IMAGE_MODELS : image_model_ids
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Imago
|
|
4
|
+
module Providers
|
|
5
|
+
class XAI < Base
|
|
6
|
+
BASE_URL = 'https://api.x.ai/v1'
|
|
7
|
+
|
|
8
|
+
KNOWN_IMAGE_MODELS = %w[
|
|
9
|
+
grok-2-image
|
|
10
|
+
grok-2-image-1212
|
|
11
|
+
].freeze
|
|
12
|
+
|
|
13
|
+
def generate(prompt, opts = {})
|
|
14
|
+
conn = connection(BASE_URL)
|
|
15
|
+
response = conn.post('images/generations') do |req|
|
|
16
|
+
req.headers['Authorization'] = "Bearer #{api_key}"
|
|
17
|
+
req.body = build_request_body(prompt, opts)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
parse_generate_response(handle_response(response))
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def models
|
|
24
|
+
KNOWN_IMAGE_MODELS
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
protected
|
|
28
|
+
|
|
29
|
+
def default_model
|
|
30
|
+
'grok-2-image'
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def env_key_name
|
|
34
|
+
'XAI_API_KEY'
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def build_request_body(prompt, opts)
|
|
40
|
+
{
|
|
41
|
+
model: model,
|
|
42
|
+
prompt: prompt,
|
|
43
|
+
n: opts[:n] || 1,
|
|
44
|
+
response_format: opts[:response_format] || 'url'
|
|
45
|
+
}.merge(opts.except(:n, :response_format))
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def parse_generate_response(body)
|
|
49
|
+
images = body['data']&.map do |img|
|
|
50
|
+
{
|
|
51
|
+
url: img['url'],
|
|
52
|
+
base64: img['b64_json']
|
|
53
|
+
}.compact
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
{
|
|
57
|
+
images: images || [],
|
|
58
|
+
created: body['created']
|
|
59
|
+
}
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
data/lib/imago.rb
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'faraday'
|
|
4
|
+
require 'json'
|
|
5
|
+
|
|
6
|
+
require_relative 'imago/version'
|
|
7
|
+
require_relative 'imago/errors'
|
|
8
|
+
require_relative 'imago/providers/base'
|
|
9
|
+
require_relative 'imago/providers/openai'
|
|
10
|
+
require_relative 'imago/providers/gemini'
|
|
11
|
+
require_relative 'imago/providers/xai'
|
|
12
|
+
require_relative 'imago/client'
|
|
13
|
+
|
|
14
|
+
module Imago
|
|
15
|
+
class << self
|
|
16
|
+
def new(provider:, model: nil, api_key: nil)
|
|
17
|
+
Client.new(provider: provider, model: model, api_key: api_key)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: imago
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.1
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Your Name
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: faraday
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '2.0'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '2.0'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: faraday-multipart
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '1.0'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '1.0'
|
|
40
|
+
description: Imago provides a simple, unified API to generate images using various
|
|
41
|
+
AI providers including OpenAI (DALL-E), Google Gemini, and xAI (Grok).
|
|
42
|
+
email:
|
|
43
|
+
- your.email@example.com
|
|
44
|
+
executables: []
|
|
45
|
+
extensions: []
|
|
46
|
+
extra_rdoc_files: []
|
|
47
|
+
files:
|
|
48
|
+
- ".rubocop.yml"
|
|
49
|
+
- ".ruby-version"
|
|
50
|
+
- README.md
|
|
51
|
+
- Rakefile
|
|
52
|
+
- imago.gemspec
|
|
53
|
+
- lib/imago.rb
|
|
54
|
+
- lib/imago/client.rb
|
|
55
|
+
- lib/imago/errors.rb
|
|
56
|
+
- lib/imago/providers/base.rb
|
|
57
|
+
- lib/imago/providers/gemini.rb
|
|
58
|
+
- lib/imago/providers/openai.rb
|
|
59
|
+
- lib/imago/providers/xai.rb
|
|
60
|
+
- lib/imago/version.rb
|
|
61
|
+
homepage: https://github.com/example/imago
|
|
62
|
+
licenses:
|
|
63
|
+
- MIT
|
|
64
|
+
metadata:
|
|
65
|
+
homepage_uri: https://github.com/example/imago
|
|
66
|
+
source_code_uri: https://github.com/example/imago
|
|
67
|
+
changelog_uri: https://github.com/example/imago/blob/main/CHANGELOG.md
|
|
68
|
+
rubygems_mfa_required: 'true'
|
|
69
|
+
rdoc_options: []
|
|
70
|
+
require_paths:
|
|
71
|
+
- lib
|
|
72
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
73
|
+
requirements:
|
|
74
|
+
- - ">="
|
|
75
|
+
- !ruby/object:Gem::Version
|
|
76
|
+
version: 3.0.0
|
|
77
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - ">="
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: '0'
|
|
82
|
+
requirements: []
|
|
83
|
+
rubygems_version: 4.0.3
|
|
84
|
+
specification_version: 4
|
|
85
|
+
summary: A unified Ruby interface for multiple image generation AI providers
|
|
86
|
+
test_files: []
|