llm.rb 15.0.3 → 15.2.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 +433 -3
- data/README.md +188 -71
- data/bin/llm.rb +50 -7
- data/data/alibaba.json +912 -823
- data/data/anthropic.json +234 -187
- data/data/bedrock.json +3702 -2058
- data/data/deepinfra.json +1288 -951
- data/data/deepseek.json +87 -53
- data/data/google.json +670 -670
- data/data/mistral.json +501 -460
- data/data/moonshot.json +43 -248
- data/data/openai.json +1008 -914
- data/data/openrouter.json +14417 -0
- data/data/xai.json +213 -201
- data/data/zai.json +242 -149
- data/docs/deepdive/advanced/compaction.md +5 -5
- data/docs/deepdive/advanced/context.md +8 -6
- data/docs/deepdive/advanced/guard.md +2 -2
- data/docs/deepdive/features/builtin_tools.md +93 -22
- data/docs/deepdive/features/{repl.md → console.md} +28 -28
- data/docs/deepdive/features/database.md +3 -3
- data/docs/deepdive/fundamentals/agents.md +13 -12
- data/docs/deepdive/fundamentals/providers.md +91 -6
- data/docs/deepdive/fundamentals/skills.md +14 -6
- data/docs/deepdive/fundamentals/stream.md +4 -4
- data/docs/deepdive/fundamentals/tools.md +63 -31
- data/docs/deepdive/reference/cost.md +2 -2
- data/docs/deepdive/reference/model_registry.md +2 -2
- data/docs/deepdive/reference/tracer.md +15 -13
- data/docs/deepdive.md +2 -2
- data/lib/llm/active_record/acts_as_agent.rb +9 -5
- data/lib/llm/agent.rb +40 -15
- data/lib/llm/{repl → console}/bar.rb +3 -3
- data/lib/llm/{repl → console}/buffer.rb +24 -9
- data/lib/llm/{repl → console}/color.rb +2 -2
- data/lib/llm/{repl → console}/command.rb +12 -12
- data/lib/llm/{repl → console}/commands/exit.rb +4 -4
- data/lib/llm/{repl → console}/commands/help.rb +1 -1
- data/lib/llm/{repl/commands/compact.rb → console/commands/keep.rb} +11 -9
- data/lib/llm/{repl → console}/commands/model.rb +2 -2
- data/lib/llm/{repl → console}/input/cache.rb +2 -2
- data/lib/llm/{repl → console}/input/char.rb +2 -2
- data/lib/llm/{repl → console}/input/row.rb +1 -1
- data/lib/llm/{repl → console}/input.rb +18 -10
- data/lib/llm/console/markdown/parser.rb +78 -0
- data/lib/llm/{repl → console}/markdown/table.rb +8 -5
- data/lib/llm/{repl → console}/markdown.rb +13 -30
- data/lib/llm/console/node.rb +69 -0
- data/lib/llm/{repl → console}/status.rb +11 -11
- data/lib/llm/{repl → console}/stream.rb +36 -9
- data/lib/llm/{repl → console}/walker.rb +1 -1
- data/lib/llm/{repl → console}/window.rb +17 -17
- data/lib/llm/{repl.rb → console.rb} +39 -19
- data/lib/llm/context/deserializer.rb +2 -1
- data/lib/llm/context.rb +29 -13
- data/lib/llm/cost.rb +13 -0
- data/lib/llm/function/async/reactor.rb +20 -1
- data/lib/llm/function/fork/task.rb +14 -10
- data/lib/llm/function.rb +1 -1
- data/lib/llm/json_adapter.rb +40 -28
- data/lib/llm/message.rb +7 -0
- data/lib/llm/provider.rb +31 -10
- data/lib/llm/providers/alibaba.rb +1 -1
- data/lib/llm/providers/anthropic.rb +1 -1
- data/lib/llm/providers/bedrock/models.rb +2 -2
- data/lib/llm/providers/bedrock.rb +1 -1
- data/lib/llm/providers/deepseek.rb +1 -1
- data/lib/llm/providers/google.rb +1 -1
- data/lib/llm/providers/ollama.rb +1 -1
- data/lib/llm/providers/openai/responses.rb +2 -1
- data/lib/llm/providers/openai.rb +2 -1
- data/lib/llm/providers/openrouter.rb +87 -0
- data/lib/llm/schema/leaf.rb +34 -2
- data/lib/llm/schema.rb +4 -2
- data/lib/llm/sequel/agent.rb +9 -5
- data/lib/llm/skill.rb +7 -1
- data/lib/llm/stream.rb +8 -3
- data/lib/llm/tool/param.rb +5 -1
- data/lib/llm/tool.rb +5 -0
- data/lib/llm/tools/bundle.rb +53 -0
- data/lib/llm/tools/edit-file.rb +7 -2
- data/lib/llm/tools/exec.rb +78 -0
- data/lib/llm/tools/git.rb +27 -26
- data/lib/llm/tools/mkdir.rb +12 -19
- data/lib/llm/tools/read_file.rb +69 -9
- data/lib/llm/tools/rg.rb +20 -24
- data/lib/llm/tools/ruby.rb +17 -25
- data/lib/llm/tools/utils.rb +75 -2
- data/lib/llm/tools/write_file.rb +4 -1
- data/lib/llm/tracer/logger.rb +2 -2
- data/lib/llm/tracer/pretty_logger.rb +4 -4
- data/lib/llm/tracer/telemetry.rb +2 -2
- data/lib/llm/tracer.rb +33 -0
- data/lib/llm/transport/curb.rb +5 -3
- data/lib/llm/transport/http.rb +5 -2
- data/lib/llm/transport/persistent_http.rb +6 -4
- data/lib/llm/transport/utils.rb +8 -6
- data/lib/llm/version.rb +1 -1
- data/lib/llm.rb +18 -12
- data/llm.gemspec +8 -8
- metadata +80 -37
- data/lib/llm/repl/node.rb +0 -44
- data/lib/llm/tools/shell.rb +0 -55
data/lib/llm/tools/read_file.rb
CHANGED
|
@@ -2,37 +2,97 @@
|
|
|
2
2
|
|
|
3
3
|
class LLM::Tool
|
|
4
4
|
##
|
|
5
|
-
# The {LLM::Tool::ReadFile
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
5
|
+
# The {LLM::Tool::ReadFile} class implements a tool that
|
|
6
|
+
# can read the contents of a file. It returns the content
|
|
7
|
+
# as structured lines ({lineno:, content:}), so the model can
|
|
8
|
+
# reference line numbers when requesting a narrower range.
|
|
9
9
|
class ReadFile < self
|
|
10
|
+
require_relative "utils"
|
|
11
|
+
include Utils
|
|
12
|
+
|
|
10
13
|
name "read-file"
|
|
11
14
|
description "read the contents of a file"
|
|
12
15
|
parameter :path, String, "the path to the file"
|
|
13
16
|
parameter :start, Integer, "start line number"
|
|
14
17
|
parameter :stop, Integer, "stop line number"
|
|
18
|
+
parameter :max_bytes, Integer, "the max number of bytes to return"
|
|
15
19
|
required %i[path]
|
|
16
20
|
|
|
21
|
+
##
|
|
22
|
+
# Returns (or sets) the advisory maximum number of bytes
|
|
23
|
+
# this tool returns to the model.
|
|
24
|
+
# @param [Integer, nil] bytes
|
|
25
|
+
# When given, sets the maximum
|
|
26
|
+
# @return [Integer]
|
|
27
|
+
def self.max_bytes(bytes = UNDEFINED)
|
|
28
|
+
if bytes.equal?(UNDEFINED)
|
|
29
|
+
@max_bytes || 75_000
|
|
30
|
+
else
|
|
31
|
+
@max_bytes = bytes
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
17
35
|
##
|
|
18
36
|
# @param [String] path
|
|
19
37
|
# @param [Integer] start
|
|
20
38
|
# @param [Integer] stop
|
|
39
|
+
# @param [Integer] max_bytes
|
|
21
40
|
# @return [Hash]
|
|
22
|
-
def call(path:, start: 1, stop: -1)
|
|
23
|
-
|
|
41
|
+
def call(path:, start: 1, stop: -1, max_bytes: self.class.max_bytes)
|
|
42
|
+
start, stop = swap_if_reversed(start, stop)
|
|
43
|
+
content = read(path, start, stop)
|
|
44
|
+
to_lines(content, start:, max_bytes:)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
##
|
|
50
|
+
# When start is greater than stop, swap them so the
|
|
51
|
+
# range reads in the right direction (e.g. start: 10,
|
|
52
|
+
# stop: 5 => reads lines 5-10).
|
|
53
|
+
# @param [Integer] start
|
|
54
|
+
# @param [Integer] stop
|
|
55
|
+
# @return [[Integer, Integer]]
|
|
56
|
+
def swap_if_reversed(start, stop)
|
|
57
|
+
stop != -1 && start > stop ? [stop, start] : [start, stop]
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
##
|
|
61
|
+
# Returns the file content within the start..stop range.
|
|
62
|
+
# @param [String] path
|
|
63
|
+
# @param [Integer] start
|
|
64
|
+
# @param [Integer] stop
|
|
65
|
+
# @return [String]
|
|
66
|
+
def read(path, start, stop)
|
|
24
67
|
File.open(path, "r") do |f|
|
|
68
|
+
cursor = 1
|
|
25
69
|
while cursor < start
|
|
26
70
|
f.gets
|
|
27
71
|
cursor += 1
|
|
28
72
|
end
|
|
29
73
|
if stop == -1
|
|
30
|
-
|
|
74
|
+
f.read
|
|
31
75
|
else
|
|
32
|
-
|
|
76
|
+
start.upto(stop).map { f.gets }.join
|
|
33
77
|
end
|
|
34
78
|
end
|
|
35
|
-
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
##
|
|
82
|
+
# Builds {lineno:, content:} lines from the content, capped at
|
|
83
|
+
# max_bytes via the shared truncate!. The truncation marker
|
|
84
|
+
# is kept out of the lines (it is not a real file line); the
|
|
85
|
+
# truncated flag communicates that content was cut.
|
|
86
|
+
# @param [String] content
|
|
87
|
+
# @param [Integer] start
|
|
88
|
+
# @param [Integer] max_bytes
|
|
89
|
+
# @return [Hash]
|
|
90
|
+
def to_lines(content, start:, max_bytes:)
|
|
91
|
+
body, truncated = truncate!(content, max_bytes:)
|
|
92
|
+
lines = body.lines.each_with_index.map do |line, index|
|
|
93
|
+
{lineno: start + index, content: line}
|
|
94
|
+
end
|
|
95
|
+
{ok: true, lines:, truncated:}
|
|
36
96
|
end
|
|
37
97
|
end
|
|
38
98
|
end
|
data/lib/llm/tools/rg.rb
CHANGED
|
@@ -7,49 +7,45 @@ class LLM::Tool
|
|
|
7
7
|
# recursively search the current working directory
|
|
8
8
|
# for one or more patterns.
|
|
9
9
|
class Rg < self
|
|
10
|
-
require_relative "
|
|
11
|
-
include Utils
|
|
10
|
+
require_relative "exec"
|
|
12
11
|
|
|
13
12
|
name "rg"
|
|
14
|
-
description "
|
|
13
|
+
description "Search the current directory for lines matching a pattern\n" \
|
|
14
|
+
"This command (rg) is spawned without a shell"
|
|
15
15
|
parameter :patterns, Array[String], "one or more search patterns"
|
|
16
16
|
parameter :path, String, "the path where the search is performed (default is cwd)"
|
|
17
17
|
parameter :timeout, Integer, "the number of seconds to wait before cancelling the action"
|
|
18
|
+
parameter :max_count, Integer, "the max number of results per file"
|
|
19
|
+
parameter :max_bytes, Integer, "the max number of bytes to return"
|
|
18
20
|
required %i[patterns]
|
|
19
|
-
defaults path: Dir.getwd, timeout: 5
|
|
21
|
+
defaults path: Dir.getwd, timeout: 5, max_count: 10, max_bytes: -> { Exec.max_bytes }
|
|
20
22
|
|
|
21
23
|
##
|
|
22
24
|
# @param [Array<String>] patterns
|
|
23
25
|
# @param [String] path
|
|
26
|
+
# @param [Integer] timeout
|
|
27
|
+
# @param [Integer] max_count
|
|
28
|
+
# @param [Integer] max_bytes
|
|
24
29
|
# @return [Hash]
|
|
25
|
-
def call(patterns:, path: Dir.getwd, timeout: 5)
|
|
26
|
-
validate!(patterns:, path:)
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
raise
|
|
30
|
+
def call(patterns:, path: Dir.getwd, timeout: 5, max_count: 10, max_bytes: Exec.max_bytes)
|
|
31
|
+
validate!(patterns:, path:, max_count:)
|
|
32
|
+
Exec.new.call(
|
|
33
|
+
arguments: ["rg", "-m", max_count, *[*patterns].flat_map { ["-e", _1] }, path],
|
|
34
|
+
timeout:,
|
|
35
|
+
max_bytes:
|
|
36
|
+
)
|
|
33
37
|
end
|
|
34
38
|
|
|
35
39
|
private
|
|
36
40
|
|
|
37
|
-
def validate!(patterns:, path:)
|
|
38
|
-
if
|
|
41
|
+
def validate!(patterns:, path:, max_count:)
|
|
42
|
+
if !(::Integer === max_count) || max_count.zero? || max_count.negative?
|
|
43
|
+
raise RuntimeError, "max_count must be a positive integer"
|
|
44
|
+
elsif path == "/"
|
|
39
45
|
raise RuntimeError, "you can't search from the root of the filesystem"
|
|
40
46
|
elsif patterns == ["."]
|
|
41
47
|
raise RuntimeError, "narrow your search"
|
|
42
48
|
end
|
|
43
49
|
end
|
|
44
|
-
|
|
45
|
-
def spawn(patterns:, path:)
|
|
46
|
-
Command
|
|
47
|
-
.new("rg")
|
|
48
|
-
.argv(*[*patterns].flat_map { ["-e", _1] }, path)
|
|
49
|
-
.spawn
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
LLM.require "test-cmd.rb", "~> 1.1"
|
|
53
|
-
Command = Test::Cmd
|
|
54
50
|
end
|
|
55
51
|
end
|
data/lib/llm/tools/ruby.rb
CHANGED
|
@@ -4,43 +4,35 @@ class LLM::Tool
|
|
|
4
4
|
##
|
|
5
5
|
# The {LLM::Tool::Ruby LLM::Tool::Ruby} class implements
|
|
6
6
|
# a tool that can execute an arbitrary string of Ruby code
|
|
7
|
-
# and return the result. It
|
|
8
|
-
#
|
|
7
|
+
# and return the result. It routes through {LLM::Tool::Exec}
|
|
8
|
+
# for process management.
|
|
9
9
|
class Ruby < self
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
require "rbconfig"
|
|
11
|
+
require_relative "exec"
|
|
12
12
|
|
|
13
13
|
name "ruby"
|
|
14
|
-
description "
|
|
14
|
+
description "Runs a string of ruby code\n" \
|
|
15
|
+
"This command (ruby) is spawned without a shell"
|
|
15
16
|
parameter :code, String, "a string of ruby code"
|
|
16
17
|
parameter :timeout, Integer, "maximum runtime before timeout"
|
|
18
|
+
parameter :max_bytes, Integer, "max number of bytes to emit"
|
|
17
19
|
required %i[code]
|
|
18
|
-
defaults timeout: 15
|
|
20
|
+
defaults timeout: 15, max_bytes: -> { Exec.max_bytes }
|
|
19
21
|
|
|
20
22
|
##
|
|
21
23
|
# @param [String] code
|
|
22
24
|
# Ruby code
|
|
23
25
|
# @param [Integer] timeout
|
|
24
26
|
# Runtime timeout
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
private
|
|
35
|
-
|
|
36
|
-
def spawn(code:)
|
|
37
|
-
Command
|
|
38
|
-
.new(RbConfig.ruby)
|
|
39
|
-
.argv("-e", code)
|
|
27
|
+
# @param [Integer] max_bytes
|
|
28
|
+
# Max bytes to emit
|
|
29
|
+
# @return [Hash]
|
|
30
|
+
def call(code:, timeout: 15, max_bytes: Exec.max_bytes)
|
|
31
|
+
Exec.new.call(
|
|
32
|
+
arguments: [RbConfig.ruby, "-e", code],
|
|
33
|
+
timeout:,
|
|
34
|
+
max_bytes:
|
|
35
|
+
)
|
|
40
36
|
end
|
|
41
|
-
|
|
42
|
-
require "rbconfig"
|
|
43
|
-
LLM.require "test-cmd.rb", "~> 1.1"
|
|
44
|
-
Command = Test::Cmd
|
|
45
37
|
end
|
|
46
38
|
end
|
data/lib/llm/tools/utils.rb
CHANGED
|
@@ -2,13 +2,53 @@
|
|
|
2
2
|
|
|
3
3
|
class LLM::Tool
|
|
4
4
|
##
|
|
5
|
-
#
|
|
5
|
+
# Tool utils.
|
|
6
6
|
module Utils
|
|
7
|
+
##
|
|
8
|
+
# Truncates a string so a tool return stays bounded.
|
|
9
|
+
# Appends a marker when truncated so the model knows
|
|
10
|
+
# more content was available. Bounds each individual
|
|
11
|
+
# component (string field) a tool returns: a tool that
|
|
12
|
+
# returns multiple strings (eg stdout and stderr) caps
|
|
13
|
+
# each separately.
|
|
14
|
+
# @param [String] content
|
|
15
|
+
# @param [Integer] max_bytes
|
|
16
|
+
# The max number of bytes to keep
|
|
17
|
+
# @return [String]
|
|
18
|
+
def truncate(content, max_bytes:)
|
|
19
|
+
body, truncated = truncate!(content, max_bytes:)
|
|
20
|
+
truncated ? "#{body}\n...\n[truncated: more than #{max_bytes} bytes]" : body
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
##
|
|
24
|
+
# Performs the truncation and returns a tuple where the
|
|
25
|
+
# first element is the content, which can be either
|
|
26
|
+
# truncated or left intact. The second element is a
|
|
27
|
+
# boolean that indicates whether truncation took place.
|
|
28
|
+
#
|
|
29
|
+
# The difference between {#truncate truncate} and this
|
|
30
|
+
# method is that {#truncate truncate} appends a marker
|
|
31
|
+
# to the truncated content that indicates truncation
|
|
32
|
+
# took place, while this method leaves the content bare
|
|
33
|
+
# so a caller can structure it itself (eg
|
|
34
|
+
# {LLM::Tool::ReadFile}) without parsing the marker back
|
|
35
|
+
# out.
|
|
36
|
+
# @param [String] content
|
|
37
|
+
# @param [Integer] max_bytes
|
|
38
|
+
# The max number of bytes to keep
|
|
39
|
+
# @return [[String, Boolean]]
|
|
40
|
+
# A tuple of the content and whether truncation took place
|
|
41
|
+
def truncate!(content, max_bytes:)
|
|
42
|
+
s = content.to_s
|
|
43
|
+
return [s, false] if s.bytesize <= max_bytes
|
|
44
|
+
[s.byteslice(0, max_bytes), true]
|
|
45
|
+
end
|
|
46
|
+
|
|
7
47
|
##
|
|
8
48
|
# Wait for a command to finish, or abort
|
|
9
49
|
# with an error when it exceeds the
|
|
10
50
|
# specified timeout.
|
|
11
|
-
# @param [Test::
|
|
51
|
+
# @param [Test::Command] command
|
|
12
52
|
# @param [Integer] timeout
|
|
13
53
|
# @return [void]
|
|
14
54
|
def wait(command:, timeout:)
|
|
@@ -22,10 +62,43 @@ class LLM::Tool
|
|
|
22
62
|
end
|
|
23
63
|
end
|
|
24
64
|
|
|
65
|
+
##
|
|
66
|
+
# Spawn a command from a name and arguments,
|
|
67
|
+
# without going through a shell. The command's
|
|
68
|
+
# stdout and stderr are each capped at `max_bytes`.
|
|
69
|
+
# @param [String] name
|
|
70
|
+
# The command name
|
|
71
|
+
# @param [Array<String>] arguments
|
|
72
|
+
# One or more arguments
|
|
73
|
+
# @param [Hash] env
|
|
74
|
+
# Extra environment variables to set for the command
|
|
75
|
+
# @param [Integer] max_bytes
|
|
76
|
+
# The max number of bytes to keep per stream.
|
|
77
|
+
# Defaults to the including tool's `self.class.max_bytes`.
|
|
78
|
+
# @raise [ArgumentError]
|
|
79
|
+
# When `max_bytes` is nil
|
|
80
|
+
# @return [Test::Command]
|
|
81
|
+
def spawn(name:, arguments:, env: {}, max_bytes: self.class.max_bytes)
|
|
82
|
+
if Integer(max_bytes, exception: false).nil?
|
|
83
|
+
raise ArgumentError, "max_bytes cannot be nil"
|
|
84
|
+
end
|
|
85
|
+
Command
|
|
86
|
+
.new(name)
|
|
87
|
+
.env(env)
|
|
88
|
+
.arguments(*[*arguments])
|
|
89
|
+
.limit(stdout: max_bytes, stderr: max_bytes)
|
|
90
|
+
.spawn
|
|
91
|
+
end
|
|
92
|
+
|
|
25
93
|
##
|
|
26
94
|
# @return [Numeric]
|
|
27
95
|
def now
|
|
28
96
|
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
29
97
|
end
|
|
98
|
+
|
|
99
|
+
##
|
|
100
|
+
# requires test-cmd.rb
|
|
101
|
+
LLM.require "test-cmd.rb", "~> 2.7.1"
|
|
102
|
+
Command = Test::Command
|
|
30
103
|
end
|
|
31
104
|
end
|
data/lib/llm/tools/write_file.rb
CHANGED
|
@@ -10,13 +10,16 @@ class LLM::Tool
|
|
|
10
10
|
description "write to a file"
|
|
11
11
|
parameter :path, String, "The file path"
|
|
12
12
|
parameter :content, String, "The file content"
|
|
13
|
+
parameter :newline, Boolean, "insert final newline (yes or no)"
|
|
13
14
|
required %i[path content]
|
|
15
|
+
defaults newline: true
|
|
14
16
|
|
|
15
17
|
##
|
|
16
18
|
# @param [String] path
|
|
17
19
|
# @param [String] content
|
|
18
20
|
# @return [Hash]
|
|
19
|
-
def call(path:, content:)
|
|
21
|
+
def call(path:, content:, newline: true)
|
|
22
|
+
content = content.end_with?("\n") ? content : "#{content}\n" if newline
|
|
20
23
|
File.open(path, "w") { _1.write(content) }
|
|
21
24
|
{ok: true}
|
|
22
25
|
end
|
data/lib/llm/tracer/logger.rb
CHANGED
|
@@ -9,9 +9,9 @@ module LLM
|
|
|
9
9
|
# @example
|
|
10
10
|
# llm = LLM.openai(key: ENV["KEY"])
|
|
11
11
|
# # Log to a file
|
|
12
|
-
# llm.tracer = LLM::Tracer
|
|
12
|
+
# llm.tracer = LLM::Tracer.logger(llm, path: "/tmp/log.txt")
|
|
13
13
|
# # Log to $stdout (default)
|
|
14
|
-
# llm.tracer = LLM::Tracer
|
|
14
|
+
# llm.tracer = LLM::Tracer.logger(llm, io: $stdout)
|
|
15
15
|
class Tracer::Logger < Tracer
|
|
16
16
|
##
|
|
17
17
|
# @param (see LLM::Tracer#initialize)
|
|
@@ -9,10 +9,10 @@ module LLM
|
|
|
9
9
|
#
|
|
10
10
|
# @example
|
|
11
11
|
# llm = LLM.openai(key: ENV["KEY"])
|
|
12
|
-
# llm.tracer = LLM::Tracer
|
|
12
|
+
# llm.tracer = LLM::Tracer.pretty_logger(llm)
|
|
13
13
|
#
|
|
14
14
|
# @example Writing to a file
|
|
15
|
-
# llm.tracer = LLM::Tracer
|
|
15
|
+
# llm.tracer = LLM::Tracer.pretty_logger(llm, path: "log.txt")
|
|
16
16
|
class Tracer::PrettyLogger < Tracer
|
|
17
17
|
##
|
|
18
18
|
# @param (see LLM::Tracer#initialize)
|
|
@@ -73,8 +73,8 @@ module LLM
|
|
|
73
73
|
|
|
74
74
|
private
|
|
75
75
|
|
|
76
|
-
def setup!(io: $stderr)
|
|
77
|
-
@io = io
|
|
76
|
+
def setup!(io: $stderr, path: nil)
|
|
77
|
+
@io = path ? ::File.open(path, "a") : io
|
|
78
78
|
@start = nil
|
|
79
79
|
end
|
|
80
80
|
|
data/lib/llm/tracer/telemetry.rb
CHANGED
|
@@ -18,7 +18,7 @@ module LLM
|
|
|
18
18
|
# require "pp"
|
|
19
19
|
#
|
|
20
20
|
# llm = LLM.openai(key: ENV["KEY"])
|
|
21
|
-
# llm.tracer = LLM::Tracer
|
|
21
|
+
# llm.tracer = LLM::Tracer.telemetry(llm)
|
|
22
22
|
#
|
|
23
23
|
# ctx = LLM::Context.new(llm)
|
|
24
24
|
# ctx.talk "hello"
|
|
@@ -33,7 +33,7 @@ module LLM
|
|
|
33
33
|
# exporter = OpenTelemetry::Exporter::OTLP::Exporter.new
|
|
34
34
|
#
|
|
35
35
|
# llm = LLM.openai(key: ENV["KEY"])
|
|
36
|
-
# llm.tracer = LLM::Tracer
|
|
36
|
+
# llm.tracer = LLM::Tracer.telemetry(llm, exporter:)
|
|
37
37
|
#
|
|
38
38
|
# ctx = LLM::Context.new(llm)
|
|
39
39
|
# ctx.talk "hello"
|
data/lib/llm/tracer.rb
CHANGED
|
@@ -14,6 +14,39 @@ module LLM
|
|
|
14
14
|
require_relative "tracer/null"
|
|
15
15
|
require_relative "tracer/pretty_logger"
|
|
16
16
|
|
|
17
|
+
##
|
|
18
|
+
# Builds a {LLM::Tracer::PrettyLogger} for a provider.
|
|
19
|
+
# @param [LLM::Provider] llm
|
|
20
|
+
# A provider
|
|
21
|
+
# @param [Hash] options
|
|
22
|
+
# Forwarded to {LLM::Tracer::PrettyLogger} (eg `io:`)
|
|
23
|
+
# @return [LLM::Tracer::PrettyLogger]
|
|
24
|
+
def self.pretty_logger(llm, **options)
|
|
25
|
+
PrettyLogger.new(llm, **options)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
##
|
|
29
|
+
# Builds a {LLM::Tracer::Logger} for a provider.
|
|
30
|
+
# @param [LLM::Provider] llm
|
|
31
|
+
# A provider
|
|
32
|
+
# @param [Hash] options
|
|
33
|
+
# Forwarded to {LLM::Tracer::Logger} (eg `path:` or `io:`)
|
|
34
|
+
# @return [LLM::Tracer::Logger]
|
|
35
|
+
def self.logger(llm, **options)
|
|
36
|
+
Logger.new(llm, **options)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
##
|
|
40
|
+
# Builds a {LLM::Tracer::Telemetry} for a provider.
|
|
41
|
+
# @param [LLM::Provider] llm
|
|
42
|
+
# A provider
|
|
43
|
+
# @param [Hash] options
|
|
44
|
+
# Forwarded to {LLM::Tracer::Telemetry} (eg `exporter:`)
|
|
45
|
+
# @return [LLM::Tracer::Telemetry]
|
|
46
|
+
def self.telemetry(llm, **options)
|
|
47
|
+
Telemetry.new(llm, **options)
|
|
48
|
+
end
|
|
49
|
+
|
|
17
50
|
##
|
|
18
51
|
# @return [LLM::Provider]
|
|
19
52
|
attr_reader :llm
|
data/lib/llm/transport/curb.rb
CHANGED
|
@@ -28,12 +28,14 @@ class LLM::Transport
|
|
|
28
28
|
# @param [String] host
|
|
29
29
|
# @param [Integer] port
|
|
30
30
|
# @param [Integer] timeout
|
|
31
|
+
# @param [Integer] connect_timeout
|
|
31
32
|
# @param [Boolean] ssl
|
|
32
33
|
# @return [LLM::Transport::Curb]
|
|
33
|
-
def initialize(host:, port:, timeout:, ssl:)
|
|
34
|
+
def initialize(host:, port:, timeout:, connect_timeout:, ssl:)
|
|
34
35
|
@host = host
|
|
35
36
|
@port = port
|
|
36
37
|
@timeout = timeout
|
|
38
|
+
@connect_timeout = connect_timeout
|
|
37
39
|
@ssl = ssl
|
|
38
40
|
@base_uri = URI("#{ssl ? "https" : "http"}://#{host}:#{port}/")
|
|
39
41
|
@monitor = Monitor.new
|
|
@@ -112,13 +114,13 @@ class LLM::Transport
|
|
|
112
114
|
|
|
113
115
|
private
|
|
114
116
|
|
|
115
|
-
attr_reader :host, :port, :timeout, :ssl, :base_uri
|
|
117
|
+
attr_reader :host, :port, :timeout, :connect_timeout, :ssl, :base_uri
|
|
116
118
|
|
|
117
119
|
def build_easy(request)
|
|
118
120
|
LLM.require "curb" unless defined?(::Curl)
|
|
119
121
|
easy = ::Curl::Easy.new(request_url(request))
|
|
120
122
|
easy.timeout = timeout
|
|
121
|
-
easy.connect_timeout =
|
|
123
|
+
easy.connect_timeout = connect_timeout
|
|
122
124
|
request.headers.each { |k, v| easy.headers[k] = v }
|
|
123
125
|
easy.follow_location = true
|
|
124
126
|
easy.ssl_verify_peer = false if !ssl
|
data/lib/llm/transport/http.rb
CHANGED
|
@@ -20,12 +20,14 @@ class LLM::Transport
|
|
|
20
20
|
# @param [String] host
|
|
21
21
|
# @param [Integer] port
|
|
22
22
|
# @param [Integer] timeout
|
|
23
|
+
# @param [Integer] connect_timeout
|
|
23
24
|
# @param [Boolean] ssl
|
|
24
25
|
# @return [LLM::Transport::HTTP]
|
|
25
|
-
def initialize(host:, port:, timeout:, ssl:)
|
|
26
|
+
def initialize(host:, port:, timeout:, connect_timeout:, ssl:)
|
|
26
27
|
@host = host
|
|
27
28
|
@port = port
|
|
28
29
|
@timeout = timeout
|
|
30
|
+
@connect_timeout = connect_timeout
|
|
29
31
|
@ssl = ssl
|
|
30
32
|
@base_uri = URI("#{ssl ? "https" : "http"}://#{host}:#{port}/")
|
|
31
33
|
@monitor = Monitor.new
|
|
@@ -93,10 +95,11 @@ class LLM::Transport
|
|
|
93
95
|
|
|
94
96
|
private
|
|
95
97
|
|
|
96
|
-
attr_reader :host, :port, :timeout, :ssl, :base_uri
|
|
98
|
+
attr_reader :host, :port, :timeout, :connect_timeout, :ssl, :base_uri
|
|
97
99
|
|
|
98
100
|
def client
|
|
99
101
|
client = Net::HTTP.new(host, port)
|
|
102
|
+
client.open_timeout = connect_timeout
|
|
100
103
|
client.read_timeout = timeout
|
|
101
104
|
client.use_ssl = ssl
|
|
102
105
|
client
|
|
@@ -32,12 +32,14 @@ class LLM::Transport
|
|
|
32
32
|
# @param [String] host
|
|
33
33
|
# @param [Integer] port
|
|
34
34
|
# @param [Integer] timeout
|
|
35
|
+
# @param [Integer] connect_timeout
|
|
35
36
|
# @param [Boolean] ssl
|
|
36
37
|
# @return [LLM::Transport::PersistentHTTP]
|
|
37
|
-
def initialize(host:, port:, timeout:, ssl:)
|
|
38
|
+
def initialize(host:, port:, timeout:, connect_timeout:, ssl:)
|
|
38
39
|
@host = host
|
|
39
40
|
@port = port
|
|
40
41
|
@timeout = timeout
|
|
42
|
+
@connect_timeout = connect_timeout
|
|
41
43
|
@ssl = ssl
|
|
42
44
|
@base_uri = URI("#{ssl ? "https" : "http"}://#{host}:#{port}/")
|
|
43
45
|
@monitor = Monitor.new
|
|
@@ -100,7 +102,7 @@ class LLM::Transport
|
|
|
100
102
|
|
|
101
103
|
private
|
|
102
104
|
|
|
103
|
-
attr_reader :host, :port, :timeout, :ssl, :base_uri
|
|
105
|
+
attr_reader :host, :port, :timeout, :connect_timeout, :ssl, :base_uri
|
|
104
106
|
|
|
105
107
|
def client
|
|
106
108
|
self.class.lock do
|
|
@@ -110,14 +112,14 @@ class LLM::Transport
|
|
|
110
112
|
LLM.require "net/http/persistent" unless defined?(Net::HTTP::Persistent)
|
|
111
113
|
client = Net::HTTP::Persistent.new(name: self.class.name)
|
|
112
114
|
client.read_timeout = timeout
|
|
113
|
-
client.open_timeout =
|
|
115
|
+
client.open_timeout = connect_timeout
|
|
114
116
|
self.class.registry[client_id] = client
|
|
115
117
|
end
|
|
116
118
|
end
|
|
117
119
|
end
|
|
118
120
|
|
|
119
121
|
def client_id
|
|
120
|
-
"#{host}:#{port}:#{timeout}:#{ssl}"
|
|
122
|
+
"#{host}:#{port}:#{timeout}:#{connect_timeout}:#{ssl}"
|
|
121
123
|
end
|
|
122
124
|
|
|
123
125
|
def close_socket(http)
|
data/lib/llm/transport/utils.rb
CHANGED
|
@@ -24,18 +24,19 @@ class LLM::Transport
|
|
|
24
24
|
# @param [String] host
|
|
25
25
|
# @param [Integer] port
|
|
26
26
|
# @param [Integer, nil] timeout
|
|
27
|
+
# @param [Integer] connect_timeout
|
|
27
28
|
# @param [Boolean] ssl
|
|
28
29
|
# @param [Boolean] persistent
|
|
29
30
|
# @param [LLM::Transport, Class, Symbol, nil] transport
|
|
30
31
|
# @return [LLM::Transport]
|
|
31
|
-
def resolve_transport(host:, port:, timeout:, ssl:, persistent:, transport:)
|
|
32
|
+
def resolve_transport(host:, port:, timeout:, ssl:, persistent:, transport:, connect_timeout: 5)
|
|
32
33
|
if transport.nil?
|
|
33
|
-
default_transport(host:, port:, timeout:, ssl:, persistent:)
|
|
34
|
+
default_transport(host:, port:, timeout:, connect_timeout:, ssl:, persistent:)
|
|
34
35
|
elsif Class === transport && transport <= LLM::Transport
|
|
35
|
-
transport.new(host:, port:, timeout:, ssl:)
|
|
36
|
+
transport.new(host:, port:, timeout:, connect_timeout:, ssl:)
|
|
36
37
|
elsif Symbol === transport
|
|
37
38
|
transport = LLM::Transport.public_send(transport)
|
|
38
|
-
transport.new(host:, port:, timeout:, ssl:)
|
|
39
|
+
transport.new(host:, port:, timeout:, connect_timeout:, ssl:)
|
|
39
40
|
else
|
|
40
41
|
transport
|
|
41
42
|
end
|
|
@@ -49,12 +50,13 @@ class LLM::Transport
|
|
|
49
50
|
# @param [String] host
|
|
50
51
|
# @param [Integer] port
|
|
51
52
|
# @param [Integer, nil] timeout
|
|
53
|
+
# @param [Integer] connect_timeout
|
|
52
54
|
# @param [Boolean] ssl
|
|
53
55
|
# @param [Boolean] persistent
|
|
54
56
|
# @return [LLM::Transport]
|
|
55
|
-
def default_transport(host:, port:, timeout:, ssl:, persistent:)
|
|
57
|
+
def default_transport(host:, port:, timeout:, connect_timeout:, ssl:, persistent:)
|
|
56
58
|
target = persistent ? LLM::Transport::PersistentHTTP : LLM::Transport::HTTP
|
|
57
|
-
target.new(host:, port:, timeout:, ssl:)
|
|
59
|
+
target.new(host:, port:, timeout:, connect_timeout:, ssl:)
|
|
58
60
|
end
|
|
59
61
|
end
|
|
60
62
|
end
|
data/lib/llm/version.rb
CHANGED
data/lib/llm.rb
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
##
|
|
4
|
-
# llm.rb is a zero-dependency AI runtime for Ruby.
|
|
4
|
+
# llm.rb is a zero-dependency AI runtime for Ruby. Fourteen providers, six
|
|
5
5
|
# concurrency strategies, MCP and A2A, streaming tool calls with
|
|
6
6
|
# cancellation, context compaction, and ORM persistence.
|
|
7
7
|
#
|
|
@@ -18,6 +18,7 @@ module LLM
|
|
|
18
18
|
|
|
19
19
|
require "stringio"
|
|
20
20
|
require "securerandom"
|
|
21
|
+
require "time"
|
|
21
22
|
require_relative "llm/compactor"
|
|
22
23
|
require_relative "llm/transformer"
|
|
23
24
|
require_relative "llm/json_adapter"
|
|
@@ -77,7 +78,9 @@ module LLM
|
|
|
77
78
|
# @raise [LLM::LoadError]
|
|
78
79
|
# When the dependency cannot be loaded
|
|
79
80
|
def self.require(name, version = nil)
|
|
80
|
-
names = {"xchan" => "xchan.rb",
|
|
81
|
+
names = {"xchan" => "xchan.rb",
|
|
82
|
+
"net/http/persistent" => "net-http-persistent",
|
|
83
|
+
"unicode/display_width" => "unicode-display_width"}
|
|
81
84
|
gem(names[name] || name, version) if version
|
|
82
85
|
super(name)
|
|
83
86
|
rescue ::LoadError
|
|
@@ -260,6 +263,19 @@ module LLM
|
|
|
260
263
|
end
|
|
261
264
|
end
|
|
262
265
|
|
|
266
|
+
##
|
|
267
|
+
# @param key (see LLM::OpenRouter#initialize)
|
|
268
|
+
# @param host (see LLM::OpenRouter#initialize)
|
|
269
|
+
# @return (see LLM::OpenRouter#initialize)
|
|
270
|
+
def openrouter(key: UNDEFINED, **)
|
|
271
|
+
lock(:require) { require_relative "llm/providers/openrouter" unless defined?(LLM::OpenRouter) }
|
|
272
|
+
if key == UNDEFINED
|
|
273
|
+
LLM::OpenRouter.new(key: key(name: __method__), **)
|
|
274
|
+
else
|
|
275
|
+
LLM::OpenRouter.new(key:, **)
|
|
276
|
+
end
|
|
277
|
+
end
|
|
278
|
+
|
|
263
279
|
##
|
|
264
280
|
# @param key (see LLM::Alibaba#initialize)
|
|
265
281
|
# @param host (see LLM::Alibaba#initialize)
|
|
@@ -346,16 +362,6 @@ module LLM
|
|
|
346
362
|
LLM::Function.new(key, &b)
|
|
347
363
|
end
|
|
348
364
|
|
|
349
|
-
##
|
|
350
|
-
# @param [LLM::Provider] llm
|
|
351
|
-
# A provider
|
|
352
|
-
# @param [Hash] params
|
|
353
|
-
# Forwarded to LLM::Tracer::Logger
|
|
354
|
-
# @return [LLM::Tracer::Logger]
|
|
355
|
-
def logger(llm, **params)
|
|
356
|
-
LLM::Tracer::Logger.new(llm, params)
|
|
357
|
-
end
|
|
358
|
-
|
|
359
365
|
##
|
|
360
366
|
# Provides a thread-safe lock
|
|
361
367
|
# @param [Symbol] name The name of the lock
|