llm.rb 11.3.1 → 12.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.
Files changed (76) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +354 -2
  3. data/LICENSE +93 -17
  4. data/README.md +226 -616
  5. data/data/anthropic.json +322 -426
  6. data/data/bedrock.json +2634 -1144
  7. data/data/deepinfra.json +1513 -0
  8. data/data/deepseek.json +57 -28
  9. data/data/google.json +411 -771
  10. data/data/openai.json +1104 -771
  11. data/data/xai.json +141 -292
  12. data/data/zai.json +263 -141
  13. data/lib/llm/active_record/acts_as_agent.rb +3 -41
  14. data/lib/llm/active_record/acts_as_llm.rb +18 -0
  15. data/lib/llm/active_record.rb +3 -3
  16. data/lib/llm/agent.rb +25 -1
  17. data/lib/llm/context.rb +17 -5
  18. data/lib/llm/contract/completion.rb +2 -2
  19. data/lib/llm/json_adapter.rb +29 -3
  20. data/lib/llm/provider.rb +3 -3
  21. data/lib/llm/providers/deepinfra/audio.rb +66 -0
  22. data/lib/llm/providers/deepinfra/images.rb +90 -0
  23. data/lib/llm/providers/deepinfra/response_adapter.rb +36 -0
  24. data/lib/llm/providers/deepinfra.rb +100 -0
  25. data/lib/llm/providers/deepseek/images.rb +109 -0
  26. data/lib/llm/providers/deepseek/request_adapter.rb +32 -0
  27. data/lib/llm/providers/deepseek/response_adapter/image.rb +9 -0
  28. data/lib/llm/providers/deepseek/response_adapter.rb +29 -0
  29. data/lib/llm/providers/deepseek.rb +4 -2
  30. data/lib/llm/providers/google/request_adapter.rb +22 -5
  31. data/lib/llm/providers/google.rb +4 -4
  32. data/lib/llm/providers/llamacpp.rb +5 -5
  33. data/lib/llm/providers/openai/audio.rb +6 -2
  34. data/lib/llm/providers/openai/images.rb +9 -50
  35. data/lib/llm/providers/openai/request_adapter/respond.rb +38 -4
  36. data/lib/llm/providers/openai/response_adapter/audio.rb +5 -1
  37. data/lib/llm/providers/openai/response_adapter/completion.rb +1 -1
  38. data/lib/llm/providers/openai/response_adapter/image.rb +0 -4
  39. data/lib/llm/providers/openai/responses.rb +1 -0
  40. data/lib/llm/providers/openai/stream_parser.rb +5 -6
  41. data/lib/llm/providers/openai.rb +2 -2
  42. data/lib/llm/providers/xai/images.rb +49 -26
  43. data/lib/llm/providers/xai.rb +2 -2
  44. data/lib/llm/repl/input.rb +64 -0
  45. data/lib/llm/repl/status.rb +30 -0
  46. data/lib/llm/repl/stream.rb +46 -0
  47. data/lib/llm/repl/transcript.rb +61 -0
  48. data/lib/llm/repl/window.rb +107 -0
  49. data/lib/llm/repl.rb +78 -0
  50. data/lib/llm/response.rb +10 -0
  51. data/lib/llm/schema/leaf.rb +7 -1
  52. data/lib/llm/schema/renderer.rb +121 -0
  53. data/lib/llm/schema.rb +30 -0
  54. data/lib/llm/sequel/agent.rb +2 -43
  55. data/lib/llm/sequel/plugin.rb +25 -7
  56. data/lib/llm/tools/chdir.rb +23 -0
  57. data/lib/llm/tools/git.rb +41 -0
  58. data/lib/llm/tools/mkdir.rb +32 -0
  59. data/lib/llm/tools/pwd.rb +20 -0
  60. data/lib/llm/tools/read_file.rb +40 -0
  61. data/lib/llm/tools/rg.rb +46 -0
  62. data/lib/llm/tools/shell.rb +48 -0
  63. data/lib/llm/tools/swap_text.rb +25 -0
  64. data/lib/llm/tools/write_file.rb +24 -0
  65. data/lib/llm/tools.rb +5 -0
  66. data/lib/llm/tracer/telemetry.rb +4 -6
  67. data/lib/llm/tracer.rb +9 -21
  68. data/lib/llm/transport/execution.rb +16 -1
  69. data/lib/llm/transport/net_http_adapter.rb +1 -1
  70. data/lib/llm/uridata.rb +16 -0
  71. data/lib/llm/version.rb +1 -1
  72. data/lib/llm.rb +9 -0
  73. data/llm.gemspec +5 -18
  74. data/resources/deepdive.md +829 -263
  75. metadata +31 -18
  76. data/lib/llm/tracer/langsmith.rb +0 -144
@@ -12,421 +12,987 @@
12
12
 
13
13
  > A [r.uby.dev](https://r.uby.dev) project.
14
14
 
15
- ## Intro
15
+ ## Welcome
16
+
17
+ Welcome to the llm.rb deepdive. You are reading this document
18
+ in the markdown format. An optimized version exists
19
+ at [https://r.uby.dev/llm/deepdive](https://r.uby.dev/llm/deepdive)
20
+ and it is both easier to read and navigate.
21
+
22
+ This document is a continuation of the [homepage documentation](https://r.uby.dev/llm).
23
+ It assumes you are familiar with the basics already, and focuses on
24
+ features that didn't make it into the homepage documentation.
25
+
26
+ ## Table of contents
27
+
28
+ - [Agents](#agents)
29
+ - [As a subclass](#as-a-subclass)
30
+ - [As an object](#as-an-object)
31
+ - [Skills](#skills)
32
+ - [SKILL.md](#skillmd)
33
+ - [Run it](#run-it)
34
+ - [MCP](#mcp)
35
+ - [stdio](#stdio)
36
+ - [http](#http)
37
+ - [A2A](#a2a)
38
+ - [rest](#rest)
39
+ - [jsonrpc](#jsonrpc)
40
+ - [Transports](#transports)
41
+ - [net/http](#nethttp)
42
+ - [net/http/persistent](#nethttppersistent)
43
+ - [curb](#curb)
44
+ - [Stream](#stream)
45
+ - [IO-like object](#io-like-object)
46
+ - [LLM::Stream](#llmstream)
47
+ - [ORM](#orm)
48
+ - [ActiveRecord](#activerecord)
49
+ - [Sequel](#sequel)
50
+ - [Schema](#schema)
51
+ - [Estimation](#estimation)
52
+ - [Cancellation](#cancellation)
53
+ - [Cancel a request](#cancel-a-request)
54
+ - [Tracer](#tracer)
55
+ - [Provider-wide tracer](#provider-wide-tracer)
56
+ - [Agent-local tracer](#agent-local-tracer)
57
+ - [REPL](#repl)
58
+ - [LLM::Agent](#llmagent)
59
+ - [Images](#images)
60
+ - [Generation](#generation)
61
+ - [Edits](#edits)
62
+ - [Audio](#audio)
63
+ - [text-to-speech](#text-to-speech)
64
+ - [speech-to-text](#speech-to-text)
65
+ - [translation](#translation)
66
+
67
+ ## Agents
68
+
69
+ An agent is represented by the
70
+ [`LLM::Agent`](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html)
71
+ class, and it is built on top of
72
+ [`LLM::Context`](https://r.uby.dev/api-docs/llm.rb/LLM/Context.html) -
73
+ the heart of the runtime. An agent manages the tool loop automatically,
74
+ implements a tool loop guard for misbehaving models, and
75
+ it can use five different concurrency strategies to execute
76
+ tools.
77
+
78
+ An agent can be a subclass of
79
+ [`LLM::Agent`](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html),
80
+ or a direct
81
+ instance of it. The subclass approach is useful when you
82
+ want reusable agents that can attach behavior (as methods)
83
+ to their own class.
84
+
85
+ #### As a subclass
86
+
87
+ A subclass of
88
+ [`LLM::Agent`](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html)
89
+ can define its model, tools,
90
+ and other attributes at the class-level. All of these
91
+ attributes are optional, and they act as defaults that
92
+ can be overriden on the instance level.
93
+
94
+ The example uses the `:fork` concurrency model. It has
95
+ two primary benefits: tools are run in parallel, and in
96
+ a separate process with a separate memory address space.
97
+
98
+ The example purposefully demonstrates how the attributes
99
+ can be lazily defined with a block, or a Symbol that is
100
+ evaluated as an instance method on the subclass. It is
101
+ not strictly neccessary, though, and the example would
102
+ be simpler without it.
16
103
 
17
- This guide is a practical walkthrough of [llm.rb](https://github.com/r-uby-dev/llm.rb#readme) —
18
- Ruby's capable AI runtime.
104
+ ```ruby
105
+ class Agent < LLM::Agent
106
+ model "deepseek-v4-pro"
107
+ tools { [DoResearch, FinalizeResearch, ActOnResearch] }
108
+ stream { $stdout }
109
+ tracer :set_tracer
110
+ concurrency :fork
111
+
112
+ def research!
113
+ talk "start the research"
114
+ end
19
115
 
20
- llm.rb runs on Ruby's standard library by default and loads optional pieces
21
- only when needed. You can start with a provider and a single context, then add
22
- agents, tools, streaming, persistence, embeddings, and protocol clients
23
- without changing the shape of your code.
116
+ private
117
+
118
+ def set_tracer
119
+ LLM::Tracer::Logger.new(llm, io: $stderr)
120
+ end
121
+ end
122
+ llm = LLM.deepseek(key: ENV["KEY"])
123
+ agent = Agent.new(llm).tap(&:research!)
124
+ agent.talk "How did the research go?"
125
+ ```
24
126
 
25
- It supports OpenAI, OpenAI-compatible endpoints, Anthropic, Google Gemini,
26
- DeepSeek, xAI, Z.ai, AWS Bedrock, Ollama, and llama.cpp. ActiveRecord and
27
- Sequel support are built in, along with concurrent tool execution through
28
- threads, tasks, fibers, ractors, and fork.
127
+ #### As an object
29
128
 
30
- ## Install
129
+ The more direct, and sometimes more convienent approach, is to
130
+ create an instance of
131
+ [`LLM::Agent`](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html)
132
+ directly. The same attributes can be provided as the
133
+ second argument given to
134
+ [`LLM::Agent.new`](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html),
135
+ and the same lazy evaluation rules apply. This approach can be
136
+ great for prototyping quickly, and you can always turn to a
137
+ subclass later if that makes more sense.
31
138
 
32
- ```bash
33
- gem install llm.rb
139
+ ```ruby
140
+ llm = LLM.deepseek(key: ENV["KEY"])
141
+ agent = LLM::Agent.new(llm, stream: $stdout)
142
+ agent.talk "Hello, fellow agent"
34
143
  ```
35
144
 
36
- ## Quick Start
145
+ [Back to top](#table-of-contents)
37
146
 
38
- #### Agent
147
+ ## Tools
39
148
 
40
- [`LLM::Agent`](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html) is the
41
- recommended starting point.
42
- <br>
43
- It manages tool execution for you and keeps conversation state across turns.
149
+ A tool extends the capabilities of a model. <br>
150
+ A tool is a subclass of
151
+ [`LLM::Tool`](https://r.uby.dev/api-docs/llm.rb/LLM/Tool.html)
152
+ that has a name,
153
+ a description, and an optional set of typed parameters.
154
+
155
+ A tool also has a method associated with it, and when the
156
+ model calls a tool it will do so through this method &ndash;
157
+ alongside any parameters the tool might have defined.
158
+
159
+ In other words, a tool provides a way for a model to
160
+ call a method you have written, and it returns a value
161
+ to the model that is considered the tool's response.
162
+ The model then proceeds to process the tool's response,
163
+ and then might generate its own response, or perhaps call
164
+ another tool.
165
+
166
+ #### LLM::Tool
167
+
168
+ A tool can be defined by subclassing
169
+ [`LLM::Tool`](https://r.uby.dev/api-docs/llm.rb/LLM/Tool.html)
170
+ with
171
+ a name, description, and optional set of parameters. The
172
+ tool name, and description should be informative so the
173
+ model can understand what the tool does and how it can
174
+ serve a user's query.
44
175
 
45
176
  ```ruby
46
177
  require "llm"
178
+ require "shellwords"
179
+
180
+ class Shell < LLM::Shell
181
+ name "shell"
182
+ description "execute a shell command"
183
+ parameter :name, String, "the command's name"
184
+ parameter :arguments, Array[String], "One or more arguments"
185
+ required %i[name]
186
+ defaults arguments: []
187
+
188
+ def call(name:, arguments:)
189
+ out = `#{name.shellscape} #{arguments.map(&:shellescape).join(" ")}`
190
+ {ok: $?.success?, out:}
191
+ end
192
+ end
47
193
 
48
- llm = LLM.openai(key: ENV["KEY"])
49
- agent = LLM::Agent.new(llm, stream: $stdout)
50
- agent.talk "Hello world"
194
+ llm = LLM.deepseek(key: ENV["KEY"])
195
+ agent = LLM::Agent.new(llm, tools: [Shell], stream: $stdout)
196
+ agent.talk "What files are in the current working directory?"
51
197
  ```
52
198
 
53
- #### REPL
199
+ #### Errors
200
+
201
+ Exceptions that might be raised by a tool are automatically
202
+ rescued and returned to the model as a structured error.
203
+ Otherwise &ndash; the conversation's history could be left
204
+ in an invalid state.
54
205
 
55
- A read-eval-print loop is the simplest way to interact with an agent.
56
- <br>
57
- The loop reads input, sends it to the model, and prints the response as it
58
- arrives:
206
+ That's because a tool call must complete with a tool response,
207
+ that's the only valid response a model expects, so even in the
208
+ case of an error, something must be returned that communicates
209
+ what happened.
59
210
 
60
211
  ```ruby
61
- require "llm"
212
+ class Error < LLM::Tool
213
+ name "error"
214
+ description "demo how errors are handled"
215
+
216
+ ##
217
+ # Returns
218
+ # {error: true, kind: "RuntimeError", message: "boom"}
219
+ def call
220
+ raise "boom"
221
+ end
222
+ end
223
+ ```
62
224
 
63
- llm = LLM.openai(key: ENV["KEY"])
64
- agent = LLM::Agent.new(llm, stream: $stdout)
225
+ ## Skills
65
226
 
66
- loop do
67
- print "> "
68
- agent.talk(STDIN.gets || break)
69
- puts
70
- end
227
+ The skill concept is borrowed from tools like Claude and
228
+ Codex, but llm.rb gives it a runtime of its own. A skill
229
+ is a directory with a `SKILL.md` file. That file contains
230
+ frontmatter where the skill's name, description, and tools
231
+ can be declared.
232
+
233
+ #### SKILL.md
234
+
235
+ The `SKILL.md` file can look like this. When a skill runs,
236
+ the runtime spawns a subagent with its own context window
237
+ and message history. Some context is inherited from the
238
+ parent agent, though.
239
+
240
+ By default the subagent can only access the tools declared
241
+ by the skill. The `inherit` directive lets it inherit the
242
+ parent agent's tools instead, including A2A and MCP tools.
243
+
244
+ ```markdown
245
+ ---
246
+ name: git-skill
247
+ description: reads my git history and writes a summary
248
+ tools: ['git-log', 'git-show', 'write-file']
249
+ ---
250
+
251
+ ## Task
252
+
253
+ Collect a log of recent history.
254
+ Analyze each commit.
255
+ Write a summary to summary.txt
71
256
  ```
72
257
 
73
- #### Context
258
+ #### Run it
74
259
 
75
- [`LLM::Context`](https://r.uby.dev/api-docs/llm.rb/LLM/Context.html) is the
76
- lower-level runtime object.
77
- <br>
78
- It holds the same conversation state but leaves tool execution up to you.
79
- Use it when you want to decide when and how tools run.
260
+ Given the skill above, llm.rb only needs the path to the
261
+ directory that contains `SKILL.md`. Under the hood, a skill
262
+ is represented as a tool the model can call. That means
263
+ a skill can be called whenever it satisfies the user's
264
+ request &ndash; in the same way that a regular tool can.
265
+
266
+ This feature also works with both the ActiveRecord, and
267
+ Sequel integrations.
80
268
 
81
269
  ```ruby
82
270
  require "llm"
83
271
 
84
- llm = LLM.openai(key: ENV["KEY"])
85
- ctx = LLM::Context.new(llm, stream: $stdout)
86
- ctx.talk "Hello world"
272
+ llm = LLM.deepseek(key: ENV["KEY"])
273
+ agent = LLM::Agent.new(llm, skills: [__dir__])
274
+ agent.talk "run the git skill"
87
275
  ```
88
276
 
89
- With tools, the manual loop is explicit:
277
+ [Back to top](#table-of-contents)
278
+
279
+ ## MCP
280
+
281
+ #### stdio
282
+
283
+ The stdio transport connects to an MCP server that is launched as a
284
+ separate process, and both its standard input and standard output
285
+ streams are used for communication. It is recommended but not
286
+ required to execute commands for a stdio transport over a
287
+ persistent session via the
288
+ [`LLM::MCP#session`](https://r.uby.dev/api-docs/llm.rb/LLM/MCP.html#session-instance_method)
289
+ method &ndash; otherwise
290
+ you could end up launching the same process multiple times.
90
291
 
91
292
  ```ruby
92
- ctx = LLM::Context.new(llm, tools: [ReadFile])
93
- ctx.talk("Read README.md and summarize it.")
94
- ctx.talk(ctx.wait(:call)) while ctx.functions?
293
+ require "llm"
294
+
295
+ llm = LLM.deepseek(key: ENV["KEY"])
296
+ mcp = LLM::MCP.stdio(argv: ["npx", "-y", "@forgejo/mcp-server"])
297
+ agent = LLM::Agent.new(llm)
298
+
299
+ mcp.session do
300
+ agent.talk "What's happening on forgejo?", tools: mcp.tools
301
+ end
95
302
  ```
96
303
 
97
- For ordinary application code, prefer
98
- [`LLM::Agent`](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html).
99
- It does the same thing but manages the loop for you.
304
+ #### http
100
305
 
101
- ## Tools
306
+ The http transport connects to an MCP server over HTTP, and unlike
307
+ the stdio transport, the MCP server does not have to be running
308
+ locally. Popular services like GitHub provide their own MCP server
309
+ over HTTP, and it is one of the most capable MCP servers I have
310
+ used.
311
+
312
+ Unlike the stdio transport,
313
+ [`LLM::MCP#session`](https://r.uby.dev/api-docs/llm.rb/LLM/MCP.html#session-instance_method)
314
+ carries little benefit for the http transport and it can be
315
+ omitted. It is recommended to consider the `net_http_persistent`
316
+ transport for MCP interactions that run over HTTP, otherwise
317
+ you could end up tearing down and setting up the same connection
318
+ multiple times.
319
+
320
+ ```ruby
321
+ require "llm"
322
+
323
+ llm = LLM.deepseek(key: ENV["KEY"])
324
+ mcp = LLM::MCP.http(
325
+ url: "https://api.githubcopilot.com/mcp/",
326
+ headers: {
327
+ "Authorization" => "Bearer #{ENV.fetch('GITHUB_PAT')}"
328
+ },
329
+ transport: :net_http_persistent
330
+ )
331
+ agent = LLM::Agent.new(llm)
332
+ agent.talk "What's happening on GitHub?", tools: mcp.tools
333
+ ```
334
+
335
+ [Back to top](#table-of-contents)
336
+
337
+ ## A2A
102
338
 
103
- #### Definition
339
+ #### rest
104
340
 
105
- Tools extend what the model can do.
106
- <br>
107
- They are plain Ruby classes with typed parameters. Define one, attach it to
108
- an agent, and the model can call it when it makes sense.
341
+ The rest transport communicates with other agents via A2A
342
+ endpoints that speak both HTTP and JSON. The skills advertised
343
+ by an agent become subclasses of
344
+ [`LLM::Tool`](https://r.uby.dev/api-docs/llm.rb/LLM/Tool.html)
345
+ that can be used by both
346
+ [`LLM::Context`](https://r.uby.dev/api-docs/llm.rb/LLM/Context.html),
347
+ and [`LLM::Agent`](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html)
348
+ &ndash; similar to how MCP tools become subclasses of
349
+ [`LLM::Tool`](https://r.uby.dev/api-docs/llm.rb/LLM/Tool.html).
109
350
 
110
351
  ```ruby
111
- class ReadFile < LLM::Tool
112
- name "read-file"
113
- description "Read a file"
114
- parameter :path, String, "The filename or path"
115
- required %i[path]
116
-
117
- def call(path:)
118
- {contents: File.read(path)}
119
- end
120
- end
352
+ require "llm"
353
+
354
+ llm = LLM.deepseek(key: ENV["KEY"])
355
+ a2a = LLM::A2A.rest(url: "https://agent.example.com")
356
+ agent = LLM::Agent.new(llm, tools: a2a.skills)
357
+ agent.talk "What's happening, fellow agent?"
121
358
  ```
122
359
 
123
- Attach the tool to an agent:
360
+ #### jsonrpc
361
+
362
+ The jsonrpc transport communicates with other agents via HTTP
363
+ and a protocol known as jsonrpc. Sometimes an agent will
364
+ implement both, or just one of each. An agent's card, which
365
+ is represented by an instance of
366
+ [`LLM::A2A::Card`](https://r.uby.dev/api-docs/llm.rb/LLM/A2A/Card.html),
367
+ can be
368
+ used to discover available transports via the
369
+ [`LLM::A2A::Card#interfaces`](https://r.uby.dev/api-docs/llm.rb/LLM/A2A/Card.html#interfaces-instance_method)
370
+ method.
124
371
 
125
372
  ```ruby
126
- agent = LLM::Agent.new(llm, stream: $stdout, tools: [ReadFile])
127
- agent.talk "Read README.md and summarize the project."
373
+ require "llm"
374
+ llm = LLM.deepseek(key: ENV["KEY"])
375
+ a2a = LLM::A2A.jsonrpc(url: "https://agent.example.com")
376
+ agent = LLM::Agent.new(llm, tools: a2a.skills)
377
+ agent.talk "What's happening, fellow agent?"
128
378
  ```
129
379
 
130
- [`LLM::Tool`](https://r.uby.dev/api-docs/llm.rb/LLM/Tool.html) handles the
131
- Ruby-side definition. llm.rb adapts the tool schema to the provider at request
132
- time.
380
+ [Back to top](#table-of-contents)
381
+
382
+ ## Transports
133
383
 
134
- #### Concurrency
384
+ The [`LLM::Provider`](https://r.uby.dev/api-docs/llm.rb/LLM/Provider.html),
385
+ [`LLM::MCP`](https://r.uby.dev/api-docs/llm.rb/LLM/MCP.html), and
386
+ [`LLM::A2A`](https://r.uby.dev/api-docs/llm.rb/LLM/A2A.html) classes
387
+ all accept a `transport` option that decides which library
388
+ will be used for HTTP communication. There are three options out
389
+ of the box:
390
+ [`net-http`](https://github.com/ruby/net-http),
391
+ [`net-http-persistent`](https://github.com/drbrain/net-http-persistent),
392
+ and [`curb`](https://github.com/taf2/curb).
135
393
 
136
- When an agent calls several tools at once, you can run them in parallel.
137
- <br>
138
- This cuts down waiting time when tools do independent work like reading
139
- files or calling APIs.
394
+ #### net/http
395
+
396
+ The [`net/http`](https://github.com/ruby/net-http) transport is represented by the symbol `:net_http`. <br>
397
+ It is the default transport.
140
398
 
141
399
  ```ruby
142
- class Agent < LLM::Agent
143
- model "gpt-5.4-mini"
144
- tools ReadFile
145
- concurrency :thread
146
- end
400
+ require "llm"
147
401
 
148
- llm = LLM.openai(key: ENV["KEY"])
149
- agent = Agent.new(llm, stream: $stdout)
150
- agent.talk "Read README.md and CHANGELOG.md and compare them."
402
+ llm = LLM.deepseek(key: "...", transport: :net_http)
403
+ mcp = LLM::MCP.http(url: "...", transport: :net_http)
404
+ a2a = LLM::A2A.rest(url: "...", transport: :net_http)
151
405
  ```
152
406
 
153
- ## Structured Output
407
+ #### net/http/persistent
154
408
 
155
- #### Schema
156
-
157
- When you need JSON with a known shape, use
158
- [`LLM::Schema`](https://r.uby.dev/api-docs/llm.rb/LLM/Schema.html).
159
- <br>
160
- The model will return data that matches your schema instead of free text.
409
+ The [`net/http/persistent`](https://github.com/drbrain/net-http-persistent) transport is represented by the symbol `:net_http_persistent`. <br>
410
+ It maintains a connection pool so the cost of tearing down and
411
+ setting up a connection repeatedly is kept low, and it is built
412
+ on top of [`net/http`](https://github.com/ruby/net-http).
161
413
 
162
414
  ```ruby
163
- class Report < LLM::Schema
164
- property :category, Enum["performance", "security", "outage"]
165
- property :summary, String, "Short summary"
166
- property :services, Array[String], "Impacted services"
167
- required %i[category summary services]
168
- end
415
+ require "llm"
169
416
 
170
- agent = LLM::Agent.new(llm, schema: Report)
171
- res = agent.talk("Classify: 'API latency spiked for the billing service.'")
172
- puts res.content!
417
+ llm = LLM.deepseek(key: "...", transport: :net_http_persistent)
418
+ mcp = LLM::MCP.http(url: "...", transport: :net_http_persistent)
419
+ a2a = LLM::A2A.rest(url: "...", transport: :net_http_persistent)
173
420
  ```
174
421
 
175
- For one-off schemas, build the shape inline:
422
+ #### curb
423
+
424
+ The [`curb`](https://github.com/taf2/curb) transport is represented by the symbol `:curb`. <br>
425
+ It provides bindings for libcurl &ndash; a widely used, highly portable
426
+ and feature-rich HTTP library written in C.
176
427
 
177
428
  ```ruby
178
- schema = LLM::Schema.new.object(
179
- category: LLM::Schema.new.string.enum("bug", "feature").required,
180
- summary: LLM::Schema.new.string.required
181
- )
429
+ require "llm"
182
430
 
183
- agent = LLM::Agent.new(llm, schema:)
184
- res = agent.talk("Classify: add a dark mode toggle.")
185
- puts res.content
431
+ llm = LLM.deepseek(key: "...", transport: :curb)
432
+ mcp = LLM::MCP.http(url: "...", transport: :curb)
433
+ a2a = LLM::A2A.rest(url: "...", transport: :curb)
186
434
  ```
187
435
 
188
- ## Streaming
436
+ [Back to top](#table-of-contents)
437
+
438
+ ## Stream
189
439
 
190
- #### Stream
440
+ #### IO-like object
191
441
 
192
- Streaming works with any object that responds to `#<<`, like `$stdout`.
193
- <br>
194
- For more control, subclass
195
- [`LLM::Stream`](https://r.uby.dev/api-docs/llm.rb/LLM/Stream.html) and
196
- override its callbacks:
442
+ Any object that implements the `#<<` method can receive
443
+ chunks from a stream. That includes objects like `$stdout`.
444
+ This form of streaming is simple and limited. It is the
445
+ equivalent of
446
+ [`LLM::Stream#on_content`](https://r.uby.dev/api-docs/llm.rb/LLM/Stream.html#on_content-instance_method),
447
+ and doesn't include
448
+ any of the other
449
+ [`LLM::Stream`](https://r.uby.dev/api-docs/llm.rb/LLM/Stream.html)
450
+ hooks.
197
451
 
198
452
  ```ruby
199
- class MyStream < LLM::Stream
453
+ require "llm"
454
+
455
+ llm = LLM.deepseek(key: ENV["KEY"])
456
+ agent = LLM::Agent.new(llm, stream: $stdout)
457
+ agent.talk "hello world"
458
+ ```
459
+
460
+ #### LLM::Stream
461
+
462
+ The [`LLM::Stream`](https://r.uby.dev/api-docs/llm.rb/LLM/Stream.html)
463
+ class provides many hooks that a subclass
464
+ can implement. They range from being notified when a tool call
465
+ starts to when a tool call finishes, or when a conversation is
466
+ due to be compacted because the context window exceeded a defined
467
+ limit. All these callbacks support a responsive user interface
468
+ where the user is always aware of what is happening behind the
469
+ scenes.
470
+
471
+ ```ruby
472
+ class Stream < LLM::Stream
200
473
  def on_content(content)
201
- print content
474
+ puts content
202
475
  end
203
476
 
204
477
  def on_reasoning_content(content)
205
- warn content
478
+ puts content
206
479
  end
207
- end
208
480
 
209
- llm = LLM.openai(key: ENV["KEY"])
210
- agent = LLM::Agent.new(llm, stream: MyStream.new)
211
- agent.talk "Explain Ruby fibers."
481
+ def on_tool_call(tool, error)
482
+ # this callback can be used to either log a tool call,
483
+ # or execute a tool call during a stream.
484
+ end
485
+
486
+ def on_tool_return(tool, result)
487
+ end
488
+
489
+ def on_compaction(ctx, compactor)
490
+ # this callback is called *before* a compact happens
491
+ end
492
+
493
+ def on_compaction_finish(ctx, compactor)
494
+ # this callback is called *after* a compact happens
495
+ end
496
+ end
212
497
  ```
213
498
 
214
- ## Skills
499
+ [Back to top](#table-of-contents)
215
500
 
216
- #### Release
501
+ ## Serialization
217
502
 
218
- Skills package repeatable instructions and scoped tool access into
219
- `SKILL.md` directories.
220
- <br>
221
- They turn common workflows into named capabilities that agents can load
222
- on demand.
503
+ The [`LLM::Context`](https://r.uby.dev/api-docs/llm.rb/LLM/Context.html)
504
+ class can be serialized to JSON and stored in a string or on disk.
505
+ That is powerful because a context contains runtime state that can
506
+ be restored later, in a different process or even on a different
507
+ machine. And because an agent is implemented on top of
508
+ [`LLM::Context`](https://r.uby.dev/api-docs/llm.rb/LLM/Context.html)
509
+ this feature works for [`LLM::Agent`](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html),
510
+ too.
223
511
 
224
- ```yaml
225
- ---
226
- name: release
227
- description: Prepare a release
228
- tools: ["search-docs", "git"]
229
- ---
512
+ #### Save to disk
230
513
 
231
- ## Task
514
+ The runtime can serialize its state to a string, a text file, or
515
+ a database column. The option that fits best depends on your application
516
+ and environment. Web applications might be more interested in the [ORM](#orm)
517
+ feature, which is built on top of the serialization feature.
232
518
 
233
- Review the release state, summarize what changed, and prepare the release.
519
+ ```ruby
520
+ ##
521
+ # Create a provider
522
+ llm = LLM.deepseek(key: ENV["KEY"])
523
+
524
+ ##
525
+ # Save agent
526
+ agent1 = LLM::Agent.new(llm)
527
+ agent1.talk "remember my name is robert"
528
+ agent1.save(path: "agent.json")
529
+
530
+ ##
531
+ # Restore agent
532
+ agent2 = LLM::Agent.new(llm, stream: $stdout)
533
+ agent2.restore(path: "agent.json")
534
+ agent2.talk "what's my name?"
234
535
  ```
235
536
 
537
+ ## ORM
538
+
539
+ Both ActiveRecord, and Sequel have first-class support on the
540
+ llm.rb runtime. In both cases an ActiveRecord or Sequel model
541
+ can be turned into a model that has the same capabilities as
542
+ [`LLM::Context`](https://r.uby.dev/api-docs/llm.rb/LLM/Context.html),
543
+ or [`LLM::Agent`](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html).
544
+
545
+ The main difference is that the runtime persists directly into
546
+ the database with no requirements beyond a single column on a
547
+ single row. That means it is usually trivial to turn an existing
548
+ model into an AI-aware model.
549
+
550
+ #### ActiveRecord
551
+
552
+ The ActiveRecord interface for
553
+ [`LLM::Agent`](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html)
554
+ is
555
+ [`acts_as_agent`](https://r.uby.dev/api-docs/llm.rb/LLM/ActiveRecord/ActsAsAgent.html).
556
+ It yields an instance of
557
+ [`LLM::Agent`](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html),
558
+ and that can be used
559
+ to configure the agent (eg which model, instructions, skills,
560
+ tools, etc).
561
+
562
+ An interesting option is the `format` option, by default it
563
+ defaults to `:string` but it can also be changed to `:json`
564
+ or `:jsonb` depending on the configuration and type of underlying
565
+ column. The JSONB column type is recommended.
566
+
236
567
  ```ruby
237
- class ReleaseAgent < LLM::Agent
238
- model "gpt-5.4-mini"
239
- skills "./skills/release"
240
- end
568
+ require "active_record"
569
+ require "llm"
570
+ require "llm/active_record"
241
571
 
242
- llm = LLM.openai(key: ENV["KEY"])
243
- ReleaseAgent.new(llm, stream: $stdout).talk("Prepare the next release.")
244
- ```
572
+ class Agent < ApplicationRecord
573
+ acts_as_agent(format: :jsonb) do |agent|
574
+ agent.model "deepseek-v4-pro"
575
+ agent.instructions "solve the user's query"
576
+ agent.tools [Research, FinalizeResearch, ActOnResearch]
577
+ end
578
+
579
+ private
245
580
 
246
- When a skill runs, llm.rb starts a subagent with the skill's instructions,
247
- its allowed tools, and recent conversation context. Skills can also use
248
- `tools: inherit` to run with the parent agent's full toolset.
581
+ ##
582
+ # By convention, this method defines the provider
583
+ # for a model. If neccessary, it can be renamed and
584
+ # configured via `provider: :your_method` instead.
585
+ def set_provider
586
+ LLM.deepseek(key: ENV["KEY"])
587
+ end
249
588
 
250
- ## MCP
589
+ ##
590
+ # By convention, this method should return what is
591
+ # given as the second argument to `LLM::Context` or
592
+ # `LLM::Agent`.
593
+ #
594
+ # Often, there is no need to set it, so it can be left
595
+ # undefined or it can be reassigned in the same way as
596
+ # `set_provider`. For example: `context: :your_method`
597
+ def set_context
598
+ {}
599
+ end
600
+ end
251
601
 
252
- #### Stdio
602
+ agent = Agent.create!
603
+ agent.talk "perform research"
604
+ ```
605
+
606
+ #### Sequel
253
607
 
254
- [`LLM::MCP`](https://r.uby.dev/api-docs/llm.rb/LLM/MCP.html) lets llm.rb use
255
- tools provided by local stdio servers or remote HTTP servers.
256
- <br>
257
- This is how you connect your agent to GitHub, databases, or anything else
258
- that speaks the Model Context Protocol.
608
+ The following is a Sequel equivalent to the ActiveRecord example,
609
+ but to keep it interesting and informative, this example also
610
+ configures a per-model tracer that logs to `$stdout`. Works the
611
+ same for ActiveRecord.
259
612
 
260
613
  ```ruby
614
+ require "sequel"
261
615
  require "llm"
616
+ require "llm/sequel/plugin"
617
+
618
+ class Agent < Sequel::Model
619
+ plugin(:agent, format: :jsonb) do |agent|
620
+ agent.model "deepseek-v4-pro"
621
+ agent.instructions "solve the user's query"
622
+ agent.tools [Research, FinalizeResearch, ActOnResearch]
623
+ agent.tracer { LLM::Tracer::Logger.new(llm, io: $stdout) }
624
+ end
262
625
 
263
- llm = LLM.openai(key: ENV["KEY"])
264
- mcp = LLM::MCP.stdio(argv: ["ruby", "server.rb"])
626
+ private
265
627
 
266
- mcp.session do
267
- agent = LLM::Agent.new(llm, stream: $stdout, tools: mcp.tools)
268
- agent.talk "Use the available tools to inspect the environment."
628
+ def set_provider
629
+ LLM.deepseek(key: ENV["KEY"])
630
+ end
269
631
  end
632
+
633
+ agent = Agent.create
634
+ agent.talk "perform research"
270
635
  ```
271
636
 
272
- #### Remote
637
+ [Back to top](#table-of-contents)
638
+
639
+ ## Schema
640
+
641
+ The [`LLM::Schema`](https://r.uby.dev/api-docs/llm.rb/LLM/Schema.html)
642
+ class can be subclassed to describe
643
+ the shape of a JSON object or objects that you expect
644
+ the model to respond with.
645
+
646
+ It can be useful for a wide range of use cases but the
647
+ most popular might be classification, data extraction,
648
+ and transferring structured data between different software
649
+ rather than blobs of text that a machine cannot easily parse
650
+ in a structured way.
651
+
652
+ #### Estimation
653
+
654
+ The following example asks the model to estimate the age
655
+ of a person in a photo. The model provides a structured response
656
+ that's represented by an instance of
657
+ [`LLM::Object`](https://r.uby.dev/api-docs/llm.rb/LLM/Object.html).
273
658
 
274
- For HTTP MCP servers, use persistent connections when you make repeated
275
- tool calls:
659
+ The object returned by
660
+ [`LLM::Response#content!`](https://r.uby.dev/api-docs/llm.rb/LLM/Contract/Completion.html#content!-instance_method)
661
+ has methods that can access the age, confidence, and comments
662
+ properties.
663
+ This approach can also work for extracting data or an analysis
664
+ from a PDF, and other file types.
276
665
 
277
666
  ```ruby
278
- mcp = LLM::MCP.http(
279
- url: "https://remote-mcp.example.com",
280
- transport: :net_http_persistent
281
- )
667
+ require "llm"
668
+ require "pp"
669
+
670
+ class Estimation < LLM::Schema
671
+ property :age, Integer, "The estimated age of the person"
672
+ property :confidence, Number, "Your confidence in the estimate"
673
+ property :applicable, Boolean, "True when the photo contains a person"
674
+ property :comments, String, "Any additional comments or input"
675
+ required %i[age confidence applicable comments]
676
+ end
282
677
 
283
- agent = LLM::Agent.new(llm, stream: $stdout, tools: mcp.tools)
284
- agent.talk "Use the remote tools to inspect the repository."
678
+ llm = LLM.openai(key: ENV["KEY"])
679
+ agent = LLM::Agent.new(llm, schema: Estimation)
680
+ res = agent.ask "Given this photo, provide an age estimate", with: "photo.jpg"
681
+
682
+ ##
683
+ # Coerces the model's response from a JSON string
684
+ # to an instance of LLM::Object.
685
+ estimate = res.content!
686
+
687
+ ##
688
+ # Let's print the estimate
689
+ if estimate.applicable
690
+ print "The person is approx ", estimate.age.to_s, " years old", "\n"
691
+ print "I have a confidence rating of ", estimate.confidence.to_s, "\n"
692
+ else
693
+ print "This photo is not applicable:", "\n"
694
+ print estimate.comments
695
+ end
285
696
  ```
286
697
 
287
- ## Persistence
698
+ [Back to top](#table-of-contents)
699
+
700
+ ## Cancellation
288
701
 
289
- #### Overview
702
+ #### Cancel a request
290
703
 
291
- Agents and contexts serialize to JSON and restore later.
292
- <br>
293
- The same serialized state powers the ActiveRecord and Sequel integrations.
704
+ A common scenario when communicating with a model is to
705
+ want to cancel the request mid-stream. This could be done
706
+ for a number of different reasons, most often because the
707
+ user made a mistake, or the model is making a mistake and
708
+ the user wants to cancel the action.
294
709
 
295
- #### Filesystem
710
+ The runtime has built-in support for cancellation. So for
711
+ example it is possible to cancel a request on the main
712
+ thread from a secondary thread. A number of things happen
713
+ when a request is cancelled. First the request is cancelled
714
+ at the transport level, and each transport handles it a little
715
+ differently. The net effect in every case is that the connection
716
+ is closed.
296
717
 
297
- Persist agent state to a JSON file on disk.
718
+ The runtime then notifies the rest of the system. so for example,
719
+ if a tool was running, it will receive the `on_interrupt` / `on_cancel`
720
+ callback that lets the tool do any necessary cleanup, or execute its own
721
+ cancellation plan. Tools that were pending (not yet run but requetsed to
722
+ run) are cancelled through
723
+ [`LLM::Function#cancel`](https://r.uby.dev/api-docs/llm.rb/LLM/Function.html#cancel-instance_method).
298
724
 
299
725
  ```ruby
300
726
  require "llm"
301
727
 
302
- llm = LLM.openai(key: ENV["KEY"])
728
+ llm = LLM.deepseek(key: ENV["DEEPSEEK_SECRET"])
303
729
  agent = LLM::Agent.new(llm)
304
- agent.talk "Remember that my favorite language is Ruby"
730
+ queue = Queue.new
305
731
 
306
- # Save
307
- File.write("agent.json", agent.to_json)
732
+ Thread.new do
733
+ queue.push(nil)
734
+ sleep(2)
735
+ agent.cancel!
736
+ end
308
737
 
309
- # Restore later
310
- agent2 = LLM::Agent.new(llm, stream: $stdout)
311
- agent2.restore(path: "agent.json")
312
- agent2.talk "What is my favorite language?"
738
+ begin
739
+ queue.pop
740
+ agent.talk "write me a very long poem", stream: $stdout
741
+ rescue LLM::Interrupt
742
+ puts "request cancelled!"
743
+ end
313
744
  ```
314
745
 
315
- #### ActiveRecord
746
+ [Back to top](#table-of-contents)
747
+
748
+ ## Tracer
316
749
 
317
- [`acts_as_agent`](https://r.uby.dev/api-docs/llm.rb/LLM/ActiveRecord/ActsAsAgent.html)
318
- wraps an agent directly on an ActiveRecord model.
319
- <br>
320
- Serialized state lives in a single `data` column while your application
321
- controls provider, model, and tool configuration.
750
+ The runtime can be observed by subclasses of
751
+ [`LLM::Tracer`](https://r.uby.dev/api-docs/llm.rb/LLM/Tracer.html). <br>
752
+ The default tracers include a tracer that can write to standard
753
+ output
754
+ ([`LLM::Tracer::Logger`](https://r.uby.dev/api-docs/llm.rb/LLM/Tracer/Logger.html)),
755
+ and a generic OpenTelemetry tracer that can export spans via OTLP
756
+ ([`LLM::Tracer::Telemetry`](https://r.uby.dev/api-docs/llm.rb/LLM/Tracer/Telemetry.html)).
757
+
758
+ llm.rb has numerous hooks implemented throughout the runtime that
759
+ [`LLM::Tracer`](https://r.uby.dev/api-docs/llm.rb/LLM/Tracer.html)
760
+ subclasses can hook into, and the tracer is
761
+ purposefully designed to be extensible. The scope of a trace
762
+ can vary from an individual agent (an instance of
763
+ [`LLM::Agent`](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html)),
764
+ or for every request a provider makes (an indirect instance of
765
+ [`LLM::Provider`](https://r.uby.dev/api-docs/llm.rb/LLM/Provider.html)).
766
+
767
+ #### Provider-wide tracer
768
+
769
+ The following two examples demonstrate provider-wide tracers that
770
+ cover every request made for a single provider.
322
771
 
323
772
  ```ruby
324
- require "llm"
325
- require "active_record"
326
- require "llm/active_record"
773
+ ##
774
+ # Provider-wide tracer
775
+ # Writes to $stdout
776
+ llm = LLM.deepseek(key: ENV["KEY"])
777
+ llm.tracer = LLM::Tracer::Logger.new(llm, io: $stdout)
327
778
 
328
- class Ticket < ApplicationRecord
329
- acts_as_agent provider: :set_provider, context: :set_context
330
- model "gpt-5.4-mini"
331
- instructions "You are a concise support assistant."
332
- tools SearchDocs, Escalate
333
- concurrency :thread
779
+ ##
780
+ # Provider-wide tracer
781
+ # Writes to deepseek.log
782
+ llm = LLM.deepseek(key: ENV["KEY"])
783
+ llm.tracer = LLM::Tracer::Logger.new(llm, path: "deepseek.log")
784
+ ```
334
785
 
335
- private
786
+ #### Agent-local tracer
336
787
 
337
- def set_provider
338
- LLM.openai(key: ENV["OPENAI_SECRET"])
339
- end
788
+ The next two examples demonstrate a tracer that is local
789
+ to an agent.
340
790
 
341
- def set_context
342
- {mode: :responses, store: false}
343
- end
344
- end
791
+ ```ruby
792
+ ##
793
+ # Agent-local
794
+ # Writes to $stdout
795
+ llm = LLM.deepseek(key: ENV["KEY"])
796
+ agent = LLM::Agent.new(llm, tracer: LLM::Tracer::Logger.new(llm, io: $stdout))
797
+
798
+ ##
799
+ # Agent-local
800
+ # Writes to deepseek-agent.log
801
+ llm = LLM.deepseek(key: ENV["KEY"])
802
+ agent = LLM::Agent.new(llm, tracer: LLM::Tracer::Logger.new(llm, path: "deepseek-agent.log"))
803
+ ```
804
+
805
+ [Back to top](#table-of-contents)
806
+
807
+ ## REPL
808
+
809
+ During the development and operation of agents it can often
810
+ be helpful to drop into a read-eval-print loop. This gives
811
+ you a simple way to confirm the work was successful, inspect
812
+ anything that went wrong (for example, tool call errors), and
813
+ keep talking to the same agent so you can learn about its
814
+ decision-making process and make improvements for future runs.
815
+ It can also be used to ask the agent to correct any errors
816
+ that might have made.
817
+
818
+ ##### LLM::Agent
345
819
 
346
- ticket = Ticket.create!
347
- puts ticket.talk("How do I rotate my API key?").content
820
+ The [LLM::Agent#repl](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html#repl-instance_method)
821
+ method requires the [curses](https://github.com/ruby/curses)
822
+ gem to be installed and available for require. It is an
823
+ optional dependency that only becomes required when you
824
+ call the [LLM::Agent#repl](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html#repl-instance_method)
825
+ method.
826
+
827
+ ```ruby
828
+ require "llm"
829
+
830
+ llm = LLM.deepseek(key: ENV["KEY"])
831
+ agent = LLM::Agent.new(llm)
832
+ agent.repl
348
833
  ```
349
834
 
350
- If you need manual control over tool execution, use
351
- [`acts_as_llm`](https://r.uby.dev/api-docs/llm.rb/LLM/ActiveRecord/ActsAsLLM.html)
352
- instead. It wraps
353
- [`LLM::Context`](https://r.uby.dev/api-docs/llm.rb/LLM/Context.html) with the
354
- same persistence contract.
835
+ [Back to top](#table-of-contents)
836
+
837
+ ## Images
355
838
 
356
- ## Embeddings
839
+ The OpenAI, Google, xAI, DeepInfra, and DeepSeek providers have
840
+ builtin image generation capabilities. OpenAI, xAI, and DeepInfra
841
+ also support image edits. Google only supports image generation.
842
+ DeepSeek supports generation and edits too, but only through SVG
843
+ output rather than raster image models.
357
844
 
358
- #### Vector
845
+ #### Generation
359
846
 
360
- Embeddings turn text into vectors. Call `.embed` on any provider that supports
361
- it. The returned vectors can be stored in a vector-aware database (PostgreSQL
362
- with pgvector, SQLite with `vec0`, or a dedicated vector database) and
363
- compared by semantic similarity.
847
+ The [`LLM::Provider#images`](https://r.uby.dev/api-docs/llm.rb/LLM/Provider.html#images-instance_method)
848
+ method returns an Image
849
+ object that a subset of providers implement. At the
850
+ moment Google, xAI, OpenAI, DeepInfra, and DeepSeek have image
851
+ generation capabilities. DeepSeek is the odd one out: it generates
852
+ SVG documents rather than raster images.
364
853
 
365
854
  ```ruby
855
+ require "llm"
856
+
857
+ ##
858
+ # Store dogrocket.png
366
859
  llm = LLM.openai(key: ENV["KEY"])
367
- res = llm.embed("llm.rb manages providers, agents, tools, and state")
368
- puts res.model
369
- puts res.embeddings.first.size
860
+ res = llm.images.create(prompt: "a dog on a rocket to the moon")
861
+ IO.copy_stream res.images[0], "dogrocket.png"
370
862
  ```
371
863
 
372
- Embed multiple texts at once:
864
+ The API is the same across providers. <br>
865
+ For example &ndash; xAI:
373
866
 
374
867
  ```ruby
375
- chunks = [
376
- "LLM::Agent manages the tool loop automatically.",
377
- "LLM::Context exposes the low-level tool loop.",
378
- "MCP tools can be passed to agents as local tools."
379
- ]
380
-
381
- res = llm.embed(chunks)
382
- res.embeddings.each_with_index { |vec, i| puts "Vector #{i}: #{vec.size} dimensions" }
868
+ require "llm"
869
+
870
+ ##
871
+ # Store dogrocket.png
872
+ # Same API as OpenAI
873
+ llm = LLM.xai(key: ENV["KEY"])
874
+ res = llm.images.create(prompt: "a dog on a rocket to the moon")
875
+ IO.copy_stream res.images[0], "dogrocket.png"
383
876
  ```
384
877
 
385
- ## Multimodal
878
+ #### Edits
386
879
 
387
- #### Image
880
+ OpenAI, xAI, and DeepInfra have the same interface for image edits. <br>
881
+ DeepSeek also supports edits, but only for SVG files. <br>
882
+ Google does not have edit image support. <br>
388
883
 
389
- Prompts can be strings, arrays, or
390
- [`LLM::Prompt`](https://r.uby.dev/api-docs/llm.rb/LLM/Prompt.html) objects.
391
- <br>
392
- Arrays let you mix text with images and other content.
884
+ ```ruby
885
+ require "llm"
886
+
887
+ ##
888
+ # Edit self.jpg and add a mustache
889
+ # Save to mustache.png
890
+ llm = LLM.openai(key: ENV["KEY"])
891
+ res = llm.images.edit(prompt: "add a mustache", image: "self.jpg")
892
+ IO.copy_stream res.images[0], "mustache.png"
893
+ ```
894
+
895
+ #### DeepSeek
896
+
897
+ The DeepSeek provider does not provide an image generation model
898
+ but it is possible to ask a text-to-text model to produce
899
+ vector graphics (SVGs), and in that limited sense, it can become
900
+ a capable text-to-image model.
393
901
 
394
902
  ```ruby
395
- agent = LLM::Agent.new(llm)
396
- agent.talk [
397
- "Describe this image",
398
- agent.image_url("https://example.com/image.png")
399
- ]
903
+ require "llm"
904
+
905
+ ##
906
+ # Edit rocket.svg and change its color
907
+ # Save to rocket-edited.svg
908
+ llm = LLM.deepseek(key: ENV["KEY"])
909
+ res = llm.images.edit(prompt: "make the rocket red", image: "rocket.svg")
910
+ IO.copy_stream res.images[0], "rocket-edited.svg"
400
911
  ```
401
912
 
402
- Attach local files directly with
403
- [`LLM::Agent#ask`](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html#ask-instance_method):
913
+ An interesting property of the DeepSeek implementation is that
914
+ it can maintain a session that can perform multiple image generations
915
+ or edits rather than just one-shot generations.
916
+
917
+ It's possible because under the hood
918
+ [`LLM::Agent`](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html),
919
+ is attached to the
920
+ [`LLM::Response`](https://r.uby.dev/api-docs/llm.rb/LLM/Response.html)
921
+ object that is returned to the caller. So the response includes an
922
+ `agent` method, and it can be carried across multiple generations.
923
+ It is specific to this endpoint though. It works like this:
404
924
 
405
925
  ```ruby
406
- agent = LLM::Agent.new(llm)
407
- puts agent.ask("Summarize this document.", with: "README.md").content
926
+ require "llm"
927
+
928
+ llm = LLM.deepseek(key: ENV["DEEPSEEK_SECRET"])
929
+ agent = nil
930
+ loop do
931
+ print "> "
932
+ prompt = $stdin.gets
933
+ res = llm.images.create(prompt:, agent:)
934
+ agent = res.agent
935
+ IO.copy_stream res.images[0], "image.svg"
936
+ print "ok: saved image.svg", "\n"
937
+ end
408
938
  ```
409
939
 
410
- ## Tracing
940
+ [Back to top](#table-of-contents)
411
941
 
412
- #### Logger
942
+ ## Audio
413
943
 
414
- Attach a tracer at the provider level to log requests and tool calls:
944
+ The audio interface defined by llm.rb describes three methods,
945
+ although not every provider implements all of them. Generally
946
+ speaking the audio interface is for text-to-speech, and
947
+ speech-to-text models.
948
+
949
+ The following providers have audio support:
950
+
951
+ * OpenAI - full support
952
+ * Google - partial support
953
+ * DeepInfra - partial support
954
+
955
+ #### text-to-speech
956
+
957
+ The `create_speech` method generates an audio clip based
958
+ on the given input. This method returns a
959
+ [`LLM::URIData`](https://r.uby.dev/api-docs/llm.rb/LLM/URIData.html)
960
+ object. OpenAI, and DeepInfra support this method.
415
961
 
416
962
  ```ruby
417
- llm.tracer = LLM::Tracer::Logger.new(llm, io: $stdout)
418
- agent = LLM::Agent.new(llm)
419
- agent.talk("Hello")
963
+ require "llm"
964
+
965
+ llm = LLM.openai(key: ENV["KEY"])
966
+ res = llm.audio.create_speech(input: "Hello world")
967
+ IO.copy_stream res.audio.decoded, "helloworld.mp3"
420
968
  ```
421
969
 
422
- ## Applications
970
+ #### speech-to-text
423
971
 
424
- #### SSH
972
+ The `create_transcription` method transcribes a given
973
+ audio clip as text. OpenAI, Google and DeepInfra support
974
+ this method.
975
+
976
+ ```ruby
977
+ require "llm"
978
+
979
+ llm = LLM.google(key: ENV["KEY"])
980
+ res = llm.audio.create_transcription(file: "helloworld.mp3")
981
+ res.text # => "Hello world"
982
+ ```
425
983
 
426
- The llm.rb runtime powers small terminal applications that you can try over
427
- SSH right now.
984
+ #### translation
985
+
986
+ The `create_translation` method translates a given audio
987
+ clip, then transcribes it as text. OpenAI, and Google
988
+ support this method.
989
+
990
+ ```ruby
991
+ require "llm"
992
+
993
+ llm = LLM.google(key: ENV["KEY"])
994
+ res = llm.audio.create_translation(file: "bomdia.mp3")
995
+ res.text # => "Good day"
996
+ ```
428
997
 
429
- | Application | Try it | Runtime |
430
- |---|---|---|
431
- | [matz](https://r.uby.dev/matz/) | `ssh matz@r.uby.dev` | [mruby-llm](https://r.uby.dev/mruby-llm/) |
432
- | [robert](https://4.4bsd.dev/robert) | `ssh robert@4.4bsd.dev` | [mruby-llm](https://r.uby.dev/mruby-llm/) |
998
+ [Back to top](#table-of-contents)