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
@@ -2,8 +2,8 @@
2
2
 
3
3
  module LLM
4
4
  ##
5
- # The Gemini class implements a provider for
6
- # [Gemini](https://ai.google.dev/). The Gemini provider
5
+ # The Google class implements a provider for
6
+ # [Gemini](https://ai.google.dev/). The Google provider
7
7
  # can accept multiple inputs (text, images, audio, and video).
8
8
  # The inputs can be provided inline via the prompt for files
9
9
  # under 20MB or via the Gemini Files API for files
@@ -13,19 +13,19 @@ module LLM
13
13
  # #!/usr/bin/env ruby
14
14
  # require "llm"
15
15
  #
16
- # llm = LLM.gemini(key: ENV["KEY"])
17
- # ses = LLM::Session.new(llm)
18
- # ses.talk ["Tell me about this photo", ses.local_file("/images/photo.png")]
19
- # ses.messages.select(&:assistant?).each { print "[#{_1.role}]", _1.content, "\n" }
20
- class Gemini < Provider
21
- require_relative "gemini/error_handler"
22
- require_relative "gemini/request_adapter"
23
- require_relative "gemini/response_adapter"
24
- require_relative "gemini/stream_parser"
25
- require_relative "gemini/models"
26
- require_relative "gemini/images"
27
- require_relative "gemini/audio"
28
- require_relative "gemini/files"
16
+ # llm = LLM.google(key: ENV["KEY"])
17
+ # ctx = LLM::Context.new(llm)
18
+ # ctx.talk ["Tell me about this photo", ctx.local_file("/images/photo.png")]
19
+ # ctx.messages.select(&:assistant?).each { print "[#{_1.role}]", _1.content, "\n" }
20
+ class Google < Provider
21
+ require_relative "google/error_handler"
22
+ require_relative "google/request_adapter"
23
+ require_relative "google/response_adapter"
24
+ require_relative "google/stream_parser"
25
+ require_relative "google/models"
26
+ require_relative "google/images"
27
+ require_relative "google/audio"
28
+ require_relative "google/files"
29
29
 
30
30
  include RequestAdapter
31
31
 
@@ -37,6 +37,13 @@ module LLM
37
37
  super(host: HOST, **)
38
38
  end
39
39
 
40
+ ##
41
+ # @return [Symbol]
42
+ # Returns the provider's name
43
+ def name
44
+ :google
45
+ end
46
+
40
47
  ##
41
48
  # Provides an embedding
42
49
  # @param input (see LLM::Provider#embed)
@@ -78,33 +85,33 @@ module LLM
78
85
  ##
79
86
  # Provides an interface to Gemini's audio API
80
87
  # @see https://ai.google.dev/gemini-api/docs/audio Gemini docs
81
- # @return [LLM::Gemini::Audio]
88
+ # @return [LLM::Google::Audio]
82
89
  def audio
83
- LLM::Gemini::Audio.new(self)
90
+ LLM::Google::Audio.new(self)
84
91
  end
85
92
 
86
93
  ##
87
94
  # Provides an interface to Gemini's image generation API
88
95
  # @see https://ai.google.dev/gemini-api/docs/image-generation Gemini docs
89
- # @return [see LLM::Gemini::Images]
96
+ # @return [see LLM::Google::Images]
90
97
  def images
91
- LLM::Gemini::Images.new(self)
98
+ LLM::Google::Images.new(self)
92
99
  end
93
100
 
94
101
  ##
95
102
  # Provides an interface to Gemini's file management API
96
103
  # @see https://ai.google.dev/gemini-api/docs/files Gemini docs
97
- # @return [LLM::Gemini::Files]
104
+ # @return [LLM::Google::Files]
98
105
  def files
99
- LLM::Gemini::Files.new(self)
106
+ LLM::Google::Files.new(self)
100
107
  end
101
108
 
102
109
  ##
103
110
  # Provides an interface to Gemini's models API
104
111
  # @see https://ai.google.dev/gemini-api/docs/models Gemini docs
105
- # @return [LLM::Gemini::Models]
112
+ # @return [LLM::Google::Models]
106
113
  def models
107
- LLM::Gemini::Models.new(self)
114
+ LLM::Google::Models.new(self)
108
115
  end
109
116
 
110
117
  ##
@@ -177,11 +184,11 @@ module LLM
177
184
  end
178
185
 
179
186
  def stream_parser
180
- LLM::Gemini::StreamParser
187
+ LLM::Google::StreamParser
181
188
  end
182
189
 
183
190
  def error_handler
184
- LLM::Gemini::ErrorHandler
191
+ LLM::Google::ErrorHandler
185
192
  end
186
193
 
187
194
  def normalize_complete_params(params)
@@ -197,10 +204,18 @@ module LLM
197
204
  model.respond_to?(:id) ? model.id : model
198
205
  path = ["/v1beta/models/#{model}", action].join(":")
199
206
  req = Net::HTTP::Post.new(path, headers)
200
- messages = [*(params.delete(:messages) || []), LLM::Message.new(role, prompt)]
207
+ messages = build_complete_messages(prompt, params, role)
201
208
  body = LLM.json.dump({contents: adapt(messages)}.merge!(params))
202
209
  set_body_stream(req, StringIO.new(body))
203
210
  req
204
211
  end
212
+
213
+ def build_complete_messages(prompt, params, role)
214
+ if LLM::Prompt === prompt
215
+ [*(params.delete(:messages) || []), *prompt.to_a]
216
+ else
217
+ [*(params.delete(:messages) || []), LLM::Message.new(role, prompt)]
218
+ end
219
+ end
205
220
  end
206
221
  end
@@ -16,9 +16,9 @@ module LLM
16
16
  # require "llm"
17
17
  #
18
18
  # llm = LLM.llamacpp(key: nil)
19
- # ses = LLM::Session.new(llm)
20
- # ses.talk ["Tell me about this photo", ses.local_file("/images/photo.png")]
21
- # ses.messages.select(&:assistant?).each { print "[#{_1.role}]", _1.content, "\n" }
19
+ # ctx = LLM::Context.new(llm)
20
+ # ctx.talk ["Tell me about this photo", ctx.local_file("/images/photo.png")]
21
+ # ctx.messages.select(&:assistant?).each { print "[#{_1.role}]", _1.content, "\n" }
22
22
  class LlamaCpp < OpenAI
23
23
  ##
24
24
  # @param (see LLM::Provider#initialize)
@@ -27,6 +27,13 @@ module LLM
27
27
  super
28
28
  end
29
29
 
30
+ ##
31
+ # @return [Symbol]
32
+ # Returns the provider's name
33
+ def name
34
+ :llamacpp
35
+ end
36
+
30
37
  ##
31
38
  # @raise [NotImplementedError]
32
39
  def files
@@ -12,9 +12,9 @@ module LLM
12
12
  # require "llm"
13
13
  #
14
14
  # llm = LLM.ollama(key: nil)
15
- # ses = LLM::Session.new(llm, model: "llava")
16
- # ses.talk ["Tell me about this image", ses.local_file("/images/photo.png")]
17
- # ses.messages.select(&:assistant?).each { print "[#{_1.role}]", _1.content, "\n" }
15
+ # ctx = LLM::Context.new(llm, model: "llava")
16
+ # ctx.talk ["Tell me about this image", ctx.local_file("/images/photo.png")]
17
+ # ctx.messages.select(&:assistant?).each { print "[#{_1.role}]", _1.content, "\n" }
18
18
  class Ollama < Provider
19
19
  require_relative "ollama/error_handler"
20
20
  require_relative "ollama/request_adapter"
@@ -32,6 +32,13 @@ module LLM
32
32
  super(host: HOST, port: 11434, ssl: false, **)
33
33
  end
34
34
 
35
+ ##
36
+ # @return [Symbol]
37
+ # Returns the provider's name
38
+ def name
39
+ :ollama
40
+ end
41
+
35
42
  ##
36
43
  # Provides an embedding
37
44
  # @param input (see LLM::Provider#embed)
@@ -120,11 +127,19 @@ module LLM
120
127
  end
121
128
 
122
129
  def build_complete_request(prompt, params, role)
123
- messages = [*(params.delete(:messages) || []), LLM::Message.new(role, prompt)]
130
+ messages = build_complete_messages(prompt, params, role)
124
131
  body = LLM.json.dump({messages: [adapt(messages)].flatten}.merge!(params))
125
132
  req = Net::HTTP::Post.new("/api/chat", headers)
126
133
  set_body_stream(req, StringIO.new(body))
127
134
  req
128
135
  end
136
+
137
+ def build_complete_messages(prompt, params, role)
138
+ if LLM::Prompt === prompt
139
+ [*(params.delete(:messages) || []), *prompt.to_a]
140
+ else
141
+ [*(params.delete(:messages) || []), LLM::Message.new(role, prompt)]
142
+ end
143
+ end
129
144
  end
130
145
  end
@@ -13,10 +13,10 @@ class LLM::OpenAI
13
13
  # require "llm"
14
14
  #
15
15
  # llm = LLM.openai(key: ENV["KEY"])
16
- # ses = LLM::Session.new(llm)
16
+ # ctx = LLM::Context.new(llm)
17
17
  # file = llm.files.create file: "/books/goodread.pdf"
18
- # ses.talk ["Tell me about this PDF", file]
19
- # ses.messages.select(&:assistant?).each { print "[#{_1.role}]", _1.content, "\n" }
18
+ # ctx.talk ["Tell me about this PDF", file]
19
+ # ctx.messages.select(&:assistant?).each { print "[#{_1.role}]", _1.content, "\n" }
20
20
  class Files
21
21
  ##
22
22
  # Returns a new Files object
@@ -74,10 +74,18 @@ module LLM::OpenAI::ResponseAdapter
74
74
  def adapt_tool_calls(tools)
75
75
  (tools || []).filter_map do |tool|
76
76
  next unless tool.function
77
- {id: tool.id, name: tool.function.name, arguments: LLM.json.load(tool.function.arguments)}
77
+ {id: tool.id, name: tool.function.name, arguments: parse_tool_arguments(tool.function.arguments)}
78
78
  end
79
79
  end
80
80
 
81
+ def parse_tool_arguments(arguments)
82
+ return {} if arguments.to_s.empty?
83
+ parsed = LLM.json.load(arguments)
84
+ Hash === parsed ? parsed : {}
85
+ rescue *LLM.json.parser_error
86
+ {}
87
+ end
88
+
81
89
  include LLM::Contract::Completion
82
90
  end
83
91
  end
@@ -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: LLM.json.load(tool.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 = [*(params.delete(:input) || []), LLM::Message.new(role, prompt)]
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
@@ -10,9 +10,9 @@ module LLM
10
10
  # require "llm"
11
11
  #
12
12
  # llm = LLM.openai(key: ENV["KEY"])
13
- # ses = LLM::Session.new(llm)
14
- # ses.talk ["Tell me about this photo", ses.local_file("/images/photo.png")]
15
- # ses.messages.select(&:assistant?).each { print "[#{_1.role}]", _1.content, "\n" }
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 = [*(params.delete(:messages) || []), Message.new(role, prompt)]
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
@@ -11,9 +11,9 @@ module LLM
11
11
  # require "llm"
12
12
  #
13
13
  # llm = LLM.xai(key: ENV["KEY"])
14
- # ses = LLM::Session.new(llm)
15
- # ses.talk ["Tell me about this photo", ses.local_file("/images/photo.png")]
16
- # ses.messages.select(&:assistant?).each { print "[#{_1.role}]", _1.content, "\n" }
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
@@ -11,8 +11,8 @@ module LLM
11
11
  # require "llm"
12
12
  #
13
13
  # llm = LLM.zai(key: ENV["KEY"])
14
- # ses = LLM::Session.new(llm, stream: $stdout)
15
- # ses.talk "Hello"
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
@@ -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,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ class LLM::Schema
4
+ ##
5
+ # The {LLM::Schema::AllOf LLM::Schema::AllOf} class represents an
6
+ # allOf union in a JSON schema. It is a subclass of
7
+ # {LLM::Schema::Leaf LLM::Schema::Leaf}.
8
+ class AllOf < Leaf
9
+ ##
10
+ # Returns an allOf union for the given types.
11
+ # @return [LLM::Schema::AllOf]
12
+ def self.[](*types)
13
+ schema = LLM::Schema.new
14
+ new(types.map { LLM::Schema::Utils.resolve(schema, _1) })
15
+ end
16
+
17
+ ##
18
+ # @param [Array<LLM::Schema::Leaf>] values
19
+ # The values required by the union
20
+ # @return [LLM::Schema::AllOf]
21
+ def initialize(values)
22
+ @values = values
23
+ end
24
+
25
+ ##
26
+ # @return [Hash]
27
+ def to_h
28
+ super.merge!(allOf: @values)
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ class LLM::Schema
4
+ ##
5
+ # The {LLM::Schema::AnyOf LLM::Schema::AnyOf} class represents an
6
+ # anyOf union in a JSON schema. It is a subclass of
7
+ # {LLM::Schema::Leaf LLM::Schema::Leaf}.
8
+ class AnyOf < Leaf
9
+ ##
10
+ # Returns an anyOf union for the given types.
11
+ # @return [LLM::Schema::AnyOf]
12
+ def self.[](*types)
13
+ schema = LLM::Schema.new
14
+ new(types.map { LLM::Schema::Utils.resolve(schema, _1) })
15
+ end
16
+
17
+ ##
18
+ # @param [Array<LLM::Schema::Leaf>] values
19
+ # The values allowed by the union
20
+ # @return [LLM::Schema::AnyOf]
21
+ def initialize(values)
22
+ @values = values
23
+ end
24
+
25
+ ##
26
+ # @return [Hash]
27
+ def to_h
28
+ super.merge!(anyOf: @values)
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ class LLM::Schema
4
+ ##
5
+ # The {LLM::Schema::OneOf LLM::Schema::OneOf} class represents an
6
+ # oneOf union in a JSON schema. It is a subclass of
7
+ # {LLM::Schema::Leaf LLM::Schema::Leaf}.
8
+ class OneOf < Leaf
9
+ ##
10
+ # Returns a oneOf union for the given types.
11
+ # @return [LLM::Schema::OneOf]
12
+ def self.[](*types)
13
+ schema = LLM::Schema.new
14
+ new(types.map { LLM::Schema::Utils.resolve(schema, _1) })
15
+ end
16
+
17
+ ##
18
+ # @param [Array<LLM::Schema::Leaf>] values
19
+ # The values allowed by the union
20
+ # @return [LLM::Schema::OneOf]
21
+ def initialize(values)
22
+ @values = values
23
+ end
24
+
25
+ ##
26
+ # @return [Hash]
27
+ def to_h
28
+ super.merge!(oneOf: @values)
29
+ end
30
+ end
31
+ end