llm.rb 4.8.0 → 4.9.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/README.md +337 -590
- data/data/anthropic.json +770 -0
- data/data/deepseek.json +75 -0
- data/data/google.json +1050 -0
- data/data/openai.json +1421 -0
- data/data/xai.json +792 -0
- data/data/zai.json +330 -0
- data/lib/llm/agent.rb +42 -41
- data/lib/llm/bot.rb +1 -263
- data/lib/llm/buffer.rb +7 -0
- data/lib/llm/{session → context}/deserializer.rb +4 -3
- data/lib/llm/context.rb +292 -0
- data/lib/llm/cost.rb +26 -0
- data/lib/llm/error.rb +8 -0
- data/lib/llm/function/array.rb +61 -0
- data/lib/llm/function/fiber_group.rb +91 -0
- data/lib/llm/function/task_group.rb +89 -0
- data/lib/llm/function/thread_group.rb +94 -0
- data/lib/llm/function.rb +75 -10
- data/lib/llm/mcp/command.rb +108 -0
- data/lib/llm/mcp/error.rb +31 -0
- data/lib/llm/mcp/pipe.rb +82 -0
- data/lib/llm/mcp/rpc.rb +118 -0
- data/lib/llm/mcp/transport/stdio.rb +85 -0
- data/lib/llm/mcp.rb +102 -0
- data/lib/llm/message.rb +13 -11
- data/lib/llm/model.rb +2 -2
- data/lib/llm/prompt.rb +17 -7
- data/lib/llm/provider.rb +32 -17
- data/lib/llm/providers/anthropic/files.rb +3 -3
- data/lib/llm/providers/anthropic.rb +19 -4
- data/lib/llm/providers/deepseek.rb +10 -3
- data/lib/llm/providers/{gemini → google}/audio.rb +6 -6
- data/lib/llm/providers/{gemini → google}/error_handler.rb +2 -2
- data/lib/llm/providers/{gemini → google}/files.rb +11 -11
- data/lib/llm/providers/{gemini → google}/images.rb +7 -7
- data/lib/llm/providers/{gemini → google}/models.rb +5 -5
- data/lib/llm/providers/{gemini → google}/request_adapter/completion.rb +7 -3
- data/lib/llm/providers/{gemini → google}/request_adapter.rb +1 -1
- data/lib/llm/providers/{gemini → google}/response_adapter/completion.rb +7 -7
- data/lib/llm/providers/{gemini → google}/response_adapter/embedding.rb +1 -1
- data/lib/llm/providers/{gemini → google}/response_adapter/file.rb +1 -1
- data/lib/llm/providers/{gemini → google}/response_adapter/files.rb +1 -1
- data/lib/llm/providers/{gemini → google}/response_adapter/image.rb +1 -1
- data/lib/llm/providers/{gemini → google}/response_adapter/models.rb +1 -1
- data/lib/llm/providers/{gemini → google}/response_adapter/web_search.rb +2 -2
- data/lib/llm/providers/{gemini → google}/response_adapter.rb +8 -8
- data/lib/llm/providers/{gemini → google}/stream_parser.rb +3 -3
- data/lib/llm/providers/{gemini.rb → google.rb} +41 -26
- data/lib/llm/providers/llamacpp.rb +10 -3
- data/lib/llm/providers/ollama.rb +19 -4
- data/lib/llm/providers/openai/files.rb +3 -3
- data/lib/llm/providers/openai/response_adapter/completion.rb +9 -1
- data/lib/llm/providers/openai/response_adapter/responds.rb +9 -1
- data/lib/llm/providers/openai/responses.rb +9 -1
- data/lib/llm/providers/openai/stream_parser.rb +2 -0
- data/lib/llm/providers/openai.rb +19 -4
- data/lib/llm/providers/xai.rb +10 -3
- data/lib/llm/providers/zai.rb +9 -2
- data/lib/llm/registry.rb +81 -0
- data/lib/llm/schema/parser.rb +109 -0
- data/lib/llm/schema.rb +4 -0
- data/lib/llm/server_tool.rb +5 -5
- data/lib/llm/session.rb +10 -1
- data/lib/llm/tool.rb +85 -4
- data/lib/llm/tracer/logger.rb +1 -1
- data/lib/llm/tracer/telemetry.rb +7 -7
- data/lib/llm/tracer.rb +3 -3
- data/lib/llm/usage.rb +5 -0
- data/lib/llm/version.rb +1 -1
- data/lib/llm.rb +39 -6
- data/llm.gemspec +45 -8
- metadata +81 -28
|
@@ -38,7 +38,15 @@ module LLM::OpenAI::ResponseAdapter
|
|
|
38
38
|
end
|
|
39
39
|
|
|
40
40
|
def adapt_tool(tool)
|
|
41
|
-
{id: tool.call_id, name: tool.name, arguments:
|
|
41
|
+
{id: tool.call_id, name: tool.name, arguments: parse_tool_arguments(tool.arguments)}
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def parse_tool_arguments(arguments)
|
|
45
|
+
return {} if arguments.to_s.empty?
|
|
46
|
+
parsed = LLM.json.load(arguments)
|
|
47
|
+
Hash === parsed ? parsed : {}
|
|
48
|
+
rescue *LLM.json.parser_error
|
|
49
|
+
{}
|
|
42
50
|
end
|
|
43
51
|
end
|
|
44
52
|
end
|
|
@@ -41,7 +41,7 @@ class LLM::OpenAI
|
|
|
41
41
|
role, stream = params.delete(:role), params.delete(:stream)
|
|
42
42
|
params[:stream] = true if stream.respond_to?(:<<) || stream == true
|
|
43
43
|
req = Net::HTTP::Post.new("/v1/responses", headers)
|
|
44
|
-
messages =
|
|
44
|
+
messages = build_complete_messages(prompt, params, role)
|
|
45
45
|
@provider.tracer.set_request_metadata(user_input: extract_user_input(messages, fallback: prompt))
|
|
46
46
|
body = LLM.json.dump({input: [adapt(messages, mode: :response)].flatten}.merge!(params))
|
|
47
47
|
set_body_stream(req, StringIO.new(body))
|
|
@@ -89,6 +89,14 @@ class LLM::OpenAI
|
|
|
89
89
|
define_method(m) { |*args, **kwargs, &b| @provider.send(m, *args, **kwargs, &b) }
|
|
90
90
|
end
|
|
91
91
|
|
|
92
|
+
def build_complete_messages(prompt, params, role)
|
|
93
|
+
if LLM::Prompt === prompt
|
|
94
|
+
[*(params.delete(:input) || []), *prompt]
|
|
95
|
+
else
|
|
96
|
+
[*(params.delete(:input) || []), LLM::Message.new(role, prompt)]
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
92
100
|
def adapt_schema(params)
|
|
93
101
|
return {} unless params && params[:schema]
|
|
94
102
|
schema = params.delete(:schema)
|
|
@@ -43,6 +43,7 @@ class LLM::OpenAI
|
|
|
43
43
|
target_message = @body["choices"][index]["message"]
|
|
44
44
|
delta = choice["delta"] || {}
|
|
45
45
|
delta.each do |key, value|
|
|
46
|
+
next if value.nil?
|
|
46
47
|
if key == "content"
|
|
47
48
|
target_message[key] ||= +""
|
|
48
49
|
target_message[key] << value
|
|
@@ -57,6 +58,7 @@ class LLM::OpenAI
|
|
|
57
58
|
message_hash = {"role" => "assistant"}
|
|
58
59
|
@body["choices"][index] = {"message" => message_hash}
|
|
59
60
|
(choice["delta"] || {}).each do |key, value|
|
|
61
|
+
next if value.nil?
|
|
60
62
|
if key == "content"
|
|
61
63
|
@io << value if @io.respond_to?(:<<)
|
|
62
64
|
message_hash[key] = value
|
data/lib/llm/providers/openai.rb
CHANGED
|
@@ -10,9 +10,9 @@ module LLM
|
|
|
10
10
|
# require "llm"
|
|
11
11
|
#
|
|
12
12
|
# llm = LLM.openai(key: ENV["KEY"])
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
13
|
+
# ctx = LLM::Context.new(llm)
|
|
14
|
+
# ctx.talk ["Tell me about this photo", ctx.local_file("/images/photo.png")]
|
|
15
|
+
# ctx.messages.select(&:assistant?).each { print "[#{_1.role}]", _1.content, "\n" }
|
|
16
16
|
class OpenAI < Provider
|
|
17
17
|
require_relative "openai/error_handler"
|
|
18
18
|
require_relative "openai/request_adapter"
|
|
@@ -36,6 +36,13 @@ module LLM
|
|
|
36
36
|
super(host: HOST, **)
|
|
37
37
|
end
|
|
38
38
|
|
|
39
|
+
##
|
|
40
|
+
# @return [Symbol]
|
|
41
|
+
# Returns the provider's name
|
|
42
|
+
def name
|
|
43
|
+
:openai
|
|
44
|
+
end
|
|
45
|
+
|
|
39
46
|
##
|
|
40
47
|
# Provides an embedding
|
|
41
48
|
# @see https://platform.openai.com/docs/api-reference/embeddings/create OpenAI docs
|
|
@@ -213,13 +220,21 @@ module LLM
|
|
|
213
220
|
end
|
|
214
221
|
|
|
215
222
|
def build_complete_request(prompt, params, role)
|
|
216
|
-
messages =
|
|
223
|
+
messages = build_complete_messages(prompt, params, role)
|
|
217
224
|
body = LLM.json.dump({messages: adapt(messages, mode: :complete).flatten}.merge!(params))
|
|
218
225
|
req = Net::HTTP::Post.new(completions_path, headers)
|
|
219
226
|
set_body_stream(req, StringIO.new(body))
|
|
220
227
|
[req, messages]
|
|
221
228
|
end
|
|
222
229
|
|
|
230
|
+
def build_complete_messages(prompt, params, role)
|
|
231
|
+
if LLM::Prompt === prompt
|
|
232
|
+
[*(params.delete(:messages) || []), *prompt]
|
|
233
|
+
else
|
|
234
|
+
[*(params.delete(:messages) || []), Message.new(role, prompt)]
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
|
|
223
238
|
def extract_user_input(messages, fallback:)
|
|
224
239
|
message = messages.reverse.find(&:user?) || messages.last
|
|
225
240
|
value = message&.content || fallback
|
data/lib/llm/providers/xai.rb
CHANGED
|
@@ -11,9 +11,9 @@ module LLM
|
|
|
11
11
|
# require "llm"
|
|
12
12
|
#
|
|
13
13
|
# llm = LLM.xai(key: ENV["KEY"])
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
14
|
+
# ctx = LLM::Context.new(llm)
|
|
15
|
+
# ctx.talk ["Tell me about this photo", ctx.local_file("/images/photo.png")]
|
|
16
|
+
# ctx.messages.select(&:assistant?).each { print "[#{_1.role}]", _1.content, "\n" }
|
|
17
17
|
class XAI < OpenAI
|
|
18
18
|
require_relative "xai/images"
|
|
19
19
|
|
|
@@ -25,6 +25,13 @@ module LLM
|
|
|
25
25
|
super
|
|
26
26
|
end
|
|
27
27
|
|
|
28
|
+
##
|
|
29
|
+
# @return [Symbol]
|
|
30
|
+
# Returns the provider's name
|
|
31
|
+
def name
|
|
32
|
+
:xai
|
|
33
|
+
end
|
|
34
|
+
|
|
28
35
|
##
|
|
29
36
|
# @raise [NotImplementedError]
|
|
30
37
|
def files
|
data/lib/llm/providers/zai.rb
CHANGED
|
@@ -11,8 +11,8 @@ module LLM
|
|
|
11
11
|
# require "llm"
|
|
12
12
|
#
|
|
13
13
|
# llm = LLM.zai(key: ENV["KEY"])
|
|
14
|
-
#
|
|
15
|
-
#
|
|
14
|
+
# ctx = LLM::Context.new(llm, stream: $stdout)
|
|
15
|
+
# ctx.talk "Hello"
|
|
16
16
|
class ZAI < OpenAI
|
|
17
17
|
##
|
|
18
18
|
# @param [String] host A regional host or the default ("api.z.ai")
|
|
@@ -21,6 +21,13 @@ module LLM
|
|
|
21
21
|
super
|
|
22
22
|
end
|
|
23
23
|
|
|
24
|
+
##
|
|
25
|
+
# @return [Symbol]
|
|
26
|
+
# Returns the provider's name
|
|
27
|
+
def name
|
|
28
|
+
:zai
|
|
29
|
+
end
|
|
30
|
+
|
|
24
31
|
##
|
|
25
32
|
# @raise [NotImplementedError]
|
|
26
33
|
def files
|
data/lib/llm/registry.rb
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
##
|
|
4
|
+
# The {LLM::Registry LLM::Registry} class provides a small API over
|
|
5
|
+
# provider model data. It exposes model metadata such as pricing,
|
|
6
|
+
# capabilities, modalities, and limits from the registry files
|
|
7
|
+
# stored under `data/`. The data is provided by https://models.dev
|
|
8
|
+
# and shipped with llm.rb.
|
|
9
|
+
class LLM::Registry
|
|
10
|
+
@root = File.join(__dir__, "..", "..")
|
|
11
|
+
|
|
12
|
+
##
|
|
13
|
+
# @raise [LLM::Error]
|
|
14
|
+
# Might raise an error
|
|
15
|
+
# @param [Symbol]
|
|
16
|
+
# A provider name
|
|
17
|
+
# @return [LLM::Registry]
|
|
18
|
+
def self.for(name)
|
|
19
|
+
path = File.join @root, "data", "#{name}.json"
|
|
20
|
+
if File.file?(path)
|
|
21
|
+
new LLM.json.load(File.binread(path))
|
|
22
|
+
else
|
|
23
|
+
raise LLM::NoSuchRegistryError, "no registry found for #{name}"
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
##
|
|
28
|
+
# @param [Hash] blob
|
|
29
|
+
# A model registry
|
|
30
|
+
# @return [LLM::Registry]
|
|
31
|
+
def initialize(blob)
|
|
32
|
+
@registry = LLM::Object.from(blob)
|
|
33
|
+
@models = @registry.models
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
##
|
|
37
|
+
# @return [LLM::Object]
|
|
38
|
+
# Returns model costs
|
|
39
|
+
def cost(model:)
|
|
40
|
+
lookup(model:).cost
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
##
|
|
44
|
+
# @return [LLM::Object]
|
|
45
|
+
# Returns model modalities
|
|
46
|
+
def modalities(model:)
|
|
47
|
+
lookup(model:).modalities
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
##
|
|
51
|
+
# @return [LLM::Object]
|
|
52
|
+
# Returns model limits such as the context window size
|
|
53
|
+
def limit(model:)
|
|
54
|
+
lookup(model:).limit
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
def lookup(model:)
|
|
60
|
+
if @models.key?(model)
|
|
61
|
+
@models[model]
|
|
62
|
+
else
|
|
63
|
+
patterns = {/-\d{4}-\d{2}-\d{2}$/ => "", /\A(gpt-.*)-\d{4}$/ => "\\1"}
|
|
64
|
+
fallback = find_map(patterns) { model.dup.sub!(_1, _2) } || "none"
|
|
65
|
+
if @models.key?(fallback)
|
|
66
|
+
@models[fallback]
|
|
67
|
+
else
|
|
68
|
+
raise LLM::NoSuchModelError, "no such model: #{model} (fallback: #{fallback})"
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
##
|
|
74
|
+
# Similar to #{find} but returns the block's return value
|
|
75
|
+
# @return [Object, nil]
|
|
76
|
+
def find_map(pair)
|
|
77
|
+
result = nil
|
|
78
|
+
pair.each_pair { break if result = yield(_1, _2) }
|
|
79
|
+
result
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class LLM::Schema
|
|
4
|
+
##
|
|
5
|
+
# The {LLM::Schema::Parser LLM::Schema::Parser} module provides
|
|
6
|
+
# methods for parsing a JSON schema into {LLM::Schema::Leaf}
|
|
7
|
+
# objects. It is used by {LLM::Schema LLM::Schema} to convert
|
|
8
|
+
# external JSON schema definitions into the schema objects used
|
|
9
|
+
# throughout llm.rb.
|
|
10
|
+
module Parser
|
|
11
|
+
##
|
|
12
|
+
# Parses a JSON schema into an {LLM::Schema::Leaf}.
|
|
13
|
+
# @param [Hash] schema
|
|
14
|
+
# The JSON schema to parse
|
|
15
|
+
# @raise [TypeError]
|
|
16
|
+
# When the schema is not supported
|
|
17
|
+
# @return [LLM::Schema::Leaf]
|
|
18
|
+
def parse(schema, root = nil)
|
|
19
|
+
schema = normalize_schema(schema)
|
|
20
|
+
root ||= schema
|
|
21
|
+
schema = resolve_ref(schema, root)
|
|
22
|
+
case schema["type"]
|
|
23
|
+
when "object" then apply(parse_object(schema, root), schema)
|
|
24
|
+
when "array" then apply(parse_array(schema, root), schema)
|
|
25
|
+
when "string" then apply(parse_string(schema), schema)
|
|
26
|
+
when "integer" then apply(parse_integer(schema), schema)
|
|
27
|
+
when "number" then apply(parse_number(schema), schema)
|
|
28
|
+
when "boolean" then apply(schema().boolean, schema)
|
|
29
|
+
when "null" then apply(schema().null, schema)
|
|
30
|
+
else raise TypeError, "unsupported schema type #{schema["type"].inspect}"
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def parse_object(schema, root)
|
|
37
|
+
properties = (schema["properties"] || {})
|
|
38
|
+
.transform_keys(&:to_s)
|
|
39
|
+
.transform_values { parse(_1, root) }
|
|
40
|
+
required = schema["required"] || []
|
|
41
|
+
required.each do |key|
|
|
42
|
+
next unless properties[key]
|
|
43
|
+
properties[key].required
|
|
44
|
+
end
|
|
45
|
+
schema().object(properties)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def parse_array(schema, root)
|
|
49
|
+
items = schema["items"] ? parse(schema["items"], root) : schema().null
|
|
50
|
+
schema().array(items)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def parse_string(schema)
|
|
54
|
+
leaf = schema().string
|
|
55
|
+
leaf.min(schema["minLength"]) if schema.key?("minLength")
|
|
56
|
+
leaf.max(schema["maxLength"]) if schema.key?("maxLength")
|
|
57
|
+
leaf
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def parse_integer(schema)
|
|
61
|
+
leaf = schema().integer
|
|
62
|
+
leaf.min(schema["minimum"]) if schema.key?("minimum")
|
|
63
|
+
leaf.max(schema["maximum"]) if schema.key?("maximum")
|
|
64
|
+
leaf.multiple_of(schema["multipleOf"]) if schema.key?("multipleOf")
|
|
65
|
+
leaf
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def parse_number(schema)
|
|
69
|
+
leaf = schema().number
|
|
70
|
+
leaf.min(schema["minimum"]) if schema.key?("minimum")
|
|
71
|
+
leaf.max(schema["maximum"]) if schema.key?("maximum")
|
|
72
|
+
leaf.multiple_of(schema["multipleOf"]) if schema.key?("multipleOf")
|
|
73
|
+
leaf
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def apply(leaf, schema)
|
|
77
|
+
leaf.description(schema["description"]) if schema.key?("description")
|
|
78
|
+
leaf.default(schema["default"]) if schema.key?("default")
|
|
79
|
+
leaf.enum(*schema["enum"]) if schema.key?("enum")
|
|
80
|
+
leaf.const(schema["const"]) if schema.key?("const")
|
|
81
|
+
leaf
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def normalize_schema(schema)
|
|
85
|
+
case schema
|
|
86
|
+
when LLM::Object
|
|
87
|
+
normalize_schema(schema.to_h)
|
|
88
|
+
when Hash
|
|
89
|
+
schema.each_with_object({}) do |(key, value), out|
|
|
90
|
+
out[key.to_s] = normalize_schema(value)
|
|
91
|
+
end
|
|
92
|
+
when Array
|
|
93
|
+
schema.map { normalize_schema(_1) }
|
|
94
|
+
else
|
|
95
|
+
schema
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def resolve_ref(schema, root)
|
|
100
|
+
return schema unless schema.key?("$ref")
|
|
101
|
+
ref = schema["$ref"]
|
|
102
|
+
raise TypeError, "unsupported schema ref #{ref.inspect}" unless ref.start_with?("#/")
|
|
103
|
+
target = ref.delete_prefix("#/").split("/").reduce(root) { |node, key| node.fetch(key) }
|
|
104
|
+
normalize_schema(target).merge(schema.except("$ref"))
|
|
105
|
+
rescue KeyError
|
|
106
|
+
raise TypeError, "unresolvable schema ref #{ref.inspect}"
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
data/lib/llm/schema.rb
CHANGED
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
# end
|
|
32
32
|
class LLM::Schema
|
|
33
33
|
require_relative "schema/version"
|
|
34
|
+
require_relative "schema/parser"
|
|
34
35
|
require_relative "schema/leaf"
|
|
35
36
|
require_relative "schema/object"
|
|
36
37
|
require_relative "schema/array"
|
|
@@ -41,6 +42,9 @@ class LLM::Schema
|
|
|
41
42
|
require_relative "schema/boolean"
|
|
42
43
|
require_relative "schema/null"
|
|
43
44
|
|
|
45
|
+
@__monitor = Monitor.new
|
|
46
|
+
extend LLM::Schema::Parser
|
|
47
|
+
|
|
44
48
|
##
|
|
45
49
|
# Configures a monitor for a subclass
|
|
46
50
|
# @return [void]
|
data/lib/llm/server_tool.rb
CHANGED
|
@@ -8,10 +8,10 @@
|
|
|
8
8
|
#
|
|
9
9
|
# @example
|
|
10
10
|
# #!/usr/bin/env ruby
|
|
11
|
-
# llm = LLM.
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
# print
|
|
11
|
+
# llm = LLM.google ENV["KEY"]
|
|
12
|
+
# ctx = LLM::Context.new(llm, tools: [LLM::ServerTool.new(:google_search)])
|
|
13
|
+
# ctx.talk("Summarize today's news", role: :user)
|
|
14
|
+
# print ctx.messages.find(&:assistant?).content, "\n"
|
|
15
15
|
class LLM::ServerTool < Struct.new(:name, :options, :provider)
|
|
16
16
|
##
|
|
17
17
|
# @return [String]
|
|
@@ -24,7 +24,7 @@ class LLM::ServerTool < Struct.new(:name, :options, :provider)
|
|
|
24
24
|
def to_h
|
|
25
25
|
case provider.class.to_s
|
|
26
26
|
when "LLM::Anthropic" then options.merge("name" => name.to_s)
|
|
27
|
-
when "LLM::
|
|
27
|
+
when "LLM::Google" then {name => options}
|
|
28
28
|
else options.merge("type" => name.to_s)
|
|
29
29
|
end
|
|
30
30
|
end
|
data/lib/llm/session.rb
CHANGED
|
@@ -1,3 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative "
|
|
3
|
+
require_relative "context"
|
|
4
|
+
|
|
5
|
+
module LLM
|
|
6
|
+
# Backward-compatible alias for LLM::Context
|
|
7
|
+
# @deprecated Use {LLM::Context} instead. Scheduled for removal in v6.0.
|
|
8
|
+
Session = Context
|
|
9
|
+
|
|
10
|
+
# Scheduled for removal in v6.0
|
|
11
|
+
deprecate_constant :Session
|
|
12
|
+
end
|
data/lib/llm/tool.rb
CHANGED
|
@@ -30,14 +30,87 @@ class LLM::Tool
|
|
|
30
30
|
const_set constant, LLM::Schema.const_get(constant)
|
|
31
31
|
end
|
|
32
32
|
|
|
33
|
+
##
|
|
34
|
+
# @param [LLM::MCP] mcp
|
|
35
|
+
# The MCP client that will execute the tool call
|
|
36
|
+
# @param [Hash] tool
|
|
37
|
+
# A tool (as a raw Hash)
|
|
38
|
+
# @return [Class<LLM::Tool>]
|
|
39
|
+
# Returns a subclass of LLM::Tool
|
|
40
|
+
def self.mcp(mcp, tool)
|
|
41
|
+
klass = Class.new(LLM::Tool) do
|
|
42
|
+
name tool["name"]
|
|
43
|
+
description tool["description"]
|
|
44
|
+
params { tool["inputSchema"] || {type: "object", properties: {}} }
|
|
45
|
+
|
|
46
|
+
define_singleton_method(:inspect) do
|
|
47
|
+
"<LLM::Tool:0x#{object_id.to_s(16)} name=#{tool["name"]} (mcp)>"
|
|
48
|
+
end
|
|
49
|
+
singleton_class.alias_method :to_s, :inspect
|
|
50
|
+
|
|
51
|
+
define_singleton_method(:mcp?) do
|
|
52
|
+
true
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
define_method(:call) do |**args|
|
|
56
|
+
mcp.call_tool(tool["name"], args)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
unregister(klass)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
##
|
|
63
|
+
# Returns all subclasses of LLM::Tool
|
|
64
|
+
# @note
|
|
65
|
+
# This method excludes tools who haven't defined a name
|
|
66
|
+
# as well as tools defined via MCP.
|
|
67
|
+
# @return [Array<LLM::Tool>]
|
|
68
|
+
def self.registry
|
|
69
|
+
lock do
|
|
70
|
+
@registry.select(&:name)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
@registry = []
|
|
74
|
+
|
|
75
|
+
##
|
|
76
|
+
# Clear the registry
|
|
77
|
+
# @return [void]
|
|
78
|
+
def self.clear_registry!
|
|
79
|
+
lock do
|
|
80
|
+
@registry.clear
|
|
81
|
+
nil
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
##
|
|
86
|
+
# Register a tool in the registry
|
|
87
|
+
# @param [LLM::Tool] tool
|
|
88
|
+
# @api private
|
|
89
|
+
def self.register(tool)
|
|
90
|
+
lock do
|
|
91
|
+
@registry << tool
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
##
|
|
96
|
+
# Unregister a tool from the registry
|
|
97
|
+
# @param [LLM::Tool] tool
|
|
98
|
+
# @api private
|
|
99
|
+
def self.unregister(tool)
|
|
100
|
+
lock do
|
|
101
|
+
@registry.delete(tool)
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
33
105
|
##
|
|
34
106
|
# Registers the tool as a function when inherited
|
|
35
107
|
# @param [Class] klass The subclass
|
|
36
108
|
# @return [void]
|
|
37
|
-
def self.inherited(
|
|
109
|
+
def self.inherited(tool)
|
|
38
110
|
LLM.lock(:inherited) do
|
|
39
|
-
|
|
40
|
-
|
|
111
|
+
tool.instance_eval { @__monitor ||= Monitor.new }
|
|
112
|
+
tool.function.register(tool)
|
|
113
|
+
LLM::Tool.register(tool)
|
|
41
114
|
end
|
|
42
115
|
end
|
|
43
116
|
|
|
@@ -75,7 +148,7 @@ class LLM::Tool
|
|
|
75
148
|
# @api private
|
|
76
149
|
def self.function
|
|
77
150
|
lock do
|
|
78
|
-
@function ||= LLM::Function.new(
|
|
151
|
+
@function ||= LLM::Function.new(nil)
|
|
79
152
|
end
|
|
80
153
|
end
|
|
81
154
|
|
|
@@ -84,4 +157,12 @@ class LLM::Tool
|
|
|
84
157
|
def self.lock(&)
|
|
85
158
|
@__monitor.synchronize(&)
|
|
86
159
|
end
|
|
160
|
+
@__monitor = Monitor.new
|
|
161
|
+
|
|
162
|
+
##
|
|
163
|
+
# Returns true if the tool is an MCP tool
|
|
164
|
+
# @return [Boolean]
|
|
165
|
+
def self.mcp?
|
|
166
|
+
false
|
|
167
|
+
end
|
|
87
168
|
end
|
data/lib/llm/tracer/logger.rb
CHANGED
|
@@ -23,7 +23,7 @@ module LLM
|
|
|
23
23
|
##
|
|
24
24
|
# @param (see LLM::Tracer#on_request_start)
|
|
25
25
|
# @return [void]
|
|
26
|
-
def on_request_start(operation:, model: nil)
|
|
26
|
+
def on_request_start(operation:, model: nil, **)
|
|
27
27
|
case operation
|
|
28
28
|
when "chat" then start_chat(operation:, model:)
|
|
29
29
|
when "retrieval" then start_retrieval(operation:)
|
data/lib/llm/tracer/telemetry.rb
CHANGED
|
@@ -20,10 +20,10 @@ module LLM
|
|
|
20
20
|
# llm = LLM.openai(key: ENV["KEY"])
|
|
21
21
|
# llm.tracer = LLM::Tracer::Telemetry.new(llm)
|
|
22
22
|
#
|
|
23
|
-
#
|
|
24
|
-
#
|
|
25
|
-
#
|
|
26
|
-
#
|
|
23
|
+
# ctx = LLM::Context.new(llm)
|
|
24
|
+
# ctx.talk "hello"
|
|
25
|
+
# ctx.talk "how are you?"
|
|
26
|
+
# ctx.tracer.spans.each { |span| pp span }
|
|
27
27
|
#
|
|
28
28
|
# @example OTLP export
|
|
29
29
|
# #!/usr/bin/env ruby
|
|
@@ -36,9 +36,9 @@ module LLM
|
|
|
36
36
|
# llm = LLM.openai(key: ENV["KEY"])
|
|
37
37
|
# llm.tracer = LLM::Tracer::Telemetry.new(llm, exporter:)
|
|
38
38
|
#
|
|
39
|
-
#
|
|
40
|
-
#
|
|
41
|
-
#
|
|
39
|
+
# ctx = LLM::Context.new(llm)
|
|
40
|
+
# ctx.talk "hello"
|
|
41
|
+
# ctx.talk "how are you?"
|
|
42
42
|
class Tracer::Telemetry < Tracer
|
|
43
43
|
##
|
|
44
44
|
# param [LLM::Provider] provider
|
data/lib/llm/tracer.rb
CHANGED
|
@@ -147,7 +147,7 @@ module LLM
|
|
|
147
147
|
# Merges extra attributes for the current trace/span. Used by applications
|
|
148
148
|
# (e.g. chatbot) to add metadata, span inputs, or span outputs to the next
|
|
149
149
|
# span or to the trace. No-op by default; {LLM::Tracer::Langsmith} merges
|
|
150
|
-
# into
|
|
150
|
+
# into fiber-local storage and emits them as langsmith/GenAI attributes.
|
|
151
151
|
#
|
|
152
152
|
# @param [Hash, nil] metadata
|
|
153
153
|
# Key-value pairs merged into trace/span metadata (e.g. langsmith.metadata.*).
|
|
@@ -190,7 +190,7 @@ module LLM
|
|
|
190
190
|
##
|
|
191
191
|
# Returns and clears extra inputs for the next span. Called by the telemetry
|
|
192
192
|
# tracer when starting a span. Subclasses (e.g. Langsmith) override to
|
|
193
|
-
# return
|
|
193
|
+
# return fiber-local inputs; default returns {}.
|
|
194
194
|
#
|
|
195
195
|
# @return [Hash] Attribute key => value to set on the span at start
|
|
196
196
|
def consume_extra_inputs
|
|
@@ -200,7 +200,7 @@ module LLM
|
|
|
200
200
|
##
|
|
201
201
|
# Returns and clears extra outputs for the current span. Called by the
|
|
202
202
|
# telemetry tracer when finishing a span. Subclasses override to return
|
|
203
|
-
#
|
|
203
|
+
# fiber-local outputs; default returns {}.
|
|
204
204
|
#
|
|
205
205
|
# @return [Hash] Attribute key => value to set on the span at finish
|
|
206
206
|
def consume_extra_outputs
|
data/lib/llm/usage.rb
CHANGED
|
@@ -8,4 +8,9 @@
|
|
|
8
8
|
# It can also help track usage of the context window (which may
|
|
9
9
|
# vary by model).
|
|
10
10
|
class LLM::Usage < Struct.new(:input_tokens, :output_tokens, :reasoning_tokens, :total_tokens, keyword_init: true)
|
|
11
|
+
##
|
|
12
|
+
# @return [String]
|
|
13
|
+
def to_json(...)
|
|
14
|
+
LLM.json.dump({input_tokens:, output_tokens:, reasoning_tokens:, total_tokens:})
|
|
15
|
+
end
|
|
11
16
|
end
|
data/lib/llm/version.rb
CHANGED
data/lib/llm.rb
CHANGED
|
@@ -6,6 +6,8 @@ module LLM
|
|
|
6
6
|
require_relative "llm/tracer"
|
|
7
7
|
require_relative "llm/error"
|
|
8
8
|
require_relative "llm/contract"
|
|
9
|
+
require_relative "llm/registry"
|
|
10
|
+
require_relative "llm/cost"
|
|
9
11
|
require_relative "llm/usage"
|
|
10
12
|
require_relative "llm/prompt"
|
|
11
13
|
require_relative "llm/schema"
|
|
@@ -19,7 +21,7 @@ module LLM
|
|
|
19
21
|
require_relative "llm/multipart"
|
|
20
22
|
require_relative "llm/file"
|
|
21
23
|
require_relative "llm/provider"
|
|
22
|
-
require_relative "llm/
|
|
24
|
+
require_relative "llm/context"
|
|
23
25
|
require_relative "llm/agent"
|
|
24
26
|
require_relative "llm/buffer"
|
|
25
27
|
require_relative "llm/function"
|
|
@@ -30,7 +32,22 @@ module LLM
|
|
|
30
32
|
|
|
31
33
|
##
|
|
32
34
|
# Thread-safe monitors for different contexts
|
|
33
|
-
@monitors = {require: Monitor.new, clients: Monitor.new, inherited: Monitor.new}
|
|
35
|
+
@monitors = {require: Monitor.new, clients: Monitor.new, inherited: Monitor.new, registry: Monitor.new}
|
|
36
|
+
|
|
37
|
+
##
|
|
38
|
+
# Model registry
|
|
39
|
+
@registry = {}
|
|
40
|
+
|
|
41
|
+
##
|
|
42
|
+
# @param [Symbol, LLM::Provider] llm
|
|
43
|
+
# The name of a provider, or an instance of LLM::Provider
|
|
44
|
+
# @return [LLM::Object]
|
|
45
|
+
def self.registry_for(llm)
|
|
46
|
+
lock(:registry) do
|
|
47
|
+
name = Symbol === llm ? llm : llm.name
|
|
48
|
+
@registry[name] ||= Registry.for(name)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
34
51
|
|
|
35
52
|
module_function
|
|
36
53
|
|
|
@@ -76,10 +93,10 @@ module LLM
|
|
|
76
93
|
|
|
77
94
|
##
|
|
78
95
|
# @param (see LLM::Provider#initialize)
|
|
79
|
-
# @return (see LLM::
|
|
80
|
-
def
|
|
81
|
-
lock(:require) { require_relative "llm/providers/
|
|
82
|
-
LLM::
|
|
96
|
+
# @return (see LLM::Google#initialize)
|
|
97
|
+
def google(**)
|
|
98
|
+
lock(:require) { require_relative "llm/providers/google" unless defined?(LLM::Google) }
|
|
99
|
+
LLM::Google.new(**)
|
|
83
100
|
end
|
|
84
101
|
|
|
85
102
|
##
|
|
@@ -132,6 +149,22 @@ module LLM
|
|
|
132
149
|
LLM::ZAI.new(**)
|
|
133
150
|
end
|
|
134
151
|
|
|
152
|
+
##
|
|
153
|
+
# @param [LLM::Provider, nil] llm
|
|
154
|
+
# The provider to use for MCP transports that need one
|
|
155
|
+
# @param [Hash, nil] stdio
|
|
156
|
+
# @option stdio [Array<String>] :argv
|
|
157
|
+
# The command to run for the MCP process
|
|
158
|
+
# @option stdio [Hash] :env
|
|
159
|
+
# The environment variables to set for the MCP process
|
|
160
|
+
# @option stdio [String, nil] :cwd
|
|
161
|
+
# The working directory for the MCP process
|
|
162
|
+
# @return [LLM::MCP]
|
|
163
|
+
def mcp(llm = nil, **)
|
|
164
|
+
lock(:require) { require_relative "llm/mcp" unless defined?(LLM::MCP) }
|
|
165
|
+
LLM::MCP.new(llm, **)
|
|
166
|
+
end
|
|
167
|
+
|
|
135
168
|
##
|
|
136
169
|
# Define a function
|
|
137
170
|
# @example
|