llm.rb 2.1.0 → 3.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 (92) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +6 -0
  3. data/lib/llm/bot.rb +4 -4
  4. data/lib/llm/buffer.rb +0 -9
  5. data/lib/llm/contract/completion.rb +57 -0
  6. data/lib/llm/contract.rb +48 -0
  7. data/lib/llm/error.rb +22 -14
  8. data/lib/llm/eventhandler.rb +6 -4
  9. data/lib/llm/eventstream/parser.rb +18 -13
  10. data/lib/llm/function.rb +1 -1
  11. data/lib/llm/json_adapter.rb +109 -0
  12. data/lib/llm/message.rb +7 -28
  13. data/lib/llm/multipart/enumerator_io.rb +86 -0
  14. data/lib/llm/multipart.rb +32 -51
  15. data/lib/llm/object/builder.rb +6 -6
  16. data/lib/llm/object/kernel.rb +2 -2
  17. data/lib/llm/object.rb +23 -8
  18. data/lib/llm/provider.rb +11 -3
  19. data/lib/llm/providers/anthropic/error_handler.rb +1 -1
  20. data/lib/llm/providers/anthropic/files.rb +4 -5
  21. data/lib/llm/providers/anthropic/models.rb +1 -2
  22. data/lib/llm/providers/anthropic/{format/completion_format.rb → request_adapter/completion.rb} +19 -19
  23. data/lib/llm/providers/anthropic/{format.rb → request_adapter.rb} +7 -7
  24. data/lib/llm/providers/anthropic/response_adapter/completion.rb +66 -0
  25. data/lib/llm/providers/anthropic/{response → response_adapter}/enumerable.rb +1 -1
  26. data/lib/llm/providers/anthropic/{response → response_adapter}/file.rb +1 -1
  27. data/lib/llm/providers/anthropic/{response → response_adapter}/web_search.rb +3 -3
  28. data/lib/llm/providers/anthropic/response_adapter.rb +36 -0
  29. data/lib/llm/providers/anthropic/stream_parser.rb +6 -6
  30. data/lib/llm/providers/anthropic.rb +8 -11
  31. data/lib/llm/providers/deepseek/{format/completion_format.rb → request_adapter/completion.rb} +15 -15
  32. data/lib/llm/providers/deepseek/{format.rb → request_adapter.rb} +7 -7
  33. data/lib/llm/providers/deepseek.rb +2 -2
  34. data/lib/llm/providers/gemini/audio.rb +2 -2
  35. data/lib/llm/providers/gemini/error_handler.rb +3 -3
  36. data/lib/llm/providers/gemini/files.rb +4 -7
  37. data/lib/llm/providers/gemini/images.rb +9 -14
  38. data/lib/llm/providers/gemini/models.rb +1 -2
  39. data/lib/llm/providers/gemini/{format/completion_format.rb → request_adapter/completion.rb} +14 -14
  40. data/lib/llm/providers/gemini/{format.rb → request_adapter.rb} +8 -8
  41. data/lib/llm/providers/gemini/response_adapter/completion.rb +67 -0
  42. data/lib/llm/providers/gemini/{response → response_adapter}/embedding.rb +1 -1
  43. data/lib/llm/providers/gemini/{response → response_adapter}/file.rb +1 -1
  44. data/lib/llm/providers/gemini/{response → response_adapter}/files.rb +1 -1
  45. data/lib/llm/providers/gemini/{response → response_adapter}/image.rb +3 -3
  46. data/lib/llm/providers/gemini/{response → response_adapter}/models.rb +1 -1
  47. data/lib/llm/providers/gemini/{response → response_adapter}/web_search.rb +3 -3
  48. data/lib/llm/providers/gemini/response_adapter.rb +42 -0
  49. data/lib/llm/providers/gemini/stream_parser.rb +37 -32
  50. data/lib/llm/providers/gemini.rb +10 -14
  51. data/lib/llm/providers/ollama/error_handler.rb +1 -1
  52. data/lib/llm/providers/ollama/{format/completion_format.rb → request_adapter/completion.rb} +19 -19
  53. data/lib/llm/providers/ollama/{format.rb → request_adapter.rb} +7 -7
  54. data/lib/llm/providers/ollama/response_adapter/completion.rb +61 -0
  55. data/lib/llm/providers/ollama/{response → response_adapter}/embedding.rb +1 -1
  56. data/lib/llm/providers/ollama/response_adapter.rb +32 -0
  57. data/lib/llm/providers/ollama/stream_parser.rb +2 -2
  58. data/lib/llm/providers/ollama.rb +8 -10
  59. data/lib/llm/providers/openai/audio.rb +1 -1
  60. data/lib/llm/providers/openai/error_handler.rb +12 -2
  61. data/lib/llm/providers/openai/files.rb +3 -6
  62. data/lib/llm/providers/openai/images.rb +4 -5
  63. data/lib/llm/providers/openai/models.rb +1 -3
  64. data/lib/llm/providers/openai/moderations.rb +3 -5
  65. data/lib/llm/providers/openai/{format/completion_format.rb → request_adapter/completion.rb} +22 -22
  66. data/lib/llm/providers/openai/{format/moderation_format.rb → request_adapter/moderation.rb} +5 -5
  67. data/lib/llm/providers/openai/{format/respond_format.rb → request_adapter/respond.rb} +16 -16
  68. data/lib/llm/providers/openai/{format.rb → request_adapter.rb} +12 -12
  69. data/lib/llm/providers/openai/{response → response_adapter}/audio.rb +1 -1
  70. data/lib/llm/providers/openai/response_adapter/completion.rb +62 -0
  71. data/lib/llm/providers/openai/{response → response_adapter}/embedding.rb +1 -1
  72. data/lib/llm/providers/openai/{response → response_adapter}/enumerable.rb +1 -1
  73. data/lib/llm/providers/openai/{response → response_adapter}/file.rb +1 -1
  74. data/lib/llm/providers/openai/{response → response_adapter}/image.rb +1 -1
  75. data/lib/llm/providers/openai/{response → response_adapter}/moderations.rb +1 -1
  76. data/lib/llm/providers/openai/{response → response_adapter}/responds.rb +6 -10
  77. data/lib/llm/providers/openai/{response → response_adapter}/web_search.rb +3 -3
  78. data/lib/llm/providers/openai/response_adapter.rb +47 -0
  79. data/lib/llm/providers/openai/responses/stream_parser.rb +22 -22
  80. data/lib/llm/providers/openai/responses.rb +6 -8
  81. data/lib/llm/providers/openai/stream_parser.rb +6 -5
  82. data/lib/llm/providers/openai/vector_stores.rb +8 -9
  83. data/lib/llm/providers/openai.rb +12 -14
  84. data/lib/llm/response.rb +2 -5
  85. data/lib/llm/usage.rb +10 -0
  86. data/lib/llm/version.rb +1 -1
  87. data/lib/llm.rb +33 -1
  88. metadata +44 -35
  89. data/lib/llm/providers/anthropic/response/completion.rb +0 -39
  90. data/lib/llm/providers/gemini/response/completion.rb +0 -35
  91. data/lib/llm/providers/ollama/response/completion.rb +0 -28
  92. data/lib/llm/providers/openai/response/completion.rb +0 -40
@@ -6,14 +6,14 @@ class LLM::Gemini
6
6
  class StreamParser
7
7
  ##
8
8
  # Returns the fully constructed response body
9
- # @return [LLM::Object]
9
+ # @return [Hash]
10
10
  attr_reader :body
11
11
 
12
12
  ##
13
13
  # @param [#<<] io An IO-like object
14
14
  # @return [LLM::Gemini::StreamParser]
15
15
  def initialize(io)
16
- @body = LLM::Object.from_hash({candidates: []})
16
+ @body = {"candidates" => []}
17
17
  @io = io
18
18
  end
19
19
 
@@ -21,35 +21,37 @@ class LLM::Gemini
21
21
  # @param [Hash] chunk
22
22
  # @return [LLM::Gemini::StreamParser]
23
23
  def parse!(chunk)
24
- tap { merge_chunk!(LLM::Object.from_hash(chunk)) }
24
+ tap { merge_chunk!(chunk) }
25
25
  end
26
26
 
27
27
  private
28
28
 
29
29
  def merge_chunk!(chunk)
30
30
  chunk.each do |key, value|
31
- if key.to_s == "candidates"
31
+ k = key.to_s
32
+ if k == "candidates"
32
33
  merge_candidates!(value)
33
- elsif key.to_s == "usageMetadata" &&
34
- @body.usageMetadata.is_a?(LLM::Object) &&
35
- value.is_a?(LLM::Object)
36
- @body.usageMetadata = LLM::Object.from_hash(@body.usageMetadata.to_h.merge(value.to_h))
34
+ elsif k == "usageMetadata" &&
35
+ @body["usageMetadata"].is_a?(Hash) &&
36
+ value.is_a?(Hash)
37
+ @body["usageMetadata"] = @body["usageMetadata"].merge(value)
37
38
  else
38
- @body[key] = value
39
+ @body[k] = value
39
40
  end
40
41
  end
41
42
  end
42
43
 
43
44
  def merge_candidates!(deltas)
44
45
  deltas.each do |delta|
45
- index = delta.index
46
- @body.candidates[index] ||= LLM::Object.from_hash({content: {parts: []}})
47
- candidate = @body.candidates[index]
46
+ index = delta["index"]
47
+ @body["candidates"][index] ||= {"content" => {"parts" => []}}
48
+ candidate = @body["candidates"][index]
48
49
  delta.each do |key, value|
49
- if key.to_s == "content"
50
- merge_candidate_content!(candidate.content, value) if value
50
+ k = key.to_s
51
+ if k == "content"
52
+ merge_candidate_content!(candidate["content"], value) if value
51
53
  else
52
- candidate[key] = value # Overwrite other fields
54
+ candidate[k] = value # Overwrite other fields
53
55
  end
54
56
  end
55
57
  end
@@ -57,26 +59,27 @@ class LLM::Gemini
57
59
 
58
60
  def merge_candidate_content!(content, delta)
59
61
  delta.each do |key, value|
60
- if key.to_s == "parts"
61
- content.parts ||= []
62
- merge_content_parts!(content.parts, value) if value
62
+ k = key.to_s
63
+ if k == "parts"
64
+ content["parts"] ||= []
65
+ merge_content_parts!(content["parts"], value) if value
63
66
  else
64
- content[key] = value
67
+ content[k] = value
65
68
  end
66
69
  end
67
70
  end
68
71
 
69
72
  def merge_content_parts!(parts, deltas)
70
73
  deltas.each do |delta|
71
- if delta.text
74
+ if delta["text"]
72
75
  merge_text!(parts, delta)
73
- elsif delta.functionCall
76
+ elsif delta["functionCall"]
74
77
  merge_function_call!(parts, delta)
75
- elsif delta.inlineData
78
+ elsif delta["inlineData"]
76
79
  parts << delta
77
- elsif delta.functionResponse
80
+ elsif delta["functionResponse"]
78
81
  parts << delta
79
- elsif delta.fileData
82
+ elsif delta["fileData"]
80
83
  parts << delta
81
84
  end
82
85
  end
@@ -84,21 +87,23 @@ class LLM::Gemini
84
87
 
85
88
  def merge_text!(parts, delta)
86
89
  last_existing_part = parts.last
87
- if last_existing_part&.text
88
- last_existing_part.text << delta.text
89
- @io << delta.text if @io.respond_to?(:<<)
90
+ text = delta["text"]
91
+ if last_existing_part.is_a?(Hash) && last_existing_part["text"]
92
+ last_existing_part["text"] ||= +""
93
+ last_existing_part["text"] << text
94
+ @io << text if @io.respond_to?(:<<)
90
95
  else
91
96
  parts << delta
92
- @io << delta.text if @io.respond_to?(:<<)
97
+ @io << text if @io.respond_to?(:<<)
93
98
  end
94
99
  end
95
100
 
96
101
  def merge_function_call!(parts, delta)
97
102
  last_existing_part = parts.last
98
- if last_existing_part&.functionCall
99
- last_existing_part.functionCall = LLM::Object.from_hash(
100
- last_existing_part.functionCall.to_h.merge(delta.functionCall.to_h)
101
- )
103
+ last_call = last_existing_part.is_a?(Hash) ? last_existing_part["functionCall"] : nil
104
+ delta_call = delta["functionCall"]
105
+ if last_call.is_a?(Hash) && delta_call.is_a?(Hash)
106
+ last_existing_part["functionCall"] = last_call.merge(delta_call)
102
107
  else
103
108
  parts << delta
104
109
  end
@@ -18,18 +18,16 @@ module LLM
18
18
  # bot.chat ["Tell me about this photo", File.open("/images/horse.jpg", "rb")]
19
19
  # bot.messages.select(&:assistant?).each { print "[#{_1.role}]", _1.content, "\n" }
20
20
  class Gemini < Provider
21
- require_relative "gemini/response/embedding"
22
- require_relative "gemini/response/completion"
23
- require_relative "gemini/response/web_search"
24
21
  require_relative "gemini/error_handler"
25
- require_relative "gemini/format"
22
+ require_relative "gemini/request_adapter"
23
+ require_relative "gemini/response_adapter"
26
24
  require_relative "gemini/stream_parser"
27
25
  require_relative "gemini/models"
28
26
  require_relative "gemini/images"
29
- require_relative "gemini/files"
30
27
  require_relative "gemini/audio"
28
+ require_relative "gemini/files"
31
29
 
32
- include Format
30
+ include RequestAdapter
33
31
 
34
32
  HOST = "generativelanguage.googleapis.com"
35
33
 
@@ -50,9 +48,9 @@ module LLM
50
48
  model = model.respond_to?(:id) ? model.id : model
51
49
  path = ["/v1beta/models/#{model}", "embedContent?key=#{@key}"].join(":")
52
50
  req = Net::HTTP::Post.new(path, headers)
53
- req.body = JSON.dump({content: {parts: [{text: input}]}})
51
+ req.body = LLM.json.dump({content: {parts: [{text: input}]}})
54
52
  res = execute(request: req)
55
- LLM::Response.new(res).extend(LLM::Gemini::Response::Embedding)
53
+ ResponseAdapter.adapt(res, type: :embedding)
56
54
  end
57
55
 
58
56
  ##
@@ -68,18 +66,17 @@ module LLM
68
66
  def complete(prompt, params = {})
69
67
  params = {role: :user, model: default_model}.merge!(params)
70
68
  tools = resolve_tools(params.delete(:tools))
71
- params = [params, format_schema(params), format_tools(tools)].inject({}, &:merge!).compact
69
+ params = [params, adapt_schema(params), adapt_tools(tools)].inject({}, &:merge!).compact
72
70
  role, model, stream = [:role, :model, :stream].map { params.delete(_1) }
73
71
  action = stream ? "streamGenerateContent?key=#{@key}&alt=sse" : "generateContent?key=#{@key}"
74
72
  model.respond_to?(:id) ? model.id : model
75
73
  path = ["/v1beta/models/#{model}", action].join(":")
76
74
  req = Net::HTTP::Post.new(path, headers)
77
75
  messages = [*(params.delete(:messages) || []), LLM::Message.new(role, prompt)]
78
- body = JSON.dump({contents: format(messages)}.merge!(params))
76
+ body = LLM.json.dump({contents: adapt(messages)}.merge!(params))
79
77
  set_body_stream(req, StringIO.new(body))
80
78
  res = execute(request: req, stream:)
81
- LLM::Response.new(res)
82
- .extend(LLM::Gemini::Response::Completion)
79
+ ResponseAdapter.adapt(res, type: :completion)
83
80
  .extend(Module.new { define_method(:__tools__) { tools } })
84
81
  end
85
82
 
@@ -150,8 +147,7 @@ module LLM
150
147
  # @param query [String] The search query.
151
148
  # @return [LLM::Response] The response from the LLM provider.
152
149
  def web_search(query:)
153
- complete(query, tools: [server_tools[:google_search]])
154
- .extend(LLM::Gemini::Response::WebSearch)
150
+ ResponseAdapter.adapt(complete(query, tools: [server_tools[:google_search]]), type: :web_search)
155
151
  end
156
152
 
157
153
  private
@@ -29,7 +29,7 @@ class LLM::Ollama
29
29
  when Net::HTTPTooManyRequests
30
30
  raise LLM::RateLimitError.new { _1.response = res }, "Too many requests"
31
31
  else
32
- raise LLM::ResponseError.new { _1.response = res }, "Unexpected response"
32
+ raise LLM::Error.new { _1.response = res }, "Unexpected response"
33
33
  end
34
34
  end
35
35
  end
@@ -1,9 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module LLM::Ollama::Format
3
+ module LLM::Ollama::RequestAdapter
4
4
  ##
5
5
  # @private
6
- class CompletionFormat
6
+ class Completion
7
7
  ##
8
8
  # @param [LLM::Message] message
9
9
  # The message to format
@@ -12,64 +12,64 @@ module LLM::Ollama::Format
12
12
  end
13
13
 
14
14
  ##
15
- # Formats the message for the Ollama chat completions API
15
+ # Adapts the message for the Ollama chat completions API
16
16
  # @return [Hash]
17
- def format
17
+ def adapt
18
18
  catch(:abort) do
19
19
  if Hash === message
20
- {role: message[:role]}.merge(format_content(message[:content]))
20
+ {role: message[:role]}.merge(adapt_content(message[:content]))
21
21
  else
22
- format_message
22
+ adapt_message
23
23
  end
24
24
  end
25
25
  end
26
26
 
27
27
  private
28
28
 
29
- def format_content(content)
29
+ def adapt_content(content)
30
30
  case content
31
31
  when String
32
32
  {content:}
33
33
  when LLM::Message
34
- format_content(content.content)
34
+ adapt_content(content.content)
35
35
  when LLM::Function::Return
36
- throw(:abort, {role: "tool", tool_call_id: content.id, content: JSON.dump(content.value)})
36
+ throw(:abort, {role: "tool", tool_call_id: content.id, content: LLM.json.dump(content.value)})
37
37
  when LLM::Object
38
- format_object(content)
38
+ adapt_object(content)
39
39
  else
40
40
  prompt_error!(content)
41
41
  end
42
42
  end
43
43
 
44
- def format_message
44
+ def adapt_message
45
45
  case content
46
46
  when Array
47
- format_array
47
+ adapt_array
48
48
  else
49
- {role: message.role}.merge(format_content(content))
49
+ {role: message.role}.merge(adapt_content(content))
50
50
  end
51
51
  end
52
52
 
53
- def format_array
53
+ def adapt_array
54
54
  if content.empty?
55
55
  nil
56
56
  elsif returns.any?
57
- returns.map { {role: "tool", tool_call_id: _1.id, content: JSON.dump(_1.value)} }
57
+ returns.map { {role: "tool", tool_call_id: _1.id, content: LLM.json.dump(_1.value)} }
58
58
  else
59
- content.flat_map { {role: message.role}.merge(format_content(_1)) }
59
+ content.flat_map { {role: message.role}.merge(adapt_content(_1)) }
60
60
  end
61
61
  end
62
62
 
63
- def format_object(object)
63
+ def adapt_object(object)
64
64
  case object.kind
65
- when :local_file then format_local_file(object.value)
65
+ when :local_file then adapt_local_file(object.value)
66
66
  when :remote_file then prompt_error!(object)
67
67
  when :image_url then prompt_error!(object)
68
68
  else prompt_error!(object)
69
69
  end
70
70
  end
71
71
 
72
- def format_local_file(file)
72
+ def adapt_local_file(file)
73
73
  if file.image?
74
74
  {content: "This message has an image associated with it", images: [file.to_b64]}
75
75
  else
@@ -3,16 +3,16 @@
3
3
  class LLM::Ollama
4
4
  ##
5
5
  # @private
6
- module Format
7
- require_relative "format/completion_format"
6
+ module RequestAdapter
7
+ require_relative "request_adapter/completion"
8
8
 
9
9
  ##
10
10
  # @param [Array<LLM::Message>] messages
11
- # The messages to format
11
+ # The messages to adapt
12
12
  # @return [Array<Hash>]
13
- def format(messages)
13
+ def adapt(messages, mode: nil)
14
14
  messages.filter_map do |message|
15
- CompletionFormat.new(message).format
15
+ Completion.new(message).adapt
16
16
  end
17
17
  end
18
18
 
@@ -21,9 +21,9 @@ class LLM::Ollama
21
21
  ##
22
22
  # @param [Hash] params
23
23
  # @return [Hash]
24
- def format_tools(tools)
24
+ def adapt_tools(tools)
25
25
  return {} unless tools&.any?
26
- {tools: tools.map { _1.format(self) }}
26
+ {tools: tools.map { _1.adapt(self) }}
27
27
  end
28
28
  end
29
29
  end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LLM::Ollama::ResponseAdapter
4
+ module Completion
5
+ ##
6
+ # (see LLM::Contract::Completion#messages)
7
+ def messages
8
+ adapt_choices
9
+ end
10
+ alias_method :choices, :messages
11
+
12
+ ##
13
+ # (see LLM::Contract::Completion#input_tokens)
14
+ def input_tokens
15
+ body.prompt_eval_count || 0
16
+ end
17
+
18
+ ##
19
+ # (see LLM::Contract::Completion#output_tokens)
20
+ def output_tokens
21
+ body.eval_count || 0
22
+ end
23
+
24
+ ##
25
+ # (see LLM::Contract::Completion#total_tokens)
26
+ def total_tokens
27
+ input_tokens + output_tokens
28
+ end
29
+
30
+ ##
31
+ # (see LLM::Contract::Completion#usage)
32
+ def usage
33
+ super
34
+ end
35
+
36
+ ##
37
+ # (see LLM::Contract::Completion#model)
38
+ def model
39
+ body.model
40
+ end
41
+
42
+ private
43
+
44
+ def adapt_choices
45
+ message = body.message
46
+ role, content, calls = message.role, message.content, message.tool_calls
47
+ extra = {response: self, tool_calls: adapt_tool_calls(calls)}
48
+ [LLM::Message.new(role, content, extra)]
49
+ end
50
+
51
+ def adapt_tool_calls(tools)
52
+ return [] unless tools
53
+ tools.filter_map do |tool|
54
+ next unless tool["function"]
55
+ tool["function"]
56
+ end
57
+ end
58
+
59
+ include LLM::Contract::Completion
60
+ end
61
+ end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module LLM::Ollama::Response
3
+ module LLM::Ollama::ResponseAdapter
4
4
  module Embedding
5
5
  def embeddings = data.map { _1["embedding"] }
6
6
  def prompt_tokens = body.dig("usage", "prompt_tokens") || 0
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ class LLM::Ollama
4
+ ##
5
+ # @private
6
+ module ResponseAdapter
7
+ require_relative "response_adapter/completion"
8
+ require_relative "response_adapter/embedding"
9
+
10
+ module_function
11
+
12
+ ##
13
+ # @param [LLM::Response, Net::HTTPResponse] res
14
+ # @param [Symbol] type
15
+ # @return [LLM::Response]
16
+ def adapt(res, type:)
17
+ response = (LLM::Response === res) ? res : LLM::Response.new(res)
18
+ response.extend(select(type))
19
+ end
20
+
21
+ ##
22
+ # @api private
23
+ def select(type)
24
+ case type
25
+ when :completion then LLM::Ollama::ResponseAdapter::Completion
26
+ when :embedding then LLM::Ollama::ResponseAdapter::Embedding
27
+ else
28
+ raise ArgumentError, "Unknown response adapter type: #{type.inspect}"
29
+ end
30
+ end
31
+ end
32
+ end
@@ -6,13 +6,13 @@ class LLM::Ollama
6
6
  class StreamParser
7
7
  ##
8
8
  # Returns the fully constructed response body
9
- # @return [LLM::Object]
9
+ # @return [Hash]
10
10
  attr_reader :body
11
11
 
12
12
  ##
13
13
  # @return [LLM::OpenAI::Chunk]
14
14
  def initialize(io)
15
- @body = LLM::Object.new
15
+ @body = {}
16
16
  @io = io
17
17
  end
18
18
 
@@ -16,14 +16,13 @@ module LLM
16
16
  # bot.chat ["Tell me about this image", File.open("/images/parrot.png", "rb")]
17
17
  # bot.messages.select(&:assistant?).each { print "[#{_1.role}]", _1.content, "\n" }
18
18
  class Ollama < Provider
19
- require_relative "ollama/response/embedding"
20
- require_relative "ollama/response/completion"
21
19
  require_relative "ollama/error_handler"
22
- require_relative "ollama/format"
20
+ require_relative "ollama/request_adapter"
21
+ require_relative "ollama/response_adapter"
23
22
  require_relative "ollama/stream_parser"
24
23
  require_relative "ollama/models"
25
24
 
26
- include Format
25
+ include RequestAdapter
27
26
 
28
27
  HOST = "localhost"
29
28
 
@@ -43,9 +42,9 @@ module LLM
43
42
  def embed(input, model: default_model, **params)
44
43
  params = {model:}.merge!(params)
45
44
  req = Net::HTTP::Post.new("/v1/embeddings", headers)
46
- req.body = JSON.dump({input:}.merge!(params))
45
+ req.body = LLM.json.dump({input:}.merge!(params))
47
46
  res = execute(request: req)
48
- LLM::Response.new(res).extend(LLM::Ollama::Response::Embedding)
47
+ ResponseAdapter.adapt(res, type: :embedding)
49
48
  end
50
49
 
51
50
  ##
@@ -61,16 +60,15 @@ module LLM
61
60
  def complete(prompt, params = {})
62
61
  params = {role: :user, model: default_model, stream: true}.merge!(params)
63
62
  tools = resolve_tools(params.delete(:tools))
64
- params = [params, {format: params[:schema]}, format_tools(tools)].inject({}, &:merge!).compact
63
+ params = [params, {format: params[:schema]}, adapt_tools(tools)].inject({}, &:merge!).compact
65
64
  role, stream = params.delete(:role), params.delete(:stream)
66
65
  params[:stream] = true if stream.respond_to?(:<<) || stream == true
67
66
  req = Net::HTTP::Post.new("/api/chat", headers)
68
67
  messages = [*(params.delete(:messages) || []), LLM::Message.new(role, prompt)]
69
- body = JSON.dump({messages: [format(messages)].flatten}.merge!(params))
68
+ body = LLM.json.dump({messages: [adapt(messages)].flatten}.merge!(params))
70
69
  set_body_stream(req, StringIO.new(body))
71
70
  res = execute(request: req, stream:)
72
- LLM::Response.new(res)
73
- .extend(LLM::Ollama::Response::Completion)
71
+ ResponseAdapter.adapt(res, type: :completion)
74
72
  .extend(Module.new { define_method(:__tools__) { tools } })
75
73
  end
76
74
 
@@ -33,7 +33,7 @@ class LLM::OpenAI
33
33
  # @return [LLM::Response]
34
34
  def create_speech(input:, voice: "alloy", model: "gpt-4o-mini-tts", response_format: "mp3", **params)
35
35
  req = Net::HTTP::Post.new("/v1/audio/speech", headers)
36
- req.body = JSON.dump({input:, voice:, model:, response_format:}.merge!(params))
36
+ req.body = LLM.json.dump({input:, voice:, model:, response_format:}.merge!(params))
37
37
  io = StringIO.new("".b)
38
38
  res = execute(request: req) { _1.read_body { |chunk| io << chunk } }
39
39
  LLM::Response.new(res).tap { _1.define_singleton_method(:audio) { io } }
@@ -31,16 +31,26 @@ class LLM::OpenAI
31
31
  else
32
32
  error = body["error"] || {}
33
33
  case error["type"]
34
+ when "invalid_request_error" then handle_invalid_request(error)
34
35
  when "server_error" then raise LLM::ServerError.new { _1.response = res }, error["message"]
35
- else raise LLM::ResponseError.new { _1.response = res }, error["message"] || "Unexpected response"
36
+ else raise LLM::Error.new { _1.response = res }, error["message"] || "Unexpected response"
36
37
  end
37
38
  end
38
39
  end
39
40
 
40
41
  private
41
42
 
43
+ def handle_invalid_request(error)
44
+ case error["code"]
45
+ when "context_length_exceeded"
46
+ raise LLM::ContextWindowError.new { _1.response = res }, error["message"]
47
+ else
48
+ raise LLM::InvalidRequestError.new { _1.response = res }, error["message"]
49
+ end
50
+ end
51
+
42
52
  def body
43
- @body ||= JSON.parse(res.body)
53
+ @body ||= LLM.json.load(res.body)
44
54
  end
45
55
  end
46
56
  end
@@ -18,9 +18,6 @@ class LLM::OpenAI
18
18
  # bot.chat ["Tell me about this PDF", file]
19
19
  # bot.messages.select(&:assistant?).each { print "[#{_1.role}]", _1.content, "\n" }
20
20
  class Files
21
- require_relative "response/enumerable"
22
- require_relative "response/file"
23
-
24
21
  ##
25
22
  # Returns a new Files object
26
23
  # @param provider [LLM::Provider]
@@ -45,7 +42,7 @@ class LLM::OpenAI
45
42
  query = URI.encode_www_form(params)
46
43
  req = Net::HTTP::Get.new("/v1/files?#{query}", headers)
47
44
  res = execute(request: req)
48
- LLM::Response.new(res).extend(LLM::OpenAI::Response::Enumerable)
45
+ ResponseAdapter.adapt(res, type: :enumerable)
49
46
  end
50
47
 
51
48
  ##
@@ -65,7 +62,7 @@ class LLM::OpenAI
65
62
  req["content-type"] = multi.content_type
66
63
  set_body_stream(req, multi.body)
67
64
  res = execute(request: req)
68
- LLM::Response.new(res).extend(LLM::OpenAI::Response::File)
65
+ ResponseAdapter.adapt(res, type: :file)
69
66
  end
70
67
 
71
68
  ##
@@ -84,7 +81,7 @@ class LLM::OpenAI
84
81
  query = URI.encode_www_form(params)
85
82
  req = Net::HTTP::Get.new("/v1/files/#{file_id}?#{query}", headers)
86
83
  res = execute(request: req)
87
- LLM::Response.new(res).extend(LLM::OpenAI::Response::File)
84
+ ResponseAdapter.adapt(res, type: :file)
88
85
  end
89
86
 
90
87
  ##
@@ -27,7 +27,6 @@ class LLM::OpenAI
27
27
  # response_format: "b64_json"
28
28
  # IO.copy_stream res.images[0], "rocket.png"
29
29
  class Images
30
- require_relative "response/image"
31
30
  ##
32
31
  # Returns a new Images object
33
32
  # @param provider [LLM::Provider]
@@ -50,9 +49,9 @@ class LLM::OpenAI
50
49
  # @return [LLM::Response]
51
50
  def create(prompt:, model: "dall-e-3", **params)
52
51
  req = Net::HTTP::Post.new("/v1/images/generations", headers)
53
- req.body = JSON.dump({prompt:, n: 1, model:}.merge!(params))
52
+ req.body = LLM.json.dump({prompt:, n: 1, model:}.merge!(params))
54
53
  res = execute(request: req)
55
- LLM::Response.new(res).extend(LLM::OpenAI::Response::Image)
54
+ ResponseAdapter.adapt(res, type: :image)
56
55
  end
57
56
 
58
57
  ##
@@ -74,7 +73,7 @@ class LLM::OpenAI
74
73
  req["content-type"] = multi.content_type
75
74
  set_body_stream(req, multi.body)
76
75
  res = execute(request: req)
77
- LLM::Response.new(res).extend(LLM::OpenAI::Response::Image)
76
+ ResponseAdapter.adapt(res, type: :image)
78
77
  end
79
78
 
80
79
  ##
@@ -97,7 +96,7 @@ class LLM::OpenAI
97
96
  req["content-type"] = multi.content_type
98
97
  set_body_stream(req, multi.body)
99
98
  res = execute(request: req)
100
- LLM::Response.new(res).extend(LLM::OpenAI::Response::Image)
99
+ ResponseAdapter.adapt(res, type: :image)
101
100
  end
102
101
 
103
102
  private
@@ -17,8 +17,6 @@ class LLM::OpenAI
17
17
  # print "id: ", model.id, "\n"
18
18
  # end
19
19
  class Models
20
- require_relative "response/enumerable"
21
-
22
20
  ##
23
21
  # Returns a new Models object
24
22
  # @param provider [LLM::Provider]
@@ -43,7 +41,7 @@ class LLM::OpenAI
43
41
  query = URI.encode_www_form(params)
44
42
  req = Net::HTTP::Get.new("/v1/models?#{query}", headers)
45
43
  res = execute(request: req)
46
- LLM::Response.new(res).extend(LLM::OpenAI::Response::Enumerable)
44
+ ResponseAdapter.adapt(res, type: :enumerable)
47
45
  end
48
46
 
49
47
  private
@@ -31,8 +31,6 @@ class LLM::OpenAI
31
31
  # @see https://platform.openai.com/docs/api-reference/moderations/create OpenAI docs
32
32
  # @see https://platform.openai.com/docs/models#moderation OpenAI moderation models
33
33
  class Moderations
34
- require_relative "response/moderations"
35
-
36
34
  ##
37
35
  # Returns a new Moderations object
38
36
  # @param [LLM::Provider] provider
@@ -50,10 +48,10 @@ class LLM::OpenAI
50
48
  # @return [LLM::Response]
51
49
  def create(input:, model: "omni-moderation-latest", **params)
52
50
  req = Net::HTTP::Post.new("/v1/moderations", headers)
53
- input = Format::ModerationFormat.new(input).format
54
- req.body = JSON.dump({input:, model:}.merge!(params))
51
+ input = RequestAdapter::Moderation.new(input).adapt
52
+ req.body = LLM.json.dump({input:, model:}.merge!(params))
55
53
  res = execute(request: req)
56
- LLM::Response.new(res).extend(LLM::OpenAI::Response::Moderations)
54
+ ResponseAdapter.adapt(res, type: :moderations)
57
55
  end
58
56
 
59
57
  private