brute 3.2.1 → 3.2.2
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/message_transport/open_router.rb +77 -3
- data/lib/brute/middleware/open_router.rb +9 -4
- 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: fd242366b30cf384cb0326b8fa006f43883f1cf117d410b830288164a4d13582
|
|
4
|
+
data.tar.gz: 3d4973d9a6fa69e6fd04d464343d8d2d85fb8455236e9d03f32df17888182e1f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f0659b341410c0cb87db3cefe95e3c8c99bbf5ea9c88ba974d6b8f1adc5835919e256a577435748d652e1e229b30fb6cbaac321d297688fc9fac0812a028e369
|
|
7
|
+
data.tar.gz: 5f693c1c49b4bd74ce901e78bcd459f04c6316fe044bb871a04142b09154cb04767a1166dd64d02d2332fb9fac63572a6d13b28fbef40520c18737b1a11f98f4
|
|
@@ -3,28 +3,102 @@
|
|
|
3
3
|
require "bundler/setup"
|
|
4
4
|
require "brute"
|
|
5
5
|
require "brute/message_transport"
|
|
6
|
+
require "json"
|
|
6
7
|
|
|
7
8
|
module Brute
|
|
8
9
|
class MessageTransport
|
|
10
|
+
# MessageTransport for the open_router_enhanced gem
|
|
11
|
+
# (https://github.com/estiens/open_router_enhanced). Brute does not
|
|
12
|
+
# require it — you do:
|
|
13
|
+
#
|
|
14
|
+
# require "open_router"
|
|
9
15
|
class OpenRouter < MessageTransport
|
|
10
16
|
def self.dump(message)
|
|
11
17
|
message.to_h
|
|
12
18
|
end
|
|
13
19
|
|
|
20
|
+
# An OpenRouter::Response's messages (one per choice; in practice
|
|
21
|
+
# OpenRouter returns exactly one).
|
|
22
|
+
def messages
|
|
23
|
+
return @result.choices.map { |choice| choice["message"] || choice[:message] } if @result.respond_to?(:choices)
|
|
24
|
+
|
|
25
|
+
super
|
|
26
|
+
end
|
|
27
|
+
|
|
14
28
|
private
|
|
15
29
|
|
|
16
30
|
def wrap(message)
|
|
17
31
|
# Coerce string keys to symbol keys if necessary
|
|
18
32
|
hash = message.to_h.transform_keys(&:to_sym)
|
|
19
|
-
|
|
33
|
+
hash[:role] = hash[:role].to_sym if hash.key?(:role)
|
|
34
|
+
hash[:tool_calls] = hash[:tool_calls].map { |tc| wrap_tool_call(tc) } if hash[:tool_calls]
|
|
35
|
+
|
|
20
36
|
case hash
|
|
21
37
|
in { role: (:system | :user | :assistant | :tool) }
|
|
22
|
-
#
|
|
23
|
-
Brute::Message.
|
|
38
|
+
# Slice away provider extras (refusal, reasoning, model, ...)
|
|
39
|
+
# that Brute::Message doesn't know.
|
|
40
|
+
Brute::Message.new(**hash.slice(:role, :content, :tool_calls, :tool_call_id))
|
|
24
41
|
else
|
|
25
42
|
raise "Unrecognised message format #{message.inspect}"
|
|
26
43
|
end
|
|
27
44
|
end
|
|
45
|
+
|
|
46
|
+
# An OpenAI-wire tool call ({ id:, type:, function: { name:, arguments: JSON } })
|
|
47
|
+
# -> the flat { id:, name:, arguments: Hash } Brute::Message understands.
|
|
48
|
+
def wrap_tool_call(tool_call)
|
|
49
|
+
tc = tool_call.to_h.transform_keys(&:to_sym)
|
|
50
|
+
return tc unless tc[:function] # already flat { id:, name:, arguments: }
|
|
51
|
+
|
|
52
|
+
function = tc[:function].to_h.transform_keys(&:to_sym)
|
|
53
|
+
arguments = function[:arguments].to_s
|
|
54
|
+
|
|
55
|
+
{
|
|
56
|
+
id: tc[:id],
|
|
57
|
+
name: function[:name],
|
|
58
|
+
arguments: JSON.parse(arguments.empty? ? "{}" : arguments),
|
|
59
|
+
}
|
|
60
|
+
end
|
|
28
61
|
end
|
|
29
62
|
end
|
|
30
63
|
end
|
|
64
|
+
|
|
65
|
+
__END__
|
|
66
|
+
|
|
67
|
+
describe "brute/message_transport/open_router" do
|
|
68
|
+
require "brute/messages"
|
|
69
|
+
|
|
70
|
+
it "dumps a message to the wire format" do
|
|
71
|
+
m = Brute::Message.new(role: :user, content: "hi")
|
|
72
|
+
Brute::MessageTransport::OpenRouter.dump(m).should == { role: :user, content: "hi" }
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
it "wraps a response's choice messages with symbolised roles, dropping provider extras" do
|
|
76
|
+
fake_response = Struct.new(:choices).new([
|
|
77
|
+
{ "message" => { "role" => "assistant", "content" => "hi there",
|
|
78
|
+
"refusal" => nil, "reasoning" => nil, "model" => "openrouter/auto" } },
|
|
79
|
+
])
|
|
80
|
+
|
|
81
|
+
out = Brute::MessageTransport::OpenRouter.new(fake_response).wrap_each.to_a
|
|
82
|
+
|
|
83
|
+
out.size.should == 1
|
|
84
|
+
out.first.role.should == :assistant
|
|
85
|
+
out.first.content.should == "hi there"
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
it "unwraps OpenAI-wire tool calls and parses their JSON arguments" do
|
|
89
|
+
fake_response = Struct.new(:choices).new([
|
|
90
|
+
{ "message" => {
|
|
91
|
+
"role" => "assistant", "content" => nil,
|
|
92
|
+
"tool_calls" => [{ "id" => "tc1", "type" => "function",
|
|
93
|
+
"function" => { "name" => "shell", "arguments" => '{"command":"ls"}' } }],
|
|
94
|
+
} },
|
|
95
|
+
])
|
|
96
|
+
|
|
97
|
+
out = Brute::MessageTransport::OpenRouter.new(fake_response).wrap_each.to_a.first
|
|
98
|
+
|
|
99
|
+
out.tool_call?.should.be.true
|
|
100
|
+
out.tool_calls.first.id.should == "tc1"
|
|
101
|
+
out.tool_calls.first.name.should == "shell"
|
|
102
|
+
out.tool_calls.first.arguments.should == { "command" => "ls" }
|
|
103
|
+
end
|
|
104
|
+
end
|
|
@@ -4,20 +4,25 @@ module Brute
|
|
|
4
4
|
module Middleware
|
|
5
5
|
module OpenRouter
|
|
6
6
|
class Completion
|
|
7
|
+
# config: keyword arguments for OpenRouter::Client.new
|
|
8
|
+
# (access_token:, request_timeout:, uri_base:, extra_headers:).
|
|
9
|
+
# Defaults to OpenRouter.configuration's global settings.
|
|
10
|
+
# options: keyword arguments for OpenRouter::CompletionOptions.new
|
|
11
|
+
# (model:, temperature:, tools:, ...).
|
|
7
12
|
def initialize(app, config: {}, **options)
|
|
8
13
|
@app = app
|
|
9
|
-
@config =
|
|
10
|
-
@options = ::OpenRouter::CompletionOptions.new(options)
|
|
14
|
+
@config = config
|
|
15
|
+
@options = ::OpenRouter::CompletionOptions.new(**options)
|
|
11
16
|
end
|
|
12
17
|
|
|
13
18
|
def call(env)
|
|
14
19
|
messages = Brute::MessageTransport::OpenRouter.dump_all(env[:messages])
|
|
15
20
|
|
|
16
|
-
::OpenRouter::Client.new(
|
|
21
|
+
::OpenRouter::Client.new(**@config).then do |client|
|
|
17
22
|
client.complete(messages, @options).then do |response|
|
|
18
23
|
|
|
19
24
|
# OpenRouter in fact only returns a single message...
|
|
20
|
-
# https://github.com/estiens/open_router_enhanced/blob/main/lib/open_router/response.rb
|
|
25
|
+
# https://github.com/estiens/open_router_enhanced/blob/main/lib/open_router/response.rb
|
|
21
26
|
Brute::MessageTransport::OpenRouter.wrap_each(response) do |message|
|
|
22
27
|
env[:messages] << message
|
|
23
28
|
end
|
data/lib/brute/version.rb
CHANGED