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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +396 -1826
- data/README.md +596 -490
- data/bin/llm.rb +148 -72
- data/data/alibaba.json +1999 -0
- data/data/anthropic.json +205 -205
- data/data/bedrock.json +2171 -2114
- data/data/deepinfra.json +1143 -938
- data/data/deepseek.json +4 -5
- data/data/google.json +691 -779
- data/data/mistral.json +450 -450
- data/data/moonshot.json +100 -100
- data/data/openai.json +976 -976
- data/data/xai.json +193 -116
- data/data/zai.json +187 -187
- data/{resources → docs}/deepdive/advanced/cancellation.md +2 -2
- data/{resources → docs}/deepdive/advanced/compaction.md +5 -3
- data/{resources → docs}/deepdive/advanced/context.md +13 -0
- data/{resources → docs}/deepdive/advanced/guard.md +1 -1
- data/{resources/deepdive/fundamentals → docs/deepdive/features}/concurrency.md +6 -0
- data/{resources/deepdive/fundamentals → docs/deepdive/features}/repl.md +56 -2
- data/{resources → docs}/deepdive/fundamentals/agents.md +52 -1
- data/docs/deepdive/fundamentals/providers.md +159 -0
- data/{resources → docs}/deepdive/fundamentals/skills.md +5 -0
- data/{resources → docs}/deepdive/fundamentals/stream.md +36 -3
- data/{resources → docs}/deepdive/fundamentals/tools.md +87 -23
- data/{resources/deepdive/everything_else → docs/deepdive/reference}/cost.md +20 -10
- data/docs/deepdive/reference/model_registry.md +271 -0
- data/{resources/deepdive/advanced → docs/deepdive/reference}/tracer.md +7 -0
- data/{resources → docs}/deepdive.md +35 -27
- 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 +62 -5
- data/lib/llm/context.rb +93 -46
- data/lib/llm/cost.rb +110 -51
- data/lib/llm/error.rb +7 -0
- data/lib/llm/function/array.rb +1 -1
- data/lib/llm/function/fork/task.rb +14 -1
- data/lib/llm/function/sequential/group.rb +20 -13
- data/lib/llm/function/sequential/task.rb +1 -8
- data/lib/llm/function.rb +5 -4
- data/lib/llm/message.rb +5 -4
- data/lib/llm/provider.rb +7 -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.rb +0 -1
- data/lib/llm/providers/bedrock.rb +8 -1
- data/lib/llm/providers/deepseek/request_adapter.rb +2 -33
- data/lib/llm/providers/google.rb +0 -1
- data/lib/llm/providers/ollama.rb +0 -1
- data/lib/llm/providers/openai/responses.rb +0 -1
- data/lib/llm/providers/openai/schema.rb +37 -0
- data/lib/llm/providers/openai.rb +1 -2
- data/lib/llm/registry/model.rb +186 -0
- data/lib/llm/registry.rb +45 -14
- data/lib/llm/repl/bar.rb +11 -13
- data/lib/llm/repl/buffer.rb +1 -1
- data/lib/llm/repl/color.rb +8 -1
- 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 +2 -2
- data/lib/llm/repl/input.rb +86 -23
- data/lib/llm/repl/markdown.rb +25 -1
- data/lib/llm/repl/node.rb +7 -0
- data/lib/llm/repl/status.rb +17 -3
- data/lib/llm/repl/window.rb +91 -11
- data/lib/llm/repl.rb +18 -8
- data/lib/llm/sequel/plugin.rb +19 -5
- data/lib/llm/skill.rb +21 -8
- data/lib/llm/stream.rb +27 -0
- data/lib/llm/tool.rb +3 -5
- data/lib/llm/tools/rg.rb +2 -1
- 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 +111 -29
- data/llm.gemspec +16 -11
- metadata +89 -35
- /data/{resources → docs}/deepdive/advanced/transformer.md +0 -0
- /data/{resources → docs}/deepdive/advanced/transports.md +0 -0
- /data/{resources/deepdive/fundamentals → docs/deepdive/features}/builtin_tools.md +0 -0
- /data/{resources/deepdive/fundamentals → docs/deepdive/features}/database.md +0 -0
- /data/{resources/deepdive/fundamentals → docs/deepdive/features}/embeddings.md +0 -0
- /data/{resources → docs}/deepdive/fundamentals/schema.md +0 -0
- /data/{resources/deepdive/everything_else → docs/deepdive/media}/audio.md +0 -0
- /data/{resources/deepdive/everything_else → docs/deepdive/media}/images.md +0 -0
- /data/{resources/deepdive/everything_else → docs/deepdive/media}/ocr.md +0 -0
- /data/{resources → docs}/deepdive/protocols/a2a.md +0 -0
- /data/{resources → docs}/deepdive/protocols/mcp.md +0 -0
- /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
|
|
12
|
-
@
|
|
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 {
|
|
16
|
-
|
|
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
|
-
|
|
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
|
-
|
|
46
|
-
|
|
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
|
-
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
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
|
-
provider = provider.downcase
|
|
106
187
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
|
|
132
|
-
|
|
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
|
-
|
|
144
|
-
warn "llm.rb:
|
|
145
|
-
|
|
218
|
+
rescue Interrupt
|
|
219
|
+
warn "llm.rb: Bye!"
|
|
220
|
+
rescue => ex
|
|
221
|
+
fatal(ex)
|
|
146
222
|
end
|
|
147
223
|
end
|
|
148
224
|
main(ARGV)
|