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.
Files changed (113) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +716 -1826
  3. data/README.md +668 -253
  4. data/bin/llm.rb +156 -56
  5. data/data/alibaba.json +1999 -0
  6. data/data/anthropic.json +195 -252
  7. data/data/bedrock.json +2189 -1854
  8. data/data/deepinfra.json +1312 -728
  9. data/data/deepseek.json +42 -39
  10. data/data/google.json +1024 -404
  11. data/data/mistral.json +481 -401
  12. data/data/moonshot.json +384 -0
  13. data/data/openai.json +985 -1354
  14. data/data/xai.json +220 -115
  15. data/data/zai.json +166 -166
  16. data/docs/deepdive/advanced/cancellation.md +74 -0
  17. data/docs/deepdive/advanced/compaction.md +85 -0
  18. data/docs/deepdive/advanced/context.md +280 -0
  19. data/docs/deepdive/advanced/guard.md +371 -0
  20. data/docs/deepdive/advanced/transformer.md +67 -0
  21. data/docs/deepdive/advanced/transports.md +45 -0
  22. data/docs/deepdive/features/builtin_tools.md +191 -0
  23. data/docs/deepdive/features/concurrency.md +110 -0
  24. data/docs/deepdive/features/database.md +449 -0
  25. data/docs/deepdive/features/embeddings.md +157 -0
  26. data/docs/deepdive/features/repl.md +141 -0
  27. data/docs/deepdive/fundamentals/agents.md +253 -0
  28. data/docs/deepdive/fundamentals/providers.md +159 -0
  29. data/docs/deepdive/fundamentals/schema.md +61 -0
  30. data/docs/deepdive/fundamentals/skills.md +111 -0
  31. data/docs/deepdive/fundamentals/stream.md +143 -0
  32. data/docs/deepdive/fundamentals/tools.md +329 -0
  33. data/docs/deepdive/media/audio.md +122 -0
  34. data/docs/deepdive/media/images.md +89 -0
  35. data/docs/deepdive/media/ocr.md +48 -0
  36. data/docs/deepdive/protocols/a2a.md +106 -0
  37. data/docs/deepdive/protocols/mcp.md +111 -0
  38. data/docs/deepdive/reference/cost.md +109 -0
  39. data/docs/deepdive/reference/model_registry.md +271 -0
  40. data/docs/deepdive/reference/object.md +108 -0
  41. data/docs/deepdive/reference/tracer.md +187 -0
  42. data/{resources → docs}/deepdive.md +37 -23
  43. data/lib/llm/a2a/transport/http.rb +1 -1
  44. data/lib/llm/active_record/acts_as_llm.rb +19 -5
  45. data/lib/llm/agent.rb +107 -17
  46. data/lib/llm/context.rb +164 -134
  47. data/lib/llm/cost.rb +114 -49
  48. data/lib/llm/error.rb +7 -8
  49. data/lib/llm/function/array.rb +1 -1
  50. data/lib/llm/function/async/task.rb +2 -0
  51. data/lib/llm/function/fiber/task.rb +2 -0
  52. data/lib/llm/function/fork/task.rb +16 -1
  53. data/lib/llm/function/ractor/task.rb +2 -0
  54. data/lib/llm/function/sequential/group.rb +20 -10
  55. data/lib/llm/function/sequential/task.rb +2 -9
  56. data/lib/llm/function/task.rb +4 -0
  57. data/lib/llm/function/thread/task.rb +2 -0
  58. data/lib/llm/function.rb +33 -4
  59. data/lib/llm/guard/loop.rb +89 -0
  60. data/lib/llm/guard/null.rb +19 -0
  61. data/lib/llm/guard.rb +61 -0
  62. data/lib/llm/message.rb +5 -4
  63. data/lib/llm/provider.rb +43 -0
  64. data/lib/llm/providers/alibaba/error_handler.rb +34 -0
  65. data/lib/llm/providers/alibaba/request_adapter.rb +13 -0
  66. data/lib/llm/providers/alibaba.rb +93 -0
  67. data/lib/llm/providers/anthropic/stream_parser.rb +1 -0
  68. data/lib/llm/providers/anthropic.rb +1 -9
  69. data/lib/llm/providers/bedrock/stream_parser.rb +1 -0
  70. data/lib/llm/providers/bedrock.rb +9 -9
  71. data/lib/llm/providers/deepseek/request_adapter.rb +2 -33
  72. data/lib/llm/providers/google/stream_parser.rb +1 -0
  73. data/lib/llm/providers/google.rb +1 -9
  74. data/lib/llm/providers/moonshot.rb +76 -0
  75. data/lib/llm/providers/ollama.rb +1 -9
  76. data/lib/llm/providers/openai/responses/stream_parser.rb +1 -0
  77. data/lib/llm/providers/openai/responses.rb +6 -9
  78. data/lib/llm/providers/openai/schema.rb +37 -0
  79. data/lib/llm/providers/openai/stream_parser.rb +1 -0
  80. data/lib/llm/providers/openai.rb +4 -12
  81. data/lib/llm/registry/model.rb +186 -0
  82. data/lib/llm/registry.rb +45 -14
  83. data/lib/llm/repl/bar.rb +11 -12
  84. data/lib/llm/repl/buffer.rb +43 -16
  85. data/lib/llm/repl/color.rb +85 -0
  86. data/lib/llm/repl/command.rb +12 -0
  87. data/lib/llm/repl/commands/model.rb +39 -0
  88. data/lib/llm/repl/input/cache.rb +45 -0
  89. data/lib/llm/repl/input/char.rb +46 -0
  90. data/lib/llm/repl/input/row.rb +39 -0
  91. data/lib/llm/repl/input.rb +327 -79
  92. data/lib/llm/repl/markdown/table.rb +6 -2
  93. data/lib/llm/repl/markdown.rb +56 -6
  94. data/lib/llm/repl/node.rb +7 -0
  95. data/lib/llm/repl/status.rb +54 -5
  96. data/lib/llm/repl/stream.rb +16 -4
  97. data/lib/llm/repl/walker.rb +3 -2
  98. data/lib/llm/repl/window.rb +111 -11
  99. data/lib/llm/repl.rb +47 -21
  100. data/lib/llm/sequel/plugin.rb +19 -5
  101. data/lib/llm/skill.rb +21 -8
  102. data/lib/llm/stream.rb +35 -7
  103. data/lib/llm/tool.rb +27 -0
  104. data/lib/llm/tools/rg.rb +2 -1
  105. data/lib/llm/transformer/null.rb +21 -0
  106. data/lib/llm/transformer.rb +55 -0
  107. data/lib/llm/transport/curb.rb +23 -3
  108. data/lib/llm/usage.rb +155 -9
  109. data/lib/llm/version.rb +1 -1
  110. data/lib/llm.rb +121 -29
  111. data/llm.gemspec +16 -10
  112. metadata +100 -13
  113. 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`.