brute 6.1.3 → 6.1.5
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/lib/brute/completion/open_router.rb +8 -2
- data/lib/brute/message_transport/open_router.rb +38 -0
- data/lib/brute/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: fe1e1cc4c5d064aba3ae09ac3cce887a9b116b7ebd52ccefedb6a1e8287050c2
|
|
4
|
+
data.tar.gz: af8a28e3643305f90f59eef3878e8ac1618ccad54b15cdafed63f33773d4d717
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 29b0f666931c9891647bf88a1d8ebf6e750c75d94782e53b73d404fd36401a70273dd0fcf9d156e34b6ac5fda952b49f851e1b3f8e29f291d544c1614680a9d6
|
|
7
|
+
data.tar.gz: 5babc3e4185d9d68ca6f86613cbe03116adb5af0483d16f04f6a5b517d4233b46f6a458df3b67b985d038852619865dc36ecf695e9b3bf379503cd7ddb6585a9
|
|
@@ -47,8 +47,10 @@ module Brute
|
|
|
47
47
|
env.emit(LLM_START_EVENT)
|
|
48
48
|
|
|
49
49
|
messages = Brute::MessageTransport::OpenRouter.dump_all(env[:messages], model: options(env).model)
|
|
50
|
+
raw = nil
|
|
50
51
|
|
|
51
52
|
::OpenRouter::Client.new(**@config).then do |client|
|
|
53
|
+
client.on(:on_error) { |error| raw = error }
|
|
52
54
|
response = nil
|
|
53
55
|
env.emit(LLM_DURATION_EVENT) { response = client.complete(messages, options(env)) }
|
|
54
56
|
|
|
@@ -76,13 +78,13 @@ module Brute
|
|
|
76
78
|
# kind of failure. The classes are looked up defensively — the provider
|
|
77
79
|
# gem is only a dependency of the app that uses this middleware.
|
|
78
80
|
rescue => error
|
|
79
|
-
env.emit(LLM_FAILURE_EVENT)
|
|
81
|
+
env.emit(LLM_FAILURE_EVENT, raw || error)
|
|
80
82
|
|
|
81
83
|
if defined?(::Faraday::Error) && error.is_a?(::Faraday::Error)
|
|
82
84
|
env.emit(FARADAY_ERROR_EVENT, error)
|
|
83
85
|
|
|
84
86
|
elsif defined?(::OpenRouter::ServerError) && error.is_a?(::OpenRouter::ServerError)
|
|
85
|
-
env.emit(OPEN_ROUTER_SERVER_ERROR_EVENT, error)
|
|
87
|
+
env.emit(OPEN_ROUTER_SERVER_ERROR_EVENT, raw || error)
|
|
86
88
|
|
|
87
89
|
else
|
|
88
90
|
env.emit(STANDARD_ERROR_EVENT, error)
|
|
@@ -147,6 +149,7 @@ describe "brute/completion/open_router" do
|
|
|
147
149
|
# Run one turn against a stubbed OpenRouter::Client and hand back the env.
|
|
148
150
|
with_fake_client = lambda do |completion, response|
|
|
149
151
|
fake_client = Object.new
|
|
152
|
+
fake_client.define_singleton_method(:on) { |_event, &_block| self }
|
|
150
153
|
fake_client.define_singleton_method(:complete) { |_messages, _options| response }
|
|
151
154
|
original = OpenRouter::Client.method(:new)
|
|
152
155
|
OpenRouter::Client.define_singleton_method(:new) { |**_config| fake_client }
|
|
@@ -180,6 +183,7 @@ describe "brute/completion/open_router" do
|
|
|
180
183
|
it "advertises env[:tools] to the provider, and lets a tools: option win" do
|
|
181
184
|
captured = []
|
|
182
185
|
fake_client = Object.new
|
|
186
|
+
fake_client.define_singleton_method(:on) { |_event, &_block| self }
|
|
183
187
|
fake_client.define_singleton_method(:complete) { |_messages, options| captured << options; FakeUsageResponse.new(nil) }
|
|
184
188
|
original = OpenRouter::Client.method(:new)
|
|
185
189
|
OpenRouter::Client.define_singleton_method(:new) { |**_config| fake_client }
|
|
@@ -226,6 +230,7 @@ describe "brute/completion/open_router" do
|
|
|
226
230
|
it "takes the model from env[:model], and lets a model: option win" do
|
|
227
231
|
captured = []
|
|
228
232
|
fake_client = Object.new
|
|
233
|
+
fake_client.define_singleton_method(:on) { |_event, &_block| self }
|
|
229
234
|
fake_client.define_singleton_method(:complete) { |_messages, options| captured << options; FakeUsageResponse.new(nil) }
|
|
230
235
|
original = OpenRouter::Client.method(:new)
|
|
231
236
|
OpenRouter::Client.define_singleton_method(:new) { |**_config| fake_client }
|
|
@@ -260,6 +265,7 @@ describe "brute/completion/open_router" do
|
|
|
260
265
|
|
|
261
266
|
boom = RuntimeError.new("no route to host")
|
|
262
267
|
fake_client = Object.new
|
|
268
|
+
fake_client.define_singleton_method(:on) { |_event, &_block| self }
|
|
263
269
|
fake_client.define_singleton_method(:complete) { |_messages, _options| raise boom }
|
|
264
270
|
original = OpenRouter::Client.method(:new)
|
|
265
271
|
OpenRouter::Client.define_singleton_method(:new) { |**_config| fake_client }
|
|
@@ -37,6 +37,10 @@ module Brute
|
|
|
37
37
|
# wire's: whatever goes out under these keys is put there here.
|
|
38
38
|
hash.delete(:reasoning)
|
|
39
39
|
|
|
40
|
+
if message.tool_call?
|
|
41
|
+
hash[:tool_calls] = message.tool_calls.map { |call| call_block(call) }
|
|
42
|
+
end
|
|
43
|
+
|
|
40
44
|
reasoning = message.reasoning
|
|
41
45
|
|
|
42
46
|
if reasoning&.detailed?
|
|
@@ -47,6 +51,16 @@ module Brute
|
|
|
47
51
|
end
|
|
48
52
|
end
|
|
49
53
|
|
|
54
|
+
# The inverse of wrap_tool_call: the wire keeps the name and the
|
|
55
|
+
# arguments under `function`, and the arguments as a JSON string.
|
|
56
|
+
def self.call_block(call)
|
|
57
|
+
{
|
|
58
|
+
id: call.id,
|
|
59
|
+
type: "function",
|
|
60
|
+
function: { name: call.name, arguments: JSON.generate(call.arguments) },
|
|
61
|
+
}
|
|
62
|
+
end
|
|
63
|
+
|
|
50
64
|
# Each type names its own payload: reasoning text carries `text` and the
|
|
51
65
|
# `signature` that verifies it, a summary carries `summary`, and an
|
|
52
66
|
# encrypted item carries `data`. Written under the wrong key the payload
|
|
@@ -170,6 +184,30 @@ describe "brute/message_transport/open_router" do
|
|
|
170
184
|
Brute::MessageTransport::OpenRouter.dump(m).should == { role: :user, content: "hi" }
|
|
171
185
|
end
|
|
172
186
|
|
|
187
|
+
it "dumps a tool call as the wire wants it, and reads its own output back" do
|
|
188
|
+
call = Brute::ToolCall.new(id: "call_1", name: "search", arguments: { "query" => "cheese" })
|
|
189
|
+
dumped = Brute::MessageTransport::OpenRouter.dump(
|
|
190
|
+
Brute::Message.new(role: :assistant, content: "", tool_calls: [call]),
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
dumped[:tool_calls].should == [
|
|
194
|
+
{ id: "call_1", type: "function", function: { name: "search", arguments: '{"query":"cheese"}' } },
|
|
195
|
+
]
|
|
196
|
+
|
|
197
|
+
echoed = Struct.new(:choices).new([{ "message" => {
|
|
198
|
+
"role" => "assistant", "content" => "", "tool_calls" => dumped[:tool_calls],
|
|
199
|
+
} }])
|
|
200
|
+
back = Brute::MessageTransport::OpenRouter.new(echoed).wrap_each.to_a.first.tool_calls.first
|
|
201
|
+
|
|
202
|
+
back.id.should == "call_1"
|
|
203
|
+
back.name.should == "search"
|
|
204
|
+
back.arguments.should == { "query" => "cheese" }
|
|
205
|
+
|
|
206
|
+
Brute::MessageTransport::OpenRouter.dump(
|
|
207
|
+
Brute::Message.new(role: :user, content: "hi"),
|
|
208
|
+
).key?(:tool_calls).should.be.false
|
|
209
|
+
end
|
|
210
|
+
|
|
173
211
|
it "wraps a response's choice messages with symbolised roles, dropping provider extras" do
|
|
174
212
|
fake_response = Struct.new(:choices).new([
|
|
175
213
|
{ "message" => { "role" => "assistant", "content" => "hi there",
|
data/lib/brute/version.rb
CHANGED