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.
Files changed (67) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +120 -2
  3. data/README.md +161 -514
  4. data/lib/llm/active_record/acts_as_llm.rb +7 -8
  5. data/lib/llm/agent.rb +36 -16
  6. data/lib/llm/context.rb +30 -26
  7. data/lib/llm/contract/completion.rb +45 -0
  8. data/lib/llm/cost.rb +81 -4
  9. data/lib/llm/error.rb +1 -1
  10. data/lib/llm/function/array.rb +8 -5
  11. data/lib/llm/function/call_group.rb +39 -0
  12. data/lib/llm/function/fork/task.rb +6 -0
  13. data/lib/llm/function/ractor/task.rb +6 -0
  14. data/lib/llm/function/task.rb +10 -0
  15. data/lib/llm/function.rb +1 -0
  16. data/lib/llm/mcp/transport/http.rb +26 -46
  17. data/lib/llm/mcp/transport/stdio.rb +0 -8
  18. data/lib/llm/mcp.rb +6 -23
  19. data/lib/llm/provider.rb +23 -20
  20. data/lib/llm/providers/anthropic/error_handler.rb +6 -7
  21. data/lib/llm/providers/anthropic/files.rb +2 -2
  22. data/lib/llm/providers/anthropic/response_adapter/completion.rb +30 -0
  23. data/lib/llm/providers/anthropic.rb +1 -1
  24. data/lib/llm/providers/bedrock/error_handler.rb +8 -9
  25. data/lib/llm/providers/bedrock/models.rb +13 -13
  26. data/lib/llm/providers/bedrock/response_adapter/completion.rb +30 -0
  27. data/lib/llm/providers/bedrock.rb +1 -1
  28. data/lib/llm/providers/google/error_handler.rb +6 -7
  29. data/lib/llm/providers/google/files.rb +2 -4
  30. data/lib/llm/providers/google/images.rb +1 -1
  31. data/lib/llm/providers/google/models.rb +0 -2
  32. data/lib/llm/providers/google/response_adapter/completion.rb +30 -0
  33. data/lib/llm/providers/google.rb +1 -1
  34. data/lib/llm/providers/ollama/error_handler.rb +6 -7
  35. data/lib/llm/providers/ollama/models.rb +0 -2
  36. data/lib/llm/providers/ollama/response_adapter/completion.rb +30 -0
  37. data/lib/llm/providers/ollama.rb +1 -1
  38. data/lib/llm/providers/openai/audio.rb +3 -3
  39. data/lib/llm/providers/openai/error_handler.rb +6 -7
  40. data/lib/llm/providers/openai/files.rb +2 -2
  41. data/lib/llm/providers/openai/images.rb +3 -3
  42. data/lib/llm/providers/openai/models.rb +1 -1
  43. data/lib/llm/providers/openai/response_adapter/completion.rb +42 -0
  44. data/lib/llm/providers/openai/response_adapter/responds.rb +39 -0
  45. data/lib/llm/providers/openai/responses.rb +2 -2
  46. data/lib/llm/providers/openai/vector_stores.rb +1 -1
  47. data/lib/llm/providers/openai.rb +1 -1
  48. data/lib/llm/response.rb +10 -8
  49. data/lib/llm/sequel/plugin.rb +7 -8
  50. data/lib/llm/stream/queue.rb +15 -42
  51. data/lib/llm/stream.rb +4 -4
  52. data/lib/llm/transport/execution.rb +67 -0
  53. data/lib/llm/transport/http.rb +134 -0
  54. data/lib/llm/transport/persistent_http.rb +152 -0
  55. data/lib/llm/transport/response/http.rb +113 -0
  56. data/lib/llm/transport/response.rb +112 -0
  57. data/lib/llm/{provider/transport/http → transport}/stream_decoder.rb +8 -4
  58. data/lib/llm/transport.rb +139 -0
  59. data/lib/llm/usage.rb +14 -5
  60. data/lib/llm/version.rb +1 -1
  61. data/lib/llm.rb +2 -12
  62. data/llm.gemspec +2 -16
  63. metadata +11 -19
  64. data/lib/llm/provider/transport/http/execution.rb +0 -115
  65. data/lib/llm/provider/transport/http/interruptible.rb +0 -114
  66. data/lib/llm/provider/transport/http.rb +0 -145
  67. 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