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
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
module LLM
|
|
4
4
|
##
|
|
5
|
-
# The
|
|
6
|
-
# [Gemini](https://ai.google.dev/). The
|
|
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.
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
class
|
|
21
|
-
require_relative "
|
|
22
|
-
require_relative "
|
|
23
|
-
require_relative "
|
|
24
|
-
require_relative "
|
|
25
|
-
require_relative "
|
|
26
|
-
require_relative "
|
|
27
|
-
require_relative "
|
|
28
|
-
require_relative "
|
|
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::
|
|
88
|
+
# @return [LLM::Google::Audio]
|
|
82
89
|
def audio
|
|
83
|
-
LLM::
|
|
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::
|
|
96
|
+
# @return [see LLM::Google::Images]
|
|
90
97
|
def images
|
|
91
|
-
LLM::
|
|
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::
|
|
104
|
+
# @return [LLM::Google::Files]
|
|
98
105
|
def files
|
|
99
|
-
LLM::
|
|
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::
|
|
112
|
+
# @return [LLM::Google::Models]
|
|
106
113
|
def models
|
|
107
|
-
LLM::
|
|
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::
|
|
187
|
+
LLM::Google::StreamParser
|
|
181
188
|
end
|
|
182
189
|
|
|
183
190
|
def error_handler
|
|
184
|
-
LLM::
|
|
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 =
|
|
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
|
-
#
|
|
20
|
-
#
|
|
21
|
-
#
|
|
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
|
data/lib/llm/providers/ollama.rb
CHANGED
|
@@ -12,9 +12,9 @@ module LLM
|
|
|
12
12
|
# require "llm"
|
|
13
13
|
#
|
|
14
14
|
# llm = LLM.ollama(key: nil)
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
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 =
|
|
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
|
-
#
|
|
16
|
+
# ctx = LLM::Context.new(llm)
|
|
17
17
|
# file = llm.files.create file: "/books/goodread.pdf"
|
|
18
|
-
#
|
|
19
|
-
#
|
|
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:
|
|
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:
|
|
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,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
|