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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +716 -1826
- data/README.md +668 -253
- data/bin/llm.rb +156 -56
- data/data/alibaba.json +1999 -0
- data/data/anthropic.json +195 -252
- data/data/bedrock.json +2189 -1854
- data/data/deepinfra.json +1312 -728
- data/data/deepseek.json +42 -39
- data/data/google.json +1024 -404
- data/data/mistral.json +481 -401
- data/data/moonshot.json +384 -0
- data/data/openai.json +985 -1354
- data/data/xai.json +220 -115
- data/data/zai.json +166 -166
- data/docs/deepdive/advanced/cancellation.md +74 -0
- data/docs/deepdive/advanced/compaction.md +85 -0
- data/docs/deepdive/advanced/context.md +280 -0
- data/docs/deepdive/advanced/guard.md +371 -0
- data/docs/deepdive/advanced/transformer.md +67 -0
- data/docs/deepdive/advanced/transports.md +45 -0
- data/docs/deepdive/features/builtin_tools.md +191 -0
- data/docs/deepdive/features/concurrency.md +110 -0
- data/docs/deepdive/features/database.md +449 -0
- data/docs/deepdive/features/embeddings.md +157 -0
- data/docs/deepdive/features/repl.md +141 -0
- data/docs/deepdive/fundamentals/agents.md +253 -0
- data/docs/deepdive/fundamentals/providers.md +159 -0
- data/docs/deepdive/fundamentals/schema.md +61 -0
- data/docs/deepdive/fundamentals/skills.md +111 -0
- data/docs/deepdive/fundamentals/stream.md +143 -0
- data/docs/deepdive/fundamentals/tools.md +329 -0
- data/docs/deepdive/media/audio.md +122 -0
- data/docs/deepdive/media/images.md +89 -0
- data/docs/deepdive/media/ocr.md +48 -0
- data/docs/deepdive/protocols/a2a.md +106 -0
- data/docs/deepdive/protocols/mcp.md +111 -0
- data/docs/deepdive/reference/cost.md +109 -0
- data/docs/deepdive/reference/model_registry.md +271 -0
- data/docs/deepdive/reference/object.md +108 -0
- data/docs/deepdive/reference/tracer.md +187 -0
- data/{resources → docs}/deepdive.md +37 -23
- data/lib/llm/a2a/transport/http.rb +1 -1
- data/lib/llm/active_record/acts_as_llm.rb +19 -5
- data/lib/llm/agent.rb +107 -17
- data/lib/llm/context.rb +164 -134
- data/lib/llm/cost.rb +114 -49
- data/lib/llm/error.rb +7 -8
- data/lib/llm/function/array.rb +1 -1
- data/lib/llm/function/async/task.rb +2 -0
- data/lib/llm/function/fiber/task.rb +2 -0
- data/lib/llm/function/fork/task.rb +16 -1
- data/lib/llm/function/ractor/task.rb +2 -0
- data/lib/llm/function/sequential/group.rb +20 -10
- data/lib/llm/function/sequential/task.rb +2 -9
- data/lib/llm/function/task.rb +4 -0
- data/lib/llm/function/thread/task.rb +2 -0
- data/lib/llm/function.rb +33 -4
- data/lib/llm/guard/loop.rb +89 -0
- data/lib/llm/guard/null.rb +19 -0
- data/lib/llm/guard.rb +61 -0
- data/lib/llm/message.rb +5 -4
- data/lib/llm/provider.rb +43 -0
- data/lib/llm/providers/alibaba/error_handler.rb +34 -0
- data/lib/llm/providers/alibaba/request_adapter.rb +13 -0
- data/lib/llm/providers/alibaba.rb +93 -0
- data/lib/llm/providers/anthropic/stream_parser.rb +1 -0
- data/lib/llm/providers/anthropic.rb +1 -9
- data/lib/llm/providers/bedrock/stream_parser.rb +1 -0
- data/lib/llm/providers/bedrock.rb +9 -9
- data/lib/llm/providers/deepseek/request_adapter.rb +2 -33
- data/lib/llm/providers/google/stream_parser.rb +1 -0
- data/lib/llm/providers/google.rb +1 -9
- data/lib/llm/providers/moonshot.rb +76 -0
- data/lib/llm/providers/ollama.rb +1 -9
- data/lib/llm/providers/openai/responses/stream_parser.rb +1 -0
- data/lib/llm/providers/openai/responses.rb +6 -9
- data/lib/llm/providers/openai/schema.rb +37 -0
- data/lib/llm/providers/openai/stream_parser.rb +1 -0
- data/lib/llm/providers/openai.rb +4 -12
- data/lib/llm/registry/model.rb +186 -0
- data/lib/llm/registry.rb +45 -14
- data/lib/llm/repl/bar.rb +11 -12
- data/lib/llm/repl/buffer.rb +43 -16
- data/lib/llm/repl/color.rb +85 -0
- data/lib/llm/repl/command.rb +12 -0
- data/lib/llm/repl/commands/model.rb +39 -0
- data/lib/llm/repl/input/cache.rb +45 -0
- data/lib/llm/repl/input/char.rb +46 -0
- data/lib/llm/repl/input/row.rb +39 -0
- data/lib/llm/repl/input.rb +327 -79
- data/lib/llm/repl/markdown/table.rb +6 -2
- data/lib/llm/repl/markdown.rb +56 -6
- data/lib/llm/repl/node.rb +7 -0
- data/lib/llm/repl/status.rb +54 -5
- data/lib/llm/repl/stream.rb +16 -4
- data/lib/llm/repl/walker.rb +3 -2
- data/lib/llm/repl/window.rb +111 -11
- data/lib/llm/repl.rb +47 -21
- data/lib/llm/sequel/plugin.rb +19 -5
- data/lib/llm/skill.rb +21 -8
- data/lib/llm/stream.rb +35 -7
- data/lib/llm/tool.rb +27 -0
- data/lib/llm/tools/rg.rb +2 -1
- data/lib/llm/transformer/null.rb +21 -0
- data/lib/llm/transformer.rb +55 -0
- data/lib/llm/transport/curb.rb +23 -3
- data/lib/llm/usage.rb +155 -9
- data/lib/llm/version.rb +1 -1
- data/lib/llm.rb +121 -29
- data/llm.gemspec +16 -10
- metadata +100 -13
- 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
|
|
10
|
-
@
|
|
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 {
|
|
14
|
-
|
|
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
|
-
|
|
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
|
-
|
|
44
|
-
|
|
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
|
-
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
if
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
|
|
117
|
-
|
|
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
|
-
|
|
120
|
-
warn "llm.rb:
|
|
121
|
-
|
|
218
|
+
rescue Interrupt
|
|
219
|
+
warn "llm.rb: Bye!"
|
|
220
|
+
rescue => ex
|
|
221
|
+
fatal(ex)
|
|
122
222
|
end
|
|
123
223
|
end
|
|
124
224
|
main(ARGV)
|