llm.rb 8.1.0 → 9.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/CHANGELOG.md +120 -2
- data/README.md +161 -514
- data/lib/llm/active_record/acts_as_llm.rb +7 -8
- data/lib/llm/agent.rb +36 -16
- data/lib/llm/context.rb +30 -26
- data/lib/llm/contract/completion.rb +45 -0
- data/lib/llm/cost.rb +81 -4
- data/lib/llm/error.rb +1 -1
- data/lib/llm/function/array.rb +8 -5
- data/lib/llm/function/call_group.rb +39 -0
- data/lib/llm/function/fork/task.rb +6 -0
- data/lib/llm/function/ractor/task.rb +6 -0
- data/lib/llm/function/task.rb +10 -0
- data/lib/llm/function.rb +1 -0
- data/lib/llm/mcp/transport/http.rb +26 -46
- data/lib/llm/mcp/transport/stdio.rb +0 -8
- data/lib/llm/mcp.rb +6 -23
- data/lib/llm/provider.rb +23 -20
- data/lib/llm/providers/anthropic/error_handler.rb +6 -7
- data/lib/llm/providers/anthropic/files.rb +2 -2
- data/lib/llm/providers/anthropic/response_adapter/completion.rb +30 -0
- data/lib/llm/providers/anthropic.rb +1 -1
- data/lib/llm/providers/bedrock/error_handler.rb +8 -9
- data/lib/llm/providers/bedrock/models.rb +13 -13
- data/lib/llm/providers/bedrock/response_adapter/completion.rb +30 -0
- data/lib/llm/providers/bedrock.rb +1 -1
- data/lib/llm/providers/google/error_handler.rb +6 -7
- data/lib/llm/providers/google/files.rb +2 -4
- data/lib/llm/providers/google/images.rb +1 -1
- data/lib/llm/providers/google/models.rb +0 -2
- data/lib/llm/providers/google/response_adapter/completion.rb +30 -0
- data/lib/llm/providers/google.rb +1 -1
- data/lib/llm/providers/ollama/error_handler.rb +6 -7
- data/lib/llm/providers/ollama/models.rb +0 -2
- data/lib/llm/providers/ollama/response_adapter/completion.rb +30 -0
- data/lib/llm/providers/ollama.rb +1 -1
- data/lib/llm/providers/openai/audio.rb +3 -3
- data/lib/llm/providers/openai/error_handler.rb +6 -7
- data/lib/llm/providers/openai/files.rb +2 -2
- data/lib/llm/providers/openai/images.rb +3 -3
- data/lib/llm/providers/openai/models.rb +1 -1
- data/lib/llm/providers/openai/response_adapter/completion.rb +42 -0
- data/lib/llm/providers/openai/response_adapter/responds.rb +39 -0
- data/lib/llm/providers/openai/responses.rb +2 -2
- data/lib/llm/providers/openai/vector_stores.rb +1 -1
- data/lib/llm/providers/openai.rb +1 -1
- data/lib/llm/response.rb +10 -8
- data/lib/llm/sequel/plugin.rb +7 -8
- data/lib/llm/stream/queue.rb +15 -42
- data/lib/llm/stream.rb +4 -4
- data/lib/llm/transport/execution.rb +67 -0
- data/lib/llm/transport/http.rb +134 -0
- data/lib/llm/transport/persistent_http.rb +152 -0
- data/lib/llm/transport/response/http.rb +113 -0
- data/lib/llm/transport/response.rb +112 -0
- data/lib/llm/{provider/transport/http → transport}/stream_decoder.rb +8 -4
- data/lib/llm/transport.rb +139 -0
- data/lib/llm/usage.rb +14 -5
- data/lib/llm/version.rb +1 -1
- data/lib/llm.rb +2 -12
- data/llm.gemspec +2 -16
- metadata +11 -19
- data/lib/llm/provider/transport/http/execution.rb +0 -115
- data/lib/llm/provider/transport/http/interruptible.rb +0 -114
- data/lib/llm/provider/transport/http.rb +0 -145
- data/lib/llm/utils.rb +0 -19
|
@@ -1,115 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module LLM::Provider::Transport
|
|
4
|
-
class HTTP
|
|
5
|
-
##
|
|
6
|
-
# Internal HTTP request execution methods for {LLM::Provider}.
|
|
7
|
-
#
|
|
8
|
-
# This module handles provider-side HTTP execution, response parsing,
|
|
9
|
-
# streaming, and request body setup through
|
|
10
|
-
# {LLM::Provider::Transport::HTTP}.
|
|
11
|
-
#
|
|
12
|
-
# @api private
|
|
13
|
-
module HTTP::Execution
|
|
14
|
-
private
|
|
15
|
-
|
|
16
|
-
##
|
|
17
|
-
# Executes a HTTP request
|
|
18
|
-
# @param [Net::HTTPRequest] request
|
|
19
|
-
# The request to send
|
|
20
|
-
# @param [Proc] b
|
|
21
|
-
# A block to yield the response to (optional)
|
|
22
|
-
# @return [Net::HTTPResponse]
|
|
23
|
-
# The response from the server
|
|
24
|
-
# @raise [LLM::Error::Unauthorized]
|
|
25
|
-
# When authentication fails
|
|
26
|
-
# @raise [LLM::Error::RateLimit]
|
|
27
|
-
# When the rate limit is exceeded
|
|
28
|
-
# @raise [LLM::Error]
|
|
29
|
-
# When any other unsuccessful status code is returned
|
|
30
|
-
# @raise [SystemCallError]
|
|
31
|
-
# When there is a network error at the operating system level
|
|
32
|
-
# @return [Net::HTTPResponse]
|
|
33
|
-
def execute(request:, operation:, stream: nil, stream_parser: self.stream_parser, model: nil, inputs: nil, &b)
|
|
34
|
-
owner = transport.request_owner
|
|
35
|
-
tracer = self.tracer
|
|
36
|
-
span = tracer.on_request_start(operation:, model:, inputs:)
|
|
37
|
-
res = transport.request(request, owner:) do |http|
|
|
38
|
-
perform_request(http, request, stream, stream_parser, &b)
|
|
39
|
-
end
|
|
40
|
-
[handle_response(res, tracer, span), span, tracer]
|
|
41
|
-
rescue *transport.interrupt_errors
|
|
42
|
-
raise LLM::Interrupt, "request interrupted" if transport.interrupted?(owner)
|
|
43
|
-
raise
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
##
|
|
47
|
-
# Handles the response from a request
|
|
48
|
-
# @param [Net::HTTPResponse] res
|
|
49
|
-
# The response to handle
|
|
50
|
-
# @param [Object, nil] span
|
|
51
|
-
# The span
|
|
52
|
-
# @return [Net::HTTPResponse]
|
|
53
|
-
def handle_response(res, tracer, span)
|
|
54
|
-
case res
|
|
55
|
-
when Net::HTTPOK then res.body = parse_response(res)
|
|
56
|
-
else error_handler.new(tracer, span, res).raise_error!
|
|
57
|
-
end
|
|
58
|
-
res
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
##
|
|
62
|
-
# Parse a HTTP response
|
|
63
|
-
# @param [Net::HTTPResponse] res
|
|
64
|
-
# @return [LLM::Object, String]
|
|
65
|
-
def parse_response(res)
|
|
66
|
-
case res["content-type"]
|
|
67
|
-
when %r{\Aapplication/json\s*} then LLM::Object.from(LLM.json.load(res.body))
|
|
68
|
-
else res.body
|
|
69
|
-
end
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
##
|
|
73
|
-
# @param [Net::HTTPRequest] req
|
|
74
|
-
# The request to set the body stream for
|
|
75
|
-
# @param [IO] io
|
|
76
|
-
# The IO object to set as the body stream
|
|
77
|
-
# @return [void]
|
|
78
|
-
def set_body_stream(req, io)
|
|
79
|
-
req.body_stream = io
|
|
80
|
-
req["transfer-encoding"] = "chunked" unless req["content-length"]
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
##
|
|
84
|
-
# Performs the request on the given HTTP connection.
|
|
85
|
-
# @param [Net::HTTP] http
|
|
86
|
-
# @param [Net::HTTPRequest] request
|
|
87
|
-
# @param [Object, nil] stream
|
|
88
|
-
# @param [Class] stream_parser
|
|
89
|
-
# @param [Proc, nil] b
|
|
90
|
-
# @return [Net::HTTPResponse]
|
|
91
|
-
def perform_request(http, request, stream, stream_parser, &b)
|
|
92
|
-
if stream
|
|
93
|
-
http.request(request) do |res|
|
|
94
|
-
if Net::HTTPSuccess === res
|
|
95
|
-
parser = stream_decoder.new(stream_parser.new(stream))
|
|
96
|
-
res.read_body(parser)
|
|
97
|
-
body = parser.body
|
|
98
|
-
res.body = (Hash === body || Array === body) ? LLM::Object.from(body) : body
|
|
99
|
-
else
|
|
100
|
-
body = +""
|
|
101
|
-
res.read_body { body << _1 }
|
|
102
|
-
res.body = body
|
|
103
|
-
end
|
|
104
|
-
ensure
|
|
105
|
-
parser&.free
|
|
106
|
-
end
|
|
107
|
-
elsif b
|
|
108
|
-
http.request(request) { (Net::HTTPSuccess === _1) ? b.call(_1) : _1 }
|
|
109
|
-
else
|
|
110
|
-
http.request(request)
|
|
111
|
-
end
|
|
112
|
-
end
|
|
113
|
-
end
|
|
114
|
-
end
|
|
115
|
-
end
|
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
class LLM::Provider
|
|
4
|
-
##
|
|
5
|
-
# Internal request interruption methods for
|
|
6
|
-
# {LLM::Provider::Transport::HTTP}.
|
|
7
|
-
#
|
|
8
|
-
# This module tracks active requests by execution owner and provides
|
|
9
|
-
# the logic used to interrupt an in-flight request by closing the
|
|
10
|
-
# active HTTP connection.
|
|
11
|
-
#
|
|
12
|
-
# @api private
|
|
13
|
-
module Transport::HTTP::Interruptible
|
|
14
|
-
INTERRUPT_ERRORS = [::IOError, ::EOFError, Errno::EBADF].freeze
|
|
15
|
-
Request = Struct.new(:http, :connection, keyword_init: true)
|
|
16
|
-
|
|
17
|
-
def interrupt_errors
|
|
18
|
-
[*INTERRUPT_ERRORS, *optional_interrupt_errors]
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
##
|
|
22
|
-
# Interrupt an active request, if any.
|
|
23
|
-
# @param [Fiber] owner
|
|
24
|
-
# The execution owner whose request should be interrupted
|
|
25
|
-
# @return [nil]
|
|
26
|
-
def interrupt!(owner)
|
|
27
|
-
req = request_for(owner) or return
|
|
28
|
-
lock { (@interrupts ||= {})[owner] = true }
|
|
29
|
-
if persistent_http?(req.http)
|
|
30
|
-
close_socket(req.connection&.http)
|
|
31
|
-
req.http.finish(req.connection)
|
|
32
|
-
elsif transient_http?(req.http)
|
|
33
|
-
close_socket(req.http)
|
|
34
|
-
req.http.finish if req.http.active?
|
|
35
|
-
end
|
|
36
|
-
owner.stop if owner.respond_to?(:stop)
|
|
37
|
-
rescue *interrupt_errors
|
|
38
|
-
nil
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
private
|
|
42
|
-
|
|
43
|
-
##
|
|
44
|
-
# Closes the active socket for a request, if present.
|
|
45
|
-
# @param [Net::HTTP, nil] http
|
|
46
|
-
# @return [nil]
|
|
47
|
-
def close_socket(http)
|
|
48
|
-
socket = http&.instance_variable_get(:@socket) or return
|
|
49
|
-
socket = socket.io if socket.respond_to?(:io)
|
|
50
|
-
socket.close
|
|
51
|
-
rescue *interrupt_errors
|
|
52
|
-
nil
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
##
|
|
56
|
-
# Returns whether the active request is using a transient HTTP client.
|
|
57
|
-
# @param [Object, nil] http
|
|
58
|
-
# @return [Boolean]
|
|
59
|
-
def transient_http?(http)
|
|
60
|
-
Net::HTTP === http
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
##
|
|
64
|
-
# Returns whether the active request is using a persistent HTTP client.
|
|
65
|
-
# @param [Object, nil] http
|
|
66
|
-
# @return [Boolean]
|
|
67
|
-
def persistent_http?(http)
|
|
68
|
-
defined?(Net::HTTP::Persistent) && Net::HTTP::Persistent === http
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
##
|
|
72
|
-
# Returns the active request for an execution owner.
|
|
73
|
-
# @param [Fiber] owner
|
|
74
|
-
# @return [Request, nil]
|
|
75
|
-
def request_for(owner)
|
|
76
|
-
lock do
|
|
77
|
-
@requests ||= {}
|
|
78
|
-
@requests[owner]
|
|
79
|
-
end
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
##
|
|
83
|
-
# Records an active request for an execution owner.
|
|
84
|
-
# @param [Request] req
|
|
85
|
-
# @param [Fiber] owner
|
|
86
|
-
# @return [Request]
|
|
87
|
-
def set_request(req, owner)
|
|
88
|
-
lock do
|
|
89
|
-
@requests ||= {}
|
|
90
|
-
@requests[owner] = req
|
|
91
|
-
end
|
|
92
|
-
end
|
|
93
|
-
|
|
94
|
-
##
|
|
95
|
-
# Clears the active request for an execution owner.
|
|
96
|
-
# @param [Fiber] owner
|
|
97
|
-
# @return [Request, nil]
|
|
98
|
-
def clear_request(owner)
|
|
99
|
-
lock { @requests&.delete(owner) }
|
|
100
|
-
end
|
|
101
|
-
|
|
102
|
-
##
|
|
103
|
-
# Returns whether an execution owner was interrupted.
|
|
104
|
-
# @param [Fiber] owner
|
|
105
|
-
# @return [Boolean, nil]
|
|
106
|
-
def interrupted?(owner)
|
|
107
|
-
lock { @interrupts&.delete(owner) }
|
|
108
|
-
end
|
|
109
|
-
|
|
110
|
-
def optional_interrupt_errors
|
|
111
|
-
defined?(::Async::Stop) ? [Async::Stop] : []
|
|
112
|
-
end
|
|
113
|
-
end
|
|
114
|
-
end
|
|
@@ -1,145 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
class LLM::Provider
|
|
4
|
-
module Transport
|
|
5
|
-
##
|
|
6
|
-
# The {LLM::Provider::Transport::HTTP LLM::Provider::Transport::HTTP}
|
|
7
|
-
# class manages HTTP connections for {LLM::Provider}. It handles
|
|
8
|
-
# transient and persistent clients, tracks active requests by owner,
|
|
9
|
-
# and interrupts in-flight requests when needed.
|
|
10
|
-
#
|
|
11
|
-
# @api private
|
|
12
|
-
class HTTP
|
|
13
|
-
require_relative "http/stream_decoder"
|
|
14
|
-
require_relative "http/interruptible"
|
|
15
|
-
|
|
16
|
-
include Interruptible
|
|
17
|
-
|
|
18
|
-
##
|
|
19
|
-
# @param [String] host
|
|
20
|
-
# @param [Integer] port
|
|
21
|
-
# @param [Integer] timeout
|
|
22
|
-
# @param [Boolean] ssl
|
|
23
|
-
# @param [Boolean] persistent
|
|
24
|
-
# @return [LLM::Provider::Transport::HTTP]
|
|
25
|
-
def initialize(host:, port:, timeout:, ssl:, persistent: false)
|
|
26
|
-
@host = host
|
|
27
|
-
@port = port
|
|
28
|
-
@timeout = timeout
|
|
29
|
-
@ssl = ssl
|
|
30
|
-
@base_uri = URI("#{ssl ? "https" : "http"}://#{host}:#{port}/")
|
|
31
|
-
@persistent_client = persistent ? persistent_client : nil
|
|
32
|
-
@monitor = Monitor.new
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
##
|
|
36
|
-
# Interrupt an active request, if any.
|
|
37
|
-
# @param [Fiber] owner
|
|
38
|
-
# @return [nil]
|
|
39
|
-
def interrupt!(owner)
|
|
40
|
-
super
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
##
|
|
44
|
-
# Returns whether an execution owner was interrupted.
|
|
45
|
-
# @param [Fiber] owner
|
|
46
|
-
# @return [Boolean, nil]
|
|
47
|
-
def interrupted?(owner)
|
|
48
|
-
super
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
##
|
|
52
|
-
# Returns the current request owner.
|
|
53
|
-
# @return [Object]
|
|
54
|
-
def request_owner
|
|
55
|
-
return Fiber.current unless defined?(::Async)
|
|
56
|
-
Async::Task.current? ? Async::Task.current : Fiber.current
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
##
|
|
60
|
-
# Configures the transport to use a persistent HTTP connection pool.
|
|
61
|
-
# @return [LLM::Provider::Transport::HTTP]
|
|
62
|
-
def persist!
|
|
63
|
-
client = persistent_client
|
|
64
|
-
lock do
|
|
65
|
-
@persistent_client = client
|
|
66
|
-
self
|
|
67
|
-
end
|
|
68
|
-
end
|
|
69
|
-
alias_method :persistent, :persist!
|
|
70
|
-
|
|
71
|
-
##
|
|
72
|
-
# @return [Boolean]
|
|
73
|
-
def persistent?
|
|
74
|
-
!@persistent_client.nil?
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
##
|
|
78
|
-
# Performs a request on the current HTTP transport.
|
|
79
|
-
# @param [Net::HTTPRequest] request
|
|
80
|
-
# @param [Fiber] owner
|
|
81
|
-
# @yieldparam [Net::HTTP] http
|
|
82
|
-
# @return [Object]
|
|
83
|
-
def request(request, owner:, &)
|
|
84
|
-
if persistent?
|
|
85
|
-
request_persistent(request, owner, &)
|
|
86
|
-
else
|
|
87
|
-
request_transient(request, owner, &)
|
|
88
|
-
end
|
|
89
|
-
ensure
|
|
90
|
-
clear_request(owner)
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
##
|
|
94
|
-
# @return [String]
|
|
95
|
-
def inspect
|
|
96
|
-
"#<#{self.class.name}:0x#{object_id.to_s(16)} @persistent=#{persistent?}>"
|
|
97
|
-
end
|
|
98
|
-
|
|
99
|
-
private
|
|
100
|
-
|
|
101
|
-
attr_reader :host, :port, :timeout, :ssl, :base_uri
|
|
102
|
-
|
|
103
|
-
def request_transient(request, owner, &)
|
|
104
|
-
http = transient_client
|
|
105
|
-
set_request(Request.new(http:), owner)
|
|
106
|
-
yield http
|
|
107
|
-
end
|
|
108
|
-
|
|
109
|
-
def request_persistent(request, owner, &)
|
|
110
|
-
persistent_client.connection_for(URI.join(base_uri, request.path)) do |connection|
|
|
111
|
-
set_request(Request.new(http: persistent_client, connection:), owner)
|
|
112
|
-
yield connection.http
|
|
113
|
-
end
|
|
114
|
-
end
|
|
115
|
-
|
|
116
|
-
def persistent_client
|
|
117
|
-
LLM.lock(:clients) do
|
|
118
|
-
if LLM.clients[client_id]
|
|
119
|
-
LLM.clients[client_id]
|
|
120
|
-
else
|
|
121
|
-
require "net/http/persistent" unless defined?(Net::HTTP::Persistent)
|
|
122
|
-
client = Net::HTTP::Persistent.new(name: self.class.name)
|
|
123
|
-
client.read_timeout = timeout
|
|
124
|
-
LLM.clients[client_id] = client
|
|
125
|
-
end
|
|
126
|
-
end
|
|
127
|
-
end
|
|
128
|
-
|
|
129
|
-
def transient_client
|
|
130
|
-
client = Net::HTTP.new(host, port)
|
|
131
|
-
client.read_timeout = timeout
|
|
132
|
-
client.use_ssl = ssl
|
|
133
|
-
client
|
|
134
|
-
end
|
|
135
|
-
|
|
136
|
-
def client_id
|
|
137
|
-
"#{host}:#{port}:#{timeout}:#{ssl}"
|
|
138
|
-
end
|
|
139
|
-
|
|
140
|
-
def lock(&)
|
|
141
|
-
@monitor.synchronize(&)
|
|
142
|
-
end
|
|
143
|
-
end
|
|
144
|
-
end
|
|
145
|
-
end
|
data/lib/llm/utils.rb
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
##
|
|
4
|
-
# @private
|
|
5
|
-
module LLM::Utils
|
|
6
|
-
def camelcase(key)
|
|
7
|
-
key.to_s
|
|
8
|
-
.split("_")
|
|
9
|
-
.map.with_index { (_2 > 0) ? _1.capitalize : _1 }
|
|
10
|
-
.join
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
def snakecase(key)
|
|
14
|
-
key
|
|
15
|
-
.split(/([A-Z])/)
|
|
16
|
-
.map { (_1.size == 1) ? "_#{_1.downcase}" : _1 }
|
|
17
|
-
.join
|
|
18
|
-
end
|
|
19
|
-
end
|