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.
Files changed (57) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +242 -1
  3. data/LICENSE +92 -17
  4. data/README.md +204 -623
  5. data/data/anthropic.json +433 -249
  6. data/data/bedrock.json +2097 -1055
  7. data/data/deepinfra.json +993 -0
  8. data/data/deepseek.json +53 -28
  9. data/data/google.json +389 -771
  10. data/data/openai.json +1053 -771
  11. data/data/xai.json +133 -292
  12. data/data/zai.json +249 -141
  13. data/lib/llm/active_record/acts_as_agent.rb +3 -41
  14. data/lib/llm/active_record/acts_as_llm.rb +18 -0
  15. data/lib/llm/active_record.rb +3 -3
  16. data/lib/llm/context.rb +9 -5
  17. data/lib/llm/contract/completion.rb +2 -2
  18. data/lib/llm/provider.rb +2 -2
  19. data/lib/llm/providers/deepinfra/audio.rb +66 -0
  20. data/lib/llm/providers/deepinfra/images.rb +90 -0
  21. data/lib/llm/providers/deepinfra/response_adapter.rb +36 -0
  22. data/lib/llm/providers/deepinfra.rb +100 -0
  23. data/lib/llm/providers/deepseek/images.rb +109 -0
  24. data/lib/llm/providers/deepseek/request_adapter.rb +32 -0
  25. data/lib/llm/providers/deepseek/response_adapter/image.rb +9 -0
  26. data/lib/llm/providers/deepseek/response_adapter.rb +29 -0
  27. data/lib/llm/providers/deepseek.rb +4 -2
  28. data/lib/llm/providers/google/request_adapter.rb +22 -5
  29. data/lib/llm/providers/google.rb +4 -4
  30. data/lib/llm/providers/openai/audio.rb +6 -2
  31. data/lib/llm/providers/openai/images.rb +9 -50
  32. data/lib/llm/providers/openai/request_adapter/respond.rb +38 -4
  33. data/lib/llm/providers/openai/response_adapter/audio.rb +5 -1
  34. data/lib/llm/providers/openai/response_adapter/completion.rb +1 -1
  35. data/lib/llm/providers/openai/response_adapter/image.rb +0 -4
  36. data/lib/llm/providers/openai/responses.rb +1 -0
  37. data/lib/llm/providers/openai/stream_parser.rb +5 -6
  38. data/lib/llm/providers/openai.rb +2 -2
  39. data/lib/llm/providers/xai/images.rb +49 -26
  40. data/lib/llm/providers/xai.rb +2 -2
  41. data/lib/llm/response.rb +10 -0
  42. data/lib/llm/schema/leaf.rb +7 -1
  43. data/lib/llm/schema/renderer.rb +121 -0
  44. data/lib/llm/schema.rb +30 -0
  45. data/lib/llm/sequel/agent.rb +2 -43
  46. data/lib/llm/sequel/plugin.rb +25 -7
  47. data/lib/llm/tracer/telemetry.rb +4 -6
  48. data/lib/llm/tracer.rb +9 -21
  49. data/lib/llm/transport/execution.rb +16 -1
  50. data/lib/llm/transport/net_http_adapter.rb +1 -1
  51. data/lib/llm/uridata.rb +16 -0
  52. data/lib/llm/version.rb +1 -1
  53. data/lib/llm.rb +9 -0
  54. data/llm.gemspec +5 -18
  55. data/resources/deepdive.md +798 -264
  56. metadata +15 -18
  57. 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
@@ -5,9 +5,9 @@ module LLM::ActiveRecord
5
5
  DEFAULTS = {
6
6
  data_column: :data,
7
7
  format: :string,
8
- tracer: nil,
9
- provider: nil,
10
- context: EMPTY_HASH
8
+ provider: :set_provider,
9
+ context: :set_context,
10
+ tracer: :set_tracer
11
11
  }.freeze
12
12
 
13
13
  ##
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 Defaults to :completions
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
- params = params.merge(previous_response_id: res_id, input: @messages.to_a).compact
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.id, tool.name, attrs)
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 [Hash]
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
@@ -348,8 +348,8 @@ class LLM::Provider
348
348
 
349
349
  private
350
350
 
351
- def path(suffix)
352
- return suffix if @base_path.empty?
351
+ def path(suffix, base_path: true)
352
+ return suffix if !base_path || @base_path.empty?
353
353
  "#{@base_path}#{suffix}"
354
354
  end
355
355
 
@@ -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,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LLM::DeepSeek::ResponseAdapter
4
+ module Image
5
+ def images
6
+ [StringIO.new(content!.svg)]
7
+ end
8
+ end
9
+ end
@@ -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 [NotImplementedError]
47
+ # @raise [LLM::DeepSeek::Images]
46
48
  def images
47
- raise NotImplementedError
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 adapt_schema(params)
25
- return {} unless params and params[:schema]
26
- schema = params.delete(:schema)
27
- schema = schema.respond_to?(:object) ? schema.object : schema
28
- {generationConfig: {response_mime_type: "application/json", response_schema: schema}}
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