llm.rb 11.1.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 (48) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +84 -1
  3. data/README.md +27 -4
  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 +20 -6
  8. data/lib/llm/function/array.rb +6 -0
  9. data/lib/llm/function.rb +26 -0
  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/provider.rb +1 -18
  14. data/lib/llm/providers/anthropic/files.rb +6 -6
  15. data/lib/llm/providers/anthropic/models.rb +1 -1
  16. data/lib/llm/providers/anthropic.rb +1 -1
  17. data/lib/llm/providers/bedrock/models.rb +4 -4
  18. data/lib/llm/providers/bedrock/signature.rb +3 -3
  19. data/lib/llm/providers/bedrock.rb +1 -1
  20. data/lib/llm/providers/google/files.rb +5 -5
  21. data/lib/llm/providers/google/images.rb +1 -1
  22. data/lib/llm/providers/google/models.rb +1 -1
  23. data/lib/llm/providers/google.rb +2 -2
  24. data/lib/llm/providers/ollama/models.rb +1 -1
  25. data/lib/llm/providers/ollama.rb +2 -2
  26. data/lib/llm/providers/openai/audio.rb +3 -3
  27. data/lib/llm/providers/openai/files.rb +5 -5
  28. data/lib/llm/providers/openai/images.rb +3 -3
  29. data/lib/llm/providers/openai/models.rb +1 -1
  30. data/lib/llm/providers/openai/moderations.rb +1 -1
  31. data/lib/llm/providers/openai/responses.rb +3 -3
  32. data/lib/llm/providers/openai/vector_stores.rb +11 -11
  33. data/lib/llm/providers/openai.rb +2 -2
  34. data/lib/llm/skill.rb +1 -1
  35. data/lib/llm/tool.rb +21 -0
  36. data/lib/llm/transport/curb.rb +246 -0
  37. data/lib/llm/transport/execution.rb +1 -1
  38. data/lib/llm/transport/http.rb +9 -4
  39. data/lib/llm/transport/net_http_adapter.rb +61 -0
  40. data/lib/llm/transport/persistent_http.rb +10 -5
  41. data/lib/llm/transport/request.rb +121 -0
  42. data/lib/llm/transport/response/curb.rb +112 -0
  43. data/lib/llm/transport/response.rb +1 -0
  44. data/lib/llm/transport/utils.rb +42 -17
  45. data/lib/llm/transport.rb +17 -45
  46. data/lib/llm/version.rb +1 -1
  47. data/llm.gemspec +3 -3
  48. metadata +8 -4
@@ -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
data/lib/llm/skill.rb CHANGED
@@ -159,7 +159,7 @@ module LLM
159
159
  params[:concurrency] = concurrency if concurrency
160
160
  agent = Class.new(LLM::Agent) do
161
161
  instructions(instructions)
162
- tools(inherit_tools ? ctx.params[:tools] : tools)
162
+ tools(inherit_tools ? [*ctx.params[:tools]].reject(&:skill?) : tools)
163
163
  tracer(tracer)
164
164
  end.new(ctx.llm, params)
165
165
  agent.messages.concat(messages_for(ctx))
data/lib/llm/tool.rb CHANGED
@@ -202,6 +202,13 @@ class LLM::Tool
202
202
  false
203
203
  end
204
204
 
205
+ ##
206
+ # Returns true if the tool is a skill
207
+ # @return [Boolean]
208
+ def self.skill?
209
+ false
210
+ end
211
+
205
212
  ##
206
213
  # Returns a function bound to this tool instance.
207
214
  # @return [LLM::Function]
@@ -216,6 +223,20 @@ class LLM::Tool
216
223
  self.class.mcp?
217
224
  end
218
225
 
226
+ ##
227
+ # Returns true if the tool is an A2A tool
228
+ # @return [Boolean]
229
+ def a2a?
230
+ self.class.a2a?
231
+ end
232
+
233
+ ##
234
+ # Returns true if the tool is a skill
235
+ # @return [Boolean]
236
+ def skill?
237
+ self.class.skill?
238
+ end
239
+
219
240
  ##
220
241
  # Called when an in-flight tool run is interrupted.
221
242
  # Tools can override this to implement cooperative cleanup.