llm.rb 13.1.0 → 15.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +716 -1826
- data/README.md +668 -253
- data/bin/llm.rb +156 -56
- data/data/alibaba.json +1999 -0
- data/data/anthropic.json +195 -252
- data/data/bedrock.json +2189 -1854
- data/data/deepinfra.json +1312 -728
- data/data/deepseek.json +42 -39
- data/data/google.json +1024 -404
- data/data/mistral.json +481 -401
- data/data/moonshot.json +384 -0
- data/data/openai.json +985 -1354
- data/data/xai.json +220 -115
- data/data/zai.json +166 -166
- data/docs/deepdive/advanced/cancellation.md +74 -0
- data/docs/deepdive/advanced/compaction.md +85 -0
- data/docs/deepdive/advanced/context.md +280 -0
- data/docs/deepdive/advanced/guard.md +371 -0
- data/docs/deepdive/advanced/transformer.md +67 -0
- data/docs/deepdive/advanced/transports.md +45 -0
- data/docs/deepdive/features/builtin_tools.md +191 -0
- data/docs/deepdive/features/concurrency.md +110 -0
- data/docs/deepdive/features/database.md +449 -0
- data/docs/deepdive/features/embeddings.md +157 -0
- data/docs/deepdive/features/repl.md +141 -0
- data/docs/deepdive/fundamentals/agents.md +253 -0
- data/docs/deepdive/fundamentals/providers.md +159 -0
- data/docs/deepdive/fundamentals/schema.md +61 -0
- data/docs/deepdive/fundamentals/skills.md +111 -0
- data/docs/deepdive/fundamentals/stream.md +143 -0
- data/docs/deepdive/fundamentals/tools.md +329 -0
- data/docs/deepdive/media/audio.md +122 -0
- data/docs/deepdive/media/images.md +89 -0
- data/docs/deepdive/media/ocr.md +48 -0
- data/docs/deepdive/protocols/a2a.md +106 -0
- data/docs/deepdive/protocols/mcp.md +111 -0
- data/docs/deepdive/reference/cost.md +109 -0
- data/docs/deepdive/reference/model_registry.md +271 -0
- data/docs/deepdive/reference/object.md +108 -0
- data/docs/deepdive/reference/tracer.md +187 -0
- data/{resources → docs}/deepdive.md +37 -23
- data/lib/llm/a2a/transport/http.rb +1 -1
- data/lib/llm/active_record/acts_as_llm.rb +19 -5
- data/lib/llm/agent.rb +107 -17
- data/lib/llm/context.rb +164 -134
- data/lib/llm/cost.rb +114 -49
- data/lib/llm/error.rb +7 -8
- data/lib/llm/function/array.rb +1 -1
- data/lib/llm/function/async/task.rb +2 -0
- data/lib/llm/function/fiber/task.rb +2 -0
- data/lib/llm/function/fork/task.rb +16 -1
- data/lib/llm/function/ractor/task.rb +2 -0
- data/lib/llm/function/sequential/group.rb +20 -10
- data/lib/llm/function/sequential/task.rb +2 -9
- data/lib/llm/function/task.rb +4 -0
- data/lib/llm/function/thread/task.rb +2 -0
- data/lib/llm/function.rb +33 -4
- data/lib/llm/guard/loop.rb +89 -0
- data/lib/llm/guard/null.rb +19 -0
- data/lib/llm/guard.rb +61 -0
- data/lib/llm/message.rb +5 -4
- data/lib/llm/provider.rb +43 -0
- data/lib/llm/providers/alibaba/error_handler.rb +34 -0
- data/lib/llm/providers/alibaba/request_adapter.rb +13 -0
- data/lib/llm/providers/alibaba.rb +93 -0
- data/lib/llm/providers/anthropic/stream_parser.rb +1 -0
- data/lib/llm/providers/anthropic.rb +1 -9
- data/lib/llm/providers/bedrock/stream_parser.rb +1 -0
- data/lib/llm/providers/bedrock.rb +9 -9
- data/lib/llm/providers/deepseek/request_adapter.rb +2 -33
- data/lib/llm/providers/google/stream_parser.rb +1 -0
- data/lib/llm/providers/google.rb +1 -9
- data/lib/llm/providers/moonshot.rb +76 -0
- data/lib/llm/providers/ollama.rb +1 -9
- data/lib/llm/providers/openai/responses/stream_parser.rb +1 -0
- data/lib/llm/providers/openai/responses.rb +6 -9
- data/lib/llm/providers/openai/schema.rb +37 -0
- data/lib/llm/providers/openai/stream_parser.rb +1 -0
- data/lib/llm/providers/openai.rb +4 -12
- data/lib/llm/registry/model.rb +186 -0
- data/lib/llm/registry.rb +45 -14
- data/lib/llm/repl/bar.rb +11 -12
- data/lib/llm/repl/buffer.rb +43 -16
- data/lib/llm/repl/color.rb +85 -0
- data/lib/llm/repl/command.rb +12 -0
- data/lib/llm/repl/commands/model.rb +39 -0
- data/lib/llm/repl/input/cache.rb +45 -0
- data/lib/llm/repl/input/char.rb +46 -0
- data/lib/llm/repl/input/row.rb +39 -0
- data/lib/llm/repl/input.rb +327 -79
- data/lib/llm/repl/markdown/table.rb +6 -2
- data/lib/llm/repl/markdown.rb +56 -6
- data/lib/llm/repl/node.rb +7 -0
- data/lib/llm/repl/status.rb +54 -5
- data/lib/llm/repl/stream.rb +16 -4
- data/lib/llm/repl/walker.rb +3 -2
- data/lib/llm/repl/window.rb +111 -11
- data/lib/llm/repl.rb +47 -21
- data/lib/llm/sequel/plugin.rb +19 -5
- data/lib/llm/skill.rb +21 -8
- data/lib/llm/stream.rb +35 -7
- data/lib/llm/tool.rb +27 -0
- data/lib/llm/tools/rg.rb +2 -1
- data/lib/llm/transformer/null.rb +21 -0
- data/lib/llm/transformer.rb +55 -0
- data/lib/llm/transport/curb.rb +23 -3
- data/lib/llm/usage.rb +155 -9
- data/lib/llm/version.rb +1 -1
- data/lib/llm.rb +121 -29
- data/llm.gemspec +16 -10
- metadata +100 -13
- data/lib/llm/loop_guard.rb +0 -107
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
|
|
2
|
+
## MCP
|
|
3
|
+
|
|
4
|
+
### Introduction
|
|
5
|
+
|
|
6
|
+
#### Overview
|
|
7
|
+
|
|
8
|
+
The Model Context Protocol (MCP) connects agents to external tools
|
|
9
|
+
and data sources through a standardized interface. Instead of
|
|
10
|
+
wiring each service directly into your agent, you run an MCP
|
|
11
|
+
server that exposes its capabilities. The runtime translates the
|
|
12
|
+
server's tool list into
|
|
13
|
+
[`LLM::Tool`](https://r.uby.dev/api-docs/llm.rb/LLM/Tool.html)
|
|
14
|
+
subclasses the model can call.
|
|
15
|
+
|
|
16
|
+
#### How it works
|
|
17
|
+
|
|
18
|
+
The stdio transport runs the server as a child process and
|
|
19
|
+
communicates over stdin/stdout. Use
|
|
20
|
+
[`LLM::MCP#session`](https://r.uby.dev/api-docs/llm.rb/LLM/MCP.html#session)
|
|
21
|
+
to avoid launching the same process multiple times.
|
|
22
|
+
|
|
23
|
+
```ruby
|
|
24
|
+
require "llm"
|
|
25
|
+
|
|
26
|
+
llm = LLM.deepseek(key: ENV["KEY"])
|
|
27
|
+
mcp = LLM::MCP.stdio(argv: ["npx", "-y", "@forgejo/mcp-server"])
|
|
28
|
+
agent = LLM::Agent.new(llm)
|
|
29
|
+
|
|
30
|
+
mcp.session do
|
|
31
|
+
agent.talk "What's happening on forgejo?", tools: mcp.tools
|
|
32
|
+
end
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
#### Why would I use it?
|
|
36
|
+
|
|
37
|
+
MCP decouples tool implementation from the agent. Add tools by
|
|
38
|
+
launching a new MCP server. Update them by restarting an existing
|
|
39
|
+
one. Remove them without touching the agent's code.
|
|
40
|
+
|
|
41
|
+
#### Notes
|
|
42
|
+
|
|
43
|
+
For stdio, use
|
|
44
|
+
[`LLM::MCP#session`](https://r.uby.dev/api-docs/llm.rb/LLM/MCP.html#session)
|
|
45
|
+
to avoid launching the same process multiple times. For HTTP,
|
|
46
|
+
[`LLM::MCP#session`](https://r.uby.dev/api-docs/llm.rb/LLM/MCP.html#session)
|
|
47
|
+
carries little benefit.
|
|
48
|
+
|
|
49
|
+
### HTTP
|
|
50
|
+
|
|
51
|
+
#### Overview
|
|
52
|
+
|
|
53
|
+
The HTTP transport connects to a remote MCP server over HTTP.
|
|
54
|
+
It is the right choice for cloud-hosted servers like GitHub's
|
|
55
|
+
MCP endpoint. Tools exposed by the server become
|
|
56
|
+
[`LLM::Tool`](https://r.uby.dev/api-docs/llm.rb/LLM/Tool.html)
|
|
57
|
+
subclasses the model can call. The server does not need to run
|
|
58
|
+
locally or even on the same machine. Configure headers for
|
|
59
|
+
authentication and pick a transport backend for connection
|
|
60
|
+
management.
|
|
61
|
+
|
|
62
|
+
#### How it works
|
|
63
|
+
|
|
64
|
+
When you want to connect to a remote MCP server, provide a URL and
|
|
65
|
+
optional headers. The server's tool list is fetched and translated
|
|
66
|
+
into
|
|
67
|
+
[`LLM::Tool`](https://r.uby.dev/api-docs/llm.rb/LLM/Tool.html) subclasses the model can call.
|
|
68
|
+
|
|
69
|
+
```ruby
|
|
70
|
+
require "llm"
|
|
71
|
+
|
|
72
|
+
llm = LLM.deepseek(key: ENV["KEY"])
|
|
73
|
+
mcp = LLM::MCP.http(
|
|
74
|
+
url: "https://api.githubcopilot.com/mcp/",
|
|
75
|
+
headers: {
|
|
76
|
+
"Authorization" => "Bearer #{ENV.fetch('GITHUB_PAT')}"
|
|
77
|
+
},
|
|
78
|
+
transport: :net_http_persistent
|
|
79
|
+
)
|
|
80
|
+
agent = LLM::Agent.new(llm)
|
|
81
|
+
agent.talk "What's happening on GitHub?", tools: mcp.tools
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
#### Why would I use it?
|
|
85
|
+
|
|
86
|
+
The HTTP transport connects to remote MCP servers. This matters
|
|
87
|
+
when the server is not running locally or when tools are maintained
|
|
88
|
+
by a different team and exposed as a service.
|
|
89
|
+
|
|
90
|
+
#### Notes
|
|
91
|
+
|
|
92
|
+
For HTTP,
|
|
93
|
+
[`LLM::MCP#session`](https://r.uby.dev/api-docs/llm.rb/LLM/MCP.html#session)
|
|
94
|
+
carries little benefit.
|
|
95
|
+
|
|
96
|
+
##### Persistent connections
|
|
97
|
+
|
|
98
|
+
Set `persistent: true` to reuse HTTP connections across requests
|
|
99
|
+
to the same MCP server. This uses
|
|
100
|
+
[`Net::HTTP::Persistent`](https://github.com/drbrain/net-http-persistent)
|
|
101
|
+
under the hood and avoids the overhead of opening a new TCP
|
|
102
|
+
connection for every request.
|
|
103
|
+
|
|
104
|
+
```ruby
|
|
105
|
+
mcp = LLM::MCP.http(
|
|
106
|
+
url: "https://api.githubcopilot.com/mcp/",
|
|
107
|
+
headers: {"Authorization" => "Bearer #{ENV.fetch('GITHUB_PAT')}"},
|
|
108
|
+
persistent: true
|
|
109
|
+
)
|
|
110
|
+
```
|
|
111
|
+
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
|
|
2
|
+
## LLM::Cost
|
|
3
|
+
|
|
4
|
+
### Introduction
|
|
5
|
+
|
|
6
|
+
#### Overview
|
|
7
|
+
|
|
8
|
+
[`LLM::Cost`](https://r.uby.dev/api-docs/llm.rb/LLM/Cost.html)
|
|
9
|
+
represents the approximate cost of a conversation. It breaks the
|
|
10
|
+
total down by token type, so you can see how much was spent on input,
|
|
11
|
+
output, cached tokens, reasoning, audio, and images. Cost is computed
|
|
12
|
+
from token usage and the pricing data shipped in the
|
|
13
|
+
[model registry](../reference/model_registry.md).
|
|
14
|
+
|
|
15
|
+
#### How it works
|
|
16
|
+
|
|
17
|
+
When you want to know what a conversation cost so far, call
|
|
18
|
+
[`LLM::Context#cost`](https://r.uby.dev/api-docs/llm.rb/LLM/Context.html#cost-instance_method)
|
|
19
|
+
(or
|
|
20
|
+
[`LLM::Agent#cost`](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html#cost-instance_method))
|
|
21
|
+
and read the breakdown. The REPL shows this live in its status bar
|
|
22
|
+
after every turn:
|
|
23
|
+
|
|
24
|
+
```ruby
|
|
25
|
+
llm = LLM.deepseek(key: ENV["KEY"])
|
|
26
|
+
ctx = LLM::Context.new(llm)
|
|
27
|
+
ctx.talk "Hello"
|
|
28
|
+
|
|
29
|
+
cost = ctx.cost
|
|
30
|
+
cost.input # => 0.0000042
|
|
31
|
+
cost.output # => 0.0000084
|
|
32
|
+
cost.total # => 0.0000126
|
|
33
|
+
cost.to_s # => "0.00"
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
#### Why would I use it?
|
|
37
|
+
|
|
38
|
+
Cost tracking matters in production. Monitoring spend per
|
|
39
|
+
conversation, per agent, or per provider tells you which workflows
|
|
40
|
+
are expensive and when to switch models. Log a structured breakdown
|
|
41
|
+
with
|
|
42
|
+
[`LLM::Cost#to_h`](https://r.uby.dev/api-docs/llm.rb/LLM/Cost.html#to_h-instance_method),
|
|
43
|
+
or read
|
|
44
|
+
[`LLM::Cost#total`](https://r.uby.dev/api-docs/llm.rb/LLM/Cost.html#total-instance_method)
|
|
45
|
+
for a single number.
|
|
46
|
+
|
|
47
|
+
#### Notes
|
|
48
|
+
|
|
49
|
+
Cost is an approximation based on the pricing in the
|
|
50
|
+
[model registry](../reference/model_registry.md).
|
|
51
|
+
[`LLM::Cost.from`](https://r.uby.dev/api-docs/llm.rb/LLM/Cost.html#from-class_method)
|
|
52
|
+
returns an empty cost when the model or registry cannot be found, so
|
|
53
|
+
a missing model never crashes your code.
|
|
54
|
+
|
|
55
|
+
### Reading the breakdown
|
|
56
|
+
|
|
57
|
+
#### Overview
|
|
58
|
+
|
|
59
|
+
[`LLM::Cost`](https://r.uby.dev/api-docs/llm.rb/LLM/Cost.html)
|
|
60
|
+
exposes each cost component as a reader, plus
|
|
61
|
+
[`LLM::Cost#total`](https://r.uby.dev/api-docs/llm.rb/LLM/Cost.html#total-instance_method),
|
|
62
|
+
[`LLM::Cost#to_h`](https://r.uby.dev/api-docs/llm.rb/LLM/Cost.html#to_h-instance_method),
|
|
63
|
+
and
|
|
64
|
+
[`LLM::Cost#to_s`](https://r.uby.dev/api-docs/llm.rb/LLM/Cost.html#to_s-instance_method).
|
|
65
|
+
|
|
66
|
+
#### How it works
|
|
67
|
+
|
|
68
|
+
Each component is a Float, and defaults to `0` when no tokens of
|
|
69
|
+
that type were used. The
|
|
70
|
+
[`LLM::Cost#to_h`](https://r.uby.dev/api-docs/llm.rb/LLM/Cost.html#to_h-instance_method)
|
|
71
|
+
method returns a Hash with every component and the total:
|
|
72
|
+
|
|
73
|
+
```ruby
|
|
74
|
+
cost = ctx.cost
|
|
75
|
+
|
|
76
|
+
cost.input
|
|
77
|
+
cost.output
|
|
78
|
+
cost.cache_read
|
|
79
|
+
cost.cache_write
|
|
80
|
+
cost.reasoning
|
|
81
|
+
cost.input_audio
|
|
82
|
+
cost.output_audio
|
|
83
|
+
cost.input_image
|
|
84
|
+
|
|
85
|
+
cost.to_h # => {input: 4.2e-06, output: 8.4e-06, cache_read: 0.0,
|
|
86
|
+
# cache_write: 0.0, input_audio: 0.0, output_audio: 0.0,
|
|
87
|
+
# input_image: 0.0, reasoning: 0.0, total: 1.26e-05}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
#### Why would I use it?
|
|
91
|
+
|
|
92
|
+
The per-component breakdown shows where the money goes. High cache
|
|
93
|
+
read costs suggest a conversation benefits from prompt caching.
|
|
94
|
+
High reasoning costs point at a model that thinks a lot. Log
|
|
95
|
+
[`LLM::Cost#to_h`](https://r.uby.dev/api-docs/llm.rb/LLM/Cost.html#to_h-instance_method)
|
|
96
|
+
at the end of a session to keep a spend trail.
|
|
97
|
+
|
|
98
|
+
#### Notes
|
|
99
|
+
|
|
100
|
+
[`LLM::Cost#to_s`](https://r.uby.dev/api-docs/llm.rb/LLM/Cost.html#to_s-instance_method)
|
|
101
|
+
returns the total in a compact, human-friendly format, rounded to
|
|
102
|
+
two decimals (`"0.01"`). Components that were not used are `0`, so
|
|
103
|
+
you never need to guard against `nil` when aggregating.
|
|
104
|
+
|
|
105
|
+
The REPL renders context usage as a proportion, not a cost.
|
|
106
|
+
[`LLM::Context#context_usage`](https://r.uby.dev/api-docs/llm.rb/LLM/Context.html#context_usage-instance_method)
|
|
107
|
+
returns a `Rational` of the tokens used over the context window
|
|
108
|
+
(for example `Rational(100, 10_000)`), or `nil` when the window is
|
|
109
|
+
unknown or the conversation is too short.
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
## Model registry
|
|
2
|
+
|
|
3
|
+
### Introduction
|
|
4
|
+
|
|
5
|
+
#### Overview
|
|
6
|
+
|
|
7
|
+
The model registry is a catalog of every model each provider offers,
|
|
8
|
+
shipped with llm.rb under `data/<provider>.json`. The data is sourced
|
|
9
|
+
from [models.dev](https://models.dev) and powers cost estimation,
|
|
10
|
+
context-window limits, and the `/model` auto-complete in the REPL.
|
|
11
|
+
|
|
12
|
+
[`LLM::Registry`](https://r.uby.dev/api-docs/llm.rb/LLM/Registry.html)
|
|
13
|
+
exposes that catalog, and
|
|
14
|
+
[`LLM::Registry::Model`](https://r.uby.dev/api-docs/llm.rb/LLM/Registry/Model.html)
|
|
15
|
+
represents a single model's record. A `Model` is comparable **by price**,
|
|
16
|
+
so you can sort a provider's whole catalog from cheapest to most
|
|
17
|
+
expensive. This document covers reading the registry directly; the
|
|
18
|
+
runtime uses the same data when it estimates the cost or context window
|
|
19
|
+
of a conversation.
|
|
20
|
+
|
|
21
|
+
#### How it works
|
|
22
|
+
|
|
23
|
+
Get a registry with
|
|
24
|
+
[`LLM::Registry.for`](https://r.uby.dev/api-docs/llm.rb/LLM/Registry.html#for-class_method)
|
|
25
|
+
and a provider name, or through
|
|
26
|
+
[`LLM::Provider#registry`](https://r.uby.dev/api-docs/llm.rb/LLM/Provider.html#registry-instance_method).
|
|
27
|
+
[`LLM::Context#registry`](https://r.uby.dev/api-docs/llm.rb/LLM/Context.html#registry-instance_method)
|
|
28
|
+
and
|
|
29
|
+
[`LLM::Agent#registry`](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html#registry-instance_method)
|
|
30
|
+
delegate to their provider's registry, so
|
|
31
|
+
[`LLM::Registry#keys`](https://r.uby.dev/api-docs/llm.rb/LLM/Registry.html#keys-instance_method)
|
|
32
|
+
returns the model names, and
|
|
33
|
+
[`LLM::Registry#models`](https://r.uby.dev/api-docs/llm.rb/LLM/Registry.html#models-instance_method)
|
|
34
|
+
returns the full
|
|
35
|
+
[`LLM::Registry::Model`](https://r.uby.dev/api-docs/llm.rb/LLM/Registry/Model.html)
|
|
36
|
+
objects:
|
|
37
|
+
|
|
38
|
+
```ruby
|
|
39
|
+
require "llm"
|
|
40
|
+
|
|
41
|
+
registry = LLM::Registry.for(:openai)
|
|
42
|
+
registry.keys # => ["gpt-image-2", "gpt-5.2-pro", ...]
|
|
43
|
+
registry.models # => [LLM::Registry::Model, ...]
|
|
44
|
+
registry.models.map(&:id) # => same names as #keys
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Use *`keys`* when you only need model names, and *`models`* when you want
|
|
48
|
+
to inspect pricing, limits, or capabilities.
|
|
49
|
+
|
|
50
|
+
#### Why would I use it?
|
|
51
|
+
|
|
52
|
+
The registry lets you discover what a provider offers without
|
|
53
|
+
hardcoding model names. Build a model picker, validate a model, check a
|
|
54
|
+
context window, or price a request. Because a
|
|
55
|
+
[`LLM::Registry::Model`](https://r.uby.dev/api-docs/llm.rb/LLM/Registry/Model.html)
|
|
56
|
+
is comparable by price, you can also sort a provider's catalog and offer
|
|
57
|
+
only the cheapest models that still meet a requirement.
|
|
58
|
+
|
|
59
|
+
#### Notes
|
|
60
|
+
|
|
61
|
+
Each provider ships a `data/<provider>.json` registry file. A missing
|
|
62
|
+
model raises `LLM::NoSuchModelError`, and a missing registry raises
|
|
63
|
+
`LLM::NoSuchRegistryError`. The runtime rescues both to degrade
|
|
64
|
+
gracefully, so an unknown model's context window reads as `nil` rather
|
|
65
|
+
than crashing your code.
|
|
66
|
+
|
|
67
|
+
### Reading models
|
|
68
|
+
|
|
69
|
+
#### Overview
|
|
70
|
+
|
|
71
|
+
[`LLM::Registry::Model`](https://r.uby.dev/api-docs/llm.rb/LLM/Registry/Model.html)
|
|
72
|
+
wraps one model record and exposes its identity, pricing, limits,
|
|
73
|
+
capabilities, and modalities.
|
|
74
|
+
|
|
75
|
+
#### How it works
|
|
76
|
+
|
|
77
|
+
Pick a model from
|
|
78
|
+
[`LLM::Registry#models`](https://r.uby.dev/api-docs/llm.rb/LLM/Registry.html#models-instance_method)
|
|
79
|
+
and call its readers. Note that the raw `cost`, `limit`, and `modalities`
|
|
80
|
+
readers return
|
|
81
|
+
[`LLM::Object`](https://r.uby.dev/api-docs/llm.rb/LLM/Object.html)
|
|
82
|
+
metadata objects, while the convenience accessors return plain values:
|
|
83
|
+
|
|
84
|
+
```ruby
|
|
85
|
+
require "llm"
|
|
86
|
+
|
|
87
|
+
registry = LLM::Registry.for(:openai)
|
|
88
|
+
model = registry.models.find { _1.id == "gpt-4.1" }
|
|
89
|
+
model.id # => "gpt-4.1"
|
|
90
|
+
model.name # => "GPT-4.1"
|
|
91
|
+
model.context_window # => 1_047_576
|
|
92
|
+
model.input_cost # => 2.0 (per million input tokens)
|
|
93
|
+
model.output_cost # => 8.0 (per million output tokens)
|
|
94
|
+
model.cost # => #<LLM::Object ...> raw pricing
|
|
95
|
+
model.limit # => #<LLM::Object ...> raw limits
|
|
96
|
+
model.modalities # => #<LLM::Object ...> raw input/output lists
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
#### Why would I use it?
|
|
100
|
+
|
|
101
|
+
Pricing and limits are what most applications need from a catalog.
|
|
102
|
+
`input_cost` and `output_cost` let you compare models by spend without
|
|
103
|
+
unwrapping the raw cost object, and `context_window` tells you how large
|
|
104
|
+
a conversation a model can hold. That is exactly the data the runtime
|
|
105
|
+
uses to estimate cost and track context usage.
|
|
106
|
+
|
|
107
|
+
#### Notes
|
|
108
|
+
|
|
109
|
+
`name` falls back to `id` when a model has no human-readable name.
|
|
110
|
+
`input_cost` and `output_cost` return `nil` when a model is unpriced.
|
|
111
|
+
`context_window` returns `nil` when the limit is unknown, so it is safe
|
|
112
|
+
to compare against numbers.
|
|
113
|
+
|
|
114
|
+
### Capabilities
|
|
115
|
+
|
|
116
|
+
#### Overview
|
|
117
|
+
|
|
118
|
+
[`LLM::Registry::Model`](https://r.uby.dev/api-docs/llm.rb/LLM/Registry/Model.html)
|
|
119
|
+
exposes a model's capabilities through predicate methods, so you can
|
|
120
|
+
filter a provider's catalog by what you need before you even make a
|
|
121
|
+
call.
|
|
122
|
+
|
|
123
|
+
#### How it works
|
|
124
|
+
|
|
125
|
+
Capability predicates return booleans for whether a model supports tool
|
|
126
|
+
calls, structured output, reasoning, or open weights:
|
|
127
|
+
|
|
128
|
+
```ruby
|
|
129
|
+
require "llm"
|
|
130
|
+
|
|
131
|
+
model = LLM::Registry.for(:openai).models.find { _1.id == "gpt-4.1" }
|
|
132
|
+
model.tool_call? # => true can be called as a tool
|
|
133
|
+
model.structured_output? # => true supports typed output
|
|
134
|
+
model.reasoning? # => false not a reasoning model
|
|
135
|
+
model.open_weights? # => false weights are not public
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
#### Why would I use it?
|
|
139
|
+
|
|
140
|
+
Not every model supports every feature. If you are building a model
|
|
141
|
+
picker, filter the registry to models that support tool calls and
|
|
142
|
+
structured output before offering them to a user, and only include
|
|
143
|
+
reasoning models where you want the extra capability (and cost).
|
|
144
|
+
|
|
145
|
+
#### Notes
|
|
146
|
+
|
|
147
|
+
Capabilities vary per model and per provider, and are absent from some
|
|
148
|
+
records. The predicates coerce a missing flag to `false`, so you never
|
|
149
|
+
need a `nil` check.
|
|
150
|
+
|
|
151
|
+
### Modalities
|
|
152
|
+
|
|
153
|
+
#### Overview
|
|
154
|
+
|
|
155
|
+
Modalities describe what a model can read as input and produce as
|
|
156
|
+
output: text, image, audio, video, and PDF. A model is multimodal when
|
|
157
|
+
it handles more than text.
|
|
158
|
+
|
|
159
|
+
#### How it works
|
|
160
|
+
|
|
161
|
+
A model offers direction-aware tests and a set of convenience
|
|
162
|
+
predicates. `input?("pdf")` asks whether the model *reads* PDFs,
|
|
163
|
+
`output?("image")` whether it *produces* images, and `image?`/`audio?`/
|
|
164
|
+
`pdf?`/`video?` report support on either side:
|
|
165
|
+
|
|
166
|
+
```ruby
|
|
167
|
+
require "llm"
|
|
168
|
+
|
|
169
|
+
model = LLM::Registry.for(:openai).models.find { _1.id == "gpt-4.1" }
|
|
170
|
+
model.text? # => true reads and writes text
|
|
171
|
+
model.image? # => true reads or produces images
|
|
172
|
+
model.pdf? # => true reads or produces PDFs
|
|
173
|
+
model.audio? # => false
|
|
174
|
+
model.input?("pdf") # => true reads PDFs as input
|
|
175
|
+
model.output?("image") # => false does not produce images
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
#### Why would I use it?
|
|
179
|
+
|
|
180
|
+
`text?` is a quick way to identify a general-purpose chat LLM as opposed
|
|
181
|
+
to a single-purpose generation model (such as a speech or image model),
|
|
182
|
+
and the per-modality tests let you find a model that can accept a PDF or
|
|
183
|
+
produce audio.
|
|
184
|
+
|
|
185
|
+
#### Notes
|
|
186
|
+
|
|
187
|
+
The `supports?`-style convenience predicates (`image?`, `audio?`, `pdf?`,
|
|
188
|
+
`video?`) return `true` when the model handles a modality in *either*
|
|
189
|
+
direction, so a model that only reads PDFs and writes text still reports
|
|
190
|
+
`pdf?` as `true`.
|
|
191
|
+
|
|
192
|
+
### Sorting by price
|
|
193
|
+
|
|
194
|
+
#### Overview
|
|
195
|
+
|
|
196
|
+
[`LLM::Registry::Model`](https://r.uby.dev/api-docs/llm.rb/LLM/Registry/Model.html)
|
|
197
|
+
is `Comparable` by price, so sorting a provider's models orders them
|
|
198
|
+
from cheapest to most expensive.
|
|
199
|
+
|
|
200
|
+
#### How it works
|
|
201
|
+
|
|
202
|
+
Call `sort` on
|
|
203
|
+
[`LLM::Registry#models`](https://r.uby.dev/api-docs/llm.rb/LLM/Registry.html#models-instance_method).
|
|
204
|
+
Ordering compares input cost first, then output cost. Unpriced models
|
|
205
|
+
sort to the end:
|
|
206
|
+
|
|
207
|
+
```ruby
|
|
208
|
+
require "llm"
|
|
209
|
+
|
|
210
|
+
reg = LLM::Registry.for(:openai)
|
|
211
|
+
list = reg.models.sort
|
|
212
|
+
list.first.id # => "text-embedding-3-small"
|
|
213
|
+
list.last.id # => "chatgpt-image-latest"
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
#### Why would I use it?
|
|
217
|
+
|
|
218
|
+
Offering the cheapest capable model is a common product decision. Sort
|
|
219
|
+
by price, then filter to models that satisfy your capability and
|
|
220
|
+
modality requirements, and take the first.
|
|
221
|
+
|
|
222
|
+
#### Notes
|
|
223
|
+
|
|
224
|
+
Models without a price are treated as the most expensive so they sort
|
|
225
|
+
last rather than first. If you only compare a handful of models, the
|
|
226
|
+
`<` comparison works directly (`cheap < expensive` returns `true` when
|
|
227
|
+
`cheap` is cheaper).
|
|
228
|
+
|
|
229
|
+
### Raw lookups
|
|
230
|
+
|
|
231
|
+
#### Overview
|
|
232
|
+
|
|
233
|
+
[`LLM::Registry`](https://r.uby.dev/api-docs/llm.rb/LLM/Registry.html)
|
|
234
|
+
also offers direct `model:` keyed lookups that resolve a single model and
|
|
235
|
+
return its raw metadata object.
|
|
236
|
+
|
|
237
|
+
#### How it works
|
|
238
|
+
|
|
239
|
+
[`LLM::Registry#cost`](https://r.uby.dev/api-docs/llm.rb/LLM/Registry.html#cost-instance_method),
|
|
240
|
+
[`LLM::Registry#limit`](https://r.uby.dev/api-docs/llm.rb/LLM/Registry.html#limit-instance_method),
|
|
241
|
+
and
|
|
242
|
+
[`LLM::Registry#modalities`](https://r.uby.dev/api-docs/llm.rb/LLM/Registry.html#modalities-instance_method)
|
|
243
|
+
take a `model:` keyword and return the corresponding raw
|
|
244
|
+
[`LLM::Object`](https://r.uby.dev/api-docs/llm.rb/LLM/Object.html).
|
|
245
|
+
These lookups also apply fallback resolution, so a dated or numbered
|
|
246
|
+
model id matches its base model:
|
|
247
|
+
|
|
248
|
+
```ruby
|
|
249
|
+
require "llm"
|
|
250
|
+
|
|
251
|
+
registry = LLM::Registry.for(:openai)
|
|
252
|
+
registry.limit(model: "gpt-4.1").context # => 1_047_576
|
|
253
|
+
registry.cost(model: "gpt-4.1-2025-01-01") ==
|
|
254
|
+
registry.cost(model: "gpt-4.1") # => true, dated id falls back
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
#### Why would I use it?
|
|
258
|
+
|
|
259
|
+
These methods are convenient when you already know a model's id and only
|
|
260
|
+
want one piece of raw metadata, and they are what the runtime itself
|
|
261
|
+
uses internally. For richer access, prefer the
|
|
262
|
+
[`LLM::Registry::Model`](https://r.uby.dev/api-docs/llm.rb/LLM/Registry/Model.html)
|
|
263
|
+
objects from
|
|
264
|
+
[`LLM::Registry#models`](https://r.uby.dev/api-docs/llm.rb/LLM/Registry.html#models-instance_method).
|
|
265
|
+
|
|
266
|
+
#### Notes
|
|
267
|
+
|
|
268
|
+
A missing model raises `LLM::NoSuchModelError`. The fallback matching
|
|
269
|
+
strips date suffixes (`-YYYY-MM-DD`) and collapses numbered OpenAI ids
|
|
270
|
+
(`gpt-N-XXXX` → `gpt-N`) so a slightly out-of-date model id still
|
|
271
|
+
resolves.
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
|
|
2
|
+
## LLM::Object
|
|
3
|
+
|
|
4
|
+
### Introduction
|
|
5
|
+
|
|
6
|
+
#### Overview
|
|
7
|
+
|
|
8
|
+
[`LLM::Object`](https://r.uby.dev/api-docs/llm.rb/LLM/Object.html)
|
|
9
|
+
is the hash-like object that llm.rb uses everywhere structured data
|
|
10
|
+
flows through the runtime. Response bodies, tool arguments, schema
|
|
11
|
+
results, usage and cost data, and function parameters all come back
|
|
12
|
+
as [`LLM::Object`](https://r.uby.dev/api-docs/llm.rb/LLM/Object.html)
|
|
13
|
+
instances. It is similar in spirit to OpenStruct,
|
|
14
|
+
and it was introduced after OpenStruct became a bundled gem rather
|
|
15
|
+
than a default gem in Ruby 3.5.
|
|
16
|
+
|
|
17
|
+
#### How it works
|
|
18
|
+
|
|
19
|
+
When you want to read a value from an
|
|
20
|
+
[`LLM::Object`](https://r.uby.dev/api-docs/llm.rb/LLM/Object.html),
|
|
21
|
+
use either method-style or bracket access. Keys are indifferent, so
|
|
22
|
+
strings and symbols work interchangeably:
|
|
23
|
+
|
|
24
|
+
```ruby
|
|
25
|
+
obj = LLM::Object.from(city: "Paris", temperature: 15.0)
|
|
26
|
+
|
|
27
|
+
obj.city # => "Paris"
|
|
28
|
+
obj["city"] # => "Paris"
|
|
29
|
+
obj[:city] # => "Paris"
|
|
30
|
+
obj[:temperature] # => 15.0
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Nested hashes and arrays are converted recursively, so deep chains
|
|
34
|
+
read naturally:
|
|
35
|
+
|
|
36
|
+
```ruby
|
|
37
|
+
obj = LLM::Object.from(person: {name: "John"})
|
|
38
|
+
obj.person.name # => "John"
|
|
39
|
+
obj.person.class # => LLM::Object
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
#### Why would I use it?
|
|
43
|
+
|
|
44
|
+
Most of the time you do not construct
|
|
45
|
+
[`LLM::Object`](https://r.uby.dev/api-docs/llm.rb/LLM/Object.html)
|
|
46
|
+
instances yourself. They come back from `talk`, `ask`, `embed`, and
|
|
47
|
+
every other call that returns structured data. Knowing how they
|
|
48
|
+
behave lets you read response fields, pass tool arguments, and
|
|
49
|
+
inspect usage without reaching for `to_h` on every line.
|
|
50
|
+
|
|
51
|
+
#### Notes
|
|
52
|
+
|
|
53
|
+
An
|
|
54
|
+
[`LLM::Object`](https://r.uby.dev/api-docs/llm.rb/LLM/Object.html)
|
|
55
|
+
is enumerable and supports the usual Hash operations: `keys`,
|
|
56
|
+
`values`, `key?`, `fetch`, `dig`, `slice`, `merge`, `merge!`,
|
|
57
|
+
`delete`, `size`, and `empty?`. Use `to_h` for a plain Hash and
|
|
58
|
+
`to_hash` for one with symbol keys. A missing key returns `nil`
|
|
59
|
+
rather than raising. Because it subclasses `BasicObject`, `to_json`
|
|
60
|
+
is defined explicitly and serializes through the configured JSON
|
|
61
|
+
adapter via
|
|
62
|
+
[`LLM.json.dump`](https://r.uby.dev/api-docs/llm.rb/LLM.html#json-class_method).
|
|
63
|
+
|
|
64
|
+
### Reading and writing
|
|
65
|
+
|
|
66
|
+
#### Overview
|
|
67
|
+
|
|
68
|
+
Beyond simple reads,
|
|
69
|
+
[`LLM::Object`](https://r.uby.dev/api-docs/llm.rb/LLM/Object.html)
|
|
70
|
+
supports assignment, mutation, and iteration, so you can treat it
|
|
71
|
+
like a Hash in place.
|
|
72
|
+
|
|
73
|
+
#### How it works
|
|
74
|
+
|
|
75
|
+
Assign values with method or bracket syntax, mutate in place, and
|
|
76
|
+
iterate like a Hash:
|
|
77
|
+
|
|
78
|
+
```ruby
|
|
79
|
+
obj = LLM::Object.from({})
|
|
80
|
+
|
|
81
|
+
obj.city = "Paris" # method-style write
|
|
82
|
+
obj["country"] = "France"
|
|
83
|
+
|
|
84
|
+
obj.key?(:city) # => true
|
|
85
|
+
obj.keys # => ["city", "country"]
|
|
86
|
+
obj.merge!(population: 2_100_000)
|
|
87
|
+
|
|
88
|
+
obj.each { |key, value| puts "#{key}: #{value}" }
|
|
89
|
+
obj.transform_values!(&:upcase) if obj.any?
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
#### Why would I use it?
|
|
93
|
+
|
|
94
|
+
Tool implementations receive their arguments as an
|
|
95
|
+
[`LLM::Object`](https://r.uby.dev/api-docs/llm.rb/LLM/Object.html)
|
|
96
|
+
and often build a result Hash from them. Merging defaults, deleting
|
|
97
|
+
optional keys, and transforming values in place keeps that code
|
|
98
|
+
concise without converting back and forth between Hash and
|
|
99
|
+
[`LLM::Object`](https://r.uby.dev/api-docs/llm.rb/LLM/Object.html).
|
|
100
|
+
|
|
101
|
+
#### Notes
|
|
102
|
+
|
|
103
|
+
`merge` returns a new
|
|
104
|
+
[`LLM::Object`](https://r.uby.dev/api-docs/llm.rb/LLM/Object.html);
|
|
105
|
+
`merge!` mutates in place.
|
|
106
|
+
Assignment always stores the key as a string internally, which is
|
|
107
|
+
why string and symbol lookups both work. Equality compares against
|
|
108
|
+
anything that responds to `to_h`.
|