imago 0.1.2 → 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: 32b2597d5abd824bba56fe349f70df96231527c712c587666c07882cd046388d
4
- data.tar.gz: e7a2f4a5c7845b6bd548f03adc2586236a056192c80c707c49d6109509982a58
3
+ metadata.gz: 417e5a3f6b7a3d41cee0ce17ec1cc225862fcc72c2448fc1c9b1da989509afd3
4
+ data.tar.gz: feca98a16c48e79f386b88626d7fa5934be4637910ae250a91bf036f79a2865b
5
5
  SHA512:
6
- metadata.gz: 165ae60b51d64db276bf3ec15979ba0e06fc352074635b8386468ccf72fc0dcca9de35102e592ae9fce9eddd6dab2f59ff1855e51f44feecf053ef9f93ae5e55
7
- data.tar.gz: 9af79343ed2ea4e4ade8079b95bd8fce0fe6cadd17796064c6677d7613b1311012c38ac18e9507def8bf9c19db05a2d4a8660f5c776ea26f26777482bb1fac01
6
+ metadata.gz: bb62ccf9dcb695a401f08674d68ba37e7b3f2e8cdb7ae5820735d987aa41839f95a2fd5152533016fd8a6b704fd43df26ebf9e18d8b4f64181ed4bfbea3a4a1e
7
+ data.tar.gz: da94d8fbd02b78cce3ab04c567fa90881752670973925254c77ed724a645c9974dbb635b810e70ebe7182e5f5b658ae83f29590adaa867844760c5501cb70027
data/.rubocop.yml CHANGED
@@ -44,5 +44,22 @@ RSpec/NestedGroups:
44
44
  Layout/LineLength:
45
45
  Max: 120
46
46
 
47
+ Metrics/ClassLength:
48
+ Max: 100
49
+
50
+ Metrics/MethodLength:
51
+ Max: 25
52
+
53
+ Metrics/AbcSize:
54
+ Max: 10
55
+ Exclude:
56
+ - 'spec/**/*'
57
+
58
+ Metrics/CyclomaticComplexity:
59
+ Max: 10
60
+
61
+ Metrics/PerceivedComplexity:
62
+ Max: 10
63
+
47
64
  RSpec/MessageSpies:
48
65
  EnforcedStyle: receive
data/README.md CHANGED
@@ -124,6 +124,45 @@ result[:images].each do |image|
124
124
  end
125
125
  ```
126
126
 
127
+ ### Image Input (Image-to-Image)
128
+
129
+ Imago supports image inputs for image editing and image-to-image generation. You can provide images as URLs or base64-encoded data.
130
+
131
+ ```ruby
132
+ # URL string (auto-detect mime type from extension)
133
+ client = Imago.new(provider: :openai)
134
+ result = client.generate("Make this colorful", images: ["https://example.com/photo.jpg"])
135
+
136
+ # Base64 with explicit mime type
137
+ result = client.generate("Add a hat", images: [
138
+ { base64: "iVBORw0KGgo...", mime_type: "image/png" }
139
+ ])
140
+
141
+ # URL with explicit mime type (useful when URL has no extension)
142
+ result = client.generate("Edit this", images: [
143
+ { url: "https://example.com/photo", mime_type: "image/jpeg" }
144
+ ])
145
+
146
+ # Mixed inputs
147
+ result = client.generate("Combine these", images: [
148
+ "https://example.com/photo1.jpg",
149
+ { base64: "iVBORw0KGgo...", mime_type: "image/jpeg" }
150
+ ])
151
+ ```
152
+
153
+ #### Image Input Provider Support
154
+
155
+ | Provider | Support | Limits |
156
+ |----------|---------|--------|
157
+ | OpenAI | Yes (gpt-image-*, dall-e-2) | 16 images max |
158
+ | Gemini | Yes | 10 images max |
159
+ | xAI | No | N/A |
160
+
161
+ **Notes:**
162
+ - DALL-E 3 does not support image inputs
163
+ - Mime types are auto-detected from URL extensions (png, jpg, jpeg, webp, gif)
164
+ - Base64 images require an explicit `mime_type`
165
+
127
166
  ### Listing Available Models
128
167
 
129
168
  ```ruby
@@ -167,6 +206,9 @@ rescue Imago::ConfigurationError => e
167
206
  puts "Configuration error: #{e.message}"
168
207
  rescue Imago::ProviderNotFoundError => e
169
208
  puts "Unknown provider: #{e.message}"
209
+ rescue Imago::UnsupportedFeatureError => e
210
+ puts "Feature not supported: #{e.message}"
211
+ puts "Provider: #{e.provider}, Feature: #{e.feature}"
170
212
  end
171
213
  ```
172
214
 
data/imago.gemspec CHANGED
@@ -30,6 +30,7 @@ Gem::Specification.new do |spec|
30
30
  spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
31
31
  spec.require_paths = ['lib']
32
32
 
33
+ spec.add_dependency 'base64', '~> 0.2'
33
34
  spec.add_dependency 'faraday', '~> 2.0'
34
35
  spec.add_dependency 'faraday-multipart', '~> 1.0'
35
36
  end
data/lib/imago/errors.rb CHANGED
@@ -22,4 +22,14 @@ module Imago
22
22
  class InvalidRequestError < ApiError; end
23
23
 
24
24
  class ProviderNotFoundError < Error; end
25
+
26
+ class UnsupportedFeatureError < Error
27
+ attr_reader :provider, :feature
28
+
29
+ def initialize(message, provider: nil, feature: nil)
30
+ @provider = provider
31
+ @feature = feature
32
+ super(message)
33
+ end
34
+ end
25
35
  end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Imago
4
+ class GeminiResponseParser
5
+ def parse(body)
6
+ candidates = body['candidates'] || []
7
+ images = candidates.flat_map { |candidate| extract_images(candidate) }
8
+ { images: images }
9
+ end
10
+
11
+ private
12
+
13
+ def extract_images(candidate)
14
+ parts = candidate.dig('content', 'parts') || []
15
+ parts.filter_map { |part| parse_image_part(part) }
16
+ end
17
+
18
+ def parse_image_part(part)
19
+ return unless part['inlineData']
20
+
21
+ { base64: part['inlineData']['data'], mime_type: part['inlineData']['mimeType'] }.compact
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Imago
4
+ class ImageInput
5
+ MIME_TYPES = {
6
+ 'png' => 'image/png',
7
+ 'jpg' => 'image/jpeg',
8
+ 'jpeg' => 'image/jpeg',
9
+ 'webp' => 'image/webp',
10
+ 'gif' => 'image/gif'
11
+ }.freeze
12
+
13
+ attr_reader :url, :base64, :mime_type
14
+
15
+ def self.from(input)
16
+ case input
17
+ when String
18
+ from_url_string(input)
19
+ when Hash
20
+ from_hash(input)
21
+ else
22
+ raise ArgumentError, "Invalid image input: expected String or Hash, got #{input.class}"
23
+ end
24
+ end
25
+
26
+ def self.from_url_string(url)
27
+ mime_type = detect_mime_type(url)
28
+ new(url: url, mime_type: mime_type)
29
+ end
30
+
31
+ def self.from_hash(hash)
32
+ hash = hash.transform_keys(&:to_sym)
33
+
34
+ return from_base64_hash(hash) if hash[:base64]
35
+ return from_url_hash(hash) if hash[:url]
36
+
37
+ raise ArgumentError, 'Image hash must contain either :url or :base64 key'
38
+ end
39
+
40
+ def self.from_base64_hash(hash)
41
+ raise ArgumentError, 'mime_type is required for base64 images' unless hash[:mime_type]
42
+
43
+ new(base64: hash[:base64], mime_type: hash[:mime_type])
44
+ end
45
+
46
+ def self.from_url_hash(hash)
47
+ mime_type = hash[:mime_type] || detect_mime_type(hash[:url])
48
+ new(url: hash[:url], mime_type: mime_type)
49
+ end
50
+
51
+ def self.detect_mime_type(url)
52
+ extension = File.extname(URI.parse(url).path).delete('.').downcase
53
+ MIME_TYPES[extension]
54
+ rescue URI::InvalidURIError
55
+ nil
56
+ end
57
+
58
+ private_class_method :from_url_string, :from_hash, :from_base64_hash, :from_url_hash, :detect_mime_type
59
+
60
+ def initialize(url: nil, base64: nil, mime_type: nil)
61
+ @url = url
62
+ @base64 = base64
63
+ @mime_type = mime_type
64
+ end
65
+
66
+ def url?
67
+ !@url.nil?
68
+ end
69
+
70
+ def base64?
71
+ !@base64.nil?
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'base64'
4
+
5
+ module Imago
6
+ class MultipartBuilder
7
+ def initialize(model)
8
+ @model = model
9
+ end
10
+
11
+ def build_body(prompt, images, opts)
12
+ body = base_body(prompt)
13
+ add_images(body, images)
14
+ body.merge(opts.except(:images))
15
+ end
16
+
17
+ def build_image_part(image)
18
+ image.url? ? image.url : build_file_part(image)
19
+ end
20
+
21
+ private
22
+
23
+ def base_body(prompt)
24
+ { model: @model, prompt: prompt }
25
+ end
26
+
27
+ def add_images(body, images)
28
+ images.each_with_index do |image, index|
29
+ body["image[#{index}]"] = build_image_part(image)
30
+ end
31
+ end
32
+
33
+ def build_file_part(image)
34
+ io = StringIO.new(Base64.decode64(image.base64))
35
+ extension = extract_extension(image.mime_type)
36
+ Faraday::Multipart::FilePart.new(io, image.mime_type, "image.#{extension}")
37
+ end
38
+
39
+ def extract_extension(mime_type)
40
+ mime_type&.split('/')&.last || 'png'
41
+ end
42
+ end
43
+ end
@@ -77,6 +77,21 @@ module Imago
77
77
  else "API error: #{response.body}"
78
78
  end
79
79
  end
80
+
81
+ def normalize_images(images)
82
+ return [] if images.nil? || images.empty?
83
+
84
+ images.map { |img| ImageInput.from(img) }
85
+ end
86
+
87
+ def validate_image_count!(images, max:)
88
+ return if images.nil? || images.length <= max
89
+
90
+ raise InvalidRequestError.new(
91
+ "Too many images: #{images.length} provided, maximum is #{max}",
92
+ status_code: 400
93
+ )
94
+ end
80
95
  end
81
96
  end
82
97
  end
@@ -4,6 +4,7 @@ module Imago
4
4
  module Providers
5
5
  class Gemini < Base
6
6
  BASE_URL = 'https://generativelanguage.googleapis.com/v1beta'
7
+ MAX_IMAGES = 10
7
8
 
8
9
  KNOWN_IMAGE_MODELS = %w[
9
10
  imagen-3.0-generate-002
@@ -14,15 +15,9 @@ module Imago
14
15
  ].freeze
15
16
 
16
17
  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))
18
+ validate_image_count!(opts[:images], max: MAX_IMAGES)
19
+ response = execute_generate_request(prompt, opts)
20
+ response_parser.parse(handle_response(response))
26
21
  end
27
22
 
28
23
  def models
@@ -41,64 +36,84 @@ module Imago
41
36
 
42
37
  private
43
38
 
39
+ def response_parser
40
+ @response_parser ||= Imago::GeminiResponseParser.new
41
+ end
42
+
43
+ def execute_generate_request(prompt, opts)
44
+ conn = connection(BASE_URL)
45
+ body = build_request_body(prompt, opts)
46
+ conn.post(generate_endpoint) { |req| configure_request(req, body) }
47
+ end
48
+
49
+ def generate_endpoint
50
+ "models/#{model}:generateContent"
51
+ end
52
+
53
+ def configure_request(req, body)
54
+ req.params['key'] = api_key
55
+ req.body = body
56
+ end
57
+
44
58
  def build_request_body(prompt, opts)
45
- body = { contents: [{ parts: [{ text: build_prompt(prompt, opts) }] }] }
59
+ body = { contents: [{ parts: build_parts(prompt, opts) }] }
46
60
  body[:generationConfig] = build_generation_config(opts) if generation_config_present?(opts)
47
61
  body
48
62
  end
49
63
 
50
- def build_prompt(prompt, opts)
51
- return prompt unless opts[:negative_prompt]
64
+ def build_parts(prompt, opts)
65
+ parts = [{ text: build_prompt(prompt, opts) }]
66
+ normalize_images(opts[:images]).each { |img| parts << build_image_part(img) }
67
+ parts
68
+ end
52
69
 
53
- "#{prompt}. Avoid: #{opts[:negative_prompt]}"
70
+ def build_image_part(image)
71
+ image.url? ? build_file_data(image) : build_inline_data(image)
54
72
  end
55
73
 
56
- def generation_config_present?(opts)
57
- opts[:n] || opts[:sample_count] || opts[:aspect_ratio] || opts[:seed]
74
+ def build_file_data(image)
75
+ { fileData: { fileUri: image.url, mimeType: image.mime_type }.compact }
58
76
  end
59
77
 
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
78
+ def build_inline_data(image)
79
+ { inlineData: { data: image.base64, mimeType: image.mime_type } }
66
80
  end
67
81
 
68
- def parse_generate_response(body)
69
- candidates = body['candidates'] || []
70
- images = candidates.flat_map { |candidate| extract_images_from_candidate(candidate) }
71
- { images: images }
82
+ def build_prompt(prompt, opts)
83
+ opts[:negative_prompt] ? "#{prompt}. Avoid: #{opts[:negative_prompt]}" : prompt
72
84
  end
73
85
 
74
- def extract_images_from_candidate(candidate)
75
- parts = candidate.dig('content', 'parts') || []
76
- parts.filter_map do |part|
77
- next unless part['inlineData']
86
+ def generation_config_present?(opts)
87
+ opts[:n] || opts[:sample_count] || opts[:aspect_ratio] || opts[:seed]
88
+ end
78
89
 
79
- { base64: part['inlineData']['data'], mime_type: part['inlineData']['mimeType'] }.compact
80
- end
90
+ def build_generation_config(opts)
91
+ { candidateCount: opts[:sample_count] || opts[:n], seed: opts[:seed], aspectRatio: opts[:aspect_ratio] }.compact
81
92
  end
82
93
 
83
94
  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'] || [])
95
+ response = connection(BASE_URL).get('models') { |req| req.params['key'] = api_key }
96
+ filter_image_models(handle_response(response)['models'] || [])
91
97
  rescue ApiError
92
98
  KNOWN_IMAGE_MODELS
93
99
  end
94
100
 
95
101
  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') }
102
+ names = extract_image_model_names(models)
103
+ names.empty? ? KNOWN_IMAGE_MODELS : names
104
+ end
105
+
106
+ def extract_image_model_names(models)
107
+ content_models = models.select { |m| supports_generate_content?(m) }
108
+ content_models.map { |m| m['name'].sub('models/', '') }.select { |n| image_model?(n) }
109
+ end
110
+
111
+ def supports_generate_content?(model)
112
+ model['supportedGenerationMethods']&.include?('generateContent')
113
+ end
100
114
 
101
- image_model_names.empty? ? KNOWN_IMAGE_MODELS : image_model_names
115
+ def image_model?(name)
116
+ name.include?('imagen') || name.include?('image')
102
117
  end
103
118
  end
104
119
  end
@@ -1,26 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'faraday/multipart'
4
+
3
5
  module Imago
4
6
  module Providers
5
7
  class OpenAI < Base
6
8
  BASE_URL = 'https://api.openai.com/v1'
9
+ MAX_IMAGES = 16
7
10
 
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
11
+ KNOWN_IMAGE_MODELS = %w[dall-e-3 dall-e-2 gpt-image-1 gpt-image-1.5 gpt-image-1-mini].freeze
12
+ MODELS_SUPPORTING_IMAGE_INPUT = %w[dall-e-2 gpt-image-1 gpt-image-1.5 gpt-image-1-mini].freeze
15
13
 
16
14
  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))
15
+ has_images = opts[:images] && !opts[:images].empty?
16
+ has_images ? generate_with_images(prompt, opts) : generate_text_only(prompt, opts)
24
17
  end
25
18
 
26
19
  def models
@@ -39,40 +32,89 @@ module Imago
39
32
 
40
33
  private
41
34
 
42
- def build_request_body(prompt, opts)
43
- {
44
- model: model,
45
- prompt: prompt
46
- }.merge(opts)
35
+ def generate_text_only(prompt, opts)
36
+ response = post_with_auth('images/generations', build_request_body(prompt, opts))
37
+ parse_response(response)
38
+ end
39
+
40
+ def generate_with_images(prompt, opts)
41
+ validate_model_supports_images!
42
+ validate_image_count!(opts[:images], max: MAX_IMAGES)
43
+ response = post_multipart_edit(prompt, opts)
44
+ parse_response(response)
45
+ end
46
+
47
+ def post_with_auth(endpoint, body)
48
+ connection(BASE_URL).post(endpoint) do |req|
49
+ req.headers['Authorization'] = auth_header
50
+ req.body = body
51
+ end
47
52
  end
48
53
 
49
- def parse_generate_response(body)
54
+ def post_multipart_edit(prompt, opts)
55
+ body = build_multipart_body(prompt, opts)
56
+ multipart_connection.post('images/edits') { |req| configure_auth_request(req, body) }
57
+ end
58
+
59
+ def build_multipart_body(prompt, opts)
60
+ images = normalize_images(opts[:images])
61
+ multipart_builder.build_body(prompt, images, opts)
62
+ end
63
+
64
+ def configure_auth_request(req, body)
65
+ req.headers['Authorization'] = auth_header
66
+ req.body = body
67
+ end
68
+
69
+ def parse_response(response)
70
+ body = handle_response(response)
50
71
  images = body['data']&.map { |img| parse_image(img) }
51
72
  { images: images || [], created: body['created'] }
52
73
  end
53
74
 
75
+ def auth_header
76
+ "Bearer #{api_key}"
77
+ end
78
+
79
+ def multipart_builder
80
+ @multipart_builder ||= Imago::MultipartBuilder.new(model)
81
+ end
82
+
83
+ def multipart_connection
84
+ @multipart_connection ||= Faraday.new(url: BASE_URL) do |conn|
85
+ conn.request :multipart
86
+ conn.response :json
87
+ conn.adapter Faraday.default_adapter
88
+ end
89
+ end
90
+
91
+ def validate_model_supports_images!
92
+ return if MODELS_SUPPORTING_IMAGE_INPUT.include?(model)
93
+
94
+ raise InvalidRequestError.new(
95
+ "Model '#{model}' does not support image inputs. Supported: #{MODELS_SUPPORTING_IMAGE_INPUT.join(', ')}",
96
+ status_code: 400
97
+ )
98
+ end
99
+
100
+ def build_request_body(prompt, opts)
101
+ { model: model, prompt: prompt }.merge(opts)
102
+ end
103
+
54
104
  def parse_image(img)
55
105
  { url: img['url'], base64: img['b64_json'], revised_prompt: img['revised_prompt'] }.compact
56
106
  end
57
107
 
58
108
  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'] || [])
109
+ response = connection(BASE_URL).get('models') { |req| req.headers['Authorization'] = auth_header }
110
+ filter_image_models(handle_response(response)['data'] || [])
66
111
  rescue ApiError
67
112
  KNOWN_IMAGE_MODELS
68
113
  end
69
114
 
70
115
  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
116
+ ids = models.map { |m| m['id'] }.select { |id| id.include?('dall-e') || id.include?('image') }
117
+ ids.empty? ? KNOWN_IMAGE_MODELS : ids
76
118
  end
77
119
  end
78
120
  end
@@ -11,12 +11,8 @@ module Imago
11
11
  ].freeze
12
12
 
13
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
-
14
+ raise_if_images_provided(opts)
15
+ response = execute_generate_request(prompt, opts)
20
16
  parse_generate_response(handle_response(response))
21
17
  end
22
18
 
@@ -36,6 +32,21 @@ module Imago
36
32
 
37
33
  private
38
34
 
35
+ def execute_generate_request(prompt, opts)
36
+ conn = connection(BASE_URL)
37
+ body = build_request_body(prompt, opts)
38
+ conn.post('images/generations') { |req| configure_request(req, body) }
39
+ end
40
+
41
+ def configure_request(req, body)
42
+ req.headers['Authorization'] = auth_header
43
+ req.body = body
44
+ end
45
+
46
+ def auth_header
47
+ "Bearer #{api_key}"
48
+ end
49
+
39
50
  def build_request_body(prompt, opts)
40
51
  {
41
52
  model: model,
@@ -46,17 +57,19 @@ module Imago
46
57
  end
47
58
 
48
59
  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
60
+ images = body['data']&.map { |img| { url: img['url'], base64: img['b64_json'] }.compact }
61
+ { images: images || [], created: body['created'] }
62
+ end
55
63
 
56
- {
57
- images: images || [],
58
- created: body['created']
59
- }
64
+ def raise_if_images_provided(opts)
65
+ return unless opts[:images] && !opts[:images].empty?
66
+
67
+ raise UnsupportedFeatureError.new(
68
+ 'xAI does not currently support image inputs. ' \
69
+ 'Image-to-image generation may be available in future API versions.',
70
+ provider: :xai,
71
+ feature: :image_input
72
+ )
60
73
  end
61
74
  end
62
75
  end
data/lib/imago/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Imago
4
- VERSION = '0.1.2'
4
+ VERSION = '0.2.1'
5
5
  end
data/lib/imago.rb CHANGED
@@ -5,6 +5,9 @@ require 'json'
5
5
 
6
6
  require_relative 'imago/version'
7
7
  require_relative 'imago/errors'
8
+ require_relative 'imago/image_input'
9
+ require_relative 'imago/multipart_builder'
10
+ require_relative 'imago/gemini_response_parser'
8
11
  require_relative 'imago/providers/base'
9
12
  require_relative 'imago/providers/openai'
10
13
  require_relative 'imago/providers/gemini'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: imago
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - NEETzsche
@@ -9,6 +9,20 @@ bindir: exe
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: base64
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '0.2'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '0.2'
12
26
  - !ruby/object:Gem::Dependency
13
27
  name: faraday
14
28
  requirement: !ruby/object:Gem::Requirement
@@ -53,6 +67,9 @@ files:
53
67
  - lib/imago.rb
54
68
  - lib/imago/client.rb
55
69
  - lib/imago/errors.rb
70
+ - lib/imago/gemini_response_parser.rb
71
+ - lib/imago/image_input.rb
72
+ - lib/imago/multipart_builder.rb
56
73
  - lib/imago/providers/base.rb
57
74
  - lib/imago/providers/gemini.rb
58
75
  - lib/imago/providers/openai.rb
@@ -80,7 +97,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
80
97
  - !ruby/object:Gem::Version
81
98
  version: '0'
82
99
  requirements: []
83
- rubygems_version: 4.0.3
100
+ rubygems_version: 4.0.4
84
101
  specification_version: 4
85
102
  summary: A unified Ruby interface for multiple image generation AI providers
86
103
  test_files: []