llm.rb 16.0.0.pre.2 → 16.0.0.pre.4
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 +94 -3
- data/docs/deepdive/advanced/context.md +4 -3
- data/docs/deepdive/features/database.md +20 -0
- data/docs/deepdive/features/embeddings.md +2 -0
- data/docs/deepdive/features/files.md +52 -0
- data/docs/deepdive/features/moderations.md +48 -0
- data/docs/deepdive/features/responses.md +54 -0
- data/docs/deepdive/fundamentals/agents.md +8 -5
- data/docs/deepdive/fundamentals/providers.md +37 -0
- data/docs/deepdive/fundamentals/schema.md +50 -0
- data/docs/deepdive/fundamentals/tools.md +6 -0
- data/docs/deepdive/reference/object.md +6 -0
- data/docs/deepdive/reference/response.md +55 -0
- data/docs/deepdive.md +4 -0
- data/lib/llm/active_record/acts_as_llm.rb +42 -7
- data/lib/llm/agent.rb +4 -1
- data/lib/llm/context.rb +1 -0
- data/lib/llm/function/async/task.rb +46 -3
- data/lib/llm/function/fiber/task.rb +17 -3
- data/lib/llm/function/fork/job.rb +32 -6
- data/lib/llm/function/ractor/job.rb +33 -9
- data/lib/llm/function/sequential/group.rb +9 -4
- data/lib/llm/function/task.rb +3 -2
- data/lib/llm/function/thread/task.rb +30 -3
- data/lib/llm/function.rb +156 -7
- data/lib/llm/schema/leaf.rb +4 -2
- data/lib/llm/sequel/plugin.rb +25 -7
- data/lib/llm/step.rb +59 -0
- data/lib/llm/stream.rb +36 -4
- data/lib/llm/version.rb +1 -1
- metadata +6 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a83847d3c33a48980bd9740d7aff397db757ec91224440a97c10240c0ae3a290
|
|
4
|
+
data.tar.gz: 402d6ea4a68fc987e3597d1b6dd84089f2804c7774ef35e38fb8fc424b0e62b0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f75dda6e725c6aa48748a407e744c94c006d3db4973cf45064f7f0252fc2bc818ee7a99b564e66c4bad6ad4066bf5214b912a637b3b25b8b9f335d45d226c7e2
|
|
7
|
+
data.tar.gz: ea3e7fe4a32687c7a34f4b85a0bd4aeee5b7b68387452dd1ac98a3572f1b1f74b4f19ac3ee32b10cf6f53563b9eceb5a8e9292e6001b90728e4997c7b9a301a2
|
data/CHANGELOG.md
CHANGED
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
| Old | New |
|
|
23
23
|
|-----|-----|
|
|
24
24
|
| Ruby 3.3 or later | Ruby 3.4 or later |
|
|
25
|
+
| `record.messages` returns the messages the runtime holds | a `:jsonb` record returns a relation of `LLM::ActiveRecord::Message` rows |
|
|
25
26
|
|
|
26
27
|
* **drop Ruby 3.3 support** <br>
|
|
27
28
|
The gem now requires Ruby 3.4 or later: `required_ruby_version` is
|
|
@@ -30,6 +31,31 @@
|
|
|
30
31
|
cancel that arrives before the tool starts is not delivered there, while
|
|
31
32
|
Ruby 3.4 and 4.0 both deliver it.
|
|
32
33
|
|
|
34
|
+
* **activerecord: read a `:jsonb` record's messages from the column** <br>
|
|
35
|
+
[`LLM::ActiveRecord`](https://r.uby.dev/api-docs/llm.rb/LLM/ActiveRecord.html)
|
|
36
|
+
now answers `#messages` for a record whose `format` is `:jsonb` with an
|
|
37
|
+
[`ActiveRecord::Relation`](https://api.rubyonrails.org/classes/ActiveRecord/Relation.html)
|
|
38
|
+
over the column, through
|
|
39
|
+
[`LLM::ActiveRecord::Message`](https://r.uby.dev/api-docs/llm.rb/LLM/ActiveRecord/Message.html),
|
|
40
|
+
so a conversation can be filtered, counted, and ordered in SQL, and reading it
|
|
41
|
+
needs no provider and no credentials. Before this, every record loaded the
|
|
42
|
+
runtime and returned the messages it held, whatever the format. The relation
|
|
43
|
+
is not a drop-in replacement for that buffer: it reads what is persisted
|
|
44
|
+
rather than what a runtime holds in memory, it is unordered, so order by
|
|
45
|
+
`position`, and its rows answer `unwrap!` rather than being
|
|
46
|
+
[`LLM::Message`](https://r.uby.dev/api-docs/llm.rb/LLM/Message.html) objects.
|
|
47
|
+
Every other format, and Sequel, still loads the runtime.
|
|
48
|
+
|
|
49
|
+
### ActiveRecord
|
|
50
|
+
|
|
51
|
+
* **activerecord: add `messages!` for the runtime's own messages** <br>
|
|
52
|
+
[`LLM::ActiveRecord#messages!`](https://r.uby.dev/api-docs/llm.rb/LLM/ActiveRecord.html#messages!-instance_method)
|
|
53
|
+
returns the messages the runtime holds whatever the storage format is, so a
|
|
54
|
+
jsonb record can still reach them now that its `#messages` answers with the
|
|
55
|
+
view over the column. It reads what the context holds, including state that
|
|
56
|
+
has not been saved, where `#messages` reads the column. The Sequel plugin
|
|
57
|
+
answers to `#messages!` too, where it is the same call as `#messages`.
|
|
58
|
+
|
|
33
59
|
### Console
|
|
34
60
|
|
|
35
61
|
* **console: erase with the backspace key on OpenBSD** <br>
|
|
@@ -39,6 +65,13 @@
|
|
|
39
65
|
terminal's terminfo describes the key. All three erase. Before this only 127
|
|
40
66
|
did, so the backspace key did nothing on the OpenBSD console.
|
|
41
67
|
|
|
68
|
+
### Fix
|
|
69
|
+
|
|
70
|
+
* **fork: require xchan.rb `~> 0.24`** <br>
|
|
71
|
+
The `:fork` concurrency strategy now requires the `xchan.rb` gem at `~> 0.24`
|
|
72
|
+
instead of `~> 0.23`, and the gemspec's development dependency follows. An
|
|
73
|
+
application that uses `:fork` needs `xchan.rb` 0.24 or later.
|
|
74
|
+
|
|
42
75
|
### Function
|
|
43
76
|
|
|
44
77
|
* **function: deliver an interrupt to the tool, not to whatever is running** <br>
|
|
@@ -77,6 +110,38 @@
|
|
|
77
110
|
side effects happening and its result written to a queue nobody read, and the
|
|
78
111
|
reactor thread stayed alive until a group's `wait` stopped it.
|
|
79
112
|
|
|
113
|
+
* **function: run the interrupt hook on every strategy** <br>
|
|
114
|
+
A tool that implements
|
|
115
|
+
[`LLM::Tool#on_interrupt`](https://r.uby.dev/api-docs/llm.rb/LLM/Tool.html#on_interrupt-instance_method)
|
|
116
|
+
or
|
|
117
|
+
[`LLM::Tool#on_cancel`](https://r.uby.dev/api-docs/llm.rb/LLM/Tool.html#on_cancel-instance_method)
|
|
118
|
+
is now told on every concurrency strategy. `:thread`, `:fiber`, and `:async`
|
|
119
|
+
run the hook on the thread or fiber that ran the call, `:fork` and `:ractor`
|
|
120
|
+
run it inside the child or the ractor before the interrupt is delivered, and
|
|
121
|
+
a sequential group's cancel tells every task it holds. Before this, `:async`,
|
|
122
|
+
`:fork`, and `:ractor` never ran the hook, the others ran it on the thread
|
|
123
|
+
that cancelled, and a class-backed tool ran it on no strategy at all, because
|
|
124
|
+
`interrupt!` read the hook off the tool's definition and a class answers
|
|
125
|
+
`respond_to?` with false for an instance method. A hook that raises becomes
|
|
126
|
+
what the caller sees in place of the call's result.
|
|
127
|
+
|
|
128
|
+
### ORM
|
|
129
|
+
|
|
130
|
+
* **orm: save a conversation after each request, not once per turn** <br>
|
|
131
|
+
[`LLM::Step`](https://r.uby.dev/api-docs/llm.rb/LLM/Step.html) is prepended
|
|
132
|
+
onto every stream by
|
|
133
|
+
[`LLM::Stream.try`](https://r.uby.dev/api-docs/llm.rb/LLM/Stream.html#try-class_method),
|
|
134
|
+
so a conversation bound to a record is written down as it goes and a turn
|
|
135
|
+
that is interrupted can be continued rather than started over. It saves
|
|
136
|
+
through
|
|
137
|
+
[`LLM::Context#record`](https://r.uby.dev/api-docs/llm.rb/LLM/Context.html#record-instance_method),
|
|
138
|
+
an ActiveRecord model through `LLM::ActiveRecord::Utils.save!` and a Sequel
|
|
139
|
+
model through `LLM::Sequel::Plugin::Utils.save!`, and the write is best
|
|
140
|
+
effort, so a failure is swallowed rather than raised inside the request that
|
|
141
|
+
has just completed. Before this, `talk` and `ask` on both wrappers saved
|
|
142
|
+
once, when the whole turn had finished, so an interrupted turn persisted
|
|
143
|
+
nothing.
|
|
144
|
+
|
|
80
145
|
### Provider
|
|
81
146
|
|
|
82
147
|
* **deepseek: support image attachments in chat completions** <br>
|
|
@@ -97,13 +162,39 @@
|
|
|
97
162
|
`qwen3.5-flash`, `qwen3.7-flash`, and `qwen3.8-omni-flash`, and gives
|
|
98
163
|
`qwen3.7-plus` attachments, structured output, and a lower price; OpenAI adds
|
|
99
164
|
`gpt-daybreak-blue-latest` and `gpt-daybreak-red-latest`; Bedrock adds
|
|
100
|
-
`openai.gpt-6-sol
|
|
101
|
-
`tencent/Hy4-preview`
|
|
165
|
+
`openai.gpt-6-sol`, `openai.gpt-6-luna`, and
|
|
166
|
+
`global.anthropic.claude-sonnet-5-5`; and DeepInfra adds `tencent/Hy4-preview`
|
|
167
|
+
and the Xiaomi MiMo V2.6 Pro and V2.6 Flash models.
|
|
102
168
|
Bedrock also corrects the context and output limits of fifteen models, Google
|
|
103
169
|
lowers the limits of `gemini-2.5-computer-use-preview-10-2025` and
|
|
104
|
-
`gemini-3-pro-image`, and OpenRouter adds
|
|
170
|
+
`gemini-3-pro-image`, and OpenRouter adds five models, drops six, and reprices
|
|
105
171
|
many of the DeepSeek, Z.ai, and Qwen entries.
|
|
106
172
|
|
|
173
|
+
### Schema
|
|
174
|
+
|
|
175
|
+
* **schema: apply every `parameter` option to the leaf** <br>
|
|
176
|
+
[`LLM::Tool`](https://r.uby.dev/api-docs/llm.rb/LLM/Tool.html) now applies each
|
|
177
|
+
option given to `parameter` to the leaf its type resolves to, so settings such
|
|
178
|
+
as `min:`, `max:`, `multiple_of:`, and `const:` reach the schema instead of
|
|
179
|
+
being dropped. Before this only `required`, `default`, and `enum` were read, so
|
|
180
|
+
`parameter :age, Integer, "Age", min: 1, max: 9` sent a plain integer and lost
|
|
181
|
+
the range. A `default:` of `false` survives now as well, where the old code
|
|
182
|
+
discarded a false default, and `required: false` marks the parameter optional
|
|
183
|
+
rather than being ignored.
|
|
184
|
+
|
|
185
|
+
### Stream
|
|
186
|
+
|
|
187
|
+
* **stream: add `on_step`, called when a request completes** <br>
|
|
188
|
+
[`LLM::Stream#on_step`](https://r.uby.dev/api-docs/llm.rb/LLM/Stream.html#on_step-instance_method)
|
|
189
|
+
is a new callback, emitted from
|
|
190
|
+
[`LLM::Context#talk`](https://r.uby.dev/api-docs/llm.rb/LLM/Context.html#talk-instance_method)
|
|
191
|
+
once the prompt and the response are in the conversation. It marks the
|
|
192
|
+
boundary between one request and the next, which is the point a stream can
|
|
193
|
+
checkpoint the conversation at, because a provider will accept it again
|
|
194
|
+
there. It fires once per successful request, so a request retried after a
|
|
195
|
+
rate limit or a timeout calls it when it lands and not once per attempt, and
|
|
196
|
+
it fires even when the stream is disabled.
|
|
197
|
+
|
|
107
198
|
## v15.5.0
|
|
108
199
|
|
|
109
200
|
Changes since `v15.4.1`.
|
|
@@ -81,9 +81,10 @@ Pass `retry_budget:` to retry a request that was rate limited
|
|
|
81
81
|
Each retry sleeps a growing interval (2s, 4s, 6s, ...) and notifies
|
|
82
82
|
the stream through
|
|
83
83
|
[`LLM::Stream#on_retry`](https://r.uby.dev/api-docs/llm.rb/LLM/Stream.html#on_retry-instance_method)
|
|
84
|
-
before trying again.
|
|
85
|
-
|
|
86
|
-
|
|
84
|
+
before trying again. The budget lives on the provider, where it
|
|
85
|
+
defaults to 5 (8 on Alibaba, which rate limits more often), and an
|
|
86
|
+
agent that sets none of its own takes the provider's, so most users
|
|
87
|
+
never touch this directly.
|
|
87
88
|
|
|
88
89
|
### Identity
|
|
89
90
|
|
|
@@ -407,6 +407,26 @@ you write across every agent is not covered by default; index the state
|
|
|
407
407
|
column for those, for example with a GIN index on
|
|
408
408
|
`data jsonb_path_ops`.
|
|
409
409
|
|
|
410
|
+
A record whose `format` is `:jsonb` also returns this view from
|
|
411
|
+
`#messages`, so a conversation can be read without building a runtime:
|
|
412
|
+
no provider, and no credentials. What it returns differs from the
|
|
413
|
+
runtime's own `#messages` in three ways. It reads what is persisted
|
|
414
|
+
rather than the state a context holds in memory and has not saved. It
|
|
415
|
+
is unordered, and conversation order is `position` rather than `id`, so
|
|
416
|
+
order it explicitly. And its rows are
|
|
417
|
+
[`LLM::ActiveRecord::Message`](https://r.uby.dev/api-docs/llm.rb/LLM/ActiveRecord/Message.html)
|
|
418
|
+
records rather than
|
|
419
|
+
[`LLM::Message`](https://r.uby.dev/api-docs/llm.rb/LLM/Message.html)
|
|
420
|
+
objects, until
|
|
421
|
+
[`#unwrap!`](https://r.uby.dev/api-docs/llm.rb/LLM/ActiveRecord/Message.html#unwrap!-instance_method)
|
|
422
|
+
turns them back. `#messages!` is the runtime's own list, whatever the
|
|
423
|
+
format is, so a jsonb record reaches it in one call.
|
|
424
|
+
|
|
425
|
+
Every other format answers `#messages` with the runtime's messages, as
|
|
426
|
+
it always did, and that is the default. Sequel has no equivalent to any
|
|
427
|
+
of this: its plugin persists the same state, and there is no view to
|
|
428
|
+
read it back in the database.
|
|
429
|
+
|
|
410
430
|
### Sequel
|
|
411
431
|
|
|
412
432
|
#### Overview
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
|
|
2
|
+
## Files
|
|
3
|
+
|
|
4
|
+
### Introduction
|
|
5
|
+
|
|
6
|
+
#### Overview
|
|
7
|
+
|
|
8
|
+
Some providers keep files you upload, and let a later request refer to
|
|
9
|
+
one by id instead of resending its bytes. The Files API is how a file
|
|
10
|
+
is uploaded, listed, fetched, downloaded, and deleted, and a file id is
|
|
11
|
+
then passed to an endpoint that reads it.
|
|
12
|
+
|
|
13
|
+
#### How it works
|
|
14
|
+
|
|
15
|
+
Call the file methods on a provider that implements them. `create`
|
|
16
|
+
uploads a file and returns its record, `get` reads a record, `download`
|
|
17
|
+
returns the bytes, `all` lists the files, and `delete` removes one:
|
|
18
|
+
|
|
19
|
+
```ruby
|
|
20
|
+
require "llm"
|
|
21
|
+
|
|
22
|
+
llm = LLM.openai(key: ENV["KEY"])
|
|
23
|
+
ctx = LLM::Context.new(llm)
|
|
24
|
+
|
|
25
|
+
file = llm.files.create(file: "/books/goodread.pdf")
|
|
26
|
+
ctx.talk ["Tell me about this PDF", file]
|
|
27
|
+
|
|
28
|
+
llm.files.all # every uploaded file
|
|
29
|
+
llm.files.get(file: file.id) # one file's record
|
|
30
|
+
llm.files.download(file: file.id) # its bytes, under #file
|
|
31
|
+
llm.files.delete(file: file.id)
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
`create` takes a path, an `LLM::File`, or a `File` object, and a
|
|
35
|
+
`purpose:` that tells the provider what the file is for. An uploaded
|
|
36
|
+
file carries its id, so it can be passed to a turn directly, as above,
|
|
37
|
+
and a response from `download` exposes the bytes through `#file`.
|
|
38
|
+
|
|
39
|
+
#### Why would I use it?
|
|
40
|
+
|
|
41
|
+
A file that a provider stores can be reused across requests without
|
|
42
|
+
being uploaded each time. That keeps a large document or an image out
|
|
43
|
+
of the request body, and it is how OpenAI's vector stores, and the
|
|
44
|
+
assistants that read them, get their content.
|
|
45
|
+
|
|
46
|
+
#### Notes
|
|
47
|
+
|
|
48
|
+
OpenAI, Anthropic, and Google implement the Files API; a provider that
|
|
49
|
+
does not raises `NotImplementedError`. `download` is OpenAI-only. A
|
|
50
|
+
file id belongs to the provider that minted it: DeepSeek, for example,
|
|
51
|
+
has no Files API and raises `LLM::PromptError` when it is handed a file
|
|
52
|
+
id from another provider.
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
|
|
2
|
+
## Moderations
|
|
3
|
+
|
|
4
|
+
### Introduction
|
|
5
|
+
|
|
6
|
+
#### Overview
|
|
7
|
+
|
|
8
|
+
A moderation classifies text or an image against a set of harm
|
|
9
|
+
categories, so an application can decide what to do with it before
|
|
10
|
+
showing it, storing it, or sending it to a model. OpenAI hosts the
|
|
11
|
+
moderation endpoint.
|
|
12
|
+
|
|
13
|
+
#### How it works
|
|
14
|
+
|
|
15
|
+
Call `llm.moderations` with the content to check. The response lists a
|
|
16
|
+
moderation per input, and each one reports whether it was flagged, the
|
|
17
|
+
categories it matched, and their scores:
|
|
18
|
+
|
|
19
|
+
```ruby
|
|
20
|
+
require "llm"
|
|
21
|
+
|
|
22
|
+
llm = LLM.openai(key: ENV["KEY"])
|
|
23
|
+
|
|
24
|
+
res = llm.moderations.create(input: "I hate you")
|
|
25
|
+
mod = res.moderations[0]
|
|
26
|
+
|
|
27
|
+
mod.flagged? # => true
|
|
28
|
+
mod.categories # the categories that matched
|
|
29
|
+
mod.scores # a score per category
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
`input` accepts a string or a URL, so an image URL is classified the
|
|
33
|
+
same way text is.
|
|
34
|
+
|
|
35
|
+
#### Why would I use it?
|
|
36
|
+
|
|
37
|
+
A moderation is a cheap check that runs before an expensive or public
|
|
38
|
+
step. Screen user-generated content before publishing it, gate a
|
|
39
|
+
request before it reaches a model, or keep a record of what was
|
|
40
|
+
filtered and why.
|
|
41
|
+
|
|
42
|
+
#### Notes
|
|
43
|
+
|
|
44
|
+
Only OpenAI implements moderations; another provider raises
|
|
45
|
+
`NotImplementedError`. The default model is `omni-moderation-latest`,
|
|
46
|
+
and it classifies both text and images. The scores are model
|
|
47
|
+
estimates, so a threshold is a product decision rather than a fixed
|
|
48
|
+
value.
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
|
|
2
|
+
## Responses
|
|
3
|
+
|
|
4
|
+
### Introduction
|
|
5
|
+
|
|
6
|
+
#### Overview
|
|
7
|
+
|
|
8
|
+
OpenAI's Responses API gives every reply an id, and a later call can
|
|
9
|
+
continue from one by passing `previous_response_id:`. When the provider
|
|
10
|
+
is asked to store the reply, it keeps the history, so a client can hand
|
|
11
|
+
the thread back instead of resending it.
|
|
12
|
+
|
|
13
|
+
#### How it works
|
|
14
|
+
|
|
15
|
+
Create a response with `llm.responses.create`, then continue it by
|
|
16
|
+
passing `previous_response_id:` the id of the last one. A response can
|
|
17
|
+
be read back with `get` and removed with `delete`:
|
|
18
|
+
|
|
19
|
+
```ruby
|
|
20
|
+
require "llm"
|
|
21
|
+
|
|
22
|
+
llm = LLM.openai(key: ENV["KEY"])
|
|
23
|
+
|
|
24
|
+
first = llm.responses.create "Your task is to answer the user's questions",
|
|
25
|
+
role: :developer
|
|
26
|
+
second = llm.responses.create "5 + 5 = X ?",
|
|
27
|
+
role: :user,
|
|
28
|
+
previous_response_id: first.id
|
|
29
|
+
|
|
30
|
+
puts second.content
|
|
31
|
+
|
|
32
|
+
llm.responses.get(second) # read it back
|
|
33
|
+
llm.responses.delete(second) # remove it
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
`create` accepts the same prompt shapes as a completion (a string or a
|
|
37
|
+
list), plus `role:`, `model:`, `tools:`, and a `stream:` target. It
|
|
38
|
+
returns an `LLM::Response` like any other call.
|
|
39
|
+
|
|
40
|
+
#### Why would I use it?
|
|
41
|
+
|
|
42
|
+
Server-side state means the conversation lives with the provider
|
|
43
|
+
instead of in the request. Passing one id continues a thread without
|
|
44
|
+
rebuilding and resending the messages, which keeps requests small and
|
|
45
|
+
lets a client that lost its local history pick the thread back up.
|
|
46
|
+
|
|
47
|
+
#### Notes
|
|
48
|
+
|
|
49
|
+
Only OpenAI implements the Responses API. A context in `mode:
|
|
50
|
+
:responses` uses this endpoint and defaults to `store: false`, so the
|
|
51
|
+
conversation is not kept on the provider and the context rebuilds the
|
|
52
|
+
input itself; set `store: true` to keep it there and have the next turn
|
|
53
|
+
continue with `previous_response_id`. A plain `llm.responses.create`
|
|
54
|
+
call continues only when `previous_response_id:` is given.
|
|
@@ -245,9 +245,12 @@ of hanging.
|
|
|
245
245
|
|
|
246
246
|
The retry budget applies to rate-limited requests
|
|
247
247
|
(`LLM::RateLimitError`) and timeouts (`Timeout::Error`, covering
|
|
248
|
-
`Net::OpenTimeout` and `Net::ReadTimeout`)
|
|
249
|
-
retried. The budget
|
|
248
|
+
`Net::OpenTimeout` and `Net::ReadTimeout`); other errors are never
|
|
249
|
+
retried. The budget lives on the provider, where it defaults to five
|
|
250
|
+
(eight on Alibaba, which rate limits more often), and an agent that
|
|
251
|
+
sets none of its own takes the provider's. A raw
|
|
250
252
|
[`LLM::Context`](https://r.uby.dev/api-docs/llm.rb/LLM/Context.html)
|
|
251
|
-
defaults to zero (`retry_budget: 0`)
|
|
252
|
-
content streams, so retrying the same
|
|
253
|
-
`retry_budget: 0` to disable retries on
|
|
253
|
+
defaults to zero (`retry_budget: 0`), so retries are opt-in there. A
|
|
254
|
+
429 is refused before any content streams, so retrying the same
|
|
255
|
+
request loses nothing. Pass `retry_budget: 0` to disable retries on
|
|
256
|
+
an agent.
|
|
@@ -92,6 +92,43 @@ missing model or registry raises `LLM::NoSuchModelError` or
|
|
|
92
92
|
`LLM::NoSuchRegistryError`, which the runtime rescues to default
|
|
93
93
|
gracefully (for example, an unknown context window reads as `nil`).
|
|
94
94
|
|
|
95
|
+
### Model listings
|
|
96
|
+
|
|
97
|
+
#### Overview
|
|
98
|
+
|
|
99
|
+
The registry is the catalog llm.rb ships. A provider can also list the
|
|
100
|
+
models it serves right now, which shows a model added since your copy
|
|
101
|
+
of `data/` was written, or one available to your account alone.
|
|
102
|
+
|
|
103
|
+
#### How it works
|
|
104
|
+
|
|
105
|
+
A provider with a models endpoint exposes it through `llm.models`, and
|
|
106
|
+
`all` returns the list:
|
|
107
|
+
|
|
108
|
+
```ruby
|
|
109
|
+
require "llm"
|
|
110
|
+
|
|
111
|
+
llm = LLM.openai(key: ENV["KEY"])
|
|
112
|
+
llm.models.all.each { puts _1.id }
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
OpenAI, Anthropic, Google, Bedrock, and Ollama implement it. Bedrock
|
|
116
|
+
lists the foundation models in the configured region and accepts
|
|
117
|
+
`byProvider:` to narrow the list; Ollama lists the models pulled into
|
|
118
|
+
the local daemon.
|
|
119
|
+
|
|
120
|
+
#### Why would I use it?
|
|
121
|
+
|
|
122
|
+
A shipped catalog goes stale between releases. Asking the provider
|
|
123
|
+
keeps a model picker current and reveals models the catalog does not
|
|
124
|
+
know about.
|
|
125
|
+
|
|
126
|
+
#### Notes
|
|
127
|
+
|
|
128
|
+
Not every provider implements a models endpoint; calling `models` on
|
|
129
|
+
one that does not raises `NotImplementedError`. Unlike the registry,
|
|
130
|
+
the call needs credentials.
|
|
131
|
+
|
|
95
132
|
### Request headers
|
|
96
133
|
|
|
97
134
|
#### Overview
|
|
@@ -178,3 +178,53 @@ DSL builds, so the result is used the same way.
|
|
|
178
178
|
An unsupported `type`, an unresolvable `$ref`, or a `$ref` that does
|
|
179
179
|
not start with `#/` raises `TypeError`. When a schema omits `type`,
|
|
180
180
|
the parser infers it from `const`, `enum`, or `default`.
|
|
181
|
+
|
|
182
|
+
### Deferred types
|
|
183
|
+
|
|
184
|
+
#### Overview
|
|
185
|
+
|
|
186
|
+
A property or parameter type can be given as a block, in which case the
|
|
187
|
+
type is worked out when the schema is serialized rather than when the
|
|
188
|
+
class that declares it is defined. That is how a type that only exists
|
|
189
|
+
at runtime, such as the values a store holds right now, is described.
|
|
190
|
+
|
|
191
|
+
#### How it works
|
|
192
|
+
|
|
193
|
+
Pass a block in place of the type. It is called each time the schema is
|
|
194
|
+
serialized, and whatever it returns is resolved in turn, so it can
|
|
195
|
+
return a leaf, a class, or an array of either:
|
|
196
|
+
|
|
197
|
+
```ruby
|
|
198
|
+
class Article < LLM::Schema
|
|
199
|
+
property :kind, proc { LLM::Schema::Enum[*Article.kinds] }, "The article kind"
|
|
200
|
+
required %i[kind]
|
|
201
|
+
end
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
The same works for a tool parameter:
|
|
205
|
+
|
|
206
|
+
```ruby
|
|
207
|
+
class Publish < LLM::Tool
|
|
208
|
+
name "publish"
|
|
209
|
+
parameter :kind, proc { LLM::Schema::Enum[*Article.kinds] }, "The article kind"
|
|
210
|
+
required %i[kind]
|
|
211
|
+
|
|
212
|
+
def call(kind:)
|
|
213
|
+
Article.publish(kind:)
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
#### Why would I use it?
|
|
219
|
+
|
|
220
|
+
A schema is a class-level definition, but the choices it offers are
|
|
221
|
+
sometimes only known when a request is made. A deferred type lets the
|
|
222
|
+
schema be declared once and still send the values that are current at
|
|
223
|
+
the time of the request.
|
|
224
|
+
|
|
225
|
+
#### Notes
|
|
226
|
+
|
|
227
|
+
The block is called on every serialization, so it runs once per
|
|
228
|
+
request and sees the latest values. Everything a leaf accepts, such as
|
|
229
|
+
`required`, `default`, and `enum`, is applied to the resolved leaf
|
|
230
|
+
after the block returns.
|
|
@@ -435,6 +435,12 @@ needs most. See the
|
|
|
435
435
|
The tools that spawn subprocesses use the optional `test-cmd.rb`
|
|
436
436
|
gem for process management and interrupt handling.
|
|
437
437
|
|
|
438
|
+
`LLM::Tool.subclasses` lists the direct subclasses of
|
|
439
|
+
[`LLM::Tool`](https://r.uby.dev/api-docs/llm.rb/LLM/Tool.html), so a
|
|
440
|
+
tool defined under an intermediate base class is not in the list.
|
|
441
|
+
[`LLM::Tool.registry`](https://r.uby.dev/api-docs/llm.rb/LLM/Tool.html#registry-class_method)
|
|
442
|
+
lists every tool that has a name, wherever it sits in the hierarchy.
|
|
443
|
+
|
|
438
444
|
### Interrupts
|
|
439
445
|
|
|
440
446
|
#### Overview
|
|
@@ -61,6 +61,12 @@ is defined explicitly and serializes through the configured JSON
|
|
|
61
61
|
adapter via
|
|
62
62
|
[`LLM.json.dump`](https://r.uby.dev/api-docs/llm.rb/LLM.html#json-class_method).
|
|
63
63
|
|
|
64
|
+
The adapter is `LLM::JSONAdapter::JSON`, the standard library, by
|
|
65
|
+
default. Set `LLM.json = :oj` or `LLM.json = :yajl` to use a faster
|
|
66
|
+
backend when the matching gem is present; every adapter normalizes
|
|
67
|
+
strings to valid UTF-8 before it serializes, so the choice affects
|
|
68
|
+
speed rather than behavior.
|
|
69
|
+
|
|
64
70
|
### Reading and writing
|
|
65
71
|
|
|
66
72
|
#### Overview
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
|
|
2
|
+
## LLM::Response
|
|
3
|
+
|
|
4
|
+
### Introduction
|
|
5
|
+
|
|
6
|
+
#### Overview
|
|
7
|
+
|
|
8
|
+
Every call returns an
|
|
9
|
+
[`LLM::Response`](https://r.uby.dev/api-docs/llm.rb/LLM/Response.html).
|
|
10
|
+
It is one normalized shape that a provider, an endpoint, or a context
|
|
11
|
+
extends with the accessors that response needs, so chat completions,
|
|
12
|
+
embeddings, images, and OCR all share a common surface without losing
|
|
13
|
+
their own.
|
|
14
|
+
|
|
15
|
+
#### How it works
|
|
16
|
+
|
|
17
|
+
`LLM::Response` carries the transport response and a little common
|
|
18
|
+
state: `#res` (the underlying HTTP response), `#body` (the parsed body,
|
|
19
|
+
or a raw string), `#ok?`, `#id`, and `#file?`. A name the base class
|
|
20
|
+
does not define falls through to the body, so a provider field such as
|
|
21
|
+
`content` is reachable directly.
|
|
22
|
+
|
|
23
|
+
A completion response also includes
|
|
24
|
+
[`LLM::Contract::Completion`](https://r.uby.dev/api-docs/llm.rb/LLM/Contract/Completion.html),
|
|
25
|
+
which adds the pieces a chat reply needs:
|
|
26
|
+
|
|
27
|
+
```ruby
|
|
28
|
+
res = agent.talk "Weather in Paris?", schema: Weather
|
|
29
|
+
|
|
30
|
+
res.content # the raw text, or the structured reply
|
|
31
|
+
res.content! # the reply parsed as an LLM::Object
|
|
32
|
+
res.messages # the messages this turn produced
|
|
33
|
+
res.usage # an LLM::Usage (input, output, reasoning, cache, ...)
|
|
34
|
+
res.reasoning_content # the model's reasoning, when the provider exposes it
|
|
35
|
+
res.model # the model that answered
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
#### Why would I use it?
|
|
39
|
+
|
|
40
|
+
One shape means one set of methods to learn: the same `content!`,
|
|
41
|
+
`usage`, and `messages` work whatever the provider. When an endpoint
|
|
42
|
+
returns something shaped differently, it adds an accessor rather than
|
|
43
|
+
changing the contract.
|
|
44
|
+
|
|
45
|
+
#### Notes
|
|
46
|
+
|
|
47
|
+
`#usage` returns an
|
|
48
|
+
[`LLM::Usage`](https://r.uby.dev/api-docs/llm.rb/LLM/Usage.html), so
|
|
49
|
+
token counts are read the same way everywhere; the individual token
|
|
50
|
+
readers (`input_tokens`, `output_tokens`, `cache_read_tokens`, and so
|
|
51
|
+
on) live on the completion contract. Endpoint-specific accessors such
|
|
52
|
+
as `images`, `pages`, `audio`, or `embeddings` come from the adapter
|
|
53
|
+
that extends the response, so they are present only on responses that
|
|
54
|
+
carry that data. `#content!` parses the content as JSON, which is the
|
|
55
|
+
read to use with a schema.
|
data/docs/deepdive.md
CHANGED
|
@@ -68,6 +68,9 @@ is the best place to start if you are new to llm.rb.
|
|
|
68
68
|
- [Built-in tools](deepdive/features/builtin_tools.md)
|
|
69
69
|
- [Concurrency](deepdive/features/concurrency.md)
|
|
70
70
|
- [Embeddings](deepdive/features/embeddings.md)
|
|
71
|
+
- [Files](deepdive/features/files.md)
|
|
72
|
+
- [Moderations](deepdive/features/moderations.md)
|
|
73
|
+
- [Responses](deepdive/features/responses.md)
|
|
71
74
|
- [Database](deepdive/features/database.md)
|
|
72
75
|
- [Console](deepdive/features/console.md)
|
|
73
76
|
|
|
@@ -88,6 +91,7 @@ is the best place to start if you are new to llm.rb.
|
|
|
88
91
|
|
|
89
92
|
## Reference
|
|
90
93
|
|
|
94
|
+
- [LLM::Response](deepdive/reference/response.md)
|
|
91
95
|
- [LLM::Object](deepdive/reference/object.md)
|
|
92
96
|
- [LLM::Cost](deepdive/reference/cost.md)
|
|
93
97
|
- [Tracer](deepdive/reference/tracer.md)
|
|
@@ -56,21 +56,23 @@ module LLM::ActiveRecord
|
|
|
56
56
|
|
|
57
57
|
module InstanceMethods
|
|
58
58
|
##
|
|
59
|
-
# Continues the stored context with new input
|
|
59
|
+
# Continues the stored context with new input.
|
|
60
|
+
#
|
|
61
|
+
# The conversation is saved by {LLM::Step LLM::Step} at each request.
|
|
60
62
|
# @see LLM::Context#talk
|
|
61
63
|
# @return [LLM::Response]
|
|
62
64
|
def talk(...)
|
|
63
|
-
|
|
64
|
-
ctx.talk(...).tap { Utils.save!(self, ctx, options) }
|
|
65
|
+
ctx.talk(...)
|
|
65
66
|
end
|
|
66
67
|
|
|
67
68
|
##
|
|
68
|
-
# Continues the stored context with new input
|
|
69
|
+
# Continues the stored context with new input.
|
|
70
|
+
#
|
|
71
|
+
# The conversation is saved by {LLM::Step LLM::Step} at each request.
|
|
69
72
|
# @see LLM::Context#ask
|
|
70
73
|
# @return [LLM::Response]
|
|
71
74
|
def ask(...)
|
|
72
|
-
|
|
73
|
-
ctx.ask(...).tap { Utils.save!(self, ctx, options) }
|
|
75
|
+
ctx.ask(...)
|
|
74
76
|
end
|
|
75
77
|
|
|
76
78
|
##
|
|
@@ -89,9 +91,42 @@ module LLM::ActiveRecord
|
|
|
89
91
|
end
|
|
90
92
|
|
|
91
93
|
##
|
|
94
|
+
# Returns the messages this record holds.
|
|
95
|
+
#
|
|
96
|
+
# A record that stores its state as jsonb reads them from the column,
|
|
97
|
+
# as a relation: the conversation can be filtered, counted and ordered
|
|
98
|
+
# in the database, and nothing here needs a provider to be built. Every
|
|
99
|
+
# other format loads the runtime and hands back the messages it holds.
|
|
100
|
+
#
|
|
101
|
+
# The two do not agree about what is visible. The relation reads what is
|
|
102
|
+
# persisted rather than what the runtime holds in memory, it is
|
|
103
|
+
# unordered - conversation order is `position`, not `id` - and its rows
|
|
104
|
+
# are {LLM::ActiveRecord::Message} records until
|
|
105
|
+
# {LLM::ActiveRecord::Message#unwrap!} turns them back into
|
|
106
|
+
# {LLM::Message} objects. {#messages!} is the runtime's own collection,
|
|
107
|
+
# for a caller who wants that instead.
|
|
92
108
|
# @see LLM::Context#messages
|
|
93
|
-
# @return [
|
|
109
|
+
# @return [ActiveRecord::Relation, LLM::Buffer]
|
|
94
110
|
def messages
|
|
111
|
+
options = self.class.llm_plugin_options
|
|
112
|
+
if options[:format] == :jsonb
|
|
113
|
+
LLM::ActiveRecord::Message.for(agent: self)
|
|
114
|
+
else
|
|
115
|
+
ctx.messages
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
##
|
|
120
|
+
# Returns the messages the runtime holds, whatever the storage format is.
|
|
121
|
+
#
|
|
122
|
+
# This is what {#messages} answers with for every format but jsonb, and
|
|
123
|
+
# it is the way back to it for a jsonb record. The difference between
|
|
124
|
+
# the two is what each reads: this reads what the context holds,
|
|
125
|
+
# including state that has not been saved, where {#messages} reads the
|
|
126
|
+
# column.
|
|
127
|
+
# @see LLM::Context#messages
|
|
128
|
+
# @return [LLM::Buffer]
|
|
129
|
+
def messages!
|
|
95
130
|
ctx.messages
|
|
96
131
|
end
|
|
97
132
|
|
data/lib/llm/agent.rb
CHANGED
|
@@ -441,7 +441,10 @@ module LLM
|
|
|
441
441
|
# knows its own API, and how often it recovers from a
|
|
442
442
|
# rate limit rather than failing outright.
|
|
443
443
|
params[:retry_budget] = llm.retry_budget if params[:retry_budget].equal?(UNDEFINED)
|
|
444
|
-
|
|
444
|
+
##
|
|
445
|
+
# The context is bound to the same record the agent holds, so code
|
|
446
|
+
# written against a context alone can find it through `ctx.record`.
|
|
447
|
+
@ctx = LLM::Context.new(llm, params.merge(record: @record).compact)
|
|
445
448
|
@path and File.readable?(@path) ? @ctx.restore(path:) : nil
|
|
446
449
|
end
|
|
447
450
|
|