llm.rb 11.1.0 → 11.3.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 +141 -12
- data/README.md +104 -69
- data/lib/llm/a2a/transport/http.rb +9 -8
- data/lib/llm/a2a.rb +14 -7
- data/lib/llm/agent.rb +31 -7
- data/lib/llm/context.rb +20 -6
- data/lib/llm/error.rb +4 -0
- data/lib/llm/function/array.rb +6 -0
- data/lib/llm/function.rb +26 -0
- data/lib/llm/json_adapter.rb +8 -2
- data/lib/llm/mcp/transport/http.rb +7 -5
- data/lib/llm/mcp.rb +6 -7
- data/lib/llm/provider.rb +1 -18
- data/lib/llm/providers/anthropic/error_handler.rb +2 -0
- data/lib/llm/providers/anthropic/files.rb +6 -6
- data/lib/llm/providers/anthropic/models.rb +1 -1
- data/lib/llm/providers/anthropic.rb +1 -1
- data/lib/llm/providers/bedrock/error_handler.rb +1 -1
- data/lib/llm/providers/bedrock/models.rb +4 -4
- data/lib/llm/providers/bedrock/signature.rb +3 -3
- data/lib/llm/providers/bedrock.rb +1 -1
- data/lib/llm/providers/google/error_handler.rb +2 -0
- data/lib/llm/providers/google/files.rb +5 -5
- data/lib/llm/providers/google/images.rb +1 -1
- data/lib/llm/providers/google/models.rb +1 -1
- data/lib/llm/providers/google.rb +2 -2
- data/lib/llm/providers/ollama/error_handler.rb +2 -0
- data/lib/llm/providers/ollama/models.rb +1 -1
- data/lib/llm/providers/ollama.rb +2 -2
- data/lib/llm/providers/openai/audio.rb +3 -3
- data/lib/llm/providers/openai/error_handler.rb +2 -0
- data/lib/llm/providers/openai/files.rb +5 -5
- data/lib/llm/providers/openai/images.rb +3 -3
- data/lib/llm/providers/openai/models.rb +1 -1
- data/lib/llm/providers/openai/moderations.rb +1 -1
- data/lib/llm/providers/openai/responses.rb +3 -3
- data/lib/llm/providers/openai/vector_stores.rb +11 -11
- data/lib/llm/providers/openai.rb +2 -2
- data/lib/llm/skill.rb +1 -1
- data/lib/llm/tool.rb +21 -0
- data/lib/llm/transport/curb.rb +246 -0
- data/lib/llm/transport/execution.rb +1 -1
- data/lib/llm/transport/http.rb +9 -4
- data/lib/llm/transport/net_http_adapter.rb +61 -0
- data/lib/llm/transport/persistent_http.rb +10 -5
- data/lib/llm/transport/request.rb +121 -0
- data/lib/llm/transport/response/curb.rb +112 -0
- data/lib/llm/transport/response.rb +1 -0
- data/lib/llm/transport/utils.rb +42 -17
- data/lib/llm/transport.rb +17 -45
- data/lib/llm/version.rb +1 -1
- data/llm.gemspec +6 -5
- metadata +25 -8
|
@@ -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 =
|
|
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:)
|
data/lib/llm/providers/google.rb
CHANGED
|
@@ -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 =
|
|
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 =
|
|
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))
|
|
@@ -49,6 +49,8 @@ class LLM::Ollama
|
|
|
49
49
|
LLM::UnauthorizedError.new("Authentication error").tap { _1.response = res }
|
|
50
50
|
elsif res.rate_limited?
|
|
51
51
|
LLM::RateLimitError.new("Too many requests").tap { _1.response = res }
|
|
52
|
+
elsif res.not_found?
|
|
53
|
+
LLM::NotFoundError.new("Server response: not found (404)").tap { _1.response = res }
|
|
52
54
|
else
|
|
53
55
|
LLM::Error.new("Unexpected response").tap { _1.response = res }
|
|
54
56
|
end
|
|
@@ -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 =
|
|
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:)
|
data/lib/llm/providers/ollama.rb
CHANGED
|
@@ -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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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")
|
|
@@ -55,6 +55,8 @@ class LLM::OpenAI
|
|
|
55
55
|
LLM::UnauthorizedError.new("Authentication error").tap { _1.response = res }
|
|
56
56
|
elsif res.rate_limited?
|
|
57
57
|
LLM::RateLimitError.new("Too many requests").tap { _1.response = res }
|
|
58
|
+
elsif res.not_found?
|
|
59
|
+
LLM::NotFoundError.new("Server response: not found (404)").tap { _1.response = res }
|
|
58
60
|
else
|
|
59
61
|
error = body["error"] || {}
|
|
60
62
|
case error["type"]
|
|
@@ -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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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:)
|
data/lib/llm/providers/openai.rb
CHANGED
|
@@ -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 =
|
|
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 =
|
|
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.
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class LLM::Transport
|
|
4
|
+
##
|
|
5
|
+
# The {LLM::Transport::Curb LLM::Transport::Curb} transport is an
|
|
6
|
+
# optional adapter for libcurl via the
|
|
7
|
+
# [curb](https://github.com/taf2/curb) gem.
|
|
8
|
+
#
|
|
9
|
+
# Curb is a C extension around libcurl. It releases the GVL during
|
|
10
|
+
# I/O so other Ruby threads can run while requests are in flight. Its
|
|
11
|
+
# timeout handling is built into libcurl itself — no thread-based
|
|
12
|
+
# timeout library required. It supports HTTP/2, connection reuse, and
|
|
13
|
+
# a wider range of network protocols out of the box.
|
|
14
|
+
#
|
|
15
|
+
# Unlike the built-in Net::HTTP transports, this transport does not
|
|
16
|
+
# require any Ruby standard library HTTP client and can be used on
|
|
17
|
+
# platforms where Net::HTTP is not available or desired.
|
|
18
|
+
#
|
|
19
|
+
# @example
|
|
20
|
+
# LLM.openai(key: ENV["KEY"], transport: :curb)
|
|
21
|
+
#
|
|
22
|
+
# @api private
|
|
23
|
+
class Curb < self
|
|
24
|
+
INTERRUPT_ERRORS = [::IOError, ::EOFError, Errno::EBADF].freeze
|
|
25
|
+
ActiveRequest = Struct.new(:easy, keyword_init: true)
|
|
26
|
+
|
|
27
|
+
##
|
|
28
|
+
# @param [String] host
|
|
29
|
+
# @param [Integer] port
|
|
30
|
+
# @param [Integer] timeout
|
|
31
|
+
# @param [Boolean] ssl
|
|
32
|
+
# @return [LLM::Transport::Curb]
|
|
33
|
+
def initialize(host:, port:, timeout:, ssl:)
|
|
34
|
+
@host = host
|
|
35
|
+
@port = port
|
|
36
|
+
@timeout = timeout
|
|
37
|
+
@ssl = ssl
|
|
38
|
+
@base_uri = URI("#{ssl ? "https" : "http"}://#{host}:#{port}/")
|
|
39
|
+
@monitor = Monitor.new
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
##
|
|
43
|
+
# Returns the current request owner.
|
|
44
|
+
# @return [Object]
|
|
45
|
+
def request_owner
|
|
46
|
+
return Fiber.current unless defined?(::Async)
|
|
47
|
+
Async::Task.current? ? Async::Task.current : Fiber.current
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
##
|
|
51
|
+
# @return [Array<Class<Exception>>]
|
|
52
|
+
def interrupt_errors
|
|
53
|
+
[*INTERRUPT_ERRORS, *optional_interrupt_errors]
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
##
|
|
57
|
+
# Interrupt an active request, if any.
|
|
58
|
+
#
|
|
59
|
+
# Sets the interrupt flag so the on_body callback can raise
|
|
60
|
+
# LLM::Interrupt on the next chunk.
|
|
61
|
+
#
|
|
62
|
+
# @param [Fiber] owner
|
|
63
|
+
# @return [nil]
|
|
64
|
+
def interrupt!(owner)
|
|
65
|
+
request_for(owner) or return
|
|
66
|
+
lock { (@interrupts ||= {})[owner] = true }
|
|
67
|
+
rescue *interrupt_errors
|
|
68
|
+
nil
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
##
|
|
72
|
+
# Returns whether an execution owner was interrupted.
|
|
73
|
+
# @param [Fiber] owner
|
|
74
|
+
# @return [Boolean, nil]
|
|
75
|
+
def interrupted?(owner)
|
|
76
|
+
lock { @interrupts&.delete(owner) }
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
##
|
|
80
|
+
# Performs a request through curb and returns a transport response
|
|
81
|
+
# wrapper so the provider layer can stay transport-agnostic.
|
|
82
|
+
#
|
|
83
|
+
# @param [LLM::Transport::Request] request
|
|
84
|
+
# @param [Fiber] owner
|
|
85
|
+
# @param [LLM::Object, nil] stream
|
|
86
|
+
# @yieldparam [LLM::Transport::Response] response
|
|
87
|
+
# @return [Object]
|
|
88
|
+
def request(request, owner:, stream: nil, &b)
|
|
89
|
+
easy = build_easy(request)
|
|
90
|
+
set_request(ActiveRequest.new(easy:), owner)
|
|
91
|
+
if stream
|
|
92
|
+
perform_streaming(easy, owner, stream)
|
|
93
|
+
elsif b
|
|
94
|
+
res = perform_blocking(easy, owner)
|
|
95
|
+
if LLM::Transport::Response === res
|
|
96
|
+
res.success? ? b.call(res) : res
|
|
97
|
+
else
|
|
98
|
+
res
|
|
99
|
+
end
|
|
100
|
+
else
|
|
101
|
+
perform_blocking(easy, owner)
|
|
102
|
+
end
|
|
103
|
+
ensure
|
|
104
|
+
clear_request(owner)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
##
|
|
108
|
+
# @return [String]
|
|
109
|
+
def inspect
|
|
110
|
+
"#<#{LLM::Utils.object_id(self)}>"
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
private
|
|
114
|
+
|
|
115
|
+
attr_reader :host, :port, :timeout, :ssl, :base_uri
|
|
116
|
+
|
|
117
|
+
def build_easy(request)
|
|
118
|
+
LLM.require "curb" unless defined?(::Curl)
|
|
119
|
+
easy = ::Curl::Easy.new(request_url(request))
|
|
120
|
+
easy.timeout = timeout
|
|
121
|
+
easy.connect_timeout = timeout
|
|
122
|
+
request.headers.each { |k, v| easy.headers[k] = v }
|
|
123
|
+
easy.follow_location = true
|
|
124
|
+
easy.ssl_verify_peer = false if !ssl
|
|
125
|
+
set_body(easy, request)
|
|
126
|
+
easy
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def request_url(request)
|
|
130
|
+
path = request.path
|
|
131
|
+
return path if path.start_with?("http://", "https://")
|
|
132
|
+
scheme = ssl ? "https" : "http"
|
|
133
|
+
default_port = ssl ? 443 : 80
|
|
134
|
+
authority = port && port.to_i > 0 && port.to_i != default_port \
|
|
135
|
+
? "#{host}:#{port}" : host
|
|
136
|
+
"#{scheme}://#{authority}#{path}"
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def set_body(easy, request)
|
|
140
|
+
case request.method
|
|
141
|
+
when "POST"
|
|
142
|
+
easy.post_body = request.body if request.body
|
|
143
|
+
when "PUT"
|
|
144
|
+
easy.put_data = request.body if request.body
|
|
145
|
+
when "DELETE"
|
|
146
|
+
easy.delete = true
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def perform_blocking(easy, owner)
|
|
151
|
+
check_interrupted(owner)
|
|
152
|
+
easy.on_body { |chunk|
|
|
153
|
+
check_interrupted(owner)
|
|
154
|
+
chunk.bytesize
|
|
155
|
+
}
|
|
156
|
+
easy.perform
|
|
157
|
+
build_response(easy)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def perform_streaming(easy, owner, stream)
|
|
161
|
+
res = nil
|
|
162
|
+
raw_body = +""
|
|
163
|
+
decoder = stream.decoder.new(stream.parser.new(stream.streamer))
|
|
164
|
+
easy.on_body do |chunk|
|
|
165
|
+
raise LLM::Interrupt, "request interrupted" if interrupted?(owner)
|
|
166
|
+
if (res ||= build_response_from_headers(easy))&.success? \
|
|
167
|
+
&& res["content-type"].to_s.include?("text/event-stream")
|
|
168
|
+
decoder << chunk
|
|
169
|
+
else
|
|
170
|
+
raw_body << chunk
|
|
171
|
+
end
|
|
172
|
+
chunk.bytesize
|
|
173
|
+
end
|
|
174
|
+
easy.perform
|
|
175
|
+
res ||= build_response(easy)
|
|
176
|
+
if raw_body.empty?
|
|
177
|
+
body = decoder.body
|
|
178
|
+
res.body = (Hash === body || Array === body) \
|
|
179
|
+
? LLM::Object.from(body) : body
|
|
180
|
+
else
|
|
181
|
+
res.body = raw_body
|
|
182
|
+
end
|
|
183
|
+
res
|
|
184
|
+
ensure
|
|
185
|
+
decoder&.free
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def build_response(easy)
|
|
189
|
+
LLM::Transport::Response::Curb.new(
|
|
190
|
+
easy.response_code.to_i,
|
|
191
|
+
parse_headers(easy.header_str.to_s),
|
|
192
|
+
easy.body_str.to_s
|
|
193
|
+
)
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def build_response_from_headers(easy)
|
|
197
|
+
return nil if easy.header_str.to_s.empty?
|
|
198
|
+
LLM::Transport::Response::Curb.new(
|
|
199
|
+
easy.response_code.to_i,
|
|
200
|
+
parse_headers(easy.header_str.to_s),
|
|
201
|
+
+""
|
|
202
|
+
)
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def parse_headers(header_str)
|
|
206
|
+
headers = {}
|
|
207
|
+
header_str.each_line do |line|
|
|
208
|
+
line = line.strip
|
|
209
|
+
next if line.empty? || line.start_with?("HTTP/")
|
|
210
|
+
key, value = line.split(/: \s*/, 2)
|
|
211
|
+
headers[key.downcase] = value if key && value
|
|
212
|
+
end
|
|
213
|
+
headers
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def check_interrupted(owner)
|
|
217
|
+
raise LLM::Interrupt, "request interrupted" if interrupted?(owner)
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def request_for(owner)
|
|
221
|
+
lock do
|
|
222
|
+
@requests ||= {}
|
|
223
|
+
@requests[owner]
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def set_request(req, owner)
|
|
228
|
+
lock do
|
|
229
|
+
@requests ||= {}
|
|
230
|
+
@requests[owner] = req
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def clear_request(owner)
|
|
235
|
+
lock { @requests&.delete(owner) }
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def lock(&)
|
|
239
|
+
@monitor.synchronize(&)
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def optional_interrupt_errors
|
|
243
|
+
defined?(::Async::Stop) ? [Async::Stop] : []
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
end
|