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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a6b2ed589de27af46e6ce7babfb4a1ec32808ecfae567f29ed8db35bfd129f26
|
|
4
|
+
data.tar.gz: ddee8996d702a77709b30bd9bb360994abfbc3e4875386db04b99241541a413d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9591556daadccbb5107f8b52f6d5ca825232003336a87db144e41971ec9921282a38ea884928d0d1f5ee8ea4d84fb8b67698ddcfb1894f678ab2bc615a9dcdb8
|
|
7
|
+
data.tar.gz: 24aa31e8ddaa48bf6876bc7e4375c212d46994d37709a32bb52c797b3618f0dc770fe831919039781c2f9529b40ff0c85b12c04dea4a7894c61fa4e8515fa282
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## Unreleased
|
|
4
|
+
|
|
5
|
+
- Add Gemini image generation with base64 results, optional file output, native
|
|
6
|
+
options, image model configuration, and model checks.
|
|
7
|
+
|
|
8
|
+
- Add Gemini text embeddings for single and multiple inputs, with dimensions,
|
|
9
|
+
embedding model configuration, model checks, and the standard result envelope.
|
|
10
|
+
|
|
11
|
+
## 1.0.0 (unreleased)
|
|
12
|
+
|
|
13
|
+
- Introduce independent `AiLite::OpenAI` and `AiLite::Gemini` providers.
|
|
14
|
+
- Preserve documented legacy OpenAI entry points through `AiLite`, with deprecation
|
|
15
|
+
warnings once per entry point per process. Removal is planned for v2.0.
|
|
16
|
+
- Add OpenAI and Gemini text streaming through `chat_stream`.
|
|
17
|
+
- Add `available_models`, `model_available?`, `configured_models`, and
|
|
18
|
+
`check_configured_models` to both providers; Gemini discovery follows all pages.
|
|
19
|
+
- Add Gemini chat with native structured media input and interaction continuation,
|
|
20
|
+
plus a YouTube URL helper. Existing OpenAI utility methods remain available.
|
|
21
|
+
- Document model-list limitations, provider-specific debug output, and migration.
|
|
22
|
+
- Treat failed/incomplete OpenAI generations as errors even when HTTP status is 200.
|
|
23
|
+
- Handle fragmented UTF-8 and mixed SSE line endings in both stream parsers.
|
|
24
|
+
|
|
25
|
+
Gemini image generation, speech, file transcription, additional
|
|
26
|
+
providers, and async convenience methods are deferred beyond v1.
|
data/README.md
CHANGED
|
@@ -1,35 +1,330 @@
|
|
|
1
1
|
# AI Lite
|
|
2
2
|
|
|
3
|
-
AI Lite is a pure Ruby
|
|
3
|
+
AI Lite is a lightweight, pure Ruby client for OpenAI and Gemini. Both providers
|
|
4
|
+
support chat, streaming, embeddings, image generation, and model availability
|
|
5
|
+
checks through simple interfaces and a consistent result format.
|
|
4
6
|
|
|
5
|
-
It is built for Rails apps and plain Ruby projects
|
|
6
|
-
|
|
7
|
-
|
|
7
|
+
It is built for Rails apps and plain Ruby projects that need AI features with
|
|
8
|
+
minimal dependencies. It is especially useful in legacy Rails apps where Ruby,
|
|
9
|
+
Rails, or HTTP-client dependency constraints make larger SDKs difficult to add.
|
|
8
10
|
|
|
9
11
|
This gem is intentionally small:
|
|
10
12
|
|
|
11
13
|
- No Rails dependency
|
|
12
|
-
- No
|
|
14
|
+
- No provider SDK dependency
|
|
13
15
|
- No Faraday, HTTParty, ActiveSupport, or connection pool dependency
|
|
14
16
|
- Uses only Ruby stdlib: `Net::HTTP`, `URI`, `JSON`, `Base64`, and `SecureRandom`
|
|
15
17
|
|
|
16
|
-
|
|
18
|
+
Choose a provider and call the methods your app needs:
|
|
19
|
+
|
|
20
|
+
```ruby
|
|
21
|
+
require "ai_lite"
|
|
22
|
+
|
|
23
|
+
openai = AiLite::OpenAI.new # Reads OPENAI_API_KEY
|
|
24
|
+
gemini = AiLite::Gemini.new # Reads GEMINI_API_KEY
|
|
25
|
+
|
|
26
|
+
openai.chat("Say hello")
|
|
27
|
+
gemini.chat("Say hello")
|
|
28
|
+
|
|
29
|
+
openai.embed("Text to vectorize")
|
|
30
|
+
gemini.embed("Text to vectorize")
|
|
31
|
+
|
|
32
|
+
openai.image("A simple app icon")
|
|
33
|
+
gemini.image("A simple app icon")
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Each provider has independent configuration and native API options. OpenAI also
|
|
37
|
+
supports moderation, speech, and file transcription; Gemini supports structured
|
|
38
|
+
media input and a YouTube helper. The table below lists the available methods.
|
|
39
|
+
|
|
40
|
+
## Provider support
|
|
41
|
+
|
|
42
|
+
| Method | OpenAI | Gemini |
|
|
43
|
+
| --- | --- | --- |
|
|
44
|
+
| `chat`, `chat_stream` | Yes | Yes |
|
|
45
|
+
| Four model helpers | Yes | Yes |
|
|
46
|
+
| `youtube` | No | Yes |
|
|
47
|
+
| `embed`, `image` | Yes | Yes |
|
|
48
|
+
| `moderate`, `speak`, `transcribe` | Yes | Not implemented |
|
|
49
|
+
|
|
50
|
+
Unsupported provider methods are absent and raise Ruby's `NoMethodError` if called.
|
|
51
|
+
Configure providers during application startup; do not mutate shared configuration
|
|
52
|
+
or client headers while requests are running. The gem does not provide automatic
|
|
53
|
+
retries, worker threads, or a background-job queue.
|
|
54
|
+
|
|
55
|
+
## Gemini
|
|
56
|
+
|
|
57
|
+
```ruby
|
|
58
|
+
require "ai_lite"
|
|
59
|
+
|
|
60
|
+
gemini = AiLite::Gemini.new # Reads GEMINI_API_KEY
|
|
61
|
+
result = gemini.chat("Tell me a joke")
|
|
62
|
+
puts result["content"]
|
|
63
|
+
warn result["error"] if result["error"]
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
The lookup order is an explicit `api_key:`, Gemini configuration, then
|
|
67
|
+
`ENV["GEMINI_API_KEY"]`. OpenAI credentials are never used for Gemini. The gem
|
|
68
|
+
does not load `.env` automatically.
|
|
69
|
+
|
|
70
|
+
```ruby
|
|
71
|
+
AiLite::Gemini.configure do |config|
|
|
72
|
+
config.api_key = ENV["GEMINI_API_KEY"]
|
|
73
|
+
config.model = "gemini-3.8-flash"
|
|
74
|
+
config.embedding_model = "gemini-embedding-001"
|
|
75
|
+
config.image_model = "gemini-nano-banana-2.1"
|
|
76
|
+
config.timeout = 120
|
|
77
|
+
config.max_output_tokens = 2000
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
AiLite::Gemini.chat("Tell me a joke")
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Each provider has independent configuration and a cached client. Constructor
|
|
84
|
+
keywords override configured defaults. Configuration changes reset that provider's
|
|
85
|
+
class-level client; existing instances keep their settings. Gemini uses the
|
|
86
|
+
Interactions API at `https://generativelanguage.googleapis.com/v1beta/interactions`
|
|
87
|
+
with `x-goog-api-key` authentication. Its API version and defaults belong to Gemini.
|
|
88
|
+
|
|
89
|
+
`chat` accepts a string, a native Gemini content hash, or an array of native
|
|
90
|
+
content blocks or interaction steps. Structured inputs pass through unchanged:
|
|
91
|
+
|
|
92
|
+
```ruby
|
|
93
|
+
result = gemini.chat([
|
|
94
|
+
{ type: "video", uri: "https://www.youtube.com/watch?v=YOUR_VIDEO_ID" },
|
|
95
|
+
{ type: "text", text: "Summarize the key points." }
|
|
96
|
+
])
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Google validates media access and model support. This does not download videos,
|
|
100
|
+
upload local files, or make arbitrary video webpages accessible. Message history
|
|
101
|
+
uses Gemini's native `user_input` / `model_output` steps with `content` arrays;
|
|
102
|
+
OpenAI-style role/message arrays are not automatically translated.
|
|
103
|
+
|
|
104
|
+
Multi-turn continuation uses the familiar keyword:
|
|
105
|
+
|
|
106
|
+
```ruby
|
|
107
|
+
first = gemini.chat("My name is William.")
|
|
108
|
+
second = gemini.chat("What is my name?", previous_response_id: first["response_id"])
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
`instructions:` maps to `system_instruction`; `previous_response_id:` maps to
|
|
112
|
+
`previous_interaction_id`; `max_output_tokens:` maps into `generation_config`.
|
|
113
|
+
Gemini-specific settings remain available through `options:`:
|
|
114
|
+
|
|
115
|
+
```ruby
|
|
116
|
+
gemini.chat("Say hello", instructions: "Be concise.", options: {
|
|
117
|
+
store: false,
|
|
118
|
+
generation_config: { temperature: 0.4 }
|
|
119
|
+
})
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Interactions are stored by default by the provider; `store: false` opts out and
|
|
123
|
+
prevents later server-side continuation from that interaction. See Google's
|
|
124
|
+
[Interactions guide](https://ai.google.dev/gemini-api/docs/interactions-overview)
|
|
125
|
+
and [API reference](https://ai.google.dev/api/interactions-api).
|
|
126
|
+
|
|
127
|
+
Results use the standard `content`, `response_id`, `status`, `error`, and `raw`
|
|
128
|
+
keys. Text is extracted only from `model_output` steps; JSON-looking text is
|
|
129
|
+
parsed. `debug: true` retains the complete response, including non-text output,
|
|
130
|
+
tool calls, and usage. Non-completed interactions (including `requires_action`)
|
|
131
|
+
return an error rather than pretending the answer is complete; no tools are
|
|
132
|
+
executed automatically. Input/API/transport errors return an error envelope;
|
|
133
|
+
constructing a client without a key raises `ArgumentError`.
|
|
134
|
+
|
|
135
|
+
Gemini speech and file transcription methods are not implemented yet. Use `chat_stream` for streaming; `chat` rejects
|
|
136
|
+
`stream: true`. Both methods reject `background: true` before sending a request.
|
|
137
|
+
Legacy `AiLite` entry points still use OpenAI.
|
|
138
|
+
|
|
139
|
+
### Gemini embeddings
|
|
140
|
+
|
|
141
|
+
`embed` mirrors OpenAI's text input and return shape: a string returns one numeric
|
|
142
|
+
vector in `content`; an array returns a list of vectors in input order, including
|
|
143
|
+
an array containing just one string.
|
|
144
|
+
|
|
145
|
+
```ruby
|
|
146
|
+
gemini = AiLite::Gemini.new
|
|
147
|
+
result = gemini.embed("Text to vectorize")
|
|
148
|
+
result["content"] # [0.12, -0.34, ...]
|
|
149
|
+
|
|
150
|
+
gemini.embed(["First text", "Second text"], dimensions: 768)
|
|
151
|
+
AiLite::Gemini.embed("Search query", options: { taskType: "RETRIEVAL_QUERY" })
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
The default is `gemini-embedding-001`. Override it with `embedding_model:` in the
|
|
155
|
+
constructor, `config.embedding_model`, or `model:` per call. Both bare model IDs
|
|
156
|
+
and `models/` prefixes are accepted. Model checks include the embedding model.
|
|
157
|
+
|
|
158
|
+
Single strings use `embedContent`; arrays use one synchronous `batchEmbedContents`
|
|
159
|
+
request. `dimensions:` maps to `outputDimensionality`. Native request fields in
|
|
160
|
+
`options:` apply to each input; explicit model, input, and dimensions take precedence.
|
|
161
|
+
See Google's [embedding API reference](https://ai.google.dev/api/embeddings).
|
|
162
|
+
|
|
163
|
+
Gemini returns numeric vectors; `encoding_format:` may be omitted or set to
|
|
164
|
+
`"float"`. Other formats return an error envelope. Empty inputs and invalid dimensions
|
|
165
|
+
also return errors before making a request. `debug: true` retains the provider
|
|
166
|
+
response and any usage metadata in `raw`; `response_id` is `nil` for embeddings.
|
|
167
|
+
|
|
168
|
+
### Gemini image generation
|
|
169
|
+
|
|
170
|
+
```ruby
|
|
171
|
+
gemini = AiLite::Gemini.new
|
|
172
|
+
result = gemini.image("A cat astronaut on the moon", output_path: "tmp/cat.jpg")
|
|
173
|
+
result["content"] # Base64 image data, also returned when saving to a file
|
|
174
|
+
|
|
175
|
+
AiLite::Gemini.image("A mountain landscape", options: {
|
|
176
|
+
response_format: { type: "image", aspect_ratio: "16:9", image_size: "2K" }
|
|
177
|
+
})
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
The default image model is `gemini-nano-banana-2.1`. Set `config.image_model`,
|
|
181
|
+
pass `image_model:` to the constructor, or override `model:` per call.
|
|
182
|
+
Model checks include this setting. Requests use the Interactions API, with native
|
|
183
|
+
settings passed through `options:`. Explicit prompt and model take precedence.
|
|
184
|
+
Image requests do not inherit the chat output-token limit.
|
|
185
|
+
|
|
186
|
+
Like OpenAI's `image`, `content` contains the first generated image as base64.
|
|
187
|
+
Only image blocks from `model_output` steps are selected. `output_path:` saves
|
|
188
|
+
its decoded bytes; the parent directory must already exist. Files retain the
|
|
189
|
+
provider's encoding: the filename does not convert the image. Use `debug: true`
|
|
190
|
+
to inspect MIME type, additional images, text, and usage in `raw`. Gemini can return
|
|
191
|
+
JPEG data even when the output filename ends in `.png`; choose the extension to
|
|
192
|
+
match the returned image MIME type. AI Lite does not transcode images.
|
|
193
|
+
|
|
194
|
+
The initial interface accepts a text prompt, `model:`, `output_path:`, `debug:`,
|
|
195
|
+
and `options:`. OpenAI-specific size, quality, background, and output format
|
|
196
|
+
keywords are not shared; use Gemini's native `response_format` options as above.
|
|
197
|
+
Streaming and background execution are rejected. Failed/incomplete interactions,
|
|
198
|
+
missing or invalid image data, and file-write errors return error envelopes.
|
|
199
|
+
See Google's [image generation guide](https://ai.google.dev/gemini-api/docs/image-generation).
|
|
200
|
+
|
|
201
|
+
### Gemini model checks
|
|
202
|
+
|
|
203
|
+
```ruby
|
|
204
|
+
gemini = AiLite::Gemini.new
|
|
205
|
+
result = gemini.available_models
|
|
206
|
+
puts result["content"] unless result["error"]
|
|
207
|
+
|
|
208
|
+
gemini.model_available?("gemini-3.8-flash") # true / false / nil on lookup failure
|
|
209
|
+
gemini.configured_models # Includes chat, embedding, and image models
|
|
210
|
+
gemini.check_configured_models # Standard envelope containing a report
|
|
211
|
+
|
|
212
|
+
# All four also work at class level:
|
|
213
|
+
AiLite::Gemini.available_models(debug: true)
|
|
214
|
+
AiLite::Gemini.model_available?("gemini-3.8-flash")
|
|
215
|
+
AiLite::Gemini.configured_models # No key or network required
|
|
216
|
+
AiLite::Gemini.check_configured_models
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
`available_models` follows every page of Gemini's `/v1beta/models` endpoint and
|
|
220
|
+
returns unique model IDs in `content`, with the `models/` resource prefix removed.
|
|
221
|
+
`model_available?` accepts IDs with or without that prefix, uses exact matching
|
|
222
|
+
(no alias resolution), and fetches a fresh complete list. Invalid empty/non-string
|
|
223
|
+
IDs raise `ArgumentError`. Lookup errors return `nil`; inspect `available_models`
|
|
224
|
+
or `check_configured_models` for error details.
|
|
225
|
+
|
|
226
|
+
`configured_models` returns a local snapshot of the instance's settings; its
|
|
227
|
+
class method reports current Gemini configuration. Currently only `chat` is
|
|
228
|
+
configured: `chat_stream` and `youtube` use that same model.
|
|
229
|
+
|
|
230
|
+
`check_configured_models` fetches the list once across its pages, then returns:
|
|
231
|
+
|
|
232
|
+
```ruby
|
|
233
|
+
result["content"]["chat"]
|
|
234
|
+
# { "model" => "gemini-3.8-flash", "usage" => "chat",
|
|
235
|
+
# "available" => true, "error" => nil } # if listed
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
Any failed page makes the entire lookup fail; partial lists are not treated as
|
|
239
|
+
complete. Reports use `available: nil` on failure, with both entry-level and
|
|
240
|
+
outer errors. `debug: true` includes original page responses in
|
|
241
|
+
`raw["pages"]`, preserving model metadata and any received error response.
|
|
242
|
+
These methods make only model-list GET requests, never generation requests.
|
|
243
|
+
They do not verify free-tier eligibility, quota, endpoint compatibility, or
|
|
244
|
+
whether a model is temporarily overloaded. See Google's
|
|
245
|
+
[Models API documentation](https://ai.google.dev/api/models).
|
|
246
|
+
|
|
247
|
+
### YouTube helper
|
|
17
248
|
|
|
18
249
|
```ruby
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
250
|
+
result = gemini.youtube(
|
|
251
|
+
"https://www.youtube.com/watch?v=DV69qh0BzoA",
|
|
252
|
+
prompt: "Summarize this video in five bullet points.",
|
|
253
|
+
model: "gemini-3.5-flash-lite"
|
|
254
|
+
)
|
|
255
|
+
puts result["content"]
|
|
256
|
+
warn result["error"] if result["error"]
|
|
25
257
|
```
|
|
26
258
|
|
|
259
|
+
`AiLite::Gemini.youtube(url, prompt: ..., **kwargs)` also works. The helper
|
|
260
|
+
validates one HTTPS YouTube video URL and a nonempty prompt, builds native video
|
|
261
|
+
and text input, and delegates to `chat`. It accepts the same chat keywords,
|
|
262
|
+
including `model:`, `instructions:`, `previous_response_id:`, `max_output_tokens:`,
|
|
263
|
+
`debug:`, and `options:`, and returns the same envelope.
|
|
264
|
+
|
|
265
|
+
Supported URL forms are `youtube.com/watch?v=...` (including `www` and `m`),
|
|
266
|
+
`youtu.be/...`, and YouTube `/shorts/`, `/embed/`, and `/live/` video paths.
|
|
267
|
+
URLs, including timestamp parameters, pass through unchanged. Validation checks
|
|
268
|
+
URL shape and video ID, not whether the video is public or accessible to Gemini.
|
|
269
|
+
The helper builds the same request as this explicit input:
|
|
270
|
+
|
|
271
|
+
```ruby
|
|
272
|
+
gemini.chat([
|
|
273
|
+
{ type: "video", uri: url },
|
|
274
|
+
{ type: "text", text: "Summarize this video." }
|
|
275
|
+
])
|
|
276
|
+
# Equivalent convenience call:
|
|
277
|
+
gemini.youtube(url, prompt: "Summarize this video.")
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
Invalid URLs/prompts return an error envelope without a network request. No video
|
|
281
|
+
is downloaded locally. Gemini controls video support, limits, and processing.
|
|
282
|
+
|
|
283
|
+
For streaming, pass the video and text blocks directly to `chat_stream`; the
|
|
284
|
+
`youtube` helper itself uses non-streaming `chat`.
|
|
285
|
+
|
|
286
|
+
### Gemini streaming
|
|
287
|
+
|
|
288
|
+
```ruby
|
|
289
|
+
result = gemini.chat_stream("Tell me a short story", debug: true) do |text|
|
|
290
|
+
print text
|
|
291
|
+
$stdout.flush
|
|
292
|
+
end
|
|
293
|
+
puts
|
|
294
|
+
warn result["error"] if result["error"]
|
|
295
|
+
|
|
296
|
+
# Class-level calls also work:
|
|
297
|
+
AiLite::Gemini.chat_stream("Hello") { |text| print text }
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
`chat_stream` accepts the same input and keywords as `chat` and requires a block.
|
|
301
|
+
It yields model-output text fragments as they arrive, excluding reasoning and
|
|
302
|
+
other non-text events, and returns the standard envelope with the accumulated
|
|
303
|
+
answer and interaction ID. JSON-looking final text is parsed just like `chat`.
|
|
304
|
+
It runs in the calling thread without creating background threads.
|
|
305
|
+
|
|
306
|
+
Gemini's final streaming event may contain only partial interaction metadata.
|
|
307
|
+
With `debug: true`, `raw` therefore contains `{ "interaction" => ..., "events" => [...] }`:
|
|
308
|
+
the latest lifecycle interaction object and every received event, including tool,
|
|
309
|
+
reasoning, and usage events when provided. This differs from non-streaming `chat`,
|
|
310
|
+
whose `raw` is the complete HTTP response. No extra retrieval request is made;
|
|
311
|
+
retaining all events uses additional memory. Without debugging, `raw` is `nil`.
|
|
312
|
+
|
|
313
|
+
HTTP errors, malformed/disconnected streams, and incomplete or failed interactions
|
|
314
|
+
return an error envelope, retaining partial text when available. Always check
|
|
315
|
+
`error`, even with HTTP status `200`. Missing blocks raise `ArgumentError`, and
|
|
316
|
+
exceptions from your block propagate. The configured timeout applies to each
|
|
317
|
+
network read, not the total stream duration. See Google's
|
|
318
|
+
[streaming event reference](https://ai.google.dev/api/interactions-api#InteractionSseStreamEvent).
|
|
319
|
+
|
|
320
|
+
The remaining usage sections below describe the OpenAI provider.
|
|
321
|
+
|
|
27
322
|
## Usage
|
|
28
323
|
|
|
29
324
|
```ruby
|
|
30
325
|
require "ai_lite"
|
|
31
326
|
|
|
32
|
-
ai = AiLite.new
|
|
327
|
+
ai = AiLite::OpenAI.new
|
|
33
328
|
result = ai.chat("Say hello")
|
|
34
329
|
|
|
35
330
|
puts result["content"]
|
|
@@ -40,7 +335,7 @@ By default, the client looks for an API key in `OPENAI_API_KEY`, then falls back
|
|
|
40
335
|
You can also pass the key directly:
|
|
41
336
|
|
|
42
337
|
```ruby
|
|
43
|
-
ai = AiLite.new(api_key: "sk-...")
|
|
338
|
+
ai = AiLite::OpenAI.new(api_key: "sk-...")
|
|
44
339
|
```
|
|
45
340
|
|
|
46
341
|
## Configuration
|
|
@@ -49,7 +344,7 @@ In Rails, configure the default client from an initializer:
|
|
|
49
344
|
|
|
50
345
|
```ruby
|
|
51
346
|
# config/initializers/ai_lite.rb
|
|
52
|
-
AiLite.configure do |config|
|
|
347
|
+
AiLite::OpenAI.configure do |config|
|
|
53
348
|
config.api_key = ENV["OPENAI_API_KEY"]
|
|
54
349
|
config.model = "gpt-5.5"
|
|
55
350
|
config.moderation_model = "omni-moderation-latest"
|
|
@@ -80,16 +375,47 @@ Any value left unset falls back to AI Lite's default:
|
|
|
80
375
|
Then use the configured singleton-style client:
|
|
81
376
|
|
|
82
377
|
```ruby
|
|
83
|
-
result = AiLite.chat("Say hello")
|
|
378
|
+
result = AiLite::OpenAI.chat("Say hello")
|
|
84
379
|
```
|
|
85
380
|
|
|
86
381
|
You can still instantiate a separate client for another token:
|
|
87
382
|
|
|
88
383
|
```ruby
|
|
89
|
-
client = AiLite.new(api_key: "sk-other-token")
|
|
384
|
+
client = AiLite::OpenAI.new(api_key: "sk-other-token")
|
|
90
385
|
result = client.chat("Say hello")
|
|
91
386
|
```
|
|
92
387
|
|
|
388
|
+
## Migrating to provider namespaces
|
|
389
|
+
|
|
390
|
+
Use `AiLite::OpenAI` for OpenAI configuration, class methods, and instances.
|
|
391
|
+
OpenAI owns its API URLs, defaults, configuration, HTTP requests, and response handling.
|
|
392
|
+
Gemini has its own configuration, requests, and response handling; provider capabilities are implemented separately.
|
|
393
|
+
|
|
394
|
+
Existing `AiLite.new`, `AiLite.chat`, and the other top-level methods continue to
|
|
395
|
+
forward to OpenAI, sharing the same configuration and cached client. They emit a
|
|
396
|
+
deprecation warning to stderr once per legacy entry point per process. They will
|
|
397
|
+
remain available throughout v1.x and will be removed in v2.0.
|
|
398
|
+
|
|
399
|
+
```ruby
|
|
400
|
+
# Before
|
|
401
|
+
AiLite.configure { |config| config.api_key = ENV["OPENAI_API_KEY"] }
|
|
402
|
+
ai = AiLite.new
|
|
403
|
+
|
|
404
|
+
# After
|
|
405
|
+
AiLite::OpenAI.configure { |config| config.api_key = ENV["OPENAI_API_KEY"] }
|
|
406
|
+
ai = AiLite::OpenAI.new
|
|
407
|
+
# Class methods work too: AiLite::OpenAI.chat("Hello")
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
`AiLite.new` returns an `AiLite::OpenAI` instance. Compatibility covers documented
|
|
411
|
+
construction, configuration, utility methods, and constants; subclassing the legacy
|
|
412
|
+
`AiLite` class or relying on its exact instance class is not supported. Migrate such
|
|
413
|
+
code to `AiLite::OpenAI`.
|
|
414
|
+
|
|
415
|
+
Existing utility calls keep their methods and result envelopes. Legacy
|
|
416
|
+
constants such as `AiLite::DEFAULT_MODEL` and `AiLite::Configuration` remain
|
|
417
|
+
aliases for their OpenAI counterparts. The gem version stays at `AiLite::VERSION`.
|
|
418
|
+
|
|
93
419
|
## Chat
|
|
94
420
|
|
|
95
421
|
```ruby
|
|
@@ -147,6 +473,51 @@ puts follow_up["content"]
|
|
|
147
473
|
|
|
148
474
|
Passing `previous_response_id` through `options` remains supported for backward compatibility.
|
|
149
475
|
|
|
476
|
+
### Streaming Chat
|
|
477
|
+
|
|
478
|
+
Use `chat_stream` to receive text as it arrives. It accepts the same keywords as
|
|
479
|
+
`chat`, requires a block, and returns the usual result envelope when the stream
|
|
480
|
+
finishes.
|
|
481
|
+
|
|
482
|
+
```ruby
|
|
483
|
+
ai = AiLite::OpenAI.new
|
|
484
|
+
|
|
485
|
+
result = ai.chat_stream("Explain Ruby blocks in three sentences.") do |text|
|
|
486
|
+
print text
|
|
487
|
+
$stdout.flush
|
|
488
|
+
end
|
|
489
|
+
puts
|
|
490
|
+
|
|
491
|
+
warn result["error"] if result["error"]
|
|
492
|
+
# result["content"] contains the final answer.
|
|
493
|
+
# result["response_id"] can be passed as previous_response_id on the next call.
|
|
494
|
+
```
|
|
495
|
+
|
|
496
|
+
Class-level usage is also supported:
|
|
497
|
+
|
|
498
|
+
```ruby
|
|
499
|
+
AiLite::OpenAI.chat_stream("Say hello") { |text| print text }
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
The block receives text strings, which are not necessarily whole words or tokens.
|
|
503
|
+
JSON-looking final output is parsed in the returned `content`, just like `chat`;
|
|
504
|
+
the block still receives the original text fragments. With `debug: true`, `raw`
|
|
505
|
+
contains the terminal response object (or the available error/last event on failure),
|
|
506
|
+
not a history of every streaming event. Tool arguments, reasoning, and refusal
|
|
507
|
+
events are not yielded as text; the terminal response retains these outputs in
|
|
508
|
+
`raw` when debugging. See the [OpenAI streaming event reference](https://developers.openai.com/api/reference/resources/responses/streaming-events).
|
|
509
|
+
|
|
510
|
+
Always check `result["error"]`: failed, incomplete, malformed, or disconnected
|
|
511
|
+
streams return an error, retaining text already received when available. An HTTP
|
|
512
|
+
status of `200` alone does not mean generation completed successfully. Exceptions
|
|
513
|
+
raised by your block propagate to the caller; calling without a block raises
|
|
514
|
+
`ArgumentError` before making a request.
|
|
515
|
+
|
|
516
|
+
Streaming runs in the calling thread and creates no worker threads. In Rails,
|
|
517
|
+
forwarding chunks to a browser still requires an appropriate streaming response
|
|
518
|
+
or messaging mechanism in the application. The configured read timeout applies
|
|
519
|
+
to individual network reads, not to the total generation duration.
|
|
520
|
+
|
|
150
521
|
## Moderation
|
|
151
522
|
|
|
152
523
|
Use `moderate` to classify user-submitted text or images for potentially harmful content before saving, publishing, or sending it into another AI call.
|
|
@@ -508,9 +879,75 @@ result["content"] # transcript text
|
|
|
508
879
|
result["raw"] # full response body when available
|
|
509
880
|
```
|
|
510
881
|
|
|
882
|
+
## Model Availability
|
|
883
|
+
|
|
884
|
+
These helpers are implemented inside the OpenAI provider. Instance and class-level
|
|
885
|
+
calls are supported; class-level network calls use the configured OpenAI client.
|
|
886
|
+
|
|
887
|
+
```ruby
|
|
888
|
+
ai = AiLite::OpenAI.new
|
|
889
|
+
|
|
890
|
+
result = ai.available_models
|
|
891
|
+
if result["error"]
|
|
892
|
+
warn result["error"]
|
|
893
|
+
else
|
|
894
|
+
puts result["content"] # Array of exact model IDs
|
|
895
|
+
end
|
|
896
|
+
|
|
897
|
+
ai.model_available?("gpt-5.5") # true, false, or nil if the lookup failed
|
|
898
|
+
AiLite::OpenAI.available_models(debug: true) # raw includes the full model list
|
|
899
|
+
```
|
|
900
|
+
|
|
901
|
+
`available_models` makes one authenticated `GET /v1/models` request and returns
|
|
902
|
+
the standard envelope. Empty model lists are valid. `model_available?` makes a
|
|
903
|
+
fresh list request and compares the exact ID, without prefix or alias matching.
|
|
904
|
+
It returns `nil` on a lookup error; use `available_models` or
|
|
905
|
+
`check_configured_models` when you need the error details. Empty or non-string
|
|
906
|
+
IDs raise `ArgumentError`. As with other network methods, constructing a client
|
|
907
|
+
without an API key raises `ArgumentError`.
|
|
908
|
+
|
|
909
|
+
Inspect configured models locally:
|
|
910
|
+
|
|
911
|
+
```ruby
|
|
912
|
+
ai.configured_models
|
|
913
|
+
# => { "chat" => "gpt-5.5", "moderation" => "omni-moderation-latest",
|
|
914
|
+
# "embedding" => "text-embedding-3-small", "image" => "gpt-image-2",
|
|
915
|
+
# "speech" => "gpt-4o-mini-tts", "transcription" => "gpt-transcribe" }
|
|
916
|
+
|
|
917
|
+
AiLite::OpenAI.configured_models # No API key or network request required
|
|
918
|
+
```
|
|
919
|
+
|
|
920
|
+
`configured_models` returns a plain hash of usage names to model IDs. An instance
|
|
921
|
+
reports its own settings, including constructor overrides; the class method
|
|
922
|
+
reports the current provider configuration. Neither includes credentials.
|
|
923
|
+
|
|
924
|
+
Check all configured models with a single list request:
|
|
925
|
+
|
|
926
|
+
```ruby
|
|
927
|
+
result = ai.check_configured_models
|
|
928
|
+
result["content"]["chat"]
|
|
929
|
+
# => { "model" => "gpt-5.5", "usage" => "chat",
|
|
930
|
+
# "available" => true, "error" => nil } # if listed for this key
|
|
931
|
+
|
|
932
|
+
warn result["error"] if result["error"]
|
|
933
|
+
```
|
|
934
|
+
|
|
935
|
+
Each usage has a report entry. A successful lookup returns `available: true` or
|
|
936
|
+
`false`; lookup failures return `available: nil`, an entry-level error, and the
|
|
937
|
+
same error in the outer envelope. `debug: true` includes the original list or
|
|
938
|
+
error response in `raw`. Network checks use the client's configured timeout and
|
|
939
|
+
do not cache results or run automatically during initialization.
|
|
940
|
+
|
|
941
|
+
**These are model-list availability checks only.** They do not run chat, image,
|
|
942
|
+
speech, embedding, moderation, or transcription generation requests and create
|
|
943
|
+
no generated output. A listed model does not guarantee endpoint compatibility,
|
|
944
|
+
sufficient quota, or permission for every operation. See the
|
|
945
|
+
[OpenAI List models reference](https://developers.openai.com/api/reference/resources/models/methods/list).
|
|
946
|
+
|
|
511
947
|
## Return Shape
|
|
512
948
|
|
|
513
|
-
|
|
949
|
+
Generation methods, `available_models`, and `check_configured_models` return a hash envelope.
|
|
950
|
+
`configured_models` returns a plain hash; `model_available?` returns `true`, `false`, or `nil`.
|
|
514
951
|
|
|
515
952
|
Text output:
|
|
516
953
|
|
|
@@ -567,5 +1004,5 @@ result = ai.chat("Say hello", debug: true)
|
|
|
567
1004
|
Run the test suite:
|
|
568
1005
|
|
|
569
1006
|
```sh
|
|
570
|
-
ruby -Ilib:test test/
|
|
1007
|
+
ruby -Ilib:test test/run.rb
|
|
571
1008
|
```
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
class AiLite
|
|
2
|
+
class Gemini
|
|
3
|
+
module Chat
|
|
4
|
+
def chat(message, model: nil, instructions: nil, previous_response_id: nil, max_output_tokens: nil, debug: false, options: {})
|
|
5
|
+
response = nil
|
|
6
|
+
raw = nil
|
|
7
|
+
response_id = nil
|
|
8
|
+
payload = chat_payload(message, model: model, instructions: instructions,
|
|
9
|
+
previous_response_id: previous_response_id,
|
|
10
|
+
max_output_tokens: max_output_tokens, options: options)
|
|
11
|
+
response = post_interaction(payload)
|
|
12
|
+
raw = response.body
|
|
13
|
+
raw = JSON.parse(raw)
|
|
14
|
+
raise "Invalid Gemini response: expected an object" unless raw.is_a?(Hash)
|
|
15
|
+
|
|
16
|
+
response_id = raw["id"]
|
|
17
|
+
unless response.code.to_i.between?(200, 299)
|
|
18
|
+
return result_envelope(status: response.code.to_i, error: error_message(raw),
|
|
19
|
+
response_id: response_id, raw: raw, debug: debug)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
error = if raw["error"]
|
|
23
|
+
error_message(raw)
|
|
24
|
+
elsif raw["status"] != "completed"
|
|
25
|
+
"Gemini interaction #{raw['status'] || 'has no status'}; inspect raw with debug: true"
|
|
26
|
+
end
|
|
27
|
+
steps = raw["steps"]
|
|
28
|
+
raise "Invalid Gemini response: expected steps" unless steps.is_a?(Array) || error
|
|
29
|
+
|
|
30
|
+
text = Array(steps).flat_map do |step|
|
|
31
|
+
next [] unless step.is_a?(Hash) && step["type"] == "model_output"
|
|
32
|
+
|
|
33
|
+
Array(step["content"]).map do |part|
|
|
34
|
+
part["text"] if part.is_a?(Hash) && part["type"] == "text" && part["text"].is_a?(String)
|
|
35
|
+
end.compact
|
|
36
|
+
end.join.strip
|
|
37
|
+
content = if text.empty?
|
|
38
|
+
nil
|
|
39
|
+
else
|
|
40
|
+
begin
|
|
41
|
+
JSON.parse(text)
|
|
42
|
+
rescue JSON::ParserError
|
|
43
|
+
text
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
result_envelope(status: response.code.to_i, content: content, error: error,
|
|
47
|
+
response_id: response_id, raw: raw, debug: debug)
|
|
48
|
+
rescue StandardError => e
|
|
49
|
+
result_envelope(status: response ? response.code.to_i : "unknown", error: e.message,
|
|
50
|
+
response_id: response_id, raw: raw, debug: debug)
|
|
51
|
+
end
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
def chat_payload(message, model:, instructions:, previous_response_id:, max_output_tokens:, options:, stream: false)
|
|
55
|
+
payload = options.each_with_object({}) { |(key, value), result| result[key.to_s] = value }
|
|
56
|
+
if (!stream && payload["stream"]) || payload["background"]
|
|
57
|
+
raise ArgumentError, "Use chat_stream for streaming; background execution is not supported"
|
|
58
|
+
end
|
|
59
|
+
unless message.is_a?(String) || message.is_a?(Hash) || message.is_a?(Array)
|
|
60
|
+
raise ArgumentError, "message must be a string, a Gemini content hash, or an array of Gemini content/steps"
|
|
61
|
+
end
|
|
62
|
+
generation = payload.fetch("generation_config", {})
|
|
63
|
+
raise ArgumentError, "generation_config must be a hash" unless generation.is_a?(Hash)
|
|
64
|
+
|
|
65
|
+
generation = generation.each_with_object({}) { |(key, value), result| result[key.to_s] = value }
|
|
66
|
+
generation["max_output_tokens"] = max_output_tokens || generation["max_output_tokens"] || self.max_output_tokens
|
|
67
|
+
payload.merge!("model" => model || self.model, "input" => message, "generation_config" => generation)
|
|
68
|
+
payload["system_instruction"] = instructions if instructions
|
|
69
|
+
payload["previous_interaction_id"] = previous_response_id if previous_response_id
|
|
70
|
+
payload["stream"] = true if stream
|
|
71
|
+
payload
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
include Chat
|
|
76
|
+
private_constant :Chat
|
|
77
|
+
end
|
|
78
|
+
end
|