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.
Files changed (79) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +356 -583
  3. data/data/anthropic.json +770 -0
  4. data/data/deepseek.json +75 -0
  5. data/data/google.json +1050 -0
  6. data/data/openai.json +1421 -0
  7. data/data/xai.json +792 -0
  8. data/data/zai.json +330 -0
  9. data/lib/llm/agent.rb +42 -41
  10. data/lib/llm/bot.rb +1 -263
  11. data/lib/llm/buffer.rb +7 -0
  12. data/lib/llm/{session → context}/deserializer.rb +4 -3
  13. data/lib/llm/context.rb +292 -0
  14. data/lib/llm/cost.rb +26 -0
  15. data/lib/llm/error.rb +8 -0
  16. data/lib/llm/function/array.rb +61 -0
  17. data/lib/llm/function/fiber_group.rb +91 -0
  18. data/lib/llm/function/task_group.rb +89 -0
  19. data/lib/llm/function/thread_group.rb +94 -0
  20. data/lib/llm/function.rb +75 -10
  21. data/lib/llm/mcp/command.rb +108 -0
  22. data/lib/llm/mcp/error.rb +31 -0
  23. data/lib/llm/mcp/pipe.rb +82 -0
  24. data/lib/llm/mcp/rpc.rb +118 -0
  25. data/lib/llm/mcp/transport/http/event_handler.rb +66 -0
  26. data/lib/llm/mcp/transport/http.rb +122 -0
  27. data/lib/llm/mcp/transport/stdio.rb +85 -0
  28. data/lib/llm/mcp.rb +116 -0
  29. data/lib/llm/message.rb +13 -11
  30. data/lib/llm/model.rb +2 -2
  31. data/lib/llm/prompt.rb +17 -7
  32. data/lib/llm/provider.rb +32 -17
  33. data/lib/llm/providers/anthropic/files.rb +3 -3
  34. data/lib/llm/providers/anthropic.rb +19 -4
  35. data/lib/llm/providers/deepseek.rb +10 -3
  36. data/lib/llm/providers/{gemini → google}/audio.rb +6 -6
  37. data/lib/llm/providers/{gemini → google}/error_handler.rb +2 -2
  38. data/lib/llm/providers/{gemini → google}/files.rb +11 -11
  39. data/lib/llm/providers/{gemini → google}/images.rb +7 -7
  40. data/lib/llm/providers/{gemini → google}/models.rb +5 -5
  41. data/lib/llm/providers/{gemini → google}/request_adapter/completion.rb +7 -3
  42. data/lib/llm/providers/{gemini → google}/request_adapter.rb +1 -1
  43. data/lib/llm/providers/{gemini → google}/response_adapter/completion.rb +7 -7
  44. data/lib/llm/providers/{gemini → google}/response_adapter/embedding.rb +1 -1
  45. data/lib/llm/providers/{gemini → google}/response_adapter/file.rb +1 -1
  46. data/lib/llm/providers/{gemini → google}/response_adapter/files.rb +1 -1
  47. data/lib/llm/providers/{gemini → google}/response_adapter/image.rb +1 -1
  48. data/lib/llm/providers/{gemini → google}/response_adapter/models.rb +1 -1
  49. data/lib/llm/providers/{gemini → google}/response_adapter/web_search.rb +2 -2
  50. data/lib/llm/providers/{gemini → google}/response_adapter.rb +8 -8
  51. data/lib/llm/providers/{gemini → google}/stream_parser.rb +3 -3
  52. data/lib/llm/providers/{gemini.rb → google.rb} +41 -26
  53. data/lib/llm/providers/llamacpp.rb +10 -3
  54. data/lib/llm/providers/ollama.rb +19 -4
  55. data/lib/llm/providers/openai/files.rb +3 -3
  56. data/lib/llm/providers/openai/response_adapter/completion.rb +9 -1
  57. data/lib/llm/providers/openai/response_adapter/responds.rb +9 -1
  58. data/lib/llm/providers/openai/responses.rb +9 -1
  59. data/lib/llm/providers/openai/stream_parser.rb +2 -0
  60. data/lib/llm/providers/openai.rb +19 -4
  61. data/lib/llm/providers/xai.rb +10 -3
  62. data/lib/llm/providers/zai.rb +9 -2
  63. data/lib/llm/registry.rb +81 -0
  64. data/lib/llm/schema/all_of.rb +31 -0
  65. data/lib/llm/schema/any_of.rb +31 -0
  66. data/lib/llm/schema/one_of.rb +31 -0
  67. data/lib/llm/schema/parser.rb +145 -0
  68. data/lib/llm/schema.rb +49 -8
  69. data/lib/llm/server_tool.rb +5 -5
  70. data/lib/llm/session.rb +10 -1
  71. data/lib/llm/tool.rb +88 -6
  72. data/lib/llm/tracer/logger.rb +1 -1
  73. data/lib/llm/tracer/telemetry.rb +7 -7
  74. data/lib/llm/tracer.rb +3 -3
  75. data/lib/llm/usage.rb +5 -0
  76. data/lib/llm/version.rb +1 -1
  77. data/lib/llm.rb +39 -6
  78. data/llm.gemspec +45 -8
  79. 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
- if LLM::Schema::Leaf === type
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]
@@ -8,10 +8,10 @@
8
8
  #
9
9
  # @example
10
10
  # #!/usr/bin/env ruby
11
- # llm = LLM.gemini ENV["KEY"]
12
- # ses = LLM::Session.new(llm, tools: [LLM::ServerTool.new(:google_search)])
13
- # ses.talk("Summarize today's news", role: :user)
14
- # print ses.messages.find(&:assistant?).content, "\n"
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::Gemini" then {name => options}
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 "bot"
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, :Array,
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(klass)
110
+ def self.inherited(tool)
38
111
  LLM.lock(:inherited) do
39
- klass.instance_eval { @__monitor ||= Monitor.new }
40
- klass.function.register(klass)
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(self)
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
@@ -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:)
@@ -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
- # ses = LLM::Session.new(llm)
24
- # ses.talk "hello"
25
- # ses.talk "how are you?"
26
- # ses.tracer.spans.each { |span| pp span }
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
- # ses = LLM::Session.new(llm)
40
- # ses.talk "hello"
41
- # ses.talk "how are you?"
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 thread-local storage and emits them as langsmith/GenAI attributes.
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 thread-local inputs; default returns {}.
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
- # thread-local outputs; default returns {}.
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module LLM
4
- VERSION = "4.8.0"
4
+ VERSION = "4.10.0"
5
5
  end
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/session"
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::Gemini#initialize)
80
- def gemini(**)
81
- lock(:require) { require_relative "llm/providers/gemini" unless defined?(LLM::Gemini) }
82
- LLM::Gemini.new(**)
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@proton.me"]
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 zero-dependency Ruby toolkit for Large Language Models that
13
- includes OpenAI, Gemini, Anthropic, xAI (grok), zAI, DeepSeek, Ollama, and
14
- LlamaCpp. The toolkit includes full support for chat, streaming, tool calling,
15
- audio, images, files, and structured outputs.
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 = spec.summary
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