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.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +146 -1
  3. data/README.md +124 -32
  4. data/bin/llm.rb +38 -3
  5. data/data/bedrock.json +250 -0
  6. data/data/deepinfra.json +162 -0
  7. data/data/deepseek.json +50 -0
  8. data/data/google.json +19 -21
  9. data/data/openrouter.json +14280 -0
  10. data/data/xai.json +28 -16
  11. data/docs/deepdive/advanced/context.md +8 -6
  12. data/docs/deepdive/fundamentals/agents.md +11 -10
  13. data/docs/deepdive/fundamentals/stream.md +4 -4
  14. data/lib/llm/agent.rb +6 -0
  15. data/lib/llm/context.rb +28 -13
  16. data/lib/llm/cost.rb +13 -0
  17. data/lib/llm/function/fork/task.rb +14 -10
  18. data/lib/llm/provider.rb +29 -8
  19. data/lib/llm/providers/anthropic.rb +1 -1
  20. data/lib/llm/providers/bedrock/models.rb +2 -2
  21. data/lib/llm/providers/bedrock.rb +1 -1
  22. data/lib/llm/providers/google.rb +1 -1
  23. data/lib/llm/providers/ollama.rb +1 -1
  24. data/lib/llm/providers/openai/responses.rb +2 -1
  25. data/lib/llm/providers/openai.rb +1 -1
  26. data/lib/llm/providers/openrouter.rb +87 -0
  27. data/lib/llm/repl/buffer.rb +20 -5
  28. data/lib/llm/repl/input.rb +5 -4
  29. data/lib/llm/repl/markdown/table.rb +5 -2
  30. data/lib/llm/repl/node.rb +26 -1
  31. data/lib/llm/repl/stream.rb +30 -3
  32. data/lib/llm/repl/window.rb +7 -7
  33. data/lib/llm/repl.rb +1 -0
  34. data/lib/llm/skill.rb +7 -1
  35. data/lib/llm/stream.rb +8 -3
  36. data/lib/llm/tools/git.rb +2 -2
  37. data/lib/llm/tools/mkdir.rb +2 -2
  38. data/lib/llm/tools/rg.rb +2 -2
  39. data/lib/llm/tools/ruby.rb +2 -2
  40. data/lib/llm/tools/shell.rb +2 -2
  41. data/lib/llm/tools/utils.rb +1 -1
  42. data/lib/llm/transport/curb.rb +5 -3
  43. data/lib/llm/transport/http.rb +5 -2
  44. data/lib/llm/transport/persistent_http.rb +6 -4
  45. data/lib/llm/transport/utils.rb +8 -6
  46. data/lib/llm/version.rb +1 -1
  47. data/lib/llm.rb +16 -1
  48. data/llm.gemspec +2 -1
  49. metadata +19 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b2700592b8b9fbb85694c24a0a7379a84494b0e4f2157d6302fad1552df9d684
4
- data.tar.gz: 43864e55f008478a40860511f9ca138f86010695225f91f0b2a07fbcff1ed448
3
+ metadata.gz: 2b496d3aec5a309af0ff9eaac32cabb6c7191bf2bac1f3b664c02a56c593402e
4
+ data.tar.gz: 0ae83d7794b4dad881de7b9866b3894746efa1853e349fc5707416e367802db5
5
5
  SHA512:
6
- metadata.gz: 464e203f6e4dc1fb2ea2aaa5bd4a2f95753d569190e63e4cd590b014aed4e369fa2f2835a4d6d95394b6988210c880e976abd5d8e8c69f015b057bb1f193609d
7
- data.tar.gz: 9303d2475e2bd32dc53b65b7b9d54fa28611616a0714015bd1c0d74e8ca7cd66e6d88889b73231ed19dda38b2f2888ded08569694f23bd834a8e7c1f3fd6db35
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", "~> 1.1"` ensures a minimum gem version
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 13+ providers. Streaming, tools, guards, compaction, the
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 most effective way to learn about llm.rb is to ask [the r.uby.dev chatbot](https://r.uby.dev)
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 on_rate_limit(error)
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 Email < ApplicationRecord
344
- acts_as_agent do |agent|
345
- agent.set name: "mail",
346
- instructions: "Write concise, friendly replies to emails",
347
- model: "deepseek-v4-pro"
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 draft_reply!
351
- talk("Draft a reply to:\n\n#{body}")
361
+ def research_issues
362
+ talk("research open pull requests on r-uby-dev/llm")
352
363
  end
353
364
 
354
- def summarize
355
- talk("Summarize this email thread in a few sentences")
365
+ def research_codebase
366
+ talk("research the codebase on r-uby-dev/llm")
356
367
  end
357
368
 
358
- private
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
- # By convention, this method defines the provider for a model.
362
- # If necessary, it can be renamed with: provider: :your_method.
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(key: ENV["KEY"])
388
+ LLM.deepseek
365
389
  end
366
390
 
367
- ##
368
- # By convention, this method returns the context options given
369
- # to LLM::Context or LLM::Agent. This method can be left undefined.
370
- def set_context
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
- email = Email.create!(subject: "Streaming support", body: "How do I stream responses?")
376
- email.draft_reply!
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
- # The conversation (the email and the draft
380
- # reply) is persisted to the email's column. A
381
- # fresh instance restores it and continues the
382
- # thread, so the summary below knows what was
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. Set `retry_budget`
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(transport:)
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(transport:)
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!"