llm.rb 4.8.0 → 4.9.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 (74) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +337 -590
  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/stdio.rb +85 -0
  26. data/lib/llm/mcp.rb +102 -0
  27. data/lib/llm/message.rb +13 -11
  28. data/lib/llm/model.rb +2 -2
  29. data/lib/llm/prompt.rb +17 -7
  30. data/lib/llm/provider.rb +32 -17
  31. data/lib/llm/providers/anthropic/files.rb +3 -3
  32. data/lib/llm/providers/anthropic.rb +19 -4
  33. data/lib/llm/providers/deepseek.rb +10 -3
  34. data/lib/llm/providers/{gemini → google}/audio.rb +6 -6
  35. data/lib/llm/providers/{gemini → google}/error_handler.rb +2 -2
  36. data/lib/llm/providers/{gemini → google}/files.rb +11 -11
  37. data/lib/llm/providers/{gemini → google}/images.rb +7 -7
  38. data/lib/llm/providers/{gemini → google}/models.rb +5 -5
  39. data/lib/llm/providers/{gemini → google}/request_adapter/completion.rb +7 -3
  40. data/lib/llm/providers/{gemini → google}/request_adapter.rb +1 -1
  41. data/lib/llm/providers/{gemini → google}/response_adapter/completion.rb +7 -7
  42. data/lib/llm/providers/{gemini → google}/response_adapter/embedding.rb +1 -1
  43. data/lib/llm/providers/{gemini → google}/response_adapter/file.rb +1 -1
  44. data/lib/llm/providers/{gemini → google}/response_adapter/files.rb +1 -1
  45. data/lib/llm/providers/{gemini → google}/response_adapter/image.rb +1 -1
  46. data/lib/llm/providers/{gemini → google}/response_adapter/models.rb +1 -1
  47. data/lib/llm/providers/{gemini → google}/response_adapter/web_search.rb +2 -2
  48. data/lib/llm/providers/{gemini → google}/response_adapter.rb +8 -8
  49. data/lib/llm/providers/{gemini → google}/stream_parser.rb +3 -3
  50. data/lib/llm/providers/{gemini.rb → google.rb} +41 -26
  51. data/lib/llm/providers/llamacpp.rb +10 -3
  52. data/lib/llm/providers/ollama.rb +19 -4
  53. data/lib/llm/providers/openai/files.rb +3 -3
  54. data/lib/llm/providers/openai/response_adapter/completion.rb +9 -1
  55. data/lib/llm/providers/openai/response_adapter/responds.rb +9 -1
  56. data/lib/llm/providers/openai/responses.rb +9 -1
  57. data/lib/llm/providers/openai/stream_parser.rb +2 -0
  58. data/lib/llm/providers/openai.rb +19 -4
  59. data/lib/llm/providers/xai.rb +10 -3
  60. data/lib/llm/providers/zai.rb +9 -2
  61. data/lib/llm/registry.rb +81 -0
  62. data/lib/llm/schema/parser.rb +109 -0
  63. data/lib/llm/schema.rb +4 -0
  64. data/lib/llm/server_tool.rb +5 -5
  65. data/lib/llm/session.rb +10 -1
  66. data/lib/llm/tool.rb +85 -4
  67. data/lib/llm/tracer/logger.rb +1 -1
  68. data/lib/llm/tracer/telemetry.rb +7 -7
  69. data/lib/llm/tracer.rb +3 -3
  70. data/lib/llm/usage.rb +5 -0
  71. data/lib/llm/version.rb +1 -1
  72. data/lib/llm.rb +39 -6
  73. data/llm.gemspec +45 -8
  74. metadata +81 -28
@@ -1,21 +1,21 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class LLM::Gemini
3
+ class LLM::Google
4
4
  ##
5
- # The {LLM::Gemini::Audio LLM::Gemini::Audio} class provides an audio
5
+ # The {LLM::Google::Audio LLM::Google::Audio} class provides an audio
6
6
  # object for interacting with [Gemini's audio API](https://ai.google.dev/gemini-api/docs/audio).
7
7
  # @example
8
8
  # #!/usr/bin/env ruby
9
9
  # require "llm"
10
10
  #
11
- # llm = LLM.gemini(key: ENV["KEY"])
11
+ # llm = LLM.google(key: ENV["KEY"])
12
12
  # res = llm.audio.create_transcription(input: "/audio/rocket.mp3")
13
13
  # res.text # => "A dog on a rocket to the moon"
14
14
  class Audio
15
15
  ##
16
16
  # Returns a new Audio object
17
17
  # @param provider [LLM::Provider]
18
- # @return [LLM::Gemini::Responses]
18
+ # @return [LLM::Google::Audio]
19
19
  def initialize(provider)
20
20
  @provider = provider
21
21
  end
@@ -30,7 +30,7 @@ class LLM::Gemini
30
30
  ##
31
31
  # Create an audio transcription
32
32
  # @example
33
- # llm = LLM.gemini(key: ENV["KEY"])
33
+ # llm = LLM.google(key: ENV["KEY"])
34
34
  # res = llm.audio.create_transcription(file: "/audio/rocket.mp3")
35
35
  # res.text # => "A dog on a rocket to the moon"
36
36
  # @see https://ai.google.dev/gemini-api/docs/audio Gemini docs
@@ -52,7 +52,7 @@ class LLM::Gemini
52
52
  # Create an audio translation (in English)
53
53
  # @example
54
54
  # # Arabic => English
55
- # llm = LLM.gemini(key: ENV["KEY"])
55
+ # llm = LLM.google(key: ENV["KEY"])
56
56
  # res = llm.audio.create_translation(file: "/audio/bismillah.mp3")
57
57
  # res.text # => "In the name of Allah, the Beneficent, the Merciful."
58
58
  # @see https://ai.google.dev/gemini-api/docs/audio Gemini docs
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class LLM::Gemini
3
+ class LLM::Google
4
4
  ##
5
5
  # @private
6
6
  class ErrorHandler
@@ -21,7 +21,7 @@ class LLM::Gemini
21
21
  # The span
22
22
  # @param [Net::HTTPResponse] res
23
23
  # The response from the server
24
- # @return [LLM::Gemini::ErrorHandler]
24
+ # @return [LLM::Google::ErrorHandler]
25
25
  def initialize(tracer, span, res)
26
26
  @tracer = tracer
27
27
  @span = span
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class LLM::Gemini
3
+ class LLM::Google
4
4
  ##
5
- # The {LLM::Gemini::Files LLM::Gemini::Files} class provides a files
5
+ # The {LLM::Google::Files LLM::Google::Files} class provides a files
6
6
  # object for interacting with [Gemini's Files API](https://ai.google.dev/gemini-api/docs/files).
7
7
  # The files API allows a client to reference media files in prompts
8
8
  # where they can be referenced by their URL.
@@ -17,16 +17,16 @@ class LLM::Gemini
17
17
  # #!/usr/bin/env ruby
18
18
  # require "llm"
19
19
  #
20
- # llm = LLM.gemini(key: ENV["KEY"])
21
- # ses = LLM::Session.new(llm)
20
+ # llm = LLM.google(key: ENV["KEY"])
21
+ # ctx = LLM::Context.new(llm)
22
22
  # file = llm.files.create(file: "/audio/haiku.mp3")
23
- # ses.talk ["Tell me about this file", file]
24
- # ses.messages.select(&:assistant?).each { print "[#{_1.role}]", _1.content, "\n" }
23
+ # ctx.talk ["Tell me about this file", file]
24
+ # ctx.messages.select(&:assistant?).each { print "[#{_1.role}]", _1.content, "\n" }
25
25
  class Files
26
26
  ##
27
27
  # Returns a new Files object
28
28
  # @param provider [LLM::Provider]
29
- # @return [LLM::Gemini::Files]
29
+ # @return [LLM::Google::Files]
30
30
  def initialize(provider)
31
31
  @provider = provider
32
32
  end
@@ -34,7 +34,7 @@ class LLM::Gemini
34
34
  ##
35
35
  # List all files
36
36
  # @example
37
- # llm = LLM.gemini(key: ENV["KEY"])
37
+ # llm = LLM.google(key: ENV["KEY"])
38
38
  # res = llm.files.all
39
39
  # res.each do |file|
40
40
  # print "name: ", file.name, "\n"
@@ -55,7 +55,7 @@ class LLM::Gemini
55
55
  ##
56
56
  # Create a file
57
57
  # @example
58
- # llm = LLM.gemini(key: ENV["KEY"])
58
+ # llm = LLM.google(key: ENV["KEY"])
59
59
  # res = llm.files.create(file: "/audio/haiku.mp3")
60
60
  # @see https://ai.google.dev/gemini-api/docs/files Gemini docs
61
61
  # @param [String, LLM::File] file The file
@@ -80,7 +80,7 @@ class LLM::Gemini
80
80
  ##
81
81
  # Get a file
82
82
  # @example
83
- # llm = LLM.gemini(key: ENV["KEY"])
83
+ # llm = LLM.google(key: ENV["KEY"])
84
84
  # res = llm.files.get(file: "files/1234567890")
85
85
  # print "name: ", res.name, "\n"
86
86
  # @see https://ai.google.dev/gemini-api/docs/files Gemini docs
@@ -101,7 +101,7 @@ class LLM::Gemini
101
101
  ##
102
102
  # Delete a file
103
103
  # @example
104
- # llm = LLM.gemini(key: ENV["KEY"])
104
+ # llm = LLM.google(key: ENV["KEY"])
105
105
  # res = llm.files.delete(file: "files/1234567890")
106
106
  # @see https://ai.google.dev/gemini-api/docs/files Gemini docs
107
107
  # @param [#name, String] file The file to delete
@@ -1,15 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class LLM::Gemini
3
+ class LLM::Google
4
4
  ##
5
- # The {LLM::Gemini::Images LLM::Gemini::Images} class provides an images
5
+ # The {LLM::Google::Images LLM::Google::Images} class provides an images
6
6
  # object for interacting with Google's Imagen text-to-image models via the
7
7
  # Imagen API: https://ai.google.dev/gemini-api/docs/imagen
8
8
  #
9
9
  # @example
10
10
  # #!/usr/bin/env ruby
11
11
  # require "llm"
12
- # llm = LLM.gemini(key: ENV["KEY"])
12
+ # llm = LLM.google(key: ENV["KEY"])
13
13
  # res = llm.images.create prompt: "A dog on a rocket to the moon"
14
14
  # IO.copy_stream res.images[0], "rocket.png"
15
15
  class Images
@@ -18,7 +18,7 @@ class LLM::Gemini
18
18
  ##
19
19
  # Returns a new Images object
20
20
  # @param provider [LLM::Provider]
21
- # @return [LLM::Gemini::Responses]
21
+ # @return [LLM::Google::Images]
22
22
  def initialize(provider)
23
23
  @provider = provider
24
24
  end
@@ -26,7 +26,7 @@ class LLM::Gemini
26
26
  ##
27
27
  # Create an image
28
28
  # @example
29
- # llm = LLM.gemini(key: ENV["KEY"])
29
+ # llm = LLM.google(key: ENV["KEY"])
30
30
  # res = llm.images.create prompt: "A dog on a rocket to the moon"
31
31
  # IO.copy_stream res.images[0], "rocket.png"
32
32
  # @see https://ai.google.dev/gemini-api/docs/imagen Imagen docs
@@ -60,7 +60,7 @@ class LLM::Gemini
60
60
  ##
61
61
  # Edit an image
62
62
  # @example
63
- # llm = LLM.gemini(key: ENV["KEY"])
63
+ # llm = LLM.google(key: ENV["KEY"])
64
64
  # res = llm.images.edit image: "cat.png", prompt: "Add a hat to the cat"
65
65
  # IO.copy_stream res.images[0], "hatoncat.png"
66
66
  # @see https://ai.google.dev/gemini-api/docs/image-generation Gemini docs
@@ -68,7 +68,7 @@ class LLM::Gemini
68
68
  # @param [String] prompt The prompt
69
69
  # @param [Hash] params Other parameters (see Gemini docs)
70
70
  # @raise (see LLM::Provider#request)
71
- # @note (see LLM::Gemini::Images#create)
71
+ # @note (see LLM::Google::Images#create)
72
72
  # @return [LLM::Response]
73
73
  def edit(image:, prompt:, model: "gemini-2.5-flash-image", **params)
74
74
  raise NotImplementedError, "image editing is not yet supported by Gemini"
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class LLM::Gemini
3
+ class LLM::Google
4
4
  ##
5
- # The {LLM::Gemini::Models LLM::Gemini::Models} class provides a model
5
+ # The {LLM::Google::Models LLM::Google::Models} class provides a model
6
6
  # object for interacting with [Gemini's models API](https://ai.google.dev/api/models?hl=en#method:-models.list).
7
7
  # The models API allows a client to query Gemini for a list of models
8
8
  # that are available for use with the Gemini API.
@@ -11,7 +11,7 @@ class LLM::Gemini
11
11
  # #!/usr/bin/env ruby
12
12
  # require "llm"
13
13
  #
14
- # llm = LLM.gemini(key: ENV["KEY"])
14
+ # llm = LLM.google(key: ENV["KEY"])
15
15
  # res = llm.models.all
16
16
  # res.each do |model|
17
17
  # print "id: ", model.id, "\n"
@@ -22,7 +22,7 @@ class LLM::Gemini
22
22
  ##
23
23
  # Returns a new Models object
24
24
  # @param provider [LLM::Provider]
25
- # @return [LLM::Gemini::Models]
25
+ # @return [LLM::Google::Models]
26
26
  def initialize(provider)
27
27
  @provider = provider
28
28
  end
@@ -30,7 +30,7 @@ class LLM::Gemini
30
30
  ##
31
31
  # List all models
32
32
  # @example
33
- # llm = LLM.gemini(key: ENV["KEY"])
33
+ # llm = LLM.google(key: ENV["KEY"])
34
34
  # res = llm.models.all
35
35
  # res.each do |model|
36
36
  # print "id: ", model.id, "\n"
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module LLM::Gemini::RequestAdapter
3
+ module LLM::Google::RequestAdapter
4
4
  ##
5
5
  # @private
6
6
  class Completion
@@ -19,7 +19,7 @@ module LLM::Gemini::RequestAdapter
19
19
  if Hash === message
20
20
  {role: message[:role], parts: adapt_content(message[:content])}
21
21
  elsif message.tool_call?
22
- {role: message.role, parts: message.extra[:original_tool_calls].map { {"functionCall" => _1} }}
22
+ {role: message.role, parts: message.extra.original_tool_calls}
23
23
  else
24
24
  {role: message.role, parts: adapt_content(message.content)}
25
25
  end
@@ -37,7 +37,7 @@ module LLM::Gemini::RequestAdapter
37
37
  when LLM::Message
38
38
  adapt_content(content.content)
39
39
  when LLM::Function::Return
40
- [{functionResponse: {name: content.name, response: content.value}}]
40
+ [{functionResponse: {name: content.name, response: adapt_function_response(content.value)}}]
41
41
  when LLM::Object
42
42
  adapt_object(content)
43
43
  else
@@ -64,6 +64,10 @@ module LLM::Gemini::RequestAdapter
64
64
  [{file_data: {mime_type: file.mime_type, file_uri: file.uri}}]
65
65
  end
66
66
 
67
+ def adapt_function_response(value)
68
+ Hash === value ? value : {result: value}
69
+ end
70
+
67
71
  def prompt_error!(object)
68
72
  if LLM::Object === object
69
73
  raise LLM::PromptError, "The given LLM::Object with kind '#{content.kind}' is not " \
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class LLM::Gemini
3
+ class LLM::Google
4
4
  ##
5
5
  # @private
6
6
  module RequestAdapter
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module LLM::Gemini::ResponseAdapter
3
+ module LLM::Google::ResponseAdapter
4
4
  module Completion
5
5
  ##
6
6
  # (see LLM::Contract::Completion#messages)
@@ -64,17 +64,17 @@ module LLM::Gemini::ResponseAdapter
64
64
  content = choice.content || LLM::Object.new
65
65
  role = content.role || "model"
66
66
  parts = content.parts || [{"text" => choice.finishReason}]
67
- text = parts.filter_map { _1["text"] }.join
68
- tools = parts.filter_map { _1["functionCall"] }
67
+ text = parts.filter_map { _1["text"] }.join
68
+ tools = parts.select { _1["functionCall"] }
69
69
  extra = {index:, response: self, tool_calls: adapt_tool_calls(tools), original_tool_calls: tools}
70
70
  LLM::Message.new(role, text, extra)
71
71
  end
72
72
  end
73
73
 
74
- def adapt_tool_calls(tools)
75
- (tools || []).map do |tool|
76
- function = {name: tool.name, arguments: tool.args}
77
- function
74
+ def adapt_tool_calls(parts)
75
+ (parts || []).map do |part|
76
+ tool = part["functionCall"]
77
+ {name: tool.name, arguments: tool.args}
78
78
  end
79
79
  end
80
80
 
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module LLM::Gemini::ResponseAdapter
3
+ module LLM::Google::ResponseAdapter
4
4
  module Embedding
5
5
  def model = "text-embedding-004"
6
6
  def embeddings = body.dig("embedding", "values")
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module LLM::Gemini::ResponseAdapter
3
+ module LLM::Google::ResponseAdapter
4
4
  module File
5
5
  def name = respond_to?(:file) ? file.name : body.name
6
6
  def display_name = respond_to?(:file) ? file.displayName : body.displayName
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module LLM::Gemini::ResponseAdapter
3
+ module LLM::Google::ResponseAdapter
4
4
  module Files
5
5
  include ::Enumerable
6
6
  def each(&)
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module LLM::Gemini::ResponseAdapter
3
+ module LLM::Google::ResponseAdapter
4
4
  module Image
5
5
  ##
6
6
  # @return [Array<StringIO>]
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module LLM::Gemini::ResponseAdapter
3
+ module LLM::Google::ResponseAdapter
4
4
  module Models
5
5
  include LLM::Model::Collection
6
6
 
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module LLM::Gemini::ResponseAdapter
3
+ module LLM::Google::ResponseAdapter
4
4
  ##
5
- # The {LLM::Gemini::ResponseAdapter::WebSearch LLM::Gemini::ResponseAdapter::WebSearch}
5
+ # The {LLM::Google::ResponseAdapter::WebSearch LLM::Google::ResponseAdapter::WebSearch}
6
6
  # module provides methods for accessing web search results from a web search
7
7
  # tool call made via the {LLM::Provider#web_search LLM::Provider#web_search}
8
8
  # method.
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class LLM::Gemini
3
+ class LLM::Google
4
4
  ##
5
5
  # @private
6
6
  module ResponseAdapter
@@ -27,13 +27,13 @@ class LLM::Gemini
27
27
  # @api private
28
28
  def select(type)
29
29
  case type
30
- when :completion then LLM::Gemini::ResponseAdapter::Completion
31
- when :embedding then LLM::Gemini::ResponseAdapter::Embedding
32
- when :file then LLM::Gemini::ResponseAdapter::File
33
- when :files then LLM::Gemini::ResponseAdapter::Files
34
- when :image then LLM::Gemini::ResponseAdapter::Image
35
- when :models then LLM::Gemini::ResponseAdapter::Models
36
- when :web_search then LLM::Gemini::ResponseAdapter::WebSearch
30
+ when :completion then LLM::Google::ResponseAdapter::Completion
31
+ when :embedding then LLM::Google::ResponseAdapter::Embedding
32
+ when :file then LLM::Google::ResponseAdapter::File
33
+ when :files then LLM::Google::ResponseAdapter::Files
34
+ when :image then LLM::Google::ResponseAdapter::Image
35
+ when :models then LLM::Google::ResponseAdapter::Models
36
+ when :web_search then LLM::Google::ResponseAdapter::WebSearch
37
37
  else
38
38
  raise ArgumentError, "Unknown response adapter type: #{type.inspect}"
39
39
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class LLM::Gemini
3
+ class LLM::Google
4
4
  ##
5
5
  # @private
6
6
  class StreamParser
@@ -11,7 +11,7 @@ class LLM::Gemini
11
11
 
12
12
  ##
13
13
  # @param [#<<] io An IO-like object
14
- # @return [LLM::Gemini::StreamParser]
14
+ # @return [LLM::Google::StreamParser]
15
15
  def initialize(io)
16
16
  @body = {"candidates" => []}
17
17
  @io = io
@@ -19,7 +19,7 @@ class LLM::Gemini
19
19
 
20
20
  ##
21
21
  # @param [Hash] chunk
22
- # @return [LLM::Gemini::StreamParser]
22
+ # @return [LLM::Google::StreamParser]
23
23
  def parse!(chunk)
24
24
  tap { merge_chunk!(chunk) }
25
25
  end
@@ -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