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/lib/ai_lite.rb
CHANGED
|
@@ -1,637 +1,46 @@
|
|
|
1
|
-
require "base64"
|
|
2
|
-
require "json"
|
|
3
|
-
require "net/http"
|
|
4
|
-
require "securerandom"
|
|
5
|
-
require "uri"
|
|
6
1
|
require_relative "ai_lite/version"
|
|
2
|
+
require_relative "ai_lite/openai"
|
|
3
|
+
require_relative "ai_lite/gemini"
|
|
7
4
|
|
|
8
5
|
class AiLite
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
DEFAULT_EMBEDDING_MODEL = "text-embedding-3-small".freeze
|
|
13
|
-
DEFAULT_IMAGE_MODEL = "gpt-image-2".freeze
|
|
14
|
-
DEFAULT_SPEECH_MODEL = "gpt-4o-mini-tts".freeze
|
|
15
|
-
DEFAULT_SPEECH_VOICE = "alloy".freeze
|
|
16
|
-
DEFAULT_SPEECH_FORMAT = "mp3".freeze
|
|
17
|
-
DEFAULT_TRANSCRIPTION_MODEL = "gpt-transcribe".freeze
|
|
18
|
-
DEFAULT_TIMEOUT = 120
|
|
19
|
-
DEFAULT_MAX_OUTPUT_TOKENS = 2000
|
|
20
|
-
IMAGE_MIME_TYPES = {
|
|
21
|
-
".gif" => "image/gif",
|
|
22
|
-
".jpeg" => "image/jpeg",
|
|
23
|
-
".jpg" => "image/jpeg",
|
|
24
|
-
".png" => "image/png",
|
|
25
|
-
".webp" => "image/webp"
|
|
26
|
-
}.freeze
|
|
27
|
-
AUDIO_MIME_TYPES = {
|
|
28
|
-
".flac" => "audio/flac",
|
|
29
|
-
".m4a" => "audio/mp4",
|
|
30
|
-
".mp3" => "audio/mpeg",
|
|
31
|
-
".mp4" => "audio/mp4",
|
|
32
|
-
".mpeg" => "audio/mpeg",
|
|
33
|
-
".mpga" => "audio/mpeg",
|
|
34
|
-
".ogg" => "audio/ogg",
|
|
35
|
-
".wav" => "audio/wav",
|
|
36
|
-
".webm" => "audio/webm"
|
|
37
|
-
}.freeze
|
|
38
|
-
|
|
39
|
-
class Configuration
|
|
40
|
-
attr_accessor :api_key, :model, :moderation_model, :embedding_model, :image_model, :speech_model, :speech_voice, :transcription_model, :timeout, :max_output_tokens
|
|
41
|
-
|
|
42
|
-
def initialize
|
|
43
|
-
@api_key = nil
|
|
44
|
-
@model = DEFAULT_MODEL
|
|
45
|
-
@moderation_model = DEFAULT_MODERATION_MODEL
|
|
46
|
-
@embedding_model = DEFAULT_EMBEDDING_MODEL
|
|
47
|
-
@image_model = DEFAULT_IMAGE_MODEL
|
|
48
|
-
@speech_model = DEFAULT_SPEECH_MODEL
|
|
49
|
-
@speech_voice = DEFAULT_SPEECH_VOICE
|
|
50
|
-
@transcription_model = DEFAULT_TRANSCRIPTION_MODEL
|
|
51
|
-
@timeout = DEFAULT_TIMEOUT
|
|
52
|
-
@max_output_tokens = DEFAULT_MAX_OUTPUT_TOKENS
|
|
53
|
-
end
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
class << self
|
|
57
|
-
def configuration
|
|
58
|
-
@configuration ||= Configuration.new
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
def configure
|
|
62
|
-
yield(configuration)
|
|
63
|
-
reset_client!
|
|
64
|
-
configuration
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
def reset_configuration!
|
|
68
|
-
@configuration = Configuration.new
|
|
69
|
-
reset_client!
|
|
70
|
-
configuration
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
def client
|
|
74
|
-
@client ||= new
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
def chat(message, **kwargs)
|
|
78
|
-
client.chat(message, **kwargs)
|
|
79
|
-
end
|
|
80
|
-
|
|
81
|
-
def moderate(input = nil, **kwargs)
|
|
82
|
-
client.moderate(input, **kwargs)
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
def embed(input, **kwargs)
|
|
86
|
-
client.embed(input, **kwargs)
|
|
87
|
-
end
|
|
88
|
-
|
|
89
|
-
def image(prompt, **kwargs)
|
|
90
|
-
client.image(prompt, **kwargs)
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
def speak(text, **kwargs)
|
|
94
|
-
client.speak(text, **kwargs)
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
def transcribe(file_path, **kwargs)
|
|
98
|
-
client.transcribe(file_path, **kwargs)
|
|
99
|
-
end
|
|
100
|
-
|
|
101
|
-
def reset_client!
|
|
102
|
-
@client = nil
|
|
103
|
-
end
|
|
104
|
-
end
|
|
105
|
-
|
|
106
|
-
attr_reader :api_key, :model, :moderation_model, :embedding_model, :image_model, :speech_model, :speech_voice, :transcription_model, :timeout, :max_output_tokens, :headers
|
|
107
|
-
|
|
108
|
-
def initialize(api_key: nil, model: nil, moderation_model: nil, embedding_model: nil, image_model: nil, speech_model: nil, speech_voice: nil, transcription_model: nil, timeout: nil, max_output_tokens: nil)
|
|
109
|
-
@api_key = api_key || self.class.configuration.api_key || ENV["OPENAI_API_KEY"] || ENV["OPEN_AI_TOKEN"]
|
|
110
|
-
raise ArgumentError, "Missing OpenAI API key" if @api_key.to_s.strip.empty?
|
|
111
|
-
|
|
112
|
-
@model = model || self.class.configuration.model
|
|
113
|
-
@moderation_model = moderation_model || self.class.configuration.moderation_model
|
|
114
|
-
@embedding_model = embedding_model || self.class.configuration.embedding_model
|
|
115
|
-
@image_model = image_model || self.class.configuration.image_model
|
|
116
|
-
@speech_model = speech_model || self.class.configuration.speech_model
|
|
117
|
-
@speech_voice = speech_voice || self.class.configuration.speech_voice
|
|
118
|
-
@transcription_model = transcription_model || self.class.configuration.transcription_model
|
|
119
|
-
@timeout = timeout || self.class.configuration.timeout
|
|
120
|
-
@max_output_tokens = max_output_tokens || self.class.configuration.max_output_tokens
|
|
121
|
-
@headers = {
|
|
122
|
-
"Authorization" => "Bearer #{@api_key}",
|
|
123
|
-
"Content-Type" => "application/json"
|
|
124
|
-
}
|
|
125
|
-
end
|
|
126
|
-
|
|
127
|
-
def chat(message, model: nil, instructions: nil, max_output_tokens: nil, debug: false, options: {})
|
|
128
|
-
payload = options.merge(
|
|
129
|
-
model: model || self.model,
|
|
130
|
-
input: message,
|
|
131
|
-
max_output_tokens: max_output_tokens || self.max_output_tokens
|
|
132
|
-
)
|
|
133
|
-
payload[:instructions] = instructions if instructions
|
|
134
|
-
|
|
135
|
-
extract_content(post(payload), debug: debug)
|
|
136
|
-
rescue => e
|
|
137
|
-
prettify_data(status: "unknown", error: e.message, raw: nil, debug: debug)
|
|
138
|
-
end
|
|
139
|
-
|
|
140
|
-
def moderate(input = nil, model: nil, text: nil, image_url: nil, image_path: nil, debug: false, options: {})
|
|
141
|
-
payload = options.merge(
|
|
142
|
-
model: model || moderation_model,
|
|
143
|
-
input: moderation_input(input, text: text, image_url: image_url, image_path: image_path)
|
|
144
|
-
)
|
|
145
|
-
|
|
146
|
-
extract_moderation(post(payload, endpoint: moderation_endpoint), debug: debug)
|
|
147
|
-
rescue => e
|
|
148
|
-
prettify_data(status: "unknown", error: e.message, raw: nil, debug: debug)
|
|
149
|
-
end
|
|
150
|
-
|
|
151
|
-
def embed(input, model: nil, dimensions: nil, encoding_format: nil, debug: false, options: {})
|
|
152
|
-
payload = options.merge(
|
|
153
|
-
model: model || embedding_model,
|
|
154
|
-
input: input
|
|
155
|
-
)
|
|
156
|
-
payload[:dimensions] = dimensions if dimensions
|
|
157
|
-
payload[:encoding_format] = encoding_format if encoding_format
|
|
158
|
-
|
|
159
|
-
extract_embedding(post(payload, endpoint: embedding_endpoint), multiple: input.is_a?(Array), debug: debug)
|
|
160
|
-
rescue => e
|
|
161
|
-
prettify_data(status: "unknown", error: e.message, raw: nil, debug: debug)
|
|
162
|
-
end
|
|
163
|
-
|
|
164
|
-
def image(prompt, model: nil, size: nil, quality: nil, background: nil, output_format: nil, output_path: nil, debug: false, options: {})
|
|
165
|
-
payload = options.merge(
|
|
166
|
-
model: model || image_model,
|
|
167
|
-
prompt: prompt
|
|
168
|
-
)
|
|
169
|
-
payload[:size] = size if size
|
|
170
|
-
payload[:quality] = quality if quality
|
|
171
|
-
payload[:background] = background if background
|
|
172
|
-
payload[:output_format] = output_format if output_format
|
|
173
|
-
|
|
174
|
-
extract_image(post(payload, endpoint: image_endpoint), output_path: output_path, debug: debug)
|
|
175
|
-
rescue => e
|
|
176
|
-
prettify_data(status: "unknown", error: e.message, raw: nil, debug: debug)
|
|
6
|
+
# Preserve previously public constants while ownership stays with OpenAI.
|
|
7
|
+
OpenAI.constants(false).each do |name|
|
|
8
|
+
const_set(name, OpenAI.const_get(name))
|
|
177
9
|
end
|
|
178
10
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
model: model || speech_model,
|
|
182
|
-
input: text,
|
|
183
|
-
voice: voice || speech_voice
|
|
184
|
-
)
|
|
185
|
-
payload[:response_format] = response_format if response_format
|
|
186
|
-
payload[:speed] = speed if speed
|
|
187
|
-
payload[:instructions] = instructions if instructions
|
|
188
|
-
|
|
189
|
-
extract_speech(
|
|
190
|
-
post(payload, endpoint: speech_endpoint),
|
|
191
|
-
output_path: output_path,
|
|
192
|
-
base64: base64,
|
|
193
|
-
response_format: payload[:response_format] || payload["response_format"] || DEFAULT_SPEECH_FORMAT,
|
|
194
|
-
debug: debug
|
|
195
|
-
)
|
|
196
|
-
rescue => e
|
|
197
|
-
prettify_data(status: "unknown", error: e.message, raw: nil, debug: debug)
|
|
198
|
-
end
|
|
199
|
-
|
|
200
|
-
def transcribe(file_path, model: nil, language: nil, prompt: nil, response_format: nil, temperature: nil, timestamp_granularities: nil, debug: false, options: {})
|
|
201
|
-
fields = options.merge(
|
|
202
|
-
model: model || transcription_model
|
|
203
|
-
)
|
|
204
|
-
fields[:language] = language if language
|
|
205
|
-
fields[:prompt] = prompt if prompt
|
|
206
|
-
fields[:response_format] = response_format if response_format
|
|
207
|
-
fields[:temperature] = temperature unless temperature.nil?
|
|
208
|
-
fields[:timestamp_granularities] = timestamp_granularities if timestamp_granularities
|
|
209
|
-
|
|
210
|
-
extract_transcription(
|
|
211
|
-
post_multipart(fields, file_field: audio_file_field(file_path), endpoint: transcription_endpoint),
|
|
212
|
-
debug: debug
|
|
213
|
-
)
|
|
214
|
-
rescue => e
|
|
215
|
-
prettify_data(status: "unknown", error: e.message, raw: nil, debug: debug)
|
|
216
|
-
end
|
|
217
|
-
|
|
218
|
-
private
|
|
219
|
-
|
|
220
|
-
def post(payload, endpoint: response_endpoint)
|
|
221
|
-
uri = URI.parse(endpoint)
|
|
222
|
-
request = Net::HTTP::Post.new(uri)
|
|
223
|
-
|
|
224
|
-
headers.each do |key, value|
|
|
225
|
-
request[key] = value
|
|
226
|
-
end
|
|
227
|
-
|
|
228
|
-
request.body = JSON.generate(payload)
|
|
229
|
-
|
|
230
|
-
Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https") do |http|
|
|
231
|
-
http.open_timeout = timeout if http.respond_to?(:open_timeout=)
|
|
232
|
-
http.read_timeout = timeout if http.respond_to?(:read_timeout=)
|
|
233
|
-
http.request(request)
|
|
234
|
-
end
|
|
235
|
-
end
|
|
236
|
-
|
|
237
|
-
def post_multipart(fields, file_field:, endpoint:)
|
|
238
|
-
uri = URI.parse(endpoint)
|
|
239
|
-
boundary = "----AiLiteBoundary#{SecureRandom.hex(16)}"
|
|
240
|
-
request = Net::HTTP::Post.new(uri)
|
|
241
|
-
request["Authorization"] = headers["Authorization"]
|
|
242
|
-
request["Content-Type"] = "multipart/form-data; boundary=#{boundary}"
|
|
243
|
-
request.body = multipart_body(fields, file_field: file_field, boundary: boundary)
|
|
244
|
-
|
|
245
|
-
Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https") do |http|
|
|
246
|
-
http.open_timeout = timeout if http.respond_to?(:open_timeout=)
|
|
247
|
-
http.read_timeout = timeout if http.respond_to?(:read_timeout=)
|
|
248
|
-
http.request(request)
|
|
249
|
-
end
|
|
250
|
-
end
|
|
251
|
-
|
|
252
|
-
def response_endpoint
|
|
253
|
-
"#{API_BASE_URL}/responses"
|
|
254
|
-
end
|
|
255
|
-
|
|
256
|
-
def moderation_endpoint
|
|
257
|
-
"#{API_BASE_URL}/moderations"
|
|
258
|
-
end
|
|
259
|
-
|
|
260
|
-
def embedding_endpoint
|
|
261
|
-
"#{API_BASE_URL}/embeddings"
|
|
262
|
-
end
|
|
263
|
-
|
|
264
|
-
def image_endpoint
|
|
265
|
-
"#{API_BASE_URL}/images/generations"
|
|
266
|
-
end
|
|
267
|
-
|
|
268
|
-
def speech_endpoint
|
|
269
|
-
"#{API_BASE_URL}/audio/speech"
|
|
270
|
-
end
|
|
271
|
-
|
|
272
|
-
def transcription_endpoint
|
|
273
|
-
"#{API_BASE_URL}/audio/transcriptions"
|
|
274
|
-
end
|
|
275
|
-
|
|
276
|
-
def extract_content(response, debug: false)
|
|
277
|
-
status = response.code.to_i
|
|
278
|
-
parsed_response = JSON.parse(response.body)
|
|
279
|
-
|
|
280
|
-
unless success_status?(status)
|
|
281
|
-
return prettify_data(
|
|
282
|
-
status: status,
|
|
283
|
-
error: error_message(parsed_response),
|
|
284
|
-
response_id: parsed_response["id"],
|
|
285
|
-
raw: parsed_response,
|
|
286
|
-
debug: debug
|
|
287
|
-
)
|
|
288
|
-
end
|
|
289
|
-
|
|
290
|
-
raw_content = extract_output_text(parsed_response)
|
|
291
|
-
content = parse_content(raw_content)
|
|
292
|
-
prettify_data(
|
|
293
|
-
status: status,
|
|
294
|
-
content: content,
|
|
295
|
-
response_id: parsed_response["id"],
|
|
296
|
-
raw: parsed_response,
|
|
297
|
-
debug: debug
|
|
298
|
-
)
|
|
299
|
-
rescue JSON::ParserError => e
|
|
300
|
-
prettify_data(status: response_status(response), error: e.message, raw: response&.body, debug: debug)
|
|
301
|
-
rescue => e
|
|
302
|
-
prettify_data(status: response_status(response), error: e.message, raw: nil, debug: debug)
|
|
303
|
-
end
|
|
304
|
-
|
|
305
|
-
def extract_moderation(response, debug: false)
|
|
306
|
-
status = response.code.to_i
|
|
307
|
-
parsed_response = JSON.parse(response.body)
|
|
308
|
-
|
|
309
|
-
unless success_status?(status)
|
|
310
|
-
return prettify_data(
|
|
311
|
-
status: status,
|
|
312
|
-
error: error_message(parsed_response),
|
|
313
|
-
response_id: parsed_response["id"],
|
|
314
|
-
raw: parsed_response,
|
|
315
|
-
debug: debug
|
|
316
|
-
)
|
|
317
|
-
end
|
|
318
|
-
|
|
319
|
-
prettify_data(
|
|
320
|
-
status: status,
|
|
321
|
-
content: moderation_content(parsed_response),
|
|
322
|
-
response_id: parsed_response["id"],
|
|
323
|
-
raw: parsed_response,
|
|
324
|
-
debug: debug
|
|
325
|
-
)
|
|
326
|
-
rescue JSON::ParserError => e
|
|
327
|
-
prettify_data(status: response_status(response), error: e.message, raw: response&.body, debug: debug)
|
|
328
|
-
rescue => e
|
|
329
|
-
prettify_data(status: response_status(response), error: e.message, raw: nil, debug: debug)
|
|
330
|
-
end
|
|
331
|
-
|
|
332
|
-
def extract_embedding(response, multiple:, debug: false)
|
|
333
|
-
status = response.code.to_i
|
|
334
|
-
parsed_response = JSON.parse(response.body)
|
|
335
|
-
|
|
336
|
-
unless success_status?(status)
|
|
337
|
-
return prettify_data(
|
|
338
|
-
status: status,
|
|
339
|
-
error: error_message(parsed_response),
|
|
340
|
-
response_id: parsed_response["id"],
|
|
341
|
-
raw: parsed_response,
|
|
342
|
-
debug: debug
|
|
343
|
-
)
|
|
344
|
-
end
|
|
11
|
+
@deprecation_mutex = Mutex.new
|
|
12
|
+
@deprecated_entry_points = {}
|
|
345
13
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
unless success_status?(status)
|
|
364
|
-
return prettify_data(
|
|
365
|
-
status: status,
|
|
366
|
-
error: error_message(parsed_response),
|
|
367
|
-
response_id: parsed_response["id"],
|
|
368
|
-
raw: parsed_response,
|
|
369
|
-
debug: debug
|
|
370
|
-
)
|
|
371
|
-
end
|
|
372
|
-
|
|
373
|
-
content = image_content(parsed_response)
|
|
374
|
-
write_image_output(output_path, content) if output_path
|
|
375
|
-
|
|
376
|
-
prettify_data(
|
|
377
|
-
status: status,
|
|
378
|
-
content: content,
|
|
379
|
-
response_id: parsed_response["id"],
|
|
380
|
-
raw: parsed_response,
|
|
381
|
-
debug: debug
|
|
382
|
-
)
|
|
383
|
-
rescue JSON::ParserError => e
|
|
384
|
-
prettify_data(status: response_status(response), error: e.message, raw: response&.body, debug: debug)
|
|
385
|
-
rescue => e
|
|
386
|
-
prettify_data(status: response_status(response), error: e.message, raw: nil, debug: debug)
|
|
387
|
-
end
|
|
388
|
-
|
|
389
|
-
def extract_speech(response, output_path:, base64:, response_format:, debug: false)
|
|
390
|
-
status = response.code.to_i
|
|
391
|
-
|
|
392
|
-
unless success_status?(status)
|
|
393
|
-
parsed_response = parse_error_response(response.body)
|
|
394
|
-
|
|
395
|
-
return prettify_data(
|
|
396
|
-
status: status,
|
|
397
|
-
error: error_message(parsed_response),
|
|
398
|
-
response_id: parsed_response.is_a?(Hash) ? parsed_response["id"] : nil,
|
|
399
|
-
raw: parsed_response,
|
|
400
|
-
debug: debug
|
|
401
|
-
)
|
|
402
|
-
end
|
|
403
|
-
|
|
404
|
-
audio = response.body
|
|
405
|
-
File.binwrite(output_path, audio) if output_path
|
|
406
|
-
|
|
407
|
-
prettify_data(
|
|
408
|
-
status: status,
|
|
409
|
-
content: speech_content(audio, output_path: output_path, base64: base64, response_format: response_format),
|
|
410
|
-
response_id: nil,
|
|
411
|
-
raw: audio,
|
|
412
|
-
debug: debug
|
|
413
|
-
)
|
|
414
|
-
rescue => e
|
|
415
|
-
prettify_data(status: response_status(response), error: e.message, raw: nil, debug: debug)
|
|
416
|
-
end
|
|
417
|
-
|
|
418
|
-
def extract_transcription(response, debug: false)
|
|
419
|
-
status = response.code.to_i
|
|
420
|
-
parsed_response = parse_error_response(response.body)
|
|
421
|
-
|
|
422
|
-
unless success_status?(status)
|
|
423
|
-
return prettify_data(
|
|
424
|
-
status: status,
|
|
425
|
-
error: error_message(parsed_response),
|
|
426
|
-
response_id: parsed_response.is_a?(Hash) ? parsed_response["id"] : nil,
|
|
427
|
-
raw: parsed_response,
|
|
428
|
-
debug: debug
|
|
429
|
-
)
|
|
430
|
-
end
|
|
431
|
-
|
|
432
|
-
prettify_data(
|
|
433
|
-
status: status,
|
|
434
|
-
content: transcription_content(parsed_response),
|
|
435
|
-
response_id: parsed_response.is_a?(Hash) ? parsed_response["id"] : nil,
|
|
436
|
-
raw: parsed_response,
|
|
437
|
-
debug: debug
|
|
438
|
-
)
|
|
439
|
-
rescue => e
|
|
440
|
-
prettify_data(status: response_status(response), error: e.message, raw: nil, debug: debug)
|
|
441
|
-
end
|
|
442
|
-
|
|
443
|
-
def extract_output_text(raw)
|
|
444
|
-
Array(raw["output"]).flat_map do |item|
|
|
445
|
-
next [] unless item.is_a?(Hash) && item["type"] == "message"
|
|
446
|
-
|
|
447
|
-
Array(item["content"]).map do |content|
|
|
448
|
-
next unless content.is_a?(Hash) && content["type"] == "output_text"
|
|
449
|
-
|
|
450
|
-
content["text"]
|
|
451
|
-
end.compact
|
|
452
|
-
end.join.strip
|
|
453
|
-
end
|
|
454
|
-
|
|
455
|
-
def parse_content(raw_text)
|
|
456
|
-
return nil if raw_text.nil? || raw_text.empty?
|
|
457
|
-
|
|
458
|
-
JSON.parse(raw_text)
|
|
459
|
-
rescue JSON::ParserError
|
|
460
|
-
raw_text
|
|
461
|
-
end
|
|
462
|
-
|
|
463
|
-
def moderation_input(input, text:, image_url:, image_path:)
|
|
464
|
-
unless text || image_url || image_path
|
|
465
|
-
raise ArgumentError, "Missing moderation input" if input.nil?
|
|
466
|
-
|
|
467
|
-
return input
|
|
468
|
-
end
|
|
469
|
-
|
|
470
|
-
items = []
|
|
471
|
-
items.concat(Array(input).map { |value| moderation_input_item(value) }) unless input.nil?
|
|
472
|
-
items << { type: "text", text: text } if text
|
|
473
|
-
items << { type: "image_url", image_url: { url: image_url } } if image_url
|
|
474
|
-
items << { type: "image_url", image_url: { url: image_data_url(image_path) } } if image_path
|
|
475
|
-
raise ArgumentError, "Missing moderation input" if items.empty?
|
|
476
|
-
|
|
477
|
-
items
|
|
478
|
-
end
|
|
479
|
-
|
|
480
|
-
def moderation_input_item(value)
|
|
481
|
-
case value
|
|
482
|
-
when String
|
|
483
|
-
{ type: "text", text: value }
|
|
484
|
-
when Hash
|
|
485
|
-
value
|
|
486
|
-
else
|
|
487
|
-
raise ArgumentError, "Unsupported moderation input item: #{value.class}"
|
|
488
|
-
end
|
|
489
|
-
end
|
|
490
|
-
|
|
491
|
-
def image_data_url(path)
|
|
492
|
-
mime_type = image_mime_type(path)
|
|
493
|
-
"data:#{mime_type};base64,#{Base64.strict_encode64(File.binread(path))}"
|
|
494
|
-
end
|
|
495
|
-
|
|
496
|
-
def image_mime_type(path)
|
|
497
|
-
IMAGE_MIME_TYPES.fetch(File.extname(path).downcase) do
|
|
498
|
-
raise ArgumentError, "Unsupported image type for moderation: #{File.extname(path)}"
|
|
499
|
-
end
|
|
500
|
-
end
|
|
501
|
-
|
|
502
|
-
def audio_file_field(path)
|
|
503
|
-
raise ArgumentError, "Audio file not found: #{path}" unless File.file?(path)
|
|
504
|
-
|
|
505
|
-
{
|
|
506
|
-
name: "file",
|
|
507
|
-
path: path,
|
|
508
|
-
filename: File.basename(path),
|
|
509
|
-
content_type: audio_mime_type(path)
|
|
510
|
-
}
|
|
511
|
-
end
|
|
512
|
-
|
|
513
|
-
def audio_mime_type(path)
|
|
514
|
-
extension = File.extname(path).downcase
|
|
515
|
-
AUDIO_MIME_TYPES.fetch(extension) do
|
|
516
|
-
raise ArgumentError, "Unsupported audio type for transcription: #{extension}"
|
|
517
|
-
end
|
|
518
|
-
end
|
|
519
|
-
|
|
520
|
-
def moderation_content(raw)
|
|
521
|
-
results = raw["results"]
|
|
522
|
-
return nil unless results.is_a?(Array)
|
|
523
|
-
|
|
524
|
-
results.length == 1 ? results.first : results
|
|
525
|
-
end
|
|
526
|
-
|
|
527
|
-
def embedding_content(raw, multiple:)
|
|
528
|
-
embeddings = Array(raw["data"]).map do |item|
|
|
529
|
-
item["embedding"] if item.is_a?(Hash)
|
|
530
|
-
end.compact
|
|
531
|
-
|
|
532
|
-
multiple ? embeddings : embeddings.first
|
|
533
|
-
end
|
|
534
|
-
|
|
535
|
-
def image_content(raw)
|
|
536
|
-
image = Array(raw["data"]).find { |item| item.is_a?(Hash) && item["b64_json"] }
|
|
537
|
-
image && image["b64_json"]
|
|
538
|
-
end
|
|
539
|
-
|
|
540
|
-
def write_image_output(path, content)
|
|
541
|
-
raise "No image data returned" if content.to_s.empty?
|
|
542
|
-
|
|
543
|
-
File.binwrite(path, Base64.decode64(content))
|
|
544
|
-
end
|
|
545
|
-
|
|
546
|
-
def speech_content(audio, output_path:, base64:, response_format:)
|
|
547
|
-
return Base64.strict_encode64(audio) if base64
|
|
548
|
-
return audio unless output_path
|
|
549
|
-
|
|
550
|
-
{
|
|
551
|
-
"path" => output_path,
|
|
552
|
-
"bytes" => audio.bytesize,
|
|
553
|
-
"format" => response_format
|
|
554
|
-
}
|
|
555
|
-
end
|
|
556
|
-
|
|
557
|
-
def transcription_content(raw)
|
|
558
|
-
return raw["text"] if raw.is_a?(Hash) && raw.key?("text")
|
|
559
|
-
|
|
560
|
-
raw
|
|
561
|
-
end
|
|
562
|
-
|
|
563
|
-
def multipart_body(fields, file_field:, boundary:)
|
|
564
|
-
body = String.new(encoding: Encoding::BINARY)
|
|
565
|
-
|
|
566
|
-
fields.each do |name, value|
|
|
567
|
-
multipart_field_parts(name, value).each do |field_name, field_value|
|
|
568
|
-
body << "--#{boundary}\r\n".b
|
|
569
|
-
body << "Content-Disposition: form-data; name=\"#{multipart_quote(field_name)}\"\r\n\r\n".b
|
|
570
|
-
body << field_value.to_s.b
|
|
571
|
-
body << "\r\n".b
|
|
14
|
+
class << self
|
|
15
|
+
def new(**kwargs)
|
|
16
|
+
warn_legacy_entry_point(:new)
|
|
17
|
+
OpenAI.new(**kwargs)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
%i[configuration configure reset_configuration! client reset_client!
|
|
21
|
+
chat chat_stream moderate embed image speak transcribe
|
|
22
|
+
available_models model_available? configured_models check_configured_models].each do |method_name|
|
|
23
|
+
define_method(method_name) do |*args, **kwargs, &block|
|
|
24
|
+
warn_legacy_entry_point(method_name)
|
|
25
|
+
if kwargs.empty?
|
|
26
|
+
OpenAI.public_send(method_name, *args, &block)
|
|
27
|
+
else
|
|
28
|
+
OpenAI.public_send(method_name, *args, **kwargs, &block)
|
|
29
|
+
end
|
|
572
30
|
end
|
|
573
31
|
end
|
|
574
32
|
|
|
575
|
-
|
|
576
|
-
body << "Content-Disposition: form-data; name=\"#{multipart_quote(file_field[:name])}\"; filename=\"#{multipart_quote(file_field[:filename])}\"\r\n".b
|
|
577
|
-
body << "Content-Type: #{file_field[:content_type]}\r\n\r\n".b
|
|
578
|
-
body << File.binread(file_field[:path])
|
|
579
|
-
body << "\r\n--#{boundary}--\r\n".b
|
|
580
|
-
body
|
|
581
|
-
end
|
|
33
|
+
private
|
|
582
34
|
|
|
583
|
-
|
|
584
|
-
|
|
35
|
+
def warn_legacy_entry_point(method_name)
|
|
36
|
+
@deprecation_mutex.synchronize do
|
|
37
|
+
return if @deprecated_entry_points[method_name]
|
|
585
38
|
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
end
|
|
592
|
-
|
|
593
|
-
def multipart_value(value)
|
|
594
|
-
case value
|
|
595
|
-
when Hash
|
|
596
|
-
JSON.generate(value)
|
|
597
|
-
else
|
|
598
|
-
value
|
|
599
|
-
end
|
|
600
|
-
end
|
|
601
|
-
|
|
602
|
-
def multipart_quote(value)
|
|
603
|
-
value.to_s.gsub("\\", "\\\\").gsub("\"", "\\\"").delete("\r\n")
|
|
604
|
-
end
|
|
605
|
-
|
|
606
|
-
def parse_error_response(body)
|
|
607
|
-
JSON.parse(body)
|
|
608
|
-
rescue JSON::ParserError
|
|
609
|
-
body
|
|
610
|
-
end
|
|
611
|
-
|
|
612
|
-
def success_status?(status)
|
|
613
|
-
status >= 200 && status < 300
|
|
614
|
-
end
|
|
615
|
-
|
|
616
|
-
def error_message(raw)
|
|
617
|
-
if raw.is_a?(Hash)
|
|
618
|
-
raw.dig("error", "message") || raw["error"] || raw["message"] || raw.to_s
|
|
619
|
-
else
|
|
620
|
-
raw.to_s
|
|
39
|
+
warn "[ai-lite] AiLite.#{method_name} is deprecated. " \
|
|
40
|
+
"Use AiLite::OpenAI.#{method_name} instead. " \
|
|
41
|
+
"The legacy entry point will be removed in v2.0."
|
|
42
|
+
@deprecated_entry_points[method_name] = true
|
|
43
|
+
end
|
|
621
44
|
end
|
|
622
45
|
end
|
|
623
|
-
|
|
624
|
-
def response_status(response)
|
|
625
|
-
response&.code&.to_i || "unknown"
|
|
626
|
-
end
|
|
627
|
-
|
|
628
|
-
def prettify_data(status:, content: nil, error: nil, response_id: nil, raw:, debug: false)
|
|
629
|
-
{
|
|
630
|
-
"content" => content,
|
|
631
|
-
"response_id" => response_id,
|
|
632
|
-
"status" => status,
|
|
633
|
-
"error" => error,
|
|
634
|
-
"raw" => debug ? raw : nil
|
|
635
|
-
}
|
|
636
|
-
end
|
|
637
46
|
end
|