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
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LLM::EventStream
4
+ ##
5
+ # @private
6
+ class Event
7
+ FIELD_REGEXP = /[^:]+/
8
+ VALUE_REGEXP = /(?<=: ).+/
9
+
10
+ ##
11
+ # Returns the field name
12
+ # @return [Symbol]
13
+ attr_reader :field
14
+
15
+ ##
16
+ # Returns the field value
17
+ # @return [String]
18
+ attr_reader :value
19
+
20
+ ##
21
+ # Returns the full chunk
22
+ # @return [String]
23
+ attr_reader :chunk
24
+
25
+ ##
26
+ # @param [String] chunk
27
+ # @return [LLM::EventStream::Event]
28
+ def initialize(chunk)
29
+ @field = chunk[FIELD_REGEXP]
30
+ @value = chunk[VALUE_REGEXP]
31
+ @chunk = chunk
32
+ end
33
+
34
+ ##
35
+ # Returns true when the event represents an "id" chunk
36
+ # @return [Boolean]
37
+ def id?
38
+ @field == "id"
39
+ end
40
+
41
+ ##
42
+ # Returns true when the event represents a "data" chunk
43
+ # @return [Boolean]
44
+ def data?
45
+ @field == "data"
46
+ end
47
+
48
+ ##
49
+ # Returns true when the event represents an "event" chunk
50
+ # @return [Boolean]
51
+ def event?
52
+ @field == "event"
53
+ end
54
+
55
+ ##
56
+ # Returns true when the event represents a "retry" chunk
57
+ # @return [Boolean]
58
+ def retry?
59
+ @field == "retry"
60
+ end
61
+
62
+ ##
63
+ # Returns true when a chunk represents the end of the stream
64
+ # @return [Boolean]
65
+ def end?
66
+ @value == "[DONE]"
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LLM::EventStream
4
+ ##
5
+ # @private
6
+ class Parser
7
+ ##
8
+ # @return [LLM::EventStream::Parser]
9
+ def initialize
10
+ @buffer = StringIO.new
11
+ @events = Hash.new { |h, k| h[k] = [] }
12
+ @offset = 0
13
+ @visitors = []
14
+ end
15
+
16
+ ##
17
+ # Register a visitor
18
+ # @param [#on_data] visitor
19
+ # @return [void]
20
+ def register(visitor)
21
+ @visitors << visitor
22
+ end
23
+
24
+ ##
25
+ # Subscribe to an event
26
+ # @param [Symbol] evtname
27
+ # @param [Proc] block
28
+ # @return [void]
29
+ def on(evtname, &block)
30
+ @events[evtname.to_s] << block
31
+ end
32
+
33
+ ##
34
+ # Append an event to the internal buffer
35
+ # @return [void]
36
+ def <<(event)
37
+ io = StringIO.new(event)
38
+ IO.copy_stream io, @buffer
39
+ each_line { parse!(_1) }
40
+ end
41
+
42
+ ##
43
+ # Returns the internal buffer
44
+ # @return [String]
45
+ def body
46
+ @buffer.string
47
+ end
48
+
49
+ ##
50
+ # Free the internal buffer
51
+ # @return [void]
52
+ def free
53
+ @buffer.truncate(0)
54
+ @buffer.rewind
55
+ end
56
+
57
+ private
58
+
59
+ def parse!(event)
60
+ event = Event.new(event)
61
+ dispatch(event)
62
+ end
63
+
64
+ def dispatch(event)
65
+ @visitors.each { dispatch_visitor(_1, event) }
66
+ @events[event.field].each { _1.call(event) }
67
+ end
68
+
69
+ def dispatch_visitor(visitor, event)
70
+ method = "on_#{event.field}"
71
+ if visitor.respond_to?(method)
72
+ visitor.public_send(method, event)
73
+ elsif visitor.respond_to?("on_chunk")
74
+ visitor.on_chunk(event)
75
+ end
76
+ end
77
+
78
+ def each_line
79
+ string.each_line.with_index do
80
+ next if _2 < @offset
81
+ yield(_1)
82
+ @offset += 1
83
+ end
84
+ end
85
+
86
+ def string = @buffer.string
87
+ end
88
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ ##
4
+ # @private
5
+ module LLM::EventStream
6
+ require_relative "eventstream/parser"
7
+ require_relative "eventstream/event"
8
+ end
data/lib/llm/function.rb CHANGED
@@ -1,32 +1,29 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  ##
4
- # The {LLM::Function LLM::Function} class represents a function that can
5
- # be called by an LLM. It comes in two forms: a Proc-based function,
6
- # or a Class-based function.
4
+ # The {LLM::Function LLM::Function} class represents a
5
+ # local function that can be called by an LLM.
7
6
  #
8
- # @example
9
- # # Proc-based
7
+ # @example example #1
10
8
  # LLM.function(:system) do |fn|
11
- # fn.description "Runs system commands, emits their output"
9
+ # fn.description "Runs system commands"
12
10
  # fn.params do |schema|
13
11
  # schema.object(command: schema.string.required)
14
12
  # end
15
13
  # fn.define do |params|
16
- # Kernel.system(params.command)
14
+ # {success: Kernel.system(params.command)}
17
15
  # end
18
16
  # end
19
17
  #
20
- # @example
21
- # # Class-based
18
+ # @example example #2
22
19
  # class System
23
20
  # def call(params)
24
- # Kernel.system(params.command)
21
+ # {success: Kernel.system(params.command)}
25
22
  # end
26
23
  # end
27
24
  #
28
25
  # LLM.function(:system) do |fn|
29
- # fn.description "Runs system commands, emits their output"
26
+ # fn.description "Runs system commands"
30
27
  # fn.params do |schema|
31
28
  # schema.object(command: schema.string.required)
32
29
  # end
@@ -99,7 +96,7 @@ class LLM::Function
99
96
  # Returns a value that communicates that the function call was cancelled
100
97
  # @example
101
98
  # llm = LLM.openai(key: ENV["KEY"])
102
- # bot = LLM::Chat.new(llm, tools: [fn1, fn2])
99
+ # bot = LLM::Bot.new(llm, tools: [fn1, fn2])
103
100
  # bot.chat "I want to run the functions"
104
101
  # bot.chat bot.functions.map(&:cancel)
105
102
  # @return [LLM::Function::Return]
@@ -9,7 +9,7 @@ class LLM::Object
9
9
  # obj = LLM::Object.from_hash(person: {name: 'John'})
10
10
  # obj.person.name # => 'John'
11
11
  # obj.person.class # => LLM::Object
12
- # @param [Hash, Array] obj
12
+ # @param [Hash, LLM::Object, Array] obj
13
13
  # A Hash object
14
14
  # @return [LLM::Object]
15
15
  # An LLM::Object object initialized by visiting `obj` with recursion
@@ -19,20 +19,19 @@ class LLM::Object
19
19
  when Array then obj.map { |v| from_hash(v) }
20
20
  else
21
21
  visited = {}
22
- obj.each { visited[_1] = walk(_2) }
22
+ obj.each { visited[_1] = visit(_2) }
23
23
  new(visited)
24
24
  end
25
25
  end
26
26
 
27
27
  private
28
28
 
29
- def walk(value)
30
- if Hash === value
31
- from_hash(value)
32
- elsif Array === value
33
- value.map { |v| (Hash === v) ? from_hash(v) : v }
34
- else
35
- value
29
+ def visit(value)
30
+ case value
31
+ when self then from_hash(value.to_h)
32
+ when Hash then from_hash(value)
33
+ when Array then value.map { |v| visit(v) }
34
+ else value
36
35
  end
37
36
  end
38
37
  end
@@ -38,7 +38,7 @@ class LLM::Object
38
38
  end
39
39
 
40
40
  def inspect
41
- "#<#{self.class}:0x#{object_id.to_s(16)} @h=#{to_h.inspect}>"
41
+ "#<#{self.class}:0x#{object_id.to_s(16)} properties=#{to_h.inspect}>"
42
42
  end
43
43
  alias_method :to_s, :inspect
44
44
  end
data/lib/llm/object.rb CHANGED
@@ -17,7 +17,7 @@ class LLM::Object < BasicObject
17
17
  ##
18
18
  # @param [Hash] h
19
19
  # @return [LLM::Object]
20
- def initialize(h)
20
+ def initialize(h = {})
21
21
  @h = h.transform_keys(&:to_sym) || h
22
22
  end
23
23
 
@@ -51,6 +51,12 @@ class LLM::Object < BasicObject
51
51
  to_h.to_json(...)
52
52
  end
53
53
 
54
+ ##
55
+ # @return [Boolean]
56
+ def empty?
57
+ @h.empty?
58
+ end
59
+
54
60
  ##
55
61
  # @return [Hash]
56
62
  def to_h
data/lib/llm/provider.rb CHANGED
@@ -21,10 +21,9 @@ class LLM::Provider
21
21
  # Whether to use SSL for the connection
22
22
  def initialize(key:, host:, port: 443, timeout: 60, ssl: true)
23
23
  @key = key
24
- @http = Net::HTTP.new(host, port).tap do |http|
25
- http.use_ssl = ssl
26
- http.read_timeout = timeout
27
- end
24
+ @client = Net::HTTP.new(host, port)
25
+ @client.use_ssl = ssl
26
+ @client.read_timeout = timeout
28
27
  end
29
28
 
30
29
  ##
@@ -78,55 +77,55 @@ class LLM::Provider
78
77
  # Starts a new lazy chat powered by the chat completions API
79
78
  # @note
80
79
  # This method creates a lazy version of a
81
- # {LLM::Chat LLM::Chat} object.
80
+ # {LLM::Bot LLM::Bot} object.
82
81
  # @param prompt (see LLM::Provider#complete)
83
82
  # @param params (see LLM::Provider#complete)
84
- # @return [LLM::Chat]
83
+ # @return [LLM::Bot]
85
84
  def chat(prompt, params = {})
86
85
  role = params.delete(:role)
87
- LLM::Chat.new(self, params).lazy.chat(prompt, role:)
86
+ LLM::Bot.new(self, params).chat(prompt, role:)
88
87
  end
89
88
 
90
89
  ##
91
90
  # Starts a new chat powered by the chat completions API
92
91
  # @note
93
92
  # This method creates a non-lazy version of a
94
- # {LLM::Chat LLM::Chat} object.
93
+ # {LLM::Bot LLM::Bot} object.
95
94
  # @param prompt (see LLM::Provider#complete)
96
95
  # @param params (see LLM::Provider#complete)
97
96
  # @raise (see LLM::Provider#complete)
98
- # @return [LLM::Chat]
97
+ # @return [LLM::Bot]
99
98
  def chat!(prompt, params = {})
100
99
  role = params.delete(:role)
101
- LLM::Chat.new(self, params).chat(prompt, role:)
100
+ LLM::Bot.new(self, params).chat(prompt, role:)
102
101
  end
103
102
 
104
103
  ##
105
104
  # Starts a new lazy chat powered by the responses API
106
105
  # @note
107
106
  # This method creates a lazy variant of a
108
- # {LLM::Chat LLM::Chat} object.
107
+ # {LLM::Bot LLM::Bot} object.
109
108
  # @param prompt (see LLM::Provider#complete)
110
109
  # @param params (see LLM::Provider#complete)
111
110
  # @raise (see LLM::Provider#complete)
112
- # @return [LLM::Chat]
111
+ # @return [LLM::Bot]
113
112
  def respond(prompt, params = {})
114
113
  role = params.delete(:role)
115
- LLM::Chat.new(self, params).lazy.respond(prompt, role:)
114
+ LLM::Bot.new(self, params).respond(prompt, role:)
116
115
  end
117
116
 
118
117
  ##
119
118
  # Starts a new chat powered by the responses API
120
119
  # @note
121
120
  # This method creates a non-lazy variant of a
122
- # {LLM::Chat LLM::Chat} object.
121
+ # {LLM::Bot LLM::Bot} object.
123
122
  # @param prompt (see LLM::Provider#complete)
124
123
  # @param params (see LLM::Provider#complete)
125
124
  # @raise (see LLM::Provider#complete)
126
- # @return [LLM::Chat]
125
+ # @return [LLM::Bot]
127
126
  def respond!(prompt, params = {})
128
127
  role = params.delete(:role)
129
- LLM::Chat.new(self, params).respond(prompt, role:)
128
+ LLM::Bot.new(self, params).respond(prompt, role:)
130
129
  end
131
130
 
132
131
  ##
@@ -194,10 +193,7 @@ class LLM::Provider
194
193
  # Returns an object that can generate a JSON schema
195
194
  # @return [JSON::Schema]
196
195
  def schema
197
- @schema ||= begin
198
- require_relative "../json/schema"
199
- JSON::Schema.new
200
- end
196
+ @schema ||= JSON::Schema.new
201
197
  end
202
198
 
203
199
  ##
@@ -216,6 +212,8 @@ class LLM::Provider
216
212
 
217
213
  private
218
214
 
215
+ attr_reader :client
216
+
219
217
  ##
220
218
  # The headers to include with a request
221
219
  # @raise [NotImplementedError]
@@ -243,10 +241,21 @@ class LLM::Provider
243
241
  end
244
242
 
245
243
  ##
246
- # Initiates a HTTP request
247
- # @param [Net::HTTP] http
248
- # The HTTP object to use for the request
249
- # @param [Net::HTTPRequest] req
244
+ # @return [Class]
245
+ def event_handler
246
+ LLM::EventHandler
247
+ end
248
+
249
+ ##
250
+ # @return [Class]
251
+ # Returns the provider-specific Server-Side Events (SSE) parser
252
+ def stream_parser
253
+ raise NotImplementedError
254
+ end
255
+
256
+ ##
257
+ # Executes a HTTP request
258
+ # @param [Net::HTTPRequest] request
250
259
  # The request to send
251
260
  # @param [Proc] b
252
261
  # A block to yield the response to (optional)
@@ -260,8 +269,34 @@ class LLM::Provider
260
269
  # When any other unsuccessful status code is returned
261
270
  # @raise [SystemCallError]
262
271
  # When there is a network error at the operating system level
263
- def request(http, req, &b)
264
- res = http.request(req, &b)
272
+ # @return [Net::HTTPResponse]
273
+ def execute(request:, stream: nil, &b)
274
+ res = if stream
275
+ client.request(request) do |res|
276
+ handler = event_handler.new stream_parser.new(stream)
277
+ parser = LLM::EventStream::Parser.new
278
+ parser.register(handler)
279
+ res.read_body(parser)
280
+ # If the handler body is empty, it means the
281
+ # response was most likely not streamed or
282
+ # parsing has failed. In that case, we fallback
283
+ # on the original response body.
284
+ res.body = handler.body.empty? ? parser.body.dup : handler.body
285
+ ensure
286
+ parser&.free
287
+ end
288
+ else
289
+ client.request(request, &b)
290
+ end
291
+ handle_response(res)
292
+ end
293
+
294
+ ##
295
+ # Handles the response from a request
296
+ # @param [Net::HTTPResponse] res
297
+ # The response to handle
298
+ # @return [Net::HTTPResponse]
299
+ def handle_response(res)
265
300
  case res
266
301
  when Net::HTTPOK then res
267
302
  else error_handler.new(res).raise_error!
@@ -23,11 +23,11 @@ class LLM::Anthropic
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
@@ -40,7 +40,7 @@ class LLM::Anthropic
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::Anthropic
51
51
 
52
52
  private
53
53
 
54
- def http
55
- @provider.instance_variable_get(:@http)
56
- end
57
-
58
- [:headers, :request].each do |m|
59
- define_method(m) { |*args, &b| @provider.send(m, *args, &b) }
54
+ [:headers, :execute].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
@@ -41,9 +41,9 @@ module LLM::Anthropic::ResponseParser
41
41
  def body = @body
42
42
  def role = body.role
43
43
  def model = body.model
44
- def prompt_tokens = body.usage.input_tokens
45
- def completion_tokens = body.usage.output_tokens
46
- def total_tokens = body.usage.total_tokens
44
+ def prompt_tokens = body.usage&.input_tokens
45
+ def completion_tokens = body.usage&.output_tokens
46
+ def total_tokens = body.usage&.total_tokens
47
47
  def parts = body.content
48
48
  def texts = parts.select { _1["type"] == "text" }
49
49
  def tools = parts.select { _1["type"] == "tool_use" }
@@ -4,6 +4,7 @@ class LLM::Anthropic
4
4
  ##
5
5
  # @private
6
6
  module ResponseParser
7
+ require_relative "response_parser/completion_parser"
7
8
  def parse_embedding(body)
8
9
  {
9
10
  model: body["model"],
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ class LLM::Anthropic
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::Anthropic::StreamParser]
15
+ def initialize(io)
16
+ @body = LLM::Object.new(role: "assistant", content: [])
17
+ @io = io
18
+ end
19
+
20
+ ##
21
+ # @param [Hash] chunk
22
+ # @return [LLM::Anthropic::StreamParser]
23
+ def parse!(chunk)
24
+ tap { merge!(chunk) }
25
+ end
26
+
27
+ private
28
+
29
+ def merge!(chunk)
30
+ if chunk["type"] == "message_start"
31
+ merge_message!(chunk["message"])
32
+ elsif chunk["type"] == "content_block_start"
33
+ @body["content"][chunk["index"]] = chunk["content_block"]
34
+ elsif chunk["type"] == "content_block_delta"
35
+ if chunk["delta"]["type"] == "text_delta"
36
+ @body.content[chunk["index"]]["text"] << chunk["delta"]["text"]
37
+ @io << chunk["delta"]["text"] if @io.respond_to?(:<<)
38
+ elsif chunk["delta"]["type"] == "input_json_delta"
39
+ content = @body.content[chunk["index"]]
40
+ if Hash === content["input"]
41
+ content["input"] = chunk["delta"]["partial_json"]
42
+ else
43
+ content["input"] << chunk["delta"]["partial_json"]
44
+ end
45
+ end
46
+ elsif chunk["type"] == "message_delta"
47
+ merge_message!(chunk["delta"])
48
+ elsif chunk["type"] == "content_block_stop"
49
+ content = @body.content[chunk["index"]]
50
+ if content["input"]
51
+ content["input"] = JSON.parse(content["input"])
52
+ end
53
+ end
54
+ end
55
+
56
+ def merge_message!(message)
57
+ message.each do |key, value|
58
+ @body[key] = if value.respond_to?(:each_pair)
59
+ merge_message!(value)
60
+ else
61
+ value
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -5,10 +5,10 @@ module LLM
5
5
  # The Anthropic class implements a provider for
6
6
  # [Anthropic](https://www.anthropic.com)
7
7
  class Anthropic < Provider
8
+ require_relative "anthropic/format"
8
9
  require_relative "anthropic/error_handler"
10
+ require_relative "anthropic/stream_parser"
9
11
  require_relative "anthropic/response_parser"
10
- require_relative "anthropic/response_parser/completion_parser"
11
- require_relative "anthropic/format"
12
12
  require_relative "anthropic/models"
13
13
  include Format
14
14
 
@@ -50,12 +50,13 @@ module LLM
50
50
  def complete(prompt, params = {})
51
51
  params = {role: :user, model: default_model, max_tokens: 1024}.merge!(params)
52
52
  params = [params, format_tools(params)].inject({}, &:merge!).compact
53
- role = params.delete(:role)
53
+ role, stream = params.delete(:role), params.delete(:stream)
54
+ params[:stream] = true if stream.respond_to?(:<<) || stream == true
54
55
  req = Net::HTTP::Post.new("/v1/messages", headers)
55
56
  messages = [*(params.delete(:messages) || []), Message.new(role, prompt)]
56
57
  body = JSON.dump({messages: [format(messages)].flatten}.merge!(params))
57
58
  set_body_stream(req, StringIO.new(body))
58
- res = request(@http, req)
59
+ res = execute(request: req, stream:)
59
60
  Response::Completion.new(res).extend(response_parser)
60
61
  end
61
62
 
@@ -95,6 +96,10 @@ module LLM
95
96
  LLM::Anthropic::ResponseParser
96
97
  end
97
98
 
99
+ def stream_parser
100
+ LLM::Anthropic::StreamParser
101
+ end
102
+
98
103
  def error_handler
99
104
  LLM::Anthropic::ErrorHandler
100
105
  end
@@ -25,14 +25,14 @@ class LLM::Gemini
25
25
  when Net::HTTPBadRequest
26
26
  reason = body.dig("error", "details", 0, "reason")
27
27
  if reason == "API_KEY_INVALID"
28
- raise LLM::Error::Unauthorized.new { _1.response = res }, "Authentication error"
28
+ raise LLM::UnauthorizedError.new { _1.response = res }, "Authentication error"
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
  when Net::HTTPTooManyRequests
33
- raise LLM::Error::RateLimit.new { _1.response = res }, "Too many requests"
33
+ raise LLM::RateLimitError.new { _1.response = res }, "Too many requests"
34
34
  else
35
- raise LLM::Error::ResponseError.new { _1.response = res }, "Unexpected response"
35
+ raise LLM::ResponseError.new { _1.response = res }, "Unexpected response"
36
36
  end
37
37
  end
38
38