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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 61578ce1082ecd25f86574ef19e1ffd4cc2b68c6689b705f0544f4aa3e391ede
4
- data.tar.gz: 742b8d66f49113389c5ff08772e79498334eb6d5afe9821f05c2b8d1851cf4e8
3
+ metadata.gz: a83847d3c33a48980bd9740d7aff397db757ec91224440a97c10240c0ae3a290
4
+ data.tar.gz: 402d6ea4a68fc987e3597d1b6dd84089f2804c7774ef35e38fb8fc424b0e62b0
5
5
  SHA512:
6
- metadata.gz: d0024edabc9ac7360a71a485362aed854644187c116777623b340e5fbd391ae257139aefe07ba29ce7fe9bf04573e66ff09c0f48024153833cbb28b385e13280
7
- data.tar.gz: ec0b4f0b2ba6a66b38cd324c5c3ea6daccd4ff76cde43a1fa8c272c20da89bec735ff7b7bae3afb516d4b5a3131b4e383abe70f48365d6ba9c8a61028e233c48
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` and `openai.gpt-6-luna`; and DeepInfra adds
101
- `tencent/Hy4-preview` and the Xiaomi MiMo V2.6 Pro and V2.6 Flash models.
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 four models, drops six, and reprices
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. An `LLM::Agent` enables a budget of 5 by
85
- default (or 8 on the Alibaba provider, which rate limits more
86
- often), so most users never touch this directly.
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
@@ -50,6 +50,8 @@ an `embed` method:
50
50
  - Ollama (`qwen3:latest`)
51
51
  - Google (`gemini-embedding-2`)
52
52
  - Mistral (`mistral-embed`)
53
+ - OpenRouter (`openai/text-embedding-3-small`)
54
+ - Bedrock (the model is given per call)
53
55
 
54
56
  ### Vector stores (OpenAI)
55
57
 
@@ -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`), other errors are never
249
- retried. The budget defaults to five for agents, while a raw
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`). A 429 is refused before any
252
- content streams, so retrying the same request loses nothing. Pass
253
- `retry_budget: 0` to disable retries on an agent.
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 and flushes it.
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
- options = self.class.llm_plugin_options
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 and flushes it.
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
- options = self.class.llm_plugin_options
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 [Array<LLM::Message>]
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
- @ctx = LLM::Context.new(llm, params)
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