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
data/README.md CHANGED
@@ -10,115 +10,77 @@
10
10
  </a>
11
11
  </p>
12
12
 
13
- > A [r.uby.dev](https://r.uby.dev) project.
13
+ > A [r.uby.dev](https://r.uby.dev/llm) project.
14
14
 
15
- Ruby's capable AI runtime.
15
+ Welcome to the canonical llm.rb repository.
16
16
 
17
- It provides one Ruby interface for building with large language models:
18
- providers, agents, tools, skills, MCP, A2A, RAG, streaming, files, and persisted conversation state all share the same runtime.
17
+ llm.rb is not a library, framework or toolkit but an advanced runtime
18
+ for building highly capable AI applications on CRuby. By default
19
+ it has zero runtime dependencies although certain functionality &ndash;
20
+ such as ActiveRecord support &ndash; require optional dependencies
21
+ that are opt-in.
19
22
 
20
- The gem runs on Ruby's standard library by default and loads optional
21
- integrations only when needed. It supports OpenAI, OpenAI-compatible
22
- endpoints, Anthropic, Google Gemini, DeepSeek, xAI, Z.ai, AWS Bedrock,
23
- Ollama, and llama.cpp, with built-in ActiveRecord and Sequel support.
23
+ ## Features
24
24
 
25
- ## Services
25
+ The runtime supports OpenAI, OpenAI-compatible endpoints, Anthropic, Google
26
+ Gemini, DeepSeek, DeepInfra, xAI, Z.ai, AWS Bedrock, Ollama, and llama.cpp.
27
+ It has first-class support for streaming, tool calls, MCP
28
+ and A2A, embeddings, vector stores and the RAG pattern.
26
29
 
27
- llm.rb is a [r.uby.dev](https://r.uby.dev) project
28
- that is part of a growing family of AI-related
29
- projects that also includes publically accessible
30
- SSH services.
30
+ There are multiple HTTP backends to choose from, tools can be run concurrently
31
+ or in parallel via threads, async tasks, fibers, ractors, and fork, and it is
32
+ also possible to make a tool call while the model is still streaming.
31
33
 
32
- #### matz - the mruby expert
34
+ The runtime builds on top of three core concepts: providers, contexts, and agents,
35
+ so once you learn the fundamentals, everything else falls into place naturally. And once
36
+ you learn llm.rb, you will also be able to use <a href="https://r.uby.dev/mruby-llm">mruby-llm</a> and
37
+ <a href="https://r.uby.dev/wasm-llm">wasm-llm</a> because the API is pretty much identical.
33
38
 
34
- > ssh matz@r.uby.dev
39
+ ## Install
35
40
 
36
- See [https://r.uby.dev/matz](https://r.uby.dev/matz) for more information.
37
-
38
- #### robert - the freebsd expert
39
-
40
- > ssh robert@4.4bsd.dev
41
-
42
- See [https://4.4bsd.dev/robert](https://4.4bsd.dev/robert) for more information.
41
+ ```bash
42
+ gem install llm.rb
43
+ ```
43
44
 
44
45
  ## Quick start
45
46
 
46
- #### LLM::Context
47
-
48
- The
49
- [LLM::Context](https://r.uby.dev/api-docs/llm.rb/LLM/Context.html)
50
- object is at the heart of the runtime. Almost all other features build
51
- on top of it. It is a low-level interface to a model, and requires tool
52
- execution to be managed manually. The
53
- [LLM::Agent](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html)
54
- class is almost the same as
55
- [LLM::Context](https://r.uby.dev/api-docs/llm.rb/LLM/Context.html)
56
- but it manages tool execution for you - we'll cover agents next:
57
-
58
- ```ruby
59
- require "llm"
60
-
61
- llm = LLM.openai(key: ENV["KEY"])
62
- ctx = LLM::Context.new(llm, stream: $stdout)
63
- ctx.talk "Hello world"
64
- ```
65
-
66
47
  #### LLM::Agent
67
48
 
68
- The
69
- [LLM::Agent](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html)
70
- object is implemented on top of
71
- [LLM::Context](https://r.uby.dev/api-docs/llm.rb/LLM/Context.html).
72
- It provides the same interface, but manages tool execution for you. It
73
- also has builtin features such as a loop guard that detects repeated
74
- tool call patterns, and another guard that detects infinite tool call
75
- loops. Both guards advise the model to change course rather than raise
76
- an error:
49
+ The [`LLM::Agent`](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html) class is the default high-level interface,
50
+ and it is recommended for most use-cases. It manages tool execution
51
+ automatically, guards against infinite loops, manages conversation
52
+ state, and much more.
77
53
 
78
54
  ```ruby
79
55
  require "llm"
80
56
 
81
- llm = LLM.openai(key: ENV["KEY"])
57
+ llm = LLM.deepseek(key: ENV["KEY"])
82
58
  agent = LLM::Agent.new(llm, stream: $stdout)
83
59
  agent.talk "Hello world"
84
60
  ```
85
61
 
86
- #### Agents (Advanced)
62
+ #### LLM::Context
87
63
 
88
- An agent can be configured to require confirmation before a tool is
89
- executed. When a matching tool is called, llm.rb runs
90
- `on_tool_confirmation`. That callback must decide whether to cancel the
91
- tool call or approve it and execute it by calling
92
- `fn.spawn(strategy).wait`, and it must always return an instance of
93
- [`LLM::Function::Return`](https://r.uby.dev/api-docs/llm.rb/LLM/Function/Return.html):
64
+ The [`LLM::Context`](https://r.uby.dev/api-docs/llm.rb/LLM/Context.html) class is at the heart of the runtime
65
+ and it is what [`LLM::Agent`](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html) uses under the hood.
66
+ It requires that the tool call loop be managed manually -
67
+ sometimes that can be useful, but usually for advanced use-cases.
68
+ If you're new to llm.rb, try [`LLM::Agent`](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html) first.
94
69
 
95
70
  ```ruby
96
71
  require "llm"
97
72
 
98
- class Agent < LLM::Agent
99
- tools DeleteFile
100
- confirm "delete-file"
101
-
102
- def on_tool_confirmation(fn, strategy)
103
- path = fn.arguments.path
104
- if path.start_with?("/tmp/")
105
- fn.spawn(strategy).wait
106
- else
107
- fn.cancel(reason: "Deletion requires approval")
108
- end
109
- end
110
- end
111
-
112
- llm = LLM.openai(key: ENV["KEY"])
113
- Agent.new(llm, stream: $stdout).talk("Delete /tmp/example.txt.")
73
+ llm = LLM.deepseek(key: ENV["KEY"])
74
+ ctx = LLM::Context.new(llm, stream: $stdout)
75
+ ctx.talk "Hello world"
114
76
  ```
115
77
 
116
- #### Tools
78
+ #### LLM::Tool
117
79
 
118
- The
119
- [LLM::Tool](https://r.uby.dev/api-docs/llm.rb/LLM/Tool.html)
120
- class can be subclassed to implement your own tools that can extend the
121
- abilities of a model:
80
+ Subclasses of [`LLM::Tool`](https://r.uby.dev/api-docs/llm.rb/LLM/Tool.html) are plain Ruby classes with
81
+ an optional set of typed parameters. <br> The model can choose to
82
+ call them on your behalf, and they're one of the most powerful features
83
+ for extending the feature set or abilities of a model.
122
84
 
123
85
  ```ruby
124
86
  class ReadFile < LLM::Tool
@@ -128,629 +90,277 @@ class ReadFile < LLM::Tool
128
90
  required %i[path]
129
91
 
130
92
  def call(path:)
131
- { contents: File.read(path) }
93
+ {contents: File.read(path)}
132
94
  end
133
95
  end
134
96
  ```
135
97
 
136
- #### MCP
137
-
138
- The
139
- [LLM::MCP](https://r.uby.dev/api-docs/llm.rb/LLM/MCP.html)
140
- object lets llm.rb use tools provided by an MCP server. Those tools are
141
- exposed through the same runtime as local tools, so you can pass them
142
- to either
143
- [LLM::Context](https://r.uby.dev/api-docs/llm.rb/LLM/Context.html)
144
- or
145
- [LLM::Agent](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html).
146
- In this example, the MCP server runs over stdio and
147
- [LLM::Agent](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html)
148
- manages the tool loop. For **stdio**, `mcp.session` is the preferred
149
- pattern because it keeps one MCP session alive across discovery and
150
- tool calls:
151
-
152
- ```ruby
153
- require "llm"
154
-
155
- llm = LLM.openai(key: ENV["KEY"])
156
- mcp = LLM::MCP.stdio(argv: ["ruby", "server.rb"])
157
-
158
- mcp.session do
159
- agent = LLM::Agent.new(llm, stream: $stdout, tools: mcp.tools)
160
- agent.talk "Use the available tools to inspect the environment."
161
- end
162
- ```
163
-
164
- MCP can also be used without `session`. Although it works it is generally
165
- not recommended for the **stdio** transport because it is inefficient
166
- to start and stop a fresh MCP process for tool discovery and each tool
167
- call:
168
-
169
- ```ruby
170
- require "llm"
171
-
172
- llm = LLM.openai(key: ENV["KEY"])
173
- mcp = LLM::MCP.stdio(argv: ["ruby", "server.rb"])
174
-
175
- agent = LLM::Agent.new(llm, tools: mcp.tools)
176
- agent.talk("Use the available tools to inspect the environment.")
177
- ```
178
-
179
- The HTTP transport can be used with or without the `session` method,
180
- and unlike the stdio transport it can remain efficient without the
181
- `session` method through a persistent connection pool that is available
182
- through the
183
- [LLM::Transport.net_http_persistent](https://r.uby.dev/api-docs/llm.rb/LLM/Transport.html#method-c-net_http_persistent)
184
- transport:
185
-
186
- ```ruby
187
- require "llm"
188
-
189
- llm = LLM.openai(key: ENV["KEY"])
190
- mcp = LLM::MCP.http(
191
- url: "https://remote-mcp.example.com",
192
- transport: :net_http_persistent
193
- )
194
-
195
- agent = LLM::Agent.new(llm, tools: mcp.tools)
196
- agent.talk("Use the available tools to inspect the environment.")
197
- ```
198
-
199
- #### A2A (Agent 2 Agent)
200
-
201
- The
202
- [LLM::A2A](https://r.uby.dev/api-docs/llm.rb/LLM/A2A.html)
203
- object lets llm.rb use skills provided by a remote A2A agent. Those
204
- skills are exposed through the same runtime as local tools, so you can
205
- pass them to either
206
- [LLM::Context](https://r.uby.dev/api-docs/llm.rb/LLM/Context.html)
207
- or
208
- [LLM::Agent](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html).
209
-
210
- Use remote skills as local tools:
211
-
212
- ```ruby
213
- require "llm"
214
-
215
- a2a = LLM::A2A.rest(
216
- url: "https://remote-agent.example.com",
217
- headers: { "Authorization" => "Bearer token" }
218
- )
219
- llm = LLM.openai(key: ENV["KEY"])
220
- agent = LLM::Agent.new(llm, tools: a2a.skills)
221
- agent.talk "Analyze this CSV and summarize the trends."
222
- ```
223
-
224
- Use persistent HTTP connections:
225
-
226
- ```ruby
227
- require "llm"
228
-
229
- a2a = LLM::A2A.rest(
230
- url: "https://remote-agent.example.com",
231
- transport: :net_http_persistent
232
- )
233
- ```
234
-
235
- For more on direct messaging, task operations, push notification
236
- configs, and JSON-RPC, see the
237
- [LLM::A2A API docs](https://r.uby.dev/api-docs/llm.rb/LLM/A2A.html).
238
-
239
- #### Transports
240
-
241
- Providers use Ruby's standard library Net::HTTP transport by default.
242
- You can opt into persistent Net::HTTP connections with `persistent: true`,
243
- or provide a transport shortcut when you want a different backend.
244
- `transport: :curb` uses libcurl through the optional `curb` gem.
245
-
246
- Custom transports can implement the
247
- [LLM::Transport](https://r.uby.dev/api-docs/llm.rb/LLM/Transport.html)
248
- interface and receive transport-agnostic
249
- [LLM::Transport::Request](https://r.uby.dev/api-docs/llm.rb/LLM/Transport/Request.html)
250
- objects from providers.
251
-
252
- ```ruby
253
- require "llm"
254
-
255
- llm = LLM.openai(key: ENV["KEY"], persistent: true)
256
- llm = LLM.openai(key: ENV["KEY"], transport: :net_http_persistent)
257
- llm = LLM.openai(key: ENV["KEY"], transport: :curb)
258
- ```
259
-
260
- #### Skills
261
-
262
- Skills are reusable instructions loaded from a `SKILL.md` directory. They let
263
- you package behavior and tool access together, and they plug into the
264
- same runtime as tools, agents, MCP, and A2A. When a skill runs, llm.rb
265
- spawns a subagent with the skill instructions, access to only the tools
266
- listed in the skill, and recent conversation context:
267
-
268
- ```yaml
269
- ---
270
- name: release
271
- description: Prepare a release
272
- tools: ["search-docs", "git"]
273
- ---
274
-
275
- ## Task
276
-
277
- Review the release state, summarize what changed, and prepare the release.
278
- ```
279
-
280
- ```ruby
281
- require "llm"
282
-
283
- class ReleaseAgent < LLM::Agent
284
- model "gpt-5.4-mini"
285
- skills "./skills/release"
286
- end
287
-
288
- llm = LLM.openai(key: ENV["KEY"])
289
- ReleaseAgent.new(llm, stream: $stdout).talk("Prepare the next release.")
290
- ```
291
-
292
- A skill can also have its sub-agent inherit the parents tools through the
293
- `inherit` directive. The `inherit` directive has coverage for the "classic"
294
- tools (a subclass of [LLM::Tool](https://r.uby.dev/api-docs/llm.rb/LLM/Tool.html)),
295
- MCP tools, and A2A tools that a parent context or agent has access to:
296
-
297
- ```yaml
298
- ---
299
- name: release
300
- description: Prepare a release
301
- tools: inherit
302
- ---
303
- ```
304
-
305
98
  #### LLM::Stream
306
99
 
307
- The
308
- [LLM::Stream](https://r.uby.dev/api-docs/llm.rb/LLM/Stream.html)
309
- object lets you observe output and runtime events as they happen. You
310
- can subclass it to handle streamed content in your own application:
100
+ Streams can be simple IO objects or subclasses of
101
+ [`LLM::Stream`](https://r.uby.dev/api-docs/llm.rb/LLM/Stream.html) with structured callbacks for content,
102
+ reasoning, tool calls, tool returns, and compaction.
311
103
 
312
104
  ```ruby
313
- require "llm"
314
-
315
- class Stream < LLM::Stream
105
+ class MyStream < LLM::Stream
316
106
  def on_content(content)
317
- $stdout << content
107
+ print content
318
108
  end
319
- end
320
-
321
- llm = LLM.openai(key: ENV["KEY"])
322
- agent = LLM::Agent.new(llm, stream: Stream.new)
323
- agent.talk "Write a haiku about Ruby."
324
- ```
325
109
 
326
- #### LLM::Stream (advanced)
327
-
328
- The
329
- [LLM::Stream](https://r.uby.dev/api-docs/llm.rb/LLM/Stream.html)
330
- object can also resolve tool calls while output is still streaming. In
331
- `on_tool_call`, you can spawn the tool, push the work onto the stream
332
- queue, and later drain it with `wait`:
333
-
334
- ```ruby
335
- require "llm"
336
-
337
- class Stream < LLM::Stream
338
- def on_content(content)
339
- $stdout << content
340
- end
341
-
342
- def on_tool_call(tool, error)
343
- return queue << error if error
344
- queue << ctx.spawn(tool, :thread)
110
+ def on_reasoning_content(content)
111
+ warn content
345
112
  end
346
113
  end
347
114
 
348
- llm = LLM.openai(key: ENV["KEY"])
349
- ctx = LLM::Context.new(llm, stream: Stream.new, tools: [ReadFile])
350
- ctx.talk "Read README.md and summarize the quick start."
351
- ctx.talk(ctx.wait) while ctx.functions?
115
+ llm = LLM.deepseek(key: ENV["KEY"])
116
+ agent = LLM::Agent.new(llm, stream: MyStream.new)
117
+ agent.talk "Explain Ruby fibers."
352
118
  ```
353
119
 
354
- #### Concurrency
120
+ #### LLM::REPL
355
121
 
356
- llm.rb can run tool work concurrently. This is useful when a model calls
357
- multiple tools and you want to resolve them in parallel instead of one
358
- at a time. On
359
- [LLM::Agent](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html),
360
- you can enable this with `concurrency`. Common options are `:call` for
361
- sequential execution, `:thread`, or `:task` for concurrent IO-bound work, and
362
- `:ractor` or `:fork` for more isolated CPU-bound work:
122
+ The [LLM::Agent#repl](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html#repl-instance_method)
123
+ method allows an agent to spawn a read-eval-print loop
124
+ that can be useful during the development and operation
125
+ of agents. It can be used to debug tool calls, confirm an
126
+ agent has done what was expected, or improve the agent
127
+ by asking questions about what it has done up to that point.
363
128
 
364
- ```ruby
365
- require "llm"
366
-
367
- class Agent < LLM::Agent
368
- model "gpt-5.4-mini"
369
- tools ReadFile
370
- concurrency :thread
371
- end
372
-
373
- llm = LLM.openai(key: ENV["KEY"])
374
- agent = Agent.new(llm, stream: $stdout)
375
- agent.talk "Read README.md and CHANGELOG.md and compare them."
376
- ```
377
-
378
- #### Serialization
379
-
380
- The [`LLM::Agent`](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html)
381
- object can be serialized to JSON, which makes it suitable for storing
382
- in a file, a database column, or a Redis queue. The built-in
383
- ActiveRecord and Sequel plugins are built on top of the same underlying
384
- serialization feature:
385
-
386
- ```ruby
387
- require "llm"
388
-
389
- llm = LLM.openai(key: ENV["KEY"])
390
-
391
- # Serialize an agent
392
- agent1 = LLM::Agent.new(llm)
393
- agent1.talk "Remember that my favorite language is Ruby"
394
- string = agent1.to_json
395
-
396
- # Restore an agent (from JSON)
397
- agent2 = LLM::Agent.new(llm, stream: $stdout)
398
- agent2.restore(string:)
399
- agent2.talk "What is my favorite language?"
400
- ```
401
-
402
- #### ask
403
-
404
- [`LLM::Agent`](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html)
405
- also provides `ask`, a convenience interface that is compatible with
406
- RubyLLM's `ask` method. It accepts a prompt, an optional `with:`
407
- attachment path or paths, an optional `stream:` target, and an optional
408
- block that chunks are yielded to. It returns an
409
- [`LLM::Response`](https://r.uby.dev/api-docs/llm.rb/LLM/Response.html),
410
- so use `.content` when you want the text directly:
129
+ This feature requires that the [curses](https://github.com/ruby/curses)
130
+ library is installed and available to require.
411
131
 
412
132
  ```ruby
413
- require "llm"
414
-
415
- llm = LLM.openai(key: ENV["KEY"])
133
+ llm = LLM.deepseek(key: ENV["KEY"])
416
134
  agent = LLM::Agent.new(llm)
417
-
418
- puts agent.ask("Hello world").content
419
- puts agent.ask("Summarize this document.", with: "README.md").content
420
- agent.ask("Stream this reply.") { $stdout << _1 }
421
- ```
422
-
423
- ## Installation
424
-
425
- ```bash
426
- gem install llm.rb
135
+ agent.repl
427
136
  ```
428
137
 
429
- ## Examples
138
+ #### LLM::MCP
430
139
 
431
- #### REPL
432
-
433
- This example uses [`LLM::Agent`](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html)
434
- for an interactive REPL. <br> See the
435
- [deepdive (web)](https://r.uby.dev/llm/) or
436
- [deepdive (markdown)](resources/deepdive.md) for more examples.
140
+ The Model Context Protocol (MCP) has first-class support
141
+ in llm.rb. The stdio and http transports work out of the
142
+ box. MCP tools are translated into subclasses of
143
+ [`LLM::Tool`](https://r.uby.dev/api-docs/llm.rb/LLM/Tool.html) that can be used with [`LLM::Context`](https://r.uby.dev/api-docs/llm.rb/LLM/Context.html)
144
+ or [`LLM::Agent`](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html).
437
145
 
438
146
  ```ruby
439
147
  require "llm"
440
148
 
441
- llm = LLM.openai(key: ENV["KEY"])
442
- agent = LLM::Agent.new(llm, stream: $stdout)
443
-
444
- loop do
445
- print "> "
446
- agent.talk(STDIN.gets || break)
447
- puts
448
- end
149
+ llm = LLM.deepseek(key: ENV["KEY"])
150
+ mcp = LLM::MCP.stdio(argv: ["ruby", "server.rb"])
151
+ agent = LLM::Agent.new(llm, stream: $stdout, tools: mcp.tools)
152
+ agent.talk "Run the tool"
449
153
  ```
450
154
 
451
- #### Multimodal: Local Files
452
-
453
- In llm.rb, a prompt can be a string, an [`LLM::Prompt`](https://r.uby.dev/api-docs/llm.rb/LLM/Prompt.html), or an array.
454
- When you use an array, each element can be plain text or a tagged object such as
455
- [`agent.image_url(...)`](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html#image_url-instance_method),
456
- [`agent.local_file(...)`](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html#local_file-instance_method),
457
- or [`agent.remote_file(...)`](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html#remote_file-instance_method).
458
- Those tagged objects carry the metadata the provider adapter needs to turn one
459
- Ruby prompt into the provider-specific multimodal request schema.
460
-
461
- If the model understands that file type, you can attach a local file directly
462
- with `agent.ask(..., with: path)` instead of uploading it first through a
463
- provider Files API. Under the hood, llm.rb tags the path as a
464
- [`agent.local_file(...)`](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html#local_file-instance_method)
465
- object:
466
-
467
- ```ruby
468
- require "llm"
469
-
470
- llm = LLM.openai(key: ENV["KEY"])
471
- agent = LLM::Agent.new(llm)
472
- puts agent.ask("Summarize this document.", with: "README.md").content
473
- ```
155
+ #### LLM::A2A
474
156
 
475
- #### Context Compaction
476
-
477
- This example uses [`LLM::Agent`](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html),
478
- [`LLM::Compactor`](https://r.uby.dev/api-docs/llm.rb/LLM/Compactor.html), and
479
- [`LLM::Stream`](https://r.uby.dev/api-docs/llm.rb/LLM/Stream.html) together so
480
- long-lived conversations can summarize older history and expose the lifecycle
481
- through stream hooks. This approach is inspired by General Intelligence
482
- Systems. The
483
- compactor can also use its own `model:` if you want summarization to run on a
484
- different model from the main conversation. `token_threshold:` accepts either a
485
- fixed token count or a percentage string like `"90%"`, which resolves
486
- against the active model context window and triggers compaction once total
487
- token usage goes over that percentage. See the
488
- [deepdive (web)](https://r.uby.dev/llm/) or
489
- [deepdive (markdown)](resources/deepdive.md) for more examples.
157
+ The Agent 2 Agent (A2A) protocol has first-class support
158
+ in llm.rb. The http and jsonrpc transports work out of the
159
+ box. A2A skills are translated into subclasses of
160
+ [`LLM::Tool`](https://r.uby.dev/api-docs/llm.rb/LLM/Tool.html) that can be used with [`LLM::Context`](https://r.uby.dev/api-docs/llm.rb/LLM/Context.html)
161
+ or [`LLM::Agent`](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html).
490
162
 
491
163
  ```ruby
492
164
  require "llm"
493
165
 
494
- class Stream < LLM::Stream
495
- def on_compaction(ctx, compactor)
496
- puts "Compacting #{ctx.messages.size} messages..."
497
- end
498
-
499
- def on_compaction_finish(ctx, compactor)
500
- puts "Compacted to #{ctx.messages.size} messages."
501
- end
502
- end
503
-
504
- llm = LLM.openai(key: ENV["KEY"])
505
- agent = LLM::Agent.new(
506
- llm,
507
- stream: Stream.new,
508
- compactor: {
509
- token_threshold: "90%",
510
- retention_window: 8,
511
- model: "gpt-5.4-mini"
512
- }
513
- )
166
+ llm = LLM.deepseek(key: ENV["KEY"])
167
+ a2a = LLM::A2A.rest(url: "https://remote-agent.example.com")
168
+ agent = LLM::Agent.new(llm, stream: $stdout, tools: a2a.skills)
169
+ agent.talk "Run the skill"
514
170
  ```
515
171
 
516
- #### Reasoning
172
+ #### RAG
517
173
 
518
- This example uses [`LLM::Stream`](https://r.uby.dev/api-docs/llm.rb/LLM/Stream.html)
519
- with the OpenAI Responses API so reasoning output is streamed separately from
520
- visible assistant output. See the
521
- [deepdive (web)](https://r.uby.dev/llm/) or
522
- [deepdive (markdown)](resources/deepdive.md) for more examples.
174
+ Most providers offer an embedding model that can be
175
+ used for semantic search, or similarity search. An
176
+ embedding model can generate embeddings that can then
177
+ be stored in a database that is optimized for storing
178
+ and querying vectors, such as SQLite's [sqlite-vec](https://github.com/asg017/sqlite-vec)
179
+ or PostgreSQL's [pg-vector](https://github.com/pgvector/pgvector).
523
180
 
524
- To use the Responses API (OpenAI-specific), initialize an agent with
525
- `mode: :responses` and keep using `talk` for turns.
181
+ llm.rb also includes support for OpenAI's vector store API. It
182
+ provides a vector database as a HTTP service but we won't cover
183
+ that here.
526
184
 
527
185
  ```ruby
528
186
  require "llm"
529
187
 
530
- class Stream < LLM::Stream
531
- def on_content(content)
532
- $stdout << content
533
- end
534
-
535
- def on_reasoning_content(content)
536
- $stderr << content
537
- end
538
- end
188
+ llm = LLM.openai(key: ENV["KEY"])
189
+ body = "llm.rb is Ruby's capable AI runtime."
190
+ embedding = llm.embed([body]).embeddings.first
539
191
 
540
- llm = LLM.openai(key: ENV["KEY"])
541
- agent = LLM::Agent.new(
542
- llm,
543
- model: "gpt-5.4-mini",
544
- mode: :responses,
545
- reasoning: { effort: "medium" },
546
- stream: Stream.new
192
+ Document.create!(
193
+ title: "llm.rb",
194
+ body:,
195
+ embedding:,
547
196
  )
548
- agent.talk("Solve 17 * 19 and show your work.")
549
197
  ```
550
198
 
551
- #### Request Cancellation
552
-
553
- Need to cancel a stream? llm.rb has you covered through
554
- [`LLM::Agent#interrupt!`](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html#interrupt-21-instance_method).
555
- <br> See the [deepdive (web)](https://r.uby.dev/llm/)
556
- or [deepdive (markdown)](resources/deepdive.md) for more examples.
557
-
558
- ```ruby
559
- require "llm"
560
- require "io/console"
561
-
562
- llm = LLM.openai(key: ENV["KEY"])
563
- agent = LLM::Agent.new(llm, stream: $stdout)
564
- worker = Thread.new do
565
- agent.talk("Write a very long essay about network protocols.")
566
- rescue LLM::Interrupt
567
- puts "Request was interrupted!"
568
- end
569
-
570
- STDIN.getch
571
- agent.interrupt!
572
- worker.join
573
- ```
199
+ #### Concurrency
574
200
 
575
- #### Sequel (ORM)
201
+ The runtime supports five different concurrency strategies that have
202
+ different attributes. The choice between all of them often depends
203
+ on the requirements of your application.
576
204
 
577
- The `plugin :llm` integration wraps
578
- [`LLM::Context`](https://r.uby.dev/api-docs/llm.rb/LLM/Context.html) on a
579
- `Sequel::Model` and keeps tool execution explicit. Like the ActiveRecord
580
- wrappers, its built-in persistence contract is the serialized `data` column,
581
- while `provider:` resolves a real `LLM::Provider` instance and `context:`
582
- injects defaults such as `model:`. <br> See the
583
- [deepdive (web)](https://r.uby.dev/llm/) or
584
- [deepdive (markdown)](resources/deepdive.md) for more examples.
205
+ IO-bound tools are a good fit for the `:task`, `:thread`,
206
+ and `:fiber` strategies while true parallelism can be achieved
207
+ with the `:fork` and `:ractor` strategies. The
208
+ `:fork` strategy also provides a separate process that offers
209
+ isolation from its parent.
585
210
 
586
211
  ```ruby
587
212
  require "llm"
588
- require "net/http/persistent"
589
- require "sequel"
590
- require "sequel/plugins/llm"
591
-
592
- class Context < Sequel::Model
593
- plugin :llm, provider: :set_provider, context: :set_context
594
-
595
- private
596
-
597
- def set_provider
598
- LLM.openai(key: ENV["OPENAI_SECRET"], persistent: true)
599
- end
600
-
601
- def set_context
602
- { model: "gpt-5.4-mini", mode: :responses, store: false }
603
- end
604
- end
605
213
 
606
- ctx = Context.create
607
- ctx.talk("Remember that my favorite language is Ruby")
608
- puts ctx.talk("What is my favorite language?").content
214
+ llm = LLM.deepseek(key: ENV["KEY"])
215
+ tools = [FetchNews, FetchStocks, FetchFeeds]
216
+ agent = LLM::Agent.new(llm, tools:, concurrency: :fork)
217
+ agent.talk "Run the tools in parallel"
609
218
  ```
610
219
 
611
- #### ActiveRecord (ORM): acts_as_llm
220
+ #### ORM
612
221
 
613
- The `acts_as_llm` method wraps [`LLM::Context`](https://r.uby.dev/api-docs/llm.rb/LLM/Context.html) and
614
- provides full control over tool execution. Its built-in persistence contract is
615
- one serialized `data` column. If your app has provider, model, or usage
616
- columns, provide them to llm.rb through `provider:` and `context:` instead of
617
- relying on reserved wrapper columns.
222
+ Because both [`LLM::Context`](https://r.uby.dev/api-docs/llm.rb/LLM/Context.html), and [`LLM::Agent`](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html)
223
+ can be serialized to JSON and stored in a simple string, both ActiveRecord
224
+ and Sequel support can be implemented within a single column on a single row.
618
225
 
619
- See the [deepdive (web)](https://r.uby.dev/llm/)
620
- or [deepdive (markdown)](resources/deepdive.md) for more examples.
226
+ The runtime includes first-class support for both ActiveRecord *and* Sequel, and
227
+ for both Rack-based applications *and* Rails-based applications. On databases
228
+ where it is supported, such as PostgreSQL, the column can be optimized by using
229
+ the `jsonb` type.
621
230
 
622
231
  ```ruby
623
- require "llm"
624
232
  require "active_record"
625
- require "llm/active_record"
626
-
627
- class Context < ApplicationRecord
628
- acts_as_llm provider: :set_provider, context: :set_context
629
-
630
- private
631
-
632
- def set_provider
633
- LLM.openai(key: ENV["OPENAI_SECRET"])
634
- end
635
-
636
- def set_context
637
- { model: "gpt-5.4-mini", mode: :responses, store: false }
638
- end
639
- end
640
-
641
- ctx = Context.create!
642
- ctx.talk("Remember that my favorite language is Ruby")
643
- puts ctx.talk("What is my favorite language?").content
644
- ```
645
-
646
- ```ruby
647
233
  require "llm"
648
- require "active_record"
649
234
  require "llm/active_record"
650
235
 
651
- class Context < ApplicationRecord
652
- acts_as_llm provider: :set_provider, context: :set_context
653
-
654
- # Optional application columns can still provide the provider and context.
655
- # For example, `provider_name` and `model_name` can be normal columns.
236
+ class Agent < ApplicationRecord
237
+ acts_as_agent do |agent|
238
+ agent.model "deepseek-v4-pro"
239
+ agent.instructions "solve the user's query"
240
+ agent.tools [Research, FinalizeResearch, ActOnResearch]
241
+ end
656
242
 
657
243
  private
658
244
 
245
+ # By convention, this method defines the provider for a model.
246
+ # If necessary, it can be renamed with: provider: :your_method.
659
247
  def set_provider
660
- LLM.public_send(provider_name, key: provider_key)
248
+ LLM.deepseek(key: ENV["KEY"])
661
249
  end
662
250
 
251
+ # By convention, this method returns the context options given
252
+ # to LLM::Context or LLM::Agent.
663
253
  def set_context
664
- { model: model_name, mode: :responses, store: false }
254
+ {}
665
255
  end
666
256
  end
667
- ```
668
-
669
- #### ActiveRecord (ORM): acts_as_agent
670
257
 
671
- The `acts_as_agent` method wraps [`LLM::Agent`](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html) and
672
- manages tool execution for you. Like `acts_as_llm`, its built-in persistence
673
- contract is one serialized `data` column. If your app has provider or model
674
- columns, provide them to llm.rb through your hooks and agent DSL.
675
-
676
- See the [deepdive (web)](https://r.uby.dev/llm/)
677
- or [deepdive (markdown)](resources/deepdive.md) for more examples.
258
+ agent = Agent.create!
259
+ agent.talk "perform research"
260
+ ```
678
261
 
679
- ```ruby
680
- require "llm"
681
- require "active_record"
682
- require "llm/active_record"
262
+ ## FAQ
683
263
 
684
- class Ticket < ApplicationRecord
685
- acts_as_agent provider: :set_provider, context: :set_context
686
- model "gpt-5.4-mini"
687
- instructions "You are a concise support assistant."
688
- tools SearchDocs, Escalate
689
- concurrency :thread
264
+ <details>
265
+ <summary>What providers does llm.rb support?</summary>
266
+ <br>
267
+ <p>
268
+ China-based
690
269
 
691
- private
270
+ * DeepSeek
271
+ * zAI
692
272
 
693
- def set_provider
694
- LLM.openai(key: ENV["OPENAI_SECRET"])
695
- end
273
+ US-based
696
274
 
697
- def set_context
698
- { mode: :responses, store: false }
699
- end
700
- end
275
+ * OpenAI
276
+ * Google (Gemini)
277
+ * xAI
278
+ * AWS bedrock
279
+ * DeepInfra
280
+ * Anthropic
701
281
 
702
- ticket = Ticket.create!
703
- puts ticket.talk("How do I rotate my API key?").content
704
- ```
282
+ Openweights
705
283
 
706
- ```ruby
707
- require "llm"
708
- require "active_record"
709
- require "llm/active_record"
284
+ * DeepSeek
285
+ * zAI
286
+ * DeepInfra
287
+ * AWS bedrock
710
288
 
711
- class Ticket < ApplicationRecord
712
- acts_as_agent provider: :set_provider, context: :set_context
713
- model "gpt-5.4-mini"
714
- instructions "You are a concise support assistant."
289
+ Host your own
715
290
 
716
- private
717
-
718
- def set_provider
719
- LLM.public_send(provider_name, key: provider_key)
720
- end
291
+ * Ollama
292
+ * Llamacpp
293
+ </p>
294
+ </details>
721
295
 
722
- def set_context
723
- { mode: :responses, store: false }
724
- end
725
- end
726
- ```
296
+ <details>
297
+ <summary>I have a limited budget. What should I do?</summary>
298
+ <br>
299
+ <p>
300
+ There a few options. The first option is to host
301
+ your own model, and use the ollama or llamacpp
302
+ providers. This can be diffilcult though because
303
+ a capable model requires hardware that can
304
+ match it. If you have the ability to self-host,
305
+ this would be my first option.
306
+ </p>
307
+ <p>
308
+ The second option is DeepSeek. <br>
309
+ The deepseek-v4-flash model costs pennies to use. <br>
310
+ And llm.rb has been optimized for deepseek. For example,
311
+ DeepSeek does not have image generation capabilities
312
+ but on the llm.rb runtime it does (vector graphics only,
313
+ though).
314
+ </p>
315
+ <p>
316
+ The same is true for structured outputs. DeepSeek does
317
+ not support structured outputs in the same way as OpenAI or
318
+ Google, but the llm.rb runtime makes it appear as
319
+ though it does, through the `json_object` response
320
+ type.
321
+ </p>
322
+ If you're on a budget, DeepSeek is hard to beat.
323
+ </details>
324
+ <details>
325
+ <summary>Can I download llm.rb via a decentralized network?</summary>
326
+ <br>
327
+ You can!
328
+ <br>
329
+ We are on the <a href="https://radicle.network">radicle.network</a>
330
+ <br>
331
+ Every commit that lands on GitHub also lands on Radicle.
332
+ <br>
333
+ Our repository ID is z2PtfQ6dYwyYaW2aGrztG1sMyDmCE.
334
+ <br>
335
+ Browse on <a href="https://radicle.network/nodes/iris.radicle.network/z2PtfQ6dYwyYaW2aGrztG1sMyDmCE">the web</a>.
336
+ </details>
727
337
 
728
- #### MCP
338
+ ## Resources
729
339
 
730
- This example uses [`LLM::MCP`](https://r.uby.dev/api-docs/llm.rb/LLM/MCP.html)
731
- over HTTP so remote GitHub MCP tools run through the same
732
- `LLM::Agent` tool path as local tools. It expects a GitHub token in
733
- `ENV["GITHUB_PAT"]`. See the
734
- [deepdive (web)](https://r.uby.dev/llm/) or
735
- [deepdive (markdown)](resources/deepdive.md) for more examples.
340
+ If you like what you read so far, check out the [deepdive.md](https://r.uby.dev/llm/deepdive/)
341
+ to learn more. Unfortunately it
342
+ wasn't possible to cover every feature without the README becoming a small book.
343
+ The [r.uby.dev](https://r.uby.dev) homepage also includes more learning material
344
+ and resources.
736
345
 
737
- ```ruby
738
- require "llm"
739
- require "net/http/persistent"
346
+ ## License
740
347
 
741
- llm = LLM.openai(key: ENV["KEY"], persistent: true)
742
- mcp = LLM::MCP.http(
743
- url: "https://api.githubcopilot.com/mcp/",
744
- headers: { "Authorization" => "Bearer " + ENV["GITHUB_PAT"].to_s },
745
- persistent: true
746
- )
348
+ [Business Source License 1.1](./LICENSE)
349
+ <br>
350
+ Commercial production use requires a commercial license.
351
+ <br>
352
+ Each version converts to the [BSD Zero Clause](https://choosealicense.com/licenses/0bsd/)
353
+ four years after its first public release.
354
+ <br>
355
+ Contact [robert@r.uby.dev](mailto:robert@r.uby.dev) for a commercial license.
747
356
 
748
- agent = LLM::Agent.new(llm, stream: $stdout, tools: mcp.tools)
749
- agent.talk("Pull information about my GitHub account.")
750
- ```
357
+ ### Waivers
751
358
 
752
- ## License
359
+ Waivers are automatically granted for: <br>
753
360
 
754
- [BSD Zero Clause](https://choosealicense.com/licenses/0bsd/)
755
- <br>
756
- See [LICENSE](./LICENSE)
361
+ * Personal use
362
+ * Students
363
+ * Teachers
364
+ * Evaluation, development, and testing
365
+ * Non-profits and charities
366
+ * Companies with less than or equal to 50 employees