llm.rb 4.8.0 → 4.10.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 +356 -583
- 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/http/event_handler.rb +66 -0
- data/lib/llm/mcp/transport/http.rb +122 -0
- data/lib/llm/mcp/transport/stdio.rb +85 -0
- data/lib/llm/mcp.rb +116 -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/all_of.rb +31 -0
- data/lib/llm/schema/any_of.rb +31 -0
- data/lib/llm/schema/one_of.rb +31 -0
- data/lib/llm/schema/parser.rb +145 -0
- data/lib/llm/schema.rb +49 -8
- data/lib/llm/server_tool.rb +5 -5
- data/lib/llm/session.rb +10 -1
- data/lib/llm/tool.rb +88 -6
- 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 +86 -28
|
@@ -0,0 +1,145 @@
|
|
|
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
|
+
METADATA_KEYS = %w[description default enum const].freeze
|
|
12
|
+
|
|
13
|
+
##
|
|
14
|
+
# Parses a JSON schema into an {LLM::Schema::Leaf}.
|
|
15
|
+
# @param [Hash] schema
|
|
16
|
+
# The JSON schema to parse
|
|
17
|
+
# @raise [TypeError]
|
|
18
|
+
# When the schema is not supported
|
|
19
|
+
# @return [LLM::Schema::Leaf]
|
|
20
|
+
def parse(schema, root = nil)
|
|
21
|
+
schema = normalize_schema(schema)
|
|
22
|
+
root ||= schema
|
|
23
|
+
schema = resolve_ref(schema, root)
|
|
24
|
+
case schema["type"]
|
|
25
|
+
when "object" then apply(parse_object(schema, root), schema)
|
|
26
|
+
when "array" then apply(parse_array(schema, root), schema)
|
|
27
|
+
when "string" then apply(parse_string(schema), schema)
|
|
28
|
+
when "integer" then apply(parse_integer(schema), schema)
|
|
29
|
+
when "number" then apply(parse_number(schema), schema)
|
|
30
|
+
when "boolean" then apply(schema().boolean, schema)
|
|
31
|
+
when "null" then apply(schema().null, schema)
|
|
32
|
+
when ::Array then apply(schema().any_of(*schema["type"].map { parse(schema.except("type", *METADATA_KEYS).merge("type" => _1), root) }), schema.except("type"))
|
|
33
|
+
when nil then parse_union(schema, root)
|
|
34
|
+
else raise TypeError, "unsupported schema type #{schema["type"].inspect}"
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def parse_object(schema, root)
|
|
41
|
+
properties = (schema["properties"] || {})
|
|
42
|
+
.transform_keys(&:to_s)
|
|
43
|
+
.transform_values { parse(_1, root) }
|
|
44
|
+
required = schema["required"] || []
|
|
45
|
+
required.each do |key|
|
|
46
|
+
next unless properties[key]
|
|
47
|
+
properties[key].required
|
|
48
|
+
end
|
|
49
|
+
schema().object(properties)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def parse_array(schema, root)
|
|
53
|
+
items = schema["items"] ? parse(schema["items"], root) : schema().null
|
|
54
|
+
schema().array(items)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def parse_union(schema, root)
|
|
58
|
+
return apply(schema().any_of(*schema["anyOf"].map { parse(_1, root) }), schema) if schema.key?("anyOf")
|
|
59
|
+
return apply(schema().one_of(*schema["oneOf"].map { parse(_1, root) }), schema) if schema.key?("oneOf")
|
|
60
|
+
return apply(schema().all_of(*schema["allOf"].map { parse(_1, root) }), schema) if schema.key?("allOf")
|
|
61
|
+
return parse(infer_type(schema), root) if infer_type(schema)
|
|
62
|
+
raise TypeError, "unsupported schema type #{schema["type"].inspect}"
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def parse_string(schema)
|
|
66
|
+
leaf = schema().string
|
|
67
|
+
leaf.min(schema["minLength"]) if schema.key?("minLength")
|
|
68
|
+
leaf.max(schema["maxLength"]) if schema.key?("maxLength")
|
|
69
|
+
leaf
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def parse_integer(schema)
|
|
73
|
+
leaf = schema().integer
|
|
74
|
+
leaf.min(schema["minimum"]) if schema.key?("minimum")
|
|
75
|
+
leaf.max(schema["maximum"]) if schema.key?("maximum")
|
|
76
|
+
leaf.multiple_of(schema["multipleOf"]) if schema.key?("multipleOf")
|
|
77
|
+
leaf
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def parse_number(schema)
|
|
81
|
+
leaf = schema().number
|
|
82
|
+
leaf.min(schema["minimum"]) if schema.key?("minimum")
|
|
83
|
+
leaf.max(schema["maximum"]) if schema.key?("maximum")
|
|
84
|
+
leaf.multiple_of(schema["multipleOf"]) if schema.key?("multipleOf")
|
|
85
|
+
leaf
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def apply(leaf, schema)
|
|
89
|
+
leaf.description(schema["description"]) if schema.key?("description")
|
|
90
|
+
leaf.default(schema["default"]) if schema.key?("default")
|
|
91
|
+
leaf.enum(*schema["enum"]) if schema.key?("enum")
|
|
92
|
+
leaf.const(schema["const"]) if schema.key?("const")
|
|
93
|
+
leaf
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def normalize_schema(schema)
|
|
97
|
+
case schema
|
|
98
|
+
when LLM::Object
|
|
99
|
+
normalize_schema(schema.to_h)
|
|
100
|
+
when Hash
|
|
101
|
+
schema.each_with_object({}) do |(key, value), out|
|
|
102
|
+
out[key.to_s] = normalize_schema(value)
|
|
103
|
+
end
|
|
104
|
+
when Array
|
|
105
|
+
schema.map { normalize_schema(_1) }
|
|
106
|
+
else
|
|
107
|
+
schema
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def resolve_ref(schema, root)
|
|
112
|
+
return schema unless schema.key?("$ref")
|
|
113
|
+
ref = schema["$ref"]
|
|
114
|
+
raise TypeError, "unsupported schema ref #{ref.inspect}" unless ref.start_with?("#/")
|
|
115
|
+
target = ref.delete_prefix("#/").split("/").reduce(root) { |node, key| node.fetch(key) }
|
|
116
|
+
normalize_schema(target).merge(schema.except("$ref"))
|
|
117
|
+
rescue KeyError
|
|
118
|
+
raise TypeError, "unresolvable schema ref #{ref.inspect}"
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def infer_type(schema)
|
|
122
|
+
if schema.key?("const")
|
|
123
|
+
schema.merge("type" => type_of(schema["const"]))
|
|
124
|
+
elsif schema.key?("enum")
|
|
125
|
+
type = type_of(schema["enum"].first)
|
|
126
|
+
return unless type && schema["enum"].all? { type_of(_1) == type }
|
|
127
|
+
schema.merge("type" => type)
|
|
128
|
+
elsif schema.key?("default")
|
|
129
|
+
schema.merge("type" => type_of(schema["default"]))
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def type_of(value)
|
|
134
|
+
case value
|
|
135
|
+
when ::Hash then "object"
|
|
136
|
+
when ::Array then "array"
|
|
137
|
+
when ::String then "string"
|
|
138
|
+
when ::Integer then "integer"
|
|
139
|
+
when ::Float then "number"
|
|
140
|
+
when ::TrueClass, ::FalseClass then "boolean"
|
|
141
|
+
when ::NilClass then "null"
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
data/lib/llm/schema.rb
CHANGED
|
@@ -31,9 +31,13 @@
|
|
|
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"
|
|
38
|
+
require_relative "schema/all_of"
|
|
39
|
+
require_relative "schema/any_of"
|
|
40
|
+
require_relative "schema/one_of"
|
|
37
41
|
require_relative "schema/string"
|
|
38
42
|
require_relative "schema/enum"
|
|
39
43
|
require_relative "schema/number"
|
|
@@ -41,6 +45,26 @@ class LLM::Schema
|
|
|
41
45
|
require_relative "schema/boolean"
|
|
42
46
|
require_relative "schema/null"
|
|
43
47
|
|
|
48
|
+
@__monitor = Monitor.new
|
|
49
|
+
extend LLM::Schema::Parser
|
|
50
|
+
|
|
51
|
+
##
|
|
52
|
+
# @api private
|
|
53
|
+
module Utils
|
|
54
|
+
extend self
|
|
55
|
+
|
|
56
|
+
def resolve(schema, type)
|
|
57
|
+
if LLM::Schema::Leaf === type
|
|
58
|
+
type
|
|
59
|
+
elsif Class === type && type.respond_to?(:object)
|
|
60
|
+
type.object
|
|
61
|
+
else
|
|
62
|
+
target = type.name.split("::").last.downcase
|
|
63
|
+
schema.public_send(target)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
44
68
|
##
|
|
45
69
|
# Configures a monitor for a subclass
|
|
46
70
|
# @return [void]
|
|
@@ -61,14 +85,7 @@ class LLM::Schema
|
|
|
61
85
|
# A hash of options
|
|
62
86
|
def self.property(name, type, description, options = {})
|
|
63
87
|
lock do
|
|
64
|
-
|
|
65
|
-
prop = type
|
|
66
|
-
elsif Class === type && type.respond_to?(:object)
|
|
67
|
-
prop = type.object
|
|
68
|
-
else
|
|
69
|
-
target = type.name.split("::").last.downcase
|
|
70
|
-
prop = schema.public_send(target)
|
|
71
|
-
end
|
|
88
|
+
prop = Utils.resolve(schema, type)
|
|
72
89
|
options = {description:}.merge(options)
|
|
73
90
|
options.each { (_2 == true) ? prop.public_send(_1) : prop.public_send(_1, *_2) }
|
|
74
91
|
object[name] = prop
|
|
@@ -116,6 +133,30 @@ class LLM::Schema
|
|
|
116
133
|
Array.new(*items)
|
|
117
134
|
end
|
|
118
135
|
|
|
136
|
+
##
|
|
137
|
+
# Returns an anyOf union
|
|
138
|
+
# @param [Array<LLM::Schema::Leaf>] values The union values
|
|
139
|
+
# @return [LLM::Schema::AnyOf]
|
|
140
|
+
def any_of(*values)
|
|
141
|
+
AnyOf.new(values)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
##
|
|
145
|
+
# Returns an allOf union
|
|
146
|
+
# @param [Array<LLM::Schema::Leaf>] values The union values
|
|
147
|
+
# @return [LLM::Schema::AllOf]
|
|
148
|
+
def all_of(*values)
|
|
149
|
+
AllOf.new(values)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
##
|
|
153
|
+
# Returns a oneOf union
|
|
154
|
+
# @param [Array<LLM::Schema::Leaf>] values The union values
|
|
155
|
+
# @return [LLM::Schema::OneOf]
|
|
156
|
+
def one_of(*values)
|
|
157
|
+
OneOf.new(values)
|
|
158
|
+
end
|
|
159
|
+
|
|
119
160
|
##
|
|
120
161
|
# Returns a string
|
|
121
162
|
# @return [LLM::Schema::String]
|
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
|
@@ -22,22 +22,96 @@ class LLM::Tool
|
|
|
22
22
|
extend LLM::Tool::Param
|
|
23
23
|
|
|
24
24
|
types = [
|
|
25
|
-
:Leaf, :String, :Enum,
|
|
25
|
+
:Leaf, :String, :Enum,
|
|
26
|
+
:AllOf, :AnyOf, :OneOf,
|
|
26
27
|
:Object, :Integer, :Number,
|
|
27
|
-
:Boolean, :Null
|
|
28
|
+
:Array, :Boolean, :Null
|
|
28
29
|
]
|
|
29
30
|
types.each do |constant|
|
|
30
31
|
const_set constant, LLM::Schema.const_get(constant)
|
|
31
32
|
end
|
|
32
33
|
|
|
34
|
+
##
|
|
35
|
+
# @param [LLM::MCP] mcp
|
|
36
|
+
# The MCP client that will execute the tool call
|
|
37
|
+
# @param [Hash] tool
|
|
38
|
+
# A tool (as a raw Hash)
|
|
39
|
+
# @return [Class<LLM::Tool>]
|
|
40
|
+
# Returns a subclass of LLM::Tool
|
|
41
|
+
def self.mcp(mcp, tool)
|
|
42
|
+
klass = Class.new(LLM::Tool) do
|
|
43
|
+
name tool["name"]
|
|
44
|
+
description tool["description"]
|
|
45
|
+
params { tool["inputSchema"] || {type: "object", properties: {}} }
|
|
46
|
+
|
|
47
|
+
define_singleton_method(:inspect) do
|
|
48
|
+
"<LLM::Tool:0x#{object_id.to_s(16)} name=#{tool["name"]} (mcp)>"
|
|
49
|
+
end
|
|
50
|
+
singleton_class.alias_method :to_s, :inspect
|
|
51
|
+
|
|
52
|
+
define_singleton_method(:mcp?) do
|
|
53
|
+
true
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
define_method(:call) do |**args|
|
|
57
|
+
mcp.call_tool(tool["name"], args)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
unregister(klass)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
##
|
|
64
|
+
# Returns all subclasses of LLM::Tool
|
|
65
|
+
# @note
|
|
66
|
+
# This method excludes tools who haven't defined a name
|
|
67
|
+
# as well as tools defined via MCP.
|
|
68
|
+
# @return [Array<LLM::Tool>]
|
|
69
|
+
def self.registry
|
|
70
|
+
lock do
|
|
71
|
+
@registry.select(&:name)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
@registry = []
|
|
75
|
+
|
|
76
|
+
##
|
|
77
|
+
# Clear the registry
|
|
78
|
+
# @return [void]
|
|
79
|
+
def self.clear_registry!
|
|
80
|
+
lock do
|
|
81
|
+
@registry.clear
|
|
82
|
+
nil
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
##
|
|
87
|
+
# Register a tool in the registry
|
|
88
|
+
# @param [LLM::Tool] tool
|
|
89
|
+
# @api private
|
|
90
|
+
def self.register(tool)
|
|
91
|
+
lock do
|
|
92
|
+
@registry << tool
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
##
|
|
97
|
+
# Unregister a tool from the registry
|
|
98
|
+
# @param [LLM::Tool] tool
|
|
99
|
+
# @api private
|
|
100
|
+
def self.unregister(tool)
|
|
101
|
+
lock do
|
|
102
|
+
@registry.delete(tool)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
33
106
|
##
|
|
34
107
|
# Registers the tool as a function when inherited
|
|
35
108
|
# @param [Class] klass The subclass
|
|
36
109
|
# @return [void]
|
|
37
|
-
def self.inherited(
|
|
110
|
+
def self.inherited(tool)
|
|
38
111
|
LLM.lock(:inherited) do
|
|
39
|
-
|
|
40
|
-
|
|
112
|
+
tool.instance_eval { @__monitor ||= Monitor.new }
|
|
113
|
+
tool.function.register(tool)
|
|
114
|
+
LLM::Tool.register(tool)
|
|
41
115
|
end
|
|
42
116
|
end
|
|
43
117
|
|
|
@@ -75,7 +149,7 @@ class LLM::Tool
|
|
|
75
149
|
# @api private
|
|
76
150
|
def self.function
|
|
77
151
|
lock do
|
|
78
|
-
@function ||= LLM::Function.new(
|
|
152
|
+
@function ||= LLM::Function.new(nil)
|
|
79
153
|
end
|
|
80
154
|
end
|
|
81
155
|
|
|
@@ -84,4 +158,12 @@ class LLM::Tool
|
|
|
84
158
|
def self.lock(&)
|
|
85
159
|
@__monitor.synchronize(&)
|
|
86
160
|
end
|
|
161
|
+
@__monitor = Monitor.new
|
|
162
|
+
|
|
163
|
+
##
|
|
164
|
+
# Returns true if the tool is an MCP tool
|
|
165
|
+
# @return [Boolean]
|
|
166
|
+
def self.mcp?
|
|
167
|
+
false
|
|
168
|
+
end
|
|
87
169
|
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
|
data/llm.gemspec
CHANGED
|
@@ -5,17 +5,52 @@ require_relative "lib/llm/version"
|
|
|
5
5
|
Gem::Specification.new do |spec|
|
|
6
6
|
spec.name = "llm.rb"
|
|
7
7
|
spec.version = LLM::VERSION
|
|
8
|
-
spec.authors = ["Antar Azri", "0x1eef"]
|
|
9
|
-
spec.email = ["azantar@proton.me", "0x1eef@
|
|
8
|
+
spec.authors = ["Antar Azri", "0x1eef", "Christos Maris", "Rodrigo Serrano"]
|
|
9
|
+
spec.email = ["azantar@proton.me", "0x1eef@hardenedbsd.org"]
|
|
10
10
|
|
|
11
11
|
spec.summary = <<~SUMMARY
|
|
12
|
-
llm.rb is a
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
12
|
+
llm.rb is a Ruby-centric toolkit for building real LLM-powered systems — where
|
|
13
|
+
LLMs are part of your architecture, not just API calls. It gives you explicit
|
|
14
|
+
control over contexts, tools, concurrency, and providers, so you can compose
|
|
15
|
+
reliable, production-ready workflows without hidden abstractions.
|
|
16
16
|
SUMMARY
|
|
17
17
|
|
|
18
|
-
spec.description =
|
|
18
|
+
spec.description = <<~DESCRIPTION
|
|
19
|
+
llm.rb is a Ruby-centric toolkit for building real LLM-powered systems — where
|
|
20
|
+
LLMs are part of your architecture, not just API calls. It gives you explicit
|
|
21
|
+
control over contexts, tools, concurrency, and providers, so you can compose
|
|
22
|
+
reliable, production-ready workflows without hidden abstractions.
|
|
23
|
+
|
|
24
|
+
Built for engineers who want to understand and control their LLM systems. No
|
|
25
|
+
frameworks, no hidden magic — just composable primitives for building real
|
|
26
|
+
applications, from scripts to full systems like Relay.
|
|
27
|
+
|
|
28
|
+
## Key Features
|
|
29
|
+
|
|
30
|
+
- **Contexts are central** — Hold history, tools, schema, usage, cost, persistence, and execution state
|
|
31
|
+
- **Tool execution is explicit** — Run local, provider-native, and MCP tools sequentially or concurrently
|
|
32
|
+
- **One API across providers** — Unified interface for OpenAI, Anthropic, Google, xAI, zAI, DeepSeek, Ollama, and LlamaCpp
|
|
33
|
+
- **Thread-safe where it matters** — Providers are shareable, while contexts stay isolated and stateful
|
|
34
|
+
- **Production-ready** — Cost tracking, observability, persistence, and performance tuning built in
|
|
35
|
+
- **Stdlib-only by default** — Runs on Ruby standard library, with optional features loaded only when used
|
|
36
|
+
|
|
37
|
+
## Capabilities
|
|
38
|
+
|
|
39
|
+
- Chat & Contexts with persistence
|
|
40
|
+
- Streaming responses
|
|
41
|
+
- Tool calling with JSON Schema validation
|
|
42
|
+
- Concurrent execution (threads, fibers, async tasks)
|
|
43
|
+
- Agents with auto-execution
|
|
44
|
+
- Structured outputs
|
|
45
|
+
- MCP (Model Context Protocol) support
|
|
46
|
+
- Multimodal inputs (text, images, audio, documents)
|
|
47
|
+
- Audio generation, transcription, translation
|
|
48
|
+
- Image generation and editing
|
|
49
|
+
- Files API for document processing
|
|
50
|
+
- Embeddings and vector stores
|
|
51
|
+
- Local model registry for capabilities, limits, and pricing
|
|
52
|
+
DESCRIPTION
|
|
53
|
+
|
|
19
54
|
spec.license = "0BSD"
|
|
20
55
|
spec.required_ruby_version = ">= 3.2.0"
|
|
21
56
|
|
|
@@ -23,10 +58,12 @@ Gem::Specification.new do |spec|
|
|
|
23
58
|
spec.metadata["homepage_uri"] = "https://github.com/llmrb/llm.rb"
|
|
24
59
|
spec.metadata["source_code_uri"] = "https://github.com/llmrb/llm.rb"
|
|
25
60
|
spec.metadata["documentation_uri"] = "https://0x1eef.github.io/x/llm.rb"
|
|
61
|
+
spec.metadata["changelog_uri"] = "https://0x1eef.github.io/x/llm.rb/file.CHANGELOG.html"
|
|
26
62
|
|
|
27
63
|
spec.files = Dir[
|
|
28
64
|
"README.md", "LICENSE",
|
|
29
65
|
"lib/*.rb", "lib/**/*.rb",
|
|
66
|
+
"data/*.json",
|
|
30
67
|
"llm.gemspec"
|
|
31
68
|
]
|
|
32
69
|
spec.require_paths = ["lib"]
|
|
@@ -44,4 +81,4 @@ Gem::Specification.new do |spec|
|
|
|
44
81
|
spec.add_development_dependency "net-http-persistent", "~> 4.0"
|
|
45
82
|
spec.add_development_dependency "opentelemetry-sdk", "~> 1.10"
|
|
46
83
|
spec.add_development_dependency "logger", "~> 1.7"
|
|
47
|
-
end
|
|
84
|
+
end
|