llm.rb 3.0.0 → 3.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/README.md +121 -135
- data/lib/llm/contract/completion.rb +8 -0
- data/lib/llm/message.rb +2 -1
- data/lib/llm/object/kernel.rb +4 -4
- data/lib/llm/object.rb +42 -2
- data/lib/llm/providers/anthropic/response_adapter/completion.rb +8 -2
- data/lib/llm/providers/anthropic/stream_parser.rb +19 -5
- data/lib/llm/providers/anthropic.rb +20 -10
- data/lib/llm/providers/gemini/response_adapter/completion.rb +6 -0
- data/lib/llm/providers/gemini/stream_parser.rb +4 -0
- data/lib/llm/providers/gemini.rb +22 -12
- data/lib/llm/providers/ollama/response_adapter/completion.rb +6 -0
- data/lib/llm/providers/ollama.rb +20 -10
- data/lib/llm/providers/openai/response_adapter/completion.rb +12 -3
- data/lib/llm/providers/openai/stream_parser.rb +4 -2
- data/lib/llm/providers/openai.rb +23 -11
- data/lib/llm/response.rb +12 -2
- data/lib/llm/usage.rb +4 -3
- data/lib/llm/version.rb +1 -1
- data/lib/llm.rb +3 -0
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: fa682f0c6793298daeaac88092cb52f03652cbbbf28adfd6b62f94b8a263f3f3
|
|
4
|
+
data.tar.gz: 1fb08983372becef70d866bdc4ee79ee8d8bba55ace5d4be4637a69e91341747
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 720e09be8b25a9fde7d92887636d572edcdbd39a1b3a23ae1f44baaddb9f881c95927f63f16248f3a3d22da1704973f69f51309c487e1c97195175b772499b0d
|
|
7
|
+
data.tar.gz: 8cf35f7829b4e66ef002652643779658cf9c8cf8726f8b563eb5ca59ebcfc3a71eeb9b4cc473dfc4556324448855b6733fe3d48a73fb6e70fb91102544eb7061
|
data/README.md
CHANGED
|
@@ -19,32 +19,38 @@ A simple chatbot that maintains a conversation and streams responses in real-tim
|
|
|
19
19
|
#!/usr/bin/env ruby
|
|
20
20
|
require "llm"
|
|
21
21
|
|
|
22
|
-
llm = LLM.openai(key: ENV
|
|
22
|
+
llm = LLM.openai(key: ENV.fetch("KEY"))
|
|
23
23
|
bot = LLM::Bot.new(llm, stream: $stdout)
|
|
24
24
|
loop do
|
|
25
25
|
print "> "
|
|
26
|
-
bot.chat(gets)
|
|
27
|
-
|
|
26
|
+
bot.chat(STDIN.gets)
|
|
27
|
+
puts
|
|
28
28
|
end
|
|
29
29
|
```
|
|
30
30
|
|
|
31
31
|
#### Prompts
|
|
32
32
|
|
|
33
|
+
> ℹ️ **Tip:** Some providers (such as OpenAI) support `system` and `developer`
|
|
34
|
+
> roles, but the examples in this README stick to `user` roles since they are
|
|
35
|
+
> supported across all providers.
|
|
36
|
+
|
|
33
37
|
A prompt builder that produces a chain of messages that can be sent in one request:
|
|
34
38
|
|
|
35
39
|
```ruby
|
|
36
40
|
#!/usr/bin/env ruby
|
|
37
41
|
require "llm"
|
|
38
42
|
|
|
39
|
-
llm = LLM.openai(key: ENV
|
|
43
|
+
llm = LLM.openai(key: ENV.fetch("KEY"))
|
|
40
44
|
bot = LLM::Bot.new(llm)
|
|
45
|
+
|
|
41
46
|
prompt = bot.build_prompt do
|
|
42
|
-
it.
|
|
47
|
+
it.user "Answer concisely."
|
|
43
48
|
it.user "Was 2024 a leap year?"
|
|
44
|
-
it.user "How many days in
|
|
49
|
+
it.user "How many days were in that year?"
|
|
45
50
|
end
|
|
46
|
-
|
|
47
|
-
|
|
51
|
+
|
|
52
|
+
res = bot.chat(prompt)
|
|
53
|
+
res.choices.each { |m| puts "[#{m.role}] #{m.content}" }
|
|
48
54
|
```
|
|
49
55
|
|
|
50
56
|
#### Schema
|
|
@@ -56,20 +62,20 @@ A bot that instructs the LLM to respond in JSON, and according to the given sche
|
|
|
56
62
|
require "llm"
|
|
57
63
|
|
|
58
64
|
class Estimation < LLM::Schema
|
|
59
|
-
property :age, Integer, "
|
|
60
|
-
property :confidence, Number, "
|
|
61
|
-
property :notes, String, "
|
|
65
|
+
property :age, Integer, "Estimated age", required: true
|
|
66
|
+
property :confidence, Number, "0.0–1.0", required: true
|
|
67
|
+
property :notes, String, "Short notes", optional: true
|
|
62
68
|
end
|
|
63
69
|
|
|
64
|
-
llm = LLM.openai(key: ENV
|
|
70
|
+
llm = LLM.openai(key: ENV.fetch("KEY"))
|
|
65
71
|
bot = LLM::Bot.new(llm, schema: Estimation)
|
|
66
72
|
img = llm.images.create(prompt: "A man in his 30s")
|
|
67
|
-
res = bot.chat bot.image_url(img.urls
|
|
68
|
-
|
|
73
|
+
res = bot.chat bot.image_url(img.urls.first)
|
|
74
|
+
data = res.choices.find(&:assistant?).content!
|
|
69
75
|
|
|
70
|
-
puts "age: #{
|
|
71
|
-
puts "confidence: #{
|
|
72
|
-
puts "notes: #{
|
|
76
|
+
puts "age: #{data["age"]}"
|
|
77
|
+
puts "confidence: #{data["confidence"]}"
|
|
78
|
+
puts "notes: #{data["notes"]}" if data["notes"]
|
|
73
79
|
```
|
|
74
80
|
|
|
75
81
|
#### Tools
|
|
@@ -83,60 +89,57 @@ require "llm"
|
|
|
83
89
|
class System < LLM::Tool
|
|
84
90
|
name "system"
|
|
85
91
|
description "Run a shell command"
|
|
86
|
-
param :command, String, "
|
|
92
|
+
param :command, String, "Command to execute", required: true
|
|
87
93
|
|
|
88
94
|
def call(command:)
|
|
89
95
|
{success: system(command)}
|
|
90
96
|
end
|
|
91
97
|
end
|
|
92
98
|
|
|
93
|
-
llm = LLM.openai(key: ENV
|
|
99
|
+
llm = LLM.openai(key: ENV.fetch("KEY"))
|
|
94
100
|
bot = LLM::Bot.new(llm, tools: [System])
|
|
101
|
+
|
|
95
102
|
prompt = bot.build_prompt do
|
|
96
|
-
it.
|
|
97
|
-
it.user "
|
|
103
|
+
it.user "You can run safe shell commands."
|
|
104
|
+
it.user "Run `date`."
|
|
98
105
|
end
|
|
106
|
+
|
|
99
107
|
bot.chat(prompt)
|
|
100
|
-
bot.chat
|
|
101
|
-
bot.messages.select(&:assistant?).each {
|
|
108
|
+
bot.chat(bot.functions.map(&:call))
|
|
109
|
+
bot.messages.select(&:assistant?).each { |m| puts "[#{m.role}] #{m.content}" }
|
|
102
110
|
```
|
|
103
111
|
|
|
104
112
|
## Features
|
|
105
113
|
|
|
106
114
|
#### General
|
|
107
|
-
- ✅
|
|
108
|
-
- 📦
|
|
109
|
-
- 🧩
|
|
110
|
-
-
|
|
111
|
-
- ♻️ Optional: per-provider, process-wide connection pool via net-http-persistent
|
|
115
|
+
- ✅ Unified API across providers
|
|
116
|
+
- 📦 Zero runtime deps (stdlib-only)
|
|
117
|
+
- 🧩 Pluggable JSON adapters (JSON, Oj, Yajl, etc)
|
|
118
|
+
- ♻️ Optional persistent HTTP pool (net-http-persistent)
|
|
112
119
|
|
|
113
120
|
#### Chat, Agents
|
|
114
|
-
- 🧠
|
|
115
|
-
- 🤖
|
|
116
|
-
- 🗂️
|
|
117
|
-
- 📡
|
|
121
|
+
- 🧠 Stateless + stateful chat (completions + responses)
|
|
122
|
+
- 🤖 Tool calling / function execution
|
|
123
|
+
- 🗂️ JSON Schema structured output
|
|
124
|
+
- 📡 Streaming responses
|
|
118
125
|
|
|
119
126
|
#### Media
|
|
120
|
-
- 🗣️
|
|
121
|
-
- 🖼️
|
|
122
|
-
- 📎
|
|
123
|
-
- 📦
|
|
124
|
-
- 💡
|
|
127
|
+
- 🗣️ TTS, transcription, translation
|
|
128
|
+
- 🖼️ Image generation + editing
|
|
129
|
+
- 📎 Files API + prompt-aware file inputs
|
|
130
|
+
- 📦 Streaming multipart uploads (no full buffering)
|
|
131
|
+
- 💡 Multimodal prompts (text, documents, audio, images, video, URLs)
|
|
125
132
|
|
|
126
133
|
#### Embeddings
|
|
127
|
-
- 🧮
|
|
128
|
-
- 🧱
|
|
134
|
+
- 🧮 Embeddings
|
|
135
|
+
- 🧱 OpenAI vector stores (RAG)
|
|
129
136
|
|
|
130
137
|
#### Miscellaneous
|
|
131
|
-
- 📜
|
|
132
|
-
- 🔧
|
|
138
|
+
- 📜 Models API
|
|
139
|
+
- 🔧 OpenAI responses + moderations
|
|
133
140
|
|
|
134
141
|
## Matrix
|
|
135
142
|
|
|
136
|
-
While the Features section above gives you the high-level picture, the table below
|
|
137
|
-
breaks things down by provider, so you can see exactly what’s supported where.
|
|
138
|
-
|
|
139
|
-
|
|
140
143
|
| Feature / Provider | OpenAI | Anthropic | Gemini | DeepSeek | xAI (Grok) | zAI | Ollama | LlamaCpp |
|
|
141
144
|
|--------------------------------------|:------:|:---------:|:------:|:--------:|:----------:|:------:|:------:|:--------:|
|
|
142
145
|
| **Chat Completions** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
@@ -187,6 +190,27 @@ llm = LLM.ollama(key: nil)
|
|
|
187
190
|
llm = LLM.llamacpp(key: nil)
|
|
188
191
|
```
|
|
189
192
|
|
|
193
|
+
#### LLM::Response
|
|
194
|
+
|
|
195
|
+
All provider methods that perform requests return an
|
|
196
|
+
[LLM::Response](https://0x1eef.github.io/x/llm.rb/LLM/Response.html).
|
|
197
|
+
If the HTTP response is JSON (`content-type: application/json`),
|
|
198
|
+
`response.body` is parsed into an
|
|
199
|
+
[LLM::Object](https://0x1eef.github.io/x/llm.rb/LLM/Object.html) for
|
|
200
|
+
dot-access. For non-JSON responses, `response.body` is a raw string.
|
|
201
|
+
It is also possible to access top-level keys directly on the response
|
|
202
|
+
(eg: `res.object` instead of `res.body.object`):
|
|
203
|
+
|
|
204
|
+
```ruby
|
|
205
|
+
#!/usr/bin/env ruby
|
|
206
|
+
require "llm"
|
|
207
|
+
|
|
208
|
+
llm = LLM.openai(key: ENV["KEY"])
|
|
209
|
+
res = llm.models.all
|
|
210
|
+
puts res.object
|
|
211
|
+
puts res.data.first.id
|
|
212
|
+
```
|
|
213
|
+
|
|
190
214
|
#### Persistence
|
|
191
215
|
|
|
192
216
|
The llm.rb library can maintain a process-wide connection pool
|
|
@@ -203,7 +227,7 @@ llm = LLM.openai(key: ENV["KEY"], persistent: true)
|
|
|
203
227
|
res1 = llm.responses.create "message 1"
|
|
204
228
|
res2 = llm.responses.create "message 2", previous_response_id: res1.response_id
|
|
205
229
|
res3 = llm.responses.create "message 3", previous_response_id: res2.response_id
|
|
206
|
-
|
|
230
|
+
puts res3.output_text
|
|
207
231
|
```
|
|
208
232
|
|
|
209
233
|
#### Thread Safety
|
|
@@ -236,15 +260,17 @@ require "llm"
|
|
|
236
260
|
|
|
237
261
|
llm = LLM.openai(key: ENV["KEY"])
|
|
238
262
|
bot = LLM::Bot.new(llm)
|
|
239
|
-
|
|
263
|
+
image_url = "https://upload.wikimedia.org/wikipedia/commons/9/97/The_Earth_seen_from_Apollo_17.jpg"
|
|
264
|
+
image_path = "/tmp/llm-logo.png"
|
|
265
|
+
pdf_path = "/tmp/llm-handbook.pdf"
|
|
240
266
|
|
|
241
267
|
prompt = bot.build_prompt do
|
|
242
|
-
it.
|
|
243
|
-
it.user ["Tell me about this
|
|
244
|
-
it.user ["Tell me about this PDF", bot.local_file(
|
|
268
|
+
it.user ["Tell me about this image", bot.image_url(image_url)]
|
|
269
|
+
it.user ["Tell me about this image", bot.local_file(image_path)]
|
|
270
|
+
it.user ["Tell me about this PDF", bot.local_file(pdf_path)]
|
|
245
271
|
end
|
|
246
272
|
bot.chat(prompt)
|
|
247
|
-
bot.messages.each {
|
|
273
|
+
bot.messages.each { |m| puts "[#{m.role}] #{m.content}" }
|
|
248
274
|
```
|
|
249
275
|
|
|
250
276
|
#### Streaming
|
|
@@ -262,20 +288,20 @@ require "llm"
|
|
|
262
288
|
|
|
263
289
|
llm = LLM.openai(key: ENV["KEY"])
|
|
264
290
|
bot = LLM::Bot.new(llm, stream: $stdout)
|
|
265
|
-
|
|
291
|
+
image_url = "https://upload.wikimedia.org/wikipedia/commons/9/97/The_Earth_seen_from_Apollo_17.jpg"
|
|
292
|
+
image_path = "/tmp/llm-logo.png"
|
|
293
|
+
pdf_path = "/tmp/llm-handbook.pdf"
|
|
266
294
|
|
|
267
295
|
prompt = bot.build_prompt do
|
|
268
|
-
it.
|
|
269
|
-
it.user ["Tell me about this
|
|
270
|
-
it.user ["Tell me about the PDF", bot.local_file(
|
|
296
|
+
it.user ["Tell me about this image", bot.image_url(image_url)]
|
|
297
|
+
it.user ["Tell me about this image", bot.local_file(image_path)]
|
|
298
|
+
it.user ["Tell me about the PDF", bot.local_file(pdf_path)]
|
|
271
299
|
end
|
|
272
300
|
bot.chat(prompt)
|
|
273
301
|
```
|
|
274
302
|
|
|
275
303
|
### Schema
|
|
276
304
|
|
|
277
|
-
#### Object
|
|
278
|
-
|
|
279
305
|
All LLM providers except Anthropic and DeepSeek allow a client to describe
|
|
280
306
|
the structure of a response that a LLM emits according to a schema that is
|
|
281
307
|
described by JSON. The schema lets a client describe what JSON object
|
|
@@ -286,56 +312,21 @@ its ability:
|
|
|
286
312
|
#!/usr/bin/env ruby
|
|
287
313
|
require "llm"
|
|
288
314
|
|
|
289
|
-
llm = LLM.openai(key: ENV["KEY"])
|
|
290
|
-
|
|
291
|
-
##
|
|
292
|
-
# Objects
|
|
293
|
-
schema = llm.schema.object(probability: llm.schema.number.required)
|
|
294
|
-
bot = LLM::Bot.new(llm, schema:)
|
|
295
|
-
bot.chat "Does the earth orbit the sun?", role: :user
|
|
296
|
-
puts bot.messages.find(&:assistant?).content! # => {probability: 1.0}
|
|
297
|
-
|
|
298
|
-
##
|
|
299
|
-
# Enums
|
|
300
|
-
schema = llm.schema.object(fruit: llm.schema.string.enum("Apple", "Orange", "Pineapple"))
|
|
301
|
-
bot = LLM::Bot.new(llm, schema:) :system
|
|
302
|
-
bot.chat "What fruit is your favorite?", role: :user
|
|
303
|
-
puts bot.messages.find(&:assistant?).content! # => {fruit: "Pineapple"}
|
|
304
|
-
|
|
305
|
-
##
|
|
306
|
-
# Arrays
|
|
307
|
-
schema = llm.schema.object(answers: llm.schema.array(llm.schema.integer.required))
|
|
308
|
-
bot = LLM::Bot.new(llm, schema:)
|
|
309
|
-
bot.chat "Tell me the answer to ((5 + 5) / 2) * 2 + 1", role: :user
|
|
310
|
-
puts bot.messages.find(&:assistant?).content! # => {answers: [11]}
|
|
311
|
-
```
|
|
312
|
-
|
|
313
|
-
#### Class
|
|
314
|
-
|
|
315
|
-
Other than the object form we saw in the previous example, a class form
|
|
316
|
-
is also supported. Under the hood, it is implemented with the object form
|
|
317
|
-
and the class form primarily exists to provide structure and organization
|
|
318
|
-
that the object form lacks:
|
|
319
|
-
|
|
320
|
-
```ruby
|
|
321
|
-
#!/usr/bin/env ruby
|
|
322
|
-
require "llm"
|
|
323
|
-
|
|
324
315
|
class Player < LLM::Schema
|
|
325
316
|
property :name, String, "The player's name", required: true
|
|
326
|
-
property :
|
|
317
|
+
property :position, Array[Number], "The player's [x, y] position", required: true
|
|
327
318
|
end
|
|
328
319
|
|
|
329
320
|
llm = LLM.openai(key: ENV["KEY"])
|
|
330
321
|
bot = LLM::Bot.new(llm, schema: Player)
|
|
331
322
|
prompt = bot.build_prompt do
|
|
332
|
-
it.
|
|
333
|
-
it.user "
|
|
323
|
+
it.user "The player's name is Sam and their position is (7, 12)."
|
|
324
|
+
it.user "Return the player's name and position"
|
|
334
325
|
end
|
|
335
326
|
|
|
336
327
|
player = bot.chat(prompt).content!
|
|
337
|
-
puts "name: #{player
|
|
338
|
-
puts "
|
|
328
|
+
puts "name: #{player['name']}"
|
|
329
|
+
puts "position: #{player['position'].join(', ')}"
|
|
339
330
|
```
|
|
340
331
|
|
|
341
332
|
### Tools
|
|
@@ -383,7 +374,7 @@ tool = LLM.function(:system) do |fn|
|
|
|
383
374
|
end
|
|
384
375
|
|
|
385
376
|
bot = LLM::Bot.new(llm, tools: [tool])
|
|
386
|
-
bot.chat "Your task is to run shell commands via a tool.", role: :
|
|
377
|
+
bot.chat "Your task is to run shell commands via a tool.", role: :user
|
|
387
378
|
|
|
388
379
|
bot.chat "What is the current date?", role: :user
|
|
389
380
|
bot.chat bot.functions.map(&:call) # report return value to the LLM
|
|
@@ -430,7 +421,7 @@ end
|
|
|
430
421
|
|
|
431
422
|
llm = LLM.openai(key: ENV["KEY"])
|
|
432
423
|
bot = LLM::Bot.new(llm, tools: [System])
|
|
433
|
-
bot.chat "Your task is to run shell commands via a tool.", role: :
|
|
424
|
+
bot.chat "Your task is to run shell commands via a tool.", role: :user
|
|
434
425
|
|
|
435
426
|
bot.chat "What is the current date?", role: :user
|
|
436
427
|
bot.chat bot.functions.map(&:call) # report return value to the LLM
|
|
@@ -460,9 +451,9 @@ require "llm"
|
|
|
460
451
|
|
|
461
452
|
llm = LLM.openai(key: ENV["KEY"])
|
|
462
453
|
bot = LLM::Bot.new(llm)
|
|
463
|
-
file = llm.files.create(file: "/book.pdf")
|
|
454
|
+
file = llm.files.create(file: "/tmp/llm-book.pdf")
|
|
464
455
|
res = bot.chat ["Tell me about this file", file]
|
|
465
|
-
res.choices.each {
|
|
456
|
+
res.choices.each { |m| puts "[#{m.role}] #{m.content}" }
|
|
466
457
|
```
|
|
467
458
|
|
|
468
459
|
### Prompts
|
|
@@ -493,17 +484,19 @@ require "llm"
|
|
|
493
484
|
|
|
494
485
|
llm = LLM.openai(key: ENV["KEY"])
|
|
495
486
|
bot = LLM::Bot.new(llm)
|
|
496
|
-
|
|
487
|
+
image_url = "https://upload.wikimedia.org/wikipedia/commons/9/97/The_Earth_seen_from_Apollo_17.jpg"
|
|
488
|
+
image_path = "/tmp/llm-logo.png"
|
|
489
|
+
pdf_path = "/tmp/llm-book.pdf"
|
|
497
490
|
|
|
498
|
-
res1 = bot.chat ["Tell me about this URL", bot.image_url(
|
|
499
|
-
res1.choices.each {
|
|
491
|
+
res1 = bot.chat ["Tell me about this image URL", bot.image_url(image_url)]
|
|
492
|
+
res1.choices.each { |m| puts "[#{m.role}] #{m.content}" }
|
|
500
493
|
|
|
501
|
-
file = llm.files.create(file:
|
|
494
|
+
file = llm.files.create(file: pdf_path)
|
|
502
495
|
res2 = bot.chat ["Tell me about this PDF", bot.remote_file(file)]
|
|
503
|
-
res2.choices.each {
|
|
496
|
+
res2.choices.each { |m| puts "[#{m.role}] #{m.content}" }
|
|
504
497
|
|
|
505
|
-
res3 = bot.chat ["Tell me about this image", bot.local_file(
|
|
506
|
-
res3.choices.each {
|
|
498
|
+
res3 = bot.chat ["Tell me about this image", bot.local_file(image_path)]
|
|
499
|
+
res3.choices.each { |m| puts "[#{m.role}] #{m.content}" }
|
|
507
500
|
```
|
|
508
501
|
|
|
509
502
|
### Audio
|
|
@@ -540,7 +533,7 @@ llm = LLM.openai(key: ENV["KEY"])
|
|
|
540
533
|
res = llm.audio.create_transcription(
|
|
541
534
|
file: File.join(Dir.home, "hello.mp3")
|
|
542
535
|
)
|
|
543
|
-
|
|
536
|
+
puts res.text # => "Hello world."
|
|
544
537
|
```
|
|
545
538
|
|
|
546
539
|
#### Translate
|
|
@@ -558,7 +551,7 @@ llm = LLM.openai(key: ENV["KEY"])
|
|
|
558
551
|
res = llm.audio.create_translation(
|
|
559
552
|
file: File.join(Dir.home, "bomdia.mp3")
|
|
560
553
|
)
|
|
561
|
-
|
|
554
|
+
puts res.text # => "Good morning."
|
|
562
555
|
```
|
|
563
556
|
|
|
564
557
|
### Images
|
|
@@ -588,8 +581,8 @@ end
|
|
|
588
581
|
#### Edit
|
|
589
582
|
|
|
590
583
|
The following example is focused on editing a local image with the aid
|
|
591
|
-
of a prompt. The image (`/
|
|
592
|
-
|
|
584
|
+
of a prompt. The image (`/tmp/llm-logo.png`) is returned to us with a hat.
|
|
585
|
+
The image is then moved to `${HOME}/logo-with-hat.png` as
|
|
593
586
|
the final step:
|
|
594
587
|
|
|
595
588
|
```ruby
|
|
@@ -600,20 +593,20 @@ require "fileutils"
|
|
|
600
593
|
|
|
601
594
|
llm = LLM.openai(key: ENV["KEY"])
|
|
602
595
|
res = llm.images.edit(
|
|
603
|
-
image: "/
|
|
604
|
-
prompt: "a
|
|
596
|
+
image: "/tmp/llm-logo.png",
|
|
597
|
+
prompt: "add a hat to the logo",
|
|
605
598
|
)
|
|
606
599
|
res.urls.each do |url|
|
|
607
600
|
FileUtils.mv OpenURI.open_uri(url).path,
|
|
608
|
-
File.join(Dir.home, "
|
|
601
|
+
File.join(Dir.home, "logo-with-hat.png")
|
|
609
602
|
end
|
|
610
603
|
```
|
|
611
604
|
|
|
612
605
|
#### Variations
|
|
613
606
|
|
|
614
607
|
The following example is focused on creating variations of a local image.
|
|
615
|
-
The image (`/
|
|
616
|
-
The images are then moved to `${HOME}/
|
|
608
|
+
The image (`/tmp/llm-logo.png`) is returned to us with five different variations.
|
|
609
|
+
The images are then moved to `${HOME}/logo-variation0.png`, `${HOME}/logo-variation1.png`
|
|
617
610
|
and so on as the final step:
|
|
618
611
|
|
|
619
612
|
```ruby
|
|
@@ -624,12 +617,12 @@ require "fileutils"
|
|
|
624
617
|
|
|
625
618
|
llm = LLM.openai(key: ENV["KEY"])
|
|
626
619
|
res = llm.images.create_variation(
|
|
627
|
-
image: "/
|
|
620
|
+
image: "/tmp/llm-logo.png",
|
|
628
621
|
n: 5
|
|
629
622
|
)
|
|
630
623
|
res.urls.each.with_index do |url, index|
|
|
631
624
|
FileUtils.mv OpenURI.open_uri(url).path,
|
|
632
|
-
File.join(Dir.home, "
|
|
625
|
+
File.join(Dir.home, "logo-variation#{index}.png")
|
|
633
626
|
end
|
|
634
627
|
```
|
|
635
628
|
|
|
@@ -639,13 +632,8 @@ end
|
|
|
639
632
|
|
|
640
633
|
The
|
|
641
634
|
[`LLM::Provider#embed`](https://0x1eef.github.io/x/llm.rb/LLM/Provider.html#embed-instance_method)
|
|
642
|
-
method
|
|
643
|
-
|
|
644
|
-
a common use-case for them is to store chunks of text in a
|
|
645
|
-
vector database, and then to query the database for *semantically
|
|
646
|
-
similar* text. These chunks of similar text can then support the
|
|
647
|
-
generation of a prompt that is used to query a large language model,
|
|
648
|
-
which will go on to generate a response:
|
|
635
|
+
method returns vector embeddings for one or more text inputs. A common
|
|
636
|
+
use is semantic search (store vectors, then query for similar text):
|
|
649
637
|
|
|
650
638
|
```ruby
|
|
651
639
|
#!/usr/bin/env ruby
|
|
@@ -653,9 +641,9 @@ require "llm"
|
|
|
653
641
|
|
|
654
642
|
llm = LLM.openai(key: ENV["KEY"])
|
|
655
643
|
res = llm.embed(["programming is fun", "ruby is a programming language", "sushi is art"])
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
644
|
+
puts res.class
|
|
645
|
+
puts res.embeddings.size
|
|
646
|
+
puts res.embeddings[0].size
|
|
659
647
|
|
|
660
648
|
##
|
|
661
649
|
# LLM::Response
|
|
@@ -669,10 +657,8 @@ print res.embeddings[0].size, "\n"
|
|
|
669
657
|
|
|
670
658
|
Almost all LLM providers provide a models endpoint that allows a client to
|
|
671
659
|
query the list of models that are available to use. The list is dynamic,
|
|
672
|
-
maintained by LLM providers, and it is independent of a specific llm.rb
|
|
673
|
-
|
|
674
|
-
objects can be used instead of a string that describes a model name (although
|
|
675
|
-
either works). Let's take a look at an example:
|
|
660
|
+
maintained by LLM providers, and it is independent of a specific llm.rb
|
|
661
|
+
release:
|
|
676
662
|
|
|
677
663
|
```ruby
|
|
678
664
|
#!/usr/bin/env ruby
|
|
@@ -682,7 +668,7 @@ require "llm"
|
|
|
682
668
|
# List all models
|
|
683
669
|
llm = LLM.openai(key: ENV["KEY"])
|
|
684
670
|
llm.models.all.each do |model|
|
|
685
|
-
|
|
671
|
+
puts "model: #{model.id}"
|
|
686
672
|
end
|
|
687
673
|
|
|
688
674
|
##
|
|
@@ -690,7 +676,7 @@ end
|
|
|
690
676
|
model = llm.models.all.find { |m| m.id == "gpt-3.5-turbo" }
|
|
691
677
|
bot = LLM::Bot.new(llm, model: model.id)
|
|
692
678
|
res = bot.chat "Hello #{model.id} :)"
|
|
693
|
-
res.choices.each {
|
|
679
|
+
res.choices.each { |m| puts "[#{m.role}] #{m.content}" }
|
|
694
680
|
```
|
|
695
681
|
|
|
696
682
|
## Install
|
|
@@ -29,6 +29,13 @@ module LLM::Contract
|
|
|
29
29
|
raise NotImplementedError, "#{self.class} does not implement '#{__method__}'"
|
|
30
30
|
end
|
|
31
31
|
|
|
32
|
+
##
|
|
33
|
+
# @return [Integer]
|
|
34
|
+
# Returns the number of reasoning tokens
|
|
35
|
+
def reasoning_tokens
|
|
36
|
+
raise NotImplementedError, "#{self.class} does not implement '#{__method__}'"
|
|
37
|
+
end
|
|
38
|
+
|
|
32
39
|
##
|
|
33
40
|
# @return [Integer]
|
|
34
41
|
# Returns the total number of tokens
|
|
@@ -43,6 +50,7 @@ module LLM::Contract
|
|
|
43
50
|
LLM::Usage.new(
|
|
44
51
|
input_tokens:,
|
|
45
52
|
output_tokens:,
|
|
53
|
+
reasoning_tokens:,
|
|
46
54
|
total_tokens:
|
|
47
55
|
)
|
|
48
56
|
end
|
data/lib/llm/message.rb
CHANGED
data/lib/llm/object/kernel.rb
CHANGED
|
@@ -20,17 +20,17 @@ class LLM::Object
|
|
|
20
20
|
::Kernel.instance_method(:method).bind(self).call(...)
|
|
21
21
|
end
|
|
22
22
|
|
|
23
|
-
def kind_of?(
|
|
24
|
-
::Kernel.instance_method(:kind_of?).bind(self).call(
|
|
23
|
+
def kind_of?(klass)
|
|
24
|
+
::Kernel.instance_method(:kind_of?).bind(self).call(klass)
|
|
25
25
|
end
|
|
26
26
|
alias_method :is_a?, :kind_of?
|
|
27
27
|
|
|
28
28
|
def respond_to?(m, include_private = false)
|
|
29
|
-
!!key(m) || self.class.method_defined?(m)
|
|
29
|
+
!!key(m) || self.class.method_defined?(m)
|
|
30
30
|
end
|
|
31
31
|
|
|
32
32
|
def respond_to_missing?(m, include_private = false)
|
|
33
|
-
!!key(m)
|
|
33
|
+
!!key(m)
|
|
34
34
|
end
|
|
35
35
|
|
|
36
36
|
def object_id
|
data/lib/llm/object.rb
CHANGED
|
@@ -68,16 +68,56 @@ class LLM::Object < BasicObject
|
|
|
68
68
|
@h.transform_keys(&:to_sym)
|
|
69
69
|
end
|
|
70
70
|
|
|
71
|
+
##
|
|
72
|
+
# @return [Array<String>]
|
|
73
|
+
def keys
|
|
74
|
+
@h.keys
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
##
|
|
78
|
+
# @return [Array]
|
|
79
|
+
def values
|
|
80
|
+
@h.values
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
##
|
|
84
|
+
# @param [String, Symbol] k
|
|
85
|
+
# @return [Boolean]
|
|
86
|
+
def key?(k)
|
|
87
|
+
@h.key?(key(k))
|
|
88
|
+
end
|
|
89
|
+
alias_method :has_key?, :key?
|
|
90
|
+
|
|
91
|
+
##
|
|
92
|
+
# @param [String, Symbol] k
|
|
93
|
+
# @return [Object]
|
|
94
|
+
def fetch(k, *args, &b)
|
|
95
|
+
@h.fetch(key(k), *args, &b)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
##
|
|
99
|
+
# @return [Integer]
|
|
100
|
+
def size
|
|
101
|
+
@h.size
|
|
102
|
+
end
|
|
103
|
+
alias_method :length, :size
|
|
104
|
+
|
|
105
|
+
##
|
|
106
|
+
# @yieldparam [String, Object]
|
|
107
|
+
def each_pair(&)
|
|
108
|
+
@h.each(&)
|
|
109
|
+
end
|
|
110
|
+
|
|
71
111
|
##
|
|
72
112
|
# @return [Object, nil]
|
|
73
113
|
def dig(...)
|
|
74
|
-
|
|
114
|
+
@h.dig(...)
|
|
75
115
|
end
|
|
76
116
|
|
|
77
117
|
##
|
|
78
118
|
# @return [Hash]
|
|
79
119
|
def slice(...)
|
|
80
|
-
|
|
120
|
+
@h.slice(...)
|
|
81
121
|
end
|
|
82
122
|
|
|
83
123
|
private
|
|
@@ -12,13 +12,19 @@ module LLM::Anthropic::ResponseAdapter
|
|
|
12
12
|
##
|
|
13
13
|
# (see LLM::Contract::Completion#input_tokens)
|
|
14
14
|
def input_tokens
|
|
15
|
-
body.usage
|
|
15
|
+
body.usage&.input_tokens || 0
|
|
16
16
|
end
|
|
17
17
|
|
|
18
18
|
##
|
|
19
19
|
# (see LLM::Contract::Completion#output_tokens)
|
|
20
20
|
def output_tokens
|
|
21
|
-
body.usage
|
|
21
|
+
body.usage&.output_tokens || 0
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
##
|
|
25
|
+
# (see LLM::Contract::Completion#reasoning_tokens)
|
|
26
|
+
def reasoning_tokens
|
|
27
|
+
0
|
|
22
28
|
end
|
|
23
29
|
|
|
24
30
|
##
|
|
@@ -40,11 +40,14 @@ class LLM::Anthropic
|
|
|
40
40
|
if Hash === content["input"]
|
|
41
41
|
content["input"] = chunk["delta"]["partial_json"]
|
|
42
42
|
else
|
|
43
|
+
content["input"] ||= +""
|
|
43
44
|
content["input"] << chunk["delta"]["partial_json"]
|
|
44
45
|
end
|
|
45
46
|
end
|
|
46
47
|
elsif chunk["type"] == "message_delta"
|
|
47
|
-
merge_message!(chunk["delta"])
|
|
48
|
+
merge_message!(chunk["delta"]) if chunk["delta"]
|
|
49
|
+
extras = chunk.reject { |k, _| k == "type" || k == "delta" }
|
|
50
|
+
merge_message!(extras) unless extras.empty?
|
|
48
51
|
elsif chunk["type"] == "content_block_stop"
|
|
49
52
|
content = @body["content"][chunk["index"]]
|
|
50
53
|
if content["input"]
|
|
@@ -54,11 +57,22 @@ class LLM::Anthropic
|
|
|
54
57
|
end
|
|
55
58
|
|
|
56
59
|
def merge_message!(message)
|
|
57
|
-
message.
|
|
58
|
-
|
|
59
|
-
|
|
60
|
+
message.each_pair do |key, value|
|
|
61
|
+
if value.respond_to?(:each_pair)
|
|
62
|
+
@body[key] ||= {}
|
|
63
|
+
deep_merge!(@body[key], value)
|
|
60
64
|
else
|
|
61
|
-
value
|
|
65
|
+
@body[key] = value
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def deep_merge!(target, source)
|
|
71
|
+
source.each_pair do |key, value|
|
|
72
|
+
if value.respond_to?(:each_pair) && target[key].respond_to?(:each_pair)
|
|
73
|
+
deep_merge!(target[key], value)
|
|
74
|
+
else
|
|
75
|
+
target[key] = value
|
|
62
76
|
end
|
|
63
77
|
end
|
|
64
78
|
end
|
|
@@ -41,16 +41,9 @@ module LLM
|
|
|
41
41
|
# When given an object a provider does not understand
|
|
42
42
|
# @return (see LLM::Provider#complete)
|
|
43
43
|
def complete(prompt, params = {})
|
|
44
|
-
params
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
role, stream = params.delete(:role), params.delete(:stream)
|
|
48
|
-
params[:stream] = true if stream.respond_to?(:<<) || stream == true
|
|
49
|
-
req = Net::HTTP::Post.new("/v1/messages", headers)
|
|
50
|
-
messages = [*(params.delete(:messages) || []), Message.new(role, prompt)]
|
|
51
|
-
body = LLM.json.dump({messages: [adapt(messages)].flatten}.merge!(params))
|
|
52
|
-
set_body_stream(req, StringIO.new(body))
|
|
53
|
-
res = execute(request: req, stream:)
|
|
44
|
+
params, stream, tools, role = normalize_complete_params(params)
|
|
45
|
+
req = build_complete_request(prompt, params, role)
|
|
46
|
+
res = execute(request: req, stream: stream)
|
|
54
47
|
ResponseAdapter.adapt(res, type: :completion)
|
|
55
48
|
.extend(Module.new { define_method(:__tools__) { tools } })
|
|
56
49
|
end
|
|
@@ -131,5 +124,22 @@ module LLM
|
|
|
131
124
|
def error_handler
|
|
132
125
|
LLM::Anthropic::ErrorHandler
|
|
133
126
|
end
|
|
127
|
+
|
|
128
|
+
def normalize_complete_params(params)
|
|
129
|
+
params = {role: :user, model: default_model, max_tokens: 1024}.merge!(params)
|
|
130
|
+
tools = resolve_tools(params.delete(:tools))
|
|
131
|
+
params = [params, adapt_tools(tools)].inject({}, &:merge!).compact
|
|
132
|
+
role, stream = params.delete(:role), params.delete(:stream)
|
|
133
|
+
params[:stream] = true if stream.respond_to?(:<<) || stream == true
|
|
134
|
+
[params, stream, tools, role]
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def build_complete_request(prompt, params, role)
|
|
138
|
+
messages = [*(params.delete(:messages) || []), Message.new(role, prompt)]
|
|
139
|
+
body = LLM.json.dump({messages: [adapt(messages)].flatten}.merge!(params))
|
|
140
|
+
req = Net::HTTP::Post.new("/v1/messages", headers)
|
|
141
|
+
set_body_stream(req, StringIO.new(body))
|
|
142
|
+
req
|
|
143
|
+
end
|
|
134
144
|
end
|
|
135
145
|
end
|
|
@@ -21,6 +21,12 @@ module LLM::Gemini::ResponseAdapter
|
|
|
21
21
|
body.usageMetadata.candidatesTokenCount || 0
|
|
22
22
|
end
|
|
23
23
|
|
|
24
|
+
##
|
|
25
|
+
# (see LLM::Contract::Completion#reasoning_tokens)
|
|
26
|
+
def reasoning_tokens
|
|
27
|
+
body.usageMetadata.thoughtsTokenCount || 0
|
|
28
|
+
end
|
|
29
|
+
|
|
24
30
|
##
|
|
25
31
|
# (see LLM::Contract::Completion#total_tokens)
|
|
26
32
|
def total_tokens
|
|
@@ -104,6 +104,10 @@ class LLM::Gemini
|
|
|
104
104
|
delta_call = delta["functionCall"]
|
|
105
105
|
if last_call.is_a?(Hash) && delta_call.is_a?(Hash)
|
|
106
106
|
last_existing_part["functionCall"] = last_call.merge(delta_call)
|
|
107
|
+
delta.each do |key, value|
|
|
108
|
+
next if key == "functionCall"
|
|
109
|
+
last_existing_part[key] = value
|
|
110
|
+
end
|
|
107
111
|
else
|
|
108
112
|
parts << delta
|
|
109
113
|
end
|
data/lib/llm/providers/gemini.rb
CHANGED
|
@@ -64,18 +64,9 @@ module LLM
|
|
|
64
64
|
# When given an object a provider does not understand
|
|
65
65
|
# @return [LLM::Response]
|
|
66
66
|
def complete(prompt, params = {})
|
|
67
|
-
params
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
role, model, stream = [:role, :model, :stream].map { params.delete(_1) }
|
|
71
|
-
action = stream ? "streamGenerateContent?key=#{@key}&alt=sse" : "generateContent?key=#{@key}"
|
|
72
|
-
model.respond_to?(:id) ? model.id : model
|
|
73
|
-
path = ["/v1beta/models/#{model}", action].join(":")
|
|
74
|
-
req = Net::HTTP::Post.new(path, headers)
|
|
75
|
-
messages = [*(params.delete(:messages) || []), LLM::Message.new(role, prompt)]
|
|
76
|
-
body = LLM.json.dump({contents: adapt(messages)}.merge!(params))
|
|
77
|
-
set_body_stream(req, StringIO.new(body))
|
|
78
|
-
res = execute(request: req, stream:)
|
|
67
|
+
params, stream, tools, role, model = normalize_complete_params(params)
|
|
68
|
+
req = build_complete_request(prompt, params, role, model, stream)
|
|
69
|
+
res = execute(request: req, stream: stream)
|
|
79
70
|
ResponseAdapter.adapt(res, type: :completion)
|
|
80
71
|
.extend(Module.new { define_method(:__tools__) { tools } })
|
|
81
72
|
end
|
|
@@ -165,5 +156,24 @@ module LLM
|
|
|
165
156
|
def error_handler
|
|
166
157
|
LLM::Gemini::ErrorHandler
|
|
167
158
|
end
|
|
159
|
+
|
|
160
|
+
def normalize_complete_params(params)
|
|
161
|
+
params = {role: :user, model: default_model}.merge!(params)
|
|
162
|
+
tools = resolve_tools(params.delete(:tools))
|
|
163
|
+
params = [params, adapt_schema(params), adapt_tools(tools)].inject({}, &:merge!).compact
|
|
164
|
+
role, model, stream = [:role, :model, :stream].map { params.delete(_1) }
|
|
165
|
+
[params, stream, tools, role, model]
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def build_complete_request(prompt, params, role, model, stream)
|
|
169
|
+
action = stream ? "streamGenerateContent?key=#{@key}&alt=sse" : "generateContent?key=#{@key}"
|
|
170
|
+
model.respond_to?(:id) ? model.id : model
|
|
171
|
+
path = ["/v1beta/models/#{model}", action].join(":")
|
|
172
|
+
req = Net::HTTP::Post.new(path, headers)
|
|
173
|
+
messages = [*(params.delete(:messages) || []), LLM::Message.new(role, prompt)]
|
|
174
|
+
body = LLM.json.dump({contents: adapt(messages)}.merge!(params))
|
|
175
|
+
set_body_stream(req, StringIO.new(body))
|
|
176
|
+
req
|
|
177
|
+
end
|
|
168
178
|
end
|
|
169
179
|
end
|
data/lib/llm/providers/ollama.rb
CHANGED
|
@@ -58,16 +58,9 @@ module LLM
|
|
|
58
58
|
# When given an object a provider does not understand
|
|
59
59
|
# @return [LLM::Response]
|
|
60
60
|
def complete(prompt, params = {})
|
|
61
|
-
params
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
role, stream = params.delete(:role), params.delete(:stream)
|
|
65
|
-
params[:stream] = true if stream.respond_to?(:<<) || stream == true
|
|
66
|
-
req = Net::HTTP::Post.new("/api/chat", headers)
|
|
67
|
-
messages = [*(params.delete(:messages) || []), LLM::Message.new(role, prompt)]
|
|
68
|
-
body = LLM.json.dump({messages: [adapt(messages)].flatten}.merge!(params))
|
|
69
|
-
set_body_stream(req, StringIO.new(body))
|
|
70
|
-
res = execute(request: req, stream:)
|
|
61
|
+
params, stream, tools, role = normalize_complete_params(params)
|
|
62
|
+
req = build_complete_request(prompt, params, role)
|
|
63
|
+
res = execute(request: req, stream: stream)
|
|
71
64
|
ResponseAdapter.adapt(res, type: :completion)
|
|
72
65
|
.extend(Module.new { define_method(:__tools__) { tools } })
|
|
73
66
|
end
|
|
@@ -110,5 +103,22 @@ module LLM
|
|
|
110
103
|
def error_handler
|
|
111
104
|
LLM::Ollama::ErrorHandler
|
|
112
105
|
end
|
|
106
|
+
|
|
107
|
+
def normalize_complete_params(params)
|
|
108
|
+
params = {role: :user, model: default_model, stream: true}.merge!(params)
|
|
109
|
+
tools = resolve_tools(params.delete(:tools))
|
|
110
|
+
params = [params, {format: params[:schema]}, adapt_tools(tools)].inject({}, &:merge!).compact
|
|
111
|
+
role, stream = params.delete(:role), params.delete(:stream)
|
|
112
|
+
params[:stream] = true if stream.respond_to?(:<<) || stream == true
|
|
113
|
+
[params, stream, tools, role]
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def build_complete_request(prompt, params, role)
|
|
117
|
+
messages = [*(params.delete(:messages) || []), LLM::Message.new(role, prompt)]
|
|
118
|
+
body = LLM.json.dump({messages: [adapt(messages)].flatten}.merge!(params))
|
|
119
|
+
req = Net::HTTP::Post.new("/api/chat", headers)
|
|
120
|
+
set_body_stream(req, StringIO.new(body))
|
|
121
|
+
req
|
|
122
|
+
end
|
|
113
123
|
end
|
|
114
124
|
end
|
|
@@ -21,19 +21,28 @@ module LLM::OpenAI::ResponseAdapter
|
|
|
21
21
|
##
|
|
22
22
|
# (see LLM::Contract::Completion#input_tokens)
|
|
23
23
|
def input_tokens
|
|
24
|
-
body.usage
|
|
24
|
+
body.usage&.prompt_tokens || 0
|
|
25
25
|
end
|
|
26
26
|
|
|
27
27
|
##
|
|
28
28
|
# (see LLM::Contract::Completion#output_tokens)
|
|
29
29
|
def output_tokens
|
|
30
|
-
body.usage
|
|
30
|
+
body.usage&.completion_tokens || 0
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
##
|
|
34
|
+
# (see LLM::Contract::Completion#reasoning_tokens)
|
|
35
|
+
def reasoning_tokens
|
|
36
|
+
body
|
|
37
|
+
.usage
|
|
38
|
+
&.completion_tokens_details
|
|
39
|
+
&.reasoning_tokens || 0
|
|
31
40
|
end
|
|
32
41
|
|
|
33
42
|
##
|
|
34
43
|
# (see LLM::Contract::Completion#total_tokens)
|
|
35
44
|
def total_tokens
|
|
36
|
-
body.usage
|
|
45
|
+
body.usage&.total_tokens || 0
|
|
37
46
|
end
|
|
38
47
|
|
|
39
48
|
##
|
|
@@ -71,7 +71,9 @@ class LLM::OpenAI
|
|
|
71
71
|
def merge_tools!(target, tools)
|
|
72
72
|
target["tool_calls"] ||= []
|
|
73
73
|
tools.each.with_index do |toola, index|
|
|
74
|
-
|
|
74
|
+
tindex = toola["index"]
|
|
75
|
+
tindex = index unless Integer === tindex && tindex >= 0
|
|
76
|
+
toolb = target["tool_calls"][tindex]
|
|
75
77
|
if toolb && toola["function"] && toolb["function"]
|
|
76
78
|
# Append to existing function arguments
|
|
77
79
|
toola["function"].each do |func_key, func_value|
|
|
@@ -79,7 +81,7 @@ class LLM::OpenAI
|
|
|
79
81
|
toolb["function"][func_key] << func_value
|
|
80
82
|
end
|
|
81
83
|
else
|
|
82
|
-
target["tool_calls"][
|
|
84
|
+
target["tool_calls"][tindex] = toola
|
|
83
85
|
end
|
|
84
86
|
end
|
|
85
87
|
end
|
data/lib/llm/providers/openai.rb
CHANGED
|
@@ -62,17 +62,9 @@ module LLM
|
|
|
62
62
|
# When given an object a provider does not understand
|
|
63
63
|
# @return (see LLM::Provider#complete)
|
|
64
64
|
def complete(prompt, params = {})
|
|
65
|
-
params
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
role, stream = params.delete(:role), params.delete(:stream)
|
|
69
|
-
params[:stream] = true if stream.respond_to?(:<<) || stream == true
|
|
70
|
-
params[:stream_options] = {include_usage: true}.merge!(params[:stream_options] || {}) if params[:stream]
|
|
71
|
-
req = Net::HTTP::Post.new(completions_path, headers)
|
|
72
|
-
messages = [*(params.delete(:messages) || []), Message.new(role, prompt)]
|
|
73
|
-
body = LLM.json.dump({messages: adapt(messages, mode: :complete).flatten}.merge!(params))
|
|
74
|
-
set_body_stream(req, StringIO.new(body))
|
|
75
|
-
res = execute(request: req, stream:)
|
|
65
|
+
params, stream, tools, role = normalize_complete_params(params)
|
|
66
|
+
req = build_complete_request(prompt, params, role)
|
|
67
|
+
res = execute(request: req, stream: stream)
|
|
76
68
|
ResponseAdapter.adapt(res, type: :completion)
|
|
77
69
|
.extend(Module.new { define_method(:__tools__) { tools } })
|
|
78
70
|
end
|
|
@@ -200,5 +192,25 @@ module LLM
|
|
|
200
192
|
def error_handler
|
|
201
193
|
LLM::OpenAI::ErrorHandler
|
|
202
194
|
end
|
|
195
|
+
|
|
196
|
+
def normalize_complete_params(params)
|
|
197
|
+
params = {role: :user, model: default_model}.merge!(params)
|
|
198
|
+
tools = resolve_tools(params.delete(:tools))
|
|
199
|
+
params = [params, adapt_schema(params), adapt_tools(tools)].inject({}, &:merge!).compact
|
|
200
|
+
role, stream = params.delete(:role), params.delete(:stream)
|
|
201
|
+
params[:stream] = true if stream.respond_to?(:<<) || stream == true
|
|
202
|
+
if params[:stream]
|
|
203
|
+
params[:stream_options] = {include_usage: true}.merge!(params[:stream_options] || {})
|
|
204
|
+
end
|
|
205
|
+
[params, stream, tools, role]
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def build_complete_request(prompt, params, role)
|
|
209
|
+
messages = [*(params.delete(:messages) || []), Message.new(role, prompt)]
|
|
210
|
+
body = LLM.json.dump({messages: adapt(messages, mode: :complete).flatten}.merge!(params))
|
|
211
|
+
req = Net::HTTP::Post.new(completions_path, headers)
|
|
212
|
+
set_body_stream(req, StringIO.new(body))
|
|
213
|
+
req
|
|
214
|
+
end
|
|
203
215
|
end
|
|
204
216
|
end
|
data/lib/llm/response.rb
CHANGED
|
@@ -26,6 +26,8 @@ module LLM
|
|
|
26
26
|
##
|
|
27
27
|
# Returns the response body
|
|
28
28
|
# @return [LLM::Object, String]
|
|
29
|
+
# Returns an LLM::Object when the response body is JSON,
|
|
30
|
+
# otherwise returns a raw string.
|
|
29
31
|
def body
|
|
30
32
|
@res.body
|
|
31
33
|
end
|
|
@@ -54,11 +56,19 @@ module LLM
|
|
|
54
56
|
private
|
|
55
57
|
|
|
56
58
|
def method_missing(m, *args, **kwargs, &b)
|
|
57
|
-
|
|
59
|
+
if LLM::Object === body
|
|
60
|
+
body.respond_to?(m) ? body[m.to_s] : super
|
|
61
|
+
else
|
|
62
|
+
super
|
|
63
|
+
end
|
|
58
64
|
end
|
|
59
65
|
|
|
60
66
|
def respond_to_missing?(m, include_private = false)
|
|
61
|
-
|
|
67
|
+
if LLM::Object === body
|
|
68
|
+
body.respond_to?(m)
|
|
69
|
+
else
|
|
70
|
+
false
|
|
71
|
+
end
|
|
62
72
|
end
|
|
63
73
|
end
|
|
64
74
|
end
|
data/lib/llm/usage.rb
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
# The {LLM::Usage LLM::Usage} class represents token usage for
|
|
5
5
|
# a given conversation or completion. As a conversation grows,
|
|
6
6
|
# so does the number of tokens used. This class helps track
|
|
7
|
-
# the number of input, output, and
|
|
8
|
-
# track usage of the context window (which may
|
|
9
|
-
|
|
7
|
+
# the number of input, output, reasoning and overall token count.
|
|
8
|
+
# It can also help track usage of the context window (which may
|
|
9
|
+
# vary by model).
|
|
10
|
+
class LLM::Usage < Struct.new(:input_tokens, :output_tokens, :reasoning_tokens, :total_tokens, keyword_init: true)
|
|
10
11
|
end
|
data/lib/llm/version.rb
CHANGED
data/lib/llm.rb
CHANGED
|
@@ -41,6 +41,9 @@ module LLM
|
|
|
41
41
|
|
|
42
42
|
##
|
|
43
43
|
# Sets the JSON adapter used by the library
|
|
44
|
+
# @note
|
|
45
|
+
# This should be set once from the main thread when your program starts.
|
|
46
|
+
# Defaults to {LLM::JSONAdapter::JSON LLM::JSONAdapter::JSON}.
|
|
44
47
|
# @param [Class, String, Symbol] adapter
|
|
45
48
|
# A JSON adapter class or its name
|
|
46
49
|
# @return [void]
|