llm.rb 15.0.3 → 15.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +146 -1
- data/README.md +124 -32
- data/bin/llm.rb +38 -3
- data/data/bedrock.json +250 -0
- data/data/deepinfra.json +162 -0
- data/data/deepseek.json +50 -0
- data/data/google.json +19 -21
- data/data/openrouter.json +14280 -0
- data/data/xai.json +28 -16
- data/docs/deepdive/advanced/context.md +8 -6
- data/docs/deepdive/fundamentals/agents.md +11 -10
- data/docs/deepdive/fundamentals/stream.md +4 -4
- data/lib/llm/agent.rb +6 -0
- data/lib/llm/context.rb +28 -13
- data/lib/llm/cost.rb +13 -0
- data/lib/llm/function/fork/task.rb +14 -10
- data/lib/llm/provider.rb +29 -8
- data/lib/llm/providers/anthropic.rb +1 -1
- data/lib/llm/providers/bedrock/models.rb +2 -2
- data/lib/llm/providers/bedrock.rb +1 -1
- data/lib/llm/providers/google.rb +1 -1
- data/lib/llm/providers/ollama.rb +1 -1
- data/lib/llm/providers/openai/responses.rb +2 -1
- data/lib/llm/providers/openai.rb +1 -1
- data/lib/llm/providers/openrouter.rb +87 -0
- data/lib/llm/repl/buffer.rb +20 -5
- data/lib/llm/repl/input.rb +5 -4
- data/lib/llm/repl/markdown/table.rb +5 -2
- data/lib/llm/repl/node.rb +26 -1
- data/lib/llm/repl/stream.rb +30 -3
- data/lib/llm/repl/window.rb +7 -7
- data/lib/llm/repl.rb +1 -0
- data/lib/llm/skill.rb +7 -1
- data/lib/llm/stream.rb +8 -3
- data/lib/llm/tools/git.rb +2 -2
- data/lib/llm/tools/mkdir.rb +2 -2
- data/lib/llm/tools/rg.rb +2 -2
- data/lib/llm/tools/ruby.rb +2 -2
- data/lib/llm/tools/shell.rb +2 -2
- data/lib/llm/tools/utils.rb +1 -1
- data/lib/llm/transport/curb.rb +5 -3
- data/lib/llm/transport/http.rb +5 -2
- data/lib/llm/transport/persistent_http.rb +6 -4
- data/lib/llm/transport/utils.rb +8 -6
- data/lib/llm/version.rb +1 -1
- data/lib/llm.rb +16 -1
- data/llm.gemspec +2 -1
- metadata +19 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2b496d3aec5a309af0ff9eaac32cabb6c7191bf2bac1f3b664c02a56c593402e
|
|
4
|
+
data.tar.gz: 0ae83d7794b4dad881de7b9866b3894746efa1853e349fc5707416e367802db5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9f890dd0720d0acbc79f9df2196f3595a68cd618fd0d09dc02fcab9a6fe27758ae72efd4b764614d5511bdb7914f586fba7194b3d1a5bec186562b9196753783
|
|
7
|
+
data.tar.gz: f25bc47b9bd5dcb0afea4f2ddbcb1ad37f74863cff962de6ceefa6157af073997589fd08fad0b2df11288aa24db41685127542b4d1748a71c4fef607d6b76ba2
|
data/CHANGELOG.md
CHANGED
|
@@ -15,6 +15,151 @@
|
|
|
15
15
|
|
|
16
16
|
## What's next
|
|
17
17
|
|
|
18
|
+
## v15.1.0
|
|
19
|
+
|
|
20
|
+
Changes since `v15.0.3`.
|
|
21
|
+
|
|
22
|
+
This release adds the OpenRouter provider, splits provider timeouts
|
|
23
|
+
into `connect_timeout` and `read_timeout`, retries timed-out requests,
|
|
24
|
+
and renames `on_rate_limit` to `on_retry`. Skills now gain a
|
|
25
|
+
frontmatter `model:` parameter and inherit the parent agent's model,
|
|
26
|
+
while the CLI gains `-m` and `-x` switches, and the REPL shows retry
|
|
27
|
+
progress and measures text by display width.
|
|
28
|
+
|
|
29
|
+
### Provider
|
|
30
|
+
|
|
31
|
+
* **add `LLM::OpenRouter` for the OpenRouter provider** <br>
|
|
32
|
+
[`LLM::OpenRouter`](https://r.uby.dev/api-docs/llm.rb/LLM/OpenRouter.html)
|
|
33
|
+
is a new provider that talks to [OpenRouter](https://openrouter.ai)
|
|
34
|
+
through its OpenAI-compatible API, contributed via
|
|
35
|
+
[PR #165](https://github.com/r-uby-dev/llm.rb/pull/165). Create an
|
|
36
|
+
instance with
|
|
37
|
+
[`LLM.openrouter`](https://r.uby.dev/api-docs/llm.rb/LLM.html#openrouter-class_method),
|
|
38
|
+
which accepts the same `key:`, `host:`, and `base_path:` options as the
|
|
39
|
+
OpenAI provider. It defaults to the `openrouter/auto` router model and
|
|
40
|
+
supports chat completions, streaming, tool calls, structured output,
|
|
41
|
+
and embeddings; image, audio, moderation, files, and vector store
|
|
42
|
+
endpoints raise `NotImplementedError`. Model metadata ships in
|
|
43
|
+
`data/openrouter.json` for the registry.
|
|
44
|
+
|
|
45
|
+
* **provider: `LLM::Provider#with` accepts headers without the `headers:` keyword** <br>
|
|
46
|
+
[`LLM::Provider#with`](https://r.uby.dev/api-docs/llm.rb/LLM/Provider.html#with-instance_method)
|
|
47
|
+
now accepts headers directly as a Hash (`llm.with("User-Agent" => "llmrb/1.0")`)
|
|
48
|
+
in addition to the legacy `headers:` keyword form. Both are merged, and
|
|
49
|
+
the keyword form remains for backwards compatibility.
|
|
50
|
+
|
|
51
|
+
* **providers: `model: nil` falls back to `default_model`** <br>
|
|
52
|
+
[`LLM::Provider`](https://r.uby.dev/api-docs/llm.rb/LLM/Provider.html)
|
|
53
|
+
now treats a `model: nil` payload as "use the default model" instead of
|
|
54
|
+
sending a model value that providers reject. Previously a `{model: nil}`
|
|
55
|
+
param overwrote the default and then was removed by `.compact`, leaving
|
|
56
|
+
undefined behavior where providers could reject the request.
|
|
57
|
+
|
|
58
|
+
* **openai: handle `model: nil` in the Responses API** <br>
|
|
59
|
+
The OpenAI Responses API path now also falls back to the default model
|
|
60
|
+
when `model: nil`, matching the completions path.
|
|
61
|
+
|
|
62
|
+
* **provider: separate `connect_timeout` and `read_timeout`** <br>
|
|
63
|
+
[`LLM::Provider`](https://r.uby.dev/api-docs/llm.rb/LLM/Provider.html)
|
|
64
|
+
now accepts a `connect_timeout:` for opening the connection (default 5
|
|
65
|
+
seconds) and a `read_timeout:` for waiting on an idle connection (default
|
|
66
|
+
600 seconds), instead of a single `timeout`. The legacy `timeout:` option
|
|
67
|
+
remains as a shorthand for `read_timeout`. The provider exposes the new
|
|
68
|
+
`#read_timeout` and `#connect_timeout` accessors.
|
|
69
|
+
|
|
70
|
+
* **provider: retry timed-out requests** <br>
|
|
71
|
+
Requests that time out (`Net::OpenTimeout` or `Net::ReadTimeout`) are now
|
|
72
|
+
retried along with rate-limited requests, up to the `retry_budget`. When
|
|
73
|
+
a heavily loaded provider (for example DeepSeek) drops the connection
|
|
74
|
+
during the connect or read phase, the request is retried instead of
|
|
75
|
+
failing, and the stream is notified through `on_retry`.
|
|
76
|
+
|
|
77
|
+
* **context: retry on `LLM::InsufficientQuotaError`, too** <br>
|
|
78
|
+
A request that raises
|
|
79
|
+
[`LLM::InsufficientQuotaError`](https://r.uby.dev/api-docs/llm.rb/LLM/InsufficientQuotaError.html)
|
|
80
|
+
is now retried like other rate-limited requests. The error is a subclass
|
|
81
|
+
of `LLM::RateLimitError` and can be raised at regular intervals, in
|
|
82
|
+
particular by the Alibaba provider.
|
|
83
|
+
|
|
84
|
+
* **cost: return `LLM::Cost.zero` when the registry has no pricing** <br>
|
|
85
|
+
Add
|
|
86
|
+
[`LLM::Cost.zero`](https://r.uby.dev/api-docs/llm.rb/LLM/Cost.html#zero-class_method),
|
|
87
|
+
a factory for a zero-valued cost breakdown.
|
|
88
|
+
[`LLM::Context#cost`](https://r.uby.dev/api-docs/llm.rb/LLM/Context.html#cost-instance_method)
|
|
89
|
+
now returns it when the active model has no pricing in the registry (for
|
|
90
|
+
example OpenRouter's `openrouter/auto` router model), instead of crashing
|
|
91
|
+
on a nil pricing entry.
|
|
92
|
+
|
|
93
|
+
### Skills
|
|
94
|
+
|
|
95
|
+
* **skills: add a `model` frontmatter parameter** <br>
|
|
96
|
+
A skill's `SKILL.md` frontmatter can now declare a `model:` value, so a
|
|
97
|
+
skill's sub-agent runs on a specific model instead of the default. This
|
|
98
|
+
lets a parent agent run on one model (for example `deepseek-v4-flash`)
|
|
99
|
+
while the skill runs on another (`deepseek-v4-pro`). The `model` value is
|
|
100
|
+
not strictly portable between providers.
|
|
101
|
+
|
|
102
|
+
* **skills: inherit the model of the parent agent** <br>
|
|
103
|
+
A skill's sub-agent now inherits the active model of the agent that
|
|
104
|
+
spawns it instead of falling back to the provider default. The frontmatter
|
|
105
|
+
`model:` parameter overrides it explicitly when set.
|
|
106
|
+
[`LLM::Context#model`](https://r.uby.dev/api-docs/llm.rb/LLM/Context.html#model-instance_method)
|
|
107
|
+
now falls back to the provider's default model when no model is set.
|
|
108
|
+
|
|
109
|
+
### Stream
|
|
110
|
+
|
|
111
|
+
* **stream: rename `on_rate_limit` to `on_retry`** <br>
|
|
112
|
+
[`LLM::Stream#on_rate_limit`](https://r.uby.dev/api-docs/llm.rb/LLM/Stream.html#on_rate_limit-instance_method)
|
|
113
|
+
is renamed to
|
|
114
|
+
[`LLM::Stream#on_retry`](https://r.uby.dev/api-docs/llm.rb/LLM/Stream.html#on_retry-instance_method),
|
|
115
|
+
which fires when a request is retried after a rate limit or a timeout,
|
|
116
|
+
and now receives the one-based retry attempt number as a second argument.
|
|
117
|
+
The old `on_rate_limit` name remains as an alias.
|
|
118
|
+
|
|
119
|
+
### CLI
|
|
120
|
+
|
|
121
|
+
* **cli: add a `-m` switch for choosing the model** <br>
|
|
122
|
+
`bin/llm.rb` now accepts `-m MODEL` to run the session on a model other
|
|
123
|
+
than the provider default, for example
|
|
124
|
+
`llm.rb -p deepseek -m deepseek-v4-pro`. A model unknown to the provider's
|
|
125
|
+
registry prints an error and exits.
|
|
126
|
+
|
|
127
|
+
* **cli: add a `-x` switch for the read timeout** <br>
|
|
128
|
+
`bin/llm.rb` now accepts `-x SECONDS` to set the provider read timeout.
|
|
129
|
+
|
|
130
|
+
* **cli: print a backtrace on fatal crashes** <br>
|
|
131
|
+
When `bin/llm.rb` hits an unexpected error, the crash message now includes
|
|
132
|
+
up to three stack lines from the backtrace, so the failure is easier to
|
|
133
|
+
locate and report than a bare diagnostic.
|
|
134
|
+
|
|
135
|
+
### Repl
|
|
136
|
+
|
|
137
|
+
* **repl: show retry progress in the status bar** <br>
|
|
138
|
+
When a request is rate limited or times out, the curses-based REPL status
|
|
139
|
+
bar shows a retry indicator with the error and the remaining attempts, for
|
|
140
|
+
example `🔁 Rate limited • attempt 2 of 5`.
|
|
141
|
+
|
|
142
|
+
* **repl: measure text width with `unicode-display_width`** <br>
|
|
143
|
+
The curses-based REPL now counts and slices text by display column width
|
|
144
|
+
instead of character count, so wrapping, table columns, and clipping stay
|
|
145
|
+
aligned for wide characters such as emoji. It requires the optional
|
|
146
|
+
`unicode-display_width` gem.
|
|
147
|
+
|
|
148
|
+
* **repl: treat `LLM::InsufficientQuotaError` as a rate limit in the status bar** <br>
|
|
149
|
+
The curses-based REPL status bar now shows `Rate limited` when a request
|
|
150
|
+
raises
|
|
151
|
+
[`LLM::InsufficientQuotaError`](https://r.uby.dev/api-docs/llm.rb/LLM/InsufficientQuotaError.html),
|
|
152
|
+
matching how ordinary `LLM::RateLimitError`s are shown, instead of falling
|
|
153
|
+
through to the raw class name.
|
|
154
|
+
|
|
155
|
+
### Registry
|
|
156
|
+
|
|
157
|
+
* **refresh model metadata** <br>
|
|
158
|
+
Update `data/*.json` with current model listings and pricing, adding
|
|
159
|
+
GPT-5.6 Sol, Terra, and Luna models to Bedrock, Grok 4.6 and Grok Imagine
|
|
160
|
+
Image 2.0 to xAI, DeepSeek V4 Flash Vision to DeepSeek, and DeepSeek V4
|
|
161
|
+
Pro 0813, Qwen3 VL, and Qwen3.8 models to DeepInfra.
|
|
162
|
+
|
|
18
163
|
## v15.0.3
|
|
19
164
|
|
|
20
165
|
Changes since `v15.0.2`.
|
|
@@ -1077,7 +1222,7 @@ reliable across all six concurrency backends. The `functions` and
|
|
|
1077
1222
|
`LLM.require` now accepts a second `version` parameter that is passed
|
|
1078
1223
|
to `Kernel#gem` before loading, enabling version constraints for
|
|
1079
1224
|
optional runtime dependencies. For example,
|
|
1080
|
-
`LLM.require "test-cmd.rb", "~>
|
|
1225
|
+
`LLM.require "test-cmd.rb", "~> 2.1"` ensures a minimum gem version
|
|
1081
1226
|
is available. This is used internally by the `Git`, `Rg`, `Mkdir`,
|
|
1082
1227
|
and `Shell` tools to enforce compatibility with the `test-cmd.rb` gem.
|
|
1083
1228
|
|
data/README.md
CHANGED
|
@@ -17,11 +17,11 @@ Welcome to the canonical llm.rb repository.
|
|
|
17
17
|
llm.rb is an advanced runtime for building agentic AI applications
|
|
18
18
|
on CRuby. It has zero runtime dependencies by default, supports
|
|
19
19
|
concurrent and parallel tool execution and has a single coherent API
|
|
20
|
-
that spans
|
|
20
|
+
that spans 14+ providers. Streaming, tools, guards, compaction, the
|
|
21
21
|
REPL, builtin MCP/A2A support and the database integrations all build
|
|
22
22
|
on the same three concepts: providers, contexts, and agents.
|
|
23
23
|
|
|
24
|
-
The
|
|
24
|
+
The easiest way to learn about llm.rb is to ask [the r.uby.dev chatbot](https://r.uby.dev)
|
|
25
25
|
a question. It is connected to the llm.rb GitHub repository, backed by
|
|
26
26
|
ActiveRecord and uses the builtin MCP feature to connect to GitHub. All
|
|
27
27
|
answers are grounded in the llm.rb source code.
|
|
@@ -108,8 +108,8 @@ class MyStream < LLM::Stream
|
|
|
108
108
|
def on_skill_return(agent, skill, result)
|
|
109
109
|
end
|
|
110
110
|
|
|
111
|
-
# A request was rate limited and will be retried.
|
|
112
|
-
def
|
|
111
|
+
# A request was rate limited or timed out and will be retried.
|
|
112
|
+
def on_retry(error, attempt)
|
|
113
113
|
end
|
|
114
114
|
end
|
|
115
115
|
|
|
@@ -204,6 +204,12 @@ with the `:fork` and `:ractor` strategies. The
|
|
|
204
204
|
The `:fork` strategy also provides a separate process that offers
|
|
205
205
|
isolation from its parent.
|
|
206
206
|
|
|
207
|
+
A couple of concurrency strategies require optional, opt-in dependencies.
|
|
208
|
+
The `async` strategy requires the [async](https://github.com/socketry/async)
|
|
209
|
+
gem and the `fork` strategy requires the [xchan.rb](https://github.com/0x1eef/xchan.rb)
|
|
210
|
+
gem. The `fiber` strategy requires a scheduler (`Fiber.scheduler`) but by
|
|
211
|
+
default Ruby does not provide one.
|
|
212
|
+
|
|
207
213
|
```ruby
|
|
208
214
|
require "llm"
|
|
209
215
|
require "llm/tools"
|
|
@@ -266,7 +272,7 @@ a separate gem but it requires a number of optional dependencies
|
|
|
266
272
|
to be installed separately. The following gems provide the full
|
|
267
273
|
experience:
|
|
268
274
|
|
|
269
|
-
gem install curses kramdown xchan.rb test-cmd.rb
|
|
275
|
+
gem install unicode-display_width curses kramdown xchan.rb test-cmd.rb
|
|
270
276
|
|
|
271
277
|
##### Persistence
|
|
272
278
|
|
|
@@ -335,53 +341,90 @@ for both Rack-based / Rails-based applications. On databases
|
|
|
335
341
|
where it is supported, such as PostgreSQL, the column can be optimized by using
|
|
336
342
|
the `jsonb` type.
|
|
337
343
|
|
|
344
|
+
The following example is based on the agent used to power the
|
|
345
|
+
[r.uby.dev chatbot](https://r.uby.dev).
|
|
346
|
+
|
|
338
347
|
```ruby
|
|
339
348
|
require "active_record"
|
|
340
349
|
require "llm"
|
|
341
350
|
require "llm/active_record"
|
|
342
351
|
|
|
343
|
-
class
|
|
344
|
-
acts_as_agent do |agent|
|
|
345
|
-
agent.set name: "
|
|
346
|
-
|
|
347
|
-
|
|
352
|
+
class Raven < ActiveRecord::Base
|
|
353
|
+
acts_as_agent(format: :jsonb) do |agent|
|
|
354
|
+
agent.set name: "raven",
|
|
355
|
+
description: "a chatbot for the r.uby.dev website",
|
|
356
|
+
instructions: proc { File.read(File.join(__dir__, "raven", "prompt.md")) },
|
|
357
|
+
tools: :tools,
|
|
358
|
+
concurrency: :async
|
|
348
359
|
end
|
|
349
360
|
|
|
350
|
-
def
|
|
351
|
-
talk("
|
|
361
|
+
def research_issues
|
|
362
|
+
talk("research open pull requests on r-uby-dev/llm")
|
|
352
363
|
end
|
|
353
364
|
|
|
354
|
-
def
|
|
355
|
-
talk("
|
|
365
|
+
def research_codebase
|
|
366
|
+
talk("research the codebase on r-uby-dev/llm")
|
|
356
367
|
end
|
|
357
368
|
|
|
358
|
-
|
|
369
|
+
##
|
|
370
|
+
# @return [LLM::MCP]
|
|
371
|
+
def github
|
|
372
|
+
@github ||= LLM::MCP.http(
|
|
373
|
+
url: "https://api.githubcopilot.com/mcp/",
|
|
374
|
+
headers: {"Authorization" => "Bearer #{ENV['GITHUB_RUBYDEV_PAT']}"},
|
|
375
|
+
transport: :net_http_persistent
|
|
376
|
+
)
|
|
377
|
+
end
|
|
359
378
|
|
|
360
379
|
##
|
|
361
|
-
#
|
|
362
|
-
|
|
380
|
+
# @return [Array<LLM::Tool>]
|
|
381
|
+
def tools
|
|
382
|
+
github.tools.select { allowlist.include?(_1.name.to_s) }
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
private
|
|
386
|
+
|
|
363
387
|
def set_provider
|
|
364
|
-
LLM.deepseek
|
|
388
|
+
LLM.deepseek
|
|
365
389
|
end
|
|
366
390
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
391
|
+
def allowlist
|
|
392
|
+
%w[
|
|
393
|
+
get_commit
|
|
394
|
+
get_file_contents
|
|
395
|
+
list_branches
|
|
396
|
+
list_commits
|
|
397
|
+
search_code
|
|
398
|
+
search_commits
|
|
399
|
+
search_repositories
|
|
400
|
+
search_issues
|
|
401
|
+
pull_request_read
|
|
402
|
+
list_pull_requests
|
|
403
|
+
list_issues
|
|
404
|
+
issue_read
|
|
405
|
+
].freeze
|
|
372
406
|
end
|
|
373
407
|
end
|
|
374
408
|
|
|
375
|
-
|
|
376
|
-
|
|
409
|
+
agent = Raven.create!
|
|
410
|
+
|
|
411
|
+
##
|
|
412
|
+
# Every call to `talk` automatically persists
|
|
413
|
+
# to the database (under the hood research_issues
|
|
414
|
+
# calls the talk method)
|
|
415
|
+
agent.research_issues
|
|
416
|
+
|
|
417
|
+
##
|
|
418
|
+
# The conversation was persisted to database. A
|
|
419
|
+
# fresh instance restores it and continues where
|
|
420
|
+
# we left off
|
|
421
|
+
agent = Raven.find(agent.id).tap(&:research_codebase)
|
|
377
422
|
|
|
378
423
|
##
|
|
379
|
-
#
|
|
380
|
-
#
|
|
381
|
-
#
|
|
382
|
-
|
|
383
|
-
# already drafted:
|
|
384
|
-
Email.find(email.id).summarize
|
|
424
|
+
# Start an agent console.
|
|
425
|
+
# Query agent's state, debug, etc.
|
|
426
|
+
# The REPL does not persist back to the database.
|
|
427
|
+
agent.repl
|
|
385
428
|
```
|
|
386
429
|
</details>
|
|
387
430
|
|
|
@@ -566,7 +609,8 @@ agent.talk "Hello"
|
|
|
566
609
|
|
|
567
610
|
Rate-limited requests are retried automatically by default. Agents
|
|
568
611
|
retry a 429 up to five times with a growing backoff before giving
|
|
569
|
-
up, so most request failures resolve on their own.
|
|
612
|
+
up, so most request failures resolve on their own. Connection and
|
|
613
|
+
read timeouts are retried the same way. Set `retry_budget`
|
|
570
614
|
to change the number of retries, or `retry_budget: 0` to disable
|
|
571
615
|
them.
|
|
572
616
|
|
|
@@ -653,6 +697,7 @@ change.
|
|
|
653
697
|
* **xAI** (`LLM.xai`)
|
|
654
698
|
* **Z.ai** (`LLM.zai`)
|
|
655
699
|
* **Moonshot (Kimi)** (`LLM.moonshot`)
|
|
700
|
+
* **OpenRouter** (`LLM.openrouter`)
|
|
656
701
|
* **Alibaba (Qwen3)** (`LLM.alibaba`, also `LLM.aliyun`)
|
|
657
702
|
* **Mistral** (`LLM.mistral`)
|
|
658
703
|
* **AWS Bedrock** (`LLM.bedrock`)
|
|
@@ -674,6 +719,7 @@ llm = LLM.anthropic
|
|
|
674
719
|
llm = LLM.deepseek
|
|
675
720
|
llm = LLM.alibaba # also: LLM.aliyun
|
|
676
721
|
llm = LLM.moonshot
|
|
722
|
+
llm = LLM.openrouter
|
|
677
723
|
llm = LLM.mistral
|
|
678
724
|
```
|
|
679
725
|
</details>
|
|
@@ -691,6 +737,7 @@ llm = LLM.anthropic(key: ENV["ANTHROPIC_API_KEY"])
|
|
|
691
737
|
llm = LLM.deepseek(key: ENV["DEEPSEEK_API_KEY"])
|
|
692
738
|
llm = LLM.alibaba(key: ENV["DASHSCOPE_API_KEY"]) # also: LLM.aliyun
|
|
693
739
|
llm = LLM.moonshot(key: ENV["MOONSHOT_API_KEY"])
|
|
740
|
+
llm = LLM.openrouter(key: ENV["OPENROUTER_API_KEY"])
|
|
694
741
|
llm = LLM.mistral(key: ENV["MISTRAL_API_KEY"])
|
|
695
742
|
```
|
|
696
743
|
</details>
|
|
@@ -734,6 +781,51 @@ llm = LLM.deepseek(
|
|
|
734
781
|
```
|
|
735
782
|
</details>
|
|
736
783
|
|
|
784
|
+
<details>
|
|
785
|
+
<summary>Timeouts</summary>
|
|
786
|
+
<br>
|
|
787
|
+
|
|
788
|
+
Providers accept two timeouts:
|
|
789
|
+
|
|
790
|
+
* `connect_timeout` - opening the connection. Defaults to 5 seconds.
|
|
791
|
+
* `read_timeout` - waiting for a response on an idle connection.
|
|
792
|
+
Defaults to 600 seconds (10 minutes).
|
|
793
|
+
|
|
794
|
+
The longer read timeout leaves room for slow reasoning models and
|
|
795
|
+
local models. The legacy `timeout:` option remains as a shorthand for
|
|
796
|
+
`read_timeout`. Timeouts are retriable:
|
|
797
|
+
[`LLM::Agent`](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html)
|
|
798
|
+
retries a timed out request up to its
|
|
799
|
+
[`retry_budget`](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html#retry_budget-class_method)
|
|
800
|
+
(five by default), so a dropped connection or a slow first token
|
|
801
|
+
is often something we can recover from.
|
|
802
|
+
|
|
803
|
+
```ruby
|
|
804
|
+
llm = LLM.deepseek(
|
|
805
|
+
connect_timeout: 5, # opening the connection
|
|
806
|
+
read_timeout: 600 # waiting for the next bytes
|
|
807
|
+
)
|
|
808
|
+
```
|
|
809
|
+
</details>
|
|
810
|
+
|
|
811
|
+
<details>
|
|
812
|
+
<summary>Headers</summary>
|
|
813
|
+
<br>
|
|
814
|
+
|
|
815
|
+
Providers can accept a custom set of headers with
|
|
816
|
+
the [`LLM::Provider#with`](https://r.uby.dev/api-docs/llm.rb/LLM/Provider.html#with-instance_method) method.
|
|
817
|
+
For example, you could set a custom User-Agent header,
|
|
818
|
+
or provide headers that carry special meaning to
|
|
819
|
+
certain providers (eg OpenAI, OpenRouter).
|
|
820
|
+
|
|
821
|
+
```ruby
|
|
822
|
+
llm = LLM.openrouter
|
|
823
|
+
llm = llm.with("HTTP-Referer" => "https://example.com")
|
|
824
|
+
llm = llm.with("X-OpenRouter-Title" => "Example App")
|
|
825
|
+
```
|
|
826
|
+
|
|
827
|
+
</details>
|
|
828
|
+
|
|
737
829
|
### RAG
|
|
738
830
|
|
|
739
831
|
Most providers offer an embedding model that can be
|
data/bin/llm.rb
CHANGED
|
@@ -49,16 +49,20 @@ def help
|
|
|
49
49
|
warn ""
|
|
50
50
|
warn "Options:"
|
|
51
51
|
warn " -p PROVIDER Choose a provider"
|
|
52
|
+
warn " -m MODEL Choose a model"
|
|
52
53
|
warn " -c STRATEGY Concurrency strategy for tool calls (eg thread, async, fork)"
|
|
53
54
|
warn " -n TRANSPORT HTTP transports - net-http (default), net-http-persistent, and curb"
|
|
55
|
+
warn " -x TIMEOUT The default read timeout (in seconds)"
|
|
54
56
|
warn " -t Temporary session that doesn't persist to disk"
|
|
55
57
|
warn " -h Show this help"
|
|
56
58
|
warn ""
|
|
57
59
|
warn "Examples:"
|
|
58
60
|
warn " #{prog} # auto-detect provider from $PROVIDER_API_KEY"
|
|
59
61
|
warn " #{prog} -p openai # use OpenAI"
|
|
62
|
+
warn " #{prog} -m gpt-5.6 # use a model other than the provider default"
|
|
60
63
|
warn " #{prog} -n curb # use libcurl"
|
|
61
64
|
warn " #{prog} -c thread # run tool calls on a separate thread"
|
|
65
|
+
warn " #{prog} -x 900 # read timeout of 15mins"
|
|
62
66
|
warn " #{prog} -h # this help"
|
|
63
67
|
warn ""
|
|
64
68
|
end
|
|
@@ -94,6 +98,11 @@ def fatal(ex)
|
|
|
94
98
|
wrapped detail, " "
|
|
95
99
|
end
|
|
96
100
|
warn ""
|
|
101
|
+
warn " Backtrace:"
|
|
102
|
+
ex.backtrace.drop(1).first(3).each do |line|
|
|
103
|
+
wrapped line, " "
|
|
104
|
+
end
|
|
105
|
+
warn ""
|
|
97
106
|
warn " This is an unexpected error. If it keeps happening,"
|
|
98
107
|
warn " consider opening an issue at"
|
|
99
108
|
warn " https://github.com/r-uby-dev/llm/issues"
|
|
@@ -153,6 +162,22 @@ def main(argv)
|
|
|
153
162
|
help
|
|
154
163
|
exit 1
|
|
155
164
|
end
|
|
165
|
+
when '-m'
|
|
166
|
+
model = argv.shift
|
|
167
|
+
if model.nil?
|
|
168
|
+
warn "llm.rb: -m switch requires an argument"
|
|
169
|
+
help
|
|
170
|
+
exit 1
|
|
171
|
+
end
|
|
172
|
+
when '-x'
|
|
173
|
+
timeout = argv.shift
|
|
174
|
+
if timeout.nil?
|
|
175
|
+
warn "llm.rb: -x switch requires an argument"
|
|
176
|
+
help
|
|
177
|
+
exit 1
|
|
178
|
+
else
|
|
179
|
+
timeout = Integer(timeout)
|
|
180
|
+
end
|
|
156
181
|
else
|
|
157
182
|
warn "llm.rb: unknown option #{option}"
|
|
158
183
|
help
|
|
@@ -164,9 +189,10 @@ def main(argv)
|
|
|
164
189
|
# No provider has been given.
|
|
165
190
|
# Try to infer one.
|
|
166
191
|
transport ||= :net_http
|
|
192
|
+
options = timeout ? {timeout:, transport:} : {transport:}
|
|
167
193
|
if provider.nil?
|
|
168
194
|
llm = providers.filter_map do
|
|
169
|
-
LLM.method(_1).call(
|
|
195
|
+
LLM.method(_1).call(**options)
|
|
170
196
|
rescue ArgumentError
|
|
171
197
|
end.first
|
|
172
198
|
if llm.nil?
|
|
@@ -175,7 +201,7 @@ def main(argv)
|
|
|
175
201
|
end
|
|
176
202
|
else
|
|
177
203
|
begin
|
|
178
|
-
llm = LLM.method(provider).call(
|
|
204
|
+
llm = LLM.method(provider).call(**options)
|
|
179
205
|
rescue ArgumentError
|
|
180
206
|
warn "llm.rb: set credentials for #{provider}"
|
|
181
207
|
exit 1
|
|
@@ -209,11 +235,20 @@ def main(argv)
|
|
|
209
235
|
File.binwrite file, JSON.pretty_generate(data)
|
|
210
236
|
end
|
|
211
237
|
|
|
238
|
+
if model
|
|
239
|
+
if not llm.registry.keys.include?(model)
|
|
240
|
+
warn "llm.rb: #{model} is not a valid #{llm.name} model"
|
|
241
|
+
exit 1
|
|
242
|
+
end
|
|
243
|
+
else
|
|
244
|
+
model = llm.default_model
|
|
245
|
+
end
|
|
246
|
+
|
|
212
247
|
##
|
|
213
248
|
# Let's go!
|
|
214
249
|
concurrency ||= :sequential
|
|
215
250
|
path = temp ? nil : data[Dir.getwd]
|
|
216
|
-
agent = LLM::Agent.new(llm, path:, concurrency:, tools: LLM::Tool.subclasses)
|
|
251
|
+
agent = LLM::Agent.new(llm, model:, path:, concurrency:, tools: LLM::Tool.subclasses)
|
|
217
252
|
agent.repl
|
|
218
253
|
rescue Interrupt
|
|
219
254
|
warn "llm.rb: Bye!"
|