llm.rb 0.8.0 → 0.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 (62) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +62 -48
  3. data/lib/llm/{chat → bot}/builder.rb +1 -1
  4. data/lib/llm/bot/conversable.rb +31 -0
  5. data/lib/llm/{chat → bot}/prompt/completion.rb +14 -4
  6. data/lib/llm/{chat → bot}/prompt/respond.rb +16 -5
  7. data/lib/llm/{chat.rb → bot.rb} +48 -66
  8. data/lib/llm/error.rb +22 -22
  9. data/lib/llm/event_handler.rb +44 -0
  10. data/lib/llm/eventstream/event.rb +69 -0
  11. data/lib/llm/eventstream/parser.rb +88 -0
  12. data/lib/llm/eventstream.rb +8 -0
  13. data/lib/llm/function.rb +9 -12
  14. data/lib/llm/object/builder.rb +8 -9
  15. data/lib/llm/object/kernel.rb +1 -1
  16. data/lib/llm/object.rb +7 -1
  17. data/lib/llm/provider.rb +61 -26
  18. data/lib/llm/providers/anthropic/error_handler.rb +3 -3
  19. data/lib/llm/providers/anthropic/models.rb +3 -7
  20. data/lib/llm/providers/anthropic/response_parser/completion_parser.rb +3 -3
  21. data/lib/llm/providers/anthropic/response_parser.rb +1 -0
  22. data/lib/llm/providers/anthropic/stream_parser.rb +66 -0
  23. data/lib/llm/providers/anthropic.rb +9 -4
  24. data/lib/llm/providers/gemini/error_handler.rb +4 -4
  25. data/lib/llm/providers/gemini/files.rb +12 -15
  26. data/lib/llm/providers/gemini/images.rb +4 -8
  27. data/lib/llm/providers/gemini/models.rb +3 -7
  28. data/lib/llm/providers/gemini/stream_parser.rb +69 -0
  29. data/lib/llm/providers/gemini.rb +19 -11
  30. data/lib/llm/providers/ollama/error_handler.rb +3 -3
  31. data/lib/llm/providers/ollama/format/completion_format.rb +1 -1
  32. data/lib/llm/providers/ollama/models.rb +3 -7
  33. data/lib/llm/providers/ollama/stream_parser.rb +44 -0
  34. data/lib/llm/providers/ollama.rb +13 -6
  35. data/lib/llm/providers/openai/audio.rb +5 -9
  36. data/lib/llm/providers/openai/error_handler.rb +3 -3
  37. data/lib/llm/providers/openai/files.rb +12 -15
  38. data/lib/llm/providers/openai/images.rb +8 -11
  39. data/lib/llm/providers/openai/models.rb +3 -7
  40. data/lib/llm/providers/openai/moderations.rb +3 -7
  41. data/lib/llm/providers/openai/response_parser/completion_parser.rb +3 -3
  42. data/lib/llm/providers/openai/response_parser.rb +3 -0
  43. data/lib/llm/providers/openai/responses.rb +10 -12
  44. data/lib/llm/providers/openai/stream_parser.rb +77 -0
  45. data/lib/llm/providers/openai.rb +11 -7
  46. data/lib/llm/providers/voyageai/error_handler.rb +3 -3
  47. data/lib/llm/providers/voyageai.rb +1 -1
  48. data/lib/llm/version.rb +1 -1
  49. data/lib/llm.rb +4 -2
  50. data/llm.gemspec +1 -1
  51. metadata +30 -25
  52. data/lib/llm/chat/conversable.rb +0 -53
  53. /data/lib/{json → llm/json}/schema/array.rb +0 -0
  54. /data/lib/{json → llm/json}/schema/boolean.rb +0 -0
  55. /data/lib/{json → llm/json}/schema/integer.rb +0 -0
  56. /data/lib/{json → llm/json}/schema/leaf.rb +0 -0
  57. /data/lib/{json → llm/json}/schema/null.rb +0 -0
  58. /data/lib/{json → llm/json}/schema/number.rb +0 -0
  59. /data/lib/{json → llm/json}/schema/object.rb +0 -0
  60. /data/lib/{json → llm/json}/schema/string.rb +0 -0
  61. /data/lib/{json → llm/json}/schema/version.rb +0 -0
  62. /data/lib/{json → llm/json}/schema.rb +0 -0
@@ -13,23 +13,24 @@ class LLM::Gemini
13
13
  # in the prompt over and over again (which could be the case in a
14
14
  # multi-turn conversation).
15
15
  #
16
- # @example
16
+ # @example example #1
17
17
  # #!/usr/bin/env ruby
18
18
  # require "llm"
19
19
  #
20
20
  # llm = LLM.gemini(ENV["KEY"])
21
- # bot = LLM::Chat.new(llm).lazy
21
+ # bot = LLM::Bot.new(llm)
22
22
  # file = llm.files.create file: "/audio/haiku.mp3"
23
23
  # bot.chat(file)
24
24
  # bot.chat("Describe the audio file I sent to you")
25
25
  # bot.chat("The audio file is the first message I sent to you.")
26
26
  # bot.messages.select(&:assistant?).each { print "[#{_1.role}]", _1.content, "\n" }
27
- # @example
27
+ #
28
+ # @example example #2
28
29
  # #!/usr/bin/env ruby
29
30
  # require "llm"
30
31
  #
31
32
  # llm = LLM.gemini(ENV["KEY"])
32
- # bot = LLM::Chat.new(llm).lazy
33
+ # bot = LLM::Bot.new(llm)
33
34
  # file = llm.files.create file: "/audio/haiku.mp3"
34
35
  # bot.chat(["Describe the audio file I sent to you", file])
35
36
  # bot.messages.select(&:assistant?).each { print "[#{_1.role}]", _1.content, "\n" }
@@ -57,7 +58,7 @@ class LLM::Gemini
57
58
  def all(**params)
58
59
  query = URI.encode_www_form(params.merge!(key: key))
59
60
  req = Net::HTTP::Get.new("/v1beta/files?#{query}", headers)
60
- res = request(http, req)
61
+ res = execute(request: req)
61
62
  LLM::Response::FileList.new(res).tap { |filelist|
62
63
  files = filelist.body["files"]&.map do |file|
63
64
  file = file.transform_keys { snakecase(_1) }
@@ -85,7 +86,7 @@ class LLM::Gemini
85
86
  req["X-Goog-Upload-Command"] = "upload, finalize"
86
87
  file.with_io do |io|
87
88
  set_body_stream(req, io)
88
- res = request(http, req)
89
+ res = execute(request: req)
89
90
  LLM::Response::File.new(res)
90
91
  end
91
92
  end
@@ -105,7 +106,7 @@ class LLM::Gemini
105
106
  file_id = file.respond_to?(:name) ? file.name : file.to_s
106
107
  query = URI.encode_www_form(params.merge!(key: key))
107
108
  req = Net::HTTP::Get.new("/v1beta/#{file_id}?#{query}", headers)
108
- res = request(http, req)
109
+ res = execute(request: req)
109
110
  LLM::Response::File.new(res)
110
111
  end
111
112
 
@@ -123,7 +124,7 @@ class LLM::Gemini
123
124
  file_id = file.respond_to?(:name) ? file.name : file.to_s
124
125
  query = URI.encode_www_form(params.merge!(key: key))
125
126
  req = Net::HTTP::Delete.new("/v1beta/#{file_id}?#{query}", headers)
126
- request(http, req)
127
+ execute(request: req)
127
128
  end
128
129
 
129
130
  ##
@@ -144,20 +145,16 @@ class LLM::Gemini
144
145
  req["X-Goog-Upload-Header-Content-Length"] = file.bytesize
145
146
  req["X-Goog-Upload-Header-Content-Type"] = file.mime_type
146
147
  req.body = JSON.dump(file: {display_name: File.basename(file.path)})
147
- res = request(http, req)
148
+ res = execute(request: req)
148
149
  res["x-goog-upload-url"]
149
150
  end
150
151
 
151
- def http
152
- @provider.instance_variable_get(:@http)
153
- end
154
-
155
152
  def key
156
153
  @provider.instance_variable_get(:@key)
157
154
  end
158
155
 
159
- [:headers, :request, :set_body_stream].each do |m|
160
- define_method(m) { |*args, &b| @provider.send(m, *args, &b) }
156
+ [:headers, :execute, :set_body_stream].each do |m|
157
+ define_method(m) { |*args, **kwargs, &b| @provider.send(m, *args, **kwargs, &b) }
161
158
  end
162
159
  end
163
160
  end
@@ -47,7 +47,7 @@ class LLM::Gemini
47
47
  generationConfig: {responseModalities: ["TEXT", "IMAGE"]}
48
48
  }.merge!(params))
49
49
  req.body = body
50
- res = request(http, req)
50
+ res = execute(request: req)
51
51
  LLM::Response::Image.new(res).extend(response_parser)
52
52
  end
53
53
 
@@ -72,7 +72,7 @@ class LLM::Gemini
72
72
  generationConfig: {responseModalities: ["TEXT", "IMAGE"]}
73
73
  }.merge!(params)).b
74
74
  set_body_stream(req, StringIO.new(body))
75
- res = request(http, req)
75
+ res = execute(request: req)
76
76
  LLM::Response::Image.new(res).extend(response_parser)
77
77
  end
78
78
 
@@ -93,12 +93,8 @@ class LLM::Gemini
93
93
  @provider.instance_variable_get(:@key)
94
94
  end
95
95
 
96
- def http
97
- @provider.instance_variable_get(:@http)
98
- end
99
-
100
- [:response_parser, :headers, :request, :set_body_stream].each do |m|
101
- define_method(m) { |*args, &b| @provider.send(m, *args, &b) }
96
+ [:response_parser, :headers, :execute, :set_body_stream].each do |m|
97
+ define_method(m) { |*args, **kwargs, &b| @provider.send(m, *args, **kwargs, &b) }
102
98
  end
103
99
  end
104
100
  end
@@ -42,7 +42,7 @@ class LLM::Gemini
42
42
  def all(**params)
43
43
  query = URI.encode_www_form(params.merge!(key: key))
44
44
  req = Net::HTTP::Get.new("/v1beta/models?#{query}", headers)
45
- res = request(http, req)
45
+ res = execute(request: req)
46
46
  LLM::Response::ModelList.new(res).tap { |modellist|
47
47
  models = modellist.body["models"].map do |model|
48
48
  model = model.transform_keys { snakecase(_1) }
@@ -54,16 +54,12 @@ class LLM::Gemini
54
54
 
55
55
  private
56
56
 
57
- def http
58
- @provider.instance_variable_get(:@http)
59
- end
60
-
61
57
  def key
62
58
  @provider.instance_variable_get(:@key)
63
59
  end
64
60
 
65
- [:headers, :request].each do |m|
66
- define_method(m) { |*args, &b| @provider.send(m, *args, &b) }
61
+ [:headers, :execute].each do |m|
62
+ define_method(m) { |*args, **kwargs, &b| @provider.send(m, *args, **kwargs, &b) }
67
63
  end
68
64
  end
69
65
  end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ class LLM::Gemini
4
+ ##
5
+ # @private
6
+ class StreamParser
7
+ ##
8
+ # Returns the fully constructed response body
9
+ # @return [LLM::Object]
10
+ attr_reader :body
11
+
12
+ ##
13
+ # @param [#<<] io An IO-like object
14
+ # @return [LLM::Gemini::StreamParser]
15
+ def initialize(io)
16
+ @body = LLM::Object.new
17
+ @io = io
18
+ end
19
+
20
+ ##
21
+ # @param [Hash] chunk
22
+ # @return [LLM::Gemini::StreamParser]
23
+ def parse!(chunk)
24
+ tap { merge!(chunk) }
25
+ end
26
+
27
+ private
28
+
29
+ def merge!(chunk)
30
+ chunk.each do |key, value|
31
+ if key == "candidates"
32
+ @body.candidates ||= []
33
+ merge_candidates!(value)
34
+ else
35
+ @body[key] = value
36
+ end
37
+ end
38
+ end
39
+
40
+ def merge_candidates!(candidates)
41
+ candidates.each.with_index do |candidate, i|
42
+ if @body.candidates[i].nil?
43
+ merge_one(@body.candidates, candidate, i)
44
+ else
45
+ merge_two(@body.candidates, candidate, i)
46
+ end
47
+ end
48
+ end
49
+
50
+ def merge_one(candidates, candidate, i)
51
+ candidate
52
+ .dig("content", "parts")
53
+ &.filter_map { _1["text"] }
54
+ &.each { @io << _1 if @io.respond_to?(:<<) }
55
+ candidates[i] = candidate
56
+ end
57
+
58
+ def merge_two(candidates, candidate, i)
59
+ parts = candidates[i].dig("content", "parts")
60
+ parts&.each&.with_index do |part, j|
61
+ if part["text"]
62
+ target = candidate["content"]["parts"][j]
63
+ part["text"] << target["text"]
64
+ @io << target["text"] if @io.respond_to?(:<<)
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
@@ -10,31 +10,34 @@ module LLM
10
10
  # prompt for files under 20MB or via the Gemini Files API for
11
11
  # files that are over 20MB
12
12
  #
13
- # @example
13
+ # @example example #1
14
14
  # #!/usr/bin/env ruby
15
15
  # require "llm"
16
16
  #
17
17
  # llm = LLM.gemini(ENV["KEY"])
18
- # bot = LLM::Chat.new(llm).lazy
19
- # bot.chat LLM::File("/images/capybara.png")
18
+ # bot = LLM::Bot.new(llm)
19
+ # bot.chat LLM.File("/images/capybara.png")
20
20
  # bot.chat "Describe the image"
21
21
  # bot.messages.select(&:assistant?).each { print "[#{_1.role}]", _1.content, "\n" }
22
- # @example
22
+ #
23
+ # @example example #2
23
24
  # #!/usr/bin/env ruby
24
25
  # require "llm"
25
26
  #
26
27
  # llm = LLM.gemini(ENV["KEY"])
27
- # bot = LLM::Chat.new(llm).lazy
28
+ # bot = LLM::Bot.new(llm)
28
29
  # bot.chat ["Describe the image", LLM::File("/images/capybara.png")]
29
30
  # bot.messages.select(&:assistant?).each { print "[#{_1.role}]", _1.content, "\n" }
30
31
  class Gemini < Provider
31
32
  require_relative "gemini/error_handler"
32
- require_relative "gemini/response_parser"
33
33
  require_relative "gemini/format"
34
+ require_relative "gemini/stream_parser"
35
+ require_relative "gemini/response_parser"
36
+ require_relative "gemini/models"
34
37
  require_relative "gemini/images"
35
38
  require_relative "gemini/files"
36
39
  require_relative "gemini/audio"
37
- require_relative "gemini/models"
40
+
38
41
  include Format
39
42
 
40
43
  HOST = "generativelanguage.googleapis.com"
@@ -57,7 +60,7 @@ module LLM
57
60
  path = ["/v1beta/models/#{model}", "embedContent?key=#{@key}"].join(":")
58
61
  req = Net::HTTP::Post.new(path, headers)
59
62
  req.body = JSON.dump({content: {parts: [{text: input}]}})
60
- res = request(@http, req)
63
+ res = execute(request: req)
61
64
  Response::Embedding.new(res).extend(response_parser)
62
65
  end
63
66
 
@@ -74,14 +77,15 @@ module LLM
74
77
  def complete(prompt, params = {})
75
78
  params = {role: :user, model: default_model}.merge!(params)
76
79
  params = [params, format_schema(params), format_tools(params)].inject({}, &:merge!).compact
77
- role, model = [:role, :model].map { params.delete(_1) }
80
+ role, model, stream = [:role, :model, :stream].map { params.delete(_1) }
81
+ action = stream ? "streamGenerateContent?key=#{@key}&alt=sse" : "generateContent?key=#{@key}"
78
82
  model.respond_to?(:id) ? model.id : model
79
- path = ["/v1beta/models/#{model}", "generateContent?key=#{@key}"].join(":")
83
+ path = ["/v1beta/models/#{model}", action].join(":")
80
84
  req = Net::HTTP::Post.new(path, headers)
81
85
  messages = [*(params.delete(:messages) || []), LLM::Message.new(role, prompt)]
82
86
  body = JSON.dump({contents: format(messages)}.merge!(params))
83
87
  set_body_stream(req, StringIO.new(body))
84
- res = request(@http, req)
88
+ res = execute(request: req, stream:)
85
89
  Response::Completion.new(res).extend(response_parser)
86
90
  end
87
91
 
@@ -140,6 +144,10 @@ module LLM
140
144
  LLM::Gemini::ResponseParser
141
145
  end
142
146
 
147
+ def stream_parser
148
+ LLM::Gemini::StreamParser
149
+ end
150
+
143
151
  def error_handler
144
152
  LLM::Gemini::ErrorHandler
145
153
  end
@@ -23,11 +23,11 @@ class LLM::Ollama
23
23
  def raise_error!
24
24
  case res
25
25
  when Net::HTTPUnauthorized
26
- raise LLM::Error::Unauthorized.new { _1.response = res }, "Authentication error"
26
+ raise LLM::UnauthorizedError.new { _1.response = res }, "Authentication error"
27
27
  when Net::HTTPTooManyRequests
28
- raise LLM::Error::RateLimit.new { _1.response = res }, "Too many requests"
28
+ raise LLM::RateLimitError.new { _1.response = res }, "Too many requests"
29
29
  else
30
- raise LLM::Error::ResponseError.new { _1.response = res }, "Unexpected response"
30
+ raise LLM::ResponseError.new { _1.response = res }, "Unexpected response"
31
31
  end
32
32
  end
33
33
  end
@@ -63,7 +63,7 @@ module LLM::Ollama::Format
63
63
  elsif returns.any?
64
64
  returns.map { {role: "tool", tool_call_id: _1.id, content: JSON.dump(_1.value)} }
65
65
  else
66
- content.flat_map { {role: message.role }.merge(format_content(_1)) }
66
+ content.flat_map { {role: message.role}.merge(format_content(_1)) }
67
67
  end
68
68
  end
69
69
 
@@ -43,7 +43,7 @@ class LLM::Ollama
43
43
  def all(**params)
44
44
  query = URI.encode_www_form(params)
45
45
  req = Net::HTTP::Get.new("/api/tags?#{query}", headers)
46
- res = request(http, req)
46
+ res = execute(request: req)
47
47
  LLM::Response::ModelList.new(res).tap { |modellist|
48
48
  models = modellist.body["models"].map do |model|
49
49
  model = model.transform_keys { snakecase(_1) }
@@ -55,12 +55,8 @@ class LLM::Ollama
55
55
 
56
56
  private
57
57
 
58
- def http
59
- @provider.instance_variable_get(:@http)
60
- end
61
-
62
- [:headers, :request].each do |m|
63
- define_method(m) { |*args, &b| @provider.send(m, *args, &b) }
58
+ [:headers, :execute].each do |m|
59
+ define_method(m) { |*args, **kwargs, &b| @provider.send(m, *args, **kwargs, &b) }
64
60
  end
65
61
  end
66
62
  end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ class LLM::Ollama
4
+ ##
5
+ # @private
6
+ class StreamParser
7
+ ##
8
+ # Returns the fully constructed response body
9
+ # @return [LLM::Object]
10
+ attr_reader :body
11
+
12
+ ##
13
+ # @return [LLM::OpenAI::Chunk]
14
+ def initialize(io)
15
+ @body = LLM::Object.new
16
+ @io = io
17
+ end
18
+
19
+ ##
20
+ # @param [Hash] chunk
21
+ # @return [LLM::OpenAI::Chunk]
22
+ def parse!(chunk)
23
+ tap { merge!(chunk) }
24
+ end
25
+
26
+ private
27
+
28
+ def merge!(chunk)
29
+ chunk.each do |key, value|
30
+ if key == "message"
31
+ if @body[key]
32
+ @body[key]["content"] << value["content"]
33
+ @io << value["content"] if @io.respond_to?(:<<)
34
+ else
35
+ @body[key] = value
36
+ @io << value["content"] if @io.respond_to?(:<<)
37
+ end
38
+ else
39
+ @body[key] = value
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -14,15 +14,17 @@ module LLM
14
14
  # require "llm"
15
15
  #
16
16
  # llm = LLM.ollama(nil)
17
- # bot = LLM::Chat.new(llm, model: "llava").lazy
17
+ # bot = LLM::Bot.new(llm, model: "llava")
18
18
  # bot.chat LLM::File("/images/capybara.png")
19
19
  # bot.chat "Describe the image"
20
20
  # bot.messages.select(&:assistant?).each { print "[#{_1.role}]", _1.content, "\n" }
21
21
  class Ollama < Provider
22
22
  require_relative "ollama/error_handler"
23
- require_relative "ollama/response_parser"
24
23
  require_relative "ollama/format"
24
+ require_relative "ollama/stream_parser"
25
+ require_relative "ollama/response_parser"
25
26
  require_relative "ollama/models"
27
+
26
28
  include Format
27
29
 
28
30
  HOST = "localhost"
@@ -44,7 +46,7 @@ module LLM
44
46
  params = {model:}.merge!(params)
45
47
  req = Net::HTTP::Post.new("/v1/embeddings", headers)
46
48
  req.body = JSON.dump({input:}.merge!(params))
47
- res = request(@http, req)
49
+ res = execute(request: req)
48
50
  Response::Embedding.new(res).extend(response_parser)
49
51
  end
50
52
 
@@ -59,14 +61,15 @@ module LLM
59
61
  # When given an object a provider does not understand
60
62
  # @return (see LLM::Provider#complete)
61
63
  def complete(prompt, params = {})
62
- params = {role: :user, model: default_model, stream: false}.merge!(params)
64
+ params = {role: :user, model: default_model, stream: true}.merge!(params)
63
65
  params = [params, {format: params[:schema]}, format_tools(params)].inject({}, &:merge!).compact
64
- role = params.delete(:role)
66
+ role, stream = params.delete(:role), params.delete(:stream)
67
+ params[:stream] = true if stream.respond_to?(:<<) || stream == true
65
68
  req = Net::HTTP::Post.new("/api/chat", headers)
66
69
  messages = [*(params.delete(:messages) || []), LLM::Message.new(role, prompt)]
67
70
  body = JSON.dump({messages: [format(messages)].flatten}.merge!(params))
68
71
  set_body_stream(req, StringIO.new(body))
69
- res = request(@http, req)
72
+ res = execute(request: req, stream:)
70
73
  Response::Completion.new(res).extend(response_parser)
71
74
  end
72
75
 
@@ -105,6 +108,10 @@ module LLM
105
108
  LLM::Ollama::ResponseParser
106
109
  end
107
110
 
111
+ def stream_parser
112
+ LLM::Ollama::StreamParser
113
+ end
114
+
108
115
  def error_handler
109
116
  LLM::Ollama::ErrorHandler
110
117
  end
@@ -35,7 +35,7 @@ class LLM::OpenAI
35
35
  req = Net::HTTP::Post.new("/v1/audio/speech", headers)
36
36
  req.body = JSON.dump({input:, voice:, model:, response_format:}.merge!(params))
37
37
  io = StringIO.new("".b)
38
- res = request(http, req) { _1.read_body { |chunk| io << chunk } }
38
+ res = execute(request: req) { _1.read_body { |chunk| io << chunk } }
39
39
  LLM::Response::Audio.new(res).tap { _1.audio = io }
40
40
  end
41
41
 
@@ -56,7 +56,7 @@ class LLM::OpenAI
56
56
  req = Net::HTTP::Post.new("/v1/audio/transcriptions", headers)
57
57
  req["content-type"] = multi.content_type
58
58
  set_body_stream(req, multi.body)
59
- res = request(http, req)
59
+ res = execute(request: req)
60
60
  LLM::Response::AudioTranscription.new(res).tap { _1.text = _1.body["text"] }
61
61
  end
62
62
 
@@ -78,18 +78,14 @@ class LLM::OpenAI
78
78
  req = Net::HTTP::Post.new("/v1/audio/translations", headers)
79
79
  req["content-type"] = multi.content_type
80
80
  set_body_stream(req, multi.body)
81
- res = request(http, req)
81
+ res = execute(request: req)
82
82
  LLM::Response::AudioTranslation.new(res).tap { _1.text = _1.body["text"] }
83
83
  end
84
84
 
85
85
  private
86
86
 
87
- def http
88
- @provider.instance_variable_get(:@http)
89
- end
90
-
91
- [:headers, :request, :set_body_stream].each do |m|
92
- define_method(m) { |*args, &b| @provider.send(m, *args, &b) }
87
+ [:headers, :execute, :set_body_stream].each do |m|
88
+ define_method(m) { |*args, **kwargs, &b| @provider.send(m, *args, **kwargs, &b) }
93
89
  end
94
90
  end
95
91
  end
@@ -23,11 +23,11 @@ class LLM::OpenAI
23
23
  def raise_error!
24
24
  case res
25
25
  when Net::HTTPUnauthorized
26
- raise LLM::Error::Unauthorized.new { _1.response = res }, "Authentication error"
26
+ raise LLM::UnauthorizedError.new { _1.response = res }, "Authentication error"
27
27
  when Net::HTTPTooManyRequests
28
- raise LLM::Error::RateLimit.new { _1.response = res }, "Too many requests"
28
+ raise LLM::RateLimitError.new { _1.response = res }, "Too many requests"
29
29
  else
30
- raise LLM::Error::ResponseError.new { _1.response = res }, "Unexpected response"
30
+ raise LLM::ResponseError.new { _1.response = res }, "Unexpected response"
31
31
  end
32
32
  end
33
33
  end
@@ -8,22 +8,23 @@ class LLM::OpenAI
8
8
  # and API endpoints. OpenAI supports multiple file formats, including text
9
9
  # files, CSV files, JSON files, and more.
10
10
  #
11
- # @example
11
+ # @example example #1
12
12
  # #!/usr/bin/env ruby
13
13
  # require "llm"
14
14
  #
15
15
  # llm = LLM.openai(ENV["KEY"])
16
- # bot = LLM::Chat.new(llm).lazy
16
+ # bot = LLM::Bot.new(llm)
17
17
  # file = llm.files.create file: "/documents/freebsd.pdf"
18
18
  # bot.chat(file)
19
19
  # bot.chat("Describe the document")
20
20
  # bot.messages.select(&:assistant?).each { print "[#{_1.role}]", _1.content, "\n" }
21
- # @example
21
+ #
22
+ # @example example #2
22
23
  # #!/usr/bin/env ruby
23
24
  # require "llm"
24
25
  #
25
26
  # llm = LLM.openai(ENV["KEY"])
26
- # bot = LLM::Chat.new(llm).lazy
27
+ # bot = LLM::Bot.new(llm)
27
28
  # file = llm.files.create file: "/documents/openbsd.pdf"
28
29
  # bot.chat(["Describe the document I sent to you", file])
29
30
  # bot.messages.select(&:assistant?).each { print "[#{_1.role}]", _1.content, "\n" }
@@ -51,7 +52,7 @@ class LLM::OpenAI
51
52
  def all(**params)
52
53
  query = URI.encode_www_form(params)
53
54
  req = Net::HTTP::Get.new("/v1/files?#{query}", headers)
54
- res = request(http, req)
55
+ res = execute(request: req)
55
56
  LLM::Response::FileList.new(res).tap { |filelist|
56
57
  files = filelist.body["data"].map { LLM::Object.from_hash(_1) }
57
58
  filelist.files = files
@@ -74,7 +75,7 @@ class LLM::OpenAI
74
75
  req = Net::HTTP::Post.new("/v1/files", headers)
75
76
  req["content-type"] = multi.content_type
76
77
  set_body_stream(req, multi.body)
77
- res = request(http, req)
78
+ res = execute(request: req)
78
79
  LLM::Response::File.new(res)
79
80
  end
80
81
 
@@ -93,7 +94,7 @@ class LLM::OpenAI
93
94
  file_id = file.respond_to?(:id) ? file.id : file
94
95
  query = URI.encode_www_form(params)
95
96
  req = Net::HTTP::Get.new("/v1/files/#{file_id}?#{query}", headers)
96
- res = request(http, req)
97
+ res = execute(request: req)
97
98
  LLM::Response::File.new(res)
98
99
  end
99
100
 
@@ -114,7 +115,7 @@ class LLM::OpenAI
114
115
  file_id = file.respond_to?(:id) ? file.id : file
115
116
  req = Net::HTTP::Get.new("/v1/files/#{file_id}/content?#{query}", headers)
116
117
  io = StringIO.new("".b)
117
- res = request(http, req) { |res| res.read_body { |chunk| io << chunk } }
118
+ res = execute(request: req) { |res| res.read_body { |chunk| io << chunk } }
118
119
  LLM::Response::DownloadFile.new(res).tap { _1.file = io }
119
120
  end
120
121
 
@@ -131,18 +132,14 @@ class LLM::OpenAI
131
132
  def delete(file:)
132
133
  file_id = file.respond_to?(:id) ? file.id : file
133
134
  req = Net::HTTP::Delete.new("/v1/files/#{file_id}", headers)
134
- res = request(http, req)
135
+ res = execute(request: req)
135
136
  LLM::Object.from_hash JSON.parse(res.body)
136
137
  end
137
138
 
138
139
  private
139
140
 
140
- def http
141
- @provider.instance_variable_get(:@http)
142
- end
143
-
144
- [:headers, :request, :set_body_stream].each do |m|
145
- define_method(m) { |*args, &b| @provider.send(m, *args, &b) }
141
+ [:headers, :execute, :set_body_stream].each do |m|
142
+ define_method(m) { |*args, **kwargs, &b| @provider.send(m, *args, **kwargs, &b) }
146
143
  end
147
144
  end
148
145
  end
@@ -7,7 +7,7 @@ class LLM::OpenAI
7
7
  # OpenAI supports multiple response formats: temporary URLs, or binary strings
8
8
  # encoded in base64. The default is to return temporary URLs.
9
9
  #
10
- # @example
10
+ # @example example #1
11
11
  # #!/usr/bin/env ruby
12
12
  # require "llm"
13
13
  # require "open-uri"
@@ -17,7 +17,8 @@ class LLM::OpenAI
17
17
  # res = llm.images.create prompt: "A dog on a rocket to the moon"
18
18
  # FileUtils.mv OpenURI.open_uri(res.urls[0]).path,
19
19
  # "rocket.png"
20
- # @example
20
+ #
21
+ # @example example #2
21
22
  # #!/usr/bin/env ruby
22
23
  # require "llm"
23
24
  #
@@ -49,7 +50,7 @@ class LLM::OpenAI
49
50
  def create(prompt:, model: "dall-e-3", **params)
50
51
  req = Net::HTTP::Post.new("/v1/images/generations", headers)
51
52
  req.body = JSON.dump({prompt:, n: 1, model:}.merge!(params))
52
- res = request(http, req)
53
+ res = execute(request: req)
53
54
  LLM::Response::Image.new(res).extend(response_parser)
54
55
  end
55
56
 
@@ -71,7 +72,7 @@ class LLM::OpenAI
71
72
  req = Net::HTTP::Post.new("/v1/images/variations", headers)
72
73
  req["content-type"] = multi.content_type
73
74
  set_body_stream(req, multi.body)
74
- res = request(http, req)
75
+ res = execute(request: req)
75
76
  LLM::Response::Image.new(res).extend(response_parser)
76
77
  end
77
78
 
@@ -94,18 +95,14 @@ class LLM::OpenAI
94
95
  req = Net::HTTP::Post.new("/v1/images/edits", headers)
95
96
  req["content-type"] = multi.content_type
96
97
  set_body_stream(req, multi.body)
97
- res = request(http, req)
98
+ res = execute(request: req)
98
99
  LLM::Response::Image.new(res).extend(response_parser)
99
100
  end
100
101
 
101
102
  private
102
103
 
103
- def http
104
- @provider.instance_variable_get(:@http)
105
- end
106
-
107
- [:response_parser, :headers, :request, :set_body_stream].each do |m|
108
- define_method(m) { |*args, &b| @provider.send(m, *args, &b) }
104
+ [:response_parser, :headers, :execute, :set_body_stream].each do |m|
105
+ define_method(m) { |*args, **kwargs, &b| @provider.send(m, *args, **kwargs, &b) }
109
106
  end
110
107
  end
111
108
  end
@@ -40,7 +40,7 @@ class LLM::OpenAI
40
40
  def all(**params)
41
41
  query = URI.encode_www_form(params)
42
42
  req = Net::HTTP::Get.new("/v1/models?#{query}", headers)
43
- res = request(http, req)
43
+ res = execute(request: req)
44
44
  LLM::Response::ModelList.new(res).tap { |modellist|
45
45
  models = modellist.body["data"].map do |model|
46
46
  LLM::Model.from_hash(model).tap { _1.provider = @provider }
@@ -51,12 +51,8 @@ class LLM::OpenAI
51
51
 
52
52
  private
53
53
 
54
- def http
55
- @provider.instance_variable_get(:@http)
56
- end
57
-
58
- [:headers, :request, :set_body_stream].each do |m|
59
- define_method(m) { |*args, &b| @provider.send(m, *args, &b) }
54
+ [:headers, :execute, :set_body_stream].each do |m|
55
+ define_method(m) { |*args, **kwargs, &b| @provider.send(m, *args, **kwargs, &b) }
60
56
  end
61
57
  end
62
58
  end