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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6ecf8ae2bc514e6cb9cb6cefb1a06476ab68d13b459446ed0c398effcf334a86
4
- data.tar.gz: 612dde127eee6f42a4aeb8f6c1fae039d74aaad8c9ed8481de22c0ec6b6cc2ce
3
+ metadata.gz: fe1e1cc4c5d064aba3ae09ac3cce887a9b116b7ebd52ccefedb6a1e8287050c2
4
+ data.tar.gz: af8a28e3643305f90f59eef3878e8ac1618ccad54b15cdafed63f33773d4d717
5
5
  SHA512:
6
- metadata.gz: fe3ee53f5b02b310397b959f307b5ddf8639ab4cedb6d747264a1b49756e65ef93bee312a609898fe03f896a27775b5adabbb30f9e278c71df9ec68a80547e81
7
- data.tar.gz: d0cba57fc257001c634474596f82b1b55517ba9f9410106b34a142df1ab8ee6db29d580016dee6db354e181741dcbad0b6be507e048dc6240193ff1cdbedba4f
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Brute
4
- VERSION = "6.1.3"
4
+ VERSION = "6.1.5"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: brute
3
3
  version: !ruby/object:Gem::Version
4
- version: 6.1.3
4
+ version: 6.1.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brute Contributors