ai-lite 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cd1b2ead0a8ae48d9617b9e73460649f817fc158430e11cc7b9ac798a86846d2
4
- data.tar.gz: f3dad03f56f3907e537dc8c72d63bf7e57e069d4b28ef666151d348b98f6b3e6
3
+ metadata.gz: 4f75f42caf4d74f3abf2ec982b7c3830c715b1d1c54f2ae0fdf02a1dec5151db
4
+ data.tar.gz: ff953a4533862067eee149c04ac5f348bf3b97e7e9f7b87fbcd18a3fe3d63233
5
5
  SHA512:
6
- metadata.gz: 6e70409efd1d697b2578ace5b878e74ef4e6214efbcbae2c295aae066e0e9ce4887cd39c95fcdde8bc73e0f6b0cfe60434dc7f1ca2611c77e649cc645b94351a
7
- data.tar.gz: 1faba64c32c14038f18c1d50a2f73bd92a0c940d8b303b3eecb89c2193d944d3b189334c5ecde81f55e8268db01e01e562686b74f20111105f0c0fbbeea387c6
6
+ metadata.gz: c2c91c198575156c15e0100c3fa12709b1cf09395785d4a634e46bf06e9bc9dbf9e8073a95e990f54c4e19a0b72322379317299443899d83eef5c587864b6781
7
+ data.tar.gz: c528951faf3fe8f453def5a8dbaf7eb00c7da77fc5135c22ccfe2ac5765a2603a0c5cf50dcdfcbe1dda7aaedf6f69ca210a19424b67ef2866c627cd40e17104e
data/README.md CHANGED
@@ -19,6 +19,7 @@ It is not meant to replace the official OpenAI SDK. It is a small wrapper for pr
19
19
  ai.chat("Say hello")
20
20
  ai.moderate("User submitted text")
21
21
  ai.embed("Text to vectorize")
22
+ ai.image("A simple app icon")
22
23
  ```
23
24
 
24
25
  ## Usage
@@ -51,6 +52,7 @@ AiLite.configure do |config|
51
52
  config.model = "gpt-5.5"
52
53
  config.moderation_model = "omni-moderation-latest"
53
54
  config.embedding_model = "text-embedding-3-small"
55
+ config.image_model = "gpt-image-2"
54
56
  config.timeout = 120
55
57
  config.max_output_tokens = 2000
56
58
  end
@@ -239,6 +241,69 @@ result["content"] # embedding vector
239
241
  result["raw"]["usage"] # token usage
240
242
  ```
241
243
 
244
+ ## Images
245
+
246
+ Use `image` to generate an image from a prompt.
247
+
248
+ ```ruby
249
+ result = ai.image("A clean Ruby gem logo on a white background")
250
+ image_data = result["content"]
251
+ ```
252
+
253
+ By default, `content` is the base64-encoded generated image:
254
+
255
+ ```ruby
256
+ {
257
+ "content" => "iVBORw0KGgo...",
258
+ "response_id" => nil,
259
+ "status" => 200,
260
+ "error" => nil,
261
+ "raw" => nil
262
+ }
263
+ ```
264
+
265
+ Write the generated image bytes directly to a file with `output_path`:
266
+
267
+ ```ruby
268
+ result = ai.image(
269
+ "A clean Ruby gem logo on a white background",
270
+ output_path: "tmp/logo.png"
271
+ )
272
+ ```
273
+
274
+ `image` sends a `POST` request to `/v1/images/generations` with:
275
+
276
+ - `model`
277
+ - `prompt`
278
+ - optional `size`
279
+ - optional `quality`
280
+ - optional `background`
281
+ - optional `output_format`
282
+ - optional `debug`
283
+ - optional extra `options`
284
+
285
+ The default image model is `gpt-image-2`.
286
+
287
+ Use `output_format` to request `png`, `webp`, or `jpeg` output:
288
+
289
+ ```ruby
290
+ result = ai.image(
291
+ "A transparent app icon",
292
+ background: "transparent",
293
+ output_format: "webp",
294
+ output_path: "tmp/icon.webp"
295
+ )
296
+ ```
297
+
298
+ Pass `debug: true` to include the raw OpenAI response, including usage when returned:
299
+
300
+ ```ruby
301
+ result = ai.image("A tiny robot sticker", debug: true)
302
+
303
+ result["content"] # base64 image data
304
+ result["raw"]["usage"] # token usage, when returned
305
+ ```
306
+
242
307
  ## Multi-Turn Chat
243
308
 
244
309
  Responses include a `response_id` that can be passed back through `options` as `previous_response_id`:
@@ -1,3 +1,3 @@
1
1
  class AiLite
2
- VERSION = "0.3.0".freeze
2
+ VERSION = "0.4.0".freeze
3
3
  end
data/lib/ai_lite.rb CHANGED
@@ -9,6 +9,7 @@ class AiLite
9
9
  DEFAULT_MODEL = "gpt-5.5".freeze
10
10
  DEFAULT_MODERATION_MODEL = "omni-moderation-latest".freeze
11
11
  DEFAULT_EMBEDDING_MODEL = "text-embedding-3-small".freeze
12
+ DEFAULT_IMAGE_MODEL = "gpt-image-2".freeze
12
13
  DEFAULT_TIMEOUT = 120
13
14
  DEFAULT_MAX_OUTPUT_TOKENS = 2000
14
15
  IMAGE_MIME_TYPES = {
@@ -20,13 +21,14 @@ class AiLite
20
21
  }.freeze
21
22
 
22
23
  class Configuration
23
- attr_accessor :api_key, :model, :moderation_model, :embedding_model, :timeout, :max_output_tokens
24
+ attr_accessor :api_key, :model, :moderation_model, :embedding_model, :image_model, :timeout, :max_output_tokens
24
25
 
25
26
  def initialize
26
27
  @api_key = nil
27
28
  @model = DEFAULT_MODEL
28
29
  @moderation_model = DEFAULT_MODERATION_MODEL
29
30
  @embedding_model = DEFAULT_EMBEDDING_MODEL
31
+ @image_model = DEFAULT_IMAGE_MODEL
30
32
  @timeout = DEFAULT_TIMEOUT
31
33
  @max_output_tokens = DEFAULT_MAX_OUTPUT_TOKENS
32
34
  end
@@ -65,20 +67,25 @@ class AiLite
65
67
  client.embed(input, **kwargs)
66
68
  end
67
69
 
70
+ def image(prompt, **kwargs)
71
+ client.image(prompt, **kwargs)
72
+ end
73
+
68
74
  def reset_client!
69
75
  @client = nil
70
76
  end
71
77
  end
72
78
 
73
- attr_reader :api_key, :model, :moderation_model, :embedding_model, :timeout, :max_output_tokens, :headers
79
+ attr_reader :api_key, :model, :moderation_model, :embedding_model, :image_model, :timeout, :max_output_tokens, :headers
74
80
 
75
- def initialize(api_key: nil, model: nil, moderation_model: nil, embedding_model: nil, timeout: nil, max_output_tokens: nil)
81
+ def initialize(api_key: nil, model: nil, moderation_model: nil, embedding_model: nil, image_model: nil, timeout: nil, max_output_tokens: nil)
76
82
  @api_key = api_key || self.class.configuration.api_key || ENV["OPENAI_API_KEY"] || ENV["OPEN_AI_TOKEN"]
77
83
  raise ArgumentError, "Missing OpenAI API key" if @api_key.to_s.strip.empty?
78
84
 
79
85
  @model = model || self.class.configuration.model
80
86
  @moderation_model = moderation_model || self.class.configuration.moderation_model
81
87
  @embedding_model = embedding_model || self.class.configuration.embedding_model
88
+ @image_model = image_model || self.class.configuration.image_model
82
89
  @timeout = timeout || self.class.configuration.timeout
83
90
  @max_output_tokens = max_output_tokens || self.class.configuration.max_output_tokens
84
91
  @headers = {
@@ -124,6 +131,21 @@ class AiLite
124
131
  prettify_data(status: "unknown", error: e.message, raw: nil, debug: debug)
125
132
  end
126
133
 
134
+ def image(prompt, model: nil, size: nil, quality: nil, background: nil, output_format: nil, output_path: nil, debug: false, options: {})
135
+ payload = options.merge(
136
+ model: model || image_model,
137
+ prompt: prompt
138
+ )
139
+ payload[:size] = size if size
140
+ payload[:quality] = quality if quality
141
+ payload[:background] = background if background
142
+ payload[:output_format] = output_format if output_format
143
+
144
+ extract_image(post(payload, endpoint: image_endpoint), output_path: output_path, debug: debug)
145
+ rescue => e
146
+ prettify_data(status: "unknown", error: e.message, raw: nil, debug: debug)
147
+ end
148
+
127
149
  private
128
150
 
129
151
  def post(payload, endpoint: response_endpoint)
@@ -155,6 +177,10 @@ class AiLite
155
177
  "#{API_BASE_URL}/embeddings"
156
178
  end
157
179
 
180
+ def image_endpoint
181
+ "#{API_BASE_URL}/images/generations"
182
+ end
183
+
158
184
  def extract_content(response, debug: false)
159
185
  status = response.code.to_i
160
186
  parsed_response = JSON.parse(response.body)
@@ -238,6 +264,36 @@ class AiLite
238
264
  prettify_data(status: response_status(response), error: e.message, raw: nil, debug: debug)
239
265
  end
240
266
 
267
+ def extract_image(response, output_path:, debug: false)
268
+ status = response.code.to_i
269
+ parsed_response = JSON.parse(response.body)
270
+
271
+ unless success_status?(status)
272
+ return prettify_data(
273
+ status: status,
274
+ error: error_message(parsed_response),
275
+ response_id: parsed_response["id"],
276
+ raw: parsed_response,
277
+ debug: debug
278
+ )
279
+ end
280
+
281
+ content = image_content(parsed_response)
282
+ write_image_output(output_path, content) if output_path
283
+
284
+ prettify_data(
285
+ status: status,
286
+ content: content,
287
+ response_id: parsed_response["id"],
288
+ raw: parsed_response,
289
+ debug: debug
290
+ )
291
+ rescue JSON::ParserError => e
292
+ prettify_data(status: response_status(response), error: e.message, raw: response&.body, debug: debug)
293
+ rescue => e
294
+ prettify_data(status: response_status(response), error: e.message, raw: nil, debug: debug)
295
+ end
296
+
241
297
  def extract_output_text(raw)
242
298
  Array(raw["output"]).flat_map do |item|
243
299
  next [] unless item.is_a?(Hash) && item["type"] == "message"
@@ -312,6 +368,17 @@ class AiLite
312
368
  multiple ? embeddings : embeddings.first
313
369
  end
314
370
 
371
+ def image_content(raw)
372
+ image = Array(raw["data"]).find { |item| item.is_a?(Hash) && item["b64_json"] }
373
+ image && image["b64_json"]
374
+ end
375
+
376
+ def write_image_output(path, content)
377
+ raise "No image data returned" if content.to_s.empty?
378
+
379
+ File.binwrite(path, Base64.decode64(content))
380
+ end
381
+
315
382
  def success_status?(status)
316
383
  status >= 200 && status < 300
317
384
  end
data/test/ai_lite_test.rb CHANGED
@@ -33,6 +33,7 @@ class AiLiteTest < Minitest::Test
33
33
  client = AiLite.new(
34
34
  api_key: "explicit-key",
35
35
  model: "gpt-test",
36
+ image_model: "gpt-image-test",
36
37
  timeout: 10
37
38
  )
38
39
 
@@ -40,6 +41,7 @@ class AiLiteTest < Minitest::Test
40
41
  assert_equal "gpt-test", client.model
41
42
  assert_equal "omni-moderation-latest", client.moderation_model
42
43
  assert_equal "text-embedding-3-small", client.embedding_model
44
+ assert_equal "gpt-image-test", client.image_model
43
45
  assert_equal 10, client.timeout
44
46
  assert_equal 2000, client.max_output_tokens
45
47
  assert_equal "Bearer explicit-key", client.headers["Authorization"]
@@ -54,6 +56,7 @@ class AiLiteTest < Minitest::Test
54
56
  config.model = "gpt-config"
55
57
  config.moderation_model = "omni-moderation-test"
56
58
  config.embedding_model = "text-embedding-test"
59
+ config.image_model = "gpt-image-test"
57
60
  config.timeout = 15
58
61
  config.max_output_tokens = 750
59
62
  end
@@ -64,6 +67,7 @@ class AiLiteTest < Minitest::Test
64
67
  assert_equal "gpt-config", client.model
65
68
  assert_equal "omni-moderation-test", client.moderation_model
66
69
  assert_equal "text-embedding-test", client.embedding_model
70
+ assert_equal "gpt-image-test", client.image_model
67
71
  assert_equal 15, client.timeout
68
72
  assert_equal 750, client.max_output_tokens
69
73
  assert_same client, AiLite.client
@@ -87,6 +91,7 @@ class AiLiteTest < Minitest::Test
87
91
  config.model = "gpt-config"
88
92
  config.moderation_model = "omni-moderation-config"
89
93
  config.embedding_model = "text-embedding-config"
94
+ config.image_model = "gpt-image-config"
90
95
  config.timeout = 15
91
96
  config.max_output_tokens = 750
92
97
  end
@@ -96,6 +101,7 @@ class AiLiteTest < Minitest::Test
96
101
  model: "gpt-explicit",
97
102
  moderation_model: "omni-moderation-explicit",
98
103
  embedding_model: "text-embedding-explicit",
104
+ image_model: "gpt-image-explicit",
99
105
  timeout: 5,
100
106
  max_output_tokens: 300
101
107
  )
@@ -104,6 +110,7 @@ class AiLiteTest < Minitest::Test
104
110
  assert_equal "gpt-explicit", client.model
105
111
  assert_equal "omni-moderation-explicit", client.moderation_model
106
112
  assert_equal "text-embedding-explicit", client.embedding_model
113
+ assert_equal "gpt-image-explicit", client.image_model
107
114
  assert_equal 5, client.timeout
108
115
  assert_equal 300, client.max_output_tokens
109
116
  end
@@ -525,6 +532,118 @@ class AiLiteTest < Minitest::Test
525
532
  end
526
533
  end
527
534
 
535
+ def test_image_sends_post_to_images_with_default_payload
536
+ client = AiLite.new(api_key: "token-abc")
537
+ image_data = Base64.strict_encode64("fake image")
538
+
539
+ with_stubbed_http(image_response(b64_json: image_data)) do |captured, _response|
540
+ result = client.image("A small ruby gem logo")
541
+ request = captured[:http].last_request
542
+ payload = JSON.parse(request.body)
543
+
544
+ assert_equal image_data, result["content"]
545
+ assert_nil result["response_id"]
546
+ assert_equal 200, result["status"]
547
+ assert_nil result["error"]
548
+ assert_nil result["raw"]
549
+ assert_equal "api.openai.com", captured[:host]
550
+ assert_equal 443, captured[:port]
551
+ assert_equal true, captured[:use_ssl]
552
+ assert_instance_of Net::HTTP::Post, request
553
+ assert_equal "/v1/images/generations", request.path
554
+ assert_equal "Bearer token-abc", request["Authorization"]
555
+ assert_equal "application/json", request["Content-Type"]
556
+ assert_equal "gpt-image-2", payload["model"]
557
+ assert_equal "A small ruby gem logo", payload["prompt"]
558
+ end
559
+ end
560
+
561
+ def test_image_includes_options_size_quality_background_output_format_and_model
562
+ client = AiLite.new(api_key: "token-abc")
563
+
564
+ with_stubbed_http(image_response) do |captured, _response|
565
+ client.image(
566
+ "A transparent app icon",
567
+ model: "gpt-image-test",
568
+ size: "1024x1536",
569
+ quality: "high",
570
+ background: "transparent",
571
+ output_format: "webp",
572
+ options: {
573
+ moderation: "auto",
574
+ output_compression: 80
575
+ }
576
+ )
577
+ payload = JSON.parse(captured[:http].last_request.body)
578
+
579
+ assert_equal "gpt-image-test", payload["model"]
580
+ assert_equal "A transparent app icon", payload["prompt"]
581
+ assert_equal "1024x1536", payload["size"]
582
+ assert_equal "high", payload["quality"]
583
+ assert_equal "transparent", payload["background"]
584
+ assert_equal "webp", payload["output_format"]
585
+ assert_equal "auto", payload["moderation"]
586
+ assert_equal 80, payload["output_compression"]
587
+ end
588
+ end
589
+
590
+ def test_image_uses_class_level_configured_client
591
+ AiLite.configure do |config|
592
+ config.api_key = "configured-key"
593
+ config.image_model = "gpt-image-config"
594
+ end
595
+
596
+ with_stubbed_http(image_response) do |captured, _response|
597
+ AiLite.image("Use configured defaults")
598
+ payload = JSON.parse(captured[:http].last_request.body)
599
+
600
+ assert_equal "gpt-image-config", payload["model"]
601
+ assert_equal "Bearer configured-key", captured[:http].last_request["Authorization"]
602
+ end
603
+ end
604
+
605
+ def test_image_writes_decoded_content_to_output_path
606
+ client = AiLite.new(api_key: "token-abc")
607
+ image_data = Base64.strict_encode64("fake image")
608
+
609
+ Tempfile.create(["generated", ".png"]) do |file|
610
+ with_stubbed_http(image_response(b64_json: image_data)) do |_captured, _response|
611
+ result = client.image("A file output", output_path: file.path)
612
+
613
+ assert_equal image_data, result["content"]
614
+ assert_equal "fake image", File.binread(file.path)
615
+ end
616
+ end
617
+ end
618
+
619
+ def test_image_debug_true_returns_raw_usage
620
+ client = AiLite.new(api_key: "token-abc")
621
+ image_data = Base64.strict_encode64("fake image")
622
+
623
+ with_stubbed_http(image_response(b64_json: image_data)) do |_captured, _response|
624
+ result = client.image("A debuggable image", debug: true)
625
+
626
+ assert_equal image_data, result["content"]
627
+ assert_equal({ "input_tokens" => 10, "output_tokens" => 20, "total_tokens" => 30 }, result["raw"]["usage"])
628
+ end
629
+ end
630
+
631
+ def test_image_output_path_without_image_data_returns_standard_envelope
632
+ client = AiLite.new(api_key: "token-abc")
633
+
634
+ Tempfile.create(["generated", ".png"]) do |file|
635
+ with_stubbed_http(image_response(b64_json: nil)) do |_captured, _response|
636
+ result = client.image("A missing image", output_path: file.path)
637
+
638
+ assert_nil result["content"]
639
+ assert_nil result["response_id"]
640
+ assert_equal 200, result["status"]
641
+ assert_equal "No image data returned", result["error"]
642
+ assert_nil result["raw"]
643
+ end
644
+ end
645
+ end
646
+
528
647
  def test_http_errors_return_standard_envelope
529
648
  client = AiLite.new(api_key: "token-abc")
530
649
  body = JSON.generate("error" => { "message" => "Invalid API key" })
@@ -659,6 +778,26 @@ class AiLiteTest < Minitest::Test
659
778
  FakeResponse.new("200", body)
660
779
  end
661
780
 
781
+ def image_response(b64_json: Base64.strict_encode64("fake image"))
782
+ image = {}
783
+ image["b64_json"] = b64_json if b64_json
784
+
785
+ body = JSON.generate(
786
+ "created" => 1_713_833_628,
787
+ "background" => "opaque",
788
+ "data" => [image],
789
+ "output_format" => "png",
790
+ "quality" => "medium",
791
+ "size" => "1024x1024",
792
+ "usage" => {
793
+ "input_tokens" => 10,
794
+ "output_tokens" => 20,
795
+ "total_tokens" => 30
796
+ }
797
+ )
798
+ FakeResponse.new("200", body)
799
+ end
800
+
662
801
  def with_env(values)
663
802
  originals = {}
664
803
 
metadata CHANGED
@@ -1,18 +1,18 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ai-lite
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - William Basmayor
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-07-07 00:00:00.000000000 Z
11
+ date: 2026-08-04 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: AI Lite is a dependency-light Ruby client for Rails apps and plain Ruby
14
14
  projects that need simple OpenAI Responses API calls.
15
- email:
15
+ email:
16
16
  executables: []
17
17
  extensions: []
18
18
  extra_rdoc_files: []
@@ -27,7 +27,7 @@ homepage: https://github.com/wbasmayor/ai-lite
27
27
  licenses:
28
28
  - MIT
29
29
  metadata: {}
30
- post_install_message:
30
+ post_install_message:
31
31
  rdoc_options: []
32
32
  require_paths:
33
33
  - lib
@@ -42,8 +42,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
42
42
  - !ruby/object:Gem::Version
43
43
  version: '0'
44
44
  requirements: []
45
- rubygems_version: 3.0.3.1
46
- signing_key:
45
+ rubygems_version: 3.5.22
46
+ signing_key:
47
47
  specification_version: 4
48
48
  summary: Minimal Ruby client for simple AI chat calls through the OpenAI Responses
49
49
  API.