llm.rb 15.4.0 → 15.5.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 +80 -0
- data/README.md +12 -14
- data/data/alibaba.json +2 -2
- data/data/bedrock.json +336 -2
- data/data/deepseek.json +4 -4
- data/data/google.json +3 -3
- data/data/moonshot.json +1 -1
- data/data/openrouter.json +299 -116
- data/docs/deepdive/reference/tracer.md +10 -0
- data/lib/llm/function.rb +6 -0
- data/lib/llm/provider.rb +19 -4
- data/lib/llm/schema/deferred.rb +68 -0
- data/lib/llm/schema/leaf.rb +3 -0
- data/lib/llm/schema.rb +13 -0
- data/lib/llm/tool/param.rb +4 -2
- data/lib/llm/tools/exec.rb +10 -2
- data/lib/llm/tracer/registry.rb +86 -0
- data/lib/llm/tracer.rb +56 -0
- data/lib/llm/version.rb +1 -1
- data/llm.gemspec +0 -6
- metadata +3 -3
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class LLM::Schema
|
|
4
|
+
##
|
|
5
|
+
# {LLM::Schema::Deferred LLM::Schema::Deferred} is a placeholder for a
|
|
6
|
+
# leaf that is resolved when the schema is rendered.
|
|
7
|
+
#
|
|
8
|
+
# A parameter type that can only be known at runtime - an enum of the
|
|
9
|
+
# keys a store holds right now, for example - is given as a proc. The
|
|
10
|
+
# proc is called when the schema is rendered rather than when the class
|
|
11
|
+
# that declares the parameter is defined, so declaring a tool does not
|
|
12
|
+
# touch whatever the type depends on:
|
|
13
|
+
#
|
|
14
|
+
# parameter :fruit, proc { Enum[*Fruit.kinds]}, "A fruit", required: true
|
|
15
|
+
#
|
|
16
|
+
# Whatever the proc returns is resolved in turn, so it can return a
|
|
17
|
+
# leaf, a class, or an array of either.
|
|
18
|
+
#
|
|
19
|
+
# @api private
|
|
20
|
+
class Deferred < Leaf
|
|
21
|
+
##
|
|
22
|
+
# @param [Proc] block
|
|
23
|
+
# A block that returns the leaf to resolve to
|
|
24
|
+
# @return [LLM::Schema::Deferred]
|
|
25
|
+
def initialize(&block)
|
|
26
|
+
super()
|
|
27
|
+
@block = block
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
##
|
|
31
|
+
# @return [Hash]
|
|
32
|
+
def to_h
|
|
33
|
+
resolved_leaf.to_h
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
##
|
|
37
|
+
# @param [Hash] options
|
|
38
|
+
# @return [String]
|
|
39
|
+
def to_json(options = {})
|
|
40
|
+
resolved_leaf.to_h.to_json(options)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
##
|
|
44
|
+
# @return [String]
|
|
45
|
+
def to_s
|
|
46
|
+
resolved_leaf.to_s
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
##
|
|
52
|
+
# Returns the leaf the block resolves to, with the settings that
|
|
53
|
+
# were given to the placeholder applied to it.
|
|
54
|
+
#
|
|
55
|
+
# The settings live on the placeholder, because they are given
|
|
56
|
+
# before the leaf it stands for exists.
|
|
57
|
+
# @return [LLM::Schema::Leaf]
|
|
58
|
+
def resolved_leaf
|
|
59
|
+
leaf = @block.call
|
|
60
|
+
leaf.description(@description) if @description
|
|
61
|
+
leaf.default(@default) unless @default.nil?
|
|
62
|
+
leaf.enum(*@enum) if @enum
|
|
63
|
+
leaf.const(@const) unless @const.nil?
|
|
64
|
+
leaf.required if required?
|
|
65
|
+
leaf
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
data/lib/llm/schema/leaf.rb
CHANGED
data/lib/llm/schema.rb
CHANGED
|
@@ -36,6 +36,7 @@ class LLM::Schema
|
|
|
36
36
|
require_relative "schema/parser"
|
|
37
37
|
require_relative "schema/renderer"
|
|
38
38
|
require_relative "schema/leaf"
|
|
39
|
+
require_relative "schema/deferred"
|
|
39
40
|
require_relative "schema/object"
|
|
40
41
|
require_relative "schema/array"
|
|
41
42
|
require_relative "schema/all_of"
|
|
@@ -56,9 +57,21 @@ class LLM::Schema
|
|
|
56
57
|
module Utils
|
|
57
58
|
extend self
|
|
58
59
|
|
|
60
|
+
##
|
|
61
|
+
# Resolves a parameter type into a leaf of the schema.
|
|
62
|
+
#
|
|
63
|
+
# A proc is called for its type, and whatever it returns is resolved
|
|
64
|
+
# in turn, so a type that can only be known at runtime - an enum of
|
|
65
|
+
# the keys a store holds right now - can be declared without being
|
|
66
|
+
# evaluated when the class that declares it is defined.
|
|
67
|
+
# @param [LLM::Schema] schema
|
|
68
|
+
# @param [Object] type
|
|
69
|
+
# @return [LLM::Schema::Leaf]
|
|
59
70
|
def resolve(schema, type)
|
|
60
71
|
if LLM::Schema::Leaf === type
|
|
61
72
|
type
|
|
73
|
+
elsif ::Proc === type
|
|
74
|
+
Deferred.new { resolve(schema, type.call) }
|
|
62
75
|
elsif ::Array === type
|
|
63
76
|
resolve_array(schema, type)
|
|
64
77
|
elsif Class === type && type.respond_to?(:object)
|
data/lib/llm/tool/param.rb
CHANGED
|
@@ -22,8 +22,10 @@ class LLM::Tool
|
|
|
22
22
|
##
|
|
23
23
|
# @param name [Symbol]
|
|
24
24
|
# The name of a parameter
|
|
25
|
-
# @param type [LLM::Schema::Leaf, Class]
|
|
26
|
-
# The parameter type (eg String)
|
|
25
|
+
# @param type [LLM::Schema::Leaf, Class, Proc]
|
|
26
|
+
# The parameter type (eg String). A proc is called for the type
|
|
27
|
+
# when the parameters are read, so a type that can only be known
|
|
28
|
+
# at runtime does not have to be known when the class is defined.
|
|
27
29
|
# @param description [String]
|
|
28
30
|
# The description of a property
|
|
29
31
|
# @param options [Hash]
|
data/lib/llm/tools/exec.rb
CHANGED
|
@@ -54,7 +54,8 @@ class LLM::Tool
|
|
|
54
54
|
# the max number of bytes to emit
|
|
55
55
|
# @return [Hash]
|
|
56
56
|
def call(arguments: [], timeout: 60, max_bytes: self.class.max_bytes)
|
|
57
|
-
|
|
57
|
+
startat = now
|
|
58
|
+
name = arguments[0]
|
|
58
59
|
command = spawn(name:, arguments: arguments[1..], env:, max_bytes:)
|
|
59
60
|
wait(command:, timeout:)
|
|
60
61
|
if command.not_found?
|
|
@@ -62,7 +63,8 @@ class LLM::Tool
|
|
|
62
63
|
else
|
|
63
64
|
{ok: command.success?,
|
|
64
65
|
stdout: truncate(command.stdout, max_bytes:),
|
|
65
|
-
stderr: truncate(command.stderr, max_bytes:)
|
|
66
|
+
stderr: truncate(command.stderr, max_bytes:),
|
|
67
|
+
duration: "#{(now - startat).round(1)} seconds"}
|
|
66
68
|
end
|
|
67
69
|
rescue LLM::Interrupt
|
|
68
70
|
command.kill! if command&.running?
|
|
@@ -72,5 +74,11 @@ class LLM::Tool
|
|
|
72
74
|
private
|
|
73
75
|
|
|
74
76
|
attr_reader :env
|
|
77
|
+
|
|
78
|
+
##
|
|
79
|
+
# @return [Float]
|
|
80
|
+
def now
|
|
81
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
82
|
+
end
|
|
75
83
|
end
|
|
76
84
|
end
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class LLM::Tracer
|
|
4
|
+
##
|
|
5
|
+
# {LLM::Tracer::Registry LLM::Tracer::Registry} counts the scopes that
|
|
6
|
+
# are open for each tracer.
|
|
7
|
+
#
|
|
8
|
+
# The count belongs to the tracer, not to the thread that opened a
|
|
9
|
+
# scope. A tool runs on a thread of its own and opens a scope there for
|
|
10
|
+
# the turn's tracer, and a count kept per thread would let that scope
|
|
11
|
+
# look like the outermost one: it would end the tracer while the turn
|
|
12
|
+
# is still running, and every write after it would fail.
|
|
13
|
+
#
|
|
14
|
+
# @api private
|
|
15
|
+
class Registry
|
|
16
|
+
##
|
|
17
|
+
# @return [LLM::Tracer::Registry]
|
|
18
|
+
def initialize
|
|
19
|
+
@counts = ObjectSpace::WeakMap.new
|
|
20
|
+
@mutex = Mutex.new
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
##
|
|
24
|
+
# Records that a scope was opened for a tracer.
|
|
25
|
+
# @param [LLM::Tracer] tracer
|
|
26
|
+
# @return [LLM::Tracer::Registry]
|
|
27
|
+
# Returns self, so a caller can tell the scope was recorded
|
|
28
|
+
def enter(tracer)
|
|
29
|
+
@mutex.synchronize do
|
|
30
|
+
@counts[tracer] = count(tracer) + 1
|
|
31
|
+
end
|
|
32
|
+
self
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
##
|
|
36
|
+
# Records that a scope was closed, and tells the tracer it is
|
|
37
|
+
# finished when it was the last one that was open.
|
|
38
|
+
#
|
|
39
|
+
# The count is brought up to date under the lock, and the tracer is
|
|
40
|
+
# told after the lock is released: a tracer that calls back into the
|
|
41
|
+
# provider from `on_exit` would otherwise deadlock against itself.
|
|
42
|
+
#
|
|
43
|
+
# An `exit` without a matching `enter` is ignored: no scope is open,
|
|
44
|
+
# so there is nothing to end, and the tracer is not told it is
|
|
45
|
+
# finished when no scope was holding it.
|
|
46
|
+
# @param [LLM::Tracer] tracer
|
|
47
|
+
# @return [void]
|
|
48
|
+
def exit(tracer)
|
|
49
|
+
last = @mutex.synchronize do
|
|
50
|
+
open = count(tracer)
|
|
51
|
+
if open.zero?
|
|
52
|
+
false
|
|
53
|
+
elsif open == 1
|
|
54
|
+
forget(tracer)
|
|
55
|
+
true
|
|
56
|
+
else
|
|
57
|
+
@counts[tracer] = open - 1
|
|
58
|
+
false
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
tracer.on_exit if last
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
private
|
|
65
|
+
|
|
66
|
+
##
|
|
67
|
+
# Drops a tracer that has no open scopes left.
|
|
68
|
+
# @param [LLM::Tracer] tracer
|
|
69
|
+
# @return [Boolean]
|
|
70
|
+
def forget(tracer)
|
|
71
|
+
if @counts.respond_to?(:delete)
|
|
72
|
+
@counts.delete(tracer)
|
|
73
|
+
else
|
|
74
|
+
@counts[tracer] = 0
|
|
75
|
+
end
|
|
76
|
+
true
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
##
|
|
80
|
+
# @param [LLM::Tracer] tracer
|
|
81
|
+
# @return [Integer]
|
|
82
|
+
def count(tracer)
|
|
83
|
+
@counts[tracer] || 0
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
data/lib/llm/tracer.rb
CHANGED
|
@@ -13,6 +13,39 @@ module LLM
|
|
|
13
13
|
require_relative "tracer/telemetry"
|
|
14
14
|
require_relative "tracer/null"
|
|
15
15
|
require_relative "tracer/pretty_logger"
|
|
16
|
+
require_relative "tracer/registry"
|
|
17
|
+
|
|
18
|
+
##
|
|
19
|
+
# Returns the registry that counts the open scopes for each tracer.
|
|
20
|
+
# @api private
|
|
21
|
+
# @return [LLM::Tracer::Registry]
|
|
22
|
+
def self.registry
|
|
23
|
+
LLM.lock(:registry) do
|
|
24
|
+
@registry ||= Registry.new
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
##
|
|
29
|
+
# Records that a scope was opened for a tracer.
|
|
30
|
+
# @api private
|
|
31
|
+
# @see LLM::Tracer::Registry#enter
|
|
32
|
+
# @param [LLM::Tracer] tracer
|
|
33
|
+
# @return [LLM::Tracer::Registry]
|
|
34
|
+
# Returns the registry, so a caller can tell the scope was recorded
|
|
35
|
+
def self.enter(tracer)
|
|
36
|
+
registry.enter(tracer)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
##
|
|
40
|
+
# Records that a scope was closed for a tracer, and tells the tracer
|
|
41
|
+
# it is finished when it was the last one that was open.
|
|
42
|
+
# @api private
|
|
43
|
+
# @see LLM::Tracer::Registry#exit
|
|
44
|
+
# @param [LLM::Tracer] tracer
|
|
45
|
+
# @return [void]
|
|
46
|
+
def self.exit(tracer)
|
|
47
|
+
registry.exit(tracer)
|
|
48
|
+
end
|
|
16
49
|
|
|
17
50
|
##
|
|
18
51
|
# Builds a {LLM::Tracer::PrettyLogger} for a provider.
|
|
@@ -61,6 +94,29 @@ module LLM
|
|
|
61
94
|
@options = {}
|
|
62
95
|
end
|
|
63
96
|
|
|
97
|
+
##
|
|
98
|
+
# Called when the tracer is no longer in use.
|
|
99
|
+
#
|
|
100
|
+
# A tracer that holds something - a file, a span, a connection pool -
|
|
101
|
+
# releases it here. The default does nothing, because most tracers hold
|
|
102
|
+
# nothing that needs releasing.
|
|
103
|
+
#
|
|
104
|
+
# It is called once, when the last scope that is open for this tracer
|
|
105
|
+
# ends, and that scope can have been opened by another thread: a tool
|
|
106
|
+
# runs on a thread of its own and scopes the turn's tracer while it
|
|
107
|
+
# does. It is called after the scoped lookup is restored, so a tracer
|
|
108
|
+
# that reads {LLM::Provider#tracer} inside it sees the tracer that the
|
|
109
|
+
# next request will see.
|
|
110
|
+
#
|
|
111
|
+
# A tracer can be scoped again afterwards, on a later turn, so it has
|
|
112
|
+
# to stay usable after this call, and it has to tolerate the call
|
|
113
|
+
# happening more than once over its life.
|
|
114
|
+
# @see LLM::Tracer::Registry
|
|
115
|
+
# @see LLM::Provider#with_tracer
|
|
116
|
+
# @return [void]
|
|
117
|
+
def on_exit
|
|
118
|
+
end
|
|
119
|
+
|
|
64
120
|
##
|
|
65
121
|
# Called before an LLM provider request is executed.
|
|
66
122
|
# @param [String] operation
|
data/lib/llm/version.rb
CHANGED
data/llm.gemspec
CHANGED
|
@@ -34,12 +34,6 @@ DESCRIPTION
|
|
|
34
34
|
]
|
|
35
35
|
spec.executables = ["llm.rb"]
|
|
36
36
|
spec.require_paths = ["lib"]
|
|
37
|
-
spec.post_install_message = "\n" \
|
|
38
|
-
"Want to see what I'm building with llm.rb? " \
|
|
39
|
-
"\n" \
|
|
40
|
-
"Checkout the https://r.uby.dev website." \
|
|
41
|
-
"\n\n"
|
|
42
|
-
|
|
43
37
|
spec.add_development_dependency "webmock", "~> 3.24.0"
|
|
44
38
|
spec.add_development_dependency "yard", "~> 0.9.37"
|
|
45
39
|
spec.add_development_dependency "redcarpet", "~> 3.6"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: llm.rb
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 15.
|
|
4
|
+
version: 15.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Robert Gleeson
|
|
@@ -624,6 +624,7 @@ files:
|
|
|
624
624
|
- lib/llm/schema/any_of.rb
|
|
625
625
|
- lib/llm/schema/array.rb
|
|
626
626
|
- lib/llm/schema/boolean.rb
|
|
627
|
+
- lib/llm/schema/deferred.rb
|
|
627
628
|
- lib/llm/schema/enum.rb
|
|
628
629
|
- lib/llm/schema/integer.rb
|
|
629
630
|
- lib/llm/schema/leaf.rb
|
|
@@ -665,6 +666,7 @@ files:
|
|
|
665
666
|
- lib/llm/tracer/logger.rb
|
|
666
667
|
- lib/llm/tracer/null.rb
|
|
667
668
|
- lib/llm/tracer/pretty_logger.rb
|
|
669
|
+
- lib/llm/tracer/registry.rb
|
|
668
670
|
- lib/llm/tracer/telemetry.rb
|
|
669
671
|
- lib/llm/transformer.rb
|
|
670
672
|
- lib/llm/transformer/null.rb
|
|
@@ -695,8 +697,6 @@ metadata:
|
|
|
695
697
|
source_code_uri: https://github.com/r-uby-dev/llm
|
|
696
698
|
documentation_uri: https://r.uby.dev
|
|
697
699
|
changelog_uri: https://github.com/r-uby-dev/llm/blob/main/CHANGELOG.md
|
|
698
|
-
post_install_message: "\nWant to see what I'm building with llm.rb? \nCheckout the
|
|
699
|
-
https://r.uby.dev website.\n\n"
|
|
700
700
|
rdoc_options: []
|
|
701
701
|
require_paths:
|
|
702
702
|
- lib
|