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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +84 -1
- data/README.md +27 -4
- data/lib/llm/a2a/transport/http.rb +9 -8
- data/lib/llm/a2a.rb +14 -7
- data/lib/llm/agent.rb +6 -3
- data/lib/llm/context.rb +20 -6
- 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/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/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/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/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/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 +3 -3
- metadata +8 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: bb1ffd1e0ecb17422014ec8f75c8b729f74d0a7cef4fd4e12681ef254411b24a
|
|
4
|
+
data.tar.gz: e5a7815d52c6fa99a38dec111c6d71aef9782272b97bfbad81e8f5bee913f918
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d7c2d1dac8ef97a5be2540896828b523ce1491e43f2fd8c78e53b8fbab34432bf6dedaee066afd43d261fb8f4e2fb9f7c3d8c112de83e60ee809d6ef77f41feb
|
|
7
|
+
data.tar.gz: d5073e8c5c156739cff4c68c65c4efaf88403229841ada3cdd1adb4920dfb58c00017ae7dbd73d12ef17b6c191b0b71ab4f15fd0c0ae29991dcffb1b840381d0
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,89 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## v11.2.0
|
|
6
|
+
|
|
7
|
+
Changes since `v11.1.0`.
|
|
8
|
+
|
|
9
|
+
This release adds `LLM::Function#skill?` and `LLM::Tool#skill?` so
|
|
10
|
+
callers can inspect whether a function or tool is backed by a skill.
|
|
11
|
+
|
|
12
|
+
It introduces `LLM::Transport::Request` as a transport-agnostic request
|
|
13
|
+
object so providers no longer depend directly on `Net::HTTP` request
|
|
14
|
+
classes, and adds an optional Curb (libcurl) backend alongside symbolic
|
|
15
|
+
transport shortcuts such as `transport: :curb`.
|
|
16
|
+
|
|
17
|
+
MCP and A2A clients now accept `persistent: true` matching provider configuration.
|
|
18
|
+
Several fixes land for tool return callback emission, function comparison by
|
|
19
|
+
tool call ID, function array filtering, skill tool inheritance, and JSON generator
|
|
20
|
+
state compatibility on Ruby 4.
|
|
21
|
+
|
|
22
|
+
### Add
|
|
23
|
+
|
|
24
|
+
* **Add `LLM::Function#skill?`** <br>
|
|
25
|
+
Add `skill?` to `LLM::Function` so callers can check whether a
|
|
26
|
+
function is backed by a skill tool.
|
|
27
|
+
|
|
28
|
+
* **Add `LLM::Tool.skill?` and `LLM::Tool#skill?`** <br>
|
|
29
|
+
Add class-level `skill?` and instance-level `skill?` to
|
|
30
|
+
`LLM::Tool`, matching the existing `mcp?` and `a2a?` pattern.
|
|
31
|
+
|
|
32
|
+
* **Add `LLM::Transport::Request`** <br>
|
|
33
|
+
Add `LLM::Transport::Request` as a transport-agnostic request object
|
|
34
|
+
and update providers to build requests without depending directly on
|
|
35
|
+
Net::HTTP request classes. The built-in Net::HTTP transports still
|
|
36
|
+
accept existing Net::HTTP request objects through a compatibility
|
|
37
|
+
bridge, while alternative transports can handle the generic request
|
|
38
|
+
shape directly.
|
|
39
|
+
|
|
40
|
+
* **Add optional Curb transport support** <br>
|
|
41
|
+
Add `LLM::Transport::Curb`, an optional libcurl-backed transport
|
|
42
|
+
that can be selected with `transport: :curb`. Providers already
|
|
43
|
+
emit `LLM::Transport::Request` objects, so the Curb backend can
|
|
44
|
+
execute requests without routing through Net::HTTP.
|
|
45
|
+
|
|
46
|
+
* **Add symbolic transport shortcuts** <br>
|
|
47
|
+
Allow providers, MCP HTTP clients, and A2A HTTP clients to accept
|
|
48
|
+
transport shortcuts such as `transport: :curb` and
|
|
49
|
+
`transport: :net_http_persistent`.
|
|
50
|
+
|
|
51
|
+
* **Add persistent HTTP selection to MCP and A2A clients** <br>
|
|
52
|
+
Allow MCP and A2A HTTP clients to accept `persistent: true`, matching
|
|
53
|
+
provider configuration and selecting the persistent Net::HTTP
|
|
54
|
+
transport by default.
|
|
55
|
+
|
|
56
|
+
### Fix
|
|
57
|
+
|
|
58
|
+
* **Support JSON generation state on Ruby 4** <br>
|
|
59
|
+
Handle JSON generator state objects in the standard JSON adapter so
|
|
60
|
+
schema objects serialize correctly when Ruby 4 calls custom `to_json`
|
|
61
|
+
methods during provider request generation.
|
|
62
|
+
|
|
63
|
+
* **Emit tool return callbacks for direct context waits** <br>
|
|
64
|
+
Emit `LLM::Stream#on_tool_return` when `LLM::Context#wait` executes
|
|
65
|
+
pending tool work directly instead of draining `LLM::Stream::Queue`.
|
|
66
|
+
|
|
67
|
+
* **Emit confirmed tool return callbacks once** <br>
|
|
68
|
+
Emit `LLM::Stream#on_tool_return` for confirmed and cancelled tool
|
|
69
|
+
calls, and exclude confirmed functions from later waits so mixed
|
|
70
|
+
confirmed and unconfirmed tool batches do not execute confirmed tools
|
|
71
|
+
twice.
|
|
72
|
+
|
|
73
|
+
* **Compare functions by tool call ID** <br>
|
|
74
|
+
Add `LLM::Function#==`, `#eql?`, and `#hash` so pending function
|
|
75
|
+
collections can compare tool calls by provider-assigned ID instead of
|
|
76
|
+
object identity.
|
|
77
|
+
|
|
78
|
+
* **Preserve function array behavior after filtering** <br>
|
|
79
|
+
Preserve `LLM::Function::Array` behavior when subtracting function
|
|
80
|
+
arrays so filtered tool batches can still spawn through the normal
|
|
81
|
+
function array API.
|
|
82
|
+
|
|
83
|
+
* **Prevent skills from inheriting skill-backed tools** <br>
|
|
84
|
+
Exclude skill-backed tools when a skill sub-agent uses `tools:
|
|
85
|
+
inherit`, preventing skills loaded through a parent context from
|
|
86
|
+
being recursively exposed to nested skill agents.
|
|
87
|
+
|
|
5
88
|
## v11.1.0
|
|
6
89
|
|
|
7
90
|
Changes since `v11.0.0`.
|
|
@@ -133,7 +216,7 @@ requests outside `#session`, `LLM::Function#def` as a short alias for
|
|
|
133
216
|
|
|
134
217
|
* **Fix context and agent JSON serialization through `LLM.json`** <br>
|
|
135
218
|
Fix `LLM::Context#to_json` and `LLM::Agent#to_json` to serialize
|
|
136
|
-
through `LLM.json.dump(...)` instead of plain `to_json`.
|
|
219
|
+
through `LLM.json.dump(...)` instead of plain `to_json`.
|
|
137
220
|
|
|
138
221
|
* **Fix block-form ORM agent DSL forwarding** <br>
|
|
139
222
|
Fix block-form `model { ... }`, `tools { ... }`, and
|
data/README.md
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
<img src="https://img.shields.io/badge/License-0BSD-orange.svg?" alt="License">
|
|
12
12
|
</a>
|
|
13
13
|
<a href="https://github.com/llmrb/llm.rb/tags">
|
|
14
|
-
<img src="https://img.shields.io/badge/version-11.
|
|
14
|
+
<img src="https://img.shields.io/badge/version-11.2.0-green.svg?" alt="Version">
|
|
15
15
|
</a>
|
|
16
16
|
</p>
|
|
17
17
|
|
|
@@ -175,7 +175,9 @@ ctx.talk(ctx.wait(:call)) while ctx.functions?
|
|
|
175
175
|
The HTTP transport can be used with or without the `session` method,
|
|
176
176
|
and unlike the stdio transport it can remain efficient without the
|
|
177
177
|
`session` method through a persistent connection pool that is available
|
|
178
|
-
through the
|
|
178
|
+
through the
|
|
179
|
+
[LLM::Transport.net_http_persistent](https://0x1eef.github.io/x/llm.rb/LLM/Transport.html#method-c-net_http_persistent)
|
|
180
|
+
transport:
|
|
179
181
|
|
|
180
182
|
```ruby
|
|
181
183
|
require "llm"
|
|
@@ -183,7 +185,7 @@ require "llm"
|
|
|
183
185
|
llm = LLM.openai(key: ENV["KEY"])
|
|
184
186
|
mcp = LLM::MCP.http(
|
|
185
187
|
url: "https://remote-mcp.example.com",
|
|
186
|
-
transport:
|
|
188
|
+
transport: :net_http_persistent
|
|
187
189
|
)
|
|
188
190
|
|
|
189
191
|
ctx = LLM::Context.new(llm, tools: mcp.tools)
|
|
@@ -224,7 +226,7 @@ require "llm"
|
|
|
224
226
|
|
|
225
227
|
a2a = LLM::A2A.rest(
|
|
226
228
|
url: "https://remote-agent.example.com",
|
|
227
|
-
transport:
|
|
229
|
+
transport: :net_http_persistent
|
|
228
230
|
)
|
|
229
231
|
```
|
|
230
232
|
|
|
@@ -232,6 +234,27 @@ For more on direct messaging, task operations, push notification
|
|
|
232
234
|
configs, and JSON-RPC, see the
|
|
233
235
|
[LLM::A2A API docs](https://0x1eef.github.io/x/llm.rb/LLM/A2A.html).
|
|
234
236
|
|
|
237
|
+
#### Transports
|
|
238
|
+
|
|
239
|
+
Providers use Ruby's standard library Net::HTTP transport by default.
|
|
240
|
+
You can opt into persistent Net::HTTP connections with `persistent: true`,
|
|
241
|
+
or provide a transport shortcut when you want a different backend.
|
|
242
|
+
`transport: :curb` uses libcurl through the optional `curb` gem.
|
|
243
|
+
|
|
244
|
+
Custom transports can implement the
|
|
245
|
+
[LLM::Transport](https://0x1eef.github.io/x/llm.rb/LLM/Transport.html)
|
|
246
|
+
interface and receive transport-agnostic
|
|
247
|
+
[LLM::Transport::Request](https://0x1eef.github.io/x/llm.rb/LLM/Transport/Request.html)
|
|
248
|
+
objects from providers.
|
|
249
|
+
|
|
250
|
+
```ruby
|
|
251
|
+
require "llm"
|
|
252
|
+
|
|
253
|
+
llm = LLM.openai(key: ENV["KEY"], persistent: true)
|
|
254
|
+
llm = LLM.openai(key: ENV["KEY"], transport: :net_http_persistent)
|
|
255
|
+
llm = LLM.openai(key: ENV["KEY"], transport: :curb)
|
|
256
|
+
```
|
|
257
|
+
|
|
235
258
|
#### Skills
|
|
236
259
|
|
|
237
260
|
Skills are reusable instructions loaded from a `SKILL.md` directory. They let
|
|
@@ -17,13 +17,14 @@ class LLM::A2A
|
|
|
17
17
|
# @param [String] url The base URL of the A2A agent
|
|
18
18
|
# @param [Hash<String, String>] headers Extra HTTP headers
|
|
19
19
|
# @param [Integer, nil] timeout The timeout in seconds
|
|
20
|
-
# @param [
|
|
20
|
+
# @param [Boolean] persistent Whether to use persistent HTTP connections
|
|
21
|
+
# @param [LLM::Transport, Class, Symbol, nil] transport Override transport
|
|
21
22
|
# @param [String] protocol_version The A2A protocol version header
|
|
22
|
-
def initialize(url:, headers: {}, timeout: nil, transport: nil, protocol_version: "1.0")
|
|
23
|
+
def initialize(url:, headers: {}, timeout: nil, persistent: false, transport: nil, protocol_version: "1.0")
|
|
23
24
|
@uri = URI.parse(url)
|
|
24
25
|
@headers = headers
|
|
25
26
|
@protocol_version = protocol_version
|
|
26
|
-
@transport = resolve_transport(@uri,
|
|
27
|
+
@transport = resolve_transport(host: @uri.host, port: uri.port, ssl: @uri.scheme == "https", timeout:, persistent:, transport:)
|
|
27
28
|
end
|
|
28
29
|
|
|
29
30
|
##
|
|
@@ -31,7 +32,7 @@ class LLM::A2A
|
|
|
31
32
|
# @param [String] path The URL path
|
|
32
33
|
# @return [Hash]
|
|
33
34
|
def get(path, accept: "application/json")
|
|
34
|
-
req =
|
|
35
|
+
req = LLM::Transport::Request.get(request_path(path), headers(accept:))
|
|
35
36
|
res = transport.request(req, owner: self)
|
|
36
37
|
parse_response(res)
|
|
37
38
|
end
|
|
@@ -42,7 +43,7 @@ class LLM::A2A
|
|
|
42
43
|
# @param [Hash] body The JSON body
|
|
43
44
|
# @return [Hash]
|
|
44
45
|
def post(path, body, content_type: "application/json", accept: "application/json")
|
|
45
|
-
req =
|
|
46
|
+
req = LLM::Transport::Request.post(request_path(path), headers(content_type:, accept:))
|
|
46
47
|
req.body = LLM.json.dump(body)
|
|
47
48
|
res = transport.request(req, owner: self)
|
|
48
49
|
parse_response(res)
|
|
@@ -53,7 +54,7 @@ class LLM::A2A
|
|
|
53
54
|
# @param [String] path The URL path
|
|
54
55
|
# @return [Hash]
|
|
55
56
|
def delete(path, accept: "application/json")
|
|
56
|
-
req =
|
|
57
|
+
req = LLM::Transport::Request.delete(request_path(path), headers(accept:))
|
|
57
58
|
res = transport.request(req, owner: self)
|
|
58
59
|
parse_response(res)
|
|
59
60
|
end
|
|
@@ -66,7 +67,7 @@ class LLM::A2A
|
|
|
66
67
|
# @yieldparam [LLM::Object] event A stream event
|
|
67
68
|
# @return [void]
|
|
68
69
|
def get_stream(path, &on_event)
|
|
69
|
-
req =
|
|
70
|
+
req = LLM::Transport::Request.get(request_path(path), headers(accept: "text/event-stream"))
|
|
70
71
|
stream(req, &on_event)
|
|
71
72
|
end
|
|
72
73
|
|
|
@@ -79,7 +80,7 @@ class LLM::A2A
|
|
|
79
80
|
# @yieldparam [LLM::Object] event A stream event
|
|
80
81
|
# @return [void]
|
|
81
82
|
def post_stream(path, body, content_type: "application/json", &on_event)
|
|
82
|
-
req =
|
|
83
|
+
req = LLM::Transport::Request.post(request_path(path), headers(content_type:, accept: "text/event-stream"))
|
|
83
84
|
req.body = LLM.json.dump(body)
|
|
84
85
|
stream(req, &on_event)
|
|
85
86
|
end
|
data/lib/llm/a2a.rb
CHANGED
|
@@ -61,8 +61,10 @@ class LLM::A2A
|
|
|
61
61
|
# Extra HTTP headers to include in requests (e.g., Authorization)
|
|
62
62
|
# @param [Integer, nil] timeout
|
|
63
63
|
# The timeout in seconds for HTTP requests
|
|
64
|
-
# @param [
|
|
65
|
-
#
|
|
64
|
+
# @param [Boolean] persistent
|
|
65
|
+
# Whether to use persistent HTTP connections
|
|
66
|
+
# @param [LLM::Transport, Class, Symbol, nil] transport
|
|
67
|
+
# Optional override with any {LLM::Transport} instance, subclass, or shortcut
|
|
66
68
|
# @param [Symbol] binding
|
|
67
69
|
# The protocol binding to use. One of `:rest` or `:jsonrpc`
|
|
68
70
|
# @param [String] base_path
|
|
@@ -70,7 +72,7 @@ class LLM::A2A
|
|
|
70
72
|
# @param [String] protocol_version
|
|
71
73
|
# The expected A2A protocol version. Defaults to `"1.0"`.
|
|
72
74
|
# @return [LLM::A2A]
|
|
73
|
-
def self.http(url:, headers: {}, timeout: 30, transport: nil, binding: :rest, base_path: "", protocol_version: "1.0")
|
|
75
|
+
def self.http(url:, headers: {}, timeout: 30, persistent: false, transport: nil, binding: :rest, base_path: "", protocol_version: "1.0")
|
|
74
76
|
new(
|
|
75
77
|
binding:,
|
|
76
78
|
base_path:,
|
|
@@ -79,6 +81,7 @@ class LLM::A2A
|
|
|
79
81
|
url:,
|
|
80
82
|
headers:,
|
|
81
83
|
timeout:,
|
|
84
|
+
persistent:,
|
|
82
85
|
transport:,
|
|
83
86
|
protocol_version:
|
|
84
87
|
)
|
|
@@ -90,13 +93,15 @@ class LLM::A2A
|
|
|
90
93
|
# @param [String] url
|
|
91
94
|
# @param [Hash<String, String>] headers
|
|
92
95
|
# @param [Integer, nil] timeout
|
|
93
|
-
# @param [
|
|
96
|
+
# @param [Boolean] persistent
|
|
97
|
+
# @param [LLM::Transport, Class, Symbol, nil] transport
|
|
94
98
|
# @return [LLM::A2A]
|
|
95
|
-
def self.rest(url:, headers: {}, timeout: 30, transport: nil, base_path: "", protocol_version: "1.0")
|
|
99
|
+
def self.rest(url:, headers: {}, timeout: 30, persistent: false, transport: nil, base_path: "", protocol_version: "1.0")
|
|
96
100
|
http(
|
|
97
101
|
url:,
|
|
98
102
|
headers:,
|
|
99
103
|
timeout:,
|
|
104
|
+
persistent:,
|
|
100
105
|
transport:,
|
|
101
106
|
binding: :rest,
|
|
102
107
|
base_path:,
|
|
@@ -109,13 +114,15 @@ class LLM::A2A
|
|
|
109
114
|
# @param [String] url
|
|
110
115
|
# @param [Hash<String, String>] headers
|
|
111
116
|
# @param [Integer, nil] timeout
|
|
112
|
-
# @param [
|
|
117
|
+
# @param [Boolean] persistent
|
|
118
|
+
# @param [LLM::Transport, Class, Symbol, nil] transport
|
|
113
119
|
# @return [LLM::A2A]
|
|
114
|
-
def self.jsonrpc(url:, headers: {}, timeout: 30, transport: nil, base_path: "", protocol_version: "1.0")
|
|
120
|
+
def self.jsonrpc(url:, headers: {}, timeout: 30, persistent: false, transport: nil, base_path: "", protocol_version: "1.0")
|
|
115
121
|
http(
|
|
116
122
|
url:,
|
|
117
123
|
headers:,
|
|
118
124
|
timeout:,
|
|
125
|
+
persistent:,
|
|
119
126
|
transport:,
|
|
120
127
|
binding: :jsonrpc,
|
|
121
128
|
base_path:,
|
data/lib/llm/agent.rb
CHANGED
|
@@ -447,10 +447,13 @@ module LLM
|
|
|
447
447
|
strategy = concurrency || :call
|
|
448
448
|
return wait(strategy) unless @confirm&.any?
|
|
449
449
|
confirmables = @ctx.functions.select { @confirm.include?(_1.name.to_s) }
|
|
450
|
-
results = confirmables.map
|
|
451
|
-
|
|
450
|
+
results = confirmables.map { method(:on_tool_confirmation).call(_1, strategy) }
|
|
451
|
+
@ctx.method(:emit_tool_returns).call(confirmables, results)
|
|
452
|
+
if (@ctx.functions - confirmables).any?
|
|
453
|
+
[*results, *wait(strategy, except: confirmables)]
|
|
454
|
+
else
|
|
455
|
+
results
|
|
452
456
|
end
|
|
453
|
-
@ctx.functions? ? [*results, *wait(strategy)] : results
|
|
454
457
|
end
|
|
455
458
|
|
|
456
459
|
##
|
data/lib/llm/context.rb
CHANGED
|
@@ -303,15 +303,21 @@ module LLM
|
|
|
303
303
|
# without using this argument.
|
|
304
304
|
# Otherwise, this controls how pending functions are resolved directly.
|
|
305
305
|
# Use `:call` for sequential execution without spawning.
|
|
306
|
+
# @param [Array<LLM::Function>] except
|
|
307
|
+
# A list of functions to exclude from the wait
|
|
306
308
|
# @return [Array<LLM::Function::Return>]
|
|
307
|
-
def wait(strategy)
|
|
309
|
+
def wait(strategy, except: [])
|
|
308
310
|
if LLM::Stream === stream && !stream.queue.empty?
|
|
309
311
|
@queue = stream.queue
|
|
310
312
|
@queue.wait
|
|
311
313
|
else
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
314
|
+
tools = except.empty? ? functions : functions - except
|
|
315
|
+
guards = guarded_returns(tools:)
|
|
316
|
+
return guards if guards
|
|
317
|
+
@queue = tools.spawn(strategy)
|
|
318
|
+
returns = @queue.wait
|
|
319
|
+
emit_tool_returns(tools, returns)
|
|
320
|
+
returns
|
|
315
321
|
end
|
|
316
322
|
ensure
|
|
317
323
|
@queue = nil
|
|
@@ -516,10 +522,10 @@ module LLM
|
|
|
516
522
|
##
|
|
517
523
|
# Builds in-band guarded returns when the guard blocks tool work.
|
|
518
524
|
# @api private
|
|
519
|
-
def guarded_returns
|
|
525
|
+
def guarded_returns(tools:)
|
|
520
526
|
warning = guard&.call(self)
|
|
521
527
|
return unless warning
|
|
522
|
-
|
|
528
|
+
tools.map { guarded_return_for(_1, warning) }
|
|
523
529
|
end
|
|
524
530
|
|
|
525
531
|
##
|
|
@@ -568,6 +574,14 @@ module LLM
|
|
|
568
574
|
})
|
|
569
575
|
end
|
|
570
576
|
|
|
577
|
+
##
|
|
578
|
+
# Emits tool return callbacks for directly waited function work.
|
|
579
|
+
# @api private
|
|
580
|
+
def emit_tool_returns(tools, returns)
|
|
581
|
+
return unless LLM::Stream === stream
|
|
582
|
+
returns.each_with_index { |result, index| stream.on_tool_return(tools[index], result) }
|
|
583
|
+
end
|
|
584
|
+
|
|
571
585
|
##
|
|
572
586
|
# Closes assistant tool-call messages that do not have matching tool
|
|
573
587
|
# responses. This can happen when a turn is interrupted while a tool
|
data/lib/llm/function/array.rb
CHANGED
data/lib/llm/function.rb
CHANGED
|
@@ -120,6 +120,25 @@ class LLM::Function
|
|
|
120
120
|
@arguments = LLM::Object.from(other)
|
|
121
121
|
end
|
|
122
122
|
|
|
123
|
+
##
|
|
124
|
+
# Compares functions by tool call ID when both sides have one.
|
|
125
|
+
# @param [LLM::Function] other
|
|
126
|
+
# @return [Boolean]
|
|
127
|
+
def ==(other)
|
|
128
|
+
return true if equal?(other)
|
|
129
|
+
return false unless self.class === other
|
|
130
|
+
return false unless id && other.id
|
|
131
|
+
id == other.id
|
|
132
|
+
end
|
|
133
|
+
alias_method :eql?, :==
|
|
134
|
+
|
|
135
|
+
##
|
|
136
|
+
# Returns a hash value compatible with {#==}.
|
|
137
|
+
# @return [Integer]
|
|
138
|
+
def hash
|
|
139
|
+
id ? id.hash : object_id.hash
|
|
140
|
+
end
|
|
141
|
+
|
|
123
142
|
##
|
|
124
143
|
# Returns a tracer, or nil
|
|
125
144
|
# @return [LLM::Tracer, nil]
|
|
@@ -300,6 +319,13 @@ class LLM::Function
|
|
|
300
319
|
@cancelled
|
|
301
320
|
end
|
|
302
321
|
|
|
322
|
+
##
|
|
323
|
+
# Returns true when this function is backed by a skill tool.
|
|
324
|
+
# @return [Boolean]
|
|
325
|
+
def skill?
|
|
326
|
+
@runner.respond_to?(:skill?) and @runner.skill?
|
|
327
|
+
end
|
|
328
|
+
|
|
303
329
|
##
|
|
304
330
|
# Returns true when a function has neither been called nor cancelled
|
|
305
331
|
# @return [Boolean]
|
data/lib/llm/json_adapter.rb
CHANGED
|
@@ -35,9 +35,15 @@ module LLM
|
|
|
35
35
|
class JSONAdapter::JSON < JSONAdapter
|
|
36
36
|
##
|
|
37
37
|
# @return (see JSONAdapter#dump)
|
|
38
|
-
def self.dump(obj,
|
|
38
|
+
def self.dump(obj, state = nil, **options)
|
|
39
39
|
require "json" unless defined?(::JSON)
|
|
40
|
-
::JSON
|
|
40
|
+
if ::JSON::State === state
|
|
41
|
+
::JSON.generate(obj, state)
|
|
42
|
+
elsif state
|
|
43
|
+
::JSON.dump(obj, state, **options)
|
|
44
|
+
else
|
|
45
|
+
::JSON.dump(obj, **options)
|
|
46
|
+
end
|
|
41
47
|
end
|
|
42
48
|
|
|
43
49
|
##
|
|
@@ -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 [
|
|
20
|
-
#
|
|
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 =
|
|
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 [
|
|
59
|
-
#
|
|
60
|
-
#
|
|
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
|
-
|
|
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
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 [
|
|
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 =
|
|
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 [
|
|
87
|
+
# @param [LLM::Transport::Request] req
|
|
88
88
|
# @param [String] body
|
|
89
89
|
# @param [String] host
|
|
90
90
|
# @param [String, nil] query
|
|
91
|
-
# @return [
|
|
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)
|