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