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
data/bin/llm.rb CHANGED
@@ -1,17 +1,45 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
3
  require "llm"
4
+ require "json"
4
5
  require "fileutils"
6
+ require "securerandom"
5
7
 
6
8
  ##
7
9
  # utils
8
10
 
9
- def keys
10
- @keys ||= Dir[File.join(__dir__, "..", "lib", "llm", "providers", "*")]
11
+ def providers
12
+ @providers ||= Dir[File.join(__dir__, "..", "lib", "llm", "providers", "*")]
11
13
  .select { File.file?(_1) }
12
14
  .map { File.basename(_1, ".rb") }
13
- .sort_by { _1 == "deepseek" ? 0 : 1 }
14
- .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
15
43
  end
16
44
 
17
45
  def help
@@ -21,12 +49,16 @@ def help
21
49
  warn ""
22
50
  warn "Options:"
23
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"
24
54
  warn " -t Temporary session that doesn't persist to disk"
25
55
  warn " -h Show this help"
26
56
  warn ""
27
57
  warn "Examples:"
28
58
  warn " #{prog} # auto-detect provider from $PROVIDER_API_KEY"
29
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"
30
62
  warn " #{prog} -h # this help"
31
63
  warn ""
32
64
  end
@@ -36,18 +68,39 @@ def loaderror(ex)
36
68
  warn ""
37
69
  warn " ── llm.rb ──────────────────────────────────────────────"
38
70
  warn ""
39
- warn " ✖ Missing dependency: #{gem}"
71
+ warn_title "Missing dependency: #{gem}"
40
72
  warn ""
41
73
  warn " The repl needs this gem, but it's not installed."
42
74
  warn ""
43
- warn " Fix: gem install #{gem}"
44
- warn " Or: bundle add #{gem}"
75
+ wrapped "Fix: gem install #{gem}", " "
76
+ wrapped "Or: bundle add #{gem}", " "
45
77
  warn ""
46
78
  warn " Tip: If you don't need the repl, you can use the"
47
- 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"
48
100
  warn ""
49
101
  warn " ───────────────────────────────────────────────────────"
50
102
  warn ""
103
+ exit 1
51
104
  end
52
105
 
53
106
  ##
@@ -64,61 +117,108 @@ def main(argv)
64
117
  exit 1
65
118
  end
66
119
 
67
- ##
68
- # C-Style option parser
69
- # No external dep
70
- while option = argv.shift
71
- case option
72
- when '-h'
73
- help
74
- exit 0
75
- when '-t'
76
- temp = true
77
- when '-p'
78
- provider = argv.shift
79
- else
80
- warn "llm.rb: unknown option #{option}"
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}"
158
+ help
159
+ exit 1
160
+ end
81
161
  end
82
- end
83
-
84
- ##
85
- # Setup the home directory
86
- # But only if the `-t` switch has not been provided
87
- if temp.nil?
88
- home = File.join(Dir.home, ".llm.rb")
89
- FileUtils.mkdir_p(File.join(home, Dir.getwd))
90
- session = File.join(home, Dir.getwd, "session.json")
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
187
 
106
- ##
107
- # We're ready to start the REPL
108
- # This should always succeed unless -p gave garbage
109
- provider = provider.downcase
110
- if LLM.respond_to?(provider)
111
- key ||= "#{provider.upcase}_API_KEY"
112
- if ENV[key].nil? || ENV[key].to_s.empty?
113
- warn "llm.rb: set #{key} to use #{provider}"
114
- exit 1
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")
209
+ File.binwrite file, JSON.pretty_generate(data)
115
210
  end
116
- llm = LLM.method(provider).call(key: ENV[key])
117
- agent = LLM::Agent.new(llm, path: temp ? nil : session, tools: LLM::Tool.subclasses)
211
+
212
+ ##
213
+ # Let's go!
214
+ concurrency ||= :sequential
215
+ path = temp ? nil : data[Dir.getwd]
216
+ agent = LLM::Agent.new(llm, path:, concurrency:, tools: LLM::Tool.subclasses)
118
217
  agent.repl
119
- else
120
- warn "llm.rb: #{provider} was not recognized"
121
- exit 1
218
+ rescue Interrupt
219
+ warn "llm.rb: Bye!"
220
+ rescue => ex
221
+ fatal(ex)
122
222
  end
123
223
  end
124
224
  main(ARGV)