ai-lite 0.6.1 → 1.1.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 +26 -0
- data/README.md +456 -19
- data/lib/ai_lite/gemini/chat.rb +78 -0
- data/lib/ai_lite/gemini/configuration.rb +23 -0
- data/lib/ai_lite/gemini/embeddings.rb +58 -0
- data/lib/ai_lite/gemini/images.rb +56 -0
- data/lib/ai_lite/gemini/models.rb +104 -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 +109 -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 -624
- data/test/ai_lite_test.rb +196 -72
- data/test/gemini_embeddings_test.rb +122 -0
- data/test/gemini_images_test.rb +154 -0
- data/test/gemini_models_test.rb +113 -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 +32 -6
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
class AiLite
|
|
2
|
+
class OpenAI
|
|
3
|
+
module Streaming
|
|
4
|
+
# Text deltas are yielded immediately; the return value is the final envelope.
|
|
5
|
+
def chat_stream(message, model: nil, instructions: nil, previous_response_id: nil, max_output_tokens: nil, debug: false, options: {}, &block)
|
|
6
|
+
raise ArgumentError, "chat_stream requires a block" unless block
|
|
7
|
+
|
|
8
|
+
status = "unknown"
|
|
9
|
+
text = +""
|
|
10
|
+
response_id = nil
|
|
11
|
+
raw = nil
|
|
12
|
+
callback_error = nil
|
|
13
|
+
begin
|
|
14
|
+
payload = options.reject { |key, _| key.to_s == "stream" }.merge(
|
|
15
|
+
model: model || self.model, input: message, stream: true,
|
|
16
|
+
max_output_tokens: max_output_tokens || self.max_output_tokens
|
|
17
|
+
)
|
|
18
|
+
payload[:instructions] = instructions if instructions
|
|
19
|
+
payload[:previous_response_id] = previous_response_id if previous_response_id
|
|
20
|
+
uri = URI.parse(response_endpoint)
|
|
21
|
+
request = Net::HTTP::Post.new(uri)
|
|
22
|
+
headers.each { |key, value| request[key] = value }
|
|
23
|
+
request["Accept"] = "text/event-stream"
|
|
24
|
+
request.body = JSON.generate(payload)
|
|
25
|
+
result = nil
|
|
26
|
+
|
|
27
|
+
Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https") do |http|
|
|
28
|
+
http.open_timeout = timeout
|
|
29
|
+
http.read_timeout = timeout
|
|
30
|
+
# Exit the request block at the terminal event, closing the connection.
|
|
31
|
+
catch(:ai_lite_stream_finished) do
|
|
32
|
+
http.request(request) do |response|
|
|
33
|
+
status = response.code.to_i
|
|
34
|
+
unless success_status?(status)
|
|
35
|
+
body = +""
|
|
36
|
+
response.read_body { |chunk| body << chunk }
|
|
37
|
+
raw = body
|
|
38
|
+
raw = JSON.parse(body)
|
|
39
|
+
result = prettify_data(status: status, error: error_message(raw), raw: raw, debug: debug)
|
|
40
|
+
throw :ai_lite_stream_finished
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
each_stream_event(response) do |event|
|
|
44
|
+
raw = event
|
|
45
|
+
response_id = event.dig("response", "id") || response_id
|
|
46
|
+
case event["type"]
|
|
47
|
+
when "response.output_text.delta"
|
|
48
|
+
delta = event.fetch("delta")
|
|
49
|
+
text << delta
|
|
50
|
+
begin
|
|
51
|
+
block.call(delta)
|
|
52
|
+
rescue StandardError => e
|
|
53
|
+
callback_error = e
|
|
54
|
+
raise
|
|
55
|
+
end
|
|
56
|
+
when "response.completed", "response.failed", "response.incomplete"
|
|
57
|
+
raw = event.fetch("response")
|
|
58
|
+
error = case event["type"]
|
|
59
|
+
when "response.failed"
|
|
60
|
+
raw["error"] ? error_message(raw) : "Response failed"
|
|
61
|
+
when "response.incomplete"
|
|
62
|
+
"Response incomplete: #{raw.dig('incomplete_details', 'reason') || 'unknown reason'}"
|
|
63
|
+
end
|
|
64
|
+
content = error ? text : parse_content(extract_output_text(raw))
|
|
65
|
+
result = prettify_data(status: status, content: content, error: error,
|
|
66
|
+
response_id: response_id, raw: raw, debug: debug)
|
|
67
|
+
throw :ai_lite_stream_finished
|
|
68
|
+
when "error"
|
|
69
|
+
result = prettify_data(status: status, content: text, error: error_message(event),
|
|
70
|
+
response_id: response_id, raw: raw, debug: debug)
|
|
71
|
+
throw :ai_lite_stream_finished
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
result || prettify_data(status: status, content: text, response_id: response_id,
|
|
78
|
+
error: "Stream ended before a terminal response event", raw: raw, debug: debug)
|
|
79
|
+
rescue StandardError => e
|
|
80
|
+
raise if callback_error.equal?(e)
|
|
81
|
+
|
|
82
|
+
prettify_data(status: status, content: text.empty? ? nil : text, response_id: response_id,
|
|
83
|
+
error: e.message, raw: raw, debug: debug)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
private
|
|
88
|
+
|
|
89
|
+
def each_stream_event(response)
|
|
90
|
+
buffer = +"".b
|
|
91
|
+
data = []
|
|
92
|
+
consume = lambda do
|
|
93
|
+
while (boundary = buffer.index(/[\r\n]/))
|
|
94
|
+
# A trailing CR might be the first byte of a CRLF split across reads.
|
|
95
|
+
break if buffer.getbyte(boundary) == 13 && boundary == buffer.bytesize - 1
|
|
96
|
+
|
|
97
|
+
width = buffer.byteslice(boundary, 2) == "\r\n" ? 2 : 1
|
|
98
|
+
line = buffer.slice!(0, boundary + width).byteslice(0, boundary)
|
|
99
|
+
if line.empty?
|
|
100
|
+
unless data.empty?
|
|
101
|
+
payload = data.join("\n").force_encoding(Encoding::UTF_8)
|
|
102
|
+
data.clear
|
|
103
|
+
unless payload == "[DONE]"
|
|
104
|
+
event = JSON.parse(payload)
|
|
105
|
+
raise "Invalid streaming event: expected an object" unless event.is_a?(Hash)
|
|
106
|
+
yield event
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
elsif line.start_with?("data:")
|
|
110
|
+
data << line.sub(/\Adata: ?/, "")
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
response.read_body do |chunk|
|
|
115
|
+
buffer << chunk.b
|
|
116
|
+
consume.call
|
|
117
|
+
end
|
|
118
|
+
# A final lone CR is a complete line delimiter at EOF.
|
|
119
|
+
if buffer.end_with?("\r")
|
|
120
|
+
buffer << "\n"
|
|
121
|
+
consume.call
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
include Streaming
|
|
128
|
+
private_constant :Streaming
|
|
129
|
+
end
|
|
130
|
+
end
|