ai-lite 0.6.0 → 1.0.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/CHANGELOG.md +18 -0
- data/README.md +386 -12
- data/lib/ai_lite/gemini/chat.rb +78 -0
- data/lib/ai_lite/gemini/configuration.rb +19 -0
- data/lib/ai_lite/gemini/models.rb +102 -0
- data/lib/ai_lite/gemini/streaming.rb +148 -0
- data/lib/ai_lite/gemini/youtube.rb +36 -0
- data/lib/ai_lite/gemini.rb +92 -0
- data/lib/ai_lite/openai/configuration.rb +51 -0
- data/lib/ai_lite/openai/models.rb +96 -0
- data/lib/ai_lite/openai/streaming.rb +130 -0
- data/lib/ai_lite/openai.rb +607 -0
- data/lib/ai_lite/version.rb +1 -1
- data/lib/ai_lite.rb +32 -623
- data/test/ai_lite_test.rb +242 -73
- data/test/gemini_models_test.rb +112 -0
- data/test/gemini_streaming_test.rb +166 -0
- data/test/gemini_test.rb +200 -0
- data/test/gemini_youtube_test.rb +57 -0
- data/test/openai_models_test.rb +138 -0
- data/test/openai_streaming_test.rb +189 -0
- data/test/run.rb +3 -0
- metadata +27 -6
data/test/ai_lite_test.rb
CHANGED
|
@@ -20,8 +20,27 @@ class AiLiteTest < Minitest::Test
|
|
|
20
20
|
end
|
|
21
21
|
end
|
|
22
22
|
|
|
23
|
+
def provider
|
|
24
|
+
AiLite::OpenAI
|
|
25
|
+
end
|
|
26
|
+
|
|
23
27
|
def setup
|
|
24
|
-
|
|
28
|
+
provider.reset_configuration!
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def test_http_200_failed_or_incomplete_generation_is_not_success
|
|
32
|
+
client = provider.new(api_key: "test-key")
|
|
33
|
+
%w[failed cancelled incomplete].each do |state|
|
|
34
|
+
body = JSON.generate("id" => "resp_partial", "status" => state,
|
|
35
|
+
"incomplete_details" => { "reason" => "max_output_tokens" }, "output" => [])
|
|
36
|
+
with_stubbed_http(FakeResponse.new("200", body)) do
|
|
37
|
+
result = client.chat("Hi", debug: true)
|
|
38
|
+
assert_equal 200, result["status"]
|
|
39
|
+
assert_includes result["error"], state
|
|
40
|
+
assert_equal "resp_partial", result["response_id"]
|
|
41
|
+
refute_nil result["raw"]
|
|
42
|
+
end
|
|
43
|
+
end
|
|
25
44
|
end
|
|
26
45
|
|
|
27
46
|
def test_has_a_version_number
|
|
@@ -30,7 +49,7 @@ class AiLiteTest < Minitest::Test
|
|
|
30
49
|
|
|
31
50
|
def test_stores_initializer_config_and_headers
|
|
32
51
|
with_env("OPENAI_API_KEY" => nil, "OPEN_AI_TOKEN" => nil) do
|
|
33
|
-
client =
|
|
52
|
+
client = provider.new(
|
|
34
53
|
api_key: "explicit-key",
|
|
35
54
|
model: "gpt-test",
|
|
36
55
|
image_model: "gpt-image-test",
|
|
@@ -57,7 +76,7 @@ class AiLiteTest < Minitest::Test
|
|
|
57
76
|
|
|
58
77
|
def test_configure_sets_defaults_for_client
|
|
59
78
|
with_env("OPENAI_API_KEY" => nil, "OPEN_AI_TOKEN" => nil) do
|
|
60
|
-
|
|
79
|
+
provider.configure do |config|
|
|
61
80
|
config.api_key = "configured-key"
|
|
62
81
|
config.model = "gpt-config"
|
|
63
82
|
config.moderation_model = "omni-moderation-test"
|
|
@@ -70,7 +89,7 @@ class AiLiteTest < Minitest::Test
|
|
|
70
89
|
config.max_output_tokens = 750
|
|
71
90
|
end
|
|
72
91
|
|
|
73
|
-
client =
|
|
92
|
+
client = provider.client
|
|
74
93
|
|
|
75
94
|
assert_equal "configured-key", client.api_key
|
|
76
95
|
assert_equal "gpt-config", client.model
|
|
@@ -82,23 +101,23 @@ class AiLiteTest < Minitest::Test
|
|
|
82
101
|
assert_equal "gpt-transcribe-test", client.transcription_model
|
|
83
102
|
assert_equal 15, client.timeout
|
|
84
103
|
assert_equal 750, client.max_output_tokens
|
|
85
|
-
assert_same client,
|
|
104
|
+
assert_same client, provider.client
|
|
86
105
|
end
|
|
87
106
|
end
|
|
88
107
|
|
|
89
108
|
def test_configure_resets_cached_client
|
|
90
|
-
|
|
91
|
-
first_client =
|
|
109
|
+
provider.configure { |config| config.api_key = "first-key" }
|
|
110
|
+
first_client = provider.client
|
|
92
111
|
|
|
93
|
-
|
|
94
|
-
second_client =
|
|
112
|
+
provider.configure { |config| config.api_key = "second-key" }
|
|
113
|
+
second_client = provider.client
|
|
95
114
|
|
|
96
115
|
refute_same first_client, second_client
|
|
97
116
|
assert_equal "second-key", second_client.api_key
|
|
98
117
|
end
|
|
99
118
|
|
|
100
119
|
def test_instance_arguments_override_configuration
|
|
101
|
-
|
|
120
|
+
provider.configure do |config|
|
|
102
121
|
config.api_key = "configured-key"
|
|
103
122
|
config.model = "gpt-config"
|
|
104
123
|
config.moderation_model = "omni-moderation-config"
|
|
@@ -111,7 +130,7 @@ class AiLiteTest < Minitest::Test
|
|
|
111
130
|
config.max_output_tokens = 750
|
|
112
131
|
end
|
|
113
132
|
|
|
114
|
-
client =
|
|
133
|
+
client = provider.new(
|
|
115
134
|
api_key: "explicit-key",
|
|
116
135
|
model: "gpt-explicit",
|
|
117
136
|
moderation_model: "omni-moderation-explicit",
|
|
@@ -138,32 +157,32 @@ class AiLiteTest < Minitest::Test
|
|
|
138
157
|
|
|
139
158
|
def test_api_key_fallback_order
|
|
140
159
|
with_env("OPENAI_API_KEY" => "openai-key", "OPEN_AI_TOKEN" => "legacy-key") do
|
|
141
|
-
assert_equal "openai-key",
|
|
142
|
-
assert_equal "explicit-key",
|
|
160
|
+
assert_equal "openai-key", provider.new.api_key
|
|
161
|
+
assert_equal "explicit-key", provider.new(api_key: "explicit-key").api_key
|
|
143
162
|
end
|
|
144
163
|
|
|
145
164
|
with_env("OPENAI_API_KEY" => nil, "OPEN_AI_TOKEN" => "legacy-key") do
|
|
146
|
-
assert_equal "legacy-key",
|
|
165
|
+
assert_equal "legacy-key", provider.new.api_key
|
|
147
166
|
end
|
|
148
167
|
end
|
|
149
168
|
|
|
150
169
|
def test_configured_api_key_takes_precedence_over_env
|
|
151
|
-
|
|
170
|
+
provider.configure { |config| config.api_key = "configured-key" }
|
|
152
171
|
|
|
153
172
|
with_env("OPENAI_API_KEY" => "openai-key", "OPEN_AI_TOKEN" => "legacy-key") do
|
|
154
|
-
assert_equal "configured-key",
|
|
155
|
-
assert_equal "explicit-key",
|
|
173
|
+
assert_equal "configured-key", provider.new.api_key
|
|
174
|
+
assert_equal "explicit-key", provider.new(api_key: "explicit-key").api_key
|
|
156
175
|
end
|
|
157
176
|
end
|
|
158
177
|
|
|
159
178
|
def test_missing_api_key_raises
|
|
160
179
|
with_env("OPENAI_API_KEY" => nil, "OPEN_AI_TOKEN" => nil) do
|
|
161
|
-
assert_raises(ArgumentError) {
|
|
180
|
+
assert_raises(ArgumentError) { provider.new }
|
|
162
181
|
end
|
|
163
182
|
end
|
|
164
183
|
|
|
165
184
|
def test_chat_sends_post_to_responses_with_default_payload
|
|
166
|
-
client =
|
|
185
|
+
client = provider.new(api_key: "token-abc")
|
|
167
186
|
|
|
168
187
|
with_stubbed_http(success_response("Hello!")) do |captured, _response|
|
|
169
188
|
result = client.chat("Say hello")
|
|
@@ -189,7 +208,7 @@ class AiLiteTest < Minitest::Test
|
|
|
189
208
|
end
|
|
190
209
|
|
|
191
210
|
def test_chat_allows_overriding_max_output_tokens
|
|
192
|
-
client =
|
|
211
|
+
client = provider.new(api_key: "token-abc")
|
|
193
212
|
|
|
194
213
|
with_stubbed_http(success_response("Short")) do |captured, _response|
|
|
195
214
|
client.chat("Summarize", max_output_tokens: 500)
|
|
@@ -200,13 +219,13 @@ class AiLiteTest < Minitest::Test
|
|
|
200
219
|
end
|
|
201
220
|
|
|
202
221
|
def test_chat_uses_configured_max_output_tokens
|
|
203
|
-
|
|
222
|
+
provider.configure do |config|
|
|
204
223
|
config.api_key = "configured-key"
|
|
205
224
|
config.max_output_tokens = 750
|
|
206
225
|
end
|
|
207
226
|
|
|
208
227
|
with_stubbed_http(success_response("Configured")) do |captured, _response|
|
|
209
|
-
|
|
228
|
+
provider.chat("Use configured defaults")
|
|
210
229
|
payload = JSON.parse(captured[:http].last_request.body)
|
|
211
230
|
|
|
212
231
|
assert_equal 750, payload["max_output_tokens"]
|
|
@@ -215,7 +234,7 @@ class AiLiteTest < Minitest::Test
|
|
|
215
234
|
end
|
|
216
235
|
|
|
217
236
|
def test_chat_includes_instructions_model_and_options
|
|
218
|
-
client =
|
|
237
|
+
client = provider.new(api_key: "token-abc")
|
|
219
238
|
|
|
220
239
|
with_stubbed_http(success_response("Aye")) do |captured, _response|
|
|
221
240
|
client.chat(
|
|
@@ -238,15 +257,13 @@ class AiLiteTest < Minitest::Test
|
|
|
238
257
|
end
|
|
239
258
|
end
|
|
240
259
|
|
|
241
|
-
def
|
|
242
|
-
client =
|
|
260
|
+
def test_chat_supports_previous_response_id
|
|
261
|
+
client = provider.new(api_key: "token-abc")
|
|
243
262
|
|
|
244
263
|
with_stubbed_http(success_response("Follow-up")) do |captured, _response|
|
|
245
264
|
result = client.chat(
|
|
246
265
|
"Explain why that is funny",
|
|
247
|
-
|
|
248
|
-
previous_response_id: "resp_previous_123"
|
|
249
|
-
}
|
|
266
|
+
previous_response_id: "resp_previous_123"
|
|
250
267
|
)
|
|
251
268
|
payload = JSON.parse(captured[:http].last_request.body)
|
|
252
269
|
|
|
@@ -256,8 +273,55 @@ class AiLiteTest < Minitest::Test
|
|
|
256
273
|
end
|
|
257
274
|
end
|
|
258
275
|
|
|
276
|
+
def test_chat_supports_message_array_input
|
|
277
|
+
client = provider.new(api_key: "token-abc")
|
|
278
|
+
messages = [
|
|
279
|
+
{ role: "developer", content: "Be concise." },
|
|
280
|
+
{ role: "user", content: "Say hello." }
|
|
281
|
+
]
|
|
282
|
+
|
|
283
|
+
with_stubbed_http(success_response("Hello!")) do |captured, _response|
|
|
284
|
+
client.chat(messages)
|
|
285
|
+
payload = JSON.parse(captured[:http].last_request.body)
|
|
286
|
+
|
|
287
|
+
assert_equal(
|
|
288
|
+
[
|
|
289
|
+
{ "role" => "developer", "content" => "Be concise." },
|
|
290
|
+
{ "role" => "user", "content" => "Say hello." }
|
|
291
|
+
],
|
|
292
|
+
payload["input"]
|
|
293
|
+
)
|
|
294
|
+
end
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
def test_chat_first_class_previous_response_id_overrides_options
|
|
298
|
+
client = provider.new(api_key: "token-abc")
|
|
299
|
+
|
|
300
|
+
with_stubbed_http(success_response("Follow-up")) do |captured, _response|
|
|
301
|
+
client.chat(
|
|
302
|
+
"Continue",
|
|
303
|
+
previous_response_id: "resp_keyword",
|
|
304
|
+
options: { previous_response_id: "resp_options" }
|
|
305
|
+
)
|
|
306
|
+
payload = JSON.parse(captured[:http].last_request.body)
|
|
307
|
+
|
|
308
|
+
assert_equal "resp_keyword", payload["previous_response_id"]
|
|
309
|
+
end
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
def test_chat_keeps_supporting_previous_response_id_through_options
|
|
313
|
+
client = provider.new(api_key: "token-abc")
|
|
314
|
+
|
|
315
|
+
with_stubbed_http(success_response("Follow-up")) do |captured, _response|
|
|
316
|
+
client.chat("Continue", options: { previous_response_id: "resp_options" })
|
|
317
|
+
payload = JSON.parse(captured[:http].last_request.body)
|
|
318
|
+
|
|
319
|
+
assert_equal "resp_options", payload["previous_response_id"]
|
|
320
|
+
end
|
|
321
|
+
end
|
|
322
|
+
|
|
259
323
|
def test_extracts_text_from_nested_output_text_items
|
|
260
|
-
client =
|
|
324
|
+
client = provider.new(api_key: "token-abc")
|
|
261
325
|
body = JSON.generate(
|
|
262
326
|
"id" => "resp_nested_123",
|
|
263
327
|
"output" => [
|
|
@@ -283,7 +347,7 @@ class AiLiteTest < Minitest::Test
|
|
|
283
347
|
end
|
|
284
348
|
|
|
285
349
|
def test_parses_json_looking_output_with_string_keys
|
|
286
|
-
client =
|
|
350
|
+
client = provider.new(api_key: "token-abc")
|
|
287
351
|
|
|
288
352
|
with_stubbed_http(success_response(JSON.generate("valid" => true))) do |_captured, _response|
|
|
289
353
|
result = client.chat("Validate")
|
|
@@ -296,7 +360,7 @@ class AiLiteTest < Minitest::Test
|
|
|
296
360
|
end
|
|
297
361
|
|
|
298
362
|
def test_debug_true_returns_raw_success_response
|
|
299
|
-
client =
|
|
363
|
+
client = provider.new(api_key: "token-abc")
|
|
300
364
|
|
|
301
365
|
with_stubbed_http(success_response("Hello!")) do |_captured, _response|
|
|
302
366
|
result = client.chat("Say hello", debug: true)
|
|
@@ -319,7 +383,7 @@ class AiLiteTest < Minitest::Test
|
|
|
319
383
|
end
|
|
320
384
|
|
|
321
385
|
def test_moderate_sends_post_to_moderations_with_default_payload
|
|
322
|
-
client =
|
|
386
|
+
client = provider.new(api_key: "token-abc")
|
|
323
387
|
|
|
324
388
|
with_stubbed_http(moderation_response) do |captured, _response|
|
|
325
389
|
result = client.moderate("Some user submitted text")
|
|
@@ -344,13 +408,13 @@ class AiLiteTest < Minitest::Test
|
|
|
344
408
|
end
|
|
345
409
|
|
|
346
410
|
def test_moderate_uses_class_level_configured_client
|
|
347
|
-
|
|
411
|
+
provider.configure do |config|
|
|
348
412
|
config.api_key = "configured-key"
|
|
349
413
|
config.moderation_model = "omni-moderation-config"
|
|
350
414
|
end
|
|
351
415
|
|
|
352
416
|
with_stubbed_http(moderation_response) do |captured, _response|
|
|
353
|
-
|
|
417
|
+
provider.moderate("Use configured defaults")
|
|
354
418
|
payload = JSON.parse(captured[:http].last_request.body)
|
|
355
419
|
|
|
356
420
|
assert_equal "omni-moderation-config", payload["model"]
|
|
@@ -359,7 +423,7 @@ class AiLiteTest < Minitest::Test
|
|
|
359
423
|
end
|
|
360
424
|
|
|
361
425
|
def test_moderate_accepts_text_and_image_url_keywords
|
|
362
|
-
client =
|
|
426
|
+
client = provider.new(api_key: "token-abc")
|
|
363
427
|
|
|
364
428
|
with_stubbed_http(moderation_response) do |captured, _response|
|
|
365
429
|
client.moderate(
|
|
@@ -382,7 +446,7 @@ class AiLiteTest < Minitest::Test
|
|
|
382
446
|
end
|
|
383
447
|
|
|
384
448
|
def test_moderate_accepts_raw_openai_input_shape
|
|
385
|
-
client =
|
|
449
|
+
client = provider.new(api_key: "token-abc")
|
|
386
450
|
input = [
|
|
387
451
|
{ type: "text", text: "Caption" },
|
|
388
452
|
{
|
|
@@ -411,7 +475,7 @@ class AiLiteTest < Minitest::Test
|
|
|
411
475
|
end
|
|
412
476
|
|
|
413
477
|
def test_moderate_reads_image_path_as_data_url
|
|
414
|
-
client =
|
|
478
|
+
client = provider.new(api_key: "token-abc")
|
|
415
479
|
|
|
416
480
|
Tempfile.create(["moderation", ".png"]) do |file|
|
|
417
481
|
file.binmode
|
|
@@ -436,7 +500,7 @@ class AiLiteTest < Minitest::Test
|
|
|
436
500
|
end
|
|
437
501
|
|
|
438
502
|
def test_moderate_returns_all_results_for_multiple_inputs
|
|
439
|
-
client =
|
|
503
|
+
client = provider.new(api_key: "token-abc")
|
|
440
504
|
safe_result = moderation_result(flagged: false)
|
|
441
505
|
flagged_result = moderation_result(flagged: true)
|
|
442
506
|
|
|
@@ -450,7 +514,7 @@ class AiLiteTest < Minitest::Test
|
|
|
450
514
|
end
|
|
451
515
|
|
|
452
516
|
def test_moderate_local_input_errors_return_standard_envelope
|
|
453
|
-
client =
|
|
517
|
+
client = provider.new(api_key: "token-abc")
|
|
454
518
|
|
|
455
519
|
result = client.moderate
|
|
456
520
|
|
|
@@ -462,7 +526,7 @@ class AiLiteTest < Minitest::Test
|
|
|
462
526
|
end
|
|
463
527
|
|
|
464
528
|
def test_embed_sends_post_to_embeddings_with_default_payload
|
|
465
|
-
client =
|
|
529
|
+
client = provider.new(api_key: "token-abc")
|
|
466
530
|
|
|
467
531
|
with_stubbed_http(embedding_response([[0.12, -0.34, 0.56]])) do |captured, _response|
|
|
468
532
|
result = client.embed("Text to vectorize")
|
|
@@ -487,7 +551,7 @@ class AiLiteTest < Minitest::Test
|
|
|
487
551
|
end
|
|
488
552
|
|
|
489
553
|
def test_embed_returns_vector_list_for_multiple_inputs
|
|
490
|
-
client =
|
|
554
|
+
client = provider.new(api_key: "token-abc")
|
|
491
555
|
embeddings = [
|
|
492
556
|
[0.11, 0.22, 0.33],
|
|
493
557
|
[0.44, 0.55, 0.66]
|
|
@@ -505,7 +569,7 @@ class AiLiteTest < Minitest::Test
|
|
|
505
569
|
end
|
|
506
570
|
|
|
507
571
|
def test_embed_includes_options_dimensions_encoding_format_and_model
|
|
508
|
-
client =
|
|
572
|
+
client = provider.new(api_key: "token-abc")
|
|
509
573
|
|
|
510
574
|
with_stubbed_http(embedding_response([[0.12, -0.34]])) do |captured, _response|
|
|
511
575
|
client.embed(
|
|
@@ -528,13 +592,13 @@ class AiLiteTest < Minitest::Test
|
|
|
528
592
|
end
|
|
529
593
|
|
|
530
594
|
def test_embed_uses_class_level_configured_client
|
|
531
|
-
|
|
595
|
+
provider.configure do |config|
|
|
532
596
|
config.api_key = "configured-key"
|
|
533
597
|
config.embedding_model = "text-embedding-config"
|
|
534
598
|
end
|
|
535
599
|
|
|
536
600
|
with_stubbed_http(embedding_response([[0.12, -0.34, 0.56]])) do |captured, _response|
|
|
537
|
-
|
|
601
|
+
provider.embed("Use configured defaults")
|
|
538
602
|
payload = JSON.parse(captured[:http].last_request.body)
|
|
539
603
|
|
|
540
604
|
assert_equal "text-embedding-config", payload["model"]
|
|
@@ -543,7 +607,7 @@ class AiLiteTest < Minitest::Test
|
|
|
543
607
|
end
|
|
544
608
|
|
|
545
609
|
def test_embed_debug_true_returns_raw_usage
|
|
546
|
-
client =
|
|
610
|
+
client = provider.new(api_key: "token-abc")
|
|
547
611
|
|
|
548
612
|
with_stubbed_http(embedding_response([[0.12, -0.34, 0.56]])) do |_captured, _response|
|
|
549
613
|
result = client.embed("Text to vectorize", debug: true)
|
|
@@ -554,7 +618,7 @@ class AiLiteTest < Minitest::Test
|
|
|
554
618
|
end
|
|
555
619
|
|
|
556
620
|
def test_image_sends_post_to_images_with_default_payload
|
|
557
|
-
client =
|
|
621
|
+
client = provider.new(api_key: "token-abc")
|
|
558
622
|
image_data = Base64.strict_encode64("fake image")
|
|
559
623
|
|
|
560
624
|
with_stubbed_http(image_response(b64_json: image_data)) do |captured, _response|
|
|
@@ -580,7 +644,7 @@ class AiLiteTest < Minitest::Test
|
|
|
580
644
|
end
|
|
581
645
|
|
|
582
646
|
def test_image_includes_options_size_quality_background_output_format_and_model
|
|
583
|
-
client =
|
|
647
|
+
client = provider.new(api_key: "token-abc")
|
|
584
648
|
|
|
585
649
|
with_stubbed_http(image_response) do |captured, _response|
|
|
586
650
|
client.image(
|
|
@@ -609,13 +673,13 @@ class AiLiteTest < Minitest::Test
|
|
|
609
673
|
end
|
|
610
674
|
|
|
611
675
|
def test_image_uses_class_level_configured_client
|
|
612
|
-
|
|
676
|
+
provider.configure do |config|
|
|
613
677
|
config.api_key = "configured-key"
|
|
614
678
|
config.image_model = "gpt-image-config"
|
|
615
679
|
end
|
|
616
680
|
|
|
617
681
|
with_stubbed_http(image_response) do |captured, _response|
|
|
618
|
-
|
|
682
|
+
provider.image("Use configured defaults")
|
|
619
683
|
payload = JSON.parse(captured[:http].last_request.body)
|
|
620
684
|
|
|
621
685
|
assert_equal "gpt-image-config", payload["model"]
|
|
@@ -624,7 +688,7 @@ class AiLiteTest < Minitest::Test
|
|
|
624
688
|
end
|
|
625
689
|
|
|
626
690
|
def test_image_writes_decoded_content_to_output_path
|
|
627
|
-
client =
|
|
691
|
+
client = provider.new(api_key: "token-abc")
|
|
628
692
|
image_data = Base64.strict_encode64("fake image")
|
|
629
693
|
|
|
630
694
|
Tempfile.create(["generated", ".png"]) do |file|
|
|
@@ -638,7 +702,7 @@ class AiLiteTest < Minitest::Test
|
|
|
638
702
|
end
|
|
639
703
|
|
|
640
704
|
def test_image_debug_true_returns_raw_usage
|
|
641
|
-
client =
|
|
705
|
+
client = provider.new(api_key: "token-abc")
|
|
642
706
|
image_data = Base64.strict_encode64("fake image")
|
|
643
707
|
|
|
644
708
|
with_stubbed_http(image_response(b64_json: image_data)) do |_captured, _response|
|
|
@@ -650,7 +714,7 @@ class AiLiteTest < Minitest::Test
|
|
|
650
714
|
end
|
|
651
715
|
|
|
652
716
|
def test_image_output_path_without_image_data_returns_standard_envelope
|
|
653
|
-
client =
|
|
717
|
+
client = provider.new(api_key: "token-abc")
|
|
654
718
|
|
|
655
719
|
Tempfile.create(["generated", ".png"]) do |file|
|
|
656
720
|
with_stubbed_http(image_response(b64_json: nil)) do |_captured, _response|
|
|
@@ -666,7 +730,7 @@ class AiLiteTest < Minitest::Test
|
|
|
666
730
|
end
|
|
667
731
|
|
|
668
732
|
def test_speak_sends_post_to_audio_speech_with_default_payload
|
|
669
|
-
client =
|
|
733
|
+
client = provider.new(api_key: "token-abc")
|
|
670
734
|
|
|
671
735
|
with_stubbed_http(speech_response("fake audio")) do |captured, _response|
|
|
672
736
|
result = client.speak("Read this aloud")
|
|
@@ -692,7 +756,7 @@ class AiLiteTest < Minitest::Test
|
|
|
692
756
|
end
|
|
693
757
|
|
|
694
758
|
def test_speak_includes_options_response_format_speed_instructions_model_and_voice
|
|
695
|
-
client =
|
|
759
|
+
client = provider.new(api_key: "token-abc")
|
|
696
760
|
|
|
697
761
|
with_stubbed_http(speech_response) do |captured, _response|
|
|
698
762
|
client.speak(
|
|
@@ -719,14 +783,14 @@ class AiLiteTest < Minitest::Test
|
|
|
719
783
|
end
|
|
720
784
|
|
|
721
785
|
def test_speak_uses_class_level_configured_client
|
|
722
|
-
|
|
786
|
+
provider.configure do |config|
|
|
723
787
|
config.api_key = "configured-key"
|
|
724
788
|
config.speech_model = "gpt-speech-config"
|
|
725
789
|
config.speech_voice = "marin"
|
|
726
790
|
end
|
|
727
791
|
|
|
728
792
|
with_stubbed_http(speech_response) do |captured, _response|
|
|
729
|
-
|
|
793
|
+
provider.speak("Use configured defaults")
|
|
730
794
|
payload = JSON.parse(captured[:http].last_request.body)
|
|
731
795
|
|
|
732
796
|
assert_equal "gpt-speech-config", payload["model"]
|
|
@@ -736,7 +800,7 @@ class AiLiteTest < Minitest::Test
|
|
|
736
800
|
end
|
|
737
801
|
|
|
738
802
|
def test_speak_writes_audio_to_output_path_and_returns_metadata
|
|
739
|
-
client =
|
|
803
|
+
client = provider.new(api_key: "token-abc")
|
|
740
804
|
|
|
741
805
|
Tempfile.create(["speech", ".mp3"]) do |file|
|
|
742
806
|
with_stubbed_http(speech_response("fake audio")) do |_captured, _response|
|
|
@@ -756,7 +820,7 @@ class AiLiteTest < Minitest::Test
|
|
|
756
820
|
end
|
|
757
821
|
|
|
758
822
|
def test_speak_base64_true_returns_encoded_audio
|
|
759
|
-
client =
|
|
823
|
+
client = provider.new(api_key: "token-abc")
|
|
760
824
|
|
|
761
825
|
with_stubbed_http(speech_response("fake audio")) do |_captured, _response|
|
|
762
826
|
result = client.speak("Encode this", base64: true)
|
|
@@ -767,7 +831,7 @@ class AiLiteTest < Minitest::Test
|
|
|
767
831
|
end
|
|
768
832
|
|
|
769
833
|
def test_speak_debug_true_returns_raw_audio
|
|
770
|
-
client =
|
|
834
|
+
client = provider.new(api_key: "token-abc")
|
|
771
835
|
|
|
772
836
|
with_stubbed_http(speech_response("fake audio")) do |_captured, _response|
|
|
773
837
|
result = client.speak("Debug this", debug: true)
|
|
@@ -778,7 +842,7 @@ class AiLiteTest < Minitest::Test
|
|
|
778
842
|
end
|
|
779
843
|
|
|
780
844
|
def test_speak_http_errors_return_standard_envelope
|
|
781
|
-
client =
|
|
845
|
+
client = provider.new(api_key: "token-abc")
|
|
782
846
|
body = JSON.generate("error" => { "message" => "Unsupported voice" })
|
|
783
847
|
|
|
784
848
|
with_stubbed_http(FakeResponse.new("400", body)) do |_captured, _response|
|
|
@@ -793,7 +857,7 @@ class AiLiteTest < Minitest::Test
|
|
|
793
857
|
end
|
|
794
858
|
|
|
795
859
|
def test_transcribe_sends_post_to_audio_transcriptions_with_default_multipart_payload
|
|
796
|
-
client =
|
|
860
|
+
client = provider.new(api_key: "token-abc")
|
|
797
861
|
|
|
798
862
|
Tempfile.create(["meeting", ".mp3"]) do |file|
|
|
799
863
|
file.binmode
|
|
@@ -823,7 +887,7 @@ class AiLiteTest < Minitest::Test
|
|
|
823
887
|
end
|
|
824
888
|
|
|
825
889
|
def test_transcribe_includes_options_and_optional_fields
|
|
826
|
-
client =
|
|
890
|
+
client = provider.new(api_key: "token-abc")
|
|
827
891
|
|
|
828
892
|
Tempfile.create(["meeting", ".wav"]) do |file|
|
|
829
893
|
file.binmode
|
|
@@ -863,7 +927,7 @@ class AiLiteTest < Minitest::Test
|
|
|
863
927
|
end
|
|
864
928
|
|
|
865
929
|
def test_transcribe_uses_class_level_configured_client
|
|
866
|
-
|
|
930
|
+
provider.configure do |config|
|
|
867
931
|
config.api_key = "configured-key"
|
|
868
932
|
config.transcription_model = "gpt-transcription-config"
|
|
869
933
|
end
|
|
@@ -874,7 +938,7 @@ class AiLiteTest < Minitest::Test
|
|
|
874
938
|
file.flush
|
|
875
939
|
|
|
876
940
|
with_stubbed_http(transcription_response) do |captured, _response|
|
|
877
|
-
|
|
941
|
+
provider.transcribe(file.path)
|
|
878
942
|
request = captured[:http].last_request
|
|
879
943
|
|
|
880
944
|
assert_multipart_field request.body, "model", "gpt-transcription-config"
|
|
@@ -884,7 +948,7 @@ class AiLiteTest < Minitest::Test
|
|
|
884
948
|
end
|
|
885
949
|
|
|
886
950
|
def test_transcribe_returns_plain_text_response_formats
|
|
887
|
-
client =
|
|
951
|
+
client = provider.new(api_key: "token-abc")
|
|
888
952
|
|
|
889
953
|
Tempfile.create(["meeting", ".webm"]) do |file|
|
|
890
954
|
file.binmode
|
|
@@ -903,7 +967,7 @@ class AiLiteTest < Minitest::Test
|
|
|
903
967
|
end
|
|
904
968
|
|
|
905
969
|
def test_transcribe_debug_true_returns_raw_response
|
|
906
|
-
client =
|
|
970
|
+
client = provider.new(api_key: "token-abc")
|
|
907
971
|
|
|
908
972
|
Tempfile.create(["meeting", ".ogg"]) do |file|
|
|
909
973
|
file.binmode
|
|
@@ -920,7 +984,7 @@ class AiLiteTest < Minitest::Test
|
|
|
920
984
|
end
|
|
921
985
|
|
|
922
986
|
def test_transcribe_http_errors_return_standard_envelope
|
|
923
|
-
client =
|
|
987
|
+
client = provider.new(api_key: "token-abc")
|
|
924
988
|
body = JSON.generate("error" => { "message" => "Unsupported audio format" })
|
|
925
989
|
|
|
926
990
|
Tempfile.create(["meeting", ".mp3"]) do |file|
|
|
@@ -941,7 +1005,7 @@ class AiLiteTest < Minitest::Test
|
|
|
941
1005
|
end
|
|
942
1006
|
|
|
943
1007
|
def test_transcribe_local_file_errors_return_standard_envelope
|
|
944
|
-
client =
|
|
1008
|
+
client = provider.new(api_key: "token-abc")
|
|
945
1009
|
|
|
946
1010
|
missing_result = client.transcribe("tmp/missing-audio.mp3")
|
|
947
1011
|
assert_nil missing_result["content"]
|
|
@@ -958,7 +1022,7 @@ class AiLiteTest < Minitest::Test
|
|
|
958
1022
|
end
|
|
959
1023
|
|
|
960
1024
|
def test_http_errors_return_standard_envelope
|
|
961
|
-
client =
|
|
1025
|
+
client = provider.new(api_key: "token-abc")
|
|
962
1026
|
body = JSON.generate("error" => { "message" => "Invalid API key" })
|
|
963
1027
|
|
|
964
1028
|
with_stubbed_http(FakeResponse.new("401", body)) do |_captured, _response|
|
|
@@ -973,7 +1037,7 @@ class AiLiteTest < Minitest::Test
|
|
|
973
1037
|
end
|
|
974
1038
|
|
|
975
1039
|
def test_debug_true_returns_raw_http_error_response
|
|
976
|
-
client =
|
|
1040
|
+
client = provider.new(api_key: "token-abc")
|
|
977
1041
|
body = JSON.generate("error" => { "message" => "Invalid API key" })
|
|
978
1042
|
|
|
979
1043
|
with_stubbed_http(FakeResponse.new("401", body)) do |_captured, _response|
|
|
@@ -987,7 +1051,7 @@ class AiLiteTest < Minitest::Test
|
|
|
987
1051
|
end
|
|
988
1052
|
|
|
989
1053
|
def test_response_parse_errors_return_standard_envelope
|
|
990
|
-
client =
|
|
1054
|
+
client = provider.new(api_key: "token-abc")
|
|
991
1055
|
|
|
992
1056
|
with_stubbed_http(FakeResponse.new("200", "not-json")) do |_captured, _response|
|
|
993
1057
|
result = client.chat("Say hello")
|
|
@@ -1001,7 +1065,7 @@ class AiLiteTest < Minitest::Test
|
|
|
1001
1065
|
end
|
|
1002
1066
|
|
|
1003
1067
|
def test_debug_true_returns_raw_response_parse_error_body
|
|
1004
|
-
client =
|
|
1068
|
+
client = provider.new(api_key: "token-abc")
|
|
1005
1069
|
|
|
1006
1070
|
with_stubbed_http(FakeResponse.new("200", "not-json")) do |_captured, _response|
|
|
1007
1071
|
result = client.chat("Say hello", debug: true)
|
|
@@ -1014,7 +1078,7 @@ class AiLiteTest < Minitest::Test
|
|
|
1014
1078
|
end
|
|
1015
1079
|
|
|
1016
1080
|
def test_network_errors_return_standard_envelope
|
|
1017
|
-
client =
|
|
1081
|
+
client = provider.new(api_key: "token-abc")
|
|
1018
1082
|
original_start = Net::HTTP.method(:start)
|
|
1019
1083
|
|
|
1020
1084
|
Net::HTTP.singleton_class.send(:define_method, :start) do |_host, _port, use_ssl:, &_block|
|
|
@@ -1163,3 +1227,108 @@ class AiLiteTest < Minitest::Test
|
|
|
1163
1227
|
Net::HTTP.singleton_class.send(:define_method, :start, original_start)
|
|
1164
1228
|
end
|
|
1165
1229
|
end
|
|
1230
|
+
|
|
1231
|
+
# Run the same HTTP and result contract against every legacy entry point.
|
|
1232
|
+
class AiLiteLegacyTest < AiLiteTest
|
|
1233
|
+
def provider
|
|
1234
|
+
AiLite
|
|
1235
|
+
end
|
|
1236
|
+
end
|
|
1237
|
+
|
|
1238
|
+
class AiLiteNamespaceTest < Minitest::Test
|
|
1239
|
+
def setup
|
|
1240
|
+
AiLite::OpenAI.reset_configuration!
|
|
1241
|
+
end
|
|
1242
|
+
|
|
1243
|
+
def test_provider_calls_are_quiet_and_constants_remain_compatible
|
|
1244
|
+
_stdout, stderr = capture_io do
|
|
1245
|
+
AiLite::OpenAI.configure { |config| config.api_key = "test-key" }
|
|
1246
|
+
assert_instance_of AiLite::OpenAI, AiLite::OpenAI.client
|
|
1247
|
+
AiLite::OpenAI.new
|
|
1248
|
+
end
|
|
1249
|
+
|
|
1250
|
+
assert_empty stderr
|
|
1251
|
+
assert_same AiLite::OpenAI::Configuration, AiLite::Configuration
|
|
1252
|
+
assert_equal AiLite::OpenAI::API_BASE_URL, AiLite::API_BASE_URL
|
|
1253
|
+
refute AiLite::OpenAI < AiLite
|
|
1254
|
+
end
|
|
1255
|
+
|
|
1256
|
+
def test_legacy_configuration_and_client_are_shared_with_openai
|
|
1257
|
+
capture_io do
|
|
1258
|
+
AiLite.configure { |config| config.api_key = "legacy-key" }
|
|
1259
|
+
assert_same AiLite.configuration, AiLite::OpenAI.configuration
|
|
1260
|
+
assert_same AiLite.client, AiLite::OpenAI.client
|
|
1261
|
+
assert_equal "legacy-key", AiLite::OpenAI.new.api_key
|
|
1262
|
+
assert_instance_of AiLite::OpenAI, AiLite.new
|
|
1263
|
+
|
|
1264
|
+
first_client = AiLite.client
|
|
1265
|
+
AiLite::OpenAI.configure { |config| config.api_key = "provider-key" }
|
|
1266
|
+
refute_same first_client, AiLite.client
|
|
1267
|
+
assert_equal "provider-key", AiLite.new.api_key
|
|
1268
|
+
|
|
1269
|
+
second_client = AiLite::OpenAI.client
|
|
1270
|
+
AiLite.reset_client!
|
|
1271
|
+
refute_same second_client, AiLite::OpenAI.client
|
|
1272
|
+
AiLite.reset_configuration!
|
|
1273
|
+
assert_nil AiLite::OpenAI.configuration.api_key
|
|
1274
|
+
end
|
|
1275
|
+
end
|
|
1276
|
+
|
|
1277
|
+
def test_each_legacy_entry_point_warns_only_once_and_preserves_arguments
|
|
1278
|
+
previous = AiLite.instance_variable_get(:@deprecated_entry_points)
|
|
1279
|
+
AiLite.instance_variable_set(:@deprecated_entry_points, {})
|
|
1280
|
+
methods = %i[new configuration configure reset_configuration! client reset_client!
|
|
1281
|
+
chat chat_stream moderate embed image speak transcribe
|
|
1282
|
+
available_models model_available? configured_models check_configured_models]
|
|
1283
|
+
|
|
1284
|
+
stdout, stderr = capture_io do
|
|
1285
|
+
methods.each do |name|
|
|
1286
|
+
test_case = self
|
|
1287
|
+
handler = lambda do |*args, **kwargs, &block|
|
|
1288
|
+
test_case.assert_equal ["input"], args
|
|
1289
|
+
test_case.assert_equal({ debug: true }, kwargs)
|
|
1290
|
+
test_case.assert_equal :block_result, block.call
|
|
1291
|
+
:result
|
|
1292
|
+
end
|
|
1293
|
+
next if name == :new
|
|
1294
|
+
|
|
1295
|
+
original = AiLite::OpenAI.method(name)
|
|
1296
|
+
begin
|
|
1297
|
+
AiLite::OpenAI.define_singleton_method(name, &handler)
|
|
1298
|
+
2.times do
|
|
1299
|
+
assert_equal :result, AiLite.public_send(name, "input", debug: true) { :block_result }
|
|
1300
|
+
end
|
|
1301
|
+
ensure
|
|
1302
|
+
AiLite::OpenAI.define_singleton_method(name, original)
|
|
1303
|
+
end
|
|
1304
|
+
end
|
|
1305
|
+
2.times { AiLite.new(api_key: "test-key") }
|
|
1306
|
+
end
|
|
1307
|
+
|
|
1308
|
+
assert_empty stdout
|
|
1309
|
+
assert_equal methods.length, stderr.lines.length
|
|
1310
|
+
methods.each do |name|
|
|
1311
|
+
assert_includes stderr, "AiLite.#{name} is deprecated. Use AiLite::OpenAI.#{name} instead."
|
|
1312
|
+
end
|
|
1313
|
+
assert_includes stderr, "removed in v2.0"
|
|
1314
|
+
ensure
|
|
1315
|
+
AiLite.instance_variable_set(:@deprecated_entry_points, previous)
|
|
1316
|
+
end
|
|
1317
|
+
|
|
1318
|
+
def test_provider_can_be_required_directly_in_a_fresh_process
|
|
1319
|
+
require "open3"
|
|
1320
|
+
require "rbconfig"
|
|
1321
|
+
script = <<~'CODE'
|
|
1322
|
+
require "ai_lite/openai"
|
|
1323
|
+
abort unless AiLite::OpenAI.new(api_key: "test-key").model == AiLite::OpenAI::DEFAULT_MODEL
|
|
1324
|
+
require "ai_lite"
|
|
1325
|
+
abort unless AiLite::Configuration.equal?(AiLite::OpenAI::Configuration)
|
|
1326
|
+
CODE
|
|
1327
|
+
stdout, stderr, status = Open3.capture3(
|
|
1328
|
+
RbConfig.ruby, "-I#{File.expand_path('../lib', __dir__)}", "-e", script
|
|
1329
|
+
)
|
|
1330
|
+
assert status.success?, stderr
|
|
1331
|
+
assert_empty stdout
|
|
1332
|
+
assert_empty stderr
|
|
1333
|
+
end
|
|
1334
|
+
end
|