ai-lite 0.2.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 +4 -4
- data/README.md +163 -19
- data/lib/ai_lite/version.rb +1 -1
- data/lib/ai_lite.rb +129 -3
- data/test/ai_lite_test.rb +256 -0
- metadata +7 -7
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4f75f42caf4d74f3abf2ec982b7c3830c715b1d1c54f2ae0fdf02a1dec5151db
|
|
4
|
+
data.tar.gz: ff953a4533862067eee149c04ac5f348bf3b97e7e9f7b87fbcd18a3fe3d63233
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c2c91c198575156c15e0100c3fa12709b1cf09395785d4a634e46bf06e9bc9dbf9e8073a95e990f54c4e19a0b72322379317299443899d83eef5c587864b6781
|
|
7
|
+
data.tar.gz: c528951faf3fe8f453def5a8dbaf7eb00c7da77fc5135c22ccfe2ac5765a2603a0c5cf50dcdfcbe1dda7aaedf6f69ca210a19424b67ef2866c627cd40e17104e
|
data/README.md
CHANGED
|
@@ -18,6 +18,8 @@ It is not meant to replace the official OpenAI SDK. It is a small wrapper for pr
|
|
|
18
18
|
```ruby
|
|
19
19
|
ai.chat("Say hello")
|
|
20
20
|
ai.moderate("User submitted text")
|
|
21
|
+
ai.embed("Text to vectorize")
|
|
22
|
+
ai.image("A simple app icon")
|
|
21
23
|
```
|
|
22
24
|
|
|
23
25
|
## Usage
|
|
@@ -49,6 +51,8 @@ AiLite.configure do |config|
|
|
|
49
51
|
config.api_key = ENV["OPENAI_API_KEY"]
|
|
50
52
|
config.model = "gpt-5.5"
|
|
51
53
|
config.moderation_model = "omni-moderation-latest"
|
|
54
|
+
config.embedding_model = "text-embedding-3-small"
|
|
55
|
+
config.image_model = "gpt-image-2"
|
|
52
56
|
config.timeout = 120
|
|
53
57
|
config.max_output_tokens = 2000
|
|
54
58
|
end
|
|
@@ -95,16 +99,43 @@ The OpenAI API URL is fixed to `https://api.openai.com/v1/responses`.
|
|
|
95
99
|
|
|
96
100
|
## Moderation
|
|
97
101
|
|
|
98
|
-
Use `moderate` to classify text or images for potentially harmful content
|
|
102
|
+
Use `moderate` to classify user-submitted text or images for potentially harmful content before saving, publishing, or sending it into another AI call.
|
|
103
|
+
|
|
104
|
+
A common use case is checking a comment before it is shown publicly:
|
|
99
105
|
|
|
100
106
|
```ruby
|
|
101
|
-
|
|
107
|
+
comment = "User submitted comment"
|
|
108
|
+
result = ai.moderate(comment)
|
|
102
109
|
|
|
103
110
|
if result["content"]["flagged"]
|
|
104
|
-
# hide, block, or route to review
|
|
111
|
+
# hide, block, or route the comment to review
|
|
112
|
+
else
|
|
113
|
+
# publish the comment
|
|
105
114
|
end
|
|
106
115
|
```
|
|
107
116
|
|
|
117
|
+
Moderation responses include an overall `flagged` value plus category booleans and scores:
|
|
118
|
+
|
|
119
|
+
```ruby
|
|
120
|
+
{
|
|
121
|
+
"content" => {
|
|
122
|
+
"flagged" => false,
|
|
123
|
+
"categories" => {
|
|
124
|
+
"hate" => false,
|
|
125
|
+
"violence" => false
|
|
126
|
+
},
|
|
127
|
+
"category_scores" => {
|
|
128
|
+
"hate" => 0.01,
|
|
129
|
+
"violence" => 0.02
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
"response_id" => "modr_...",
|
|
133
|
+
"status" => 200,
|
|
134
|
+
"error" => nil,
|
|
135
|
+
"raw" => nil
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
|
|
108
139
|
`moderate` sends a `POST` request to `/v1/moderations` with:
|
|
109
140
|
|
|
110
141
|
- `model`
|
|
@@ -144,6 +175,135 @@ result = ai.moderate([
|
|
|
144
175
|
])
|
|
145
176
|
```
|
|
146
177
|
|
|
178
|
+
## Embeddings
|
|
179
|
+
|
|
180
|
+
Use `embed` to turn text into vectors that can be stored and compared for semantic search, recommendations, duplicate detection, or retrieval-augmented generation.
|
|
181
|
+
|
|
182
|
+
```ruby
|
|
183
|
+
result = ai.embed("How do I reset my password?")
|
|
184
|
+
vector = result["content"]
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
For one string input, `content` is the embedding vector:
|
|
188
|
+
|
|
189
|
+
```ruby
|
|
190
|
+
{
|
|
191
|
+
"content" => [0.012, -0.44, 0.203],
|
|
192
|
+
"response_id" => nil,
|
|
193
|
+
"status" => 200,
|
|
194
|
+
"error" => nil,
|
|
195
|
+
"raw" => nil
|
|
196
|
+
}
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
For multiple inputs, `content` is an array of vectors in the same order:
|
|
200
|
+
|
|
201
|
+
```ruby
|
|
202
|
+
result = ai.embed([
|
|
203
|
+
"How do I reset my password?",
|
|
204
|
+
"How do I update my billing card?"
|
|
205
|
+
])
|
|
206
|
+
|
|
207
|
+
vectors = result["content"]
|
|
208
|
+
first_vector = vectors[0]
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
```ruby
|
|
212
|
+
{
|
|
213
|
+
"content" => [
|
|
214
|
+
[0.012, -0.44, 0.203],
|
|
215
|
+
[0.332, 0.021, -0.118]
|
|
216
|
+
],
|
|
217
|
+
"response_id" => nil,
|
|
218
|
+
"status" => 200,
|
|
219
|
+
"error" => nil,
|
|
220
|
+
"raw" => nil
|
|
221
|
+
}
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
`embed` sends a `POST` request to `/v1/embeddings` with:
|
|
225
|
+
|
|
226
|
+
- `model`
|
|
227
|
+
- `input`
|
|
228
|
+
- optional `dimensions`
|
|
229
|
+
- optional `encoding_format`
|
|
230
|
+
- optional `debug`
|
|
231
|
+
- optional extra `options`
|
|
232
|
+
|
|
233
|
+
The default embedding model is `text-embedding-3-small`.
|
|
234
|
+
|
|
235
|
+
Pass `debug: true` to include the raw OpenAI response, including token usage:
|
|
236
|
+
|
|
237
|
+
```ruby
|
|
238
|
+
result = ai.embed("Hello", debug: true)
|
|
239
|
+
|
|
240
|
+
result["content"] # embedding vector
|
|
241
|
+
result["raw"]["usage"] # token usage
|
|
242
|
+
```
|
|
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
|
+
|
|
147
307
|
## Multi-Turn Chat
|
|
148
308
|
|
|
149
309
|
Responses include a `response_id` that can be passed back through `options` as `previous_response_id`:
|
|
@@ -189,22 +349,6 @@ JSON-looking model output:
|
|
|
189
349
|
}
|
|
190
350
|
```
|
|
191
351
|
|
|
192
|
-
Moderation output:
|
|
193
|
-
|
|
194
|
-
```ruby
|
|
195
|
-
{
|
|
196
|
-
"content" => {
|
|
197
|
-
"flagged" => false,
|
|
198
|
-
"categories" => { ... },
|
|
199
|
-
"category_scores" => { ... }
|
|
200
|
-
},
|
|
201
|
-
"response_id" => "modr_...",
|
|
202
|
-
"status" => 200,
|
|
203
|
-
"error" => nil,
|
|
204
|
-
"raw" => nil
|
|
205
|
-
}
|
|
206
|
-
```
|
|
207
|
-
|
|
208
352
|
Failure:
|
|
209
353
|
|
|
210
354
|
```ruby
|
data/lib/ai_lite/version.rb
CHANGED
data/lib/ai_lite.rb
CHANGED
|
@@ -8,6 +8,8 @@ class AiLite
|
|
|
8
8
|
API_BASE_URL = "https://api.openai.com/v1".freeze
|
|
9
9
|
DEFAULT_MODEL = "gpt-5.5".freeze
|
|
10
10
|
DEFAULT_MODERATION_MODEL = "omni-moderation-latest".freeze
|
|
11
|
+
DEFAULT_EMBEDDING_MODEL = "text-embedding-3-small".freeze
|
|
12
|
+
DEFAULT_IMAGE_MODEL = "gpt-image-2".freeze
|
|
11
13
|
DEFAULT_TIMEOUT = 120
|
|
12
14
|
DEFAULT_MAX_OUTPUT_TOKENS = 2000
|
|
13
15
|
IMAGE_MIME_TYPES = {
|
|
@@ -19,12 +21,14 @@ class AiLite
|
|
|
19
21
|
}.freeze
|
|
20
22
|
|
|
21
23
|
class Configuration
|
|
22
|
-
attr_accessor :api_key, :model, :moderation_model, :timeout, :max_output_tokens
|
|
24
|
+
attr_accessor :api_key, :model, :moderation_model, :embedding_model, :image_model, :timeout, :max_output_tokens
|
|
23
25
|
|
|
24
26
|
def initialize
|
|
25
27
|
@api_key = nil
|
|
26
28
|
@model = DEFAULT_MODEL
|
|
27
29
|
@moderation_model = DEFAULT_MODERATION_MODEL
|
|
30
|
+
@embedding_model = DEFAULT_EMBEDDING_MODEL
|
|
31
|
+
@image_model = DEFAULT_IMAGE_MODEL
|
|
28
32
|
@timeout = DEFAULT_TIMEOUT
|
|
29
33
|
@max_output_tokens = DEFAULT_MAX_OUTPUT_TOKENS
|
|
30
34
|
end
|
|
@@ -59,19 +63,29 @@ class AiLite
|
|
|
59
63
|
client.moderate(input, **kwargs)
|
|
60
64
|
end
|
|
61
65
|
|
|
66
|
+
def embed(input, **kwargs)
|
|
67
|
+
client.embed(input, **kwargs)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def image(prompt, **kwargs)
|
|
71
|
+
client.image(prompt, **kwargs)
|
|
72
|
+
end
|
|
73
|
+
|
|
62
74
|
def reset_client!
|
|
63
75
|
@client = nil
|
|
64
76
|
end
|
|
65
77
|
end
|
|
66
78
|
|
|
67
|
-
attr_reader :api_key, :model, :moderation_model, :timeout, :max_output_tokens, :headers
|
|
79
|
+
attr_reader :api_key, :model, :moderation_model, :embedding_model, :image_model, :timeout, :max_output_tokens, :headers
|
|
68
80
|
|
|
69
|
-
def initialize(api_key: nil, model: nil, moderation_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)
|
|
70
82
|
@api_key = api_key || self.class.configuration.api_key || ENV["OPENAI_API_KEY"] || ENV["OPEN_AI_TOKEN"]
|
|
71
83
|
raise ArgumentError, "Missing OpenAI API key" if @api_key.to_s.strip.empty?
|
|
72
84
|
|
|
73
85
|
@model = model || self.class.configuration.model
|
|
74
86
|
@moderation_model = moderation_model || self.class.configuration.moderation_model
|
|
87
|
+
@embedding_model = embedding_model || self.class.configuration.embedding_model
|
|
88
|
+
@image_model = image_model || self.class.configuration.image_model
|
|
75
89
|
@timeout = timeout || self.class.configuration.timeout
|
|
76
90
|
@max_output_tokens = max_output_tokens || self.class.configuration.max_output_tokens
|
|
77
91
|
@headers = {
|
|
@@ -104,6 +118,34 @@ class AiLite
|
|
|
104
118
|
prettify_data(status: "unknown", error: e.message, raw: nil, debug: debug)
|
|
105
119
|
end
|
|
106
120
|
|
|
121
|
+
def embed(input, model: nil, dimensions: nil, encoding_format: nil, debug: false, options: {})
|
|
122
|
+
payload = options.merge(
|
|
123
|
+
model: model || embedding_model,
|
|
124
|
+
input: input
|
|
125
|
+
)
|
|
126
|
+
payload[:dimensions] = dimensions if dimensions
|
|
127
|
+
payload[:encoding_format] = encoding_format if encoding_format
|
|
128
|
+
|
|
129
|
+
extract_embedding(post(payload, endpoint: embedding_endpoint), multiple: input.is_a?(Array), debug: debug)
|
|
130
|
+
rescue => e
|
|
131
|
+
prettify_data(status: "unknown", error: e.message, raw: nil, debug: debug)
|
|
132
|
+
end
|
|
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
|
+
|
|
107
149
|
private
|
|
108
150
|
|
|
109
151
|
def post(payload, endpoint: response_endpoint)
|
|
@@ -131,6 +173,14 @@ class AiLite
|
|
|
131
173
|
"#{API_BASE_URL}/moderations"
|
|
132
174
|
end
|
|
133
175
|
|
|
176
|
+
def embedding_endpoint
|
|
177
|
+
"#{API_BASE_URL}/embeddings"
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def image_endpoint
|
|
181
|
+
"#{API_BASE_URL}/images/generations"
|
|
182
|
+
end
|
|
183
|
+
|
|
134
184
|
def extract_content(response, debug: false)
|
|
135
185
|
status = response.code.to_i
|
|
136
186
|
parsed_response = JSON.parse(response.body)
|
|
@@ -187,6 +237,63 @@ class AiLite
|
|
|
187
237
|
prettify_data(status: response_status(response), error: e.message, raw: nil, debug: debug)
|
|
188
238
|
end
|
|
189
239
|
|
|
240
|
+
def extract_embedding(response, multiple:, debug: false)
|
|
241
|
+
status = response.code.to_i
|
|
242
|
+
parsed_response = JSON.parse(response.body)
|
|
243
|
+
|
|
244
|
+
unless success_status?(status)
|
|
245
|
+
return prettify_data(
|
|
246
|
+
status: status,
|
|
247
|
+
error: error_message(parsed_response),
|
|
248
|
+
response_id: parsed_response["id"],
|
|
249
|
+
raw: parsed_response,
|
|
250
|
+
debug: debug
|
|
251
|
+
)
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
prettify_data(
|
|
255
|
+
status: status,
|
|
256
|
+
content: embedding_content(parsed_response, multiple: multiple),
|
|
257
|
+
response_id: parsed_response["id"],
|
|
258
|
+
raw: parsed_response,
|
|
259
|
+
debug: debug
|
|
260
|
+
)
|
|
261
|
+
rescue JSON::ParserError => e
|
|
262
|
+
prettify_data(status: response_status(response), error: e.message, raw: response&.body, debug: debug)
|
|
263
|
+
rescue => e
|
|
264
|
+
prettify_data(status: response_status(response), error: e.message, raw: nil, debug: debug)
|
|
265
|
+
end
|
|
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
|
+
|
|
190
297
|
def extract_output_text(raw)
|
|
191
298
|
Array(raw["output"]).flat_map do |item|
|
|
192
299
|
next [] unless item.is_a?(Hash) && item["type"] == "message"
|
|
@@ -253,6 +360,25 @@ class AiLite
|
|
|
253
360
|
results.length == 1 ? results.first : results
|
|
254
361
|
end
|
|
255
362
|
|
|
363
|
+
def embedding_content(raw, multiple:)
|
|
364
|
+
embeddings = Array(raw["data"]).map do |item|
|
|
365
|
+
item["embedding"] if item.is_a?(Hash)
|
|
366
|
+
end.compact
|
|
367
|
+
|
|
368
|
+
multiple ? embeddings : embeddings.first
|
|
369
|
+
end
|
|
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
|
+
|
|
256
382
|
def success_status?(status)
|
|
257
383
|
status >= 200 && status < 300
|
|
258
384
|
end
|
data/test/ai_lite_test.rb
CHANGED
|
@@ -33,12 +33,15 @@ 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
|
|
|
39
40
|
assert_equal "explicit-key", client.api_key
|
|
40
41
|
assert_equal "gpt-test", client.model
|
|
41
42
|
assert_equal "omni-moderation-latest", client.moderation_model
|
|
43
|
+
assert_equal "text-embedding-3-small", client.embedding_model
|
|
44
|
+
assert_equal "gpt-image-test", client.image_model
|
|
42
45
|
assert_equal 10, client.timeout
|
|
43
46
|
assert_equal 2000, client.max_output_tokens
|
|
44
47
|
assert_equal "Bearer explicit-key", client.headers["Authorization"]
|
|
@@ -52,6 +55,8 @@ class AiLiteTest < Minitest::Test
|
|
|
52
55
|
config.api_key = "configured-key"
|
|
53
56
|
config.model = "gpt-config"
|
|
54
57
|
config.moderation_model = "omni-moderation-test"
|
|
58
|
+
config.embedding_model = "text-embedding-test"
|
|
59
|
+
config.image_model = "gpt-image-test"
|
|
55
60
|
config.timeout = 15
|
|
56
61
|
config.max_output_tokens = 750
|
|
57
62
|
end
|
|
@@ -61,6 +66,8 @@ class AiLiteTest < Minitest::Test
|
|
|
61
66
|
assert_equal "configured-key", client.api_key
|
|
62
67
|
assert_equal "gpt-config", client.model
|
|
63
68
|
assert_equal "omni-moderation-test", client.moderation_model
|
|
69
|
+
assert_equal "text-embedding-test", client.embedding_model
|
|
70
|
+
assert_equal "gpt-image-test", client.image_model
|
|
64
71
|
assert_equal 15, client.timeout
|
|
65
72
|
assert_equal 750, client.max_output_tokens
|
|
66
73
|
assert_same client, AiLite.client
|
|
@@ -83,6 +90,8 @@ class AiLiteTest < Minitest::Test
|
|
|
83
90
|
config.api_key = "configured-key"
|
|
84
91
|
config.model = "gpt-config"
|
|
85
92
|
config.moderation_model = "omni-moderation-config"
|
|
93
|
+
config.embedding_model = "text-embedding-config"
|
|
94
|
+
config.image_model = "gpt-image-config"
|
|
86
95
|
config.timeout = 15
|
|
87
96
|
config.max_output_tokens = 750
|
|
88
97
|
end
|
|
@@ -91,6 +100,8 @@ class AiLiteTest < Minitest::Test
|
|
|
91
100
|
api_key: "explicit-key",
|
|
92
101
|
model: "gpt-explicit",
|
|
93
102
|
moderation_model: "omni-moderation-explicit",
|
|
103
|
+
embedding_model: "text-embedding-explicit",
|
|
104
|
+
image_model: "gpt-image-explicit",
|
|
94
105
|
timeout: 5,
|
|
95
106
|
max_output_tokens: 300
|
|
96
107
|
)
|
|
@@ -98,6 +109,8 @@ class AiLiteTest < Minitest::Test
|
|
|
98
109
|
assert_equal "explicit-key", client.api_key
|
|
99
110
|
assert_equal "gpt-explicit", client.model
|
|
100
111
|
assert_equal "omni-moderation-explicit", client.moderation_model
|
|
112
|
+
assert_equal "text-embedding-explicit", client.embedding_model
|
|
113
|
+
assert_equal "gpt-image-explicit", client.image_model
|
|
101
114
|
assert_equal 5, client.timeout
|
|
102
115
|
assert_equal 300, client.max_output_tokens
|
|
103
116
|
end
|
|
@@ -427,6 +440,210 @@ class AiLiteTest < Minitest::Test
|
|
|
427
440
|
assert_nil result["raw"]
|
|
428
441
|
end
|
|
429
442
|
|
|
443
|
+
def test_embed_sends_post_to_embeddings_with_default_payload
|
|
444
|
+
client = AiLite.new(api_key: "token-abc")
|
|
445
|
+
|
|
446
|
+
with_stubbed_http(embedding_response([[0.12, -0.34, 0.56]])) do |captured, _response|
|
|
447
|
+
result = client.embed("Text to vectorize")
|
|
448
|
+
request = captured[:http].last_request
|
|
449
|
+
payload = JSON.parse(request.body)
|
|
450
|
+
|
|
451
|
+
assert_equal [0.12, -0.34, 0.56], result["content"]
|
|
452
|
+
assert_nil result["response_id"]
|
|
453
|
+
assert_equal 200, result["status"]
|
|
454
|
+
assert_nil result["error"]
|
|
455
|
+
assert_nil result["raw"]
|
|
456
|
+
assert_equal "api.openai.com", captured[:host]
|
|
457
|
+
assert_equal 443, captured[:port]
|
|
458
|
+
assert_equal true, captured[:use_ssl]
|
|
459
|
+
assert_instance_of Net::HTTP::Post, request
|
|
460
|
+
assert_equal "/v1/embeddings", request.path
|
|
461
|
+
assert_equal "Bearer token-abc", request["Authorization"]
|
|
462
|
+
assert_equal "application/json", request["Content-Type"]
|
|
463
|
+
assert_equal "text-embedding-3-small", payload["model"]
|
|
464
|
+
assert_equal "Text to vectorize", payload["input"]
|
|
465
|
+
end
|
|
466
|
+
end
|
|
467
|
+
|
|
468
|
+
def test_embed_returns_vector_list_for_multiple_inputs
|
|
469
|
+
client = AiLite.new(api_key: "token-abc")
|
|
470
|
+
embeddings = [
|
|
471
|
+
[0.11, 0.22, 0.33],
|
|
472
|
+
[0.44, 0.55, 0.66]
|
|
473
|
+
]
|
|
474
|
+
|
|
475
|
+
with_stubbed_http(embedding_response(embeddings)) do |captured, _response|
|
|
476
|
+
result = client.embed(["First text", "Second text"])
|
|
477
|
+
payload = JSON.parse(captured[:http].last_request.body)
|
|
478
|
+
|
|
479
|
+
assert_equal ["First text", "Second text"], payload["input"]
|
|
480
|
+
assert_equal embeddings, result["content"]
|
|
481
|
+
assert_equal 200, result["status"]
|
|
482
|
+
assert_nil result["error"]
|
|
483
|
+
end
|
|
484
|
+
end
|
|
485
|
+
|
|
486
|
+
def test_embed_includes_options_dimensions_encoding_format_and_model
|
|
487
|
+
client = AiLite.new(api_key: "token-abc")
|
|
488
|
+
|
|
489
|
+
with_stubbed_http(embedding_response([[0.12, -0.34]])) do |captured, _response|
|
|
490
|
+
client.embed(
|
|
491
|
+
"Short text",
|
|
492
|
+
model: "text-embedding-3-large",
|
|
493
|
+
dimensions: 2,
|
|
494
|
+
encoding_format: "float",
|
|
495
|
+
options: {
|
|
496
|
+
user: "user-123"
|
|
497
|
+
}
|
|
498
|
+
)
|
|
499
|
+
payload = JSON.parse(captured[:http].last_request.body)
|
|
500
|
+
|
|
501
|
+
assert_equal "text-embedding-3-large", payload["model"]
|
|
502
|
+
assert_equal "Short text", payload["input"]
|
|
503
|
+
assert_equal 2, payload["dimensions"]
|
|
504
|
+
assert_equal "float", payload["encoding_format"]
|
|
505
|
+
assert_equal "user-123", payload["user"]
|
|
506
|
+
end
|
|
507
|
+
end
|
|
508
|
+
|
|
509
|
+
def test_embed_uses_class_level_configured_client
|
|
510
|
+
AiLite.configure do |config|
|
|
511
|
+
config.api_key = "configured-key"
|
|
512
|
+
config.embedding_model = "text-embedding-config"
|
|
513
|
+
end
|
|
514
|
+
|
|
515
|
+
with_stubbed_http(embedding_response([[0.12, -0.34, 0.56]])) do |captured, _response|
|
|
516
|
+
AiLite.embed("Use configured defaults")
|
|
517
|
+
payload = JSON.parse(captured[:http].last_request.body)
|
|
518
|
+
|
|
519
|
+
assert_equal "text-embedding-config", payload["model"]
|
|
520
|
+
assert_equal "Bearer configured-key", captured[:http].last_request["Authorization"]
|
|
521
|
+
end
|
|
522
|
+
end
|
|
523
|
+
|
|
524
|
+
def test_embed_debug_true_returns_raw_usage
|
|
525
|
+
client = AiLite.new(api_key: "token-abc")
|
|
526
|
+
|
|
527
|
+
with_stubbed_http(embedding_response([[0.12, -0.34, 0.56]])) do |_captured, _response|
|
|
528
|
+
result = client.embed("Text to vectorize", debug: true)
|
|
529
|
+
|
|
530
|
+
assert_equal [0.12, -0.34, 0.56], result["content"]
|
|
531
|
+
assert_equal({ "prompt_tokens" => 4, "total_tokens" => 4 }, result["raw"]["usage"])
|
|
532
|
+
end
|
|
533
|
+
end
|
|
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
|
+
|
|
430
647
|
def test_http_errors_return_standard_envelope
|
|
431
648
|
client = AiLite.new(api_key: "token-abc")
|
|
432
649
|
body = JSON.generate("error" => { "message" => "Invalid API key" })
|
|
@@ -542,6 +759,45 @@ class AiLiteTest < Minitest::Test
|
|
|
542
759
|
}
|
|
543
760
|
end
|
|
544
761
|
|
|
762
|
+
def embedding_response(embeddings)
|
|
763
|
+
body = JSON.generate(
|
|
764
|
+
"object" => "list",
|
|
765
|
+
"data" => embeddings.each_with_index.map do |embedding, index|
|
|
766
|
+
{
|
|
767
|
+
"object" => "embedding",
|
|
768
|
+
"index" => index,
|
|
769
|
+
"embedding" => embedding
|
|
770
|
+
}
|
|
771
|
+
end,
|
|
772
|
+
"model" => "text-embedding-3-small",
|
|
773
|
+
"usage" => {
|
|
774
|
+
"prompt_tokens" => 4,
|
|
775
|
+
"total_tokens" => 4
|
|
776
|
+
}
|
|
777
|
+
)
|
|
778
|
+
FakeResponse.new("200", body)
|
|
779
|
+
end
|
|
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
|
+
|
|
545
801
|
def with_env(values)
|
|
546
802
|
originals = {}
|
|
547
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.
|
|
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-
|
|
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.
|
|
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.
|