llm.rb 11.3.1 → 12.0.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/CHANGELOG.md +242 -1
- data/LICENSE +92 -17
- data/README.md +204 -623
- data/data/anthropic.json +433 -249
- data/data/bedrock.json +2097 -1055
- data/data/deepinfra.json +993 -0
- data/data/deepseek.json +53 -28
- data/data/google.json +389 -771
- data/data/openai.json +1053 -771
- data/data/xai.json +133 -292
- data/data/zai.json +249 -141
- data/lib/llm/active_record/acts_as_agent.rb +3 -41
- data/lib/llm/active_record/acts_as_llm.rb +18 -0
- data/lib/llm/active_record.rb +3 -3
- data/lib/llm/context.rb +9 -5
- data/lib/llm/contract/completion.rb +2 -2
- data/lib/llm/provider.rb +2 -2
- data/lib/llm/providers/deepinfra/audio.rb +66 -0
- data/lib/llm/providers/deepinfra/images.rb +90 -0
- data/lib/llm/providers/deepinfra/response_adapter.rb +36 -0
- data/lib/llm/providers/deepinfra.rb +100 -0
- data/lib/llm/providers/deepseek/images.rb +109 -0
- data/lib/llm/providers/deepseek/request_adapter.rb +32 -0
- data/lib/llm/providers/deepseek/response_adapter/image.rb +9 -0
- data/lib/llm/providers/deepseek/response_adapter.rb +29 -0
- data/lib/llm/providers/deepseek.rb +4 -2
- data/lib/llm/providers/google/request_adapter.rb +22 -5
- data/lib/llm/providers/google.rb +4 -4
- data/lib/llm/providers/openai/audio.rb +6 -2
- data/lib/llm/providers/openai/images.rb +9 -50
- data/lib/llm/providers/openai/request_adapter/respond.rb +38 -4
- data/lib/llm/providers/openai/response_adapter/audio.rb +5 -1
- data/lib/llm/providers/openai/response_adapter/completion.rb +1 -1
- data/lib/llm/providers/openai/response_adapter/image.rb +0 -4
- data/lib/llm/providers/openai/responses.rb +1 -0
- data/lib/llm/providers/openai/stream_parser.rb +5 -6
- data/lib/llm/providers/openai.rb +2 -2
- data/lib/llm/providers/xai/images.rb +49 -26
- data/lib/llm/providers/xai.rb +2 -2
- data/lib/llm/response.rb +10 -0
- data/lib/llm/schema/leaf.rb +7 -1
- data/lib/llm/schema/renderer.rb +121 -0
- data/lib/llm/schema.rb +30 -0
- data/lib/llm/sequel/agent.rb +2 -43
- data/lib/llm/sequel/plugin.rb +25 -7
- data/lib/llm/tracer/telemetry.rb +4 -6
- data/lib/llm/tracer.rb +9 -21
- data/lib/llm/transport/execution.rb +16 -1
- data/lib/llm/transport/net_http_adapter.rb +1 -1
- data/lib/llm/uridata.rb +16 -0
- data/lib/llm/version.rb +1 -1
- data/lib/llm.rb +9 -0
- data/llm.gemspec +5 -18
- data/resources/deepdive.md +798 -264
- metadata +15 -18
- data/lib/llm/tracer/langsmith.rb +0 -144
|
@@ -204,6 +204,24 @@ module LLM::ActiveRecord
|
|
|
204
204
|
|
|
205
205
|
private
|
|
206
206
|
|
|
207
|
+
##
|
|
208
|
+
# @return [LLM::Provider]
|
|
209
|
+
def set_provider
|
|
210
|
+
raise NotImplementedError, "implement the set_provider callback"
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
##
|
|
214
|
+
# @return [Hash]
|
|
215
|
+
def set_context
|
|
216
|
+
EMPTY_HASH.dup
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
##
|
|
220
|
+
# @return [LLM::Tracer]
|
|
221
|
+
def set_tracer
|
|
222
|
+
nil
|
|
223
|
+
end
|
|
224
|
+
|
|
207
225
|
##
|
|
208
226
|
# @return [LLM::Context]
|
|
209
227
|
def ctx
|
data/lib/llm/active_record.rb
CHANGED
data/lib/llm/context.rb
CHANGED
|
@@ -75,19 +75,22 @@ module LLM
|
|
|
75
75
|
# The parameters to maintain throughout the conversation.
|
|
76
76
|
# Any parameter the provider supports can be included and
|
|
77
77
|
# not only those listed here.
|
|
78
|
-
# @option params [Symbol] :mode
|
|
78
|
+
# @option params [Symbol] :mode
|
|
79
|
+
# Defaults to `:responses` for OpenAI, otherwise it defaults
|
|
80
|
+
# to `:completions`.
|
|
79
81
|
# @option params [String] :model Defaults to the provider's default model
|
|
80
82
|
# @option params [Array<LLM::Function>, nil] :tools Defaults to nil
|
|
81
83
|
# @option params [Array<String>, nil] :skills Defaults to nil
|
|
82
84
|
def initialize(llm, params = {})
|
|
83
85
|
@llm = llm
|
|
84
|
-
@mode = params.delete(:mode) || :completions
|
|
86
|
+
@mode = params.delete(:mode) || (llm.name == :openai ? :responses : :completions)
|
|
85
87
|
@compactor = params.delete(:compactor)
|
|
86
88
|
@guard = params.delete(:guard)
|
|
87
89
|
@transformer = params.delete(:transformer)
|
|
88
90
|
tools = [*params.delete(:tools), *load_skills(params.delete(:skills))]
|
|
89
91
|
@params = {model: llm.default_model, schema: nil}.compact.merge!(params)
|
|
90
92
|
@params[:tools] = tools unless tools.empty?
|
|
93
|
+
@params[:store] ||= false if @mode == :responses
|
|
91
94
|
@messages = LLM::Buffer.new(llm)
|
|
92
95
|
end
|
|
93
96
|
|
|
@@ -199,7 +202,7 @@ module LLM
|
|
|
199
202
|
role = params[:role] || @llm.user_role
|
|
200
203
|
role = @llm.tool_role if params[:role].nil? && [*prompt].grep(LLM::Function::Return).any?
|
|
201
204
|
@messages.concat LLM::Prompt === prompt ? prompt.to_a : [LLM::Message.new(role, prompt)]
|
|
202
|
-
@messages.concat [res.choices[-1]]
|
|
205
|
+
@messages.concat [res.choices[-1]].compact
|
|
203
206
|
res
|
|
204
207
|
end
|
|
205
208
|
|
|
@@ -548,7 +551,8 @@ module LLM
|
|
|
548
551
|
prompt, params = transform(prompt, params)
|
|
549
552
|
bind!(params[:stream], params[:model], params[:tools])
|
|
550
553
|
res_id = params[:store] == false ? nil : @messages.find(&:assistant?)&.response&.response_id
|
|
551
|
-
|
|
554
|
+
input = res_id ? [] : @messages.to_a
|
|
555
|
+
params = params.merge(previous_response_id: res_id, input:).compact
|
|
552
556
|
[prompt, params, @llm.responses.create(prompt, params)]
|
|
553
557
|
end
|
|
554
558
|
|
|
@@ -597,7 +601,7 @@ module LLM
|
|
|
597
601
|
[*message.extra.tool_calls].each do |tool|
|
|
598
602
|
next if returns.any? { _1.id == tool[:id] }
|
|
599
603
|
attrs = {cancelled: true, reason: "function call cancelled"}
|
|
600
|
-
cancelled << LLM::Function::Return.new(tool
|
|
604
|
+
cancelled << LLM::Function::Return.new(tool[:id], tool[:name], attrs)
|
|
601
605
|
end
|
|
602
606
|
messages << LLM::Message.new(@llm.tool_role, cancelled) unless cancelled.empty?
|
|
603
607
|
end
|
|
@@ -98,10 +98,10 @@ module LLM::Contract
|
|
|
98
98
|
end
|
|
99
99
|
|
|
100
100
|
##
|
|
101
|
-
# @return [
|
|
101
|
+
# @return [LLM::Object]
|
|
102
102
|
# Returns the LLM response after parsing it as JSON
|
|
103
103
|
def content!
|
|
104
|
-
LLM.json.load(content)
|
|
104
|
+
LLM::Object.from LLM.json.load(content)
|
|
105
105
|
end
|
|
106
106
|
|
|
107
107
|
##
|
data/lib/llm/provider.rb
CHANGED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class LLM::DeepInfra
|
|
4
|
+
class Audio
|
|
5
|
+
##
|
|
6
|
+
# @param [LLM::Provider] provider
|
|
7
|
+
# A provider
|
|
8
|
+
# @return [LLM::DeepInfra::Audio]
|
|
9
|
+
def initialize(provider)
|
|
10
|
+
@provider = provider
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
##
|
|
14
|
+
# @param [String] input
|
|
15
|
+
# A string of text
|
|
16
|
+
# @param [String] model
|
|
17
|
+
# A text-to-speech model.
|
|
18
|
+
# Defaults to hexgrad/Kokoro-82M.
|
|
19
|
+
# @param [Hash] params
|
|
20
|
+
# Any other model-specific parameters
|
|
21
|
+
# @return [LLM::Response]
|
|
22
|
+
def create_speech(input:, model: "hexgrad/Kokoro-82M", **params)
|
|
23
|
+
path = path("/v1/inference/#{model}", base_path: false)
|
|
24
|
+
req = LLM::Transport::Request.post(path, headers)
|
|
25
|
+
req.body = JSON.dump(params.merge(text: input))
|
|
26
|
+
res, span, tracer = execute(request: req, operation: "request")
|
|
27
|
+
res = ResponseAdapter.adapt LLM::Response.new(res), type: :audio
|
|
28
|
+
tracer.on_request_finish(operation: "request", model:, res:, span:)
|
|
29
|
+
res
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
##
|
|
33
|
+
# @see https://deepinfra.com/models/automatic-speech-recognition speech-to-text models
|
|
34
|
+
# @see https://docs.deepinfra.com/apis/speech API docs
|
|
35
|
+
# @param [String, LLM::File] file
|
|
36
|
+
# An audio file
|
|
37
|
+
# @param [String] model
|
|
38
|
+
# A speech-to-text model.
|
|
39
|
+
# @param [Hash] params
|
|
40
|
+
# Any other model-specific parameters
|
|
41
|
+
# @return [LLM::Response]
|
|
42
|
+
def create_transcription(file:, model: "openai/whisper-large-v3", **params)
|
|
43
|
+
path = path("/v1/inference/#{model}", base_path: false)
|
|
44
|
+
multi = LLM::Multipart.new(params.merge!(audio: LLM.File(file)))
|
|
45
|
+
req = LLM::Transport::Request.post(path, headers)
|
|
46
|
+
req["content-type"] = multi.content_type
|
|
47
|
+
transport.set_body_stream(req, multi.body)
|
|
48
|
+
res, span, tracer = execute(request: req, operation: "request")
|
|
49
|
+
res = LLM::Response.new(res)
|
|
50
|
+
tracer.on_request_finish(operation: "request", model:, res:, span:)
|
|
51
|
+
res
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
##
|
|
55
|
+
# @raise [NotImplementedError]
|
|
56
|
+
def create_translation(...)
|
|
57
|
+
raise NotImplementedError
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
[:path, :headers, :execute, :transport].each do |m|
|
|
63
|
+
define_method(m) { |*args, **kwargs, &b| @provider.send(m, *args, **kwargs, &b) }
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class LLM::DeepInfra
|
|
4
|
+
##
|
|
5
|
+
# The {LLM::DeepInfra::Images LLM::DeepInfra::Images} class provides an
|
|
6
|
+
# interface for [DeepInfra's images API](https://docs.deepinfra.com/apis/image-generation).
|
|
7
|
+
# DeepInfra returns base64-encoded image data.
|
|
8
|
+
#
|
|
9
|
+
# @example
|
|
10
|
+
# #!/usr/bin/env ruby
|
|
11
|
+
# require "llm"
|
|
12
|
+
#
|
|
13
|
+
# llm = LLM.deepinfra(key: ENV["KEY"])
|
|
14
|
+
# res = llm.images.create prompt: "A dog on a rocket to the moon"
|
|
15
|
+
# IO.copy_stream res.images[0], "rocket.png"
|
|
16
|
+
class Images
|
|
17
|
+
##
|
|
18
|
+
# @param [LLM::Provider] provider
|
|
19
|
+
# @return [LLM::DeepInfra::Images]
|
|
20
|
+
def initialize(provider)
|
|
21
|
+
@provider = provider
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
##
|
|
25
|
+
# @see https://deepinfra.com/models/text-to-image DeepInfra Image Models
|
|
26
|
+
# @param [String] prompt
|
|
27
|
+
# A prompt
|
|
28
|
+
# @param [String] model
|
|
29
|
+
# A text-to-image model.
|
|
30
|
+
# Defaults to the black-forest-labs/FLUX-2-klein-4b.
|
|
31
|
+
# @param [String] size
|
|
32
|
+
# Image size (eg 1024x1024)
|
|
33
|
+
# @param [Integer] n
|
|
34
|
+
# The number of images to default
|
|
35
|
+
# @param [String] response_format
|
|
36
|
+
# No other options other than the default are supported.
|
|
37
|
+
# @param [String] quality
|
|
38
|
+
# Exists for compat. Noop.
|
|
39
|
+
# @param [String] style
|
|
40
|
+
# Exists for compat. Noop.
|
|
41
|
+
# @return [LLM::Response<LLM::OpenAI::ResponseAdapter::Image>]
|
|
42
|
+
# Returns a response
|
|
43
|
+
def create(prompt:, model: "black-forest-labs/FLUX-2-klein-4b", size: "1024x1024", n: 1, response_format: "b64_json", quality: nil, style: nil)
|
|
44
|
+
req = LLM::Transport::Request.post(path("/images/generations"), headers)
|
|
45
|
+
params = {prompt:, model:, size:, n:, response_format:, quality:, style:}.compact
|
|
46
|
+
req.body = LLM.json.dump(params)
|
|
47
|
+
res, span, tracer = execute(request: req, operation: "request")
|
|
48
|
+
res = LLM::OpenAI::ResponseAdapter.adapt(res, type: :image)
|
|
49
|
+
tracer.on_request_finish(operation: "request", model:, res:, span:)
|
|
50
|
+
res
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
##
|
|
54
|
+
# @see https://deepinfra.com/models/text-to-image DeepInfra Image Models
|
|
55
|
+
# @param [String, LLM::File, File] image
|
|
56
|
+
# The image to edit.
|
|
57
|
+
# @param [String] prompt
|
|
58
|
+
# A text description of the desired edits.
|
|
59
|
+
# @param [String] model
|
|
60
|
+
# The model to use.
|
|
61
|
+
# @param [String] size
|
|
62
|
+
# Image size (eg 1024x1024)
|
|
63
|
+
# @param [Integer] n
|
|
64
|
+
# The number of images to generate.
|
|
65
|
+
# @param [String] response_format
|
|
66
|
+
# DeepInfra currently supports b64_json.
|
|
67
|
+
# @param [Hash] params
|
|
68
|
+
# Other parameters supported by DeepInfra, such as :mask or :user.
|
|
69
|
+
# @return [LLM::Response<LLM::OpenAI::ResponseAdapter::Image>]
|
|
70
|
+
# Returns a response
|
|
71
|
+
def edit(image:, prompt:, model: "black-forest-labs/FLUX-2-klein-4b", size: "1024x1024", n: 1, response_format: "b64_json", **params)
|
|
72
|
+
params = params.merge!(image: LLM.File(image), prompt:, model:, size:, n:, response_format:)
|
|
73
|
+
params[:mask] = LLM.File(params[:mask]) if params[:mask]
|
|
74
|
+
multi = LLM::Multipart.new(params)
|
|
75
|
+
req = LLM::Transport::Request.post(path("/images/edits"), headers)
|
|
76
|
+
req["content-type"] = multi.content_type
|
|
77
|
+
transport.set_body_stream(req, multi.body)
|
|
78
|
+
res, span, tracer = execute(request: req, operation: "request")
|
|
79
|
+
res = LLM::OpenAI::ResponseAdapter.adapt(res, type: :image)
|
|
80
|
+
tracer.on_request_finish(operation: "request", model:, res:, span:)
|
|
81
|
+
res
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
private
|
|
85
|
+
|
|
86
|
+
[:path, :headers, :execute, :transport].each do |m|
|
|
87
|
+
define_method(m) { |*args, **kwargs, &b| @provider.send(m, *args, **kwargs, &b) }
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class LLM::DeepInfra
|
|
4
|
+
##
|
|
5
|
+
# @private
|
|
6
|
+
module ResponseAdapter
|
|
7
|
+
module Audio
|
|
8
|
+
##
|
|
9
|
+
# @return [LLM::URIData]
|
|
10
|
+
def audio
|
|
11
|
+
@audio ||= LLM::URIData.parse(super)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
module_function
|
|
16
|
+
|
|
17
|
+
##
|
|
18
|
+
# @param [LLM::Response, Net::HTTPResponse] res
|
|
19
|
+
# @param [Symbol] type
|
|
20
|
+
# @return [LLM::Response]
|
|
21
|
+
def adapt(res, type:)
|
|
22
|
+
response = (LLM::Response === res) ? res : LLM::Response.new(res)
|
|
23
|
+
adapter = select(type)
|
|
24
|
+
response.extend(adapter)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
##
|
|
28
|
+
# @api private
|
|
29
|
+
def select(type)
|
|
30
|
+
case type
|
|
31
|
+
when :audio then Audio
|
|
32
|
+
else LLM::OpenAI::ResponseAdapter.select(type)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "openai" unless defined?(LLM::OpenAI)
|
|
4
|
+
|
|
5
|
+
module LLM
|
|
6
|
+
##
|
|
7
|
+
# The DeepInfra class implements a provider for
|
|
8
|
+
# [DeepInfra](https://deepinfra.com)
|
|
9
|
+
# through its OpenAI-compatible API.
|
|
10
|
+
#
|
|
11
|
+
# @example
|
|
12
|
+
# #!/usr/bin/env ruby
|
|
13
|
+
# require "llm"
|
|
14
|
+
#
|
|
15
|
+
# llm = LLM.deepinfra(key: ENV["KEY"])
|
|
16
|
+
# ctx = LLM::Context.new(llm)
|
|
17
|
+
# ctx.talk "Hello"
|
|
18
|
+
# ctx.messages.select(&:assistant?).each { print "[#{_1.role}]", _1.content, "\n" }
|
|
19
|
+
class DeepInfra < OpenAI
|
|
20
|
+
HOST = "api.deepinfra.com"
|
|
21
|
+
BASE_PATH = "/v1/openai"
|
|
22
|
+
require_relative "deepinfra/images"
|
|
23
|
+
require_relative "deepinfra/audio"
|
|
24
|
+
require_relative "deepinfra/response_adapter"
|
|
25
|
+
|
|
26
|
+
##
|
|
27
|
+
# @param key (see LLM::Provider#initialize)
|
|
28
|
+
# @param host (see LLM::Provider#initialize)
|
|
29
|
+
# @param base_path (see LLM::Provider#initialize)
|
|
30
|
+
# @return [LLM::DeepInfra]
|
|
31
|
+
def initialize(host: HOST, base_path: BASE_PATH, **)
|
|
32
|
+
super
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
##
|
|
36
|
+
# @return [Symbol]
|
|
37
|
+
# Returns the provider's name
|
|
38
|
+
def name
|
|
39
|
+
:deepinfra
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
##
|
|
43
|
+
# Provides an interface to DeepInfra's OpenAI-compatible image API.
|
|
44
|
+
# @see https://deepinfra.com/models/text-to-image DeepInfra image models
|
|
45
|
+
# @return [LLM::DeepInfra::Images]
|
|
46
|
+
def images
|
|
47
|
+
LLM::DeepInfra::Images.new(self)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
##
|
|
51
|
+
# Provides an embedding.
|
|
52
|
+
# @see https://deepinfra.com/BAAI/bge-m3 BAAI/bge-m3
|
|
53
|
+
# @param input (see LLM::Provider#embed)
|
|
54
|
+
# @param model (see LLM::Provider#embed)
|
|
55
|
+
# @param params (see LLM::Provider#embed)
|
|
56
|
+
# @raise (see LLM::Provider#request)
|
|
57
|
+
# @return (see LLM::Provider#embed)
|
|
58
|
+
def embed(input, model: "BAAI/bge-m3", **params)
|
|
59
|
+
super
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
##
|
|
63
|
+
# @raise [NotImplementedError]
|
|
64
|
+
def responses
|
|
65
|
+
raise NotImplementedError
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
##
|
|
69
|
+
# @return [LLM::DeepInfra::Audio]
|
|
70
|
+
def audio
|
|
71
|
+
LLM::DeepInfra::Audio.new(self)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
##
|
|
75
|
+
# @raise [NotImplementedError]
|
|
76
|
+
def files
|
|
77
|
+
raise NotImplementedError
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
##
|
|
81
|
+
# @raise [NotImplementedError]
|
|
82
|
+
def moderations
|
|
83
|
+
raise NotImplementedError
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
##
|
|
87
|
+
# @raise [NotImplementedError]
|
|
88
|
+
def vector_stores
|
|
89
|
+
raise NotImplementedError
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
##
|
|
93
|
+
# Returns the default model for chat completions
|
|
94
|
+
# @see https://deepinfra.com/models/zai-org/GLM-5.2 zai-org/GLM-5.2
|
|
95
|
+
# @return [String]
|
|
96
|
+
def default_model
|
|
97
|
+
"zai-org/GLM-5.2"
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class LLM::DeepSeek
|
|
4
|
+
##
|
|
5
|
+
# The {LLM::DeepSeek::Images LLM::DeepSeek::Images} class
|
|
6
|
+
# provides image generation capabilities through DeepSeek.
|
|
7
|
+
#
|
|
8
|
+
# DeepSeek does not provide an image generation model however
|
|
9
|
+
# its text-to-text models can generate vector graphics (SVGS)
|
|
10
|
+
# and that's the approach that this class takes. It is somewhat
|
|
11
|
+
# experimental.
|
|
12
|
+
#
|
|
13
|
+
# An SVG document can be converted to PNG or another format
|
|
14
|
+
# with tools like rsvg-convert.
|
|
15
|
+
class Images
|
|
16
|
+
##
|
|
17
|
+
# @param [LLM::DeepSeek] provider
|
|
18
|
+
# @return [LLM::DeepSeek::Images]
|
|
19
|
+
def initialize(provider)
|
|
20
|
+
@provider = provider
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
##
|
|
24
|
+
# @param [String] prompt
|
|
25
|
+
# A prompt
|
|
26
|
+
# @param [String] model
|
|
27
|
+
# A text-to-image model.
|
|
28
|
+
# @param [void] size
|
|
29
|
+
# This parameter is a noop.
|
|
30
|
+
# Exists for compatibility with other providers.
|
|
31
|
+
# @param [void] n
|
|
32
|
+
# This parameter is a noop.
|
|
33
|
+
# Exists for compatibility with other providers.
|
|
34
|
+
# @param [void] response_format
|
|
35
|
+
# This parameter is a noop.
|
|
36
|
+
# Exists for compatibility with other providers.
|
|
37
|
+
# @param [void] quality
|
|
38
|
+
# This parameter is a noop.
|
|
39
|
+
# Exists for compatibility with other providers.
|
|
40
|
+
# @param [void] style
|
|
41
|
+
# This parameter is a noop.
|
|
42
|
+
# Exists for compatibility with other providers.
|
|
43
|
+
# @return [LLM::Response<LLM::DeepSeek::ResponseAdapter::Image>]
|
|
44
|
+
# Returns a response
|
|
45
|
+
def create(prompt:, model: @provider.default_model, agent: nil, size: nil, n: nil, response_format: nil, quality: nil, style: nil)
|
|
46
|
+
agent ||= LLM::Agent.new(@provider, model:, instructions: create_instructions, response_format: {type: "json_object"})
|
|
47
|
+
res = agent.talk(prompt)
|
|
48
|
+
res = LLM::DeepSeek::ResponseAdapter.adapt(res, type: :image)
|
|
49
|
+
res.define_singleton_method(:agent) { agent }
|
|
50
|
+
res
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
##
|
|
54
|
+
# @param [String] prompt
|
|
55
|
+
# A prompt
|
|
56
|
+
# @param [String] model
|
|
57
|
+
# A text-to-image model.
|
|
58
|
+
# @param [String, LLM::File] image
|
|
59
|
+
# The path to an SVG file
|
|
60
|
+
# @param [void] size
|
|
61
|
+
# This parameter is a noop.
|
|
62
|
+
# Exists for compatibility with other providers.
|
|
63
|
+
# @param [void] n
|
|
64
|
+
# This parameter is a noop.
|
|
65
|
+
# Exists for compatibility with other providers.
|
|
66
|
+
# @param [void] response_format
|
|
67
|
+
# This parameter is a noop.
|
|
68
|
+
# Exists for compatibility with other providers.
|
|
69
|
+
# @param [void] quality
|
|
70
|
+
# This parameter is a noop.
|
|
71
|
+
# Exists for compatibility with other providers.
|
|
72
|
+
# @param [void] style
|
|
73
|
+
# This parameter is a noop.
|
|
74
|
+
# Exists for compatibility with other providers.
|
|
75
|
+
# @return [LLM::Response<LLM::DeepSeek::ResponseAdapter::Image>]
|
|
76
|
+
# Returns a response
|
|
77
|
+
def edit(prompt:, image:, model: @provider.default_model, agent: nil, size: nil, n: nil, response_format: nil, quality: nil, style: nil)
|
|
78
|
+
file = LLM.File(image)
|
|
79
|
+
agent ||= LLM::Agent.new(@provider, model:, instructions: edit_instructions(file), response_format: {type: "json_object"})
|
|
80
|
+
res = agent.talk(prompt)
|
|
81
|
+
res = LLM::DeepSeek::ResponseAdapter.adapt(res, type: :image)
|
|
82
|
+
res.define_singleton_method(:agent) { agent }
|
|
83
|
+
res
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
private
|
|
87
|
+
|
|
88
|
+
def create_instructions
|
|
89
|
+
"Generate a complete SVG document that satisfies the user's prompt. " \
|
|
90
|
+
"Respond with a JSON object that has exactly one key: svg. " \
|
|
91
|
+
"The value of svg must be a valid standalone SVG document as a string. " \
|
|
92
|
+
"Do not include markdown, code fences, commentary, or any keys other than svg."
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def edit_instructions(file)
|
|
96
|
+
file.with_io do |io|
|
|
97
|
+
"Edit the SVG document that is provided according to the user's prompt" \
|
|
98
|
+
"Respond with a JSON object that has exactly one key: svg. " \
|
|
99
|
+
"The value of svg must be a valid standalone SVG document as a string. " \
|
|
100
|
+
"Do not include markdown, code fences, commentary, or any keys other than svg." \
|
|
101
|
+
"The SVG document follows:\n\n#{io.read}" \
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
[:path, :headers, :execute, :transport].each do |m|
|
|
106
|
+
define_method(m) { |*args, **kwargs, &b| @provider.send(m, *args, **kwargs, &b) }
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
@@ -5,6 +5,7 @@ class LLM::DeepSeek
|
|
|
5
5
|
# @private
|
|
6
6
|
module RequestAdapter
|
|
7
7
|
require_relative "request_adapter/completion"
|
|
8
|
+
|
|
8
9
|
##
|
|
9
10
|
# @param [Array<LLM::Message>] messages
|
|
10
11
|
# The messages to adapt
|
|
@@ -17,6 +18,37 @@ class LLM::DeepSeek
|
|
|
17
18
|
|
|
18
19
|
private
|
|
19
20
|
|
|
21
|
+
##
|
|
22
|
+
# Adapt a schema for the DeepSeek chat completions API.
|
|
23
|
+
#
|
|
24
|
+
# DeepSeek does not support OpenAI's `json_schema` response format,
|
|
25
|
+
# so llm.rb falls back to `json_object` and injects a system message
|
|
26
|
+
# that describes the expected shape in prompt-friendly terms.
|
|
27
|
+
#
|
|
28
|
+
# @param [Hash] params
|
|
29
|
+
# The request params
|
|
30
|
+
# @return [Hash]
|
|
31
|
+
def adapt_schema(params)
|
|
32
|
+
return {} unless params && params[:schema]
|
|
33
|
+
schema = params.delete(:schema)
|
|
34
|
+
schema = schema.respond_to?(:object) ? schema.object : schema
|
|
35
|
+
params[:messages] ||= []
|
|
36
|
+
params[:messages] << LLM::Message.new(system_role, adapt_prompt(schema))
|
|
37
|
+
{response_format: {type: "json_object"}}
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
##
|
|
41
|
+
# Build the system prompt that describes the schema.
|
|
42
|
+
# @param [#to_s] schema
|
|
43
|
+
# The schema object
|
|
44
|
+
# @return [String]
|
|
45
|
+
def adapt_prompt(schema)
|
|
46
|
+
"Respond with a single valid JSON object. " \
|
|
47
|
+
"Do not include markdown, code fences, commentary, or any text outside the JSON object. " \
|
|
48
|
+
"The JSON object must match this schema: " \
|
|
49
|
+
"#{schema}"
|
|
50
|
+
end
|
|
51
|
+
|
|
20
52
|
##
|
|
21
53
|
# @param [Array<LLM::Function>] tools
|
|
22
54
|
# @return [Hash]
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class LLM::DeepSeek
|
|
4
|
+
##
|
|
5
|
+
# @private
|
|
6
|
+
module ResponseAdapter
|
|
7
|
+
require_relative "response_adapter/image"
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
##
|
|
11
|
+
# @param [LLM::Response, Net::HTTPResponse] res
|
|
12
|
+
# @param [Symbol] type
|
|
13
|
+
# @return [LLM::Response]
|
|
14
|
+
def adapt(res, type:)
|
|
15
|
+
response = (LLM::Response === res) ? res : LLM::Response.new(res)
|
|
16
|
+
adapter = select(type)
|
|
17
|
+
response.extend(adapter)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
##
|
|
21
|
+
# @api private
|
|
22
|
+
def select(type)
|
|
23
|
+
case type
|
|
24
|
+
when :image then LLM::DeepSeek::ResponseAdapter::Image
|
|
25
|
+
else LLM::OpenAI::ResponseAdapter.select(type)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -19,6 +19,8 @@ module LLM
|
|
|
19
19
|
# ctx.messages.select(&:assistant?).each { print "[#{_1.role}]", _1.content, "\n" }
|
|
20
20
|
class DeepSeek < OpenAI
|
|
21
21
|
require_relative "deepseek/request_adapter"
|
|
22
|
+
require_relative "deepseek/response_adapter"
|
|
23
|
+
require_relative "deepseek/images"
|
|
22
24
|
include DeepSeek::RequestAdapter
|
|
23
25
|
|
|
24
26
|
##
|
|
@@ -42,9 +44,9 @@ module LLM
|
|
|
42
44
|
end
|
|
43
45
|
|
|
44
46
|
##
|
|
45
|
-
# @raise [
|
|
47
|
+
# @raise [LLM::DeepSeek::Images]
|
|
46
48
|
def images
|
|
47
|
-
|
|
49
|
+
LLM::DeepSeek::Images.new(self)
|
|
48
50
|
end
|
|
49
51
|
|
|
50
52
|
##
|
|
@@ -21,11 +21,17 @@ class LLM::Google
|
|
|
21
21
|
##
|
|
22
22
|
# @param [Hash] params
|
|
23
23
|
# @return [Hash]
|
|
24
|
-
def
|
|
25
|
-
return {} unless params
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
24
|
+
def adapt_generation_config(params)
|
|
25
|
+
return {} unless params
|
|
26
|
+
config = {}
|
|
27
|
+
if params[:schema]
|
|
28
|
+
schema = params.delete(:schema)
|
|
29
|
+
schema = schema.respond_to?(:object) ? schema.object : schema
|
|
30
|
+
config.merge!(response_mime_type: "application/json", response_schema: schema)
|
|
31
|
+
end
|
|
32
|
+
params_map.each { config[_1] = params.delete(_2) if params.key?(_2) }
|
|
33
|
+
config.merge!(params)
|
|
34
|
+
config.empty? ? {} : {generationConfig: config}
|
|
29
35
|
end
|
|
30
36
|
|
|
31
37
|
##
|
|
@@ -36,5 +42,16 @@ class LLM::Google
|
|
|
36
42
|
platform, functions = [tools.grep(LLM::ServerTool), tools.grep(LLM::Function)]
|
|
37
43
|
{tools: [*platform, {functionDeclarations: functions.map { _1.adapt(self) }}]}
|
|
38
44
|
end
|
|
45
|
+
|
|
46
|
+
##
|
|
47
|
+
# @return [Hash]
|
|
48
|
+
def params_map
|
|
49
|
+
{
|
|
50
|
+
topP: :top_p,
|
|
51
|
+
topK: :top_k,
|
|
52
|
+
maxOutputTokens: :max_tokens,
|
|
53
|
+
stopSequences: :stop
|
|
54
|
+
}
|
|
55
|
+
end
|
|
39
56
|
end
|
|
40
57
|
end
|