llm.rb 14.0.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 (92) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +396 -1826
  3. data/README.md +596 -490
  4. data/bin/llm.rb +148 -72
  5. data/data/alibaba.json +1999 -0
  6. data/data/anthropic.json +205 -205
  7. data/data/bedrock.json +2171 -2114
  8. data/data/deepinfra.json +1143 -938
  9. data/data/deepseek.json +4 -5
  10. data/data/google.json +691 -779
  11. data/data/mistral.json +450 -450
  12. data/data/moonshot.json +100 -100
  13. data/data/openai.json +976 -976
  14. data/data/xai.json +193 -116
  15. data/data/zai.json +187 -187
  16. data/{resources → docs}/deepdive/advanced/cancellation.md +2 -2
  17. data/{resources → docs}/deepdive/advanced/compaction.md +5 -3
  18. data/{resources → docs}/deepdive/advanced/context.md +13 -0
  19. data/{resources → docs}/deepdive/advanced/guard.md +1 -1
  20. data/{resources/deepdive/fundamentals → docs/deepdive/features}/concurrency.md +6 -0
  21. data/{resources/deepdive/fundamentals → docs/deepdive/features}/repl.md +56 -2
  22. data/{resources → docs}/deepdive/fundamentals/agents.md +52 -1
  23. data/docs/deepdive/fundamentals/providers.md +159 -0
  24. data/{resources → docs}/deepdive/fundamentals/skills.md +5 -0
  25. data/{resources → docs}/deepdive/fundamentals/stream.md +36 -3
  26. data/{resources → docs}/deepdive/fundamentals/tools.md +87 -23
  27. data/{resources/deepdive/everything_else → docs/deepdive/reference}/cost.md +20 -10
  28. data/docs/deepdive/reference/model_registry.md +271 -0
  29. data/{resources/deepdive/advanced → docs/deepdive/reference}/tracer.md +7 -0
  30. data/{resources → docs}/deepdive.md +35 -27
  31. data/lib/llm/a2a/transport/http.rb +1 -1
  32. data/lib/llm/active_record/acts_as_llm.rb +19 -5
  33. data/lib/llm/agent.rb +62 -5
  34. data/lib/llm/context.rb +93 -46
  35. data/lib/llm/cost.rb +110 -51
  36. data/lib/llm/error.rb +7 -0
  37. data/lib/llm/function/array.rb +1 -1
  38. data/lib/llm/function/fork/task.rb +14 -1
  39. data/lib/llm/function/sequential/group.rb +20 -13
  40. data/lib/llm/function/sequential/task.rb +1 -8
  41. data/lib/llm/function.rb +5 -4
  42. data/lib/llm/message.rb +5 -4
  43. data/lib/llm/provider.rb +7 -0
  44. data/lib/llm/providers/alibaba/error_handler.rb +34 -0
  45. data/lib/llm/providers/alibaba/request_adapter.rb +13 -0
  46. data/lib/llm/providers/alibaba.rb +93 -0
  47. data/lib/llm/providers/anthropic.rb +0 -1
  48. data/lib/llm/providers/bedrock.rb +8 -1
  49. data/lib/llm/providers/deepseek/request_adapter.rb +2 -33
  50. data/lib/llm/providers/google.rb +0 -1
  51. data/lib/llm/providers/ollama.rb +0 -1
  52. data/lib/llm/providers/openai/responses.rb +0 -1
  53. data/lib/llm/providers/openai/schema.rb +37 -0
  54. data/lib/llm/providers/openai.rb +1 -2
  55. data/lib/llm/registry/model.rb +186 -0
  56. data/lib/llm/registry.rb +45 -14
  57. data/lib/llm/repl/bar.rb +11 -13
  58. data/lib/llm/repl/buffer.rb +1 -1
  59. data/lib/llm/repl/color.rb +8 -1
  60. data/lib/llm/repl/command.rb +12 -0
  61. data/lib/llm/repl/commands/model.rb +39 -0
  62. data/lib/llm/repl/input/cache.rb +45 -0
  63. data/lib/llm/repl/input/char.rb +2 -2
  64. data/lib/llm/repl/input.rb +86 -23
  65. data/lib/llm/repl/markdown.rb +25 -1
  66. data/lib/llm/repl/node.rb +7 -0
  67. data/lib/llm/repl/status.rb +17 -3
  68. data/lib/llm/repl/window.rb +91 -11
  69. data/lib/llm/repl.rb +18 -8
  70. data/lib/llm/sequel/plugin.rb +19 -5
  71. data/lib/llm/skill.rb +21 -8
  72. data/lib/llm/stream.rb +27 -0
  73. data/lib/llm/tool.rb +3 -5
  74. data/lib/llm/tools/rg.rb +2 -1
  75. data/lib/llm/transport/curb.rb +23 -3
  76. data/lib/llm/usage.rb +155 -9
  77. data/lib/llm/version.rb +1 -1
  78. data/lib/llm.rb +111 -29
  79. data/llm.gemspec +16 -11
  80. metadata +89 -35
  81. /data/{resources → docs}/deepdive/advanced/transformer.md +0 -0
  82. /data/{resources → docs}/deepdive/advanced/transports.md +0 -0
  83. /data/{resources/deepdive/fundamentals → docs/deepdive/features}/builtin_tools.md +0 -0
  84. /data/{resources/deepdive/fundamentals → docs/deepdive/features}/database.md +0 -0
  85. /data/{resources/deepdive/fundamentals → docs/deepdive/features}/embeddings.md +0 -0
  86. /data/{resources → docs}/deepdive/fundamentals/schema.md +0 -0
  87. /data/{resources/deepdive/everything_else → docs/deepdive/media}/audio.md +0 -0
  88. /data/{resources/deepdive/everything_else → docs/deepdive/media}/images.md +0 -0
  89. /data/{resources/deepdive/everything_else → docs/deepdive/media}/ocr.md +0 -0
  90. /data/{resources → docs}/deepdive/protocols/a2a.md +0 -0
  91. /data/{resources → docs}/deepdive/protocols/mcp.md +0 -0
  92. /data/{resources/deepdive/everything_else → docs/deepdive/reference}/object.md +0 -0
data/bin/llm.rb CHANGED
@@ -8,12 +8,38 @@ require "securerandom"
8
8
  ##
9
9
  # utils
10
10
 
11
- def keys
12
- @keys ||= Dir[File.join(__dir__, "..", "lib", "llm", "providers", "*")]
11
+ def providers
12
+ @providers ||= Dir[File.join(__dir__, "..", "lib", "llm", "providers", "*")]
13
13
  .select { File.file?(_1) }
14
14
  .map { File.basename(_1, ".rb") }
15
- .sort_by { _1 == "deepseek" ? 0 : 1 }
16
- .map { "#{_1.upcase}_API_KEY" } - ["BEDROCK_API_KEY"]
15
+ .sort_by { |provider|
16
+ # <3 DeepSeek
17
+ case provider
18
+ when "deepseek" then -1
19
+ when "ollama", "llamacpp" then 1
20
+ else 0
21
+ end
22
+ }
23
+ end
24
+
25
+ def warn_title(text)
26
+ wrap(text, 51).each_with_index { |chunk, i| warn(i.zero? ? " ✖ #{chunk}" : " #{chunk}") }
27
+ end
28
+
29
+ def wrapped(text, prefix)
30
+ line = "#{prefix}#{text}"
31
+ line.length <= 58 ? warn(line) : wrap(text, 51).each { |chunk| warn "#{prefix}#{chunk}" }
32
+ end
33
+
34
+ def wrap(text, width)
35
+ text.split.each_with_object([+""]) do |word, lines|
36
+ line_length = lines.last.length
37
+ word_length = word.length
38
+ if line_length.positive? and line_length + word_length + 1 > width
39
+ lines << +""
40
+ end
41
+ lines.last << (line_length.zero? ? word : " #{word}")
42
+ end
17
43
  end
18
44
 
19
45
  def help
@@ -23,12 +49,16 @@ def help
23
49
  warn ""
24
50
  warn "Options:"
25
51
  warn " -p PROVIDER Choose a provider"
52
+ warn " -c STRATEGY Concurrency strategy for tool calls (eg thread, async, fork)"
53
+ warn " -n TRANSPORT HTTP transports - net-http (default), net-http-persistent, and curb"
26
54
  warn " -t Temporary session that doesn't persist to disk"
27
55
  warn " -h Show this help"
28
56
  warn ""
29
57
  warn "Examples:"
30
58
  warn " #{prog} # auto-detect provider from $PROVIDER_API_KEY"
31
59
  warn " #{prog} -p openai # use OpenAI"
60
+ warn " #{prog} -n curb # use libcurl"
61
+ warn " #{prog} -c thread # run tool calls on a separate thread"
32
62
  warn " #{prog} -h # this help"
33
63
  warn ""
34
64
  end
@@ -38,18 +68,39 @@ def loaderror(ex)
38
68
  warn ""
39
69
  warn " ── llm.rb ──────────────────────────────────────────────"
40
70
  warn ""
41
- warn " ✖ Missing dependency: #{gem}"
71
+ warn_title "Missing dependency: #{gem}"
42
72
  warn ""
43
73
  warn " The repl needs this gem, but it's not installed."
44
74
  warn ""
45
- warn " Fix: gem install #{gem}"
46
- warn " Or: bundle add #{gem}"
75
+ wrapped "Fix: gem install #{gem}", " "
76
+ wrapped "Or: bundle add #{gem}", " "
47
77
  warn ""
48
78
  warn " Tip: If you don't need the repl, you can use the"
49
- warn " library directly with: require \"llm\""
79
+ wrapped "library directly with: require \"llm\"", " "
80
+ warn ""
81
+ warn " ───────────────────────────────────────────────────────"
82
+ warn ""
83
+ end
84
+
85
+ def fatal(ex)
86
+ title = ex.message.lines.first.to_s.strip
87
+ detail = ex.message.lines.drop(1).map(&:strip).reject(&:empty?).first
88
+ warn ""
89
+ warn " ── llm.rb ──────────────────────────────────────────────"
90
+ warn ""
91
+ warn_title "#{ex.class}: #{title}"
92
+ if detail
93
+ warn ""
94
+ wrapped detail, " "
95
+ end
96
+ warn ""
97
+ warn " This is an unexpected error. If it keeps happening,"
98
+ warn " consider opening an issue at"
99
+ warn " https://github.com/r-uby-dev/llm/issues"
50
100
  warn ""
51
101
  warn " ───────────────────────────────────────────────────────"
52
102
  warn ""
103
+ exit 1
53
104
  end
54
105
 
55
106
  ##
@@ -66,83 +117,108 @@ def main(argv)
66
117
  exit 1
67
118
  end
68
119
 
69
- ##
70
- # C-Style option parser
71
- # No external dep
72
- while option = argv.shift
73
- case option
74
- when '-h'
75
- help
76
- exit 0
77
- when '-t'
78
- temp = true
79
- when '-p'
80
- provider = argv.shift
81
- if provider.nil?
82
- warn "llm.rb: -p switch requires an argument"
120
+ begin
121
+ ##
122
+ # C-Style option parser
123
+ # No external dep
124
+ while option = argv.shift
125
+ case option
126
+ when '-h'
127
+ help
128
+ exit 0
129
+ when '-t'
130
+ temp = true
131
+ when '-c'
132
+ concurrency = argv.shift
133
+ if concurrency.nil?
134
+ warn "llm.rb: -c switch requires an argument"
135
+ help
136
+ exit 1
137
+ else
138
+ concurrency = concurrency.to_sym
139
+ end
140
+ when '-n'
141
+ transport = argv.shift
142
+ if transport.nil?
143
+ warn "llm.rb: -n switch requires an argument"
144
+ help
145
+ exit 1
146
+ else
147
+ transport = transport.gsub("-", "_").to_sym
148
+ end
149
+ when '-p'
150
+ provider = argv.shift
151
+ if provider.nil?
152
+ warn "llm.rb: -p switch requires an argument"
153
+ help
154
+ exit 1
155
+ end
156
+ else
157
+ warn "llm.rb: unknown option #{option}"
83
158
  help
84
159
  exit 1
85
160
  end
86
- else
87
- warn "llm.rb: unknown option #{option}"
88
- help
89
- exit 1
90
161
  end
91
- end
92
162
 
93
- ##
94
- # No provider has been given.
95
- # Try to infer one.
96
- if provider.nil?
97
- key = keys.find { ENV[_1] }
98
- if key.nil?
99
- warn "llm.rb: provide a provider with the -p switch"
100
- exit 1
163
+ ##
164
+ # No provider has been given.
165
+ # Try to infer one.
166
+ transport ||= :net_http
167
+ if provider.nil?
168
+ llm = providers.filter_map do
169
+ LLM.method(_1).call(transport:)
170
+ rescue ArgumentError
171
+ end.first
172
+ if llm.nil?
173
+ warn "llm.rb: provide a provider with the -p switch"
174
+ exit 1
175
+ end
101
176
  else
102
- provider, = key.split("_")
177
+ begin
178
+ llm = LLM.method(provider).call(transport:)
179
+ rescue ArgumentError
180
+ warn "llm.rb: set credentials for #{provider}"
181
+ exit 1
182
+ rescue NameError
183
+ warn "llm.rb: unknown provider (#{provider})"
184
+ exit 1
185
+ end
103
186
  end
104
- end
105
- provider = provider.downcase
106
187
 
107
- ##
108
- # Setup the filesystem where <provider>.json maps
109
- # the current working directory to a session file,
110
- # and where the session file is stored in
111
- # `~/.llm.rb/<provider>/<uuid>.json`.
112
- # This can be skipped with the `-t` option.
113
- if temp.nil?
114
- home = File.join(Dir.home, ".llm.rb")
115
- file = File.join(home, "#{provider}.json")
116
- parent = File.join(home, provider)
117
-
118
- FileUtils.mkdir_p(parent)
119
- FileUtils.touch(file)
120
-
121
- if File.size(file).zero?
122
- data = LLM::Object.from({})
188
+ ##
189
+ # Setup the filesystem where <provider>.json maps
190
+ # the current working directory to a session file,
191
+ # and where the session file is stored in
192
+ # `~/.llm.rb/<provider>/<uuid>.json`.
193
+ # This can be skipped with the `-t` option.
194
+ if temp.nil?
195
+ home = File.join(Dir.home, ".llm.rb")
196
+ file = File.join(home, "#{llm.name}.json")
197
+ parent = File.join(home, llm.name.to_s)
198
+
199
+ FileUtils.mkdir_p(parent)
200
+ FileUtils.touch(file)
201
+
202
+ if File.size(file).zero?
203
+ data = LLM::Object.from({})
204
+ File.binwrite file, JSON.pretty_generate(data)
205
+ else
206
+ data = LLM::Object.from JSON.parse(File.read(file))
207
+ end
208
+ data[Dir.getwd] ||= File.join(parent, "#{SecureRandom.uuid}.json")
123
209
  File.binwrite file, JSON.pretty_generate(data)
124
- else
125
- data = LLM::Object.from JSON.parse(File.read(file))
126
210
  end
127
- data[Dir.getwd] ||= File.join(parent, "#{SecureRandom.uuid}.json")
128
- end
129
211
 
130
- ##
131
- # We're ready to start the REPL
132
- # This should always succeed unless -p gave garbage
133
- if LLM.respond_to?(provider)
134
- key ||= "#{provider.upcase}_API_KEY"
135
- if ENV[key].nil? || ENV[key].to_s.empty?
136
- warn "llm.rb: set #{key} to use #{provider}"
137
- exit 1
138
- end
139
- llm = LLM.method(provider).call(key: ENV[key])
212
+ ##
213
+ # Let's go!
214
+ concurrency ||= :sequential
140
215
  path = temp ? nil : data[Dir.getwd]
141
- agent = LLM::Agent.new(llm, path:, tools: LLM::Tool.subclasses)
216
+ agent = LLM::Agent.new(llm, path:, concurrency:, tools: LLM::Tool.subclasses)
142
217
  agent.repl
143
- else
144
- warn "llm.rb: #{provider} was not recognized"
145
- exit 1
218
+ rescue Interrupt
219
+ warn "llm.rb: Bye!"
220
+ rescue => ex
221
+ fatal(ex)
146
222
  end
147
223
  end
148
224
  main(ARGV)