llm.rb 11.0.0 → 11.2.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 (52) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +126 -1
  3. data/README.md +58 -18
  4. data/lib/llm/a2a/transport/http.rb +9 -8
  5. data/lib/llm/a2a.rb +14 -7
  6. data/lib/llm/agent.rb +6 -3
  7. data/lib/llm/context.rb +41 -6
  8. data/lib/llm/function/array.rb +6 -0
  9. data/lib/llm/function.rb +38 -4
  10. data/lib/llm/json_adapter.rb +8 -2
  11. data/lib/llm/mcp/transport/http.rb +7 -5
  12. data/lib/llm/mcp.rb +6 -7
  13. data/lib/llm/object/builder.rb +1 -0
  14. data/lib/llm/object.rb +9 -0
  15. data/lib/llm/provider.rb +1 -18
  16. data/lib/llm/providers/anthropic/files.rb +6 -6
  17. data/lib/llm/providers/anthropic/models.rb +1 -1
  18. data/lib/llm/providers/anthropic.rb +1 -1
  19. data/lib/llm/providers/bedrock/models.rb +4 -4
  20. data/lib/llm/providers/bedrock/signature.rb +3 -3
  21. data/lib/llm/providers/bedrock.rb +1 -1
  22. data/lib/llm/providers/google/files.rb +5 -5
  23. data/lib/llm/providers/google/images.rb +1 -1
  24. data/lib/llm/providers/google/models.rb +1 -1
  25. data/lib/llm/providers/google.rb +2 -2
  26. data/lib/llm/providers/ollama/models.rb +1 -1
  27. data/lib/llm/providers/ollama.rb +2 -2
  28. data/lib/llm/providers/openai/audio.rb +3 -3
  29. data/lib/llm/providers/openai/files.rb +5 -5
  30. data/lib/llm/providers/openai/images.rb +3 -3
  31. data/lib/llm/providers/openai/models.rb +1 -1
  32. data/lib/llm/providers/openai/moderations.rb +1 -1
  33. data/lib/llm/providers/openai/responses.rb +3 -3
  34. data/lib/llm/providers/openai/vector_stores.rb +11 -11
  35. data/lib/llm/providers/openai.rb +2 -2
  36. data/lib/llm/schema.rb +23 -5
  37. data/lib/llm/skill.rb +44 -14
  38. data/lib/llm/tool.rb +21 -0
  39. data/lib/llm/tracer/telemetry.rb +3 -1
  40. data/lib/llm/transport/curb.rb +246 -0
  41. data/lib/llm/transport/execution.rb +1 -1
  42. data/lib/llm/transport/http.rb +9 -4
  43. data/lib/llm/transport/net_http_adapter.rb +61 -0
  44. data/lib/llm/transport/persistent_http.rb +10 -5
  45. data/lib/llm/transport/request.rb +121 -0
  46. data/lib/llm/transport/response/curb.rb +112 -0
  47. data/lib/llm/transport/response.rb +1 -0
  48. data/lib/llm/transport/utils.rb +42 -17
  49. data/lib/llm/transport.rb +17 -45
  50. data/lib/llm/version.rb +1 -1
  51. data/llm.gemspec +3 -3
  52. metadata +8 -4
@@ -16,16 +16,18 @@ module LLM::MCP::Transport
16
16
  # Extra headers to send with requests
17
17
  # @param [Integer, nil] timeout
18
18
  # The timeout in seconds. Defaults to nil
19
- # @param [LLM::Transport, Class, nil] transport
20
- # Optional override with any {LLM::Transport} instance or subclass
19
+ # @param [Boolean] persistent
20
+ # Whether to use persistent HTTP connections
21
+ # @param [LLM::Transport, Class, Symbol, nil] transport
22
+ # Optional override with any {LLM::Transport} instance, subclass, or shortcut
21
23
  # @return [LLM::MCP::Transport::HTTP]
22
- def initialize(url:, headers: {}, timeout: nil, transport: nil)
24
+ def initialize(url:, headers: {}, timeout: nil, persistent: false, transport: nil)
23
25
  @uri = URI.parse(url)
24
26
  @headers = headers
25
- @transport = resolve_transport(uri, transport, timeout)
26
27
  @queue = []
27
28
  @monitor = Monitor.new
28
29
  @running = false
30
+ @transport = resolve_transport(host: uri.host, port: uri.port, ssl: uri.scheme == "https", timeout:, persistent:, transport:)
29
31
  end
30
32
 
31
33
  ##
@@ -62,7 +64,7 @@ module LLM::MCP::Transport
62
64
  # @return [void]
63
65
  def write(message)
64
66
  raise LLM::MCP::Error, "MCP transport is not running" unless running?
65
- req = Net::HTTP::Post.new(uri.request_uri, headers.merge("content-type" => "application/json"))
67
+ req = LLM::Transport::Request.post(uri.request_uri, headers.merge("content-type" => "application/json"))
66
68
  req.body = LLM.json.dump(message)
67
69
  res = transport.request(req, owner: self) { consume(_1) }
68
70
  res = LLM::Transport::Response.from(res)
data/lib/llm/mcp.rb CHANGED
@@ -55,9 +55,11 @@ class LLM::MCP
55
55
  # The URL for the MCP HTTP endpoint
56
56
  # @option http [Hash] :headers
57
57
  # Extra headers for requests
58
- # @option http [LLM::Transport, Class] :transport
59
- # Optional override with any {LLM::Transport} instance or subclass,
60
- # similar to {LLM::Provider}
58
+ # @option http [Boolean] :persistent
59
+ # Whether to use persistent HTTP connections
60
+ # @option http [LLM::Transport, Class, Symbol] :transport
61
+ # Optional override with any {LLM::Transport} instance, subclass, or
62
+ # shortcut, similar to {LLM::Provider}
61
63
  # @param [Integer] timeout
62
64
  # The maximum amount of time to wait when reading from an MCP process
63
65
  # @return [LLM::MCP] A new MCP instance
@@ -69,10 +71,7 @@ class LLM::MCP
69
71
  @command = Command.new(**stdio)
70
72
  @transport = Transport::Stdio.new(command:)
71
73
  elsif http
72
- persistent = http.delete(:persistent)
73
- transport = http.delete(:transport)
74
- transport ||= LLM::Transport::PersistentHTTP if persistent
75
- @transport = Transport::HTTP.new(**http, timeout:, transport:)
74
+ @transport = Transport::HTTP.new(**http, timeout:)
76
75
  else
77
76
  raise ArgumentError, "stdio or http is required"
78
77
  end
@@ -17,6 +17,7 @@ class LLM::Object
17
17
  case obj
18
18
  when self then from(obj.to_h)
19
19
  when Array then obj.map { |v| from(v) }
20
+ when String then obj
20
21
  else
21
22
  visited = {}
22
23
  obj.each { visited[_1] = visit(_2) }
data/lib/llm/object.rb CHANGED
@@ -184,6 +184,15 @@ class LLM::Object < BasicObject
184
184
  @h.slice(*args)
185
185
  end
186
186
 
187
+ ##
188
+ # @param [Hash, #to_h] other
189
+ # @return [Boolean]
190
+ def ==(other)
191
+ return false unless other.respond_to?(:to_h)
192
+ to_h == other.to_h || to_hash == other.to_h
193
+ end
194
+ alias_method :eql?, :==
195
+
187
196
  private
188
197
 
189
198
  def method_missing(m, *args, &b)
data/lib/llm/provider.rb CHANGED
@@ -35,7 +35,7 @@ class LLM::Provider
35
35
  @base_path = LLM::Utils.normalize_base_path(base_path)
36
36
  @base_uri = URI("#{ssl ? "https" : "http"}://#{host}:#{port}/")
37
37
  @headers = {"User-Agent" => "llm.rb v#{LLM::VERSION}"}
38
- @transport = resolve_transport(transport, persistent:)
38
+ @transport = LLM::Transport::Utils.resolve_transport(host:, port:, timeout:, ssl:, transport:, persistent:)
39
39
  @monitor = Monitor.new
40
40
  end
41
41
 
@@ -417,23 +417,6 @@ class LLM::Provider
417
417
  @monitor.synchronize(&)
418
418
  end
419
419
 
420
- ##
421
- # @api private
422
- def default_transport(persistent:)
423
- transport_class = persistent ? LLM::Transport::PersistentHTTP : LLM::Transport::HTTP
424
- transport_class.new(host:, port:, timeout:, ssl:)
425
- end
426
-
427
- ##
428
- # @api private
429
- def resolve_transport(transport, persistent:)
430
- return default_transport(persistent:) if transport.nil?
431
- if Class === transport && transport <= LLM::Transport
432
- return transport.new(host:, port:, timeout:, ssl:)
433
- end
434
- transport
435
- end
436
-
437
420
  ##
438
421
  # @api private
439
422
  def thread
@@ -37,7 +37,7 @@ class LLM::Anthropic
37
37
  # @return [LLM::Response]
38
38
  def all(**params)
39
39
  query = URI.encode_www_form(params)
40
- req = Net::HTTP::Get.new("/v1/files?#{query}", headers)
40
+ req = LLM::Transport::Request.get("/v1/files?#{query}", headers)
41
41
  res, span, tracer = execute(request: req, operation: "request")
42
42
  res = ResponseAdapter.adapt(res, type: :enumerable)
43
43
  tracer.on_request_finish(operation: "request", res:, span:)
@@ -56,7 +56,7 @@ class LLM::Anthropic
56
56
  # @return [LLM::Response]
57
57
  def create(file:, **params)
58
58
  multi = LLM::Multipart.new(params.merge!(file: LLM.File(file)))
59
- req = Net::HTTP::Post.new("/v1/files", headers)
59
+ req = LLM::Transport::Request.post("/v1/files", headers)
60
60
  req["content-type"] = multi.content_type
61
61
  transport.set_body_stream(req, multi.body)
62
62
  res, span, tracer = execute(request: req, operation: "request")
@@ -79,7 +79,7 @@ class LLM::Anthropic
79
79
  def get(file:, **params)
80
80
  file_id = file.respond_to?(:id) ? file.id : file
81
81
  query = URI.encode_www_form(params)
82
- req = Net::HTTP::Get.new("/v1/files/#{file_id}?#{query}", headers)
82
+ req = LLM::Transport::Request.get("/v1/files/#{file_id}?#{query}", headers)
83
83
  res, span, tracer = execute(request: req, operation: "request")
84
84
  res = ResponseAdapter.adapt(res, type: :file)
85
85
  tracer.on_request_finish(operation: "request", res:, span:)
@@ -100,7 +100,7 @@ class LLM::Anthropic
100
100
  def get_metadata(file:, **params)
101
101
  query = URI.encode_www_form(params)
102
102
  file_id = file.respond_to?(:id) ? file.id : file
103
- req = Net::HTTP::Get.new("/v1/files/#{file_id}?#{query}", headers)
103
+ req = LLM::Transport::Request.get("/v1/files/#{file_id}?#{query}", headers)
104
104
  res, span, tracer = execute(request: req, operation: "request")
105
105
  res = ResponseAdapter.adapt(res, type: :file)
106
106
  tracer.on_request_finish(operation: "request", res:, span:)
@@ -120,7 +120,7 @@ class LLM::Anthropic
120
120
  # @return [LLM::Response]
121
121
  def delete(file:)
122
122
  file_id = file.respond_to?(:id) ? file.id : file
123
- req = Net::HTTP::Delete.new("/v1/files/#{file_id}", headers)
123
+ req = LLM::Transport::Request.delete("/v1/files/#{file_id}", headers)
124
124
  res, span, tracer = execute(request: req, operation: "request")
125
125
  res = LLM::Response.new(res)
126
126
  tracer.on_request_finish(operation: "request", res:, span:)
@@ -145,7 +145,7 @@ class LLM::Anthropic
145
145
  def download(file:, **params)
146
146
  query = URI.encode_www_form(params)
147
147
  file_id = file.respond_to?(:id) ? file.id : file
148
- req = Net::HTTP::Get.new("/v1/files/#{file_id}/content?#{query}", headers)
148
+ req = LLM::Transport::Request.get("/v1/files/#{file_id}/content?#{query}", headers)
149
149
  io = StringIO.new("".b)
150
150
  res, span, tracer = execute(request: req, operation: "request") { |res| res.read_body { |chunk| io << chunk } }
151
151
  res = LLM::Response.new(res).tap { _1.define_singleton_method(:file) { io } }
@@ -39,7 +39,7 @@ class LLM::Anthropic
39
39
  # @return [LLM::Response]
40
40
  def all(**params)
41
41
  query = URI.encode_www_form(params)
42
- req = Net::HTTP::Get.new("/v1/models?#{query}", headers)
42
+ req = LLM::Transport::Request.get("/v1/models?#{query}", headers)
43
43
  res, span, tracer = execute(request: req, operation: "request")
44
44
  res = ResponseAdapter.adapt(res, type: :models)
45
45
  tracer.on_request_finish(operation: "request", res:, span:)
@@ -160,7 +160,7 @@ module LLM
160
160
  messages = build_complete_messages(prompt, params, role)
161
161
  payload = adapt(messages)
162
162
  body = LLM.json.dump(payload.merge!(params))
163
- req = Net::HTTP::Post.new("/v1/messages", headers)
163
+ req = LLM::Transport::Request.post("/v1/messages", headers)
164
164
  transport.set_body_stream(req, StringIO.new(body))
165
165
  req
166
166
  end
@@ -57,13 +57,13 @@ class LLM::Bedrock
57
57
  ##
58
58
  # @param [String] host
59
59
  # @param [Hash] params
60
- # @return [Net::HTTP::Get]
60
+ # @return [LLM::Transport::Request]
61
61
  def build_request(host, params)
62
62
  path = "/foundation-models"
63
63
  query = URI.encode_www_form(params) unless params.empty?
64
64
  path = "#{path}?#{query}" if query && !query.empty?
65
65
  body = ""
66
- req = Net::HTTP::Get.new(path, {"Content-Type" => "application/json", "Accept" => "application/json"})
66
+ req = LLM::Transport::Request.get(path, {"Content-Type" => "application/json", "Accept" => "application/json"})
67
67
  req.tap { sign!(req, body, host, query) }
68
68
  end
69
69
 
@@ -84,11 +84,11 @@ class LLM::Bedrock
84
84
  end
85
85
 
86
86
  ##
87
- # @param [Net::HTTPRequest] req
87
+ # @param [LLM::Transport::Request] req
88
88
  # @param [String] body
89
89
  # @param [String] host
90
90
  # @param [String, nil] query
91
- # @return [Net::HTTPRequest]
91
+ # @return [LLM::Transport::Request]
92
92
  def sign!(req, body, host = credentials.host, query = nil)
93
93
  creds = credentials.tap { _1.host = host }
94
94
  Signature.new(credentials: creds, method: "GET", path: "/foundation-models", query:, body:).sign!(req)
@@ -8,7 +8,7 @@ class LLM::Bedrock
8
8
  # Signs HTTP requests and headers with AWS Signature V4.
9
9
  #
10
10
  # Returns the signed headers as a Hash through #to_h, ready to merge
11
- # into a Net::HTTPRequest or other HTTP client. Everything else is
11
+ # into an {LLM::Transport::Request} or other HTTP client. Everything else is
12
12
  # private.
13
13
  #
14
14
  # Uses only Ruby's stdlib (openssl, digest) with no external deps.
@@ -89,8 +89,8 @@ class LLM::Bedrock
89
89
  end
90
90
 
91
91
  ##
92
- # @param [Net::HTTPRequest] req
93
- # @return [Net::HTTPRequest]
92
+ # @param [LLM::Transport::Request] req
93
+ # @return [LLM::Transport::Request]
94
94
  def sign!(req)
95
95
  to_h.each { |k, v| req[k] = v }
96
96
  req
@@ -217,7 +217,7 @@ module LLM
217
217
  body = LLM.json.dump(payload)
218
218
  path = stream ? "/model/#{model_id}/converse-stream" \
219
219
  : "/model/#{model_id}/converse"
220
- req = Net::HTTP::Post.new(path, headers)
220
+ req = LLM::Transport::Request.post(path, headers)
221
221
  transport.set_body_stream(req, StringIO.new(body))
222
222
  [req, messages, body]
223
223
  end
@@ -45,7 +45,7 @@ class LLM::Google
45
45
  # @return [LLM::Response]
46
46
  def all(**params)
47
47
  query = URI.encode_www_form(params.merge!(key: key))
48
- req = Net::HTTP::Get.new("/v1beta/files?#{query}", headers)
48
+ req = LLM::Transport::Request.get("/v1beta/files?#{query}", headers)
49
49
  res, span, tracer = execute(request: req, operation: "request")
50
50
  res = ResponseAdapter.adapt(res, type: :files)
51
51
  tracer.on_request_finish(operation: "request", res:, span:)
@@ -64,7 +64,7 @@ class LLM::Google
64
64
  # @return [LLM::Response]
65
65
  def create(file:, **params)
66
66
  file = LLM.File(file)
67
- req = Net::HTTP::Post.new(request_upload_url(file:), {})
67
+ req = LLM::Transport::Request.post(request_upload_url(file:), {})
68
68
  req["content-length"] = file.bytesize
69
69
  req["X-Goog-Upload-Offset"] = 0
70
70
  req["X-Goog-Upload-Command"] = "upload, finalize"
@@ -91,7 +91,7 @@ class LLM::Google
91
91
  def get(file:, **params)
92
92
  file_id = file.respond_to?(:name) ? file.name : file.to_s
93
93
  query = URI.encode_www_form(params.merge!(key: key))
94
- req = Net::HTTP::Get.new("/v1beta/#{file_id}?#{query}", headers)
94
+ req = LLM::Transport::Request.get("/v1beta/#{file_id}?#{query}", headers)
95
95
  res, span, tracer = execute(request: req, operation: "request")
96
96
  res = ResponseAdapter.adapt(res, type: :file)
97
97
  tracer.on_request_finish(operation: "request", res:, span:)
@@ -111,7 +111,7 @@ class LLM::Google
111
111
  def delete(file:, **params)
112
112
  file_id = file.respond_to?(:name) ? file.name : file.to_s
113
113
  query = URI.encode_www_form(params.merge!(key: key))
114
- req = Net::HTTP::Delete.new("/v1beta/#{file_id}?#{query}", headers)
114
+ req = LLM::Transport::Request.delete("/v1beta/#{file_id}?#{query}", headers)
115
115
  res, span, tracer = execute(request: req, operation: "request")
116
116
  res = LLM::Response.new(res)
117
117
  tracer.on_request_finish(operation: "request", res:, span:)
@@ -128,7 +128,7 @@ class LLM::Google
128
128
  private
129
129
 
130
130
  def request_upload_url(file:)
131
- req = Net::HTTP::Post.new("/upload/v1beta/files?key=#{key}", headers)
131
+ req = LLM::Transport::Request.post("/upload/v1beta/files?key=#{key}", headers)
132
132
  req["X-Goog-Upload-Protocol"] = "resumable"
133
133
  req["X-Goog-Upload-Command"] = "start"
134
134
  req["X-Goog-Upload-Header-Content-Length"] = file.bytesize
@@ -40,7 +40,7 @@ class LLM::Google
40
40
  # @raise (see LLM::Provider#request)
41
41
  # @return [LLM::Response]
42
42
  def create(prompt:, n: 1, image_size: nil, aspect_ratio: nil, person_generation: nil, model: "imagen-4.0-generate-001", **params)
43
- req = Net::HTTP::Post.new("/v1beta/models/#{model}:predict?key=#{key}", headers)
43
+ req = LLM::Transport::Request.post("/v1beta/models/#{model}:predict?key=#{key}", headers)
44
44
  body = LLM.json.dump({
45
45
  parameters: {
46
46
  sampleCount: n,
@@ -39,7 +39,7 @@ class LLM::Google
39
39
  # @return [LLM::Response]
40
40
  def all(**params)
41
41
  query = URI.encode_www_form(params.merge!(key: key))
42
- req = Net::HTTP::Get.new("/v1beta/models?#{query}", headers)
42
+ req = LLM::Transport::Request.get("/v1beta/models?#{query}", headers)
43
43
  res, span, tracer = execute(request: req, operation: "request")
44
44
  res = ResponseAdapter.adapt(res, type: :models)
45
45
  tracer.on_request_finish(operation: "request", res:, span:)
@@ -56,7 +56,7 @@ module LLM
56
56
  def embed(input, model: "gemini-embedding-001", **params)
57
57
  model = model.respond_to?(:id) ? model.id : model
58
58
  path = ["/v1beta/models/#{model}", "embedContent?key=#{@key}"].join(":")
59
- req = Net::HTTP::Post.new(path, headers)
59
+ req = LLM::Transport::Request.post(path, headers)
60
60
  req.body = LLM.json.dump({content: {parts: [{text: input}]}})
61
61
  res, span, tracer = execute(request: req, operation: "embeddings", model:)
62
62
  res = ResponseAdapter.adapt(res, type: :embedding)
@@ -205,7 +205,7 @@ module LLM
205
205
  action = stream ? "streamGenerateContent?key=#{@key}&alt=sse" : "generateContent?key=#{@key}"
206
206
  model.respond_to?(:id) ? model.id : model
207
207
  path = ["/v1beta/models/#{model}", action].join(":")
208
- req = Net::HTTP::Post.new(path, headers)
208
+ req = LLM::Transport::Request.post(path, headers)
209
209
  messages = build_complete_messages(prompt, params, role)
210
210
  body = LLM.json.dump({contents: adapt(messages)}.merge!(params))
211
211
  transport.set_body_stream(req, StringIO.new(body))
@@ -40,7 +40,7 @@ class LLM::Ollama
40
40
  # @return [LLM::Response]
41
41
  def all(**params)
42
42
  query = URI.encode_www_form(params)
43
- req = Net::HTTP::Get.new("/api/tags?#{query}", headers)
43
+ req = LLM::Transport::Request.get("/api/tags?#{query}", headers)
44
44
  res, span, tracer = execute(request: req, operation: "request")
45
45
  res = ResponseAdapter.adapt(res, type: :models)
46
46
  tracer.on_request_finish(operation: "request", res:, span:)
@@ -48,7 +48,7 @@ module LLM
48
48
  # @return [LLM::Response]
49
49
  def embed(input, model: default_model, **params)
50
50
  params = {model:}.merge!(params)
51
- req = Net::HTTP::Post.new("/v1/embeddings", headers)
51
+ req = LLM::Transport::Request.post("/v1/embeddings", headers)
52
52
  req.body = LLM.json.dump({input:}.merge!(params))
53
53
  res, span, tracer = execute(request: req, operation: "embeddings", model:)
54
54
  res = ResponseAdapter.adapt(res, type: :embedding)
@@ -129,7 +129,7 @@ module LLM
129
129
  def build_complete_request(prompt, params, role)
130
130
  messages = build_complete_messages(prompt, params, role)
131
131
  body = LLM.json.dump({messages: [adapt(messages)].flatten}.merge!(params))
132
- req = Net::HTTP::Post.new("/api/chat", headers)
132
+ req = LLM::Transport::Request.post("/api/chat", headers)
133
133
  transport.set_body_stream(req, StringIO.new(body))
134
134
  req
135
135
  end
@@ -32,7 +32,7 @@ class LLM::OpenAI
32
32
  # @raise (see LLM::Provider#request)
33
33
  # @return [LLM::Response]
34
34
  def create_speech(input:, voice: "alloy", model: "gpt-4o-mini-tts", response_format: "mp3", **params)
35
- req = Net::HTTP::Post.new(path("/audio/speech"), headers)
35
+ req = LLM::Transport::Request.post(path("/audio/speech"), headers)
36
36
  req.body = LLM.json.dump({input:, voice:, model:, response_format:}.merge!(params))
37
37
  io = StringIO.new("".b)
38
38
  res, span, tracer = execute(request: req, operation: "request") { _1.read_body { |chunk| io << chunk } }
@@ -55,7 +55,7 @@ class LLM::OpenAI
55
55
  # @return [LLM::Response]
56
56
  def create_transcription(file:, model: "whisper-1", **params)
57
57
  multi = LLM::Multipart.new(params.merge!(file: LLM.File(file), model:))
58
- req = Net::HTTP::Post.new(path("/audio/transcriptions"), headers)
58
+ req = LLM::Transport::Request.post(path("/audio/transcriptions"), headers)
59
59
  req["content-type"] = multi.content_type
60
60
  transport.set_body_stream(req, multi.body)
61
61
  res, span, tracer = execute(request: req, operation: "request")
@@ -79,7 +79,7 @@ class LLM::OpenAI
79
79
  # @return [LLM::Response]
80
80
  def create_translation(file:, model: "whisper-1", **params)
81
81
  multi = LLM::Multipart.new(params.merge!(file: LLM.File(file), model:))
82
- req = Net::HTTP::Post.new(path("/audio/translations"), headers)
82
+ req = LLM::Transport::Request.post(path("/audio/translations"), headers)
83
83
  req["content-type"] = multi.content_type
84
84
  transport.set_body_stream(req, multi.body)
85
85
  res, span, tracer = execute(request: req, operation: "request")
@@ -40,7 +40,7 @@ class LLM::OpenAI
40
40
  # @return [LLM::Response]
41
41
  def all(**params)
42
42
  query = URI.encode_www_form(params)
43
- req = Net::HTTP::Get.new(path("/files?#{query}"), headers)
43
+ req = LLM::Transport::Request.get(path("/files?#{query}"), headers)
44
44
  res, span, tracer = execute(request: req, operation: "request")
45
45
  res = ResponseAdapter.adapt(res, type: :enumerable)
46
46
  tracer.on_request_finish(operation: "request", res:, span:)
@@ -60,7 +60,7 @@ class LLM::OpenAI
60
60
  # @return [LLM::Response]
61
61
  def create(file:, purpose: "assistants", **params)
62
62
  multi = LLM::Multipart.new(params.merge!(file: LLM.File(file), purpose:))
63
- req = Net::HTTP::Post.new(path("/files"), headers)
63
+ req = LLM::Transport::Request.post(path("/files"), headers)
64
64
  req["content-type"] = multi.content_type
65
65
  transport.set_body_stream(req, multi.body)
66
66
  res, span, tracer = execute(request: req, operation: "request")
@@ -83,7 +83,7 @@ class LLM::OpenAI
83
83
  def get(file:, **params)
84
84
  file_id = file.respond_to?(:id) ? file.id : file
85
85
  query = URI.encode_www_form(params)
86
- req = Net::HTTP::Get.new(path("/files/#{file_id}?#{query}"), headers)
86
+ req = LLM::Transport::Request.get(path("/files/#{file_id}?#{query}"), headers)
87
87
  res, span, tracer = execute(request: req, operation: "request")
88
88
  res = ResponseAdapter.adapt(res, type: :file)
89
89
  tracer.on_request_finish(operation: "request", res:, span:)
@@ -105,7 +105,7 @@ class LLM::OpenAI
105
105
  def download(file:, **params)
106
106
  query = URI.encode_www_form(params)
107
107
  file_id = file.respond_to?(:id) ? file.id : file
108
- req = Net::HTTP::Get.new(path("/files/#{file_id}/content?#{query}"), headers)
108
+ req = LLM::Transport::Request.get(path("/files/#{file_id}/content?#{query}"), headers)
109
109
  io = StringIO.new("".b)
110
110
  res, span, tracer = execute(request: req, operation: "request") { |res| res.read_body { |chunk| io << chunk } }
111
111
  res = LLM::Response.new(res).tap { _1.define_singleton_method(:file) { io } }
@@ -125,7 +125,7 @@ class LLM::OpenAI
125
125
  # @return [LLM::Response]
126
126
  def delete(file:)
127
127
  file_id = file.respond_to?(:id) ? file.id : file
128
- req = Net::HTTP::Delete.new(path("/files/#{file_id}"), headers)
128
+ req = LLM::Transport::Request.delete(path("/files/#{file_id}"), headers)
129
129
  res, span, tracer = execute(request: req, operation: "request")
130
130
  res = LLM::Response.new(res)
131
131
  tracer.on_request_finish(operation: "request", res:, span:)
@@ -50,7 +50,7 @@ class LLM::OpenAI
50
50
  # @raise (see LLM::Provider#request)
51
51
  # @return [LLM::Response]
52
52
  def create(prompt:, model: "dall-e-3", response_format: "b64_json", **params)
53
- req = Net::HTTP::Post.new(path("/images/generations"), headers)
53
+ req = LLM::Transport::Request.post(path("/images/generations"), headers)
54
54
  req.body = LLM.json.dump({prompt:, n: 1, model:, response_format:}.merge!(params))
55
55
  res, span, tracer = execute(request: req, operation: "request")
56
56
  res = ResponseAdapter.adapt(res, type: :image)
@@ -76,7 +76,7 @@ class LLM::OpenAI
76
76
  def create_variation(image:, model: "dall-e-2", response_format: "b64_json", **params)
77
77
  image = LLM.File(image)
78
78
  multi = LLM::Multipart.new(params.merge!(image:, model:, response_format:))
79
- req = Net::HTTP::Post.new(path("/images/variations"), headers)
79
+ req = LLM::Transport::Request.post(path("/images/variations"), headers)
80
80
  req["content-type"] = multi.content_type
81
81
  transport.set_body_stream(req, multi.body)
82
82
  res, span, tracer = execute(request: req, operation: "request")
@@ -102,7 +102,7 @@ class LLM::OpenAI
102
102
  def edit(image:, prompt:, model: "dall-e-2", response_format: "b64_json", **params)
103
103
  image = LLM.File(image)
104
104
  multi = LLM::Multipart.new(params.merge!(image:, prompt:, model:, response_format:))
105
- req = Net::HTTP::Post.new(path("/images/edits"), headers)
105
+ req = LLM::Transport::Request.post(path("/images/edits"), headers)
106
106
  req["content-type"] = multi.content_type
107
107
  transport.set_body_stream(req, multi.body)
108
108
  res, span, tracer = execute(request: req, operation: "request")
@@ -39,7 +39,7 @@ class LLM::OpenAI
39
39
  # @return [LLM::Response]
40
40
  def all(**params)
41
41
  query = URI.encode_www_form(params)
42
- req = Net::HTTP::Get.new(path("/models?#{query}"), headers)
42
+ req = LLM::Transport::Request.get(path("/models?#{query}"), headers)
43
43
  res, span, tracer = execute(request: req, operation: "request")
44
44
  res = ResponseAdapter.adapt(res, type: :models)
45
45
  tracer.on_request_finish(operation: "request", res:, span:)
@@ -47,7 +47,7 @@ class LLM::OpenAI
47
47
  # @param [String, LLM::Model] model The model to use
48
48
  # @return [LLM::Response]
49
49
  def create(input:, model: "omni-moderation-latest", **params)
50
- req = Net::HTTP::Post.new(path("/moderations"), headers)
50
+ req = LLM::Transport::Request.post(path("/moderations"), headers)
51
51
  input = RequestAdapter::Moderation.new(input).adapt
52
52
  req.body = LLM.json.dump({input:, model:}.merge!(params))
53
53
  res, span, tracer = execute(request: req, operation: "request")
@@ -40,7 +40,7 @@ class LLM::OpenAI
40
40
  params = [params, adapt_schema(params), adapt_tools(tools)].inject({}, &:merge!).compact
41
41
  role, stream = params.delete(:role), params.delete(:stream)
42
42
  params[:stream] = true if @provider.streamable?(stream) || stream == true
43
- req = Net::HTTP::Post.new(path("/responses"), headers)
43
+ req = LLM::Transport::Request.post(path("/responses"), headers)
44
44
  messages = build_complete_messages(prompt, params, role)
45
45
  @provider.tracer.set_request_metadata(user_input: extract_user_input(messages, fallback: prompt))
46
46
  body = LLM.json.dump({input: [adapt(messages, mode: :response)].flatten}.merge!(params))
@@ -61,7 +61,7 @@ class LLM::OpenAI
61
61
  def get(response, **params)
62
62
  response_id = response.respond_to?(:id) ? response.id : response
63
63
  query = URI.encode_www_form(params)
64
- req = Net::HTTP::Get.new(path("/responses/#{response_id}?#{query}"), headers)
64
+ req = LLM::Transport::Request.get(path("/responses/#{response_id}?#{query}"), headers)
65
65
  res, span, tracer = execute(request: req, operation: "request")
66
66
  res = ResponseAdapter.adapt(res, type: :responds)
67
67
  tracer.on_request_finish(operation: "request", res:, span:)
@@ -76,7 +76,7 @@ class LLM::OpenAI
76
76
  # @return [LLM::Object] Response body
77
77
  def delete(response)
78
78
  response_id = response.respond_to?(:id) ? response.id : response
79
- req = Net::HTTP::Delete.new(path("/responses/#{response_id}"), headers)
79
+ req = LLM::Transport::Request.delete(path("/responses/#{response_id}"), headers)
80
80
  res, span, tracer = execute(request: req, operation: "request")
81
81
  res = LLM::Response.new(res)
82
82
  tracer.on_request_finish(operation: "request", res:, span:)
@@ -31,7 +31,7 @@ class LLM::OpenAI
31
31
  # @return [LLM::Response]
32
32
  def all(**params)
33
33
  query = URI.encode_www_form(params)
34
- req = Net::HTTP::Get.new(path("/vector_stores?#{query}"), headers)
34
+ req = LLM::Transport::Request.get(path("/vector_stores?#{query}"), headers)
35
35
  res, span, tracer = execute(request: req, operation: "request")
36
36
  res = ResponseAdapter.adapt(res, type: :enumerable)
37
37
  tracer.on_request_finish(operation: "request", res:, span:)
@@ -47,7 +47,7 @@ class LLM::OpenAI
47
47
  # @return [LLM::Response]
48
48
  # @see https://platform.openai.com/docs/api-reference/vector_stores/create OpenAI docs
49
49
  def create(name:, file_ids: nil, **params)
50
- req = Net::HTTP::Post.new(path("/vector_stores"), headers)
50
+ req = LLM::Transport::Request.post(path("/vector_stores"), headers)
51
51
  req.body = LLM.json.dump(params.merge({name:, file_ids:}).compact)
52
52
  res, span, tracer = execute(request: req, operation: "request")
53
53
  res = LLM::Response.new(res)
@@ -72,7 +72,7 @@ class LLM::OpenAI
72
72
  # @see https://platform.openai.com/docs/api-reference/vector_stores/retrieve OpenAI docs
73
73
  def get(vector:)
74
74
  vector_id = vector.respond_to?(:id) ? vector.id : vector
75
- req = Net::HTTP::Get.new(path("/vector_stores/#{vector_id}"), headers)
75
+ req = LLM::Transport::Request.get(path("/vector_stores/#{vector_id}"), headers)
76
76
  res, span, tracer = execute(request: req, operation: "request")
77
77
  res = LLM::Response.new(res)
78
78
  tracer.on_request_finish(operation: "request", res:, span:)
@@ -89,7 +89,7 @@ class LLM::OpenAI
89
89
  # @see https://platform.openai.com/docs/api-reference/vector_stores/modify OpenAI docs
90
90
  def modify(vector:, name: nil, **params)
91
91
  vector_id = vector.respond_to?(:id) ? vector.id : vector
92
- req = Net::HTTP::Post.new(path("/vector_stores/#{vector_id}"), headers)
92
+ req = LLM::Transport::Request.post(path("/vector_stores/#{vector_id}"), headers)
93
93
  req.body = LLM.json.dump(params.merge({name:}).compact)
94
94
  res, span, tracer = execute(request: req, operation: "request")
95
95
  res = LLM::Response.new(res)
@@ -105,7 +105,7 @@ class LLM::OpenAI
105
105
  # @see https://platform.openai.com/docs/api-reference/vector_stores/delete OpenAI docs
106
106
  def delete(vector:)
107
107
  vector_id = vector.respond_to?(:id) ? vector.id : vector
108
- req = Net::HTTP::Delete.new(path("/vector_stores/#{vector_id}"), headers)
108
+ req = LLM::Transport::Request.delete(path("/vector_stores/#{vector_id}"), headers)
109
109
  res, span, tracer = execute(request: req, operation: "request")
110
110
  res = LLM::Response.new(res)
111
111
  tracer.on_request_finish(operation: "request", res:, span:)
@@ -122,7 +122,7 @@ class LLM::OpenAI
122
122
  # @see https://platform.openai.com/docs/api-reference/vector_stores/search OpenAI docs
123
123
  def search(vector:, query:, **params)
124
124
  vector_id = vector.respond_to?(:id) ? vector.id : vector
125
- req = Net::HTTP::Post.new(path("/vector_stores/#{vector_id}/search"), headers)
125
+ req = LLM::Transport::Request.post(path("/vector_stores/#{vector_id}/search"), headers)
126
126
  req.body = LLM.json.dump(params.merge({query:}).compact)
127
127
  res, span, tracer = execute(request: req, operation: "retrieval")
128
128
  res = ResponseAdapter.adapt(res, type: :enumerable)
@@ -140,7 +140,7 @@ class LLM::OpenAI
140
140
  def all_files(vector:, **params)
141
141
  vector_id = vector.respond_to?(:id) ? vector.id : vector
142
142
  query = URI.encode_www_form(params)
143
- req = Net::HTTP::Get.new(path("/vector_stores/#{vector_id}/files?#{query}"), headers)
143
+ req = LLM::Transport::Request.get(path("/vector_stores/#{vector_id}/files?#{query}"), headers)
144
144
  res, span, tracer = execute(request: req, operation: "request")
145
145
  res = ResponseAdapter.adapt(res, type: :enumerable)
146
146
  tracer.on_request_finish(operation: "request", res:, span:)
@@ -159,7 +159,7 @@ class LLM::OpenAI
159
159
  def add_file(vector:, file:, attributes: nil, **params)
160
160
  vector_id = vector.respond_to?(:id) ? vector.id : vector
161
161
  file_id = file.respond_to?(:id) ? file.id : file
162
- req = Net::HTTP::Post.new(path("/vector_stores/#{vector_id}/files"), headers)
162
+ req = LLM::Transport::Request.post(path("/vector_stores/#{vector_id}/files"), headers)
163
163
  req.body = LLM.json.dump(params.merge({file_id:, attributes:}).compact)
164
164
  res, span, tracer = execute(request: req, operation: "request")
165
165
  res = LLM::Response.new(res)
@@ -190,7 +190,7 @@ class LLM::OpenAI
190
190
  def update_file(vector:, file:, attributes:, **params)
191
191
  vector_id = vector.respond_to?(:id) ? vector.id : vector
192
192
  file_id = file.respond_to?(:id) ? file.id : file
193
- req = Net::HTTP::Post.new(path("/vector_stores/#{vector_id}/files/#{file_id}"), headers)
193
+ req = LLM::Transport::Request.post(path("/vector_stores/#{vector_id}/files/#{file_id}"), headers)
194
194
  req.body = LLM.json.dump(params.merge({attributes:}).compact)
195
195
  res, span, tracer = execute(request: req, operation: "request")
196
196
  res = LLM::Response.new(res)
@@ -209,7 +209,7 @@ class LLM::OpenAI
209
209
  vector_id = vector.respond_to?(:id) ? vector.id : vector
210
210
  file_id = file.respond_to?(:id) ? file.id : file
211
211
  query = URI.encode_www_form(params)
212
- req = Net::HTTP::Get.new(path("/vector_stores/#{vector_id}/files/#{file_id}?#{query}"), headers)
212
+ req = LLM::Transport::Request.get(path("/vector_stores/#{vector_id}/files/#{file_id}?#{query}"), headers)
213
213
  res, span, tracer = execute(request: req, operation: "request")
214
214
  res = LLM::Response.new(res)
215
215
  tracer.on_request_finish(operation: "request", res:, span:)
@@ -226,7 +226,7 @@ class LLM::OpenAI
226
226
  def delete_file(vector:, file:)
227
227
  vector_id = vector.respond_to?(:id) ? vector.id : vector
228
228
  file_id = file.respond_to?(:id) ? file.id : file
229
- req = Net::HTTP::Delete.new(path("/vector_stores/#{vector_id}/files/#{file_id}"), headers)
229
+ req = LLM::Transport::Request.delete(path("/vector_stores/#{vector_id}/files/#{file_id}"), headers)
230
230
  res, span, tracer = execute(request: req, operation: "request")
231
231
  res = LLM::Response.new(res)
232
232
  tracer.on_request_finish(operation: "request", res:, span:)
@@ -52,7 +52,7 @@ module LLM
52
52
  # @raise (see LLM::Provider#request)
53
53
  # @return (see LLM::Provider#embed)
54
54
  def embed(input, model: "text-embedding-3-small", **params)
55
- req = Net::HTTP::Post.new(path("/embeddings"), headers)
55
+ req = LLM::Transport::Request.post(path("/embeddings"), headers)
56
56
  req.body = LLM.json.dump({input:, model:}.merge!(params))
57
57
  res, span, tracer = execute(request: req, operation: "embeddings", model:)
58
58
  res = ResponseAdapter.adapt(res, type: :embedding)
@@ -222,7 +222,7 @@ module LLM
222
222
  def build_complete_request(prompt, params, role)
223
223
  messages = build_complete_messages(prompt, params, role)
224
224
  body = LLM.json.dump({messages: adapt(messages, mode: :complete).flatten}.merge!(params))
225
- req = Net::HTTP::Post.new(completions_path, headers)
225
+ req = LLM::Transport::Request.post(completions_path, headers)
226
226
  transport.set_body_stream(req, StringIO.new(body))
227
227
  [req, messages]
228
228
  end