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.
- checksums.yaml +4 -4
- data/README.md +6 -0
- data/lib/llm/bot.rb +4 -4
- data/lib/llm/buffer.rb +0 -9
- data/lib/llm/contract/completion.rb +57 -0
- data/lib/llm/contract.rb +48 -0
- data/lib/llm/error.rb +22 -14
- data/lib/llm/eventhandler.rb +6 -4
- data/lib/llm/eventstream/parser.rb +18 -13
- data/lib/llm/function.rb +1 -1
- data/lib/llm/json_adapter.rb +109 -0
- data/lib/llm/message.rb +7 -28
- data/lib/llm/multipart/enumerator_io.rb +86 -0
- data/lib/llm/multipart.rb +32 -51
- data/lib/llm/object/builder.rb +6 -6
- data/lib/llm/object/kernel.rb +2 -2
- data/lib/llm/object.rb +23 -8
- data/lib/llm/provider.rb +11 -3
- data/lib/llm/providers/anthropic/error_handler.rb +1 -1
- data/lib/llm/providers/anthropic/files.rb +4 -5
- data/lib/llm/providers/anthropic/models.rb +1 -2
- data/lib/llm/providers/anthropic/{format/completion_format.rb → request_adapter/completion.rb} +19 -19
- data/lib/llm/providers/anthropic/{format.rb → request_adapter.rb} +7 -7
- data/lib/llm/providers/anthropic/response_adapter/completion.rb +66 -0
- data/lib/llm/providers/anthropic/{response → response_adapter}/enumerable.rb +1 -1
- data/lib/llm/providers/anthropic/{response → response_adapter}/file.rb +1 -1
- data/lib/llm/providers/anthropic/{response → response_adapter}/web_search.rb +3 -3
- data/lib/llm/providers/anthropic/response_adapter.rb +36 -0
- data/lib/llm/providers/anthropic/stream_parser.rb +6 -6
- data/lib/llm/providers/anthropic.rb +8 -11
- data/lib/llm/providers/deepseek/{format/completion_format.rb → request_adapter/completion.rb} +15 -15
- data/lib/llm/providers/deepseek/{format.rb → request_adapter.rb} +7 -7
- data/lib/llm/providers/deepseek.rb +2 -2
- data/lib/llm/providers/gemini/audio.rb +2 -2
- data/lib/llm/providers/gemini/error_handler.rb +3 -3
- data/lib/llm/providers/gemini/files.rb +4 -7
- data/lib/llm/providers/gemini/images.rb +9 -14
- data/lib/llm/providers/gemini/models.rb +1 -2
- data/lib/llm/providers/gemini/{format/completion_format.rb → request_adapter/completion.rb} +14 -14
- data/lib/llm/providers/gemini/{format.rb → request_adapter.rb} +8 -8
- data/lib/llm/providers/gemini/response_adapter/completion.rb +67 -0
- data/lib/llm/providers/gemini/{response → response_adapter}/embedding.rb +1 -1
- data/lib/llm/providers/gemini/{response → response_adapter}/file.rb +1 -1
- data/lib/llm/providers/gemini/{response → response_adapter}/files.rb +1 -1
- data/lib/llm/providers/gemini/{response → response_adapter}/image.rb +3 -3
- data/lib/llm/providers/gemini/{response → response_adapter}/models.rb +1 -1
- data/lib/llm/providers/gemini/{response → response_adapter}/web_search.rb +3 -3
- data/lib/llm/providers/gemini/response_adapter.rb +42 -0
- data/lib/llm/providers/gemini/stream_parser.rb +37 -32
- data/lib/llm/providers/gemini.rb +10 -14
- data/lib/llm/providers/ollama/error_handler.rb +1 -1
- data/lib/llm/providers/ollama/{format/completion_format.rb → request_adapter/completion.rb} +19 -19
- data/lib/llm/providers/ollama/{format.rb → request_adapter.rb} +7 -7
- data/lib/llm/providers/ollama/response_adapter/completion.rb +61 -0
- data/lib/llm/providers/ollama/{response → response_adapter}/embedding.rb +1 -1
- data/lib/llm/providers/ollama/response_adapter.rb +32 -0
- data/lib/llm/providers/ollama/stream_parser.rb +2 -2
- data/lib/llm/providers/ollama.rb +8 -10
- data/lib/llm/providers/openai/audio.rb +1 -1
- data/lib/llm/providers/openai/error_handler.rb +12 -2
- data/lib/llm/providers/openai/files.rb +3 -6
- data/lib/llm/providers/openai/images.rb +4 -5
- data/lib/llm/providers/openai/models.rb +1 -3
- data/lib/llm/providers/openai/moderations.rb +3 -5
- data/lib/llm/providers/openai/{format/completion_format.rb → request_adapter/completion.rb} +22 -22
- data/lib/llm/providers/openai/{format/moderation_format.rb → request_adapter/moderation.rb} +5 -5
- data/lib/llm/providers/openai/{format/respond_format.rb → request_adapter/respond.rb} +16 -16
- data/lib/llm/providers/openai/{format.rb → request_adapter.rb} +12 -12
- data/lib/llm/providers/openai/{response → response_adapter}/audio.rb +1 -1
- data/lib/llm/providers/openai/response_adapter/completion.rb +62 -0
- data/lib/llm/providers/openai/{response → response_adapter}/embedding.rb +1 -1
- data/lib/llm/providers/openai/{response → response_adapter}/enumerable.rb +1 -1
- data/lib/llm/providers/openai/{response → response_adapter}/file.rb +1 -1
- data/lib/llm/providers/openai/{response → response_adapter}/image.rb +1 -1
- data/lib/llm/providers/openai/{response → response_adapter}/moderations.rb +1 -1
- data/lib/llm/providers/openai/{response → response_adapter}/responds.rb +6 -10
- data/lib/llm/providers/openai/{response → response_adapter}/web_search.rb +3 -3
- data/lib/llm/providers/openai/response_adapter.rb +47 -0
- data/lib/llm/providers/openai/responses/stream_parser.rb +22 -22
- data/lib/llm/providers/openai/responses.rb +6 -8
- data/lib/llm/providers/openai/stream_parser.rb +6 -5
- data/lib/llm/providers/openai/vector_stores.rb +8 -9
- data/lib/llm/providers/openai.rb +12 -14
- data/lib/llm/response.rb +2 -5
- data/lib/llm/usage.rb +10 -0
- data/lib/llm/version.rb +1 -1
- data/lib/llm.rb +33 -1
- metadata +44 -35
- data/lib/llm/providers/anthropic/response/completion.rb +0 -39
- data/lib/llm/providers/gemini/response/completion.rb +0 -35
- data/lib/llm/providers/ollama/response/completion.rb +0 -28
- data/lib/llm/providers/openai/response/completion.rb +0 -40
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2e60be1fa699baabf9a1df129d0263d0c2b6fecc3ce2b818128eed319aa7bb18
|
|
4
|
+
data.tar.gz: a39d32cd9cfcfb7fa4152ba3e02960522b6ae4b5d6e22705b846f5b9dcd2972b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4d434afe1a6acaeef6036178c5914500e047450ee7d92999dedd80f66a6be22c73f4d788f86739842ee283c579b68476c0a114f50e25e7db7bbfa6e6cdf1a5bc
|
|
7
|
+
data.tar.gz: e818753b0b06cf4053652d11b2e3c79d7ef58de0b0acc5fb7fa147e55c693eee583f1c9e1f80b08096de95247c03644fad9e596f4511d57a99d2abd5339087c2
|
data/README.md
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
> **Minimal footprint** <br>
|
|
2
|
+
> Zero dependencies outside Ruby’s standard library. <br>
|
|
3
|
+
> Zero runtime dependencies.
|
|
4
|
+
|
|
1
5
|
## About
|
|
2
6
|
|
|
3
7
|
llm.rb is a zero-dependency Ruby toolkit for Large Language Models that
|
|
@@ -102,6 +106,7 @@ bot.messages.select(&:assistant?).each { print "[#{it.role}] ", it.content, "\n"
|
|
|
102
106
|
#### General
|
|
103
107
|
- ✅ A single unified interface for multiple providers
|
|
104
108
|
- 📦 Zero dependencies outside Ruby's standard library
|
|
109
|
+
- 🧩 Choose your own JSON parser (JSON stdlib, Oj, Yajl, etc)
|
|
105
110
|
- 🚀 Simple, composable API
|
|
106
111
|
- ♻️ Optional: per-provider, process-wide connection pool via net-http-persistent
|
|
107
112
|
|
|
@@ -115,6 +120,7 @@ bot.messages.select(&:assistant?).each { print "[#{it.role}] ", it.content, "\n"
|
|
|
115
120
|
- 🗣️ Text-to-speech, transcription, and translation
|
|
116
121
|
- 🖼️ Image generation, editing, and variation support
|
|
117
122
|
- 📎 File uploads and prompt-aware file interaction
|
|
123
|
+
- 📦 Streams multipart uploads and avoids buffering large files in memory
|
|
118
124
|
- 💡 Multimodal prompts (text, documents, audio, images, videos, URLs, etc)
|
|
119
125
|
|
|
120
126
|
#### Embeddings
|
data/lib/llm/bot.rb
CHANGED
|
@@ -119,7 +119,7 @@ module LLM
|
|
|
119
119
|
# if there are no assistant messages
|
|
120
120
|
# @return [LLM::Object]
|
|
121
121
|
def usage
|
|
122
|
-
@messages.find(&:assistant?)&.usage || LLM::Object.
|
|
122
|
+
@messages.find(&:assistant?)&.usage || LLM::Object.from({})
|
|
123
123
|
end
|
|
124
124
|
|
|
125
125
|
##
|
|
@@ -141,7 +141,7 @@ module LLM
|
|
|
141
141
|
# @return [LLM::Object]
|
|
142
142
|
# Returns a tagged object
|
|
143
143
|
def image_url(url)
|
|
144
|
-
LLM::Object.
|
|
144
|
+
LLM::Object.from(value: url, kind: :image_url)
|
|
145
145
|
end
|
|
146
146
|
|
|
147
147
|
##
|
|
@@ -151,7 +151,7 @@ module LLM
|
|
|
151
151
|
# @return [LLM::Object]
|
|
152
152
|
# Returns a tagged object
|
|
153
153
|
def local_file(path)
|
|
154
|
-
LLM::Object.
|
|
154
|
+
LLM::Object.from(value: LLM.File(path), kind: :local_file)
|
|
155
155
|
end
|
|
156
156
|
|
|
157
157
|
##
|
|
@@ -161,7 +161,7 @@ module LLM
|
|
|
161
161
|
# @return [LLM::Object]
|
|
162
162
|
# Returns a tagged object
|
|
163
163
|
def remote_file(res)
|
|
164
|
-
LLM::Object.
|
|
164
|
+
LLM::Object.from(value: res, kind: :remote_file)
|
|
165
165
|
end
|
|
166
166
|
|
|
167
167
|
private
|
data/lib/llm/buffer.rb
CHANGED
|
@@ -35,15 +35,6 @@ module LLM
|
|
|
35
35
|
end
|
|
36
36
|
end
|
|
37
37
|
|
|
38
|
-
##
|
|
39
|
-
# Returns an array of unread messages
|
|
40
|
-
# @see LLM::Message#read?
|
|
41
|
-
# @see LLM::Message#read!
|
|
42
|
-
# @return [Array<LLM::Message>]
|
|
43
|
-
def unread
|
|
44
|
-
reject(&:read?)
|
|
45
|
-
end
|
|
46
|
-
|
|
47
38
|
##
|
|
48
39
|
# Find a message (in descending order)
|
|
49
40
|
# @return [LLM::Message, nil]
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LLM::Contract
|
|
4
|
+
##
|
|
5
|
+
# Defines the interface all completion responses must implement
|
|
6
|
+
# @abstract
|
|
7
|
+
module Completion
|
|
8
|
+
extend LLM::Contract
|
|
9
|
+
|
|
10
|
+
##
|
|
11
|
+
# @return [Array<LLM::Messsage>]
|
|
12
|
+
# Returns one or more messages
|
|
13
|
+
def messages
|
|
14
|
+
raise NotImplementedError, "#{self.class} does not implement '#{__method__}'"
|
|
15
|
+
end
|
|
16
|
+
alias_method :choices, :messages
|
|
17
|
+
|
|
18
|
+
##
|
|
19
|
+
# @return [Integer]
|
|
20
|
+
# Returns the number of input tokens
|
|
21
|
+
def input_tokens
|
|
22
|
+
raise NotImplementedError, "#{self.class} does not implement '#{__method__}'"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
##
|
|
26
|
+
# @return [Integer]
|
|
27
|
+
# Returns the number of output tokens
|
|
28
|
+
def output_tokens
|
|
29
|
+
raise NotImplementedError, "#{self.class} does not implement '#{__method__}'"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
##
|
|
33
|
+
# @return [Integer]
|
|
34
|
+
# Returns the total number of tokens
|
|
35
|
+
def total_tokens
|
|
36
|
+
raise NotImplementedError, "#{self.class} does not implement '#{__method__}'"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
##
|
|
40
|
+
# @return [LLM::Usage]
|
|
41
|
+
# Returns usage information
|
|
42
|
+
def usage
|
|
43
|
+
LLM::Usage.new(
|
|
44
|
+
input_tokens:,
|
|
45
|
+
output_tokens:,
|
|
46
|
+
total_tokens:
|
|
47
|
+
)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
##
|
|
51
|
+
# @return [String]
|
|
52
|
+
# Returns the model name
|
|
53
|
+
def model
|
|
54
|
+
raise NotImplementedError, "#{self.class} does not implement '#{__method__}'"
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
data/lib/llm/contract.rb
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LLM
|
|
4
|
+
##
|
|
5
|
+
# The `LLM::Contract` module provides the ability for modules
|
|
6
|
+
# who are extended by it to implement contracts which must be
|
|
7
|
+
# implemented by other modules who include a given contract.
|
|
8
|
+
#
|
|
9
|
+
# @example
|
|
10
|
+
# module LLM::Contract
|
|
11
|
+
# # ..
|
|
12
|
+
# end
|
|
13
|
+
#
|
|
14
|
+
# module LLM::Contract
|
|
15
|
+
# module Completion
|
|
16
|
+
# extend LLM::Contract
|
|
17
|
+
# # inheriting modules must implement these methods
|
|
18
|
+
# # otherwise an error is raised on include
|
|
19
|
+
# def foo = nil
|
|
20
|
+
# def bar = nil
|
|
21
|
+
# end
|
|
22
|
+
# end
|
|
23
|
+
#
|
|
24
|
+
# module LLM::OpenAI::ResponseAdapter
|
|
25
|
+
# module Completion
|
|
26
|
+
# def foo = nil
|
|
27
|
+
# def bar = nil
|
|
28
|
+
# include LLM::Contract::Completion
|
|
29
|
+
# end
|
|
30
|
+
# end
|
|
31
|
+
module Contract
|
|
32
|
+
ContractError = Class.new(LLM::Error)
|
|
33
|
+
require_relative "contract/completion"
|
|
34
|
+
|
|
35
|
+
##
|
|
36
|
+
# @api private
|
|
37
|
+
def included(mod)
|
|
38
|
+
meths = mod.instance_methods(false)
|
|
39
|
+
if meths.empty?
|
|
40
|
+
raise ContractError, "#{mod} does not implement any methods required by #{self}"
|
|
41
|
+
end
|
|
42
|
+
missing = instance_methods - meths
|
|
43
|
+
if missing.any?
|
|
44
|
+
raise ContractError, "#{mod} does not implement methods (#{missing.join(", ")}) required by #{self}"
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
data/lib/llm/error.rb
CHANGED
|
@@ -4,40 +4,40 @@ module LLM
|
|
|
4
4
|
##
|
|
5
5
|
# The superclass of all LLM errors
|
|
6
6
|
class Error < RuntimeError
|
|
7
|
+
##
|
|
8
|
+
# @return [Net::HTTPResponse, nil]
|
|
9
|
+
# Returns the response associated with an error, or nil
|
|
10
|
+
attr_accessor :response
|
|
11
|
+
|
|
7
12
|
def initialize(...)
|
|
8
13
|
block_given? ? yield(self) : nil
|
|
9
14
|
super
|
|
10
15
|
end
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
##
|
|
14
|
-
# The superclass of all HTTP protocol errors
|
|
15
|
-
class ResponseError < Error
|
|
16
|
-
##
|
|
17
|
-
# @return [Net::HTTPResponse]
|
|
18
|
-
# Returns the response associated with an error
|
|
19
|
-
attr_accessor :response
|
|
20
16
|
|
|
21
17
|
def message
|
|
22
|
-
|
|
18
|
+
if response
|
|
19
|
+
[super, response.body].join("\n")
|
|
20
|
+
else
|
|
21
|
+
super
|
|
22
|
+
end
|
|
23
23
|
end
|
|
24
24
|
end
|
|
25
25
|
|
|
26
26
|
##
|
|
27
27
|
# HTTPUnauthorized
|
|
28
|
-
UnauthorizedError = Class.new(
|
|
28
|
+
UnauthorizedError = Class.new(Error)
|
|
29
29
|
|
|
30
30
|
##
|
|
31
31
|
# HTTPTooManyRequests
|
|
32
|
-
RateLimitError = Class.new(
|
|
32
|
+
RateLimitError = Class.new(Error)
|
|
33
33
|
|
|
34
34
|
##
|
|
35
35
|
# HTTPServerError
|
|
36
|
-
ServerError = Class.new(
|
|
36
|
+
ServerError = Class.new(Error)
|
|
37
37
|
|
|
38
38
|
##
|
|
39
39
|
# When no images are found in a response
|
|
40
|
-
NoImageError = Class.new(
|
|
40
|
+
NoImageError = Class.new(Error)
|
|
41
41
|
|
|
42
42
|
##
|
|
43
43
|
# When an given an input object that is not understood
|
|
@@ -46,4 +46,12 @@ module LLM
|
|
|
46
46
|
##
|
|
47
47
|
# When given a prompt object that is not understood
|
|
48
48
|
PromptError = Class.new(FormatError)
|
|
49
|
+
|
|
50
|
+
##
|
|
51
|
+
# When given an invalid request
|
|
52
|
+
InvalidRequestError = Class.new(Error)
|
|
53
|
+
|
|
54
|
+
##
|
|
55
|
+
# When the context window is exceeded
|
|
56
|
+
ContextWindowError = Class.new(InvalidRequestError)
|
|
49
57
|
end
|
data/lib/llm/eventhandler.rb
CHANGED
|
@@ -17,9 +17,10 @@ module LLM
|
|
|
17
17
|
# @return [void]
|
|
18
18
|
def on_data(event)
|
|
19
19
|
return if event.end?
|
|
20
|
-
chunk =
|
|
20
|
+
chunk = LLM.json.load(event.value)
|
|
21
|
+
return unless chunk
|
|
21
22
|
@parser.parse!(chunk)
|
|
22
|
-
rescue
|
|
23
|
+
rescue *LLM.json.parser_error
|
|
23
24
|
end
|
|
24
25
|
|
|
25
26
|
##
|
|
@@ -31,9 +32,10 @@ module LLM
|
|
|
31
32
|
# @return [void]
|
|
32
33
|
def on_chunk(event)
|
|
33
34
|
return if event.end?
|
|
34
|
-
chunk =
|
|
35
|
+
chunk = LLM.json.load(event.chunk)
|
|
36
|
+
return unless chunk
|
|
35
37
|
@parser.parse!(chunk)
|
|
36
|
-
rescue
|
|
38
|
+
rescue *LLM.json.parser_error
|
|
37
39
|
end
|
|
38
40
|
|
|
39
41
|
##
|
|
@@ -7,9 +7,9 @@ module LLM::EventStream
|
|
|
7
7
|
##
|
|
8
8
|
# @return [LLM::EventStream::Parser]
|
|
9
9
|
def initialize
|
|
10
|
-
@buffer =
|
|
10
|
+
@buffer = +""
|
|
11
11
|
@events = Hash.new { |h, k| h[k] = [] }
|
|
12
|
-
@
|
|
12
|
+
@cursor = 0
|
|
13
13
|
@visitors = []
|
|
14
14
|
end
|
|
15
15
|
|
|
@@ -34,8 +34,7 @@ module LLM::EventStream
|
|
|
34
34
|
# Append an event to the internal buffer
|
|
35
35
|
# @return [void]
|
|
36
36
|
def <<(event)
|
|
37
|
-
|
|
38
|
-
IO.copy_stream io, @buffer
|
|
37
|
+
@buffer << event
|
|
39
38
|
each_line { parse!(_1) }
|
|
40
39
|
end
|
|
41
40
|
|
|
@@ -43,15 +42,15 @@ module LLM::EventStream
|
|
|
43
42
|
# Returns the internal buffer
|
|
44
43
|
# @return [String]
|
|
45
44
|
def body
|
|
46
|
-
@buffer.
|
|
45
|
+
@buffer.dup
|
|
47
46
|
end
|
|
48
47
|
|
|
49
48
|
##
|
|
50
49
|
# Free the internal buffer
|
|
51
50
|
# @return [void]
|
|
52
51
|
def free
|
|
53
|
-
@buffer.
|
|
54
|
-
@
|
|
52
|
+
@buffer.clear
|
|
53
|
+
@cursor = 0
|
|
55
54
|
end
|
|
56
55
|
|
|
57
56
|
private
|
|
@@ -76,13 +75,19 @@ module LLM::EventStream
|
|
|
76
75
|
end
|
|
77
76
|
|
|
78
77
|
def each_line
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
78
|
+
while (newline = @buffer.index("\n", @cursor))
|
|
79
|
+
line = @buffer[@cursor..newline]
|
|
80
|
+
@cursor = newline + 1
|
|
81
|
+
yield(line)
|
|
83
82
|
end
|
|
83
|
+
if @cursor < @buffer.length
|
|
84
|
+
line = @buffer[@cursor..]
|
|
85
|
+
@cursor = @buffer.length
|
|
86
|
+
yield(line)
|
|
87
|
+
end
|
|
88
|
+
return if @cursor.zero?
|
|
89
|
+
@buffer = @buffer[@cursor..] || +""
|
|
90
|
+
@cursor = 0
|
|
84
91
|
end
|
|
85
|
-
|
|
86
|
-
def string = @buffer.string
|
|
87
92
|
end
|
|
88
93
|
end
|
data/lib/llm/function.rb
CHANGED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LLM
|
|
4
|
+
##
|
|
5
|
+
# The JSONAdapter class defines the interface for JSON parsers
|
|
6
|
+
# that can be used by the library when dealing with JSON. The
|
|
7
|
+
# following parsers are supported:
|
|
8
|
+
# * {LLM::JSONAdapter::JSON LLM::JSONAdapter::JSON} (default)
|
|
9
|
+
# * {LLM::JSONAdapter::Oj LLM::JSONAdapter::Oj}
|
|
10
|
+
# * {LLM::JSONAdapter::Yajl LLM::JSONAdapter::Yajl}
|
|
11
|
+
#
|
|
12
|
+
# @example Change parser
|
|
13
|
+
# LLM.json = LLM::JSONAdapter::Oj
|
|
14
|
+
class JSONAdapter
|
|
15
|
+
##
|
|
16
|
+
# @return [String]
|
|
17
|
+
# Returns a JSON string representation of the given object
|
|
18
|
+
def self.dump(*) = raise NotImplementedError
|
|
19
|
+
|
|
20
|
+
##
|
|
21
|
+
# @return [Object]
|
|
22
|
+
# Returns a Ruby object parsed from the given JSON string
|
|
23
|
+
def self.load(*) = raise NotImplementedError
|
|
24
|
+
|
|
25
|
+
##
|
|
26
|
+
# @return [Exception]
|
|
27
|
+
# Returns the error raised when parsing fails
|
|
28
|
+
def self.parser_error = [StandardError]
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
##
|
|
32
|
+
# The {LLM::JSONAdapter::JSON LLM::JSONAdapter::JSON} class
|
|
33
|
+
# provides a JSON adapter backed by the standard library
|
|
34
|
+
# JSON module.
|
|
35
|
+
class JSONAdapter::JSON < JSONAdapter
|
|
36
|
+
##
|
|
37
|
+
# @return (see JSONAdapter#dump)
|
|
38
|
+
def self.dump(obj)
|
|
39
|
+
require "json" unless defined?(::JSON)
|
|
40
|
+
::JSON.dump(obj)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
##
|
|
44
|
+
# @return (see JSONAdapter#load)
|
|
45
|
+
def self.load(string)
|
|
46
|
+
require "json" unless defined?(::JSON)
|
|
47
|
+
::JSON.parse(string)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
##
|
|
51
|
+
# @return (see JSONAdapter#parser_error)
|
|
52
|
+
def self.parser_error
|
|
53
|
+
require "json" unless defined?(::JSON)
|
|
54
|
+
[::JSON::ParserError]
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
##
|
|
59
|
+
# The {LLM::JSONAdapter::Oj LLM::JSONAdapter::Oj} class
|
|
60
|
+
# provides a JSON adapter backed by the Oj gem.
|
|
61
|
+
class JSONAdapter::Oj < JSONAdapter
|
|
62
|
+
##
|
|
63
|
+
# @return (see JSONAdapter#dump)
|
|
64
|
+
def self.dump(obj)
|
|
65
|
+
require "oj" unless defined?(::Oj)
|
|
66
|
+
::Oj.dump(obj)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
##
|
|
70
|
+
# @return (see JSONAdapter#load)
|
|
71
|
+
def self.load(string)
|
|
72
|
+
require "oj" unless defined?(::Oj)
|
|
73
|
+
::Oj.load(string, mode: :compat, symbol_keys: false, symbolize_names: false)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
##
|
|
77
|
+
# @return (see JSONAdapter#parser_error)
|
|
78
|
+
def self.parser_error
|
|
79
|
+
require "oj" unless defined?(::Oj)
|
|
80
|
+
[::Oj::ParseError, ::EncodingError]
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
##
|
|
85
|
+
# The {LLM::JSONAdapter::Yajl LLM::JSONAdapter::Yajl} class
|
|
86
|
+
# provides a JSON adapter backed by the Yajl gem.
|
|
87
|
+
class JSONAdapter::Yajl < JSONAdapter
|
|
88
|
+
##
|
|
89
|
+
# @return (see JSONAdapter#dump)
|
|
90
|
+
def self.dump(obj)
|
|
91
|
+
require "yajl" unless defined?(::Yajl)
|
|
92
|
+
::Yajl::Encoder.encode(obj)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
##
|
|
96
|
+
# @return (see JSONAdapter#load)
|
|
97
|
+
def self.load(string)
|
|
98
|
+
require "yajl" unless defined?(::Yajl)
|
|
99
|
+
::Yajl::Parser.parse(string)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
##
|
|
103
|
+
# @return (see JSONAdapter#parser_error)
|
|
104
|
+
def self.parser_error
|
|
105
|
+
require "yajl" unless defined?(::Yajl)
|
|
106
|
+
[::Yajl::ParseError]
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
data/lib/llm/message.rb
CHANGED
|
@@ -54,7 +54,7 @@ module LLM
|
|
|
54
54
|
# Try to parse the content as JSON
|
|
55
55
|
# @return [Hash]
|
|
56
56
|
def content!
|
|
57
|
-
|
|
57
|
+
LLM.json.load(content)
|
|
58
58
|
end
|
|
59
59
|
|
|
60
60
|
##
|
|
@@ -67,20 +67,6 @@ module LLM
|
|
|
67
67
|
end
|
|
68
68
|
end
|
|
69
69
|
|
|
70
|
-
##
|
|
71
|
-
# Marks the message as read
|
|
72
|
-
# @return [void]
|
|
73
|
-
def read!
|
|
74
|
-
@read = true
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
##
|
|
78
|
-
# Returns true when the message has been read
|
|
79
|
-
# @return [Boolean]
|
|
80
|
-
def read?
|
|
81
|
-
@read
|
|
82
|
-
end
|
|
83
|
-
|
|
84
70
|
##
|
|
85
71
|
# Returns true when the message is an assistant message
|
|
86
72
|
# @return [Boolean]
|
|
@@ -134,25 +120,18 @@ module LLM
|
|
|
134
120
|
# Returns annotations associated with the message
|
|
135
121
|
# @return [Array<LLM::Object>]
|
|
136
122
|
def annotations
|
|
137
|
-
@annotations ||= LLM::Object.
|
|
123
|
+
@annotations ||= LLM::Object.from(extra["annotations"] || [])
|
|
138
124
|
end
|
|
139
125
|
|
|
140
126
|
##
|
|
141
127
|
# @note
|
|
142
128
|
# This method returns token usage for assistant messages,
|
|
143
|
-
# and it returns
|
|
129
|
+
# and it returns nil for non-assistant messages
|
|
144
130
|
# Returns token usage statistics
|
|
145
|
-
# @return [LLM::Object]
|
|
131
|
+
# @return [LLM::Object, nil]
|
|
146
132
|
def usage
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
input_tokens: response.prompt_tokens || 0,
|
|
150
|
-
output_tokens: response.completion_tokens || 0,
|
|
151
|
-
total_tokens: response.total_tokens || 0
|
|
152
|
-
})
|
|
153
|
-
else
|
|
154
|
-
LLM::Object.from_hash({})
|
|
155
|
-
end
|
|
133
|
+
return nil unless response
|
|
134
|
+
@usage ||= response.usage
|
|
156
135
|
end
|
|
157
136
|
alias_method :token_usage, :usage
|
|
158
137
|
|
|
@@ -168,7 +147,7 @@ module LLM
|
|
|
168
147
|
private
|
|
169
148
|
|
|
170
149
|
def tool_calls
|
|
171
|
-
@tool_calls ||= LLM::Object.
|
|
150
|
+
@tool_calls ||= LLM::Object.from(@extra[:tool_calls] || [])
|
|
172
151
|
end
|
|
173
152
|
|
|
174
153
|
def tools
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class LLM::Multipart
|
|
4
|
+
##
|
|
5
|
+
# @private
|
|
6
|
+
# Wraps an Enumerator as an IO-like object for streaming bodies.
|
|
7
|
+
class EnumeratorIO
|
|
8
|
+
##
|
|
9
|
+
# @param [Enumerator] enum
|
|
10
|
+
# The enumerator yielding body chunks
|
|
11
|
+
def initialize(enum)
|
|
12
|
+
@enum = enum
|
|
13
|
+
@buffer = +""
|
|
14
|
+
@eof = false
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
##
|
|
18
|
+
# Reads bytes from the stream
|
|
19
|
+
# @param [Integer, nil] length
|
|
20
|
+
# The number of bytes to read (all when nil)
|
|
21
|
+
# @param [String] outbuf
|
|
22
|
+
# The buffer to fill
|
|
23
|
+
# @return [String, nil]
|
|
24
|
+
# Returns the data read, or nil on EOF
|
|
25
|
+
def read(length = nil, outbuf = +"")
|
|
26
|
+
outbuf.clear
|
|
27
|
+
if length.nil?
|
|
28
|
+
read_all(outbuf)
|
|
29
|
+
else
|
|
30
|
+
read_chunk(length, outbuf)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
##
|
|
35
|
+
# Returns true when no more data is available
|
|
36
|
+
# @return [Boolean]
|
|
37
|
+
def eof?
|
|
38
|
+
@eof && @buffer.empty?
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
##
|
|
42
|
+
# Raises when called, the stream is not rewindable
|
|
43
|
+
# @raise [IOError]
|
|
44
|
+
def rewind
|
|
45
|
+
raise IOError, "stream is not rewindable"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def read_all(outbuf)
|
|
51
|
+
fill_buffer
|
|
52
|
+
return nil if eof?
|
|
53
|
+
outbuf << @buffer
|
|
54
|
+
@buffer.clear
|
|
55
|
+
while (chunk = next_chunk)
|
|
56
|
+
outbuf << chunk
|
|
57
|
+
end
|
|
58
|
+
@eof = true
|
|
59
|
+
outbuf
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def read_chunk(length, outbuf)
|
|
63
|
+
fill_buffer while @buffer.bytesize < length && !@eof
|
|
64
|
+
return nil if eof?
|
|
65
|
+
outbuf << @buffer.byteslice(0, length)
|
|
66
|
+
@buffer = @buffer.byteslice(length..-1) || +""
|
|
67
|
+
outbuf
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def fill_buffer
|
|
71
|
+
return if @eof
|
|
72
|
+
chunk = next_chunk
|
|
73
|
+
if chunk
|
|
74
|
+
@buffer << chunk
|
|
75
|
+
else
|
|
76
|
+
@eof = true
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def next_chunk
|
|
81
|
+
@enum.next
|
|
82
|
+
rescue StopIteration
|
|
83
|
+
nil
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|